@testspectra/matchers 1.0.0
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/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/matchers.d.ts +14 -0
- package/dist/matchers.js +114 -0
- package/dist/proto.d.ts +46 -0
- package/dist/proto.js +114 -0
- package/dist/runner/collection.d.ts +13 -0
- package/dist/runner/collection.js +51 -0
- package/dist/runner/single.d.ts +21 -0
- package/dist/runner/single.js +138 -0
- package/dist/spectra.d.ts +34 -0
- package/dist/spectra.js +156 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +5 -0
- package/package.json +21 -0
- package/src/index.ts +6 -0
- package/src/matchers.ts +131 -0
- package/src/proto.ts +163 -0
- package/src/runner/collection.ts +73 -0
- package/src/runner/single.ts +166 -0
- package/src/spectra.ts +185 -0
- package/src/types.ts +113 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { SingleElementMatcher, ElementTarget } from "../types.js";
|
|
2
|
+
import { executeSingleMatcher, resolveElement } from "../matchers.js";
|
|
3
|
+
|
|
4
|
+
type AssertionItem = {
|
|
5
|
+
target?: ElementTarget;
|
|
6
|
+
matcher: SingleElementMatcher;
|
|
7
|
+
args: any[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class SingleElementRunner implements PromiseLike<void> {
|
|
11
|
+
private _target: ElementTarget | undefined;
|
|
12
|
+
private _action: (() => Promise<any>) | null = null;
|
|
13
|
+
private _assertions: AssertionItem[] = [];
|
|
14
|
+
|
|
15
|
+
constructor(target?: ElementTarget, action?: () => Promise<any>) {
|
|
16
|
+
this._target = target;
|
|
17
|
+
this._action = action || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- Chained Actions on current target ---
|
|
21
|
+
|
|
22
|
+
click(): this {
|
|
23
|
+
const target = this.requireTarget("click");
|
|
24
|
+
this._action = async () => {
|
|
25
|
+
const el = await resolveElement(target);
|
|
26
|
+
await el.click();
|
|
27
|
+
};
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
doubleClick(): this {
|
|
32
|
+
const target = this.requireTarget("doubleClick");
|
|
33
|
+
this._action = async () => {
|
|
34
|
+
const el = await resolveElement(target);
|
|
35
|
+
await el.doubleClick();
|
|
36
|
+
};
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
longPress(durationMs = 1000): this {
|
|
41
|
+
const target = this.requireTarget("longPress");
|
|
42
|
+
this._action = async () => {
|
|
43
|
+
const el = await resolveElement(target);
|
|
44
|
+
if (typeof (el as any).touchAction === "function") {
|
|
45
|
+
await (el as any).touchAction([
|
|
46
|
+
{ action: "longPress", ms: durationMs },
|
|
47
|
+
{ action: "release" },
|
|
48
|
+
]);
|
|
49
|
+
} else {
|
|
50
|
+
await el.click();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type(value: string, options?: { clearFirst?: boolean }): this {
|
|
57
|
+
const target = this.requireTarget("type");
|
|
58
|
+
this._action = async () => {
|
|
59
|
+
const el = await resolveElement(target);
|
|
60
|
+
if (options?.clearFirst) {
|
|
61
|
+
await el.clearValue();
|
|
62
|
+
}
|
|
63
|
+
await el.setValue(value);
|
|
64
|
+
};
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
clear(): this {
|
|
69
|
+
const target = this.requireTarget("clear");
|
|
70
|
+
this._action = async () => {
|
|
71
|
+
const el = await resolveElement(target);
|
|
72
|
+
await el.clearValue();
|
|
73
|
+
};
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
select(value: string): this {
|
|
78
|
+
const target = this.requireTarget("select");
|
|
79
|
+
this._action = async () => {
|
|
80
|
+
const el = await resolveElement(target);
|
|
81
|
+
await el.selectByVisibleText(value);
|
|
82
|
+
};
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
hover(): this {
|
|
87
|
+
const target = this.requireTarget("hover");
|
|
88
|
+
this._action = async () => {
|
|
89
|
+
const el = await resolveElement(target);
|
|
90
|
+
await el.moveTo();
|
|
91
|
+
};
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
waitForElement(timeoutMs = 10000): this {
|
|
96
|
+
const target = this.requireTarget("waitForElement");
|
|
97
|
+
this._action = async () => {
|
|
98
|
+
const el = await resolveElement(target);
|
|
99
|
+
await el.waitForDisplayed({ timeout: timeoutMs });
|
|
100
|
+
};
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Fluent Assertion Matchers ---
|
|
105
|
+
|
|
106
|
+
should(matcher: SingleElementMatcher, ...args: any[]): this;
|
|
107
|
+
should(target: ElementTarget, matcher: SingleElementMatcher, ...args: any[]): this;
|
|
108
|
+
should(a: ElementTarget | SingleElementMatcher, b?: any, ...rest: any[]): this {
|
|
109
|
+
const isFirstArgMatcher =
|
|
110
|
+
typeof a === "string" &&
|
|
111
|
+
(a.includes(".") ||
|
|
112
|
+
a.includes("exist") ||
|
|
113
|
+
a.includes("visible") ||
|
|
114
|
+
a.includes("enabled") ||
|
|
115
|
+
a.includes("disabled") ||
|
|
116
|
+
a.includes("selected") ||
|
|
117
|
+
a.includes("checked"));
|
|
118
|
+
|
|
119
|
+
if (b !== undefined && !isFirstArgMatcher) {
|
|
120
|
+
// Form: should(target, matcher, ...args)
|
|
121
|
+
this._assertions.push({
|
|
122
|
+
target: a as ElementTarget,
|
|
123
|
+
matcher: b as SingleElementMatcher,
|
|
124
|
+
args: rest,
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
// Form: should(matcher, ...args) -> on current target
|
|
128
|
+
this._assertions.push({
|
|
129
|
+
target: this._target,
|
|
130
|
+
matcher: a as SingleElementMatcher,
|
|
131
|
+
args: b !== undefined ? [b, ...rest] : rest,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- Execution when awaited ---
|
|
138
|
+
|
|
139
|
+
async then<TResult1 = void, TResult2 = never>(
|
|
140
|
+
onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null,
|
|
141
|
+
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
|
142
|
+
): Promise<TResult1 | TResult2> {
|
|
143
|
+
try {
|
|
144
|
+
if (this._action) {
|
|
145
|
+
await this._action();
|
|
146
|
+
}
|
|
147
|
+
for (const assert of this._assertions) {
|
|
148
|
+
await executeSingleMatcher(assert.target, assert.matcher, assert.args);
|
|
149
|
+
}
|
|
150
|
+
const res = (onfulfilled ? await onfulfilled() : undefined) as TResult1;
|
|
151
|
+
return res;
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (onrejected) {
|
|
154
|
+
return onrejected(err);
|
|
155
|
+
}
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private requireTarget(actionName: string): ElementTarget {
|
|
161
|
+
if (!this._target) {
|
|
162
|
+
throw new Error(`Cannot execute '${actionName}' without specifying an element target.`);
|
|
163
|
+
}
|
|
164
|
+
return this._target;
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/spectra.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClickOptions,
|
|
3
|
+
ElementTarget,
|
|
4
|
+
KeyOption,
|
|
5
|
+
LongPressOptions,
|
|
6
|
+
ScrollOptions,
|
|
7
|
+
SwipeOptions,
|
|
8
|
+
TypeOptions,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
import { SingleElementRunner } from "./runner/single.js";
|
|
11
|
+
import { MultiElementRunner } from "./runner/collection.js";
|
|
12
|
+
import { resolveElement } from "./matchers.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Main Spectra testing API orchestrator.
|
|
16
|
+
* Combines direct actions, single element queries (get), and collections (getAll).
|
|
17
|
+
*/
|
|
18
|
+
export class SpectraStatic {
|
|
19
|
+
/**
|
|
20
|
+
* Targets a single element by selector or Page Object element.
|
|
21
|
+
*/
|
|
22
|
+
get(target: ElementTarget): SingleElementRunner {
|
|
23
|
+
return new SingleElementRunner(target);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Targets multiple elements / collections by selector.
|
|
28
|
+
*/
|
|
29
|
+
getAll(selector: string): MultiElementRunner {
|
|
30
|
+
return new MultiElementRunner(selector);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Browser & Navigation Actions ---
|
|
34
|
+
|
|
35
|
+
navigate(url: string): SingleElementRunner {
|
|
36
|
+
return new SingleElementRunner(undefined, async () => {
|
|
37
|
+
await browser.url(url);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
back(): SingleElementRunner {
|
|
42
|
+
return new SingleElementRunner(undefined, async () => {
|
|
43
|
+
await browser.back();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
refresh(): SingleElementRunner {
|
|
48
|
+
return new SingleElementRunner(undefined, async () => {
|
|
49
|
+
await browser.refresh();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Direct Interaction Actions ---
|
|
54
|
+
|
|
55
|
+
click(target: ElementTarget, textOrOptions?: string | ClickOptions): SingleElementRunner {
|
|
56
|
+
return new SingleElementRunner(target, async () => {
|
|
57
|
+
const el = await resolveElement(target);
|
|
58
|
+
await el.click();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
doubleClick(target: ElementTarget, textOrOptions?: string | ClickOptions): SingleElementRunner {
|
|
63
|
+
return new SingleElementRunner(target, async () => {
|
|
64
|
+
const el = await resolveElement(target);
|
|
65
|
+
await el.doubleClick();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
longPress(target: ElementTarget, options?: LongPressOptions | number): SingleElementRunner {
|
|
70
|
+
const duration = typeof options === "number" ? options : options?.duration || 1000;
|
|
71
|
+
return new SingleElementRunner(target, async () => {
|
|
72
|
+
const el = await resolveElement(target);
|
|
73
|
+
if (typeof (el as any).touchAction === "function") {
|
|
74
|
+
await (el as any).touchAction([
|
|
75
|
+
{ action: "longPress", ms: duration },
|
|
76
|
+
{ action: "release" },
|
|
77
|
+
]);
|
|
78
|
+
} else {
|
|
79
|
+
await el.click();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type(target: ElementTarget, value: string, options?: TypeOptions): SingleElementRunner {
|
|
85
|
+
return new SingleElementRunner(target, async () => {
|
|
86
|
+
const el = await resolveElement(target);
|
|
87
|
+
if (options?.clearFirst) {
|
|
88
|
+
await el.clearValue();
|
|
89
|
+
}
|
|
90
|
+
await el.setValue(value);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
clear(target: ElementTarget): SingleElementRunner {
|
|
95
|
+
return new SingleElementRunner(target, async () => {
|
|
96
|
+
const el = await resolveElement(target);
|
|
97
|
+
await el.clearValue();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
select(target: ElementTarget, value: string): SingleElementRunner {
|
|
102
|
+
return new SingleElementRunner(target, async () => {
|
|
103
|
+
const el = await resolveElement(target);
|
|
104
|
+
await el.selectByVisibleText(value);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
hover(target: ElementTarget): SingleElementRunner {
|
|
109
|
+
return new SingleElementRunner(target, async () => {
|
|
110
|
+
const el = await resolveElement(target);
|
|
111
|
+
await el.moveTo();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pressKey(key: KeyOption | string): SingleElementRunner {
|
|
116
|
+
return new SingleElementRunner(undefined, async () => {
|
|
117
|
+
await browser.keys(key);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
dragDrop(sourceTarget: ElementTarget, destTarget: ElementTarget): SingleElementRunner {
|
|
122
|
+
return new SingleElementRunner(sourceTarget, async () => {
|
|
123
|
+
const source = await resolveElement(sourceTarget);
|
|
124
|
+
const target = await resolveElement(destTarget);
|
|
125
|
+
await source.dragAndDrop(target);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Gesture Actions ---
|
|
130
|
+
|
|
131
|
+
scroll(options?: ScrollOptions): SingleElementRunner {
|
|
132
|
+
return new SingleElementRunner(options?.selector, async () => {
|
|
133
|
+
if (options?.selector) {
|
|
134
|
+
const el = await resolveElement(options.selector);
|
|
135
|
+
await el.scrollIntoView();
|
|
136
|
+
} else {
|
|
137
|
+
await browser.execute((direction, pixels) => {
|
|
138
|
+
const delta = pixels || 500;
|
|
139
|
+
window.scrollBy({
|
|
140
|
+
top: direction === "up" ? -delta : direction === "down" ? delta : 0,
|
|
141
|
+
left: direction === "left" ? -delta : direction === "right" ? delta : 0,
|
|
142
|
+
behavior: "smooth",
|
|
143
|
+
});
|
|
144
|
+
}, options?.direction || "down", options?.pixels || 500);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
swipe(options: SwipeOptions): SingleElementRunner {
|
|
150
|
+
return new SingleElementRunner(options.selector, async () => {
|
|
151
|
+
if (typeof (browser as any).touchAction === "function") {
|
|
152
|
+
const distance = options.distance || 300;
|
|
153
|
+
await (browser as any).touchAction([
|
|
154
|
+
{ action: "press", x: 200, y: 500 },
|
|
155
|
+
{
|
|
156
|
+
action: "moveTo",
|
|
157
|
+
x: options.direction === "right" ? 200 + distance : options.direction === "left" ? 200 - distance : 200,
|
|
158
|
+
y: options.direction === "down" ? 500 + distance : options.direction === "up" ? 500 - distance : 500,
|
|
159
|
+
},
|
|
160
|
+
{ action: "release" },
|
|
161
|
+
]);
|
|
162
|
+
} else {
|
|
163
|
+
// Fallback to web scroll
|
|
164
|
+
await this.scroll({ direction: options.direction, pixels: options.distance });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Timing Actions ---
|
|
170
|
+
|
|
171
|
+
wait(durationMs: number): SingleElementRunner {
|
|
172
|
+
return new SingleElementRunner(undefined, async () => {
|
|
173
|
+
await browser.pause(durationMs);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
waitForElement(target: ElementTarget, timeoutMs = 10000): SingleElementRunner {
|
|
178
|
+
return new SingleElementRunner(target, async () => {
|
|
179
|
+
const el = await resolveElement(target);
|
|
180
|
+
await el.waitForDisplayed({ timeout: timeoutMs });
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const Spectra = new SpectraStatic();
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Canonical Action and Assertion Types
|
|
3
|
+
* Matches backend/src/models/test_step.rs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ActionKey =
|
|
7
|
+
| "navigate"
|
|
8
|
+
| "click"
|
|
9
|
+
| "type"
|
|
10
|
+
| "clear"
|
|
11
|
+
| "select"
|
|
12
|
+
| "scroll"
|
|
13
|
+
| "swipe"
|
|
14
|
+
| "wait"
|
|
15
|
+
| "waitForElement"
|
|
16
|
+
| "pressKey"
|
|
17
|
+
| "longPress"
|
|
18
|
+
| "doubleClick"
|
|
19
|
+
| "hover"
|
|
20
|
+
| "dragDrop"
|
|
21
|
+
| "back"
|
|
22
|
+
| "refresh";
|
|
23
|
+
|
|
24
|
+
export type AssertionKey =
|
|
25
|
+
| "elementDisplayed"
|
|
26
|
+
| "elementNotDisplayed"
|
|
27
|
+
| "elementExists"
|
|
28
|
+
| "elementEnabled"
|
|
29
|
+
| "elementDisabled"
|
|
30
|
+
| "textContains"
|
|
31
|
+
| "textEquals"
|
|
32
|
+
| "textNotContains"
|
|
33
|
+
| "urlContains"
|
|
34
|
+
| "urlEquals"
|
|
35
|
+
| "valueEquals"
|
|
36
|
+
| "valueContains"
|
|
37
|
+
| "attributeEquals"
|
|
38
|
+
| "attributeContains"
|
|
39
|
+
| "hasClass"
|
|
40
|
+
| "hasAttribute"
|
|
41
|
+
| "isSelected"
|
|
42
|
+
| "elementCount"
|
|
43
|
+
| "elementCountGreaterThan"
|
|
44
|
+
| "pageLoaded"
|
|
45
|
+
| "noErrors";
|
|
46
|
+
|
|
47
|
+
export type KeyOption =
|
|
48
|
+
| "Enter"
|
|
49
|
+
| "Tab"
|
|
50
|
+
| "Escape"
|
|
51
|
+
| "Backspace"
|
|
52
|
+
| "Delete"
|
|
53
|
+
| "ArrowUp"
|
|
54
|
+
| "ArrowDown"
|
|
55
|
+
| "ArrowLeft"
|
|
56
|
+
| "ArrowRight"
|
|
57
|
+
| "Space";
|
|
58
|
+
|
|
59
|
+
export type Direction = "up" | "down" | "left" | "right";
|
|
60
|
+
|
|
61
|
+
export type ElementTarget = string | WebdriverIO.Element | ChainablePromiseElement;
|
|
62
|
+
|
|
63
|
+
export interface ScrollOptions {
|
|
64
|
+
direction?: Direction;
|
|
65
|
+
pixels?: number;
|
|
66
|
+
selector?: ElementTarget;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SwipeOptions {
|
|
70
|
+
direction: Direction;
|
|
71
|
+
distance?: number;
|
|
72
|
+
selector?: ElementTarget;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface LongPressOptions {
|
|
76
|
+
text?: string;
|
|
77
|
+
duration?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ClickOptions {
|
|
81
|
+
text?: string;
|
|
82
|
+
clickType?: "single" | "double";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface TypeOptions {
|
|
86
|
+
clearFirst?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type SingleElementMatcher =
|
|
90
|
+
| "be.visible"
|
|
91
|
+
| "not.be.visible"
|
|
92
|
+
| "exist"
|
|
93
|
+
| "not.exist"
|
|
94
|
+
| "be.enabled"
|
|
95
|
+
| "be.disabled"
|
|
96
|
+
| "be.checked"
|
|
97
|
+
| "be.selected"
|
|
98
|
+
| "have.value"
|
|
99
|
+
| "contain.value"
|
|
100
|
+
| "have.text"
|
|
101
|
+
| "contain.text"
|
|
102
|
+
| "have.class"
|
|
103
|
+
| "have.attr"
|
|
104
|
+
| "have.url"
|
|
105
|
+
| "contain.url"
|
|
106
|
+
| "have.title"
|
|
107
|
+
| "contain.title";
|
|
108
|
+
|
|
109
|
+
export type MultiElementMatcher =
|
|
110
|
+
| "have.length"
|
|
111
|
+
| "have.length.greaterThan"
|
|
112
|
+
| "be.empty"
|
|
113
|
+
| "exist";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
|
13
|
+
"forceConsistentCasingInFileNames": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|