flowstride 1.0.19 → 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.
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runNativeCI = runNativeCI;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_child_process_1 = require("node:child_process");
10
+ const promises_1 = require("node:timers/promises");
11
+ const lexer_1 = require("../parser/lexer");
12
+ const parser_1 = require("../parser/parser");
13
+ function listFlows(target) {
14
+ const stat = node_fs_1.default.statSync(target);
15
+ if (stat.isFile())
16
+ return target.endsWith('.flow') ? [target] : [];
17
+ return node_fs_1.default.readdirSync(target).filter(name => name.endsWith('.flow')).sort().map(name => node_path_1.default.join(target, name));
18
+ }
19
+ function safeName(name) { return name.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 150); }
20
+ function xml(value) { return String(value).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').replace(/[<>&"']/g, char => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&apos;' }[char])); }
21
+ async function runNativeCI(worker, orchestrator, target, config, engine) {
22
+ const started = Date.now();
23
+ const artifactRoot = config.artifactsDir;
24
+ node_fs_1.default.mkdirSync(artifactRoot, { recursive: true });
25
+ const directory = node_fs_1.default.mkdtempSync(node_path_1.default.join(artifactRoot, 'run-'));
26
+ config.artifactsDir = directory;
27
+ const steps = new Map();
28
+ const completed = new Set();
29
+ const errors = [];
30
+ let expected = 0;
31
+ let files = [];
32
+ let server;
33
+ let interrupted = false;
34
+ const interrupt = () => { interrupted = true; worker.abort(); orchestrator.abort?.(); };
35
+ const event = (data) => {
36
+ if (!data?.payload)
37
+ return;
38
+ const p = data.payload;
39
+ if (data.type === 'LOG_EVENT' && p.status === 'error') {
40
+ const active = [...steps.values()].reverse().find(step => step.fileName === p.fileName && step.status === 'running');
41
+ if (active)
42
+ active.error = p.details?.rawError || p.summary;
43
+ }
44
+ if (data.type === 'STEP_UPDATE' || data.type === 'STEP_ADD') {
45
+ const key = `${p.fileName}:${p.id}`;
46
+ steps.set(key, { ...steps.get(key), ...p, screenshot: undefined });
47
+ if (p.status === 'failed' && typeof p.screenshot === 'string') {
48
+ const match = p.screenshot.match(/^data:image\/(png|jpeg);base64,(.+)$/s) || (/^[A-Za-z0-9+/=\r\n]+$/.test(p.screenshot) ? ['', 'jpeg', p.screenshot] : null);
49
+ if (match)
50
+ node_fs_1.default.writeFileSync(node_path_1.default.join(directory, `${safeName(p.fileName)}-${safeName(p.id)}.${match[1] === 'jpeg' ? 'jpg' : 'png'}`), Buffer.from(match[2], 'base64'));
51
+ }
52
+ }
53
+ if (data.type === 'FILE_COMPLETE' || data.type === 'FILE_FAILED')
54
+ completed.add(p.fileName);
55
+ if (data.type === 'FILE_FAILED' && ![...steps.values()].some(step => step.fileName === p.fileName && step.status === 'failed'))
56
+ errors.push(`${p.fileName}: ${p.error || 'Execution failed'}`);
57
+ if (data.type === 'BROWSER_ERROR' && config.browserErrors !== 'record')
58
+ errors.push(`Browser: ${p.message}`);
59
+ if (data.type === 'WORKER_ERROR')
60
+ errors.push(p.message || 'Worker failed');
61
+ };
62
+ // Modern orchestrators forward child-process events here; the worker listener
63
+ // also supports older compatible Pro packages. Step maps make duplicates harmless.
64
+ config.onRunEvent = event;
65
+ if (orchestrator.nativeEventProtocol !== 1)
66
+ worker.on('worker_event', event);
67
+ process.on('SIGINT', interrupt);
68
+ process.on('SIGTERM', interrupt);
69
+ try {
70
+ files = listFlows(target);
71
+ if (!files.length)
72
+ throw new Error('No .flow tests found.');
73
+ for (const file of files) {
74
+ const features = new parser_1.Parser(new lexer_1.Lexer(node_fs_1.default.readFileSync(file, 'utf8')).tokenise()).parse();
75
+ expected += features.reduce((n, feature) => n + feature.scenarios.reduce((m, scenario) => m + scenario.steps.length, 0), 0);
76
+ }
77
+ if (!expected)
78
+ throw new Error('No executable test steps found.');
79
+ if (config.webServer) {
80
+ const settings = config.webServer;
81
+ const probe = async () => { try {
82
+ return (await fetch(settings.url, { signal: AbortSignal.timeout(1000) })).ok;
83
+ }
84
+ catch {
85
+ return false;
86
+ } };
87
+ if (await probe())
88
+ throw new Error('webServer readiness URL is already serving; refusing to test an unowned server.');
89
+ const output = node_fs_1.default.openSync(node_path_1.default.join(directory, 'web-server.log'), 'a');
90
+ try {
91
+ server = (0, node_child_process_1.spawn)(settings.command, { cwd: settings.cwd, shell: true, detached: process.platform !== 'win32', stdio: ['ignore', output, output] });
92
+ }
93
+ finally {
94
+ node_fs_1.default.closeSync(output);
95
+ }
96
+ let spawnError;
97
+ server.on('error', error => { spawnError = error; });
98
+ const deadline = Date.now() + (settings.timeout ?? 30000);
99
+ while (!(await probe())) {
100
+ if (interrupted)
101
+ throw new Error('Run interrupted during server startup.');
102
+ if (spawnError)
103
+ throw spawnError;
104
+ if (server.exitCode !== null || server.signalCode !== null)
105
+ throw new Error('webServer exited before readiness.');
106
+ if (Date.now() > deadline)
107
+ throw new Error('webServer readiness timed out.');
108
+ await (0, promises_1.setTimeout)(100);
109
+ }
110
+ }
111
+ if (interrupted)
112
+ throw new Error('Run interrupted before execution.');
113
+ const ok = await orchestrator.start(target);
114
+ if (ok === false && ![...steps.values()].some(step => step.status === 'failed'))
115
+ errors.push('Engine reported a failed run.');
116
+ }
117
+ catch (error) {
118
+ errors.push(error instanceof Error ? error.message : String(error));
119
+ }
120
+ finally {
121
+ try {
122
+ await worker.shutdown();
123
+ }
124
+ catch (error) {
125
+ errors.push(`Cleanup failed: ${String(error)}`);
126
+ }
127
+ try {
128
+ await orchestrator.close?.();
129
+ }
130
+ catch (error) {
131
+ errors.push(`Orchestrator cleanup failed: ${String(error)}`);
132
+ }
133
+ if (server?.pid) {
134
+ const stop = (signal) => { try {
135
+ process.kill(process.platform === 'win32' ? server.pid : -server.pid, signal);
136
+ }
137
+ catch (error) {
138
+ if (error.code !== 'ESRCH')
139
+ errors.push(`Server cleanup: ${error.message}`);
140
+ } };
141
+ stop('SIGTERM');
142
+ await Promise.race([new Promise(resolve => server.once('exit', () => resolve())), (0, promises_1.setTimeout)(1000)]);
143
+ // Terminate remaining descendants even if the shell exited first.
144
+ stop('SIGKILL');
145
+ }
146
+ worker.off('worker_event', event);
147
+ delete config.onRunEvent;
148
+ process.off('SIGINT', interrupt);
149
+ process.off('SIGTERM', interrupt);
150
+ }
151
+ if (interrupted)
152
+ errors.push('Run interrupted.');
153
+ const results = [...steps.values()];
154
+ const terminal = results.filter(step => ['passed', 'failed', 'skipped', 'not_tested'].includes(step.status));
155
+ const incomplete = Math.max(0, expected - terminal.length);
156
+ for (const file of files)
157
+ if (!completed.has(node_path_1.default.basename(file)))
158
+ errors.push(`Incomplete file: ${node_path_1.default.basename(file)}`);
159
+ if (results.some(step => !['passed', 'failed', 'skipped', 'not_tested'].includes(step.status)))
160
+ errors.push('Unfinished test steps.');
161
+ if (terminal.length > expected)
162
+ errors.push('Engine produced an unexpected step count.');
163
+ const metrics = { total: expected, passed: results.filter(s => s.status === 'passed').length,
164
+ failed: results.filter(s => s.status === 'failed').length, skipped: results.filter(s => ['skipped', 'not_tested'].includes(s.status)).length, incomplete };
165
+ const result = { protocol: 1, engine, status: interrupted ? 'interrupted' : errors.length || metrics.failed || incomplete ? 'failed' : 'passed',
166
+ duration: Date.now() - started, artifactsDir: directory, files: files.map(file => node_path_1.default.basename(file)), steps: results, errors, metrics };
167
+ // Existing workers finalize videos into these locations. Copy only run files,
168
+ // never the project directory or credentials.
169
+ if (config.recordVideo !== false)
170
+ for (const file of files) {
171
+ for (const folder of ['artifacts', 'mock-cloud']) {
172
+ const video = node_path_1.default.join(process.cwd(), '.flowstride', folder, node_path_1.default.basename(file, '.flow') + '.webm');
173
+ if (node_fs_1.default.existsSync(video) && node_fs_1.default.statSync(video).mtimeMs >= started) {
174
+ node_fs_1.default.copyFileSync(video, node_path_1.default.join(directory, node_path_1.default.basename(video)));
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ config.artifactsDir = artifactRoot;
180
+ node_fs_1.default.writeFileSync(node_path_1.default.join(artifactRoot, 'summary.json'), JSON.stringify(result, null, 2));
181
+ const cases = results.map(step => `<testcase classname="${xml(step.fileName)}" name="${xml(step.description || step.id)}">${step.status === 'failed' ? `<failure message="${xml(step.error || 'Test step failed')}"/>` : ['skipped', 'not_tested'].includes(step.status) ? '<skipped/>' : ''}</testcase>`);
182
+ for (const error of errors)
183
+ cases.push(`<testcase name="execution"><error message="${xml(error)}"/></testcase>`);
184
+ if (incomplete)
185
+ cases.push(`<testcase name="completion"><failure message="${incomplete} incomplete steps"/></testcase>`);
186
+ node_fs_1.default.writeFileSync(node_path_1.default.join(artifactRoot, 'junit.xml'), `<?xml version="1.0"?><testsuite name="Flowstride" tests="${cases.length}" failures="${metrics.failed + (incomplete ? 1 : 0)}" errors="${errors.length}" skipped="${metrics.skipped}">${cases.join('')}</testsuite>`);
187
+ if (process.env.GITHUB_STEP_SUMMARY)
188
+ node_fs_1.default.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `\n### Flowstride · ${config.device}\nResult: **${result.status}** (${engine})\n\nPassed: ${metrics.passed}; failed: ${metrics.failed}; skipped: ${metrics.skipped}; incomplete: ${metrics.incomplete}.\n`);
189
+ return result;
190
+ }
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runAction = runAction;
37
+ const config_1 = require("../../ci/config");
38
+ const runtime_1 = require("../../ci/runtime");
37
39
  const fs = __importStar(require("fs"));
38
40
  const path = __importStar(require("path"));
39
41
  const dotenv = __importStar(require("dotenv"));
@@ -69,12 +71,8 @@ async function runAction(targetPath, options) {
69
71
  console.log(`[Environment]: Loaded default .env file`);
70
72
  }
71
73
  }
72
- const isCI = !!(process.env.CI ||
73
- process.env.GITHUB_ACTIONS ||
74
- process.env.GITLAB_CI ||
75
- process.env.CIRCLECI ||
76
- process.env.JENKINS_URL ||
77
- process.env.FLOWSTRIDE_API_KEY);
74
+ const config = (0, config_1.loadNativeConfig)(root, options);
75
+ const isCI = !!config.ci;
78
76
  if (isCI) {
79
77
  console.log("\n[CI/CD]: Environment detected. Engaging Smart Automation Behaviors...");
80
78
  }
@@ -84,39 +82,13 @@ async function runAction(targetPath, options) {
84
82
  process.exit(1);
85
83
  }
86
84
  console.log(`\n[Flowstride]: Starting execution suite at "${resolvedPath}"...`);
87
- const configPath = path.resolve(root, "flowstride.config.json");
88
- let localConfig = {};
89
- if (fs.existsSync(configPath)) {
90
- try {
91
- const fileContent = fs.readFileSync(configPath, "utf-8");
92
- localConfig = JSON.parse(fileContent);
93
- console.log(`[Config]: Loaded project configuration from flowstride.config.json`);
94
- }
95
- catch (e) {
96
- console.warn(`[Warning]: Could not parse flowstride.config.json: ${e.message}`);
97
- }
98
- }
99
- const config = {
100
- headless: isCI
101
- ? true
102
- : options.headed
103
- ? false
104
- : (localConfig.headless ?? true),
105
- timeout: localConfig.timeout ?? 30000,
106
- bail: options.bail ?? localConfig.bail ?? false,
107
- baseUrl: process.env.BASE_URL ?? localConfig.baseUrl,
108
- defaultDialogBehavior: isCI
109
- ? "dismiss"
110
- : (localConfig.defaultDialogBehavior ?? "dismiss"),
111
- pluginsDir: localConfig.pluginsDir,
112
- };
113
85
  if (isCI) {
114
86
  console.log(` > Headless Mode: ${config.headless ? "Forced ON" : "OFF"}`);
115
87
  console.log(` > Dialog Protection: ${(config.defaultDialogBehavior || "dismiss").toUpperCase()}`);
116
88
  console.log(` > Artifact Auto-Staging: ENABLED\n`);
117
89
  }
118
- let workerCount = options.workers ? parseInt(options.workers, 10) : 1;
119
- if (isNaN(workerCount) || workerCount < 1) {
90
+ let workerCount = options.workers ? Number(options.workers) : 1;
91
+ if (!Number.isSafeInteger(workerCount) || workerCount < 1) {
120
92
  console.error(`\n[Error]: Invalid worker count "${options.workers}". Must be an integer greater than 0.`);
121
93
  process.exit(1);
122
94
  }
@@ -126,6 +98,8 @@ async function runAction(targetPath, options) {
126
98
  console.log(`[Warning]: Auto-capping to ${maxSafeWorkers} workers to prevent system thrashing and crashes.\n`);
127
99
  workerCount = maxSafeWorkers;
128
100
  }
101
+ if (config.manifest?.maxWorkers !== undefined && config.manifest.maxWorkers !== 'unlimited')
102
+ workerCount = Math.min(workerCount, config.manifest.maxWorkers);
129
103
  const web = new playwright_1.WebAdapter(config);
130
104
  const api = new undici_1.ApiAdapter(config);
131
105
  const store = new temporary_store_1.TemporaryStore();
@@ -144,6 +118,8 @@ async function runAction(targetPath, options) {
144
118
  try {
145
119
  const proWorkerModule = await import((0, url_1.pathToFileURL)(proWorkerPath).href);
146
120
  const proOrchModule = await import((0, url_1.pathToFileURL)(proOrchPath).href);
121
+ if (config.ci && proOrchModule.FlowOrchestrator.nativeCIProtocol !== 1)
122
+ throw new Error('Installed Pro engine does not support native CI protocol 1; install the matching Pro candidate.');
147
123
  ActiveFlowWorker = proWorkerModule.FlowWorker;
148
124
  ActiveFlowOrchestrator = proOrchModule.FlowOrchestrator;
149
125
  engineMode = "Enterprise (Pro)";
@@ -155,6 +131,11 @@ async function runAction(targetPath, options) {
155
131
  console.log(`\n[Engine]: Booting Flowstride in ${engineMode} mode...`);
156
132
  const worker = new ActiveFlowWorker(config, web, api, store, sessions, pluginManager);
157
133
  const orchestrator = new ActiveFlowOrchestrator(worker, workerCount, config);
134
+ if (isCI) {
135
+ const result = await (0, runtime_1.runNativeCI)(worker, orchestrator, resolvedPath, config, engineMode);
136
+ console.log(`[Flowstride]: ${result.status}; reports: ${config.artifactsDir}`);
137
+ process.exit(result.status === 'passed' ? 0 : result.status === 'interrupted' ? 130 : 1);
138
+ }
158
139
  try {
159
140
  await orchestrator.start(resolvedPath);
160
141
  console.log("\n[Flowstride]: Execution suite completed successfully.");
package/dist/cli/index.js CHANGED
@@ -37,10 +37,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
+ const config_1 = require("../ci/config");
41
+ const init_1 = require("../ci/init");
40
42
  const commander_1 = require("commander");
41
43
  const run_1 = require("./commands/run");
42
44
  const report_1 = require("./commands/report");
43
- const init_1 = require("./commands/init");
45
+ const init_2 = require("./commands/init");
44
46
  const open_1 = require("./commands/open");
45
47
  const fs = __importStar(require("fs"));
46
48
  const path = __importStar(require("path"));
@@ -54,12 +56,12 @@ const API_BASE = process.env.FLOWSTRIDE_API_URL || "https://cloud.flowstride.io"
54
56
  program
55
57
  .name("flowstride")
56
58
  .description("Flowstride: The Enterprise Flow-First Automation CLI")
57
- .version("1.0.19");
59
+ .version("1.0.20-rc.2");
58
60
  program
59
61
  .command("init")
60
62
  .description("Initialise a new Flowstride workspace in the current directory")
61
63
  .action(async () => {
62
- await (0, init_1.initAction)();
64
+ await (0, init_2.initAction)();
63
65
  });
64
66
  program
65
67
  .command("login")
@@ -271,11 +273,17 @@ program
271
273
  .option("--headed", "Run browser in headed mode")
272
274
  .option("--headless", "Run browser in headless mode")
273
275
  .option("--workers <number>", "Number of parallel workers to spawn (Pro feature)")
276
+ .option('--ci', 'Run without a dashboard and write CI reports')
277
+ .option('--device <profile>', 'desktop, phone, or landscape')
278
+ .option('--config <file>', 'Path to the Flowstride configuration')
279
+ .option('--env <name>', 'Environment profile')
274
280
  .action(async (targetPath, options) => {
275
281
  if (typeof targetPath === "object") {
276
282
  options = targetPath;
277
283
  targetPath = undefined;
278
284
  }
285
+ if (options.ci)
286
+ process.env.FLOWSTRIDE_NATIVE_CI = '1';
279
287
  let token = process.env.FLOWSTRIDE_TOKEN || null;
280
288
  let forceOS = false;
281
289
  const localCredentialsPath = path.join(process.cwd(), ".flowstride", "credentials.json");
@@ -326,6 +334,19 @@ program
326
334
  else if (response.ok) {
327
335
  const data = await response.json();
328
336
  const tier = data.tier.toUpperCase();
337
+ if ((0, config_1.loadNativeConfig)(process.cwd(), options).ci && fs.existsSync(nodeModulesPath)) {
338
+ const workspaceResponse = await fetch(`${API_BASE}/api/workspace/status`, {
339
+ headers: { Authorization: `Bearer ${token}` },
340
+ });
341
+ if (!workspaceResponse.ok)
342
+ forceOS = await handleOfflinePrompt();
343
+ else {
344
+ const workspaceData = await workspaceResponse.json();
345
+ options.manifest = workspaceData.manifest;
346
+ options.workspaceId = workspaceData.workspace?._id;
347
+ options.isCloudSyncActive = true;
348
+ }
349
+ }
329
350
  if (tier === "PERSONAL") {
330
351
  console.log(chalk_1.default.yellow(`🟡 [OS]: [${data.workspaceName}] (Tier: PERSONAL)`));
331
352
  }
@@ -344,7 +365,7 @@ program
344
365
  if (options.headed) {
345
366
  options.headless = false;
346
367
  }
347
- const resolvedTargetPath = targetPath ? targetPath : "./flows";
368
+ const resolvedTargetPath = targetPath || (0, config_1.loadNativeConfig)(process.cwd(), options).testDir;
348
369
  const strictUiFlowsDir = path.resolve(process.cwd(), resolvedTargetPath);
349
370
  if (!fs.existsSync(strictUiFlowsDir)) {
350
371
  console.error(`\n[ERROR]: Could not find ${strictUiFlowsDir}.`);
@@ -352,11 +373,17 @@ program
352
373
  process.exit(1);
353
374
  }
354
375
  options.forceOS = forceOS;
376
+ if (!forceOS && token) {
377
+ options.machineToken = token;
378
+ const file = fs.existsSync(localCredentialsPath) ? localCredentialsPath : globalCredentialsPath;
379
+ if (!options.workspaceId && fs.existsSync(file))
380
+ options.workspaceId = JSON.parse(fs.readFileSync(file, 'utf8')).workspaceId;
381
+ }
355
382
  await (0, run_1.runAction)(strictUiFlowsDir, options);
356
383
  });
357
384
  async function handleOfflinePrompt() {
358
385
  console.log(chalk_1.default.yellow("🛜 OFFLINE: Unable to reach Flowstride Cloud. Enterprise Sync paused."));
359
- if (process.env.CI || process.env.FLOWSTRIDE_TOKEN) {
386
+ if (process.env.CI || process.env.FLOWSTRIDE_NATIVE_CI || process.env.FLOWSTRIDE_TOKEN) {
360
387
  console.log(chalk_1.default.yellow("⚠️ CI/CD Environment: Automatically falling back to OS Engine."));
361
388
  return true;
362
389
  }
@@ -381,4 +408,16 @@ program
381
408
  .action(async () => {
382
409
  await (0, open_1.openAction)();
383
410
  });
411
+ program.command('install')
412
+ .description('Install Chromium matching this Flowstride engine')
413
+ .option('--with-deps', 'Install operating-system browser dependencies')
414
+ .action((options) => {
415
+ const { execFileSync } = require('node:child_process');
416
+ execFileSync(process.execPath, [path.join(path.dirname(require.resolve('playwright/package.json')), 'cli.js'), 'install', ...(options.withDeps ? ['--with-deps'] : []), 'chromium'], { stdio: 'inherit', env: { ...process.env, PLAYWRIGHT_SKIP_BROWSER_GC: '1' } });
417
+ });
418
+ program.command('ci')
419
+ .description('Set up native Flowstride CI')
420
+ .command('init <provider>')
421
+ .description('Generate a GitHub Actions workflow without overwriting existing files')
422
+ .action((provider) => (0, init_1.initCI)(provider));
384
423
  program.parse(process.argv);
@@ -0,0 +1,13 @@
1
+ /** Register observable state and explicitly test-only fixtures in a browser game. */
2
+ export interface GameBridgeOptions {
3
+ readState?: () => unknown;
4
+ locate?: (id: string) => {
5
+ x: number;
6
+ y: number;
7
+ };
8
+ testMode?: boolean;
9
+ fixtures?: Record<string, (data: unknown) => unknown>;
10
+ }
11
+ export declare function registerGame(options: GameBridgeOptions): {
12
+ dispose(): void;
13
+ };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerGame = registerGame;
4
+ function registerGame(options) {
5
+ if (typeof window === 'undefined')
6
+ throw new Error('Game bridges must be registered in the browser.');
7
+ if (typeof options.readState !== 'function' && typeof options.locate !== 'function')
8
+ throw new Error('A game bridge requires readState or locate.');
9
+ const bridge = { readState: options.readState, locate: options.locate,
10
+ testMode: options.testMode === true,
11
+ fixtures: options.testMode === true ? options.fixtures || {} : {} };
12
+ window.__FLOWSTRIDE_GAME__ = bridge;
13
+ return { dispose() { if (window.__FLOWSTRIDE_GAME__ === bridge)
14
+ delete window.__FLOWSTRIDE_GAME__; } };
15
+ }
@@ -0,0 +1,27 @@
1
+ import type { Page } from 'playwright';
2
+ import type { FlowstrideConfig } from '../types';
3
+ export declare class GameSDK {
4
+ private page;
5
+ private config;
6
+ private snapshots;
7
+ private states;
8
+ constructor(page: () => Page, config: FlowstrideConfig);
9
+ private timeout;
10
+ private bridgeReady;
11
+ private point;
12
+ private touch;
13
+ tap(options: any): Promise<void>;
14
+ drag(options: any): Promise<void>;
15
+ swipe(options: any): Promise<void>;
16
+ key(options: any): Promise<void>;
17
+ checkpoint(options: any): Promise<string>;
18
+ snapshot(options: any): Promise<void>;
19
+ private difference;
20
+ expectVisual(options: any): Promise<number>;
21
+ state(options?: any): Promise<any>;
22
+ expectState(options: any): Promise<void>;
23
+ snapshotState(options: any): Promise<void>;
24
+ expectPreserved(options: any): Promise<void>;
25
+ fixture(options: any): Promise<any>;
26
+ select(options: any): Promise<void>;
27
+ }