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