as-test 1.0.15 → 1.1.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/CHANGELOG.md +53 -1
- package/README.md +45 -4
- package/as-test.config.schema.json +5 -0
- package/assembly/util/wipc.ts +11 -4
- package/bin/commands/clean-core.js +92 -0
- package/bin/commands/clean.js +6 -0
- package/bin/commands/init-core.js +33 -225
- package/bin/commands/run-core.js +433 -289
- package/bin/commands/web-runner-source.js +14 -700
- package/bin/commands/web-session.js +1144 -0
- package/bin/index.js +390 -78
- package/bin/types.js +1 -0
- package/bin/util.js +87 -16
- package/lib/build/index.d.ts +1 -0
- package/lib/build/index.js +1098 -0
- package/lib/build/web-runner/client.d.ts +1 -0
- package/lib/build/web-runner/client.js +167 -0
- package/lib/build/web-runner/html.d.ts +1 -0
- package/lib/build/web-runner/html.js +201 -0
- package/lib/build/web-runner/worker.d.ts +1 -0
- package/lib/build/web-runner/worker.js +271 -0
- package/lib/src/index.ts +1248 -0
- package/package.json +14 -4
- package/transform/lib/mock.js +50 -27
package/bin/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { build, formatInvocation as formatBuildInvocation, getBuildInvocationPreview,
|
|
3
|
+
import { build, formatInvocation as formatBuildInvocation, getBuildInvocationPreview, } from "./commands/build.js";
|
|
4
4
|
import { createRunReporter, run } from "./commands/run.js";
|
|
5
5
|
import { executeBuildCommand } from "./commands/build.js";
|
|
6
6
|
import { executeRunCommand } from "./commands/run.js";
|
|
@@ -8,19 +8,29 @@ import { executeTestCommand } from "./commands/test.js";
|
|
|
8
8
|
import { executeFuzzCommand } from "./commands/fuzz.js";
|
|
9
9
|
import { executeInitCommand } from "./commands/init.js";
|
|
10
10
|
import { executeDoctorCommand } from "./commands/doctor.js";
|
|
11
|
+
import { executeCleanCommand } from "./commands/clean.js";
|
|
11
12
|
import { fuzz } from "./commands/fuzz-core.js";
|
|
12
|
-
import { applyMode, formatTime, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
|
|
13
|
+
import { applyMode, getDefaultModeNames, formatTime, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
|
|
13
14
|
import * as path from "path";
|
|
14
15
|
import { spawnSync } from "child_process";
|
|
15
16
|
import { glob } from "glob";
|
|
16
17
|
import { createInterface } from "readline";
|
|
17
|
-
import {
|
|
18
|
+
import { existsSync } from "fs";
|
|
18
19
|
import { availableParallelism, cpus } from "os";
|
|
19
20
|
import { BuildWorkerPool } from "./build-worker-pool.js";
|
|
21
|
+
import { PersistentWebSessionHost } from "./commands/web-session.js";
|
|
20
22
|
const _args = process.argv.slice(2);
|
|
21
23
|
const flags = [];
|
|
22
24
|
const args = [];
|
|
23
|
-
const COMMANDS = [
|
|
25
|
+
const COMMANDS = [
|
|
26
|
+
"run",
|
|
27
|
+
"build",
|
|
28
|
+
"test",
|
|
29
|
+
"fuzz",
|
|
30
|
+
"init",
|
|
31
|
+
"doctor",
|
|
32
|
+
"clean",
|
|
33
|
+
];
|
|
24
34
|
const version = getCliVersion();
|
|
25
35
|
const configPath = resolveConfigPath(_args);
|
|
26
36
|
const selectedModes = resolveModeNames(_args);
|
|
@@ -123,6 +133,12 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
123
133
|
process.exit(1);
|
|
124
134
|
});
|
|
125
135
|
}
|
|
136
|
+
else if (command === "clean") {
|
|
137
|
+
executeCleanCommand(configPath, selectedModes, resolveExecutionModes).catch((error) => {
|
|
138
|
+
printCliError(error);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
126
142
|
}
|
|
127
143
|
catch (error) {
|
|
128
144
|
printCliError(error);
|
|
@@ -190,6 +206,12 @@ function info() {
|
|
|
190
206
|
chalk.dim("<--mode x>") +
|
|
191
207
|
" " +
|
|
192
208
|
"Validate environment/config/runtime setup");
|
|
209
|
+
console.log(" " +
|
|
210
|
+
chalk.bold.magentaBright("clean") +
|
|
211
|
+
" " +
|
|
212
|
+
chalk.dim("<--mode x>") +
|
|
213
|
+
" " +
|
|
214
|
+
"Remove build, crash, and log outputs");
|
|
193
215
|
console.log("");
|
|
194
216
|
console.log(chalk.bold("Flags:"));
|
|
195
217
|
console.log(" " +
|
|
@@ -346,6 +368,15 @@ function printCommandHelp(command) {
|
|
|
346
368
|
process.stdout.write(" --help, -h Show this help\n");
|
|
347
369
|
return;
|
|
348
370
|
}
|
|
371
|
+
if (command == "clean") {
|
|
372
|
+
process.stdout.write(chalk.bold("Usage: ast clean [flags]\n\n"));
|
|
373
|
+
process.stdout.write("Remove configured build outputs, crash reports, and logs for the selected modes.\n\n");
|
|
374
|
+
process.stdout.write(chalk.bold("Flags:\n"));
|
|
375
|
+
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
376
|
+
process.stdout.write(" --mode <name[,name...]> Clean one or multiple named modes\n");
|
|
377
|
+
process.stdout.write(" --help, -h Show this help\n");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
349
380
|
info();
|
|
350
381
|
}
|
|
351
382
|
function resolveConfigPath(rawArgs) {
|
|
@@ -1036,16 +1067,7 @@ function resolveCommandTokens(rawArgs, command) {
|
|
|
1036
1067
|
}
|
|
1037
1068
|
return values;
|
|
1038
1069
|
}
|
|
1039
|
-
async function buildFileForMode(
|
|
1040
|
-
const reuse = await getBuildReuseInfo(args.configPath, args.file, args.modeName, args.buildFeatureToggles);
|
|
1041
|
-
if (reuse) {
|
|
1042
|
-
const source = cache.get(reuse.signature);
|
|
1043
|
-
if (source && source != reuse.outFile && existsSync(source)) {
|
|
1044
|
-
mkdirSync(path.dirname(reuse.outFile), { recursive: true });
|
|
1045
|
-
copyFileSync(source, reuse.outFile);
|
|
1046
|
-
return true;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1070
|
+
async function buildFileForMode(args) {
|
|
1049
1071
|
if (args.buildPool) {
|
|
1050
1072
|
await args.buildPool.buildFileMode({
|
|
1051
1073
|
configPath: args.configPath,
|
|
@@ -1057,12 +1079,8 @@ async function buildFileForMode(cache, args) {
|
|
|
1057
1079
|
else {
|
|
1058
1080
|
await build(args.configPath, [args.file], args.modeName, args.buildFeatureToggles);
|
|
1059
1081
|
}
|
|
1060
|
-
if (reuse) {
|
|
1061
|
-
cache.set(reuse.signature, reuse.outFile);
|
|
1062
|
-
}
|
|
1063
|
-
return false;
|
|
1064
1082
|
}
|
|
1065
|
-
async function runTestSequential(runFlags, configPath, selectors, suiteSelectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, emitRunComplete = true) {
|
|
1083
|
+
async function runTestSequential(runFlags, configPath, selectors, suiteSelectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, webSession = null, emitRunComplete = true) {
|
|
1066
1084
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1067
1085
|
if (!files.length) {
|
|
1068
1086
|
if (!allowNoSpecFiles) {
|
|
@@ -1091,6 +1109,7 @@ async function runTestSequential(runFlags, configPath, selectors, suiteSelectors
|
|
|
1091
1109
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
1092
1110
|
const result = await run(runFlags, configPath, [file], false, {
|
|
1093
1111
|
reporter,
|
|
1112
|
+
webSession,
|
|
1094
1113
|
suiteSelectors,
|
|
1095
1114
|
emitRunStart: false,
|
|
1096
1115
|
emitRunComplete: false,
|
|
@@ -1144,14 +1163,13 @@ async function runBuildModes(configPath, selectors, modes, buildFeatureToggles,
|
|
|
1144
1163
|
const loadedConfig = loadConfig(resolvedConfigPath, true);
|
|
1145
1164
|
const allStartedAt = Date.now();
|
|
1146
1165
|
let builtCount = 0;
|
|
1147
|
-
const buildReuseCache = new Map();
|
|
1148
1166
|
for (const modeName of modes) {
|
|
1149
1167
|
const startedAt = Date.now();
|
|
1150
1168
|
if (effective.buildJobs > 1) {
|
|
1151
1169
|
const pool = new BuildWorkerPool(effective.buildJobs);
|
|
1152
1170
|
try {
|
|
1153
1171
|
await runOrderedPool(files, effective.buildJobs, async (file) => {
|
|
1154
|
-
await buildFileForMode(
|
|
1172
|
+
await buildFileForMode({
|
|
1155
1173
|
configPath,
|
|
1156
1174
|
file,
|
|
1157
1175
|
modeName,
|
|
@@ -1166,7 +1184,7 @@ async function runBuildModes(configPath, selectors, modes, buildFeatureToggles,
|
|
|
1166
1184
|
}
|
|
1167
1185
|
else {
|
|
1168
1186
|
for (const file of files) {
|
|
1169
|
-
await buildFileForMode(
|
|
1187
|
+
await buildFileForMode({
|
|
1170
1188
|
configPath,
|
|
1171
1189
|
file,
|
|
1172
1190
|
modeName,
|
|
@@ -1184,10 +1202,17 @@ async function runRuntimeModes(runFlags, configPath, selectors, suiteSelectors,
|
|
|
1184
1202
|
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
1185
1203
|
const modeSummaryTotal = Math.max(modes.length, 1);
|
|
1186
1204
|
const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
|
|
1187
|
-
|
|
1205
|
+
let effectiveRunFlags = {
|
|
1188
1206
|
...runFlags,
|
|
1189
1207
|
...resolveEffectiveParallelJobs(runFlags, fileSummaryTotal),
|
|
1190
1208
|
};
|
|
1209
|
+
if (await usesHeadfulWebMode(configPath, modes)) {
|
|
1210
|
+
effectiveRunFlags = {
|
|
1211
|
+
...effectiveRunFlags,
|
|
1212
|
+
jobs: 1,
|
|
1213
|
+
runJobs: 1,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1191
1216
|
if (effectiveRunFlags.jobs > 1) {
|
|
1192
1217
|
if (modes.length > 1) {
|
|
1193
1218
|
const failed = await runRuntimeMatrixParallel(effectiveRunFlags, configPath, selectors, suiteSelectors, modes, modeSummaryTotal, fileSummaryTotal);
|
|
@@ -1225,6 +1250,28 @@ async function runRuntimeModes(runFlags, configPath, selectors, suiteSelectors,
|
|
|
1225
1250
|
}
|
|
1226
1251
|
process.exit(failed ? 1 : 0);
|
|
1227
1252
|
}
|
|
1253
|
+
async function usesHeadfulWebMode(configPath, modes) {
|
|
1254
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
1255
|
+
const loaded = loadConfig(resolvedConfigPath, true);
|
|
1256
|
+
for (const modeName of modes) {
|
|
1257
|
+
const active = applyMode(loaded, modeName).config;
|
|
1258
|
+
if (!usesWebBrowser(active))
|
|
1259
|
+
continue;
|
|
1260
|
+
const runtimeCmd = active.runOptions.runtime.cmd?.trim() ||
|
|
1261
|
+
(active.buildOptions.target == "web"
|
|
1262
|
+
? "node .as-test/runners/default.web.js"
|
|
1263
|
+
: "");
|
|
1264
|
+
if (!runtimeCmd.includes("--headless")) {
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
async function createSharedHeadfulWebSession(configPath, modes) {
|
|
1271
|
+
return (await usesHeadfulWebMode(configPath, modes))
|
|
1272
|
+
? await PersistentWebSessionHost.start(false)
|
|
1273
|
+
: null;
|
|
1274
|
+
}
|
|
1228
1275
|
async function runRuntimeMatrix(runFlags, configPath, selectors, suiteSelectors, modes, modeSummaryTotal, fileSummaryTotal) {
|
|
1229
1276
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1230
1277
|
if (!files.length) {
|
|
@@ -1255,13 +1302,11 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, suiteSelectors,
|
|
|
1255
1302
|
}));
|
|
1256
1303
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1257
1304
|
const buildIntervals = [];
|
|
1258
|
-
const buildReuseCache = new Map();
|
|
1259
1305
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
1260
1306
|
const file = files[fileIndex];
|
|
1261
1307
|
const fileName = path.basename(file);
|
|
1262
1308
|
const fileResults = [];
|
|
1263
1309
|
const modeTimes = modes.map(() => "...");
|
|
1264
|
-
const buildReuseCache = new Map();
|
|
1265
1310
|
if (liveMatrix) {
|
|
1266
1311
|
renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
|
|
1267
1312
|
}
|
|
@@ -1329,10 +1374,17 @@ async function runTestModes(runFlags, configPath, selectors, suiteSelectors, fuz
|
|
|
1329
1374
|
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
1330
1375
|
const modeSummaryTotal = Math.max(modes.length, 1);
|
|
1331
1376
|
const fileSummaryTotal = await resolveConfiguredFileTotal(configPath, selectors);
|
|
1332
|
-
|
|
1377
|
+
let effectiveRunFlags = {
|
|
1333
1378
|
...runFlags,
|
|
1334
1379
|
...resolveEffectiveParallelJobs(runFlags, fileSummaryTotal),
|
|
1335
1380
|
};
|
|
1381
|
+
if (await usesHeadfulWebMode(configPath, modes)) {
|
|
1382
|
+
effectiveRunFlags = {
|
|
1383
|
+
...effectiveRunFlags,
|
|
1384
|
+
jobs: 1,
|
|
1385
|
+
runJobs: 1,
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1336
1388
|
if (effectiveRunFlags.jobs > 1) {
|
|
1337
1389
|
if (modes.length > 1) {
|
|
1338
1390
|
const failed = await runTestMatrixParallel(effectiveRunFlags, configPath, selectors, suiteSelectors, fuzzerSelectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
|
|
@@ -1354,34 +1406,40 @@ async function runTestModes(runFlags, configPath, selectors, suiteSelectors, fuz
|
|
|
1354
1406
|
return;
|
|
1355
1407
|
}
|
|
1356
1408
|
let failed = false;
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
if (reporterSession.reporterKind == "default") {
|
|
1364
|
-
process.stdout.write("\n");
|
|
1365
|
-
}
|
|
1366
|
-
const fuzzResults = await runFuzzMatrixResults(configPath, selectors, fuzzerSelectors, [modeName], fuzzOverrides, reporterSession.reporter);
|
|
1367
|
-
if (fuzzResults.some(hasFuzzFailures))
|
|
1409
|
+
const sharedWebSession = await createSharedHeadfulWebSession(configPath, modes);
|
|
1410
|
+
try {
|
|
1411
|
+
for (const modeName of modes) {
|
|
1412
|
+
const reporterSession = await createRunReporter(configPath, effectiveRunFlags.reporterPath, modeName);
|
|
1413
|
+
const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, suiteSelectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, sharedWebSession, !fuzzEnabled);
|
|
1414
|
+
if (modeResult.failed)
|
|
1368
1415
|
failed = true;
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1416
|
+
if (fuzzEnabled) {
|
|
1417
|
+
if (reporterSession.reporterKind == "default") {
|
|
1418
|
+
process.stdout.write("\n");
|
|
1419
|
+
}
|
|
1420
|
+
const fuzzResults = await runFuzzMatrixResults(configPath, selectors, fuzzerSelectors, [modeName], fuzzOverrides, reporterSession.reporter);
|
|
1421
|
+
if (fuzzResults.some(hasFuzzFailures))
|
|
1422
|
+
failed = true;
|
|
1423
|
+
reporterSession.reporter.onRunComplete?.({
|
|
1424
|
+
clean: runFlags.clean,
|
|
1425
|
+
snapshotEnabled: effectiveRunFlags.snapshot !== false,
|
|
1426
|
+
showCoverage: effectiveRunFlags.showCoverage,
|
|
1427
|
+
buildTime: modeResult.summary.buildTime +
|
|
1428
|
+
getMergedIntervalDuration(collectFuzzBuildIntervals(fuzzResults)),
|
|
1429
|
+
snapshotSummary: modeResult.summary.snapshotSummary,
|
|
1430
|
+
coverageSummary: modeResult.summary.coverageSummary,
|
|
1431
|
+
stats: modeResult.summary.stats,
|
|
1432
|
+
reports: modeResult.summary.reports,
|
|
1433
|
+
fuzzSummary: summarizeFuzzExecutions(fuzzResults),
|
|
1434
|
+
modeSummary: buildSingleModeSummary(modeResult.summary.stats, modeResult.summary.snapshotSummary, modeSummaryTotal),
|
|
1435
|
+
});
|
|
1436
|
+
reporterSession.reporter.flush?.();
|
|
1437
|
+
}
|
|
1383
1438
|
}
|
|
1384
1439
|
}
|
|
1440
|
+
finally {
|
|
1441
|
+
await sharedWebSession?.close();
|
|
1442
|
+
}
|
|
1385
1443
|
process.exit(failed ? 1 : 0);
|
|
1386
1444
|
}
|
|
1387
1445
|
async function runTestMatrix(runFlags, configPath, selectors, suiteSelectors, fuzzerSelectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
|
|
@@ -1425,7 +1483,6 @@ async function runTestMatrix(runFlags, configPath, selectors, suiteSelectors, fu
|
|
|
1425
1483
|
const fileName = path.basename(file);
|
|
1426
1484
|
const fileResults = [];
|
|
1427
1485
|
const modeTimes = modes.map(() => "...");
|
|
1428
|
-
const buildReuseCache = new Map();
|
|
1429
1486
|
if (liveMatrix) {
|
|
1430
1487
|
renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
|
|
1431
1488
|
}
|
|
@@ -1433,7 +1490,7 @@ async function runTestMatrix(runFlags, configPath, selectors, suiteSelectors, fu
|
|
|
1433
1490
|
const modeName = modes[i];
|
|
1434
1491
|
try {
|
|
1435
1492
|
const buildStartedAt = Date.now();
|
|
1436
|
-
await buildFileForMode(
|
|
1493
|
+
await buildFileForMode({
|
|
1437
1494
|
configPath,
|
|
1438
1495
|
file,
|
|
1439
1496
|
modeName,
|
|
@@ -1625,11 +1682,10 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, suiteSe
|
|
|
1625
1682
|
: null;
|
|
1626
1683
|
const fileResults = [];
|
|
1627
1684
|
const modeTimes = modes.map(() => "...");
|
|
1628
|
-
const buildReuseCache = new Map();
|
|
1629
1685
|
for (let i = 0; i < modes.length; i++) {
|
|
1630
1686
|
const modeName = modes[i];
|
|
1631
1687
|
const buildStartedAt = Date.now();
|
|
1632
|
-
await buildFileForMode(
|
|
1688
|
+
await buildFileForMode({
|
|
1633
1689
|
configPath,
|
|
1634
1690
|
file,
|
|
1635
1691
|
modeName,
|
|
@@ -1727,7 +1783,7 @@ async function runTestSingleParallel(runFlags, configPath, selectors, suiteSelec
|
|
|
1727
1783
|
? renderQueuedFileStart(queueDisplay, path.basename(file))
|
|
1728
1784
|
: null;
|
|
1729
1785
|
const buildStartedAt = Date.now();
|
|
1730
|
-
await buildFileForMode(
|
|
1786
|
+
await buildFileForMode({
|
|
1731
1787
|
configPath,
|
|
1732
1788
|
file,
|
|
1733
1789
|
modeName,
|
|
@@ -1831,11 +1887,10 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, suiteSelec
|
|
|
1831
1887
|
: null;
|
|
1832
1888
|
const fileResults = [];
|
|
1833
1889
|
const modeTimes = modes.map(() => "...");
|
|
1834
|
-
const buildReuseCache = new Map();
|
|
1835
1890
|
for (let i = 0; i < modes.length; i++) {
|
|
1836
1891
|
const modeName = modes[i];
|
|
1837
1892
|
const buildStartedAt = Date.now();
|
|
1838
|
-
await buildFileForMode(
|
|
1893
|
+
await buildFileForMode({
|
|
1839
1894
|
configPath,
|
|
1840
1895
|
file,
|
|
1841
1896
|
modeName,
|
|
@@ -2210,10 +2265,8 @@ function resolveExecutionModes(configPath, selectedModes) {
|
|
|
2210
2265
|
return selectedModes;
|
|
2211
2266
|
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
2212
2267
|
const config = loadConfig(resolvedConfigPath, false);
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
return [undefined];
|
|
2216
|
-
return configuredModes;
|
|
2268
|
+
const defaultModes = getDefaultModeNames(config);
|
|
2269
|
+
return [undefined, ...defaultModes];
|
|
2217
2270
|
}
|
|
2218
2271
|
async function resolveSelectedFiles(configPath, selectors, warn = true) {
|
|
2219
2272
|
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
@@ -2487,6 +2540,7 @@ async function ensureWebBrowsersReady(configPath, modes, browserOverride) {
|
|
|
2487
2540
|
continue;
|
|
2488
2541
|
}
|
|
2489
2542
|
active.runOptions.runtime.browser = resolved.browser;
|
|
2543
|
+
await ensurePlaywrightBrowserDepsReady(requestedBrowser, resolved.browser);
|
|
2490
2544
|
process.env.BROWSER = resolved.browser;
|
|
2491
2545
|
}
|
|
2492
2546
|
if (!missing.length)
|
|
@@ -2549,6 +2603,10 @@ function resolveNamedBrowser(browser) {
|
|
|
2549
2603
|
return { browser: candidate };
|
|
2550
2604
|
}
|
|
2551
2605
|
}
|
|
2606
|
+
const systemFallback = resolveSystemBrowserExecutable(normalized);
|
|
2607
|
+
if (systemFallback) {
|
|
2608
|
+
return { browser: systemFallback };
|
|
2609
|
+
}
|
|
2552
2610
|
const playwrightFallback = resolvePlaywrightBrowserExecutable(normalized);
|
|
2553
2611
|
if (playwrightFallback) {
|
|
2554
2612
|
return { browser: playwrightFallback };
|
|
@@ -2567,22 +2625,26 @@ async function handleMissingWebBrowsers(missing) {
|
|
|
2567
2625
|
: (entry.modeName ?? "default"))
|
|
2568
2626
|
.join(", ");
|
|
2569
2627
|
const details = "no web-capable browser was found in PATH, BROWSER, or Playwright cache";
|
|
2628
|
+
const selected = choosePreferredBrowserInstall(missing);
|
|
2629
|
+
const installCommand = selected == "webkit"
|
|
2630
|
+
? 'npx -y playwright install webkit'
|
|
2631
|
+
: `npx -y playwright install ${selected}`;
|
|
2570
2632
|
if (!canPromptForWebInstall()) {
|
|
2571
|
-
throw new Error(`web target requires a browser for mode(s) ${scope}; ${details}. Export BROWSER or install one with "
|
|
2633
|
+
throw new Error(`web target requires a browser for mode(s) ${scope}; ${details}. Export BROWSER or install one with "${installCommand}".`);
|
|
2572
2634
|
}
|
|
2573
2635
|
process.stdout.write(chalk.bold.blue("◇ Browser Setup Needed") +
|
|
2574
2636
|
"\n" +
|
|
2575
2637
|
`│ ${details}\n` +
|
|
2638
|
+
`│ requested browser: ${selected}\n` +
|
|
2576
2639
|
"│\n");
|
|
2577
|
-
const choice = await promptLine(
|
|
2640
|
+
const choice = await promptLine(`Install ${selected} with Playwright now? [Y/n] `);
|
|
2578
2641
|
const normalized = choice.trim().toLowerCase();
|
|
2579
2642
|
if (normalized == "n" || normalized == "no") {
|
|
2580
|
-
throw new Error(
|
|
2643
|
+
throw new Error(`browser install skipped. Export BROWSER or install one with "${installCommand}", then rerun.`);
|
|
2581
2644
|
}
|
|
2582
2645
|
if (normalized != "" && normalized != "y" && normalized != "yes") {
|
|
2583
2646
|
throw new Error(`invalid answer "${choice}". Expected yes or no.`);
|
|
2584
2647
|
}
|
|
2585
|
-
const selected = "chromium";
|
|
2586
2648
|
process.stdout.write(chalk.dim(`installing ${selected} via Playwright...\n`));
|
|
2587
2649
|
const install = spawnSync("npx", ["-y", "playwright", "install", selected], {
|
|
2588
2650
|
stdio: "inherit",
|
|
@@ -2597,6 +2659,100 @@ async function handleMissingWebBrowsers(missing) {
|
|
|
2597
2659
|
}
|
|
2598
2660
|
process.env.BROWSER = browserPath;
|
|
2599
2661
|
}
|
|
2662
|
+
async function ensurePlaywrightBrowserDepsReady(requestedBrowser, resolvedBrowser) {
|
|
2663
|
+
if (process.platform != "linux")
|
|
2664
|
+
return;
|
|
2665
|
+
if (!isPlaywrightBrowserExecutable(resolvedBrowser))
|
|
2666
|
+
return;
|
|
2667
|
+
const browser = normalizeBrowserInstallName(requestedBrowser);
|
|
2668
|
+
if (!browser)
|
|
2669
|
+
return;
|
|
2670
|
+
const dryRun = spawnSync("npx", ["-y", "playwright", "install-deps", "--dry-run", browser], {
|
|
2671
|
+
encoding: "utf8",
|
|
2672
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2673
|
+
shell: false,
|
|
2674
|
+
});
|
|
2675
|
+
if (dryRun.status === 0)
|
|
2676
|
+
return;
|
|
2677
|
+
const installCommand = `npx -y playwright install-deps ${browser}`;
|
|
2678
|
+
const details = extractPlaywrightDepsSummary(dryRun).trim();
|
|
2679
|
+
if (!canPromptForWebInstall()) {
|
|
2680
|
+
throw new Error([
|
|
2681
|
+
`Playwright ${browser} system dependencies are missing on Linux.`,
|
|
2682
|
+
details.length ? details : null,
|
|
2683
|
+
`Install them with "${installCommand}" and rerun.`,
|
|
2684
|
+
]
|
|
2685
|
+
.filter(Boolean)
|
|
2686
|
+
.join("\n"));
|
|
2687
|
+
}
|
|
2688
|
+
process.stdout.write(chalk.bold.blue("◇ Browser Deps Needed") +
|
|
2689
|
+
"\n" +
|
|
2690
|
+
`│ Playwright ${browser} needs Linux system packages before it can launch.\n` +
|
|
2691
|
+
(details.length
|
|
2692
|
+
? `│\n${details
|
|
2693
|
+
.split("\n")
|
|
2694
|
+
.map((line) => `│ ${line}`)
|
|
2695
|
+
.join("\n")}\n`
|
|
2696
|
+
: "") +
|
|
2697
|
+
"│\n");
|
|
2698
|
+
const choice = await promptLine(`Install Playwright ${browser} system dependencies now? [Y/n] `);
|
|
2699
|
+
const normalized = choice.trim().toLowerCase();
|
|
2700
|
+
if (normalized == "n" || normalized == "no") {
|
|
2701
|
+
throw new Error(`browser dependency install skipped. Run "${installCommand}", then rerun.`);
|
|
2702
|
+
}
|
|
2703
|
+
if (normalized != "" && normalized != "y" && normalized != "yes") {
|
|
2704
|
+
throw new Error(`invalid answer "${choice}". Expected yes or no.`);
|
|
2705
|
+
}
|
|
2706
|
+
process.stdout.write(chalk.dim(`installing Playwright ${browser} system dependencies...\n`));
|
|
2707
|
+
const install = spawnSync("npx", ["-y", "playwright", "install-deps", browser], {
|
|
2708
|
+
stdio: "inherit",
|
|
2709
|
+
shell: false,
|
|
2710
|
+
});
|
|
2711
|
+
if (install.status !== 0) {
|
|
2712
|
+
throw new Error(`Playwright system dependency install failed for ${browser}`);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
function choosePreferredBrowserInstall(missing) {
|
|
2716
|
+
for (const entry of missing) {
|
|
2717
|
+
const normalized = normalizeBrowserInstallName(entry.browser);
|
|
2718
|
+
if (normalized)
|
|
2719
|
+
return normalized;
|
|
2720
|
+
}
|
|
2721
|
+
return "chromium";
|
|
2722
|
+
}
|
|
2723
|
+
function normalizeBrowserInstallName(browser) {
|
|
2724
|
+
const normalized = browser?.trim().toLowerCase() ?? "";
|
|
2725
|
+
if (!normalized.length)
|
|
2726
|
+
return null;
|
|
2727
|
+
if (normalized == "firefox")
|
|
2728
|
+
return "firefox";
|
|
2729
|
+
if (normalized == "webkit")
|
|
2730
|
+
return "webkit";
|
|
2731
|
+
if (normalized == "chromium" ||
|
|
2732
|
+
normalized == "chrome" ||
|
|
2733
|
+
normalized == "google-chrome" ||
|
|
2734
|
+
normalized == "google-chrome-stable" ||
|
|
2735
|
+
normalized == "chromium-browser" ||
|
|
2736
|
+
normalized == "msedge") {
|
|
2737
|
+
return "chromium";
|
|
2738
|
+
}
|
|
2739
|
+
return null;
|
|
2740
|
+
}
|
|
2741
|
+
function isPlaywrightBrowserExecutable(browser) {
|
|
2742
|
+
const normalized = browser.trim().replace(/\\/g, "/").toLowerCase();
|
|
2743
|
+
return (normalized.includes("/ms-playwright/") ||
|
|
2744
|
+
normalized.endsWith("/pw_run.sh") ||
|
|
2745
|
+
normalized.endsWith("/playwright.exe"));
|
|
2746
|
+
}
|
|
2747
|
+
function extractPlaywrightDepsSummary(result) {
|
|
2748
|
+
const stdout = typeof result.stdout == "string"
|
|
2749
|
+
? result.stdout
|
|
2750
|
+
: result.stdout?.toString("utf8") ?? "";
|
|
2751
|
+
const stderr = typeof result.stderr == "string"
|
|
2752
|
+
? result.stderr
|
|
2753
|
+
: result.stderr?.toString("utf8") ?? "";
|
|
2754
|
+
return [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
|
|
2755
|
+
}
|
|
2600
2756
|
function canPromptForWebInstall() {
|
|
2601
2757
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2602
2758
|
}
|
|
@@ -2613,20 +2769,158 @@ function promptLine(question) {
|
|
|
2613
2769
|
});
|
|
2614
2770
|
}
|
|
2615
2771
|
function resolvePlaywrightBrowserExecutable(browser) {
|
|
2616
|
-
const
|
|
2617
|
-
if (!
|
|
2772
|
+
const patterns = getPlaywrightBrowserPatterns(browser);
|
|
2773
|
+
if (!patterns.length)
|
|
2618
2774
|
return null;
|
|
2619
|
-
const
|
|
2620
|
-
|
|
2621
|
-
|
|
2775
|
+
for (const cacheRoot of getPlaywrightCacheRoots()) {
|
|
2776
|
+
if (!existsSync(cacheRoot))
|
|
2777
|
+
continue;
|
|
2778
|
+
for (const pattern of patterns) {
|
|
2779
|
+
const matches = glob.sync(path.join(cacheRoot, pattern)).sort();
|
|
2780
|
+
if (matches.length)
|
|
2781
|
+
return matches[matches.length - 1];
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return null;
|
|
2785
|
+
}
|
|
2786
|
+
function getPlaywrightCacheRoots() {
|
|
2787
|
+
const roots = new Set();
|
|
2788
|
+
const configured = process.env.PLAYWRIGHT_BROWSERS_PATH?.trim() ?? "";
|
|
2789
|
+
if (configured.length && configured != "0") {
|
|
2790
|
+
roots.add(path.resolve(configured));
|
|
2791
|
+
}
|
|
2792
|
+
const home = process.env.HOME ?? "";
|
|
2793
|
+
if (process.platform == "darwin" && home.length) {
|
|
2794
|
+
roots.add(path.join(home, "Library", "Caches", "ms-playwright"));
|
|
2795
|
+
}
|
|
2796
|
+
else if (process.platform == "win32") {
|
|
2797
|
+
const localAppData = process.env.LOCALAPPDATA?.trim() ?? "";
|
|
2798
|
+
if (localAppData.length) {
|
|
2799
|
+
roots.add(path.join(localAppData, "ms-playwright"));
|
|
2800
|
+
}
|
|
2801
|
+
const userProfile = process.env.USERPROFILE?.trim() ?? "";
|
|
2802
|
+
if (userProfile.length) {
|
|
2803
|
+
roots.add(path.join(userProfile, "AppData", "Local", "ms-playwright"));
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
else if (home.length) {
|
|
2807
|
+
roots.add(path.join(home, ".cache", "ms-playwright"));
|
|
2808
|
+
}
|
|
2809
|
+
return [...roots];
|
|
2810
|
+
}
|
|
2811
|
+
function getPlaywrightBrowserPatterns(browser) {
|
|
2812
|
+
if (process.platform == "darwin") {
|
|
2813
|
+
const macMap = {
|
|
2814
|
+
chromium: [
|
|
2815
|
+
"chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
|
2816
|
+
"chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium",
|
|
2817
|
+
"chromium_headless_shell-*/chrome-headless-shell-mac*/chrome-headless-shell",
|
|
2818
|
+
],
|
|
2819
|
+
chrome: [
|
|
2820
|
+
"chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
|
2821
|
+
"chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium",
|
|
2822
|
+
"chromium_headless_shell-*/chrome-headless-shell-mac*/chrome-headless-shell",
|
|
2823
|
+
],
|
|
2824
|
+
firefox: [
|
|
2825
|
+
"firefox-*/firefox/*.app/Contents/MacOS/firefox",
|
|
2826
|
+
"firefox-*/*.app/Contents/MacOS/firefox",
|
|
2827
|
+
"firefox-*/firefox/firefox",
|
|
2828
|
+
],
|
|
2829
|
+
webkit: ["webkit-*/pw_run.sh"],
|
|
2830
|
+
};
|
|
2831
|
+
return macMap[browser] ?? [];
|
|
2832
|
+
}
|
|
2833
|
+
if (process.platform == "win32") {
|
|
2834
|
+
const winMap = {
|
|
2835
|
+
chromium: [
|
|
2836
|
+
"chromium-*/chrome-win/chrome.exe",
|
|
2837
|
+
"chromium-*/chrome-win64/chrome.exe",
|
|
2838
|
+
"chromium_headless_shell-*/chrome-headless-shell-win64/chrome-headless-shell.exe",
|
|
2839
|
+
],
|
|
2840
|
+
chrome: [
|
|
2841
|
+
"chromium-*/chrome-win/chrome.exe",
|
|
2842
|
+
"chromium-*/chrome-win64/chrome.exe",
|
|
2843
|
+
"chromium_headless_shell-*/chrome-headless-shell-win64/chrome-headless-shell.exe",
|
|
2844
|
+
],
|
|
2845
|
+
firefox: ["firefox-*/firefox/firefox.exe"],
|
|
2846
|
+
webkit: ["webkit-*/Playwright.exe"],
|
|
2847
|
+
};
|
|
2848
|
+
return winMap[browser] ?? [];
|
|
2849
|
+
}
|
|
2850
|
+
const linuxMap = {
|
|
2851
|
+
chromium: [
|
|
2852
|
+
"chromium-*/chrome-linux/chrome",
|
|
2853
|
+
"chromium-*/chrome-linux64/chrome",
|
|
2854
|
+
"chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell",
|
|
2855
|
+
],
|
|
2856
|
+
chrome: [
|
|
2857
|
+
"chromium-*/chrome-linux/chrome",
|
|
2858
|
+
"chromium-*/chrome-linux64/chrome",
|
|
2859
|
+
"chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell",
|
|
2860
|
+
],
|
|
2622
2861
|
firefox: ["firefox-*/firefox/firefox"],
|
|
2623
2862
|
webkit: ["webkit-*/pw_run.sh"],
|
|
2624
2863
|
};
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2864
|
+
return linuxMap[browser] ?? [];
|
|
2865
|
+
}
|
|
2866
|
+
function resolveSystemBrowserExecutable(browser) {
|
|
2867
|
+
if (process.platform == "darwin") {
|
|
2868
|
+
const home = process.env.HOME ?? "";
|
|
2869
|
+
const macSearchRoots = [
|
|
2870
|
+
"/Applications",
|
|
2871
|
+
home.length ? path.join(home, "Applications") : "",
|
|
2872
|
+
].filter(Boolean);
|
|
2873
|
+
const macAppPaths = {
|
|
2874
|
+
chromium: [
|
|
2875
|
+
"Chromium.app/Contents/MacOS/Chromium",
|
|
2876
|
+
"Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
2877
|
+
"Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
|
2878
|
+
],
|
|
2879
|
+
chrome: [
|
|
2880
|
+
"Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
2881
|
+
"Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
|
2882
|
+
"Chromium.app/Contents/MacOS/Chromium",
|
|
2883
|
+
],
|
|
2884
|
+
firefox: [
|
|
2885
|
+
"Firefox.app/Contents/MacOS/firefox",
|
|
2886
|
+
"Firefox Developer Edition.app/Contents/MacOS/firefox",
|
|
2887
|
+
],
|
|
2888
|
+
msedge: [
|
|
2889
|
+
"Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
2890
|
+
],
|
|
2891
|
+
webkit: [],
|
|
2892
|
+
};
|
|
2893
|
+
for (const root of macSearchRoots) {
|
|
2894
|
+
for (const relativePath of macAppPaths[browser] ?? []) {
|
|
2895
|
+
const fullPath = path.join(root, relativePath);
|
|
2896
|
+
if (existsSync(fullPath))
|
|
2897
|
+
return fullPath;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
return null;
|
|
2901
|
+
}
|
|
2902
|
+
if (process.platform == "win32") {
|
|
2903
|
+
const programFiles = process.env.ProgramFiles?.trim() ?? "";
|
|
2904
|
+
const programFilesX86 = process.env["ProgramFiles(x86)"]?.trim() ?? "";
|
|
2905
|
+
const localAppData = process.env.LOCALAPPDATA?.trim() ?? "";
|
|
2906
|
+
const roots = [programFiles, programFilesX86, localAppData].filter(Boolean);
|
|
2907
|
+
const winPaths = {
|
|
2908
|
+
chromium: [
|
|
2909
|
+
"Chromium/Application/chrome.exe",
|
|
2910
|
+
"Google/Chrome/Application/chrome.exe",
|
|
2911
|
+
],
|
|
2912
|
+
chrome: ["Google/Chrome/Application/chrome.exe"],
|
|
2913
|
+
firefox: ["Mozilla Firefox/firefox.exe"],
|
|
2914
|
+
msedge: ["Microsoft/Edge/Application/msedge.exe"],
|
|
2915
|
+
webkit: [],
|
|
2916
|
+
};
|
|
2917
|
+
for (const root of roots) {
|
|
2918
|
+
for (const relativePath of winPaths[browser] ?? []) {
|
|
2919
|
+
const fullPath = path.join(root, relativePath);
|
|
2920
|
+
if (existsSync(fullPath))
|
|
2921
|
+
return fullPath;
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2630
2924
|
}
|
|
2631
2925
|
return null;
|
|
2632
2926
|
}
|
|
@@ -2652,6 +2946,7 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
|
|
|
2652
2946
|
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
2653
2947
|
const config = loadConfig(resolvedConfigPath, true);
|
|
2654
2948
|
const configuredModes = Object.keys(config.modes);
|
|
2949
|
+
const defaultModes = getDefaultModeNames(config);
|
|
2655
2950
|
const configuredModeLabels = configuredModes.length
|
|
2656
2951
|
? configuredModes
|
|
2657
2952
|
: ["default"];
|
|
@@ -2667,13 +2962,30 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
|
|
|
2667
2962
|
if (listFlags.listModes) {
|
|
2668
2963
|
process.stdout.write(chalk.bold("Configured modes:\n"));
|
|
2669
2964
|
for (const modeName of configuredModeLabels) {
|
|
2670
|
-
|
|
2965
|
+
if (modeName == "default") {
|
|
2966
|
+
process.stdout.write(` - ${modeName}\n`);
|
|
2967
|
+
continue;
|
|
2968
|
+
}
|
|
2969
|
+
const mode = config.modes[modeName];
|
|
2970
|
+
const suffix = mode?.default === false ? " (manual)" : " (default)";
|
|
2971
|
+
process.stdout.write(` - ${modeName}${suffix}\n`);
|
|
2671
2972
|
}
|
|
2672
2973
|
process.stdout.write(chalk.bold("\nSelected modes:\n"));
|
|
2673
2974
|
for (const modeName of selectedModeLabels) {
|
|
2674
2975
|
process.stdout.write(` - ${modeName}\n`);
|
|
2675
2976
|
}
|
|
2676
|
-
|
|
2977
|
+
if (!modes.length && configuredModes.length) {
|
|
2978
|
+
process.stdout.write(chalk.bold("\nDefault-selected modes:\n"));
|
|
2979
|
+
if (defaultModes.length) {
|
|
2980
|
+
for (const modeName of defaultModes) {
|
|
2981
|
+
process.stdout.write(` - ${modeName}\n`);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
else {
|
|
2985
|
+
process.stdout.write(" - default\n");
|
|
2986
|
+
}
|
|
2987
|
+
process.stdout.write("\n");
|
|
2988
|
+
}
|
|
2677
2989
|
}
|
|
2678
2990
|
if (!listFlags.list)
|
|
2679
2991
|
return;
|