chore: seminarhof customer project

This commit is contained in:
e2e
2026-06-21 22:23:43 +02:00
commit e0331cbb1c
161 changed files with 18517 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
import { Given, When, Then } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import type { AppWorld } from '../support/world.js'
/**
* Generic step library — shared across every project that copies this harness.
*
* Project-specific business steps (e.g. "I create a booking for <date>")
* belong in their own *.steps.ts files. Keep this file framework-agnostic.
*/
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()
})
/**
* Resolve a "field" by trying multiple strategies. CSS selectors win when the
* name looks like one. Otherwise: data-testid, then accessible label, then
* placeholder. Mantine sometimes parks data-testid on the wrapper div, so we
* descend to the inner control when present.
*/
const looksLikeCss = (s: string) => /[\[#.>:\s]/.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()
// Wait for the testid to attach — page often paints before React Query
// resolves, so a synchronous count() check fires too early.
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 to label / placeholder.
}
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<AppWorld['page']['locator']>,
timeout = 5000,
) {
try {
await target.waitFor({ state: 'visible', timeout })
return true
} catch {
return false
}
}
async function activate(target: ReturnType<AppWorld['page']['locator']>, 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<AppWorld['page']['locator']>) {
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) {
console.log(`[select] resolving "${name}"`)
const target = await field(this, name)
const tag = await target
.evaluate((el) => (el as HTMLElement).tagName.toLowerCase())
.catch(() => '')
console.log(`[select] resolved → ${tag}`)
if (tag === 'select') {
await target.selectOption({ label: value })
return
}
console.log(`[select] clicking to open`)
await target.click()
console.log(`[select] clicking option "${value}"`)
await this.page
.getByRole('option', { name: value, exact: false })
.first()
.click()
console.log(`[select] done`)
},
)
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) {
// 1) data-testid first — feature files use this for testid'd row actions.
const byId = this.page.getByTestId(label).first()
if (await isVisibleWithin(byId)) {
await clickAndConfirmIfNeeded(this, byId)
return
}
// 2) Common interactive roles. Mantine renders tabs with role=tab,
// anchors and buttons-as-links with role=link, etc.
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
}
}
// 3) Plain text fallback for click-handler-on-div widgets.
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())
})
When(
'I answer the next prompt with {string}',
async function (this: AppWorld, value: string) {
await this.page.evaluate((v) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).prompt = () => v
}, value)
},
)
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))
},
)