pw-element-interactions 0.0.9 → 0.1.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.
@@ -0,0 +1,235 @@
1
+ ---
2
+ name: pw-element-interactions
3
+ description: >
4
+ Use this skill whenever writing, editing, or generating Playwright tests! Triggers on any mention of
5
+ Playwright tests, pw-element-interactions or pw-element-repository packages, the Steps API, ElementRepository, ElementInteractions, baseFixture,
6
+ ContextStore, page-repository.json, or any request to write, fix, or add to a Playwright test in this project.
7
+ ---
8
+
9
+ # pw-element-interactions — Agent Skill
10
+
11
+ A two-package Playwright framework that fully decouples **element acquisition** (`pw-element-repository`) from **element interaction** (`pw-element-interactions`). Tests reference elements by plain strings (`'HomePage'`, `'submitButton'`); raw selectors never appear in test code.
12
+
13
+ ---
14
+
15
+ ## 🚨 ABSOLUTE RULES — READ BEFORE DOING ANYTHING ELSE
16
+
17
+ These rules are non-negotiable and override any perceived helpfulness or initiative:
18
+
19
+ ### 1. NEVER write tests unless explicitly asked
20
+ - NEVER create, write, or scaffold a test file unless the user has directly asked for it in this conversation.
21
+ - NEVER infer that tests are needed from context, file structure, or prior messages.
22
+ - If unsure whether the user wants a test written, **ask first. Do not write first.**
23
+ - When asked to write tests, ALWAYS start by producing a brief plan — test file(s), scenarios, and locators needed — and wait for the user to approve it before writing anything.
24
+ - If the plan covers more than one test file, suggest splitting into separate sessions (one per file) before proceeding.
25
+
26
+ ### 2. NEVER edit `page-repository.json` without explicit permission
27
+ - NEVER add, modify, or delete entries in `page-repository.json` (or any locator JSON file) without the user explicitly approving the change.
28
+ - If new locators are needed, **show the user exactly what you intend to add** and wait for a clear "yes" before touching the file.
29
+
30
+ ### 3. NEVER invent selectors — use Playwright MCP to inspect the live site
31
+ - NEVER guess or invent CSS selectors, XPath, IDs, or text values.
32
+ - ALWAYS use the Playwright MCP to navigate to the page and inspect the real DOM before adding any locator.
33
+ - If the Playwright MCP is not connected, stop and tell the user: *"I need the Playwright MCP to inspect the site. Please add it to your Claude Code MCP settings and restart."* Do not proceed until it is available.
34
+
35
+ ### 4. NEVER invent type definitions or API shapes
36
+ - NEVER create `.d.ts` stubs or type shims for `pw-element-interactions` or `pw-element-repository`.
37
+ - If a type is missing, report the problem to the user and ask how to proceed. Do not work around it silently.
38
+
39
+ ### 5. ALWAYS inspect a screenshot when a test fails
40
+ - The base fixture automatically captures a `failure-screenshot` on every failed test — run `npx playwright show-report` and open the report in a browser using Playwright MCP or a browser MCP to view it.
41
+ - Ensure `reporter: 'html'` is set in `playwright.config.ts` — this is required for `failure-screenshot` attachments to appear in the report.
42
+ - If the report is not accessible, use the Playwright MCP to take a screenshot of the current page state manually.
43
+ - NEVER attempt to fix a failing test based solely on the error message or stack trace — always verify visually first.
44
+ - Describe what you see in the screenshot to the user, then propose a fix based on the visual evidence.
45
+ - If the screenshot suggests a selector problem, re-inspect the live DOM via Playwright MCP before touching `page-repository.json`.
46
+ - After a fully passing run, do NOT open the report unless the user asks.
47
+
48
+ ### 6. Before creating or modifying `playwright.config.ts`, read the existing file first — do not overwrite it.
49
+
50
+ ---
51
+
52
+ ## 1. Adding Locators
53
+
54
+ All selectors live in `tests/data/page-repository.json`.
55
+
56
+ ```json
57
+ {
58
+ "pages": [
59
+ {
60
+ "name": "HomePage",
61
+ "elements": [
62
+ {
63
+ "elementName": "submitButton",
64
+ "selector": {
65
+ "css": "button[data-test='submit']",
66
+ "xpath": "//button[@data-test='submit']",
67
+ "id": "submit-btn",
68
+ "text": "Submit"
69
+ }
70
+ }
71
+ ]
72
+ }
73
+ ]
74
+ }
75
+ ```
76
+
77
+ **Naming conventions:**
78
+ - `name` — PascalCase page identifier, e.g. `CheckoutPage`, `ProductDetailsPage`
79
+ - `elementName` — camelCase element identifier, e.g. `submitButton`, `galleryImages`
80
+
81
+ ---
82
+
83
+ ## 2. Setup — Fixtures
84
+
85
+ Before writing `tests/fixtures/base.ts`, **read it first if it already exists** — do not overwrite it without checking. The `baseFixture` automatically includes screenshot-on-failure capture, so no extension is needed:
86
+
87
+ ```ts
88
+ // tests/fixtures/base.ts
89
+ import { test as base } from '@playwright/test';
90
+ import { baseFixture } from 'pw-element-interactions';
91
+
92
+ export const test = baseFixture(base, 'tests/data/page-repository.json');
93
+ export { expect } from '@playwright/test';
94
+ ```
95
+
96
+ ```ts
97
+ // tests/example.spec.ts
98
+ import { test } from '../fixtures/base';
99
+
100
+ test('example', async ({ steps }) => {
101
+ await steps.navigateTo('https://example.com/');
102
+ await steps.click('HomePage', 'submitButton');
103
+ });
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 3. Steps API
109
+
110
+ Every method takes `pageName` and `elementName` as its first two arguments, matching keys in your JSON file.
111
+
112
+ ### 🧭 Navigation
113
+
114
+ Relative URLs are resolved against `baseURL` from `playwright.config.ts`. If a relative URL is passed and `baseURL` is not configured, an error will be thrown.
115
+
116
+ ```ts
117
+ await steps.navigateTo('https://example.com/path');
118
+ await steps.refresh();
119
+ await steps.backOrForward('BACKWARDS'); // or 'FORWARDS'
120
+ await steps.setViewport(1280, 720);
121
+ ```
122
+
123
+ ### 🖱️ Interaction
124
+
125
+ ```ts
126
+ await steps.click('PageName', 'elementName');
127
+ await steps.clickWithoutScrolling('PageName', 'elementName');
128
+ await steps.clickIfPresent('PageName', 'elementName');
129
+ await steps.clickRandom('PageName', 'elementName');
130
+ await steps.hover('PageName', 'elementName');
131
+ await steps.scrollIntoView('PageName', 'elementName');
132
+ await steps.fill('PageName', 'elementName', 'my input');
133
+ await steps.typeSequentially('PageName', 'elementName', 'my input');
134
+ await steps.typeSequentially('PageName', 'elementName', 'my input', 50); // custom delay ms
135
+ await steps.uploadFile('PageName', 'elementName', 'tests/fixtures/file.pdf');
136
+ await steps.dragAndDrop('PageName', 'elementName', { target: otherLocator });
137
+ await steps.dragAndDrop('PageName', 'elementName', { xOffset: 100, yOffset: 0 });
138
+ await steps.dragAndDropListedElement('PageName', 'elementName', 'Item Label', { target: otherLocator });
139
+ ```
140
+
141
+ For dropdown selection, import `DropdownSelectType` at the top of your test file:
142
+
143
+ ```ts
144
+ import { DropdownSelectType } from 'pw-element-interactions';
145
+ ```
146
+
147
+ Then use it in your test:
148
+
149
+ ```ts
150
+ const value1 = await steps.selectDropdown('PageName', 'elementName'); // random (default)
151
+ const value2 = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.RANDOM }); // explicit random
152
+ const value3 = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.VALUE, value: 'xl' }); // by value
153
+ const value4 = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.INDEX, index: 2 }); // by index
154
+ ```
155
+
156
+ ### 📊 Data Extraction
157
+
158
+ ```ts
159
+ const text = await steps.getText('PageName', 'elementName');
160
+ const href = await steps.getAttribute('PageName', 'elementName', 'href');
161
+ ```
162
+
163
+ ### ✅ Verification
164
+
165
+ ```ts
166
+ await steps.verifyPresence('PageName', 'elementName');
167
+ await steps.verifyAbsence('PageName', 'elementName');
168
+ await steps.verifyText('PageName', 'elementName', 'Expected text');
169
+ await steps.verifyText('PageName', 'elementName', undefined, { notEmpty: true });
170
+ await steps.verifyCount('PageName', 'elementName', { exact: 3 });
171
+ await steps.verifyCount('PageName', 'elementName', { greaterThan: 0 });
172
+ await steps.verifyCount('PageName', 'elementName', { lessThan: 10 });
173
+ await steps.verifyImages('PageName', 'elementName');
174
+ await steps.verifyImages('PageName', 'elementName', false); // skip scroll-into-view
175
+ await steps.verifyUrlContains('/dashboard');
176
+ ```
177
+
178
+ ### ⏳ Waiting
179
+
180
+ ```ts
181
+ await steps.waitForState('PageName', 'elementName'); // default: 'visible'
182
+ await steps.waitForState('PageName', 'elementName', 'hidden');
183
+ await steps.waitForState('PageName', 'elementName', 'attached');
184
+ await steps.waitForState('PageName', 'elementName', 'detached');
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 4. Accessing the Repository Directly
190
+
191
+ Use `repo` when you need to filter by visible text, iterate all matches, or pick a random item:
192
+
193
+ ```ts
194
+ test('navigate to Forms', async ({ page, repo, steps }) => {
195
+ await steps.navigateTo('https://example.com/');
196
+ const formsLink = await repo.getByText(page, 'HomePage', 'categories', 'Forms');
197
+ await formsLink?.click();
198
+ await steps.verifyAbsence('HomePage', 'categories');
199
+ });
200
+ ```
201
+
202
+ ### Repository API
203
+
204
+ ```ts
205
+ await repo.get(page, 'PageName', 'elementName');
206
+ await repo.getAll(page, 'PageName', 'elementName');
207
+ await repo.getRandom(page, 'PageName', 'elementName');
208
+ await repo.getByText(page, 'PageName', 'elementName', 'Desired Text');
209
+ repo.getSelector('PageName', 'elementName'); // sync, returns raw selector string
210
+ ```
211
+
212
+ ---
213
+
214
+ ### ⚠️ NEVER use index-based element access — always target by context
215
+
216
+ NEVER access elements by hardcoded index (e.g. `elements[1]`, `elements[3]`). Order can change and will silently break tests. Always identify elements by visible text, labels, attributes, or sibling content:
217
+
218
+ ```ts
219
+ // ❌ Fragile — breaks if order changes
220
+ const nameCell = tableRows[1]?.locator('td:first-child');
221
+
222
+ // ✅ Robust — finds the element by its meaningful label
223
+ const nameRow = await repo.getByText(page, 'FormsPage', 'submissionEntries', 'Name');
224
+ const nameValue = await nameRow?.locator('td:nth-child(2)').textContent();
225
+ expect(nameValue?.trim()).toBe(testData.name);
226
+ ```
227
+
228
+ Before writing any verification logic against a list or table, inspect the live page via Playwright MCP to understand the structure and identify the most stable way to target each element. If no meaningful context exists to distinguish elements, stop and ask the user how to proceed.
229
+
230
+ ---
231
+
232
+ ## 5. Workflow
233
+
234
+ - After any fix, feature, or test is confirmed working, run a `git commit` with a clear message before moving on.
235
+ - Do not batch multiple successes into a single commit.
package/skills/SKILL.md DELETED
@@ -1,322 +0,0 @@
1
- ---
2
- name: skills
3
- description: >
4
- Use this skill whenever writing or generating Playwright tests in a project that uses
5
- pw-element-interactions and pw-element-repository. Triggers on any request to write a
6
- test, add a locator, create a page object, use the Steps API, or interact with elements
7
- using this stack. Also use when asked to add entries to a page-repository JSON file,
8
- use fixtures, select dropdowns, verify elements, wait for states, or perform any
9
- browser interaction through this framework. Always consult this skill before generating
10
- test code or locator JSON — do not guess API shapes or invent method signatures.
11
- ---
12
-
13
- # pw-element-interactions — Test Authoring Reference
14
-
15
- This framework separates **where elements are defined** from **how they are used**. Selectors live in a JSON file; tests reference elements by readable string keys. No raw CSS or XPath ever appears in test code.
16
-
17
- ---
18
-
19
- ## 0. Understanding the Website Structure
20
-
21
- Before writing tests or adding locators for an unfamiliar page or component, use the **Playwright MCP** to inspect the live site. This is the only reliable way to discover real selectors, element hierarchy, and page behaviour — do not guess or invent selectors from memory.
22
-
23
- Typical uses:
24
- - Navigating to a page and reading its DOM to find the right CSS selector or text for a new locator entry
25
- - Verifying that an element is actually present and visible before writing an assertion
26
- - Understanding the flow between pages before writing a multi-step test
27
-
28
- If the Playwright MCP is not connected, ask the user to install it before proceeding:
29
-
30
- ```
31
- I need the Playwright MCP to inspect the site and find accurate selectors.
32
- Please install it by adding the following to your Claude Code MCP settings:
33
-
34
- {
35
- "mcpServers": {
36
- "playwright": {
37
- "command": "npx",
38
- "args": ["@playwright/mcp@latest"]
39
- }
40
- }
41
- }
42
-
43
- Then restart Claude Code and try again.
44
- ```
45
-
46
- Do not attempt to write locators or test steps for unknown pages without first using the Playwright MCP to confirm the structure.
47
-
48
- ---
49
-
50
- ## 1. Adding Locators
51
-
52
- All selectors are stored in a single JSON file (commonly `tests/data/page-repository.json`). Each page groups its elements by name. Provide as many selector strategies as you like — the repository uses the first one that resolves:
53
-
54
- ```json
55
- {
56
- "pages": [
57
- {
58
- "name": "HomePage",
59
- "elements": [
60
- {
61
- "elementName": "submitButton",
62
- "selector": {
63
- "css": "button[data-test='submit']",
64
- "xpath": "//button[@data-test='submit']",
65
- "id": "submit-btn",
66
- "text": "Submit"
67
- }
68
- }
69
- ]
70
- }
71
- ]
72
- }
73
- ```
74
-
75
- **Naming conventions:**
76
- - `name` — PascalCase page identifier, e.g. `CheckoutPage`, `ProductDetailsPage`
77
- - `elementName` — camelCase element identifier, e.g. `submitButton`, `gallery-images`
78
-
79
- ---
80
-
81
- ## 2. Setup
82
-
83
- ### Option A — Fixtures (recommended)
84
-
85
- Define once in a fixture file; every test gets `steps`, `repo`, `interactions`, and `contextStore` for free:
86
-
87
- ```ts
88
- // tests/fixtures/base.ts
89
- import { test as base } from '@playwright/test';
90
- import { baseFixture } from 'pw-element-interactions';
91
-
92
- export const test = baseFixture(base, 'tests/data/page-repository.json');
93
- export { expect } from '@playwright/test';
94
- ```
95
-
96
- ```ts
97
- // tests/checkout.spec.ts
98
- import { test } from '../fixtures/base';
99
-
100
- test('complete checkout', async ({ steps }) => {
101
- await steps.navigateTo('/checkout');
102
- await steps.click('CheckoutPage', 'submitButton');
103
- });
104
- ```
105
-
106
- ### Option B — Manual initialisation
107
-
108
- ```ts
109
- import { ElementRepository } from 'pw-element-repository';
110
- import { Steps } from 'pw-element-interactions';
111
-
112
- const repo = new ElementRepository('tests/data/page-repository.json', 15000);
113
- const steps = new Steps(page, repo);
114
- ```
115
-
116
- ---
117
-
118
- ## 3. Steps API
119
-
120
- Every method takes `pageName` and `elementName` as its first two arguments, matching keys in your JSON file.
121
-
122
- ### 🧭 Navigation
123
-
124
- ```ts
125
- await steps.navigateTo('/path');
126
- await steps.refresh();
127
- await steps.backOrForward('BACKWARDS'); // or 'FORWARDS'
128
- await steps.setViewport(1280, 720);
129
- ```
130
-
131
- ### 🖱️ Interaction
132
-
133
- ```ts
134
- // Standard click — waits for visible, stable, actionable
135
- await steps.click('PageName', 'elementName');
136
-
137
- // Click without scrolling — use for elements behind sticky headers or overlays
138
- await steps.clickWithoutScrolling('PageName', 'elementName');
139
-
140
- // Click only if the element is visible — silently skips if not (e.g. cookie banners)
141
- await steps.clickIfPresent('PageName', 'elementName');
142
-
143
- // Click a random element from a matched list (e.g. product cards)
144
- await steps.clickRandom('PageName', 'elementName');
145
-
146
- // Hover to trigger a tooltip or dropdown
147
- await steps.hover('PageName', 'elementName');
148
-
149
- // Scroll element into view
150
- await steps.scrollIntoView('PageName', 'elementName');
151
-
152
- // Clear and type text
153
- await steps.fill('PageName', 'elementName', 'my input');
154
-
155
- // Type character by character — use for OTP inputs or fields with keyup listeners
156
- await steps.typeSequentially('PageName', 'elementName', 'my input');
157
- await steps.typeSequentially('PageName', 'elementName', 'my input', 50); // custom delay ms
158
-
159
- // Upload a file
160
- await steps.uploadFile('PageName', 'elementName', 'tests/fixtures/file.pdf');
161
-
162
- // Select from a <select> dropdown — returns the selected value
163
- import { DropdownSelectType } from 'pw-element-interactions';
164
-
165
- const value = await steps.selectDropdown('PageName', 'elementName'); // random (default)
166
- const value = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.RANDOM });
167
- const value = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.VALUE, value: 'xl' });
168
- const value = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.INDEX, index: 2 });
169
-
170
- // Drag and drop
171
- await steps.dragAndDrop('PageName', 'elementName', { target: otherLocator });
172
- await steps.dragAndDrop('PageName', 'elementName', { xOffset: 100, yOffset: 0 });
173
-
174
- // Drag a specific listed item by its text
175
- await steps.dragAndDropListedElement('PageName', 'elementName', 'Item Label', { target: otherLocator });
176
- ```
177
-
178
- ### 📊 Data Extraction
179
-
180
- ```ts
181
- const text = await steps.getText('PageName', 'elementName'); // trimmed text; '' if null
182
- const href = await steps.getAttribute('PageName', 'elementName', 'href'); // null if absent
183
- ```
184
-
185
- ### ✅ Verification
186
-
187
- ```ts
188
- // Presence / absence
189
- await steps.verifyPresence('PageName', 'elementName');
190
- await steps.verifyAbsence('PageName', 'elementName');
191
-
192
- // Text — exact match or non-empty check
193
- await steps.verifyText('PageName', 'elementName', 'Expected text');
194
- await steps.verifyText('PageName', 'elementName', undefined, { notEmpty: true });
195
-
196
- // Count
197
- await steps.verifyCount('PageName', 'elementName', { exact: 3 });
198
- await steps.verifyCount('PageName', 'elementName', { greaterThan: 0 });
199
- await steps.verifyCount('PageName', 'elementName', { lessThan: 10 });
200
-
201
- // Images — checks visibility, valid src, naturalWidth > 0, and browser decode()
202
- await steps.verifyImages('PageName', 'elementName');
203
- await steps.verifyImages('PageName', 'elementName', false); // skip scroll-into-view
204
-
205
- // URL
206
- await steps.verifyUrlContains('/dashboard');
207
- ```
208
-
209
- ### ⏳ Waiting
210
-
211
- ```ts
212
- await steps.waitForState('PageName', 'elementName'); // default: 'visible'
213
- await steps.waitForState('PageName', 'elementName', 'hidden');
214
- await steps.waitForState('PageName', 'elementName', 'attached');
215
- await steps.waitForState('PageName', 'elementName', 'detached');
216
- ```
217
-
218
- ---
219
-
220
- ## 4. Accessing the Repository Directly
221
-
222
- When you need more than a simple locator lookup — filtering by visible text, iterating all matches, or picking a random item — use `repo` directly alongside `steps`:
223
-
224
- ```ts
225
- test('navigate to Forms', async ({ page, repo, steps }) => {
226
- await steps.navigateTo('/');
227
-
228
- // Resolve a locator by the visible text it contains
229
- const formsLink = await repo.getByText(page, 'HomePage', 'categories', 'Forms');
230
- await formsLink?.click();
231
-
232
- await steps.verifyAbsence('HomePage', 'categories');
233
- });
234
- ```
235
-
236
- ### Repository API
237
-
238
- ```ts
239
- // Single locator — waits for DOM attachment
240
- await repo.get(page, 'PageName', 'elementName');
241
-
242
- // All matching locators — use for iteration
243
- await repo.getAll(page, 'PageName', 'elementName');
244
-
245
- // A random locator from the matched set — waits for visibility
246
- await repo.getRandom(page, 'PageName', 'elementName');
247
-
248
- // First locator whose visible text contains the given string
249
- await repo.getByText(page, 'PageName', 'elementName', 'Desired Text');
250
-
251
- // Sync — returns the raw selector string, e.g. "css=.btn"
252
- repo.getSelector('PageName', 'elementName');
253
- ```
254
-
255
- ---
256
-
257
- ## 5. Complete Test Example
258
-
259
- ```ts
260
- import { test } from '../fixtures/base';
261
- import { DropdownSelectType } from 'pw-element-interactions';
262
-
263
- test('add product to cart and verify', async ({ page, repo, steps }) => {
264
- await steps.navigateTo('/');
265
-
266
- // Click a category by its visible label
267
- const accessories = await repo.getByText(page, 'HomePage', 'categories', 'Accessories');
268
- await accessories?.click();
269
-
270
- // Pick a random product
271
- await steps.clickRandom('AccessoriesPage', 'productCards');
272
- await steps.verifyUrlContains('/product/');
273
-
274
- // Select a size from the dropdown
275
- const size = await steps.selectDropdown('ProductPage', 'sizeSelector', {
276
- type: DropdownSelectType.RANDOM,
277
- });
278
- console.log(`Selected size: ${size}`);
279
-
280
- // Verify the image gallery loaded correctly
281
- await steps.verifyCount('ProductPage', 'galleryImages', { greaterThan: 0 });
282
- await steps.verifyImages('ProductPage', 'galleryImages');
283
-
284
- // Verify product title is not blank
285
- await steps.verifyText('ProductPage', 'productTitle', undefined, { notEmpty: true });
286
-
287
- // Add to cart and wait for confirmation
288
- await steps.click('ProductPage', 'addToCartButton');
289
- await steps.waitForState('ProductPage', 'confirmationModal', 'visible');
290
- });
291
- ```
292
-
293
- ---
294
-
295
- ## 6. Extending Fixtures
296
-
297
- Add your own fixtures on top of the base without losing `steps`, `repo`, or `contextStore`:
298
-
299
- ```ts
300
- // tests/fixtures/base.ts
301
- import { test as base } from '@playwright/test';
302
- import { baseFixture } from 'pw-element-interactions';
303
- import { AuthService } from '../services/AuthService';
304
-
305
- type MyFixtures = { authService: AuthService };
306
-
307
- export const test = baseFixture(base, 'tests/data/page-repository.json')
308
- .extend<MyFixtures>({
309
- authService: async ({ page }, use) => {
310
- await use(new AuthService(page));
311
- },
312
- });
313
-
314
- export { expect } from '@playwright/test';
315
- ```
316
-
317
- ```ts
318
- test('authenticated flow', async ({ steps, authService }) => {
319
- await authService.login('user@test.com', 'secret');
320
- await steps.verifyUrlContains('/dashboard');
321
- });
322
- ```