mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-28 00:48:07 +00:00
Frontend unit tests run in a plain node environment, so nothing that renders can be covered and no hook can be tested under real React. The workaround that forces is already in the tree: the use-global-shortcuts test mocks out react itself, so its useEffect never re-runs on a dependency change and never goes through React's commit/cleanup ordering -- it pins the stub rather than the hook. Split the suite into two rstest projects: *.test.ts(x) keeps running on node, *.dom.test.ts(x) runs on happy-dom. A global DOM environment was measured first and rejected -- it takes the same 743 tests from 3.3s to 9.5s -- and rstest has no per-file environment docblock, so projects is the only way to charge that cost to the tests that need it. useIsMobile is the first consumer: it had no tests and cannot have any without a document, since it reads window.matchMedia.
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
import { afterEach, describe, expect, it } from "@rstest/core";
|
|
import { act, cleanup, renderHook } from "@testing-library/react";
|
|
|
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
|
|
type HappyDOMWindow = typeof window & {
|
|
happyDOM: { setViewport: (viewport: { width?: number }) => void };
|
|
};
|
|
|
|
function setViewportWidth(width: number) {
|
|
(window as HappyDOMWindow).happyDOM.setViewport({ width });
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
setViewportWidth(1024);
|
|
});
|
|
|
|
describe("useIsMobile", () => {
|
|
it("reports the viewport it mounted into", () => {
|
|
setViewportWidth(1024);
|
|
expect(renderHook(() => useIsMobile()).result.current).toBe(false);
|
|
cleanup();
|
|
|
|
setViewportWidth(500);
|
|
expect(renderHook(() => useIsMobile()).result.current).toBe(true);
|
|
});
|
|
|
|
it("re-renders when the viewport crosses the breakpoint", () => {
|
|
setViewportWidth(1024);
|
|
const { result } = renderHook(() => useIsMobile());
|
|
expect(result.current).toBe(false);
|
|
|
|
act(() => setViewportWidth(500));
|
|
expect(result.current).toBe(true);
|
|
|
|
act(() => setViewportWidth(1024));
|
|
expect(result.current).toBe(false);
|
|
});
|
|
|
|
it("switches at 768px", () => {
|
|
setViewportWidth(767);
|
|
expect(renderHook(() => useIsMobile()).result.current).toBe(true);
|
|
cleanup();
|
|
|
|
setViewportWidth(768);
|
|
expect(renderHook(() => useIsMobile()).result.current).toBe(false);
|
|
});
|
|
|
|
it("drops its media-query listener on unmount", () => {
|
|
// The subscribe callback creates its own MediaQueryList, so the only way to
|
|
// observe the leak is to count through the factory it calls.
|
|
const nativeMatchMedia = window.matchMedia.bind(window);
|
|
let added = 0;
|
|
let removed = 0;
|
|
window.matchMedia = (query: string) => {
|
|
const mql = nativeMatchMedia(query);
|
|
const nativeAdd = mql.addEventListener.bind(mql);
|
|
const nativeRemove = mql.removeEventListener.bind(mql);
|
|
mql.addEventListener = ((...args: Parameters<typeof nativeAdd>) => {
|
|
added += 1;
|
|
return nativeAdd(...args);
|
|
}) as typeof mql.addEventListener;
|
|
mql.removeEventListener = ((...args: Parameters<typeof nativeRemove>) => {
|
|
removed += 1;
|
|
return nativeRemove(...args);
|
|
}) as typeof mql.removeEventListener;
|
|
return mql;
|
|
};
|
|
|
|
try {
|
|
const { unmount } = renderHook(() => useIsMobile());
|
|
expect(added).toBeGreaterThan(0);
|
|
expect(removed).toBe(0);
|
|
|
|
unmount();
|
|
expect(removed).toBe(added);
|
|
} finally {
|
|
window.matchMedia = nativeMatchMedia;
|
|
}
|
|
});
|
|
});
|