[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