Skip to content

Commit e2e3592

Browse files
committed
Expand E2E tests from 9 to 53 with resilient selectors
Add tests for homepage sections (feature cards, code showcase, vision CTA, footer), blog post structure and navigation, 404/sitemap/robots, mobile viewport (hamburger menu, hidden avatars), and accessibility (landmarks, heading hierarchy, ARIA, keyboard nav, external link rel). Tests use minimum counts and semantic selectors over exact content strings to avoid churn when copy or layout changes.
1 parent f3ddee4 commit e2e3592

5 files changed

Lines changed: 449 additions & 14 deletions

File tree

tests/e2e/accessibility.spec.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
// ── Landmark Roles ──
4+
5+
test('page has banner, main, and contentinfo landmarks', async ({ page }) => {
6+
await page.goto('/');
7+
await expect(page.getByRole('banner')).toBeVisible();
8+
await expect(page.getByRole('main')).toBeVisible();
9+
await expect(page.getByRole('contentinfo')).toBeVisible();
10+
});
11+
12+
// ── Heading Hierarchy ──
13+
14+
test('homepage has exactly one h1', async ({ page }) => {
15+
await page.goto('/');
16+
await expect(page.locator('h1')).toHaveCount(1);
17+
});
18+
19+
test('blog index has exactly one h1', async ({ page }) => {
20+
await page.goto('/blogs/');
21+
await expect(page.locator('h1')).toHaveCount(1);
22+
});
23+
24+
test('blog post has exactly one h1', async ({ page }) => {
25+
// Navigate to first available post
26+
await page.goto('/blogs/');
27+
const href = await page.locator('a.blog-card').first().getAttribute('href');
28+
await page.goto(href!);
29+
await expect(page.locator('h1')).toHaveCount(1);
30+
});
31+
32+
// ── Images ──
33+
34+
test('all images have alt text', async ({ page }) => {
35+
await page.goto('/');
36+
await expect(page.locator('img:not([alt])')).toHaveCount(0);
37+
});
38+
39+
// ── Keyboard Navigation ──
40+
41+
test('code showcase tabs are keyboard navigable', async ({ page }) => {
42+
await page.goto('/');
43+
const firstTab = page.getByRole('tab').first();
44+
await firstTab.click();
45+
await expect(firstTab).toHaveAttribute('aria-selected', 'true');
46+
47+
// ArrowDown moves to next tab
48+
await firstTab.press('ArrowDown');
49+
const secondTab = page.getByRole('tab').nth(1);
50+
await expect(secondTab).toHaveAttribute('aria-selected', 'true');
51+
});
52+
53+
// ── External Links ──
54+
55+
test('external links in header/footer have rel="noopener"', async ({ page }) => {
56+
await page.goto('/');
57+
for (const region of ['header', 'footer']) {
58+
const links = page.locator(`${region} a[target="_blank"]`);
59+
const count = await links.count();
60+
expect(count).toBeGreaterThan(0);
61+
for (const link of await links.all()) {
62+
const rel = await link.getAttribute('rel');
63+
expect(rel).toContain('noopener');
64+
}
65+
}
66+
});
67+
68+
// ── ARIA Attributes ──
69+
70+
test('code showcase uses proper ARIA roles', async ({ page }) => {
71+
await page.goto('/');
72+
await expect(page.locator('[role="tablist"]')).toHaveCount(1);
73+
await expect(page.locator('[role="tabpanel"]')).toHaveCount(1);
74+
await expect(page.locator('[role="tab"][aria-selected="true"]')).toHaveCount(1);
75+
});
76+
77+
test('mobile menu toggle has aria-expanded', async ({ page }) => {
78+
await page.goto('/');
79+
const toggle = page.locator('.mobile-menu-toggle');
80+
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
81+
});

tests/e2e/blogs.spec.ts

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,105 @@
11
import { test, expect } from '@playwright/test';
22

3-
test('blogs page renders post listing', async ({ page }) => {
3+
// ── Blog Index Page ──
4+
5+
test('blogs page renders heading', async ({ page }) => {
46
await page.goto('/blogs/');
5-
await expect(page.getByRole('heading', { name: 'Blog', level: 1 })).toBeVisible();
7+
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
8+
await expect(page).toHaveTitle(/Blog/);
69
});
710

8-
test('both existing posts are listed', async ({ page }) => {
11+
test('blog index lists posts with metadata', async ({ page }) => {
912
await page.goto('/blogs/');
10-
await expect(page.getByText(/thinking about AI all wrong/i)).toBeVisible();
11-
await expect(page.getByText(/Outside-In and Inside-Out/i)).toBeVisible();
13+
const cards = page.locator('a.blog-card');
14+
const count = await cards.count();
15+
expect(count).toBeGreaterThanOrEqual(2);
16+
17+
// Each card should have a year (date metadata) and link to a blog post
18+
for (const card of await cards.all()) {
19+
await expect(card).toContainText(/\d{4}/);
20+
await expect(card).toHaveAttribute('href', /\/blogs\//);
21+
}
1222
});
1323

24+
// ── Blog Post Navigation ──
25+
1426
test('clicking a post navigates to post page', async ({ page }) => {
1527
await page.goto('/blogs/');
16-
await page.getByText(/thinking about AI all wrong/i).first().click();
28+
// Click the first blog card
29+
await page.locator('a.blog-card').first().click();
1730
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
1831
await expect(page.getByText('← Back to all posts')).toBeVisible();
1932
});
2033

21-
test('blog post page renders content', async ({ page }) => {
22-
await page.goto('/blogs/thinking-about-ai/');
34+
test('back link navigates to blog index', async ({ page }) => {
35+
// Navigate to any post, then back
36+
await page.goto('/blogs/');
37+
await page.locator('a.blog-card').first().click();
38+
await page.getByRole('link', { name: '← Back to all posts' }).click();
39+
// Wait for the blog index to fully render
40+
await expect(page.locator('a.blog-card').first()).toBeVisible();
41+
});
42+
43+
// ── Blog Post Structure ──
44+
45+
test('blog post has heading, metadata, and prose content', async ({ page }) => {
46+
// Navigate to first available post via the index
47+
await page.goto('/blogs/');
48+
const firstCard = page.locator('a.blog-card').first();
49+
const href = await firstCard.getAttribute('href');
50+
await page.goto(href!);
51+
52+
// Heading
2353
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
24-
// post body should have some prose content
54+
55+
// Metadata eyebrow (author · date)
56+
const eyebrow = page.locator('.blog-post-eyebrow');
57+
await expect(eyebrow).toBeVisible();
58+
await expect(eyebrow).toContainText(/\d{4}/); // has a year
59+
60+
// Prose body is non-empty
2561
await expect(page.locator('.prose')).not.toBeEmpty();
2662
});
63+
64+
test('blog post has tags when present', async ({ page }) => {
65+
await page.goto('/blogs/');
66+
const firstCard = page.locator('a.blog-card').first();
67+
const href = await firstCard.getAttribute('href');
68+
await page.goto(href!);
69+
70+
// Tags section should exist (all current posts have tags)
71+
const tags = page.locator('.blog-post-tags .tag');
72+
const count = await tags.count();
73+
expect(count).toBeGreaterThanOrEqual(1);
74+
});
75+
76+
test('blog post has discussion link', async ({ page }) => {
77+
await page.goto('/blogs/');
78+
const firstCard = page.locator('a.blog-card').first();
79+
const href = await firstCard.getAttribute('href');
80+
await page.goto(href!);
81+
82+
const link = page.getByRole('link', { name: /Discuss this post/i });
83+
await expect(link).toBeVisible();
84+
await expect(link).toHaveAttribute('target', '_blank');
85+
await expect(link).toHaveAttribute('rel', /noopener/);
86+
});
87+
88+
// ── Multiple Posts Render ──
89+
90+
test('all blog posts from index are reachable', async ({ page }) => {
91+
await page.goto('/blogs/');
92+
const cards = page.locator('a.blog-card');
93+
const hrefs: string[] = [];
94+
for (const card of await cards.all()) {
95+
const href = await card.getAttribute('href');
96+
if (href) hrefs.push(href);
97+
}
98+
expect(hrefs.length).toBeGreaterThanOrEqual(2);
99+
100+
// Each post should load and have an h1
101+
for (const href of hrefs) {
102+
await page.goto(href);
103+
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
104+
}
105+
});

tests/e2e/home.spec.ts

Lines changed: 157 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,180 @@
11
import { test, expect } from '@playwright/test';
22

3+
// ── Meta & SEO ──
4+
35
test('homepage has Mellea title', async ({ page }) => {
46
await page.goto('/');
57
await expect(page).toHaveTitle(/Mellea/);
68
});
79

8-
test('hero heading is visible', async ({ page }) => {
10+
test('homepage has meta description', async ({ page }) => {
911
await page.goto('/');
10-
await expect(page.getByRole('heading', { name: /Mellea/i, level: 1 })).toBeVisible();
12+
const desc = page.locator('meta[name="description"]');
13+
await expect(desc).toHaveAttribute('content', /.+/);
1114
});
1215

13-
test('install command is visible', async ({ page }) => {
16+
test('homepage has canonical URL', async ({ page }) => {
1417
await page.goto('/');
15-
await expect(page.getByText('uv pip install mellea')).toBeVisible();
18+
const canonical = page.locator('link[rel="canonical"]');
19+
await expect(canonical).toHaveAttribute('href', /\/$/);
20+
});
21+
22+
// ── Header / Navigation ──
23+
24+
test('header logo links to homepage', async ({ page }) => {
25+
await page.goto('/');
26+
const logo = page.getByRole('banner').getByRole('link', { name: /Mellea/i });
27+
await expect(logo).toBeVisible();
28+
await expect(logo).toHaveAttribute('href', '/');
1629
});
1730

1831
test('nav links are present', async ({ page }) => {
1932
await page.goto('/');
2033
const header = page.getByRole('banner');
34+
await expect(header.getByRole('link', { name: 'Docs' })).toBeVisible();
2135
await expect(header.getByRole('link', { name: 'Blog' })).toBeVisible();
2236
await expect(header.getByRole('link', { name: 'GitHub' })).toBeVisible();
37+
await expect(header.getByRole('link', { name: /Get Started/ })).toBeVisible();
38+
});
39+
40+
test('external nav links open in new tab', async ({ page }) => {
41+
await page.goto('/');
42+
const header = page.getByRole('banner');
43+
for (const name of ['Docs', 'GitHub']) {
44+
const link = header.getByRole('link', { name });
45+
await expect(link).toHaveAttribute('target', '_blank');
46+
await expect(link).toHaveAttribute('rel', /noopener/);
47+
}
48+
});
49+
50+
// ── Hero Section ──
51+
52+
test('hero heading is visible', async ({ page }) => {
53+
await page.goto('/');
54+
await expect(page.getByRole('heading', { name: /Mellea/i, level: 1 })).toBeVisible();
55+
});
56+
57+
test('install command is visible with copy button', async ({ page }) => {
58+
await page.goto('/');
59+
await expect(page.getByText('uv pip install mellea')).toBeVisible();
60+
await expect(page.getByLabel('Copy install command')).toBeVisible();
61+
});
62+
63+
test('hero has Get Started CTA', async ({ page }) => {
64+
await page.goto('/');
65+
const hero = page.locator('.hero');
66+
await expect(hero.getByRole('link', { name: /Get Started/ })).toBeVisible();
67+
});
68+
69+
test('GitHub stats section renders', async ({ page }) => {
70+
await page.goto('/');
71+
const stats = page.locator('.gh-stats');
72+
await expect(stats).toBeVisible();
73+
await expect(stats.getByText(/View on GitHub/)).toBeVisible();
74+
});
75+
76+
// ── Feature Strip ──
77+
78+
test('feature strip has multiple items', async ({ page }) => {
79+
await page.goto('/');
80+
const items = page.locator('.feature-strip .feature-item');
81+
const count = await items.count();
82+
expect(count).toBeGreaterThanOrEqual(3);
83+
});
84+
85+
// ── How It Works Section ──
86+
87+
test('how it works section renders with heading', async ({ page }) => {
88+
await page.goto('/');
89+
await expect(page.getByRole('heading', { name: /How it works/i })).toBeVisible();
90+
});
91+
92+
test('feature cards are visible with learn more links', async ({ page }) => {
93+
await page.goto('/');
94+
const cards = page.locator('.feature-card');
95+
const count = await cards.count();
96+
expect(count).toBeGreaterThanOrEqual(4);
97+
98+
// Each card has a heading and a learn-more link
99+
for (const card of await cards.all()) {
100+
await expect(card.getByRole('heading')).toBeVisible();
101+
const link = card.getByRole('link', { name: /Learn more/ });
102+
await expect(link).toHaveAttribute('target', '_blank');
103+
}
104+
});
105+
106+
// ── Code Showcase ──
107+
108+
test('code showcase renders with tabs', async ({ page }) => {
109+
await page.goto('/');
110+
const tabs = page.locator('[role="tab"]');
111+
const count = await tabs.count();
112+
expect(count).toBeGreaterThanOrEqual(2);
113+
});
114+
115+
test('code showcase tab switching changes code panel', async ({ page }) => {
116+
await page.goto('/');
117+
const panel = page.locator('[role="tabpanel"]');
118+
const tabs = page.locator('[role="tab"]');
119+
120+
// Get initial panel text
121+
const firstContent = await panel.textContent();
122+
123+
// Click second tab — panel content should change
124+
await tabs.nth(1).click();
125+
const secondContent = await panel.textContent();
126+
expect(secondContent).not.toBe(firstContent);
127+
});
128+
129+
test('code showcase has copy button', async ({ page }) => {
130+
await page.goto('/');
131+
await expect(page.getByLabel('Copy code')).toBeVisible();
23132
});
24133

25-
test('recent blog posts section is visible', async ({ page }) => {
134+
test('active tab shows description and learn more link', async ({ page }) => {
135+
await page.goto('/');
136+
const activeItem = page.locator('.showcase-item--active');
137+
await expect(activeItem.locator('.showcase-item-desc')).toBeVisible();
138+
await expect(activeItem.getByRole('link', { name: /Learn more/ })).toBeVisible();
139+
});
140+
141+
// ── Recent Blog Posts ──
142+
143+
test('recent blog posts section has heading and cards', async ({ page }) => {
26144
await page.goto('/');
27145
await expect(page.getByText('From the blog')).toBeVisible();
146+
const cards = page.locator('.blog-grid .blog-card');
147+
const count = await cards.count();
148+
expect(count).toBeGreaterThanOrEqual(1);
149+
});
150+
151+
// ── Vision / CTA Section ──
152+
153+
test('vision section has closing CTAs', async ({ page }) => {
154+
await page.goto('/');
155+
const vision = page.locator('.vision-section');
156+
await expect(vision).toBeVisible();
157+
await expect(vision.getByRole('link', { name: /Get Started/ })).toBeVisible();
158+
await expect(vision.getByRole('link', { name: /GitHub/ })).toBeVisible();
159+
});
160+
161+
// ── Footer ──
162+
163+
test('footer is visible with copyright and links', async ({ page }) => {
164+
await page.goto('/');
165+
const footer = page.getByRole('contentinfo');
166+
await expect(footer).toBeVisible();
167+
await expect(footer).toContainText(/© \d{4}/);
168+
await expect(footer.getByRole('link', { name: 'Blog' })).toBeVisible();
169+
await expect(footer.getByRole('link', { name: 'Docs' })).toBeVisible();
170+
await expect(footer.getByRole('link', { name: 'GitHub' })).toBeVisible();
171+
});
172+
173+
// ── Skip Link (Accessibility) ──
174+
175+
test('skip-to-content link exists', async ({ page }) => {
176+
await page.goto('/');
177+
const skip = page.locator('a.skip-link');
178+
await expect(skip).toHaveCount(1);
179+
await expect(skip).toHaveAttribute('href', '#main-content');
28180
});

0 commit comments

Comments
 (0)