From 199bb0eb8e27c485de46295dcf0ee25f8cbccf5f Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 19 Mar 2025 13:05:28 -0700 Subject: [PATCH 001/181] Revert "fix(list-box): use `aria-disabled` instead of invalid `disabled` attribute" (#2130) This reverts commit e1b3ef22c9ee09474bacadbb0b22b41326566bab. --- src/ListBox/ListBoxMenuItem.svelte | 2 +- tests/ComboBox/ComboBox.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ListBox/ListBoxMenuItem.svelte b/src/ListBox/ListBoxMenuItem.svelte index 569f0e9e..dd2ff422 100644 --- a/src/ListBox/ListBoxMenuItem.svelte +++ b/src/ListBox/ListBoxMenuItem.svelte @@ -25,7 +25,7 @@ class:bx--list-box__menu-item--active={active} class:bx--list-box__menu-item--highlighted={highlighted || active} aria-selected={active} - aria-disabled={disabled ? true : undefined} + disabled={disabled ? true : undefined} {...$$restProps} on:click on:mouseenter diff --git a/tests/ComboBox/ComboBox.test.ts b/tests/ComboBox/ComboBox.test.ts index 697031a8..2ac7534f 100644 --- a/tests/ComboBox/ComboBox.test.ts +++ b/tests/ComboBox/ComboBox.test.ts @@ -165,7 +165,7 @@ describe("ComboBox", () => { await user.click(screen.getByRole("textbox")); const disabledOption = screen.getByText(/Fax/).closest('[role="option"]'); assert(disabledOption); - expect(disabledOption).toHaveAttribute("aria-disabled", "true"); + expect(disabledOption).toHaveAttribute("disabled", "true"); await user.click(disabledOption); expect(screen.getByRole("textbox")).toHaveValue(""); From 0e082e495098b90b7d44f82b3ffd1fabc2dec858 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 19 Mar 2025 13:06:20 -0700 Subject: [PATCH 002/181] v0.88.3 --- CHANGELOG.md | 2 ++ COMPONENT_INDEX.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 594e1c11..4241b3cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.88.3](https://github.com/carbon-design-system/carbon-components-svelte/compare/v0.88.2...v0.88.3) (2025-03-19) + ### [0.88.2](https://github.com/carbon-design-system/carbon-components-svelte/compare/v0.88.1...v0.88.2) (2025-03-19) ### Bug Fixes diff --git a/COMPONENT_INDEX.md b/COMPONENT_INDEX.md index b2c72de3..9a14e927 100644 --- a/COMPONENT_INDEX.md +++ b/COMPONENT_INDEX.md @@ -1,6 +1,6 @@ # Component Index -> 165 components exported from carbon-components-svelte@0.88.2. +> 165 components exported from carbon-components-svelte@0.88.3. ## Components diff --git a/package-lock.json b/package-lock.json index 3ad4f3f3..bd402490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "carbon-components-svelte", - "version": "0.88.2", + "version": "0.88.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "carbon-components-svelte", - "version": "0.88.2", + "version": "0.88.3", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index ab100279..68599533 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "carbon-components-svelte", - "version": "0.88.2", + "version": "0.88.3", "license": "Apache-2.0", "description": "Svelte implementation of the Carbon Design System", "type": "module", From 49d961bbb501c7e9de2c1c7638c8d240e95669fb Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 19 Mar 2025 13:08:24 -0700 Subject: [PATCH 003/181] chore(changelog): add release notes for v0.88.3 [ci skip] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4241b3cd..12c285f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. See [standa ### [0.88.3](https://github.com/carbon-design-system/carbon-components-svelte/compare/v0.88.2...v0.88.3) (2025-03-19) +### Bug Fixes + +- Revert **list-box:** use `aria-disabled` instead of invalid `disabled` attribute ([#2125](https://github.com/carbon-design-system/carbon-components-svelte/issues/2125)) ([e1b3ef2](https://github.com/carbon-design-system/carbon-components-svelte/commit/e1b3ef22c9ee09474bacadbb0b22b41326566bab)) + ### [0.88.2](https://github.com/carbon-design-system/carbon-components-svelte/compare/v0.88.1...v0.88.2) (2025-03-19) ### Bug Fixes From d67b3e0a844a7df517ef02ce5b7aa37625422d13 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Wed, 19 Mar 2025 13:21:14 -0700 Subject: [PATCH 004/181] docs(code-snippet): remove note on compatibility --- docs/src/pages/components/CodeSnippet.svx | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/src/pages/components/CodeSnippet.svx b/docs/src/pages/components/CodeSnippet.svx index f29d59b1..394becc4 100644 --- a/docs/src/pages/components/CodeSnippet.svx +++ b/docs/src/pages/components/CodeSnippet.svx @@ -29,8 +29,6 @@ let comment = ` This component uses the native, asynchronous [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText) to copy text. -Please note that the `clipboard.writeText` API is not supported in [IE 11 nor Safari iOS version 13.3 or lower](https://caniuse.com/mdn-api_clipboard_writetext). - You can override the default copy functionality with your own implementation. See [Overriding copy functionality](#overriding-copy-functionality). From 7317192e90b1fc6dbc6ca51b16150f49bc55fd2f Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 12:20:46 -0700 Subject: [PATCH 005/181] test(inline-notification): add unit tests --- tests/InlineNotification.test.svelte | 49 ------ .../InlineNotification.close.test.svelte | 12 ++ .../InlineNotification.test.svelte | 29 ++++ .../InlineNotification.test.ts | 156 ++++++++++++++++++ .../InlineNotificationCustom.test.svelte | 14 ++ 5 files changed, 211 insertions(+), 49 deletions(-) delete mode 100644 tests/InlineNotification.test.svelte create mode 100644 tests/InlineNotification/InlineNotification.close.test.svelte create mode 100644 tests/InlineNotification/InlineNotification.test.svelte create mode 100644 tests/InlineNotification/InlineNotification.test.ts create mode 100644 tests/InlineNotification/InlineNotificationCustom.test.svelte diff --git a/tests/InlineNotification.test.svelte b/tests/InlineNotification.test.svelte deleted file mode 100644 index 856bc2b8..00000000 --- a/tests/InlineNotification.test.svelte +++ /dev/null @@ -1,49 +0,0 @@ - - - - - { - console.log(e.detail.timeout); - }} -/> - - -
- Learn more -
-
- -Learn more - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/InlineNotification/InlineNotification.close.test.svelte b/tests/InlineNotification/InlineNotification.close.test.svelte new file mode 100644 index 00000000..6f66810c --- /dev/null +++ b/tests/InlineNotification/InlineNotification.close.test.svelte @@ -0,0 +1,12 @@ + + + { + e.preventDefault(); + console.log("close", e.detail); + }} +/> diff --git a/tests/InlineNotification/InlineNotification.test.svelte b/tests/InlineNotification/InlineNotification.test.svelte new file mode 100644 index 00000000..320683d2 --- /dev/null +++ b/tests/InlineNotification/InlineNotification.test.svelte @@ -0,0 +1,29 @@ + + + { + console.log("close", e.detail); + }} +/> diff --git a/tests/InlineNotification/InlineNotification.test.ts b/tests/InlineNotification/InlineNotification.test.ts new file mode 100644 index 00000000..d7d3b6de --- /dev/null +++ b/tests/InlineNotification/InlineNotification.test.ts @@ -0,0 +1,156 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import InlineNotification from "./InlineNotification.test.svelte"; +import InlineNotificationCustom from "./InlineNotificationCustom.test.svelte"; +import InlineNotificationClose from "./InlineNotification.close.test.svelte"; + +describe("InlineNotification", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("should render with default props", () => { + render(InlineNotification); + + expect(screen.getByRole("alert")).toHaveClass( + "bx--inline-notification--error", + ); + expect(screen.getByText("Error:")).toBeInTheDocument(); + expect( + screen.getByText("An internal server error occurred."), + ).toBeInTheDocument(); + }); + + it("should handle different kinds", () => { + ( + [ + "error", + "info", + "info-square", + "success", + "warning", + "warning-alt", + ] as const + ).forEach((kind) => { + const { container } = render(InlineNotification, { + props: { kind }, + }); + + expect( + container.querySelector(`.bx--inline-notification--${kind}`), + ).toBeInTheDocument(); + container.remove(); + }); + }); + + it("should handle low contrast variant", () => { + render(InlineNotification, { + props: { lowContrast: true }, + }); + + expect(screen.getByRole("alert")).toHaveClass( + "bx--inline-notification--low-contrast", + ); + }); + + it("should handle close button click", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(InlineNotification); + + await user.click(screen.getByRole("button")); + + expect(consoleLog).toHaveBeenCalledWith("close", { timeout: false }); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + }); + + it("should hide close button", () => { + render(InlineNotification, { + props: { hideCloseButton: true }, + }); + + expect( + screen.queryByLabelText("Close notification"), + ).not.toBeInTheDocument(); + expect(screen.getByRole("alert")).toHaveClass( + "bx--inline-notification--hide-close-button", + ); + }); + + it("should handle custom icon descriptions", () => { + render(InlineNotification, { + props: { + statusIconDescription: "Custom status", + closeButtonDescription: "Custom close", + }, + }); + + expect(screen.getByText("Custom status")).toBeInTheDocument(); + expect(screen.getByRole("button")).toHaveAttribute( + "aria-label", + "Custom close", + ); + }); + + it("should handle custom role", () => { + render(InlineNotification, { + props: { role: "status" }, + }); + + expect(screen.getByRole("status")).toBeInTheDocument(); + }); + + it("should handle timeout", async () => { + vi.useFakeTimers(); + const consoleLog = vi.spyOn(console, "log"); + render(InlineNotification, { props: { timeout: 1000 } }); + + expect(screen.getByRole("alert")).toBeInTheDocument(); + await vi.advanceTimersByTimeAsync(1000); + + expect(consoleLog).toHaveBeenCalledWith("close", { timeout: true }); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + vi.useRealTimers(); + }); + + it("should handle custom slots", () => { + render(InlineNotificationCustom); + + const title = screen.getByText("Custom Title:"); + expect(title).not.toHaveClass("bx--inline-notification__title"); + expect(title.tagName).toBe("STRONG"); + + const subtitle = screen.getByText("Custom subtitle content."); + expect(subtitle).not.toHaveClass("bx--inline-notification__subtitle"); + expect(subtitle.tagName).toBe("STRONG"); + }); + + it("should render action button", () => { + render(InlineNotificationCustom); + + expect( + screen.getByRole("button", { name: "Learn more" }), + ).toBeInTheDocument(); + }); + + it("should cleanup timeout on unmount", () => { + vi.useFakeTimers(); + const clearTimeoutSpy = vi.spyOn(window, "clearTimeout"); + + const { unmount } = render(InlineNotification, { + props: { timeout: 1_000 }, + }); + + unmount(); + expect(clearTimeoutSpy).toHaveBeenCalled(); + vi.useRealTimers(); + }); + + it("should prevent default close behavior", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(InlineNotificationClose); + + await user.click(screen.getByRole("button")); + expect(consoleLog).toHaveBeenCalledWith("close", { timeout: false }); + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); +}); diff --git a/tests/InlineNotification/InlineNotificationCustom.test.svelte b/tests/InlineNotification/InlineNotificationCustom.test.svelte new file mode 100644 index 00000000..4186595f --- /dev/null +++ b/tests/InlineNotification/InlineNotificationCustom.test.svelte @@ -0,0 +1,14 @@ + + + + Custom Title: + Custom subtitle content. + + Learn more + + From 7c436bd747c736c9ceac719fac4d094e5c6f4a59 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 12:27:37 -0700 Subject: [PATCH 006/181] test(toast-notification): add unit tests --- tests/ToastNotification.test.svelte | 38 ----- .../ToastNotification.close.test.svelte | 13 ++ .../ToastNotification.test.svelte | 33 ++++ .../ToastNotification.test.ts | 159 ++++++++++++++++++ .../ToastNotificationCustom.test.svelte | 9 + 5 files changed, 214 insertions(+), 38 deletions(-) delete mode 100644 tests/ToastNotification.test.svelte create mode 100644 tests/ToastNotification/ToastNotification.close.test.svelte create mode 100644 tests/ToastNotification/ToastNotification.test.svelte create mode 100644 tests/ToastNotification/ToastNotification.test.ts create mode 100644 tests/ToastNotification/ToastNotificationCustom.test.svelte diff --git a/tests/ToastNotification.test.svelte b/tests/ToastNotification.test.svelte deleted file mode 100644 index 7610e7c9..00000000 --- a/tests/ToastNotification.test.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - - - - { - console.log(e.detail.timeout); - }} -/> - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/ToastNotification/ToastNotification.close.test.svelte b/tests/ToastNotification/ToastNotification.close.test.svelte new file mode 100644 index 00000000..28293b17 --- /dev/null +++ b/tests/ToastNotification/ToastNotification.close.test.svelte @@ -0,0 +1,13 @@ + + + { + e.preventDefault(); + console.log("close", e.detail); + }} +/> diff --git a/tests/ToastNotification/ToastNotification.test.svelte b/tests/ToastNotification/ToastNotification.test.svelte new file mode 100644 index 00000000..068e4cd2 --- /dev/null +++ b/tests/ToastNotification/ToastNotification.test.svelte @@ -0,0 +1,33 @@ + + + { + console.log("close", e.detail); + }} +/> diff --git a/tests/ToastNotification/ToastNotification.test.ts b/tests/ToastNotification/ToastNotification.test.ts new file mode 100644 index 00000000..2fdb8659 --- /dev/null +++ b/tests/ToastNotification/ToastNotification.test.ts @@ -0,0 +1,159 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import ToastNotification from "./ToastNotification.test.svelte"; +import ToastNotificationCustom from "./ToastNotificationCustom.test.svelte"; +import ToastNotificationClose from "./ToastNotification.close.test.svelte"; + +describe("ToastNotification", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("should render with default props", () => { + render(ToastNotification); + + expect(screen.getByRole("alert")).toHaveClass( + "bx--toast-notification--error", + ); + expect(screen.getByText("Error")).toBeInTheDocument(); + expect( + screen.getByText("An internal server error occurred."), + ).toBeInTheDocument(); + expect(screen.getByText("2024-03-21 12:00:00")).toBeInTheDocument(); + }); + + it("should handle different kinds", () => { + const kinds = [ + "error", + "info", + "info-square", + "success", + "warning", + "warning-alt", + ] as const; + + kinds.forEach((kind) => { + const { container } = render(ToastNotification, { + props: { kind }, + }); + + expect( + container.querySelector(`.bx--toast-notification--${kind}`), + ).toBeInTheDocument(); + container.remove(); + }); + }); + + it("should handle low contrast variant", () => { + render(ToastNotification, { + props: { lowContrast: true }, + }); + + expect(screen.getByRole("alert")).toHaveClass( + "bx--toast-notification--low-contrast", + ); + }); + + it("should handle close button click", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(ToastNotification); + + await user.click(screen.getByRole("button")); + + expect(consoleLog).toHaveBeenCalledWith("close", { timeout: false }); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + }); + + it("should hide close button", () => { + render(ToastNotification, { + props: { hideCloseButton: true }, + }); + + expect( + screen.queryByLabelText("Close notification"), + ).not.toBeInTheDocument(); + }); + + it("should handle custom icon descriptions", () => { + render(ToastNotification, { + props: { + statusIconDescription: "Custom status", + closeButtonDescription: "Custom close", + }, + }); + + expect(screen.getByText("Custom status")).toBeInTheDocument(); + expect(screen.getByRole("button")).toHaveAttribute( + "aria-label", + "Custom close", + ); + }); + + it("should handle custom role", () => { + render(ToastNotification, { + props: { role: "status" }, + }); + + expect(screen.getByRole("status")).toBeInTheDocument(); + }); + + it("should handle timeout", async () => { + vi.useFakeTimers(); + const consoleLog = vi.spyOn(console, "log"); + + render(ToastNotification, { props: { timeout: 1000 } }); + + expect(screen.getByRole("alert")).toBeInTheDocument(); + + await vi.advanceTimersByTimeAsync(1000); + + expect(consoleLog).toHaveBeenCalledWith("close", { timeout: true }); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + vi.useRealTimers(); + }); + + it("should handle custom slots", () => { + render(ToastNotificationCustom); + + const title = screen.getByText("Custom Title:"); + expect(title).not.toHaveClass("bx--toast-notification__title"); + expect(title.tagName).toBe("STRONG"); + + const subtitle = screen.getByText("Custom subtitle content."); + expect(subtitle).not.toHaveClass("bx--toast-notification__subtitle"); + expect(subtitle.tagName).toBe("STRONG"); + + const caption = screen.getByText("Custom caption content."); + expect(caption).not.toHaveClass("bx--toast-notification__caption"); + expect(caption.tagName).toBe("STRONG"); + }); + + it("should handle full width", () => { + render(ToastNotification, { props: { fullWidth: true } }); + + const notification = screen.getByRole("alert"); + expect(notification).toHaveStyle({ width: "100%" }); + }); + + it("should cleanup timeout on unmount", () => { + vi.useFakeTimers(); + const clearTimeoutSpy = vi.spyOn(window, "clearTimeout"); + + const { unmount } = render(ToastNotification, { + props: { timeout: 1000 }, + }); + + unmount(); + expect(clearTimeoutSpy).toHaveBeenCalled(); + vi.useRealTimers(); + }); + + it("should prevent default close behavior", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(ToastNotificationClose); + + await user.click(screen.getByRole("button")); + expect(consoleLog).toHaveBeenCalledWith("close", { timeout: false }); + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); +}); diff --git a/tests/ToastNotification/ToastNotificationCustom.test.svelte b/tests/ToastNotification/ToastNotificationCustom.test.svelte new file mode 100644 index 00000000..9b2b2436 --- /dev/null +++ b/tests/ToastNotification/ToastNotificationCustom.test.svelte @@ -0,0 +1,9 @@ + + + + Custom Title: + Custom subtitle content. + Custom caption content. + From e35a25de819bd9db985e966a0a6bd4cb6eceacfb Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 12:35:26 -0700 Subject: [PATCH 007/181] test(text-area): add unit tests --- tests/TextArea.test.svelte | 19 --- tests/TextArea/TextArea.test.svelte | 49 +++++++ tests/TextArea/TextArea.test.ts | 153 ++++++++++++++++++++++ tests/TextArea/TextAreaCustom.test.svelte | 7 + 4 files changed, 209 insertions(+), 19 deletions(-) delete mode 100644 tests/TextArea.test.svelte create mode 100644 tests/TextArea/TextArea.test.svelte create mode 100644 tests/TextArea/TextArea.test.ts create mode 100644 tests/TextArea/TextAreaCustom.test.svelte diff --git a/tests/TextArea.test.svelte b/tests/TextArea.test.svelte deleted file mode 100644 index 104dea23..00000000 --- a/tests/TextArea.test.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - From eb413a1fbac54503f9ab1464e6dc1538bb685b84 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 12:39:51 -0700 Subject: [PATCH 008/181] test(text-input): add unit tests --- tests/TextInput.test.svelte | 45 ----- tests/TextInput/TextInput.test.svelte | 55 ++++++ tests/TextInput/TextInput.test.ts | 198 ++++++++++++++++++++ tests/TextInput/TextInputCustom.test.svelte | 7 + 4 files changed, 260 insertions(+), 45 deletions(-) delete mode 100644 tests/TextInput.test.svelte create mode 100644 tests/TextInput/TextInput.test.svelte create mode 100644 tests/TextInput/TextInput.test.ts create mode 100644 tests/TextInput/TextInputCustom.test.svelte diff --git a/tests/TextInput.test.svelte b/tests/TextInput.test.svelte deleted file mode 100644 index 63b5dd87..00000000 --- a/tests/TextInput.test.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - - console.log(e.detail)} - on:change={(e) => (value = e.detail)} - on:paste={(e) => console.log(e)} -/> - - - - - - - - - - - - - - - - - - - - diff --git a/tests/TextInput/TextInput.test.svelte b/tests/TextInput/TextInput.test.svelte new file mode 100644 index 00000000..5dc88aac --- /dev/null +++ b/tests/TextInput/TextInput.test.svelte @@ -0,0 +1,55 @@ + + + + +
{value}
diff --git a/tests/TextInput/TextInput.test.ts b/tests/TextInput/TextInput.test.ts new file mode 100644 index 00000000..74271739 --- /dev/null +++ b/tests/TextInput/TextInput.test.ts @@ -0,0 +1,198 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import TextInput from "./TextInput.test.svelte"; +import TextInputCustom from "./TextInputCustom.test.svelte"; + +describe("TextInput", () => { + it("should render with default props", () => { + render(TextInput); + + expect(screen.getByLabelText("User name")).toBeInTheDocument(); + }); + + it("should handle placeholder text", () => { + render(TextInput, { + props: { placeholder: "Enter user name..." }, + }); + + expect( + screen.getByPlaceholderText("Enter user name..."), + ).toBeInTheDocument(); + }); + + it("should handle different sizes", () => { + (["sm", "xl"] as const).forEach((size) => { + const { container } = render(TextInput, { + props: { size }, + }); + + const input = container.querySelector("input"); + expect(input).toHaveClass(`bx--text-input--${size}`); + container.remove(); + }); + }); + + it("should handle light variant", () => { + render(TextInput, { props: { light: true } }); + + expect(screen.getByRole("textbox")).toHaveClass("bx--text-input--light"); + }); + + it("should handle disabled state", () => { + render(TextInput, { props: { disabled: true } }); + + const input = screen.getByRole("textbox"); + expect(input).toBeDisabled(); + expect(screen.getByText("User name")).toHaveClass("bx--label--disabled"); + }); + + it("should handle helper text", () => { + render(TextInput, { props: { helperText: "Helper text" } }); + + expect(screen.getByText("Helper text")).toHaveClass( + "bx--form__helper-text", + ); + }); + + it("should handle invalid state", () => { + render(TextInput, { + props: { invalid: true, invalidText: "Invalid input" }, + }); + + const input = screen.getByRole("textbox"); + expect(input).toHaveClass("bx--text-input--invalid"); + expect(input).toHaveAttribute("aria-invalid", "true"); + expect(screen.getByText("Invalid input")).toHaveClass( + "bx--form-requirement", + ); + }); + + it("should handle warning state", () => { + render(TextInput, { + props: { warn: true, warnText: "Warning message" }, + }); + + const input = screen.getByRole("textbox"); + expect(input).toHaveClass("bx--text-input--warning"); + expect(screen.getByText("Warning message")).toHaveClass( + "bx--form-requirement", + ); + }); + + it("should handle hidden label", () => { + render(TextInput, { props: { hideLabel: true } }); + + expect(screen.getByText("User name")).toHaveClass("bx--visually-hidden"); + }); + + it("should handle custom id", () => { + render(TextInput, { props: { id: "custom-id" } }); + + const input = screen.getByRole("textbox"); + expect(input).toHaveAttribute("id", "custom-id"); + expect(screen.getByText("User name")).toHaveAttribute("for", "custom-id"); + }); + + it("should handle custom name", () => { + render(TextInput, { props: { name: "custom-name" } }); + + expect(screen.getByRole("textbox")).toHaveAttribute("name", "custom-name"); + }); + + it("should handle required state", () => { + render(TextInput, { props: { required: true } }); + + expect(screen.getByRole("textbox")).toHaveAttribute("required"); + }); + + it("should handle inline variant", () => { + render(TextInput, { props: { inline: true } }); + + expect(screen.getByText("User name")).toHaveClass("bx--label--inline"); + }); + + it("should handle readonly state", () => { + render(TextInput, { + props: { readonly: true }, + }); + + expect(screen.getByRole("textbox")).toHaveAttribute("readonly"); + }); + + it("should handle custom slots", () => { + render(TextInputCustom); + + expect(screen.getByText("Custom Label Text").tagName).toBe("SPAN"); + }); + + it("should handle value binding", async () => { + render(TextInput); + + const input = screen.getByRole("textbox"); + await user.type(input, "Test value"); + expect(screen.getByTestId("value").textContent).toBe("Test value"); + }); + + it("should handle number type input", async () => { + render(TextInput, { props: { type: "number" } }); + + const input = screen.getByLabelText("User name"); + await user.type(input, "123"); + expect(input).toHaveValue(123); + + await user.clear(input); + expect(input).toHaveValue(null); + }); + + it("should not show helper text when invalid", () => { + render(TextInput, { + props: { + invalid: true, + invalidText: "Invalid input", + helperText: "Helper text", + }, + }); + + expect(screen.queryByText("Helper text")).not.toBeInTheDocument(); + expect(screen.getByText("Invalid input")).toBeInTheDocument(); + }); + + it("should not show helper text when warning", () => { + render(TextInput, { + props: { + warn: true, + warnText: "Warning message", + helperText: "Helper text", + }, + }); + + expect(screen.queryByText("Helper text")).not.toBeInTheDocument(); + expect(screen.getByText("Warning message")).toBeInTheDocument(); + }); + + it("should handle disabled helper text", () => { + render(TextInput, { + props: { + disabled: true, + helperText: "Helper text", + }, + }); + + expect(screen.getByText("Helper text")).toHaveClass( + "bx--form__helper-text--disabled", + ); + }); + + it("should handle inline helper text", () => { + render(TextInput, { + props: { + inline: true, + helperText: "Helper text", + }, + }); + + expect(screen.getByText("Helper text")).toHaveClass( + "bx--form__helper-text--inline", + ); + }); +}); diff --git a/tests/TextInput/TextInputCustom.test.svelte b/tests/TextInput/TextInputCustom.test.svelte new file mode 100644 index 00000000..2eea26dc --- /dev/null +++ b/tests/TextInput/TextInputCustom.test.svelte @@ -0,0 +1,7 @@ + + + + Custom Label Text + From 6dccd5cbe2040ab1e47d0f2ac8386cd25e9e137a Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 12:52:19 -0700 Subject: [PATCH 009/181] test(number-input): add unit tests --- tests/NumberInput.test.svelte | 51 ---- tests/NumberInput/NumberInput.test.svelte | 59 +++++ tests/NumberInput/NumberInput.test.ts | 237 ++++++++++++++++++ .../NumberInput/NumberInputCustom.test.svelte | 7 + 4 files changed, 303 insertions(+), 51 deletions(-) delete mode 100644 tests/NumberInput.test.svelte create mode 100644 tests/NumberInput/NumberInput.test.svelte create mode 100644 tests/NumberInput/NumberInput.test.ts create mode 100644 tests/NumberInput/NumberInputCustom.test.svelte diff --git a/tests/NumberInput.test.svelte b/tests/NumberInput.test.svelte deleted file mode 100644 index c1176783..00000000 --- a/tests/NumberInput.test.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - -{value} - - { - console.log({ input: e.detail }); // null | number - }} - on:change={(e) => { - console.log({ change: e.detail }); // null | number - }} - on:keydown - on:keyup - on:paste -/> - - { - console.log({ input: e.detail }); // null | number - }} - on:change={(e) => { - console.log(e.detail); // null | number - }} - on:keydown - on:keyup - on:paste -/> - - diff --git a/tests/NumberInput/NumberInput.test.svelte b/tests/NumberInput/NumberInput.test.svelte new file mode 100644 index 00000000..9794540c --- /dev/null +++ b/tests/NumberInput/NumberInput.test.svelte @@ -0,0 +1,59 @@ + + + + +
{value}
diff --git a/tests/NumberInput/NumberInput.test.ts b/tests/NumberInput/NumberInput.test.ts new file mode 100644 index 00000000..769a9f7c --- /dev/null +++ b/tests/NumberInput/NumberInput.test.ts @@ -0,0 +1,237 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import NumberInput from "./NumberInput.test.svelte"; +import NumberInputCustom from "./NumberInputCustom.test.svelte"; + +describe("NumberInput", () => { + it("should render with default props", () => { + render(NumberInput); + + expect(screen.getByLabelText("Clusters")).toBeInTheDocument(); + expect(screen.getByRole("spinbutton")).toHaveValue(0); + }); + + it("should handle step value", () => { + render(NumberInput, { props: { step: 0.1 } }); + + expect(screen.getByRole("spinbutton")).toHaveAttribute("step", "0.1"); + }); + + it("should handle min and max values", () => { + render(NumberInput, { props: { min: 4, max: 20 } }); + + const input = screen.getByRole("spinbutton"); + expect(input).toHaveAttribute("min", "4"); + expect(input).toHaveAttribute("max", "20"); + }); + + it("should handle different sizes", () => { + (["sm", "xl"] as const).forEach((size) => { + const { container } = render(NumberInput, { + props: { size }, + }); + + const input = container.querySelector("input"); + expect(input?.closest(".bx--number")).toHaveClass(`bx--number--${size}`); + container.remove(); + }); + }); + + it("should handle light variant", () => { + render(NumberInput, { props: { light: true } }); + + expect(screen.getByRole("spinbutton").closest(".bx--number")).toHaveClass( + "bx--number--light", + ); + }); + + it("should handle disabled state", () => { + render(NumberInput, { + props: { disabled: true }, + }); + + const input = screen.getByRole("spinbutton"); + expect(input).toBeDisabled(); + expect(screen.getByText("Clusters")).toHaveClass("bx--label--disabled"); + }); + + it("should handle helper text", () => { + render(NumberInput, { + props: { helperText: "Helper text" }, + }); + + expect(screen.getByText("Helper text")).toHaveClass( + "bx--form__helper-text", + ); + }); + + it("should handle invalid state", () => { + render(NumberInput, { + props: { invalid: true, invalidText: "Invalid input" }, + }); + + const input = screen.getByRole("spinbutton"); + expect(input).toHaveAttribute("aria-invalid", "true"); + expect(screen.getByText("Invalid input")).toBeInTheDocument(); + + expect(input.closest(".bx--number")).toHaveAttribute( + "data-invalid", + "true", + ); + }); + + it("should handle warning state", () => { + render(NumberInput, { + props: { warn: true, warnText: "Warning message" }, + }); + + const input = screen.getByRole("spinbutton"); + expect(input.closest(".bx--number__input-wrapper")).toHaveClass( + "bx--number__input-wrapper--warning", + ); + expect(screen.getByText("Warning message")).toBeInTheDocument(); + }); + + it("should handle hidden label", () => { + render(NumberInput, { props: { hideLabel: true } }); + + expect(screen.getByText("Clusters")).toHaveClass("bx--visually-hidden"); + }); + + it("should handle custom id", () => { + render(NumberInput, { props: { id: "custom-id" } }); + + const input = screen.getByRole("spinbutton"); + expect(input).toHaveAttribute("id", "custom-id"); + expect(screen.getByText("Clusters")).toHaveAttribute("for", "custom-id"); + }); + + it("should handle custom name", () => { + render(NumberInput, { props: { name: "custom-name" } }); + + expect(screen.getByRole("spinbutton")).toHaveAttribute( + "name", + "custom-name", + ); + }); + + it("should handle readonly state", () => { + render(NumberInput, { props: { readonly: true } }); + + expect(screen.getByRole("spinbutton")).toHaveAttribute("readonly"); + }); + + it("should handle hidden steppers", () => { + render(NumberInput, { props: { hideSteppers: true } }); + + expect( + screen.queryByRole("button", { name: "Increment number" }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Decrement number" }), + ).not.toBeInTheDocument(); + }); + + // TODO(bug): The icon descriptions are not being applied. + it.skip("should handle custom icon descriptions", () => { + render(NumberInput, { + props: { iconDescription: "Custom description" }, + }); + + screen.getAllByRole("button").forEach((button) => { + expect(button).toHaveAttribute("title", "Custom description"); + }); + }); + + it("should handle custom slots", () => { + render(NumberInputCustom); + + expect(screen.getByText("Custom Label Text")).toBeInTheDocument(); + }); + + it("should handle value binding", async () => { + render(NumberInput); + + const input = screen.getByRole("spinbutton"); + await user.type(input, "5"); + expect(screen.getByTestId("value").textContent).toBe("5"); + }); + + it("should handle increment/decrement buttons", async () => { + render(NumberInput); + + const incrementButton = screen.getByRole("button", { + name: "Increment number", + }); + const decrementButton = screen.getByRole("button", { + name: "Decrement number", + }); + + await user.click(incrementButton); + expect(screen.getByTestId("value").textContent).toBe("1"); + + await user.click(decrementButton); + expect(screen.getByTestId("value").textContent).toBe("0"); + }); + + it("should handle empty value when allowEmpty is true", async () => { + render(NumberInput, { + props: { allowEmpty: true }, + }); + + const input = screen.getByRole("spinbutton"); + await user.clear(input); + expect(input).toHaveValue(null); + }); + + it("should handle min/max validation", async () => { + render(NumberInput, { props: { min: 4, max: 20 } }); + + const input = screen.getByRole("spinbutton"); + await user.type(input, "25"); + expect(screen.getByTestId("value").textContent).toBe("25"); + expect(screen.getByRole("spinbutton")).toHaveAttribute( + "aria-invalid", + "true", + ); + }); + + it("should not show helper text when invalid", () => { + render(NumberInput, { + props: { + invalid: true, + invalidText: "Invalid input", + helperText: "Helper text", + }, + }); + + expect(screen.queryByText("Helper text")).not.toBeInTheDocument(); + expect(screen.getByText("Invalid input")).toBeInTheDocument(); + }); + + it("should not show helper text when warning", () => { + render(NumberInput, { + props: { + warn: true, + warnText: "Warning message", + helperText: "Helper text", + }, + }); + + expect(screen.queryByText("Helper text")).not.toBeInTheDocument(); + expect(screen.getByText("Warning message")).toBeInTheDocument(); + }); + + it("should handle disabled helper text", () => { + render(NumberInput, { + props: { + disabled: true, + helperText: "Helper text", + }, + }); + + expect(screen.getByText("Helper text")).toHaveClass( + "bx--form__helper-text--disabled", + ); + }); +}); diff --git a/tests/NumberInput/NumberInputCustom.test.svelte b/tests/NumberInput/NumberInputCustom.test.svelte new file mode 100644 index 00000000..4fa329a9 --- /dev/null +++ b/tests/NumberInput/NumberInputCustom.test.svelte @@ -0,0 +1,7 @@ + + + + Custom Label Text + From ec7d79878355826efe5253a16c4be15d72b5daf2 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 13:03:15 -0700 Subject: [PATCH 010/181] test(radio-button): add unit tests --- tests/RadioButton.test.svelte | 35 ----- tests/RadioButton/RadioButton.test.svelte | 32 +++++ tests/RadioButton/RadioButton.test.ts | 128 ++++++++++++++++++ .../RadioButton/RadioButtonCustom.test.svelte | 7 + 4 files changed, 167 insertions(+), 35 deletions(-) delete mode 100644 tests/RadioButton.test.svelte create mode 100644 tests/RadioButton/RadioButton.test.svelte create mode 100644 tests/RadioButton/RadioButton.test.ts create mode 100644 tests/RadioButton/RadioButtonCustom.test.svelte diff --git a/tests/RadioButton.test.svelte b/tests/RadioButton.test.svelte deleted file mode 100644 index e22109ce..00000000 --- a/tests/RadioButton.test.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - { - console.log(e.detail); // string - }} -> - - - - - - - - - - - - - - - - diff --git a/tests/RadioButton/RadioButton.test.svelte b/tests/RadioButton/RadioButton.test.svelte new file mode 100644 index 00000000..166c4b42 --- /dev/null +++ b/tests/RadioButton/RadioButton.test.svelte @@ -0,0 +1,32 @@ + + + { + console.log("change"); + }} +/> diff --git a/tests/RadioButton/RadioButton.test.ts b/tests/RadioButton/RadioButton.test.ts new file mode 100644 index 00000000..6103c8d8 --- /dev/null +++ b/tests/RadioButton/RadioButton.test.ts @@ -0,0 +1,128 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import RadioButton from "./RadioButton.test.svelte"; +import RadioButtonCustom from "./RadioButtonCustom.test.svelte"; + +describe("RadioButton", () => { + it("should render with default props", () => { + render(RadioButton); + + const input = screen.getByRole("radio"); + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute("name", "test-group"); + expect(input).toHaveAttribute("value", ""); + expect(input).not.toBeChecked(); + expect(screen.getByText("Option 1")).toBeInTheDocument(); + }); + + it("should handle checked state", () => { + render(RadioButton, { props: { checked: true } }); + + expect(screen.getByRole("radio")).toBeChecked(); + }); + + it("should handle disabled state", () => { + render(RadioButton, { props: { disabled: true } }); + + expect(screen.getByRole("radio")).toBeDisabled(); + }); + + it("should handle required state", () => { + render(RadioButton, { props: { required: true } }); + + expect(screen.getByRole("radio")).toHaveAttribute("required"); + }); + + it("should handle label position", () => { + render(RadioButton, { props: { labelPosition: "left" } }); + + expect( + screen.getByText("Option 1").closest(".bx--radio-button-wrapper"), + ).toHaveClass("bx--radio-button-wrapper--label-left"); + }); + + it("should handle hidden label", () => { + render(RadioButton, { props: { hideLabel: true } }); + + expect(screen.getByText("Option 1")).toHaveClass("bx--visually-hidden"); + }); + + it("should handle custom id", () => { + render(RadioButton, { props: { id: "custom-id" } }); + + expect(screen.getByRole("radio")).toHaveAttribute("id", "custom-id"); + + const radioButton = screen + .getByText("Option 1") + .closest(".bx--radio-button-wrapper"); + assert(radioButton); + expect(radioButton.querySelector("label")).toHaveAttribute( + "for", + "custom-id", + ); + }); + + it("should handle custom name", () => { + render(RadioButton, { props: { name: "custom-name" } }); + + expect(screen.getByRole("radio")).toHaveAttribute("name", "custom-name"); + }); + + it("should handle custom value", () => { + render(RadioButton, { props: { value: "custom-value" } }); + + expect(screen.getByRole("radio")).toHaveAttribute("value", "custom-value"); + }); + + it("should handle custom slots", () => { + render(RadioButtonCustom); + + expect(screen.getByText("Custom Label Text")).toBeInTheDocument(); + }); + + it("should handle change event", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(RadioButton); + + const input = screen.getByRole("radio"); + await user.click(input); + + expect(input).toBeChecked(); + expect(consoleLog).toHaveBeenCalledWith("change"); + }); + + // TODO(bug): forward focus/blur events. + it.skip("should handle focus and blur events", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(RadioButton); + + const input = screen.getByRole("radio"); + await user.tab(); + expect(input).toHaveFocus(); + expect(consoleLog).toHaveBeenCalledWith("focus"); + + await user.tab(); + expect(input).not.toHaveFocus(); + expect(consoleLog).toHaveBeenCalledWith("blur"); + }); + + it("should handle disabled state with events", async () => { + render(RadioButton, { props: { disabled: true } }); + + const input = screen.getByRole("radio"); + await user.click(input); + expect(input).not.toBeChecked(); + }); + + it("should handle required state with form validation", () => { + render(RadioButton, { props: { required: true } }); + + expect(screen.getByRole("radio")).toHaveAttribute("required"); + }); + + it("should handle label text slot", () => { + render(RadioButtonCustom); + + expect(screen.getByText("Custom Label Text").tagName).toBe("SPAN"); + }); +}); diff --git a/tests/RadioButton/RadioButtonCustom.test.svelte b/tests/RadioButton/RadioButtonCustom.test.svelte new file mode 100644 index 00000000..5913e719 --- /dev/null +++ b/tests/RadioButton/RadioButtonCustom.test.svelte @@ -0,0 +1,7 @@ + + + + Custom Label Text + From 6e62ce5416487e7938ce8ad6b0c883f319365240 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 13:10:59 -0700 Subject: [PATCH 011/181] test(time-picker): add unit tests --- tests/TimePicker.test.svelte | 67 ------- tests/TimePicker/TimePicker.test.svelte | 68 +++++++ tests/TimePicker/TimePicker.test.ts | 182 ++++++++++++++++++ tests/TimePicker/TimePickerCustom.test.svelte | 19 ++ 4 files changed, 269 insertions(+), 67 deletions(-) delete mode 100644 tests/TimePicker.test.svelte create mode 100644 tests/TimePicker/TimePicker.test.svelte create mode 100644 tests/TimePicker/TimePicker.test.ts create mode 100644 tests/TimePicker/TimePickerCustom.test.svelte diff --git a/tests/TimePicker.test.svelte b/tests/TimePicker.test.svelte deleted file mode 100644 index 8f25bf81..00000000 --- a/tests/TimePicker.test.svelte +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/TimePicker/TimePicker.test.svelte b/tests/TimePicker/TimePicker.test.svelte new file mode 100644 index 00000000..e2b06eb3 --- /dev/null +++ b/tests/TimePicker/TimePicker.test.svelte @@ -0,0 +1,68 @@ + + + { + console.log("change"); + }} + on:input={() => { + console.log("input"); + }} + on:keydown={() => { + console.log("keydown"); + }} + on:keyup={() => { + console.log("keyup"); + }} + on:focus={() => { + console.log("focus"); + }} + on:blur={() => { + console.log("blur"); + }} +> + + + + + + + + + diff --git a/tests/TimePicker/TimePicker.test.ts b/tests/TimePicker/TimePicker.test.ts new file mode 100644 index 00000000..43cdbcdf --- /dev/null +++ b/tests/TimePicker/TimePicker.test.ts @@ -0,0 +1,182 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import TimePicker from "./TimePicker.test.svelte"; +import TimePickerCustom from "./TimePickerCustom.test.svelte"; + +describe("TimePicker", () => { + it("should render with default props", () => { + render(TimePicker); + + const input = screen.getByRole("textbox"); + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute("name", "test-time"); + expect(input).toHaveAttribute("placeholder", "hh:mm"); + expect(input).toHaveAttribute("pattern", "(1[012]|[1-9]):[0-5][0-9](\\s)?"); + expect(input).toHaveAttribute("maxlength", "5"); + expect(screen.getByText("Time")).toBeInTheDocument(); + expect(screen.getByText("AM")).toBeInTheDocument(); + expect(screen.getByText("PM")).toBeInTheDocument(); + expect(screen.getByText("PDT")).toBeInTheDocument(); + expect(screen.getByText("GMT")).toBeInTheDocument(); + }); + + it("should handle different sizes", () => { + (["sm", "xl"] as const).forEach((size) => { + const { container } = render(TimePicker, { + props: { size }, + }); + + expect(container.querySelector(".bx--time-picker")).toHaveClass( + `bx--time-picker--${size}`, + ); + container.remove(); + }); + }); + + it("should handle light variant", () => { + render(TimePicker, { props: { light: true } }); + + const timePicker = screen.getByRole("textbox").closest(".bx--time-picker"); + expect(timePicker).toHaveClass("bx--time-picker--light"); + }); + + it("should handle disabled state", () => { + render(TimePicker, { props: { disabled: true } }); + + const input = screen.getByRole("textbox"); + expect(input).toBeDisabled(); + expect(screen.getByText("Time")).toHaveClass("bx--label--disabled"); + }); + + it("should handle invalid state", () => { + render(TimePicker, { + props: { invalid: true, invalidText: "Invalid time" }, + }); + + const input = screen.getByRole("textbox"); + expect(input).toHaveClass("bx--text-input--invalid"); + expect(input).toHaveAttribute("data-invalid"); + expect(screen.getByText("Invalid time")).toHaveClass( + "bx--form-requirement", + ); + }); + + it("should handle hidden label", () => { + render(TimePicker, { props: { hideLabel: true } }); + + expect(screen.getByText("Time")).toHaveClass("bx--visually-hidden"); + }); + + it("should handle custom id", () => { + render(TimePicker, { props: { id: "custom-id" } }); + + const input = screen.getByRole("textbox"); + expect(input).toHaveAttribute("id", "custom-id"); + expect(screen.getByText("Time")).toHaveAttribute("for", "custom-id"); + }); + + it("should handle custom name", () => { + render(TimePicker, { props: { name: "custom-name" } }); + + expect(screen.getByRole("textbox")).toHaveAttribute("name", "custom-name"); + }); + + it("should handle custom placeholder", () => { + render(TimePicker, { props: { placeholder: "Enter time" } }); + + expect(screen.getByRole("textbox")).toHaveAttribute( + "placeholder", + "Enter time", + ); + }); + + it("should handle custom pattern", () => { + render(TimePicker, { props: { pattern: "custom-pattern" } }); + + expect(screen.getByRole("textbox")).toHaveAttribute( + "pattern", + "custom-pattern", + ); + }); + + it("should handle custom maxlength", () => { + render(TimePicker, { props: { maxlength: 10 } }); + + expect(screen.getByRole("textbox")).toHaveAttribute("maxlength", "10"); + }); + + it("should handle value binding", async () => { + render(TimePicker); + + const input = screen.getByRole("textbox"); + await user.type(input, "10:30"); + expect(input).toHaveValue("10:30"); + }); + + it("should handle change event", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(TimePicker); + + const input = screen.getByRole("textbox"); + await user.type(input, "10:30"); + expect(consoleLog).toHaveBeenCalledWith("focus"); + expect(consoleLog).toHaveBeenCalledWith("input"); + await user.keyboard("{Enter}"); + expect(consoleLog).toHaveBeenCalledWith("keydown"); + expect(consoleLog).toHaveBeenCalledWith("input"); + + expect(input).toHaveValue("10:30"); + await user.keyboard("{Tab}"); + expect(consoleLog).toHaveBeenCalledWith("change"); + }); + + it("should handle focus and blur events", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(TimePicker); + + const input = screen.getByRole("textbox"); + await user.tab(); + expect(input).toHaveFocus(); + expect(consoleLog).toHaveBeenCalledWith("focus"); + + await user.tab(); + expect(input).not.toHaveFocus(); + expect(consoleLog).toHaveBeenCalledWith("blur"); + }); + + it("should handle disabled state with events", async () => { + render(TimePicker, { props: { disabled: true } }); + + const input = screen.getByRole("textbox"); + await user.type(input, "10:30"); + expect(input).toHaveValue(""); + }); + + it("should handle invalid state with helper text", () => { + render(TimePicker, { + props: { + invalid: true, + invalidText: "Invalid time", + }, + }); + + expect(screen.getByText("Invalid time")).toBeInTheDocument(); + }); + + it("should handle label text slot", () => { + render(TimePickerCustom); + + const label = screen.getByText("Custom Label Text"); + expect(label).toBeInTheDocument(); + expect(label.tagName).toBe("SPAN"); + }); + + it("should handle TimePickerSelect components", () => { + render(TimePicker); + + const selects = screen.getAllByRole("combobox"); + expect(selects).toHaveLength(2); + expect(selects[0]).toHaveValue("pm"); + expect(selects[1]).toHaveValue("pdt"); + }); +}); diff --git a/tests/TimePicker/TimePickerCustom.test.svelte b/tests/TimePicker/TimePickerCustom.test.svelte new file mode 100644 index 00000000..3e9c4867 --- /dev/null +++ b/tests/TimePicker/TimePickerCustom.test.svelte @@ -0,0 +1,19 @@ + + + + Custom Label Text + + + + + + + + + From 490d3b42ea510aab3de3e89a5bcf47ad38a67931 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 13:28:01 -0700 Subject: [PATCH 012/181] test(radio-tile): add unit tests --- tests/RadioTile.test.svelte | 17 -- tests/RadioTile/RadioTile.group.test.svelte | 25 +++ tests/RadioTile/RadioTile.single.test.svelte | 7 + tests/RadioTile/RadioTile.test.svelte | 43 ++++ tests/RadioTile/RadioTile.test.ts | 197 +++++++++++++++++++ tests/RadioTile/RadioTileCustom.test.svelte | 9 + 6 files changed, 281 insertions(+), 17 deletions(-) delete mode 100644 tests/RadioTile.test.svelte create mode 100644 tests/RadioTile/RadioTile.group.test.svelte create mode 100644 tests/RadioTile/RadioTile.single.test.svelte create mode 100644 tests/RadioTile/RadioTile.test.svelte create mode 100644 tests/RadioTile/RadioTile.test.ts create mode 100644 tests/RadioTile/RadioTileCustom.test.svelte diff --git a/tests/RadioTile.test.svelte b/tests/RadioTile.test.svelte deleted file mode 100644 index 45356d3b..00000000 --- a/tests/RadioTile.test.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - { - console.log(e.detail); // string - }} -> - Lite plan - Standard plan - Plus plan - diff --git a/tests/RadioTile/RadioTile.group.test.svelte b/tests/RadioTile/RadioTile.group.test.svelte new file mode 100644 index 00000000..defafb8a --- /dev/null +++ b/tests/RadioTile/RadioTile.group.test.svelte @@ -0,0 +1,25 @@ + + + + {#each values as value} + {value} + {/each} + + +
+ Selected: {selected} +
+ + diff --git a/tests/RadioTile/RadioTile.single.test.svelte b/tests/RadioTile/RadioTile.single.test.svelte new file mode 100644 index 00000000..6a986a02 --- /dev/null +++ b/tests/RadioTile/RadioTile.single.test.svelte @@ -0,0 +1,7 @@ + + + +
Custom content
+
diff --git a/tests/RadioTile/RadioTile.test.svelte b/tests/RadioTile/RadioTile.test.svelte new file mode 100644 index 00000000..84d734e4 --- /dev/null +++ b/tests/RadioTile/RadioTile.test.svelte @@ -0,0 +1,43 @@ + + + + { + console.log("change"); + }} + on:keydown + on:click + on:mouseover + on:mouseenter + on:mouseleave + > + Test content + + diff --git a/tests/RadioTile/RadioTile.test.ts b/tests/RadioTile/RadioTile.test.ts new file mode 100644 index 00000000..351c01dc --- /dev/null +++ b/tests/RadioTile/RadioTile.test.ts @@ -0,0 +1,197 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import RadioTile from "./RadioTile.test.svelte"; +import RadioTileCustom from "./RadioTileCustom.test.svelte"; +import RadioTileSingle from "./RadioTile.single.test.svelte"; +import RadioTileGroup from "./RadioTile.group.test.svelte"; + +describe("RadioTile", () => { + it("should render with default props", () => { + render(RadioTile); + + const input = screen.getByRole("radio"); + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute("name", "test-group"); + expect(input).toHaveAttribute("value", "test"); + expect(input).not.toBeChecked(); + expect(screen.getByText("Test content")).toBeInTheDocument(); + expect(screen.getByTitle("Tile checkmark")).toBeInTheDocument(); + }); + + it("should handle checked state", () => { + render(RadioTile, { + props: { checked: true }, + }); + + const input = screen.getByRole("radio"); + expect(input).toBeChecked(); + expect(screen.getByText("Test content").closest(".bx--tile")).toHaveClass( + "bx--tile--is-selected", + ); + }); + + it("should handle light variant", () => { + render(RadioTile, { + props: { light: true }, + }); + + expect(screen.getByText("Test content").closest(".bx--tile")).toHaveClass( + "bx--tile--light", + ); + }); + + it("should handle disabled state", () => { + render(RadioTile, { + props: { disabled: true }, + }); + + const input = screen.getByRole("radio"); + expect(input).toBeDisabled(); + expect(screen.getByText("Test content").closest(".bx--tile")).toHaveClass( + "bx--tile--disabled", + ); + }); + + it("should handle required state", () => { + render(RadioTile, { + props: { required: true }, + }); + + expect(screen.getByRole("radio")).toHaveAttribute("required"); + }); + + it("should handle custom value", () => { + render(RadioTile, { + props: { value: "custom-value" }, + }); + + expect(screen.getByRole("radio")).toHaveAttribute("value", "custom-value"); + }); + + it("should handle custom tabindex", () => { + render(RadioTile, { + props: { tabindex: "1" }, + }); + + expect(screen.getByRole("radio")).toHaveAttribute("tabindex", "1"); + }); + + it("should handle custom icon description", () => { + render(RadioTile, { + props: { iconDescription: "Custom checkmark" }, + }); + + expect(screen.getByTitle("Custom checkmark")).toBeInTheDocument(); + }); + + it("should handle custom id", () => { + render(RadioTile, { props: { id: "custom-id" } }); + + expect(screen.getByRole("radio")).toHaveAttribute("id", "custom-id"); + + const radioTileLabel = screen.getByText("Test content").closest("label"); + assert(radioTileLabel); + expect(radioTileLabel).toHaveAttribute("for", "custom-id"); + }); + + // TODO(bug): support standalone radio tile. + it.skip("should handle custom name", () => { + render(RadioTileSingle); + + expect(screen.getByRole("radio")).toHaveAttribute("name", "custom-name"); + }); + + it("should handle custom slots", () => { + render(RadioTileCustom); + + expect(screen.getByText("Custom content")).toBeInTheDocument(); + }); + + it("should handle change event", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(RadioTile); + + const input = screen.getByRole("radio"); + await user.click(input); + + expect(input).toBeChecked(); + expect(consoleLog).toHaveBeenCalledWith("change"); + }); + + it("should handle keyboard events", async () => { + render(RadioTileGroup); + + const inputs = screen.getAllByRole("radio"); + + expect(inputs[1]).not.toHaveFocus(); + expect(inputs[1]).toBeChecked(); + + await user.tab(); + expect(inputs[1]).toHaveFocus(); + + await user.keyboard("{ArrowDown}"); + expect(inputs[2]).toHaveFocus(); + expect(inputs[2]).toBeChecked(); + + await user.keyboard("{ArrowDown}"); + expect(inputs[0]).toHaveFocus(); + expect(inputs[0]).toBeChecked(); + }); + + it("supports programmatic selection", async () => { + render(RadioTileGroup); + + const inputs = screen.getAllByRole("radio"); + expect(inputs[1]).not.toHaveFocus(); + expect(inputs[1]).toBeChecked(); + expect(screen.getByText(/Selected: Standard plan/)).toBeInTheDocument(); + + await user.click(inputs[2]); + expect(inputs[2]).toHaveFocus(); + expect(inputs[2]).toBeChecked(); + expect(screen.getByText(/Selected: Plus plan/)).toBeInTheDocument(); + + await user.click(screen.getByRole("button")); + expect(inputs[1]).not.toHaveFocus(); + expect(inputs[1]).toBeChecked(); + expect(screen.getByText(/Selected: Standard plan/)).toBeInTheDocument(); + }); + + it("should handle disabled state with events", async () => { + render(RadioTile, { + props: { disabled: true }, + }); + + const input = screen.getByRole("radio"); + await user.click(input); + expect(input).not.toBeChecked(); + }); + + it("should handle mouse events", async () => { + render(RadioTile); + + const tile = screen.getByText("Test content").closest(".bx--tile"); + assert(tile); + await user.hover(tile); + await user.unhover(tile); + }); + + it("should handle custom content slot", () => { + render(RadioTileCustom); + + const content = screen.getByText("Custom content"); + expect(content).toBeInTheDocument(); + expect(content.tagName).toBe("DIV"); + }); + + it("should handle TileGroup context", () => { + render(RadioTile, { props: { checked: true } }); + + const input = screen.getByRole("radio"); + expect(input).toBeChecked(); + expect(screen.getByText("Test content").closest(".bx--tile")).toHaveClass( + "bx--tile--is-selected", + ); + expect(input).toHaveAttribute("name", "test-group"); + }); +}); diff --git a/tests/RadioTile/RadioTileCustom.test.svelte b/tests/RadioTile/RadioTileCustom.test.svelte new file mode 100644 index 00000000..9d97ddf9 --- /dev/null +++ b/tests/RadioTile/RadioTileCustom.test.svelte @@ -0,0 +1,9 @@ + + + + +
Custom content
+
+
From f89e9df8f01ac8f0ca48df690e0b16da0093801b Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 15:50:54 -0700 Subject: [PATCH 013/181] test(expandable-tile): add unit tests --- tests/ExpandableTile.test.svelte | 22 --- .../ExpandableTile/ExpandableTile.test.svelte | 46 ++++++ tests/ExpandableTile/ExpandableTile.test.ts | 149 ++++++++++++++++++ .../ExpandableTileCustom.test.svelte | 31 ++++ tests/setup-tests.ts | 37 +++++ 5 files changed, 263 insertions(+), 22 deletions(-) delete mode 100644 tests/ExpandableTile.test.svelte create mode 100644 tests/ExpandableTile/ExpandableTile.test.svelte create mode 100644 tests/ExpandableTile/ExpandableTile.test.ts create mode 100644 tests/ExpandableTile/ExpandableTileCustom.test.svelte diff --git a/tests/ExpandableTile.test.svelte b/tests/ExpandableTile.test.svelte deleted file mode 100644 index 53927f6d..00000000 --- a/tests/ExpandableTile.test.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - -
Above the fold content here
-
Below the fold content here
-
- - -
Above the fold content here
-
Below the fold content here
-
- - -
Above the fold content here
-
Below the fold content here
-
diff --git a/tests/ExpandableTile/ExpandableTile.test.svelte b/tests/ExpandableTile/ExpandableTile.test.svelte new file mode 100644 index 00000000..9592038f --- /dev/null +++ b/tests/ExpandableTile/ExpandableTile.test.svelte @@ -0,0 +1,46 @@ + + + +
+ Above the fold content here +
+
+ Below the fold content here +
+
diff --git a/tests/ExpandableTile/ExpandableTile.test.ts b/tests/ExpandableTile/ExpandableTile.test.ts new file mode 100644 index 00000000..471722aa --- /dev/null +++ b/tests/ExpandableTile/ExpandableTile.test.ts @@ -0,0 +1,149 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import ExpandableTile from "./ExpandableTile.test.svelte"; +import ExpandableTileCustom from "./ExpandableTileCustom.test.svelte"; + +describe("ExpandableTile", () => { + it("should render with default props", () => { + render(ExpandableTile); + + const tile = screen.getByRole("button"); + expect(tile).toBeInTheDocument(); + expect(tile).toHaveAttribute("aria-expanded", "false"); + expect(tile).toHaveAttribute("title", "Interact to expand Tile"); + expect(screen.getByTestId("above-content")).toBeInTheDocument(); + expect(screen.getByTestId("below-content")).toBeInTheDocument(); + }); + + it("should handle expanded state", () => { + render(ExpandableTile, { props: { expanded: true } }); + + const tile = screen.getByRole("button"); + expect(tile).toHaveAttribute("aria-expanded", "true"); + expect(tile).toHaveAttribute("title", "Interact to collapse Tile"); + expect(tile).toHaveClass("bx--tile--is-expanded"); + }); + + it("should handle light variant", () => { + render(ExpandableTile, { props: { light: true } }); + + expect(screen.getByRole("button")).toHaveClass("bx--tile--light"); + }); + + it("should handle custom icon text", async () => { + render(ExpandableTile, { + props: { + tileCollapsedIconText: "Custom collapsed text", + tileExpandedIconText: "Custom expanded text", + }, + }); + + const tile = screen.getByRole("button"); + expect(tile).toHaveAttribute("title", "Custom collapsed text"); + + await user.click(tile); + expect(tile).toHaveAttribute("title", "Custom expanded text"); + }); + + it("should handle custom labels", async () => { + render(ExpandableTile, { + props: { + tileCollapsedLabel: "Show more", + tileExpandedLabel: "Show less", + }, + }); + + expect(screen.getByText("Show more")).toBeInTheDocument(); + + await user.click(screen.getByRole("button")); + expect(screen.getByText("Show less")).toBeInTheDocument(); + }); + + it("should handle custom tabindex", () => { + render(ExpandableTile, { props: { tabindex: "1" } }); + + expect(screen.getByRole("button")).toHaveAttribute("tabindex", "1"); + }); + + it("should handle custom id", () => { + render(ExpandableTile, { props: { id: "custom-id" } }); + + expect(screen.getByRole("button")).toHaveAttribute("id", "custom-id"); + }); + + it("should toggle expanded state on click", async () => { + render(ExpandableTile); + + const tile = screen.getByRole("button"); + expect(tile).toHaveAttribute("aria-expanded", "false"); + + await user.click(tile); + expect(tile).toHaveAttribute("aria-expanded", "true"); + + await user.click(tile); + expect(tile).toHaveAttribute("aria-expanded", "false"); + }); + + it("should handle keyboard events", async () => { + render(ExpandableTile); + + const tile = screen.getByRole("button"); + await user.tab(); + expect(tile).toHaveFocus(); + + await user.keyboard("{Enter}"); + expect(tile).toHaveAttribute("aria-expanded", "true"); + + await user.keyboard(" "); + expect(tile).toHaveAttribute("aria-expanded", "false"); + }); + + it("should handle interactive content without toggling", async () => { + render(ExpandableTileCustom); + + const tileButton = screen.getAllByRole("button")[0]; + const link = screen.getByTestId("test-link"); + const button = screen.getByTestId("test-button"); + + expect(tileButton).toHaveAttribute("aria-expanded", "false"); + + await user.click(link); + expect(tileButton).toHaveAttribute("aria-expanded", "false"); + + await user.click(button); + expect(tileButton).toHaveAttribute("aria-expanded", "false"); + }); + + it("should handle mouse events", async () => { + render(ExpandableTile); + + const tile = screen.getByRole("button"); + await user.hover(tile); + await user.unhover(tile); + }); + + it("should handle custom content slots", () => { + render(ExpandableTile); + + const aboveContent = screen.getByTestId("above-content"); + const belowContent = screen.getByTestId("below-content"); + + expect(aboveContent).toHaveTextContent("Above the fold content here"); + expect(belowContent).toHaveTextContent("Below the fold content here"); + }); + + it("should handle max height and padding", async () => { + render(ExpandableTile, { + props: { + tileMaxHeight: 200, + tilePadding: 20, + }, + }); + + const tile = screen.getByRole("button"); + expect(tile.getAttribute("style")).toBe("max-height: 105px;"); + + await user.click(tile); + expect(tile.getAttribute("style")).toBe("max-height: none;"); + }); +}); diff --git a/tests/ExpandableTile/ExpandableTileCustom.test.svelte b/tests/ExpandableTile/ExpandableTileCustom.test.svelte new file mode 100644 index 00000000..035b59bf --- /dev/null +++ b/tests/ExpandableTile/ExpandableTileCustom.test.svelte @@ -0,0 +1,31 @@ + + + +
+ { + linkClicked = true; + }} + > + Test link + +

+ +
+
Below the fold content here
+
diff --git a/tests/setup-tests.ts b/tests/setup-tests.ts index c69af710..8f22aad9 100644 --- a/tests/setup-tests.ts +++ b/tests/setup-tests.ts @@ -6,4 +6,41 @@ import "../css/all.css"; // Mock scrollIntoView since it's not implemented in JSDOM Element.prototype.scrollIntoView = vi.fn(); +// Mock ResizeObserver since it's not implemented in JSDOM +class ResizeObserverMock { + callback: ResizeObserverCallback; + elements: Element[]; + + constructor(callback: ResizeObserverCallback) { + this.callback = callback; + this.elements = []; + } + + observe(element: Element) { + this.elements.push(element); + this.callback( + [ + { + target: element, + contentRect: { height: 100 } as DOMRectReadOnly, + borderBoxSize: [], + contentBoxSize: [], + devicePixelContentBoxSize: [], + }, + ], + this, + ); + } + + unobserve(element: Element) { + this.elements = this.elements.filter((el) => el !== element); + } + + disconnect() { + this.elements = []; + } +} + +global.ResizeObserver = ResizeObserverMock; + export const user = userEvent.setup(); From f5342d4b96ffc8cd9cf3479c51e308b31627dfd2 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 16:02:16 -0700 Subject: [PATCH 014/181] test(structured-list): add unit tests --- tests/StructuredList.test.svelte | 100 ------------- .../StructuredList/StructuredList.test.svelte | 79 +++++++++++ tests/StructuredList/StructuredList.test.ts | 131 ++++++++++++++++++ .../StructuredListCustom.test.svelte | 26 ++++ 4 files changed, 236 insertions(+), 100 deletions(-) delete mode 100644 tests/StructuredList.test.svelte create mode 100644 tests/StructuredList/StructuredList.test.svelte create mode 100644 tests/StructuredList/StructuredList.test.ts create mode 100644 tests/StructuredList/StructuredListCustom.test.svelte diff --git a/tests/StructuredList.test.svelte b/tests/StructuredList.test.svelte deleted file mode 100644 index f04ece1f..00000000 --- a/tests/StructuredList.test.svelte +++ /dev/null @@ -1,100 +0,0 @@ - - - { - console.log(e.detail); // string - }} -> - - - Column A - Column B - Column C - - - - - Row 1 - Row 1 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc dui magna, - finibus id tortor sed, aliquet bibendum augue. Aenean posuere sem vel - euismod dignissim. Nulla ut cursus dolor. Pellentesque vulputate nisl a - porttitor interdum. - - - - Row 2 - Row 2 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc dui magna, - finibus id tortor sed, aliquet bibendum augue. Aenean posuere sem vel - euismod dignissim. Nulla ut cursus dolor. Pellentesque vulputate nisl a - porttitor interdum. - - - - Row 3 - Row 3 - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc dui magna, - finibus id tortor sed, aliquet bibendum augue. Aenean posuere sem vel - euismod dignissim. Nulla ut cursus dolor. Pellentesque vulputate nisl a - porttitor interdum. - - - - - - - - - ColumnA - ColumnB - ColumnC - {""} - - - - {#each [1, 2, 3] as item} - - Row {item} - Row {item} - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc dui - magna, finibus id tortor sed, aliquet bibendum augue. Aenean posuere - sem vel euismod dignissim. Nulla ut cursus dolor. Pellentesque - vulputate nisl a porttitor interdum. - - - - - - - {/each} - - - - diff --git a/tests/StructuredList/StructuredList.test.svelte b/tests/StructuredList/StructuredList.test.svelte new file mode 100644 index 00000000..cacf5f64 --- /dev/null +++ b/tests/StructuredList/StructuredList.test.svelte @@ -0,0 +1,79 @@ + + + { + console.log("click"); + }} + on:mouseover={() => { + console.log("mouseover"); + }} + on:mouseenter={() => { + console.log("mouseenter"); + }} + on:mouseleave={() => { + console.log("mouseleave"); + }} + on:change={(e) => { + console.log("change", e.detail); + }} +> + + + Column A + Column B + Column C + {#if selection} + {""} + {/if} + + + + {#each ["1", "2", "3"] as item} + + Row {item} + Row {item} + Content {item} + {#if selection} + + + + + {/if} + + {/each} + + + +
{selected}
diff --git a/tests/StructuredList/StructuredList.test.ts b/tests/StructuredList/StructuredList.test.ts new file mode 100644 index 00000000..c11e7c89 --- /dev/null +++ b/tests/StructuredList/StructuredList.test.ts @@ -0,0 +1,131 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import StructuredList from "./StructuredList.test.svelte"; +import StructuredListCustom from "./StructuredListCustom.test.svelte"; + +describe("StructuredList", () => { + it("should render with default props", () => { + render(StructuredList); + + const list = screen.getByRole("table"); + expect(list).toBeInTheDocument(); + expect(list).toHaveClass("bx--structured-list"); + + // Check header cells + const headerCells = screen.getAllByRole("columnheader"); + expect(headerCells).toHaveLength(3); + expect(headerCells[0]).toHaveTextContent("Column A"); + expect(headerCells[1]).toHaveTextContent("Column B"); + expect(headerCells[2]).toHaveTextContent("Column C"); + + // Check body cells + const cells = screen.getAllByRole("cell"); + expect(cells).toHaveLength(9); // 3 rows x 3 columns + expect(cells[0]).toHaveTextContent("Row 1"); + expect(cells[1]).toHaveTextContent("Row 1"); + expect(cells[2]).toHaveTextContent("Content 1"); + }); + + it("should handle condensed variant", () => { + render(StructuredList, { props: { condensed: true } }); + + expect(screen.getByRole("table")).toHaveClass( + "bx--structured-list--condensed", + ); + }); + + it("should handle flush variant", () => { + render(StructuredList, { props: { flush: true } }); + + expect(screen.getByRole("table")).toHaveClass("bx--structured-list--flush"); + }); + + it("should handle selection variant", () => { + render(StructuredList, { props: { selection: true } }); + + const list = screen.getByRole("table"); + expect(list).toHaveClass("bx--structured-list--selection"); + + const inputs = screen.getAllByRole("radio"); + expect(inputs).toHaveLength(3); + + const checkmarks = screen.getAllByTitle("select an option"); + expect(checkmarks).toHaveLength(3); + }); + + it("should handle selected state", async () => { + render(StructuredList, { + props: { selection: true, selected: "row-1-value" }, + }); + + const selectedInput = screen.getByRole("radio", { checked: true }); + expect(selectedInput.closest("label")).toHaveTextContent("Row 1"); + + await user.click(screen.getAllByRole("radio")[1]); + expect( + screen.getByRole("radio", { checked: true }).closest("label"), + ).toHaveTextContent("Row 2"); + }); + + it("should handle selection change", async () => { + render(StructuredList, { props: { selection: true } }); + + const secondInput = screen.getAllByRole("radio")[1]; + await user.click(secondInput); + + expect(screen.getByTestId("value").textContent).toBe("row-2-value"); + }); + + it("should handle custom content", () => { + render(StructuredListCustom); + + expect(screen.getByTestId("custom-header")).toHaveTextContent( + "Custom Header", + ); + expect(screen.getByTestId("custom-content")).toHaveTextContent( + "Custom Content", + ); + }); + + it("should handle mouse events", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(StructuredList); + + const list = screen.getByRole("table"); + + await user.click(list); + expect(consoleLog).toHaveBeenCalledWith("click"); + await user.hover(list); + expect(consoleLog).toHaveBeenCalledWith("mouseover"); + await user.unhover(list); + expect(consoleLog).toHaveBeenCalledWith("mouseleave"); + }); + + it("should handle noWrap cells", () => { + render(StructuredList); + + const noWrapCells = screen + .getAllByRole("cell") + .filter( + (cell) => + cell.textContent?.startsWith("Row") && cell.textContent?.length === 5, + ); + + noWrapCells.forEach((cell) => { + expect(cell).toHaveClass("bx--structured-list-td"); + }); + }); + + it("should emit change event on selection", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(StructuredList, { props: { selection: true } }); + + expect(consoleLog).not.toHaveBeenCalled(); + + await user.click(screen.getAllByRole("radio")[1]); + expect(consoleLog).toHaveBeenCalledWith("change", "row-2-value"); + + await user.click(screen.getAllByRole("radio")[0]); + expect(consoleLog).toHaveBeenCalledWith("change", "row-1-value"); + }); +}); diff --git a/tests/StructuredList/StructuredListCustom.test.svelte b/tests/StructuredList/StructuredListCustom.test.svelte new file mode 100644 index 00000000..3ef8a400 --- /dev/null +++ b/tests/StructuredList/StructuredListCustom.test.svelte @@ -0,0 +1,26 @@ + + + + + + +
Custom Header
+
+
+
+ + + +
Custom Content
+
+
+
+
From a4b10500a30d5f7fc6edcd7a915ade3f09a4af5c Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 16:05:12 -0700 Subject: [PATCH 015/181] test(skeleton-text): add unit tests --- tests/SkeletonText.test.svelte | 15 ---- tests/SkeletonText/SkeletonText.test.svelte | 27 +++++++ tests/SkeletonText/SkeletonText.test.ts | 90 +++++++++++++++++++++ 3 files changed, 117 insertions(+), 15 deletions(-) delete mode 100644 tests/SkeletonText.test.svelte create mode 100644 tests/SkeletonText/SkeletonText.test.svelte create mode 100644 tests/SkeletonText/SkeletonText.test.ts diff --git a/tests/SkeletonText.test.svelte b/tests/SkeletonText.test.svelte deleted file mode 100644 index 9a96f2bd..00000000 --- a/tests/SkeletonText.test.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - diff --git a/tests/SkeletonText/SkeletonText.test.svelte b/tests/SkeletonText/SkeletonText.test.svelte new file mode 100644 index 00000000..1caddcfc --- /dev/null +++ b/tests/SkeletonText/SkeletonText.test.svelte @@ -0,0 +1,27 @@ + + + { + console.log("click"); + }} + on:mouseover={() => { + console.log("mouseover"); + }} + on:mouseenter={() => { + console.log("mouseenter"); + }} + on:mouseleave={() => { + console.log("mouseleave"); + }} +/> diff --git a/tests/SkeletonText/SkeletonText.test.ts b/tests/SkeletonText/SkeletonText.test.ts new file mode 100644 index 00000000..7796b09a --- /dev/null +++ b/tests/SkeletonText/SkeletonText.test.ts @@ -0,0 +1,90 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import SkeletonText from "./SkeletonText.test.svelte"; + +describe("SkeletonText", () => { + it("should render with default props", () => { + render(SkeletonText); + const element = screen.getByRole("paragraph"); + expect(element).toHaveClass("bx--skeleton__text"); + expect(element).toHaveStyle({ width: "100%" }); + }); + + it("should render heading variant", () => { + render(SkeletonText, { props: { heading: true } }); + const element = screen.getByRole("paragraph"); + expect(element).toHaveClass("bx--skeleton__text", "bx--skeleton__heading"); + }); + + it("should render paragraph variant with default lines", () => { + render(SkeletonText, { props: { paragraph: true } }); + + const elements = screen.getAllByRole("paragraph"); + expect(elements).toHaveLength(3); // default lines is 3 + elements.forEach((element) => { + expect(element).toHaveClass("bx--skeleton__text"); + }); + }); + + it("should render paragraph with custom line count", () => { + render(SkeletonText, { props: { paragraph: true, lines: 8 } }); + + const elements = screen.getAllByRole("paragraph"); + expect(elements).toHaveLength(8); + }); + + it("should render with custom width", () => { + render(SkeletonText, { props: { width: "50%" } }); + + const element = screen.getByRole("paragraph"); + expect(element).toHaveStyle({ width: "50%" }); + }); + + it("should render paragraph with pixel width", () => { + render(SkeletonText, { props: { paragraph: true, width: "200px" } }); + + const elements = screen.getAllByRole("paragraph"); + elements.forEach((element) => { + const width = element.style.width; + expect(width).toMatch(/^\d+px$/); + const numWidth = parseInt(width); + expect(numWidth).toBeGreaterThanOrEqual(125); // 200 - 75 + expect(numWidth).toBeLessThanOrEqual(200); + }); + }); + + it("should handle mouse events", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(SkeletonText); + + const element = screen.getByRole("paragraph"); + + await user.click(element); + expect(consoleLog).toHaveBeenCalledWith("click"); + + await user.hover(element); + expect(consoleLog).toHaveBeenCalledWith("mouseover"); + + await user.unhover(element); + expect(consoleLog).toHaveBeenCalledWith("mouseleave"); + }); + + it("should handle paragraph mouse events", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(SkeletonText, { props: { paragraph: true } }); + + const container = screen.getAllByRole("paragraph")[0].parentElement; + expect(container).toBeTruthy(); + + if (container) { + await user.click(container); + expect(consoleLog).toHaveBeenCalledWith("click"); + + await user.hover(container); + expect(consoleLog).toHaveBeenCalledWith("mouseover"); + + await user.unhover(container); + expect(consoleLog).toHaveBeenCalledWith("mouseleave"); + } + }); +}); From 3607c70070accca0578102ab497b878d841d6cd8 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 16:09:09 -0700 Subject: [PATCH 016/181] test(skeleton-placeholder): add unit tests --- tests/SkeletonPlaceholder.test.svelte | 7 --- .../SkeletonPlaceholder.test.svelte | 20 +++++++ .../SkeletonPlaceholder.test.ts | 58 +++++++++++++++++++ 3 files changed, 78 insertions(+), 7 deletions(-) delete mode 100644 tests/SkeletonPlaceholder.test.svelte create mode 100644 tests/SkeletonPlaceholder/SkeletonPlaceholder.test.svelte create mode 100644 tests/SkeletonPlaceholder/SkeletonPlaceholder.test.ts diff --git a/tests/SkeletonPlaceholder.test.svelte b/tests/SkeletonPlaceholder.test.svelte deleted file mode 100644 index 46ceb881..00000000 --- a/tests/SkeletonPlaceholder.test.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/tests/SkeletonPlaceholder/SkeletonPlaceholder.test.svelte b/tests/SkeletonPlaceholder/SkeletonPlaceholder.test.svelte new file mode 100644 index 00000000..c79dc33b --- /dev/null +++ b/tests/SkeletonPlaceholder/SkeletonPlaceholder.test.svelte @@ -0,0 +1,20 @@ + + + { + console.log("click"); + }} + on:mouseover={() => { + console.log("mouseover"); + }} + on:mouseenter={() => { + console.log("mouseenter"); + }} + on:mouseleave={() => { + console.log("mouseleave"); + }} +/> diff --git a/tests/SkeletonPlaceholder/SkeletonPlaceholder.test.ts b/tests/SkeletonPlaceholder/SkeletonPlaceholder.test.ts new file mode 100644 index 00000000..0df9df0a --- /dev/null +++ b/tests/SkeletonPlaceholder/SkeletonPlaceholder.test.ts @@ -0,0 +1,58 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import SkeletonPlaceholder from "./SkeletonPlaceholder.test.svelte"; + +describe("SkeletonPlaceholder", () => { + it("should render with default props", () => { + render(SkeletonPlaceholder); + + const element = screen.getByTestId("skeleton-placeholder"); + expect(element).toHaveClass("bx--skeleton__placeholder"); + }); + + it("should render with custom size", () => { + render(SkeletonPlaceholder, { + props: { style: "height: 12rem; width: 12rem;" }, + }); + + const element = screen.getByTestId("skeleton-placeholder"); + expect(element).toHaveStyle({ height: "12rem", width: "12rem" }); + }); + + it("should handle mouse events", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(SkeletonPlaceholder); + + const element = screen.getByTestId("skeleton-placeholder"); + + await user.click(element); + expect(consoleLog).toHaveBeenCalledWith("click"); + + await user.hover(element); + expect(consoleLog).toHaveBeenCalledWith("mouseover"); + + await user.unhover(element); + expect(consoleLog).toHaveBeenCalledWith("mouseleave"); + }); + + it("should accept additional attributes", () => { + render(SkeletonPlaceholder, { + props: { + "data-testid": "custom-placeholder", + "aria-label": "Loading placeholder", + }, + }); + + const element = screen.getByTestId("custom-placeholder"); + expect(element).toHaveAttribute("aria-label", "Loading placeholder"); + }); + + it("should accept additional classes", () => { + render(SkeletonPlaceholder, { + props: { class: "custom-class" }, + }); + + const element = screen.getByTestId("skeleton-placeholder"); + expect(element).toHaveClass("bx--skeleton__placeholder", "custom-class"); + }); +}); From 0b799d64b7ec5a7d774f239b42b854810b03fdc9 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 16:28:46 -0700 Subject: [PATCH 017/181] test(dropdown): add unit tests --- tests/Dropdown.test.svelte | 115 ---------- tests/Dropdown/Dropdown.test.svelte | 53 +++++ tests/Dropdown/Dropdown.test.ts | 266 ++++++++++++++++++++++++ tests/Dropdown/DropdownSlot.test.svelte | 25 +++ 4 files changed, 344 insertions(+), 115 deletions(-) delete mode 100644 tests/Dropdown.test.svelte create mode 100644 tests/Dropdown/Dropdown.test.svelte create mode 100644 tests/Dropdown/Dropdown.test.ts create mode 100644 tests/Dropdown/DropdownSlot.test.svelte diff --git a/tests/Dropdown.test.svelte b/tests/Dropdown.test.svelte deleted file mode 100644 index 3ba410d6..00000000 --- a/tests/Dropdown.test.svelte +++ /dev/null @@ -1,115 +0,0 @@ - - - { - console.log(e.detail.selectedId); - }} - translateWithId={(id) => { - console.log(id); // "open" | "close" - return id; - }} - let:item - let:index -> - {item.id} - {index} - - - { - return item.text + " (" + item.id + ")"; - }} - titleText="Contact" - selectedId="0" - items={itemsWithoutConst} -/> - - - - - - - - - - - - diff --git a/tests/Dropdown/Dropdown.test.svelte b/tests/Dropdown/Dropdown.test.svelte new file mode 100644 index 00000000..6e5663f9 --- /dev/null +++ b/tests/Dropdown/Dropdown.test.svelte @@ -0,0 +1,53 @@ + + + diff --git a/tests/Dropdown/Dropdown.test.ts b/tests/Dropdown/Dropdown.test.ts new file mode 100644 index 00000000..4154d81e --- /dev/null +++ b/tests/Dropdown/Dropdown.test.ts @@ -0,0 +1,266 @@ +import { render, screen } from "@testing-library/svelte"; +import { user } from "../setup-tests"; +import Dropdown from "./Dropdown.test.svelte"; +import DropdownSlot from "./DropdownSlot.test.svelte"; + +const items = [ + { id: "0", text: "Slack" }, + { id: "1", text: "Email" }, + { id: "2", text: "Fax" }, +] as const; + +describe("Dropdown", () => { + it("should render with default props", () => { + render(Dropdown, { + props: { items, selectedId: "0", titleText: "Contact" }, + }); + + expect(screen.getByText("Contact")).toBeInTheDocument(); + const button = screen.getByRole("button"); + expect(button.querySelector(".bx--list-box__label")).toHaveTextContent( + "Slack", + ); + }); + + it("should handle custom item display text", () => { + render(Dropdown, { + props: { + items, + selectedId: "0", + titleText: "Contact", + itemToString: (item) => `${item.text} (${item.id})`, + }, + }); + + const button = screen.getByRole("button"); + expect(button.querySelector(".bx--list-box__label")).toHaveTextContent( + "Slack (0)", + ); + }); + + it("should handle hidden label", () => { + render(Dropdown, { + props: { + items, + selectedId: "0", + titleText: "Contact", + hideLabel: true, + }, + }); + + const label = screen.getByText("Contact"); + expect(label).toHaveClass("bx--visually-hidden"); + }); + + it("should handle light variant", () => { + render(Dropdown, { + props: { + items, + selectedId: "0", + light: true, + }, + }); + + const button = screen.getByRole("button"); + expect(button.closest(".bx--dropdown")).toHaveClass("bx--dropdown--light"); + }); + + it("should handle inline variant", () => { + render(Dropdown, { + props: { + items, + selectedId: "0", + type: "inline", + }, + }); + + const button = screen.getByRole("button"); + expect(button).toBeEnabled(); + expect(button).toHaveTextContent("Slack"); + expect(button.closest(".bx--dropdown__wrapper")).toHaveClass( + "bx--dropdown__wrapper--inline", + ); + }); + + it("should handle size variants", async () => { + const { rerender } = render(Dropdown, { + props: { + items, + selectedId: "0", + size: "sm", + }, + }); + + const button = screen.getByRole("button"); + expect(button.closest(".bx--dropdown")).toHaveClass("bx--dropdown--sm"); + + await rerender({ items, selectedId: "0", size: "xl" }); + expect(button.closest(".bx--dropdown")).toHaveClass("bx--dropdown--xl"); + }); + + it("should handle invalid state", () => { + render(Dropdown, { + props: { + items, + selectedId: "0", + invalid: true, + invalidText: "Invalid selection", + }, + }); + + const button = screen.getByRole("button"); + expect(button).toBeEnabled(); + expect(button).toHaveTextContent("Slack"); + expect(button.closest(".bx--dropdown")).toHaveAttribute( + "data-invalid", + "true", + ); + expect(screen.getByText("Invalid selection")).toBeInTheDocument(); + }); + + it("should handle warning state", () => { + render(Dropdown, { + props: { + items, + selectedId: "0", + warn: true, + warnText: "Warning message", + }, + }); + + const button = screen.getByRole("button"); + expect(button).toBeEnabled(); + expect(button).toHaveTextContent("Slack"); + expect(button.closest(".bx--dropdown")).toHaveClass( + "bx--dropdown--warning", + ); + expect(screen.getByText("Warning message")).toBeInTheDocument(); + }); + + it("should handle disabled state", () => { + render(Dropdown, { + props: { + items, + selectedId: "0", + disabled: true, + }, + }); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + expect(screen.getByRole("button")).toHaveAttribute("disabled"); + expect(screen.getByRole("button")).toHaveTextContent("Slack"); + }); + + it("should handle helper text", () => { + render(Dropdown, { + props: { + items, + selectedId: "0", + helperText: "Help text", + }, + }); + + expect(screen.getByText("Help text")).toHaveClass("bx--form__helper-text"); + }); + + it("should handle item selection", async () => { + const { component } = render(Dropdown, { + props: { + items, + selectedId: "0", + }, + }); + + const selectHandler = vi.fn(); + component.$on("select", selectHandler); + + const button = screen.getByRole("button"); + await user.click(button); + + const menuItemText = screen.getByText("Email"); + const menuItem = menuItemText.closest(".bx--list-box__menu-item"); + expect(menuItem).not.toBeNull(); + await user.click(menuItem!); + + expect(selectHandler).toHaveBeenCalledWith( + expect.objectContaining({ + detail: { selectedId: "1", selectedItem: items[1] }, + }), + ); + }); + + it("should handle keyboard navigation", async () => { + render(Dropdown, { + props: { + items, + selectedId: "0", + }, + }); + + const button = screen.getByRole("button"); + await user.tab(); + expect(button).toHaveFocus(); + + await user.keyboard("{Enter}"); + expect(screen.getByRole("listbox")).toBeVisible(); + expect(screen.getByRole("option", { selected: true })).toHaveTextContent( + "Slack", + ); + + await user.keyboard("{ArrowDown}{ArrowDown}"); + await user.keyboard("{Enter}"); + + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + expect(button).toHaveTextContent("Email"); + }); + + it("should handle disabled items", async () => { + const itemsWithDisabled = [ + { id: "0", text: "Slack" }, + { id: "1", text: "Email", disabled: true }, + { id: "2", text: "Fax" }, + ]; + + render(Dropdown, { + props: { + items: itemsWithDisabled, + selectedId: "0", + }, + }); + + const button = screen.getByRole("button"); + await user.click(button); + + const menuItemText = screen.getByText("Email"); + const menuItem = menuItemText.closest(".bx--list-box__menu-item"); + expect(menuItem).not.toBeNull(); + expect(menuItem).toHaveAttribute("disabled"); + }); + + it("should handle custom slot content", async () => { + render(DropdownSlot); + + await user.click(screen.getByRole("button")); + + const customItems = screen.getAllByTestId("custom-item"); + expect(customItems).toHaveLength(3); + expect(customItems[0]).toHaveTextContent("Item 1: Option 1"); + expect(customItems[1]).toHaveTextContent("Item 2: Option 2"); + expect(customItems[2]).toHaveTextContent("Item 3: Option 3"); + }); + + it("should close on outside click", async () => { + render(Dropdown, { + props: { + items, + selectedId: "0", + }, + }); + + await user.click(screen.getByRole("button")); + expect(screen.getByRole("listbox")).toBeVisible(); + + await user.click(document.body); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + }); +}); diff --git a/tests/Dropdown/DropdownSlot.test.svelte b/tests/Dropdown/DropdownSlot.test.svelte new file mode 100644 index 00000000..7be8caad --- /dev/null +++ b/tests/Dropdown/DropdownSlot.test.svelte @@ -0,0 +1,25 @@ + + + + + Item {index + 1}: {item.text} + + From f200dadb97603998f13b897bd863627d2ff61f83 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 16:42:13 -0700 Subject: [PATCH 018/181] test(multi-select): more unit tests --- tests/MultiSelect/MultiSelect.test.ts | 367 +++++++++++++++++++++----- 1 file changed, 297 insertions(+), 70 deletions(-) diff --git a/tests/MultiSelect/MultiSelect.test.ts b/tests/MultiSelect/MultiSelect.test.ts index aed26b4a..7d2b8fae 100644 --- a/tests/MultiSelect/MultiSelect.test.ts +++ b/tests/MultiSelect/MultiSelect.test.ts @@ -2,7 +2,13 @@ import { render, screen } from "@testing-library/svelte"; import { MultiSelect } from "carbon-components-svelte"; import { user } from "../setup-tests"; -describe("MultiSelect sorts items correctly", () => { +const items = [ + { id: "0", text: "Slack" }, + { id: "1", text: "Email" }, + { id: "2", text: "Fax" }, +] as const; + +describe("MultiSelect", () => { /** Opens the dropdown. */ const openMenu = async () => await user.click( @@ -29,96 +35,317 @@ describe("MultiSelect sorts items correctly", () => { const nthRenderedOptionText = (index: number) => screen.queryAllByRole("option").at(index)?.textContent?.trim(); - it("initially sorts items alphabetically", async () => { - render(MultiSelect, { - items: [ - { id: "1", text: "C" }, - { id: "3", text: "A" }, - { id: "2", text: "B" }, - ], + describe("sorting behavior", () => { + it("initially sorts items alphabetically", async () => { + render(MultiSelect, { + items: [ + { id: "1", text: "C" }, + { id: "3", text: "A" }, + { id: "2", text: "B" }, + ], + }); + + // Initially, items should be sorted alphabetically. + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + expect(nthRenderedOptionText(1)).toBe("B"); + expect(nthRenderedOptionText(2)).toBe("C"); }); - await openMenu(); - expect(nthRenderedOptionText(0)).toBe("A"); - expect(nthRenderedOptionText(1)).toBe("B"); - expect(nthRenderedOptionText(2)).toBe("C"); + it("immediately moves selected items to the top (with selectionFeedback: top)", async () => { + render(MultiSelect, { + items: [ + { id: "3", text: "C" }, + { id: "1", text: "A" }, + { id: "2", text: "B" }, + ], + selectionFeedback: "top", + }); + + // Initially, items should be sorted alphabetically. + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + + await toggleOption("C"); + expect(nthRenderedOptionText(0)).toBe("C"); + + await toggleOption("C"); + expect(nthRenderedOptionText(0)).toBe("A"); + }); + + it("sorts newly-toggled items only after the dropdown is reoponed (with selectionFeedback: top-after-reopen)", async () => { + render(MultiSelect, { + items: [ + { id: "3", text: "C" }, + { id: "1", text: "A" }, + { id: "2", text: "B" }, + ], + }); + + // Initially, items should be sorted alphabetically. + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + + // While the menu is still open, a newly-selected item should not move. + await toggleOption("C"); + expect(nthRenderedOptionText(0)).toBe("A"); + + // The newly-selected item should move after the menu is closed and + // re-opened. + await closeMenu(); + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("C"); + + // A deselected item should not move while the dropdown is still open. + await toggleOption("C"); + expect(nthRenderedOptionText(0)).toBe("C"); + + // The deselected item should move after closing and re-opening the dropdown. + await closeMenu(); + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + }); + + it("never moves selected items to the top (with selectionFeedback: fixed)", async () => { + render(MultiSelect, { + items: [ + { id: "3", text: "C" }, + { id: "1", text: "A" }, + { id: "2", text: "B" }, + ], + selectionFeedback: "fixed", + }); + + // Items should be sorted alphabetically. + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + + // A newly-selected item should not move after the selection is made. + await toggleOption("C"); + expect(nthRenderedOptionText(0)).toBe("A"); + + // The newly-selected item also shouldn’t move after the dropdown is closed + // and reopened. + await closeMenu(); + await openMenu(); + expect(nthRenderedOptionText(0)).toBe("A"); + }); }); - it("immediately moves selected items to the top (with selectionFeedback: top)", async () => { - render(MultiSelect, { - items: [ - { id: "3", text: "C" }, - { id: "1", text: "A" }, - { id: "2", text: "B" }, - ], - selectionFeedback: "top", + describe("filtering behavior", () => { + it("should filter items based on input value", async () => { + render(MultiSelect, { + items, + filterable: true, + placeholder: "Filter items...", + }); + + await openMenu(); + const input = screen.getByPlaceholderText("Filter items..."); + await user.type(input, "em"); + + expect(screen.queryByText("Slack")).not.toBeInTheDocument(); + expect(screen.getByText("Email")).toBeInTheDocument(); + expect(screen.queryByText("Fax")).not.toBeInTheDocument(); }); - await openMenu(); - expect(nthRenderedOptionText(0)).toBe("A"); + it("should use custom filter function", async () => { + render(MultiSelect, { + items, + filterable: true, + filterItem: (item, value) => + item.text.toLowerCase().startsWith(value.toLowerCase()), + }); - await toggleOption("C"); - expect(nthRenderedOptionText(0)).toBe("C"); + await openMenu(); + const input = screen.getByRole("combobox"); + await user.type(input, "e"); - await toggleOption("C"); - expect(nthRenderedOptionText(0)).toBe("A"); + expect(screen.queryByText("Slack")).not.toBeInTheDocument(); + expect(screen.getByText("Email")).toBeInTheDocument(); + expect(screen.queryByText("Fax")).not.toBeInTheDocument(); + }); + + // TODO(bug): ListBoxSelection aria-labels should be user-friendly + it.skip("should clear filter on selection clear", async () => { + render(MultiSelect, { + items, + filterable: true, + selectedIds: ["0"], + }); + + const clearButton = screen.getByLabelText("Clear all"); + await user.click(clearButton); + + const input = screen.getByRole("combobox"); + expect(input).toHaveValue(""); + }); }); - it("sorts newly-toggled items only after the dropdown is reoponed (with selectionFeedback: top-after-reopen)", async () => { - render(MultiSelect, { - items: [ - { id: "3", text: "C" }, - { id: "1", text: "A" }, - { id: "2", text: "B" }, - ], + describe("keyboard navigation", () => { + it("should handle arrow keys for navigation", async () => { + render(MultiSelect, { items }); + + await openMenu(); + await user.keyboard("{ArrowDown}"); + + const options = screen.getAllByRole("option"); + expect(options[0]).toHaveClass("bx--list-box__menu-item--highlighted"); }); - // Initially, items should be sorted alphabetically. - await openMenu(); - expect(nthRenderedOptionText(0)).toBe("A"); + it("should select item with Enter key", async () => { + const { component } = render(MultiSelect, { items }); + const selectHandler = vi.fn(); + component.$on("select", selectHandler); - // While the menu is still open, a newly-selected item should not move. - await toggleOption("C"); - expect(nthRenderedOptionText(0)).toBe("A"); + await openMenu(); + await user.keyboard("{ArrowDown}"); + await user.keyboard("{Enter}"); - // The newly-selected item should move after the menu is closed and - // re-opened. - await closeMenu(); - await openMenu(); - expect(nthRenderedOptionText(0)).toBe("C"); + expect(selectHandler).toHaveBeenCalled(); + }); - // A deselected item should not move while the dropdown is still open. - await toggleOption("C"); - expect(nthRenderedOptionText(0)).toBe("C"); + it("should close menu with Escape key", async () => { + render(MultiSelect, { items }); - // The deselected item should move after closing and re-opening the dropdown. - await closeMenu(); - await openMenu(); - expect(nthRenderedOptionText(0)).toBe("A"); + await openMenu(); + await user.keyboard("{Escape}"); + + const button = screen.getByRole("button"); + expect(button).toHaveAttribute("aria-expanded", "false"); + }); }); - it("never moves selected items to the top (with selectionFeedback: fixed)", async () => { - render(MultiSelect, { - items: [ - { id: "3", text: "C" }, - { id: "1", text: "A" }, - { id: "2", text: "B" }, - ], - selectionFeedback: "fixed", + describe("accessibility", () => { + it("should handle hidden label", () => { + render(MultiSelect, { + items, + titleText: "Contact methods", + hideLabel: true, + }); + + const label = screen.getByText("Contact methods"); + expect(label).toHaveClass("bx--visually-hidden"); }); - // Items should be sorted alphabetically. - await openMenu(); - expect(nthRenderedOptionText(0)).toBe("A"); + it("should handle custom aria-label", async () => { + render(MultiSelect, { + items, + "aria-label": "Custom label", + }); - // A newly-selected item should not move after the selection is made. - await toggleOption("C"); - expect(nthRenderedOptionText(0)).toBe("A"); + await openMenu(); + const menu = screen.getByLabelText("Custom label"); + expect(menu).toBeInTheDocument(); + }); + }); - // The newly-selected item also shouldn’t move after the dropdown is closed - // and reopened. - await closeMenu(); - await openMenu(); - expect(nthRenderedOptionText(0)).toBe("A"); + describe("variants and states", () => { + it("should render in light variant", async () => { + render(MultiSelect, { + items, + light: true, + }); + + await openMenu(); + const listBox = screen.getByRole("listbox").closest(".bx--list-box"); + expect(listBox).toHaveClass("bx--list-box--light"); + }); + + it("should render in inline variant", () => { + render(MultiSelect, { + items, + type: "inline", + }); + + const wrapper = screen + .getByRole("button") + .closest(".bx--multi-select__wrapper"); + expect(wrapper).toHaveClass("bx--multi-select__wrapper--inline"); + }); + + it("should handle invalid state", () => { + render(MultiSelect, { + items, + invalid: true, + invalidText: "Invalid selection", + }); + + expect(screen.getByText("Invalid selection")).toBeInTheDocument(); + const wrapper = screen.getByRole("button").closest(".bx--list-box"); + expect(wrapper).toHaveClass("bx--multi-select--invalid"); + }); + + it("should handle warning state", () => { + render(MultiSelect, { + items, + warn: true, + warnText: "Warning message", + }); + + expect(screen.getByText("Warning message")).toBeInTheDocument(); + const wrapper = screen.getByRole("button").closest(".bx--list-box"); + expect(wrapper).toHaveClass("bx--list-box--warning"); + }); + + it("should handle disabled state", () => { + render(MultiSelect, { items, disabled: true }); + + const field = screen.getByRole("button"); + expect(field).toHaveAttribute("aria-disabled", "true"); + expect(field).toHaveAttribute("tabindex", "-1"); + expect(field.closest(".bx--multi-select")).toHaveAttribute( + "tabindex", + "-1", + ); + }); + + it("should handle disabled items", async () => { + const itemsWithDisabled = [ + { id: "0", text: "Slack" }, + { id: "1", text: "Email", disabled: true }, + { id: "2", text: "Fax" }, + ]; + + render(MultiSelect, { + items: itemsWithDisabled, + }); + + await openMenu(); + const emailOption = screen + .getByText("Email") + .closest(".bx--list-box__menu-item"); + expect(emailOption).toHaveAttribute("disabled"); + }); + }); + + describe("custom formatting", () => { + it("should handle custom itemToString", () => { + render(MultiSelect, { + items, + selectedIds: ["0"], + itemToString: (item) => `${item.text} (${item.id})`, + }); + + expect(screen.getByText("Slack (0)")).toBeInTheDocument(); + }); + + it("should handle custom itemToInput", async () => { + render(MultiSelect, { + items, + itemToInput: (item) => ({ + name: `contact_${item.id}`, + value: item.text.toLowerCase(), + }), + }); + + await openMenu(); + const checkbox = screen.getByText("Slack"); + const checkboxWrapper = checkbox.closest(".bx--checkbox-wrapper"); + assert(checkboxWrapper); + + const checkboxInput = checkboxWrapper.querySelector("input"); + expect(checkboxInput).toHaveAttribute("name", "contact_0"); + }); }); }); From 1478486d8ff20ec505f43fbaa435897dbc40fe19 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 16:54:42 -0700 Subject: [PATCH 019/181] test(password-input): add unit tests --- tests/PasswordInput.test.svelte | 30 --- tests/PasswordInput/PasswordInput.test.svelte | 66 ++++++ tests/PasswordInput/PasswordInput.test.ts | 196 ++++++++++++++++++ 3 files changed, 262 insertions(+), 30 deletions(-) delete mode 100644 tests/PasswordInput.test.svelte create mode 100644 tests/PasswordInput/PasswordInput.test.svelte create mode 100644 tests/PasswordInput/PasswordInput.test.ts diff --git a/tests/PasswordInput.test.svelte b/tests/PasswordInput.test.svelte deleted file mode 100644 index d690c514..00000000 --- a/tests/PasswordInput.test.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/tests/PasswordInput/PasswordInput.test.svelte b/tests/PasswordInput/PasswordInput.test.svelte new file mode 100644 index 00000000..2f451be2 --- /dev/null +++ b/tests/PasswordInput/PasswordInput.test.svelte @@ -0,0 +1,66 @@ + + + { + console.log("focus"); + }} + on:blur={() => { + console.log("blur"); + }} + on:paste +/> + +
{value}
diff --git a/tests/PasswordInput/PasswordInput.test.ts b/tests/PasswordInput/PasswordInput.test.ts new file mode 100644 index 00000000..33605239 --- /dev/null +++ b/tests/PasswordInput/PasswordInput.test.ts @@ -0,0 +1,196 @@ +import { render, screen } from "@testing-library/svelte"; +import PasswordInput from "./PasswordInput.test.svelte"; +import { user } from "../setup-tests"; + +describe("PasswordInput", () => { + describe("Default", () => { + it("should render with a label", () => { + render(PasswordInput, { + labelText: "Password", + placeholder: "Enter password...", + }); + + expect(screen.getByLabelText("Password")).toBeInTheDocument(); + expect( + screen.getByPlaceholderText("Enter password..."), + ).toBeInTheDocument(); + }); + + it("should toggle password visibility", async () => { + render(PasswordInput, { labelText: "Password", value: "secret123" }); + + const input = screen.getByLabelText("Password"); + expect(input).toHaveAttribute("type", "password"); + + await user.click(screen.getByText("Show password")); + expect(input).toHaveAttribute("type", "text"); + + await user.click(screen.getByText("Hide password")); + expect(input).toHaveAttribute("type", "password"); + }); + + it("should handle custom visibility labels", async () => { + render(PasswordInput, { + labelText: "Password", + hidePasswordLabel: "Custom hide", + showPasswordLabel: "Custom show", + }); + + expect(screen.getByLabelText("Password")).toBeInTheDocument(); + await user.click(screen.getByText("Custom show")); + expect(screen.getByText("Custom hide")).toBeInTheDocument(); + await user.click(screen.getByText("Custom hide")); + expect(screen.getByText("Custom show")).toBeInTheDocument(); + }); + }); + + describe("Tooltip", () => { + it("should handle custom tooltip alignment", () => { + render(PasswordInput, { + labelText: "Password", + tooltipAlignment: "start", + tooltipPosition: "left", + }); + + const button = screen.getByRole("button"); + expect(button).toHaveClass("bx--tooltip--align-start"); + expect(button).toHaveClass("bx--tooltip--left"); + }); + }); + + describe("States", () => { + it("should handle invalid state", () => { + render(PasswordInput, { + labelText: "Password", + invalid: true, + invalidText: "Password must be at least 8 characters", + }); + + expect( + screen.getByText("Password must be at least 8 characters"), + ).toBeInTheDocument(); + const wrapper = screen + .getByLabelText("Password") + .closest(".bx--text-input__field-wrapper"); + expect(wrapper).toHaveAttribute("data-invalid"); + }); + + it("should handle warning state", () => { + render(PasswordInput, { + labelText: "Password", + warn: true, + warnText: "Password will expire soon", + }); + + expect(screen.getByText("Password will expire soon")).toBeInTheDocument(); + const input = screen.getByLabelText("Password"); + expect(input).toHaveClass("bx--text-input--warning"); + }); + + it("should handle disabled state", () => { + render(PasswordInput, { + labelText: "Password", + disabled: true, + value: "disabled-password", + }); + + const input = screen.getByLabelText("Password"); + expect(input).toBeDisabled(); + expect(input).toHaveValue("disabled-password"); + + const toggleButton = screen.getByRole("button"); + expect(toggleButton).toBeDisabled(); + expect(toggleButton).toHaveClass("bx--btn--disabled"); + }); + + it("should handle helper text", () => { + render(PasswordInput, { + labelText: "Password", + helperText: "Your password should be hard to guess", + }); + + expect( + screen.getByText("Your password should be hard to guess"), + ).toBeInTheDocument(); + }); + }); + + describe("Variants", () => { + it("should render light variant", () => { + render(PasswordInput, { labelText: "Password", light: true }); + + const wrapper = screen + .getByLabelText("Password") + .closest(".bx--text-input-wrapper"); + expect(wrapper).toHaveClass("bx--text-input-wrapper--light"); + }); + + it("should render inline variant", () => { + render(PasswordInput, { + labelText: "Password", + inline: true, + }); + + const wrapper = screen + .getByLabelText("Password") + .closest(".bx--text-input-wrapper"); + expect(wrapper).toHaveClass("bx--text-input-wrapper--inline"); + }); + + it("should render in small size", () => { + render(PasswordInput, { labelText: "Password", size: "sm" }); + + const input = screen.getByLabelText("Password"); + expect(input).toHaveClass("bx--text-input--sm"); + }); + + it("should render in extra-large size", () => { + render(PasswordInput, { + labelText: "Password", + size: "xl", + }); + + const input = screen.getByLabelText("Password"); + expect(input).toHaveClass("bx--text-input--xl"); + }); + }); + + describe("Label handling", () => { + it("should handle hidden label", () => { + render(PasswordInput, { labelText: "Password", hideLabel: true }); + + const label = screen.getByText("Password"); + expect(label).toHaveClass("bx--visually-hidden"); + }); + + it("should handle custom id", () => { + render(PasswordInput, { labelText: "Password", id: "custom-id" }); + + const input = screen.getByLabelText("Password"); + expect(input).toHaveAttribute("id", "custom-id"); + }); + }); + + describe("Events", () => { + it("should handle input events", async () => { + render(PasswordInput, { labelText: "Password" }); + + const input = screen.getByLabelText("Password"); + await user.type(input, "test123"); + expect(screen.getByTestId("value")).toHaveTextContent("test123"); + }); + + it("should handle focus and blur events", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(PasswordInput, { labelText: "Password" }); + + expect(consoleLog).not.toHaveBeenCalled(); + const input = screen.getByLabelText("Password"); + await user.click(input); + expect(consoleLog).toHaveBeenCalledWith("focus"); + + await user.tab(); + expect(consoleLog).toHaveBeenCalledWith("blur"); + }); + }); +}); From f7ac0e3f224317ef8664be718bc6ff75ce37e821 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 17:05:24 -0700 Subject: [PATCH 020/181] test(progress-indicator): add unit tests --- tests/ProgressIndicator.test.svelte | 83 ------ .../ProgressIndicator.test.svelte | 35 +++ .../ProgressIndicator.test.ts | 247 ++++++++++++++++++ 3 files changed, 282 insertions(+), 83 deletions(-) delete mode 100644 tests/ProgressIndicator.test.svelte create mode 100644 tests/ProgressIndicator/ProgressIndicator.test.svelte create mode 100644 tests/ProgressIndicator/ProgressIndicator.test.ts diff --git a/tests/ProgressIndicator.test.svelte b/tests/ProgressIndicator.test.svelte deleted file mode 100644 index 0f1e5cc7..00000000 --- a/tests/ProgressIndicator.test.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/ProgressIndicator/ProgressIndicator.test.svelte b/tests/ProgressIndicator/ProgressIndicator.test.svelte new file mode 100644 index 00000000..4f8a4d06 --- /dev/null +++ b/tests/ProgressIndicator/ProgressIndicator.test.svelte @@ -0,0 +1,35 @@ + + + { + console.log("change", e.detail); + }} +> + {#each steps as step} + + {/each} + diff --git a/tests/ProgressIndicator/ProgressIndicator.test.ts b/tests/ProgressIndicator/ProgressIndicator.test.ts new file mode 100644 index 00000000..649d37cb --- /dev/null +++ b/tests/ProgressIndicator/ProgressIndicator.test.ts @@ -0,0 +1,247 @@ +import { render, screen } from "@testing-library/svelte"; +import ProgressIndicator from "./ProgressIndicator.test.svelte"; +import { user } from "../setup-tests"; + +describe("ProgressIndicator", () => { + describe("Default (horizontal)", () => { + it("should render steps with correct states", () => { + render(ProgressIndicator, { + currentIndex: 2, + steps: [ + { label: "Step 1", description: "First step", complete: true }, + { label: "Step 2", description: "Second step", complete: true }, + { label: "Step 3", description: "Third step", complete: true }, + { label: "Step 4", description: "Fourth step", complete: false }, + ], + }); + + const listItems = screen.getAllByRole("listitem"); + + // Check if all steps are rendered + expect(listItems).toHaveLength(4); + + // Check completed steps + const completedSteps = listItems.filter((step) => + step.classList.contains("bx--progress-step--complete"), + ); + expect(completedSteps).toHaveLength(3); + + // Check current step + expect(listItems[2]).toHaveTextContent("Step 3"); + + // Check incomplete step + const incompleteStep = screen.getByText("Step 4"); + expect(incompleteStep).toBeInTheDocument(); + expect(incompleteStep.closest("li")).not.toHaveClass( + "bx--progress-step--complete", + ); + }); + + it("should update currentIndex when clicking on completed steps", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(ProgressIndicator, { + currentIndex: 2, + steps: [ + { label: "Step 1", description: "First step", complete: true }, + { label: "Step 2", description: "Second step", complete: true }, + { label: "Step 3", description: "Third step", complete: true }, + { label: "Step 4", description: "Fourth step", complete: false }, + ], + }); + + expect(consoleLog).not.toHaveBeenCalled(); + + // Click on a completed step + await user.click(screen.getByText("Step 1")); + expect(consoleLog).toHaveBeenCalledWith("change", 0); + }); + + it("should not update currentIndex when preventChangeOnClick is true", async () => { + const { component } = render(ProgressIndicator, { + currentIndex: 2, + preventChangeOnClick: true, + steps: [ + { label: "Step 1", description: "First step", complete: true }, + { label: "Step 2", description: "Second step", complete: true }, + { label: "Step 3", description: "Third step", complete: true }, + { label: "Step 4", description: "Fourth step", complete: false }, + ], + }); + + const changeHandler = vi.fn(); + component.$on("change", changeHandler); + + // Click on a completed step + await user.click(screen.getByText("Step 1")); + expect(changeHandler).not.toHaveBeenCalled(); + }); + }); + + describe("Invalid and disabled states", () => { + it("should render invalid step", () => { + render(ProgressIndicator, { + steps: [ + { label: "Step 1", description: "First step", complete: true }, + { + label: "Step 2", + description: "Second step", + complete: false, + invalid: true, + disabled: false, + }, + { label: "Step 3", description: "Third step", complete: false }, + ], + }); + + const invalidStep = screen.getByText("Step 2").closest("li"); + expect(invalidStep).toHaveClass("bx--progress-step--incomplete"); + }); + + it("should render disabled steps", () => { + render(ProgressIndicator, { + steps: [ + { label: "Step 1", description: "First step", complete: true }, + { + label: "Step 2", + description: "Second step", + complete: false, + invalid: false, + disabled: true, + }, + { + label: "Step 3", + description: "Third step", + complete: false, + invalid: false, + disabled: true, + }, + ], + }); + + const disabledSteps = screen.getAllByRole("listitem").slice(1); + disabledSteps.forEach((step) => { + expect(step).toHaveClass("bx--progress-step--disabled"); + }); + }); + }); + + describe("Variants", () => { + it("should render vertical variant", () => { + render(ProgressIndicator, { + vertical: true, + steps: [ + { label: "Step 1", description: "First step", complete: false }, + { label: "Step 2", description: "Second step", complete: false }, + { label: "Step 3", description: "Third step", complete: false }, + ], + }); + + const progressIndicator = screen.getByRole("list"); + expect(progressIndicator).toHaveClass("bx--progress--vertical"); + }); + + it("should render with equal spacing", () => { + render(ProgressIndicator, { + spaceEqually: true, + steps: [ + { label: "Step 1", description: "First step", complete: false }, + { label: "Step 2", description: "Second step", complete: false }, + { label: "Step 3", description: "Third step", complete: false }, + ], + }); + + const progressIndicator = screen.getByRole("list"); + expect(progressIndicator).toHaveClass("bx--progress--space-equal"); + }); + + it("should not apply equal spacing in vertical variant", () => { + render(ProgressIndicator, { + vertical: true, + spaceEqually: true, + steps: [ + { label: "Step 1", description: "First step", complete: false }, + { label: "Step 2", description: "Second step", complete: false }, + { label: "Step 3", description: "Third step", complete: false }, + ], + }); + + const progressIndicator = screen.getByRole("list"); + expect(progressIndicator).not.toHaveClass("bx--progress--space-equal"); + }); + }); + + describe("Accessibility", () => { + it("should have correct button attributes for different states", () => { + render(ProgressIndicator, { + currentIndex: 1, + steps: [ + { label: "Step 1", description: "First step", complete: true }, + { label: "Step 2", description: "Second step", complete: false }, + { label: "Step 3", description: "Third step", complete: false }, + ], + }); + + const buttons = screen.getAllByRole("button"); + + // Complete step button should be clickable + expect(buttons[0]).toHaveAttribute("tabindex", "0"); + expect(buttons[0]).toHaveAttribute("aria-disabled", "false"); + expect(buttons[0]).not.toHaveClass( + "bx--progress-step-button--unclickable", + ); + + // Current step button should be unclickable + expect(buttons[1]).toHaveAttribute("tabindex", "-1"); + expect(buttons[1]).toHaveAttribute("aria-disabled", "false"); + expect(buttons[1]).toHaveClass("bx--progress-step-button--unclickable"); + + // Incomplete step button should be unclickable + expect(buttons[2]).toHaveAttribute("tabindex", "0"); + expect(buttons[2]).toHaveAttribute("aria-disabled", "false"); + expect(buttons[2]).not.toHaveClass( + "bx--progress-step-button--unclickable", + ); + }); + + it("should have correct button attributes for disabled state", () => { + render(ProgressIndicator, { + steps: [ + { label: "Step 1", description: "First step", complete: true }, + { + label: "Step 2", + description: "Second step", + complete: false, + disabled: true, + }, + ], + }); + + const disabledButton = screen.getAllByRole("button")[1]; + expect(disabledButton).toHaveAttribute("disabled"); + expect(disabledButton).toHaveAttribute("aria-disabled", "true"); + expect(disabledButton).toHaveAttribute("tabindex", "-1"); + }); + + it("should support keyboard navigation for complete steps", async () => { + const consoleLog = vi.spyOn(console, "log"); + render(ProgressIndicator, { + currentIndex: 1, + steps: [ + { label: "Step 1", description: "First step", complete: true }, + { label: "Step 2", description: "Second step", complete: false }, + ], + }); + + expect(consoleLog).not.toHaveBeenCalled(); + const completeStepButton = screen.getAllByRole("button")[0]; + await user.tab(); + expect(completeStepButton).toHaveFocus(); + + await user.keyboard("{Enter}"); + expect(consoleLog).toHaveBeenCalledWith("change", 0); + + await user.keyboard(" "); + expect(consoleLog).toHaveBeenCalledWith("change", 0); + }); + }); +}); From d45409c7f3245565c117b290718534e828fd164e Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 17:19:33 -0700 Subject: [PATCH 021/181] test(grid): add unit tests --- tests/FullWidthGrid.test.svelte | 12 --- tests/Grid.test.svelte | 12 --- tests/Grid/Grid.test.svelte | 44 ++++++++++ tests/Grid/Grid.test.ts | 137 ++++++++++++++++++++++++++++++++ tests/NarrowGrid.test.svelte | 12 --- 5 files changed, 181 insertions(+), 36 deletions(-) delete mode 100644 tests/FullWidthGrid.test.svelte delete mode 100644 tests/Grid.test.svelte create mode 100644 tests/Grid/Grid.test.svelte create mode 100644 tests/Grid/Grid.test.ts delete mode 100644 tests/NarrowGrid.test.svelte diff --git a/tests/FullWidthGrid.test.svelte b/tests/FullWidthGrid.test.svelte deleted file mode 100644 index 40157f83..00000000 --- a/tests/FullWidthGrid.test.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Column - Column - Column - Column - - diff --git a/tests/Grid.test.svelte b/tests/Grid.test.svelte deleted file mode 100644 index b6a74224..00000000 --- a/tests/Grid.test.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Column - Column - Column - Column - - diff --git a/tests/Grid/Grid.test.svelte b/tests/Grid/Grid.test.svelte new file mode 100644 index 00000000..96cf0199 --- /dev/null +++ b/tests/Grid/Grid.test.svelte @@ -0,0 +1,44 @@ + + +{#if as} + +
+ +
+
+{:else} + + + +{/if} diff --git a/tests/Grid/Grid.test.ts b/tests/Grid/Grid.test.ts new file mode 100644 index 00000000..e9d7be1c --- /dev/null +++ b/tests/Grid/Grid.test.ts @@ -0,0 +1,137 @@ +import { render } from "@testing-library/svelte"; +import Grid from "./Grid.test.svelte"; + +describe("Grid", () => { + describe("Default", () => { + it("should render as a div by default", () => { + const { container } = render(Grid); + const grid = container.querySelector("div.bx--grid"); + expect(grid).toHaveClass("bx--grid"); + }); + + it("should support rest props", () => { + const { container } = render(Grid, { + props: { + "data-testid": "custom-grid", + "aria-label": "Grid layout", + }, + }); + const grid = container.querySelector("[data-testid='custom-grid']"); + expect(grid).toHaveClass("bx--grid"); + expect(grid).toHaveAttribute("aria-label", "Grid layout"); + }); + }); + + it("should render condensed variant", () => { + const { container } = render(Grid, { + props: { condensed: true }, + }); + const grid = container.querySelector("div.bx--grid"); + expect(grid).toHaveClass("bx--grid--condensed"); + }); + + it("should render narrow variant", () => { + const { container } = render(Grid, { + props: { narrow: true }, + }); + const grid = container.querySelector("div.bx--grid"); + expect(grid).toHaveClass("bx--grid--narrow"); + }); + + it("should render full width variant", () => { + const { container } = render(Grid, { + props: { fullWidth: true }, + }); + const grid = container.querySelector("div.bx--grid"); + expect(grid).toHaveClass("bx--grid--full-width"); + }); + + it("should render with no gutter", () => { + const { container } = render(Grid, { + props: { noGutter: true }, + }); + const grid = container.querySelector("div.bx--grid"); + expect(grid).toHaveClass("bx--no-gutter"); + }); + + it("should render with no left gutter", () => { + const { container } = render(Grid, { + props: { noGutterLeft: true }, + }); + const grid = container.querySelector("div.bx--grid"); + expect(grid).toHaveClass("bx--no-gutter--left"); + }); + + it("should render with no right gutter", () => { + const { container } = render(Grid, { + props: { + noGutterRight: true, + }, + }); + const grid = container.querySelector("div.bx--grid"); + expect(grid).toHaveClass("bx--no-gutter--right"); + }); + + it("should render with row padding", () => { + const { container } = render(Grid, { + props: { + padding: true, + }, + }); + const grid = container.querySelector("div.bx--grid"); + expect(grid).toHaveClass("bx--row-padding"); + }); + + it("should render as a custom element using the as prop", () => { + const { container } = render(Grid, { props: { as: true } }); + + const header = container.querySelector("header"); + expect(header).toHaveClass("bx--grid"); + }); + + it("should pass all variant classes and rest props to custom element", () => { + const { container } = render(Grid, { + props: { + as: true, + condensed: true, + narrow: true, + fullWidth: true, + noGutter: true, + padding: true, + "data-testid": "custom-header", + "aria-label": "Custom header grid", + }, + }); + const header = container.querySelector("[data-testid='custom-header']"); + expect(header).toHaveClass( + "bx--grid", + "bx--grid--condensed", + "bx--grid--narrow", + "bx--grid--full-width", + "bx--no-gutter", + "bx--row-padding", + ); + expect(header).toHaveAttribute("aria-label", "Custom header grid"); + }); + + it("should combine multiple variant classes", () => { + const { container } = render(Grid, { + props: { + condensed: true, + narrow: true, + noGutterLeft: true, + noGutterRight: true, + padding: true, + }, + }); + const grid = container.querySelector("div.bx--grid"); + expect(grid).toHaveClass( + "bx--grid", + "bx--grid--condensed", + "bx--grid--narrow", + "bx--no-gutter--left", + "bx--no-gutter--right", + "bx--row-padding", + ); + }); +}); diff --git a/tests/NarrowGrid.test.svelte b/tests/NarrowGrid.test.svelte deleted file mode 100644 index ff498960..00000000 --- a/tests/NarrowGrid.test.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Column - Column - Column - Column - - From 95f6c97a57594222767e7e4ec7783d252fe54644 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 17:08:13 -0700 Subject: [PATCH 022/181] test: remove old files --- tests/CopyableCodeSnippet.test.svelte | 8 ---- tests/DynamicCodeSnippet.test.svelte | 10 ----- tests/FilterableComboBox.test.svelte | 19 ---------- tests/InlineLoadingUx.test.svelte | 54 --------------------------- 4 files changed, 91 deletions(-) delete mode 100644 tests/CopyableCodeSnippet.test.svelte delete mode 100644 tests/DynamicCodeSnippet.test.svelte delete mode 100644 tests/FilterableComboBox.test.svelte delete mode 100644 tests/InlineLoadingUx.test.svelte diff --git a/tests/CopyableCodeSnippet.test.svelte b/tests/CopyableCodeSnippet.test.svelte deleted file mode 100644 index b22b0eb6..00000000 --- a/tests/CopyableCodeSnippet.test.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - copy(code)}>{code} diff --git a/tests/DynamicCodeSnippet.test.svelte b/tests/DynamicCodeSnippet.test.svelte deleted file mode 100644 index 9c7ff1db..00000000 --- a/tests/DynamicCodeSnippet.test.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/tests/FilterableComboBox.test.svelte b/tests/FilterableComboBox.test.svelte deleted file mode 100644 index d4e216b3..00000000 --- a/tests/FilterableComboBox.test.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/tests/InlineLoadingUx.test.svelte b/tests/InlineLoadingUx.test.svelte deleted file mode 100644 index 85ee8031..00000000 --- a/tests/InlineLoadingUx.test.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - - - - {#if state !== "dormant"} - - {:else} - - {/if} - From c6c80d35a90bf8b41b6c18932b6a26f6448e0cb8 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Thu, 20 Mar 2025 17:20:53 -0700 Subject: [PATCH 023/181] test: remove CSS import from set-up file --- tests/setup-tests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/setup-tests.ts b/tests/setup-tests.ts index 8f22aad9..9b36ceec 100644 --- a/tests/setup-tests.ts +++ b/tests/setup-tests.ts @@ -1,7 +1,6 @@ /// import "@testing-library/jest-dom/vitest"; import { userEvent } from "@testing-library/user-event"; -import "../css/all.css"; // Mock scrollIntoView since it's not implemented in JSDOM Element.prototype.scrollIntoView = vi.fn(); From dd1338ffc47926a13e231d4a0f724e923f2219e2 Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Sat, 22 Mar 2025 12:59:16 -0700 Subject: [PATCH 024/181] fix(list-box-selection): fix `aria-label` for clear button (#2134) `ListBoxSelection`, used by `MultiSelect` and `ComboBox`, currently applies the wrong `aria-label` for the clear selection button. It uses the `translateId` (e.g., `"clearAll"`) instead of the user-friendly copy. --- src/ListBox/ListBoxSelection.svelte | 6 ++++-- tests/ComboBox/ComboBox.test.ts | 10 ++++++++++ tests/MultiSelect/MultiSelect.test.ts | 5 ++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/ListBox/ListBoxSelection.svelte b/src/ListBox/ListBoxSelection.svelte index beb800d8..f4192015 100644 --- a/src/ListBox/ListBoxSelection.svelte +++ b/src/ListBox/ListBoxSelection.svelte @@ -48,7 +48,9 @@ $: translationId = selectionCount ? translationIds.clearAll : translationIds.clearSelection; - + $: buttonLabel = + translateWithId?.(translationIds.clearAll) ?? + defaultTranslations[translationIds.clearAll]; $: description = translateWithId?.(translationId) ?? defaultTranslations[translationId]; @@ -79,7 +81,7 @@ } }} {disabled} - aria-label={translationIds.clearAll} + aria-label={buttonLabel} title={description} > diff --git a/tests/ComboBox/ComboBox.test.ts b/tests/ComboBox/ComboBox.test.ts index 2ac7534f..f14fd581 100644 --- a/tests/ComboBox/ComboBox.test.ts +++ b/tests/ComboBox/ComboBox.test.ts @@ -193,6 +193,16 @@ describe("ComboBox", () => { expect(screen.getByRole("listbox")).toHaveClass("bx--list-box--up"); }); + it("should clear filter on selection clear", async () => { + render(ComboBoxCustom, { props: { selectedId: "1" } }); + + const clearButton = screen.getByLabelText("Clear selected item"); + await user.click(clearButton); + + const input = screen.getByRole("textbox"); + expect(input).toHaveValue(""); + }); + it("should programmatically clear selection", async () => { render(ComboBoxCustom, { props: { selectedId: "1" } }); diff --git a/tests/MultiSelect/MultiSelect.test.ts b/tests/MultiSelect/MultiSelect.test.ts index 7d2b8fae..cfd668e6 100644 --- a/tests/MultiSelect/MultiSelect.test.ts +++ b/tests/MultiSelect/MultiSelect.test.ts @@ -166,15 +166,14 @@ describe("MultiSelect", () => { expect(screen.queryByText("Fax")).not.toBeInTheDocument(); }); - // TODO(bug): ListBoxSelection aria-labels should be user-friendly - it.skip("should clear filter on selection clear", async () => { + it("should clear filter on selection clear", async () => { render(MultiSelect, { items, filterable: true, selectedIds: ["0"], }); - const clearButton = screen.getByLabelText("Clear all"); + const clearButton = screen.getByLabelText("Clear all selected items"); await user.click(clearButton); const input = screen.getByRole("combobox"); From 1462e300d69f0cd7ee5476dfe3a7ea892ac8f4ad Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Sat, 22 Mar 2025 13:02:28 -0700 Subject: [PATCH 025/181] fix(radio-button): forward `focus`, `blur` events (#2135) As identified in #2131, `focus` and `blur` events should be forwarded to the underlying `RadioButton` element. --- COMPONENT_INDEX.md | 2 ++ docs/src/COMPONENT_API.json | 10 ++++++++++ src/RadioButton/RadioButton.svelte | 2 ++ tests/RadioButton/RadioButton.test.svelte | 6 ++++++ tests/RadioButton/RadioButton.test.ts | 3 +-- types/RadioButton/RadioButton.svelte.d.ts | 6 +++++- 6 files changed, 26 insertions(+), 3 deletions(-) diff --git a/COMPONENT_INDEX.md b/COMPONENT_INDEX.md index 9a14e927..be19a1ca 100644 --- a/COMPONENT_INDEX.md +++ b/COMPONENT_INDEX.md @@ -2966,6 +2966,8 @@ None. | Event name | Type | Detail | | :--------- | :-------- | :----- | +| focus | forwarded | -- | +| blur | forwarded | -- | | change | forwarded | -- | ## `RadioButtonGroup` diff --git a/docs/src/COMPONENT_API.json b/docs/src/COMPONENT_API.json index d1115787..e4826601 100644 --- a/docs/src/COMPONENT_API.json +++ b/docs/src/COMPONENT_API.json @@ -11389,6 +11389,16 @@ } ], "events": [ + { + "type": "forwarded", + "name": "focus", + "element": "input" + }, + { + "type": "forwarded", + "name": "blur", + "element": "input" + }, { "type": "forwarded", "name": "change", diff --git a/src/RadioButton/RadioButton.svelte b/src/RadioButton/RadioButton.svelte index 6fecf17e..3ce9aa22 100644 --- a/src/RadioButton/RadioButton.svelte +++ b/src/RadioButton/RadioButton.svelte @@ -71,6 +71,8 @@ required={$groupRequired ?? required} {value} class:bx--radio-button={true} + on:focus + on:blur on:change on:change={() => { if (update) { diff --git a/tests/RadioButton/RadioButton.test.svelte b/tests/RadioButton/RadioButton.test.svelte index 166c4b42..59282887 100644 --- a/tests/RadioButton/RadioButton.test.svelte +++ b/tests/RadioButton/RadioButton.test.svelte @@ -26,6 +26,12 @@ {id} {name} {ref} + on:focus={() => { + console.log("focus"); + }} + on:blur={() => { + console.log("blur"); + }} on:change={() => { console.log("change"); }} diff --git a/tests/RadioButton/RadioButton.test.ts b/tests/RadioButton/RadioButton.test.ts index 6103c8d8..3b321c57 100644 --- a/tests/RadioButton/RadioButton.test.ts +++ b/tests/RadioButton/RadioButton.test.ts @@ -91,8 +91,7 @@ describe("RadioButton", () => { expect(consoleLog).toHaveBeenCalledWith("change"); }); - // TODO(bug): forward focus/blur events. - it.skip("should handle focus and blur events", async () => { + it("should handle focus and blur events", async () => { const consoleLog = vi.spyOn(console, "log"); render(RadioButton); diff --git a/types/RadioButton/RadioButton.svelte.d.ts b/types/RadioButton/RadioButton.svelte.d.ts index bc2e94d3..6933bef7 100644 --- a/types/RadioButton/RadioButton.svelte.d.ts +++ b/types/RadioButton/RadioButton.svelte.d.ts @@ -71,6 +71,10 @@ export type RadioButtonProps = Omit<$RestProps, keyof $Props> & $Props; export default class RadioButton extends SvelteComponentTyped< RadioButtonProps, - { change: WindowEventMap["change"] }, + { + focus: WindowEventMap["focus"]; + blur: WindowEventMap["blur"]; + change: WindowEventMap["change"]; + }, { labelText: {} } > {} From ca9beebaeac7eaed8079c010a86a78926b00147f Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Sat, 22 Mar 2025 13:03:20 -0700 Subject: [PATCH 026/181] fix(radio-tile): allow standalone `RadioTile` usage (#2136) Although `RadioTile` is meant to be used inside a `TileGroup`, it feels unpolished for standalone usage to crash due to a missing parent context. This fixes `RadioTile` to fail open by providing a no-op `add: () => {}` when `TileGroup` context is not found. --- src/Tile/RadioTile.svelte | 1 + tests/RadioTile/RadioTile.test.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tile/RadioTile.svelte b/src/Tile/RadioTile.svelte index 5e960c89..8ded1870 100644 --- a/src/Tile/RadioTile.svelte +++ b/src/Tile/RadioTile.svelte @@ -36,6 +36,7 @@ const { add, update, selectedValue, groupName, groupRequired } = getContext( "TileGroup", ) ?? { + add: () => {}, groupName: readable(undefined), groupRequired: readable(undefined), selectedValue: readable(checked ? value : undefined), diff --git a/tests/RadioTile/RadioTile.test.ts b/tests/RadioTile/RadioTile.test.ts index 351c01dc..9697654a 100644 --- a/tests/RadioTile/RadioTile.test.ts +++ b/tests/RadioTile/RadioTile.test.ts @@ -94,8 +94,7 @@ describe("RadioTile", () => { expect(radioTileLabel).toHaveAttribute("for", "custom-id"); }); - // TODO(bug): support standalone radio tile. - it.skip("should handle custom name", () => { + it("should handle custom name", () => { render(RadioTileSingle); expect(screen.getByRole("radio")).toHaveAttribute("name", "custom-name"); From 43511e09ecf312c1b8e9339856b9d7d0785036de Mon Sep 17 00:00:00 2001 From: Eric Liu Date: Sat, 22 Mar 2025 13:03:52 -0700 Subject: [PATCH 027/181] fix(text-area): allow visually hidden label (#2137) This fixes an accessibility issue with `TextArea`. Currently, if `hideLabel` is `true`, the label is not rendered at all. The expected behavior is that it should be visually hidden while still being available to screen readers. --- src/TextArea/TextArea.svelte | 2 +- tests/TextArea/TextArea.test.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/TextArea/TextArea.svelte b/src/TextArea/TextArea.svelte index 5c54aeb5..77f39419 100644 --- a/src/TextArea/TextArea.svelte +++ b/src/TextArea/TextArea.svelte @@ -71,7 +71,7 @@ on:mouseleave class:bx--form-item={true} > - {#if (labelText || $$slots.labelText) && !hideLabel} + {#if labelText || $$slots.labelText}