diff --git a/.eslintrc b/.eslintrc index 8f41681f66..7881308767 100644 --- a/.eslintrc +++ b/.eslintrc @@ -217,6 +217,7 @@ "playwright/no-commented-out-tests": "error", "playwright/no-focused-test": "error", "playwright/no-skipped-test": "warn", + "playwright/no-wait-for-timeout": "error", "no-return-await": "warn", "no-return-assign": "warn", "no-await-in-loop": "warn", @@ -225,7 +226,8 @@ "settings": { "playwright": { "messages": { - "noSkippedTest": "Test skipped. If unresolved, raise a JIRA ticket or GitHub issue, then reference it in a code comment above." + "noSkippedTest": "Test skipped. If unresolved, raise a JIRA ticket or GitHub issue, then reference it in a code comment above.", + "noWaitForTimeout": "Hardcoded timeouts make tests fragile and inefficient. Use Playwright’s built-in waits (e.g. `locator.waitFor()`) or auto-retrying assertions (e.g. `await expect(locator).toBeVisible()`) to wait for conditions like element visibility or text presence." } } } diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 1f7b2d987a..87f8cb74d7 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -16,13 +16,15 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4] - shardTotal: [4] + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] + shardTotal: [8] steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ">=20.9.0 20" + - name: Cache central NPM modules uses: actions/cache@v4 with: @@ -30,6 +32,7 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} restore-keys: | ${{ runner.os }}-node- + - name: Install dependencies run: | npm ci @@ -42,7 +45,7 @@ jobs: run: npm run test:ct -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report to Artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: blob-report-${{ matrix.shardIndex }} @@ -51,7 +54,7 @@ jobs: merge-reports: name: "Merge reports from all shards" - if: always() + if: ${{ !cancelled() }} needs: [playwright-tests] runs-on: ubuntu-latest steps: diff --git a/.prettierignore b/.prettierignore index 6862f64e80..e49ff94c3a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ node_modules/ coverage/ package-lock.json +.eslintrc \ No newline at end of file diff --git a/playwright-ct.config.ts b/playwright-ct.config.ts index e1399f06a6..cad13e6b2a 100644 --- a/playwright-ct.config.ts +++ b/playwright-ct.config.ts @@ -8,31 +8,27 @@ const playwrightDir = resolve(__dirname, "./playwright"); * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - /** Directory with the test files. */ testDir: resolve(__dirname, "./src/components"), - /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */ + snapshotDir: resolve(playwrightDir, "./__snapshots__"), - /* The output directory for files created during test execution */ + outputDir: resolve(playwrightDir, "./test-results"), - /* Maximum time one test can run for. */ + timeout: 30 * 1000, - /* Run tests in files in parallel */ + fullyParallel: true, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 8 : undefined, - // Limit the number of failures on CI to save resources + + retries: 3, + maxFailures: process.env.CI ? 10 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? "blob" : [["html", { outputFolder: resolve(playwrightDir, "./test-report") }]], - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "retain-on-failure", - /* Port to use for Playwright component endpoint. */ + ctPort: 3100, /* Custom config for internal bundler Playwright uses for component tests. See https://playwright.dev/docs/test-components#under-the-hood */ ctViteConfig: { @@ -45,7 +41,7 @@ export default defineConfig({ }, }, testMatch: /.*\.pw\.tsx/, - /* Configure projects for major browsers */ + projects: [ { name: "chromium", diff --git a/playwright/index.tsx b/playwright/index.tsx index 856c32ed24..c3ddeef0ce 100644 --- a/playwright/index.tsx +++ b/playwright/index.tsx @@ -6,25 +6,10 @@ import I18nProvider from "../src/components/i18n-provider/i18n-provider.componen import { noTheme, sageTheme } from "../src/style/themes"; import enGB from "../src/locales/en-gb"; import "../src/style/fonts.css"; -import * as dateLocales from "./support/date-fns-locales"; export type HooksConfig = { validationRedesignOptIn?: boolean; theme?: string; - localeName?: keyof typeof dateLocales; -}; - -const computedLocale = (str: keyof typeof dateLocales) => { - return { - locale: () => str, - date: { - dateFnsLocale: () => dateLocales[str], - ariaLabels: { - previousMonthButton: () => "Previous month", - nextMonthButton: () => "Next month", - }, - }, - }; }; const mountedTheme = (theme: string) => { @@ -42,7 +27,6 @@ const mountedTheme = (theme: string) => { beforeMount(async ({ App, hooksConfig }) => { const { theme = "sage", - localeName, validationRedesignOptIn, } = hooksConfig || {}; return ( @@ -51,7 +35,7 @@ beforeMount(async ({ App, hooksConfig }) => { validationRedesignOptIn={validationRedesignOptIn} > - + diff --git a/playwright/support/date-fns-locales/index.ts b/playwright/support/date-fns-locales/index.ts deleted file mode 100644 index 4116f919a9..0000000000 --- a/playwright/support/date-fns-locales/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -export { - enUS, - enCA, - enZA, - de, - es, - fr, - frCA, - zhCN, - zhHK, - pl, - bg, - hu, - fi, - deAT, - ko, - arEG, - hi, - sl, - lv, -} from "date-fns/locale"; diff --git a/playwright/support/helper.ts b/playwright/support/helper.ts index 6d1175a1f1..1af1e7269e 100644 --- a/playwright/support/helper.ts +++ b/playwright/support/helper.ts @@ -3,9 +3,6 @@ import AxeBuilder from "@axe-core/playwright"; import { expect } from "@playwright/experimental-ct-react17"; import { label, legendSpan } from "../components/index"; -const OPEN_MODAL = '[data-state="open"]'; -const CLOSED_MODAL = '[data-state="closed"]'; - /** * Retrieve a computed style for an element. * @param locator The Playwright locator to evaluate (see: https://playwright.dev/docs/locators) @@ -120,58 +117,6 @@ export const checkGoldenOutline = async ( ); }; -export const checkElementIsInDOM = async (page: Page, locatorStr: string) => { - expect(await page.$$(locatorStr)).toHaveLength(1); -}; - -export const checkElementIsNotInDOM = async ( - page: Page, - locatorStr: string, -) => { - expect(await page.$$(locatorStr)).toHaveLength(0); -}; - -export const checkDialogIsInDOM = async (page: Page) => { - await checkElementIsInDOM(page, OPEN_MODAL); - await checkElementIsNotInDOM(page, CLOSED_MODAL); -}; - -export const checkDialogIsNotInDOM = async (page: Page) => { - await checkElementIsNotInDOM(page, OPEN_MODAL); - await checkElementIsInDOM(page, CLOSED_MODAL); -}; - -/** - * Asserts if an element has event was calledOnce - * @param callbackData an array with callback data - * @param eventName {string} event name - * @example await expectEventWasCalledOnce(messages, "onClick"); - */ -export const expectEventWasCalledOnce = async ( - callbackData: string[], - eventName: string, -) => { - const count = JSON.stringify(callbackData.length); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const callbackName = JSON.stringify(callbackData[0]._reactName); - expect(count).toBe("1"); - expect(callbackName).toBe(`"${eventName}"`); -}; - -/** - * Asserts that event was NOT called - * @param callbackData an array with callback data - * @example await expectEventWasNotCalled(messages); - */ -export const expectEventWasNotCalled = async (callbackData: string[]) => { - const count = JSON.stringify(callbackData.length); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(count).toBe("0"); - expect(callbackData).toEqual([]); -}; - /** * Creates a safe regExp and uses the .toHaveClass() assertion * As there is not a "contains" assertion for the .toHaveClass() assertion @@ -193,17 +138,6 @@ export const containsClass = async ( await expect(locatorFunc).toHaveClass(classNameRegEx); }; -/** - * Uses the .toHaveFocus() assertion with a waitFor before to - * ensure the locator is available, and enough time has passed for the focus to be set. - * @param locatorFunc the locator you'd like to use - * @example await toBeFocusedDelayed(exampleLocator(page)); - */ -export const toBeFocusedDelayed = async (locatorFunc: Locator) => { - await locatorFunc.waitFor({ timeout: 10000 }); - await expect(locatorFunc).toBeFocused(); -}; - const positions = { first: 0, second: 1, diff --git a/src/components/accordion/accordion.pw.tsx b/src/components/accordion/accordion.pw.tsx index f26920ff84..10dd27a843 100644 --- a/src/components/accordion/accordion.pw.tsx +++ b/src/components/accordion/accordion.pw.tsx @@ -12,7 +12,6 @@ import { getRotationAngle, assertCssValueIsApproximately, getStyle, - expectEventWasCalledOnce, checkAccessibility, } from "../../../playwright/support/helper"; import { getDataElementByValue } from "../../../playwright/components"; @@ -203,30 +202,6 @@ test.describe("should render Accordion component", () => { await expect(accordionContent(page)).toBeVisible(); }); - [true, false].forEach((isExpanded) => { - test(`should call onChange callback when a click event is triggered and expanded is set to ${isExpanded}`, async ({ - mount, - page, - }) => { - const messages: string[] = []; - - await mount( - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - messages.push(data); - }} - />, - ); - - await accordionTitleContainer(page).click(); - - await expectEventWasCalledOnce(messages, "onClick"); - }); - }); - testData.forEach((titleValue) => { test(`should render Accordion component with ${titleValue} as a title`, async ({ mount, diff --git a/src/components/advanced-color-picker/advanced-color-picker.pw.tsx b/src/components/advanced-color-picker/advanced-color-picker.pw.tsx index 513d4e6d26..3fdb48fa4c 100644 --- a/src/components/advanced-color-picker/advanced-color-picker.pw.tsx +++ b/src/components/advanced-color-picker/advanced-color-picker.pw.tsx @@ -301,83 +301,6 @@ test.describe("should render AdvancedColorPicker component and check props", () }); }); -test.describe("should render AdvancedColorPicker component and check events", () => { - test("should call onChange callback when a click event is triggered", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const colorToPick = simpleColorPickerInput(page, 0); - await colorToPick.click(); - expect(callbackCount).toBe(1); - }); - - test("should call onOpen callback when a click event is triggered", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - await closeIconButton(page).click(); - const firstCell = advancedColorPickerCell(page); - await firstCell.click(); - expect(callbackCount).toBe(1); - }); - - test("should call onClose callback when a click event is triggered", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const closeButton = closeIconButton(page); - await closeButton.click(); - expect(callbackCount).toBe(1); - }); - - test("should not call onBlur callback when a blur event is triggered on another color", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const elementToFocus = simpleColorPickerInput(page, 7); - await elementToFocus.focus(); - const elementToBlur = simpleColorPickerInput(page, 7); - await elementToBlur.blur(); - expect(callbackCount).toBe(0); - }); -}); - test.describe("Accessibility tests for AdvancedColorPicker component", () => { test("should pass accessibility tests for AdvancedColorPicker default", async ({ mount, diff --git a/src/components/alert/alert.pw.tsx b/src/components/alert/alert.pw.tsx index 20b994ccd6..674b08bf2c 100644 --- a/src/components/alert/alert.pw.tsx +++ b/src/components/alert/alert.pw.tsx @@ -1,15 +1,12 @@ import React from "react"; import { test, expect } from "@playwright/experimental-ct-react17"; import { - alertCrossIcon, alertTitle, alertSubtitle, alertDialog, } from "../../../playwright/components/alert"; import { checkAccessibility, - checkDialogIsInDOM, - checkDialogIsNotInDOM, waitForAnimationEnd, } from "../../../playwright/support/helper"; import { CHARACTERS, SIZE } from "../../../playwright/support/constants"; @@ -53,7 +50,7 @@ test.describe("should render Alert component", () => { }); }); - test("with close icon button that does not close dialog when escape key pressed and disableEscKey prop is true", async ({ + test("dialog does not close when escape key pressed and disableEscKey prop is true", async ({ mount, page, }) => { @@ -63,35 +60,42 @@ test.describe("should render Alert component", () => { , ); - await checkDialogIsInDOM(page); + const alert = page.getByRole("alertdialog"); + await alert.waitFor(); + await page.keyboard.press("Escape"); - await checkDialogIsInDOM(page); + + await expect(alert).toBeVisible(); }); - test("with keyboard accessible close icon button which closes the dialog when enter key is pressed", async ({ + test("dialog closes when enter key is pressed on close button", async ({ mount, page, }) => { await mount(Alert); - await checkDialogIsInDOM(page); - const cross = alertCrossIcon(page); - await page.keyboard.press("Tab"); - await expect(cross).toBeFocused(); - await cross.press("Enter"); - await checkDialogIsNotInDOM(page); + const alert = page.getByRole("alertdialog"); + await alert.waitFor(); + + const closeButton = page.getByRole("button", { name: "Close" }); + await closeButton.press("Enter"); + + await expect(alert).toBeHidden(); }); - test("with close icon button that closes dialog when clicked", async ({ + test("dialog closes when close button is clicked", async ({ mount, page, }) => { await mount(Alert); - await checkDialogIsInDOM(page); - const cross = alertCrossIcon(page); - await cross.click(); - await checkDialogIsNotInDOM(page); + const alert = page.getByRole("alertdialog"); + await alert.waitFor(); + + const closeButton = page.getByRole("button", { name: "Close" }); + await closeButton.click(); + + await expect(alert).toBeHidden(); }); viewportHeights.forEach((height) => { @@ -136,24 +140,6 @@ test.describe("should render Alert component", () => { }); }); - test("with close icon button that calls the onCancel callback when clicked", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const cross = alertCrossIcon(page); - await cross.click(); - expect(callbackCount).toBe(1); - }); - // TODO: Skipped due to flaky focus behaviour. To review in FE-6428 test.skip("setting the topModalOverride prop should ensure the Alert is rendered on top of any others", async ({ mount, diff --git a/src/components/alert/alert.test.tsx b/src/components/alert/alert.test.tsx index ca9432280a..74e8f4e83e 100644 --- a/src/components/alert/alert.test.tsx +++ b/src/components/alert/alert.test.tsx @@ -1,7 +1,24 @@ import React from "react"; import { render, screen } from "@testing-library/react"; -import Alert from "."; +import userEvent from "@testing-library/user-event"; +import Alert, { AlertProps } from "."; +const ControlledAlert = ({ onCancel, ...rest }: Partial) => { + const [open, setOpen] = React.useState(true); + + return ( + { + setOpen(false); + onCancel?.(ev); + }} + title="Invalid client" + subtitle="Please check the client details" + /> + ); +}; test("include correct component, element and role data tags", () => { render( { expect(closeButton).toHaveAttribute("data-element", "foo"); expect(closeButton).toHaveAttribute("data-role", "bar"); }); + +test("calls onCancel when close button is clicked", async () => { + const user = userEvent.setup(); + const onCancel = jest.fn(); + + render(); + + const closeButton = screen.getByRole("button", { name: /Close/i }); + await user.click(closeButton); + + expect(onCancel).toHaveBeenCalledTimes(1); +}); diff --git a/src/components/badge/badge.pw.tsx b/src/components/badge/badge.pw.tsx index fbc5a7ef7f..b0c36d7338 100644 --- a/src/components/badge/badge.pw.tsx +++ b/src/components/badge/badge.pw.tsx @@ -5,10 +5,7 @@ import { badgeCounter, badgeCrossIcon, } from "../../../playwright/components/badge/index"; -import { - checkAccessibility, - expectEventWasCalledOnce, -} from "../../../playwright/support/helper"; +import { checkAccessibility } from "../../../playwright/support/helper"; import { CHARACTERS } from "../../../playwright/support/constants"; import BadgeComponent from "./components.test-pw"; @@ -97,28 +94,6 @@ test.describe("should render Badge component", () => { await expect(badgeCrossIcon(page)).not.toBeVisible(); }); - test("should call onClick callback when a click event is triggered", async ({ - mount, - page, - }) => { - const messages: string[] = []; - - await mount( - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - messages.push(data); - }} - />, - ); - - const badgeToClick = badge(page); - await badgeToClick.click(); - await expectEventWasCalledOnce(messages, "onClick"); - }); - test("should render with expected border radius styling", async ({ mount, page, diff --git a/src/components/badge/badge.test.tsx b/src/components/badge/badge.test.tsx index 4b907af20f..56ca394f58 100644 --- a/src/components/badge/badge.test.tsx +++ b/src/components/badge/badge.test.tsx @@ -96,6 +96,18 @@ describe("Badge", () => { expect(screen.getByRole("button")).not.toHaveAttribute("aria-label"); }); + it("calls onClick callback when badge is clicked", async () => { + const onClick = jest.fn(); + const user = userEvent.setup(); + + renderComponent({ counter: 5, onClick }); + + const badgeButton = screen.getByRole("button"); + await user.click(badgeButton); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + it("should apply the correct cursor style when onClick is not specified", () => { renderComponent({ counter: 9 }); diff --git a/src/components/breadcrumbs/breadcrumbs.pw.tsx b/src/components/breadcrumbs/breadcrumbs.pw.tsx index 92cb4f2875..6a1bad63e5 100644 --- a/src/components/breadcrumbs/breadcrumbs.pw.tsx +++ b/src/components/breadcrumbs/breadcrumbs.pw.tsx @@ -11,11 +11,7 @@ import { FocusedCrumbBecomesCurrent, OnDarkBackground, } from "./components.test-pw"; -import { - checkAccessibility, - expectEventWasNotCalled, - expectEventWasCalledOnce, -} from "../../../playwright/support/helper"; +import { checkAccessibility } from "../../../playwright/support/helper"; import { CHARACTERS } from "../../../playwright/support/constants"; test.describe("should render Breadcrumbs component", async () => { @@ -236,53 +232,6 @@ test("when Crumb's isCurrent prop is true, Crumb divider should not exist", asyn ); }); -test("should call the onClick callback when clicked", async ({ - mount, - page, -}) => { - const messages: string[] = []; - - await mount( - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - messages.push(data); - }} - />, - ); - - const crumbToClick = crumbAtIndex(page, 0); - await crumbToClick.click(); - - await expectEventWasCalledOnce(messages, "onClick"); -}); - -test("should not set the onClick or href props when isCurrent is true", async ({ - mount, - page, -}) => { - const messages: string[] = []; - - await mount( - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - messages.push(data); - }} - isCurrent - />, - ); - - const crumbToClick = crumbAtIndex(page, 0); - await crumbToClick.click(); - - await expectEventWasNotCalled(messages); - await expect(crumbToClick.locator("a")).not.toHaveAttribute("href", "/"); -}); - test.describe("Accessibility tests for Breadcrumbs component", async () => { test("should pass accessibility tests for Breadcrumbs default story", async ({ mount, diff --git a/src/components/confirm/confirm.pw.tsx b/src/components/confirm/confirm.pw.tsx index a3d24bccbf..7f6f47a91b 100644 --- a/src/components/confirm/confirm.pw.tsx +++ b/src/components/confirm/confirm.pw.tsx @@ -13,7 +13,6 @@ import { import { assertCssValueIsApproximately, checkAccessibility, - checkDialogIsInDOM, getStyle, waitForAnimationEnd, } from "../../../playwright/support/helper"; @@ -248,9 +247,12 @@ test.describe("should render Confirm component", () => { test(`should check Esc key is disabled`, async ({ mount, page }) => { await mount(); - await checkDialogIsInDOM(page); + const confirm = page.getByRole("alertdialog"); + await confirm.waitFor(); + await page.keyboard.press("Escape"); - await checkDialogIsInDOM(page); + + await expect(confirm).toBeVisible(); }); test(`should check close icon is enabled`, async ({ mount, page }) => { @@ -537,106 +539,51 @@ test.describe("should render Confirm component", () => { }); }); -test.describe("should render Confirm component for event tests", () => { - test(`should call onCancel callback when a click event is triggered`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const button = page.getByRole("button").filter({ hasText: "No" }); - await button.click(); - expect(callbackCount).toBe(1); - }); - - test(`should call onConfirm callback when a click event is triggered`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const button = page.getByRole("button").filter({ hasText: "Yes" }); - await button.click(); - expect(callbackCount).toBe(1); - }); - - test(`should check onCancel callback when Escape key event is triggered`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - await page.getByRole("alertdialog").press("Escape"); - expect(callbackCount).toBe(1); - }); - - // TODO: Skipped due to flaky focus behaviour. To review in FE-6428 - test.skip("setting the topModalOverride prop should ensure the Confirm is rendered on top of any others", async ({ - mount, - page, - }) => { - await mount(); - - const dialog = page.getByRole("alertdialog"); - const dialogCancelBtn = dialog.getByRole("button").first(); - const dialogConfirmBtn = dialog.getByRole("button").last(); - const dialogTextbox = page.getByLabel("Confirm textbox"); - - await waitForAnimationEnd(dialog); - await dialog.press("Tab"); - await expect(dialogTextbox).toBeFocused(); - await dialogTextbox.press("Tab"); - await expect(dialogCancelBtn).toBeFocused(); - await dialogCancelBtn.press("Tab"); - await expect(dialogConfirmBtn).toBeFocused(); - await dialogConfirmBtn.press("Enter"); - - const sidebar = getDataElementByValue(page, "sidebar"); - const sidebarClose = sidebar.getByLabel("Close"); - const sidebarTextbox = page.getByLabel("Sidebar textbox"); - - await waitForAnimationEnd(sidebar); - await sidebar.press("Tab"); - await expect(sidebarClose).toBeFocused(); - await sidebarClose.press("Tab"); - await expect(sidebarTextbox).toBeFocused(); - await sidebarTextbox.press("Tab"); - await expect(sidebarClose).toBeFocused(); - await sidebarClose.press("Enter"); - - const dialogFullscreen = getDataElementByValue(page, "dialog-full-screen"); - const dialogFullscreenClose = dialogFullscreen.getByLabel("Close"); - const dialogFullscreenTextbox = page.getByLabel("Fullscreen textbox"); - - await waitForAnimationEnd(dialogFullscreen); - await dialogFullscreen.press("Tab"); - await expect(dialogFullscreenClose).toBeFocused(); - await dialogFullscreenClose.press("Tab"); - await expect(dialogFullscreenTextbox).toBeFocused(); - await dialogFullscreenTextbox.press("Tab"); - await expect(dialogFullscreenClose).toBeFocused(); - }); +// TODO: Skipped due to flaky focus behaviour. To review in FE-6428 +test.skip("setting the topModalOverride prop should ensure the Confirm is rendered on top of any others", async ({ + mount, + page, +}) => { + await mount(); + + const dialog = page.getByRole("alertdialog"); + const dialogCancelBtn = dialog.getByRole("button").first(); + const dialogConfirmBtn = dialog.getByRole("button").last(); + const dialogTextbox = page.getByLabel("Confirm textbox"); + + await waitForAnimationEnd(dialog); + await dialog.press("Tab"); + await expect(dialogTextbox).toBeFocused(); + await dialogTextbox.press("Tab"); + await expect(dialogCancelBtn).toBeFocused(); + await dialogCancelBtn.press("Tab"); + await expect(dialogConfirmBtn).toBeFocused(); + await dialogConfirmBtn.press("Enter"); + + const sidebar = getDataElementByValue(page, "sidebar"); + const sidebarClose = sidebar.getByLabel("Close"); + const sidebarTextbox = page.getByLabel("Sidebar textbox"); + + await waitForAnimationEnd(sidebar); + await sidebar.press("Tab"); + await expect(sidebarClose).toBeFocused(); + await sidebarClose.press("Tab"); + await expect(sidebarTextbox).toBeFocused(); + await sidebarTextbox.press("Tab"); + await expect(sidebarClose).toBeFocused(); + await sidebarClose.press("Enter"); + + const dialogFullscreen = getDataElementByValue(page, "dialog-full-screen"); + const dialogFullscreenClose = dialogFullscreen.getByLabel("Close"); + const dialogFullscreenTextbox = page.getByLabel("Fullscreen textbox"); + + await waitForAnimationEnd(dialogFullscreen); + await dialogFullscreen.press("Tab"); + await expect(dialogFullscreenClose).toBeFocused(); + await dialogFullscreenClose.press("Tab"); + await expect(dialogFullscreenTextbox).toBeFocused(); + await dialogFullscreenTextbox.press("Tab"); + await expect(dialogFullscreenClose).toBeFocused(); }); test.describe("should check accessibility for Confirm", () => { diff --git a/src/components/confirm/confirm.test.tsx b/src/components/confirm/confirm.test.tsx index a524cfaa58..a8b2fef16f 100644 --- a/src/components/confirm/confirm.test.tsx +++ b/src/components/confirm/confirm.test.tsx @@ -89,20 +89,32 @@ test("should render disabled cancel button and close icon when disableCancel is expect(screen.getByRole("button", { name: "Close" })).toBeDisabled(); }); -test("should not call onCancel when disableCancel is set and ESC key is pressed", async () => { +test("calls onCancel when Escape key is pressed", async () => { + const onCancel = jest.fn(); + const user = userEvent.setup(); + + render( {}} onCancel={onCancel} />); + + await user.keyboard("{Escape}"); + + expect(onCancel).toHaveBeenCalledTimes(1); +}); + +test("does not call onCancel when disableCancel is set and Escape key is pressed", async () => { const onCancel = jest.fn(); const user = userEvent.setup(); render( {}} - onCancel={() => {}} + onCancel={onCancel} showCloseIcon disableCancel />, ); - await user.keyboard("{esc}"); + await user.keyboard("{Escape}"); + expect(onCancel).not.toHaveBeenCalled(); }); diff --git a/src/components/date/__internal__/date-picker/date-picker.component.tsx b/src/components/date/__internal__/date-picker/date-picker.component.tsx index c5b640126f..14a7890b08 100644 --- a/src/components/date/__internal__/date-picker/date-picker.component.tsx +++ b/src/components/date/__internal__/date-picker/date-picker.component.tsx @@ -221,6 +221,7 @@ export const DatePicker = ({ > & Partial) => { - const [state, setState] = React.useState( - value?.length !== undefined ? value : "01/05/2022", - ); + const [state, setState] = React.useState(value ?? "01/05/2022"); const handleOnChange = (ev: DateChangeEvent) => { if (onChange) { @@ -83,7 +81,7 @@ export const DateInputValidationNewDesign = () => { ); }; -export const DateInputWithButton = ({ +export const WithSiblingButton = ({ onChange, onBlur, value, @@ -124,35 +122,6 @@ export const DateInputWithButton = ({ ); }; -export const DateWithLocales = ({ - onChange, -}: Partial & { - onChange: () => void; -}) => { - const [state, setState] = useState("04/04/2019"); - const [rawValue, setRawValue] = useState(null); - const [formattedValue, setFormattedValue] = useState(""); - - const setValue = (ev: DateChangeEvent) => { - setState(ev.target.value.formattedValue); - setRawValue(ev.target.value.rawValue); - setFormattedValue(ev.target.value.formattedValue); - onChange(); - }; - return ( - <> - -
{rawValue}
-
{formattedValue}
- - ); -}; - export const DateInputInsideDialog = ({ onChange, onBlur, diff --git a/src/components/date/date.pw.tsx b/src/components/date/date.pw.tsx index 94fecb12fa..cc714d9c97 100644 --- a/src/components/date/date.pw.tsx +++ b/src/components/date/date.pw.tsx @@ -5,8 +5,7 @@ import advancedFormat from "dayjs/plugin/advancedFormat"; import { DateInputCustom, DateInputValidationNewDesign, - DateInputWithButton, - DateWithLocales, + WithSiblingButton, DateInputInsideDialog, } from "./components.test-pw"; import { DateInputProps } from "."; @@ -20,7 +19,6 @@ import { assertCssValueIsApproximately, checkAccessibility, getStyle, - toBeFocusedDelayed, containsClass, } from "../../../playwright/support/helper"; import { @@ -32,13 +30,10 @@ import { dayPickerWrapper, dayPickerHeading, } from "../../../playwright/components/date-input/index"; -import { HooksConfig } from "../../../playwright"; -import { alertDialogPreview } from "../../../playwright/components/dialog"; dayjs.extend(advancedFormat); const testData = [CHARACTERS.DIACRITICS, CHARACTERS.SPECIALCHARACTERS]; -const DAY_PICKER_PREFIX = "rdp-"; const TODAY = dayjs().format("dddd, MMMM Do, YYYY"); const DATE_INPUT = dayjs("2022-05-01").format("DD/MM/YYYY"); const TODAY_DATE_INPUT = dayjs().format("DD/MM/YYYY"); @@ -50,12 +45,6 @@ const PREVIOUS_MONTH = dayjs("2022-05-01") const MIN_DATE = "04/04/2030"; const DAY_BEFORE_MIN_DATE = "Wednesday, April 3rd, 2030"; const DAY_AFTER_MAX_DATE = "Friday, April 5th, 2030"; -const DDMMYYY_DATE_TO_ENTER = "27,05,2022"; -const MMDDYYYY_DATE_TO_ENTER = "05,27,2022"; -const YYYYMMDD_DATE_TO_ENTER = "2022,05,27"; -const DDMMYYY_DATE_TO_ENTER_SHORT = "1,7,22"; -const MMDDYYYY_DATE_TO_ENTER_SHORT = "7,1,22"; -const YYYYMMDD_DATE_TO_ENTER_SHORT = "22,7,1"; const arrowKeys = ["ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown"]; test.describe("Functionality tests", () => { @@ -191,7 +180,7 @@ test.describe("Functionality tests", () => { await calendarIcon.click(); await calendarIcon.click(); const wrapper = dayPickerWrapper(page); - await expect(wrapper).toHaveCount(0); + await expect(wrapper).toBeHidden(); }); [ @@ -245,16 +234,18 @@ test.describe("Functionality tests", () => { mount, page, }) => { - await mount(); + await mount(); - const inputParent = getDataElementByValue(page, "input").locator(".."); - await inputParent.click(); - const rightArrow = getDataElementByValue(page, "chevron_right").locator( - "..", - ); - await rightArrow.focus(); - await rightArrow.press(key); - const pickerHeading = dayPickerHeading(page); + const input = page.getByLabel("Date"); + await input.click(); + + const datePicker = page.getByTestId("date-picker"); + await datePicker.waitFor(); + + const nextMonthButton = page.getByRole("button", { name: "Next month" }); + await nextMonthButton.press(key); + + const pickerHeading = datePicker.getByRole("status"); await expect(pickerHeading).toHaveText("May 2022"); }); }); @@ -264,16 +255,19 @@ test.describe("Functionality tests", () => { mount, page, }) => { - await mount(); + await mount(); - const inputParent = getDataElementByValue(page, "input").locator(".."); - await inputParent.click(); - const rightArrow = getDataElementByValue(page, "chevron_left").locator( - "..", - ); - await rightArrow.focus(); - await rightArrow.press(key); - const pickerHeading = dayPickerHeading(page); + const dateInput = page.getByLabel("Date"); + await dateInput.click(); + + const nextMonthButton = page.getByRole("button", { name: "Next month" }); + await nextMonthButton.waitFor(); + + await nextMonthButton.focus(); + await nextMonthButton.press(key); + + const datePicker = page.getByTestId("date-picker"); + const pickerHeading = datePicker.getByRole("status"); await expect(pickerHeading).toHaveText("May 2022"); }); }); @@ -284,50 +278,50 @@ test.describe("Functionality tests", () => { }) => { await mount(); - await page.focus("body"); - await page.keyboard.press("Tab"); - const input = getDataElementByValue(page, "input"); - await expect(input).toBeFocused(); + const input = page.getByLabel("Date"); + await input.click(); + + const dayPicker = page.getByTestId("date-picker"); + await dayPicker.waitFor(); + await page.keyboard.press("Tab"); - const arrowLeft = getDataElementByValue(page, "chevron_left").locator(".."); - await expect(arrowLeft).toBeFocused(); + const previousMonthButton = page.getByRole("button", { + name: "Previous month", + }); + await expect(previousMonthButton).toBeFocused(); + await page.keyboard.press("Tab"); - const arrowRight = getDataElementByValue(page, "chevron_right").locator( - "..", - ); - await expect(arrowRight).toBeFocused(); + const nextMonthButton = page.getByRole("button", { + name: "Next month", + }); + await expect(nextMonthButton).toBeFocused(); + await page.keyboard.press("Tab"); - const dayPicker = page.locator(`.rdp-selected`).locator("button"); - await expect(dayPicker).toBeFocused(); + const dayButton = page.getByRole("button", { + name: "Monday, December 12th, 2022", + }); + await expect(dayButton).toBeFocused(); }); test(`should close the picker and focus the next element in the DOM when focus is on a day element and tab pressed`, async ({ mount, page, }) => { - await mount(); + await mount(); - await page.focus("body"); - await page.keyboard.press("Tab"); - const input = getDataElementByValue(page, "input"); - await expect(input).toBeFocused(); - await page.keyboard.press("Tab"); - const arrowLeft = getDataElementByValue(page, "chevron_left").locator(".."); - await expect(arrowLeft).toBeFocused(); - await page.keyboard.press("Tab"); - const arrowRight = getDataElementByValue(page, "chevron_right").locator( - "..", - ); - await expect(arrowRight).toBeFocused(); - await page.keyboard.press("Tab"); - const dayPicker = page - .locator(`.${DAY_PICKER_PREFIX}selected`) - .locator("button"); - await expect(dayPicker).toBeFocused(); - await page.keyboard.press("Tab"); - const wrapper = dayPickerWrapper(page); - await expect(wrapper).toHaveCount(0); - const fooButton = page.getByRole("button"); + const dateInput = page.getByRole("textbox"); + await dateInput.click(); + + const dayPicker = page.getByTestId("date-picker"); + await dayPicker.waitFor(); + + const dayButton = page.getByRole("button", { + name: "Sunday, May 1st, 2022", + }); + await dayButton.press("Tab"); + + const fooButton = page.getByRole("button", { name: "foo" }); + await expect(dayPicker).toBeHidden(); await expect(fooButton).toBeFocused(); }); @@ -337,276 +331,226 @@ test.describe("Functionality tests", () => { }) => { await mount(); - await page.focus("body"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - const dayPicker = page - .locator(`.${DAY_PICKER_PREFIX}today`) - .locator("button"); - await toBeFocusedDelayed(dayPicker); - await page.keyboard.press("Tab"); - const wrapper = dayPickerWrapper(page); - await expect(wrapper).toHaveCount(0); + await page.getByLabel("Date").press("Tab"); + + const todayButton = page.getByRole("button", { name: `Today, ${TODAY}` }); + await todayButton.waitFor(); + + await page + .getByRole("button", { name: "Next month", exact: true }) + .press("Tab"); + + await expect(todayButton).toBeFocused(); }); - test(`should navigate through the day elements using the arrow keys`, async ({ + test(`day buttons are navigable with the arrow keys`, async ({ mount, page, }) => { await mount(); - await page.focus("body"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press(arrowKeys[3]); - const focusedElement1 = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "21" }); - await toBeFocusedDelayed(focusedElement1); - await page.keyboard.press(arrowKeys[3]); - const focusedElement2 = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "28" }); - await toBeFocusedDelayed(focusedElement2); - - await page.keyboard.press(arrowKeys[1]); - const focusedElement3 = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "27" }); - await toBeFocusedDelayed(focusedElement3); - await page.keyboard.press(arrowKeys[1]); - const focusedElement4 = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "26" }); - await toBeFocusedDelayed(focusedElement4); - - await page.keyboard.press(arrowKeys[0]); - const focusedElement5 = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "27" }); - await toBeFocusedDelayed(focusedElement5); - await page.keyboard.press(arrowKeys[0]); - const focusedElement6 = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "28" }); - await toBeFocusedDelayed(focusedElement6); - - await page.keyboard.press(arrowKeys[2]); - const focusedElement7 = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "21" }); - await toBeFocusedDelayed(focusedElement7); - await page.keyboard.press(arrowKeys[2]); - const focusedElement8 = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "14" }); - await toBeFocusedDelayed(focusedElement8); + await page.getByLabel("Date").press("Tab"); + + const datePicker = page.getByTestId("date-picker"); + await datePicker.waitFor(); + + await page + .getByRole("button", { name: "Next month", exact: true }) + .press("Tab"); + const day14Button = page.getByRole("button", { + name: "Thursday, April 14th, 2022", + }); + await expect(day14Button).toBeFocused(); + + const day21Button = page.getByRole("button", { + name: "Thursday, April 21st, 2022", + }); + await day14Button.press("ArrowDown"); + await expect(day21Button).toBeFocused(); + + const day22Button = page.getByRole("button", { + name: "Friday, April 22nd, 2022", + }); + await day21Button.press("ArrowRight"); + await expect(day22Button).toBeFocused(); + + const day15Button = page.getByRole("button", { + name: "Friday, April 15th, 2022", + }); + await day22Button.press("ArrowUp"); + await expect(day15Button).toBeFocused(); + + await day15Button.press("ArrowLeft"); + await expect(day14Button).toBeFocused(); }); - test(`should navigate to the previous month when left arrow pressed on first day element of a month`, async ({ + test(`navigates to previous month when left arrow is pressed while first day of a month button is focused`, async ({ mount, page, }) => { - await mount(); + await mount(); - await page.focus("body"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press(arrowKeys[1]); - const focusedElement = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "13" }); - await toBeFocusedDelayed(focusedElement); - const pickerHeading = dayPickerHeading(page); - await expect(pickerHeading).toHaveText(PREVIOUS_MONTH); + await page.getByLabel("Date").press("Tab"); + + const datePicker = page.getByTestId("date-picker"); + await datePicker.waitFor(); + + const day1Button = page.getByRole("button", { + name: "Friday, April 1st, 2022", + }); + await day1Button.press("ArrowLeft"); + + const day31Button = page.getByRole("button", { + name: "Thursday, March 31st, 2022", + }); + await day31Button.waitFor(); + + await expect(day31Button).toBeFocused(); + + const pickerHeading = datePicker.getByRole("status"); + await expect(pickerHeading).toHaveText("March 2022"); }); - [ - ["24", "1"], - ["25", "2"], - ["26", "3"], - ["27", "4"], - ["28", "5"], - ["29", "6"], - ["30", "7"], - ].forEach(([result, day]) => { - test(`should navigate to day ${result} of previous month when up arrow pressed on day ${day} of first week of current month`, async ({ - mount, - page, - }) => { - await mount(); - - await page.focus("body"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press(arrowKeys[2]); - if (day === "1") { - const focusedElement = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: result }); - await toBeFocusedDelayed(focusedElement); - } else { - const focusedElement = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: result }); - await toBeFocusedDelayed(focusedElement); - } - const pickerHeading = dayPickerHeading(page); - await expect(pickerHeading).toHaveText(PREVIOUS_MONTH); + test(`navigates to previous month, when up arrow is pressed on a day button if a previous-month day is displayed above`, async ({ + mount, + page, + }) => { + await mount(); + + await page.getByLabel("Date").press("Tab"); + + const datePicker = page.getByTestId("date-picker"); + await datePicker.waitFor(); + + const day4Button = page.getByRole("button", { + name: "Monday, April 4th, 2022", + }); + await day4Button.press("ArrowUp"); + + const day28Button = page.getByRole("button", { + name: "Monday, March 28th, 2022", }); + await day28Button.waitFor(); + + await expect(day28Button).toBeFocused(); + + const pickerHeading = datePicker.getByRole("status"); + await expect(pickerHeading).toHaveText("March 2022"); }); - [ - ["7", "31"], - ["6", "30"], - ["5", "29"], - ["4", "28"], - ["3", "27"], - ["2", "26"], - ["1", "25"], - ].forEach(([result, day]) => { - test(`should navigate to day ${result} of next month when down arrow pressed on day ${day} of last week of current month`, async ({ - mount, - page, - }) => { - await mount(); - - await page.focus("body"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press(arrowKeys[3]); - if (day === "30" || day === "31") { - const focusedElement = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: result }); - await toBeFocusedDelayed(focusedElement); - } else { - const focusedElement = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: result }) - .filter({ hasNotText: "30" }) - .filter({ hasNotText: "31" }); - await toBeFocusedDelayed(focusedElement); - } - const pickerHeading = dayPickerHeading(page); - await expect(pickerHeading).toHaveText(NEXT_MONTH); + test("navigates to previous month, when down arrow is pressed on a day button if a previous-month day is displayed below", async ({ + mount, + page, + }) => { + await mount(); + + await page.getByLabel("Date").press("Tab"); + + const datePicker = page.getByTestId("date-picker"); + await datePicker.waitFor(); + + const day24Button = page.getByRole("button", { + name: "Sunday, April 24th, 2022", + }); + await day24Button.press("ArrowDown"); + + const day1Button = page.getByRole("button", { + name: "Sunday, May 1st, 2022", }); + await day1Button.waitFor(); + + await expect(day1Button).toBeFocused(); + + const pickerHeading = datePicker.getByRole("status"); + await expect(pickerHeading).toHaveText("May 2022"); }); ["Enter", "Space"].forEach((key) => { - test(`should update the selected date when ${key} pressed on a day element`, async ({ + test(`updates selected date when ${key} pressed on a day button`, async ({ mount, page, }) => { await mount(); - await page.focus("body"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press(arrowKeys[1]); - const focusedElement = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "13" }); - await toBeFocusedDelayed(focusedElement); - await page.keyboard.press(key); - await expect(getDataElementByValue(page, "input")).toHaveValue( - "13/04/2022", - ); + const dateInput = page.getByLabel("Date"); + await dateInput.press("Tab"); + + const datePicker = page.getByTestId("date-picker"); + await datePicker.waitFor(); + + const day25Button = page.getByRole("button", { + name: "Monday, April 25th, 2022", + }); + await day25Button.press(key); + + await expect(dateInput).toHaveValue("25/04/2022"); }); }); - test(`should close the picker when escape is pressed and input focused`, async ({ + test("closes picker and refocuses input, when escape key is pressed", async ({ mount, page, }) => { await mount(); - await page.focus("body"); - await page.keyboard.press("Tab"); - const wrapper = dayPickerWrapper(page); - await expect(wrapper).toHaveCount(1); - await page.keyboard.press("Escape"); - await expect(wrapper).toHaveCount(0); - }); + const dateInput = page.getByLabel("Date"); + await dateInput.press("Tab"); - test(`should close the picker when escape is pressed and focus is within the picker and refocus the input`, async ({ - mount, - page, - }) => { - await mount(); + const datePicker = page.getByTestId("date-picker"); + await datePicker.waitFor(); - await page.focus("body"); - await page.keyboard.press("Tab"); - const wrapper = dayPickerWrapper(page); - await expect(wrapper).toHaveCount(1); - await page.keyboard.press("Tab"); - await page.keyboard.press("Escape"); - await expect(wrapper).toHaveCount(0); - await expect(getDataElementByValue(page, "input")).toBeFocused(); + await datePicker.press("Escape"); + + await expect(dateInput).toBeFocused(); + await expect(datePicker).toBeHidden(); }); - test(`should close the picker when shift + tab is pressed and focus is on the previous month button in the picker and refocus the input`, async ({ + test("closes picker and refocuses input, when shift + tab is pressed on previous month button", async ({ mount, page, }) => { await mount(); - await page.focus("body"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - const wrapper = dayPickerWrapper(page); - await expect(wrapper).toHaveCount(1); - await page.keyboard.press("Shift+Tab"); - await expect(wrapper).toHaveCount(0); - await expect(getDataElementByValue(page, "input")).toBeFocused(); + const dateInput = page.getByLabel("Date"); + await dateInput.press("Tab"); + + const previousMonthButton = page.getByRole("button", { + name: "Previous month", + }); + await previousMonthButton.waitFor(); + + await previousMonthButton.press("Shift+Tab"); + + const datePicker = page.getByTestId("date-picker"); + await expect(dateInput).toBeFocused(); + await expect(datePicker).toBeHidden(); }); - test(`should navigate to the next month when right arrow pressed on last day element of a month`, async ({ + test("navigates to the next month, when right arrow is pressed on last day of a month button", async ({ mount, page, }) => { await mount(); - await page.focus("body"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await page.keyboard.press(arrowKeys[0]); - const focusedElement = page - .locator(`.${DAY_PICKER_PREFIX}focused`) - .locator("button") - .filter({ hasText: "1" }) - .filter({ hasNotText: "31" }); - await toBeFocusedDelayed(focusedElement); - const pickerHeading = dayPickerHeading(page); - await expect(pickerHeading).toHaveText(NEXT_MONTH); + await page.getByLabel("Date").press("Tab"); + + const datePicker = page.getByTestId("date-picker"); + await datePicker.waitFor(); + + const day31Button = page.getByRole("button", { + name: "Tuesday, May 31st, 2022", + }); + await day31Button.press("ArrowRight"); + + const day1Button = page.getByRole("button", { + name: "Wednesday, June 1st, 2022", + }); + await day1Button.waitFor(); + + await expect(day1Button).toBeFocused(); + + const pickerHeading = datePicker.getByRole("status"); + await expect(pickerHeading).toHaveText("June 2022"); }); [ @@ -770,10 +714,10 @@ test.describe("Functionality tests", () => { test(`should check the autofocus prop`, async ({ mount, page }) => { await mount(); - const input = getDataElementByValue(page, "input"); - await expect(input).toBeFocused(); const wrapper = dayPickerWrapper(page); await expect(wrapper).toBeVisible(); + const input = getDataElementByValue(page, "input"); + await expect(input).toBeFocused(); }); test("date picker does not float above the rest of the page, when disablePortal prop is true", async ({ @@ -926,298 +870,22 @@ test.describe("Functionality tests", () => { }); test.describe("When nested inside of a Dialog component", () => { - // TODO: Skipped due to flaky focus behaviour. To review in FE-6428 - test.skip("should not close the Dialog when Datepicker is closed by pressing an escape key", async ({ + test("should not close the Dialog when Datepicker is closed by pressing an escape key", async ({ mount, page, }) => { await mount(); - await page.focus("body"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - const wrapper = dayPickerWrapper(page); - const dialogElement = alertDialogPreview(page); - await expect(wrapper).toHaveCount(1); - await page.keyboard.press("Tab"); - await page.keyboard.press("Escape"); - await expect(wrapper).toHaveCount(0); - await expect(getDataElementByValue(page, "input")).toBeFocused(); - await expect(dialogElement).toBeVisible(); - await page.keyboard.press("Escape"); - await expect(dialogElement).not.toBeVisible(); - }); -}); - -test.describe("Events tests", () => { - test(`should call onChange callback when a clear event is triggered`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const input = getDataElementByValue(page, "input"); - await input.clear(); - expect(callbackCount).toBe(1); - }); - - test(`should call onChange callback when a type event is triggered`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); + const dateInput = page.getByLabel("Date"); + await dateInput.click(); - const input = getDataElementByValue(page, "input"); - await input.fill("1"); - expect(callbackCount).toBe(1); - }); - - ( - [ - ["en-US", "05/12/2022", "enUS"], - ["en-CA", "05/12/2022", "enCA"], - ["en-ZA", "12/05/2022", "enZA"], - ["de", "12.05.2022", "de"], - ["es-ES", "12/05/2022", "es"], - ["fr-FR", "12/05/2022", "fr"], - ["fr-CA", "12/05/2022", "frCA"], - ["zh-CN", "2022/05/12", "zhCN"], - ["pl-PL", "12.05.2022", "pl"], - ["bg-BG", "12.05.2022", "bg"], - ["zh-HK", "12/05/2022", "zhHK"], - ["hu-HU", "2022. 05. 12.", "hu"], - ["fi-FI", "12.05.2022", "fi"], - ["de-AT", "12.05.2022", "deAT"], - ["ko-KR", "2022. 05. 12.", "ko"], - ["ar-EG", "12/05/2022", "arEG"], - ["hi-HI", "12/05/2022", "hi"], - ["sl-SI", "12. 05. 2022", "sl"], - ["lv", "12.05.2022.", "lv"], - ] as const - ).forEach(([localeValue, formattedValueParam, dateFnsLocaleKey]) => { - test(`should use ${localeValue} locale and change the formattedValue to ${formattedValueParam} after selecting date`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - { - hooksConfig: { localeName: dateFnsLocaleKey }, - }, - ); + const datePicker = page.getByTestId("date-picker"); + await datePicker.waitFor(); - const getDatetoEnter = () => { - if (["en-US", "en-CA"].includes(localeValue)) - return MMDDYYYY_DATE_TO_ENTER; - if (["zh-CN", "hu-HU", "ko-KR"].includes(localeValue)) - return YYYYMMDD_DATE_TO_ENTER; - return DDMMYYY_DATE_TO_ENTER; - }; + await datePicker.press("Escape"); - const input = getDataElementByValue(page, "input"); - await input.fill(getDatetoEnter()); - await input.click(); - - const pickerByText = page.getByRole("gridcell").filter({ hasText: "12" }); - await pickerByText.click(); - await expect(input).toHaveValue(formattedValueParam); - - expect(callbackCount).toBe(3); - }); - }); - - ( - [ - ["en-US", "05/27/2022", "enUS"], - ["en-CA", "05/27/2022", "enCA"], - ["en-ZA", "27/05/2022", "enZA"], - ["de", "27.05.2022", "de"], - ["es-ES", "27/05/2022", "es"], - ["fr-FR", "27/05/2022", "fr"], - ["fr-CA", "27/05/2022", "frCA"], - ["zh-CN", "2022/05/27", "zhCN"], - ["pl-PL", "27.05.2022", "pl"], - ["bg-BG", "27.05.2022", "bg"], - ["zh-HK", "27/05/2022", "zhHK"], - ["hu-HU", "2022. 05. 27.", "hu"], - ["fi-FI", "27.05.2022", "fi"], - ["de-AT", "27.05.2022", "deAT"], - ["ko-KR", "2022. 05. 27.", "ko"], - ["ar-EG", "27/05/2022", "arEG"], - ["hi-HI", "27/05/2022", "hi"], - ["sl-SI", "27. 05. 2022", "sl"], - ["lv", "27.05.2022.", "lv"], - ] as const - ).forEach(([localeValue, formattedValueParam, dateFnsLocaleKey]) => { - test(`should use ${localeValue} locale and change the formattedValue to ${formattedValueParam} after typing the date`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - { - hooksConfig: { localeName: dateFnsLocaleKey }, - }, - ); - - const getDatetoEnter = () => { - if (["en-US", "en-CA"].includes(localeValue)) - return MMDDYYYY_DATE_TO_ENTER; - if (["zh-CN", "hu-HU", "ko-KR"].includes(localeValue)) - return YYYYMMDD_DATE_TO_ENTER; - return DDMMYYY_DATE_TO_ENTER; - }; - - const input = getDataElementByValue(page, "input"); - await input.fill(getDatetoEnter()); - await input.blur(); - await expect(input).toHaveValue(formattedValueParam); - - expect(callbackCount).toBe(2); - }); - }); - - ( - [ - ["en-US", "07/01/2022", "enUS"], - ["en-CA", "07/01/2022", "enCA"], - ["en-ZA", "01/07/2022", "enZA"], - ["de", "01.07.2022", "de"], - ["es-ES", "01/07/2022", "es"], - ["fr-FR", "01/07/2022", "fr"], - ["fr-CA", "01/07/2022", "frCA"], - ["zh-CN", "2022/07/01", "zhCN"], - ["pl-PL", "01.07.2022", "pl"], - ["bg-BG", "01.07.2022", "bg"], - ["zh-HK", "01/07/2022", "zhHK"], - ["hu-HU", "2022. 07. 01.", "hu"], - ["fi-FI", "01.07.2022", "fi"], - ["de-AT", "01.07.2022", "deAT"], - ["ko-KR", "2022. 07. 01.", "ko"], - ["ar-EG", "01/07/2022", "arEG"], - ["hi-HI", "01/07/2022", "hi"], - ["sl-SI", "01. 07. 2022", "sl"], - ["lv", "01.07.2022.", "lv"], - ] as const - ).forEach(([localeValue, formattedValueParam, dateFnsLocaleKey]) => { - test(`should use ${localeValue} locale and change the formattedValue to ${formattedValueParam} after typing short date`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - { - hooksConfig: { localeName: dateFnsLocaleKey }, - }, - ); - - const getDatetoEnter = () => { - if (["en-US", "en-CA"].includes(localeValue)) - return MMDDYYYY_DATE_TO_ENTER_SHORT; - if (["zh-CN", "hu-HU", "ko-KR"].includes(localeValue)) - return YYYYMMDD_DATE_TO_ENTER_SHORT; - return DDMMYYY_DATE_TO_ENTER_SHORT; - }; - - const input = getDataElementByValue(page, "input"); - await input.fill(getDatetoEnter()); - await input.blur(); - await expect(input).toHaveValue(formattedValueParam); - - expect(callbackCount).toBe(2); - }); - }); - - test(`should call onBlur callback when a blur event is triggered`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const input = getDataElementByValue(page, "input"); - await input.clear(); - await input.blur(); - expect(callbackCount).toBe(1); - }); - - test(`should call the onBlur callback using allowEmptyValue prop and output an empty and not null rawValue`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - - await mount( - { - callbackCount += 1; - }} - allowEmptyValue - />, - ); - - const input = getDataElementByValue(page, "input"); - await input.clear(); - await input.blur(); - await expect(input).toHaveAttribute("value", ""); - expect(callbackCount).toBe(1); - }); - - test(`should call the onChange callback using allowEmptyValue prop and output an empty and not null rawValue`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - - await mount( - { - callbackCount += 1; - }} - allowEmptyValue - />, - ); - - const input = getDataElementByValue(page, "input"); - await input.fill(MIN_DATE); - await input.clear(); - await expect(input).toHaveAttribute("Value", ""); - expect(callbackCount).toBe(2); + await expect(page.getByRole("dialog")).toBeVisible(); + await expect(datePicker).toBeHidden(); }); }); diff --git a/src/components/dialog-full-screen/dialog-full-screen.pw.tsx b/src/components/dialog-full-screen/dialog-full-screen.pw.tsx index b73155a14a..c89ef9100b 100644 --- a/src/components/dialog-full-screen/dialog-full-screen.pw.tsx +++ b/src/components/dialog-full-screen/dialog-full-screen.pw.tsx @@ -317,11 +317,13 @@ test.describe("render DialogFullScreen component and check properties", () => { await expect(dialogFullScreen).not.toBeVisible(); await button.click(); - await expect(dialogFullScreen).toBeVisible(); + await dialogFullScreen.waitFor(); + const closeButton = page.getByLabel("Close"); await closeButton.click(); - await expect(button).toBeFocused(); + await expect(dialogFullScreen).not.toBeVisible(); + await expect(button).toBeFocused(); }); test("when Dialog Full Screen is open on render, then closed, opened and then closed again, the call to action element should be focused", async ({ @@ -747,7 +749,6 @@ test.describe("test background scroll when tabbing", () => { }) => { await mount(); - await page.waitForTimeout(500); await continuePressingSHIFTTAB(page, 2); await iconIsFocused(page, 0); await expect( @@ -764,7 +765,6 @@ test.describe("test background scroll when tabbing", () => { , ); - await page.waitForTimeout(500); const toastIcon = getDataElementByValue(page, "close").nth(1); await toastIcon.focus(); await continuePressingTAB(page, 5); @@ -783,7 +783,6 @@ test.describe("test background scroll when tabbing", () => { , ); - await page.waitForTimeout(500); await continuePressingSHIFTTAB(page, 8); await iconIsFocused(page, 0); await expect( diff --git a/src/components/dialog/dialog.pw.tsx b/src/components/dialog/dialog.pw.tsx index 44f60c5753..0a7aed81b5 100644 --- a/src/components/dialog/dialog.pw.tsx +++ b/src/components/dialog/dialog.pw.tsx @@ -365,7 +365,6 @@ test.describe("Testing Dialog component properties", () => { const firstTextbox = page.getByLabel("Textbox1"); const closeButton = page.getByLabel("Close"); - await page.waitForTimeout(250); await dialog.press("Tab"); await expect(closeButton).toBeFocused(); @@ -395,7 +394,6 @@ test.describe("Testing Dialog component properties", () => { const firstTextbox = page.getByLabel("Textbox1"); const closeButton = page.getByLabel("Close"); - await page.waitForTimeout(250); await dialog.press("Shift+Tab"); await expect(thirdTextbox).toBeFocused(); @@ -423,7 +421,6 @@ test.describe("Testing Dialog component properties", () => { const textbox = page.getByLabel("Textbox"); const closeButton = page.getByLabel("Close"); - await page.waitForTimeout(250); await dialog.press("Tab"); await closeButton.press("Tab"); await textbox.press("Tab"); @@ -445,7 +442,6 @@ test.describe("Testing Dialog component properties", () => { const textbox = page.getByLabel("Textbox"); const closeButton = page.getByLabel("Close"); - await page.waitForTimeout(250); await dialog.press("Shift+Tab"); await textbox.press("Shift+Tab"); @@ -508,7 +504,6 @@ test.describe("when there is a button inside Dialog, which opens a Toast", () => const openToastButton = page .getByRole("button") .filter({ hasText: "Open Toast" }); - await page.waitForTimeout(250); await openToastButton.click(); const toast = toastComponent(page); @@ -527,7 +522,6 @@ test.describe("when there is a button inside Dialog, which opens a Toast", () => const openToastButton = page .getByRole("button") .filter({ hasText: "Open Toast" }); - await page.waitForTimeout(250); await openToastButton.click(); await page.mouse.click(0, 0); // click outside Toast and Dialog @@ -554,7 +548,6 @@ test.describe("when there is a button inside Dialog, which opens a Toast", () => .filter({ hasText: "Toast message 2" }) .getByLabel("Close"); - await page.waitForTimeout(250); await dialog.press("Tab"); await dialogCloseButton.press("Tab"); await textbox.press("Tab"); @@ -586,7 +579,6 @@ test.describe("when there is a button inside Dialog, which opens a Toast", () => .filter({ hasText: "Toast message 2" }) .getByLabel("Close"); - await page.waitForTimeout(250); await dialog.press("Shift+Tab"); await toast2CloseButton.press("Shift+Tab"); await toast1CloseButton.press("Shift+Tab"); diff --git a/src/components/flat-table/components.test-pw.tsx b/src/components/flat-table/components.test-pw.tsx index ee2b3c1726..d8eebd6f68 100644 --- a/src/components/flat-table/components.test-pw.tsx +++ b/src/components/flat-table/components.test-pw.tsx @@ -2793,221 +2793,6 @@ export const FlatTableLastColumnHasRowspan = () => { ); }; -export const KeyboardNavigationWithPagination = ( - props: Partial & { highlighted?: boolean }, -) => { - const { highlighted, ...rest } = props; - const rows = [ - {}} key="0"> - John Doe - London - Single - 0 - , - {}} key="1"> - Jane Doe - York - Married - 2 - , - {}} key="2"> - John Smith - Edinburgh - Single - 1 - , - {}} key="3"> - Jane Smith - Newcastle - Married - 5 - , - {}} key="4"> - Liz Anya - Stoke - Single - 2 - , - {}} key="5"> - Karl Ickbred - Newcastle - Single - 0 - , - ]; - - const [recordsRange, setRecordsRange] = useState({ - start: 0, - end: 5, - }); - const [update, setUpdate] = React.useState(false); - const [currentPage, setCurrentPage] = useState(1); - - const renderRows = () => { - const { start, end } = recordsRange; - if (start < 0) return rows; - if (end > rows.length) return rows.slice(start, rows.length); - return rows.slice(start, end); - }; - - const handlePagination = (newPage: number, newPageSize: number) => { - const start = (newPage - 1) * newPageSize; - const end = start + newPageSize; - setRecordsRange({ - start, - end, - }); - setCurrentPage(newPage); - }; - - React.useEffect(() => { - setTimeout(() => setUpdate(true), 300); - }, [update]); - - React.useEffect(() => { - setUpdate(false); - }, [currentPage]); - - const loading = ( - <> - - Loading State - - - ); - - return ( -
- handlePagination(next, size)} - pageSizeSelectionOptions={[ - { - id: "1", - name: 1, - }, - { - id: "5", - name: 5, - }, - ]} - /> - } - {...rest} - > - - - Name - Location - Relationship Status - Dependents - - - {update ? renderRows() : loading} - -
- ); -}; - -export const HighlightedRowWithLoadingState = ( - props: Partial & { - expandableArea: "wholeRow" | "firstColumn"; - }, -) => { - const { expandableArea, ...rest } = props; - const [highlighted, setHighlighted] = useState(true); - const rows = ( - <> - {}} - expandable - expandableArea={expandableArea} - > - John Doe - London - Single - 0 - - {}} - expandable - expandableArea={expandableArea} - > - Jane Doe - York - Married - 2 - - setHighlighted((p) => !p)} - expandable - expandableArea={expandableArea} - > - John Smith - Edinburgh - Single - 1 - - {}} - expandable - expandableArea={expandableArea} - > - Jane Smith - Newcastle - Married - 5 - - - ); - - const [update, setUpdate] = React.useState(false); - - React.useEffect(() => { - setTimeout(() => setUpdate(true), 300); - }, [update]); - - const loading = ( - <> - - Loading State - - - ); - - return ( -
- - - - Name - Location - Relationship Status - Dependents - - - {update ? rows : loading} - -
- ); -}; - export const FlatTableWithStickyColumn = () => ( diff --git a/src/components/flat-table/flat-table.pw.tsx b/src/components/flat-table/flat-table.pw.tsx index 84a0950359..41d609d755 100644 --- a/src/components/flat-table/flat-table.pw.tsx +++ b/src/components/flat-table/flat-table.pw.tsx @@ -26,8 +26,6 @@ import { FlatTableParentSubrowSelectableComponent, FlatTableChildSubrowSelectableComponent, FlatTablePartiallySelectedOrHighlightedRows, - KeyboardNavigationWithPagination, - HighlightedRowWithLoadingState, FlatTableDraggableComponent, FlatTablePagerStickyHeaderComponent, FlatTableCheckboxComponent, @@ -1575,56 +1573,6 @@ test.describe("Prop tests", () => { await checkNewFocusStyling(bodyRow3); }); - test(`should render with expandable rows expanded by Spacebar and subrows not accessible`, async ({ - mount, - page, - }) => { - await mount(); - - const transformValue = await getStyle( - flatTableExpandableIcon(page, 0), - "transform", - ); - expect(getRotationAngle(transformValue)).toBe(-90); - await expect(flatTableSubrows(page)).toHaveCount(0); - - const bodyRowByPosition = flatTableBodyRowByPosition(page, 0); - await bodyRowByPosition.focus(); - await bodyRowByPosition.press("Space"); - await expect(flatTableSubrowByPosition(page, 0)).toHaveCount(1); - await expect(flatTableSubrowByPosition(page, 1)).toHaveCount(1); - - await bodyRowByPosition.press("Tab"); - await bodyRowByPosition.press("ArrowDown"); - await page.waitForTimeout(250); - await checkNewFocusStyling(flatTableBodyRowByPosition(page, 3)); - }); - - test(`should render with expandable rows expanded by Enter key and subrows not accessible`, async ({ - mount, - page, - }) => { - await mount(); - - const transformValue = await getStyle( - flatTableExpandableIcon(page, 0), - "transform", - ); - expect(getRotationAngle(transformValue)).toBe(-90); - await expect(flatTableSubrows(page)).toHaveCount(0); - - const bodyRowByPosition = flatTableBodyRowByPosition(page, 0); - await bodyRowByPosition.focus(); - await bodyRowByPosition.press("Enter"); - await expect(flatTableSubrowByPosition(page, 0)).toHaveCount(1); - await expect(flatTableSubrowByPosition(page, 1)).toHaveCount(1); - - await bodyRowByPosition.press("Tab"); - await bodyRowByPosition.press("ArrowDown"); - await page.waitForTimeout(250); - await checkNewFocusStyling(flatTableBodyRowByPosition(page, 3)); - }); - test(`should render with a clickable link in an expandable row`, async ({ mount, page, @@ -2024,54 +1972,6 @@ test.describe("Prop tests", () => { }); }); - test(`should render with the first column of cells accessible with down arrow key press when expandableArea is set to 'firstColumn'`, async ({ - mount, - page, - }) => { - await mount(); - - const tableCell0 = flatTableCell(page, 0); - const tableCell4 = flatTableCell(page, 4); - const tableCell8 = flatTableCell(page, 8); - const tableCell12 = flatTableCell(page, 12); - - await page.waitForTimeout(300); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await expect(tableCell0).toBeFocused(); - await tableCell0.press("ArrowDown"); - await expect(tableCell4).toBeFocused(); - await tableCell4.press("ArrowDown"); - await expect(tableCell8).toBeFocused(); - await tableCell8.press("ArrowDown"); - await expect(tableCell12).toBeFocused(); - await tableCell12.press("ArrowDown"); - await expect(tableCell12).toBeFocused(); - }); - - test(`should render with the first column of cells accessible with up arrow key press when expandableArea is set to 'firstColumn'`, async ({ - mount, - page, - }) => { - await mount(); - - const tableCell0 = flatTableCell(page, 0); - const tableCell4 = flatTableCell(page, 4); - const tableCell8 = flatTableCell(page, 8); - const tableCell12 = flatTableCell(page, 12); - - await tableCell12.focus(); - await expect(tableCell12).toBeFocused(); - await tableCell12.press("ArrowUp"); - await expect(tableCell8).toBeFocused(); - await tableCell8.press("ArrowUp"); - await expect(tableCell4).toBeFocused(); - await tableCell4.press("ArrowUp"); - await expect(tableCell0).toBeFocused(); - await tableCell0.press("ArrowUp"); - await expect(tableCell0).toBeFocused(); - }); - test(`should render with any focusable rows accessible, including expanded sub rows, with down arrow key`, async ({ mount, page, @@ -2131,92 +2031,6 @@ test.describe("Prop tests", () => { await expect(bodyRowByPos0).toBeFocused(); }); - test(`should render with tabIndex 0 on the first row when after the loading state has finished`, async ({ - mount, - page, - }) => { - await mount(); - - await page.waitForTimeout(300); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await checkNewFocusStyling(flatTableBodyRowByPosition(page, 0)); - await page.keyboard.press("Tab"); - await expect(flatTableBodyRowByPosition(page, 0)).not.toBeFocused(); - await expect(flatTableBodyRowByPosition(page, 1)).not.toBeFocused(); - await expect(flatTableBodyRowByPosition(page, 2)).not.toBeFocused(); - await expect(flatTableBodyRowByPosition(page, 3)).not.toBeFocused(); - await flatTablePageSelectNext(page).click(); - await page.waitForTimeout(300); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await checkNewFocusStyling(flatTableBodyRowByPosition(page, 0)); - }); - - test(`should render with the tabIndex on the highlighted row when the loading state has finished`, async ({ - mount, - page, - }) => { - await mount(); - - await page.waitForTimeout(300); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await expect(flatTableBodyRowByPosition(page, 0)).not.toBeFocused(); - await checkNewFocusStyling(flatTableBodyRowByPosition(page, 2)); - await page.keyboard.press("Tab"); - await expect(flatTableBodyRowByPosition(page, 3)).not.toBeFocused(); - await flatTablePageSelectNext(page).click(); - await page.waitForTimeout(300); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await checkNewFocusStyling(flatTableBodyRowByPosition(page, 0)); - }); - - test(`should render with the tabIndex on the highlighted row when the loading state has finished and remove it when row is no longer highlighted`, async ({ - mount, - page, - }) => { - await mount(); - - await page.waitForTimeout(300); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await expect(flatTableBodyRowByPosition(page, 0)).not.toBeFocused(); - await expect(flatTableBodyRowByPosition(page, 2)).toBeFocused(); - await page.keyboard.press("Tab"); - await expect(flatTableBodyRowByPosition(page, 3)).not.toBeFocused(); - await flatTableBodyRowByPosition(page, 2).click(); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await expect(flatTableBodyRowByPosition(page, 0)).toBeFocused(); - }); - - // TODO: Skipped due to flaky focus behaviour. To review in FE-6428 - test.skip(`should render with the tabIndex on the first cell in a highlighted row when the loading state has finished and remove it when row is no longer highlighted`, async ({ - mount, - page, - }) => { - await mount( - , - ); - - await page.waitForTimeout(300); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await waitForAnimationEnd(flatTable(page)); - await expect(flatTableCell(page, 0)).not.toBeFocused(); - await expect(flatTableCell(page, 8)).toBeFocused(); - await page.keyboard.press("Tab"); - await waitForAnimationEnd(flatTable(page)); - await expect(flatTableCell(page, 12)).not.toBeFocused(); - await flatTableCell(page, 8).click(); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await waitForAnimationEnd(flatTable(page)); - await expect(flatTableCell(page, 0)).toBeFocused(); - }); - test(`should render with action popover in a cell opened by mouse`, async ({ mount, page, @@ -2573,97 +2387,6 @@ test.describe("Prop tests", () => { }); }); -test.describe("Event tests", () => { - test(`should call getOrder when draggable row order is changed`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - await page.waitForTimeout(1000); - const draggableItem = flatTableDraggableItem(page, 0); - const dropPosition = flatTableDraggableItemByPosition(page, 3); - await page.waitForTimeout(1000); - await draggableItem.dragTo(dropPosition); - expect(callbackCount).toBe(1); - }); - - test(`should call onClick when a clickable row is clicked`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - await flatTableBodyRowByPosition(page, 0).click(); - expect(callbackCount).toBe(1); - }); - - test(`should call onChange when a selectable row is clicked`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - await flatTableCheckboxCell(page, 1).locator("input").click(); - expect(callbackCount).toBe(1); - }); - - test(`should call onClick when a selectable row is clicked`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - await flatTableCheckboxCell(page, 1).locator("input").click(); - expect(callbackCount).toBe(1); - }); - - test(`should call onClick when first column is sorted`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - await flatTableSortable(page).nth(0).click(); - expect(callbackCount).toBe(1); - }); -}); - test.describe("Accessibility tests", () => { test(`should render with ariaDescribedBy for accessibility tests`, async ({ mount, diff --git a/src/components/global-header/global-header.pw.tsx b/src/components/global-header/global-header.pw.tsx index 0ae1ec991a..262e598e9e 100644 --- a/src/components/global-header/global-header.pw.tsx +++ b/src/components/global-header/global-header.pw.tsx @@ -6,10 +6,7 @@ import { GlobalHeaderWithLogo, } from "./component.test-pw"; import navigationBar from "../../../playwright/components/navigation-bar"; -import { - globalHeader, - globalHeaderLogo, -} from "../../../playwright/components/global-header"; +import { globalHeaderLogo } from "../../../playwright/components/global-header"; import { checkAccessibility } from "../../../playwright/support/helper"; test.describe("Global Header component", () => { @@ -19,7 +16,8 @@ test.describe("Global Header component", () => { }) => { await mount(); - await page.waitForTimeout(500); + const globalHeader = page.getByRole("navigation"); + await globalHeader.waitFor({ state: "visible" }); const errorState = page.locator("#error-div"); await expect(errorState).toHaveText(""); @@ -31,10 +29,13 @@ test.describe("Global Header component", () => { }) => { await mount(); - const globalHeaderZIndex = await globalHeader(page).evaluate((element) => { - const style = getComputedStyle(element); - return style.zIndex; - }); + const globalHeaderZIndex = await page + .getByRole("navigation") + .filter({ hasText: "Product Switcher" }) + .evaluate((element) => { + const style = getComputedStyle(element); + return style.zIndex; + }); const navigationBarZIndex = await navigationBar(page).evaluate( (element) => { diff --git a/src/components/menu/menu.pw.tsx b/src/components/menu/menu.pw.tsx index 62c5765835..81badd9c79 100644 --- a/src/components/menu/menu.pw.tsx +++ b/src/components/menu/menu.pw.tsx @@ -62,7 +62,6 @@ import { MenuItems, ClosedMenuFullScreenWithButtons, MenuDividerComponent, - InGlobalHeaderStory, SubMenuWithVeryLongLabel, MenuComponentScrollableWithSearch, MenuSegmentTitleComponentWithAdditionalMenuItem, @@ -70,7 +69,6 @@ import { MenuItemWithPopoverContainerChild, SubmenuMaxWidth, } from "./component.test-pw"; -import { NavigationBarWithSubmenuAndChangingHeight } from "../navigation-bar/navigation-bar-test.stories"; const span = "span"; const div = "div"; @@ -442,25 +440,6 @@ test.describe("Prop tests for Menu component", () => { expect(boundLeft).toBeGreaterThan(leftLess); }); - test(`should verify the Search component close icon is focusable when using keyboard to navigate up the list of items`, async ({ - mount, - page, - }) => { - await mount(); - - await page.keyboard.press("Tab"); - await page.keyboard.press("Enter"); - await page.keyboard.press("ArrowDown"); - await searchDefaultInput(page).fill("FooBar"); - await page.keyboard.press("End"); - await continuePressingSHIFTTAB(page, 2); - await page.waitForTimeout(2000); - const cross = searchCrossIcon(page).locator(".."); - await expect(cross).toBeFocused(); - await page.keyboard.press("Shift+Tab"); - await expect(searchDefaultInput(page)).toBeFocused(); - }); - test(`should verify that the Search component is focusable by using the downarrow key when rendered as the parent of a scrollable submenu`, async ({ mount, page, @@ -2436,54 +2415,26 @@ test.describe("Styling, Scrolling & Navigation Bar Tests for Menu Component", () await expect(offscreenText).not.toBeInViewport(); }); - test(`should render with all the content of a long submenu accessible with the keyboard while remaining visible`, async ({ - mount, + test("submenu items in a scrollable block scroll into view when focused", async ({ page, + mount, }) => { - await mount(); + await page.setViewportSize({ width: 1200, height: 800 }); + await mount(); - await page.setViewportSize({ width: 1000, height: 500 }); - await page.keyboard.press("Tab"); - await page.keyboard.press("ArrowDown"); - const subMenuItem = submenuItem(page, 1); - await expect(subMenuItem).toHaveCount(20); - for (let i = 0; i < 20; i++) { - await page.keyboard.press("ArrowDown"); - } - const focusedElement = page.locator("*:focus"); - await expect(focusedElement).toHaveText("Foo 20"); - const subMenu = page.locator( - '[data-component="submenu-wrapper"] ul > li:nth-child(20)', - ); - await expect(subMenu).toBeInViewport(); - }); + const menuItemThree = page + .getByRole("listitem") + .filter({ hasText: "Menu Item Three" }) + .first(); + await menuItemThree.getByRole("button").press("ArrowDown"); - test(`should render with all the content of a long submenu accessible with the keyboard while remaining visible if the navbar height changes`, async ({ - mount, - page, - }) => { - await mount(); + const submenuList = menuItemThree.getByRole("list"); + await submenuList.waitFor(); - await page.setViewportSize({ width: 1000, height: 500 }); - await page.keyboard.press("Tab"); - await page.keyboard.press("ArrowDown"); - const subMenuItem = submenuItem(page, 1); - await expect(subMenuItem).toHaveCount(21); - for (let i = 0; i < 3; i++) { - await page.keyboard.press("ArrowDown"); - } - await page.keyboard.press("Enter"); + const lastSubmenuItem = menuItemThree.getByRole("listitem").last(); + await lastSubmenuItem.getByRole("link").focus(); - await page.waitForTimeout(100); - await page.keyboard.press("Tab"); - await page.keyboard.press("ArrowDown"); - for (let i = 0; i < 21; i++) { - await page.keyboard.press("ArrowDown"); - } - const subMenu = page.locator( - '[data-component="submenu-wrapper"] ul > li:nth-child(21)', - ); - await expect(subMenu).toBeInViewport(); + await expect(lastSubmenuItem).toBeInViewport(); }); test("should render the menu with the expected styling when menu item has a PopoverContainer child with renderOpenComponent passed", async ({ diff --git a/src/components/multi-action-button/multi-action-button.pw.tsx b/src/components/multi-action-button/multi-action-button.pw.tsx index 44638574a2..8776ad0ff2 100644 --- a/src/components/multi-action-button/multi-action-button.pw.tsx +++ b/src/components/multi-action-button/multi-action-button.pw.tsx @@ -230,26 +230,25 @@ test.describe("Functional tests", () => { }); }); - test(`should verify dialog is not closed by pressing Esc key when nest inside of a Dialog`, async ({ + test(`does not close parent dialog when Escape key is pressed and child button list is visible`, async ({ mount, page, }) => { await mount(); - const actionButton = getComponent(page, "multi-action-button").locator( - "button", - ); - await actionButton.click(); - const listButton1 = getDataElementByValue(page, "additional-buttons") - .locator("span > span") - .nth(0); - await expect(listButton1).toHaveCount(1); - await page.keyboard.press("Escape"); - await expect(listButton1).toHaveCount(0); const dialog = page.getByRole("dialog"); - await expect(dialog).toHaveCount(1); + await dialog.waitFor(); + + const actionButton = page.getByRole("button", { name: "default text " }); + await actionButton.click(); + + const buttonList = page.getByRole("list"); + await buttonList.waitFor(); + await page.keyboard.press("Escape"); - await expect(dialog).toHaveCount(0); + + await buttonList.waitFor({ state: "hidden" }); + await expect(dialog).toBeVisible(); }); test(`should verify pressing ArrowUp key does not loop focus to bottom`, async ({ diff --git a/src/components/numeral-date/numeral-date.pw.tsx b/src/components/numeral-date/numeral-date.pw.tsx index b5c4f5deae..121d28509d 100644 --- a/src/components/numeral-date/numeral-date.pw.tsx +++ b/src/components/numeral-date/numeral-date.pw.tsx @@ -640,50 +640,6 @@ test.describe("NumeralDate component", () => { await expect(help).toHaveAttribute("aria-label", CHARACTERS.STANDARD); }); - test.describe("Event tests for NumeralDate component", () => { - test("should call onChange callback when a type event is triggered", async ({ - mount, - page, - }) => { - const inputValue = "1"; - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - await numeralDateInput(page, 0).fill(inputValue); - - expect(callbackCount).toBe(1); - }); - - test("should call onBlur callback when a blur event is triggered", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const input = numeralDateInput(page, 0); - await input.focus(); - await input.blur(); - - await page.waitForTimeout(50); - - await expect(input).not.toBeFocused(); - expect(callbackCount).toBe(1); - }); - }); - test.describe("Accessibility tests for NumeralDate component", () => { test("should pass accessibility tests for NumeralDate component", async ({ mount, diff --git a/src/components/popover-container/components.test-pw.tsx b/src/components/popover-container/components.test-pw.tsx index 646437080d..b70b65acdc 100644 --- a/src/components/popover-container/components.test-pw.tsx +++ b/src/components/popover-container/components.test-pw.tsx @@ -1,10 +1,5 @@ import React, { useState } from "react"; import Button from "../button"; -import Link from "../link"; -import { DraggableContainer, DraggableItem } from "../draggable"; -import { Checkbox } from "../checkbox"; -import Pill from "../pill"; -import Badge from "../badge"; import Box from "../box"; import { Select, Option } from "../select"; import RadioButton, { RadioButtonGroup } from "../radio-button"; @@ -75,43 +70,6 @@ export const PopoverContainerWithSelect = () => { ); }; -export const Title = () => { - const [open, setOpen] = useState(false); - const onOpen = () => setOpen(true); - const onClose = () => setOpen(false); - return ( - - - Contents - - - ); -}; - -export const Position = () => { - const [open, setOpen] = useState(false); - const onOpen = () => setOpen(true); - const onClose = () => setOpen(false); - return ( -
- - Contents - -
- ); -}; - export const CoverButton = ({ children }: { children?: React.ReactNode }) => { const [open, setOpen] = useState(false); const onOpen = () => setOpen(true); @@ -131,240 +89,6 @@ export const CoverButton = ({ children }: { children?: React.ReactNode }) => { ); }; -export const RenderProps = () => ( - - ( - - )} - renderCloseComponent={({ - "data-element": dataElement, - onClick, - ref, - "aria-label": ariaLabel, - }) => ( - - )} - > - Content - - -); - -export const Controlled = () => { - const [open, setOpen] = useState(false); - const onOpen = () => setOpen(true); - const onClose = () => setOpen(false); - return ( - - - -
- - Contents - -
- ); -}; - -export const Complex = () => { - const [open, setOpen] = useState(false); - const onOpen = () => setOpen(true); - const onClose = () => setOpen(false); - return ( - - - This is example link text - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export const Filter = () => { - type OptionsType = { - value: string; - checked: boolean; - }; - - const initValues: OptionsType[] = [ - { value: "Option 1", checked: false }, - { value: "Option 2", checked: false }, - { value: "Option 3", checked: false }, - ]; - const [open, setOpen] = useState(false); - const [options, setOptions] = useState(initValues); - const [filters, setFilters] = useState([]); - const clearAllOptions = () => { - const temps = options; - for (let i = 0; i < temps.length; i++) { - temps[i].checked = false; - } - setOptions([...temps]); - }; - const clearFilters = () => setFilters([]); - const updateCheckValue = (e: React.ChangeEvent) => { - const temps = options; - const findCorrectIndex = temps.findIndex( - (item) => item.value === e.target.value, - ); - if (findCorrectIndex !== -1) { - temps[findCorrectIndex].checked = !temps[findCorrectIndex].checked; - setOptions([...temps]); - } - }; - const updateFilters = () => - setFilters(options.filter((filter) => filter.checked === true)); - const handleBadgeClose = () => { - clearAllOptions(); - clearFilters(); - }; - const applyFilters = () => { - updateFilters(); - setOpen(false); - }; - const onOpen = () => { - setOpen(true); - }; - const onClose = () => { - setOpen(false); - }; - const renderCheckboxes = () => { - const checkboxStyle = { - marginBottom: "10px", - }; - return options.map((option) => { - return ( - - ); - }); - }; - const renderPills = () => { - return filters.map((filter) => { - return filter.checked ? ( - - {filter.value} - - ) : null; - }); - }; - return ( - - ( - - - - )} - renderCloseComponent={() => { - return <>; - }} - > - {renderCheckboxes()} - - - {renderPills()} - - ); -}; - export const DisableAnimation = () => ( { - testData.forEach((title) => { - test(`should render title using ${title} special characters`, async ({ - mount, - page, - }) => { - await mount(); - - await expect(popoverContainerTitle(page)).toHaveText(title); - }); - }); - - [true, false].forEach((openValue) => { - test(`should render when open prop is set to ${openValue}`, async ({ - mount, - page, - }) => { - await mount(); - - const popoverContainerContentElement = popoverContainerContent(page); - - if (openValue) { - await expect(popoverContainerContentElement).toBeVisible(); - } else { - await expect(popoverContainerContentElement).not.toBeVisible(); - } - }); - }); - ( [ ["left", 131, 670, 560, 345], @@ -175,15 +133,6 @@ test.describe("Check props of Popover Container component", () => { }); }); - test("should render with ariaDescribedBy", async ({ mount, page }) => { - await mount(); - - await expect(popoverContainerContent(page)).toHaveAttribute( - "aria-describedby", - testString, - ); - }); - test("should render with openButtonAriaLabel", async ({ mount, page }) => { await mount(); @@ -285,49 +234,6 @@ test.describe("Check props of Popover Container component", () => { }); }); - keysToTrigger.forEach((key) => { - test(`should open using ${key} keyboard key`, async ({ mount, page }) => { - await mount( - Contents, - ); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.press(key); - - await expect(popoverContainerContent(page)).toBeVisible(); - }); - }); - - keysToTrigger.forEach((key) => { - test(`should close using ${key} keyboard key`, async ({ mount, page }) => { - await mount( - Contents, - ); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.click(); - const popoverCloseIconElement = popoverCloseIcon(page); - await popoverCloseIconElement.press(key); - - await expect(popoverContainerContent(page)).not.toBeVisible(); - }); - }); - - test("should close using escape keyboard key", async ({ mount, page }) => { - await mount( - Contents, - ); - - const openButton = page.getByRole("button", { name: "String is awesome" }); - await openButton.click(); - const popoverContainer = page.getByRole("dialog", { - name: "String is awesome", - }); - await popoverContainer.press("Escape"); - - await expect(popoverContainer).toBeHidden(); - }); - test("should not close when an option is selected from a Select component inside", async ({ mount, page, @@ -383,180 +289,38 @@ test.describe("Check props of Popover Container component", () => { await expect(popoverContainer).toBeHidden(); }); - test.describe("Event tests", () => { - test("should call onOpen callback when a click event is triggered", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.click(); - - expect(callbackCount).toBe(1); - }); - - keysToTrigger.forEach((key) => { - test(`should call onOpen callback when a keyboard event is triggered by ${key} key`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - />, - ); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.press(key); - - expect(callbackCount).toBe(1); - }); - }); - - test("should call onClose callback when a click event is triggered", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - open - />, - ); - - const popoverCloseIconElement = popoverCloseIcon(page); - await popoverCloseIconElement.click(); - - expect(callbackCount).toBe(1); - }); - - keysToTrigger.forEach((key) => { - test(`should call onClose callback when a keyboard event is triggered by ${key} key`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - open - />, - ); - - const popoverCloseIconElement = popoverCloseIcon(page); - await popoverCloseIconElement.press(key); - - expect(callbackCount).toBe(1); - }); - }); - - test("should call onClose callback when the escape key is pressed", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - open - />, - ); - - const popoverContainerElement = popoverContainerComponent(page); - await popoverContainerElement.press("Escape"); - - expect(callbackCount).toBe(1); - }); - - test("should call onClose callback when a click event is triggered outside the container", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - open - />, - ); - - await page.locator("body").click(); - - expect(callbackCount).toBe(1); - }); - - test("should not call onClose callback when a click event is triggered outside the container and the container is closed", async ({ - mount, - page, - }) => { - let callbackCount = 0; - await mount( - { - callbackCount += 1; - }} - open={false} - />, - ); - - await page.locator("body").click(); - - expect(callbackCount).toBe(0); - }); - - test("should focus the next element after the open button when user tabs and last element in the container is focused", async ({ - mount, - page, - }) => { - await mount(); + test("should focus the next element after the open button when user tabs and last element in the container is focused", async ({ + mount, + page, + }) => { + await mount(); - const childButton = page.getByText("Inside container"); - const siblingButton = page.getByText("After open button"); - const container = popoverContainerContent(page); + const childButton = page.getByText("Inside container"); + const siblingButton = page.getByText("After open button"); + const container = popoverContainerContent(page); - await childButton.focus(); - await expect(childButton).toBeFocused(); - await childButton.press("Tab"); - await expect(siblingButton).toBeFocused(); - await expect(container).not.toBeVisible(); - }); + await childButton.focus(); + await expect(childButton).toBeFocused(); + await childButton.press("Tab"); + await expect(siblingButton).toBeFocused(); + await expect(container).not.toBeVisible(); + }); - test("should focus the open button element when user back tabs and first element in the container is focused", async ({ - mount, - page, - }) => { - await mount(); + test("should focus the open button element when user back tabs and first element in the container is focused", async ({ + mount, + page, + }) => { + await mount(); - const closeButton = popoverCloseIcon(page); - const openButton = popoverSettingsIcon(page); - const container = popoverContainerContent(page); + const closeButton = popoverCloseIcon(page); + const openButton = popoverSettingsIcon(page); + const container = popoverContainerContent(page); - await closeButton.focus(); - await expect(closeButton).toBeFocused(); - await closeButton.press("Shift+Tab"); - await expect(openButton).toBeFocused(); - await expect(container).not.toBeVisible(); - }); + await closeButton.focus(); + await expect(closeButton).toBeFocused(); + await closeButton.press("Shift+Tab"); + await expect(openButton).toBeFocused(); + await expect(container).not.toBeVisible(); }); test("should trap focus when user is tabbing and the container covers the trigger button", async ({ @@ -654,112 +418,17 @@ test.describe("Check props of Popover Container component", () => { await checkAccessibility(page); }); - test("should check accessibility for Title example", async ({ - mount, - page, - }) => { - await mount(); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.click(); - - await checkAccessibility(page); - }); - - test("should check accessibility for Position example", async ({ - mount, - page, - }) => { - await mount(<Position />); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.click(); - - await checkAccessibility(page); - }); - test("should check accessibility for CoverButton example", async ({ mount, page, }) => { await mount(<CoverButton />); - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.click(); - - await checkAccessibility(page); - }); - - test("should check accessibility for RenderProps example", async ({ - mount, - page, - }) => { - await mount(<RenderProps />); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.click(); - - await checkAccessibility(page); - }); + const openButton = page.getByRole("button"); + await openButton.click(); - test("should check accessibility for Controlled example", async ({ - mount, - page, - }) => { - await mount(<Controlled />); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.click(); - - await checkAccessibility(page); - }); - - test("should check accessibility for Complex example", async ({ - mount, - page, - }) => { - await mount(<Complex />); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.click(); - - await checkAccessibility(page); - }); - - test("should check accessibility for Filter example", async ({ - mount, - page, - }) => { - await mount(<Filter />); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.click(); - - await checkAccessibility(page); - }); - - test("should check accessibility for Filter example with filter button clicked", async ({ - mount, - page, - }) => { - await mount(<Filter />); - - const popoverSettingsIconElement = popoverSettingsIcon(page); - await popoverSettingsIconElement.click(); - const checkboxElementOne = page.locator(`[role="checkbox"]`).nth(0); - const checkboxElementTwo = page.locator(`[role="checkbox"]`).nth(1); - const checkboxElementThree = page.locator(`[role="checkbox"]`).nth(2); - - await checkboxElementOne.check(); - await checkboxElementTwo.check(); - await checkboxElementThree.check(); - const mainTextElement = getDataElementByValue(page, "main-text").nth(1); - await mainTextElement.click(); - - const assertions = [0, 1, 2].map((index) => - expect(getComponent(page, "pill").nth(index)).toBeVisible(), - ); - await Promise.all(assertions); + const popoverTitle = page.getByText("Cover Button"); + await popoverTitle.waitFor(); await checkAccessibility(page); }); diff --git a/src/components/popover-container/popover-container.test.tsx b/src/components/popover-container/popover-container.test.tsx index 8cdf71c7a5..987dcd5824 100644 --- a/src/components/popover-container/popover-container.test.tsx +++ b/src/components/popover-container/popover-container.test.tsx @@ -361,7 +361,7 @@ describe("opening the popup", () => { expect(popup).toHaveTextContent("Ta da!"); }); - it("calls onOpen callback when popup is opened", async () => { + it("calls onOpen callback when open button is clicked", async () => { const onOpen = jest.fn(); const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); render(<PopoverContainer onOpen={onOpen}>Content</PopoverContainer>); @@ -373,6 +373,24 @@ describe("opening the popup", () => { }); }); + it.each(["Space", "Enter"])( + "calls onOpen callback when open button is opened via the %s key", + async (key) => { + const onOpen = jest.fn(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(<PopoverContainer onOpen={onOpen}>Content</PopoverContainer>); + + act(() => { + screen.getByRole("button").focus(); + }); + await user.keyboard(`[${key}]`); + + await waitFor(() => { + expect(onOpen).toHaveBeenCalledTimes(1); + }); + }, + ); + it("open button still has focus after popup is opened", async () => { const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); render(<PopoverContainer>Ta da!</PopoverContainer>); @@ -461,7 +479,7 @@ describe("closing the popup", () => { expect(button).toHaveFocus(); }); - it("calls onClose callback when popup is closed", async () => { + it("calls onClose callback when close button is clicked", async () => { const onClose = jest.fn(); const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); render(<PopoverContainer onClose={onClose}>Content</PopoverContainer>); @@ -475,6 +493,48 @@ describe("closing the popup", () => { }); }); + it.each(["Space", "Enter"])( + `calls onClose callback when close button is pressed via the %s key`, + async (key) => { + const onClose = jest.fn(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render(<PopoverContainer onClose={onClose}>Content</PopoverContainer>); + + await user.click(screen.getByRole("button")); + + const closeButton = await screen.findByRole("button", { name: "close" }); + + act(() => { + closeButton.focus(); + }); + await user.keyboard(`[${key}]`); + + await waitFor(() => { + expect(onClose).toHaveBeenCalledTimes(1); + }); + }, + ); + + it("calls onClose when content outside of the popup is clicked", async () => { + const onClose = jest.fn(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + render( + <> + <PopoverContainer onClose={onClose}>Content</PopoverContainer> + <p>Outside popup</p> + </>, + ); + + await user.click(screen.getByRole("button")); + await screen.findByRole("dialog"); + + await user.click(screen.getByText("Outside popup")); + + await waitFor(() => { + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); + it("closes popup when open button is clicked twice", async () => { const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); render( diff --git a/src/components/select/filterable-select/components.test-pw.tsx b/src/components/select/filterable-select/components.test-pw.tsx index 67a5c8ed9a..a8e34c2a4c 100644 --- a/src/components/select/filterable-select/components.test-pw.tsx +++ b/src/components/select/filterable-select/components.test-pw.tsx @@ -45,177 +45,6 @@ export const FilterableSelectComponent = ( ); }; -export const FilterableSelectWithLazyLoadingComponent = ( - props: Partial<FilterableSelectProps>, -) => { - const preventLoading = useRef(false); - const [value, setValue] = useState("black"); - const [isLoading, setIsLoading] = useState(true); - const asyncList = [ - <Option text="Amber" value="amber" key="Amber" />, - <Option text="Black" value="black" key="Black" />, - <Option text="Blue" value="blue" key="Blue" />, - <Option text="Brown" value="brown" key="Brown" />, - <Option text="Green" value="green" key="Green" />, - ]; - const [optionList, setOptionList] = useState([ - <Option text="Black" value="black" key="Black" />, - ]); - function onChangeHandler(event: React.ChangeEvent<HTMLInputElement>) { - setValue(event.target.value); - } - function loadList() { - if (preventLoading.current) { - return; - } - preventLoading.current = true; - setIsLoading(true); - setTimeout(() => { - setIsLoading(false); - setOptionList(asyncList); - }, 2000); - } - return ( - <FilterableSelect - label="color" - value={value} - onChange={onChangeHandler} - onOpen={() => loadList()} - isLoading={isLoading} - {...props} - > - {optionList} - </FilterableSelect> - ); -}; - -export const FilterableSelectLazyLoadTwiceComponent = ( - props: Partial<FilterableSelectProps>, -) => { - const preventLoading = useRef(false); - const [value, setValue] = useState(""); - const [isLoading, setIsLoading] = useState(true); - const asyncList = [ - <Option text="Amber" value="amber" key="Amber" />, - <Option text="Black" value="black" key="Black" />, - <Option text="Blue" value="blue" key="Blue" />, - <Option text="Brown" value="brown" key="Brown" />, - <Option text="Green" value="green" key="Green" />, - ]; - const [optionList, setOptionList] = useState<React.ReactElement[]>([]); - function loadList() { - if (preventLoading.current) { - return; - } - preventLoading.current = true; - setIsLoading(true); - setTimeout(() => { - setIsLoading(false); - setOptionList(asyncList); - }, 2000); - } - function clearData() { - setOptionList([]); - setValue(""); - preventLoading.current = false; - } - return ( - <div> - <Button onClick={clearData} mb={2} data-element="reset-button"> - reset - </Button> - <FilterableSelect - label="color" - value={value} - onChange={(event) => setValue(event.target.value)} - onOpen={() => loadList()} - isLoading={isLoading} - {...props} - > - {optionList} - </FilterableSelect> - </div> - ); -}; - -export const FilterableSelectWithInfiniteScrollComponent = ( - props: Partial<FilterableSelectProps>, -) => { - const preventLoading = useRef(false); - const preventLazyLoading = useRef(false); - const lazyLoadingCounter = useRef(0); - const [value, setValue] = useState(""); - const [isLoading, setIsLoading] = useState(true); - const asyncList = [ - <Option text="Amber" value="amber" key="Amber" />, - <Option text="Black" value="black" key="Black" />, - <Option text="Blue" value="blue" key="Blue" />, - <Option text="Brown" value="brown" key="Brown" />, - <Option text="Green" value="green" key="Green" />, - ]; - const getLazyLoaded = () => { - const counter = lazyLoadingCounter.current; - return [ - <Option - text={`Lazy Loaded A${counter}`} - value={`lazyA${counter}`} - key={`lazyA${counter}`} - />, - <Option - text={`Lazy Loaded B${counter}`} - value={`lazyB${counter}`} - key={`lazyB${counter}`} - />, - <Option - text={`Lazy Loaded C${counter}`} - value={`lazyC${counter}`} - key={`lazyC${counter}`} - />, - ]; - }; - const [optionList, setOptionList] = useState<React.ReactElement[]>([]); - function onChangeHandler(event: React.ChangeEvent<HTMLInputElement>) { - setValue(event.target.value); - } - function loadList() { - if (preventLoading.current) { - return; - } - preventLoading.current = true; - setIsLoading(true); - setTimeout(() => { - setIsLoading(false); - setOptionList(asyncList); - }, 2000); - } - function onLazyLoading() { - if (preventLazyLoading.current) { - return; - } - preventLazyLoading.current = true; - setIsLoading(true); - setTimeout(() => { - preventLazyLoading.current = false; - lazyLoadingCounter.current += 1; - setIsLoading(false); - setOptionList((prevList) => [...prevList, ...getLazyLoaded()]); - }, 2000); - } - return ( - <FilterableSelect - label="color" - value={value} - onChange={onChangeHandler} - onOpen={() => loadList()} - isLoading={isLoading} - onListScrollBottom={onLazyLoading} - {...props} - > - {optionList} - </FilterableSelect> - ); -}; - export const FilterableSelectObjectAsValueComponent = ( props: Partial<FilterableSelectProps>, ) => { @@ -412,61 +241,7 @@ export const FilterableSelectWithActionButtonComponent = () => { ); }; -export const FilterableSelectOnChangeEventComponent = ({ - onChange, - ...props -}: Partial<FilterableSelectProps>) => { - const [state, setState] = useState(""); - const setValue = (event: React.ChangeEvent<HTMLInputElement>) => { - setState(event.target.value); - if (onChange) { - onChange(event); - } - }; - return ( - <FilterableSelect - label="color" - value={state} - labelInline - onChange={setValue} - {...props} - > - <Option text="Amber" value="1" /> - <Option text="Black" value="2" /> - <Option text="Blue" value="3" /> - <Option text="Brown" value="4" /> - <Option text="Green" value="5" /> - </FilterableSelect> - ); -}; - -export const FilterableSelectListActionEventComponent = ( - props: Partial<FilterableSelectProps>, -) => { - const [value, setValue] = useState(""); - return ( - <FilterableSelect - label="color" - value={value} - labelInline - onChange={(event) => setValue(event.target.value)} - {...props} - listActionButton={ - <Button iconType="add" iconPosition="after"> - Add a New Element - </Button> - } - > - <Option text="Amber" value="1" /> - <Option text="Black" value="2" /> - <Option text="Blue" value="3" /> - <Option text="Brown" value="4" /> - <Option text="Green" value="5" /> - </FilterableSelect> - ); -}; - -export const FilterableSelectWithManyOptionsAndVirtualScrolling = () => ( +export const WithVirtualScrolling = () => ( <FilterableSelect name="virtualised" id="virtualised" diff --git a/src/components/select/filterable-select/filterable-select.pw.tsx b/src/components/select/filterable-select/filterable-select.pw.tsx index ec10b56d92..f2f7845004 100644 --- a/src/components/select/filterable-select/filterable-select.pw.tsx +++ b/src/components/select/filterable-select/filterable-select.pw.tsx @@ -1,24 +1,20 @@ import { expect, test } from "@playwright/experimental-ct-react17"; import React from "react"; -import { FilterableSelectProps } from "../../../../src/components/select"; +import FilterableSelect, { FilterableSelectProps } from "."; +import Option from "../option"; import { FilterableSelectComponent, - FilterableSelectWithLazyLoadingComponent, - FilterableSelectLazyLoadTwiceComponent, - FilterableSelectWithInfiniteScrollComponent, FilterableSelectObjectAsValueComponent, FilterableSelectMultiColumnsComponent, FilterableSelectMultiColumnsNestedComponent, FilterableSelectWithActionButtonComponent, - FilterableSelectOnChangeEventComponent, - FilterableSelectListActionEventComponent, - FilterableSelectWithManyOptionsAndVirtualScrolling, + WithVirtualScrolling, FilterableSelectNestedInDialog, SelectionConfirmed, FilterableSelectWithDisabledOption, FilterableSelectControlled, WithObjectAsValue, -} from "../../../../src/components/select/filterable-select/components.test-pw"; +} from "./components.test-pw"; import { commonDataElementInputPreview, getDataElementByValue, @@ -45,7 +41,6 @@ import { selectListWrapper, selectOption, selectOptionByText, - selectResetButton, } from "../../../../playwright/components/select"; import { assertCssValueIsApproximately, @@ -533,154 +528,101 @@ test.describe("FilterableSelect component", () => { }); }); - [ - ["A", "Amber", "Black", "Orange"], - ["O", "Brown", "Orange", "Yellow"], - ].forEach(([text, optionValue1, optionValue2, optionValue3]) => { + ( + [ + ["A", ["Amber", "Black", "Orange"]], + ["O", ["Brown", "Orange", "Yellow"]], + [" O", ["Brown", "Orange", "Yellow"]], + ["O ", ["Brown", "Orange", "Yellow"]], + [" O ", ["Brown", "Orange", "Yellow"]], + ] as const + ).forEach(([text, filteredOptionText]) => { test(`should filter options when ${text} is typed`, async ({ mount, page, }) => { await mount(<FilterableSelectComponent />); - await commonDataElementInputPreview(page).type(text); - await expect(selectInput(page)).toHaveAttribute("aria-expanded", "true"); - await expect(selectListWrapper(page)).toBeVisible(); - const option1 = selectOption(page, positionOfElement("first")); - const option2 = selectOption(page, positionOfElement("second")); - const option3 = selectOption(page, positionOfElement("third")); - await expect(option1).toHaveText(optionValue1); - await expect(option1).toBeVisible(); - await expect(option1).toHaveCSS("background-color", "rgb(153, 173, 183)"); - await expect(option2).toHaveText(optionValue2); - await expect(option2).toBeVisible(); - await expect(option2).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); - await expect(option3).toHaveText(optionValue3); - await expect(option3).toBeVisible(); - await expect(option3).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); - }); - }); + const input = page.getByRole("combobox"); + const dropdownList = page.getByRole("listbox"); - [ - [" O", "Brown", "Orange", "Yellow"], - ["O ", "Brown", "Orange", "Yellow"], - [" O ", "Brown", "Orange", "Yellow"], - ].forEach(([text, optionValue1, optionValue2, optionValue3]) => { - test(`should filter options when "${text}" is typed`, async ({ - mount, - page, - }) => { - await mount(<FilterableSelectComponent />); - - await commonDataElementInputPreview(page).type(text); - await expect(selectInput(page)).toHaveAttribute("aria-expanded", "true"); - await expect(selectListWrapper(page)).toBeVisible(); + await input.fill(text); + await dropdownList.waitFor(); - const option1 = selectOption(page, positionOfElement("first")); - const option2 = selectOption(page, positionOfElement("second")); - const option3 = selectOption(page, positionOfElement("third")); - await expect(option1).toHaveText(optionValue1); - await expect(option1).toBeVisible(); - await expect(option1).toHaveCSS("background-color", "rgb(153, 173, 183)"); - await expect(option2).toHaveText(optionValue2); - await expect(option2).toBeVisible(); - await expect(option2).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); - await expect(option3).toHaveText(optionValue3); - await expect(option3).toBeVisible(); - await expect(option3).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); + await expect(input).toHaveAttribute("aria-expanded", "true"); + await expect(dropdownList).toBeVisible(); + await expect(dropdownList.getByRole("option")).toHaveText( + filteredOptionText, + ); }); }); - test("should render the lazy loader when the prop is set", async ({ + test("renders loader when loading prop is set to true", async ({ mount, page, }) => { - await mount(<FilterableSelectWithLazyLoadingComponent />); + await mount(<FilterableSelectComponent isLoading />); - await dropdownButton(page).click(); - await expect(selectListWrapper(page)).toBeVisible(); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).toBeVisible()), - ); - }); + const input = page.getByRole("combobox"); + const dropdownList = page.getByRole("listbox"); - test("should render the lazy loader when the prop is set and list is opened again", async ({ - mount, - page, - }) => { - await mount(<FilterableSelectLazyLoadTwiceComponent />); + await input.click(); + await dropdownList.waitFor(); - const option = "Amber"; - const buttonElement = dropdownButton(page); - const wrapperElement = selectListWrapper(page); - await buttonElement.click(); - await expect(wrapperElement).toBeVisible(); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).toBeVisible()), - ); - await expect(selectOptionByText(page, option)).toBeVisible(); - await buttonElement.click(); - await selectResetButton(page).click(); - await buttonElement.click(); - await expect(wrapperElement).toBeVisible(); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).toBeVisible()), - ); + await expect(loader(page, 1)).toBeVisible(); }); - test("should render a lazy loaded option when the infinite scroll prop is set", async ({ + test("scroll position of option list doesn't change, if the component's options are dynamically changed", async ({ mount, page, }) => { - await mount(<FilterableSelectWithInfiniteScrollComponent />); - - const option = "Lazy Loaded A1"; - const selectListWrapperElement = selectListWrapper(page); - await dropdownButton(page).click(); - await expect(selectListWrapperElement).toBeVisible(); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).toBeVisible()), - ); - await expect(selectOptionByText(page, option)).toHaveCount(0); - await page.waitForTimeout(2000); - await selectListScrollableWrapper(page).evaluate((wrapper) => { - wrapper.scrollBy(0, 500); - }); - await page.waitForTimeout(250); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).not.toBeVisible()), + const { update } = await mount( + <FilterableSelect label="Colour"> + <Option text="Amber" value="Amber" /> + <Option text="Black" value="Black" /> + <Option text="Cyan" value="Cyan" /> + <Option text="Dark Blue" value="Dark Blue" /> + <Option text="Emerald" value="Emerald" /> + <Option text="Fuchsia" value="Fuchsia" /> + <Option text="Gold" value="Gold" /> + </FilterableSelect>, ); - await expect(selectOptionByText(page, option)).toBeVisible(); - }); - test("the list should not change scroll position when the lazy-loaded options appear", async ({ - mount, - page, - }) => { - await mount(<FilterableSelectWithInfiniteScrollComponent />); + const input = page.getByRole("combobox"); + await input.focus(); + await input.press("ArrowDown"); - // open the select list and choose an option - const inputElement = commonDataElementInputPreview(page); - await inputElement.focus(); - await inputElement.press("ArrowDown"); - const firstOption = selectOptionByText(page, "Amber"); - await firstOption.waitFor(); - await firstOption.click(); + const dropdownList = page.getByRole("listbox"); + await dropdownList.waitFor(); - // reopen the list and scroll to initiate the lazy loading. It's important to not use the keyboard here as that - // won't trigger the bug. - const scrollableWrapper = selectListScrollableWrapper(page); - await dropdownButton(page).click(); - await scrollableWrapper.evaluate((wrapper) => wrapper.scrollBy(0, 500)); - const scrollPositionBeforeLoad = await scrollableWrapper.evaluate( + await page.keyboard.press("ArrowUp"); + await expect(page.getByRole("option").last()).toBeInViewport(); + + const scrollPosition = await dropdownList.evaluate( (element) => element.scrollTop, ); - await selectOptionByText(page, "Lazy Loaded A1").waitFor(); - const scrollPositionAfterLoad = await scrollableWrapper.evaluate( + await update( + <FilterableSelect label="Colour"> + <Option text="Amber" value="Amber" /> + <Option text="Black" value="Black" /> + <Option text="Cyan" value="Cyan" /> + <Option text="Dark Blue" value="Dark Blue" /> + <Option text="Emerald" value="Emerald" /> + <Option text="Fuchsia" value="Fuchsia" /> + <Option text="Gold" value="Gold" /> + <Option text="Hot Pink" value="Hot Pink" /> + <Option text="Indigo" value="Indigo" /> + </FilterableSelect>, + ); + + await expect(page.getByRole("option")).toHaveCount(9); + + // check that the scroll position hasn't changed + const newScrollPosition = await dropdownList.evaluate( (element) => element.scrollTop, ); - expect(scrollPositionAfterLoad).toBe(scrollPositionBeforeLoad); + expect(newScrollPosition).toBeCloseTo(scrollPosition, 1); }); test("should list options when value is set and select list is opened again", async ({ @@ -825,14 +767,15 @@ test.describe("FilterableSelect component", () => { }) => { await mount(<FilterableSelectObjectAsValueComponent />); - const position = "first"; - const positionValue = "Amber"; - const inputElement = getDataElementByValue(page, "input"); - await expect(inputElement).toHaveValue("Green"); - await expect(selectInput(page)).toHaveAttribute("aria-expanded", "false"); - await dropdownButton(page).click(); - await selectOption(page, positionOfElement(position)).click(); - await expect(inputElement).toHaveValue(positionValue); + const input = page.getByRole("combobox"); + await expect(input).toHaveValue("Green"); + await expect(input).toHaveAttribute("aria-expanded", "false"); + + await input.click(); + await page.getByRole("listbox").waitFor(); + + await page.getByRole("option", { name: "Amber" }).click(); + await expect(input).toHaveValue("Amber"); }); test("should render option list with proper maxHeight value", async ({ @@ -973,7 +916,7 @@ test.describe("FilterableSelect component", () => { const inputElement = commonDataElementInputPreview(page); await inputElement.click(); await expect(inputElement).toBeFocused(); - await inputElement.type(text); + await inputElement.fill(text); const highlightedValue = boldedAndUnderlinedValue(page, text); await expect(highlightedValue).toHaveCSS( "text-decoration-line", @@ -997,7 +940,7 @@ test.describe("FilterableSelect component", () => { const inputElement = commonDataElementInputPreview(page); await inputElement.click(); await expect(inputElement).toBeFocused(); - await inputElement.type(text); + await inputElement.fill(text); await expect(selectListWrapper(page)).toBeVisible(); const headerElements = multiColumnsSelectListHeader(page); await expect(headerElements).toHaveCount(columns); @@ -1237,211 +1180,79 @@ test.describe("FilterableSelect component", () => { }); }); -test.describe("Check events for FilterableSelect component", () => { - test("should call onClick event when mouse is clicked on dropdown icon", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<FilterableSelectComponent onClick={callback} />); - - await dropdownButton(page).click(); - expect(callbackCount).toBe(1); - }); - - test("should call onFocus when input is focused", async ({ mount, page }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<FilterableSelectComponent onFocus={callback} />); - - await commonDataElementInputPreview(page).focus(); - expect(callbackCount).toBe(1); - }); - - // TODO: Skipped due to flaky focus behaviour. To review in FE-6428 - test.skip("should call onOpen when select is opened by focusing the input", async ({ +test.describe("onListScrollBottom prop", () => { + test("calls onListScrollBottom when the list is scrolled to the bottom", async ({ mount, page, }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<FilterableSelectComponent openOnFocus onOpen={callback} />); - - await commonDataElementInputPreview(page).focus(); - // this waitFor call seems to be needed for the test to consistently pass - await commonDataElementInputPreview(page).waitFor(); - expect(callbackCount).toBe(1); - }); + let called = false; - test("should call onOpen when FilterableSelect is opened by clicking on Icon", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<FilterableSelectComponent onOpen={callback} />); + await mount( + <FilterableSelectComponent + onListScrollBottom={() => { + called = true; + }} + />, + ); - await dropdownButton(page).click(); - expect(callbackCount).toBe(1); - }); + await page.getByRole("combobox").click(); + await page.getByRole("listbox").waitFor(); - test("should call onBlur event when the list is closed", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<FilterableSelectComponent onBlur={callback} />); + const lastOption = page.getByRole("option").last(); + await lastOption.scrollIntoViewIfNeeded(); - await dropdownButton(page).click(); - await commonDataElementInputPreview(page).blur(); - expect(callbackCount).toBe(1); + await expect(async () => { + expect(called).toBeTruthy(); + }).toPass(); }); - test("should call onChange event when a list option is selected", async ({ + test("does not call onListScrollBottom when an option is clicked", async ({ mount, page, }) => { - type CallbackArgument = Parameters< - Required<FilterableSelectProps>["onChange"] - >[0]; - const callbackArguments: CallbackArgument[] = []; - const callback = (e: CallbackArgument) => { - callbackArguments.push(e); - }; - await mount(<FilterableSelectComponent onChange={callback} />); - - const position = "first"; - const option = "1"; - await dropdownButton(page).click(); - await selectOption(page, positionOfElement(position)).click(); - expect(callbackArguments.length).toBe(1); - expect(callbackArguments[0]).toMatchObject({ - target: { value: option }, - selectionConfirmed: true, - }); - }); + let called = false; - keyToTrigger.slice(0, 2).forEach((key) => { - test(`should call onKeyDown event when ${key} key is pressed`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<FilterableSelectComponent onKeyDown={callback} />); - - const inputElement = commonDataElementInputPreview(page); - await inputElement.focus(); - await inputElement.press(key); - expect(callbackCount).toBe(1); - }); - }); - - test("should call onFilterChange event when a filter string is input", async ({ - mount, - page, - }) => { - type CallbackArgument = Parameters< - Required<FilterableSelectProps>["onFilterChange"] - >[0]; - const callbackArguments: CallbackArgument[] = []; - const callback = (e: CallbackArgument) => { - callbackArguments.push(e); - }; await mount( - <FilterableSelectOnChangeEventComponent onFilterChange={callback} />, + <FilterableSelectComponent + onListScrollBottom={() => { + called = true; + }} + />, ); - const text = "B"; - const inputElement = commonDataElementInputPreview(page); - await inputElement.focus(); - await inputElement.type(text); - expect(callbackArguments.length).toBe(1); - expect(callbackArguments[0]).toBe(text); - }); + await page.getByRole("combobox").click(); + await page.getByRole("listbox").waitFor(); - test("should call onListAction event when the Action Button is clicked", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount( - <FilterableSelectListActionEventComponent onListAction={callback} />, - ); + const firstOption = page.getByRole("option").first(); + await firstOption.click(); - await dropdownButton(page).click(); - await filterableSelectAddElementButton(page).click(); - expect(callbackCount).toBe(1); + expect(called).toBeFalsy(); }); - test("should call onListScrollBottom event when the list is scrolled to the bottom", async ({ + test("does not call onListScrollBottom when an option is clicked and list is re-opened", async ({ mount, page, }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<FilterableSelectComponent onListScrollBottom={callback} />); + let called = false; - await dropdownButton(page).click(); - await selectListScrollableWrapper(page).evaluate((wrapper) => - wrapper.scrollBy(0, 500), + await mount( + <FilterableSelectComponent + onListScrollBottom={() => { + called = true; + }} + />, ); - await page.waitForTimeout(250); - expect(callbackCount).toBe(1); - }); - test("should not call onListScrollBottom callback when an option is clicked", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<FilterableSelectComponent onListScrollBottom={callback} />); - - await dropdownButton(page).click(); - await selectOption(page, positionOfElement("first")).click(); - expect(callbackCount).toBe(0); - }); + const input = page.getByRole("combobox"); + await input.click(); + await page.getByRole("listbox").waitFor(); - test("should not be called when an option is clicked and list is re-opened", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; + const firstOption = page.getByRole("option").first(); + await firstOption.click(); - await mount(<FilterableSelectComponent onListScrollBottom={callback} />); + await input.click(); - await dropdownButton(page).click(); - await selectListScrollableWrapper(page).evaluate((wrapper) => - wrapper.scrollBy(0, 500), - ); - await selectOption(page, positionOfElement("first")).click(); - await dropdownButton(page).click(); - expect(callbackCount).toBe(1); + expect(called).toBeFalsy(); }); }); @@ -1450,7 +1261,7 @@ test.describe("Check virtual scrolling", () => { mount, page, }) => { - await mount(<FilterableSelectWithManyOptionsAndVirtualScrolling />); + await mount(<WithVirtualScrolling />); await dropdownButton(page).click(); await expect(selectOptionByText(page, "Option 1.")).toBeInViewport(); @@ -1464,28 +1275,33 @@ test.describe("Check virtual scrolling", () => { mount, page, }) => { - await mount(<FilterableSelectWithManyOptionsAndVirtualScrolling />); + await mount(<WithVirtualScrolling />); - await dropdownButton(page).click(); - await selectListScrollableWrapper(page).evaluate((wrapper) => - wrapper.scrollTo(0, 750), - ); - await page.waitForTimeout(250); - await expect(selectOptionByText(page, "Option 1.")).toHaveCount(0); - await expect(selectOptionByText(page, "Option 20.")).toBeInViewport(); - const option30 = selectOptionByText(page, "Option 30."); - await expect(option30).toHaveCount(1); - await expect(option30).not.toBeInViewport(); - await expect(selectOptionByText(page, "Option 40.")).toHaveCount(0); + const input = page.getByRole("combobox"); + await input.click(); + + const list = page.getByRole("listbox"); + await list.waitFor(); + + const firstOption = page.getByRole("option").first(); + const lastOption = page.getByRole("option").last(); + + await expect(firstOption).toHaveText("Option 1."); + await expect(lastOption).toHaveText("Option 15."); + + await lastOption.scrollIntoViewIfNeeded(); + + await expect(firstOption).not.toHaveText("Option 1."); + await expect(lastOption).not.toHaveText("Option 15."); }); test("should filter options when text is typed, taking into account non-rendered options", async ({ mount, page, }) => { - await mount(<FilterableSelectWithManyOptionsAndVirtualScrolling />); + await mount(<WithVirtualScrolling />); - await commonDataElementInputPreview(page).type("Option 100"); + await commonDataElementInputPreview(page).fill("Option 100"); await expect(selectOptionByText(page, "Option 100.")).toBeInViewport(); await expect(selectOptionByText(page, "Option 1000.")).toBeInViewport(); await expect(selectOptionByText(page, "Option 1002.")).toBeInViewport(); @@ -1498,8 +1314,10 @@ test.describe("Check virtual scrolling", () => { }) => { await mount(<FilterableSelectComponent />); - await commonDataElementInputPreview(page).type("foo"); - await commonDataElementInputPreview(page).press(key); + const input = page.getByRole("combobox"); + await input.fill("foo"); + await input.press(key); + await expect(page.getByText('No results for "foo"')).toBeVisible(); }); }); @@ -1675,7 +1493,7 @@ test.describe("Selection confirmed", () => { await dropdownButton(page).click(); const inputElement = selectInput(page); - await inputElement.type("th"); + await inputElement.fill("th"); await expect( page.locator('[data-element="confirmed-selection-3"]'), ).not.toBeVisible(); @@ -1692,7 +1510,7 @@ test.describe("Selection confirmed", () => { await mount(<SelectionConfirmed />); const inputElement = selectInput(page); - await inputElement.type("foo"); + await inputElement.fill("foo"); await inputElement.press("Enter"); // note: need to check count rather than visibility here - when the test fails and selectionConfirmed is set, // the span with the data-element prop exists but has size 0 due to having no text content - which Playwright @@ -1711,7 +1529,7 @@ test("should not throw when filter text does not match option text", async ({ <FilterableSelectComponent value={undefined} onChange={undefined} />, ); - await commonDataElementInputPreview(page).type("abc"); + await commonDataElementInputPreview(page).fill("abc"); await selectInput(page).press("Enter"); await expect(getDataElementByValue(page, "input")).toHaveValue(""); }); @@ -1724,7 +1542,7 @@ test("should not select a disabled option when a filter is typed", async ({ await dropdownButton(page).click(); const inputElement = selectInput(page); - await inputElement.type("t"); + await inputElement.fill("t"); await inputElement.press("Enter"); await expect( page.locator('[data-element="confirmed-selection-2"]'), @@ -1967,26 +1785,10 @@ test.describe("Accessibility tests for FilterableSelect component", () => { mount, page, }) => { - await mount(<FilterableSelectWithLazyLoadingComponent />); + await mount(<FilterableSelectComponent isLoading />); await dropdownButton(page).click(); await expect(loader(page, 1)).toBeVisible(); - await checkAccessibility(page); - }); - - test("should pass accessibility tests with onListScrollBottom prop", async ({ - mount, - page, - }) => { - await mount(<FilterableSelectWithInfiniteScrollComponent />); - - await dropdownButton(page).click(); - await checkAccessibility(page); - // wait for content to finish loading before scrolling - await expect(selectOptionByText(page, "Amber")).toBeVisible(); - await selectListScrollableWrapper(page).evaluate((wrapper) => - wrapper.scrollBy(0, 500), - ); await checkAccessibility(page, undefined, "scrollable-region-focusable"); }); @@ -2086,7 +1888,7 @@ test.describe("Accessibility tests for FilterableSelect component", () => { mount, page, }) => { - await mount(<FilterableSelectWithManyOptionsAndVirtualScrolling />); + await mount(<WithVirtualScrolling />); await dropdownButton(page).click(); await checkAccessibility(page, undefined, "scrollable-region-focusable"); diff --git a/src/components/select/filterable-select/filterable-select.test.tsx b/src/components/select/filterable-select/filterable-select.test.tsx index 6ec945b38a..815c69b5a9 100644 --- a/src/components/select/filterable-select/filterable-select.test.tsx +++ b/src/components/select/filterable-select/filterable-select.test.tsx @@ -891,6 +891,25 @@ describe("when the user clicks on the input icon", () => { expect(onClickFn).toHaveBeenCalled(); }); + + it("should call `onOpen` callback if prop is passed", async () => { + const onOpenFn = jest.fn(); + const user = userEvent.setup(); + render( + <FilterableSelect + label="filterable-select" + onChange={() => {}} + value="" + onOpen={onOpenFn} + > + <Option value="opt1" text="red" /> + </FilterableSelect>, + ); + const icon = within(screen.getByRole("presentation")).getByTestId("icon"); + await user.click(icon); + + expect(onOpenFn).toHaveBeenCalled(); + }); }); describe("the placement of the list element", () => { diff --git a/src/components/select/multi-select/components.test-pw.tsx b/src/components/select/multi-select/components.test-pw.tsx index 04a3cb0f55..c31cfca575 100644 --- a/src/components/select/multi-select/components.test-pw.tsx +++ b/src/components/select/multi-select/components.test-pw.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from "react"; +import React, { useState } from "react"; import { MultiSelect, Option, @@ -6,7 +6,6 @@ import { CustomSelectChangeEvent, } from ".."; import OptionRow from "../option-row/option-row.component"; -import Button from "../../button/button.component"; import Dialog from "../../dialog"; import Box from "../../box"; import CarbonProvider from "../../carbon-provider/carbon-provider.component"; @@ -101,178 +100,6 @@ export const MultiSelectLongPillComponent = ( ); }; -export const MultiSelectWithLazyLoadingComponent = ( - props: Partial<MultiSelectProps>, -) => { - const preventLoading = useRef(false); - const [value, setValue] = useState<string[]>([]); - const [isLoading, setIsLoading] = useState(true); - const asyncList = [ - <Option text="Amber" value="amber" key="Amber" />, - <Option text="Black" value="black" key="Black" />, - <Option text="Blue" value="blue" key="Blue" />, - <Option text="Brown" value="brown" key="Brown" />, - <Option text="Green" value="green" key="Green" />, - ]; - const [optionList, setOptionList] = useState<React.ReactElement[]>([]); - function onChangeHandler(event: React.ChangeEvent<HTMLInputElement>) { - setValue(event.target.value as unknown as string[]); - } - function loadList() { - if (preventLoading.current) { - return; - } - preventLoading.current = true; - setIsLoading(true); - setTimeout(() => { - setIsLoading(false); - setOptionList(asyncList); - }, 2000); - } - return ( - <MultiSelect - label="color" - value={value} - onChange={onChangeHandler} - onOpen={() => loadList()} - isLoading={isLoading} - {...props} - > - {optionList} - </MultiSelect> - ); -}; - -export const MultiSelectLazyLoadTwiceComponent = ( - props: Partial<MultiSelectProps>, -) => { - const preventLoading = useRef(false); - const [value, setValue] = useState<string[]>([]); - const [isLoading, setIsLoading] = useState(true); - const asyncList = [ - <Option text="Amber" value="amber" key="Amber" />, - <Option text="Black" value="black" key="Black" />, - <Option text="Blue" value="blue" key="Blue" />, - <Option text="Brown" value="brown" key="Brown" />, - <Option text="Green" value="green" key="Green" />, - ]; - const [optionList, setOptionList] = useState<React.ReactElement[]>([]); - function loadList() { - if (preventLoading.current) { - return; - } - preventLoading.current = true; - setIsLoading(true); - setTimeout(() => { - setIsLoading(false); - setOptionList(asyncList); - }, 2000); - } - function clearData() { - setOptionList([]); - setValue([]); - preventLoading.current = false; - } - return ( - <div> - <Button onClick={clearData} mb={2} data-element="reset-button"> - reset - </Button> - <MultiSelect - label="color" - value={value} - onChange={(event) => - setValue(event.target.value as unknown as string[]) - } - onOpen={() => loadList()} - isLoading={isLoading} - {...props} - > - {optionList} - </MultiSelect> - </div> - ); -}; - -export const MultiSelectWithInfiniteScrollComponent = ( - props: Partial<MultiSelectProps>, -) => { - const preventLoading = useRef(false); - const preventLazyLoading = useRef(false); - const lazyLoadingCounter = useRef(0); - const [value, setValue] = useState<string[]>([]); - const [isLoading, setIsLoading] = useState(true); - const asyncList = [ - <Option text="Amber" value="amber" key="Amber" />, - <Option text="Black" value="black" key="Black" />, - <Option text="Blue" value="blue" key="Blue" />, - <Option text="Brown" value="brown" key="Brown" />, - <Option text="Green" value="green" key="Green" />, - ]; - const getLazyLoaded = () => { - const counter = lazyLoadingCounter.current; - return [ - <Option - text={`Lazy Loaded A${counter}`} - value={`lazyA${counter}`} - key={`lazyA${counter}`} - />, - <Option - text={`Lazy Loaded B${counter}`} - value={`lazyB${counter}`} - key={`lazyB${counter}`} - />, - <Option - text={`Lazy Loaded C${counter}`} - value={`lazyC${counter}`} - key={`lazyC${counter}`} - />, - ]; - }; - const [optionList, setOptionList] = useState<React.ReactElement[]>([]); - function onChangeHandler(event: React.ChangeEvent<HTMLInputElement>) { - setValue(event.target.value as unknown as string[]); - } - - function loadList() { - if (preventLoading.current) { - return; - } - preventLoading.current = true; - setIsLoading(true); - setTimeout(() => { - setIsLoading(false); - setOptionList(asyncList); - }, 2000); - } - function onLazyLoading() { - if (preventLazyLoading.current) { - return; - } - preventLazyLoading.current = true; - setIsLoading(true); - setTimeout(() => { - preventLazyLoading.current = false; - lazyLoadingCounter.current += 1; - setIsLoading(false); - setOptionList((prevList) => [...prevList, ...getLazyLoaded()]); - }, 2000); - } - return ( - <MultiSelect - label="color" - value={value} - onChange={onChangeHandler} - onOpen={() => loadList()} - isLoading={isLoading} - onListScrollBottom={onLazyLoading} - {...props} - > - {optionList} - </MultiSelect> - ); -}; - export const MultiSelectObjectAsValueComponent = ( props: Partial<MultiSelectProps>, ) => { @@ -396,34 +223,6 @@ export const MultiSelectMaxOptionsComponent = ( ); }; -export const MultiSelectOnFilterChangeEventComponent = ({ - onChange, - ...props -}: Partial<MultiSelectProps>) => { - const [state, setState] = useState<string[]>([]); - const setValue = (event: React.ChangeEvent<HTMLInputElement>) => { - setState(event.target.value as unknown as string[]); - if (onChange) { - onChange(event); - } - }; - return ( - <MultiSelect - label="color" - value={state} - labelInline - onChange={setValue} - {...props} - > - <Option text="Amber" value="1" /> - <Option text="Black" value="2" /> - <Option text="Blue" value="3" /> - <Option text="Brown" value="4" /> - <Option text="Green" value="5" /> - </MultiSelect> - ); -}; - export const MultiSelectCustomColorComponent = ( props: Partial<MultiSelectProps>, ) => { @@ -444,7 +243,7 @@ export const MultiSelectCustomColorComponent = ( ); }; -export const MultiSelectWithManyOptionsAndVirtualScrolling = () => ( +export const WithVirtualScrolling = () => ( <MultiSelect name="virtualised" id="virtualised" diff --git a/src/components/select/multi-select/multi-select.pw.tsx b/src/components/select/multi-select/multi-select.pw.tsx index fe8a6cfe40..c548c3b7c5 100644 --- a/src/components/select/multi-select/multi-select.pw.tsx +++ b/src/components/select/multi-select/multi-select.pw.tsx @@ -1,25 +1,22 @@ import { expect, test } from "@playwright/experimental-ct-react17"; import React from "react"; -import { MultiSelectProps } from "../../../../src/components/select"; +import MultiSelect, { MultiSelectProps } from "."; +import Option from "../option"; import { MultiSelectComponent, MultiSelectDefaultValueComponent, MultiSelectMaxOptionsComponent, - MultiSelectWithLazyLoadingComponent, - MultiSelectWithInfiniteScrollComponent, - MultiSelectLazyLoadTwiceComponent, MultiSelectObjectAsValueComponent, MultiSelectMultiColumnsComponent, MultiSelectCustomColorComponent, MultiSelectLongPillComponent, - MultiSelectOnFilterChangeEventComponent, - MultiSelectWithManyOptionsAndVirtualScrolling, + WithVirtualScrolling, MultiSelectNestedInDialog, MultiSelectErrorOnChangeNewValidation, SelectionConfirmed, MultiSelectWithDisabledOption, OptionsWithSameName, -} from "../../../../src/components/select/multi-select/components.test-pw"; +} from "./components.test-pw"; import { commonDataElementInputPreview, getDataElementByValue, @@ -48,7 +45,6 @@ import { selectListWrapper, selectOption, selectOptionByText, - selectResetButton, } from "../../../../playwright/components/select"; import { assertCssValueIsApproximately, @@ -582,7 +578,7 @@ test.describe("MultiSelect component", () => { }) => { await mount(<MultiSelectComponent />); - await commonDataElementInputPreview(page).type(text); + await commonDataElementInputPreview(page).fill(text); await expect(selectInput(page)).toHaveAttribute("aria-expanded", "true"); await expect(selectListWrapper(page)).toBeVisible(); const optionOne = selectOption(page, positionOfElement("first")); @@ -613,98 +609,78 @@ test.describe("MultiSelect component", () => { }) => { await mount(<MultiSelectComponent />); - await commonDataElementInputPreview(page).type("foo"); + await commonDataElementInputPreview(page).fill("foo"); await commonDataElementInputPreview(page).press(key); await expect(page.getByText('No results for "foo"')).toBeVisible(); }); }); - test("should render the lazy loader when the prop is set", async ({ + test("renders loader when loading prop is set to true", async ({ mount, page, }) => { - await mount(<MultiSelectWithLazyLoadingComponent />); + await mount(<MultiSelectComponent isLoading />); - await dropdownButton(page).click(); - await expect(selectListWrapper(page)).toBeVisible(); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).toBeVisible()), - ); - }); + const input = page.getByRole("combobox"); + const dropdownList = page.getByRole("listbox"); - test("should render the lazy loader when the prop is set and list is opened again", async ({ - mount, - page, - }) => { - await mount(<MultiSelectLazyLoadTwiceComponent />); + await input.click(); + await dropdownList.waitFor(); - const buttonElement = dropdownButton(page); - const wrapperElement = selectListWrapper(page); - await buttonElement.click(); - await expect(wrapperElement).toBeVisible(); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).toBeVisible()), - ); - await expect(selectOptionByText(page, listOption)).toBeVisible(); - await buttonElement.click(); - await selectResetButton(page).click(); - await buttonElement.click(); - await expect(wrapperElement).toBeVisible(); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).toBeVisible()), - ); + await expect(loader(page, 1)).toBeVisible(); }); - test("should render a lazy loaded option when the infinite scroll prop is set", async ({ + test("scroll position of option list doesn't change, if the component's options are dynamically changed", async ({ mount, page, }) => { - await mount(<MultiSelectWithInfiniteScrollComponent />); - - const option = "Lazy Loaded A1"; - const selectListWrapperElement = selectListWrapper(page); - await dropdownButton(page).click(); - await expect(selectListWrapperElement).toBeVisible(); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).toBeVisible()), + const { update } = await mount( + <MultiSelect label="Colour"> + <Option text="Amber" value="Amber" /> + <Option text="Black" value="Black" /> + <Option text="Cyan" value="Cyan" /> + <Option text="Dark Blue" value="Dark Blue" /> + <Option text="Emerald" value="Emerald" /> + <Option text="Fuchsia" value="Fuchsia" /> + <Option text="Gold" value="Gold" /> + </MultiSelect>, ); - await expect(selectOptionByText(page, option)).toHaveCount(0); - await page.waitForTimeout(2000); - await selectListScrollableWrapper(page).evaluate((wrapper) => { - wrapper.scrollBy(0, 500); - }); - await page.waitForTimeout(250); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).not.toBeVisible()), - ); - await expect(selectOptionByText(page, option)).toBeVisible(); - }); - test("the list should not change scroll position when the lazy-loaded options appear", async ({ - mount, - page, - }) => { - await mount(<MultiSelectWithInfiniteScrollComponent />); + const input = page.getByRole("combobox"); + await input.focus(); + await input.press("ArrowDown"); - // open the select list and choose an option - const inputElement = commonDataElementInputPreview(page); - await inputElement.focus(); - await inputElement.press("ArrowDown"); - const firstOption = selectOptionByText(page, "Amber"); - await firstOption.waitFor(); - await firstOption.click(); + const dropdownList = page.getByRole("listbox"); + await dropdownList.waitFor(); - const scrollableWrapper = selectListScrollableWrapper(page); - await scrollableWrapper.evaluate((wrapper) => wrapper.scrollBy(0, 500)); - const scrollPositionBeforeLoad = await scrollableWrapper.evaluate( + await page.keyboard.press("ArrowUp"); + await expect(page.getByRole("option").last()).toBeInViewport(); + + const scrollPosition = await dropdownList.evaluate( (element) => element.scrollTop, ); - await selectOptionByText(page, "Lazy Loaded A1").waitFor(); - const scrollPositionAfterLoad = await scrollableWrapper.evaluate( + await update( + <MultiSelect label="Colour"> + <Option text="Amber" value="Amber" /> + <Option text="Black" value="Black" /> + <Option text="Cyan" value="Cyan" /> + <Option text="Dark Blue" value="Dark Blue" /> + <Option text="Emerald" value="Emerald" /> + <Option text="Fuchsia" value="Fuchsia" /> + <Option text="Gold" value="Gold" /> + <Option text="Hot Pink" value="Hot Pink" /> + <Option text="Indigo" value="Indigo" /> + </MultiSelect>, + ); + + await expect(page.getByRole("option")).toHaveCount(9); + + // check that the scroll position hasn't changed + const newScrollPosition = await dropdownList.evaluate( (element) => element.scrollTop, ); - expect(scrollPositionAfterLoad).toBe(scrollPositionBeforeLoad); + expect(newScrollPosition).toBeCloseTo(scrollPosition, 1); }); test("should list options when value is set and select list is opened again", async ({ @@ -887,7 +863,7 @@ test.describe("MultiSelect component", () => { const inputElement = commonDataElementInputPreview(page); await inputElement.click(); await expect(inputElement).toBeFocused(); - await inputElement.type(text); + await inputElement.fill(text); const highlightedValue = boldedAndUnderlinedValue(page, text); await expect(highlightedValue).toHaveCSS( "text-decoration-line", @@ -911,7 +887,7 @@ test.describe("MultiSelect component", () => { const inputElement = commonDataElementInputPreview(page); await inputElement.click(); await expect(inputElement).toBeFocused(); - await inputElement.type(text); + await inputElement.fill(text); await expect(selectListWrapper(page)).toBeVisible(); const headerElements = multiColumnsSelectListHeader(page); await expect(headerElements).toHaveCount(columns); @@ -1177,173 +1153,85 @@ test.describe("MultiSelect component", () => { await expect(multiSelectPillByText(page, "Blue")).toHaveCount(0); await expect(multiSelectPillByText(page, "Black")).toHaveCount(2); }); -}); -test.describe("Check events for MultiSelect component", () => { - test("should call onClick event when mouse is clicked on text input", async ({ + test("calls onListScrollBottom when the dropdown list is scrolled to the bottom", async ({ mount, page, }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<MultiSelectComponent onClick={callback} />); + let called = false; + await mount( + <MultiSelectComponent + onListScrollBottom={() => { + called = true; + }} + />, + ); - await commonDataElementInputPreview(page).click(); - expect(callbackCount).toBe(1); - }); + const input = page.getByRole("combobox"); + await input.click(); - test("should call onFocus when MultiSelect is brought into focus", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<MultiSelectComponent onFocus={callback} />); + const dropdownList = page.getByRole("listbox"); + await dropdownList.waitFor(); - await commonDataElementInputPreview(page).focus(); - expect(callbackCount).toBe(1); - }); + const lastOption = page.getByRole("option").last(); + await lastOption.scrollIntoViewIfNeeded(); - test("should call onOpen when MultiSelect is opened", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<MultiSelectComponent onOpen={callback} />); - - await dropdownButton(page).click(); - expect(callbackCount).toBe(1); + await expect(async () => { + expect(called).toBeTruthy(); + }).toPass(); }); - test("should call onChange event once when a list option is selected", async ({ + test("does not call onListScrollBottom when an option is clicked", async ({ mount, page, }) => { - type CallbackArgument = Parameters< - Required<MultiSelectProps>["onChange"] - >[0]; - const callbackArguments: CallbackArgument[] = []; - const callback = (e: CallbackArgument) => { - callbackArguments.push(e); - }; - await mount(<MultiSelectComponent onChange={callback} />); + let called = false; + await mount( + <MultiSelectComponent + onListScrollBottom={() => { + called = true; + }} + />, + ); - const position = "first"; - const option = ["1"]; - await dropdownButton(page).click(); - await selectOption(page, positionOfElement(position)).click(); - expect(callbackArguments.length).toBe(1); - expect(callbackArguments[0]).toMatchObject({ - target: { value: option }, - selectionConfirmed: true, - }); - }); + const input = page.getByRole("combobox"); + await input.click(); - [ - keyToTrigger[0], - keyToTrigger[1], - keyToTrigger[2], - keyToTrigger[3], - keyToTrigger[6], - ].forEach((key) => { - test(`should call onKeyDown event when ${key} key is pressed`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<MultiSelectComponent onKeyDown={callback} />); + const dropdownList = page.getByRole("listbox"); + await dropdownList.waitFor(); - const inputElement = commonDataElementInputPreview(page); - await inputElement.focus(); - await inputElement.press(key); - expect(callbackCount).toBe(1); - }); - }); - - test("should call onFilterChange event when a filter string is input", async ({ - mount, - page, - }) => { - type CallbackArgument = Parameters< - Required<MultiSelectProps>["onFilterChange"] - >[0]; - const callbackArguments: CallbackArgument[] = []; - const callback = (e: CallbackArgument) => { - callbackArguments.push(e); - }; - await mount( - <MultiSelectOnFilterChangeEventComponent onFilterChange={callback} />, - ); + const firstOption = page.getByRole("option").first(); + await firstOption.click(); - const text = "B"; - const inputElement = commonDataElementInputPreview(page); - await inputElement.focus(); - await inputElement.type(text); - expect(callbackArguments.length).toBe(1); - expect(callbackArguments[0]).toBe(text); + expect(called).toBeFalsy(); }); - test("should call onListScrollBottom event when the list is scrolled to the bottom", async ({ + test("does not call onListScrollBottom when an option is clicked and list is re-opened", async ({ mount, page, }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<MultiSelectComponent onListScrollBottom={callback} />); - - await dropdownButton(page).click(); - await selectListScrollableWrapper(page).evaluate((wrapper) => - wrapper.scrollBy(0, 500), + let called = false; + await mount( + <MultiSelectComponent + onListScrollBottom={() => { + called = true; + }} + />, ); - await page.waitForTimeout(250); - expect(callbackCount).toBe(1); - }); - test("should not call onListScrollBottom callback when an option is clicked", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<MultiSelectComponent onListScrollBottom={callback} />); + const input = page.getByRole("combobox"); + await input.click(); - await dropdownButton(page).click(); - await selectOption(page, positionOfElement("first")).click(); - expect(callbackCount).toBe(0); - }); + const dropdownList = page.getByRole("listbox"); + await dropdownList.waitFor(); - test("should not be called when an option is clicked and list is re-opened", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; + const firstOption = page.getByRole("option").first(); + await firstOption.click(); - await mount(<MultiSelectComponent onListScrollBottom={callback} />); + await input.click(); + await dropdownList.waitFor({ state: "hidden" }); - await dropdownButton(page).click(); - await selectListScrollableWrapper(page).evaluate((wrapper) => - wrapper.scrollBy(0, 500), - ); - await selectOption(page, positionOfElement("first")).click(); - await dropdownButton(page).click(); - expect(callbackCount).toBe(1); + expect(called).toBeFalsy(); }); }); @@ -1352,7 +1240,7 @@ test.describe("Check virtual scrolling", () => { mount, page, }) => { - await mount(<MultiSelectWithManyOptionsAndVirtualScrolling />); + await mount(<WithVirtualScrolling />); await dropdownButton(page).click(); await expect(selectOptionByText(page, "Option 1.")).toBeInViewport(); @@ -1366,28 +1254,33 @@ test.describe("Check virtual scrolling", () => { mount, page, }) => { - await mount(<MultiSelectWithManyOptionsAndVirtualScrolling />); + await mount(<WithVirtualScrolling />); - await dropdownButton(page).click(); - await selectListScrollableWrapper(page).evaluate((wrapper) => - wrapper.scrollTo(0, 750), - ); - await page.waitForTimeout(250); - await expect(selectOptionByText(page, "Option 1.")).toHaveCount(0); - await expect(selectOptionByText(page, "Option 20.")).toBeInViewport(); - const option30 = selectOptionByText(page, "Option 30."); - await expect(option30).toHaveCount(1); - await expect(option30).not.toBeInViewport(); - await expect(selectOptionByText(page, "Option 40.")).toHaveCount(0); + const input = page.getByRole("combobox"); + await input.click(); + + const list = page.getByRole("listbox"); + await list.waitFor(); + + const firstOption = page.getByRole("option").first(); + const lastOption = page.getByRole("option").last(); + + await expect(firstOption).toHaveText("Option 1."); + await expect(lastOption).toHaveText("Option 15."); + + await lastOption.scrollIntoViewIfNeeded(); + + await expect(firstOption).not.toHaveText("Option 1."); + await expect(lastOption).not.toHaveText("Option 15."); }); test("should filter options when text is typed, taking into account non-rendered options", async ({ mount, page, }) => { - await mount(<MultiSelectWithManyOptionsAndVirtualScrolling />); + await mount(<WithVirtualScrolling />); - await commonDataElementInputPreview(page).type("Option 100"); + await commonDataElementInputPreview(page).fill("Option 100"); await expect(selectOptionByText(page, "Option 100.")).toBeInViewport(); await expect(selectOptionByText(page, "Option 1000.")).toBeInViewport(); await expect(selectOptionByText(page, "Option 1002.")).toBeInViewport(); @@ -1614,7 +1507,7 @@ test("should not add an empty Pill when filter text does not match option text", const pillElement = multiSelectPill(page); await expect(pillElement).not.toBeVisible(); - await commonDataElementInputPreview(page).type("abc"); + await commonDataElementInputPreview(page).fill("abc"); await selectInput(page).press("Enter"); await expect(pillElement).not.toBeVisible(); }); @@ -1627,7 +1520,7 @@ test("should not select a disabled option when a filter is typed", async ({ await dropdownButton(page).click(); const inputElement = selectInput(page); - await inputElement.type("t"); + await inputElement.fill("t"); await inputElement.press("Enter"); await expect( page.locator('[data-element="confirmed-selection-2"]'), @@ -1867,26 +1760,10 @@ test.describe("Accessibility tests for MultiSelect component", () => { mount, page, }) => { - await mount(<MultiSelectWithLazyLoadingComponent />); + await mount(<MultiSelectComponent isLoading />); await dropdownButton(page).click(); await expect(loader(page, 1)).toBeVisible(); - await checkAccessibility(page); - }); - - test("should pass accessibility tests with onListScrollBottom prop", async ({ - mount, - page, - }) => { - await mount(<MultiSelectWithInfiniteScrollComponent />); - - await dropdownButton(page).click(); - await checkAccessibility(page); - // wait for content to finish loading before scrolling - await expect(selectOptionByText(page, "Amber")).toBeVisible(); - await selectListScrollableWrapper(page).evaluate((wrapper) => - wrapper.scrollBy(0, 500), - ); await checkAccessibility(page, undefined, "scrollable-region-focusable"); }); @@ -1983,7 +1860,7 @@ test.describe("Accessibility tests for MultiSelect component", () => { mount, page, }) => { - await mount(<MultiSelectWithManyOptionsAndVirtualScrolling />); + await mount(<WithVirtualScrolling />); await dropdownButton(page).click(); await checkAccessibility(page, undefined, "scrollable-region-focusable"); diff --git a/src/components/select/simple-select/components.test-pw.tsx b/src/components/select/simple-select/components.test-pw.tsx index 80269442fe..28cbb46c4d 100644 --- a/src/components/select/simple-select/components.test-pw.tsx +++ b/src/components/select/simple-select/components.test-pw.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import Typography from "../../../components/typography"; import Content from "../../../components/content"; import { @@ -44,135 +44,6 @@ export const SimpleSelectComponent = (props: Partial<SimpleSelectProps>) => { ); }; -export const SimpleSelectWithLazyLoadingComponent = ( - props: Partial<SimpleSelectProps>, -) => { - const preventLoading = useRef(false); - const [value, setValue] = useState("black"); - const [isLoading, setIsLoading] = useState(true); - const asyncList = [ - <Option text="Amber" value="amber" key="Amber" />, - <Option text="Black" value="black" key="Black" />, - <Option text="Blue" value="blue" key="Blue" />, - <Option text="Brown" value="brown" key="Brown" />, - <Option text="Green" value="green" key="Green" />, - ]; - const [optionList, setOptionList] = useState([ - <Option text="Black" value="black" key="Black" />, - ]); - function onChangeHandler(event: React.ChangeEvent<HTMLInputElement>) { - setValue(event.target.value); - } - function loadList() { - if (preventLoading.current) { - return; - } - preventLoading.current = true; - setIsLoading(true); - setTimeout(() => { - setIsLoading(false); - setOptionList(asyncList); - }, 2000); - } - return ( - <SimpleSelect - name="isLoading" - id="isLoading" - label="color" - value={value} - onChange={onChangeHandler} - onOpen={() => loadList()} - isLoading={isLoading} - {...props} - > - {optionList} - </SimpleSelect> - ); -}; - -export const SimpleSelectWithInfiniteScrollComponent = ( - props: Partial<SimpleSelectProps>, -) => { - const preventLoading = useRef(false); - const preventLazyLoading = useRef(false); - const lazyLoadingCounter = useRef(0); - const [value, setValue] = useState(""); - const [isLoading, setIsLoading] = useState(true); - const asyncList = [ - <Option text="Amber" value="amber" key="Amber" />, - <Option text="Black" value="black" key="Black" />, - <Option text="Blue" value="blue" key="Blue" />, - <Option text="Brown" value="brown" key="Brown" />, - <Option text="Green" value="green" key="Green" />, - ]; - const getLazyLoaded = () => { - const counter = lazyLoadingCounter.current; - return [ - <Option - text={`Lazy Loaded A${counter}`} - value={`lazyA${counter}`} - key={`lazyA${counter}`} - />, - <Option - text={`Lazy Loaded B${counter}`} - value={`lazyB${counter}`} - key={`lazyB${counter}`} - />, - <Option - text={`Lazy Loaded C${counter}`} - value={`lazyC${counter}`} - key={`lazyC${counter}`} - />, - ]; - }; - const [optionList, setOptionList] = useState([ - <Option text="" value="" key="" />, - ]); - useState<string[]>([]); - function onChangeHandler(event: React.ChangeEvent<HTMLInputElement>) { - setValue(event.target.value); - } - function loadList() { - if (preventLoading.current) { - return; - } - preventLoading.current = true; - setIsLoading(true); - setTimeout(() => { - setIsLoading(false); - setOptionList(asyncList); - }, 2000); - } - function onLazyLoading() { - if (preventLazyLoading.current) { - return; - } - preventLazyLoading.current = true; - setIsLoading(true); - setTimeout(() => { - preventLazyLoading.current = false; - lazyLoadingCounter.current += 1; - setIsLoading(false); - setOptionList((prevList) => [...prevList, ...getLazyLoaded()]); - }, 2000); - } - return ( - <SimpleSelect - name="infiniteScroll" - id="infiniteScroll" - label="color" - value={value} - onChange={onChangeHandler} - onOpen={() => loadList()} - isLoading={isLoading} - onListScrollBottom={onLazyLoading} - {...props} - > - {optionList} - </SimpleSelect> - ); -}; - export const SimpleSelectObjectAsValueComponent = ( props: Partial<SimpleSelectProps>, ) => { @@ -219,8 +90,8 @@ export const SimpleSelectMultipleColumnsComponent = ( <SimpleSelect name="withMultipleColumns" id="withMultipleColumns" + label="Clients" multiColumn - defaultValue="2" {...props} tableHeader={ <tr> @@ -326,40 +197,6 @@ export const SimpleSelectGroupComponent = ( ); }; -export const SimpleSelectEventsComponent = ({ - onChange, - ...props -}: Partial<SimpleSelectProps>) => { - const [state, setState] = useState(""); - const setValue = (event: React.ChangeEvent<HTMLInputElement>) => { - setState(event.target.value); - if (onChange) { - onChange(event); - } - }; - return ( - <SimpleSelect - label="color" - value={state} - labelInline - onChange={setValue} - {...props} - > - <Option text="Amber" value="1" /> - <Option text="Black" value="2" /> - <Option text="Blue" value="3" /> - <Option text="Brown" value="4" /> - <Option text="Green" value="5" /> - <Option text="Orange" value="6" /> - <Option text="Pink" value="7" /> - <Option text="Purple" value="8" /> - <Option text="Red" value="9" /> - <Option text="White" value="10" /> - <Option text="Yellow" value="11" /> - </SimpleSelect> - ); -}; - export const SimpleSelectWithLongWrappingTextComponent = () => ( <Box width={400}> <SimpleSelect name="simple" id="simple" label="label" labelInline> diff --git a/src/components/select/simple-select/simple-select.pw.tsx b/src/components/select/simple-select/simple-select.pw.tsx index 593d7d236b..881a8cc627 100644 --- a/src/components/select/simple-select/simple-select.pw.tsx +++ b/src/components/select/simple-select/simple-select.pw.tsx @@ -1,16 +1,14 @@ import { expect, test } from "@playwright/experimental-ct-react17"; import React from "react"; -import { SimpleSelectProps } from ".."; +import SimpleSelect, { SimpleSelectProps } from "."; +import Option from "../option"; import { SimpleSelectComponent, - SimpleSelectWithLazyLoadingComponent, - SimpleSelectWithInfiniteScrollComponent, SimpleSelectMultipleColumnsComponent, SimpleSelectObjectAsValueComponent, SimpleSelectCustomOptionChildrenComponent, SimpleSelectGroupComponent, SimpleSelectWithLongWrappingTextComponent, - SimpleSelectEventsComponent, WithVirtualScrolling, SimpleSelectNestedInDialog, SelectWithOptionGroupHeader, @@ -35,7 +33,6 @@ import { multiColumnsSelectListHeader, multiColumnsSelectListHeaderColumn, multiColumnsSelectListRow, - multiColumnsSelectListRowAt, selectElementInput, selectInput, selectList, @@ -473,108 +470,71 @@ test.describe("SimpleSelect component", () => { await expect(selectOptionByText(page, optionValue11)).toBeInViewport(); }); - test("should render the lazy loader when the prop is set", async ({ + test("renders loader when isLoading prop is set to true", async ({ mount, page, }) => { - await mount(<SimpleSelectWithLazyLoadingComponent />); + await mount(<SimpleSelectComponent isLoading />); - await selectText(page).click(); - await expect(selectListWrapper(page)).toBeVisible(); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).toBeVisible()), - ); - }); + const dropdownIcon = page.getByTestId("input-icon-toggle"); + const dropdownList = page.getByRole("listbox"); - test("should render a lazy loaded option when the infinite scroll prop is set", async ({ - mount, - page, - }) => { - await mount(<SimpleSelectWithInfiniteScrollComponent />); + await dropdownIcon.click(); + await dropdownList.waitFor(); - const option = "Lazy Loaded A1"; - const selectListWrapperElement = selectListWrapper(page); - await selectText(page).click(); - await expect(selectListWrapperElement).toBeVisible(); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).toBeVisible()), - ); - await expect(selectOptionByText(page, option)).toHaveCount(0); - await page.waitForTimeout(2000); - await selectListScrollableWrapper(page).evaluate((wrapper) => { - wrapper.scrollBy(0, 500); - }); - await page.waitForTimeout(250); - await Promise.all( - [0, 1, 2].map((i) => expect(loader(page, i)).not.toBeVisible()), - ); - await expect(selectOptionByText(page, option)).toBeVisible(); + await expect(loader(page, 1)).toBeVisible(); }); - // TODO: Skipped due to flaky focus behaviour. To review in FE-6428 - test.skip("infinite scroll example should not cycle back to the start when using down arrow key", async ({ + test("scroll position of option list doesn't change, if the component's options are dynamically changed", async ({ mount, page, }) => { - // this is a slow test which can sometimes take more than the 30-second default timeout - so tell Playwright - // to increase it - test.slow(); - - await mount(<SimpleSelectWithInfiniteScrollComponent />); - - const inputElement = commonDataElementInputPreview(page); - await inputElement.focus(); - await inputElement.press("ArrowDown"); - const firstOption = selectOptionByText(page, "Amber"); - await firstOption.waitFor(); - for (let i = 0; i < 5; i++) { - // eslint-disable-next-line no-await-in-loop - await inputElement.press("ArrowDown"); - } - await selectOptionByText(page, "Lazy Loaded A1").waitFor(); + const { update } = await mount( + <SimpleSelect label="Colour"> + <Option text="Amber" value="Amber" /> + <Option text="Black" value="Black" /> + <Option text="Cyan" value="Cyan" /> + <Option text="Dark Blue" value="Dark Blue" /> + <Option text="Emerald" value="Emerald" /> + <Option text="Fuchsia" value="Fuchsia" /> + <Option text="Gold" value="Gold" /> + </SimpleSelect>, + ); - // run this 10 times to try to catch any intermittent failures - for (let i = 0; i < 10; i++) { - for (let j = 0; j < 3; j++) { - // eslint-disable-next-line no-await-in-loop - await inputElement.press("ArrowDown"); - } - // wait for new lazy-loaded options to appear - // eslint-disable-next-line no-await-in-loop - await page.waitForTimeout(2000); - // eslint-disable-next-line no-await-in-loop - await expect(firstOption).not.toBeInViewport(); - } - }); + const dropdownIcon = page.getByTestId("input-icon-toggle"); + await dropdownIcon.click(); - test("the list should not change scroll position when the lazy-loaded options appear", async ({ - mount, - page, - }) => { - await mount(<SimpleSelectWithInfiniteScrollComponent />); + const dropdownList = page.getByRole("listbox"); + await dropdownList.waitFor(); - // open the select list and choose an option - const inputElement = commonDataElementInputPreview(page); - await inputElement.focus(); - await inputElement.press("ArrowDown"); - const firstOption = selectOptionByText(page, "Amber"); - await firstOption.waitFor(); - await firstOption.click(); + await page.keyboard.press("ArrowUp"); + await expect(page.getByRole("option").last()).toBeInViewport(); - // reopen the list and scroll to initiate the lazy loading. It's important to not use the keyboard here as that - // won't trigger the bug. - const scrollableWrapper = selectListScrollableWrapper(page); - await selectText(page).click(); - await scrollableWrapper.evaluate((wrapper) => wrapper.scrollBy(0, 500)); - const scrollPositionBeforeLoad = await scrollableWrapper.evaluate( + const scrollPosition = await dropdownList.evaluate( (element) => element.scrollTop, ); - await selectOptionByText(page, "Lazy Loaded A1").waitFor(); - const scrollPositionAfterLoad = await scrollableWrapper.evaluate( + await update( + <SimpleSelect label="Colour"> + <Option text="Amber" value="Amber" /> + <Option text="Black" value="Black" /> + <Option text="Cyan" value="Cyan" /> + <Option text="Dark Blue" value="Dark Blue" /> + <Option text="Emerald" value="Emerald" /> + <Option text="Fuchsia" value="Fuchsia" /> + <Option text="Gold" value="Gold" /> + <Option text="Hot Pink" value="Hot Pink" /> + <Option text="Indigo" value="Indigo" /> + </SimpleSelect>, + ); + + await expect(page.getByRole("option")).toHaveCount(9); + + // check that the scroll position hasn't changed + const newScrollPosition = await dropdownList.evaluate( (element) => element.scrollTop, ); - expect(scrollPositionAfterLoad).toBe(scrollPositionBeforeLoad); + expect(newScrollPosition).toBeCloseTo(scrollPosition, 1); }); test("keyboard navigation should work correctly in multicolumn mode and ensure the selected option is visible", async ({ @@ -583,14 +543,20 @@ test.describe("SimpleSelect component", () => { }) => { await mount(<SimpleSelectMultipleColumnsComponent />); - const inputElement = commonDataElementInputPreview(page); - await inputElement.focus(); - for (let i = 0; i < 3; i++) { - // eslint-disable-next-line no-await-in-loop - await inputElement.press("ArrowDown"); - } - await expect(getDataElementByValue(page, "input")).toHaveValue("Jill Moe"); - await expect(multiColumnsSelectListRowAt(page, 4)).toBeVisible(); + await page.getByText("Please Select...").click(); + await page.getByRole("listbox").waitFor(); + + await page.keyboard.press("ArrowDown"); + await page.keyboard.press("ArrowDown"); + await page.keyboard.press("ArrowDown"); + await page.keyboard.press("ArrowDown"); + await page.keyboard.press("ArrowDown"); + + const lastOption = page.getByRole("option", { name: "Bill Zoe" }); + const input = page.getByRole("combobox"); + + await expect(lastOption).toBeInViewport(); + await expect(input).toHaveValue("Bill Zoe"); }); test("should have correct option highlighted when select list is opened and value is an object", async ({ @@ -674,7 +640,7 @@ test.describe("SimpleSelect component", () => { mount, page, }) => { - await mount(<SimpleSelectMultipleColumnsComponent />); + await mount(<SimpleSelectMultipleColumnsComponent defaultValue="2" />); const columns = 3; await selectText(page).click(); @@ -1152,102 +1118,6 @@ test.describe("Check height of Select list when opened", () => { }); }); -test.describe("Check events for SimpleSelect component", () => { - test("should call onChange event when a list option is selected", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<SimpleSelectEventsComponent onChange={callback} />); - - const position = "first"; - await selectText(page).click(); - await selectOption(page, positionOfElement(position)).click(); - expect(callbackCount).toBe(1); - }); - - test("should call onBlur event when the list is closed", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<SimpleSelectComponent onBlur={callback} />); - - await selectText(page).click(); - await commonDataElementInputPreview(page).blur(); - expect(callbackCount).toBe(1); - }); - - test("should call onClick event when mouse is clicked on text input", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<SimpleSelectComponent onClick={callback} />); - - // need to force the click as the input is covered by the span containing "please select" - // [not clear if this onClick is even needed since a user isn't able to click, but leaving the test in pending - // pending discussion/investigation] - await commonDataElementInputPreview(page).click({ force: true }); - expect(callbackCount).toBe(1); - }); - - test("should call onOpen when select list is opened", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<SimpleSelectComponent onOpen={callback} />); - - await commonDataElementInputPreview(page).click({ force: true }); - expect(callbackCount).toBe(1); - }); - - test("should call onFocus when SimpleSelect is brought into focus", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<SimpleSelectComponent onFocus={callback} />); - - await commonDataElementInputPreview(page).focus(); - expect(callbackCount).toBe(1); - }); - - keyToTrigger.slice(0, 2).forEach((key) => { - test(`should call onKeyDown event when ${key} key is pressed`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<SimpleSelectComponent onKeyDown={callback} />); - - const inputElement = commonDataElementInputPreview(page); - await inputElement.focus(); - await inputElement.press(key); - expect(callbackCount).toBe(1); - }); - }); -}); - test.describe("Check virtual scrolling", () => { test("does not render all virtualised options", async ({ mount, page }) => { await mount(<WithVirtualScrolling />); @@ -1545,35 +1415,57 @@ test.describe("Selection confirmed", () => { mount, page, }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - await mount(<SimpleSelectComponent onListScrollBottom={callback} />); + let called = false; + await mount( + <SimpleSelectComponent + onListScrollBottom={() => { + called = true; + }} + />, + ); - await dropdownButton(page).click(); - await selectOption(page, positionOfElement("first")).click(); - expect(callbackCount).toBe(0); + const dropdownIcon = page.getByTestId("input-icon-toggle"); + const optionList = page.getByRole("listbox"); + + await dropdownIcon.click(); + await optionList.waitFor(); + + const firstOption = page.getByRole("option").first(); + + await firstOption.click(); + + expect(called).toBeFalsy(); }); test("should not be called when an option is clicked and list is re-opened", async ({ mount, page, }) => { - let callbackCount = 0; - const callback = () => { - callbackCount += 1; - }; - - await mount(<SimpleSelectComponent onListScrollBottom={callback} />); + let called = false; - await dropdownButton(page).click(); - await selectListScrollableWrapper(page).evaluate((wrapper) => - wrapper.scrollBy(0, 500), + await mount( + <SimpleSelectComponent + onListScrollBottom={() => { + called = true; + }} + />, ); - await selectOption(page, positionOfElement("first")).click(); - await dropdownButton(page).click(); - expect(callbackCount).toBe(1); + + const dropdownIcon = page.getByTestId("input-icon-toggle"); + const optionList = page.getByRole("listbox"); + + await dropdownIcon.click(); + await optionList.waitFor(); + + const firstOption = page.getByRole("option").first(); + + await firstOption.click(); + await optionList.waitFor({ state: "hidden" }); + + await dropdownIcon.click(); + await optionList.waitFor(); + + expect(called).toBeFalsy(); }); }); @@ -1797,26 +1689,10 @@ test.describe("Accessibility tests for SimpleSelect component", () => { mount, page, }) => { - await mount(<SimpleSelectWithLazyLoadingComponent />); + await mount(<SimpleSelectComponent isLoading />); await selectText(page).click(); await expect(loader(page, 1)).toBeVisible(); - await checkAccessibility(page); - }); - - test("should pass accessibility tests with onListScrollBottom prop", async ({ - mount, - page, - }) => { - await mount(<SimpleSelectWithInfiniteScrollComponent />); - - await selectText(page).click(); - await checkAccessibility(page); - // wait for content to finish loading before scrolling - await expect(selectOptionByText(page, "Amber")).toBeVisible(); - await selectListScrollableWrapper(page).evaluate((wrapper) => - wrapper.scrollBy(0, 500), - ); await checkAccessibility(page, undefined, "scrollable-region-focusable"); }); diff --git a/src/components/simple-color-picker/simple-color-picker.pw.tsx b/src/components/simple-color-picker/simple-color-picker.pw.tsx index b38ea14354..7e58a4f0f7 100644 --- a/src/components/simple-color-picker/simple-color-picker.pw.tsx +++ b/src/components/simple-color-picker/simple-color-picker.pw.tsx @@ -18,7 +18,6 @@ import { getStyle, verifyRequiredAsteriskForLegend, } from "../../../playwright/support/helper"; -import { SimpleColorPickerProps, SimpleColorProps } from "."; import { SimpleColorCustom, SimpleColorPickerCustom, @@ -70,10 +69,6 @@ const colors = [ }, ]; -const indexes = Array.from({ - length: colors.length, -}).map((_, index) => index); - test("should have the expected styling when focused", async ({ mount, page, @@ -356,180 +351,6 @@ test.describe("Check functionality for SimpleColorPicker component", () => { }); }); -test.describe("Check events for SimpleColorPicker component", () => { - test("should call onChange callback when a click event is triggered", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorPickerProps["onChange"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorPickerCustom onChange={callback} />); - - await simpleColorPickerInput(page, 5).click(); - expect(callbackCount).toBe(1); - }); - - test("should call onChange callback and focus the correct item when the right arrow key is pressed", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorPickerProps["onChange"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorPickerCustom onChange={callback} />); - let expectedCount = 0; - - for (const index of indexes) { - expectedCount += 1; - const next = index < colors.length - 1 ? index + 1 : 0; - const colorInput = simpleColorPickerInput(page, index); - // eslint-disable-next-line no-await-in-loop - await colorInput.press("ArrowRight"); - const nextColor = simpleColorPickerInput(page, next); - // eslint-disable-next-line no-await-in-loop - await expect(nextColor).toBeFocused(); - // eslint-disable-next-line no-await-in-loop - await expect(nextColor).toHaveAttribute("value", colors[next].color); - // eslint-disable-next-line no-await-in-loop - expect(callbackCount).toBe(expectedCount); - } - }); - - test("should call onChange callback and focus the correct item when the left arrow key is pressed", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorPickerProps["onChange"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorPickerCustom onChange={callback} />); - let expectedCount = 0; - - for (const index of indexes) { - expectedCount += 1; - const next = index > 0 ? index - 1 : colors.length - 1; - const colorInput = simpleColorPickerInput(page, index); - // eslint-disable-next-line no-await-in-loop - await colorInput.press("ArrowLeft"); - const nextColor = simpleColorPickerInput(page, next); - // eslint-disable-next-line no-await-in-loop - await expect(nextColor).toBeFocused(); - // eslint-disable-next-line no-await-in-loop - await expect(nextColor).toHaveAttribute("value", colors[next].color); - // eslint-disable-next-line no-await-in-loop - expect(callbackCount).toBe(expectedCount); - } - }); - - [ - [9, 4], - [8, 3], - [7, 2], - [6, 1], - [5, 0], - ].forEach(([indexPress, focusedIndex]) => { - test(`should call onChange callback and focus the correct item when the up arrow key is pressed from the element with index ${indexPress}`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorPickerProps["onChange"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorPickerCustom onChange={callback} />); - - await simpleColorPickerInput(page, indexPress).press("ArrowUp"); - await expect(simpleColorPickerInput(page, focusedIndex)).toBeFocused(); - await expect(simpleColorPickerInput(page, focusedIndex)).toHaveAttribute( - "value", - colors[focusedIndex].color, - ); - expect(callbackCount).toBe(1); - }); - }); - - ( - [ - [0, 5], - [1, 6], - [2, 7], - [3, 8], - [4, 9], - ] as const - ).forEach(([indexPress, focusedIndex]) => { - test(`should call onChange callback and focus the correct item when the down arrow key is pressed from the element with index ${indexPress}`, async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorPickerProps["onChange"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorPickerCustom onChange={callback} />); - - await simpleColorPickerInput(page, indexPress).press("ArrowDown"); - await expect(simpleColorPickerInput(page, focusedIndex)).toBeFocused(); - await expect(simpleColorPickerInput(page, focusedIndex)).toHaveAttribute( - "value", - colors[focusedIndex].color, - ); - expect(callbackCount).toBe(1); - }); - }); - - test("should call onBlur callback when a blur event is triggered on the component", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorPickerProps["onBlur"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorPickerCustom onBlur={callback} />); - - const colorInput = simpleColorPickerInput(page, 5); - await colorInput.focus(); - await page.locator("body").click({ position: { x: 0, y: 0 } }); - expect(callbackCount).toBe(1); - }); - - test("should not call onBlur callback when a blur event is triggered on a color", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorPickerProps["onBlur"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorPickerCustom onBlur={callback} />); - - const colorInput = simpleColorPickerInput(page, 5); - await colorInput.focus(); - await colorInput.blur(); - expect(callbackCount).toBe(0); - }); - - test("should not call onBlur callback when a blur event is triggered and isBlurBlocked prop is true", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorPickerProps["onBlur"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorPickerCustom onBlur={callback} isBlurBlocked />); - - const colorInput = simpleColorPickerInput(page, 5); - await colorInput.focus(); - await colorInput.blur(); - expect(callbackCount).toBe(0); - }); -}); - test.describe("Check functionality for SimpleColor component", () => { test("should render with value prop", async ({ mount, page }) => { await mount(<SimpleColorCustom value={colors[7].color} />); @@ -574,51 +395,6 @@ test.describe("Check functionality for SimpleColor component", () => { }); }); -test.describe("Check events for SimpleColor component", () => { - test("should call onChange callback when a click event is triggered", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorProps["onChange"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorCustom onChange={callback} />); - - await simpleColor(page, 1).click(); - expect(callbackCount).toBe(1); - }); - - test("should call onBlur callback when a blur event is triggered", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorProps["onBlur"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorCustom onBlur={callback} />); - - await simpleColor(page, 1).click(); - await page.locator("*:focus").blur(); - expect(callbackCount).toBe(1); - }); - - test("should call onMouseDown callback when a click event is triggered", async ({ - mount, - page, - }) => { - let callbackCount = 0; - const callback: SimpleColorProps["onMouseDown"] = () => { - callbackCount += 1; - }; - await mount(<SimpleColorCustom onMouseDown={callback} />); - - await simpleColor(page, 1).click(); - expect(callbackCount).toBe(1); - }); -}); - test.describe("Check accessibility for SimpleColorPicker component", () => { test("should check accessibility for all proper colors", async ({ mount, diff --git a/src/components/split-button/components.test-pw.tsx b/src/components/split-button/components.test-pw.tsx index 9491b3ba09..295d95d7f8 100644 --- a/src/components/split-button/components.test-pw.tsx +++ b/src/components/split-button/components.test-pw.tsx @@ -1,8 +1,7 @@ import React, { useState } from "react"; -import Button, { ButtonProps } from "../button"; +import Button from "../button"; import Dialog from "../dialog"; import SplitButton, { SplitButtonProps } from "./split-button.component"; -import Box from "../box"; export const SplitButtonList = (props: Partial<SplitButtonProps>) => { return ( @@ -27,18 +26,6 @@ export const SplitButtonNestedInDialog = () => { ); }; -const ButtonWrapper = (props: ButtonProps) => { - return <Button {...props} />; -}; - -export const WithWrapper = (props: Partial<SplitButtonProps>) => ( - <SplitButton text="Split button" {...props}> - <ButtonWrapper>Button 1</ButtonWrapper> - <ButtonWrapper>Button 2</ButtonWrapper> - <ButtonWrapper>Button 3</ButtonWrapper> - </SplitButton> -); - export const TwoSplitButtons = () => { return ( <div> @@ -55,18 +42,3 @@ export const TwoSplitButtons = () => { </div> ); }; - -export const TwoButtonsWithWrapper = (props: Partial<SplitButtonProps>) => ( - <Box> - <SplitButton text="Split button 1" {...props}> - <ButtonWrapper>Button 1</ButtonWrapper> - <ButtonWrapper>Button 2</ButtonWrapper> - <ButtonWrapper>Button 3</ButtonWrapper> - </SplitButton> - <SplitButton text="Split button 2" {...props}> - <ButtonWrapper>Button 4</ButtonWrapper> - <ButtonWrapper>Button 5</ButtonWrapper> - <ButtonWrapper>Button 6</ButtonWrapper> - </SplitButton> - </Box> -); diff --git a/src/components/split-button/split-button.pw.tsx b/src/components/split-button/split-button.pw.tsx index d694506ba8..e6a50959c7 100644 --- a/src/components/split-button/split-button.pw.tsx +++ b/src/components/split-button/split-button.pw.tsx @@ -3,9 +3,7 @@ import { expect, test } from "@playwright/experimental-ct-react17"; import { SplitButtonList, SplitButtonNestedInDialog, - WithWrapper, TwoSplitButtons, - TwoButtonsWithWrapper, } from "./components.test-pw"; import { Accordion } from "../accordion"; import SplitButton, { SplitButtonProps } from "."; @@ -27,11 +25,9 @@ import { splitMainButton, } from "../../../playwright/components/split-button"; import { accordionDefaultTitle } from "../../../playwright/components/accordion"; -import { alertDialogPreview } from "../../../playwright/components/dialog"; import { CHARACTERS } from "../../../playwright/support/constants"; const testData = [CHARACTERS.DIACRITICS, CHARACTERS.SPECIALCHARACTERS]; -const keyToTrigger = ["Enter", "Space", "ArrowDown", "ArrowUp"] as const; test.describe("Styling tests", () => { test(`should render with the expected styling`, async ({ mount, page }) => { @@ -361,406 +357,50 @@ test.describe("Functional tests", () => { await expect(additionalButton(page, 2)).toBeVisible(); }); - test(`should close additional buttons when clicking on the main button`, async ({ - mount, - page, - }) => { - await mount(<SplitButtonList />); - - await splitToggleButton(page).nth(0).click(); - await expect(additionalButtonsContainer(page)).toBeVisible(); - - await splitMainButton(page).click(); - await expect(additionalButtonsContainer(page)).not.toBeVisible(); - }); - - test(`should verify pressing Tab moves focus to next child button, then closes the list and focuses the next element on the page`, async ({ + test(`closes the list and focuses the next element on the page, when Tab key is pressed on last child button`, async ({ mount, page, }) => { await mount(<TwoSplitButtons />); - await splitToggleButton(page).nth(0).click(); - await expect(additionalButton(page, 0)).toBeFocused(); - await page.keyboard.press("Tab"); - await expect(additionalButton(page, 1)).toBeFocused(); - await page.keyboard.press("Tab"); - await expect(additionalButton(page, 2)).toBeFocused(); - await page.keyboard.press("Tab"); - - await expect(additionalButton(page, 0)).not.toBeVisible(); - await expect(mainButton(page).nth(1)).toBeFocused(); - }); - - test(`should verify pressing ArrowDown moves focus to next child button and does not loop when last child is focused`, async ({ - mount, - page, - }) => { - await mount(<TwoSplitButtons />); - - await splitToggleButton(page).nth(0).click(); - await page.keyboard.press("ArrowDown"); - await expect(additionalButton(page, 1)).toBeFocused(); - await page.keyboard.press("ArrowDown"); - await expect(additionalButton(page, 2)).toBeFocused(); - await page.keyboard.press("ArrowDown"); - await expect(additionalButton(page, 2)).toBeFocused(); - }); - - test(`should verify pressing Shift+Tab moves focus to previous child button, then closes the list and focuses the toggle button`, async ({ - mount, - page, - }) => { - await mount(<TwoSplitButtons />); - - await splitToggleButton(page).nth(0).click(); - await additionalButton(page, 1).focus(); - await page.keyboard.press("Shift+Tab"); - await expect(additionalButton(page, 0)).toBeFocused(); - await page.keyboard.press("Shift+Tab"); - - await expect(additionalButton(page, 0)).not.toBeVisible(); - await expect(splitToggleButton(page).first()).toBeFocused(); - }); - - test(`should verify pressing ArrowUp moves focus to previous child button and does not loop when first child is focused`, async ({ - mount, - page, - }) => { - await mount(<TwoSplitButtons />); - - await splitToggleButton(page).nth(0).click(); - await additionalButton(page, 2).focus(); - await page.keyboard.press("ArrowUp"); - await expect(additionalButton(page, 1)).toBeFocused(); - await page.keyboard.press("ArrowUp"); - await expect(additionalButton(page, 0)).toBeFocused(); - await page.keyboard.press("ArrowUp"); - await expect(additionalButton(page, 0)).toBeFocused(); - }); - - test(`should verify pressing metaKey + ArrowUp moves focus to first child button`, async ({ - mount, - page, - }) => { - await mount(<TwoSplitButtons />); - - await splitToggleButton(page).nth(0).click(); - await additionalButton(page, 2).focus(); - await page.keyboard.down("Meta"); - await page.keyboard.press("ArrowUp"); - await expect(additionalButton(page, 0)).toBeFocused(); - }); - - test(`should verify pressing ctrlKey + ArrowUp moves focus to first child button`, async ({ - mount, - page, - }) => { - await mount(<TwoSplitButtons />); - - await splitToggleButton(page).nth(0).click(); - await additionalButton(page, 2).focus(); - await page.keyboard.down("Control"); - await page.keyboard.press("ArrowUp"); - await expect(additionalButton(page, 0)).toBeFocused(); - }); - - test(`should verify pressing Home moves focus to first child button`, async ({ - mount, - page, - }) => { - await mount(<TwoSplitButtons />); - - await splitToggleButton(page).nth(0).click(); - await additionalButton(page, 2).focus(); - await page.keyboard.press("Home"); - await expect(additionalButton(page, 0)).toBeFocused(); - }); - - test(`should verify pressing metaKey + ArrowDown moves focus to last child button`, async ({ - mount, - page, - }) => { - await mount(<TwoSplitButtons />); - - await splitToggleButton(page).nth(0).click(); - await page.keyboard.down("Meta"); - await page.keyboard.press("ArrowDown"); - await expect(additionalButton(page, 2)).toBeFocused(); - }); - - test(`should verify pressing ctrlKey + ArrowDown moves focus to last child button`, async ({ - mount, - page, - }) => { - await mount(<TwoSplitButtons />); - - await splitToggleButton(page).nth(0).click(); - await page.keyboard.down("Control"); - await page.keyboard.press("ArrowDown"); - await expect(additionalButton(page, 2)).toBeFocused(); - }); - - test(`should verify pressing End moves focus to last child button`, async ({ - mount, - page, - }) => { - await mount(<TwoSplitButtons />); - - await splitToggleButton(page).nth(0).click(); - await page.keyboard.press("End"); - await expect(additionalButton(page, 2)).toBeFocused(); - }); - - test(`should verify clicking additional buttons closes SplitButton`, async ({ - mount, - page, - }) => { - await mount(<SplitButtonList />); - - await splitToggleButton(page).nth(0).click(); - await additionalButton(page, 0).click(); - await expect(additionalButtonsContainer(page)).toHaveCount(0); - }); - - test(`should verify pressing Esc closes SplitButton`, async ({ - mount, - page, - }) => { - await mount(<SplitButtonList />); - - await splitToggleButton(page).nth(0).click(); - await additionalButton(page, 1).focus(); - await page.keyboard.press("Escape"); - await expect(additionalButtonsContainer(page)).toHaveCount(0); - }); - - [...keyToTrigger].forEach((key) => { - test(`should verify pressing ${key} on the main button opens list and focuses first button`, async ({ - mount, - page, - }) => { - await mount(<SplitButtonList />); - - await splitToggleButton(page).nth(0).press(key); - await expect(additionalButton(page, 0)).toBeFocused(); - }); - }); -}); - -test.describe("Functional tests in a custom component", () => { - test(`should verify clicking the toggle button opens the additional buttons`, async ({ - mount, - page, - }) => { - await mount(<WithWrapper />); - - await splitToggleButton(page).nth(0).click(); - const buttonList = page.getByRole("list"); - await buttonList.waitFor(); - await expect(additionalButton(page, 0)).toBeVisible(); - await expect(additionalButton(page, 1)).toBeVisible(); - await expect(additionalButton(page, 2)).toBeVisible(); - }); + const dropdownButton = page + .getByRole("button", { name: "Show more" }) + .first(); + await dropdownButton.click(); - test(`should verify pressing Tab moves focus to next child button, then closes the list and focuses the next element on the page`, async ({ - mount, - page, - }) => { - await mount(<TwoButtonsWithWrapper />); - - await splitToggleButton(page).nth(0).click(); const buttonList = page.getByRole("list"); await buttonList.waitFor(); - const firstButton = buttonList.getByRole("button").first(); - const secondButton = buttonList.getByRole("button").nth(1); - const thirdButton = buttonList.getByRole("button").last(); + const lastButton = page.getByRole("button", { name: "Button 3" }); + await lastButton.press("Tab"); - await expect(firstButton).toBeFocused(); - await firstButton.press("Tab"); - await expect(secondButton).toBeFocused(); - await secondButton.press("Tab"); - await expect(thirdButton).toBeFocused(); - await thirdButton.press("Tab"); + await expect( + page.getByRole("button", { name: "Split Button 2" }), + ).toBeFocused(); await expect(buttonList).toBeHidden(); - await expect(mainButton(page).nth(1)).toBeFocused(); - }); - - test(`should verify pressing ArrowDown moves focus to next child button and should not loop when last child is focused`, async ({ - mount, - page, - }) => { - await mount(<TwoButtonsWithWrapper />); - - await splitToggleButton(page).nth(0).click(); - const buttonList = page.getByRole("list"); - await buttonList.waitFor(); - - await page.keyboard.press("ArrowDown"); - await page.keyboard.press("ArrowDown"); - await page.keyboard.press("ArrowDown"); - await page.waitForTimeout(1000); - await expect(additionalButton(page, 2)).toBeFocused(); - }); - - test(`should verify pressing Shift+Tab moves focus to previous child button, then closes the list and focuses the toggle button`, async ({ - mount, - page, - }) => { - await mount(<TwoButtonsWithWrapper />); - - await splitToggleButton(page).nth(0).click(); - const buttonList = page.getByRole("list"); - await buttonList.waitFor(); - - await additionalButton(page, 1).focus(); - await page.keyboard.press("Shift+Tab", { delay: 1000 }); - await expect(additionalButton(page, 0)).toBeFocused(); - await page.keyboard.press("Shift+Tab", { delay: 1000 }); - - await expect(additionalButton(page, 0)).not.toBeVisible(); - await expect(splitToggleButton(page).nth(0)).toBeFocused(); }); +}); - test(`should verify pressing ArrowUp moves focus to previous child button and does not loop when first child is focused`, async ({ +test.describe("when SplitButton is nested inside of a Dialog component", () => { + test(`does not close dialog when Escape key is pressed while child buttons are visible`, async ({ mount, page, }) => { - await mount(<TwoButtonsWithWrapper />); - - await splitToggleButton(page).nth(0).click(); - const buttonList = page.getByRole("list"); - await buttonList.waitFor(); - await additionalButton(page, 2).focus(); - - await page.keyboard.press("ArrowUp"); - await page.keyboard.press("ArrowUp"); - await page.keyboard.press("ArrowUp"); + await mount(<SplitButtonNestedInDialog />); - await expect(additionalButton(page, 0)).toBeFocused(); - }); + const dialog = page.getByRole("dialog"); + await dialog.waitFor(); - test(`should verify pressing metaKey + ArrowUp moves focus to first child button`, async ({ - mount, - page, - }) => { - await mount(<TwoButtonsWithWrapper />); + const toggleButton = page.getByRole("button", { name: "Show more" }); + await toggleButton.click(); - await splitToggleButton(page).nth(0).click(); const buttonList = page.getByRole("list"); await buttonList.waitFor(); - await additionalButton(page, 2).focus(); - await page.keyboard.down("Meta"); - await page.keyboard.press("ArrowUp"); - await expect(additionalButton(page, 0)).toBeFocused(); - }); - - test(`should verify pressing ctrlKey + ArrowUp moves focus to first child button`, async ({ - mount, - page, - }) => { - await mount(<TwoButtonsWithWrapper />); - await splitToggleButton(page).nth(0).click(); - const buttonList = page.getByRole("list"); - await buttonList.waitFor(); - await additionalButton(page, 2).focus(); - await page.keyboard.down("Control"); - await page.keyboard.press("ArrowUp"); - await expect(additionalButton(page, 0)).toBeFocused(); - }); - - test(`should verify pressing Home moves focus to first child button`, async ({ - mount, - page, - }) => { - await mount(<TwoButtonsWithWrapper />); - - await splitToggleButton(page).nth(0).click(); - await additionalButton(page, 2).focus(); - await page.keyboard.press("Home"); - await expect(additionalButton(page, 0)).toBeFocused(); - }); - - test(`should verify pressing metaKey + ArrowDown moves focus to last child button`, async ({ - mount, - page, - }) => { - await mount(<TwoButtonsWithWrapper />); - - await splitToggleButton(page).nth(0).click(); - - await page.keyboard.down("Meta"); - await page.keyboard.press("ArrowDown"); - await expect(additionalButton(page, 2)).toBeFocused(); - }); - - test(`should verify pressing ctrlKey + ArrowDown moves focus to last child button`, async ({ - mount, - page, - }) => { - await mount(<TwoButtonsWithWrapper />); - - await splitToggleButton(page).nth(0).click(); - await page.keyboard.down("Control"); - await page.keyboard.press("ArrowDown"); - await expect(additionalButton(page, 2)).toBeFocused(); - }); - - test(`should verify pressing End moves focus to last child button`, async ({ - mount, - page, - }) => { - await mount(<TwoButtonsWithWrapper />); - - await splitToggleButton(page).nth(0).click(); - await page.keyboard.press("End"); - await expect(additionalButton(page, 2)).toBeFocused(); - }); - - test(`should verify clicking additional buttons closes SplitButton`, async ({ - mount, - page, - }) => { - await mount(<WithWrapper />); - - await splitToggleButton(page).nth(0).click(); - - await additionalButton(page, 0).click(); - await expect(additionalButtonsContainer(page)).toHaveCount(0); - }); - - test(`should verify pressing Esc closes SplitButton`, async ({ - mount, - page, - }) => { - await mount(<WithWrapper />); - - await splitToggleButton(page).nth(0).click(); - const buttonList = page.getByRole("list"); - await buttonList.waitFor(); - await additionalButton(page, 1).focus(); await page.keyboard.press("Escape"); - await expect(additionalButtonsContainer(page)).toHaveCount(0); - }); -}); - -test.describe("when SplitButton is nested inside of a Dialog component", () => { - test(`should not close the Dialog when SplitButton is closed by pressing an escape key`, async ({ - mount, - page, - }) => { - await mount(<SplitButtonNestedInDialog />); - await splitToggleButton(page).nth(0).click(); - await expect(additionalButtonsContainer(page)).toHaveCount(1); - await additionalButton(page, 1).focus(); - await page.keyboard.press("Escape"); - await expect(additionalButtonsContainer(page)).toHaveCount(0); - await expect(alertDialogPreview(page)).toHaveCount(1); - await page.keyboard.press("Escape"); - await expect(alertDialogPreview(page)).toHaveCount(0); + await buttonList.waitFor({ state: "hidden" }); + await expect(dialog).toBeVisible(); }); }); diff --git a/src/components/split-button/split-button.test.tsx b/src/components/split-button/split-button.test.tsx index 1553edd964..46c4c55675 100644 --- a/src/components/split-button/split-button.test.tsx +++ b/src/components/split-button/split-button.test.tsx @@ -52,7 +52,7 @@ testStyledSystemMargin( () => screen.getByTestId("split-button-container"), ); -test("should render with only the main and toggle buttons visible when only required props passed", () => { +test("renders the main and toggle buttons", () => { render( <SplitButton text="Main"> <Button>Single Button</Button> @@ -66,23 +66,7 @@ test("should render with only the main and toggle buttons visible when only requ ).not.toBeInTheDocument(); }); -test("should render with the main, toggle and child buttons visible when required props passed and toggle is clicked", async () => { - const user = userEvent.setup(); - render( - <SplitButton text="Main"> - <Button>Single Button</Button> - </SplitButton>, - ); - - const toggle = await screen.findByRole("button", { name: "Show more" }); - await user.click(toggle); - - expect(screen.getByRole("button", { name: "Main" })).toBeVisible(); - expect(toggle).toBeVisible(); - expect(screen.getByRole("button", { name: "Single Button" })).toBeVisible(); -}); - -test("should render with the main, toggle and multiple child buttons visible when required props passed and toggle is clicked", async () => { +test("renders child buttons when toggle button is clicked", async () => { const user = userEvent.setup(); render( <SplitButton text="Main"> @@ -108,6 +92,23 @@ test("should render with the main, toggle and multiple child buttons visible whe ).toBeVisible(); }); +test("focuses first child button when toggle button is clicked", async () => { + const user = userEvent.setup(); + render( + <SplitButton text="Main"> + <Button>Single Button</Button> + </SplitButton>, + ); + + await user.click(screen.getByRole("button", { name: "Show more" })); + + const childButton = await screen.findByRole("button", { + name: "Single Button", + }); + + expect(childButton).toHaveFocus(); +}); + test("should render with the correct styles when 'size' is 'small''", async () => { const { fontSize, minHeight, paddingLeft, paddingRight } = buildSizeConfig("small"); @@ -395,22 +396,6 @@ test("should render additional button text with align set to 'right'", async () expect(childButton).toHaveStyle({ textAlign: "right" }); }); -test("should render the child buttons when a click event detected on toggle button", async () => { - const user = userEvent.setup(); - render( - <SplitButton text="Main"> - <Button>Single Button</Button> - </SplitButton>, - ); - - await user.click(screen.getByRole("button", { name: "Show more" })); - const childButton = await screen.findByRole("button", { - name: "Single Button", - }); - - expect(childButton).toBeVisible(); -}); - test("should not render the child buttons when a click event detected on toggle button and 'disabled' prop set", async () => { const user = userEvent.setup(); render( @@ -752,7 +737,7 @@ test("should support navigating the additional buttons via up arrow key but stop expect(button1).toHaveFocus(); }); -test("should support navigating to the last child button via end key", async () => { +test("focuses last child button when End key is pressed", async () => { const user = userEvent.setup(); render( <SplitButton text="Main"> @@ -777,30 +762,118 @@ test("should support navigating to the last child button via end key", async () expect(button3).toHaveFocus(); }); -test("should support navigating to the first child button via home key", async () => { +test("focuses last child button when Control and ArrowDown key are pressed together", async () => { const user = userEvent.setup(); render( <SplitButton text="Main"> - <Button key="testKey1">Extra Button 1</Button> - <Button key="testKey2">Extra Button 2</Button> - <Button key="testKey3">Extra Button 3</Button> + <Button>Extra Button 1</Button> + <Button>Extra Button 2</Button> + <Button>Extra Button 3</Button> </SplitButton>, ); - const toggle = screen.getByRole("button", { name: "Show more" }); - toggle.focus(); - await user.keyboard("{arrowDown}"); + await user.click(screen.getByRole("button", { name: "Show more" })); + + const button3 = await screen.findByRole("button", { + name: "Extra Button 3", + }); + + await user.keyboard("{Control>}{ArrowDown}{/Control}"); + + expect(button3).toHaveFocus(); +}); + +test("focuses last child button when Meta and ArrowDown key are pressed together", async () => { + const user = userEvent.setup(); + render( + <SplitButton text="Main"> + <Button>Extra Button 1</Button> + <Button>Extra Button 2</Button> + <Button>Extra Button 3</Button> + </SplitButton>, + ); + + await user.click(screen.getByRole("button", { name: "Show more" })); + + const button3 = await screen.findByRole("button", { + name: "Extra Button 3", + }); + + await user.keyboard("{Meta>}{ArrowDown}{/Meta}"); + + expect(button3).toHaveFocus(); +}); + +test("focuses first child button when Home key is pressed", async () => { + const user = userEvent.setup(); + render( + <SplitButton text="Main"> + <Button>Extra Button 1</Button> + <Button>Extra Button 2</Button> + <Button>Extra Button 3</Button> + </SplitButton>, + ); + + await user.click(screen.getByRole("button", { name: "Show more" })); + const button1 = await screen.findByRole("button", { name: "Extra Button 1", }); const button3 = await screen.findByRole("button", { name: "Extra Button 3", }); - expect(button1).toHaveFocus(); - await user.keyboard("{end}"); + + await user.keyboard("{End}"); expect(button3).toHaveFocus(); - await user.keyboard("{home}"); + + await user.keyboard("{Home}"); + expect(button1).toHaveFocus(); +}); + +test("focuses first child button when Control and ArrowUp key are pressed together", async () => { + const user = userEvent.setup(); + render( + <SplitButton text="Main"> + <Button>Extra Button 1</Button> + <Button>Extra Button 2</Button> + <Button>Extra Button 3</Button> + </SplitButton>, + ); + + await user.click(screen.getByRole("button", { name: "Show more" })); + const button1 = await screen.findByRole("button", { + name: "Extra Button 1", + }); + await user.keyboard("{End}"); + + expect(screen.getByRole("button", { name: "Extra Button 3" })).toHaveFocus(); + + await user.keyboard("{Control>}{ArrowUp}{/Control}"); + + expect(button1).toHaveFocus(); +}); + +test("focuses first child button when Meta and ArrowUp key are pressed together", async () => { + const user = userEvent.setup(); + render( + <SplitButton text="Main"> + <Button>Extra Button 1</Button> + <Button>Extra Button 2</Button> + <Button>Extra Button 3</Button> + </SplitButton>, + ); + + await user.click(screen.getByRole("button", { name: "Show more" })); + const button1 = await screen.findByRole("button", { + name: "Extra Button 1", + }); + await user.keyboard("{End}"); + + expect(screen.getByRole("button", { name: "Extra Button 3" })).toHaveFocus(); + + await user.keyboard("{Meta>}{ArrowUp}{/Meta}"); + expect(button1).toHaveFocus(); }); @@ -832,7 +905,6 @@ test("should support navigating the additional buttons via tab key", async () => expect(button2).toHaveFocus(); await user.tab(); expect(button3).toHaveFocus(); - await user.tab(); }); test("should support navigating the additional buttons via shift+tab key, hide the list when pressed on first button and refocus toggle", async () => { diff --git a/src/components/text-editor/text-editor.pw.tsx b/src/components/text-editor/text-editor.pw.tsx index 7616e2aed7..d6cc85659e 100644 --- a/src/components/text-editor/text-editor.pw.tsx +++ b/src/components/text-editor/text-editor.pw.tsx @@ -28,12 +28,6 @@ import { } from "../../../playwright/support/helper"; import { VALIDATION, CHARACTERS } from "../../../playwright/support/constants"; -const textForInput = "Testing is awesome"; -const linkText = "https://carbon.sage.com"; -const longText = - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; -const longTextAssert = - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore "; const testData = [CHARACTERS.DIACRITICS, CHARACTERS.SPECIALCHARACTERS]; const buttonNames = ["bold", "italic", "bullet-list", "number-list"]; @@ -43,328 +37,291 @@ test.describe("Functionality tests", () => { const textInput = textEditorInput(page); await textInput.clear(); - await textInput.fill(textForInput); + await textInput.fill("Testing is awesome"); await expect(textEditorCounter(page)).toHaveText("2,982 characters left"); }); - buttonNames.slice(0, 2).forEach((buttonType) => { - test(`should render text using ${buttonType} style`, async ({ - mount, - page, - }) => { - await mount(<TextEditorCustom />); - - const textInput = textEditorInput(page); - const toolbar = textEditorToolbar(page, buttonType); - await toolbar.click(); - await textInput.clear(); - await textInput.fill(textForInput); - - if (buttonType === "bold") { - await expect(innerText(page)).toHaveCSS("font-weight", "700"); - } else { - await expect(innerText(page)).toHaveCSS("font-style", "italic"); - } - await expect(toolbar).toHaveCSS("background-color", "rgb(0, 50, 76)"); - }); - }); - - test(`should render text in bullet-list style`, async ({ mount, page }) => { - await mount(<TextEditorCustom />); - - const textInput = textEditorInput(page); - const toolbar = textEditorToolbar(page, "bullet-list"); - await toolbar.click(); - await textInput.clear(); - await textInput.pressSequentially("Testing", { delay: 100 }); - await page.keyboard.press("Enter"); - await textInput.pressSequentially("is", { delay: 100 }); - await page.keyboard.press("Enter"); - await textInput.pressSequentially("awesome", { delay: 100 }); - - await expect(innerTextList(page, "ul", 1)).toHaveText("Testing"); - await expect(innerTextList(page, "ul", 2)).toHaveText("is"); - await expect(innerTextList(page, "ul", 3)).toHaveText("awesome"); - - await expect(toolbar).toHaveCSS("background-color", "rgb(0, 50, 76)"); - }); - - test(`should render text in number-list style`, async ({ mount, page }) => { - await mount(<TextEditorCustom />); - - const textInput = textEditorInput(page); - const toolbar = textEditorToolbar(page, "number-list"); - await toolbar.click(); - await textInput.clear(); - await textInput.pressSequentially("Testing", { delay: 100 }); - await page.keyboard.press("Enter"); - await textInput.pressSequentially("is", { delay: 100 }); - await page.keyboard.press("Enter"); - await textInput.pressSequentially("awesome", { delay: 100 }); - - await expect(innerTextList(page, "ol", 1)).toHaveText("Testing"); - await expect(innerTextList(page, "ol", 2)).toHaveText("is"); - await expect(innerTextList(page, "ol", 3)).toHaveText("awesome"); - - await expect(toolbar).toHaveCSS("background-color", "rgb(0, 50, 76)"); - }); - - test(`should focus all editor toolbar buttons using ArrowRight keyboard key`, async ({ + test("renders text as bold when bold button is selected", async ({ mount, page, }) => { await mount(<TextEditorCustom />); - const textInput = textEditorInput(page); - await textInput.click(); - await page.keyboard.press("Tab", { delay: 1000 }); - // Expect bold to be focused - await expect(textEditorToolbar(page, "bold")).toBeFocused(); - // Expect italic to be focused - await page.keyboard.press("ArrowRight", { delay: 1000 }); - await expect(textEditorToolbar(page, "italic")).toBeFocused(); - // Expect bullet-list to be focused - await page.keyboard.press("ArrowRight", { delay: 1000 }); - await expect(textEditorToolbar(page, "bullet-list")).toBeFocused(); - // Expect number-list to be focused - await page.keyboard.press("ArrowRight", { delay: 1000 }); - await expect(textEditorToolbar(page, "number-list")).toBeFocused(); - }); + const textInput = page.getByRole("textbox"); + const boldButton = page.getByRole("button", { name: "bold" }); + await boldButton.click(); - test(`should focus all editor toolbar buttons using ArrowLeft keyboard key`, async ({ - mount, - page, - }) => { - await mount(<TextEditorCustom />); + await textInput.fill("Testing"); - const textInput = textEditorInput(page); - await textInput.click(); - await page.keyboard.press("Tab", { delay: 1000 }); - // Expect bold to be focused - await expect(textEditorToolbar(page, "bold")).toBeFocused(); - // Expect italic to be focused - await page.keyboard.press("ArrowLeft", { delay: 1000 }); - await expect(textEditorToolbar(page, "number-list")).toBeFocused(); - // Expect bullet-list to be focused - await page.keyboard.press("ArrowLeft", { delay: 1000 }); - await expect(textEditorToolbar(page, "bullet-list")).toBeFocused(); - // Expect number-list to be focused - await page.keyboard.press("ArrowLeft", { delay: 1000 }); - await expect(textEditorToolbar(page, "italic")).toBeFocused(); + await expect(page.getByText("Testing")).toHaveCSS("font-weight", "700"); + await expect(boldButton).toHaveCSS("background-color", "rgb(0, 50, 76)"); }); - test(`should activate bold button using Enter keyboard key`, async ({ + test("renders text as italic when italic button is selected", async ({ mount, page, }) => { await mount(<TextEditorCustom />); - const textInput = textEditorInput(page); - await textInput.click(); - await page.keyboard.press("Tab", { delay: 1000 }); - await expect(textEditorToolbar(page, "bold")).toBeFocused(); - await page.keyboard.press("Enter", { delay: 1000 }); - await expect(textEditorToolbar(page, "bold")).toHaveCSS( - "background-color", - "rgb(0, 50, 76)", - ); + const textInput = page.getByRole("textbox"); + const italicButton = page.getByRole("button", { name: "italic" }); + await italicButton.click(); + + await textInput.fill("Testing"); + + await expect(page.getByText("Testing")).toHaveCSS("font-style", "italic"); + await expect(italicButton).toHaveCSS("background-color", "rgb(0, 50, 76)"); }); - test(`should activate italic button using Enter keyboard key`, async ({ + test("renders text as an unordered list when bullet list button is selected", async ({ mount, page, }) => { await mount(<TextEditorCustom />); - const textInput = textEditorInput(page); - await textInput.click(); - await page.keyboard.press("Tab", { delay: 1000 }); - await page.keyboard.press("ArrowRight", { delay: 1000 }); + const textInput = page.getByRole("textbox"); + const bulletListButton = page.getByRole("button", { name: "bullet-list" }); + await bulletListButton.click(); - await expect(textEditorToolbar(page, "italic")).toBeFocused(); - await page.keyboard.press("Enter", { delay: 1000 }); - await expect(textEditorToolbar(page, "italic")).toHaveCSS( + await textInput.fill("Testing"); + await page.keyboard.press("Enter"); + await textInput.pressSequentially("is"); + await page.keyboard.press("Enter"); + await textInput.pressSequentially("awesome"); + + await expect(textInput.locator("ul").locator("li")).toHaveText([ + "Testing", + "is", + "awesome", + ]); + await expect(bulletListButton).toHaveCSS( "background-color", "rgb(0, 50, 76)", ); }); - test(`should activate bullet-list button using Enter keyboard key`, async ({ + test("renders text as an ordered list when number list button is selected", async ({ mount, page, }) => { await mount(<TextEditorCustom />); - const textInput = textEditorInput(page); - await textInput.click(); - await page.keyboard.press("Tab", { delay: 1000 }); - await page.keyboard.press("ArrowRight", { delay: 1000 }); - await page.keyboard.press("ArrowRight", { delay: 1000 }); + const textInput = page.getByRole("textbox"); + const numberListButton = page.getByRole("button", { name: "number-list" }); + await numberListButton.click(); - await expect(textEditorToolbar(page, "bullet-list")).toBeFocused(); - await page.keyboard.press("Enter", { delay: 1000 }); - await expect(textEditorToolbar(page, "bullet-list")).toHaveCSS( + await textInput.fill("Testing"); + await page.keyboard.press("Enter"); + await textInput.pressSequentially("is"); + await page.keyboard.press("Enter"); + await textInput.pressSequentially("awesome"); + + await expect(textInput.locator("ol").locator("li")).toHaveText([ + "Testing", + "is", + "awesome", + ]); + await expect(numberListButton).toHaveCSS( "background-color", "rgb(0, 50, 76)", ); }); - test(`should activate number-list button using Enter keyboard key`, async ({ + test(`all toolbar buttons can be focused when navigating with the right arrow key`, async ({ mount, page, }) => { await mount(<TextEditorCustom />); - const textInput = textEditorInput(page); + const textInput = page.getByRole("textbox"); await textInput.click(); - await page.keyboard.press("Tab", { delay: 1000 }); - await page.keyboard.press("ArrowRight", { delay: 1000 }); - await page.keyboard.press("ArrowRight", { delay: 1000 }); - await page.keyboard.press("ArrowRight", { delay: 1000 }); - await expect(textEditorToolbar(page, "number-list")).toBeFocused(); - await page.keyboard.press("Enter", { delay: 1000 }); - await expect(textEditorToolbar(page, "number-list")).toHaveCSS( - "background-color", - "rgb(0, 50, 76)", - ); - }); + await page.keyboard.press("Tab"); + await expect(page.getByRole("button", { name: "bold" })).toBeFocused(); - test(`should activate bold button using Space keyboard key`, async ({ - mount, - page, - }) => { - await mount(<TextEditorCustom />); + await page.keyboard.press("ArrowRight"); + await expect(page.getByRole("button", { name: "italic" })).toBeFocused(); - const boldButton = page.getByRole("button", { name: "bold" }); - await boldButton.press("Space"); + await page.keyboard.press("ArrowRight"); + await expect( + page.getByRole("button", { name: "bullet-list" }), + ).toBeFocused(); - await expect(textEditorToolbar(page, "bold")).toHaveCSS( - "background-color", - "rgb(0, 50, 76)", - ); + await page.keyboard.press("ArrowRight"); + await expect( + page.getByRole("button", { name: "number-list" }), + ).toBeFocused(); }); - test(`should activate italic button using Space keyboard key`, async ({ + test(`all toolbar buttons can be focused when navigating with the left arrow key`, async ({ mount, page, }) => { await mount(<TextEditorCustom />); - const italicButton = page.getByRole("button", { name: "italic" }); - await italicButton.press("Space"); + const textInput = page.getByRole("textbox"); + await textInput.click(); - await expect(textEditorToolbar(page, "italic")).toHaveCSS( - "background-color", - "rgb(0, 50, 76)", - ); - }); + await page.keyboard.press("Tab"); + await expect(page.getByRole("button", { name: "bold" })).toBeFocused(); - test(`should activate bullet-list button using Space keyboard key`, async ({ - mount, - page, - }) => { - await mount(<TextEditorCustom />); + await page.keyboard.press("ArrowLeft"); + await expect( + page.getByRole("button", { name: "number-list" }), + ).toBeFocused(); - const bulletListButton = page.getByRole("button", { name: "bullet-list" }); - await bulletListButton.press("Space"); + await page.keyboard.press("ArrowLeft"); + await expect( + page.getByRole("button", { name: "bullet-list" }), + ).toBeFocused(); - await expect(textEditorToolbar(page, "bullet-list")).toHaveCSS( - "background-color", - "rgb(0, 50, 76)", - ); + await page.keyboard.press("ArrowLeft"); + await expect(page.getByRole("button", { name: "italic" })).toBeFocused(); }); - test(`should activate number-list button using Space keyboard key`, async ({ - mount, - page, - }) => { - await mount(<TextEditorCustom />); + ["Space", "Enter"].forEach((key) => + test(`focuses text input when bold button is selected using ${key} keyboard key`, async ({ + mount, + page, + }) => { + await mount(<TextEditorCustom />); - const numberListButton = page.getByRole("button", { name: "number-list" }); - await numberListButton.press("Space"); + const boldButton = page.getByRole("button", { name: "bold" }); + await boldButton.press(key); - await expect(textEditorToolbar(page, "number-list")).toHaveCSS( - "background-color", - "rgb(0, 50, 76)", - ); - }); + const textInput = page.getByRole("textbox"); + await expect(textInput).toBeFocused(); + }), + ); - buttonNames.forEach((buttonType, times) => { - test(`should focus the input when ${buttonType} is selected/deselected with enter key`, async ({ + ["Space", "Enter"].forEach((key) => + test(`focuses text input when italic button is selected using ${key} keyboard key`, async ({ mount, page, }) => { await mount(<TextEditorCustom />); - const textInput = textEditorInput(page); - await textInput.focus(); - await page.keyboard.press("Tab"); - for (let i = 0; i < times; i++) { - await page.keyboard.press("ArrowRight"); - } - await expect(textEditorToolbar(page, buttonType)).toBeFocused(); - await page.keyboard.press("Enter"); + const italicButton = page.getByRole("button", { name: "italic" }); + await italicButton.press(key); + + const textInput = page.getByRole("textbox"); await expect(textInput).toBeFocused(); - await page.keyboard.press("Tab"); - for (let i = 0; i < times; i++) { - await page.keyboard.press("ArrowRight"); - } - await expect(textEditorToolbar(page, buttonType)).toBeFocused(); - await page.keyboard.press("Enter"); + }), + ); + + ["Space", "Enter"].forEach((key) => + test(`focuses text input when bullet list button is selected using ${key} keyboard key`, async ({ + mount, + page, + }) => { + await mount(<TextEditorCustom />); + + const bulletListButton = page.getByRole("button", { + name: "bullet-list", + }); + await bulletListButton.press(key); + + const textInput = page.getByRole("textbox"); await expect(textInput).toBeFocused(); - }); - }); + }), + ); - buttonNames.forEach((buttonType, times) => { - test(`should focus the input when ${buttonType} is selected/deselected with space key`, async ({ + ["Space", "Enter"].forEach((key) => + test(`focuses text input when number list button is selected using ${key} keyboard key`, async ({ mount, page, }) => { await mount(<TextEditorCustom />); - const textInput = textEditorInput(page); - await textInput.focus(); - await page.keyboard.press("Tab"); - for (let i = 0; i < times; i++) { - await page.keyboard.press("ArrowRight"); - } - await expect(textEditorToolbar(page, buttonType)).toBeFocused(); - await page.keyboard.press("Space"); + const numberListButton = page.getByRole("button", { + name: "number-list", + }); + await numberListButton.press(key); + + const textInput = page.getByRole("textbox"); await expect(textInput).toBeFocused(); - await page.keyboard.press("Tab"); - for (let i = 0; i < times; i++) { - await page.keyboard.press("ArrowRight"); - } - await expect(textEditorToolbar(page, buttonType)).toBeFocused(); - await page.keyboard.press("Space"); + }), + ); + + ["Space", "Enter"].forEach((key) => + test(`focuses text input when bold button is deselected using the ${key} key`, async ({ + page, + mount, + }) => { + await mount(<TextEditorCustom />); + + const boldButton = page.getByRole("button", { name: "bold" }); + await boldButton.click(); + + await boldButton.press(key); + + const textInput = page.getByRole("textbox"); await expect(textInput).toBeFocused(); - }); - }); + }), + ); - buttonNames.forEach((buttonType) => { - test(`should focus the input when ${buttonType} is selected/deselected via user clicking`, async ({ + ["Space", "Enter"].forEach((key) => + test(`focuses text input when italic button is deselected using the ${key} key`, async ({ + page, mount, + }) => { + await mount(<TextEditorCustom />); + + const italicButton = page.getByRole("button", { name: "italic" }); + await italicButton.click(); + + await italicButton.press(key); + + const textInput = page.getByRole("textbox"); + await expect(textInput).toBeFocused(); + }), + ); + + ["Space", "Enter"].forEach((key) => + test(`focuses text input when bullet list button is deselected using the ${key} key`, async ({ page, + mount, }) => { await mount(<TextEditorCustom />); - const textInput = textEditorInput(page); - const toolbarButton = textEditorToolbar(page, buttonType); - await toolbarButton.click(); + const bulletListButton = page.getByRole("button", { + name: "bullet-list", + }); + await bulletListButton.click(); + + await bulletListButton.press(key); + + const textInput = page.getByRole("textbox"); await expect(textInput).toBeFocused(); - await toolbarButton.click(); + }), + ); + + ["Space", "Enter"].forEach((key) => + test(`focuses text input when number list button is deselected using the ${key} key`, async ({ + page, + mount, + }) => { + await mount(<TextEditorCustom />); + + const numberListButton = page.getByRole("button", { + name: "number-list", + }); + await numberListButton.click(); + + await numberListButton.press(key); + + const textInput = page.getByRole("textbox"); await expect(textInput).toBeFocused(); - }); - }); + }), + ); test(`should render formatted link`, async ({ mount, page }) => { + const linkText = "https://carbon.sage.com"; await mount(<TextEditorCustom />); - const textInput = textEditorInput(page); - await textInput.clear(); - await textInput.pressSequentially(linkText, { delay: 100 }); + const textInput = page.getByRole("textbox"); + await textInput.fill(linkText); await expect(innerText(page)).toHaveText(linkText); }); @@ -375,12 +332,15 @@ test.describe("Functionality tests", () => { }) => { await mount(<TextEditorCustom characterLimit={100} />); - const textInput = textEditorInput(page); - await textInput.clear(); - await textInput.pressSequentially(longText, { delay: 100 }); + const textInput = page.getByRole("textbox"); + + await textInput.fill("a".repeat(100)); + await textInput.press("a"); - await expect(textEditorCounter(page)).toHaveText("0 characters left"); - await expect(innerText(page)).toHaveText(longTextAssert); + await expect(textInput).not.toHaveText("a".repeat(101)); + await expect(page.getByTestId("character-count")).toHaveText( + "0 characters left", + ); }); test(`should focus the first button when focus is moved to the input from the toolbar and tab key pressed`, async ({ @@ -461,20 +421,19 @@ test.describe("Functionality tests", () => { ).toBeFocused(); }); - test(`should focus optional buttons when focus is moved outside the component and shift+tab is pressed`, async ({ + test(`should focus save button when focus is moved outside the component and shift+tab is pressed`, async ({ mount, page, }) => { await mount(<TextEditorCustom />); const saveButton = page.getByRole("button").filter({ hasText: "Save" }); - const cancelButton = page.getByRole("button").filter({ hasText: "Cancel" }); - await saveButton.focus(); - await page.keyboard.press("Tab"); + + await saveButton.press("Tab"); + await expect(saveButton).not.toBeFocused(); + await page.keyboard.press("Shift+Tab"); await expect(saveButton).toBeFocused(); - await page.keyboard.press("Shift+Tab"); - await expect(cancelButton).toBeFocused(); }); test(`should verify pressing right-arrow loops on toolbar buttons and does not move focus to optional buttons`, async ({ @@ -1026,7 +985,7 @@ test.describe("Substitute unit tests", () => { }) => { await mount(<TextEditorCustom />); await textEditorToolbar(page, "bullet-list").click(); - await textEditorInput(page).pressSequentially("foo"); + await textEditorInput(page).fill("foo"); await page.keyboard.press("Enter"); await textEditorInput(page).pressSequentially("bar"); await page.keyboard.press("Enter"); @@ -1044,7 +1003,7 @@ test.describe("Substitute unit tests", () => { }) => { await mount(<TextEditorCustom />); await textEditorToolbar(page, "number-list").click(); - await textEditorInput(page).pressSequentially("foo"); + await textEditorInput(page).fill("foo"); await page.keyboard.press("Enter"); await textEditorInput(page).pressSequentially("bar"); await page.keyboard.press("Enter"); @@ -1062,7 +1021,7 @@ test.describe("Substitute unit tests", () => { }) => { await mount(<TextEditorCustom />); await textEditorToolbar(page, "bullet-list").click(); - await textEditorInput(page).pressSequentially("foo"); + await textEditorInput(page).fill("foo"); await page.keyboard.press("Enter"); await textEditorInput(page).pressSequentially("bar"); await page.keyboard.press("Enter"); @@ -1083,7 +1042,7 @@ test.describe("Substitute unit tests", () => { await textEditorToolbar(page, "bold").click(); await textEditorToolbar(page, "italic").click(); await textEditorToolbar(page, "bullet-list").click(); - await textEditorInput(page).pressSequentially("foo"); + await textEditorInput(page).fill("foo"); await page.keyboard.press("Enter"); await textEditorInput(page).pressSequentially("bar"); await page.keyboard.press("Enter"); @@ -1108,7 +1067,7 @@ test.describe("Substitute unit tests", () => { await textEditorToolbar(page, "bold").click(); await textEditorToolbar(page, "italic").click(); await textEditorToolbar(page, "number-list").click(); - await textEditorInput(page).pressSequentially("foo"); + await textEditorInput(page).fill("foo"); await page.keyboard.press("Enter"); await textEditorInput(page).pressSequentially("bar"); await page.keyboard.press("Enter"); diff --git a/src/components/toast/toast.pw.tsx b/src/components/toast/toast.pw.tsx index b35087d795..c4e318d8c3 100644 --- a/src/components/toast/toast.pw.tsx +++ b/src/components/toast/toast.pw.tsx @@ -134,21 +134,13 @@ test.describe("Toast component", () => { }); }); - [ - [1, 1000], - [15, 15000], - [25, 25000], - ].forEach(([timeout, waitTime]) => { - test(`should render with timeout prop set to ${timeout}`, async ({ - mount, - page, - }) => { - await mount(<ToastComponent timeout={timeout} />); + test(`disappears after 1ms, when timeout prop is set to 1`, async ({ + mount, + page, + }) => { + await mount(<ToastComponent timeout={1} />); - await expect(toastComponent(page)).toBeVisible(); - await page.waitForTimeout(waitTime); - await expect(toastComponent(page)).not.toBeVisible(); - }); + await expect(toastComponent(page)).not.toBeVisible(); }); test("should render with targetPortalId prop", async ({ mount, page }) => {