import { World, setWorldConstructor } from '@cucumber/cucumber' import { chromium, type Browser, type BrowserContext, type Page, } from '@playwright/test' import { config } from './types.js' 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) } async login(email: string, password: string) { // Use Playwright's request API (same cookie jar, no SameSite restrictions) // so CSRF double-submit works across the auth-js proxy boundary. const csrfRes = await this.page.request.get(`${config.apiUrl}/auth/csrf`) const { csrfToken } = (await csrfRes.json()) as { csrfToken: string } const body = new URLSearchParams({ email, password, csrfToken, callbackUrl: '/' }) await this.page.request.post(`${config.apiUrl}/auth/callback/credentials`, { headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: body.toString(), }) // Navigate to the app after auth cookies are set await this.gotoApp('/companies') } async logout() { await this.gotoApp('/logout') } async callRpc(name: string, data: Record = {}) { 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 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 { return this.page.innerText('body') } } setWorldConstructor(AppWorld)