ff-automationv2 1.0.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/ARCHITECTURE.md +112 -0
- package/FireFlink_Architecture.drawio +68 -0
- package/ONBOARDING.md +29 -0
- package/README.md +28 -0
- package/TECHNICAL_DEEP_DIVE.md +26 -0
- package/eslint.config.ts +29 -0
- package/package.json +51 -0
- package/src/ai/llmcalls/decodeApiKey.ts +14 -0
- package/src/ai/llmcalls/llmAction.ts +89 -0
- package/src/ai/llmcalls/parseLlmOputput.ts +69 -0
- package/src/ai/llmprompts/promptRegistry.ts +16 -0
- package/src/ai/llmprompts/systemPrompts/actionExtractorPrompt.ts +70 -0
- package/src/ai/llmprompts/systemPrompts/errorDescriptionPrompt.ts +23 -0
- package/src/ai/llmprompts/systemPrompts/fireflinkElementIndexExtactors.ts +198 -0
- package/src/ai/llmprompts/systemPrompts/userStoryToListPrompt.ts +24 -0
- package/src/ai/llmprompts/systemPrompts/visionPrompt.ts +28 -0
- package/src/ai/llmprompts/userPrompts/userPrompt.ts +41 -0
- package/src/automation/actions/executor.ts +75 -0
- package/src/automation/actions/interaction/click.ts +25 -0
- package/src/automation/actions/interaction/enterInput.ts +27 -0
- package/src/automation/actions/interface/interactionActionInterface.ts +27 -0
- package/src/automation/actions/interface/navigationActionInterface.ts +22 -0
- package/src/automation/actions/interface/waitActionInterface.ts +6 -0
- package/src/automation/actions/navigation/getTitle.ts +9 -0
- package/src/automation/actions/navigation/goBack.ts +9 -0
- package/src/automation/actions/navigation/navigate.ts +10 -0
- package/src/automation/actions/navigation/refresh.ts +9 -0
- package/src/automation/actions/wait/wait.ts +10 -0
- package/src/automation/browserSession/initiateBrowserSession.ts +81 -0
- package/src/core/constants/supportedActions.ts +8 -0
- package/src/core/interfaces/StableDomInterface.ts +6 -0
- package/src/core/interfaces/actionInterface.ts +13 -0
- package/src/core/interfaces/automationRunnerInterface.ts +3 -0
- package/src/core/interfaces/browserCapabilitiesInterface.ts +5 -0
- package/src/core/interfaces/browserConfigurationInterface.ts +3 -0
- package/src/core/interfaces/domAnalysisInterface.ts +34 -0
- package/src/core/interfaces/executionDetails.ts +29 -0
- package/src/core/interfaces/fireflinkScriptPayloadInterface.ts +39 -0
- package/src/core/interfaces/llmConfigurationInterface.ts +3 -0
- package/src/core/interfaces/llmResponseInterface.ts +38 -0
- package/src/core/interfaces/promptInterface.ts +21 -0
- package/src/core/interfaces/scriptGenrationDataInterface.ts +16 -0
- package/src/core/interfaces/toolsInterface.ts +5 -0
- package/src/core/main/actionHandlerFactory.ts +86 -0
- package/src/core/main/executionContext.ts +18 -0
- package/src/core/main/runAutomationScript.ts +177 -0
- package/src/core/main/stepProcessor.ts +28 -0
- package/src/core/types/llmResponseType.ts +11 -0
- package/src/core/types/promptMap.ts +7 -0
- package/src/core/types/promptType.ts +7 -0
- package/src/core/types/visionllmInputType.ts +4 -0
- package/src/domAnalysis/getRelaventElements.ts +24 -0
- package/src/domAnalysis/relativeElementsFromDom.ts +94 -0
- package/src/domAnalysis/searchBest.ts +159 -0
- package/src/domAnalysis/simplifyAndFlatten.ts +118 -0
- package/src/fireflinkData/fireflinkLocators/elementsFromHTML.ts +656 -0
- package/src/fireflinkData/fireflinkLocators/getListOfLocators.ts +31 -0
- package/src/fireflinkData/fireflinkLocators/typeList.ts +36 -0
- package/src/fireflinkData/fireflinkScript/scriptGenrationData.ts +30 -0
- package/src/index.ts +5 -0
- package/src/llmConfig/llmConfiguration.ts +26 -0
- package/src/service/fireflinkApi.service.ts +46 -0
- package/src/service/scriptRunner.service.ts +83 -0
- package/src/utils/DomExtraction/jsForAttributeInjection.ts +254 -0
- package/src/utils/javascript/jsFindElement.ts +161 -0
- package/src/utils/javascript/jsForShadowRoot.ts +216 -0
- package/src/utils/javascript/jsForToaster.ts +60 -0
- package/src/utils/logger/logData.ts +36 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const elementType: { [key: string]: string } = {
|
|
2
|
+
'Link': 'link',
|
|
3
|
+
'Text Field': 'textfield',
|
|
4
|
+
'Icon': 'icon',
|
|
5
|
+
'Button': 'button',
|
|
6
|
+
'Radio Button': 'radioButton',
|
|
7
|
+
'Text': 'text',
|
|
8
|
+
'Textarea': 'textarea',
|
|
9
|
+
'Image': 'image',
|
|
10
|
+
'Dropdown': 'dropdown',
|
|
11
|
+
'Checkbox': 'checkbox',
|
|
12
|
+
'Tab': 'tab',
|
|
13
|
+
'Action Overflow button': 'action overflow button',
|
|
14
|
+
'Hamburger menu': 'hamburger icon',
|
|
15
|
+
'Toggle Button': 'toggle button',
|
|
16
|
+
'Suggestion': 'suggestion',
|
|
17
|
+
'Time Picker': 'time picker',
|
|
18
|
+
'Date Picker': 'date picker',
|
|
19
|
+
'Toaster Message': 'toaster message',
|
|
20
|
+
'Card': 'card',
|
|
21
|
+
'Tooltip': 'tooltip',
|
|
22
|
+
'Option': 'option',
|
|
23
|
+
'Calender': 'calender',
|
|
24
|
+
'Sliders': 'sliders',
|
|
25
|
+
'Visual Testing': 'visual testing'
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export const normalizedElementType: Record<string, string> =
|
|
29
|
+
Object.fromEntries(
|
|
30
|
+
Object.entries(elementType).map(([key, value]) => [
|
|
31
|
+
key.toLowerCase().replace(/\s+/g, ""),
|
|
32
|
+
value,
|
|
33
|
+
])
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IScriptGenerationData } from "../../core/interfaces/scriptGenrationDataInterface.js";
|
|
2
|
+
import { IPayload } from "../../core/interfaces/fireflinkScriptPayloadInterface.js";
|
|
3
|
+
import { FireFlinkApiService } from "../../service/fireflinkApi.service.js";
|
|
4
|
+
import { ScriptRunner } from "../../service/scriptRunner.service.js";
|
|
5
|
+
|
|
6
|
+
const apiService = new FireFlinkApiService();
|
|
7
|
+
const scriptRunner = new ScriptRunner(apiService);
|
|
8
|
+
|
|
9
|
+
export class ScriptDataAppender {
|
|
10
|
+
private data: IScriptGenerationData[] = [];
|
|
11
|
+
add(item: IScriptGenerationData): void {
|
|
12
|
+
this.data.push(item);
|
|
13
|
+
}
|
|
14
|
+
getData(): IScriptGenerationData[] {
|
|
15
|
+
return this.data;
|
|
16
|
+
}
|
|
17
|
+
produceScriptGenerationData(
|
|
18
|
+
result: any,
|
|
19
|
+
payload: IPayload,
|
|
20
|
+
token: string,
|
|
21
|
+
serverHost: string
|
|
22
|
+
): Promise<any> {
|
|
23
|
+
return scriptRunner.runScriptFromPayload(
|
|
24
|
+
result,
|
|
25
|
+
payload,
|
|
26
|
+
token,
|
|
27
|
+
serverHost
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
import { IServiceProviderBaseUrlConfiguration } from "../core/interfaces/llmConfigurationInterface.js";
|
|
3
|
+
export class ServiceProviderBaseUrlProvider
|
|
4
|
+
implements IServiceProviderBaseUrlConfiguration {
|
|
5
|
+
|
|
6
|
+
private readonly baseUrls: Record<string, string> = {
|
|
7
|
+
Groq: "https://api.groq.com/openai/v1",
|
|
8
|
+
DefaultFireFlink: "https://api.groq.com/openai/v1",
|
|
9
|
+
OpenAi: "https://api.openai.com/v1",
|
|
10
|
+
Anthropic: "https://api.anthropic.com/v1/",
|
|
11
|
+
Gemini: "https://generativelanguage.googleapis.com/v1beta/openai/"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
public getBaseUrl(provider: string): string {
|
|
15
|
+
const url = this.baseUrls[provider];
|
|
16
|
+
|
|
17
|
+
if (!url) {
|
|
18
|
+
throw new Error(`Unsupported service provider: ${provider}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import axios, { AxiosError } from "axios";
|
|
2
|
+
import { IFireFlinkApiService } from "../core/interfaces/fireflinkScriptPayloadInterface.js";
|
|
3
|
+
|
|
4
|
+
export class FireFlinkApiService implements IFireFlinkApiService {
|
|
5
|
+
|
|
6
|
+
async callFireFlinkApi(
|
|
7
|
+
headers: Record<string, string>,
|
|
8
|
+
body: any,
|
|
9
|
+
url: string
|
|
10
|
+
): Promise<any> {
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const response = await axios.post(url, body, {
|
|
14
|
+
headers,
|
|
15
|
+
timeout: 30000,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return response.data;
|
|
19
|
+
|
|
20
|
+
} catch (error: any) {
|
|
21
|
+
|
|
22
|
+
const err = error as AxiosError;
|
|
23
|
+
|
|
24
|
+
if (err.code === "ECONNABORTED") {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Request to FireFlink API timed out",
|
|
27
|
+
{ cause: error }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (err.response) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`HTTP Error ${err.response.status}: ${JSON.stringify(
|
|
34
|
+
err.response.data
|
|
35
|
+
)}`,
|
|
36
|
+
{ cause: error }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Unexpected error while calling FireFlink API",
|
|
42
|
+
{ cause: error }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IPayload,
|
|
3
|
+
IScriptRunner,
|
|
4
|
+
IStep,
|
|
5
|
+
IFireFlinkApiService,
|
|
6
|
+
} from "../core/interfaces/fireflinkScriptPayloadInterface";
|
|
7
|
+
|
|
8
|
+
import { logger } from "../utils/logger/logData.js"
|
|
9
|
+
|
|
10
|
+
export class ScriptRunner implements IScriptRunner {
|
|
11
|
+
constructor(private apiService: IFireFlinkApiService) { }
|
|
12
|
+
|
|
13
|
+
async runScriptFromPayload(
|
|
14
|
+
result: any[] | [],
|
|
15
|
+
payload: IPayload,
|
|
16
|
+
token: string,
|
|
17
|
+
serverHost: string
|
|
18
|
+
): Promise<any> {
|
|
19
|
+
let stepData: IStep[];
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
stepData =
|
|
23
|
+
typeof result === "string" ? JSON.parse(result) : result;
|
|
24
|
+
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger.error(`Failed to parse final llm data: ${error}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
logger.info("Sending script payload to FireFlink");
|
|
30
|
+
|
|
31
|
+
const body = {
|
|
32
|
+
stepData: stepData,
|
|
33
|
+
scriptName: payload.scriptName,
|
|
34
|
+
scriptType: payload.scriptType,
|
|
35
|
+
projectId: payload.projectId,
|
|
36
|
+
testCaseId: payload.testCaseId,
|
|
37
|
+
promptId: payload.promptId,
|
|
38
|
+
pageDetails: payload.pageDetails,
|
|
39
|
+
generatedBy: payload.generatedBy,
|
|
40
|
+
webSocketId: payload.webSocketId,
|
|
41
|
+
licenseType: payload.licenseType,
|
|
42
|
+
licenseId: payload.licenseId,
|
|
43
|
+
userId: payload.userId,
|
|
44
|
+
topic: payload.topic,
|
|
45
|
+
projectType: payload.projectType,
|
|
46
|
+
tokensConsumed: payload.tokensConsumed,
|
|
47
|
+
};
|
|
48
|
+
logger.info("PayLoad for Fireflink : ", body)
|
|
49
|
+
const headers = {
|
|
50
|
+
Accept: "*/*",
|
|
51
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
projectId: payload.projectId || "",
|
|
54
|
+
Authorization: `Bearer ${token}`,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const url = `${serverHost}/project/optimize/v3/bulk/scripts/script-payload`;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const finalResult = await this.apiService.callFireFlinkApi(
|
|
61
|
+
headers,
|
|
62
|
+
JSON.stringify(body),
|
|
63
|
+
url
|
|
64
|
+
);
|
|
65
|
+
if (finalResult) {
|
|
66
|
+
logger.error(
|
|
67
|
+
"Successfully submitted automation script to FireFlink API."
|
|
68
|
+
);
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"FireFlink API responded with an error: " + JSON.stringify(finalResult));
|
|
72
|
+
}
|
|
73
|
+
return finalResult;
|
|
74
|
+
} catch (error: any) {
|
|
75
|
+
logger.error(
|
|
76
|
+
`Failed to submit automation script, ${error}`
|
|
77
|
+
);
|
|
78
|
+
throw new Error(
|
|
79
|
+
"Failed to submit automation script", { cause: error }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { Browser } from "webdriverio";
|
|
2
|
+
|
|
3
|
+
export interface AnnotatedDOMResult {
|
|
4
|
+
dom: string;
|
|
5
|
+
selectors: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function getAnnotatedDOM(
|
|
9
|
+
browser: Browser
|
|
10
|
+
): Promise<AnnotatedDOMResult> {
|
|
11
|
+
|
|
12
|
+
return await browser.execute<AnnotatedDOMResult, []>(() => {
|
|
13
|
+
|
|
14
|
+
let nodeIndex = 0;
|
|
15
|
+
const xpaths: Record<string, string> = {};
|
|
16
|
+
|
|
17
|
+
// ==========================================================
|
|
18
|
+
// COVERAGE CHECK
|
|
19
|
+
// ==========================================================
|
|
20
|
+
function getCoverageStatus(el: Element): string {
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
if (!el || !el.getBoundingClientRect) return "any";
|
|
24
|
+
|
|
25
|
+
const rect = el.getBoundingClientRect();
|
|
26
|
+
if (rect.width === 0 || rect.height === 0) return "false";
|
|
27
|
+
|
|
28
|
+
const x = rect.left + rect.width / 2;
|
|
29
|
+
const y = rect.top + rect.height / 2;
|
|
30
|
+
|
|
31
|
+
const topEl = el.ownerDocument.elementFromPoint(x, y);
|
|
32
|
+
if (!topEl) return "false";
|
|
33
|
+
|
|
34
|
+
if (topEl === el || el.contains(topEl)) return "false";
|
|
35
|
+
|
|
36
|
+
const style = getComputedStyle(topEl);
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
style.pointerEvents === "none" ||
|
|
40
|
+
style.opacity === "0" ||
|
|
41
|
+
style.visibility === "hidden"
|
|
42
|
+
) return "false";
|
|
43
|
+
|
|
44
|
+
return "true";
|
|
45
|
+
} catch {
|
|
46
|
+
return "any";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ==========================================================
|
|
51
|
+
// INTERACTIVITY METADATA
|
|
52
|
+
// ==========================================================
|
|
53
|
+
function annotateInteractivity(el: Element): void {
|
|
54
|
+
|
|
55
|
+
const s = getComputedStyle(el);
|
|
56
|
+
|
|
57
|
+
el.setAttribute("element-visibility", s.visibility || "");
|
|
58
|
+
el.setAttribute("element-pointer-events", s.pointerEvents || "");
|
|
59
|
+
el.setAttribute("element-zindex", s.zIndex || "");
|
|
60
|
+
el.setAttribute("element-covered", getCoverageStatus(el));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ==========================================================
|
|
64
|
+
// GLOBAL XPATH (Shadow + Iframe Safe)
|
|
65
|
+
// ==========================================================
|
|
66
|
+
function getXPath(node: Element): string {
|
|
67
|
+
|
|
68
|
+
if (!node || node.nodeType !== 1) return "";
|
|
69
|
+
|
|
70
|
+
const segments: string[] = [];
|
|
71
|
+
let current: Node | null = node;
|
|
72
|
+
|
|
73
|
+
while (current && current.nodeType === 1) {
|
|
74
|
+
|
|
75
|
+
const el = current as Element;
|
|
76
|
+
const parent = el.parentNode;
|
|
77
|
+
|
|
78
|
+
let index = 1;
|
|
79
|
+
|
|
80
|
+
if (parent && (parent as Element).children) {
|
|
81
|
+
const siblings = Array.from(
|
|
82
|
+
(parent as Element).children
|
|
83
|
+
).filter(n => n.tagName === el.tagName);
|
|
84
|
+
|
|
85
|
+
index = siblings.indexOf(el) + 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
segments.unshift(
|
|
89
|
+
el.tagName.toLowerCase() + "[" + index + "]"
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// ---------------------------
|
|
93
|
+
// Shadow DOM handling
|
|
94
|
+
// ---------------------------
|
|
95
|
+
if (parent instanceof ShadowRoot) {
|
|
96
|
+
segments.unshift("shadow");
|
|
97
|
+
current = parent.host;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------
|
|
102
|
+
// Iframe root handling
|
|
103
|
+
// ---------------------------
|
|
104
|
+
if (
|
|
105
|
+
el.ownerDocument !== document &&
|
|
106
|
+
el === el.ownerDocument.documentElement
|
|
107
|
+
) {
|
|
108
|
+
const frameEl =
|
|
109
|
+
el.ownerDocument.defaultView?.frameElement as Element | null;
|
|
110
|
+
|
|
111
|
+
if (frameEl && frameEl.getAttribute("ff-xpath")) {
|
|
112
|
+
return (
|
|
113
|
+
frameEl.getAttribute("ff-xpath") +
|
|
114
|
+
"/" +
|
|
115
|
+
segments.join("/")
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!parent || parent.nodeType !== 1) break;
|
|
121
|
+
|
|
122
|
+
current = parent;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return "/" + segments.join("/");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ==========================================================
|
|
129
|
+
// PROCESS NODE
|
|
130
|
+
// ==========================================================
|
|
131
|
+
function processNode(node: Element): void {
|
|
132
|
+
|
|
133
|
+
if (!node || node.nodeType !== 1) return;
|
|
134
|
+
|
|
135
|
+
const fireId = `Fire-Flink-${nodeIndex++}`;
|
|
136
|
+
const xpath = getXPath(node);
|
|
137
|
+
|
|
138
|
+
node.setAttribute("ff-inspect", fireId);
|
|
139
|
+
node.setAttribute("ff-xpath", xpath);
|
|
140
|
+
|
|
141
|
+
xpaths[fireId] = xpath;
|
|
142
|
+
|
|
143
|
+
annotateInteractivity(node);
|
|
144
|
+
|
|
145
|
+
// ---------------------------
|
|
146
|
+
// Shadow DOM
|
|
147
|
+
// ---------------------------
|
|
148
|
+
const shadowRoot = (node as HTMLElement).shadowRoot;
|
|
149
|
+
if (shadowRoot) {
|
|
150
|
+
shadowRoot.querySelectorAll("*")
|
|
151
|
+
.forEach(el => processNode(el as Element));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------
|
|
155
|
+
// Same-origin iframe
|
|
156
|
+
// ---------------------------
|
|
157
|
+
if (node.tagName === "IFRAME") {
|
|
158
|
+
try {
|
|
159
|
+
const doc =
|
|
160
|
+
(node as HTMLIFrameElement).contentDocument;
|
|
161
|
+
|
|
162
|
+
if (doc) {
|
|
163
|
+
processNode(doc.documentElement);
|
|
164
|
+
doc.querySelectorAll("*")
|
|
165
|
+
.forEach(el => processNode(el as Element));
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
node.setAttribute("ff-cross-origin", "true");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
node.querySelectorAll(":scope > *")
|
|
173
|
+
.forEach(el => processNode(el as Element));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ==========================================================
|
|
177
|
+
// INITIAL INJECTION
|
|
178
|
+
// ==========================================================
|
|
179
|
+
document.querySelectorAll("*")
|
|
180
|
+
.forEach(el => processNode(el as Element));
|
|
181
|
+
|
|
182
|
+
// ==========================================================
|
|
183
|
+
// SERIALIZER
|
|
184
|
+
// ==========================================================
|
|
185
|
+
function serializeNode(node: Node): string {
|
|
186
|
+
|
|
187
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
188
|
+
return node.textContent || "";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return "";
|
|
192
|
+
|
|
193
|
+
const el = node as Element;
|
|
194
|
+
const tag = el.tagName.toLowerCase();
|
|
195
|
+
let html = `<${tag}`;
|
|
196
|
+
|
|
197
|
+
for (const attr of Array.from(el.attributes)) {
|
|
198
|
+
|
|
199
|
+
// Avoid dumping large srcdoc inline
|
|
200
|
+
if (attr.name === "srcdoc") continue;
|
|
201
|
+
|
|
202
|
+
html += ` ${attr.name}="${attr.value.replace(/"/g, """)}"`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
html += ">";
|
|
206
|
+
|
|
207
|
+
// Shadow DOM serialization
|
|
208
|
+
const shadowRoot = (el as HTMLElement).shadowRoot;
|
|
209
|
+
if (shadowRoot) {
|
|
210
|
+
html += "<!--#shadow-root-open-->";
|
|
211
|
+
shadowRoot.querySelectorAll("*")
|
|
212
|
+
.forEach(child => {
|
|
213
|
+
html += serializeNode(child);
|
|
214
|
+
});
|
|
215
|
+
html += "<!--#/shadow-root-->";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Iframe serialization
|
|
219
|
+
if (
|
|
220
|
+
tag === "iframe" &&
|
|
221
|
+
el.getAttribute("ff-cross-origin") !== "true"
|
|
222
|
+
) {
|
|
223
|
+
try {
|
|
224
|
+
const doc =
|
|
225
|
+
(el as HTMLIFrameElement).contentDocument;
|
|
226
|
+
|
|
227
|
+
if (doc) {
|
|
228
|
+
html += "<!--#iframe-content-->";
|
|
229
|
+
html += serializeNode(doc.documentElement);
|
|
230
|
+
html += "<!--#/iframe-content-->";
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// cross-origin iframe, cannot access content
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const child of Array.from(el.childNodes)) {
|
|
238
|
+
html += serializeNode(child);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
html += `</${tag}>`;
|
|
242
|
+
|
|
243
|
+
return html;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
dom:
|
|
248
|
+
"<!DOCTYPE html>\\n" +
|
|
249
|
+
serializeNode(document.documentElement),
|
|
250
|
+
selectors: xpaths
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export const Find_element_by_ff_js_script = `
|
|
2
|
+
const target = arguments[0];
|
|
3
|
+
const callback = arguments[arguments.length - 1];
|
|
4
|
+
|
|
5
|
+
const MAX_WAIT = 2000;
|
|
6
|
+
const INTERVAL = 50;
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
|
|
9
|
+
// ---------------- FINDER ----------------
|
|
10
|
+
function findByFfInspect(root) {
|
|
11
|
+
const visited = new Set();
|
|
12
|
+
const stack = [{ node: root, frames: [] }];
|
|
13
|
+
|
|
14
|
+
while (stack.length > 0) {
|
|
15
|
+
const { node, frames } = stack.pop();
|
|
16
|
+
if (!node) continue;
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
node.nodeType === Node.DOCUMENT_NODE ||
|
|
20
|
+
node.nodeType === Node.DOCUMENT_FRAGMENT_NODE
|
|
21
|
+
) {
|
|
22
|
+
if (visited.has(node)) continue;
|
|
23
|
+
visited.add(node);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
27
|
+
|
|
28
|
+
if (node.getAttribute && node.getAttribute("ff-inspect") === target) {
|
|
29
|
+
return { node, frames };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (node.shadowRoot) {
|
|
33
|
+
stack.push({ node: node.shadowRoot, frames });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (node.tagName === "IFRAME") {
|
|
37
|
+
try {
|
|
38
|
+
const doc = node.contentDocument || node.contentWindow?.document;
|
|
39
|
+
if (doc) {
|
|
40
|
+
const meta = {
|
|
41
|
+
id: node.id || null,
|
|
42
|
+
name: node.getAttribute("name") || null,
|
|
43
|
+
title: node.getAttribute("title") || null,
|
|
44
|
+
aria: node.getAttribute("aria-label") || null,
|
|
45
|
+
src: node.getAttribute("src") || null,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
stack.push({
|
|
49
|
+
node: doc,
|
|
50
|
+
frames: [...frames, meta]
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const kids = node.children || node.childNodes;
|
|
57
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
58
|
+
stack.push({ node: kids[i], frames });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
else if (
|
|
63
|
+
node.nodeType === Node.DOCUMENT_NODE ||
|
|
64
|
+
node.nodeType === Node.DOCUMENT_FRAGMENT_NODE
|
|
65
|
+
) {
|
|
66
|
+
const kids = node.childNodes;
|
|
67
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
68
|
+
stack.push({ node: kids[i], frames });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------- CLICKABILITY ----------------
|
|
77
|
+
function isClickable(el) {
|
|
78
|
+
if (!el || el.nodeType !== Node.ELEMENT_NODE) return false;
|
|
79
|
+
const tag = el.tagName.toLowerCase();
|
|
80
|
+
if (tag === "button" || tag === "a") return true;
|
|
81
|
+
if (el.getAttribute("role") === "button") return true;
|
|
82
|
+
if (tag === "input") {
|
|
83
|
+
const t = (el.getAttribute("type") || "").toLowerCase();
|
|
84
|
+
return t === "button" || t === "submit";
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findClickableInside(root) {
|
|
90
|
+
const queue = [root];
|
|
91
|
+
while (queue.length) {
|
|
92
|
+
const node = queue.shift();
|
|
93
|
+
if (isClickable(node)) return node;
|
|
94
|
+
const kids = node.children || node.childNodes;
|
|
95
|
+
for (let k of kids) queue.push(k);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------- XPATH BUILDER ----------------
|
|
101
|
+
function buildXPath(el) {
|
|
102
|
+
if (!el) return null;
|
|
103
|
+
|
|
104
|
+
if (el.id) return \`//*[@id="\${el.id}"]\`;
|
|
105
|
+
|
|
106
|
+
const name = el.getAttribute("name");
|
|
107
|
+
if (name) return \`//*[@name="\${name}"]\`;
|
|
108
|
+
|
|
109
|
+
const text = el.textContent && el.textContent.trim();
|
|
110
|
+
if (text && text.length < 50) {
|
|
111
|
+
const escaped = text.replace(/"/g, '\\\\"');
|
|
112
|
+
const xp = \`//*[text()="\${escaped}"]\`;
|
|
113
|
+
try {
|
|
114
|
+
const n = document.evaluate(xp, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
115
|
+
if (n.singleNodeValue === el) return xp;
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const parts = [];
|
|
120
|
+
let node = el;
|
|
121
|
+
|
|
122
|
+
while (node && node.nodeType === Node.ELEMENT_NODE) {
|
|
123
|
+
let idx = 1;
|
|
124
|
+
let sib = node.previousSibling;
|
|
125
|
+
while (sib) {
|
|
126
|
+
if (sib.nodeType === Node.ELEMENT_NODE && sib.nodeName === node.nodeName) idx++;
|
|
127
|
+
sib = sib.previousSibling;
|
|
128
|
+
}
|
|
129
|
+
parts.unshift(node.nodeName.toLowerCase() + "[" + idx + "]");
|
|
130
|
+
node = node.parentNode instanceof ShadowRoot ? node.parentNode.host : node.parentNode;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return "/" + parts.join("/");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------- WAIT LOOP ----------------
|
|
137
|
+
(function attempt() {
|
|
138
|
+
const found = findByFfInspect(document);
|
|
139
|
+
|
|
140
|
+
if (!found) {
|
|
141
|
+
if (Date.now() - start > MAX_WAIT) {
|
|
142
|
+
return callback({ success: false, reason: "timeout" });
|
|
143
|
+
}
|
|
144
|
+
return setTimeout(attempt, INTERVAL);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let targetNode = found.node;
|
|
148
|
+
if (!isClickable(targetNode)) {
|
|
149
|
+
const inside = findClickableInside(targetNode);
|
|
150
|
+
if (inside) targetNode = inside;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const xpath = buildXPath(targetNode);
|
|
154
|
+
|
|
155
|
+
return callback({
|
|
156
|
+
success: true,
|
|
157
|
+
xpath,
|
|
158
|
+
frames: found.frames
|
|
159
|
+
});
|
|
160
|
+
})();
|
|
161
|
+
`;
|