@dev-blinq/cucumber_client 1.0.1383-dev → 1.0.1383-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 +93 -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 +711 -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
|
@@ -2,162 +2,702 @@ import { readdirSync, readFileSync } from "fs";
|
|
|
2
2
|
import { getRunsServiceBaseURL } from "../utils/index.js";
|
|
3
3
|
import { axiosClient } from "../utils/axiosClient.js";
|
|
4
4
|
import path from "path";
|
|
5
|
-
|
|
5
|
+
import { EventEmitter } from "events";
|
|
6
|
+
import { v4 as uuidv4 } from "uuid"; // Import uuid
|
|
6
7
|
export class NamesService {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
8
|
+
screenshotMap;
|
|
9
|
+
TOKEN;
|
|
10
|
+
projectDir;
|
|
11
|
+
logger;
|
|
12
|
+
constructor({ screenshotMap, TOKEN, projectDir, logger }) {
|
|
13
|
+
this.screenshotMap = screenshotMap;
|
|
14
|
+
this.TOKEN = TOKEN;
|
|
15
|
+
this.projectDir = projectDir;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
}
|
|
18
|
+
//@ts-expect-error
|
|
19
|
+
async generateStepName({ commands, stepsNames, parameters, map }) {
|
|
20
|
+
let screenshot = this.screenshotMap.get(commands[commands.length - 1].inputID);
|
|
21
|
+
if (!screenshot && commands.length > 1) {
|
|
22
|
+
screenshot = this.screenshotMap.get(commands[commands.length - 2].inputID);
|
|
23
|
+
}
|
|
24
|
+
const url = `${getRunsServiceBaseURL()}/generate-step-information/generate`;
|
|
25
|
+
const TIMEOUT = 120; // 2 minutes
|
|
26
|
+
const { data } = await axiosClient({
|
|
27
|
+
url,
|
|
28
|
+
method: "POST",
|
|
29
|
+
data: {
|
|
30
|
+
commands,
|
|
31
|
+
stepsNames,
|
|
32
|
+
parameters,
|
|
33
|
+
//screenshot,
|
|
34
|
+
map,
|
|
35
|
+
},
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
38
|
+
"X-Source": "recorder",
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const actionUrl = `${getRunsServiceBaseURL()}/action/get-action?id=${data.id}`;
|
|
42
|
+
let result = { status: 500 };
|
|
43
|
+
for (let i = 0; i < TIMEOUT; i++) {
|
|
44
|
+
try {
|
|
45
|
+
const action = await axiosClient({
|
|
46
|
+
url: actionUrl,
|
|
47
|
+
method: "GET",
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
50
|
+
"X-Source": "recorder",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (action.data.status) {
|
|
54
|
+
result = action;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (i === TIMEOUT - 1) {
|
|
60
|
+
this.logger.error("Timeout while generating step details: ", error instanceof Error ? error : { message: JSON.stringify(error) });
|
|
61
|
+
console.error("Timeout while generating step details: ", error instanceof Error ? error.message : error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
65
|
+
}
|
|
66
|
+
if (result.status !== 200) {
|
|
67
|
+
return { success: false, message: "Error while generating step details" };
|
|
68
|
+
}
|
|
69
|
+
return result.data;
|
|
70
|
+
}
|
|
71
|
+
async generateScenarioAndFeatureNames(scenarioAsText) {
|
|
72
|
+
if (!this.projectDir) {
|
|
73
|
+
throw new Error("Project directory not found");
|
|
74
|
+
}
|
|
75
|
+
// read all files with .feature extension into an object {files:[{name: "", content: ""}]}
|
|
76
|
+
const featureFiles = readdirSync(path.join(this.projectDir, "features"));
|
|
77
|
+
const featureFilesContent = featureFiles
|
|
78
|
+
.filter((file) => file.endsWith(".feature"))
|
|
79
|
+
.map((file) => {
|
|
80
|
+
return {
|
|
81
|
+
name: file,
|
|
82
|
+
content: readFileSync(path.join(this.projectDir, "features", file), "utf8"),
|
|
83
|
+
};
|
|
60
84
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
} catch (error) {
|
|
67
|
-
if (i === TIMEOUT - 1) {
|
|
68
|
-
console.error("Timeout while generating step details: ", error);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (result.status !== 200) {
|
|
76
|
-
return { success: false, message: "Error while generating step details" };
|
|
77
|
-
}
|
|
78
|
-
return result.data;
|
|
79
|
-
}
|
|
80
|
-
async generateScenarioAndFeatureNames(scenarioAsText) {
|
|
81
|
-
if (!this.projectDir) {
|
|
82
|
-
throw new Error("Project directory not found");
|
|
83
|
-
}
|
|
84
|
-
// read all files with .feature extension into an object {files:[{name: "", content: ""}]}
|
|
85
|
-
const featureFiles = readdirSync(path.join(this.projectDir, "features"));
|
|
86
|
-
const featureFilesContent = featureFiles
|
|
87
|
-
.filter((file) => file.endsWith(".feature"))
|
|
88
|
-
.map((file) => {
|
|
89
|
-
return {
|
|
90
|
-
name: file,
|
|
91
|
-
content: readFileSync(path.join(this.projectDir, "features", file), "utf8"),
|
|
85
|
+
const genObject = {
|
|
86
|
+
files: featureFilesContent,
|
|
87
|
+
scenario: scenarioAsText,
|
|
92
88
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
89
|
+
// get screenshot for the last command
|
|
90
|
+
const url = `${getRunsServiceBaseURL()}/generate-step-information/generate_scenario_feature`;
|
|
91
|
+
const TIMEOUT = 120; // 2 minutes
|
|
92
|
+
const { data } = await axiosClient({
|
|
93
|
+
url,
|
|
94
|
+
method: "POST",
|
|
95
|
+
data: {
|
|
96
|
+
...genObject,
|
|
97
|
+
},
|
|
98
|
+
headers: {
|
|
99
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
100
|
+
"X-Source": "recorder",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const actionUrl = `${getRunsServiceBaseURL()}/action/get-action?id=${data.id}`;
|
|
104
|
+
let result = { status: 500 };
|
|
105
|
+
for (let i = 0; i < TIMEOUT; i++) {
|
|
106
|
+
try {
|
|
107
|
+
const action = await axiosClient({
|
|
108
|
+
url: actionUrl,
|
|
109
|
+
method: "GET",
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
112
|
+
"X-Source": "recorder",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
if (action.data.status) {
|
|
116
|
+
result = action;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (i === TIMEOUT - 1) {
|
|
122
|
+
console.error("Timeout while generating step details: ", error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
126
|
+
}
|
|
127
|
+
if (result.status !== 200) {
|
|
128
|
+
return { success: false, message: "Error while generating step details" };
|
|
129
|
+
}
|
|
130
|
+
return result.data;
|
|
131
|
+
}
|
|
132
|
+
//@ts-expect-error
|
|
133
|
+
async generateCommandName({ command }) {
|
|
134
|
+
const url = `${getRunsServiceBaseURL()}/generate-step-information/generate-element-name`;
|
|
135
|
+
const screenshot = this.screenshotMap.get(command.inputID);
|
|
136
|
+
const result = await axiosClient({
|
|
137
|
+
url,
|
|
138
|
+
method: "POST",
|
|
139
|
+
data: { ...command, screenshot },
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
142
|
+
"X-Source": "recorder",
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
if (result.status !== 200) {
|
|
146
|
+
return { success: false, message: "Error while generating command details" };
|
|
147
|
+
}
|
|
148
|
+
return result.data;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export class PublishService {
|
|
152
|
+
TOKEN;
|
|
153
|
+
constructor(TOKEN) {
|
|
154
|
+
this.TOKEN = TOKEN;
|
|
155
|
+
}
|
|
156
|
+
async saveScenario({ scenario, featureName, override, branch, isEditing, projectId, env }) {
|
|
157
|
+
const runsURL = getRunsServiceBaseURL();
|
|
158
|
+
const workspaceURL = runsURL.replace("/runs", "/workspace");
|
|
159
|
+
const url = `${workspaceURL}/publish-recording`;
|
|
160
|
+
const result = await axiosClient({
|
|
161
|
+
url,
|
|
162
|
+
method: "POST",
|
|
163
|
+
data: {
|
|
164
|
+
scenario,
|
|
165
|
+
featureName,
|
|
166
|
+
override,
|
|
167
|
+
env,
|
|
168
|
+
},
|
|
169
|
+
params: {
|
|
170
|
+
branch,
|
|
171
|
+
},
|
|
172
|
+
headers: {
|
|
173
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
174
|
+
"X-Source": "recorder",
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
if (result.status !== 200) {
|
|
178
|
+
return { success: false, message: "Error while saving scenario" };
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
this.updateProjectMetadata({
|
|
182
|
+
featureName,
|
|
183
|
+
scenarioName: scenario.name,
|
|
184
|
+
projectId,
|
|
185
|
+
branch,
|
|
186
|
+
isEditing: override,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (error) { }
|
|
190
|
+
return { success: true, data: result.data };
|
|
191
|
+
}
|
|
192
|
+
async updateProjectMetadata({ featureName, scenarioName, projectId, branch, isEditing }) {
|
|
193
|
+
try {
|
|
194
|
+
await axiosClient({
|
|
195
|
+
method: "POST",
|
|
196
|
+
url: `${getRunsServiceBaseURL()}/project/updateProjectMetadata`,
|
|
197
|
+
data: {
|
|
198
|
+
featureName,
|
|
199
|
+
scenarioName,
|
|
200
|
+
eventType: isEditing ? "editScenario" : "createScenario",
|
|
201
|
+
projectId,
|
|
202
|
+
branch: branch,
|
|
203
|
+
},
|
|
204
|
+
headers: {
|
|
205
|
+
Authorization: `Bearer ${this.TOKEN}`,
|
|
206
|
+
"X-Source": "recorder",
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
// logger.error("Failed to update project metadata: " + error.message);
|
|
212
|
+
// @ts-ignore
|
|
213
|
+
console.error("run_recorder", `Failed to update project metadata: ${error.message ?? error}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export class RemoteBrowserService extends EventEmitter {
|
|
218
|
+
CDP_CONNECT_URL;
|
|
219
|
+
context;
|
|
220
|
+
pages = new Map();
|
|
221
|
+
_selectedPageId = null;
|
|
222
|
+
wsUrlBase; // Store the base URL
|
|
223
|
+
constructor({ CDP_CONNECT_URL, context }) {
|
|
224
|
+
super();
|
|
225
|
+
this.CDP_CONNECT_URL = CDP_CONNECT_URL;
|
|
226
|
+
this.context = context;
|
|
227
|
+
this.wsUrlBase = this.CDP_CONNECT_URL.replace(/^http/, "ws") + "/devtools/page/";
|
|
228
|
+
this.log("🚀 RemoteBrowserService initialized", { CDP_CONNECT_URL });
|
|
229
|
+
this.context.grantPermissions(["clipboard-read", "clipboard-write"]).catch((error) => {
|
|
230
|
+
this.log("⚠️ Failed to pre-grant clipboard permissions", { error });
|
|
231
|
+
});
|
|
232
|
+
this.initializeListeners();
|
|
233
|
+
}
|
|
234
|
+
log(message, data) {
|
|
235
|
+
const timestamp = new Date().toISOString();
|
|
236
|
+
console.log(`[${timestamp}] [RemoteBrowserService] ${message}`, data ? JSON.stringify(data, null, 2) : "");
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Gets the CDP Target ID for a page *directly* from the page.
|
|
240
|
+
* This is the only reliable method during restarts.
|
|
241
|
+
*/
|
|
242
|
+
async getCdpTargetId(page) {
|
|
243
|
+
try {
|
|
244
|
+
if (page.isClosed()) {
|
|
245
|
+
this.log("⚠️ Attempted to get CDP ID from a closed page");
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const cdpSession = await page.context().newCDPSession(page);
|
|
249
|
+
const { targetInfo } = await cdpSession.send("Target.getTargetInfo");
|
|
250
|
+
this.log("🔍 Retrieved TargetInfo via CDP session", targetInfo);
|
|
251
|
+
await cdpSession.detach();
|
|
252
|
+
if (targetInfo && targetInfo.targetId) {
|
|
253
|
+
this.log("✅ Found CDP ID by session", { id: targetInfo.targetId, url: page.url() });
|
|
254
|
+
return targetInfo.targetId;
|
|
255
|
+
}
|
|
256
|
+
throw new Error("Target.getTargetInfo did not return a targetId");
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
this.log("❌ Error getting CDP ID by session", { url: page.url(), error });
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async initializeListeners() {
|
|
264
|
+
this.log("📡 Initializing listeners");
|
|
265
|
+
// Initialize with existing pages
|
|
266
|
+
const existingPages = this.context.pages();
|
|
267
|
+
this.log("📄 Found existing pages", { count: existingPages.length });
|
|
268
|
+
for (const page of existingPages) {
|
|
269
|
+
const stableTabId = uuidv4();
|
|
270
|
+
const cdpTargetId = await this.getCdpTargetId(page);
|
|
271
|
+
this.pages.set(stableTabId, { page, cdpTargetId });
|
|
272
|
+
this.log("✅ Existing page added to map", { stableTabId, cdpTargetId, url: page.url() });
|
|
273
|
+
this.attachPageLifecycleListeners(page, stableTabId);
|
|
274
|
+
this.log("🔍 Attached lifecycle listeners to existing page", { stableTabId });
|
|
275
|
+
}
|
|
276
|
+
if (this.pages.size > 0 && !this._selectedPageId) {
|
|
277
|
+
this._selectedPageId = Array.from(this.pages.keys())[0];
|
|
278
|
+
}
|
|
279
|
+
await this.syncState();
|
|
280
|
+
this.context.on("page", async (page) => {
|
|
281
|
+
const stableTabId = uuidv4();
|
|
282
|
+
this.log("🆕 New page event triggered", { stableTabId, url: page.url() });
|
|
283
|
+
// We get the ID immediately, but it might be null if the page is too new
|
|
284
|
+
const cdpTargetId = await this.getCdpTargetId(page);
|
|
285
|
+
this.pages.set(stableTabId, { page, cdpTargetId });
|
|
286
|
+
if (cdpTargetId) {
|
|
287
|
+
this.log("✅ Page mapped to CDP ID", { stableTabId, cdpTargetId });
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
this.log("⚠️ Could not find CDP ID for new page yet", { stableTabId });
|
|
291
|
+
}
|
|
292
|
+
if (!this._selectedPageId) {
|
|
293
|
+
// Select the first page that opens
|
|
294
|
+
this._selectedPageId = stableTabId;
|
|
295
|
+
this.log("🎯 Initial selected page set", { selectedPageId: this._selectedPageId });
|
|
296
|
+
}
|
|
297
|
+
this.attachPageLifecycleListeners(page, stableTabId);
|
|
298
|
+
await this.syncState();
|
|
125
299
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
300
|
+
}
|
|
301
|
+
attachPageLifecycleListeners(page, stableTabId) {
|
|
302
|
+
page.on("load", () => this.syncState());
|
|
303
|
+
page.on("framenavigated", () => this.syncState());
|
|
304
|
+
page.on("close", async () => {
|
|
305
|
+
this.log("🗑️ Page close event", { stableTabId });
|
|
306
|
+
this.pages.delete(stableTabId);
|
|
307
|
+
if (this._selectedPageId === stableTabId) {
|
|
308
|
+
this._selectedPageId = this.pages.size > 0 ? Array.from(this.pages.keys())[0] : null;
|
|
309
|
+
this.log("🔄 Selected page changed after close", { newSelectedId: this._selectedPageId });
|
|
310
|
+
}
|
|
311
|
+
await this.syncState();
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async syncState() {
|
|
315
|
+
try {
|
|
316
|
+
this.log("🔄 Starting state sync");
|
|
317
|
+
const state = await this.getState();
|
|
318
|
+
this.log("✅ State sync complete", { pagesCount: state.pages.length, selectedPageId: state.selectedPageId });
|
|
319
|
+
this.emit("BrowserService.stateSync", state);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
this.log("❌ Error syncing state", { error });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async getState() {
|
|
326
|
+
this.log("📊 Getting current state");
|
|
327
|
+
const pagesData = [];
|
|
328
|
+
const pagesToDelete = []; // To clean up closed pages
|
|
329
|
+
for (const [stableTabId, pageInfo] of this.pages.entries()) {
|
|
330
|
+
try {
|
|
331
|
+
if (pageInfo.page.isClosed()) {
|
|
332
|
+
this.log("🧹 Found closed page during getState, marking for deletion", { stableTabId });
|
|
333
|
+
pagesToDelete.push(stableTabId);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
// Get the one, true, live CDP ID
|
|
337
|
+
const currentCdpId = await this.getCdpTargetId(pageInfo.page);
|
|
338
|
+
if (currentCdpId && pageInfo.cdpTargetId !== currentCdpId) {
|
|
339
|
+
this.log("🔄 CDP ID changed", { stableTabId, old: pageInfo.cdpTargetId, new: currentCdpId });
|
|
340
|
+
pageInfo.cdpTargetId = currentCdpId; // Update our internal reference
|
|
341
|
+
}
|
|
342
|
+
// Manually construct the WebSocket URL
|
|
343
|
+
const wsDebuggerUrl = currentCdpId ? `${this.wsUrlBase}${currentCdpId}` : "";
|
|
344
|
+
if (!wsDebuggerUrl) {
|
|
345
|
+
this.log("⚠️ Could not get CDP ID, wsDebuggerUrl will be empty", { stableTabId });
|
|
346
|
+
}
|
|
347
|
+
const title = await pageInfo.page.title();
|
|
348
|
+
pagesData.push({
|
|
349
|
+
id: stableTabId,
|
|
350
|
+
title,
|
|
351
|
+
url: pageInfo.page.url(),
|
|
352
|
+
wsDebuggerUrl: wsDebuggerUrl, // Use the constructed URL
|
|
353
|
+
});
|
|
354
|
+
this.log(`🟥Page data: ${JSON.stringify(pagesData, null, 2)}`);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
this.log("❌ Error getting page data", { stableTabId, error });
|
|
358
|
+
pagesToDelete.push(stableTabId); // Mark for deletion
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
pagesToDelete.forEach((id) => this.pages.delete(id));
|
|
362
|
+
if (this._selectedPageId && !this.pages.has(this._selectedPageId)) {
|
|
363
|
+
this._selectedPageId = pagesData.length > 0 ? pagesData[0].id : null;
|
|
364
|
+
this.log("🔄 Corrected selectedPageId", { new: this._selectedPageId });
|
|
365
|
+
}
|
|
366
|
+
if (!this._selectedPageId && pagesData.length > 0) {
|
|
367
|
+
this._selectedPageId = pagesData[0].id;
|
|
368
|
+
this.log("🎯 Set default selectedPageId", { new: this._selectedPageId });
|
|
369
|
+
}
|
|
370
|
+
const state = {
|
|
371
|
+
pages: pagesData,
|
|
372
|
+
selectedPageId: this._selectedPageId,
|
|
373
|
+
};
|
|
374
|
+
this.log("📦 Final state", state);
|
|
375
|
+
return state;
|
|
376
|
+
}
|
|
377
|
+
async createTab(url = "about:blank") {
|
|
378
|
+
try {
|
|
379
|
+
this.log("🆕 Creating new tab", { url });
|
|
380
|
+
const page = await this.context.newPage(); // This will trigger the 'page' event
|
|
381
|
+
if (url !== "about:blank") {
|
|
382
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
383
|
+
}
|
|
384
|
+
for (const [stableTabId, pageInfo] of this.pages.entries()) {
|
|
385
|
+
if (pageInfo.page === page) {
|
|
386
|
+
this._selectedPageId = stableTabId;
|
|
387
|
+
this.log("✅ New tab created and selected", { stableTabId, url });
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
await this.syncState();
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
this.log("❌ Error creating tab", { error });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async closeTab(stableTabId) {
|
|
398
|
+
try {
|
|
399
|
+
this.log("🗑️ Closing tab", { stableTabId });
|
|
400
|
+
const pageInfo = this.pages.get(stableTabId);
|
|
401
|
+
if (pageInfo) {
|
|
402
|
+
await pageInfo.page.close(); // This will trigger the 'close' event
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
this.log("⚠️ Page not found for closing", { stableTabId });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
this.log("❌ Error closing tab", { error });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async selectTab(stableTabId) {
|
|
413
|
+
try {
|
|
414
|
+
this.log("🎯 Selecting tab", { stableTabId });
|
|
415
|
+
const pageInfo = this.pages.get(stableTabId);
|
|
416
|
+
if (pageInfo) {
|
|
417
|
+
this._selectedPageId = stableTabId;
|
|
418
|
+
await pageInfo.page.bringToFront();
|
|
419
|
+
this.log("✅ Tab selected successfully", { stableTabId });
|
|
420
|
+
await this.syncState();
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
this.log("⚠️ Page not found for selection", { stableTabId });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
this.log("❌ Error selecting tab", { error });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
getPageInfo(stableTabId) {
|
|
431
|
+
if (!stableTabId) {
|
|
432
|
+
this.log("⚠️ Operation requested without a selected tab");
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const pageInfo = this.pages.get(stableTabId);
|
|
436
|
+
if (!pageInfo) {
|
|
437
|
+
this.log("⚠️ Page not found for operation", { stableTabId });
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
return pageInfo;
|
|
441
|
+
}
|
|
442
|
+
async navigateTab(stableTabId, url) {
|
|
443
|
+
const pageInfo = this.getPageInfo(stableTabId);
|
|
444
|
+
if (!pageInfo) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
this.log("🌐 Navigating tab", { stableTabId, url });
|
|
449
|
+
await pageInfo.page.goto(url, { waitUntil: "domcontentloaded" });
|
|
450
|
+
this._selectedPageId = stableTabId;
|
|
451
|
+
await this.syncState();
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
this.log("❌ Error navigating tab", { stableTabId, url, error });
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async reloadTab(stableTabId) {
|
|
459
|
+
const pageInfo = this.getPageInfo(stableTabId);
|
|
460
|
+
if (!pageInfo) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
this.log("🔄 Reloading tab", { stableTabId, url: pageInfo.page.url() });
|
|
465
|
+
await pageInfo.page.reload({ waitUntil: "domcontentloaded" });
|
|
466
|
+
await this.syncState();
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
this.log("❌ Error reloading tab", { stableTabId, error });
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async goBack(stableTabId) {
|
|
474
|
+
const pageInfo = this.getPageInfo(stableTabId);
|
|
475
|
+
if (!pageInfo) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
this.log("⬅️ Navigating back", { stableTabId });
|
|
480
|
+
const response = await pageInfo.page.goBack({ waitUntil: "domcontentloaded" });
|
|
481
|
+
if (!response) {
|
|
482
|
+
this.log("ℹ️ No history entry to go back to", { stableTabId });
|
|
483
|
+
}
|
|
484
|
+
await this.syncState();
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
this.log("❌ Error navigating back", { stableTabId, error });
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async goForward(stableTabId) {
|
|
492
|
+
const pageInfo = this.getPageInfo(stableTabId);
|
|
493
|
+
if (!pageInfo) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
this.log("➡️ Navigating forward", { stableTabId });
|
|
498
|
+
const response = await pageInfo.page.goForward({ waitUntil: "domcontentloaded" });
|
|
499
|
+
if (!response) {
|
|
500
|
+
this.log("ℹ️ No history entry to go forward to", { stableTabId });
|
|
501
|
+
}
|
|
502
|
+
await this.syncState();
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
this.log("❌ Error navigating forward", { stableTabId, error });
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async applyClipboardPayload(payload) {
|
|
510
|
+
const pageInfo = this.getPageInfo(this._selectedPageId);
|
|
511
|
+
if (!pageInfo) {
|
|
512
|
+
this.log("⚠️ No active page available for clipboard payload");
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
await pageInfo.page
|
|
517
|
+
.context()
|
|
518
|
+
.grantPermissions(["clipboard-read", "clipboard-write"])
|
|
519
|
+
.catch(() => { });
|
|
520
|
+
await pageInfo.page.evaluate(async (clipboardPayload) => {
|
|
521
|
+
const toArrayBuffer = (base64) => {
|
|
522
|
+
if (!base64) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
const binary = atob(base64);
|
|
526
|
+
const length = binary.length;
|
|
527
|
+
const bytes = new Uint8Array(length);
|
|
528
|
+
for (let index = 0; index < length; index += 1) {
|
|
529
|
+
bytes[index] = binary.charCodeAt(index);
|
|
530
|
+
}
|
|
531
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
532
|
+
};
|
|
533
|
+
const createFileFromPayload = (filePayload) => {
|
|
534
|
+
const buffer = toArrayBuffer(filePayload?.data);
|
|
535
|
+
if (!buffer) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const name = filePayload?.name || "clipboard-file";
|
|
539
|
+
const type = filePayload?.type || "application/octet-stream";
|
|
540
|
+
const lastModified = filePayload?.lastModified ?? Date.now();
|
|
541
|
+
try {
|
|
542
|
+
return new File([buffer], name, { type, lastModified });
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
console.warn("Clipboard bridge could not recreate File", error);
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
let dataTransfer = null;
|
|
550
|
+
try {
|
|
551
|
+
dataTransfer = new DataTransfer();
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
console.warn("Clipboard bridge could not create DataTransfer", error);
|
|
555
|
+
}
|
|
556
|
+
const callLegacyExecCommand = (command, value) => {
|
|
557
|
+
const execCommand = document.execCommand;
|
|
558
|
+
if (typeof execCommand === "function") {
|
|
559
|
+
try {
|
|
560
|
+
return execCommand.call(document, command, false, value);
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
console.warn("Clipboard bridge failed to execute legacy command", error);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
567
|
+
};
|
|
568
|
+
if (dataTransfer) {
|
|
569
|
+
if (clipboardPayload?.text) {
|
|
570
|
+
try {
|
|
571
|
+
dataTransfer.setData("text/plain", clipboardPayload.text);
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
console.warn("Clipboard bridge failed to attach text/plain", error);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (clipboardPayload?.html) {
|
|
578
|
+
try {
|
|
579
|
+
dataTransfer.setData("text/html", clipboardPayload.html);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
console.warn("Clipboard bridge failed to attach text/html", error);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (Array.isArray(clipboardPayload?.files)) {
|
|
586
|
+
for (const filePayload of clipboardPayload.files) {
|
|
587
|
+
const file = createFileFromPayload(filePayload);
|
|
588
|
+
if (file) {
|
|
589
|
+
try {
|
|
590
|
+
dataTransfer.items.add(file);
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
console.warn("Clipboard bridge failed to attach file", error);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
let target = document.activeElement;
|
|
600
|
+
if (!target || target === document.body) {
|
|
601
|
+
target = document.body;
|
|
602
|
+
}
|
|
603
|
+
let pasteHandled = false;
|
|
604
|
+
if (dataTransfer && target && typeof target.dispatchEvent === "function") {
|
|
605
|
+
try {
|
|
606
|
+
const clipboardEvent = new ClipboardEvent("paste", {
|
|
607
|
+
clipboardData: dataTransfer,
|
|
608
|
+
bubbles: true,
|
|
609
|
+
cancelable: true,
|
|
610
|
+
});
|
|
611
|
+
pasteHandled = target.dispatchEvent(clipboardEvent);
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
console.warn("Clipboard bridge failed to dispatch paste event", error);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (pasteHandled) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (clipboardPayload?.html) {
|
|
621
|
+
const inserted = callLegacyExecCommand("insertHTML", clipboardPayload.html);
|
|
622
|
+
if (inserted) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
const selection = window.getSelection?.();
|
|
627
|
+
if (selection && selection.rangeCount > 0) {
|
|
628
|
+
const range = selection.getRangeAt(0);
|
|
629
|
+
range.deleteContents();
|
|
630
|
+
const fragment = range.createContextualFragment(clipboardPayload.html);
|
|
631
|
+
range.insertNode(fragment);
|
|
632
|
+
range.collapse(false);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
console.warn("Clipboard bridge could not insert HTML via Range APIs", error);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (clipboardPayload?.text) {
|
|
641
|
+
const inserted = callLegacyExecCommand("insertText", clipboardPayload.text);
|
|
642
|
+
if (inserted) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
const selection = window.getSelection?.();
|
|
647
|
+
if (selection && selection.rangeCount > 0) {
|
|
648
|
+
const range = selection.getRangeAt(0);
|
|
649
|
+
range.deleteContents();
|
|
650
|
+
range.insertNode(document.createTextNode(clipboardPayload.text));
|
|
651
|
+
range.collapse(false);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
console.warn("Clipboard bridge could not insert text via Range APIs", error);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (clipboardPayload?.text && target && "value" in target) {
|
|
660
|
+
try {
|
|
661
|
+
const input = target;
|
|
662
|
+
const start = input.selectionStart ?? input.value.length ?? 0;
|
|
663
|
+
const end = input.selectionEnd ?? input.value.length ?? 0;
|
|
664
|
+
const value = input.value ?? "";
|
|
665
|
+
const text = clipboardPayload.text;
|
|
666
|
+
input.value = `${value.slice(0, start)}${text}${value.slice(end)}`;
|
|
667
|
+
const caret = start + text.length;
|
|
668
|
+
if (typeof input.setSelectionRange === "function") {
|
|
669
|
+
input.setSelectionRange(caret, caret);
|
|
670
|
+
}
|
|
671
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
console.warn("Clipboard bridge failed to insert text into input", error);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}, payload);
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
this.log("❌ Error applying clipboard payload", { error });
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
getSelectedPage() {
|
|
685
|
+
const pageInfo = this._selectedPageId ? this.pages.get(this._selectedPageId) : null;
|
|
686
|
+
this.log("🔍 Getting selected page", {
|
|
687
|
+
selectedPageId: this._selectedPageId,
|
|
688
|
+
found: !!pageInfo,
|
|
689
|
+
url: pageInfo?.page.url(),
|
|
690
|
+
});
|
|
691
|
+
return pageInfo?.page || null;
|
|
692
|
+
}
|
|
693
|
+
destroy() {
|
|
694
|
+
this.log("💥 Destroying RemoteBrowserService");
|
|
695
|
+
this.context.removeAllListeners("page");
|
|
696
|
+
for (const [, pageInfo] of this.pages.entries()) {
|
|
697
|
+
pageInfo.page.removeAllListeners(); // Remove all listeners from each page
|
|
698
|
+
}
|
|
699
|
+
this.pages.clear();
|
|
700
|
+
this._selectedPageId = null;
|
|
701
|
+
this.removeAllListeners();
|
|
702
|
+
}
|
|
163
703
|
}
|