Skip to content
Merged
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
15 changes: 15 additions & 0 deletions e2e/setup/fixtures/package-name/rstest.esm.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { join } from 'node:path';
import { defineConfig } from '@rstest/core';

import fse from 'fs-extra';

fse.copySync(
join(__dirname, './test-setup-esm-fixtures'),
join(__dirname, './node_modules/test-setup-esm'),
);

export default defineConfig({
passWithNoTests: true,
setupFiles: ['test-setup-esm'],
exclude: ['**/node_modules/**', '**/dist/**'],
});
15 changes: 15 additions & 0 deletions e2e/setup/fixtures/package-name/rstest.fileProtocol.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { join } from 'node:path';
import { defineConfig } from '@rstest/core';

import fse from 'fs-extra';

fse.copySync(
join(__dirname, './test-setup-fixtures'),
join(__dirname, './node_modules/test-setup'),
);

export default defineConfig({
passWithNoTests: true,
setupFiles: [import.meta.resolve('test-setup')],
exclude: ['**/node_modules/**', '**/dist/**'],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
process.env.RETEST_SETUP_FLAG = '1';
process.env.NODE_ENV = 'rstest:production';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"private": true,
"name": "@rstest/tests-setup-esm",
"exports": {
".": {
"import": "./index.js"
}
},
"version": "1.0.0"
}
26 changes: 26 additions & 0 deletions e2e/setup/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,30 @@ describe('test setup file', async () => {
});
await expectExecSuccess();
});

it('should resolve setup file correctly when setupFiles path with file protocol', async () => {
const { expectExecSuccess } = await runRstestCli({
command: 'rstest',
args: ['run', '-c', 'rstest.fileProtocol.config.ts'],
options: {
nodeOptions: {
cwd: join(__dirname, 'fixtures/package-name'),
},
},
});
await expectExecSuccess();
});

it('should resolve setup file correctly when setupFile is pure es module', async () => {
const { expectExecSuccess } = await runRstestCli({
command: 'rstest',
args: ['run', '-c', 'rstest.esm.config.ts'],
options: {
nodeOptions: {
cwd: join(__dirname, 'fixtures/package-name'),
},
},
});
await expectExecSuccess();
});
});
2 changes: 1 addition & 1 deletion packages/core/src/core/listTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type {
import {
bgColor,
color,
getSetupFiles,
getTaskNameWithPrefix,
getTestEntries,
logger,
Expand All @@ -28,6 +27,7 @@ const collectTests = async ({
context: RstestContext;
globTestSourceEntries: (name: string) => Promise<Record<string, string>>;
}) => {
const { getSetupFiles } = await import('../utils/getSetupFiles');
const setupFiles = Object.fromEntries(
context.projects.map((project) => {
const {
Expand Down
10 changes: 3 additions & 7 deletions packages/core/src/core/runTests.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { createCoverageProvider } from '../coverage';
import { createPool } from '../pool';
import type { EntryInfo } from '../types';
import {
clearScreen,
color,
getSetupFiles,
getTestEntries,
logger,
} from '../utils';
import { clearScreen, color, getTestEntries, logger } from '../utils';
import { isCliShortcutsEnabled, setupCliShortcuts } from './cliShortcuts';
import { runGlobalSetup, runGlobalTeardown } from './globalSetup';
import { createRsbuildServer, prepareRsbuild } from './rsbuild';
Expand Down Expand Up @@ -54,6 +48,8 @@ export async function runTests(context: Rstest): Promise<void> {
return entries;
};

const { getSetupFiles } = await import('../utils/getSetupFiles');

const setupFiles = Object.fromEntries(
context.projects.map((project) => {
const {
Expand Down
50 changes: 50 additions & 0 deletions packages/core/src/utils/getSetupFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { rspack } from '@rsbuild/core';
import pathe from 'pathe';
import { color, getAbsolutePath } from './helper';
import { formatTestEntryName } from './testFiles';

const tryResolve = (request: string, rootPath: string) => {
const { resolver } = rspack.experiments;
const esmFirstResolver = new resolver.ResolverFactory({
conditionNames: ['node', 'import', 'require'],
});
const { path: resolvedPath } = esmFirstResolver.sync(rootPath, request);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value destructuring assumes resolver.sync always returns an object with a 'path' property. If the resolver returns undefined, null, or an object without a 'path' property, this will cause a runtime error. Add validation to check if the resolved result is valid before destructuring.

Copilot uses AI. Check for mistakes.
return resolvedPath;
};

export const getSetupFiles = (
setups: string[],
rootPath: string,
): Record<string, string> => {
if (!setups.length) {
return {};
}
return Object.fromEntries(
setups.map((filePath) => {
const setupFile = filePath.startsWith('file://')
? fileURLToPath(filePath)
: filePath;
const setupFilePath = getAbsolutePath(rootPath, setupFile);
try {
if (!existsSync(setupFilePath)) {
let errorMessage = `Setup file ${color.red(setupFile)} not found`;
if (setupFilePath !== setupFile) {
errorMessage += color.gray(` (resolved path: ${setupFilePath})`);
}
throw errorMessage;
}
const relativePath = pathe.relative(rootPath, setupFilePath);
return [formatTestEntryName(relativePath), setupFilePath];
} catch (err) {
const resolvedPath = tryResolve(setupFile, rootPath);
// support use package name as setupFiles value
if (resolvedPath) {
return [formatTestEntryName(setupFile), resolvedPath];
}
throw err;
}
}),
);
};
45 changes: 1 addition & 44 deletions packages/core/src/utils/testFiles.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { existsSync } from 'node:fs';
import fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import pathe from 'pathe';
import { glob } from 'tinyglobby';
import type { Project } from '../types';
import { castArray, color, getAbsolutePath, parsePosix } from './helper';
import { castArray, color, parsePosix } from './helper';

export const filterFiles = (
testFiles: string[],
Expand Down Expand Up @@ -126,47 +124,6 @@ export const getTestEntries = async ({
);
};

const tryResolve = (request: string, rootPath: string) => {
try {
const require = createRequire(rootPath);
return require.resolve(request, { paths: [rootPath] });
} catch (_err) {
return undefined;
}
};

export const getSetupFiles = (
setups: string[],
rootPath: string,
): Record<string, string> => {
if (!setups.length) {
return {};
}
return Object.fromEntries(
setups.map((setupFile) => {
const setupFilePath = getAbsolutePath(rootPath, setupFile);
try {
if (!existsSync(setupFilePath)) {
let errorMessage = `Setup file ${color.red(setupFile)} not found`;
if (setupFilePath !== setupFile) {
errorMessage += color.gray(` (resolved path: ${setupFilePath})`);
}
throw errorMessage;
}
const relativePath = pathe.relative(rootPath, setupFilePath);
return [formatTestEntryName(relativePath), setupFilePath];
} catch (err) {
const resolvedPath = tryResolve(setupFile, rootPath);
// support use package name as setupFiles value
if (resolvedPath) {
return [formatTestEntryName(setupFile), resolvedPath];
}
throw err;
}
}),
);
};

export const prettyTestPath = (testPath: string): string => {
const { dir, base } = parsePosix(testPath);

Expand Down
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

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

Loading