playwright 1.56.1 → 1.57.0
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/README.md +3 -3
- package/ThirdPartyNotices.txt +202 -282
- package/lib/agents/copilot-setup-steps.yml +34 -0
- package/lib/agents/generateAgents.js +292 -160
- package/lib/agents/playwright-test-coverage.prompt.md +31 -0
- package/lib/agents/playwright-test-generate.prompt.md +8 -0
- package/lib/agents/{generator.md → playwright-test-generator.agent.md} +8 -22
- package/lib/agents/playwright-test-heal.prompt.md +6 -0
- package/lib/agents/{healer.md → playwright-test-healer.agent.md} +5 -28
- package/lib/agents/playwright-test-plan.prompt.md +9 -0
- package/lib/agents/{planner.md → playwright-test-planner.agent.md} +5 -68
- package/lib/common/config.js +6 -0
- package/lib/common/expectBundle.js +0 -9
- package/lib/common/expectBundleImpl.js +267 -249
- package/lib/common/testLoader.js +3 -2
- package/lib/common/testType.js +3 -12
- package/lib/common/validators.js +68 -0
- package/lib/index.js +9 -9
- package/lib/isomorphic/teleReceiver.js +1 -0
- package/lib/isomorphic/testServerConnection.js +14 -0
- package/lib/loader/loaderMain.js +1 -1
- package/lib/matchers/expect.js +12 -13
- package/lib/matchers/matchers.js +16 -0
- package/lib/mcp/browser/browserServerBackend.js +1 -1
- package/lib/mcp/browser/config.js +9 -24
- package/lib/mcp/browser/context.js +4 -21
- package/lib/mcp/browser/response.js +20 -11
- package/lib/mcp/browser/tab.js +25 -10
- package/lib/mcp/browser/tools/evaluate.js +2 -3
- package/lib/mcp/browser/tools/form.js +2 -3
- package/lib/mcp/browser/tools/keyboard.js +4 -5
- package/lib/mcp/browser/tools/pdf.js +1 -1
- package/lib/mcp/browser/tools/runCode.js +75 -0
- package/lib/mcp/browser/tools/screenshot.js +33 -15
- package/lib/mcp/browser/tools/snapshot.js +13 -14
- package/lib/mcp/browser/tools/tabs.js +2 -2
- package/lib/mcp/browser/tools/utils.js +0 -11
- package/lib/mcp/browser/tools/verify.js +3 -4
- package/lib/mcp/browser/tools.js +2 -0
- package/lib/mcp/program.js +21 -1
- package/lib/mcp/sdk/exports.js +1 -3
- package/lib/mcp/sdk/http.js +9 -2
- package/lib/mcp/sdk/proxyBackend.js +1 -1
- package/lib/mcp/sdk/server.js +13 -5
- package/lib/mcp/sdk/tool.js +2 -6
- package/lib/mcp/test/browserBackend.js +43 -33
- package/lib/mcp/test/generatorTools.js +3 -3
- package/lib/mcp/test/plannerTools.js +103 -5
- package/lib/mcp/test/seed.js +25 -15
- package/lib/mcp/test/streams.js +9 -4
- package/lib/mcp/test/testBackend.js +31 -29
- package/lib/mcp/test/testContext.js +143 -40
- package/lib/mcp/test/testTools.js +12 -21
- package/lib/plugins/webServerPlugin.js +37 -9
- package/lib/program.js +11 -20
- package/lib/reporters/html.js +2 -23
- package/lib/reporters/internalReporter.js +4 -2
- package/lib/reporters/junit.js +4 -2
- package/lib/reporters/list.js +1 -5
- package/lib/reporters/merge.js +12 -6
- package/lib/reporters/teleEmitter.js +3 -1
- package/lib/runner/dispatcher.js +26 -2
- package/lib/runner/failureTracker.js +5 -5
- package/lib/runner/loadUtils.js +2 -1
- package/lib/runner/loaderHost.js +1 -1
- package/lib/runner/reporters.js +5 -4
- package/lib/runner/testRunner.js +8 -9
- package/lib/runner/testServer.js +8 -3
- package/lib/runner/workerHost.js +3 -0
- package/lib/worker/testInfo.js +28 -17
- package/lib/worker/testTracing.js +1 -0
- package/lib/worker/workerMain.js +15 -6
- package/package.json +2 -2
- package/types/test.d.ts +96 -3
- package/types/testReporter.d.ts +5 -0
- package/lib/mcp/sdk/mdb.js +0 -208
package/lib/runner/dispatcher.js
CHANGED
|
@@ -26,6 +26,8 @@ var import_utils2 = require("playwright-core/lib/utils");
|
|
|
26
26
|
var import_rebase = require("./rebase");
|
|
27
27
|
var import_workerHost = require("./workerHost");
|
|
28
28
|
var import_ipc = require("../common/ipc");
|
|
29
|
+
var import_internalReporter = require("../reporters/internalReporter");
|
|
30
|
+
var import_util = require("../util");
|
|
29
31
|
class Dispatcher {
|
|
30
32
|
constructor(config, reporter, failureTracker) {
|
|
31
33
|
this._workerSlots = [];
|
|
@@ -70,7 +72,7 @@ class Dispatcher {
|
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
this._queue.splice(jobIndex, 1);
|
|
73
|
-
const jobDispatcher = new JobDispatcher(job, this._reporter, this._failureTracker, () => this.stop().catch(() => {
|
|
75
|
+
const jobDispatcher = new JobDispatcher(job, this._config, this._reporter, this._failureTracker, () => this.stop().catch(() => {
|
|
74
76
|
}));
|
|
75
77
|
this._workerSlots[workerIndex].busy = true;
|
|
76
78
|
this._workerSlots[workerIndex].jobDispatcher = jobDispatcher;
|
|
@@ -209,7 +211,7 @@ class Dispatcher {
|
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
213
|
class JobDispatcher {
|
|
212
|
-
constructor(job, reporter, failureTracker, stopCallback) {
|
|
214
|
+
constructor(job, config, reporter, failureTracker, stopCallback) {
|
|
213
215
|
this.jobResult = new import_utils.ManualPromise();
|
|
214
216
|
this._listeners = [];
|
|
215
217
|
this._failedTests = /* @__PURE__ */ new Set();
|
|
@@ -219,6 +221,7 @@ class JobDispatcher {
|
|
|
219
221
|
this._parallelIndex = 0;
|
|
220
222
|
this._workerIndex = 0;
|
|
221
223
|
this.job = job;
|
|
224
|
+
this._config = config;
|
|
222
225
|
this._reporter = reporter;
|
|
223
226
|
this._failureTracker = failureTracker;
|
|
224
227
|
this._stopCallback = stopCallback;
|
|
@@ -449,10 +452,30 @@ class JobDispatcher {
|
|
|
449
452
|
import_utils.eventsHelper.addEventListener(worker, "stepBegin", this._onStepBegin.bind(this)),
|
|
450
453
|
import_utils.eventsHelper.addEventListener(worker, "stepEnd", this._onStepEnd.bind(this)),
|
|
451
454
|
import_utils.eventsHelper.addEventListener(worker, "attach", this._onAttach.bind(this)),
|
|
455
|
+
import_utils.eventsHelper.addEventListener(worker, "testPaused", this._onTestPaused.bind(this, worker)),
|
|
452
456
|
import_utils.eventsHelper.addEventListener(worker, "done", this._onDone.bind(this)),
|
|
453
457
|
import_utils.eventsHelper.addEventListener(worker, "exit", this.onExit.bind(this))
|
|
454
458
|
];
|
|
455
459
|
}
|
|
460
|
+
_onTestPaused(worker, params) {
|
|
461
|
+
const sendMessage = async (message) => {
|
|
462
|
+
try {
|
|
463
|
+
if (this.jobResult.isDone())
|
|
464
|
+
throw new Error("Test has already stopped");
|
|
465
|
+
const response = await worker.sendCustomMessage({ testId: params.testId, request: message.request });
|
|
466
|
+
if (response.error)
|
|
467
|
+
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, response.error);
|
|
468
|
+
return response;
|
|
469
|
+
} catch (e) {
|
|
470
|
+
const error = (0, import_util.serializeError)(e);
|
|
471
|
+
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, error);
|
|
472
|
+
return { response: void 0, error };
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
for (const error of params.errors)
|
|
476
|
+
(0, import_internalReporter.addLocationAndSnippetToError)(this._config.config, error);
|
|
477
|
+
this._failureTracker.onTestPaused?.({ ...params, sendMessage });
|
|
478
|
+
}
|
|
456
479
|
skipWholeJob() {
|
|
457
480
|
const allTestsSkipped = this.job.tests.every((test) => test.expectedStatus === "skipped");
|
|
458
481
|
if (allTestsSkipped && !this._failureTracker.hasReachedMaxFailures()) {
|
|
@@ -460,6 +483,7 @@ class JobDispatcher {
|
|
|
460
483
|
const result = test._appendTestResult();
|
|
461
484
|
this._reporter.onTestBegin?.(test, result);
|
|
462
485
|
result.status = "skipped";
|
|
486
|
+
result.annotations = [...test.annotations];
|
|
463
487
|
this._reportTestEnd(test, result);
|
|
464
488
|
}
|
|
465
489
|
return true;
|
|
@@ -22,13 +22,13 @@ __export(failureTracker_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(failureTracker_exports);
|
|
24
24
|
class FailureTracker {
|
|
25
|
-
constructor(
|
|
26
|
-
this._config = _config;
|
|
25
|
+
constructor(config, options) {
|
|
27
26
|
this._failureCount = 0;
|
|
28
27
|
this._hasWorkerErrors = false;
|
|
29
28
|
this._topLevelProjects = [];
|
|
30
|
-
this.
|
|
31
|
-
this.
|
|
29
|
+
this._config = config;
|
|
30
|
+
this._pauseOnError = !!options?.pauseOnError;
|
|
31
|
+
this._pauseAtEnd = !!options?.pauseAtEnd;
|
|
32
32
|
}
|
|
33
33
|
onRootSuite(rootSuite, topLevelProjects) {
|
|
34
34
|
this._rootSuite = rootSuite;
|
|
@@ -45,7 +45,7 @@ class FailureTracker {
|
|
|
45
45
|
return this._pauseOnError;
|
|
46
46
|
}
|
|
47
47
|
pauseAtEnd(inProject) {
|
|
48
|
-
return this.
|
|
48
|
+
return this._topLevelProjects.includes(inProject) && this._pauseAtEnd;
|
|
49
49
|
}
|
|
50
50
|
hasReachedMaxFailures() {
|
|
51
51
|
return this.maxFailures() > 0 && this._failureCount >= this.maxFailures();
|
package/lib/runner/loadUtils.js
CHANGED
|
@@ -159,7 +159,8 @@ async function createRootSuite(testRun, errors, shouldFilterOnly) {
|
|
|
159
159
|
if (config.config.shard) {
|
|
160
160
|
const testGroups = [];
|
|
161
161
|
for (const projectSuite of rootSuite.suites) {
|
|
162
|
-
|
|
162
|
+
for (const group of (0, import_testGroups.createTestGroups)(projectSuite, config.config.shard.total))
|
|
163
|
+
testGroups.push(group);
|
|
163
164
|
}
|
|
164
165
|
const testGroupsInThisShard = (0, import_testGroups.filterForShard)(config.config.shard, testGroups);
|
|
165
166
|
const testsInThisShard = /* @__PURE__ */ new Set();
|
package/lib/runner/loaderHost.js
CHANGED
|
@@ -48,7 +48,7 @@ class InProcessLoaderHost {
|
|
|
48
48
|
return true;
|
|
49
49
|
}
|
|
50
50
|
async loadTestFile(file, testErrors) {
|
|
51
|
-
const result = await (0, import_testLoader.loadTestFile)(file, this._config
|
|
51
|
+
const result = await (0, import_testLoader.loadTestFile)(file, this._config, testErrors);
|
|
52
52
|
this._poolBuilder.buildPools(result, testErrors);
|
|
53
53
|
return result;
|
|
54
54
|
}
|
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
|
}
|
|
@@ -125,6 +124,8 @@ function computeCommandHash(config) {
|
|
|
125
124
|
command.cliGrepInvert = config.cliGrepInvert;
|
|
126
125
|
if (config.cliOnlyChanged)
|
|
127
126
|
command.cliOnlyChanged = config.cliOnlyChanged;
|
|
127
|
+
if (config.config.tags.length)
|
|
128
|
+
command.tags = config.config.tags.join(" ");
|
|
128
129
|
if (Object.keys(command).length)
|
|
129
130
|
parts.push((0, import_utils.calculateSha1)(JSON.stringify(command)).substring(0, 7));
|
|
130
131
|
return parts.join("-");
|
package/lib/runner/testRunner.js
CHANGED
|
@@ -51,7 +51,8 @@ var import_reporters = require("./reporters");
|
|
|
51
51
|
var import_tasks = require("./tasks");
|
|
52
52
|
var import_lastRun = require("./lastRun");
|
|
53
53
|
const TestRunnerEvent = {
|
|
54
|
-
TestFilesChanged: "testFilesChanged"
|
|
54
|
+
TestFilesChanged: "testFilesChanged",
|
|
55
|
+
TestPaused: "testPaused"
|
|
55
56
|
};
|
|
56
57
|
class TestRunner extends import_events.default {
|
|
57
58
|
constructor(configLocation, configCLIOverrides) {
|
|
@@ -93,7 +94,7 @@ class TestRunner extends import_events.default {
|
|
|
93
94
|
}
|
|
94
95
|
async installBrowsers() {
|
|
95
96
|
const executables = import_server.registry.defaultExecutables();
|
|
96
|
-
await import_server.registry.install(executables
|
|
97
|
+
await import_server.registry.install(executables);
|
|
97
98
|
}
|
|
98
99
|
async loadConfig() {
|
|
99
100
|
const { config, error } = await this._loadConfig(this._configCLIOverrides);
|
|
@@ -245,16 +246,13 @@ class TestRunner extends import_events.default {
|
|
|
245
246
|
...params.video === "on" || params.video === "off" ? { video: params.video } : {},
|
|
246
247
|
...params.headed !== void 0 ? { headless: !params.headed } : {},
|
|
247
248
|
_optionContextReuseMode: params.reuseContext ? "when-possible" : void 0,
|
|
248
|
-
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : void 0
|
|
249
|
+
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : void 0,
|
|
250
|
+
actionTimeout: params.actionTimeout
|
|
249
251
|
},
|
|
250
252
|
...params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {},
|
|
251
253
|
...params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {},
|
|
252
254
|
...params.workers ? { workers: params.workers } : {}
|
|
253
255
|
};
|
|
254
|
-
if (params.trace === "on")
|
|
255
|
-
process.env.PW_LIVE_TRACE_STACKS = "1";
|
|
256
|
-
else
|
|
257
|
-
process.env.PW_LIVE_TRACE_STACKS = void 0;
|
|
258
256
|
const config = await this._loadConfigOrReportError(new import_internalReporter.InternalReporter([userReporter]), overrides);
|
|
259
257
|
if (!config)
|
|
260
258
|
return { status: "failed" };
|
|
@@ -269,7 +267,7 @@ class TestRunner extends import_events.default {
|
|
|
269
267
|
const testIdSet = new Set(params.testIds);
|
|
270
268
|
config.preOnlyTestFilters.push((test) => testIdSet.has(test.id));
|
|
271
269
|
}
|
|
272
|
-
const configReporters = params.disableConfigReporters ? [] : await (0, import_reporters.createReporters)(config, "test"
|
|
270
|
+
const configReporters = params.disableConfigReporters ? [] : await (0, import_reporters.createReporters)(config, "test");
|
|
273
271
|
const reporter = new import_internalReporter.InternalReporter([...configReporters, userReporter]);
|
|
274
272
|
const stop = new import_utils.ManualPromise();
|
|
275
273
|
const tasks = [
|
|
@@ -278,6 +276,7 @@ class TestRunner extends import_events.default {
|
|
|
278
276
|
...(0, import_tasks.createRunTestsTasks)(config)
|
|
279
277
|
];
|
|
280
278
|
const testRun = new import_tasks.TestRun(config, reporter, { pauseOnError: params.pauseOnError, pauseAtEnd: params.pauseAtEnd });
|
|
279
|
+
testRun.failureTracker.onTestPaused = (params2) => this.emit(TestRunnerEvent.TestPaused, params2);
|
|
281
280
|
const run = (0, import_tasks.runTasks)(testRun, tasks, 0, stop).then(async (status) => {
|
|
282
281
|
this._testRun = void 0;
|
|
283
282
|
return status;
|
|
@@ -363,7 +362,7 @@ async function runAllTestsWithConfig(config) {
|
|
|
363
362
|
const listOnly = config.cliListOnly;
|
|
364
363
|
(0, import_gitCommitInfoPlugin.addGitCommitInfoPlugin)(config);
|
|
365
364
|
(0, import_webServerPlugin.webServerPluginsForConfig)(config).forEach((p) => config.plugins.push({ factory: p }));
|
|
366
|
-
const reporters = await (0, import_reporters.createReporters)(config, listOnly ? "list" : "test"
|
|
365
|
+
const reporters = await (0, import_reporters.createReporters)(config, listOnly ? "list" : "test");
|
|
367
366
|
const lastRun = new import_lastRun.LastRunReporter(config);
|
|
368
367
|
if (config.cliLastFailed)
|
|
369
368
|
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;
|
|
@@ -74,6 +75,7 @@ class TestServerDispatcher {
|
|
|
74
75
|
};
|
|
75
76
|
this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params);
|
|
76
77
|
this._testRunner.on(import_testRunner.TestRunnerEvent.TestFilesChanged, (testFiles) => this._dispatchEvent("testFilesChanged", { testFiles }));
|
|
78
|
+
this._testRunner.on(import_testRunner.TestRunnerEvent.TestPaused, (params) => this._dispatchEvent("testPaused", { errors: params.errors }));
|
|
77
79
|
}
|
|
78
80
|
async _wireReporter(messageSink) {
|
|
79
81
|
return await (0, import_reporters.createReporterForTestServer)(this._serializer, messageSink);
|
|
@@ -110,7 +112,6 @@ class TestServerDispatcher {
|
|
|
110
112
|
await this._testRunner.installBrowsers();
|
|
111
113
|
}
|
|
112
114
|
async runGlobalSetup(params) {
|
|
113
|
-
await this.runGlobalTeardown();
|
|
114
115
|
const { reporter, report } = await this._collectingReporter();
|
|
115
116
|
this._globalSetupReport = report;
|
|
116
117
|
const { status } = await this._testRunner.runGlobalSetup([reporter, new import_list.default()]);
|
|
@@ -151,7 +152,9 @@ class TestServerDispatcher {
|
|
|
151
152
|
const wireReporter = await this._wireReporter((e) => this._dispatchEvent("report", e));
|
|
152
153
|
const { status } = await this._testRunner.runTests(wireReporter, {
|
|
153
154
|
...params,
|
|
154
|
-
doNotRunDepsOutsideProjectFilter: true
|
|
155
|
+
doNotRunDepsOutsideProjectFilter: true,
|
|
156
|
+
pauseAtEnd: params.pauseAtEnd,
|
|
157
|
+
pauseOnError: params.pauseOnError
|
|
155
158
|
});
|
|
156
159
|
return { status };
|
|
157
160
|
}
|
|
@@ -191,17 +194,19 @@ class TestServerDispatcher {
|
|
|
191
194
|
};
|
|
192
195
|
process.stdout.write = stdoutWrite;
|
|
193
196
|
process.stderr.write = stderrWrite;
|
|
197
|
+
process.stdin.isTTY = void 0;
|
|
194
198
|
} else {
|
|
195
199
|
import_utilsBundle.debug.log = originalDebugLog;
|
|
196
200
|
process.stdout.write = originalStdoutWrite;
|
|
197
201
|
process.stderr.write = originalStderrWrite;
|
|
202
|
+
process.stdin.isTTY = originalStdinIsTTY;
|
|
198
203
|
}
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
206
|
async function runUIMode(configFile, configCLIOverrides, options) {
|
|
202
207
|
const configLocation = (0, import_configLoader.resolveConfigLocation)(configFile);
|
|
203
208
|
return await innerRunTestServer(configLocation, configCLIOverrides, options, async (server, cancelPromise) => {
|
|
204
|
-
await (0, import_server.installRootRedirect)(server,
|
|
209
|
+
await (0, import_server.installRootRedirect)(server, void 0, { ...options, webApp: "uiMode.html" });
|
|
205
210
|
if (options.host !== void 0 || options.port !== void 0) {
|
|
206
211
|
await (0, import_server.openTraceInBrowser)(server.urlPrefix("human-readable"));
|
|
207
212
|
} else {
|
package/lib/runner/workerHost.js
CHANGED
|
@@ -79,6 +79,9 @@ class WorkerHost extends import_processHost.ProcessHost {
|
|
|
79
79
|
runTestGroup(runPayload) {
|
|
80
80
|
this.sendMessageNoReply({ method: "runTestGroup", params: runPayload });
|
|
81
81
|
}
|
|
82
|
+
async sendCustomMessage(payload) {
|
|
83
|
+
return await this.sendMessage({ method: "customMessage", params: payload });
|
|
84
|
+
}
|
|
82
85
|
hash() {
|
|
83
86
|
return this._hash;
|
|
84
87
|
}
|
package/lib/worker/testInfo.js
CHANGED
|
@@ -43,14 +43,13 @@ var import_testTracing = require("./testTracing");
|
|
|
43
43
|
var import_util2 = require("./util");
|
|
44
44
|
var import_transform = require("../transform/transform");
|
|
45
45
|
class TestInfoImpl {
|
|
46
|
-
constructor(configInternal, projectInternal, workerParams, test, retry, onStepBegin, onStepEnd, onAttach) {
|
|
46
|
+
constructor(configInternal, projectInternal, workerParams, test, retry, onStepBegin, onStepEnd, onAttach, onTestPaused) {
|
|
47
47
|
this._snapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
|
|
48
48
|
this._ariaSnapshotNames = { lastAnonymousSnapshotIndex: 0, lastNamedSnapshotIndex: {} };
|
|
49
|
-
this.
|
|
49
|
+
this._interruptedPromise = new import_utils.ManualPromise();
|
|
50
50
|
this._lastStepId = 0;
|
|
51
51
|
this._steps = [];
|
|
52
52
|
this._stepMap = /* @__PURE__ */ new Map();
|
|
53
|
-
this._onDidFinishTestFunctions = [];
|
|
54
53
|
this._hasNonRetriableError = false;
|
|
55
54
|
this._hasUnhandledError = false;
|
|
56
55
|
this._allowSkips = false;
|
|
@@ -64,6 +63,7 @@ class TestInfoImpl {
|
|
|
64
63
|
this._onStepBegin = onStepBegin;
|
|
65
64
|
this._onStepEnd = onStepEnd;
|
|
66
65
|
this._onAttach = onAttach;
|
|
66
|
+
this._onTestPaused = onTestPaused;
|
|
67
67
|
this._startTime = (0, import_utils.monotonicTime)();
|
|
68
68
|
this._startWallTime = Date.now();
|
|
69
69
|
this._requireFile = test?._requireFile ?? "";
|
|
@@ -107,11 +107,17 @@ class TestInfoImpl {
|
|
|
107
107
|
return import_path.default.join(this.project.snapshotDir, relativeTestFilePath + "-snapshots");
|
|
108
108
|
})();
|
|
109
109
|
this._attachmentsPush = this.attachments.push.bind(this.attachments);
|
|
110
|
-
|
|
110
|
+
const attachmentsPush = (...attachments) => {
|
|
111
111
|
for (const a of attachments)
|
|
112
112
|
this._attach(a, this._parentStep()?.stepId);
|
|
113
113
|
return this.attachments.length;
|
|
114
114
|
};
|
|
115
|
+
Object.defineProperty(this.attachments, "push", {
|
|
116
|
+
value: attachmentsPush,
|
|
117
|
+
writable: true,
|
|
118
|
+
enumerable: false,
|
|
119
|
+
configurable: true
|
|
120
|
+
});
|
|
115
121
|
this._tracing = new import_testTracing.TestTracing(this, workerParams.artifactsDir);
|
|
116
122
|
this.skip = (0, import_transform.wrapFunctionWithLocation)((location, ...args) => this._modifier("skip", location, args));
|
|
117
123
|
this.fixme = (0, import_transform.wrapFunctionWithLocation)((location, ...args) => this._modifier("fixme", location, args));
|
|
@@ -262,7 +268,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
262
268
|
this._tracing.appendBeforeActionForStep({
|
|
263
269
|
stepId,
|
|
264
270
|
parentId: parentStep?.stepId,
|
|
265
|
-
title: step.title,
|
|
271
|
+
title: step.shortTitle ?? step.title,
|
|
266
272
|
category: step.category,
|
|
267
273
|
params: step.params,
|
|
268
274
|
stack: step.location ? [step.location] : [],
|
|
@@ -272,7 +278,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
272
278
|
return step;
|
|
273
279
|
}
|
|
274
280
|
_interrupt() {
|
|
275
|
-
this.
|
|
281
|
+
this._interruptedPromise.resolve();
|
|
276
282
|
this._timeoutManager.interrupt();
|
|
277
283
|
if (this.status === "passed")
|
|
278
284
|
this.status = "interrupted";
|
|
@@ -314,7 +320,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
314
320
|
}
|
|
315
321
|
});
|
|
316
322
|
} catch (error) {
|
|
317
|
-
if (!this.
|
|
323
|
+
if (!this._interruptedPromise.isDone() && error instanceof import_timeoutManager.TimeoutManagerError)
|
|
318
324
|
this._failWithError(error);
|
|
319
325
|
throw error;
|
|
320
326
|
}
|
|
@@ -329,6 +335,14 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
329
335
|
_setDebugMode() {
|
|
330
336
|
this._timeoutManager.setIgnoreTimeouts();
|
|
331
337
|
}
|
|
338
|
+
async _didFinishTestFunction() {
|
|
339
|
+
const shouldPause = this._workerParams.pauseAtEnd && !this._isFailure() || this._workerParams.pauseOnError && this._isFailure();
|
|
340
|
+
if (shouldPause) {
|
|
341
|
+
this._onTestPaused({ testId: this.testId, errors: this._isFailure() ? this.errors : [] });
|
|
342
|
+
await this._interruptedPromise;
|
|
343
|
+
}
|
|
344
|
+
await this._onDidFinishTestFunctionCallback?.();
|
|
345
|
+
}
|
|
332
346
|
// ------------ TestInfo methods ------------
|
|
333
347
|
async attach(name, options = {}) {
|
|
334
348
|
const step = this._addStep({
|
|
@@ -337,14 +351,17 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
337
351
|
});
|
|
338
352
|
this._attach(
|
|
339
353
|
await (0, import_util.normalizeAndSaveAttachment)(this.outputPath(), name, options),
|
|
340
|
-
step.
|
|
354
|
+
step.stepId
|
|
341
355
|
);
|
|
342
356
|
step.complete({});
|
|
343
357
|
}
|
|
344
358
|
_attach(attachment, stepId) {
|
|
345
359
|
const index = this._attachmentsPush(attachment) - 1;
|
|
346
|
-
|
|
347
|
-
|
|
360
|
+
let step = stepId ? this._stepMap.get(stepId) : void 0;
|
|
361
|
+
if (!!step?.group)
|
|
362
|
+
step = void 0;
|
|
363
|
+
if (step) {
|
|
364
|
+
step.attachmentIndices.push(index);
|
|
348
365
|
} else {
|
|
349
366
|
const stepId2 = `attach@${(0, import_utils.createGuid)()}`;
|
|
350
367
|
this._tracing.appendBeforeActionForStep({ stepId: stepId2, title: `Attach ${(0, import_utils.escapeWithQuotes)(attachment.name, '"')}`, category: "test.attach", stack: [] });
|
|
@@ -356,7 +373,7 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
356
373
|
contentType: attachment.contentType,
|
|
357
374
|
path: attachment.path,
|
|
358
375
|
body: attachment.body?.toString("base64"),
|
|
359
|
-
stepId
|
|
376
|
+
stepId: step?.stepId
|
|
360
377
|
});
|
|
361
378
|
}
|
|
362
379
|
outputPath(...pathSegments) {
|
|
@@ -445,12 +462,6 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
445
462
|
setTimeout(timeout) {
|
|
446
463
|
this._timeoutManager.setTimeout(timeout);
|
|
447
464
|
}
|
|
448
|
-
_pauseOnError() {
|
|
449
|
-
return this._workerParams.pauseOnError;
|
|
450
|
-
}
|
|
451
|
-
_pauseAtEnd() {
|
|
452
|
-
return this._workerParams.pauseAtEnd;
|
|
453
|
-
}
|
|
454
465
|
}
|
|
455
466
|
class TestStepInfoImpl {
|
|
456
467
|
constructor(testInfo, stepId, title, parentStep) {
|
package/lib/worker/workerMain.js
CHANGED
|
@@ -98,6 +98,7 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
98
98
|
const fakeTestInfo = new import_testInfo.TestInfoImpl(this._config, this._project, this._params, void 0, 0, () => {
|
|
99
99
|
}, () => {
|
|
100
100
|
}, () => {
|
|
101
|
+
}, () => {
|
|
101
102
|
});
|
|
102
103
|
const runnable = { type: "teardown" };
|
|
103
104
|
await fakeTestInfo._runWithTimeout(runnable, () => this._loadIfNeeded()).catch(() => {
|
|
@@ -179,7 +180,7 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
179
180
|
let fatalUnknownTestIds;
|
|
180
181
|
try {
|
|
181
182
|
await this._loadIfNeeded();
|
|
182
|
-
const fileSuite = await (0, import_testLoader.loadTestFile)(runPayload.file, this._config
|
|
183
|
+
const fileSuite = await (0, import_testLoader.loadTestFile)(runPayload.file, this._config);
|
|
183
184
|
const suite = (0, import_suiteUtils.bindFileSuiteToProject)(this._project, fileSuite);
|
|
184
185
|
if (this._params.repeatEachIndex)
|
|
185
186
|
(0, import_suiteUtils.applyRepeatEachIndex)(this._project, suite, this._params.repeatEachIndex);
|
|
@@ -222,6 +223,16 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
222
223
|
this._runFinished.resolve();
|
|
223
224
|
}
|
|
224
225
|
}
|
|
226
|
+
async customMessage(payload) {
|
|
227
|
+
try {
|
|
228
|
+
if (this._currentTest?.testId !== payload.testId)
|
|
229
|
+
throw new Error("Test has already stopped");
|
|
230
|
+
const response = await this._currentTest._onCustomMessageCallback?.(payload.request);
|
|
231
|
+
return { response };
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return { response: {}, error: (0, import_util2.testInfoError)(error) };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
225
236
|
async _runTest(test, retry, nextTest) {
|
|
226
237
|
const testInfo = new import_testInfo.TestInfoImpl(
|
|
227
238
|
this._config,
|
|
@@ -231,7 +242,8 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
231
242
|
retry,
|
|
232
243
|
(stepBeginPayload) => this.dispatchEvent("stepBegin", stepBeginPayload),
|
|
233
244
|
(stepEndPayload) => this.dispatchEvent("stepEnd", stepEndPayload),
|
|
234
|
-
(attachment) => this.dispatchEvent("attach", attachment)
|
|
245
|
+
(attachment) => this.dispatchEvent("attach", attachment),
|
|
246
|
+
(testPausedPayload) => this.dispatchEvent("testPaused", testPausedPayload)
|
|
235
247
|
);
|
|
236
248
|
const processAnnotation = (annotation) => {
|
|
237
249
|
testInfo.annotations.push(annotation);
|
|
@@ -318,10 +330,7 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
318
330
|
await testInfo._runAsStep({ title: "After Hooks", category: "hook" }, async () => {
|
|
319
331
|
let firstAfterHooksError;
|
|
320
332
|
try {
|
|
321
|
-
await testInfo._runWithTimeout({ type: "test", slot: afterHooksSlot },
|
|
322
|
-
for (const fn of testInfo._onDidFinishTestFunctions)
|
|
323
|
-
await fn();
|
|
324
|
-
});
|
|
333
|
+
await testInfo._runWithTimeout({ type: "test", slot: afterHooksSlot }, () => testInfo._didFinishTestFunction());
|
|
325
334
|
} catch (error) {
|
|
326
335
|
firstAfterHooksError = firstAfterHooksError ?? error;
|
|
327
336
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.57.0",
|
|
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.
|
|
67
|
+
"playwright-core": "1.57.0"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|
package/types/test.d.ts
CHANGED
|
@@ -873,7 +873,9 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|
|
873
873
|
* - A module name like `'my-awesome-reporter'`.
|
|
874
874
|
* - A relative path to the reporter like `'./reporters/my-awesome-reporter.js'`.
|
|
875
875
|
*
|
|
876
|
-
* You can pass options to the reporter in a tuple like `['json', { outputFile: './report.json' }]`.
|
|
876
|
+
* You can pass options to the reporter in a tuple like `['json', { outputFile: './report.json' }]`. If the property
|
|
877
|
+
* is not specified, Playwright uses the `'dot'` reporter when the CI environment variable is set, and the `'list'`
|
|
878
|
+
* reporter otherwise.
|
|
877
879
|
*
|
|
878
880
|
* Learn more in the [reporters guide](https://playwright.dev/docs/test-reporters).
|
|
879
881
|
*
|
|
@@ -990,6 +992,31 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|
|
990
992
|
* });
|
|
991
993
|
* ```
|
|
992
994
|
*
|
|
995
|
+
* If your webserver runs on varying ports, use `wait` to capture the port:
|
|
996
|
+
*
|
|
997
|
+
* ```js
|
|
998
|
+
* import { defineConfig } from '@playwright/test';
|
|
999
|
+
*
|
|
1000
|
+
* export default defineConfig({
|
|
1001
|
+
* webServer: {
|
|
1002
|
+
* command: 'npm run start',
|
|
1003
|
+
* wait: {
|
|
1004
|
+
* stdout: '/Listening on port (?<my_server_port>\\d+)/'
|
|
1005
|
+
* },
|
|
1006
|
+
* },
|
|
1007
|
+
* });
|
|
1008
|
+
* ```
|
|
1009
|
+
*
|
|
1010
|
+
* ```js
|
|
1011
|
+
* import { test, expect } from '@playwright/test';
|
|
1012
|
+
*
|
|
1013
|
+
* test.use({ baseUrl: `http://localhost:${process.env.MY_SERVER_PORT ?? 3000}` });
|
|
1014
|
+
*
|
|
1015
|
+
* test('homepage', async ({ page }) => {
|
|
1016
|
+
* await page.goto('/');
|
|
1017
|
+
* });
|
|
1018
|
+
* ```
|
|
1019
|
+
*
|
|
993
1020
|
*/
|
|
994
1021
|
webServer?: TestConfigWebServer | TestConfigWebServer[];
|
|
995
1022
|
/**
|
|
@@ -1758,6 +1785,26 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|
|
1758
1785
|
*/
|
|
1759
1786
|
snapshotPathTemplate?: string;
|
|
1760
1787
|
|
|
1788
|
+
/**
|
|
1789
|
+
* Tag or tags prepended to each test in the report. Useful for tagging your test run to differentiate between
|
|
1790
|
+
* [CI environments](https://playwright.dev/docs/test-sharding#merging-reports-from-multiple-environments).
|
|
1791
|
+
*
|
|
1792
|
+
* Note that each tag must start with `@` symbol. Learn more about [tagging](https://playwright.dev/docs/test-annotations#tag-tests).
|
|
1793
|
+
*
|
|
1794
|
+
* **Usage**
|
|
1795
|
+
*
|
|
1796
|
+
* ```js
|
|
1797
|
+
* // playwright.config.ts
|
|
1798
|
+
* import { defineConfig } from '@playwright/test';
|
|
1799
|
+
*
|
|
1800
|
+
* export default defineConfig({
|
|
1801
|
+
* tag: process.env.CI_ENVIRONMENT_NAME, // for example "@APIv2"
|
|
1802
|
+
* });
|
|
1803
|
+
* ```
|
|
1804
|
+
*
|
|
1805
|
+
*/
|
|
1806
|
+
tag?: string|Array<string>;
|
|
1807
|
+
|
|
1761
1808
|
/**
|
|
1762
1809
|
* Directory that will be recursively scanned for test files. Defaults to the directory of the configuration file.
|
|
1763
1810
|
*
|
|
@@ -2035,6 +2082,11 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
|
|
2035
2082
|
current: number;
|
|
2036
2083
|
};
|
|
2037
2084
|
|
|
2085
|
+
/**
|
|
2086
|
+
* Resolved global tags. See [testConfig.tag](https://playwright.dev/docs/api/class-testconfig#test-config-tag).
|
|
2087
|
+
*/
|
|
2088
|
+
tags: Array<string>;
|
|
2089
|
+
|
|
2038
2090
|
/**
|
|
2039
2091
|
* See [testConfig.updateSnapshots](https://playwright.dev/docs/api/class-testconfig#test-config-update-snapshots).
|
|
2040
2092
|
*/
|
|
@@ -6180,7 +6232,7 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
|
|
|
6180
6232
|
* const name = this.constructor.name + '.' + (context.name as string);
|
|
6181
6233
|
* return test.step(name, async () => {
|
|
6182
6234
|
* return await target.call(this, ...args);
|
|
6183
|
-
* });
|
|
6235
|
+
* }, { box: true });
|
|
6184
6236
|
* };
|
|
6185
6237
|
* }
|
|
6186
6238
|
*
|
|
@@ -6339,7 +6391,7 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
|
|
|
6339
6391
|
* const name = this.constructor.name + '.' + (context.name as string);
|
|
6340
6392
|
* return test.step(name, async () => {
|
|
6341
6393
|
* return await target.call(this, ...args);
|
|
6342
|
-
* });
|
|
6394
|
+
* }, { box: true });
|
|
6343
6395
|
* };
|
|
6344
6396
|
* }
|
|
6345
6397
|
*
|
|
@@ -7703,6 +7755,27 @@ interface AsymmetricMatchers {
|
|
|
7703
7755
|
* @param expected Expected array that is a subset of the received value.
|
|
7704
7756
|
*/
|
|
7705
7757
|
arrayContaining(sample: Array<unknown>): AsymmetricMatcher;
|
|
7758
|
+
/**
|
|
7759
|
+
* `expect.arrayOf()` matches array of objects created from the
|
|
7760
|
+
* [`constructor`](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-array-of-option-constructor)
|
|
7761
|
+
* or a corresponding primitive type. Use it inside
|
|
7762
|
+
* [expect(value).toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal)
|
|
7763
|
+
* to perform pattern matching.
|
|
7764
|
+
*
|
|
7765
|
+
* **Usage**
|
|
7766
|
+
*
|
|
7767
|
+
* ```js
|
|
7768
|
+
* // Match instance of a class.
|
|
7769
|
+
* class Example {}
|
|
7770
|
+
* expect([new Example(), new Example()]).toEqual(expect.arrayOf(Example));
|
|
7771
|
+
*
|
|
7772
|
+
* // Match any string.
|
|
7773
|
+
* expect(['a', 'b', 'c']).toEqual(expect.arrayOf(String));
|
|
7774
|
+
* ```
|
|
7775
|
+
*
|
|
7776
|
+
* @param constructor Constructor of the expected object like `ExampleClass`, or a primitive boxed type like `Number`.
|
|
7777
|
+
*/
|
|
7778
|
+
arrayOf(sample: unknown): AsymmetricMatcher;
|
|
7706
7779
|
/**
|
|
7707
7780
|
* Compares floating point numbers for approximate equality. Use this method inside
|
|
7708
7781
|
* [expect(value).toEqual(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-to-equal)
|
|
@@ -8086,6 +8159,7 @@ interface GenericAssertions<R> {
|
|
|
8086
8159
|
* - [expect(value).any(constructor)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-any)
|
|
8087
8160
|
* - [expect(value).anything()](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-anything)
|
|
8088
8161
|
* - [expect(value).arrayContaining(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-array-containing)
|
|
8162
|
+
* - [expect(value).arrayOf(constructor)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-array-of)
|
|
8089
8163
|
* - [expect(value).closeTo(expected[, numDigits])](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-close-to)
|
|
8090
8164
|
* - [expect(value).objectContaining(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-object-containing)
|
|
8091
8165
|
* - [expect(value).stringContaining(expected)](https://playwright.dev/docs/api/class-genericassertions#generic-assertions-string-containing)
|
|
@@ -10145,6 +10219,25 @@ interface TestConfigWebServer {
|
|
|
10145
10219
|
*/
|
|
10146
10220
|
stdout?: "pipe"|"ignore";
|
|
10147
10221
|
|
|
10222
|
+
/**
|
|
10223
|
+
* Consider command started only when given output has been produced.
|
|
10224
|
+
*/
|
|
10225
|
+
wait?: {
|
|
10226
|
+
/**
|
|
10227
|
+
* Regular expression to wait for in the `stdout` of the command output. Named capture groups are stored in the
|
|
10228
|
+
* environment, for example `/Listening on port (?<my_server_port>\\d+)/` will store the port number in
|
|
10229
|
+
* `process.env['MY_SERVER_PORT']`.
|
|
10230
|
+
*/
|
|
10231
|
+
stdout?: RegExp;
|
|
10232
|
+
|
|
10233
|
+
/**
|
|
10234
|
+
* Regular expression to wait for in the `stderr` of the command output. Named capture groups are stored in the
|
|
10235
|
+
* environment, for example `/Listening on port (?<my_server_port>\\d+)/` will store the port number in
|
|
10236
|
+
* `process.env['MY_SERVER_PORT']`.
|
|
10237
|
+
*/
|
|
10238
|
+
stderr?: RegExp;
|
|
10239
|
+
};
|
|
10240
|
+
|
|
10148
10241
|
/**
|
|
10149
10242
|
* How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
|
|
10150
10243
|
*/
|