diff --git a/src/components/AstroPiModel/AstroPiControls/MotionInput.test.jsx b/src/components/AstroPiModel/AstroPiControls/MotionInput.test.jsx index 61f154f13..1bfd42f6e 100644 --- a/src/components/AstroPiModel/AstroPiControls/MotionInput.test.jsx +++ b/src/components/AstroPiModel/AstroPiControls/MotionInput.test.jsx @@ -7,8 +7,8 @@ import Sk from "skulpt"; let container; let store; -const start_motion_function = jest.fn(); -const stop_motion_function = jest.fn(); +const start_motion_function = vi.fn(); +const stop_motion_function = vi.fn(); describe("No motion and code running", () => { beforeEach(() => { diff --git a/src/components/AstroPiModel/FlightCase.test.jsx b/src/components/AstroPiModel/FlightCase.test.jsx index 1652605c2..ceb9e066d 100644 --- a/src/components/AstroPiModel/FlightCase.test.jsx +++ b/src/components/AstroPiModel/FlightCase.test.jsx @@ -13,7 +13,7 @@ import FlightCase from "./FlightCase"; // By mocking @react-three/drei to return a scene synchronously, we ensure that // the event handlers are properly registered and the tests can run as // expected. -jest.mock("@react-three/drei", () => ({ +vi.mock("@react-three/drei", () => ({ useGLTF: () => ({ scene: { getObjectByName: () => ({ material: null }), diff --git a/src/components/AstroPiModel/OrientationPanel/OrientationPanel.test.jsx b/src/components/AstroPiModel/OrientationPanel/OrientationPanel.test.jsx index 04f5e5d24..a850cfca5 100644 --- a/src/components/AstroPiModel/OrientationPanel/OrientationPanel.test.jsx +++ b/src/components/AstroPiModel/OrientationPanel/OrientationPanel.test.jsx @@ -3,7 +3,7 @@ import { render } from "@testing-library/react"; import OrientationPanel from "./OrientationPanel"; let panel; -const resetFunction = jest.fn(); +const resetFunction = vi.fn(); beforeAll(() => { panel = render( diff --git a/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.test.jsx b/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.test.jsx index 0f9c37c86..98037ac5b 100644 --- a/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.test.jsx +++ b/src/components/AstroPiModel/OrientationPanel/OrientationResetButton.test.jsx @@ -3,7 +3,7 @@ import { render, fireEvent } from "@testing-library/react"; import OrientationResetButton from "./OrientationResetButton"; let resetButton; -const resetFunction = jest.fn(); +const resetFunction = vi.fn(); beforeEach(() => { resetButton = render( diff --git a/src/components/AstroPiModel/Simulator.test.jsx b/src/components/AstroPiModel/Simulator.test.jsx index 4cfb2212d..1ff8a88f8 100644 --- a/src/components/AstroPiModel/Simulator.test.jsx +++ b/src/components/AstroPiModel/Simulator.test.jsx @@ -16,7 +16,7 @@ test("Three canvas renders", () => { }); test("Moving pointer over model does not change orientation", () => { - const updateOrientation = jest.fn(); + const updateOrientation = vi.fn(); const simulator = render(); const canvas = simulator.container.querySelector("canvas"); fireEvent.pointerMove(canvas); @@ -24,7 +24,7 @@ test("Moving pointer over model does not change orientation", () => { }); test("Dragging model changes orientation", async () => { - const updateOrientation = jest.fn(); + const updateOrientation = vi.fn(); const simulator = render(); const canvas = simulator.container.querySelector("canvas"); fireEvent.pointerDown(canvas); @@ -33,7 +33,7 @@ test("Dragging model changes orientation", async () => { }); test("Dragging before model has loaded does not throw", () => { - const updateOrientation = jest.fn(); + const updateOrientation = vi.fn(); const loadedMod = window.mod; window.mod = undefined; diff --git a/src/components/DownloadButton/DownloadButton.test.jsx b/src/components/DownloadButton/DownloadButton.test.jsx index cefaee724..03e0e2635 100644 --- a/src/components/DownloadButton/DownloadButton.test.jsx +++ b/src/components/DownloadButton/DownloadButton.test.jsx @@ -8,12 +8,12 @@ import JSZip from "jszip"; import JSZipUtils from "jszip-utils"; import { postMessageToScratchIframe } from "../../utils/scratchIframe"; -jest.mock("file-saver"); -jest.mock("jszip"); -jest.mock("jszip-utils", () => ({ - getBinaryContent: jest.fn(), +vi.mock("file-saver"); +vi.mock("jszip"); +vi.mock("jszip-utils", () => ({ + default: { getBinaryContent: vi.fn() }, })); -jest.mock("../../utils/scratchIframe"); +vi.mock("../../utils/scratchIframe"); describe("Downloading project with name set", () => { let downloadButton; diff --git a/src/components/Editor/EditorInput/EditorInput.test.jsx b/src/components/Editor/EditorInput/EditorInput.test.jsx index 84b2426aa..64f2e6a38 100644 --- a/src/components/Editor/EditorInput/EditorInput.test.jsx +++ b/src/components/Editor/EditorInput/EditorInput.test.jsx @@ -11,14 +11,14 @@ import { import { matchMedia, setMedia } from "mock-match-media"; import { MOBILE_BREAKPOINT } from "../../../utils/mediaQueryBreakpoints"; -window.HTMLElement.prototype.scrollIntoView = jest.fn(); +window.HTMLElement.prototype.scrollIntoView = vi.fn(); let mockMediaQuery = (query) => { return matchMedia(query).matches; }; -jest.mock("react-responsive", () => ({ - ...jest.requireActual("react-responsive"), +vi.mock("react-responsive", async () => ({ + ...(await vi.importActual("react-responsive")), useMediaQuery: ({ query }) => mockMediaQuery(query), })); diff --git a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx index 9a7f2ca3e..e3550fed9 100644 --- a/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx +++ b/src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx @@ -11,8 +11,8 @@ let mockMediaQuery = (query) => { return matchMedia(query).matches; }; -jest.mock("react-responsive", () => ({ - ...jest.requireActual("react-responsive"), +vi.mock("react-responsive", async () => ({ + ...(await vi.importActual("react-responsive")), useMediaQuery: ({ query }) => mockMediaQuery(query), })); diff --git a/src/components/Menus/ContextMenu/ContextMenu.test.jsx b/src/components/Menus/ContextMenu/ContextMenu.test.jsx index f14830fe2..6b0f45635 100644 --- a/src/components/Menus/ContextMenu/ContextMenu.test.jsx +++ b/src/components/Menus/ContextMenu/ContextMenu.test.jsx @@ -4,7 +4,7 @@ import { axe, toHaveNoViolations } from "jest-axe"; import ContextMenu from "./ContextMenu"; expect.extend(toHaveNoViolations); -const action1 = jest.fn(); +const action1 = vi.fn(); describe("With file items", () => { beforeEach(() => { diff --git a/src/components/Menus/Sidebar/DownloadPanel/DownloadPanel.test.jsx b/src/components/Menus/Sidebar/DownloadPanel/DownloadPanel.test.jsx index c5fb0852e..8dae35e72 100644 --- a/src/components/Menus/Sidebar/DownloadPanel/DownloadPanel.test.jsx +++ b/src/components/Menus/Sidebar/DownloadPanel/DownloadPanel.test.jsx @@ -5,16 +5,16 @@ import { MemoryRouter } from "react-router"; import configureStore from "redux-mock-store"; import FileSaver from "file-saver"; -jest.mock("file-saver"); -jest.mock("jszip"); -jest.mock("jszip-utils", () => ({ - getBinaryContent: jest.fn(), +vi.mock("file-saver"); +vi.mock("jszip"); +vi.mock("jszip-utils", () => ({ + default: { getBinaryContent: vi.fn() }, })); let container; -const logInHandler = jest.fn(); -const signUpHandler = jest.fn(); +const logInHandler = vi.fn(); +const signUpHandler = vi.fn(); beforeAll(() => { document.addEventListener("editor-logIn", logInHandler); diff --git a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.jsx b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.jsx index bfa47f5e8..cd1d22e9c 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.jsx +++ b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.jsx @@ -6,11 +6,13 @@ import { act } from "react"; import Modal from "react-modal"; import { scratchblocksInit } from "../../../../utils/scratchblocks"; import { renderWithProviders } from "../../../../utils/renderWithProviders"; +import demoInstructions from "../../../../assets/markdown/demoInstructions.md?raw"; +import populateMarkdownTemplate from "../../../../utils/populateMarkdownTemplate"; -window.HTMLElement.prototype.scrollTo = jest.fn(); +window.HTMLElement.prototype.scrollTo = vi.fn(); -jest.mock("../../../../utils/scratchblocks", () => ({ - scratchblocksInit: jest.fn(), +vi.mock("../../../../utils/scratchblocks", () => ({ + scratchblocksInit: vi.fn(), })); // Stand-in for the real (jsdom-unfriendly) scratchblocks SVG rendering: swap @@ -305,7 +307,7 @@ describe("When instructionsEditable is true", () => { }); expect(store.getState().editor.project.instructions).toBe( - "demoInstructions.md", + populateMarkdownTemplate(demoInstructions, (str) => str), ); }); diff --git a/src/components/Menus/Sidebar/InstructionsPanel/ProgressBar/ProgressBar.test.jsx b/src/components/Menus/Sidebar/InstructionsPanel/ProgressBar/ProgressBar.test.jsx index 061b87e8e..2a7174441 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/ProgressBar/ProgressBar.test.jsx +++ b/src/components/Menus/Sidebar/InstructionsPanel/ProgressBar/ProgressBar.test.jsx @@ -76,7 +76,7 @@ describe("When on a middle step", () => { }); test("Clicking previous step button calls scrollTo when panelRef is provided", () => { - const mockScrollTo = jest.fn(); + const mockScrollTo = vi.fn(); const panelRef = { current: { scrollTo: mockScrollTo } }; renderProgressBarOnStep(1, 3, panelRef); @@ -93,7 +93,7 @@ describe("When on a middle step", () => { }); test("Clicking next step button calls scrollTo when panelRef is provided", () => { - const mockScrollTo = jest.fn(); + const mockScrollTo = vi.fn(); const panelRef = { current: { scrollTo: mockScrollTo } }; renderProgressBarOnStep(1, 3, panelRef); @@ -110,7 +110,7 @@ describe("When on a middle step", () => { }); test("Does not call scrollTo when panelRef is null", () => { - const mockScrollTo = jest.fn(); + const mockScrollTo = vi.fn(); renderProgressBarOnStep(1, 3, null); @@ -123,7 +123,7 @@ describe("When on a middle step", () => { }); test("Does not call scrollTo when panelRef.current is null", () => { - const mockScrollTo = jest.fn(); + const mockScrollTo = vi.fn(); const panelRef = { current: null }; renderProgressBarOnStep(1, 3, panelRef); diff --git a/src/components/Menus/Sidebar/ProjectsPanel/ProjectsPanel.test.jsx b/src/components/Menus/Sidebar/ProjectsPanel/ProjectsPanel.test.jsx index f759e4e3b..9ab640af8 100644 --- a/src/components/Menus/Sidebar/ProjectsPanel/ProjectsPanel.test.jsx +++ b/src/components/Menus/Sidebar/ProjectsPanel/ProjectsPanel.test.jsx @@ -6,7 +6,7 @@ import { MemoryRouter } from "react-router-dom"; import ProjectsPanel from "./ProjectsPanel"; -document.dispatchEvent = jest.fn(); +document.dispatchEvent = vi.fn(); const initialState = { editor: { diff --git a/src/components/Menus/Sidebar/SettingsPanel/ThemeToggle/ThemeToggle.test.jsx b/src/components/Menus/Sidebar/SettingsPanel/ThemeToggle/ThemeToggle.test.jsx index 8a78f9d27..0a037c045 100644 --- a/src/components/Menus/Sidebar/SettingsPanel/ThemeToggle/ThemeToggle.test.jsx +++ b/src/components/Menus/Sidebar/SettingsPanel/ThemeToggle/ThemeToggle.test.jsx @@ -3,7 +3,7 @@ import { act, render, fireEvent, screen } from "@testing-library/react"; import ThemeToggle from "./ThemeToggle"; import { Cookies, CookiesProvider } from "react-cookie"; -const themeUpdatedHandler = jest.fn(); +const themeUpdatedHandler = vi.fn(); beforeAll(() => { document.addEventListener("editor-themeUpdated", (e) => @@ -20,11 +20,11 @@ describe("When default theme is light mode and cookie unset", () => { matches: false, media: query, onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + addListener: vi.fn(), // Deprecated + removeListener: vi.fn(), // Deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), }); cookies = new Cookies(); toggleContainer = render( @@ -68,11 +68,11 @@ describe("When default theme is dark mode and cookie unset", () => { matches: true, media: query, onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + addListener: vi.fn(), // Deprecated + removeListener: vi.fn(), // Deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), }); cookies = new Cookies(); toggleContainer = render( diff --git a/src/components/Menus/Sidebar/SidebarBar.test.jsx b/src/components/Menus/Sidebar/SidebarBar.test.jsx index 9014b4fbb..cce2d8336 100644 --- a/src/components/Menus/Sidebar/SidebarBar.test.jsx +++ b/src/components/Menus/Sidebar/SidebarBar.test.jsx @@ -4,7 +4,7 @@ import configureStore from "redux-mock-store"; import { Provider } from "react-redux"; import SidebarBar from "./SidebarBar"; -const toggleOption = jest.fn(); +const toggleOption = vi.fn(); const mockStore = configureStore([]); const initialState = { diff --git a/src/components/Menus/Sidebar/SidebarBarOption.test.jsx b/src/components/Menus/Sidebar/SidebarBarOption.test.jsx index 98008b379..c1915dfad 100644 --- a/src/components/Menus/Sidebar/SidebarBarOption.test.jsx +++ b/src/components/Menus/Sidebar/SidebarBarOption.test.jsx @@ -2,7 +2,7 @@ import React from "react"; import { fireEvent, render, screen } from "@testing-library/react"; import SidebarBarOption from "./SidebarBarOption"; -const toggleOption = jest.fn(); +const toggleOption = vi.fn(); beforeEach(() => { render( diff --git a/src/components/Mobile/MobileProject/MobileProject.test.jsx b/src/components/Mobile/MobileProject/MobileProject.test.jsx index 7893a8ecf..e34051738 100644 --- a/src/components/Mobile/MobileProject/MobileProject.test.jsx +++ b/src/components/Mobile/MobileProject/MobileProject.test.jsx @@ -5,7 +5,7 @@ import configureStore from "redux-mock-store"; import MobileProject from "./MobileProject"; import { showSidebar } from "../../../redux/EditorSlice"; -window.HTMLElement.prototype.scrollIntoView = jest.fn(); +window.HTMLElement.prototype.scrollIntoView = vi.fn(); const middlewares = []; const mockStore = configureStore(middlewares); diff --git a/src/components/Modals/ErrorModal.test.jsx b/src/components/Modals/ErrorModal.test.jsx index e9a9ac0fa..a668ac3fa 100644 --- a/src/components/Modals/ErrorModal.test.jsx +++ b/src/components/Modals/ErrorModal.test.jsx @@ -120,7 +120,7 @@ test("Additional closeModal function fired", () => { }, }; const store = mockStore(initialState); - const testOnClose = jest.fn(); + const testOnClose = vi.fn(); render( diff --git a/src/components/Modals/GeneralModal.test.jsx b/src/components/Modals/GeneralModal.test.jsx index 8bad7a18c..b6a8e810e 100644 --- a/src/components/Modals/GeneralModal.test.jsx +++ b/src/components/Modals/GeneralModal.test.jsx @@ -2,8 +2,8 @@ import { fireEvent, render, screen } from "@testing-library/react"; import React from "react"; import GeneralModal from "./GeneralModal"; -const defaultCallback = jest.fn(); -const closeModal = jest.fn(); +const defaultCallback = vi.fn(); +const closeModal = vi.fn(); describe("With close button", () => { beforeEach(() => { @@ -16,7 +16,7 @@ describe("With close button", () => { defaultCallback={defaultCallback} heading="My modal heading" text={[{ content: "Paragraph1", type: "paragraph" }]} - buttons={[]} + buttons={[]} /> , ); @@ -52,7 +52,7 @@ describe("Without close button", () => { defaultCallback={defaultCallback} heading="My modal heading" text={[{ content: "Paragraph1", type: "paragraph" }]} - buttons={[]} + buttons={[]} /> , ); diff --git a/src/components/ProjectBar/ProjectBar.test.jsx b/src/components/ProjectBar/ProjectBar.test.jsx index 46c7f0cfc..bd37f068f 100644 --- a/src/components/ProjectBar/ProjectBar.test.jsx +++ b/src/components/ProjectBar/ProjectBar.test.jsx @@ -6,12 +6,12 @@ import { MemoryRouter } from "react-router-dom"; import ProjectBar from "./ProjectBar"; import useIsOnline from "../../hooks/useIsOnline"; -jest.mock("axios"); -jest.mock("../../hooks/useIsOnline"); +vi.mock("axios"); +vi.mock("../../hooks/useIsOnline"); -jest.mock("react-router-dom", () => ({ - ...jest.requireActual("react-router-dom"), - useNavigate: () => jest.fn(), +vi.mock("react-router-dom", async () => ({ + ...(await vi.importActual("react-router-dom")), + useNavigate: () => vi.fn(), })); const project = { diff --git a/src/components/ProjectBar/ScratchProjectBar.test.jsx b/src/components/ProjectBar/ScratchProjectBar.test.jsx index f054cf26d..85840103e 100644 --- a/src/components/ProjectBar/ScratchProjectBar.test.jsx +++ b/src/components/ProjectBar/ScratchProjectBar.test.jsx @@ -7,18 +7,18 @@ import ScratchProjectBar from "./ScratchProjectBar"; import editorReducer, { editorInitialState } from "../../redux/EditorSlice"; import { postMessageToScratchIframe } from "../../utils/scratchIframe"; -jest.mock("axios"); -jest.mock("../../utils/scratchIframe", () => ({ - ...jest.requireActual("../../utils/scratchIframe"), - postMessageToScratchIframe: jest.fn(), +vi.mock("axios"); +vi.mock("../../utils/scratchIframe", async () => ({ + ...(await vi.importActual("../../utils/scratchIframe")), + postMessageToScratchIframe: vi.fn(), })); -jest.mock("react-router-dom", () => ({ - ...jest.requireActual("react-router-dom"), - useNavigate: () => jest.fn(), +vi.mock("react-router-dom", async () => ({ + ...(await vi.importActual("react-router-dom")), + useNavigate: () => vi.fn(), })); -jest.useFakeTimers(); +vi.useFakeTimers(); const scratchProject = { name: "Hello world", @@ -99,11 +99,11 @@ const dispatchScratchMessage = (type, origin = getScratchOrigin()) => { }; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); afterEach(() => { - jest.clearAllTimers(); + vi.clearAllTimers(); }); describe("When project is Scratch", () => { @@ -175,7 +175,7 @@ describe("When project is Scratch", () => { dispatchScratchMessage("scratch-gui-project-changed"); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(postMessageToScratchIframe).toHaveBeenCalledWith({ @@ -202,7 +202,7 @@ describe("When project is Scratch", () => { dispatchScratchMessage("scratch-gui-project-changed"); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(postMessageToScratchIframe).toHaveBeenCalledWith({ @@ -279,7 +279,7 @@ describe("Additional Scratch manual save states", () => { dispatchScratchMessage("scratch-gui-project-changed"); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(postMessageToScratchIframe).not.toHaveBeenCalled(); diff --git a/src/components/ProjectName/ProjectName.test.jsx b/src/components/ProjectName/ProjectName.test.jsx index 5f6c8e95b..e18954acc 100644 --- a/src/components/ProjectName/ProjectName.test.jsx +++ b/src/components/ProjectName/ProjectName.test.jsx @@ -17,10 +17,10 @@ let store; let editButton; let inputField; -jest.mock("../../redux/EditorSlice"); -jest.mock("react-redux", () => ({ - ...jest.requireActual("react-redux"), - useDispatch: () => jest.fn(), +vi.mock("../../redux/EditorSlice"); +vi.mock("react-redux", async () => ({ + ...(await vi.importActual("react-redux")), + useDispatch: () => vi.fn(), })); describe("With a label", () => { @@ -178,7 +178,7 @@ describe("With no label", () => { }); test("Updates project name", () => { - expect(store.getActions()).toEqual([updateProjectName(project.name)]); + expect(updateProjectName).toHaveBeenCalledWith(project.name); }); test("Disables input field", async () => { @@ -198,7 +198,7 @@ describe("With no label", () => { }); test("Updates project name", () => { - expect(store.getActions()).toEqual([updateProjectName(project.name)]); + expect(updateProjectName).toHaveBeenCalledWith(project.name); }); test("Disables input field", async () => { diff --git a/src/components/RunButton/StopButton.test.jsx b/src/components/RunButton/StopButton.test.jsx index 8cc4b0c0b..a1f968a9a 100644 --- a/src/components/RunButton/StopButton.test.jsx +++ b/src/components/RunButton/StopButton.test.jsx @@ -6,11 +6,11 @@ import store from "../../app/store"; import { codeRunHandled, triggerCodeRun } from "../../redux/EditorSlice"; beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); test("Clicking stop button sets codeRunStopped to true", () => { @@ -45,7 +45,7 @@ test("Clicking stop button changes it to 'Stopping...' after a time out", () => expect(stopButton.textContent).toEqual("Stop Code"); act(() => { - jest.runAllTimers(); + vi.runAllTimers(); }); expect(stopButton.textContent).toEqual("runButton.stopping"); }); diff --git a/src/components/SaveButton/SaveButton.test.jsx b/src/components/SaveButton/SaveButton.test.jsx index fff7f01c1..ccad526cb 100644 --- a/src/components/SaveButton/SaveButton.test.jsx +++ b/src/components/SaveButton/SaveButton.test.jsx @@ -6,9 +6,9 @@ import { triggerSave } from "../../redux/EditorSlice"; import SaveButton from "./SaveButton"; import useIsOnline from "../../hooks/useIsOnline"; -jest.mock("../../hooks/useIsOnline"); +vi.mock("../../hooks/useIsOnline"); -const logInHandler = jest.fn(); +const logInHandler = vi.fn(); describe("When project is loaded", () => { beforeAll(() => { diff --git a/src/components/UploadButton/UploadButton.test.jsx b/src/components/UploadButton/UploadButton.test.jsx index 8e3052303..74b6ec36b 100644 --- a/src/components/UploadButton/UploadButton.test.jsx +++ b/src/components/UploadButton/UploadButton.test.jsx @@ -5,7 +5,7 @@ import { configureStore } from "@reduxjs/toolkit"; import UploadButton from "./UploadButton"; import { postMessageToScratchIframe } from "../../utils/scratchIframe"; -jest.mock("../../utils/scratchIframe"); +vi.mock("../../utils/scratchIframe"); const createStore = (project) => configureStore({ @@ -38,7 +38,7 @@ describe("UploadButton", () => { test("clicking the button triggers the file input", () => { const button = screen.getByRole("button", { name: "Upload" }); - const clickSpy = jest.spyOn(fileInput, "click"); + const clickSpy = vi.spyOn(fileInput, "click"); fireEvent.click(button); expect(clickSpy).toHaveBeenCalledTimes(1); }); diff --git a/src/components/WebComponentProject/WebComponentProject.integration.test.jsx b/src/components/WebComponentProject/WebComponentProject.integration.test.jsx index 89abb59b1..ba1db1b8b 100644 --- a/src/components/WebComponentProject/WebComponentProject.integration.test.jsx +++ b/src/components/WebComponentProject/WebComponentProject.integration.test.jsx @@ -12,7 +12,7 @@ import { } from "../../redux/EditorSlice"; import { projectIdentifierChangedEvent } from "../../events/WebComponentCustomEvents"; -const projectIdentifierChangedHandler = jest.fn(); +const projectIdentifierChangedHandler = vi.fn(); beforeAll(() => { document.addEventListener( diff --git a/src/components/WebComponentProject/WebComponentProject.test.jsx b/src/components/WebComponentProject/WebComponentProject.test.jsx index 9f2aa64fb..d7df95a85 100644 --- a/src/components/WebComponentProject/WebComponentProject.test.jsx +++ b/src/components/WebComponentProject/WebComponentProject.test.jsx @@ -7,10 +7,10 @@ import WebComponentProject, { } from "./WebComponentProject"; import { RUN_EVENT_DEBOUNCE_MS } from "./runEventCodeSnapshot"; -const codeChangedHandler = jest.fn(); -const runStartedHandler = jest.fn(); -const runCompletedHandler = jest.fn(); -const stepChangedHandler = jest.fn(); +const codeChangedHandler = vi.fn(); +const runStartedHandler = vi.fn(); +const runCompletedHandler = vi.fn(); +const stepChangedHandler = vi.fn(); beforeAll(() => { document.addEventListener("editor-codeChanged", codeChangedHandler); @@ -19,11 +19,11 @@ beforeAll(() => { document.addEventListener("editor-stepChanged", stepChangedHandler); }); -jest.useFakeTimers(); +vi.useFakeTimers(); const flushRunEventDebounce = () => { act(() => { - jest.advanceTimersByTime(RUN_EVENT_DEBOUNCE_MS); + vi.advanceTimersByTime(RUN_EVENT_DEBOUNCE_MS); }); }; @@ -96,7 +96,7 @@ describe("When state set", () => { test("Triggers codeChanged event", () => { act(() => { - jest.runAllTimers(); + vi.runAllTimers(); }); expect(codeChangedHandler).toHaveBeenCalled(); expect(codeChangedHandler.mock.lastCall[0].detail).toHaveProperty("step"); diff --git a/src/components/WebComponentProject/runEventCodeSnapshot.test.js b/src/components/WebComponentProject/runEventCodeSnapshot.test.js index 3a6fb1e11..b92bf2749 100644 --- a/src/components/WebComponentProject/runEventCodeSnapshot.test.js +++ b/src/components/WebComponentProject/runEventCodeSnapshot.test.js @@ -13,21 +13,21 @@ const components = [ ]; const flushDebounce = () => { - jest.advanceTimersByTime(RUN_EVENT_DEBOUNCE_MS); + vi.advanceTimersByTime(RUN_EVENT_DEBOUNCE_MS); }; describe("runEventCodeSnapshot", () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); resetRunEventCodeSnapshot(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); test("allows the first run for a project after debounce", () => { - const onRunStarted = jest.fn(); + const onRunStarted = vi.fn(); scheduleRunEventCycle("project-a", components, {}, { onRunStarted }); flushDebounce(); @@ -37,7 +37,7 @@ describe("runEventCodeSnapshot", () => { }); test("suppresses repeated runs with unchanged code", () => { - const onRunStarted = jest.fn(); + const onRunStarted = vi.fn(); scheduleRunEventCycle("project-a", components, {}, { onRunStarted }); flushDebounce(); @@ -51,7 +51,7 @@ describe("runEventCodeSnapshot", () => { }); test("allows a run after code changes", () => { - const onRunStarted = jest.fn(); + const onRunStarted = vi.fn(); scheduleRunEventCycle("project-a", components, {}, { onRunStarted }); flushDebounce(); @@ -69,8 +69,8 @@ describe("runEventCodeSnapshot", () => { }); test("collapses a rapid burst into one run event", () => { - const onRunStarted = jest.fn(); - const onRunCompletedIfRunAlreadyEnded = jest.fn(); + const onRunStarted = vi.fn(); + const onRunCompletedIfRunAlreadyEnded = vi.fn(); scheduleRunEventCycle( "project-a", @@ -95,8 +95,8 @@ describe("runEventCodeSnapshot", () => { }); test("emits run completed on run end after debounced start", () => { - const onRunStarted = jest.fn(); - const onRunCompleted = jest.fn(); + const onRunStarted = vi.fn(); + const onRunCompleted = vi.fn(); scheduleRunEventCycle("project-a", components, {}, { onRunStarted }); flushDebounce(); @@ -108,7 +108,7 @@ describe("runEventCodeSnapshot", () => { }); test("resets snapshot when the project identifier changes", () => { - const onRunStarted = jest.fn(); + const onRunStarted = vi.fn(); scheduleRunEventCycle("project-a", components, {}, { onRunStarted }); flushDebounce(); @@ -122,7 +122,7 @@ describe("runEventCodeSnapshot", () => { }); test("allows separate bursts after debounce quiet period", () => { - const onRunStarted = jest.fn(); + const onRunStarted = vi.fn(); scheduleRunEventCycle( "project-a", @@ -133,7 +133,7 @@ describe("runEventCodeSnapshot", () => { flushDebounce(); endRunEventCycle(); - jest.advanceTimersByTime(RUN_EVENT_DEBOUNCE_MS); + vi.advanceTimersByTime(RUN_EVENT_DEBOUNCE_MS); scheduleRunEventCycle( "project-a", @@ -148,7 +148,7 @@ describe("runEventCodeSnapshot", () => { }); test("does not fire pending callbacks after debounce is cancelled", () => { - const onRunStarted = jest.fn(); + const onRunStarted = vi.fn(); scheduleRunEventCycle( "project-a", diff --git a/src/containers/WebComponentLoader.test.jsx b/src/containers/WebComponentLoader.test.jsx index 9ded0f337..2564b066f 100644 --- a/src/containers/WebComponentLoader.test.jsx +++ b/src/containers/WebComponentLoader.test.jsx @@ -20,17 +20,17 @@ import { useProject } from "../hooks/useProject"; import { useProjectPersistence } from "../hooks/useProjectPersistence"; import { Cookies, CookiesProvider } from "react-cookie"; -jest.mock("../hooks/useProject", () => ({ - useProject: jest.fn(), +vi.mock("../hooks/useProject", () => ({ + useProject: vi.fn(), })); -jest.mock("../hooks/useProjectPersistence", () => ({ - useProjectPersistence: jest.fn(), +vi.mock("../hooks/useProjectPersistence", () => ({ + useProjectPersistence: vi.fn(), })); -const mockedChangeLanguage = jest.fn(() => Promise.resolve()); +const mockedChangeLanguage = vi.fn(() => Promise.resolve()); -jest.mock("react-i18next", () => ({ +vi.mock("react-i18next", () => ({ useTranslation: () => { return { i18n: { @@ -56,7 +56,7 @@ const user = { access_token: "my_token" }; describe("When initially rendered", () => { beforeEach(() => { - document.dispatchEvent = jest.fn(); + document.dispatchEvent = vi.fn(); const mockStore = configureStore([]); const initialState = { editor: { @@ -362,7 +362,7 @@ describe("When no user is in state", () => { , ); - expect(container.querySelector("#wc")).toHaveClass( + expect(container.querySelector("[id='wc']")).toHaveClass( "--light", "--use-editor-styles", ); @@ -385,8 +385,8 @@ describe("When no user is in state", () => { , ); - expect(container.querySelector("#wc")).toHaveClass("--dark"); - expect(container.querySelector("#wc")).not.toHaveClass( + expect(container.querySelector("[id='wc']")).toHaveClass("--dark"); + expect(container.querySelector("[id='wc']")).not.toHaveClass( "--use-editor-styles", ); }); @@ -483,7 +483,7 @@ describe("When user is in state", () => { describe("when user data is set", () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); render( @@ -494,12 +494,12 @@ describe("When user is in state", () => { }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); test("removes if user data is removed from local storage", () => { localStorage.removeItem(authKey); - jest.advanceTimersByTime(50000); + vi.advanceTimersByTime(50000); expect(store.getActions()).toEqual( expect.arrayContaining([setUser(null)]), ); @@ -513,7 +513,7 @@ describe("When user is in state", () => { other_user_data: "data", }), ); - jest.advanceTimersByTime(50000); + vi.advanceTimersByTime(50000); expect(store.getActions()).toEqual( expect.arrayContaining([ setUser({ access_token: "new_token", other_user_data: "data" }), diff --git a/src/hooks/useAutoSave/useAutoSave.test.js b/src/hooks/useAutoSave/useAutoSave.test.js index 5a370e313..6d67bce44 100644 --- a/src/hooks/useAutoSave/useAutoSave.test.js +++ b/src/hooks/useAutoSave/useAutoSave.test.js @@ -11,8 +11,8 @@ let mockDispatch; let resolveSave; let rejectSave; -jest.mock("react-redux", () => ({ - ...jest.requireActual("react-redux"), +vi.mock("react-redux", async () => ({ + ...(await vi.importActual("react-redux")), useDispatch: () => mockDispatch, useSelector: (selector) => selector({ @@ -26,12 +26,12 @@ jest.mock("react-redux", () => ({ }), })); -jest.mock("../../redux/EditorSlice", () => ({ - ...jest.requireActual("../../redux/EditorSlice"), - syncProject: jest.fn((_) => jest.fn()), +vi.mock("../../redux/EditorSlice", async () => ({ + ...(await vi.importActual("../../redux/EditorSlice")), + syncProject: vi.fn((_) => vi.fn()), })); -jest.useFakeTimers(); +vi.useFakeTimers(); const user1 = { access_token: "myAccessToken1", @@ -71,10 +71,10 @@ const editedProject = { }; const saveAction = { type: "SAVE_PROJECT" }; -const saveProject = jest.fn(() => saveAction); +const saveProject = vi.fn(() => saveAction); const createAsyncThunkDispatchMock = () => - jest.fn(() => { + vi.fn(() => { const thunkPromise = new Promise((resolve) => { resolveSave = resolve; rejectSave = (error) => @@ -99,7 +99,7 @@ beforeEach(() => { mockInitialProjectInstructions = project.instructions ?? null; mockSaving = "idle"; mockCodeRunInProgress = false; - syncProject.mockImplementation(jest.fn((_) => saveProject)); + syncProject.mockImplementation(vi.fn((_) => saveProject)); mockDispatch = createAsyncThunkDispatchMock(); }); @@ -182,7 +182,7 @@ describe("useAutoSave", () => { }); act(() => { - jest.advanceTimersByTime(10000); + vi.advanceTimersByTime(10000); }); expect(mockDispatch).toHaveBeenCalledTimes(2); @@ -319,7 +319,7 @@ describe("useAutoSave", () => { expect(mockDispatch).toHaveBeenCalledTimes(1); act(() => { - jest.advanceTimersByTime(10000); + vi.advanceTimersByTime(10000); }); expect(mockDispatch).toHaveBeenCalledTimes(2); diff --git a/src/hooks/useContainerMinWidth.test.js b/src/hooks/useContainerMinWidth.test.js index e74743819..1ed9de359 100644 --- a/src/hooks/useContainerMinWidth.test.js +++ b/src/hooks/useContainerMinWidth.test.js @@ -8,14 +8,14 @@ let observers; class MockResizeObserver { constructor(callback) { this.callback = callback; - this.observe = jest.fn(); - this.disconnect = jest.fn(); + this.observe = vi.fn(); + this.disconnect = vi.fn(); observers.push(this); } } const makeElement = (width) => ({ - getBoundingClientRect: jest.fn(() => ({ width })), + getBoundingClientRect: vi.fn(() => ({ width })), }); const renderObservedHook = (width) => { diff --git a/src/hooks/useIsOnline.test.js b/src/hooks/useIsOnline.test.js index d8406b664..0c38c4d1a 100644 --- a/src/hooks/useIsOnline.test.js +++ b/src/hooks/useIsOnline.test.js @@ -17,7 +17,7 @@ describe("useIsOnline", () => { get: () => true, }); - mockPostMessage = jest.fn(); + mockPostMessage = vi.fn(); swEventTarget = new EventTarget(); swEventTarget.controller = { postMessage: mockPostMessage }; @@ -82,17 +82,17 @@ describe("useIsOnline", () => { describe("CHECK_ONLINE polling", () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); test("does not poll while online", async () => { renderHook(() => useIsOnline()); await act(async () => { - jest.advanceTimersByTime(6000); + vi.advanceTimersByTime(6000); }); expect(mockPostMessage).not.toHaveBeenCalled(); }); @@ -103,13 +103,13 @@ describe("useIsOnline", () => { expect(result.current).toBe(false); await act(async () => { - jest.advanceTimersByTime(3000); + vi.advanceTimersByTime(3000); }); expect(mockPostMessage).toHaveBeenCalledTimes(1); expect(mockPostMessage).toHaveBeenCalledWith({ type: "CHECK_ONLINE" }); await act(async () => { - jest.advanceTimersByTime(3000); + vi.advanceTimersByTime(3000); }); expect(mockPostMessage).toHaveBeenCalledTimes(2); }); @@ -121,7 +121,7 @@ describe("useIsOnline", () => { expect(result.current).toBe(true); await act(async () => { - jest.advanceTimersByTime(6000); + vi.advanceTimersByTime(6000); }); expect(mockPostMessage).not.toHaveBeenCalled(); }); @@ -134,7 +134,7 @@ describe("useIsOnline", () => { unmount(); await act(async () => { - jest.advanceTimersByTime(6000); + vi.advanceTimersByTime(6000); }); expect(mockPostMessage).not.toHaveBeenCalled(); }); diff --git a/src/hooks/useProject.test.jsx b/src/hooks/useProject.test.jsx index d2805d7af..f13ff2b3f 100644 --- a/src/hooks/useProject.test.jsx +++ b/src/hooks/useProject.test.jsx @@ -6,21 +6,23 @@ import { useProject } from "./useProject"; import { syncProject, setProject } from "../redux/EditorSlice"; import { defaultPythonProject } from "../utils/defaultProjects"; -jest.mock("react-redux", () => ({ - ...jest.requireActual("react-redux"), - useDispatch: () => jest.fn(), +vi.mock("react-redux", async () => ({ + ...(await vi.importActual("react-redux")), + useDispatch: () => vi.fn(), })); -const loadProject = jest.fn(); +const loadProject = vi.fn(); const reactAppApiEndpoint = "localhost"; -jest.mock("../redux/EditorSlice"); +vi.mock("../redux/EditorSlice"); -jest.mock("../utils/apiCallHandler", () => () => ({ - readProject: async (identifier, projectType) => - Promise.resolve({ - data: { identifier: identifier, project_type: projectType }, - }), +vi.mock("../utils/apiCallHandler", () => ({ + default: () => ({ + readProject: async (identifier, projectType) => + Promise.resolve({ + data: { identifier: identifier, project_type: projectType }, + }), + }), })); const cachedProject = { @@ -136,7 +138,7 @@ describe("When not embedded", () => { }); test("If embedded prop is true before embedded state is set, loads from server instead of cache", () => { - syncProject.mockImplementation(jest.fn((_) => jest.fn())); + syncProject.mockImplementation(vi.fn((_) => vi.fn())); localStorage.setItem( cachedProject.identifier, JSON.stringify(cachedProject), @@ -157,7 +159,7 @@ describe("When not embedded", () => { }); test("If cached project does not match identifier, does not use cached project", async () => { - syncProject.mockImplementationOnce(jest.fn((_) => jest.fn())); + syncProject.mockImplementationOnce(vi.fn((_) => vi.fn())); localStorage.setItem("project", JSON.stringify(cachedProject)); renderHook( () => useProject({ projectIdentifier: "my-favourite-project" }), @@ -172,7 +174,7 @@ describe("When not embedded", () => { }); test("If cached project does not match locale, does not use cached project", async () => { - syncProject.mockImplementationOnce(jest.fn((_) => jest.fn())); + syncProject.mockImplementationOnce(vi.fn((_) => vi.fn())); localStorage.setItem("project", JSON.stringify(cachedProject)); renderHook( () => @@ -190,7 +192,7 @@ describe("When not embedded", () => { test("If current project has changed and locale changes, keeps current project", async () => { setCurrentProjectWithEdits(); - syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); + syncProject.mockImplementationOnce(vi.fn((_) => loadProject)); renderHook( () => @@ -233,7 +235,7 @@ describe("When not embedded", () => { }); test("If cached project does not match locale and browserPreview query is used outside embedded viewer, does not use cached project", () => { - syncProject.mockImplementation(jest.fn((_) => jest.fn())); + syncProject.mockImplementation(vi.fn((_) => vi.fn())); window.history.pushState( {}, "", @@ -258,7 +260,7 @@ describe("When not embedded", () => { }); test("If cached project does not match identifier and locale, loads correct uncached project", async () => { - syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); + syncProject.mockImplementationOnce(vi.fn((_) => loadProject)); localStorage.setItem("project", JSON.stringify(cachedProject)); renderHook( () => @@ -282,7 +284,7 @@ describe("When not embedded", () => { }); test("If loadCache is set to false, loads correct uncached project", async () => { - syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); + syncProject.mockImplementationOnce(vi.fn((_) => loadProject)); localStorage.setItem("project", JSON.stringify(cachedProject)); renderHook( () => @@ -306,7 +308,7 @@ describe("When not embedded", () => { }); test("If no cached project, loads uncached project", async () => { - syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); + syncProject.mockImplementationOnce(vi.fn((_) => loadProject)); renderHook( () => useProject({ @@ -328,7 +330,7 @@ describe("When not embedded", () => { }); test("If requested locale does not match the set language, does not set project", async () => { - syncProject.mockImplementationOnce(jest.fn((_) => jest.fn())); + syncProject.mockImplementationOnce(vi.fn((_) => vi.fn())); renderHook( () => useProject({ projectIdentifier: "my-favourite-project" }), { @@ -374,7 +376,7 @@ describe("When not embedded", () => { }); test("If loadRemix of a project is requested and remixLoadFailed is false", async () => { - syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); + syncProject.mockImplementationOnce(vi.fn((_) => loadProject)); renderHook( () => useProject({ @@ -396,7 +398,7 @@ describe("When not embedded", () => { }); test("If loadRemix of a project is requested and remixLoadFailed is true", async () => { - syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); + syncProject.mockImplementationOnce(vi.fn((_) => loadProject)); renderHook( () => useProject({ @@ -420,7 +422,7 @@ describe("When not embedded", () => { }); test("it calls loadProject when access token becomes available", async () => { - syncProject.mockImplementation(jest.fn((_) => loadProject)); + syncProject.mockImplementation(vi.fn((_) => loadProject)); const { rerender } = renderHook((props) => useProject(props), { initialProps: { projectIdentifier: project1.identifier, @@ -444,7 +446,7 @@ describe("When not embedded", () => { }); test("it does not call loadProject when access token changes", async () => { - syncProject.mockImplementation(jest.fn((_) => loadProject)); + syncProject.mockImplementation(vi.fn((_) => loadProject)); const { rerender } = renderHook((props) => useProject(props), { initialProps: { projectIdentifier: project1.identifier, @@ -468,7 +470,7 @@ describe("When not embedded", () => { }); test("If assetsIdentifer is set then set assetsOnly to true when loading project", async () => { - syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); + syncProject.mockImplementationOnce(vi.fn((_) => loadProject)); renderHook( () => useProject({ @@ -723,7 +725,7 @@ describe("When embedded", () => { }); test("If embedded and cached project, loads from server", async () => { - syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); + syncProject.mockImplementationOnce(vi.fn((_) => loadProject)); localStorage.setItem("hello-world-project", JSON.stringify(cachedProject)); renderHook( () => diff --git a/src/hooks/useProjectPersistence.test.js b/src/hooks/useProjectPersistence.test.js index 601121a97..d3c72ae09 100644 --- a/src/hooks/useProjectPersistence.test.js +++ b/src/hooks/useProjectPersistence.test.js @@ -14,8 +14,8 @@ let mockSaving = "idle"; let mockCodeRunInProgress = false; let mockDispatch; -jest.mock("react-redux", () => ({ - ...jest.requireActual("react-redux"), +vi.mock("react-redux", async () => ({ + ...(await vi.importActual("react-redux")), useDispatch: () => mockDispatch, useSelector: (selector) => selector({ @@ -29,22 +29,22 @@ jest.mock("react-redux", () => ({ }), })); -jest.mock("../redux/EditorSlice", () => ({ - ...jest.requireActual("../redux/EditorSlice"), - syncProject: jest.fn((_) => jest.fn()), - expireJustLoaded: jest.fn(), - setHasShownSavePrompt: jest.fn(), +vi.mock("../redux/EditorSlice", async () => ({ + ...(await vi.importActual("../redux/EditorSlice")), + syncProject: vi.fn((_) => vi.fn()), + expireJustLoaded: vi.fn(), + setHasShownSavePrompt: vi.fn(), })); -jest.mock("../utils/Notifications"); +vi.mock("../utils/Notifications"); const remixAction = { type: "REMIX_PROJECT" }; -const remixProject = jest.fn(() => remixAction); +const remixProject = vi.fn(() => remixAction); const saveAction = { type: "SAVE_PROJECT" }; -const saveProject = jest.fn(() => saveAction); -const loadProject = jest.fn(); +const saveProject = vi.fn(() => saveAction); +const loadProject = vi.fn(); -jest.useFakeTimers(); +vi.useFakeTimers(); const user1 = { access_token: "myAccessToken1", @@ -89,7 +89,7 @@ const editedProject = { }; const createAsyncThunkDispatchMock = (resolveImmediately = saveAction) => - jest.fn(() => { + vi.fn(() => { const thunkPromise = typeof resolveImmediately === "function" ? new Promise((resolve) => { @@ -135,7 +135,7 @@ describe("When not logged in", () => { justLoaded: true, }), ); - jest.runAllTimers(); + vi.runAllTimers(); }); test("Expires justLoaded", () => { @@ -156,7 +156,7 @@ describe("When not logged in", () => { justLoaded: false, }), ); - jest.runAllTimers(); + vi.runAllTimers(); }); test("Login prompt shown", () => { @@ -184,7 +184,7 @@ describe("When not logged in", () => { hasShownSavePrompt: true, }), ); - jest.runAllTimers(); + vi.runAllTimers(); }); test("Login prompt shown", () => { @@ -210,7 +210,7 @@ describe("When logged in", () => { justLoaded: true, }), ); - jest.runAllTimers(); + vi.runAllTimers(); }); test("Expires justLoaded", () => { @@ -231,7 +231,7 @@ describe("When logged in", () => { justLoaded: true, }), ); - jest.runAllTimers(); + vi.runAllTimers(); }); test("Expires justLoaded", () => { @@ -254,7 +254,7 @@ describe("When logged in", () => { justLoaded: false, }), ); - jest.runAllTimers(); + vi.runAllTimers(); }); test("Save prompt shown", () => { @@ -282,7 +282,7 @@ describe("When logged in", () => { hasShownSavePrompt: true, }), ); - jest.runAllTimers(); + vi.runAllTimers(); }); test("Save prompt not shown again", () => { @@ -298,8 +298,8 @@ describe("When logged in", () => { describe("When project has identifier and save triggered", () => { beforeEach(() => { - syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); - syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); + syncProject.mockImplementationOnce(vi.fn((_) => remixProject)); + syncProject.mockImplementationOnce(vi.fn((_) => loadProject)); renderHook(() => useProjectPersistence({ @@ -308,7 +308,7 @@ describe("When logged in", () => { saveTriggered: true, }), ); - jest.runAllTimers(); + vi.runAllTimers(); }); test("Clicking save dispatches remixProject with correct parameters", async () => { @@ -329,8 +329,8 @@ describe("When logged in", () => { describe("When project has identifier and save triggered without loadRemix", () => { beforeEach(() => { - syncProject.mockImplementationOnce(jest.fn((_) => remixProject)); - syncProject.mockImplementationOnce(jest.fn((_) => loadProject)); + syncProject.mockImplementationOnce(vi.fn((_) => remixProject)); + syncProject.mockImplementationOnce(vi.fn((_) => loadProject)); renderHook(() => useProjectPersistence({ @@ -340,7 +340,7 @@ describe("When logged in", () => { loadRemix: false, }), ); - jest.runAllTimers(); + vi.runAllTimers(); }); test("Clicking save dispatches remixProject with correct parameters", async () => { @@ -358,7 +358,7 @@ describe("When logged in", () => { describe("When project has no identifier and save triggered", () => { beforeEach(() => { - syncProject.mockImplementationOnce(jest.fn((_) => saveProject)); + syncProject.mockImplementationOnce(vi.fn((_) => saveProject)); renderHook(() => useProjectPersistence({ user: user2, @@ -366,7 +366,7 @@ describe("When logged in", () => { saveTriggered: true, }), ); - jest.runAllTimers(); + vi.runAllTimers(); }); test("Save project is called with the correct parameters", async () => { @@ -381,7 +381,7 @@ describe("When logged in", () => { describe("When user can autosave to the API", () => { beforeEach(() => { - syncProject.mockImplementation(jest.fn((_) => saveProject)); + syncProject.mockImplementation(vi.fn((_) => saveProject)); }); test("Does not autosave unchanged project to database", () => { @@ -392,7 +392,7 @@ describe("When logged in", () => { saveTriggered: false, }), ); - jest.runAllTimers(); + vi.runAllTimers(); expect(saveProject).not.toHaveBeenCalled(); }); @@ -404,7 +404,7 @@ describe("When logged in", () => { saveTriggered: false, }), ); - jest.runAllTimers(); + vi.runAllTimers(); expect(saveProject).toHaveBeenCalledWith({ project: editedProject, accessToken: user1.access_token, @@ -439,7 +439,7 @@ describe("When logged in", () => { ); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(saveProject).toHaveBeenCalledTimes(1); @@ -447,7 +447,7 @@ describe("When logged in", () => { rerender({ project: furtherEditedProject }); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(saveProject).toHaveBeenCalledTimes(1); @@ -458,7 +458,7 @@ describe("When logged in", () => { }); act(() => { - jest.advanceTimersByTime(10000); + vi.advanceTimersByTime(10000); }); expect(saveProject).toHaveBeenCalledTimes(2); @@ -478,7 +478,7 @@ describe("When logged in", () => { saveTriggered: false, }), ); - jest.runAllTimers(); + vi.runAllTimers(); expect(saveProject).not.toHaveBeenCalled(); expect(expireJustLoaded).toHaveBeenCalled(); }); @@ -492,7 +492,7 @@ describe("When logged in", () => { saveTriggered: false, }), ); - jest.runAllTimers(); + vi.runAllTimers(); expect(saveProject).toHaveBeenCalledWith({ project: editedProject, accessToken: user1.access_token, @@ -519,9 +519,9 @@ describe("When logged in", () => { saveTriggered: false, }), ); - jest.advanceTimersByTime(2500); + vi.advanceTimersByTime(2500); expect(saveProject).not.toHaveBeenCalled(); - jest.runAllTimers(); + vi.runAllTimers(); expect(saveProject).toHaveBeenCalledWith({ project: largeProject, accessToken: user1.access_token, @@ -537,7 +537,7 @@ describe("When logged in", () => { saveTriggered: true, }), ); - jest.runAllTimers(); + vi.runAllTimers(); expect(saveProject).toHaveBeenCalledWith({ project, accessToken: user1.access_token, @@ -555,7 +555,7 @@ describe("When logged in", () => { saveTriggered: false, }), ); - jest.runAllTimers(); + vi.runAllTimers(); expect(saveProject).toHaveBeenCalledWith({ project, accessToken: user1.access_token, diff --git a/src/hooks/useScratchSave/scratchSaveLifecycle.test.js b/src/hooks/useScratchSave/scratchSaveLifecycle.test.js index dc7bff4e5..1b6a3fea0 100644 --- a/src/hooks/useScratchSave/scratchSaveLifecycle.test.js +++ b/src/hooks/useScratchSave/scratchSaveLifecycle.test.js @@ -1,6 +1,6 @@ import { createScratchSaveLifecycle } from "./scratchSaveLifecycle"; -jest.useFakeTimers(); +vi.useFakeTimers(); const createLifecycle = ({ context = {}, @@ -16,7 +16,7 @@ const createLifecycle = ({ let dirty = context.dirty ?? true; let projectChangedAt = context.projectChangedAt ?? Date.now(); - const postSave = jest.fn(); + const postSave = vi.fn(); const baseContext = { canAutoSave: () => context.canAutoSave ?? true, @@ -68,7 +68,7 @@ describe("scratchSaveLifecycle", () => { expect(postSave).not.toHaveBeenCalled(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); expect(postSave).toHaveBeenCalledWith({ autosave: true }); }); @@ -78,16 +78,16 @@ describe("scratchSaveLifecycle", () => { createLifecycle(); triggerProjectChanged(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); expect(postSave).toHaveBeenCalledTimes(1); triggerProjectChanged(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); expect(postSave).toHaveBeenCalledTimes(1); resolveInFlight(); lifecycle.markSaveSucceeded(false); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); expect(postSave).toHaveBeenCalledTimes(2); }); @@ -97,12 +97,12 @@ describe("scratchSaveLifecycle", () => { createLifecycle(); triggerProjectChanged(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); resolveInFlight(); lifecycle.markSaveSucceeded(true); triggerProjectChanged(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); expect(postSave).toHaveBeenCalledTimes(1); const flushPromise = lifecycle.flushPendingAutoSave(); @@ -141,18 +141,18 @@ describe("scratchSaveLifecycle", () => { createLifecycle(); triggerProjectChanged(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); expect(postSave).toHaveBeenCalledTimes(1); lifecycle.markAutosaveFailed(true); lifecycle.flushQueuedSave(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); expect(postSave).toHaveBeenCalledTimes(2); expect(schedulerState.autosaveRetryUsed).toBe(true); lifecycle.markAutosaveFailed(true); lifecycle.flushQueuedSave(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); expect(postSave).toHaveBeenCalledTimes(2); expect(schedulerState.queued).toBe(false); }); @@ -161,17 +161,17 @@ describe("scratchSaveLifecycle", () => { const { postSave, triggerProjectChanged, lifecycle } = createLifecycle(); triggerProjectChanged(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); lifecycle.markAutosaveFailed(true); lifecycle.flushQueuedSave(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); lifecycle.markAutosaveFailed(true); lifecycle.flushQueuedSave(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); expect(postSave).toHaveBeenCalledTimes(2); triggerProjectChanged(); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); expect(postSave).toHaveBeenCalledTimes(3); }); }); diff --git a/src/hooks/useScratchSave/useScratchSaveState.test.jsx b/src/hooks/useScratchSave/useScratchSaveState.test.jsx index d971e2656..f011ee83c 100644 --- a/src/hooks/useScratchSave/useScratchSaveState.test.jsx +++ b/src/hooks/useScratchSave/useScratchSaveState.test.jsx @@ -14,12 +14,12 @@ import { getAutoSaveHostApi, } from "../../utils/save/autoSaveHostApi"; -jest.mock("../../utils/scratchIframe", () => ({ - getScratchAllowedOrigin: jest.fn(), - postMessageToScratchIframe: jest.fn(), +vi.mock("../../utils/scratchIframe", () => ({ + getScratchAllowedOrigin: vi.fn(), + postMessageToScratchIframe: vi.fn(), })); -jest.useFakeTimers(); +vi.useFakeTimers(); const scratchOrigin = "https://scratch-frame.example.com"; const scratchProject = { @@ -85,7 +85,7 @@ describe("useScratchSaveState", () => { const originalScratchFrameUrl = process.env.REACT_APP_SCRATCH_FRAME_URL; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); process.env.REACT_APP_SCRATCH_FRAME_URL = scratchOrigin; getScratchAllowedOrigin.mockReturnValue(scratchOrigin); }); @@ -94,7 +94,7 @@ describe("useScratchSaveState", () => { scratchSaveUnmount?.(); scratchSaveUnmount = null; clearScratchAutoSaveHostApi(); - jest.clearAllTimers(); + vi.clearAllTimers(); process.env.REACT_APP_SCRATCH_FRAME_URL = originalScratchFrameUrl; }); @@ -128,7 +128,7 @@ describe("useScratchSaveState", () => { dispatchScratchMessage("scratch-gui-project-changed"); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(postMessageToScratchIframe).not.toHaveBeenCalled(); @@ -141,7 +141,7 @@ describe("useScratchSaveState", () => { dispatchScratchMessage("scratch-gui-ready"); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(postMessageToScratchIframe).toHaveBeenCalledWith({ @@ -155,13 +155,13 @@ describe("useScratchSaveState", () => { dispatchScratchUserEdit(); act(() => { - jest.advanceTimersByTime(1999); + vi.advanceTimersByTime(1999); }); expect(postMessageToScratchIframe).not.toHaveBeenCalled(); act(() => { - jest.advanceTimersByTime(1); + vi.advanceTimersByTime(1); }); expect(postMessageToScratchIframe).toHaveBeenCalledWith({ @@ -175,19 +175,19 @@ describe("useScratchSaveState", () => { dispatchScratchUserEdit(); act(() => { - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); }); dispatchScratchMessage("scratch-gui-project-changed"); act(() => { - jest.advanceTimersByTime(999); + vi.advanceTimersByTime(999); }); expect(postMessageToScratchIframe).not.toHaveBeenCalled(); act(() => { - jest.advanceTimersByTime(1); + vi.advanceTimersByTime(1); }); expect(postMessageToScratchIframe).toHaveBeenCalledTimes(1); @@ -203,19 +203,19 @@ describe("useScratchSaveState", () => { dispatchScratchUserEdit(); act(() => { - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); }); dispatchScratchMessage("scratch-gui-saving-succeeded"); act(() => { - jest.advanceTimersByTime(999); + vi.advanceTimersByTime(999); }); expect(postMessageToScratchIframe).not.toHaveBeenCalled(); act(() => { - jest.advanceTimersByTime(1); + vi.advanceTimersByTime(1); }); expect(postMessageToScratchIframe).toHaveBeenCalledWith({ @@ -232,7 +232,7 @@ describe("useScratchSaveState", () => { dispatchScratchUserEdit(); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); dispatchScratchMessage("scratch-gui-saving-started"); @@ -291,19 +291,19 @@ describe("useScratchSaveState", () => { dispatchScratchUserEdit(); act(() => { - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); }); dispatchScratchMessage("scratch-gui-saving-failed"); act(() => { - jest.advanceTimersByTime(999); + vi.advanceTimersByTime(999); }); expect(postMessageToScratchIframe).not.toHaveBeenCalled(); act(() => { - jest.advanceTimersByTime(1); + vi.advanceTimersByTime(1); }); expect(postMessageToScratchIframe).toHaveBeenCalledWith({ @@ -339,7 +339,7 @@ describe("useScratchSaveState", () => { dispatchScratchUserEdit(); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); dispatchScratchMessage("scratch-gui-saving-started"); @@ -348,13 +348,13 @@ describe("useScratchSaveState", () => { dispatchScratchMessage("scratch-gui-project-changed"); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(postMessageToScratchIframe).toHaveBeenCalledTimes(1); act(() => { - jest.advanceTimersByTime(10000); + vi.advanceTimersByTime(10000); }); expect(postMessageToScratchIframe).toHaveBeenCalledTimes(2); @@ -366,7 +366,7 @@ describe("useScratchSaveState", () => { dispatchScratchUserEdit(); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); dispatchScratchMessage("scratch-gui-saving-started"); @@ -375,7 +375,7 @@ describe("useScratchSaveState", () => { dispatchScratchMessage("scratch-gui-project-changed"); act(() => { - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); }); expect(postMessageToScratchIframe).toHaveBeenCalledTimes(1); @@ -414,7 +414,7 @@ describe("useScratchSaveState", () => { dispatchScratchUserEdit(); act(() => { - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); }); expect(postMessageToScratchIframe).not.toHaveBeenCalled(); diff --git a/src/redux/EditorSlice.test.js b/src/redux/EditorSlice.test.js index 3237afccb..ec785f862 100644 --- a/src/redux/EditorSlice.test.js +++ b/src/redux/EditorSlice.test.js @@ -25,20 +25,28 @@ import reducer, { setFriendlyError, } from "./EditorSlice"; -const mockCreateRemix = jest.fn(); -const mockDeleteProject = jest.fn(); -const mockLoadAssets = jest.fn(); -const mockReadProject = jest.fn(); -const mockCreateOrUpdateProject = jest.fn(); - -jest.mock("../utils/apiCallHandler", () => () => ({ - createRemix: jest.fn(mockCreateRemix), - deleteProject: jest.fn(mockDeleteProject), - loadAssets: jest.fn(mockLoadAssets), - readProject: jest.fn(mockReadProject), - createOrUpdateProject: jest.fn(mockCreateOrUpdateProject), +const mockCreateRemix = vi.fn(); +const mockDeleteProject = vi.fn(); +const mockLoadAssets = vi.fn(); +const mockReadProject = vi.fn(); +const mockCreateOrUpdateProject = vi.fn(); + +vi.mock("../utils/apiCallHandler", () => ({ + default: () => ({ + createRemix: vi.fn(mockCreateRemix), + deleteProject: vi.fn(mockDeleteProject), + loadAssets: vi.fn(mockLoadAssets), + readProject: vi.fn(mockReadProject), + createOrUpdateProject: vi.fn(mockCreateOrUpdateProject), + }), })); +const originalDateNow = Date.now; + +afterEach(() => { + Date.now = originalDateNow; +}); + const friendlyErrorHtml = '
Friendly error title
' + '
A friendly summary of the error
'; @@ -301,7 +309,7 @@ test("Action setFriendlyError sets friendlyError", () => { }); describe("When project has no identifier", () => { - const dispatch = jest.fn(); + const dispatch = vi.fn(); const project = { name: "hello world", project_type: "python", @@ -329,7 +337,7 @@ describe("When project has no identifier", () => { let saveAction; beforeEach(() => { - Date.now = jest.fn(() => 1669808953); + Date.now = vi.fn(() => 1669808953); saveThunk = syncProject("save"); saveAction = saveThunk({ project, @@ -387,7 +395,7 @@ describe("When project has no identifier", () => { }); describe("When project has an identifier", () => { - const dispatch = jest.fn(); + const dispatch = vi.fn(); const project = { name: "hello world", project_type: "python", @@ -420,6 +428,7 @@ describe("When project has an identifier", () => { beforeEach(() => { localStorage.clear(); + Date.now = vi.fn(() => 1669808953); saveThunk = syncProject("save"); saveAction = saveThunk({ @@ -463,6 +472,7 @@ describe("When project has an identifier", () => { const expectedState = { project: project, saving: "success", + lastSavedTime: 1669808953, initialComponents: [ { name: "main", @@ -540,7 +550,7 @@ describe("When project has an identifier", () => { }); describe("When deleting a project", () => { - const dispatch = jest.fn(); + const dispatch = vi.fn(); let project = { identifier: "my-amazing-project", name: "hello world" }; const access_token = "myToken"; const initialState = { @@ -730,7 +740,7 @@ describe("Updating file name", () => { }); describe("Loading a project", () => { - const dispatch = jest.fn(); + const dispatch = vi.fn(); const identifier = "my-project-identifier"; const accessToken = "myToken"; const locale = "es-LA"; @@ -989,7 +999,7 @@ describe("initialComponents snapshot", () => { }); test("Scratch save lifecycle actions update editor save state", () => { - Date.now = jest.fn(() => 1669808953); + Date.now = vi.fn(() => 1669808953); const initialState = reducer( undefined, setProject({ diff --git a/src/redux/reducers/loadProjectReducers.test.js b/src/redux/reducers/loadProjectReducers.test.js index 95c50c2c4..07ddde7d4 100644 --- a/src/redux/reducers/loadProjectReducers.test.js +++ b/src/redux/reducers/loadProjectReducers.test.js @@ -3,13 +3,15 @@ import produce from "immer"; import reducer, { syncProject } from "../../redux/EditorSlice"; import { loadProjectRejected } from "./loadProjectReducers"; -const mockReadProject = jest.fn(); -jest.mock("../../utils/apiCallHandler", () => () => ({ - readProject: jest.fn(mockReadProject), +const mockReadProject = vi.fn(); +vi.mock("../../utils/apiCallHandler", () => ({ + default: () => ({ + readProject: vi.fn(mockReadProject), + }), })); const requestingAProject = function (project, projectFile) { - const dispatch = jest.fn(); + const dispatch = vi.fn(); const initialState = { editor: { project: {}, diff --git a/src/utils/Notifications.test.js b/src/utils/Notifications.test.js index 4548661f1..6e921e91a 100644 --- a/src/utils/Notifications.test.js +++ b/src/utils/Notifications.test.js @@ -5,10 +5,10 @@ import { showSavePrompt, } from "./Notifications"; -jest.mock("./i18n", () => ({ - t: (string) => string, +vi.mock("./i18n", () => ({ + default: { t: (string) => string }, })); -jest.mock("react-toastify"); +vi.mock("react-toastify"); test("Calling showSavedMessage calls toast with correct string", () => { showSavedMessage(); diff --git a/src/utils/ResizableWithHandle.test.jsx b/src/utils/ResizableWithHandle.test.jsx index 27a4593a9..ca1aa10d0 100644 --- a/src/utils/ResizableWithHandle.test.jsx +++ b/src/utils/ResizableWithHandle.test.jsx @@ -4,7 +4,7 @@ import ResizableWithHandle from "./ResizableWithHandle"; let mockResizeStop; -jest.mock("re-resizable", () => ({ +vi.mock("re-resizable", () => ({ Resizable: ({ children, handleComponent, diff --git a/src/utils/SelectButtons.test.jsx b/src/utils/SelectButtons.test.jsx index 8fc6a5c91..1dbbc7c3c 100644 --- a/src/utils/SelectButtons.test.jsx +++ b/src/utils/SelectButtons.test.jsx @@ -2,7 +2,7 @@ import { fireEvent, render, screen } from "@testing-library/react"; import React from "react"; import SelectButtons from "./SelectButtons"; -const setValue = jest.fn(); +const setValue = vi.fn(); beforeEach(() => { render( diff --git a/src/utils/ToastCloseButton.test.jsx b/src/utils/ToastCloseButton.test.jsx index 88fd9fd48..044b82b11 100644 --- a/src/utils/ToastCloseButton.test.jsx +++ b/src/utils/ToastCloseButton.test.jsx @@ -2,7 +2,7 @@ import React from "react"; import { fireEvent, render, screen } from "@testing-library/react"; import ToastCloseButton from "./ToastCloseButton"; -const closeToast = jest.fn(); +const closeToast = vi.fn(); beforeEach(() => { render(); diff --git a/src/utils/apiCallHandler.test.js b/src/utils/apiCallHandler.test.js index d478e73fa..e5fb96d14 100644 --- a/src/utils/apiCallHandler.test.js +++ b/src/utils/apiCallHandler.test.js @@ -2,7 +2,7 @@ import axios from "axios"; import ApiCallHandler from "./apiCallHandler"; -jest.mock("axios"); +vi.mock("axios"); const host = "http://localhost:3009"; const defaultHeaders = { headers: { Accept: "application/json" } }; const accessToken = "39a09671-be55-4847-baf5-8919a0c24a25"; diff --git a/src/utils/save/autoSaveHostApi.test.js b/src/utils/save/autoSaveHostApi.test.js index f68fc20ca..8146994d5 100644 --- a/src/utils/save/autoSaveHostApi.test.js +++ b/src/utils/save/autoSaveHostApi.test.js @@ -24,8 +24,8 @@ describe("autoSaveHostApi", () => { }); test("flushes project and scratch saves in sequence", async () => { - const projectFlush = jest.fn(() => Promise.resolve()); - const scratchFlush = jest.fn(() => Promise.resolve()); + const projectFlush = vi.fn(() => Promise.resolve()); + const scratchFlush = vi.fn(() => Promise.resolve()); registerAutoSaveHostApi({ flushPendingAutoSave: projectFlush, diff --git a/src/utils/save/autoSaveLifecycle.test.js b/src/utils/save/autoSaveLifecycle.test.js index 195b68e38..c2620172b 100644 --- a/src/utils/save/autoSaveLifecycle.test.js +++ b/src/utils/save/autoSaveLifecycle.test.js @@ -1,12 +1,12 @@ import { syncProject } from "../../redux/EditorSlice"; import { createAutoSaveLifecycle } from "./autoSaveLifecycle"; -jest.mock("../../redux/EditorSlice", () => ({ - ...jest.requireActual("../../redux/EditorSlice"), - syncProject: jest.fn((_) => jest.fn()), +vi.mock("../../redux/EditorSlice", async () => ({ + ...(await vi.importActual("../../redux/EditorSlice")), + syncProject: vi.fn((_) => vi.fn()), })); -jest.useFakeTimers(); +vi.useFakeTimers(); const user1 = { access_token: "myAccessToken1", @@ -30,7 +30,7 @@ const editedProject = { }; const saveAction = { type: "SAVE_PROJECT" }; -const saveProject = jest.fn(() => saveAction); +const saveProject = vi.fn(() => saveAction); /** flushPendingAutoSave is async; yield so it reaches its next await. */ const awaitAsyncFlush = () => Promise.resolve(); @@ -56,7 +56,7 @@ const createLifecycle = ({ let resolveSave; let rejectSave; - const dispatch = jest.fn(() => { + const dispatch = vi.fn(() => { const thunkPromise = new Promise((resolve) => { resolveSave = resolve; rejectSave = (error) => @@ -110,7 +110,7 @@ const createLifecycle = ({ }; beforeEach(() => { - syncProject.mockImplementation(jest.fn((_) => saveProject)); + syncProject.mockImplementation(vi.fn((_) => saveProject)); }); describe("autoSaveLifecycle", () => { @@ -162,7 +162,7 @@ describe("autoSaveLifecycle", () => { lifecycle.requestAutoSave(); expect(dispatch).toHaveBeenCalledTimes(1); - jest.advanceTimersByTime(10000); + vi.advanceTimersByTime(10000); expect(inFlightSavePromiseRef.current).not.toBeNull(); await completeInFlightSave({ inFlightSavePromiseRef, resolveSave }); diff --git a/src/utils/scratchIframe.test.js b/src/utils/scratchIframe.test.js index e373a56b4..a523ab205 100644 --- a/src/utils/scratchIframe.test.js +++ b/src/utils/scratchIframe.test.js @@ -13,13 +13,13 @@ describe("scratchIframe", () => { let originalQuerySelector; beforeEach(() => { - mockPostMessage = jest.fn(); + mockPostMessage = vi.fn(); mockContentWindow = { postMessage: mockPostMessage }; - mockShadowQuerySelector = jest.fn(() => ({ + mockShadowQuerySelector = vi.fn(() => ({ contentWindow: mockContentWindow, })); originalQuerySelector = document.querySelector; - document.querySelector = jest.fn(() => ({ + document.querySelector = vi.fn(() => ({ shadowRoot: { querySelector: mockShadowQuerySelector, }, @@ -91,7 +91,7 @@ describe("scratchIframe", () => { }); it("calls the handler with the updated project id", () => { - const handler = jest.fn(); + const handler = vi.fn(); const unsubscribe = subscribeToScratchProjectIdentifierUpdates(handler); window.dispatchEvent( @@ -111,7 +111,7 @@ describe("scratchIframe", () => { it("accepts updates when REACT_APP_SCRATCH_FRAME_URL contains a path", () => { process.env.REACT_APP_SCRATCH_FRAME_URL = "https://scratch-frame.example.com/branches/main"; - const handler = jest.fn(); + const handler = vi.fn(); const unsubscribe = subscribeToScratchProjectIdentifierUpdates(handler); window.dispatchEvent( @@ -129,7 +129,7 @@ describe("scratchIframe", () => { }); it("ignores unrelated messages", () => { - const handler = jest.fn(); + const handler = vi.fn(); const unsubscribe = subscribeToScratchProjectIdentifierUpdates(handler); window.dispatchEvent( diff --git a/src/utils/setupTests.vitest.js b/src/utils/setupTests.vitest.js index b4716c0e1..7f1497d1d 100644 --- a/src/utils/setupTests.vitest.js +++ b/src/utils/setupTests.vitest.js @@ -1,3 +1,74 @@ // Vitest equivalent of setupTests.js, used by every file not listed in // JEST_ONLY_TEST_FILES (test-runner-migration.js). import "@testing-library/jest-dom"; +import { vi } from "vitest"; + +class MemoryStorage { + #store = new Map(); + + getItem(key) { + return this.#store.has(key) ? this.#store.get(key) : null; + } + + setItem(key, value) { + this.#store.set(key, String(value)); + } + + removeItem(key) { + this.#store.delete(key); + } + + clear() { + this.#store.clear(); + } + + get length() { + return this.#store.size; + } + + key(index) { + return Array.from(this.#store.keys())[index] ?? null; + } +} + +Object.defineProperty(window, "localStorage", { + value: new MemoryStorage(), + configurable: true, +}); + +window.matchMedia = (query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), // Deprecated + removeListener: vi.fn(), // Deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), +}); + +vi.mock("@raspberrypifoundation/python-friendly-error-messages", () => ({ + loadCopydeckFor: vi.fn(), + registerAdapter: vi.fn(), + cpythonAdapter: {}, + friendlyExplain: vi.fn(), +})); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (str) => (str.includes("null") ? null : str), + i18n: { + changeLanguage: () => new Promise(() => {}), + language: "ja-JP", + options: { + locales: ["en", "es-LA", "fr-FR", "ja-JP"], + }, + }, + }), + Trans: ({ children, i18nKey }) => children || i18nKey, + initReactI18next: {}, +})); + +vi.mock("./i18n", () => ({ + default: { t: (string) => string }, +})); diff --git a/test-runner-migration.js b/test-runner-migration.js index f53d49d09..198ab5ffd 100644 --- a/test-runner-migration.js +++ b/test-runner-migration.js @@ -6,68 +6,14 @@ // Vitest - anything not listed here runs under Vitest by default, including // any new test file. const JEST_ONLY_TEST_FILES = [ - "src/components/AstroPiModel/AstroPiControls/MotionInput.test.jsx", - "src/components/AstroPiModel/FlightCase.test.jsx", - "src/components/AstroPiModel/OrientationPanel/OrientationPanel.test.jsx", - "src/components/AstroPiModel/OrientationPanel/OrientationResetButton.test.jsx", - "src/components/AstroPiModel/Simulator.test.jsx", - "src/components/DownloadButton/DownloadButton.test.jsx", - "src/components/Editor/EditorInput/EditorInput.test.jsx", - "src/components/Editor/EditorPanel/EditorPanel.test.jsx", - "src/components/Editor/ErrorMessage/ErrorMessage.test.jsx", - "src/components/Editor/Output/Output.test.jsx", "src/components/Editor/Project/Project.test.jsx", "src/components/Editor/Project/ScratchContainer.test.jsx", - "src/components/Editor/Runners/HtmlRunner/HtmlRenderer.test.jsx", "src/components/Editor/Runners/HtmlRunner/HtmlRunner.test.jsx", "src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideRunner.test.jsx", "src/components/Editor/Runners/PythonRunner/PyodideRunner/PyodideWorker.test.js", "src/components/Editor/Runners/PythonRunner/PyodideRunner/VisualOutputPane.test.jsx", "src/components/Editor/Runners/PythonRunner/PythonRunner.test.jsx", "src/components/Editor/Runners/PythonRunner/SkulptRunner/SkulptRunner.test.jsx", - "src/components/Menus/ContextMenu/ContextMenu.test.jsx", - "src/components/Menus/FileMenu/FileMenu.test.jsx", - "src/components/Menus/Sidebar/DownloadPanel/DownloadPanel.test.jsx", - "src/components/Menus/Sidebar/FilePanel/FilePanel.test.jsx", - "src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.jsx", - "src/components/Menus/Sidebar/InstructionsPanel/ProgressBar/ProgressBar.test.jsx", - "src/components/Menus/Sidebar/ProjectsPanel/ProjectsPanel.test.jsx", - "src/components/Menus/Sidebar/SettingsPanel/SettingsPanel.test.jsx", - "src/components/Menus/Sidebar/SettingsPanel/ThemeToggle/ThemeToggle.test.jsx", - "src/components/Menus/Sidebar/Sidebar.test.jsx", - "src/components/Menus/Sidebar/SidebarBar.test.jsx", - "src/components/Menus/Sidebar/SidebarBarOption.test.jsx", - "src/components/Mobile/MobileProject/MobileProject.test.jsx", - "src/components/Modals/ErrorModal.test.jsx", - "src/components/Modals/GeneralModal.test.jsx", - "src/components/ProjectBar/ProjectBar.test.jsx", - "src/components/ProjectBar/ScratchProjectBar.test.jsx", - "src/components/ProjectName/ProjectName.test.jsx", - "src/components/RunButton/StopButton.test.jsx", - "src/components/SaveButton/SaveButton.test.jsx", - "src/components/SaveStatus/SaveStatus.test.jsx", - "src/components/UploadButton/UploadButton.test.jsx", - "src/components/WebComponentProject/WebComponentProject.integration.test.jsx", - "src/components/WebComponentProject/WebComponentProject.test.jsx", - "src/components/WebComponentProject/runEventCodeSnapshot.test.js", - "src/containers/WebComponentLoader.test.jsx", - "src/hooks/useAutoSave/useAutoSave.test.js", - "src/hooks/useContainerMinWidth.test.js", - "src/hooks/useIsOnline.test.js", - "src/hooks/useProject.test.jsx", - "src/hooks/useProjectPersistence.test.js", - "src/hooks/useScratchSave/scratchSaveLifecycle.test.js", - "src/hooks/useScratchSave/useScratchSaveState.test.jsx", - "src/redux/EditorSlice.test.js", - "src/redux/reducers/loadProjectReducers.test.js", - "src/utils/Notifications.test.js", - "src/utils/ResizableWithHandle.test.jsx", - "src/utils/SelectButtons.test.jsx", - "src/utils/ToastCloseButton.test.jsx", - "src/utils/apiCallHandler.test.js", - "src/utils/save/autoSaveHostApi.test.js", - "src/utils/save/autoSaveLifecycle.test.js", - "src/utils/scratchIframe.test.js", ]; module.exports = { JEST_ONLY_TEST_FILES }; diff --git a/vite.config.js b/vite.config.js index 8d151382b..369e45770 100644 --- a/vite.config.js +++ b/vite.config.js @@ -170,6 +170,7 @@ export default defineConfig(async ({ mode }) => { test: { environment: "jsdom", globals: true, + mockReset: true, setupFiles: [path.resolve(__dirname, "src/utils/setupTests.vitest.js")], include: [ "src/**/__tests__/**/*.{js,jsx,ts,tsx}",