mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
replace Jest with Vitest as the frontend test runner (#1163)
This commit is contained in:
parent
a663302ba2
commit
edeea95e7d
1434
frontend/package-lock.json
generated
1434
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,7 @@
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"test": "jest",
|
||||
"test": "vitest",
|
||||
"preview": "vite preview",
|
||||
"make-i18n": "node scripts/make-i18n-translations.cjs",
|
||||
"prelint": "npm run make-i18n",
|
||||
@ -56,18 +56,10 @@
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest/presets/js-with-ts",
|
||||
"testEnvironment": "jest-environment-jsdom",
|
||||
"modulePaths": [
|
||||
"<rootDir>/src"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^18.0.0 ",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
@ -85,14 +77,13 @@
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwindcss": "^3.4.2",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.4.3"
|
||||
"typescript": "^5.4.3",
|
||||
"vitest": "^1.5.0"
|
||||
},
|
||||
"packageManager": "npm@10.5.0",
|
||||
"volta": {
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useTypingEffect } from "./useTypingEffect";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("useTypingEffect", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
});
|
||||
|
||||
// This test fails because the hook improperly handles this case.
|
||||
it.skip("should handle empty strings array", () => {
|
||||
const { result } = renderHook(() => useTypingEffect([]));
|
||||
@ -23,13 +29,13 @@ describe("useTypingEffect", () => {
|
||||
|
||||
// Fast-forward time by to simulate typing message
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(msToRun - 1); // exclude the last character for testing
|
||||
vi.advanceTimersByTime(msToRun - 1); // exclude the last character for testing
|
||||
});
|
||||
|
||||
expect(result.current).toBe(message.slice(0, -1));
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1); // include the last character
|
||||
vi.advanceTimersByTime(1); // include the last character
|
||||
});
|
||||
|
||||
expect(result.current).toBe(message);
|
||||
@ -46,13 +52,13 @@ describe("useTypingEffect", () => {
|
||||
const msToRun = (message.length - 2) * 100 * playbackRate;
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(msToRun - 1); // exclude the last character for testing
|
||||
vi.advanceTimersByTime(msToRun - 1); // exclude the last character for testing
|
||||
});
|
||||
|
||||
expect(result.current).toBe(message.slice(0, -1));
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1); // include the last character
|
||||
vi.advanceTimersByTime(1); // include the last character
|
||||
});
|
||||
|
||||
expect(result.current).toBe(message);
|
||||
@ -67,7 +73,7 @@ describe("useTypingEffect", () => {
|
||||
|
||||
// Fast-forward to end of first string
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(msToRunFirstString);
|
||||
vi.advanceTimersByTime(msToRunFirstString);
|
||||
});
|
||||
|
||||
expect(result.current).toBe(messages[0]); // Hello
|
||||
@ -75,14 +81,14 @@ describe("useTypingEffect", () => {
|
||||
// Fast-forward through the delay and through the second string
|
||||
act(() => {
|
||||
// TODO: Improve to clarify the expected timing
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(result.current).toBe(messages[1]); // World
|
||||
});
|
||||
|
||||
it("should call setTypingActive with false when typing completes without loop", () => {
|
||||
const setTypingActiveMock = jest.fn();
|
||||
const setTypingActiveMock = vi.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useTypingEffect(["Hello, world!", "This is a test message."], {
|
||||
@ -94,7 +100,7 @@ describe("useTypingEffect", () => {
|
||||
expect(setTypingActiveMock).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(setTypingActiveMock).toHaveBeenCalledWith(false);
|
||||
@ -102,7 +108,7 @@ describe("useTypingEffect", () => {
|
||||
});
|
||||
|
||||
it("should call addAssistantMessageToChat with the typeThis argument when typing completes without loop", () => {
|
||||
const addAssistantMessageToChatMock = jest.fn();
|
||||
const addAssistantMessageToChatMock = vi.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useTypingEffect(["Hello, world!", "This is a test message."], {
|
||||
@ -116,7 +122,7 @@ describe("useTypingEffect", () => {
|
||||
expect(addAssistantMessageToChatMock).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(addAssistantMessageToChatMock).toHaveBeenCalledTimes(1);
|
||||
@ -127,7 +133,7 @@ describe("useTypingEffect", () => {
|
||||
});
|
||||
|
||||
it("should call takeOneAndType when typing completes without loop", () => {
|
||||
const takeOneAndTypeMock = jest.fn();
|
||||
const takeOneAndTypeMock = vi.fn();
|
||||
|
||||
renderHook(() =>
|
||||
useTypingEffect(["Hello, world!", "This is a test message."], {
|
||||
@ -139,7 +145,7 @@ describe("useTypingEffect", () => {
|
||||
expect(takeOneAndTypeMock).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(takeOneAndTypeMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
@ -1,23 +1,25 @@
|
||||
import * as jose from "jose";
|
||||
import type { Mock } from "vitest";
|
||||
import { fetchToken, validateToken, getToken } from "./auth";
|
||||
|
||||
jest.mock("jose", () => ({
|
||||
decodeJwt: jest.fn(),
|
||||
vi.mock("jose", () => ({
|
||||
decodeJwt: vi.fn(),
|
||||
}));
|
||||
|
||||
// SUGGESTION: Prefer using msw for mocking requests (see https://mswjs.io/)
|
||||
global.fetch = jest.fn(() =>
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ token: "newToken" }),
|
||||
}),
|
||||
) as jest.Mock;
|
||||
) as Mock;
|
||||
|
||||
Storage.prototype.getItem = vi.fn();
|
||||
Storage.prototype.setItem = vi.fn();
|
||||
|
||||
describe("Auth Service", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
Storage.prototype.getItem = jest.fn();
|
||||
Storage.prototype.setItem = jest.fn();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchToken", () => {
|
||||
@ -32,7 +34,7 @@ describe("Auth Service", () => {
|
||||
});
|
||||
|
||||
it("throws an error if response status is not 200", async () => {
|
||||
(fetch as jest.Mock).mockImplementationOnce(() =>
|
||||
(fetch as Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({ status: 401 }),
|
||||
);
|
||||
await expect(fetchToken()).rejects.toThrow("Get token failed.");
|
||||
@ -41,17 +43,17 @@ describe("Auth Service", () => {
|
||||
|
||||
describe("validateToken", () => {
|
||||
it("returns true for a valid token", () => {
|
||||
(jose.decodeJwt as jest.Mock).mockReturnValue({ sid: "123" });
|
||||
(jose.decodeJwt as Mock).mockReturnValue({ sid: "123" });
|
||||
expect(validateToken("validToken")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for an invalid token", () => {
|
||||
(jose.decodeJwt as jest.Mock).mockReturnValue({});
|
||||
(jose.decodeJwt as Mock).mockReturnValue({});
|
||||
expect(validateToken("invalidToken")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when decodeJwt throws", () => {
|
||||
(jose.decodeJwt as jest.Mock).mockImplementation(() => {
|
||||
(jose.decodeJwt as Mock).mockImplementation(() => {
|
||||
throw new Error("Invalid token");
|
||||
});
|
||||
expect(validateToken("badToken")).toBe(false);
|
||||
@ -60,15 +62,15 @@ describe("Auth Service", () => {
|
||||
|
||||
describe("getToken", () => {
|
||||
it("returns existing valid token from localStorage", async () => {
|
||||
(jose.decodeJwt as jest.Mock).mockReturnValue({ sid: "123" });
|
||||
(Storage.prototype.getItem as jest.Mock).mockReturnValue("existingToken");
|
||||
(jose.decodeJwt as Mock).mockReturnValue({ sid: "123" });
|
||||
(Storage.prototype.getItem as Mock).mockReturnValue("existingToken");
|
||||
|
||||
const token = await getToken();
|
||||
expect(token).toBe("existingToken");
|
||||
});
|
||||
|
||||
it("fetches, validates, and stores a new token when existing token is invalid", async () => {
|
||||
(jose.decodeJwt as jest.Mock)
|
||||
(jose.decodeJwt as Mock)
|
||||
.mockReturnValueOnce({})
|
||||
.mockReturnValueOnce({ sid: "123" });
|
||||
|
||||
@ -78,7 +80,7 @@ describe("Auth Service", () => {
|
||||
});
|
||||
|
||||
it("throws an error when fetched token is invalid", async () => {
|
||||
(jose.decodeJwt as jest.Mock).mockReturnValue({});
|
||||
(jose.decodeJwt as Mock).mockReturnValue({});
|
||||
await expect(getToken()).rejects.toThrow("Token validation failed.");
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { Mock } from "vitest";
|
||||
import {
|
||||
ResDelMsg,
|
||||
ResFetchMsg,
|
||||
@ -7,12 +8,12 @@ import {
|
||||
import { clearMsgs, fetchMsgTotal, fetchMsgs } from "./session";
|
||||
|
||||
// SUGGESTION: Prefer using msw for mocking requests (see https://mswjs.io/)
|
||||
global.fetch = jest.fn();
|
||||
Storage.prototype.getItem = jest.fn();
|
||||
global.fetch = vi.fn();
|
||||
Storage.prototype.getItem = vi.fn();
|
||||
|
||||
describe("Session Service", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -26,7 +27,7 @@ describe("Session Service", () => {
|
||||
msg_total: 10,
|
||||
};
|
||||
|
||||
(fetch as jest.Mock).mockImplementationOnce(() =>
|
||||
(fetch as Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
json: () => Promise.resolve(expectedResult),
|
||||
@ -45,7 +46,7 @@ describe("Session Service", () => {
|
||||
it("throws an error if response status is not 200", async () => {
|
||||
// NOTE: The current implementation ONLY handles 200 status;
|
||||
// this means throwing even with a status of 201, 204, etc.
|
||||
(fetch as jest.Mock).mockImplementationOnce(() =>
|
||||
(fetch as Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({ status: 401 }),
|
||||
);
|
||||
|
||||
@ -67,7 +68,7 @@ describe("Session Service", () => {
|
||||
],
|
||||
};
|
||||
|
||||
(fetch as jest.Mock).mockImplementationOnce(() =>
|
||||
(fetch as Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
json: () => Promise.resolve(expectedResult),
|
||||
@ -84,7 +85,7 @@ describe("Session Service", () => {
|
||||
});
|
||||
|
||||
it("throws an error if response status is not 200", async () => {
|
||||
(fetch as jest.Mock).mockImplementationOnce(() =>
|
||||
(fetch as Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({ status: 401 }),
|
||||
);
|
||||
|
||||
@ -98,7 +99,7 @@ describe("Session Service", () => {
|
||||
ok: "true",
|
||||
};
|
||||
|
||||
(fetch as jest.Mock).mockImplementationOnce(() =>
|
||||
(fetch as Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
status: 200,
|
||||
json: () => Promise.resolve(expectedResult),
|
||||
@ -116,7 +117,7 @@ describe("Session Service", () => {
|
||||
});
|
||||
|
||||
it("throws an error if response status is not 200", async () => {
|
||||
(fetch as jest.Mock).mockImplementationOnce(() =>
|
||||
(fetch as Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({ status: 401 }),
|
||||
);
|
||||
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
import { mergeAndUpdateSettings } from "./settingsService";
|
||||
import { ArgConfigType } from "../types/ConfigType";
|
||||
|
||||
// We need to mock this to avoid `SyntaxError` from using `Socket` in `settingsService` during testing
|
||||
jest.mock("./socket", () => ({
|
||||
send: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("mergeAndUpdateSettings", () => {
|
||||
it("should return initial settings if newSettings is empty", () => {
|
||||
const oldSettings = { key1: "value1" };
|
||||
|
||||
@ -1,27 +1,28 @@
|
||||
import type { Mock } from "vitest";
|
||||
import { getCachedConfig } from "./storage";
|
||||
|
||||
describe("getCachedConfig", () => {
|
||||
beforeEach(() => {
|
||||
// Clear all instances and calls to constructor and all methods
|
||||
Storage.prototype.getItem = jest.fn();
|
||||
Storage.prototype.getItem = vi.fn();
|
||||
});
|
||||
|
||||
it("should return an empty object when local storage is null or undefined", () => {
|
||||
(Storage.prototype.getItem as jest.Mock).mockReturnValue(null);
|
||||
(Storage.prototype.getItem as Mock).mockReturnValue(null);
|
||||
expect(getCachedConfig()).toEqual({});
|
||||
|
||||
(Storage.prototype.getItem as jest.Mock).mockReturnValue(undefined);
|
||||
(Storage.prototype.getItem as Mock).mockReturnValue(undefined);
|
||||
expect(getCachedConfig()).toEqual({});
|
||||
});
|
||||
|
||||
it("should return an empty object when local storage has invalid JSON", () => {
|
||||
(Storage.prototype.getItem as jest.Mock).mockReturnValue("invalid JSON");
|
||||
(Storage.prototype.getItem as Mock).mockReturnValue("invalid JSON");
|
||||
expect(getCachedConfig()).toEqual({});
|
||||
});
|
||||
|
||||
it("should return parsed object when local storage has valid JSON", () => {
|
||||
const validJSON = '{"key":"value"}';
|
||||
(Storage.prototype.getItem as jest.Mock).mockReturnValue(validJSON);
|
||||
(Storage.prototype.getItem as Mock).mockReturnValue(validJSON);
|
||||
expect(getCachedConfig()).toEqual({ key: "value" });
|
||||
});
|
||||
});
|
||||
|
||||
@ -19,10 +19,10 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react",
|
||||
"types": ["vite/client"]
|
||||
"types": ["vite/client", "vitest/globals"]
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"src",
|
||||
"vite-env.d.ts",
|
||||
"vite.config.ts"
|
||||
]
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import viteTsconfigPaths from "vite-tsconfig-paths";
|
||||
@ -31,4 +32,8 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user