✨ feat: implement new feature for enhanced user experience
This commit is contained in:
139
tests/e2e/fixtures.ts
Normal file
139
tests/e2e/fixtures.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { test as base, expect, type Page } from "@playwright/test"
|
||||
import { ensureTestUser, type TestUserCredentials } from "../api/helpers/test-user"
|
||||
|
||||
const API_BASE = "/api"
|
||||
|
||||
/**
|
||||
* Shared E2E test fixtures.
|
||||
*
|
||||
* Worker-scoped:
|
||||
* - `testUser` – persistent test user (created/reused once per worker)
|
||||
*
|
||||
* Test-scoped:
|
||||
* - `authedPage` – Page with logged-in user (token injection via sessionStorage)
|
||||
* - `accessToken` – raw JWT access token
|
||||
*/
|
||||
type E2eWorkerFixtures = {
|
||||
testUser: TestUserCredentials
|
||||
}
|
||||
|
||||
type E2eFixtures = {
|
||||
authedPage: Page
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export const test = base.extend<E2eFixtures, E2eWorkerFixtures>({
|
||||
/**
|
||||
* Override page fixture: BrowserSync keeps a WebSocket open permanently,
|
||||
* preventing "load" and "networkidle" from resolving. We default all
|
||||
* navigation methods to "domcontentloaded".
|
||||
*/
|
||||
page: async ({ page }, use) => {
|
||||
const origGoto = page.goto.bind(page)
|
||||
const origReload = page.reload.bind(page)
|
||||
const origGoBack = page.goBack.bind(page)
|
||||
const origGoForward = page.goForward.bind(page)
|
||||
|
||||
page.goto = ((url: string, opts?: any) =>
|
||||
origGoto(url, { waitUntil: "domcontentloaded", ...opts })) as typeof page.goto
|
||||
page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload
|
||||
page.goBack = ((opts?: any) => origGoBack({ waitUntil: "domcontentloaded", ...opts })) as typeof page.goBack
|
||||
page.goForward = ((opts?: any) =>
|
||||
origGoForward({ waitUntil: "domcontentloaded", ...opts })) as typeof page.goForward
|
||||
|
||||
await use(page)
|
||||
},
|
||||
|
||||
// Worker-scoped: create/reuse test user once per worker
|
||||
testUser: [
|
||||
async ({ playwright }, use) => {
|
||||
const baseURL = process.env.CODING_URL || "https://localhost:3000"
|
||||
const user = await ensureTestUser(baseURL)
|
||||
await use(user)
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
|
||||
accessToken: async ({ testUser }, use) => {
|
||||
await use(testUser.accessToken)
|
||||
},
|
||||
|
||||
// Test-scoped: Page with logged-in user via sessionStorage token injection
|
||||
authedPage: async ({ page, testUser, baseURL }, use) => {
|
||||
// Navigate to home so domain is set for sessionStorage
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" })
|
||||
await page.waitForLoadState("domcontentloaded")
|
||||
|
||||
// Inject auth token into sessionStorage (adapt key names to your app)
|
||||
await page.evaluate(
|
||||
({ token, user }) => {
|
||||
sessionStorage.setItem("auth_token", token)
|
||||
sessionStorage.setItem(
|
||||
"auth_user",
|
||||
JSON.stringify({
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
})
|
||||
)
|
||||
},
|
||||
{
|
||||
token: testUser.accessToken,
|
||||
user: testUser,
|
||||
}
|
||||
)
|
||||
|
||||
// Reload so the app reads the token from sessionStorage
|
||||
await page.reload({ waitUntil: "domcontentloaded" })
|
||||
await page.waitForLoadState("domcontentloaded")
|
||||
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
|
||||
|
||||
await use(page)
|
||||
},
|
||||
})
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Wait for the SPA to be ready (appContainer rendered).
|
||||
* Returns the detected language prefix from the URL.
|
||||
*/
|
||||
export async function waitForSpaReady(page: Page): Promise<string> {
|
||||
await page.waitForLoadState("domcontentloaded")
|
||||
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
|
||||
const url = page.url()
|
||||
const match = url.match(/\/([a-z]{2})(\/|$)/)
|
||||
return match?.[1] || "de"
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a route, prepending the current language prefix.
|
||||
*/
|
||||
export async function navigateToRoute(page: Page, routePath: string): Promise<void> {
|
||||
const url = page.url()
|
||||
const match = url.match(/\/([a-z]{2})(\/|$)/)
|
||||
const lang = match?.[1] || "de"
|
||||
const fullPath = routePath === "/" ? `/${lang}` : `/${lang}${routePath}`
|
||||
await page.goto(fullPath)
|
||||
await page.waitForLoadState("domcontentloaded")
|
||||
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Click an SPA link and verify no full page reload occurred.
|
||||
*/
|
||||
export async function clickSpaLink(page: Page, linkSelector: string): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
;(window as any).__spa_navigation_marker = true
|
||||
})
|
||||
await page.locator(linkSelector).first().click()
|
||||
await page.waitForLoadState("domcontentloaded")
|
||||
const markerExists = await page.evaluate(() => {
|
||||
return (window as any).__spa_navigation_marker === true
|
||||
})
|
||||
if (!markerExists) {
|
||||
throw new Error(`SPA navigation failed: full page reload detected when clicking "${linkSelector}"`)
|
||||
}
|
||||
}
|
||||
|
||||
export { expect, API_BASE, type Page }
|
||||
39
tests/e2e/home.spec.ts
Normal file
39
tests/e2e/home.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { test, expect, waitForSpaReady } from "./fixtures"
|
||||
|
||||
test.describe("Home Page", () => {
|
||||
test("should load the start page", async ({ page }) => {
|
||||
await page.goto("/de/")
|
||||
const lang = await waitForSpaReady(page)
|
||||
expect(lang).toBe("de")
|
||||
})
|
||||
|
||||
test("should have a visible header with navigation", async ({ page }) => {
|
||||
await page.goto("/de/")
|
||||
await waitForSpaReady(page)
|
||||
|
||||
const header = page.locator("header")
|
||||
await expect(header).toBeVisible()
|
||||
|
||||
const nav = header.locator("nav")
|
||||
await expect(nav).toBeVisible()
|
||||
})
|
||||
|
||||
test("should have language prefix in URL", async ({ page }) => {
|
||||
await page.goto("/de/")
|
||||
await waitForSpaReady(page)
|
||||
expect(page.url()).toContain("/de")
|
||||
})
|
||||
|
||||
test("should switch language to English", async ({ page }) => {
|
||||
await page.goto("/de/")
|
||||
await waitForSpaReady(page)
|
||||
|
||||
// Click on English language link
|
||||
const enLink = page.locator('a[href*="/en"]').first()
|
||||
if (await enLink.isVisible()) {
|
||||
await enLink.click()
|
||||
await page.waitForLoadState("domcontentloaded")
|
||||
expect(page.url()).toContain("/en")
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user