Skip to content

Commit a152bf9

Browse files
Test: Add test for Case Studies Page (#122)
1 parent 80114ec commit a152bf9

File tree

2 files changed

+344
-2
lines changed

2 files changed

+344
-2
lines changed

app/components/sticky-nav-main.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export default function StickyNavigationMain({
88
children
99
}: StickyNavigationProps) {
1010
return (
11-
<nav className="fixed top-0 right-0 left-0 z-50 shadow-xs backdrop-blur-[3px] transition duration-300 group-data-[scroll=false]:bg-transparent group-data-[scroll=true]:bg-white">
11+
<header className="fixed top-0 right-0 left-0 z-50 shadow-xs backdrop-blur-[3px] transition duration-300 group-data-[scroll=false]:bg-transparent group-data-[scroll=true]:bg-white">
1212
{children}
13-
</nav>
13+
</header>
1414
);
1515
}

e2e/case-studies.spec.ts

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

0 commit comments

Comments
 (0)