ff-automationv2 2.1.1-beta.3 → 2.1.1
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/bitbucket-pipelines.yml +2 -4
- package/package.json +3 -1
- package/src/ai/llmprompts/systemPrompts/actionExtractorPrompt.ts +35 -54
- package/src/ai/llmprompts/systemPrompts/fireflinkElementIndexExtactors.ts +43 -68
- package/src/core/main/runAutomationScript.ts +14 -3
- package/src/domAnalysis/getRelaventElements.ts +5 -1
- package/src/domAnalysis/searchBest.ts +8 -2
- package/src/utils/logger/logData.ts +45 -0
package/bitbucket-pipelines.yml
CHANGED
|
@@ -2,8 +2,7 @@ image: node:20
|
|
|
2
2
|
|
|
3
3
|
pipelines:
|
|
4
4
|
branches:
|
|
5
|
-
|
|
6
|
-
"feature/*":
|
|
5
|
+
"feature/automation_V2":
|
|
7
6
|
- step:
|
|
8
7
|
name: Publish Beta
|
|
9
8
|
caches:
|
|
@@ -19,8 +18,7 @@ pipelines:
|
|
|
19
18
|
|
|
20
19
|
- echo "Publishing as beta"
|
|
21
20
|
- npm publish --tag beta --access public
|
|
22
|
-
|
|
23
|
-
release:
|
|
21
|
+
"release/automation_V2":
|
|
24
22
|
- step:
|
|
25
23
|
name: Publish Production
|
|
26
24
|
caches:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ff-automationv2",
|
|
3
|
-
"version": "2.1.1
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "This lib is used to automate the manual testcase",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"license": "ISC",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@eslint/js": "10.0.1",
|
|
31
|
+
"@types/js-beautify": "^1.14.3",
|
|
31
32
|
"@types/jsdom": "27.0.0",
|
|
32
33
|
"@types/node": "25.2.2",
|
|
33
34
|
"eslint": "10.0.0",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"cheerio": "1.2.0",
|
|
44
45
|
"fs-extra": "11.3.3",
|
|
45
46
|
"fuzzball": "2.2.3",
|
|
47
|
+
"js-beautify": "1.15.1",
|
|
46
48
|
"jsdom": "27.4.0",
|
|
47
49
|
"openai": "6.18.0",
|
|
48
50
|
"uuid": "13.0.0",
|
|
@@ -7,63 +7,44 @@ export async function keywordExtractor(
|
|
|
7
7
|
{ priorAndNextSteps }: keywordExtractor
|
|
8
8
|
): Promise<string> {
|
|
9
9
|
|
|
10
|
-
const allowedActions: string[] = [
|
|
11
|
-
"enter",
|
|
12
|
-
"wait",
|
|
13
|
-
"verify",
|
|
14
|
-
"scroll",
|
|
15
|
-
"navigate",
|
|
16
|
-
"click",
|
|
17
|
-
"maximize",
|
|
18
|
-
"get",
|
|
19
|
-
"upload",
|
|
20
|
-
"close",
|
|
21
|
-
"open",
|
|
22
|
-
"drag_and_drop",
|
|
23
|
-
"switch"
|
|
24
|
-
];
|
|
10
|
+
const allowedActions: string[] = ["enter", "wait", "verify", "scroll", "navigate", "click", "maximize", "get", "upload", "close", "open", "drag_and_drop", "switch", "cleartext"];
|
|
25
11
|
|
|
26
12
|
const prompt = `
|
|
27
13
|
You are an expert in Web application testing.
|
|
28
|
-
From the step
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
- If the step
|
|
35
|
-
- If
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
- Do
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- action
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"elementType":""
|
|
63
|
-
"action": ""
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
No other text.
|
|
14
|
+
From the step, extract ONLY the meaningful keywords so that i can search for the element in the dom.
|
|
15
|
+
Rules:
|
|
16
|
+
- Only give response for the current step.
|
|
17
|
+
- understand the step and context from the ${JSON.stringify(priorAndNextSteps)}.and keywords should be from step. it should not be related to other steps.
|
|
18
|
+
- 3 to 5 keywords maximum.
|
|
19
|
+
- If the step is about entering text or Uploading file, Should NOT include input value from the step into keywords.
|
|
20
|
+
- If the step has words like tag name audio, video, image,svg, checkbox etc, include them in the keywords.
|
|
21
|
+
- If icon is metioned in step than 'svg' should add in keywords and for Upload action first keyword should be 'file'.
|
|
22
|
+
- First keywords should be from step next Keywords must be distinct and based on the element's label meaning only.
|
|
23
|
+
- If a keyword contains exactly two words, you MUST always include both the spaced and concatenated (no-space) versions; omission of either is invalid.
|
|
24
|
+
- Do NOT split single-word keywords and do NOT include relation terms (above, below, next to, etc.) in keywords.
|
|
25
|
+
- Treat each keyword independently—never merge different keywords or combine them with relation words.
|
|
26
|
+
- Example: Click on Sign In above Create Account button → ["Sign In", "SignIn", "Create Account", "CreateAccount"]
|
|
27
|
+
- Keywords can be string or number if the step contains a number ,add it also as keyword.
|
|
28
|
+
- Do Not include any other unrelated keywords for step.
|
|
29
|
+
- Do NOT include generic UI words (button, field, etc) and action words (tap, click, press, etc).
|
|
30
|
+
- Do NOT include status/technical words (displayed, enabled, authenticate, visible).
|
|
31
|
+
- If an element label contains multiple words (e.g., "Sign In", "Add to Cart"), keep them together as ONE keyword and do not split them and also for keywords you generated, do not split them.
|
|
32
|
+
- element_name: extract name of the element that mentioned in the the step.(eg:tap on x -> element_name:x) keep element_name as short as possible and make the first letter of first word of the element_name as capital. beacuse element_name is also used to find element in the dom. and if element_name is not mentioned in step than return action of the step as element_name.
|
|
33
|
+
- action: click for taping, clicking or selecting, enter for entering input, wait for waiting or sleeping,scroll for scrolling and swiping, navigate for navigating to page using url or navigateing back to previous page, get for getting,fetching element,maximize for maximizing browser window, close for closing browser window,open for opening browser window, upload for uploading file using path, drag_and_drop for dragging and dropping element, switch is for switching to tab or window or frame,cleartext for clearing or removing text from element.
|
|
34
|
+
- action must be one of from this list ${JSON.stringify(allowedActions)}.if not one of them, return '0'. if step about set or find action return '0'
|
|
35
|
+
- For navigate action, keywords should contain only one keyword which is full url from the step and should not include any other text. and if step has another actions, including navigate action, don't return navigate action return another action witch is in the step. and element_name should be "URL".
|
|
36
|
+
Navigate action is allowed only when the step intent is purely navigation (e.g., "Navigate to URL").
|
|
37
|
+
If the step includes verification intent (verify/check/confirm/etc), you MUST return "verify" and MUST NOT return "navigate".
|
|
38
|
+
only give navigate action if step has only navigate action.
|
|
39
|
+
|
|
40
|
+
Respond only with JSON using this format:
|
|
41
|
+
{
|
|
42
|
+
"keywords": [key1,key2,key3,key4,key5],
|
|
43
|
+
"element_name": "x",
|
|
44
|
+
"action": "x"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
No other text.
|
|
67
48
|
`;
|
|
68
49
|
|
|
69
50
|
return prompt;
|
|
@@ -26,8 +26,8 @@ export async function ffInspectorNumExtractor({
|
|
|
26
26
|
"WaitTillAlertIsPresent",
|
|
27
27
|
"VerifyTextPresentOnAlertPopup"
|
|
28
28
|
];
|
|
29
|
-
const elementType = [
|
|
30
|
-
|
|
29
|
+
const elementType = ['link', 'textfield', 'icon', 'button', 'radioButton', 'text', 'textarea', 'image', 'dropdown', 'checkbox', 'tab', 'action overflow button', 'hamburger icon', 'toggle button', 'suggestion', 'time picker', 'date picker', 'toaster message', 'card', 'tooltip', 'option', 'calender', 'sliders', 'visual testing'];
|
|
30
|
+
const enterActions = ["enter", "clearandenter"];
|
|
31
31
|
let prompt;
|
|
32
32
|
|
|
33
33
|
// ---------------- ALERT ----------------
|
|
@@ -123,75 +123,50 @@ Return ONLY valid JSON:
|
|
|
123
123
|
// ---------------- DEFAULT (CLICK / ENTER / UPLOAD etc.) ----------------
|
|
124
124
|
else {
|
|
125
125
|
prompt = `
|
|
126
|
-
You are a deterministic UI action extraction engine.
|
|
127
|
-
|
|
128
|
-
OBJECTIVE:
|
|
129
|
-
Identify the SINGLE best matching element from the Simplified JSON
|
|
130
|
-
for the given step and return structured automation data.
|
|
131
|
-
|
|
132
|
-
-----------------------------------------
|
|
133
|
-
|
|
134
|
-
CONTEXT STEPS:
|
|
135
|
-
${JSON.stringify(priorAndNextSteps)}
|
|
136
|
-
|
|
137
|
-
CLICK ACTION WORDS:
|
|
138
|
-
${JSON.stringify(clickActions)}
|
|
139
|
-
|
|
140
|
-
ALLOWED ELEMENT TYPES:
|
|
141
|
-
${elementType}
|
|
142
|
-
-----------------------------------------
|
|
143
|
-
|
|
144
|
-
CRITICAL MATCHING LOGIC:
|
|
145
|
-
|
|
146
|
-
1. First understand intent:
|
|
147
|
-
- click words → click
|
|
148
|
-
- enter/type/write → enter_text
|
|
149
|
-
- upload → upload
|
|
150
|
-
- drag_and_drop → drag_and_drop
|
|
151
|
-
|
|
152
|
-
2. For ENTER actions:
|
|
153
|
-
DO NOT pick first input blindly.
|
|
154
|
-
Score candidates using priority:
|
|
155
|
-
|
|
156
|
-
Priority Order:
|
|
157
|
-
(1) Exact label text match
|
|
158
|
-
(2) Placeholder match
|
|
159
|
-
(3) Name attribute semantic match
|
|
160
|
-
(4) ID semantic match
|
|
161
|
-
(5) type="email" if step contains "email"
|
|
162
|
-
(6) DOM sibling label relationship
|
|
163
|
-
|
|
164
|
-
Choose highest scoring match.
|
|
165
|
-
|
|
166
|
-
If NO strong semantic match → return:
|
|
167
|
-
attribute_value = "Fire-Flink-0"
|
|
168
|
-
|
|
169
|
-
3. For CLICK actions:
|
|
170
|
-
- Prefer exact text match
|
|
171
|
-
- If multiple, choose closest contextual match
|
|
172
|
-
- If icon + svg exists, prefer svg element
|
|
173
|
-
|
|
174
|
-
4. Never hallucinate.
|
|
175
|
-
5. Only use FF values from provided JSON.
|
|
176
|
-
6. No null values. Use empty string "".
|
|
177
|
-
7. Output must be valid JSON only.
|
|
178
|
-
|
|
179
|
-
-----------------------------------------
|
|
180
|
-
SIMPLIFIED JSON:
|
|
181
|
-
${extractedDomJson}
|
|
182
|
-
-----------------------------------------
|
|
183
|
-
|
|
184
|
-
Respond ONLY with JSON:
|
|
185
126
|
|
|
127
|
+
-You are an intelligent assistant that extracts structured UI action data.
|
|
128
|
+
-Given a structured UI JSON representation with uniquely identified elements (ff-inspect values like Fire-Flink-1, Fire-Flink-2, Fire-Flink-3... in DOM order),
|
|
129
|
+
-locate the most appropriate element for an automation step by performing keyword-based matching using exact, partial, and fuzzy strategies, and
|
|
130
|
+
return the identifier of the best match.
|
|
131
|
+
|
|
132
|
+
Return **only valid JSON** in the following format:
|
|
186
133
|
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
134
|
+
"attribute_value": "Fire-Flink-x",
|
|
135
|
+
"action": "x",
|
|
136
|
+
"input_text": "x",
|
|
137
|
+
"keyword": "x",
|
|
138
|
+
"num_of_scrolls": "0",
|
|
139
|
+
"direction": "down",
|
|
140
|
+
"element_type": "x"
|
|
194
141
|
}
|
|
142
|
+
|
|
143
|
+
Rules:
|
|
144
|
+
You are an AI assistant. For the step, extract the element keyword from the step (the field name like 'Leaving from', 'Going To').
|
|
145
|
+
- Use context from the ${JSON.stringify(priorAndNextSteps)}, keyword and filtered dom to search for FF-inspecter.
|
|
146
|
+
- **Find the FF-inspecter attribute value of the element in the Simplified JSON whose text or attributes best match the atep.**
|
|
147
|
+
- If multiple matches exist, use the reference element and choose the closest match by ff-inspect distance.
|
|
148
|
+
**important example: step: "click on login below user login"
|
|
149
|
+
simplified json:
|
|
150
|
+
login - ff-inspect-300
|
|
151
|
+
user login - ff-inspect-790
|
|
152
|
+
login - ff-inspect-803
|
|
153
|
+
then should return login - ff-inspect-803 witch is near to user reference element user login - ff-inspect-790**
|
|
154
|
+
- Respect direction (up/down), default is down.
|
|
155
|
+
- Do not return elements far away from the reference.
|
|
156
|
+
- Select the closest semantic match to step and return only its attribute_value, else Fire-Flink-0.
|
|
157
|
+
- Action: ${clickActions} for clicking or selecting, ${enterActions} for entering text, 'upload' for uploading file, 'drag_and_drop' for dragging and dropping, 'cleartext' for clearing text.
|
|
158
|
+
- For click action, if only step involving 'clicking on icon' then try to return related svg tags attribute_value, if they are available in simplified json.
|
|
159
|
+
- For Action ${enterActions} extract input text from step.
|
|
160
|
+
- **For ${enterActions} actions, never directly select a input tag if should be realted to step; if u can't find input tag related to step then prefer the closest label/span/div/b/i etc, it can be any tag related to the step.**
|
|
161
|
+
- **For ${enterActions} step if you can't find perfect matching element return attribute_value as Fire-Flink-0 for the step. it should be exact match for the step.**
|
|
162
|
+
- For ${enterActions} step, If the step implies autogenerated data or random data(email, phone, credentials, identifiers, etc.), generate a valid dummy input_text suitable for the field.
|
|
163
|
+
example: step: "Enter sss.@gmail.com in email field" and step :
|
|
164
|
+
you should check only for email in Simplified JSON and input_text should be "sss.@gmail.com" dont add any extra text.
|
|
165
|
+
- For Action "upload" extract file path from step.
|
|
166
|
+
- if u cant find any input_text or any other dont give null just return them "" empty.
|
|
167
|
+
- Based on step give most relevant type of element. use this list to choose element_type: ${elementType} and Never change syntax of element_type, follow the syntax of element_type in list.if element_type is not there in list return 'link'.
|
|
168
|
+
Simplified JSON: ${extractedDomJson}
|
|
169
|
+
|
|
195
170
|
`;
|
|
196
171
|
}
|
|
197
172
|
return prompt;
|
|
@@ -40,7 +40,7 @@ export class AutomationRunner implements IAutomationRunner {
|
|
|
40
40
|
this.implicit
|
|
41
41
|
);
|
|
42
42
|
const domProcessor = new DomProcessingEngine();
|
|
43
|
-
let stepCount =
|
|
43
|
+
let stepCount = 1;
|
|
44
44
|
const listOfSteps = stepResult.response.manualSteps
|
|
45
45
|
if (listOfSteps.length === 0) {
|
|
46
46
|
throw new Error("No executable manual steps were returned by the LLM.");
|
|
@@ -57,12 +57,14 @@ export class AutomationRunner implements IAutomationRunner {
|
|
|
57
57
|
input: { currentStep: step }
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
logger.info(JSON.stringify(result, null, 2))
|
|
61
|
+
|
|
60
62
|
const action = result.response.action?.toLowerCase();
|
|
61
63
|
const handler = actionHandlers[action];
|
|
62
64
|
|
|
63
65
|
logger.info
|
|
64
66
|
(
|
|
65
|
-
`Processing step: "${step}" with action: "${action}" and keywords: ${JSON.stringify(result.response.keywords)}`
|
|
67
|
+
`Processing step ${stepCount}: "${step}" with action: "${action}" and keywords: ${JSON.stringify(result.response.keywords)}`
|
|
66
68
|
);
|
|
67
69
|
|
|
68
70
|
if (!handler) {
|
|
@@ -93,15 +95,19 @@ export class AutomationRunner implements IAutomationRunner {
|
|
|
93
95
|
continue;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
|
|
96
99
|
const browser = await context.session.getCurrentBrowser();
|
|
97
100
|
domInfo = await getAnnotatedDOM(browser);
|
|
101
|
+
await logger.saveDOM(domInfo.dom, `annotated-dom-${stepCount}`);
|
|
98
102
|
|
|
99
103
|
extractedRelevantDom = domProcessor.process({
|
|
100
104
|
keywords: result.response.keywords,
|
|
101
105
|
rawDom: domInfo.dom,
|
|
102
|
-
|
|
106
|
+
stepCount: stepCount
|
|
103
107
|
});
|
|
104
108
|
|
|
109
|
+
await logger.saveJSON(extractedRelevantDom, `relevant-dom-${stepCount}`);
|
|
110
|
+
|
|
105
111
|
const stepResult = await stepProcessor.getLLMResponse({
|
|
106
112
|
type: PromptType.FF_INSPECTOR,
|
|
107
113
|
args: {
|
|
@@ -114,12 +120,17 @@ export class AutomationRunner implements IAutomationRunner {
|
|
|
114
120
|
input: { currentStep: step }
|
|
115
121
|
});
|
|
116
122
|
|
|
123
|
+
logger.info(JSON.stringify(stepResult, null, 2))
|
|
124
|
+
|
|
117
125
|
const fireflinkIndex = stepResult.response.attribute_value;
|
|
118
126
|
const xpath = domInfo.selectors[fireflinkIndex];
|
|
119
127
|
|
|
120
128
|
if (!xpath) {
|
|
121
129
|
throw new Error(`Unable to resolve xpath for ${fireflinkIndex}`);
|
|
122
130
|
}
|
|
131
|
+
else if (fireflinkIndex == 0) {
|
|
132
|
+
throw new Error(`Unable to find element for ${step}`);
|
|
133
|
+
}
|
|
123
134
|
|
|
124
135
|
await handler({
|
|
125
136
|
selector: xpath,
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { DomSimplifier } from "./simplifyAndFlatten.js";
|
|
2
2
|
import { DomSearcher } from "./searchBest.js";
|
|
3
3
|
import { DomRelationshipBuilder } from "./relativeElementsFromDom.js";
|
|
4
|
+
import { logger } from "../utils/logger/logData.js";
|
|
5
|
+
|
|
4
6
|
export interface DomProcessingRequest {
|
|
5
7
|
keywords: string[] | string;
|
|
6
8
|
rawDom: string;
|
|
7
|
-
|
|
9
|
+
stepCount: number;
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
|
|
10
13
|
export class DomProcessingEngine {
|
|
11
14
|
|
|
12
15
|
private simplifier = new DomSimplifier();
|
|
@@ -16,6 +19,7 @@ export class DomProcessingEngine {
|
|
|
16
19
|
public process(request: DomProcessingRequest) {
|
|
17
20
|
|
|
18
21
|
const flat = this.simplifier.simplify(request.rawDom);
|
|
22
|
+
logger.saveJSON(flat, `flat-dom-${request.stepCount}`);
|
|
19
23
|
const searched = this.searcher.search(flat, request.keywords);
|
|
20
24
|
const structured = this.relationshipBuilder.build(searched);
|
|
21
25
|
|
|
@@ -123,13 +123,19 @@ export class DomSearcher {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
if (collected.length < topN * 4) {
|
|
126
|
-
fuzzyList
|
|
127
|
-
.
|
|
126
|
+
fuzzyList
|
|
127
|
+
.sort((a, b) => b[0] - a[0])
|
|
128
|
+
.forEach(([_, idx]) => {
|
|
129
|
+
if (collected.length < topN * 4) {
|
|
130
|
+
addWithContext(idx, collected);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
if (collected.length < topN * 4) {
|
|
131
136
|
for (const idx of tagMatches) {
|
|
132
137
|
addWithContext(idx, collected);
|
|
138
|
+
if (collected.length >= topN * 4) break;
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import beautify from "js-beautify";
|
|
3
4
|
|
|
4
5
|
const logsDir = path.resolve(process.cwd(), "logs");
|
|
6
|
+
const domDir = path.resolve(process.cwd(), "dom");
|
|
5
7
|
|
|
6
8
|
if (!fs.existsSync(logsDir)) {
|
|
7
9
|
fs.mkdirSync(logsDir, { recursive: true });
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
if (!fs.existsSync(domDir)) {
|
|
13
|
+
fs.mkdirSync(domDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
const logFilePath = path.join(logsDir, "ai-execution-logs.txt");
|
|
11
17
|
|
|
12
18
|
export const logger = {
|
|
@@ -32,5 +38,44 @@ export const logger = {
|
|
|
32
38
|
const message = `[ERROR] ${new Date().toISOString()} - ${formattedArgs.join(" ")}\n`;
|
|
33
39
|
|
|
34
40
|
fs.appendFileSync(logFilePath, message, "utf-8");
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
saveDOM: (domContent: string, fileName?: string) => {
|
|
44
|
+
|
|
45
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
46
|
+
const finalFileName = fileName
|
|
47
|
+
? `${fileName}.html`
|
|
48
|
+
: `dom-${timestamp}.html`;
|
|
49
|
+
|
|
50
|
+
const filePath = path.join(domDir, finalFileName);
|
|
51
|
+
|
|
52
|
+
const formattedHTML = beautify.html(domContent, {
|
|
53
|
+
indent_size: 2,
|
|
54
|
+
preserve_newlines: false,
|
|
55
|
+
wrap_line_length: 120
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
fs.writeFileSync(filePath, formattedHTML, "utf-8");
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
saveJSON: (data: unknown, fileName?: string) => {
|
|
62
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
63
|
+
|
|
64
|
+
const finalFileName = fileName
|
|
65
|
+
? `${fileName}.json`
|
|
66
|
+
: `data-${timestamp}.json`;
|
|
67
|
+
|
|
68
|
+
const filePath = path.join(domDir, finalFileName);
|
|
69
|
+
|
|
70
|
+
fs.writeFileSync(
|
|
71
|
+
filePath,
|
|
72
|
+
JSON.stringify(data, null, 2),
|
|
73
|
+
"utf-8"
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
|
|
35
77
|
}
|
|
78
|
+
|
|
79
|
+
|
|
36
80
|
};
|
|
81
|
+
|