[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:
Adrian Flores Cortes 2026-01-24 18:12:21 -06:00
parent eb95d0e276
commit f59bbfac64
3 changed files with 768 additions and 0 deletions

220
src/hooks/query-keys.ts Normal file
View 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(),
],
};

View 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();
});
});

View 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();
});
});