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..aff2205e --- /dev/null +++ b/packages/web-components/src/internals/tests.tsx @@ -0,0 +1,110 @@ +import { render } from "@internals/test-helpers"; +import { expect, it } from "@jest/globals"; +import { type ComponentType } from "react"; + +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: {}