Migrate remaining jest tests to vitest - #1608
Open
zetter-rpf wants to merge 16 commits into
Open
Conversation
Previously ScratchContainer.test.jsx only ran under Jest, blocked by
the window.localStorage gap fixed in an earlier commit.
This change swaps jest.fn/jest.mock/jest.clearAllMocks/
jest.useFakeTimers/jest.useRealTimers/jest.advanceTimersByTime for
their vi equivalents and jest.requireActual for the async
vi.importActual pattern.
The test used require("./ScratchContainer") instead of a static
import specifically so the module loaded after the overlayscrollbars
mocks were set up - ScratchContainer.jsx calls
OverlayScrollbars.plugin() at module scope, not just inside the
component. Vitest does not resolve a bare require() the way Jest
does, so this switched to a static import, which surfaced the same
ordering problem in ESM form: vi.mock is hoisted above the import,
but the mock factories referenced mockOverlayScrollbarsComponent and
mockPlugin, plain consts declared below the (also hoisted) mocks,
so ScratchContainer.jsx ran into them before initialization. Moved
those declarations into a vi.hoisted() block, which Vitest hoists to
the same position as vi.mock, resolving the ordering.
Confirmed via full Jest and Vitest runs: 176 Jest + 875 Vitest tests
pass, the same total as before the move.
Previously HtmlRunner.test.jsx only ran under Jest, blocked by the
window.localStorage gap fixed in an earlier commit (HtmlRunner.jsx
calls localStorage.setItem).
This change swaps jest.spyOn for vi.spyOn, and jest.requireActual for
the async vi.importActual pattern for both the react-responsive mock
and the node-html-parser mock. The latter also called
jest.requireActual("node-html-parser") a second time, synchronously,
inside a later beforeEachs mockImplementation callback - not possible
with vi.importActuals async API, so that beforeEach now reuses a
single actualParseHtml reference resolved once via a top-level await
vi.importActual(...) instead of re-requiring the real module on every
render.
Confirmed via full Jest and Vitest runs: 158 Jest + 893 Vitest tests
pass, the same total as before the move.
Previously PythonRunner.test.jsx only ran under Jest. It has no Jest-only APIs itself, but rendering it crashed under Vitest because PyodideRunner.jsx constructs `new Worker(...)` on mount and Vitests jsdom environment, unlike jest-environment-jsdom, does not define a global Worker. setupTests.vitest.js now defines a minimal stub Worker class, matching the window.Worker = PyodideWorker stub setupTests.js already installs for Jest via PyodideWorker.mock.js, which fixes this for any test that mounts a component constructing a Worker, not just this one. Confirmed via full Jest and Vitest runs: 144 Jest + 907 Vitest tests pass, the same total as before the move.
Previously PyodideRunner.test.jsx only ran under Jest. Rendering it
needs window.Worker, since PyodideRunner.jsx constructs one on mount,
and it also imports the shared PyodideWorker.mock.js directly to
track and drive worker instances via getLastInstance() -
that mock file uses jest.fn(), so it does not work under Vitest.
Added PyodideWorker.mock.vitest.js as its vi.fn()-based counterpart,
used by this test.
Rather than installing window.Worker globally in
setupTests.vitest.js as first attempted, both this file and
PythonRunner.test.jsx (migrated previously) now set it themselves,
right where its needed - Vitest gives each test file its own fresh
window, so a module-scope assignment cant leak into other test
files, and nothing here reassigns it between tests, so no
before/after hook is needed either.
The "When friendly errors are enabled" tests used
require("@raspberrypifoundation/python-friendly-error-messages")
inside a beforeEach specifically to read back the (Jest-)global-mocked
functions without going through ESM import hoisting. Vitest cant
resolve that packages real dist build via require any better than via
import, so this switched to a static import of the same three
functions, which Vitests hoisted vi.mock in setupTests.vitest.js
already intercepts.
Confirmed via full Jest and Vitest runs: 101 Jest + 950 Vitest tests
pass, the same total as before the move.
Previously SkulptRunner.test.jsx only ran under Jest.
This change swaps jest.fn/jest.spyOn for vi equivalents and
jest.requireActual for the async vi.importActual pattern for the
react-responsive mock. As with PyodideRunner, the "When friendly
errors are enabled" tests used
require("@raspberrypifoundation/python-friendly-error-messages")
inside a beforeEach to read back the global-mocked functions; that
require call is replaced with a static import of the same three
functions, resolved via the hoisted vi.mock in setupTests.vitest.js.
Confirmed via full Jest and Vitest runs: 42 Jest + 1009 Vitest tests
pass, the same total as before the move.
Previously VisualOutputPane.test.jsx only ran under Jest. This change swaps jest.mock for vi.mock, and adds a local vi.mock for plotly.js (also wrapped with a default key, since the component imports it as a default and Vitest does not apply Jests CJS default-export interop to mock factories) rather than relying on setupTests.js global plotly.js mock, which has no Vitest counterpart - this component is the only place plotly.js is imported, so the mock belongs with the test that needs it rather than in shared setup. "it applies the attributes within turtle" asserted a style value of color: "red" (with literal quotes) applied via node.setAttribute in elementFromProps, then re-parsed through innerHTML. Vitests newer jsdom/cssstyle rejects that malformed color value as invalid CSS and drops the declaration entirely, where Jests older jsdom apparently tolerated it - fixed the fixture to the valid color: red, and updated the assertion to color: rgb(255, 0, 0), which is what jest-dom normalizes a parsed color attribute to. Confirmed via full Jest and Vitest runs: 30 Jest + 1021 Vitest tests pass, the same total as before the move.
Previously PyodideWorker.test.jsx only ran under Jest, per the earlier note that it dynamically re-imports the worker script per test via resetModules but TextEncoder broke post-reset. This change swaps jest.fn for vi.fn and jest.resetModules() for vi.resetModules(), and switches the per-test require() of PyodideWorker.js (needed to get a fresh module each test, since the script assigns globalThis.PyodideWorker as a side effect and closes over per-worker state) to an awaited dynamic import() in an async beforeEach, since Vitest does not support a bare require() the way Jest does. The actual TextEncoder break was unrelated to resetModules: the test imported TextEncoder from "util" and assigned it to global.TextEncoder, but that named import resolves to undefined under Vitest, so the file was really assigning global.TextEncoder = undefined. Removed the import and the reassignment entirely - TextEncoder is already a real global under Vitests jsdom environment (Node has provided it as a global since v11), so the tests remaining `new TextEncoder()` calls resolve correctly on their own. Confirmed via full Jest and Vitest runs: 8 Jest + 1043 Vitest tests pass, the same total as before the move.
Previously Project.test.jsx only ran under Jest, per the earlier note that its module-level jest.useFakeTimers() produced an empty render under Vitest. This change swaps jest.fn/jest.mock for vi equivalents and jest.requireActual for the async vi.importActual pattern for the react-router-dom mock. The useFakeTimers() call itself is dropped entirely: nothing in the file ever advances or asserts against fake time, and the one test using waitFor() (for a project-saved message dispatched from a plain useEffect, not a timer) hung indefinitely under Vitest with fake timers active, since testing-library dom waitFor only knows how to drive Jests fake timers forward - it detects them by checking for a global `jest`, so under Vitest it falls back to real timers for its own polling, which fake timers had already intercepted. This is the last file on the Jest list: test-runner-migration.js JEST_ONLY_TEST_FILES is now empty, so every test in the repo runs under Vitest. An empty array made jest.config.js testMatch fall back to Jests own defaults and match all 267 test files instead of none, so testMatch now matches an always-empty path once the list is empty. yarn test now exits 1 with "no tests found" rather than running the whole suite under the wrong runner; whether to add --passWithNoTests or retire the Jest CI step and config entirely is still an open follow-up. Confirmed via full Vitest run: 1051 tests pass, the same total as before the move.
zetter-rpf
temporarily deployed
to
previews/1608/merge
August 24, 2026 07:53 — with
GitHub Actions
Inactive
Previously the repo ran tests under Jest via jest.config.js, config/jest/babelTransform.js, scripts/test.js and src/utils/setupTests.js, kept alive alongside Vitest by test-runner-migration.js listing any files not yet ported. That list is now empty, so nothing exercises the Jest path any more, and .babelrc (only read by the Jest babel transform) and PyodideWorker.mock.js (only imported by the removed Jest setup file) were dead weight too. This change deletes those Jest-only files and drops vite.config.js/setupTests.vitest.js references to the now-removed test-runner-migration.js, leaving Vitest as the sole test runner.
Previously "yarn test" shelled out to Jest (scripts/test.js, now removed) and CI ran both a Jest pass and a separate Vitest pass, with Jest's "--coverage" output feeding a "Record coverage" step that posts the clover coverage percentage as a PR comment. Vitest's CI step never passed "--coverage" and no @vitest/coverage-* provider is installed, so that comment was already effectively dead code, only still fed by the Jest run. Nothing in CI enforces a coverage threshold or otherwise checks this output; it was purely informational and nobody currently relies on it, so dropping it removes no active guarantee. This change makes "yarn test" run Vitest directly (watch mode locally, single run in CI, matching the previous Jest behaviour), drops the now-redundant "test:vitest"/"test:all" scripts, and removes the separate Jest CI step, the "Record coverage" step and its script, and the JEST_JUNIT_OUTPUT_DIR env var. The VS Code task and dev container extension/setting for running the current test file are updated to invoke Vitest instead of Jest. Alternatively we could have kept the coverage PR comment by adding @vitest/coverage-v8 and clover reporter config, but that is a new feature rather than a like-for-like removal, so it is left for a separate change if wanted.
Previously .eslintrc.json extended "react-app/jest", which pulled in eslint-plugin-jest for the "jest" globals env (describe/it/expect/etc) and a subset of eslint-plugin-testing-library rules on test files. With Jest removed there is no reason to keep eslint-plugin-jest installed, but dropping it blindly would leave every Vitest test file failing "no-undef" on those globals and lose the testing-library lint coverage. This change removes the "react-app/jest" extend and the "jest" eslint plugin, declares the Vitest globals (describe/it/test/expect/ beforeEach/afterEach/beforeAll/afterAll) explicitly on the existing test-file overrides, and swaps in "plugin:testing-library/react" on those same overrides so the equivalent testing-library rules still apply. "yarn lint" is clean with no new warnings.
Previously Jest and its supporting packages (jest, jest-circus, jest-environment-jsdom, jest-css-modules-transform, jest-scss-transform, jest-transform-stub, jest-transformer-svg, jest-watch-typeahead, jest-canvas-mock, jest-junit, jest-github-actions-reporter, babel-jest, eslint-plugin-jest) stayed installed to run the Jest half of the test suite. All tests now run on Vitest and the Jest config/scripts were removed in earlier commits, so these are unused. This change removes them via "yarn remove" and updates yarn.lock. "jest-axe" and "@testing-library/jest-dom" are kept: despite the name, they are matcher libraries used directly in test files and work independently of which test runner executes them. "yarn lint", "yarn test --run" and "yarn build" all still pass.
Previously README.md and AGENTS.md (CLAUDE.md) described the project as testing on Jest, documented the old combined Jest+Vitest CI command with Jest-specific coverage/reporter flags, and listed config/ as holding Jest config. Jest has now been fully removed and config/ only holds the nginx deploy template. This change updates both docs to describe Vitest as the test runner, simplifies the documented CI test command to "yarn test --run", and corrects the config/ directory description.
Previously a comment in scratchblocksLocales.js attributed the trimmed-locale-list approach to avoiding "jest module import errors", and a comment in InstructionsPanel.test.jsx referenced jest's `resetMocks: true` clearing a mocked implementation per-test. Both files run under Vitest now, and vite.config.js sets the equivalent `mockReset: true` option. This change updates both comments to name Vitest and its `mockReset` option instead of Jest.
Tried dropping all Babel-related dependencies, since @babel/core, babel-eslint and babel-preset-react-app looked like they might only be needed to support Jest's babel-jest transform (now removed). @babel/core and babel-preset-react-app check out: nothing besides the deleted .babelrc referenced them, and lint/tests/build all stay green without them. babel-eslint does not: eslint-config-react-app's base config hard- codes "parser: babel-eslint", and our own top-level .eslintrc.json parser must override that to have any effect. Swapping in the built-in espree parser fails outright on the ES2021/2022 syntax already in use (numeric separators, uninitialized class fields, top-level await), throwing parse errors. Swapping in a newer standalone espree (9.6.1) parses that syntax fine, but ESLint 7's bundled eslint-scope/no-undef predate the PropertyDefinition AST node for class fields, so it wrongly flags every class field as "not defined" - a new dependency added to replace one still being removed, while also not being clean. babel-eslint stays.
Previously the only editor task ran every test in the current file.
Vitest supports filtering to a single test by appending ":<line>" to
the file path, so a test under the cursor can be run without pasting
its name into -t or waiting on the whole file.
This change adds a "Test Current Line (Vitest)" task using VS Code's
built-in "${lineNumber}" variable.
zetter-rpf
marked this pull request as ready for review
August 24, 2026 08:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This follows on from #1570 and #1607 and migrates the remaining tests to vitest.
See commits for more
As doing this I think there are a few things we should do after:
Changes made with Claude