Skip to content

Commit df330d8

Browse files
ManaiakalaniCopilot
andcommitted
test: add Playwright fit-and-finish test suite
52 tests across desktop + mobile viewports covering: - Page loads, titles, navigation - Content presence (hero, about, projects, thoughts) - Theme toggle and GeoCities mode toggle - No dead analytics scripts, no console errors, no broken images - Mobile responsive (no horizontal overflow) - Internal links, meta tags, font loading Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 82fead2 commit df330d8

File tree

5 files changed

+323
-0
lines changed

5 files changed

+323
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
test-results/
3+
playwright-report/

package-lock.json

Lines changed: 79 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "manaiakalani.com",
3+
"version": "1.0.0",
4+
"description": "Personal website for **Maximilian Stein** — Community Strategy Lead for **Microsoft Intune** and **Microsoft Security** within Customer Experience Engineering (CxE).",
5+
"main": "cube.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/Manaiakalani/Manaiakalani.com.git"
12+
},
13+
"keywords": [],
14+
"author": "",
15+
"license": "ISC",
16+
"bugs": {
17+
"url": "https://github.com/Manaiakalani/Manaiakalani.com/issues"
18+
},
19+
"homepage": "https://github.com/Manaiakalani/Manaiakalani.com#readme",
20+
"devDependencies": {
21+
"@playwright/test": "^1.58.2"
22+
}
23+
}

playwright.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { defineConfig } = require('@playwright/test');
2+
3+
module.exports = defineConfig({
4+
testDir: './tests',
5+
timeout: 30000,
6+
use: {
7+
baseURL: 'https://manaiakalani.com',
8+
screenshot: 'only-on-failure',
9+
},
10+
projects: [
11+
{ name: 'desktop', use: { viewport: { width: 1280, height: 800 } } },
12+
{ name: 'mobile', use: { viewport: { width: 375, height: 812 } } },
13+
],
14+
});

tests/fit-and-finish.spec.js

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
const { test, expect } = require('@playwright/test');
2+
3+
const PAGES = [
4+
{ path: '/', title: 'Maximilian Stein', name: 'index' },
5+
{ path: '/projects.html', title: 'Projects — Maximilian Stein', name: 'projects' },
6+
{ path: '/thoughts.html', title: 'Thoughts — Maximilian Stein', name: 'thoughts' },
7+
];
8+
9+
// ── Page loads & titles ──
10+
for (const pg of PAGES) {
11+
test(`${pg.name}: loads with correct title`, async ({ page }) => {
12+
const res = await page.goto(pg.path);
13+
expect(res.status()).toBe(200);
14+
await expect(page).toHaveTitle(pg.title);
15+
});
16+
}
17+
18+
// ── Navigation ──
19+
test('nav links are present and correct on all pages', async ({ page }) => {
20+
for (const pg of PAGES) {
21+
await page.goto(pg.path);
22+
const nav = page.locator('nav');
23+
await expect(nav).toBeVisible();
24+
await expect(nav.locator('a[href="index.html"]')).toBeVisible();
25+
await expect(nav.locator('a[href="projects.html"]')).toBeVisible();
26+
await expect(nav.locator('a[href="thoughts.html"]')).toBeVisible();
27+
}
28+
});
29+
30+
// ── Index page specifics ──
31+
test('index: hero section with Aloha greeting', async ({ page }) => {
32+
await page.goto('/');
33+
const hero = page.locator('header');
34+
await expect(hero).toBeVisible();
35+
// Check for "Aloha" somewhere on the page
36+
await expect(page.locator('text=Aloha')).toBeVisible();
37+
});
38+
39+
test('index: about section exists', async ({ page }) => {
40+
await page.goto('/');
41+
await expect(page.locator('#about')).toBeVisible();
42+
});
43+
44+
test('index: featured projects teaser exists', async ({ page }) => {
45+
await page.goto('/');
46+
const cards = page.locator('.project-card');
47+
const count = await cards.count();
48+
expect(count).toBeGreaterThanOrEqual(1);
49+
});
50+
51+
test('index: footer with ASCII cube canvas exists', async ({ page }) => {
52+
await page.goto('/');
53+
await expect(page.locator('footer')).toBeVisible();
54+
await expect(page.locator('#ascii-cube')).toBeVisible();
55+
});
56+
57+
// ── Projects page ──
58+
test('projects: has project cards', async ({ page }) => {
59+
await page.goto('/projects.html');
60+
const cards = page.locator('.project-card');
61+
const count = await cards.count();
62+
expect(count).toBeGreaterThanOrEqual(5);
63+
});
64+
65+
test('projects: cards have title and description', async ({ page }) => {
66+
await page.goto('/projects.html');
67+
const firstCard = page.locator('.project-card').first();
68+
await expect(firstCard.locator('h3')).toBeVisible();
69+
await expect(firstCard.locator('p')).toBeVisible();
70+
});
71+
72+
test('projects: GitHub link is visible', async ({ page }) => {
73+
await page.goto('/projects.html');
74+
const ghLink = page.locator('a.github-link');
75+
await expect(ghLink).toBeVisible();
76+
});
77+
78+
// ── Thoughts page ──
79+
test('thoughts: has thought entries', async ({ page }) => {
80+
await page.goto('/thoughts.html');
81+
const entries = page.locator('.thought-entry');
82+
const count = await entries.count();
83+
expect(count).toBeGreaterThanOrEqual(3);
84+
});
85+
86+
// ── Theme toggle ──
87+
test('theme toggle switches dark/light', async ({ page }) => {
88+
await page.goto('/');
89+
const toggle = page.locator('.theme-toggle');
90+
await expect(toggle).toBeVisible();
91+
92+
// Get initial theme
93+
const initial = await page.locator('html').getAttribute('data-theme');
94+
95+
// Click toggle
96+
await toggle.click();
97+
const after = await page.locator('html').getAttribute('data-theme');
98+
expect(after).not.toBe(initial);
99+
});
100+
101+
// ── GeoCities toggle ──
102+
test('geocities toggle activates retro mode', async ({ page }) => {
103+
await page.goto('/');
104+
const toggle = page.locator('.geocities-toggle');
105+
await expect(toggle).toBeVisible();
106+
107+
// Activate
108+
await toggle.click();
109+
await expect(page.locator('html')).toHaveAttribute('data-geocities', 'true');
110+
111+
// Check injected elements appear
112+
await expect(page.locator('.gc-construction-banner')).toBeVisible();
113+
await expect(page.locator('.gc-flames-bar').first()).toBeVisible();
114+
});
115+
116+
test('geocities toggle deactivates cleanly', async ({ page }) => {
117+
await page.goto('/');
118+
const toggle = page.locator('.geocities-toggle');
119+
120+
// Activate then deactivate
121+
await toggle.click();
122+
await expect(page.locator('.gc-construction-banner')).toBeVisible();
123+
await toggle.click({ force: true });
124+
125+
// Elements should be removed
126+
await expect(page.locator('.gc-construction-banner')).toHaveCount(0);
127+
await expect(page.locator('html')).not.toHaveAttribute('data-geocities', 'true');
128+
});
129+
130+
// ── No dead Umami scripts ──
131+
test('no dead analytics scripts on any page', async ({ page }) => {
132+
for (const pg of PAGES) {
133+
await page.goto(pg.path);
134+
const html = await page.content();
135+
expect(html).not.toContain('analytics.manaiakalani.info');
136+
}
137+
});
138+
139+
// ── No console errors ──
140+
for (const pg of PAGES) {
141+
test(`${pg.name}: no console errors`, async ({ page }) => {
142+
const errors = [];
143+
page.on('console', msg => {
144+
if (msg.type() === 'error') errors.push(msg.text());
145+
});
146+
await page.goto(pg.path, { waitUntil: 'networkidle' });
147+
// Filter out known third-party noise (e.g. font loading, favicon)
148+
const real = errors.filter(e =>
149+
!e.includes('favicon') && !e.includes('fonts.googleapis') &&
150+
!e.includes('WebGL') && !e.includes('THREE.')
151+
);
152+
expect(real).toEqual([]);
153+
});
154+
}
155+
156+
// ── No broken images ──
157+
for (const pg of PAGES) {
158+
test(`${pg.name}: no broken images`, async ({ page }) => {
159+
await page.goto(pg.path, { waitUntil: 'networkidle' });
160+
const images = await page.locator('img').all();
161+
for (const img of images) {
162+
const nat = await img.evaluate(el => el.naturalWidth);
163+
const src = await img.getAttribute('src');
164+
expect(nat, `broken image: ${src}`).toBeGreaterThan(0);
165+
}
166+
});
167+
}
168+
169+
// ── Responsive: nav doesn't overflow on mobile ──
170+
test('mobile: no horizontal overflow', async ({ page }) => {
171+
await page.goto('/');
172+
const bodyWidth = await page.evaluate(() => document.body.scrollWidth);
173+
const viewWidth = await page.evaluate(() => window.innerWidth);
174+
expect(bodyWidth).toBeLessThanOrEqual(viewWidth + 5); // small tolerance
175+
});
176+
177+
// ── Links don't 404 ──
178+
test('internal links resolve (no 404s)', async ({ page }) => {
179+
await page.goto('/');
180+
const hrefs = await page.locator('nav a').evaluateAll(els =>
181+
els.map(a => a.getAttribute('href')).filter(h => h && !h.startsWith('http'))
182+
);
183+
for (const href of hrefs) {
184+
const res = await page.goto(`/${href}`);
185+
expect(res.status(), `${href} returned ${res.status()}`).toBe(200);
186+
}
187+
});
188+
189+
// ── Meta tags ──
190+
test('index: has meta description', async ({ page }) => {
191+
await page.goto('/');
192+
const desc = await page.locator('meta[name="description"]').getAttribute('content');
193+
expect(desc).toBeTruthy();
194+
expect(desc.length).toBeGreaterThan(20);
195+
});
196+
197+
// ── Font loading ──
198+
test('index: Doto font is loaded', async ({ page }) => {
199+
await page.goto('/', { waitUntil: 'networkidle' });
200+
const fontLoaded = await page.evaluate(() =>
201+
document.fonts.check('16px "Doto"')
202+
);
203+
expect(fontLoaded).toBe(true);
204+
});

0 commit comments

Comments
 (0)