import { Given, When, Then } from '@cucumber/cucumber' import { expect } from '@playwright/test' import type { AppWorld } from '../support/world.js' Given('I visit {string}', async function (this: AppWorld, path: string) { await this.gotoApp(path) }) Given( 'I am logged in as {string} with password {string}', async function (this: AppWorld, email: string, password: string) { await this.login(email, password) }, ) When('I log in as {string} with password {string}', async function ( this: AppWorld, email: string, password: string, ) { await this.login(email, password) }) When('I log out', async function (this: AppWorld) { await this.logout() }) const looksLikeCss = (s: string) => /[\[#.>:]/.test(s) || s.startsWith('input') const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') async function field(world: AppWorld, name: string) { if (looksLikeCss(name)) return world.page.locator(name).first() const byId = world.page.locator(`[data-testid="${name}"]`).first() try { await byId.waitFor({ state: 'attached', timeout: 5000 }) const inner = byId.locator('input, textarea, select').first() if (await inner.count()) return inner return byId } catch { // fall through } const byLabel = world.page.getByLabel(name, { exact: false }) if (await byLabel.count()) return byLabel.first() return world.page.getByPlaceholder(name, { exact: false }).first() } async function isVisibleWithin( target: ReturnType, timeout = 5000, ) { try { await target.waitFor({ state: 'visible', timeout }) return true } catch { return false } } async function activate(target: ReturnType, timeout = 5000) { try { await target.click({ timeout }) return } catch { await target.evaluate((node) => { ;(node as HTMLElement).click() }) } } async function settleAfterClick(world: AppWorld, previousUrl?: string) { if (previousUrl) { await world.page .waitForURL((url) => url.toString() !== previousUrl, { timeout: 3000 }) .catch(() => {}) } await world.page.waitForLoadState('networkidle', { timeout: 1000 }).catch(() => {}) await world.page.waitForTimeout(750) } async function clickAndConfirmIfNeeded(world: AppWorld, target: ReturnType) { await activate(target) await settleAfterClick(world) if ( await isVisibleWithin( world.page.getByRole('button', { name: /bestätigen\?|confirm\?/i }).first(), 1000, ) ) { await activate( world.page.getByRole('button', { name: /bestätigen\?|confirm\?/i }).first(), 1000, ) await world.page.waitForLoadState('networkidle', { timeout: 1000 }).catch(() => {}) await world.page.waitForTimeout(250) return } if ( await isVisibleWithin( world.page.getByText(/bestätigen\?|confirm\?/i, { exact: false }).first(), 1000, ) ) { await activate( world.page.getByText(/bestätigen\?|confirm\?/i, { exact: false }).first(), 1000, ) await world.page.waitForLoadState('networkidle', { timeout: 1000 }).catch(() => {}) await world.page.waitForTimeout(250) } } When( 'I fill {string} with {string}', async function (this: AppWorld, name: string, value: string) { const target = await field(this, name) await target.fill('') await target.fill(value) }, ) When('I check {string}', async function (this: AppWorld, name: string) { const target = await field(this, name) await target.check() }) When('I uncheck {string}', async function (this: AppWorld, name: string) { const target = await field(this, name) await target.uncheck() }) When( 'I select {string} from {string}', async function (this: AppWorld, value: string, name: string) { const target = await field(this, name) const tag = await target .evaluate((el) => (el as HTMLElement).tagName.toLowerCase()) .catch(() => '') if (tag === 'select') { await target.selectOption({ label: value }) return } await target.click() await this.page.getByRole('option', { name: value, exact: false }).first().click() }, ) Then( 'in row {string} I do not see {string}', async function (this: AppWorld, rowText: string, text: string) { const row = this.page.getByRole('row', { name: new RegExp(rowText) }) await expect(row.getByText(text, { exact: false }).first()).toBeHidden() }, ) When( 'in row {string} I click {string}', async function (this: AppWorld, rowText: string, label: string) { const row = this.page.getByRole('row', { name: new RegExp(rowText) }) const roles = ['button', 'link', 'menuitem'] as const const previousUrl = this.page.url() for (const role of roles) { const candidate = row.getByRole(role, { name: new RegExp(label, 'i') }).first() if (await isVisibleWithin(candidate)) { await activate(candidate) await settleAfterClick(this, previousUrl) return } } const byText = row.getByText(label, { exact: false }).first() if (await isVisibleWithin(byText)) { await activate(byText) await settleAfterClick(this, previousUrl) return } throw new Error(`Could not click "${label}" inside row "${rowText}"`) }, ) When('I click {string}', async function (this: AppWorld, label: string) { const byId = this.page.getByTestId(label).first() if (await isVisibleWithin(byId)) { await clickAndConfirmIfNeeded(this, byId) return } const fuzzyName = new RegExp(escapeRegex(label), 'i') const roles = ['button', 'tab', 'link', 'menuitem'] as const for (const role of roles) { const candidate = this.page.getByRole(role, { name: fuzzyName }).first() if (await isVisibleWithin(candidate)) { await clickAndConfirmIfNeeded(this, candidate) return } } const byText = this.page.getByText(label, { exact: false }).first() if (await isVisibleWithin(byText)) { await clickAndConfirmIfNeeded(this, byText) return } throw new Error(`Could not click "${label}"`) }) When('I accept the next confirmation', async function (this: AppWorld) { this.page.once('dialog', (d) => void d.accept()) }) Then('I see {string}', async function (this: AppWorld, text: string) { await this.expectText(text) }) Then( 'the field {string} has value {string}', async function (this: AppWorld, name: string, value: string) { const target = await field(this, name) await expect(target).toHaveValue(value) }, ) Then('I do not see {string}', async function (this: AppWorld, text: string) { await expect( this.page.getByText(text, { exact: false }).first(), ).toBeHidden() }) Then('the URL contains {string}', async function (this: AppWorld, fragment: string) { await this.page.waitForURL((url) => url.toString().includes(fragment)) }) Then( 'the URL does not contain {string}', async function (this: AppWorld, fragment: string) { await this.page.waitForURL((url) => !url.toString().includes(fragment)) }, )