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.
- package/lib/agents/generateAgents.js +78 -32
- package/lib/agents/generator.md +83 -49
- package/lib/agents/{fixer.md → healer.md} +16 -20
- package/lib/agents/planner.md +81 -34
- package/lib/mcp/browser/browserContextFactory.js +67 -32
- package/lib/mcp/browser/config.js +1 -0
- package/lib/mcp/browser/watchdog.js +2 -0
- package/lib/mcp/program.js +1 -1
- package/lib/mcp/sdk/http.js +6 -0
- package/lib/mcp/sdk/server.js +4 -1
- package/lib/mcp/test/testTools.js +17 -12
- package/lib/program.js +7 -4
- package/lib/reporters/html.js +58 -26
- package/lib/runner/dispatcher.js +1 -1
- package/lib/runner/failureTracker.js +5 -3
- package/lib/runner/loadUtils.js +4 -1
- package/lib/runner/projectUtils.js +8 -2
- package/lib/runner/tasks.js +7 -4
- package/lib/runner/testRunner.js +2 -2
- package/lib/runner/testServer.js +4 -1
- package/lib/transform/compilationCache.js +22 -5
- package/package.json +2 -2
- package/types/test.d.ts +11 -1
- package/lib/mcp/browser/processUtils.js +0 -102
|
@@ -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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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);
|
package/lib/mcp/program.js
CHANGED
|
@@ -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");
|
package/lib/mcp/sdk/http.js
CHANGED
|
@@ -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
|
package/lib/mcp/sdk/server.js
CHANGED
|
@@ -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/
|
|
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 = ".
|
|
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
|
|
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
|
|
151
|
-
if (!import_fs.default.existsSync(
|
|
152
|
-
await import_fs.default.promises.
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
177
|
-
|
|
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
|
}
|
package/lib/reporters/html.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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.
|
|
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
|
-
|
|
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) {
|
package/lib/runner/dispatcher.js
CHANGED
|
@@ -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();
|
package/lib/runner/loadUtils.js
CHANGED
|
@@ -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
|
});
|
package/lib/runner/tasks.js
CHANGED
|
@@ -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
|
-
|
|
218
|
-
testRun.
|
|
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
|
-
|
|
251
|
-
testRun.
|
|
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([
|
package/lib/runner/testRunner.js
CHANGED
|
@@ -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:
|
|
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 });
|
package/lib/runner/testServer.js
CHANGED
|
@@ -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,
|
|
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) {
|