playwright 1.56.0-alpha-2025-09-17 → 1.56.0-alpha-1758292576000

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.
@@ -28,6 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var browserContextFactory_exports = {};
30
30
  __export(browserContextFactory_exports, {
31
+ SharedContextFactory: () => SharedContextFactory,
31
32
  contextFactory: () => contextFactory
32
33
  });
33
34
  module.exports = __toCommonJS(browserContextFactory_exports);
@@ -38,11 +39,12 @@ var import_path = __toESM(require("path"));
38
39
  var playwright = __toESM(require("playwright-core"));
39
40
  var import_registry = require("playwright-core/lib/server/registry/index");
40
41
  var import_server = require("playwright-core/lib/server");
41
- var import_processUtils = require("./processUtils");
42
42
  var import_log = require("../log");
43
43
  var import_config = require("./config");
44
44
  var import_server2 = require("../sdk/server");
45
45
  function contextFactory(config) {
46
+ if (config.sharedBrowserContext)
47
+ return SharedContextFactory.create(config);
46
48
  if (config.browser.remoteEndpoint)
47
49
  return new RemoteContextFactory(config);
48
50
  if (config.browser.cdpEndpoint)
@@ -162,32 +164,32 @@ class PersistentContextFactory {
162
164
  (0, import_log.testDebug)("lock user data dir", userDataDir);
163
165
  const browserType = playwright[this.config.browser.browserName];
164
166
  for (let i = 0; i < 5; i++) {
165
- if (!await alreadyRunning(this.config, browserType, userDataDir))
166
- break;
167
- await new Promise((resolve) => setTimeout(resolve, 1e3));
168
- }
169
- const launchOptions = {
170
- tracesDir,
171
- ...this.config.browser.launchOptions,
172
- ...this.config.browser.contextOptions,
173
- handleSIGINT: false,
174
- handleSIGTERM: false,
175
- ignoreDefaultArgs: [
176
- "--disable-extensions"
177
- ],
178
- assistantMode: true
179
- };
180
- try {
181
- const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
182
- const close = () => this._closeBrowserContext(browserContext, userDataDir);
183
- return { browserContext, close };
184
- } catch (error) {
185
- if (error.message.includes("Executable doesn't exist"))
186
- throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
187
- if (error.message.includes("ProcessSingleton") || error.message.includes("Invalid URL"))
188
- throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
189
- throw error;
167
+ const launchOptions = {
168
+ tracesDir,
169
+ ...this.config.browser.launchOptions,
170
+ ...this.config.browser.contextOptions,
171
+ handleSIGINT: false,
172
+ handleSIGTERM: false,
173
+ ignoreDefaultArgs: [
174
+ "--disable-extensions"
175
+ ],
176
+ assistantMode: true
177
+ };
178
+ try {
179
+ const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
180
+ const close = () => this._closeBrowserContext(browserContext, userDataDir);
181
+ return { browserContext, close };
182
+ } catch (error) {
183
+ if (error.message.includes("Executable doesn't exist"))
184
+ throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
185
+ if (error.message.includes("ProcessSingleton") || error.message.includes("Invalid URL")) {
186
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
187
+ continue;
188
+ }
189
+ throw error;
190
+ }
190
191
  }
192
+ throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
191
193
  }
192
194
  async _closeBrowserContext(browserContext, userDataDir) {
193
195
  (0, import_log.testDebug)("close browser context (persistent)");
@@ -207,12 +209,6 @@ class PersistentContextFactory {
207
209
  return result;
208
210
  }
209
211
  }
210
- async function alreadyRunning(config, browserType, userDataDir) {
211
- const execPath = config.browser.launchOptions.executablePath ?? (0, import_processUtils.getBrowserExecPath)(config.browser.launchOptions.channel ?? browserType.name());
212
- if (!execPath)
213
- return false;
214
- return !!(0, import_processUtils.findBrowserProcess)(execPath, userDataDir);
215
- }
216
212
  async function injectCdpPort(browserConfig) {
217
213
  if (browserConfig.browserName === "chromium")
218
214
  browserConfig.launchOptions.cdpPort = await findFreePort();
@@ -238,7 +234,46 @@ async function startTraceServer(config, tracesDir) {
238
234
  function createHash(data) {
239
235
  return import_crypto.default.createHash("sha256").update(data).digest("hex").slice(0, 7);
240
236
  }
237
+ class SharedContextFactory {
238
+ static create(config) {
239
+ if (SharedContextFactory._instance)
240
+ throw new Error("SharedContextFactory already exists");
241
+ const baseConfig = { ...config, sharedBrowserContext: false };
242
+ const baseFactory = contextFactory(baseConfig);
243
+ SharedContextFactory._instance = new SharedContextFactory(baseFactory);
244
+ return SharedContextFactory._instance;
245
+ }
246
+ constructor(baseFactory) {
247
+ this._baseFactory = baseFactory;
248
+ }
249
+ async createContext(clientInfo, abortSignal, toolName) {
250
+ if (!this._contextPromise) {
251
+ (0, import_log.testDebug)("create shared browser context");
252
+ this._contextPromise = this._baseFactory.createContext(clientInfo, abortSignal, toolName);
253
+ }
254
+ const { browserContext } = await this._contextPromise;
255
+ (0, import_log.testDebug)(`shared context client connected`);
256
+ return {
257
+ browserContext,
258
+ close: async () => {
259
+ (0, import_log.testDebug)(`shared context client disconnected`);
260
+ }
261
+ };
262
+ }
263
+ static async dispose() {
264
+ await SharedContextFactory._instance?._dispose();
265
+ }
266
+ async _dispose() {
267
+ const contextPromise = this._contextPromise;
268
+ this._contextPromise = void 0;
269
+ if (!contextPromise)
270
+ return;
271
+ const { close } = await contextPromise;
272
+ await close();
273
+ }
274
+ }
241
275
  // Annotate the CommonJS export names for ESM import in node:
242
276
  0 && (module.exports = {
277
+ SharedContextFactory,
243
278
  contextFactory
244
279
  });
@@ -164,6 +164,7 @@ function configFromCLIOptions(cliOptions) {
164
164
  saveSession: cliOptions.saveSession,
165
165
  saveTrace: cliOptions.saveTrace,
166
166
  secrets: cliOptions.secrets,
167
+ sharedBrowserContext: cliOptions.sharedBrowserContext,
167
168
  outputDir: cliOptions.outputDir,
168
169
  imageResponses: cliOptions.imageResponses,
169
170
  timeouts: {
@@ -21,6 +21,7 @@ __export(watchdog_exports, {
21
21
  setupExitWatchdog: () => setupExitWatchdog
22
22
  });
23
23
  module.exports = __toCommonJS(watchdog_exports);
24
+ var import_browserContextFactory = require("./browserContextFactory");
24
25
  var import_context = require("./context");
25
26
  function setupExitWatchdog() {
26
27
  let isExiting = false;
@@ -30,6 +31,7 @@ function setupExitWatchdog() {
30
31
  isExiting = true;
31
32
  setTimeout(() => process.exit(0), 15e3);
32
33
  await import_context.Context.disposeAll();
34
+ await import_browserContextFactory.SharedContextFactory.dispose();
33
35
  process.exit(0);
34
36
  };
35
37
  process.stdin.on("close", handleExit);
@@ -41,7 +41,7 @@ var import_browserServerBackend = require("./browser/browserServerBackend");
41
41
  var import_extensionContextFactory = require("./extension/extensionContextFactory");
42
42
  var import_host = require("./vscode/host");
43
43
  function decorateCommand(command, version) {
44
- command.option("--allowed-origins <origins>", "semicolon-separated list of origins to allow the browser to request. Default is to allow all.", import_config.semicolonSeparatedList).option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.", import_config.semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", import_config.commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--cdp-header <headers...>", "CDP headers to send with the connect request, multiple can be specified.", import_config.headerParser).option("--config <path>", "path to the configuration file.").option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').option("--grant-permissions <permissions...>", 'List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".', import_config.commaSeparatedList).option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".').option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.").option("--secrets <path>", "path to a file containing secrets in the dotenv format", import_config.dotenvFileLoader).option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--timeout-action <timeout>", "specify action timeout in milliseconds, defaults to 5000ms", import_config.numberParser).option("--timeout-navigation <timeout>", "specify navigation timeout in milliseconds, defaults to 60000ms", import_config.numberParser).option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280, 720"').addOption(new import_utilsBundle.ProgramOption("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vscode", "VS Code tools.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
44
+ command.option("--allowed-origins <origins>", "semicolon-separated list of origins to allow the browser to request. Default is to allow all.", import_config.semicolonSeparatedList).option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.", import_config.semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", import_config.commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--cdp-header <headers...>", "CDP headers to send with the connect request, multiple can be specified.", import_config.headerParser).option("--config <path>", "path to the configuration file.").option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').option("--grant-permissions <permissions...>", 'List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".', import_config.commaSeparatedList).option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".').option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.").option("--secrets <path>", "path to a file containing secrets in the dotenv format", import_config.dotenvFileLoader).option("--shared-browser-context", "reuse the same browser context between all connected HTTP clients.").option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--timeout-action <timeout>", "specify action timeout in milliseconds, defaults to 5000ms", import_config.numberParser).option("--timeout-navigation <timeout>", "specify navigation timeout in milliseconds, defaults to 60000ms", import_config.numberParser).option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280, 720"').addOption(new import_utilsBundle.ProgramOption("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vscode", "VS Code tools.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
45
45
  (0, import_watchdog.setupExitWatchdog)();
46
46
  if (options.vision) {
47
47
  console.error("The --vision option is deprecated, use --caps=vision instead");
@@ -72,6 +72,12 @@ async function installHttpTransport(httpServer, serverBackendFactory) {
72
72
  const streamableSessions = /* @__PURE__ */ new Map();
73
73
  httpServer.on("request", async (req, res) => {
74
74
  const url = new URL(`http://localhost${req.url}`);
75
+ if (url.pathname === "/killkillkill" && req.method === "GET") {
76
+ res.statusCode = 200;
77
+ res.end("Killing process");
78
+ process.emit("SIGINT");
79
+ return;
80
+ }
75
81
  if (url.pathname.startsWith("/sse"))
76
82
  await handleSSE(serverBackendFactory, req, res, url, sseSessions);
77
83
  else
@@ -82,7 +82,10 @@ const initializeServer = async (server, backend, runHeartbeat) => {
82
82
  const capabilities = server.getClientCapabilities();
83
83
  let clientRoots = [];
84
84
  if (capabilities?.roots) {
85
- const { roots } = await server.listRoots();
85
+ const { roots } = await server.listRoots().catch((e) => {
86
+ serverDebug(e);
87
+ return { roots: [] };
88
+ });
86
89
  clientRoots = roots;
87
90
  }
88
91
  const clientInfo = {
@@ -41,6 +41,7 @@ var import_bundle = require("../sdk/bundle");
41
41
  var import_base = require("../../reporters/base");
42
42
  var import_list = __toESM(require("../../reporters/list"));
43
43
  var import_listModeReporter = __toESM(require("../../reporters/listModeReporter"));
44
+ var import_projectUtils = require("../../runner/projectUtils");
44
45
  var import_testTool = require("./testTool");
45
46
  var import_streams = require("./streams");
46
47
  const listTests = (0, import_testTool.defineTestTool)({
@@ -79,7 +80,8 @@ const runTests = (0, import_testTool.defineTestTool)({
79
80
  const testRunner = await context.createTestRunner();
80
81
  const result = await testRunner.runTests(reporter, {
81
82
  locations: params.locations,
82
- projects: params.projects
83
+ projects: params.projects,
84
+ disableConfigReporters: true
83
85
  });
84
86
  const text = stream.content();
85
87
  return {
@@ -114,7 +116,8 @@ const debugTest = (0, import_testTool.defineTestTool)({
114
116
  // For automatic recovery
115
117
  timeout: 0,
116
118
  workers: 1,
117
- pauseOnError: true
119
+ pauseOnError: true,
120
+ disableConfigReporters: true
118
121
  });
119
122
  const text = stream.content();
120
123
  return {
@@ -132,7 +135,7 @@ const setupPage = (0, import_testTool.defineTestTool)({
132
135
  description: "Setup the page for test",
133
136
  inputSchema: import_bundle.z.object({
134
137
  project: import_bundle.z.string().optional().describe('Project to use for setup. For example: "chromium", if no project is provided uses the first project in the config.'),
135
- testLocation: import_bundle.z.string().optional().describe('Location of the test to use for setup. For example: "test/e2e/file.spec.ts:20". Sets up blank page if no location is provided.')
138
+ testLocation: import_bundle.z.string().optional().describe('Location of the seed test to use for setup. For example: "test/seed/default.spec.ts:20".')
136
139
  }),
137
140
  type: "readOnly"
138
141
  },
@@ -143,16 +146,17 @@ const setupPage = (0, import_testTool.defineTestTool)({
143
146
  const testRunner = await context.createTestRunner();
144
147
  let testLocation = params.testLocation;
145
148
  if (!testLocation) {
146
- testLocation = ".template.spec.ts";
149
+ testLocation = "default.seed.spec.ts";
147
150
  const config = await testRunner.loadConfig();
148
- const project = params.project ? config.projects.find((p) => p.project.name === params.project) : config.projects[0];
151
+ const project = params.project ? config.projects.find((p) => p.project.name === params.project) : (0, import_projectUtils.findTopLevelProjects)(config)[0];
149
152
  const testDir = project?.project.testDir || configDir;
150
- const templateFile = import_path.default.join(testDir, testLocation);
151
- if (!import_fs.default.existsSync(templateFile)) {
152
- await import_fs.default.promises.writeFile(templateFile, `
153
- import { test, expect } from '@playwright/test';
154
- test('template', async ({ page }) => {});
155
- `);
153
+ const seedFile = import_path.default.join(testDir, testLocation);
154
+ if (!import_fs.default.existsSync(seedFile)) {
155
+ await import_fs.default.promises.mkdir(import_path.default.dirname(seedFile), { recursive: true });
156
+ await import_fs.default.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test';
157
+
158
+ test('seed', async ({ page }) => {});
159
+ `);
156
160
  }
157
161
  }
158
162
  const result = await testRunner.runTests(reporter, {
@@ -161,7 +165,8 @@ const setupPage = (0, import_testTool.defineTestTool)({
161
165
  projects: params.project ? [params.project] : void 0,
162
166
  timeout: 0,
163
167
  workers: 1,
164
- pauseAtEnd: true
168
+ pauseAtEnd: true,
169
+ disableConfigReporters: true
165
170
  });
166
171
  const text = stream.content();
167
172
  return {
package/lib/program.js CHANGED
@@ -173,12 +173,15 @@ function addTestMCPServerCommand(program3) {
173
173
  function addInitAgentsCommand(program3) {
174
174
  const command = program3.command("init-agents", { hidden: true });
175
175
  command.description("Initialize repository agents for the Claude Code");
176
- command.option("--claude", "Initialize repository agents for the Claude Code");
177
- command.option("--opencode", "Initialize repository agents for the Opencode");
176
+ const option = command.createOption("--loop <loop>", "Agentic loop provider");
177
+ option.choices(["claude", "opencode", "vscode"]);
178
+ command.addOption(option);
178
179
  command.action(async (opts) => {
179
- if (opts.opencode)
180
+ if (opts.loop === "opencode")
180
181
  await (0, import_generateAgents.initOpencodeRepo)();
181
- else
182
+ else if (opts.loop === "vscode")
183
+ await (0, import_generateAgents.initVSCodeRepo)();
184
+ else if (opts.loop === "claude")
182
185
  await (0, import_generateAgents.initClaudeCodeRepo)();
183
186
  });
184
187
  }
@@ -113,7 +113,24 @@ class HtmlReporter {
113
113
  else if (process.env.PLAYWRIGHT_HTML_NO_SNIPPETS)
114
114
  noSnippets = true;
115
115
  noSnippets = noSnippets || this._options.noSnippets;
116
- const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, process.env.PLAYWRIGHT_HTML_TITLE || this._options.title, noSnippets);
116
+ let noCopyPrompt;
117
+ if (process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT === "false" || process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT === "0")
118
+ noCopyPrompt = false;
119
+ else if (process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT)
120
+ noCopyPrompt = true;
121
+ noCopyPrompt = noCopyPrompt || this._options.noCopyPrompt;
122
+ let noFiles;
123
+ if (process.env.PLAYWRIGHT_HTML_NO_FILES === "false" || process.env.PLAYWRIGHT_HTML_NO_FILES === "0")
124
+ noFiles = false;
125
+ else if (process.env.PLAYWRIGHT_HTML_NO_FILES)
126
+ noFiles = true;
127
+ noFiles = noFiles || this._options.noFiles;
128
+ const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, {
129
+ title: process.env.PLAYWRIGHT_HTML_TITLE || this._options.title,
130
+ noSnippets,
131
+ noCopyPrompt,
132
+ noFiles
133
+ });
117
134
  this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors);
118
135
  }
119
136
  async onExit() {
@@ -197,41 +214,38 @@ function startHtmlReportServer(folder) {
197
214
  return server;
198
215
  }
199
216
  class HtmlBuilder {
200
- constructor(config, outputDir, attachmentsBaseURL, title, noSnippets = false) {
217
+ constructor(config, outputDir, attachmentsBaseURL, options) {
201
218
  this._stepsInFile = new import_utils.MultiMap();
202
219
  this._hasTraces = false;
203
220
  this._config = config;
204
221
  this._reportFolder = outputDir;
205
- this._noSnippets = noSnippets;
222
+ this._options = options;
206
223
  import_fs.default.mkdirSync(this._reportFolder, { recursive: true });
207
224
  this._dataZipFile = new import_zipBundle.yazl.ZipFile();
208
225
  this._attachmentsBaseURL = attachmentsBaseURL;
209
- this._title = title;
210
226
  }
211
227
  async build(metadata, projectSuites, result, topLevelErrors) {
212
228
  const data = /* @__PURE__ */ new Map();
213
229
  for (const projectSuite of projectSuites) {
230
+ const projectName = projectSuite.project().name;
214
231
  for (const fileSuite of projectSuite.suites) {
215
- const fileName = this._relativeLocation(fileSuite.location).file;
216
- const fileId = (0, import_utils.calculateSha1)((0, import_utils.toPosixPath)(fileName)).slice(0, 20);
217
- let fileEntry = data.get(fileId);
218
- if (!fileEntry) {
219
- fileEntry = {
220
- testFile: { fileId, fileName, tests: [] },
221
- testFileSummary: { fileId, fileName, tests: [], stats: emptyStats() }
222
- };
223
- data.set(fileId, fileEntry);
224
- }
225
- const { testFile, testFileSummary } = fileEntry;
226
- const testEntries = [];
227
- this._processSuite(fileSuite, projectSuite.project().name, [], testEntries);
228
- for (const test of testEntries) {
229
- testFile.tests.push(test.testCase);
230
- testFileSummary.tests.push(test.testCaseSummary);
232
+ if (this._options.noFiles) {
233
+ for (const describeSuite of fileSuite.suites) {
234
+ const groupName = describeSuite.title;
235
+ this._createEntryForSuite(data, projectName, describeSuite, groupName, true);
236
+ }
237
+ const hasTestsOutsideGroups = fileSuite.tests.length > 0;
238
+ if (hasTestsOutsideGroups) {
239
+ const fileName = "<anonymous>";
240
+ this._createEntryForSuite(data, projectName, fileSuite, fileName, false);
241
+ }
242
+ } else {
243
+ const fileName = this._relativeLocation(fileSuite.location).file;
244
+ this._createEntryForSuite(data, projectName, fileSuite, fileName, true);
231
245
  }
232
246
  }
233
247
  }
234
- if (!this._noSnippets)
248
+ if (!this._options.noSnippets)
235
249
  createSnippets(this._stepsInFile);
236
250
  let ok = true;
237
251
  for (const [fileId, { testFile, testFileSummary }] of data) {
@@ -260,13 +274,13 @@ class HtmlBuilder {
260
274
  }
261
275
  const htmlReport = {
262
276
  metadata,
263
- title: this._title,
264
277
  startTime: result.startTime.getTime(),
265
278
  duration: result.duration,
266
279
  files: [...data.values()].map((e) => e.testFileSummary),
267
280
  projectNames: projectSuites.map((r) => r.project().name),
268
281
  stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()) },
269
- errors: topLevelErrors.map((error) => (0, import_base.formatError)(import_base.internalScreen, error).message)
282
+ errors: topLevelErrors.map((error) => (0, import_base.formatError)(import_base.internalScreen, error).message),
283
+ options: this._options
270
284
  };
271
285
  htmlReport.files.sort((f1, f2) => {
272
286
  const w1 = f1.stats.unexpected * 1e3 + f1.stats.flaky;
@@ -331,13 +345,31 @@ class HtmlBuilder {
331
345
  _addDataFile(fileName, data) {
332
346
  this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
333
347
  }
334
- _processSuite(suite, projectName, path2, outTests) {
348
+ _createEntryForSuite(data, projectName, suite, fileName, deep) {
349
+ const fileId = (0, import_utils.calculateSha1)(fileName).slice(0, 20);
350
+ let fileEntry = data.get(fileId);
351
+ if (!fileEntry) {
352
+ fileEntry = {
353
+ testFile: { fileId, fileName, tests: [] },
354
+ testFileSummary: { fileId, fileName, tests: [], stats: emptyStats() }
355
+ };
356
+ data.set(fileId, fileEntry);
357
+ }
358
+ const { testFile, testFileSummary } = fileEntry;
359
+ const testEntries = [];
360
+ this._processSuite(suite, projectName, [], deep, testEntries);
361
+ for (const test of testEntries) {
362
+ testFile.tests.push(test.testCase);
363
+ testFileSummary.tests.push(test.testCaseSummary);
364
+ }
365
+ }
366
+ _processSuite(suite, projectName, path2, deep, outTests) {
335
367
  const newPath = [...path2, suite.title];
336
368
  suite.entries().forEach((e) => {
337
369
  if (e.type === "test")
338
370
  outTests.push(this._createTestEntry(e, projectName, newPath));
339
- else
340
- this._processSuite(e, projectName, newPath, outTests);
371
+ else if (deep)
372
+ this._processSuite(e, projectName, newPath, deep, outTests);
341
373
  });
342
374
  }
343
375
  _createTestEntry(test, projectName, path2) {
@@ -164,7 +164,7 @@ class Dispatcher {
164
164
  extraEnv: this._extraEnvByProjectId.get(testGroup.projectId) || {},
165
165
  outputDir,
166
166
  pauseOnError: this._failureTracker.pauseOnError(),
167
- pauseAtEnd: this._failureTracker.pauseAtEnd()
167
+ pauseAtEnd: this._failureTracker.pauseAtEnd(projectConfig)
168
168
  });
169
169
  const handleOutput = (params) => {
170
170
  const chunk = chunkFromParams(params);
@@ -26,11 +26,13 @@ class FailureTracker {
26
26
  this._config = _config;
27
27
  this._failureCount = 0;
28
28
  this._hasWorkerErrors = false;
29
+ this._topLevelProjects = [];
29
30
  this._pauseOnError = options?.pauseOnError ?? false;
30
31
  this._pauseAtEnd = options?.pauseAtEnd ?? false;
31
32
  }
32
- onRootSuite(rootSuite) {
33
+ onRootSuite(rootSuite, topLevelProjects) {
33
34
  this._rootSuite = rootSuite;
35
+ this._topLevelProjects = topLevelProjects;
34
36
  }
35
37
  onTestEnd(test, result) {
36
38
  if (test.outcome() === "unexpected" && test.results.length > test.retries)
@@ -42,8 +44,8 @@ class FailureTracker {
42
44
  pauseOnError() {
43
45
  return this._pauseOnError;
44
46
  }
45
- pauseAtEnd() {
46
- return this._pauseAtEnd;
47
+ pauseAtEnd(inProject) {
48
+ return this._pauseAtEnd && this._topLevelProjects.includes(inProject);
47
49
  }
48
50
  hasReachedMaxFailures() {
49
51
  return this.maxFailures() > 0 && this._failureCount >= this.maxFailures();
@@ -168,14 +168,17 @@ async function createRootSuite(testRun, errors, shouldFilterOnly) {
168
168
  }
169
169
  if (config.postShardTestFilters.length)
170
170
  (0, import_suiteUtils.filterTestsRemoveEmptySuites)(rootSuite, (test) => config.postShardTestFilters.every((filter) => filter(test)));
171
+ const topLevelProjects = [];
171
172
  {
172
173
  const projectClosure2 = new Map((0, import_projectUtils.buildProjectsClosure)(rootSuite.suites.map((suite) => suite._fullProject)));
173
174
  for (const [project, level] of projectClosure2.entries()) {
174
175
  if (level === "dependency")
175
176
  rootSuite._prependSuite(buildProjectSuite(project, projectSuites.get(project)));
177
+ else
178
+ topLevelProjects.push(project);
176
179
  }
177
180
  }
178
- return rootSuite;
181
+ return { rootSuite, topLevelProjects };
179
182
  }
180
183
  function createProjectSuite(project, fileSuites) {
181
184
  const projectSuite = new import_test.Suite(project.project.name, "project");
@@ -32,7 +32,8 @@ __export(projectUtils_exports, {
32
32
  buildProjectsClosure: () => buildProjectsClosure,
33
33
  buildTeardownToSetupsMap: () => buildTeardownToSetupsMap,
34
34
  collectFilesForProject: () => collectFilesForProject,
35
- filterProjects: () => filterProjects
35
+ filterProjects: () => filterProjects,
36
+ findTopLevelProjects: () => findTopLevelProjects
36
37
  });
37
38
  module.exports = __toCommonJS(projectUtils_exports);
38
39
  var import_fs = __toESM(require("fs"));
@@ -116,6 +117,10 @@ function buildProjectsClosure(projects, hasTests) {
116
117
  visit(0, p);
117
118
  return result;
118
119
  }
120
+ function findTopLevelProjects(config) {
121
+ const closure = buildProjectsClosure(config.projects);
122
+ return [...closure].filter((entry) => entry[1] === "top-level").map((entry) => entry[0]);
123
+ }
119
124
  function buildDependentProjects(forProjects, projects) {
120
125
  const reverseDeps = new Map(projects.map((p) => [p, []]));
121
126
  for (const project of projects) {
@@ -231,5 +236,6 @@ async function collectFiles(testDir, respectGitIgnore) {
231
236
  buildProjectsClosure,
232
237
  buildTeardownToSetupsMap,
233
238
  collectFilesForProject,
234
- filterProjects
239
+ filterProjects,
240
+ findTopLevelProjects
235
241
  });
@@ -65,6 +65,7 @@ class TestRun {
65
65
  this.phases = [];
66
66
  this.projectFiles = /* @__PURE__ */ new Map();
67
67
  this.projectSuites = /* @__PURE__ */ new Map();
68
+ this.topLevelProjects = [];
68
69
  this.config = config;
69
70
  this.reporter = reporter;
70
71
  this.failureTracker = new import_failureTracker.FailureTracker(config, options);
@@ -214,8 +215,9 @@ function createListFilesTask() {
214
215
  return {
215
216
  title: "load tests",
216
217
  setup: async (testRun, errors) => {
217
- testRun.rootSuite = await (0, import_loadUtils.createRootSuite)(testRun, errors, false);
218
- testRun.failureTracker.onRootSuite(testRun.rootSuite);
218
+ const { rootSuite, topLevelProjects } = await (0, import_loadUtils.createRootSuite)(testRun, errors, false);
219
+ testRun.rootSuite = rootSuite;
220
+ testRun.failureTracker.onRootSuite(rootSuite, topLevelProjects);
219
221
  await (0, import_loadUtils.collectProjectsAndTestFiles)(testRun, false);
220
222
  for (const [project, files] of testRun.projectFiles) {
221
223
  const projectSuite = new import_test.Suite(project.project.name, "project");
@@ -247,8 +249,9 @@ function createLoadTask(mode, options) {
247
249
  const changedFiles = await (0, import_vcs.detectChangedTestFiles)(testRun.config.cliOnlyChanged, testRun.config.configDir);
248
250
  testRun.config.preOnlyTestFilters.push((test) => changedFiles.has(test.location.file));
249
251
  }
250
- testRun.rootSuite = await (0, import_loadUtils.createRootSuite)(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
251
- testRun.failureTracker.onRootSuite(testRun.rootSuite);
252
+ const { rootSuite, topLevelProjects } = await (0, import_loadUtils.createRootSuite)(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
253
+ testRun.rootSuite = rootSuite;
254
+ testRun.failureTracker.onRootSuite(rootSuite, topLevelProjects);
252
255
  if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard && !testRun.config.cliOnlyChanged) {
253
256
  if (testRun.config.cliArgs.length) {
254
257
  throw new Error([
@@ -269,12 +269,12 @@ class TestRunner extends import_events.default {
269
269
  const testIdSet = new Set(params.testIds);
270
270
  config.preOnlyTestFilters.push((test) => testIdSet.has(test.id));
271
271
  }
272
- const configReporters = await (0, import_reporters.createReporters)(config, "test", true);
272
+ const configReporters = params.disableConfigReporters ? [] : await (0, import_reporters.createReporters)(config, "test", true);
273
273
  const reporter = new import_internalReporter.InternalReporter([...configReporters, userReporter]);
274
274
  const stop = new import_utils.ManualPromise();
275
275
  const tasks = [
276
276
  (0, import_tasks.createApplyRebaselinesTask)(),
277
- (0, import_tasks.createLoadTask)("out-of-process", { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }),
277
+ (0, import_tasks.createLoadTask)("out-of-process", { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: params.doNotRunDepsOutsideProjectFilter }),
278
278
  ...(0, import_tasks.createRunTestsTasks)(config)
279
279
  ];
280
280
  const testRun = new import_tasks.TestRun(config, reporter, { pauseOnError: params.pauseOnError, pauseAtEnd: params.pauseAtEnd });
@@ -149,7 +149,10 @@ class TestServerDispatcher {
149
149
  }
150
150
  async runTests(params) {
151
151
  const wireReporter = await this._wireReporter((e) => this._dispatchEvent("report", e));
152
- const { status } = await this._testRunner.runTests(wireReporter, params);
152
+ const { status } = await this._testRunner.runTests(wireReporter, {
153
+ ...params,
154
+ doNotRunDepsOutsideProjectFilter: true
155
+ });
153
156
  return { status };
154
157
  }
155
158
  async watch(params) {