import { test, expect } from '@playwright/test'; test.describe('Visual & Responsive', () => { test('bottom nav is fixed at bottom', async ({ page }) => { await page.goto('/'); const nav = page.locator('nav.fixed.bottom-0'); await expect(nav).toBeVisible(); }); test('page uses dark theme', async ({ page }) => { await page.goto('/'); const bgColor = await page.locator('#root').evaluate(el => window.getComputedStyle(el).backgroundColor ); // Should be dark (close to #0a0a0a) expect(bgColor).toMatch(/rgba?\(\s*10/); }); test('music-accent color is used', async ({ page }) => { await page.goto('/'); // Check that accent-colored elements exist const accentElements = page.locator('[class*="music-accent"]'); await expect(accentElements).toHaveCount(atLeast(1)); }); test('material icons are loaded', async ({ page }) => { await page.goto('/'); const icon = page.locator('.material-icons').first(); await expect(icon).toBeVisible(); }); test('page transitions work', async ({ page }) => { await page.goto('/'); await page.getByText('Search').click(); await page.waitForTimeout(300); await expect(page).toHaveURL(/\/search/); await page.getByText('Home').click(); await page.waitForTimeout(300); await expect(page).toHaveURL(/\/$/); }); test('responsive: content fits on mobile width', async ({ page }) => { await page.setViewportSize({ width: 375, height: 812 }); await page.goto('/'); await expect(page.getByText('Mood Radio')).toBeVisible(); }); test('responsive: content fits on tablet width', async ({ page }) => { await page.setViewportSize({ width: 768, height: 1024 }); await page.goto('/'); await expect(page.getByText('Mood Radio')).toBeVisible(); }); function atLeast(n: number) { return { pass(received: number) { return received >= n; }, message: () => `expected at least ${n} elements`, }; } }); test.describe('Accessibility', () => { test('all images have alt text or are decorative', async ({ page }) => { await page.goto('/'); const images = await page.locator('img').all(); for (const img of images) { const alt = await img.getAttribute('alt'); expect(alt !== null).toBeTruthy(); } }); test('buttons have accessible names', async ({ page }) => { await page.goto('/'); const buttons = await page.locator('button').all(); // Buttons should either have text content, aria-label, or be icon buttons for (const btn of buttons) { const hasText = await btn.textContent(); const hasAria = await btn.getAttribute('aria-label'); const hasTitle = await btn.getAttribute('title'); const hasMaterialIcon = await btn.locator('.material-icons').count(); // Button is accessible if it has text, aria-label, title, or material icon expect( (hasText && hasText.trim().length > 0) || hasAria || hasTitle || hasMaterialIcon > 0 ).toBeTruthy(); } }); test('links have accessible names', async ({ page }) => { await page.goto('/'); const links = await page.locator('a').all(); for (const link of links) { const hasText = await link.textContent(); const hasAria = await link.getAttribute('aria-label'); const hasMaterialIcon = await link.locator('.material-icons').count(); expect( (hasText && hasText.trim().length > 0) || hasAria || hasMaterialIcon > 0 ).toBeTruthy(); } }); test('page has proper HTML structure', async ({ page }) => { await page.goto('/'); await expect(page.locator('h1, h2')).toHaveCount(atLeast(1)); }); function atLeast(n: number) { return { pass(received: number) { return received >= n; }, message: () => `expected at least ${n} elements`, }; } }); test.describe('Performance', () => { test('home page loads under 3 seconds', async ({ page }) => { const start = Date.now(); await page.goto('/'); const loadTime = Date.now() - start; expect(loadTime).toBeLessThan(3000); }); test('page navigation is fast', async ({ page }) => { await page.goto('/'); const start = Date.now(); await page.getByText('Search').click(); await page.waitForURL(/\/search/); const navTime = Date.now() - start; expect(navTime).toBeLessThan(1000); }); });