@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 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 locator
258
- await repo.getAll(page, 'PageName', 'elementName'); // array of locators
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:**
@@ -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
- await this.utils.waitForState(options.target, 'visible');
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 options.target.boundingBox();
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(options.target, {
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(options.target, { timeout: this.ELEMENT_TIMEOUT });
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 locator = await this.repo.getRandom(this.page, pageName, elementName);
145
- if (!locator)
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(locator);
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 locator = await this.repo.getByText(this.page, pageName, elementName, elementText);
282
- if (!locator)
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(locator, options);
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 locator = await this.repo.getByIndex(this.page, pageName, elementName, index);
696
- if (!locator)
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(locator);
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.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.6",
59
- "@civitas-cerebrum/email-client": "^0.0.4",
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 three stages. You MUST complete each stage and get user approval before advancing.
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 **three stages**. Each stage has a hard gate — you MUST get user approval before advancing to the next stage.
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 three stages regardless of perceived simplicity.
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. **Commit** — commit after confirmed success
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?" -> "Commit" [label="yes"];
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: otherLocator });
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: otherLocator });
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