@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,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";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Database Canonical Action and Assertion Types
3
+ * Matches backend/src/models/test_step.rs
4
+ */
5
+ export {};
@@ -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
- content += ` ${actionName}: typeof import('../../actions/${entry.name.replace(".ts", ".js")}');\n`;
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
- content += ` ${stepName}: typeof import('../../steps/${entry.name.replace(".ts", ".js")}');\n`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testspectra/cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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": "workspace:*",
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"] || "workspace:*",
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 browser.url('/login');
338
+ await Spectra.navigate('/login');
299
339
  }
300
340
 
301
341
  static async login(username: string, pass: string) {
302
- await this.usernameInput.setValue(username);
303
- await this.passwordInput.setValue(pass);
304
- await this.submitButton.click();
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.setValue(username);
329
- await this.passwordInput.setValue(pass);
330
- await this.submitButton.click();
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 web locators and fixtures", async () => {
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
- const flash = await $("#flash");
396
- await expect(flash).toBeDisplayed();
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 LoginPage.open();
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
- const welcome = await $("~welcome_text");
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 LoginPage.open();
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
- const welcome = await $("~welcome_text");
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() {
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./matchers.js";
3
+ export * from "./runner/single.js";
4
+ export * from "./runner/collection.js";
5
+ export * from "./spectra.js";
6
+ export * from "./proto.js";