@testspectra/cli 1.0.5 → 1.0.7
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 +59 -5
- package/dist/commands/init.js +67 -24
- 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 +69 -24
- 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.6.tgz +0 -0
- package/testspectra-cli-1.0.7.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
|
@@ -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`.
|
|
@@ -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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
3
4
|
import { ConfigLoader } from "../config/loader.js";
|
|
4
5
|
import { TypeGenerator } from "../types/generator.js";
|
|
5
6
|
export async function initCommand(options) {
|
|
@@ -92,6 +93,37 @@ export default defineConfig({
|
|
|
92
93
|
console.log(`\x1b[32m[TestSpectra]\x1b[0m Initialized spectra.config.ts with defineConfig`);
|
|
93
94
|
// 3. Create or update package.json with npm scripts & devDependencies
|
|
94
95
|
const packageJsonPath = path.join(cwd, "package.json");
|
|
96
|
+
// Dynamically resolve CLI version
|
|
97
|
+
let cliVersion = "^1.0.6";
|
|
98
|
+
try {
|
|
99
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
100
|
+
const __dirname = path.dirname(__filename);
|
|
101
|
+
const cliPackageJsonPath = path.resolve(__dirname, "../../package.json");
|
|
102
|
+
if (fs.existsSync(cliPackageJsonPath)) {
|
|
103
|
+
const cliPkg = JSON.parse(fs.readFileSync(cliPackageJsonPath, "utf-8"));
|
|
104
|
+
if (cliPkg.version) {
|
|
105
|
+
cliVersion = `^${cliPkg.version}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
// Check if cwd is inside the testspectra monorepo with a pnpm-workspace.yaml
|
|
111
|
+
let isInternalWorkspace = false;
|
|
112
|
+
let cur = cwd;
|
|
113
|
+
while (cur !== path.dirname(cur)) {
|
|
114
|
+
if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml"))) {
|
|
115
|
+
try {
|
|
116
|
+
const wsContent = fs.readFileSync(path.join(cur, "pnpm-workspace.yaml"), "utf-8");
|
|
117
|
+
if (wsContent.includes("cli")) {
|
|
118
|
+
isInternalWorkspace = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
}
|
|
124
|
+
cur = path.dirname(cur);
|
|
125
|
+
}
|
|
126
|
+
const cliDepVersion = isInternalWorkspace ? "workspace:*" : cliVersion;
|
|
95
127
|
let pkg = {
|
|
96
128
|
name: path.basename(cwd),
|
|
97
129
|
version: "1.0.0",
|
|
@@ -102,7 +134,7 @@ export default defineConfig({
|
|
|
102
134
|
"type-check": "tsc -b",
|
|
103
135
|
},
|
|
104
136
|
devDependencies: {
|
|
105
|
-
"@testspectra/cli":
|
|
137
|
+
"@testspectra/cli": cliDepVersion,
|
|
106
138
|
"@types/node": "^20.14.0",
|
|
107
139
|
"@wdio/globals": "^9.2.8",
|
|
108
140
|
"@wdio/mocha-framework": "^9.2.8",
|
|
@@ -122,7 +154,9 @@ export default defineConfig({
|
|
|
122
154
|
},
|
|
123
155
|
devDependencies: {
|
|
124
156
|
...(existing.devDependencies || {}),
|
|
125
|
-
"@testspectra/cli": existing.devDependencies?.["@testspectra/cli"]
|
|
157
|
+
"@testspectra/cli": existing.devDependencies?.["@testspectra/cli"]?.startsWith("workspace:") && !isInternalWorkspace
|
|
158
|
+
? cliVersion
|
|
159
|
+
: existing.devDependencies?.["@testspectra/cli"] || cliDepVersion,
|
|
126
160
|
"@types/node": existing.devDependencies?.["@types/node"] || "^20.14.0",
|
|
127
161
|
"@wdio/globals": existing.devDependencies?.["@wdio/globals"] || "^9.2.8",
|
|
128
162
|
"@wdio/mocha-framework": existing.devDependencies?.["@wdio/mocha-framework"] || "^9.2.8",
|
|
@@ -283,14 +317,18 @@ export default defineConfig({
|
|
|
283
317
|
return $('button[type="submit"]');
|
|
284
318
|
}
|
|
285
319
|
|
|
320
|
+
static get flashAlert(): ChainablePromiseElement {
|
|
321
|
+
return $('#flash');
|
|
322
|
+
}
|
|
323
|
+
|
|
286
324
|
static async open() {
|
|
287
|
-
await
|
|
325
|
+
await Spectra.navigate('/login');
|
|
288
326
|
}
|
|
289
327
|
|
|
290
328
|
static async login(username: string, pass: string) {
|
|
291
|
-
await this.usernameInput
|
|
292
|
-
await this.passwordInput
|
|
293
|
-
await this.submitButton
|
|
329
|
+
await Spectra.type(this.usernameInput, username);
|
|
330
|
+
await Spectra.type(this.passwordInput, pass);
|
|
331
|
+
await Spectra.click(this.submitButton);
|
|
294
332
|
}
|
|
295
333
|
}
|
|
296
334
|
`;
|
|
@@ -308,14 +346,18 @@ export default defineConfig({
|
|
|
308
346
|
return $('~login_button');
|
|
309
347
|
}
|
|
310
348
|
|
|
349
|
+
static get welcomeText(): ChainablePromiseElement {
|
|
350
|
+
return $('~welcome_text');
|
|
351
|
+
}
|
|
352
|
+
|
|
311
353
|
static async open() {
|
|
312
354
|
// Mobile app startup
|
|
313
355
|
}
|
|
314
356
|
|
|
315
357
|
static async login(username: string, pass: string) {
|
|
316
|
-
await this.usernameInput
|
|
317
|
-
await this.passwordInput
|
|
318
|
-
await this.submitButton
|
|
358
|
+
await Spectra.type(this.usernameInput, username);
|
|
359
|
+
await Spectra.type(this.passwordInput, pass);
|
|
360
|
+
await Spectra.click(this.submitButton);
|
|
319
361
|
}
|
|
320
362
|
}
|
|
321
363
|
`;
|
|
@@ -366,32 +408,33 @@ export default defineConfig({
|
|
|
366
408
|
}
|
|
367
409
|
`;
|
|
368
410
|
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
|
|
411
|
+
// 10. Scaffold Specs (Pure test scripts, ZERO triple-slash lines, ZERO raw selectors)
|
|
412
|
+
const specWeb = `it("should authenticate user using Page Objects and Spectra assertions", async () => {
|
|
371
413
|
await browser.intercept("/api/status", "GET", Fixture.userData);
|
|
414
|
+
|
|
415
|
+
// 1. Navigation via Page Object
|
|
372
416
|
await LoginPage.open();
|
|
373
|
-
await LoginPage.login("tomsmith", "SuperSecretPassword!");
|
|
374
417
|
|
|
375
|
-
|
|
376
|
-
await
|
|
418
|
+
// 2. High-level business flow via Shared Step & Page Object
|
|
419
|
+
await Step.loginUser("tomsmith", "SuperSecretPassword!");
|
|
420
|
+
|
|
421
|
+
// 3. Direct callable assertion on Page Object element
|
|
422
|
+
await LoginPage.flashAlert.shouldBeVisible();
|
|
423
|
+
await LoginPage.flashAlert.shouldContainText("You logged into a secure area!");
|
|
377
424
|
});
|
|
378
425
|
`;
|
|
379
426
|
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!");
|
|
427
|
+
const specAndroid = `it("should authenticate user on Android Appium device using Page Objects", async () => {
|
|
428
|
+
await Step.loginUser("tomsmith", "SuperSecretPassword!");
|
|
383
429
|
|
|
384
|
-
|
|
385
|
-
await expect(welcome).toBeDisplayed();
|
|
430
|
+
await LoginPage.welcomeText.shouldBeVisible();
|
|
386
431
|
});
|
|
387
432
|
`;
|
|
388
433
|
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!");
|
|
434
|
+
const specIos = `it("should authenticate user on iOS Appium device using Page Objects", async () => {
|
|
435
|
+
await Step.loginUser("tomsmith", "SuperSecretPassword!");
|
|
392
436
|
|
|
393
|
-
|
|
394
|
-
await expect(welcome).toBeDisplayed();
|
|
437
|
+
await LoginPage.welcomeText.shouldBeVisible();
|
|
395
438
|
});
|
|
396
439
|
`;
|
|
397
440
|
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
|
+
}
|