Skip to content

Commit

Permalink
feat(core): hopefully enable vitest test-utils
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Jan 12, 2025
1 parent 8e5e0d8 commit 79a7c9f
Show file tree
Hide file tree
Showing 7 changed files with 958 additions and 4 deletions.
10 changes: 8 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"default": "./dist/test-utils/polyfills/*.js"
},
"./test-utils/jest-globals": "./dist/test-utils/jest-globals/index.js",
"./test-utils/vitest": "./dist/test-utils/vitest/index.js",
"./test-utils": "./dist/test-utils/index.js",
"./colors": {
"sass": "./dist/_colors.scss",
Expand Down Expand Up @@ -109,7 +110,8 @@
"ts-morph": "^25.0.0",
"ts-node": "^10.9.2",
"tsx": "^4.19.2",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"vitest": "^2.1.8"
},
"peerDependencies": {
"@jest/globals": "^29.7.0",
Expand All @@ -119,7 +121,8 @@
"@testing-library/react": ">= 14",
"@testing-library/user-event": ">= 14",
"react": ">= 18",
"react-dom": ">= 18"
"react-dom": ">= 18",
"vitest": ">= 2"
},
"peerDependenciesMeta": {
"@jest/globals": {
Expand All @@ -145,6 +148,9 @@
},
"react-dom": {
"optional": true
},
"vitest": {
"optional": true
}
},
"publishConfig": {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/test-utils/vitest/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./match-media.js";
export * from "./resize-observer.js";
export * from "./timers.js";
38 changes: 38 additions & 0 deletions packages/core/src/test-utils/vitest/match-media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { type MockInstance, vi } from "vitest";
import {
createMatchMediaSpy,
type MatchMediaChangeViewport,
} from "../mocks/match-media-implementation.js";
import { matchDesktop, type MatchMediaMatcher } from "../mocks/match-media.js";

/**
* @example Default Behavior
* ```tsx
* import { matchPhone, render, spyOnMatchMedia } from "@react-md/core/test-utils";
*
* const matchMedia = spyOnMatchMedia();
* render(<Test />);
*
* // expect desktop results
*
* matchMedia.changeViewport(matchPhone);
* // expect phone results
* ```
*
* @example Set Default Media
* ```tsx
* import { matchPhone, render, spyOnMatchMedia } from "@react-md/core/test-utils";
*
* const matchMedia = spyOnMatchMedia(matchPhone);
* render(<Test />);
*
* // expect phone results
* ```
*
* @since 6.0.0
*/
export function spyOnMatchMedia(
defaultMatch: MatchMediaMatcher = matchDesktop
): MockInstance<typeof window.matchMedia> & MatchMediaChangeViewport {
return createMatchMediaSpy(vi.spyOn(window, "matchMedia"), defaultMatch);
}
109 changes: 109 additions & 0 deletions packages/core/src/test-utils/vitest/resize-observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { afterEach, vi } from "vitest";
import { resizeObserverManager } from "../../useResizeObserver.js";
import {
type SetupResizeObserverMockOptions,
ResizeObserverMock,
} from "../mocks/ResizeObserver.js";

/**
* Initializes the `ResizeObserverMock` to be used for tests.
*
* @example Main Usage
* ```tsx
* import { render, screen } from "@react-md/core/test-utils";
* import {
* cleanupResizeObserverAfterEach,
* setupResizeObserverMock,
* } from "test-utils/jest-globals";
* import { useResizeObserver } from "@react-md/core/useResizeObserver";
* import { useCallback, useState } from "react";
*
* function ExampleComponent() {
* const [size, setSize] = useState({ height: 0, width: 0 });
* const ref = useResizeObserver({
* onUpdate: useCallback((entry) => {
* setSize({
* height: entry.contentRect.height,
* width: entry.contentRect.width,
* });
* }, []),
* });
*
* return (
* <>
* <div data-testid="size">{JSON.stringify(size)}</div>
* <div data-testid="resize-target" ref={ref} />
* </>
* );
* }
*
* cleanupResizeObserverAfterEach();
*
* describe("ExampleComponent", () => {
* it("should do stuff", () => {
* const observer = setupResizeObserverMock();
* render(<ExampleComponent />);
*
* const size = screen.getByTestId("size");
* const resizeTarget = screen.getByTestId("resize-target");
*
* // jsdom sets all element sizes to 0 by default
* expect(size).toHaveTextContent(JSON.stringify({ height: 0, width: 0 }));
*
* // you can trigger with a custom change
* act(() => {
* observer.resizeElement(resizeTarget, { height: 100, width: 100 });
* });
* expect(size).toHaveTextContent(JSON.stringify({ height: 100, width: 100 }));
*
* // or you can mock the `getBoundingClientRect` result
* jest.spyOn(resizeTarget, "getBoundingClientRect").mockReturnValue({
* ...document.body.getBoundingClientRect(),
* height: 200,
* width: 200,
* });
*
* act(() => {
* observer.resizeElement(resizeTarget);
* });
* expect(size).toHaveTextContent(JSON.stringify({ height: 200, width: 200 }));
* });
* });
* ```
*
* @since 6.0.0
*/
export function setupResizeObserverMock(
options: SetupResizeObserverMockOptions = {}
): ResizeObserverMock {
const { raf, manager = resizeObserverManager } = options;

const resizeObserver = new ResizeObserverMock((entries) => {
if (raf) {
window.cancelAnimationFrame(manager.frame);
manager.frame = window.requestAnimationFrame(() => {
manager.handleResizeEntries(entries);
});
} else {
manager.handleResizeEntries(entries);
}
});
manager.sharedObserver = resizeObserver;
return resizeObserver;
}

/**
* @see {@link setupResizeObserverMock}
* @since 6.0.0
*/
export function cleanupResizeObserverAfterEach(restoreAllMocks = true): void {
afterEach(() => {
resizeObserverManager.frame = 0;
resizeObserverManager.subscriptions = new Map();
resizeObserverManager.sharedObserver = undefined;

if (restoreAllMocks) {
vi.restoreAllMocks();
}
});
}
15 changes: 15 additions & 0 deletions packages/core/src/test-utils/vitest/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import "@testing-library/jest-dom/vitest";

import { INTERACTION_CONFIG } from "../../interaction/config.js";
import { TRANSITION_CONFIG } from "../../transition/config.js";

// @ts-expect-error I do not have globals enabled
beforeEach(() => {
// set the mode to `none` in tests since ripples require
// `getBoundingClientRect()` to create correct CSS. You'll either see warnings
// in the console around invalid css values or `NaN`.
INTERACTION_CONFIG.mode = "none";

// disable transitions in tests since it just makes it more difficult
TRANSITION_CONFIG.disabled = true;
});
48 changes: 48 additions & 0 deletions packages/core/src/test-utils/vitest/timers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { type MockInstance, vi } from "vitest";

/**
* @since 6.0.0
*/
export type RafSpy = MockInstance<typeof requestAnimationFrame>;

/**
* @example
* ```ts
* import { testImmediateRaf } from "@react-md/core/test-utils/jest-globals";
*
* describe("some test suite", () => {
* it("should test something with requestAnimationFrame", () => {
* const raf = testImmediateRaf();
*
* // do some testing with requestAnimationFrame
*
* // reset to original at the end of the test
* raf.mockRestore()
* });
* });
* ```
*
* @example Automatic Cleanup
* ```ts
* import { testImmediateRaf } from "@react-md/core/test-utils/jest-globals";
*
* afterEach(() => {
* jest.restoreAllMocks();
* });
*
* describe("some test suite", () => {
* it("should test something with requestAnimationFrame", () => {
* const raf = testImmediateRaf();
*
* // do some testing with requestAnimationFrame
* });
* });
* ```
*
* @since 6.0.0
*/
export const testImmediateRaf = (): RafSpy =>
vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
cb(0);
return 0;
});
Loading

0 comments on commit 79a7c9f

Please sign in to comment.