@playwright/mcp 0.0.31 → 0.0.33

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.
Files changed (52) hide show
  1. package/README.md +27 -6
  2. package/config.d.ts +5 -0
  3. package/index.d.ts +1 -6
  4. package/lib/browserContextFactory.js +64 -54
  5. package/lib/browserServerBackend.js +121 -0
  6. package/lib/config.js +10 -9
  7. package/lib/context.js +107 -182
  8. package/lib/extension/cdpRelay.js +346 -0
  9. package/lib/extension/extensionContextFactory.js +56 -0
  10. package/lib/extension/main.js +26 -0
  11. package/lib/httpServer.js +20 -182
  12. package/lib/index.js +6 -3
  13. package/lib/loop/loop.js +69 -0
  14. package/lib/loop/loopClaude.js +152 -0
  15. package/lib/loop/loopOpenAI.js +141 -0
  16. package/lib/loop/main.js +60 -0
  17. package/lib/loopTools/context.js +66 -0
  18. package/lib/loopTools/main.js +49 -0
  19. package/lib/loopTools/perform.js +32 -0
  20. package/lib/loopTools/snapshot.js +29 -0
  21. package/lib/loopTools/tool.js +18 -0
  22. package/lib/mcp/inProcessTransport.js +72 -0
  23. package/lib/mcp/server.js +93 -0
  24. package/lib/{transport.js → mcp/transport.js} +30 -42
  25. package/lib/package.js +3 -3
  26. package/lib/program.js +39 -9
  27. package/lib/response.js +165 -0
  28. package/lib/sessionLog.js +121 -0
  29. package/lib/tab.js +138 -24
  30. package/lib/tools/common.js +10 -23
  31. package/lib/tools/console.js +4 -15
  32. package/lib/tools/dialogs.js +12 -17
  33. package/lib/tools/evaluate.js +12 -21
  34. package/lib/tools/files.js +9 -16
  35. package/lib/tools/install.js +3 -7
  36. package/lib/tools/keyboard.js +28 -42
  37. package/lib/tools/mouse.js +27 -50
  38. package/lib/tools/navigate.js +12 -35
  39. package/lib/tools/network.js +5 -15
  40. package/lib/tools/pdf.js +7 -16
  41. package/lib/tools/screenshot.js +35 -33
  42. package/lib/tools/snapshot.js +44 -69
  43. package/lib/tools/tabs.js +10 -41
  44. package/lib/tools/tool.js +15 -0
  45. package/lib/tools/utils.js +2 -9
  46. package/lib/tools/wait.js +3 -6
  47. package/lib/tools.js +3 -0
  48. package/lib/utils.js +26 -0
  49. package/package.json +11 -6
  50. package/lib/connection.js +0 -81
  51. package/lib/pageSnapshot.js +0 -43
  52. package/lib/server.js +0 -48
package/lib/tab.js CHANGED
@@ -13,18 +13,25 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { PageSnapshot } from './pageSnapshot.js';
17
- import { callOnPageNoTrace } from './tools/utils.js';
16
+ import { EventEmitter } from 'events';
17
+ import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
18
18
  import { logUnhandledError } from './log.js';
19
- export class Tab {
19
+ import { ManualPromise } from './manualPromise.js';
20
+ export const TabEvents = {
21
+ modalState: 'modalState'
22
+ };
23
+ export class Tab extends EventEmitter {
20
24
  context;
21
25
  page;
26
+ _lastTitle = 'about:blank';
22
27
  _consoleMessages = [];
23
28
  _recentConsoleMessages = [];
24
29
  _requests = new Map();
25
- _snapshot;
26
30
  _onPageClose;
31
+ _modalStates = [];
32
+ _downloads = [];
27
33
  constructor(context, page, onPageClose) {
34
+ super();
28
35
  this.context = context;
29
36
  this.page = page;
30
37
  this._onPageClose = onPageClose;
@@ -34,18 +41,52 @@ export class Tab {
34
41
  page.on('response', response => this._requests.set(response.request(), response));
35
42
  page.on('close', () => this._onClose());
36
43
  page.on('filechooser', chooser => {
37
- this.context.setModalState({
44
+ this.setModalState({
38
45
  type: 'fileChooser',
39
46
  description: 'File chooser',
40
47
  fileChooser: chooser,
41
- }, this);
48
+ });
42
49
  });
43
- page.on('dialog', dialog => this.context.dialogShown(this, dialog));
50
+ page.on('dialog', dialog => this._dialogShown(dialog));
44
51
  page.on('download', download => {
45
- void this.context.downloadStarted(this, download);
52
+ void this._downloadStarted(download);
46
53
  });
47
54
  page.setDefaultNavigationTimeout(60000);
48
55
  page.setDefaultTimeout(5000);
56
+ page[tabSymbol] = this;
57
+ }
58
+ static forPage(page) {
59
+ return page[tabSymbol];
60
+ }
61
+ modalStates() {
62
+ return this._modalStates;
63
+ }
64
+ setModalState(modalState) {
65
+ this._modalStates.push(modalState);
66
+ this.emit(TabEvents.modalState, modalState);
67
+ }
68
+ clearModalState(modalState) {
69
+ this._modalStates = this._modalStates.filter(state => state !== modalState);
70
+ }
71
+ modalStatesMarkdown() {
72
+ return renderModalStates(this.context, this.modalStates());
73
+ }
74
+ _dialogShown(dialog) {
75
+ this.setModalState({
76
+ type: 'dialog',
77
+ description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
78
+ dialog,
79
+ });
80
+ }
81
+ async _downloadStarted(download) {
82
+ const entry = {
83
+ download,
84
+ finished: false,
85
+ outputFile: await this.context.outputFile(download.suggestedFilename())
86
+ };
87
+ this._downloads.push(entry);
88
+ await download.saveAs(entry.outputFile);
89
+ entry.finished = true;
49
90
  }
50
91
  _clearCollectedArtifacts() {
51
92
  this._consoleMessages.length = 0;
@@ -60,8 +101,16 @@ export class Tab {
60
101
  this._clearCollectedArtifacts();
61
102
  this._onPageClose(this);
62
103
  }
63
- async title() {
64
- return await callOnPageNoTrace(this.page, page => page.title());
104
+ async updateTitle() {
105
+ await this._raceAgainstModalStates(async () => {
106
+ this._lastTitle = await callOnPageNoTrace(this.page, page => page.title());
107
+ });
108
+ }
109
+ lastTitle() {
110
+ return this._lastTitle;
111
+ }
112
+ isCurrentTab() {
113
+ return this === this.context.currentTab();
65
114
  }
66
115
  async waitForLoadState(state, options) {
67
116
  await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(logUnhandledError));
@@ -81,22 +130,17 @@ export class Tab {
81
130
  // on chromium, the download event is fired *after* page.goto rejects, so we wait a lil bit
82
131
  const download = await Promise.race([
83
132
  downloadEvent,
84
- new Promise(resolve => setTimeout(resolve, 1000)),
133
+ new Promise(resolve => setTimeout(resolve, 3000)),
85
134
  ]);
86
135
  if (!download)
87
136
  throw e;
137
+ // Make sure other "download" listeners are notified first.
138
+ await new Promise(resolve => setTimeout(resolve, 500));
139
+ return;
88
140
  }
89
141
  // Cap load event to 5 seconds, the page is operational at this point.
90
142
  await this.waitForLoadState('load', { timeout: 5000 });
91
143
  }
92
- hasSnapshot() {
93
- return !!this._snapshot;
94
- }
95
- snapshotOrDie() {
96
- if (!this._snapshot)
97
- throw new Error('No snapshot available');
98
- return this._snapshot;
99
- }
100
144
  consoleMessages() {
101
145
  return this._consoleMessages;
102
146
  }
@@ -104,12 +148,71 @@ export class Tab {
104
148
  return this._requests;
105
149
  }
106
150
  async captureSnapshot() {
107
- this._snapshot = await PageSnapshot.create(this.page);
151
+ let tabSnapshot;
152
+ const modalStates = await this._raceAgainstModalStates(async () => {
153
+ const snapshot = await this.page._snapshotForAI();
154
+ tabSnapshot = {
155
+ url: this.page.url(),
156
+ title: await this.page.title(),
157
+ ariaSnapshot: snapshot,
158
+ modalStates: [],
159
+ consoleMessages: [],
160
+ downloads: this._downloads,
161
+ };
162
+ });
163
+ if (tabSnapshot) {
164
+ // Assign console message late so that we did not lose any to modal state.
165
+ tabSnapshot.consoleMessages = this._recentConsoleMessages;
166
+ this._recentConsoleMessages = [];
167
+ }
168
+ return tabSnapshot ?? {
169
+ url: this.page.url(),
170
+ title: '',
171
+ ariaSnapshot: '',
172
+ modalStates,
173
+ consoleMessages: [],
174
+ downloads: [],
175
+ };
108
176
  }
109
- takeRecentConsoleMessages() {
110
- const result = this._recentConsoleMessages.slice();
111
- this._recentConsoleMessages.length = 0;
112
- return result;
177
+ _javaScriptBlocked() {
178
+ return this._modalStates.some(state => state.type === 'dialog');
179
+ }
180
+ async _raceAgainstModalStates(action) {
181
+ if (this.modalStates().length)
182
+ return this.modalStates();
183
+ const promise = new ManualPromise();
184
+ const listener = (modalState) => promise.resolve([modalState]);
185
+ this.once(TabEvents.modalState, listener);
186
+ return await Promise.race([
187
+ action().then(() => {
188
+ this.off(TabEvents.modalState, listener);
189
+ return [];
190
+ }),
191
+ promise,
192
+ ]);
193
+ }
194
+ async waitForCompletion(callback) {
195
+ await this._raceAgainstModalStates(() => waitForCompletion(this, callback));
196
+ }
197
+ async refLocator(params) {
198
+ return (await this.refLocators([params]))[0];
199
+ }
200
+ async refLocators(params) {
201
+ const snapshot = await this.page._snapshotForAI();
202
+ return params.map(param => {
203
+ if (!snapshot.includes(`[ref=${param.ref}]`))
204
+ throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`);
205
+ return this.page.locator(`aria-ref=${param.ref}`).describe(param.element);
206
+ });
207
+ }
208
+ async waitForTimeout(time) {
209
+ if (this._javaScriptBlocked()) {
210
+ await new Promise(f => setTimeout(f, time));
211
+ return;
212
+ }
213
+ await callOnPageNoTrace(this.page, page => {
214
+ return page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
215
+ });
113
216
  }
114
217
  }
115
218
  function messageToConsoleMessage(message) {
@@ -133,3 +236,14 @@ function pageErrorToConsoleMessage(errorOrValue) {
133
236
  toString: () => String(errorOrValue),
134
237
  };
135
238
  }
239
+ export function renderModalStates(context, modalStates) {
240
+ const result = ['### Modal state'];
241
+ if (modalStates.length === 0)
242
+ result.push('- There is no modal state present');
243
+ for (const state of modalStates) {
244
+ const tool = context.tools.filter(tool => 'clearsModalState' in tool).find(tool => tool.clearsModalState === state.type);
245
+ result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
246
+ }
247
+ return result;
248
+ }
249
+ const tabSymbol = Symbol('tabSymbol');
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { z } from 'zod';
17
- import { defineTool } from './tool.js';
17
+ import { defineTabTool, defineTool } from './tool.js';
18
18
  const close = defineTool({
19
19
  capability: 'core',
20
20
  schema: {
@@ -24,16 +24,13 @@ const close = defineTool({
24
24
  inputSchema: z.object({}),
25
25
  type: 'readOnly',
26
26
  },
27
- handle: async (context) => {
28
- await context.close();
29
- return {
30
- code: [`await page.close()`],
31
- captureSnapshot: false,
32
- waitForNetwork: false,
33
- };
27
+ handle: async (context, params, response) => {
28
+ await context.closeBrowserContext();
29
+ response.setIncludeTabs();
30
+ response.addCode(`await page.close()`);
34
31
  },
35
32
  });
36
- const resize = defineTool({
33
+ const resize = defineTabTool({
37
34
  capability: 'core',
38
35
  schema: {
39
36
  name: 'browser_resize',
@@ -45,21 +42,11 @@ const resize = defineTool({
45
42
  }),
46
43
  type: 'readOnly',
47
44
  },
48
- handle: async (context, params) => {
49
- const tab = context.currentTabOrDie();
50
- const code = [
51
- `// Resize browser window to ${params.width}x${params.height}`,
52
- `await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`
53
- ];
54
- const action = async () => {
45
+ handle: async (tab, params, response) => {
46
+ response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
47
+ await tab.waitForCompletion(async () => {
55
48
  await tab.page.setViewportSize({ width: params.width, height: params.height });
56
- };
57
- return {
58
- code,
59
- action,
60
- captureSnapshot: true,
61
- waitForNetwork: true
62
- };
49
+ });
63
50
  },
64
51
  });
65
52
  export default [
@@ -14,8 +14,8 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { z } from 'zod';
17
- import { defineTool } from './tool.js';
18
- const console = defineTool({
17
+ import { defineTabTool } from './tool.js';
18
+ const console = defineTabTool({
19
19
  capability: 'core',
20
20
  schema: {
21
21
  name: 'browser_console_messages',
@@ -24,19 +24,8 @@ const console = defineTool({
24
24
  inputSchema: z.object({}),
25
25
  type: 'readOnly',
26
26
  },
27
- handle: async (context) => {
28
- const messages = context.currentTabOrDie().consoleMessages();
29
- const log = messages.map(message => message.toString()).join('\n');
30
- return {
31
- code: [`// <internal code to get console messages>`],
32
- action: async () => {
33
- return {
34
- content: [{ type: 'text', text: log }]
35
- };
36
- },
37
- captureSnapshot: false,
38
- waitForNetwork: false,
39
- };
27
+ handle: async (tab, params, response) => {
28
+ tab.consoleMessages().map(message => response.addResult(message.toString()));
40
29
  },
41
30
  });
42
31
  export default [
@@ -14,8 +14,8 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { z } from 'zod';
17
- import { defineTool } from './tool.js';
18
- const handleDialog = defineTool({
17
+ import { defineTabTool } from './tool.js';
18
+ const handleDialog = defineTabTool({
19
19
  capability: 'core',
20
20
  schema: {
21
21
  name: 'browser_handle_dialog',
@@ -27,23 +27,18 @@ const handleDialog = defineTool({
27
27
  }),
28
28
  type: 'destructive',
29
29
  },
30
- handle: async (context, params) => {
31
- const dialogState = context.modalStates().find(state => state.type === 'dialog');
30
+ handle: async (tab, params, response) => {
31
+ response.setIncludeSnapshot();
32
+ const dialogState = tab.modalStates().find(state => state.type === 'dialog');
32
33
  if (!dialogState)
33
34
  throw new Error('No dialog visible');
34
- if (params.accept)
35
- await dialogState.dialog.accept(params.promptText);
36
- else
37
- await dialogState.dialog.dismiss();
38
- context.clearModalState(dialogState);
39
- const code = [
40
- `// <internal code to handle "${dialogState.dialog.type()}" dialog>`,
41
- ];
42
- return {
43
- code,
44
- captureSnapshot: true,
45
- waitForNetwork: false,
46
- };
35
+ tab.clearModalState(dialogState);
36
+ await tab.waitForCompletion(async () => {
37
+ if (params.accept)
38
+ await dialogState.dialog.accept(params.promptText);
39
+ else
40
+ await dialogState.dialog.dismiss();
41
+ });
47
42
  },
48
43
  clearsModalState: 'dialog',
49
44
  });
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { z } from 'zod';
17
- import { defineTool } from './tool.js';
17
+ import { defineTabTool } from './tool.js';
18
18
  import * as javascript from '../javascript.js';
19
19
  import { generateLocator } from './utils.js';
20
20
  const evaluateSchema = z.object({
@@ -22,7 +22,7 @@ const evaluateSchema = z.object({
22
22
  element: z.string().optional().describe('Human-readable element description used to obtain permission to interact with the element'),
23
23
  ref: z.string().optional().describe('Exact target element reference from the page snapshot'),
24
24
  });
25
- const evaluate = defineTool({
25
+ const evaluate = defineTabTool({
26
26
  capability: 'core',
27
27
  schema: {
28
28
  name: 'browser_evaluate',
@@ -31,30 +31,21 @@ const evaluate = defineTool({
31
31
  inputSchema: evaluateSchema,
32
32
  type: 'destructive',
33
33
  },
34
- handle: async (context, params) => {
35
- const tab = context.currentTabOrDie();
36
- const code = [];
34
+ handle: async (tab, params, response) => {
35
+ response.setIncludeSnapshot();
37
36
  let locator;
38
37
  if (params.ref && params.element) {
39
- const snapshot = tab.snapshotOrDie();
40
- locator = snapshot.refLocator({ ref: params.ref, element: params.element });
41
- code.push(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`);
38
+ locator = await tab.refLocator({ ref: params.ref, element: params.element });
39
+ response.addCode(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`);
42
40
  }
43
41
  else {
44
- code.push(`await page.evaluate(${javascript.quote(params.function)});`);
42
+ response.addCode(`await page.evaluate(${javascript.quote(params.function)});`);
45
43
  }
46
- return {
47
- code,
48
- action: async () => {
49
- const receiver = locator ?? tab.page;
50
- const result = await receiver._evaluateFunction(params.function);
51
- return {
52
- content: [{ type: 'text', text: '- Result: ' + (JSON.stringify(result, null, 2) || 'undefined') }],
53
- };
54
- },
55
- captureSnapshot: false,
56
- waitForNetwork: false,
57
- };
44
+ await tab.waitForCompletion(async () => {
45
+ const receiver = locator ?? tab.page;
46
+ const result = await receiver._evaluateFunction(params.function);
47
+ response.addResult(JSON.stringify(result, null, 2) || 'undefined');
48
+ });
58
49
  },
59
50
  });
60
51
  export default [
@@ -14,8 +14,8 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { z } from 'zod';
17
- import { defineTool } from './tool.js';
18
- const uploadFile = defineTool({
17
+ import { defineTabTool } from './tool.js';
18
+ const uploadFile = defineTabTool({
19
19
  capability: 'core',
20
20
  schema: {
21
21
  name: 'browser_file_upload',
@@ -26,23 +26,16 @@ const uploadFile = defineTool({
26
26
  }),
27
27
  type: 'destructive',
28
28
  },
29
- handle: async (context, params) => {
30
- const modalState = context.modalStates().find(state => state.type === 'fileChooser');
29
+ handle: async (tab, params, response) => {
30
+ response.setIncludeSnapshot();
31
+ const modalState = tab.modalStates().find(state => state.type === 'fileChooser');
31
32
  if (!modalState)
32
33
  throw new Error('No file chooser visible');
33
- const code = [
34
- `// <internal code to chose files ${params.paths.join(', ')}`,
35
- ];
36
- const action = async () => {
34
+ response.addCode(`await fileChooser.setFiles(${JSON.stringify(params.paths)})`);
35
+ tab.clearModalState(modalState);
36
+ await tab.waitForCompletion(async () => {
37
37
  await modalState.fileChooser.setFiles(params.paths);
38
- context.clearModalState(modalState);
39
- };
40
- return {
41
- code,
42
- action,
43
- captureSnapshot: true,
44
- waitForNetwork: true,
45
- };
38
+ });
46
39
  },
47
40
  clearsModalState: 'fileChooser',
48
41
  });
@@ -15,9 +15,9 @@
15
15
  */
16
16
  import { fork } from 'child_process';
17
17
  import path from 'path';
18
+ import { fileURLToPath } from 'url';
18
19
  import { z } from 'zod';
19
20
  import { defineTool } from './tool.js';
20
- import { fileURLToPath } from 'node:url';
21
21
  const install = defineTool({
22
22
  capability: 'core-install',
23
23
  schema: {
@@ -27,7 +27,7 @@ const install = defineTool({
27
27
  inputSchema: z.object({}),
28
28
  type: 'destructive',
29
29
  },
30
- handle: async (context) => {
30
+ handle: async (context, params, response) => {
31
31
  const channel = context.config.browser?.launchOptions?.channel ?? context.config.browser?.browserName ?? 'chrome';
32
32
  const cliUrl = import.meta.resolve('playwright/package.json');
33
33
  const cliPath = path.join(fileURLToPath(cliUrl), '..', 'cli.js');
@@ -45,11 +45,7 @@ const install = defineTool({
45
45
  reject(new Error(`Failed to install browser: ${output.join('')}`));
46
46
  });
47
47
  });
48
- return {
49
- code: [`// Browser ${channel} installed`],
50
- captureSnapshot: false,
51
- waitForNetwork: false,
52
- };
48
+ response.setIncludeTabs();
53
49
  },
54
50
  });
55
51
  export default [
@@ -14,11 +14,11 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { z } from 'zod';
17
- import { defineTool } from './tool.js';
17
+ import { defineTabTool } from './tool.js';
18
18
  import { elementSchema } from './snapshot.js';
19
19
  import { generateLocator } from './utils.js';
20
20
  import * as javascript from '../javascript.js';
21
- const pressKey = defineTool({
21
+ const pressKey = defineTabTool({
22
22
  capability: 'core',
23
23
  schema: {
24
24
  name: 'browser_press_key',
@@ -29,19 +29,13 @@ const pressKey = defineTool({
29
29
  }),
30
30
  type: 'destructive',
31
31
  },
32
- handle: async (context, params) => {
33
- const tab = context.currentTabOrDie();
34
- const code = [
35
- `// Press ${params.key}`,
36
- `await page.keyboard.press('${params.key}');`,
37
- ];
38
- const action = () => tab.page.keyboard.press(params.key);
39
- return {
40
- code,
41
- action,
42
- captureSnapshot: true,
43
- waitForNetwork: true
44
- };
32
+ handle: async (tab, params, response) => {
33
+ response.setIncludeSnapshot();
34
+ response.addCode(`// Press ${params.key}`);
35
+ response.addCode(`await page.keyboard.press('${params.key}');`);
36
+ await tab.waitForCompletion(async () => {
37
+ await tab.page.keyboard.press(params.key);
38
+ });
45
39
  },
46
40
  });
47
41
  const typeSchema = elementSchema.extend({
@@ -49,7 +43,7 @@ const typeSchema = elementSchema.extend({
49
43
  submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'),
50
44
  slowly: z.boolean().optional().describe('Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.'),
51
45
  });
52
- const type = defineTool({
46
+ const type = defineTabTool({
53
47
  capability: 'core',
54
48
  schema: {
55
49
  name: 'browser_type',
@@ -58,32 +52,24 @@ const type = defineTool({
58
52
  inputSchema: typeSchema,
59
53
  type: 'destructive',
60
54
  },
61
- handle: async (context, params) => {
62
- const snapshot = context.currentTabOrDie().snapshotOrDie();
63
- const locator = snapshot.refLocator(params);
64
- const code = [];
65
- const steps = [];
66
- if (params.slowly) {
67
- code.push(`// Press "${params.text}" sequentially into "${params.element}"`);
68
- code.push(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
69
- steps.push(() => locator.pressSequentially(params.text));
70
- }
71
- else {
72
- code.push(`// Fill "${params.text}" into "${params.element}"`);
73
- code.push(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
74
- steps.push(() => locator.fill(params.text));
75
- }
76
- if (params.submit) {
77
- code.push(`// Submit text`);
78
- code.push(`await page.${await generateLocator(locator)}.press('Enter');`);
79
- steps.push(() => locator.press('Enter'));
80
- }
81
- return {
82
- code,
83
- action: () => steps.reduce((acc, step) => acc.then(step), Promise.resolve()),
84
- captureSnapshot: true,
85
- waitForNetwork: true,
86
- };
55
+ handle: async (tab, params, response) => {
56
+ const locator = await tab.refLocator(params);
57
+ await tab.waitForCompletion(async () => {
58
+ if (params.slowly) {
59
+ response.setIncludeSnapshot();
60
+ response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
61
+ await locator.pressSequentially(params.text);
62
+ }
63
+ else {
64
+ response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
65
+ await locator.fill(params.text);
66
+ }
67
+ if (params.submit) {
68
+ response.setIncludeSnapshot();
69
+ response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`);
70
+ await locator.press('Enter');
71
+ }
72
+ });
87
73
  },
88
74
  });
89
75
  export default [