@pedropaulovc/playwright 1.59.0-next → 1.59.0-next.3

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.
@@ -95,8 +95,7 @@ class FullConfigInternal {
95
95
  threshold: 3e5
96
96
  /* 5 minutes */
97
97
  }),
98
- // @ts-expect-error runAgents is hidden
99
- runAgents: takeFirst(configCLIOverrides.runAgents, "none"),
98
+ runAgents: takeFirst(configCLIOverrides.runAgents, userConfig.runAgents, "none"),
100
99
  shard: takeFirst(configCLIOverrides.shard, userConfig.shard, null),
101
100
  tags: globalTags,
102
101
  updateSnapshots: takeFirst(configCLIOverrides.updateSnapshots, userConfig.updateSnapshots, "missing"),
@@ -162,7 +161,7 @@ class FullProjectInternal {
162
161
  testDir,
163
162
  snapshotDir: takeFirst(pathResolve(configDir, projectConfig.snapshotDir), pathResolve(configDir, config.snapshotDir), testDir),
164
163
  testIgnore: takeFirst(projectConfig.testIgnore, config.testIgnore, []),
165
- testMatch: takeFirst(projectConfig.testMatch, config.testMatch, "**/*.@(spec|test).?(c|m)[jt]s?(x)"),
164
+ testMatch: takeFirst(projectConfig.testMatch, config.testMatch, "**/*.@(spec|test).{md,?(c|m)[jt]s?(x)}"),
166
165
  timeout: takeFirst(configCLIOverrides.debug ? 0 : void 0, configCLIOverrides.timeout, projectConfig.timeout, config.timeout, defaultTimeout),
167
166
  use: (0, import_util.mergeObjects)(config.use, projectConfig.use, configCLIOverrides.use),
168
167
  dependencies: projectConfig.dependencies || [],
package/lib/index.js CHANGED
@@ -138,7 +138,7 @@ const playwrightFixtures = {
138
138
  }, { option: true, box: true }],
139
139
  serviceWorkers: [({ contextOptions }, use) => use(contextOptions.serviceWorkers ?? "allow"), { option: true, box: true }],
140
140
  contextOptions: [{}, { option: true, box: true }],
141
- agentOptions: [void 0, { option: true, box: true }],
141
+ agentOptions: [({}, use) => use(void 0), { option: true, box: true }],
142
142
  _combinedContextOptions: [async ({
143
143
  acceptDownloads,
144
144
  bypassCSP,
@@ -459,7 +459,6 @@ const baseFullConfig = {
459
459
  tags: [],
460
460
  updateSnapshots: "missing",
461
461
  updateSourceMethod: "patch",
462
- // @ts-expect-error runAgents is hidden
463
462
  runAgents: "none",
464
463
  version: "",
465
464
  workers: 0,
@@ -60,20 +60,19 @@ class Response {
60
60
  return new Response(++Response._ordinal, context, toolName, toolArgs);
61
61
  }
62
62
  addTextResult(result) {
63
- this._results.push({ text: result });
63
+ this._results.push({ title: "", data: result });
64
64
  }
65
- async addResult(result) {
66
- if (result.data && !result.suggestedFilename)
67
- result.suggestedFilename = (0, import_utils.dateAsFileName)(result.ext ?? "bin");
68
- if (this._context.config.outputMode === "file") {
69
- if (!result.suggestedFilename)
70
- result.suggestedFilename = (0, import_utils.dateAsFileName)(result.ext ?? (result.text ? "txt" : "bin"));
65
+ async addResult(title, data, file) {
66
+ let filename;
67
+ if (!file.suggestedFilename) {
68
+ if (typeof data !== "string")
69
+ filename = (0, import_utils.dateAsFileName)(file.prefix, file.ext);
70
+ if (this._context.config.outputMode === "file")
71
+ filename = (0, import_utils.dateAsFileName)(file.prefix, file.ext);
72
+ } else {
73
+ filename = await this._context.outputFile(file.suggestedFilename, { origin: "llm", title });
71
74
  }
72
- const entry = { text: result.text, data: result.data, title: result.title };
73
- if (result.suggestedFilename)
74
- entry.filename = await this._context.outputFile(result.suggestedFilename, { origin: "llm", title: result.title ?? "Saved result" });
75
- this._results.push(entry);
76
- return { fileName: entry.filename };
75
+ this._results.push({ data, title, filename });
77
76
  }
78
77
  addError(error) {
79
78
  this._errors.push(error);
@@ -109,12 +108,12 @@ class Response {
109
108
  for (const result of this._results) {
110
109
  if (result.filename) {
111
110
  text.push(`- [${result.title}](${rootPath ? import_path.default.relative(rootPath, result.filename) : result.filename})`);
112
- if (result.data)
111
+ if (typeof result.data === "string")
112
+ await import_fs.default.promises.writeFile(result.filename, this._redactText(result.data), "utf-8");
113
+ else
113
114
  await import_fs.default.promises.writeFile(result.filename, result.data);
114
- else if (result.text)
115
- await import_fs.default.promises.writeFile(result.filename, this._redactText(result.text));
116
- } else if (result.text) {
117
- text.push(result.text);
115
+ } else if (typeof result.data === "string" && result.data.trim()) {
116
+ text.push(result.data);
118
117
  }
119
118
  }
120
119
  }
@@ -134,16 +133,16 @@ class Response {
134
133
  }
135
134
  if (tabSnapshot?.modalStates.length) {
136
135
  const text = addSection("Modal state");
137
- text.push(...(0, import_tab.renderModalStates)(tabSnapshot.modalStates));
136
+ text.push(...(0, import_tab.renderModalStates)(this._context.config, tabSnapshot.modalStates));
138
137
  }
139
138
  if (tabSnapshot && this._includeSnapshot === "full") {
140
139
  let fileName;
141
140
  if (this._includeSnapshotFileName)
142
141
  fileName = await this._context.outputFile(this._includeSnapshotFileName, { origin: "llm", title: "Saved snapshot" });
143
142
  else if (this._context.config.outputMode === "file")
144
- fileName = await this._context.outputFile(`snapshot-${this._ordinal}.yml`, { origin: "code", title: "Saved snapshot" });
143
+ fileName = await this._context.outputFile((0, import_utils.dateAsFileName)("snapshot", "yml"), { origin: "code", title: "Saved snapshot" });
145
144
  if (fileName) {
146
- await import_fs.default.promises.writeFile(fileName, tabSnapshot.ariaSnapshot);
145
+ await import_fs.default.promises.writeFile(fileName, tabSnapshot.ariaSnapshot, "utf-8");
147
146
  const text = addSection("Snapshot");
148
147
  text.push(`- File: ${rootPath ? import_path.default.relative(rootPath, fileName) : fileName}`);
149
148
  } else {
@@ -211,7 +210,7 @@ function renderTabMarkdown(tab) {
211
210
  }
212
211
  function renderTabsMarkdown(tabs) {
213
212
  if (!tabs.length)
214
- return ['No open tabs. Use the "browser_navigate" tool to navigate to a page first.'];
213
+ return ["No open tabs. Navigate to a URL to create one."];
215
214
  const lines = [];
216
215
  for (let i = 0; i < tabs.length; i++) {
217
216
  const tab = tabs[i];
@@ -57,7 +57,7 @@ class Tab extends import_events.EventEmitter {
57
57
  type: "fileChooser",
58
58
  description: "File chooser",
59
59
  fileChooser: chooser,
60
- clearedBy: import_files.uploadFile.schema.name
60
+ clearedBy: { tool: import_files.uploadFile.schema.name, skill: "upload" }
61
61
  });
62
62
  });
63
63
  page.on("dialog", (dialog) => this._dialogShown(dialog));
@@ -112,7 +112,7 @@ class Tab extends import_events.EventEmitter {
112
112
  type: "dialog",
113
113
  description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
114
114
  dialog,
115
- clearedBy: import_dialogs.handleDialog.schema.name
115
+ clearedBy: { tool: import_dialogs.handleDialog.schema.name, skill: "dialog-accept or dialog-dismiss" }
116
116
  });
117
117
  }
118
118
  async _downloadStarted(download) {
@@ -299,12 +299,12 @@ function pageErrorToConsoleMessage(errorOrValue) {
299
299
  toString: () => String(errorOrValue)
300
300
  };
301
301
  }
302
- function renderModalStates(modalStates) {
302
+ function renderModalStates(config, modalStates) {
303
303
  const result = [];
304
304
  if (modalStates.length === 0)
305
305
  result.push("- There is no modal state present");
306
306
  for (const state of modalStates)
307
- result.push(`- [${state.description}]: can be handled by the "${state.clearedBy}" tool`);
307
+ result.push(`- [${state.description}]: can be handled by ${config.skillMode ? state.clearedBy.skill : state.clearedBy.tool}`);
308
308
  return result;
309
309
  }
310
310
  const consoleMessageLevels = ["error", "warning", "info", "debug"];
@@ -54,9 +54,7 @@ const resize = (0, import_tool.defineTabTool)({
54
54
  },
55
55
  handle: async (tab, params, response) => {
56
56
  response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
57
- await tab.waitForCompletion(async () => {
58
- await tab.page.setViewportSize({ width: params.width, height: params.height });
59
- });
57
+ await tab.page.setViewportSize({ width: params.width, height: params.height });
60
58
  }
61
59
  });
62
60
  var common_default = [
@@ -38,7 +38,7 @@ const console = (0, import_tool.defineTabTool)({
38
38
  handle: async (tab, params, response) => {
39
39
  const messages = await tab.consoleMessages(params.level);
40
40
  const text = messages.map((message) => message.toString()).join("\n");
41
- await response.addResult({ text, suggestedFilename: params.filename });
41
+ await response.addResult("Console", text, { prefix: "console", ext: "log", suggestedFilename: params.filename });
42
42
  }
43
43
  });
44
44
  const consoleClear = (0, import_tool.defineTabTool)({
@@ -37,7 +37,6 @@ const handleDialog = (0, import_tool.defineTabTool)({
37
37
  type: "action"
38
38
  },
39
39
  handle: async (tab, params, response) => {
40
- response.setIncludeSnapshot();
41
40
  const dialogState = tab.modalStates().find((state) => state.type === "dialog");
42
41
  if (!dialogState)
43
42
  throw new Error("No dialog visible");
@@ -27,8 +27,7 @@ var import_tool = require("./tool");
27
27
  const evaluateSchema = import_mcpBundle.z.object({
28
28
  function: import_mcpBundle.z.string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
29
29
  element: import_mcpBundle.z.string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
30
- ref: import_mcpBundle.z.string().optional().describe("Exact target element reference from the page snapshot"),
31
- filename: import_mcpBundle.z.string().optional().describe("Filename to save the result to. If not provided, result is returned as JSON string.")
30
+ ref: import_mcpBundle.z.string().optional().describe("Exact target element reference from the page snapshot")
32
31
  });
33
32
  const evaluate = (0, import_tool.defineTabTool)({
34
33
  capability: "core",
@@ -40,10 +39,11 @@ const evaluate = (0, import_tool.defineTabTool)({
40
39
  type: "action"
41
40
  },
42
41
  handle: async (tab, params, response) => {
43
- response.setIncludeSnapshot();
44
42
  let locator;
45
- if (params.ref && params.element) {
46
- locator = await tab.refLocator({ ref: params.ref, element: params.element });
43
+ if (!params.function.includes("=>"))
44
+ params.function = `() => (${params.function})`;
45
+ if (params.ref) {
46
+ locator = await tab.refLocator({ ref: params.ref, element: params.element || "element" });
47
47
  response.addCode(`await page.${locator.resolved}.evaluate(${(0, import_utils.escapeWithQuotes)(params.function)});`);
48
48
  } else {
49
49
  response.addCode(`await page.evaluate(${(0, import_utils.escapeWithQuotes)(params.function)});`);
@@ -52,7 +52,7 @@ const evaluate = (0, import_tool.defineTabTool)({
52
52
  const receiver = locator?.locator ?? tab.page;
53
53
  const result = await receiver._evaluateFunction(params.function);
54
54
  const text = JSON.stringify(result, null, 2) || "undefined";
55
- await response.addResult({ text, suggestedFilename: params.filename });
55
+ response.addTextResult(text);
56
56
  });
57
57
  }
58
58
  });
@@ -38,7 +38,14 @@ const press = (0, import_tool.defineTabTool)({
38
38
  handle: async (tab, params, response) => {
39
39
  response.addCode(`// Press ${params.key}`);
40
40
  response.addCode(`await page.keyboard.press('${params.key}');`);
41
- await tab.page.keyboard.press(params.key);
41
+ if (params.key === "Enter") {
42
+ response.setIncludeSnapshot();
43
+ await tab.waitForCompletion(async () => {
44
+ await tab.page.keyboard.press("Enter");
45
+ });
46
+ } else {
47
+ await tab.page.keyboard.press(params.key);
48
+ }
42
49
  }
43
50
  });
44
51
  const pressSequentially = (0, import_tool.defineTabTool)({
@@ -43,7 +43,7 @@ const requests = (0, import_tool.defineTabTool)({
43
43
  if (rendered)
44
44
  text.push(rendered);
45
45
  }
46
- await response.addResult({ text: text.join("\n"), suggestedFilename: params.filename });
46
+ await response.addResult("Network", text.join("\n"), { prefix: "network", ext: "log", suggestedFilename: params.filename });
47
47
  }
48
48
  });
49
49
  const networkClear = (0, import_tool.defineTabTool)({
@@ -39,8 +39,8 @@ const pdf = (0, import_tool.defineTabTool)({
39
39
  },
40
40
  handle: async (tab, params, response) => {
41
41
  const data = await tab.page.pdf();
42
- const suggestedFilename = params.filename ?? (0, import_utils2.dateAsFileName)("pdf");
43
- await response.addResult({ data, title: "Page as pdf", suggestedFilename });
42
+ const suggestedFilename = params.filename ?? (0, import_utils2.dateAsFileName)("page", "pdf");
43
+ await response.addResult("Page as pdf", data, { prefix: "page", ext: "pdf", suggestedFilename });
44
44
  response.addCode(`await page.pdf(${(0, import_utils.formatObject)({ path: suggestedFilename })});`);
45
45
  }
46
46
  });
@@ -36,8 +36,7 @@ var import_utils = require("playwright-core/lib/utils");
36
36
  var import_mcpBundle = require("playwright-core/lib/mcpBundle");
37
37
  var import_tool = require("./tool");
38
38
  const codeSchema = import_mcpBundle.z.object({
39
- code: import_mcpBundle.z.string().describe(`A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: \`async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }\``),
40
- filename: import_mcpBundle.z.string().optional().describe("Filename to save the result to. If not provided, result is returned as JSON string.")
39
+ code: import_mcpBundle.z.string().describe(`A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: \`async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }\``)
41
40
  });
42
41
  const runCode = (0, import_tool.defineTabTool)({
43
42
  capability: "core",
@@ -49,7 +48,6 @@ const runCode = (0, import_tool.defineTabTool)({
49
48
  type: "action"
50
49
  },
51
50
  handle: async (tab, params, response) => {
52
- response.setIncludeSnapshot();
53
51
  response.addCode(`await (${params.code})(page);`);
54
52
  const __end__ = new import_utils.ManualPromise();
55
53
  const context = {
@@ -69,7 +67,7 @@ const runCode = (0, import_tool.defineTabTool)({
69
67
  await import_vm.default.runInContext(snippet, context);
70
68
  const result = await __end__;
71
69
  if (typeof result === "string")
72
- await response.addResult({ text: result, suggestedFilename: params.filename });
70
+ response.addTextResult(result);
73
71
  });
74
72
  }
75
73
  });
@@ -45,8 +45,6 @@ const screenshot = (0, import_tool.defineTabTool)({
45
45
  type: "readOnly"
46
46
  },
47
47
  handle: async (tab, params, response) => {
48
- if (!!params.element !== !!params.ref)
49
- throw new Error("Both element and ref must be provided or neither.");
50
48
  if (params.fullPage && params.ref)
51
49
  throw new Error("fullPage cannot be used with element screenshots.");
52
50
  const fileType = params.type || "png";
@@ -56,17 +54,16 @@ const screenshot = (0, import_tool.defineTabTool)({
56
54
  scale: "css",
57
55
  ...params.fullPage !== void 0 && { fullPage: params.fullPage }
58
56
  };
59
- const isElementScreenshot = params.element && params.ref;
60
- const screenshotTarget = isElementScreenshot ? params.element : params.fullPage ? "full page" : "viewport";
57
+ const screenshotTarget = params.ref ? params.element || "element" : params.fullPage ? "full page" : "viewport";
61
58
  const ref = params.ref ? await tab.refLocator({ element: params.element || "", ref: params.ref }) : null;
62
59
  const data = ref ? await ref.locator.screenshot(options) : await tab.page.screenshot(options);
63
- const fileName = params.filename || (0, import_utils3.dateAsFileName)(fileType);
64
- response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
60
+ const suggestedFilename = params.filename || (0, import_utils3.dateAsFileName)(ref ? "element" : "page", fileType);
61
+ response.addCode(`// Screenshot ${screenshotTarget} and save it as ${suggestedFilename}`);
65
62
  if (ref)
66
- response.addCode(`await page.${ref.resolved}.screenshot(${(0, import_utils2.formatObject)({ ...options, path: fileName })});`);
63
+ response.addCode(`await page.${ref.resolved}.screenshot(${(0, import_utils2.formatObject)({ ...options, path: suggestedFilename })});`);
67
64
  else
68
- response.addCode(`await page.screenshot(${(0, import_utils2.formatObject)({ ...options, path: fileName })});`);
69
- await response.addResult({ data, title: `Screenshot of ${screenshotTarget}`, suggestedFilename: fileName });
65
+ response.addCode(`await page.screenshot(${(0, import_utils2.formatObject)({ ...options, path: suggestedFilename })});`);
66
+ await response.addResult(`Screenshot of ${screenshotTarget}`, data, { prefix: ref ? "element" : "page", ext: fileType, suggestedFilename });
70
67
  response.addImage({
71
68
  contentType: fileType === "png" ? "image/png" : "image/jpeg",
72
69
  data: scaleImageToFitMessage(data, fileType)
@@ -170,8 +170,9 @@ const check = (0, import_tool.defineTabTool)({
170
170
  type: "input"
171
171
  },
172
172
  handle: async (tab, params, response) => {
173
- const { resolved } = await tab.refLocator(params);
173
+ const { locator, resolved } = await tab.refLocator(params);
174
174
  response.addCode(`await page.${resolved}.check();`);
175
+ await locator.check();
175
176
  }
176
177
  });
177
178
  const uncheck = (0, import_tool.defineTabTool)({
@@ -185,8 +186,9 @@ const uncheck = (0, import_tool.defineTabTool)({
185
186
  type: "input"
186
187
  },
187
188
  handle: async (tab, params, response) => {
188
- const { resolved } = await tab.refLocator(params);
189
+ const { locator, resolved } = await tab.refLocator(params);
189
190
  response.addCode(`await page.${resolved}.uncheck();`);
191
+ await locator.uncheck();
190
192
  }
191
193
  });
192
194
  var snapshot_default = [
@@ -62,9 +62,9 @@ async function waitForCompletion(tab, callback) {
62
62
  async function callOnPageNoTrace(page, callback) {
63
63
  return await page._wrapApiCall(() => callback(page), { internal: true });
64
64
  }
65
- function dateAsFileName(extension) {
65
+ function dateAsFileName(prefix, extension) {
66
66
  const date = /* @__PURE__ */ new Date();
67
- return `page-${date.toISOString().replace(/[:.]/g, "-")}.${extension}`;
67
+ return `${prefix}-${date.toISOString().replace(/[:.]/g, "-")}.${extension}`;
68
68
  }
69
69
  function eventWaiter(page, event, timeout) {
70
70
  const disposables = [];
@@ -73,6 +73,7 @@ Please run the command below. It will install a local copy of ffmpeg and will no
73
73
  return;
74
74
  }
75
75
  if (options.daemon) {
76
+ config.skillMode = true;
76
77
  config.outputDir = import_path.default.join(process.cwd(), ".playwright-cli");
77
78
  config.outputMode = "file";
78
79
  config.codegen = "none";
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: playwright-cli
3
+ description: Automates browser interactions for web testing, form filling, screenshots, and data extraction. Use when the user needs to navigate websites, interact with web pages, fill forms, take screenshots, test web applications, or extract information from web pages.
4
+ allowed-tools: Bash(playwright-cli:*)
5
+ ---
6
+
7
+ # Browser Automation with playwright-cli
8
+
9
+ ## Quick start
10
+
11
+ ```bash
12
+ playwright-cli open https://playwright.dev
13
+ playwright-cli click e15
14
+ playwright-cli type "page.click"
15
+ playwright-cli press Enter
16
+ ```
17
+
18
+ ## Core workflow
19
+
20
+ 1. Navigate: `playwright-cli open https://example.com`
21
+ 2. Interact using refs from the snapshot
22
+ 3. Re-snapshot after significant changes
23
+
24
+ ## Commands
25
+
26
+ ### Core
27
+
28
+ ```bash
29
+ playwright-cli open https://example.com/
30
+ playwright-cli close
31
+ playwright-cli type "search query"
32
+ playwright-cli click e3
33
+ playwright-cli dblclick e7
34
+ playwright-cli fill e5 "user@example.com"
35
+ playwright-cli drag e2 e8
36
+ playwright-cli hover e4
37
+ playwright-cli select e9 "option-value"
38
+ playwright-cli upload ./document.pdf
39
+ playwright-cli check e12
40
+ playwright-cli uncheck e12
41
+ playwright-cli snapshot
42
+ playwright-cli eval "document.title"
43
+ playwright-cli eval "el => el.textContent" e5
44
+ playwright-cli dialog-accept
45
+ playwright-cli dialog-accept "confirmation text"
46
+ playwright-cli dialog-dismiss
47
+ playwright-cli resize 1920 1080
48
+ ```
49
+
50
+ ### Navigation
51
+
52
+ ```bash
53
+ playwright-cli go-back
54
+ playwright-cli go-forward
55
+ playwright-cli reload
56
+ ```
57
+
58
+ ### Keyboard
59
+
60
+ ```bash
61
+ playwright-cli press Enter
62
+ playwright-cli press ArrowDown
63
+ playwright-cli keydown Shift
64
+ playwright-cli keyup Shift
65
+ ```
66
+
67
+ ### Mouse
68
+
69
+ ```bash
70
+ playwright-cli mousemove 150 300
71
+ playwright-cli mousedown
72
+ playwright-cli mousedown right
73
+ playwright-cli mouseup
74
+ playwright-cli mouseup right
75
+ playwright-cli mousewheel 0 100
76
+ ```
77
+
78
+ ### Save as
79
+
80
+ ```bash
81
+ playwright-cli screenshot
82
+ playwright-cli screenshot e5
83
+ playwright-cli pdf
84
+ ```
85
+
86
+ ### Tabs
87
+
88
+ ```bash
89
+ playwright-cli tab-list
90
+ playwright-cli tab-new
91
+ playwright-cli tab-new https://example.com/page
92
+ playwright-cli tab-close
93
+ playwright-cli tab-close 2
94
+ playwright-cli tab-select 0
95
+ ```
96
+
97
+ ### DevTools
98
+
99
+ ```bash
100
+ playwright-cli console
101
+ playwright-cli console warning
102
+ playwright-cli network
103
+ playwright-cli run-code "await page.waitForTimeout(1000)"
104
+ playwright-cli tracing-start
105
+ playwright-cli tracing-stop
106
+ ```
107
+
108
+ ### Sessions
109
+
110
+ ```bash
111
+ playwright-cli --session=mysession open example.com
112
+ playwright-cli --session=mysession click e6
113
+ playwright-cli session-list
114
+ playwright-cli session-stop mysession
115
+ playwright-cli session-stop-all
116
+ playwright-cli session-delete
117
+ playwright-cli session-delete mysession
118
+ ```
119
+
120
+ ## Example: Form submission
121
+
122
+ ```bash
123
+ playwright-cli open https://example.com/form
124
+ playwright-cli snapshot
125
+
126
+ playwright-cli fill e1 "user@example.com"
127
+ playwright-cli fill e2 "password123"
128
+ playwright-cli click e3
129
+ playwright-cli snapshot
130
+ ```
131
+
132
+ ## Example: Multi-tab workflow
133
+
134
+ ```bash
135
+ playwright-cli open https://example.com
136
+ playwright-cli tab-new https://example.com/other
137
+ playwright-cli tab-list
138
+ playwright-cli tab-select 0
139
+ playwright-cli snapshot
140
+ ```
141
+
142
+ ## Example: Debugging with DevTools
143
+
144
+ ```bash
145
+ playwright-cli open https://example.com
146
+ playwright-cli click e4
147
+ playwright-cli fill e7 "test"
148
+ playwright-cli console
149
+ playwright-cli network
150
+ ```
151
+
152
+ ```bash
153
+ playwright-cli open https://example.com
154
+ playwright-cli tracing-start
155
+ playwright-cli click e4
156
+ playwright-cli fill e7 "test"
157
+ playwright-cli tracing-stop
158
+ ```