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/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(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(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
- }
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(buildReuseCache, {
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(buildReuseCache, {
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
- const effectiveRunFlags = {
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
- const effectiveRunFlags = {
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
- 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))
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
- 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?.();
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(buildReuseCache, {
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(buildReuseCache, {
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(new Map(), {
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(buildReuseCache, {
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 configuredModes = Object.keys(config.modes);
2214
- if (!configuredModes.length)
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 "npx -y playwright install chromium" or "npx -y playwright install webkit".`);
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("Install Chromium with Playwright now? [Y/n] ");
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('browser install skipped. Export BROWSER or install one with "npx -y playwright install chromium" or "npx -y playwright install webkit", then rerun.');
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 cacheRoot = path.join(process.env.HOME ?? "", ".cache", "ms-playwright");
2617
- if (!cacheRoot.length || !existsSync(cacheRoot))
2772
+ const patterns = getPlaywrightBrowserPatterns(browser);
2773
+ if (!patterns.length)
2618
2774
  return null;
2619
- const map = {
2620
- chromium: ["chromium-*/chrome-linux64/chrome"],
2621
- chrome: ["chromium-*/chrome-linux64/chrome"],
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
- 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];
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
- process.stdout.write(` - ${modeName}\n`);
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
- process.stdout.write("\n");
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;