- 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>
481 lines
16 KiB
TypeScript
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);
|
|
});
|
|
}); |