import { test, expect } from '@playwright/test'; import type { Page } from '@playwright/test'; // Test configuration const TEST_HOST = 'tickets.acme.test'; const MOCK_ORG_DATA = { orgId: 'acme-corp', name: 'ACME Corporation', branding: { logoUrl: 'https://example.com/acme-logo.png', theme: { accent: '#FF6B35', bgCanvas: '#1A1B1E', bgSurface: '#2A2B2E', textPrimary: '#FFFFFF', textSecondary: '#B0B0B0', }, }, domains: [ { host: TEST_HOST, verified: true, createdAt: '2024-01-01T00:00:00Z', verifiedAt: '2024-01-01T01:00:00Z', }, ], }; // Mock the domain resolution API async function mockDomainResolution(page: Page, orgData = MOCK_ORG_DATA) { await page.route('**/resolveDomain*', async route => { const url = new URL(route.request().url()); const host = url.searchParams.get('host'); if (host === TEST_HOST || host === 'mock.acme.test') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(orgData), }); } else { await route.fulfill({ status: 404, contentType: 'application/json', body: JSON.stringify({ error: 'Organization not found', host }), }); } }); } // Mock domain verification APIs async function mockDomainAPIs(page: Page) { // Mock request verification await page.route('**/requestDomainVerification', async route => { const body = await route.request().postDataJSON(); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, host: body.host, verificationToken: 'bct-verify-123456789', instructions: { type: 'TXT', name: '_bct-verification', value: 'bct-verify-123456789', ttl: 300, }, }), }); }); // Mock verify domain await page.route('**/verifyDomain', async route => { const body = await route.request().postDataJSON(); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, host: body.host, verified: true, verifiedAt: new Date().toISOString(), message: 'Domain successfully verified', }), }); }); } test.describe('Whitelabel System', () => { test.beforeEach(async ({ page }) => { // Mock all domain-related APIs await mockDomainResolution(page); await mockDomainAPIs(page); }); test('should resolve organization from host and apply theme', async ({ page }) => { // Visit with custom host parameter to simulate domain resolution await page.goto('/?host=mock.acme.test'); // Wait for organization resolution await page.waitForTimeout(1000); // Check that organization theme is applied const rootStyles = await page.evaluate(() => { const root = document.documentElement; return { accent: getComputedStyle(root).getPropertyValue('--color-accent-500'), bgCanvas: getComputedStyle(root).getPropertyValue('--color-bg-canvas'), textPrimary: getComputedStyle(root).getPropertyValue('--color-text-primary'), }; }); // Verify theme colors are applied expect(rootStyles.accent.trim()).toBe(MOCK_ORG_DATA.branding.theme.accent); expect(rootStyles.bgCanvas.trim()).toBe(MOCK_ORG_DATA.branding.theme.bgCanvas); }); test('should display organization logo and name in header', async ({ page }) => { await page.goto('/?host=mock.acme.test'); await page.waitForTimeout(1000); // Check for organization name in header const orgName = await page.locator('text="ACME Corporation"').first(); await expect(orgName).toBeVisible(); // Check for logo (if present) const logo = page.locator('img[alt*="ACME Corporation logo"]'); if (await logo.count() > 0) { await expect(logo).toBeVisible(); } }); test('should handle organization not found gracefully', async ({ page }) => { // Mock no organization found await page.route('**/resolveDomain*', async route => { await route.fulfill({ status: 404, contentType: 'application/json', body: JSON.stringify({ error: 'Organization not found' }), }); }); await page.goto('/?host=unknown.domain.test'); await page.waitForTimeout(1000); // Should still render the app with default theme await expect(page.locator('[data-testid="app-layout"]')).toBeVisible(); // Should apply default theme colors const rootStyles = await page.evaluate(() => { const root = document.documentElement; return getComputedStyle(root).getPropertyValue('--color-accent-500'); }); // Should have default accent color expect(rootStyles.trim()).toBe('#F0C457'); }); }); test.describe('Branding Settings', () => { test.beforeEach(async ({ page }) => { await mockDomainResolution(page); await page.goto('/login'); // Mock authentication await page.evaluate(() => { localStorage.setItem('auth-token', 'mock-token'); localStorage.setItem('user-data', JSON.stringify({ id: 'user-1', name: 'Test User', email: 'test@acme.com', role: 'admin', organization: { id: 'acme-corp', name: 'ACME Corporation' }, })); }); }); test('should display branding settings page', async ({ page }) => { await page.goto('/org/acme-corp/branding'); // Wait for page to load await expect(page.locator('h1')).toContainText('Branding Settings'); // Check for color inputs await expect(page.locator('text="Accent Color"')).toBeVisible(); await expect(page.locator('text="Canvas Background"')).toBeVisible(); await expect(page.locator('text="Surface Background"')).toBeVisible(); await expect(page.locator('text="Primary Text"')).toBeVisible(); await expect(page.locator('text="Secondary Text"')).toBeVisible(); }); test('should enable live preview mode', async ({ page }) => { await page.goto('/org/acme-corp/branding'); // Click live preview button await page.click('button:has-text("Live Preview")'); // Verify preview mode is active await expect(page.locator('text="Live preview mode is active"')).toBeVisible(); // Button should change to "Exit Preview" await expect(page.locator('button:has-text("Exit Preview")')).toBeVisible(); }); test('should update colors and show live preview', async ({ page }) => { await page.goto('/org/acme-corp/branding'); // Enable live preview await page.click('button:has-text("Live Preview")'); // Change accent color const newAccentColor = '#00FF88'; await page.fill('input[value*="#"]', newAccentColor); // Check that the theme variable is updated const appliedColor = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--color-accent-500')); expect(appliedColor.trim()).toBe(newAccentColor); // Check that Save button is enabled await expect(page.locator('button:has-text("Save Changes")')).toBeEnabled(); }); test('should validate color formats', async ({ page }) => { await page.goto('/org/acme-corp/branding'); // Enter invalid color await page.fill('input[value*="#"]', 'invalid-color'); // Should show validation error await expect(page.locator('text*="Invalid color"')).toBeVisible(); // Save button should be disabled await expect(page.locator('button:has-text("Save Changes")')).toBeDisabled(); }); test('should save branding changes', async ({ page }) => { await page.goto('/org/acme-corp/branding'); // Change a color to make form dirty await page.fill('input[value*="#"]', '#FF0000'); // Click save await page.click('button:has-text("Save Changes")'); // Should show success message await expect(page.locator('text*="saved successfully"')).toBeVisible(); // Save button should be disabled again await expect(page.locator('button:has-text("Save Changes")')).toBeDisabled(); }); }); test.describe('Domain Settings', () => { test.beforeEach(async ({ page }) => { await mockDomainResolution(page); await mockDomainAPIs(page); await page.goto('/login'); await page.evaluate(() => { localStorage.setItem('auth-token', 'mock-token'); localStorage.setItem('user-data', JSON.stringify({ id: 'user-1', name: 'Test User', email: 'test@acme.com', role: 'admin', organization: { id: 'acme-corp', name: 'ACME Corporation' }, })); }); }); test('should display domain settings page', async ({ page }) => { await page.goto('/org/acme-corp/domains'); await expect(page.locator('h1')).toContainText('Domain Settings'); await expect(page.locator('text="Add Custom Domain"')).toBeVisible(); }); test('should show existing verified domain', async ({ page }) => { await page.goto('/org/acme-corp/domains'); // Should show the verified domain from mock data await expect(page.locator(`text="${TEST_HOST}"`)).toBeVisible(); await expect(page.locator('text="Verified"')).toBeVisible(); }); test('should add new domain and show verification instructions', async ({ page }) => { await page.goto('/org/acme-corp/domains'); // Enter new domain const newDomain = 'tickets.newcorp.com'; await page.fill('input[placeholder*="tickets.example.com"]', newDomain); // Click add domain await page.click('button:has-text("Add Domain")'); // Should show success message await expect(page.locator('text*="added successfully"')).toBeVisible(); // Should show the new domain with unverified status await expect(page.locator(`text="${newDomain}"`)).toBeVisible(); await expect(page.locator('text="Unverified"')).toBeVisible(); }); test('should show DNS instructions for unverified domain', async ({ page }) => { await page.goto('/org/acme-corp/domains'); // Add a domain first await page.fill('input[placeholder*="tickets.example.com"]', 'test.example.com'); await page.click('button:has-text("Add Domain")'); // Click to show DNS instructions await page.click('button:has-text("Show DNS Instructions")'); // Should show DNS configuration details await expect(page.locator('text="DNS Configuration Required"')).toBeVisible(); await expect(page.locator('text="_bct-verification"')).toBeVisible(); await expect(page.locator('text="bct-verify-"')).toBeVisible(); }); test('should verify domain successfully', async ({ page }) => { await page.goto('/org/acme-corp/domains'); // Add a domain first await page.fill('input[placeholder*="tickets.example.com"]', 'verify.example.com'); await page.click('button:has-text("Add Domain")'); // Click verify button await page.click('button:has-text("Check Verification")'); // Should show success message await expect(page.locator('text*="verified successfully"')).toBeVisible(); // Status should change to verified await expect(page.locator('text="Verified"')).toBeVisible(); }); test('should validate domain format', async ({ page }) => { await page.goto('/org/acme-corp/domains'); // Enter invalid domain await page.fill('input[placeholder*="tickets.example.com"]', 'invalid-domain'); await page.click('button:has-text("Add Domain")'); // Should show validation error await expect(page.locator('text*="valid domain name"')).toBeVisible(); }); test('should copy DNS record values', async ({ page }) => { await page.goto('/org/acme-corp/domains'); // Add domain and show instructions await page.fill('input[placeholder*="tickets.example.com"]', 'copy.example.com'); await page.click('button:has-text("Add Domain")'); await page.click('button:has-text("Show DNS Instructions")'); // Mock clipboard API await page.evaluate(() => { Object.assign(navigator, { clipboard: { writeText: () => Promise.resolve(), }, }); }); // Click copy button const copyButton = page.locator('button').filter({ hasText: 'Copy' }).first(); await copyButton.click(); // Copy button should briefly show checkmark await expect(page.locator('svg[data-testid="check-icon"]').first()).toBeVisible(); }); }); test.describe('Theme Application', () => { test('should apply theme CSS variables correctly', async ({ page }) => { const customTheme = { orgId: 'custom-org', name: 'Custom Theme Org', branding: { logoUrl: 'https://example.com/logo.png', theme: { accent: '#FF1234', bgCanvas: '#000000', bgSurface: '#111111', textPrimary: '#FFFFFF', textSecondary: '#CCCCCC', }, }, domains: [], }; await mockDomainResolution(page, customTheme); await page.goto('/?host=custom.test.com'); await page.waitForTimeout(1000); // Check all theme variables are applied const themeVars = await page.evaluate(() => { const root = document.documentElement; const computedStyle = getComputedStyle(root); return { accent: computedStyle.getPropertyValue('--color-accent-500').trim(), bgCanvas: computedStyle.getPropertyValue('--color-bg-canvas').trim(), bgSurface: computedStyle.getPropertyValue('--color-bg-surface').trim(), textPrimary: computedStyle.getPropertyValue('--color-text-primary').trim(), textSecondary: computedStyle.getPropertyValue('--color-text-secondary').trim(), }; }); expect(themeVars.accent).toBe('#FF1234'); expect(themeVars.bgCanvas).toBe('#000000'); expect(themeVars.bgSurface).toBe('#111111'); expect(themeVars.textPrimary).toBe('#FFFFFF'); expect(themeVars.textSecondary).toBe('#CCCCCC'); }); test('should prevent FOUC with early theme application', async ({ page }) => { // Navigate to page and immediately check if theme is applied const startTime = Date.now(); await page.goto('/?host=mock.acme.test'); // Check theme within first 100ms await page.waitForTimeout(100); const hasThemeApplied = await page.evaluate(() => document.documentElement.hasAttribute('data-org-theme')); // Theme should be applied very quickly to prevent FOUC const loadTime = Date.now() - startTime; expect(hasThemeApplied).toBe(true); expect(loadTime).toBeLessThan(1000); // Should load within 1 second }); test('should update theme when organization changes', async ({ page }) => { // Start with one organization await mockDomainResolution(page); await page.goto('/?host=mock.acme.test'); await page.waitForTimeout(500); const initialAccent = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--color-accent-500').trim()); // Change to different organization with different theme const newTheme = { orgId: 'new-org', name: 'New Organization', branding: { theme: { accent: '#00FF00', bgCanvas: '#001122', bgSurface: '#112233', textPrimary: '#FFFF00', textSecondary: '#CCCC00', }, }, domains: [], }; await page.route('**/resolveDomain*', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(newTheme), }); }); // Simulate organization change (in real app this would happen via URL change) await page.evaluate(() => { // Trigger a manual bootstrap refresh if (window.location.search.includes('refresh=true')) {return;} window.location.search = '?refresh=true&host=new.test.com'; }); await page.waitForTimeout(1000); const newAccent = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--color-accent-500').trim()); expect(newAccent).toBe('#00FF00'); expect(newAccent).not.toBe(initialAccent); }); });