Files
blackcanyontickets/reactrebuild0825/tests/branding-fouc.spec.ts
dzinesco 8ed7ae95d1 feat: comprehensive project completion and documentation
- Enhanced event creation wizard with multi-step validation
- Added advanced QR scanning system with offline support
- Implemented comprehensive territory management features
- Expanded analytics with export functionality and KPIs
- Created complete design token system with theme switching
- Added 25+ Playwright test files for comprehensive coverage
- Implemented enterprise-grade permission system
- Enhanced component library with 80+ React components
- Added Firebase integration for deployment
- Completed Phase 3 development goals substantially

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 15:04:37 -06:00

392 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 } 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');
// Take screenshot immediately after HTML loads but before CSS/JS
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');
// Take screenshot after full page load
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);
// Take screenshot after delay to catch any flashes
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
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();
});
});
});