ff-automationv2 2.1.3-beta.4 → 2.1.3-beta.6

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/RELEASE_GUIDE.md CHANGED
@@ -3,7 +3,7 @@
3
3
  git checkout feature/your-feature-name
4
4
  git add .
5
5
  git commit -m "feat: new changes"
6
- git pull --rebase
6
+ git pull
7
7
  npm version prerelease --preid=beta
8
8
  git push --follow-tags
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ff-automationv2",
3
- "version": "2.1.3-beta.4",
3
+ "version": "2.1.3-beta.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "This lib is used to automate the manual testcase",
@@ -48,6 +48,6 @@
48
48
  "jsdom": "27.4.0",
49
49
  "openai": "6.18.0",
50
50
  "uuid": "13.0.0",
51
- "webdriverio": "9.23.3"
51
+ "webdriverio": "8.46.0"
52
52
  }
53
53
  }
@@ -19,97 +19,141 @@ export class ActionExecutor implements IActionExecutor {
19
19
 
20
20
 
21
21
  async navigate(url: string): Promise<void> {
22
- await navigate({
23
- driver: this.browser,
24
- url,
25
- scriptDataAppender: this.scriptDataAppender
26
- });
22
+ try {
23
+ await navigate({
24
+ driver: this.browser,
25
+ url,
26
+ scriptDataAppender: this.scriptDataAppender
27
+ });
28
+ }
29
+ catch (error) {
30
+ throw error;
31
+ }
27
32
  }
28
33
 
29
34
  async goBack(): Promise<void> {
30
- await goBack({
31
- driver: this.browser,
32
- scriptDataAppender: this.scriptDataAppender
33
- });
35
+ try {
36
+ await goBack({
37
+ driver: this.browser,
38
+ scriptDataAppender: this.scriptDataAppender
39
+ });
40
+ }
41
+ catch (error) {
42
+ throw error;
43
+ }
34
44
  }
35
45
 
36
46
  async refresh(): Promise<void> {
37
- await pageRefresh({
38
- driver: this.browser,
39
- scriptDataAppender: this.scriptDataAppender
40
- });
47
+ try {
48
+ await pageRefresh({
49
+ driver: this.browser,
50
+ scriptDataAppender: this.scriptDataAppender
51
+ });
52
+ }
53
+ catch (error) {
54
+ throw error;
55
+ }
41
56
  }
42
57
 
43
58
 
44
59
  async wait(time: string): Promise<void> {
45
- await wait({
46
- driver: this.browser,
47
- time,
48
- scriptDataAppender: this.scriptDataAppender
49
- });
60
+ try {
61
+ await wait({
62
+ driver: this.browser,
63
+ time,
64
+ scriptDataAppender: this.scriptDataAppender
65
+ });
66
+ }
67
+ catch (error) {
68
+ throw error;
69
+ }
50
70
  }
51
71
  async click(pageDOM: string, selector: string, fireflinkIndex: string, elementName: string, elementType: string): Promise<void> {
52
- await click({
53
- driver: this.browser,
54
- selector,
55
- scriptDataAppender: this.scriptDataAppender,
56
- elementGetter: this.elementGetter,
57
- fireflinkIndex,
58
- pageDOM,
59
- elementName,
60
- elementType,
61
- platform: this.platform
62
- });
72
+ try {
73
+ await click({
74
+ driver: this.browser,
75
+ selector,
76
+ scriptDataAppender: this.scriptDataAppender,
77
+ elementGetter: this.elementGetter,
78
+ fireflinkIndex,
79
+ pageDOM,
80
+ elementName,
81
+ elementType,
82
+ platform: this.platform
83
+ });
84
+ }
85
+ catch (error) {
86
+ throw error;
87
+ }
63
88
  }
64
89
  async enterInput(selector: string, value: string, fireflinkIndex: string, pageDOM: string, elementName: string, elementType: string): Promise<void> {
65
- await enterInput({
66
- driver: this.browser,
67
- selector: selector,
68
- value,
69
- scriptDataAppender: this.scriptDataAppender,
70
- elementGetter: this.elementGetter,
71
- fireflinkIndex,
72
- pageDOM,
73
- elementName,
74
- elementType,
75
- platform: this.platform
76
- });
90
+ try {
91
+ await enterInput({
92
+ driver: this.browser,
93
+ selector: selector,
94
+ value,
95
+ scriptDataAppender: this.scriptDataAppender,
96
+ elementGetter: this.elementGetter,
97
+ fireflinkIndex,
98
+ pageDOM,
99
+ elementName,
100
+ elementType,
101
+ platform: this.platform
102
+ });
103
+ }
104
+ catch (error) {
105
+ throw error;
106
+ }
77
107
  }
78
108
  async maximize(): Promise<void> {
79
- await maximize({
80
- driver: this.browser,
81
- scriptDataAppender: this.scriptDataAppender
82
- });
109
+ try {
110
+ await maximize({
111
+ driver: this.browser,
112
+ scriptDataAppender: this.scriptDataAppender
113
+ });
114
+ }
115
+ catch (error) {
116
+ throw error;
117
+ }
83
118
  }
84
119
 
85
120
  async clear(pageDOM: string, selector: string, fireflinkIndex: string, elementName: string, elementType: string): Promise<void> {
86
- await clear({
87
- driver: this.browser,
88
- selector: selector,
89
- scriptDataAppender: this.scriptDataAppender,
90
- elementGetter: this.elementGetter,
91
- fireflinkIndex,
92
- pageDOM,
93
- elementName,
94
- elementType,
95
- platform: this.platform
96
- });
121
+ try {
122
+ await clear({
123
+ driver: this.browser,
124
+ selector: selector,
125
+ scriptDataAppender: this.scriptDataAppender,
126
+ elementGetter: this.elementGetter,
127
+ fireflinkIndex,
128
+ pageDOM,
129
+ elementName,
130
+ elementType,
131
+ platform: this.platform
132
+ });
133
+ }
134
+ catch (error) {
135
+ throw error;
136
+ }
97
137
  }
98
138
 
99
139
  async clearAndEnter(pageDOM: string, selector: string, value: string, fireflinkIndex: string, elementName: string, elementType: string): Promise<void> {
100
- await clearAndEnter({
101
- driver: this.browser,
102
- selector: selector,
103
- value,
104
- scriptDataAppender: this.scriptDataAppender,
105
- elementGetter: this.elementGetter,
106
- fireflinkIndex,
107
- pageDOM,
108
- elementName,
109
- elementType,
110
- platform: this.platform
111
- });
140
+ try {
141
+ await clearAndEnter({
142
+ driver: this.browser,
143
+ selector: selector,
144
+ value,
145
+ scriptDataAppender: this.scriptDataAppender,
146
+ elementGetter: this.elementGetter,
147
+ fireflinkIndex,
148
+ pageDOM,
149
+ elementName,
150
+ elementType,
151
+ platform: this.platform
152
+ });
153
+ }
154
+ catch (error) {
155
+ throw error;
156
+ }
112
157
  }
113
158
 
114
-
115
159
  }
@@ -20,6 +20,6 @@ export async function click(args: ClickInterface): Promise<void> {
20
20
  }]
21
21
  });
22
22
  } catch (error: any) {
23
- throw new Error("Click action failed", { cause: error });
23
+ throw new Error(`Click action failed :`, { cause: error });
24
24
  }
25
25
  }
@@ -29,8 +29,7 @@ export async function enterInput(args: EnterInputInterface): Promise<void> {
29
29
 
30
30
  }
31
31
  catch (error: any) {
32
- console.log(error);
33
- throw new Error("enter action failed", { cause: error });
32
+ throw new Error(`enter action failed:`, { cause: error });
34
33
  }
35
34
 
36
- }
35
+ }
@@ -1,3 +1,4 @@
1
+ import { AutomationRequest } from "../../core/interfaces/executionDetails.js";
1
2
  export interface IAutomationRunner {
2
- run(): Promise<void>;
3
+ run(args:AutomationRequest): Promise<void>;
3
4
  }
@@ -1,122 +1,135 @@
1
1
  import { ExecutionContext } from "../main/executionContext.js";
2
2
  import { ActionExecutor } from "../../automation/actions/executor.js";
3
3
 
4
+ type ActionHandler = (result?: any) => Promise<void>;
5
+
4
6
  export function createActionHandlers(
5
7
  context: ExecutionContext,
6
8
  pageLoad: number,
7
9
  implicit: number
8
- ): Record<string, (result: any) => Promise<void>> {
9
-
10
- return {
11
-
12
- open: async () => {
13
- await context.session.open({
14
- browserName: "chrome",
15
- pageLoad,
16
- implicit
17
- });
18
-
19
- context.executor = new ActionExecutor(
20
- await context.session.getCurrentBrowser(),
21
- context.scriptAppender,
22
- context.locator,
23
- context.request.platform
24
- );
25
-
26
- context.scriptAppender.add({
27
- nlpName: "OpenBrowser",
28
- elementsData: [],
29
- stepInputs: []
30
- });
31
- },
32
- close: async (result: any) => {
33
- await context.session.close();
34
- },
35
-
36
- navigate: async (result: any) => {
37
- if (!context.executor) {
38
- throw new Error("Browser not initialized.");
39
- }
40
-
41
- await context.executor.navigate(result.response.keywords[0]);
42
- },
43
- maximize: async () => {
44
- if (!context.executor) {
45
- throw new Error("Browser not initialized.");
46
- }
47
- await context.executor.maximize();
48
- },
49
- goBack: async () => {
50
- if (!context.executor) {
51
- throw new Error("Browser not initialized.");
52
- }
53
- await context.executor.goBack();
54
- },
55
- refresh: async () => {
56
- if (!context.executor) {
57
- throw new Error("Browser not initialized.");
58
- }
59
- await context.executor.refresh();
60
- },
61
- wait: async (result: any) => {
62
- if (!context.executor) {
63
- throw new Error("Browser not initialized.");
64
- }
65
- await context.executor.wait(result.value);
66
- },
67
- click: async (result: any) => {
68
- if (!context.executor) {
69
- throw new Error("Browser not initialized.");
70
- }
71
- await context.executor.click(
72
- result.pageDOM,
73
- result.selector,
74
- result.fireflinkIndex,
75
- result.elementName,
76
- result.elementType
77
- );
78
- },
79
-
80
- enter: async (result: any) => {
81
- if (!context.executor) {
82
- throw new Error("Browser not initialized.");
83
- }
84
- await context.executor.enterInput(
85
- result.selector,
86
- result.value,
87
- result.fireflinkIndex,
88
- result.pageDOM,
89
- result.elementName,
90
- result.elementType
91
- );
92
- },
93
- clear: async (result: any) => {
94
- if (!context.executor) {
95
- throw new Error("Browser not initialized.");
96
- }
97
- await context.executor.clear(
98
- result.pageDOM,
99
- result.selector,
100
- result.fireflinkIndex,
101
- result.elementName,
102
- result.elementType
103
- );
104
- },
105
- clearThenEnterInput: async (result: any) => {
106
- if (!context.executor) {
107
- throw new Error("Browser not initialized.");
108
- }
109
- await context.executor.clearAndEnter(
110
- result.pageDOM,
111
- result.selector,
112
- result.value,
113
- result.fireflinkIndex,
114
- result.elementName,
115
- result.elementType
116
- );
117
- },
10
+ ): Record<string, ActionHandler> {
118
11
 
12
+ const requireExecutor = (): ActionExecutor => {
13
+ if (!context.executor) {
14
+ throw new Error("Browser not initialized.");
15
+ }
16
+ return context.executor;
119
17
  };
120
18
 
19
+ const withExecutor =
20
+ (fn: (executor: ActionExecutor, result?: any) => Promise<void>): ActionHandler =>
21
+ async (result?: any) => {
22
+ try {
23
+ const executor = requireExecutor();
24
+ await fn(executor, result);
25
+ }
26
+ catch (error) {
27
+ throw error
28
+ }
29
+ };
30
+ try {
31
+ return {
32
+
33
+ open: async () => {
34
+ try {
35
+ await context.session.open({
36
+ browserName: "chrome",
37
+ pageLoad,
38
+ implicit
39
+ });
40
+
41
+ context.executor = new ActionExecutor(
42
+ await context.session.getCurrentBrowser(),
43
+ context.scriptAppender,
44
+ context.locator,
45
+ context.request.platform
46
+ );
47
+
48
+ context.scriptAppender.add({
49
+ nlpName: "OpenBrowser",
50
+ elementsData: [],
51
+ stepInputs: []
52
+ });
53
+ }
54
+ catch (error) {
55
+ throw new Error(`Error occuried in createActionHandlers , error : ${error}`)
56
+ }
57
+ },
58
+
59
+ close: async () => {
60
+ try {
61
+ await context.session.close();
62
+ }
63
+ catch (error) {
64
+ throw new Error(`Error occuried in createActionHandlers , error : ${error}`)
65
+ }
66
+ },
67
+
68
+ navigate: withExecutor(async (executor, result) => {
69
+ await executor.navigate(result.response.keywords[0]);
70
+ }),
71
+
72
+ maximize: withExecutor(async (executor) => {
73
+ await executor.maximize();
74
+ }),
75
+
76
+ goBack: withExecutor(async (executor) => {
77
+ await executor.goBack();
78
+ }),
79
+
80
+ refresh: withExecutor(async (executor) => {
81
+ await executor.refresh();
82
+ }),
83
+
84
+ wait: withExecutor(async (executor, result) => {
85
+ await executor.wait(result.value);
86
+ }),
87
+
88
+ click: withExecutor(async (executor, result) => {
89
+ await executor.click(
90
+ result.pageDOM,
91
+ result.selector,
92
+ result.fireflinkIndex,
93
+ result.elementName,
94
+ result.elementType
95
+ );
96
+ }),
97
+
98
+ enter: withExecutor(async (executor, result) => {
99
+ await executor.enterInput(
100
+ result.selector,
101
+ result.value,
102
+ result.fireflinkIndex,
103
+ result.pageDOM,
104
+ result.elementName,
105
+ result.elementType
106
+ );
107
+ }),
108
+
109
+ clear: withExecutor(async (executor, result) => {
110
+ await executor.clear(
111
+ result.pageDOM,
112
+ result.selector,
113
+ result.fireflinkIndex,
114
+ result.elementName,
115
+ result.elementType
116
+ );
117
+ }),
118
+
119
+ clearThenEnterInput: withExecutor(async (executor, result) => {
120
+ await executor.clearAndEnter(
121
+ result.pageDOM,
122
+ result.selector,
123
+ result.value,
124
+ result.fireflinkIndex,
125
+ result.elementName,
126
+ result.elementType
127
+ );
128
+ }),
129
+ };
130
+ }
131
+ catch (error) {
132
+ throw error
133
+ }
121
134
 
122
- }
135
+ }
@@ -16,12 +16,10 @@ import { logger } from "../../utils/logger/logData.js"
16
16
  export class AutomationRunner implements IAutomationRunner {
17
17
  static sessionTerminationDetails: Record<string, boolean> = {};
18
18
  constructor(
19
- private readonly request: AutomationRequest,
20
19
  private pageLoad: number = 20000,
21
20
  private implicit: number = 15000,
22
- private tokensConsumed: number = 0
23
21
 
24
- ) { AutomationRunner.sessionTerminationDetails[request.testCaseId] = false }
22
+ ) { }
25
23
 
26
24
  static getSessionTerminationInfo(testCaseId: string): boolean {
27
25
  return AutomationRunner.sessionTerminationDetails[testCaseId]
@@ -35,16 +33,14 @@ export class AutomationRunner implements IAutomationRunner {
35
33
  domInfo: any,
36
34
  extractedRelevantDom: any,
37
35
  stepProcessor: StepProcessor,
38
- scriptRunner: ScriptRunner
36
+ scriptRunner: ScriptRunner,
37
+ request: AutomationRequest
39
38
  ) {
40
39
  try {
41
40
  logger.info("Starting cleanup process...");
42
41
  if (context?.session) {
43
42
  try {
44
43
  const browser = await context.session.getCurrentBrowser();
45
- // if (browser) {
46
- // await browser.deleteSession?.();
47
- // }
48
44
  } catch (e) {
49
45
  logger.error("Browser cleanup failed:", e);
50
46
  }
@@ -59,7 +55,7 @@ export class AutomationRunner implements IAutomationRunner {
59
55
 
60
56
  scriptRunner = null as any;
61
57
 
62
- delete AutomationRunner.sessionTerminationDetails[this.request.testCaseId];
58
+ delete AutomationRunner.sessionTerminationDetails[request.testCaseId];
63
59
 
64
60
  if (global.gc) {
65
61
  global.gc();
@@ -73,18 +69,19 @@ export class AutomationRunner implements IAutomationRunner {
73
69
  }
74
70
 
75
71
 
76
- async run(): Promise<void> {
72
+ async run(request: AutomationRequest): Promise<void> {
73
+ AutomationRunner.sessionTerminationDetails[request.testCaseId] = false;
77
74
  const apiService = new FireFlinkApiService();
78
75
  const scriptRunner = new ScriptRunner(apiService);
79
- const context = new ExecutionContext(this.request);
76
+ const context = new ExecutionContext(request);
80
77
  const configProvider = new ServiceProviderBaseUrlProvider();
81
- const baseUrl = configProvider.getBaseUrl(this.request.serviceProvider);
78
+ const baseUrl = configProvider.getBaseUrl(request.serviceProvider);
82
79
  let domInfo: any = null;
83
80
  let extractedRelevantDom: any = null;
84
- const llm = new llmAction(this.request.apiKey, baseUrl, this.request.model, this.request.visionApikey);
81
+ const llm = new llmAction(request.apiKey, baseUrl, request.model, request.visionApikey);
85
82
  const stepProcessor = new StepProcessor(llm);
86
83
 
87
- const stepResult = await stepProcessor.getLLMResponse({ type: PromptType.USER_STORY_TO_LIST, args: {}, input: { userStory: this.request.userStory } });
84
+ const stepResult = await stepProcessor.getLLMResponse({ type: PromptType.USER_STORY_TO_LIST, args: {}, input: { userStory: request.userStory } });
88
85
 
89
86
  const actionHandlers = createActionHandlers(
90
87
  context,
@@ -103,7 +100,7 @@ export class AutomationRunner implements IAutomationRunner {
103
100
 
104
101
  for (const step of listOfSteps) {
105
102
  try {
106
- if (AutomationRunner.getSessionTerminationInfo(this.request.testCaseId)) { break; }
103
+ if (AutomationRunner.getSessionTerminationInfo(request.testCaseId)) { break; }
107
104
 
108
105
  logger.info
109
106
  (
@@ -167,7 +164,7 @@ export class AutomationRunner implements IAutomationRunner {
167
164
  await logger.saveJSON(extractedRelevantDom, `relevant-dom-${stepCount}`);
168
165
  let stepResult: any;
169
166
  if (action == "verify") {
170
- const stepResult = await stepProcessor.getLLMResponse({
167
+ stepResult = await stepProcessor.getLLMResponse({
171
168
  type: PromptType.VERIFY_PROMPT,
172
169
  args: {
173
170
  extractedDomJson: JSON.stringify(extractedRelevantDom, null, 2),
@@ -224,35 +221,35 @@ export class AutomationRunner implements IAutomationRunner {
224
221
  elementsData: [],
225
222
  stepInputs: []
226
223
  });
227
-
228
- logger.error(`Error executing step "${step}":`, error.message);
224
+ logger.error(`Error executing step "${step}":`, error);
229
225
  break;
230
226
  }
231
227
  }
232
228
  const payload: IPayload = {
233
- scriptName: this.request.scriptName,
234
- scriptType: this.request.scriptType,
235
- projectId: this.request.projectId,
236
- testCaseId: this.request.testCaseId,
237
- promptId: this.request.promptId,
238
- pageDetails: this.request.pageDetails,
239
- generatedBy: this.request.generatedBy,
240
- webSocketId: this.request.webSocketId,
241
- licenseType: this.request.licenseType,
242
- licenseId: this.request.licenseId,
243
- userId: this.request.userId,
244
- topic: this.request.topic,
245
- projectType: this.request.projectType,
229
+ scriptName: request.scriptName,
230
+ scriptType: request.scriptType,
231
+ projectId: request.projectId,
232
+ testCaseId: request.testCaseId,
233
+ promptId: request.promptId,
234
+ pageDetails: request.pageDetails,
235
+ generatedBy: request.generatedBy,
236
+ webSocketId: request.webSocketId,
237
+ licenseType: request.licenseType,
238
+ licenseId: request.licenseId,
239
+ userId: request.userId,
240
+ topic: request.topic,
241
+ projectType: request.projectType,
246
242
  tokensConsumed: (await stepProcessor.getResultTokenUsage()).totalTokens,
247
243
  }
248
244
  try {
249
245
  await scriptRunner.runScriptFromPayload(
250
246
  context.scriptAppender.getData(),
251
247
  payload,
252
- this.request.token,
253
- this.request.serverHost
248
+ request.token,
249
+ request.serverHost
254
250
  );
255
251
  } catch (error: any) {
252
+
256
253
  throw new Error(
257
254
  "Failed to send payload to FireFlink API:", { cause: error }
258
255
  );
@@ -263,7 +260,8 @@ export class AutomationRunner implements IAutomationRunner {
263
260
  domInfo,
264
261
  extractedRelevantDom,
265
262
  stepProcessor,
266
- scriptRunner
263
+ scriptRunner,
264
+ request
267
265
  );
268
266
  }
269
267
  }
@@ -4,6 +4,19 @@ import beautify from "js-beautify";
4
4
 
5
5
  const logsDir = path.resolve(process.cwd(), "logs");
6
6
  const domDir = path.resolve(process.cwd(), "dom");
7
+ const now = new Date();
8
+
9
+ const istDate = new Intl.DateTimeFormat("sv-SE", {
10
+ timeZone: "Asia/Kolkata",
11
+ year: "numeric",
12
+ month: "2-digit",
13
+ day: "2-digit",
14
+ hour: "2-digit",
15
+ minute: "2-digit",
16
+ second: "2-digit",
17
+ fractionalSecondDigits: 3,
18
+ hour12: false,
19
+ }).format(now).replace(" ", "T");
7
20
 
8
21
  if (!fs.existsSync(logsDir)) {
9
22
  fs.mkdirSync(logsDir, { recursive: true });
@@ -23,26 +36,49 @@ export const logger = {
23
36
  : arg
24
37
  );
25
38
 
26
- const message = `[INFO] ${new Date().toISOString()} - ${formattedArgs.join(" ")}\n`;
39
+ const message = `[INFO] ${istDate}+05:30 - ${formattedArgs.join(" ")}\n`;
27
40
 
28
41
  fs.appendFileSync(logFilePath, message, "utf-8");
29
42
  },
30
-
31
43
  error: (...args: any[]) => {
32
- const formattedArgs = args.map(arg =>
33
- typeof arg === "object"
34
- ? JSON.stringify(arg, null, 2)
35
- : arg
36
- );
44
+ const formatted = args.map(arg => {
45
+ if (arg instanceof Error) {
46
+ let output = arg.stack || `${arg.name}: ${arg.message}`;
47
+ output = output.replace(
48
+ /invalid session id:\s*invalid session id:/gi,
49
+ "invalid session id:"
50
+ );
51
+
52
+ if (arg.cause instanceof Error) {
53
+ let causeStack = arg.cause.stack || "";
54
+
55
+ causeStack = causeStack.replace(
56
+ /invalid session id:\s*invalid session id:/gi,
57
+ "invalid session id:"
58
+ );
59
+
60
+ output += `\nCaused by: ${causeStack}`;
61
+ }
62
+
63
+ return output;
64
+ }
65
+
66
+ if (typeof arg === "object") {
67
+ return JSON.stringify(arg, null, 2);
68
+ }
69
+
70
+ return String(arg);
71
+ });
37
72
 
38
- const message = `[ERROR] ${new Date().toISOString()} - ${formattedArgs.join(" ")}\n`;
73
+ const message =
74
+ `[ERROR] ${new Date().toISOString()} - ${formatted.join(" ")}\n`;
39
75
 
40
76
  fs.appendFileSync(logFilePath, message, "utf-8");
41
77
  },
42
78
 
43
79
  saveDOM: (domContent: string, fileName?: string) => {
44
80
 
45
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
81
+ const timestamp = new Date().toLocaleString().replace(/[:.]/g, "-");
46
82
  const finalFileName = fileName
47
83
  ? `${fileName}.html`
48
84
  : `dom-${timestamp}.html`;
@@ -59,7 +95,7 @@ export const logger = {
59
95
  },
60
96
 
61
97
  saveJSON: (data: unknown, fileName?: string) => {
62
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
98
+ const timestamp = new Date().toLocaleString().replace(/[:.]/g, "-");
63
99
 
64
100
  const finalFileName = fileName
65
101
  ? `${fileName}.json`