Skip to content

fix(test): auto-inline packages that use expect.extend() to fix module instance splitting#1113

Merged
fengmk2 merged 4 commits intovoidzero-dev:mainfrom
kazupon:fix/897
Mar 23, 2026
Merged

fix(test): auto-inline packages that use expect.extend() to fix module instance splitting#1113
fengmk2 merged 4 commits intovoidzero-dev:mainfrom
kazupon:fix/897

Conversation

@kazupon
Copy link
Collaborator

@kazupon kazupon commented Mar 23, 2026

Summary

Fixes expect.extend() matchers from third-party packages (e.g., @testing-library/jest-dom) not being registered on the test runner's expect instance, causing errors like Invalid Chai property: toBeInTheDocument.

resolve #897

Root Cause

When third-party packages call require('vitest').expect.extend(matchers) internally, the npm override (vitest@voidzero-dev/vite-plus-test) causes Node.js to load a separate module instance with its own chai and expect.

Matchers are registered on this separate instance, not on the test runner's expect.

The root issue is the externalization behavior of vitest's module runner. By default, third-party packages in node_modules are externalized — loaded via Node.js native require/import.

When an externalized package calls require('vitest'), Node.js resolves it through the npm override, producing a different module instance with a separate chai.

Fix

Patch vitest's ModuleRunnerTransform plugin during re-packaging (build.ts) to automatically add known affected packages to server.deps.inline in the configResolved hook.

This forces the Vite module runner to process these packages through its transform pipeline instead of externalizing them to Node.js.

Normal flow (plain vitest — no npm override)

sequenceDiagram
    participant Runner as Test Runner
    participant JestDOM as @testing-library/jest-dom
    participant NodeJS as Node.js require()
    participant Vitest as vitest module

    Runner->>Vitest: import vitest (cached by Node.js)
    Runner->>Runner: createExpect() → globalExpect

    Note over JestDOM: setupFiles execution
    JestDOM->>NodeJS: require('vitest')
    NodeJS-->>JestDOM: Same vitest module (cached)
    JestDOM->>Vitest: expect.extend(matchers)
    Note over Vitest: OK: Matchers registered on<br/>the same chai instance

    Runner->>Runner: expect(el).toBeInTheDocument() OK:
Loading

Bug flow (vite-plus — npm override splits module instances)

sequenceDiagram
    participant Runner as Test Runner
    participant JestDOM as @testing-library/jest-dom
    participant NodeJS as Node.js require()
    participant VitestA as vitest (runner instance)
    participant VitestB as @voidzero-dev/vite-plus-test<br/>(override instance)

    Runner->>VitestA: import vitest
    Runner->>Runner: createExpect() with chai-A

    Note over JestDOM: setupFiles execution (externalized by default)
    JestDOM->>NodeJS: require('vitest')
    NodeJS->>NodeJS: npm override rewrites to<br/>@voidzero-dev/vite-plus-test
    NodeJS-->>JestDOM: Different module instance
    JestDOM->>VitestB: expect.extend(matchers)
    Note over VitestB: Matchers registered on chai-B

    Runner->>Runner: expect(el).toBeInTheDocument()
    Note over Runner: NG: chai-A has no matchers<br/>Invalid Chai property
Loading

Fix flow (with server.deps.inline patch)

sequenceDiagram
    participant Runner as Test Runner
    participant JestDOM as @testing-library/jest-dom
    participant ModRunner as Vite Module Runner
    participant Resolve as vitest:resolve-core
    participant Vitest as vitest (single instance)

    Runner->>Vitest: import vitest (cached)
    Runner->>Runner: createExpect() with chai

    Note over JestDOM: setupFiles execution (inlined by patch)
    JestDOM->>ModRunner: require('vitest')
    ModRunner->>ModRunner: Transform to __vite_ssr_import__
    ModRunner->>Resolve: resolve 'vitest'
    Resolve-->>ModRunner: dist/index.js (same file)
    ModRunner-->>JestDOM: Same module instance (cached)
    JestDOM->>Vitest: expect.extend(matchers)
    Note over Vitest: OK: Matchers registered on<br/>the same chai instance

    Runner->>Runner: expect(el).toBeInTheDocument() OK:
Loading

Auto-inlined packages

These three packages cover 99.5% of weekly downloads among all affected packages.

The list was determined by downloading npm tarballs of all known expect.extend using packages and inspecting their source code.

@netlify
Copy link

netlify bot commented Mar 23, 2026

Deploy Preview for viteplus-preview canceled.

Name Link
🔨 Latest commit 32c47cd
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-preview/deploys/69c13cea8909250008940372

@kazupon kazupon requested a review from fengmk2 March 23, 2026 09:34
@fengmk2
Copy link
Member

fengmk2 commented Mar 23, 2026

@kazupon I fork the reproduce repo to https://github.com/why-reproductions-are-required/vite-plus-jest-dom-repro, can you add it to e2e test case? Ensure that this validation logic will always exist in the future when we modify the patch solution or when vitest can support it later.

@kazupon
Copy link
Collaborator Author

kazupon commented Mar 23, 2026

@fengmk2
Ah, sorry.
I'll add to it as e2e test case!

@fengmk2 fengmk2 merged commit 15afbc0 into voidzero-dev:main Mar 23, 2026
51 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@testing-library/jest-dom/vitest setup fails with "Invalid Chai property: toBeInTheDocument"

2 participants