diff --git a/packages/odyssey-storybook/src/axe-util.ts b/packages/odyssey-storybook/src/axe-util.ts index c53dee04bd..dafc977861 100644 --- a/packages/odyssey-storybook/src/axe-util.ts +++ b/packages/odyssey-storybook/src/axe-util.ts @@ -13,11 +13,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import axe from "axe-core"; -export const sleep = (ms = 1000) => new Promise((r) => setTimeout(r, ms)); - export const axeRun = async (interaction = "") => { - await sleep(); - await axe .run({ runOnly: { @@ -34,14 +30,16 @@ export const axeRun = async (interaction = "") => { }) .then((results) => { if (results.violations.length) { - console.log("Accessibility issues found ==> ", results.violations); - throw new Error(`Accessibility issues found ${interaction}`); + throw new Error( + `Accessibility issues found in "${interaction}". ${JSON.stringify( + results.violations, + null, + 2 + )}` + ); } }) .catch((e) => { - console.log( - e instanceof Error ? e.message : "Unknown Error in play-test" - ); throw new Error( e instanceof Error ? e.message : "Unknown Error in play-test" ); diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Autocomplete/Autocomplete.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Autocomplete/Autocomplete.stories.tsx index 585e0e340f..3426585ae7 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Autocomplete/Autocomplete.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Autocomplete/Autocomplete.stories.tsx @@ -13,8 +13,41 @@ import { Autocomplete, AutocompleteProps } from "@okta/odyssey-react-mui"; import { Meta, StoryObj } from "@storybook/react"; +import { userEvent, waitFor, within, screen } from "@storybook/testing-library"; +import { axeRun } from "../../../axe-util"; +import { expect } from "@storybook/jest"; import { MuiThemeDecorator } from "../../../../.storybook/components"; +type StationType = { label: string }; + +const stations: ReadonlyArray = [ + { label: "Anderson Station" }, + { label: "Bara Gaon Complex" }, + { label: "Ceres" }, + { label: "Corley Station" }, + { label: "Deep Transfer Station Three" }, + { label: "Eros" }, + { label: "Free Navy Supply Depot" }, + { label: "Ganymede" }, + { label: "Gewitter Base" }, + { label: "Iapetus Station" }, + { label: "Kelso Station" }, + { label: "Laconian Transfer Station" }, + { label: "Mao Station" }, + { label: "Medina Station" }, + { label: "Nauvoo" }, + { label: "Oshima" }, + { label: "Osiris Station" }, + { label: "Pallas" }, + { label: "Phoebe Station" }, + { label: "Prospero Station" }, + { label: "Shirazi-Ma Complex" }, + { label: "Terryon Lock" }, + { label: "Thoth Station" }, + { label: "Tycho Station" }, + { label: "Vesta" }, +]; + const storybookMeta: Meta = { title: "MUI Components/Forms/Autocomplete", component: Autocomplete, @@ -44,42 +77,13 @@ const storybookMeta: Meta = { args: { label: "Destination", hint: "Select your destination in the Sol system.", + options: stations, }, decorators: [MuiThemeDecorator], }; export default storybookMeta; -type StationType = { label: string }; - -const stations: ReadonlyArray = [ - { label: "Anderson Station" }, - { label: "Bara Gaon Complex" }, - { label: "Ceres" }, - { label: "Corley Station" }, - { label: "Deep Transfer Station Three" }, - { label: "Eros" }, - { label: "Free Navy Supply Depot" }, - { label: "Ganymede" }, - { label: "Gewitter Base" }, - { label: "Iapetus Station" }, - { label: "Kelso Station" }, - { label: "Laconian Transfer Station" }, - { label: "Mao Station" }, - { label: "Medina Station" }, - { label: "Nauvoo" }, - { label: "Oshima" }, - { label: "Osiris Station" }, - { label: "Pallas" }, - { label: "Phoebe Station" }, - { label: "Prospero Station" }, - { label: "Shirazi-Ma Complex" }, - { label: "Terryon Lock" }, - { label: "Thoth Station" }, - { label: "Tycho Station" }, - { label: "Vesta" }, -]; - type AutocompleteType = AutocompleteProps< StationType | undefined, boolean | undefined, @@ -87,13 +91,40 @@ type AutocompleteType = AutocompleteProps< >; export const Default: StoryObj = { - render: function C(props) { - return ; + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + const comboBoxElement = canvas.getByRole("combobox") as HTMLInputElement; + await step("Filter and Select from listbox", async () => { + userEvent.click(comboBoxElement); + const listboxElement = screen.getByRole("listbox"); + expect(listboxElement).toBeVisible(); + }); + await step("Check for 'No options' in the list", async () => { + await axeRun("Autocomplete Default"); + waitFor(() => { + userEvent.type(comboBoxElement, "q"); + const noOptionsElement = screen.getByText("No options"); + expect(noOptionsElement).toBeVisible(); + }); + }); + await step("Check for Filtered item from the list", async () => { + userEvent.clear(comboBoxElement); + userEvent.type(comboBoxElement, "z"); + const listItem = screen.getByRole("listbox").firstChild as HTMLLIElement; + expect(listItem?.textContent).toBe("Shirazi-Ma Complex"); + userEvent.click(listItem); + expect(comboBoxElement.value).toBe("Shirazi-Ma Complex"); + }); + await step("Clear the selected item", async () => { + const clearButton = canvas.getByTitle("Clear"); + userEvent.click(clearButton); + expect(comboBoxElement.value).toBe(""); + userEvent.tab(); + }); }, }; export const Disabled: StoryObj = { - ...Default, args: { isDisabled: true, value: { label: "Tycho Station" }, @@ -101,28 +132,59 @@ export const Disabled: StoryObj = { }; export const IsCustomValueAllowed: StoryObj = { - ...Default, args: { isCustomValueAllowed: true, }, + play: async ({ canvasElement, step }) => { + await step("Enter custom value", async () => { + const canvas = within(canvasElement); + const comboBoxElement = canvas.getByRole("combobox") as HTMLInputElement; + userEvent.click(comboBoxElement); + userEvent.type(comboBoxElement, "qwerty"); + userEvent.tab(); + expect(comboBoxElement.value).toBe("qwerty"); + }); + }, }; export const Loading: StoryObj = { - ...Default, args: { isLoading: true, }, }; export const Multiple: StoryObj = { - ...Default, args: { hasMultipleChoices: true, }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + const comboBoxElement = canvas.getByRole("combobox") as HTMLInputElement; + await step("Check for list box to be visible", async () => { + userEvent.click(comboBoxElement); + const listboxElement = screen.getByRole("listbox"); + expect(listboxElement).toBeVisible(); + }); + await step("Select multiple items", async () => { + userEvent.type(comboBoxElement, "z"); + userEvent.click(screen.getByRole("listbox").firstChild as HTMLLIElement); + userEvent.clear(comboBoxElement); + userEvent.type(comboBoxElement, "w"); + userEvent.click(screen.getByRole("listbox").firstChild as HTMLLIElement); + await axeRun("Autocomplete Multiple"); + }); + await step("Clear the selected items", async () => { + waitFor(() => { + const clearButton = canvas.getByTitle("Clear"); + userEvent.click(clearButton); + expect(comboBoxElement.value).toBe(""); + userEvent.tab(); + }); + }); + }, }; export const MultipleDisabled: StoryObj = { - ...Default, args: { hasMultipleChoices: true, isDisabled: true, @@ -131,7 +193,6 @@ export const MultipleDisabled: StoryObj = { }; export const MultipleReadOnly: StoryObj = { - ...Default, args: { hasMultipleChoices: true, isReadOnly: true, @@ -140,7 +201,6 @@ export const MultipleReadOnly: StoryObj = { }; export const ReadOnly: StoryObj = { - ...Default, args: { isReadOnly: true, value: { label: "Tycho Station" }, diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Banner/Banner.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Banner/Banner.stories.tsx index f8539205a1..7a5be46150 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Banner/Banner.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Banner/Banner.stories.tsx @@ -14,6 +14,9 @@ import { Banner, BannerProps } from "@okta/odyssey-react-mui"; import { Meta, StoryObj } from "@storybook/react"; import { MuiThemeDecorator } from "../../../../.storybook/components"; +import { userEvent, within } from "@storybook/testing-library"; +import { expect, jest } from "@storybook/jest"; +import { axeRun } from "../../../axe-util"; const storybookMeta: Meta = { title: "MUI Components/Alerts/Banner", @@ -83,12 +86,28 @@ export const BannerWithLink: StoryObj = { severity: "error", text: "An unidentified flying object compromised Hangar 18.", }, + play: async ({ canvasElement, step }) => { + await step("check for the link text", async () => { + const canvas = within(canvasElement); + const link = canvas.getByText("View report") as HTMLAnchorElement; + expect(link?.tagName).toBe("A"); + expect(link?.href).toBe(`${link?.baseURI}#anchor`); + }); + }, }; export const DismissibleBanner: StoryObj = { args: { - onClose: function noRefCheck(event) { - event; - }, + onClose: jest.fn(), + }, + play: async ({ args, canvasElement, step }) => { + await step("dismiss the banner on click", async () => { + const canvas = within(canvasElement); + const button = canvas.getByTitle("Close"); + userEvent.click(button); + userEvent.tab(); + expect(args.onClose).toHaveBeenCalled(); + await axeRun("Dismissible Banner"); + }); }, }; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Button/Button.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Button/Button.stories.tsx index f35572fb7f..f1802456f2 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Button/Button.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Button/Button.stories.tsx @@ -10,17 +10,16 @@ * See the License for the specific language governing permissions and limitations under the License. */ -import type { Meta, ReactRenderer, StoryObj } from "@storybook/react"; +import type { Meta, StoryObj } from "@storybook/react"; import { Button, AddIcon } from "@okta/odyssey-react-mui"; import type { ButtonProps } from "@okta/odyssey-react-mui"; import { MuiThemeDecorator } from "../../../../.storybook/components/MuiThemeDecorator"; -import { userEvent, within } from "@storybook/testing-library"; +import { userEvent, waitFor, within } from "@storybook/testing-library"; import { expect } from "@storybook/jest"; import { axeRun } from "../../../axe-util"; -// eslint-disable-next-line import/no-extraneous-dependencies -import { StepFunction } from "@storybook/types"; +import type { PlaywrightProps } from "../storybookTypes"; const storybookMeta: Meta = { title: "MUI Components/Button", @@ -62,34 +61,39 @@ const storybookMeta: Meta = { export default storybookMeta; -const interactWithButton = async ( - args: ButtonProps, - canvasElement: HTMLElement, - step: StepFunction, - action: string -) => { - try { +const interactWithButton = + ({ canvasElement, step }: PlaywrightProps) => + async ({ + args, + actionName, + hoverState, + }: { + args: ButtonProps; + actionName: string; + hoverState: boolean; + }) => { if (args.text) { await step("hover and click", async () => { const canvas = within(canvasElement); const button = canvas.getByText(args.text ?? ""); - await userEvent.tab(); - await userEvent.click(button); - await expect(args.onClick).toHaveBeenCalledTimes(1); - await axeRun(action); + userEvent.tab(); + userEvent.click(button); + expect(args.onClick).toHaveBeenCalledTimes(1); + axeRun(actionName); + if (!hoverState) { + waitFor(() => userEvent.tab()); + } }); } - } catch (e) { - console.log(e instanceof Error && e.message); - throw new Error( - e instanceof Error ? e.message : "Unknown Error in play-test" - ); - } -}; + }; export const ButtonPrimary: StoryObj = { play: async ({ args, canvasElement, step }) => { - interactWithButton(args, canvasElement, step, "Button Primary: Hover"); + interactWithButton({ canvasElement, step })({ + args, + actionName: "Button Primary", + hoverState: false, + }); }, }; @@ -99,7 +103,11 @@ export const ButtonSecondary: StoryObj = { variant: "secondary", }, play: async ({ args, canvasElement, step }) => { - interactWithButton(args, canvasElement, step, "Button Secondary: Hover"); + interactWithButton({ canvasElement, step })({ + args, + actionName: "Button Secondary", + hoverState: false, + }); }, }; @@ -109,7 +117,11 @@ export const ButtonDanger: StoryObj = { variant: "danger", }, play: async ({ args, canvasElement, step }) => { - interactWithButton(args, canvasElement, step, "Button Danger: Hover"); + interactWithButton({ canvasElement, step })({ + args, + actionName: "Button Danger", + hoverState: false, + }); }, }; @@ -119,7 +131,11 @@ export const ButtonFloating: StoryObj = { variant: "floating", }, play: async ({ args, canvasElement, step }) => { - interactWithButton(args, canvasElement, step, "Button Floating: Hover"); + interactWithButton({ canvasElement, step })({ + args, + actionName: "Button Floating", + hoverState: false, + }); }, }; @@ -129,7 +145,11 @@ export const ButtonSmall: StoryObj = { size: "small", }, play: async ({ args, canvasElement, step }) => { - interactWithButton(args, canvasElement, step, "Button Small: Hover"); + interactWithButton({ canvasElement, step })({ + args, + actionName: "Button Small", + hoverState: true, + }); }, }; @@ -137,9 +157,14 @@ export const ButtonMedium: StoryObj = { args: { text: "Add crew", size: "medium", + variant: "secondary", }, play: async ({ args, canvasElement, step }) => { - interactWithButton(args, canvasElement, step, "Button Medium: Hover"); + interactWithButton({ canvasElement, step })({ + args, + actionName: "Button Medium", + hoverState: true, + }); }, }; @@ -147,9 +172,14 @@ export const ButtonLarge: StoryObj = { args: { text: "Add crew", size: "large", + variant: "danger", }, play: async ({ args, canvasElement, step }) => { - interactWithButton(args, canvasElement, step, "Button Large: Hover"); + interactWithButton({ canvasElement, step })({ + args, + actionName: "Button Large", + hoverState: true, + }); }, }; @@ -157,9 +187,14 @@ export const ButtonFullWidth: StoryObj = { args: { text: "Add crew", isFullWidth: true, + variant: "floating", }, play: async ({ args, canvasElement, step }) => { - interactWithButton(args, canvasElement, step, "Button FullWidth: Hover"); + interactWithButton({ canvasElement, step })({ + args, + actionName: "Button Fullwidth", + hoverState: true, + }); }, }; @@ -176,7 +211,11 @@ export const ButtonWithIcon: StoryObj = { startIcon: , }, play: async ({ args, canvasElement, step }) => { - interactWithButton(args, canvasElement, step, "Button With Icon: Hover"); + interactWithButton({ canvasElement, step })({ + args, + actionName: "Button with Icon", + hoverState: false, + }); }, }; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Checkbox/Checkbox.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Checkbox/Checkbox.stories.tsx index cd368b0193..1df8015edb 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Checkbox/Checkbox.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Checkbox/Checkbox.stories.tsx @@ -11,15 +11,12 @@ */ import { Checkbox, CheckboxProps } from "@okta/odyssey-react-mui"; -import { Meta, ReactRenderer, StoryObj } from "@storybook/react"; - +import { Meta, StoryObj } from "@storybook/react"; import { MuiThemeDecorator } from "../../../../.storybook/components"; - import { userEvent, within } from "@storybook/testing-library"; import { expect } from "@storybook/jest"; import { axeRun } from "../../../axe-util"; -// eslint-disable-next-line import/no-extraneous-dependencies -import { StepFunction } from "@storybook/types"; +import type { PlaywrightProps } from "../storybookTypes"; const storybookMeta: Meta = { title: "MUI Components/Forms/Checkbox", @@ -50,26 +47,27 @@ const storybookMeta: Meta = { export default storybookMeta; -const checkTheBox = async ( - canvasElement: HTMLElement, - step: StepFunction, - action: string -) => { - await step("check the box", async () => { - const canvas = within(canvasElement); - const checkBox = canvas.getByRole("checkbox") as HTMLInputElement; - checkBox && (await userEvent.click(checkBox)); - expect(checkBox.checked).toBe(true); - await axeRun(action); - }); -}; +const checkTheBox = + ({ canvasElement, step }: PlaywrightProps) => + async (actionName: string) => { + await step("check the box", async () => { + const canvas = within(canvasElement); + const checkBox = canvas.getByRole("checkbox") as HTMLInputElement; + if (checkBox) { + userEvent.click(checkBox); + } + userEvent.tab(); + expect(checkBox).toBeChecked(); + axeRun(actionName); + }); + }; export const Default: StoryObj = { args: { label: "Enable warp drive recalibration", }, play: async ({ canvasElement, step }) => { - checkTheBox(canvasElement, step, "Checkbox Default"); + checkTheBox({ canvasElement, step })("Checkbox Default"); }, }; @@ -79,6 +77,6 @@ export const Required: StoryObj = { isRequired: true, }, play: async ({ canvasElement, step }) => { - checkTheBox(canvasElement, step, "Checkbox Required"); + checkTheBox({ canvasElement, step })("Checkbox Required"); }, }; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Radio/Radio.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Radio/Radio.stories.tsx index 235885a06e..35f08d6419 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Radio/Radio.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Radio/Radio.stories.tsx @@ -11,15 +11,11 @@ */ import { Radio, RadioProps } from "@okta/odyssey-react-mui"; -import { Meta, ReactRenderer, StoryObj } from "@storybook/react"; - +import { Meta, StoryObj } from "@storybook/react"; import { MuiThemeDecorator } from "../../../../.storybook/components"; - import { userEvent, within } from "@storybook/testing-library"; import { expect } from "@storybook/jest"; import { axeRun } from "../../../axe-util"; -// eslint-disable-next-line import/no-extraneous-dependencies -import { StepFunction } from "@storybook/types"; const storybookMeta: Meta = { title: "MUI Components/Forms/Radio", @@ -50,22 +46,16 @@ const storybookMeta: Meta = { export default storybookMeta; -const selectRadio = async ( - canvasElement: HTMLElement, - step: StepFunction, - action: string -) => { - await step("select the radio button", async () => { - const canvas = within(canvasElement); - const radio = canvas.getByRole("radio") as HTMLInputElement; - radio && (await userEvent.click(radio)); - expect(radio.checked).toBe(true); - await axeRun(action); - }); -}; - export const Default: StoryObj = { play: async ({ canvasElement, step }) => { - selectRadio(canvasElement, step, "Radio Default"); + await step("select the radio button", async () => { + const canvas = within(canvasElement); + const radio = canvas.getByRole("radio") as HTMLInputElement; + if (radio) { + userEvent.click(radio); + } + expect(radio).toBeChecked(); + axeRun("Radio Default"); + }); }, }; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Tabs/Tabs.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Tabs/Tabs.stories.tsx index 48aa2f7882..3382c1410f 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Tabs/Tabs.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Tabs/Tabs.stories.tsx @@ -11,6 +11,7 @@ */ import { Meta, StoryObj } from "@storybook/react"; + import { FavoriteIcon, TabItemProps, @@ -18,6 +19,11 @@ import { Tabs, } from "@okta/odyssey-react-mui"; import { MuiThemeDecorator } from "../../../../.storybook/components"; +import { userEvent, waitFor, within } from "@storybook/testing-library"; +import { expect } from "@storybook/jest"; +import { axeRun } from "../../../axe-util"; +import { screen } from "@storybook/testing-library"; +import type { PlaywrightProps } from "../storybookTypes"; const storybookMeta: Meta = { title: "MUI Components/Tabs", @@ -43,20 +49,44 @@ const storybookMeta: Meta = { export default storybookMeta; -const DefaultTemplate: StoryObj = { - render: function C(args) { - const tabs: TabItemProps[] = []; +const selectTab = + ({ canvasElement, step }: PlaywrightProps) => + async (actionName: string, tabName: string) => { + await step(`select the ${tabName} tab`, async () => { + await axeRun(actionName); - tabs.push({ - label: "Planets", - value: "planets", - children: "Information about Planets.", - }); - tabs.push({ - label: "Moons", - value: "moons", - children: "Information about Moons.", + waitFor(() => { + const canvas = within(canvasElement); + const tabElement = canvas.getByText(tabName); + userEvent.click(tabElement); + userEvent.tab(); + const tabData = canvas.getByText(`Information about ${tabName}`); + expect(tabData).toBeInTheDocument(); + + if (actionName === "Tab Disabled") { + const disabledTab = canvas.getByText("Disabled Tab"); + userEvent.click(disabledTab); + const tabData = screen.queryByText("Tab is disabled"); + expect(tabData).not.toBeInTheDocument(); + } + }); }); + }; + +const DefaultTemplate: StoryObj = { + render: function C(args) { + const tabs: TabItemProps[] = [ + { + label: "Planets", + value: "planets", + children: "Information about Planets", + }, + { + label: "Moons", + value: "moons", + children: "Information about Moons", + }, + ]; if (args?.label) { tabs.push({ @@ -80,6 +110,9 @@ const ExampleTabContent = ({ label }: { label: string }) => { export const Default: StoryObj = { ...DefaultTemplate, + play: async ({ canvasElement, step }) => { + selectTab({ canvasElement, step })("Tab Default", "Moons"); + }, }; export const Disabled: StoryObj = { @@ -87,6 +120,10 @@ export const Disabled: StoryObj = { args: { isDisabled: true, label: "Disabled Tab", + children: "Tab is disabled", + }, + play: async ({ canvasElement, step }) => { + selectTab({ canvasElement, step })("Tab Disabled", "Moons"); }, }; @@ -97,4 +134,7 @@ export const Icons: StoryObj = { label: "Icon Tab", children: , }, + play: async ({ canvasElement, step }) => { + selectTab({ canvasElement, step })("Tab Icon", "Icon Tab"); + }, }; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Toast/Toast.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Toast/Toast.stories.tsx index ebd03c8312..a4bf4094e0 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Toast/Toast.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Toast/Toast.stories.tsx @@ -10,23 +10,15 @@ * See the License for the specific language governing permissions and limitations under the License. */ -import { Meta, ReactRenderer, StoryObj } from "@storybook/react"; -import { - Button, - ButtonProps, - Toast, - ToastProps, - ToastStack, -} from "@okta/odyssey-react-mui"; +import { Meta, StoryObj } from "@storybook/react"; +import { Button, Toast, ToastProps, ToastStack } from "@okta/odyssey-react-mui"; import { useCallback, useState } from "react"; import { MuiThemeDecorator } from "../../../../.storybook/components"; - import { userEvent, waitFor, within } from "@storybook/testing-library"; import { expect } from "@storybook/jest"; -import { axeRun, sleep } from "../../../axe-util"; -// eslint-disable-next-line import/no-extraneous-dependencies -import { StepFunction } from "@storybook/types"; +import { axeRun } from "../../../axe-util"; +import type { PlaywrightProps } from "../storybookTypes"; const meta: Meta = { title: "MUI Components/Alerts/Toast", @@ -61,48 +53,43 @@ const meta: Meta = { role: "status", linkText: "Info", text: "The mission to Sagittarius A is set for January 7.", + autoHideDuration: 10000, }, decorators: [MuiThemeDecorator], }; export default meta; -const dismissToast = async (args: ToastProps, canvasElement: HTMLElement) => { - try { +const openToast = + ({ canvasElement, step }: PlaywrightProps) => + async (args: ToastProps, actionName: string) => { const canvas = within(canvasElement); - const toast = await canvas.getAllByRole(args.role || "status")[0]; - const dismissToast = - toast && (await toast.querySelector('[aria-label="close"]')); - if (dismissToast) { - dismissToast && (await waitFor(() => userEvent.click(dismissToast))); - toast && (await waitFor(() => expect(toast).not.toBeVisible())); + await step(`open ${actionName}`, async () => { + await waitFor(() => { + const buttonElement = canvas.getByText(`Open ${args.severity} toast`); + userEvent.hover(buttonElement); + userEvent.click(buttonElement); + userEvent.tab(); + }); + axeRun(actionName); + }); + if (args.isDismissable) { + await step("dismiss toast", async () => { + const toastElement = canvas.getAllByRole(args.role || "status")[0]; + if (toastElement) { + const dismissToastButton = toastElement.querySelector( + '[aria-label="close"]' + ); + if (dismissToastButton) { + userEvent.click(dismissToastButton); + waitFor(() => { + expect(toastElement).not.toBeInTheDocument(); + }); + } + } + }); } - } catch (e) { - console.log(e instanceof Error ? e.message : "error"); - } -}; - -const openToast = async ( - args: ToastProps, - canvasElement: HTMLElement, - step: StepFunction, - action: string, - dismissible = false -) => { - await step("open toast, and dismiss", async () => { - const canvas = within(canvasElement); - const button = canvas.getByText(`Open ${args.severity} toast`); - await userEvent.tab(); - await userEvent.click(button); - await sleep(); - await axeRun(action); - - await sleep(); - if (dismissible) { - dismissToast(args, canvasElement); - } - }); -}; + }; const Single: StoryObj = { args: { @@ -144,7 +131,7 @@ export const Info: StoryObj = { severity: "info", }, play: async ({ args, canvasElement, step }) => { - openToast(args, canvasElement, step, "Info Toast"); + openToast({ canvasElement, step })(args, "Info Toast"); }, }; @@ -156,7 +143,7 @@ export const ErrorToast: StoryObj = { severity: "error", }, play: async ({ args, canvasElement, step }) => { - openToast(args, canvasElement, step, "Error Toast"); + openToast({ canvasElement, step })(args, "Error Toast"); }, }; @@ -168,7 +155,7 @@ export const Warning: StoryObj = { severity: "warning", }, play: async ({ args, canvasElement, step }) => { - openToast(args, canvasElement, step, "Warning Toast"); + openToast({ canvasElement, step })(args, "Warning Toast"); }, }; @@ -180,7 +167,7 @@ export const Success: StoryObj = { severity: "success", }, play: async ({ args, canvasElement, step }) => { - openToast(args, canvasElement, step, "Success Toast"); + openToast({ canvasElement, step })(args, "Success Toast"); }, }; @@ -192,7 +179,7 @@ export const Dismissible: StoryObj = { linkUrl: "#", }, play: async ({ args, canvasElement, step }) => { - openToast(args, canvasElement, step, "Dismissible Toast", true); + openToast({ canvasElement, step })(args, "Dismissible Toast"); }, }; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Tooltip/Tooltip.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Tooltip/Tooltip.stories.tsx index d3c07d7ad8..85c0c99601 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Tooltip/Tooltip.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Tooltip/Tooltip.stories.tsx @@ -18,6 +18,9 @@ import { TooltipProps, } from "@okta/odyssey-react-mui"; import { MuiThemeDecorator } from "../../../../.storybook/components"; +import { userEvent, within } from "@storybook/testing-library"; +import { axeRun } from "../../../axe-util"; +import type { PlaywrightProps } from "../storybookTypes"; import TooltipMdx from "./Tooltip.mdx"; @@ -76,6 +79,17 @@ const Template: StoryObj = { }, }; +const showTooltip = + ({ canvasElement, step }: PlaywrightProps) => + async (actionName: string) => { + await step("show the tooltip on hover", async () => { + const canvas = within(canvasElement); + const button = canvas.getByText("Launch"); + userEvent.hover(button); + await axeRun(actionName); + }); + }; + export const Default: StoryObj = { ...Template, args: { @@ -84,6 +98,9 @@ export const Default: StoryObj = { placement: "top", text: "This will begin a 10-second countdown", }, + play: async ({ canvasElement, step }) => { + showTooltip({ canvasElement, step })("Tooltip Default"); + }, }; export const Icon: StoryObj = { diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/storybookTypes.ts b/packages/odyssey-storybook/src/components/odyssey-mui/storybookTypes.ts new file mode 100644 index 0000000000..4bbd3f104d --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-mui/storybookTypes.ts @@ -0,0 +1,19 @@ +/*! + * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import type { ReactRenderer } from "@storybook/react"; +import type { PlayFunctionContext } from "@storybook/types"; + +export type PlaywrightProps = Pick< + PlayFunctionContext, + "canvasElement" | "step" +>;