From 37b56ede60a56418aaafe0fb4450a09a4ee7c1b0 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Mon, 19 Oct 2026 11:33:53 +0800 Subject: [PATCH 1/2] test: add vitest unit tests for the plugin - Add a root vitest config and a test:unit script, matching the next branch. - Run the unit tests in the vitest workflow. - Exclude test files from the declaration build. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/vitest.yml | 3 +++ package.json | 6 ++++-- pnpm-lock.yaml | 37 +++++++++++++++++++++++++++++++++++- tsconfig.json | 2 +- vitest.config.ts | 10 ++++++++++ 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 vitest.config.ts diff --git a/.github/workflows/vitest.yml b/.github/workflows/vitest.yml index fc062d92..aad075ac 100644 --- a/.github/workflows/vitest.yml +++ b/.github/workflows/vitest.yml @@ -27,6 +27,9 @@ jobs: - name: Install project dependencies run: pnpm i + - name: Run unit tests + run: pnpm run test:unit + - name: Install playwright browser working-directory: examples/vite-6 run: pnpm exec playwright install chromium diff --git a/package.json b/package.json index a2662f89..cba7d046 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "prepublishOnly": "pnpm build", "release": "pnpm build && changeset publish", "check": "package-check", - "test": "node scripts/test-examples.ts" + "test": "node scripts/test-examples.ts", + "test:unit": "vitest run" }, "repository": { "type": "git", @@ -68,7 +69,8 @@ "rollup-plugin-cleaner": "^1.0.0", "solid-js": "^1.9.3", "typescript": "^5.2.2", - "vite": "^6.0.0" + "vite": "^6.0.0", + "vitest": "^3.0.7" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.0.0 || ^7.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eed12c18..23823a84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ importers: vite: specifier: ^6.0.0 version: 6.0.9(@types/node@18.19.66)(lightningcss@1.32.0) + vitest: + specifier: ^3.0.7 + version: 3.0.7(@types/node@18.19.66)(@vitest/browser@3.0.7)(jsdom@26.0.0(supports-color@8.1.1))(lightningcss@1.32.0)(msw@2.7.3(@types/node@18.19.66)(typescript@5.7.2))(supports-color@8.1.1) examples/vite-3: dependencies: @@ -5968,6 +5971,28 @@ snapshots: '@types/node': 18.19.66 optional: true + '@vitest/browser@3.0.7(@types/node@18.19.66)(playwright@1.50.1)(typescript@5.7.2)(vite@6.0.9(@types/node@18.19.66)(lightningcss@1.32.0))(vitest@3.0.7)': + dependencies: + '@testing-library/dom': 10.4.0 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) + '@vitest/mocker': 3.0.7(msw@2.7.3(@types/node@18.19.66)(typescript@5.7.2))(vite@6.0.9(@types/node@18.19.66)(lightningcss@1.32.0)) + '@vitest/utils': 3.0.7 + magic-string: 0.30.17 + msw: 2.7.3(@types/node@18.19.66)(typescript@5.7.2) + sirv: 3.0.1 + tinyrainbow: 2.0.0 + vitest: 3.0.7(@types/node@18.19.66)(@vitest/browser@3.0.7)(jsdom@26.0.0(supports-color@8.1.1))(lightningcss@1.32.0)(msw@2.7.3(@types/node@18.19.66)(typescript@5.7.2))(supports-color@8.1.1) + ws: 8.18.1 + optionalDependencies: + playwright: 1.50.1 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - typescript + - utf-8-validate + - vite + optional: true + '@vitest/browser@3.0.7(@types/node@18.19.66)(playwright@1.50.1)(typescript@5.7.2)(vite@6.2.0(@types/node@18.19.66)(lightningcss@1.32.0))(vitest@3.0.7)': dependencies: '@testing-library/dom': 10.4.0 @@ -6038,6 +6063,16 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/mocker@3.0.7(msw@2.7.3(@types/node@18.19.66)(typescript@5.7.2))(vite@6.0.9(@types/node@18.19.66)(lightningcss@1.32.0))': + dependencies: + '@vitest/spy': 3.0.7 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + msw: 2.7.3(@types/node@18.19.66)(typescript@5.7.2) + vite: 6.0.9(@types/node@18.19.66)(lightningcss@1.32.0) + optional: true + '@vitest/mocker@3.0.7(msw@2.7.3(@types/node@18.19.66)(typescript@5.7.2))(vite@6.2.0(@types/node@18.19.66)(lightningcss@1.32.0))': dependencies: '@vitest/spy': 3.0.7 @@ -8040,7 +8075,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 18.19.66 - '@vitest/browser': 3.0.7(@types/node@18.19.66)(playwright@1.50.1)(typescript@5.7.2)(vite@6.2.0(@types/node@18.19.66)(lightningcss@1.32.0))(vitest@3.0.7) + '@vitest/browser': 3.0.7(@types/node@18.19.66)(playwright@1.50.1)(typescript@5.7.2)(vite@6.0.9(@types/node@18.19.66)(lightningcss@1.32.0))(vitest@3.0.7) jsdom: 26.0.0(supports-color@8.1.1) transitivePeerDependencies: - jiti diff --git a/tsconfig.json b/tsconfig.json index 70191cbf..9c12304d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "src", "cypress" ], "exclude": [ - "**/*.spec.ts" + "**/*.spec.ts", "**/*.test.ts" ], "compilerOptions": { "target": "ESNext", diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..2f1a903a --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +// The plugin's own unit tests (`pnpm test:unit`). The example apps under +// examples/* carry their own vitest configs and run through `pnpm test`. +export default defineConfig({ + test: { + include: ['src/**/*.test.ts'], + environment: 'node', + }, +}); From a7ec25f1aca35464cef348a677855bca3371cc19 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Mon, 19 Oct 2026 11:46:59 +0800 Subject: [PATCH 2/2] fix: load jest-dom ESM setup file on vitest before 1.0 - Vitest before 1.0 resolves setupFiles with require.resolve (local-pkg 0.4), which picks jest-dom's CommonJS vitest entry. That file reads require('vitest').expect, which is undefined, and throws "Cannot read properties of undefined (reading 'extend')". - When the project's vitest is older than 1.0, inject the absolute path of jest-dom's ESM vitest entry instead of the bare specifier. - Vitest 1.0+ keeps the bare specifier. - Add tests for both vitest ranges. Fixes #141 Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/jest-dom-old-vitest.md | 5 ++++ src/index.ts | 33 +++++++++++++++++++- src/jest-dom.test.ts | 50 +++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 .changeset/jest-dom-old-vitest.md create mode 100644 src/jest-dom.test.ts diff --git a/.changeset/jest-dom-old-vitest.md b/.changeset/jest-dom-old-vitest.md new file mode 100644 index 00000000..676cd5a2 --- /dev/null +++ b/.changeset/jest-dom-old-vitest.md @@ -0,0 +1,5 @@ +--- +'vite-plugin-solid': patch +--- + +Fix `TypeError: Cannot read properties of undefined (reading 'extend')` in tests on Vitest before 1.0 with `@testing-library/jest-dom` 6 or later installed (#141). The plugin now injects jest-dom's ESM `vitest` entry as an absolute path for those Vitest versions, because they load the CommonJS entry from a bare specifier. diff --git a/src/index.ts b/src/index.ts index 82770246..bd8b2fd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import solid from 'babel-preset-solid'; import { readFileSync } from 'fs'; import { mergeAndConcat } from 'merge-anything'; import { createRequire } from 'module'; +import path from 'path'; import solidRefresh from 'solid-refresh/babel'; import type { Alias, AliasOptions, FilterPattern, Plugin } from 'vite'; import { createFilter, version } from 'vite'; @@ -206,6 +207,32 @@ function getJestDomExport(setupFiles: string[]) { ); } +function readPackageJson(specifier: string, requires: NodeRequire[]) { + for (const req of requires) { + try { + const file = req.resolve(`${specifier}/package.json`); + return { dir: path.dirname(file), pkg: JSON.parse(readFileSync(file, 'utf-8')) }; + } catch (e) {} + } + return undefined; +} + +/** + * Vitest before 1.0 resolves `setupFiles` with `require.resolve`, which picks + * jest-dom's CommonJS `vitest` entry. In that file `require('vitest').expect` + * is undefined, so it throws "Cannot read properties of undefined (reading + * 'extend')". For those versions, return the path of jest-dom's ESM entry. #141 + */ +function getJestDomVitestEsmPath(root: string): string | undefined { + const rootRequire = createRequire(path.join(path.resolve(root), 'package.json')); + const vitest = readPackageJson('vitest', [rootRequire, require]); + if (!vitest || +String(vitest.pkg.version).split('.')[0] >= 1) return undefined; + const jestDom = readPackageJson('@testing-library/jest-dom', [require, rootRequire]); + const entry = jestDom?.pkg.exports?.['./vitest']?.import; + const file = typeof entry === 'string' ? entry : entry?.default; + return typeof file === 'string' ? path.join(jestDom.dir, file) : undefined; +} + export default function solidPlugin(options: Partial = {}): Plugin { const filter = createFilter(options.include, options.exclude); @@ -267,7 +294,11 @@ export default function solidPlugin(options: Partial = {}): Plugin { // https://main.vitest.dev/guide/browser/assertion-api.html#assertion-api const jestDomImport = getJestDomExport(userSetupFiles); if (jestDomImport) { - test.setupFiles = [jestDomImport]; + test.setupFiles = [ + (jestDomImport === '@testing-library/jest-dom/vitest' && + getJestDomVitestEsmPath(userConfig.root || process.cwd())) || + jestDomImport, + ]; } } } diff --git a/src/jest-dom.test.ts b/src/jest-dom.test.ts new file mode 100644 index 00000000..6fb4860c --- /dev/null +++ b/src/jest-dom.test.ts @@ -0,0 +1,50 @@ +import { existsSync } from 'node:fs'; +import { mkdtemp, mkdir, realpath, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import type { ConfigEnv, UserConfig } from 'vite'; +import { afterAll, describe, expect, it } from 'vitest'; +import solidPlugin from './index.js'; + +const roots: string[] = []; + +// A project root whose installed vitest has the given version. +async function projectWithVitest(version: string) { + const root = await realpath(await mkdtemp(path.join(tmpdir(), 'solid-jest-dom-'))); + roots.push(root); + await writeFile(path.join(root, 'package.json'), JSON.stringify({ name: 'app', type: 'module' })); + await mkdir(path.join(root, 'node_modules/vitest'), { recursive: true }); + await writeFile( + path.join(root, 'node_modules/vitest/package.json'), + JSON.stringify({ name: 'vitest', version }), + ); + return root; +} + +async function setupFilesFor(root: string): Promise { + const plugin = solidPlugin(); + const userConfig: UserConfig = { root, mode: 'test' }; + const env: ConfigEnv = { command: 'serve', mode: 'test' }; + const config = await (plugin.config as Function).call({}, userConfig, env); + return config.test?.setupFiles; +} + +afterAll(async () => { + await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true }))); +}); + +describe('jest-dom setup file', () => { + // #141: vitest < 1 loaded jest-dom's CommonJS entry, which threw + // "Cannot read properties of undefined (reading 'extend')". + it('injects the ESM entry path for vitest before 1.0', async () => { + const [setupFile] = await setupFilesFor(await projectWithVitest('0.34.6')); + expect(setupFile).toMatch(/@testing-library[\\/]jest-dom[\\/]dist[\\/]vitest\.mjs$/); + expect(path.isAbsolute(setupFile)).toBe(true); + expect(existsSync(setupFile)).toBe(true); + }); + + it('keeps the bare specifier for vitest 1.0 and later', async () => { + const setupFiles = await setupFilesFor(await projectWithVitest('1.0.0')); + expect(setupFiles).toEqual(['@testing-library/jest-dom/vitest']); + }); +});