@schalkneethling/calavera-skill-frontend-testing 0.2.0-next.1

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.
@@ -0,0 +1,295 @@
1
+ # Locator Strategies
2
+
3
+ Locators determine how tests find elements. Good locators are resilient to implementation changes while verifying accessibility.
4
+
5
+ ## Accessibility-Focused Locator Principle
6
+
7
+ Both libraries prioritize user-facing semantics, but their APIs and complete priority lists differ.
8
+ Use the library-specific names shown below rather than treating this as one identical ordering.
9
+
10
+ ### 1. Accessible Roles (First Choice)
11
+
12
+ Query the accessibility tree. If this fails, the UI likely has accessibility issues.
13
+
14
+ ```javascript
15
+ // Playwright
16
+ page.getByRole("button", { name: /submit/i });
17
+ page.getByRole("textbox", { name: /email/i });
18
+ page.getByRole("heading", { level: 1 });
19
+ page.getByRole("navigation");
20
+ page.getByRole("listitem");
21
+
22
+ // Testing Library
23
+ screen.getByRole("button", { name: /submit/i });
24
+ screen.getByRole("checkbox", { checked: true });
25
+ ```
26
+
27
+ **Why first**: Users and assistive technologies perceive the page through roles. Testing with roles validates both functionality and accessibility.
28
+
29
+ **Name option**: Filters by accessible name (visible text, aria-label, or aria-labelledby).
30
+
31
+ ```javascript
32
+ // Multiple buttons? Filter by name
33
+ page.getByRole("button", { name: /save/i }); // Save button
34
+ page.getByRole("button", { name: /cancel/i }); // Cancel button
35
+ page.getByRole("button", { name: /delete/i }); // Delete button
36
+ ```
37
+
38
+ **Common roles**:
39
+
40
+ - `button`, `link`, `textbox`, `checkbox`, `radio`
41
+ - `combobox` (select), `listbox`, `option`
42
+ - `heading`, `navigation`, `main`, `article`
43
+ - `dialog`, `alert`, `alertdialog`
44
+ - `list`, `listitem`, `table`, `row`, `cell`
45
+ - `tab`, `tablist`, `tabpanel`
46
+
47
+ ### 2. Label Text (Forms)
48
+
49
+ How users find form fields. Validates label-input association.
50
+
51
+ ```javascript
52
+ // Playwright
53
+ page.getByLabel("Email address");
54
+ page.getByLabel(/password/i);
55
+
56
+ // Testing Library
57
+ screen.getByLabelText("Email address");
58
+ screen.getByLabelText(/confirm password/i);
59
+ ```
60
+
61
+ **Why second**: Form users navigate by labels. Tests using labels fail if labels are missing or improperly associated—catching real accessibility bugs.
62
+
63
+ ### 3. Placeholder Text (Fallback for Forms)
64
+
65
+ Use only when labels aren't available. Placeholder is not a label substitute.
66
+
67
+ ```javascript
68
+ page.getByPlaceholder("Search products...");
69
+ screen.getByPlaceholderText("Enter your query");
70
+ ```
71
+
72
+ **Why third**: Better than test IDs, but UI should have proper labels.
73
+
74
+ ### 4. Visible Text (Non-Interactive Content)
75
+
76
+ For elements identified by their content.
77
+
78
+ ```javascript
79
+ // Playwright
80
+ page.getByText("Welcome back, Sarah");
81
+ page.getByText(/order confirmed/i);
82
+
83
+ // Testing Library
84
+ screen.getByText("No results found");
85
+ screen.getByText(/loading/i);
86
+ ```
87
+
88
+ **Why fourth**: Useful for assertions on content, but doesn't verify semantic structure.
89
+
90
+ ### 5. Alt Text (Images)
91
+
92
+ For images with meaningful alt text.
93
+
94
+ ```javascript
95
+ page.getByAltText("Company logo");
96
+ screen.getByAltText(/product photo/i);
97
+ ```
98
+
99
+ ### 6. Title Attribute
100
+
101
+ Rarely used. Most elements shouldn't rely on title for identification.
102
+
103
+ ```javascript
104
+ page.getByTitle("Close dialog");
105
+ screen.getByTitle(/help/i);
106
+ ```
107
+
108
+ ### 7. Test IDs (Last Resort)
109
+
110
+ Escape hatch when semantic queries fail. Provides no accessibility verification.
111
+
112
+ ```javascript
113
+ // Playwright
114
+ page.getByTestId("pricing-calculator");
115
+
116
+ // Testing Library
117
+ screen.getByTestId("data-grid");
118
+ ```
119
+
120
+ **When appropriate**:
121
+
122
+ - Complex dynamic components (data grids, charts)
123
+ - Elements with no stable accessible name
124
+ - Third-party components you can't modify
125
+
126
+ **When to avoid**:
127
+
128
+ - Any element that has text, label, or semantic role
129
+ - As a default choice to "make tests easier"
130
+
131
+ ## Playwright-Specific Patterns
132
+
133
+ ### Chaining and Filtering
134
+
135
+ Narrow down when multiple matches exist:
136
+
137
+ ```javascript
138
+ // Find delete button in specific row
139
+ const userRow = page.getByRole("row").filter({ hasText: "john@example.com" });
140
+ await userRow.getByRole("button", { name: /delete/i }).click();
141
+
142
+ // Combine locators
143
+ const submitButton = page
144
+ .getByRole("button", { name: /submit/i })
145
+ .and(page.locator("[type=submit]"));
146
+ ```
147
+
148
+ ### Scoped Queries with `within`
149
+
150
+ Test elements within a container:
151
+
152
+ ```javascript
153
+ // Playwright
154
+ const form = page.getByRole("form", { name: /checkout/i });
155
+ await form.getByLabel("Card number").fill("4111111111111111");
156
+
157
+ // Testing Library
158
+ import { within } from "@testing-library/react";
159
+ const form = screen.getByRole("form");
160
+ within(form).getByLabelText("Email");
161
+ ```
162
+
163
+ ### Handling Multiple Elements
164
+
165
+ When strict matching fails:
166
+
167
+ ```javascript
168
+ // Filter first and wait for the expected stable result set.
169
+ const deleteButtons = page.getByRole("button", { name: /delete/i });
170
+ await expect(deleteButtons).toHaveCount(3);
171
+ for (let index = 0; index < (await deleteButtons.count()); index++) {
172
+ await expect(deleteButtons.nth(index)).toBeVisible();
173
+ }
174
+
175
+ // Or be more specific with the query
176
+ page.getByRole("button", { name: "Delete", exact: true });
177
+ ```
178
+
179
+ ## Testing Library Variants
180
+
181
+ ### get vs query vs find
182
+
183
+ | Variant | No match | Multiple matches | Async |
184
+ | --------- | ------------ | ---------------- | ----------- |
185
+ | `getBy` | Throws | Throws | No |
186
+ | `queryBy` | Returns null | Throws | No |
187
+ | `findBy` | Throws | Throws | Yes (waits) |
188
+
189
+ ```javascript
190
+ // Element should exist now
191
+ screen.getByRole("button", { name: /submit/i });
192
+
193
+ // Element might not exist (testing absence)
194
+ expect(screen.queryByRole("alert")).not.toBeInTheDocument();
195
+
196
+ // Element appears after async action
197
+ await screen.findByRole("alert");
198
+ ```
199
+
200
+ ### Regex vs String Matching
201
+
202
+ ```javascript
203
+ // Exact match (case-sensitive)
204
+ screen.getByText("Submit Order");
205
+
206
+ // Flexible match (recommended for resilience)
207
+ screen.getByText(/submit order/i);
208
+
209
+ // Partial match
210
+ screen.getByText(/submit/i);
211
+ ```
212
+
213
+ ## Anti-Patterns
214
+
215
+ ### CSS Selectors as Primary Strategy
216
+
217
+ ```javascript
218
+ // BAD: Tied to implementation
219
+ page.locator(".btn-primary");
220
+ page.locator("#submit-form-btn");
221
+ page.locator("div.container > form > button");
222
+
223
+ // GOOD: Tied to user experience
224
+ page.getByRole("button", { name: /submit/i });
225
+ ```
226
+
227
+ ### XPath
228
+
229
+ ```javascript
230
+ // BAD: Brittle, hard to read
231
+ page.locator("//div[@class='form']//button[text()='Submit']");
232
+
233
+ // GOOD: Clear and semantic
234
+ page.getByRole("button", { name: /submit/i });
235
+ ```
236
+
237
+ ### Position-Based Selection
238
+
239
+ ```javascript
240
+ // BAD: Breaks when order changes
241
+ page.getByRole("button").nth(2);
242
+
243
+ // GOOD: Explicit about which element
244
+ page.getByRole("button", { name: /delete account/i });
245
+ ```
246
+
247
+ ### Test IDs for Everything
248
+
249
+ ```javascript
250
+ // BAD: No accessibility verification
251
+ page.getByTestId("submit-button");
252
+ page.getByTestId("email-input");
253
+ page.getByTestId("error-message");
254
+
255
+ // GOOD: Validates accessibility
256
+ page.getByRole("button", { name: /submit/i });
257
+ page.getByLabel("Email");
258
+ page.getByRole("alert");
259
+ ```
260
+
261
+ ## Debugging Locators
262
+
263
+ ### Playwright
264
+
265
+ ```javascript
266
+ // Log all button text
267
+ console.log(await page.getByRole("button").allTextContents());
268
+
269
+ // Use Playwright Inspector
270
+ // npx playwright test --debug
271
+
272
+ // Use codegen for suggestions
273
+ // npx playwright codegen example.com
274
+ ```
275
+
276
+ ### Testing Library
277
+
278
+ ```javascript
279
+ // Log the DOM
280
+ screen.debug();
281
+
282
+ // Log specific element
283
+ screen.debug(screen.getByRole("form"));
284
+
285
+ // Use Testing Playground
286
+ // testing-playground.com
287
+ // Or browser extension
288
+ ```
289
+
290
+ ## References
291
+
292
+ - [Playwright Locators](https://playwright.dev/docs/locators)
293
+ - [Testing Library Queries](https://testing-library.com/docs/queries/about/)
294
+ - [ARIA Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
295
+ - [Testing Library Priority Guide](https://testing-library.com/docs/queries/about/#priority)