@testspectra/cli 1.0.4 → 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 +61 -7
- package/dist/commands/init.js +31 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/step/index.d.ts +6 -0
- package/dist/step/index.js +6 -0
- package/dist/step/matchers.d.ts +14 -0
- package/dist/step/matchers.js +114 -0
- package/dist/step/proto.d.ts +46 -0
- package/dist/step/proto.js +114 -0
- package/dist/step/runner/collection.d.ts +13 -0
- package/dist/step/runner/collection.js +51 -0
- package/dist/step/runner/single.d.ts +21 -0
- package/dist/step/runner/single.js +138 -0
- package/dist/step/spectra.d.ts +34 -0
- package/dist/step/spectra.js +156 -0
- package/dist/step/types.d.ts +32 -0
- package/dist/step/types.js +5 -0
- package/dist/types/generator.js +44 -6
- package/package.json +1 -1
- package/src/commands/init.ts +31 -22
- package/src/index.ts +1 -0
- package/src/step/index.ts +6 -0
- package/src/step/matchers.ts +131 -0
- package/src/step/proto.ts +163 -0
- package/src/step/runner/collection.ts +73 -0
- package/src/step/runner/single.ts +166 -0
- package/src/step/spectra.ts +185 -0
- package/src/step/types.ts +113 -0
- package/src/types/generator.ts +44 -6
- package/testspectra-cli-1.0.5.tgz +0 -0
- package/testspectra-cli-1.0.6.tgz +0 -0
- package/tsconfig.json +1 -0
- package/testspectra-cli-1.0.3.tgz +0 -0
- package/testspectra-cli-1.0.4.tgz +0 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ The **TestSpectra CLI** (`spectra` / `testspectra`) is the command-line orchestr
|
|
|
16
16
|
│ │
|
|
17
17
|
▼ ▼
|
|
18
18
|
┌──────────────────────┐ ┌────────────────────────┐
|
|
19
|
-
│ spectra
|
|
19
|
+
│ spectra run / CLI │ │ Solution TSConfigs │
|
|
20
20
|
│ (CLI Orchestration) │ │ (tsconfig.*.json) │
|
|
21
21
|
└───────────┬──────────┘ └────────────┬───────────┘
|
|
22
22
|
│ │
|
|
@@ -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(...)`
|
|
39
|
-
- **Actions**: `Spectra.
|
|
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.
|
|
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
|
-
###
|
|
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`.
|
|
@@ -72,7 +126,7 @@ To avoid ambient type clashes between platforms (such as different method signat
|
|
|
72
126
|
- **`fixtures.d.ts`**: Single platform-agnostic declaration file referenced by all platform declarations.
|
|
73
127
|
|
|
74
128
|
#### Zero-Terminal Background Type Generation (TS Plugin)
|
|
75
|
-
With the built-in TypeScript Server plugin (`@testspectra/cli`),
|
|
129
|
+
With the built-in TypeScript Server plugin (`@testspectra/cli`), background type generation is completely automatic:
|
|
76
130
|
- When you open the project in **VS Code**, **Cursor**, or **WebStorm**, the IDE's TypeScript Server automatically boots the `@testspectra/cli` Language Service plugin.
|
|
77
131
|
- The plugin generates and updates `.testspectra/types/` ambient declarations in the background in real-time as you author Page Objects, Actions, Steps, and Fixtures.
|
|
78
132
|
|
|
@@ -81,7 +135,7 @@ Project verification is executed with standard `tsc`:
|
|
|
81
135
|
tsc -b
|
|
82
136
|
```
|
|
83
137
|
|
|
84
|
-
###
|
|
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";
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
291
|
+
await Spectra.navigate('/login');
|
|
288
292
|
}
|
|
289
293
|
|
|
290
294
|
static async login(username: string, pass: string) {
|
|
291
|
-
await this.usernameInput
|
|
292
|
-
await this.passwordInput
|
|
293
|
-
await this.submitButton
|
|
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
|
|
317
|
-
await this.passwordInput
|
|
318
|
-
await this.submitButton
|
|
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
|
|
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
|
-
|
|
376
|
-
await
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,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
|
+
}
|