Files
blackcanyontickets/reactrebuild0825/tests/whitelabel.spec.ts
dzinesco d5c3953888 fix(typescript): resolve build errors and improve type safety
- Fix billing components ConnectError type compatibility with exactOptionalPropertyTypes
- Update Select component usage to match proper API (options vs children)
- Remove unused imports and fix optional property assignments in system components
- Resolve duplicate Order/Ticket type definitions and add null safety checks
- Handle optional branding properties correctly in organization features
- Add window property type declarations for test environment
- Fix Playwright API usage (page.setOffline → page.context().setOffline)
- Clean up unused imports, variables, and parameters across codebase
- Add comprehensive global type declarations for test window extensions

Resolves major TypeScript compilation issues and improves type safety throughout the application.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 13:31:19 -06:00

481 lines
16 KiB
TypeScript

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