@rstest/core 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/0~1472.js CHANGED
@@ -4,7 +4,7 @@ import { pathToFileURL } from "./6198.js";
4
4
  import "./1157.js";
5
5
  import { logger as logger_logger, color } from "./3160.js";
6
6
  async function loadBrowserModule(options = {}) {
7
- const coreVersion = "0.8.3";
7
+ const coreVersion = "0.8.4";
8
8
  const { projectRoots = [] } = options;
9
9
  let browserModule;
10
10
  let browserVersion;
package/dist/0~2173.js CHANGED
@@ -147,9 +147,10 @@ async function setupCliShortcuts({ closeServer, runAll, updateSnapshot, runFaile
147
147
  }
148
148
  async function runBrowserModeTests(context, browserProjects, options) {
149
149
  const projectRoots = browserProjects.map((p)=>p.rootPath);
150
- const { runBrowserTests } = await loadBrowserModule({
150
+ const { validateBrowserConfig, runBrowserTests } = await loadBrowserModule({
151
151
  projectRoots
152
152
  });
153
+ validateBrowserConfig(context);
153
154
  return runBrowserTests(context, options);
154
155
  }
155
156
  async function runTests(context) {
@@ -165,7 +166,7 @@ async function runTests(context) {
165
166
  const browserResult = await runBrowserModeTests(context, browserProjects, {
166
167
  skipOnTestRunEnd: false
167
168
  });
168
- if (coverage.enabled && browserResult?.results) {
169
+ if (coverage.enabled && browserResult?.results.length && !browserResult.unhandledErrors?.length) {
169
170
  const coverageProvider = await createCoverageProvider(coverage, context.rootPath);
170
171
  if (coverageProvider) {
171
172
  const { generateCoverage } = await import("./0~4403.js").then((mod)=>({
@@ -213,6 +214,7 @@ async function runTests(context) {
213
214
  skipOnTestRunEnd: shouldUnifyReporter,
214
215
  shardedEntries: shard ? browserEntries : void 0
215
216
  });
217
+ browserResultPromise.catch(()=>{});
216
218
  }
217
219
  if (!hasNodeTestsToRun) {
218
220
  if (browserResultPromise) await browserResultPromise;
@@ -338,6 +340,7 @@ async function runTests(context) {
338
340
  const errors = returns.flatMap((r)=>r.errors || []);
339
341
  if (shouldUnifyReporter && browserResult?.results) results.push(...browserResult.results);
340
342
  if (shouldUnifyReporter && browserResult?.testResults) testResults.push(...browserResult.testResults);
343
+ if (shouldUnifyReporter && browserResult?.unhandledErrors) errors.push(...browserResult.unhandledErrors);
341
344
  context.updateReporterResultState(results, testResults, currentDeletedEntries);
342
345
  const nodeHasFailure = results.some((r)=>'fail' === r.status) || errors.length;
343
346
  const browserHasFailure = shouldUnifyReporter && browserResult?.hasFailure;
package/dist/0~5835.js CHANGED
@@ -6,10 +6,6 @@ import { pathToFileURL } from "./6198.js";
6
6
  import { node_vm, asModule, shouldInterop, interopModule } from "./0~3346.js";
7
7
  import { posix } from "./7011.js";
8
8
  const external_node_path_ = __webpack_require__("node:path");
9
- let latestAssetFiles = {};
10
- const updateLatestAssetFiles = (assetFiles)=>{
11
- latestAssetFiles = assetFiles;
12
- };
13
9
  const isRelativePath = (p)=>/^\.\.?\//.test(p);
14
10
  const createRequire = (filename, distPath, rstestContext, assetFiles, interopDefault)=>{
15
11
  const _require = (()=>{
@@ -22,7 +18,7 @@ const createRequire = (filename, distPath, rstestContext, assetFiles, interopDef
22
18
  const require = (id)=>{
23
19
  const currentDirectory = posix.dirname(distPath);
24
20
  const joinedPath = isRelativePath(id) ? posix.join(currentDirectory, id) : id;
25
- const content = assetFiles[joinedPath] || latestAssetFiles[joinedPath];
21
+ const content = assetFiles[joinedPath];
26
22
  if (content) try {
27
23
  return cacheableLoadModule({
28
24
  codeContent: content,
@@ -104,7 +100,7 @@ const loadModule = ({ codeContent, distPath, testPath, rstestContext, assetFiles
104
100
  require: createRequire(testPath, distPath, rstestContext, assetFiles, interopDefault),
105
101
  readWasmFile: (wasmPath, callback)=>{
106
102
  const joinedPath = isRelativePath(wasmPath) ? posix.join(posix.dirname(distPath), wasmPath) : wasmPath;
107
- const content = assetFiles[posix.normalize(joinedPath)] || latestAssetFiles[posix.normalize(joinedPath)];
103
+ const content = assetFiles[posix.normalize(joinedPath)];
108
104
  if (content) callback(null, Buffer.from(content, 'base64'));
109
105
  else callback(new Error(`WASM file ${joinedPath} not found in asset files.`));
110
106
  },
@@ -145,4 +141,5 @@ const cacheableLoadModule = ({ codeContent, distPath, testPath, rstestContext, a
145
141
  moduleCache.set(testPath, mod);
146
142
  return mod;
147
143
  };
148
- export { cacheableLoadModule, loadModule, updateLatestAssetFiles };
144
+ const clearModuleCache = ()=>moduleCache.clear();
145
+ export { cacheableLoadModule, clearModuleCache, loadModule };
package/dist/0~6923.js CHANGED
@@ -12,14 +12,10 @@ var loadEsModule_EsmMode = /*#__PURE__*/ function(EsmMode) {
12
12
  return EsmMode;
13
13
  }({});
14
14
  const isRelativePath = (p)=>/^\.\.?\//.test(p);
15
- let latestAssetFiles = {};
16
- const updateLatestAssetFiles = (assetFiles)=>{
17
- latestAssetFiles = assetFiles;
18
- };
19
15
  const defineRstestDynamicImport = ({ distPath, testPath, assetFiles, interopDefault, returnModule, esmMode })=>async (specifier, importAttributes)=>{
20
16
  const currentDirectory = posix.dirname(distPath);
21
17
  const joinedPath = isRelativePath(specifier) ? posix.join(currentDirectory, specifier) : specifier;
22
- const content = assetFiles[joinedPath] || latestAssetFiles[joinedPath];
18
+ const content = assetFiles[joinedPath];
23
19
  if (content) try {
24
20
  return await loadModule({
25
21
  codeContent: content,
@@ -117,7 +113,7 @@ const loadModule = async ({ codeContent, distPath, testPath, assetFiles, interop
117
113
  });
118
114
  meta.readWasmFile = (wasmPath, callback)=>{
119
115
  const joinedPath = isRelativePath(wasmPath.pathname) ? posix.join(posix.dirname(distPath), wasmPath.pathname) : wasmPath.pathname;
120
- const content = assetFiles[posix.normalize(joinedPath)] || latestAssetFiles[posix.normalize(joinedPath)];
116
+ const content = assetFiles[posix.normalize(joinedPath)];
121
117
  if (content) callback(null, Buffer.from(content, 'base64'));
122
118
  else callback(new Error(`WASM file ${joinedPath} not found in asset files.`));
123
119
  };
@@ -150,4 +146,5 @@ const loadModule = async ({ codeContent, distPath, testPath, assetFiles, interop
150
146
  const ns = esm.namespace;
151
147
  return ns.default && ns.default instanceof Promise ? ns.default : ns;
152
148
  };
153
- export { asModule, loadEsModule_EsmMode, loadModule, updateLatestAssetFiles };
149
+ const clearModuleCache = ()=>esmCache.clear();
150
+ export { asModule, clearModuleCache, loadEsModule_EsmMode, loadModule };
package/dist/0~7882.js CHANGED
@@ -372,7 +372,7 @@ async function createInteractive(cwd, projectInfo, isAgent) {
372
372
  }
373
373
  const provider = providerSelection;
374
374
  const preview = computeFilePreview(cwd, projectInfo);
375
- const deps = getDependenciesWithVersions(effectiveFramework, provider, "0.8.3");
375
+ const deps = getDependenciesWithVersions(effectiveFramework, provider, "0.8.4");
376
376
  const depsList = Object.entries(deps).map(([name, version])=>`${name}@${version}`).join(', ');
377
377
  const previewLines = [
378
378
  `${color.cyan('+')} Create ${preview.configFile}`,
@@ -450,7 +450,7 @@ async function generateFiles(cwd, projectInfo, provider) {
450
450
  updatePackageJsonScripts(cwd, {
451
451
  'test:browser': 'rstest --config=rstest.browser.config.ts'
452
452
  });
453
- const deps = getDependenciesWithVersions(effectiveFramework, provider, "0.8.3");
453
+ const deps = getDependenciesWithVersions(effectiveFramework, provider, "0.8.4");
454
454
  updatePackageJsonDevDeps(cwd, deps);
455
455
  return createdFiles;
456
456
  }
package/dist/0~89.js CHANGED
@@ -5,7 +5,7 @@ import { Tinypool } from "tinypool";
5
5
  import node_inspector from "node:inspector";
6
6
  import { basename, needFlagExperimentalDetectModule, isDeno, dirname, castArray, resolve as pathe_M_eThtNZ_resolve, serializableConfig, node_process, isDebug, color, getForceColorEnv, ADDITIONAL_NODE_BUILTINS, bgColor, join } from "./3160.js";
7
7
  import { fileURLToPath } from "./6198.js";
8
- import { node_v8, createBirpc } from "./3216.js";
8
+ import { node_v8, parseWorkerMetaMessage, createBirpc } from "./9869.js";
9
9
  import { TEMP_RSTEST_OUTPUT_DIR, TEMP_RSTEST_OUTPUT_DIR_GLOB } from "./1157.js";
10
10
  import { posix } from "./7011.js";
11
11
  import { isBuiltin } from "./4881.js";
@@ -21,9 +21,184 @@ function memory_isMemorySufficient(options) {
21
21
  const isMemorySufficient = memoryUsageRatio < memoryThreshold && heapUsed < maxHeapSize;
22
22
  return isMemorySufficient;
23
23
  }
24
+ const MAX_CAPTURED_STDERR_BYTES = 131072;
25
+ const STDERR_SETTLE_MAX_WAIT = 200;
26
+ const WORKER_EXIT_ERROR = 'Worker exited unexpectedly';
27
+ const MAX_STDERR_MESSAGE_BYTES = 65536;
28
+ const createDeferred = ()=>{
29
+ let resolvePromise = ()=>void 0;
30
+ const deferred = {
31
+ settled: false,
32
+ resolve (value) {
33
+ if (deferred.settled) return;
34
+ deferred.settled = true;
35
+ resolvePromise(value);
36
+ },
37
+ promise: new Promise((resolve)=>{
38
+ resolvePromise = resolve;
39
+ })
40
+ };
41
+ return deferred;
42
+ };
43
+ const withTimeout = (promise, timeout)=>new Promise((resolve)=>{
44
+ let finished = false;
45
+ const timer = setTimeout(()=>{
46
+ if (finished) return;
47
+ finished = true;
48
+ resolve(void 0);
49
+ }, timeout);
50
+ timer.unref();
51
+ promise.then((value)=>{
52
+ if (finished) return;
53
+ finished = true;
54
+ clearTimeout(timer);
55
+ resolve(value);
56
+ }, ()=>{
57
+ if (finished) return;
58
+ finished = true;
59
+ clearTimeout(timer);
60
+ resolve(void 0);
61
+ });
62
+ });
63
+ const formatCapturedStderr = (text)=>{
64
+ const bytes = Buffer.byteLength(text);
65
+ if (bytes <= MAX_STDERR_MESSAGE_BYTES) return text;
66
+ const half = Math.floor(MAX_STDERR_MESSAGE_BYTES / 2);
67
+ const head = text.slice(0, half);
68
+ const tail = text.slice(-half);
69
+ const hiddenBytes = bytes - Buffer.byteLength(head) - Buffer.byteLength(tail);
70
+ return `${head}\n\n... [truncated ${hiddenBytes} bytes of stderr] ...\n\n${tail}`;
71
+ };
72
+ const getChildProcessByPid = (pool, pid)=>{
73
+ for (const worker of pool.threads){
74
+ const childProcess = worker.process;
75
+ if (childProcess?.pid === pid) return childProcess;
76
+ }
77
+ };
78
+ const createWorkerStderrCapture = (pool)=>{
79
+ const workerStates = new Map();
80
+ const taskBindings = new Map();
81
+ const taskBindingWaiters = new Map();
82
+ const workerCloseWaiters = new Map();
83
+ const trackedWorkers = new Map();
84
+ const getWorkerState = (pid)=>{
85
+ let state = workerStates.get(pid);
86
+ if (!state) {
87
+ state = {
88
+ bytes: 0,
89
+ chunks: [],
90
+ seq: 0
91
+ };
92
+ workerStates.set(pid, state);
93
+ }
94
+ return state;
95
+ };
96
+ const appendStderr = (pid, text)=>{
97
+ if (!text) return;
98
+ const state = getWorkerState(pid);
99
+ const bytes = Buffer.byteLength(text);
100
+ state.seq += 1;
101
+ state.bytes += bytes;
102
+ state.chunks.push({
103
+ bytes,
104
+ seq: state.seq,
105
+ text
106
+ });
107
+ while(state.bytes > MAX_CAPTURED_STDERR_BYTES && state.chunks.length > 0){
108
+ const dropped = state.chunks.shift();
109
+ if (dropped) state.bytes -= dropped.bytes;
110
+ }
111
+ };
112
+ const attachWorker = (pid)=>{
113
+ if (trackedWorkers.has(pid)) return;
114
+ const childProcess = getChildProcessByPid(pool, pid);
115
+ const stderr = childProcess?.stderr;
116
+ if (!childProcess || !stderr) return;
117
+ const closeWaiter = createDeferred();
118
+ workerCloseWaiters.set(pid, closeWaiter);
119
+ const onData = (chunk)=>{
120
+ const text = 'string' == typeof chunk ? chunk : chunk.toString();
121
+ appendStderr(pid, text);
122
+ };
123
+ stderr.on('data', onData);
124
+ trackedWorkers.set(pid, {
125
+ onData,
126
+ stream: stderr
127
+ });
128
+ const detachTrackedWorker = ()=>{
129
+ const tracked = trackedWorkers.get(pid);
130
+ if (!tracked) return;
131
+ tracked.stream.off('data', tracked.onData);
132
+ trackedWorkers.delete(pid);
133
+ };
134
+ childProcess.once('close', ()=>{
135
+ detachTrackedWorker();
136
+ closeWaiter.resolve(void 0);
137
+ });
138
+ };
139
+ const bindTaskToPid = (taskId, pid)=>{
140
+ attachWorker(pid);
141
+ const binding = {
142
+ pid,
143
+ startSeq: getWorkerState(pid).seq
144
+ };
145
+ taskBindings.set(taskId, binding);
146
+ taskBindingWaiters.get(taskId)?.resolve(binding);
147
+ };
148
+ const waitForTaskBinding = async (taskId)=>{
149
+ const binding = taskBindings.get(taskId);
150
+ if (binding) return binding;
151
+ const waiter = taskBindingWaiters.get(taskId);
152
+ if (!waiter) return;
153
+ return withTimeout(waiter.promise, STDERR_SETTLE_MAX_WAIT);
154
+ };
155
+ const waitForWorkerClose = async (pid)=>{
156
+ attachWorker(pid);
157
+ const waiter = workerCloseWaiters.get(pid);
158
+ if (!waiter) return;
159
+ await withTimeout(waiter.promise, STDERR_SETTLE_MAX_WAIT);
160
+ };
161
+ const getTaskStderr = (taskId)=>{
162
+ const binding = taskBindings.get(taskId);
163
+ if (!binding) return '';
164
+ const state = workerStates.get(binding.pid);
165
+ if (!state || 0 === state.chunks.length) return '';
166
+ return state.chunks.filter((chunk)=>chunk.seq > binding.startSeq).map((chunk)=>chunk.text).join('').trim();
167
+ };
168
+ const enhanceWorkerExitError = async (taskId, err)=>{
169
+ if (!(err instanceof Error) || !err.message.includes(WORKER_EXIT_ERROR)) return;
170
+ const binding = taskBindings.get(taskId) ?? await waitForTaskBinding(taskId);
171
+ if (!binding) return;
172
+ await waitForWorkerClose(binding.pid);
173
+ const stderr = formatCapturedStderr(getTaskStderr(taskId));
174
+ if (stderr.length > 0) err.message += `\n\nMaybe related stderr:\n${stderr}`;
175
+ };
176
+ const createTask = (taskId)=>{
177
+ if (!taskBindingWaiters.has(taskId)) taskBindingWaiters.set(taskId, createDeferred());
178
+ };
179
+ const clearTask = (taskId)=>{
180
+ taskBindings.delete(taskId);
181
+ taskBindingWaiters.delete(taskId);
182
+ };
183
+ const cleanup = ()=>{
184
+ for (const { onData, stream } of trackedWorkers.values())stream.off('data', onData);
185
+ trackedWorkers.clear();
186
+ workerStates.clear();
187
+ taskBindings.clear();
188
+ taskBindingWaiters.clear();
189
+ workerCloseWaiters.clear();
190
+ };
191
+ return {
192
+ createTask,
193
+ bindTaskToPid,
194
+ clearTask,
195
+ enhanceWorkerExitError,
196
+ cleanup
197
+ };
198
+ };
24
199
  const forks_filename = fileURLToPath(import.meta.url);
25
200
  const forks_dirname = dirname(forks_filename);
26
- function createChannel(rpcMethods) {
201
+ function createChannel(rpcMethods, onWorkerMeta) {
27
202
  const emitter = new node_events();
28
203
  const cleanup = ()=>emitter.removeAllListeners();
29
204
  const events = {
@@ -35,6 +210,8 @@ function createChannel(rpcMethods) {
35
210
  emitter.on(events.message, callback);
36
211
  },
37
212
  postMessage: (message)=>{
213
+ const workerMeta = parseWorkerMetaMessage(message);
214
+ if (workerMeta) return void onWorkerMeta?.(workerMeta);
38
215
  emitter.emit(events.response, message);
39
216
  }
40
217
  };
@@ -66,29 +243,50 @@ const createForksPool = (poolOptions)=>{
66
243
  isolateWorkers: isolate
67
244
  };
68
245
  const pool = new Tinypool(options);
246
+ const stderrCapture = createWorkerStderrCapture(pool);
247
+ let nextTaskId = 0;
69
248
  return {
70
249
  name: 'forks',
71
250
  runTest: async ({ options, rpcMethods })=>{
72
- const { channel, cleanup } = createChannel(rpcMethods);
251
+ const taskId = ++nextTaskId;
252
+ stderrCapture.createTask(taskId);
253
+ const { channel, cleanup } = createChannel(rpcMethods, ({ pid })=>{
254
+ stderrCapture.bindTaskToPid(taskId, pid);
255
+ });
73
256
  try {
74
257
  return await pool.run(options, {
75
258
  channel
76
259
  });
260
+ } catch (err) {
261
+ await stderrCapture.enhanceWorkerExitError(taskId, err);
262
+ throw err;
77
263
  } finally{
78
264
  cleanup();
265
+ stderrCapture.clearTask(taskId);
79
266
  }
80
267
  },
81
268
  collectTests: async ({ options, rpcMethods })=>{
82
- const { channel, cleanup } = createChannel(rpcMethods);
269
+ const taskId = ++nextTaskId;
270
+ stderrCapture.createTask(taskId);
271
+ const { channel, cleanup } = createChannel(rpcMethods, ({ pid })=>{
272
+ stderrCapture.bindTaskToPid(taskId, pid);
273
+ });
83
274
  try {
84
275
  return await pool.run(options, {
85
276
  channel
86
277
  });
278
+ } catch (err) {
279
+ await stderrCapture.enhanceWorkerExitError(taskId, err);
280
+ throw err;
87
281
  } finally{
88
282
  cleanup();
283
+ stderrCapture.clearTask(taskId);
89
284
  }
90
285
  },
91
- close: ()=>pool.destroy()
286
+ close: async ()=>{
287
+ stderrCapture.cleanup();
288
+ await pool.destroy();
289
+ }
92
290
  };
93
291
  };
94
292
  const external_node_os_ = __webpack_require__("node:os");
package/dist/0~9634.js CHANGED
@@ -107,9 +107,10 @@ const collectBrowserTests = async ({ context, browserProjects, shardedEntries })
107
107
  loadBrowserModule: mod.loadBrowserModule
108
108
  }));
109
109
  const projectRoots = browserProjects.map((p)=>p.rootPath);
110
- const { listBrowserTests } = await loadBrowserModule({
110
+ const { validateBrowserConfig, listBrowserTests } = await loadBrowserModule({
111
111
  projectRoots
112
112
  });
113
+ validateBrowserConfig(context);
113
114
  return listBrowserTests(context, {
114
115
  shardedEntries
115
116
  });
package/dist/3160.js CHANGED
@@ -1011,19 +1011,12 @@ const isDebug = ()=>{
1011
1011
  ].some((key)=>values.includes(key));
1012
1012
  };
1013
1013
  const getForceColorEnv = ()=>{
1014
- if (void 0 !== process.env.FORCE_COLOR) return {};
1015
- const noColorEnabled = '1' === process.env.NO_COLOR;
1016
- const isAgent = determineAgent().isAgent;
1017
- if (noColorEnabled) return {
1018
- FORCE_COLOR: '0'
1019
- };
1020
- if (isAgent || !picocolors.isColorSupported) return {
1014
+ const userSetColorEnv = void 0 !== process.env.FORCE_COLOR || void 0 !== process.env.NO_COLOR;
1015
+ if (determineAgent().isAgent && !userSetColorEnv) return {
1021
1016
  NO_COLOR: '1',
1022
1017
  FORCE_COLOR: '0'
1023
1018
  };
1024
- return {
1025
- FORCE_COLOR: '1'
1026
- };
1019
+ return {};
1027
1020
  };
1028
1021
  const color = (0, picocolors.createColors)();
1029
1022
  if (isDebug()) src_logger.level = 'verbose';
package/dist/6151.js CHANGED
@@ -13473,6 +13473,14 @@ const initSpy = ()=>{
13473
13473
  }, defaultName, mockFn);
13474
13474
  };
13475
13475
  const spyOn = (obj, methodName, accessType)=>{
13476
+ if (accessType) {
13477
+ const descriptor = Object.getOwnPropertyDescriptor(obj, methodName);
13478
+ const accessor = 'get' === accessType ? Reflect.get(descriptor ?? {}, 'get') : Reflect.get(descriptor ?? {}, 'set');
13479
+ if ('function' == typeof accessor && spy_isMockFunction(accessor)) return accessor;
13480
+ } else {
13481
+ const method = obj[methodName];
13482
+ if (spy_isMockFunction(method)) return method;
13483
+ }
13476
13484
  const accessTypeMap = {
13477
13485
  get: 'getter',
13478
13486
  set: 'setter'
package/dist/9131.js CHANGED
@@ -504,7 +504,7 @@ function prepareCli() {
504
504
  if (!npm_execpath || npm_execpath.includes('npx-cli.js') || npm_execpath.includes('.bun')) logger_logger.log();
505
505
  }
506
506
  function showRstest() {
507
- logger_logger.greet(" Rstest v0.8.3");
507
+ logger_logger.greet(" Rstest v0.8.4");
508
508
  logger_logger.log('');
509
509
  }
510
510
  const applyCommonOptions = (cli)=>{
@@ -562,7 +562,7 @@ const runRest = async ({ options, filters, command })=>{
562
562
  function setupCommands() {
563
563
  const cli = dist('rstest');
564
564
  cli.help();
565
- cli.version("0.8.3");
565
+ cli.version("0.8.4");
566
566
  applyCommonOptions(cli);
567
567
  cli.command('[...filters]', 'run tests').option('-w, --watch', 'Run tests in watch mode').action(async (filters, options)=>{
568
568
  if (!determineAgent().isAgent) showRstest();
@@ -1134,13 +1134,6 @@ const createDefaultConfig = ()=>({
1134
1134
  }
1135
1135
  });
1136
1136
  const withDefaultConfig = (config)=>{
1137
- if (config.browser?.enabled === true) {
1138
- if (!config.browser.provider) throw new Error('browser.provider is required when browser.enabled is true.');
1139
- const supportedProviders = [
1140
- 'playwright'
1141
- ];
1142
- if (!supportedProviders.includes(config.browser.provider)) throw new Error(`browser.provider must be one of: ${supportedProviders.join(', ')}.`);
1143
- }
1144
1137
  const merged = mergeRstestConfig(createDefaultConfig(), config);
1145
1138
  merged.setupFiles = castArray(merged.setupFiles);
1146
1139
  merged.globalSetup = castArray(merged.globalSetup);
@@ -1159,7 +1152,8 @@ const withDefaultConfig = (config)=>{
1159
1152
  browser: merged.browser?.browser ?? 'chromium',
1160
1153
  headless: merged.browser?.headless ?? T,
1161
1154
  port: merged.browser?.port,
1162
- strictPort: merged.browser?.strictPort ?? false
1155
+ strictPort: merged.browser?.strictPort ?? false,
1156
+ viewport: merged.browser?.viewport
1163
1157
  };
1164
1158
  return {
1165
1159
  ...merged,
@@ -1763,13 +1757,9 @@ class DefaultReporter {
1763
1757
  this.projectConfigs = projectConfigs ?? new Map();
1764
1758
  this.options = options;
1765
1759
  this.testState = testState;
1766
- }
1767
- ensureStatusRenderer() {
1768
- if (this.statusRenderer) return;
1769
- if (isTTY() || this.options.logger) this.statusRenderer = new StatusRenderer(this.rootPath, this.testState, this.options.logger);
1760
+ if (isTTY() || options.logger) this.statusRenderer = new StatusRenderer(rootPath, testState, options.logger);
1770
1761
  }
1771
1762
  onTestFileStart() {
1772
- this.ensureStatusRenderer();
1773
1763
  this.statusRenderer?.onTestFileStart();
1774
1764
  }
1775
1765
  onTestFileResult(test) {
@@ -3462,7 +3452,7 @@ class MdReporter {
3462
3452
  }
3463
3453
  renderFrontMatter(lines) {
3464
3454
  const frontMatter = {
3465
- tool: "@rstest/core@0.8.3",
3455
+ tool: "@rstest/core@0.8.4",
3466
3456
  timestamp: new Date().toISOString()
3467
3457
  };
3468
3458
  if (this.options.header.env) frontMatter.runtime = {
@@ -3832,7 +3822,7 @@ class Rstest {
3832
3822
  updateSnapshot: rstestConfig.update ? 'all' : T ? 'none' : 'new'
3833
3823
  });
3834
3824
  this.snapshotManager = snapshotManager;
3835
- this.version = "0.8.3";
3825
+ this.version = "0.8.4";
3836
3826
  this.rootPath = rootPath;
3837
3827
  this.originalConfig = userConfig;
3838
3828
  this.normalizedConfig = rstestConfig;
@@ -1,4 +1,22 @@
1
1
  import "node:module";
2
+ const WORKER_META_MESSAGE_TYPE = 'rstest:worker-meta';
3
+ const WORKER_META_MESSAGE_VERSION = 1;
4
+ const WORKER_META_MESSAGE_NAMESPACE = 'rstest';
5
+ const isRecord = (value)=>'object' == typeof value && null !== value;
6
+ const createWorkerMetaMessage = (pid)=>({
7
+ __rstest_internal__: WORKER_META_MESSAGE_NAMESPACE,
8
+ payload: {
9
+ pid
10
+ },
11
+ type: WORKER_META_MESSAGE_TYPE,
12
+ version: WORKER_META_MESSAGE_VERSION
13
+ });
14
+ const parseWorkerMetaMessage = (message)=>{
15
+ if (!isRecord(message)) return;
16
+ if (message.__rstest_internal__ === WORKER_META_MESSAGE_NAMESPACE && message.type === WORKER_META_MESSAGE_TYPE && message.version === WORKER_META_MESSAGE_VERSION && isRecord(message.payload) && 'number' == typeof message.payload.pid) return {
17
+ pid: message.payload.pid
18
+ };
19
+ };
2
20
  const TYPE_REQUEST = "q";
3
21
  const TYPE_RESPONSE = "s";
4
22
  const DEFAULT_TIMEOUT = 6e4;
@@ -205,4 +223,4 @@ function nanoid(size = 21) {
205
223
  return id;
206
224
  }
207
225
  export { default as node_v8 } from "node:v8";
208
- export { createBirpc };
226
+ export { createBirpc, createWorkerMetaMessage, parseWorkerMetaMessage };
@@ -22392,6 +22392,14 @@ const initSpy = ()=>{
22392
22392
  }, defaultName, mockFn);
22393
22393
  };
22394
22394
  const spyOn = (obj, methodName, accessType)=>{
22395
+ if (accessType) {
22396
+ const descriptor = Object.getOwnPropertyDescriptor(obj, methodName);
22397
+ const accessor = 'get' === accessType ? Reflect.get(descriptor ?? {}, 'get') : Reflect.get(descriptor ?? {}, 'set');
22398
+ if ('function' == typeof accessor && spy_isMockFunction(accessor)) return accessor;
22399
+ } else {
22400
+ const method = obj[methodName];
22401
+ if (spy_isMockFunction(method)) return method;
22402
+ }
22395
22403
  const accessTypeMap = {
22396
22404
  get: 'getter',
22397
22405
  set: 'setter'
@@ -319,6 +319,12 @@ declare type BrowserModeConfig = {
319
319
  * If not specified, a random available port will be used.
320
320
  */
321
321
  port?: number;
322
+ /**
323
+ * Default runner iframe viewport.
324
+ *
325
+ * When not specified, the browser UI fills the preview panel.
326
+ */
327
+ viewport?: BrowserViewport;
322
328
  /**
323
329
  * Whether to exit if the specified port is already in use.
324
330
  *
@@ -336,6 +342,11 @@ declare type BrowserModeConfig = {
336
342
  */
337
343
  declare type BrowserName = 'chromium' | 'firefox' | 'webkit';
338
344
 
345
+ declare type BrowserViewport = {
346
+ width: number;
347
+ height: number;
348
+ } | DevicePreset;
349
+
339
350
  declare type BuiltInReporterNames = 'default' | 'verbose' | 'md' | 'github-actions' | 'junit';
340
351
 
341
352
  declare type BuiltinReporterOptions = {
@@ -633,6 +644,20 @@ declare type DescribeFn = (description: string, fn?: () => void) => void;
633
644
 
634
645
  declare type DescribeForFn = <T>(cases: readonly T[]) => (description: string, fn?: (param: T) => MaybePromise<void>) => void;
635
646
 
647
+ /**
648
+ * Device presets aligned with Chrome DevTools device toolbar.
649
+ *
650
+ * These values are stable identifiers (not user-facing labels).
651
+ *
652
+ * IMPORTANT: Keep this union in sync with
653
+ * `@rstest/browser` preset runtime source:
654
+ * `packages/browser/src/viewportPresets.ts`.
655
+ *
656
+ * `@rstest/core` owns `defineConfig` typing, while `@rstest/browser` owns
657
+ * runtime validation and resolution for preset ids.
658
+ */
659
+ declare type DevicePreset = 'iPhoneSE' | 'iPhoneXR' | 'iPhone12Pro' | 'iPhone14ProMax' | 'Pixel7' | 'SamsungGalaxyS8Plus' | 'SamsungGalaxyS20Ultra' | 'iPadMini' | 'iPadAir' | 'iPadPro' | 'SurfacePro7' | 'SurfaceDuo' | 'GalaxyZFold5' | 'AsusZenbookFold' | 'SamsungGalaxyA51A71' | 'NestHub' | 'NestHubMax';
660
+
636
661
  /** The test file output path */
637
662
  declare type DistPath = string;
638
663
 
@@ -1680,6 +1705,7 @@ declare type NormalizedBrowserModeConfig = {
1680
1705
  headless: boolean;
1681
1706
  port?: number;
1682
1707
  strictPort: boolean;
1708
+ viewport?: BrowserViewport;
1683
1709
  };
1684
1710
 
1685
1711
  declare type NormalizedConfig = Required<Omit<RstestConfig, OptionalKeys | 'pool' | 'projects' | 'coverage' | 'setupFiles' | 'globalSetup' | 'exclude' | 'testEnvironment' | 'browser'>> & Partial<Pick<RstestConfig, OptionalKeys>> & {
package/dist/browser.d.ts CHANGED
@@ -315,6 +315,12 @@ declare type BrowserModeConfig = {
315
315
  * If not specified, a random available port will be used.
316
316
  */
317
317
  port?: number;
318
+ /**
319
+ * Default runner iframe viewport.
320
+ *
321
+ * When not specified, the browser UI fills the preview panel.
322
+ */
323
+ viewport?: BrowserViewport;
318
324
  /**
319
325
  * Whether to exit if the specified port is already in use.
320
326
  *
@@ -367,8 +373,15 @@ export declare interface BrowserTestRunResult {
367
373
  };
368
374
  /** Whether the test run had failures */
369
375
  hasFailure: boolean;
376
+ /** Errors that occurred before/outside test execution (e.g., browser launch failure) */
377
+ unhandledErrors?: Error[];
370
378
  }
371
379
 
380
+ declare type BrowserViewport = {
381
+ width: number;
382
+ height: number;
383
+ } | DevicePreset;
384
+
372
385
  declare type BuiltInReporterNames = 'default' | 'verbose' | 'md' | 'github-actions' | 'junit';
373
386
 
374
387
  declare type BuiltinReporterOptions = {
@@ -766,6 +779,20 @@ declare type DescribeFn = (description: string, fn?: () => void) => void;
766
779
 
767
780
  declare type DescribeForFn = <T>(cases: readonly T[]) => (description: string, fn?: (param: T) => MaybePromise<void>) => void;
768
781
 
782
+ /**
783
+ * Device presets aligned with Chrome DevTools device toolbar.
784
+ *
785
+ * These values are stable identifiers (not user-facing labels).
786
+ *
787
+ * IMPORTANT: Keep this union in sync with
788
+ * `@rstest/browser` preset runtime source:
789
+ * `packages/browser/src/viewportPresets.ts`.
790
+ *
791
+ * `@rstest/core` owns `defineConfig` typing, while `@rstest/browser` owns
792
+ * runtime validation and resolution for preset ids.
793
+ */
794
+ export declare type DevicePreset = 'iPhoneSE' | 'iPhoneXR' | 'iPhone12Pro' | 'iPhone14ProMax' | 'Pixel7' | 'SamsungGalaxyS8Plus' | 'SamsungGalaxyS20Ultra' | 'iPadMini' | 'iPadAir' | 'iPadPro' | 'SurfacePro7' | 'SurfaceDuo' | 'GalaxyZFold5' | 'AsusZenbookFold' | 'SamsungGalaxyA51A71' | 'NestHub' | 'NestHubMax';
795
+
769
796
  /**
770
797
  * @param a Expected value
771
798
  * @param b Received value
@@ -2195,6 +2222,7 @@ declare type NormalizedBrowserModeConfig = {
2195
2222
  headless: boolean;
2196
2223
  port?: number;
2197
2224
  strictPort: boolean;
2225
+ viewport?: BrowserViewport;
2198
2226
  };
2199
2227
 
2200
2228
  declare type NormalizedConfig = Required<Omit<RstestConfig, OptionalKeys | 'pool' | 'projects' | 'coverage' | 'setupFiles' | 'globalSetup' | 'exclude' | 'testEnvironment' | 'browser'>> & Partial<Pick<RstestConfig, OptionalKeys>> & {
@@ -42,12 +42,12 @@ const runGlobalSetup = async (data)=>{
42
42
  const { loadModule } = data.outputModule ? await import("./0~6923.js").then((mod)=>({
43
43
  EsmMode: mod.loadEsModule_EsmMode,
44
44
  asModule: mod.asModule,
45
- loadModule: mod.loadModule,
46
- updateLatestAssetFiles: mod.updateLatestAssetFiles
45
+ clearModuleCache: mod.clearModuleCache,
46
+ loadModule: mod.loadModule
47
47
  })) : await import("./0~5835.js").then((mod)=>({
48
48
  cacheableLoadModule: mod.cacheableLoadModule,
49
- loadModule: mod.loadModule,
50
- updateLatestAssetFiles: mod.updateLatestAssetFiles
49
+ clearModuleCache: mod.clearModuleCache,
50
+ loadModule: mod.loadModule
51
51
  }));
52
52
  const module = await loadModule({
53
53
  codeContent: setupCodeContent,
package/dist/index.d.ts CHANGED
@@ -295,6 +295,12 @@ declare type BrowserModeConfig = {
295
295
  * If not specified, a random available port will be used.
296
296
  */
297
297
  port?: number;
298
+ /**
299
+ * Default runner iframe viewport.
300
+ *
301
+ * When not specified, the browser UI fills the preview panel.
302
+ */
303
+ viewport?: BrowserViewport;
298
304
  /**
299
305
  * Whether to exit if the specified port is already in use.
300
306
  *
@@ -312,6 +318,11 @@ declare type BrowserModeConfig = {
312
318
  */
313
319
  declare type BrowserName = 'chromium' | 'firefox' | 'webkit';
314
320
 
321
+ declare type BrowserViewport = {
322
+ width: number;
323
+ height: number;
324
+ } | DevicePreset;
325
+
315
326
  declare type BuiltInReporterNames = 'default' | 'verbose' | 'md' | 'github-actions' | 'junit';
316
327
 
317
328
  declare type BuiltinReporterOptions = {
@@ -780,6 +791,20 @@ declare type DescribeFn = (description: string, fn?: () => void) => void;
780
791
 
781
792
  declare type DescribeForFn = <T>(cases: readonly T[]) => (description: string, fn?: (param: T) => MaybePromise<void>) => void;
782
793
 
794
+ /**
795
+ * Device presets aligned with Chrome DevTools device toolbar.
796
+ *
797
+ * These values are stable identifiers (not user-facing labels).
798
+ *
799
+ * IMPORTANT: Keep this union in sync with
800
+ * `@rstest/browser` preset runtime source:
801
+ * `packages/browser/src/viewportPresets.ts`.
802
+ *
803
+ * `@rstest/core` owns `defineConfig` typing, while `@rstest/browser` owns
804
+ * runtime validation and resolution for preset ids.
805
+ */
806
+ declare type DevicePreset = 'iPhoneSE' | 'iPhoneXR' | 'iPhone12Pro' | 'iPhone14ProMax' | 'Pixel7' | 'SamsungGalaxyS8Plus' | 'SamsungGalaxyS20Ultra' | 'iPadMini' | 'iPadAir' | 'iPadPro' | 'SurfacePro7' | 'SurfaceDuo' | 'GalaxyZFold5' | 'AsusZenbookFold' | 'SamsungGalaxyA51A71' | 'NestHub' | 'NestHubMax';
807
+
783
808
  /**
784
809
  * @param a Expected value
785
810
  * @param b Received value
@@ -2215,6 +2240,7 @@ declare type NormalizedBrowserModeConfig = {
2215
2240
  headless: boolean;
2216
2241
  port?: number;
2217
2242
  strictPort: boolean;
2243
+ viewport?: BrowserViewport;
2218
2244
  };
2219
2245
 
2220
2246
  declare type NormalizedConfig = Required<Omit<RstestConfig, OptionalKeys | 'pool' | 'projects' | 'coverage' | 'setupFiles' | 'globalSetup' | 'exclude' | 'testEnvironment' | 'browser'>> & Partial<Pick<RstestConfig, OptionalKeys>> & {
package/dist/worker.d.ts CHANGED
@@ -282,6 +282,12 @@ declare type BrowserModeConfig = {
282
282
  * If not specified, a random available port will be used.
283
283
  */
284
284
  port?: number;
285
+ /**
286
+ * Default runner iframe viewport.
287
+ *
288
+ * When not specified, the browser UI fills the preview panel.
289
+ */
290
+ viewport?: BrowserViewport;
285
291
  /**
286
292
  * Whether to exit if the specified port is already in use.
287
293
  *
@@ -299,6 +305,11 @@ declare type BrowserModeConfig = {
299
305
  */
300
306
  declare type BrowserName = 'chromium' | 'firefox' | 'webkit';
301
307
 
308
+ declare type BrowserViewport = {
309
+ width: number;
310
+ height: number;
311
+ } | DevicePreset;
312
+
302
313
  declare type BuiltInReporterNames = 'default' | 'verbose' | 'md' | 'github-actions' | 'junit';
303
314
 
304
315
  declare type BuiltinReporterOptions = {
@@ -635,6 +646,20 @@ declare type DescribeFn = (description: string, fn?: () => void) => void;
635
646
 
636
647
  declare type DescribeForFn = <T>(cases: readonly T[]) => (description: string, fn?: (param: T) => MaybePromise<void>) => void;
637
648
 
649
+ /**
650
+ * Device presets aligned with Chrome DevTools device toolbar.
651
+ *
652
+ * These values are stable identifiers (not user-facing labels).
653
+ *
654
+ * IMPORTANT: Keep this union in sync with
655
+ * `@rstest/browser` preset runtime source:
656
+ * `packages/browser/src/viewportPresets.ts`.
657
+ *
658
+ * `@rstest/core` owns `defineConfig` typing, while `@rstest/browser` owns
659
+ * runtime validation and resolution for preset ids.
660
+ */
661
+ declare type DevicePreset = 'iPhoneSE' | 'iPhoneXR' | 'iPhone12Pro' | 'iPhone14ProMax' | 'Pixel7' | 'SamsungGalaxyS8Plus' | 'SamsungGalaxyS20Ultra' | 'iPadMini' | 'iPadAir' | 'iPadPro' | 'SurfacePro7' | 'SurfaceDuo' | 'GalaxyZFold5' | 'AsusZenbookFold' | 'SamsungGalaxyA51A71' | 'NestHub' | 'NestHubMax';
662
+
638
663
  /**
639
664
  * @param a Expected value
640
665
  * @param b Received value
@@ -1765,6 +1790,7 @@ declare type NormalizedBrowserModeConfig = {
1765
1790
  headless: boolean;
1766
1791
  port?: number;
1767
1792
  strictPort: boolean;
1793
+ viewport?: BrowserViewport;
1768
1794
  };
1769
1795
 
1770
1796
  declare type NormalizedConfig = Required<Omit<RstestConfig, OptionalKeys | 'pool' | 'projects' | 'coverage' | 'setupFiles' | 'globalSetup' | 'exclude' | 'testEnvironment' | 'browser'>> & Partial<Pick<RstestConfig, OptionalKeys>> & {
package/dist/worker.js CHANGED
@@ -2,7 +2,7 @@ import "node:module";
2
2
  import { __webpack_require__ } from "./rslib-runtime.js";
3
3
  import { basename, isAbsolute, undoSerializableConfig, dirname, color, resolve as pathe_M_eThtNZ_resolve, join } from "./3160.js";
4
4
  import "./487.js";
5
- import { node_v8, createBirpc } from "./3216.js";
5
+ import { node_v8, createWorkerMetaMessage, createBirpc } from "./9869.js";
6
6
  import { createCoverageProvider } from "./5734.js";
7
7
  import { formatTestError, setRealTimers, getRealTimers } from "./1294.js";
8
8
  import { globalApis } from "./1157.js";
@@ -242,29 +242,26 @@ const preparePool = async ({ entryInfo: { distPath, testPath }, updateSnapshot,
242
242
  };
243
243
  };
244
244
  const loadFiles = async ({ setupEntries, assetFiles, rstestContext, distPath, testPath, interopDefault, isolate, outputModule })=>{
245
- const { loadModule, updateLatestAssetFiles } = outputModule ? await import("./0~6923.js").then((mod)=>({
245
+ const { loadModule } = outputModule ? await import("./0~6923.js").then((mod)=>({
246
246
  EsmMode: mod.loadEsModule_EsmMode,
247
247
  asModule: mod.asModule,
248
- loadModule: mod.loadModule,
249
- updateLatestAssetFiles: mod.updateLatestAssetFiles
248
+ clearModuleCache: mod.clearModuleCache,
249
+ loadModule: mod.loadModule
250
250
  })) : await import("./0~5835.js").then((mod)=>({
251
251
  cacheableLoadModule: mod.cacheableLoadModule,
252
- loadModule: mod.loadModule,
253
- updateLatestAssetFiles: mod.updateLatestAssetFiles
252
+ clearModuleCache: mod.clearModuleCache,
253
+ loadModule: mod.loadModule
254
254
  }));
255
- if (!isolate) {
256
- updateLatestAssetFiles(assetFiles);
257
- await loadModule({
258
- codeContent: `if (global && typeof global.__rstest_clean_core_cache__ === 'function') {
255
+ if (!isolate) await loadModule({
256
+ codeContent: `if (global && typeof global.__rstest_clean_core_cache__ === 'function') {
259
257
  global.__rstest_clean_core_cache__();
260
258
  }`,
261
- distPath: '',
262
- testPath,
263
- rstestContext,
264
- assetFiles,
265
- interopDefault
266
- });
267
- }
259
+ distPath: '',
260
+ testPath,
261
+ rstestContext,
262
+ assetFiles,
263
+ interopDefault
264
+ });
268
265
  for (const { distPath, testPath } of setupEntries){
269
266
  const setupCodeContent = assetFiles[distPath];
270
267
  await loadModule({
@@ -286,6 +283,7 @@ const loadFiles = async ({ setupEntries, assetFiles, rstestContext, distPath, te
286
283
  });
287
284
  };
288
285
  const runInPool = async (options)=>{
286
+ if ('function' == typeof process.send) process.send(createWorkerMetaMessage(process.pid));
289
287
  isTeardown = false;
290
288
  const { entryInfo: { distPath, testPath }, setupEntries, assets, type, context: { project, runtimeConfig: { isolate, bail } } } = options;
291
289
  const cleanups = [];
@@ -305,6 +303,19 @@ const runInPool = async (options)=>{
305
303
  const teardown = async ()=>{
306
304
  await new Promise((resolve)=>getRealTimers().setTimeout(resolve));
307
305
  await Promise.all(cleanups.map((fn)=>fn()));
306
+ if (!isolate) {
307
+ const { clearModuleCache } = options.context.outputModule ? await import("./0~6923.js").then((mod)=>({
308
+ EsmMode: mod.loadEsModule_EsmMode,
309
+ asModule: mod.asModule,
310
+ clearModuleCache: mod.clearModuleCache,
311
+ loadModule: mod.loadModule
312
+ })) : await import("./0~5835.js").then((mod)=>({
313
+ cacheableLoadModule: mod.cacheableLoadModule,
314
+ clearModuleCache: mod.clearModuleCache,
315
+ loadModule: mod.loadModule
316
+ }));
317
+ clearModuleCache();
318
+ }
308
319
  isTeardown = true;
309
320
  };
310
321
  if ('collect' === type) try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rstest/core",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "The Rsbuild-based test tool.",
5
5
  "bugs": {
6
6
  "url": "https://github.com/web-infra-dev/rstest/issues"
@@ -98,8 +98,8 @@
98
98
  "tinyspy": "^4.0.4",
99
99
  "url-extras": "^0.1.0",
100
100
  "webpack-license-plugin": "^4.5.1",
101
- "@rstest/tsconfig": "0.0.1",
102
- "@rstest/browser-ui": "0.0.0"
101
+ "@rstest/browser-ui": "0.0.0",
102
+ "@rstest/tsconfig": "0.0.1"
103
103
  },
104
104
  "peerDependencies": {
105
105
  "happy-dom": "*",