- Add comprehensive analytics components with export functionality - Implement territory management with manager performance tracking - Add seatmap components for venue layout management - Create customer management features with modal interface - Add advanced hooks for dashboard flags and territory data - Implement seat selection and venue management utilities - Add type definitions for ticketing and seatmap systems 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
389 lines
14 KiB
TypeScript
389 lines
14 KiB
TypeScript
/**
|
|
* Playwright E2E Tests for Organization Branding FOUC Prevention
|
|
*
|
|
* Tests that verify zero Flash of Unstyled Content (FOUC) during
|
|
* organization branding application
|
|
*/
|
|
|
|
import { test, expect, type Page } from '@playwright/test';
|
|
|
|
test.describe('Organization Branding - FOUC Prevention', () => {
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
// Clear localStorage before each test to ensure clean state
|
|
await page.goto('about:blank');
|
|
await page.evaluate(() => {
|
|
localStorage.clear();
|
|
});
|
|
});
|
|
|
|
test('should apply cached branding before React hydration', async ({ page }) => {
|
|
// First, prime the cache by visiting the app
|
|
await page.goto('http://localhost:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Verify cache was created
|
|
const cacheExists = await page.evaluate(() => {
|
|
return localStorage.getItem('bct_branding') !== null;
|
|
});
|
|
expect(cacheExists).toBe(true);
|
|
|
|
// Clear page and revisit to test cache application
|
|
await page.goto('about:blank');
|
|
await page.goto('http://localhost:5173', { waitUntil: 'commit' });
|
|
|
|
// Check that CSS variables are applied immediately, before React loads
|
|
const cssVarsApplied = await page.evaluate(() => {
|
|
const root = document.documentElement;
|
|
const accent = getComputedStyle(root).getPropertyValue('--color-accent');
|
|
const canvas = getComputedStyle(root).getPropertyValue('--color-bg-canvas');
|
|
const surface = getComputedStyle(root).getPropertyValue('--color-bg-surface');
|
|
|
|
return {
|
|
accent: accent.trim(),
|
|
canvas: canvas.trim(),
|
|
surface: surface.trim(),
|
|
hasAccent: accent.trim() !== '',
|
|
hasCanvas: canvas.trim() !== '',
|
|
hasSurface: surface.trim() !== '',
|
|
};
|
|
});
|
|
|
|
expect(cssVarsApplied.hasAccent).toBe(true);
|
|
expect(cssVarsApplied.hasCanvas).toBe(true);
|
|
expect(cssVarsApplied.hasSurface).toBe(true);
|
|
|
|
console.log('CSS Variables applied:', cssVarsApplied);
|
|
});
|
|
|
|
test('should not show white flash during initial load', async ({ page }) => {
|
|
// Set viewport to consistent size
|
|
await page.setViewportSize({ width: 1280, height: 720 });
|
|
|
|
// Navigate and capture screenshots at different load stages
|
|
const response = page.goto('http://localhost:5173', { waitUntil: 'commit' });
|
|
|
|
// Take screenshot immediately after HTML loads but before CSS/JS
|
|
await page.waitForLoadState('domcontentloaded');
|
|
const domContentLoadedScreenshot = await page.screenshot({
|
|
clip: { x: 0, y: 0, width: 1280, height: 100 } // Just capture header area
|
|
});
|
|
|
|
// Wait for full page load
|
|
await response;
|
|
await page.waitForLoadState('networkidle');
|
|
const fullyLoadedScreenshot = await page.screenshot({
|
|
clip: { x: 0, y: 0, width: 1280, height: 100 }
|
|
});
|
|
|
|
// Take another screenshot after a brief delay to catch any flashes
|
|
await page.waitForTimeout(200);
|
|
const delayedScreenshot = await page.screenshot({
|
|
clip: { x: 0, y: 0, width: 1280, height: 100 }
|
|
});
|
|
|
|
// Verify that the background isn't white at any point
|
|
const hasWhiteFlash = await page.evaluate(() => {
|
|
const canvas = document.documentElement.style.getPropertyValue('--color-bg-canvas') ||
|
|
getComputedStyle(document.documentElement).getPropertyValue('--color-bg-canvas');
|
|
const body = getComputedStyle(document.body).backgroundColor;
|
|
|
|
return {
|
|
canvasColor: canvas.trim(),
|
|
bodyBg: body,
|
|
isWhite: body === 'rgb(255, 255, 255)' || body === '#ffffff' || body === 'white'
|
|
};
|
|
});
|
|
|
|
console.log('Background color check:', hasWhiteFlash);
|
|
expect(hasWhiteFlash.isWhite).toBe(false);
|
|
});
|
|
|
|
test('should maintain organization colors during navigation', async ({ page }) => {
|
|
await page.goto('http://localhost:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Get initial colors
|
|
const initialColors = await page.evaluate(() => {
|
|
const root = document.documentElement;
|
|
return {
|
|
accent: getComputedStyle(root).getPropertyValue('--color-accent').trim(),
|
|
canvas: getComputedStyle(root).getPropertyValue('--color-bg-canvas').trim(),
|
|
surface: getComputedStyle(root).getPropertyValue('--color-bg-surface').trim(),
|
|
};
|
|
});
|
|
|
|
expect(initialColors.accent).toBeTruthy();
|
|
expect(initialColors.canvas).toBeTruthy();
|
|
expect(initialColors.surface).toBeTruthy();
|
|
|
|
// Navigate to different pages within the app
|
|
const routes = ['/', '/events', '/dashboard', '/settings'];
|
|
|
|
for (const route of routes) {
|
|
await page.goto(`http://localhost:5173${route}`);
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
const currentColors = await page.evaluate(() => {
|
|
const root = document.documentElement;
|
|
return {
|
|
accent: getComputedStyle(root).getPropertyValue('--color-accent').trim(),
|
|
canvas: getComputedStyle(root).getPropertyValue('--color-bg-canvas').trim(),
|
|
surface: getComputedStyle(root).getPropertyValue('--color-bg-surface').trim(),
|
|
};
|
|
});
|
|
|
|
// Colors should remain consistent across pages
|
|
expect(currentColors.accent).toBe(initialColors.accent);
|
|
expect(currentColors.canvas).toBe(initialColors.canvas);
|
|
expect(currentColors.surface).toBe(initialColors.surface);
|
|
}
|
|
});
|
|
|
|
test('should handle missing cache gracefully', async ({ page }) => {
|
|
// Ensure no cache exists
|
|
await page.goto('about:blank');
|
|
await page.evaluate(() => {
|
|
localStorage.removeItem('bct_branding');
|
|
localStorage.removeItem('bct_last_org_id');
|
|
});
|
|
|
|
// Navigate to app
|
|
await page.goto('http://localhost:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Should still apply default theme without flash
|
|
const themeApplied = await page.evaluate(() => {
|
|
const root = document.documentElement;
|
|
const accent = getComputedStyle(root).getPropertyValue('--color-accent').trim();
|
|
const canvas = getComputedStyle(root).getPropertyValue('--color-bg-canvas').trim();
|
|
|
|
return {
|
|
accent,
|
|
canvas,
|
|
hasTheme: accent !== '' && canvas !== '',
|
|
isDefaultTheme: accent === '#F0C457' || accent === 'rgb(240, 196, 87)'
|
|
};
|
|
});
|
|
|
|
expect(themeApplied.hasTheme).toBe(true);
|
|
console.log('Default theme applied:', themeApplied);
|
|
});
|
|
|
|
test('should update favicon without visible delay', async ({ page }) => {
|
|
await page.goto('http://localhost:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Check if favicon was set (mock organization should have one)
|
|
const favicon = await page.evaluate(() => {
|
|
const faviconLink = document.querySelector('link[rel*="icon"]') as HTMLLinkElement;
|
|
return {
|
|
exists: !!faviconLink,
|
|
href: faviconLink?.href || null,
|
|
hasCustomFavicon: faviconLink?.href && !faviconLink.href.includes('vite.svg')
|
|
};
|
|
});
|
|
|
|
console.log('Favicon state:', favicon);
|
|
|
|
// For development, we might not have a custom favicon, but the system should handle it
|
|
expect(favicon.exists).toBe(true);
|
|
});
|
|
|
|
test('should show organization loading guard', async ({ page }) => {
|
|
// Navigate to app and capture loading states
|
|
await page.goto('http://localhost:5173');
|
|
|
|
// Look for loading indicators during organization resolution
|
|
const loadingElements = await page.evaluate(() => {
|
|
const spinners = document.querySelectorAll('.animate-spin').length;
|
|
const loadingTexts = Array.from(document.querySelectorAll('*')).some(
|
|
el => el.textContent?.includes('Loading Organization')
|
|
);
|
|
|
|
return {
|
|
spinners,
|
|
hasLoadingText: loadingTexts,
|
|
};
|
|
});
|
|
|
|
// Wait for full load
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// After loading, check that organization is resolved
|
|
const orgResolved = await page.evaluate(() => {
|
|
// Check if header shows organization info
|
|
const header = document.querySelector('header') || document.querySelector('[data-testid="header"]');
|
|
return {
|
|
hasHeader: !!header,
|
|
headerContent: header?.textContent || '',
|
|
};
|
|
});
|
|
|
|
expect(orgResolved.hasHeader).toBe(true);
|
|
console.log('Organization resolution:', orgResolved);
|
|
});
|
|
|
|
test('should handle organization error states', async ({ page }) => {
|
|
// Mock a network error for organization resolution
|
|
await page.route('**/resolveDomain*', route => {
|
|
route.fulfill({
|
|
status: 500,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ error: 'Internal Server Error' })
|
|
});
|
|
});
|
|
|
|
await page.goto('http://localhost:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Should still render with default theme
|
|
const errorHandled = await page.evaluate(() => {
|
|
const hasErrorUI = document.body.textContent?.includes('Configuration Error') ||
|
|
document.body.textContent?.includes('No organization');
|
|
const hasDefaultTheme = getComputedStyle(document.documentElement)
|
|
.getPropertyValue('--color-accent').trim() !== '';
|
|
|
|
return {
|
|
hasErrorUI,
|
|
hasDefaultTheme,
|
|
bodyText: document.body.textContent?.substring(0, 200)
|
|
};
|
|
});
|
|
|
|
console.log('Error handling:', errorHandled);
|
|
expect(errorHandled.hasDefaultTheme).toBe(true);
|
|
});
|
|
|
|
test('should demonstrate theme switching (live preview)', async ({ page }) => {
|
|
await page.goto('http://localhost:5173/admin/branding');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Look for branding settings interface
|
|
const brandingUI = await page.evaluate(() => {
|
|
const hasColorInputs = document.querySelectorAll('input[type="color"]').length > 0;
|
|
const hasPreviewButton = Array.from(document.querySelectorAll('button')).some(
|
|
btn => btn.textContent?.includes('Preview')
|
|
);
|
|
|
|
return {
|
|
hasColorInputs,
|
|
hasPreviewButton,
|
|
pageTitle: document.title,
|
|
hasThemeControls: hasColorInputs || hasPreviewButton
|
|
};
|
|
});
|
|
|
|
console.log('Branding settings UI:', brandingUI);
|
|
|
|
// If branding UI is available, test live preview
|
|
if (brandingUI.hasThemeControls) {
|
|
// Get current accent color
|
|
const currentAccent = await page.evaluate(() => {
|
|
return getComputedStyle(document.documentElement)
|
|
.getPropertyValue('--color-accent').trim();
|
|
});
|
|
|
|
// Try to change accent color (if UI allows)
|
|
const colorInput = await page.$('input[type="color"]');
|
|
if (colorInput) {
|
|
await colorInput.fill('#FF0000'); // Change to red
|
|
|
|
// Check if preview mode updates colors
|
|
const previewButton = await page.getByText('Preview', { exact: false }).first();
|
|
if (previewButton) {
|
|
await previewButton.click();
|
|
|
|
await page.waitForTimeout(500); // Allow time for theme to apply
|
|
|
|
const updatedAccent = await page.evaluate(() => {
|
|
return getComputedStyle(document.documentElement)
|
|
.getPropertyValue('--color-accent').trim();
|
|
});
|
|
|
|
console.log('Theme preview test:', {
|
|
original: currentAccent,
|
|
updated: updatedAccent,
|
|
changed: currentAccent !== updatedAccent
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('Performance Impact', () => {
|
|
test('should not significantly impact initial page load time', async ({ page }) => {
|
|
const startTime = Date.now();
|
|
|
|
await page.goto('http://localhost:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log(`Page load time: ${loadTime}ms`);
|
|
|
|
// Load time should be reasonable (adjust threshold as needed)
|
|
expect(loadTime).toBeLessThan(5000); // 5 seconds max
|
|
});
|
|
|
|
test('should not cause layout shifts', async ({ page }) => {
|
|
await page.goto('http://localhost:5173');
|
|
|
|
// Measure Cumulative Layout Shift (CLS)
|
|
const cls = await page.evaluate(() => {
|
|
return new Promise(resolve => {
|
|
let clsValue = 0;
|
|
new PerformanceObserver((list) => {
|
|
for (const entry of list.getEntries()) {
|
|
if (entry.entryType === 'layout-shift') {
|
|
clsValue += (entry as any).value;
|
|
}
|
|
}
|
|
}).observe({ entryTypes: ['layout-shift'] });
|
|
|
|
setTimeout(() => resolve(clsValue), 2000);
|
|
});
|
|
});
|
|
|
|
console.log(`Cumulative Layout Shift: ${cls}`);
|
|
|
|
// CLS should be minimal (0.1 is considered good)
|
|
expect(cls).toBeLessThan(0.1);
|
|
});
|
|
});
|
|
|
|
test.describe('Accessibility', () => {
|
|
test('should maintain WCAG contrast ratios with custom themes', async ({ page }) => {
|
|
await page.goto('http://localhost:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Check contrast ratios of key elements
|
|
const contrastResults = await page.evaluate(() => {
|
|
const getContrast = (element: Element) => {
|
|
const styles = getComputedStyle(element);
|
|
return {
|
|
color: styles.color,
|
|
backgroundColor: styles.backgroundColor,
|
|
element: element.tagName
|
|
};
|
|
};
|
|
|
|
// Check various text elements
|
|
const elements = [
|
|
...Array.from(document.querySelectorAll('h1, h2, h3')),
|
|
...Array.from(document.querySelectorAll('p, span')),
|
|
...Array.from(document.querySelectorAll('button')),
|
|
].slice(0, 10); // Limit to first 10 elements
|
|
|
|
return elements.map(getContrast);
|
|
});
|
|
|
|
console.log('Contrast analysis:', contrastResults);
|
|
|
|
// Basic check - elements should have defined colors
|
|
contrastResults.forEach(result => {
|
|
expect(result.color).toBeTruthy();
|
|
});
|
|
});
|
|
}); |