790 lines
24 KiB
TypeScript
790 lines
24 KiB
TypeScript
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<AppWorld['page']['locator']>,
|
|
timeout = 5_000,
|
|
) {
|
|
try {
|
|
await target.waitFor({ state: 'visible', timeout })
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function activate(target: ReturnType<AppWorld['page']['locator']>, timeout = 5_000) {
|
|
try {
|
|
await target.click({ timeout })
|
|
return
|
|
} catch {
|
|
await target.evaluate((node) => {
|
|
;(node as HTMLElement).click()
|
|
})
|
|
}
|
|
}
|
|
|
|
async function firstVisible(target: ReturnType<AppWorld['page']['locator']>) {
|
|
return await nthVisible(target, 0)
|
|
}
|
|
|
|
async function nthVisible(
|
|
target: ReturnType<AppWorld['page']['locator']>,
|
|
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))
|
|
},
|
|
)
|