playwright 1.57.0-alpha-2025-10-28 → 1.57.0-alpha-2025-10-30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agents/copilot-setup-steps.yml +34 -0
- package/lib/agents/generateAgents.js +47 -23
- package/lib/agents/{pwt-coverage.prompt.md → playwright-test-coverage.prompt.md} +3 -3
- package/lib/agents/{pwt-generate.prompt.md → playwright-test-generate.prompt.md} +1 -1
- package/lib/agents/{pwt-generator.agent.md → playwright-test-generator.agent.md} +1 -1
- package/lib/agents/{pwt-heal.prompt.md → playwright-test-heal.prompt.md} +1 -1
- package/lib/agents/{pwt-healer.agent.md → playwright-test-healer.agent.md} +1 -1
- package/lib/agents/{pwt-plan.prompt.md → playwright-test-plan.prompt.md} +1 -1
- package/lib/agents/{pwt-planner.agent.md → playwright-test-planner.agent.md} +1 -1
- package/lib/mcp/test/streams.js +5 -3
- package/lib/mcp/test/testContext.js +69 -29
- package/lib/mcp/test/testTools.js +20 -24
- package/lib/plugins/webServerPlugin.js +29 -5
- package/lib/reporters/html.js +2 -2
- package/lib/reporters/list.js +1 -5
- package/lib/reporters/merge.js +1 -1
- package/lib/runner/reporters.js +3 -4
- package/lib/runner/testRunner.js +2 -2
- package/lib/runner/testServer.js +3 -0
- package/package.json +2 -2
- package/types/test.d.ts +11 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: "Copilot Setup Steps"
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
paths:
|
|
7
|
+
- .github/workflows/copilot-setup-steps.yml
|
|
8
|
+
pull_request:
|
|
9
|
+
paths:
|
|
10
|
+
- .github/workflows/copilot-setup-steps.yml
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
copilot-setup-steps:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: lts/*
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: npm ci
|
|
28
|
+
|
|
29
|
+
- name: Install Playwright Browsers
|
|
30
|
+
run: npx playwright install --with-deps
|
|
31
|
+
|
|
32
|
+
# Customize this step as needed
|
|
33
|
+
- name: Build application
|
|
34
|
+
run: npx run build
|
|
@@ -100,9 +100,6 @@ class ClaudeGenerator {
|
|
|
100
100
|
await import_fs.default.promises.mkdir(".claude/agents", { recursive: true });
|
|
101
101
|
for (const agent of agents)
|
|
102
102
|
await writeFile(`.claude/agents/${agent.header.name}.md`, ClaudeGenerator.agentSpec(agent), "\u{1F916}", "agent definition");
|
|
103
|
-
await deleteFile(`.claude/agents/playwright-test-planner.md`, "legacy planner agent");
|
|
104
|
-
await deleteFile(`.claude/agents/playwright-test-generator.md`, "legacy generator agent");
|
|
105
|
-
await deleteFile(`.claude/agents/playwright-test-healer.md`, "legacy healer agent");
|
|
106
103
|
await writeFile(".mcp.json", JSON.stringify({
|
|
107
104
|
mcpServers: {
|
|
108
105
|
"playwright-test": {
|
|
@@ -115,7 +112,7 @@ class ClaudeGenerator {
|
|
|
115
112
|
}
|
|
116
113
|
static agentSpec(agent) {
|
|
117
114
|
const claudeToolMap = /* @__PURE__ */ new Map([
|
|
118
|
-
["search", ["Glob", "Grep", "Read"]],
|
|
115
|
+
["search", ["Glob", "Grep", "Read", "LS"]],
|
|
119
116
|
["edit", ["Edit", "MultiEdit", "Write"]]
|
|
120
117
|
]);
|
|
121
118
|
function asClaudeTool(tool) {
|
|
@@ -126,13 +123,15 @@ class ClaudeGenerator {
|
|
|
126
123
|
}
|
|
127
124
|
const examples = agent.examples.length ? ` Examples: ${agent.examples.map((example) => `<example>${example}</example>`).join("")}` : "";
|
|
128
125
|
const lines = [];
|
|
126
|
+
const header = {
|
|
127
|
+
name: agent.header.name,
|
|
128
|
+
description: agent.header.description + examples,
|
|
129
|
+
tools: agent.header.tools.map((tool) => asClaudeTool(tool)).join(", "),
|
|
130
|
+
model: agent.header.model,
|
|
131
|
+
color: agent.header.color
|
|
132
|
+
};
|
|
129
133
|
lines.push(`---`);
|
|
130
|
-
lines.push(
|
|
131
|
-
lines.push(`description: ${agent.header.description}.${examples}`);
|
|
132
|
-
lines.push(`tools: ${agent.header.tools.map((tool) => asClaudeTool(tool)).join(", ")}`);
|
|
133
|
-
lines.push(`model: ${agent.header.model}`);
|
|
134
|
-
lines.push(`color: ${agent.header.color}`);
|
|
135
|
-
lines.push(`---`);
|
|
134
|
+
lines.push(import_utilsBundle.yaml.stringify(header, { lineWidth: 1e5 }) + `---`);
|
|
136
135
|
lines.push("");
|
|
137
136
|
lines.push(agent.instructions);
|
|
138
137
|
return lines.join("\n");
|
|
@@ -151,9 +150,6 @@ class OpencodeGenerator {
|
|
|
151
150
|
prompt.push(...agent.examples.map((example) => `<example>${example}</example>`));
|
|
152
151
|
await writeFile(`.opencode/prompts/${agent.header.name}.md`, prompt.join("\n"), "\u{1F916}", "agent definition");
|
|
153
152
|
}
|
|
154
|
-
await deleteFile(`.opencode/prompts/playwright-test-planner.md`, "legacy planner agent");
|
|
155
|
-
await deleteFile(`.opencode/prompts/playwright-test-generator.md`, "legacy generator agent");
|
|
156
|
-
await deleteFile(`.opencode/prompts/playwright-test-healer.md`, "legacy healer agent");
|
|
157
153
|
await writeFile("opencode.json", OpencodeGenerator.configuration(agents), "\u{1F527}", "opencode configuration");
|
|
158
154
|
initRepoDone();
|
|
159
155
|
}
|
|
@@ -212,18 +208,46 @@ class CopilotGenerator {
|
|
|
212
208
|
await deleteFile(`.github/chatmodes/\u{1F3AD} generator.chatmode.md`, "legacy generator chatmode");
|
|
213
209
|
await deleteFile(`.github/chatmodes/\u{1F3AD} healer.chatmode.md`, "legacy healer chatmode");
|
|
214
210
|
await VSCodeGenerator.appendToMCPJson();
|
|
211
|
+
const cwdFolder = import_path.default.basename(process.cwd());
|
|
212
|
+
const mcpConfig = {
|
|
213
|
+
"mcpServers": {
|
|
214
|
+
"playwright-test": {
|
|
215
|
+
"type": "stdio",
|
|
216
|
+
"command": "npx",
|
|
217
|
+
"args": [
|
|
218
|
+
`--prefix=/home/runner/work/${cwdFolder}/${cwdFolder}`,
|
|
219
|
+
"playwright",
|
|
220
|
+
"run-test-mcp-server",
|
|
221
|
+
"--headless",
|
|
222
|
+
`--config=/home/runner/work/${cwdFolder}/${cwdFolder}`
|
|
223
|
+
],
|
|
224
|
+
"tools": ["*"]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
if (!import_fs.default.existsSync(".github/copilot-setup-steps.yml")) {
|
|
229
|
+
const yaml2 = import_fs.default.readFileSync(import_path.default.join(__dirname, "copilot-setup-steps.yml"), "utf-8");
|
|
230
|
+
await writeFile(".github/workflows/copilot-setup-steps.yml", yaml2, "\u{1F527}", "GitHub Copilot setup steps");
|
|
231
|
+
}
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log("");
|
|
234
|
+
console.log(" \u{1F527} TODO: GitHub > Settings > Copilot > Coding agent > MCP configuration");
|
|
235
|
+
console.log("------------------------------------------------------------------");
|
|
236
|
+
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
237
|
+
console.log("------------------------------------------------------------------");
|
|
215
238
|
initRepoDone();
|
|
216
239
|
}
|
|
217
240
|
static agentSpec(agent) {
|
|
218
241
|
const examples = agent.examples.length ? ` Examples: ${agent.examples.map((example) => `<example>${example}</example>`).join("")}` : "";
|
|
219
242
|
const lines = [];
|
|
243
|
+
const header = {
|
|
244
|
+
name: agent.header.name,
|
|
245
|
+
description: agent.header.description + examples,
|
|
246
|
+
tools: agent.header.tools,
|
|
247
|
+
model: "Claude Sonnet 4"
|
|
248
|
+
};
|
|
220
249
|
lines.push(`---`);
|
|
221
|
-
lines.push(
|
|
222
|
-
lines.push(`description: ${agent.header.description}.${examples}`);
|
|
223
|
-
lines.push(`tools:
|
|
224
|
-
${agent.header.tools.map((tool) => ` - ${tool}`).join("\n")}`);
|
|
225
|
-
lines.push(`model: Claude Sonnet 4`);
|
|
226
|
-
lines.push(`---`);
|
|
250
|
+
lines.push(import_utilsBundle.yaml.stringify(header) + `---`);
|
|
227
251
|
lines.push("");
|
|
228
252
|
lines.push(agent.instructions);
|
|
229
253
|
lines.push("");
|
|
@@ -307,7 +331,7 @@ class VSCodeGenerator {
|
|
|
307
331
|
}
|
|
308
332
|
}
|
|
309
333
|
async function writeFile(filePath, content, icon, description) {
|
|
310
|
-
console.log(
|
|
334
|
+
console.log(` ${icon} ${import_path.default.relative(process.cwd(), filePath)} ${import_utilsBundle.colors.dim("- " + description)}`);
|
|
311
335
|
await (0, import_utils.mkdirIfNeeded)(filePath);
|
|
312
336
|
await import_fs.default.promises.writeFile(filePath, content, "utf-8");
|
|
313
337
|
}
|
|
@@ -318,12 +342,12 @@ async function deleteFile(filePath, description) {
|
|
|
318
342
|
} catch {
|
|
319
343
|
return;
|
|
320
344
|
}
|
|
321
|
-
console.log(
|
|
345
|
+
console.log(` \u2702\uFE0F ${import_path.default.relative(process.cwd(), filePath)} ${import_utilsBundle.colors.dim("- " + description)}`);
|
|
322
346
|
await import_fs.default.promises.unlink(filePath);
|
|
323
347
|
}
|
|
324
348
|
async function initRepo(config, projectName, options) {
|
|
325
349
|
const project = (0, import_seed.seedProject)(config, projectName);
|
|
326
|
-
console.log(
|
|
350
|
+
console.log(` \u{1F3AD} Using project "${project.project.name}" as a primary project`);
|
|
327
351
|
if (!import_fs.default.existsSync("specs")) {
|
|
328
352
|
await import_fs.default.promises.mkdir("specs");
|
|
329
353
|
await writeFile(import_path.default.join("specs", "README.md"), `# Specs
|
|
@@ -353,7 +377,7 @@ This is a directory for test plans.
|
|
|
353
377
|
}
|
|
354
378
|
}
|
|
355
379
|
function initRepoDone() {
|
|
356
|
-
console.log("\u2705 Done.");
|
|
380
|
+
console.log(" \u2705 Done.");
|
|
357
381
|
}
|
|
358
382
|
async function loadPrompt(file, params) {
|
|
359
383
|
const content = await import_fs.default.promises.readFile(import_path.default.join(__dirname, file), "utf-8");
|
|
@@ -8,7 +8,7 @@ Parameters:
|
|
|
8
8
|
- Seed file (optional): the seed file to use, defaults to `${seedFile}`
|
|
9
9
|
- Test plan file (optional): the test plan file to write, under `specs/` folder.
|
|
10
10
|
|
|
11
|
-
1. Call #
|
|
11
|
+
1. Call #playwright-test-planner subagent with prompt:
|
|
12
12
|
|
|
13
13
|
<plan>
|
|
14
14
|
<task-text><!-- the task --></task-text>
|
|
@@ -16,7 +16,7 @@ Parameters:
|
|
|
16
16
|
<plan-file><!-- path to test plan file to generate --></plan-file>
|
|
17
17
|
</plan>
|
|
18
18
|
|
|
19
|
-
2. For each test case from the test plan file (1.1, 1.2, ...), one after another, not in parallel, call #
|
|
19
|
+
2. For each test case from the test plan file (1.1, 1.2, ...), one after another, not in parallel, call #playwright-test-generator subagent with prompt:
|
|
20
20
|
|
|
21
21
|
<generate>
|
|
22
22
|
<test-suite><!-- Verbatim name of the test spec group w/o ordinal like "Multiplication tests" --></test-suite>
|
|
@@ -26,6 +26,6 @@ Parameters:
|
|
|
26
26
|
<body><!-- Test case content including steps and expectations --></body>
|
|
27
27
|
</generate>
|
|
28
28
|
|
|
29
|
-
3. Call #
|
|
29
|
+
3. Call #playwright-test-healer subagent with prompt:
|
|
30
30
|
|
|
31
31
|
<heal>Run all tests and fix the failing ones one after another.</heal>
|
package/lib/mcp/test/streams.js
CHANGED
|
@@ -22,14 +22,16 @@ __export(streams_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(streams_exports);
|
|
24
24
|
var import_stream = require("stream");
|
|
25
|
+
var import_util = require("../../util");
|
|
25
26
|
class StringWriteStream extends import_stream.Writable {
|
|
26
|
-
constructor(progress) {
|
|
27
|
+
constructor(progress, stdio) {
|
|
27
28
|
super();
|
|
28
29
|
this._progress = progress;
|
|
30
|
+
this._prefix = stdio === "stdout" ? "" : "[err] ";
|
|
29
31
|
}
|
|
30
32
|
_write(chunk, encoding, callback) {
|
|
31
|
-
const text = chunk.toString();
|
|
32
|
-
this._progress({ message: text.endsWith("\n") ? text.slice(0, -1) : text });
|
|
33
|
+
const text = (0, import_util.stripAnsiEscapes)(chunk.toString());
|
|
34
|
+
this._progress({ message: `${this._prefix}${text.endsWith("\n") ? text.slice(0, -1) : text}` });
|
|
33
35
|
callback();
|
|
34
36
|
}
|
|
35
37
|
}
|
|
@@ -29,7 +29,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
var testContext_exports = {};
|
|
30
30
|
__export(testContext_exports, {
|
|
31
31
|
GeneratorJournal: () => GeneratorJournal,
|
|
32
|
-
TestContext: () => TestContext
|
|
32
|
+
TestContext: () => TestContext,
|
|
33
|
+
createScreen: () => createScreen
|
|
33
34
|
});
|
|
34
35
|
module.exports = __toCommonJS(testContext_exports);
|
|
35
36
|
var import_fs = __toESM(require("fs"));
|
|
@@ -124,39 +125,77 @@ class TestContext {
|
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
async runSeedTest(seedFile, projectName, progress) {
|
|
127
|
-
|
|
128
|
+
await this.runWithGlobalSetup(async (testRunner, reporter) => {
|
|
129
|
+
const result = await testRunner.runTests(reporter, {
|
|
130
|
+
headed: !this.options?.headless,
|
|
131
|
+
locations: ["/" + (0, import_utils.escapeRegExp)(seedFile) + "/"],
|
|
132
|
+
projects: [projectName],
|
|
133
|
+
timeout: 0,
|
|
134
|
+
workers: 1,
|
|
135
|
+
pauseAtEnd: true,
|
|
136
|
+
disableConfigReporters: true,
|
|
137
|
+
failOnLoadErrors: true
|
|
138
|
+
});
|
|
139
|
+
if (result.status === "passed" && !reporter.suite?.allTests().length)
|
|
140
|
+
throw new Error("seed test not found.");
|
|
141
|
+
if (result.status !== "passed")
|
|
142
|
+
throw new Error("Errors while running the seed test.");
|
|
143
|
+
}, progress);
|
|
144
|
+
}
|
|
145
|
+
async runWithGlobalSetup(callback, progress) {
|
|
146
|
+
const { screen, claimStdio, releaseStdio } = createScreen(progress);
|
|
128
147
|
const configDir = this.configLocation.configDir;
|
|
129
|
-
const reporter = new import_list.default({ configDir, screen });
|
|
130
148
|
const testRunner = await this.createTestRunner();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
...import_base.terminalScreen,
|
|
150
|
-
isTTY: false,
|
|
151
|
-
colors: import_utils.noColors,
|
|
152
|
-
stdout: stream,
|
|
153
|
-
stderr: stream
|
|
154
|
-
};
|
|
155
|
-
return { screen, stream };
|
|
149
|
+
claimStdio();
|
|
150
|
+
try {
|
|
151
|
+
const setupReporter = new import_list.default({ configDir, screen, includeTestId: true });
|
|
152
|
+
const { status } = await testRunner.runGlobalSetup([setupReporter]);
|
|
153
|
+
if (status !== "passed")
|
|
154
|
+
throw new Error("Failed to run global setup");
|
|
155
|
+
} finally {
|
|
156
|
+
releaseStdio();
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const reporter = new import_list.default({ configDir, screen, includeTestId: true });
|
|
160
|
+
return await callback(testRunner, reporter);
|
|
161
|
+
} finally {
|
|
162
|
+
claimStdio();
|
|
163
|
+
await testRunner.runGlobalTeardown().finally(() => {
|
|
164
|
+
releaseStdio();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
156
167
|
}
|
|
157
168
|
async close() {
|
|
158
169
|
}
|
|
159
170
|
}
|
|
171
|
+
function createScreen(progress) {
|
|
172
|
+
const stdout = new import_streams.StringWriteStream(progress, "stdout");
|
|
173
|
+
const stderr = new import_streams.StringWriteStream(progress, "stderr");
|
|
174
|
+
const screen = {
|
|
175
|
+
...import_base.terminalScreen,
|
|
176
|
+
isTTY: false,
|
|
177
|
+
colors: import_utils.noColors,
|
|
178
|
+
stdout,
|
|
179
|
+
stderr
|
|
180
|
+
};
|
|
181
|
+
const originalStdoutWrite = process.stdout.write;
|
|
182
|
+
const originalStderrWrite = process.stderr.write;
|
|
183
|
+
const claimStdio = () => {
|
|
184
|
+
process.stdout.write = (chunk) => {
|
|
185
|
+
stdout.write(chunk);
|
|
186
|
+
return true;
|
|
187
|
+
};
|
|
188
|
+
process.stderr.write = (chunk) => {
|
|
189
|
+
stderr.write(chunk);
|
|
190
|
+
return true;
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
const releaseStdio = () => {
|
|
194
|
+
process.stdout.write = originalStdoutWrite;
|
|
195
|
+
process.stderr.write = originalStderrWrite;
|
|
196
|
+
};
|
|
197
|
+
return { screen, claimStdio, releaseStdio };
|
|
198
|
+
}
|
|
160
199
|
const bestPracticesMarkdown = `
|
|
161
200
|
# Best practices
|
|
162
201
|
- Do not improvise, do not add directives that were not asked for
|
|
@@ -172,5 +211,6 @@ const bestPracticesMarkdown = `
|
|
|
172
211
|
// Annotate the CommonJS export names for ESM import in node:
|
|
173
212
|
0 && (module.exports = {
|
|
174
213
|
GeneratorJournal,
|
|
175
|
-
TestContext
|
|
214
|
+
TestContext,
|
|
215
|
+
createScreen
|
|
176
216
|
});
|
|
@@ -34,9 +34,9 @@ __export(testTools_exports, {
|
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(testTools_exports);
|
|
36
36
|
var import_bundle = require("../sdk/bundle");
|
|
37
|
-
var import_list = __toESM(require("../../reporters/list"));
|
|
38
37
|
var import_listModeReporter = __toESM(require("../../reporters/listModeReporter"));
|
|
39
38
|
var import_testTool = require("./testTool");
|
|
39
|
+
var import_testContext = require("./testContext");
|
|
40
40
|
const listTests = (0, import_testTool.defineTestTool)({
|
|
41
41
|
schema: {
|
|
42
42
|
name: "test_list",
|
|
@@ -46,7 +46,7 @@ const listTests = (0, import_testTool.defineTestTool)({
|
|
|
46
46
|
type: "readOnly"
|
|
47
47
|
},
|
|
48
48
|
handle: async (context, _, progress) => {
|
|
49
|
-
const { screen } =
|
|
49
|
+
const { screen } = (0, import_testContext.createScreen)(progress);
|
|
50
50
|
const reporter = new import_listModeReporter.default({ screen, includeTestId: true });
|
|
51
51
|
const testRunner = await context.createTestRunner();
|
|
52
52
|
await testRunner.listTests(reporter, {});
|
|
@@ -65,15 +65,13 @@ const runTests = (0, import_testTool.defineTestTool)({
|
|
|
65
65
|
type: "readOnly"
|
|
66
66
|
},
|
|
67
67
|
handle: async (context, params, progress) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
disableConfigReporters: true
|
|
76
|
-
});
|
|
68
|
+
await context.runWithGlobalSetup(async (testRunner, reporter) => {
|
|
69
|
+
await testRunner.runTests(reporter, {
|
|
70
|
+
locations: params.locations,
|
|
71
|
+
projects: params.projects,
|
|
72
|
+
disableConfigReporters: true
|
|
73
|
+
});
|
|
74
|
+
}, progress);
|
|
77
75
|
return { content: [] };
|
|
78
76
|
}
|
|
79
77
|
});
|
|
@@ -91,19 +89,17 @@ const debugTest = (0, import_testTool.defineTestTool)({
|
|
|
91
89
|
type: "readOnly"
|
|
92
90
|
},
|
|
93
91
|
handle: async (context, params, progress) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
disableConfigReporters: true
|
|
106
|
-
});
|
|
92
|
+
await context.runWithGlobalSetup(async (testRunner, reporter) => {
|
|
93
|
+
await testRunner.runTests(reporter, {
|
|
94
|
+
headed: !context.options?.headless,
|
|
95
|
+
testIds: [params.test.id],
|
|
96
|
+
// For automatic recovery
|
|
97
|
+
timeout: 0,
|
|
98
|
+
workers: 1,
|
|
99
|
+
pauseOnError: true,
|
|
100
|
+
disableConfigReporters: true
|
|
101
|
+
});
|
|
102
|
+
}, progress);
|
|
107
103
|
return { content: [] };
|
|
108
104
|
}
|
|
109
105
|
});
|
|
@@ -115,17 +115,38 @@ class WebServerPlugin {
|
|
|
115
115
|
});
|
|
116
116
|
this._killProcess = gracefullyClose;
|
|
117
117
|
debugWebServer(`Process started`);
|
|
118
|
+
if (this._options.wait?.stdout || this._options.wait?.stderr)
|
|
119
|
+
this._waitForStdioPromise = new import_utils.ManualPromise();
|
|
120
|
+
let stdoutWaitCollector = this._options.wait?.stdout ? "" : void 0;
|
|
121
|
+
let stderrWaitCollector = this._options.wait?.stderr ? "" : void 0;
|
|
122
|
+
const resolveStdioPromise = () => {
|
|
123
|
+
stderrWaitCollector = void 0;
|
|
124
|
+
stdoutWaitCollector = void 0;
|
|
125
|
+
this._waitForStdioPromise?.resolve();
|
|
126
|
+
};
|
|
118
127
|
launchedProcess.stderr.on("data", (data) => {
|
|
128
|
+
if (stderrWaitCollector !== void 0) {
|
|
129
|
+
stderrWaitCollector += data.toString();
|
|
130
|
+
if (this._options.wait?.stderr?.test(stderrWaitCollector))
|
|
131
|
+
resolveStdioPromise();
|
|
132
|
+
}
|
|
119
133
|
if (debugWebServer.enabled || (this._options.stderr === "pipe" || !this._options.stderr))
|
|
120
134
|
this._reporter.onStdErr?.(prefixOutputLines(data.toString(), this._options.name));
|
|
121
135
|
});
|
|
122
136
|
launchedProcess.stdout.on("data", (data) => {
|
|
137
|
+
if (stdoutWaitCollector !== void 0) {
|
|
138
|
+
stdoutWaitCollector += data.toString();
|
|
139
|
+
if (this._options.wait?.stdout?.test(stdoutWaitCollector))
|
|
140
|
+
resolveStdioPromise();
|
|
141
|
+
}
|
|
123
142
|
if (debugWebServer.enabled || this._options.stdout === "pipe")
|
|
124
143
|
this._reporter.onStdOut?.(prefixOutputLines(data.toString(), this._options.name));
|
|
125
144
|
});
|
|
126
145
|
}
|
|
127
146
|
async _waitForProcess() {
|
|
128
|
-
if (
|
|
147
|
+
if (this._options.wait?.time)
|
|
148
|
+
await new Promise((resolve) => setTimeout(resolve, this._options.wait.time));
|
|
149
|
+
if (!this._isAvailableCallback && !this._waitForStdioPromise) {
|
|
129
150
|
this._processExitedPromise.catch(() => {
|
|
130
151
|
});
|
|
131
152
|
return;
|
|
@@ -133,10 +154,13 @@ class WebServerPlugin {
|
|
|
133
154
|
debugWebServer(`Waiting for availability...`);
|
|
134
155
|
const launchTimeout = this._options.timeout || 60 * 1e3;
|
|
135
156
|
const cancellationToken = { canceled: false };
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
const deadline = (0, import_utils.monotonicTime)() + launchTimeout;
|
|
158
|
+
const racingPromises = [this._processExitedPromise];
|
|
159
|
+
if (this._isAvailableCallback)
|
|
160
|
+
racingPromises.push((0, import_utils.raceAgainstDeadline)(() => waitFor(this._isAvailableCallback, cancellationToken), deadline));
|
|
161
|
+
if (this._waitForStdioPromise)
|
|
162
|
+
racingPromises.push((0, import_utils.raceAgainstDeadline)(() => this._waitForStdioPromise, deadline));
|
|
163
|
+
const { timedOut } = await Promise.race(racingPromises);
|
|
140
164
|
cancellationToken.canceled = true;
|
|
141
165
|
if (timedOut)
|
|
142
166
|
throw new Error(`Timed out waiting ${launchTimeout}ms from config.webServer.`);
|
package/lib/reporters/html.js
CHANGED
|
@@ -130,10 +130,10 @@ class HtmlReporter {
|
|
|
130
130
|
if (process.env.CI || !this._buildResult)
|
|
131
131
|
return;
|
|
132
132
|
const { ok, singleTestId } = this._buildResult;
|
|
133
|
-
const shouldOpen =
|
|
133
|
+
const shouldOpen = !!process.stdin.isTTY && (this._open === "always" || !ok && this._open === "on-failure");
|
|
134
134
|
if (shouldOpen) {
|
|
135
135
|
await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId);
|
|
136
|
-
} else if (this._options._mode === "test" &&
|
|
136
|
+
} else if (this._options._mode === "test" && !!process.stdin.isTTY) {
|
|
137
137
|
const packageManagerCommand = (0, import_utils.getPackageManagerExecCommand)();
|
|
138
138
|
const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? "" : " " + import_path.default.relative(process.cwd(), this._outputFolder);
|
|
139
139
|
const hostArg = this._host ? ` --host ${this._host}` : "";
|
package/lib/reporters/list.js
CHANGED
|
@@ -39,7 +39,6 @@ class ListReporter extends import_base.TerminalReporter {
|
|
|
39
39
|
this._stepIndex = /* @__PURE__ */ new Map();
|
|
40
40
|
this._needNewLine = false;
|
|
41
41
|
this._printSteps = (0, import_utils.getAsBooleanFromENV)("PLAYWRIGHT_LIST_PRINT_STEPS", options?.printSteps);
|
|
42
|
-
this._prefixStdio = options?.prefixStdio;
|
|
43
42
|
}
|
|
44
43
|
onBegin(suite) {
|
|
45
44
|
super.onBegin(suite);
|
|
@@ -143,10 +142,7 @@ class ListReporter extends import_base.TerminalReporter {
|
|
|
143
142
|
return;
|
|
144
143
|
const text = chunk.toString("utf-8");
|
|
145
144
|
this._updateLineCountAndNewLineFlagForOutput(text);
|
|
146
|
-
|
|
147
|
-
stream.write(`[${stdio}] ${chunk}`);
|
|
148
|
-
else
|
|
149
|
-
stream.write(chunk);
|
|
145
|
+
stream.write(chunk);
|
|
150
146
|
}
|
|
151
147
|
onTestEnd(test, result) {
|
|
152
148
|
super.onTestEnd(test, result);
|
package/lib/reporters/merge.js
CHANGED
|
@@ -41,7 +41,7 @@ var import_teleReceiver = require("../isomorphic/teleReceiver");
|
|
|
41
41
|
var import_reporters = require("../runner/reporters");
|
|
42
42
|
var import_util = require("../util");
|
|
43
43
|
async function createMergedReport(config, dir, reporterDescriptions, rootDirOverride) {
|
|
44
|
-
const reporters = await (0, import_reporters.createReporters)(config, "merge",
|
|
44
|
+
const reporters = await (0, import_reporters.createReporters)(config, "merge", reporterDescriptions);
|
|
45
45
|
const multiplexer = new import_multiplexer.Multiplexer(reporters);
|
|
46
46
|
const stringPool = new import_stringInternPool.StringInternPool();
|
|
47
47
|
let printStatus = () => {
|
package/lib/runner/reporters.js
CHANGED
|
@@ -47,7 +47,7 @@ var import_line = __toESM(require("../reporters/line"));
|
|
|
47
47
|
var import_list = __toESM(require("../reporters/list"));
|
|
48
48
|
var import_listModeReporter = __toESM(require("../reporters/listModeReporter"));
|
|
49
49
|
var import_reporterV2 = require("../reporters/reporterV2");
|
|
50
|
-
async function createReporters(config, mode,
|
|
50
|
+
async function createReporters(config, mode, descriptions) {
|
|
51
51
|
const defaultReporters = {
|
|
52
52
|
blob: import_blob.BlobReporter,
|
|
53
53
|
dot: mode === "list" ? import_listModeReporter.default : import_dot.default,
|
|
@@ -63,7 +63,7 @@ async function createReporters(config, mode, isTestServer, descriptions) {
|
|
|
63
63
|
descriptions ??= config.config.reporter;
|
|
64
64
|
if (config.configCLIOverrides.additionalReporters)
|
|
65
65
|
descriptions = [...descriptions, ...config.configCLIOverrides.additionalReporters];
|
|
66
|
-
const runOptions = reporterOptions(config, mode
|
|
66
|
+
const runOptions = reporterOptions(config, mode);
|
|
67
67
|
for (const r of descriptions) {
|
|
68
68
|
const [name, arg] = r;
|
|
69
69
|
const options = { ...runOptions, ...arg };
|
|
@@ -104,11 +104,10 @@ function createErrorCollectingReporter(screen) {
|
|
|
104
104
|
errors: () => errors
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
-
function reporterOptions(config, mode
|
|
107
|
+
function reporterOptions(config, mode) {
|
|
108
108
|
return {
|
|
109
109
|
configDir: config.configDir,
|
|
110
110
|
_mode: mode,
|
|
111
|
-
_isTestServer: isTestServer,
|
|
112
111
|
_commandHash: computeCommandHash(config)
|
|
113
112
|
};
|
|
114
113
|
}
|
package/lib/runner/testRunner.js
CHANGED
|
@@ -269,7 +269,7 @@ class TestRunner extends import_events.default {
|
|
|
269
269
|
const testIdSet = new Set(params.testIds);
|
|
270
270
|
config.preOnlyTestFilters.push((test) => testIdSet.has(test.id));
|
|
271
271
|
}
|
|
272
|
-
const configReporters = params.disableConfigReporters ? [] : await (0, import_reporters.createReporters)(config, "test"
|
|
272
|
+
const configReporters = params.disableConfigReporters ? [] : await (0, import_reporters.createReporters)(config, "test");
|
|
273
273
|
const reporter = new import_internalReporter.InternalReporter([...configReporters, userReporter]);
|
|
274
274
|
const stop = new import_utils.ManualPromise();
|
|
275
275
|
const tasks = [
|
|
@@ -363,7 +363,7 @@ async function runAllTestsWithConfig(config) {
|
|
|
363
363
|
const listOnly = config.cliListOnly;
|
|
364
364
|
(0, import_gitCommitInfoPlugin.addGitCommitInfoPlugin)(config);
|
|
365
365
|
(0, import_webServerPlugin.webServerPluginsForConfig)(config).forEach((p) => config.plugins.push({ factory: p }));
|
|
366
|
-
const reporters = await (0, import_reporters.createReporters)(config, listOnly ? "list" : "test"
|
|
366
|
+
const reporters = await (0, import_reporters.createReporters)(config, listOnly ? "list" : "test");
|
|
367
367
|
const lastRun = new import_lastRun.LastRunReporter(config);
|
|
368
368
|
if (config.cliLastFailed)
|
|
369
369
|
await lastRun.filterLastFailed();
|
package/lib/runner/testServer.js
CHANGED
|
@@ -45,6 +45,7 @@ var import_testRunner = require("./testRunner");
|
|
|
45
45
|
const originalDebugLog = import_utilsBundle.debug.log;
|
|
46
46
|
const originalStdoutWrite = process.stdout.write;
|
|
47
47
|
const originalStderrWrite = process.stderr.write;
|
|
48
|
+
const originalStdinIsTTY = process.stdin.isTTY;
|
|
48
49
|
class TestServer {
|
|
49
50
|
constructor(configLocation, configCLIOverrides) {
|
|
50
51
|
this._configLocation = configLocation;
|
|
@@ -191,10 +192,12 @@ class TestServerDispatcher {
|
|
|
191
192
|
};
|
|
192
193
|
process.stdout.write = stdoutWrite;
|
|
193
194
|
process.stderr.write = stderrWrite;
|
|
195
|
+
process.stdin.isTTY = void 0;
|
|
194
196
|
} else {
|
|
195
197
|
import_utilsBundle.debug.log = originalDebugLog;
|
|
196
198
|
process.stdout.write = originalStdoutWrite;
|
|
197
199
|
process.stderr.write = originalStderrWrite;
|
|
200
|
+
process.stdin.isTTY = originalStdinIsTTY;
|
|
198
201
|
}
|
|
199
202
|
}
|
|
200
203
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright",
|
|
3
|
-
"version": "1.57.0-alpha-2025-10-
|
|
3
|
+
"version": "1.57.0-alpha-2025-10-30",
|
|
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.57.0-alpha-2025-10-
|
|
67
|
+
"playwright-core": "1.57.0-alpha-2025-10-30"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|
package/types/test.d.ts
CHANGED
|
@@ -10170,6 +10170,17 @@ interface TestConfigWebServer {
|
|
|
10170
10170
|
*/
|
|
10171
10171
|
stdout?: "pipe"|"ignore";
|
|
10172
10172
|
|
|
10173
|
+
/**
|
|
10174
|
+
* Consider command started only when given output has been produced or a time in milliseconds has passed.
|
|
10175
|
+
*/
|
|
10176
|
+
wait?: {
|
|
10177
|
+
stdout?: RegExp;
|
|
10178
|
+
|
|
10179
|
+
stderr?: RegExp;
|
|
10180
|
+
|
|
10181
|
+
time?: number;
|
|
10182
|
+
};
|
|
10183
|
+
|
|
10173
10184
|
/**
|
|
10174
10185
|
* How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
|
|
10175
10186
|
*/
|