@civitas-cerebrum/element-interactions 0.1.1 → 0.1.2
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 +69 -6
- package/dist/enum/Options.d.ts +3 -2
- package/dist/fixture/BaseFixture.d.ts +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -1
- package/dist/interactions/Interaction.js +12 -4
- package/dist/interactions/facade/ElementInteractions.d.ts +2 -2
- package/dist/steps/CommonSteps.d.ts +2 -2
- package/dist/steps/CommonSteps.js +56 -48
- package/package.json +3 -3
- package/skills/element-interactions.md +130 -16
package/README.md
CHANGED
|
@@ -240,7 +240,11 @@ test('Complete checkout flow', async ({ steps }) => {
|
|
|
240
240
|
|
|
241
241
|
### 4. Access `repo` directly when needed
|
|
242
242
|
|
|
243
|
+
Repository methods return `Element` wrappers (not raw Playwright `Locator` objects). For most use cases, the `Steps` API handles this transparently. When using `repo` directly, the `Element` interface provides common methods like `click()`, `fill()`, `textContent()`, etc. To access the underlying Playwright `Locator` (e.g. for Playwright-specific assertions), cast to `WebElement`:
|
|
244
|
+
|
|
243
245
|
```ts
|
|
246
|
+
import { WebElement } from '@civitas-cerebrum/element-interactions';
|
|
247
|
+
|
|
244
248
|
test('Navigate to Forms category', async ({ page, repo, steps }) => {
|
|
245
249
|
await steps.navigateTo('/');
|
|
246
250
|
|
|
@@ -249,21 +253,28 @@ test('Navigate to Forms category', async ({ page, repo, steps }) => {
|
|
|
249
253
|
|
|
250
254
|
await steps.verifyAbsence('HomePage', 'categories');
|
|
251
255
|
});
|
|
256
|
+
|
|
257
|
+
test('Use underlying Locator for advanced assertions', async ({ page, repo }) => {
|
|
258
|
+
const element = await repo.get(page, 'PageName', 'elementName');
|
|
259
|
+
const locator = (element as WebElement).locator;
|
|
260
|
+
await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)');
|
|
261
|
+
});
|
|
252
262
|
```
|
|
253
263
|
|
|
254
264
|
**Full Repository API:**
|
|
255
265
|
|
|
256
266
|
```ts
|
|
257
|
-
await repo.get(page, 'PageName', 'elementName'); // single
|
|
258
|
-
await repo.getAll(page, 'PageName', 'elementName'); // array of
|
|
267
|
+
await repo.get(page, 'PageName', 'elementName'); // single Element
|
|
268
|
+
await repo.getAll(page, 'PageName', 'elementName'); // array of Elements
|
|
259
269
|
await repo.getRandom(page, 'PageName', 'elementName'); // random from matches
|
|
260
|
-
await repo.getByText(page, 'PageName', 'elementName', 'Text'); // filter by visible text
|
|
270
|
+
await repo.getByText(page, 'PageName', 'elementName', 'Text'); // filter by visible text (exact, then contains)
|
|
261
271
|
await repo.getByAttribute(page, 'PageName', 'elementName', 'data-status', 'active'); // filter by attribute
|
|
262
272
|
await repo.getByAttribute(page, 'PageName', 'elementName', 'href', '/path', { exact: false }); // partial match
|
|
263
273
|
await repo.getByIndex(page, 'PageName', 'elementName', 2); // zero-based index
|
|
264
274
|
await repo.getByRole(page, 'PageName', 'elementName', 'button'); // explicit HTML role attribute
|
|
265
275
|
await repo.getVisible(page, 'PageName', 'elementName'); // first visible match
|
|
266
276
|
repo.getSelector('PageName', 'elementName'); // sync, returns raw selector string
|
|
277
|
+
repo.getSelectorRaw('PageName', 'elementName'); // sync, returns { strategy, value }
|
|
267
278
|
repo.setDefaultTimeout(10000); // change default wait timeout
|
|
268
279
|
```
|
|
269
280
|
|
|
@@ -327,7 +338,7 @@ Every method below automatically fetches the Playwright `Locator` using your `pa
|
|
|
327
338
|
* **`uncheck(pageName, elementName)`** — Unchecks a checkbox. No-op if already unchecked.
|
|
328
339
|
* **`hover(pageName, elementName)`** — Hovers over an element to trigger dropdowns or tooltips.
|
|
329
340
|
* **`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.
|
|
341
|
+
* **`dragAndDrop(pageName, elementName, options: DragAndDropOptions)`** — Drags an element to a target element (`{ target: Locator | Element }`), by coordinate offset (`{ xOffset, yOffset }`), or both.
|
|
331
342
|
* **`dragAndDropListedElement(pageName, elementName, elementText, options: DragAndDropOptions)`** — Finds a specific element by its text from a list, then drags it to a destination.
|
|
332
343
|
* **`fill(pageName, elementName, text: string)`** — Clears and fills an input field with the provided text.
|
|
333
344
|
* **`uploadFile(pageName, elementName, filePath: string)`** — Uploads a file to an `<input type="file">` element.
|
|
@@ -457,13 +468,36 @@ Send and receive emails in your tests. Supports plain text, inline HTML, and HTM
|
|
|
457
468
|
|
|
458
469
|
### Setup
|
|
459
470
|
|
|
460
|
-
Pass email credentials to `baseFixture` via the options parameter:
|
|
471
|
+
Pass email credentials to `baseFixture` via the options parameter. You can use the split config (recommended) or the legacy combined format:
|
|
461
472
|
|
|
462
473
|
```ts
|
|
463
474
|
// tests/fixtures/base.ts
|
|
464
475
|
import { test as base, expect } from '@playwright/test';
|
|
465
476
|
import { baseFixture } from '@civitas-cerebrum/element-interactions';
|
|
466
477
|
|
|
478
|
+
// Split config (recommended) — configure smtp, imap, or both
|
|
479
|
+
export const test = baseFixture(base, 'tests/data/page-repository.json', {
|
|
480
|
+
emailCredentials: {
|
|
481
|
+
smtp: {
|
|
482
|
+
email: process.env.SENDER_EMAIL!,
|
|
483
|
+
password: process.env.SENDER_PASSWORD!,
|
|
484
|
+
host: process.env.SENDER_SMTP_HOST!,
|
|
485
|
+
},
|
|
486
|
+
imap: {
|
|
487
|
+
email: process.env.RECEIVER_EMAIL!,
|
|
488
|
+
password: process.env.RECEIVER_PASSWORD!,
|
|
489
|
+
},
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
export { expect };
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Only need to send? Provide `smtp` only. Only need to receive? Provide `imap` only. The client will throw a clear error if you call a method that requires the missing credential.
|
|
496
|
+
|
|
497
|
+
<details>
|
|
498
|
+
<summary>Legacy combined format (still supported)</summary>
|
|
499
|
+
|
|
500
|
+
```ts
|
|
467
501
|
export const test = baseFixture(base, 'tests/data/page-repository.json', {
|
|
468
502
|
emailCredentials: {
|
|
469
503
|
senderEmail: process.env.SENDER_EMAIL!,
|
|
@@ -473,9 +507,10 @@ export const test = baseFixture(base, 'tests/data/page-repository.json', {
|
|
|
473
507
|
receiverPassword: process.env.RECEIVER_PASSWORD!,
|
|
474
508
|
}
|
|
475
509
|
});
|
|
476
|
-
export { expect };
|
|
477
510
|
```
|
|
478
511
|
|
|
512
|
+
</details>
|
|
513
|
+
|
|
479
514
|
### Sending Emails
|
|
480
515
|
|
|
481
516
|
```ts
|
|
@@ -538,6 +573,32 @@ const allEmails = await steps.receiveAllEmails({
|
|
|
538
573
|
});
|
|
539
574
|
```
|
|
540
575
|
|
|
576
|
+
### Marking Emails
|
|
577
|
+
|
|
578
|
+
Mark emails as read, unread, flagged, unflagged, or archived:
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
import { EmailMarkAction } from '@civitas-cerebrum/element-interactions';
|
|
582
|
+
|
|
583
|
+
// Mark matching emails as read
|
|
584
|
+
await steps.markEmail(EmailMarkAction.READ, {
|
|
585
|
+
filters: [{ type: EmailFilterType.SUBJECT, value: 'OTP' }]
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Flag all emails from a sender
|
|
589
|
+
await steps.markEmail(EmailMarkAction.FLAGGED, {
|
|
590
|
+
filters: [{ type: EmailFilterType.FROM, value: 'noreply@example.com' }]
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// Archive emails
|
|
594
|
+
await steps.markEmail(EmailMarkAction.ARCHIVED, {
|
|
595
|
+
filters: [{ type: EmailFilterType.SUBJECT, value: 'Report' }]
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Mark all emails in folder
|
|
599
|
+
await steps.markEmail(EmailMarkAction.UNREAD);
|
|
600
|
+
```
|
|
601
|
+
|
|
541
602
|
### Cleaning the Inbox
|
|
542
603
|
|
|
543
604
|
Delete emails matching filters, or clean the entire inbox:
|
|
@@ -570,6 +631,8 @@ await steps.cleanEmails();
|
|
|
570
631
|
| `folder` | `string` | `'INBOX'` | IMAP folder to search |
|
|
571
632
|
| `waitTimeout` | `number` | `30000` | Max time (ms) to wait for a match |
|
|
572
633
|
| `pollInterval` | `number` | `3000` | How often (ms) to poll the inbox |
|
|
634
|
+
| `expectedCount` | `number` | — | Specific number of expected results |
|
|
635
|
+
| `maxFetchLimit` | `number` | `50` | Max emails to fetch per polling cycle |
|
|
573
636
|
| `downloadDir` | `string` | `os.tmpdir()/pw-emails` | Where to save the downloaded HTML |
|
|
574
637
|
|
|
575
638
|
**`ReceivedEmail` return type:**
|
package/dist/enum/Options.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Locator } from '@playwright/test';
|
|
2
|
+
import { Element } from '@civitas-cerebrum/element-repository';
|
|
2
3
|
/**
|
|
3
4
|
* Defines the strategy for selecting an option from a dropdown element.
|
|
4
5
|
*/
|
|
@@ -50,8 +51,8 @@ export type CountVerifyOptions = {
|
|
|
50
51
|
* You must provide either a `targetLocator` OR both `xOffset` and `yOffset`.
|
|
51
52
|
*/
|
|
52
53
|
export interface DragAndDropOptions {
|
|
53
|
-
/** The destination element to drop the dragged element onto. */
|
|
54
|
-
target?: Locator;
|
|
54
|
+
/** The destination element to drop the dragged element onto. Accepts a Playwright Locator or an Element from the repository. */
|
|
55
|
+
target?: Locator | Element;
|
|
55
56
|
/** The horizontal offset from the center of the element (positive moves right). */
|
|
56
57
|
xOffset?: number;
|
|
57
58
|
/** The vertical offset from the center of the element (positive moves down). */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ElementInteractions } from '../interactions/facade/ElementInteractions';
|
|
2
2
|
import { ElementRepository } from '@civitas-cerebrum/element-repository';
|
|
3
|
-
import { EmailCredentials } from '@civitas-cerebrum/email-client';
|
|
3
|
+
import { EmailCredentials, EmailClientConfig } from '@civitas-cerebrum/email-client';
|
|
4
4
|
import { ContextStore } from '@civitas-cerebrum/context-store';
|
|
5
5
|
import { test as base } from '@playwright/test';
|
|
6
6
|
import { Steps } from '../steps/CommonSteps';
|
|
@@ -11,7 +11,7 @@ type StepFixture = {
|
|
|
11
11
|
steps: Steps;
|
|
12
12
|
};
|
|
13
13
|
export interface BaseFixtureOptions {
|
|
14
|
-
emailCredentials?: EmailCredentials;
|
|
14
|
+
emailCredentials?: EmailCredentials | EmailClientConfig;
|
|
15
15
|
}
|
|
16
16
|
export declare function baseFixture<T extends {}>(baseTest: ReturnType<typeof base.extend<T>>, locatorPath: string, options?: BaseFixtureOptions): import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & StepFixture, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
17
17
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,10 @@ export { Extractions } from './interactions/Extraction';
|
|
|
6
6
|
export { reformatDateString } from './utils/DateUtilities';
|
|
7
7
|
export { Utils } from './utils/ElementUtilities';
|
|
8
8
|
export { ElementInteractions } from './interactions/facade/ElementInteractions';
|
|
9
|
+
export type { Element } from '@civitas-cerebrum/element-repository';
|
|
10
|
+
export { ElementType, WebElement, PlatformElement, isWeb, isPlatform } from '@civitas-cerebrum/element-repository';
|
|
9
11
|
export { Steps } from './steps/CommonSteps';
|
|
10
12
|
export { baseFixture, BaseFixtureOptions } from './fixture/BaseFixture';
|
|
11
13
|
export { EmailClient } from '@civitas-cerebrum/email-client';
|
|
12
|
-
export type { EmailCredentials, EmailFilter, EmailSendOptions, EmailReceiveOptions, ReceivedEmail, } from '@civitas-cerebrum/email-client';
|
|
13
|
-
export { EmailFilterType } from '@civitas-cerebrum/email-client';
|
|
14
|
+
export type { EmailCredentials, EmailClientConfig, SmtpCredentials, ImapCredentials, EmailFilter, EmailSendOptions, EmailReceiveOptions, ReceivedEmail, EmailMarkOptions, } from '@civitas-cerebrum/email-client';
|
|
15
|
+
export { EmailFilterType, EmailMarkAction } from '@civitas-cerebrum/email-client';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.EmailFilterType = exports.EmailClient = exports.baseFixture = exports.Steps = exports.ElementInteractions = exports.Utils = exports.reformatDateString = exports.Extractions = exports.Interactions = exports.Verifications = exports.Navigation = void 0;
|
|
17
|
+
exports.EmailMarkAction = exports.EmailFilterType = exports.EmailClient = exports.baseFixture = exports.Steps = exports.isPlatform = exports.isWeb = exports.PlatformElement = exports.WebElement = exports.ElementType = exports.ElementInteractions = exports.Utils = exports.reformatDateString = exports.Extractions = exports.Interactions = exports.Verifications = exports.Navigation = void 0;
|
|
18
18
|
// Enums
|
|
19
19
|
__exportStar(require("./enum/Options"), exports);
|
|
20
20
|
// Supporting Action Classes
|
|
@@ -34,6 +34,12 @@ Object.defineProperty(exports, "Utils", { enumerable: true, get: function () { r
|
|
|
34
34
|
// Element Interactions Facade
|
|
35
35
|
var ElementInteractions_1 = require("./interactions/facade/ElementInteractions");
|
|
36
36
|
Object.defineProperty(exports, "ElementInteractions", { enumerable: true, get: function () { return ElementInteractions_1.ElementInteractions; } });
|
|
37
|
+
var element_repository_1 = require("@civitas-cerebrum/element-repository");
|
|
38
|
+
Object.defineProperty(exports, "ElementType", { enumerable: true, get: function () { return element_repository_1.ElementType; } });
|
|
39
|
+
Object.defineProperty(exports, "WebElement", { enumerable: true, get: function () { return element_repository_1.WebElement; } });
|
|
40
|
+
Object.defineProperty(exports, "PlatformElement", { enumerable: true, get: function () { return element_repository_1.PlatformElement; } });
|
|
41
|
+
Object.defineProperty(exports, "isWeb", { enumerable: true, get: function () { return element_repository_1.isWeb; } });
|
|
42
|
+
Object.defineProperty(exports, "isPlatform", { enumerable: true, get: function () { return element_repository_1.isPlatform; } });
|
|
37
43
|
// Test Steps Facade
|
|
38
44
|
var CommonSteps_1 = require("./steps/CommonSteps");
|
|
39
45
|
Object.defineProperty(exports, "Steps", { enumerable: true, get: function () { return CommonSteps_1.Steps; } });
|
|
@@ -45,3 +51,4 @@ var email_client_1 = require("@civitas-cerebrum/email-client");
|
|
|
45
51
|
Object.defineProperty(exports, "EmailClient", { enumerable: true, get: function () { return email_client_1.EmailClient; } });
|
|
46
52
|
var email_client_2 = require("@civitas-cerebrum/email-client");
|
|
47
53
|
Object.defineProperty(exports, "EmailFilterType", { enumerable: true, get: function () { return email_client_2.EmailFilterType; } });
|
|
54
|
+
Object.defineProperty(exports, "EmailMarkAction", { enumerable: true, get: function () { return email_client_2.EmailMarkAction; } });
|
|
@@ -3,6 +3,13 @@ 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
|
+
/** Resolves a Locator or Element to a Playwright Locator. */
|
|
7
|
+
function resolveLocator(target) {
|
|
8
|
+
if ('_type' in target) {
|
|
9
|
+
return target.locator;
|
|
10
|
+
}
|
|
11
|
+
return target;
|
|
12
|
+
}
|
|
6
13
|
/**
|
|
7
14
|
* The `Interactions` class provides a robust set of methods for interacting
|
|
8
15
|
* with DOM elements via Playwright Locators. It abstracts away common boilerplate
|
|
@@ -136,9 +143,10 @@ class Interactions {
|
|
|
136
143
|
async dragAndDrop(locator, options) {
|
|
137
144
|
await this.utils.waitForState(locator, 'visible');
|
|
138
145
|
if (options.target) {
|
|
139
|
-
|
|
146
|
+
const target = resolveLocator(options.target);
|
|
147
|
+
await this.utils.waitForState(target, 'visible');
|
|
140
148
|
if (options.xOffset !== undefined && options.yOffset !== undefined) {
|
|
141
|
-
const targetBox = await
|
|
149
|
+
const targetBox = await target.boundingBox();
|
|
142
150
|
if (!targetBox) {
|
|
143
151
|
throw new Error(`[Action] Error -> Unable to get bounding box for target element.`);
|
|
144
152
|
}
|
|
@@ -146,13 +154,13 @@ class Interactions {
|
|
|
146
154
|
x: (targetBox.width / 2) + options.xOffset,
|
|
147
155
|
y: (targetBox.height / 2) + options.yOffset
|
|
148
156
|
};
|
|
149
|
-
await locator.dragTo(
|
|
157
|
+
await locator.dragTo(target, {
|
|
150
158
|
targetPosition,
|
|
151
159
|
timeout: this.ELEMENT_TIMEOUT
|
|
152
160
|
});
|
|
153
161
|
return;
|
|
154
162
|
}
|
|
155
|
-
await locator.dragTo(
|
|
163
|
+
await locator.dragTo(target, { timeout: this.ELEMENT_TIMEOUT });
|
|
156
164
|
return;
|
|
157
165
|
}
|
|
158
166
|
if (options.xOffset !== undefined && options.yOffset !== undefined) {
|
|
@@ -3,7 +3,7 @@ import { Interactions } from '../Interaction';
|
|
|
3
3
|
import { Navigation } from '../Navigation';
|
|
4
4
|
import { Verifications } from '../Verification';
|
|
5
5
|
import { Extractions } from '../Extraction';
|
|
6
|
-
import { EmailClient, EmailCredentials } from '@civitas-cerebrum/email-client';
|
|
6
|
+
import { EmailClient, EmailCredentials, EmailClientConfig } from '@civitas-cerebrum/email-client';
|
|
7
7
|
/**
|
|
8
8
|
* A facade class that centralizes package capabilities.
|
|
9
9
|
* It provides access to navigation, interaction, verification,
|
|
@@ -23,5 +23,5 @@ export declare class ElementInteractions {
|
|
|
23
23
|
* @param timeout - Optional global timeout override (in milliseconds) for all interactions and verifications. Defaults to 30000 ms (30 seconds).
|
|
24
24
|
* @param emailCredentials - Optional email credentials to enable the email sub-API.
|
|
25
25
|
*/
|
|
26
|
-
constructor(page: Page, timeout?: number, emailCredentials?: EmailCredentials);
|
|
26
|
+
constructor(page: Page, timeout?: number, emailCredentials?: EmailCredentials | EmailClientConfig);
|
|
27
27
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Page, Response } from '@playwright/test';
|
|
2
2
|
import { ElementRepository } from '@civitas-cerebrum/element-repository';
|
|
3
|
-
import { EmailCredentials, EmailSendOptions, EmailReceiveOptions, ReceivedEmail, EmailMarkAction, EmailFilter } from '@civitas-cerebrum/email-client';
|
|
3
|
+
import { EmailCredentials, EmailClientConfig, EmailSendOptions, EmailReceiveOptions, ReceivedEmail, EmailMarkAction, EmailFilter } from '@civitas-cerebrum/email-client';
|
|
4
4
|
import { DropdownSelectOptions, TextVerifyOptions, CountVerifyOptions, DragAndDropOptions, ListedElementMatch, VerifyListedOptions, GetListedDataOptions, FillFormValue, GetAllOptions, ScreenshotOptions } from '../enum/Options';
|
|
5
5
|
/**
|
|
6
6
|
* The `Steps` class serves as a unified Facade for test orchestration.
|
|
@@ -24,7 +24,7 @@ export declare class Steps {
|
|
|
24
24
|
* @param timeout - Optional global timeout override (in milliseconds).
|
|
25
25
|
* @param emailCredentials - Optional email credentials to enable the email sub-API.
|
|
26
26
|
*/
|
|
27
|
-
constructor(page: Page, repo: ElementRepository, timeout?: number, emailCredentials?: EmailCredentials);
|
|
27
|
+
constructor(page: Page, repo: ElementRepository, timeout?: number, emailCredentials?: EmailCredentials | EmailClientConfig);
|
|
28
28
|
/**
|
|
29
29
|
* Navigates the browser to the specified URL.
|
|
30
30
|
* @param url - The URL or path to navigate to (e.g. `'/dashboard'` or `'https://example.com'`).
|
|
@@ -4,6 +4,14 @@ exports.Steps = void 0;
|
|
|
4
4
|
const ElementInteractions_1 = require("../interactions/facade/ElementInteractions");
|
|
5
5
|
const ElementUtilities_1 = require("../utils/ElementUtilities");
|
|
6
6
|
const Logger_1 = require("../logger/Logger");
|
|
7
|
+
/**
|
|
8
|
+
* Extracts the underlying Playwright Locator from an Element wrapper.
|
|
9
|
+
* This bridges the platform-agnostic Element interface from element-repository
|
|
10
|
+
* with the Playwright-specific interaction/verification/extraction classes.
|
|
11
|
+
*/
|
|
12
|
+
function toLocator(element) {
|
|
13
|
+
return element.locator;
|
|
14
|
+
}
|
|
7
15
|
const log = {
|
|
8
16
|
navigate: (0, Logger_1.logger)('navigate'),
|
|
9
17
|
interact: (0, Logger_1.logger)('interact'),
|
|
@@ -118,7 +126,7 @@ class Steps {
|
|
|
118
126
|
*/
|
|
119
127
|
async click(pageName, elementName) {
|
|
120
128
|
log.interact('Clicking on "%s" in "%s"', elementName, pageName);
|
|
121
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
129
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
122
130
|
await this.interact.click(locator);
|
|
123
131
|
}
|
|
124
132
|
/**
|
|
@@ -129,7 +137,7 @@ class Steps {
|
|
|
129
137
|
*/
|
|
130
138
|
async clickWithoutScrolling(pageName, elementName) {
|
|
131
139
|
log.interact('Clicking (no scroll) on "%s" in "%s"', elementName, pageName);
|
|
132
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
140
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
133
141
|
await this.interact.clickWithoutScrolling(locator);
|
|
134
142
|
}
|
|
135
143
|
/**
|
|
@@ -141,10 +149,10 @@ class Steps {
|
|
|
141
149
|
*/
|
|
142
150
|
async clickRandom(pageName, elementName) {
|
|
143
151
|
log.interact('Clicking a random element from "%s" in "%s"', elementName, pageName);
|
|
144
|
-
const
|
|
145
|
-
if (!
|
|
152
|
+
const element = await this.repo.getRandom(this.page, pageName, elementName);
|
|
153
|
+
if (!element)
|
|
146
154
|
throw new Error(`No visible element found for "${elementName}" in "${pageName}"`);
|
|
147
|
-
await this.interact.click(
|
|
155
|
+
await this.interact.click(toLocator(element));
|
|
148
156
|
}
|
|
149
157
|
/**
|
|
150
158
|
* Clicks on an element only if it is present in the DOM.
|
|
@@ -155,7 +163,7 @@ class Steps {
|
|
|
155
163
|
*/
|
|
156
164
|
async clickIfPresent(pageName, elementName) {
|
|
157
165
|
log.interact('Clicking on "%s" in "%s" (if present)', elementName, pageName);
|
|
158
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
166
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
159
167
|
return this.interact.clickIfPresent(locator);
|
|
160
168
|
}
|
|
161
169
|
/**
|
|
@@ -166,7 +174,7 @@ class Steps {
|
|
|
166
174
|
*/
|
|
167
175
|
async rightClick(pageName, elementName) {
|
|
168
176
|
log.interact('Right-clicking on "%s" in "%s"', elementName, pageName);
|
|
169
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
177
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
170
178
|
await this.interact.rightClick(locator);
|
|
171
179
|
}
|
|
172
180
|
/**
|
|
@@ -176,7 +184,7 @@ class Steps {
|
|
|
176
184
|
*/
|
|
177
185
|
async doubleClick(pageName, elementName) {
|
|
178
186
|
log.interact('Double-clicking on "%s" in "%s"', elementName, pageName);
|
|
179
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
187
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
180
188
|
await this.interact.doubleClick(locator);
|
|
181
189
|
}
|
|
182
190
|
/**
|
|
@@ -186,7 +194,7 @@ class Steps {
|
|
|
186
194
|
*/
|
|
187
195
|
async check(pageName, elementName) {
|
|
188
196
|
log.interact('Checking "%s" in "%s"', elementName, pageName);
|
|
189
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
197
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
190
198
|
await this.interact.check(locator);
|
|
191
199
|
}
|
|
192
200
|
/**
|
|
@@ -196,7 +204,7 @@ class Steps {
|
|
|
196
204
|
*/
|
|
197
205
|
async uncheck(pageName, elementName) {
|
|
198
206
|
log.interact('Unchecking "%s" in "%s"', elementName, pageName);
|
|
199
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
207
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
200
208
|
await this.interact.uncheck(locator);
|
|
201
209
|
}
|
|
202
210
|
/**
|
|
@@ -206,7 +214,7 @@ class Steps {
|
|
|
206
214
|
*/
|
|
207
215
|
async hover(pageName, elementName) {
|
|
208
216
|
log.interact('Hovering over "%s" in "%s"', elementName, pageName);
|
|
209
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
217
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
210
218
|
await this.interact.hover(locator);
|
|
211
219
|
}
|
|
212
220
|
/**
|
|
@@ -216,7 +224,7 @@ class Steps {
|
|
|
216
224
|
*/
|
|
217
225
|
async scrollIntoView(pageName, elementName) {
|
|
218
226
|
log.interact('Scrolling "%s" in "%s" into view', elementName, pageName);
|
|
219
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
227
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
220
228
|
await this.interact.scrollIntoView(locator);
|
|
221
229
|
}
|
|
222
230
|
/**
|
|
@@ -227,7 +235,7 @@ class Steps {
|
|
|
227
235
|
*/
|
|
228
236
|
async fill(pageName, elementName, text) {
|
|
229
237
|
log.interact('Filling "%s" in "%s" with text: "%s"', elementName, pageName, text);
|
|
230
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
238
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
231
239
|
await this.interact.fill(locator, text);
|
|
232
240
|
}
|
|
233
241
|
/**
|
|
@@ -238,7 +246,7 @@ class Steps {
|
|
|
238
246
|
*/
|
|
239
247
|
async uploadFile(pageName, elementName, filePath) {
|
|
240
248
|
log.interact('Uploading file "%s" to "%s" in "%s"', filePath, elementName, pageName);
|
|
241
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
249
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
242
250
|
await this.interact.uploadFile(locator, filePath);
|
|
243
251
|
}
|
|
244
252
|
/**
|
|
@@ -252,7 +260,7 @@ class Steps {
|
|
|
252
260
|
*/
|
|
253
261
|
async selectDropdown(pageName, elementName, options) {
|
|
254
262
|
log.interact('Selecting dropdown option for "%s" in "%s" using options: %O', elementName, pageName, options ?? 'default (random)');
|
|
255
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
263
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
256
264
|
return await this.interact.selectDropdown(locator, options);
|
|
257
265
|
}
|
|
258
266
|
/**
|
|
@@ -264,7 +272,7 @@ class Steps {
|
|
|
264
272
|
*/
|
|
265
273
|
async dragAndDrop(pageName, elementName, options) {
|
|
266
274
|
log.interact('Dragging and dropping "%s" in "%s"', elementName, pageName);
|
|
267
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
275
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
268
276
|
await this.interact.dragAndDrop(locator, options);
|
|
269
277
|
}
|
|
270
278
|
/**
|
|
@@ -278,10 +286,10 @@ class Steps {
|
|
|
278
286
|
*/
|
|
279
287
|
async dragAndDropListedElement(pageName, elementName, elementText, options) {
|
|
280
288
|
log.interact('Dragging and dropping "%s" in "%s"', elementText, pageName);
|
|
281
|
-
const
|
|
282
|
-
if (!
|
|
289
|
+
const element = await this.repo.getByText(this.page, pageName, elementName, elementText);
|
|
290
|
+
if (!element)
|
|
283
291
|
throw new Error(`No element with text "${elementText}" found for "${elementName}" in "${pageName}"`);
|
|
284
|
-
await this.interact.dragAndDrop(
|
|
292
|
+
await this.interact.dragAndDrop(toLocator(element), options);
|
|
285
293
|
}
|
|
286
294
|
/**
|
|
287
295
|
* Sets the value of a range/slider input element.
|
|
@@ -291,7 +299,7 @@ class Steps {
|
|
|
291
299
|
*/
|
|
292
300
|
async setSliderValue(pageName, elementName, value) {
|
|
293
301
|
log.interact('Setting slider "%s" in "%s" to value: %d', elementName, pageName, value);
|
|
294
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
302
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
295
303
|
await this.interact.setSliderValue(locator, value);
|
|
296
304
|
}
|
|
297
305
|
/**
|
|
@@ -314,7 +322,7 @@ class Steps {
|
|
|
314
322
|
*/
|
|
315
323
|
async getText(pageName, elementName) {
|
|
316
324
|
log.extract('Getting text from "%s" in "%s"', elementName, pageName);
|
|
317
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
325
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
318
326
|
return await this.extract.getText(locator);
|
|
319
327
|
}
|
|
320
328
|
/**
|
|
@@ -326,7 +334,7 @@ class Steps {
|
|
|
326
334
|
*/
|
|
327
335
|
async getAttribute(pageName, elementName, attributeName) {
|
|
328
336
|
log.extract('Getting attribute "%s" from "%s" in "%s"', attributeName, elementName, pageName);
|
|
329
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
337
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
330
338
|
return await this.extract.getAttribute(locator, attributeName);
|
|
331
339
|
}
|
|
332
340
|
// ==========================================
|
|
@@ -339,7 +347,7 @@ class Steps {
|
|
|
339
347
|
*/
|
|
340
348
|
async verifyPresence(pageName, elementName) {
|
|
341
349
|
log.verify('Verifying presence of "%s" in "%s"', elementName, pageName);
|
|
342
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
350
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
343
351
|
await this.verify.presence(locator);
|
|
344
352
|
}
|
|
345
353
|
/**
|
|
@@ -364,7 +372,7 @@ class Steps {
|
|
|
364
372
|
async verifyText(pageName, elementName, expectedText, options) {
|
|
365
373
|
const logDetail = options?.notEmpty ? 'is not empty' : `matches: "${expectedText}"`;
|
|
366
374
|
log.verify('Verifying text of "%s" in "%s" %s', elementName, pageName, logDetail);
|
|
367
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
375
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
368
376
|
await this.verify.text(locator, expectedText, options);
|
|
369
377
|
}
|
|
370
378
|
/**
|
|
@@ -376,7 +384,7 @@ class Steps {
|
|
|
376
384
|
*/
|
|
377
385
|
async verifyCount(pageName, elementName, options) {
|
|
378
386
|
log.verify('Verifying count for "%s" in "%s" with options: %O', elementName, pageName, options);
|
|
379
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
387
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
380
388
|
await this.verify.count(locator, options);
|
|
381
389
|
}
|
|
382
390
|
/**
|
|
@@ -388,7 +396,7 @@ class Steps {
|
|
|
388
396
|
*/
|
|
389
397
|
async verifyImages(pageName, elementName, scroll = true) {
|
|
390
398
|
log.verify('Verifying images for "%s" in "%s" (scroll: %s)', elementName, pageName, scroll);
|
|
391
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
399
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
392
400
|
await this.verify.images(locator, scroll);
|
|
393
401
|
}
|
|
394
402
|
/**
|
|
@@ -399,7 +407,7 @@ class Steps {
|
|
|
399
407
|
*/
|
|
400
408
|
async verifyTextContains(pageName, elementName, expectedText) {
|
|
401
409
|
log.verify('Verifying "%s" in "%s" contains text: "%s"', elementName, pageName, expectedText);
|
|
402
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
410
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
403
411
|
await this.verify.textContains(locator, expectedText);
|
|
404
412
|
}
|
|
405
413
|
/**
|
|
@@ -424,7 +432,7 @@ class Steps {
|
|
|
424
432
|
*/
|
|
425
433
|
async verifyAttribute(pageName, elementName, attributeName, expectedValue) {
|
|
426
434
|
log.verify('Verifying "%s" in "%s" has attribute "%s" = "%s"', elementName, pageName, attributeName, expectedValue);
|
|
427
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
435
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
428
436
|
await this.verify.attribute(locator, attributeName, expectedValue);
|
|
429
437
|
}
|
|
430
438
|
/**
|
|
@@ -445,7 +453,7 @@ class Steps {
|
|
|
445
453
|
*/
|
|
446
454
|
async verifyInputValue(pageName, elementName, expectedValue) {
|
|
447
455
|
log.verify('Verifying input value of "%s" in "%s" matches: "%s"', elementName, pageName, expectedValue);
|
|
448
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
456
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
449
457
|
await this.verify.inputValue(locator, expectedValue);
|
|
450
458
|
}
|
|
451
459
|
/**
|
|
@@ -476,7 +484,7 @@ class Steps {
|
|
|
476
484
|
*/
|
|
477
485
|
async clickListedElement(pageName, elementName, options) {
|
|
478
486
|
log.interact('Clicking listed element in "%s" > "%s" with options: %O', pageName, elementName, options);
|
|
479
|
-
const baseLocator = await this.repo.get(this.page, pageName, elementName);
|
|
487
|
+
const baseLocator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
480
488
|
const target = await this.interact.getListedElement(baseLocator, options, this.repo);
|
|
481
489
|
await this.interact.click(target);
|
|
482
490
|
}
|
|
@@ -499,7 +507,7 @@ class Steps {
|
|
|
499
507
|
*/
|
|
500
508
|
async verifyListedElement(pageName, elementName, options) {
|
|
501
509
|
log.verify('Verifying listed element in "%s" > "%s" with options: %O', pageName, elementName, options);
|
|
502
|
-
const baseLocator = await this.repo.get(this.page, pageName, elementName);
|
|
510
|
+
const baseLocator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
503
511
|
const target = await this.interact.getListedElement(baseLocator, options, this.repo);
|
|
504
512
|
if (options.expectedText !== undefined) {
|
|
505
513
|
await this.verify.text(target, options.expectedText);
|
|
@@ -528,7 +536,7 @@ class Steps {
|
|
|
528
536
|
*/
|
|
529
537
|
async getListedElementData(pageName, elementName, options) {
|
|
530
538
|
log.extract('Extracting data from listed element in "%s" > "%s" with options: %O', pageName, elementName, options);
|
|
531
|
-
const baseLocator = await this.repo.get(this.page, pageName, elementName);
|
|
539
|
+
const baseLocator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
532
540
|
const target = await this.interact.getListedElement(baseLocator, options, this.repo);
|
|
533
541
|
if (options.extractAttribute) {
|
|
534
542
|
return await this.extract.getAttribute(target, options.extractAttribute);
|
|
@@ -547,7 +555,7 @@ class Steps {
|
|
|
547
555
|
*/
|
|
548
556
|
async waitForState(pageName, elementName, state = 'visible') {
|
|
549
557
|
log.wait('Waiting for "%s" in "%s" to be "%s"', elementName, pageName, state);
|
|
550
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
558
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
551
559
|
await this.utils.waitForState(locator, state);
|
|
552
560
|
}
|
|
553
561
|
/**
|
|
@@ -561,7 +569,7 @@ class Steps {
|
|
|
561
569
|
*/
|
|
562
570
|
async typeSequentially(pageName, elementName, text, delay = 100) {
|
|
563
571
|
log.interact('Typing "%s" sequentially into "%s" in "%s" (delay: %dms)', text, elementName, pageName, delay);
|
|
564
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
572
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
565
573
|
await this.interact.typeSequentially(locator, text, delay);
|
|
566
574
|
}
|
|
567
575
|
// ==========================================
|
|
@@ -652,7 +660,7 @@ class Steps {
|
|
|
652
660
|
*/
|
|
653
661
|
async clearInput(pageName, elementName) {
|
|
654
662
|
log.interact('Clearing input "%s" in "%s"', elementName, pageName);
|
|
655
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
663
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
656
664
|
await this.interact.clearInput(locator);
|
|
657
665
|
}
|
|
658
666
|
/**
|
|
@@ -664,7 +672,7 @@ class Steps {
|
|
|
664
672
|
*/
|
|
665
673
|
async selectMultiple(pageName, elementName, values) {
|
|
666
674
|
log.interact('Selecting multiple values on "%s" in "%s": %O', elementName, pageName, values);
|
|
667
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
675
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
668
676
|
return await this.interact.selectMultiple(locator, values);
|
|
669
677
|
}
|
|
670
678
|
/**
|
|
@@ -677,7 +685,7 @@ class Steps {
|
|
|
677
685
|
*/
|
|
678
686
|
async waitAndClick(pageName, elementName, state = 'visible') {
|
|
679
687
|
log.interact('Waiting for "%s" in "%s" to be "%s", then clicking', elementName, pageName, state);
|
|
680
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
688
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
681
689
|
await this.utils.waitForState(locator, state);
|
|
682
690
|
await this.interact.click(locator);
|
|
683
691
|
}
|
|
@@ -692,10 +700,10 @@ class Steps {
|
|
|
692
700
|
*/
|
|
693
701
|
async clickNth(pageName, elementName, index) {
|
|
694
702
|
log.interact('Clicking element at index %d of "%s" in "%s"', index, elementName, pageName);
|
|
695
|
-
const
|
|
696
|
-
if (!
|
|
703
|
+
const element = await this.repo.getByIndex(this.page, pageName, elementName, index);
|
|
704
|
+
if (!element)
|
|
697
705
|
throw new Error(`No element at index ${index} for "${elementName}" in "${pageName}"`);
|
|
698
|
-
await this.interact.click(
|
|
706
|
+
await this.interact.click(toLocator(element));
|
|
699
707
|
}
|
|
700
708
|
// ==========================================
|
|
701
709
|
// 📊 ADDITIONAL DATA EXTRACTION STEPS
|
|
@@ -721,7 +729,7 @@ class Steps {
|
|
|
721
729
|
*/
|
|
722
730
|
async getAll(pageName, elementName, options) {
|
|
723
731
|
log.extract('Extracting all from "%s" in "%s" with options: %O', elementName, pageName, options ?? 'text');
|
|
724
|
-
let locator = await this.repo.get(this.page, pageName, elementName);
|
|
732
|
+
let locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
725
733
|
if (options?.child) {
|
|
726
734
|
if (typeof options.child === 'string') {
|
|
727
735
|
locator = locator.locator(options.child);
|
|
@@ -747,7 +755,7 @@ class Steps {
|
|
|
747
755
|
*/
|
|
748
756
|
async getCount(pageName, elementName) {
|
|
749
757
|
log.extract('Getting count of "%s" in "%s"', elementName, pageName);
|
|
750
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
758
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
751
759
|
return await this.extract.getCount(locator);
|
|
752
760
|
}
|
|
753
761
|
/**
|
|
@@ -759,7 +767,7 @@ class Steps {
|
|
|
759
767
|
*/
|
|
760
768
|
async getInputValue(pageName, elementName) {
|
|
761
769
|
log.extract('Getting input value of "%s" in "%s"', elementName, pageName);
|
|
762
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
770
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
763
771
|
return await this.extract.getInputValue(locator);
|
|
764
772
|
}
|
|
765
773
|
/**
|
|
@@ -771,7 +779,7 @@ class Steps {
|
|
|
771
779
|
*/
|
|
772
780
|
async getCssProperty(pageName, elementName, property) {
|
|
773
781
|
log.extract('Getting CSS "%s" from "%s" in "%s"', property, elementName, pageName);
|
|
774
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
782
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
775
783
|
return await this.extract.getCssProperty(locator, property);
|
|
776
784
|
}
|
|
777
785
|
// ==========================================
|
|
@@ -787,7 +795,7 @@ class Steps {
|
|
|
787
795
|
*/
|
|
788
796
|
async verifyOrder(pageName, elementName, expectedTexts) {
|
|
789
797
|
log.verify('Verifying order of "%s" in "%s": %O', elementName, pageName, expectedTexts);
|
|
790
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
798
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
791
799
|
await this.verify.order(locator, expectedTexts);
|
|
792
800
|
}
|
|
793
801
|
/**
|
|
@@ -800,7 +808,7 @@ class Steps {
|
|
|
800
808
|
*/
|
|
801
809
|
async verifyCssProperty(pageName, elementName, property, expectedValue) {
|
|
802
810
|
log.verify('Verifying CSS "%s" of "%s" in "%s" = "%s"', property, elementName, pageName, expectedValue);
|
|
803
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
811
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
804
812
|
await this.verify.cssProperty(locator, property, expectedValue);
|
|
805
813
|
}
|
|
806
814
|
/**
|
|
@@ -812,7 +820,7 @@ class Steps {
|
|
|
812
820
|
*/
|
|
813
821
|
async verifyListOrder(pageName, elementName, direction) {
|
|
814
822
|
log.verify('Verifying "%s" in "%s" is sorted %s', elementName, pageName, direction);
|
|
815
|
-
const locator = await this.repo.get(this.page, pageName, elementName);
|
|
823
|
+
const locator = toLocator(await this.repo.get(this.page, pageName, elementName));
|
|
816
824
|
await this.verify.listOrder(locator, direction);
|
|
817
825
|
}
|
|
818
826
|
// ==========================================
|
|
@@ -841,7 +849,7 @@ class Steps {
|
|
|
841
849
|
async screenshot(pageNameOrOptions, elementName, options) {
|
|
842
850
|
if (typeof pageNameOrOptions === 'string' && elementName) {
|
|
843
851
|
log.extract('Taking screenshot of "%s" in "%s"', elementName, pageNameOrOptions);
|
|
844
|
-
const locator = await this.repo.get(this.page, pageNameOrOptions, elementName);
|
|
852
|
+
const locator = toLocator(await this.repo.get(this.page, pageNameOrOptions, elementName));
|
|
845
853
|
return await this.extract.screenshot(locator, options);
|
|
846
854
|
}
|
|
847
855
|
const opts = typeof pageNameOrOptions === 'object' ? pageNameOrOptions : options;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@civitas-cerebrum/element-interactions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A robust, readable interaction and assertion Facade for Playwright. Abstract away boilerplate into semantic, English-like methods, making your test automation framework cleaner, easier to maintain, and accessible to non-developers.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"license": "MIT",
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@civitas-cerebrum/context-store": "^0.0.2",
|
|
58
|
-
"@civitas-cerebrum/element-repository": "^0.0
|
|
59
|
-
"@civitas-cerebrum/email-client": "^0.0.
|
|
58
|
+
"@civitas-cerebrum/element-repository": "^0.1.0",
|
|
59
|
+
"@civitas-cerebrum/email-client": "^0.0.5",
|
|
60
60
|
"debug": "^4.4.3",
|
|
61
61
|
"dotenv": "^17.3.1"
|
|
62
62
|
}
|
|
@@ -18,7 +18,7 @@ A two-package Playwright framework that decouples **element acquisition** (`@civ
|
|
|
18
18
|
These rules are non-negotiable. They override helpfulness, initiative, and assumptions. If you are unsure about any rule, ask the user. Do not guess.
|
|
19
19
|
|
|
20
20
|
### 1. Do NOT skip stages
|
|
21
|
-
- This skill operates in
|
|
21
|
+
- This skill operates in four stages. You MUST complete each stage and get user approval before advancing.
|
|
22
22
|
- Do NOT jump ahead. Do NOT write automation code during the discovery stage.
|
|
23
23
|
- Exception: API questions and fix/edit requests bypass the staged flow (see Opening section).
|
|
24
24
|
|
|
@@ -57,10 +57,10 @@ These rules are non-negotiable. They override helpfulness, initiative, and assum
|
|
|
57
57
|
|
|
58
58
|
## Staged Workflow
|
|
59
59
|
|
|
60
|
-
This skill operates in **
|
|
60
|
+
This skill operates in **four stages**. Each stage has a hard gate — you MUST get user approval before advancing to the next stage.
|
|
61
61
|
|
|
62
62
|
<HARD-GATE>
|
|
63
|
-
Do NOT write any automation code until Stage 3. Do NOT create selectors until Stage 2. Do NOT skip the discovery conversation in Stage 1. Every engagement follows all
|
|
63
|
+
Do NOT write any automation code until Stage 3. Do NOT create selectors until Stage 2. Do NOT skip the discovery conversation in Stage 1. Every engagement follows all four stages regardless of perceived simplicity.
|
|
64
64
|
</HARD-GATE>
|
|
65
65
|
|
|
66
66
|
### Checklist
|
|
@@ -74,7 +74,9 @@ You MUST create a task for each of these items and complete them in order:
|
|
|
74
74
|
5. **User approves selectors** — hard gate
|
|
75
75
|
6. **Stage 3: Write Automation** — write the test using the Steps API and approved selectors
|
|
76
76
|
7. **Run and validate** — execute the test, inspect failures visually, iterate
|
|
77
|
-
8. **
|
|
77
|
+
8. **Stage 4: API Compliance Review** — review all scenarios against the API Reference to catch incorrect usage
|
|
78
|
+
9. **Fix any issues found** — correct API misuse, re-run tests
|
|
79
|
+
10. **Commit** — commit after confirmed success
|
|
78
80
|
|
|
79
81
|
### Process Flow
|
|
80
82
|
|
|
@@ -114,6 +116,12 @@ digraph element_interactions {
|
|
|
114
116
|
"New selector needed?" [shape=diamond];
|
|
115
117
|
"Mini-inspection:\ninspect DOM, propose,\nget approval" [shape=box];
|
|
116
118
|
"Inspect screenshot, fix, re-run" [shape=box];
|
|
119
|
+
|
|
120
|
+
"STAGE 4: API Compliance Review" [shape=box, style=bold];
|
|
121
|
+
"Review all test code\nagainst API Reference" [shape=box];
|
|
122
|
+
"Issues found?" [shape=diamond];
|
|
123
|
+
"Fix API misuse,\nre-run tests" [shape=box];
|
|
124
|
+
"All scenarios pass\nafter fixes?" [shape=diamond];
|
|
117
125
|
"Commit" [shape=doublecircle];
|
|
118
126
|
|
|
119
127
|
"Skill activated" -> "Read user message";
|
|
@@ -153,12 +161,20 @@ digraph element_interactions {
|
|
|
153
161
|
"STAGE 3: Write Automation" -> "Write test using Steps API";
|
|
154
162
|
"Write test using Steps API" -> "Run test";
|
|
155
163
|
"Run test" -> "Test passes?";
|
|
156
|
-
"Test passes?" -> "
|
|
164
|
+
"Test passes?" -> "STAGE 4: API Compliance Review" [label="yes"];
|
|
157
165
|
"Test passes?" -> "New selector needed?" [label="no"];
|
|
158
166
|
"New selector needed?" -> "Mini-inspection:\ninspect DOM, propose,\nget approval" [label="yes"];
|
|
159
167
|
"New selector needed?" -> "Inspect screenshot, fix, re-run" [label="no — code issue"];
|
|
160
168
|
"Mini-inspection:\ninspect DOM, propose,\nget approval" -> "Inspect screenshot, fix, re-run";
|
|
161
169
|
"Inspect screenshot, fix, re-run" -> "Run test";
|
|
170
|
+
|
|
171
|
+
"STAGE 4: API Compliance Review" -> "Review all test code\nagainst API Reference";
|
|
172
|
+
"Review all test code\nagainst API Reference" -> "Issues found?";
|
|
173
|
+
"Issues found?" -> "Commit" [label="no — all correct"];
|
|
174
|
+
"Issues found?" -> "Fix API misuse,\nre-run tests" [label="yes"];
|
|
175
|
+
"Fix API misuse,\nre-run tests" -> "All scenarios pass\nafter fixes?";
|
|
176
|
+
"All scenarios pass\nafter fixes?" -> "Commit" [label="yes"];
|
|
177
|
+
"All scenarios pass\nafter fixes?" -> "Inspect screenshot, fix, re-run" [label="no — regression"];
|
|
162
178
|
}
|
|
163
179
|
```
|
|
164
180
|
|
|
@@ -284,7 +300,7 @@ Show the user the exact JSON entries you want to add:
|
|
|
284
300
|
|
|
285
301
|
### Writing Process
|
|
286
302
|
|
|
287
|
-
1. **Check project setup.** Read `tests/fixtures/base.ts` and `playwright.config.ts` — create or update only if missing or broken.
|
|
303
|
+
1. **Check project setup.** Read `tests/fixtures/base.ts` and `playwright.config.ts` — create or update only if missing or broken. Also verify that `.gitignore` includes `.claude/` and `CLAUDE.md` to prevent Claude Code configuration from being pushed to the repository — add them if missing.
|
|
288
304
|
2. **Add approved selectors** to `page-repository.json` (if not already done).
|
|
289
305
|
3. **Write the test file** using the Steps API. Every interaction goes through `steps.*` methods — no raw `page.locator()` calls.
|
|
290
306
|
4. **Run the test** with `npx playwright test <test-file>`.
|
|
@@ -297,6 +313,59 @@ When the user asks to fix or edit an existing test, skip Stages 1 and 2. Read th
|
|
|
297
313
|
|
|
298
314
|
---
|
|
299
315
|
|
|
316
|
+
## Stage 4: API Compliance Review
|
|
317
|
+
|
|
318
|
+
**Goal:** After all scenarios pass, review every test file written in this session against the API Reference to ensure correct usage of the `@civitas-cerebrum/element-interactions` package.
|
|
319
|
+
|
|
320
|
+
**This stage triggers automatically** once all tests pass in Stage 3. Do NOT skip it — even if the tests pass, they may be using the API incorrectly (wrong argument order, deprecated methods, missing options, incorrect types).
|
|
321
|
+
|
|
322
|
+
### Review Checklist
|
|
323
|
+
|
|
324
|
+
For each test file, verify:
|
|
325
|
+
|
|
326
|
+
1. **Method signatures** — every `steps.*` call matches the exact signature in the API Reference (correct argument count, correct argument order, correct types).
|
|
327
|
+
2. **Imports** — all types used (`DropdownSelectType`, `EmailFilterType`, `FillFormValue`, etc.) are imported from `@civitas-cerebrum/element-interactions` (or `@civitas-cerebrum/email-client` for email types). No invented imports.
|
|
328
|
+
3. **Page/element naming** — `pageName` uses PascalCase, `elementName` uses camelCase, and both match entries in `page-repository.json`.
|
|
329
|
+
4. **Listed element options** — `child` uses `{ pageName, elementName }` repo references where possible instead of inline selectors (per Rule 5).
|
|
330
|
+
5. **Dropdown select usage** — `DropdownSelectType.RANDOM`, `.VALUE`, or `.INDEX` with the correct companion field (`value` or `index`).
|
|
331
|
+
6. **Email API usage** — `steps.sendEmail` / `steps.receiveEmail` / `steps.receiveAllEmails` / `steps.cleanEmails` match the documented signatures. Filter types use `EmailFilterType` enum.
|
|
332
|
+
7. **No raw Playwright calls** — no `page.locator()`, `page.click()`, `page.fill()`, or other raw Playwright methods where a `steps.*` equivalent exists.
|
|
333
|
+
8. **Fixture usage** — the test destructures only fixtures provided by `baseFixture` (`steps`, `repo`, `interactions`, `contextStore`, `page`) plus any custom fixtures defined in the project's `base.ts`.
|
|
334
|
+
9. **Waiting methods** — correct state strings (`'visible'`, `'hidden'`, `'attached'`, `'detached'`) and correct usage of `waitForResponse` callback pattern.
|
|
335
|
+
10. **Verification methods** — correct option shapes (`{ exactly }`, `{ greaterThan }`, `{ lessThan }` for `verifyCount`; `{ notEmpty: true }` for `verifyText`).
|
|
336
|
+
|
|
337
|
+
### Process
|
|
338
|
+
|
|
339
|
+
1. **Read each test file** written or modified in this session.
|
|
340
|
+
2. **Cross-reference every API call** against the API Reference section below.
|
|
341
|
+
3. **Report findings** to the user — list any issues found with the specific line, what's wrong, and the correct usage.
|
|
342
|
+
4. **If issues are found:** investigate *why* the non-compliant code was written — was the API misunderstood? Was a method signature wrong in the scenario? Did a previous stage produce incorrect assumptions? Understanding the root cause prevents the same mistake from recurring in the next scenario. Then fix, re-run the tests, and confirm they still pass.
|
|
343
|
+
5. **If fixes cause a test failure:** follow Rule 6 — inspect the failure screenshot first before attempting any further fix. Do NOT guess from the error message alone.
|
|
344
|
+
6. **If no issues are found:** confirm compliance and proceed to commit.
|
|
345
|
+
|
|
346
|
+
### Output Format
|
|
347
|
+
|
|
348
|
+
Present the review as:
|
|
349
|
+
|
|
350
|
+
> **API Compliance Review**
|
|
351
|
+
>
|
|
352
|
+
> Reviewed: `tests/example.spec.ts`, `tests/login.spec.ts`
|
|
353
|
+
>
|
|
354
|
+
> - **`example.spec.ts:15`** — `steps.backOrForward('back')` should be `steps.backOrForward('BACKWARDS')` (uses uppercase enum-style strings)
|
|
355
|
+
> - **`login.spec.ts:8`** — missing import for `DropdownSelectType`
|
|
356
|
+
>
|
|
357
|
+
> [number] issue(s) found. Fixing now.
|
|
358
|
+
|
|
359
|
+
Or if clean:
|
|
360
|
+
|
|
361
|
+
> **API Compliance Review**
|
|
362
|
+
>
|
|
363
|
+
> Reviewed: `tests/example.spec.ts`
|
|
364
|
+
>
|
|
365
|
+
> All API calls match the documented signatures. No issues found.
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
300
369
|
## API Reference
|
|
301
370
|
|
|
302
371
|
The following sections document the full API available for writing tests in Stage 3.
|
|
@@ -358,7 +427,7 @@ Every method takes `pageName` and `elementName` as its first two arguments, matc
|
|
|
358
427
|
|
|
359
428
|
**Imports** — add at the top of your test file as needed:
|
|
360
429
|
```ts
|
|
361
|
-
import { DropdownSelectType, ListedElementMatch, VerifyListedOptions, GetListedDataOptions, FillFormValue, ScreenshotOptions } from '@civitas-cerebrum/element-interactions';
|
|
430
|
+
import { DropdownSelectType, ListedElementMatch, VerifyListedOptions, GetListedDataOptions, FillFormValue, ScreenshotOptions, EmailFilterType, EmailMarkAction, WebElement } from '@civitas-cerebrum/element-interactions';
|
|
362
431
|
```
|
|
363
432
|
|
|
364
433
|
#### Navigation
|
|
@@ -404,10 +473,10 @@ const val2 = await steps.selectDropdown('PageName', 'elementName', { type: Dropd
|
|
|
404
473
|
const val3 = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.INDEX, index: 2 });
|
|
405
474
|
await steps.selectMultiple('PageName', 'multiSelect', ['opt1', 'opt2']);
|
|
406
475
|
|
|
407
|
-
// Drag and drop
|
|
408
|
-
await steps.dragAndDrop('PageName', 'elementName', { target:
|
|
476
|
+
// Drag and drop — target accepts a Locator or Element from the repository
|
|
477
|
+
await steps.dragAndDrop('PageName', 'elementName', { target: otherLocatorOrElement });
|
|
409
478
|
await steps.dragAndDrop('PageName', 'elementName', { xOffset: 100, yOffset: 0 });
|
|
410
|
-
await steps.dragAndDropListedElement('PageName', 'elementName', 'Item Label', { target:
|
|
479
|
+
await steps.dragAndDropListedElement('PageName', 'elementName', 'Item Label', { target: otherLocatorOrElement });
|
|
411
480
|
```
|
|
412
481
|
|
|
413
482
|
#### Data Extraction
|
|
@@ -511,27 +580,34 @@ const buf3 = await steps.screenshot('PageName', 'elementName'); // e
|
|
|
511
580
|
|
|
512
581
|
### Accessing the Repository Directly
|
|
513
582
|
|
|
514
|
-
Use `repo` when you need to filter by visible text, iterate all matches, or pick a random item
|
|
583
|
+
Use `repo` when you need to filter by visible text, iterate all matches, or pick a random item. Repository methods return `Element` wrappers (not raw Playwright `Locator` objects). The `Element` interface provides common methods like `click()`, `fill()`, `textContent()`, etc. To access the underlying Playwright `Locator` (e.g. for Playwright-specific assertions), cast to `WebElement`:
|
|
515
584
|
|
|
516
585
|
```ts
|
|
586
|
+
import { WebElement } from '@civitas-cerebrum/element-interactions';
|
|
587
|
+
|
|
517
588
|
test('example', async ({ page, repo, steps }) => {
|
|
518
589
|
await steps.navigateTo('/');
|
|
519
590
|
const link = await repo.getByText(page, 'HomePage', 'categories', 'Forms');
|
|
520
|
-
await link?.click();
|
|
591
|
+
await link?.click(); // Element.click() works directly
|
|
592
|
+
|
|
593
|
+
// When you need the underlying Locator:
|
|
594
|
+
const element = await repo.get(page, 'PageName', 'elementName');
|
|
595
|
+
const locator = (element as WebElement).locator; // access raw Playwright Locator
|
|
521
596
|
});
|
|
522
597
|
```
|
|
523
598
|
|
|
524
599
|
```ts
|
|
525
|
-
await repo.get(page, 'PageName', 'elementName');
|
|
526
|
-
await repo.getAll(page, 'PageName', 'elementName');
|
|
527
|
-
await repo.getRandom(page, 'PageName', 'elementName');
|
|
528
|
-
await repo.getByText(page, 'PageName', 'elementName', 'Text');
|
|
600
|
+
await repo.get(page, 'PageName', 'elementName'); // single Element
|
|
601
|
+
await repo.getAll(page, 'PageName', 'elementName'); // array of Elements
|
|
602
|
+
await repo.getRandom(page, 'PageName', 'elementName'); // random from matches
|
|
603
|
+
await repo.getByText(page, 'PageName', 'elementName', 'Text'); // exact match, then contains fallback
|
|
529
604
|
await repo.getByAttribute(page, 'PageName', 'elementName', 'data-status', 'active');
|
|
530
605
|
await repo.getByAttribute(page, 'PageName', 'elementName', 'href', '/path', { exact: false });
|
|
531
606
|
await repo.getByIndex(page, 'PageName', 'elementName', 2);
|
|
532
607
|
await repo.getByRole(page, 'PageName', 'elementName', 'button');
|
|
533
608
|
await repo.getVisible(page, 'PageName', 'elementName');
|
|
534
609
|
repo.getSelector('PageName', 'elementName'); // sync, returns raw selector string
|
|
610
|
+
repo.getSelectorRaw('PageName', 'elementName'); // sync, returns { strategy, value }
|
|
535
611
|
repo.setDefaultTimeout(10000);
|
|
536
612
|
```
|
|
537
613
|
|
|
@@ -556,7 +632,25 @@ Send and receive emails in tests. Supports plain-text, inline HTML, and HTML fil
|
|
|
556
632
|
|
|
557
633
|
#### Setup
|
|
558
634
|
|
|
635
|
+
Pass email credentials using the split config (recommended) or the legacy combined format. You can provide `smtp` only, `imap` only, or both:
|
|
636
|
+
|
|
559
637
|
```ts
|
|
638
|
+
// Split config (recommended)
|
|
639
|
+
export const test = baseFixture(base, 'tests/data/page-repository.json', {
|
|
640
|
+
emailCredentials: {
|
|
641
|
+
smtp: {
|
|
642
|
+
email: process.env.SENDER_EMAIL!,
|
|
643
|
+
password: process.env.SENDER_PASSWORD!,
|
|
644
|
+
host: process.env.SENDER_SMTP_HOST!,
|
|
645
|
+
},
|
|
646
|
+
imap: {
|
|
647
|
+
email: process.env.RECEIVER_EMAIL!,
|
|
648
|
+
password: process.env.RECEIVER_PASSWORD!,
|
|
649
|
+
},
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// Legacy combined format (still supported)
|
|
560
654
|
export const test = baseFixture(base, 'tests/data/page-repository.json', {
|
|
561
655
|
emailCredentials: {
|
|
562
656
|
senderEmail: process.env.SENDER_EMAIL!,
|
|
@@ -572,6 +666,7 @@ export const test = baseFixture(base, 'tests/data/page-repository.json', {
|
|
|
572
666
|
|
|
573
667
|
```ts
|
|
574
668
|
await steps.sendEmail({ to: 'user@example.com', subject: 'Test', text: 'Hello' });
|
|
669
|
+
await steps.sendEmail({ to: 'user@example.com', subject: 'Report', html: '<h1>Results</h1>' });
|
|
575
670
|
await steps.sendEmail({ to: 'user@example.com', subject: 'Report', htmlFile: 'emails/report.html' });
|
|
576
671
|
```
|
|
577
672
|
|
|
@@ -598,6 +693,25 @@ const allEmails = await steps.receiveAllEmails({
|
|
|
598
693
|
});
|
|
599
694
|
```
|
|
600
695
|
|
|
696
|
+
#### Marking Emails
|
|
697
|
+
|
|
698
|
+
```ts
|
|
699
|
+
import { EmailMarkAction } from '@civitas-cerebrum/element-interactions';
|
|
700
|
+
|
|
701
|
+
await steps.markEmail(EmailMarkAction.READ, {
|
|
702
|
+
filters: [{ type: EmailFilterType.SUBJECT, value: 'OTP' }]
|
|
703
|
+
});
|
|
704
|
+
await steps.markEmail(EmailMarkAction.FLAGGED, {
|
|
705
|
+
filters: [{ type: EmailFilterType.FROM, value: 'noreply@example.com' }]
|
|
706
|
+
});
|
|
707
|
+
await steps.markEmail(EmailMarkAction.ARCHIVED, {
|
|
708
|
+
filters: [{ type: EmailFilterType.SUBJECT, value: 'Report' }]
|
|
709
|
+
});
|
|
710
|
+
await steps.markEmail(EmailMarkAction.UNREAD); // mark all in folder
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Mark actions: `READ`, `UNREAD`, `FLAGGED`, `UNFLAGGED`, `ARCHIVED`.
|
|
714
|
+
|
|
601
715
|
#### Cleaning the Inbox
|
|
602
716
|
|
|
603
717
|
```ts
|