UNPKG

@uuv/playwright

Version:

A solution to facilitate the writing and execution of E2E tests understandable by any human being using cucumber(BDD) and playwright

769 lines (696 loc) 35.8 kB
/******************************* NE PAS MODIFIER, FICHIER GENERE *******************************/ /** * Software Name : UUV * * SPDX-License-Identifier: MIT * * This software is distributed under the MIT License, * see the "LICENSE" file for more details * * Authors: NJAKO MOLOM Louis Fredice & SERVICAL Stanley * Software description: Make test writing fast, understandable by any human * understanding English or French. */ import { DEFAULT_TIMEOUT, fs, KEY_PRESS } from "@uuv/runner-commons"; import { checkA11y, injectAxe } from "axe-playwright"; import { devices, expect } from "@playwright/test"; import { Locator, Page } from "playwright"; import { DataTable } from "@cucumber/cucumber"; import { addCookie, click, COOKIE_NAME, deleteCookieByName, FILTER_TYPE, findWithRoleAndName, findWithRoleAndNameAndContent, findWithRoleAndNameAndContentDisabled, findWithRoleAndNameAndContentEnabled, findWithRoleAndNameFocused, getCookie, getPageOrElement, getTimeout, MockCookie, notFoundWithRoleAndName, SelectedElementCookie, TimeoutCookie, withinRoleAndName } from "../core-engine"; import { Given, Then, When, World } from "../../../preprocessor/run/world"; import { ContextObject, RunOptions } from "axe-core"; import path from "path"; /** * Sets the viewport dimensions with one of the presets defined by your runtime engine as Cypress: [Link](https://docs.cypress.io/api/commands/viewport#Arguments) or Playwright: [Link](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json) * */ Given(`I set viewport to preset {string}`, async function(viewportPreset: string) { await this.page.setViewportSize(devices[viewportPreset].viewport); }); /** * Sets the viewport dimensions to the specified width and length * */ Given( `I set viewport with width {int} and height {int}`, async function (width: number, height: number) { await this.page.setViewportSize({ width: width, height: height }); } ); /** * Start a keyboard navigation session from the top of the page<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a> * */ Given( `I start a keyboard navigation from the top of the page`, async function() { await this.page.mouse.click(0.5, 0.5); } ); /** * Navigate to the Uri passed as a argument (full url consisting of the BASE_URL + Uri) or navigate to Url if begin with http:// or https:// * */ Given(`I visit path {string}`, async function(siteUrl: string) { await deleteCookieByName(this, COOKIE_NAME.SELECTED_ELEMENT); await this.page.goto(`${siteUrl}`); }); /** * Triggers a click on the selected element.<br/>Make sure you've selected an element beforehand with the <strong>within...</strong> phrases. * */ When(`I click`, async function() { const keyBoardFocusTargetObj = keyBoardFocusTarget(this); if ((await keyBoardFocusTargetObj.count()) === 1) { await keyBoardFocusTargetObj.click({ timeout: DEFAULT_TIMEOUT }); } else { await getPageOrElement(this).then((element: Locator) => element.click({ timeout: DEFAULT_TIMEOUT })); } }); /** * Triggers a click on the element with given role and specific name * */ When(`I click on element with role {string} and name {string}`, async function(role: string, name: string) { await click(this, role, name); }); // TODO : permet de gérer les label accessibles donc pas que les aria : https://playwright.dev/docs/api/class-locator#locator-get-by-label /** * Selects the element whose aria-label is specified <br />⚠ remember to deselect the element with <b>[I reset context](#i-reset-context)</b> if you're no longer acting on it * */ When(`within the element with aria-label {string}`, async function(expectedAriaLabel: string) { const sanitizedExpectedAriaLabel = encodeURIComponent(expectedAriaLabel).replaceAll("%20", " "); await getPageOrElement(this).then(async (element) => { const locator = element.getByLabel(sanitizedExpectedAriaLabel, { exact: true }); await expect(locator).toHaveCount(1, { timeout: await getTimeout(this) }); await locator.focus({ timeout: 10000 }); }); await addCookie(this, COOKIE_NAME.SELECTED_ELEMENT, new SelectedElementCookie(FILTER_TYPE.ARIA_LABEL, sanitizedExpectedAriaLabel)); }); /** * Deletes selected element and timeout * */ When(`I reset context`, async function() { await deleteCookieByName(this, COOKIE_NAME.SELECTED_ELEMENT); await deleteCookieByName(this, COOKIE_NAME.TIMEOUT); const keyBoardFocusTargetObj = keyBoardFocusTarget(this); if ((await keyBoardFocusTargetObj.count()) === 1) { await keyBoardFocusTargetObj.blur(); } }); /** * Selects the element whose selector is specified <br />⚠ remember to deselect the element with <b>[I reset context](#i-reset-context)</b> if you're no longer acting on it * */ When(`within the element with selector {string}`, async function(selector: string) { await getPageOrElement(this).then(async (element) => { const locator = element.locator(selector); await expect(locator).toHaveCount(1, { timeout: await getTimeout(this) }); await locator.focus({ timeout: 10000 }); }); await addCookie(this, COOKIE_NAME.SELECTED_ELEMENT, new SelectedElementCookie(FILTER_TYPE.SELECTOR, selector)); }); /** * Writes the sentence passed as a parameter (useful for example to fill in a form field).<br/>Make sure you've selected an element beforehand with the <strong>within...</strong> phrases. * */ When(`I type the sentence {string}`, async function(textToType: string) { await type(this, textToType); }); /** * Writes the sentence passed as a parameter (useful for example to fill in a form field).<br/>Make sure you've selected an element beforehand with the <strong>within...</strong> phrases. * */ When(`I enter the value {string}`, async function(textToType: string) { await type(this, textToType); }); /** * Select the combo box option passed as a parameter (useful for example to fill in a combobox form field).<br/>Make sure you've selected a combo box element beforehand with the <strong>within a combo box named 'yourComboboxNamee'</strong> phrases. * */ When(`I select the value {string}`, async function(valueToSet: string) { const keyBoardFocusTargetObj = keyBoardFocusTarget(this); if ((await keyBoardFocusTargetObj.count()) === 1) { await keyBoardFocusTargetObj.selectOption({ label: valueToSet }); } else { await getPageOrElement(this).then(async (element: Locator) => { // console.debug(element); await element.focus({ timeout: 10000 }); await element.selectOption({ label: valueToSet }); }); } }); /** * Check that the current page have the following partial [result](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/) based on RGAA standard * */ When(`I should see a combo box named {string} with value {string}`, async function(name: string, expectedValue: string) { await getPageOrElement(this).then(async (element) => { const byRole = await element.getByRole("combobox", { name: name, exact: true }); await expect(byRole).toHaveCount(1, { timeout: await getTimeout(this) }); await expect(byRole.getByRole("option", { name: expectedValue, selected: true, exact: true })) .toHaveCount(1, { timeout: await getTimeout(this) }); await deleteCookieByName(this, COOKIE_NAME.SELECTED_ELEMENT); }); }); /** * Check that the current page have the following partial [result](https://accessibilite.numerique.gouv.fr/methode/criteres-et-tests/) based on RGAA standard * */ When(`I select the value {string} in the combo box named {string}`, async function(valueToSet: string, name: string) { await getPageOrElement(this).then(async (element) => { const byRole = await element.getByRole("combobox", { name: name, exact: true }); await expect(byRole).toHaveCount(1, { timeout: await getTimeout(this) }); await byRole.selectOption({ label: valueToSet }); await deleteCookieByName(this, COOKIE_NAME.SELECTED_ELEMENT); }); }); /** * Press specified key multiple times using | as: num|{key} * */ When(`I press {int} times on {string}`, async function(nbTimes: number, key: string) { for (let i = 1; i <= nbTimes; i++) { await pressKey(this, key); } }); /** * Press specified key: <table><thead><tr><th>Key</th><th>Description</th></tr></thead><tbody><tr><td>{tab}</td><td>Tabulation</td></tr><tr><td>{reverseTab}</td><td>Reverse tabulation</td></tr><tr><td>{down}</td><td>Arrow Down</td></tr><tr><td>{right}</td><td>Arrow Right</td></tr><tr><td>{left}</td><td>Arrow Left</td></tr><tr><td>{up}</td><td>Arrow Up</td></tr></tbody></table><br/>Make sure you've selected an element beforehand with the <strong>within...</strong> phrases. * */ When(`I press {string}`, async function(key: string) { await pressKey(this, key); }); /** * Move to the previous html element that can be reached with back Tab<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a> * */ When(`I go to previous keyboard element`, async function() { await this.page.keyboard.press("ShiftLeft+Tab"); }); /** * Move to the next html element that can be reached with Tab<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a> * */ When(`I go to next keyboard element`, async function() { await this.page.keyboard.press("Tab"); }); /** * Sets the timeout value (in millisecond) for finding element in the DOM <br />⚠ remember to deselect the element with <b>[I reset context](#i-reset-context)</b> if you're no longer acting on it * */ When(`I set timeout with value {int}`, async function(newTimeout: number) { await addCookie(this, COOKIE_NAME.TIMEOUT, new TimeoutCookie("timeout", newTimeout)); }); /** * Selects the element whose [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and accessible [name](https://russmaxdesign.github.io/html-elements-names/) are specified <br />⚠ remember to deselect the element with <b>[I reset context](#i-reset-context)</b> if you're no longer acting on it * */ When(`within the element with role {string} and name {string}`, async function(role: string, name: string) { await withinRoleAndName(this, role, name); }); /** * Selects the element whose data-testId attribute is specified <br />⚠ remember to deselect the element with <b>[I reset context](#i-reset-context)</b> if you're no longer acting on it * */ When(`within the element with testId {string}`, async function(testId: string) { testId = encodeURIComponent(testId); await getPageOrElement(this).then(async (element) => { const locator = element.getByTestId(testId); await expect(locator).toHaveCount(1, { timeout: await getTimeout(this) }); await locator.focus({ timeout: 10000 }); }); await addCookie(this, COOKIE_NAME.SELECTED_ELEMENT, new SelectedElementCookie(FILTER_TYPE.TEST_ID, testId)); }); /** * Mock a named API response with body. <i>If you use Playwright as runtime engine, <b>request</b> and <b>named</b> are unused.</i> * */ When( `I mock a request {} on url {string} named {string} with content {}`, async function(verb: string, url: string, name: string, body: any) { await addCookie(this, COOKIE_NAME.MOCK_URL, new MockCookie(name, url, verb)); await this.page.route(url, async route => { await route.fulfill({ body }); await afterMock(this, url, verb, name); }); } ); /** * Mock a named API response with status code * */ When( `I mock a request {} on url {string} named {string} with status code {int}`, async function(verb: string, url: string, name: string, statusCode: number) { await addCookie(this, COOKIE_NAME.MOCK_URL, new MockCookie(name, url, verb)); await this.page.route(url, async route => { await route.fulfill({ status: statusCode }); await afterMock(this, url, verb, name); }); } ); /** * Mock a named API response with file's extension .json, .js, .coffee, .html, .txt, .csv, .png, .jpg, .jpeg, .gif, .tif, .tiff, .zip * */ When( `I mock a request {} on url {string} named {string} with fixture {}`, async function(verb: string, url: string, name: string, fixture: any) { await addCookie(this, COOKIE_NAME.MOCK_URL, new MockCookie(name, url, verb)); const data = fs.readFileSync(path.join(getConfigDir(), `playwright/fixtures/${fixture}`), "utf8"); await this.page.route(url, async route => { await route.fulfill({ body: data }); await afterMock(this, url, verb, name); }); } ); /** * Checks that an Html element exists with the specified selector * */ Then(`I should see an element with selector {string}`, async function(selector: string) { await getPageOrElement(this).then(async (element) => expect(element.locator(selector)).toHaveCount(1, { timeout: await getTimeout(this) })); }); /** * Sets one or more headers to the indicated http request and only for the Http method (GET / POST / etc...) passed as a argument.<i> If you use Playwright as execution engine, <b>method</b> isn't used.</i> * */ When( `I set header(s) for uri {string} and method {string}`, async function(url: string, method: string, headersToSet: DataTable) { await this.page.route(url, async route => { const headers = route.request().headers(); await route.continue({ headers: { ...headers, ...headersToSet.rowsHash() } }); }); } ); /** * Sets one or more headers to the indicated http request * */ When( `I set header(s) for uri {string}`, async function(url: string, headersToSet: DataTable) { await this.page.route(url, async route => { const headers = route.request().headers(); await route.continue({ headers: { ...headers, ...headersToSet.rowsHash() } }); }); } ); /** * Checks the current html page have the specified title * */ Then(`I should see the page title {string}`, async function(pageTitle: string) { await expect(this.page).toHaveTitle(pageTitle); }); /** * Checks that an Html element exists with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and [name](https://russmaxdesign.github.io/html-elements-names/) * */ Then(`I should see an element with role {string} and name {string}`, async function(role: string, name: string) { await findWithRoleAndName(this, role, name); }); /** * Checks that an Html element exists with the specified content * */ Then(`I should see an element with content {string}`, async function(textContent: string) { // TODO partie pris de faire en exactitude. A voir si on doit faire 2 phrases https://playwright.dev/docs/api/class-locator#locator-get-by-text await getPageOrElement(this).then(async (element) => expect(element.getByText(textContent, { exact: true })).toHaveCount(1, { timeout: await getTimeout(this) })); }); /** * Checks that an Html element does not exists with the specified content * */ Then(`I should not see an element with content {string}`, async function(textContent: string) { await getPageOrElement(this).then(async (element) => expect(element.getByText(textContent, { exact: true })).toHaveCount(0, { timeout: await getTimeout(this) })); }); /** * Checks that an Html element exists with the specified data-testid attribute * */ Then(`I should see an element with testId {string}`, async function(testId: string) { testId = encodeURIComponent(testId); await getPageOrElement(this).then(async (element) => expect(element.getByTestId(testId, { exact: true })).toHaveCount(1, { timeout: await getTimeout(this) })); }); /** * Checks that an Html element does not exists with the specified data-testid attribute * */ Then(`I should not see an element with testId {string}`, async function(testId: string) { testId = encodeURIComponent(testId); await getPageOrElement(this).then(async (element) => expect(element.getByTestId(testId, { exact: true })).toHaveCount(0, { timeout: await getTimeout(this) })); }); /** * Check that the current page have no accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) * */ Then( `I should not have any axe-core accessibility issue`, async function() { await injectAxe(this.page as Page); await checkA11y(this.page as Page); }); /** * Check that the current page have no accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) [with an option](https://github.com/dequelabs/axe-core/blob/HEAD/doc/API.md#options-parameter) * */ Then( `I should not have any axe-core accessibility issue with option json fixture {}`, async function(option: any) { await injectAxe(this.page as Page); const optionFile = await fs.readFileSync(path.join(getConfigDir(), `playwright/fixtures/${option}`)); const optionJson = JSON.parse(optionFile.toString()); await checkA11y(this.page as Page, undefined, { axeOptions: optionJson as RunOptions }); }); function getConfigDir(): string { // eslint-disable-next-line dot-notation return process.env["CONFIG_DIR"] ? process.env["CONFIG_DIR"] : ""; } /** * Check that the current page have no accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) [with an option](https://github.com/dequelabs/axe-core/blob/HEAD/doc/API.md#options-parameter) on the specific [context](https://github.com/dequelabs/axe-core/blob/HEAD/doc/API.md#context-parameter) * */ Then( `I should not have any axe-core accessibility issue with context json fixture {} and option json fixture {}`, async function(context: any, option: any) { await injectAxe(this.page as Page); const contextFile = await fs.readFileSync(path.join(getConfigDir(), `playwright/fixtures/${context}`)); const optionFile = await fs.readFileSync(path.join(getConfigDir(), `playwright/fixtures/${option}`)); const optionJson = JSON.parse(optionFile.toString()); await checkA11y(this.page as Page, JSON.parse(contextFile.toString()) as ContextObject, { axeOptions: optionJson as RunOptions }); }); /** * Check that the current page have not critical accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) * */ Then( `I should not have any axe-core critical accessibility issue`, async function() { await injectAxe(this.page as Page); await checkA11y(this.page as Page, undefined, { includedImpacts: ["critical"] }); }); /** * Check that the current page have not accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) with one or more impacts in: 'minor','moderate','serious','critical' * */ Then( `I should not have any axe-core accessibility issue with {} impact(s)`, async function(impacts: any) { await injectAxe(this.page as Page); await checkA11y(this.page as Page, undefined, { includedImpacts: [impacts] }); }); /** * Check that the current page have not accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) [with one or more Accessibility Standards](https://github.com/dequelabs/axe-core/blob/HEAD/doc/API.md#axe-core-tags) * */ Then( `I should not have any axe-core accessibility issue with accessibility standard(s) {}`, async function(tags: any) { await injectAxe(this.page as Page); await checkA11y(this.page as Page, undefined, { axeOptions: { runOnly: { type: "tag", values: [tags] } } }); }); /** * Checks that an Html element does not exists with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and [name](https://russmaxdesign.github.io/html-elements-names/) * */ Then( `I should not see an element with role {string} and name {string}`, async function(role: string, name: string) { await notFoundWithRoleAndName(this, role, name); } ); /** * Checks that an Html element exists with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types), [name](https://russmaxdesign.github.io/html-elements-names/) and content * */ Then( `I should see an element with role {string} and name {string} and content {string}`, async function(expectedRole: string, name: string, expectedTextContent: string) { await findWithRoleAndNameAndContent(this, expectedRole, name, expectedTextContent); } ); /** * Checks that the Html element with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and [name](https://russmaxdesign.github.io/html-elements-names/) is focused<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a> * */ Then( `the element with role {string} and name {string} should be keyboard focused`, async function(expectedRole: string, name: string) { await findWithRoleAndNameFocused(this, expectedRole, name); } ); /** * Checks that the Html element with the specified selector is focused<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a> * */ Then( `the element with selector {string} should be keyboard focused`, async function(selector: string) { await getPageOrElement(this).then(async (element) => { const locator = element.locator(selector); await locator.focus({ timeout: 10000 }); await expect(locator).toHaveCount(1, { timeout: await getTimeout(this) }); await expect(locator).toBeFocused(); }); }); /** * Checks that an Html element exists with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types), [name](https://russmaxdesign.github.io/html-elements-names/) and content, and with the disabled attribute set to true * */ Then( `I should see an element with role {string} and name {string} and content {string} disabled`, async function(expectedRole: string, name: string, expectedTextContent: string) { await findWithRoleAndNameAndContentDisabled(this, expectedRole, name, expectedTextContent); } ); /** * Checks that an Html element exists with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types), [name](https://russmaxdesign.github.io/html-elements-names/) and content, and with the disabled attribute set to false * */ Then( `I should see an element with role {string} and name {string} and content {string} enabled`, async function(expectedRole: string, name: string, expectedTextContent: string) { await findWithRoleAndNameAndContentEnabled(this, expectedRole, name, expectedTextContent); } ); /** * Checks that an Html element exists with the specified aria-label attribute * */ Then(`I should see an element with aria-label {string}`, async function(expectedAriaLabel: string) { expectedAriaLabel = encodeURIComponent(expectedAriaLabel); await getPageOrElement(this).then(async (element) => expect(element.getByLabel(expectedAriaLabel, { exact: true })).toHaveCount(1, { timeout: await getTimeout(this) })); }); /** * Checks that an Html element does not exists with the specified aria-label attribute * */ Then(`I should not see an element with aria-label {string}`, async function(expectedAriaLabel: string) { expectedAriaLabel = encodeURIComponent(expectedAriaLabel); await getPageOrElement(this).then(async (element) => expect(element.getByLabel(expectedAriaLabel, { exact: true })).toHaveCount(0, { timeout: await getTimeout(this) })); }); /** * Checks that an Html element exists with the specified aria-label attribute and content * */ Then(`I should see an element with aria-label {string} and content {string}`, async function(expectedAriaLabel: string, expectedTextContent: string) { expectedAriaLabel = encodeURIComponent(expectedAriaLabel); await getPageOrElement(this).then(async (element) => { const byLabel = element.getByLabel(expectedAriaLabel, { exact: true }); await expect(byLabel).toHaveCount(1, { timeout: await getTimeout(this) }); await expect(byLabel.filter({ hasText: expectedTextContent })).toHaveCount(1); }); }); /** * Move to the previous html element that can be reached with Tab and checks that the Html element with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and [name](https://russmaxdesign.github.io/html-elements-names/) is focused<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a> * */ When(`the previous keyboard element focused should have name {string} and role {string}`, async function(expectedRole: string, name: string) { await this.page.keyboard.press("ShiftLeft+Tab"); await findWithRoleAndNameFocused(this, expectedRole, name); }); /** * "Move to the next html element that can be reached with Tab and checks that the Html element with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and [name](https://russmaxdesign.github.io/html-elements-names/) is focused<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a> * */ When(`the next keyboard element focused should have name {string} and role {string}`, async function(expectedRole: string, name: string) { await this.page.keyboard.press("Tab"); await findWithRoleAndNameFocused(this, expectedRole, name); }); /** * Wait that a named mock has been consumed until timeout * */ Then(`I should consume a mock named {string}`, async function(name: string) { const cookie = await getCookie(this, COOKIE_NAME.MOCK_URL); expect(() => { if (!cookie.isValid()) { throw new Error(`Mock ${name} must be defined before use this sentence`); } }).not.toThrow(); await expect.poll(async () => { const cookie = await getCookie(this, COOKIE_NAME.MOCK_URL); const mockUrls: MockCookie[] = JSON.parse(cookie.value); const mockUrl = mockUrls.find(mock => mock.name === name); if (mockUrl) { return mockUrl?.isConsumed; } return false; }).toBeDefined(); }); /** * Wait milliseconds. <b>Warning:</b> use this sentence <b>in production</b> can make your <b>test flaky</b>. * */ Then(`I wait {int} ms`, async function(ms: number) { await this.page.waitForTimeout(ms); }); /** * Checks Html attributes of the selected element * */ Then( `I should see these attributes with values`, async function(expectedAttributeList: DataTable) { await getPageOrElement(this).then(async (element) => { // console.debug("expectedAttributeList.raw(),", expectedAttributeList.raw()) for (const expectedAttribute of expectedAttributeList.raw()) { const attributeName = expectedAttribute[0]; const attributeValue = expectedAttribute[1]; // await showAttributesInLocator(element); await expect(element).toHaveAttribute(attributeName, attributeValue); } }); } ); /** * Checks that there is a list with the specified [name](https://russmaxdesign.github.io/html-elements-names/) containing list items.<br/> <u>Example</u>\n```gherkin\nThen I should see a list named "test-list" and containing\n| First element |\n| Second element |\n| Third element |\n``` * */ Then( `I should see a list named {string} and containing`, async function(expectedListName: string, expectedElementsOfList: DataTable) { await withinRoleAndName(this, "list", expectedListName); await getPageOrElement(this).then(async (element) => { const listitem = await element.getByRole("listitem", { exact: true }).all(); const foundedElement: any[] = []; for (const element of listitem) { const textContent = await element.textContent(); foundedElement.push([textContent]); } await expect(foundedElement.length).toBeGreaterThan(0); // console.debug(`expected [${expectedElementsOfList.raw()}] to be [${foundedElement}]`); await expect(listitem.length).toEqual(expectedElementsOfList.raw().length); await expect(foundedElement).toEqual(expectedElementsOfList.raw()); }); } ); /** * Checks that there is a grid with the specified [name](https://russmaxdesign.github.io/html-elements-names/) containing list items.<br/> <u>Example</u>\n```gherkin\nWhen I visit path "https://e2e-test-quest.github.io/simple-webapp/grid.html"\nThen I should see a grid named "HTML Grid Example" and containing\n| Make | Model | Price |\n| ------------ | ------- | ------ |\n| Toyota | Celica | 35000 |\n| Ford | Mondeo | 32000 |\n| Porsche | Boxster | 72000 |\n| BMW | M50 | 60000 |\n| Aston Martin | DBX | 190000 |\n``` * */ Then( `I should see a grid named {string} and containing`, async function(expectedListName: string, pExpectedElementsOfList: DataTable) { const expectedElementsOfList = removeHeaderSeparatorLine(pExpectedElementsOfList); await findWithRoleAndName(this, "grid", expectedListName); await getPageOrElement(this).then(async (element) => { await expectTableToHaveContent(element, expectedElementsOfList, "gridcell"); }); } ); /** * Checks that there is a treegrid with the specified [name](https://russmaxdesign.github.io/html-elements-names/) containing list items.<br/> <u>Example</u>\n```gherkin\nWhen I visit path "https://e2e-test-quest.github.io/simple-webapp/treegrid.html"\nThen I should see a treegrid named "HTML Treegrid Example" and containing\n| Make | Model | Price |\n| ------------ | ------- | ------ |\n| Toyota | Celica | 35000 |\n| Ford | Mondeo | 32000 |\n| Porsche | Boxster | 72000 |\n| BMW | M50 | 60000 |\n| Aston Martin | DBX | 190000 |\n``` * */ Then( `I should see a treegrid named {string} and containing`, async function(expectedListName: string, pExpectedElementsOfList: DataTable) { const expectedElementsOfList = removeHeaderSeparatorLine(pExpectedElementsOfList); await findWithRoleAndName(this, "treegrid", expectedListName); await getPageOrElement(this).then(async (element) => { await expectTableToHaveContent(element, expectedElementsOfList, "gridcell"); }); } ); /** * Checks that there is a table with the specified [name](https://russmaxdesign.github.io/html-elements-names/) containing list items.<br/> <u>Example</u>\n```gherkin\nWhen I visit path "https://e2e-test-quest.github.io/simple-webapp/table.html"\nThen I should see a table named "test-list" and containing\n| Company | Contact | Country |\n| ----------------------------- | ---------------- | ------- |\n| Alfreds Futterkiste | Maria Anders | Germany |\n| Centro comercial Moctezuma | Francisco Chang | Mexico |\n| Ernst Handel | Roland Mendel | Austria |\n| Island Trading | Helen Bennett | UK |\n| Laughing Bacchus Winecellars | Yoshi Tannamuri | Canada |\n| Magazzini Alimentari Riuniti | Giovanni Rovelli | Italy |\n``` * */ Then( `I should see a table named {string} and containing`, async function(expectedListName: string, pExpectedElementsOfList: DataTable) { const expectedElementsOfList = removeHeaderSeparatorLine(pExpectedElementsOfList); await findWithRoleAndName(this, "table", expectedListName); await getPageOrElement(this).then(async (element) => { await expectTableToHaveContent(element, expectedElementsOfList, "cell"); }); } ); /** * Checks that an Html element exists with [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) `heading`, the specified [name](https://russmaxdesign.github.io/html-elements-names/) and title level * */ Then(`I should see a title named {string} with level {int}`, async function(name: string, level: number) { await findWithRoleAndName(this, "heading", name, { level }); }); function removeHeaderSeparatorLine(pExpectedElementsOfList: DataTable) { const expectedElementsOfList = pExpectedElementsOfList.raw(); if (expectedElementsOfList.length > 1) { expectedElementsOfList.splice(1, 1); } return expectedElementsOfList; } async function expectTableToHaveContent(element: Locator, expectedElementsOfList: string[][], pCellAccessibleRole: string) { const rows = await element.getByRole("row", { exact: true }).all(); return await Promise.all(rows.map(async (row: Locator, rowNumber: number) => { const cellAccessibleRole = rowNumber === 0 ? "columnheader" : pCellAccessibleRole; const cellsElement = await row.getByRole(cellAccessibleRole as any, { exact: true }).all(); let cellNumber = 0; return await Promise.all(cellsElement.map((cell: Locator) => { const expectedValue = expectedElementsOfList[rowNumber][cellNumber]; expect(cell, { message: `${cellAccessibleRole} at index [${rowNumber}, ${cellNumber}] should be ${expectedValue}` }).toHaveAccessibleName(expectedValue); cellNumber++; })); })); } async function pressKey(world: World, key: string) { switch (key) { case KEY_PRESS.TAB: await world.page.keyboard.press("Tab"); break; case KEY_PRESS.REVERSE_TAB: await world.page.keyboard.press("ShiftLeft+Tab"); break; case KEY_PRESS.UP: await world.page.keyboard.press("ArrowUp"); break; case KEY_PRESS.DOWN: await world.page.keyboard.press("ArrowDown"); break; case KEY_PRESS.LEFT: await world.page.keyboard.press("ArrowLeft"); break; case KEY_PRESS.RIGHT: await world.page.keyboard.press("ArrowRight"); break; default: console.error("the command" + key + " is unrecognized."); break; } await deleteCookieByName(world, COOKIE_NAME.SELECTED_ELEMENT); await addCookie(world, COOKIE_NAME.SELECTED_ELEMENT, new SelectedElementCookie(FILTER_TYPE.SELECTOR_PARENT, "*:focus")); } function keyBoardFocusTarget(world: World) { return world.page.locator(":focus"); } async function setMockAsConsumed(name: string, mock: MockCookie, world: World) { const newMockCookie = new MockCookie(name, mock.url, mock.verb); newMockCookie.isConsumed = true; await addCookie(world, COOKIE_NAME.MOCK_URL, newMockCookie); } async function afterMock(world: World, url: string, verb: string, name: string) { await world.page.waitForResponse(url); const cookie = await getCookie(world, COOKIE_NAME.MOCK_URL); const mockCookie: MockCookie[] = JSON.parse(cookie.value); for (const mock of mockCookie) { if (mock.name === name && mock.verb === verb && mock.url === url) { await setMockAsConsumed(name, mock, world); } } } async function type(world: World, textToType: string) { const keyBoardFocusTargetObj = keyBoardFocusTarget(world); if ((await keyBoardFocusTargetObj.count()) === 1) { await keyBoardFocusTargetObj.type(textToType); } else { await getPageOrElement(world).then(async (element: Locator) => { // console.debug(element); await element.focus({ timeout: 10000 }); await element.type(textToType); }); } }