@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.
@@ -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();
@@ -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";
@@ -51,7 +51,43 @@ export class TypeGenerator {
51
51
  content += ` }\n`;
52
52
  content += ` interface Browser {\n`;
53
53
  content += ` intercept<TData = unknown>(path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', fixture?: InterceptInput<TData>, options?: InterceptFixtureOptions): Promise<Mock>;\n`;
54
+ content += ` shouldHaveUrl(expectedUrl: string): Promise<void>;\n`;
55
+ content += ` shouldContainUrl(expectedUrl: string): Promise<void>;\n`;
56
+ content += ` shouldHaveTitle(expectedTitle: string): Promise<void>;\n`;
57
+ content += ` shouldContainTitle(expectedTitle: string): Promise<void>;\n`;
54
58
  content += ` }\n`;
59
+ content += ` interface Element {\n`;
60
+ content += ` shouldBeVisible(): Promise<Element>;\n`;
61
+ content += ` shouldNotBeVisible(): Promise<Element>;\n`;
62
+ content += ` shouldExist(): Promise<Element>;\n`;
63
+ content += ` shouldNotExist(): Promise<Element>;\n`;
64
+ content += ` shouldBeEnabled(): Promise<Element>;\n`;
65
+ content += ` shouldBeDisabled(): Promise<Element>;\n`;
66
+ content += ` shouldBeSelected(): Promise<Element>;\n`;
67
+ content += ` shouldBeChecked(): Promise<Element>;\n`;
68
+ content += ` shouldHaveValue(expectedValue: string): Promise<Element>;\n`;
69
+ content += ` shouldContainValue(expectedValue: string): Promise<Element>;\n`;
70
+ content += ` shouldHaveText(expectedText: string): Promise<Element>;\n`;
71
+ content += ` shouldContainText(expectedText: string): Promise<Element>;\n`;
72
+ content += ` shouldHaveClass(className: string): Promise<Element>;\n`;
73
+ content += ` shouldHaveAttribute(attributeName: string, expectedValue?: string): Promise<Element>;\n`;
74
+ content += ` }\n`;
75
+ content += `}\n\n`;
76
+ content += `interface Promise<T> {\n`;
77
+ content += ` shouldBeVisible(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
78
+ content += ` shouldNotBeVisible(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
79
+ content += ` shouldExist(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
80
+ content += ` shouldNotExist(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
81
+ content += ` shouldBeEnabled(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
82
+ content += ` shouldBeDisabled(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
83
+ content += ` shouldBeSelected(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
84
+ content += ` shouldBeChecked(this: Promise<WebdriverIO.Element>): Promise<WebdriverIO.Element>;\n`;
85
+ content += ` shouldHaveValue(this: Promise<WebdriverIO.Element>, expectedValue: string): Promise<WebdriverIO.Element>;\n`;
86
+ content += ` shouldContainValue(this: Promise<WebdriverIO.Element>, expectedValue: string): Promise<WebdriverIO.Element>;\n`;
87
+ content += ` shouldHaveText(this: Promise<WebdriverIO.Element>, expectedText: string): Promise<WebdriverIO.Element>;\n`;
88
+ content += ` shouldContainText(this: Promise<WebdriverIO.Element>, expectedText: string): Promise<WebdriverIO.Element>;\n`;
89
+ content += ` shouldHaveClass(this: Promise<WebdriverIO.Element>, className: string): Promise<WebdriverIO.Element>;\n`;
90
+ content += ` shouldHaveAttribute(this: Promise<WebdriverIO.Element>, attributeName: string, expectedValue?: string): Promise<WebdriverIO.Element>;\n`;
55
91
  content += `}\n\n`;
56
92
 
57
93
  // 1. Page Objects Global Declarations (Hierarchical)
@@ -83,8 +119,8 @@ export class TypeGenerator {
83
119
  }
84
120
  }
85
121
 
86
- // 3. Actions Global Interface
87
- content += `\ninterface TestSpectraActions {\n`;
122
+ // 3. Actions Global Interface (Extends built-in @testspectra/cli Spectra)
123
+ content += `\ninterface TestSpectraActions extends Omit<typeof import('@testspectra/cli').Spectra, ''> {\n`;
88
124
  if (fs.existsSync(actionsDir)) {
89
125
  const actionEntries = fs.readdirSync(actionsDir, { withFileTypes: true });
90
126
  for (const entry of actionEntries) {
@@ -105,13 +141,14 @@ export class TypeGenerator {
105
141
  }
106
142
  }
107
143
  if (matchedFile) {
108
- content += ` ${actionName}: typeof import('${matchedFile}');\n`;
144
+ content += ` ${actionName}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
109
145
  } else {
110
146
  content += ` ${actionName}: (...args: any[]) => Promise<any>;\n`;
111
147
  }
112
148
  } else if (entry.isFile() && entry.name.endsWith(".ts")) {
113
149
  const actionName = entry.name.split(".")[0];
114
- content += ` ${actionName}: typeof import('../../actions/${entry.name.replace(".ts", ".js")}');\n`;
150
+ const modPath = `../../actions/${entry.name.replace(".ts", ".js")}`;
151
+ content += ` ${actionName}: (typeof import('${modPath}') extends { default: infer T } ? T : typeof import('${modPath}'));\n`;
115
152
  }
116
153
  }
117
154
  }
@@ -140,13 +177,14 @@ export class TypeGenerator {
140
177
  }
141
178
  }
142
179
  if (matchedFile) {
143
- content += ` ${stepName}: typeof import('${matchedFile}');\n`;
180
+ content += ` ${stepName}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
144
181
  } else {
145
182
  content += ` ${stepName}: (...args: any[]) => Promise<any>;\n`;
146
183
  }
147
184
  } else if (entry.isFile() && entry.name.endsWith(".ts")) {
148
185
  const stepName = entry.name.split(".")[0];
149
- content += ` ${stepName}: typeof import('../../steps/${entry.name.replace(".ts", ".js")}');\n`;
186
+ const modPath = `../../steps/${entry.name.replace(".ts", ".js")}`;
187
+ content += ` ${stepName}: (typeof import('${modPath}') extends { default: infer T } ? T : typeof import('${modPath}'));\n`;
150
188
  }
151
189
  }
152
190
  }
Binary file
Binary file
package/tsconfig.json CHANGED
@@ -9,6 +9,7 @@
9
9
  "strict": true,
10
10
  "esModuleInterop": true,
11
11
  "skipLibCheck": true,
12
+ "types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
12
13
  "forceConsistentCasingInFileNames": true
13
14
  },
14
15
  "include": ["src/**/*"],
Binary file
Binary file