diff --git a/.github/workflows/ci_compatibility-deno.yml b/.github/workflows/ci_compatibility-deno.yml
index ceb41dc..2d8d6ab 100644
--- a/.github/workflows/ci_compatibility-deno.yml
+++ b/.github/workflows/ci_compatibility-deno.yml
@@ -39,7 +39,7 @@ jobs:
node-version: lts/*
- name: โ Actions - Setup Deno
- uses: denoland/setup-deno@v1
+ uses: denoland/setup-deno@v2
with:
deno-version: ${{ matrix.deno-version }}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d79d4a5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Lojhan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index af1c924..34e9242 100644
--- a/README.md
+++ b/README.md
@@ -1,72 +1,155 @@
+
+

+
# poku-react-testing
-React testing helpers and a Poku plugin for DOM-backed test execution.
+Enjoying **Poku**? [Give him a star to show your support](https://github.com/wellwelwel/poku) ๐
+
+---
+
+๐ [**Documentation**](https://github.com/Lojhan/poku-react-testing#readme)
+
+
+
+---
+
+๐งช [**poku-react-testing**](https://github.com/Lojhan/poku-react-testing) is a **Poku** plugin for React component testing with DOM adapters.
+
+> [!TIP]
+>
+> Render React components in isolated test files โ automatic TSX loader injection, DOM environment setup, and optional render metrics included.
+
+---
-## Features
+## Quickstart
-- Lightweight `render`, `renderHook`, `cleanup`, `screen`, and `fireEvent` helpers.
-- Poku plugin that injects TSX loader and DOM setup automatically for `.tsx` and `.jsx` tests.
-- DOM adapters:
- - `happy-dom` (default)
- - `jsdom` (optional)
- - custom setup module
-- Optional render metrics with configurable reporting.
+### Install
-## Install
+
+
+|
```bash
-npm install --save-dev poku-react-testing poku react react-dom
+# Node.js
+npm i -D poku-react-testing
```
-If you want to run tests with `jsdom`:
+ |
+
```bash
-npm install --save-dev jsdom
+# Bun
+bun add -d poku-react-testing
```
-## Usage
+ |
+
-### 1) Configure Poku
+```bash
+# Deno (optional)
+deno add npm:poku-react-testing
+```
-```ts
+ |
+
+
+
+Install a DOM adapter (at least one is required):
+
+
+
+|
+
+```bash
+# happy-dom (recommended)
+npm i -D happy-dom \
+ @happy-dom/global-registrator
+```
+
+ |
+
+
+```bash
+# jsdom
+npm i -D jsdom
+```
+
+ |
+
+
+
+### Enable the Plugin
+
+```js
+// poku.config.js
import { defineConfig } from 'poku';
-import { reactTestingPlugin } from 'poku-react-testing';
+import { reactTestingPlugin } from 'poku-react-testing/plugin';
export default defineConfig({
plugins: [
reactTestingPlugin({
dom: 'happy-dom',
- domUrl: 'http://localhost:3000/',
- metrics: false,
}),
],
});
```
-### 2) Write tests
+### Write Tests
```tsx
+// tests/my-component.test.tsx
import { afterEach, assert, test } from 'poku';
import { cleanup, render, screen } from 'poku-react-testing';
afterEach(cleanup);
-test('renders component', () => {
+test('renders a heading', () => {
render(Hello
);
assert.strictEqual(screen.getByRole('heading').textContent, 'Hello');
});
```
-## Metrics
+---
+
+## Compatibility
-Metrics are disabled by default. Enable metrics explicitly:
+### Runtime ร DOM Adapter
+
+| | Node.js โฅ 20 | Bun โฅ 1 | Deno โฅ 2 |
+|---|:---:|:---:|:---:|
+| **happy-dom** | โ
| โ
| โ
|
+| **jsdom** | โ
| โ
| โ ๏ธ |
+| **custom setup** | โ
| โ
| โ
|
+
+> [!NOTE]
+>
+> `jsdom` under Deno may be unstable depending on Deno's npm compatibility layer for the current `jsdom` version. Use `happy-dom` for Deno environments.
+
+---
+
+## Options
```ts
reactTestingPlugin({
+ /**
+ * DOM adapter to use. Defaults to 'happy-dom'.
+ * - 'happy-dom' โ fast, recommended for most tests
+ * - 'jsdom' โ broader browser API coverage
+ * - { setupModule } โ path to a custom DOM setup module
+ */
+ dom: 'happy-dom',
+
+ /** Base URL assigned to the DOM environment. */
+ domUrl: 'http://localhost:3000/',
+
+ /**
+ * Render metrics. Disabled by default.
+ * Pass `true` for defaults, or an object for fine-grained control.
+ */
metrics: {
enabled: true,
- topN: 10,
- minDurationMs: 1,
+ topN: 5,
+ minDurationMs: 0,
reporter(summary) {
console.log(summary.topSlowest);
},
@@ -74,17 +157,8 @@ reactTestingPlugin({
});
```
-`metrics: true` is shorthand for enabling metrics with default options.
-
-## Build and Validate
-
-```bash
-npm run check
-npm run build
-npm pack --dry-run
-```
+---
-## Release
+## License
-- Push a tag like `v0.1.0` to trigger publish workflow.
-- Set repository secret `NPM_TOKEN` with publish permissions.
+[MIT](./LICENSE)
diff --git a/package.json b/package.json
index 46a5808..8a03707 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"test:happy": "cross-env POKU_REACT_TEST_DOM=happy-dom node --import=tsx ./node_modules/poku/lib/bin/index.js --showLogs",
"test:jsdom": "cross-env POKU_REACT_TEST_DOM=jsdom node --import=tsx ./node_modules/poku/lib/bin/index.js --showLogs",
"test:bun": "cross-env POKU_REACT_TEST_DOM=happy-dom bun ./node_modules/poku/lib/bin/index.js --showLogs && cross-env POKU_REACT_TEST_DOM=jsdom bun ./node_modules/poku/lib/bin/index.js --showLogs",
- "test:deno": "cross-env POKU_REACT_TEST_DOM=happy-dom deno run -A npm:poku --showLogs && cross-env POKU_REACT_TEST_DOM=jsdom deno run -A npm:poku --showLogs",
+ "test:deno": "cross-env POKU_REACT_TEST_DOM=happy-dom deno run -A npm:poku --showLogs",
"clean": "rimraf dist",
"build": "tsup src/index.ts src/plugin.ts src/react-testing.ts src/dom-setup-happy.ts src/dom-setup-jsdom.ts --format esm --dts --target node20 --sourcemap --clean --tsconfig tsconfig.tsup.json",
"typecheck": "tsc -p tsconfig.build.json --noEmit",
@@ -52,7 +52,7 @@
"jsdom"
],
"author": "",
- "license": "ISC",
+ "license": "MIT",
"publishConfig": {
"access": "public",
"provenance": true
diff --git a/src/dom-setup-happy.ts b/src/dom-setup-happy.ts
index 1f883bd..31dc60e 100644
--- a/src/dom-setup-happy.ts
+++ b/src/dom-setup-happy.ts
@@ -1,5 +1,7 @@
import { GlobalRegistrator } from '@happy-dom/global-registrator';
+declare const Deno: unknown;
+
type ReactActGlobal = typeof globalThis & {
IS_REACT_ACT_ENVIRONMENT?: boolean;
};
@@ -10,9 +12,20 @@ const defaultUrl = 'http://localhost:3000/';
const configuredUrl = process.env.POKU_REACT_DOM_URL || defaultUrl;
if (!globalThis.window || !globalThis.document) {
+ const isDenoRuntime = typeof Deno !== 'undefined';
+ const nativeEvent = isDenoRuntime ? globalThis.Event : undefined;
+ const nativeDispatchEvent = isDenoRuntime
+ ? globalThis.dispatchEvent?.bind(globalThis)
+ : undefined;
+
GlobalRegistrator.register({
url: configuredUrl,
});
+
+ if (isDenoRuntime) {
+ if (nativeEvent) (globalThis as unknown as Record).Event = nativeEvent;
+ if (nativeDispatchEvent) globalThis.dispatchEvent = nativeDispatchEvent;
+ }
}
if (typeof reactGlobal.IS_REACT_ACT_ENVIRONMENT === 'undefined') {
diff --git a/src/plugin.ts b/src/plugin.ts
index 8d4e4fb..6eb063d 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -83,6 +83,31 @@ type NormalizedMetricsOptions = {
reporter?: (summary: ReactMetricsSummary) => void;
};
+type RuntimeSupport = {
+ supportsNodeLikeImport: boolean;
+ supportsDenoPreload: boolean;
+};
+
+type BuildRunnerCommandInput = {
+ runtime: string;
+ command: string[];
+ file: string;
+ domSetupPath: string;
+};
+
+type BuildRunnerCommandOutput = {
+ shouldHandle: boolean;
+ command: string[];
+};
+
+type EnvSnapshot = {
+ previousDomUrl: string | undefined;
+ previousMetricsFlag: string | undefined;
+};
+
+const DEFAULT_TOP_N = 5;
+const DEFAULT_MIN_DURATION_MS = 0;
+
const isRenderMetricMessage = (message: unknown): message is RenderMetricMessage => {
if (!message || typeof message !== 'object') return false;
return (message as Record).type === 'POKU_REACT_RENDER_METRIC';
@@ -94,6 +119,19 @@ const getComponentName = (componentName: unknown) =>
: 'AnonymousComponent';
const isTsxImport = (arg: string) => arg === '--import=tsx' || arg === '--loader=tsx';
+const isNodeRuntime = (runtime: string) => runtime === 'node';
+const isBunRuntime = (runtime: string) => runtime === 'bun';
+const isDenoRuntime = (runtime: string) => runtime === 'deno';
+
+const getRuntimeSupport = (runtime: string): RuntimeSupport => ({
+ supportsNodeLikeImport: isNodeRuntime(runtime) || isBunRuntime(runtime),
+ supportsDenoPreload: isDenoRuntime(runtime),
+});
+
+const canHandleRuntime = (runtime: string) => {
+ const support = getRuntimeSupport(runtime);
+ return support.supportsNodeLikeImport || support.supportsDenoPreload;
+};
const resolveDomSetupPath = (adapter: ReactDomAdapter | undefined) => {
if (!adapter || adapter === 'happy-dom') return happyDomSetupPath;
@@ -102,32 +140,41 @@ const resolveDomSetupPath = (adapter: ReactDomAdapter | undefined) => {
return resolve(process.cwd(), adapter.setupModule);
};
+const getPositiveIntegerOrDefault = (value: unknown, fallback: number) => {
+ const numeric = Number(value);
+ if (!Number.isFinite(numeric) || numeric <= 0) return fallback;
+ return Math.floor(numeric);
+};
+
+const getNonNegativeNumberOrDefault = (value: unknown, fallback: number) => {
+ const numeric = Number(value);
+ if (!Number.isFinite(numeric) || numeric < 0) return fallback;
+ return numeric;
+};
+
const normalizeMetricsOptions = (
metrics: ReactTestingPluginOptions['metrics'],
): NormalizedMetricsOptions => {
if (metrics === true) {
return {
enabled: true,
- topN: 5,
- minDurationMs: 0,
+ topN: DEFAULT_TOP_N,
+ minDurationMs: DEFAULT_MIN_DURATION_MS,
};
}
if (!metrics) {
return {
enabled: false,
- topN: 5,
- minDurationMs: 0,
+ topN: DEFAULT_TOP_N,
+ minDurationMs: DEFAULT_MIN_DURATION_MS,
};
}
const normalized: NormalizedMetricsOptions = {
enabled: metrics.enabled ?? true,
- topN: Number.isFinite(metrics.topN) && Number(metrics.topN) > 0 ? Math.floor(Number(metrics.topN)) : 5,
- minDurationMs:
- Number.isFinite(metrics.minDurationMs) && Number(metrics.minDurationMs) >= 0
- ? Number(metrics.minDurationMs)
- : 0,
+ topN: getPositiveIntegerOrDefault(metrics.topN, DEFAULT_TOP_N),
+ minDurationMs: getNonNegativeNumberOrDefault(metrics.minDurationMs, DEFAULT_MIN_DURATION_MS),
};
if (metrics.reporter) normalized.reporter = metrics.reporter;
@@ -135,16 +182,49 @@ const normalizeMetricsOptions = (
return normalized;
};
-/**
- * Create a Poku plugin that prepares DOM globals and TSX execution for React tests.
- */
-export const createReactTestingPlugin = (options: ReactTestingPluginOptions = {}) => {
- const metrics: RenderMetric[] = [];
- const previousDomUrl = process.env.POKU_REACT_DOM_URL;
- const previousMetricsFlag = process.env.POKU_REACT_ENABLE_METRICS;
- const domSetupPath = resolveDomSetupPath(options.dom);
- const metricsOptions = normalizeMetricsOptions(options.metrics);
+const buildRunnerCommand = ({
+ runtime,
+ command,
+ file,
+ domSetupPath,
+}: BuildRunnerCommandInput): BuildRunnerCommandOutput => {
+ const support = getRuntimeSupport(runtime);
+
+ if (!support.supportsNodeLikeImport && !support.supportsDenoPreload) {
+ return { shouldHandle: false, command };
+ }
+
+ if (!reactExtensions.has(extname(file))) {
+ return { shouldHandle: false, command };
+ }
+
+ const fileIndex = command.lastIndexOf(file);
+ if (fileIndex === -1) return { shouldHandle: false, command };
+ const beforeFile = command.slice(1, fileIndex);
+ const afterFile = command.slice(fileIndex + 1);
+
+ const hasTsx = beforeFile.some(isTsxImport);
+ const hasNodeLikeDomSetup = beforeFile.some((arg) => arg === `--import=${domSetupPath}`);
+ const hasDenoDomSetup = beforeFile.some((arg) => arg === `--preload=${domSetupPath}`);
+
+ const extraImports: string[] = [];
+ if (isNodeRuntime(runtime) && !hasTsx) extraImports.push('--import=tsx');
+ if (support.supportsNodeLikeImport && !hasNodeLikeDomSetup) extraImports.push(`--import=${domSetupPath}`);
+ if (support.supportsDenoPreload && !hasDenoDomSetup) extraImports.push(`--preload=${domSetupPath}`);
+
+ return {
+ shouldHandle: true,
+ command: [runtime, ...beforeFile, ...extraImports, file, ...afterFile],
+ };
+};
+
+const captureEnvSnapshot = (): EnvSnapshot => ({
+ previousDomUrl: process.env.POKU_REACT_DOM_URL,
+ previousMetricsFlag: process.env.POKU_REACT_ENABLE_METRICS,
+});
+
+const applyEnvironmentOptions = (options: ReactTestingPluginOptions, metricsOptions: NormalizedMetricsOptions) => {
if (options.domUrl) {
process.env.POKU_REACT_DOM_URL = options.domUrl;
}
@@ -152,29 +232,80 @@ export const createReactTestingPlugin = (options: ReactTestingPluginOptions = {}
if (metricsOptions.enabled) {
process.env.POKU_REACT_ENABLE_METRICS = '1';
}
+};
- return definePlugin({
- name: 'react-testing',
- ipc: metricsOptions.enabled,
+const restoreEnvironmentOptions = (snapshot: EnvSnapshot) => {
+ if (typeof snapshot.previousDomUrl === 'undefined') {
+ delete process.env.POKU_REACT_DOM_URL;
+ } else {
+ process.env.POKU_REACT_DOM_URL = snapshot.previousDomUrl;
+ }
- runner(command, file) {
- if (command[0] !== 'node') return command;
- if (!reactExtensions.has(extname(file))) return command;
+ if (typeof snapshot.previousMetricsFlag === 'undefined') {
+ delete process.env.POKU_REACT_ENABLE_METRICS;
+ } else {
+ process.env.POKU_REACT_ENABLE_METRICS = snapshot.previousMetricsFlag;
+ }
+};
- const fileIndex = command.lastIndexOf(file);
- if (fileIndex === -1) return command;
+const selectTopSlowestMetrics = (metrics: RenderMetric[], options: NormalizedMetricsOptions) =>
+ [...metrics]
+ .filter((metric) => metric.durationMs >= options.minDurationMs)
+ .sort((a, b) => b.durationMs - a.durationMs)
+ .slice(0, options.topN);
+
+const createMetricsSummary = (
+ metrics: RenderMetric[],
+ options: NormalizedMetricsOptions,
+): ReactMetricsSummary | null => {
+ if (!options.enabled || metrics.length === 0) return null;
+
+ const topSlowest = selectTopSlowestMetrics(metrics, options);
+ if (topSlowest.length === 0) return null;
+
+ return {
+ totalCaptured: metrics.length,
+ totalReported: topSlowest.length,
+ topSlowest,
+ };
+};
+
+const printMetricsSummary = (summary: ReactMetricsSummary) => {
+ const lines = summary.topSlowest.map(
+ (metric) =>
+ ` - ${metric.componentName} in ${metric.file}: ${metric.durationMs.toFixed(2)}ms`,
+ );
+
+ console.log('\n[poku-react-testing] Slowest component renders');
+ for (const line of lines) console.log(line);
+};
+
+/**
+ * Create a Poku plugin that prepares DOM globals and TSX execution for React tests.
+ */
+export const createReactTestingPlugin = (options: ReactTestingPluginOptions = {}) => {
+ const metrics: RenderMetric[] = [];
+ const envSnapshot = captureEnvSnapshot();
+ const domSetupPath = resolveDomSetupPath(options.dom);
+ const metricsOptions = normalizeMetricsOptions(options.metrics);
- const beforeFile = command.slice(1, fileIndex);
- const afterFile = command.slice(fileIndex + 1);
+ applyEnvironmentOptions(options, metricsOptions);
- const hasTsx = beforeFile.some(isTsxImport);
- const hasDomSetup = beforeFile.some((arg) => arg === `--import=${domSetupPath}`);
+ return definePlugin({
+ name: 'react-testing',
+ ipc: metricsOptions.enabled,
- const extraImports: string[] = [];
- if (!hasTsx) extraImports.push('--import=tsx');
- if (!hasDomSetup) extraImports.push(`--import=${domSetupPath}`);
+ runner(command, file) {
+ const runtime = command[0];
+ if (!runtime) return command;
+ const result = buildRunnerCommand({
+ runtime,
+ command,
+ file,
+ domSetupPath,
+ });
- return ['node', ...beforeFile, ...extraImports, file, ...afterFile];
+ return result.command;
},
onTestProcess(child, file) {
@@ -192,45 +323,17 @@ export const createReactTestingPlugin = (options: ReactTestingPluginOptions = {}
},
teardown() {
- if (typeof previousDomUrl === 'undefined') {
- delete process.env.POKU_REACT_DOM_URL;
- } else {
- process.env.POKU_REACT_DOM_URL = previousDomUrl;
- }
+ restoreEnvironmentOptions(envSnapshot);
- if (typeof previousMetricsFlag === 'undefined') {
- delete process.env.POKU_REACT_ENABLE_METRICS;
- } else {
- process.env.POKU_REACT_ENABLE_METRICS = previousMetricsFlag;
- }
-
- if (!metricsOptions.enabled || metrics.length === 0) return;
-
- const topSlowest = [...metrics]
- .filter((metric) => metric.durationMs >= metricsOptions.minDurationMs)
- .sort((a, b) => b.durationMs - a.durationMs)
- .slice(0, metricsOptions.topN);
-
- if (topSlowest.length === 0) return;
-
- const summary: ReactMetricsSummary = {
- totalCaptured: metrics.length,
- totalReported: topSlowest.length,
- topSlowest,
- };
+ const summary = createMetricsSummary(metrics, metricsOptions);
+ if (!summary) return;
if (metricsOptions.reporter) {
metricsOptions.reporter(summary);
return;
}
- const lines = topSlowest.map(
- (metric) =>
- ` - ${metric.componentName} in ${metric.file}: ${metric.durationMs.toFixed(2)}ms`,
- );
-
- console.log('\n[poku-react-testing] Slowest component renders');
- for (const line of lines) console.log(line);
+ printMetricsSummary(summary);
},
});
};
@@ -239,3 +342,17 @@ export const createReactTestingPlugin = (options: ReactTestingPluginOptions = {}
* Alias for `createReactTestingPlugin`.
*/
export const reactTestingPlugin = createReactTestingPlugin;
+
+export const __internal = {
+ buildRunnerCommand,
+ canHandleRuntime,
+ captureEnvSnapshot,
+ applyEnvironmentOptions,
+ restoreEnvironmentOptions,
+ normalizeMetricsOptions,
+ selectTopSlowestMetrics,
+ createMetricsSummary,
+ getComponentName,
+ isRenderMetricMessage,
+ resolveDomSetupPath,
+};
diff --git a/tests/plugin-internals.test.ts b/tests/plugin-internals.test.ts
new file mode 100644
index 0000000..397de89
--- /dev/null
+++ b/tests/plugin-internals.test.ts
@@ -0,0 +1,193 @@
+import { assert, test } from 'poku';
+import { __internal } from '../src/plugin.ts';
+
+const ORIGINAL_DOM_URL = process.env.POKU_REACT_DOM_URL;
+const ORIGINAL_METRICS = process.env.POKU_REACT_ENABLE_METRICS;
+
+const restoreEnvBaseline = () => {
+ if (typeof ORIGINAL_DOM_URL === 'undefined') {
+ delete process.env.POKU_REACT_DOM_URL;
+ } else {
+ process.env.POKU_REACT_DOM_URL = ORIGINAL_DOM_URL;
+ }
+
+ if (typeof ORIGINAL_METRICS === 'undefined') {
+ delete process.env.POKU_REACT_ENABLE_METRICS;
+ } else {
+ process.env.POKU_REACT_ENABLE_METRICS = ORIGINAL_METRICS;
+ }
+};
+
+test('normalizes metrics defaults when disabled', () => {
+ const normalized = __internal.normalizeMetricsOptions(undefined);
+
+ assert.strictEqual(normalized.enabled, false);
+ assert.strictEqual(normalized.topN, 5);
+ assert.strictEqual(normalized.minDurationMs, 0);
+});
+
+test('normalizes metrics with option object', () => {
+ const reporterCalls: number[] = [];
+ const normalized = __internal.normalizeMetricsOptions({
+ enabled: true,
+ topN: 9.8,
+ minDurationMs: 2.5,
+ reporter(summary) {
+ reporterCalls.push(summary.totalCaptured);
+ },
+ });
+
+ assert.strictEqual(normalized.enabled, true);
+ assert.strictEqual(normalized.topN, 9);
+ assert.strictEqual(normalized.minDurationMs, 2.5);
+ assert.strictEqual(typeof normalized.reporter, 'function');
+ assert.strictEqual(reporterCalls.length, 0);
+});
+
+test('buildRunnerCommand injects tsx and dom setup for node', () => {
+ const result = __internal.buildRunnerCommand({
+ runtime: 'node',
+ command: ['node', '--trace-warnings', 'tests/example.test.tsx'],
+ file: 'tests/example.test.tsx',
+ domSetupPath: '/tmp/dom-setup.ts',
+ });
+
+ assert.strictEqual(result.shouldHandle, true);
+ assert.deepStrictEqual(result.command, [
+ 'node',
+ '--trace-warnings',
+ '--import=tsx',
+ '--import=/tmp/dom-setup.ts',
+ 'tests/example.test.tsx',
+ ]);
+});
+
+test('buildRunnerCommand injects dom setup for bun without tsx import', () => {
+ const result = __internal.buildRunnerCommand({
+ runtime: 'bun',
+ command: ['bun', 'tests/example.test.tsx'],
+ file: 'tests/example.test.tsx',
+ domSetupPath: '/tmp/dom-setup.ts',
+ });
+
+ assert.strictEqual(result.shouldHandle, true);
+ assert.deepStrictEqual(result.command, [
+ 'bun',
+ '--import=/tmp/dom-setup.ts',
+ 'tests/example.test.tsx',
+ ]);
+});
+
+test('buildRunnerCommand injects preload for deno', () => {
+ const result = __internal.buildRunnerCommand({
+ runtime: 'deno',
+ command: ['deno', 'run', '-A', 'tests/example.test.tsx'],
+ file: 'tests/example.test.tsx',
+ domSetupPath: '/tmp/dom-setup.ts',
+ });
+
+ assert.strictEqual(result.shouldHandle, true);
+ assert.deepStrictEqual(result.command, [
+ 'deno',
+ 'run',
+ '-A',
+ '--preload=/tmp/dom-setup.ts',
+ 'tests/example.test.tsx',
+ ]);
+});
+
+test('buildRunnerCommand leaves unsupported runtime unchanged', () => {
+ const original = ['python', 'tests/example.test.tsx'];
+ const result = __internal.buildRunnerCommand({
+ runtime: 'python',
+ command: original,
+ file: 'tests/example.test.tsx',
+ domSetupPath: '/tmp/dom-setup.ts',
+ });
+
+ assert.strictEqual(result.shouldHandle, false);
+ assert.deepStrictEqual(result.command, original);
+});
+
+test('buildRunnerCommand leaves non-react extension unchanged', () => {
+ const original = ['node', 'tests/example.test.ts'];
+ const result = __internal.buildRunnerCommand({
+ runtime: 'node',
+ command: original,
+ file: 'tests/example.test.ts',
+ domSetupPath: '/tmp/dom-setup.ts',
+ });
+
+ assert.strictEqual(result.shouldHandle, false);
+ assert.deepStrictEqual(result.command, original);
+});
+
+test('environment helpers apply and restore options', () => {
+ restoreEnvBaseline();
+
+ const snapshot = __internal.captureEnvSnapshot();
+ __internal.applyEnvironmentOptions(
+ { domUrl: 'http://example.local/', metrics: true },
+ __internal.normalizeMetricsOptions(true),
+ );
+
+ assert.strictEqual(process.env.POKU_REACT_DOM_URL, 'http://example.local/');
+ assert.strictEqual(process.env.POKU_REACT_ENABLE_METRICS, '1');
+
+ __internal.restoreEnvironmentOptions(snapshot);
+
+ assert.strictEqual(process.env.POKU_REACT_DOM_URL, ORIGINAL_DOM_URL);
+ assert.strictEqual(process.env.POKU_REACT_ENABLE_METRICS, ORIGINAL_METRICS);
+});
+
+test('createMetricsSummary returns ordered top metrics with filters', () => {
+ const metrics = [
+ { file: 'a', componentName: 'A', durationMs: 0.4 },
+ { file: 'b', componentName: 'B', durationMs: 4.2 },
+ { file: 'c', componentName: 'C', durationMs: 3.3 },
+ ];
+
+ const summary = __internal.createMetricsSummary(
+ metrics,
+ __internal.normalizeMetricsOptions({ enabled: true, topN: 2, minDurationMs: 1 }),
+ );
+
+ assert.ok(summary);
+ assert.strictEqual(summary?.totalCaptured, 3);
+ assert.strictEqual(summary?.totalReported, 2);
+ assert.deepStrictEqual(
+ summary?.topSlowest.map((item) => item.componentName),
+ ['B', 'C'],
+ );
+});
+
+test('createMetricsSummary returns null when disabled', () => {
+ const summary = __internal.createMetricsSummary(
+ [{ file: 'a', componentName: 'A', durationMs: 8 }],
+ __internal.normalizeMetricsOptions(false),
+ );
+
+ assert.strictEqual(summary, null);
+});
+
+test('getComponentName falls back for non-string values', () => {
+ assert.strictEqual(__internal.getComponentName('MyComp'), 'MyComp');
+ assert.strictEqual(__internal.getComponentName(''), 'AnonymousComponent');
+ assert.strictEqual(__internal.getComponentName(null), 'AnonymousComponent');
+});
+
+test('isRenderMetricMessage validates expected payloads', () => {
+ assert.strictEqual(__internal.isRenderMetricMessage({ type: 'POKU_REACT_RENDER_METRIC' }), true);
+ assert.strictEqual(__internal.isRenderMetricMessage({ type: 'OTHER' }), false);
+ assert.strictEqual(__internal.isRenderMetricMessage(null), false);
+});
+
+test('resolveDomSetupPath resolves built-in and custom adapters', () => {
+ const happyPath = __internal.resolveDomSetupPath('happy-dom');
+ const jsdomPath = __internal.resolveDomSetupPath('jsdom');
+ const customPath = __internal.resolveDomSetupPath({ setupModule: 'tests/setup/custom.ts' });
+
+ assert.ok(happyPath.includes('dom-setup-happy'));
+ assert.ok(jsdomPath.includes('dom-setup-jsdom'));
+ assert.ok(customPath.endsWith('/tests/setup/custom.ts'));
+});