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/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, getBuildReuseInfo, } from "./commands/build.js";
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 { copyFileSync, existsSync, mkdirSync } from "fs";
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 = ["run", "build", "test", "fuzz", "init", "doctor"];
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(cache, args) {
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(buildReuseCache, {
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(buildReuseCache, {
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
- const effectiveRunFlags = {
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
- const effectiveRunFlags = {
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
- for (const modeName of modes) {
1358
- const reporterSession = await createRunReporter(configPath, effectiveRunFlags.reporterPath, modeName);
1359
- const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, suiteSelectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, !fuzzEnabled);
1360
- if (modeResult.failed)
1361
- failed = true;
1362
- if (fuzzEnabled) {
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
- reporterSession.reporter.onRunComplete?.({
1370
- clean: runFlags.clean,
1371
- snapshotEnabled: effectiveRunFlags.snapshot !== false,
1372
- showCoverage: effectiveRunFlags.showCoverage,
1373
- buildTime: modeResult.summary.buildTime +
1374
- getMergedIntervalDuration(collectFuzzBuildIntervals(fuzzResults)),
1375
- snapshotSummary: modeResult.summary.snapshotSummary,
1376
- coverageSummary: modeResult.summary.coverageSummary,
1377
- stats: modeResult.summary.stats,
1378
- reports: modeResult.summary.reports,
1379
- fuzzSummary: summarizeFuzzExecutions(fuzzResults),
1380
- modeSummary: buildSingleModeSummary(modeResult.summary.stats, modeResult.summary.snapshotSummary, modeSummaryTotal),
1381
- });
1382
- reporterSession.reporter.flush?.();
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(buildReuseCache, {
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(buildReuseCache, {
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(new Map(), {
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(buildReuseCache, {
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 configuredModes = Object.keys(config.modes);
2214
- if (!configuredModes.length)
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 "npx -y playwright install chromium" or "npx -y playwright install webkit".`);
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("Install Chromium with Playwright now? [Y/n] ");
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('browser install skipped. Export BROWSER or install one with "npx -y playwright install chromium" or "npx -y playwright install webkit", then rerun.');
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 cacheRoot = path.join(process.env.HOME ?? "", ".cache", "ms-playwright");
2617
- if (!cacheRoot.length || !existsSync(cacheRoot))
2773
+ const patterns = getPlaywrightBrowserPatterns(browser);
2774
+ if (!patterns.length)
2618
2775
  return null;
2619
- const map = {
2620
- chromium: ["chromium-*/chrome-linux64/chrome"],
2621
- chrome: ["chromium-*/chrome-linux64/chrome"],
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
- const patterns = map[browser] ?? [];
2626
- for (const pattern of patterns) {
2627
- const matches = glob.sync(path.join(cacheRoot, pattern)).sort();
2628
- if (matches.length)
2629
- return matches[matches.length - 1];
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
- process.stdout.write(` - ${modeName}\n`);
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
- process.stdout.write("\n");
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;