From 56ccaf72c92b80f24853e88c3d1aa7a4165c69ea Mon Sep 17 00:00:00 2001 From: Branko Conjic Date: Sat, 11 May 2024 19:21:27 -0400 Subject: [PATCH] refactor: update dependencies and add tests (#72) * chore: update dependencies * chore: update dependencies * fix: fix typo * fix(docs): add missing CSS * docs(wedges): add changesets * chore: bump node version * style: fix CSS inconsistencies * refactor: improve jest coverage report * test(wedges): add Alert tests * chore: update dependencies * test(wedges): update AvatarGroup tests * feat(Avatar): add delayMs prop * test(wedges): improve Avatar tests * refactor(Avatar): remove default value * test: improve Badge tests * test(AvatarGroup): improve coverage * test(wedges): improve test cases * fix(Kbd): allow keys to be a string * fix(Kbd): don't render if invalid key * test(Kbd): add Kbd tests * fix(docs): fix type error * test(Tag): refactor tests * test(wedges): set verbose inline * test(Textarea): add tests * test(Toggle): add tests * build: run build * docs: add changesets --- .changeset/fluffy-dogs-hang.md | 5 + .changeset/fuzzy-spoons-lay.md | 5 + .changeset/large-apes-trade.md | 5 + .changeset/silent-books-tie.md | 5 + .changeset/silver-lobsters-double.md | 5 + .changeset/soft-jobs-add.md | 5 + .changeset/sour-walls-hammer.md | 5 + .changeset/stale-rings-raise.md | 5 + .nvmrc | 2 +- CONTRIBUTING.md | 4 +- apps/docs/src/examples/index.ts | 2 +- apps/docs/src/examples/tag/preview.tsx | 2 +- apps/docs/src/styles/docsearch.css | 2 +- jest.config.js | 3 +- package.json | 11 +- .../src/components/Alert/Alert.test.tsx | 73 +++ .../src/components/Avatar/Avatar.test.tsx | 305 ++++----- .../wedges/src/components/Avatar/Avatar.tsx | 6 +- .../AvatarGroup/AvatarGroup.test.tsx | 82 +-- .../components/AvatarGroup/AvatarGroup.tsx | 2 +- .../src/components/Badge/Badge.test.tsx | 177 +++--- .../src/components/Checkbox/Checkbox.test.tsx | 43 ++ .../CheckboxGroup/CheckboxGroup.test.tsx | 28 + .../wedges/src/components/Kbd/Kbd.test.tsx | 69 +++ packages/wedges/src/components/Kbd/Kbd.tsx | 6 +- .../src/components/Label/Label.test.tsx | 60 ++ .../wedges/src/components/Tag/Tag.test.tsx | 85 +-- .../src/components/Textarea/Textarea.test.tsx | 45 ++ .../src/components/Toggle/Toggle.test.tsx | 54 ++ pnpm-lock.yaml | 585 ++++++++++-------- 30 files changed, 1098 insertions(+), 588 deletions(-) create mode 100644 .changeset/fluffy-dogs-hang.md create mode 100644 .changeset/fuzzy-spoons-lay.md create mode 100644 .changeset/large-apes-trade.md create mode 100644 .changeset/silent-books-tie.md create mode 100644 .changeset/silver-lobsters-double.md create mode 100644 .changeset/soft-jobs-add.md create mode 100644 .changeset/sour-walls-hammer.md create mode 100644 .changeset/stale-rings-raise.md create mode 100644 packages/wedges/src/components/Alert/Alert.test.tsx create mode 100644 packages/wedges/src/components/Checkbox/Checkbox.test.tsx create mode 100644 packages/wedges/src/components/CheckboxGroup/CheckboxGroup.test.tsx create mode 100644 packages/wedges/src/components/Kbd/Kbd.test.tsx create mode 100644 packages/wedges/src/components/Label/Label.test.tsx create mode 100644 packages/wedges/src/components/Textarea/Textarea.test.tsx create mode 100644 packages/wedges/src/components/Toggle/Toggle.test.tsx diff --git a/.changeset/fluffy-dogs-hang.md b/.changeset/fluffy-dogs-hang.md new file mode 100644 index 0000000..23a8613 --- /dev/null +++ b/.changeset/fluffy-dogs-hang.md @@ -0,0 +1,5 @@ +--- +"@lemonsqueezy/wedges": minor +--- + +add Kbd tests diff --git a/.changeset/fuzzy-spoons-lay.md b/.changeset/fuzzy-spoons-lay.md new file mode 100644 index 0000000..bc6b220 --- /dev/null +++ b/.changeset/fuzzy-spoons-lay.md @@ -0,0 +1,5 @@ +--- +"@lemonsqueezy/wedges": minor +--- + +add Textarea tests diff --git a/.changeset/large-apes-trade.md b/.changeset/large-apes-trade.md new file mode 100644 index 0000000..5e78fd2 --- /dev/null +++ b/.changeset/large-apes-trade.md @@ -0,0 +1,5 @@ +--- +"@lemonsqueezy/wedges": patch +--- + +improve Badge tests diff --git a/.changeset/silent-books-tie.md b/.changeset/silent-books-tie.md new file mode 100644 index 0000000..80dffd2 --- /dev/null +++ b/.changeset/silent-books-tie.md @@ -0,0 +1,5 @@ +--- +"@lemonsqueezy/wedges": patch +--- + +improve AvatarGroup test coverage diff --git a/.changeset/silver-lobsters-double.md b/.changeset/silver-lobsters-double.md new file mode 100644 index 0000000..bfa66f1 --- /dev/null +++ b/.changeset/silver-lobsters-double.md @@ -0,0 +1,5 @@ +--- +"@lemonsqueezy/wedges": minor +--- + +add Toggle tests diff --git a/.changeset/soft-jobs-add.md b/.changeset/soft-jobs-add.md new file mode 100644 index 0000000..408fcec --- /dev/null +++ b/.changeset/soft-jobs-add.md @@ -0,0 +1,5 @@ +--- +"@lemonsqueezy/wedges": minor +--- + +add `delayMs` prop for the Avatar component diff --git a/.changeset/sour-walls-hammer.md b/.changeset/sour-walls-hammer.md new file mode 100644 index 0000000..156cf70 --- /dev/null +++ b/.changeset/sour-walls-hammer.md @@ -0,0 +1,5 @@ +--- +"@lemonsqueezy/wedges": minor +--- + +add Avatar tests diff --git a/.changeset/stale-rings-raise.md b/.changeset/stale-rings-raise.md new file mode 100644 index 0000000..33e5503 --- /dev/null +++ b/.changeset/stale-rings-raise.md @@ -0,0 +1,5 @@ +--- +"@lemonsqueezy/wedges": patch +--- + +improve Tag tests diff --git a/.nvmrc b/.nvmrc index 25bf17f..2edeafb 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 \ No newline at end of file +20 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a8c6ca..0ffb57f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,9 @@ In the Wedges project, we utilize a variety of tools to ensure code quality, con - **[ESLint](https://eslint.org/)**: for code linting. Make sure to check and fix any linting issues before submitting your code. -- **[Jest](https://jestjs.io/)**: for testing. We encourage writing tests for new features and bug fixes. +- **[React Testing Library](https://testing-library.com/)**: for testing. We encourage writing tests for new features and bug fixes. + +- **[Jest Framework](https://jestjs.io/)**: for running tests. - **[husky](https://typicode.github.io/husky/#/)**: for Git hooks. It ensures that linters, and other checks are passed before commits and pushes. diff --git a/apps/docs/src/examples/index.ts b/apps/docs/src/examples/index.ts index b1fc233..ea41b38 100644 --- a/apps/docs/src/examples/index.ts +++ b/apps/docs/src/examples/index.ts @@ -3275,7 +3275,7 @@ export function Example() { return ( { + onClose={(e: React.MouseEvent) => { e.preventDefault(); // eslint-disable-next-line no-console alert("Custom onClose callback with preventDefault()"); diff --git a/apps/docs/src/examples/tag/preview.tsx b/apps/docs/src/examples/tag/preview.tsx index 677a99f..410cd2b 100644 --- a/apps/docs/src/examples/tag/preview.tsx +++ b/apps/docs/src/examples/tag/preview.tsx @@ -4,7 +4,7 @@ export default function Example() { return ( { + onClose={(e: React.MouseEvent) => { e.preventDefault(); // eslint-disable-next-line no-console alert("Custom onClose callback with preventDefault()"); diff --git a/apps/docs/src/styles/docsearch.css b/apps/docs/src/styles/docsearch.css index 441a224..46a007e 100644 --- a/apps/docs/src/styles/docsearch.css +++ b/apps/docs/src/styles/docsearch.css @@ -284,7 +284,7 @@ .DocSearch-NoResults .DocSearch-Help { margin-left: 0.75rem; margin-bottom: 0.5rem; - font-family: theme("fontFamily.display"); + font-family: theme("fontFamily.sans"); color: var(--docsearch-heading-color); font-size: 0.875rem; font-weight: 500; diff --git a/jest.config.js b/jest.config.js index e3802b8..53c7953 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,13 +13,12 @@ const config = { }, testEnvironment: "jsdom", collectCoverageFrom: [ - "packages/**/*.tsx", + "packages/wedges/src/components/**/*.tsx", "!packages/**/icons/**/*", // Exclude files in the 'icons' folder ], moduleFileExtensions: ["ts", "tsx", "js", "jsx"], preset: "ts-jest", testMatch: ["**/?(*.)+(test).+(ts|tsx|js)"], - verbose: true, }; export default config; diff --git a/package.json b/package.json index 7b27409..0168c3a 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "format:fix": "prettier --write ./packages/**/src ./apps/** --cache", "lint": "turbo run lint", "lint:fix": "turbo run lint:fix", - "test": "NODE_OPTIONS='--experimental-vm-modules' jest --passWithNoTests", - "test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage --passWithNoTests", + "test": "NODE_OPTIONS='--experimental-vm-modules' jest --passWithNoTests --coverage --coverageReporters=\"text-summary\"", + "test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage --verbose --passWithNoTests", "prepare": "husky", "version": "changeset version", "version:dev": "changeset version --snapshot --no-git-tag --tag dev", @@ -26,12 +26,14 @@ "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "@ianvs/prettier-plugin-sort-imports": "^4.2.1", + "@testing-library/jest-dom": "^6.4.5", "@testing-library/react": "^15.0.7", "@types/jest": "^29.5.12", "@types/node": "^20.12.11", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", "@wedges/eslint-config": "workspace:*", + "clsx": "^2.1.1", "husky": "^9.0.11", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -40,12 +42,11 @@ "react": "^18.3.1", "react-dom": "18.3.1", "rimraf": "^5.0.6", + "tailwind-merge": "^2.3.0", "tailwindcss": "3.4.3", "ts-jest": "^29.1.2", "turbo": "^1.13.3", - "typescript": "^5.4.5", - "tailwind-merge": "^2.3.0", - "clsx": "^2.1.1" + "typescript": "^5.4.5" }, "packageManager": "pnpm@9.1.0", "dependencies": { diff --git a/packages/wedges/src/components/Alert/Alert.test.tsx b/packages/wedges/src/components/Alert/Alert.test.tsx new file mode 100644 index 0000000..9b57087 --- /dev/null +++ b/packages/wedges/src/components/Alert/Alert.test.tsx @@ -0,0 +1,73 @@ +import * as React from "react"; +import { jest } from "@jest/globals"; +import { fireEvent, render, screen } from "@testing-library/react"; + +import "@testing-library/jest-dom"; + +import { Alert } from "."; + +const TEST_TEXT = "Warning!"; + +describe("Alert", () => { + it("should forward ref to the HTMLDivElement", () => { + const ref = React.createRef(); + render({TEST_TEXT}); + + if (ref.current !== null) { + expect(ref.current).toBeInstanceOf(HTMLDivElement); + expect(ref.current.textContent).toBe(TEST_TEXT); + } else { + fail("ref.current is null"); + } + }); + + test("should render alert role", () => { + render({TEST_TEXT}); + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); + + describe("when the close button is clicked", () => { + test("then it should invoke onClose callback", () => { + const handleClose = jest.fn(); + render(); + fireEvent.click(screen.getByLabelText("Close")); + expect(handleClose).toHaveBeenCalled(); + }); + + test("then it should no longer be visible", () => { + const { queryByRole } = render(); + fireEvent.click(screen.getByLabelText("Close")); + expect(queryByRole("alert")).not.toBeInTheDocument(); + }); + }); + + describe("given it has a string title", () => { + test("then it should display the title within an AlertTitle component", () => { + render(); + expect(screen.getByText(TEST_TEXT)).toBeInTheDocument(); + }); + }); + + describe("given it has a component as a title", () => { + test("then it should render the title as-is", () => { + const titleElement = {TEST_TEXT}; + render(); + expect(screen.getByText(TEST_TEXT)).toHaveStyle("font-weight: bold"); + }); + }); + + describe("given it has a custom before and after slot", () => { + test("then it should render these elements correctly", () => { + render(Before} after={} />); + expect(screen.getByText("Before")).toBeInTheDocument(); + expect(screen.getByText("After")).toBeInTheDocument(); + }); + }); + + describe("given children are provided", () => { + test("Then it should render the children inside the Alert", () => { + render({TEST_TEXT}); + expect(screen.getByText(TEST_TEXT)).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/wedges/src/components/Avatar/Avatar.test.tsx b/packages/wedges/src/components/Avatar/Avatar.test.tsx index ebb2431..2bd18d5 100644 --- a/packages/wedges/src/components/Avatar/Avatar.test.tsx +++ b/packages/wedges/src/components/Avatar/Avatar.test.tsx @@ -8,198 +8,201 @@ const DELAY = 80; const FALLBACK_TEXT = "JD"; const IMAGE_ALT_TEXT = "Alt text example"; const IMG_SRC = "test.jpg"; -const TEST_ID = "wg-avatar"; describe("Avatar", () => { - it("should forward ref", () => { + it("should forward ref to HTMLSpanElement", () => { const ref = React.createRef(); - const { getByTestId } = render(); + render(); - expect(getByTestId(TEST_ID)).toBe(ref.current); - }); -}); + if (ref.current !== null) { + expect(ref.current).toBeInstanceOf(HTMLSpanElement); + } else { + fail("ref.current is null"); + } + }); + + describe("given an image", () => { + const orignalGlobalImage = window.Image; + const imageRef = React.createRef(); + let rendered: RenderResult; + + // Mock the Image constructor to return a mock image that will call the onload + beforeAll(() => { + (window.Image as any) = class MockImage { + onload: () => void = () => {}; + src = ""; + constructor() { + setTimeout(() => { + this.onload(); + }, DELAY); + + return this; + } + }; + }); + + afterAll(() => { + window.Image = orignalGlobalImage; + }); + + beforeEach(() => { + rendered = render(); + }); + + it("should render the Lemon Squeezy fallback first", () => { + const fallback = rendered.container.querySelector("svg"); -describe("Given an Avatar with a working image", () => { - const orignalGlobalImage = window.Image; - const imageRef = React.createRef(); - let rendered: RenderResult; - - // Mock the Image constructor to return a mock image that will call the onload - beforeAll(() => { - (window.Image as any) = class MockImage { - onload: () => void = () => {}; - src = ""; - constructor() { - setTimeout(() => { - this.onload(); - }, DELAY); - - return this; - } - }; - }); + expect(fallback).not.toBeNull(); + }); - afterAll(() => { - window.Image = orignalGlobalImage; - }); + it("should render the image after it has loaded", async () => { + const image = await rendered.findByRole("img"); + const imageSrc = image?.getAttribute("src"); - beforeEach(() => { - rendered = render(); - }); + expect(imageSrc).toBe(IMG_SRC); + }); + + it("should pass ref to the image", async () => { + const image = await rendered.findByRole("img"); + + expect(image).toBe(imageRef.current); + }); + }); - it("should render the Lemon Squeezy fallback first", () => { - const fallback = rendered.container.querySelector("svg"); + describe("given an image, alt and initials", () => { + const orignalGlobalImage = window.Image; + let rendered: RenderResult; - expect(fallback).not.toBeNull(); - }); + // Mock the Image constructor to return a mock image that will call the onload + beforeAll(() => { + (window.Image as any) = class MockImage { + onload: () => void = () => {}; + src = ""; + constructor() { + setTimeout(() => { + this.onload(); + }, DELAY); - it("should render the image after it has loaded", async () => { - const image = await rendered.findByRole("img"); - const imageSrc = image?.getAttribute("src"); + return this; + } + }; + }); - expect(imageSrc).toBe(IMG_SRC); - }); + afterAll(() => { + window.Image = orignalGlobalImage; + }); - it("should pass ref to the image", async () => { - const image = await rendered.findByRole("img"); + beforeEach(() => { + rendered = render(); + }); - expect(image).toBe(imageRef.current); - }); -}); - -describe("Given an Avatar with a working image, alt and initials", () => { - const orignalGlobalImage = window.Image; - let rendered: RenderResult; - - // Mock the Image constructor to return a mock image that will call the onload - beforeAll(() => { - (window.Image as any) = class MockImage { - onload: () => void = () => {}; - src = ""; - constructor() { - setTimeout(() => { - this.onload(); - }, DELAY); - - return this; - } - }; - }); - - afterAll(() => { - window.Image = orignalGlobalImage; - }); + it("should render the initials fallback first", () => { + const fallback = rendered.getByText(FALLBACK_TEXT); - beforeEach(() => { - rendered = render(); - }); + expect(fallback).not.toBeNull(); + }); - it("should render the initials fallback first", () => { - const fallback = rendered.getByText(FALLBACK_TEXT); + it("should render the image with alt text after it has loaded", async () => { + const image = await rendered.findByRole("img"); + const imageAlt = image?.getAttribute("alt"); - expect(fallback).not.toBeNull(); + expect(imageAlt).toBe(IMAGE_ALT_TEXT); + }); }); - it("should render the image with alt text after it has loaded", async () => { - const image = await rendered.findByRole("img"); - const imageAlt = image?.getAttribute("alt"); + describe("given an image and children", () => { + const orignalGlobalImage = window.Image; + let rendered: RenderResult; - expect(imageAlt).toBe(IMAGE_ALT_TEXT); - }); -}); + // Mock the Image constructor to return a mock image that will call the onload + beforeAll(() => { + (window.Image as any) = class MockImage { + // eslint-disable-next-line @typescript-eslint/no-empty-function + onload: () => void = () => {}; + src = ""; + constructor() { + setTimeout(() => { + this.onload(); + }, DELAY); -describe("Given an Avatar with a working image and children", () => { - const orignalGlobalImage = window.Image; - let rendered: RenderResult; - - // Mock the Image constructor to return a mock image that will call the onload - beforeAll(() => { - (window.Image as any) = class MockImage { - // eslint-disable-next-line @typescript-eslint/no-empty-function - onload: () => void = () => {}; - src = ""; - constructor() { - setTimeout(() => { - this.onload(); - }, DELAY); - - return this; - } - }; - }); + return this; + } + }; + }); - afterAll(() => { - window.Image = orignalGlobalImage; - }); + afterAll(() => { + window.Image = orignalGlobalImage; + }); - beforeEach(() => { - rendered = render({FALLBACK_TEXT}); - }); + beforeEach(() => { + rendered = render({FALLBACK_TEXT}); + }); - it("should render the children fallback first", () => { - const fallback = rendered.getByText(FALLBACK_TEXT); + it("should render the children fallback first", () => { + const fallback = rendered.getByText(FALLBACK_TEXT); - expect(fallback).not.toBeNull(); - }); + expect(fallback).not.toBeNull(); + }); - it("should render the image after it has loaded", async () => { - const image = await rendered.findByRole("img"); - const imageSrc = image?.getAttribute("src"); + it("should render the image after it has loaded", async () => { + const image = await rendered.findByRole("img"); + const imageSrc = image?.getAttribute("src"); - expect(imageSrc).toBe(IMG_SRC); + expect(imageSrc).toBe(IMG_SRC); + }); }); -}); -describe("Given an Avatar with only children", () => { - let rendered: RenderResult; + describe("given only children", () => { + let rendered: RenderResult; - beforeEach(() => { - rendered = render( - - {FALLBACK_TEXT} - - ); - }); + beforeEach(() => { + rendered = render( + + {FALLBACK_TEXT} + + ); + }); - it("should render the children", () => { - const fallback = rendered.getByText(FALLBACK_TEXT); + it("should render the children", () => { + const fallback = rendered.getByText(FALLBACK_TEXT); - expect(fallback).not.toBeNull(); - }); + expect(fallback).not.toBeNull(); + }); - it("should allow children to have any width", () => { - const root = rendered.container.querySelector(".aspect-auto"); + it("should allow children to have any width", () => { + const root = rendered.container.querySelector(".aspect-auto"); - expect(root).not.toBeNull(); - expect(root?.classList.contains("aspect-auto")).toBe(true); + expect(root).not.toBeNull(); + expect(root?.classList.contains("aspect-auto")).toBe(true); + }); }); -}); -describe("Given an Avatar without image, initilas or children", () => { - it("should render the default fallback", () => { - const { container } = render(); - const fallback = container.querySelector("svg"); + describe("given an Avatar without image, initials or children", () => { + it("should render the default fallback", () => { + const { container } = render(); + const fallback = container.querySelector("svg"); - expect(fallback).not.toBeNull(); + expect(fallback).not.toBeNull(); + }); }); -}); -describe("Given an Avatar with status and notification", () => { - it("should render the status and notification", () => { - const { container } = render(); - const status = container.querySelector(".bg-wg-red"); - const notification = container.querySelector(".bg-wg-green"); + describe("given an Avatar with status and notification", () => { + it("should render the status and notification", () => { + const { container } = render(); + const status = container.querySelector(".bg-wg-red"); + const notification = container.querySelector(".bg-wg-green"); - expect(status).not.toBeNull(); - expect(notification).not.toBeNull(); + expect(status).not.toBeNull(); + expect(notification).not.toBeNull(); + }); }); -}); -describe("Given an Avatar with size", () => { - it("should apply correct size classes", () => { - const { container } = render(); - const size = container.querySelector(".min-w-16"); + describe("given an Avatar with size", () => { + it("should apply correct size classes", () => { + const { container } = render(); + const size = container.querySelector(".min-w-16"); - expect(size).not.toBeNull(); + expect(size).not.toBeNull(); + }); }); }); diff --git a/packages/wedges/src/components/Avatar/Avatar.tsx b/packages/wedges/src/components/Avatar/Avatar.tsx index 53f889d..241dd97 100644 --- a/packages/wedges/src/components/Avatar/Avatar.tsx +++ b/packages/wedges/src/components/Avatar/Avatar.tsx @@ -44,7 +44,9 @@ type BaseAvatarProps = { export type AvatarProps = React.ComponentPropsWithoutRef & BaseAvatarProps & - AvatarVariantProps; + AvatarVariantProps & { + delayMs?: number; + }; /* ------------------------------- Components ------------------------------- */ const AvatarRoot = React.forwardRef< @@ -122,6 +124,7 @@ const AvatarWedges = React.forwardRef((props, ref) = notification, size = "md", src, + delayMs, status, style, ...otherProps @@ -170,6 +173,7 @@ const AvatarWedges = React.forwardRef((props, ref) = { expect(getByTestId(TEST_ID)).toBe(ref.current); }); -}); -describe("Given an AvatarGroup with custom class", () => { - it("should include the custom class name in the class list", () => { - const rendered = render(); - const root = rendered.getByTestId(TEST_ID); + describe("given it has a custom class", () => { + it("should include the custom class name in the class list", () => { + const rendered = render(); + const root = rendered.getByTestId(TEST_ID); - expect(root.classList.contains("text-sm")).toBe(true); + expect(root.classList.contains("text-sm")).toBe(true); + }); }); -}); -describe("Given an AvatarGroup with 'items' prop", () => { - it("should not render any children if 'items' is an empty array", () => { - const { getByTestId } = render(); + describe("given it has `items` prop", () => { + it("should not render any children if 'items' is an empty array", () => { + const { getByTestId } = render(); - expect(getByTestId(TEST_ID).children.length).toBe(0); - }); + expect(getByTestId(TEST_ID).children.length).toBe(0); + }); - it("should render the correct number of children", () => { - const { getByTestId } = render(); + it("should render the correct number of children", () => { + const { getByTestId } = render(); - expect(getByTestId(TEST_ID).children.length).toBe(3); - }); + expect(getByTestId(TEST_ID).children.length).toBe(3); + }); - it("should pass the size to children components when 'size' prop is set", () => { - const rendered = render(); - const root = rendered.getByTestId(TEST_ID); + it("should pass the size to children components when 'size' prop is set", () => { + const rendered = render(); + const root = rendered.getByTestId(TEST_ID); - expect(root.querySelector("span")?.classList.contains("min-w-16")).toBe(true); - }); + expect(root.querySelector("span")?.classList.contains("min-w-16")).toBe(true); + }); - it("should apply the correct z-index on children items based on 'previousOnTop' prop", () => { - const { container } = render( - - ); + it("should apply the correct z-index on children items based on 'previousOnTop' prop", () => { + const { container } = render( + + ); - const item1 = container.querySelector(".item-1"); - const item2 = container.querySelector(".item-2"); + const item1 = container.querySelector(".item-1"); + const item2 = container.querySelector(".item-2"); - expect(item1?.style.zIndex).toBe("2"); - expect(item2?.style.zIndex).toBe("1"); + expect(item1?.style.zIndex).toBe("2"); + expect(item2?.style.zIndex).toBe("1"); + }); + }); + + describe("given it has empty array passed for the `items` prop", () => { + it("should not render any children", () => { + const { getByTestId } = render(); + + expect(getByTestId(TEST_ID).children.length).toBe(0); + }); }); -}); -describe("Given an AvatarGroup with 'moreLabel' prop", () => { - it("should render the label", () => { - const { getByText } = render(); + describe("given an AvatarGroup with 'moreLabel' prop", () => { + it("should render the label", () => { + const { getByText } = render(); - expect(getByText("More")).not.toBeNull(); + expect(getByText("More")).not.toBeNull(); + }); }); }); diff --git a/packages/wedges/src/components/AvatarGroup/AvatarGroup.tsx b/packages/wedges/src/components/AvatarGroup/AvatarGroup.tsx index 47ee951..cfe3f43 100644 --- a/packages/wedges/src/components/AvatarGroup/AvatarGroup.tsx +++ b/packages/wedges/src/components/AvatarGroup/AvatarGroup.tsx @@ -117,7 +117,7 @@ const AvatarGroupWedges = React.forwardRef((pr {...otherProps} > <> - {items + {items.length > 0 ? items.map((item, i) => { const { alt: itemAlt, diff --git a/packages/wedges/src/components/Badge/Badge.test.tsx b/packages/wedges/src/components/Badge/Badge.test.tsx index a799b6f..5889b69 100644 --- a/packages/wedges/src/components/Badge/Badge.test.tsx +++ b/packages/wedges/src/components/Badge/Badge.test.tsx @@ -3,117 +3,124 @@ import { render } from "@testing-library/react"; import { Badge } from "."; +const TEST_TEXT = "Badge"; + describe("Badge", () => { it("should forward ref to the HTMLSpanElement", () => { const ref = React.createRef(); - const { getByTestId } = render(); - - expect(getByTestId("wg-badge")).toBe(ref.current); + render({TEST_TEXT}); + + if (ref.current !== null) { + expect(ref.current).toBeInstanceOf(HTMLSpanElement); + expect(ref.current.textContent).toBe(TEST_TEXT); + } else { + fail("ref.current is null"); + } }); -}); -describe("Given a Badge with color and variant", () => { - it("should apply the correct classes for the specified color and variant", () => { - const rendered = render(); - const root = rendered.getByTestId("wg-badge"); + describe("given it has a `color` and `variant`", () => { + it("should apply the correct classes for the specified `color` and `variant`", () => { + const rendered = render(); + const root = rendered.getByTestId("wg-badge"); - expect(root.classList.contains("wg-bg-wg-green-50")).toBe(true); - expect(root.classList.contains("rounded-full")).toBe(true); + expect(root.classList.contains("wg-bg-wg-green-50")).toBe(true); + expect(root.classList.contains("rounded-full")).toBe(true); + }); }); -}); -describe("Given a Badge with stroke", () => { - it("should apply a stroke class", () => { - const rendered = render(); - const root = rendered.getByTestId("wg-badge"); + describe("given it has a `stroke`", () => { + it("should apply a stroke class", () => { + const rendered = render(); + const root = rendered.getByTestId("wg-badge"); - expect(root.classList.contains("outline")).toBe(true); + expect(root.classList.contains("outline")).toBe(true); + }); }); -}); -describe("Given a Badge with custom class", () => { - it("should include the custom class name in the class list", () => { - const rendered = render(); - const root = rendered.getByTestId("wg-badge"); + describe("given it has a custom class", () => { + it("should include the custom class name in the class list", () => { + const rendered = render(); + const root = rendered.getByTestId("wg-badge"); - expect(root.classList.contains("text-sm")).toBe(true); + expect(root.classList.contains("text-sm")).toBe(true); + }); }); -}); -describe("Given a Badge with 'before', 'children', and 'after' props", () => { - it("should render correctly", () => { - const rendered = render( - After} before={
Before
}> - Children -
- ); - - expect(rendered.getByText("Before")).not.toBeNull(); - expect(rendered.getByText("Children")).not.toBeNull(); - expect(rendered.getByText("After")).not.toBeNull(); + describe("given it has 'before', 'children', and 'after' props", () => { + it("should render correctly", () => { + const rendered = render( + After} before={
Before
}> + Children +
+ ); + + expect(rendered.getByText("Before")).not.toBeNull(); + expect(rendered.getByText("Children")).not.toBeNull(); + expect(rendered.getByText("After")).not.toBeNull(); + }); }); -}); -describe("Given a Badge with only a 'before' prop", () => { - it("should render the 'before' content and not render 'children' or 'after' content", () => { - const rendered = render(Before} data-testid="wg-badge" />); - const root = rendered.getByTestId("wg-badge"); + describe("given it has only a 'before' prop", () => { + it("should render the 'before' content and not render 'children' or 'after' content", () => { + const rendered = render(Before} data-testid="wg-badge" />); + const root = rendered.getByTestId("wg-badge"); - expect(root.children.length).toBe(1); - expect(rendered.getByText("Before")).not.toBeNull(); + expect(root.children.length).toBe(1); + expect(rendered.getByText("Before")).not.toBeNull(); + }); }); -}); -describe("Given a Badge with only an 'after' prop", () => { - it("should render the 'after' content and not render 'children' or 'before' content", () => { - const rendered = render(After} data-testid="wg-badge" />); - const root = rendered.getByTestId("wg-badge"); + describe("given it has only an 'after' prop", () => { + it("should render the 'after' content and not render 'children' or 'before' content", () => { + const rendered = render(After} data-testid="wg-badge" />); + const root = rendered.getByTestId("wg-badge"); - expect(root.children.length).toBe(1); - expect(rendered.getByText("After")).not.toBeNull(); + expect(root.children.length).toBe(1); + expect(rendered.getByText("After")).not.toBeNull(); + }); }); -}); - -describe("Given a Badge with 'before' and 'after' props as React nodes", () => { - it("should render the React nodes correctly in the 'before' and 'after' slots", () => { - const BeforeComponent = () =>
Before Component
; - const AfterComponent = () =>
After Component
; - const { getByText } = render( - } before={}> - Children - - ); - - expect(getByText("Before Component")).not.toBeNull(); - expect(getByText("Children")).not.toBeNull(); - expect(getByText("After Component")).not.toBeNull(); + describe("given it has React nodes passed for 'before' and 'after'", () => { + it("should render the nodes correctly in the 'before' and 'after' slots", () => { + const BeforeComponent = () =>
Before Component
; + const AfterComponent = () =>
After Component
; + + const { getByText } = render( + } before={}> + Children + + ); + + expect(getByText("Before Component")).not.toBeNull(); + expect(getByText("Children")).not.toBeNull(); + expect(getByText("After Component")).not.toBeNull(); + }); }); -}); -describe("Given a Badge with undefined 'before' and 'after' props", () => { - it("should render the 'children' content without 'before' and 'after' content", () => { - const rendered = render( - - Children - - ); - const root = rendered.getByTestId("wg-badge"); - - // Verify that 'before' and 'after' are not rendered - expect(root.children.length).toBe(1); - - // Verify that children are rendered correctly - expect(rendered.getByText("Children")).not.toBeNull(); + describe("given it has undefined set for 'before' and 'after' props", () => { + it("should render the 'children' content without 'before' and 'after' content", () => { + const rendered = render( + + Children + + ); + const root = rendered.getByTestId("wg-badge"); + + // Verify that 'before' and 'after' are not rendered + expect(root.children.length).toBe(1); + + // Verify that children are rendered correctly + expect(rendered.getByText("Children")).not.toBeNull(); + }); }); -}); -describe("Given a Badge with undefined 'children' prop", () => { - it("should render without 'children' content", () => { - const rendered = render(); - const root = rendered.getByTestId("wg-badge"); + describe("given it has undefined passed as 'children' prop", () => { + it("should render without 'children' content", () => { + const rendered = render(); + const root = rendered.getByTestId("wg-badge"); - // Verify that 'before' and 'after' are not rendered - expect(root.children.length).toBe(0); + // Verify that 'before' and 'after' are not rendered + expect(root.children.length).toBe(0); + }); }); }); diff --git a/packages/wedges/src/components/Checkbox/Checkbox.test.tsx b/packages/wedges/src/components/Checkbox/Checkbox.test.tsx new file mode 100644 index 0000000..d80891d --- /dev/null +++ b/packages/wedges/src/components/Checkbox/Checkbox.test.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; + +import "@testing-library/jest-dom"; + +import { fireEvent, render } from "@testing-library/react"; + +import Checkbox from "./Checkbox"; + +const TEST_TEXT = "Checkbox"; +const TEST_ID = "wg-checkbox"; + +describe("Checkbox", () => { + it("should forward ref to the HTMLButtonElement", () => { + const ref = React.createRef(); + const { getByTestId } = render(); + + expect(getByTestId(TEST_ID)).toBe(ref.current); + }); + + it("should render a checkbox role", () => { + const { getByRole } = render(); + expect(getByRole("checkbox")).toBeInTheDocument(); + }); + + it("should render the label", () => { + const { getByText } = render({TEST_TEXT}); + expect(getByText(TEST_TEXT)).toBeInTheDocument(); + }); + + it("should render the description", () => { + const { getByText } = render(); + expect(getByText(TEST_TEXT)).toBeInTheDocument(); + }); + + it("should toggle the checkbox", () => { + const { getByRole } = render(); + const checkbox = getByRole("checkbox"); + + expect(checkbox).not.toBeChecked(); + fireEvent.click(checkbox); + expect(checkbox).toBeChecked(); + }); +}); diff --git a/packages/wedges/src/components/CheckboxGroup/CheckboxGroup.test.tsx b/packages/wedges/src/components/CheckboxGroup/CheckboxGroup.test.tsx new file mode 100644 index 0000000..b2e7a16 --- /dev/null +++ b/packages/wedges/src/components/CheckboxGroup/CheckboxGroup.test.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { render } from "@testing-library/react"; + +import "@testing-library/jest-dom"; + +import CheckboxGroup from "./CheckboxGroup"; + +const TEST_ID = "Warning!"; +const TEST_TEXT = "Checkbox"; + +describe("CheckboxGroup", () => { + it("should forward ref to the HTMLDivElement", () => { + const ref = React.createRef(); + const { getByTestId } = render(); + + expect(getByTestId(TEST_ID)).toBe(ref.current); + }); + + it("should render a group role", () => { + const { getByRole } = render(); + expect(getByRole("group")).toBeInTheDocument(); + }); + + it("should render the label", () => { + const { getByText } = render(); + expect(getByText(TEST_TEXT)).toBeInTheDocument(); + }); +}); diff --git a/packages/wedges/src/components/Kbd/Kbd.test.tsx b/packages/wedges/src/components/Kbd/Kbd.test.tsx new file mode 100644 index 0000000..832e659 --- /dev/null +++ b/packages/wedges/src/components/Kbd/Kbd.test.tsx @@ -0,0 +1,69 @@ +import * as React from "react"; +import { render, screen } from "@testing-library/react"; + +import "@testing-library/jest-dom"; + +import Kbd from "./Kbd"; + +const TEST_TEXT = "K"; +const TEST_ID = "wg-kbd"; + +describe("Kbd", () => { + it("should forward ref to the kbd element", () => { + const ref = React.createRef(); + render({TEST_TEXT}); + + if (ref.current !== null) { + expect(ref.current).toBeInstanceOf(HTMLElement); + expect(ref.current.textContent).toBe(TEST_TEXT); + } else { + fail("ref.current is null"); + } + }); + + it("renders single key correctly", () => { + const { getByTestId, getByTitle } = render(); + expect(getByTestId(TEST_ID)).toBeInTheDocument(); + expect(getByTitle("Command")).toBeInTheDocument(); + }); + + it("renders multiple keys correctly", () => { + const { getByTestId, getByTitle } = render( + + ); + expect(getByTestId(TEST_ID)).toBeInTheDocument(); + expect(getByTitle("Command")).toBeInTheDocument(); + expect(getByTitle("Shift")).toBeInTheDocument(); + }); + + it("renders the children", () => { + render({TEST_TEXT}); + expect(screen.getByText(TEST_TEXT)).toBeInTheDocument(); + }); + + it("should render the correct size", () => { + const { getByTestId } = render( + + {TEST_TEXT} + + ); + expect(getByTestId(TEST_ID)).toHaveClass("text-lg"); + }); + + it("supports custom className", () => { + const { getByTestId } = render(); + expect(getByTestId(TEST_ID)).toHaveClass("class"); + }); + + it("does not render when keys prop is empty", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("does not render an invalid key", () => { + const invalidKey = "InvalidKey"; // A key not present in kbdKeysMap + // @ts-expect-error -- Testing invalid key + render(); + expect(screen.queryByText(invalidKey)).not.toBeInTheDocument(); + }); +}); diff --git a/packages/wedges/src/components/Kbd/Kbd.tsx b/packages/wedges/src/components/Kbd/Kbd.tsx index 2dbeacc..6a748f8 100644 --- a/packages/wedges/src/components/Kbd/Kbd.tsx +++ b/packages/wedges/src/components/Kbd/Kbd.tsx @@ -29,7 +29,7 @@ export type KbdProps = Omit, "size"> & /* ------------------------------- Components ------------------------------- */ const Key = ({ keyName }: { keyName: KbdKey }) => { - const isKey = typeof keyName === "string"; + const isKey = typeof keyName === "string" && keyName in kbdKeysMap; if (!isKey) { return null; @@ -47,9 +47,11 @@ const Kbd = React.forwardRef( return keys.map((k) => ); } - return null; + return ; }; + if ((!keys || keys.length === 0) && !children) return null; + return ( {renderKeys()} diff --git a/packages/wedges/src/components/Label/Label.test.tsx b/packages/wedges/src/components/Label/Label.test.tsx new file mode 100644 index 0000000..2282a45 --- /dev/null +++ b/packages/wedges/src/components/Label/Label.test.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import { render, screen } from "@testing-library/react"; + +import "@testing-library/jest-dom"; + +import { Label } from "."; + +const TEST_TEXT = "Label"; + +describe("Label", () => { + it("should forward ref to the HTMLLabelElement", () => { + const ref = React.createRef(); + render(); + + if (ref.current !== null) { + expect(ref.current).toBeInstanceOf(HTMLLabelElement); + expect(ref.current.textContent).toBe(TEST_TEXT); + } else { + fail("ref.current is null"); + } + }); + + it("renders the label with children", () => { + render(); + expect(screen.getByText(TEST_TEXT)).toBeInTheDocument(); + }); + + it("renders an asterisk when the label is required", () => { + render(); + expect(screen.getByText("*")).toBeInTheDocument(); + }); + + it("does not render an asterisk when the label is not required", () => { + render(); + expect(screen.queryByText("*")).not.toBeInTheDocument(); + }); + + it("displays additional description text when provided", () => { + render(); + expect(screen.getByText("Additional description")).toBeInTheDocument(); + }); + + describe("when used with asChild prop", () => { + it("renders the label as a child of another component", () => { + render( + + ); + expect(screen.getByRole("button", { name: TEST_TEXT })).toBeInTheDocument(); + }); + }); + + describe("when children, tooltip and description are NOT provided", () => { + it("should not render the label", () => { + const { container } = render(