stably 4.9.0 → 4.10.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.
Files changed (47) hide show
  1. package/dist/index.mjs +1 -1
  2. package/dist/stably-plugin-cli/.claude-plugin/plugin.json +5 -0
  3. package/dist/stably-plugin-cli/skills/bash-commands/SKILL.md +65 -0
  4. package/dist/stably-plugin-cli/skills/browser-interaction-guide/SKILL.md +144 -0
  5. package/dist/stably-plugin-cli/skills/bulk-test-handling/SKILL.md +104 -0
  6. package/dist/stably-plugin-cli/skills/debugging-test-failures/SKILL.md +146 -0
  7. package/dist/{stably-plugin → stably-plugin-cli}/skills/playwright-best-practices/SKILL.md +11 -5
  8. package/dist/stably-plugin-cli/skills/playwright-config-auth/SKILL.md +217 -0
  9. package/dist/stably-plugin-cli/skills/stably-sdk-reference/SKILL.md +307 -0
  10. package/dist/stably-plugin-cli/skills/test-creation-workflow/SKILL.md +311 -0
  11. package/package.json +2 -2
  12. package/dist/stably-plugin/.claude-plugin/plugin.json +0 -5
  13. package/dist/stably-plugin/skills/playwright-best-practices/references/accessibility.md +0 -359
  14. package/dist/stably-plugin/skills/playwright-best-practices/references/annotations.md +0 -526
  15. package/dist/stably-plugin/skills/playwright-best-practices/references/assertions-waiting.md +0 -361
  16. package/dist/stably-plugin/skills/playwright-best-practices/references/browser-apis.md +0 -391
  17. package/dist/stably-plugin/skills/playwright-best-practices/references/browser-extensions.md +0 -506
  18. package/dist/stably-plugin/skills/playwright-best-practices/references/canvas-webgl.md +0 -493
  19. package/dist/stably-plugin/skills/playwright-best-practices/references/ci-cd.md +0 -407
  20. package/dist/stably-plugin/skills/playwright-best-practices/references/clock-mocking.md +0 -364
  21. package/dist/stably-plugin/skills/playwright-best-practices/references/component-testing.md +0 -500
  22. package/dist/stably-plugin/skills/playwright-best-practices/references/console-errors.md +0 -420
  23. package/dist/stably-plugin/skills/playwright-best-practices/references/debugging.md +0 -491
  24. package/dist/stably-plugin/skills/playwright-best-practices/references/electron.md +0 -509
  25. package/dist/stably-plugin/skills/playwright-best-practices/references/error-testing.md +0 -360
  26. package/dist/stably-plugin/skills/playwright-best-practices/references/file-operations.md +0 -375
  27. package/dist/stably-plugin/skills/playwright-best-practices/references/fixtures-hooks.md +0 -417
  28. package/dist/stably-plugin/skills/playwright-best-practices/references/flaky-tests.md +0 -494
  29. package/dist/stably-plugin/skills/playwright-best-practices/references/global-setup.md +0 -434
  30. package/dist/stably-plugin/skills/playwright-best-practices/references/i18n.md +0 -508
  31. package/dist/stably-plugin/skills/playwright-best-practices/references/iframes.md +0 -403
  32. package/dist/stably-plugin/skills/playwright-best-practices/references/locators.md +0 -242
  33. package/dist/stably-plugin/skills/playwright-best-practices/references/mobile-testing.md +0 -409
  34. package/dist/stably-plugin/skills/playwright-best-practices/references/multi-context.md +0 -288
  35. package/dist/stably-plugin/skills/playwright-best-practices/references/multi-user.md +0 -393
  36. package/dist/stably-plugin/skills/playwright-best-practices/references/network-advanced.md +0 -452
  37. package/dist/stably-plugin/skills/playwright-best-practices/references/page-object-model.md +0 -315
  38. package/dist/stably-plugin/skills/playwright-best-practices/references/performance-testing.md +0 -476
  39. package/dist/stably-plugin/skills/playwright-best-practices/references/performance.md +0 -453
  40. package/dist/stably-plugin/skills/playwright-best-practices/references/projects-dependencies.md +0 -456
  41. package/dist/stably-plugin/skills/playwright-best-practices/references/security-testing.md +0 -430
  42. package/dist/stably-plugin/skills/playwright-best-practices/references/service-workers.md +0 -504
  43. package/dist/stably-plugin/skills/playwright-best-practices/references/test-coverage.md +0 -495
  44. package/dist/stably-plugin/skills/playwright-best-practices/references/test-data.md +0 -492
  45. package/dist/stably-plugin/skills/playwright-best-practices/references/test-organization.md +0 -361
  46. package/dist/stably-plugin/skills/playwright-best-practices/references/third-party.md +0 -464
  47. package/dist/stably-plugin/skills/playwright-best-practices/references/websockets.md +0 -403
@@ -1,403 +0,0 @@
1
- # iFrame Testing
2
-
3
- ## Table of Contents
4
-
5
- 1. [Basic iFrame Access](#basic-iframe-access)
6
- 2. [Cross-Origin iFrames](#cross-origin-iframes)
7
- 3. [Nested iFrames](#nested-iframes)
8
- 4. [Dynamic iFrames](#dynamic-iframes)
9
- 5. [iFrame Navigation](#iframe-navigation)
10
- 6. [Common Patterns](#common-patterns)
11
-
12
- ## Basic iFrame Access
13
-
14
- ### Using frameLocator
15
-
16
- ```typescript
17
- // Access iframe by selector
18
- const frame = page.frameLocator("iframe#payment");
19
- await frame.getByRole("button", { name: "Pay" }).click();
20
-
21
- // Access by name attribute
22
- const namedFrame = page.frameLocator('iframe[name="checkout"]');
23
- await namedFrame.getByLabel("Card number").fill("4242424242424242");
24
-
25
- // Access by title
26
- const titledFrame = page.frameLocator('iframe[title="Payment Form"]');
27
-
28
- // Access by src (partial match)
29
- const srcFrame = page.frameLocator('iframe[src*="stripe.com"]');
30
- ```
31
-
32
- ### Frame vs FrameLocator
33
-
34
- ```typescript
35
- // frameLocator - for locator-based operations (recommended)
36
- const frameLocator = page.frameLocator("#my-iframe");
37
- await frameLocator.getByRole("button").click();
38
-
39
- // frame() - for Frame object operations (navigation, evaluation)
40
- const frame = page.frame({ name: "my-frame" });
41
- if (frame) {
42
- await frame.goto("https://example.com");
43
- const title = await frame.title();
44
- }
45
-
46
- // Get all frames
47
- const frames = page.frames();
48
- for (const f of frames) {
49
- console.log("Frame URL:", f.url());
50
- }
51
- ```
52
-
53
- ### Waiting for iFrame Content
54
-
55
- ```typescript
56
- // Wait for iframe to load
57
- const frame = page.frameLocator("#dynamic-iframe");
58
-
59
- // Wait for element inside iframe
60
- await expect(frame.getByRole("heading")).toBeVisible({ timeout: 10000 });
61
-
62
- // Wait for iframe src to change
63
- await page.waitForFunction(() => {
64
- const iframe = document.querySelector("iframe#my-frame") as HTMLIFrameElement;
65
- return iframe?.src.includes("loaded");
66
- });
67
- ```
68
-
69
- ## Cross-Origin iFrames
70
-
71
- ### Accessing Cross-Origin Content
72
-
73
- ```typescript
74
- // Cross-origin iframes work seamlessly with frameLocator
75
- const thirdPartyFrame = page.frameLocator('iframe[src*="third-party.com"]');
76
-
77
- // Interact with elements inside cross-origin iframe
78
- await thirdPartyFrame.getByRole("textbox").fill("test@example.com");
79
- await thirdPartyFrame.getByRole("button", { name: "Submit" }).click();
80
-
81
- // Wait for cross-origin iframe to be ready
82
- await expect(thirdPartyFrame.locator("body")).toBeVisible();
83
- ```
84
-
85
- ### Payment Provider iFrames (Stripe, PayPal)
86
-
87
- ```typescript
88
- test("Stripe payment iframe", async ({ page }) => {
89
- await page.goto("/checkout");
90
-
91
- // Stripe uses multiple iframes for each field
92
- const cardFrame = page
93
- .frameLocator('iframe[name*="__privateStripeFrame"]')
94
- .first();
95
-
96
- // Wait for Stripe to initialize
97
- await expect(cardFrame.locator('[placeholder="Card number"]')).toBeVisible({
98
- timeout: 15000,
99
- });
100
-
101
- // Fill card details
102
- await cardFrame
103
- .locator('[placeholder="Card number"]')
104
- .fill("4242424242424242");
105
- await cardFrame.locator('[placeholder="MM / YY"]').fill("12/30");
106
- await cardFrame.locator('[placeholder="CVC"]').fill("123");
107
- });
108
- ```
109
-
110
- ### Handling OAuth in iFrames
111
-
112
- ```typescript
113
- test("OAuth iframe flow", async ({ page }) => {
114
- await page.goto("/login");
115
- await page.getByRole("button", { name: "Sign in with Google" }).click();
116
-
117
- // If OAuth opens in iframe instead of popup
118
- const oauthFrame = page.frameLocator('iframe[src*="accounts.google.com"]');
119
-
120
- // Wait for OAuth form
121
- await expect(oauthFrame.getByLabel("Email")).toBeVisible({ timeout: 10000 });
122
- await oauthFrame.getByLabel("Email").fill("test@gmail.com");
123
- });
124
- ```
125
-
126
- ## Nested iFrames
127
-
128
- ### Accessing Nested Frames
129
-
130
- ```typescript
131
- // Parent iframe contains child iframe
132
- const parentFrame = page.frameLocator("#outer-frame");
133
- const childFrame = parentFrame.frameLocator("#inner-frame");
134
-
135
- // Interact with deeply nested content
136
- await childFrame.getByRole("button", { name: "Submit" }).click();
137
-
138
- // Multiple levels of nesting
139
- const level1 = page.frameLocator("#level1");
140
- const level2 = level1.frameLocator("#level2");
141
- const level3 = level2.frameLocator("#level3");
142
- await level3.getByText("Deep content").click();
143
- ```
144
-
145
- ### Finding Elements Across Frame Hierarchy
146
-
147
- ```typescript
148
- // Helper to search all frames for an element
149
- async function findInAnyFrame(
150
- page: Page,
151
- selector: string,
152
- ): Promise<Locator | null> {
153
- // Check main page first
154
- const mainCount = await page.locator(selector).count();
155
- if (mainCount > 0) return page.locator(selector);
156
-
157
- // Check all frames
158
- for (const frame of page.frames()) {
159
- const count = await frame.locator(selector).count();
160
- if (count > 0) {
161
- return frame.locator(selector);
162
- }
163
- }
164
- return null;
165
- }
166
-
167
- test("find element in any frame", async ({ page }) => {
168
- await page.goto("/complex-page");
169
- const element = await findInAnyFrame(page, '[data-testid="submit-btn"]');
170
- if (element) await element.click();
171
- });
172
- ```
173
-
174
- ## Dynamic iFrames
175
-
176
- ### iFrames Created at Runtime
177
-
178
- ```typescript
179
- test("handle dynamically created iframe", async ({ page }) => {
180
- await page.goto("/dashboard");
181
-
182
- // Click button that creates iframe
183
- await page.getByRole("button", { name: "Open Widget" }).click();
184
-
185
- // Wait for iframe to appear in DOM
186
- await page.waitForSelector("iframe#widget-frame");
187
-
188
- // Now access the frame
189
- const widgetFrame = page.frameLocator("#widget-frame");
190
- await expect(widgetFrame.getByText("Widget Loaded")).toBeVisible();
191
- });
192
- ```
193
-
194
- ### iFrames with Changing src
195
-
196
- ```typescript
197
- test("iframe src changes", async ({ page }) => {
198
- await page.goto("/multi-step");
199
-
200
- const frame = page.frameLocator("#step-frame");
201
-
202
- // Step 1
203
- await expect(frame.getByText("Step 1")).toBeVisible();
204
- await frame.getByRole("button", { name: "Next" }).click();
205
-
206
- // Wait for iframe to reload with new content
207
- await expect(frame.getByText("Step 2")).toBeVisible({ timeout: 10000 });
208
- await frame.getByRole("button", { name: "Next" }).click();
209
-
210
- // Step 3
211
- await expect(frame.getByText("Step 3")).toBeVisible({ timeout: 10000 });
212
- });
213
- ```
214
-
215
- ### Lazy-Loaded iFrames
216
-
217
- ```typescript
218
- test("lazy loaded iframe", async ({ page }) => {
219
- await page.goto("/page-with-lazy-iframe");
220
-
221
- // Scroll to trigger lazy load
222
- await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
223
-
224
- // Wait for iframe to load
225
- const lazyFrame = page.frameLocator("#lazy-iframe");
226
- await expect(lazyFrame.locator("body")).not.toBeEmpty({ timeout: 15000 });
227
-
228
- // Interact with content
229
- await lazyFrame.getByRole("button").click();
230
- });
231
- ```
232
-
233
- ## iFrame Navigation
234
-
235
- ### Navigating Within iFrame
236
-
237
- ```typescript
238
- test("iframe internal navigation", async ({ page }) => {
239
- await page.goto("/app");
240
-
241
- // Get frame object for navigation control
242
- const frame = page.frame({ name: "content-frame" });
243
- if (!frame) throw new Error("Frame not found");
244
-
245
- // Navigate within iframe
246
- await frame.goto("https://embedded-app.com/page2");
247
-
248
- // Wait for navigation
249
- await frame.waitForURL("**/page2");
250
-
251
- // Verify content
252
- await expect(frame.getByRole("heading")).toHaveText("Page 2");
253
- });
254
- ```
255
-
256
- ### Handling Frame Navigation Events
257
-
258
- ```typescript
259
- test("track iframe navigation", async ({ page }) => {
260
- const navigations: string[] = [];
261
-
262
- // Listen to frame navigation
263
- page.on("framenavigated", (frame) => {
264
- if (frame.parentFrame()) {
265
- // This is an iframe navigation
266
- navigations.push(frame.url());
267
- }
268
- });
269
-
270
- await page.goto("/with-iframe");
271
- await page
272
- .frameLocator("#nav-frame")
273
- .getByRole("link", { name: "Page 2" })
274
- .click();
275
-
276
- // Verify navigation occurred
277
- expect(navigations.some((url) => url.includes("page2"))).toBe(true);
278
- });
279
- ```
280
-
281
- ## Common Patterns
282
-
283
- ### iFrame Fixture
284
-
285
- ```typescript
286
- // fixtures.ts
287
- import { test as base, FrameLocator } from "@playwright/test";
288
-
289
- export const test = base.extend<{ paymentFrame: FrameLocator }>({
290
- paymentFrame: async ({ page }, use) => {
291
- await page.goto("/checkout");
292
-
293
- // Wait for payment iframe to be ready
294
- const frame = page.frameLocator('iframe[src*="payment"]');
295
- await expect(frame.locator("body")).toBeVisible({ timeout: 15000 });
296
-
297
- await use(frame);
298
- },
299
- });
300
-
301
- // test file
302
- test("complete payment", async ({ paymentFrame }) => {
303
- await paymentFrame.getByLabel("Card").fill("4242424242424242");
304
- await paymentFrame.getByRole("button", { name: "Pay" }).click();
305
- });
306
- ```
307
-
308
- ### Debugging iFrame Issues
309
-
310
- ```typescript
311
- test("debug iframe content", async ({ page }) => {
312
- await page.goto("/page-with-iframes");
313
-
314
- // List all frames
315
- console.log("All frames:");
316
- for (const frame of page.frames()) {
317
- console.log(` - ${frame.name() || "(unnamed)"}: ${frame.url()}`);
318
- }
319
-
320
- // Screenshot specific iframe content
321
- const frame = page.frame({ name: "target-frame" });
322
- if (frame) {
323
- const body = frame.locator("body");
324
- await body.screenshot({ path: "iframe-content.png" });
325
- }
326
-
327
- // Get iframe HTML for debugging
328
- const frameContent = page.frameLocator("#my-frame");
329
- const html = await frameContent.locator("body").innerHTML();
330
- console.log("iFrame HTML:", html.substring(0, 500));
331
- });
332
- ```
333
-
334
- ### Handling iFrame Load Failures
335
-
336
- ```typescript
337
- test("handle iframe load failure", async ({ page }) => {
338
- await page.goto("/page-with-unreliable-iframe");
339
-
340
- const frame = page.frameLocator("#unreliable-frame");
341
-
342
- try {
343
- // Try to interact with iframe content
344
- await expect(frame.getByRole("button")).toBeVisible({ timeout: 5000 });
345
- await frame.getByRole("button").click();
346
- } catch (error) {
347
- // Fallback: refresh iframe
348
- await page.evaluate(() => {
349
- const iframe = document.querySelector(
350
- "#unreliable-frame",
351
- ) as HTMLIFrameElement;
352
- if (iframe) iframe.src = iframe.src;
353
- });
354
-
355
- // Retry
356
- await expect(frame.getByRole("button")).toBeVisible({ timeout: 10000 });
357
- await frame.getByRole("button").click();
358
- }
359
- });
360
- ```
361
-
362
- ### Mocking iFrame Content
363
-
364
- ```typescript
365
- test("mock iframe response", async ({ page }) => {
366
- // Intercept iframe src request
367
- await page.route("**/embedded-widget**", (route) => {
368
- route.fulfill({
369
- contentType: "text/html",
370
- body: `
371
- <!DOCTYPE html>
372
- <html>
373
- <body>
374
- <h1>Mocked Widget</h1>
375
- <button>Mocked Button</button>
376
- </body>
377
- </html>
378
- `,
379
- });
380
- });
381
-
382
- await page.goto("/page-with-widget");
383
-
384
- const frame = page.frameLocator("#widget-frame");
385
- await expect(frame.getByRole("heading")).toHaveText("Mocked Widget");
386
- });
387
- ```
388
-
389
- ## Anti-Patterns to Avoid
390
-
391
- | Anti-Pattern | Problem | Solution |
392
- | ------------------------------------- | --------------------------------- | -------------------------------------------------- |
393
- | Using `page.frame()` for interactions | Less reliable than frameLocator | Use `page.frameLocator()` for element interactions |
394
- | Hardcoding iframe index | Fragile if DOM order changes | Use name, id, or src attribute selectors |
395
- | Not waiting for iframe load | Race conditions | Wait for element inside iframe to be visible |
396
- | Assuming same-origin | Cross-origin has different timing | Always wait for iframe content explicitly |
397
- | Ignoring nested iframes | Element not found | Chain frameLocator calls for nested frames |
398
-
399
- ## Related References
400
-
401
- - **Locators**: See [locators.md](locators.md) for selector strategies
402
- - **Third-party services**: See [third-party.md](third-party.md) for payment iframe patterns
403
- - **Debugging**: See [debugging.md](debugging.md) for troubleshooting iframe issues
@@ -1,242 +0,0 @@
1
- # Locator Strategies
2
-
3
- ## Table of Contents
4
-
5
- 1. [Priority Order](#priority-order)
6
- 2. [User-Facing Locators](#user-facing-locators)
7
- 3. [Filtering & Chaining](#filtering--chaining)
8
- 4. [Dynamic Content](#dynamic-content)
9
- 5. [Shadow DOM](#shadow-dom)
10
- 6. [Iframes](#iframes)
11
-
12
- ## Priority Order
13
-
14
- Use locators in this order of preference:
15
-
16
- 1. **Role-based** (most resilient): `getByRole`
17
- 2. **Label-based**: `getByLabel`, `getByPlaceholder`
18
- 3. **Text-based**: `getByText`, `getByTitle`
19
- 4. **Test IDs** (when semantic locators aren't possible): `getByTestId`
20
- 5. **CSS/XPath** (last resort): `locator('css=...')`, `locator('xpath=...')`
21
-
22
- ## User-Facing Locators
23
-
24
- ### getByRole
25
-
26
- Most robust approach - matches how users and assistive technology perceive the page.
27
-
28
- ```typescript
29
- // Buttons
30
- page.getByRole("button", { name: "Submit" });
31
- page.getByRole("button", { name: /submit/i }); // case-insensitive regex
32
-
33
- // Links
34
- page.getByRole("link", { name: "Home" });
35
-
36
- // Form elements
37
- page.getByRole("textbox", { name: "Email" });
38
- page.getByRole("checkbox", { name: "Remember me" });
39
- page.getByRole("combobox", { name: "Country" });
40
- page.getByRole("radio", { name: "Option A" });
41
-
42
- // Headings
43
- page.getByRole("heading", { name: "Welcome", level: 1 });
44
-
45
- // Lists & items
46
- page.getByRole("list").getByRole("listitem");
47
-
48
- // Navigation & regions
49
- page.getByRole("navigation");
50
- page.getByRole("main");
51
- page.getByRole("dialog");
52
- page.getByRole("alert");
53
- ```
54
-
55
- ### getByLabel
56
-
57
- For form elements with associated labels.
58
-
59
- ```typescript
60
- // Input with <label for="email">
61
- page.getByLabel("Email address");
62
-
63
- // Input with aria-label
64
- page.getByLabel("Search");
65
-
66
- // Exact match
67
- page.getByLabel("Email", { exact: true });
68
- ```
69
-
70
- ### getByPlaceholder
71
-
72
- ```typescript
73
- page.getByPlaceholder("Enter your email");
74
- page.getByPlaceholder(/email/i);
75
- ```
76
-
77
- ### getByText
78
-
79
- ```typescript
80
- // Partial match (default)
81
- page.getByText("Welcome");
82
-
83
- // Exact match
84
- page.getByText("Welcome to our site", { exact: true });
85
-
86
- // Regex
87
- page.getByText(/welcome/i);
88
- ```
89
-
90
- ### getByTestId
91
-
92
- Configure custom test ID attribute in `playwright.config.ts`:
93
-
94
- ```typescript
95
- use: {
96
- testIdAttribute: "data-testid"; // default
97
- }
98
- ```
99
-
100
- Usage:
101
-
102
- ```typescript
103
- // HTML: <button data-testid="submit-btn">Submit</button>
104
- page.getByTestId("submit-btn");
105
- ```
106
-
107
- ## Filtering & Chaining
108
-
109
- ### filter()
110
-
111
- Narrow down locators:
112
-
113
- ```typescript
114
- // Filter by text
115
- page.getByRole("listitem").filter({ hasText: "Product" });
116
-
117
- // Filter by NOT having text
118
- page.getByRole("listitem").filter({ hasNotText: "Out of stock" });
119
-
120
- // Filter by child locator
121
- page.getByRole("listitem").filter({
122
- has: page.getByRole("button", { name: "Buy" }),
123
- });
124
-
125
- // Combine filters
126
- page
127
- .getByRole("listitem")
128
- .filter({ hasText: "Product" })
129
- .filter({ has: page.getByText("$9.99") });
130
- ```
131
-
132
- ### Chaining
133
-
134
- ```typescript
135
- // Navigate down the DOM tree
136
- page.getByRole("article").getByRole("heading");
137
-
138
- // Get parent/ancestor
139
- page.getByText("Child").locator("..");
140
- page.getByText("Child").locator("xpath=ancestor::article");
141
- ```
142
-
143
- ### nth() and first()/last()
144
-
145
- ```typescript
146
- page.getByRole("listitem").first();
147
- page.getByRole("listitem").last();
148
- page.getByRole("listitem").nth(2); // 0-indexed
149
- ```
150
-
151
- ## Dynamic Content
152
-
153
- ### Waiting for Elements
154
-
155
- Locators auto-wait for actionability by default. For explicit state waiting:
156
-
157
- ```typescript
158
- await page.getByRole("button").waitFor({ state: "visible" });
159
- await page.getByText("Loading").waitFor({ state: "hidden" });
160
- ```
161
-
162
- > **For comprehensive waiting strategies** (element state, navigation, network, polling with `toPass()`), see [assertions-waiting.md](assertions-waiting.md#waiting-strategies).
163
-
164
- ### Lists with Dynamic Items
165
-
166
- ```typescript
167
- // Wait for specific count
168
- await expect(page.getByRole("listitem")).toHaveCount(5);
169
-
170
- // Get all matching elements
171
- const items = await page.getByRole("listitem").all();
172
- for (const item of items) {
173
- await expect(item).toBeVisible();
174
- }
175
- ```
176
-
177
- ## Shadow DOM
178
-
179
- Playwright pierces shadow DOM by default:
180
-
181
- ```typescript
182
- // Automatically finds elements inside shadow roots
183
- page.getByRole("button", { name: "Shadow Button" });
184
-
185
- // Explicit shadow DOM traversal (if needed)
186
- page.locator("my-component").locator("internal:shadow=button");
187
- ```
188
-
189
- ## Iframes
190
-
191
- ```typescript
192
- // By frame name or URL
193
- const frame = page.frameLocator('iframe[name="content"]');
194
- await frame.getByRole("button").click();
195
-
196
- // By index
197
- const frame = page.frameLocator("iframe").first();
198
-
199
- // Nested iframes
200
- const nestedFrame = page.frameLocator("#outer").frameLocator("#inner");
201
- await nestedFrame.getByText("Content").click();
202
- ```
203
-
204
- ## Debugging Locators
205
-
206
- ```typescript
207
- // Highlight element in headed mode
208
- await page.getByRole("button").highlight();
209
-
210
- // Count matches
211
- const count = await page.getByRole("listitem").count();
212
-
213
- // Check if exists without waiting
214
- const exists = (await page.getByRole("button").count()) > 0;
215
-
216
- // Use Playwright Inspector
217
- // PWDEBUG=1 npx playwright test
218
- ```
219
-
220
- ## Common Issues & Solutions
221
-
222
- | Issue | Solution |
223
- | ----------------------- | ------------------------------------------------ |
224
- | Multiple elements match | Add filters or use `nth()`, `first()`, `last()` |
225
- | Element not found | Check visibility, wait for load, verify selector |
226
- | Stale element | Locators are lazy; re-query if DOM changes |
227
- | Dynamic IDs | Use stable attributes like role, text, test-id |
228
- | Hidden elements | Use `{ force: true }` only when necessary |
229
-
230
- ## Anti-Patterns to Avoid
231
-
232
- | Anti-Pattern | Problem | Solution |
233
- | --------------------------------- | --------------------------------- | ------------------------------------------------- |
234
- | `page.locator('.btn-primary')` | Brittle, implementation-dependent | `page.getByRole('button', { name: 'Submit' })` |
235
- | `page.locator('#dynamic-id-123')` | Breaks when IDs change | Use stable attributes like role, text, or test-id |
236
- | Testing implementation details | Breaks on refactoring | Test user-visible behavior |
237
-
238
- ## Related References
239
-
240
- - **Debugging selector issues**: See [debugging.md](debugging.md) for troubleshooting
241
- - **Waiting for elements**: See [assertions-waiting.md](assertions-waiting.md) for waiting strategies
242
- - **Using in Page Objects**: See [page-object-model.md](page-object-model.md) for organizing locators