Update npm packages (73 packages including @jqhtml 2.3.36)

Update npm registry domain from privatenpm.hanson.xyz to npm.internal.hanson.xyz

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-02-20 11:31:28 +00:00
parent d01a6179aa
commit b5eb27a827
1690 changed files with 47348 additions and 16848 deletions

8
node_modules/playwright/README.md generated vendored
View File

@@ -1,16 +1,16 @@
# 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-143.0.7499.4-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-144.0.2-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord)
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-145.0.7632.6-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-146.0.1-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
Playwright is a framework for Web Testing and Automation. It allows testing [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable** and **fast**.
Playwright is a framework for Web Testing and Automation. It allows testing [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable**, and **fast**.
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->143.0.7499.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->145.0.7632.6<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->144.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->146.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.

File diff suppressed because it is too large Load Diff

89
node_modules/playwright/lib/agents/agentParser.js generated vendored Normal file
View File

@@ -0,0 +1,89 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var agentParser_exports = {};
__export(agentParser_exports, {
parseAgentSpec: () => parseAgentSpec
});
module.exports = __toCommonJS(agentParser_exports);
var import_fs = __toESM(require("fs"));
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
async function parseAgentSpec(filePath) {
const source = await import_fs.default.promises.readFile(filePath, "utf-8");
const { header, content } = extractYamlAndContent(source);
const { instructions, examples } = extractInstructionsAndExamples(content);
return {
...header,
instructions,
examples
};
}
function extractYamlAndContent(markdown) {
const lines = markdown.split("\n");
if (lines[0] !== "---")
throw new Error("Markdown file must start with YAML front matter (---)");
let yamlEndIndex = -1;
for (let i = 1; i < lines.length; i++) {
if (lines[i] === "---") {
yamlEndIndex = i;
break;
}
}
if (yamlEndIndex === -1)
throw new Error("YAML front matter must be closed with ---");
const yamlLines = lines.slice(1, yamlEndIndex);
const yamlRaw = yamlLines.join("\n");
const contentLines = lines.slice(yamlEndIndex + 1);
const content = contentLines.join("\n");
let header;
try {
header = import_utilsBundle.yaml.parse(yamlRaw);
} catch (error) {
throw new Error(`Failed to parse YAML header: ${error.message}`);
}
if (!header.name)
throw new Error('YAML header must contain a "name" field');
if (!header.description)
throw new Error('YAML header must contain a "description" field');
return { header, content };
}
function extractInstructionsAndExamples(content) {
const examples = [];
const instructions = content.split("<example>")[0].trim();
const exampleRegex = /<example>([\s\S]*?)<\/example>/g;
let match;
while ((match = exampleRegex.exec(content)) !== null) {
const example = match[1].trim();
examples.push(example.replace(/[\n]/g, " ").replace(/ +/g, " "));
}
return { instructions, examples };
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
parseAgentSpec
});

View File

@@ -39,67 +39,20 @@ var import_path = __toESM(require("path"));
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var import_utils = require("playwright-core/lib/utils");
var import_seed = require("../mcp/test/seed");
class AgentParser {
static async loadAgents() {
const files = await import_fs.default.promises.readdir(__dirname);
return Promise.all(files.filter((file) => file.endsWith(".agent.md")).map((file) => AgentParser.parseFile(import_path.default.join(__dirname, file))));
}
static async parseFile(filePath) {
const source = await import_fs.default.promises.readFile(filePath, "utf-8");
const { header, content } = this.extractYamlAndContent(source);
const { instructions, examples } = this.extractInstructionsAndExamples(content);
return { header, instructions, examples };
}
static extractYamlAndContent(markdown) {
const lines = markdown.split("\n");
if (lines[0] !== "---")
throw new Error("Markdown file must start with YAML front matter (---)");
let yamlEndIndex = -1;
for (let i = 1; i < lines.length; i++) {
if (lines[i] === "---") {
yamlEndIndex = i;
break;
}
}
if (yamlEndIndex === -1)
throw new Error("YAML front matter must be closed with ---");
const yamlLines = lines.slice(1, yamlEndIndex);
const yamlRaw = yamlLines.join("\n");
const contentLines = lines.slice(yamlEndIndex + 1);
const content = contentLines.join("\n");
let header;
try {
header = import_utilsBundle.yaml.parse(yamlRaw);
} catch (error) {
throw new Error(`Failed to parse YAML header: ${error.message}`);
}
if (!header.name)
throw new Error('YAML header must contain a "name" field');
if (!header.description)
throw new Error('YAML header must contain a "description" field');
return { header, content };
}
static extractInstructionsAndExamples(content) {
const examples = [];
const instructions = content.split("<example>")[0].trim();
const exampleRegex = /<example>([\s\S]*?)<\/example>/g;
let match;
while ((match = exampleRegex.exec(content)) !== null) {
const example = match[1].trim();
examples.push(example.replace(/[\n]/g, " ").replace(/ +/g, " "));
}
return { instructions, examples };
}
var import_agentParser = require("./agentParser");
async function loadAgentSpecs() {
const files = await import_fs.default.promises.readdir(__dirname);
return Promise.all(files.filter((file) => file.endsWith(".agent.md")).map((file) => (0, import_agentParser.parseAgentSpec)(import_path.default.join(__dirname, file))));
}
class ClaudeGenerator {
static async init(config, projectName, prompts) {
await initRepo(config, projectName, {
promptsFolder: prompts ? ".claude/prompts" : void 0
});
const agents = await AgentParser.loadAgents();
const agents = await loadAgentSpecs();
await import_fs.default.promises.mkdir(".claude/agents", { recursive: true });
for (const agent of agents)
await writeFile(`.claude/agents/${agent.header.name}.md`, ClaudeGenerator.agentSpec(agent), "\u{1F916}", "agent definition");
await writeFile(`.claude/agents/${agent.name}.md`, ClaudeGenerator.agentSpec(agent), "\u{1F916}", "agent definition");
await writeFile(".mcp.json", JSON.stringify({
mcpServers: {
"playwright-test": {
@@ -124,11 +77,11 @@ class ClaudeGenerator {
const examples = agent.examples.length ? ` Examples: ${agent.examples.map((example) => `<example>${example}</example>`).join("")}` : "";
const lines = [];
const header = {
name: agent.header.name,
description: agent.header.description + examples,
tools: agent.header.tools.map((tool) => asClaudeTool(tool)).join(", "),
model: agent.header.model,
color: agent.header.color
name: agent.name,
description: agent.description + examples,
tools: agent.tools.map((tool) => asClaudeTool(tool)).join(", "),
model: agent.model,
color: agent.color
};
lines.push(`---`);
lines.push(import_utilsBundle.yaml.stringify(header, { lineWidth: 1e5 }) + `---`);
@@ -143,12 +96,12 @@ class OpencodeGenerator {
defaultAgentName: "Build",
promptsFolder: prompts ? ".opencode/prompts" : void 0
});
const agents = await AgentParser.loadAgents();
const agents = await loadAgentSpecs();
for (const agent of agents) {
const prompt = [agent.instructions];
prompt.push("");
prompt.push(...agent.examples.map((example) => `<example>${example}</example>`));
await writeFile(`.opencode/prompts/${agent.header.name}.md`, prompt.join("\n"), "\u{1F916}", "agent definition");
await writeFile(`.opencode/prompts/${agent.name}.md`, prompt.join("\n"), "\u{1F916}", "agent definition");
}
await writeFile("opencode.json", OpencodeGenerator.configuration(agents), "\u{1F527}", "opencode configuration");
initRepoDone();
@@ -176,13 +129,13 @@ class OpencodeGenerator {
result["agent"] = {};
for (const agent of agents) {
const tools = {};
result["agent"][agent.header.name] = {
description: agent.header.description,
result["agent"][agent.name] = {
description: agent.description,
mode: "subagent",
prompt: `{file:.opencode/prompts/${agent.header.name}.md}`,
prompt: `{file:.opencode/prompts/${agent.name}.md}`,
tools
};
for (const tool of agent.header.tools)
for (const tool of agent.tools)
asOpencodeTool(tools, tool);
}
result["mcp"]["playwright-test"] = {
@@ -200,10 +153,10 @@ class CopilotGenerator {
promptsFolder: prompts ? ".github/prompts" : void 0,
promptSuffix: "prompt"
});
const agents = await AgentParser.loadAgents();
const agents = await loadAgentSpecs();
await import_fs.default.promises.mkdir(".github/agents", { recursive: true });
for (const agent of agents)
await writeFile(`.github/agents/${agent.header.name}.agent.md`, CopilotGenerator.agentSpec(agent), "\u{1F916}", "agent definition");
await writeFile(`.github/agents/${agent.name}.agent.md`, CopilotGenerator.agentSpec(agent), "\u{1F916}", "agent definition");
await deleteFile(`.github/chatmodes/ \u{1F3AD} planner.chatmode.md`, "legacy planner chatmode");
await deleteFile(`.github/chatmodes/\u{1F3AD} generator.chatmode.md`, "legacy generator chatmode");
await deleteFile(`.github/chatmodes/\u{1F3AD} healer.chatmode.md`, "legacy healer chatmode");
@@ -228,14 +181,14 @@ class CopilotGenerator {
const examples = agent.examples.length ? ` Examples: ${agent.examples.map((example) => `<example>${example}</example>`).join("")}` : "";
const lines = [];
const header = {
"name": agent.header.name,
"description": agent.header.description + examples,
"tools": agent.header.tools,
"name": agent.name,
"description": agent.description + examples,
"tools": agent.tools,
"model": "Claude Sonnet 4",
"mcp-servers": CopilotGenerator.mcpServers
};
lines.push(`---`);
lines.push(import_utilsBundle.yaml.stringify(header) + `---`);
lines.push(import_utilsBundle.yaml.stringify(header, { lineWidth: 1e5 }) + `---`);
lines.push("");
lines.push(agent.instructions);
lines.push("");
@@ -260,7 +213,7 @@ class VSCodeGenerator {
await initRepo(config, projectName, {
promptsFolder: void 0
});
const agents = await AgentParser.loadAgents();
const agents = await loadAgentSpecs();
const nameMap = /* @__PURE__ */ new Map([
["playwright-test-planner", " \u{1F3AD} planner"],
["playwright-test-generator", "\u{1F3AD} generator"],
@@ -268,7 +221,7 @@ class VSCodeGenerator {
]);
await import_fs.default.promises.mkdir(".github/chatmodes", { recursive: true });
for (const agent of agents)
await writeFile(`.github/chatmodes/${nameMap.get(agent.header.name)}.chatmode.md`, VSCodeGenerator.agentSpec(agent), "\u{1F916}", "chatmode definition");
await writeFile(`.github/chatmodes/${nameMap.get(agent.name)}.chatmode.md`, VSCodeGenerator.agentSpec(agent), "\u{1F916}", "chatmode definition");
await VSCodeGenerator.appendToMCPJson();
initRepoDone();
}
@@ -307,7 +260,7 @@ class VSCodeGenerator {
return `${vscodeMcpName}/${second}`;
return vscodeToolMap.get(first) || first;
}
const tools = agent.header.tools.map(asVscodeTool).flat().sort((a, b) => {
const tools = agent.tools.map(asVscodeTool).flat().sort((a, b) => {
const indexA = vscodeToolsOrder.indexOf(a);
const indexB = vscodeToolsOrder.indexOf(b);
if (indexA === -1 && indexB === -1)
@@ -320,7 +273,7 @@ class VSCodeGenerator {
}).map((tool) => `'${tool}'`).join(", ");
const lines = [];
lines.push(`---`);
lines.push(`description: ${agent.header.description}.`);
lines.push(`description: ${agent.description}.`);
lines.push(`tools: [${tools}]`);
lines.push(`---`);
lines.push("");

View File

@@ -17,6 +17,7 @@ tools:
- playwright-test/browser_navigate_back
- playwright-test/browser_network_requests
- playwright-test/browser_press_key
- playwright-test/browser_run_code
- playwright-test/browser_select_option
- playwright-test/browser_snapshot
- playwright-test/browser_take_screenshot

View File

@@ -87,20 +87,22 @@ class FullConfigInternal {
maxFailures: takeFirst(configCLIOverrides.debug ? 1 : void 0, configCLIOverrides.maxFailures, userConfig.maxFailures, 0),
metadata: metadata ?? userConfig.metadata,
preserveOutput: takeFirst(userConfig.preserveOutput, "always"),
projects: [],
quiet: takeFirst(configCLIOverrides.quiet, userConfig.quiet, false),
reporter: takeFirst(configCLIOverrides.reporter, resolveReporters(userConfig.reporter, configDir), [[defaultReporter]]),
reportSlowTests: takeFirst(userConfig.reportSlowTests, {
max: 5,
threshold: 3e5
/* 5 minutes */
}),
quiet: takeFirst(configCLIOverrides.quiet, userConfig.quiet, false),
projects: [],
// @ts-expect-error runAgents is hidden
runAgents: takeFirst(configCLIOverrides.runAgents, "none"),
shard: takeFirst(configCLIOverrides.shard, userConfig.shard, null),
tags: globalTags,
updateSnapshots: takeFirst(configCLIOverrides.updateSnapshots, userConfig.updateSnapshots, "missing"),
updateSourceMethod: takeFirst(configCLIOverrides.updateSourceMethod, userConfig.updateSourceMethod, "patch"),
version: require("../../package.json").version,
workers: resolveWorkers(takeFirst(configCLIOverrides.debug ? 1 : void 0, configCLIOverrides.workers, userConfig.workers, "50%")),
workers: resolveWorkers(takeFirst(configCLIOverrides.debug || configCLIOverrides.pause ? 1 : void 0, configCLIOverrides.workers, userConfig.workers, "50%")),
webServer: null
};
for (const key in userConfig) {

View File

@@ -44,6 +44,8 @@ let loaderChannel;
function registerESMLoader() {
if (process.env.PW_DISABLE_TS_ESM)
return true;
if ("Bun" in globalThis)
return true;
if (loaderChannel)
return true;
const register = require("node:module").register;

View File

@@ -18,26 +18,11 @@ var __copyProps = (to, from, except, desc) => {
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var expectBundle_exports = {};
__export(expectBundle_exports, {
DIM_COLOR: () => DIM_COLOR,
EXPECTED_COLOR: () => EXPECTED_COLOR,
INVERTED_COLOR: () => INVERTED_COLOR,
RECEIVED_COLOR: () => RECEIVED_COLOR,
expect: () => expect,
printReceived: () => printReceived
expect: () => expect
});
module.exports = __toCommonJS(expectBundle_exports);
const expect = require("./expectBundleImpl").expect;
const EXPECTED_COLOR = require("./expectBundleImpl").EXPECTED_COLOR;
const INVERTED_COLOR = require("./expectBundleImpl").INVERTED_COLOR;
const RECEIVED_COLOR = require("./expectBundleImpl").RECEIVED_COLOR;
const DIM_COLOR = require("./expectBundleImpl").DIM_COLOR;
const printReceived = require("./expectBundleImpl").printReceived;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
DIM_COLOR,
EXPECTED_COLOR,
INVERTED_COLOR,
RECEIVED_COLOR,
expect,
printReceived
expect
});

File diff suppressed because one or more lines are too long

View File

@@ -30,6 +30,13 @@ class ProcessRunner {
const response = { method, params };
sendMessageToParent({ method: "__dispatch__", params: response });
}
async sendRequest(method, params) {
return await sendRequestToParent(method, params);
}
async sendMessageNoReply(method, params) {
void sendRequestToParent(method, params).catch(() => {
});
}
}
let gracefullyCloseCalled = false;
let forceExitInitiated = false;
@@ -70,6 +77,8 @@ process.on("message", async (message) => {
sendMessageToParent({ method: "__dispatch__", params: response });
}
}
if (message.method === "__response__")
handleResponseFromParent(message.params);
});
const kForceExitTimeout = +(process.env.PWTEST_FORCE_EXIT_TIMEOUT || 3e4);
async function gracefullyCloseAndExit(forceExit) {
@@ -98,6 +107,25 @@ function sendMessageToParent(message) {
}
}
}
let lastId = 0;
const requestCallbacks = /* @__PURE__ */ new Map();
async function sendRequestToParent(method, params) {
const id = ++lastId;
sendMessageToParent({ method: "__request__", params: { id, method, params } });
const promise = new import_utils.ManualPromise();
requestCallbacks.set(id, promise);
return promise;
}
function handleResponseFromParent(response) {
const promise = requestCallbacks.get(response.id);
if (!promise)
return;
requestCallbacks.delete(response.id);
if (response.error)
promise.reject(new Error(response.error.message));
else
promise.resolve(response.result);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ProcessRunner

View File

@@ -22,21 +22,21 @@ __export(validators_exports, {
validateTestDetails: () => validateTestDetails
});
module.exports = __toCommonJS(validators_exports);
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
const testAnnotationSchema = import_utilsBundle.zod.object({
type: import_utilsBundle.zod.string(),
description: import_utilsBundle.zod.string().optional()
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
const testAnnotationSchema = import_mcpBundle.z.object({
type: import_mcpBundle.z.string(),
description: import_mcpBundle.z.string().optional()
});
const testDetailsSchema = import_utilsBundle.zod.object({
tag: import_utilsBundle.zod.union([
import_utilsBundle.zod.string().optional(),
import_utilsBundle.zod.array(import_utilsBundle.zod.string())
const testDetailsSchema = import_mcpBundle.z.object({
tag: import_mcpBundle.z.union([
import_mcpBundle.z.string().optional(),
import_mcpBundle.z.array(import_mcpBundle.z.string())
]).transform((val) => Array.isArray(val) ? val : val !== void 0 ? [val] : []).refine((val) => val.every((v) => v.startsWith("@")), {
message: "Tag must start with '@'"
}),
annotation: import_utilsBundle.zod.union([
annotation: import_mcpBundle.z.union([
testAnnotationSchema,
import_utilsBundle.zod.array(testAnnotationSchema).optional()
import_mcpBundle.z.array(testAnnotationSchema).optional()
]).transform((val) => Array.isArray(val) ? val : val !== void 0 ? [val] : [])
});
function validateTestAnnotation(annotation) {

62
node_modules/playwright/lib/index.js generated vendored
View File

@@ -138,6 +138,7 @@ const playwrightFixtures = {
}, { option: true, box: true }],
serviceWorkers: [({ contextOptions }, use) => use(contextOptions.serviceWorkers ?? "allow"), { option: true, box: true }],
contextOptions: [{}, { option: true, box: true }],
agentOptions: [void 0, { option: true, box: true }],
_combinedContextOptions: [async ({
acceptDownloads,
bypassCSP,
@@ -162,7 +163,7 @@ const playwrightFixtures = {
baseURL,
contextOptions,
serviceWorkers
}, use) => {
}, use, testInfo) => {
const options = {};
if (acceptDownloads !== void 0)
options.acceptDownloads = acceptDownloads;
@@ -213,21 +214,19 @@ const playwrightFixtures = {
...options
});
}, { box: true }],
_setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
_setupContextOptions: [async ({ playwright, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
if (testIdAttribute)
playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute);
testInfo.snapshotSuffix = process.platform;
if ((0, import_utils.debugMode)() === "inspector")
testInfo._setDebugMode();
playwright._defaultContextOptions = _combinedContextOptions;
playwright._defaultContextTimeout = actionTimeout || 0;
playwright._defaultContextNavigationTimeout = navigationTimeout || 0;
await use();
playwright._defaultContextOptions = void 0;
playwright._defaultContextTimeout = void 0;
playwright._defaultContextNavigationTimeout = void 0;
}, { auto: "all-hooks-included", title: "context configuration", box: true }],
_setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => {
_setupArtifacts: [async ({ playwright, screenshot, _combinedContextOptions }, use, testInfo) => {
testInfo.setTimeout(testInfo.project.timeout);
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
await artifactsRecorder.willStartTest(testInfo);
@@ -275,20 +274,32 @@ const playwrightFixtures = {
if (!keepTestTimeout)
(0, import_globals.currentTestInfo)()?._setDebugMode();
},
runBeforeCreateBrowserContext: async (options) => {
for (const [key, value] of Object.entries(_combinedContextOptions)) {
if (!(key in options))
options[key] = value;
}
},
runBeforeCreateRequestContext: async (options) => {
for (const [key, value] of Object.entries(_combinedContextOptions)) {
if (!(key in options))
options[key] = value;
}
},
runAfterCreateBrowserContext: async (context) => {
await artifactsRecorder?.didCreateBrowserContext(context);
await artifactsRecorder.didCreateBrowserContext(context);
const testInfo2 = (0, import_globals.currentTestInfo)();
if (testInfo2)
attachConnectedHeaderIfNeeded(testInfo2, context.browser());
},
runAfterCreateRequestContext: async (context) => {
await artifactsRecorder?.didCreateRequestContext(context);
await artifactsRecorder.didCreateRequestContext(context);
},
runBeforeCloseBrowserContext: async (context) => {
await artifactsRecorder?.willCloseBrowserContext(context);
await artifactsRecorder.willCloseBrowserContext(context);
},
runBeforeCloseRequestContext: async (context) => {
await artifactsRecorder?.willCloseRequestContext(context);
await artifactsRecorder.willCloseRequestContext(context);
}
};
const clientInstrumentation = playwright._instrumentation;
@@ -401,6 +412,39 @@ const playwrightFixtures = {
page = await context.newPage();
await use(page);
},
agent: async ({ page, agentOptions }, use, testInfo) => {
const testInfoImpl = testInfo;
const cachePathTemplate = agentOptions?.cachePathTemplate ?? "{testDir}/{testFilePath}-cache.json";
const resolvedCacheFile = testInfoImpl._applyPathTemplate(cachePathTemplate, "", ".json");
const cacheFile = testInfoImpl.config.runAgents === "all" ? void 0 : await testInfoImpl._cloneStorage(resolvedCacheFile);
const cacheOutFile = import_path.default.join(testInfoImpl.artifactsDir(), "agent-cache-" + (0, import_utils.createGuid)() + ".json");
const provider = agentOptions?.provider && testInfo.config.runAgents !== "none" ? agentOptions.provider : void 0;
if (provider)
testInfo.setTimeout(0);
const cache = {
cacheFile,
cacheOutFile
};
const agent = await page.agent({
provider,
cache,
limits: agentOptions?.limits,
secrets: agentOptions?.secrets,
systemPrompt: agentOptions?.systemPrompt,
expect: {
timeout: testInfoImpl._projectInternal.expect?.timeout
}
});
await use(agent);
const usage = await agent.usage();
if (usage.turns > 0)
await testInfoImpl.attach("agent-usage", { contentType: "application/json", body: Buffer.from(JSON.stringify(usage, null, 2)) });
if (!resolvedCacheFile || !cacheOutFile)
return;
if (testInfo.status !== "passed")
return;
await testInfoImpl._upstreamStorage(resolvedCacheFile, cacheOutFile);
},
request: async ({ playwright }, use) => {
const request = await playwright.request.newContext();
await use(request);

View File

@@ -58,6 +58,10 @@ class TeleReporterReceiver {
this._onTestBegin(params.testId, params.result);
return;
}
if (method === "onTestPaused") {
this._onTestPaused(params.testId, params.resultId, params.errors);
return;
}
if (method === "onTestEnd") {
this._onTestEnd(params.test, params.result);
return;
@@ -116,6 +120,13 @@ class TeleReporterReceiver {
testResult.setStartTimeNumber(payload.startTime);
this._reporter.onTestBegin?.(test, testResult);
}
_onTestPaused(testId, resultId, errors) {
const test = this._tests.get(testId);
const result = test.results.find((r) => r._id === resultId);
result.errors.push(...errors);
result.error = result.errors[0];
void this._reporter.onTestPaused?.(test, result);
}
_onTestEnd(testEndPayload, payload) {
const test = this._tests.get(testEndPayload.testId);
test.timeout = testEndPayload.timeout;
@@ -123,8 +134,8 @@ class TeleReporterReceiver {
const result = test.results.find((r) => r._id === payload.id);
result.duration = payload.duration;
result.status = payload.status;
result.errors = payload.errors;
result.error = result.errors?.[0];
result.errors.push(...payload.errors ?? []);
result.error = result.errors[0];
if (!!payload.attachments)
result.attachments = this._parseAttachments(payload.attachments);
if (payload.annotations) {
@@ -448,6 +459,8 @@ const baseFullConfig = {
tags: [],
updateSnapshots: "missing",
updateSourceMethod: "patch",
// @ts-expect-error runAgents is hidden
runAgents: "none",
version: "",
workers: 0,
webServer: null

View File

@@ -36,7 +36,7 @@ class TeleSuiteUpdater {
this._receiver = new import_teleReceiver.TeleReporterReceiver(this._createReporter(), {
mergeProjects: true,
mergeTestCases: true,
resolvePath: (rootDir, relativePath) => rootDir + options.pathSeparator + relativePath,
resolvePath: createPathResolve(options.pathSeparator),
clearPreviousResultsWhenTestBegins: true
});
this._options = options;
@@ -44,8 +44,8 @@ class TeleSuiteUpdater {
_createReporter() {
return {
version: () => "v2",
onConfigure: (c) => {
this.config = c;
onConfigure: (config) => {
this.config = config;
this._lastRunReceiver = new import_teleReceiver.TeleReporterReceiver({
version: () => "v2",
onBegin: (suite) => {
@@ -55,8 +55,9 @@ class TeleSuiteUpdater {
}, {
mergeProjects: true,
mergeTestCases: false,
resolvePath: (rootDir, relativePath) => rootDir + this._options.pathSeparator + relativePath
resolvePath: createPathResolve(this._options.pathSeparator)
});
void this._lastRunReceiver.dispatch({ method: "onConfigure", params: { config } });
},
onBegin: (suite) => {
if (!this.rootSuite)
@@ -131,6 +132,25 @@ class TeleSuiteUpdater {
};
}
}
function createPathResolve(pathSeparator) {
return (rootDir, relativePath) => {
const segments = [];
for (const segment of [...rootDir.split(pathSeparator), ...relativePath.split(pathSeparator)]) {
const isAfterDrive = pathSeparator === "\\" && segments.length === 1 && segments[0].endsWith(":");
const isFirst = !segments.length;
if (!segment && !isFirst && !isAfterDrive)
continue;
if (segment === ".")
continue;
if (segment === "..") {
segments.pop();
continue;
}
segments.push(segment);
}
return segments.join(pathSeparator);
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
TeleSuiteUpdater

View File

@@ -19,7 +19,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
var testTree_exports = {};
__export(testTree_exports, {
TestTree: () => TestTree,
collectTestIds: () => collectTestIds,
sortAndPropagateStatus: () => sortAndPropagateStatus,
statusEx: () => statusEx
});
@@ -242,16 +241,6 @@ class TestTree {
shortRoot.location = this.rootItem.location;
this.rootItem = shortRoot;
}
testIds() {
const result = /* @__PURE__ */ new Set();
const visit = (treeItem) => {
if (treeItem.kind === "case")
treeItem.tests.forEach((t) => result.add(t.id));
treeItem.children.forEach(visit);
};
visit(this.rootItem);
return result;
}
fileNames() {
const result = /* @__PURE__ */ new Set();
const visit = (treeItem) => {
@@ -276,7 +265,7 @@ class TestTree {
return this._treeItemById.get(id);
}
collectTestIds(treeItem) {
return treeItem ? collectTestIds(treeItem) : /* @__PURE__ */ new Set();
return collectTestIds(treeItem);
}
}
function sortAndPropagateStatus(treeItem) {
@@ -313,22 +302,28 @@ function sortAndPropagateStatus(treeItem) {
}
function collectTestIds(treeItem) {
const testIds = /* @__PURE__ */ new Set();
const locations = /* @__PURE__ */ new Set();
const visit = (treeItem2) => {
if (treeItem2.kind !== "test" && treeItem2.kind !== "case") {
treeItem2.children.forEach(visit);
return;
}
let fileItem = treeItem2;
while (fileItem && fileItem.parent && !(fileItem.kind === "group" && fileItem.subKind === "file"))
fileItem = fileItem.parent;
locations.add(fileItem.location.file);
if (treeItem2.kind === "case")
treeItem2.tests.map((t) => t.id).forEach((id) => testIds.add(id));
else if (treeItem2.kind === "test")
testIds.add(treeItem2.id);
treeItem2.tests.forEach((test) => testIds.add(test.id));
else
treeItem2.children?.forEach(visit);
testIds.add(treeItem2.id);
};
visit(treeItem);
return testIds;
return { testIds, locations };
}
const statusEx = Symbol("statusEx");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
TestTree,
collectTestIds,
sortAndPropagateStatus,
statusEx
});

View File

@@ -19,9 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
var expect_exports = {};
__export(expect_exports, {
expect: () => expect,
mergeExpects: () => mergeExpects,
printReceivedStringContainExpectedResult: () => printReceivedStringContainExpectedResult,
printReceivedStringContainExpectedSubstring: () => printReceivedStringContainExpectedSubstring
mergeExpects: () => mergeExpects
});
module.exports = __toCommonJS(expect_exports);
var import_utils = require("playwright-core/lib/utils");
@@ -33,15 +31,6 @@ var import_expectBundle = require("../common/expectBundle");
var import_globals = require("../common/globals");
var import_util = require("../util");
var import_testInfo = require("../worker/testInfo");
const printSubstring = (val) => val.replace(/"|\\/g, "\\$&");
const printReceivedStringContainExpectedSubstring = (received, start, length) => (0, import_expectBundle.RECEIVED_COLOR)(
'"' + printSubstring(received.slice(0, start)) + (0, import_expectBundle.INVERTED_COLOR)(printSubstring(received.slice(start, start + length))) + printSubstring(received.slice(start + length)) + '"'
);
const printReceivedStringContainExpectedResult = (received, result) => result === null ? (0, import_expectBundle.printReceived)(received) : printReceivedStringContainExpectedSubstring(
received,
result.index,
result[0].length
);
function createMatchers(actual, info, prefix) {
return new Proxy((0, import_expectBundle.expect)(actual), new ExpectMetaInfoProxyHandler(actual, info, prefix));
}
@@ -318,7 +307,5 @@ function mergeExpects(...expects) {
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
expect,
mergeExpects,
printReceivedStringContainExpectedResult,
printReceivedStringContainExpectedSubstring
mergeExpects
});

View File

@@ -19,43 +19,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
var matcherHint_exports = {};
__export(matcherHint_exports, {
ExpectError: () => ExpectError,
callLogText: () => callLogText,
formatMatcherMessage: () => formatMatcherMessage,
isJestError: () => isJestError
});
module.exports = __toCommonJS(matcherHint_exports);
var import_utils = require("playwright-core/lib/utils");
var import_expectBundle = require("../common/expectBundle");
function formatMatcherMessage(state, details) {
const receiver = details.receiver ?? (details.locator ? "locator" : "page");
let message = (0, import_expectBundle.DIM_COLOR)("expect(") + (0, import_expectBundle.RECEIVED_COLOR)(receiver) + (0, import_expectBundle.DIM_COLOR)(")" + (state.promise ? "." + state.promise : "") + (state.isNot ? ".not" : "") + ".") + details.matcherName + (0, import_expectBundle.DIM_COLOR)("(") + (0, import_expectBundle.EXPECTED_COLOR)(details.expectation) + (0, import_expectBundle.DIM_COLOR)(")") + " failed\n\n";
const diffLines = details.printedDiff?.split("\n");
if (diffLines?.length === 2) {
details.printedExpected = diffLines[0];
details.printedReceived = diffLines[1];
details.printedDiff = void 0;
}
const align = !details.errorMessage && details.printedExpected?.startsWith("Expected:") && (!details.printedReceived || details.printedReceived.startsWith("Received:"));
if (details.locator)
message += `Locator: ${align ? " " : ""}${String(details.locator)}
`;
if (details.printedExpected)
message += details.printedExpected + "\n";
if (details.printedReceived)
message += details.printedReceived + "\n";
if (details.timedOut && details.timeout)
message += `Timeout: ${align ? " " : ""}${details.timeout}ms
`;
if (details.printedDiff)
message += details.printedDiff + "\n";
if (details.errorMessage) {
message += details.errorMessage;
if (!details.errorMessage.endsWith("\n"))
message += "\n";
}
message += callLogText(details.log);
return message;
}
class ExpectError extends Error {
constructor(jestError, customMessage, stackFrames) {
super("");
@@ -68,20 +35,10 @@ class ExpectError extends Error {
}
}
function isJestError(e) {
return e instanceof Error && "matcherResult" in e;
return e instanceof Error && "matcherResult" in e && !!e.matcherResult;
}
const callLogText = (log) => {
if (!log || !log.some((l) => !!l))
return "";
return `
Call log:
${(0, import_expectBundle.DIM_COLOR)(log.join("\n"))}
`;
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ExpectError,
callLogText,
formatMatcherMessage,
isJestError
});

View File

@@ -61,7 +61,6 @@ var import_toMatchSnapshot = require("./toMatchSnapshot");
var import_config = require("../common/config");
var import_globals = require("../common/globals");
var import_testInfo = require("../worker/testInfo");
var import_matcherHint = require("./matcherHint");
function toBeAttached(locator, options) {
const attached = !options || options.attached === void 0 || options.attached;
const expected = attached ? "attached" : "detached";
@@ -297,7 +296,9 @@ async function toBeOK(response) {
response._fetchLog(),
isTextEncoding ? response.text() : null
]) : [];
const message = () => (0, import_matcherHint.formatMatcherMessage)(this, {
const message = () => (0, import_utils.formatMatcherMessage)(this.utils, {
isNot: this.isNot,
promise: this.promise,
matcherName,
receiver: "response",
expectation: "",

View File

@@ -21,8 +21,8 @@ __export(toBeTruthy_exports, {
toBeTruthy: () => toBeTruthy
});
module.exports = __toCommonJS(toBeTruthy_exports);
var import_utils = require("playwright-core/lib/utils");
var import_util = require("../util");
var import_matcherHint = require("./matcherHint");
async function toBeTruthy(matcherName, locator, receiverType, expected, arg, query, options = {}) {
(0, import_util.expectTypes)(locator, [receiverType], matcherName);
const timeout = options.timeout ?? this.timeout;
@@ -45,10 +45,12 @@ async function toBeTruthy(matcherName, locator, receiverType, expected, arg, que
printedReceived = errorMessage ? "" : `Received: ${received}`;
}
const message = () => {
return (0, import_matcherHint.formatMatcherMessage)(this, {
return (0, import_utils.formatMatcherMessage)(this.utils, {
isNot: this.isNot,
promise: this.promise,
matcherName,
expectation: arg,
locator,
locator: locator.toString(),
timeout,
timedOut,
printedExpected,

View File

@@ -23,7 +23,6 @@ __export(toEqual_exports, {
module.exports = __toCommonJS(toEqual_exports);
var import_utils = require("playwright-core/lib/utils");
var import_util = require("../util");
var import_matcherHint = require("./matcherHint");
const EXPECTED_LABEL = "Expected";
const RECEIVED_LABEL = "Received";
async function toEqual(matcherName, locator, receiverType, query, expected, options = {}) {
@@ -70,10 +69,12 @@ async function toEqual(matcherName, locator, receiverType, query, expected, opti
);
}
const message = () => {
return (0, import_matcherHint.formatMatcherMessage)(this, {
return (0, import_utils.formatMatcherMessage)(this.utils, {
isNot: this.isNot,
promise: this.promise,
matcherName,
expectation: "expected",
locator,
locator: locator.toString(),
timeout,
timedOut,
printedExpected,

View File

@@ -22,9 +22,6 @@ __export(toHaveURL_exports, {
});
module.exports = __toCommonJS(toHaveURL_exports);
var import_utils = require("playwright-core/lib/utils");
var import_expect = require("./expect");
var import_matcherHint = require("./matcherHint");
var import_expectBundle = require("../common/expectBundle");
async function toHaveURLWithPredicate(page, expected, options) {
const matcherName = "toHaveURL";
const timeout = options?.timeout ?? this.timeout;
@@ -75,18 +72,20 @@ function toHaveURLMessage(state, matcherName, expected, received, pass, timedOut
let printedDiff;
if (typeof expected === "function") {
printedExpected = `Expected: predicate to ${!state.isNot ? "succeed" : "fail"}`;
printedReceived = `Received: ${(0, import_expectBundle.printReceived)(receivedString)}`;
printedReceived = `Received: ${state.utils.printReceived(receivedString)}`;
} else {
if (pass) {
printedExpected = `Expected pattern: not ${state.utils.printExpected(expected)}`;
const formattedReceived = (0, import_expect.printReceivedStringContainExpectedResult)(receivedString, null);
const formattedReceived = (0, import_utils.printReceivedStringContainExpectedResult)(state.utils, receivedString, null);
printedReceived = `Received string: ${formattedReceived}`;
} else {
const labelExpected = `Expected ${typeof expected === "string" ? "string" : "pattern"}`;
printedDiff = state.utils.printDiffOrStringify(expected, receivedString, labelExpected, "Received string", false);
}
}
return (0, import_matcherHint.formatMatcherMessage)(state, {
return (0, import_utils.formatMatcherMessage)(state.utils, {
isNot: state.isNot,
promise: state.promise,
matcherName,
expectation: "expected",
timeout,

View File

@@ -34,9 +34,7 @@ module.exports = __toCommonJS(toMatchAriaSnapshot_exports);
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_utils = require("playwright-core/lib/utils");
var import_matcherHint = require("./matcherHint");
var import_util = require("../util");
var import_expect = require("./expect");
var import_globals = require("../common/globals");
async function toMatchAriaSnapshot(locator, expectedParam, options = {}) {
const matcherName = "toMatchAriaSnapshot";
@@ -79,16 +77,18 @@ async function toMatchAriaSnapshot(locator, expectedParam, options = {}) {
if (errorMessage) {
printedExpected = `Expected: ${this.isNot ? "not " : ""}${this.utils.printExpected(expected)}`;
} else if (pass) {
const receivedString = (0, import_expect.printReceivedStringContainExpectedSubstring)(typedReceived.raw, typedReceived.raw.indexOf(expected), expected.length);
const receivedString = (0, import_utils.printReceivedStringContainExpectedSubstring)(this.utils, typedReceived.raw, typedReceived.raw.indexOf(expected), expected.length);
printedExpected = `Expected: not ${this.utils.printExpected(expected)}`;
printedReceived = `Received: ${receivedString}`;
} else {
printedDiff = this.utils.printDiffOrStringify(expected, typedReceived.raw, "Expected", "Received", false);
}
return (0, import_matcherHint.formatMatcherMessage)(this, {
return (0, import_utils.formatMatcherMessage)(this.utils, {
isNot: this.isNot,
promise: this.promise,
matcherName,
expectation: "expected",
locator,
locator: locator.toString(),
timeout,
timedOut,
printedExpected,

View File

@@ -39,7 +39,6 @@ var import_utils = require("playwright-core/lib/utils");
var import_utils2 = require("playwright-core/lib/utils");
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var import_util = require("../util");
var import_matcherHint = require("./matcherHint");
var import_globals = require("../common/globals");
const NonConfigProperties = [
"clip",
@@ -50,7 +49,7 @@ const NonConfigProperties = [
"timeout"
];
class SnapshotHelper {
constructor(testInfo, matcherName, locator, anonymousSnapshotExtension, configOptions, nameOrOptions, optOptions) {
constructor(state, testInfo, matcherName, locator, anonymousSnapshotExtension, configOptions, nameOrOptions, optOptions) {
let name;
if (Array.isArray(nameOrOptions) || typeof nameOrOptions === "string") {
name = nameOrOptions;
@@ -90,6 +89,7 @@ class SnapshotHelper {
this.mimeType = import_utilsBundle.mime.getType(import_path.default.basename(this.expectedPath)) ?? "application/octet-stream";
this.comparator = (0, import_utils.getComparator)(this.mimeType);
this.testInfo = testInfo;
this.state = state;
this.kind = this.mimeType.startsWith("image/") ? "Screenshot" : "Snapshot";
}
createMatcherResult(message, pass, log) {
@@ -162,7 +162,7 @@ class SnapshotHelper {
step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-diff"), contentType: this.mimeType, path: this.diffPath });
}
if (log?.length)
output.push((0, import_matcherHint.callLogText)(log));
output.push((0, import_utils.callLogText)(this.state.utils, log));
else
output.push("");
return this.createMatcherResult(output.join("\n"), false, log);
@@ -181,6 +181,7 @@ function toMatchSnapshot(received, nameOrOptions = {}, optOptions = {}) {
return { pass: !this.isNot, message: () => "", name: "toMatchSnapshot", expected: nameOrOptions };
const configOptions = testInfo._projectInternal.expect?.toMatchSnapshot || {};
const helper = new SnapshotHelper(
this,
testInfo,
"toMatchSnapshot",
void 0,
@@ -216,7 +217,7 @@ function toMatchSnapshot(received, nameOrOptions = {}, optOptions = {}) {
const result = helper.comparator(received, expected, helper.options);
if (!result)
return helper.handleMatching();
const header = (0, import_matcherHint.formatMatcherMessage)(this, { matcherName: "toMatchSnapshot", receiver: (0, import_utils.isString)(received) ? "string" : "Buffer", expectation: "expected" });
const header = (0, import_utils.formatMatcherMessage)(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: "toMatchSnapshot", receiver: (0, import_utils.isString)(received) ? "string" : "Buffer", expectation: "expected" });
return helper.handleDifferent(received, expected, void 0, result.diff, header, result.errorMessage, void 0, this._stepInfo);
}
function toHaveScreenshotStepTitle(nameOrOptions = {}, optOptions = {}) {
@@ -236,7 +237,7 @@ async function toHaveScreenshot(pageOrLocator, nameOrOptions = {}, optOptions =
(0, import_util.expectTypes)(pageOrLocator, ["Page", "Locator"], "toHaveScreenshot");
const [page, locator] = pageOrLocator.constructor.name === "Page" ? [pageOrLocator, void 0] : [pageOrLocator.page(), pageOrLocator];
const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
const helper = new SnapshotHelper(testInfo, "toHaveScreenshot", locator, void 0, configOptions, nameOrOptions, optOptions);
const helper = new SnapshotHelper(this, testInfo, "toHaveScreenshot", locator, void 0, configOptions, nameOrOptions, optOptions);
if (!helper.expectedPath.toLowerCase().endsWith(".png"))
throw new Error(`Screenshot name "${import_path.default.basename(helper.expectedPath)}" must have '.png' extension`);
(0, import_util.expectTypes)(pageOrLocator, ["Page", "Locator"], "toHaveScreenshot");
@@ -273,7 +274,7 @@ async function toHaveScreenshot(pageOrLocator, nameOrOptions = {}, optOptions =
if (!hasSnapshot) {
const { actual: actual2, previous: previous2, diff: diff2, errorMessage: errorMessage2, log: log2, timedOut: timedOut2 } = await page._expectScreenshot(expectScreenshotOptions);
if (errorMessage2) {
const header2 = (0, import_matcherHint.formatMatcherMessage)(this, { matcherName: "toHaveScreenshot", locator, expectation: "expected", timeout, timedOut: timedOut2 });
const header2 = (0, import_utils.formatMatcherMessage)(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: "toHaveScreenshot", locator: locator?.toString(), expectation: "expected", timeout, timedOut: timedOut2 });
return helper.handleDifferent(actual2, void 0, previous2, diff2, header2, errorMessage2, log2, this._stepInfo);
}
return helper.handleMissing(actual2, this._stepInfo);
@@ -297,11 +298,11 @@ async function toHaveScreenshot(pageOrLocator, nameOrOptions = {}, optOptions =
if (helper.updateSnapshots === "changed" || helper.updateSnapshots === "all") {
if (actual)
return writeFiles(actual);
let header2 = (0, import_matcherHint.formatMatcherMessage)(this, { matcherName: "toHaveScreenshot", locator, expectation: "expected", timeout, timedOut });
let header2 = (0, import_utils.formatMatcherMessage)(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: "toHaveScreenshot", locator: locator?.toString(), expectation: "expected", timeout, timedOut });
header2 += " Failed to re-generate expected.\n";
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header2, errorMessage, log, this._stepInfo);
}
const header = (0, import_matcherHint.formatMatcherMessage)(this, { matcherName: "toHaveScreenshot", locator, expectation: "expected", timeout, timedOut });
const header = (0, import_utils.formatMatcherMessage)(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: "toHaveScreenshot", locator: locator?.toString(), expectation: "expected", timeout, timedOut });
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo);
}
function writeFileSync(aPath, content) {

View File

@@ -21,17 +21,15 @@ __export(toMatchText_exports, {
toMatchText: () => toMatchText
});
module.exports = __toCommonJS(toMatchText_exports);
var import_utils = require("playwright-core/lib/utils");
var import_util = require("../util");
var import_expect = require("./expect");
var import_matcherHint = require("./matcherHint");
var import_expectBundle = require("../common/expectBundle");
async function toMatchText(matcherName, receiver, receiverType, query, expected, options = {}) {
(0, import_util.expectTypes)(receiver, [receiverType], matcherName);
const locator = receiverType === "Locator" ? receiver : void 0;
if (!(typeof expected === "string") && !(expected && typeof expected.test === "function")) {
const errorMessage2 = `Error: ${(0, import_expectBundle.EXPECTED_COLOR)("expected")} value must be a string or regular expression
const errorMessage2 = `Error: ${this.utils.EXPECTED_COLOR("expected")} value must be a string or regular expression
${this.utils.printWithType("Expected", expected, this.utils.printExpected)}`;
throw new Error((0, import_matcherHint.formatMatcherMessage)(this, { locator, matcherName, expectation: "expected", errorMessage: errorMessage2 }));
throw new Error((0, import_utils.formatMatcherMessage)(this.utils, { promise: this.promise, isNot: this.isNot, locator: locator?.toString(), matcherName, expectation: "expected", errorMessage: errorMessage2 }));
}
const timeout = options.timeout ?? this.timeout;
const { matches: pass, received, log, timedOut, errorMessage } = await query(!!this.isNot, timeout);
@@ -53,13 +51,13 @@ ${this.utils.printWithType("Expected", expected, this.utils.printExpected)}`;
if (typeof expected === "string") {
printedExpected = `Expected${expectedSuffix}: not ${this.utils.printExpected(expected)}`;
if (!errorMessage) {
const formattedReceived = (0, import_expect.printReceivedStringContainExpectedSubstring)(receivedString, receivedString.indexOf(expected), expected.length);
const formattedReceived = (0, import_utils.printReceivedStringContainExpectedSubstring)(this.utils, receivedString, receivedString.indexOf(expected), expected.length);
printedReceived = `Received${receivedSuffix}: ${formattedReceived}`;
}
} else {
printedExpected = `Expected${expectedSuffix}: not ${this.utils.printExpected(expected)}`;
if (!errorMessage) {
const formattedReceived = (0, import_expect.printReceivedStringContainExpectedResult)(receivedString, typeof expected.exec === "function" ? expected.exec(receivedString) : null);
const formattedReceived = (0, import_utils.printReceivedStringContainExpectedResult)(this.utils, receivedString, typeof expected.exec === "function" ? expected.exec(receivedString) : null);
printedReceived = `Received${receivedSuffix}: ${formattedReceived}`;
}
}
@@ -70,10 +68,12 @@ ${this.utils.printWithType("Expected", expected, this.utils.printExpected)}`;
printedDiff = this.utils.printDiffOrStringify(expected, receivedString, `Expected${expectedSuffix}`, `Received${receivedSuffix}`, false);
}
const message = () => {
return (0, import_matcherHint.formatMatcherMessage)(this, {
return (0, import_utils.formatMatcherMessage)(this.utils, {
promise: this.promise,
isNot: this.isNot,
matcherName,
expectation: "expected",
locator,
locator: locator?.toString(),
timeout,
timedOut,
printedExpected,

View File

@@ -1,16 +0,0 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var actions_d_exports = {};
module.exports = __toCommonJS(actions_d_exports);

View File

@@ -29,7 +29,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
var browserContextFactory_exports = {};
__export(browserContextFactory_exports, {
SharedContextFactory: () => SharedContextFactory,
contextFactory: () => contextFactory
contextFactory: () => contextFactory,
identityBrowserContextFactory: () => identityBrowserContextFactory
});
module.exports = __toCommonJS(browserContextFactory_exports);
var import_crypto = __toESM(require("crypto"));
@@ -53,16 +54,27 @@ function contextFactory(config) {
return new IsolatedContextFactory(config);
return new PersistentContextFactory(config);
}
function identityBrowserContextFactory(browserContext) {
return {
createContext: async (clientInfo, abortSignal, options) => {
return {
browserContext,
close: async () => {
}
};
}
};
}
class BaseContextFactory {
constructor(name, config) {
this._logName = name;
this.config = config;
}
async _obtainBrowser(clientInfo) {
async _obtainBrowser(clientInfo, options) {
if (this._browserPromise)
return this._browserPromise;
(0, import_log.testDebug)(`obtain browser (${this._logName})`);
this._browserPromise = this._doObtainBrowser(clientInfo);
this._browserPromise = this._doObtainBrowser(clientInfo, options);
void this._browserPromise.then((browser) => {
browser.on("disconnected", () => {
this._browserPromise = void 0;
@@ -72,28 +84,27 @@ class BaseContextFactory {
});
return this._browserPromise;
}
async _doObtainBrowser(clientInfo) {
async _doObtainBrowser(clientInfo, options) {
throw new Error("Not implemented");
}
async createContext(clientInfo) {
async createContext(clientInfo, _, options) {
(0, import_log.testDebug)(`create browser context (${this._logName})`);
const browser = await this._obtainBrowser(clientInfo);
const browserContext = await this._doCreateContext(browser);
const browser = await this._obtainBrowser(clientInfo, options);
const browserContext = await this._doCreateContext(browser, clientInfo);
await addInitScript(browserContext, this.config.browser.initScript);
return {
browserContext,
close: (afterClose) => this._closeBrowserContext(browserContext, browser, afterClose)
close: () => this._closeBrowserContext(browserContext, browser)
};
}
async _doCreateContext(browser) {
async _doCreateContext(browser, clientInfo) {
throw new Error("Not implemented");
}
async _closeBrowserContext(browserContext, browser, afterClose) {
async _closeBrowserContext(browserContext, browser) {
(0, import_log.testDebug)(`close browser context (${this._logName})`);
if (browser.contexts().length === 1)
this._browserPromise = void 0;
await browserContext.close().catch(import_log.logUnhandledError);
await afterClose();
if (browser.contexts().length === 0) {
(0, import_log.testDebug)(`close browser (${this._logName})`);
await browser.close().catch(import_log.logUnhandledError);
@@ -104,7 +115,7 @@ class IsolatedContextFactory extends BaseContextFactory {
constructor(config) {
super("isolated", config);
}
async _doObtainBrowser(clientInfo) {
async _doObtainBrowser(clientInfo, options) {
await injectCdpPort(this.config.browser);
const browserType = playwright[this.config.browser.browserName];
const tracesDir = await computeTracesDir(this.config, clientInfo);
@@ -114,15 +125,16 @@ class IsolatedContextFactory extends BaseContextFactory {
tracesDir,
...this.config.browser.launchOptions,
handleSIGINT: false,
handleSIGTERM: false
handleSIGTERM: false,
...options.forceHeadless !== void 0 ? { headless: options.forceHeadless === "headless" } : {}
}).catch((error) => {
if (error.message.includes("Executable doesn't exist"))
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
throw error;
});
}
async _doCreateContext(browser) {
return browser.newContext(this.config.browser.contextOptions);
async _doCreateContext(browser, clientInfo) {
return browser.newContext(await browserContextOptionsFromConfig(this.config, clientInfo));
}
}
class CdpContextFactory extends BaseContextFactory {
@@ -130,7 +142,10 @@ class CdpContextFactory extends BaseContextFactory {
super("cdp", config);
}
async _doObtainBrowser() {
return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint, { headers: this.config.browser.cdpHeaders });
return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint, {
headers: this.config.browser.cdpHeaders,
timeout: this.config.browser.cdpTimeout
});
}
async _doCreateContext(browser) {
return this.config.browser.isolated ? await browser.newContext() : browser.contexts()[0];
@@ -158,7 +173,7 @@ class PersistentContextFactory {
this._userDataDirs = /* @__PURE__ */ new Set();
this.config = config;
}
async createContext(clientInfo) {
async createContext(clientInfo, abortSignal, options) {
await injectCdpPort(this.config.browser);
(0, import_log.testDebug)("create browser context (persistent)");
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo);
@@ -172,23 +187,29 @@ class PersistentContextFactory {
const launchOptions = {
tracesDir,
...this.config.browser.launchOptions,
...this.config.browser.contextOptions,
...await browserContextOptionsFromConfig(this.config, clientInfo),
handleSIGINT: false,
handleSIGTERM: false,
ignoreDefaultArgs: [
"--disable-extensions"
],
assistantMode: true
assistantMode: true,
...options.forceHeadless !== void 0 ? { headless: options.forceHeadless === "headless" } : {}
};
try {
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
await addInitScript(browserContext, this.config.browser.initScript);
const close = (afterClose) => this._closeBrowserContext(browserContext, userDataDir, afterClose);
const close = () => this._closeBrowserContext(browserContext, userDataDir);
return { browserContext, close };
} catch (error) {
if (error.message.includes("Executable doesn't exist"))
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
if (error.message.includes("ProcessSingleton") || error.message.includes("Invalid URL")) {
if (error.message.includes("cannot open shared object file: No such file or directory")) {
const browserName = launchOptions.channel ?? this.config.browser.browserName;
throw new Error(`Missing system dependencies required to run browser ${browserName}. Install them with: sudo npx playwright install-deps ${browserName}`);
}
if (error.message.includes("ProcessSingleton") || // On Windows the process exits silently with code 21 when the profile is in use.
error.message.includes("exitCode=21")) {
await new Promise((resolve) => setTimeout(resolve, 1e3));
continue;
}
@@ -197,13 +218,14 @@ class PersistentContextFactory {
}
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
}
async _closeBrowserContext(browserContext, userDataDir, afterClose) {
async _closeBrowserContext(browserContext, userDataDir) {
(0, import_log.testDebug)("close browser context (persistent)");
(0, import_log.testDebug)("release user data dir", userDataDir);
await browserContext.close().catch(() => {
});
await afterClose();
this._userDataDirs.delete(userDataDir);
if (process.env.PWMCP_PROFILES_DIR_FOR_TEST && userDataDir.startsWith(process.env.PWMCP_PROFILES_DIR_FOR_TEST))
await import_fs.default.promises.rm(userDataDir, { recursive: true }).catch(import_log.logUnhandledError);
(0, import_log.testDebug)("close browser context complete (persistent)");
}
async _createUserDataDir(clientInfo) {
@@ -257,10 +279,10 @@ class SharedContextFactory {
constructor(baseFactory) {
this._baseFactory = baseFactory;
}
async createContext(clientInfo, abortSignal, toolName) {
async createContext(clientInfo, abortSignal, options) {
if (!this._contextPromise) {
(0, import_log.testDebug)("create shared browser context");
this._contextPromise = this._baseFactory.createContext(clientInfo, abortSignal, toolName);
this._contextPromise = this._baseFactory.createContext(clientInfo, abortSignal, options);
}
const { browserContext } = await this._contextPromise;
(0, import_log.testDebug)(`shared context client connected`);
@@ -280,17 +302,28 @@ class SharedContextFactory {
if (!contextPromise)
return;
const { close } = await contextPromise;
await close(async () => {
});
await close();
}
}
async function computeTracesDir(config, clientInfo) {
if (!config.saveTrace && !config.capabilities?.includes("tracing"))
return;
return await (0, import_config.outputFile)(config, clientInfo, `traces`, { origin: "code", reason: "Collecting trace" });
return await (0, import_config.outputFile)(config, clientInfo, `traces`, { origin: "code", title: "Collecting trace" });
}
async function browserContextOptionsFromConfig(config, clientInfo) {
const result = { ...config.browser.contextOptions };
if (config.saveVideo) {
const dir = await (0, import_config.outputFile)(config, clientInfo, `videos`, { origin: "code", title: "Saving video" });
result.recordVideo = {
dir,
size: config.saveVideo
};
}
return result;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SharedContextFactory,
contextFactory
contextFactory,
identityBrowserContextFactory
});

View File

@@ -47,24 +47,32 @@ class BrowserServerBackend {
}
async callTool(name, rawArguments) {
const tool = this._tools.find((tool2) => tool2.schema.name === name);
if (!tool)
throw new Error(`Tool "${name}" not found`);
if (!tool) {
return {
content: [{ type: "text", text: `### Error
Tool "${name}" not found` }],
isError: true
};
}
const parsedArguments = tool.schema.inputSchema.parse(rawArguments || {});
const context = this._context;
const response = new import_response.Response(context, name, parsedArguments);
response.logBegin();
const response = import_response.Response.create(context, name, parsedArguments);
context.setRunningTool(name);
let responseObject;
try {
await tool.handle(context, parsedArguments, response);
await response.finish();
this._sessionLog?.logResponse(response);
responseObject = await response.build();
this._sessionLog?.logResponse(name, parsedArguments, responseObject);
} catch (error) {
response.addError(String(error));
return {
content: [{ type: "text", text: `### Error
${String(error)}` }],
isError: true
};
} finally {
context.setRunningTool(void 0);
}
response.logEnd();
return response.serialize();
return responseObject;
}
serverClosed() {
void this._context?.dispose().catch(import_log.logUnhandledError);

View File

@@ -1,66 +0,0 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var codegen_exports = {};
__export(codegen_exports, {
escapeWithQuotes: () => escapeWithQuotes,
formatObject: () => formatObject,
quote: () => quote
});
module.exports = __toCommonJS(codegen_exports);
function escapeWithQuotes(text, char = "'") {
const stringified = JSON.stringify(text);
const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"');
if (char === "'")
return char + escapedText.replace(/[']/g, "\\'") + char;
if (char === '"')
return char + escapedText.replace(/["]/g, '\\"') + char;
if (char === "`")
return char + escapedText.replace(/[`]/g, "\\`") + char;
throw new Error("Invalid escape char");
}
function quote(text) {
return escapeWithQuotes(text, "'");
}
function formatObject(value, indent = " ", mode = "multiline") {
if (typeof value === "string")
return quote(value);
if (Array.isArray(value))
return `[${value.map((o) => formatObject(o)).join(", ")}]`;
if (typeof value === "object") {
const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
if (!keys.length)
return "{}";
const tokens = [];
for (const key of keys)
tokens.push(`${key}: ${formatObject(value[key])}`);
if (mode === "multiline")
return `{
${tokens.join(`,
${indent}`)}
}`;
return `{ ${tokens.join(", ")} }`;
}
return String(value);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
escapeWithQuotes,
formatObject,
quote
});

View File

@@ -32,13 +32,15 @@ __export(config_exports, {
configFromCLIOptions: () => configFromCLIOptions,
defaultConfig: () => defaultConfig,
dotenvFileLoader: () => dotenvFileLoader,
enumParser: () => enumParser,
headerParser: () => headerParser,
numberParser: () => numberParser,
outputDir: () => outputDir,
outputFile: () => outputFile,
resolutionParser: () => resolutionParser,
resolveCLIConfig: () => resolveCLIConfig,
resolveConfig: () => resolveConfig
resolveConfig: () => resolveConfig,
semicolonSeparatedList: () => semicolonSeparatedList
});
module.exports = __toCommonJS(config_exports);
var import_fs = __toESM(require("fs"));
@@ -60,8 +62,19 @@ const defaultConfig = {
viewport: null
}
},
console: {
level: "info"
},
network: {
allowedOrigins: void 0,
blockedOrigins: void 0
},
server: {},
saveTrace: false,
snapshot: {
mode: "incremental",
output: "stdout"
},
timeouts: {
action: 5e3,
navigation: 6e4
@@ -88,6 +101,12 @@ async function validateConfig(config) {
throw new Error(`Init script file does not exist: ${script}`);
}
}
if (config.browser.initPage) {
for (const page of config.browser.initPage) {
if (!await (0, import_util.fileExistsAsync)(page))
throw new Error(`Init page file does not exist: ${page}`);
}
}
if (config.sharedBrowserContext && config.saveVideo)
throw new Error("saveVideo is not supported when sharedBrowserContext is true");
}
@@ -143,13 +162,6 @@ function configFromCLIOptions(cliOptions) {
contextOptions.serviceWorkers = "block";
if (cliOptions.grantPermissions)
contextOptions.permissions = cliOptions.grantPermissions;
if (cliOptions.saveVideo) {
contextOptions.recordVideo = {
// Videos are moved to output directory on saveAs.
dir: tmpDir(),
size: cliOptions.saveVideo
};
}
const result = {
browser: {
browserName,
@@ -168,11 +180,22 @@ function configFromCLIOptions(cliOptions) {
allowedHosts: cliOptions.allowedHosts
},
capabilities: cliOptions.caps,
console: {
level: cliOptions.consoleLevel
},
network: {
allowedOrigins: cliOptions.allowedOrigins,
blockedOrigins: cliOptions.blockedOrigins
},
allowUnrestrictedFileAccess: cliOptions.allowUnrestrictedFileAccess,
codegen: cliOptions.codegen,
saveSession: cliOptions.saveSession,
saveTrace: cliOptions.saveTrace,
saveVideo: cliOptions.saveVideo,
secrets: cliOptions.secrets,
sharedBrowserContext: cliOptions.sharedBrowserContext,
snapshot: cliOptions.snapshotMode ? { mode: cliOptions.snapshotMode } : void 0,
outputMode: cliOptions.outputMode,
outputDir: cliOptions.outputDir,
imageResponses: cliOptions.imageResponses,
testIdAttribute: cliOptions.testIdAttribute,
@@ -186,12 +209,17 @@ function configFromCLIOptions(cliOptions) {
function configFromEnv() {
const options = {};
options.allowedHosts = commaSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_HOSTNAMES);
options.allowedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_ALLOWED_ORIGINS);
options.allowUnrestrictedFileAccess = envToBoolean(process.env.PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS);
options.blockedOrigins = semicolonSeparatedList(process.env.PLAYWRIGHT_MCP_BLOCKED_ORIGINS);
options.blockServiceWorkers = envToBoolean(process.env.PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS);
options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS);
options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT);
options.cdpHeader = headerParser(process.env.PLAYWRIGHT_MCP_CDP_HEADERS, {});
options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG);
if (process.env.PLAYWRIGHT_MCP_CONSOLE_LEVEL)
options.consoleLevel = enumParser("--console-level", ["error", "warning", "info", "debug"], process.env.PLAYWRIGHT_MCP_CONSOLE_LEVEL);
options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE);
options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
options.grantPermissions = commaSeparatedList(process.env.PLAYWRIGHT_MCP_GRANT_PERMISSIONS);
@@ -205,8 +233,8 @@ function configFromEnv() {
if (initScript)
options.initScript = [initScript];
options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === "omit")
options.imageResponses = "omit";
if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES)
options.imageResponses = enumParser("--image-responses", ["allow", "omit"], process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES);
options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
options.port = numberParser(process.env.PLAYWRIGHT_MCP_PORT);
@@ -242,7 +270,8 @@ function outputDir(config, clientInfo) {
}
async function outputFile(config, clientInfo, fileName, options) {
const file = await resolveFile(config, clientInfo, fileName, options);
(0, import_utilsBundle.debug)("pw:mcp:file")(options.reason, file);
await import_fs.default.promises.mkdir(import_path.default.dirname(file), { recursive: true });
(0, import_utilsBundle.debug)("pw:mcp:file")(options.title, file);
return file;
}
async function resolveFile(config, clientInfo, fileName, options) {
@@ -285,16 +314,33 @@ function mergeConfig(base, overrides) {
...pickDefined(base),
...pickDefined(overrides),
browser,
console: {
...pickDefined(base.console),
...pickDefined(overrides.console)
},
network: {
...pickDefined(base.network),
...pickDefined(overrides.network)
},
server: {
...pickDefined(base.server),
...pickDefined(overrides.server)
},
snapshot: {
...pickDefined(base.snapshot),
...pickDefined(overrides.snapshot)
},
timeouts: {
...pickDefined(base.timeouts),
...pickDefined(overrides.timeouts)
}
};
}
function semicolonSeparatedList(value) {
if (!value)
return void 0;
return value.split(";").map((v) => v.trim());
}
function commaSeparatedList(value) {
if (!value)
return void 0;
@@ -335,6 +381,11 @@ function headerParser(arg, previous) {
result[name] = value;
return result;
}
function enumParser(name, options, value) {
if (!options.includes(value))
throw new Error(`Invalid ${name}: ${value}. Valid values are: ${options.join(", ")}`);
return value;
}
function envToBoolean(value) {
if (value === "true" || value === "1")
return true;
@@ -358,11 +409,13 @@ function sanitizeForFilePath(s) {
configFromCLIOptions,
defaultConfig,
dotenvFileLoader,
enumParser,
headerParser,
numberParser,
outputDir,
outputFile,
resolutionParser,
resolveCLIConfig,
resolveConfig
resolveConfig,
semicolonSeparatedList
});

View File

@@ -28,19 +28,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var context_exports = {};
__export(context_exports, {
Context: () => Context,
InputRecorder: () => InputRecorder
Context: () => Context
});
module.exports = __toCommonJS(context_exports);
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var import_utils = require("playwright-core/lib/utils");
var import_playwright_core = require("playwright-core");
var import_url = require("url");
var import_os = __toESM(require("os"));
var import_log = require("../log");
var import_tab = require("./tab");
var import_config = require("./config");
var codegen = __toESM(require("./codegen"));
var import_utils = require("./tools/utils");
const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
class Context {
constructor(options) {
@@ -68,14 +66,13 @@ class Context {
}
currentTabOrDie() {
if (!this._currentTab)
throw new Error('No open pages available. Use the "browser_navigate" tool to navigate to a page first.');
throw new Error("No open pages available.");
return this._currentTab;
}
async newTab() {
const { browserContext } = await this._ensureBrowserContext();
const { browserContext } = await this._ensureBrowserContext({});
const page = await browserContext.newPage();
this._currentTab = this._tabs.find((t) => t.page === page);
await this._currentTab.initializedPromise;
return this._currentTab;
}
async selectTab(index) {
@@ -86,8 +83,8 @@ class Context {
this._currentTab = tab;
return tab;
}
async ensureTab() {
const { browserContext } = await this._ensureBrowserContext();
async ensureTab(options = {}) {
const { browserContext } = await this._ensureBrowserContext(options);
if (!this._currentTab)
await browserContext.newPage();
return this._currentTab;
@@ -137,31 +134,11 @@ class Context {
testDebug("close context");
const promise = this._browserContextPromise;
this._browserContextPromise = void 0;
this._browserContextOption = void 0;
await promise.then(async ({ browserContext, close }) => {
if (this.config.saveTrace)
await browserContext.tracing.stop();
const videos = this.config.saveVideo ? browserContext.pages().map((page) => page.video()).filter((video) => !!video) : [];
await close(async () => {
for (const video of videos) {
const name = await this.outputFile((0, import_utils.dateAsFileName)("webm"), { origin: "code", reason: "Saving video" });
await import_fs.default.promises.mkdir(import_path.default.dirname(name), { recursive: true });
const p = await video.path();
if (import_fs.default.existsSync(p)) {
try {
await import_fs.default.promises.rename(p, name);
} catch (e) {
if (e.code !== "EXDEV")
(0, import_log.logUnhandledError)(e);
try {
await import_fs.default.promises.copyFile(p, name);
await import_fs.default.promises.unlink(p);
} catch (e2) {
(0, import_log.logUnhandledError)(e2);
}
}
}
}
});
await close();
});
}
async dispose() {
@@ -169,28 +146,45 @@ class Context {
await this.closeBrowserContext();
Context._allContexts.delete(this);
}
async ensureBrowserContext() {
const { browserContext } = await this._ensureBrowserContext();
async _setupRequestInterception(context) {
if (this.config.network?.allowedOrigins?.length) {
await context.route("**", (route) => route.abort("blockedbyclient"));
for (const origin of this.config.network.allowedOrigins)
await context.route(originOrHostGlob(origin), (route) => route.continue());
}
if (this.config.network?.blockedOrigins?.length) {
for (const origin of this.config.network.blockedOrigins)
await context.route(originOrHostGlob(origin), (route) => route.abort("blockedbyclient"));
}
}
async ensureBrowserContext(options = {}) {
const { browserContext } = await this._ensureBrowserContext(options);
return browserContext;
}
_ensureBrowserContext() {
if (!this._browserContextPromise) {
this._browserContextPromise = this._setupBrowserContext();
this._browserContextPromise.catch(() => {
this._browserContextPromise = void 0;
});
}
_ensureBrowserContext(options) {
if (this._browserContextPromise && (options.forceHeadless === void 0 || this._browserContextOption?.forceHeadless === options.forceHeadless))
return this._browserContextPromise;
const closePrework = this._browserContextPromise ? this.closeBrowserContext() : Promise.resolve();
this._browserContextPromise = closePrework.then(() => this._setupBrowserContext(options));
this._browserContextPromise.catch(() => {
this._browserContextPromise = void 0;
this._browserContextOption = void 0;
});
this._browserContextOption = options;
return this._browserContextPromise;
}
async _setupBrowserContext() {
async _setupBrowserContext(options) {
if (this._closeBrowserContextPromise)
throw new Error("Another browser context is being closed.");
if (this.config.testIdAttribute)
import_playwright_core.selectors.setTestIdAttribute(this.config.testIdAttribute);
const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, this._runningToolName);
const result = await this._browserContextFactory.createContext(this._clientInfo, this._abortController.signal, { toolName: this._runningToolName, ...options });
const { browserContext } = result;
if (this.sessionLog)
await InputRecorder.create(this, browserContext);
if (!this.config.allowUnrestrictedFileAccess) {
browserContext._setAllowedProtocols(["http:", "https:", "about:", "data:"]);
browserContext._setAllowedDirectories(allRootPaths(this._clientInfo));
}
await this._setupRequestInterception(browserContext);
for (const page of browserContext.pages())
this._onPageCreated(page);
browserContext.on("page", (page) => this._onPageCreated(page));
@@ -206,62 +200,45 @@ class Context {
}
lookupSecret(secretName) {
if (!this.config.secrets?.[secretName])
return { value: secretName, code: codegen.quote(secretName) };
return { value: secretName, code: (0, import_utils.escapeWithQuotes)(secretName, "'") };
return {
value: this.config.secrets[secretName],
code: `process.env['${secretName}']`
};
}
firstRootPath() {
return allRootPaths(this._clientInfo)[0];
}
}
class InputRecorder {
constructor(context, browserContext) {
this._context = context;
this._browserContext = browserContext;
function allRootPaths(clientInfo) {
const paths = [];
for (const root of clientInfo.roots) {
const url = new URL(root.uri);
let rootPath;
try {
rootPath = (0, import_url.fileURLToPath)(url);
} catch (e) {
if (e.code === "ERR_INVALID_FILE_URL_PATH" && import_os.default.platform() === "win32")
rootPath = decodeURIComponent(url.pathname);
}
if (!rootPath)
continue;
paths.push(rootPath);
}
static async create(context, browserContext) {
const recorder = new InputRecorder(context, browserContext);
await recorder._initialize();
return recorder;
}
async _initialize() {
const sessionLog = this._context.sessionLog;
await this._browserContext._enableRecorder({
mode: "recording",
recorderMode: "api"
}, {
actionAdded: (page, data, code) => {
if (this._context.isRunningTool())
return;
const tab = import_tab.Tab.forPage(page);
if (tab)
sessionLog.logUserAction(data.action, tab, code, false);
},
actionUpdated: (page, data, code) => {
if (this._context.isRunningTool())
return;
const tab = import_tab.Tab.forPage(page);
if (tab)
sessionLog.logUserAction(data.action, tab, code, true);
},
signalAdded: (page, data) => {
if (this._context.isRunningTool())
return;
if (data.signal.name !== "navigation")
return;
const tab = import_tab.Tab.forPage(page);
const navigateAction = {
name: "navigate",
url: data.signal.url,
signals: []
};
if (tab)
sessionLog.logUserAction(navigateAction, tab, `await page.goto('${data.signal.url}');`, false);
}
});
if (paths.length === 0)
paths.push(process.cwd());
return paths;
}
function originOrHostGlob(originOrHost) {
try {
const url = new URL(originOrHost);
if (url.origin !== "null")
return `${url.origin}/**`;
} catch {
}
return `*://${originOrHost}/**`;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Context,
InputRecorder
Context
});

View File

@@ -1,7 +1,9 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
@@ -15,179 +17,213 @@ var __copyProps = (to, from, except, desc) => {
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var response_exports = {};
__export(response_exports, {
Response: () => Response,
parseResponse: () => parseResponse,
renderTabMarkdown: () => renderTabMarkdown,
renderTabsMarkdown: () => renderTabsMarkdown,
requestDebug: () => requestDebug
});
module.exports = __toCommonJS(response_exports);
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var import_tab = require("./tab");
var import_utils = require("./tools/utils");
const requestDebug = (0, import_utilsBundle.debug)("pw:mcp:request");
class Response {
constructor(context, toolName, toolArgs) {
this._result = [];
constructor(ordinal, context, toolName, toolArgs) {
this._results = [];
this._errors = [];
this._code = [];
this._images = [];
this._includeSnapshot = "none";
this._includeTabs = false;
this._ordinal = ordinal;
this._context = context;
this.toolName = toolName;
this.toolArgs = toolArgs;
}
addResult(result) {
this._result.push(result);
static {
this._ordinal = 0;
}
static create(context, toolName, toolArgs) {
return new Response(++Response._ordinal, context, toolName, toolArgs);
}
addTextResult(result) {
this._results.push({ text: result });
}
async addResult(result) {
if (result.data && !result.suggestedFilename)
result.suggestedFilename = (0, import_utils.dateAsFileName)(result.ext ?? "bin");
if (this._context.config.outputMode === "file") {
if (!result.suggestedFilename)
result.suggestedFilename = (0, import_utils.dateAsFileName)(result.ext ?? (result.text ? "txt" : "bin"));
}
const entry = { text: result.text, data: result.data, title: result.title };
if (result.suggestedFilename)
entry.filename = await this._context.outputFile(result.suggestedFilename, { origin: "llm", title: result.title ?? "Saved result" });
this._results.push(entry);
return { fileName: entry.filename };
}
addError(error) {
this._result.push(error);
this._isError = true;
}
isError() {
return this._isError;
}
result() {
return this._result.join("\n");
this._errors.push(error);
}
addCode(code) {
this._code.push(code);
}
code() {
return this._code.join("\n");
}
addImage(image) {
this._images.push(image);
}
images() {
return this._images;
setIncludeSnapshot() {
this._includeSnapshot = this._context.config.snapshot.mode;
}
setIncludeSnapshot(full) {
this._includeSnapshot = full ?? "incremental";
setIncludeFullSnapshot(includeSnapshotFileName) {
this._includeSnapshot = "full";
this._includeSnapshotFileName = includeSnapshotFileName;
}
setIncludeTabs() {
this._includeTabs = true;
}
async finish() {
if (this._includeSnapshot !== "none" && this._context.currentTab())
this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
for (const tab of this._context.tabs())
await tab.updateTitle();
}
tabSnapshot() {
return this._tabSnapshot;
}
logBegin() {
if (requestDebug.enabled)
requestDebug(this.toolName, this.toolArgs);
}
logEnd() {
if (requestDebug.enabled)
requestDebug(this.serialize({ omitSnapshot: true, omitBlobs: true }));
}
serialize(options = {}) {
const response = [];
if (this._result.length) {
response.push("### Result");
response.push(this._result.join("\n"));
response.push("");
async build() {
const rootPath = this._context.firstRootPath();
const sections = [];
const addSection = (title) => {
const section = { title, content: [] };
sections.push(section);
return section.content;
};
if (this._errors.length) {
const text = addSection("Error");
text.push("### Error");
text.push(this._errors.join("\n"));
}
if (this._code.length) {
response.push(`### Ran Playwright code
\`\`\`js
${this._code.join("\n")}
\`\`\``);
response.push("");
if (this._results.length) {
const text = addSection("Result");
for (const result of this._results) {
if (result.filename) {
text.push(`- [${result.title}](${rootPath ? import_path.default.relative(rootPath, result.filename) : result.filename})`);
if (result.data)
await import_fs.default.promises.writeFile(result.filename, result.data);
else if (result.text)
await import_fs.default.promises.writeFile(result.filename, this._redactText(result.text));
} else if (result.text) {
text.push(result.text);
}
}
}
if (this._includeSnapshot !== "none" || this._includeTabs)
response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs));
if (this._tabSnapshot?.modalStates.length) {
response.push(...(0, import_tab.renderModalStates)(this._context, this._tabSnapshot.modalStates));
response.push("");
} else if (this._tabSnapshot) {
const includeSnapshot = options.omitSnapshot ? "none" : this._includeSnapshot;
response.push(renderTabSnapshot(this._tabSnapshot, includeSnapshot));
response.push("");
if (this._context.config.codegen !== "none" && this._code.length) {
const text = addSection("Ran Playwright code");
text.push(...this._code);
}
const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot() : void 0;
const tabHeaders = await Promise.all(this._context.tabs().map((tab) => tab.headerSnapshot()));
if (tabHeaders.some((header) => header.changed)) {
if (tabHeaders.length !== 1) {
const text2 = addSection("Open tabs");
text2.push(...renderTabsMarkdown(tabHeaders));
}
const text = addSection("Page");
text.push(...renderTabMarkdown(tabHeaders[0]));
}
if (tabSnapshot?.modalStates.length) {
const text = addSection("Modal state");
text.push(...(0, import_tab.renderModalStates)(tabSnapshot.modalStates));
}
if (tabSnapshot && this._includeSnapshot === "full") {
let fileName;
if (this._includeSnapshotFileName)
fileName = await this._context.outputFile(this._includeSnapshotFileName, { origin: "llm", title: "Saved snapshot" });
else if (this._context.config.outputMode === "file")
fileName = await this._context.outputFile(`snapshot-${this._ordinal}.yml`, { origin: "code", title: "Saved snapshot" });
if (fileName) {
await import_fs.default.promises.writeFile(fileName, tabSnapshot.ariaSnapshot);
const text = addSection("Snapshot");
text.push(`- File: ${rootPath ? import_path.default.relative(rootPath, fileName) : fileName}`);
} else {
const text = addSection("Snapshot");
text.push("```yaml");
text.push(tabSnapshot.ariaSnapshot);
text.push("```");
}
}
if (tabSnapshot && this._includeSnapshot === "incremental") {
const text = addSection("Snapshot");
text.push("```yaml");
if (tabSnapshot.ariaSnapshotDiff !== void 0)
text.push(tabSnapshot.ariaSnapshotDiff);
else
text.push(tabSnapshot.ariaSnapshot);
text.push("```");
}
if (tabSnapshot?.events.filter((event) => event.type !== "request").length) {
const text = addSection("Events");
for (const event of tabSnapshot.events) {
if (event.type === "console") {
if ((0, import_tab.shouldIncludeMessage)(this._context.config.console.level, event.message.type))
text.push(`- ${trimMiddle(event.message.toString(), 100)}`);
} else if (event.type === "download-start") {
text.push(`- Downloading file ${event.download.download.suggestedFilename()} ...`);
} else if (event.type === "download-finish") {
text.push(`- Downloaded file ${event.download.download.suggestedFilename()} to "${rootPath ? import_path.default.relative(rootPath, event.download.outputFile) : event.download.outputFile}"`);
}
}
}
const allText = sections.flatMap((section) => {
const content2 = [];
content2.push(`### ${section.title}`);
content2.push(...section.content);
content2.push("");
return content2;
}).join("\n");
const content = [
{ type: "text", text: response.join("\n") }
{
type: "text",
text: this._redactText(allText)
}
];
if (this._context.config.imageResponses !== "omit") {
for (const image of this._images)
content.push({ type: "image", data: options.omitBlobs ? "<blob>" : image.data.toString("base64"), mimeType: image.contentType });
content.push({ type: "image", data: image.data.toString("base64"), mimeType: image.contentType });
}
this._redactSecrets(content);
return { content, isError: this._isError };
return {
content,
...this._errors.length > 0 ? { isError: true } : {}
};
}
_redactSecrets(content) {
if (!this._context.config.secrets)
return;
for (const item of content) {
if (item.type !== "text")
continue;
for (const [secretName, secretValue] of Object.entries(this._context.config.secrets))
item.text = item.text.replaceAll(secretValue, `<secret>${secretName}</secret>`);
}
_redactText(text) {
for (const [secretName, secretValue] of Object.entries(this._context.config.secrets ?? {}))
text = text.replaceAll(secretValue, `<secret>${secretName}</secret>`);
return text;
}
}
function renderTabSnapshot(tabSnapshot, includeSnapshot) {
const lines = [];
if (tabSnapshot.consoleMessages.length) {
lines.push(`### New console messages`);
for (const message of tabSnapshot.consoleMessages)
lines.push(`- ${trim(message.toString(), 100)}`);
lines.push("");
}
if (tabSnapshot.downloads.length) {
lines.push(`### Downloads`);
for (const entry of tabSnapshot.downloads) {
if (entry.finished)
lines.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
else
lines.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
}
lines.push("");
}
if (includeSnapshot === "incremental" && tabSnapshot.ariaSnapshotDiff === "") {
return lines.join("\n");
}
lines.push(`### Page state`);
lines.push(`- Page URL: ${tabSnapshot.url}`);
lines.push(`- Page Title: ${tabSnapshot.title}`);
if (includeSnapshot !== "none") {
lines.push(`- Page Snapshot:`);
lines.push("```yaml");
if (includeSnapshot === "incremental" && tabSnapshot.ariaSnapshotDiff !== void 0)
lines.push(tabSnapshot.ariaSnapshotDiff);
else
lines.push(tabSnapshot.ariaSnapshot);
lines.push("```");
}
return lines.join("\n");
}
function renderTabsMarkdown(tabs, force = false) {
if (tabs.length === 1 && !force)
return [];
if (!tabs.length) {
return [
"### Open tabs",
'No open tabs. Use the "browser_navigate" tool to navigate to a page first.',
""
];
}
const lines = ["### Open tabs"];
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
const current = tab.isCurrentTab() ? " (current)" : "";
lines.push(`- ${i}:${current} [${tab.lastTitle()}] (${tab.page.url()})`);
}
lines.push("");
function renderTabMarkdown(tab) {
const lines = [`- Page URL: ${tab.url}`];
if (tab.title)
lines.push(`- Page Title: ${tab.title}`);
return lines;
}
function trim(text, maxLength) {
function renderTabsMarkdown(tabs) {
if (!tabs.length)
return ['No open tabs. Use the "browser_navigate" tool to navigate to a page first.'];
const lines = [];
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
const current = tab.current ? " (current)" : "";
lines.push(`- ${i}:${current} [${tab.title}](${tab.url})`);
}
return lines;
}
function trimMiddle(text, maxLength) {
if (text.length <= maxLength)
return text;
return text.slice(0, maxLength) + "...";
return text.slice(0, Math.floor(maxLength / 2)) + "..." + text.slice(-3 - Math.floor(maxLength / 2));
}
function parseSections(text) {
const sections = /* @__PURE__ */ new Map();
@@ -207,31 +243,36 @@ function parseResponse(response) {
return void 0;
const text = response.content[0].text;
const sections = parseSections(text);
const error = sections.get("Error");
const result = sections.get("Result");
const code = sections.get("Ran Playwright code");
const tabs = sections.get("Open tabs");
const pageState = sections.get("Page state");
const consoleMessages = sections.get("New console messages");
const page = sections.get("Page");
const snapshot = sections.get("Snapshot");
const events = sections.get("Events");
const modalState = sections.get("Modal state");
const downloads = sections.get("Downloads");
const codeNoFrame = code?.replace(/^```js\n/, "").replace(/\n```$/, "");
const isError = response.isError;
const attachments = response.content.slice(1);
const attachments = response.content.length > 1 ? response.content.slice(1) : void 0;
return {
result,
error,
code: codeNoFrame,
tabs,
pageState,
consoleMessages,
page,
snapshot,
events,
modalState,
downloads,
isError,
attachments
attachments,
text
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Response,
parseResponse,
renderTabMarkdown,
renderTabsMarkdown,
requestDebug
});

View File

@@ -33,124 +33,39 @@ __export(sessionLog_exports, {
module.exports = __toCommonJS(sessionLog_exports);
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_log = require("../log");
var import_config = require("./config");
var import_response = require("./response");
class SessionLog {
constructor(sessionFolder) {
this._ordinal = 0;
this._pendingEntries = [];
this._sessionFileQueue = Promise.resolve();
this._folder = sessionFolder;
this._file = import_path.default.join(this._folder, "session.md");
}
static async create(config, clientInfo) {
const sessionFolder = await (0, import_config.outputFile)(config, clientInfo, `session-${Date.now()}`, { origin: "code", reason: "Saving session" });
const sessionFolder = await (0, import_config.outputFile)(config, clientInfo, `session-${Date.now()}`, { origin: "code", title: "Saving session" });
await import_fs.default.promises.mkdir(sessionFolder, { recursive: true });
console.error(`Session: ${sessionFolder}`);
return new SessionLog(sessionFolder);
}
logResponse(response) {
const entry = {
timestamp: performance.now(),
toolCall: {
toolName: response.toolName,
toolArgs: response.toolArgs,
result: response.result(),
isError: response.isError()
},
code: response.code(),
tabSnapshot: response.tabSnapshot()
};
this._appendEntry(entry);
}
logUserAction(action, tab, code, isUpdate) {
code = code.trim();
if (isUpdate) {
const lastEntry = this._pendingEntries[this._pendingEntries.length - 1];
if (lastEntry?.userAction?.name === action.name) {
lastEntry.userAction = action;
lastEntry.code = code;
return;
}
}
if (action.name === "navigate") {
const lastEntry = this._pendingEntries[this._pendingEntries.length - 1];
if (lastEntry?.tabSnapshot?.url === action.url)
return;
}
const entry = {
timestamp: performance.now(),
userAction: action,
code,
tabSnapshot: {
url: tab.page.url(),
title: "",
ariaSnapshot: action.ariaSnapshot || "",
modalStates: [],
consoleMessages: [],
downloads: []
}
};
this._appendEntry(entry);
}
_appendEntry(entry) {
this._pendingEntries.push(entry);
if (this._flushEntriesTimeout)
clearTimeout(this._flushEntriesTimeout);
this._flushEntriesTimeout = setTimeout(() => this._flushEntries(), 1e3);
}
async _flushEntries() {
clearTimeout(this._flushEntriesTimeout);
const entries = this._pendingEntries;
this._pendingEntries = [];
logResponse(toolName, toolArgs, responseObject) {
const parsed = (0, import_response.parseResponse)(responseObject);
if (parsed)
delete parsed.text;
const lines = [""];
for (const entry of entries) {
const ordinal = (++this._ordinal).toString().padStart(3, "0");
if (entry.toolCall) {
lines.push(
`### Tool call: ${entry.toolCall.toolName}`,
`- Args`,
"```json",
JSON.stringify(entry.toolCall.toolArgs, null, 2),
"```"
);
if (entry.toolCall.result) {
lines.push(
entry.toolCall.isError ? `- Error` : `- Result`,
"```",
entry.toolCall.result,
"```"
);
}
}
if (entry.userAction) {
const actionData = { ...entry.userAction };
delete actionData.ariaSnapshot;
delete actionData.selector;
delete actionData.signals;
lines.push(
`### User action: ${entry.userAction.name}`,
`- Args`,
"```json",
JSON.stringify(actionData, null, 2),
"```"
);
}
if (entry.code) {
lines.push(
`- Code`,
"```js",
entry.code,
"```"
);
}
if (entry.tabSnapshot) {
const fileName = `${ordinal}.snapshot.yml`;
import_fs.default.promises.writeFile(import_path.default.join(this._folder, fileName), entry.tabSnapshot.ariaSnapshot).catch(import_log.logUnhandledError);
lines.push(`- Snapshot: ${fileName}`);
}
lines.push("", "");
lines.push(
`### Tool call: ${toolName}`,
`- Args`,
"```json",
JSON.stringify(toolArgs, null, 2),
"```"
);
if (parsed) {
lines.push(`- Result`);
lines.push("```json");
lines.push(JSON.stringify(parsed, null, 2));
lines.push("```");
}
lines.push("");
this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
}
}

View File

@@ -20,7 +20,8 @@ var tab_exports = {};
__export(tab_exports, {
Tab: () => Tab,
TabEvents: () => TabEvents,
renderModalStates: () => renderModalStates
renderModalStates: () => renderModalStates,
shouldIncludeMessage: () => shouldIncludeMessage
});
module.exports = __toCommonJS(tab_exports);
var import_events = require("events");
@@ -36,19 +37,20 @@ const TabEvents = {
class Tab extends import_events.EventEmitter {
constructor(context, page, onPageClose) {
super();
this._lastTitle = "about:blank";
this._lastHeader = { title: "about:blank", url: "about:blank", current: false };
this._consoleMessages = [];
this._recentConsoleMessages = [];
this._downloads = [];
this._requests = /* @__PURE__ */ new Set();
this._modalStates = [];
this._downloads = [];
this._needsFullSnapshot = false;
this._eventEntries = [];
this._recentEventEntries = [];
this.context = context;
this.page = page;
this._onPageClose = onPageClose;
page.on("console", (event) => this._handleConsoleMessage(messageToConsoleMessage(event)));
page.on("pageerror", (error) => this._handleConsoleMessage(pageErrorToConsoleMessage(error)));
page.on("request", (request) => this._requests.add(request));
page.on("request", (request) => this._handleRequest(request));
page.on("close", () => this._onClose());
page.on("filechooser", (chooser) => {
this.setModalState({
@@ -65,7 +67,7 @@ class Tab extends import_events.EventEmitter {
page.setDefaultNavigationTimeout(this.context.config.timeouts.navigation);
page.setDefaultTimeout(this.context.config.timeouts.action);
page[tabSymbol] = this;
this.initializedPromise = this._initialize();
this._initializedPromise = this._initialize();
}
static forPage(page) {
return page[tabSymbol];
@@ -105,9 +107,6 @@ class Tab extends import_events.EventEmitter {
clearModalState(modalState) {
this._modalStates = this._modalStates.filter((state) => state !== modalState);
}
modalStatesMarkdown() {
return renderModalStates(this.context, this.modalStates());
}
_dialogShown(dialog) {
this.setModalState({
type: "dialog",
@@ -120,53 +119,68 @@ class Tab extends import_events.EventEmitter {
const entry = {
download,
finished: false,
outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: "web", reason: "Saving download" })
outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: "web", title: "Saving download" })
};
this._downloads.push(entry);
this._addLogEntry({ type: "download-start", wallTime: Date.now(), download: entry });
await download.saveAs(entry.outputFile);
entry.finished = true;
this._addLogEntry({ type: "download-finish", wallTime: Date.now(), download: entry });
}
_clearCollectedArtifacts() {
this._consoleMessages.length = 0;
this._recentConsoleMessages.length = 0;
this._downloads.length = 0;
this._requests.clear();
this._eventEntries.length = 0;
this._recentEventEntries.length = 0;
}
_handleRequest(request) {
this._requests.add(request);
this._addLogEntry({ type: "request", wallTime: Date.now(), request });
}
_handleConsoleMessage(message) {
this._consoleMessages.push(message);
this._recentConsoleMessages.push(message);
this._addLogEntry({ type: "console", wallTime: Date.now(), message });
}
_addLogEntry(entry) {
this._eventEntries.push(entry);
this._recentEventEntries.push(entry);
}
_onClose() {
this._clearCollectedArtifacts();
this._onPageClose(this);
}
async updateTitle() {
async headerSnapshot() {
let title;
await this._raceAgainstModalStates(async () => {
this._lastTitle = await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.title());
title = await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.title());
});
}
lastTitle() {
return this._lastTitle;
if (this._lastHeader.title !== title || this._lastHeader.url !== this.page.url() || this._lastHeader.current !== this.isCurrentTab()) {
this._lastHeader = { title: title ?? "", url: this.page.url(), current: this.isCurrentTab() };
return { ...this._lastHeader, changed: true };
}
return { ...this._lastHeader, changed: false };
}
isCurrentTab() {
return this === this.context.currentTab();
}
async waitForLoadState(state, options) {
await this._initializedPromise;
await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.waitForLoadState(state, options).catch(import_log.logUnhandledError));
}
async navigate(url) {
await this._initializedPromise;
this._clearCollectedArtifacts();
const downloadEvent = (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.waitForEvent("download").catch(import_log.logUnhandledError));
const { promise: downloadEvent, abort: abortDownloadEvent } = (0, import_utils2.eventWaiter)(this.page, "download", 3e3);
try {
await this.page.goto(url, { waitUntil: "domcontentloaded" });
abortDownloadEvent();
} catch (_e) {
const e = _e;
const mightBeDownload = e.message.includes("net::ERR_ABORTED") || e.message.includes("Download is starting");
if (!mightBeDownload)
throw e;
const download = await Promise.race([
downloadEvent,
new Promise((resolve) => setTimeout(resolve, 3e3))
]);
const download = await downloadEvent;
if (!download)
throw e;
await new Promise((resolve) => setTimeout(resolve, 500));
@@ -174,40 +188,36 @@ class Tab extends import_events.EventEmitter {
}
await this.waitForLoadState("load", { timeout: 5e3 });
}
async consoleMessages(type) {
await this.initializedPromise;
return this._consoleMessages.filter((message) => type ? message.type === type : true);
async consoleMessages(level) {
await this._initializedPromise;
return this._consoleMessages.filter((message) => shouldIncludeMessage(level, message.type));
}
async requests() {
await this.initializedPromise;
await this._initializedPromise;
return this._requests;
}
async captureSnapshot() {
await this._initializedPromise;
let tabSnapshot;
const modalStates = await this._raceAgainstModalStates(async () => {
const snapshot = await this.page._snapshotForAI({ track: "response" });
tabSnapshot = {
url: this.page.url(),
title: await this.page.title(),
ariaSnapshot: snapshot.full,
ariaSnapshotDiff: this._needsFullSnapshot ? void 0 : snapshot.incremental,
modalStates: [],
consoleMessages: [],
downloads: this._downloads
events: []
};
});
if (tabSnapshot) {
tabSnapshot.consoleMessages = this._recentConsoleMessages;
this._recentConsoleMessages = [];
tabSnapshot.events = this._recentEventEntries;
this._recentEventEntries = [];
}
this._needsFullSnapshot = !tabSnapshot;
return tabSnapshot ?? {
url: this.page.url(),
title: "",
ariaSnapshot: "",
ariaSnapshotDiff: "",
modalStates,
consoleMessages: [],
downloads: []
events: []
};
}
_javaScriptBlocked() {
@@ -228,15 +238,20 @@ class Tab extends import_events.EventEmitter {
]);
}
async waitForCompletion(callback) {
await this._initializedPromise;
await this._raceAgainstModalStates(() => (0, import_utils2.waitForCompletion)(this, callback));
}
async refLocator(params) {
await this._initializedPromise;
return (await this.refLocators([params]))[0];
}
async refLocators(params) {
await this._initializedPromise;
return Promise.all(params.map(async (param) => {
try {
const locator = this.page.locator(`aria-ref=${param.ref}`).describe(param.element);
let locator = this.page.locator(`aria-ref=${param.ref}`);
if (param.element)
locator = locator.describe(param.element);
const { resolvedSelector } = await locator._resolveSelector();
return { locator, resolved: (0, import_utils.asLocator)("javascript", resolvedSelector) };
} catch (e) {
@@ -250,7 +265,8 @@ class Tab extends import_events.EventEmitter {
return;
}
await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => {
return page.evaluate(() => new Promise((f) => setTimeout(f, 1e3)));
return page.evaluate(() => new Promise((f) => setTimeout(f, 1e3))).catch(() => {
});
});
}
}
@@ -275,18 +291,53 @@ function pageErrorToConsoleMessage(errorOrValue) {
toString: () => String(errorOrValue)
};
}
function renderModalStates(context, modalStates) {
const result = ["### Modal state"];
function renderModalStates(modalStates) {
const result = [];
if (modalStates.length === 0)
result.push("- There is no modal state present");
for (const state of modalStates)
result.push(`- [${state.description}]: can be handled by the "${state.clearedBy}" tool`);
return result;
}
const consoleMessageLevels = ["error", "warning", "info", "debug"];
function shouldIncludeMessage(thresholdLevel, type) {
const messageLevel = consoleLevelForMessageType(type);
return consoleMessageLevels.indexOf(messageLevel) <= consoleMessageLevels.indexOf(thresholdLevel);
}
function consoleLevelForMessageType(type) {
switch (type) {
case "assert":
case "error":
return "error";
case "warning":
return "warning";
case "count":
case "dir":
case "dirxml":
case "info":
case "log":
case "table":
case "time":
case "timeEnd":
return "info";
case "clear":
case "debug":
case "endGroup":
case "profile":
case "profileEnd":
case "startGroup":
case "startGroupCollapsed":
case "trace":
return "debug";
default:
return "info";
}
}
const tabSymbol = Symbol("tabSymbol");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Tab,
TabEvents,
renderModalStates
renderModalStates,
shouldIncludeMessage
});

View File

@@ -43,6 +43,7 @@ var import_keyboard = __toESM(require("./tools/keyboard"));
var import_mouse = __toESM(require("./tools/mouse"));
var import_navigate = __toESM(require("./tools/navigate"));
var import_network = __toESM(require("./tools/network"));
var import_open = __toESM(require("./tools/open"));
var import_pdf = __toESM(require("./tools/pdf"));
var import_runCode = __toESM(require("./tools/runCode"));
var import_snapshot = __toESM(require("./tools/snapshot"));
@@ -60,9 +61,10 @@ const browserTools = [
...import_form.default,
...import_install.default,
...import_keyboard.default,
...import_mouse.default,
...import_navigate.default,
...import_network.default,
...import_mouse.default,
...import_open.default,
...import_pdf.default,
...import_runCode.default,
...import_screenshot.default,

View File

@@ -21,20 +21,22 @@ __export(common_exports, {
default: () => common_default
});
module.exports = __toCommonJS(common_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
var import_response = require("../response");
const close = (0, import_tool.defineTool)({
capability: "core",
schema: {
name: "browser_close",
title: "Close browser",
description: "Close the page",
inputSchema: import_bundle.z.object({}),
inputSchema: import_mcpBundle.z.object({}),
type: "action"
},
handle: async (context, params, response) => {
await context.closeBrowserContext();
response.setIncludeTabs();
const result = (0, import_response.renderTabsMarkdown)([]);
response.addTextResult(result.join("\n"));
response.addCode(`await page.close()`);
}
});
@@ -44,9 +46,9 @@ const resize = (0, import_tool.defineTabTool)({
name: "browser_resize",
title: "Resize browser window",
description: "Resize the browser window",
inputSchema: import_bundle.z.object({
width: import_bundle.z.number().describe("Width of the browser window"),
height: import_bundle.z.number().describe("Height of the browser window")
inputSchema: import_mcpBundle.z.object({
width: import_mcpBundle.z.number().describe("Width of the browser window"),
height: import_mcpBundle.z.number().describe("Height of the browser window")
}),
type: "action"
},

View File

@@ -21,7 +21,7 @@ __export(console_exports, {
default: () => console_default
});
module.exports = __toCommonJS(console_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
const console = (0, import_tool.defineTabTool)({
capability: "core",
@@ -29,14 +29,16 @@ const console = (0, import_tool.defineTabTool)({
name: "browser_console_messages",
title: "Get console messages",
description: "Returns all console messages",
inputSchema: import_bundle.z.object({
onlyErrors: import_bundle.z.boolean().optional().describe("Only return error messages")
inputSchema: import_mcpBundle.z.object({
level: import_mcpBundle.z.enum(["error", "warning", "info", "debug"]).default("info").describe('Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info".'),
filename: import_mcpBundle.z.string().optional().describe("Filename to save the console messages to. If not provided, messages are returned as text.")
}),
type: "readOnly"
},
handle: async (tab, params, response) => {
const messages = await tab.consoleMessages(params.onlyErrors ? "error" : void 0);
messages.map((message) => response.addResult(message.toString()));
const messages = await tab.consoleMessages(params.level);
const text = messages.map((message) => message.toString()).join("\n");
await response.addResult({ text, suggestedFilename: params.filename });
}
});
var console_default = [

View File

@@ -22,7 +22,7 @@ __export(dialogs_exports, {
handleDialog: () => handleDialog
});
module.exports = __toCommonJS(dialogs_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
const handleDialog = (0, import_tool.defineTabTool)({
capability: "core",
@@ -30,9 +30,9 @@ const handleDialog = (0, import_tool.defineTabTool)({
name: "browser_handle_dialog",
title: "Handle a dialog",
description: "Handle a dialog",
inputSchema: import_bundle.z.object({
accept: import_bundle.z.boolean().describe("Whether to accept the dialog."),
promptText: import_bundle.z.string().optional().describe("The text of the prompt in case of a prompt dialog.")
inputSchema: import_mcpBundle.z.object({
accept: import_mcpBundle.z.boolean().describe("Whether to accept the dialog."),
promptText: import_mcpBundle.z.string().optional().describe("The text of the prompt in case of a prompt dialog.")
}),
type: "action"
},

View File

@@ -1,9 +1,7 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
@@ -17,27 +15,20 @@ var __copyProps = (to, from, except, desc) => {
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var evaluate_exports = {};
__export(evaluate_exports, {
default: () => evaluate_default
});
module.exports = __toCommonJS(evaluate_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_utils = require("playwright-core/lib/utils");
var import_tool = require("./tool");
var javascript = __toESM(require("../codegen"));
const evaluateSchema = import_bundle.z.object({
function: import_bundle.z.string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
element: import_bundle.z.string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
ref: import_bundle.z.string().optional().describe("Exact target element reference from the page snapshot")
const evaluateSchema = import_mcpBundle.z.object({
function: import_mcpBundle.z.string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
element: import_mcpBundle.z.string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
ref: import_mcpBundle.z.string().optional().describe("Exact target element reference from the page snapshot"),
filename: import_mcpBundle.z.string().optional().describe("Filename to save the result to. If not provided, result is returned as JSON string.")
});
const evaluate = (0, import_tool.defineTabTool)({
capability: "core",
@@ -53,14 +44,15 @@ const evaluate = (0, import_tool.defineTabTool)({
let locator;
if (params.ref && params.element) {
locator = await tab.refLocator({ ref: params.ref, element: params.element });
response.addCode(`await page.${locator.resolved}.evaluate(${javascript.quote(params.function)});`);
response.addCode(`await page.${locator.resolved}.evaluate(${(0, import_utils.escapeWithQuotes)(params.function)});`);
} else {
response.addCode(`await page.evaluate(${javascript.quote(params.function)});`);
response.addCode(`await page.evaluate(${(0, import_utils.escapeWithQuotes)(params.function)});`);
}
await tab.waitForCompletion(async () => {
const receiver = locator?.locator ?? tab.page;
const result = await receiver._evaluateFunction(params.function);
response.addResult(JSON.stringify(result, null, 2) || "undefined");
const text = JSON.stringify(result, null, 2) || "undefined";
await response.addResult({ text, suggestedFilename: params.filename });
});
}
});

View File

@@ -22,7 +22,7 @@ __export(files_exports, {
uploadFile: () => uploadFile
});
module.exports = __toCommonJS(files_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
const uploadFile = (0, import_tool.defineTabTool)({
capability: "core",
@@ -30,8 +30,8 @@ const uploadFile = (0, import_tool.defineTabTool)({
name: "browser_file_upload",
title: "Upload files",
description: "Upload one or multiple files",
inputSchema: import_bundle.z.object({
paths: import_bundle.z.array(import_bundle.z.string()).optional().describe("The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.")
inputSchema: import_mcpBundle.z.object({
paths: import_mcpBundle.z.array(import_mcpBundle.z.string()).optional().describe("The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.")
}),
type: "action"
},

View File

@@ -1,9 +1,7 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
@@ -17,35 +15,27 @@ var __copyProps = (to, from, except, desc) => {
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var form_exports = {};
__export(form_exports, {
default: () => form_default
});
module.exports = __toCommonJS(form_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_utils = require("playwright-core/lib/utils");
var import_tool = require("./tool");
var codegen = __toESM(require("../codegen"));
const fillForm = (0, import_tool.defineTabTool)({
capability: "core",
schema: {
name: "browser_fill_form",
title: "Fill form",
description: "Fill multiple form fields",
inputSchema: import_bundle.z.object({
fields: import_bundle.z.array(import_bundle.z.object({
name: import_bundle.z.string().describe("Human-readable field name"),
type: import_bundle.z.enum(["textbox", "checkbox", "radio", "combobox", "slider"]).describe("Type of the field"),
ref: import_bundle.z.string().describe("Exact target field reference from the page snapshot"),
value: import_bundle.z.string().describe("Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.")
inputSchema: import_mcpBundle.z.object({
fields: import_mcpBundle.z.array(import_mcpBundle.z.object({
name: import_mcpBundle.z.string().describe("Human-readable field name"),
type: import_mcpBundle.z.enum(["textbox", "checkbox", "radio", "combobox", "slider"]).describe("Type of the field"),
ref: import_mcpBundle.z.string().describe("Exact target field reference from the page snapshot"),
value: import_mcpBundle.z.string().describe("Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.")
})).describe("Fields to fill in")
}),
type: "input"
@@ -63,7 +53,7 @@ const fillForm = (0, import_tool.defineTabTool)({
response.addCode(`${locatorSource}.setChecked(${field.value});`);
} else if (field.type === "combobox") {
await locator.selectOption({ label: field.value });
response.addCode(`${locatorSource}.selectOption(${codegen.quote(field.value)});`);
response.addCode(`${locatorSource}.selectOption(${(0, import_utils.escapeWithQuotes)(field.value)});`);
}
}
}

View File

@@ -33,15 +33,16 @@ __export(install_exports, {
module.exports = __toCommonJS(install_exports);
var import_child_process = require("child_process");
var import_path = __toESM(require("path"));
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
var import_response = require("../response");
const install = (0, import_tool.defineTool)({
capability: "core-install",
schema: {
name: "browser_install",
title: "Install the browser specified in the config",
description: "Install the browser specified in the config. Call this if you get an error about the browser not being installed.",
inputSchema: import_bundle.z.object({}),
inputSchema: import_mcpBundle.z.object({}),
type: "action"
},
handle: async (context, params, response) => {
@@ -61,7 +62,9 @@ const install = (0, import_tool.defineTool)({
reject(new Error(`Failed to install browser: ${output.join("")}`));
});
});
response.setIncludeTabs();
const tabHeaders = await Promise.all(context.tabs().map((tab) => tab.headerSnapshot()));
const result = (0, import_response.renderTabsMarkdown)(tabHeaders);
response.addTextResult(result.join("\n"));
}
});
var install_default = [

View File

@@ -21,7 +21,7 @@ __export(keyboard_exports, {
default: () => keyboard_default
});
module.exports = __toCommonJS(keyboard_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
var import_snapshot = require("./snapshot");
const pressKey = (0, import_tool.defineTabTool)({
@@ -30,24 +30,46 @@ const pressKey = (0, import_tool.defineTabTool)({
name: "browser_press_key",
title: "Press a key",
description: "Press a key on the keyboard",
inputSchema: import_bundle.z.object({
key: import_bundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
inputSchema: import_mcpBundle.z.object({
key: import_mcpBundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
}),
type: "input"
},
handle: async (tab, params, response) => {
response.setIncludeSnapshot();
response.addCode(`// Press ${params.key}`);
response.addCode(`await page.keyboard.press('${params.key}');`);
await tab.waitForCompletion(async () => {
await tab.page.keyboard.press(params.key);
});
await tab.page.keyboard.press(params.key);
}
});
const pressSequentially = (0, import_tool.defineTabTool)({
capability: "internal",
schema: {
name: "browser_press_sequentially",
title: "Press sequentially",
description: "Press text sequentially on the keyboard",
inputSchema: import_mcpBundle.z.object({
text: import_mcpBundle.z.string().describe("Text to press sequentially"),
submit: import_mcpBundle.z.boolean().optional().describe("Whether to submit entered text (press Enter after)")
}),
type: "input"
},
handle: async (tab, params, response) => {
response.addCode(`// Press ${params.text}`);
response.addCode(`await page.keyboard.type('${params.text}');`);
await tab.page.keyboard.type(params.text);
if (params.submit) {
response.addCode(`await page.keyboard.press('Enter');`);
response.setIncludeSnapshot();
await tab.waitForCompletion(async () => {
await tab.page.keyboard.press("Enter");
});
}
}
});
const typeSchema = import_snapshot.elementSchema.extend({
text: import_bundle.z.string().describe("Text to type into the element"),
submit: import_bundle.z.boolean().optional().describe("Whether to submit entered text (press Enter after)"),
slowly: import_bundle.z.boolean().optional().describe("Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.")
text: import_mcpBundle.z.string().describe("Text to type into the element"),
submit: import_mcpBundle.z.boolean().optional().describe("Whether to submit entered text (press Enter after)"),
slowly: import_mcpBundle.z.boolean().optional().describe("Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.")
});
const type = (0, import_tool.defineTabTool)({
capability: "core",
@@ -80,5 +102,6 @@ const type = (0, import_tool.defineTabTool)({
});
var keyboard_default = [
pressKey,
type
type,
pressSequentially
];

View File

@@ -21,10 +21,10 @@ __export(mouse_exports, {
default: () => mouse_default
});
module.exports = __toCommonJS(mouse_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
const elementSchema = import_bundle.z.object({
element: import_bundle.z.string().describe("Human-readable element description used to obtain permission to interact with the element")
const elementSchema = import_mcpBundle.z.object({
element: import_mcpBundle.z.string().describe("Human-readable element description used to obtain permission to interact with the element")
});
const mouseMove = (0, import_tool.defineTabTool)({
capability: "vision",
@@ -33,8 +33,8 @@ const mouseMove = (0, import_tool.defineTabTool)({
title: "Move mouse",
description: "Move mouse to a given position",
inputSchema: elementSchema.extend({
x: import_bundle.z.number().describe("X coordinate"),
y: import_bundle.z.number().describe("Y coordinate")
x: import_mcpBundle.z.number().describe("X coordinate"),
y: import_mcpBundle.z.number().describe("Y coordinate")
}),
type: "input"
},
@@ -53,8 +53,8 @@ const mouseClick = (0, import_tool.defineTabTool)({
title: "Click",
description: "Click left mouse button at a given position",
inputSchema: elementSchema.extend({
x: import_bundle.z.number().describe("X coordinate"),
y: import_bundle.z.number().describe("Y coordinate")
x: import_mcpBundle.z.number().describe("X coordinate"),
y: import_mcpBundle.z.number().describe("Y coordinate")
}),
type: "input"
},
@@ -78,10 +78,10 @@ const mouseDrag = (0, import_tool.defineTabTool)({
title: "Drag mouse",
description: "Drag left mouse button to a given position",
inputSchema: elementSchema.extend({
startX: import_bundle.z.number().describe("Start X coordinate"),
startY: import_bundle.z.number().describe("Start Y coordinate"),
endX: import_bundle.z.number().describe("End X coordinate"),
endY: import_bundle.z.number().describe("End Y coordinate")
startX: import_mcpBundle.z.number().describe("Start X coordinate"),
startY: import_mcpBundle.z.number().describe("Start Y coordinate"),
endX: import_mcpBundle.z.number().describe("End X coordinate"),
endY: import_mcpBundle.z.number().describe("End Y coordinate")
}),
type: "input"
},

View File

@@ -21,7 +21,7 @@ __export(navigate_exports, {
default: () => navigate_default
});
module.exports = __toCommonJS(navigate_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
const navigate = (0, import_tool.defineTool)({
capability: "core",
@@ -29,14 +29,23 @@ const navigate = (0, import_tool.defineTool)({
name: "browser_navigate",
title: "Navigate to a URL",
description: "Navigate to a URL",
inputSchema: import_bundle.z.object({
url: import_bundle.z.string().describe("The URL to navigate to")
inputSchema: import_mcpBundle.z.object({
url: import_mcpBundle.z.string().describe("The URL to navigate to")
}),
type: "action"
},
handle: async (context, params, response) => {
const tab = await context.ensureTab();
await tab.navigate(params.url);
let url = params.url;
try {
new URL(url);
} catch (e) {
if (url.startsWith("localhost"))
url = "http://" + url;
else
url = "https://" + url;
}
await tab.navigate(url);
response.setIncludeSnapshot();
response.addCode(`await page.goto('${params.url}');`);
}
@@ -47,7 +56,7 @@ const goBack = (0, import_tool.defineTabTool)({
name: "browser_navigate_back",
title: "Go back",
description: "Go back to the previous page",
inputSchema: import_bundle.z.object({}),
inputSchema: import_mcpBundle.z.object({}),
type: "action"
},
handle: async (tab, params, response) => {

View File

@@ -21,7 +21,7 @@ __export(network_exports, {
default: () => network_default
});
module.exports = __toCommonJS(network_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
const requests = (0, import_tool.defineTabTool)({
capability: "core",
@@ -29,24 +29,33 @@ const requests = (0, import_tool.defineTabTool)({
name: "browser_network_requests",
title: "List network requests",
description: "Returns all network requests since loading the page",
inputSchema: import_bundle.z.object({}),
inputSchema: import_mcpBundle.z.object({
includeStatic: import_mcpBundle.z.boolean().default(false).describe("Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false."),
filename: import_mcpBundle.z.string().optional().describe("Filename to save the network requests to. If not provided, requests are returned as text.")
}),
type: "readOnly"
},
handle: async (tab, params, response) => {
const requests2 = await tab.requests();
for (const request of requests2)
response.addResult(await renderRequest(request));
const text = [];
for (const request of requests2) {
const rendered = await renderRequest(request, params.includeStatic);
if (rendered)
text.push(rendered);
}
await response.addResult({ text: text.join("\n"), suggestedFilename: params.filename });
}
});
async function renderRequest(request) {
async function renderRequest(request, includeStatic) {
const response = request._hasResponse ? await request.response() : void 0;
const isStaticRequest = ["document", "stylesheet", "image", "media", "font", "script", "manifest"].includes(request.resourceType());
const isSuccessfulRequest = !response || response.status() < 400;
if (isStaticRequest && isSuccessfulRequest && !includeStatic)
return void 0;
const result = [];
result.push(`[${request.method().toUpperCase()}] ${request.url()}`);
const hasResponse = request._hasResponse;
if (hasResponse) {
const response = await request.response();
if (response)
result.push(`=> [${response.status()}] ${response.statusText()}`);
}
if (response)
result.push(`=> [${response.status()}] ${response.statusText()}`);
return result.join(" ");
}
var network_default = [

57
node_modules/playwright/lib/mcp/browser/tools/open.js generated vendored Normal file
View File

@@ -0,0 +1,57 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var open_exports = {};
__export(open_exports, {
default: () => open_default
});
module.exports = __toCommonJS(open_exports);
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
const open = (0, import_tool.defineTool)({
capability: "internal",
schema: {
name: "browser_open",
title: "Open URL",
description: "Open a URL in the browser",
inputSchema: import_mcpBundle.z.object({
url: import_mcpBundle.z.string().describe("The URL to open"),
headed: import_mcpBundle.z.boolean().optional().describe("Run browser in headed mode")
}),
type: "action"
},
handle: async (context, params, response) => {
const forceHeadless = params.headed ? "headed" : "headless";
const tab = await context.ensureTab({ forceHeadless });
let url = params.url;
try {
new URL(url);
} catch (e) {
if (url.startsWith("localhost"))
url = "http://" + url;
else
url = "https://" + url;
}
await tab.navigate(url);
response.setIncludeSnapshot();
response.addCode(`await page.goto('${params.url}');`);
}
});
var open_default = [
open
];

View File

@@ -1,9 +1,7 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
@@ -17,26 +15,18 @@ var __copyProps = (to, from, except, desc) => {
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var pdf_exports = {};
__export(pdf_exports, {
default: () => pdf_default
});
module.exports = __toCommonJS(pdf_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_utils = require("playwright-core/lib/utils");
var import_tool = require("./tool");
var javascript = __toESM(require("../codegen"));
var import_utils = require("./utils");
const pdfSchema = import_bundle.z.object({
filename: import_bundle.z.string().optional().describe("File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified. Prefer relative file names to stay within the output directory.")
var import_utils2 = require("./utils");
const pdfSchema = import_mcpBundle.z.object({
filename: import_mcpBundle.z.string().optional().describe("File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified. Prefer relative file names to stay within the output directory.")
});
const pdf = (0, import_tool.defineTabTool)({
capability: "pdf",
@@ -48,10 +38,10 @@ const pdf = (0, import_tool.defineTabTool)({
type: "readOnly"
},
handle: async (tab, params, response) => {
const fileName = await tab.context.outputFile(params.filename ?? (0, import_utils.dateAsFileName)("pdf"), { origin: "llm", reason: "Saving PDF" });
response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
response.addResult(`Saved page as ${fileName}`);
await tab.page.pdf({ path: fileName });
const data = await tab.page.pdf();
const suggestedFilename = params.filename ?? (0, import_utils2.dateAsFileName)("pdf");
await response.addResult({ data, title: "Page as pdf", suggestedFilename });
response.addCode(`await page.pdf(${(0, import_utils.formatObject)({ path: suggestedFilename })});`);
}
});
var pdf_default = [

View File

@@ -33,10 +33,11 @@ __export(runCode_exports, {
module.exports = __toCommonJS(runCode_exports);
var import_vm = __toESM(require("vm"));
var import_utils = require("playwright-core/lib/utils");
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
const codeSchema = import_bundle.z.object({
code: import_bundle.z.string().describe(`Playwright code snippet to run. The snippet should access the \`page\` object to interact with the page. Can make multiple statements. For example: \`await page.getByRole('button', { name: 'Submit' }).click();\``)
const codeSchema = import_mcpBundle.z.object({
code: import_mcpBundle.z.string().describe(`A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: \`async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }\``),
filename: import_mcpBundle.z.string().optional().describe("Filename to save the result to. If not provided, result is returned as JSON string.")
});
const runCode = (0, import_tool.defineTabTool)({
capability: "core",
@@ -49,7 +50,7 @@ const runCode = (0, import_tool.defineTabTool)({
},
handle: async (tab, params, response) => {
response.setIncludeSnapshot();
response.addCode(params.code);
response.addCode(`await (${params.code})(page);`);
const __end__ = new import_utils.ManualPromise();
const context = {
page: tab.page,
@@ -59,14 +60,16 @@ const runCode = (0, import_tool.defineTabTool)({
await tab.waitForCompletion(async () => {
const snippet = `(async () => {
try {
${params.code};
__end__.resolve();
const result = await (${params.code})(page);
__end__.resolve(JSON.stringify(result));
} catch (e) {
__end__.reject(e);
}
})()`;
import_vm.default.runInContext(snippet, context);
await __end__;
await import_vm.default.runInContext(snippet, context);
const result = await __end__;
if (typeof result === "string")
await response.addResult({ text: result, suggestedFilename: params.filename });
});
}
});

View File

@@ -1,9 +1,7 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var screenshot_exports = {};
__export(screenshot_exports, {
@@ -32,19 +22,18 @@ __export(screenshot_exports, {
scaleImageToFitMessage: () => scaleImageToFitMessage
});
module.exports = __toCommonJS(screenshot_exports);
var import_fs = __toESM(require("fs"));
var import_utils = require("playwright-core/lib/utils");
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var import_bundle = require("../../sdk/bundle");
var import_utils2 = require("playwright-core/lib/utils");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
var javascript = __toESM(require("../codegen"));
var import_utils2 = require("./utils");
const screenshotSchema = import_bundle.z.object({
type: import_bundle.z.enum(["png", "jpeg"]).default("png").describe("Image format for the screenshot. Default is png."),
filename: import_bundle.z.string().optional().describe("File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory."),
element: import_bundle.z.string().optional().describe("Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too."),
ref: import_bundle.z.string().optional().describe("Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too."),
fullPage: import_bundle.z.boolean().optional().describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.")
var import_utils3 = require("./utils");
const screenshotSchema = import_mcpBundle.z.object({
type: import_mcpBundle.z.enum(["png", "jpeg"]).default("png").describe("Image format for the screenshot. Default is png."),
filename: import_mcpBundle.z.string().optional().describe("File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory."),
element: import_mcpBundle.z.string().optional().describe("Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too."),
ref: import_mcpBundle.z.string().optional().describe("Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too."),
fullPage: import_mcpBundle.z.boolean().optional().describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.")
});
const screenshot = (0, import_tool.defineTabTool)({
capability: "core",
@@ -61,7 +50,6 @@ const screenshot = (0, import_tool.defineTabTool)({
if (params.fullPage && params.ref)
throw new Error("fullPage cannot be used with element screenshots.");
const fileType = params.type || "png";
const fileName = await tab.context.outputFile(params.filename || (0, import_utils2.dateAsFileName)(fileType), { origin: "llm", reason: "Saving screenshot" });
const options = {
type: fileType,
quality: fileType === "png" ? void 0 : 90,
@@ -70,19 +58,18 @@ const screenshot = (0, import_tool.defineTabTool)({
};
const isElementScreenshot = params.element && params.ref;
const screenshotTarget = isElementScreenshot ? params.element : params.fullPage ? "full page" : "viewport";
response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
const ref = params.ref ? await tab.refLocator({ element: params.element || "", ref: params.ref }) : null;
const data = ref ? await ref.locator.screenshot(options) : await tab.page.screenshot(options);
const fileName = params.filename || (0, import_utils3.dateAsFileName)(fileType);
response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
if (ref)
response.addCode(`await page.${ref.resolved}.screenshot(${javascript.formatObject(options)});`);
response.addCode(`await page.${ref.resolved}.screenshot(${(0, import_utils2.formatObject)({ ...options, path: fileName })});`);
else
response.addCode(`await page.screenshot(${javascript.formatObject(options)});`);
const buffer = ref ? await ref.locator.screenshot(options) : await tab.page.screenshot(options);
await (0, import_utils.mkdirIfNeeded)(fileName);
await import_fs.default.promises.writeFile(fileName, buffer);
response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);
response.addCode(`await page.screenshot(${(0, import_utils2.formatObject)({ ...options, path: fileName })});`);
await response.addResult({ data, title: `Screenshot of ${screenshotTarget}`, suggestedFilename: fileName });
response.addImage({
contentType: fileType === "png" ? "image/png" : "image/jpeg",
data: scaleImageToFitMessage(buffer, fileType)
data: scaleImageToFitMessage(data, fileType)
});
}
});

View File

@@ -1,9 +1,7 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var snapshot_exports = {};
__export(snapshot_exports, {
@@ -32,31 +22,33 @@ __export(snapshot_exports, {
elementSchema: () => elementSchema
});
module.exports = __toCommonJS(snapshot_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_utils = require("playwright-core/lib/utils");
var import_tool = require("./tool");
var javascript = __toESM(require("../codegen"));
const snapshot = (0, import_tool.defineTool)({
capability: "core",
schema: {
name: "browser_snapshot",
title: "Page snapshot",
description: "Capture accessibility snapshot of the current page, this is better than screenshot",
inputSchema: import_bundle.z.object({}),
inputSchema: import_mcpBundle.z.object({
filename: import_mcpBundle.z.string().optional().describe("Save snapshot to markdown file instead of returning it in the response.")
}),
type: "readOnly"
},
handle: async (context, params, response) => {
await context.ensureTab();
response.setIncludeSnapshot("full");
response.setIncludeFullSnapshot(params.filename);
}
});
const elementSchema = import_bundle.z.object({
element: import_bundle.z.string().describe("Human-readable element description used to obtain permission to interact with the element"),
ref: import_bundle.z.string().describe("Exact target element reference from the page snapshot")
const elementSchema = import_mcpBundle.z.object({
element: import_mcpBundle.z.string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
ref: import_mcpBundle.z.string().describe("Exact target element reference from the page snapshot")
});
const clickSchema = elementSchema.extend({
doubleClick: import_bundle.z.boolean().optional().describe("Whether to perform a double click instead of a single click"),
button: import_bundle.z.enum(["left", "right", "middle"]).optional().describe("Button to click, defaults to left"),
modifiers: import_bundle.z.array(import_bundle.z.enum(["Alt", "Control", "ControlOrMeta", "Meta", "Shift"])).optional().describe("Modifier keys to press")
doubleClick: import_mcpBundle.z.boolean().optional().describe("Whether to perform a double click instead of a single click"),
button: import_mcpBundle.z.enum(["left", "right", "middle"]).optional().describe("Button to click, defaults to left"),
modifiers: import_mcpBundle.z.array(import_mcpBundle.z.enum(["Alt", "Control", "ControlOrMeta", "Meta", "Shift"])).optional().describe("Modifier keys to press")
});
const click = (0, import_tool.defineTabTool)({
capability: "core",
@@ -74,7 +66,7 @@ const click = (0, import_tool.defineTabTool)({
button: params.button,
modifiers: params.modifiers
};
const formatted = javascript.formatObject(options, " ", "oneline");
const formatted = (0, import_utils.formatObject)(options, " ", "oneline");
const optionsAttr = formatted !== "{}" ? formatted : "";
if (params.doubleClick)
response.addCode(`await page.${resolved}.dblclick(${optionsAttr});`);
@@ -94,11 +86,11 @@ const drag = (0, import_tool.defineTabTool)({
name: "browser_drag",
title: "Drag mouse",
description: "Perform drag and drop between two elements",
inputSchema: import_bundle.z.object({
startElement: import_bundle.z.string().describe("Human-readable source element description used to obtain the permission to interact with the element"),
startRef: import_bundle.z.string().describe("Exact source element reference from the page snapshot"),
endElement: import_bundle.z.string().describe("Human-readable target element description used to obtain the permission to interact with the element"),
endRef: import_bundle.z.string().describe("Exact target element reference from the page snapshot")
inputSchema: import_mcpBundle.z.object({
startElement: import_mcpBundle.z.string().describe("Human-readable source element description used to obtain the permission to interact with the element"),
startRef: import_mcpBundle.z.string().describe("Exact source element reference from the page snapshot"),
endElement: import_mcpBundle.z.string().describe("Human-readable target element description used to obtain the permission to interact with the element"),
endRef: import_mcpBundle.z.string().describe("Exact target element reference from the page snapshot")
}),
type: "input"
},
@@ -133,7 +125,7 @@ const hover = (0, import_tool.defineTabTool)({
}
});
const selectOptionSchema = elementSchema.extend({
values: import_bundle.z.array(import_bundle.z.string()).describe("Array of values to select in the dropdown. This can be a single value or multiple values.")
values: import_mcpBundle.z.array(import_mcpBundle.z.string()).describe("Array of values to select in the dropdown. This can be a single value or multiple values.")
});
const selectOption = (0, import_tool.defineTabTool)({
capability: "core",
@@ -147,7 +139,7 @@ const selectOption = (0, import_tool.defineTabTool)({
handle: async (tab, params, response) => {
response.setIncludeSnapshot();
const { locator, resolved } = await tab.refLocator(params);
response.addCode(`await page.${resolved}.selectOption(${javascript.formatObject(params.values)});`);
response.addCode(`await page.${resolved}.selectOption(${(0, import_utils.formatObject)(params.values)});`);
await tab.waitForCompletion(async () => {
await locator.selectOption(params.values);
});
@@ -164,7 +156,7 @@ const pickLocator = (0, import_tool.defineTabTool)({
},
handle: async (tab, params, response) => {
const { resolved } = await tab.refLocator(params);
response.addResult(resolved);
response.addTextResult(resolved);
}
});
var snapshot_default = [

View File

@@ -21,17 +21,18 @@ __export(tabs_exports, {
default: () => tabs_default
});
module.exports = __toCommonJS(tabs_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
var import_response = require("../response");
const browserTabs = (0, import_tool.defineTool)({
capability: "core-tabs",
schema: {
name: "browser_tabs",
title: "Manage tabs",
description: "List, create, close, or select a browser tab.",
inputSchema: import_bundle.z.object({
action: import_bundle.z.enum(["list", "new", "close", "select"]).describe("Operation to perform"),
index: import_bundle.z.number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed.")
inputSchema: import_mcpBundle.z.object({
action: import_mcpBundle.z.enum(["list", "new", "close", "select"]).describe("Operation to perform"),
index: import_mcpBundle.z.number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed.")
}),
type: "action"
},
@@ -39,27 +40,26 @@ const browserTabs = (0, import_tool.defineTool)({
switch (params.action) {
case "list": {
await context.ensureTab();
response.setIncludeTabs();
return;
break;
}
case "new": {
await context.newTab();
response.setIncludeTabs();
return;
break;
}
case "close": {
await context.closeTab(params.index);
response.setIncludeSnapshot("full");
return;
break;
}
case "select": {
if (params.index === void 0)
throw new Error("Tab index is required");
await context.selectTab(params.index);
response.setIncludeSnapshot("full");
return;
break;
}
}
const tabHeaders = await Promise.all(context.tabs().map((tab) => tab.headerSnapshot()));
const result = (0, import_response.renderTabsMarkdown)(tabHeaders);
response.addTextResult(result.join("\n"));
}
});
var tabs_default = [

View File

@@ -32,11 +32,9 @@ function defineTabTool(tool) {
const tab = await context.ensureTab();
const modalStates = tab.modalStates().map((state) => state.type);
if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.
` + tab.modalStatesMarkdown().join("\n"));
response.addError(`Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.`);
else if (!tool.clearsModalState && modalStates.length)
response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.
` + tab.modalStatesMarkdown().join("\n"));
response.addError(`Error: Tool "${tool.schema.name}" does not handle the modal state.`);
else
return tool.handle(tab, params, response);
}

View File

@@ -21,7 +21,7 @@ __export(tracing_exports, {
default: () => tracing_default
});
module.exports = __toCommonJS(tracing_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
const tracingStart = (0, import_tool.defineTool)({
capability: "tracing",
@@ -29,12 +29,12 @@ const tracingStart = (0, import_tool.defineTool)({
name: "browser_start_tracing",
title: "Start tracing",
description: "Start trace recording",
inputSchema: import_bundle.z.object({}),
inputSchema: import_mcpBundle.z.object({}),
type: "readOnly"
},
handle: async (context, params, response) => {
const browserContext = await context.ensureBrowserContext();
const tracesDir = await context.outputFile(`traces`, { origin: "code", reason: "Collecting trace" });
const tracesDir = await context.outputFile(`traces`, { origin: "code", title: "Collecting trace" });
const name = "trace-" + Date.now();
await browserContext.tracing.start({
name,
@@ -45,7 +45,7 @@ const tracingStart = (0, import_tool.defineTool)({
const traceLegend = `- Action log: ${tracesDir}/${name}.trace
- Network log: ${tracesDir}/${name}.network
- Resources with content by sha1: ${tracesDir}/resources`;
response.addResult(`Tracing started, saving to ${tracesDir}.
response.addTextResult(`Tracing started, saving to ${tracesDir}.
${traceLegend}`);
browserContext.tracing[traceLegendSymbol] = traceLegend;
}
@@ -56,14 +56,14 @@ const tracingStop = (0, import_tool.defineTool)({
name: "browser_stop_tracing",
title: "Stop tracing",
description: "Stop trace recording",
inputSchema: import_bundle.z.object({}),
inputSchema: import_mcpBundle.z.object({}),
type: "readOnly"
},
handle: async (context, params, response) => {
const browserContext = await context.ensureBrowserContext();
await browserContext.tracing.stop();
const traceLegend = browserContext.tracing[traceLegendSymbol];
response.addResult(`Tracing stopped.
response.addTextResult(`Tracing stopped.
${traceLegend}`);
}
});

View File

@@ -20,59 +20,44 @@ var utils_exports = {};
__export(utils_exports, {
callOnPageNoTrace: () => callOnPageNoTrace,
dateAsFileName: () => dateAsFileName,
eventWaiter: () => eventWaiter,
waitForCompletion: () => waitForCompletion
});
module.exports = __toCommonJS(utils_exports);
async function waitForCompletion(tab, callback) {
const requests = /* @__PURE__ */ new Set();
let frameNavigated = false;
let waitCallback = () => {
};
const waitBarrier = new Promise((f) => {
waitCallback = f;
});
const responseListener = (request) => {
requests.delete(request);
if (!requests.size)
waitCallback();
};
const requestListener = (request) => {
requests.add(request);
void request.response().then(() => responseListener(request)).catch(() => {
});
};
const frameNavigateListener = (frame) => {
if (frame.parentFrame())
return;
frameNavigated = true;
dispose();
clearTimeout(timeout);
void tab.waitForLoadState("load").then(waitCallback);
};
const onTimeout = () => {
dispose();
waitCallback();
const requests = [];
const requestListener = (request) => requests.push(request);
const disposeListeners = () => {
tab.page.off("request", requestListener);
};
tab.page.on("request", requestListener);
tab.page.on("requestfailed", responseListener);
tab.page.on("framenavigated", frameNavigateListener);
const timeout = setTimeout(onTimeout, 1e4);
const dispose = () => {
tab.page.off("request", requestListener);
tab.page.off("requestfailed", responseListener);
tab.page.off("framenavigated", frameNavigateListener);
clearTimeout(timeout);
};
let result;
try {
const result = await callback();
if (!requests.size && !frameNavigated)
waitCallback();
await waitBarrier;
await tab.waitForTimeout(1e3);
return result;
result = await callback();
await tab.waitForTimeout(500);
} finally {
dispose();
disposeListeners();
}
const requestedNavigation = requests.some((request) => request.isNavigationRequest());
if (requestedNavigation) {
await tab.page.mainFrame().waitForLoadState("load", { timeout: 1e4 }).catch(() => {
});
return result;
}
const promises = [];
for (const request of requests) {
if (["document", "stylesheet", "script", "xhr", "fetch"].includes(request.resourceType()))
promises.push(request.response().then((r) => r?.finished()).catch(() => {
}));
else
promises.push(request.response().catch(() => {
}));
}
const timeout = new Promise((resolve) => setTimeout(resolve, 5e3));
await Promise.race([Promise.all(promises), timeout]);
if (requests.length)
await tab.waitForTimeout(500);
return result;
}
async function callOnPageNoTrace(page, callback) {
return await page._wrapApiCall(() => callback(page), { internal: true });
@@ -81,9 +66,29 @@ function dateAsFileName(extension) {
const date = /* @__PURE__ */ new Date();
return `page-${date.toISOString().replace(/[:.]/g, "-")}.${extension}`;
}
function eventWaiter(page, event, timeout) {
const disposables = [];
const eventPromise = new Promise((resolve, reject) => {
page.on(event, resolve);
disposables.push(() => page.off(event, resolve));
});
let abort;
const abortPromise = new Promise((resolve, reject) => {
abort = () => resolve(void 0);
});
const timeoutPromise = new Promise((f) => {
const timeoutId = setTimeout(() => f(void 0), timeout);
disposables.push(() => clearTimeout(timeoutId));
});
return {
promise: Promise.race([eventPromise, abortPromise, timeoutPromise]).finally(() => disposables.forEach((dispose) => dispose())),
abort
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
callOnPageNoTrace,
dateAsFileName,
eventWaiter,
waitForCompletion
});

View File

@@ -1,9 +1,7 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
@@ -17,32 +15,24 @@ var __copyProps = (to, from, except, desc) => {
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var verify_exports = {};
__export(verify_exports, {
default: () => verify_default
});
module.exports = __toCommonJS(verify_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_utils = require("playwright-core/lib/utils");
var import_tool = require("./tool");
var javascript = __toESM(require("../codegen"));
const verifyElement = (0, import_tool.defineTabTool)({
capability: "testing",
schema: {
name: "browser_verify_element_visible",
title: "Verify element visible",
description: "Verify element is visible on the page",
inputSchema: import_bundle.z.object({
role: import_bundle.z.string().describe('ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":`'),
accessibleName: import_bundle.z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"`')
inputSchema: import_mcpBundle.z.object({
role: import_mcpBundle.z.string().describe('ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":`'),
accessibleName: import_mcpBundle.z.string().describe('ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"`')
}),
type: "assertion"
},
@@ -52,8 +42,8 @@ const verifyElement = (0, import_tool.defineTabTool)({
response.addError(`Element with role "${params.role}" and accessible name "${params.accessibleName}" not found`);
return;
}
response.addCode(`await expect(page.getByRole(${javascript.escapeWithQuotes(params.role)}, { name: ${javascript.escapeWithQuotes(params.accessibleName)} })).toBeVisible();`);
response.addResult("Done");
response.addCode(`await expect(page.getByRole(${(0, import_utils.escapeWithQuotes)(params.role)}, { name: ${(0, import_utils.escapeWithQuotes)(params.accessibleName)} })).toBeVisible();`);
response.addTextResult("Done");
}
});
const verifyText = (0, import_tool.defineTabTool)({
@@ -62,8 +52,8 @@ const verifyText = (0, import_tool.defineTabTool)({
name: "browser_verify_text_visible",
title: "Verify text visible",
description: `Verify text is visible on the page. Prefer ${verifyElement.schema.name} if possible.`,
inputSchema: import_bundle.z.object({
text: import_bundle.z.string().describe('TEXT to verify. Can be found in the snapshot like this: `- role "Accessible Name": {TEXT}` or like this: `- text: {TEXT}`')
inputSchema: import_mcpBundle.z.object({
text: import_mcpBundle.z.string().describe('TEXT to verify. Can be found in the snapshot like this: `- role "Accessible Name": {TEXT}` or like this: `- text: {TEXT}`')
}),
type: "assertion"
},
@@ -73,8 +63,8 @@ const verifyText = (0, import_tool.defineTabTool)({
response.addError("Text not found");
return;
}
response.addCode(`await expect(page.getByText(${javascript.escapeWithQuotes(params.text)})).toBeVisible();`);
response.addResult("Done");
response.addCode(`await expect(page.getByText(${(0, import_utils.escapeWithQuotes)(params.text)})).toBeVisible();`);
response.addTextResult("Done");
}
});
const verifyList = (0, import_tool.defineTabTool)({
@@ -83,10 +73,10 @@ const verifyList = (0, import_tool.defineTabTool)({
name: "browser_verify_list_visible",
title: "Verify list visible",
description: "Verify list is visible on the page",
inputSchema: import_bundle.z.object({
element: import_bundle.z.string().describe("Human-readable list description"),
ref: import_bundle.z.string().describe("Exact target element reference that points to the list"),
items: import_bundle.z.array(import_bundle.z.string()).describe("Items to verify")
inputSchema: import_mcpBundle.z.object({
element: import_mcpBundle.z.string().describe("Human-readable list description"),
ref: import_mcpBundle.z.string().describe("Exact target element reference that points to the list"),
items: import_mcpBundle.z.array(import_mcpBundle.z.string()).describe("Items to verify")
}),
type: "assertion"
},
@@ -103,10 +93,10 @@ const verifyList = (0, import_tool.defineTabTool)({
}
const ariaSnapshot = `\`
- list:
${itemTexts.map((t) => ` - listitem: ${javascript.escapeWithQuotes(t, '"')}`).join("\n")}
${itemTexts.map((t) => ` - listitem: ${(0, import_utils.escapeWithQuotes)(t, '"')}`).join("\n")}
\``;
response.addCode(`await expect(page.locator('body')).toMatchAriaSnapshot(${ariaSnapshot});`);
response.addResult("Done");
response.addTextResult("Done");
}
});
const verifyValue = (0, import_tool.defineTabTool)({
@@ -115,11 +105,11 @@ const verifyValue = (0, import_tool.defineTabTool)({
name: "browser_verify_value",
title: "Verify value",
description: "Verify element value",
inputSchema: import_bundle.z.object({
type: import_bundle.z.enum(["textbox", "checkbox", "radio", "combobox", "slider"]).describe("Type of the element"),
element: import_bundle.z.string().describe("Human-readable element description"),
ref: import_bundle.z.string().describe("Exact target element reference that points to the element"),
value: import_bundle.z.string().describe('Value to verify. For checkbox, use "true" or "false".')
inputSchema: import_mcpBundle.z.object({
type: import_mcpBundle.z.enum(["textbox", "checkbox", "radio", "combobox", "slider"]).describe("Type of the element"),
element: import_mcpBundle.z.string().describe("Human-readable element description"),
ref: import_mcpBundle.z.string().describe("Exact target element reference that points to the element"),
value: import_mcpBundle.z.string().describe('Value to verify. For checkbox, use "true" or "false".')
}),
type: "assertion"
},
@@ -132,7 +122,7 @@ const verifyValue = (0, import_tool.defineTabTool)({
response.addError(`Expected value "${params.value}", but got "${value}"`);
return;
}
response.addCode(`await expect(${locatorSource}).toHaveValue(${javascript.quote(params.value)});`);
response.addCode(`await expect(${locatorSource}).toHaveValue(${(0, import_utils.escapeWithQuotes)(params.value)});`);
} else if (params.type === "checkbox" || params.type === "radio") {
const value = await locator.isChecked();
if (value !== (params.value === "true")) {
@@ -142,7 +132,7 @@ const verifyValue = (0, import_tool.defineTabTool)({
const matcher = value ? "toBeChecked" : "not.toBeChecked";
response.addCode(`await expect(${locatorSource}).${matcher}();`);
}
response.addResult("Done");
response.addTextResult("Done");
}
});
var verify_default = [

View File

@@ -21,7 +21,7 @@ __export(wait_exports, {
default: () => wait_default
});
module.exports = __toCommonJS(wait_exports);
var import_bundle = require("../../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_tool = require("./tool");
const wait = (0, import_tool.defineTool)({
capability: "core",
@@ -29,10 +29,10 @@ const wait = (0, import_tool.defineTool)({
name: "browser_wait_for",
title: "Wait for",
description: "Wait for text to appear or disappear or a specified time to pass",
inputSchema: import_bundle.z.object({
time: import_bundle.z.number().optional().describe("The time to wait in seconds"),
text: import_bundle.z.string().optional().describe("The text to wait for"),
textGone: import_bundle.z.string().optional().describe("The text to wait for to disappear")
inputSchema: import_mcpBundle.z.object({
time: import_mcpBundle.z.number().optional().describe("The time to wait in seconds"),
text: import_mcpBundle.z.string().optional().describe("The text to wait for"),
textGone: import_mcpBundle.z.string().optional().describe("The text to wait for to disappear")
}),
type: "assertion"
},
@@ -54,7 +54,7 @@ const wait = (0, import_tool.defineTool)({
response.addCode(`await page.getByText(${JSON.stringify(params.text)}).first().waitFor({ state: 'visible' });`);
await locator.waitFor({ state: "visible" });
}
response.addResult(`Waited for ${params.text || params.textGone || params.time}`);
response.addTextResult(`Waited for ${params.text || params.textGone || params.time}`);
response.setIncludeSnapshot();
}
});

View File

@@ -44,7 +44,7 @@ class CDPRelayServer {
this._playwrightConnection = null;
this._extensionConnection = null;
this._nextSessionId = 1;
this._wsHost = (0, import_http2.httpAddressToString)(server.address()).replace(/^http/, "ws");
this._wsHost = (0, import_http2.addressToString)(server.address(), { protocol: "ws" });
this._browserChannel = browserChannel;
this._userDataDir = userDataDir;
this._executablePath = executablePath;

View File

@@ -33,7 +33,7 @@ __export(extensionContextFactory_exports, {
module.exports = __toCommonJS(extensionContextFactory_exports);
var playwright = __toESM(require("playwright-core"));
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var import_http = require("../sdk/http");
var import_utils = require("playwright-core/lib/utils");
var import_cdpRelay = require("./cdpRelay");
const debugLogger = (0, import_utilsBundle.debug)("pw:mcp:relay");
class ExtensionContextFactory {
@@ -42,8 +42,8 @@ class ExtensionContextFactory {
this._userDataDir = userDataDir;
this._executablePath = executablePath;
}
async createContext(clientInfo, abortSignal, toolName) {
const browser = await this._obtainBrowser(clientInfo, abortSignal, toolName);
async createContext(clientInfo, abortSignal, options) {
const browser = await this._obtainBrowser(clientInfo, abortSignal, options?.toolName);
return {
browserContext: browser.contexts()[0],
close: async () => {
@@ -55,10 +55,11 @@ class ExtensionContextFactory {
async _obtainBrowser(clientInfo, abortSignal, toolName) {
const relay = await this._startRelay(abortSignal);
await relay.ensureExtensionConnectionForMCPContext(clientInfo, abortSignal, toolName);
return await playwright.chromium.connectOverCDP(relay.cdpEndpoint());
return await playwright.chromium.connectOverCDP(relay.cdpEndpoint(), { isLocal: true });
}
async _startRelay(abortSignal) {
const httpServer = await (0, import_http.startHttpServer)({});
const httpServer = (0, import_utils.createHttpServer)();
await (0, import_utils.startHttpServer)(httpServer, {});
if (abortSignal.aborted) {
httpServer.close();
throw new Error(abortSignal.reason);

File diff suppressed because one or more lines are too long

View File

@@ -1,81 +0,0 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var bundle_exports = {};
__export(bundle_exports, {
CallToolRequestSchema: () => CallToolRequestSchema,
Client: () => Client,
ListRootsRequestSchema: () => ListRootsRequestSchema,
ListToolsRequestSchema: () => ListToolsRequestSchema,
PingRequestSchema: () => PingRequestSchema,
ProgressNotificationSchema: () => ProgressNotificationSchema,
SSEClientTransport: () => SSEClientTransport,
SSEServerTransport: () => SSEServerTransport,
Server: () => Server,
StdioClientTransport: () => StdioClientTransport,
StdioServerTransport: () => StdioServerTransport,
StreamableHTTPClientTransport: () => StreamableHTTPClientTransport,
StreamableHTTPServerTransport: () => StreamableHTTPServerTransport,
z: () => z,
zodToJsonSchema: () => zodToJsonSchema
});
module.exports = __toCommonJS(bundle_exports);
var bundle = __toESM(require("../../mcpBundleImpl"));
const zodToJsonSchema = bundle.zodToJsonSchema;
const Client = bundle.Client;
const Server = bundle.Server;
const SSEClientTransport = bundle.SSEClientTransport;
const SSEServerTransport = bundle.SSEServerTransport;
const StdioClientTransport = bundle.StdioClientTransport;
const StdioServerTransport = bundle.StdioServerTransport;
const StreamableHTTPServerTransport = bundle.StreamableHTTPServerTransport;
const StreamableHTTPClientTransport = bundle.StreamableHTTPClientTransport;
const CallToolRequestSchema = bundle.CallToolRequestSchema;
const ListRootsRequestSchema = bundle.ListRootsRequestSchema;
const ProgressNotificationSchema = bundle.ProgressNotificationSchema;
const ListToolsRequestSchema = bundle.ListToolsRequestSchema;
const PingRequestSchema = bundle.PingRequestSchema;
const z = bundle.z;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
CallToolRequestSchema,
Client,
ListRootsRequestSchema,
ListToolsRequestSchema,
PingRequestSchema,
ProgressNotificationSchema,
SSEClientTransport,
SSEServerTransport,
Server,
StdioClientTransport,
StdioServerTransport,
StreamableHTTPClientTransport,
StreamableHTTPServerTransport,
z,
zodToJsonSchema
});

View File

@@ -16,14 +16,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
var exports_exports = {};
module.exports = __toCommonJS(exports_exports);
__reExport(exports_exports, require("./inProcessTransport"), module.exports);
__reExport(exports_exports, require("./proxyBackend"), module.exports);
__reExport(exports_exports, require("./server"), module.exports);
__reExport(exports_exports, require("./tool"), module.exports);
__reExport(exports_exports, require("./http"), module.exports);
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
...require("./inProcessTransport"),
...require("./proxyBackend"),
...require("./server"),
...require("./tool"),
...require("./http")

View File

@@ -28,51 +28,36 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var http_exports = {};
__export(http_exports, {
httpAddressToString: () => httpAddressToString,
installHttpTransport: () => installHttpTransport,
startHttpServer: () => startHttpServer
addressToString: () => addressToString,
startMcpHttpServer: () => startMcpHttpServer
});
module.exports = __toCommonJS(http_exports);
var import_assert = __toESM(require("assert"));
var import_http = __toESM(require("http"));
var import_crypto = __toESM(require("crypto"));
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var mcpBundle = __toESM(require("./bundle"));
var mcpBundle = __toESM(require("playwright-core/lib/mcpBundle"));
var import_utils = require("playwright-core/lib/utils");
var mcpServer = __toESM(require("./server"));
const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
async function startHttpServer(config, abortSignal) {
const { host, port } = config;
const httpServer = import_http.default.createServer();
decorateServer(httpServer);
await new Promise((resolve, reject) => {
httpServer.on("error", reject);
abortSignal?.addEventListener("abort", () => {
httpServer.close();
reject(new Error("Aborted"));
});
httpServer.listen(port, host, () => {
resolve();
httpServer.removeListener("error", reject);
});
});
return httpServer;
async function startMcpHttpServer(config, serverBackendFactory, allowedHosts) {
const httpServer = (0, import_utils.createHttpServer)();
await (0, import_utils.startHttpServer)(httpServer, config);
return await installHttpTransport(httpServer, serverBackendFactory, allowedHosts);
}
function httpAddressToString(address) {
function addressToString(address, options) {
(0, import_assert.default)(address, "Could not bind server socket");
if (typeof address === "string")
return address;
const resolvedPort = address.port;
let resolvedHost = address.family === "IPv4" ? address.address : `[${address.address}]`;
if (resolvedHost === "0.0.0.0" || resolvedHost === "[::]")
resolvedHost = "localhost";
return `http://${resolvedHost}:${resolvedPort}`;
throw new Error("Unexpected address type: " + address);
let host = address.family === "IPv4" ? address.address : `[${address.address}]`;
if (options.normalizeLoopback && (host === "0.0.0.0" || host === "[::]" || host === "[::1]" || host === "127.0.0.1"))
host = "localhost";
return `${options.protocol}://${host}:${address.port}`;
}
async function installHttpTransport(httpServer, serverBackendFactory, unguessableUrl, allowedHosts) {
const url = httpAddressToString(httpServer.address());
async function installHttpTransport(httpServer, serverBackendFactory, allowedHosts) {
const url = addressToString(httpServer.address(), { protocol: "http", normalizeLoopback: true });
const host = new URL(url).host;
allowedHosts = (allowedHosts || [host]).map((h) => h.toLowerCase());
const allowAnyHost = allowedHosts.includes("*");
const pathPrefix = unguessableUrl ? `/${import_crypto.default.randomUUID()}` : "";
const sseSessions = /* @__PURE__ */ new Map();
const streamableSessions = /* @__PURE__ */ new Map();
httpServer.on("request", async (req, res) => {
@@ -87,12 +72,7 @@ async function installHttpTransport(httpServer, serverBackendFactory, unguessabl
return res.end("Access is only allowed at " + allowedHosts.join(", "));
}
}
if (!req.url?.startsWith(pathPrefix)) {
res.statusCode = 404;
return res.end("Not found");
}
const path = req.url?.slice(pathPrefix.length);
const url2 = new URL(`http://localhost${path}`);
const url2 = new URL(`http://localhost${req.url}`);
if (url2.pathname === "/killkillkill" && req.method === "GET") {
res.statusCode = 200;
res.end("Killing process");
@@ -104,7 +84,7 @@ async function installHttpTransport(httpServer, serverBackendFactory, unguessabl
else
await handleStreamable(serverBackendFactory, req, res, streamableSessions);
});
return `${url}${pathPrefix}`;
return url;
}
async function handleSSE(serverBackendFactory, req, res, url, sessions) {
if (req.method === "POST") {
@@ -165,23 +145,8 @@ async function handleStreamable(serverBackendFactory, req, res, sessions) {
res.statusCode = 400;
res.end("Invalid request");
}
function decorateServer(server) {
const sockets = /* @__PURE__ */ new Set();
server.on("connection", (socket) => {
sockets.add(socket);
socket.once("close", () => sockets.delete(socket));
});
const close = server.close;
server.close = (callback) => {
for (const socket of sockets)
socket.destroy();
sockets.clear();
return close.call(server, callback);
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
httpAddressToString,
installHttpTransport,
startHttpServer
addressToString,
startMcpHttpServer
});

View File

@@ -1,128 +0,0 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var proxyBackend_exports = {};
__export(proxyBackend_exports, {
ProxyBackend: () => ProxyBackend
});
module.exports = __toCommonJS(proxyBackend_exports);
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var mcpBundle = __toESM(require("./bundle"));
const errorsDebug = (0, import_utilsBundle.debug)("pw:mcp:errors");
const { z, zodToJsonSchema } = mcpBundle;
class ProxyBackend {
constructor(mcpProviders) {
this._mcpProviders = mcpProviders;
this._contextSwitchTool = this._defineContextSwitchTool();
}
async initialize(clientInfo) {
this._clientInfo = clientInfo;
}
async listTools() {
const currentClient = await this._ensureCurrentClient();
const response = await currentClient.listTools();
if (this._mcpProviders.length === 1)
return response.tools;
return [
...response.tools,
this._contextSwitchTool
];
}
async callTool(name, args) {
if (name === this._contextSwitchTool.name)
return this._callContextSwitchTool(args);
const currentClient = await this._ensureCurrentClient();
return await currentClient.callTool({
name,
arguments: args
});
}
serverClosed() {
void this._currentClient?.close().catch(errorsDebug);
}
async _callContextSwitchTool(params) {
try {
const factory = this._mcpProviders.find((factory2) => factory2.name === params.name);
if (!factory)
throw new Error("Unknown connection method: " + params.name);
await this._setCurrentClient(factory);
return {
content: [{ type: "text", text: "### Result\nSuccessfully changed connection method.\n" }]
};
} catch (error) {
return {
content: [{ type: "text", text: `### Result
Error: ${error}
` }],
isError: true
};
}
}
_defineContextSwitchTool() {
return {
name: "browser_connect",
description: [
"Connect to a browser using one of the available methods:",
...this._mcpProviders.map((factory) => `- "${factory.name}": ${factory.description}`)
].join("\n"),
inputSchema: zodToJsonSchema(z.object({
name: z.enum(this._mcpProviders.map((factory) => factory.name)).default(this._mcpProviders[0].name).describe("The method to use to connect to the browser")
}), { strictUnions: true }),
annotations: {
title: "Connect to a browser context",
readOnlyHint: true,
openWorldHint: false
}
};
}
async _ensureCurrentClient() {
if (this._currentClient)
return this._currentClient;
return await this._setCurrentClient(this._mcpProviders[0]);
}
async _setCurrentClient(factory) {
await this._currentClient?.close();
this._currentClient = void 0;
const client = new mcpBundle.Client({ name: "Playwright MCP Proxy", version: "0.0.0" });
client.registerCapabilities({
roots: {
listRoots: true
}
});
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] }));
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
const transport = await factory.connect();
await client.connect(transport);
this._currentClient = client;
return client;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ProxyBackend
});

View File

@@ -28,16 +28,18 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var server_exports = {};
__export(server_exports, {
allRootPaths: () => allRootPaths,
connect: () => connect,
createServer: () => createServer,
firstRootPath: () => firstRootPath,
start: () => start,
wrapInClient: () => wrapInClient,
wrapInProcess: () => wrapInProcess
});
module.exports = __toCommonJS(server_exports);
var import_url = require("url");
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var mcpBundle = __toESM(require("./bundle"));
var mcpBundle = __toESM(require("playwright-core/lib/mcpBundle"));
var import_http = require("./http");
var import_inProcessTransport = require("./inProcessTransport");
const serverDebug = (0, import_utilsBundle.debug)("pw:mcp:server");
@@ -46,10 +48,18 @@ async function connect(factory, transport, runHeartbeat) {
const server = createServer(factory.name, factory.version, factory.create(), runHeartbeat);
await server.connect(transport);
}
async function wrapInProcess(backend) {
function wrapInProcess(backend) {
const server = createServer("Internal", "0.0.0", backend, false);
return new import_inProcessTransport.InProcessTransport(server);
}
async function wrapInClient(backend, options) {
const server = createServer("Internal", "0.0.0", backend, false);
const transport = new import_inProcessTransport.InProcessTransport(server);
const client = new mcpBundle.Client({ name: options.name, version: options.version });
await client.connect(transport);
await client.ping();
return client;
}
function createServer(name, version, backend, runHeartbeat) {
const server = new mcpBundle.Server({ name, version }, {
capabilities: {
@@ -141,8 +151,7 @@ async function start(serverBackendFactory, options) {
await connect(serverBackendFactory, new mcpBundle.StdioServerTransport(), false);
return;
}
const httpServer = await (0, import_http.startHttpServer)(options);
const url = await (0, import_http.installHttpTransport)(httpServer, serverBackendFactory, false, options.allowedHosts);
const url = await (0, import_http.startMcpHttpServer)(options, serverBackendFactory, options.allowedHosts);
const mcpConfig = { mcpServers: {} };
mcpConfig.mcpServers[serverBackendFactory.nameInConfig] = {
url: `${url}/mcp`
@@ -167,6 +176,20 @@ function firstRootPath(clientInfo) {
return void 0;
}
}
function allRootPaths(clientInfo) {
const paths = [];
for (const root of clientInfo.roots) {
try {
const url = new URL(root.uri);
const path = (0, import_url.fileURLToPath)(url);
if (path)
paths.push(path);
} catch (error) {
serverDebug(error);
}
}
return paths;
}
function mergeTextParts(result) {
const content = [];
const testParts = [];
@@ -190,9 +213,11 @@ function mergeTextParts(result) {
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
allRootPaths,
connect,
createServer,
firstRootPath,
start,
wrapInClient,
wrapInProcess
});

View File

@@ -22,13 +22,13 @@ __export(tool_exports, {
toMcpTool: () => toMcpTool
});
module.exports = __toCommonJS(tool_exports);
var import_bundle = require("../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
function toMcpTool(tool) {
const readOnly = tool.type === "readOnly" || tool.type === "assertion";
return {
name: tool.name,
description: tool.description,
inputSchema: (0, import_bundle.zodToJsonSchema)(tool.inputSchema, { strictUnions: true }),
inputSchema: import_mcpBundle.z.toJSONSchema(tool.inputSchema),
annotations: {
title: tool.title,
readOnlyHint: readOnly,

296
node_modules/playwright/lib/mcp/terminal/cli.js generated vendored Normal file
View File

@@ -0,0 +1,296 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var import_child_process = require("child_process");
var import_crypto = __toESM(require("crypto"));
var import_fs = __toESM(require("fs"));
var import_net = __toESM(require("net"));
var import_os = __toESM(require("os"));
var import_path = __toESM(require("path"));
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var import_socketConnection = require("./socketConnection");
const debugCli = (0, import_utilsBundle.debug)("pw:cli");
const packageJSON = require("../../../package.json");
async function runCliCommand(sessionName, args) {
const session = await connectToDaemon(sessionName);
const result = await session.runCliCommand(args);
console.log(result);
session.dispose();
}
async function socketExists(socketPath) {
try {
const stat = await import_fs.default.promises.stat(socketPath);
if (stat?.isSocket())
return true;
} catch (e) {
}
return false;
}
class SocketSession {
constructor(connection) {
this._nextMessageId = 1;
this._callbacks = /* @__PURE__ */ new Map();
this._connection = connection;
this._connection.onmessage = (message) => this._onMessage(message);
this._connection.onclose = () => this.dispose();
}
async callTool(name, args) {
return this._send(name, args);
}
async runCliCommand(args) {
return await this._send("runCliCommand", { args });
}
async _send(method, params = {}) {
const messageId = this._nextMessageId++;
const message = {
id: messageId,
method,
params
};
await this._connection.send(message);
return new Promise((resolve, reject) => {
this._callbacks.set(messageId, { resolve, reject });
});
}
dispose() {
for (const callback of this._callbacks.values())
callback.reject(new Error("Disposed"));
this._callbacks.clear();
this._connection.close();
}
_onMessage(object) {
if (object.id && this._callbacks.has(object.id)) {
const callback = this._callbacks.get(object.id);
this._callbacks.delete(object.id);
if (object.error)
callback.reject(new Error(object.error));
else
callback.resolve(object.result);
} else if (object.id) {
throw new Error(`Unexpected message id: ${object.id}`);
} else {
throw new Error(`Unexpected message without id: ${JSON.stringify(object)}`);
}
}
}
function localCacheDir() {
if (process.platform === "linux")
return process.env.XDG_CACHE_HOME || import_path.default.join(import_os.default.homedir(), ".cache");
if (process.platform === "darwin")
return import_path.default.join(import_os.default.homedir(), "Library", "Caches");
if (process.platform === "win32")
return process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local");
throw new Error("Unsupported platform: " + process.platform);
}
function playwrightCacheDir() {
return import_path.default.join(localCacheDir(), "ms-playwright");
}
function calculateSha1(buffer) {
const hash = import_crypto.default.createHash("sha1");
hash.update(buffer);
return hash.digest("hex");
}
function socketDirHash() {
return calculateSha1(__dirname);
}
function daemonSocketDir() {
return import_path.default.resolve(playwrightCacheDir(), "daemon", socketDirHash());
}
function daemonSocketPath(sessionName) {
const socketName = `${sessionName}.sock`;
if (import_os.default.platform() === "win32")
return `\\\\.\\pipe\\${socketDirHash()}-${socketName}`;
return import_path.default.resolve(daemonSocketDir(), socketName);
}
async function connectToDaemon(sessionName) {
const socketPath = daemonSocketPath(sessionName);
debugCli(`Connecting to daemon at ${socketPath}`);
if (await socketExists(socketPath)) {
debugCli(`Socket file exists, attempting to connect...`);
try {
return await connectToSocket(socketPath);
} catch (e) {
if (import_os.default.platform() !== "win32")
await import_fs.default.promises.unlink(socketPath).catch(() => {
});
}
}
const cliPath = import_path.default.join(__dirname, "../../../cli.js");
debugCli(`Will launch daemon process: ${cliPath}`);
const userDataDir = import_path.default.resolve(daemonSocketDir(), `${sessionName}-user-data`);
const child = (0, import_child_process.spawn)(process.execPath, [cliPath, "run-mcp-server", `--daemon=${socketPath}`, `--user-data-dir=${userDataDir}`], {
detached: true,
stdio: "ignore",
cwd: process.cwd()
// Will be used as root.
});
child.unref();
const maxRetries = 50;
const retryDelay = 100;
for (let i = 0; i < maxRetries; i++) {
await new Promise((resolve) => setTimeout(resolve, 100));
try {
return await connectToSocket(socketPath);
} catch (e) {
if (e.code !== "ENOENT")
throw e;
debugCli(`Retrying to connect to daemon at ${socketPath} (${i + 1}/${maxRetries})`);
}
}
throw new Error(`Failed to connect to daemon at ${socketPath} after ${maxRetries * retryDelay}ms`);
}
async function connectToSocket(socketPath) {
const socket = await new Promise((resolve, reject) => {
const socket2 = import_net.default.createConnection(socketPath, () => {
debugCli(`Connected to daemon at ${socketPath}`);
resolve(socket2);
});
socket2.on("error", reject);
});
return new SocketSession(new import_socketConnection.SocketConnection(socket));
}
function currentSessionPath() {
return import_path.default.resolve(daemonSocketDir(), "current-session");
}
async function getCurrentSession() {
try {
const session = await import_fs.default.promises.readFile(currentSessionPath(), "utf-8");
return session.trim() || "default";
} catch {
return "default";
}
}
async function setCurrentSession(sessionName) {
await import_fs.default.promises.mkdir(daemonSocketDir(), { recursive: true });
await import_fs.default.promises.writeFile(currentSessionPath(), sessionName);
}
async function canConnectToSocket(socketPath) {
return new Promise((resolve) => {
const socket = import_net.default.createConnection(socketPath, () => {
socket.destroy();
resolve(true);
});
socket.on("error", () => {
resolve(false);
});
});
}
async function listSessions() {
const dir = daemonSocketDir();
try {
const files = await import_fs.default.promises.readdir(dir);
const sessions = [];
for (const file of files) {
if (file.endsWith("-user-data")) {
const sessionName = file.slice(0, -"-user-data".length);
const socketPath = daemonSocketPath(sessionName);
const live = await canConnectToSocket(socketPath);
sessions.push({ name: sessionName, live });
}
}
return sessions;
} catch {
return [];
}
}
function resolveSessionName(args) {
if (args.session)
return args.session;
if (process.env.PLAYWRIGHT_CLI_SESSION)
return process.env.PLAYWRIGHT_CLI_SESSION;
return "default";
}
async function handleSessionCommand(args) {
const subcommand = args._[1];
if (!subcommand) {
const current = await getCurrentSession();
console.log(current);
return;
}
if (subcommand === "list") {
const sessions = await listSessions();
const current = await getCurrentSession();
console.log("Sessions:");
for (const session of sessions) {
const marker = session.name === current ? "->" : " ";
const liveMarker = session.live ? " (live)" : "";
console.log(`${marker} ${session.name}${liveMarker}`);
}
if (sessions.length === 0)
console.log(" (no sessions)");
return;
}
if (subcommand === "set") {
const sessionName = args._[2];
if (!sessionName) {
console.error("Usage: playwright-cli session set <session-name>");
process.exit(1);
}
await setCurrentSession(sessionName);
console.log(`Current session set to: ${sessionName}`);
return;
}
console.error(`Unknown session subcommand: ${subcommand}`);
process.exit(1);
}
async function main() {
const argv = process.argv.slice(2);
const args = require("minimist")(argv);
const help = require("./help.json");
const commandName = args._[0];
if (args.version || args.v) {
console.log(packageJSON.version);
process.exit(0);
}
if (commandName === "session") {
await handleSessionCommand(args);
return;
}
const command = help.commands[commandName];
if (args.help || args.h) {
if (command) {
console.log(command);
} else {
console.log("playwright-cli - run playwright mcp commands from terminal\n");
console.log(help.global);
}
process.exit(0);
}
if (!command) {
console.error(`Unknown command: ${commandName}
`);
console.log(help.global);
process.exit(1);
}
let sessionName = resolveSessionName(args);
if (sessionName === "default" && !args.session && !process.env.PLAYWRIGHT_CLI_SESSION)
sessionName = await getCurrentSession();
runCliCommand(sessionName, args).catch((e) => {
console.error(e.message);
process.exit(1);
});
}
main().catch((e) => {
console.error(e.message);
process.exit(1);
});

56
node_modules/playwright/lib/mcp/terminal/command.js generated vendored Normal file
View File

@@ -0,0 +1,56 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var command_exports = {};
__export(command_exports, {
declareCommand: () => declareCommand,
parseCommand: () => parseCommand
});
module.exports = __toCommonJS(command_exports);
function declareCommand(command) {
return command;
}
function parseCommand(command, args) {
const shape = command.args ? command.args.shape : {};
const argv = args["_"];
const options = command.options?.parse({ ...args, _: void 0 }) ?? {};
const argsObject = {};
let i = 0;
for (const name of Object.keys(shape))
argsObject[name] = argv[++i];
let parsedArgsObject = {};
try {
parsedArgsObject = command.args?.parse(argsObject) ?? {};
} catch (e) {
throw new Error(formatZodError(e));
}
const toolName = typeof command.toolName === "function" ? command.toolName(parsedArgsObject, options) : command.toolName;
const toolParams = command.toolParams(parsedArgsObject, options);
return { toolName, toolParams };
}
function formatZodError(error) {
const issue = error.issues[0];
if (issue.code === "invalid_type")
return `${issue.message} in <${issue.path.join(".")}>`;
return error.issues.map((i) => i.message).join("\n");
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
declareCommand,
parseCommand
});

333
node_modules/playwright/lib/mcp/terminal/commands.js generated vendored Normal file
View File

@@ -0,0 +1,333 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var commands_exports = {};
__export(commands_exports, {
commands: () => commands
});
module.exports = __toCommonJS(commands_exports);
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_command = require("./command");
const click = (0, import_command.declareCommand)({
name: "click",
description: "Perform click on a web page",
args: import_mcpBundle.z.object({
ref: import_mcpBundle.z.string().describe("Exact target element reference from the page snapshot")
}),
options: import_mcpBundle.z.object({
button: import_mcpBundle.z.string().optional().describe("Button to click, defaults to left"),
modifiers: import_mcpBundle.z.array(import_mcpBundle.z.string()).optional().describe("Modifier keys to press")
}),
toolName: "browser_click",
toolParams: ({ ref }, { button, modifiers }) => ({ ref, button, modifiers })
});
const doubleClick = (0, import_command.declareCommand)({
name: "dblclick",
description: "Perform double click on a web page",
args: import_mcpBundle.z.object({
ref: import_mcpBundle.z.string().describe("Exact target element reference from the page snapshot")
}),
options: import_mcpBundle.z.object({
button: import_mcpBundle.z.string().optional().describe("Button to click, defaults to left"),
modifiers: import_mcpBundle.z.array(import_mcpBundle.z.string()).optional().describe("Modifier keys to press")
}),
toolName: "browser_click",
toolParams: ({ ref }, { button, modifiers }) => ({ ref, button, modifiers, doubleClick: true })
});
const close = (0, import_command.declareCommand)({
name: "close",
description: "Close the page",
args: import_mcpBundle.z.object({}),
toolName: "browser_close",
toolParams: () => ({})
});
const consoleMessages = (0, import_command.declareCommand)({
name: "console",
description: "Returns all console messages",
args: import_mcpBundle.z.object({
level: import_mcpBundle.z.string().optional().describe('Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info".')
}),
toolName: "browser_console_messages",
toolParams: ({ level }) => ({ level })
});
const drag = (0, import_command.declareCommand)({
name: "drag",
description: "Perform drag and drop between two elements",
args: import_mcpBundle.z.object({
startRef: import_mcpBundle.z.string().describe("Exact source element reference from the page snapshot"),
endRef: import_mcpBundle.z.string().describe("Exact target element reference from the page snapshot")
}),
options: import_mcpBundle.z.object({
headed: import_mcpBundle.z.boolean().default(false).describe("Run browser in headed mode")
}),
toolName: "browser_drag",
toolParams: ({ startRef, endRef }) => ({ startRef, endRef })
});
const evaluate = (0, import_command.declareCommand)({
name: "evaluate",
description: "Evaluate JavaScript expression on page or element",
args: import_mcpBundle.z.object({
function: import_mcpBundle.z.string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
ref: import_mcpBundle.z.string().optional().describe("Exact target element reference from the page snapshot")
}),
toolName: "browser_evaluate",
toolParams: ({ function: fn, ref }) => ({ function: fn, ref })
});
const fileUpload = (0, import_command.declareCommand)({
name: "upload-file",
description: "Upload one or multiple files",
args: import_mcpBundle.z.object({}),
options: import_mcpBundle.z.object({
paths: import_mcpBundle.z.array(import_mcpBundle.z.string()).optional().describe("The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.")
}),
toolName: "browser_file_upload",
toolParams: (_, { paths }) => ({ paths })
});
const handleDialog = (0, import_command.declareCommand)({
name: "handle-dialog",
description: "Handle a dialog",
args: import_mcpBundle.z.object({
accept: import_mcpBundle.z.boolean().describe("Whether to accept the dialog."),
promptText: import_mcpBundle.z.string().optional().describe("The text of the prompt in case of a prompt dialog.")
}),
toolName: "browser_handle_dialog",
toolParams: ({ accept, promptText }) => ({ accept, promptText })
});
const hover = (0, import_command.declareCommand)({
name: "hover",
description: "Hover over element on page",
args: import_mcpBundle.z.object({
ref: import_mcpBundle.z.string().describe("Exact target element reference from the page snapshot")
}),
toolName: "browser_hover",
toolParams: ({ ref }) => ({ ref })
});
const open = (0, import_command.declareCommand)({
name: "open",
description: "Open URL",
args: import_mcpBundle.z.object({
url: import_mcpBundle.z.string().describe("The URL to navigate to")
}),
options: import_mcpBundle.z.object({
headed: import_mcpBundle.z.boolean().default(false).describe("Run browser in headed mode")
}),
toolName: "browser_open",
toolParams: ({ url }, { headed }) => ({ url, headed })
});
const navigateBack = (0, import_command.declareCommand)({
name: "go-back",
description: "Go back to the previous page",
args: import_mcpBundle.z.object({}),
toolName: "browser_navigate_back",
toolParams: () => ({})
});
const networkRequests = (0, import_command.declareCommand)({
name: "network-requests",
description: "Returns all network requests since loading the page",
args: import_mcpBundle.z.object({}),
options: import_mcpBundle.z.object({
includeStatic: import_mcpBundle.z.boolean().optional().describe("Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false.")
}),
toolName: "browser_network_requests",
toolParams: (_, { includeStatic }) => ({ includeStatic })
});
const pressKey = (0, import_command.declareCommand)({
name: "press",
description: "Press a key on the keyboard",
args: import_mcpBundle.z.object({
key: import_mcpBundle.z.string().describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`")
}),
toolName: "browser_press_key",
toolParams: ({ key }) => ({ key })
});
const resize = (0, import_command.declareCommand)({
name: "resize",
description: "Resize the browser window",
args: import_mcpBundle.z.object({
width: import_mcpBundle.z.number().describe("Width of the browser window"),
height: import_mcpBundle.z.number().describe("Height of the browser window")
}),
toolName: "browser_resize",
toolParams: ({ width, height }) => ({ width, height })
});
const runCode = (0, import_command.declareCommand)({
name: "run-code",
description: "Run Playwright code snippet",
args: import_mcpBundle.z.object({
code: import_mcpBundle.z.string().describe("A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction.")
}),
toolName: "browser_run_code",
toolParams: ({ code }) => ({ code })
});
const selectOption = (0, import_command.declareCommand)({
name: "select-option",
description: "Select an option in a dropdown",
args: import_mcpBundle.z.object({
ref: import_mcpBundle.z.string().describe("Exact target element reference from the page snapshot"),
values: import_mcpBundle.z.array(import_mcpBundle.z.string()).describe("Array of values to select in the dropdown. This can be a single value or multiple values.")
}),
toolName: "browser_select_option",
toolParams: ({ ref, values }) => ({ ref, values })
});
const snapshot = (0, import_command.declareCommand)({
name: "snapshot",
description: "Capture accessibility snapshot of the current page, this is better than screenshot",
args: import_mcpBundle.z.object({}),
options: import_mcpBundle.z.object({
filename: import_mcpBundle.z.string().optional().describe("Save snapshot to markdown file instead of returning it in the response.")
}),
toolName: "browser_snapshot",
toolParams: (_, { filename }) => ({ filename })
});
const screenshot = (0, import_command.declareCommand)({
name: "screenshot",
description: "Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.",
args: import_mcpBundle.z.object({
ref: import_mcpBundle.z.string().optional().describe("Exact target element reference from the page snapshot.")
}),
options: import_mcpBundle.z.object({
filename: import_mcpBundle.z.string().optional().describe("File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified."),
fullPage: import_mcpBundle.z.boolean().optional().describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport.")
}),
toolName: "browser_take_screenshot",
toolParams: ({ ref }, { filename, fullPage }) => ({ filename, ref, fullPage })
});
const type = (0, import_command.declareCommand)({
name: "type",
description: "Type text into editable element",
args: import_mcpBundle.z.object({
text: import_mcpBundle.z.string().describe("Text to type into the element")
}),
options: import_mcpBundle.z.object({
submit: import_mcpBundle.z.boolean().optional().describe("Whether to submit entered text (press Enter after)")
}),
toolName: "browser_press_sequentially",
toolParams: ({ text }, { submit }) => ({ text, submit })
});
const waitFor = (0, import_command.declareCommand)({
name: "wait-for",
description: "Wait for text to appear or disappear or a specified time to pass",
args: import_mcpBundle.z.object({}),
options: import_mcpBundle.z.object({
time: import_mcpBundle.z.number().optional().describe("The time to wait in seconds"),
text: import_mcpBundle.z.string().optional().describe("The text to wait for"),
textGone: import_mcpBundle.z.string().optional().describe("The text to wait for to disappear")
}),
toolName: "browser_wait_for",
toolParams: (_, { time, text, textGone }) => ({ time, text, textGone })
});
const tab = (0, import_command.declareCommand)({
name: "tab",
description: "Close a browser tab",
args: import_mcpBundle.z.object({
action: import_mcpBundle.z.string().describe(`Action to perform on tabs, 'list' | 'new' | 'close' | 'select'`),
index: import_mcpBundle.z.number().optional().describe("Tab index. If omitted, current tab is closed.")
}),
toolName: "browser_tabs",
toolParams: ({ action, index }) => ({ action, index })
});
const mouseClickXy = (0, import_command.declareCommand)({
name: "mouse-click-xy",
description: "Click left mouse button at a given position",
args: import_mcpBundle.z.object({
x: import_mcpBundle.z.number().describe("X coordinate"),
y: import_mcpBundle.z.number().describe("Y coordinate")
}),
toolName: "browser_mouse_click_xy",
toolParams: ({ x, y }) => ({ x, y })
});
const mouseDragXy = (0, import_command.declareCommand)({
name: "mouse-drag-xy",
description: "Drag left mouse button to a given position",
args: import_mcpBundle.z.object({
startX: import_mcpBundle.z.number().describe("Start X coordinate"),
startY: import_mcpBundle.z.number().describe("Start Y coordinate"),
endX: import_mcpBundle.z.number().describe("End X coordinate"),
endY: import_mcpBundle.z.number().describe("End Y coordinate")
}),
toolName: "browser_mouse_drag_xy",
toolParams: ({ startX, startY, endX, endY }) => ({ startX, startY, endX, endY })
});
const mouseMoveXy = (0, import_command.declareCommand)({
name: "mouse-move-xy",
description: "Move mouse to a given position",
args: import_mcpBundle.z.object({
x: import_mcpBundle.z.number().describe("X coordinate"),
y: import_mcpBundle.z.number().describe("Y coordinate")
}),
toolName: "browser_mouse_move_xy",
toolParams: ({ x, y }) => ({ x, y })
});
const pdfSave = (0, import_command.declareCommand)({
name: "pdf-save",
description: "Save page as PDF",
args: import_mcpBundle.z.object({}),
options: import_mcpBundle.z.object({
filename: import_mcpBundle.z.string().optional().describe("File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.")
}),
toolName: "browser_pdf_save",
toolParams: (_, { filename }) => ({ filename })
});
const startTracing = (0, import_command.declareCommand)({
name: "start-tracing",
description: "Start trace recording",
args: import_mcpBundle.z.object({}),
toolName: "browser_start_tracing",
toolParams: () => ({})
});
const stopTracing = (0, import_command.declareCommand)({
name: "stop-tracing",
description: "Stop trace recording",
args: import_mcpBundle.z.object({}),
toolName: "browser_stop_tracing",
toolParams: () => ({})
});
const commandsArray = [
click,
close,
doubleClick,
consoleMessages,
drag,
evaluate,
fileUpload,
handleDialog,
hover,
open,
navigateBack,
networkRequests,
pressKey,
resize,
runCode,
selectOption,
snapshot,
screenshot,
type,
waitFor,
tab,
mouseClickXy,
mouseDragXy,
mouseMoveXy,
pdfSave,
startTracing,
stopTracing
];
const commands = Object.fromEntries(commandsArray.map((cmd) => [cmd.name, cmd]));
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
commands
});

129
node_modules/playwright/lib/mcp/terminal/daemon.js generated vendored Normal file
View File

@@ -0,0 +1,129 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var daemon_exports = {};
__export(daemon_exports, {
startMcpDaemonServer: () => startMcpDaemonServer
});
module.exports = __toCommonJS(daemon_exports);
var import_promises = __toESM(require("fs/promises"));
var import_net = __toESM(require("net"));
var import_os = __toESM(require("os"));
var import_path = __toESM(require("path"));
var import_url = __toESM(require("url"));
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var import_socketConnection = require("./socketConnection");
var import_commands = require("./commands");
var import_command = require("./command");
const daemonDebug = (0, import_utilsBundle.debug)("pw:daemon");
async function socketExists(socketPath) {
try {
const stat = await import_promises.default.stat(socketPath);
if (stat?.isSocket())
return true;
} catch (e) {
}
return false;
}
async function startMcpDaemonServer(socketPath, serverBackendFactory) {
if (import_os.default.platform() !== "win32" && await socketExists(socketPath)) {
daemonDebug(`Socket already exists, removing: ${socketPath}`);
try {
await import_promises.default.unlink(socketPath);
} catch (error) {
daemonDebug(`Failed to remove existing socket: ${error}`);
throw error;
}
}
const backend = serverBackendFactory.create();
const cwd = import_url.default.pathToFileURL(process.cwd()).href;
await backend.initialize?.({
name: "playwright-cli",
version: "1.0.0",
roots: [{
uri: cwd,
name: "cwd"
}],
timestamp: Date.now()
});
await import_promises.default.mkdir(import_path.default.dirname(socketPath), { recursive: true });
const server = import_net.default.createServer((socket) => {
daemonDebug("new client connection");
const connection = new import_socketConnection.SocketConnection(socket);
connection.onclose = () => {
daemonDebug("client disconnected");
};
connection.onmessage = async (message) => {
const { id, method, params } = message;
try {
daemonDebug("received command", method);
if (method === "runCliCommand") {
const { toolName, toolParams } = parseCliCommand(params.args);
const response = await backend.callTool(toolName, toolParams, () => {
});
await connection.send({ id, result: formatResult(response) });
} else {
throw new Error(`Unknown method: ${method}`);
}
} catch (e) {
daemonDebug("command failed", e);
await connection.send({ id, error: e.message });
}
};
});
return new Promise((resolve, reject) => {
server.on("error", (error) => {
daemonDebug(`server error: ${error.message}`);
reject(error);
});
server.listen(socketPath, () => {
daemonDebug(`daemon server listening on ${socketPath}`);
resolve(socketPath);
});
});
}
function formatResult(result) {
const lines = [];
for (const content of result.content) {
if (content.type === "text")
lines.push(content.text);
else
lines.push(`<${content.type} content>`);
}
return lines.join("\n");
}
function parseCliCommand(args) {
const command = import_commands.commands[args._[0]];
if (!command)
throw new Error("Command is required");
return (0, import_command.parseCommand)(command, args);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
startMcpDaemonServer
});

32
node_modules/playwright/lib/mcp/terminal/help.json generated vendored Normal file
View File

@@ -0,0 +1,32 @@
{
"global": "Usage: playwright-cli <command> [options]\nCommands:\n click <ref> perform click on a web page\n close close the page\n dblclick <ref> perform double click on a web page\n console <level> returns all console messages\n drag <startRef> <endRef> perform drag and drop between two elements\n evaluate <function> <ref> evaluate javascript expression on page or element\n upload-file upload one or multiple files\n handle-dialog <accept> <promptText> handle a dialog\n hover <ref> hover over element on page\n open <url> open url\n go-back go back to the previous page\n network-requests returns all network requests since loading the page\n press <key> press a key on the keyboard\n resize <width> <height> resize the browser window\n run-code <code> run playwright code snippet\n select-option <ref> <values> select an option in a dropdown\n snapshot capture accessibility snapshot of the current page, this is better than screenshot\n screenshot <ref> take a screenshot of the current page. you can't perform actions based on the screenshot, use browser_snapshot for actions.\n type <text> type text into editable element\n wait-for wait for text to appear or disappear or a specified time to pass\n tab <action> <index> close a browser tab\n mouse-click-xy <x> <y> click left mouse button at a given position\n mouse-drag-xy <startX> <startY> <endX> <endY> drag left mouse button to a given position\n mouse-move-xy <x> <y> move mouse to a given position\n pdf-save save page as pdf\n start-tracing start trace recording\n stop-tracing stop trace recording",
"commands": {
"click": "playwright-cli click <ref>\n\nPerform click on a web page\n\nArguments:\n <ref>\tExact target element reference from the page snapshot\nOptions:\n --button\tbutton to click, defaults to left\n --modifiers\tmodifier keys to press",
"close": "playwright-cli close \n\nClose the page\n",
"dblclick": "playwright-cli dblclick <ref>\n\nPerform double click on a web page\n\nArguments:\n <ref>\tExact target element reference from the page snapshot\nOptions:\n --button\tbutton to click, defaults to left\n --modifiers\tmodifier keys to press",
"console": "playwright-cli console <level>\n\nReturns all console messages\n\nArguments:\n <level>\tLevel of the console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\".",
"drag": "playwright-cli drag <startRef> <endRef>\n\nPerform drag and drop between two elements\n\nArguments:\n <startRef>\tExact source element reference from the page snapshot\n <endRef>\tExact target element reference from the page snapshot\nOptions:\n --headed\trun browser in headed mode",
"evaluate": "playwright-cli evaluate <function> <ref>\n\nEvaluate JavaScript expression on page or element\n\nArguments:\n <function>\t() => { /* code */ } or (element) => { /* code */ } when element is provided\n <ref>\tExact target element reference from the page snapshot",
"upload-file": "playwright-cli upload-file \n\nUpload one or multiple files\n\nOptions:\n --paths\tthe absolute paths to the files to upload. can be single file or multiple files. if omitted, file chooser is cancelled.",
"handle-dialog": "playwright-cli handle-dialog <accept> <promptText>\n\nHandle a dialog\n\nArguments:\n <accept>\tWhether to accept the dialog.\n <promptText>\tThe text of the prompt in case of a prompt dialog.",
"hover": "playwright-cli hover <ref>\n\nHover over element on page\n\nArguments:\n <ref>\tExact target element reference from the page snapshot",
"open": "playwright-cli open <url>\n\nOpen URL\n\nArguments:\n <url>\tThe URL to navigate to\nOptions:\n --headed\trun browser in headed mode",
"go-back": "playwright-cli go-back \n\nGo back to the previous page\n",
"network-requests": "playwright-cli network-requests \n\nReturns all network requests since loading the page\n\nOptions:\n --includeStatic\twhether to include successful static resources like images, fonts, scripts, etc. defaults to false.",
"press": "playwright-cli press <key>\n\nPress a key on the keyboard\n\nArguments:\n <key>\tName of the key to press or a character to generate, such as `ArrowLeft` or `a`",
"resize": "playwright-cli resize <width> <height>\n\nResize the browser window\n\nArguments:\n <width>\tWidth of the browser window\n <height>\tHeight of the browser window",
"run-code": "playwright-cli run-code <code>\n\nRun Playwright code snippet\n\nArguments:\n <code>\tA JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction.",
"select-option": "playwright-cli select-option <ref> <values>\n\nSelect an option in a dropdown\n\nArguments:\n <ref>\tExact target element reference from the page snapshot\n <values>\tArray of values to select in the dropdown. This can be a single value or multiple values.",
"snapshot": "playwright-cli snapshot \n\nCapture accessibility snapshot of the current page, this is better than screenshot\n\nOptions:\n --filename\tsave snapshot to markdown file instead of returning it in the response.",
"screenshot": "playwright-cli screenshot <ref>\n\nTake a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.\n\nArguments:\n <ref>\tExact target element reference from the page snapshot.\nOptions:\n --filename\tfile name to save the screenshot to. defaults to `page-{timestamp}.{png|jpeg}` if not specified.\n --fullPage\twhen true, takes a screenshot of the full scrollable page, instead of the currently visible viewport.",
"type": "playwright-cli type <text>\n\nType text into editable element\n\nArguments:\n <text>\tText to type into the element\nOptions:\n --submit\twhether to submit entered text (press enter after)",
"wait-for": "playwright-cli wait-for \n\nWait for text to appear or disappear or a specified time to pass\n\nOptions:\n --time\tthe time to wait in seconds\n --text\tthe text to wait for\n --textGone\tthe text to wait for to disappear",
"tab": "playwright-cli tab <action> <index>\n\nClose a browser tab\n\nArguments:\n <action>\tAction to perform on tabs, 'list' | 'new' | 'close' | 'select'\n <index>\tTab index. If omitted, current tab is closed.",
"mouse-click-xy": "playwright-cli mouse-click-xy <x> <y>\n\nClick left mouse button at a given position\n\nArguments:\n <x>\tX coordinate\n <y>\tY coordinate",
"mouse-drag-xy": "playwright-cli mouse-drag-xy <startX> <startY> <endX> <endY>\n\nDrag left mouse button to a given position\n\nArguments:\n <startX>\tStart X coordinate\n <startY>\tStart Y coordinate\n <endX>\tEnd X coordinate\n <endY>\tEnd Y coordinate",
"mouse-move-xy": "playwright-cli mouse-move-xy <x> <y>\n\nMove mouse to a given position\n\nArguments:\n <x>\tX coordinate\n <y>\tY coordinate",
"pdf-save": "playwright-cli pdf-save \n\nSave page as PDF\n\nOptions:\n --filename\tfile name to save the pdf to. defaults to `page-{timestamp}.pdf` if not specified.",
"start-tracing": "playwright-cli start-tracing \n\nStart trace recording\n",
"stop-tracing": "playwright-cli stop-tracing \n\nStop trace recording\n"
}
}

View File

@@ -0,0 +1,88 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_commands = require("./commands");
function generateCommandHelp(command) {
const args = [];
const shape = command.args ? command.args.shape : {};
for (const [name, schema] of Object.entries(shape)) {
const zodSchema = schema;
const description = zodSchema.description ?? "";
args.push({ name, description });
}
const lines = [
`playwright-cli ${command.name} ${Object.keys(shape).map((k) => `<${k}>`).join(" ")}`,
"",
command.description,
""
];
if (args.length) {
lines.push("Arguments:");
lines.push(...args.map(({ name, description }) => ` <${name}> ${description}`));
}
if (command.options) {
lines.push("Options:");
const optionsShape = command.options.shape;
for (const [name, schema] of Object.entries(optionsShape)) {
const zodSchema = schema;
const description = (zodSchema.description ?? "").toLowerCase();
lines.push(` --${name} ${description}`);
}
}
return lines.join("\n");
}
function generateHelp() {
const lines = [];
lines.push("Usage: playwright-cli <command> [options]");
lines.push("Commands:");
for (const command of Object.values(import_commands.commands))
lines.push(" " + generateHelpEntry(command));
return lines.join("\n");
}
function generateHelpEntry(command) {
const args = [];
const shape = command.args.shape;
for (const [name, schema] of Object.entries(shape)) {
const zodSchema = schema;
const description = zodSchema.description ?? "";
args.push({ name, description });
}
const prefix = `${command.name} ${Object.keys(shape).map((k) => `<${k}>`).join(" ")}`;
const suffix = command.description.toLowerCase();
const padding = " ".repeat(Math.max(1, 40 - prefix.length));
return prefix + padding + suffix;
}
async function main() {
const help = {
global: generateHelp(),
commands: Object.fromEntries(
Object.entries(import_commands.commands).map(([name, command]) => [name, generateCommandHelp(command)])
)
};
const fileName = import_path.default.resolve(__dirname, "help.json").replace("lib", "src");
console.log("Writing ", import_path.default.relative(process.cwd(), fileName));
await import_fs.default.promises.writeFile(fileName, JSON.stringify(help, null, 2));
}
void main();

View File

@@ -0,0 +1,80 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var socketConnection_exports = {};
__export(socketConnection_exports, {
SocketConnection: () => SocketConnection
});
module.exports = __toCommonJS(socketConnection_exports);
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
const daemonDebug = (0, import_utilsBundle.debug)("pw:daemon");
class SocketConnection {
constructor(socket) {
this._pendingBuffers = [];
this._socket = socket;
socket.on("data", (buffer) => this._onData(buffer));
socket.on("close", () => {
this.onclose?.();
});
socket.on("error", (e) => daemonDebug(`error: ${e.message}`));
}
async send(message) {
await new Promise((resolve, reject) => {
this._socket.write(`${JSON.stringify(message)}
`, (error) => {
if (error)
reject(error);
else
resolve(void 0);
});
});
}
close() {
this._socket.destroy();
}
_onData(buffer) {
let end = buffer.indexOf("\n");
if (end === -1) {
this._pendingBuffers.push(buffer);
return;
}
this._pendingBuffers.push(buffer.slice(0, end));
const message = Buffer.concat(this._pendingBuffers).toString();
this._dispatchMessage(message);
let start = end + 1;
end = buffer.indexOf("\n", start);
while (end !== -1) {
const message2 = buffer.toString(void 0, start, end);
this._dispatchMessage(message2);
start = end + 1;
end = buffer.indexOf("\n", start);
}
this._pendingBuffers = [buffer.slice(start)];
}
_dispatchMessage(message) {
try {
this.onmessage?.(JSON.parse(message));
} catch (e) {
daemonDebug("failed to dispatch message", e);
}
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
SocketConnection
});

View File

@@ -25,13 +25,14 @@ var import_config = require("../browser/config");
var import_browserServerBackend = require("../browser/browserServerBackend");
var import_tab = require("../browser/tab");
var import_util = require("../../util");
var import_browserContextFactory = require("../browser/browserContextFactory");
function createCustomMessageHandler(testInfo, context) {
let backend;
return async (data) => {
if (data.initialize) {
if (backend)
throw new Error("MCP backend is already initialized");
backend = new import_browserServerBackend.BrowserServerBackend({ ...import_config.defaultConfig, capabilities: ["testing"] }, identityFactory(context));
backend = new import_browserServerBackend.BrowserServerBackend({ ...import_config.defaultConfig, capabilities: ["testing"] }, (0, import_browserContextFactory.identityBrowserContextFactory)(context));
await backend.initialize(data.initialize.clientInfo);
const pausedMessage = await generatePausedMessage(testInfo, context);
return { initialize: { pausedMessage } };
@@ -73,7 +74,7 @@ async function generatePausedMessage(testInfo, context) {
`- Page Title: ${await page.title()}`.trim()
);
let console = testInfo.errors.length ? await import_tab.Tab.collectConsoleMessages(page) : [];
console = console.filter((msg) => !msg.type || msg.type === "error");
console = console.filter((msg) => msg.type === "error");
if (console.length) {
lines.push("- Console Messages:");
for (const message of console)
@@ -91,17 +92,6 @@ async function generatePausedMessage(testInfo, context) {
lines.push(`### Task`, `Try recovering from the error prior to continuing`);
return lines.join("\n");
}
function identityFactory(browserContext) {
return {
createContext: async (clientInfo, abortSignal, toolName) => {
return {
browserContext,
close: async () => {
}
};
}
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createCustomMessageHandler

View File

@@ -35,7 +35,7 @@ __export(generatorTools_exports, {
module.exports = __toCommonJS(generatorTools_exports);
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_bundle = require("../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_testTool = require("./testTool");
var import_testContext = require("./testContext");
const setupPage = (0, import_testTool.defineTestTool)({
@@ -43,10 +43,10 @@ const setupPage = (0, import_testTool.defineTestTool)({
name: "generator_setup_page",
title: "Setup generator page",
description: "Setup the page for test.",
inputSchema: import_bundle.z.object({
plan: import_bundle.z.string().describe("The plan for the test. This should be the actual test plan with all the steps."),
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.'),
seedFile: import_bundle.z.string().optional().describe('A seed file contains a single test that is used to setup the page for testing, for example: "tests/seed.spec.ts". If no seed file is provided, a default seed file is created.')
inputSchema: import_mcpBundle.z.object({
plan: import_mcpBundle.z.string().describe("The plan for the test. This should be the actual test plan with all the steps."),
project: import_mcpBundle.z.string().optional().describe('Project to use for setup. For example: "chromium", if no project is provided uses the first project in the config.'),
seedFile: import_mcpBundle.z.string().optional().describe('A seed file contains a single test that is used to setup the page for testing, for example: "tests/seed.spec.ts". If no seed file is provided, a default seed file is created.')
}),
type: "readOnly"
},
@@ -62,7 +62,7 @@ const generatorReadLog = (0, import_testTool.defineTestTool)({
name: "generator_read_log",
title: "Retrieve test log",
description: "Retrieve the performed test log",
inputSchema: import_bundle.z.object({}),
inputSchema: import_mcpBundle.z.object({}),
type: "readOnly"
},
handle: async (context) => {
@@ -80,9 +80,9 @@ const generatorWriteTest = (0, import_testTool.defineTestTool)({
name: "generator_write_test",
title: "Write test",
description: "Write the generated test to the test file",
inputSchema: import_bundle.z.object({
fileName: import_bundle.z.string().describe("The file to write the test to"),
code: import_bundle.z.string().describe("The generated test code")
inputSchema: import_mcpBundle.z.object({
fileName: import_mcpBundle.z.string().describe("The file to write the test to"),
code: import_mcpBundle.z.string().describe("The generated test code")
}),
type: "readOnly"
},

View File

@@ -35,16 +35,16 @@ __export(plannerTools_exports, {
module.exports = __toCommonJS(plannerTools_exports);
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_bundle = require("../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_testTool = require("./testTool");
const setupPage = (0, import_testTool.defineTestTool)({
schema: {
name: "planner_setup_page",
title: "Setup planner page",
description: "Setup the page for test planning",
inputSchema: import_bundle.z.object({
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.'),
seedFile: import_bundle.z.string().optional().describe('A seed file contains a single test that is used to setup the page for testing, for example: "tests/seed.spec.ts". If no seed file is provided, a default seed file is created.')
inputSchema: import_mcpBundle.z.object({
project: import_mcpBundle.z.string().optional().describe('Project to use for setup. For example: "chromium", if no project is provided uses the first project in the config.'),
seedFile: import_mcpBundle.z.string().optional().describe('A seed file contains a single test that is used to setup the page for testing, for example: "tests/seed.spec.ts". If no seed file is provided, a default seed file is created.')
}),
type: "readOnly"
},
@@ -54,16 +54,18 @@ const setupPage = (0, import_testTool.defineTestTool)({
return { content: [{ type: "text", text: output }], isError: status !== "paused" };
}
});
const planSchema = import_bundle.z.object({
overview: import_bundle.z.string().describe("A brief overview of the application to be tested"),
suites: import_bundle.z.array(import_bundle.z.object({
name: import_bundle.z.string().describe("The name of the suite"),
seedFile: import_bundle.z.string().describe("A seed file that was used to setup the page for testing."),
tests: import_bundle.z.array(import_bundle.z.object({
name: import_bundle.z.string().describe("The name of the test"),
file: import_bundle.z.string().describe('The file the test should be saved to, for example: "tests/<suite-name>/<test-name>.spec.ts".'),
steps: import_bundle.z.array(import_bundle.z.string().describe(`The steps to be executed to perform the test. For example: 'Click on the "Submit" button'`)),
expectedResults: import_bundle.z.array(import_bundle.z.string().describe("The expected results of the steps for test to verify."))
const planSchema = import_mcpBundle.z.object({
overview: import_mcpBundle.z.string().describe("A brief overview of the application to be tested"),
suites: import_mcpBundle.z.array(import_mcpBundle.z.object({
name: import_mcpBundle.z.string().describe("The name of the suite"),
seedFile: import_mcpBundle.z.string().describe("A seed file that was used to setup the page for testing."),
tests: import_mcpBundle.z.array(import_mcpBundle.z.object({
name: import_mcpBundle.z.string().describe("The name of the test"),
file: import_mcpBundle.z.string().describe('The file the test should be saved to, for example: "tests/<suite-name>/<test-name>.spec.ts".'),
steps: import_mcpBundle.z.array(import_mcpBundle.z.object({
perform: import_mcpBundle.z.string().optional().describe(`Action to perform. For example: 'Click on the "Submit" button'.`),
expect: import_mcpBundle.z.string().array().describe(`Expected result of the action where appropriate. For example: 'The page should show the "Thank you for your submission" message'`)
}))
}))
}))
});
@@ -90,8 +92,8 @@ const saveTestPlan = (0, import_testTool.defineTestTool)({
title: "Save test plan as markdown file",
description: "Save the test plan as a markdown file",
inputSchema: planSchema.extend({
name: import_bundle.z.string().describe('The name of the test plan, for example: "Test Plan".'),
fileName: import_bundle.z.string().describe('The file to save the test plan to, for example: "spec/test.plan.md". Relative to the workspace root.')
name: import_mcpBundle.z.string().describe('The name of the test plan, for example: "Test Plan".'),
fileName: import_mcpBundle.z.string().describe('The file to save the test plan to, for example: "spec/test.plan.md". Relative to the workspace root.')
}),
type: "readOnly"
},
@@ -118,12 +120,11 @@ const saveTestPlan = (0, import_testTool.defineTestTool)({
lines.push(`**File:** \`${test.file}\``);
lines.push(``);
lines.push(`**Steps:**`);
for (let k = 0; k < test.steps.length; k++)
lines.push(` ${k + 1}. ${test.steps[k]}`);
lines.push(``);
lines.push(`**Expected Results:**`);
for (const result of test.expectedResults)
lines.push(` - ${result}`);
for (let k = 0; k < test.steps.length; k++) {
lines.push(` ${k + 1}. ${test.steps[k].perform ?? "-"}`);
for (const expect of test.steps[k].expect)
lines.push(` - expect: ${expect}`);
}
}
}
lines.push(``);

View File

@@ -31,15 +31,15 @@ __export(testBackend_exports, {
TestServerBackend: () => TestServerBackend
});
module.exports = __toCommonJS(testBackend_exports);
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var mcp = __toESM(require("../sdk/exports"));
var import_testContext = require("./testContext");
var testTools = __toESM(require("./testTools.js"));
var generatorTools = __toESM(require("./generatorTools.js"));
var plannerTools = __toESM(require("./plannerTools.js"));
var import_tools = require("../browser/tools");
var import_bundle = require("../sdk/bundle");
class TestServerBackend {
constructor(configOption, options) {
constructor(configPath, options) {
this.name = "Playwright";
this.version = "0.0.1";
this._tools = [
@@ -55,10 +55,10 @@ class TestServerBackend {
...import_tools.browserTools.map((tool) => wrapBrowserTool(tool))
];
this._options = options || {};
this._configOption = configOption;
this._configPath = configPath;
}
async initialize(clientInfo) {
this._context = new import_testContext.TestContext(clientInfo, this._configOption, this._options);
this._context = new import_testContext.TestContext(clientInfo, this._configPath, this._options);
}
async listTools() {
return this._tools.map((tool) => mcp.toMcpTool(tool.schema));
@@ -74,13 +74,13 @@ class TestServerBackend {
}
}
serverClosed() {
void this._context.close();
void this._context?.close();
}
}
const typesWithIntent = ["action", "assertion", "input"];
function wrapBrowserTool(tool) {
const inputSchema = typesWithIntent.includes(tool.schema.type) ? tool.schema.inputSchema.extend({
intent: import_bundle.z.string().describe("The intent of the call, for example the test step description plan idea")
intent: import_mcpBundle.z.string().describe("The intent of the call, for example the test step description plan idea")
}) : tool.schema.inputSchema;
return {
schema: {

View File

@@ -62,7 +62,7 @@ class GeneratorJournal {
const result = [];
result.push(`# Plan`);
result.push(this._plan);
result.push(`# Seed file: ${import_path.default.relative(this._rootPath, this._seed.file)}`);
result.push(`# Seed file: ${(0, import_utils.toPosixPath)(import_path.default.relative(this._rootPath, this._seed.file))}`);
result.push("```ts");
result.push(this._seed.content);
result.push("```");
@@ -172,7 +172,7 @@ class TestContext {
const { testRunner, screen, claimStdio, releaseStdio } = testRunnerAndScreen;
claimStdio();
try {
const setupReporter = new import_list.default({ configDir, screen, includeTestId: true });
const setupReporter = new MCPListReporter({ configDir, screen, includeTestId: true });
const { status: status2 } = await testRunner.runGlobalSetup([setupReporter]);
if (status2 !== "passed")
return { output: testRunnerAndScreen.output.join("\n"), status: status2 };
@@ -191,7 +191,7 @@ class TestContext {
}
};
try {
const reporter = new import_list.default({ configDir, screen, includeTestId: true });
const reporter = new MCPListReporter({ configDir, screen, includeTestId: true });
status = await Promise.race([
testRunner.runTests(reporter, params).then((result) => result.status),
testRunnerAndScreen.waitForTestPaused().then(() => "paused")
@@ -271,6 +271,12 @@ const bestPracticesMarkdown = `
- NEVER! use page.waitForTimeout()
- NEVER! use page.evaluate()
`;
class MCPListReporter extends import_list.default {
async onTestPaused() {
await new Promise(() => {
});
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
GeneratorJournal,

View File

@@ -33,7 +33,7 @@ __export(testTools_exports, {
runTests: () => runTests
});
module.exports = __toCommonJS(testTools_exports);
var import_bundle = require("../sdk/bundle");
var import_mcpBundle = require("playwright-core/lib/mcpBundle");
var import_listModeReporter = __toESM(require("../../reporters/listModeReporter"));
var import_testTool = require("./testTool");
const listTests = (0, import_testTool.defineTestTool)({
@@ -41,7 +41,7 @@ const listTests = (0, import_testTool.defineTestTool)({
name: "test_list",
title: "List tests",
description: "List tests",
inputSchema: import_bundle.z.object({}),
inputSchema: import_mcpBundle.z.object({}),
type: "readOnly"
},
handle: async (context) => {
@@ -56,15 +56,15 @@ const runTests = (0, import_testTool.defineTestTool)({
name: "test_run",
title: "Run tests",
description: "Run tests",
inputSchema: import_bundle.z.object({
locations: import_bundle.z.array(import_bundle.z.string()).optional().describe('Folder, file or location to run: "test/e2e" or "test/e2e/file.spec.ts" or "test/e2e/file.spec.ts:20"'),
projects: import_bundle.z.array(import_bundle.z.string()).optional().describe('Projects to run, projects from playwright.config.ts, by default runs all projects. Running with "chromium" is a good start')
inputSchema: import_mcpBundle.z.object({
locations: import_mcpBundle.z.array(import_mcpBundle.z.string()).optional().describe('Folder, file or location to run: "test/e2e" or "test/e2e/file.spec.ts" or "test/e2e/file.spec.ts:20"'),
projects: import_mcpBundle.z.array(import_mcpBundle.z.string()).optional().describe('Projects to run, projects from playwright.config.ts, by default runs all projects. Running with "chromium" is a good start')
}),
type: "readOnly"
},
handle: async (context, params) => {
const { output } = await context.runTestsWithGlobalSetupAndPossiblePause({
locations: params.locations,
locations: params.locations ?? [],
projects: params.projects,
disableConfigReporters: true
});
@@ -76,10 +76,10 @@ const debugTest = (0, import_testTool.defineTestTool)({
name: "test_debug",
title: "Debug single test",
description: "Debug single test",
inputSchema: import_bundle.z.object({
test: import_bundle.z.object({
id: import_bundle.z.string().describe("Test ID to debug."),
title: import_bundle.z.string().describe("Human readable test title for granting permission to debug the test.")
inputSchema: import_mcpBundle.z.object({
test: import_mcpBundle.z.object({
id: import_mcpBundle.z.string().describe("Test ID to debug."),
title: import_mcpBundle.z.string().describe("Human readable test title for granting permission to debug the test.")
})
}),
type: "readOnly"
@@ -87,6 +87,8 @@ const debugTest = (0, import_testTool.defineTestTool)({
handle: async (context, params) => {
const { output, status } = await context.runTestsWithGlobalSetupAndPossiblePause({
headed: context.computedHeaded,
locations: [],
// we can make this faster by passing the test's location, so we don't need to scan all tests to find the ID
testIds: [params.test.id],
// For automatic recovery
timeout: 0,

File diff suppressed because one or more lines are too long

View File

@@ -245,8 +245,8 @@ async function runTests(args, opts) {
(0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
}
async function runTestServer(opts) {
const host = opts.host || "localhost";
const port = opts.port ? +opts.port : 0;
const host = opts.host;
const port = opts.port ? +opts.port : void 0;
const status = await testServer.runTestServer(opts.config, {}, { host, port });
const exitCode = status === "interrupted" ? 130 : status === "passed" ? 0 : 1;
(0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
@@ -282,12 +282,15 @@ function overridesFromOptions(options) {
retries: options.retries ? parseInt(options.retries, 10) : void 0,
reporter: resolveReporterOption(options.reporter),
shard: resolveShardOption(options.shard),
shardWeights: resolveShardWeightsOption(),
timeout: options.timeout ? parseInt(options.timeout, 10) : void 0,
tsconfig: options.tsconfig ? import_path.default.resolve(process.cwd(), options.tsconfig) : void 0,
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : void 0,
updateSnapshots: options.updateSnapshots,
updateSourceMethod: options.updateSourceMethod,
workers: options.workers
runAgents: options.runAgents,
workers: options.workers,
pause: process.env.PWPAUSE ? true : void 0
};
if (options.browser) {
const browserOpt = options.browser.toLowerCase();
@@ -301,7 +304,7 @@ function overridesFromOptions(options) {
};
});
}
if (options.headed || options.debug)
if (options.headed || options.debug || overrides.pause)
overrides.use = { headless: false };
if (!options.ui && options.debug) {
overrides.debug = true;
@@ -340,6 +343,17 @@ function resolveShardOption(shard) {
}
return { current, total };
}
function resolveShardWeightsOption() {
const shardWeights = process.env.PWTEST_SHARD_WEIGHTS;
if (!shardWeights)
return void 0;
return shardWeights.split(":").map((w) => {
const weight = parseInt(w, 10);
if (isNaN(weight) || weight < 0)
throw new Error(`PWTEST_SHARD_WEIGHTS="${shardWeights}" weights must be non-negative numbers`);
return weight;
});
}
function resolveReporter(id) {
if (import_config.builtInReporters.includes(id))
return id;

View File

@@ -36,6 +36,7 @@ __export(base_exports, {
formatRetry: () => formatRetry,
internalScreen: () => internalScreen,
kOutputSymbol: () => kOutputSymbol,
markErrorsAsReported: () => markErrorsAsReported,
nonTerminalScreen: () => nonTerminalScreen,
prepareErrorStack: () => prepareErrorStack,
relativeFilePath: () => relativeFilePath,
@@ -296,19 +297,37 @@ class TerminalReporter {
formatError(error) {
return formatError(this.screen, error);
}
formatResultErrors(test, result) {
return formatResultErrors(this.screen, test, result);
}
writeLine(line) {
this.screen.stdout?.write(line ? line + "\n" : "\n");
}
}
function formatResultErrors(screen, test, result) {
const lines = [];
if (test.outcome() === "unexpected") {
const errorDetails = formatResultFailure(screen, test, result, " ");
if (errorDetails.length > 0)
lines.push("");
for (const error of errorDetails)
lines.push(error.message, "");
}
return lines.join("\n");
}
function formatFailure(screen, config, test, index, options) {
const lines = [];
const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error", includeTestId: options?.includeTestId });
lines.push(screen.colors.red(header));
let printedHeader = false;
for (const result of test.results) {
const resultLines = [];
const errors = formatResultFailure(screen, test, result, " ");
if (!errors.length)
continue;
if (!printedHeader) {
const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error", includeTestId: options?.includeTestId });
lines.push(screen.colors.red(header));
printedHeader = true;
}
if (result.retry) {
resultLines.push("");
resultLines.push(screen.colors.gray(separator(screen, ` Retry #${result.retry}`)));
@@ -383,6 +402,10 @@ function quotePathIfNeeded(path2) {
return `"${path2}"`;
return path2;
}
const kReportedSymbol = Symbol("reported");
function markErrorsAsReported(result) {
result[kReportedSymbol] = result.errors.length;
}
function formatResultFailure(screen, test, result, initialIndent) {
const errorDetails = [];
if (result.status === "passed" && test.expectedStatus === "failed") {
@@ -395,7 +418,8 @@ function formatResultFailure(screen, test, result, initialIndent) {
message: indent(screen.colors.red(`Test was interrupted.`), initialIndent)
});
}
for (const error of result.errors) {
const reportedIndex = result[kReportedSymbol] || 0;
for (const error of result.errors.slice(reportedIndex)) {
const formattedError = formatError(screen, error);
errorDetails.push({
message: indent(formattedError.message, initialIndent),
@@ -423,7 +447,7 @@ function formatTestTitle(screen, config, test, step, options = {}) {
const projectLabel = options.includeTestId ? `project=` : "";
const projectTitle = projectName ? `[${projectLabel}${projectName}] \u203A ` : "";
const testTitle = `${testId}${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`;
const extraTags = test.tags.filter((t) => !testTitle.includes(t));
const extraTags = test.tags.filter((t) => !testTitle.includes(t) && !config.tags.includes(t));
return `${testTitle}${stepSuffix(step)}${extraTags.length ? " " + extraTags.join(" ") : ""}`;
}
function formatTestHeader(screen, config, test, options = {}) {
@@ -599,6 +623,7 @@ function groupAttachments(attachments) {
formatRetry,
internalScreen,
kOutputSymbol,
markErrorsAsReported,
nonTerminalScreen,
prepareErrorStack,
relativeFilePath,

View File

@@ -56,6 +56,7 @@ class BlobReporter extends import_teleEmitter.TeleReporterEmitter {
const metadata = {
version: currentBlobReportVersion,
userAgent: (0, import_utils2.getUserAgent)(),
// TODO: remove after some time, recommend config.tag instead.
name: process.env.PWTEST_BOT_NAME,
shard: config.shard ?? void 0,
pathSeparator: import_path.default.sep
@@ -67,6 +68,8 @@ class BlobReporter extends import_teleEmitter.TeleReporterEmitter {
this._config = config;
super.onConfigure(config);
}
async onTestPaused(test, result) {
}
async onEnd(result) {
await super.onEnd(result);
const zipFileName = await this._prepareOutputFile();

View File

@@ -73,6 +73,23 @@ class DotReporter extends import_base.TerminalReporter {
this.writeLine("\n" + this.formatError(error).message);
this._counter = 0;
}
async onTestPaused(test, result) {
if (!process.stdin.isTTY && !process.env.PW_TEST_DEBUG_REPORTERS)
return;
this.screen.stdout.write("\n");
if (test.outcome() === "unexpected") {
this.writeLine(this.screen.colors.red(this.formatTestHeader(test, { indent: " " })));
this.writeLine(this.formatResultErrors(test, result));
(0, import_base.markErrorsAsReported)(result);
this.writeLine(this.screen.colors.yellow(" Paused on error. Press Ctrl+C to end.") + "\n");
} else {
this.writeLine(this.screen.colors.yellow(this.formatTestHeader(test, { indent: " " })));
this.writeLine(this.screen.colors.yellow(" Paused at test end. Press Ctrl+C to end.") + "\n");
}
this._counter = 0;
await new Promise(() => {
});
}
async onEnd(result) {
await super.onEnd(result);
this.screen.stdout.write("\n");

View File

@@ -51,6 +51,7 @@ const isHtmlReportOption = (type) => {
class HtmlReporter {
constructor(options) {
this._topLevelErrors = [];
this._machines = [];
this._options = options;
}
version() {
@@ -104,6 +105,9 @@ class HtmlReporter {
onError(error) {
this._topLevelErrors.push(error);
}
onMachineEnd(result) {
this._machines.push(result);
}
async onEnd(result) {
const projectSuites = this.suite.suites;
await (0, import_utils.removeFolders)([this._outputFolder]);
@@ -124,7 +128,7 @@ class HtmlReporter {
noSnippets,
noCopyPrompt
});
this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors);
this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors, this._machines);
}
async onExit() {
if (process.env.CI || !this._buildResult)
@@ -215,7 +219,7 @@ class HtmlBuilder {
this._dataZipFile = new import_zipBundle.yazl.ZipFile();
this._attachmentsBaseURL = attachmentsBaseURL;
}
async build(metadata, projectSuites, result, topLevelErrors) {
async build(metadata, projectSuites, result, topLevelErrors, machines) {
const data = /* @__PURE__ */ new Map();
for (const projectSuite of projectSuites) {
const projectName = projectSuite.project().name;
@@ -259,7 +263,13 @@ class HtmlBuilder {
projectNames: projectSuites.map((r) => r.project().name),
stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()) },
errors: topLevelErrors.map((error) => (0, import_base.formatError)(import_base.internalScreen, error).message),
options: this._options
options: this._options,
machines: machines.map((s) => ({
duration: s.duration,
startTime: s.startTime.getTime(),
tag: s.tag,
shardIndex: s.shardIndex
}))
};
htmlReport.files.sort((f1, f2) => {
const w1 = f1.stats.unexpected * 1e3 + f1.stats.flaky;

View File

@@ -66,6 +66,10 @@ class InternalReporter {
onStdErr(chunk, test, result) {
this._reporter.onStdErr?.(chunk, test, result);
}
async onTestPaused(test, result) {
this._addSnippetToTestErrors(test, result);
return await this._reporter.onTestPaused?.(test, result);
}
onTestEnd(test, result) {
this._addSnippetToTestErrors(test, result);
this._reporter.onTestEnd?.(test, result);
@@ -112,6 +116,8 @@ function addLocationAndSnippetToError(config, error, file) {
const location = error.location;
if (!location)
return;
if (!!error.snippet)
return;
try {
const tokens = [];
const source = import_fs.default.readFileSync(location.file, "utf8");

View File

@@ -74,6 +74,24 @@ class LineReporter extends import_base.TerminalReporter {
if (this.screen.isTTY && step.category === "test.step")
this._updateLine(test, result, step.parent);
}
async onTestPaused(test, result) {
if (!process.stdin.isTTY && !process.env.PW_TEST_DEBUG_REPORTERS)
return;
if (!process.env.PW_TEST_DEBUG_REPORTERS)
this.screen.stdout.write(`\x1B[1A\x1B[2K`);
if (test.outcome() === "unexpected") {
this.writeLine(this.screen.colors.red(this.formatTestHeader(test, { indent: " ", index: ++this._failures })));
this.writeLine(this.formatResultErrors(test, result));
(0, import_base.markErrorsAsReported)(result);
this.writeLine(this.screen.colors.yellow(` Paused on error. Press Ctrl+C to end.`) + "\n\n");
} else {
this.writeLine(this.screen.colors.yellow(this.formatTestHeader(test, { indent: " " })));
this.writeLine(this.screen.colors.yellow(` Paused at test end. Press Ctrl+C to end.`) + "\n\n");
}
this._updateLine(test, result, void 0);
await new Promise(() => {
});
}
onTestEnd(test, result) {
super.onTestEnd(test, result);
if (!this.willRetry(test) && (test.outcome() === "flaky" || test.outcome() === "unexpected" || result.status === "interrupted")) {

View File

@@ -38,6 +38,7 @@ class ListReporter extends import_base.TerminalReporter {
this._resultIndex = /* @__PURE__ */ new Map();
this._stepIndex = /* @__PURE__ */ new Map();
this._needNewLine = false;
this._paused = /* @__PURE__ */ new Set();
this._printSteps = (0, import_utils.getAsBooleanFromENV)("PLAYWRIGHT_LIST_PRINT_STEPS", options?.printSteps);
}
onBegin(suite) {
@@ -144,8 +145,29 @@ class ListReporter extends import_base.TerminalReporter {
this._updateLineCountAndNewLineFlagForOutput(text);
stream.write(chunk);
}
async onTestPaused(test, result) {
if (!process.stdin.isTTY && !process.env.PW_TEST_DEBUG_REPORTERS)
return;
this._paused.add(result);
this._updateTestLine(test, result);
this._maybeWriteNewLine();
if (test.outcome() === "unexpected") {
const errors = this.formatResultErrors(test, result);
this.writeLine(errors);
this._updateLineCountAndNewLineFlagForOutput(errors);
(0, import_base.markErrorsAsReported)(result);
}
this._appendLine(this.screen.colors.yellow(`Paused ${test.outcome() === "unexpected" ? "on error" : "at test end"}. Press Ctrl+C to end.`), this._testPrefix("", ""));
await new Promise(() => {
});
}
onTestEnd(test, result) {
super.onTestEnd(test, result);
const wasPaused = this._paused.delete(result);
if (!wasPaused)
this._updateTestLine(test, result);
}
_updateTestLine(test, result) {
const title = this.formatTestTitle(test);
let prefix = "";
let text = "";

View File

@@ -55,10 +55,15 @@ async function createMergedReport(config, dir, reporterDescriptions, rootDirOver
throw new Error(`No report files found in ${dir}`);
const eventData = await mergeEvents(dir, shardFiles, stringPool, printStatus, rootDirOverride);
const pathSeparator = rootDirOverride ? import_path.default.sep : eventData.pathSeparatorFromMetadata ?? import_path.default.sep;
const pathPackage = pathSeparator === "/" ? import_path.default.posix : import_path.default.win32;
const receiver = new import_teleReceiver.TeleReporterReceiver(multiplexer, {
mergeProjects: false,
mergeTestCases: false,
resolvePath: (rootDir, relativePath) => stringPool.internString(rootDir + pathSeparator + relativePath),
// When merging on a different OS, an absolute path like `C:\foo\bar` from win may look like
// a relative path on posix, and vice versa.
// Therefore, we cannot use `path.resolve()` here - it will resolve relative-looking paths
// against `process.cwd()`, while we just want to normalize ".." and "." segments.
resolvePath: (rootDir, relativePath) => stringPool.internString(pathPackage.normalize(pathPackage.join(rootDir, relativePath))),
configOverrides: config.config
});
printStatus(`processing test events`);
@@ -72,7 +77,7 @@ async function createMergedReport(config, dir, reporterDescriptions, rootDirOver
}
};
await dispatchEvents(eventData.prologue);
for (const { reportFile, eventPatchers, metadata, tags } of eventData.reports) {
for (const { reportFile, eventPatchers, metadata, tags, startTime, duration } of eventData.reports) {
const reportJsonl = await import_fs.default.promises.readFile(reportFile);
const events = parseTestEvents(reportJsonl);
new import_stringInternPool.JsonStringInternalizer(stringPool).traverse(events);
@@ -83,6 +88,12 @@ async function createMergedReport(config, dir, reporterDescriptions, rootDirOver
eventPatchers.patchers.push(new GlobalErrorPatcher(tags.join(" ")));
eventPatchers.patchEvents(events);
await dispatchEvents(events);
multiplexer.onMachineEnd({
startTime: new Date(startTime),
duration,
tag: tags,
shardIndex: metadata.shard?.current
});
}
await dispatchEvents(eventData.epilogue);
}
@@ -181,6 +192,8 @@ async function mergeEvents(dir, shardReportFiles, stringPool, printStatus, rootD
eventPatchers.patchers.push(new PathSeparatorPatcher(metadata.pathSeparator));
eventPatchers.patchEvents(parsedEvents);
let tags = [];
let startTime = 0;
let duration = 0;
for (const event of parsedEvents) {
if (event.method === "onConfigure") {
configureEvents.push(event);
@@ -188,14 +201,18 @@ async function mergeEvents(dir, shardReportFiles, stringPool, printStatus, rootD
} else if (event.method === "onProject") {
projectEvents.push(event);
} else if (event.method === "onEnd") {
endEvents.push(event);
endEvents.push({ event, metadata, tags });
startTime = event.params.result.startTime;
duration = event.params.result.duration;
}
}
reports.push({
eventPatchers,
reportFile: localPath,
metadata,
tags
tags,
startTime,
duration
});
}
return {
@@ -270,8 +287,8 @@ function mergeConfigs(to, from) {
function mergeEndEvents(endEvents) {
let startTime = endEvents.length ? 1e13 : Date.now();
let status = "passed";
let duration = 0;
for (const event of endEvents) {
let endTime = 0;
for (const { event } of endEvents) {
const shardResult = event.params.result;
if (shardResult.status === "failed")
status = "failed";
@@ -280,12 +297,12 @@ function mergeEndEvents(endEvents) {
else if (shardResult.status === "interrupted" && status !== "failed" && status !== "timedout")
status = "interrupted";
startTime = Math.min(startTime, shardResult.startTime);
duration = Math.max(duration, shardResult.duration);
endTime = Math.max(endTime, shardResult.startTime + shardResult.duration);
}
const result = {
status,
startTime,
duration
duration: endTime - startTime
};
return {
method: "onEnd",

View File

@@ -48,10 +48,18 @@ class Multiplexer {
for (const reporter of this._reporters)
wrap(() => reporter.onStdErr?.(chunk, test, result));
}
async onTestPaused(test, result) {
for (const reporter of this._reporters)
await wrapAsync(() => reporter.onTestPaused?.(test, result));
}
onTestEnd(test, result) {
for (const reporter of this._reporters)
wrap(() => reporter.onTestEnd?.(test, result));
}
onMachineEnd(result) {
for (const reporter of this._reporters)
wrap(() => reporter.onMachineEnd?.(result));
}
async onEnd(result) {
for (const reporter of this._reporters) {
const outResult = await wrapAsync(() => reporter.onEnd?.(result));

View File

@@ -37,7 +37,8 @@ var import_teleReceiver = require("../isomorphic/teleReceiver");
class TeleReporterEmitter {
constructor(messageSink, options = {}) {
this._resultKnownAttachmentCounts = /* @__PURE__ */ new Map();
// In case there is blob reporter and UI mode, make sure one does override
this._resultKnownErrorCounts = /* @__PURE__ */ new Map();
// In case there is blob reporter and UI mode, make sure one doesn't override
// the id assigned by the other.
this._idSymbol = Symbol("id");
this._messageSink = messageSink;
@@ -66,6 +67,20 @@ class TeleReporterEmitter {
}
});
}
async onTestPaused(test, result) {
const resultId = result[this._idSymbol];
this._resultKnownErrorCounts.set(resultId, result.errors.length);
this._messageSink({
method: "onTestPaused",
params: {
testId: test.id,
resultId,
errors: result.errors
}
});
await new Promise(() => {
});
}
onTestEnd(test, result) {
const testEnd = {
testId: test.id,
@@ -81,7 +96,9 @@ class TeleReporterEmitter {
result: this._serializeResultEnd(result)
}
});
this._resultKnownAttachmentCounts.delete(result[this._idSymbol]);
const resultId = result[this._idSymbol];
this._resultKnownAttachmentCounts.delete(resultId);
this._resultKnownErrorCounts.delete(resultId);
}
onStepBegin(test, result, step) {
step[this._idSymbol] = (0, import_utils.createGuid)();
@@ -221,11 +238,12 @@ class TeleReporterEmitter {
};
}
_serializeResultEnd(result) {
const id = result[this._idSymbol];
return {
id: result[this._idSymbol],
id,
duration: result.duration,
status: result.status,
errors: result.errors,
errors: this._resultKnownErrorCounts.has(id) ? result.errors.slice(this._resultKnownAttachmentCounts.get(id)) : result.errors,
annotations: result.annotations?.length ? this._relativeAnnotationLocations(result.annotations) : void 0
};
}

View File

@@ -28,6 +28,7 @@ var import_workerHost = require("./workerHost");
var import_ipc = require("../common/ipc");
var import_internalReporter = require("../reporters/internalReporter");
var import_util = require("../util");
var import_storage = require("./storage");
class Dispatcher {
constructor(config, reporter, failureTracker) {
this._workerSlots = [];
@@ -197,6 +198,12 @@ class Dispatcher {
const producedEnv = this._producedEnvByProjectId.get(testGroup.projectId) || {};
this._producedEnvByProjectId.set(testGroup.projectId, { ...producedEnv, ...worker.producedEnv() });
});
worker.onRequest("cloneStorage", async (params) => {
return await import_storage.Storage.clone(params.storageFile, outputDir);
});
worker.onRequest("upstreamStorage", async (params) => {
await import_storage.Storage.upstream(params.storageFile, params.storageOutFile);
});
return worker;
}
producedEnvByProjectId() {
@@ -458,11 +465,15 @@ class JobDispatcher {
];
}
_onTestPaused(worker, params) {
const data = this._dataByTestId.get(params.testId);
if (!data)
return;
const { result, test } = data;
const sendMessage = async (message) => {
try {
if (this.jobResult.isDone())
throw new Error("Test has already stopped");
const response = await worker.sendCustomMessage({ testId: params.testId, request: message.request });
const response = await worker.sendCustomMessage({ testId: test.id, request: message.request });
if (response.error)
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, response.error);
return response;
@@ -472,8 +483,12 @@ class JobDispatcher {
return { response: void 0, error };
}
};
for (const error of params.errors)
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, error);
result.status = params.status;
result.errors = params.errors;
result.error = result.errors[0];
void this._reporter.onTestPaused?.(test, result).then(() => {
worker.sendResume({});
});
this._failureTracker.onTestPaused?.({ ...params, sendMessage });
}
skipWholeJob() {

View File

@@ -162,7 +162,7 @@ async function createRootSuite(testRun, errors, shouldFilterOnly) {
for (const group of (0, import_testGroups.createTestGroups)(projectSuite, config.config.shard.total))
testGroups.push(group);
}
const testGroupsInThisShard = (0, import_testGroups.filterForShard)(config.config.shard, testGroups);
const testGroupsInThisShard = (0, import_testGroups.filterForShard)(config.config.shard, config.configCLIOverrides.shardWeights, testGroups);
const testsInThisShard = /* @__PURE__ */ new Set();
for (const group of testGroupsInThisShard) {
for (const test of group.tests)
@@ -317,7 +317,7 @@ async function loadTestList(config, filePath) {
const relativeFile = (0, import_utils.toPosixPath)(import_path.default.relative(config.config.rootDir, test.location.file));
if (relativeFile !== d.file)
return false;
return d.titlePath.length === titles.length && d.titlePath.every((_, index) => titles[index] === d.titlePath[index]);
return d.titlePath.length <= titles.length && d.titlePath.every((_, index) => titles[index] === d.titlePath[index]);
});
} catch (e) {
throw (0, import_util.errorWithFile)(filePath, "Cannot read test list file: " + e.message);

View File

@@ -44,6 +44,7 @@ class ProcessHost extends import_events.EventEmitter {
this._lastMessageId = 0;
this._callbacks = /* @__PURE__ */ new Map();
this._producedEnv = {};
this._requestHandlers = /* @__PURE__ */ new Map();
this._runnerScript = runnerScript;
this._processName = processName;
this._extraEnv = env;
@@ -51,6 +52,9 @@ class ProcessHost extends import_events.EventEmitter {
async startRunner(runnerParams, options = {}) {
(0, import_utils.assert)(!this.process, "Internal error: starting the same process twice");
this.process = import_child_process.default.fork(require.resolve("../common/process"), {
// Note: we pass detached:false, so that workers are in the same process group.
// This way Ctrl+C or a kill command can shutdown all workers in case they misbehave.
// Otherwise user can end up with a bunch of workers stuck in a busy loop without self-destructing.
detached: false,
env: {
...process.env,
@@ -92,6 +96,18 @@ class ProcessHost extends import_events.EventEmitter {
} else {
this.emit(method, params);
}
} else if (message.method === "__request__") {
const { id, method, params } = message.params;
const handler = this._requestHandlers.get(method);
if (!handler) {
this.send({ method: "__response__", params: { id, error: { message: "Unknown method" } } });
} else {
handler(params).then((result) => {
this.send({ method: "__response__", params: { id, result } });
}).catch((error2) => {
this.send({ method: "__response__", params: { id, error: { message: error2.message } } });
});
}
} else {
this.emit(message.method, message.params);
}
@@ -135,6 +151,9 @@ class ProcessHost extends import_events.EventEmitter {
}
async onExit() {
}
onRequest(method, handler) {
this._requestHandlers.set(method, handler);
}
async stop() {
if (!this._processDidExit && !this._didSendStop) {
this.send({ method: "__stop__" });

View File

@@ -145,7 +145,7 @@ function buildDependentProjects(forProjects, projects) {
return result;
}
async function collectFilesForProject(project, fsCache = /* @__PURE__ */ new Map()) {
const extensions = /* @__PURE__ */ new Set([".js", ".ts", ".mjs", ".mts", ".cjs", ".cts", ".jsx", ".tsx", ".mjsx", ".mtsx", ".cjsx", ".ctsx"]);
const extensions = /* @__PURE__ */ new Set([".js", ".ts", ".mjs", ".mts", ".cjs", ".cts", ".jsx", ".tsx", ".mjsx", ".mtsx", ".cjsx", ".ctsx", ".md"]);
const testFileExtension = (file) => extensions.has(import_path.default.extname(file));
const allFiles = await cachedCollectFiles(project.project.testDir, project.respectGitIgnore, fsCache);
const testMatch = (0, import_util2.createFileMatcher)(project.project.testMatch);

91
node_modules/playwright/lib/runner/storage.js generated vendored Normal file
View File

@@ -0,0 +1,91 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var storage_exports = {};
__export(storage_exports, {
Storage: () => Storage
});
module.exports = __toCommonJS(storage_exports);
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_utils = require("playwright-core/lib/utils");
class Storage {
static {
this._storages = /* @__PURE__ */ new Map();
}
static {
this._serializeQueue = Promise.resolve();
}
static clone(storageFile, outputDir) {
return Storage._withStorage(storageFile, (storage) => storage._clone(outputDir));
}
static upstream(storageFile, storageOutFile) {
return Storage._withStorage(storageFile, (storage) => storage._upstream(storageOutFile));
}
static _withStorage(fileName, runnable) {
this._serializeQueue = this._serializeQueue.then(() => {
let storage = Storage._storages.get(fileName);
if (!storage) {
storage = new Storage(fileName);
Storage._storages.set(fileName, storage);
}
return runnable(storage);
});
return this._serializeQueue;
}
constructor(fileName) {
this._fileName = fileName;
}
async _clone(outputDir) {
const entries = await this._load();
if (this._lastSnapshotFileName)
return this._lastSnapshotFileName;
const snapshotFile = import_path.default.join(outputDir, `pw-storage-${(0, import_utils.createGuid)()}.json`);
await import_fs.default.promises.writeFile(snapshotFile, JSON.stringify(entries, null, 2)).catch(() => {
});
this._lastSnapshotFileName = snapshotFile;
return snapshotFile;
}
async _upstream(storageOutFile) {
const entries = await this._load();
const newEntries = await import_fs.default.promises.readFile(storageOutFile, "utf8").then(JSON.parse).catch(() => ({}));
for (const [key, newValue] of Object.entries(newEntries))
entries[key] = newValue;
this._lastSnapshotFileName = void 0;
await import_fs.default.promises.writeFile(this._fileName, JSON.stringify(entries, null, 2));
}
async _load() {
if (!this._entriesPromise)
this._entriesPromise = import_fs.default.promises.readFile(this._fileName, "utf8").then(JSON.parse).catch(() => ({}));
return this._entriesPromise;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Storage
});

View File

@@ -92,15 +92,23 @@ function createTestGroups(projectSuite, expectedParallelism) {
}
return result;
}
function filterForShard(shard, testGroups) {
function filterForShard(shard, weights, testGroups) {
weights ??= Array.from({ length: shard.total }, () => 1);
if (weights.length !== shard.total)
throw new Error(`PWTEST_SHARD_WEIGHTS number of weights must match the shard total of ${shard.total}`);
const totalWeight = weights.reduce((a, b) => a + b, 0);
let shardableTotal = 0;
for (const group of testGroups)
shardableTotal += group.tests.length;
const shardSize = Math.floor(shardableTotal / shard.total);
const extraOne = shardableTotal - shardSize * shard.total;
const currentShard = shard.current - 1;
const from = shardSize * currentShard + Math.min(extraOne, currentShard);
const to = from + shardSize + (currentShard < extraOne ? 1 : 0);
const shardSizes = weights.map((w) => Math.floor(w * shardableTotal / totalWeight));
const remainder = shardableTotal - shardSizes.reduce((a, b) => a + b, 0);
for (let i = 0; i < remainder; i++) {
shardSizes[i % shardSizes.length]++;
}
let from = 0;
for (let i = 0; i < shard.current - 1; i++)
from += shardSizes[i];
const to = from + shardSizes[shard.current - 1];
let current = 0;
const result = /* @__PURE__ */ new Set();
for (const group of testGroups) {

View File

@@ -63,6 +63,7 @@ class TestRunner extends import_events.default {
this._queue = Promise.resolve();
this._watchTestDirs = false;
this._populateDependenciesOnList = false;
this._startingEnv = {};
this.configLocation = configLocation;
this._configCLIOverrides = configCLIOverrides;
this._watcher = new import_fsWatcher.Watcher((events) => {
@@ -75,6 +76,7 @@ class TestRunner extends import_events.default {
(0, import_utils.setPlaywrightTestProcessEnv)();
this._watchTestDirs = !!params.watchTestDirs;
this._populateDependenciesOnList = !!params.populateDependenciesOnList;
this._startingEnv = { ...process.env };
}
resizeTerminal(params) {
process.stdout.columns = params.cols;
@@ -107,15 +109,20 @@ class TestRunner extends import_events.default {
const reporter = new import_internalReporter.InternalReporter(userReporters);
const config = await this._loadConfigOrReportError(reporter, this._configCLIOverrides);
if (!config)
return { status: "failed" };
return { status: "failed", env: [] };
const { status, cleanup } = await (0, import_tasks.runTasksDeferCleanup)(new import_tasks.TestRun(config, reporter), [
...(0, import_tasks.createGlobalSetupTasks)(config)
]);
const env = [];
for (const key of /* @__PURE__ */ new Set([...Object.keys(process.env), ...Object.keys(this._startingEnv)])) {
if (this._startingEnv[key] !== process.env[key])
env.push([key, process.env[key] ?? null]);
}
if (status !== "passed")
await cleanup();
else
this._globalSetup = { cleanup };
return { status };
return { status, env };
}
async runGlobalTeardown() {
const globalSetup = this._globalSetup;
@@ -251,6 +258,7 @@ class TestRunner extends import_events.default {
},
...params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {},
...params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {},
...params.runAgents ? { runAgents: params.runAgents } : {},
...params.workers ? { workers: params.workers } : {}
};
const config = await this._loadConfigOrReportError(new import_internalReporter.InternalReporter([userReporter]), overrides);
@@ -258,7 +266,7 @@ class TestRunner extends import_events.default {
return { status: "failed" };
config.cliListOnly = false;
config.cliPassWithNoTests = true;
config.cliArgs = params.locations || [];
config.cliArgs = params.locations;
config.cliGrep = params.grep;
config.cliGrepInvert = params.grepInvert;
config.cliProjectFilter = params.projects?.length ? params.projects : void 0;
@@ -376,7 +384,8 @@ async function runAllTestsWithConfig(config) {
(0, import_tasks.createLoadTask)("in-process", { filterOnly: true, failOnLoadErrors: true }),
...(0, import_tasks.createRunTestsTasks)(config)
];
const status = await (0, import_tasks.runTasks)(new import_tasks.TestRun(config, reporter), tasks, config.config.globalTimeout);
const testRun = new import_tasks.TestRun(config, reporter, { pauseAtEnd: config.configCLIOverrides.pause, pauseOnError: config.configCLIOverrides.pause });
const status = await (0, import_tasks.runTasks)(testRun, tasks, config.config.globalTimeout);
await new Promise((resolve) => process.stdout.write("", () => resolve()));
await new Promise((resolve) => process.stderr.write("", () => resolve()));
return status;

View File

@@ -114,8 +114,8 @@ class TestServerDispatcher {
async runGlobalSetup(params) {
const { reporter, report } = await this._collectingReporter();
this._globalSetupReport = report;
const { status } = await this._testRunner.runGlobalSetup([reporter, new import_list.default()]);
return { report, status };
const { status, env } = await this._testRunner.runGlobalSetup([reporter, new import_list.default()]);
return { report, status, env };
}
async runGlobalTeardown() {
const { status } = await this._testRunner.runGlobalTeardown();

View File

@@ -267,7 +267,8 @@ async function runTests(watchOptions, testServerConnection, options) {
await testServerConnection.runTests({
grep: watchOptions.grep,
testIds: options?.testIds,
locations: watchOptions?.files,
locations: watchOptions?.files ?? [],
// TODO: always collect locations based on knowledge about tree, so that we don't have to load all tests
projects: watchOptions.projects,
connectWsEndpoint,
reuseContext: connectWsEndpoint ? true : void 0,

View File

@@ -61,6 +61,9 @@ class WorkerHost extends import_processHost.ProcessHost {
pauseAtEnd: options.pauseAtEnd
};
}
artifactsDir() {
return this._params.artifactsDir;
}
async start() {
await import_fs.default.promises.mkdir(this._params.artifactsDir, { recursive: true });
return await this.startRunner(this._params, {
@@ -82,6 +85,9 @@ class WorkerHost extends import_processHost.ProcessHost {
async sendCustomMessage(payload) {
return await this.sendMessage({ method: "customMessage", params: payload });
}
sendResume(payload) {
this.sendMessageNoReply({ method: "resume", params: payload });
}
hash() {
return this._hash;
}

Some files were not shown because too many files have changed in this diff Show More