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.

معلومات المصدر

المستودع
cypress-io/cypress
آخر نشاط في المصدر
١٧ سبتمبر ٢٠٢٦ في ١٧:٠٠
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٥١٬٠٢٦
التفرعات
٣٬٦٤٥

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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.
عرض على GitHub