From 69b70624ad815dbce13184edb924a543a29b99aa Mon Sep 17 00:00:00 2001 From: jonniebigodes Date: Thu, 10 Oct 2024 14:07:56 +0100 Subject: [PATCH 1/4] Initial commit --- src/content/snapshotOptions/hoverfocus.md | 249 -------------- src/content/snapshotOptions/hoverfocus.mdx | 379 +++++++++++++++++++++ 2 files changed, 379 insertions(+), 249 deletions(-) delete mode 100644 src/content/snapshotOptions/hoverfocus.md create mode 100644 src/content/snapshotOptions/hoverfocus.mdx diff --git a/src/content/snapshotOptions/hoverfocus.md b/src/content/snapshotOptions/hoverfocus.md deleted file mode 100644 index 74787f99..00000000 --- a/src/content/snapshotOptions/hoverfocus.md +++ /dev/null @@ -1,249 +0,0 @@ ---- -layout: "../../layouts/Layout.astro" -title: Hover and focus states -description: Learn how to capture hover and focus states -sidebar: { order: 5, label: Hover and focus } ---- - -# Hover and focus states - -Components can respond differently based on hover or focus events. Here are a few techniques for capturing the result of these user events Chromatic. - -## Make your stories interactive - -Stories are capable of simulating user interactions via the [`play`](https://storybook.js.org/docs/writing-stories/play-function) function in Storybook 6.4 and above. Interactions allow you to verify how a component responds to user input (e.g., hover, focus, click, type). Chromatic awaits `play` function execution before taking a snapshot. - -## JavaScript-triggered hover states - -If the hover behavior is triggered via JavaScript like tooltips or dropdowns, write a `play` function to simulate it using Storybook's instrumented version of Testing Library. For example: - -```js -// Form.stories.js|jsx - -/* - * Replace the @storybook/test package with the following if you are using a version of Storybook earlier than 8.0: - * import { userEvent, waitFor, within } from "@storybook/testing-library"; - */ -import { userEvent, waitFor, within } from "@storybook/test"; - -import { Form } from "./LoginForm"; - -export default { - component: Form, - title: "Form", -}; - -export const WithHoverState = { - play: async ({ canvasElement }) => { - // Starts querying the component from its root - const canvas = within(canvasElement); - // Looks up the input and fills it. - const emailInput = canvas.getByLabelText("email", { - selector: "input", - }); - await userEvent.type(emailInput, "Example"); - // Looks up the button and interacts with it. - const submitButton = canvas.getByRole("button"); - await userEvent.click(submitButton); - // Triggers the hover state - await waitFor(async () => { - await userEvent.hover(canvas.getByLabelText("Email error")); - }); - }, -}; -``` - -## CSS :hover state - -CSS includes the `:hover` pseudo-class that allow precise styling of an element on cursor hover. This is a "trusted event" for web browsers, meaning it can't be simulated by the `play` function. There are multiple ways you can snapshot this state. - -
- - Use CSS class names - -Add a CSS class name that mirrors the states you're trying to test (e.g., `hover`, `active`): - -```css -/* Component styles */ -MyComponent:hover, -MyComponent.hover { - background: purple; -} - -MyComponent:active, -MyComponent.active { - background: green; -} -``` - -Then write a story that utilizes the class name: - -```js -// MyComponent.stories.js|jsx - -import { MyComponent } from "./MyComponent"; - -export default { - component: MyComponent, - title: "MyComponent", -}; - -export const HoverStatewithClass = { - args: { - ...HoverState.args, - className: "hover", - }, -}; - -export const ActiveStatewithClass = { - args: { - ...ActiveState.args, - className: "active", - }, -}; -``` - -You can also extend this technique using a JS wrapper that [automates adding a class](https://github.com/Workday/canvas-kit/pull/377/files). - -
- -
- - Trigger CSS states via props - -Although not recommended, you can test an element's states by creating a separate "pure" stateless component. Then use it to test the exact configurations you are after via props. For example: - -```js -// MyComponent.js|jsx - -export function MyComponent({ isHovered, isActive, label }) { - return ( - - ); -} - -MyComponent.defaultProps = { - isHovered: false, - isActive: false, - label: "Submit", -}; -``` - -You can write the following story to trigger the props: - -```js -// MyComponent.stories.js|jsx - -import { MyComponent } from "./MyComponent"; - -export default { - component: MyComponent, - title: "MyComponent", -}; - -export const HoverState = { - args: { - isHovered: true, - label: `I'm :hover`, - }, -}; - -export const ActiveState = { - args: { - isActive: true, - label: `I'm :active`, - }, -}; -``` - -
- -
-With Storybook's Pseudo States addon - -For atomic, functional components with CSS pseudo-classes (e.g., `hover`, `active`), try the [Storybook's Pseudo States addon](https://storybook.js.org/addons/storybook-addon-pseudo-states) to test pseudo states. For example: - -```js -// Button.stories.js|jsx - -import { Button } from "./Button"; - -export default { - component: Button, - title: "Button", -}; - -export const WithHoverState = { - args: { - size: "small", - label: "Button", - }, - parameters: { - // Toggles the component hover state via parameter. - pseudo: { hover: true }, - }, -}; -``` - -
- -## Focus - -To simulate how the component responds when an element is focused (i.e., through mouse or keyboard), write a `play` function emulating the behavior. For example: - -```js -// Button.stories.js|jsx - -import { userEvent, within } from "@storybook/testing-library"; - -import { Button } from "./Button"; - -export default { - component: Button, - title: "Button", -}; - -export const WithFocusState = { - play: async ({ canvasElement }) => { - // Starts querying the component from its root - const canvas = within(canvasElement); - - // Looks up the button and interacts with it. - await canvas.getByRole("button").focus(); - }, -}; -``` - ---- - -### Frequently asked questions - -
- - Why are focus states visible in Storybook but not in a snapshot? - -Snapshots can sometimes exclude outline and other focus styles because Chromatic trims each snapshot to the dimensions of the root node of the story. - -To capture those styles, wrap the story in a [decorator](https://storybook.js.org/docs/writing-stories/decorators#component-decorators) that adds slight padding. - -```js -// MyComponent.stories.js|jsx - -import { MyComponent } from "./MyComponent"; - -export default { - component: MyComponent, - title: "Example Story", - decorators: [ - (Story) => ( -
- -
- ), - ], -}; -``` - -
diff --git a/src/content/snapshotOptions/hoverfocus.mdx b/src/content/snapshotOptions/hoverfocus.mdx new file mode 100644 index 00000000..eb0e86f4 --- /dev/null +++ b/src/content/snapshotOptions/hoverfocus.mdx @@ -0,0 +1,379 @@ +--- +layout: "../../layouts/Layout.astro" +title: Hover and focus states +description: Learn how to capture hover and focus states +sidebar: { order: 5, label: Hover and focus } +--- + +import IntegrationSnippets from "../../components/IntegrationSnippets.astro"; + + + +# Hover and focus states + +Components can respond differently based on hover or focus events. Here are a few techniques for capturing the result of these user events Chromatic. + +## Make your stories interactive + +Stories are capable of simulating user interactions via the [`play`](https://storybook.js.org/docs/writing-stories/play-function) function in Storybook 6.4 and above. Interactions allow you to verify how a component responds to user input (e.g., hover, focus, click, type). Chromatic awaits `play` function execution before taking a snapshot. + +## JavaScript-triggered hover states + +If the hover behavior is triggered via JavaScript like tooltips or dropdowns, write a `play` function to simulate it using Storybook's instrumented version of Testing Library. For example: + +```js +// Form.stories.js|jsx + +/* + * Replace the @storybook/test package with the following if you are using a version of Storybook earlier than 8.0: + * import { userEvent, waitFor, within } from "@storybook/testing-library"; + */ +import { userEvent, waitFor, within } from "@storybook/test"; + +import { Form } from "./LoginForm"; + +export default { + component: Form, + title: "Form", +}; + +export const WithHoverState = { + play: async ({ canvasElement }) => { + // Starts querying the component from its root + const canvas = within(canvasElement); + // Looks up the input and fills it. + const emailInput = canvas.getByLabelText("email", { + selector: "input", + }); + await userEvent.type(emailInput, "Example"); + // Looks up the button and interacts with it. + const submitButton = canvas.getByRole("button"); + await userEvent.click(submitButton); + // Triggers the hover state + await waitFor(async () => { + await userEvent.hover(canvas.getByLabelText("Email error")); + }); + }, +}; +``` + +## CSS :hover state + +CSS includes the `:hover` pseudo-class that allow precise styling of an element on cursor hover. This is a "trusted event" for web browsers, meaning it can't be simulated by the `play` function. There are multiple ways you can snapshot this state. + +
+ + Use CSS class names + +Add a CSS class name that mirrors the states you're trying to test (e.g., `hover`, `active`): + +```css +/* Component styles */ +MyComponent:hover, +MyComponent.hover { + background: purple; +} + +MyComponent:active, +MyComponent.active { + background: green; +} +``` + +Then write a story that utilizes the class name: + +```js +// MyComponent.stories.js|jsx + +import { MyComponent } from "./MyComponent"; + +export default { + component: MyComponent, + title: "MyComponent", +}; + +export const HoverStatewithClass = { + args: { + ...HoverState.args, + className: "hover", + }, +}; + +export const ActiveStatewithClass = { + args: { + ...ActiveState.args, + className: "active", + }, +}; +``` + +You can also extend this technique using a JS wrapper that [automates adding a class](https://github.com/Workday/canvas-kit/pull/377/files). + +
+ +
+ + Trigger CSS states via props + +Although not recommended, you can test an element's states by creating a separate "pure" stateless component. Then use it to test the exact configurations you are after via props. For example: + +```js +// MyComponent.js|jsx + +export function MyComponent({ isHovered, isActive, label }) { + return ( + + ); +} + +MyComponent.defaultProps = { + isHovered: false, + isActive: false, + label: "Submit", +}; +``` + +You can write the following story to trigger the props: + +```js +// MyComponent.stories.js|jsx + +import { MyComponent } from "./MyComponent"; + +export default { + component: MyComponent, + title: "MyComponent", +}; + +export const HoverState = { + args: { + isHovered: true, + label: `I'm :hover`, + }, +}; + +export const ActiveState = { + args: { + isActive: true, + label: `I'm :active`, + }, +}; +``` + +
+ +
+With Storybook's Pseudo States addon + +For atomic, functional components with CSS pseudo-classes (e.g., `hover`, `active`), try the [Storybook's Pseudo States addon](https://storybook.js.org/addons/storybook-addon-pseudo-states) to test pseudo states. For example: + +```js +// Button.stories.js|jsx + +import { Button } from "./Button"; + +export default { + component: Button, + title: "Button", +}; + +export const WithHoverState = { + args: { + size: "small", + label: "Button", + }, + parameters: { + // Toggles the component hover state via parameter. + pseudo: { hover: true }, + }, +}; +``` + +
+ + + +## Focusing DOM elements + +Enabling focus states in your UI components is essential for ensuring usability, specifically for users navigating with a keyboard or other assistive technologies to interact with the UI seamlessly. Chromatic enables + +Chromatic enables you to test how your UI components respond when a specific element receives focus, ensuring that they meet the required accessibility standards. + +When testing UIs, verifying how they respond when a specific element receives focus is essential. This is especially important for ensuring your UI is accessible and meets the required standards. + +enables you to test how your UI components respond when a specific element receives focus, ensuring that they meet the required accessibility standards. + +When testing UIs, verifying how they respond when a specific element receives focus is essential. This is especially important for ensuring your UI is accessible and meets the required standards. + + +{/* +To test how your UI components respond when a specific element receives focus, you can use Chromatic to capture the focused element in the snapshot, ensuring that your UI meets the required accessibility standards. + +When testing UIs, verifying how they respond when a specific element receives focus is essential. This is especially important for ensuring your UI is accessible and meets the required standards. + + +By focusing on specific elements, you can test how your UI components respond to the focus state, ensuring that they are accessible and usable for all users. Chromatic enables pinpointing focus states in your UI components, allowing you to test how they respond to focus events and ensuring that they meet the required accessibility standards. + +. By enabling them in your UI, you're allowing users, specifically those navigating with a keyboard or other assistive technologies, to interact with the UI seamlessly and efficiently. + +When testing UIs, verifying how they respond when a specific element receives focus is essential. This is especially important for ensuring your UI is accessible and meets the required standards. By focusing on specific elements, you can test how your UI components respond to the focus state, ensuring that they are accessible and usable for all users. Chromatic enables pinpointing focus states in your UI components, allowing you to test how they respond to focus events and ensuring that they meet the required accessibility standards. */} + +### With Storybook + +If you're working with a UI element that provides a visual response to the user focusing on a specific element, whether with CSS or JavaScript, you can emulate this behavior by adjusting your tests to include a [`play`](https://storybook.js.org/docs/writing-stories/play-function) function that mirrors the user's interaction. For example: + +```ts title="src/components/Auth.stories.ts|tsx" +// Adjust this import to match your framework (e.g., nextjs, vue3-vite) +import type { Meta, StoryObj } from "@storybook/your-framework"; + +/* +* Replace the @storybook/test package with the following if you are using a version of Storybook earlier than 8.0: +* import { userEvent, within } from "@storybook/testing-library"; +* import { expect } from "@storybook/jest"; +*/ +import { expect, userEvent, within } from "@storybook/test"; + +import { LoginForm } from "./LoginForm"; + +const meta: Meta = { + component: LoginForm, + title: "LoginForm", +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type(canvas.getByLabelText("email"), "test@email.com"); + await userEvent.type(canvas.getByLabelText("password"), "KC@2N6^?vsV+)w1t"); + + const SubmitButton = canvas.getByRole("button", { name: "Login" }); + await SubmitButton.focus(); + await expect(SubmitButton).toHaveFocus(); + }, +}; +``` +{/* , you can emulate this behavior by adjusting your story to include a `play` function that mirrors the user's interaction with the component. This function should focus on the element you want to test. + + +If you're running tests with Storybook, you can emulate the user focusing on a specific element by adjusting your tests to include a [`play`]() function that mirrors the user's interaction with the component wh + +you can emulate the user focusing on a specific element by adjusting your story's to include an interaction test that */} + + +### With Playwright or Cypress + +To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can use the built-in methods and verify how the UI responds when a specific element receives focus (e.g., buttons, inputs). When Chromatic runs your tests, it snapshots the focused element, enabling you to test JavaScript-based interactions that trigger focus events in your UI elements. + +{/* +This allows you to test JavaScript-based interactions that trigger focus events. When Chromatic runs your tests, it will capture the focused element in the snapshot. + + +To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can use the built-in `focus` methods and verify how the UI responds when a specific element receives focus (e.g., buttons, input fields). This allows you to test JavaScript-based interactions that trigger focus events. When Chromatic runs your tests, it will capture the focused element in the snapshot. + + + +To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can use the built-in `focus` methods and verify how the UI responds when a specific element receives focus (e.g., buttons, input fields). This allows you to test JavaScript interactions that trigger focus events. When Chromatic runs your tests, it will capture the focused element in the snapshot. */} + +{/* to interact with UI elements and test JavaScript based interactions that trigger focus events. + + + simulating how the UI responds when focused. + + UI. When Chromatic runs your tests, it will + +, enabling you to test JavaScript interactions that trigger focus events. + +This method triggers the focus event on the element, allowing you to test how your component responds to the focus state. + + +Caveat about css */} + + +{/* To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can you use the built-in `focus` methods and verify how the UI responds when a specific element recieves focus (e.g., buttons, input fields). When Chromatic runs your tests it will capture the focused element in the snapshot, enabling you to test J + + +To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can you use the built-in `focus` methods and verify how the UI responds when a specific element recieves focus (e.g., buttons, input fields), alowing you to test JavaScript interactions that trigger focus events. When Chromatic runs your tests it will capture the focused element in the snapshot. + +and verify how the UI responds when a specific element recieves focus (e.g., buttons, input fields). When Chromatic runs your tests it will capture the focused element in the snapshot, enabling you to test J + +To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can use the Playwright's `focus` locator or Cypress's `focus` commands to verify how the UI behaves when the element is focused. When Chromatic runs your tests, it will capture the focused element in the snapshot, enabling you to test JavaScript interactions that trigger focus events. + +To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can use the Playwright's [`focus`]() locator or Cypress's [`focus` command]() to test JavaScript interactions that trigger focus events in your UI elements. When Chromatic runs your tests, it will capture the focused element in the snapshot, enabling you to test JavaScript interactions that trigger focus events. */} + + +{/* prettier-ignore-start */} + + + + ```ts title="tests/Auth.spec.js|ts" + import { test, expect } from "@chromatic-com/playwright"; + + test.describe("Authentication", () => { + test("Verifies the authentication works with keyboard navigation", async ({ page }) => { + await page.goto("/auth"); + + await page.locator('input[name="email"]').fill("test@email.com"); + await page.locator('input[name="password"]').fill("KC@2N6^?vsV+)w1t"); + + await page.getByRole("button", {name: "Login"}).focus(); + await expect(page.getByRole("button")).toBeFocused(); + }); + }); + ``` + + + ```ts title="cypress/e2e/Auth.cy.js|ts" + describe("Authentication", () => { + it("Verifies the authentication works with keyboard navigation", () => { + cy.visit("/auth"); + + cy.get('input[name="email"]').type("test@email.com"); + cy.get('input[name="password"]').type("KC@2N6^?vsV+)w1t"); + + cy.get('button[type="submit"]').focus(); + cy.get('button[type="submit"]').should('have.focus'); + }); + }); + ``` + + + +{/* prettier-ignore-end */} + +--- + +## Frequently asked questions + +
+Why are focused states visible in Storybook but not captured in a snapshot? + +By default, when Chromatic snapshots a Storybook story, it will trim the snapshot to the dimensions of the root node of the story. However, this behavior can lead to some inconsistencies, such as excluding outlined elements and other focus styles from the snapshot. + +To solve it, you can adjust your story and provide a [decorator](https://storybook.js.org/docs/writing-stories/decorators#component-decorators) introducing some padding to the story enabling it to be snapshotted correctly. + +```ts title="src/components/Login.stories.ts|tsx" +// Adjust this import to match your framework (e.g., nextjs, vue3-vite) +import type { Meta, StoryObj } from "@storybook/your-framework"; + +import { LoginForm } from "./LoginForm"; + +const meta: Meta = { + component: LoginForm, + title: "LoginForm", + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +``` +
\ No newline at end of file From b7aff91bb54151ab9550111f7764a5321698776d Mon Sep 17 00:00:00 2001 From: jonniebigodes Date: Thu, 10 Oct 2024 17:52:24 +0100 Subject: [PATCH 2/4] Adjustments --- src/content/snapshotOptions/hoverfocus.mdx | 78 ++-------------------- 1 file changed, 7 insertions(+), 71 deletions(-) diff --git a/src/content/snapshotOptions/hoverfocus.mdx b/src/content/snapshotOptions/hoverfocus.mdx index eb0e86f4..b44a3ebe 100644 --- a/src/content/snapshotOptions/hoverfocus.mdx +++ b/src/content/snapshotOptions/hoverfocus.mdx @@ -197,32 +197,9 @@ export const WithHoverState = { ## Focusing DOM elements -Enabling focus states in your UI components is essential for ensuring usability, specifically for users navigating with a keyboard or other assistive technologies to interact with the UI seamlessly. Chromatic enables - -Chromatic enables you to test how your UI components respond when a specific element receives focus, ensuring that they meet the required accessibility standards. - -When testing UIs, verifying how they respond when a specific element receives focus is essential. This is especially important for ensuring your UI is accessible and meets the required standards. - -enables you to test how your UI components respond when a specific element receives focus, ensuring that they meet the required accessibility standards. - -When testing UIs, verifying how they respond when a specific element receives focus is essential. This is especially important for ensuring your UI is accessible and meets the required standards. - - -{/* -To test how your UI components respond when a specific element receives focus, you can use Chromatic to capture the focused element in the snapshot, ensuring that your UI meets the required accessibility standards. - -When testing UIs, verifying how they respond when a specific element receives focus is essential. This is especially important for ensuring your UI is accessible and meets the required standards. - - -By focusing on specific elements, you can test how your UI components respond to the focus state, ensuring that they are accessible and usable for all users. Chromatic enables pinpointing focus states in your UI components, allowing you to test how they respond to focus events and ensuring that they meet the required accessibility standards. - -. By enabling them in your UI, you're allowing users, specifically those navigating with a keyboard or other assistive technologies, to interact with the UI seamlessly and efficiently. - -When testing UIs, verifying how they respond when a specific element receives focus is essential. This is especially important for ensuring your UI is accessible and meets the required standards. By focusing on specific elements, you can test how your UI components respond to the focus state, ensuring that they are accessible and usable for all users. Chromatic enables pinpointing focus states in your UI components, allowing you to test how they respond to focus events and ensuring that they meet the required accessibility standards. */} - ### With Storybook -If you're working with a UI element that provides a visual response to the user focusing on a specific element, whether with CSS or JavaScript, you can emulate this behavior by adjusting your tests to include a [`play`](https://storybook.js.org/docs/writing-stories/play-function) function that mirrors the user's interaction. For example: +If you're working with a UI component that provides a visual response to the user focusing on a specific element, whether with CSS or JavaScript, you can simulate this behavior by adjusting your tests to include a [`play`](https://storybook.js.org/docs/writing-stories/play-function) function that mirrors the user's interaction. For example: ```ts title="src/components/Auth.stories.ts|tsx" // Adjust this import to match your framework (e.g., nextjs, vue3-vite) @@ -257,54 +234,10 @@ export const Default: Story = { }, }; ``` -{/* , you can emulate this behavior by adjusting your story to include a `play` function that mirrors the user's interaction with the component. This function should focus on the element you want to test. - - -If you're running tests with Storybook, you can emulate the user focusing on a specific element by adjusting your tests to include a [`play`]() function that mirrors the user's interaction with the component wh - -you can emulate the user focusing on a specific element by adjusting your story's to include an interaction test that */} - ### With Playwright or Cypress -To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can use the built-in methods and verify how the UI responds when a specific element receives focus (e.g., buttons, inputs). When Chromatic runs your tests, it snapshots the focused element, enabling you to test JavaScript-based interactions that trigger focus events in your UI elements. - -{/* -This allows you to test JavaScript-based interactions that trigger focus events. When Chromatic runs your tests, it will capture the focused element in the snapshot. - - -To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can use the built-in `focus` methods and verify how the UI responds when a specific element receives focus (e.g., buttons, input fields). This allows you to test JavaScript-based interactions that trigger focus events. When Chromatic runs your tests, it will capture the focused element in the snapshot. - - - -To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can use the built-in `focus` methods and verify how the UI responds when a specific element receives focus (e.g., buttons, input fields). This allows you to test JavaScript interactions that trigger focus events. When Chromatic runs your tests, it will capture the focused element in the snapshot. */} - -{/* to interact with UI elements and test JavaScript based interactions that trigger focus events. - - - simulating how the UI responds when focused. - - UI. When Chromatic runs your tests, it will - -, enabling you to test JavaScript interactions that trigger focus events. - -This method triggers the focus event on the element, allowing you to test how your component responds to the focus state. - - -Caveat about css */} - - -{/* To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can you use the built-in `focus` methods and verify how the UI responds when a specific element recieves focus (e.g., buttons, input fields). When Chromatic runs your tests it will capture the focused element in the snapshot, enabling you to test J - - -To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can you use the built-in `focus` methods and verify how the UI responds when a specific element recieves focus (e.g., buttons, input fields), alowing you to test JavaScript interactions that trigger focus events. When Chromatic runs your tests it will capture the focused element in the snapshot. - -and verify how the UI responds when a specific element recieves focus (e.g., buttons, input fields). When Chromatic runs your tests it will capture the focused element in the snapshot, enabling you to test J - -To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can use the Playwright's `focus` locator or Cypress's `focus` commands to verify how the UI behaves when the element is focused. When Chromatic runs your tests, it will capture the focused element in the snapshot, enabling you to test JavaScript interactions that trigger focus events. - -To emulate the user focusing on a specific element in your Playwright or Cypress tests, you can use the Playwright's [`focus`]() locator or Cypress's [`focus` command]() to test JavaScript interactions that trigger focus events in your UI elements. When Chromatic runs your tests, it will capture the focused element in the snapshot, enabling you to test JavaScript interactions that trigger focus events. */} - +If you're running tests with Playwright or Cypress, you can simulate JavaScript-based focus events using Playwright's `focus` [locator](https://playwright.dev/docs/next/api/class-locator#locator-focus) or Cypress's [`focus`](https://docs.cypress.io/api/commands/focus) command to verify how the UI responds when a specific element receives focus. For example: {/* prettier-ignore-start */} @@ -350,7 +283,7 @@ To emulate the user focusing on a specific element in your Playwright or Cypress ## Frequently asked questions
-Why are focused states visible in Storybook but not captured in a snapshot? +Why are focus states visible in Storybook but not captured in a snapshot? By default, when Chromatic snapshots a Storybook story, it will trim the snapshot to the dimensions of the root node of the story. However, this behavior can lead to some inconsistencies, such as excluding outlined elements and other focus styles from the snapshot. @@ -376,4 +309,7 @@ const meta: Meta = { export default meta; ``` -
\ No newline at end of file + + + + From a0ed29d9caf2f17acc9f74e9153ada4344cfc4c7 Mon Sep 17 00:00:00 2001 From: jonniebigodes Date: Fri, 11 Oct 2024 15:16:31 +0100 Subject: [PATCH 3/4] Additional polishing --- src/content/snapshotOptions/hoverfocus.mdx | 267 ++++++++++----------- 1 file changed, 123 insertions(+), 144 deletions(-) diff --git a/src/content/snapshotOptions/hoverfocus.mdx b/src/content/snapshotOptions/hoverfocus.mdx index b44a3ebe..8561f596 100644 --- a/src/content/snapshotOptions/hoverfocus.mdx +++ b/src/content/snapshotOptions/hoverfocus.mdx @@ -7,199 +7,178 @@ sidebar: { order: 5, label: Hover and focus } import IntegrationSnippets from "../../components/IntegrationSnippets.astro"; - - # Hover and focus states Components can respond differently based on hover or focus events. Here are a few techniques for capturing the result of these user events Chromatic. -## Make your stories interactive - -Stories are capable of simulating user interactions via the [`play`](https://storybook.js.org/docs/writing-stories/play-function) function in Storybook 6.4 and above. Interactions allow you to verify how a component responds to user input (e.g., hover, focus, click, type). Chromatic awaits `play` function execution before taking a snapshot. - ## JavaScript-triggered hover states -If the hover behavior is triggered via JavaScript like tooltips or dropdowns, write a `play` function to simulate it using Storybook's instrumented version of Testing Library. For example: - -```js -// Form.stories.js|jsx +{/* prettier-ignore-start */} -/* - * Replace the @storybook/test package with the following if you are using a version of Storybook earlier than 8.0: - * import { userEvent, waitFor, within } from "@storybook/testing-library"; - */ -import { userEvent, waitFor, within } from "@storybook/test"; + + + ```ts title="src/components/Auth.stories.ts|tsx" + // Adjust this import to match your framework (e.g., nextjs, vue3-vite) + import type { Meta, StoryObj } from "@storybook/your-framework"; + + /* + * Replace the @storybook/test package with the following if you are using a version of Storybook earlier than 8.0: + * import { userEvent, waitFor, within } from "@storybook/testing-library"; + * import { expect } from "@storybook/jest"; + */ + import { expect, userEvent, waitFor, within } from '@storybook/test' + + import { LoginForm } from "./LoginForm"; + + const meta: Meta = { + component: LoginForm, + title: "LoginForm", + }; + + export default meta; + type Story = StoryObj; + + export const WithInvalidCredentials: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await userEvent.type(canvas.getByLabelText("email"), "test@email.com"); + await userEvent.type(canvas.getByLabelText('password'), 'password'); + + await userEvent.hover(canvas.getByLabelText('password')); + + await waitFor(async () => { + await expect(canvas.getByText('Must be at least 16 characters long')).toBeVisible(); + }; + }, + }; + ``` + + + ```ts title="tests/Auth.spec.js|ts" + import { expect, test } from "@chromatic-com/playwright"; -import { Form } from "./LoginForm"; + test.describe("Authentication", () => { + test("Attempts to authenticate the user with invalid credentials", async ({ page }) => { + await page.goto("/auth"); -export default { - component: Form, - title: "Form", -}; + await page.locator('input[name="email"]').fill('test@email.com'); + await page.locator('input[name="password"]').fill('password'); -export const WithHoverState = { - play: async ({ canvasElement }) => { - // Starts querying the component from its root - const canvas = within(canvasElement); - // Looks up the input and fills it. - const emailInput = canvas.getByLabelText("email", { - selector: "input", - }); - await userEvent.type(emailInput, "Example"); - // Looks up the button and interacts with it. - const submitButton = canvas.getByRole("button"); - await userEvent.click(submitButton); - // Triggers the hover state - await waitFor(async () => { - await userEvent.hover(canvas.getByLabelText("Email error")); + await page.locator('input[name="password"]').hover(); + await expect(page.getByText('Must be at least 16 characters long')).toBeVisible(); + }); }); - }, -}; -``` + ``` + + + ```ts title="cypress/e2e/Auth.cy.js|ts" + describe("Authentication", () => { + it("Attempts to authenticate the user with invalid credentials", () => { + cy.visit("/auth"); -## CSS :hover state + cy.get('input[type="email"]').type('test@email.com'); + cy.get('input[name="password"]').type("password"); -CSS includes the `:hover` pseudo-class that allow precise styling of an element on cursor hover. This is a "trusted event" for web browsers, meaning it can't be simulated by the `play` function. There are multiple ways you can snapshot this state. + cy.get('input[name="password"]').trigger('mouseover'); + cy.get('span').contains('Enter a valid email address').should('be.visible'); -
+ }); + }); + ``` + + - Use CSS class names +{/* prettier-ignore-end */} -Add a CSS class name that mirrors the states you're trying to test (e.g., `hover`, `active`): +## CSS :hover state -```css -/* Component styles */ -MyComponent:hover, -MyComponent.hover { - background: purple; -} +The [`:hover`](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) pseudo-class in CSS allows precise styling on cursor hover. It's considered a "trusted event" for web browsers mainly because it's directly initiated by the user's interaction, making it difficult to simulate programmatically in a testing environment. Listed below are some recommendations for testing this state in Chromatic. -MyComponent:active, -MyComponent.active { - background: green; -} -``` +### With the Pseudo States addon -Then write a story that utilizes the class name: +The [`@storybook/addon-pseudo-states`](https://storybook.js.org/addons/storybook-addon-pseudo-states) addon allows you to toggle between different pseudo-classes (e.g., `hover`, `active`), allowing you to test these states. When enabled, the addon attempts to override the existing styles and apply a custom class to every element that contains any of the available pseudo-classes. To test the element's hover state, adjust your stories to include the `hover` option. For example: -```js -// MyComponent.stories.js|jsx +```ts title="src/components/Auth.stories.ts|tsx" +// Adjust this import to match your framework (e.g., nextjs, vue3-vite) +import type { Meta, StoryObj } from "@storybook/your-framework"; -import { MyComponent } from "./MyComponent"; +import { LoginForm } from "./LoginForm"; -export default { - component: MyComponent, - title: "MyComponent", +const meta: Meta = { + component: LoginForm, + title: "LoginForm", }; -export const HoverStatewithClass = { - args: { - ...HoverState.args, - className: "hover", - }, -}; +export default meta; +type Story = StoryObj; -export const ActiveStatewithClass = { - args: { - ...ActiveState.args, - className: "active", +export const Default: Story = { + parameters: { + pseudo: { + // The hover option can be toggled for selected elements. For more information see the addon's documentation. + hover: true, + }, }, -}; +} ``` -You can also extend this technique using a JS wrapper that [automates adding a class](https://github.com/Workday/canvas-kit/pull/377/files). - -
- -
- - Trigger CSS states via props +### Using CSS class names -Although not recommended, you can test an element's states by creating a separate "pure" stateless component. Then use it to test the exact configurations you are after via props. For example: +If you're working with a component that relies on CSS classes to apply hover styles, you can adjust your component's styles to include class names that mirror the states you're trying to test. -```js -// MyComponent.js|jsx - -export function MyComponent({ isHovered, isActive, label }) { - return ( - - ); +```css title="src/components/Auth.css" +LoginForm:hover, +LoginForm.hover { + background: purple; } -MyComponent.defaultProps = { - isHovered: false, - isActive: false, - label: "Submit", -}; +LoginForm:active, +LoginForm.active { + background: green; +} ``` -You can write the following story to trigger the props: +Then, add a test that toggles the class names in your tests to simulate the hover state. -```js -// MyComponent.stories.js|jsx +```ts title="src/components/Auth.stories.ts|tsx" +// Adjust this import to match your framework (e.g., nextjs, vue3-vite) +import type { Meta, StoryObj } from "@storybook/your-framework"; -import { MyComponent } from "./MyComponent"; +import { LoginForm } from "./LoginForm"; -export default { - component: MyComponent, - title: "MyComponent", +const meta: Meta = { + component: LoginForm, + title: "LoginForm", }; -export const HoverState = { - args: { - isHovered: true, - label: `I'm :hover`, - }, -}; +export default meta; +type Story = StoryObj; -export const ActiveState = { +export const HoverStatewithClass: Story = { args: { - isActive: true, - label: `I'm :active`, + className: "hover", }, }; -``` - -
- -
-With Storybook's Pseudo States addon - -For atomic, functional components with CSS pseudo-classes (e.g., `hover`, `active`), try the [Storybook's Pseudo States addon](https://storybook.js.org/addons/storybook-addon-pseudo-states) to test pseudo states. For example: - -```js -// Button.stories.js|jsx - -import { Button } from "./Button"; -export default { - component: Button, - title: "Button", -}; - -export const WithHoverState = { +export const ActiveStatewithClass: Story = { args: { - size: "small", - label: "Button", - }, - parameters: { - // Toggles the component hover state via parameter. - pseudo: { hover: true }, + className: "active", }, }; ``` -
- - +
+ℹ️ This approach requires manually toggling the class names in your component's test to simulate the necessary states. If you're using a CSS-in-JS framework, you can [automate](https://github.com/Workday/canvas-kit/pull/377/files) this process by creating a JavaScript wrapper that adds the class names programmatically. +
## Focusing DOM elements +Interacting with components often involves focusing on specific elements, such as form fields, buttons, or links. This state is essential in providing visual feedback to the user, primarily when relying on keyboard navigation. Chromatic allows you to verify how components react when a specific element receives focus, ensuring that your UI is accessible and provides a seamless user experience across different devices and browsers. + ### With Storybook -If you're working with a UI component that provides a visual response to the user focusing on a specific element, whether with CSS or JavaScript, you can simulate this behavior by adjusting your tests to include a [`play`](https://storybook.js.org/docs/writing-stories/play-function) function that mirrors the user's interaction. For example: +If you're working with a component that provides a visual response to the user focusing on a specific element, whether with CSS or JavaScript, you can simulate this behavior by adjusting your tests to include a [`play`](https://storybook.js.org/docs/writing-stories/play-function) function that mirrors the user's interaction. For example: ```ts title="src/components/Auth.stories.ts|tsx" // Adjust this import to match your framework (e.g., nextjs, vue3-vite) @@ -237,7 +216,7 @@ export const Default: Story = { ### With Playwright or Cypress -If you're running tests with Playwright or Cypress, you can simulate JavaScript-based focus events using Playwright's `focus` [locator](https://playwright.dev/docs/next/api/class-locator#locator-focus) or Cypress's [`focus`](https://docs.cypress.io/api/commands/focus) command to verify how the UI responds when a specific element receives focus. For example: +If you're running tests with Playwright or Cypress, you can simulate JavaScript-based focus events using Playwright's `focus` [locator](https://playwright.dev/docs/next/api/class-locator#locator-focus) or Cypress's `focus` [command](https://docs.cypress.io/api/commands/focus) to verify how the UI responds when a specific element receives focus. For example: {/* prettier-ignore-start */} @@ -285,9 +264,10 @@ If you're running tests with Playwright or Cypress, you can simulate JavaScript-
Why are focus states visible in Storybook but not captured in a snapshot? -By default, when Chromatic snapshots a Storybook story, it will trim the snapshot to the dimensions of the root node of the story. However, this behavior can lead to some inconsistencies, such as excluding outlined elements and other focus styles from the snapshot. +By default, when Chromatic snapshots a Storybook story, it trims the snapshot to the dimensions of the story's root node. However, this behavior can lead to inconsistencies, such as excluding outlined elements and other focus styles from the snapshot. + +To solve it, you can adjust your story and provide a [decorator](https://storybook.js.org/docs/writing-stories/decorators#component-decorators) that introduces some padding to the story, enabling it to be snapshotted correctly. -To solve it, you can adjust your story and provide a [decorator](https://storybook.js.org/docs/writing-stories/decorators#component-decorators) introducing some padding to the story enabling it to be snapshotted correctly. ```ts title="src/components/Login.stories.ts|tsx" // Adjust this import to match your framework (e.g., nextjs, vue3-vite) @@ -311,5 +291,4 @@ export default meta; ```
- - +{/* TODO add CSS question */} \ No newline at end of file From 311e35361583b7fc53b68f0962241a80161a74cf Mon Sep 17 00:00:00 2001 From: jonniebigodes Date: Tue, 15 Oct 2024 14:51:52 +0100 Subject: [PATCH 4/4] Docs: Hover and focus states updates --- src/content/snapshotOptions/hoverfocus.mdx | 76 +++++++++++----------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/src/content/snapshotOptions/hoverfocus.mdx b/src/content/snapshotOptions/hoverfocus.mdx index 8561f596..f19bd8f4 100644 --- a/src/content/snapshotOptions/hoverfocus.mdx +++ b/src/content/snapshotOptions/hoverfocus.mdx @@ -13,6 +13,8 @@ Components can respond differently based on hover or focus events. Here are a fe ## JavaScript-triggered hover states +If you're working with a component that relies on JavaScript to trigger hover states (e.g., tooltips, dropdowns), you can adjust your tests and include Storybook's [play](https://storybook.js.org/docs/writing-stories/play-function) function or [Playwright's](https://playwright.dev/docs/api/class-locator#locator-hover) and [Cypress's](https://docs.cypress.io/api/commands/hover) APIs to simulate the state and verify the component's behavior. + {/* prettier-ignore-start */} @@ -26,7 +28,7 @@ Components can respond differently based on hover or focus events. Here are a fe * import { userEvent, waitFor, within } from "@storybook/testing-library"; * import { expect } from "@storybook/jest"; */ - import { expect, userEvent, waitFor, within } from '@storybook/test' + import { expect, userEvent, waitFor, within } from "@storybook/test"; import { LoginForm } from "./LoginForm"; @@ -38,17 +40,17 @@ Components can respond differently based on hover or focus events. Here are a fe export default meta; type Story = StoryObj; - export const WithInvalidCredentials: Story = { + export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.type(canvas.getByLabelText("email"), "test@email.com"); - await userEvent.type(canvas.getByLabelText('password'), 'password'); - - await userEvent.hover(canvas.getByLabelText('password')); + + await userEvent.type(canvas.getByLabelText('password'), "password"); + // Triggers the hover state + await userEvent.hover(canvas.getByLabelText("password")); await waitFor(async () => { - await expect(canvas.getByText('Must be at least 16 characters long')).toBeVisible(); + await expect(canvas.getByText("Must be at least 16 characters long")).toBeVisible(); }; }, }; @@ -62,12 +64,12 @@ Components can respond differently based on hover or focus events. Here are a fe test("Attempts to authenticate the user with invalid credentials", async ({ page }) => { await page.goto("/auth"); - await page.locator('input[name="email"]').fill('test@email.com'); - await page.locator('input[name="password"]').fill('password'); + await page.locator('input[name="email"]').fill("test@email.com"); + await page.locator('input[name="password"]').fill("password"); await page.locator('input[name="password"]').hover(); - await expect(page.getByText('Must be at least 16 characters long')).toBeVisible(); - }); + await expect(page.getByText("Must be at least 16 characters long")).toBeVisible(); + }); }); ```
@@ -76,14 +78,12 @@ Components can respond differently based on hover or focus events. Here are a fe describe("Authentication", () => { it("Attempts to authenticate the user with invalid credentials", () => { cy.visit("/auth"); - - cy.get('input[type="email"]').type('test@email.com'); + cy.get('input[type="email"]').type("test@email.com"); cy.get('input[name="password"]').type("password"); - cy.get('input[name="password"]').trigger('mouseover'); - cy.get('span').contains('Enter a valid email address').should('be.visible'); - - }); + cy.get('input[name="password"]').trigger("mouseover"); + cy.get('span').contains("Enter a valid email address").should("be.visible"); + }); }); ``` @@ -93,11 +93,11 @@ Components can respond differently based on hover or focus events. Here are a fe ## CSS :hover state -The [`:hover`](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) pseudo-class in CSS allows precise styling on cursor hover. It's considered a "trusted event" for web browsers mainly because it's directly initiated by the user's interaction, making it difficult to simulate programmatically in a testing environment. Listed below are some recommendations for testing this state in Chromatic. +The [`:hover`](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) pseudo-class in CSS allows precise styling on cursor hover. It's considered a "trusted event" for web browsers mainly because it's directly initiated by the user's interaction, making it difficult to simulate programmatically in a testing environment. Listed below are some recommendations for testing this state with Chromatic. ### With the Pseudo States addon -The [`@storybook/addon-pseudo-states`](https://storybook.js.org/addons/storybook-addon-pseudo-states) addon allows you to toggle between different pseudo-classes (e.g., `hover`, `active`), allowing you to test these states. When enabled, the addon attempts to override the existing styles and apply a custom class to every element that contains any of the available pseudo-classes. To test the element's hover state, adjust your stories to include the `hover` option. For example: +The [`@storybook/addon-pseudo-states`](https://storybook.js.org/addons/storybook-addon-pseudo-states) addon allows you to emulate different pseudo-classes (e.g., `hover`, `active`) by overriding the existing styles and applying a custom class selector to every element that contains any pseudo-classes. You can adjust your Storybook tests and include the `hover` option to test the component's hover state. ```ts title="src/components/Auth.stories.ts|tsx" // Adjust this import to match your framework (e.g., nextjs, vue3-vite) @@ -125,35 +125,35 @@ export const Default: Story = { ### Using CSS class names -If you're working with a component that relies on CSS classes to apply hover styles, you can adjust your component's styles to include class names that mirror the states you're trying to test. +If you're working with a component that relies on CSS classes to apply hover styles, you can adjust the component's styles to include class names that mirror the states you're trying to test. To do so, change your CSS file to include the required class names as follows: -```css title="src/components/Auth.css" -LoginForm:hover, -LoginForm.hover { +```css title="src/components/MyComponent.css" +MyComponent:hover, +MyComponent.hover { background: purple; } -LoginForm:active, -LoginForm.active { +MyComponent:active, +MyComponent.active { background: green; } ``` -Then, add a test that toggles the class names in your tests to simulate the hover state. +Then, add a test that toggles the class name to simulate the hover state. -```ts title="src/components/Auth.stories.ts|tsx" +```ts title="src/components/MyComponent.stories.ts|tsx" // Adjust this import to match your framework (e.g., nextjs, vue3-vite) import type { Meta, StoryObj } from "@storybook/your-framework"; -import { LoginForm } from "./LoginForm"; +import { MyComponent } from "./MyComponent"; -const meta: Meta = { - component: LoginForm, - title: "LoginForm", +const meta: Meta = { + component: MyComponent, + title: "MyComponent", }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const HoverStatewithClass: Story = { args: { @@ -248,7 +248,7 @@ If you're running tests with Playwright or Cypress, you can simulate JavaScript- cy.get('input[name="password"]').type("KC@2N6^?vsV+)w1t"); cy.get('button[type="submit"]').focus(); - cy.get('button[type="submit"]').should('have.focus'); + cy.get('button[type="submit"]').should("have.focus"); }); }); ``` @@ -276,9 +276,9 @@ import type { Meta, StoryObj } from "@storybook/your-framework"; import { LoginForm } from "./LoginForm"; const meta: Meta = { - component: LoginForm, - title: "LoginForm", - decorators: [ + component: LoginForm, + title: "LoginForm", + decorators: [ (Story) => (
@@ -289,6 +289,4 @@ const meta: Meta = { export default meta; ``` - - -{/* TODO add CSS question */} \ No newline at end of file + \ No newline at end of file