Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/jest-dom-old-vitest.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions .github/workflows/vitest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
37 changes: 36 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 32 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Options> = {}): Plugin {
const filter = createFilter(options.include, options.exclude);

Expand Down Expand Up @@ -267,7 +294,11 @@ export default function solidPlugin(options: Partial<Options> = {}): 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,
];
}
}
}
Expand Down
50 changes: 50 additions & 0 deletions src/jest-dom.test.ts
Original file line number Diff line number Diff line change
@@ -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<string[]> {
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']);
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"src", "cypress"
],
"exclude": [
"**/*.spec.ts"
"**/*.spec.ts", "**/*.test.ts"
],
"compilerOptions": {
"target": "ESNext",
Expand Down
10 changes: 10 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -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',
},
});
Loading