@maeris/maeris-player 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Maeris Player
2
+
3
+ Local Playwright-based runner for Maeris "Run in Browser".
4
+
5
+ ## Goals
6
+ - Replay recorder steps through the Chrome extension in a local browser window.
7
+ - Mirror the cloud runner as closely as possible (status updates, assertions, screenshots).
8
+ - Provide a lightweight CLI or UI shim that interfaces with the extension via WebSocket messages.
9
+
10
+ ## Architecture
11
+ 1. **CLI (`src/cli/run.js`)** starts a WebSocket server and waits for the UI to send `RUN_STEPS`, or replays a local JSON file when `--steps` is provided.
12
+ 2. **Runner core (`src/runner/index.js`)** normalizes steps, opens Playwright, streams `STEP_*`/`REPLAY_*` events over WebSocket (port 8090).
13
+ 3. **Playwright adapter (`src/runner/playwrightAdapter.js`)** performs clicks, inputs, navigations, assertions; captures screenshots on failure.
14
+ 4. **Event contract (`src/runner/events.js`)** defines the message names (`STEP_STARTED`, `STEP_PASSED`, `STEP_FAILED`, `NAVIGATION`, `REPLAY_COMPLETE`, etc.).
15
+
16
+ ## Running locally
17
+ ```bash
18
+ npm install
19
+
20
+ # listen for Run in Browser traffic from the web app (headed by default)
21
+ npm start
22
+
23
+ # replay a recorded JSON file
24
+ npm start -- --steps path/to/recorded.json --url https://demo-app/ --browser chromium
25
+ ```
26
+
27
+ ## Usage via npm
28
+ ```bash
29
+ npx @maeris/maeris-player
30
+ ```
31
+
32
+ This starts the local runner on `ws://localhost:8090`. Keep it running, then click **Run in Browser** in the Maeris UI.
33
+
34
+ ## HTTPS / WSS support (for prod without extension)
35
+ If you want to connect from an `https://` web app directly to the runner, start it with TLS:
36
+
37
+ ```bash
38
+ npx @maeris/maeris-player setup-cert
39
+ MAERIS_TLS_CERT=/path/to/cert.pem MAERIS_TLS_KEY=/path/to/key.pem npx @maeris/maeris-player
40
+ ```
41
+
42
+ The runner will listen on `wss://localhost:8090`. You will need to trust the certificate in your OS/browser.
43
+ # maeris-player
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@maeris/maeris-player",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Local Playwright-based runner for Maeris",
6
+ "scripts": {
7
+ "start": "node src/cli/run.js",
8
+ "serve": "node src/runner/server.js",
9
+ "lint": "echo \"No lint yet\"",
10
+ "test": "echo \"No tests yet\""
11
+ },
12
+ "bin": {
13
+ "maeris-player": "src/cli/run.js"
14
+ },
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "playwright": "^1.44.0",
18
+ "selfsigned": "^2.4.1",
19
+ "ws": "^8.13.0",
20
+ "yargs": "^20.0.0"
21
+ }
22
+ }
package/src/cli/run.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const yargs = require('yargs/yargs');
5
+ const { hideBin } = require('yargs/helpers');
6
+ const { Runner } = require('../runner');
7
+ const { setupCert, printInstructions } = require('./setup-cert');
8
+
9
+ const argv = yargs(hideBin(process.argv))
10
+ .option('steps', {
11
+ alias: 's',
12
+ type: 'string',
13
+ describe: 'Path to the recorded step payload (JSON format)',
14
+ })
15
+ .option('url', {
16
+ alias: 'u',
17
+ type: 'string',
18
+ describe: 'Starting URL for the run',
19
+ })
20
+ .option('browser', {
21
+ alias: 'b',
22
+ type: 'string',
23
+ default: 'chromium',
24
+ describe: 'Playwright browser to launch',
25
+ })
26
+ .option('headed', {
27
+ type: 'boolean',
28
+ default: true,
29
+ describe: 'Run browser in headed (visible) mode',
30
+ })
31
+ .option('run-id', {
32
+ alias: 'r',
33
+ type: 'string',
34
+ describe: 'External run identifier for tracking',
35
+ })
36
+ .option('serve', {
37
+ type: 'boolean',
38
+ default: true,
39
+ describe: 'Start the runner server and wait for UI commands',
40
+ })
41
+ .option('cert-dir', {
42
+ type: 'string',
43
+ describe: 'Directory to store TLS certs for setup-cert',
44
+ })
45
+ .help()
46
+ .parse();
47
+
48
+ function loadSteps(filePath) {
49
+ const resolved = path.resolve(filePath);
50
+ if (!fs.existsSync(resolved)) {
51
+ throw new Error(`Step file not found: ${resolved}`);
52
+ }
53
+ const raw = fs.readFileSync(resolved, 'utf8');
54
+ const parsed = JSON.parse(raw);
55
+ if (Array.isArray(parsed)) return parsed;
56
+ if (parsed && Array.isArray(parsed.steps)) return parsed.steps;
57
+ return [];
58
+ }
59
+
60
+ (async () => {
61
+ try {
62
+ if (argv._?.[0] === 'setup-cert') {
63
+ const result = await setupCert({ certDir: argv['cert-dir'] });
64
+ printInstructions(result);
65
+ return;
66
+ }
67
+
68
+ const steps = argv.steps ? loadSteps(argv.steps) : [];
69
+ const options = {
70
+ steps,
71
+ startUrl: argv.url,
72
+ browser: argv.browser,
73
+ headless: !argv.headed,
74
+ runId: argv['run-id'] || `local_${Date.now()}`,
75
+ };
76
+ console.log('Runner starting with', steps.length, 'steps');
77
+ const runner = new Runner(options);
78
+ runner.on('event', (payload) => {
79
+ console.log('[runner event]', payload.type, payload.stepIndex ?? '', payload.message || payload.url || '');
80
+ });
81
+ if (argv.serve) {
82
+ runner.startServer();
83
+ }
84
+ if (steps.length > 0) {
85
+ await runner.startRun({
86
+ steps,
87
+ startUrl: argv.url,
88
+ browser: argv.browser,
89
+ runId: options.runId,
90
+ headless: options.headless,
91
+ });
92
+ } else if (argv.serve) {
93
+ console.log('Waiting for UI RUN_STEPS command...');
94
+ } else {
95
+ console.log('No steps provided. Exiting.');
96
+ }
97
+ } catch (error) {
98
+ console.error('Runner failed:', error);
99
+ process.exit(1);
100
+ }
101
+ })();
@@ -0,0 +1,81 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const selfsigned = require('selfsigned');
5
+
6
+ function ensureDir(dir) {
7
+ fs.mkdirSync(dir, { recursive: true });
8
+ }
9
+
10
+ function writeFile(filepath, contents) {
11
+ fs.writeFileSync(filepath, contents, { encoding: 'utf8', mode: 0o600 });
12
+ }
13
+
14
+ function buildCertOptions() {
15
+ return {
16
+ keySize: 2048,
17
+ days: 3650,
18
+ algorithm: 'sha256',
19
+ extensions: [
20
+ { name: 'basicConstraints', cA: true },
21
+ { name: 'keyUsage', keyCertSign: true, digitalSignature: true, keyEncipherment: true },
22
+ { name: 'extKeyUsage', serverAuth: true },
23
+ {
24
+ name: 'subjectAltName',
25
+ altNames: [
26
+ { type: 2, value: 'localhost' },
27
+ { type: 7, ip: '127.0.0.1' }
28
+ ]
29
+ }
30
+ ]
31
+ };
32
+ }
33
+
34
+ function buildAttrs() {
35
+ return [
36
+ { name: 'commonName', value: 'maeris-player.local' },
37
+ { name: 'organizationName', value: 'Maeris' },
38
+ { name: 'organizationalUnitName', value: 'Local Runner' }
39
+ ];
40
+ }
41
+
42
+ async function setupCert({ certDir }) {
43
+ const baseDir = certDir || path.join(os.homedir(), '.maeris-player', 'certs');
44
+ ensureDir(baseDir);
45
+
46
+ const keyPath = path.join(baseDir, 'localhost.key.pem');
47
+ const certPath = path.join(baseDir, 'localhost.cert.pem');
48
+
49
+ const attrs = buildAttrs();
50
+ const opts = buildCertOptions();
51
+ const { private: key, cert } = selfsigned.generate(attrs, opts);
52
+
53
+ writeFile(keyPath, key);
54
+ writeFile(certPath, cert);
55
+
56
+ return { certPath, keyPath, baseDir };
57
+ }
58
+
59
+ function printInstructions({ certPath, keyPath }) {
60
+ const mac = `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`;
61
+ const win = `certutil -addstore -f "ROOT" "${certPath}"`;
62
+ const linux = `sudo cp "${certPath}" /usr/local/share/ca-certificates/maeris-player.crt && sudo update-ca-certificates`;
63
+
64
+ console.log('\n✅ Local TLS cert generated.');
65
+ console.log(` cert: ${certPath}`);
66
+ console.log(` key: ${keyPath}\n`);
67
+ console.log('Next: trust the cert in your OS (one-time).');
68
+ console.log('\nmacOS:');
69
+ console.log(` ${mac}`);
70
+ console.log('\nWindows (Admin PowerShell):');
71
+ console.log(` ${win}`);
72
+ console.log('\nLinux (Debian/Ubuntu):');
73
+ console.log(` ${linux}`);
74
+ console.log('\nThen start the runner with:');
75
+ console.log(` MAERIS_TLS_CERT="${certPath}" MAERIS_TLS_KEY="${keyPath}" npx @maeris/maeris-player\n`);
76
+ }
77
+
78
+ module.exports = {
79
+ setupCert,
80
+ printInstructions,
81
+ };
@@ -0,0 +1,17 @@
1
+ const WebSocket = require('ws');
2
+ const { EVENTS } = require('../runner/events');
3
+
4
+ module.exports = function attachExtensionBridge({ url = 'ws://localhost:8090' }) {
5
+ const socket = new WebSocket(url);
6
+ socket.on('open', () => {
7
+ console.log('[extension bridge] connected to runner');
8
+ });
9
+ socket.on('message', (message) => {
10
+ const payload = JSON.parse(message);
11
+ console.log('[extension bridge] event', payload.type);
12
+ if (payload.type === EVENTS.SERVER_READY) {
13
+ // handshake with extension UI if needed
14
+ }
15
+ });
16
+ return socket;
17
+ };
@@ -0,0 +1,31 @@
1
+ const EVENTS = {
2
+ SERVER_READY: 'SERVER_READY',
3
+ REPLAY_STARTED: 'REPLAY_STARTED',
4
+ REPLAY_COMPLETE: 'REPLAY_COMPLETE',
5
+ REPLAY_ERROR: 'REPLAY_ERROR',
6
+ STEP_STARTED: 'STEP_STARTED',
7
+ STEP_PASSED: 'STEP_PASSED',
8
+ STEP_FAILED: 'STEP_FAILED',
9
+ NAVIGATION: 'NAVIGATION',
10
+ RUN_STEPS: 'RUN_STEPS',
11
+ STOP_RUN: 'STOP_RUN',
12
+ };
13
+
14
+ function createStepPayload(type, step, meta = {}) {
15
+ return {
16
+ type,
17
+ timestamp: Date.now(),
18
+ stepIndex: step.index,
19
+ stepId: step.id,
20
+ fluId: step.flu_id,
21
+ componentId: step.component_id,
22
+ selector: step.selector,
23
+ stepLabel: step.name || step.label || step.component_label,
24
+ ...meta,
25
+ };
26
+ }
27
+
28
+ module.exports = {
29
+ EVENTS,
30
+ createStepPayload,
31
+ };
@@ -0,0 +1,133 @@
1
+ const EventEmitter = require('events');
2
+ const fs = require('fs');
3
+ const https = require('https');
4
+ const WebSocket = require('ws');
5
+ const PlaywrightAdapter = require('./playwrightAdapter');
6
+ const { EVENTS } = require('./events');
7
+ const normalizeSteps = require('../utils/normalizeSteps');
8
+
9
+ const EVENT_PORT = process.env.MAERIS_RUNNER_PORT || 8090;
10
+
11
+ class Runner extends EventEmitter {
12
+ constructor(options = {}) {
13
+ super();
14
+ this.options = options;
15
+ this.adapter = new PlaywrightAdapter(options.browser);
16
+ this.server = null;
17
+ this.isRunning = false;
18
+ }
19
+
20
+ async startRun(options) {
21
+ if (this.isRunning) {
22
+ throw new Error('Runner already executing a test');
23
+ }
24
+ this.isRunning = true;
25
+ this.adapter.removeAllListeners('event');
26
+ this.adapter.on('event', (event) => {
27
+ this.broadcast(event);
28
+ });
29
+ this.broadcast({ type: EVENTS.REPLAY_STARTED, runId: options.runId });
30
+ const normalizedSteps = normalizeSteps(options.steps);
31
+ try {
32
+ await this.adapter.play(normalizedSteps, {
33
+ startUrl: options.startUrl,
34
+ runId: options.runId,
35
+ headless: options.headless !== undefined ? options.headless : true,
36
+ stepDelayMs: options.stepDelayMs,
37
+ });
38
+ this.broadcast({ type: EVENTS.REPLAY_COMPLETE, runId: options.runId });
39
+ } catch (error) {
40
+ this.broadcast({ type: EVENTS.REPLAY_ERROR, error: error.message });
41
+ throw error;
42
+ } finally {
43
+ this.isRunning = false;
44
+ }
45
+ }
46
+
47
+ broadcast(payload) {
48
+ const wsPayload = JSON.stringify(payload);
49
+ this.emit('event', payload);
50
+ if (this.server && this.server.clients) {
51
+ this.server.clients.forEach((client) => {
52
+ if (client.readyState === WebSocket.OPEN) {
53
+ client.send(wsPayload);
54
+ }
55
+ });
56
+ }
57
+ }
58
+
59
+ startServer() {
60
+ if (this.server) return;
61
+ const tlsKeyPath = process.env.MAERIS_TLS_KEY;
62
+ const tlsCertPath = process.env.MAERIS_TLS_CERT;
63
+ const useTls = Boolean(tlsKeyPath && tlsCertPath);
64
+
65
+ if (useTls) {
66
+ const key = fs.readFileSync(tlsKeyPath);
67
+ const cert = fs.readFileSync(tlsCertPath);
68
+ const httpsServer = https.createServer({ key, cert });
69
+ this.server = new WebSocket.Server({ server: httpsServer });
70
+ httpsServer.listen(EVENT_PORT);
71
+ console.log(`[Runner] WSS server listening on wss://localhost:${EVENT_PORT}`);
72
+ } else {
73
+ this.server = new WebSocket.Server({ port: EVENT_PORT });
74
+ console.log(`[Runner] WebSocket server listening on ws://localhost:${EVENT_PORT}`);
75
+ }
76
+ this.server.on('connection', (socket) => {
77
+ console.log("[Runner] Client connected");
78
+ socket.send(JSON.stringify({ type: EVENTS.SERVER_READY }));
79
+ socket.on('message', (message) => this.handleCommand(socket, message));
80
+ socket.on('close', () => {
81
+ // keep server alive for future runs
82
+ });
83
+ });
84
+ }
85
+
86
+ async handleCommand(socket, message) {
87
+ let parsed;
88
+ try {
89
+ parsed = JSON.parse(message);
90
+ } catch (err) {
91
+ console.error('[Runner] Invalid message', err);
92
+ return;
93
+ }
94
+ console.log('[Runner] command payload', parsed);
95
+ console.log('[Runner] received message', parsed.type);
96
+ const { type, steps, startUrl, browser, runId, headless, stepDelayMs } = parsed;
97
+ if (type === EVENTS.RUN_STEPS) {
98
+ try {
99
+ console.log(`[Runner] starting run ${runId || 'generated'} with ${steps?.length || 0} steps`);
100
+ await this.startRun({
101
+ steps,
102
+ startUrl,
103
+ browser: browser || this.options.browser,
104
+ runId: runId || `local_${Date.now()}`,
105
+ headless,
106
+ stepDelayMs,
107
+ });
108
+ socket.send(JSON.stringify({ type: EVENTS.REPLAY_COMPLETE }));
109
+ } catch (err) {
110
+ socket.send(JSON.stringify({ type: EVENTS.REPLAY_ERROR, error: err.message }));
111
+ }
112
+ } else if (type === EVENTS.STOP_RUN) {
113
+ this.adapter.cancel();
114
+ socket.send(JSON.stringify({ type: EVENTS.REPLAY_ERROR, error: 'Run stopped' }));
115
+ }
116
+ }
117
+
118
+ closeServer() {
119
+ if (this.server) {
120
+ this.server.close();
121
+ this.server = null;
122
+ }
123
+ }
124
+ }
125
+
126
+ module.exports = {
127
+ Runner,
128
+ run: async (options) => {
129
+ const instance = new Runner(options);
130
+ instance.startServer();
131
+ await instance.startRun(options);
132
+ },
133
+ };
@@ -0,0 +1,203 @@
1
+ const EventEmitter = require('events');
2
+ const playwright = require('playwright');
3
+ const { EVENTS, createStepPayload } = require('./events');
4
+
5
+ const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
6
+ const DEFAULT_STEP_DELAY_MS = 2000;
7
+
8
+ class PlaywrightAdapter extends EventEmitter {
9
+ constructor(browserName = 'chromium') {
10
+ super();
11
+ this.browserName = browserName;
12
+ this.browser = null;
13
+ this.context = null;
14
+ this.page = null;
15
+ this.cancelled = false;
16
+ }
17
+
18
+ async play(steps, options = {}) {
19
+ this.cancelled = false;
20
+ const browserType = playwright[this.browserName] || playwright.chromium;
21
+ this.browser = await browserType.launch({
22
+ headless: options.headless !== undefined ? options.headless : true,
23
+ });
24
+ this.context = await this.browser.newContext({ viewport: DEFAULT_VIEWPORT });
25
+ this.page = await this.context.newPage();
26
+ const stepDelayMs = this.resolveStepDelay(options);
27
+
28
+ if (options.startUrl) {
29
+ await this.page.goto(options.startUrl, { waitUntil: 'networkidle' });
30
+ this.emit('event', {
31
+ type: EVENTS.NAVIGATION,
32
+ timestamp: Date.now(),
33
+ url: options.startUrl,
34
+ });
35
+ }
36
+
37
+ try {
38
+ for (let index = 0; index < steps.length; index += 1) {
39
+ const step = steps[index];
40
+ this.emit('event', createStepPayload(EVENTS.STEP_STARTED, step));
41
+ try {
42
+ await this.performStep(this.page, step);
43
+ this.emit('event', createStepPayload(EVENTS.STEP_PASSED, step));
44
+ } catch (error) {
45
+ const screenshot = await page.screenshot({ fullPage: true });
46
+ this.emit(
47
+ 'event',
48
+ createStepPayload(EVENTS.STEP_FAILED, step, {
49
+ message: error.message,
50
+ screenshot: screenshot.toString('base64'),
51
+ })
52
+ );
53
+ throw error;
54
+ }
55
+ if (index < steps.length - 1 && stepDelayMs > 0) {
56
+ await this.sleep(stepDelayMs);
57
+ }
58
+ }
59
+ } finally {
60
+ if (this.browser) {
61
+ await this.browser.close().catch(() => {});
62
+ this.browser = null;
63
+ }
64
+ this.context = null;
65
+ this.page = null;
66
+ }
67
+ }
68
+
69
+ resolveStepDelay(options) {
70
+ if (Number.isFinite(options.stepDelayMs)) {
71
+ return options.stepDelayMs;
72
+ }
73
+ const envDelay = Number.parseInt(process.env.MAERIS_STEP_DELAY_MS, 10);
74
+ if (Number.isFinite(envDelay)) {
75
+ return envDelay;
76
+ }
77
+ return DEFAULT_STEP_DELAY_MS;
78
+ }
79
+
80
+ sleep(ms) {
81
+ return new Promise((resolve) => setTimeout(resolve, ms));
82
+ }
83
+
84
+ getPreferredSelector(step) {
85
+ const selectors = [
86
+ step.css_selector,
87
+ step.selector,
88
+ step.raw_selector,
89
+ step.original_css_selector,
90
+ step.selector_value,
91
+ ];
92
+ const firstValid = selectors.find(
93
+ (value) => typeof value === 'string' && value.trim().length > 0
94
+ );
95
+ if (firstValid) return firstValid.trim();
96
+ if (step.xpath) return step.xpath.trim();
97
+ return null;
98
+ }
99
+
100
+ getLocator(page, step) {
101
+ const selector = this.getPreferredSelector(step);
102
+ if (selector) {
103
+ if (selector.startsWith('//') || selector.startsWith('(')) {
104
+ return page.locator(`xpath=${selector}`);
105
+ }
106
+ return page.locator(selector);
107
+ }
108
+
109
+ if (step.metaText) {
110
+ return page.locator(`:text("${step.metaText}")`).first();
111
+ }
112
+ if (step.metaTag) {
113
+ return page.locator(step.metaTag).first();
114
+ }
115
+ if (step.element_tag) {
116
+ return page.locator(step.element_tag).first();
117
+ }
118
+ return page.locator('*').first();
119
+ }
120
+
121
+ applyNth(locator, step) {
122
+ const nth =
123
+ typeof step.nth_appearance === 'number'
124
+ ? step.nth_appearance
125
+ : typeof step.nthAppearance === 'number'
126
+ ? step.nthAppearance
127
+ : typeof step.step_index === 'number'
128
+ ? step.step_index
129
+ : null;
130
+ return nth !== null ? locator.nth(nth) : locator;
131
+ }
132
+
133
+ async performStep(page, step) {
134
+ const action = (step.action || 'click').toLowerCase();
135
+ const label = step.label || step.name || step.component_label || step.metaText;
136
+ const stepIdentifier = `#${step.index ?? '??'} ${label || 'Step'}`;
137
+ console.log(
138
+ `[PlaywrightAdapter] Performing step ${stepIdentifier} -> action '${action}'`
139
+ );
140
+ if (action.includes('navigate') || action.includes('goto')) {
141
+ const target = step.value || step.page_url || step.metaUrl;
142
+ if (!target) {
143
+ throw new Error('Navigation target missing');
144
+ }
145
+ await page.goto(target, { waitUntil: 'networkidle' });
146
+ this.emit('event', {
147
+ type: EVENTS.NAVIGATION,
148
+ timestamp: Date.now(),
149
+ url: target,
150
+ });
151
+ return;
152
+ }
153
+
154
+ if (this.cancelled) {
155
+ throw new Error("Run cancelled");
156
+ }
157
+ const locator = this.applyNth(this.getLocator(page, step), step);
158
+ await locator.waitFor({ state: 'visible', timeout: 10000 });
159
+
160
+ if (action.includes('click')) {
161
+ await locator.click({ force: true });
162
+ } else if (action.includes('send') || action.includes('input') || action.includes('type')) {
163
+ const value = step.value || step.mock_input || '';
164
+ await locator.fill('');
165
+ if (value) {
166
+ await locator.fill(value);
167
+ }
168
+ } else if (action.includes('assert')) {
169
+ await this.handleAssertion(locator, step);
170
+ } else {
171
+ await locator.click({ force: true });
172
+ }
173
+ }
174
+
175
+ async handleAssertion(locator, step) {
176
+ const assertionType = (step.assertion_type || '').toLowerCase();
177
+ const assertionValue = step.assertion_value;
178
+ if (assertionType.includes('visible')) {
179
+ await locator.waitFor({ state: 'visible', timeout: 10000 });
180
+ } else if (assertionType.includes('value')) {
181
+ const value = await locator.inputValue();
182
+ if (assertionValue && !value.includes(assertionValue)) {
183
+ throw new Error(`Expected value to include "${assertionValue}", got "${value}"`);
184
+ }
185
+ } else if (assertionValue) {
186
+ const text = await locator.innerText();
187
+ if (!text.includes(assertionValue)) {
188
+ throw new Error(`Expected text to include "${assertionValue}", got "${text}"`);
189
+ }
190
+ } else {
191
+ await locator.waitFor({ state: 'visible', timeout: 10000 });
192
+ }
193
+ }
194
+
195
+ cancel() {
196
+ this.cancelled = true;
197
+ if (this.browser) {
198
+ this.browser.close().catch(() => {});
199
+ }
200
+ }
201
+ }
202
+
203
+ module.exports = PlaywrightAdapter;
@@ -0,0 +1,5 @@
1
+ const { Runner } = require('./index');
2
+
3
+ const runner = new Runner({});
4
+ runner.startServer();
5
+ console.log('Runner server started');
@@ -0,0 +1,13 @@
1
+ module.exports = function normalizeSteps(rawSteps = []) {
2
+ const stepsArray = Array.isArray(rawSteps)
3
+ ? rawSteps
4
+ : rawSteps.steps || rawSteps.components || [];
5
+ return stepsArray.map((step, index) => ({
6
+ ...step,
7
+ index,
8
+ selector: step.css_selector || step.xpath || step.selector || null,
9
+ action: step.action_taken || step.type || 'click',
10
+ flu_id: step.flu_id || step.id,
11
+ component_id: step.component_id || step.id,
12
+ }));
13
+ };