Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4425446
feat(core): support custom test environments
9aoy May 20, 2026
1fa7ba8
fix: align custom environment resolution and docs
9aoy May 20, 2026
21fc374
fix: custom environments, transform mode, and review feedback
9aoy May 20, 2026
d76a0cd
style: format test files
9aoy May 20, 2026
019d878
fix: stabilize custom test environment resolution
9aoy May 20, 2026
3ff7c7d
fix: lint
9aoy May 21, 2026
c5f69a5
fix: expose package environment marker during setup
9aoy May 21, 2026
ea7d542
docs: update custom-environment-vs-setup-files
9aoy May 21, 2026
ca4c2b4
docs: add Test environment types
9aoy May 21, 2026
6f32f28
docs: add Browser Mode tip
9aoy May 21, 2026
e3f6a20
fix(core): refine custom environment loading
9aoy May 21, 2026
f37d878
Merge remote-tracking branch 'origin/main' into feat/custom-test-envi…
9aoy May 21, 2026
4223d16
fix(core): resolve test environment before worker run
9aoy May 21, 2026
bb8b3d0
chore: merge main into custom test environment
9aoy May 22, 2026
2c18728
chore: format Vitest migration docs
9aoy May 22, 2026
91a5e67
fix(core): continue environment fallback after import errors
9aoy May 22, 2026
6a72fec
fix(core): surface custom environment import errors
9aoy May 22, 2026
13ce612
fix(core): report fallback environment import errors
9aoy May 25, 2026
db997ce
docs: update custom environment guide
9aoy May 25, 2026
be31c9c
docs: clarify custom environment migration
9aoy May 25, 2026
8be2ee7
fix: resolve custom environment packages from project roots
9aoy May 25, 2026
33cb79b
fix: defer custom environment imports to worker
9aoy May 26, 2026
bded029
refactor: keep environment loading in worker
9aoy May 26, 2026
e17b033
fix: align environment package fallback with node resolution
9aoy May 26, 2026
f5e418a
fix: resolve environment packages with rspack resolver
9aoy May 26, 2026
51ca46e
fix: avoid require condition for environment imports
9aoy May 26, 2026
348cb4f
docs: move custom environment migration guide
9aoy May 27, 2026
470b0f5
chore: merge main and update custom env target
9aoy Jun 11, 2026
caad7f5
Merge remote-tracking branch 'origin/main' into 9aoy/pr-1287-resolve-…
9aoy Jun 23, 2026
69b1295
fix(core): type environment comments
9aoy Jun 23, 2026
fdeeb59
fix: address environment review feedback
9aoy Jun 23, 2026
ff4dc2b
fix: keep watch plugin snapshots stable
9aoy Jun 23, 2026
c88dc86
fix: refresh custom environment watch reruns
9aoy Jun 23, 2026
b6d4c1a
fix: handle optional watch changed files
9aoy Jun 23, 2026
3467532
fix: refresh custom environment workers in watch
9aoy Jun 24, 2026
aabbe8c
fix: watch custom environment dependencies
9aoy Jun 24, 2026
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
34 changes: 34 additions & 0 deletions e2e/dom/customBuiltinNodeEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import fs from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from '@rstest/core';
import { runCli } from './utils';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const teardownFile = join(
__dirname,
'fixtures',
'test',
'custom-node-environment.teardown.txt',
);

describe('custom builtin node environment', () => {
it('should load a local custom environment that extends builtin node', async () => {
fs.rmSync(teardownFile, { force: true });

const { expectExecSuccess } = await runCli(
'test/customNodeEnvironment',
undefined,
{
args: ['--config', 'rstest.customNodeEnvironment.config.mts'],
},
);

await expectExecSuccess();

expect(fs.readFileSync(teardownFile, 'utf8')).toBe('node-marker');

fs.rmSync(teardownFile, { force: true });
});
});
34 changes: 34 additions & 0 deletions e2e/dom/customEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import fs from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from '@rstest/core';
import { runCli } from './utils';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const teardownFile = join(
__dirname,
'fixtures',
'test',
'custom-environment.teardown.txt',
);

describe('custom environment', () => {
it('should load a local custom environment and extend builtin environments', async () => {
fs.rmSync(teardownFile, { force: true });

const { expectExecSuccess } = await runCli(
'test/customEnvironment',
undefined,
{
args: ['--config', 'rstest.customEnvironment.config.mts'],
},
);

await expectExecSuccess();

expect(fs.readFileSync(teardownFile, 'utf8')).toBe('custom-marker');

fs.rmSync(teardownFile, { force: true });
});
});
3 changes: 2 additions & 1 deletion e2e/dom/fixtures/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
},
"dependencies": {
"react": "^19.2.7",
"react-dom": "^19.2.7"
"react-dom": "^19.2.7",
"rstest-environment-package-marker": "workspace:*"
},
"devDependencies": {
"@rsbuild/core": "~2.1.0-beta.0",
Expand Down
28 changes: 28 additions & 0 deletions e2e/dom/fixtures/rstest-environment-package-marker/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const teardownFile = join(
__dirname,
'..',
'test',
'package-environment.teardown.txt',
);

/** @type {import('@rstest/core').TestEnvironment<typeof globalThis, { marker: string }>} */
const environment = {
name: 'package-marker',
async setup(global, options) {
global.__PACKAGE_ENV_MARKER__ = options.marker;

return {
async teardown() {
delete global.__PACKAGE_ENV_MARKER__;
await writeFile(teardownFile, options.marker, 'utf8');
},
};
},
};

export default environment;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "rstest-environment-package-marker",
"private": true,
"type": "module",
"exports": {
"import": "./index.mjs"
}
}
17 changes: 17 additions & 0 deletions e2e/dom/fixtures/rstest.customEnvironment.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineConfig, type RstestConfig } from '@rstest/core';
import rsbuildConfig from './rsbuild.config';

export default defineConfig({
...(rsbuildConfig as RstestConfig),
setupFiles: ['./test/setup.ts'],
testEnvironment: {
name: './test/customEnvironment.mjs',
target: 'web',
options: {
marker: 'custom-marker',
jsdom: {
url: 'https://custom-env.example/',
},
},
},
});
12 changes: 12 additions & 0 deletions e2e/dom/fixtures/rstest.customNodeEnvironment.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig, type RstestConfig } from '@rstest/core';
import rsbuildConfig from './rsbuild.config';

export default defineConfig({
...(rsbuildConfig as RstestConfig),
testEnvironment: {
name: './test/customNodeEnvironment.mjs',
options: {
marker: 'node-marker',
},
},
});
12 changes: 12 additions & 0 deletions e2e/dom/fixtures/rstest.packageEnvironment.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig, type RstestConfig } from '@rstest/core';
import rsbuildConfig from './rsbuild.config';

export default defineConfig({
...(rsbuildConfig as RstestConfig),
testEnvironment: {
name: 'package-marker',
Comment thread
9aoy marked this conversation as resolved.
options: {
marker: 'package-marker',
},
},
});
28 changes: 28 additions & 0 deletions e2e/dom/fixtures/test/customEnvironment.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { builtinEnvironments } from '@rstest/core';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const teardownFile = path.join(__dirname, 'custom-environment.teardown.txt');

export default {
name: 'custom-jsdom',
async setup(global, options) {
const jsdomEnvironment = await builtinEnvironments.jsdom.setup(
global,
options.jsdom ?? {},
);

global.__CUSTOM_ENV_MARKER__ = options.marker;

return {
async teardown() {
delete global.__CUSTOM_ENV_MARKER__;
fs.writeFileSync(teardownFile, String(options.marker));
await jsdomEnvironment.teardown();
},
};
},
};
8 changes: 8 additions & 0 deletions e2e/dom/fixtures/test/customEnvironment.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { expect, test } from '@rstest/core';

test('custom environment can extend builtin jsdom', () => {
expect(window.location.href).toBe('https://custom-env.example/');
expect(
(globalThis as { __CUSTOM_ENV_MARKER__?: string }).__CUSTOM_ENV_MARKER__,
).toBe('custom-marker');
});
31 changes: 31 additions & 0 deletions e2e/dom/fixtures/test/customNodeEnvironment.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { builtinEnvironments } from '@rstest/core';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const teardownFile = path.join(
__dirname,
'custom-node-environment.teardown.txt',
);

export default {
name: 'custom-node',
async setup(global, options) {
const nodeEnvironment = await builtinEnvironments.node.setup(
global,
options,
);

global.__CUSTOM_NODE_ENV_MARKER__ = options.marker;

return {
async teardown() {
delete global.__CUSTOM_NODE_ENV_MARKER__;
fs.writeFileSync(teardownFile, String(options.marker));
await nodeEnvironment.teardown();
},
};
},
};
10 changes: 10 additions & 0 deletions e2e/dom/fixtures/test/customNodeEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { expect, test } from '@rstest/core';

test('custom environment can extend builtin node', () => {
expect(
(globalThis as { __CUSTOM_NODE_ENV_MARKER__?: string })
.__CUSTOM_NODE_ENV_MARKER__,
).toBe('node-marker');
expect('window' in globalThis).toBe(false);
expect(typeof process.cwd()).toBe('string');
});
8 changes: 8 additions & 0 deletions e2e/dom/fixtures/test/packageEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { expect, test } from '@rstest/core';

test('custom environment can load from rstest-environment-* package fallback', () => {
expect(
(globalThis as { __PACKAGE_ENV_MARKER__?: string }).__PACKAGE_ENV_MARKER__,
).toBe('package-marker');
expect('window' in globalThis).toBe(false);
});
14 changes: 13 additions & 1 deletion e2e/dom/handledError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ describe('custom-environment', () => {
);
await expectExecFailed();

expectStderrLog(/Unknown test environment: custom-environment/);
expectStderrLog(/Failed to resolve testEnvironment "custom-environment"/);
});
});

describe('prototype-environment-name', () => {
it('should throw an explicit resolution error for prototype property names', async () => {
const { expectExecFailed, expectStderrLog } = await runCli(
filters,
'toString',
);
await expectExecFailed();

expectStderrLog(/Failed to resolve testEnvironment "toString"/);
});
});
34 changes: 34 additions & 0 deletions e2e/dom/packageEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import fs from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from '@rstest/core';
import { runCli } from './utils';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const teardownFile = join(
__dirname,
'fixtures',
'test',
'package-environment.teardown.txt',
);

describe('package environment fallback', () => {
it('should resolve rstest-environment-* packages from testEnvironment.name', async () => {
fs.rmSync(teardownFile, { force: true });

const { expectExecSuccess } = await runCli(
'test/packageEnvironment',
undefined,
{
args: ['--config', 'rstest.packageEnvironment.config.mts'],
},
);

await expectExecSuccess();

expect(fs.readFileSync(teardownFile, 'utf8')).toBe('package-marker');

fs.rmSync(teardownFile, { force: true });
});
});
84 changes: 84 additions & 0 deletions e2e/watch/customEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it, rs } from '@rstest/core';
import { prepareFixtures, runRstestCli } from '../scripts';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

rs.setConfig({
retry: 3,
});

describe.skipIf(process.platform === 'win32')(
'watch custom environment',
() => {
it('should re-run all tests when custom environment file changes', async () => {
const fixturesTargetPath = `${__dirname}/fixtures-test-custom-environment${process.env.RSTEST_OUTPUT_MODULE !== 'false' ? '-module' : ''}`;

const { fs } = await prepareFixtures({
fixturesPath: `${__dirname}/fixtures-custom-environment`,
fixturesTargetPath,
});

const { cli } = await runRstestCli({
command: 'rstest',
args: ['watch', '--disableConsoleIntercept'],
options: {
nodeOptions: {
cwd: fixturesTargetPath,
},
},
});

await cli.waitForStdout('Duration');
expect(cli.stdout).toMatch('Tests 1 passed');
expect(cli.stdout).not.toMatch('Test files to re-run:');

const environmentPath = path.join(
fixturesTargetPath,
'test-environment.mjs',
);
const environmentHelperPath = path.join(
fixturesTargetPath,
'environment-helper.mjs',
);

cli.resetStd();
fs.update(environmentPath, (content) => {
return content.replace(
'global.__CUSTOM_ENV_MARKER__ = marker;',
'global.__CUSTOM_ENV_MARKER__ = `${marker}-modified`;',
);
});
fs.update(path.join(fixturesTargetPath, 'index.test.ts'), (content) => {
return content.replace("'initial'", "'initial-modified'");
});

await cli.waitForStdout('Duration');
expect(cli.stdout).toMatch('Tests 1 passed');

cli.resetStd();
fs.update(path.join(fixturesTargetPath, 'index.test.ts'), (content) => {
return content.replace("'initial-modified'", "'dependency-modified'");
});

await cli.waitForStdout('Tests 1 failed');

cli.resetStd();
fs.update(environmentHelperPath, (content) => {
return content.replace("'initial'", "'dependency'");
});

await cli.waitForStdout('Duration');
expect(cli.stdout).toMatch('Tests 1 passed');

cli.resetStd();
fs.delete(environmentPath);

await cli.waitForStderr('Failed to resolve testEnvironment');

cli.exec.kill();
});
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const marker = 'initial';
Loading
Loading