@civitas-cerebrum/element-interactions 0.1.1

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 ADDED
@@ -0,0 +1,633 @@
1
+ # Playwright Element Interactions
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/@civitas-cerebrum/element-interactions?color=rgb(88%2C%20171%2C%2070))](https://www.npmjs.com/package/@civitas-cerebrum/element-interactions)
4
+
5
+ A robust set of Playwright steps for readable interaction and assertions.
6
+
7
+ `@civitas-cerebrum/element-interactions` pairs perfectly with `@civitas-cerebrum/element-repository` to achieve a fully decoupled test automation architecture. By separating **Element Acquisition** from **Element Interaction**, your test scripts become highly readable, easily maintainable, and completely free of raw locators.
8
+
9
+ ### โœจ The Unified Steps API
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.
12
+
13
+ ### ๐Ÿง  AI-Friendly Test Development & Boilerplate Reduction
14
+
15
+ Stop writing the same three lines of code for every single interaction. This library handles the fetching, waiting, and acting automatically.
16
+
17
+ Because the API is highly semantic and completely decoupled from the DOM, it is an **ideal framework for AI coding assistants**. AI models can easily generate robust test flows using plain-English strings (`'CheckoutPage'`, `'submitButton'`) without hallucinating complex CSS selectors, writing flaky interactions, or forgetting critical `waitFor` states.
18
+
19
+ **Before (Raw Playwright):**
20
+
21
+ ```ts
22
+ // Hardcode or manage raw locators inside your test
23
+ const submitBtn = page.locator('button[data-test="submit-order"]');
24
+
25
+ // Explicitly wait for DOM stability and visibility
26
+ await submitBtn.waitFor({ state: 'visible', timeout: 30000 });
27
+
28
+ // Perform the interaction
29
+ await submitBtn.click();
30
+ ```
31
+
32
+ **After (@civitas-cerebrum/element-interactions):**
33
+
34
+ ```ts
35
+ // Locate, wait, and interact โ€” one line
36
+ await steps.click('CheckoutPage', 'submitButton');
37
+ ```
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
+
41
+ ---
42
+
43
+ ## ๐Ÿ“ฆ Installation
44
+
45
+ ```bash
46
+ npm i @civitas-cerebrum/element-interactions
47
+ ```
48
+
49
+ **Peer dependencies:** `@playwright/test` is required. The `Steps` API additionally requires `@civitas-cerebrum/element-repository`.
50
+
51
+ ---
52
+
53
+ ## ๐Ÿค– Claude Code Integration
54
+
55
+ `@civitas-cerebrum/element-interactions` ships with a built-in **Claude Code skill** โ€” an agent instruction file that teaches Claude how to use the framework correctly. The skill is included in the npm package, so there's nothing extra to install. When Claude Code detects it in your `node_modules`, it can write, debug, and maintain your Playwright tests using the Steps API, inspect live pages via the Playwright MCP, and manage your page repository โ€” all with guardrails that prevent common AI mistakes like inventing selectors or overwriting config files.
56
+
57
+ ### Quick Start with Claude Code
58
+
59
+ **1. Initialize a new Playwright project** (skip if you already have one):
60
+
61
+ ```bash
62
+ npm init playwright@latest playwright-project
63
+ cd playwright-project
64
+ ```
65
+
66
+ **2. Install the packages:**
67
+
68
+ ```bash
69
+ npm i @civitas-cerebrum/element-interactions @civitas-cerebrum/element-repository
70
+ ```
71
+
72
+ That's it โ€” the skill is now available to Claude Code. When you ask it to write tests, it will automatically scaffold the fixture file, page repository, and config if they don't exist yet.
73
+
74
+ **3. Ask Claude to discover your app and automate a scenario:**
75
+
76
+ Open Claude Code in your project directory and prompt it with something like:
77
+
78
+ > *"Inspect the repo & explore https://your-app-url.com and automate an example end-to-end scenario."*
79
+
80
+ Claude will navigate to your site, identify key pages and interactions, scaffold the fixture file and page repository, and write a working test โ€” all in one shot.
81
+
82
+ > **Tip:** For the best results, connect the Playwright MCP first so Claude can inspect the live DOM and verify selectors against the real page before writing any locators. Run `/plugins` in Claude Code and enable Playwright from the list.
83
+
84
+ ### What the Skill Enables
85
+
86
+ Once loaded, Claude Code will:
87
+
88
+ - **Scaffold your project** โ€” creating the fixture file, page repository, and Playwright config on first use.
89
+ - **Write tests using the Steps API** โ€” generating clean, readable test flows with `steps.click()`, `steps.verifyText()`, etc.
90
+ - **Inspect the live DOM before adding locators** โ€” using the Playwright MCP to verify selectors instead of guessing.
91
+ - **Ask before modifying `page-repository.json`** โ€” showing you the exact changes it wants to make.
92
+ - **Inspect failure screenshots** โ€” using the HTML report to diagnose test failures visually before proposing fixes.
93
+ - **Follow project conventions** โ€” PascalCase page names, camelCase element names, no raw selectors in test code.
94
+
95
+ > **Tip:** For the best experience, ensure `reporter: 'html'` is set in your `playwright.config.ts` so failure screenshots are captured and viewable in the report.
96
+
97
+ ---
98
+
99
+ ## โœจ Features
100
+
101
+ * **Zero locator boilerplate** โ€” The `Steps` API fetches elements and interacts with them in a single call.
102
+ * **Automatic failure screenshots** โ€” `baseFixture` captures a full-page screenshot on every failed test and attaches it to the HTML report.
103
+ * **Standardized waiting** โ€” Built-in methods wait for elements to reach specific DOM states (visible, hidden, attached, detached).
104
+ * **Advanced image verification** โ€” `verifyImages` evaluates actual browser decoding and `naturalWidth`, not just DOM presence.
105
+ * **Smart dropdowns** โ€” Select by value, index, or randomly, with automatic skipping of disabled and empty options.
106
+ * **Flexible assertions** โ€” Verify exact text, non-empty text, URL substrings, or dynamic element counts (greater than, less than, exact).
107
+ * **Smart interactions** โ€” Drag to other elements, type sequentially, wait for specific element state, verify images and more!
108
+
109
+ ---
110
+
111
+ ## ๐Ÿ—‚๏ธ Defining Locators
112
+
113
+ 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.
114
+
115
+ ```json
116
+ {
117
+ "pages": [
118
+ {
119
+ "name": "HomePage",
120
+ "elements": [
121
+ {
122
+ "elementName": "submitButton",
123
+ "selector": {
124
+ "css": "button[data-test='submit']"
125
+ }
126
+ }
127
+ ]
128
+ }
129
+ ]
130
+ }
131
+ ```
132
+
133
+ Each selector object supports `css`, `xpath`, `id`, or `text` as the locator strategy.
134
+
135
+ **Naming conventions:**
136
+ - `name` โ€” PascalCase page identifier, e.g. `CheckoutPage`, `ProductDetailsPage`
137
+ - `elementName` โ€” camelCase element identifier, e.g. `submitButton`, `galleryImages`
138
+
139
+ ---
140
+
141
+ ## ๐Ÿ’ป Usage: The `Steps` API (Recommended)
142
+
143
+ Initialize `Steps` by passing the current Playwright `page` and your `ElementRepository` instance.
144
+
145
+ ```ts
146
+ import { test } from '@playwright/test';
147
+ import { ElementRepository } from '@civitas-cerebrum/element-repository';
148
+ import { Steps, DropdownSelectType } from '@civitas-cerebrum/element-interactions';
149
+
150
+ test('Add random product and verify image gallery', async ({ page }) => {
151
+ const repo = new ElementRepository('tests/data/locators.json');
152
+ const steps = new Steps(page, repo);
153
+
154
+ await steps.navigateTo('/');
155
+ await steps.click('HomePage', 'category-accessories');
156
+
157
+ await steps.clickRandom('AccessoriesPage', 'product-cards');
158
+ await steps.verifyUrlContains('/product/');
159
+
160
+ const selectedSize = await steps.selectDropdown('ProductDetailsPage', 'size-selector', {
161
+ type: DropdownSelectType.RANDOM,
162
+ });
163
+ console.log(`Selected size: ${selectedSize}`);
164
+
165
+ await steps.verifyCount('ProductDetailsPage', 'gallery-images', { greaterThan: 0 });
166
+ await steps.verifyText('ProductDetailsPage', 'product-title', undefined, { notEmpty: true });
167
+ await steps.verifyImages('ProductDetailsPage', 'gallery-images');
168
+ await steps.waitForState('CheckoutPage', 'confirmation-modal', 'visible');
169
+ });
170
+ ```
171
+
172
+ ---
173
+
174
+ ## ๐Ÿ”ง Fixtures: Zero-Setup Tests (Recommended)
175
+
176
+ For larger projects, manually initializing `repo` and `steps` in every test becomes repetitive. `baseFixture` injects all core dependencies automatically via Playwright's fixture system.
177
+
178
+ ### Included fixtures
179
+
180
+ | Fixture | Type | Description |
181
+ |---|---|---|
182
+ | `steps` | `Steps` | The full Steps API, ready to use |
183
+ | `repo` | `ElementRepository` | Direct repository access for advanced locator queries |
184
+ | `interactions` | `ElementInteractions` | Raw interactions API for custom locators |
185
+ | `contextStore` | `ContextStore` | Shared in-memory store for passing data between steps |
186
+
187
+ `baseFixture` also attaches a full-page `failure-screenshot` to the Playwright HTML report on every failed test.
188
+
189
+ > **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.
190
+
191
+ ### 1. Playwright Config
192
+
193
+ ```ts
194
+ // playwright.config.ts
195
+ import { defineConfig } from '@playwright/test';
196
+
197
+ export default defineConfig({
198
+ testDir: './tests',
199
+ reporter: 'html',
200
+ use: {
201
+ baseURL: 'https://your-project-url.com',
202
+ headless: true,
203
+ },
204
+ });
205
+ ```
206
+
207
+ ### 2. Create your fixture file
208
+
209
+ ```ts
210
+ // tests/fixtures/base.ts
211
+ import { test as base, expect } from '@playwright/test';
212
+ import { baseFixture } from '@civitas-cerebrum/element-interactions';
213
+
214
+ export const test = baseFixture(base, 'tests/data/page-repository.json');
215
+ export { expect };
216
+ ```
217
+
218
+ ### 3. Use fixtures in your tests
219
+
220
+ ```ts
221
+ // tests/checkout.spec.ts
222
+ import { test, expect } from '../fixtures/base';
223
+ import { DropdownSelectType } from '@civitas-cerebrum/element-interactions';
224
+
225
+ test('Complete checkout flow', async ({ steps }) => {
226
+ await steps.navigateTo('/');
227
+ await steps.click('HomePage', 'category-accessories');
228
+ await steps.clickRandom('AccessoriesPage', 'product-cards');
229
+ await steps.verifyUrlContains('/product/');
230
+
231
+ const selectedSize = await steps.selectDropdown('ProductDetailsPage', 'size-selector', {
232
+ type: DropdownSelectType.RANDOM,
233
+ });
234
+
235
+ await steps.verifyImages('ProductDetailsPage', 'gallery-images');
236
+ await steps.click('ProductDetailsPage', 'add-to-cart-button');
237
+ await steps.waitForState('CheckoutPage', 'confirmation-modal', 'visible');
238
+ });
239
+ ```
240
+
241
+ ### 4. Access `repo` directly when needed
242
+
243
+ ```ts
244
+ test('Navigate to Forms category', async ({ page, repo, steps }) => {
245
+ await steps.navigateTo('/');
246
+
247
+ const formsLink = await repo.getByText(page, 'HomePage', 'categories', 'Forms');
248
+ await formsLink?.click();
249
+
250
+ await steps.verifyAbsence('HomePage', 'categories');
251
+ });
252
+ ```
253
+
254
+ **Full Repository API:**
255
+
256
+ ```ts
257
+ await repo.get(page, 'PageName', 'elementName'); // single locator
258
+ await repo.getAll(page, 'PageName', 'elementName'); // array of locators
259
+ await repo.getRandom(page, 'PageName', 'elementName'); // random from matches
260
+ await repo.getByText(page, 'PageName', 'elementName', 'Text'); // filter by visible text
261
+ await repo.getByAttribute(page, 'PageName', 'elementName', 'data-status', 'active'); // filter by attribute
262
+ await repo.getByAttribute(page, 'PageName', 'elementName', 'href', '/path', { exact: false }); // partial match
263
+ await repo.getByIndex(page, 'PageName', 'elementName', 2); // zero-based index
264
+ await repo.getByRole(page, 'PageName', 'elementName', 'button'); // explicit HTML role attribute
265
+ await repo.getVisible(page, 'PageName', 'elementName'); // first visible match
266
+ repo.getSelector('PageName', 'elementName'); // sync, returns raw selector string
267
+ repo.setDefaultTimeout(10000); // change default wait timeout
268
+ ```
269
+
270
+ ### 5. Extend with your own fixtures
271
+
272
+ Because `baseFixture` returns a standard Playwright `test` object, you can layer your own fixtures on top:
273
+
274
+ ```ts
275
+ // tests/fixtures/base.ts
276
+ import { test as base } from '@playwright/test';
277
+ import { baseFixture } from '@civitas-cerebrum/element-interactions';
278
+ import { AuthService } from '../services/AuthService';
279
+
280
+ type MyFixtures = {
281
+ authService: AuthService;
282
+ };
283
+
284
+ const testWithBase = baseFixture(base, 'tests/data/page-repository.json');
285
+
286
+ export const test = testWithBase.extend<MyFixtures>({
287
+ authService: async ({ page }, use) => {
288
+ await use(new AuthService(page));
289
+ },
290
+ });
291
+
292
+ export { expect } from '@playwright/test';
293
+ ```
294
+
295
+ ```ts
296
+ test('Authenticated flow', async ({ steps, authService }) => {
297
+ await authService.login('user@test.com', 'secret');
298
+ await steps.verifyUrlContains('/dashboard');
299
+ });
300
+ ```
301
+
302
+ ---
303
+
304
+ ## ๐Ÿ› ๏ธ API Reference: `Steps`
305
+
306
+ Every method below automatically fetches the Playwright `Locator` using your `pageName` and `elementName` keys from the repository.
307
+
308
+ ### ๐Ÿงญ Navigation
309
+
310
+ * **`navigateTo(url: string)`** โ€” Navigates the browser to the specified absolute or relative URL.
311
+ * **`refresh()`** โ€” Reloads the current page.
312
+ * **`backOrForward(direction: 'back' | 'forward')`** โ€” Navigates the browser history stack in the given direction.
313
+ * **`setViewport(width: number, height: number)`** โ€” Resizes the browser viewport to the specified pixel dimensions.
314
+ * **`switchToNewTab(action: () => Promise<void>)`** โ€” Executes an action that opens a new tab (e.g. clicking a link with `target="_blank"`), waits for the new tab, and returns the new `Page` object.
315
+ * **`closeTab(targetPage?: Page)`** โ€” Closes the specified tab (or the current one) and returns the remaining active page.
316
+ * **`getTabCount()`** โ€” Returns the number of currently open tabs/pages in the browser context.
317
+
318
+ ### ๐Ÿ–ฑ๏ธ Interaction
319
+
320
+ * **`click(pageName, elementName)`** โ€” Clicks an element. Automatically waits for the element to be attached, visible, stable, and actionable.
321
+ * **`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.
322
+ * **`clickIfPresent(pageName, elementName)`** โ€” Clicks an element only if it is visible; skips silently otherwise. Returns `boolean` (`true` if clicked). Ideal for optional elements like cookie banners.
323
+ * **`clickRandom(pageName, elementName)`** โ€” Clicks a random element from all matches. Useful for lists or grids.
324
+ * **`rightClick(pageName, elementName)`** โ€” Right-clicks an element to trigger a context menu.
325
+ * **`doubleClick(pageName, elementName)`** โ€” Double-clicks an element.
326
+ * **`check(pageName, elementName)`** โ€” Checks a checkbox or radio button. No-op if already checked.
327
+ * **`uncheck(pageName, elementName)`** โ€” Unchecks a checkbox. No-op if already unchecked.
328
+ * **`hover(pageName, elementName)`** โ€” Hovers over an element to trigger dropdowns or tooltips.
329
+ * **`scrollIntoView(pageName, elementName)`** โ€” Smoothly scrolls an element into the viewport.
330
+ * **`dragAndDrop(pageName, elementName, options: DragAndDropOptions)`** โ€” Drags an element to a target element (`{ target: Locator }`), by coordinate offset (`{ xOffset, yOffset }`), or both.
331
+ * **`dragAndDropListedElement(pageName, elementName, elementText, options: DragAndDropOptions)`** โ€” Finds a specific element by its text from a list, then drags it to a destination.
332
+ * **`fill(pageName, elementName, text: string)`** โ€” Clears and fills an input field with the provided text.
333
+ * **`uploadFile(pageName, elementName, filePath: string)`** โ€” Uploads a file to an `<input type="file">` element.
334
+ * **`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).
335
+ * **`setSliderValue(pageName, elementName, value: number)`** โ€” Sets a range input (`<input type="range">`) to the specified numeric value.
336
+ * **`pressKey(key: string)`** โ€” Presses a keyboard key at the page level (e.g. `'Enter'`, `'Escape'`, `'Tab'`).
337
+ * **`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.
338
+
339
+ ### ๐Ÿ“Š Data Extraction
340
+
341
+ * **`getText(pageName, elementName)`** โ€” Returns the trimmed text content of an element, or an empty string if null.
342
+ * **`getAttribute(pageName, elementName, attributeName: string)`** โ€” Returns the value of an HTML attribute (e.g. `href`, `aria-pressed`), or `null` if it doesn't exist.
343
+
344
+ ### โœ… Verification
345
+
346
+ * **`verifyPresence(pageName, elementName)`** โ€” Asserts that an element is attached to the DOM and visible.
347
+ * **`verifyAbsence(pageName, elementName)`** โ€” Asserts that an element is hidden or detached from the DOM.
348
+ * **`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.
349
+ * **`verifyCount(pageName, elementName, options: CountVerifyOptions)`** โ€” Asserts element count. Accepts `{ exactly: number }`, `{ greaterThan: number }`, or `{ lessThan: number }`.
350
+ * **`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.
351
+ * **`verifyTextContains(pageName, elementName, expectedText: string)`** โ€” Asserts that an element's text contains the expected substring.
352
+ * **`verifyState(pageName, elementName, state)`** โ€” Asserts the state of an element. Supported states: `'enabled'`, `'disabled'`, `'editable'`, `'checked'`, `'focused'`, `'visible'`, `'hidden'`, `'attached'`, `'inViewport'`.
353
+ * **`verifyAttribute(pageName, elementName, attributeName: string, expectedValue: string)`** โ€” Asserts that an element has a specific HTML attribute with an exact value.
354
+ * **`verifyUrlContains(text: string)`** โ€” Asserts that the current URL contains the expected substring.
355
+ * **`verifyInputValue(pageName, elementName, expectedValue: string)`** โ€” Asserts that an input, textarea, or select element has the expected value.
356
+ * **`verifyTabCount(expectedCount: number)`** โ€” Asserts the number of currently open tabs/pages in the browser context.
357
+
358
+ ### ๐Ÿ“‹ Listed Elements
359
+
360
+ Operate on a specific element within a list (table rows, cards, list items) by matching its visible text or an HTML attribute. Optionally drill into a child element within the matched item.
361
+
362
+ ```ts
363
+ import { ListedElementMatch, VerifyListedOptions, GetListedDataOptions } from '@civitas-cerebrum/element-interactions';
364
+ ```
365
+
366
+ * **`clickListedElement(pageName, elementName, options: ListedElementMatch)`** โ€” Finds and clicks a specific element from a list. Identify the target by `{ text }` or `{ attribute: { name, value } }`, and optionally drill into a child with `{ child: 'css-selector' }` or `{ child: { pageName, elementName } }`.
367
+ * **`verifyListedElement(pageName, elementName, options: VerifyListedOptions)`** โ€” Finds a listed element and asserts against it. Use `{ expectedText }` to verify text, `{ expected: { name, value } }` to verify an attribute, or omit both to assert visibility.
368
+ * **`getListedElementData(pageName, elementName, options: GetListedDataOptions)`** โ€” Extracts data from a listed element. Returns the element's text content by default, or an attribute value when `{ extractAttribute: 'attrName' }` is specified.
369
+
370
+ ```ts
371
+ // Click the row containing "John"
372
+ await steps.clickListedElement('UsersPage', 'tableRows', { text: 'John' });
373
+
374
+ // Click a child button inside the row matching an attribute
375
+ await steps.clickListedElement('UsersPage', 'tableRows', {
376
+ attribute: { name: 'data-id', value: '5' },
377
+ child: 'button.edit'
378
+ });
379
+
380
+ // Verify text of a child cell in the row containing "Name"
381
+ await steps.verifyListedElement('FormsPage', 'submissionEntries', {
382
+ text: 'Name',
383
+ child: 'td:nth-child(2)',
384
+ expectedText: 'John Doe'
385
+ });
386
+
387
+ // Verify an attribute on a listed element
388
+ await steps.verifyListedElement('UsersPage', 'tableRows', {
389
+ attribute: { name: 'data-id', value: '5' },
390
+ expected: { name: 'class', value: 'active' }
391
+ });
392
+
393
+ // Extract an href from a child link inside a listed element
394
+ const href = await steps.getListedElementData('UsersPage', 'tableRows', {
395
+ text: 'John',
396
+ child: 'a.profile-link',
397
+ extractAttribute: 'href'
398
+ });
399
+ ```
400
+
401
+ ### โณ Wait
402
+
403
+ * **`waitForState(pageName, elementName, state?: 'visible' | 'attached' | 'hidden' | 'detached')`** โ€” Waits for an element to reach a specific DOM state. Defaults to `'visible'`.
404
+ * **`waitForNetworkIdle()`** โ€” Waits until there are no in-flight network requests for at least 500ms.
405
+ * **`waitForResponse(urlPattern: string | RegExp, action: () => Promise<void>)`** โ€” Executes an action and waits for a matching network response. Returns the `Response` object.
406
+ * **`waitAndClick(pageName, elementName, state?: string)`** โ€” Waits for an element to reach a state (default `'visible'`), then clicks it.
407
+
408
+ ### ๐Ÿงฉ Composite / Workflow
409
+
410
+ * **`fillForm(pageName, fields: Record<string, FillFormValue>)`** โ€” Fills multiple form fields in one call. String values fill text inputs; `DropdownSelectOptions` values trigger dropdown selection.
411
+ * **`retryUntil(action, verification, maxRetries?, delayMs?)`** โ€” Retries an action until a verification passes, or until the max attempts (default `3`) are reached.
412
+ * **`clearInput(pageName, elementName)`** โ€” Clears the value of an input or textarea without filling new text.
413
+ * **`selectMultiple(pageName, elementName, values: string[])`** โ€” Selects multiple options from a `<select multiple>` element by their value attributes.
414
+ * **`clickNth(pageName, elementName, index: number)`** โ€” Clicks the element at a specific zero-based index from all matches.
415
+
416
+ ### ๐Ÿ“Š Additional Data Extraction
417
+
418
+ * **`getAll(pageName, elementName, options?: GetAllOptions)`** โ€” Extracts text (or attributes) from all matching elements. Supports `{ child }` and `{ extractAttribute }`.
419
+ * **`getCount(pageName, elementName)`** โ€” Returns the number of DOM elements matching the locator.
420
+ * **`getInputValue(pageName, elementName)`** โ€” Returns the current `value` property of an input, textarea, or select element.
421
+ * **`getCssProperty(pageName, elementName, property: string)`** โ€” Returns a computed CSS property value (e.g. `'rgb(255, 0, 0)'`).
422
+
423
+ ### โœ… Additional Verification
424
+
425
+ * **`verifyOrder(pageName, elementName, expectedTexts: string[])`** โ€” Asserts that elements' text contents appear in the exact order specified.
426
+ * **`verifyCssProperty(pageName, elementName, property: string, expectedValue: string)`** โ€” Asserts that a computed CSS property matches the expected value.
427
+ * **`verifyListOrder(pageName, elementName, direction: 'asc' | 'desc')`** โ€” Asserts that elements' text contents are sorted in the specified direction.
428
+
429
+ ### ๐Ÿ“ธ Screenshot
430
+
431
+ * **`screenshot()`** โ€” Captures a page screenshot. Pass `{ fullPage: true }` for scrollable capture, `{ path: 'file.png' }` to save to disk.
432
+ * **`screenshot(pageName, elementName, options?)`** โ€” Captures a screenshot of a specific element.
433
+
434
+ ---
435
+
436
+ ## ๐Ÿงฑ Advanced: Raw Interactions API
437
+
438
+ To bypass the repository or work with dynamically generated locators, use `ElementInteractions` directly:
439
+
440
+ ```ts
441
+ import { ElementInteractions } from '@civitas-cerebrum/element-interactions';
442
+
443
+ const interactions = new ElementInteractions(page);
444
+
445
+ const customLocator = page.locator('button.dynamic-class');
446
+ await interactions.interact.clickWithoutScrolling(customLocator);
447
+ await interactions.verify.count(customLocator, { greaterThan: 2 });
448
+ ```
449
+
450
+ All core `interact`, `verify`, and `navigate` methods are available on `ElementInteractions`.
451
+
452
+ ---
453
+
454
+ ## ๐Ÿ“ง Email API
455
+
456
+ Send and receive emails in your tests. Supports plain text, inline HTML, and HTML file templates for full customisation.
457
+
458
+ ### Setup
459
+
460
+ Pass email credentials to `baseFixture` via the options parameter:
461
+
462
+ ```ts
463
+ // tests/fixtures/base.ts
464
+ import { test as base, expect } from '@playwright/test';
465
+ import { baseFixture } from '@civitas-cerebrum/element-interactions';
466
+
467
+ export const test = baseFixture(base, 'tests/data/page-repository.json', {
468
+ emailCredentials: {
469
+ senderEmail: process.env.SENDER_EMAIL!,
470
+ senderPassword: process.env.SENDER_PASSWORD!,
471
+ senderSmtpHost: process.env.SENDER_SMTP_HOST!,
472
+ receiverEmail: process.env.RECEIVER_EMAIL!,
473
+ receiverPassword: process.env.RECEIVER_PASSWORD!,
474
+ }
475
+ });
476
+ export { expect };
477
+ ```
478
+
479
+ ### Sending Emails
480
+
481
+ ```ts
482
+ // Plain text
483
+ await steps.sendEmail({
484
+ to: 'user@example.com',
485
+ subject: 'Test Email',
486
+ text: 'Hello from Playwright!'
487
+ });
488
+
489
+ // Inline HTML
490
+ await steps.sendEmail({
491
+ to: 'user@example.com',
492
+ subject: 'HTML Email',
493
+ html: '<h1>Hello</h1><p>Inline HTML content</p>'
494
+ });
495
+
496
+ // HTML file from project directory โ€” great for branded templates
497
+ await steps.sendEmail({
498
+ to: 'user@example.com',
499
+ subject: 'Monthly Report',
500
+ htmlFile: 'emails/report-template.html'
501
+ });
502
+ ```
503
+
504
+ ### Receiving Emails
505
+
506
+ Use composable filters to search the inbox. Combine as many filters as needed โ€” all are applied with AND logic. Filtering tries exact match first, then falls back to partial case-insensitive match (with a warning log).
507
+
508
+ ```ts
509
+ import { EmailFilterType } from '@civitas-cerebrum/element-interactions';
510
+ // Note: EmailFilterType and other email types can also be imported from '@civitas-cerebrum/email-client'
511
+
512
+ // Single filter โ€” get the latest matching email
513
+ const email = await steps.receiveEmail({
514
+ filters: [{ type: EmailFilterType.SUBJECT, value: 'Your OTP Code' }]
515
+ });
516
+
517
+ // Open the downloaded HTML in the browser
518
+ await steps.navigateTo('file://' + email.filePath);
519
+
520
+ // Now interact with the email content like any web page
521
+ const otpCode = await steps.getText('EmailPage', 'otpCode');
522
+
523
+ // Combine multiple filters
524
+ const email2 = await steps.receiveEmail({
525
+ filters: [
526
+ { type: EmailFilterType.SUBJECT, value: 'Verification' },
527
+ { type: EmailFilterType.FROM, value: 'noreply@example.com' },
528
+ { type: EmailFilterType.CONTENT, value: 'verification code' },
529
+ ]
530
+ });
531
+
532
+ // Get ALL matching emails
533
+ const allEmails = await steps.receiveAllEmails({
534
+ filters: [
535
+ { type: EmailFilterType.FROM, value: 'alerts@example.com' },
536
+ { type: EmailFilterType.SINCE, value: new Date('2025-01-01') },
537
+ ]
538
+ });
539
+ ```
540
+
541
+ ### Cleaning the Inbox
542
+
543
+ Delete emails matching filters, or clean the entire inbox:
544
+
545
+ ```ts
546
+ // Delete emails from a specific sender
547
+ await steps.cleanEmails({
548
+ filters: [{ type: EmailFilterType.FROM, value: 'noreply@example.com' }]
549
+ });
550
+
551
+ // Delete all emails in the inbox
552
+ await steps.cleanEmails();
553
+ ```
554
+
555
+ **Filter types (`EmailFilterType`):**
556
+
557
+ | Type | Value Type | Description |
558
+ |---|---|---|
559
+ | `SUBJECT` | `string` | Filter by email subject |
560
+ | `FROM` | `string` | Filter by sender address |
561
+ | `TO` | `string` | Filter by recipient address |
562
+ | `CONTENT` | `string` | Filter by email body (HTML or plain text) |
563
+ | `SINCE` | `Date` | Only include emails received after this date |
564
+
565
+ **`receiveEmail()` / `receiveAllEmails()` options:**
566
+
567
+ | Option | Type | Default | Description |
568
+ |---|---|---|---|
569
+ | `filters` | `EmailFilter[]` | โ€” | **Required.** Composable filters (AND logic) |
570
+ | `folder` | `string` | `'INBOX'` | IMAP folder to search |
571
+ | `waitTimeout` | `number` | `30000` | Max time (ms) to wait for a match |
572
+ | `pollInterval` | `number` | `3000` | How often (ms) to poll the inbox |
573
+ | `downloadDir` | `string` | `os.tmpdir()/pw-emails` | Where to save the downloaded HTML |
574
+
575
+ **`ReceivedEmail` return type:**
576
+
577
+ | Property | Type | Description |
578
+ |---|---|---|
579
+ | `filePath` | `string` | Absolute path to the downloaded HTML file |
580
+ | `subject` | `string` | Email subject line |
581
+ | `from` | `string` | Sender address |
582
+ | `date` | `Date` | When the email was sent |
583
+ | `html` | `string` | Raw HTML content |
584
+ | `text` | `string` | Plain text content |
585
+
586
+ ---
587
+
588
+ ## ๐Ÿค Contributing
589
+
590
+ Contributions are welcome! Please read the guidelines below before opening a PR.
591
+
592
+ ### ๐Ÿงช Testing locally
593
+
594
+ Verify your changes end-to-end in a real consumer project using [`yalc`](https://github.com/wclr/yalc):
595
+
596
+ ```bash
597
+ # Install yalc globally (one-time)
598
+ npm i -g yalc
599
+
600
+ # In the element-interactions repo folder
601
+ yalc publish
602
+
603
+ # In your consumer project
604
+ yalc add @civitas-cerebrum/element-interactions
605
+ ```
606
+
607
+ Push updates without re-adding:
608
+
609
+ ```bash
610
+ yalc publish --push
611
+ ```
612
+
613
+ Restore the original npm version when done:
614
+
615
+ ```bash
616
+ yalc remove @civitas-cerebrum/element-interactions
617
+ npm install
618
+ ```
619
+
620
+ ### ๐Ÿ“‹ PR guidelines
621
+
622
+ **Architecture.** Every new capability must follow this order:
623
+
624
+ 1. Implement the core method in the appropriate domain class (`interact`, `verify`, `navigate`, etc.).
625
+ 2. Expose it via a `Steps` wrapper.
626
+
627
+ PRs that skip step 1 will not be merged.
628
+
629
+ **Logging.** Core interaction methods must not contain any logs. `Steps` wrappers are responsible for logging what action is being performed.
630
+
631
+ **Unit tests.** Every new method must include a unit test. Tests run against the [Vue test app](https://github.com/civitas-cerebrum/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.
632
+
633
+ **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.
@@ -0,0 +1,4 @@
1
+ import { EmailCredentials } from '@civitas-cerebrum/email-client';
2
+ export declare function validateEmailEnv(): void;
3
+ export declare function loadEmailConfig(): EmailCredentials;
4
+ export declare function isEmailConfigured(): boolean;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateEmailEnv = validateEmailEnv;
4
+ exports.loadEmailConfig = loadEmailConfig;
5
+ exports.isEmailConfigured = isEmailConfigured;
6
+ const dotenv_1 = require("dotenv");
7
+ // Load .env variables. If process.env variables already exist (like in CI),
8
+ // dotenv will safely ignore them and keep the CI values.
9
+ (0, dotenv_1.config)();
10
+ function validateEmailEnv() {
11
+ const required = [
12
+ 'SENDER_EMAIL',
13
+ 'SENDER_PASSWORD',
14
+ 'SENDER_SMTP_HOST',
15
+ 'RECEIVER_EMAIL',
16
+ 'RECEIVER_PASSWORD',
17
+ ];
18
+ const missing = required.filter((key) => !process.env[key]);
19
+ if (missing.length > 0) {
20
+ throw new Error(`Missing required email env variables: ${missing.join(', ')}\n` +
21
+ `Create .env file from .env.example and fill in your credentials.`);
22
+ }
23
+ }
24
+ function loadEmailConfig() {
25
+ validateEmailEnv();
26
+ return {
27
+ senderEmail: process.env.SENDER_EMAIL,
28
+ senderPassword: process.env.SENDER_PASSWORD,
29
+ senderSmtpHost: process.env.SENDER_SMTP_HOST,
30
+ receiverEmail: process.env.RECEIVER_EMAIL,
31
+ receiverPassword: process.env.RECEIVER_PASSWORD,
32
+ };
33
+ }
34
+ function isEmailConfigured() {
35
+ try {
36
+ validateEmailEnv();
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }