@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
|
@@ -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
|
@@ -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
|
|
|
@@ -97,6 +98,39 @@ export default defineConfig({
|
|
|
97
98
|
|
|
98
99
|
// 3. Create or update package.json with npm scripts & devDependencies
|
|
99
100
|
const packageJsonPath = path.join(cwd, "package.json");
|
|
101
|
+
|
|
102
|
+
// Dynamically resolve CLI version
|
|
103
|
+
let cliVersion = "^1.0.6";
|
|
104
|
+
try {
|
|
105
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
106
|
+
const __dirname = path.dirname(__filename);
|
|
107
|
+
const cliPackageJsonPath = path.resolve(__dirname, "../../package.json");
|
|
108
|
+
if (fs.existsSync(cliPackageJsonPath)) {
|
|
109
|
+
const cliPkg = JSON.parse(fs.readFileSync(cliPackageJsonPath, "utf-8"));
|
|
110
|
+
if (cliPkg.version) {
|
|
111
|
+
cliVersion = `^${cliPkg.version}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
116
|
+
// Check if cwd is inside the testspectra monorepo with a pnpm-workspace.yaml
|
|
117
|
+
let isInternalWorkspace = false;
|
|
118
|
+
let cur = cwd;
|
|
119
|
+
while (cur !== path.dirname(cur)) {
|
|
120
|
+
if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml"))) {
|
|
121
|
+
try {
|
|
122
|
+
const wsContent = fs.readFileSync(path.join(cur, "pnpm-workspace.yaml"), "utf-8");
|
|
123
|
+
if (wsContent.includes("cli")) {
|
|
124
|
+
isInternalWorkspace = true;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
cur = path.dirname(cur);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const cliDepVersion = isInternalWorkspace ? "workspace:*" : cliVersion;
|
|
133
|
+
|
|
100
134
|
let pkg: any = {
|
|
101
135
|
name: path.basename(cwd),
|
|
102
136
|
version: "1.0.0",
|
|
@@ -107,7 +141,7 @@ export default defineConfig({
|
|
|
107
141
|
"type-check": "tsc -b",
|
|
108
142
|
},
|
|
109
143
|
devDependencies: {
|
|
110
|
-
"@testspectra/cli":
|
|
144
|
+
"@testspectra/cli": cliDepVersion,
|
|
111
145
|
"@types/node": "^20.14.0",
|
|
112
146
|
"@wdio/globals": "^9.2.8",
|
|
113
147
|
"@wdio/mocha-framework": "^9.2.8",
|
|
@@ -127,7 +161,9 @@ export default defineConfig({
|
|
|
127
161
|
},
|
|
128
162
|
devDependencies: {
|
|
129
163
|
...(existing.devDependencies || {}),
|
|
130
|
-
"@testspectra/cli": existing.devDependencies?.["@testspectra/cli"]
|
|
164
|
+
"@testspectra/cli": existing.devDependencies?.["@testspectra/cli"]?.startsWith("workspace:") && !isInternalWorkspace
|
|
165
|
+
? cliVersion
|
|
166
|
+
: existing.devDependencies?.["@testspectra/cli"] || cliDepVersion,
|
|
131
167
|
"@types/node": existing.devDependencies?.["@types/node"] || "^20.14.0",
|
|
132
168
|
"@wdio/globals": existing.devDependencies?.["@wdio/globals"] || "^9.2.8",
|
|
133
169
|
"@wdio/mocha-framework": existing.devDependencies?.["@wdio/mocha-framework"] || "^9.2.8",
|
|
@@ -294,14 +330,18 @@ export default defineConfig({
|
|
|
294
330
|
return $('button[type="submit"]');
|
|
295
331
|
}
|
|
296
332
|
|
|
333
|
+
static get flashAlert(): ChainablePromiseElement {
|
|
334
|
+
return $('#flash');
|
|
335
|
+
}
|
|
336
|
+
|
|
297
337
|
static async open() {
|
|
298
|
-
await
|
|
338
|
+
await Spectra.navigate('/login');
|
|
299
339
|
}
|
|
300
340
|
|
|
301
341
|
static async login(username: string, pass: string) {
|
|
302
|
-
await this.usernameInput
|
|
303
|
-
await this.passwordInput
|
|
304
|
-
await this.submitButton
|
|
342
|
+
await Spectra.type(this.usernameInput, username);
|
|
343
|
+
await Spectra.type(this.passwordInput, pass);
|
|
344
|
+
await Spectra.click(this.submitButton);
|
|
305
345
|
}
|
|
306
346
|
}
|
|
307
347
|
`;
|
|
@@ -320,14 +360,18 @@ export default defineConfig({
|
|
|
320
360
|
return $('~login_button');
|
|
321
361
|
}
|
|
322
362
|
|
|
363
|
+
static get welcomeText(): ChainablePromiseElement {
|
|
364
|
+
return $('~welcome_text');
|
|
365
|
+
}
|
|
366
|
+
|
|
323
367
|
static async open() {
|
|
324
368
|
// Mobile app startup
|
|
325
369
|
}
|
|
326
370
|
|
|
327
371
|
static async login(username: string, pass: string) {
|
|
328
|
-
await this.usernameInput
|
|
329
|
-
await this.passwordInput
|
|
330
|
-
await this.submitButton
|
|
372
|
+
await Spectra.type(this.usernameInput, username);
|
|
373
|
+
await Spectra.type(this.passwordInput, pass);
|
|
374
|
+
await Spectra.click(this.submitButton);
|
|
331
375
|
}
|
|
332
376
|
}
|
|
333
377
|
`;
|
|
@@ -386,34 +430,35 @@ export default defineConfig({
|
|
|
386
430
|
`;
|
|
387
431
|
fs.writeFileSync(path.join(cwd, "hooks/default/before.ios.hook.ts"), hookIos, "utf-8");
|
|
388
432
|
|
|
389
|
-
// 10. Scaffold Specs (Pure test scripts, ZERO triple-slash lines)
|
|
390
|
-
const specWeb = `it("should authenticate user using
|
|
433
|
+
// 10. Scaffold Specs (Pure test scripts, ZERO triple-slash lines, ZERO raw selectors)
|
|
434
|
+
const specWeb = `it("should authenticate user using Page Objects and Spectra assertions", async () => {
|
|
391
435
|
await browser.intercept("/api/status", "GET", Fixture.userData);
|
|
436
|
+
|
|
437
|
+
// 1. Navigation via Page Object
|
|
392
438
|
await LoginPage.open();
|
|
393
|
-
await LoginPage.login("tomsmith", "SuperSecretPassword!");
|
|
394
439
|
|
|
395
|
-
|
|
396
|
-
await
|
|
440
|
+
// 2. High-level business flow via Shared Step & Page Object
|
|
441
|
+
await Step.loginUser("tomsmith", "SuperSecretPassword!");
|
|
442
|
+
|
|
443
|
+
// 3. Direct callable assertion on Page Object element
|
|
444
|
+
await LoginPage.flashAlert.shouldBeVisible();
|
|
445
|
+
await LoginPage.flashAlert.shouldContainText("You logged into a secure area!");
|
|
397
446
|
});
|
|
398
447
|
`;
|
|
399
448
|
fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/web.test.ts"), specWeb, "utf-8");
|
|
400
449
|
|
|
401
|
-
const specAndroid = `it("should authenticate user on Android Appium device", async () => {
|
|
402
|
-
await
|
|
403
|
-
await LoginPage.login("tomsmith", "SuperSecretPassword!");
|
|
450
|
+
const specAndroid = `it("should authenticate user on Android Appium device using Page Objects", async () => {
|
|
451
|
+
await Step.loginUser("tomsmith", "SuperSecretPassword!");
|
|
404
452
|
|
|
405
|
-
|
|
406
|
-
await expect(welcome).toBeDisplayed();
|
|
453
|
+
await LoginPage.welcomeText.shouldBeVisible();
|
|
407
454
|
});
|
|
408
455
|
`;
|
|
409
456
|
fs.writeFileSync(path.join(cwd, "specs/TC-LOGIN-01/android.test.ts"), specAndroid, "utf-8");
|
|
410
457
|
|
|
411
|
-
const specIos = `it("should authenticate user on iOS Appium device", async () => {
|
|
412
|
-
await
|
|
413
|
-
await LoginPage.login("tomsmith", "SuperSecretPassword!");
|
|
458
|
+
const specIos = `it("should authenticate user on iOS Appium device using Page Objects", async () => {
|
|
459
|
+
await Step.loginUser("tomsmith", "SuperSecretPassword!");
|
|
414
460
|
|
|
415
|
-
|
|
416
|
-
await expect(welcome).toBeDisplayed();
|
|
461
|
+
await LoginPage.welcomeText.shouldBeVisible();
|
|
417
462
|
});
|
|
418
463
|
`;
|
|
419
464
|
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() {
|