mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore: complete jest to vitest migration (#24216)
This commit is contained in:
@@ -91,12 +91,6 @@ updates:
|
||||
emotion:
|
||||
patterns:
|
||||
- "@emotion*"
|
||||
exclude-patterns:
|
||||
- "jest-runner-eslint"
|
||||
jest:
|
||||
patterns:
|
||||
- "jest"
|
||||
- "@types/jest"
|
||||
vite:
|
||||
patterns:
|
||||
- "vite*"
|
||||
|
||||
@@ -34,16 +34,14 @@ the most important.
|
||||
- [React](https://reactjs.org/) for the UI framework
|
||||
- [Typescript](https://www.typescriptlang.org/) to keep our sanity
|
||||
- [Vite](https://vitejs.dev/) to build the project
|
||||
- [Material V5](https://mui.com/material-ui/getting-started/) for UI components
|
||||
- [react-router](https://reactrouter.com/en/main) for routing
|
||||
- [TanStack Query v4](https://tanstack.com/query/v4/docs/react/overview) for
|
||||
- [TanStack Query](https://tanstack.com/query/v4/docs/react/overview) for
|
||||
fetching data
|
||||
- [axios](https://github.com/axios/axios) as fetching lib
|
||||
- [Vitest](https://vitest.dev/) for integration testing
|
||||
- [Playwright](https://playwright.dev/) for end-to-end (E2E) testing
|
||||
- [Jest](https://jestjs.io/) for integration testing
|
||||
- [Storybook](https://storybook.js.org/) and
|
||||
[Chromatic](https://www.chromatic.com/) for visual testing
|
||||
- [PNPM](https://pnpm.io/) as the package manager
|
||||
- [pnpm](https://pnpm.io/) as the package manager
|
||||
|
||||
## Structure
|
||||
|
||||
@@ -51,7 +49,6 @@ All UI-related code is in the `site` folder. Key directories include:
|
||||
|
||||
- **e2e** - End-to-end (E2E) tests
|
||||
- **src** - Source code
|
||||
- **mocks** - [Manual mocks](https://jestjs.io/docs/manual-mocks) used by Jest
|
||||
- **@types** - Custom types for dependencies that don't have defined types
|
||||
(largely code that has no server-side equivalent)
|
||||
- **api** - API function calls and types
|
||||
@@ -59,7 +56,7 @@ All UI-related code is in the `site` folder. Key directories include:
|
||||
- **components** - Reusable UI components without Coder specific business
|
||||
logic
|
||||
- **hooks** - Custom React hooks
|
||||
- **modules** - Coder-specific UI components
|
||||
- **modules** - Coder specific logic and components related to multiple parts of the UI
|
||||
- **pages** - Page-level components
|
||||
- **testHelpers** - Helper functions for integration testing
|
||||
- **theme** - theme configuration and color definitions
|
||||
@@ -286,9 +283,9 @@ local machine and forward the necessary ports to your workspace. At the end of
|
||||
the script, you will land _inside_ your workspace with environment variables set
|
||||
so you can simply execute the test (`pnpm run playwright:test`).
|
||||
|
||||
### Integration/Unit – Jest
|
||||
### Integration/Unit
|
||||
|
||||
We use Jest mostly for testing code that does _not_ pertain to React. Functions and classes that contain notable app logic, and which are well abstracted from React should have accompanying tests. If the logic is tightly coupled to a React component, a Storybook test or an E2E test may be a better option depending on the scenario.
|
||||
We use unit and integration tests mostly for testing code that does _not_ pertain to React. Functions and classes that contain notable app logic, and which are well abstracted from React should have accompanying tests. If the logic is tightly coupled to a React component, a Storybook test or an E2E test is usually a better option.
|
||||
|
||||
### Visual Testing – Storybook
|
||||
|
||||
|
||||
+7
-7
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"entry": ["./src/index.tsx", "./src/serviceWorker.ts"],
|
||||
"project": ["./src/**/*.ts", "./src/**/*.tsx", "./e2e/**/*.ts"],
|
||||
"project": [
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx",
|
||||
"./test/**/*.ts",
|
||||
"./e2e/**/*.ts"
|
||||
],
|
||||
"ignore": ["**/*Generated.ts", "src/api/chatModelOptions.ts"],
|
||||
"ignoreBinaries": ["protoc"],
|
||||
"ignoreDependencies": [
|
||||
"@babel/plugin-syntax-typescript",
|
||||
"@types/react-virtualized-auto-sizer",
|
||||
"babel-plugin-react-compiler",
|
||||
"jest_workaround",
|
||||
"ts-proto"
|
||||
],
|
||||
"jest": {
|
||||
"entry": "./src/**/*.jest.{ts,tsx}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
module.exports = {
|
||||
// Use a big timeout for CI.
|
||||
testTimeout: 20_000,
|
||||
maxWorkers: 8,
|
||||
projects: [
|
||||
{
|
||||
displayName: "test",
|
||||
roots: ["<rootDir>"],
|
||||
setupFiles: ["./jest.polyfills.js"],
|
||||
setupFilesAfterEnv: ["./jest.setup.ts"],
|
||||
extensionsToTreatAsEsm: [".ts"],
|
||||
transform: {
|
||||
"^.+\\.(t|j)sx?$": [
|
||||
"@swc/jest",
|
||||
{
|
||||
jsc: {
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
importSource: "@emotion/react",
|
||||
},
|
||||
},
|
||||
experimental: {
|
||||
plugins: [["jest_workaround", {}]],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: "jest-fixed-jsdom",
|
||||
testEnvironmentOptions: {
|
||||
customExportConditions: [""],
|
||||
},
|
||||
testRegex: "(/__tests__/.*|(\\.|/)(jest))\\.tsx?$",
|
||||
testPathIgnorePatterns: ["/node_modules/", "/e2e/"],
|
||||
transformIgnorePatterns: [],
|
||||
moduleDirectories: ["node_modules"],
|
||||
moduleNameMapper: {
|
||||
"\\.css$": "<rootDir>/src/testHelpers/styleMock.ts",
|
||||
"^@fontsource": "<rootDir>/src/testHelpers/styleMock.ts",
|
||||
"^@pierre/diffs/react$":
|
||||
"<rootDir>/src/testHelpers/pierreDiffsReactMock.tsx",
|
||||
},
|
||||
},
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
// included files
|
||||
"<rootDir>/**/*.ts",
|
||||
"<rootDir>/**/*.tsx",
|
||||
// excluded files
|
||||
"!<rootDir>/**/*.stories.tsx",
|
||||
"!<rootDir>/_jest/**/*.*",
|
||||
"!<rootDir>/api.ts",
|
||||
"!<rootDir>/coverage/**/*.*",
|
||||
"!<rootDir>/e2e/**/*.*",
|
||||
"!<rootDir>/jest-runner.eslint.config.js",
|
||||
"!<rootDir>/jest.config.js",
|
||||
"!<rootDir>/out/**/*.*",
|
||||
"!<rootDir>/storybook-static/**/*.*",
|
||||
],
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Necessary for MSW
|
||||
*
|
||||
* @note The block below contains polyfills for Node.js globals
|
||||
* required for Jest to function when running JSDOM tests.
|
||||
* These HAVE to be require's and HAVE to be in this exact
|
||||
* order, since "undici" depends on the "TextEncoder" global API.
|
||||
*
|
||||
* Consider migrating to a more modern test runner if
|
||||
* you don't want to deal with this.
|
||||
*/
|
||||
const { TextDecoder, TextEncoder } = require("node:util");
|
||||
const { ReadableStream } = require("node:stream/web");
|
||||
|
||||
Object.defineProperties(globalThis, {
|
||||
TextDecoder: { value: TextDecoder },
|
||||
TextEncoder: { value: TextEncoder },
|
||||
ReadableStream: { value: ReadableStream },
|
||||
});
|
||||
|
||||
const { Blob, File } = require("node:buffer");
|
||||
const { fetch, Headers, FormData, Request, Response } = require("undici");
|
||||
|
||||
Object.defineProperties(globalThis, {
|
||||
fetch: { value: fetch, writable: true },
|
||||
Blob: { value: Blob },
|
||||
File: { value: File },
|
||||
Headers: { value: Headers },
|
||||
FormData: { value: FormData },
|
||||
Request: { value: Request },
|
||||
Response: { value: Response },
|
||||
matchMedia: {
|
||||
value: (query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import "jest-location-mock";
|
||||
import crypto from "node:crypto";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { useMemo } from "react";
|
||||
import type { Region } from "#/api/typesGenerated";
|
||||
import type { ProxyLatencyReport } from "#/contexts/useProxyLatency";
|
||||
import { server } from "#/testHelpers/server";
|
||||
|
||||
// useProxyLatency does some http requests to determine latency.
|
||||
// This would fail unit testing, or at least make it very slow with
|
||||
// actual network requests. So just globally mock this hook.
|
||||
jest.mock("#/contexts/useProxyLatency", () => ({
|
||||
useProxyLatency: (proxies?: Region[]) => {
|
||||
// Must use `useMemo` here to avoid infinite loop.
|
||||
// Mocking the hook with a hook.
|
||||
const proxyLatencies = useMemo(() => {
|
||||
if (!proxies) {
|
||||
return {} as Record<string, ProxyLatencyReport>;
|
||||
}
|
||||
return proxies.reduce(
|
||||
(acc, proxy) => {
|
||||
acc[proxy.id] = {
|
||||
accurate: true,
|
||||
// Return a constant latency of 8ms.
|
||||
// If you make this random it could break stories.
|
||||
latencyMS: 8,
|
||||
at: new Date(),
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ProxyLatencyReport>,
|
||||
);
|
||||
}, [proxies]);
|
||||
|
||||
return { proxyLatencies, refetch: jest.fn() };
|
||||
},
|
||||
}));
|
||||
|
||||
global.scrollTo = jest.fn();
|
||||
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
// Polyfill pointer capture methods for JSDOM compatibility with Radix UI
|
||||
window.HTMLElement.prototype.hasPointerCapture = jest
|
||||
.fn()
|
||||
.mockReturnValue(false);
|
||||
window.HTMLElement.prototype.setPointerCapture = jest.fn();
|
||||
window.HTMLElement.prototype.releasePointerCapture = jest.fn();
|
||||
window.open = jest.fn();
|
||||
navigator.sendBeacon = jest.fn();
|
||||
|
||||
global.ResizeObserver = require("resize-observer-polyfill");
|
||||
|
||||
// Polyfill the getRandomValues that is used on utils/random.ts
|
||||
Object.defineProperty(global.self, "crypto", {
|
||||
value: {
|
||||
getRandomValues: crypto.randomFillSync,
|
||||
},
|
||||
});
|
||||
|
||||
// Establish API mocking before all tests through MSW.
|
||||
beforeAll(() =>
|
||||
server.listen({
|
||||
onUnhandledRequest: "warn",
|
||||
}),
|
||||
);
|
||||
|
||||
// Reset any request handlers that we may add during the tests,
|
||||
// so they don't affect other tests.
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
server.resetHandlers();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
// Clean up after the tests are finished.
|
||||
afterAll(() => server.close());
|
||||
|
||||
// biome-ignore lint/complexity/noUselessEmptyExport: This is needed because we are compiling under `--isolatedModules`
|
||||
export {};
|
||||
+3
-14
@@ -28,11 +28,10 @@
|
||||
"storybook": "STORYBOOK=true storybook dev -p 6006",
|
||||
"storybook:build": "storybook build",
|
||||
"storybook:ci": "storybook build --test",
|
||||
"test": "vitest run --project=unit && jest",
|
||||
"test": "vitest run --project=unit",
|
||||
"test:storybook": "vitest --project=storybook",
|
||||
"test:ci": "vitest run --project=unit && jest --silent",
|
||||
"test:ci": "vitest run --project=unit",
|
||||
"test:watch": "vitest --project=unit",
|
||||
"test:watch-jest": "jest --watch",
|
||||
"stats": "STATS=true pnpm build && npx http-server ./stats -p 8081 -c-1",
|
||||
"update-emojis": "cp -rf ./node_modules/emoji-datasource-apple/img/apple/64/* ./static/emojis && cp -f ./node_modules/emoji-datasource-apple/img/apple/sheets-256/64.png ./static/emojis/spritesheet.png"
|
||||
},
|
||||
@@ -109,7 +108,6 @@
|
||||
"react-window": "1.8.11",
|
||||
"recharts": "2.15.4",
|
||||
"remark-gfm": "4.0.1",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"semver": "7.7.3",
|
||||
"sonner": "2.0.7",
|
||||
"streamdown": "2.5.0",
|
||||
@@ -118,7 +116,6 @@
|
||||
"tzdata": "1.0.46",
|
||||
"ua-parser-js": "1.0.41",
|
||||
"ufuzzy": "npm:@leeoniya/ufuzzy@1.0.10",
|
||||
"undici": "6.22.0",
|
||||
"unique-names-generator": "4.7.1",
|
||||
"uuid": "9.0.1",
|
||||
"websocket-ts": "2.2.1",
|
||||
@@ -138,8 +135,6 @@
|
||||
"@storybook/addon-themes": "10.3.3",
|
||||
"@storybook/addon-vitest": "10.3.3",
|
||||
"@storybook/react-vite": "10.3.3",
|
||||
"@swc/core": "1.3.38",
|
||||
"@swc/jest": "0.2.37",
|
||||
"@tailwindcss/typography": "0.5.19",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
"@testing-library/react": "14.3.1",
|
||||
@@ -149,7 +144,6 @@
|
||||
"@types/express": "4.17.17",
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@types/humanize-duration": "3.27.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/lodash": "4.17.21",
|
||||
"@types/node": "20.19.25",
|
||||
"@types/novnc__novnc": "1.5.0",
|
||||
@@ -170,18 +164,14 @@
|
||||
"chromatic": "11.29.0",
|
||||
"dpdm": "3.14.0",
|
||||
"express": "4.21.2",
|
||||
"jest": "29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-environment-jsdom": "29.5.0",
|
||||
"jest-fixed-jsdom": "0.0.11",
|
||||
"jest-location-mock": "2.0.0",
|
||||
"jest-websocket-mock": "2.5.0",
|
||||
"jest_workaround": "0.1.14",
|
||||
"jsdom": "27.2.0",
|
||||
"knip": "5.71.0",
|
||||
"msw": "2.4.8",
|
||||
"postcss": "8.5.6",
|
||||
"protobufjs": "7.5.4",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"rollup-plugin-visualizer": "7.0.1",
|
||||
"rxjs": "7.8.2",
|
||||
"ssh2": "1.17.0",
|
||||
@@ -224,7 +214,6 @@
|
||||
"storybook-addon-remix-react-router"
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"@swc/core",
|
||||
"esbuild",
|
||||
"ssh2"
|
||||
]
|
||||
|
||||
Generated
+3
-2316
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
export default jest.fn();
|
||||
export default vi.fn();
|
||||
|
||||
@@ -97,8 +97,8 @@ export const IconField: FC<IconFieldProps> = ({
|
||||
Unfortunately, React doesn't provide an API to start warming a lazy component,
|
||||
so we just have to sneak it into the DOM, which is kind of annoying, but means
|
||||
that users shouldn't ever spend time waiting for it to load.
|
||||
- Except we don't do it when running tests, because Jest doesn't define
|
||||
`IntersectionObserver`, and it would make them slower anyway. */}
|
||||
- Except we don't do it when running tests, because it would make them
|
||||
slower anyway. */}
|
||||
{process.env.NODE_ENV !== "test" && (
|
||||
<div className="sr-only" aria-hidden="true">
|
||||
<Suspense>
|
||||
|
||||
@@ -49,8 +49,7 @@ export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
setPreferredColorScheme(event.matches ? "light" : "dark");
|
||||
};
|
||||
|
||||
// `addEventListener` here is a recent API that only _very_ up-to-date
|
||||
// browsers support, and that isn't mocked in Jest.
|
||||
// `addEventListener` here is a recent API that isn't mocked in tests.
|
||||
themeQuery.addEventListener?.("change", listener);
|
||||
return () => {
|
||||
themeQuery.removeEventListener?.("change", listener);
|
||||
|
||||
@@ -70,11 +70,11 @@ function setupMockClipboard(isSecure: boolean): SetupMockClipboardResult {
|
||||
// Don't need these other methods for any of the tests; read and write are
|
||||
// both synchronous and slower than the promise-based methods, so ideally
|
||||
// we won't ever need to call them in the hook logic
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
read: jest.fn(),
|
||||
write: jest.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
read: vi.fn(),
|
||||
write: vi.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -145,19 +145,19 @@ describe.each(secureContextValues)("useClipboard - secure: %j", (isSecure) => {
|
||||
} = setupMockClipboard(isSecure);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
|
||||
// Can't use jest.spyOn here because there's no guarantee that the mock
|
||||
// Can't use vi.spyOn here because there's no guarantee that the mock
|
||||
// browser environment actually implements execCommand. Trying to spy on an
|
||||
// undefined value will throw an error
|
||||
global.document.execCommand = mockExecCommand;
|
||||
|
||||
jest.spyOn(window, "navigator", "get").mockImplementation(() => ({
|
||||
vi.spyOn(window, "navigator", "get").mockImplementation(() => ({
|
||||
...originalNavigator,
|
||||
clipboard: mockClipboard,
|
||||
}));
|
||||
|
||||
jest.spyOn(console, "error").mockImplementation((errorValue, ...rest) => {
|
||||
vi.spyOn(console, "error").mockImplementation((errorValue, ...rest) => {
|
||||
const canIgnore =
|
||||
errorValue instanceof Error &&
|
||||
errorValue.message === COPY_FAILED_MESSAGE;
|
||||
@@ -169,9 +169,9 @@ describe.each(secureContextValues)("useClipboard - secure: %j", (isSecure) => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runAllTimers();
|
||||
jest.useRealTimers();
|
||||
jest.resetAllMocks();
|
||||
vi.runAllTimers();
|
||||
vi.useRealTimers();
|
||||
vi.resetAllMocks();
|
||||
global.document.execCommand = originalExecCommand;
|
||||
|
||||
// Still have to reset the mock clipboard state because the same mock values
|
||||
@@ -193,7 +193,7 @@ describe.each(secureContextValues)("useClipboard - secure: %j", (isSecure) => {
|
||||
// tests more annoying. Getting around that by waiting for all timeouts to
|
||||
// wrap up, but note that the value of showCopiedSuccess will become false
|
||||
// after runAllTimersAsync finishes
|
||||
await act(() => jest.runAllTimersAsync());
|
||||
await act(() => vi.runAllTimersAsync());
|
||||
|
||||
const clipboardText = getClipboardText();
|
||||
expect(clipboardText).toEqual(textToCopy);
|
||||
@@ -214,7 +214,7 @@ describe.each(secureContextValues)("useClipboard - secure: %j", (isSecure) => {
|
||||
|
||||
it("Should notify the user of an error using the provided callback", async () => {
|
||||
const textToCopy = "birds";
|
||||
const onError = jest.fn();
|
||||
const onError = vi.fn();
|
||||
const { result } = renderUseClipboard({ onError });
|
||||
|
||||
setSimulateFailure(true);
|
||||
@@ -223,7 +223,7 @@ describe.each(secureContextValues)("useClipboard - secure: %j", (isSecure) => {
|
||||
});
|
||||
|
||||
it("Should dispatch a new toast message to the global snackbar when errors happen while no error callback is provided to the hook", async () => {
|
||||
const toastErrorSpy = jest.spyOn(toast, "error");
|
||||
const toastErrorSpy = vi.spyOn(toast, "error");
|
||||
const textToCopy = "crow";
|
||||
const { result } = renderUseClipboard();
|
||||
|
||||
@@ -239,7 +239,7 @@ describe.each(secureContextValues)("useClipboard - secure: %j", (isSecure) => {
|
||||
// Snackbar state transitions that you might get if the hook uses the
|
||||
// default
|
||||
const textToCopy = "hamster";
|
||||
const { result } = renderUseClipboard({ onError: jest.fn() });
|
||||
const { result } = renderUseClipboard({ onError: vi.fn() });
|
||||
|
||||
setSimulateFailure(true);
|
||||
await act(() => result.current.copyToClipboard(textToCopy));
|
||||
@@ -264,7 +264,7 @@ describe.each(secureContextValues)("useClipboard - secure: %j", (isSecure) => {
|
||||
// inside of useEffect calls without having to think about dependencies too
|
||||
// much
|
||||
it("Ensures that the copyToClipboard function always maintains a stable reference across all re-renders", async () => {
|
||||
const initialOnError = jest.fn();
|
||||
const initialOnError = vi.fn();
|
||||
const { result, rerender } = renderUseClipboard({
|
||||
onError: initialOnError,
|
||||
clearErrorOnSuccess: true,
|
||||
@@ -278,7 +278,7 @@ describe.each(secureContextValues)("useClipboard - secure: %j", (isSecure) => {
|
||||
|
||||
// Re-render with new onError prop and then swap back to simplify
|
||||
// testing
|
||||
rerender({ onError: jest.fn() });
|
||||
rerender({ onError: vi.fn() });
|
||||
expect(result.current.copyToClipboard).toBe(initialCopy);
|
||||
rerender({ onError: initialOnError });
|
||||
|
||||
@@ -310,13 +310,13 @@ describe.each(secureContextValues)("useClipboard - secure: %j", (isSecure) => {
|
||||
});
|
||||
|
||||
it("Always uses the most up-to-date onError prop", async () => {
|
||||
const initialOnError = jest.fn();
|
||||
const initialOnError = vi.fn();
|
||||
const { result, rerender } = renderUseClipboard({
|
||||
onError: initialOnError,
|
||||
});
|
||||
setSimulateFailure(true);
|
||||
|
||||
const secondOnError = jest.fn();
|
||||
const secondOnError = vi.fn();
|
||||
rerender({ onError: secondOnError });
|
||||
await act(() => result.current.copyToClipboard("dummy-text"));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
|
||||
import type { Mock } from "vitest";
|
||||
import { agentLogsKey } from "#/api/queries/workspaces";
|
||||
import type { WorkspaceAgentLog } from "#/api/typesGenerated";
|
||||
import { MockWorkspaceAgent } from "#/testHelpers/entities";
|
||||
@@ -41,7 +42,7 @@ export const ClickOnDownload: Story = {
|
||||
`${MockWorkspaceAgent.name}-logs.txt`,
|
||||
),
|
||||
);
|
||||
const blob: Blob = (args.download as jest.Mock).mock.calls[0][0];
|
||||
const blob: Blob = (args.download as Mock).mock.calls[0][0];
|
||||
await expect(blob.type).toEqual("text/plain");
|
||||
},
|
||||
};
|
||||
|
||||
+10
-10
@@ -84,12 +84,12 @@ describe("useAgentContainers", () => {
|
||||
});
|
||||
|
||||
it("handles parsing errors from WebSocket", async () => {
|
||||
const toastErrorSpy = jest.spyOn(toast, "error");
|
||||
const watchAgentContainersSpy = jest.spyOn(API, "watchAgentContainers");
|
||||
const toastErrorSpy = vi.spyOn(toast, "error");
|
||||
const watchAgentContainersSpy = vi.spyOn(API, "watchAgentContainers");
|
||||
|
||||
const mockSocket = {
|
||||
addEventListener: jest.fn(),
|
||||
close: jest.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
close: vi.fn(),
|
||||
};
|
||||
watchAgentContainersSpy.mockReturnValue(
|
||||
mockSocket as unknown as OneWayWebSocket<WorkspaceAgentListContainersResponse>,
|
||||
@@ -146,12 +146,12 @@ describe("useAgentContainers", () => {
|
||||
});
|
||||
|
||||
it("handles WebSocket errors", async () => {
|
||||
const toastErrorSpy = jest.spyOn(toast, "error");
|
||||
const watchAgentContainersSpy = jest.spyOn(API, "watchAgentContainers");
|
||||
const toastErrorSpy = vi.spyOn(toast, "error");
|
||||
const watchAgentContainersSpy = vi.spyOn(API, "watchAgentContainers");
|
||||
|
||||
const mockSocket = {
|
||||
addEventListener: jest.fn(),
|
||||
close: jest.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
close: vi.fn(),
|
||||
};
|
||||
watchAgentContainersSpy.mockReturnValue(
|
||||
mockSocket as unknown as OneWayWebSocket<WorkspaceAgentListContainersResponse>,
|
||||
@@ -204,7 +204,7 @@ describe("useAgentContainers", () => {
|
||||
});
|
||||
|
||||
it("does not establish WebSocket connection when agent is not connected", () => {
|
||||
const watchAgentContainersSpy = jest.spyOn(API, "watchAgentContainers");
|
||||
const watchAgentContainersSpy = vi.spyOn(API, "watchAgentContainers");
|
||||
|
||||
const disconnectedAgent = {
|
||||
...MockWorkspaceAgent,
|
||||
@@ -222,7 +222,7 @@ describe("useAgentContainers", () => {
|
||||
});
|
||||
|
||||
it("does not establish WebSocket connection when dev container feature is not enabled", async () => {
|
||||
const watchAgentContainersSpy = jest.spyOn(API, "watchAgentContainers");
|
||||
const watchAgentContainersSpy = vi.spyOn(API, "watchAgentContainers");
|
||||
|
||||
server.use(
|
||||
http.get(
|
||||
+8
-7
@@ -1,6 +1,7 @@
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { act } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { MockInstance } from "vitest";
|
||||
import * as apiModule from "#/api/api";
|
||||
import type { WorkspaceAgentLog } from "#/api/typesGenerated";
|
||||
import { MockWorkspaceAgent } from "#/testHelpers/entities";
|
||||
@@ -45,7 +46,7 @@ type MountHookOptions = Readonly<{
|
||||
type MountHookResult = Readonly<{
|
||||
serverResult: ServerResult;
|
||||
rerender: (props: { agentId: string; enabled: boolean }) => void;
|
||||
toastError: jest.SpyInstance;
|
||||
toastError: MockInstance;
|
||||
|
||||
// Note: the `current` property is only "halfway" readonly; the value is
|
||||
// readonly, but the key is still mutable
|
||||
@@ -56,9 +57,8 @@ function mountHook(options: MountHookOptions): MountHookResult {
|
||||
const { initialAgentId, enabled = true } = options;
|
||||
const serverResult: ServerResult = { current: undefined };
|
||||
|
||||
jest
|
||||
.spyOn(apiModule, "watchWorkspaceAgentLogs")
|
||||
.mockImplementation((agentId, params) => {
|
||||
vi.spyOn(apiModule, "watchWorkspaceAgentLogs").mockImplementation(
|
||||
(agentId, params) => {
|
||||
return new OneWayWebSocket({
|
||||
apiRoute: `/api/v2/workspaceagents/${agentId}/logs`,
|
||||
searchParams: new URLSearchParams({
|
||||
@@ -71,10 +71,11 @@ function mountHook(options: MountHookOptions): MountHookResult {
|
||||
return mockSocket;
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
void jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
const toastError = jest.spyOn(toast, "error");
|
||||
void vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const toastError = vi.spyOn(toast, "error");
|
||||
|
||||
const { result: hookResult, rerender } = renderHook(
|
||||
(props) => useAgentLogs(props),
|
||||
+5
-5
@@ -67,10 +67,10 @@ const mockRequiredParameter = createMockParameter({
|
||||
});
|
||||
|
||||
describe("DynamicParameter", () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const mockOnChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Input Parameter", () => {
|
||||
@@ -800,7 +800,7 @@ describe("DynamicParameter", () => {
|
||||
});
|
||||
|
||||
it("calls onChange when numeric value changes (debounced)", () => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
render(
|
||||
<DynamicParameter
|
||||
parameter={mockNumberInputParameter}
|
||||
@@ -813,11 +813,11 @@ describe("DynamicParameter", () => {
|
||||
fireEvent.change(input, { target: { value: "7" } });
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith("7");
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
|
||||
import type { Mock } from "vitest";
|
||||
import { agentLogsKey, buildLogsKey } from "#/api/queries/workspaces";
|
||||
import { MockWorkspace, MockWorkspaceAgent } from "#/testHelpers/entities";
|
||||
import { withDesktopViewport } from "#/testHelpers/storybook";
|
||||
@@ -61,7 +62,7 @@ export const DownloadLogs: Story = {
|
||||
`${MockWorkspace.name}-logs.zip`,
|
||||
),
|
||||
);
|
||||
const blob: Blob = (args.download as jest.Mock).mock.calls[0][0];
|
||||
const blob: Blob = (args.download as Mock).mock.calls[0][0];
|
||||
await expect(blob.type).toEqual("application/zip");
|
||||
},
|
||||
};
|
||||
|
||||
+21
-27
@@ -30,7 +30,7 @@ const renderPage = async (searchParams: URLSearchParams) => {
|
||||
return view;
|
||||
};
|
||||
|
||||
test("Create template from starter template", async () => {
|
||||
test("Create template from starter template", { timeout: 20_000 }, async () => {
|
||||
// Render page, fill the name and submit
|
||||
const searchParams = new URLSearchParams({
|
||||
exampleId: MockTemplateExample.id,
|
||||
@@ -38,14 +38,14 @@ test("Create template from starter template", async () => {
|
||||
const { router, container } = await renderPage(searchParams);
|
||||
const form = container.querySelector("form") as HTMLFormElement;
|
||||
|
||||
jest.spyOn(API, "createTemplateVersion").mockResolvedValueOnce({
|
||||
vi.spyOn(API, "createTemplateVersion").mockResolvedValueOnce({
|
||||
...MockTemplateVersion,
|
||||
job: {
|
||||
...MockTemplateVersion.job,
|
||||
status: "pending",
|
||||
},
|
||||
});
|
||||
jest.spyOn(API, "getTemplateVersion").mockResolvedValue({
|
||||
vi.spyOn(API, "getTemplateVersion").mockResolvedValue({
|
||||
...MockTemplateVersion,
|
||||
job: {
|
||||
...MockTemplateVersion.job,
|
||||
@@ -53,13 +53,11 @@ test("Create template from starter template", async () => {
|
||||
error_code: "REQUIRED_TEMPLATE_VARIABLES",
|
||||
},
|
||||
});
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionVariables")
|
||||
.mockResolvedValue([
|
||||
MockTemplateVersionVariable1,
|
||||
MockTemplateVersionVariable2,
|
||||
MockTemplateVersionVariable3,
|
||||
]);
|
||||
vi.spyOn(API, "getTemplateVersionVariables").mockResolvedValue([
|
||||
MockTemplateVersionVariable1,
|
||||
MockTemplateVersionVariable2,
|
||||
MockTemplateVersionVariable3,
|
||||
]);
|
||||
await userEvent.type(screen.getByLabelText(/Name/), "my-template");
|
||||
await userEvent.click(within(form).getByRole("button", { name: /save/i }));
|
||||
|
||||
@@ -84,12 +82,10 @@ test("Create template from starter template", async () => {
|
||||
// Select third variable on radio
|
||||
await userEvent.click(screen.getByLabelText(/True/));
|
||||
// Setup the mock for the second template version creation before submit the form
|
||||
jest.clearAllMocks();
|
||||
jest
|
||||
.spyOn(API, "createTemplateVersion")
|
||||
.mockResolvedValue(MockTemplateVersion);
|
||||
jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate);
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(API, "createTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
vi.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
vi.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate);
|
||||
await userEvent.click(within(form).getByRole("button", { name: /save/i }));
|
||||
await waitFor(() => expect(API.createTemplate).toBeCalledTimes(1));
|
||||
expect(router.state.location.pathname).toEqual(
|
||||
@@ -109,11 +105,11 @@ test("Create template from starter template", async () => {
|
||||
});
|
||||
|
||||
test("Create template from duplicating a template", async () => {
|
||||
jest.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
|
||||
jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionVariables")
|
||||
.mockResolvedValue([MockTemplateVersionVariable1]);
|
||||
vi.spyOn(API, "getTemplateByName").mockResolvedValue(MockTemplate);
|
||||
vi.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
vi.spyOn(API, "getTemplateVersionVariables").mockResolvedValue([
|
||||
MockTemplateVersionVariable1,
|
||||
]);
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
fromTemplate: MockTemplate.id,
|
||||
@@ -133,11 +129,9 @@ test("Create template from duplicating a template", async () => {
|
||||
}),
|
||||
).toHaveValue(MockTemplateVersionVariable1.value);
|
||||
// Create template
|
||||
jest
|
||||
.spyOn(API, "createTemplateVersion")
|
||||
.mockResolvedValue(MockTemplateVersion);
|
||||
jest.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate);
|
||||
vi.spyOn(API, "createTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
vi.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
vi.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate);
|
||||
await userEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
await waitFor(() => {
|
||||
expect(router.state.location.pathname).toEqual(
|
||||
@@ -148,7 +142,7 @@ test("Create template from duplicating a template", async () => {
|
||||
|
||||
test("The page displays an error if the upload fails", async () => {
|
||||
const errMsg = "Unsupported content type header";
|
||||
jest.spyOn(API, "uploadFile").mockRejectedValueOnce(
|
||||
vi.spyOn(API, "uploadFile").mockRejectedValueOnce(
|
||||
mockApiError({
|
||||
message: errMsg,
|
||||
}),
|
||||
+1
-1
@@ -9,7 +9,7 @@ import CreateTokenPage from "./CreateTokenPage";
|
||||
|
||||
describe("TokenPage", () => {
|
||||
it("shows the success modal", async () => {
|
||||
jest.spyOn(API, "createToken").mockResolvedValueOnce({
|
||||
vi.spyOn(API, "createToken").mockResolvedValueOnce({
|
||||
key: "abcd",
|
||||
});
|
||||
|
||||
+56
-59
@@ -40,17 +40,16 @@ describe("CreateWorkspacePage", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
|
||||
jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
|
||||
jest.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]);
|
||||
jest.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([]);
|
||||
jest.spyOn(API, "createWorkspace").mockResolvedValue(MockWorkspace);
|
||||
jest.spyOn(API, "checkAuthorization").mockResolvedValue(MockPermissions);
|
||||
vi.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
|
||||
vi.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]);
|
||||
vi.spyOn(API, "getTemplateVersionPresets").mockResolvedValue([]);
|
||||
vi.spyOn(API, "createWorkspace").mockResolvedValue(MockWorkspace);
|
||||
vi.spyOn(API, "checkAuthorization").mockResolvedValue(MockPermissions);
|
||||
|
||||
jest
|
||||
.spyOn(API, "templateVersionDynamicParameters")
|
||||
.mockImplementation((_versionId, _ownerId, callbacks) => {
|
||||
vi.spyOn(API, "templateVersionDynamicParameters").mockImplementation(
|
||||
(_versionId, _ownerId, callbacks) => {
|
||||
const [mockWebSocket, publisher] = createMockWebSocket("ws://test");
|
||||
|
||||
mockWebSocket.addEventListener("message", (event) => {
|
||||
@@ -73,11 +72,13 @@ describe("CreateWorkspacePage", () => {
|
||||
);
|
||||
|
||||
return mockWebSocket;
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("WebSocket Integration", () => {
|
||||
@@ -107,9 +108,8 @@ describe("CreateWorkspacePage", () => {
|
||||
it("sends parameter updates via WebSocket when form values change", async () => {
|
||||
const [mockWebSocket, publisher] = createMockWebSocket("ws://test");
|
||||
|
||||
jest
|
||||
.spyOn(API, "templateVersionDynamicParameters")
|
||||
.mockImplementation((_versionId, _ownerId, callbacks) => {
|
||||
vi.spyOn(API, "templateVersionDynamicParameters").mockImplementation(
|
||||
(_versionId, _ownerId, callbacks) => {
|
||||
mockWebSocket.addEventListener("message", (event) => {
|
||||
callbacks.onMessage(JSON.parse(event.data));
|
||||
});
|
||||
@@ -130,7 +130,8 @@ describe("CreateWorkspacePage", () => {
|
||||
);
|
||||
|
||||
return mockWebSocket;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
renderCreateWorkspacePage();
|
||||
await waitForLoaderToBeRemoved();
|
||||
@@ -144,7 +145,7 @@ describe("CreateWorkspacePage", () => {
|
||||
within(instanceTypeField).getByRole("combobox");
|
||||
expect(instanceTypeSelect).toBeInTheDocument();
|
||||
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(instanceTypeSelect);
|
||||
@@ -160,29 +161,29 @@ describe("CreateWorkspacePage", () => {
|
||||
await userEvent.click(mediumOption!);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(mockWebSocket.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"instance_type":"t3.medium"'),
|
||||
);
|
||||
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("handles WebSocket error gracefully", async () => {
|
||||
const [mockWebSocket, mockPublisher] = createMockWebSocket("ws://test");
|
||||
|
||||
jest
|
||||
.spyOn(API, "templateVersionDynamicParameters")
|
||||
.mockImplementation((_versionId, _ownerId, callbacks) => {
|
||||
vi.spyOn(API, "templateVersionDynamicParameters").mockImplementation(
|
||||
(_versionId, _ownerId, callbacks) => {
|
||||
mockWebSocket.addEventListener("error", () => {
|
||||
callbacks.onError(new Error("Connection failed"));
|
||||
});
|
||||
|
||||
return mockWebSocket;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
renderCreateWorkspacePage();
|
||||
|
||||
@@ -199,15 +200,15 @@ describe("CreateWorkspacePage", () => {
|
||||
it("handles WebSocket close event", async () => {
|
||||
const [mockWebSocket, mockPublisher] = createMockWebSocket("ws://test");
|
||||
|
||||
jest
|
||||
.spyOn(API, "templateVersionDynamicParameters")
|
||||
.mockImplementation((_versionId, _ownerId, callbacks) => {
|
||||
vi.spyOn(API, "templateVersionDynamicParameters").mockImplementation(
|
||||
(_versionId, _ownerId, callbacks) => {
|
||||
mockWebSocket.addEventListener("close", () => {
|
||||
callbacks.onClose();
|
||||
});
|
||||
|
||||
return mockWebSocket;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
renderCreateWorkspacePage();
|
||||
|
||||
@@ -225,9 +226,8 @@ describe("CreateWorkspacePage", () => {
|
||||
|
||||
it("only parameters from latest response are displayed", async () => {
|
||||
const [mockWebSocket, mockPublisher] = createMockWebSocket("ws://test");
|
||||
jest
|
||||
.spyOn(API, "templateVersionDynamicParameters")
|
||||
.mockImplementation((_versionId, _ownerId, callbacks) => {
|
||||
vi.spyOn(API, "templateVersionDynamicParameters").mockImplementation(
|
||||
(_versionId, _ownerId, callbacks) => {
|
||||
mockWebSocket.addEventListener("message", (event) => {
|
||||
callbacks.onMessage(JSON.parse(event.data));
|
||||
});
|
||||
@@ -244,7 +244,8 @@ describe("CreateWorkspacePage", () => {
|
||||
);
|
||||
|
||||
return mockWebSocket;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
renderCreateWorkspacePage();
|
||||
await waitForLoaderToBeRemoved();
|
||||
@@ -277,9 +278,8 @@ describe("CreateWorkspacePage", () => {
|
||||
|
||||
describe("Dynamic Parameter Types", () => {
|
||||
it("displays parameter validation errors", async () => {
|
||||
jest
|
||||
.spyOn(API, "templateVersionDynamicParameters")
|
||||
.mockImplementation((_versionId, _ownerId, callbacks) => {
|
||||
vi.spyOn(API, "templateVersionDynamicParameters").mockImplementation(
|
||||
(_versionId, _ownerId, callbacks) => {
|
||||
const [mockWebSocket, publisher] = createMockWebSocket("ws://test");
|
||||
|
||||
mockWebSocket.addEventListener("message", (event) => {
|
||||
@@ -293,7 +293,8 @@ describe("CreateWorkspacePage", () => {
|
||||
);
|
||||
|
||||
return mockWebSocket;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
renderCreateWorkspacePage();
|
||||
await waitForLoaderToBeRemoved();
|
||||
@@ -337,9 +338,8 @@ describe("CreateWorkspacePage", () => {
|
||||
diagnostics: [],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(API, "templateVersionDynamicParameters")
|
||||
.mockImplementation((_versionId, _ownerId, callbacks) => {
|
||||
vi.spyOn(API, "templateVersionDynamicParameters").mockImplementation(
|
||||
(_versionId, _ownerId, callbacks) => {
|
||||
const [mockWebSocket, publisher] = createMockWebSocket("ws://test");
|
||||
|
||||
mockWebSocket.addEventListener("message", (event) => {
|
||||
@@ -355,7 +355,7 @@ describe("CreateWorkspacePage", () => {
|
||||
);
|
||||
|
||||
const originalSend = mockWebSocket.send;
|
||||
mockWebSocket.send = jest.fn((data) => {
|
||||
mockWebSocket.send = vi.fn((data) => {
|
||||
originalSend.call(mockWebSocket, data);
|
||||
|
||||
if (typeof data === "string" && data.includes('"200"')) {
|
||||
@@ -368,7 +368,8 @@ describe("CreateWorkspacePage", () => {
|
||||
});
|
||||
|
||||
return mockWebSocket;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
renderCreateWorkspacePage();
|
||||
await waitForLoaderToBeRemoved();
|
||||
@@ -414,9 +415,9 @@ describe("CreateWorkspacePage", () => {
|
||||
|
||||
describe("External Authentication", () => {
|
||||
it("displays external auth providers", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionExternalAuth")
|
||||
.mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
|
||||
vi.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
|
||||
MockTemplateVersionExternalAuthGithub,
|
||||
]);
|
||||
|
||||
renderCreateWorkspacePage();
|
||||
await waitForLoaderToBeRemoved();
|
||||
@@ -430,11 +431,9 @@ describe("CreateWorkspacePage", () => {
|
||||
});
|
||||
|
||||
it("shows authenticated state for connected providers", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionExternalAuth")
|
||||
.mockResolvedValue([
|
||||
MockTemplateVersionExternalAuthGithubAuthenticated,
|
||||
]);
|
||||
vi.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
|
||||
MockTemplateVersionExternalAuthGithubAuthenticated,
|
||||
]);
|
||||
|
||||
renderCreateWorkspacePage();
|
||||
await waitForLoaderToBeRemoved();
|
||||
@@ -446,9 +445,9 @@ describe("CreateWorkspacePage", () => {
|
||||
});
|
||||
|
||||
it("prevents auto-creation when required external auth is missing", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionExternalAuth")
|
||||
.mockResolvedValue([MockTemplateVersionExternalAuthGithub]);
|
||||
vi.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
|
||||
MockTemplateVersionExternalAuthGithub,
|
||||
]);
|
||||
|
||||
renderCreateWorkspacePage(
|
||||
`/templates/${MockTemplate.name}/workspace?mode=auto&version=${MockTemplate.id}`,
|
||||
@@ -470,14 +469,12 @@ describe("CreateWorkspacePage", () => {
|
||||
|
||||
describe("Auto-creation Mode", () => {
|
||||
it("falls back to form mode when auto-creation fails", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionExternalAuth")
|
||||
.mockResolvedValue([
|
||||
MockTemplateVersionExternalAuthGithubAuthenticated,
|
||||
]);
|
||||
jest
|
||||
.spyOn(API, "createWorkspace")
|
||||
.mockRejectedValue(new Error("Auto-creation failed"));
|
||||
vi.spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
|
||||
MockTemplateVersionExternalAuthGithubAuthenticated,
|
||||
]);
|
||||
vi.spyOn(API, "createWorkspace").mockRejectedValue(
|
||||
new Error("Auto-creation failed"),
|
||||
);
|
||||
|
||||
renderCreateWorkspacePage(
|
||||
`/templates/${MockTemplate.name}/workspace?mode=auto`,
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
|
||||
import type { Mock } from "vitest";
|
||||
import { MockOrganizationSyncSettings } from "#/testHelpers/entities";
|
||||
import { ExportPolicyButton } from "./ExportPolicyButton";
|
||||
|
||||
@@ -32,7 +33,7 @@ export const ClickExportPolicy: Story = {
|
||||
"organizations_policy.json",
|
||||
),
|
||||
);
|
||||
const blob: Blob = (args.download as jest.Mock).mock.lastCall[0];
|
||||
const blob: Blob = (args.download as Mock).mock.lastCall![0];
|
||||
await expect(blob.type).toEqual("application/json");
|
||||
await expect(await blob.text()).toEqual(
|
||||
JSON.stringify(MockOrganizationSyncSettings, null, 2),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
|
||||
import type { Mock } from "vitest";
|
||||
import {
|
||||
MockGroupSyncSettings,
|
||||
MockOrganization,
|
||||
@@ -40,7 +41,7 @@ export const ClickExportGroupPolicy: Story = {
|
||||
`${MockOrganization.name}_groups-policy.json`,
|
||||
),
|
||||
);
|
||||
const blob: Blob = (args.download as jest.Mock).mock.lastCall[0];
|
||||
const blob: Blob = (args.download as Mock).mock.lastCall![0];
|
||||
await expect(blob.type).toEqual("application/json");
|
||||
await expect(await blob.text()).toEqual(
|
||||
JSON.stringify(MockGroupSyncSettings, null, 2),
|
||||
@@ -66,7 +67,7 @@ export const ClickExportRolePolicy: Story = {
|
||||
`${MockOrganization.name}_roles-policy.json`,
|
||||
),
|
||||
);
|
||||
const blob: Blob = (args.download as jest.Mock).mock.lastCall[0];
|
||||
const blob: Blob = (args.download as Mock).mock.lastCall![0];
|
||||
await expect(blob.type).toEqual("application/json");
|
||||
await expect(await blob.text()).toEqual(
|
||||
JSON.stringify(MockRoleSyncSettings, null, 2),
|
||||
|
||||
+7
-6
@@ -14,9 +14,10 @@ import {
|
||||
import TemplateEmbedPage from "./TemplateEmbedPage";
|
||||
|
||||
test("Users can fill the parameters and copy the open in coder url", async () => {
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValue([parameter1, parameter2]);
|
||||
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValue([
|
||||
parameter1,
|
||||
parameter2,
|
||||
]);
|
||||
|
||||
renderWithAuth(
|
||||
<TemplateLayout>
|
||||
@@ -47,10 +48,10 @@ test("Users can fill the parameters and copy the open in coder url", async () =>
|
||||
await user.clear(secondParameterField);
|
||||
await user.type(secondParameterField, "123456");
|
||||
|
||||
jest.spyOn(window.navigator.clipboard, "writeText");
|
||||
vi.spyOn(navigator.clipboard, "writeText");
|
||||
const copyButton = screen.getByRole("button", { name: /copy/i });
|
||||
await userEvent.click(copyButton);
|
||||
expect(window.navigator.clipboard.writeText).toBeCalledWith(
|
||||
`[](http://localhost/templates/${MockTemplate.organization_name}/${MockTemplate.name}/workspace?mode=manual&name=my-first-workspace¶m.first_parameter=firstParameterValue¶m.second_parameter=123456)`,
|
||||
expect(navigator.clipboard.writeText).toBeCalledWith(
|
||||
`[](${location.origin}/templates/${MockTemplate.organization_name}/${MockTemplate.name}/workspace?mode=manual&name=my-first-workspace¶m.first_parameter=firstParameterValue¶m.second_parameter=123456)`,
|
||||
);
|
||||
});
|
||||
@@ -50,7 +50,7 @@ interface TemplateEmbedPageViewProps {
|
||||
templateParameters?: TemplateVersionParameter[];
|
||||
}
|
||||
|
||||
const deploymentUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const deploymentUrl = `${location.protocol}//${location.host}`;
|
||||
|
||||
function getClipboardCopyContent(
|
||||
templateName: string,
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import TemplateFilesPage from "./TemplateFilesPage";
|
||||
|
||||
// Occasionally, Jest encounters HTML5 canvas errors. As the SyntaxHighlight is
|
||||
// not required for these tests, we can safely mock it.
|
||||
jest.mock("#/components/SyntaxHighlighter/SyntaxHighlighter", () => ({
|
||||
vi.mock("#/components/SyntaxHighlighter/SyntaxHighlighter", () => ({
|
||||
SyntaxHighlighter: () => <div data-testid="syntax-highlighter" />,
|
||||
}));
|
||||
|
||||
+4
-4
@@ -10,14 +10,14 @@ const wrapper = ({ children }: { children: React.ReactNode }) =>
|
||||
|
||||
test("delete dialog starts closed", () => {
|
||||
const { result } = renderHook(
|
||||
() => useDeletionDialogState(MockTemplate.id, jest.fn()),
|
||||
() => useDeletionDialogState(MockTemplate.id, vi.fn()),
|
||||
{ wrapper },
|
||||
);
|
||||
expect(result.current.isDeleteDialogOpen).toBeFalsy();
|
||||
});
|
||||
|
||||
test("confirm template deletion", async () => {
|
||||
const onDeleteTemplate = jest.fn();
|
||||
const onDeleteTemplate = vi.fn();
|
||||
const { result } = renderHook(
|
||||
() => useDeletionDialogState(MockTemplate.id, onDeleteTemplate),
|
||||
{ wrapper },
|
||||
@@ -28,7 +28,7 @@ test("confirm template deletion", async () => {
|
||||
});
|
||||
expect(result.current.isDeleteDialogOpen).toBeTruthy();
|
||||
|
||||
jest.spyOn(API, "deleteTemplate");
|
||||
vi.spyOn(API, "deleteTemplate");
|
||||
await act(async () => result.current.confirmDelete());
|
||||
await waitFor(() => expect(API.deleteTemplate).toBeCalledTimes(1));
|
||||
await waitFor(() => expect(onDeleteTemplate).toBeCalledTimes(1));
|
||||
@@ -36,7 +36,7 @@ test("confirm template deletion", async () => {
|
||||
|
||||
test("cancel template deletion", () => {
|
||||
const { result } = renderHook(
|
||||
() => useDeletionDialogState(MockTemplate.id, jest.fn()),
|
||||
() => useDeletionDialogState(MockTemplate.id, vi.fn()),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
+15
-7
@@ -105,10 +105,14 @@ const fillAndSubmitForm = async ({
|
||||
await userEvent.click(submitButton);
|
||||
};
|
||||
|
||||
describe("TemplateSettingsPage", () => {
|
||||
describe("TemplateSettingsPage", { timeout: 20_000 }, () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("succeeds", async () => {
|
||||
await renderTemplateSettingsPage();
|
||||
jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
|
||||
vi.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
|
||||
...MockTemplate,
|
||||
...validFormValues,
|
||||
});
|
||||
@@ -118,7 +122,7 @@ describe("TemplateSettingsPage", () => {
|
||||
|
||||
it("displays an error if the name is taken", async () => {
|
||||
await renderTemplateSettingsPage();
|
||||
jest.spyOn(API, "updateTemplateMeta").mockRejectedValueOnce(
|
||||
vi.spyOn(API, "updateTemplateMeta").mockRejectedValueOnce(
|
||||
mockApiError({
|
||||
message: `Template with name "test-template" already exists`,
|
||||
validations: [
|
||||
@@ -173,14 +177,16 @@ describe("TemplateSettingsPage", () => {
|
||||
});
|
||||
}),
|
||||
);
|
||||
const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta");
|
||||
const updateTemplateMetaSpy = vi.spyOn(API, "updateTemplateMeta");
|
||||
const deprecationMessage = "This template is deprecated";
|
||||
|
||||
await renderTemplateSettingsPage();
|
||||
await deprecateTemplate(deprecationMessage);
|
||||
await waitFor(() =>
|
||||
expect(updateTemplateMetaSpy).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
|
||||
const [templateId, data] = updateTemplateMetaSpy.mock.calls[0];
|
||||
|
||||
expect(templateId).toEqual(MockTemplate.id);
|
||||
expect(data).toEqual(
|
||||
expect.objectContaining({ deprecation_message: deprecationMessage }),
|
||||
@@ -198,13 +204,15 @@ describe("TemplateSettingsPage", () => {
|
||||
});
|
||||
}),
|
||||
);
|
||||
const updateTemplateMetaSpy = jest.spyOn(API, "updateTemplateMeta");
|
||||
const updateTemplateMetaSpy = vi.spyOn(API, "updateTemplateMeta");
|
||||
|
||||
await renderTemplateSettingsPage();
|
||||
await deprecateTemplate("This template should not be able to deprecate");
|
||||
await waitFor(() =>
|
||||
expect(updateTemplateMetaSpy).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
|
||||
const [templateId, data] = updateTemplateMetaSpy.mock.calls[0];
|
||||
|
||||
expect(templateId).toEqual(MockTemplate.id);
|
||||
expect(data).toEqual(
|
||||
expect.objectContaining({ deprecation_message: "" }),
|
||||
+20
-26
@@ -19,7 +19,7 @@ import TemplateVariablesPage from "./TemplateVariablesPage";
|
||||
// a real `delay(1000)` call. Without this mock the 1 s wall-clock wait races
|
||||
// against the default `waitFor` timeout (also 1 s), making the "submit"
|
||||
// assertion flaky in CI.
|
||||
jest.mock("#/utils/delay", () => ({
|
||||
vi.mock("#/utils/delay", () => ({
|
||||
delay: () => Promise.resolve(),
|
||||
}));
|
||||
|
||||
@@ -39,16 +39,14 @@ const renderTemplateVariablesPage = async () => {
|
||||
|
||||
describe("TemplateVariablesPage", () => {
|
||||
it("renders with variables", async () => {
|
||||
jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate);
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersion")
|
||||
.mockResolvedValueOnce(MockTemplateVersion);
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionVariables")
|
||||
.mockResolvedValueOnce([
|
||||
MockTemplateVersionVariable1,
|
||||
MockTemplateVersionVariable2,
|
||||
]);
|
||||
vi.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate);
|
||||
vi.spyOn(API, "getTemplateVersion").mockResolvedValueOnce(
|
||||
MockTemplateVersion,
|
||||
);
|
||||
vi.spyOn(API, "getTemplateVersionVariables").mockResolvedValueOnce([
|
||||
MockTemplateVersionVariable1,
|
||||
MockTemplateVersionVariable2,
|
||||
]);
|
||||
|
||||
await renderTemplateVariablesPage();
|
||||
|
||||
@@ -64,20 +62,16 @@ describe("TemplateVariablesPage", () => {
|
||||
});
|
||||
|
||||
it("user submits the form successfully", async () => {
|
||||
jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate);
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersion")
|
||||
.mockResolvedValue(MockTemplateVersion);
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionVariables")
|
||||
.mockResolvedValueOnce([
|
||||
MockTemplateVersionVariable1,
|
||||
MockTemplateVersionVariable2,
|
||||
]);
|
||||
jest
|
||||
.spyOn(API, "createTemplateVersion")
|
||||
.mockResolvedValueOnce(MockTemplateVersion2);
|
||||
jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({
|
||||
vi.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate);
|
||||
vi.spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
|
||||
vi.spyOn(API, "getTemplateVersionVariables").mockResolvedValueOnce([
|
||||
MockTemplateVersionVariable1,
|
||||
MockTemplateVersionVariable2,
|
||||
]);
|
||||
vi.spyOn(API, "createTemplateVersion").mockResolvedValueOnce(
|
||||
MockTemplateVersion2,
|
||||
);
|
||||
vi.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({
|
||||
message: "done",
|
||||
});
|
||||
|
||||
@@ -107,7 +101,7 @@ describe("TemplateVariablesPage", () => {
|
||||
await userEvent.type(secondVariableField, validFormValues.second_variable);
|
||||
|
||||
// Submit the form
|
||||
const toastSuccessSpy = jest.spyOn(toast, "success");
|
||||
const toastSuccessSpy = vi.spyOn(toast, "success");
|
||||
const submitButton = await screen.findByText(/save/i);
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
@@ -40,8 +40,8 @@ vi.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
// Occasionally, Jest encounters HTML5 canvas errors. As the MonacoEditor is not
|
||||
// required for these tests, we can safely mock it.
|
||||
// Monaco is a large and complicated codebase that slows tests down and we don't
|
||||
// need to test.
|
||||
vi.mock("#/pages/TemplateVersionEditorPage/MonacoEditor", () => ({
|
||||
MonacoEditor: (props: MonacoEditorProps) => (
|
||||
<textarea
|
||||
|
||||
+6
-6
@@ -17,13 +17,13 @@ const TEMPLATE_VERSION_FILES = {
|
||||
};
|
||||
|
||||
const setup = async () => {
|
||||
jest
|
||||
.spyOn(templateVersionUtils, "getTemplateVersionFiles")
|
||||
.mockResolvedValue(TEMPLATE_VERSION_FILES);
|
||||
vi.spyOn(templateVersionUtils, "getTemplateVersionFiles").mockResolvedValue(
|
||||
TEMPLATE_VERSION_FILES,
|
||||
);
|
||||
|
||||
jest
|
||||
.spyOn(CreateDayString, "createDayString")
|
||||
.mockImplementation(() => "a minute ago");
|
||||
vi.spyOn(CreateDayString, "createDayString").mockImplementation(
|
||||
() => "a minute ago",
|
||||
);
|
||||
|
||||
renderWithAuth(<TemplateVersionPage />, {
|
||||
route: `/templates/${TEMPLATE_NAME}/versions/${VERSION_NAME}`,
|
||||
+15
-17
@@ -36,16 +36,16 @@ const fillAndSubmitSecurityForm = () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(API, "getAuthMethods").mockResolvedValue(MockAuthMethodsAll);
|
||||
jest.spyOn(API, "getUserLoginType").mockResolvedValue({
|
||||
vi.spyOn(API, "getAuthMethods").mockResolvedValue(MockAuthMethodsAll);
|
||||
vi.spyOn(API, "getUserLoginType").mockResolvedValue({
|
||||
login_type: "password",
|
||||
});
|
||||
});
|
||||
|
||||
test("update password successfully", async () => {
|
||||
jest
|
||||
.spyOn(API, "updateUserPassword")
|
||||
.mockImplementationOnce((_userId, _data) => Promise.resolve(undefined));
|
||||
vi.spyOn(API, "updateUserPassword").mockImplementationOnce((_userId, _data) =>
|
||||
Promise.resolve(undefined),
|
||||
);
|
||||
const { user } = await renderPage();
|
||||
fillAndSubmitSecurityForm();
|
||||
|
||||
@@ -54,11 +54,11 @@ test("update password successfully", async () => {
|
||||
expect(API.updateUserPassword).toBeCalledTimes(1);
|
||||
expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
|
||||
|
||||
await waitFor(() => expect(window.location).toBeAt("/"));
|
||||
await waitFor(() => expect(location.pathname).toBe("/"));
|
||||
});
|
||||
|
||||
test("update password with incorrect old password", async () => {
|
||||
jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce(
|
||||
vi.spyOn(API, "updateUserPassword").mockRejectedValueOnce(
|
||||
mockApiError({
|
||||
message: "Incorrect password.",
|
||||
validations: [{ detail: "Incorrect password.", field: "old_password" }],
|
||||
@@ -76,7 +76,7 @@ test("update password with incorrect old password", async () => {
|
||||
});
|
||||
|
||||
test("update password when submit returns an unknown error", async () => {
|
||||
jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({
|
||||
vi.spyOn(API, "updateUserPassword").mockRejectedValueOnce({
|
||||
data: "unknown error",
|
||||
});
|
||||
|
||||
@@ -92,16 +92,14 @@ test("update password when submit returns an unknown error", async () => {
|
||||
test("change login type to OIDC", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { user: userData } = await renderPage();
|
||||
const convertToOAUTHSpy = jest
|
||||
.spyOn(API, "convertToOAUTH")
|
||||
.mockResolvedValue({
|
||||
state_string: "some-state-string",
|
||||
expires_at: "2021-01-01T00:00:00Z",
|
||||
to_type: "oidc",
|
||||
user_id: userData.id,
|
||||
} as OAuthConversionResponse);
|
||||
const convertToOAUTHSpy = vi.spyOn(API, "convertToOAUTH").mockResolvedValue({
|
||||
state_string: "some-state-string",
|
||||
expires_at: "2021-01-01T00:00:00Z",
|
||||
to_type: "oidc",
|
||||
user_id: userData.id,
|
||||
} as OAuthConversionResponse);
|
||||
|
||||
jest.spyOn(SSO, "redirectToOIDCAuth").mockImplementation(() => {
|
||||
vi.spyOn(SSO, "redirectToOIDCAuth").mockImplementation(() => {
|
||||
// Does a noop
|
||||
return "";
|
||||
});
|
||||
+7
-7
@@ -24,7 +24,7 @@ afterEach(() => {
|
||||
|
||||
describe("WorkspaceBuildPage", () => {
|
||||
test("gets the right workspace build", async () => {
|
||||
const getWorkspaceBuildSpy = jest
|
||||
const getWorkspaceBuildSpy = vi
|
||||
.spyOn(API, "getWorkspaceBuildByNumber")
|
||||
.mockResolvedValue(MockWorkspaceBuild);
|
||||
renderWithAuth(<WorkspaceBuildPage />, {
|
||||
@@ -54,8 +54,8 @@ describe("WorkspaceBuildPage", () => {
|
||||
output: "",
|
||||
};
|
||||
client.send(JSON.stringify(log));
|
||||
await expect(server).toReceiveMessage(log);
|
||||
expect(server).toHaveReceivedMessages([log]);
|
||||
expect(await server.nextMessage).toEqual(log);
|
||||
expect(server.messages).toEqual([log]);
|
||||
|
||||
client.onmessage = async () => {
|
||||
renderWithAuth(<WorkspaceBuildPage />, {
|
||||
@@ -73,9 +73,8 @@ describe("WorkspaceBuildPage", () => {
|
||||
test("shows selected agent logs", async () => {
|
||||
let mockServer: MockWebSocketServer | undefined;
|
||||
|
||||
jest
|
||||
.spyOn(apiModule, "watchWorkspaceAgentLogs")
|
||||
.mockImplementation((agentId, params) => {
|
||||
vi.spyOn(apiModule, "watchWorkspaceAgentLogs").mockImplementation(
|
||||
(agentId, params) => {
|
||||
return new OneWayWebSocket({
|
||||
apiRoute: `/api/v2/workspaceagents/${agentId}/logs`,
|
||||
searchParams: new URLSearchParams({
|
||||
@@ -88,7 +87,8 @@ describe("WorkspaceBuildPage", () => {
|
||||
return socket;
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithAuth(<WorkspaceBuildPage />, {
|
||||
+49
-39
@@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
|
||||
import MockServerSocket from "jest-websocket-mock";
|
||||
import { HttpResponse, http } from "msw";
|
||||
import type { FC } from "react";
|
||||
import type { MockInstance } from "vitest";
|
||||
import * as apiModule from "#/api/api";
|
||||
import type { TemplateVersionParameter, Workspace } from "#/api/typesGenerated";
|
||||
import {
|
||||
@@ -45,12 +46,12 @@ const renderWorkspacePage = async (
|
||||
workspace: Workspace,
|
||||
options: RenderWorkspacePageOptions = {},
|
||||
) => {
|
||||
jest.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace);
|
||||
jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
|
||||
jest
|
||||
.spyOn(API, "getDeploymentConfig")
|
||||
.mockResolvedValueOnce(MockDeploymentConfig);
|
||||
jest.spyOn(apiModule, "watchWorkspaceAgentLogs");
|
||||
vi.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace);
|
||||
vi.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
|
||||
vi.spyOn(API, "getDeploymentConfig").mockResolvedValueOnce(
|
||||
MockDeploymentConfig,
|
||||
);
|
||||
vi.spyOn(apiModule, "watchWorkspaceAgentLogs");
|
||||
|
||||
const result = renderWithAuth(<WorkspacePage />, {
|
||||
...options,
|
||||
@@ -74,7 +75,7 @@ const renderWorkspacePage = async (
|
||||
const testButton = async (
|
||||
workspace: Workspace,
|
||||
name: string | RegExp,
|
||||
actionMock: jest.SpyInstance,
|
||||
actionMock: MockInstance,
|
||||
) => {
|
||||
await renderWorkspacePage(workspace);
|
||||
const workspaceActions = screen.getByTestId("workspace-actions");
|
||||
@@ -92,7 +93,7 @@ afterEach(() => {
|
||||
describe("WorkspacePage", () => {
|
||||
it("requests a delete job when the user presses Delete and confirms", async () => {
|
||||
const user = userEvent.setup({ delay: 0 });
|
||||
const deleteWorkspaceMock = jest
|
||||
const deleteWorkspaceMock = vi
|
||||
.spyOn(API, "deleteWorkspace")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild);
|
||||
await renderWorkspacePage(MockWorkspace);
|
||||
@@ -135,7 +136,7 @@ describe("WorkspacePage", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const deleteWorkspaceMock = jest
|
||||
const deleteWorkspaceMock = vi
|
||||
.spyOn(API, "deleteWorkspace")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuildDelete);
|
||||
await renderWorkspacePage(MockFailedWorkspace);
|
||||
@@ -183,7 +184,7 @@ describe("WorkspacePage", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const startWorkspaceMock = jest
|
||||
const startWorkspaceMock = vi
|
||||
.spyOn(API, "startWorkspace")
|
||||
.mockImplementation(() => Promise.resolve(MockWorkspaceBuild));
|
||||
|
||||
@@ -191,7 +192,7 @@ describe("WorkspacePage", () => {
|
||||
});
|
||||
|
||||
it("requests a stop job when the user presses Stop", async () => {
|
||||
const stopWorkspaceMock = jest
|
||||
const stopWorkspaceMock = vi
|
||||
.spyOn(API, "stopWorkspace")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild);
|
||||
|
||||
@@ -199,7 +200,7 @@ describe("WorkspacePage", () => {
|
||||
});
|
||||
|
||||
it("requests a stop when the user presses Restart", async () => {
|
||||
const stopWorkspaceMock = jest
|
||||
const stopWorkspaceMock = vi
|
||||
.spyOn(API, "stopWorkspace")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild);
|
||||
|
||||
@@ -226,7 +227,7 @@ describe("WorkspacePage", () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ delay: 0 });
|
||||
const cancelWorkspaceMock = jest
|
||||
const cancelWorkspaceMock = vi
|
||||
.spyOn(API, "cancelWorkspaceBuild")
|
||||
.mockImplementation(() => Promise.resolve({ message: "job canceled" }));
|
||||
await renderWorkspacePage(MockStartingWorkspace);
|
||||
@@ -257,7 +258,7 @@ describe("WorkspacePage", () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ delay: 0 });
|
||||
const cancelWorkspaceMock = jest
|
||||
const cancelWorkspaceMock = vi
|
||||
.spyOn(API, "cancelWorkspaceBuild")
|
||||
.mockImplementation(() => Promise.resolve({ message: "job canceled" }));
|
||||
await renderWorkspacePage(MockPendingWorkspace);
|
||||
@@ -282,11 +283,11 @@ describe("WorkspacePage", () => {
|
||||
|
||||
it("requests an update when the user presses Update", async () => {
|
||||
// Mocks
|
||||
jest
|
||||
.spyOn(API, "getWorkspaceByOwnerAndName")
|
||||
.mockResolvedValueOnce(MockOutdatedWorkspace);
|
||||
vi.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValueOnce(
|
||||
MockOutdatedWorkspace,
|
||||
);
|
||||
|
||||
const updateWorkspaceMock = jest
|
||||
const updateWorkspaceMock = vi
|
||||
.spyOn(API, "updateWorkspace")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild);
|
||||
|
||||
@@ -310,10 +311,10 @@ describe("WorkspacePage", () => {
|
||||
// in a few releases when dynamic parameters takes over the world.
|
||||
it("updates the parameters when they are missing during update", async () => {
|
||||
// Mocks
|
||||
jest
|
||||
.spyOn(API, "getWorkspaceByOwnerAndName")
|
||||
.mockResolvedValueOnce(MockOutdatedWorkspace);
|
||||
const updateWorkspaceSpy = jest
|
||||
vi.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValueOnce(
|
||||
MockOutdatedWorkspace,
|
||||
);
|
||||
const updateWorkspaceSpy = vi
|
||||
.spyOn(API, "updateWorkspace")
|
||||
.mockRejectedValueOnce(
|
||||
new MissingBuildParameters(
|
||||
@@ -379,7 +380,7 @@ describe("WorkspacePage", () => {
|
||||
|
||||
it("restart the workspace with one time parameters when having the confirmation dialog", async () => {
|
||||
localStorage.removeItem(`${MockUserOwner.id}_ignoredWarnings`);
|
||||
jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValue([
|
||||
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([
|
||||
{
|
||||
...MockTemplateVersionParameter1,
|
||||
ephemeral: true,
|
||||
@@ -388,10 +389,11 @@ describe("WorkspacePage", () => {
|
||||
required: false,
|
||||
},
|
||||
]);
|
||||
jest
|
||||
.spyOn(API, "getWorkspaceBuildParameters")
|
||||
.mockResolvedValue([{ name: "rebuild", value: "false" }]);
|
||||
const restartWorkspaceSpy = jest.spyOn(API, "restartWorkspace");
|
||||
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([
|
||||
{ name: "rebuild", value: "false" },
|
||||
]);
|
||||
|
||||
const restartWorkspaceSpy = vi.spyOn(API, "restartWorkspace");
|
||||
const user = userEvent.setup();
|
||||
await renderWorkspacePage(MockWorkspace);
|
||||
await user.click(screen.getByTestId("build-parameters-button"));
|
||||
@@ -421,7 +423,7 @@ describe("WorkspacePage", () => {
|
||||
const retryDebugButtonRe = /^Debug$/i;
|
||||
|
||||
describe("Retries a failed 'Start' transition", () => {
|
||||
const mockRetry = jest
|
||||
const mockRetry = vi
|
||||
.spyOn(API, "retryWorkspace")
|
||||
.mockResolvedValue(MockWorkspaceBuild);
|
||||
const failedStart: Workspace = {
|
||||
@@ -456,7 +458,7 @@ describe("WorkspacePage", () => {
|
||||
});
|
||||
|
||||
describe("Retries a failed 'Stop' transition", () => {
|
||||
const mockStop = jest.spyOn(API, "stopWorkspace");
|
||||
const mockStop = vi.spyOn(API, "stopWorkspace");
|
||||
const failedStop: Workspace = {
|
||||
...MockFailedWorkspace,
|
||||
latest_build: {
|
||||
@@ -477,7 +479,7 @@ describe("WorkspacePage", () => {
|
||||
});
|
||||
|
||||
describe("Retries a failed 'Delete' transition", () => {
|
||||
const mockDelete = jest.spyOn(API, "deleteWorkspace");
|
||||
const mockDelete = vi.spyOn(API, "deleteWorkspace");
|
||||
const failedDelete: Workspace = {
|
||||
...MockFailedWorkspace,
|
||||
latest_build: {
|
||||
@@ -524,7 +526,7 @@ describe("WorkspacePage", () => {
|
||||
return HttpResponse.json([parameter]);
|
||||
}),
|
||||
);
|
||||
const retryWorkspaceSpy = jest
|
||||
const retryWorkspaceSpy = vi
|
||||
.spyOn(API, "retryWorkspace")
|
||||
.mockResolvedValue(MockWorkspaceBuild);
|
||||
|
||||
@@ -534,9 +536,13 @@ describe("WorkspacePage", () => {
|
||||
});
|
||||
await user.click(retryWithBuildParametersButton);
|
||||
await screen.findByText("Build Options");
|
||||
const parameterField = screen.getByLabelText(parameter.display_name, {
|
||||
exact: false,
|
||||
});
|
||||
const parameterField = await screen.findByLabelText(
|
||||
parameter.display_name,
|
||||
{
|
||||
exact: false,
|
||||
},
|
||||
);
|
||||
|
||||
await user.clear(parameterField);
|
||||
await user.type(parameterField, "some-value");
|
||||
const submitButton = screen.getByText("Build workspace");
|
||||
@@ -572,7 +578,7 @@ describe("WorkspacePage", () => {
|
||||
return HttpResponse.json([parameter]);
|
||||
}),
|
||||
);
|
||||
const retryWorkspaceSpy = jest
|
||||
const retryWorkspaceSpy = vi
|
||||
.spyOn(API, "retryWorkspace")
|
||||
.mockResolvedValue(MockWorkspaceBuild);
|
||||
|
||||
@@ -582,9 +588,13 @@ describe("WorkspacePage", () => {
|
||||
});
|
||||
await user.click(retryWithBuildParametersButton);
|
||||
await screen.findByText("Build Options");
|
||||
const parameterField = screen.getByLabelText(parameter.display_name, {
|
||||
exact: false,
|
||||
});
|
||||
const parameterField = await screen.findByLabelText(
|
||||
parameter.display_name,
|
||||
{
|
||||
exact: false,
|
||||
},
|
||||
);
|
||||
|
||||
await user.clear(parameterField);
|
||||
await user.type(parameterField, "some-value");
|
||||
const submitButton = screen.getByText("Build workspace");
|
||||
@@ -602,7 +612,7 @@ describe("WorkspacePage", () => {
|
||||
|
||||
describe("Navigation to other pages", () => {
|
||||
it("Shows a quota link when quota budget is greater than 0. Link navigates user to /workspaces route with the URL params populated with the corresponding organization", async () => {
|
||||
jest.spyOn(API, "getWorkspaceQuota").mockResolvedValueOnce({
|
||||
vi.spyOn(API, "getWorkspaceQuota").mockResolvedValueOnce({
|
||||
budget: 25,
|
||||
credits_consumed: 2,
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { createContext, type FC, type PropsWithChildren } from "react";
|
||||
|
||||
export type WorkerInitializationRenderOptions = {
|
||||
theme?: {
|
||||
light?: string;
|
||||
dark?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkerPoolOptions = {
|
||||
poolSize?: number;
|
||||
workerFactory?: () => Worker;
|
||||
};
|
||||
|
||||
export type SupportedLanguages = string;
|
||||
|
||||
export const WorkerPoolContextProvider: FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => <>{children}</>;
|
||||
|
||||
export const VirtualizerContext = createContext(undefined);
|
||||
|
||||
export const FileDiff: FC = () => null;
|
||||
|
||||
export const File: FC = () => null;
|
||||
@@ -1 +0,0 @@
|
||||
export default {};
|
||||
@@ -16,10 +16,10 @@ describe(createMockWebSocket.name, () => {
|
||||
it("Sends events from server to socket", () => {
|
||||
const [socket, server] = createMockWebSocket("wss://www.dog.ceo/shake");
|
||||
|
||||
const onOpen = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onMessage = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
const onOpen = vi.fn();
|
||||
const onError = vi.fn();
|
||||
const onMessage = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
|
||||
socket.addEventListener("open", onOpen);
|
||||
socket.addEventListener("error", onError);
|
||||
@@ -51,7 +51,7 @@ describe(createMockWebSocket.name, () => {
|
||||
|
||||
it("Sends JSON data to the socket for message events", () => {
|
||||
const [socket, server] = createMockWebSocket("wss://www.dog.ceo/wag");
|
||||
const onMessage = jest.fn();
|
||||
const onMessage = vi.fn();
|
||||
|
||||
// Could type this as a special JSON type, but unknown is good enough,
|
||||
// since any invalid values will throw in the test case
|
||||
@@ -91,10 +91,10 @@ describe(createMockWebSocket.name, () => {
|
||||
it("Only registers each socket event handler once", () => {
|
||||
const [socket, server] = createMockWebSocket("wss://www.dog.ceo/borf");
|
||||
|
||||
const onOpen = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onMessage = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
const onOpen = vi.fn();
|
||||
const onError = vi.fn();
|
||||
const onMessage = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
|
||||
// Do it once
|
||||
socket.addEventListener("open", onOpen);
|
||||
@@ -122,10 +122,10 @@ describe(createMockWebSocket.name, () => {
|
||||
it("Lets a socket unsubscribe to event types", () => {
|
||||
const [socket, server] = createMockWebSocket("wss://www.dog.ceo/zoomies");
|
||||
|
||||
const onOpen = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onMessage = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
const onOpen = vi.fn();
|
||||
const onError = vi.fn();
|
||||
const onMessage = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
|
||||
socket.addEventListener("open", onOpen);
|
||||
socket.addEventListener("error", onError);
|
||||
@@ -152,7 +152,7 @@ describe(createMockWebSocket.name, () => {
|
||||
const [socket, server] = createMockWebSocket("wss://www.dog.ceo/woof");
|
||||
expect(server.isConnectionOpen).toBe(true);
|
||||
|
||||
const onMessage = jest.fn();
|
||||
const onMessage = vi.fn();
|
||||
socket.addEventListener("message", onMessage);
|
||||
|
||||
socket.close();
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Mock } from "vitest";
|
||||
import type { WebSocketEventType } from "#/utils/OneWayWebSocket";
|
||||
|
||||
type SocketSendData = Parameters<WebSocket["send"]>[0];
|
||||
@@ -19,15 +20,15 @@ type CallbackStore = {
|
||||
type MockWebSocket = Omit<WebSocket, "send"> & {
|
||||
/**
|
||||
* A version of the WebSocket `send` method that has been pre-wrapped inside
|
||||
* a Jest mock.
|
||||
* a vitest mock.
|
||||
*
|
||||
* The Jest mock functionality should be used at a minimum. Basically:
|
||||
* The vitest mock functionality should be used at a minimum. Basically:
|
||||
* 1. If you want to check that the mock socket sent something to the mock
|
||||
* server: call the `send` method as a function, and then check the
|
||||
* `clientSentData` on `MockWebSocketServer` to see what data got
|
||||
* received.
|
||||
* 2. If you need to make sure that the client-side `send` method got called
|
||||
* at all: you can use the Jest mock functionality, but you should
|
||||
* at all: you can use the vitest mock functionality, but you should
|
||||
* probably also be checking `clientSentData` still and making additional
|
||||
* assertions with it.
|
||||
*
|
||||
@@ -35,7 +36,7 @@ type MockWebSocket = Omit<WebSocket, "send"> & {
|
||||
* communication was successful, not whether the client-side method was
|
||||
* called.
|
||||
*/
|
||||
send: jest.Mock<void, [SocketSendData], unknown>;
|
||||
send: Mock<(data: SocketSendData) => void>;
|
||||
};
|
||||
|
||||
export function createMockWebSocket(
|
||||
@@ -76,9 +77,9 @@ export function createMockWebSocket(
|
||||
onerror: null,
|
||||
onmessage: null,
|
||||
onopen: null,
|
||||
dispatchEvent: jest.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
|
||||
send: jest.fn((data) => {
|
||||
send: vi.fn((data) => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ describe(OneWayWebSocket.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const onOpen = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onMessage = jest.fn();
|
||||
const onOpen = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
const onError = vi.fn();
|
||||
const onMessage = vi.fn();
|
||||
|
||||
oneWay.addEventListener("open", onOpen);
|
||||
oneWay.addEventListener("close", onClose);
|
||||
@@ -83,10 +83,10 @@ describe(OneWayWebSocket.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const onOpen = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onMessage = jest.fn();
|
||||
const onOpen = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
const onError = vi.fn();
|
||||
const onMessage = vi.fn();
|
||||
|
||||
oneWay.addEventListener("open", onOpen);
|
||||
oneWay.addEventListener("close", onClose);
|
||||
@@ -128,10 +128,10 @@ describe(OneWayWebSocket.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const onOpen = jest.fn();
|
||||
const onClose = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onMessage = jest.fn();
|
||||
const onOpen = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
const onError = vi.fn();
|
||||
const onMessage = vi.fn();
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
oneWay.addEventListener("open", onOpen);
|
||||
@@ -170,19 +170,19 @@ describe(OneWayWebSocket.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const onOpen1 = jest.fn();
|
||||
const onClose1 = jest.fn();
|
||||
const onError1 = jest.fn();
|
||||
const onMessage1 = jest.fn();
|
||||
const onOpen1 = vi.fn();
|
||||
const onClose1 = vi.fn();
|
||||
const onError1 = vi.fn();
|
||||
const onMessage1 = vi.fn();
|
||||
oneWay.addEventListener("open", onOpen1);
|
||||
oneWay.addEventListener("close", onClose1);
|
||||
oneWay.addEventListener("error", onError1);
|
||||
oneWay.addEventListener("message", onMessage1);
|
||||
|
||||
const onOpen2 = jest.fn();
|
||||
const onClose2 = jest.fn();
|
||||
const onError2 = jest.fn();
|
||||
const onMessage2 = jest.fn();
|
||||
const onOpen2 = vi.fn();
|
||||
const onClose2 = vi.fn();
|
||||
const onError2 = vi.fn();
|
||||
const onMessage2 = vi.fn();
|
||||
oneWay.addEventListener("open", onOpen2);
|
||||
oneWay.addEventListener("close", onClose2);
|
||||
oneWay.addEventListener("error", onError2);
|
||||
@@ -251,7 +251,7 @@ describe(OneWayWebSocket.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const onMessage = jest.fn();
|
||||
const onMessage = vi.fn();
|
||||
oneWay.addEventListener("message", onMessage);
|
||||
|
||||
const payload = {
|
||||
@@ -281,7 +281,7 @@ describe(OneWayWebSocket.name, () => {
|
||||
},
|
||||
});
|
||||
|
||||
const onMessage = jest.fn();
|
||||
const onMessage = vi.fn();
|
||||
oneWay.addEventListener("message", onMessage);
|
||||
|
||||
const payload = "definitely not valid JSON";
|
||||
@@ -290,7 +290,7 @@ describe(OneWayWebSocket.name, () => {
|
||||
});
|
||||
mockServer.publishMessage(event);
|
||||
|
||||
const arg: OneWayMessageEvent<never> = onMessage.mock.lastCall[0];
|
||||
const arg: OneWayMessageEvent<never> = onMessage.mock.lastCall![0];
|
||||
expect(arg.sourceEvent).toEqual(event);
|
||||
expect(arg.parsedMessage).toEqual(undefined);
|
||||
expect(arg.parseError).toBeInstanceOf(Error);
|
||||
@@ -1,3 +1,9 @@
|
||||
// Monaco editor calls document.queryCommandSupported at import time,
|
||||
// which is absent from JSDOM.
|
||||
if (typeof document.queryCommandSupported !== "function") {
|
||||
document.queryCommandSupported = () => false;
|
||||
}
|
||||
|
||||
// Pointer capture stubs required for Radix UI in JSDOM.
|
||||
globalThis.HTMLElement.prototype.hasPointerCapture = vi
|
||||
.fn()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Blob as NativeBlob } from "node:buffer";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
|
||||
// JSDom `Blob` is missing important methods[1] that have been standardized for
|
||||
// years. MDN categorizes this API as baseline[2].
|
||||
@@ -9,4 +10,20 @@ import { Blob as NativeBlob } from "node:buffer";
|
||||
// changes.
|
||||
globalThis.Blob = NativeBlob;
|
||||
|
||||
globalThis.ResizeObserver = require("resize-observer-polyfill");
|
||||
globalThis.ResizeObserver = ResizeObserver;
|
||||
|
||||
// JSDOM does not implement window.matchMedia. Monaco editor (and other
|
||||
// libraries) rely on it for dark/light theme detection.
|
||||
if (typeof globalThis.matchMedia !== "function") {
|
||||
globalThis.matchMedia = (query: string) =>
|
||||
({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
}) as MediaQueryList;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user