switch to vitest

This commit is contained in:
Scott Lamb 2023-12-17 23:07:28 -08:00
parent 24880a5c2d
commit 3911334fee
13 changed files with 1592 additions and 688 deletions

View File

@ -7,7 +7,8 @@
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"rust-lang.rust-analyzer", "rust-lang.rust-analyzer",
"yzhang.markdown-all-in-one" "yzhang.markdown-all-in-one",
"zixuanchen.vitest-explorer"
], ],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace. // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [ "unwantedRecommendations": [

10
.vscode/settings.json vendored
View File

@ -35,11 +35,7 @@
} }
], ],
// It seems like rust-analyzer is supposed to be able to format "editor.defaultFormatter": "rust-lang.rust-analyzer"
// Rust files, but with "matklad.rust-analyzer" here, VS Code says
// "There is no formatter for 'rust' files installed."
"editor.defaultFormatter": "matklad.rust-analyzer"
//"editor.defaultFormatter": null
}, },
"markdown.extension.list.indentationSize": "inherit", "markdown.extension.list.indentationSize": "inherit",
"markdown.extension.toc.unorderedList.marker": "*", "markdown.extension.toc.unorderedList.marker": "*",
@ -47,5 +43,7 @@
// Specify the path to the workspace version of TypeScript. Note this only // Specify the path to the workspace version of TypeScript. Note this only
// takes effect when workspace version is selected in the UI. // takes effect when workspace version is selected in the UI.
// https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript // https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript
"typescript.tsdk": "./ui/node_modules/typescript/lib" "typescript.tsdk": "./ui/node_modules/typescript/lib",
"cmake.configureOnOpen": false,
"vitest.enable": true
} }

View File

@ -1,38 +0,0 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2023 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
// Environment based on `jsdom` with some extra globals, inspired by
// the following comment:
// https://github.com/jsdom/jsdom/issues/1724#issuecomment-1446858041
import JSDOMEnvironment from "jest-environment-jsdom";
// https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string
export default class FixJSDOMEnvironment extends JSDOMEnvironment {
constructor(...args: ConstructorParameters<typeof JSDOMEnvironment>) {
super(...args);
// Tests use fetch calls with relative URLs + msw to intercept.
this.global.fetch = (
resource: RequestInfo | URL,
options?: RequestInit
) => {
throw "must use msw to fetch: " + resource;
};
class MyRequest extends Request {
constructor(input: RequestInfo | URL, init?: RequestInit | undefined) {
input = new URL(input as string, "http://localhost");
super(input, init);
}
}
this.global.Headers = Headers;
this.global.Request = MyRequest;
this.global.Response = Response;
// `src/LiveCamera/parser.ts` uses TextDecoder.
this.global.TextDecoder = TextDecoder;
}
}

View File

@ -1,36 +0,0 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2023 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
import type { Config } from "jest";
const config: Config = {
testEnvironment: "./FixJSDomEnvironment.ts",
transform: {
// https://github.com/swc-project/jest
"\\.[tj]sx?$": [
"@swc/jest",
{
// https://swc.rs/docs/configuration/compilation
// https://github.com/swc-project/jest/issues/167#issuecomment-1809868077
jsc: {
transform: {
react: {
runtime: "automatic",
},
},
},
},
],
},
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
// https://github.com/jaredLunde/react-hook/issues/300#issuecomment-1845227937
moduleNameMapper: {
"@react-hook/(.*)": "<rootDir>/node_modules/@react-hook/$1/dist/main",
},
};
export default config;

2117
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,12 +27,12 @@
"format": "prettier --write --ignore-path .gitignore .", "format": "prettier --write --ignore-path .gitignore .",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview", "preview": "vite preview",
"test": "jest" "test": "vitest"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:jest/recommended", "plugin:vitest/recommended",
"plugin:react/recommended", "plugin:react/recommended",
"plugin:react/jsx-runtime", "plugin:react/jsx-runtime",
"plugin:react-hooks/recommended" "plugin:react-hooks/recommended"
@ -54,7 +54,6 @@
"sourceType": "module" "sourceType": "module"
}, },
"rules": { "rules": {
"jest/no-disabled-tests": "off",
"no-restricted-imports": [ "no-restricted-imports": [
"error", "error",
{ {
@ -86,12 +85,10 @@
"@babel/preset-react": "^7.23.3", "@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3", "@babel/preset-typescript": "^7.23.3",
"@swc/core": "^1.3.100", "@swc/core": "^1.3.100",
"@swc/jest": "^0.2.29",
"@testing-library/dom": "^8.11.3", "@testing-library/dom": "^8.11.3",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.5.11",
"@types/node": "^18.8.1", "@types/node": "^18.8.1",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10", "@types/react-dom": "^18.0.10",
@ -99,18 +96,17 @@
"@typescript-eslint/parser": "^6.14.0", "@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react-swc": "^3.5.0", "@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-vitest": "^0.3.18",
"http-proxy-middleware": "^2.0.4", "http-proxy-middleware": "^2.0.4",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"msw": "^1.3.2", "msw": "^1.3.2",
"prettier": "^2.6.0", "prettier": "^2.6.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.1.0", "typescript": "^5.1.0",
"vite": "^5.0.8", "vite": "^5.0.8",
"vite-plugin-compression": "^0.5.1" "vite-plugin-compression": "^0.5.1",
"vitest": "^1.0.4"
} }
} }

View File

@ -7,6 +7,7 @@ import App from "./App";
import { renderWithCtx } from "./testutil"; import { renderWithCtx } from "./testutil";
import { rest } from "msw"; import { rest } from "msw";
import { setupServer } from "msw/node"; import { setupServer } from "msw/node";
import { beforeAll, afterAll, afterEach, expect, test } from "vitest";
const server = setupServer( const server = setupServer(
rest.get("/api/", (req, res, ctx) => { rest.get("/api/", (req, res, ctx) => {

View File

@ -4,6 +4,7 @@
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
import { expect, test } from "vitest";
const ThrowsLiteralComponent = () => { const ThrowsLiteralComponent = () => {
throw "simple string error"; throw "simple string error";

View File

@ -11,6 +11,7 @@ import { Recording, VideoSampleEntry } from "../api";
import { renderWithCtx } from "../testutil"; import { renderWithCtx } from "../testutil";
import { Camera, Stream } from "../types"; import { Camera, Stream } from "../types";
import VideoList from "./VideoList"; import VideoList from "./VideoList";
import { beforeAll, afterAll, afterEach, expect, test } from "vitest";
const TEST_CAMERA: Camera = { const TEST_CAMERA: Camera = {
uuid: "c7278ba0-a001-420c-911e-fff4e33f6916", uuid: "c7278ba0-a001-420c-911e-fff4e33f6916",

View File

@ -8,6 +8,7 @@ import { rest } from "msw";
import { setupServer } from "msw/node"; import { setupServer } from "msw/node";
import Login from "./Login"; import Login from "./Login";
import { renderWithCtx } from "./testutil"; import { renderWithCtx } from "./testutil";
import { beforeAll, afterEach, afterAll, test, vi, expect } from "vitest";
// Set up a fake API backend. // Set up a fake API backend.
const server = setupServer( const server = setupServer(
@ -47,20 +48,20 @@ afterAll(() => server.close());
// https://github.com/facebook/jest/issues/13018 ? // https://github.com/facebook/jest/issues/13018 ?
// //
// Argh! // Argh!
// beforeEach(() => jest.useFakeTimers({ // beforeEach(() => vi.useFakeTimers({
// legacyFakeTimers: true, // legacyFakeTimers: true,
// })); // }));
// afterEach(() => { // afterEach(() => {
// act(() => { // act(() => {
// jest.runOnlyPendingTimers(); // vi.runOnlyPendingTimers();
// jest.useRealTimers(); // vi.useRealTimers();
// }); // });
// }); // });
test("success", async () => { test("success", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const handleClose = jest.fn().mockName("handleClose"); const handleClose = vi.fn().mockName("handleClose");
const onSuccess = jest.fn().mockName("handleOpen"); const onSuccess = vi.fn().mockName("handleOpen");
renderWithCtx( renderWithCtx(
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} /> <Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
); );
@ -75,8 +76,8 @@ test("success", async () => {
// so the delay("infinite") request just sticks around, even though the fetch // so the delay("infinite") request just sticks around, even though the fetch
// has been aborted. Maybe https://github.com/mswjs/msw/pull/585 will fix it. // has been aborted. Maybe https://github.com/mswjs/msw/pull/585 will fix it.
test.skip("close while pending", async () => { test.skip("close while pending", async () => {
const handleClose = jest.fn().mockName("handleClose"); const handleClose = vi.fn().mockName("handleClose");
const onSuccess = jest.fn().mockName("handleOpen"); const onSuccess = vi.fn().mockName("handleOpen");
const { rerender } = renderWithCtx( const { rerender } = renderWithCtx(
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} /> <Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
); );
@ -96,8 +97,8 @@ test.skip("close while pending", async () => {
// TODO: fix and re-enable this test. // TODO: fix and re-enable this test.
// It depends on the timers; see TODO above. // It depends on the timers; see TODO above.
test.skip("bad credentials", async () => { test.skip("bad credentials", async () => {
const handleClose = jest.fn().mockName("handleClose"); const handleClose = vi.fn().mockName("handleClose");
const onSuccess = jest.fn().mockName("handleOpen"); const onSuccess = vi.fn().mockName("handleOpen");
renderWithCtx( renderWithCtx(
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} /> <Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
); );
@ -110,8 +111,8 @@ test.skip("bad credentials", async () => {
// TODO: fix and re-enable this test. // TODO: fix and re-enable this test.
// It depends on the timers; see TODO above. // It depends on the timers; see TODO above.
test.skip("server error", async () => { test.skip("server error", async () => {
const handleClose = jest.fn().mockName("handleClose"); const handleClose = vi.fn().mockName("handleClose");
const onSuccess = jest.fn().mockName("handleOpen"); const onSuccess = vi.fn().mockName("handleOpen");
renderWithCtx( renderWithCtx(
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} /> <Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
); );
@ -127,8 +128,8 @@ test.skip("server error", async () => {
// TODO: fix and re-enable this test. // TODO: fix and re-enable this test.
// It depends on the timers; see TODO above. // It depends on the timers; see TODO above.
test.skip("network error", async () => { test.skip("network error", async () => {
const handleClose = jest.fn().mockName("handleClose"); const handleClose = vi.fn().mockName("handleClose");
const onSuccess = jest.fn().mockName("handleOpen"); const onSuccess = vi.fn().mockName("handleOpen");
renderWithCtx( renderWithCtx(
<Login open={true} onSuccess={onSuccess} handleClose={handleClose} /> <Login open={true} onSuccess={onSuccess} handleClose={handleClose} />
); );

View File

@ -40,7 +40,7 @@ async function myfetch(
): Promise<FetchResult<Response>> { ): Promise<FetchResult<Response>> {
let response; let response;
try { try {
response = await fetch(url, init); response = await fetch(window.location.origin + url, init);
} catch (e) { } catch (e) {
if (!(e instanceof DOMException)) { if (!(e instanceof DOMException)) {
throw e; throw e;

View File

@ -5,12 +5,13 @@
import { act, render, screen, waitFor } from "@testing-library/react"; import { act, render, screen, waitFor } from "@testing-library/react";
import { useEffect } from "react"; import { useEffect } from "react";
import { SnackbarProvider, useSnackbars } from "./snackbars"; import { SnackbarProvider, useSnackbars } from "./snackbars";
import { beforeEach, afterEach, expect, test, vi } from "vitest";
// Mock out timers. // Mock out timers.
beforeEach(() => jest.useFakeTimers()); beforeEach(() => { vi.useFakeTimers(); });
afterEach(() => { afterEach(() => {
jest.runOnlyPendingTimers(); vi.runOnlyPendingTimers();
jest.useRealTimers(); vi.useRealTimers();
}); });
test("notifications that time out", async () => { test("notifications that time out", async () => {
@ -34,24 +35,24 @@ test("notifications that time out", async () => {
expect(screen.queryByText(/message B/)).not.toBeInTheDocument(); expect(screen.queryByText(/message B/)).not.toBeInTheDocument();
// ...then start to close... // ...then start to close...
act(() => jest.advanceTimersByTime(5000)); act(() => vi.advanceTimersByTime(5000));
expect(screen.getByText(/message A/)).toBeInTheDocument(); expect(screen.getByText(/message A/)).toBeInTheDocument();
expect(screen.queryByText(/message B/)).not.toBeInTheDocument(); expect(screen.queryByText(/message B/)).not.toBeInTheDocument();
// ...then it should close and message B should open... // ...then it should close and message B should open...
act(() => jest.runOnlyPendingTimers()); act(() => vi.runOnlyPendingTimers());
await waitFor(() => await waitFor(() =>
expect(screen.queryByText(/message A/)).not.toBeInTheDocument() expect(screen.queryByText(/message A/)).not.toBeInTheDocument()
); );
expect(screen.getByText(/message B/)).toBeInTheDocument(); expect(screen.getByText(/message B/)).toBeInTheDocument();
// ...then message B should start to close... // ...then message B should start to close...
act(() => jest.advanceTimersByTime(5000)); act(() => vi.advanceTimersByTime(5000));
expect(screen.queryByText(/message A/)).not.toBeInTheDocument(); expect(screen.queryByText(/message A/)).not.toBeInTheDocument();
expect(screen.getByText(/message B/)).toBeInTheDocument(); expect(screen.getByText(/message B/)).toBeInTheDocument();
// ...then message B should fully close. // ...then message B should fully close.
act(() => jest.runOnlyPendingTimers()); act(() => vi.runOnlyPendingTimers());
expect(screen.queryByText(/message A/)).not.toBeInTheDocument(); expect(screen.queryByText(/message A/)).not.toBeInTheDocument();
await waitFor(() => await waitFor(() =>
expect(screen.queryByText(/message B/)).not.toBeInTheDocument() expect(screen.queryByText(/message B/)).not.toBeInTheDocument()

13
ui/vitest.config.ts Normal file
View File

@ -0,0 +1,13 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2023 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/setupTests.ts"],
},
});