diff --git a/e2e/watch/fixtures-shortcuts/rstest.config.ts b/e2e/watch/fixtures-shortcuts/rstest.config.ts index 03ca6b8b..d354f9b7 100644 --- a/e2e/watch/fixtures-shortcuts/rstest.config.ts +++ b/e2e/watch/fixtures-shortcuts/rstest.config.ts @@ -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, }, }, }, diff --git a/e2e/watch/fixtures/rstest.config.ts b/e2e/watch/fixtures/rstest.config.ts index 3114ac1e..953fa7b4 100644 --- a/e2e/watch/fixtures/rstest.config.ts +++ b/e2e/watch/fixtures/rstest.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ tools: { rspack: { watchOptions: { - aggregateTimeout: 10, + aggregateTimeout: 20, }, }, }, diff --git a/e2e/watch/index.test.ts b/e2e/watch/index.test.ts index d48aedb6..3ec7e187 100644 --- a/e2e/watch/index.test.ts +++ b/e2e/watch/index.test.ts @@ -1,11 +1,15 @@ 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 { prepareFixtures, runRstestCli } from '../scripts'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +rs.setConfig({ + retry: 3, +}); + describe('watch', () => { it('test files should be ran when create / update / delete', async () => { const { fs } = await prepareFixtures({ @@ -42,8 +46,13 @@ describe('watch', () => { });`, ); - await cli.waitForStdout('Duration'); - expect(cli.stdout).toMatch('Tests 2 passed'); + await expect + .poll(() => cli.stdout, { + interval: 20, + timeout: 2000, + }) + .toMatch('Tests 2 passed'); + expect(cli.stdout).toMatch(/Test files to re-run.*:\n.*bar\.test\.ts\n\n/); // update @@ -52,7 +61,12 @@ describe('watch', () => { return content.replace("toBe('bar')", "toBe('BAR')"); }); - await cli.waitForStdout('Duration'); + await expect + .poll(() => cli.stdout, { + interval: 20, + timeout: 2000, + }) + .toMatch('Test Files 1 failed'); expect(cli.stdout).toMatch('Test Files 1 failed'); expect(cli.stdout).toMatch('✗ bar > bar should be to bar'); expect(cli.stdout).toMatch(/Test files to re-run.*:\n.*bar\.test\.ts\n\n/); @@ -60,9 +74,12 @@ describe('watch', () => { // delete cli.resetStd(); fs.delete('./fixtures-test-0/bar.test.ts'); - await cli.waitForStdout('Duration'); + await expect + .poll(() => cli.stdout, { + interval: 20, + }) + .toMatch('Test Files 1 passed'); expect(cli.stdout).toMatch('No test files need re-run.'); - expect(cli.stdout).toMatch('Test Files 1 passed'); cli.exec.kill(); }); diff --git a/e2e/watch/restart.test.ts b/e2e/watch/restart.test.ts index 336dda62..9801d291 100644 --- a/e2e/watch/restart.test.ts +++ b/e2e/watch/restart.test.ts @@ -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 { fs } = await prepareFixtures({ @@ -23,7 +27,16 @@ describe('restart', () => { fs.create( configFile, `import { defineConfig } from '@rstest/core'; -export default defineConfig({}); +export default defineConfig({ + name: 'restart', + tools: { + rspack: { + watchOptions: { + ignored: '**/**' + }, + }, + }, +}); `, ); diff --git a/e2e/watch/shortcuts.test.ts b/e2e/watch/shortcuts.test.ts index 0d9bfb2c..8ce27788 100644 --- a/e2e/watch/shortcuts.test.ts +++ b/e2e/watch/shortcuts.test.ts @@ -93,4 +93,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(); + }); }); diff --git a/examples/node/test/index1.test.ts b/examples/node/test/index1.test.ts new file mode 100644 index 00000000..4fca44ec --- /dev/null +++ b/examples/node/test/index1.test.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from '@rstest/core'; + +describe('Index1', () => { + it('should add two numbers correctly', () => { + expect(1 + 1).toBe(2); + }); +}); diff --git a/packages/core/src/core/plugins/entry.ts b/packages/core/src/core/plugins/entry.ts index 0e6dc49d..1f0a4e51 100644 --- a/packages/core/src/core/plugins/entry.ts +++ b/packages/core/src/core/plugins/entry.ts @@ -26,6 +26,20 @@ class TestFileWatchPlugin { } } +const rstestVirtualEntryFlag = 'rstest-virtual-entry-'; + +let rerunTrigger: Record void> = {}; + +const registerRerunTrigger = (name: string, fn: () => void) => { + rerunTrigger[name] = fn; +}; + +export const triggerRerun = (): void => { + Object.values(rerunTrigger).forEach((fn) => { + fn(); + }); +}; + export const pluginEntryWatch: (params: { context: RstestContext; globTestSourceEntries: (name: string) => Promise>; @@ -40,8 +54,16 @@ export const pluginEntryWatch: (params: { }) => ({ name: 'rstest:entry-watch', setup: (api) => { - api.modifyRspackConfig(async (config, { environment }) => { + api.onCloseDevServer(() => { + rerunTrigger = {}; + }); + + api.modifyRspackConfig(async (config, { environment, rspack }) => { if (isWatch) { + // FIXME: inspect config will retrigger initConfig + if (rerunTrigger[environment.name]) { + return; + } config.plugins.push(new TestFileWatchPlugin(environment.config.root)); config.entry = async () => { const sourceEntries = await globTestSourceEntries(environment.name); @@ -51,7 +73,30 @@ export const pluginEntryWatch: (params: { }; }; + // Add virtual entry to trigger recompile + const virtualEntryName = `${rstestVirtualEntryFlag}${config.name!}.js`; + const virtualEntryPath = `${environment.config.root}/${virtualEntryName}`; + + const virtualModulesPlugin = + new rspack.experiments.VirtualModulesPlugin({ + [virtualEntryPath]: `export const virtualEntry = ${Date.now()}`, + }); + + registerRerunTrigger(environment.name, () => + virtualModulesPlugin.writeModule( + virtualEntryPath, + `export const virtualEntry = ${Date.now()}`, + ), + ); + + config.experiments ??= {}; + config.experiments.nativeWatcher = true; + + config.plugins.push(virtualModulesPlugin); + config.watchOptions ??= {}; + config.watchOptions.aggregateTimeout = 5; + // TODO: rspack should support `(string | RegExp)[]` type // https://github.com/web-infra-dev/rspack/issues/10596 config.watchOptions.ignored = castArray( diff --git a/packages/core/src/core/runTests.ts b/packages/core/src/core/runTests.ts index 8d2d29d1..e4710caa 100644 --- a/packages/core/src/core/runTests.ts +++ b/packages/core/src/core/runTests.ts @@ -269,6 +269,7 @@ export async function runTests(context: Rstest): Promise { }; const { onBeforeRestart } = await import('./restart'); + const { triggerRerun } = await import('./plugins/entry'); onBeforeRestart(async () => { await pool.close(); @@ -281,9 +282,13 @@ export async function runTests(context: Rstest): Promise { } }); + let forceRerunOnce = false; + rsbuildInstance.onAfterDevCompile(async ({ isFirstCompile }) => { snapshotManager.clear(); - await run({ mode: isFirstCompile ? 'all' : 'on-demand' }); + await run({ + mode: isFirstCompile || forceRerunOnce ? 'all' : 'on-demand', + }); if (isFirstCompile && enableCliShortcuts) { const closeCliShortcuts = await setupCliShortcuts({ @@ -297,9 +302,8 @@ export async function runTests(context: Rstest): Promise { context.normalizedConfig.testNamePattern = undefined; context.fileFilters = undefined; - // TODO: should rerun compile with new entries - await run({ mode: 'all' }); - afterTestsWatchRun(); + forceRerunOnce = true; + triggerRerun(); }, runWithTestNamePattern: async (pattern?: string) => { clearScreen();