pw-element-interactions 0.0.8 → 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 +154 -79
- package/dist/fixture/BaseFixture.js +10 -0
- package/dist/interactions/Interaction.js +5 -4
- 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 +12 -4
- package/scripts/postinstall.js +25 -0
- package/skills/pw-element-interactions.md +235 -0
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`
|
|
68
92
|
|
|
69
|
-
|
|
93
|
+
---
|
|
94
|
+
|
|
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.
|
|
127
142
|
|
|
128
|
-
|
|
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
|
+
```
|
|
160
|
+
|
|
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,63 +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
|
|
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.
|
|
239
267
|
|
|
240
268
|
### 📊 Data Extraction
|
|
241
269
|
|
|
242
|
-
* **`getText(pageName
|
|
243
|
-
* **`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.
|
|
244
272
|
|
|
245
273
|
### ✅ Verification
|
|
246
274
|
|
|
247
|
-
* **`verifyPresence(pageName
|
|
248
|
-
* **`verifyAbsence(pageName
|
|
249
|
-
* **`verifyText(pageName
|
|
250
|
-
* **`verifyCount(pageName
|
|
251
|
-
* **`verifyImages(pageName
|
|
252
|
-
* **`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.
|
|
253
281
|
|
|
254
282
|
### ⏳ Wait
|
|
255
283
|
|
|
256
|
-
* **`waitForState(pageName
|
|
284
|
+
* **`waitForState(pageName, elementName, state?: 'visible' | 'attached' | 'hidden' | 'detached')`** — Waits for an element to reach a specific DOM state. Defaults to `'visible'`.
|
|
257
285
|
|
|
258
286
|
---
|
|
259
287
|
|
|
260
|
-
## 🧱 Advanced
|
|
288
|
+
## 🧱 Advanced: Raw Interactions API
|
|
261
289
|
|
|
262
|
-
|
|
290
|
+
To bypass the repository or work with dynamically generated locators, use `ElementInteractions` directly:
|
|
263
291
|
|
|
264
292
|
```ts
|
|
265
293
|
import { ElementInteractions } from 'pw-element-interactions';
|
|
266
294
|
|
|
267
|
-
// Initialize
|
|
268
295
|
const interactions = new ElementInteractions(page);
|
|
269
296
|
|
|
270
|
-
// Pass Playwright Locators directly
|
|
271
297
|
const customLocator = page.locator('button.dynamic-class');
|
|
272
298
|
await interactions.interact.clickWithoutScrolling(customLocator);
|
|
273
299
|
await interactions.verify.count(customLocator, { greaterThan: 2 });
|
|
274
300
|
```
|
|
275
301
|
|
|
276
|
-
|
|
302
|
+
All core `interact`, `verify`, and `navigate` methods are available on `ElementInteractions`.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## 🤝 Contributing
|
|
307
|
+
|
|
308
|
+
Contributions are welcome! Please read the guidelines below before opening a PR.
|
|
309
|
+
|
|
310
|
+
### 🧪 Testing locally
|
|
311
|
+
|
|
312
|
+
Verify your changes end-to-end in a real consumer project using [`yalc`](https://github.com/wclr/yalc):
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# Install yalc globally (one-time)
|
|
316
|
+
npm i -g yalc
|
|
317
|
+
|
|
318
|
+
# In the pw-element-interactions folder
|
|
319
|
+
yalc publish
|
|
320
|
+
|
|
321
|
+
# In your consumer project
|
|
322
|
+
yalc add pw-element-interactions
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Push updates without re-adding:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
yalc publish --push
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Restore the original npm version when done:
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
yalc remove pw-element-interactions
|
|
335
|
+
npm install
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### 📋 PR guidelines
|
|
339
|
+
|
|
340
|
+
**Architecture.** Every new capability must follow this order:
|
|
341
|
+
|
|
342
|
+
1. Implement the core method in the appropriate domain class (`interact`, `verify`, `navigate`, etc.).
|
|
343
|
+
2. Expose it via a `Steps` wrapper.
|
|
344
|
+
|
|
345
|
+
PRs that skip step 1 will not be merged.
|
|
346
|
+
|
|
347
|
+
**Logging.** Core interaction methods must not contain any logs. `Steps` wrappers are responsible for logging what action is being performed.
|
|
348
|
+
|
|
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.
|
|
350
|
+
|
|
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;
|
|
@@ -206,7 +208,6 @@ class Interactions {
|
|
|
206
208
|
*/
|
|
207
209
|
async typeSequentially(locator, text, delay = 100) {
|
|
208
210
|
await this.utils.waitForState(locator, 'visible');
|
|
209
|
-
console.log(`[Action] -> Typing "${text}" sequentially with a ${delay}ms delay.`);
|
|
210
211
|
await locator.pressSequentially(text, {
|
|
211
212
|
delay,
|
|
212
213
|
timeout: this.ELEMENT_TIMEOUT
|
|
@@ -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;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createLogger = createLogger;
|
|
7
|
+
const debug_1 = __importDefault(require("debug"));
|
|
8
|
+
/**
|
|
9
|
+
* The library-wide namespace prefix.
|
|
10
|
+
* All loggers created by this factory are scoped under this prefix.
|
|
11
|
+
*
|
|
12
|
+
* Logging is ON by default for all tester:* namespaces.
|
|
13
|
+
* To narrow output, set the DEBUG environment variable:
|
|
14
|
+
*
|
|
15
|
+
* DEBUG=tester:steps:* → Steps output only
|
|
16
|
+
* DEBUG=tester:steps:interact → interaction steps only
|
|
17
|
+
* DEBUG=tester:interactions → raw Interaction class
|
|
18
|
+
* DEBUG=tester:steps:*,tester:interactions → combine multiple scopes
|
|
19
|
+
*
|
|
20
|
+
* To disable logging entirely:
|
|
21
|
+
*
|
|
22
|
+
* TESTER_DEBUG=false npx playwright test → suppress all tester:* logs
|
|
23
|
+
*
|
|
24
|
+
* Or in playwright.config.ts / your test setup file:
|
|
25
|
+
*
|
|
26
|
+
* process.env.TESTER_DEBUG = 'false';
|
|
27
|
+
*/
|
|
28
|
+
const PREFIX = 'tester';
|
|
29
|
+
const logsDisabled = process.env.TESTER_DEBUG === 'false';
|
|
30
|
+
if (!logsDisabled && !process.env.DEBUG) {
|
|
31
|
+
debug_1.default.enable(`${PREFIX}:*`);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The column width used to pad namespace labels in log output.
|
|
35
|
+
* All namespaces are right-padded to this length so log columns align.
|
|
36
|
+
*
|
|
37
|
+
* If you add a namespace longer than this value, increase it to match.
|
|
38
|
+
*/
|
|
39
|
+
const NAMESPACE_PAD = 9; // 'interactions' is the longest built-in namespace
|
|
40
|
+
/**
|
|
41
|
+
* Creates a namespaced debug logger with a padded label for aligned output.
|
|
42
|
+
*
|
|
43
|
+
* Namespaces shorter than `NAMESPACE_PAD` are right-padded with spaces so
|
|
44
|
+
* that log messages from different loggers line up in the terminal:
|
|
45
|
+
*
|
|
46
|
+
* tester:navigate Navigating to URL: "/"
|
|
47
|
+
* tester:interact Clicking on "submitButton" in "FormsPage"
|
|
48
|
+
* tester:verify Verifying presence of "table" in "FormsPage"
|
|
49
|
+
* tester:wait Waiting for "modal" in "FormsPage" to be "visible"
|
|
50
|
+
*
|
|
51
|
+
* @param namespace - A colon-delimited scope appended to the library prefix.
|
|
52
|
+
* Examples: 'navigate', 'interact', 'verify', 'wait'
|
|
53
|
+
* @returns A `debug` instance bound to `tester:<namespace>` (padded).
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { createLogger } from '../logger/Logger';
|
|
58
|
+
*
|
|
59
|
+
* const log = createLogger('interact');
|
|
60
|
+
* log('Clicking on "%s" in "%s"', elementName, pageName);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
function createLogger(namespace) {
|
|
64
|
+
const padded = namespace.padEnd(NAMESPACE_PAD);
|
|
65
|
+
return (0, debug_1.default)(`${PREFIX}:${padded}`);
|
|
66
|
+
}
|