Skip to main content

server-mocha-to-vitest

Converts one @packages/server unit spec from mocha (test/unit/**/*_spec.ts) to vitest (*.spec.ts) without changing what the test proves. Use when migrating a server unit spec, when a converted spec passes but its mocks may not be intercepting, or when the user mentions spec_helper, mockery, proxyquire, sinon, snap-shot-it, or the mocha/vitest split in packages/server.

Source facts

Repository
cypress-io/cypress
Last source activity
September 17, 2026 at 17:00
Detected SKILL.md language
English
Stars
51,026
Forks
3,645

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
server-mocha-to-vitest
description
Converts one @packages/server unit spec from mocha (test/unit/**/*_spec.ts) to vitest (*.spec.ts) without changing what the test proves. Use when migrating a server unit spec, when a converted spec passes but its mocks may not be intercepting, or when the user mentions spec_helper, mockery, proxyquire, sinon, snap-shot-it, or the mocha/vitest split in packages/server.
# Migrate a `@packages/server` unit spec from mocha to vitest Scope: `packages/server/test/unit` only. Integration and performance suites stay on mocha. This skill lists the things that go wrong *silently*: a converted spec that is green while its mocks never engaged, or that asserts less than the mocha original did. Plain API mapping (`sinon.stub` to `vi.fn`, `to.deep.equal` to `toEqual`, `context` to `describe`) is not covered; you already know it. Parent issue: cypress-io/cypress#34846. ## The contract - `*_spec.ts` runs on mocha via `test/scripts/run.js`. `*.spec.ts` runs on vitest via `packages/server/vitest.config.ts`. `yarn test-unit` runs both. - Convert by `git mv foo_spec.ts foo.spec.ts` and rewriting the file. The mocha file must be gone when you finish. Never leave a file half-converted: every `it` in the original is ported, or the file is not touched at all. - The new file imports from `vitest` explicitly and uses nothing else for test plumbing: no `spec_helper`, `mockery`, `proxyquire`, `sinon`, `chai` or its plugins, `snap-shot-it`, `mocked-env`. Keep `nock`, `supertest`, `express`, `lodash`, `bluebird` where the original used them for real work. - Never weaken a test: no `.skip`, no dropped or loosened assertion, no `vitest -u` to make a snapshot agree. If a test cannot be made green, leave it failing and say why in the PR. - Do not touch `.circleci/`, `vitest.config.ts`, or `cloud/environment_spec.ts`. The last one is named in four CI workflows and migrates in its own human PR. - Specs never connect to a non-loopback address. Tests must pass on a sandboxed host. ## `spec_helper.js` is not ported. Inline what the file needs. `test/spec_helper.js` installs root hooks for the whole mocha process. A mocha spec depends on them even when it never imports `spec_helper`, because some other spec in the same run did. Under vitest each file is on its own, so read `spec_helper.js` and inline only the hooks this file actually relies on: | Original behavior | Inline as | |---|---| | `setCtx(makeDataContext({}))` before, `getCtx()._reset()` + `clearCtx()` after | only if the SUT calls `getCtx()`; see the GraphQL note below before using `makeDataContext` | | `nock.disableNetConnect()` + `nock.enableNetConnect(/localhost/)` before, `nock.cleanAll()` + `nock.enableNetConnect()` after | any spec that uses `nock`, even when the original had no hooks (`cloud/api/create_instance.spec.ts`) | | `cache.remove()` before each | specs touching `lib/cache` | | `GracefulExit.resetForTesting()` | specs that start servers; it is a silent no-op unless `globalThis.IS_TEST = true` is set first (`server-base.spec.ts`) | | `process.env` cloned and restored after each | specs that set env; `vi.stubEnv` + `vi.unstubAllEnvs()` also works | | `mockElectron(mockery)` | `vi.mock('electron', ...)` only if the SUT's import graph reaches `electron`; most do not | | `sinon.restore()` after each | `vi.restoreAllMocks()`, with the caveat below | A spec with no hooks is fine. `automation/commands/key_press.spec.ts` needs none. ## Mocks that load the real module without telling you **Prove each mock intercepts.** Add a temporary `throw` or call count to the factory, run the spec, then remove it. A green run alone proves nothing. **`vi.mock` specifiers must be the string the importer wrote**, resolved from your spec's location for relative paths. `vi.mock('../../../../lib/cloud/network/fetch')` from the spec matches the SUT's `import ... from '../network/fetch'` because both resolve to the same file. A path that resolves elsewhere is ignored and the real module loads. **A bare `require()` inside the SUT is never intercepted by `vi.mock`**, with or without `server.deps.inline`. vite-node hands transformed modules a plain node `require`, and vitest's mocker only hooks ESM imports. Seed the CJS cache instead, which is what mockery did: ```ts const requireCjs = createRequire(import.meta.url) const morganPath = requireCjs.resolve('morgan') requireCjs.cache[morganPath] = { exports: morganMockFactory } as NodeModule // afterAll / afterEach: delete requireCjs.cache[morganPath] ``` The `exports` value is exactly what the SUT's `require()` returns, so a callable `module.exports = function` package (`morgan`, the `@cypress/*` preprocessors) is mocked with the function itself. Examples: `plugins/child/run_plugins.spec.ts`, `server-base.spec.ts`. **Partial mocks of node builtins must override `default` too.** A SUT that does `import fs from 'fs'` reads `fs.createReadStream` off the default export. Spread the actual module into both the named exports and a rebuilt `default`, or the real function runs: ```ts vi.mock('fs', async (importOriginal) => { const actual = await importOriginal<typeof import('fs')>() return { ...actual, createReadStream: stub, default: { ...actual, createReadStream: stub } } }) ``` Example: `cloud/api/put_protocol_artifact.spec.ts`. **A two-hop `proxyquire` chain usually collapses to one `vi.mock` of the leaf.** The second hop existed because proxyquire cannot reach a transitive dependency. `vi.mock` applies to the whole module graph, so mock `chrome-remote-interface` once with `vi.hoisted` state and import the SUT statically. Only reach for `vi.resetModules()` plus a dynamic import when the SUT holds module-level mutable state that mocha reset by re-requiring. Example: `browsers/cri-client.spec.ts`. **`vi.spyOn` on an ESM namespace import does work** for `lib/*.ts` modules, because vite's SSR transform rewrites named imports to live property reads. `vi.spyOn(ensureUrl, 'isListening')` patches what `server-base.ts` sees. It keeps the real implementation unless you add `.mockImplementation`, which `sinon.stub` did not. ## Assertions that flip meaning **sinon `calledWith` is a prefix match. vitest `toHaveBeenCalledWith` is exact-arity.** `client.send('Network.enable', opts, sessionId)` satisfies `calledWith('Network.enable')` but fails `toHaveBeenCalledWith('Network.enable')`. Worse, `not.toHaveBeenCalledWith('Fetch.disable')` passes vacuously against a 3-argument call. Either assert the full argument list or filter `mock.calls` by leading arguments. Examples: `expectCalledWith` in `browsers/cri-client.spec.ts`, `callsFor` in `server-base.spec.ts`. **`sinon.stub().withArgs(x)` returns the filtered stub.** When the original assigned that return value, the "filter" never gated anything and the stub answered every call. Porting it as an argument-conditional mock returns `undefined` for the real calls. Read what the mocha stub actually did before translating it. **`vi.restoreAllMocks()` wipes `vi.fn().mockReturnValue(...)`.** `sinon.restore()` left `.returns()` behavior on plain stubs alone. Shared fixtures need `vi.fn(() => value)` so the implementation survives restore. **Bluebird.** Server code still chains `.return()`, `.spread()`, `.tap()` on values it awaits. Where the SUT chains Bluebird methods on a mocked return, `mockResolvedValue` hands back a native promise and the SUT throws a confusing `not a function`. Return `Bluebird.resolve(...)` from that mock. Check the current SUT first; many call sites are plain `await` now and native promises are fine (`ensureUrl.isListening` in `server-base.ts` is one). **The transpiler is visible in stringified functions.** A test that asserts on `fn.toString()` of a handler defined in the spec reads esbuild's output under vitest, not ts-node's. Keep the assertion exact and update the literal; do not normalize it away. **Decomposed Unicode literals get normalized by editors.** `'é'` as `e` + U+0301 becomes U+00E9 on save. Byte-compare string literals against `git show HEAD:<mocha file>` when a spec contains combining characters (`automation/commands/key_press.spec.ts`). ## GraphQL, nock, timers **Importing `lib/server-base`, `lib/routes`, or `lib/makeDataContext` builds the GraphQL schema a second time** in the worker and `graphql` rejects the duplicate realm. Mock `@packages/data-context/graphql/makeGraphQLServer` with `graphqlWS` and `graphQLHTTP` stubs. Mocking that alone is not enough if you also call `makeDataContext`; use a minimal `DataContext` object carrying only the members the SUT touches (`routes.spec.ts`). **nock `delayConnection` with one large `advanceTimersByTimeAsync`** fires the delayed connection before the request timeout and consumes the wrong interceptor. Use `vi.runAllTimersAsync()` in rounds or step the clock in small increments. Also note that chaining `delayConnection(5000)` then `delayConnection(0)` on the same interceptor mutates one object, so both replies get delay 0; port that verbatim rather than "fixing" it. **Fake timers:** `vi.useFakeTimers()` / `vi.useRealTimers()` in hooks. `clock.tickAsync(n)` becomes `await vi.advanceTimersByTimeAsync(n)`. A non-async helper that the original `await`ed must stay non-async, or you add a microtask and change what mid-flight assertions observe. ## Source edits for testability Allowed only when obvious on sight: an optional parameter with a default that preserves behavior, exporting a function that already exists, extracting an inline expression into a named pure function in the same file. Not allowed: changing control flow, changing a signature callers depend on, touching more than one `lib/` file per batch, or anything you cannot justify in one sentence. Every source edit gets its own heading in the PR body. When in doubt, skip the spec; it stays in the pool. ## Verify ```bash yarn workspace @packages/server test-unit-vitest -- test/unit/<dir>/foo.spec.ts yarn workspace @packages/server test-unit # both runners, from a real terminal yarn lint --scope @packages/server yarn workspace @packages/server check-ts ``` Run the converted spec at least twice. Run `test-unit` from a terminal, not with stdout piped to a file: `util/tty_spec` needs `process.stdout.getWindowSize`, which only exists on a TTY, and fails on develop too when piped. Count `it(` in the mocha original and in the vitest file; they must match (loops that generate tests expand at runtime, so compare the static count). ## Style Single quotes, no semicolons, 2-space indent, trailing commas, blank line before `return`, `import type` for type-only imports, no `console`. Prefer no comment; when one is needed, say why in one or two lines and describe the present code, never how it used to work.
View on GitHub