Skip to content
Closed
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
14 changes: 14 additions & 0 deletions e2e/soft-mode-happy-dom/fixtures/rstest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig } from '@rstest/core';

export default defineConfig({
experiments: {
softMode: true,
},
testEnvironment: 'happy-dom',
// Force single worker so file-a always precedes file-b — assertions
// about cross-file state reset are deterministic.
pool: {
maxWorkers: 1,
},
setupFiles: ['./test/setup.ts'],
});
20 changes: 20 additions & 0 deletions e2e/soft-mode-happy-dom/fixtures/test/a-init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from '@rstest/core';

declare global {
// eslint-disable-next-line no-var
var __soft_worker_pid__: number | undefined;
}

globalThis.__soft_worker_pid__ ??= process.pid;

describe('soft mode happy-dom — file A', () => {
it('owns a fresh DOM body', () => {
expect(document.body.innerHTML).toBe('');
document.body.innerHTML = '<div id="from-a">x</div>';
});

it('mutates storage', () => {
expect(localStorage.length).toBe(0);
localStorage.setItem('from-a', 'leaked-if-soft-reset-broken');
});
});
33 changes: 33 additions & 0 deletions e2e/soft-mode-happy-dom/fixtures/test/b-verify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from '@rstest/core';

declare global {
// eslint-disable-next-line no-var
var __soft_worker_pid__: number | undefined;
}

describe('soft mode happy-dom — file B', () => {
it('runs in the same worker as file A', () => {
expect(globalThis.__soft_worker_pid__).toBe(process.pid);
});

it('starts with a clean DOM body', () => {
expect(document.body.innerHTML).toBe('');
expect(document.querySelector('#from-a')).toBeNull();
});

it('starts with clean storage', () => {
expect(localStorage.length).toBe(0);
expect(localStorage.getItem('from-a')).toBeNull();
});

it('HTMLElement.prototype.focus descriptor restored across files', () => {
// setup.ts records the descriptor SHAPE on each module re-eval BEFORE
// re-patching. With softMode handling happy-dom, file-a saw 'value'
// (fresh DOM) and file-b also saw 'value' because softResetEnv's
// protoSnapshot restore wiped file-a's getter-only patch.
const history = (
globalThis as { __soft_focus_descriptor_history__?: Array<string> }
).__soft_focus_descriptor_history__;
expect(history).toEqual(['value', 'value']);
});
});
38 changes: 38 additions & 0 deletions e2e/soft-mode-happy-dom/fixtures/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Mirrors the jsdom fixture's setup-spy.ts but for happy-dom. happy-dom
* exposes the same Element/HTMLElement/Node globals jsdom does, so the
* vendor-style focus monkey-patch + descriptor-history recording is
* portable. softResetEnv's protoSnapshot path handles both env names.
*/
if (typeof HTMLElement !== 'undefined') {
const preDescriptor = Object.getOwnPropertyDescriptor(
HTMLElement.prototype,
'focus',
);
(
globalThis as { __soft_focus_descriptor_history__?: Array<string> }
).__soft_focus_descriptor_history__ ??= [];
(
globalThis as { __soft_focus_descriptor_history__?: Array<string> }
).__soft_focus_descriptor_history__!.push(
!preDescriptor
? 'missing'
: 'value' in preDescriptor && typeof preDescriptor.value === 'function'
? 'value'
: preDescriptor.get && !preDescriptor.set
? 'get-only'
: 'other',
);

const originalFocus = HTMLElement.prototype.focus;
Object.defineProperty(HTMLElement.prototype, 'focus', {
configurable: true,
get: () =>
function patchedFocus(this: HTMLElement, options?: FocusOptions) {
(globalThis as { __soft_focus_calls__?: number }).__soft_focus_calls__ =
((globalThis as { __soft_focus_calls__?: number })
.__soft_focus_calls__ ?? 0) + 1;
return originalFocus.call(this, options);
},
});
}
29 changes: 29 additions & 0 deletions e2e/soft-mode-happy-dom/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, it } from '@rstest/core';
import { runRstestCli } from '../scripts/';

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

describe('experiments.softMode — happy-dom', () => {
it('resets DOM/storage/prototype state between files (happy-dom env)', async ({
onTestFinished,
}) => {
// Mirrors the jsdom soft-mode fixture for happy-dom. Verifies that
// `softResetEnv` and the prototype-snapshot path handle the
// `happy-dom` env-name branch the same way they handle `jsdom`.
const { expectExecSuccess } = await runRstestCli({
command: 'rstest',
args: ['run'],
onTestFinished,
options: {
nodeOptions: {
cwd: join(__dirname, './fixtures'),
},
},
});

await expectExecSuccess();
});
});
15 changes: 15 additions & 0 deletions e2e/soft-mode-recycle/fixtures/rstest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineConfig } from '@rstest/core';

export default defineConfig({
// maxFilesPerWorker: 1 disposes the runner after every task, so each
// file gets a fresh worker process. With pool.maxWorkers: 2 and 4 files,
// every file's `process.pid` must be unique — the driver asserts that
// pidcount === filecount. Default soft-mode reuse would yield fewer
// pids than files (covered by e2e/soft-mode-reuse).
experiments: {
softMode: { maxFilesPerWorker: 1 },
},
pool: {
maxWorkers: 2,
},
});
10 changes: 10 additions & 0 deletions e2e/soft-mode-recycle/fixtures/test/f1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from '@rstest/core';
import { recordPid } from './record-pid';

recordPid('f1');

describe('soft mode recycle — file 1', () => {
it('runs and records its pid', () => {
expect(true).toBe(true);
});
});
10 changes: 10 additions & 0 deletions e2e/soft-mode-recycle/fixtures/test/f2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from '@rstest/core';
import { recordPid } from './record-pid';

recordPid('f2');

describe('soft mode recycle — file 2', () => {
it('runs and records its pid', () => {
expect(true).toBe(true);
});
});
10 changes: 10 additions & 0 deletions e2e/soft-mode-recycle/fixtures/test/f3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from '@rstest/core';
import { recordPid } from './record-pid';

recordPid('f3');

describe('soft mode recycle — file 3', () => {
it('runs and records its pid', () => {
expect(true).toBe(true);
});
});
10 changes: 10 additions & 0 deletions e2e/soft-mode-recycle/fixtures/test/f4.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from '@rstest/core';
import { recordPid } from './record-pid';

recordPid('f4');

describe('soft mode recycle — file 4', () => {
it('runs and records its pid', () => {
expect(true).toBe(true);
});
});
17 changes: 17 additions & 0 deletions e2e/soft-mode-recycle/fixtures/test/record-pid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { appendFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';

/**
* Record this file's `process.pid` + the file's tag into a shared log.
* The driver reads this back after the fixture run and asserts the pid
* count matches the file count — proving the runner was disposed after
* each task (the configured `maxFilesPerWorker: 1` cap).
*/
export const PID_LOG_PATH =
process.env.RSTEST_SOFT_RECYCLE_LOG ??
join(tmpdir(), 'rstest-soft-mode-recycle.log');

export const recordPid = (fileTag: string): void => {
appendFileSync(PID_LOG_PATH, `${process.pid}\t${fileTag}\n`);
};
50 changes: 50 additions & 0 deletions e2e/soft-mode-recycle/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from '@rstest/core';
import { runRstestCli } from '../scripts/';

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

describe('experiments.softMode — maxFilesPerWorker recycle', () => {
it('disposes the runner after each task when maxFilesPerWorker=1', async ({
onTestFinished,
}) => {
const logPath = join(
tmpdir(),
`rstest-soft-mode-recycle-${process.pid}-${Date.now()}.log`,
);
onTestFinished(() => {
if (existsSync(logPath)) unlinkSync(logPath);
});

const { expectExecSuccess } = await runRstestCli({
command: 'rstest',
args: ['run'],
onTestFinished,
options: {
nodeOptions: {
cwd: join(__dirname, './fixtures'),
env: {
RSTEST_SOFT_RECYCLE_LOG: logPath,
},
},
},
});
await expectExecSuccess();

// 4 fixture files × maxFilesPerWorker=1 → every runner is disposed
// after a single task, so every file MUST observe a unique pid. If
// recycling were broken (cap ignored or off-by-one), some pair of
// files would share a pid and this assertion would catch it.
const lines = readFileSync(logPath, 'utf8')
.split('\n')
.filter((l) => l.trim().length > 0);
expect(lines.length).toBe(4);

const pids = new Set(lines.map((line) => line.split('\t')[0]));
expect(pids.size).toBe(4);
});
});
14 changes: 14 additions & 0 deletions e2e/soft-mode-reuse/fixtures/rstest.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig } from '@rstest/core';

export default defineConfig({
experiments: {
softMode: true,
},
// 4 test files + 2 workers → at least one worker handles ≥2 files,
// so at least one pair of files must share `process.pid`. The driver
// (e2e/soft-mode-reuse/index.test.ts) asserts the worker-reuse
// invariant after the fixture run by reading the recorded pids back.
pool: {
maxWorkers: 2,
},
});
10 changes: 10 additions & 0 deletions e2e/soft-mode-reuse/fixtures/test/f1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from '@rstest/core';
import { recordPid } from './record-pid';

recordPid('f1');

describe('soft mode reuse — file 1', () => {
it('runs and records its pid', () => {
expect(true).toBe(true);
});
});
10 changes: 10 additions & 0 deletions e2e/soft-mode-reuse/fixtures/test/f2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from '@rstest/core';
import { recordPid } from './record-pid';

recordPid('f2');

describe('soft mode reuse — file 2', () => {
it('runs and records its pid', () => {
expect(true).toBe(true);
});
});
10 changes: 10 additions & 0 deletions e2e/soft-mode-reuse/fixtures/test/f3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from '@rstest/core';
import { recordPid } from './record-pid';

recordPid('f3');

describe('soft mode reuse — file 3', () => {
it('runs and records its pid', () => {
expect(true).toBe(true);
});
});
10 changes: 10 additions & 0 deletions e2e/soft-mode-reuse/fixtures/test/f4.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from '@rstest/core';
import { recordPid } from './record-pid';

recordPid('f4');

describe('soft mode reuse — file 4', () => {
it('runs and records its pid', () => {
expect(true).toBe(true);
});
});
20 changes: 20 additions & 0 deletions e2e/soft-mode-reuse/fixtures/test/record-pid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { appendFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';

/**
* Record this file's process.pid + the file's name into a shared log
* file. The driver test reads the log after the fixture finishes and
* asserts that fewer unique pids exist than files — proving the worker
* pool reused workers across files.
*
* Cannot use `globalThis` because each worker process has its own
* global, so a counter there is per-worker not workspace-wide.
*/
export const PID_LOG_PATH =
process.env.RSTEST_SOFT_REUSE_LOG ??
join(tmpdir(), 'rstest-soft-mode-reuse.log');

export const recordPid = (fileTag: string): void => {
appendFileSync(PID_LOG_PATH, `${process.pid}\t${fileTag}\n`);
};
Loading
Loading