@rstest/browser 0.7.9 → 0.8.0
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/browser-container/container-static/css/container.425b2695.css +1 -0
- package/dist/browser-container/container-static/js/837.e631233e.js +22311 -0
- package/dist/browser-container/container-static/js/837.e631233e.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{container.5ddd46c3.js → container.93bdade7.js} +665 -532
- package/dist/browser-container/container-static/js/{lib-react.a9c9a89b.js → lib-react.48335539.js} +4 -4
- package/dist/browser-container/container-static/js/lib-react.48335539.js.LICENSE.txt +1 -0
- package/dist/browser-container/container.html +6 -1
- package/dist/hostController.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +136 -62
- package/package.json +13 -13
- package/src/hostController.ts +252 -100
- package/src/index.ts +15 -4
- package/dist/browser-container/container-static/css/container.dc438e35.css +0 -1
- package/dist/browser-container/container-static/js/916.5aee8d2f.js +0 -23549
- package/dist/browser-container/container-static/js/916.5aee8d2f.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.a9c9a89b.js.LICENSE.txt +0 -1
package/src/hostController.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
|
+
import type { AddressInfo } from 'node:net';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import {
|
|
7
|
+
type BrowserTestRunOptions,
|
|
8
|
+
type BrowserTestRunResult,
|
|
6
9
|
color,
|
|
7
10
|
type FormattedError,
|
|
8
11
|
getSetupFiles,
|
|
@@ -136,11 +139,9 @@ class ContainerRpcManager {
|
|
|
136
139
|
|
|
137
140
|
private setupConnectionHandler(): void {
|
|
138
141
|
this.wss.on('connection', (ws: WebSocket) => {
|
|
139
|
-
logger.
|
|
140
|
-
logger.
|
|
141
|
-
|
|
142
|
-
`[Browser UI] Current ws: ${this.ws ? 'exists' : 'null'}, new ws: ${ws ? 'exists' : 'null'}`,
|
|
143
|
-
),
|
|
142
|
+
logger.debug('[Browser UI] Container WebSocket connected');
|
|
143
|
+
logger.debug(
|
|
144
|
+
`[Browser UI] Current ws: ${this.ws ? 'exists' : 'null'}, new ws: ${ws ? 'exists' : 'null'}`,
|
|
144
145
|
);
|
|
145
146
|
this.attachWebSocket(ws);
|
|
146
147
|
});
|
|
@@ -203,18 +204,14 @@ class ContainerRpcManager {
|
|
|
203
204
|
testFile: string,
|
|
204
205
|
testNamePattern?: string,
|
|
205
206
|
): Promise<void> {
|
|
206
|
-
logger.
|
|
207
|
-
|
|
208
|
-
`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`,
|
|
209
|
-
),
|
|
207
|
+
logger.debug(
|
|
208
|
+
`[Browser UI] reloadTestFile called, rpc: ${this.rpc ? 'exists' : 'null'}, ws: ${this.ws ? 'exists' : 'null'}`,
|
|
210
209
|
);
|
|
211
210
|
if (!this.rpc) {
|
|
212
|
-
logger.
|
|
213
|
-
color.yellow('[Browser UI] RPC not available, skipping reloadTestFile'),
|
|
214
|
-
);
|
|
211
|
+
logger.debug('[Browser UI] RPC not available, skipping reloadTestFile');
|
|
215
212
|
return;
|
|
216
213
|
}
|
|
217
|
-
logger.
|
|
214
|
+
logger.debug(`[Browser UI] Calling reloadTestFile: ${testFile}`);
|
|
218
215
|
await this.rpc.reloadTestFile(testFile, testNamePattern);
|
|
219
216
|
}
|
|
220
217
|
}
|
|
@@ -248,6 +245,8 @@ type WatchContext = {
|
|
|
248
245
|
lastTestFiles: TestFileInfo[];
|
|
249
246
|
hooksEnabled: boolean;
|
|
250
247
|
cleanupRegistered: boolean;
|
|
248
|
+
chunkHashes: Map<string, string>;
|
|
249
|
+
affectedTestFiles: string[];
|
|
251
250
|
};
|
|
252
251
|
|
|
253
252
|
const watchContext: WatchContext = {
|
|
@@ -255,6 +254,8 @@ const watchContext: WatchContext = {
|
|
|
255
254
|
lastTestFiles: [],
|
|
256
255
|
hooksEnabled: false,
|
|
257
256
|
cleanupRegistered: false,
|
|
257
|
+
chunkHashes: new Map(),
|
|
258
|
+
affectedTestFiles: [],
|
|
258
259
|
};
|
|
259
260
|
|
|
260
261
|
// ============================================================================
|
|
@@ -363,6 +364,99 @@ const excludePatternsToRegExp = (patterns: string[]): RegExp | null => {
|
|
|
363
364
|
return new RegExp(`[\\\\/](${keywords.join('|')})[\\\\/]`);
|
|
364
365
|
};
|
|
365
366
|
|
|
367
|
+
type StatsModule = {
|
|
368
|
+
nameForCondition?: string;
|
|
369
|
+
children?: StatsModule[];
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
type StatsChunk = {
|
|
373
|
+
id?: string | number;
|
|
374
|
+
names?: string[];
|
|
375
|
+
hash?: string;
|
|
376
|
+
files?: string[];
|
|
377
|
+
modules?: StatsModule[];
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Find test file path from chunk modules by matching against known entry files.
|
|
382
|
+
*/
|
|
383
|
+
const findTestFileInModules = (
|
|
384
|
+
modules: StatsModule[] | undefined,
|
|
385
|
+
entryTestFiles: Set<string>,
|
|
386
|
+
): string | null => {
|
|
387
|
+
if (!modules) return null;
|
|
388
|
+
|
|
389
|
+
for (const m of modules) {
|
|
390
|
+
if (m.nameForCondition) {
|
|
391
|
+
const normalizedPath = normalize(m.nameForCondition);
|
|
392
|
+
if (entryTestFiles.has(normalizedPath)) {
|
|
393
|
+
return normalizedPath;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (m.children) {
|
|
397
|
+
const found = findTestFileInModules(m.children, entryTestFiles);
|
|
398
|
+
if (found) return found;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get a stable identifier for a chunk.
|
|
406
|
+
* Prefers chunk.id or chunk.names[0] over file paths for stability.
|
|
407
|
+
*/
|
|
408
|
+
const getChunkKey = (chunk: StatsChunk): string | null => {
|
|
409
|
+
if (chunk.id != null) {
|
|
410
|
+
return String(chunk.id);
|
|
411
|
+
}
|
|
412
|
+
if (chunk.names && chunk.names.length > 0) {
|
|
413
|
+
return chunk.names[0]!;
|
|
414
|
+
}
|
|
415
|
+
if (chunk.files && chunk.files.length > 0) {
|
|
416
|
+
return chunk.files[0]!;
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Compare chunk hashes and find affected test files for watch mode re-runs.
|
|
423
|
+
* Uses chunk.id/names as stable keys instead of relying on file path patterns.
|
|
424
|
+
*/
|
|
425
|
+
const getAffectedTestFiles = (
|
|
426
|
+
chunks: StatsChunk[] | undefined,
|
|
427
|
+
entryTestFiles: Set<string>,
|
|
428
|
+
): string[] => {
|
|
429
|
+
if (!chunks) return [];
|
|
430
|
+
|
|
431
|
+
const affectedFiles = new Set<string>();
|
|
432
|
+
const currentHashes = new Map<string, string>();
|
|
433
|
+
|
|
434
|
+
for (const chunk of chunks) {
|
|
435
|
+
if (!chunk.hash) continue;
|
|
436
|
+
|
|
437
|
+
// First check if this chunk contains a test entry file
|
|
438
|
+
const testFile = findTestFileInModules(chunk.modules, entryTestFiles);
|
|
439
|
+
if (!testFile) continue;
|
|
440
|
+
|
|
441
|
+
// Get a stable key for this chunk
|
|
442
|
+
const chunkKey = getChunkKey(chunk);
|
|
443
|
+
if (!chunkKey) continue;
|
|
444
|
+
|
|
445
|
+
const prevHash = watchContext.chunkHashes.get(chunkKey);
|
|
446
|
+
currentHashes.set(chunkKey, chunk.hash);
|
|
447
|
+
|
|
448
|
+
if (prevHash !== undefined && prevHash !== chunk.hash) {
|
|
449
|
+
affectedFiles.add(testFile);
|
|
450
|
+
logger.debug(
|
|
451
|
+
`[Watch] Chunk hash changed for ${chunkKey}: ${prevHash} -> ${chunk.hash} (test: ${testFile})`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
watchContext.chunkHashes = currentHashes;
|
|
457
|
+
return Array.from(affectedFiles);
|
|
458
|
+
};
|
|
459
|
+
|
|
366
460
|
const getRuntimeConfigFromProject = (
|
|
367
461
|
project: ProjectContext,
|
|
368
462
|
): RuntimeConfig => {
|
|
@@ -747,8 +841,8 @@ const createBrowserRuntime = async ({
|
|
|
747
841
|
plugins: userPlugins,
|
|
748
842
|
server: {
|
|
749
843
|
printUrls: false,
|
|
750
|
-
port: context.normalizedConfig.browser.port,
|
|
751
|
-
strictPort: context.normalizedConfig.browser.
|
|
844
|
+
port: context.normalizedConfig.browser.port ?? 4000,
|
|
845
|
+
strictPort: context.normalizedConfig.browser.strictPort,
|
|
752
846
|
},
|
|
753
847
|
dev: {
|
|
754
848
|
client: {
|
|
@@ -766,56 +860,63 @@ const createBrowserRuntime = async ({
|
|
|
766
860
|
{
|
|
767
861
|
name: 'rstest:browser-user-config',
|
|
768
862
|
setup(api) {
|
|
769
|
-
api.modifyEnvironmentConfig(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
863
|
+
api.modifyEnvironmentConfig({
|
|
864
|
+
handler: (config, { mergeEnvironmentConfig }) => {
|
|
865
|
+
// Merge order: current config -> userConfig -> rstest required config (highest priority)
|
|
866
|
+
const merged = mergeEnvironmentConfig(config, userRsbuildConfig, {
|
|
867
|
+
resolve: {
|
|
868
|
+
alias: rstestInternalAliases,
|
|
775
869
|
},
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
// Enable source map for inline snapshot support
|
|
783
|
-
sourceMap: {
|
|
784
|
-
js: 'source-map',
|
|
870
|
+
output: {
|
|
871
|
+
target: 'web',
|
|
872
|
+
// Enable source map for inline snapshot support
|
|
873
|
+
sourceMap: {
|
|
874
|
+
js: 'source-map',
|
|
875
|
+
},
|
|
785
876
|
},
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
877
|
+
tools: {
|
|
878
|
+
rspack: (rspackConfig) => {
|
|
879
|
+
rspackConfig.mode = 'development';
|
|
880
|
+
rspackConfig.lazyCompilation = {
|
|
881
|
+
imports: true,
|
|
882
|
+
entries: false,
|
|
883
|
+
};
|
|
884
|
+
rspackConfig.plugins = rspackConfig.plugins || [];
|
|
885
|
+
rspackConfig.plugins.push(virtualManifestPlugin);
|
|
886
|
+
|
|
887
|
+
// Extract and merge sourcemaps from pre-built @rstest/core files
|
|
888
|
+
// This preserves the sourcemap chain for inline snapshot support
|
|
889
|
+
// See: https://rspack.dev/config/module-rules#rulesextractsourcemap
|
|
890
|
+
const browserRuntimeDir = dirname(browserRuntimePath);
|
|
891
|
+
rspackConfig.module = rspackConfig.module || {};
|
|
892
|
+
rspackConfig.module.rules = rspackConfig.module.rules || [];
|
|
893
|
+
rspackConfig.module.rules.unshift({
|
|
894
|
+
test: /\.js$/,
|
|
895
|
+
include: browserRuntimeDir,
|
|
896
|
+
extractSourceMap: true,
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
if (isDebug()) {
|
|
900
|
+
logger.log(
|
|
901
|
+
`[rstest:browser] extractSourceMap rule added for: ${browserRuntimeDir}`,
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
},
|
|
814
905
|
},
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
// Completely overwrite entry to prevent Rsbuild default entry detection from taking effect.
|
|
909
|
+
// In browser mode, entry is fully controlled by rstest (not user's src/index.ts).
|
|
910
|
+
// This must be done after mergeEnvironmentConfig to ensure highest priority.
|
|
911
|
+
merged.source = merged.source || {};
|
|
912
|
+
merged.source.entry = {
|
|
913
|
+
runner: resolveBrowserFile('client/entry.ts'),
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
return merged;
|
|
917
|
+
},
|
|
918
|
+
// Execute after all other plugins to ensure rstest's entry config has the highest priority
|
|
919
|
+
order: 'post',
|
|
819
920
|
});
|
|
820
921
|
},
|
|
821
922
|
},
|
|
@@ -834,10 +935,34 @@ const createBrowserRuntime = async ({
|
|
|
834
935
|
logger.log(color.cyan('\nFile changed, re-running tests...\n'));
|
|
835
936
|
});
|
|
836
937
|
|
|
837
|
-
api.onAfterDevCompile(async () => {
|
|
938
|
+
api.onAfterDevCompile(async ({ stats }) => {
|
|
939
|
+
// Collect hashes even during initial build to establish baseline
|
|
940
|
+
if (stats) {
|
|
941
|
+
const projectEntries = await collectProjectEntries(context);
|
|
942
|
+
const entryTestFiles = new Set<string>(
|
|
943
|
+
projectEntries.flatMap((entry) =>
|
|
944
|
+
entry.testFiles.map((f) => normalize(f)),
|
|
945
|
+
),
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
const statsJson = stats.toJson({ all: true });
|
|
949
|
+
const affected = getAffectedTestFiles(
|
|
950
|
+
statsJson.chunks,
|
|
951
|
+
entryTestFiles,
|
|
952
|
+
);
|
|
953
|
+
watchContext.affectedTestFiles = affected;
|
|
954
|
+
|
|
955
|
+
if (affected.length > 0) {
|
|
956
|
+
logger.debug(
|
|
957
|
+
`[Watch] Affected test files: ${affected.join(', ')}`,
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
838
962
|
if (!watchContext.hooksEnabled) {
|
|
839
963
|
return;
|
|
840
964
|
}
|
|
965
|
+
|
|
841
966
|
await onTriggerRerun();
|
|
842
967
|
});
|
|
843
968
|
},
|
|
@@ -890,10 +1015,8 @@ const createBrowserRuntime = async ({
|
|
|
890
1015
|
res.end(html);
|
|
891
1016
|
return true;
|
|
892
1017
|
} catch (error) {
|
|
893
|
-
logger.
|
|
894
|
-
|
|
895
|
-
`[Browser UI] Failed to fetch container HTML from dev server: ${String(error)}`,
|
|
896
|
-
),
|
|
1018
|
+
logger.debug(
|
|
1019
|
+
`[Browser UI] Failed to fetch container HTML from dev server: ${String(error)}`,
|
|
897
1020
|
);
|
|
898
1021
|
return false;
|
|
899
1022
|
}
|
|
@@ -925,10 +1048,8 @@ const createBrowserRuntime = async ({
|
|
|
925
1048
|
res.end(buffer);
|
|
926
1049
|
return true;
|
|
927
1050
|
} catch (error) {
|
|
928
|
-
logger.
|
|
929
|
-
|
|
930
|
-
`[Browser UI] Failed to proxy asset from dev server: ${String(error)}`,
|
|
931
|
-
),
|
|
1051
|
+
logger.debug(
|
|
1052
|
+
`[Browser UI] Failed to proxy asset from dev server: ${String(error)}`,
|
|
932
1053
|
);
|
|
933
1054
|
return false;
|
|
934
1055
|
}
|
|
@@ -952,15 +1073,13 @@ const createBrowserRuntime = async ({
|
|
|
952
1073
|
res.statusCode = 204;
|
|
953
1074
|
res.end();
|
|
954
1075
|
} catch (error) {
|
|
955
|
-
logger.
|
|
956
|
-
color.yellow(`[Browser UI] Failed to open editor: ${String(error)}`),
|
|
957
|
-
);
|
|
1076
|
+
logger.debug(`[Browser UI] Failed to open editor: ${String(error)}`);
|
|
958
1077
|
res.statusCode = 500;
|
|
959
1078
|
res.end('Failed to open editor');
|
|
960
1079
|
}
|
|
961
1080
|
return;
|
|
962
1081
|
}
|
|
963
|
-
if (url.pathname === '/'
|
|
1082
|
+
if (url.pathname === '/') {
|
|
964
1083
|
if (await respondWithDevServerHtml(url, res)) {
|
|
965
1084
|
return;
|
|
966
1085
|
}
|
|
@@ -1003,12 +1122,16 @@ const createBrowserRuntime = async ({
|
|
|
1003
1122
|
|
|
1004
1123
|
const { port } = await devServer.listen();
|
|
1005
1124
|
|
|
1006
|
-
// Create WebSocket server on
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1125
|
+
// Create WebSocket server on an available port
|
|
1126
|
+
// Using port: 0 lets the OS assign an available port, avoiding conflicts
|
|
1127
|
+
// when the fixed port (e.g., container port + 1) is already in use
|
|
1128
|
+
const wss = new WebSocketServer({ port: 0 });
|
|
1129
|
+
await new Promise<void>((resolve, reject) => {
|
|
1130
|
+
wss.once('listening', resolve);
|
|
1131
|
+
wss.once('error', reject);
|
|
1132
|
+
});
|
|
1133
|
+
const wsPort = (wss.address() as AddressInfo).port;
|
|
1134
|
+
logger.debug(`[Browser UI] WebSocket server started on port ${wsPort}`);
|
|
1012
1135
|
|
|
1013
1136
|
let browserLauncher: BrowserType;
|
|
1014
1137
|
const browserName = context.normalizedConfig.browser.browser;
|
|
@@ -1059,7 +1182,11 @@ const createBrowserRuntime = async ({
|
|
|
1059
1182
|
// Main Entry Point
|
|
1060
1183
|
// ============================================================================
|
|
1061
1184
|
|
|
1062
|
-
export const runBrowserController = async (
|
|
1185
|
+
export const runBrowserController = async (
|
|
1186
|
+
context: Rstest,
|
|
1187
|
+
options?: BrowserTestRunOptions,
|
|
1188
|
+
): Promise<BrowserTestRunResult | void> => {
|
|
1189
|
+
const { skipOnTestRunEnd = false } = options ?? {};
|
|
1063
1190
|
const buildStart = Date.now();
|
|
1064
1191
|
const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
|
|
1065
1192
|
let containerDevServer: string | undefined;
|
|
@@ -1068,10 +1195,8 @@ export const runBrowserController = async (context: Rstest): Promise<void> => {
|
|
|
1068
1195
|
if (containerDevServerEnv) {
|
|
1069
1196
|
try {
|
|
1070
1197
|
containerDevServer = new URL(containerDevServerEnv).toString();
|
|
1071
|
-
logger.
|
|
1072
|
-
|
|
1073
|
-
`[Browser UI] Using dev server for container: ${containerDevServer}`,
|
|
1074
|
-
),
|
|
1198
|
+
logger.debug(
|
|
1199
|
+
`[Browser UI] Using dev server for container: ${containerDevServer}`,
|
|
1075
1200
|
);
|
|
1076
1201
|
} catch (error) {
|
|
1077
1202
|
logger.error(
|
|
@@ -1276,9 +1401,12 @@ export const runBrowserController = async (context: Rstest): Promise<void> => {
|
|
|
1276
1401
|
// Create RPC methods that can access test state variables
|
|
1277
1402
|
const createRpcMethods = (): HostRpcMethods => ({
|
|
1278
1403
|
async rerunTest(testFile: string, testNamePattern?: string) {
|
|
1404
|
+
const projectName = context.normalizedConfig.name || 'project';
|
|
1405
|
+
const relativePath = relative(context.rootPath, testFile);
|
|
1406
|
+
const displayPath = `<${projectName}>/${relativePath}`;
|
|
1279
1407
|
logger.log(
|
|
1280
1408
|
color.cyan(
|
|
1281
|
-
`\nRe-running test: ${
|
|
1409
|
+
`\nRe-running test: ${displayPath}${testNamePattern ? ` (pattern: ${testNamePattern})` : ''}\n`,
|
|
1282
1410
|
),
|
|
1283
1411
|
);
|
|
1284
1412
|
await rpcManager.reloadTestFile(testFile, testNamePattern);
|
|
@@ -1405,14 +1533,12 @@ export const runBrowserController = async (context: Rstest): Promise<void> => {
|
|
|
1405
1533
|
|
|
1406
1534
|
// Only navigate on first creation
|
|
1407
1535
|
if (isNewPage) {
|
|
1408
|
-
await containerPage.goto(`http://localhost:${port}
|
|
1536
|
+
await containerPage.goto(`http://localhost:${port}/`, {
|
|
1409
1537
|
waitUntil: 'load',
|
|
1410
1538
|
});
|
|
1411
1539
|
|
|
1412
1540
|
logger.log(
|
|
1413
|
-
color.cyan(
|
|
1414
|
-
`\nContainer page opened at http://localhost:${port}/container.html\n`,
|
|
1415
|
-
),
|
|
1541
|
+
color.cyan(`\nBrowser mode opened at http://localhost:${port}/\n`),
|
|
1416
1542
|
);
|
|
1417
1543
|
}
|
|
1418
1544
|
|
|
@@ -1474,7 +1600,21 @@ export const runBrowserController = async (context: Rstest): Promise<void> => {
|
|
|
1474
1600
|
await rpcManager.notifyTestFileUpdate(currentTestFiles);
|
|
1475
1601
|
}
|
|
1476
1602
|
|
|
1477
|
-
|
|
1603
|
+
const affectedFiles = watchContext.affectedTestFiles;
|
|
1604
|
+
watchContext.affectedTestFiles = [];
|
|
1605
|
+
|
|
1606
|
+
if (affectedFiles.length > 0) {
|
|
1607
|
+
logger.log(
|
|
1608
|
+
color.cyan(
|
|
1609
|
+
`Re-running ${affectedFiles.length} affected test file(s)...\n`,
|
|
1610
|
+
),
|
|
1611
|
+
);
|
|
1612
|
+
for (const testFile of affectedFiles) {
|
|
1613
|
+
await rpcManager.reloadTestFile(testFile);
|
|
1614
|
+
}
|
|
1615
|
+
} else if (!filesChanged) {
|
|
1616
|
+
logger.log(color.cyan('Tests will be re-executed automatically\n'));
|
|
1617
|
+
}
|
|
1478
1618
|
};
|
|
1479
1619
|
}
|
|
1480
1620
|
|
|
@@ -1505,14 +1645,24 @@ export const runBrowserController = async (context: Rstest): Promise<void> => {
|
|
|
1505
1645
|
ensureProcessExitCode(1);
|
|
1506
1646
|
}
|
|
1507
1647
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1648
|
+
const result: BrowserTestRunResult = {
|
|
1649
|
+
results: reporterResults,
|
|
1650
|
+
testResults: caseResults,
|
|
1651
|
+
duration,
|
|
1652
|
+
hasFailure: isFailure,
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
// Only call onTestRunEnd if not skipped (for unified reporter output)
|
|
1656
|
+
if (!skipOnTestRunEnd) {
|
|
1657
|
+
for (const reporter of context.reporters) {
|
|
1658
|
+
await reporter.onTestRunEnd?.({
|
|
1659
|
+
results: context.reporterResults.results,
|
|
1660
|
+
testResults: context.reporterResults.testResults,
|
|
1661
|
+
duration,
|
|
1662
|
+
snapshotSummary: context.snapshotManager.summary,
|
|
1663
|
+
getSourcemap: async () => null,
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1516
1666
|
}
|
|
1517
1667
|
|
|
1518
1668
|
// Enable watch hooks AFTER initial test run to avoid duplicate runs
|
|
@@ -1522,6 +1672,8 @@ export const runBrowserController = async (context: Rstest): Promise<void> => {
|
|
|
1522
1672
|
color.cyan('\nWatch mode enabled - will re-run tests on file changes\n'),
|
|
1523
1673
|
);
|
|
1524
1674
|
}
|
|
1675
|
+
|
|
1676
|
+
return result;
|
|
1525
1677
|
};
|
|
1526
1678
|
|
|
1527
1679
|
// ============================================================================
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BrowserTestRunOptions,
|
|
3
|
+
BrowserTestRunResult,
|
|
4
|
+
Rstest,
|
|
5
|
+
} from '@rstest/core/browser';
|
|
2
6
|
import {
|
|
3
7
|
type ListBrowserTestsResult,
|
|
4
8
|
listBrowserTests as listBrowserTestsImpl,
|
|
5
9
|
runBrowserController,
|
|
6
10
|
} from './hostController';
|
|
7
11
|
|
|
8
|
-
export async function runBrowserTests(
|
|
9
|
-
|
|
12
|
+
export async function runBrowserTests(
|
|
13
|
+
context: Rstest,
|
|
14
|
+
options?: BrowserTestRunOptions,
|
|
15
|
+
): Promise<BrowserTestRunResult | void> {
|
|
16
|
+
return runBrowserController(context, options);
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
export async function listBrowserTests(
|
|
@@ -15,4 +22,8 @@ export async function listBrowserTests(
|
|
|
15
22
|
return listBrowserTestsImpl(context);
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
export type {
|
|
25
|
+
export type {
|
|
26
|
+
BrowserTestRunOptions,
|
|
27
|
+
BrowserTestRunResult,
|
|
28
|
+
ListBrowserTestsResult,
|
|
29
|
+
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@import "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap";@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-duration:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-gray-400:#99a1af;--color-zinc-950:#09090b;--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1)}@supports (color:color(display-p3 0 0 0)){:root,:host{--color-gray-400:color(display-p3 .605734 .630385 .680158);--color-zinc-950:color(display-p3 .0353716 .0353595 .0435539)}}@supports (color:lab(0% 0 0)){:root,:host{--color-gray-400:lab(65.9269% -.832707 -8.17473);--color-zinc-950:lab(2.51107% .242703 -.886115)}}}@layer base,components;@layer utilities{.absolute{position:absolute}.absolute\!{position:absolute!important}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.bottom-0{bottom:calc(var(--spacing)*0)}.z-10{z-index:10}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.m-0\!{margin:calc(var(--spacing)*0)!important}.m-1\!{margin:calc(var(--spacing)*1)!important}.mr-0\!{margin-right:calc(var(--spacing)*0)!important}.block{display:block}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-flex{display:inline-flex}.inline-flex\!{display:inline-flex!important}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-5{height:calc(var(--spacing)*5)}.h-5\!{height:calc(var(--spacing)*5)!important}.h-7{height:calc(var(--spacing)*7)}.h-\[52px\]{height:52px}.h-full{height:100%}.h-screen{height:100vh}.min-h-0{min-height:calc(var(--spacing)*0)}.w-1\.5{width:calc(var(--spacing)*1.5)}.w-5\!{width:calc(var(--spacing)*5)!important}.w-7{width:calc(var(--spacing)*7)}.w-\[18px\]{width:18px}.w-full{width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.translate-y-\[calc\(50\%-2px\)\]{--tw-translate-y:calc(50% - 2px);translate:var(--tw-translate-x)var(--tw-translate-y)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.grid-cols-\[auto_minmax\(0\,1fr\)_auto\]{grid-template-columns:auto minmax(0,1fr) auto}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-\[2px\]{gap:2px}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.bg-black\/\[0\.02\]{background-color:#00000005}@supports (color:color-mix(in lab, red, red)){.bg-black\/\[0\.02\]{background-color:color-mix(in oklab,var(--color-black)2%,transparent)}}.bg-transparent{background-color:#0000}.bg-zinc-950{background-color:var(--color-zinc-950)}.p-0{padding:calc(var(--spacing)*0)}.p-0\!{padding:calc(var(--spacing)*0)!important}.p-3{padding:calc(var(--spacing)*3)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-2{padding-block:calc(var(--spacing)*2)}.font-mono{font-family:var(--font-mono)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]\!{font-size:10px!important}.text-\[13px\]{font-size:13px}.text-\[13px\]\!{font-size:13px!important}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.font-semibold\!{--tw-font-weight:var(--font-weight-semibold)!important;font-weight:var(--font-weight-semibold)!important}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-black\!{color:var(--color-black)!important}.text-gray-400{color:var(--color-gray-400)}.text-white{color:var(--color-white)}.opacity-0{opacity:0}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}}:root{--lightningcss-light: ;--lightningcss-dark:initial;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}body{color:#eef1f6;background:#06070d;min-height:100vh;margin:0;font-family:Space Grotesk,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;overflow:hidden}.animate-spin{animation-duration:.6s}@keyframes status-flash{0%{filter:brightness()}5%{filter:brightness(2.5)}15%{filter:brightness(1.8)}40%{filter:brightness(1.3)}to{filter:brightness()}}.status-icon-flash{animation:1.5s ease-out status-flash}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
|