Skip to content

Commit 278a103

Browse files
authored
Add: Add test for service list page (#128)
* Add: Add test for service list page * fix test
1 parent 7570405 commit 278a103

File tree

1 file changed

+331
-0
lines changed

1 file changed

+331
-0
lines changed

e2e/services.spec.ts

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

0 commit comments

Comments
 (0)