From af30f81ab3f21f8cc46983474abff52ec71eb5a1 Mon Sep 17 00:00:00 2001 From: Stephen O'Gorman Date: Mon, 2 Oct 2023 16:04:16 +0100 Subject: [PATCH 1/3] test(dialog-fullscreen): playwright refactor --- .../dialog-full-screen.cy.tsx | 427 -------------- cypress/locators/dialog-full-screen/index.js | 12 - .../locators/dialog-full-screen/locators.js | 5 - playwright/components/index.ts | 9 + playwright/support/helper.ts | 20 + .../dialog-full-screen/components.test-pw.tsx | 506 ++++++++++++++++ .../dialog-full-screen-test.stories.tsx | 196 +------ .../dialog-full-screen.pw.tsx | 550 ++++++++++++++++++ 8 files changed, 1086 insertions(+), 639 deletions(-) delete mode 100644 cypress/components/dialog-full-screen/dialog-full-screen.cy.tsx delete mode 100644 cypress/locators/dialog-full-screen/index.js delete mode 100644 cypress/locators/dialog-full-screen/locators.js create mode 100644 src/components/dialog-full-screen/components.test-pw.tsx create mode 100644 src/components/dialog-full-screen/dialog-full-screen.pw.tsx diff --git a/cypress/components/dialog-full-screen/dialog-full-screen.cy.tsx b/cypress/components/dialog-full-screen/dialog-full-screen.cy.tsx deleted file mode 100644 index 2b409e2a85..0000000000 --- a/cypress/components/dialog-full-screen/dialog-full-screen.cy.tsx +++ /dev/null @@ -1,427 +0,0 @@ -import React from "react"; -import { DialogFullScreenProps } from "../../../src/components/dialog-full-screen"; -import { - DialogFullScreenComponent, - NestedDialog, - MultipleDialogsInDifferentProviders, - DialogFullScreenWithHeaderChildren, - mainDialogTitle, - nestedDialogTitle, - DialogFullScreenBackgroundScrollTestComponent, - DialogFullScreenBackgroundScrollWithOtherFocusableContainers, -} from "../../../src/components/dialog-full-screen/dialog-full-screen-test.stories"; -import { - Default as DefaultDocsStory, - WithComplexExample, - WithDisableContentPadding, - WithHeaderChildren, - WithHelp, - WithHideableHeaderChildren, - WithBox, - FocusingADifferentFirstElement, - OtherFocusableContainers, -} from "../../../src/components/dialog-full-screen/dialog-full-screen.stories"; -import { - dialogTitle, - dialogSubtitle, - alertDialogPreview as dialogPreview, -} from "../../locators/dialog"; -import { - dialogFullScreenPreview, - dialogFullScreenChildren, -} from "../../locators/dialog-full-screen"; -import { - closeIconButton, - openDialogByName, - portal, - getDataElementByValue, - tooltipPreview, - getComponent, - getElement, -} from "../../locators/index"; -import { buttonDataComponent } from "../../locators/button"; -import CypressMountWithProviders from "../../support/component-helper/cypress-mount"; -import { contentElement } from "../../locators/content/index"; -import { keyCode, continuePressingTABKey } from "../../support/helper"; -import { CHARACTERS } from "../../support/component-helper/constants"; - -const specialCharacters = [CHARACTERS.DIACRITICS, CHARACTERS.SPECIALCHARACTERS]; -const testAria = "cypress_aria"; - -context("Testing DialogFullScreen component", () => { - describe("render DialogFullScreen component and check properties", () => { - it("should close Dialog Full Screen component after click on closeIcon", () => { - CypressMountWithProviders(); - - closeIconButton().should("exist"); - dialogFullScreenPreview().should("exist"); - closeIconButton().click(); - dialogFullScreenPreview().should("not.exist"); - }); - - it.each(specialCharacters)( - "should render DialogFullScreen using %s as title", - (title) => { - CypressMountWithProviders(); - - dialogTitle().should("have.text", title); - } - ); - - it.each(specialCharacters)( - "should render DialogFullScreen using %s as subtitle", - (subtitle) => { - CypressMountWithProviders( - - ); - - dialogSubtitle().should("have.text", subtitle); - } - ); - - it.each(specialCharacters)( - "should render DialogFullScreen component with %s as a children", - (childrenValue) => { - CypressMountWithProviders( - {childrenValue} - ); - - dialogFullScreenChildren().should("have.text", childrenValue); - } - ); - - it("should render DialogFullScreen with disabledEscKey prop and not be closed after clicking Escape button", () => { - CypressMountWithProviders(); - - dialogFullScreenPreview().should("exist"); - dialogFullScreenPreview().trigger("keyup", keyCode("Esc")); - dialogFullScreenPreview().should("exist"); - }); - - it("should close DialogFullScreen after pressing Escape button", () => { - CypressMountWithProviders(); - dialogFullScreenPreview().should("exist"); - dialogFullScreenPreview().trigger("keyup", keyCode("Esc")); - dialogFullScreenPreview().should("not.exist"); - }); - - it("should call the cancel action after closing the DialogFullScreen", () => { - const callback: DialogFullScreenProps["onCancel"] = cy - .stub() - .as("onCancel"); - - CypressMountWithProviders( - - ); - - closeIconButton().click(); - cy.get("@onCancel").should("have.been.calledOnce"); - }); - - it("should allow to close nested DialogFullScreen and then the main DialogFullScreen window", () => { - CypressMountWithProviders(); - - openDialogByName(`Open ${mainDialogTitle}`).click(); - - openDialogByName(`Open ${nestedDialogTitle}`).click(); - - dialogPreview().should("exist"); - - portal().contains(nestedDialogTitle).should("be.visible"); - - dialogFullScreenPreview() - .contains(mainDialogTitle) - .should("not.be.visible"); - - portal().contains(nestedDialogTitle).trigger("keyup", keyCode("Esc")); - - dialogPreview().should("not.exist"); - - dialogFullScreenPreview().contains(mainDialogTitle).should("be.visible"); - - dialogFullScreenPreview().trigger("keyup", keyCode("Esc")); - - dialogFullScreenPreview().should("not.exist"); - }); - - it("should render nested dialogs with the aria-modal property only set on the top one", () => { - CypressMountWithProviders(); - - openDialogByName(`Open ${mainDialogTitle}`).click(); - - getElement("dialog-full-screen") - .should("have.attr", "aria-modal") - .and("eq", "true"); - - openDialogByName(`Open ${nestedDialogTitle}`).click(); - - getElement("dialog-full-screen").should("not.have.attr", "aria-modal"); - - getElement("dialog").should("have.attr", "aria-modal").and("eq", "true"); - }); - - it("should always place focus on the inner dialog when tabbing with nested dialogs after focus is lost", () => { - CypressMountWithProviders(); - - openDialogByName(`Open ${mainDialogTitle}`).click(); - - openDialogByName(`Open ${nestedDialogTitle}`).click(); - - // click on the body in order to lose focus - cy.get("body").click(); - cy.get("body").tab(); - - closeIconButton().eq(1).should("be.focused"); - }); - - it("should render nested dialogs with the aria-modal property only set on the top one, even when the dialogs are wrapped in separate CarbonProviders", () => { - cy.mount(); - - openDialogByName("Open Modal 1").click(); - - getElement("dialog-full-screen") - .should("have.attr", "aria-modal") - .and("eq", "true"); - - openDialogByName("Open Modal 2").click(); - - getElement("dialog-full-screen").should("not.have.attr", "aria-modal"); - - getElement("dialog").should("have.attr", "aria-modal").and("eq", "true"); - }); - - it("should render DialogFullScreen component with aria-describedby", () => { - CypressMountWithProviders( - - ); - - dialogFullScreenPreview() - .should("have.attr", "aria-describedby") - .and("contain", testAria); - }); - - it("should render DialogFullScreen component with aria-label", () => { - CypressMountWithProviders( - - ); - dialogFullScreenPreview() - .should("have.attr", "aria-label") - .and("contain", testAria); - }); - - it("should render DialogFullScreen component with aria-labelledby", () => { - CypressMountWithProviders( - - ); - - dialogFullScreenPreview() - .should("have.attr", "aria-labelledby") - .and("contain", testAria); - }); - - it("should render DialogFullScreen component using focusFirstElement", () => { - CypressMountWithProviders(); - cy.contains("This should be focused first now").should("have.focus"); - }); - - it("should render Dialog component disabling autofocus", () => { - CypressMountWithProviders(); - cy.contains("This should be focused first now").should("not.have.focus"); - }); - - it("should render DialogFullScreen component with help", () => { - CypressMountWithProviders( - - ); - getDataElementByValue("question").trigger("mouseover"); - tooltipPreview().should("have.text", "Some help text"); - }); - - it("should render DialogFullScreen component with role", () => { - CypressMountWithProviders(); - dialogFullScreenPreview() - .should("have.attr", "role") - .and("contain", "dialog"); - }); - - it("should not render close icon when ShowCloseIcon is set to false", () => { - CypressMountWithProviders( - - ); - - closeIconButton().should("not.exist"); - }); - - it("should render close icon when ShowCloseIcon is set to true. When you click the CloseIcon the Dialog is closed", () => { - CypressMountWithProviders(); - - dialogFullScreenPreview().should("exist"); - closeIconButton().click(); - dialogFullScreenPreview().should("not.exist"); - }); - - it("should render Dialog Full Screen with header children", () => { - CypressMountWithProviders(); - - getComponent("pill").eq(0).should("have.text", "A pill"); - - getComponent("pill").eq(1).should("have.text", "Another pill"); - }); - - it("should render Dialog component with content padding disabled", () => { - CypressMountWithProviders( - - ); - contentElement() - .should("have.css", "padding-right", "0px") - .should("have.css", "padding-bottom", "0px"); - }); - - // skip this test for now as FE-6053 has not been fixed yet - it.skip("should render DialogFullScreen component with DisableClose prop", () => { - CypressMountWithProviders(); - closeIconButton().click(); - dialogFullScreenPreview().should("exist"); - }); - }); - - describe("should check accessibility for Dialog Full Screen", () => { - it("should check accessibility for default Dialog Full Screen component", () => { - CypressMountWithProviders(); - - buttonDataComponent().contains("Open DialogFullScreen").click(); - closeIconButton().should("be.visible"); - cy.checkAccessibility(); - }); - - it("should check accessibility for default Dialog Full Screen with complex example", () => { - CypressMountWithProviders(); - - buttonDataComponent().contains("Open DialogFullScreen").click(); - closeIconButton().should("be.visible"); - cy.checkAccessibility(); - }); - - it("should check accessibility for default Dialog Full Screen with disabled content padding", () => { - CypressMountWithProviders(); - - buttonDataComponent().contains("Open DialogFullScreen").click(); - closeIconButton().should("be.visible"); - cy.checkAccessibility(); - }); - - it("should check accessibility for default Dialog Full Screen component with header children", () => { - CypressMountWithProviders(); - - buttonDataComponent().contains("Open DialogFullScreen").click(); - closeIconButton().should("be.visible"); - cy.checkAccessibility(); - }); - - it("should check accessibility for default Dialog Full Screen component with help", () => { - CypressMountWithProviders(); - - buttonDataComponent().contains("Open DialogFullScreen").click(); - closeIconButton().should("be.visible"); - cy.checkAccessibility(); - }); - - it("should check accessibility for default Dialog Full Screen component with hideable header children", () => { - CypressMountWithProviders(); - - buttonDataComponent().contains("Open DialogFullScreen").click(); - closeIconButton().should("be.visible"); - cy.checkAccessibility(); - }); - - it("should check accessibility for default Dialog Full Screen component with box", () => { - CypressMountWithProviders(); - - buttonDataComponent().contains("Open DialogFullScreen").click(); - closeIconButton().should("be.visible"); - cy.checkAccessibility(); - }); - - it("should check accessibility for default Dialog Full Screen component using autoFocus", () => { - CypressMountWithProviders(); - - buttonDataComponent() - .contains("Open Demo using focusFirstElement") - .click(); - closeIconButton().should("be.visible"); - cy.checkAccessibility(); - closeIconButton().click(); - - buttonDataComponent().contains("Open Demo using autoFocus").click(); - closeIconButton().should("be.visible"); - cy.checkAccessibility(); - }); - - it("should check accessibility for default Dialog Full Screen component with other focusable containers", () => { - CypressMountWithProviders(); - - buttonDataComponent().contains("Open DialogFullScreen").click(); - closeIconButton().should("be.visible"); - cy.checkAccessibility(); - }); - }); - - describe("test background scroll when tabbing", () => { - it("tabbing forward through the dialog and back to the start should not make the background scroll to the bottom", () => { - CypressMountWithProviders( - - ); - - continuePressingTABKey(3); - - closeIconButton().should("be.focused"); - - cy.checkNotInViewport("#bottom-box"); - }); - - it("tabbing backward through the dialog and back to the start should not make the background scroll to the bottom", () => { - CypressMountWithProviders( - - ); - - continuePressingTABKey(2, true); - - closeIconButton().should("be.focused"); - - cy.checkNotInViewport("#bottom-box"); - }); - - it("tabbing forward through the dialog and other focusable containers back to the start should not make the background scroll to the bottom", () => { - CypressMountWithProviders( - - ); - - continuePressingTABKey(6); - - closeIconButton().eq(0).should("be.focused"); - - cy.checkNotInViewport("#bottom-box"); - }); - - it("tabbing backward through the dialog and other focusable containers back to the start should not make the background scroll to the bottom", () => { - CypressMountWithProviders( - - ); - - continuePressingTABKey(7, true); - - closeIconButton().eq(0).should("be.focused"); - - cy.checkNotInViewport("#bottom-box"); - }); - }); -}); diff --git a/cypress/locators/dialog-full-screen/index.js b/cypress/locators/dialog-full-screen/index.js deleted file mode 100644 index 24c62e1ff3..0000000000 --- a/cypress/locators/dialog-full-screen/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import { - DIALOG_FULL_SCREEN_CHILDREN, - DIALOG_FULL_SCREEN, - DIALOG_FULL_SCREEN_CLOSE_STATE, -} from "./locators"; - -// component preview locators -export const dialogFullScreenChildren = () => - cy.get(DIALOG_FULL_SCREEN_CHILDREN).eq(0); -export const dialogFullScreenPreview = () => cy.get(DIALOG_FULL_SCREEN); -export const dialogFullScreenPreviewClosedState = () => - cy.get(DIALOG_FULL_SCREEN_CLOSE_STATE); diff --git a/cypress/locators/dialog-full-screen/locators.js b/cypress/locators/dialog-full-screen/locators.js deleted file mode 100644 index 1e29e18766..0000000000 --- a/cypress/locators/dialog-full-screen/locators.js +++ /dev/null @@ -1,5 +0,0 @@ -// component preview locators -export const DIALOG_FULL_SCREEN_CHILDREN = '[data-component="form"]'; -export const DIALOG_FULL_SCREEN = 'div[data-element="dialog-full-screen"]'; -export const DIALOG_FULL_SCREEN_CLOSE_STATE = - 'div[data-component="dialog-full-screen"][data-state="closed"]'; diff --git a/playwright/components/index.ts b/playwright/components/index.ts index bd57a4605e..d98765264b 100644 --- a/playwright/components/index.ts +++ b/playwright/components/index.ts @@ -10,6 +10,7 @@ import { LABEL, STICKY_FOOTER, COMMMON_DATA_ELEMENT_INPUT, + PORTAL, } from "./locators"; export const icon = (page: Page) => { @@ -67,3 +68,11 @@ export const label = (page: Page) => { export const legendSpan = (page: Page) => { return page.locator("legend > span"); }; + +export const openDialogByName = (page: Page, name: string) => { + getDataElementByValue(page, "main-text").filter({ hasText: name }); +}; + +export const portal = (page: Page) => { + return page.locator(PORTAL).nth(1).locator("h1"); +}; diff --git a/playwright/support/helper.ts b/playwright/support/helper.ts index a26655bb1a..f33aa191f1 100644 --- a/playwright/support/helper.ts +++ b/playwright/support/helper.ts @@ -330,3 +330,23 @@ export const getDesignTokensByCssProperty = async ( } return tokens; }; + +export const continuePressingTAB = async (page: Page, count: number) => { + const promises = []; + + for (let i = 0; i < count; i++) { + promises.push(page.keyboard.press(`Tab`)); + } + + await Promise.all(promises); +}; + +export const continuePressingSHIFTTAB = async (page: Page, count: number) => { + const promises = []; + + for (let i = 0; i < count; i++) { + promises.push(page.keyboard.press(`Shift+Tab`)); + } + + await Promise.all(promises); +}; diff --git a/src/components/dialog-full-screen/components.test-pw.tsx b/src/components/dialog-full-screen/components.test-pw.tsx new file mode 100644 index 0000000000..3ee959a64e --- /dev/null +++ b/src/components/dialog-full-screen/components.test-pw.tsx @@ -0,0 +1,506 @@ +import React, { useState, useRef } from "react"; +import DialogFullScreen, { DialogFullScreenProps } from "."; +import Dialog from "../dialog"; +import Button from "../button"; +import Form from "../form"; +import Textbox from "../textbox"; +import Pill from "../pill"; +import Box from "../box"; +import CarbonProvider from "../carbon-provider"; +import Toast from "../toast"; +import { Accordion } from "../accordion"; +import useMediaQuery from "../../hooks/useMediaQuery"; +import Typography from "../typography"; + +const mainDialogTitle = "Main Dialog"; +const nestedDialogTitle = "Nested Dialog"; + +export const DialogFullScreenComponent = ({ + children = "This is an example", + ...props +}: Partial) => { + const [isOpen, setIsOpen] = useState(true); + const ref = useRef(null); + return ( + <> + setIsOpen(false)} + {...props} + > + + + + + + +
{children}
+
+ + ); +}; + +export const NestedDialog = () => { + const [mainDialogOpen, setMainDialogOpen] = React.useState(false); + const [nestedDialogOpen, setNestedDialogOpen] = React.useState(false); + + const handleMainDialogOpen = () => { + setMainDialogOpen(true); + }; + + const handleMainDialogCancel = () => { + setMainDialogOpen(false); + }; + + const handleNestedDialogOpen = () => { + setNestedDialogOpen(true); + }; + + const handleNestedDialogCancel = () => { + setNestedDialogOpen(false); + }; + + return ( + <> + + + + + Nested Dialog Content + + + + ); +}; + +export const MultipleDialogsInDifferentProviders = () => { + const [isModal1Open, setIsModal1Open] = React.useState(false); + const [isModal2Open, setIsModal2Open] = React.useState(false); + return ( + <> + + + + setIsModal1Open(false)} + > + This is Modal 1 + + + + + + + setIsModal2Open(false)}> + This is Modal 2 + + + + + ); +}; + +export const DialogFullScreenWithHeaderChildren = () => { + const [isOpen, setIsOpen] = React.useState(true); + const HeaderChildren = ( +
+ A pill + + Another pill + +
+ ); + return ( + <> + + setIsOpen(false)} + title="An example of a long header" + subtitle="Subtitle" + aria-label="aria-label" + headerChildren={HeaderChildren} + > +
setIsOpen(false)}>Cancel + } + saveButton={ + + } + > +
+ This is an example of a full screen Dialog with a Form as content +
+ + + + + + + +
+ + ); +}; + +export const DialogFullScreenBackgroundScrollTestComponent = () => { + return ( + {}}> + + + + I should not be scrolled into view + + + + ); +}; + +export const DialogFullScreenBackgroundScrollWithOtherFocusableContainers = () => { + const toast1Ref = useRef(null); + const toast2Ref = useRef(null); + return ( +
+ {}} + focusableContainers={[toast1Ref, toast2Ref]} + > + + + + I should not be scrolled into view + + + + {}} ref={toast1Ref} targetPortalId="stacked"> + Toast message 1 + + {}} ref={toast2Ref} targetPortalId="stacked"> + Toast message 2 + +
+ ); +}; + +export const WithHelp = () => { + const [isOpen, setIsOpen] = useState(false); + return ( + <> + + setIsOpen(false)} + title="An example of a long header" + subtitle="Subtitle" + help="Some help text" + > +
setIsOpen(false)}>Cancel + } + saveButton={ + + } + > +
+ This is an example of a full screen Dialog with a Form as content +
+ + + + + + + +
+ + ); +}; + +export const WithHideableHeaderChildren = () => { + const [isOpen, setIsOpen] = useState(false); + const aboveBreakpoint = useMediaQuery("(min-width: 568px)"); + const verticalMargin = aboveBreakpoint ? "26px" : 0; + const HeaderChildrenAboveBreakpoint = ( +
+ A pill + + Another pill + +
+ ); + const HeaderChildrenBelowBreakpoint = ( + + + A pill + + Another pill + + + + ); + return ( + <> + + setIsOpen(false)} + title="An example of a long header" + subtitle="Subtitle" + headerChildren={ + aboveBreakpoint + ? HeaderChildrenAboveBreakpoint + : HeaderChildrenBelowBreakpoint + } + > +
setIsOpen(false)}>Cancel + } + saveButton={ + + } + > +
+ This is an example of a full screen Dialog with a Form as content +
+ + + + + + + +
+ + ); +}; + +export const WithBox = () => { + const [isOpen, setIsOpen] = useState(false); + return ( + <> + + setIsOpen(false)} + title="Title" + subtitle="Subtitle" + > + +
setIsOpen(false)}>Cancel + } + saveButton={ + + } + > +
+ This is an example of a full screen Dialog with a Form as content +
+ + + + + + + +
+
+ + ); +}; + +export const FocusingADifferentFirstElement = () => { + const [isOpenOne, setIsOpenOne] = useState(false); + const [isOpenTwo, setIsOpenTwo] = useState(false); + const ref = useRef(null); + return ( + <> + + setIsOpenOne(false)} + title="Title" + subtitle="Subtitle" + > +

Focus an element that doesnt support autofocus

+
+ + +
+ +
+ + setIsOpenTwo(false)} + title="Title" + subtitle="Subtitle" + > +

Focus an element that supports autoFocus

+
+ + +
+ +
+ + ); +}; + +export const OtherFocusableContainers = () => { + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isToast1Open, setIsToast1Open] = useState(false); + const [isToast2Open, setIsToast2Open] = useState(false); + const toast1Ref = useRef(null); + const toast2Ref = useRef(null); + return ( + <> + + setIsDialogOpen(false)} + title="Title" + subtitle="Subtitle" + focusableContainers={[toast1Ref, toast2Ref]} + > +
setIsDialogOpen(false)}>Cancel + } + saveButton={ + + } + > + + This is an example of a dialog with a Form as content + + + + + + + +
+ setIsToast1Open(false)} + ref={toast1Ref} + targetPortalId="stacked" + > + Toast message 1 + + setIsToast2Open(false)} + ref={toast2Ref} + targetPortalId="stacked" + > + Toast message 2 + + + ); +}; + +export const DialogFullScreenWithTitleAsReactComponent = ( + props: Partial +) => { + const TitleComponent = () => ( +
+ Row1 + Row2 +
+ ); + return ( + } + onCancel={() => {}} + {...props} + > + + + ); +}; diff --git a/src/components/dialog-full-screen/dialog-full-screen-test.stories.tsx b/src/components/dialog-full-screen/dialog-full-screen-test.stories.tsx index 4f638d9914..6afe819b3a 100644 --- a/src/components/dialog-full-screen/dialog-full-screen-test.stories.tsx +++ b/src/components/dialog-full-screen/dialog-full-screen-test.stories.tsx @@ -1,14 +1,9 @@ -import React, { useState, useRef } from "react"; +import React, { useState } from "react"; import { action } from "@storybook/addon-actions"; import DialogFullScreen, { DialogFullScreenProps } from "."; import Dialog from "../dialog"; import Button from "../button"; import Form from "../form"; -import Textbox from "../textbox"; -import Pill from "../pill"; -import Box from "../box"; -import CarbonProvider from "../carbon-provider"; -import Toast from "../toast"; export default { title: "Dialog Full Screen/Test", @@ -132,192 +127,3 @@ export const Nested = () => { }; Nested.storyName = "nested"; - -export const mainDialogTitle = "Main Dialog"; -export const nestedDialogTitle = "Nested Dialog"; -export const DialogFullScreenComponent = ({ - children = "This is an example", - ...props -}: Partial) => { - const [isOpen, setIsOpen] = useState(true); - const ref = React.useRef(null); - return ( - <> - setIsOpen(false)} - focusFirstElement={ref} - {...props} - > - - - - - - -
{children}
-
- - ); -}; - -export const NestedDialog = () => { - const [mainDialogOpen, setMainDialogOpen] = React.useState(false); - const [nestedDialogOpen, setNestedDialogOpen] = React.useState(false); - - const handleMainDialogOpen = () => { - setMainDialogOpen(true); - }; - - const handleMainDialogCancel = () => { - setMainDialogOpen(false); - }; - - const handleNestedDialogOpen = () => { - setNestedDialogOpen(true); - }; - - const handleNestedDialogCancel = () => { - setNestedDialogOpen(false); - }; - - return ( - <> - - - - - Nested Dialog Content - - - - ); -}; - -export const MultipleDialogsInDifferentProviders = () => { - const [isModal1Open, setIsModal1Open] = React.useState(false); - const [isModal2Open, setIsModal2Open] = React.useState(false); - return ( - <> - - - - setIsModal1Open(false)} - > - This is Modal 1 - - - - - - - setIsModal2Open(false)}> - This is Modal 2 - - - - - ); -}; - -export const DialogFullScreenWithHeaderChildren = () => { - const [isOpen, setIsOpen] = React.useState(true); - const HeaderChildren = ( -
- A pill - - Another pill - -
- ); - return ( - <> - - setIsOpen(false)} - title="An example of a long header" - subtitle="Subtitle" - headerChildren={HeaderChildren} - > -
setIsOpen(false)}>Cancel - } - saveButton={ - - } - > -
- This is an example of a full screen Dialog with a Form as content -
- - - - - - - -
- - ); -}; - -export const DialogFullScreenBackgroundScrollTestComponent = () => { - return ( - - - I should not be scrolled into view - - {}}> - - - - ); -}; - -export const DialogFullScreenBackgroundScrollWithOtherFocusableContainers = () => { - const toast1Ref = useRef(null); - const toast2Ref = useRef(null); - return ( - - - I should not be scrolled into view - - {}} - focusableContainers={[toast1Ref, toast2Ref]} - > - - - {}} ref={toast1Ref} targetPortalId="stacked"> - Toast message 1 - - {}} ref={toast2Ref} targetPortalId="stacked"> - Toast message 2 - - - ); -}; diff --git a/src/components/dialog-full-screen/dialog-full-screen.pw.tsx b/src/components/dialog-full-screen/dialog-full-screen.pw.tsx new file mode 100644 index 0000000000..b0b117ce24 --- /dev/null +++ b/src/components/dialog-full-screen/dialog-full-screen.pw.tsx @@ -0,0 +1,550 @@ +import React from "react"; +import { expect, test } from "@playwright/experimental-ct-react17"; +import type { Page } from "@playwright/test"; +import { + DialogFullScreenComponent, + NestedDialog, + MultipleDialogsInDifferentProviders, + DialogFullScreenWithHeaderChildren, + DialogFullScreenBackgroundScrollTestComponent, + DialogFullScreenBackgroundScrollWithOtherFocusableContainers, + WithHelp, + WithHideableHeaderChildren, + WithBox, + OtherFocusableContainers, + FocusingADifferentFirstElement, + DialogFullScreenWithTitleAsReactComponent, +} from "./components.test-pw"; +import { + portal, + getDataElementByValue, + tooltipPreview, + getComponent, +} from "../../../playwright/components/index"; +import { + continuePressingTAB, + continuePressingSHIFTTAB, + checkAccessibility, +} from "../../../playwright/support/helper"; +import { CHARACTERS } from "../../../playwright/support/constants"; + +const specialCharacters = [CHARACTERS.DIACRITICS, CHARACTERS.SPECIALCHARACTERS]; +const testAria = "cypress_aria"; +const mainDialogTitle = "Main Dialog"; +const nestedDialogTitle = "Nested Dialog"; + +const iconIsFocused = async (page: Page, whichIcon: number) => { + const closeIcon = getDataElementByValue(page, "close").nth(whichIcon); + await expect(closeIcon).toHaveCSS( + "box-shadow", + "rgb(255, 188, 25) 0px 0px 0px 3px, rgba(0, 0, 0, 0.9) 0px 0px 0px 6px" + ); + await expect(closeIcon).toHaveCSS("outline", "rgba(0, 0, 0, 0) solid 3px"); +}; + +test.describe("render DialogFullScreen component and check properties", () => { + test(`should close component after click on closeIcon`, async ({ + mount, + page, + }) => { + await mount(); + + const closeIcon = getDataElementByValue(page, "close").first(); + const dialogFullScreen = page.getByRole("dialog"); + await expect(closeIcon).toBeAttached; + await expect(dialogFullScreen).toBeAttached; + await closeIcon.click(); + await expect(dialogFullScreen).not.toBeAttached; + }); + + specialCharacters.forEach((title) => { + test(`should render component using ${title} as title`, async ({ + mount, + page, + }) => { + await mount(); + + await expect(getDataElementByValue(page, "title")).toHaveText(title); + }); + }); + + specialCharacters.forEach((subtitle) => { + test(`should render using ${subtitle} as subtitle`, async ({ + mount, + page, + }) => { + await mount( + + ); + + await expect(getDataElementByValue(page, "subtitle")).toHaveText( + subtitle + ); + }); + }); + + specialCharacters.forEach((childrenValue) => { + test(`should render component with ${childrenValue} as a children`, async ({ + mount, + page, + }) => { + await mount( + {childrenValue} + ); + + await expect(getDataElementByValue(page, "form-content")).toContainText( + childrenValue + ); + }); + }); + + test("should render with disabledEscKey prop and not be closed after clicking Escape button", async ({ + mount, + page, + }) => { + await mount(); + + const dialogFullScreen = page.getByRole("dialog"); + await expect(dialogFullScreen).toBeAttached; + await page.keyboard.press("Escape"); + await expect(dialogFullScreen).not.toBeAttached; + }); + + test("should close after pressing Escape button", async ({ mount, page }) => { + await mount(); + + const dialogFullScreen = page.getByRole("dialog"); + await expect(dialogFullScreen).toBeAttached; + await page.keyboard.press("Escape"); + await expect(dialogFullScreen).not.toBeAttached; + }); + + test("should call the cancel action after closing", async ({ + mount, + page, + }) => { + let called = false; + await mount( + { + called = true; + }} + /> + ); + + const closeIcon = getDataElementByValue(page, "close").first(); + await closeIcon.click(); + expect(called).toBe(true); + }); + + test("should allow to close nested DialogFullScreen and then the main DialogFullScreen window", async ({ + mount, + page, + }) => { + await mount(); + + await page + .getByRole("button") + .filter({ hasText: `Open ${mainDialogTitle}` }) + .click(); + await page + .getByRole("button") + .filter({ hasText: `Open ${nestedDialogTitle}` }) + .click(); + + const fullDialog = page.getByRole("dialog"); + const alertDialogPreview = page.getByRole("alertdialog"); + const nestedButton = portal(page); + + await expect(page.getByRole("dialog")).toHaveCount(1); + await expect(nestedButton).toContainText("Nested Dialog"); + await expect(nestedButton).toBeVisible(); + + await page.keyboard.press("Escape"); + await expect(alertDialogPreview).toHaveCount(0); + await expect(fullDialog).toHaveCount(1); + + await page.keyboard.press("Escape"); + await expect(fullDialog).toHaveCount(0); + }); + + test("should render nested dialogs with the aria-modal property only set on the top one", async ({ + mount, + page, + }) => { + await mount(); + + await page + .getByRole("button") + .filter({ hasText: `Open ${mainDialogTitle}` }) + .click(); + + const fullDialog = getDataElementByValue(page, "dialog-full-screen"); + await expect(fullDialog).toHaveAttribute("aria-modal", "true"); + + await page + .getByRole("button") + .filter({ hasText: `Open ${nestedDialogTitle}` }) + .click(); + + await expect(fullDialog).not.toHaveAttribute("aria-modal", "true"); + await expect(page.getByRole("dialog")).toHaveAttribute( + "aria-modal", + "true" + ); + }); + + test("should always place focus on the inner dialog when tabbing with nested dialogs after focus is lost", async ({ + mount, + page, + }) => { + await mount(); + + await page + .getByRole("button") + .filter({ hasText: `Open ${mainDialogTitle}` }) + .click(); + await page + .getByRole("button") + .filter({ hasText: `Open ${nestedDialogTitle}` }) + .click(); + + await page.keyboard.press("Tab"); + const dialogNested = page.getByRole("dialog"); + await dialogNested.click(); + await page.keyboard.press("Tab"); + const closeIcon = getDataElementByValue(page, "close").nth(2); + await expect(closeIcon).toHaveCSS( + "box-shadow", + "rgb(255, 188, 25) 0px 0px 0px 3px, rgba(0, 0, 0, 0.9) 0px 0px 0px 6px" + ); + await expect(closeIcon).toHaveCSS("outline", "rgba(0, 0, 0, 0) solid 3px"); + }); + + test("should render nested dialogs with the aria-modal property only set on the top one, even when the dialogs are wrapped in separate CarbonProviders", async ({ + mount, + page, + }) => { + await mount(); + + await page.getByRole("button").filter({ hasText: `Open Modal 1` }).click(); + const fullDialog = getDataElementByValue(page, "dialog-full-screen"); + await expect(fullDialog).toHaveAttribute("aria-modal", "true"); + + await page.getByRole("button").filter({ hasText: `Open Modal 2` }).click(); + await expect(fullDialog).not.toHaveAttribute("aria-modal", "true"); + await expect(page.getByRole("dialog")).toHaveAttribute( + "aria-modal", + "true" + ); + }); + + test("should render component with aria-describedby", async ({ + mount, + page, + }) => { + await mount(); + + await expect(page.getByRole("dialog")).toHaveAttribute( + "aria-describedby", + testAria + ); + }); + + test("should render component with aria-label", async ({ mount, page }) => { + await mount(); + + await expect(page.getByRole("dialog")).toHaveAttribute( + "aria-label", + testAria + ); + }); + + test("should render component with aria-labelledby", async ({ + mount, + page, + }) => { + await mount( + + ); + + await expect(page.getByRole("dialog")).toHaveAttribute( + "aria-labelledby", + testAria + ); + }); + + test("should render component using focusFirstElement", async ({ + mount, + page, + }) => { + await mount(); + + const focusedButton = page + .getByRole("button") + .filter({ hasText: "This should be focused first now" }); + await expect(focusedButton).toHaveCSS( + "box-shadow", + "rgb(255, 188, 25) 0px 0px 0px 3px, rgba(0, 0, 0, 0.9) 0px 0px 0px 6px" + ); + await expect(focusedButton).toHaveCSS( + "outline", + "rgba(0, 0, 0, 0) solid 3px" + ); + }); + + test("should render component with autofocus disabled", async ({ + mount, + page, + }) => { + await mount(); + + const focusedButton = page + .getByRole("button") + .filter({ hasText: "This should be focused first now" }); + await expect(focusedButton).not.toHaveCSS( + "box-shadow", + "rgb(255, 188, 25) 0px 0px 0px 3px, rgba(0, 0, 0, 0.9) 0px 0px 0px 6px" + ); + const hasFocus = await focusedButton.focus(); + expect(hasFocus).toBeFalsy(); + }); + + test("should render component with help text", async ({ mount, page }) => { + await mount( + + ); + + const helpIcon = getDataElementByValue(page, "question"); + await helpIcon.hover(); + await expect(tooltipPreview(page)).toHaveText("Some help text"); + }); + + test("should render component with role", async ({ mount, page }) => { + await mount(); + + await expect( + getDataElementByValue(page, "dialog-full-screen") + ).toHaveAttribute("role", "dialog"); + }); + + test("should not render close icon when ShowCloseIcon is set to false", async ({ + mount, + page, + }) => { + await mount(); + + await expect(getDataElementByValue(page, "close")).toHaveCount(0); + }); + + test("should render close icon when ShowCloseIcon is set to true. When you click the CloseIcon the Dialog is closed", async ({ + mount, + page, + }) => { + await mount(); + + const fullDialog = getDataElementByValue(page, "dialog-full-screen"); + await expect(fullDialog).toHaveCount(1); + const closeIcon = getDataElementByValue(page, "close").nth(1); + await closeIcon.click(); + await expect(fullDialog).toHaveCount(0); + }); + + test("should render component with header children", async ({ + mount, + page, + }) => { + await mount(); + + await expect(getComponent(page, "pill").nth(0)).toHaveText("A pill"); + await expect(getComponent(page, "pill").nth(1)).toHaveText("Another pill"); + }); + + test("should render component with content padding disabled", async ({ + mount, + page, + }) => { + await mount( + + ); + + const content = getDataElementByValue(page, "content"); + await expect(content).toHaveCSS("padding-right", "0px"); + await expect(content).toHaveCSS("padding-bottom", "0px"); + }); + + // skip this test for now as FE-6053 has not been fixed yet + test.skip("should render component with DisableClose prop", async ({ + mount, + page, + }) => { + await mount(); + + const closeIcon = getDataElementByValue(page, "close").nth(1); + await closeIcon.click(); + await expect(getDataElementByValue(page, "dialog-full-screen")).toHaveCount( + 1 + ); + }); +}); + +test.describe("Accessibility for DialogFullScreen", () => { + test("should check accessibility for default component", async ({ + mount, + page, + }) => { + await mount(); + + await checkAccessibility(page); + }); + + test("should check accessibility with disabled content padding", async ({ + mount, + page, + }) => { + await mount(); + + await checkAccessibility(page); + }); + + test("should check accessibility with header children", async ({ + mount, + page, + }) => { + await mount(); + + await checkAccessibility(page); + }); + + test("should check accessibility with help", async ({ mount, page }) => { + await mount(); + + const openButton = page + .getByRole("button") + .filter({ hasText: "Open DialogFullScreen" }); + await openButton.click(); + await checkAccessibility(page); + }); + + test("should check accessibility with hideable header children", async ({ + mount, + page, + }) => { + await mount(); + + const openButton = page + .getByRole("button") + .filter({ hasText: "Open DialogFullScreen" }); + await openButton.click(); + await checkAccessibility(page); + }); + + test("should check accessibility with box", async ({ mount, page }) => { + await mount(); + + const openButton = page + .getByRole("button") + .filter({ hasText: "Open DialogFullScreen" }); + await openButton.click(); + await checkAccessibility(page); + }); + + test("should check accessibility using autoFocus", async ({ + mount, + page, + }) => { + await mount(); + + const focusFirstButton = page + .getByRole("button") + .filter({ hasText: "Open Demo using focusFirstElement" }); + await focusFirstButton.click(); + await checkAccessibility(page); + + const closeIcon = getDataElementByValue(page, "close").first(); + await closeIcon.click(); + + const autoFocusButton = page + .getByRole("button") + .filter({ hasText: "Open Demo using autoFocus" }); + await autoFocusButton.click(); + await checkAccessibility(page); + }); + + test("should check accessibility with other focusable containers", async ({ + mount, + page, + }) => { + await mount(); + + const openButton = page + .getByRole("button") + .filter({ hasText: "Open DialogFullScreen" }); + await openButton.click(); + await checkAccessibility(page); + }); +}); + +test.describe("test background scroll when tabbing", () => { + test("tabbing forward through the dialog and back to the start should not make the background scroll to the bottom", async ({ + mount, + page, + }) => { + await mount(); + + await continuePressingTAB(page, 3); + await iconIsFocused(page, 0); + await expect(page.getByTestId("#bottom-box")).not.toBeInViewport(); + }); + + test("tabbing backward through the dialog and back to the start should not make the background scroll to the bottom", async ({ + mount, + page, + }) => { + await mount(); + + await page.waitForTimeout(500); + await continuePressingSHIFTTAB(page, 2); + await iconIsFocused(page, 0); + await expect(page.getByTestId("#bottom-box")).not.toBeInViewport(); + }); + + test("tabbing forward through the dialog and other focusable containers back to the start should not make the background scroll to the bottom", async ({ + mount, + page, + }) => { + await mount( + + ); + + await page.waitForTimeout(500); + const toastIcon = getDataElementByValue(page, "close").nth(1); + await toastIcon.focus(); + await continuePressingTAB(page, 5); + await iconIsFocused(page, 0); + await expect(page.getByTestId("#bottom-box")).not.toBeInViewport(); + }); + + test("tabbing backward through the dialog and other focusable containers back to the start should not make the background scroll to the bottom", async ({ + mount, + page, + }) => { + await mount( + + ); + + await page.waitForTimeout(500); + await continuePressingSHIFTTAB(page, 8); + await iconIsFocused(page, 0); + await expect(page.getByTestId("#bottom-box")).not.toBeInViewport(); + }); +}); From c090f2dc5ebda712d14f7d1559b06338cb767375 Mon Sep 17 00:00:00 2001 From: Stephen O'Gorman Date: Fri, 13 Oct 2023 16:13:24 +0100 Subject: [PATCH 2/3] test(playwright): more flaky tests --- .../batch-selection/batch-selection.pw.tsx | 27 +- src/components/button/button.pw.tsx | 60 +- src/components/dialog/dialog.pw.tsx | 590 +++++++++--------- 3 files changed, 336 insertions(+), 341 deletions(-) diff --git a/src/components/batch-selection/batch-selection.pw.tsx b/src/components/batch-selection/batch-selection.pw.tsx index 555d0685fe..04804f4e44 100644 --- a/src/components/batch-selection/batch-selection.pw.tsx +++ b/src/components/batch-selection/batch-selection.pw.tsx @@ -13,6 +13,7 @@ import { batchSelectionComponent, batchSelectionButtonsByPosition, } from "../../../playwright/components/batch-selection"; +import { getComponent } from "../../../playwright/components/index"; import { HooksConfig } from "../../../playwright"; const BATCH_SELECTION_COLOR = [ @@ -39,25 +40,25 @@ test.describe("check BatchSelection component properties", () => { test("should check hidden BatchSelection", async ({ mount, page }) => { await mount(