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 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 Facade. This eliminates repetitive locator fetching and transforms your tests into clean, plain-English steps.
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
- // 1. Hardcode or manage raw locators inside your test
22
+ // Hardcode or manage raw locators inside your test
23
23
  const submitBtn = page.locator('button[data-test="submit-order"]');
24
24
 
25
- // 2. Explicitly wait for DOM stability and visibility
25
+ // Explicitly wait for DOM stability and visibility
26
26
  await submitBtn.waitFor({ state: 'visible', timeout: 30000 });
27
27
 
28
- // 3. Perform the interaction
28
+ // Perform the interaction
29
29
  await submitBtn.click();
30
30
  ```
31
31
 
32
- **Now (with pw-element-interactions):**
32
+ **After (pw-element-interactions):**
33
33
 
34
34
  ```ts
35
- // 1. Locate, wait, and interact in a single, readable, AI-friendly line
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 Dependencies:**
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
- ## 🚀 What is it good for?
53
+ ## Features
55
54
 
56
- * **Zero Locator Boilerplate:** The new `Steps` API fetches elements and interacts with them in a single method call.
57
- * **Separation of Concerns:** Keep your interaction logic entirely detached from how elements are found on the page.
58
- * **Readable Tests:** Abstract away Playwright boilerplate into semantic methods (`clickIfPresent`, `verifyPresence`, `selectDropdown`).
59
- * **Standardized Waiting:** Easily wait for elements to reach specific DOM states (visible, hidden, attached, detached) with built-in utility methods.
60
- * **Advanced Visual Checks:** Includes a highly reliable `verifyImages` method that evaluates actual browser decoding and `naturalWidth` to ensure images aren't just in the DOM, but are properly rendered.
61
- * **Smart Dropdowns:** Easily select dropdown options by value, index, or completely randomly (skipping disabled or empty options automatically).
62
- * **Flexible Verifications:** Easily verify exact text, non-empty text, or dynamic element counts (greater than, less than, or exact).
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
- ## 💻 Usage: The `Steps` API (Recommended)
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
- Initialize the `Steps` class by passing the current Playwright `page` object and your `ElementRepository` instance.
93
+ ---
94
+
95
+ ## 💻 Usage: The `Steps` API (Recommended)
70
96
 
71
- ### Example Scenario
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
- // 5. Smart Dropdown Interaction
94
- const selectedSize = await steps.selectDropdown('ProductDetailsPage', 'size-selector', {
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` inside every test quickly becomes repetitive. `pw-element-interactions` ships a `baseFixture` factory that injects all core dependencies automatically via Playwright's fixture system.
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
- ### What's included
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
- ### 1. Create your fixture file
141
+ `baseFixture` also attaches a full-page `failure-screenshot` to the Playwright HTML report on every failed test.
127
142
 
128
- Call `baseFixture` once, passing your own `test` base and the path to your locator repository:
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
- ### 2. Use fixtures in your tests
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
- ### 3. Access `repo` directly when needed
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
- ### 4. Extend with your own fixtures
208
+ ### 5. Extend with your own fixtures
180
209
 
181
- Because `baseFixture` returns a standard Playwright `test` object, you can chain your own fixtures on top of it cleanly:
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
- The `Steps` class automatically handles fetching the Playwright `Locator` using your `pageName` and `elementName` keys from the repository.
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)`**: Navigates the browser to the specified absolute or relative URL.
222
- * **`refresh()`**: Reloads the current page.
223
- * **`backOrForward(direction: 'BACKWARDS' | 'FORWARDS')`**: Navigates the browser history stack either backwards or forwards. Mirrors the behavior of the browser's native Back and Forward buttons.
224
- * **`setViewport(width: number, height: number)`**: Resizes the browser viewport to the specified pixel dimensions. Useful for simulating different device screen sizes or responsive breakpoints.
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: string, elementName: string)`**: Retrieves an element from the repository and performs a standard Playwright click. Automatically waits for the element to be attached, visible, stable, and actionable.
229
- * **`clickWithoutScrolling(pageName: string, elementName: string)`**: Dispatches a native `click` event directly to the element, bypassing Playwright's default scrolling and intersection observer checks. Highly useful for clicking elements obscured by sticky headers or transparent overlays.
230
- * **`clickIfPresent(pageName: string, elementName: string)`**: Checks if an element is visible before attempting to click it. Safely skips the action without failing the test if the element is hidden. Great for optional elements like cookie banners.
231
- * **`clickRandom(pageName: string, elementName: string)`**: Retrieves a random element from a resolved list of locators and clicks it. Useful for clicking random items in a list or grid.
232
- * **`hover(pageName: string, elementName: string)`**: Retrieves an element and hovers over it. Useful for triggering dropdowns or tooltips.
233
- * **`scrollIntoView(pageName: string, elementName: string)`**: Retrieves an element and smoothly scrolls it into the viewport if it is not already visible.
234
- * **`dragAndDrop(pageName: string, elementName: string, options: DragAndDropOptions)`**: Drags an element to a specified destination. Supports dropping onto another element (`{ target: Locator }`), dragging by coordinates (`{ xOffset: number, yOffset: number }`), or dropping onto a target at a specific offset.
235
- * **`dragAndDropListedElement(pageName: string, elementName: string, elementText: string, options: DragAndDropOptions)`**: Finds a specific element by its text from a list of elements and drags it to a specified destination based on the provided options.
236
- * **`fill(pageName: string, elementName: string, text: string)`**: Clears any existing value in the target input field and types the provided text.
237
- * **`uploadFile(pageName: string, elementName: string, filePath: string)`**: Uploads a local file from the provided `filePath` to an `<input type="file">` element.
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 }`).
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: string, elementName: string)`**: Safely retrieves and trims the text content of a specified element. Returns an empty string if null.
243
- * **`getAttribute(pageName: string, elementName: string, attributeName: string)`**: Retrieves the value of a specified HTML attribute (e.g., `href`, `aria-pressed`) from an element. Returns `null` if the attribute doesn't exist.
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: string, elementName: string)`**: Asserts that a specified element is attached to the DOM and is visible.
248
- * **`verifyAbsence(pageName: string, elementName: string)`**: Asserts that a specified element is hidden or completely detached from the DOM.
249
- * **`verifyText(pageName: string, elementName: string, expectedText?: string, options?: TextVerifyOptions)`**: Asserts the text of an element. Provide `expectedText` for an exact match, or pass `{ notEmpty: true }` in the options to simply assert that the dynamically generated text is not blank.
250
- * **`verifyCount(pageName: string, elementName: string, options: CountVerifyOptions)`**: Asserts the number of elements matching the locator. Accepts a configuration object to evaluate: `{ exact: number }`, `{ greaterThan: number }`, or `{ lessThan: number }`.
251
- * **`verifyImages(pageName: string, elementName: string, scroll?: boolean)`**: Performs a rigorous verification of one or more images. Asserts visibility, checks for a valid `src` attribute, ensures `naturalWidth > 0`, and evaluates the native browser `decode()` promise. Smoothly scrolls into view by default (`scroll: true`).
252
- * **`verifyUrlContains(text: string)`**: Asserts that the active browser URL contains the expected substring.
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: string, elementName: string, state?: 'visible' | 'attached' | 'hidden' | 'detached')`**: Waits for an element to reach a specific state in the DOM. Defaults to `'visible'`.
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 Usage: Raw Interactions API
288
+ ## 🧱 Advanced: Raw Interactions API
261
289
 
262
- If you need to bypass the repository or interact with custom locators dynamically generated in your tests, you can use the underlying `ElementInteractions` class directly.
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
- *Note: All core interaction (`interact`), verification (`verify`), and navigation (`navigate`) methods are also available when using `ElementInteractions` directly.*
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
- console.log(`[Action] -> Locator was not visible. Skipping click.`);
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
- console.log(`[Action] -> Uploading file from path "${filePath}"`);
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
- console.warn(msg);
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
- * 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
- */
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
- * 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
- */
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
- await this.page.goto(url);
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(`[Verify] -> You must provide either an 'expectedText' string or set '{ notEmpty: true }' in options.`);
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(`[Verify] -> No images found for '${imagesLocator}'.`);
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(`[Verify] -> 'exact' count cannot be negative.`);
143
+ throw new Error(`'exact' count cannot be negative.`);
144
144
  }
145
145
  if (options.greaterThan !== undefined && options.greaterThan < 0) {
146
- throw new Error(`[Verify] -> 'greaterThan' count cannot be negative.`);
146
+ throw new Error(`'greaterThan' count cannot be negative.`);
147
147
  }
148
148
  if (options.lessThan !== undefined && options.lessThan <= 0) {
149
- throw new Error(`[Verify] -> 'lessThan' must be greater than 0. Element counts cannot be negative.`);
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(`[Verify] -> You must provide 'exact', 'greaterThan', or 'lessThan' in CountVerifyOptions.`);
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
+ }