import { Given, Then, When } from '@cucumber/cucumber' import { expect, type Locator } from '@playwright/test' import { dirname, resolve } from 'path' import { fileURLToPath } from 'url' import type { AppWorld } from '../support/world.js' const looksLikeCss = (s: string) => s.startsWith('#') || s.startsWith('.') || s.startsWith('[') || s.startsWith('input') || s.includes('>') || s.includes(':') const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const __dirname = dirname(fileURLToPath(import.meta.url)) const repoRoot = resolve(__dirname, '../../..') const uploadFixturePath = resolve(repoRoot, 'apps/website/public/favicon-16x16.png') 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() if (await byId.count()) { const inner = byId.locator('input, textarea, select').first() if (await inner.count()) return inner return byId } const byLabel = world.page.getByLabel(name, { exact: false }) if (await byLabel.count()) return firstVisible(byLabel) const byPlaceholder = world.page.getByPlaceholder(name, { exact: false }) if (await byPlaceholder.count()) return firstVisible(byPlaceholder) return world.page.getByText(name, { exact: false }).first() } async function isVisibleWithin( target: ReturnType, timeout = 5_000, ) { try { await target.waitFor({ state: 'visible', timeout }) return true } catch { return false } } async function activate(target: ReturnType, timeout = 5_000) { try { await target.click({ timeout }) return } catch { await target.evaluate((node) => { ;(node as HTMLElement).click() }) } } async function firstVisible(target: ReturnType) { return await nthVisible(target, 0) } async function nthVisible( target: ReturnType, visibleIndex: number, ) { const count = await target.count() let seenVisible = 0 for (let index = 0; index < count; index += 1) { const candidate = target.nth(index) if (await candidate.isVisible().catch(() => false)) { if (seenVisible === visibleIndex) { return candidate } seenVisible += 1 } } return target.first() } async function visibleOption(world: AppWorld, name: string, visibleIndex = 0) { return nthVisible( world.page.getByRole('option', { name: new RegExp(`^${escapeRegex(name)}$`, 'i'), }), visibleIndex, ) } async function settle(world: AppWorld) { await world.page.waitForLoadState('networkidle', { timeout: 2_000 }).catch(() => {}) await world.page.waitForTimeout(250) } async function useActor(world: AppWorld, actor: string) { await world.useActor(actor) } async function labeledComboboxControl(world: AppWorld, label: string) { const name = new RegExp(escapeRegex(label), 'i') const byCombobox = world.page.getByRole('combobox', { name }) if (await byCombobox.count()) { return firstVisible(byCombobox) } const byTextbox = world.page.getByRole('textbox', { name }) if (await byTextbox.count()) { return firstVisible(byTextbox) } return firstVisible(world.page.getByLabel(label, { exact: false })) } async function selectComboboxOption( world: AppWorld, label: string, search: string, option = search, ) { const input = await labeledComboboxControl(world, label) let lastVisibleOptions: string[] = [] let lastListId = '' const isReadonly = await input .evaluate((node) => node instanceof HTMLInputElement && node.readOnly) .catch(() => false) await expect(input).toBeEnabled({ timeout: 5_000 }) await input.click() if (!isReadonly) { await input.fill('') await input.fill(search) await world.page.waitForTimeout(150) } lastListId = (await input.getAttribute('aria-controls').catch(() => '')) ?? '' if (!lastListId) { await input.press('ArrowDown').catch(() => {}) await world.page.waitForTimeout(150) lastListId = (await input.getAttribute('aria-controls').catch(() => '')) ?? '' } const optionsRoot = lastListId ? world.page.locator(`#${lastListId}`) : world.page lastVisibleOptions = await optionsRoot .locator('[role="option"]') .evaluateAll((nodes) => nodes .map((node) => (node as HTMLElement).innerText.trim()) .filter((value): value is string => Boolean(value)) .slice(0, 15), ) .catch(() => []) const optionLocator = lastListId ? optionsRoot .getByRole('option', { name: new RegExp(`^${escapeRegex(option)}$`, 'i'), }) .first() : await visibleOption(world, option) const fuzzyOptionLocator = optionsRoot .locator('[role="option"]') .filter({ hasText: new RegExp(escapeRegex(search), 'i') }) .first() if (await isVisibleWithin(optionLocator, 3_000)) { await optionLocator.click() await settle(world) if (!isReadonly || (await input.inputValue().catch(() => '')).trim()) { return } } if (await isVisibleWithin(fuzzyOptionLocator, 1_500)) { await fuzzyOptionLocator.click() await settle(world) if (!isReadonly || (await input.inputValue().catch(() => '')).trim()) { return } } await input.press('ArrowDown') await input.press('Enter') await settle(world) if ((await input.inputValue().catch(() => '')).trim()) { return } throw new Error( `Could not select option "${option}" from "${label}". Listbox: ${lastListId || 'none'}. Visible options after typing: ${lastVisibleOptions.join(' | ') || 'none'}`, ) } async function selectCountryOption(world: AppWorld, label: string, country: string) { const input = await labeledComboboxControl(world, label) let lastVisibleOptions: string[] = [] let lastListId = '' await expect(input).toBeEnabled({ timeout: 5_000 }) for (let attempt = 0; attempt < 2; attempt += 1) { await input.click() await input.fill('') await input.fill(country) await world.page.waitForTimeout(150) lastListId = (await input.getAttribute('aria-controls').catch(() => '')) ?? '' if (!lastListId) { await input.press('ArrowDown').catch(() => {}) await world.page.waitForTimeout(150) lastListId = (await input.getAttribute('aria-controls').catch(() => '')) ?? '' } const optionsRoot = lastListId ? world.page.locator(`#${lastListId}`) : world.page lastVisibleOptions = await optionsRoot .locator('[role="option"]') .evaluateAll((nodes) => nodes .map((node) => (node as HTMLElement).innerText.trim()) .filter((value): value is string => Boolean(value)) .slice(0, 20), ) .catch(() => []) const optionLocator = lastListId ? optionsRoot .getByRole('option', { name: new RegExp(`^${escapeRegex(country)}$`, 'i'), }) .first() : await visibleOption(world, country) await optionLocator.waitFor({ state: 'visible', timeout: 5_000 }).catch(() => {}) if (await optionLocator.isVisible().catch(() => false)) { await optionLocator.click() } else { await input.press('ArrowDown').catch(() => {}) await input.press('Enter').catch(() => {}) } await input.blur().catch(() => {}) await settle(world) if ((await input.inputValue().catch(() => '')).trim().toLowerCase() === country.toLowerCase()) { return } } throw new Error( `Could not select country "${country}" from "${label}". Listbox: ${lastListId || 'none'}. Visible options after typing: ${lastVisibleOptions.join(' | ') || 'none'}`, ) } async function chooseOptionFromControl( world: AppWorld, control: Locator, option: string, ) { await control.click({ force: true }) const tagName = await control.evaluate((node) => node.tagName.toLowerCase()) if (tagName === 'input') { await control.fill('').catch(() => {}) await control.fill(option).catch(() => {}) } const optionLocator = await visibleOption(world, option) if (await isVisibleWithin(optionLocator, 1_500)) { await optionLocator.click() await settle(world) return true } return false } async function setContainerValue( world: AppWorld, container: Locator, value: string, ) { const control = await firstVisible(container.locator('input, textarea, button')) await control.scrollIntoViewIfNeeded().catch(() => {}) if (await chooseOptionFromControl(world, control, value)) { return } const tagName = await control.evaluate((node) => node.tagName.toLowerCase()) if (tagName === 'input' || tagName === 'textarea') { await control.fill('').catch(() => {}) await control.fill(value) await control.blur().catch(() => {}) await world.page.waitForTimeout(700) await settle(world) return } throw new Error(`Could not set value "${value}" in the selected control`) } async function uploadCurrentStepFile(world: AppWorld, filePath = uploadFixturePath) { const input = world.page.locator('input[type="file"]').first() await input.setInputFiles(filePath) await world.page.getByText('favicon-16x16.png', { exact: false }).last().waitFor({ state: 'visible', }) await settle(world) } async function isUploadStepVisible(world: AppWorld) { return isVisibleWithin(world.page.getByRole('button', { name: /^Choose$/i }).first(), 1_500) } async function advanceToUploadStep(world: AppWorld) { await clickPrimaryStepButton(world, 'Next') if (await isUploadStepVisible(world)) { return } const stateErrorVisible = await world.page .getByText('Please select at least one state.', { exact: false }) .first() .isVisible() .catch(() => false) if (stateErrorVisible) { await selectComboboxOption(world, 'Where would you like to work in Germany?', 'Berlin') await world.page.keyboard.press('Escape').catch(() => {}) await settle(world) await clickPrimaryStepButton(world, 'Next') } await expectAdvancedToUploadStep(world) } async function expectAdvancedToUploadStep(world: AppWorld) { if (await isUploadStepVisible(world)) { return } const visibleMessages = ( await Promise.all([ 'Please select at least one state.', 'Please select an option.', 'You must select a valid country.', 'jobs.basicinfo.validation.dateofbirthrequired', ].map(async (message) => (await world.page.getByText(message, { exact: false }).first().isVisible().catch(() => false)) ? message : null )) ).filter(Boolean) const workLocationInput = world.page .getByLabel('Where would you like to work in Germany?', { exact: false }) .first() const dateInput = world.page.locator('input[data-dates-input="true"]').first() const countryInput = world.page .getByLabel('In which country did you finish your nursing education?', { exact: false }) .first() const selectedStates = await world.page .locator('[data-combobox-pill]') .evaluateAll((nodes) => nodes .map((node) => node.textContent?.trim() ?? '') .filter(Boolean), ) .catch(() => []) const radioStates = { yes: await Promise.all( [0, 1, 2].map((index) => world.page .getByRole('radio', { name: /^Yes$/i }) .nth(index) .isChecked() .catch(() => null), ), ), no: await Promise.all( [0, 1, 2].map((index) => world.page .getByRole('radio', { name: /^No$/i }) .nth(index) .isChecked() .catch(() => null), ), ), } const controlState = { url: world.page.url(), workLocationValue: await workLocationInput.inputValue().catch(() => ''), selectedStates, dateValue: await dateInput.inputValue().catch(() => ''), countryValue: await countryInput.inputValue().catch(() => ''), radioStates, } const bodyText = (await world.page.locator('body').innerText()).replace(/\s+/g, ' ').slice(0, 500) throw new Error( `Application did not advance to the upload step. Visible validation messages: ${visibleMessages.join(' | ') || 'none'}. Control state: ${JSON.stringify(controlState)}. Body excerpt: ${bodyText}`, ) } async function selectVisibleRadioByName( world: AppWorld, name: string, visibleIndex: number, ) { const radio = await nthVisible(world.page.getByRole('radio', { name: new RegExp(`^${escapeRegex(name)}$`, 'i') }), visibleIndex) const id = await radio.getAttribute('id') if (id) { await world.page.locator(`label[for="${id}"]`).click() return } await radio.click({ force: true }) } async function clickPrimaryStepButton(world: AppWorld, label: 'Next' | 'Submit') { await world.page.getByRole('button', { name: new RegExp(`^${label}$`, 'i') }).click() await settle(world) } async function startCandidateApplication(world: AppWorld) { const suffix = Date.now().toString(36) world.candidateName = `E2E Candidate ${suffix}` world.candidateEmail = `e2e-candidate-${suffix}@heygermany.test` await world.page.getByLabel('Full Name', { exact: false }).fill(world.candidateName) await world.page.getByLabel('E-Mail', { exact: false }).fill(world.candidateEmail) await world.page.locator('#terms').check() await world.page.getByRole('button', { name: /^Check My Qualification$/i }).click() await world.page.waitForURL((url) => url.toString().includes('/jobs/application')) await settle(world) } async function completeCandidateApplication(world: AppWorld) { await selectComboboxOption(world, 'Where would you like to work in Germany?', 'Berlin') await world.page.keyboard.press('Escape').catch(() => {}) await settle(world) await selectVisibleRadioByName(world, 'Yes', 0) await selectVisibleRadioByName(world, 'No', 1) await selectVisibleRadioByName(world, 'No', 2) const dateInput = await firstVisible(world.page.locator('input[data-dates-input="true"]')) await dateInput.fill('01/15/1990') await selectCountryOption( world, 'In which country did you finish your nursing education?', 'Philippines', ) await advanceToUploadStep(world) await uploadCurrentStepFile(world) await clickPrimaryStepButton(world, 'Next') await world.page.locator('#license-not-provided').check() await clickPrimaryStepButton(world, 'Next') await world.page.getByText("I don't have a language certificate yet.", { exact: false }).click() await selectComboboxOption(world, 'Level', 'B1') await clickPrimaryStepButton(world, 'Next') const submitButton = world.page.getByRole('button', { name: /^Submit(?: Application)?$/i, }) if (!(await isVisibleWithin(submitButton, 5_000))) { const visibleButtons = await world.page .getByRole('button') .evaluateAll((nodes) => nodes .map((node) => (node as HTMLElement).innerText.trim()) .filter(Boolean), ) .catch(() => []) const visibleHeadings = await world.page .locator('h1, h2, h3') .evaluateAll((nodes) => nodes .map((node) => (node as HTMLElement).innerText.trim()) .filter(Boolean), ) .catch(() => []) const levelValue = await world.page .getByRole('textbox', { name: /Level/i }) .first() .inputValue() .catch(() => '') const bodyText = (await world.page.locator('body').innerText()).replace(/\s+/g, ' ').slice(0, 600) throw new Error( `Application did not reach the final submit state. Level value: ${levelValue || 'empty'}. Visible headings: ${visibleHeadings.join(' | ') || 'none'}. Visible buttons: ${visibleButtons.join(' | ') || 'none'}. Body excerpt: ${bodyText}`, ) } await submitButton.click() await settle(world) } Given('I visit {string}', async function (this: AppWorld, path: string) { await this.gotoApp(path) }) Given( 'the {string} actor visits {string}', async function (this: AppWorld, actor: string, path: string) { await useActor(this, actor) await this.gotoApp(path) }, ) When( 'I log in as {string} with password {string}', async function (this: AppWorld, email: string, password: string) { await this.login(email, password) }, ) When( 'the {string} actor logs in as {string} with password {string}', async function (this: AppWorld, actor: string, email: string, password: string) { await useActor(this, actor) await this.login(email, password) }, ) When('I log out', async function (this: AppWorld) { await this.gotoApp('/en/logout') }) When('I wait for {int} milliseconds', async function (this: AppWorld, ms: number) { await this.page.waitForTimeout(ms) }) When('the {string} actor starts a new application', async function (this: AppWorld, actor: string) { await useActor(this, actor) await startCandidateApplication(this) }) When( 'the {string} actor completes and submits the candidate application', async function (this: AppWorld, actor: string) { await useActor(this, actor) await completeCandidateApplication(this) }, ) When('the {string} actor logs into backoffice', async function (this: AppWorld, actor: string) { await useActor(this, actor) await this.gotoApp('/en/backoffice/auth/login') await this.login('e2e-backoffice-login@heygermany.test', 'BackofficePass123!') }) When('the {string} actor logs into company', async function (this: AppWorld, actor: string) { await useActor(this, actor) await this.gotoApp('/en/company/auth/login') await this.login('e2e-company-login@heygermany.test', 'CompanyPass123!') }) When( 'the {string} actor opens the current candidate in backoffice', async function (this: AppWorld, actor: string) { if (!this.candidateName) { throw new Error('No candidate name is available for the current scenario') } await useActor(this, actor) const row = this.page.locator('tr', { hasText: this.candidateName }).first() await row.waitFor({ state: 'visible' }) const popupPromise = this.context.waitForEvent('page') await row.click() const popup = await popupPromise await popup.waitForLoadState('domcontentloaded') await popup.waitForLoadState('networkidle', { timeout: 2_000 }).catch(() => {}) this.setActorPage(actor, popup) const candidateId = popup.url().match(/\/candidate\/([^/?#]+)/)?.[1] if (candidateId) { this.candidateId = candidateId } }, ) When( 'the {string} actor opens the current candidate in company', async function (this: AppWorld, actor: string) { if (!this.candidateName) { throw new Error('No candidate name is available for the current scenario') } await useActor(this, actor) await this.gotoApp('/en/company/candidates') const link = this.page.locator('a', { hasText: this.candidateName }).first() await link.waitFor({ state: 'visible' }) await link.click() await settle(this) const candidateId = this.page.url().match(/\/candidate\/([^/?#]+)/)?.[1] if (candidateId) { this.candidateId = candidateId } }, ) When( 'the {string} actor marks the candidate as {string}', async function (this: AppWorld, actor: string, status: string) { await useActor(this, actor) const combobox = await firstVisible( this.page .locator('input[placeholder="Select status"], button[title="Select status"]') .or(this.page.getByRole('combobox')) ) await combobox.click() const option = this.page.getByRole('option', { name: new RegExp(`^${escapeRegex(status)}$`, 'i'), }).first() await option.waitFor({ state: 'visible' }) await option.click() await settle(this) }, ) When( 'the {string} actor sets the backoffice field {string} to {string}', async function (this: AppWorld, actor: string, label: string, value: string) { await useActor(this, actor) const labelText = this.page.getByText(label, { exact: false }).first() const labeledContainer = labelText.locator( 'xpath=ancestor::div[.//input or .//button][1]', ) if (await labeledContainer.count()) { try { await setContainerValue(this, labeledContainer, value) return } catch { // fall back to direct field lookup below } } const target = await field(this, label) await target.scrollIntoViewIfNeeded().catch(() => {}) if (await chooseOptionFromControl(this, target, value)) { return } const tagName = await target.evaluate((node) => node.tagName.toLowerCase()).catch(() => '') if (tagName === 'input' || tagName === 'textarea') { await target.fill('').catch(() => {}) await target.fill(value).catch(() => {}) await target.blur().catch(() => {}) await this.page.waitForTimeout(700) await settle(this) return } throw new Error(`Could not set backoffice field "${label}" to "${value}"`) }, ) When( 'the {string} actor sets the verification field {string} to {string}', async function (this: AppWorld, actor: string, fieldName: string, value: string) { await useActor(this, actor) const row = this.page.locator('tr', { hasText: fieldName }).first() await row.waitFor({ state: 'visible' }) await setContainerValue(this, row.locator('td').last(), value) }, ) When( 'the {string} actor revisits the current candidate application', async function (this: AppWorld, actor: string) { await useActor(this, actor) await this.gotoApp('/en/jobs/application') }, ) 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 click {string}', async function (this: AppWorld, label: string) { const byId = this.page.getByTestId(label).first() if (await isVisibleWithin(byId, 1_000)) { await activate(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, 1_000)) { await activate(candidate) return } } const byText = this.page.getByText(label, { exact: false }).first() if (await isVisibleWithin(byText, 1_000)) { await activate(byText) return } throw new Error(`Could not click "${label}"`) }) When('the {string} actor clicks {string}', async function (this: AppWorld, actor: string, label: string) { await useActor(this, actor) const byId = this.page.getByTestId(label).first() if (await isVisibleWithin(byId, 1_000)) { await activate(byId) await settle(this) 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, 1_000)) { await activate(candidate) await settle(this) return } } const byText = this.page.getByText(label, { exact: false }).first() if (await isVisibleWithin(byText, 1_000)) { await activate(byText) await settle(this) return } throw new Error(`Could not click "${label}"`) }) Then('I see {string}', async function (this: AppWorld, text: string) { await this.expectText(text) }) Then('the {string} actor sees {string}', async function (this: AppWorld, actor: string, text: string) { await useActor(this, actor) await this.expectText(text) }) Then( 'the {string} actor sees the current candidate name', async function (this: AppWorld, actor: string) { if (!this.candidateName) { throw new Error('No candidate name is available for the current scenario') } await useActor(this, actor) await this.expectText(this.candidateName) }, ) 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)) }, ) Then( 'the {string} actor URL contains {string}', async function (this: AppWorld, actor: string, fragment: string) { await useActor(this, actor) await this.page.waitForURL((url) => url.toString().includes(fragment)) }, )