@testspectra/cli 1.0.5 → 1.0.6

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
@@ -35,13 +35,67 @@ The **TestSpectra CLI** (`spectra` / `testspectra`) is the command-line orchestr
35
35
 
36
36
  ### A. Zero-Import Multi-Platform Authoring
37
37
  TestSpectra allows developers to write clean, boilerplate-free test specs, steps, actions, and page objects without manual `import` statements for globals or framework entities:
38
- - **Page Objects**: `LoginPage.login(...)` resolves automatically based on target platform.
39
- - **Actions**: `Spectra.verifyOtp(...)` provides typed action access.
38
+ - **Page Objects**: `LoginPage.login(...)` and getters `LoginPage.flashAlert` resolve automatically based on target platform.
39
+ - **Actions (`Spectra.*`)**: Built-in 16 atomic actions (`Spectra.navigate`, `Spectra.click`, `Spectra.type`, `Spectra.select`, `Spectra.scroll`, `Spectra.swipe`, `Spectra.wait`, etc.) matching TestSpectra database schema, merged seamlessly with user-defined custom actions (`Spectra.verifyOtp`).
40
+ - **Direct Callable Assertions**: Instant, auto-completed assertion methods available directly on `WebdriverIO.Element`, `ChainablePromiseElement`, and `browser` (`shouldBeVisible()`, `shouldContainText()`, `shouldHaveValue()`, `shouldHaveUrl()`, etc.).
40
41
  - **Steps**: `Step.loginUser(...)` exposes shared business step flows.
41
42
  - **Fixtures**: `Fixture.userData` provides typed, platform-agnostic test fixture access.
42
43
  - **WebdriverIO Globals**: `$`, `$$`, `browser`, `expect` are available globally across all platforms.
43
44
 
44
- ### B. Platform Hierarchy & Resolution
45
+ ### B. Built-in Regular Actions & Direct Callable Assertions
46
+
47
+ TestSpectra provides a bundled, 100% database-schema-compatible library of built-in atomic actions and direct element assertions:
48
+
49
+ #### 1. Direct Callable Element Assertions (`should*`)
50
+ All Page Object elements and locators support direct callable assertions without extra nesting:
51
+ ```typescript
52
+ it("should authenticate user using Page Objects and assertions", async () => {
53
+ // Navigation & high-level business flows
54
+ await LoginPage.open();
55
+ await Step.loginUser("tomsmith", "SuperSecretPassword!");
56
+
57
+ // Direct assertions on Page Object elements
58
+ await LoginPage.flashAlert.shouldBeVisible();
59
+ await LoginPage.flashAlert.shouldContainText("You logged into a secure area!");
60
+ await LoginPage.usernameInput.shouldHaveValue("tomsmith");
61
+ });
62
+ ```
63
+
64
+ #### Available Assertion Methods:
65
+ | Kategori | Method Callable |
66
+ | :--- | :--- |
67
+ | **Visibility** | `await el.shouldBeVisible()` / `await el.shouldNotBeVisible()` |
68
+ | **DOM Existence** | `await el.shouldExist()` / `await el.shouldNotExist()` |
69
+ | **Element State** | `await el.shouldBeEnabled()` / `await el.shouldBeDisabled()` / `await el.shouldBeSelected()` / `await el.shouldBeChecked()` |
70
+ | **Text Matching** | `await el.shouldHaveText(expected)` / `await el.shouldContainText(expected)` |
71
+ | **Input Value** | `await el.shouldHaveValue(expected)` / `await el.shouldContainValue(expected)` |
72
+ | **CSS Class** | `await el.shouldHaveClass(className)` |
73
+ | **Attribute** | `await el.shouldHaveAttribute(name, val?)` |
74
+ | **Browser URL / Title**| `await browser.shouldHaveUrl(url)` / `await browser.shouldHaveTitle(title)` |
75
+
76
+ #### 2. Built-in Atomic Actions (`Spectra.*`)
77
+ Available inside Page Objects, Actions, and Steps for raw element manipulation:
78
+ ```typescript
79
+ // Navigation
80
+ await Spectra.navigate("https://example.com/login");
81
+ await Spectra.back();
82
+ await Spectra.refresh();
83
+
84
+ // Interactions with Page Object getters or locators
85
+ await Spectra.type(this.usernameInput, "tomsmith");
86
+ await Spectra.click(this.submitButton);
87
+ await Spectra.select(this.countryDropdown, "ID");
88
+ await Spectra.clear(this.searchField);
89
+ await Spectra.pressKey("Enter");
90
+
91
+ // Gestures & Timing
92
+ await Spectra.scroll({ direction: "down", pixels: 300 });
93
+ await Spectra.swipe({ direction: "up", distance: 500 });
94
+ await Spectra.wait(1000);
95
+ await Spectra.waitForElement(this.flashAlert, 5000);
96
+ ```
97
+
98
+ ### C. Platform Hierarchy & Resolution
45
99
  Entities are structured with hierarchical platform stems:
46
100
  ```
47
101
  page-objects/
@@ -63,7 +117,7 @@ Resolution order:
63
117
  - **`mobile`**: `mobile` → `common`
64
118
  - **`common`**: `common`
65
119
 
66
- ### C. Solution-Style TypeScript Configuration & Language Service Plugin
120
+ ### D. Solution-Style TypeScript Configuration & Language Service Plugin
67
121
  To avoid ambient type clashes between platforms (such as different method signatures across Web and Mobile Page Objects), TestSpectra uses standard TypeScript **Project References**:
68
122
  - **`tsconfig.json`**: Root project reference orchestrator with `@testspectra/cli` plugin registered.
69
123
  - **`tsconfig.web.json`**: Scoped exclusively to Web files and `.testspectra/types/web.d.ts`.
@@ -81,7 +135,7 @@ Project verification is executed with standard `tsc`:
81
135
  tsc -b
82
136
  ```
83
137
 
84
- ### D. Typed Configuration with `defineConfig`
138
+ ### E. Typed Configuration with `defineConfig`
85
139
  Configuration is strongly typed with full JSDoc documentation via `spectra.config.ts`:
86
140
  ```typescript
87
141
  import { defineConfig } from "@testspectra/cli";
@@ -283,14 +283,18 @@ export default defineConfig({
283
283
  return $('button[type="submit"]');
284
284
  }
285
285
 
286
+ static get flashAlert(): ChainablePromiseElement {
287
+ return $('#flash');
288
+ }
289
+
286
290
  static async open() {
287
- await browser.url('/login');
291
+ await Spectra.navigate('/login');
288
292
  }
289
293
 
290
294
  static async login(username: string, pass: string) {
291
- await this.usernameInput.setValue(username);
292
- await this.passwordInput.setValue(pass);
293
- await this.submitButton.click();
295
+ await Spectra.type(this.usernameInput, username);
296
+ await Spectra.type(this.passwordInput, pass);
297
+ await Spectra.click(this.submitButton);
294
298
  }
295
299
  }
296
300
  `;
@@ -308,14 +312,18 @@ export default defineConfig({
308
312
  return $('~login_button');
309
313
  }
310
314
 
315
+ static get welcomeText(): ChainablePromiseElement {
316
+ return $('~welcome_text');
317
+ }
318
+
311
319
  static async open() {
312
320
  // Mobile app startup
313
321
  }
314
322
 
315
323
  static async login(username: string, pass: string) {
316
- await this.usernameInput.setValue(username);
317
- await this.passwordInput.setValue(pass);
318
- await this.submitButton.click();
324
+ await Spectra.type(this.usernameInput, username);
325
+ await Spectra.type(this.passwordInput, pass);
326
+ await Spectra.click(this.submitButton);
319
327
  }
320
328
  }
321
329
  `;
@@ -366,32 +374,33 @@ export default defineConfig({
366
374
  }
367
375
  `;
368
376
  fs.writeFileSync(path.join(cwd, "hooks/default/before.ios.hook.ts"), hookIos, "utf-8");
369
- // 10. Scaffold Specs (Pure test scripts, ZERO triple-slash lines)
370
- const specWeb = `it("should authenticate user using web locators and fixtures", async () => {
377
+ // 10. Scaffold Specs (Pure test scripts, ZERO triple-slash lines, ZERO raw selectors)
378
+ const specWeb = `it("should authenticate user using Page Objects and Spectra assertions", async () => {
371
379
  await browser.intercept("/api/status", "GET", Fixture.userData);
380
+
381
+ // 1. Navigation via Page Object
372
382
  await LoginPage.open();
373
- await LoginPage.login("tomsmith", "SuperSecretPassword!");
374
383
 
375
- const flash = await $("#flash");
376
- await expect(flash).toBeDisplayed();
384
+ // 2. High-level business flow via Shared Step & Page Object
385
+ await Step.loginUser("tomsmith", "SuperSecretPassword!");
386
+
387
+ // 3. Direct callable assertion on Page Object element
388
+ await LoginPage.flashAlert.shouldBeVisible();
389
+ await LoginPage.flashAlert.shouldContainText("You logged into a secure area!");
377
390
  });
378
391
  `;
379
392
  fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/web.test.ts"), specWeb, "utf-8");
380
- const specAndroid = `it("should authenticate user on Android Appium device", async () => {
381
- await LoginPage.open();
382
- await LoginPage.login("tomsmith", "SuperSecretPassword!");
393
+ const specAndroid = `it("should authenticate user on Android Appium device using Page Objects", async () => {
394
+ await Step.loginUser("tomsmith", "SuperSecretPassword!");
383
395
 
384
- const welcome = await $("~welcome_text");
385
- await expect(welcome).toBeDisplayed();
396
+ await LoginPage.welcomeText.shouldBeVisible();
386
397
  });
387
398
  `;
388
399
  fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/android.test.ts"), specAndroid, "utf-8");
389
- const specIos = `it("should authenticate user on iOS Appium device", async () => {
390
- await LoginPage.open();
391
- await LoginPage.login("tomsmith", "SuperSecretPassword!");
400
+ const specIos = `it("should authenticate user on iOS Appium device using Page Objects", async () => {
401
+ await Step.loginUser("tomsmith", "SuperSecretPassword!");
392
402
 
393
- const welcome = await $("~welcome_text");
394
- await expect(welcome).toBeDisplayed();
403
+ await LoginPage.welcomeText.shouldBeVisible();
395
404
  });
396
405
  `;
397
406
  fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/ios.test.ts"), specIos, "utf-8");
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Command } from "commander";
2
2
  export * from "./config/schema.js";
3
3
  export * from "./config/loader.js";
4
4
  export * from "./types/generator.js";
5
+ export * from "./step/index.js";
5
6
  export { init as initTsPlugin, default as tsPlugin } from "./plugin.js";
6
7
  export declare function createCliProgram(): Command;
7
8
  export declare function runCli(args?: string[]): Promise<void>;
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { runCommand } from "./commands/run.js";
6
6
  export * from "./config/schema.js";
7
7
  export * from "./config/loader.js";
8
8
  export * from "./types/generator.js";
9
+ export * from "./step/index.js";
9
10
  export { init as initTsPlugin, default as tsPlugin } from "./plugin.js";
10
11
  export function createCliProgram() {
11
12
  const program = new Command();
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./matchers.js";
3
+ export * from "./runner/single.js";
4
+ export * from "./runner/collection.js";
5
+ export * from "./spectra.js";
6
+ export * from "./proto.js";
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./matchers.js";
3
+ export * from "./runner/single.js";
4
+ export * from "./runner/collection.js";
5
+ export * from "./spectra.js";
6
+ export * from "./proto.js";
@@ -0,0 +1,14 @@
1
+ import { SingleElementMatcher, MultiElementMatcher, ElementTarget } from "./types.js";
2
+ /**
3
+ * Resolves an ElementTarget (selector string, WebdriverIO.Element, or ChainablePromiseElement)
4
+ * to an actionable WebdriverIO.Element.
5
+ */
6
+ export declare function resolveElement(target: ElementTarget): Promise<WebdriverIO.Element>;
7
+ /**
8
+ * Executes a single element matcher against the WebdriverIO browser / element context.
9
+ */
10
+ export declare function executeSingleMatcher(target: ElementTarget | undefined, matcher: SingleElementMatcher, args: any[]): Promise<void>;
11
+ /**
12
+ * Executes a multi-element collection matcher against WebdriverIO $$.
13
+ */
14
+ export declare function executeMultiMatcher(selector: string, matcher: MultiElementMatcher, args: any[]): Promise<void>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Resolves an ElementTarget (selector string, WebdriverIO.Element, or ChainablePromiseElement)
3
+ * to an actionable WebdriverIO.Element.
4
+ */
5
+ export async function resolveElement(target) {
6
+ if (typeof target === "string") {
7
+ const el = await $(target);
8
+ return el;
9
+ }
10
+ const el = await target;
11
+ return el;
12
+ }
13
+ /**
14
+ * Executes a single element matcher against the WebdriverIO browser / element context.
15
+ */
16
+ export async function executeSingleMatcher(target, matcher, args) {
17
+ const isBrowserLevel = matcher === "have.url" ||
18
+ matcher === "contain.url" ||
19
+ matcher === "have.title" ||
20
+ matcher === "contain.title";
21
+ if (isBrowserLevel) {
22
+ const expected = args[0];
23
+ switch (matcher) {
24
+ case "have.url":
25
+ await expect(browser).toHaveUrl(expected);
26
+ break;
27
+ case "contain.url":
28
+ await expect(browser).toHaveUrl(expect.stringContaining(expected));
29
+ break;
30
+ case "have.title":
31
+ await expect(browser).toHaveTitle(expected);
32
+ break;
33
+ case "contain.title":
34
+ await expect(browser).toHaveTitle(expect.stringContaining(expected));
35
+ break;
36
+ }
37
+ return;
38
+ }
39
+ if (!target) {
40
+ throw new Error(`Assertion matcher '${matcher}' requires an element target.`);
41
+ }
42
+ const el = await resolveElement(target);
43
+ switch (matcher) {
44
+ case "be.visible":
45
+ await expect(el).toBeDisplayed();
46
+ break;
47
+ case "not.be.visible":
48
+ await expect(el).not.toBeDisplayed();
49
+ break;
50
+ case "exist":
51
+ await expect(el).toExist();
52
+ break;
53
+ case "not.exist":
54
+ await expect(el).not.toExist();
55
+ break;
56
+ case "be.enabled":
57
+ await expect(el).toBeEnabled();
58
+ break;
59
+ case "be.disabled":
60
+ await expect(el).toBeDisabled();
61
+ break;
62
+ case "be.checked":
63
+ case "be.selected":
64
+ await expect(el).toBeSelected();
65
+ break;
66
+ case "have.value":
67
+ await expect(el).toHaveValue(args[0]);
68
+ break;
69
+ case "contain.value":
70
+ await expect(el).toHaveValue(expect.stringContaining(args[0]));
71
+ break;
72
+ case "have.text":
73
+ await expect(el).toHaveText(args[0]);
74
+ break;
75
+ case "contain.text":
76
+ await expect(el).toHaveText(expect.stringContaining(args[0]));
77
+ break;
78
+ case "have.class":
79
+ await expect(el).toHaveElementClass(args[0]);
80
+ break;
81
+ case "have.attr":
82
+ if (args.length >= 2) {
83
+ await expect(el).toHaveAttribute(args[0], args[1]);
84
+ }
85
+ else {
86
+ await expect(el).toHaveAttribute(args[0]);
87
+ }
88
+ break;
89
+ default:
90
+ throw new Error(`Unsupported matcher: ${matcher}`);
91
+ }
92
+ }
93
+ /**
94
+ * Executes a multi-element collection matcher against WebdriverIO $$.
95
+ */
96
+ export async function executeMultiMatcher(selector, matcher, args) {
97
+ const elements = await $$(selector);
98
+ switch (matcher) {
99
+ case "have.length":
100
+ await expect(elements).toBeElementsArrayOfSize(args[0]);
101
+ break;
102
+ case "have.length.greaterThan":
103
+ await expect(elements).toBeElementsArrayOfSize({ gte: args[0] + 1 });
104
+ break;
105
+ case "be.empty":
106
+ await expect(elements).toBeElementsArrayOfSize(0);
107
+ break;
108
+ case "exist":
109
+ await expect(elements).toBeElementsArrayOfSize({ gte: 1 });
110
+ break;
111
+ default:
112
+ throw new Error(`Unsupported multi-element matcher: ${matcher}`);
113
+ }
114
+ }
@@ -0,0 +1,46 @@
1
+ declare global {
2
+ namespace WebdriverIO {
3
+ interface Element {
4
+ shouldBeVisible(): Promise<Element>;
5
+ shouldNotBeVisible(): Promise<Element>;
6
+ shouldExist(): Promise<Element>;
7
+ shouldNotExist(): Promise<Element>;
8
+ shouldBeEnabled(): Promise<Element>;
9
+ shouldBeDisabled(): Promise<Element>;
10
+ shouldBeSelected(): Promise<Element>;
11
+ shouldBeChecked(): Promise<Element>;
12
+ shouldHaveValue(expectedValue: string): Promise<Element>;
13
+ shouldContainValue(expectedValue: string): Promise<Element>;
14
+ shouldHaveText(expectedText: string): Promise<Element>;
15
+ shouldContainText(expectedText: string): Promise<Element>;
16
+ shouldHaveClass(className: string): Promise<Element>;
17
+ shouldHaveAttribute(attributeName: string, expectedValue?: string): Promise<Element>;
18
+ }
19
+ interface Browser {
20
+ shouldHaveUrl(expectedUrl: string): Promise<void>;
21
+ shouldContainUrl(expectedUrl: string): Promise<void>;
22
+ shouldHaveTitle(expectedTitle: string): Promise<void>;
23
+ shouldContainTitle(expectedTitle: string): Promise<void>;
24
+ }
25
+ }
26
+ interface Promise<T> {
27
+ shouldBeVisible(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;
28
+ shouldNotBeVisible(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;
29
+ shouldExist(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;
30
+ shouldNotExist(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;
31
+ shouldBeEnabled(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;
32
+ shouldBeDisabled(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;
33
+ shouldBeSelected(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;
34
+ shouldBeChecked(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;
35
+ shouldHaveValue(this: Promise<WebdriverIO.Element>, expectedValue: string): Promise<WebdriverIO.Element>;
36
+ shouldContainValue(this: Promise<WebdriverIO.Element>, expectedValue: string): Promise<WebdriverIO.Element>;
37
+ shouldHaveText(this: Promise<WebdriverIO.Element>, expectedText: string): Promise<WebdriverIO.Element>;
38
+ shouldContainText(this: Promise<WebdriverIO.Element>, expectedText: string): Promise<WebdriverIO.Element>;
39
+ shouldHaveClass(this: Promise<WebdriverIO.Element>, className: string): Promise<WebdriverIO.Element>;
40
+ shouldHaveAttribute(this: Promise<WebdriverIO.Element>, attributeName: string, expectedValue?: string): Promise<WebdriverIO.Element>;
41
+ }
42
+ }
43
+ /**
44
+ * Install direct callable assertion methods onto WebdriverIO browser/element and Promise prototypes.
45
+ */
46
+ export declare function installPrototypes(): void;
@@ -0,0 +1,114 @@
1
+ import { resolveElement } from "./matchers.js";
2
+ /**
3
+ * Install direct callable assertion methods onto WebdriverIO browser/element and Promise prototypes.
4
+ */
5
+ export function installPrototypes() {
6
+ const elementCommands = {
7
+ async shouldBeVisible() {
8
+ const el = await resolveElement(this);
9
+ await expect(el).toBeDisplayed();
10
+ return el;
11
+ },
12
+ async shouldNotBeVisible() {
13
+ const el = await resolveElement(this);
14
+ await expect(el).not.toBeDisplayed();
15
+ return el;
16
+ },
17
+ async shouldExist() {
18
+ const el = await resolveElement(this);
19
+ await expect(el).toExist();
20
+ return el;
21
+ },
22
+ async shouldNotExist() {
23
+ const el = await resolveElement(this);
24
+ await expect(el).not.toExist();
25
+ return el;
26
+ },
27
+ async shouldBeEnabled() {
28
+ const el = await resolveElement(this);
29
+ await expect(el).toBeEnabled();
30
+ return el;
31
+ },
32
+ async shouldBeDisabled() {
33
+ const el = await resolveElement(this);
34
+ await expect(el).toBeDisabled();
35
+ return el;
36
+ },
37
+ async shouldBeSelected() {
38
+ const el = await resolveElement(this);
39
+ await expect(el).toBeSelected();
40
+ return el;
41
+ },
42
+ async shouldBeChecked() {
43
+ const el = await resolveElement(this);
44
+ await expect(el).toBeSelected();
45
+ return el;
46
+ },
47
+ async shouldHaveValue(val) {
48
+ const el = await resolveElement(this);
49
+ await expect(el).toHaveValue(val);
50
+ return el;
51
+ },
52
+ async shouldContainValue(val) {
53
+ const el = await resolveElement(this);
54
+ await expect(el).toHaveValue(expect.stringContaining(val));
55
+ return el;
56
+ },
57
+ async shouldHaveText(text) {
58
+ const el = await resolveElement(this);
59
+ await expect(el).toHaveText(text);
60
+ return el;
61
+ },
62
+ async shouldContainText(text) {
63
+ const el = await resolveElement(this);
64
+ await expect(el).toHaveText(expect.stringContaining(text));
65
+ return el;
66
+ },
67
+ async shouldHaveClass(cls) {
68
+ const el = await resolveElement(this);
69
+ await expect(el).toHaveElementClass(cls);
70
+ return el;
71
+ },
72
+ async shouldHaveAttribute(name, val) {
73
+ const el = await resolveElement(this);
74
+ if (val !== undefined) {
75
+ await expect(el).toHaveAttribute(name, val);
76
+ }
77
+ else {
78
+ await expect(el).toHaveAttribute(name);
79
+ }
80
+ return el;
81
+ },
82
+ };
83
+ const browserCommands = {
84
+ async shouldHaveUrl(url) {
85
+ await expect(browser).toHaveUrl(url);
86
+ },
87
+ async shouldContainUrl(url) {
88
+ await expect(browser).toHaveUrl(expect.stringContaining(url));
89
+ },
90
+ async shouldHaveTitle(title) {
91
+ await expect(browser).toHaveTitle(title);
92
+ },
93
+ async shouldContainTitle(title) {
94
+ await expect(browser).toHaveTitle(expect.stringContaining(title));
95
+ },
96
+ };
97
+ if (typeof browser !== "undefined" && browser.addCommand) {
98
+ for (const [name, fn] of Object.entries(elementCommands)) {
99
+ browser.addCommand(name, fn, true);
100
+ }
101
+ for (const [name, fn] of Object.entries(browserCommands)) {
102
+ browser.addCommand(name, fn, false);
103
+ }
104
+ }
105
+ // Attach to Promise.prototype for ChainablePromiseElement unwrapping
106
+ for (const [name, fn] of Object.entries(elementCommands)) {
107
+ if (typeof Promise.prototype[name] !== "function") {
108
+ Promise.prototype[name] = async function (...args) {
109
+ const el = await this;
110
+ return await fn.call(el, ...args);
111
+ };
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,13 @@
1
+ import { MultiElementMatcher } from "../types.js";
2
+ import { SingleElementRunner } from "./single.js";
3
+ export declare class MultiElementRunner implements PromiseLike<void> {
4
+ private _selector;
5
+ private _assertions;
6
+ constructor(selector: string);
7
+ should(matcher: MultiElementMatcher, ...args: any[]): this;
8
+ eq(index: number): SingleElementRunner;
9
+ first(): SingleElementRunner;
10
+ last(): SingleElementRunner;
11
+ each(callback: (el: SingleElementRunner, index: number) => Promise<void>): Promise<void>;
12
+ then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
13
+ }
@@ -0,0 +1,51 @@
1
+ import { executeMultiMatcher } from "../matchers.js";
2
+ import { SingleElementRunner } from "./single.js";
3
+ export class MultiElementRunner {
4
+ _selector;
5
+ _assertions = [];
6
+ constructor(selector) {
7
+ this._selector = selector;
8
+ }
9
+ // --- Collection Assertions ---
10
+ should(matcher, ...args) {
11
+ this._assertions.push({ matcher, args });
12
+ return this;
13
+ }
14
+ // --- Item Navigation ---
15
+ eq(index) {
16
+ const indexedSelector = `(${this._selector})[${index + 1}]`;
17
+ return new SingleElementRunner(indexedSelector);
18
+ }
19
+ first() {
20
+ return this.eq(0);
21
+ }
22
+ last() {
23
+ const indexedSelector = `(${this._selector})[last()]`;
24
+ return new SingleElementRunner(indexedSelector);
25
+ }
26
+ // --- Iteration Callback ---
27
+ async each(callback) {
28
+ const elements = await $$(this._selector);
29
+ const count = await elements.length;
30
+ for (let i = 0; i < count; i++) {
31
+ const runner = this.eq(i);
32
+ await callback(runner, i);
33
+ }
34
+ }
35
+ // --- Execution when awaited ---
36
+ async then(onfulfilled, onrejected) {
37
+ try {
38
+ for (const assert of this._assertions) {
39
+ await executeMultiMatcher(this._selector, assert.matcher, assert.args);
40
+ }
41
+ const res = (onfulfilled ? await onfulfilled() : undefined);
42
+ return res;
43
+ }
44
+ catch (err) {
45
+ if (onrejected) {
46
+ return onrejected(err);
47
+ }
48
+ throw err;
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,21 @@
1
+ import { SingleElementMatcher, ElementTarget } from "../types.js";
2
+ export declare class SingleElementRunner implements PromiseLike<void> {
3
+ private _target;
4
+ private _action;
5
+ private _assertions;
6
+ constructor(target?: ElementTarget, action?: () => Promise<any>);
7
+ click(): this;
8
+ doubleClick(): this;
9
+ longPress(durationMs?: number): this;
10
+ type(value: string, options?: {
11
+ clearFirst?: boolean;
12
+ }): this;
13
+ clear(): this;
14
+ select(value: string): this;
15
+ hover(): this;
16
+ waitForElement(timeoutMs?: number): this;
17
+ should(matcher: SingleElementMatcher, ...args: any[]): this;
18
+ should(target: ElementTarget, matcher: SingleElementMatcher, ...args: any[]): this;
19
+ then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
20
+ private requireTarget;
21
+ }