flowstride 1.0.18 → 1.0.20-rc.2

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.
Files changed (41) hide show
  1. package/NATIVE_CI.md +155 -0
  2. package/README.md +40 -33
  3. package/RELEASE_NOTES.md +40 -0
  4. package/dist/adapters/web/playwright.d.ts +4 -0
  5. package/dist/adapters/web/playwright.js +85 -38
  6. package/dist/ci/config.d.ts +3 -0
  7. package/dist/ci/config.js +76 -0
  8. package/dist/ci/deadline.d.ts +2 -0
  9. package/dist/ci/deadline.js +16 -0
  10. package/dist/ci/init.d.ts +1 -0
  11. package/dist/ci/init.js +21 -0
  12. package/dist/ci/runtime.d.ts +19 -0
  13. package/dist/ci/runtime.js +190 -0
  14. package/dist/cli/commands/open.d.ts +1 -0
  15. package/dist/cli/commands/open.js +12 -0
  16. package/dist/cli/commands/run.js +15 -34
  17. package/dist/cli/index.js +51 -5
  18. package/dist/cli/manual_server/workspace_server.d.ts +1 -0
  19. package/dist/cli/manual_server/workspace_server.js +751 -0
  20. package/dist/game/bridge.d.ts +13 -0
  21. package/dist/game/bridge.js +15 -0
  22. package/dist/game/sdk.d.ts +27 -0
  23. package/dist/game/sdk.js +255 -0
  24. package/dist/game/three-bridge.d.ts +18 -0
  25. package/dist/game/three-bridge.js +59 -0
  26. package/dist/parser/lexer.js +4 -0
  27. package/dist/parser/parser.js +19 -3
  28. package/dist/runner/flow_orchestrator.d.ts +5 -1
  29. package/dist/runner/flow_orchestrator.js +31 -7
  30. package/dist/runner/flow_worker.js +57 -13
  31. package/dist/runner/plugin_manager.d.ts +1 -0
  32. package/dist/runner/plugin_manager.js +33 -1
  33. package/dist/types/index.d.ts +36 -0
  34. package/examples/native-game/flows/canvas.flow +13 -0
  35. package/examples/native-game/flowstride.config.json +12 -0
  36. package/examples/native-game/server.mjs +12 -0
  37. package/package.json +14 -5
  38. package/src/ui/dist/assets/index-DIC22ZK_.js +638 -0
  39. package/src/ui/dist/assets/workspace_engine_bg-BVlpAZ1H.wasm +0 -0
  40. package/src/ui/dist/index.html +1 -1
  41. package/src/ui/dist/assets/index-BCdexgt5.js +0 -270
package/NATIVE_CI.md ADDED
@@ -0,0 +1,155 @@
1
+ # Native CI and browser-game candidate
2
+
3
+ This candidate adds native CI and game APIs to both engines. Managed Cloud
4
+ execution remains retired. Existing npm OSS / private Pro archive distribution
5
+ and fallback remain supported. Naija Throne's current runner is unchanged;
6
+ its migration is a separate acceptance step. Native CI uses protocol 1.
7
+ The public CLI selects compatible Pro engines or logs the reason and falls back
8
+ to OSS. Older Pro CLIs may own the npm binary and do not understand the new
9
+ commands; install both candidates when testing the Pro path.
10
+
11
+ The candidates pin Playwright 1.59.1 in both engines; the native install command
12
+ installs that engine's matching Chromium.
13
+
14
+ ## Local candidate check
15
+
16
+ Use a disposable project first. Install the OSS archive, then the Pro archive if
17
+ needed. Both candidates must be installed for testing the new Pro path. The
18
+ currently hosted Cloud download still serves the existing release; this local
19
+ candidate does not change that endpoint or its archive.
20
+
21
+ ```sh
22
+ npm install --save-dev /absolute/path/flowstride-1.0.20-rc.2.tgz
23
+ # Optional: install the local matching Pro candidate after project dependencies.
24
+ npm install --no-save /absolute/path/flowstride-pro-1.0.15-rc.1.tgz
25
+ npx --no-install flowstride install
26
+ npx --no-install flowstride run --ci
27
+ ```
28
+
29
+ Pro continues to require your normal valid developer credentials for licensed
30
+ features. Installing Pro uses `--no-save`: another `npm ci` can remove it. Keep
31
+ project dependency installation before Pro installation. An installed Pro package
32
+ can own `node_modules/.bin/flowstride`; both that entry point and the public OSS
33
+ entry point must be covered when checking fallback.
34
+
35
+ The sample in `examples/native-game` uses a tiny local canvas game. Copy its
36
+ files into a disposable project containing the installed candidates, then run
37
+ the command above. `server.mjs` is the game fixture's HTTP server; it contains no
38
+ Flowstride runner, adapter, worker, or CI orchestration code. A real application
39
+ uses its normal development/start command instead.
40
+
41
+ ## Configuration
42
+
43
+ `flowstride.config.json` supports `testDir`, `baseUrl`, `timeout`, `device`
44
+ (`desktop`, `phone`, `landscape`), optional `viewport`, `pluginsDir`,
45
+ `artifactsDir`, `recordVideo` (boolean), `browserErrors` (`fail` or `record`), and
46
+ `webServer` (`command`, local readiness `url`, optional `cwd` and `timeout`).
47
+ Malformed configuration and unsafe output paths fail before execution.
48
+
49
+ Native CI is selected by `--ci` or a recognized CI environment. It runs without
50
+ a dashboard server, returns nonzero for failed/incomplete/crashed execution,
51
+ and writes latest `summary.json`/`junit.xml` reports at the configured artifact
52
+ root. Media and attachments go into a unique run directory referenced by the
53
+ summary, so local reruns retain history without mixing evidence.
54
+ SIGINT/SIGTERM produce interrupted results. The run owns its optional local
55
+ server and refuses an already-serving readiness URL.
56
+
57
+ Cloud reporting is off by default in native CI. `cloudSync: true` explicitly
58
+ allows existing authenticated reporting; its existing server-side security and
59
+ storage limitations still apply. Normal interactive reporting and the restored
60
+ Cloud URL overrides remain separate from retired managed execution.
61
+
62
+ `npx flowstride ci init github` generates a workflow explicitly, never during
63
+ installation, and refuses to overwrite it. It assumes an npm project with a
64
+ committed lockfile. Other package managers and monorepos can use the composite
65
+ action's install-command and working-directory inputs. The composite action is
66
+ in the OSS repository's `action/` directory and becomes remotely usable only
67
+ once that source is pushed. Pin its commit when configuring a consumer workflow.
68
+ Pro jobs pass a developer token as a secret; dependency installation precedes
69
+ Pro login/install, matching-browser installation, execution, and artifact upload.
70
+
71
+ ## Native game actions
72
+
73
+ Register the built-in alias with `let game = "flowstride:game"`. It requires no
74
+ custom plugin file. Available actions:
75
+
76
+ - `tap`: canvas selector, x/y; relative coordinates by default, or `unit: "pixels"`.
77
+ - `drag`/`swipe`: canvas, from/to coordinates, optional duration and input type.
78
+ - `key`: key name and optional hold duration.
79
+ - `checkpoint`: named screenshot, optionally of one canvas.
80
+ - `snapshot` and `expectVisual`: canvas comparison with explicit minChange/maxChange.
81
+ - `state`/`expectState`: exposed state, optional dotted path and expected equals.
82
+ - `snapshotState`/`expectPreserved`: compare selected state across actions/reloads.
83
+ - `fixture`: a named fixture registered by the application in explicit test mode.
84
+ - `select`: a stable object ID resolved by the game bridge, then real pointer input.
85
+
86
+ Visual checks establish pixel differences, not gameplay correctness; pair them
87
+ with state or UI assertions. Mobile profiles emulate Chromium devices, not
88
+ physical phones. Touch dragging currently uses Chromium CDP.
89
+
90
+ For a generic browser game, import `registerGame` from
91
+ `flowstride/dist/game/bridge.js` and supply readState and optional test-only
92
+ fixtures. The function manages the browser bridge; no global setup is required
93
+ in the test runner.
94
+
95
+ ## Three.js integration
96
+
97
+ Import `registerThreeGame` from `flowstride/dist/game/three-bridge.js` in the game
98
+ bundle. Register the live scene, camera, canvas, the application's own Vector3
99
+ and Raycaster constructors, and an optional objects map keyed by stable IDs.
100
+ Alternatively, assign `userData.flowstrideId` to scene objects. Optional local
101
+ pick points use `userData.flowstridePickPoint: [x, y, z]`.
102
+
103
+ The bridge projects the live object and raycasts the live scene; hidden,
104
+ occluded, off-screen, covered or non-pickable objects fail explicitly. It creates
105
+ no renderer or animation loop. Dispose the bridge when the scene is replaced.
106
+
107
+ Optional `readState` provides observable state. `fixtures` are available only
108
+ when the application explicitly registers with `testMode: true`; use that
109
+ setting in dedicated test builds, never public production builds. Naija Throne
110
+ still defines campaign-specific fixtures/rules, using these supported APIs.
111
+
112
+ Custom plugins now receive `sdk.game`, `sdk.browser.page()`, and `sdk.run`
113
+ (device, artifact directory and attach method), alongside the existing SDK.
114
+ They need no global bridge or internal Flowstride class imports.
115
+
116
+ ## Validation before release
117
+
118
+ Build both packages and run `npm run test:native`. For the optional live Three.js
119
+ fixture set `FLOWSTRIDE_THREE_BUILD` to a Three.js build directory containing
120
+ three.module.js and three.core.js. OSS CLI handoff tests use
121
+ `FLOWSTRIDE_PRO_SOURCE` pointing at the built Pro package.
122
+
123
+ Run the real archive install/handoff harness against both candidates, including
124
+ missing/broken Pro and revoked-token fallback. Validate GitHub's Linux execution,
125
+ real licensed download and Naija Throne migration after local acceptance.
126
+ No push, npm publication, Cloud archive replacement or Naija Throne migration
127
+ is performed by building these candidates.
128
+
129
+ ## Candidate acceptance automation
130
+
131
+ The repository's `Native CI acceptance` workflow tests Node 22 and 24 on Linux.
132
+ It uses a checksum-pinned archive in `.github/fixtures`, checks its compiled engine
133
+ against a fresh build, checks referenced dashboard assets, then installs that exact
134
+ archive in a clean consumer. It exercises native browser installation, passing and
135
+ deliberately failing game execution, media, server cleanup and workflow preservation.
136
+ It also runs the native source suite, including delayed uploads and cancellation.
137
+ The Pro archive belongs only in the private Pro repository.
138
+
139
+ These jobs do not publish npm packages, replace the hosted Pro archive, deploy
140
+ Cloud, or migrate application repositories. Native Cloud reporting stays opt-in.
141
+ Qualification covers Chromium and emulated desktop/phone/landscape profiles.
142
+ Physical Android/iOS devices, Safari/Firefox and other game engines are outside
143
+ this candidate's verified support scope. Real Cloud licensing, reporting and
144
+ video storage require a separate live acceptance run.
145
+
146
+ ## OSS npm preview release
147
+
148
+ OSS `1.0.20-rc.2` requires Node.js 22.12.0+. Publish the verified archive under
149
+ `next`; install with `npm install --save-dev flowstride@next` after publication.
150
+ Use the release archive and checksum rather than rebuilding from a dirty checkout.
151
+ The matching local Pro candidate remains `1.0.15-rc.1`; hosted Pro remains
152
+ `1.0.14`. Complete authentication before reinstalling the local Pro candidate.
153
+ The unchanged dashboard bundle is reused from the previously qualified rc.1
154
+ archive; the engine is rebuilt from this source. The release input manifest
155
+ records both that archive's checksum and the merged base commit.
package/README.md CHANGED
@@ -6,6 +6,34 @@ Flowstride OS combines the human-readable structure of Behavior-Driven Developme
6
6
 
7
7
  [Documentation](https://docs.flowstride.io/)
8
8
 
9
+ ## Native CI and game testing preview — 1.0.20-rc.2
10
+
11
+ This is an opt-in preview intended for the npm `next` tag. The default stable
12
+ release remains available through `latest`. After the preview is published:
13
+
14
+ ```sh
15
+ npm install --save-dev flowstride@next
16
+ npx --no-install flowstride install
17
+ npx --no-install flowstride run --ci
18
+ ```
19
+
20
+ Node.js 22.12.0+ is required; acceptance covers the minimum version and current
21
+ Node.js 22/24 on Linux, plus Node.js 24 on macOS. The preview adds native CI
22
+ exit codes, JSON/JUnit reports, videos/screenshots, device profiles, explicit
23
+ GitHub workflow generation, and supported canvas/Three.js testing APIs.
24
+ See [native CI setup](NATIVE_CI.md) and [release notes](RELEASE_NOTES.md).
25
+
26
+ For this preview, use OSS alone or manually install the matching private Pro
27
+ `1.0.15-rc.1` candidate after project dependencies. Normal Cloud login still
28
+ serves Pro `1.0.14`; signing in afterward can replace the preview's Pro package,
29
+ and that older CLI does not support the new native commands. Cloud reporting
30
+ and video-storage defects remain open. Native CI keeps Cloud reporting off by
31
+ default. Managed Cloud execution remains retired.
32
+
33
+ This release qualifies Chromium desktop and emulated phone/landscape profiles
34
+ and browser-game fixtures. It does not establish physical-phone, Safari,
35
+ Firefox, other game-engine, or complete application-game coverage.
36
+
9
37
  ---
10
38
 
11
39
  ## Why Flowstride?
@@ -21,7 +49,7 @@ Traditional E2E frameworks force you to choose between highly technical code (wh
21
49
 
22
50
  ## Installation & Quick Start
23
51
 
24
- Flowstride requires Node.js 18 or higher.
52
+ Flowstride requires Node.js 22.12.0 or higher. Use a current patched Node.js 22 or 24 release.
25
53
 
26
54
  **1. Install globally (or as a dev dependency in your project):**
27
55
 
@@ -71,41 +99,20 @@ npx flowstride login
71
99
 
72
100
  ---
73
101
 
74
- ## Privacy & Data Security
75
-
76
- Flowstride is designed with a **zero-trust, local-first architecture**. Whether you are testing internal microservices, staging environments, or production APIs, your sensitive data, PHI, and credentials remain strictly on your infrastructure.
77
-
78
- ### Open Source Core: 100% Local Isolation
79
-
80
- The Open Source version of Flowstride operates entirely as an ephemeral, local execution engine.
81
-
82
- - **Air-Gapped Execution:** Flowstride does not "phone home." The OS runner executes strictly on your local machine or within your private CI/CD pipeline. There is no external Flowstride database storing your test payloads or network logs.
83
-
84
- - **Ephemeral Memory:** Flowstride is inherently stateless. API request headers, response bodies, and DOM snapshots exist only in Node.js RAM during execution. The millisecond the execution process terminates, all memory is wiped.
85
-
86
- - **The DevTools Analogy:** Our network interception works natively within Playwright's browser context routing. It is fundamentally identical to opening the Chrome DevTools "Network" tab locally. We do not act as an external Man-In-The-Middle (MITM) proxy.
87
-
88
- - **Opt-In Local State:** Artifacts saved to disk—such as execution videos (`.flowstride/mock-cloud/`) or explicitly persisted browser states (`.flowstride/sessions/`)—are written strictly to your local file system to mirror the functionality of tools like Postman. _(Note: We strongly recommend adding `.flowstride/` to your `.gitignore` to prevent accidental credential commits)._
89
-
90
- ### Flowstride Enterprise: Secure Cloud Telemetry
91
-
92
- For teams using Flowstride Enterprise to aggregate multi-flow metrics, we maintain a strict metadata-only boundary.
93
-
94
- - **Metadata, Not Payloads:** When cloud sync is active, Flowstride routes only execution metadata to your team's dashboard (e.g., Workspace ID, execution duration, total steps, and pass/fail tallies). **Zero bytes** of request headers, bearer tokens, or JSON bodies are ever transmitted to Flowstride Cloud.
95
-
96
- - **Deep Debugging Stays Local:** The rich, interactive visual dashboard, including the JSON explorer and API Replay tools, runs on a dynamic, locally bound port on your machine. You get the UI of a modern cloud app with the security of a local binary.
97
-
98
- ### CI/CD Leak Protection & Terminal Hygiene
99
-
100
- Standard testing frameworks do dump failing API requests, and response bodies directly into the terminal so developers can debug them. In a CI/CD environment, this is a massive security liability, as those secrets are permanently burned into plaintext Jenkins, GitHub Actions, or GitLab CI logs.
101
-
102
- Flowstride completely neutralizes this threat by architecturally separating execution output from diagnostic telemetry:
102
+ ## Execution data and Cloud integration
103
103
 
104
- 1. **Clean Standard Output (`stdout`):** Flowstride's terminal output only displays high-level, human-readable BDD execution steps (e.g., `✔ Given "User lands on home page"` or `✖ Action Failed`). No JSON request bodies, `cURL` strings, or bearer tokens are ever printed to the terminal console.
104
+ Tests run on your machine or CI runner. Native CI writes reports, screenshots,
105
+ videos and attachments to the configured local artifact directory. Interactive
106
+ API diagnostics are available in the local dashboard. These outputs can contain
107
+ application data or credentials; automatic redaction is not guaranteed.
105
108
 
106
- 2. **WebSocket Isolation:** All sensitive raw network data (payloads, headers, and responses) is emitted _exclusively_ over a secure, ephemeral WebSocket connection directly to the developer's local UI dashboard.
109
+ Cloud login/licensing, FlowMail OTP retrieval and explicitly enabled Cloud
110
+ reporting contact their configured services. Native CI leaves Cloud reporting
111
+ off unless `cloudSync: true` is explicitly configured. Existing Cloud reporting
112
+ and storage limitations remain documented in the preview release notes.
107
113
 
108
- 3. **Total Evaporation:** Because CI/CD runners only capture `stdout` text and do not connect to local WebSockets, your highly sensitive diagnostic data simply evaporates the moment the pipeline execution ends. Your pipeline logs remain 100% free of plaintext secrets.
114
+ Use test credentials, keep runtime state and artifacts out of source control,
115
+ and choose CI artifact access and retention appropriate to the test data.
109
116
 
110
117
  ---
111
118
 
@@ -0,0 +1,40 @@
1
+ # Flowstride 1.0.20-rc.2 — npm preview
2
+
3
+ Publish channel: `next`. This preview does not move `latest` and does not publish
4
+ or replace the private Pro engine. The matching Pro candidate is `1.0.15-rc.1`.
5
+
6
+ ## Changes since rc.1
7
+
8
+ - Require Node.js 22.12.0+ and align package, lockfile, CLI version and guides.
9
+ - Add an explicit preview publication configuration targeting the public npm
10
+ registry and `next`, plus matching CLI-version/engine metadata checks.
11
+ - Clarify preview installation, native CI/game scope and Cloud/Pro limitations.
12
+ - Replace absolute privacy guarantees in the README with the actual data flow.
13
+ - Rebuild the engine from the merged OSS source; reuse the unchanged dashboard
14
+ from the checksum-verified rc.1 archive. No runner or game behaviour changes.
15
+
16
+ ## Native features inherited from rc.1
17
+
18
+ `flowstride run --ci`, `flowstride install [--with-deps]`,
19
+ `flowstride ci init github`, supported plugin/game SDKs, canvas/Three.js bridges,
20
+ Chromium desktop/phone/landscape profiles, JSON/JUnit reports and failure media,
21
+ safe file-chooser handling and interrupted-run cleanup.
22
+
23
+ ## Known limits
24
+
25
+ Cloud login still downloads Pro 1.0.14, which does not support these native
26
+ commands. Test OSS alone, or authenticate first and then manually install the
27
+ matching local Pro candidate. Project dependency installation can remove a
28
+ no-save Pro package; install Pro afterward. Public-CLI native fallback remains
29
+ available for absent, broken or incompatible Pro engines.
30
+
31
+ Live Cloud reporting and video-storage issues are unresolved. Native CI keeps
32
+ reporting off by default; this preview does not enable the retired managed
33
+ runner. Application-specific game rules/fixtures remain application code.
34
+ Physical phones, Safari/Firefox and other game engines are not qualified.
35
+
36
+ ## Publication
37
+
38
+ Only the verified OSS archive should be published. Use the `next` tag explicitly;
39
+ then verify `flowstride@next` and reinstall from npm in a disposable consumer.
40
+ No npm publication is performed while preparing this candidate.
@@ -2,6 +2,7 @@ import { Page } from "playwright";
2
2
  import { FlowstrideConfig } from "../../types";
3
3
  export declare class WebAdapter {
4
4
  private config;
5
+ pageErrors: string[];
5
6
  private browser;
6
7
  private context;
7
8
  private page;
@@ -25,6 +26,8 @@ export declare class WebAdapter {
25
26
  synthesizeCurl(method: string, url: string, headers: Record<string, string>, postData: string | null): string;
26
27
  private checkAmbiguity;
27
28
  close(targetText: string): Promise<void>;
29
+ extractText(selector: string, elementType?: string): Promise<string>;
30
+ extractValue(selector: string, elementType?: string): Promise<string>;
28
31
  click(selector: string, elementType?: string): Promise<void>;
29
32
  hover(selector: string, elementType?: string): Promise<void>;
30
33
  type(selector: string, value: string, elementType?: string): Promise<void>;
@@ -43,6 +46,7 @@ export declare class WebAdapter {
43
46
  expectValue(selector: string, val: string, elementType?: string): Promise<void>;
44
47
  expectTranscript(expectedText: string): Promise<void>;
45
48
  getAttribute(selector: string, attr: string, elementType?: string): Promise<string | null>;
49
+ private translateCoordinateSelector;
46
50
  private isRawCssSelector;
47
51
  private buildHeuristicSelector;
48
52
  private getLocator;
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.WebAdapter = void 0;
40
+ const playwright_1 = require("playwright");
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  const playwright_extra_1 = require("playwright-extra");
@@ -44,6 +45,7 @@ const puppeteer_extra_plugin_stealth_1 = __importDefault(require("puppeteer-extr
44
45
  playwright_extra_1.chromium.use((0, puppeteer_extra_plugin_stealth_1.default)());
45
46
  class WebAdapter {
46
47
  config;
48
+ pageErrors = [];
47
49
  browser = null;
48
50
  context = null;
49
51
  page = null;
@@ -62,33 +64,36 @@ class WebAdapter {
62
64
  ? [
63
65
  "--use-fake-ui-for-media-stream",
64
66
  "--use-fake-device-for-media-stream",
65
- "--disable-blink-features=AutomationControlled", // Cloudflare Mask
67
+ "--disable-blink-features=AutomationControlled",
66
68
  ]
67
69
  : [
68
70
  "--window-size=1280,800",
69
71
  "--use-fake-ui-for-media-stream",
70
72
  "--use-fake-device-for-media-stream",
71
- "--disable-blink-features=AutomationControlled", // Cloudflare Mask
73
+ "--disable-blink-features=AutomationControlled",
72
74
  ];
73
75
  if (audioInjectionPath) {
74
76
  launchArgs.push(`--use-file-for-fake-audio-capture=${audioInjectionPath}`);
75
77
  }
76
78
  this.browser = await playwright_extra_1.chromium.launch({
77
79
  headless: this.config.headless,
80
+ // Native CI finalizes videos before closing the browser on cancellation.
81
+ ...(this.config.ci ? { handleSIGINT: false, handleSIGTERM: false } : {}),
78
82
  args: launchArgs,
79
83
  });
80
84
  const videoDir = path.join(process.cwd(), ".flowstride", "videos");
81
85
  if (!fs.existsSync(videoDir))
82
86
  fs.mkdirSync(videoDir, { recursive: true });
83
- const customUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
87
+ const profile = this.config.device === 'phone' || this.config.device === 'landscape'
88
+ ? { ...playwright_1.devices['Pixel 7'], viewport: this.config.device === 'landscape' ? { width: 844, height: 390 } : { width: 390, height: 844 } }
89
+ : { viewport: { width: 1280, height: 800 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' };
90
+ const viewport = this.config.viewport || profile.viewport;
84
91
  this.context = await this.browser.newContext({
85
- viewport: null,
86
- userAgent: customUserAgent,
87
- recordVideo: {
88
- dir: videoDir,
89
- size: { width: 1280, height: 800 },
90
- },
92
+ ...profile, viewport,
93
+ ...(this.config.recordVideo === false ? {} : { recordVideo: { dir: videoDir, size: viewport } }),
91
94
  });
95
+ this.pageErrors = [];
96
+ this.context.on('page', page => page.on('pageerror', error => this.pageErrors.push(error.message)));
92
97
  this.context.on("dialog", async (dialog) => {
93
98
  if (this.dialogHandler) {
94
99
  await this.dialogHandler(dialog);
@@ -390,6 +395,18 @@ class WebAdapter {
390
395
  await this.page.mouse.up();
391
396
  await this.page.waitForTimeout(300);
392
397
  }
398
+ async extractText(selector, elementType) {
399
+ const loc = await this.getLocator(selector, elementType, false, false);
400
+ const targetLoc = await this.checkAmbiguity(loc, selector);
401
+ await targetLoc.waitFor({ state: "visible" });
402
+ return (await targetLoc.innerText()) || "";
403
+ }
404
+ async extractValue(selector, elementType) {
405
+ const loc = await this.getLocator(selector, elementType, false, false);
406
+ const targetLoc = await this.checkAmbiguity(loc, selector);
407
+ await targetLoc.waitFor({ state: "visible" });
408
+ return (await targetLoc.inputValue()) || "";
409
+ }
393
410
  async click(selector, elementType) {
394
411
  const loc = await this.getLocator(selector, elementType, false, false);
395
412
  const targetLoc = await this.checkAmbiguity(loc, selector);
@@ -402,7 +419,6 @@ class WebAdapter {
402
419
  const targetLoc = await this.checkAmbiguity(loc, selector);
403
420
  await targetLoc.waitFor({ state: "visible" });
404
421
  await targetLoc.scrollIntoViewIfNeeded();
405
- // 1. Accessibility WAI-ARIA Hack (Focus)
406
422
  await targetLoc.focus({ timeout: 3000 }).catch(() => { });
407
423
  await targetLoc
408
424
  .evaluate((node) => {
@@ -410,21 +426,17 @@ class WebAdapter {
410
426
  node.focus();
411
427
  })
412
428
  .catch(() => { });
413
- // 2. Get Exact Center Coordinates for Physical Move
414
429
  let box = null;
415
430
  try {
416
431
  box = await targetLoc.boundingBox({ timeout: 3000 });
417
432
  }
418
- catch (e) { } // Catch in case it mutated instantly during focus
433
+ catch (e) { }
419
434
  if (box) {
420
435
  const centerX = box.x + box.width / 2;
421
436
  const centerY = box.y + box.height / 2;
422
- // Physically drag the mouse to the element to satisfy Hover Intent APIs
423
437
  await this.page.mouse.move(centerX, centerY, { steps: 5 });
424
438
  }
425
- // 3. Playwright Native Hover Backup (Caught to prevent mutation crashes)
426
439
  await targetLoc.hover({ force: true, timeout: 3000 }).catch(() => { });
427
- // 4. The Ultimate Wakeup Call: Synthesize Full Coordinate-Loaded Events
428
440
  await targetLoc
429
441
  .evaluate((node) => {
430
442
  const rect = node.getBoundingClientRect();
@@ -450,11 +462,9 @@ class WebAdapter {
450
462
  });
451
463
  })
452
464
  .catch(() => { });
453
- // 5. We perform a programmatic click that won't trigger W3C navigation if handled by React toggles
454
465
  await targetLoc
455
466
  .click({ delay: 50, force: true, timeout: 3000 })
456
467
  .catch(() => { });
457
- // 6. The Paint Buffer
458
468
  await this.page.waitForTimeout(600);
459
469
  }
460
470
  async type(selector, value, elementType) {
@@ -474,10 +484,10 @@ class WebAdapter {
474
484
  await targetLoc.evaluate((node) => node.blur());
475
485
  let actualValue = "";
476
486
  try {
477
- actualValue = await targetLoc.inputValue({ timeout: 500 });
487
+ actualValue = await targetLoc.inputValue({ timeout: this.config.timeout });
478
488
  }
479
489
  catch {
480
- actualValue = (await targetLoc.textContent({ timeout: 500 })) || "";
490
+ actualValue = (await targetLoc.textContent({ timeout: this.config.timeout })) || "";
481
491
  }
482
492
  const cleanActual = actualValue.replace(/[^a-zA-Z0-9]/g, "");
483
493
  const cleanExpected = value.replace(/[^a-zA-Z0-9]/g, "");
@@ -609,11 +619,10 @@ class WebAdapter {
609
619
  throw new Error(`File Upload Error: Could not find the file at "${absolutePath}". Please ensure the file exists in your project directory.`);
610
620
  }
611
621
  try {
612
- const fileChooserPromise = this.page.waitForEvent("filechooser", {
613
- timeout: 5000,
614
- });
615
- await targetLoc.click();
616
- const fileChooser = await fileChooserPromise;
622
+ const [fileChooser] = await Promise.all([
623
+ this.page.waitForEvent('filechooser', { timeout: this.config.timeout }),
624
+ targetLoc.click({ timeout: this.config.timeout }),
625
+ ]);
617
626
  await fileChooser.setFiles(absolutePath);
618
627
  }
619
628
  catch (e) {
@@ -902,7 +911,7 @@ class WebAdapter {
902
911
  .replace(/[^\w\s]/g, "")
903
912
  .replace(/\s+/g, " ");
904
913
  if (cleanBody.includes(cleanExpected)) {
905
- return; // Assertion passed!
914
+ return;
906
915
  }
907
916
  await this.page.waitForTimeout(500);
908
917
  }
@@ -912,9 +921,32 @@ class WebAdapter {
912
921
  const loc = await this.getLocator(selector, elementType, true, true);
913
922
  return await loc.first().getAttribute(attr);
914
923
  }
924
+ translateCoordinateSelector(selector) {
925
+ const cleanSelector = selector.replace(/^['"]|['"]$/g, "").trim();
926
+ if (!cleanSelector)
927
+ return null;
928
+ const parts = cleanSelector.split(/\s+/);
929
+ const validTags = ["table", "tr", "th", "td"];
930
+ const isCoordinatePath = parts.every((part) => validTags.some((tag) => part.toLowerCase().startsWith(tag)));
931
+ if (!isCoordinatePath)
932
+ return null;
933
+ let translated = "";
934
+ for (let i = 0; i < parts.length; i++) {
935
+ const part = parts[i];
936
+ const match = part.match(/^([a-z]+)(?:\[(\d+)\])?$/i);
937
+ if (!match)
938
+ return null;
939
+ const tag = match[1];
940
+ const index = match[2] ? ` >> nth=${match[2]}` : "";
941
+ translated += (i > 0 ? " >> " : "") + tag + index;
942
+ }
943
+ return translated;
944
+ }
915
945
  isRawCssSelector(selector) {
916
946
  if (/\b(near|above|under|below|leftOf|rightOf|inside|in)\s+['"]/i.test(selector))
917
947
  return false;
948
+ if (selector.includes(">> nth="))
949
+ return true;
918
950
  if (/^[#\.\[]/.test(selector))
919
951
  return true;
920
952
  if (/^[a-zA-Z0-9]+\s*[\.\#:][a-zA-Z0-9_\-]+/.test(selector))
@@ -954,6 +986,9 @@ class WebAdapter {
954
986
  "iframe",
955
987
  "img",
956
988
  "svg",
989
+ "tr",
990
+ "th",
991
+ "td",
957
992
  ];
958
993
  if (htmlTags.includes(selector.toLowerCase()))
959
994
  return true;
@@ -995,27 +1030,35 @@ class WebAdapter {
995
1030
  return `:is(${attributes}, ${partialActionable})`;
996
1031
  }
997
1032
  async getLocator(rawSelector, elementType, isForceAction = false, isAssertionOrAnchor = false, fuzzy = false) {
998
- if (this.isRawCssSelector(rawSelector.trim())) {
999
- let loc = this.page.locator(rawSelector);
1033
+ const translatedRaw = this.translateCoordinateSelector(rawSelector);
1034
+ const processingSelector = translatedRaw || rawSelector;
1035
+ if (this.isRawCssSelector(processingSelector.trim())) {
1036
+ let loc = this.page.locator(processingSelector);
1000
1037
  if (!isForceAction)
1001
1038
  loc = loc.filter({ visible: true });
1002
1039
  return loc;
1003
1040
  }
1004
- let target = rawSelector;
1041
+ let target = processingSelector;
1005
1042
  let relation = "";
1006
1043
  let anchor = "";
1007
1044
  const spatialRegex = /\b(near|above|under|below|leftOf|rightOf|inside|in)\s+(['"].*?['"])/i;
1008
- const match = rawSelector.match(spatialRegex);
1045
+ const match = processingSelector.match(spatialRegex);
1009
1046
  if (match) {
1010
- target = rawSelector.substring(0, match.index).trim();
1047
+ target = processingSelector.substring(0, match.index).trim();
1011
1048
  relation = match[1].toLowerCase();
1012
1049
  anchor = match[2];
1013
1050
  }
1051
+ const translatedTarget = this.translateCoordinateSelector(target);
1052
+ if (translatedTarget) {
1053
+ target = translatedTarget;
1054
+ }
1014
1055
  let arrayIndex = null;
1015
- const indexMatch = target.match(/\[(\d+)\]/);
1016
- if (indexMatch) {
1017
- arrayIndex = parseInt(indexMatch[1], 10);
1018
- target = target.replace(indexMatch[0], "").trim();
1056
+ if (!translatedTarget) {
1057
+ const indexMatch = target.match(/\[(\d+)\]/);
1058
+ if (indexMatch) {
1059
+ arrayIndex = parseInt(indexMatch[1], 10);
1060
+ target = target.replace(indexMatch[0], "").trim();
1061
+ }
1019
1062
  }
1020
1063
  const targetClean = target.replace(/^['"]|['"]$/g, "");
1021
1064
  const targetIsRaw = this.isRawCssSelector(targetClean);
@@ -1025,10 +1068,12 @@ class WebAdapter {
1025
1068
  let alternativeSelector = "";
1026
1069
  if (relation && anchor) {
1027
1070
  const anchorClean = anchor.replace(/^['"]|['"]$/g, "");
1028
- const anchorIsRaw = this.isRawCssSelector(anchorClean);
1071
+ const translatedAnchor = this.translateCoordinateSelector(anchorClean);
1072
+ const finalAnchorClean = translatedAnchor || anchorClean;
1073
+ const anchorIsRaw = this.isRawCssSelector(finalAnchorClean);
1029
1074
  const anchorSelector = anchorIsRaw
1030
- ? anchorClean
1031
- : this.buildHeuristicSelector(anchorClean, undefined, true, fuzzy);
1075
+ ? finalAnchorClean
1076
+ : this.buildHeuristicSelector(finalAnchorClean, undefined, true, fuzzy);
1032
1077
  const anchorLoc = this.page.locator(anchorSelector).first();
1033
1078
  try {
1034
1079
  if ((await anchorLoc.count()) > 0) {
@@ -1055,7 +1100,9 @@ class WebAdapter {
1055
1100
  break;
1056
1101
  case "inside":
1057
1102
  case "in":
1058
- alternativeSelector = `${anchorSelector}:has-text("${targetClean}")`;
1103
+ if (!anchorSelector.includes("nth=")) {
1104
+ alternativeSelector = `${anchorSelector}:has-text("${targetClean}")`;
1105
+ }
1059
1106
  finalSelector = `${anchorSelector} >> ${finalSelector}`;
1060
1107
  break;
1061
1108
  }
@@ -0,0 +1,3 @@
1
+ import type { FlowstrideConfig } from '../types';
2
+ export declare function isCIEnvironment(): boolean;
3
+ export declare function loadNativeConfig(root: string, options: Record<string, any>): FlowstrideConfig;