From 98b8c135112578e63b4cdf4f1563018934b402fb Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Sun, 9 Feb 2025 16:53:37 +0330 Subject: [PATCH 1/4] test: add button tests --- internals/test-helpers/index.ts | 2 +- package.json | 1 + packages/web-components/jest.setup.ts | 11 ++ .../src/button/base/base-button.ts | 6 +- .../src/button/standard/button.test.tsx | 162 ++++++++++++++++++ .../web-components/src/internals/index.ts | 1 + .../web-components/src/internals/tests.tsx | 124 ++++++++++++++ pnpm-lock.yaml | 8 + 8 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 packages/web-components/src/button/standard/button.test.tsx create mode 100644 packages/web-components/src/internals/tests.tsx diff --git a/internals/test-helpers/index.ts b/internals/test-helpers/index.ts index d0682557..bea2bff4 100644 --- a/internals/test-helpers/index.ts +++ b/internals/test-helpers/index.ts @@ -1,3 +1,3 @@ -export { act, render } from "@testing-library/react"; +export { act, cleanup, fireEvent, render } from "@testing-library/react"; export { default as userEvent } from "@testing-library/user-event"; export * from "shadow-dom-testing-library"; diff --git a/package.json b/package.json index efb0872f..4e143181 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@types/stream-json": "^1.7.7", "@vitejs/plugin-react": "^4.3.3", "custom-elements-manifest": "^2.1.0", + "element-internals-polyfill": "^1.3.13", "eslint": "^9.12.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.3", diff --git a/packages/web-components/jest.setup.ts b/packages/web-components/jest.setup.ts index ba7aa37a..0638102a 100644 --- a/packages/web-components/jest.setup.ts +++ b/packages/web-components/jest.setup.ts @@ -1 +1,12 @@ +import { cleanup } from "@internals/test-helpers"; +import { afterEach, beforeAll, jest } from "@jest/globals"; import "@testing-library/jest-dom/jest-globals"; + +beforeAll(async () => { + await import("element-internals-polyfill"); +}); + +afterEach(() => { + cleanup(); + jest.clearAllMocks(); +}); diff --git a/packages/web-components/src/button/base/base-button.ts b/packages/web-components/src/button/base/base-button.ts index 8e566870..cdd94400 100644 --- a/packages/web-components/src/button/base/base-button.ts +++ b/packages/web-components/src/button/base/base-button.ts @@ -119,7 +119,10 @@ export abstract class BaseButton extends BaseClass implements FormSubmitter { */ protected renderSpinner() { return html` -
+
`; @@ -158,6 +161,7 @@ export abstract class BaseButton extends BaseClass implements FormSubmitter { return html` + + , + ); + + await click(getCheckbox()); + await click(getSubmitButton()); + + expect(handleSubmit.mock.calls.length).toBe(1); + expect(getCheckboxValue()).toBe("on"); + + await click(getResetButton()); + expect(handleReset.mock.calls.length).toBe(1); + }); +}); diff --git a/packages/web-components/src/internals/index.ts b/packages/web-components/src/internals/index.ts index 4dff1b42..ddf69331 100644 --- a/packages/web-components/src/internals/index.ts +++ b/packages/web-components/src/internals/index.ts @@ -1,3 +1,4 @@ export * from "./animations.ts"; export * from "./keyboard.ts"; +export * from "./tests.tsx"; export * from "./tokens.ts"; diff --git a/packages/web-components/src/internals/tests.tsx b/packages/web-components/src/internals/tests.tsx new file mode 100644 index 00000000..a1765f07 --- /dev/null +++ b/packages/web-components/src/internals/tests.tsx @@ -0,0 +1,124 @@ +import { render, screen } from "@internals/test-helpers"; +import { expect, it } from "@jest/globals"; +import { type ComponentType } from "react"; + +export const getComponent = ( + Component: ComponentType, + props: T, +): HTMLElement => { + render( + , + ); + + return screen.getByTestId("test-component"); +}; + +export const itShouldMount = ( + Component: ComponentType, + requiredProps: T, +): void => { + it("should be mounted and unmounted without errors", () => { + const elem = ; + + const result = render(elem); + + expect(() => { + result.rerender(elem); + result.unmount(); + }).not.toThrow(); + }); +}; + +export const itSupportsClassName = ( + Component: ComponentType, + requiredProps: T, +): void => { + it("should supports CSS classes", () => { + const className = "tapsi-test-class"; + + const { container } = render( + , + ); + + expect(container.firstChild).toHaveClass(className); + }); +}; + +export const itSupportsStyle = ( + Component: ComponentType, + requiredProps: T, + selector?: string, + options?: { withPortal?: boolean }, +): void => { + it("should supports inline styles", () => { + const { withPortal = false } = options ?? {}; + + const getTarget = (container: HTMLElement): HTMLElement => { + const portal = withPortal + ? document.querySelector("[data-slot='Portal:Root']") + : null; + + return selector + ? portal + ? (portal.querySelector(selector) as HTMLElement) + : (container.querySelector(selector) as HTMLElement) + : portal + ? (container.firstChild as HTMLElement) + : (container.firstChild as HTMLElement); + }; + + const style = { border: "1px solid red", backgroundColor: "black" }; + + const { container } = render( + , + ); + + expect(getTarget(container)).toHaveStyle(style); + }); +}; + +export const itSupportsDataSetProps = ( + Component: ComponentType, + requiredProps: T, + selector?: string, + options?: { withPortal?: boolean }, +): void => { + it("supports `data-*` props", () => { + const { withPortal = false } = options ?? {}; + + const getTarget = (container: HTMLElement): HTMLElement => { + const portal = withPortal + ? document.querySelector("[data-slot='Portal:Root']") + : null; + + return selector + ? portal + ? (portal.querySelector(selector) as HTMLElement) + : (container.querySelector(selector) as HTMLElement) + : portal + ? (container.firstChild as HTMLElement) + : (container.firstChild as HTMLElement); + }; + + const { container } = render( + , + ); + + expect(getTarget(container)).toHaveAttribute( + "data-other-attribute", + "test", + ); + }); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efe1820d..2f149fbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: custom-elements-manifest: specifier: ^2.1.0 version: 2.1.0 + element-internals-polyfill: + specifier: ^1.3.13 + version: 1.3.13 eslint: specifier: ^9.12.0 version: 9.19.0 @@ -2184,6 +2187,9 @@ packages: electron-to-chromium@1.5.88: resolution: {integrity: sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw==} + element-internals-polyfill@1.3.13: + resolution: {integrity: sha512-viZ7wJsvh6eFwGQX512zEaccK/c6RRFSerJsdkfe3DW/ZtruvNeOR33fpPZgfXxvqRdU2lK33KM4S6GqaTgVKQ==} + emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -6591,6 +6597,8 @@ snapshots: electron-to-chromium@1.5.88: {} + element-internals-polyfill@1.3.13: {} + emittery@0.13.1: {} emoji-regex-xs@1.0.0: {} From 82012c4cafa8c2ba148e5b04d8c85cc467664c5a Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Sun, 9 Feb 2025 17:18:15 +0330 Subject: [PATCH 2/4] refactor: remove the getComponent function --- .../src/button/standard/button.test.tsx | 32 ++++++++----------- .../web-components/src/internals/tests.tsx | 16 +--------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/packages/web-components/src/button/standard/button.test.tsx b/packages/web-components/src/button/standard/button.test.tsx index 6a1c751a..2560a0fd 100644 --- a/packages/web-components/src/button/standard/button.test.tsx +++ b/packages/web-components/src/button/standard/button.test.tsx @@ -5,7 +5,6 @@ import { describe, expect, it, jest } from "@jest/globals"; import { createComponent } from "@lit/react"; import React, { type FormEventHandler } from "react"; import { - getComponent, itShouldMount, itSupportsClassName, itSupportsDataSetProps, @@ -19,6 +18,7 @@ const Button = createComponent({ }); const handleClick = jest.fn(); +const getTestButton = () => screen.getByTestId("test-button"); const mockRequiredProps = { label: "test-button-label", @@ -31,9 +31,8 @@ describe("🧪 button/standard: UI", () => { itSupportsDataSetProps(Button, mockRequiredProps); it("should trigger `handleClick` function after clicking on the button", async () => { - const testButton = getComponent(Button, { - onClick: handleClick, - }); + render(