@dev-blinq/cucumber_client 1.0.1382-dev → 1.0.1382-stage
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/bin/assets/bundled_scripts/recorder.js +107 -107
- package/bin/assets/preload/css_gen.js +10 -10
- package/bin/assets/preload/toolbar.js +27 -29
- package/bin/assets/preload/unique_locators.js +1 -1
- package/bin/assets/preload/yaml.js +288 -275
- package/bin/assets/scripts/aria_snapshot.js +223 -220
- package/bin/assets/scripts/dom_attr.js +329 -329
- package/bin/assets/scripts/dom_parent.js +169 -174
- package/bin/assets/scripts/event_utils.js +94 -94
- package/bin/assets/scripts/pw.js +2050 -1949
- package/bin/assets/scripts/recorder.js +13 -23
- package/bin/assets/scripts/snapshot_capturer.js +147 -147
- package/bin/assets/scripts/unique_locators.js +163 -44
- package/bin/assets/scripts/yaml.js +796 -783
- package/bin/assets/templates/_hooks_template.txt +6 -2
- package/bin/assets/templates/utils_template.txt +2 -2
- package/bin/client/code_cleanup/utils.js +5 -1
- package/bin/client/code_gen/api_codegen.js +2 -2
- package/bin/client/code_gen/code_inversion.js +107 -2
- package/bin/client/code_gen/function_signature.js +4 -0
- package/bin/client/code_gen/page_reflection.js +846 -906
- package/bin/client/code_gen/playwright_codeget.js +27 -3
- package/bin/client/cucumber/feature.js +4 -0
- package/bin/client/cucumber/feature_data.js +2 -2
- package/bin/client/cucumber/project_to_document.js +8 -2
- package/bin/client/cucumber/steps_definitions.js +6 -3
- package/bin/client/cucumber_selector.js +17 -1
- package/bin/client/local_agent.js +3 -2
- package/bin/client/parse_feature_file.js +23 -26
- package/bin/client/playground/projects/env.json +2 -2
- package/bin/client/project.js +186 -202
- package/bin/client/recorderv3/bvt_init.js +345 -0
- package/bin/client/recorderv3/bvt_recorder.js +708 -100
- package/bin/client/recorderv3/implemented_steps.js +2 -0
- package/bin/client/recorderv3/index.js +4 -303
- package/bin/client/recorderv3/scriptTest.js +1 -1
- package/bin/client/recorderv3/services.js +694 -154
- package/bin/client/recorderv3/step_runner.js +315 -206
- package/bin/client/recorderv3/step_utils.js +473 -25
- package/bin/client/recorderv3/update_feature.js +9 -5
- package/bin/client/recorderv3/wbr_entry.js +61 -0
- package/bin/client/recording.js +1 -0
- package/bin/client/upload-service.js +3 -2
- package/bin/client/utils/socket_logger.js +132 -0
- package/bin/index.js +4 -1
- package/bin/logger.js +3 -2
- package/bin/min/consoleApi.min.cjs +2 -3
- package/bin/min/injectedScript.min.cjs +16 -16
- package/package.json +20 -10
|
@@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import url from "url";
|
|
6
6
|
import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
|
|
7
|
-
import { NamesService } from "./services.js";
|
|
7
|
+
import { NamesService, PublishService, RemoteBrowserService } from "./services.js";
|
|
8
8
|
import { BVTStepRunner } from "./step_runner.js";
|
|
9
9
|
import { readFile, writeFile } from "fs/promises";
|
|
10
10
|
import { updateStepDefinitions, loadStepDefinitions, getCommandsForImplementedStep } from "./step_utils.js";
|
|
@@ -12,11 +12,12 @@ import { updateFeatureFile } from "./update_feature.js";
|
|
|
12
12
|
import { parseStepTextParameters } from "../cucumber/utils.js";
|
|
13
13
|
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
|
|
14
14
|
import chokidar from "chokidar";
|
|
15
|
-
import logger from "../../logger.js";
|
|
16
15
|
import { unEscapeNonPrintables } from "../cucumber/utils.js";
|
|
17
16
|
import { findAvailablePort } from "../utils/index.js";
|
|
18
|
-
import
|
|
19
|
-
|
|
17
|
+
import socketLogger from "../utils/socket_logger.js";
|
|
18
|
+
import { tmpdir } from "os";
|
|
19
|
+
import { faker } from "@faker-js/faker/locale/en_US";
|
|
20
|
+
import { chromium } from "playwright-core";
|
|
20
21
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
21
22
|
|
|
22
23
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -29,7 +30,139 @@ export function getInitScript(config, options) {
|
|
|
29
30
|
path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
|
|
30
31
|
"utf8"
|
|
31
32
|
);
|
|
32
|
-
|
|
33
|
+
const clipboardBridgeScript = `
|
|
34
|
+
;(() => {
|
|
35
|
+
if (window.__bvtRecorderClipboardBridgeInitialized) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
window.__bvtRecorderClipboardBridgeInitialized = true;
|
|
39
|
+
|
|
40
|
+
const emitPayload = (payload, attempt = 0) => {
|
|
41
|
+
const reporter = window.__bvt_reportClipboard;
|
|
42
|
+
if (typeof reporter === "function") {
|
|
43
|
+
try {
|
|
44
|
+
reporter(payload);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn("Clipboard bridge failed to report payload", error);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (attempt < 5) {
|
|
51
|
+
setTimeout(() => emitPayload(payload, attempt + 1), 50 * (attempt + 1));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const fileToBase64 = (file) => {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
try {
|
|
58
|
+
const reader = new FileReader();
|
|
59
|
+
reader.onload = () => {
|
|
60
|
+
const { result } = reader;
|
|
61
|
+
if (typeof result === "string") {
|
|
62
|
+
const index = result.indexOf("base64,");
|
|
63
|
+
resolve(index !== -1 ? result.substring(index + 7) : result);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (result instanceof ArrayBuffer) {
|
|
67
|
+
const bytes = new Uint8Array(result);
|
|
68
|
+
let binary = "";
|
|
69
|
+
const chunk = 0x8000;
|
|
70
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
71
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
72
|
+
}
|
|
73
|
+
resolve(btoa(binary));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
resolve(null);
|
|
77
|
+
};
|
|
78
|
+
reader.onerror = () => resolve(null);
|
|
79
|
+
reader.readAsDataURL(file);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.warn("Clipboard bridge failed to serialize file", error);
|
|
82
|
+
resolve(null);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleClipboardEvent = async (event) => {
|
|
88
|
+
try {
|
|
89
|
+
const payload = { trigger: event.type };
|
|
90
|
+
const clipboardData = event.clipboardData;
|
|
91
|
+
|
|
92
|
+
if (clipboardData) {
|
|
93
|
+
try {
|
|
94
|
+
const text = clipboardData.getData("text/plain");
|
|
95
|
+
if (text) {
|
|
96
|
+
payload.text = text;
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn("Clipboard bridge could not read text/plain", error);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const html = clipboardData.getData("text/html");
|
|
104
|
+
if (html) {
|
|
105
|
+
payload.html = html;
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.warn("Clipboard bridge could not read text/html", error);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const files = clipboardData.files;
|
|
112
|
+
if (files && files.length > 0) {
|
|
113
|
+
const serialized = [];
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
const data = await fileToBase64(file);
|
|
116
|
+
if (data) {
|
|
117
|
+
serialized.push({
|
|
118
|
+
name: file.name,
|
|
119
|
+
type: file.type,
|
|
120
|
+
lastModified: file.lastModified,
|
|
121
|
+
data,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (serialized.length > 0) {
|
|
126
|
+
payload.files = serialized;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!payload.text) {
|
|
132
|
+
try {
|
|
133
|
+
const selection = window.getSelection?.();
|
|
134
|
+
const selectionText = selection?.toString?.();
|
|
135
|
+
if (selectionText) {
|
|
136
|
+
payload.text = selectionText;
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Ignore selection access errors.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
emitPayload(payload);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.warn("Clipboard bridge could not process event", error);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
document.addEventListener(
|
|
150
|
+
"copy",
|
|
151
|
+
(event) => {
|
|
152
|
+
void handleClipboardEvent(event);
|
|
153
|
+
},
|
|
154
|
+
true
|
|
155
|
+
);
|
|
156
|
+
document.addEventListener(
|
|
157
|
+
"cut",
|
|
158
|
+
(event) => {
|
|
159
|
+
void handleClipboardEvent(event);
|
|
160
|
+
},
|
|
161
|
+
true
|
|
162
|
+
);
|
|
163
|
+
})();
|
|
164
|
+
`;
|
|
165
|
+
return preScript + recorderScript + clipboardBridgeScript;
|
|
33
166
|
}
|
|
34
167
|
|
|
35
168
|
async function evaluate(frame, script) {
|
|
@@ -45,7 +178,6 @@ async function evaluate(frame, script) {
|
|
|
45
178
|
async function findNestedFrameSelector(frame, obj) {
|
|
46
179
|
try {
|
|
47
180
|
const parent = frame.parentFrame();
|
|
48
|
-
if (parent) console.log(`Parent frame: ${JSON.stringify(parent)}`);
|
|
49
181
|
if (!parent) return { children: obj };
|
|
50
182
|
const frameElement = await frame.frameElement();
|
|
51
183
|
if (!frameElement) return;
|
|
@@ -54,6 +186,7 @@ async function findNestedFrameSelector(frame, obj) {
|
|
|
54
186
|
}, frameElement);
|
|
55
187
|
return findNestedFrameSelector(parent, { children: obj, selectors });
|
|
56
188
|
} catch (e) {
|
|
189
|
+
socketLogger.error(`Error in findNestedFrameSelector: ${e}`);
|
|
57
190
|
console.error(e);
|
|
58
191
|
}
|
|
59
192
|
}
|
|
@@ -150,17 +283,30 @@ const transformAction = (action, el, isVerify, isPopupCloseClick, isInHoverMode,
|
|
|
150
283
|
};
|
|
151
284
|
}
|
|
152
285
|
default: {
|
|
286
|
+
socketLogger.error(`Action not supported: ${action.name}`);
|
|
153
287
|
console.log("action not supported", action);
|
|
154
288
|
throw new Error("action not supported");
|
|
155
289
|
}
|
|
156
290
|
}
|
|
157
291
|
};
|
|
292
|
+
const diffPaths = (currentPath, newPath) => {
|
|
293
|
+
const currentDomain = new URL(currentPath).hostname;
|
|
294
|
+
const newDomain = new URL(newPath).hostname;
|
|
295
|
+
if (currentDomain !== newDomain) {
|
|
296
|
+
return true;
|
|
297
|
+
} else {
|
|
298
|
+
const currentRoute = new URL(currentPath).pathname;
|
|
299
|
+
const newRoute = new URL(newPath).pathname;
|
|
300
|
+
return currentRoute !== newRoute;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
158
303
|
/**
|
|
159
304
|
* @typedef {Object} BVTRecorderInput
|
|
160
305
|
* @property {string} envName
|
|
161
306
|
* @property {string} projectDir
|
|
162
307
|
* @property {string} TOKEN
|
|
163
308
|
* @property {(name:string, data:any)=> void} sendEvent
|
|
309
|
+
* @property {Object} logger
|
|
164
310
|
*/
|
|
165
311
|
export class BVTRecorder {
|
|
166
312
|
#currentURL = "";
|
|
@@ -174,7 +320,6 @@ export class BVTRecorder {
|
|
|
174
320
|
*/
|
|
175
321
|
constructor(initialState) {
|
|
176
322
|
Object.assign(this, initialState);
|
|
177
|
-
this.logger = logger;
|
|
178
323
|
this.screenshotMap = new Map();
|
|
179
324
|
this.snapshotMap = new Map();
|
|
180
325
|
this.scenariosStepsMap = new Map();
|
|
@@ -184,40 +329,20 @@ export class BVTRecorder {
|
|
|
184
329
|
projectDir: this.projectDir,
|
|
185
330
|
logger: this.logger,
|
|
186
331
|
});
|
|
187
|
-
this.
|
|
188
|
-
projectDir: this.projectDir,
|
|
189
|
-
sendExecutionStatus: (data) => {
|
|
190
|
-
if (data && data.type) {
|
|
191
|
-
switch (data.type) {
|
|
192
|
-
case "cmdExecutionStart":
|
|
193
|
-
console.log("Sending cmdExecutionStart event for cmdId:", data);
|
|
194
|
-
this.sendEvent(this.events.cmdExecutionStart, data);
|
|
195
|
-
break;
|
|
196
|
-
case "cmdExecutionSuccess":
|
|
197
|
-
console.log("Sending cmdExecutionSuccess event for cmdId:", data);
|
|
198
|
-
this.sendEvent(this.events.cmdExecutionSuccess, data);
|
|
199
|
-
break;
|
|
200
|
-
case "cmdExecutionError":
|
|
201
|
-
console.log("Sending cmdExecutionError event for cmdId:", data);
|
|
202
|
-
this.sendEvent(this.events.cmdExecutionError, data);
|
|
203
|
-
break;
|
|
204
|
-
case "interceptResults":
|
|
205
|
-
console.log("Sending interceptResults event");
|
|
206
|
-
this.sendEvent(this.events.interceptResults, data);
|
|
207
|
-
break;
|
|
208
|
-
default:
|
|
209
|
-
console.warn("Unknown command execution status type:", data.type);
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
});
|
|
332
|
+
this.workspaceService = new PublishService(this.TOKEN);
|
|
215
333
|
this.pageSet = new Set();
|
|
334
|
+
this.pageMetaDataSet = new Set();
|
|
216
335
|
this.lastKnownUrlPath = "";
|
|
217
|
-
// TODO: what is world?
|
|
218
336
|
this.world = { attach: () => {} };
|
|
219
337
|
this.shouldTakeScreenshot = true;
|
|
220
338
|
this.watcher = null;
|
|
339
|
+
this.networkEventsFolder = path.join(tmpdir(), "blinq_network_events");
|
|
340
|
+
this.tempProjectFolder = `${tmpdir()}/bvt_temp_project_${Math.floor(Math.random() * 1000000)}`;
|
|
341
|
+
this.tempSnapshotsFolder = path.join(this.tempProjectFolder, "data/snapshots");
|
|
342
|
+
|
|
343
|
+
if (existsSync(this.networkEventsFolder)) {
|
|
344
|
+
rmSync(this.networkEventsFolder, { recursive: true, force: true });
|
|
345
|
+
}
|
|
221
346
|
}
|
|
222
347
|
events = {
|
|
223
348
|
onFrameNavigate: "BVTRecorder.onFrameNavigate",
|
|
@@ -232,12 +357,18 @@ export class BVTRecorder {
|
|
|
232
357
|
cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
|
|
233
358
|
cmdExecutionError: "BVTRecorder.cmdExecutionError",
|
|
234
359
|
interceptResults: "BVTRecorder.interceptResults",
|
|
360
|
+
onDebugURLChange: "BVTRecorder.onDebugURLChange",
|
|
361
|
+
updateCommand: "BVTRecorder.updateCommand",
|
|
362
|
+
browserStateSync: "BrowserService.stateSync",
|
|
363
|
+
browserStateError: "BrowserService.stateError",
|
|
364
|
+
clipboardPush: "BrowserService.clipboardPush",
|
|
365
|
+
clipboardError: "BrowserService.clipboardError",
|
|
235
366
|
};
|
|
236
367
|
bindings = {
|
|
237
368
|
__bvt_recordCommand: async ({ frame, page, context }, event) => {
|
|
238
369
|
this.#activeFrame = frame;
|
|
239
370
|
const nestFrmLoc = await findNestedFrameSelector(frame);
|
|
240
|
-
|
|
371
|
+
this.logger.info(`Time taken for action: ${event.statistics.time}`);
|
|
241
372
|
await this.onAction({ ...event, nestFrmLoc });
|
|
242
373
|
},
|
|
243
374
|
__bvt_getMode: async () => {
|
|
@@ -256,12 +387,30 @@ export class BVTRecorder {
|
|
|
256
387
|
await this.onClosePopup();
|
|
257
388
|
},
|
|
258
389
|
__bvt_log: async (src, message) => {
|
|
259
|
-
|
|
260
|
-
console.log(`Inside Browser: ${message}`);
|
|
390
|
+
this.logger.info(`Inside Browser: ${message}`);
|
|
261
391
|
},
|
|
262
392
|
__bvt_getObject: (_src, obj) => {
|
|
263
393
|
this.processObject(obj);
|
|
264
394
|
},
|
|
395
|
+
__bvt_reportClipboard: async ({ page }, payload) => {
|
|
396
|
+
try {
|
|
397
|
+
if (!payload) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
401
|
+
if (activePage && activePage !== page) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const pageUrl = typeof page?.url === "function" ? page.url() : null;
|
|
405
|
+
this.sendEvent(this.events.clipboardPush, {
|
|
406
|
+
data: payload,
|
|
407
|
+
trigger: payload?.trigger ?? "copy",
|
|
408
|
+
pageUrl,
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
this.logger.error("Error forwarding clipboard payload from page", error);
|
|
412
|
+
}
|
|
413
|
+
},
|
|
265
414
|
};
|
|
266
415
|
|
|
267
416
|
getSnapshot = async (attr) => {
|
|
@@ -319,10 +468,11 @@ export class BVTRecorder {
|
|
|
319
468
|
}
|
|
320
469
|
|
|
321
470
|
async _initBrowser({ url }) {
|
|
471
|
+
socketLogger.info("Only present in 1.0.1293-stage");
|
|
322
472
|
this.#remoteDebuggerPort = await findAvailablePort();
|
|
323
473
|
process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
|
|
324
474
|
|
|
325
|
-
this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
475
|
+
// this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
326
476
|
this.world = { attach: () => {} };
|
|
327
477
|
|
|
328
478
|
const ai_config_file = path.join(this.projectDir, "ai_config.json");
|
|
@@ -331,7 +481,7 @@ export class BVTRecorder {
|
|
|
331
481
|
try {
|
|
332
482
|
ai_config = JSON.parse(readFileSync(ai_config_file, "utf8"));
|
|
333
483
|
} catch (error) {
|
|
334
|
-
|
|
484
|
+
this.logger.error("Error reading ai_config.json", error);
|
|
335
485
|
}
|
|
336
486
|
}
|
|
337
487
|
this.config = ai_config;
|
|
@@ -343,18 +493,47 @@ export class BVTRecorder {
|
|
|
343
493
|
],
|
|
344
494
|
};
|
|
345
495
|
|
|
346
|
-
let startTime = Date.now();
|
|
347
496
|
const bvtContext = await initContext(url, false, false, this.world, 450, initScripts, this.envName);
|
|
348
|
-
let stopTime = Date.now();
|
|
349
|
-
this.logger.info(`Browser launched in ${(stopTime - startTime) / 1000} s`);
|
|
350
497
|
this.bvtContext = bvtContext;
|
|
351
|
-
|
|
352
|
-
|
|
498
|
+
this.stepRunner = new BVTStepRunner({
|
|
499
|
+
projectDir: this.projectDir,
|
|
500
|
+
sendExecutionStatus: (data) => {
|
|
501
|
+
if (data && data.type) {
|
|
502
|
+
switch (data.type) {
|
|
503
|
+
case "cmdExecutionStart":
|
|
504
|
+
this.sendEvent(this.events.cmdExecutionStart, data);
|
|
505
|
+
break;
|
|
506
|
+
case "cmdExecutionSuccess":
|
|
507
|
+
this.sendEvent(this.events.cmdExecutionSuccess, data);
|
|
508
|
+
break;
|
|
509
|
+
case "cmdExecutionError":
|
|
510
|
+
this.sendEvent(this.events.cmdExecutionError, data);
|
|
511
|
+
break;
|
|
512
|
+
case "interceptResults":
|
|
513
|
+
this.sendEvent(this.events.interceptResults, data);
|
|
514
|
+
break;
|
|
515
|
+
default:
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
bvtContext: this.bvtContext,
|
|
521
|
+
});
|
|
522
|
+
this.context = bvtContext.playContext;
|
|
353
523
|
this.web = bvtContext.stable || bvtContext.web;
|
|
354
524
|
this.web.tryAllStrategies = true;
|
|
355
525
|
this.page = bvtContext.page;
|
|
356
|
-
|
|
357
526
|
this.pageSet.add(this.page);
|
|
527
|
+
if (process.env.REMOTE_RECORDER === "true") {
|
|
528
|
+
this.browserEmitter = new RemoteBrowserService({
|
|
529
|
+
CDP_CONNECT_URL: `http://localhost:${this.#remoteDebuggerPort}`,
|
|
530
|
+
context: this.context,
|
|
531
|
+
});
|
|
532
|
+
this.browserEmitter.on(this.events.browserStateSync, (state) => {
|
|
533
|
+
this.page = this.browserEmitter.getSelectedPage();
|
|
534
|
+
this.sendEvent(this.events.browserStateSync, state);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
358
537
|
this.lastKnownUrlPath = this._updateUrlPath();
|
|
359
538
|
const browser = await this.context.browser();
|
|
360
539
|
this.browser = browser;
|
|
@@ -367,6 +546,14 @@ export class BVTRecorder {
|
|
|
367
546
|
this.web.onRestoreSaveState = (url) => {
|
|
368
547
|
this._initBrowser({ url });
|
|
369
548
|
};
|
|
549
|
+
|
|
550
|
+
// create a second browser for locator generation
|
|
551
|
+
this.backgroundBrowser = await chromium.launch({
|
|
552
|
+
headless: true,
|
|
553
|
+
});
|
|
554
|
+
this.backgroundContext = await this.backgroundBrowser.newContext({});
|
|
555
|
+
await this.backgroundContext.addInitScript({ content: this.getInitScripts(this.config) });
|
|
556
|
+
await this.backgroundContext.newPage();
|
|
370
557
|
}
|
|
371
558
|
async onClosePopup() {
|
|
372
559
|
// console.log("close popups");
|
|
@@ -381,13 +568,15 @@ export class BVTRecorder {
|
|
|
381
568
|
}
|
|
382
569
|
return;
|
|
383
570
|
} catch (error) {
|
|
384
|
-
console.error("Error evaluting in context:", error);
|
|
571
|
+
// console.error("Error evaluting in context:", error);
|
|
572
|
+
this.logger.error("Error evaluating in context:", error);
|
|
385
573
|
}
|
|
386
574
|
}
|
|
387
575
|
}
|
|
388
576
|
|
|
389
577
|
getMode() {
|
|
390
|
-
console.log("getMode", this.#mode);
|
|
578
|
+
// console.log("getMode", this.#mode);
|
|
579
|
+
this.logger.info("Current mode:", this.#mode);
|
|
391
580
|
return this.#mode;
|
|
392
581
|
}
|
|
393
582
|
|
|
@@ -412,7 +601,7 @@ export class BVTRecorder {
|
|
|
412
601
|
|
|
413
602
|
// eval init script on current tab
|
|
414
603
|
// await this._initPage(this.page);
|
|
415
|
-
this.#currentURL =
|
|
604
|
+
this.#currentURL = url;
|
|
416
605
|
|
|
417
606
|
await this.page.dispatchEvent("html", "scroll");
|
|
418
607
|
await delay(1000);
|
|
@@ -429,6 +618,8 @@ export class BVTRecorder {
|
|
|
429
618
|
this.sendEvent(this.events.onBrowserClose);
|
|
430
619
|
}
|
|
431
620
|
} catch (error) {
|
|
621
|
+
this.logger.error("Error in page close event");
|
|
622
|
+
this.logger.error(error);
|
|
432
623
|
console.error("Error in page close event");
|
|
433
624
|
console.error(error);
|
|
434
625
|
}
|
|
@@ -439,8 +630,10 @@ export class BVTRecorder {
|
|
|
439
630
|
if (frame !== page.mainFrame()) return;
|
|
440
631
|
this.handlePageTransition();
|
|
441
632
|
} catch (error) {
|
|
633
|
+
this.logger.error("Error in handlePageTransition event");
|
|
634
|
+
this.logger.error(error);
|
|
442
635
|
console.error("Error in handlePageTransition event");
|
|
443
|
-
|
|
636
|
+
console.error(error);
|
|
444
637
|
}
|
|
445
638
|
try {
|
|
446
639
|
if (frame !== this.#activeFrame) return;
|
|
@@ -450,15 +643,18 @@ export class BVTRecorder {
|
|
|
450
643
|
element: { inputID: "frame" },
|
|
451
644
|
});
|
|
452
645
|
|
|
453
|
-
const
|
|
646
|
+
const newUrl = frame.url();
|
|
647
|
+
const newPath = new URL(newUrl).pathname;
|
|
454
648
|
const newTitle = await frame.title();
|
|
455
|
-
|
|
649
|
+
const changed = diffPaths(this.#currentURL, newUrl);
|
|
650
|
+
|
|
651
|
+
if (changed) {
|
|
456
652
|
this.sendEvent(this.events.onFrameNavigate, { url: newPath, title: newTitle });
|
|
457
|
-
this.#currentURL =
|
|
653
|
+
this.#currentURL = newUrl;
|
|
458
654
|
}
|
|
459
|
-
// await this._setRecordingMode(frame);
|
|
460
|
-
// await this._initPage(page);
|
|
461
655
|
} catch (error) {
|
|
656
|
+
this.logger.error("Error in frame navigate event");
|
|
657
|
+
this.logger.error(error);
|
|
462
658
|
console.error("Error in frame navigate event");
|
|
463
659
|
// console.error(error);
|
|
464
660
|
}
|
|
@@ -541,13 +737,9 @@ export class BVTRecorder {
|
|
|
541
737
|
|
|
542
738
|
try {
|
|
543
739
|
const result = await client.send("Page.getNavigationHistory");
|
|
544
|
-
// console.log("Navigation History:", result);
|
|
545
740
|
const entries = result.entries;
|
|
546
741
|
const currentIndex = result.currentIndex;
|
|
547
742
|
|
|
548
|
-
// ignore if currentIndex is not the last entry
|
|
549
|
-
// if (currentIndex !== entries.length - 1) return;
|
|
550
|
-
|
|
551
743
|
const currentEntry = entries[currentIndex];
|
|
552
744
|
const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
|
|
553
745
|
this.previousIndex = currentIndex;
|
|
@@ -560,6 +752,8 @@ export class BVTRecorder {
|
|
|
560
752
|
navigationAction: transitionInfo.action,
|
|
561
753
|
};
|
|
562
754
|
} catch (error) {
|
|
755
|
+
this.logger.error("Error in getCurrentTransition event");
|
|
756
|
+
this.logger.error(error);
|
|
563
757
|
console.error("Error in getTransistionType event", error);
|
|
564
758
|
} finally {
|
|
565
759
|
await client.detach();
|
|
@@ -628,6 +822,8 @@ export class BVTRecorder {
|
|
|
628
822
|
// add listener for frame navigation on new tab
|
|
629
823
|
this._addFrameNavigateListener(page);
|
|
630
824
|
} catch (error) {
|
|
825
|
+
this.logger.error("Error in page event");
|
|
826
|
+
this.logger.error(error);
|
|
631
827
|
console.error("Error in page event");
|
|
632
828
|
console.error(error);
|
|
633
829
|
}
|
|
@@ -669,6 +865,7 @@ export class BVTRecorder {
|
|
|
669
865
|
const { data } = await client.send("Page.captureScreenshot", { format: "png" });
|
|
670
866
|
return data;
|
|
671
867
|
} catch (error) {
|
|
868
|
+
this.logger.error("Error in taking browser screenshot");
|
|
672
869
|
console.error("Error in taking browser screenshot", error);
|
|
673
870
|
} finally {
|
|
674
871
|
await client.detach();
|
|
@@ -684,6 +881,52 @@ export class BVTRecorder {
|
|
|
684
881
|
console.error("Error in saving screenshot: ", error);
|
|
685
882
|
}
|
|
686
883
|
}
|
|
884
|
+
async generateLocators(event) {
|
|
885
|
+
const snapshotDetails = event.snapshotDetails;
|
|
886
|
+
if (!snapshotDetails) {
|
|
887
|
+
throw new Error("No snapshot details found");
|
|
888
|
+
}
|
|
889
|
+
const mode = event.mode;
|
|
890
|
+
const inputID = event.element.inputID;
|
|
891
|
+
|
|
892
|
+
const { id, contextId, doc } = snapshotDetails;
|
|
893
|
+
// const selector = `[data-blinq-id="${id}"]`;
|
|
894
|
+
const newPage = await this.backgroundContext.newPage();
|
|
895
|
+
await newPage.setContent(doc, { waitUntil: "domcontentloaded" });
|
|
896
|
+
const locatorsObj = await newPage.evaluate(
|
|
897
|
+
([id, contextId, mode]) => {
|
|
898
|
+
const recorder = window.__bvt_Recorder;
|
|
899
|
+
const contextElement = document.querySelector(`[data-blinq-context-id="${contextId}"]`);
|
|
900
|
+
const el = document.querySelector(`[data-blinq-id="${id}"]`);
|
|
901
|
+
if (contextElement) {
|
|
902
|
+
const result = recorder.locatorGenerator.toContextLocators(el, contextElement);
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
const isRecordingText = mode === "recordingText";
|
|
906
|
+
return recorder.locatorGenerator.getElementLocators(el, {
|
|
907
|
+
excludeText: isRecordingText,
|
|
908
|
+
});
|
|
909
|
+
},
|
|
910
|
+
[id, contextId, mode]
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
console.log(`Generated locators: for ${inputID}: `, JSON.stringify(locatorsObj));
|
|
914
|
+
await newPage.close();
|
|
915
|
+
if (event.nestFrmLoc?.children) {
|
|
916
|
+
locatorsObj.nestFrmLoc = event.nestFrmLoc.children;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
this.sendEvent(this.events.updateCommand, {
|
|
920
|
+
locators: {
|
|
921
|
+
locators: locatorsObj.locators,
|
|
922
|
+
nestFrmLoc: locatorsObj.nestFrmLoc,
|
|
923
|
+
iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
924
|
+
},
|
|
925
|
+
allStrategyLocators: locatorsObj.allStrategyLocators,
|
|
926
|
+
inputID,
|
|
927
|
+
});
|
|
928
|
+
// const
|
|
929
|
+
}
|
|
687
930
|
async onAction(event) {
|
|
688
931
|
this._updateUrlPath();
|
|
689
932
|
// const locators = this.overlayLocators(event);
|
|
@@ -697,25 +940,26 @@ export class BVTRecorder {
|
|
|
697
940
|
event.mode === "recordingHover",
|
|
698
941
|
event.mode === "multiInspecting"
|
|
699
942
|
),
|
|
700
|
-
locators: {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
},
|
|
704
|
-
allStrategyLocators: event.allStrategyLocators,
|
|
943
|
+
// locators: {
|
|
944
|
+
// locators: event.locators,
|
|
945
|
+
// iframe_src: !event.frame.isTop ? event.frame.url : undefined,
|
|
946
|
+
// },
|
|
947
|
+
// allStrategyLocators: event.allStrategyLocators,
|
|
705
948
|
url: event.frame.url,
|
|
706
949
|
title: event.frame.title,
|
|
707
950
|
extract: {},
|
|
708
951
|
lastKnownUrlPath: this.lastKnownUrlPath,
|
|
709
952
|
};
|
|
710
|
-
if (event.nestFrmLoc?.children) {
|
|
711
|
-
|
|
712
|
-
}
|
|
953
|
+
// if (event.nestFrmLoc?.children) {
|
|
954
|
+
// cmdEvent.locators.nestFrmLoc = event.nestFrmLoc.children;
|
|
955
|
+
// }
|
|
713
956
|
// this.logger.info({ event });
|
|
714
957
|
if (this.shouldTakeScreenshot) {
|
|
715
958
|
await this.storeScreenshot(event);
|
|
716
959
|
}
|
|
717
960
|
this.sendEvent(this.events.onNewCommand, cmdEvent);
|
|
718
961
|
this._updateUrlPath();
|
|
962
|
+
await this.generateLocators(event);
|
|
719
963
|
}
|
|
720
964
|
_updateUrlPath() {
|
|
721
965
|
try {
|
|
@@ -737,7 +981,6 @@ export class BVTRecorder {
|
|
|
737
981
|
this.previousHistoryLength = null;
|
|
738
982
|
this.previousUrl = null;
|
|
739
983
|
this.previousEntries = null;
|
|
740
|
-
|
|
741
984
|
await closeContext();
|
|
742
985
|
this.pageSet.clear();
|
|
743
986
|
}
|
|
@@ -789,7 +1032,6 @@ export class BVTRecorder {
|
|
|
789
1032
|
}
|
|
790
1033
|
|
|
791
1034
|
async startRecordingInput() {
|
|
792
|
-
console.log("startRecordingInput");
|
|
793
1035
|
await this.setMode("recordingInput");
|
|
794
1036
|
}
|
|
795
1037
|
async stopRecordingInput() {
|
|
@@ -813,9 +1055,17 @@ export class BVTRecorder {
|
|
|
813
1055
|
}
|
|
814
1056
|
|
|
815
1057
|
async abortExecution() {
|
|
816
|
-
this.bvtContext.web.abortedExecution = true;
|
|
817
1058
|
await this.stepRunner.abortExecution();
|
|
818
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
async pauseExecution({ cmdId }) {
|
|
1062
|
+
await this.stepRunner.pauseExecution(cmdId);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async resumeExecution({ cmdId }) {
|
|
1066
|
+
await this.stepRunner.resumeExecution(cmdId);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
819
1069
|
async dealyedRevertMode() {
|
|
820
1070
|
const timerId = setTimeout(async () => {
|
|
821
1071
|
await this.revertMode();
|
|
@@ -824,18 +1074,24 @@ export class BVTRecorder {
|
|
|
824
1074
|
}
|
|
825
1075
|
async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
|
|
826
1076
|
const { skipAfter = true, skipBefore = !isFirstStep } = options || {};
|
|
1077
|
+
|
|
1078
|
+
const env = path.basename(this.envName, ".json");
|
|
827
1079
|
const _env = {
|
|
828
1080
|
TOKEN: this.TOKEN,
|
|
829
1081
|
TEMP_RUN: true,
|
|
830
1082
|
REPORT_FOLDER: this.bvtContext.reportFolder,
|
|
831
1083
|
BLINQ_ENV: this.envName,
|
|
832
|
-
|
|
833
|
-
|
|
1084
|
+
DEBUG: "blinq:route",
|
|
1085
|
+
BVT_TEMP_SNAPSHOTS_FOLDER: step.isImplemented ? path.join(this.tempSnapshotsFolder, env) : undefined,
|
|
834
1086
|
};
|
|
835
1087
|
|
|
836
1088
|
this.bvtContext.navigate = true;
|
|
837
1089
|
this.bvtContext.loadedRoutes = null;
|
|
838
|
-
|
|
1090
|
+
if (listenNetwork) {
|
|
1091
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = true;
|
|
1092
|
+
} else {
|
|
1093
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1094
|
+
}
|
|
839
1095
|
for (const [key, value] of Object.entries(_env)) {
|
|
840
1096
|
process.env[key] = value;
|
|
841
1097
|
}
|
|
@@ -871,13 +1127,26 @@ export class BVTRecorder {
|
|
|
871
1127
|
delete process.env[key];
|
|
872
1128
|
}
|
|
873
1129
|
this.bvtContext.navigate = false;
|
|
874
|
-
this.bvtContext.web.abortedExecution = false;
|
|
875
1130
|
}
|
|
876
1131
|
}
|
|
877
|
-
async saveScenario({ scenario, featureName, override, isSingleStep }) {
|
|
878
|
-
await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
|
|
879
|
-
if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
|
|
880
|
-
await this.
|
|
1132
|
+
async saveScenario({ scenario, featureName, override, isSingleStep, branch, isEditing, env }) {
|
|
1133
|
+
// await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
|
|
1134
|
+
// if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
|
|
1135
|
+
const res = await this.workspaceService.saveScenario({
|
|
1136
|
+
scenario,
|
|
1137
|
+
featureName,
|
|
1138
|
+
override,
|
|
1139
|
+
isSingleStep,
|
|
1140
|
+
branch,
|
|
1141
|
+
isEditing,
|
|
1142
|
+
projectId: path.basename(this.projectDir),
|
|
1143
|
+
env: env ?? this.envName,
|
|
1144
|
+
});
|
|
1145
|
+
if (res.success) {
|
|
1146
|
+
await this.cleanup({ tags: scenario.tags });
|
|
1147
|
+
} else {
|
|
1148
|
+
throw new Error(res.message || "Error saving scenario");
|
|
1149
|
+
}
|
|
881
1150
|
}
|
|
882
1151
|
async getImplementedSteps() {
|
|
883
1152
|
const stepsAndScenarios = await getImplementedSteps(this.projectDir);
|
|
@@ -961,10 +1230,11 @@ export class BVTRecorder {
|
|
|
961
1230
|
if (existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
|
|
962
1231
|
try {
|
|
963
1232
|
const testData = JSON.parse(readFileSync(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
964
|
-
this.logger.info("Test data", testData);
|
|
1233
|
+
// this.logger.info("Test data", testData);
|
|
965
1234
|
this.sendEvent(this.events.getTestData, testData);
|
|
966
1235
|
} catch (e) {
|
|
967
|
-
this.logger.error("Error reading test data file", e);
|
|
1236
|
+
// this.logger.error("Error reading test data file", e);
|
|
1237
|
+
console.log("Error reading test data file", e);
|
|
968
1238
|
}
|
|
969
1239
|
}
|
|
970
1240
|
|
|
@@ -973,10 +1243,12 @@ export class BVTRecorder {
|
|
|
973
1243
|
this.watcher.on("all", async (event, path) => {
|
|
974
1244
|
try {
|
|
975
1245
|
const testData = JSON.parse(await readFile(_getDataFile(this.world, this.bvtContext, this.web), "utf8"));
|
|
976
|
-
this.logger.info("Test data", testData);
|
|
1246
|
+
// this.logger.info("Test data", testData);
|
|
1247
|
+
console.log("Test data changed", testData);
|
|
977
1248
|
this.sendEvent(this.events.getTestData, testData);
|
|
978
1249
|
} catch (e) {
|
|
979
|
-
this.logger.error("Error reading test data file", e);
|
|
1250
|
+
// this.logger.error("Error reading test data file", e);
|
|
1251
|
+
console.log("Error reading test data file", e);
|
|
980
1252
|
}
|
|
981
1253
|
});
|
|
982
1254
|
}
|
|
@@ -1009,7 +1281,7 @@ export class BVTRecorder {
|
|
|
1009
1281
|
.filter((file) => file.endsWith(".feature"))
|
|
1010
1282
|
.map((file) => path.join(this.projectDir, "features", file));
|
|
1011
1283
|
try {
|
|
1012
|
-
const parsedFiles = featureFiles.map((file) => parseFeatureFile(file));
|
|
1284
|
+
const parsedFiles = featureFiles.map((file) => this.parseFeatureFile(file));
|
|
1013
1285
|
const output = {};
|
|
1014
1286
|
parsedFiles.forEach((file) => {
|
|
1015
1287
|
if (!file.feature) return;
|
|
@@ -1037,7 +1309,7 @@ export class BVTRecorder {
|
|
|
1037
1309
|
loadExistingScenario({ featureName, scenarioName }) {
|
|
1038
1310
|
const step_definitions = loadStepDefinitions(this.projectDir);
|
|
1039
1311
|
const featureFilePath = path.join(this.projectDir, "features", featureName);
|
|
1040
|
-
const gherkinDoc = parseFeatureFile(featureFilePath);
|
|
1312
|
+
const gherkinDoc = this.parseFeatureFile(featureFilePath);
|
|
1041
1313
|
const scenario = gherkinDoc.feature.children.find((child) => child.scenario.name === scenarioName)?.scenario;
|
|
1042
1314
|
|
|
1043
1315
|
const steps = [];
|
|
@@ -1196,20 +1468,356 @@ export class BVTRecorder {
|
|
|
1196
1468
|
await this.cleanupExecution({ tags });
|
|
1197
1469
|
await this.initExecution({ tags });
|
|
1198
1470
|
}
|
|
1199
|
-
}
|
|
1200
1471
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1472
|
+
parseFeatureFile(featureFilePath) {
|
|
1473
|
+
try {
|
|
1474
|
+
let id = 0;
|
|
1475
|
+
const uuidFn = () => (++id).toString(16);
|
|
1476
|
+
const builder = new AstBuilder(uuidFn);
|
|
1477
|
+
const matcher = new GherkinClassicTokenMatcher();
|
|
1478
|
+
const parser = new Parser(builder, matcher);
|
|
1479
|
+
const source = readFileSync(featureFilePath, "utf8");
|
|
1480
|
+
const gherkinDocument = parser.parse(source);
|
|
1481
|
+
return gherkinDocument;
|
|
1482
|
+
} catch (e) {
|
|
1483
|
+
this.logger.error(`Error parsing feature file: ${featureFilePath}`);
|
|
1484
|
+
console.log(e);
|
|
1485
|
+
}
|
|
1486
|
+
return {};
|
|
1213
1487
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1488
|
+
|
|
1489
|
+
stopRecordingNetwork(input) {
|
|
1490
|
+
if (this.bvtContext) {
|
|
1491
|
+
this.bvtContext.STORE_DETAILED_NETWORK_DATA = false;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
async fakeParams(params) {
|
|
1496
|
+
const newFakeParams = {};
|
|
1497
|
+
Object.keys(params).forEach((key) => {
|
|
1498
|
+
if (!params[key].startsWith("{{") || !params[key].endsWith("}}")) {
|
|
1499
|
+
newFakeParams[key] = params[key];
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
try {
|
|
1504
|
+
const value = params[key].substring(2, params[key].length - 2).trim();
|
|
1505
|
+
const faking = value.split("(")[0].split(".");
|
|
1506
|
+
let argument = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
1507
|
+
argument = isNaN(Number(argument)) || argument === "" ? argument : Number(argument);
|
|
1508
|
+
let fakeFunc = faker;
|
|
1509
|
+
faking.forEach((f) => {
|
|
1510
|
+
fakeFunc = fakeFunc[f];
|
|
1511
|
+
});
|
|
1512
|
+
const newValue = fakeFunc(argument);
|
|
1513
|
+
newFakeParams[key] = newValue;
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
newFakeParams[key] = params[key];
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
return newFakeParams;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
async getBrowserState() {
|
|
1523
|
+
try {
|
|
1524
|
+
const state = await this.browserEmitter?.getState();
|
|
1525
|
+
this.sendEvent(this.events.browserStateSync, state);
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
this.logger.error("Error getting browser state:", error);
|
|
1528
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1529
|
+
message: "Error getting browser state",
|
|
1530
|
+
code: "GET_STATE_ERROR",
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
async applyClipboardPayload(message) {
|
|
1536
|
+
const payload = message?.data ?? message;
|
|
1537
|
+
if (!payload) {
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
try {
|
|
1542
|
+
if (this.browserEmitter && typeof this.browserEmitter.applyClipboardPayload === "function") {
|
|
1543
|
+
await this.browserEmitter.applyClipboardPayload(payload);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
const activePage = this.browserEmitter?.getSelectedPage() ?? this.page;
|
|
1548
|
+
if (!activePage) {
|
|
1549
|
+
this.logger.warn("No active page available to apply clipboard payload");
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
await this.injectClipboardIntoPage(activePage, payload);
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
this.logger.error("Error applying clipboard payload to page", error);
|
|
1556
|
+
this.sendEvent(this.events.clipboardError, {
|
|
1557
|
+
message: "Failed to apply clipboard contents to the remote session",
|
|
1558
|
+
trigger: message?.trigger ?? "paste",
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
async injectClipboardIntoPage(page, payload) {
|
|
1564
|
+
if (!page) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
try {
|
|
1569
|
+
await page
|
|
1570
|
+
.context()
|
|
1571
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
1572
|
+
.catch(() => {});
|
|
1573
|
+
await page.evaluate(async (clipboardPayload) => {
|
|
1574
|
+
const toArrayBuffer = (base64) => {
|
|
1575
|
+
if (!base64) {
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
const binaryString = atob(base64);
|
|
1579
|
+
const len = binaryString.length;
|
|
1580
|
+
const bytes = new Uint8Array(len);
|
|
1581
|
+
for (let i = 0; i < len; i += 1) {
|
|
1582
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
1583
|
+
}
|
|
1584
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1585
|
+
};
|
|
1586
|
+
|
|
1587
|
+
const createFileFromPayload = (filePayload) => {
|
|
1588
|
+
const buffer = toArrayBuffer(filePayload?.data);
|
|
1589
|
+
if (!buffer) {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
const name = filePayload?.name || "clipboard-file";
|
|
1593
|
+
const type = filePayload?.type || "application/octet-stream";
|
|
1594
|
+
const lastModified = filePayload?.lastModified ?? Date.now();
|
|
1595
|
+
try {
|
|
1596
|
+
return new File([buffer], name, { type, lastModified });
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
console.warn("Clipboard bridge could not recreate File object", error);
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
|
|
1603
|
+
let dataTransfer = null;
|
|
1604
|
+
try {
|
|
1605
|
+
dataTransfer = new DataTransfer();
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
console.warn("Clipboard bridge could not create DataTransfer", error);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
if (dataTransfer) {
|
|
1611
|
+
if (clipboardPayload?.text) {
|
|
1612
|
+
try {
|
|
1613
|
+
dataTransfer.setData("text/plain", clipboardPayload.text);
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
console.warn("Clipboard bridge failed to set text/plain", error);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
if (clipboardPayload?.html) {
|
|
1619
|
+
try {
|
|
1620
|
+
dataTransfer.setData("text/html", clipboardPayload.html);
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
console.warn("Clipboard bridge failed to set text/html", error);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (Array.isArray(clipboardPayload?.files)) {
|
|
1626
|
+
for (const filePayload of clipboardPayload.files) {
|
|
1627
|
+
const file = createFileFromPayload(filePayload);
|
|
1628
|
+
if (file) {
|
|
1629
|
+
try {
|
|
1630
|
+
dataTransfer.items.add(file);
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
console.warn("Clipboard bridge failed to append file", error);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
let target = document.activeElement || document.body;
|
|
1640
|
+
if (!target) {
|
|
1641
|
+
target = document.body || null;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
let pasteHandled = false;
|
|
1645
|
+
if (dataTransfer && target && typeof target.dispatchEvent === "function") {
|
|
1646
|
+
try {
|
|
1647
|
+
const clipboardEvent = new ClipboardEvent("paste", {
|
|
1648
|
+
clipboardData: dataTransfer,
|
|
1649
|
+
bubbles: true,
|
|
1650
|
+
cancelable: true,
|
|
1651
|
+
});
|
|
1652
|
+
pasteHandled = target.dispatchEvent(clipboardEvent);
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
console.warn("Clipboard bridge failed to dispatch synthetic paste event", error);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (pasteHandled) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const callLegacyExecCommand = (command, value) => {
|
|
1663
|
+
const execCommand = document && document["execCommand"];
|
|
1664
|
+
if (typeof execCommand === "function") {
|
|
1665
|
+
try {
|
|
1666
|
+
return execCommand.call(document, command, false, value);
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
console.warn("Clipboard bridge failed to execute legacy command", error);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return false;
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
if (clipboardPayload?.html) {
|
|
1675
|
+
const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
|
|
1676
|
+
if (inserted) {
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
try {
|
|
1680
|
+
const selection = window.getSelection?.();
|
|
1681
|
+
if (selection && selection.rangeCount > 0) {
|
|
1682
|
+
const range = selection.getRangeAt(0);
|
|
1683
|
+
range.deleteContents();
|
|
1684
|
+
const fragment = range.createContextualFragment(clipboardPayload.html);
|
|
1685
|
+
range.insertNode(fragment);
|
|
1686
|
+
range.collapse(false);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (clipboardPayload?.text) {
|
|
1695
|
+
const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
|
|
1696
|
+
if (inserted) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
try {
|
|
1700
|
+
const selection = window.getSelection?.();
|
|
1701
|
+
if (selection && selection.rangeCount > 0) {
|
|
1702
|
+
const range = selection.getRangeAt(0);
|
|
1703
|
+
range.deleteContents();
|
|
1704
|
+
range.insertNode(document.createTextNode(clipboardPayload.text));
|
|
1705
|
+
range.collapse(false);
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
console.warn("Clipboard bridge could not insert text via Range APIs", error);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (clipboardPayload?.text && target && "value" in target) {
|
|
1714
|
+
try {
|
|
1715
|
+
const input = target;
|
|
1716
|
+
const start = input.selectionStart ?? input.value.length ?? 0;
|
|
1717
|
+
const end = input.selectionEnd ?? input.value.length ?? 0;
|
|
1718
|
+
const value = input.value ?? "";
|
|
1719
|
+
const text = clipboardPayload.text;
|
|
1720
|
+
input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
|
|
1721
|
+
const caret = start + text.length;
|
|
1722
|
+
if (typeof input.setSelectionRange === "function") {
|
|
1723
|
+
input.setSelectionRange(caret, caret);
|
|
1724
|
+
}
|
|
1725
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
console.warn("Clipboard bridge failed to mutate input element", error);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}, payload);
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
throw error;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
async createTab(url) {
|
|
1737
|
+
try {
|
|
1738
|
+
await this.browserEmitter?.createTab(url);
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
this.logger.error("Error creating tab:", error);
|
|
1741
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1742
|
+
message: "Error creating tab",
|
|
1743
|
+
code: "CREATE_TAB_ERROR",
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
async closeTab({ pageId }) {
|
|
1749
|
+
try {
|
|
1750
|
+
await this.browserEmitter?.closeTab(pageId);
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
this.logger.error("Error closing tab:", error);
|
|
1753
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1754
|
+
message: "Error closing tab",
|
|
1755
|
+
code: "CLOSE_TAB_ERROR",
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
async selectTab({ pageId }) {
|
|
1761
|
+
try {
|
|
1762
|
+
await this.browserEmitter?.selectTab(pageId);
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
this.logger.error("Error selecting tab:", error);
|
|
1765
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1766
|
+
message: "Error selecting tab",
|
|
1767
|
+
code: "SELECT_TAB_ERROR",
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
async navigateTab({ pageId, url }) {
|
|
1773
|
+
try {
|
|
1774
|
+
if (!pageId || !url) {
|
|
1775
|
+
this.logger.error("navigateTab called without pageId or url", { pageId, url });
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
await this.browserEmitter?.navigateTab(pageId, url);
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
this.logger.error("Error navigating tab:", error);
|
|
1781
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1782
|
+
message: "Error navigating tab",
|
|
1783
|
+
code: "NAVIGATE_TAB_ERROR",
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
async reloadTab(pageId) {
|
|
1789
|
+
try {
|
|
1790
|
+
await this.browserEmitter?.reloadTab(pageId);
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
this.logger.error("Error reloading tab:", error);
|
|
1793
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1794
|
+
message: "Error reloading tab",
|
|
1795
|
+
code: "RELOAD_TAB_ERROR",
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
async goBack(pageId) {
|
|
1801
|
+
try {
|
|
1802
|
+
await this.browserEmitter?.goBack(pageId);
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
this.logger.error("Error navigating back:", error);
|
|
1805
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1806
|
+
message: "Error navigating back",
|
|
1807
|
+
code: "GO_BACK_ERROR",
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
async goForward(pageId) {
|
|
1813
|
+
try {
|
|
1814
|
+
await this.browserEmitter?.goForward(pageId);
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
this.logger.error("Error navigating forward:", error);
|
|
1817
|
+
this.sendEvent(this.events.browserStateError, {
|
|
1818
|
+
message: "Error navigating forward",
|
|
1819
|
+
code: "GO_FORWARD_ERROR",
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|