[SYNC] feat: Add query-keys hook and e2e tests
- src/hooks/query-keys.ts - tests/e2e/registration.spec.ts - tests/e2e/subscription.spec.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
eb95d0e276
commit
f59bbfac64
220
src/hooks/query-keys.ts
Normal file
220
src/hooks/query-keys.ts
Normal file
@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Centralized Query Keys for React Query / TanStack Query
|
||||
* Provides consistent query key management across the application
|
||||
*
|
||||
* Benefits:
|
||||
* - Single source of truth for all query keys
|
||||
* - Easy invalidation and refetching
|
||||
* - Type safety for query keys
|
||||
* - Consistent naming conventions
|
||||
*/
|
||||
|
||||
// Base query key factory
|
||||
export const createQueryKey = (
|
||||
entity: string,
|
||||
params?: Record<string, any>,
|
||||
) => {
|
||||
return params ? [entity, params] : [entity];
|
||||
};
|
||||
|
||||
// Entity-specific query keys
|
||||
export const queryKeys = {
|
||||
// Auth module
|
||||
auth: {
|
||||
all: () => createQueryKey('auth'),
|
||||
session: () => createQueryKey('auth', { type: 'session' }),
|
||||
user: (id?: string) => createQueryKey('auth', { type: 'user', id }),
|
||||
permissions: () => createQueryKey('auth', { type: 'permissions' }),
|
||||
oauthProviders: () => createQueryKey('auth', { type: 'oauth' }),
|
||||
},
|
||||
|
||||
// Users module
|
||||
users: {
|
||||
all: (filters?: Record<string, any>) =>
|
||||
createQueryKey('users', filters),
|
||||
byId: (id: string) => createQueryKey('users', { id }),
|
||||
profile: () => createQueryKey('users', { type: 'profile' }),
|
||||
preferences: () => createQueryKey('users', { type: 'preferences' }),
|
||||
invitations: (status?: string) =>
|
||||
createQueryKey('users', { type: 'invitations', status }),
|
||||
},
|
||||
|
||||
// Tenants module
|
||||
tenants: {
|
||||
all: () => createQueryKey('tenants'),
|
||||
current: () => createQueryKey('tenants', { type: 'current' }),
|
||||
byId: (id: string) => createQueryKey('tenants', { id }),
|
||||
settings: (tenantId?: string) =>
|
||||
createQueryKey('tenants', { type: 'settings', tenantId }),
|
||||
usage: (tenantId?: string) =>
|
||||
createQueryKey('tenants', { type: 'usage', tenantId }),
|
||||
},
|
||||
|
||||
// Billing module
|
||||
billing: {
|
||||
subscriptions: {
|
||||
all: (status?: string) =>
|
||||
createQueryKey('billing', { type: 'subscriptions', status }),
|
||||
current: () => createQueryKey('billing', { type: 'subscription', current: true }),
|
||||
byId: (id: string) => createQueryKey('billing', { type: 'subscription', id }),
|
||||
},
|
||||
invoices: {
|
||||
all: (filters?: Record<string, any>) =>
|
||||
createQueryKey('billing', { type: 'invoices', ...filters }),
|
||||
byId: (id: string) => createQueryKey('billing', { type: 'invoice', id }),
|
||||
downloadUrl: (id: string) => createQueryKey('billing', { type: 'invoice', id, action: 'download' }),
|
||||
},
|
||||
payments: {
|
||||
all: (filters?: Record<string, any>) =>
|
||||
createQueryKey('billing', { type: 'payments', ...filters }),
|
||||
byId: (id: string) => createQueryKey('billing', { type: 'payment', id }),
|
||||
},
|
||||
plans: {
|
||||
all: () => createQueryKey('billing', { type: 'plans' }),
|
||||
byId: (id: string) => createQueryKey('billing', { type: 'plan', id }),
|
||||
features: () => createQueryKey('billing', { type: 'planFeatures' }),
|
||||
},
|
||||
},
|
||||
|
||||
// Notifications module
|
||||
notifications: {
|
||||
all: (filters?: Record<string, any>) =>
|
||||
createQueryKey('notifications', filters),
|
||||
unread: (count?: boolean) =>
|
||||
createQueryKey('notifications', { unread: true, count }),
|
||||
preferences: () => createQueryKey('notifications', { type: 'preferences' }),
|
||||
devices: () => createQueryKey('notifications', { type: 'devices' }),
|
||||
},
|
||||
|
||||
// Feature Flags module
|
||||
featureFlags: {
|
||||
all: () => createQueryKey('featureFlags'),
|
||||
forUser: (userId: string) => createQueryKey('featureFlags', { userId }),
|
||||
forTenant: (tenantId: string) => createQueryKey('featureFlags', { tenantId }),
|
||||
evaluations: (params?: Record<string, any>) =>
|
||||
createQueryKey('featureFlags', { type: 'evaluations', ...params }),
|
||||
},
|
||||
|
||||
// Audit module
|
||||
audit: {
|
||||
logs: (filters?: Record<string, any>) =>
|
||||
createQueryKey('audit', { type: 'logs', ...filters }),
|
||||
activities: (filters?: Record<string, any>) =>
|
||||
createQueryKey('audit', { type: 'activities', ...filters }),
|
||||
summary: (period?: string) =>
|
||||
createQueryKey('audit', { type: 'summary', period }),
|
||||
},
|
||||
|
||||
// AI module
|
||||
ai: {
|
||||
configs: (tenantId?: string) =>
|
||||
createQueryKey('ai', { type: 'configs', tenantId }),
|
||||
usage: (filters?: Record<string, any>) =>
|
||||
createQueryKey('ai', { type: 'usage', ...filters }),
|
||||
models: () => createQueryKey('ai', { type: 'models' }),
|
||||
},
|
||||
|
||||
// Storage module
|
||||
storage: {
|
||||
files: (filters?: Record<string, any>) =>
|
||||
createQueryKey('storage', { type: 'files', ...filters }),
|
||||
usage: () => createQueryKey('storage', { type: 'usage' }),
|
||||
uploadUrl: (filename: string) =>
|
||||
createQueryKey('storage', { type: 'uploadUrl', filename }),
|
||||
},
|
||||
|
||||
// Webhooks module
|
||||
webhooks: {
|
||||
all: () => createQueryKey('webhooks'),
|
||||
byId: (id: string) => createQueryKey('webhooks', { id }),
|
||||
deliveries: (webhookId: string, filters?: Record<string, any>) =>
|
||||
createQueryKey('webhooks', { type: 'deliveries', webhookId, ...filters }),
|
||||
logs: (webhookId: string) =>
|
||||
createQueryKey('webhooks', { type: 'logs', webhookId }),
|
||||
},
|
||||
|
||||
// RBAC module
|
||||
rbac: {
|
||||
roles: {
|
||||
all: () => createQueryKey('rbac', { type: 'roles' }),
|
||||
byId: (id: string) => createQueryKey('rbac', { type: 'role', id }),
|
||||
permissions: (roleId: string) =>
|
||||
createQueryKey('rbac', { type: 'permissions', roleId }),
|
||||
},
|
||||
permissions: {
|
||||
all: () => createQueryKey('rbac', { type: 'permissions' }),
|
||||
byRole: (roleId: string) => createQueryKey('rbac', { type: 'permissions', roleId }),
|
||||
},
|
||||
userRoles: (userId?: string) =>
|
||||
createQueryKey('rbac', { type: 'userRoles', userId }),
|
||||
},
|
||||
|
||||
// WhatsApp module
|
||||
whatsapp: {
|
||||
configs: (tenantId?: string) =>
|
||||
createQueryKey('whatsapp', { type: 'configs', tenantId }),
|
||||
messages: (filters?: Record<string, any>) =>
|
||||
createQueryKey('whatsapp', { type: 'messages', ...filters }),
|
||||
templates: () => createQueryKey('whatsapp', { type: 'templates' }),
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Type helpers for query keys
|
||||
export type QueryKey = typeof queryKeys[keyof typeof queryKeys];
|
||||
|
||||
// Helper functions for common operations
|
||||
export const queryKeyHelpers = {
|
||||
/**
|
||||
* Get all query keys for a module
|
||||
*/
|
||||
getModuleKeys: (module: keyof typeof queryKeys) => {
|
||||
return Object.values(queryKeys[module]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a query key matches a pattern
|
||||
*/
|
||||
matches: (queryKey: any[], pattern: any[]): boolean => {
|
||||
return JSON.stringify(queryKey.slice(0, pattern.length)) ===
|
||||
JSON.stringify(pattern);
|
||||
},
|
||||
|
||||
/**
|
||||
* Extract parameters from a query key
|
||||
*/
|
||||
extractParams: (queryKey: any[]): Record<string, any> => {
|
||||
const params: Record<string, any> = {};
|
||||
|
||||
if (queryKey.length > 1 && typeof queryKey[1] === 'object') {
|
||||
Object.assign(params, queryKey[1]);
|
||||
}
|
||||
|
||||
return params;
|
||||
},
|
||||
};
|
||||
|
||||
// Export commonly used combinations
|
||||
export const commonQueryKeys = {
|
||||
// User-specific data
|
||||
userData: (userId: string) => [
|
||||
...queryKeys.auth.user(userId),
|
||||
...queryKeys.users.byId(userId),
|
||||
...queryKeys.users.preferences(),
|
||||
],
|
||||
|
||||
// Tenant-specific data
|
||||
tenantData: (tenantId: string) => [
|
||||
...queryKeys.tenants.byId(tenantId),
|
||||
...queryKeys.tenants.settings(tenantId),
|
||||
...queryKeys.billing.subscriptions.current(),
|
||||
...queryKeys.featureFlags.forTenant(tenantId),
|
||||
],
|
||||
|
||||
// Dashboard data
|
||||
dashboard: () => [
|
||||
...queryKeys.notifications.unread(true),
|
||||
...queryKeys.audit.summary('7d'),
|
||||
...queryKeys.billing.invoices.all({ limit: 5 }),
|
||||
...queryKeys.storage.usage(),
|
||||
],
|
||||
};
|
||||
200
tests/e2e/registration.spec.ts
Normal file
200
tests/e2e/registration.spec.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
test.describe('Registration Flow', () => {
|
||||
let page: Page;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page = page;
|
||||
});
|
||||
|
||||
test('should display registration page', async () => {
|
||||
await page.goto('/register');
|
||||
|
||||
// Check page title
|
||||
await expect(page).toHaveTitle(/Register/);
|
||||
|
||||
// Check form elements
|
||||
await expect(page.locator('input[name="email"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="password"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="confirmPassword"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="firstName"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="lastName"]')).toBeVisible();
|
||||
await expect(page.locator('button[type="submit"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show validation errors for invalid data', async () => {
|
||||
await page.goto('/register');
|
||||
|
||||
// Submit empty form
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Check for validation errors
|
||||
await expect(page.locator('text=Email is required')).toBeVisible();
|
||||
await expect(page.locator('text=Password is required')).toBeVisible();
|
||||
await expect(page.locator('text=First name is required')).toBeVisible();
|
||||
await page.locator('text=Last name is required')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should validate email format', async () => {
|
||||
await page.goto('/register');
|
||||
|
||||
// Enter invalid email
|
||||
await page.locator('input[name="email"]').fill('invalid-email');
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
await expect(page.locator('text=Please enter a valid email address')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should validate password matching', async () => {
|
||||
await page.goto('/register');
|
||||
|
||||
// Enter mismatched passwords
|
||||
await page.locator('input[name="password"]').fill('password123');
|
||||
await page.locator('input[name="confirmPassword"]').fill('password456');
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
await expect(page.locator('text=Passwords do not match')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should register new user successfully', async () => {
|
||||
await page.goto('/register');
|
||||
|
||||
// Fill form with valid data
|
||||
const userData = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: `john.doe.${Date.now()}@example.com`,
|
||||
password: 'Password123!',
|
||||
confirmPassword: 'Password123!',
|
||||
};
|
||||
|
||||
await page.locator('input[name="firstName"]').fill(userData.firstName);
|
||||
await page.locator('input[name="lastName"]').fill(userData.lastName);
|
||||
await page.locator('input[name="email"]').fill(userData.email);
|
||||
await page.locator('input[name="password"]').fill(userData.password);
|
||||
await page.locator('input[name="confirmPassword"]').fill(userData.confirmPassword);
|
||||
|
||||
// Submit form
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Should redirect to email verification page
|
||||
await expect(page).toHaveURL(/verify-email/);
|
||||
|
||||
// Should show success message
|
||||
await expect(page.locator('text=Registration successful')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle email verification', async () => {
|
||||
// Navigate to verification page with token
|
||||
const verificationToken = 'test-verification-token';
|
||||
await page.goto(`/verify-email?token=${verificationToken}`);
|
||||
|
||||
// Check verification form
|
||||
await expect(page.locator('input[name="token"]')).toHaveValue(verificationToken);
|
||||
await expect(page.locator('button[type="submit"]')).toBeVisible();
|
||||
|
||||
// Submit verification
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Should redirect to dashboard
|
||||
await expect(page).toHaveURL(/dashboard/);
|
||||
|
||||
// Should show welcome message
|
||||
await expect(page.locator('text=Welcome to your account')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle expired verification token', async () => {
|
||||
const expiredToken = 'expired-token-123';
|
||||
await page.goto(`/verify-email?token=${expiredToken}`);
|
||||
|
||||
// Should show error message
|
||||
await expect(page.locator('text=Verification link has expired')).toBeVisible();
|
||||
|
||||
// Should show resend option
|
||||
await expect(page.locator('button:has-text("Resend verification")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should resend verification email', async () => {
|
||||
await page.goto('/verify-email?token=expired');
|
||||
|
||||
// Click resend button
|
||||
await page.locator('button:has-text("Resend verification")').click();
|
||||
|
||||
// Should show success message
|
||||
await expect(page.locator('text=Verification email sent')).toBeVisible();
|
||||
|
||||
// Check that original email field is pre-filled
|
||||
await expect(page.locator('input[name="email"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should prevent registration with existing email', async () => {
|
||||
await page.goto('/register');
|
||||
|
||||
// Try to register with existing email
|
||||
await page.locator('input[name="firstName"]').fill('Jane');
|
||||
await page.locator('input[name="lastName"]').fill('Smith');
|
||||
await page.locator('input[name="email"]').fill('existing@example.com');
|
||||
await page.locator('input[name="password"]').fill('Password123!');
|
||||
await page.locator('input[name="confirmPassword"]').fill('Password123!');
|
||||
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Should show error message
|
||||
await expect(page.locator('text=An account with this email already exists')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle registration with weak password', async () => {
|
||||
await page.goto('/register');
|
||||
|
||||
// Try to register with weak password
|
||||
await page.locator('input[name="firstName"]').fill('Test');
|
||||
await page.locator('input[name="lastName"]').fill('User');
|
||||
await page.locator('input[name="email"]').fill(`test.${Date.now()}@example.com`);
|
||||
await page.locator('input[name="password"]').fill('123');
|
||||
await page.locator('input[name="confirmPassword"]').fill('123');
|
||||
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Should show password requirements
|
||||
await expect(page.locator('text=Password must be at least 8 characters')).toBeVisible();
|
||||
await expect(page.locator('text=Password must contain at least one uppercase letter')).toBeVisible();
|
||||
await expect(page.locator('text=Password must contain at least one number')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle network errors gracefully', async () => {
|
||||
// Mock network error
|
||||
await page.route('/api/auth/register', route => route.abort('failed'));
|
||||
|
||||
await page.goto('/register');
|
||||
|
||||
// Fill form
|
||||
await page.locator('input[name="firstName"]').fill('Test');
|
||||
await page.locator('input[name="lastName"]').fill('User');
|
||||
await page.locator('input[name="email"]').fill(`test.${Date.now()}@example.com`);
|
||||
await page.locator('input[name="password"]').fill('Password123!');
|
||||
await page.locator('input[name="confirmPassword"]').fill('Password123!');
|
||||
|
||||
// Submit form
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Should show network error message
|
||||
await expect(page.locator('text=Network error occurred. Please try again.')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be accessible', async () => {
|
||||
await page.goto('/register');
|
||||
|
||||
// Check accessibility
|
||||
const accessibilityScan = await page.accessibility.snapshot();
|
||||
expect(accessibilityScan).toHaveNoViolations();
|
||||
|
||||
// Check keyboard navigation
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.locator('input[name="email"]')).toBeFocused();
|
||||
|
||||
// Check form labels
|
||||
await expect(page.locator('label:has-text("Email Address")')).toBeVisible();
|
||||
await expect(page.locator('label:has-text("Password")')).toBeVisible();
|
||||
});
|
||||
});
|
||||
348
tests/e2e/subscription.spec.ts
Normal file
348
tests/e2e/subscription.spec.ts
Normal file
@ -0,0 +1,348 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
test.describe('Subscription Flow', () => {
|
||||
let page: Page;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page = page;
|
||||
});
|
||||
|
||||
test('should display pricing page', async () => {
|
||||
await page.goto('/pricing');
|
||||
|
||||
// Check page title
|
||||
await expect(page).toHaveTitle(/Pricing/);
|
||||
|
||||
// Check pricing plans
|
||||
await expect(page.locator('[data-testid="pricing-plans"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="plan-basic"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="plan-pro"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="plan-enterprise"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show plan details', async () => {
|
||||
await page.goto('/pricing');
|
||||
|
||||
// Click on Pro plan
|
||||
await page.locator('[data-testid="plan-pro"]').click();
|
||||
|
||||
// Check modal opens with details
|
||||
await expect(page.locator('[data-testid="plan-modal"]')).toBeVisible();
|
||||
await expect(page.locator('text=Pro Plan Features')).toBeVisible();
|
||||
|
||||
// Check features list
|
||||
await expect(page.locator('[data-testid="feature-list"]')).toBeVisible();
|
||||
await expect(page.locator('text=Unlimited users')).toBeVisible();
|
||||
await expect(page.locator('text=100GB storage')).toBeVisible();
|
||||
await expect(page.locator('text=Priority support')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should select plan and proceed to checkout', async () => {
|
||||
await page.goto('/pricing');
|
||||
|
||||
// Select Pro plan
|
||||
await page.locator('[data-testid="plan-pro"] [data-testid="select-plan"]').click();
|
||||
|
||||
// Should redirect to checkout
|
||||
await expect(page).toHaveURL(/checkout/);
|
||||
|
||||
// Check checkout page elements
|
||||
await expect(page.locator('[data-testid="checkout-form"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="plan-summary"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="payment-method"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should fill payment form', async () => {
|
||||
// Mock user is logged in
|
||||
await page.goto('/checkout?plan=pro');
|
||||
|
||||
// Fill card details
|
||||
await page.locator('[data-testid="card-number"]').fill('4242424242424242');
|
||||
await page.locator('[data-testid="card-expiry"]').fill('12/25');
|
||||
await page.locator('[data-testid="card-cvc"]').fill('123');
|
||||
await page.locator('[data-testid="card-name"]').fill('John Doe');
|
||||
|
||||
// Fill billing address
|
||||
await page.locator('[data-testid="billing-address"]').fill('123 Main St');
|
||||
await page.locator('[data-testid="billing-city"]').fill('New York');
|
||||
await page.locator('[data-testid="billing-zip"]').fill('10001');
|
||||
await page.locator('[data-testid="billing-country"]').selectOption('US');
|
||||
|
||||
// Accept terms
|
||||
await page.locator('[data-testid="accept-terms"]').check();
|
||||
|
||||
// Enable auto-renew
|
||||
await page.locator('[data-testid="auto-renew"]').check();
|
||||
});
|
||||
|
||||
test('should validate payment form', async () => {
|
||||
await page.goto('/checkout?plan=pro');
|
||||
|
||||
// Submit empty form
|
||||
await page.locator('[data-testid="submit-payment"]').click();
|
||||
|
||||
// Check validation errors
|
||||
await expect(page.locator('text=Card number is required')).toBeVisible();
|
||||
await expect(page.locator('text=Expiry date is required')).toBeVisible();
|
||||
await expect(page.locator('text=CVC is required')).toBeVisible();
|
||||
await expect(page.locator('text=Name on card is required')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should validate card number format', async () => {
|
||||
await page.goto('/checkout?plan=pro');
|
||||
|
||||
// Enter invalid card number
|
||||
await page.locator('[data-testid="card-number"]').fill('123456');
|
||||
await page.locator('[data-testid="submit-payment"]').click();
|
||||
|
||||
await expect(page.locator('text=Please enter a valid card number')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should process subscription successfully', async () => {
|
||||
// Mock Stripe success response
|
||||
await page.route('/api/billing/subscribe', route => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
subscription: {
|
||||
id: 'sub_123',
|
||||
status: 'active',
|
||||
plan: 'pro',
|
||||
current_period_end: '2026-02-20',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/checkout?plan=pro');
|
||||
|
||||
// Fill valid payment details
|
||||
await page.locator('[data-testid="card-number"]').fill('4242424242424242');
|
||||
await page.locator('[data-testid="card-expiry"]').fill('12/25');
|
||||
await page.locator('[data-testid="card-cvc"]').fill('123');
|
||||
await page.locator('[data-testid="card-name"]').fill('John Doe');
|
||||
await page.locator('[data-testid="accept-terms"]').check();
|
||||
|
||||
// Submit payment
|
||||
await page.locator('[data-testid="submit-payment"]').click();
|
||||
|
||||
// Should show success message
|
||||
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
|
||||
await expect(page.locator('text=Subscription successful!')).toBeVisible();
|
||||
|
||||
// Should redirect to dashboard
|
||||
await expect(page).toHaveURL(/dashboard/);
|
||||
});
|
||||
|
||||
test('should handle payment failure', async () => {
|
||||
// Mock Stripe error response
|
||||
await page.route('/api/billing/subscribe', route => {
|
||||
route.fulfill({
|
||||
status: 400,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
error: {
|
||||
message: 'Your card was declined.',
|
||||
code: 'card_declined',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/checkout?plan=pro');
|
||||
|
||||
// Fill payment details
|
||||
await page.locator('[data-testid="card-number"]').fill('4000000000000002');
|
||||
await page.locator('[data-testid="card-expiry"]').fill('12/25');
|
||||
await page.locator('[data-testid="card-cvc"]').fill('123');
|
||||
await page.locator('[data-testid="card-name"]').fill('John Doe');
|
||||
await page.locator('[data-testid="accept-terms"]').check();
|
||||
|
||||
// Submit payment
|
||||
await page.locator('[data-testid="submit-payment"]').click();
|
||||
|
||||
// Should show error message
|
||||
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
|
||||
await expect(page.locator('text=Your card was declined')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should support multiple payment methods', async () => {
|
||||
await page.goto('/checkout?plan=pro');
|
||||
|
||||
// Check available payment methods
|
||||
await expect(page.locator('[data-testid="payment-card"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="payment-paypal"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="payment-bank-transfer"]')).toBeVisible();
|
||||
|
||||
// Select PayPal
|
||||
await page.locator('[data-testid="payment-paypal"]').click();
|
||||
|
||||
// Should show PayPal form
|
||||
await expect(page.locator('[data-testid="paypal-form"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="paypal-email"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should apply discount code', async () => {
|
||||
await page.goto('/checkout?plan=pro');
|
||||
|
||||
// Enter discount code
|
||||
await page.locator('[data-testid="discount-code"]').fill('SAVE20');
|
||||
await page.locator('[data-testid="apply-discount"]').click();
|
||||
|
||||
// Should show discount applied
|
||||
await expect(page.locator('[data-testid="discount-applied"]')).toBeVisible();
|
||||
await expect(page.locator('text=20% discount applied')).toBeVisible();
|
||||
|
||||
// Check updated price
|
||||
await expect(page.locator('[data-testid="final-price"]')).toContainText('$79.20');
|
||||
});
|
||||
|
||||
test('should handle invalid discount code', async () => {
|
||||
await page.goto('/checkout?plan=pro');
|
||||
|
||||
// Enter invalid discount code
|
||||
await page.locator('[data-testid="discount-code"]').fill('INVALID');
|
||||
await page.locator('[data-testid="apply-discount"]').click();
|
||||
|
||||
// Should show error
|
||||
await expect(page.locator('[data-testid="discount-error"]')).toBeVisible();
|
||||
await expect(page.locator('text=Invalid discount code')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show subscription management', async () => {
|
||||
// Mock authenticated user with active subscription
|
||||
await page.goto('/settings/billing');
|
||||
|
||||
// Check subscription details
|
||||
await expect(page.locator('[data-testid="current-plan"]')).toBeVisible();
|
||||
await expect(page.locator('text=Pro Plan')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="subscription-status"]')).toContainText('Active');
|
||||
await expect(page.locator('[data-testid="next-billing"]')).toBeVisible();
|
||||
|
||||
// Check management options
|
||||
await expect(page.locator('[data-testid="update-payment"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="cancel-subscription"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="download-invoices"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle subscription upgrade', async () => {
|
||||
await page.goto('/settings/billing');
|
||||
|
||||
// Click upgrade button
|
||||
await page.locator('[data-testid="upgrade-plan"]').click();
|
||||
|
||||
// Should show upgrade options
|
||||
await expect(page.locator('[data-testid="upgrade-modal"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="plan-enterprise"]')).toBeVisible();
|
||||
|
||||
// Select Enterprise plan
|
||||
await page.locator('[data-testid="select-enterprise"]').click();
|
||||
|
||||
// Should show prorated billing info
|
||||
await expect(page.locator('[data-testid="prorated-amount"]')).toBeVisible();
|
||||
await expect(page.locator('text=Prorated amount due today')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle subscription cancellation', async () => {
|
||||
await page.goto('/settings/billing');
|
||||
|
||||
// Click cancel subscription
|
||||
await page.locator('[data-testid="cancel-subscription"]').click();
|
||||
|
||||
// Should show cancellation modal
|
||||
await expect(page.locator('[data-testid="cancel-modal"]')).toBeVisible();
|
||||
await expect(page.locator('text=Are you sure you want to cancel?')).toBeVisible();
|
||||
|
||||
// Select cancellation reason
|
||||
await page.locator('[data-testid="cancellation-reason"]').selectOption('too_expensive');
|
||||
|
||||
// Add feedback
|
||||
await page.locator('[data-testid="cancellation-feedback"]').fill('Found a cheaper alternative');
|
||||
|
||||
// Confirm cancellation
|
||||
await page.locator('[data-testid="confirm-cancellation"]').click();
|
||||
|
||||
// Should show confirmation
|
||||
await expect(page.locator('[data-testid="cancellation-success"]')).toBeVisible();
|
||||
await expect(page.locator('text=Subscription will be cancelled at the end of the billing period')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle subscription reactivation', async () => {
|
||||
// Mock cancelled subscription
|
||||
await page.goto('/settings/billing');
|
||||
|
||||
// Check reactivation option is available
|
||||
await expect(page.locator('[data-testid="reactivate-subscription"]')).toBeVisible();
|
||||
|
||||
// Click reactivate
|
||||
await page.locator('[data-testid="reactivate-subscription"]').click();
|
||||
|
||||
// Should show reactivation modal
|
||||
await expect(page.locator('[data-testid="reactivate-modal"]')).toBeVisible();
|
||||
await expect(page.locator('text=Reactivate your subscription')).toBeVisible();
|
||||
|
||||
// Confirm reactivation
|
||||
await page.locator('[data-testid="confirm-reactivation"]').click();
|
||||
|
||||
// Should process reactivation
|
||||
await expect(page.locator('[data-testid="reactivation-success"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display invoice history', async () => {
|
||||
await page.goto('/settings/billing');
|
||||
|
||||
// Click invoices tab
|
||||
await page.locator('[data-testid="invoices-tab"]').click();
|
||||
|
||||
// Check invoice list
|
||||
await expect(page.locator('[data-testid="invoice-list"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="invoice-item"]')).toHaveCount(3);
|
||||
|
||||
// Check invoice details
|
||||
await expect(page.locator('[data-testid="invoice-date"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="invoice-amount"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="invoice-status"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="download-invoice"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should download invoice', async () => {
|
||||
// Mock download
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
await page.goto('/settings/billing');
|
||||
await page.locator('[data-testid="invoices-tab"]').click();
|
||||
|
||||
// Click download button
|
||||
await page.locator('[data-testid="download-invoice"]').first().click();
|
||||
|
||||
// Wait for download
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toMatch(/invoice-\d+\.pdf/);
|
||||
});
|
||||
|
||||
test('should update payment method', async () => {
|
||||
await page.goto('/settings/billing');
|
||||
|
||||
// Click update payment method
|
||||
await page.locator('[data-testid="update-payment"]').click();
|
||||
|
||||
// Should show payment form
|
||||
await expect(page.locator('[data-testid="payment-form"]')).toBeVisible();
|
||||
|
||||
// Fill new card details
|
||||
await page.locator('[data-testid="card-number"]').fill('5555555555554444');
|
||||
await page.locator('[data-testid="card-expiry"]').fill('10/26');
|
||||
await page.locator('[data-testid="card-cvc"]').fill('456');
|
||||
await page.locator('[data-testid="card-name"]').fill('Jane Smith');
|
||||
|
||||
// Submit update
|
||||
await page.locator('[data-testid="update-payment-submit"]').click();
|
||||
|
||||
// Should show success message
|
||||
await expect(page.locator('[data-testid="update-success"]')).toBeVisible();
|
||||
await expect(page.locator('text=Payment method updated successfully')).toBeVisible();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user