chore: continue vitest test migrations (#21639)

This commit is contained in:
ケイラ
2026-01-23 11:28:59 -07:00
committed by GitHub
parent 338b952d71
commit 98834a7837
7 changed files with 140 additions and 145 deletions
+1 -8
View File
@@ -32,14 +32,7 @@ module.exports = {
customExportConditions: [""],
},
testRegex: "(/__tests__/.*|(\\.|/)(jest))\\.tsx?$",
testPathIgnorePatterns: [
"/node_modules/",
"/e2e/",
// TODO: This test is timing out after upgrade a few Jest dependencies
// and I was not able to figure out why. When running it specifically, I
// can see many act warnings that may can help us to find the issue.
"/usePaginatedQuery.test.ts",
],
testPathIgnorePatterns: ["/node_modules/", "/e2e/"],
transformIgnorePatterns: [],
moduleDirectories: ["node_modules", "<rootDir>/src"],
moduleNameMapper: {
@@ -21,9 +21,9 @@ describe("api.ts", () => {
session_token: "abc_123_test",
};
jest
.spyOn(axiosInstance, "post")
.mockResolvedValueOnce({ data: loginResponse });
vi.spyOn(axiosInstance, "post").mockResolvedValueOnce({
data: loginResponse,
});
// when
const result = await API.login("test", "123");
@@ -41,7 +41,7 @@ describe("api.ts", () => {
message: "Validation failed",
errors: [{ field: "email", code: "email" }],
};
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
const axiosMockPost = vi.fn().mockImplementationOnce(() => {
return Promise.reject(expectedError);
});
axiosInstance.post = axiosMockPost;
@@ -57,7 +57,7 @@ describe("api.ts", () => {
describe("logout", () => {
it("should return without erroring", async () => {
// given
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
const axiosMockPost = vi.fn().mockImplementationOnce(() => {
return Promise.resolve();
});
axiosInstance.post = axiosMockPost;
@@ -76,7 +76,7 @@ describe("api.ts", () => {
const expectedError = {
message: "Failed to logout.",
};
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
const axiosMockPost = vi.fn().mockImplementationOnce(() => {
return Promise.reject(expectedError);
});
@@ -96,7 +96,7 @@ describe("api.ts", () => {
const apiKeyResponse: TypesGen.GenerateAPIKeyResponse = {
key: "abc_123_test",
};
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
const axiosMockPost = vi.fn().mockImplementationOnce(() => {
return Promise.resolve({ data: apiKeyResponse });
});
@@ -117,7 +117,7 @@ describe("api.ts", () => {
const expectedError = {
message: "No Cookie!",
};
const axiosMockPost = jest.fn().mockImplementationOnce(() => {
const axiosMockPost = vi.fn().mockImplementationOnce(() => {
return Promise.reject(expectedError);
});
@@ -175,16 +175,16 @@ describe("api.ts", () => {
describe("update", () => {
describe("given a running workspace", () => {
it("stops with current version before starting with the latest version", async () => {
jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
transition: "stop",
});
jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
template_version_id: MockTemplateVersion2.id,
transition: "start",
});
jest.spyOn(API, "getTemplate").mockResolvedValueOnce({
vi.spyOn(API, "getTemplate").mockResolvedValueOnce({
...MockTemplate,
active_version_id: MockTemplateVersion2.id,
});
@@ -201,17 +201,15 @@ describe("api.ts", () => {
});
it("fails when having missing parameters", async () => {
jest
.spyOn(API, "postWorkspaceBuild")
.mockResolvedValue(MockWorkspaceBuild);
jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]);
jest
.spyOn(API, "getTemplateVersionRichParameters")
.mockResolvedValue([
MockTemplateVersionParameter1,
{ ...MockTemplateVersionParameter2, mutable: false },
]);
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValue(
MockWorkspaceBuild,
);
vi.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]);
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValue([
MockTemplateVersionParameter1,
{ ...MockTemplateVersionParameter2, mutable: false },
]);
let error = new Error();
try {
@@ -229,20 +227,20 @@ describe("api.ts", () => {
});
it("creates a build with no parameters if it is already filled", async () => {
jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
transition: "stop",
});
jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
template_version_id: MockTemplateVersion2.id,
transition: "start",
});
jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
jest
.spyOn(API, "getWorkspaceBuildParameters")
.mockResolvedValue([MockWorkspaceBuildParameter1]);
jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValue([
vi.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([
MockWorkspaceBuildParameter1,
]);
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValue([
{
...MockTemplateVersionParameter1,
required: true,
@@ -263,10 +261,10 @@ describe("api.ts", () => {
});
describe("given a stopped workspace", () => {
it("creates a build with start and the latest template", async () => {
jest
.spyOn(API, "postWorkspaceBuild")
.mockResolvedValueOnce(MockWorkspaceBuild);
jest.spyOn(API, "getTemplate").mockResolvedValueOnce({
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce(
MockWorkspaceBuild,
);
vi.spyOn(API, "getTemplate").mockResolvedValueOnce({
...MockTemplate,
active_version_id: MockTemplateVersion2.id,
});
@@ -1,14 +1,14 @@
import { renderHook, waitFor } from "@testing-library/react";
import { act, renderHook } from "@testing-library/react";
import { useDebouncedFunction, useDebouncedValue } from "./debounce";
beforeAll(() => {
jest.useFakeTimers();
jest.spyOn(global, "setTimeout");
vi.useFakeTimers();
vi.spyOn(global, "setTimeout");
});
afterAll(() => {
jest.useRealTimers();
jest.clearAllMocks();
vi.useRealTimers();
vi.clearAllMocks();
});
describe(useDebouncedValue.name, () => {
@@ -62,7 +62,7 @@ describe(useDebouncedValue.name, () => {
}, i * 100);
}
await jest.advanceTimersByTimeAsync(time - 100);
await vi.advanceTimersByTimeAsync(time - 100);
expect(result.current).toEqual(0);
});
@@ -74,8 +74,13 @@ describe(useDebouncedValue.name, () => {
expect(result.current).toEqual(false);
rerender({ value: !initialValue, time });
await jest.runAllTimersAsync();
await waitFor(() => expect(result.current).toEqual(true));
// Waiting until the debounce time has elapsed will trigger a state update.
await act(async () => {
await vi.runAllTimersAsync();
});
expect(result.current).toEqual(true);
});
// Very important that we not do any async logic for this test
@@ -122,7 +127,7 @@ describe(`${useDebouncedFunction.name}`, () => {
-42,
];
const dummyFunction = jest.fn();
const dummyFunction = vi.fn();
for (const input of invalidInputs) {
expect(() => {
renderDebouncedFunction(dummyFunction, input);
@@ -136,12 +141,12 @@ describe(`${useDebouncedFunction.name}`, () => {
describe("hook", () => {
it("Should provide stable function references across re-renders", () => {
const time = 5000;
const { result, rerender } = renderDebouncedFunction(jest.fn(), time);
const { result, rerender } = renderDebouncedFunction(vi.fn(), time);
const { debounced: oldDebounced, cancelDebounce: oldCancel } =
result.current;
rerender({ callback: jest.fn(), time });
rerender({ callback: vi.fn(), time });
const { debounced: newDebounced, cancelDebounce: newCancel } =
result.current;
@@ -151,43 +156,43 @@ describe(`${useDebouncedFunction.name}`, () => {
it("Resets any pending debounces if the timer argument changes", async () => {
const time = 5000;
const mockCallback = jest.fn();
const mockCallback = vi.fn();
const { result, rerender } = renderDebouncedFunction(mockCallback, time);
result.current.debounced();
rerender({ callback: mockCallback, time: time + 1 });
await jest.runAllTimersAsync();
await vi.runAllTimersAsync();
expect(mockCallback).not.toBeCalled();
});
});
describe("debounced function", () => {
it("Resolve the debounce after specified milliseconds pass with no other calls", async () => {
const mockCallback = jest.fn();
const mockCallback = vi.fn();
const { result } = renderDebouncedFunction(mockCallback, 100);
result.current.debounced();
await jest.runOnlyPendingTimersAsync();
await vi.runOnlyPendingTimersAsync();
expect(mockCallback).toBeCalledTimes(1);
});
it("Always uses the most recent callback argument passed in (even if it switches while a debounce is queued)", async () => {
const mockCallback1 = jest.fn();
const mockCallback2 = jest.fn();
const mockCallback1 = vi.fn();
const mockCallback2 = vi.fn();
const time = 500;
const { result, rerender } = renderDebouncedFunction(mockCallback1, time);
result.current.debounced();
rerender({ callback: mockCallback2, time });
await jest.runAllTimersAsync();
await vi.runAllTimersAsync();
expect(mockCallback1).not.toBeCalled();
expect(mockCallback2).toBeCalledTimes(1);
});
it("Should reset the debounce timer with repeated calls to the method", async () => {
const mockCallback = jest.fn();
const mockCallback = vi.fn();
const { result } = renderDebouncedFunction(mockCallback, 2000);
for (let i = 0; i < 10; i++) {
@@ -196,20 +201,20 @@ describe(`${useDebouncedFunction.name}`, () => {
}, i * 100);
}
await jest.runAllTimersAsync();
await vi.runAllTimersAsync();
expect(mockCallback).toBeCalledTimes(1);
});
});
describe("cancelDebounce function", () => {
it("Should be able to cancel a pending debounce", async () => {
const mockCallback = jest.fn();
const mockCallback = vi.fn();
const { result } = renderDebouncedFunction(mockCallback, 2000);
result.current.debounced();
result.current.cancelDebounce();
await jest.runAllTimersAsync();
await vi.runAllTimersAsync();
expect(mockCallback).not.toBeCalled();
});
});
+7 -5
View File
@@ -43,10 +43,12 @@ export function useDebouncedFunction<
);
}
const timeoutIdRef = useRef<number | undefined>(undefined);
const timeoutIdRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
);
const cancelDebounce = useCallback(() => {
if (timeoutIdRef.current !== undefined) {
window.clearTimeout(timeoutIdRef.current);
clearTimeout(timeoutIdRef.current);
}
timeoutIdRef.current = undefined;
@@ -70,7 +72,7 @@ export function useDebouncedFunction<
(...args: Args): void => {
cancelDebounce();
timeoutIdRef.current = window.setTimeout(
timeoutIdRef.current = setTimeout(
() => void callbackRef.current(...args),
debounceTimeRef.current,
);
@@ -105,10 +107,10 @@ export function useDebouncedValue<T>(value: T, debounceTimeoutMs: number): T {
return;
}
const timeoutId = window.setTimeout(() => {
const timeoutId = setTimeout(() => {
setDebouncedValue(value);
}, debounceTimeoutMs);
return () => window.clearTimeout(timeoutId);
return () => clearTimeout(timeoutId);
}, [value, debounceTimeoutMs]);
return debouncedValue;
@@ -190,7 +190,7 @@ describe(useEmbeddedMetadata.name, () => {
const cleanupTags = seedInitialMetadata(key);
const { result: reactResult, manager } = renderMetadataHook(key);
const nonReactSubscriber = jest.fn();
const nonReactSubscriber = vi.fn();
manager.subscribe(nonReactSubscriber);
const expectedUpdate1: RuntimeHtmlMetadata = {
@@ -1,9 +1,3 @@
// TODO: This test is timing out after upgrade a few Jest dependencies
// and I was not able to figure out why. When running it specifically, I
// can see many act warnings that may can help us to find the issue.
// (Note: This comment was originally written by Bruno, and was relocated by
// me. If you go poking at `git blame`, disabling these tests was not my idea.
import { renderHookWithAuth } from "testHelpers/hooks";
import { waitFor } from "@testing-library/react";
import {
@@ -12,13 +6,8 @@ import {
usePaginatedQuery,
} from "./usePaginatedQuery";
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
jest.clearAllMocks();
afterEach(() => {
vi.clearAllMocks();
});
function render<
@@ -39,16 +28,12 @@ function render<
});
}
/**
* There are a lot of test cases in this file. Scoping mocking to inner describe
* function calls to limit the cognitive load of maintaining all this stuff
*/
describe.skip(usePaginatedQuery.name, () => {
describe(usePaginatedQuery.name, () => {
describe("queryPayload method", () => {
const mockQueryFn = jest.fn(() => Promise.resolve({ count: 0 }));
const mockQueryFn = vi.fn(() => Promise.resolve({ count: 0 }));
it("Passes along an undefined payload if queryPayload is not used", async () => {
const mockQueryKey = jest.fn(() => ["mockQuery"]);
const mockQueryKey = vi.fn(() => ["mockQuery"]);
await render({
queryKey: mockQueryKey,
@@ -64,7 +49,7 @@ describe.skip(usePaginatedQuery.name, () => {
});
it("Passes along type-safe payload if queryPayload is provided", async () => {
const mockQueryKey = jest.fn(({ payload }) => {
const mockQueryKey = vi.fn(({ payload }) => {
return ["mockQuery", payload];
});
@@ -85,8 +70,8 @@ describe.skip(usePaginatedQuery.name, () => {
});
describe("Querying for current page", () => {
const mockQueryKey = jest.fn(() => ["mock"]);
const mockQueryFn = jest.fn(() => Promise.resolve({ count: 50 }));
const mockQueryKey = vi.fn(() => ["mock"]);
const mockQueryFn = vi.fn(() => Promise.resolve({ count: 50 }));
it("Parses page number if it exists in URL params", async () => {
const pageNumbers = [1, 2, 7, 39, 743];
@@ -113,7 +98,7 @@ describe.skip(usePaginatedQuery.name, () => {
});
describe("Prefetching", () => {
const mockQueryKey = jest.fn(({ pageNumber }) => ["query", pageNumber]);
const mockQueryKey = vi.fn(({ pageNumber }) => ["query", pageNumber]);
type Context = { pageNumber: number; limit: number };
const mockQueryFnImplementation = ({ pageNumber, limit }: Context) => {
@@ -134,7 +119,7 @@ describe.skip(usePaginatedQuery.name, () => {
) => {
// Have to reinitialize mock function every call to avoid false positives
// from shared mutable tracking state
const mockQueryFn = jest.fn(mockQueryFnImplementation);
const mockQueryFn = vi.fn(mockQueryFnImplementation);
const { result } = await render(
{ queryKey: mockQueryKey, queryFn: mockQueryFn },
`/?page=${startingPage}`,
@@ -143,16 +128,12 @@ describe.skip(usePaginatedQuery.name, () => {
const pageMatcher = expect.objectContaining({ pageNumber: targetPage });
if (shouldMatch) {
await waitFor(() => expect(result.current.totalRecords).toBeDefined());
await waitFor(() => expect(mockQueryFn).toBeCalledWith(pageMatcher));
await waitFor(() =>
expect(mockQueryFn).toHaveBeenCalledWith(pageMatcher),
);
} else {
// Can't use waitFor to test this, because the expect call will
// immediately succeed for the not case, even though queryFn needs to be
// called async via React Query
setTimeout(() => {
expect(mockQueryFn).not.toBeCalledWith(pageMatcher);
}, 1000);
jest.runAllTimers();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(mockQueryFn).not.toHaveBeenCalledWith(pageMatcher);
}
};
@@ -175,7 +156,7 @@ describe.skip(usePaginatedQuery.name, () => {
it("Reuses the same queryKey and queryFn methods for the current page and all prefetching (on a given render)", async () => {
const startPage = 2;
const mockQueryFn = jest.fn(mockQueryFnImplementation);
const mockQueryFn = vi.fn(mockQueryFnImplementation);
await render(
{ queryKey: mockQueryKey, queryFn: mockQueryFn },
@@ -183,26 +164,34 @@ describe.skip(usePaginatedQuery.name, () => {
);
const currentMatcher = expect.objectContaining({ pageNumber: startPage });
expect(mockQueryKey).toBeCalledWith(currentMatcher);
expect(mockQueryFn).toBeCalledWith(currentMatcher);
expect(mockQueryKey).toHaveBeenCalledWith(currentMatcher);
expect(mockQueryFn).toHaveBeenCalledWith(currentMatcher);
const prevPageMatcher = expect.objectContaining({
pageNumber: startPage - 1,
});
await waitFor(() => expect(mockQueryKey).toBeCalledWith(prevPageMatcher));
await waitFor(() => expect(mockQueryFn).toBeCalledWith(prevPageMatcher));
await waitFor(() =>
expect(mockQueryKey).toHaveBeenCalledWith(prevPageMatcher),
);
await waitFor(() =>
expect(mockQueryFn).toHaveBeenCalledWith(prevPageMatcher),
);
const nextPageMatcher = expect.objectContaining({
pageNumber: startPage + 1,
});
await waitFor(() => expect(mockQueryKey).toBeCalledWith(nextPageMatcher));
await waitFor(() => expect(mockQueryFn).toBeCalledWith(nextPageMatcher));
await waitFor(() =>
expect(mockQueryKey).toHaveBeenCalledWith(nextPageMatcher),
);
await waitFor(() =>
expect(mockQueryFn).toHaveBeenCalledWith(nextPageMatcher),
);
});
});
describe("Safety nets/redirects for invalid pages", () => {
const mockQueryKey = jest.fn(() => ["mock"]);
const mockQueryFn = jest.fn(({ pageNumber, limit }) =>
const mockQueryKey = vi.fn(() => ["mock"]);
const mockQueryFn = vi.fn(({ pageNumber, limit }) =>
Promise.resolve({
data: new Array(limit).fill(pageNumber),
count: 100,
@@ -244,7 +233,7 @@ describe.skip(usePaginatedQuery.name, () => {
page: "1000",
});
const onInvalidPageChange = jest.fn();
const onInvalidPageChange = vi.fn();
await render({
onInvalidPageChange,
queryKey: mockQueryKey,
@@ -270,8 +259,8 @@ describe.skip(usePaginatedQuery.name, () => {
});
describe("Passing in searchParams property", () => {
const mockQueryKey = jest.fn(() => ["mock"]);
const mockQueryFn = jest.fn(({ pageNumber, limit }) =>
const mockQueryKey = vi.fn(() => ["mock"]);
const mockQueryFn = vi.fn(({ pageNumber, limit }) =>
Promise.resolve({
data: new Array(limit).fill(pageNumber),
count: 100,
@@ -309,25 +298,19 @@ describe.skip(usePaginatedQuery.name, () => {
});
});
describe.skip(`${usePaginatedQuery.name} - Returned properties`, () => {
describe(`${usePaginatedQuery.name} - Returned properties`, () => {
describe("Page change methods", () => {
const mockQueryKey = jest.fn(() => ["mock"]);
const mockQueryKey = vi.fn(() => ["mock"]);
const mockQueryFn = jest.fn(({ pageNumber, limit }) => {
type Data = PaginatedData & { data: readonly number[] };
return new Promise<Data>((resolve) => {
setTimeout(() => {
resolve({
data: new Array(limit).fill(pageNumber),
count: 100,
});
}, 10_000);
const mockQueryFn = vi.fn(({ pageNumber, limit }) => {
return Promise.resolve({
data: new Array(limit).fill(pageNumber),
count: 100,
});
});
test("goToFirstPage always succeeds regardless of fetch status", async () => {
const queryFns = [mockQueryFn, jest.fn(() => Promise.reject("Too bad"))];
const queryFns = [mockQueryFn, vi.fn(() => Promise.reject("Too bad"))];
for (const queryFn of queryFns) {
const { result, unmount } = await render(
@@ -351,14 +334,22 @@ describe.skip(`${usePaginatedQuery.name} - Returned properties`, () => {
"/?page=1",
);
expect(result.current.hasNextPage).toBe(false);
result.current.goToNextPage();
expect(result.current.currentPage).toBe(1);
// Wait for the query to complete and check we have next pages
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.hasNextPage).toBe(true);
await jest.runAllTimersAsync();
await waitFor(() => expect(result.current.hasNextPage).toBe(true));
// Can go to next page when hasNextPage is true
result.current.goToNextPage();
await waitFor(() => expect(result.current.currentPage).toBe(2));
// Navigate to last page (page 4, since we have 100 items with 25 per page)
result.current.onPageChange(4);
await waitFor(() => expect(result.current.currentPage).toBe(4));
// Now hasNextPage should be false and goToNextPage should not change the page
expect(result.current.hasNextPage).toBe(false);
result.current.goToNextPage();
expect(result.current.currentPage).toBe(4);
});
test("goToPreviousPage works only if hasPreviousPage is true", async () => {
@@ -370,14 +361,22 @@ describe.skip(`${usePaginatedQuery.name} - Returned properties`, () => {
"/?page=3",
);
expect(result.current.hasPreviousPage).toBe(false);
result.current.goToPreviousPage();
expect(result.current.currentPage).toBe(3);
// Wait for the query to complete and check we have previous pages
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.hasPreviousPage).toBe(true);
await jest.runAllTimersAsync();
await waitFor(() => expect(result.current.hasPreviousPage).toBe(true));
// Can go to previous page when hasPreviousPage is true
result.current.goToPreviousPage();
await waitFor(() => expect(result.current.currentPage).toBe(2));
// Navigate to first page
result.current.goToFirstPage();
await waitFor(() => expect(result.current.currentPage).toBe(1));
// Now hasPreviousPage should be false and goToPreviousPage should not change the page
expect(result.current.hasPreviousPage).toBe(false);
result.current.goToPreviousPage();
expect(result.current.currentPage).toBe(1);
});
test("onPageChange accounts for floats and truncates numeric values before navigating", async () => {
@@ -386,7 +385,7 @@ describe.skip(`${usePaginatedQuery.name} - Returned properties`, () => {
queryFn: mockQueryFn,
});
await jest.runAllTimersAsync();
// Wait for the initial query to complete
await waitFor(() => expect(result.current.isSuccess).toBe(true));
result.current.onPageChange(2.5);
@@ -399,18 +398,16 @@ describe.skip(`${usePaginatedQuery.name} - Returned properties`, () => {
queryFn: mockQueryFn,
});
await jest.runAllTimersAsync();
// Wait for the initial query to complete
await waitFor(() => expect(result.current.isSuccess).toBe(true));
result.current.onPageChange(Number.NaN);
result.current.onPageChange(Number.POSITIVE_INFINITY);
result.current.onPageChange(Number.NEGATIVE_INFINITY);
setTimeout(() => {
expect(result.current.currentPage).toBe(1);
}, 1000);
jest.runAllTimers();
// Give it a moment to ensure no navigation happens
await new Promise((resolve) => setTimeout(resolve, 10));
expect(result.current.currentPage).toBe(1);
});
});
});