pw-element-interactions 0.0.7 โ†’ 0.0.9

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.
package/README.md CHANGED
@@ -236,6 +236,7 @@ The `Steps` class automatically handles fetching the Playwright `Locator` using
236
236
  * **`fill(pageName: string, elementName: string, text: string)`**: Clears any existing value in the target input field and types the provided text.
237
237
  * **`uploadFile(pageName: string, elementName: string, filePath: string)`**: Uploads a local file from the provided `filePath` to an `<input type="file">` element.
238
238
  * **`selectDropdown(pageName: string, elementName: string, options?: DropdownSelectOptions)`**: Selects an option from a `<select>` element and returns its `value`. Defaults to a random, non-disabled option (`{ type: DropdownSelectType.RANDOM }`). Alternatively, select by exact value (`{ type: DropdownSelectType.VALUE, value: '...' }`) or zero-based index (`{ type: DropdownSelectType.INDEX, index: 1 }`).
239
+ * **`typeSequentially(pageName: string, elementName: string, text: string, delay?: number)`**: Types the provided text into an element character by character with a configurable delay between key presses (defaults to `100ms`). Ideal for OTP inputs, search bars, or any field with `keyup` listeners that do not respond correctly to bulk `fill()` operations.
239
240
 
240
241
  ### ๐Ÿ“Š Data Extraction
241
242
 
@@ -273,4 +274,74 @@ await interactions.interact.clickWithoutScrolling(customLocator);
273
274
  await interactions.verify.count(customLocator, { greaterThan: 2 });
274
275
  ```
275
276
 
276
- *Note: All core interaction (`interact`), verification (`verify`), and navigation (`navigate`) methods are also available when using `ElementInteractions` directly.*
277
+ *Note: All core interaction (`interact`), verification (`verify`), and navigation (`navigate`) methods are also available when using `ElementInteractions` directly.*
278
+
279
+ ---
280
+
281
+ ## ๐Ÿค Contributing
282
+
283
+ Contributions are welcome! Please read the rules below carefully before opening a PR โ€” they exist to keep the architecture clean, the test suite reliable, and the codebase consistent.
284
+
285
+ ### ๐Ÿงช Testing Locally Before Opening a PR
286
+
287
+ Before pushing changes, verify your implementation works end-to-end in a real consumer project using [`yalc`](https://github.com/wclr/yalc) โ€” a local package publishing tool that mirrors the npm install flow without actually publishing.
288
+
289
+ ```bash
290
+ # 1. Install yalc globally (one-time setup)
291
+ npm i -g yalc
292
+
293
+ # 2. In the pw-element-interactions folder โ€” publish to the local yalc store
294
+ yalc publish
295
+
296
+ # 3. In your consumer project โ€” add the locally published package
297
+ yalc add pw-element-interactions
298
+ ```
299
+
300
+ After making further changes, push updates to the consumer project without re-adding:
301
+
302
+ ```bash
303
+ # In pw-element-interactions
304
+ yalc publish --push
305
+ ```
306
+
307
+ To restore the original npm version when you're done:
308
+
309
+ ```bash
310
+ # In your consumer project
311
+ yalc remove pw-element-interactions
312
+ npm install
313
+ ```
314
+
315
+ ### ๐Ÿ“‹ PR Guidelines
316
+
317
+ PRs must respect the layered architecture of this library. Every new capability follows a strict implementation order:
318
+
319
+ 1. **Implement in the appropriate class first.** The core method must be added to the correct underlying class (`interact`, `verify`, `navigate`, or similar) before it is exposed anywhere else. Do not add a method only to `Steps` or `ElementInteractions` without first placing the logic in the right domain class.
320
+ 2. **Then expose it via `Steps`.** Once the core method exists in its proper class, add the corresponding wrapper to the `Steps` (CommonSteps) class so it is accessible through the unified API.
321
+
322
+ PRs that skip step 1 and add convenience methods without a properly placed underlying implementation will not be merged.
323
+
324
+ ### ๐Ÿชต Logging
325
+
326
+ The logging responsibility is clearly divided and must be respected:
327
+
328
+ * **Interaction methods must not contain any logs.** Keep them focused purely on the mechanics of the action.
329
+ * **`Steps` methods are responsible for logging.** Every `Steps` wrapper should log what action is being performed, providing observability at the right level of abstraction.
330
+
331
+ ### ๐Ÿงฌ Unit Tests
332
+
333
+ Every new interaction method must be accompanied by a unit test.
334
+
335
+ Unit tests are run against the proprietary Vue test application at [https://github.com/Umutayb/vue-test-app](https://github.com/Umutayb/vue-test-app). This app is built from its Docker image during the CI pipeline to serve as the test target. All new tests must use this app.
336
+
337
+ If the component or UI element needed to test a new interaction does not exist in the Vue test app, **you must add it there first**:
338
+
339
+ 1. Open a PR against `vue-test-app` to add the required component.
340
+ 2. Wait for that PR to be merged.
341
+ 3. Only then open or update the PR in this repository that adds the interaction and its test.
342
+
343
+ PRs that require a missing component but do not have a corresponding merged `vue-test-app` PR will not be merged.
344
+
345
+ ### ๐Ÿ“ Documentation
346
+
347
+ Every new `Steps` method must be documented in the [API Reference](#๏ธ-api-reference-steps) section of this README. Add your method to the appropriate group (Navigation, Interaction, Data Extraction, Verification, or Wait) following the existing format: method signature, a plain-English description of what it does, and any relevant parameter or return value notes. PRs that add a public method without a corresponding README entry will not be merged.
@@ -83,4 +83,13 @@ export declare class Interactions {
83
83
  * @returns A promise that resolves to the matched Playwright Locator, or null if not found.
84
84
  */
85
85
  getByText(baseLocator: Locator, pageName: string, elementName: string, desiredText: string, strict?: boolean): Promise<ReturnType<Page['locator']> | null>;
86
+ /**
87
+ * Types into the target element character by character with a specified delay.
88
+ * Use this for OTP inputs, search-as-you-type fields, or when `fill()`
89
+ * doesn't trigger necessary keyboard events (like 'keyup' or 'keydown').
90
+ * @param locator - The Playwright Locator pointing to the input element.
91
+ * @param text - The string of text to type sequentially.
92
+ * @param delay - Time in milliseconds to wait between key presses. Defaults to 100ms.
93
+ */
94
+ typeSequentially(locator: Locator, text: string, delay?: number): Promise<void>;
86
95
  }
@@ -196,5 +196,20 @@ class Interactions {
196
196
  }
197
197
  return locator;
198
198
  }
199
+ /**
200
+ * Types into the target element character by character with a specified delay.
201
+ * Use this for OTP inputs, search-as-you-type fields, or when `fill()`
202
+ * doesn't trigger necessary keyboard events (like 'keyup' or 'keydown').
203
+ * @param locator - The Playwright Locator pointing to the input element.
204
+ * @param text - The string of text to type sequentially.
205
+ * @param delay - Time in milliseconds to wait between key presses. Defaults to 100ms.
206
+ */
207
+ async typeSequentially(locator, text, delay = 100) {
208
+ await this.utils.waitForState(locator, 'visible');
209
+ await locator.pressSequentially(text, {
210
+ delay,
211
+ timeout: this.ELEMENT_TIMEOUT
212
+ });
213
+ }
199
214
  }
200
215
  exports.Interactions = Interactions;
@@ -183,4 +183,13 @@ export declare class Steps {
183
183
  * @param state - The state to wait for: 'visible' | 'attached' | 'hidden' | 'detached'. Defaults to 'visible'.
184
184
  */
185
185
  waitForState(pageName: string, elementName: string, state?: 'visible' | 'attached' | 'hidden' | 'detached'): Promise<void>;
186
+ /**
187
+ * Types text into a specific element character by character with a delay.
188
+ * Ideal for OTP inputs, search bars, or fields with sensitive 'keyup' listeners.
189
+ * @param pageName - The page or component grouping name in your repository.
190
+ * @param elementName - The specific element name in your repository.
191
+ * @param text - The string to type sequentially.
192
+ * @param delay - Optional delay between key presses in milliseconds (defaults to 100).
193
+ */
194
+ typeSequentially(pageName: string, elementName: string, text: string, delay?: number): Promise<void>;
186
195
  }
@@ -298,5 +298,18 @@ class Steps {
298
298
  const locator = await this.repo.get(this.page, pageName, elementName);
299
299
  await this.utils.waitForState(locator, state);
300
300
  }
301
+ /**
302
+ * Types text into a specific element character by character with a delay.
303
+ * Ideal for OTP inputs, search bars, or fields with sensitive 'keyup' listeners.
304
+ * @param pageName - The page or component grouping name in your repository.
305
+ * @param elementName - The specific element name in your repository.
306
+ * @param text - The string to type sequentially.
307
+ * @param delay - Optional delay between key presses in milliseconds (defaults to 100).
308
+ */
309
+ async typeSequentially(pageName, elementName, text, delay = 100) {
310
+ console.log(`[Step] -> Typing "${text}" sequentially into '${elementName}' in '${pageName}' (Delay: ${delay}ms)`);
311
+ const locator = await this.repo.get(this.page, pageName, elementName);
312
+ await this.interact.typeSequentially(locator, text, delay);
313
+ }
301
314
  }
302
315
  exports.Steps = Steps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pw-element-interactions",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "A robust, readable interaction and assertion Facade for Playwright. Abstract away boilerplate into semantic, English-like methods, making your test automation framework cleaner, easier to maintain, and accessible to non-developers.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,10 +8,13 @@
8
8
  "clean": "rm -rf dist",
9
9
  "build": "npm run clean && tsc",
10
10
  "test": "npx playwright test",
11
- "prepublishOnly": "npm run build"
11
+ "prepublishOnly": "npm run build",
12
+ "postinstall": "node scripts/postinstall.js"
12
13
  },
13
14
  "files": [
14
- "dist"
15
+ "dist",
16
+ "skills",
17
+ "scripts/postinstall.js"
15
18
  ],
16
19
  "peerDependencies": {
17
20
  "@civitas-cerebrum/context-store": ">=0.0.2",
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // Resolve the root of the consuming project (three levels up from
7
+ // node_modules/pw-element-interactions/scripts/postinstall.js)
8
+ const projectRoot = path.resolve(__dirname, '..', '..', '..');
9
+
10
+ const src = path.join(__dirname, '..', 'skills', 'SKILL.md');
11
+ const dest = path.join(projectRoot, '.claude', 'skills', 'pw-element-interactions', 'SKILL.md');
12
+
13
+ try {
14
+ if (!fs.existsSync(src)) {
15
+ console.warn('[pw-element-interactions] Skill file not found, skipping.');
16
+ process.exit(0);
17
+ }
18
+
19
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
20
+ fs.copyFileSync(src, dest);
21
+ console.log('[pw-element-interactions] โœ” Claude Code skill installed โ€” restart Claude Code to pick it up.');
22
+ } catch (err) {
23
+ // Never fail the install โ€” skill copy is best-effort
24
+ console.warn(`[pw-element-interactions] Could not install Claude Code skill: ${err.message}`);
25
+ }
@@ -0,0 +1,322 @@
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
+ ```