From f59bbfac644374479c650a7853c39c4dc71d96ce Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sat, 24 Jan 2026 18:12:21 -0600 Subject: [PATCH] [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 --- src/hooks/query-keys.ts | 220 +++++++++++++++++++++ tests/e2e/registration.spec.ts | 200 +++++++++++++++++++ tests/e2e/subscription.spec.ts | 348 +++++++++++++++++++++++++++++++++ 3 files changed, 768 insertions(+) create mode 100644 src/hooks/query-keys.ts create mode 100644 tests/e2e/registration.spec.ts create mode 100644 tests/e2e/subscription.spec.ts diff --git a/src/hooks/query-keys.ts b/src/hooks/query-keys.ts new file mode 100644 index 0000000..4b8882d --- /dev/null +++ b/src/hooks/query-keys.ts @@ -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, +) => { + 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) => + 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) => + 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) => + 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) => + 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) => + createQueryKey('featureFlags', { type: 'evaluations', ...params }), + }, + + // Audit module + audit: { + logs: (filters?: Record) => + createQueryKey('audit', { type: 'logs', ...filters }), + activities: (filters?: Record) => + 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) => + createQueryKey('ai', { type: 'usage', ...filters }), + models: () => createQueryKey('ai', { type: 'models' }), + }, + + // Storage module + storage: { + files: (filters?: Record) => + 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) => + 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) => + 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 => { + const params: Record = {}; + + 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(), + ], +}; diff --git a/tests/e2e/registration.spec.ts b/tests/e2e/registration.spec.ts new file mode 100644 index 0000000..282d219 --- /dev/null +++ b/tests/e2e/registration.spec.ts @@ -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(); + }); +}); diff --git a/tests/e2e/subscription.spec.ts b/tests/e2e/subscription.spec.ts new file mode 100644 index 0000000..12a6c01 --- /dev/null +++ b/tests/e2e/subscription.spec.ts @@ -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(); + }); +});