Skip to content

Commit 10296e6

Browse files
Test: Add test for case study detail page (#125)
1 parent 4c0ece9 commit 10296e6

File tree

1 file changed

+364
-0
lines changed

1 file changed

+364
-0
lines changed

e2e/case-study-detail.spec.ts

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
import { test, expect } from "@playwright/test";
2+
import { getCaseStudies } from "@/app/[lang]/(hyperjump)/case-studies/data";
3+
4+
// Base URL
5+
const baseURL = "http://localhost:3000";
6+
7+
// Test matrix: locales x data x devices
8+
const locales = [
9+
...getCaseStudies("en").map((cs) => ({
10+
code: "en",
11+
title: cs.title,
12+
slug: cs.slug,
13+
path: `/en/case-studies/${cs.slug}`
14+
})),
15+
...getCaseStudies("id").map((cs) => ({
16+
code: "id",
17+
title: cs.title,
18+
slug: cs.slug,
19+
path: `/id/case-studies/${cs.slug}`
20+
}))
21+
] as const;
22+
23+
const viewports = [
24+
{ name: "mobile", size: { width: 360, height: 740 } },
25+
{ name: "tablet", size: { width: 820, height: 1180 } },
26+
{ name: "desktop", size: { width: 1280, height: 800 } },
27+
{ name: "large", size: { width: 1536, height: 960 } }
28+
] as const;
29+
30+
// Utility: assert all images load (no broken images)
31+
async function expectAllImagesLoaded(page: import("@playwright/test").Page) {
32+
await page.waitForLoadState("networkidle");
33+
const main = page.locator("main");
34+
const images = main.locator('img, [style*="background-image"], image');
35+
const count = await images.count();
36+
if (count === 0) {
37+
return;
38+
}
39+
for (let i = 0; i < count; i++) {
40+
const el = images.nth(i);
41+
const tag = await el.evaluate((n) => n.tagName.toLowerCase());
42+
if (tag === "img" || tag === "image") {
43+
await expect(el).toBeVisible();
44+
// Ensure naturalWidth > 0
45+
const ok = await el.evaluate(
46+
(img: HTMLImageElement | SVGImageElement) => {
47+
// @ts-ignore
48+
const nw = (img as any).naturalWidth ?? 1; // SVGImageElement may not have naturalWidth
49+
// @ts-ignore
50+
const nh = (img as any).naturalHeight ?? 1;
51+
return (nw > 0 && nh > 0) || (img as any).href?.baseVal; // allow SVG xlink:href
52+
}
53+
);
54+
expect(ok, "image failed to load or has zero natural size").toBeTruthy();
55+
} else {
56+
await expect(el).toBeVisible();
57+
}
58+
}
59+
}
60+
61+
// Utility: get nav and footer link locators
62+
function getHeader(page: import("@playwright/test").Page) {
63+
// Header is sticky nav; fall back to first header/nav region
64+
const header = page.locator("header, nav").first();
65+
return header;
66+
}
67+
68+
function getMenuNav(page: import("@playwright/test").Page) {
69+
return page.locator('nav[aria-label="Main"]');
70+
}
71+
72+
function getFooter(page: import("@playwright/test").Page) {
73+
return page.getByRole("contentinfo");
74+
}
75+
76+
// Utility: navigate and ensure route
77+
async function gotoAndWait(page: import("@playwright/test").Page, url: string) {
78+
await page.goto(url, { waitUntil: "domcontentloaded" });
79+
await expect(page).toHaveURL(
80+
new RegExp(url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
81+
); // escape
82+
}
83+
84+
// Shared assertions for header nav links based on code in app/[lang]/(hyperjump)/components/nav.tsx
85+
const expectedMenuPaths = (locale: string) => [
86+
`/${locale}/services`,
87+
`/${locale}/products`,
88+
`/${locale}/case-studies`,
89+
`/${locale}#faqs`
90+
];
91+
92+
// Hero and list sections selectors from page.tsx
93+
const selectors = {
94+
hero: "#hero",
95+
heroHeading:
96+
"#hero div div[dangerouslysetinnerhtml], #hero h1, #hero .text-3xl",
97+
heroDesc: "#hero p",
98+
exploreHeading: "main h3",
99+
cardsGrid: "section >> .grid",
100+
card: "section .grid > div",
101+
cardButton: "section .grid > div a, section .grid > div button"
102+
};
103+
104+
for (const { code: locale, path, title, slug } of locales) {
105+
test.describe("Case Study Detail Page", () => {
106+
test.describe(`/${slug} -${locale.toUpperCase()} locale`, () => {
107+
test.beforeEach(async ({ page }) => {
108+
await gotoAndWait(page, `${baseURL}${path}`);
109+
});
110+
111+
// 1. Navigation & Links
112+
test.describe("Navigation & Links", () => {
113+
test("header nav links route correctly", async ({ page }) => {
114+
const header = getHeader(page);
115+
await expect(header).toBeVisible();
116+
117+
const menuNav = getMenuNav(page);
118+
const expected = expectedMenuPaths(locale);
119+
for (const href of expected) {
120+
await expect(
121+
menuNav.locator(`a[href='${href}']`).first()
122+
).toBeVisible();
123+
}
124+
125+
// Click-through checks for non-anchor-with-fragment links
126+
for (const href of expected) {
127+
menuNav.locator(`a[href='${href}']`).first().click();
128+
await expect(page).toHaveURL(`${baseURL}${href}`);
129+
// Go back to subject page
130+
if (href !== `/${locale}/case-studies/${slug}`) {
131+
await page.goBack();
132+
await expect(page).toHaveURL(`${baseURL}${path}`);
133+
}
134+
}
135+
});
136+
137+
test("footer links and social icons are visible and valid", async ({
138+
page
139+
}) => {
140+
const footer = getFooter(page);
141+
await expect(footer).toBeVisible();
142+
143+
const footerLinks = footer.getByRole("link");
144+
await expect(footerLinks.first()).toBeVisible();
145+
146+
// Ensure each link has href
147+
const count = await footerLinks.count();
148+
for (let i = 0; i < count; i++) {
149+
const link = footerLinks.nth(i);
150+
const href = await link.getAttribute("href");
151+
expect(href, "footer link should have href").toBeTruthy();
152+
}
153+
});
154+
155+
test("content buttons link to intended destinations", async ({
156+
page
157+
}) => {
158+
const cardLinks = page.locator(selectors.cardButton);
159+
const n = await cardLinks.count();
160+
for (let i = 0; i < n; i++) {
161+
const link = cardLinks.nth(i);
162+
await expect(link).toBeVisible();
163+
const href = await link.getAttribute("href");
164+
expect(href).toBeTruthy();
165+
// Only test one click to avoid navigating away multiple times unnecessarily
166+
if (i === 0 && href) {
167+
await link.click();
168+
await expect(page).toHaveURL(
169+
new RegExp(`${href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)
170+
);
171+
await page.goBack();
172+
await expect(page).toHaveURL(`${baseURL}${path}`);
173+
}
174+
}
175+
});
176+
});
177+
178+
// 2. Branding
179+
test.describe("Branding", () => {
180+
test("logo visible in header and links to home", async ({ page }) => {
181+
const header = getHeader(page);
182+
const logo = header
183+
.getByRole("link")
184+
.filter({ has: page.getByAltText("Hyperjump Logo") })
185+
.first();
186+
await expect(logo).toBeVisible();
187+
188+
const href = await logo.getAttribute("href");
189+
expect(href).toBe(`/${locale}`);
190+
});
191+
192+
test("logo visible in footer and links to home", async ({ page }) => {
193+
const footer = getFooter(page);
194+
const logo = footer
195+
.getByRole("link")
196+
.filter({ has: page.getByAltText("Hyperjump Logo") })
197+
.first();
198+
await expect(logo).toBeVisible();
199+
const href = await logo.getAttribute("href");
200+
expect(href).toBe(`/${locale}`);
201+
});
202+
});
203+
204+
// 3. Language Switching
205+
test.describe("Language Switching", () => {
206+
test("switch language to the other locale and back", async ({
207+
page
208+
}) => {
209+
const footer = getFooter(page);
210+
const select = footer.getByRole("combobox");
211+
await expect(select).toBeVisible();
212+
213+
// Current value should be locale
214+
await expect(select).toHaveValue(locale);
215+
216+
const other = locale === "en" ? "id" : "en";
217+
await select.selectOption(other);
218+
await page.waitForURL(new RegExp(`/(${other})/case-studies`));
219+
220+
// Verify content changes (hero heading changes with locale)
221+
const heading = page
222+
.locator("#hero")
223+
.locator(
224+
".text-3xl, .text-4xl, [class*='text-'][class*='font-medium']"
225+
)
226+
.first();
227+
await expect(heading).toBeVisible();
228+
229+
// Switch back
230+
const select2 = getFooter(page).getByRole("combobox");
231+
await select2.selectOption(locale);
232+
await page.waitForURL(new RegExp(`/(${locale})/case-studies`));
233+
});
234+
});
235+
236+
// 4. Images
237+
test.describe("Images", () => {
238+
test("all images load without errors", async ({ page }) => {
239+
await expectAllImagesLoaded(page);
240+
});
241+
});
242+
243+
// 5. Text & Content
244+
test.describe("Text & Content", () => {
245+
test("hero section visible with heading and description", async ({
246+
page
247+
}) => {
248+
const hero = page.locator(selectors.hero);
249+
await expect(hero).toBeVisible();
250+
251+
// Heading and description visible
252+
await expect(
253+
hero
254+
.locator(
255+
".text-3xl, .text-4xl, [class*='text-'][class*='font-medium']"
256+
)
257+
.first()
258+
).toBeVisible();
259+
260+
await expect(hero).toHaveText(title);
261+
});
262+
263+
test("content are visible", async ({ page }) => {
264+
const article = page.locator("article");
265+
await expect(article).toBeVisible();
266+
267+
const articleHeadings = article.locator("h2");
268+
const articleHeadingsCount = await articleHeadings.count();
269+
for (let i = 0; i < articleHeadingsCount; i++) {
270+
const h2 = articleHeadings.nth(i);
271+
await expect(h2).toBeVisible();
272+
await expect(h2).not.toBeEmpty();
273+
}
274+
275+
const articleParagraphs = article.locator("p");
276+
const articleParagraphsCount = await articleParagraphs.count();
277+
for (let i = 0; i < articleParagraphsCount; i++) {
278+
const p = articleParagraphs.nth(i);
279+
await expect(p).toBeVisible();
280+
await expect(p).not.toBeEmpty();
281+
}
282+
});
283+
284+
test("footer text visible", async ({ page }) => {
285+
const footer = getFooter(page);
286+
await expect(footer).toBeVisible();
287+
await expect(footer.locator("p").first()).toBeVisible();
288+
});
289+
});
290+
291+
// 6. SEO & Metadata
292+
test.describe("SEO & Metadata", () => {
293+
test("meta title and description are set correctly", async ({
294+
page
295+
}) => {
296+
const title = await page.title();
297+
expect(title).toMatch(title);
298+
299+
const metaDesc = await page
300+
.locator('head meta[name="description"]')
301+
.getAttribute("content");
302+
expect(metaDesc).toBeTruthy();
303+
// Basic sanity: should include hero description text excerpt
304+
const bodyText = await page.locator("body").innerText();
305+
expect(bodyText.length).toBeGreaterThan(50);
306+
});
307+
});
308+
309+
// 7. Test Structure by UI sections
310+
test.describe("Sections", () => {
311+
test("Header", async ({ page }) => {
312+
const header = getHeader(page);
313+
await expect(header).toBeVisible();
314+
});
315+
316+
test("Hero Section", async ({ page }) => {
317+
const hero = page.locator(selectors.hero);
318+
await expect(hero).toBeVisible();
319+
});
320+
321+
test("Case Studies Section", async ({ page }) => {
322+
const grid = page.locator(selectors.cardsGrid);
323+
await expect(grid).toBeVisible();
324+
});
325+
326+
test("Footer", async ({ page }) => {
327+
const footer = getFooter(page);
328+
await expect(footer).toBeVisible();
329+
});
330+
});
331+
332+
// 8. Responsive Design
333+
test.describe("Responsive Design", () => {
334+
for (const viewport of viewports) {
335+
test(`layout renders correctly at ${viewport.name} (${viewport.size.width}x${viewport.size.height})`, async ({
336+
browser
337+
}) => {
338+
const context = await browser.newContext({
339+
viewport: viewport.size
340+
});
341+
const page = await context.newPage();
342+
await page.goto(`${baseURL}${path}`, {
343+
waitUntil: "domcontentloaded"
344+
});
345+
346+
const header = getHeader(page);
347+
await expect(header).toBeVisible();
348+
349+
const hero = page.locator(selectors.hero);
350+
await expect(hero).toBeVisible();
351+
352+
const grid = page.locator(selectors.cardsGrid);
353+
await expect(grid).toBeVisible();
354+
355+
const footer = getFooter(page);
356+
await expect(footer).toBeVisible();
357+
358+
await context.close();
359+
});
360+
}
361+
});
362+
});
363+
});
364+
}

0 commit comments

Comments
 (0)