Testing (Vitest / Jest)
$debug is installed by the plugin's runtime: browser injection, SSR virtual import, dev-server global. A unit-test runner runs none of that, so source that calls $debug(...), especially at module-evaluation time, throws ReferenceError: $debug is not defined. A no-op stub fixes it: it behaves exactly like a stripped production build.
Vitest: automatic (nothing to add)
If mtDebug() is in the Vite config Vitest reads (the usual case: Vitest merges your vite.config), the plugin detects the test run and registers the stub for you as the first setupFiles entry. Just run your tests: $debug is defined. Opt out with mtDebug({ test: false }).
How it works
This is not a Vite or Vitest plugin hook you add: the plugin you already have contributes a setup file, a global installed before your tests import anything. Automatic registration of the no-op stub covers the default $debug only. For a custom globalName, set test: false and register the stub yourself, see Programmatic stub. With test: 'log' this does not apply: the plugin generates the setup module from your resolved configuration, so a custom global works there with no extra configuration.
The test option replaces testStub and testLog from 0.6. Both still work, with a warning, until 2.0: see Renamed options.
Explicit: Jest, or a Vitest config without the plugin
Add the shipped setup module to setupFiles, first, before any other setup file that imports your source:
// vitest.config.ts (or jest config)
export default defineConfig({
test: {
setupFiles: ["@mars-tech/vite-debug-plugin/testing"],
},
});Importing the module installs a no-op $debug: the callable plus the full MTDebug surface, all 14 methods.
Manual stub (no dependency)
If you would rather not import the package in tests, write the stub by hand. It must be the first setupFiles entry and contain no imports. ES modules evaluate a file's imports before its body, so a stub placed after imports (or in a file that transitively imports modules using $debug) runs too late and the error is still thrown:
// tests/debug-stub.js (setupFiles: ["./tests/debug-stub.js", ...]) — NO imports
const noop = () => {};
globalThis.$debug = Object.assign(noop, {
ns: () => noop,
enable: noop,
disable: noop,
setLevel: noop,
setPrefixFilter: noop,
assert: noop,
time: noop,
timeEnd: noop,
count: noop,
group: noop,
groupCollapsed: noop,
groupEnd: noop,
table: noop,
dir: noop,
});Seeing $debug output in tests
The stub is a no-op: it stops tests from throwing, but swallows the output. To actually see logs during a test run, from your test files and from the components or library code they exercise, set one option:
// vite.config.ts
mtDebug({ test: "log" });Under Vitest the plugin swaps the no-op for a real, formatted console $debug. It is config-aware: the plugin generates its setup module from your resolved prefix, globalName, levels, prefixFilter and redact / redactDefaults, so test output matches your dev output and is masked the same way, CI logs included. Each Vitest project gets its own module, so projects never share settings. The logger includes variadic payloads and printf substitution and the payload rendering of the browser and SSR runtimes: it is the same code path. Nothing else to add. With enabled: false, test: 'log' falls back to the no-op stub.
Filter with the same environment controls as the runtime:
MT_DEBUG=cart* MT_DEBUG_LEVEL=info vitestA line logged by a library built with brandPrefix keeps its own prefix, so app and library logs stay distinguishable under test:
XYZ Info [A/UsesB] - UsesB mounted ← your app (prefix XYZ)
ABC Info [cart/action] - cart.add(…) ← library B (prefix ABC)Mock the library (vi.mock('…')) and its lines simply stop appearing: only your app's logs remain. See Branding library logs with brandPrefix.
Inline output
Vitest groups console output under each test by default. Pass --disableConsoleIntercept (or --reporter=verbose) to see the lines inline.
Calling $debug in test files needs no runtime change: the stub or logger defines it. For your editor and CI, TypeScript picks up the global from the /// <reference types="@mars-tech/vite-debug-plugin/client" /> you already added (as long as your test files are in a tsconfig that includes it, see Enable the global type), and the shipped /eslint flat config declares $debug so no-undef stays quiet (see Tell ESLint about the global).
String redact entries in tests
The generated setup module carries your RegExp entries and the redactDefaults patterns, and never your string entries: a module's text can end up in Vitest's temporary files and module cache. The logger reads string entries from the process that loads your config instead. They reach:
threadsandvmThreadsworkers, on any Vitest version;forksandvmForksworkers from Vitest 4, which the plugin provides them to through Vitest'sprovide/inject.
Where they cannot reach, the logger does not print what it cannot mask: $debug prints nothing and says so once. That is the case for:
forksandvmForkson Vitest 3. Upgrade Vitest, usepool: "threads", or describe each such value with aRegExpfor its shape.- A project that runs in a browser (Vitest browser mode,
--browserincluded). Vitest copies the test page's console to its terminal, and string entries are never sent to a browser, so with string entries configured that project's$debugprints nothing and the plugin warns. With onlyRegExpentries andredactDefaults, it logs, masked by those, and hands the console no live value. To see output there, replace each string entry with aRegExpfor the value's shape.
When several projects run in one process, a logger that reads the other projects' rules applies them too, beside its own: one project's string entries can also mask another project's output. See Where string entries cannot reach.
A logger in your own setup file
Without the plugin (Jest, or a Vitest config that does not load it), install the logger from your own first setup file. redact and redactDefaults take the same values as the plugin options:
// tests/setup.ts (setupFiles: ["./tests/setup.ts"])
import { installMtDebugConsole } from "@mars-tech/vite-debug-plugin/testing";
installMtDebugConsole({
prefix: "APP",
redactDefaults: true,
redact: [/sk_live_[A-Za-z0-9]+/],
});Under the plugin with test: 'log', do not also list @mars-tech/vite-debug-plugin/testing/log in setupFiles: the generated module already installs the configured logger. A setup file of your own runs after it. If that file imports /testing/log, the configured logger stays in place; if it calls installMtDebugConsole(…), the new logger takes its options and still masks with your project's redact / redactDefaults first. Neither can switch your project's masking off.
A logger you install yourself under test: 'stub' or test: false reads your project's string entries the same way, and prints nothing where it cannot find them. A threads worker always has them; on the forks pools, use test: 'log' (Vitest 4) or pool: "threads" instead.
Programmatic stub: custom globalName or spies
Use the programmatic form from your own setup file:
// tests/setup.ts (setupFiles: ["./tests/setup.ts"])
import { installMtDebugStub } from "@mars-tech/vite-debug-plugin/testing";
import { vi } from "vitest";
installMtDebugStub({ globalName: "$log", impl: vi.fn() }); // spy on every callglobalName must match the plugin's globalName option (default "$debug"). impl receives every base $debug(...) call, including calls made through a logger returned by ns(); it defaults to a no-op. The helper methods (time, count, group…) stay no-ops. The same module also exports installMtDebugConsole, the config-aware console logger shown in A logger in your own setup file. See Subpath exports.