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>
This commit is contained in:
481
reactrebuild0825/tests/whitelabel.spec.ts
Normal file
481
reactrebuild0825/tests/whitelabel.spec.ts
Normal file
@@ -0,0 +1,481 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user