@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 +59 -5
- 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.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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { executeSingleMatcher, resolveElement } from "../matchers.js";
|
|
2
|
+
export class SingleElementRunner {
|
|
3
|
+
_target;
|
|
4
|
+
_action = null;
|
|
5
|
+
_assertions = [];
|
|
6
|
+
constructor(target, action) {
|
|
7
|
+
this._target = target;
|
|
8
|
+
this._action = action || null;
|
|
9
|
+
}
|
|
10
|
+
// --- Chained Actions on current target ---
|
|
11
|
+
click() {
|
|
12
|
+
const target = this.requireTarget("click");
|
|
13
|
+
this._action = async () => {
|
|
14
|
+
const el = await resolveElement(target);
|
|
15
|
+
await el.click();
|
|
16
|
+
};
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
doubleClick() {
|
|
20
|
+
const target = this.requireTarget("doubleClick");
|
|
21
|
+
this._action = async () => {
|
|
22
|
+
const el = await resolveElement(target);
|
|
23
|
+
await el.doubleClick();
|
|
24
|
+
};
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
longPress(durationMs = 1000) {
|
|
28
|
+
const target = this.requireTarget("longPress");
|
|
29
|
+
this._action = async () => {
|
|
30
|
+
const el = await resolveElement(target);
|
|
31
|
+
if (typeof el.touchAction === "function") {
|
|
32
|
+
await el.touchAction([
|
|
33
|
+
{ action: "longPress", ms: durationMs },
|
|
34
|
+
{ action: "release" },
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await el.click();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
type(value, options) {
|
|
44
|
+
const target = this.requireTarget("type");
|
|
45
|
+
this._action = async () => {
|
|
46
|
+
const el = await resolveElement(target);
|
|
47
|
+
if (options?.clearFirst) {
|
|
48
|
+
await el.clearValue();
|
|
49
|
+
}
|
|
50
|
+
await el.setValue(value);
|
|
51
|
+
};
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
clear() {
|
|
55
|
+
const target = this.requireTarget("clear");
|
|
56
|
+
this._action = async () => {
|
|
57
|
+
const el = await resolveElement(target);
|
|
58
|
+
await el.clearValue();
|
|
59
|
+
};
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
select(value) {
|
|
63
|
+
const target = this.requireTarget("select");
|
|
64
|
+
this._action = async () => {
|
|
65
|
+
const el = await resolveElement(target);
|
|
66
|
+
await el.selectByVisibleText(value);
|
|
67
|
+
};
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
hover() {
|
|
71
|
+
const target = this.requireTarget("hover");
|
|
72
|
+
this._action = async () => {
|
|
73
|
+
const el = await resolveElement(target);
|
|
74
|
+
await el.moveTo();
|
|
75
|
+
};
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
waitForElement(timeoutMs = 10000) {
|
|
79
|
+
const target = this.requireTarget("waitForElement");
|
|
80
|
+
this._action = async () => {
|
|
81
|
+
const el = await resolveElement(target);
|
|
82
|
+
await el.waitForDisplayed({ timeout: timeoutMs });
|
|
83
|
+
};
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
should(a, b, ...rest) {
|
|
87
|
+
const isFirstArgMatcher = typeof a === "string" &&
|
|
88
|
+
(a.includes(".") ||
|
|
89
|
+
a.includes("exist") ||
|
|
90
|
+
a.includes("visible") ||
|
|
91
|
+
a.includes("enabled") ||
|
|
92
|
+
a.includes("disabled") ||
|
|
93
|
+
a.includes("selected") ||
|
|
94
|
+
a.includes("checked"));
|
|
95
|
+
if (b !== undefined && !isFirstArgMatcher) {
|
|
96
|
+
// Form: should(target, matcher, ...args)
|
|
97
|
+
this._assertions.push({
|
|
98
|
+
target: a,
|
|
99
|
+
matcher: b,
|
|
100
|
+
args: rest,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Form: should(matcher, ...args) -> on current target
|
|
105
|
+
this._assertions.push({
|
|
106
|
+
target: this._target,
|
|
107
|
+
matcher: a,
|
|
108
|
+
args: b !== undefined ? [b, ...rest] : rest,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
// --- Execution when awaited ---
|
|
114
|
+
async then(onfulfilled, onrejected) {
|
|
115
|
+
try {
|
|
116
|
+
if (this._action) {
|
|
117
|
+
await this._action();
|
|
118
|
+
}
|
|
119
|
+
for (const assert of this._assertions) {
|
|
120
|
+
await executeSingleMatcher(assert.target, assert.matcher, assert.args);
|
|
121
|
+
}
|
|
122
|
+
const res = (onfulfilled ? await onfulfilled() : undefined);
|
|
123
|
+
return res;
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (onrejected) {
|
|
127
|
+
return onrejected(err);
|
|
128
|
+
}
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
requireTarget(actionName) {
|
|
133
|
+
if (!this._target) {
|
|
134
|
+
throw new Error(`Cannot execute '${actionName}' without specifying an element target.`);
|
|
135
|
+
}
|
|
136
|
+
return this._target;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ClickOptions, ElementTarget, KeyOption, LongPressOptions, ScrollOptions, SwipeOptions, TypeOptions } from "./types.js";
|
|
2
|
+
import { SingleElementRunner } from "./runner/single.js";
|
|
3
|
+
import { MultiElementRunner } from "./runner/collection.js";
|
|
4
|
+
/**
|
|
5
|
+
* Main Spectra testing API orchestrator.
|
|
6
|
+
* Combines direct actions, single element queries (get), and collections (getAll).
|
|
7
|
+
*/
|
|
8
|
+
export declare class SpectraStatic {
|
|
9
|
+
/**
|
|
10
|
+
* Targets a single element by selector or Page Object element.
|
|
11
|
+
*/
|
|
12
|
+
get(target: ElementTarget): SingleElementRunner;
|
|
13
|
+
/**
|
|
14
|
+
* Targets multiple elements / collections by selector.
|
|
15
|
+
*/
|
|
16
|
+
getAll(selector: string): MultiElementRunner;
|
|
17
|
+
navigate(url: string): SingleElementRunner;
|
|
18
|
+
back(): SingleElementRunner;
|
|
19
|
+
refresh(): SingleElementRunner;
|
|
20
|
+
click(target: ElementTarget, textOrOptions?: string | ClickOptions): SingleElementRunner;
|
|
21
|
+
doubleClick(target: ElementTarget, textOrOptions?: string | ClickOptions): SingleElementRunner;
|
|
22
|
+
longPress(target: ElementTarget, options?: LongPressOptions | number): SingleElementRunner;
|
|
23
|
+
type(target: ElementTarget, value: string, options?: TypeOptions): SingleElementRunner;
|
|
24
|
+
clear(target: ElementTarget): SingleElementRunner;
|
|
25
|
+
select(target: ElementTarget, value: string): SingleElementRunner;
|
|
26
|
+
hover(target: ElementTarget): SingleElementRunner;
|
|
27
|
+
pressKey(key: KeyOption | string): SingleElementRunner;
|
|
28
|
+
dragDrop(sourceTarget: ElementTarget, destTarget: ElementTarget): SingleElementRunner;
|
|
29
|
+
scroll(options?: ScrollOptions): SingleElementRunner;
|
|
30
|
+
swipe(options: SwipeOptions): SingleElementRunner;
|
|
31
|
+
wait(durationMs: number): SingleElementRunner;
|
|
32
|
+
waitForElement(target: ElementTarget, timeoutMs?: number): SingleElementRunner;
|
|
33
|
+
}
|
|
34
|
+
export declare const Spectra: SpectraStatic;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { SingleElementRunner } from "./runner/single.js";
|
|
2
|
+
import { MultiElementRunner } from "./runner/collection.js";
|
|
3
|
+
import { resolveElement } from "./matchers.js";
|
|
4
|
+
/**
|
|
5
|
+
* Main Spectra testing API orchestrator.
|
|
6
|
+
* Combines direct actions, single element queries (get), and collections (getAll).
|
|
7
|
+
*/
|
|
8
|
+
export class SpectraStatic {
|
|
9
|
+
/**
|
|
10
|
+
* Targets a single element by selector or Page Object element.
|
|
11
|
+
*/
|
|
12
|
+
get(target) {
|
|
13
|
+
return new SingleElementRunner(target);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Targets multiple elements / collections by selector.
|
|
17
|
+
*/
|
|
18
|
+
getAll(selector) {
|
|
19
|
+
return new MultiElementRunner(selector);
|
|
20
|
+
}
|
|
21
|
+
// --- Browser & Navigation Actions ---
|
|
22
|
+
navigate(url) {
|
|
23
|
+
return new SingleElementRunner(undefined, async () => {
|
|
24
|
+
await browser.url(url);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
back() {
|
|
28
|
+
return new SingleElementRunner(undefined, async () => {
|
|
29
|
+
await browser.back();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
refresh() {
|
|
33
|
+
return new SingleElementRunner(undefined, async () => {
|
|
34
|
+
await browser.refresh();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// --- Direct Interaction Actions ---
|
|
38
|
+
click(target, textOrOptions) {
|
|
39
|
+
return new SingleElementRunner(target, async () => {
|
|
40
|
+
const el = await resolveElement(target);
|
|
41
|
+
await el.click();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
doubleClick(target, textOrOptions) {
|
|
45
|
+
return new SingleElementRunner(target, async () => {
|
|
46
|
+
const el = await resolveElement(target);
|
|
47
|
+
await el.doubleClick();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
longPress(target, options) {
|
|
51
|
+
const duration = typeof options === "number" ? options : options?.duration || 1000;
|
|
52
|
+
return new SingleElementRunner(target, async () => {
|
|
53
|
+
const el = await resolveElement(target);
|
|
54
|
+
if (typeof el.touchAction === "function") {
|
|
55
|
+
await el.touchAction([
|
|
56
|
+
{ action: "longPress", ms: duration },
|
|
57
|
+
{ action: "release" },
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await el.click();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
type(target, value, options) {
|
|
66
|
+
return new SingleElementRunner(target, async () => {
|
|
67
|
+
const el = await resolveElement(target);
|
|
68
|
+
if (options?.clearFirst) {
|
|
69
|
+
await el.clearValue();
|
|
70
|
+
}
|
|
71
|
+
await el.setValue(value);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
clear(target) {
|
|
75
|
+
return new SingleElementRunner(target, async () => {
|
|
76
|
+
const el = await resolveElement(target);
|
|
77
|
+
await el.clearValue();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
select(target, value) {
|
|
81
|
+
return new SingleElementRunner(target, async () => {
|
|
82
|
+
const el = await resolveElement(target);
|
|
83
|
+
await el.selectByVisibleText(value);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
hover(target) {
|
|
87
|
+
return new SingleElementRunner(target, async () => {
|
|
88
|
+
const el = await resolveElement(target);
|
|
89
|
+
await el.moveTo();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
pressKey(key) {
|
|
93
|
+
return new SingleElementRunner(undefined, async () => {
|
|
94
|
+
await browser.keys(key);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
dragDrop(sourceTarget, destTarget) {
|
|
98
|
+
return new SingleElementRunner(sourceTarget, async () => {
|
|
99
|
+
const source = await resolveElement(sourceTarget);
|
|
100
|
+
const target = await resolveElement(destTarget);
|
|
101
|
+
await source.dragAndDrop(target);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// --- Gesture Actions ---
|
|
105
|
+
scroll(options) {
|
|
106
|
+
return new SingleElementRunner(options?.selector, async () => {
|
|
107
|
+
if (options?.selector) {
|
|
108
|
+
const el = await resolveElement(options.selector);
|
|
109
|
+
await el.scrollIntoView();
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
await browser.execute((direction, pixels) => {
|
|
113
|
+
const delta = pixels || 500;
|
|
114
|
+
window.scrollBy({
|
|
115
|
+
top: direction === "up" ? -delta : direction === "down" ? delta : 0,
|
|
116
|
+
left: direction === "left" ? -delta : direction === "right" ? delta : 0,
|
|
117
|
+
behavior: "smooth",
|
|
118
|
+
});
|
|
119
|
+
}, options?.direction || "down", options?.pixels || 500);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
swipe(options) {
|
|
124
|
+
return new SingleElementRunner(options.selector, async () => {
|
|
125
|
+
if (typeof browser.touchAction === "function") {
|
|
126
|
+
const distance = options.distance || 300;
|
|
127
|
+
await browser.touchAction([
|
|
128
|
+
{ action: "press", x: 200, y: 500 },
|
|
129
|
+
{
|
|
130
|
+
action: "moveTo",
|
|
131
|
+
x: options.direction === "right" ? 200 + distance : options.direction === "left" ? 200 - distance : 200,
|
|
132
|
+
y: options.direction === "down" ? 500 + distance : options.direction === "up" ? 500 - distance : 500,
|
|
133
|
+
},
|
|
134
|
+
{ action: "release" },
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Fallback to web scroll
|
|
139
|
+
await this.scroll({ direction: options.direction, pixels: options.distance });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// --- Timing Actions ---
|
|
144
|
+
wait(durationMs) {
|
|
145
|
+
return new SingleElementRunner(undefined, async () => {
|
|
146
|
+
await browser.pause(durationMs);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
waitForElement(target, timeoutMs = 10000) {
|
|
150
|
+
return new SingleElementRunner(target, async () => {
|
|
151
|
+
const el = await resolveElement(target);
|
|
152
|
+
await el.waitForDisplayed({ timeout: timeoutMs });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export const Spectra = new SpectraStatic();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Canonical Action and Assertion Types
|
|
3
|
+
* Matches backend/src/models/test_step.rs
|
|
4
|
+
*/
|
|
5
|
+
export type ActionKey = "navigate" | "click" | "type" | "clear" | "select" | "scroll" | "swipe" | "wait" | "waitForElement" | "pressKey" | "longPress" | "doubleClick" | "hover" | "dragDrop" | "back" | "refresh";
|
|
6
|
+
export type AssertionKey = "elementDisplayed" | "elementNotDisplayed" | "elementExists" | "elementEnabled" | "elementDisabled" | "textContains" | "textEquals" | "textNotContains" | "urlContains" | "urlEquals" | "valueEquals" | "valueContains" | "attributeEquals" | "attributeContains" | "hasClass" | "hasAttribute" | "isSelected" | "elementCount" | "elementCountGreaterThan" | "pageLoaded" | "noErrors";
|
|
7
|
+
export type KeyOption = "Enter" | "Tab" | "Escape" | "Backspace" | "Delete" | "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight" | "Space";
|
|
8
|
+
export type Direction = "up" | "down" | "left" | "right";
|
|
9
|
+
export type ElementTarget = string | WebdriverIO.Element | ChainablePromiseElement;
|
|
10
|
+
export interface ScrollOptions {
|
|
11
|
+
direction?: Direction;
|
|
12
|
+
pixels?: number;
|
|
13
|
+
selector?: ElementTarget;
|
|
14
|
+
}
|
|
15
|
+
export interface SwipeOptions {
|
|
16
|
+
direction: Direction;
|
|
17
|
+
distance?: number;
|
|
18
|
+
selector?: ElementTarget;
|
|
19
|
+
}
|
|
20
|
+
export interface LongPressOptions {
|
|
21
|
+
text?: string;
|
|
22
|
+
duration?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface ClickOptions {
|
|
25
|
+
text?: string;
|
|
26
|
+
clickType?: "single" | "double";
|
|
27
|
+
}
|
|
28
|
+
export interface TypeOptions {
|
|
29
|
+
clearFirst?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export type SingleElementMatcher = "be.visible" | "not.be.visible" | "exist" | "not.exist" | "be.enabled" | "be.disabled" | "be.checked" | "be.selected" | "have.value" | "contain.value" | "have.text" | "contain.text" | "have.class" | "have.attr" | "have.url" | "contain.url" | "have.title" | "contain.title";
|
|
32
|
+
export type MultiElementMatcher = "have.length" | "have.length.greaterThan" | "be.empty" | "exist";
|
package/dist/types/generator.js
CHANGED
|
@@ -44,7 +44,43 @@ export class TypeGenerator {
|
|
|
44
44
|
content += ` }\n`;
|
|
45
45
|
content += ` interface Browser {\n`;
|
|
46
46
|
content += ` intercept<TData = unknown>(path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', fixture?: InterceptInput<TData>, options?: InterceptFixtureOptions): Promise<Mock>;\n`;
|
|
47
|
+
content += ` shouldHaveUrl(expectedUrl: string): Promise<void>;\n`;
|
|
48
|
+
content += ` shouldContainUrl(expectedUrl: string): Promise<void>;\n`;
|
|
49
|
+
content += ` shouldHaveTitle(expectedTitle: string): Promise<void>;\n`;
|
|
50
|
+
content += ` shouldContainTitle(expectedTitle: string): Promise<void>;\n`;
|
|
47
51
|
content += ` }\n`;
|
|
52
|
+
content += ` interface Element {\n`;
|
|
53
|
+
content += ` shouldBeVisible(): Promise<Element>;\n`;
|
|
54
|
+
content += ` shouldNotBeVisible(): Promise<Element>;\n`;
|
|
55
|
+
content += ` shouldExist(): Promise<Element>;\n`;
|
|
56
|
+
content += ` shouldNotExist(): Promise<Element>;\n`;
|
|
57
|
+
content += ` shouldBeEnabled(): Promise<Element>;\n`;
|
|
58
|
+
content += ` shouldBeDisabled(): Promise<Element>;\n`;
|
|
59
|
+
content += ` shouldBeSelected(): Promise<Element>;\n`;
|
|
60
|
+
content += ` shouldBeChecked(): Promise<Element>;\n`;
|
|
61
|
+
content += ` shouldHaveValue(expectedValue: string): Promise<Element>;\n`;
|
|
62
|
+
content += ` shouldContainValue(expectedValue: string): Promise<Element>;\n`;
|
|
63
|
+
content += ` shouldHaveText(expectedText: string): Promise<Element>;\n`;
|
|
64
|
+
content += ` shouldContainText(expectedText: string): Promise<Element>;\n`;
|
|
65
|
+
content += ` shouldHaveClass(className: string): Promise<Element>;\n`;
|
|
66
|
+
content += ` shouldHaveAttribute(attributeName: string, expectedValue?: string): Promise<Element>;\n`;
|
|
67
|
+
content += ` }\n`;
|
|
68
|
+
content += `}\n\n`;
|
|
69
|
+
content += `interface Promise<T> {\n`;
|
|
70
|
+
content += ` shouldBeVisible(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
|
|
71
|
+
content += ` shouldNotBeVisible(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
|
|
72
|
+
content += ` shouldExist(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
|
|
73
|
+
content += ` shouldNotExist(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
|
|
74
|
+
content += ` shouldBeEnabled(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
|
|
75
|
+
content += ` shouldBeDisabled(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
|
|
76
|
+
content += ` shouldBeSelected(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
|
|
77
|
+
content += ` shouldBeChecked(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
|
|
78
|
+
content += ` shouldHaveValue(this: Promise<WebdriverIO.Element>, expectedValue: string): Promise<WebdriverIO.Element>;\n`;
|
|
79
|
+
content += ` shouldContainValue(this: Promise<WebdriverIO.Element>, expectedValue: string): Promise<WebdriverIO.Element>;\n`;
|
|
80
|
+
content += ` shouldHaveText(this: Promise<WebdriverIO.Element>, expectedText: string): Promise<WebdriverIO.Element>;\n`;
|
|
81
|
+
content += ` shouldContainText(this: Promise<WebdriverIO.Element>, expectedText: string): Promise<WebdriverIO.Element>;\n`;
|
|
82
|
+
content += ` shouldHaveClass(this: Promise<WebdriverIO.Element>, className: string): Promise<WebdriverIO.Element>;\n`;
|
|
83
|
+
content += ` shouldHaveAttribute(this: Promise<WebdriverIO.Element>, attributeName: string, expectedValue?: string): Promise<WebdriverIO.Element>;\n`;
|
|
48
84
|
content += `}\n\n`;
|
|
49
85
|
// 1. Page Objects Global Declarations (Hierarchical)
|
|
50
86
|
// 2. Page Objects Global Declarations (Hierarchical)
|
|
@@ -75,8 +111,8 @@ export class TypeGenerator {
|
|
|
75
111
|
}
|
|
76
112
|
}
|
|
77
113
|
}
|
|
78
|
-
// 3. Actions Global Interface
|
|
79
|
-
content += `\ninterface TestSpectraActions {\n`;
|
|
114
|
+
// 3. Actions Global Interface (Extends built-in @testspectra/cli Spectra)
|
|
115
|
+
content += `\ninterface TestSpectraActions extends Omit<typeof import('@testspectra/cli').Spectra, ''> {\n`;
|
|
80
116
|
if (fs.existsSync(actionsDir)) {
|
|
81
117
|
const actionEntries = fs.readdirSync(actionsDir, { withFileTypes: true });
|
|
82
118
|
for (const entry of actionEntries) {
|
|
@@ -99,7 +135,7 @@ export class TypeGenerator {
|
|
|
99
135
|
}
|
|
100
136
|
}
|
|
101
137
|
if (matchedFile) {
|
|
102
|
-
content += ` ${actionName}: typeof import('${matchedFile}');\n`;
|
|
138
|
+
content += ` ${actionName}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
|
|
103
139
|
}
|
|
104
140
|
else {
|
|
105
141
|
content += ` ${actionName}: (...args: any[]) => Promise<any>;\n`;
|
|
@@ -107,7 +143,8 @@ export class TypeGenerator {
|
|
|
107
143
|
}
|
|
108
144
|
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
109
145
|
const actionName = entry.name.split(".")[0];
|
|
110
|
-
|
|
146
|
+
const modPath = `../../actions/${entry.name.replace(".ts", ".js")}`;
|
|
147
|
+
content += ` ${actionName}: (typeof import('${modPath}') extends { default: infer T } ? T : typeof import('${modPath}'));\n`;
|
|
111
148
|
}
|
|
112
149
|
}
|
|
113
150
|
}
|
|
@@ -137,7 +174,7 @@ export class TypeGenerator {
|
|
|
137
174
|
}
|
|
138
175
|
}
|
|
139
176
|
if (matchedFile) {
|
|
140
|
-
content += ` ${stepName}: typeof import('${matchedFile}');\n`;
|
|
177
|
+
content += ` ${stepName}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
|
|
141
178
|
}
|
|
142
179
|
else {
|
|
143
180
|
content += ` ${stepName}: (...args: any[]) => Promise<any>;\n`;
|
|
@@ -145,7 +182,8 @@ export class TypeGenerator {
|
|
|
145
182
|
}
|
|
146
183
|
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
147
184
|
const stepName = entry.name.split(".")[0];
|
|
148
|
-
|
|
185
|
+
const modPath = `../../steps/${entry.name.replace(".ts", ".js")}`;
|
|
186
|
+
content += ` ${stepName}: (typeof import('${modPath}') extends { default: infer T } ? T : typeof import('${modPath}'));\n`;
|
|
149
187
|
}
|
|
150
188
|
}
|
|
151
189
|
}
|
package/package.json
CHANGED
package/src/commands/init.ts
CHANGED
|
@@ -294,14 +294,18 @@ export default defineConfig({
|
|
|
294
294
|
return $('button[type="submit"]');
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
static get flashAlert(): ChainablePromiseElement {
|
|
298
|
+
return $('#flash');
|
|
299
|
+
}
|
|
300
|
+
|
|
297
301
|
static async open() {
|
|
298
|
-
await
|
|
302
|
+
await Spectra.navigate('/login');
|
|
299
303
|
}
|
|
300
304
|
|
|
301
305
|
static async login(username: string, pass: string) {
|
|
302
|
-
await this.usernameInput
|
|
303
|
-
await this.passwordInput
|
|
304
|
-
await this.submitButton
|
|
306
|
+
await Spectra.type(this.usernameInput, username);
|
|
307
|
+
await Spectra.type(this.passwordInput, pass);
|
|
308
|
+
await Spectra.click(this.submitButton);
|
|
305
309
|
}
|
|
306
310
|
}
|
|
307
311
|
`;
|
|
@@ -320,14 +324,18 @@ export default defineConfig({
|
|
|
320
324
|
return $('~login_button');
|
|
321
325
|
}
|
|
322
326
|
|
|
327
|
+
static get welcomeText(): ChainablePromiseElement {
|
|
328
|
+
return $('~welcome_text');
|
|
329
|
+
}
|
|
330
|
+
|
|
323
331
|
static async open() {
|
|
324
332
|
// Mobile app startup
|
|
325
333
|
}
|
|
326
334
|
|
|
327
335
|
static async login(username: string, pass: string) {
|
|
328
|
-
await this.usernameInput
|
|
329
|
-
await this.passwordInput
|
|
330
|
-
await this.submitButton
|
|
336
|
+
await Spectra.type(this.usernameInput, username);
|
|
337
|
+
await Spectra.type(this.passwordInput, pass);
|
|
338
|
+
await Spectra.click(this.submitButton);
|
|
331
339
|
}
|
|
332
340
|
}
|
|
333
341
|
`;
|
|
@@ -386,34 +394,35 @@ export default defineConfig({
|
|
|
386
394
|
`;
|
|
387
395
|
fs.writeFileSync(path.join(cwd, "hooks/default/before.ios.hook.ts"), hookIos, "utf-8");
|
|
388
396
|
|
|
389
|
-
// 10. Scaffold Specs (Pure test scripts, ZERO triple-slash lines)
|
|
390
|
-
const specWeb = `it("should authenticate user using
|
|
397
|
+
// 10. Scaffold Specs (Pure test scripts, ZERO triple-slash lines, ZERO raw selectors)
|
|
398
|
+
const specWeb = `it("should authenticate user using Page Objects and Spectra assertions", async () => {
|
|
391
399
|
await browser.intercept("/api/status", "GET", Fixture.userData);
|
|
400
|
+
|
|
401
|
+
// 1. Navigation via Page Object
|
|
392
402
|
await LoginPage.open();
|
|
393
|
-
await LoginPage.login("tomsmith", "SuperSecretPassword!");
|
|
394
403
|
|
|
395
|
-
|
|
396
|
-
await
|
|
404
|
+
// 2. High-level business flow via Shared Step & Page Object
|
|
405
|
+
await Step.loginUser("tomsmith", "SuperSecretPassword!");
|
|
406
|
+
|
|
407
|
+
// 3. Direct callable assertion on Page Object element
|
|
408
|
+
await LoginPage.flashAlert.shouldBeVisible();
|
|
409
|
+
await LoginPage.flashAlert.shouldContainText("You logged into a secure area!");
|
|
397
410
|
});
|
|
398
411
|
`;
|
|
399
412
|
fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/web.test.ts"), specWeb, "utf-8");
|
|
400
413
|
|
|
401
|
-
const specAndroid = `it("should authenticate user on Android Appium device", async () => {
|
|
402
|
-
await
|
|
403
|
-
await LoginPage.login("tomsmith", "SuperSecretPassword!");
|
|
414
|
+
const specAndroid = `it("should authenticate user on Android Appium device using Page Objects", async () => {
|
|
415
|
+
await Step.loginUser("tomsmith", "SuperSecretPassword!");
|
|
404
416
|
|
|
405
|
-
|
|
406
|
-
await expect(welcome).toBeDisplayed();
|
|
417
|
+
await LoginPage.welcomeText.shouldBeVisible();
|
|
407
418
|
});
|
|
408
419
|
`;
|
|
409
420
|
fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/android.test.ts"), specAndroid, "utf-8");
|
|
410
421
|
|
|
411
|
-
const specIos = `it("should authenticate user on iOS Appium device", async () => {
|
|
412
|
-
await
|
|
413
|
-
await LoginPage.login("tomsmith", "SuperSecretPassword!");
|
|
422
|
+
const specIos = `it("should authenticate user on iOS Appium device using Page Objects", async () => {
|
|
423
|
+
await Step.loginUser("tomsmith", "SuperSecretPassword!");
|
|
414
424
|
|
|
415
|
-
|
|
416
|
-
await expect(welcome).toBeDisplayed();
|
|
425
|
+
await LoginPage.welcomeText.shouldBeVisible();
|
|
417
426
|
});
|
|
418
427
|
`;
|
|
419
428
|
fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/ios.test.ts"), specIos, "utf-8");
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { runCommand } from "./commands/run.js";
|
|
|
7
7
|
export * from "./config/schema.js";
|
|
8
8
|
export * from "./config/loader.js";
|
|
9
9
|
export * from "./types/generator.js";
|
|
10
|
+
export * from "./step/index.js";
|
|
10
11
|
export { init as initTsPlugin, default as tsPlugin } from "./plugin.js";
|
|
11
12
|
|
|
12
13
|
export function createCliProgram() {
|