playwright 1.56.0-alpha-1758818034000 → 1.56.0-alpha-1758839353000

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.
@@ -51,6 +51,7 @@ Your process is methodical and thorough:
51
51
  @playwright/test source code that follows following convention:
52
52
 
53
53
  - One file per scenario, one test in a file
54
+ - Use seed test content (copyright, structure) to emit consistent tests.
54
55
  - File name must be fs-friendly scenario name
55
56
  - Test must be placed in a describe matching the top-level test plan item
56
57
  - Test title must match the scenario name
@@ -33,6 +33,7 @@ __export(bundle_exports, {
33
33
  ListRootsRequestSchema: () => ListRootsRequestSchema,
34
34
  ListToolsRequestSchema: () => ListToolsRequestSchema,
35
35
  PingRequestSchema: () => PingRequestSchema,
36
+ ProgressNotificationSchema: () => ProgressNotificationSchema,
36
37
  SSEServerTransport: () => SSEServerTransport,
37
38
  Server: () => Server,
38
39
  StdioClientTransport: () => StdioClientTransport,
@@ -54,6 +55,7 @@ const StreamableHTTPServerTransport = bundle.StreamableHTTPServerTransport;
54
55
  const StreamableHTTPClientTransport = bundle.StreamableHTTPClientTransport;
55
56
  const CallToolRequestSchema = bundle.CallToolRequestSchema;
56
57
  const ListRootsRequestSchema = bundle.ListRootsRequestSchema;
58
+ const ProgressNotificationSchema = bundle.ProgressNotificationSchema;
57
59
  const ListToolsRequestSchema = bundle.ListToolsRequestSchema;
58
60
  const PingRequestSchema = bundle.PingRequestSchema;
59
61
  const z = bundle.z;
@@ -64,6 +66,7 @@ const z = bundle.z;
64
66
  ListRootsRequestSchema,
65
67
  ListToolsRequestSchema,
66
68
  PingRequestSchema,
69
+ ProgressNotificationSchema,
67
70
  SSEServerTransport,
68
71
  Server,
69
72
  StdioClientTransport,
@@ -46,6 +46,7 @@ const z = mcpBundle.z;
46
46
  class MDBBackend {
47
47
  constructor(topLevelBackend) {
48
48
  this._stack = [];
49
+ this._progress = [];
49
50
  this._topLevelBackend = topLevelBackend;
50
51
  }
51
52
  async initialize(server, clientInfo) {
@@ -77,15 +78,22 @@ class MDBBackend {
77
78
  entry.resultPromise = resultPromise;
78
79
  client.callTool({
79
80
  name,
80
- arguments: args
81
+ arguments: args,
82
+ _meta: {
83
+ progressToken: name + "@" + (0, import_utils.createGuid)().slice(0, 8)
84
+ }
81
85
  }).then((result2) => {
82
86
  resultPromise.resolve(result2);
83
- }).catch((e) => resultPromise.reject(e));
87
+ }).catch((e) => {
88
+ resultPromise.resolve({ content: [{ type: "text", text: String(e) }], isError: true });
89
+ });
84
90
  const result = await Promise.race([interruptPromise, resultPromise]);
85
91
  if (interruptPromise.isDone())
86
92
  mdbDebug("client call intercepted", result);
87
93
  else
88
94
  mdbDebug("client call result", result);
95
+ result.content.unshift(...this._progress);
96
+ this._progress.length = 0;
89
97
  return result;
90
98
  }
91
99
  async _client() {
@@ -106,6 +114,13 @@ class MDBBackend {
106
114
  const client = new mcpBundle.Client({ name: "Pushing client", version: "0.0.0" }, { capabilities: { roots: {} } });
107
115
  client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] }));
108
116
  client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
117
+ client.setNotificationHandler(mcpBundle.ProgressNotificationSchema, (notification) => {
118
+ if (notification.method === "notifications/progress") {
119
+ const { message } = notification.params;
120
+ if (message)
121
+ this._progress.push({ type: "text", text: message });
122
+ }
123
+ });
109
124
  await client.connect(transport);
110
125
  mdbDebug("connected to the new client");
111
126
  const { tools } = await client.listTools();
@@ -189,8 +204,8 @@ class ServerBackendWithCloseListener {
189
204
  async listTools() {
190
205
  return this._backend.listTools();
191
206
  }
192
- async callTool(name, args) {
193
- return this._backend.callTool(name, args);
207
+ async callTool(name, args, progress) {
208
+ return this._backend.callTool(name, args, progress);
194
209
  }
195
210
  serverClosed(server) {
196
211
  this._backend.serverClosed?.(server);
@@ -61,13 +61,27 @@ function createServer(name, version, backend, runHeartbeat) {
61
61
  return { tools };
62
62
  });
63
63
  let initializePromise;
64
- server.setRequestHandler(mcpBundle.CallToolRequestSchema, async (request) => {
64
+ server.setRequestHandler(mcpBundle.CallToolRequestSchema, async (request, extra) => {
65
65
  serverDebug("callTool", request);
66
+ const progressToken = request.params._meta?.progressToken;
67
+ let progressCounter = 0;
68
+ const progress = progressToken ? (params) => {
69
+ extra.sendNotification({
70
+ method: "notifications/progress",
71
+ params: {
72
+ progressToken,
73
+ progress: params.progress ?? ++progressCounter,
74
+ total: params.total,
75
+ message: params.message
76
+ }
77
+ }).catch(serverDebug);
78
+ } : () => {
79
+ };
66
80
  try {
67
81
  if (!initializePromise)
68
82
  initializePromise = initializeServer(server, backend, runHeartbeat);
69
83
  await initializePromise;
70
- return await backend.callTool(request.params.name, request.params.arguments || {});
84
+ return mergeTextParts(await backend.callTool(request.params.name, request.params.arguments || {}, progress));
71
85
  } catch (error) {
72
86
  return {
73
87
  content: [{ type: "text", text: "### Result\n" + String(error) }],
@@ -145,6 +159,27 @@ function firstRootPath(clientInfo) {
145
159
  const url = firstRootUri ? new URL(firstRootUri) : void 0;
146
160
  return url ? (0, import_url.fileURLToPath)(url) : void 0;
147
161
  }
162
+ function mergeTextParts(result) {
163
+ const content = [];
164
+ const testParts = [];
165
+ for (const part of result.content) {
166
+ if (part.type === "text") {
167
+ testParts.push(part.text);
168
+ continue;
169
+ }
170
+ if (testParts.length > 0) {
171
+ content.push({ type: "text", text: testParts.join("\n") });
172
+ testParts.length = 0;
173
+ }
174
+ content.push(part);
175
+ }
176
+ if (testParts.length > 0)
177
+ content.push({ type: "text", text: testParts.join("\n") });
178
+ return {
179
+ ...result,
180
+ content
181
+ };
182
+ }
148
183
  // Annotate the CommonJS export names for ESM import in node:
149
184
  0 && (module.exports = {
150
185
  connect,
@@ -23,17 +23,15 @@ __export(streams_exports, {
23
23
  module.exports = __toCommonJS(streams_exports);
24
24
  var import_stream = require("stream");
25
25
  class StringWriteStream extends import_stream.Writable {
26
- constructor() {
27
- super(...arguments);
28
- this._chunks = [];
26
+ constructor(progress) {
27
+ super();
28
+ this._progress = progress;
29
29
  }
30
30
  _write(chunk, encoding, callback) {
31
- this._chunks.push(chunk.toString());
31
+ const text = chunk.toString();
32
+ this._progress({ message: text.endsWith("\n") ? text.slice(0, -1) : text });
32
33
  callback();
33
34
  }
34
- content() {
35
- return this._chunks.join("");
36
- }
37
35
  }
38
36
  // Annotate the CommonJS export names for ESM import in node:
39
37
  0 && (module.exports = {
@@ -45,13 +45,13 @@ class TestServerBackend {
45
45
  this._configOption = configOption;
46
46
  }
47
47
  async initialize(server, clientInfo) {
48
+ const rootPath = mcp.firstRootPath(clientInfo);
48
49
  if (this._configOption) {
49
- this._context.setConfigLocation((0, import_configLoader.resolveConfigLocation)(this._configOption));
50
+ this._context.initialize(rootPath, (0, import_configLoader.resolveConfigLocation)(this._configOption));
50
51
  return;
51
52
  }
52
- const rootPath = mcp.firstRootPath(clientInfo);
53
53
  if (rootPath) {
54
- this._context.setConfigLocation((0, import_configLoader.resolveConfigLocation)(rootPath));
54
+ this._context.initialize(rootPath, (0, import_configLoader.resolveConfigLocation)(rootPath));
55
55
  return;
56
56
  }
57
57
  throw new Error("No config option or MCP root path provided");
@@ -62,12 +62,12 @@ class TestServerBackend {
62
62
  ...import_tools.browserTools.map((tool) => mcp.toMcpTool(tool.schema))
63
63
  ];
64
64
  }
65
- async callTool(name, args) {
65
+ async callTool(name, args, progress) {
66
66
  const tool = this._tools.find((tool2) => tool2.schema.name === name);
67
67
  if (!tool)
68
68
  throw new Error(`Tool not found: ${name}. Available tools: ${this._tools.map((tool2) => tool2.schema.name).join(", ")}`);
69
69
  const parsedArguments = tool.schema.inputSchema.parse(args || {});
70
- return await tool.handle(this._context, parsedArguments);
70
+ return await tool.handle(this._context, parsedArguments, progress);
71
71
  }
72
72
  serverClosed() {
73
73
  void this._context.close();
@@ -26,8 +26,9 @@ class TestContext {
26
26
  constructor(options) {
27
27
  this.options = options;
28
28
  }
29
- setConfigLocation(configLocation) {
29
+ initialize(rootPath, configLocation) {
30
30
  this.configLocation = configLocation;
31
+ this.rootPath = rootPath || configLocation.configDir;
31
32
  }
32
33
  async createTestRunner() {
33
34
  if (this._testRunner)
@@ -44,6 +44,7 @@ var import_listModeReporter = __toESM(require("../../reporters/listModeReporter"
44
44
  var import_projectUtils = require("../../runner/projectUtils");
45
45
  var import_testTool = require("./testTool");
46
46
  var import_streams = require("./streams");
47
+ var import_util = require("../../util");
47
48
  const listTests = (0, import_testTool.defineTestTool)({
48
49
  schema: {
49
50
  name: "test_list",
@@ -52,14 +53,12 @@ const listTests = (0, import_testTool.defineTestTool)({
52
53
  inputSchema: import_bundle.z.object({}),
53
54
  type: "readOnly"
54
55
  },
55
- handle: async (context) => {
56
- const { screen, stream } = createScreen();
56
+ handle: async (context, _, progress) => {
57
+ const { screen } = createScreen(progress);
57
58
  const reporter = new import_listModeReporter.default({ screen, includeTestId: true });
58
59
  const testRunner = await context.createTestRunner();
59
60
  await testRunner.listTests(reporter, {});
60
- return {
61
- content: [{ type: "text", text: stream.content() }]
62
- };
61
+ return { content: [] };
63
62
  }
64
63
  });
65
64
  const runTests = (0, import_testTool.defineTestTool)({
@@ -73,22 +72,17 @@ const runTests = (0, import_testTool.defineTestTool)({
73
72
  }),
74
73
  type: "readOnly"
75
74
  },
76
- handle: async (context, params) => {
77
- const { screen, stream } = createScreen();
75
+ handle: async (context, params, progress) => {
76
+ const { screen } = createScreen(progress);
78
77
  const configDir = context.configLocation.configDir;
79
- const reporter = new import_list.default({ configDir, screen, includeTestId: true });
78
+ const reporter = new import_list.default({ configDir, screen, includeTestId: true, prefixStdio: "out" });
80
79
  const testRunner = await context.createTestRunner();
81
80
  await testRunner.runTests(reporter, {
82
81
  locations: params.locations,
83
82
  projects: params.projects,
84
83
  disableConfigReporters: true
85
84
  });
86
- const text = stream.content();
87
- return {
88
- content: [
89
- { type: "text", text }
90
- ]
91
- };
85
+ return { content: [] };
92
86
  }
93
87
  });
94
88
  const debugTest = (0, import_testTool.defineTestTool)({
@@ -104,12 +98,12 @@ const debugTest = (0, import_testTool.defineTestTool)({
104
98
  }),
105
99
  type: "readOnly"
106
100
  },
107
- handle: async (context, params) => {
108
- const { screen, stream } = createScreen();
101
+ handle: async (context, params, progress) => {
102
+ const { screen } = createScreen(progress);
109
103
  const configDir = context.configLocation.configDir;
110
- const reporter = new import_list.default({ configDir, screen });
104
+ const reporter = new import_list.default({ configDir, screen, includeTestId: true, prefixStdio: "out" });
111
105
  const testRunner = await context.createTestRunner();
112
- const result = await testRunner.runTests(reporter, {
106
+ await testRunner.runTests(reporter, {
113
107
  headed: !context.options?.headless,
114
108
  testIds: [params.test.id],
115
109
  // For automatic recovery
@@ -118,49 +112,64 @@ const debugTest = (0, import_testTool.defineTestTool)({
118
112
  pauseOnError: true,
119
113
  disableConfigReporters: true
120
114
  });
121
- const text = stream.content();
122
- return {
123
- content: [
124
- { type: "text", text }
125
- ],
126
- isError: result.status !== "passed"
127
- };
115
+ return { content: [] };
128
116
  }
129
117
  });
130
118
  const setupPage = (0, import_testTool.defineTestTool)({
131
119
  schema: {
132
120
  name: "test_setup_page",
133
121
  title: "Setup page",
134
- description: "Setup the page for test",
122
+ description: "Setup the page for test.",
135
123
  inputSchema: import_bundle.z.object({
136
124
  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.'),
137
- testLocation: import_bundle.z.string().optional().describe('Location of the seed test to use for setup. For example: "tests/seed.spec.ts" or "tests/seed.spec.ts:20".')
125
+ 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.')
138
126
  }),
139
127
  type: "readOnly"
140
128
  },
141
- handle: async (context, params) => {
142
- const { screen, stream } = createScreen();
129
+ handle: async (context, params, progress) => {
130
+ const { screen } = createScreen(progress);
143
131
  const configDir = context.configLocation.configDir;
144
132
  const reporter = new import_list.default({ configDir, screen });
145
133
  const testRunner = await context.createTestRunner();
146
- let testLocation = params.testLocation;
147
- if (!testLocation) {
148
- testLocation = "default.seed.spec.ts";
149
- const config = await testRunner.loadConfig();
150
- const project = params.project ? config.projects.find((p) => p.project.name === params.project) : (0, import_projectUtils.findTopLevelProjects)(config)[0];
151
- const testDir = project?.project.testDir || configDir;
152
- const seedFile = import_path.default.join(testDir, testLocation);
153
- if (!import_fs.default.existsSync(seedFile)) {
154
- await import_fs.default.promises.mkdir(import_path.default.dirname(seedFile), { recursive: true });
155
- await import_fs.default.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test';
134
+ const config = await testRunner.loadConfig();
135
+ const project = params.project ? config.projects.find((p) => p.project.name === params.project) : (0, import_projectUtils.findTopLevelProjects)(config)[0];
136
+ const testDir = project?.project.testDir || configDir;
137
+ let seedFile;
138
+ if (!params.seedFile) {
139
+ seedFile = import_path.default.resolve(testDir, "seed.spec.ts");
140
+ await import_fs.default.promises.mkdir(import_path.default.dirname(seedFile), { recursive: true });
141
+ await import_fs.default.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test';
156
142
 
157
- test('seed', async ({ page }) => {});
143
+ test.describe('Test group', () => {
144
+ test('seed', async ({ page }) => {
145
+ // generate code here.
146
+ });
147
+ });
158
148
  `);
149
+ } else {
150
+ const candidateFiles = [];
151
+ candidateFiles.push(import_path.default.resolve(testDir, params.seedFile));
152
+ candidateFiles.push(import_path.default.resolve(configDir, params.seedFile));
153
+ candidateFiles.push(import_path.default.resolve(process.cwd(), params.seedFile));
154
+ for (const candidateFile of candidateFiles) {
155
+ if (await (0, import_util.fileExistsAsync)(candidateFile)) {
156
+ seedFile = candidateFile;
157
+ break;
158
+ }
159
159
  }
160
+ if (!seedFile)
161
+ throw new Error("seed test not found.");
160
162
  }
163
+ const seedFileContent = await import_fs.default.promises.readFile(seedFile, "utf8");
164
+ progress({ message: `### Seed test
165
+ File: ${import_path.default.relative(context.rootPath, seedFile)}
166
+ \`\`\`ts
167
+ ${seedFileContent}
168
+ \`\`\`
169
+ ` });
161
170
  const result = await testRunner.runTests(reporter, {
162
171
  headed: !context.options?.headless,
163
- locations: [testLocation],
172
+ locations: [seedFile],
164
173
  projects: params.project ? [params.project] : void 0,
165
174
  timeout: 0,
166
175
  workers: 1,
@@ -168,21 +177,15 @@ test('seed', async ({ page }) => {});
168
177
  disableConfigReporters: true,
169
178
  failOnLoadErrors: true
170
179
  });
171
- if (result.status === "passed" && !reporter.suite?.allTests().length) {
172
- return {
173
- content: [{ type: "text", text: "Error: seed test not found." }],
174
- isError: true
175
- };
176
- }
177
- const text = stream.content();
178
- return {
179
- content: [{ type: "text", text }],
180
- isError: result.status !== "passed"
181
- };
180
+ if (result.status === "passed" && !reporter.suite?.allTests().length)
181
+ throw new Error("seed test not found.");
182
+ if (result.status !== "passed")
183
+ throw new Error("Errors while running the seed test.");
184
+ return { content: [] };
182
185
  }
183
186
  });
184
- function createScreen() {
185
- const stream = new import_streams.StringWriteStream();
187
+ function createScreen(progress) {
188
+ const stream = new import_streams.StringWriteStream(progress);
186
189
  const screen = {
187
190
  ...import_base.terminalScreen,
188
191
  isTTY: false,