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.
- package/README.md +121 -117
- package/dist/fixture/BaseFixture.js +10 -0
- package/dist/interactions/Interaction.js +5 -3
- package/dist/interactions/Navigation.d.ts +8 -4
- package/dist/interactions/Navigation.js +17 -5
- package/dist/interactions/Verification.js +6 -6
- package/dist/logger/Logger.d.ts +25 -0
- package/dist/logger/Logger.js +66 -0
- package/dist/steps/CommonSteps.d.ts +0 -145
- package/dist/steps/CommonSteps.js +38 -172
- package/dist/utils/ElementUtilities.js +3 -3
- package/package.json +8 -3
- package/scripts/postinstall.js +1 -1
- package/skills/pw-element-interactions.md +235 -0
- package/skills/SKILL.md +0 -322
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A robust set of Playwright steps for readable interaction and assertions.
|
|
|
8
8
|
|
|
9
9
|
### ✨ The Unified Steps API
|
|
10
10
|
|
|
11
|
-
With the introduction of the `Steps` class, you can now combine your element repository and interactions into a single, flattened
|
|
11
|
+
With the introduction of the `Steps` class, you can now combine your element repository and interactions into a single, flattened facade. This eliminates repetitive locator fetching and transforms your tests into clean, plain-English steps.
|
|
12
12
|
|
|
13
13
|
### 🤖 AI-Friendly Test Development & Boilerplate Reduction
|
|
14
14
|
|
|
@@ -19,56 +19,82 @@ Because the API is highly semantic and completely decoupled from the DOM, it is
|
|
|
19
19
|
**Before (Raw Playwright):**
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
|
-
//
|
|
22
|
+
// Hardcode or manage raw locators inside your test
|
|
23
23
|
const submitBtn = page.locator('button[data-test="submit-order"]');
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// Explicitly wait for DOM stability and visibility
|
|
26
26
|
await submitBtn.waitFor({ state: 'visible', timeout: 30000 });
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// Perform the interaction
|
|
29
29
|
await submitBtn.click();
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
**
|
|
32
|
+
**After (pw-element-interactions):**
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
|
-
//
|
|
35
|
+
// Locate, wait, and interact — one line
|
|
36
36
|
await steps.click('CheckoutPage', 'submitButton');
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
Because the API is semantic and decoupled from the DOM, it also works exceptionally well with AI coding assistants. Models can generate robust test flows using plain-English strings (`'CheckoutPage'`, `'submitButton'`) without hallucinating CSS selectors or writing flaky interactions.
|
|
40
|
+
|
|
39
41
|
---
|
|
40
42
|
|
|
41
43
|
## 📦 Installation
|
|
42
44
|
|
|
43
|
-
Install the package via your preferred package manager:
|
|
44
|
-
|
|
45
45
|
```bash
|
|
46
46
|
npm i pw-element-interactions
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
**Peer
|
|
50
|
-
This package requires `@playwright/test` to be installed in your project. If you are using the `Steps` API, you will also need `pw-element-repository`.
|
|
49
|
+
**Peer dependencies:** `@playwright/test` is required. The `Steps` API additionally requires `pw-element-repository`.
|
|
51
50
|
|
|
52
51
|
---
|
|
53
52
|
|
|
54
|
-
##
|
|
53
|
+
## ✨ Features
|
|
55
54
|
|
|
56
|
-
* **Zero
|
|
57
|
-
* **
|
|
58
|
-
* **
|
|
59
|
-
* **
|
|
60
|
-
* **
|
|
61
|
-
* **
|
|
62
|
-
* **
|
|
63
|
-
* **Advanced Drag & Drop:** Seamlessly drag elements to other elements, drop them at specific coordinate offsets, or combine both strategies natively.
|
|
55
|
+
* **Zero locator boilerplate** — The `Steps` API fetches elements and interacts with them in a single call.
|
|
56
|
+
* **Automatic failure screenshots** — `baseFixture` captures a full-page screenshot on every failed test and attaches it to the HTML report.
|
|
57
|
+
* **Standardized waiting** — Built-in methods wait for elements to reach specific DOM states (visible, hidden, attached, detached).
|
|
58
|
+
* **Advanced image verification** — `verifyImages` evaluates actual browser decoding and `naturalWidth`, not just DOM presence.
|
|
59
|
+
* **Smart dropdowns** — Select by value, index, or randomly, with automatic skipping of disabled and empty options.
|
|
60
|
+
* **Flexible assertions** — Verify exact text, non-empty text, URL substrings, or dynamic element counts (greater than, less than, exact).
|
|
61
|
+
* **Drag and drop** — Drag to other elements, to coordinate offsets, or combine both strategies.
|
|
64
62
|
|
|
65
63
|
---
|
|
66
64
|
|
|
67
|
-
##
|
|
65
|
+
## 🗂️ Defining Locators
|
|
66
|
+
|
|
67
|
+
All selectors live in a page repository JSON file — the single source of truth for element locations. No raw selectors should appear in test code.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"pages": [
|
|
72
|
+
{
|
|
73
|
+
"name": "HomePage",
|
|
74
|
+
"elements": [
|
|
75
|
+
{
|
|
76
|
+
"elementName": "submitButton",
|
|
77
|
+
"selector": {
|
|
78
|
+
"css": "button[data-test='submit']"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Each selector object supports `css`, `xpath`, `id`, or `text` as the locator strategy.
|
|
88
|
+
|
|
89
|
+
**Naming conventions:**
|
|
90
|
+
- `name` — PascalCase page identifier, e.g. `CheckoutPage`, `ProductDetailsPage`
|
|
91
|
+
- `elementName` — camelCase element identifier, e.g. `submitButton`, `galleryImages`
|
|
92
|
+
|
|
93
|
+
---
|
|
68
94
|
|
|
69
|
-
|
|
95
|
+
## 💻 Usage: The `Steps` API (Recommended)
|
|
70
96
|
|
|
71
|
-
|
|
97
|
+
Initialize `Steps` by passing the current Playwright `page` and your `ElementRepository` instance.
|
|
72
98
|
|
|
73
99
|
```ts
|
|
74
100
|
import { test } from '@playwright/test';
|
|
@@ -76,34 +102,23 @@ import { ElementRepository } from 'pw-element-repository';
|
|
|
76
102
|
import { Steps, DropdownSelectType } from 'pw-element-interactions';
|
|
77
103
|
|
|
78
104
|
test('Add random product and verify image gallery', async ({ page }) => {
|
|
79
|
-
// 1. Initialize Repository & Steps
|
|
80
105
|
const repo = new ElementRepository('tests/data/locators.json');
|
|
81
106
|
const steps = new Steps(page, repo);
|
|
82
107
|
|
|
83
|
-
// 2. Navigate
|
|
84
108
|
await steps.navigateTo('/');
|
|
85
|
-
|
|
86
|
-
// 3. Direct Interaction (Fetches and clicks in one line)
|
|
87
109
|
await steps.click('HomePage', 'category-accessories');
|
|
88
110
|
|
|
89
|
-
// 4. Randomized Acquisition & Action
|
|
90
111
|
await steps.clickRandom('AccessoriesPage', 'product-cards');
|
|
91
112
|
await steps.verifyUrlContains('/product/');
|
|
92
113
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
type: DropdownSelectType.RANDOM
|
|
114
|
+
const selectedSize = await steps.selectDropdown('ProductDetailsPage', 'size-selector', {
|
|
115
|
+
type: DropdownSelectType.RANDOM,
|
|
96
116
|
});
|
|
97
117
|
console.log(`Selected size: ${selectedSize}`);
|
|
98
118
|
|
|
99
|
-
// 6. Flexible Assertions & Data Extraction
|
|
100
119
|
await steps.verifyCount('ProductDetailsPage', 'gallery-images', { greaterThan: 0 });
|
|
101
120
|
await steps.verifyText('ProductDetailsPage', 'product-title', undefined, { notEmpty: true });
|
|
102
|
-
|
|
103
|
-
// 7. Advanced Image Verification
|
|
104
121
|
await steps.verifyImages('ProductDetailsPage', 'gallery-images');
|
|
105
|
-
|
|
106
|
-
// 8. Explicit Waits
|
|
107
122
|
await steps.waitForState('CheckoutPage', 'confirmation-modal', 'visible');
|
|
108
123
|
});
|
|
109
124
|
```
|
|
@@ -112,9 +127,9 @@ test('Add random product and verify image gallery', async ({ page }) => {
|
|
|
112
127
|
|
|
113
128
|
## 🔧 Fixtures: Zero-Setup Tests (Recommended)
|
|
114
129
|
|
|
115
|
-
For larger projects, manually initializing `repo` and `steps`
|
|
130
|
+
For larger projects, manually initializing `repo` and `steps` in every test becomes repetitive. `baseFixture` injects all core dependencies automatically via Playwright's fixture system.
|
|
116
131
|
|
|
117
|
-
###
|
|
132
|
+
### Included fixtures
|
|
118
133
|
|
|
119
134
|
| Fixture | Type | Description |
|
|
120
135
|
|---|---|---|
|
|
@@ -123,9 +138,27 @@ For larger projects, manually initializing `repo` and `steps` inside every test
|
|
|
123
138
|
| `interactions` | `ElementInteractions` | Raw interactions API for custom locators |
|
|
124
139
|
| `contextStore` | `ContextStore` | Shared in-memory store for passing data between steps |
|
|
125
140
|
|
|
126
|
-
|
|
141
|
+
`baseFixture` also attaches a full-page `failure-screenshot` to the Playwright HTML report on every failed test.
|
|
142
|
+
|
|
143
|
+
> **Note:** `reporter: 'html'` must be set in `playwright.config.ts` for screenshots to appear. Run `npx playwright show-report` after a failed run to inspect them.
|
|
144
|
+
|
|
145
|
+
### 1. Playwright Config
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// playwright.config.ts
|
|
149
|
+
import { defineConfig } from '@playwright/test';
|
|
150
|
+
|
|
151
|
+
export default defineConfig({
|
|
152
|
+
testDir: './tests',
|
|
153
|
+
reporter: 'html',
|
|
154
|
+
use: {
|
|
155
|
+
baseURL: 'https://your-project-url.com',
|
|
156
|
+
headless: true,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
127
160
|
|
|
128
|
-
|
|
161
|
+
### 2. Create your fixture file
|
|
129
162
|
|
|
130
163
|
```ts
|
|
131
164
|
// tests/fixtures/base.ts
|
|
@@ -136,9 +169,7 @@ export const test = baseFixture(base, 'tests/data/page-repository.json');
|
|
|
136
169
|
export { expect };
|
|
137
170
|
```
|
|
138
171
|
|
|
139
|
-
###
|
|
140
|
-
|
|
141
|
-
Import `test` from your fixture file. All four fixtures are available as named parameters — no setup code required:
|
|
172
|
+
### 3. Use fixtures in your tests
|
|
142
173
|
|
|
143
174
|
```ts
|
|
144
175
|
// tests/checkout.spec.ts
|
|
@@ -161,9 +192,7 @@ test('Complete checkout flow', async ({ steps }) => {
|
|
|
161
192
|
});
|
|
162
193
|
```
|
|
163
194
|
|
|
164
|
-
###
|
|
165
|
-
|
|
166
|
-
For advanced queries like resolving a locator by visible text, destructure `repo` alongside `steps`:
|
|
195
|
+
### 4. Access `repo` directly when needed
|
|
167
196
|
|
|
168
197
|
```ts
|
|
169
198
|
test('Navigate to Forms category', async ({ page, repo, steps }) => {
|
|
@@ -176,9 +205,9 @@ test('Navigate to Forms category', async ({ page, repo, steps }) => {
|
|
|
176
205
|
});
|
|
177
206
|
```
|
|
178
207
|
|
|
179
|
-
###
|
|
208
|
+
### 5. Extend with your own fixtures
|
|
180
209
|
|
|
181
|
-
Because `baseFixture` returns a standard Playwright `test` object, you can
|
|
210
|
+
Because `baseFixture` returns a standard Playwright `test` object, you can layer your own fixtures on top:
|
|
182
211
|
|
|
183
212
|
```ts
|
|
184
213
|
// tests/fixtures/base.ts
|
|
@@ -201,8 +230,6 @@ export const test = testWithBase.extend<MyFixtures>({
|
|
|
201
230
|
export { expect } from '@playwright/test';
|
|
202
231
|
```
|
|
203
232
|
|
|
204
|
-
All fixtures are then available together in any test:
|
|
205
|
-
|
|
206
233
|
```ts
|
|
207
234
|
test('Authenticated flow', async ({ steps, authService }) => {
|
|
208
235
|
await authService.login('user@test.com', 'secret');
|
|
@@ -214,134 +241,111 @@ test('Authenticated flow', async ({ steps, authService }) => {
|
|
|
214
241
|
|
|
215
242
|
## 🛠️ API Reference: `Steps`
|
|
216
243
|
|
|
217
|
-
|
|
244
|
+
Every method below automatically fetches the Playwright `Locator` using your `pageName` and `elementName` keys from the repository.
|
|
218
245
|
|
|
219
246
|
### 🧭 Navigation
|
|
220
247
|
|
|
221
|
-
* **`navigateTo(url: string)
|
|
222
|
-
* **`refresh()
|
|
223
|
-
* **`backOrForward(direction: 'BACKWARDS' | 'FORWARDS')
|
|
224
|
-
* **`setViewport(width: number, height: number)
|
|
248
|
+
* **`navigateTo(url: string)`** — Navigates the browser to the specified absolute or relative URL.
|
|
249
|
+
* **`refresh()`** — Reloads the current page.
|
|
250
|
+
* **`backOrForward(direction: 'BACKWARDS' | 'FORWARDS')`** — Navigates the browser history stack in the given direction.
|
|
251
|
+
* **`setViewport(width: number, height: number)`** — Resizes the browser viewport to the specified pixel dimensions.
|
|
225
252
|
|
|
226
253
|
### 🖱️ Interaction
|
|
227
254
|
|
|
228
|
-
* **`click(pageName
|
|
229
|
-
* **`clickWithoutScrolling(pageName
|
|
230
|
-
* **`clickIfPresent(pageName
|
|
231
|
-
* **`clickRandom(pageName
|
|
232
|
-
* **`hover(pageName
|
|
233
|
-
* **`scrollIntoView(pageName
|
|
234
|
-
* **`dragAndDrop(pageName
|
|
235
|
-
* **`dragAndDropListedElement(pageName
|
|
236
|
-
* **`fill(pageName
|
|
237
|
-
* **`uploadFile(pageName
|
|
238
|
-
* **`selectDropdown(pageName
|
|
239
|
-
* **`typeSequentially(pageName
|
|
255
|
+
* **`click(pageName, elementName)`** — Clicks an element. Automatically waits for the element to be attached, visible, stable, and actionable.
|
|
256
|
+
* **`clickWithoutScrolling(pageName, elementName)`** — Dispatches a native `click` event directly, bypassing Playwright's scrolling and intersection observer checks. Useful for elements obscured by sticky headers or overlays.
|
|
257
|
+
* **`clickIfPresent(pageName, elementName)`** — Clicks an element only if it is visible; skips silently otherwise. Ideal for optional elements like cookie banners.
|
|
258
|
+
* **`clickRandom(pageName, elementName)`** — Clicks a random element from all matches. Useful for lists or grids.
|
|
259
|
+
* **`hover(pageName, elementName)`** — Hovers over an element to trigger dropdowns or tooltips.
|
|
260
|
+
* **`scrollIntoView(pageName, elementName)`** — Smoothly scrolls an element into the viewport.
|
|
261
|
+
* **`dragAndDrop(pageName, elementName, options: DragAndDropOptions)`** — Drags an element to a target element (`{ target: Locator }`), by coordinate offset (`{ xOffset, yOffset }`), or both.
|
|
262
|
+
* **`dragAndDropListedElement(pageName, elementName, elementText, options: DragAndDropOptions)`** — Finds a specific element by its text from a list, then drags it to a destination.
|
|
263
|
+
* **`fill(pageName, elementName, text: string)`** — Clears and fills an input field with the provided text.
|
|
264
|
+
* **`uploadFile(pageName, elementName, filePath: string)`** — Uploads a file to an `<input type="file">` element.
|
|
265
|
+
* **`selectDropdown(pageName, elementName, options?: DropdownSelectOptions)`** — Selects an option from a `<select>` element and returns its `value`. Defaults to `{ type: DropdownSelectType.RANDOM }`. Also supports `VALUE` (exact match) and `INDEX` (zero-based).
|
|
266
|
+
* **`typeSequentially(pageName, elementName, text: string, delay?: number)`** — Types text character by character with a configurable delay (default `100ms`). Ideal for OTP inputs or fields with `keyup` listeners.
|
|
240
267
|
|
|
241
268
|
### 📊 Data Extraction
|
|
242
269
|
|
|
243
|
-
* **`getText(pageName
|
|
244
|
-
* **`getAttribute(pageName
|
|
270
|
+
* **`getText(pageName, elementName)`** — Returns the trimmed text content of an element, or an empty string if null.
|
|
271
|
+
* **`getAttribute(pageName, elementName, attributeName: string)`** — Returns the value of an HTML attribute (e.g. `href`, `aria-pressed`), or `null` if it doesn't exist.
|
|
245
272
|
|
|
246
273
|
### ✅ Verification
|
|
247
274
|
|
|
248
|
-
* **`verifyPresence(pageName
|
|
249
|
-
* **`verifyAbsence(pageName
|
|
250
|
-
* **`verifyText(pageName
|
|
251
|
-
* **`verifyCount(pageName
|
|
252
|
-
* **`verifyImages(pageName
|
|
253
|
-
* **`verifyUrlContains(text: string)
|
|
275
|
+
* **`verifyPresence(pageName, elementName)`** — Asserts that an element is attached to the DOM and visible.
|
|
276
|
+
* **`verifyAbsence(pageName, elementName)`** — Asserts that an element is hidden or detached from the DOM.
|
|
277
|
+
* **`verifyText(pageName, elementName, expectedText?, options?: TextVerifyOptions)`** — Asserts element text. Provide `expectedText` for an exact match, or `{ notEmpty: true }` to assert the text is not blank.
|
|
278
|
+
* **`verifyCount(pageName, elementName, options: CountVerifyOptions)`** — Asserts element count. Accepts `{ exact: number }`, `{ greaterThan: number }`, or `{ lessThan: number }`.
|
|
279
|
+
* **`verifyImages(pageName, elementName, scroll?: boolean)`** — Verifies image rendering: checks visibility, valid `src`, `naturalWidth > 0`, and the browser's native `decode()` promise. Scrolls into view by default.
|
|
280
|
+
* **`verifyUrlContains(text: string)`** — Asserts that the current URL contains the expected substring.
|
|
254
281
|
|
|
255
282
|
### ⏳ Wait
|
|
256
283
|
|
|
257
|
-
* **`waitForState(pageName
|
|
284
|
+
* **`waitForState(pageName, elementName, state?: 'visible' | 'attached' | 'hidden' | 'detached')`** — Waits for an element to reach a specific DOM state. Defaults to `'visible'`.
|
|
258
285
|
|
|
259
286
|
---
|
|
260
287
|
|
|
261
|
-
## 🧱 Advanced
|
|
288
|
+
## 🧱 Advanced: Raw Interactions API
|
|
262
289
|
|
|
263
|
-
|
|
290
|
+
To bypass the repository or work with dynamically generated locators, use `ElementInteractions` directly:
|
|
264
291
|
|
|
265
292
|
```ts
|
|
266
293
|
import { ElementInteractions } from 'pw-element-interactions';
|
|
267
294
|
|
|
268
|
-
// Initialize
|
|
269
295
|
const interactions = new ElementInteractions(page);
|
|
270
296
|
|
|
271
|
-
// Pass Playwright Locators directly
|
|
272
297
|
const customLocator = page.locator('button.dynamic-class');
|
|
273
298
|
await interactions.interact.clickWithoutScrolling(customLocator);
|
|
274
299
|
await interactions.verify.count(customLocator, { greaterThan: 2 });
|
|
275
300
|
```
|
|
276
301
|
|
|
277
|
-
|
|
302
|
+
All core `interact`, `verify`, and `navigate` methods are available on `ElementInteractions`.
|
|
278
303
|
|
|
279
304
|
---
|
|
280
305
|
|
|
281
306
|
## 🤝 Contributing
|
|
282
307
|
|
|
283
|
-
Contributions are welcome! Please read the
|
|
308
|
+
Contributions are welcome! Please read the guidelines below before opening a PR.
|
|
284
309
|
|
|
285
|
-
### 🧪 Testing
|
|
310
|
+
### 🧪 Testing locally
|
|
286
311
|
|
|
287
|
-
|
|
312
|
+
Verify your changes end-to-end in a real consumer project using [`yalc`](https://github.com/wclr/yalc):
|
|
288
313
|
|
|
289
314
|
```bash
|
|
290
|
-
#
|
|
315
|
+
# Install yalc globally (one-time)
|
|
291
316
|
npm i -g yalc
|
|
292
317
|
|
|
293
|
-
#
|
|
318
|
+
# In the pw-element-interactions folder
|
|
294
319
|
yalc publish
|
|
295
320
|
|
|
296
|
-
#
|
|
321
|
+
# In your consumer project
|
|
297
322
|
yalc add pw-element-interactions
|
|
298
323
|
```
|
|
299
324
|
|
|
300
|
-
|
|
325
|
+
Push updates without re-adding:
|
|
301
326
|
|
|
302
327
|
```bash
|
|
303
|
-
# In pw-element-interactions
|
|
304
328
|
yalc publish --push
|
|
305
329
|
```
|
|
306
330
|
|
|
307
|
-
|
|
331
|
+
Restore the original npm version when done:
|
|
308
332
|
|
|
309
333
|
```bash
|
|
310
|
-
# In your consumer project
|
|
311
334
|
yalc remove pw-element-interactions
|
|
312
335
|
npm install
|
|
313
336
|
```
|
|
314
337
|
|
|
315
|
-
### 📋 PR
|
|
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.
|
|
338
|
+
### 📋 PR guidelines
|
|
334
339
|
|
|
335
|
-
|
|
340
|
+
**Architecture.** Every new capability must follow this order:
|
|
336
341
|
|
|
337
|
-
|
|
342
|
+
1. Implement the core method in the appropriate domain class (`interact`, `verify`, `navigate`, etc.).
|
|
343
|
+
2. Expose it via a `Steps` wrapper.
|
|
338
344
|
|
|
339
|
-
|
|
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.
|
|
345
|
+
PRs that skip step 1 will not be merged.
|
|
342
346
|
|
|
343
|
-
|
|
347
|
+
**Logging.** Core interaction methods must not contain any logs. `Steps` wrappers are responsible for logging what action is being performed.
|
|
344
348
|
|
|
345
|
-
|
|
349
|
+
**Unit tests.** Every new method must include a unit test. Tests run against the [Vue test app](https://github.com/Umutayb/vue-test-app), which is built from its Docker image during CI. If the component you need doesn't exist in the test app, open a PR there first and wait for it to merge before updating this repository.
|
|
346
350
|
|
|
347
|
-
Every new `Steps` method must be
|
|
351
|
+
**Documentation.** Every new `Steps` method must be added to the [API Reference](#️-api-reference-steps) section of this README, following the existing format. PRs without documentation will not be merged.
|
|
@@ -20,5 +20,15 @@ function baseFixture(baseTest, locatorPath) {
|
|
|
20
20
|
contextStore: async ({}, use) => {
|
|
21
21
|
await use(new context_store_1.ContextStore());
|
|
22
22
|
},
|
|
23
|
+
page: async ({ page }, use, testInfo) => {
|
|
24
|
+
await use(page);
|
|
25
|
+
if (testInfo.status !== testInfo.expectedStatus) {
|
|
26
|
+
const screenshot = await page.screenshot({ fullPage: true });
|
|
27
|
+
await testInfo.attach('failure-screenshot', {
|
|
28
|
+
body: screenshot,
|
|
29
|
+
contentType: 'image/png',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
},
|
|
23
33
|
});
|
|
24
34
|
}
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Interactions = void 0;
|
|
4
4
|
const Options_1 = require("../enum/Options");
|
|
5
5
|
const ElementUtilities_1 = require("../utils/ElementUtilities");
|
|
6
|
+
const Logger_1 = require("../logger/Logger");
|
|
7
|
+
const log = (0, Logger_1.createLogger)('interactions');
|
|
6
8
|
/**
|
|
7
9
|
* The `Interactions` class provides a robust set of methods for interacting
|
|
8
10
|
* with DOM elements via Playwright Locators. It abstracts away common boilerplate
|
|
@@ -52,7 +54,7 @@ class Interactions {
|
|
|
52
54
|
await locator.click({ timeout: this.ELEMENT_TIMEOUT });
|
|
53
55
|
}
|
|
54
56
|
else {
|
|
55
|
-
|
|
57
|
+
log('Locator was not visible, skipping click');
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
@@ -71,7 +73,7 @@ class Interactions {
|
|
|
71
73
|
*/
|
|
72
74
|
async uploadFile(locator, filePath) {
|
|
73
75
|
await this.utils.waitForState(locator, 'attached');
|
|
74
|
-
|
|
76
|
+
log('Uploading file from path "%s"', filePath);
|
|
75
77
|
await locator.setInputFiles(filePath, { timeout: this.ELEMENT_TIMEOUT });
|
|
76
78
|
}
|
|
77
79
|
/**
|
|
@@ -191,7 +193,7 @@ class Interactions {
|
|
|
191
193
|
const msg = `Element '${elementName}' on '${pageName}' with text "${desiredText}" not found.\nAvailable texts found in locator: ${availableTexts.length > 0 ? `\n- ${availableTexts.join('\n- ')}` : 'None (Base locator found no elements or elements had no text)'}`;
|
|
192
194
|
if (strict)
|
|
193
195
|
throw new Error(msg);
|
|
194
|
-
|
|
196
|
+
log('⚠ %s', msg);
|
|
195
197
|
return null;
|
|
196
198
|
}
|
|
197
199
|
return locator;
|
|
@@ -11,10 +11,14 @@ export declare class Navigation {
|
|
|
11
11
|
*/
|
|
12
12
|
constructor(page: Page);
|
|
13
13
|
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
* Navigates the active browser page to the specified URL.
|
|
15
|
+
* Automatically waits for the page to reach the default 'load' state.
|
|
16
|
+
* @param url - The absolute or relative URL to navigate to.
|
|
17
|
+
* Relative URLs (e.g. '/path') are resolved against `baseURL` from playwright.config.ts.
|
|
18
|
+
* Protocol-relative URLs (e.g. '//example.com') are passed directly to the browser.
|
|
19
|
+
* ⚠️ If a relative URL is passed and no baseURL is configured, an error will be thrown.
|
|
20
|
+
* Prefer fully qualified URLs to avoid ambiguity.
|
|
21
|
+
*/
|
|
18
22
|
toUrl(url: string): Promise<void>;
|
|
19
23
|
/**
|
|
20
24
|
* Reloads the current page.
|
|
@@ -15,12 +15,24 @@ class Navigation {
|
|
|
15
15
|
this.page = page;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
* Navigates the active browser page to the specified URL.
|
|
19
|
+
* Automatically waits for the page to reach the default 'load' state.
|
|
20
|
+
* @param url - The absolute or relative URL to navigate to.
|
|
21
|
+
* Relative URLs (e.g. '/path') are resolved against `baseURL` from playwright.config.ts.
|
|
22
|
+
* Protocol-relative URLs (e.g. '//example.com') are passed directly to the browser.
|
|
23
|
+
* ⚠️ If a relative URL is passed and no baseURL is configured, an error will be thrown.
|
|
24
|
+
* Prefer fully qualified URLs to avoid ambiguity.
|
|
25
|
+
*/
|
|
22
26
|
async toUrl(url) {
|
|
23
|
-
|
|
27
|
+
let resolved = url;
|
|
28
|
+
if (!url.startsWith('http')) {
|
|
29
|
+
const baseURL = this.page.context()._options?.baseURL;
|
|
30
|
+
if (!baseURL) {
|
|
31
|
+
throw new Error(`Cannot resolve relative URL "${url}" — no baseURL is configured in playwright.config.ts.`);
|
|
32
|
+
}
|
|
33
|
+
resolved = new URL(url, baseURL).href;
|
|
34
|
+
}
|
|
35
|
+
await this.page.goto(resolved);
|
|
24
36
|
}
|
|
25
37
|
/**
|
|
26
38
|
* Reloads the current page.
|
|
@@ -36,7 +36,7 @@ class Verifications {
|
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
if (expectedText === undefined) {
|
|
39
|
-
throw new Error(`
|
|
39
|
+
throw new Error(`You must provide either an 'expectedText' string or set '{ notEmpty: true }' in options.`);
|
|
40
40
|
}
|
|
41
41
|
await (0, test_1.expect)(locator).toHaveText(expectedText, { timeout: this.ELEMENT_TIMEOUT });
|
|
42
42
|
}
|
|
@@ -109,7 +109,7 @@ class Verifications {
|
|
|
109
109
|
async images(imagesLocator, scroll = true) {
|
|
110
110
|
const productImages = await imagesLocator.all();
|
|
111
111
|
if (productImages.length === 0) {
|
|
112
|
-
throw new Error(`
|
|
112
|
+
throw new Error(`No images found for '${imagesLocator}'.`);
|
|
113
113
|
}
|
|
114
114
|
for (let i = 0; i < productImages.length; i++) {
|
|
115
115
|
const productImage = productImages[i];
|
|
@@ -140,20 +140,20 @@ class Verifications {
|
|
|
140
140
|
*/
|
|
141
141
|
async count(locator, options) {
|
|
142
142
|
if (options.exactly !== undefined && options.exactly < 0) {
|
|
143
|
-
throw new Error(`
|
|
143
|
+
throw new Error(`'exact' count cannot be negative.`);
|
|
144
144
|
}
|
|
145
145
|
if (options.greaterThan !== undefined && options.greaterThan < 0) {
|
|
146
|
-
throw new Error(`
|
|
146
|
+
throw new Error(`'greaterThan' count cannot be negative.`);
|
|
147
147
|
}
|
|
148
148
|
if (options.lessThan !== undefined && options.lessThan <= 0) {
|
|
149
|
-
throw new Error(`
|
|
149
|
+
throw new Error(`'lessThan' must be greater than 0. Element counts cannot be negative.`);
|
|
150
150
|
}
|
|
151
151
|
if (options.exactly !== undefined) {
|
|
152
152
|
await (0, test_1.expect)(locator).toHaveCount(options.exactly, { timeout: this.ELEMENT_TIMEOUT });
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
155
|
if (options.greaterThan === undefined && options.lessThan === undefined) {
|
|
156
|
-
throw new Error(`
|
|
156
|
+
throw new Error(`You must provide 'exact', 'greaterThan', or 'lessThan' in CountVerifyOptions.`);
|
|
157
157
|
}
|
|
158
158
|
await locator.first().waitFor({ state: 'attached', timeout: this.ELEMENT_TIMEOUT }).catch(() => { });
|
|
159
159
|
const actualCount = await locator.count();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Debug from 'debug';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a namespaced debug logger with a padded label for aligned output.
|
|
4
|
+
*
|
|
5
|
+
* Namespaces shorter than `NAMESPACE_PAD` are right-padded with spaces so
|
|
6
|
+
* that log messages from different loggers line up in the terminal:
|
|
7
|
+
*
|
|
8
|
+
* tester:navigate Navigating to URL: "/"
|
|
9
|
+
* tester:interact Clicking on "submitButton" in "FormsPage"
|
|
10
|
+
* tester:verify Verifying presence of "table" in "FormsPage"
|
|
11
|
+
* tester:wait Waiting for "modal" in "FormsPage" to be "visible"
|
|
12
|
+
*
|
|
13
|
+
* @param namespace - A colon-delimited scope appended to the library prefix.
|
|
14
|
+
* Examples: 'navigate', 'interact', 'verify', 'wait'
|
|
15
|
+
* @returns A `debug` instance bound to `tester:<namespace>` (padded).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { createLogger } from '../logger/Logger';
|
|
20
|
+
*
|
|
21
|
+
* const log = createLogger('interact');
|
|
22
|
+
* log('Clicking on "%s" in "%s"', elementName, pageName);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function createLogger(namespace: string): Debug.Debugger;
|