Files
seminarhof-e2e-mqo0qs8e/e2e/tests/support/world.ts
2026-06-21 18:46:29 +02:00

129 lines
4.1 KiB
TypeScript

import { World, setWorldConstructor } from '@cucumber/cucumber'
import {
chromium,
type Browser,
type BrowserContext,
type Page,
} from '@playwright/test'
import { config } from './types.js'
/**
* AppWorld — generic Playwright world for portal-style apps.
*
* Exposes the primitives every feature needs:
* - openBrowser / closeBrowser — lifecycle
* - gotoApp(path) — navigate within the frontend
* - login(email, password) — fill + submit /login, wait for redirect
* - logout() — visit /logout
* - expectText(text) — assert visible text
*
* Project-specific helpers (e.g. createBooking, assignRoom) belong in
* step files, not here. Keep this generic.
*/
export class AppWorld extends World {
browser!: Browser
context!: BrowserContext
page!: Page
async openBrowser() {
const headed = process.env.HEADED === '1' || process.env.HEADED === 'true'
this.browser = await chromium.launch({
headless: !headed,
slowMo: headed ? 150 : 0,
})
this.context = await this.browser.newContext({
locale: process.env.E2E_LOCALE ?? 'de-DE',
})
await this.context.addInitScript((apiUrl) => {
;(window as typeof window & { __E2E_API_URL?: string }).__E2E_API_URL = apiUrl
}, config.apiUrl)
this.page = await this.context.newPage()
this.page.setDefaultTimeout(config.responseTimeout)
}
async closeBrowser() {
await this.context?.close()
await this.browser?.close()
}
async gotoApp(path: string) {
const url = path.startsWith('http')
? path
: `${config.appUrl}${path.startsWith('/') ? path : `/${path}`}`
await this.page.goto(url)
await this.page.waitForFunction(
() => document.documentElement.dataset.appHydrated === 'true',
undefined,
{ timeout: config.responseTimeout },
)
await this.page.waitForLoadState('networkidle', { timeout: 1000 }).catch(() => {})
await this.page.waitForTimeout(500)
}
/**
* Fills the login form on /login and waits for navigation away from it.
* Assumes the login form has inputs named/typed `email` and a password
* input. Override per-project if the form differs.
*/
async login(email: string, password: string) {
await this.gotoApp('/login')
await this.page.locator('input[type="email"]').fill(email)
await this.page.locator('input[type="password"]').fill(password)
await this.page.getByRole('button', { name: /(sign in|login|anmelden)/i }).click()
await this.page.waitForURL((url) => !url.pathname.startsWith('/login'), {
timeout: config.responseTimeout,
})
}
async logout() {
await this.gotoApp('/logout')
}
// Calls an RPC through the browser page so session cookies are included.
async callRpc(name: string, data: Record<string, unknown> = {}) {
const apiUrl = config.apiUrl
const result = await this.page.evaluate(
async ({ apiUrl, name, data }) => {
const res = await fetch(`${apiUrl}/rpc/${name}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ data }),
})
if (!res.ok) {
const body = await res.text().catch(() => '')
throw new Error(`RPC ${name} failed (${res.status}): ${body}`)
}
return res.json()
},
{ apiUrl, name, data },
)
return result
}
async runLifecycle() {
return this.callRpc('runBookingLifecycleNow', {})
}
async expectText(text: string) {
const locator = this.page.getByText(text, { exact: false })
const deadline = Date.now() + config.responseTimeout
while (Date.now() < deadline) {
const count = await locator.count()
for (let i = 0; i < count; i++) {
if (await locator.nth(i).isVisible().catch(() => false)) {
return
}
}
await this.page.waitForTimeout(100)
}
throw new Error(`Timed out waiting for visible text: ${text}`)
}
async getPageText(): Promise<string> {
return this.page.innerText('body')
}
}
setWorldConstructor(AppWorld)