Skip to content
Draft
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
3 changes: 2 additions & 1 deletion e2e/watch/fixtures-shortcuts/rstest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ process.stdin.isTTY = true;
process.stdin.setRawMode = () => process.stdin;

export default defineConfig({
name: 'shortcuts',
reporters: ['default'],
disableConsoleIntercept: true,
tools: {
rspack: {
watchOptions: {
aggregateTimeout: 10,
aggregateTimeout: 20,
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion e2e/watch/fixtures/rstest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default defineConfig({
tools: {
rspack: {
watchOptions: {
aggregateTimeout: 10,
aggregateTimeout: 20,
},
},
},
Expand Down
17 changes: 15 additions & 2 deletions e2e/watch/restart.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from '@rstest/core';
import { describe, expect, it, rs } from '@rstest/core';
import { remove } from 'fs-extra';
import { prepareFixtures, runRstestCli } from '../scripts/';

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

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

describe('restart', () => {
it('should restart when rstest config file changed', async () => {
const fixturesTargetPath = `${__dirname}/fixtures-test-1${process.env.RSTEST_OUTPUT_MODULE !== 'false' ? '-module' : ''}`;
Expand All @@ -22,7 +26,16 @@ describe('restart', () => {
fs.create(
configFile,
`import { defineConfig } from '@rstest/core';
export default defineConfig({});
export default defineConfig({
name: 'restart',
tools: {
rspack: {
watchOptions: {
ignored: '**/**'
},
},
},
});
`,
);

Expand Down
37 changes: 37 additions & 0 deletions e2e/watch/shortcuts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,41 @@ describe('CLI shortcuts', () => {

cli.exec.kill();
});

it('shortcut `a` should work as expected with command filter', async () => {
const fixturesTargetPath = `${__dirname}/fixtures-test-shortcuts-a`;
await prepareFixtures({
fixturesPath: `${__dirname}/fixtures-shortcuts`,
fixturesTargetPath,
});

const { cli } = await runRstestCli({
command: 'rstest',
args: ['watch', 'index1'],
options: {
nodeOptions: {
env: {
FORCE_TTY: 'true',
CI: undefined,
},
cwd: fixturesTargetPath,
},
},
});

// initial run
await cli.waitForStdout('Duration');
expect(cli.stdout).toMatch('Tests 1 failed');

await cli.waitForStdout('press h to show help');

cli.resetStd();

// rerun all tests
cli.exec.process!.stdin!.write('a');
await cli.waitForStdout('Duration');
expect(cli.stdout).toMatch('Tests 1 failed | 1 passed');

cli.exec.kill();
});
});
7 changes: 7 additions & 0 deletions examples/node/test/index1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { describe, expect, it } from '@rstest/core';

describe('Index1', () => {
it('should add two numbers correctly', () => {
expect(1 + 1).toBe(2);
});
});
56 changes: 55 additions & 1 deletion packages/core/src/core/plugins/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ class TestFileWatchPlugin {
}
}

const rstestVirtualEntryFlag = 'rstest-virtual-entry-';

const rerunTriggers = new Map<string, () => void>();
const configuredWatchConfigs = new WeakMap<object, Set<string>>();

export const triggerRerun = (): boolean => {
let hasTrigger = false;
for (const trigger of rerunTriggers.values()) {
hasTrigger = true;
trigger();
}
return hasTrigger;
};

export const pluginEntryWatch: (params: {
context: RstestContext;
globTestSourceEntries: (name: string) => Promise<Record<string, string>>;
Expand All @@ -42,9 +56,23 @@ export const pluginEntryWatch: (params: {
}) => ({
name: 'rstest:entry-watch',
setup: (api) => {
api.onCloseDevServer(() => {
rerunTriggers.clear();
});

const outputDistPathRoot = context.normalizedConfig.output.distPath.root;
api.modifyRspackConfig(async (config, { environment }) => {
api.modifyRspackConfig(async (config, { environment, rspack }) => {
if (isWatch) {
let configuredEnvironments = configuredWatchConfigs.get(config);
if (!configuredEnvironments) {
configuredEnvironments = new Set();
configuredWatchConfigs.set(config, configuredEnvironments);
}
if (configuredEnvironments.has(environment.name)) {
return;
}
configuredEnvironments.add(environment.name);

config.plugins.push(new TestFileWatchPlugin(environment.config.root));
config.entry = async () => {
const sourceEntries = await globTestSourceEntries(environment.name);
Expand All @@ -55,6 +83,32 @@ export const pluginEntryWatch: (params: {
};
};

const virtualEntryName = `${rstestVirtualEntryFlag}${environment.name}.js`;
const virtualEntryPath = `${environment.config.root}/${virtualEntryName}`;
let virtualEntryVersion = 0;
const getVirtualEntryContent = () =>
`export const virtualEntry = ${virtualEntryVersion};`;

const virtualModulesPlugin =
new rspack.experiments.VirtualModulesPlugin({
[virtualEntryPath]: getVirtualEntryContent(),
});

config.experiments ??= {};
config.experiments.nativeWatcher = true;
config.plugins.push({
apply(compiler: Rspack.Compiler) {
virtualModulesPlugin.apply(compiler);
rerunTriggers.set(environment.name, () => {
virtualEntryVersion += 1;
virtualModulesPlugin.writeModule(
virtualEntryPath,
getVirtualEntryContent(),
);
});
},
});

config.watchOptions ??= {};
// FIXME: Temporarily default to 5 to debounce rerun in watch mode.
config.watchOptions.aggregateTimeout = 5;
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/core/projectPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ const getProjectEntries = async ({
});
};

const areFileFiltersEqual = (a?: string[], b?: string[]): boolean => {
const left = a || [];
const right = b || [];

return left.length === right.length && left.every((v, i) => v === right[i]);
};

export type RunProjectPlan = {
projects: ProjectContext[];
entriesCache: Map<string, ProjectEntries>;
Expand Down Expand Up @@ -75,8 +82,14 @@ export const createRunProjectPlanState = ({
if (context.relatedResolutionEmpty) {
return {};
}
if (entriesCache.has(name)) {
return entriesCache.get(name)!.entries;
const cachedEntries = entriesCache.get(name);
if (
cachedEntries &&
(!isWatchMode ||
context.normalizedConfig.shard ||
areFileFiltersEqual(cachedEntries.fileFilters, context.fileFilters))
) {
return cachedEntries.entries;
}

const project = allProjects.find((p) => p.environmentName === name);
Expand Down
18 changes: 14 additions & 4 deletions packages/core/src/core/runTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,7 @@ export async function runTests(context: Rstest): Promise<void> {
};

const { onBeforeRestart } = await import('./restart');
const { triggerRerun } = await import('./plugins/entry');

onBeforeRestart(async () => {
await runLifecycleStep('global teardown', () => runGlobalTeardown());
Expand All @@ -977,10 +978,16 @@ export async function runTests(context: Rstest): Promise<void> {
}
});

let forceRerunOnce = false;

rsbuildInstance.onAfterDevCompile(async ({ isFirstCompile }) => {
snapshotManager.clear();
await run({ buildStart, mode: isFirstCompile ? 'all' : 'on-demand' });
await run({
buildStart,
mode: isFirstCompile || forceRerunOnce ? 'all' : 'on-demand',
});
buildStart = undefined;
forceRerunOnce = false;

if (isFirstCompile && enableCliShortcuts) {
const closeCliShortcuts = await setupCliShortcuts({
Expand All @@ -1002,9 +1009,12 @@ export async function runTests(context: Rstest): Promise<void> {
context.normalizedConfig.testNamePattern = undefined;
context.fileFilters = undefined;

// TODO: should rerun compile with new entries
await run({ mode: 'all' });
afterTestsWatchRun();
forceRerunOnce = true;
if (!triggerRerun()) {
await run({ mode: 'all' });
forceRerunOnce = false;
afterTestsWatchRun();
}
},
runWithTestNamePattern: async (pattern?: string) => {
clearScreen();
Expand Down
Loading