From 61e1e7106806fa4a8e1a47f17b64ad8d8041a312 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Sun, 26 Apr 2026 05:59:59 -0300 Subject: [PATCH 1/2] fix: propagate outer plugins into multi-suite sub-suites --- README.md | 3 +- package-lock.json | 10 +- package.json | 4 +- src/index.ts | 179 ++++++++++-------- .../forward-plugins/suite-a/a.test.ts | 3 + .../forward-plugins/suite-b/a.test.ts | 3 + test/integration/forward-plugins.test.ts | 68 +++++++ 7 files changed, 181 insertions(+), 89 deletions(-) create mode 100644 test/__fixtures__/forward-plugins/suite-a/a.test.ts create mode 100644 test/__fixtures__/forward-plugins/suite-b/a.test.ts create mode 100644 test/integration/forward-plugins.test.ts diff --git a/README.md b/README.md index 9226c1d..c06d0ad 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ Enjoying **Poku**? [Give him a star to show your support](https://github.com/wel > [!TIP] > -> Run multiple suites as one: independent concurrency, reporters, plugins, and quiet mode per suite, with isolated executions and a single consolidated report at the end. +> - Run multiple suites as one: independent concurrency, reporters, plugins, and quiet mode per suite, with isolated executions and a single consolidated report at the end. +> - Combine with [**@pokujs/coverage**](https://github.com/pokujs/coverage) to merge the suites automatically on teardown. --- diff --git a/package-lock.json b/package-lock.json index 9e14280..a7c6cc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@biomejs/biome": "^1.9.4", "@ianvs/prettier-plugin-sort-imports": "^4.7.0", "@types/node": "^25.3.3", - "poku": "^4.2.0", + "poku": "^4.3.0", "prettier": "^3.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" @@ -28,7 +28,7 @@ "url": "https://github.com/sponsors/wellwelwel" }, "peerDependencies": { - "poku": "^4.1.0" + "poku": "^4.3.0" } }, "node_modules/@babel/code-frame": { @@ -982,9 +982,9 @@ "license": "ISC" }, "node_modules/poku": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/poku/-/poku-4.2.0.tgz", - "integrity": "sha512-GygMGFGgEJ9kfs6Z+QPg/ODs9OF3oGHN8+hYIxtBox3pwYISO+Vu660vH1e+YzjpGoaoy2o5y6YwE1tX5yZx3Q==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/poku/-/poku-4.3.0.tgz", + "integrity": "sha512-s6xHA93lzirvScBuW5UxUAbx4Cw6C/5MEMTe/27jTtLkDmIsWNpUH2CiMbSOKMxLGj7C3JoM2zfacu3kCrlk3Q==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index f3a35fa..0dbd558 100644 --- a/package.json +++ b/package.json @@ -40,13 +40,13 @@ "@biomejs/biome": "^1.9.4", "@ianvs/prettier-plugin-sort-imports": "^4.7.0", "@types/node": "^25.3.3", - "poku": "^4.2.0", + "poku": "^4.3.0", "prettier": "^3.8.1", "tsx": "^4.21.0", "typescript": "^5.9.3" }, "peerDependencies": { - "poku": "^4.2.0" + "poku": "^4.3.0" }, "publishConfig": { "access": "public" diff --git a/src/index.ts b/src/index.ts index eec6652..65e9696 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,86 +4,103 @@ import process from 'node:process'; import { kill, envFile as loadEnvFile, poku } from 'poku'; import { onSigint, reporterRegistry } from 'poku/plugins'; -export const multiSuite = (suites: ConfigFile[]): PokuPlugin => ({ - name: 'multi-suite', - discoverFiles: () => [], - setup: async (context: PluginContext) => { - const overallStart = new Date(); - const overallHrtime = process.hrtime(); - - const onSignal = () => { - process.stdout.write('\u001B[?25h'); - process.exit(1); - }; - - process.removeListener('SIGINT', onSigint); - process.once('SIGINT', onSignal); - - let finalCode: 0 | 1 = 0; - - for (const { include, envFile, kill: suiteKill, ...config } of suites) { - const dirs = include ? ([] as string[]).concat(include) : ['.']; - const tasks: Promise[] = []; - - if (envFile) tasks.push(loadEnvFile(envFile)); - if (suiteKill) { - if (suiteKill.port && suiteKill.port.length > 0) - tasks.push(kill.port(suiteKill.port)); - if (suiteKill.range && suiteKill.range.length > 0) { - for (const [from, to] of suiteKill.range) - tasks.push(kill.range(from, to)); +const stripLifecycle = (plugin: PokuPlugin): PokuPlugin => ({ + name: plugin.name, + runner: plugin.runner, + onTestProcess: plugin.onTestProcess, + ipc: plugin.ipc, +}); + +export const multiSuite = (suites: ConfigFile[]): PokuPlugin => { + const self: PokuPlugin = { + name: 'multi-suite', + discoverFiles: () => [], + setup: async (context: PluginContext) => { + const overallStart = new Date(); + const overallHrtime = process.hrtime(); + + const onSignal = () => { + process.stdout.write('\u001B[?25h'); + process.exit(1); + }; + + process.removeListener('SIGINT', onSigint); + process.once('SIGINT', onSignal); + + let finalCode: 0 | 1 = 0; + + for (const { include, envFile, kill: suiteKill, ...config } of suites) { + const dirs = include ? ([] as string[]).concat(include) : ['.']; + const tasks: Promise[] = []; + + if (envFile) tasks.push(loadEnvFile(envFile)); + if (suiteKill) { + if (suiteKill.port && suiteKill.port.length > 0) + tasks.push(kill.port(suiteKill.port)); + if (suiteKill.range && suiteKill.range.length > 0) { + for (const [from, to] of suiteKill.range) + tasks.push(kill.range(from, to)); + } + if (suiteKill.pid && suiteKill.pid.length > 0) + tasks.push(kill.pid(suiteKill.pid)); } - if (suiteKill.pid && suiteKill.pid.length > 0) - tasks.push(kill.pid(suiteKill.pid)); + if (tasks.length > 0) await Promise.all(tasks); + + const { reporter: suiteReporterConfig } = config; + const suiteBase = + typeof suiteReporterConfig === 'function' + ? suiteReporterConfig(context.configs) + : typeof suiteReporterConfig === 'string' && + suiteReporterConfig in reporterRegistry + ? reporterRegistry[suiteReporterConfig](context.configs) + : context.reporter; + + const suiteReporter = () => ({ + ...suiteBase, + onRunResult() {}, + onExit() {}, + }); + + const outerPlugins = context.configs.plugins ?? []; + const forwardedOuter = outerPlugins + .filter((p) => p !== self) + .map(stripLifecycle); + const subSuitePlugins = config.plugins ?? []; + const mergedPlugins = [...forwardedOuter, ...subSuitePlugins]; + + const code = await poku(dirs, { + ...config, + plugins: mergedPlugins, + reporter: suiteReporter, + noExit: true, + }); + + if (code !== 0) finalCode = 1; } - if (tasks.length > 0) await Promise.all(tasks); - - const { reporter: suiteReporterConfig } = config; - const suiteBase = - typeof suiteReporterConfig === 'function' - ? suiteReporterConfig(context.configs) - : typeof suiteReporterConfig === 'string' && - suiteReporterConfig in reporterRegistry - ? reporterRegistry[suiteReporterConfig](context.configs) - : context.reporter; - - const suiteReporter = () => ({ - ...suiteBase, - onRunResult() {}, - onExit() {}, - }); - - const code = await poku(dirs, { - plugins: config.plugins ?? [], - ...config, - reporter: suiteReporter, - noExit: true, - }); - - if (code !== 0) finalCode = 1; - } - - const elapsed = process.hrtime(overallHrtime); - - context.timespan.started = overallStart; - context.timespan.duration = elapsed[0] * 1e3 + elapsed[1] / 1e6; - context.timespan.finished = new Date(); - - if (!context.configs.quiet) { - context.reporter.onRunResult({ - code: finalCode, - timespan: context.timespan, - results: context.results, - }); - - context.reporter.onExit({ - code: finalCode, - timespan: context.timespan, - results: context.results, - }); - } - - process.removeListener('SIGINT', onSignal); - process.exitCode = finalCode; - }, -}); + + const elapsed = process.hrtime(overallHrtime); + + context.timespan.started = overallStart; + context.timespan.duration = elapsed[0] * 1e3 + elapsed[1] / 1e6; + context.timespan.finished = new Date(); + + if (!context.configs.quiet) { + context.reporter.onRunResult({ + code: finalCode, + timespan: context.timespan, + results: context.results, + }); + + context.reporter.onExit({ + code: finalCode, + timespan: context.timespan, + results: context.results, + }); + } + + process.removeListener('SIGINT', onSignal); + process.exitCode = finalCode; + }, + }; + return self; +}; diff --git a/test/__fixtures__/forward-plugins/suite-a/a.test.ts b/test/__fixtures__/forward-plugins/suite-a/a.test.ts new file mode 100644 index 0000000..cc26b4e --- /dev/null +++ b/test/__fixtures__/forward-plugins/suite-a/a.test.ts @@ -0,0 +1,3 @@ +import process from 'node:process'; + +process.exit(0); diff --git a/test/__fixtures__/forward-plugins/suite-b/a.test.ts b/test/__fixtures__/forward-plugins/suite-b/a.test.ts new file mode 100644 index 0000000..cc26b4e --- /dev/null +++ b/test/__fixtures__/forward-plugins/suite-b/a.test.ts @@ -0,0 +1,3 @@ +import process from 'node:process'; + +process.exit(0); diff --git a/test/integration/forward-plugins.test.ts b/test/integration/forward-plugins.test.ts new file mode 100644 index 0000000..9a48d7d --- /dev/null +++ b/test/integration/forward-plugins.test.ts @@ -0,0 +1,68 @@ +import { mkdir } from 'node:fs/promises'; +import { assert, describe, it, poku } from 'poku'; +import type { PokuPlugin } from 'poku/plugins'; +import { multiSuite } from '../../src/index.js'; + +const OUTER_DIR = 'test/__fixtures__/empty'; + +describe('Plugin: multi-suite forwards per-file hooks to sub-suites', async () => { + await mkdir(OUTER_DIR, { recursive: true }); + + await it('outer runner/onTestProcess fire per sub-suite file; setup/teardown run once', async () => { + const runnerCalls: string[] = []; + const onTestProcessCalls: string[] = []; + let setupCount = 0; + let teardownCount = 0; + + const probe: PokuPlugin = { + name: 'probe', + setup: () => { + setupCount += 1; + }, + teardown: () => { + teardownCount += 1; + }, + runner: (command, file) => { + runnerCalls.push(file); + return command; + }, + onTestProcess: (_child, file) => { + onTestProcessCalls.push(file); + }, + }; + + const originalExitCode = process.exitCode; + + await poku(OUTER_DIR, { + noExit: true, + quiet: true, + plugins: [ + probe, + multiSuite([ + { include: 'test/__fixtures__/forward-plugins/suite-a' }, + { include: 'test/__fixtures__/forward-plugins/suite-b' }, + ]), + ], + }); + + process.exitCode = originalExitCode; + + assert.strictEqual(setupCount, 1, 'outer setup must run exactly once'); + assert.strictEqual(teardownCount, 1, 'outer teardown must run exactly once'); + assert.strictEqual( + runnerCalls.length, + 2, + 'runner must fire per sub-suite test file' + ); + assert.ok( + runnerCalls.some((f) => f.endsWith('suite-a/a.test.ts')) && + runnerCalls.some((f) => f.endsWith('suite-b/a.test.ts')), + 'runner must see both sub-suite files' + ); + assert.strictEqual( + onTestProcessCalls.length, + 2, + 'onTestProcess must fire per sub-suite test file' + ); + }); +}); From 6d8d86615eaea6c23de750b6e8e21ce6be96c0fe Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Sun, 26 Apr 2026 06:04:13 -0300 Subject: [PATCH 2/2] chore: fix lint --- test/integration/forward-plugins.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/integration/forward-plugins.test.ts b/test/integration/forward-plugins.test.ts index 9a48d7d..9d14360 100644 --- a/test/integration/forward-plugins.test.ts +++ b/test/integration/forward-plugins.test.ts @@ -1,6 +1,6 @@ +import type { PokuPlugin } from 'poku/plugins'; import { mkdir } from 'node:fs/promises'; import { assert, describe, it, poku } from 'poku'; -import type { PokuPlugin } from 'poku/plugins'; import { multiSuite } from '../../src/index.js'; const OUTER_DIR = 'test/__fixtures__/empty'; @@ -48,7 +48,11 @@ describe('Plugin: multi-suite forwards per-file hooks to sub-suites', async () = process.exitCode = originalExitCode; assert.strictEqual(setupCount, 1, 'outer setup must run exactly once'); - assert.strictEqual(teardownCount, 1, 'outer teardown must run exactly once'); + assert.strictEqual( + teardownCount, + 1, + 'outer teardown must run exactly once' + ); assert.strictEqual( runnerCalls.length, 2,