playwright 1.56.0-alpha-1758292576000 → 1.56.0-alpha-2025-09-21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/isomorphic/testTree.js +35 -8
- package/lib/mcp/browser/browserContextFactory.js +2 -2
- package/lib/mcp/browser/config.js +11 -4
- package/lib/mcp/browser/context.js +2 -2
- package/lib/mcp/browser/sessionLog.js +1 -1
- package/lib/mcp/browser/tab.js +1 -1
- package/lib/mcp/browser/tools/pdf.js +2 -1
- package/lib/mcp/browser/tools/screenshot.js +1 -1
- package/lib/mcp/browser/tools/tracing.js +1 -1
- package/lib/mcp/browser/tools/utils.js +6 -0
- package/lib/mcp/test/testTools.js +2 -3
- package/package.json +2 -2
|
@@ -25,7 +25,7 @@ __export(testTree_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(testTree_exports);
|
|
27
27
|
class TestTree {
|
|
28
|
-
constructor(rootFolder, rootSuite, loadErrors, projectFilters, pathSeparator) {
|
|
28
|
+
constructor(rootFolder, rootSuite, loadErrors, projectFilters, pathSeparator, hideFiles) {
|
|
29
29
|
this._treeItemById = /* @__PURE__ */ new Map();
|
|
30
30
|
this._treeItemByTestId = /* @__PURE__ */ new Map();
|
|
31
31
|
const filterProjects = projectFilters && [...projectFilters.values()].some(Boolean);
|
|
@@ -43,10 +43,10 @@ class TestTree {
|
|
|
43
43
|
hasLoadErrors: false
|
|
44
44
|
};
|
|
45
45
|
this._treeItemById.set(rootFolder, this.rootItem);
|
|
46
|
-
const visitSuite = (project, parentSuite, parentGroup) => {
|
|
47
|
-
for (const suite of parentSuite.suites) {
|
|
46
|
+
const visitSuite = (project, parentSuite, parentGroup, mode) => {
|
|
47
|
+
for (const suite of mode === "tests" ? [] : parentSuite.suites) {
|
|
48
48
|
if (!suite.title) {
|
|
49
|
-
visitSuite(project, suite, parentGroup);
|
|
49
|
+
visitSuite(project, suite, parentGroup, "all");
|
|
50
50
|
continue;
|
|
51
51
|
}
|
|
52
52
|
let group = parentGroup.children.find((item) => item.kind === "group" && item.title === suite.title);
|
|
@@ -66,9 +66,9 @@ class TestTree {
|
|
|
66
66
|
};
|
|
67
67
|
this._addChild(parentGroup, group);
|
|
68
68
|
}
|
|
69
|
-
visitSuite(project, suite, group);
|
|
69
|
+
visitSuite(project, suite, group, "all");
|
|
70
70
|
}
|
|
71
|
-
for (const test of parentSuite.tests) {
|
|
71
|
+
for (const test of mode === "suites" ? [] : parentSuite.tests) {
|
|
72
72
|
const title = test.title;
|
|
73
73
|
let testCaseItem = parentGroup.children.find((t) => t.kind !== "group" && t.title === title);
|
|
74
74
|
if (!testCaseItem) {
|
|
@@ -124,8 +124,16 @@ class TestTree {
|
|
|
124
124
|
if (filterProjects && !projectFilters.get(projectSuite.title))
|
|
125
125
|
continue;
|
|
126
126
|
for (const fileSuite of projectSuite.suites) {
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
if (hideFiles) {
|
|
128
|
+
visitSuite(projectSuite.project(), fileSuite, this.rootItem, "suites");
|
|
129
|
+
if (fileSuite.tests.length) {
|
|
130
|
+
const defaultDescribeItem = this._defaultDescribeItem();
|
|
131
|
+
visitSuite(projectSuite.project(), fileSuite, defaultDescribeItem, "tests");
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
const fileItem = this._fileItem(fileSuite.location.file.split(pathSeparator), true);
|
|
135
|
+
visitSuite(projectSuite.project(), fileSuite, fileItem, "all");
|
|
136
|
+
}
|
|
129
137
|
}
|
|
130
138
|
}
|
|
131
139
|
for (const loadError of loadErrors) {
|
|
@@ -192,6 +200,25 @@ class TestTree {
|
|
|
192
200
|
this._addChild(parentFileItem, fileItem);
|
|
193
201
|
return fileItem;
|
|
194
202
|
}
|
|
203
|
+
_defaultDescribeItem() {
|
|
204
|
+
let defaultDescribeItem = this._treeItemById.get("<anonymous>");
|
|
205
|
+
if (!defaultDescribeItem) {
|
|
206
|
+
defaultDescribeItem = {
|
|
207
|
+
kind: "group",
|
|
208
|
+
subKind: "describe",
|
|
209
|
+
id: "<anonymous>",
|
|
210
|
+
title: "<anonymous>",
|
|
211
|
+
location: { file: "", line: 0, column: 0 },
|
|
212
|
+
duration: 0,
|
|
213
|
+
parent: this.rootItem,
|
|
214
|
+
children: [],
|
|
215
|
+
status: "none",
|
|
216
|
+
hasLoadErrors: false
|
|
217
|
+
};
|
|
218
|
+
this._addChild(this.rootItem, defaultDescribeItem);
|
|
219
|
+
}
|
|
220
|
+
return defaultDescribeItem;
|
|
221
|
+
}
|
|
195
222
|
sortAndPropagateStatus() {
|
|
196
223
|
sortAndPropagateStatus(this.rootItem);
|
|
197
224
|
}
|
|
@@ -102,7 +102,7 @@ class IsolatedContextFactory extends BaseContextFactory {
|
|
|
102
102
|
async _doObtainBrowser(clientInfo) {
|
|
103
103
|
await injectCdpPort(this.config.browser);
|
|
104
104
|
const browserType = playwright[this.config.browser.browserName];
|
|
105
|
-
const tracesDir = await (0, import_config.outputFile)(this.config, clientInfo, `traces
|
|
105
|
+
const tracesDir = await (0, import_config.outputFile)(this.config, clientInfo, `traces`, { origin: "code" });
|
|
106
106
|
if (this.config.saveTrace)
|
|
107
107
|
await startTraceServer(this.config, tracesDir);
|
|
108
108
|
return browserType.launch({
|
|
@@ -157,7 +157,7 @@ class PersistentContextFactory {
|
|
|
157
157
|
await injectCdpPort(this.config.browser);
|
|
158
158
|
(0, import_log.testDebug)("create browser context (persistent)");
|
|
159
159
|
const userDataDir = this.config.browser.userDataDir ?? await this._createUserDataDir(clientInfo);
|
|
160
|
-
const tracesDir = await (0, import_config.outputFile)(this.config, clientInfo, `traces
|
|
160
|
+
const tracesDir = await (0, import_config.outputFile)(this.config, clientInfo, `traces`, { origin: "code" });
|
|
161
161
|
if (this.config.saveTrace)
|
|
162
162
|
await startTraceServer(this.config, tracesDir);
|
|
163
163
|
this._userDataDirs.add(userDataDir);
|
|
@@ -217,12 +217,19 @@ async function loadConfig(configFile) {
|
|
|
217
217
|
throw new Error(`Failed to load config file: ${configFile}, ${error}`);
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
-
async function outputFile(config, clientInfo,
|
|
220
|
+
async function outputFile(config, clientInfo, fileName, options) {
|
|
221
221
|
const rootPath = (0, import_server.firstRootPath)(clientInfo);
|
|
222
222
|
const outputDir = config.outputDir ?? (rootPath ? import_path.default.join(rootPath, ".playwright-mcp") : void 0) ?? import_path.default.join(process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir(), "playwright-mcp-output", String(clientInfo.timestamp));
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
if (options.origin === "code")
|
|
224
|
+
return import_path.default.resolve(outputDir, fileName);
|
|
225
|
+
if (options.origin === "llm") {
|
|
226
|
+
fileName = fileName.split("\\").join("/");
|
|
227
|
+
const resolvedFile = import_path.default.resolve(outputDir, fileName);
|
|
228
|
+
if (!resolvedFile.startsWith(import_path.default.resolve(outputDir) + import_path.default.sep))
|
|
229
|
+
throw new Error(`Resolved file path for ${fileName} is outside of the output directory`);
|
|
230
|
+
return resolvedFile;
|
|
231
|
+
}
|
|
232
|
+
return import_path.default.join(outputDir, sanitizeForFilePath(fileName));
|
|
226
233
|
}
|
|
227
234
|
function pickDefined(obj) {
|
|
228
235
|
return Object.fromEntries(
|
|
@@ -95,8 +95,8 @@ class Context {
|
|
|
95
95
|
await tab.page.close();
|
|
96
96
|
return url;
|
|
97
97
|
}
|
|
98
|
-
async outputFile(
|
|
99
|
-
return (0, import_config.outputFile)(this.config, this._clientInfo,
|
|
98
|
+
async outputFile(fileName, options) {
|
|
99
|
+
return (0, import_config.outputFile)(this.config, this._clientInfo, fileName, options);
|
|
100
100
|
}
|
|
101
101
|
_onPageCreated(page) {
|
|
102
102
|
const tab = new import_tab.Tab(this, page, (tab2) => this._onPageClosed(tab2));
|
|
@@ -44,7 +44,7 @@ class SessionLog {
|
|
|
44
44
|
this._file = import_path.default.join(this._folder, "session.md");
|
|
45
45
|
}
|
|
46
46
|
static async create(config, clientInfo) {
|
|
47
|
-
const sessionFolder = await (0, import_config.outputFile)(config, clientInfo, `session-${Date.now()}
|
|
47
|
+
const sessionFolder = await (0, import_config.outputFile)(config, clientInfo, `session-${Date.now()}`, { origin: "code" });
|
|
48
48
|
await import_fs.default.promises.mkdir(sessionFolder, { recursive: true });
|
|
49
49
|
console.error(`Session: ${sessionFolder}`);
|
|
50
50
|
return new SessionLog(sessionFolder);
|
package/lib/mcp/browser/tab.js
CHANGED
|
@@ -91,7 +91,7 @@ class Tab extends import_events.EventEmitter {
|
|
|
91
91
|
const entry = {
|
|
92
92
|
download,
|
|
93
93
|
finished: false,
|
|
94
|
-
outputFile: await this.context.outputFile(download.suggestedFilename())
|
|
94
|
+
outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: "web" })
|
|
95
95
|
};
|
|
96
96
|
this._downloads.push(entry);
|
|
97
97
|
await download.saveAs(entry.outputFile);
|
|
@@ -34,6 +34,7 @@ module.exports = __toCommonJS(pdf_exports);
|
|
|
34
34
|
var import_bundle = require("../../sdk/bundle");
|
|
35
35
|
var import_tool = require("./tool");
|
|
36
36
|
var javascript = __toESM(require("../codegen"));
|
|
37
|
+
var import_utils = require("./utils");
|
|
37
38
|
const pdfSchema = import_bundle.z.object({
|
|
38
39
|
filename: import_bundle.z.string().optional().describe("File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.")
|
|
39
40
|
});
|
|
@@ -47,7 +48,7 @@ const pdf = (0, import_tool.defineTabTool)({
|
|
|
47
48
|
type: "readOnly"
|
|
48
49
|
},
|
|
49
50
|
handle: async (tab, params, response) => {
|
|
50
|
-
const fileName = await tab.context.outputFile(params.filename ?? `page-${(
|
|
51
|
+
const fileName = await tab.context.outputFile(params.filename ?? `page-${(0, import_utils.dateAsFileName)()}.pdf`, { origin: "llm" });
|
|
51
52
|
response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
|
|
52
53
|
response.addResult(`Saved page as ${fileName}`);
|
|
53
54
|
await tab.page.pdf({ path: fileName });
|
|
@@ -63,7 +63,7 @@ const screenshot = (0, import_tool.defineTabTool)({
|
|
|
63
63
|
},
|
|
64
64
|
handle: async (tab, params, response) => {
|
|
65
65
|
const fileType = params.type || "png";
|
|
66
|
-
const fileName = await tab.context.outputFile(params.filename ?? `page-${(
|
|
66
|
+
const fileName = await tab.context.outputFile(params.filename ?? `page-${(0, import_utils.dateAsFileName)()}.${fileType}`, { origin: "llm" });
|
|
67
67
|
const options = {
|
|
68
68
|
type: fileType,
|
|
69
69
|
quality: fileType === "png" ? void 0 : 90,
|
|
@@ -34,7 +34,7 @@ const tracingStart = (0, import_tool.defineTool)({
|
|
|
34
34
|
},
|
|
35
35
|
handle: async (context, params, response) => {
|
|
36
36
|
const browserContext = await context.ensureBrowserContext();
|
|
37
|
-
const tracesDir = await context.outputFile(`traces
|
|
37
|
+
const tracesDir = await context.outputFile(`traces`, { origin: "code" });
|
|
38
38
|
const name = "trace-" + Date.now();
|
|
39
39
|
await browserContext.tracing.start({
|
|
40
40
|
name,
|
|
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var utils_exports = {};
|
|
20
20
|
__export(utils_exports, {
|
|
21
21
|
callOnPageNoTrace: () => callOnPageNoTrace,
|
|
22
|
+
dateAsFileName: () => dateAsFileName,
|
|
22
23
|
generateLocator: () => generateLocator,
|
|
23
24
|
waitForCompletion: () => waitForCompletion
|
|
24
25
|
});
|
|
@@ -82,9 +83,14 @@ async function generateLocator(locator) {
|
|
|
82
83
|
async function callOnPageNoTrace(page, callback) {
|
|
83
84
|
return await page._wrapApiCall(() => callback(page), { internal: true });
|
|
84
85
|
}
|
|
86
|
+
function dateAsFileName() {
|
|
87
|
+
const date = /* @__PURE__ */ new Date();
|
|
88
|
+
return date.toISOString().replace(/[:.]/g, "-");
|
|
89
|
+
}
|
|
85
90
|
// Annotate the CommonJS export names for ESM import in node:
|
|
86
91
|
0 && (module.exports = {
|
|
87
92
|
callOnPageNoTrace,
|
|
93
|
+
dateAsFileName,
|
|
88
94
|
generateLocator,
|
|
89
95
|
waitForCompletion
|
|
90
96
|
});
|
|
@@ -78,7 +78,7 @@ const runTests = (0, import_testTool.defineTestTool)({
|
|
|
78
78
|
const configDir = context.configLocation.configDir;
|
|
79
79
|
const reporter = new import_list.default({ configDir, screen, includeTestId: true });
|
|
80
80
|
const testRunner = await context.createTestRunner();
|
|
81
|
-
|
|
81
|
+
await testRunner.runTests(reporter, {
|
|
82
82
|
locations: params.locations,
|
|
83
83
|
projects: params.projects,
|
|
84
84
|
disableConfigReporters: true
|
|
@@ -87,8 +87,7 @@ const runTests = (0, import_testTool.defineTestTool)({
|
|
|
87
87
|
return {
|
|
88
88
|
content: [
|
|
89
89
|
{ type: "text", text }
|
|
90
|
-
]
|
|
91
|
-
isError: result.status !== "passed"
|
|
90
|
+
]
|
|
92
91
|
};
|
|
93
92
|
}
|
|
94
93
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright",
|
|
3
|
-
"version": "1.56.0-alpha-
|
|
3
|
+
"version": "1.56.0-alpha-2025-09-21",
|
|
4
4
|
"description": "A high-level API to automate web browsers",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"license": "Apache-2.0",
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"playwright-core": "1.56.0-alpha-
|
|
67
|
+
"playwright-core": "1.56.0-alpha-2025-09-21"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|