personal-website-sveltekit/tests/about.test.ts

238 lines
9.9 KiB
TypeScript
Raw Normal View History

import { expect, test } from '@playwright/test';
test.describe('About page', () => {
test('has expected main heading', async ({ page }) => {
await page.goto('/about');
await expect(page.getByRole('heading', { level: 1, name: 'About' })).toBeVisible();
});
test('header/footer links hover: color becomes shellYellow', async ({ page }) => {
await page.goto('/about');
const shellYellow = await page.evaluate(() => {
const probe = document.createElement('div');
probe.style.color = 'var(--shellYellow)';
document.body.appendChild(probe);
const color = getComputedStyle(probe).color;
probe.remove();
return color;
});
const areas = [
'header[aria-label="header navigation"]',
'footer nav[aria-label="footer navigation"]',
];
for (const area of areas) {
const nav = page.locator(area);
await expect(nav).toBeVisible();
const link = nav.getByRole('link', { name: 'Portfolio', exact: true });
await expect(link).toBeVisible();
const before = await link.evaluate((el) => {
const cs = getComputedStyle(el as Element) as CSSStyleDeclaration;
return { color: cs.color };
});
await link.hover();
const after = await link.evaluate((el) => {
const cs = getComputedStyle(el as Element) as CSSStyleDeclaration;
return { color: cs.color };
});
expect(after.color).toBe(shellYellow);
// Sanity: it should change from the default color
expect(after.color).not.toBe(before.color);
}
});
test('current page (About) link is active in header and footer', async ({ page }) => {
await page.goto('/about');
const areas = [
'header[aria-label="header navigation"]',
'footer nav[aria-label="footer navigation"]',
];
for (const area of areas) {
const nav = page.locator(area);
const aboutLink = nav.getByRole('link', { name: 'About', exact: true });
await expect(aboutLink).toBeVisible();
const isActive = await aboutLink.evaluate((el) => (el as Element).classList.contains('active'));
expect(isActive).toBeTruthy();
}
});
test('tech list hover changes color to shellYellow', async ({ page }) => {
await page.goto('/about');
const techList = page.locator('.tech-list');
await expect(techList).toBeVisible();
// Resolve the actual computed rgb color value for --shellYellow in the browser context
const shellYellow = await page.evaluate(() => {
const probe = document.createElement('div');
probe.style.color = 'var(--shellYellow)';
document.body.appendChild(probe);
const color = getComputedStyle(probe).color;
probe.remove();
return color;
});
const names = ['Svelte', 'Hono', 'TypeScript', 'Drizzle ORM', 'React', 'Next.js', 'Docker'];
for (const name of names) {
const link = techList.locator(`a[title="${name}"]`).first();
await expect(link).toBeVisible();
const before = await link.evaluate((el) => getComputedStyle(el as Element).color);
await link.hover();
const after = await link.evaluate((el) => getComputedStyle(el as Element).color);
expect(before).not.toBe(shellYellow);
expect(after).toBe(shellYellow);
}
});
test('tech list has accessible links for key technologies', async ({ page }) => {
await page.goto('/about');
const techList = page.locator('.tech-list');
await expect(techList).toBeVisible();
const names = ['Svelte', 'Hono', 'TypeScript', 'Drizzle ORM', 'React', 'Next.js', 'Docker'];
for (const name of names) {
const link = techList.locator(`a[title="${name}"]`).first();
await expect(link).toBeVisible();
await expect(link).toHaveAccessibleName(new RegExp(name, 'i'));
}
});
test('tablet viewport (~800px): extracurricular wraps to multiple rows', async ({ page }) => {
await page.setViewportSize({ width: 800, height: 1000 });
await page.goto('/about');
const container = page.locator('.extracurricular');
await expect(container).toBeVisible();
const cards = container.locator('.card');
const count = await cards.count();
expect(count).toBeGreaterThanOrEqual(3);
const [c0, c1, c2] = await Promise.all([
cards.nth(0).boundingBox(),
cards.nth(1).boundingBox(),
cards.nth(2).boundingBox(),
]);
expect(c0 && c1 && c2).toBeTruthy();
if (c0 && c1 && c2) {
// first two side-by-side on same row, third wrapped below
expect(Math.abs(c0.y - c1.y)).toBeLessThan(10);
expect(c2.y).toBeGreaterThan(c0.y + 10);
}
});
test('mobile viewport (375px): extracurricular cards stack vertically', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 900 });
await page.goto('/about');
const container = page.locator('.extracurricular');
const cards = container.locator('.card');
const count = await cards.count();
expect(count).toBeGreaterThanOrEqual(2);
const [a, b] = await Promise.all([
cards.nth(0).boundingBox(),
cards.nth(1).boundingBox(),
]);
expect(a && b).toBeTruthy();
if (a && b) {
expect(b.y).toBeGreaterThan(a.y + 10);
expect(Math.abs(b.x - a.x)).toBeLessThan(40);
}
});
// Mirror header link presence from home tests
test('header navigation shows expected links', async ({ page }) => {
await page.goto('/about');
const headerNav = page.locator('header[aria-label="header navigation"]');
await expect(headerNav).toBeVisible();
await expect(headerNav.getByRole('link', { name: 'Home', exact: true })).toBeVisible();
await expect(headerNav.getByRole('link', { name: 'About', exact: true })).toBeVisible();
await expect(headerNav.getByRole('link', { name: 'Portfolio', exact: true })).toBeVisible();
await expect(headerNav.getByRole('link', { name: 'Uses', exact: true })).toBeVisible();
});
// Mirror header navigation flow from home tests (starting on /about)
test('header navigation links go to correct routes (from /about)', async ({ page }) => {
await page.goto('/about');
const headerNav = page.locator('header[aria-label="header navigation"]');
await headerNav.getByRole('link', { name: 'Portfolio', exact: true }).click();
await expect(page).toHaveURL(/\/portfolio\/?$/);
await headerNav.getByRole('link', { name: 'Uses', exact: true }).click();
await expect(page).toHaveURL(/\/uses\/?$/);
await headerNav.getByRole('link', { name: 'Home', exact: true }).click();
await expect(page).toHaveURL(/\/?$/);
await headerNav.getByRole('link', { name: 'About', exact: true }).click();
await expect(page).toHaveURL(/\/about\/?$/);
});
// Mirror footer link presence from home tests
test('footer shows expected links', async ({ page }) => {
await page.goto('/about');
const footerNav = page.getByRole('navigation', { name: 'footer navigation' });
await expect(footerNav).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Home', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'About', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Portfolio', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Uses', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Privacy', exact: true })).toBeVisible();
await expect(footerNav.getByRole('link', { name: 'Favorite Articles', exact: true })).toBeVisible();
});
// Mirror footer navigation flow from home tests (starting on /about)
test('footer navigation links go to correct routes (from /about)', async ({ page }) => {
await page.goto('/about');
const footerNav = page.getByRole('navigation', { name: 'footer navigation' });
await footerNav.getByRole('link', { name: 'Privacy', exact: true }).scrollIntoViewIfNeeded();
await footerNav.getByRole('link', { name: 'Privacy', exact: true }).click();
await expect(page).toHaveURL(/\/privacy\/?$/);
// Favorite Articles may route to /articles or /articles/1
const fav = footerNav.getByRole('link', { name: 'Favorite Articles', exact: true });
await fav.scrollIntoViewIfNeeded();
const href = await fav.getAttribute('href');
expect(href).toMatch(/\/articles(\/\d+)?\/?$/);
await page.goto(href!);
await expect(page).toHaveURL(/\/articles(\/\d+)?\/?$/, { timeout: 15000 });
await footerNav.getByRole('link', { name: 'About', exact: true }).scrollIntoViewIfNeeded();
await footerNav.getByRole('link', { name: 'About', exact: true }).click();
await expect(page).toHaveURL(/\/about\/?$/);
await footerNav.getByRole('link', { name: 'Home', exact: true }).scrollIntoViewIfNeeded();
await footerNav.getByRole('link', { name: 'Home', exact: true }).click();
await expect(page).toHaveURL(/\/?$/);
});
// Mobile viewport: ensure cat section has no horizontal overflow and second image fits viewport
test('mobile: cat section no horizontal overflow; second cat image fully visible', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 800 });
await page.goto('/about');
const catSection = page.locator('.cat-pics');
await catSection.scrollIntoViewIfNeeded();
// The cat section itself should not horizontally overflow its own box
const sectionOverflowX = await catSection.evaluate((el) => el.scrollWidth - el.clientWidth);
expect(sectionOverflowX).toBeLessThanOrEqual(2);
// Second image inside .cat-pics is fully within the cat section horizontally
const img = page.locator('.cat-pics figure:nth-of-type(2) img');
await expect(img).toBeVisible();
const [imgBox, sectionBox] = await Promise.all([
img.boundingBox(),
catSection.boundingBox(),
]);
expect(imgBox && sectionBox).toBeTruthy();
if (imgBox && sectionBox) {
expect(imgBox.x).toBeGreaterThanOrEqual(sectionBox.x - 1);
expect(imgBox.x + imgBox.width).toBeLessThanOrEqual(sectionBox.x + sectionBox.width + 1);
}
});
});