music-app/e2e/visual.spec.ts
2026-07-03 01:06:35 +00:00

141 lines
4.4 KiB
TypeScript

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);
});
});