lexxit-automation-framework 3.0.2 → 3.0.4
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/dist-obf/actions/baseHandler.js +1 -1
- package/dist-obf/actions/browserManager.js +1 -1
- package/dist-obf/actions/checkboxHandler.js +1 -1
- package/dist-obf/actions/clickHandler.js +1 -1
- package/dist-obf/actions/customcodehandler.js +1 -1
- package/dist-obf/actions/dropdownHandler.js +1 -1
- package/dist-obf/actions/radiobuttonHandler.js +1 -1
- package/dist-obf/actions/textHandler.js +1 -1
- package/dist-obf/api/server.js +1 -1
- package/dist-obf/config.js +1 -1
- package/dist-obf/executor/functionMap.js +1 -1
- package/dist-obf/executor/mapping.js +1 -1
- package/dist-obf/executor/scriptExecutor.js +1 -1
- package/dist-obf/executor/stepExecutor.js +1 -1
- package/dist-obf/runner/testRunner.js +1 -1
- package/dist-obf/sse/sseManager.js +1 -1
- package/dist-obf/store/executionStore.js +1 -1
- package/dist-obf/store/testDataStore.js +1 -1
- package/dist-obf/types/types.js +1 -1
- package/dist-obf/utils/healingService.js +1 -1
- package/dist-obf/utils/locatorService.js +1 -1
- package/dist-obf/utils/logger.js +1 -1
- package/dist-obf/utils/metadataService.js +1 -1
- package/dist-obf/utils/waitConditions.js +1 -1
- package/dist-obf/validator/validator.js +1 -1
- package/package.json +7 -1
- package/npmignore +0 -8
- package/public/dashboard.html +0 -591
- package/src/actions/baseHandler.ts +0 -351
- package/src/actions/browserManager.ts +0 -432
- package/src/actions/checkboxHandler.ts +0 -276
- package/src/actions/clickHandler.ts +0 -513
- package/src/actions/customcodehandler.ts +0 -251
- package/src/actions/dropdownHandler.ts +0 -501
- package/src/actions/radiobuttonHandler.ts +0 -286
- package/src/actions/textHandler.ts +0 -498
- package/src/api/server.ts +0 -210
- package/src/config.ts +0 -7
- package/src/executor/functionMap.ts +0 -153
- package/src/executor/mapping.ts +0 -70
- package/src/executor/scriptExecutor.ts +0 -162
- package/src/executor/stepExecutor.ts +0 -289
- package/src/runner/testRunner.ts +0 -78
- package/src/sse/sseManager.ts +0 -130
- package/src/store/executionStore.ts +0 -152
- package/src/store/testDataStore.ts +0 -46
- package/src/types/types.ts +0 -159
- package/src/utils/healingService.ts +0 -210
- package/src/utils/locatorService.ts +0 -73
- package/src/utils/logger.ts +0 -27
- package/src/utils/metadataService.ts +0 -137
- package/src/utils/waitConditions.ts +0 -141
- package/src/validator/validator.ts +0 -140
- package/tsconfig.json +0 -16
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
// import { StepRequest, StepResult } from '../types/types';
|
|
2
|
-
// import { FUNCTION_MAP } from './functionMap';
|
|
3
|
-
// import { Mapping } from './mapping';
|
|
4
|
-
// import { TextHandler } from '../actions/textHandler';
|
|
5
|
-
// import { ClickHandler } from '../actions/clickHandler';
|
|
6
|
-
// import { CheckboxHandler } from '../actions/checkboxHandler';
|
|
7
|
-
// import { RadiobuttonHandler } from '../actions/radiobuttonHandler';
|
|
8
|
-
// import { DropdownHandler } from '../actions/dropdownHandler';
|
|
9
|
-
// import { BrowserManager } from '../actions/browserManager';
|
|
10
|
-
|
|
11
|
-
// export class StepExecutor {
|
|
12
|
-
// private browserManager: BrowserManager;
|
|
13
|
-
|
|
14
|
-
// constructor(browserManager: BrowserManager) {
|
|
15
|
-
// this.browserManager = browserManager;
|
|
16
|
-
// }
|
|
17
|
-
|
|
18
|
-
// private getHandlerInstance(handlerName: string): any {
|
|
19
|
-
// if (handlerName === 'browserManager') {
|
|
20
|
-
// return this.browserManager;
|
|
21
|
-
// }
|
|
22
|
-
|
|
23
|
-
// const page = this.browserManager.getPage();
|
|
24
|
-
// const handlerMap: Record<string, any> = {
|
|
25
|
-
// textHandler: new TextHandler(page),
|
|
26
|
-
// clickHandler: new ClickHandler(page),
|
|
27
|
-
// checkboxHandler: new CheckboxHandler(page),
|
|
28
|
-
// radiobuttonHandler: new RadiobuttonHandler(page),
|
|
29
|
-
// dropdownHandler: new DropdownHandler(page)
|
|
30
|
-
// };
|
|
31
|
-
|
|
32
|
-
// return handlerMap[handlerName];
|
|
33
|
-
// }
|
|
34
|
-
|
|
35
|
-
// private buildSkippedResult(step: StepRequest): StepResult {
|
|
36
|
-
// return {
|
|
37
|
-
// uid: null,
|
|
38
|
-
// obj_uid: step.obj_uid ?? null,
|
|
39
|
-
// page_uid: step.page_uid ?? null,
|
|
40
|
-
// step_name: step.step_name,
|
|
41
|
-
// status: 'skip',
|
|
42
|
-
// expected_result: 'Step skipped',
|
|
43
|
-
// comments: 'Skipped due to previous step failure',
|
|
44
|
-
// duration: '0 seconds',
|
|
45
|
-
// start_time: new Date().toISOString().replace('T', ' ').split('.')[0]
|
|
46
|
-
// };
|
|
47
|
-
// }
|
|
48
|
-
|
|
49
|
-
// private buildErrorResult(step: StepRequest, message: string): StepResult {
|
|
50
|
-
// return {
|
|
51
|
-
// uid: null,
|
|
52
|
-
// obj_uid: step.obj_uid ?? null,
|
|
53
|
-
// page_uid: step.page_uid ?? null,
|
|
54
|
-
// step_name: step.step_name,
|
|
55
|
-
// status: 'fail',
|
|
56
|
-
// expected_result: 'Step executed successfully',
|
|
57
|
-
// comments: message,
|
|
58
|
-
// duration: '0 seconds',
|
|
59
|
-
// start_time: new Date().toISOString().replace('T', ' ').split('.')[0]
|
|
60
|
-
// };
|
|
61
|
-
// }
|
|
62
|
-
|
|
63
|
-
// async executeStep(
|
|
64
|
-
// step: StepRequest,
|
|
65
|
-
// skip: boolean = false,
|
|
66
|
-
// context: { browser?: string; headless?: boolean; videoEnabled?: boolean; testScriptUid?: string } = {}
|
|
67
|
-
// ): Promise<StepResult> {
|
|
68
|
-
// if (skip) {
|
|
69
|
-
// return this.buildSkippedResult(step);
|
|
70
|
-
// }
|
|
71
|
-
|
|
72
|
-
// try {
|
|
73
|
-
// const parsed = Mapping.parse(step.step_script);
|
|
74
|
-
// const handlerInfo = FUNCTION_MAP[parsed.functionName];
|
|
75
|
-
|
|
76
|
-
// if (!handlerInfo) {
|
|
77
|
-
// return this.buildErrorResult(step, `No handler mapped for function: ${parsed.functionName}`);
|
|
78
|
-
// }
|
|
79
|
-
|
|
80
|
-
// const handlerInstance = this.getHandlerInstance(handlerInfo.handler);
|
|
81
|
-
|
|
82
|
-
// if (!handlerInstance) {
|
|
83
|
-
// return this.buildErrorResult(step, `Handler instance not found: ${handlerInfo.handler}`);
|
|
84
|
-
// }
|
|
85
|
-
|
|
86
|
-
// const method = handlerInstance[handlerInfo.method];
|
|
87
|
-
|
|
88
|
-
// if (typeof method !== 'function') {
|
|
89
|
-
// return this.buildErrorResult(step, `Method '${handlerInfo.method}' not found on handler '${handlerInfo.handler}'`);
|
|
90
|
-
// }
|
|
91
|
-
|
|
92
|
-
// const callArgs = this.buildCallArgs(handlerInfo.handler, parsed, step, context);
|
|
93
|
-
// const result: StepResult = await method.apply(handlerInstance, callArgs);
|
|
94
|
-
|
|
95
|
-
// result.step_name = step.step_name;
|
|
96
|
-
// result.obj_uid = step.obj_uid ?? null;
|
|
97
|
-
// result.page_uid = step.page_uid ?? null;
|
|
98
|
-
|
|
99
|
-
// return result;
|
|
100
|
-
// } catch (error: any) {
|
|
101
|
-
// return this.buildErrorResult(step, `Unexpected error executing step: ${error.message || error}`);
|
|
102
|
-
// }
|
|
103
|
-
// }
|
|
104
|
-
|
|
105
|
-
// private buildCallArgs(
|
|
106
|
-
// handlerName: string,
|
|
107
|
-
// parsed: { functionName: string; args: string[] },
|
|
108
|
-
// step: StepRequest,
|
|
109
|
-
// context: { browser?: string; headless?: boolean; videoEnabled?: boolean; testScriptUid?: string }
|
|
110
|
-
// ): any[] {
|
|
111
|
-
// if (handlerName === 'browserManager') {
|
|
112
|
-
// if (parsed.functionName === 'openBrowser') {
|
|
113
|
-
// return [context.browser, context.headless, context.videoEnabled];
|
|
114
|
-
// }
|
|
115
|
-
// if (parsed.functionName === 'closeBrowser') {
|
|
116
|
-
// return [context.testScriptUid];
|
|
117
|
-
// }
|
|
118
|
-
// return [...parsed.args, step.step_name];
|
|
119
|
-
// }
|
|
120
|
-
|
|
121
|
-
// const label = step.label || '';
|
|
122
|
-
// const locators = step.locators || [];
|
|
123
|
-
// return [label, locators, ...parsed.args.slice(2), step.step_name];
|
|
124
|
-
// }
|
|
125
|
-
// }
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
import { StepRequest, StepResult } from '../types/types';
|
|
129
|
-
import { FUNCTION_MAP } from './functionMap';
|
|
130
|
-
import { Mapping } from './mapping';
|
|
131
|
-
import { TextHandler } from '../actions/textHandler';
|
|
132
|
-
import { ClickHandler } from '../actions/clickHandler';
|
|
133
|
-
import { CheckboxHandler } from '../actions/checkboxHandler';
|
|
134
|
-
import { RadiobuttonHandler } from '../actions/radiobuttonHandler';
|
|
135
|
-
import { DropdownHandler } from '../actions/dropdownHandler';
|
|
136
|
-
import { BrowserManager } from '../actions/browserManager';
|
|
137
|
-
import { CustomCodeHandler } from '../actions/customcodehandler';
|
|
138
|
-
import { TestDataStore } from '../store/testDataStore';
|
|
139
|
-
import { ScreenshotMode } from '../types/types';
|
|
140
|
-
import { DEFAULT_SCREENSHOT_MODE } from '../config';
|
|
141
|
-
|
|
142
|
-
type StepContext = {
|
|
143
|
-
browser?: string;
|
|
144
|
-
headless?: boolean;
|
|
145
|
-
videoEnabled?: boolean;
|
|
146
|
-
testScriptUid?: string;
|
|
147
|
-
screenshotMode?: ScreenshotMode;
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
export class StepExecutor {
|
|
151
|
-
private browserManager: BrowserManager;
|
|
152
|
-
private testData: TestDataStore;
|
|
153
|
-
|
|
154
|
-
constructor(browserManager: BrowserManager, testData?: TestDataStore) {
|
|
155
|
-
this.browserManager = browserManager;
|
|
156
|
-
this.testData = testData ?? new TestDataStore();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private getHandlerInstance(handlerName: string, screenshotMode: ScreenshotMode): any {
|
|
160
|
-
if (handlerName === 'browserManager') {
|
|
161
|
-
return this.browserManager;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const page = this.browserManager.getPage();
|
|
165
|
-
const handlerMap: Record<string, any> = {
|
|
166
|
-
textHandler: new TextHandler(page),
|
|
167
|
-
clickHandler: new ClickHandler(page, screenshotMode),
|
|
168
|
-
checkboxHandler: new CheckboxHandler(page),
|
|
169
|
-
radiobuttonHandler: new RadiobuttonHandler(page),
|
|
170
|
-
dropdownHandler: new DropdownHandler(page),
|
|
171
|
-
customCodeHandler: new CustomCodeHandler(page, this.testData)
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
return handlerMap[handlerName];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
private buildSkippedResult(step: StepRequest): StepResult {
|
|
178
|
-
return {
|
|
179
|
-
uid: null,
|
|
180
|
-
obj_uid: step.obj_uid ?? null,
|
|
181
|
-
page_uid: step.page_uid ?? null,
|
|
182
|
-
step_name: step.step_name,
|
|
183
|
-
step_script: step.step_script,
|
|
184
|
-
status: 'SKIP',
|
|
185
|
-
expected_result: 'Step skipped',
|
|
186
|
-
comments: 'Skipped due to previous step failure',
|
|
187
|
-
duration: 0,
|
|
188
|
-
start_time: new Date().toISOString().replace('T', ' ').split('.')[0]
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private buildErrorResult(step: StepRequest, message: string): StepResult {
|
|
193
|
-
return {
|
|
194
|
-
uid: null,
|
|
195
|
-
obj_uid: step.obj_uid ?? null,
|
|
196
|
-
page_uid: step.page_uid ?? null,
|
|
197
|
-
step_name: step.step_name,
|
|
198
|
-
step_script: step.step_script,
|
|
199
|
-
status: 'FAIL',
|
|
200
|
-
expected_result: 'Step executed successfully',
|
|
201
|
-
comments: message,
|
|
202
|
-
duration: 0,
|
|
203
|
-
start_time: new Date().toISOString().replace('T', ' ').split('.')[0]
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async executeStep(
|
|
208
|
-
step: StepRequest,
|
|
209
|
-
skip: boolean = false,
|
|
210
|
-
context: StepContext = {}
|
|
211
|
-
): Promise<StepResult> {
|
|
212
|
-
if (skip) {
|
|
213
|
-
return this.buildSkippedResult(step);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const screenshotMode: ScreenshotMode = context.screenshotMode ?? DEFAULT_SCREENSHOT_MODE;
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
const parsed = Mapping.parse(step.step_script);
|
|
220
|
-
const handlerInfo = FUNCTION_MAP[parsed.functionName];
|
|
221
|
-
|
|
222
|
-
if (!handlerInfo) {
|
|
223
|
-
return this.buildErrorResult(step, `No handler mapped for function: ${parsed.functionName}`);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const handlerInstance = this.getHandlerInstance(handlerInfo.handler, screenshotMode);
|
|
227
|
-
|
|
228
|
-
if (!handlerInstance) {
|
|
229
|
-
return this.buildErrorResult(step, `Handler instance not found: ${handlerInfo.handler}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const method = handlerInstance[handlerInfo.method];
|
|
233
|
-
|
|
234
|
-
if (typeof method !== 'function') {
|
|
235
|
-
return this.buildErrorResult(step, `Method '${handlerInfo.method}' not found on handler '${handlerInfo.handler}'`);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const callArgs = this.buildCallArgs(handlerInfo.handler, parsed, step, context);
|
|
239
|
-
const result: StepResult = await method.apply(handlerInstance, callArgs);
|
|
240
|
-
const screenshot = await this.browserManager.captureScreenshot(
|
|
241
|
-
context.screenshotMode || 'never',
|
|
242
|
-
result.status === 'FAIL'
|
|
243
|
-
);
|
|
244
|
-
result.screenshot = screenshot;
|
|
245
|
-
|
|
246
|
-
result.step_name = step.step_name;
|
|
247
|
-
result.obj_uid = step.obj_uid ?? null;
|
|
248
|
-
result.page_uid = step.page_uid ?? null;
|
|
249
|
-
|
|
250
|
-
return result;
|
|
251
|
-
} catch (error: any) {
|
|
252
|
-
return this.buildErrorResult(step, `Unexpected error executing step: ${error.message || error}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private buildCallArgs(
|
|
257
|
-
handlerName: string,
|
|
258
|
-
parsed: { functionName: string; args: string[] },
|
|
259
|
-
step: StepRequest,
|
|
260
|
-
context: StepContext
|
|
261
|
-
): any[] {
|
|
262
|
-
if (handlerName === 'browserManager') {
|
|
263
|
-
if (parsed.functionName === 'openBrowser') {
|
|
264
|
-
return [context.browser, context.headless, context.videoEnabled];
|
|
265
|
-
}
|
|
266
|
-
if (parsed.functionName === 'closeBrowser') {
|
|
267
|
-
return [context.testScriptUid];
|
|
268
|
-
}
|
|
269
|
-
return [...parsed.args, step.step_name];
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// customCodeHandler: runCustomCode(codeStr, returnVariable?, xpath?)
|
|
273
|
-
// args come straight from the step_script — no label/locators prepended
|
|
274
|
-
// if (handlerName === 'customCodeHandler') {
|
|
275
|
-
// return [...parsed.args, step.step_name];
|
|
276
|
-
// }
|
|
277
|
-
if (handlerName === 'customCodeHandler') {
|
|
278
|
-
const [codeStr, returnVariable, xpath] = parsed.args;
|
|
279
|
-
return [codeStr, returnVariable, xpath, step.step_name];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const label = step.label || '';
|
|
283
|
-
const locators = step.locators || [];
|
|
284
|
-
const resolvedArgs = parsed.args.slice(2).map(arg =>
|
|
285
|
-
typeof arg === 'string' ? this.testData.resolve(arg) : arg
|
|
286
|
-
);
|
|
287
|
-
return [label, locators, ...resolvedArgs, step.step_name];
|
|
288
|
-
}
|
|
289
|
-
}
|
package/src/runner/testRunner.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { TestSetRequest, TestSetResult, TestScriptResult } from '../types/types';
|
|
2
|
-
import { ScriptExecutor } from '../executor/scriptExecutor';
|
|
3
|
-
import { executionStore } from '../store/executionStore';
|
|
4
|
-
import { sseManager } from '../sse/sseManager';
|
|
5
|
-
|
|
6
|
-
const MAX_PARALLEL = 10;
|
|
7
|
-
|
|
8
|
-
export async function testRunner(payload: TestSetRequest, executionId: string): Promise<TestSetResult> {
|
|
9
|
-
const startMs = Date.now();
|
|
10
|
-
|
|
11
|
-
const execMode = payload.exec_mode?.mode || 'fast';
|
|
12
|
-
const delayMs = execMode === 'fast' ? 0 : (payload.exec_mode?.delay_ms || (execMode === 'medium' ? 1000 : 3000));
|
|
13
|
-
const effectiveVideoEnabled = execMode === 'fast' ? false : payload.video_enabled;
|
|
14
|
-
|
|
15
|
-
// ─── Emit execution start ──────────────────────────────────────────────────
|
|
16
|
-
sseManager.emit(executionId, 'execution_start', {
|
|
17
|
-
test_set_name: payload.test_set_name,
|
|
18
|
-
total_scripts: payload.scripts.length,
|
|
19
|
-
parallel: payload.parallel,
|
|
20
|
-
exec_mode: payload.exec_mode
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const scriptExecutor = new ScriptExecutor();
|
|
24
|
-
let results: TestScriptResult[] = [];
|
|
25
|
-
|
|
26
|
-
if (payload.parallel) {
|
|
27
|
-
results = await runInBatches(payload, scriptExecutor, MAX_PARALLEL, executionId, effectiveVideoEnabled, delayMs);
|
|
28
|
-
} else {
|
|
29
|
-
for (const script of payload.scripts) {
|
|
30
|
-
const result = await scriptExecutor.executeScript(script, effectiveVideoEnabled, executionId, delayMs);
|
|
31
|
-
results.push(result);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const passed = results.filter((r) => r.status === 'PASS').length;
|
|
36
|
-
const failed = results.filter((r) => r.status === 'FAIL').length;
|
|
37
|
-
const overallStatus = failed > 0 ? 'FAIL' : 'PASS';
|
|
38
|
-
|
|
39
|
-
const finalResult: TestSetResult = {
|
|
40
|
-
executionId,
|
|
41
|
-
status: overallStatus,
|
|
42
|
-
total_scripts: results.length,
|
|
43
|
-
passed,
|
|
44
|
-
failed,
|
|
45
|
-
duration: Date.now() - startMs,
|
|
46
|
-
results
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
executionStore.setFinalResult(executionId, finalResult);
|
|
50
|
-
executionStore.completeExecution(executionId, overallStatus);
|
|
51
|
-
|
|
52
|
-
// ─── Emit execution complete ───────────────────────────────────────────────
|
|
53
|
-
sseManager.emit(executionId, 'execution_complete', finalResult);
|
|
54
|
-
|
|
55
|
-
return finalResult;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function runInBatches(
|
|
59
|
-
payload: TestSetRequest,
|
|
60
|
-
scriptExecutor: ScriptExecutor,
|
|
61
|
-
maxParallel: number,
|
|
62
|
-
executionId: string,
|
|
63
|
-
videoEnabled: boolean,
|
|
64
|
-
delayMs: number
|
|
65
|
-
): Promise<TestScriptResult[]> {
|
|
66
|
-
const results: TestScriptResult[] = [];
|
|
67
|
-
const scripts = payload.scripts;
|
|
68
|
-
|
|
69
|
-
for (let i = 0; i < scripts.length; i += maxParallel) {
|
|
70
|
-
const batch = scripts.slice(i, i + maxParallel);
|
|
71
|
-
const batchResults = await Promise.all(
|
|
72
|
-
batch.map((script) => scriptExecutor.executeScript(script, videoEnabled, executionId, delayMs))
|
|
73
|
-
);
|
|
74
|
-
results.push(...batchResults);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return results;
|
|
78
|
-
}
|
package/src/sse/sseManager.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { Response } from 'express';
|
|
2
|
-
import { SSEEvent, SSEEventType } from '../types/types';
|
|
3
|
-
import { Logger } from '../utils/logger';
|
|
4
|
-
|
|
5
|
-
const log = Logger.create('SSEManager');
|
|
6
|
-
|
|
7
|
-
export class SSEManager {
|
|
8
|
-
private clients: Map<string, Set<Response>> = new Map();
|
|
9
|
-
private eventBuffer: Map<string, SSEEvent[]> = new Map();
|
|
10
|
-
private readonly MAX_BUFFER = 500;
|
|
11
|
-
private readonly BUFFER_TTL = 2 * 60 * 60 * 1000;
|
|
12
|
-
|
|
13
|
-
// ─── Add Client ────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
addClient(executionId: string, res: Response): void {
|
|
16
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
17
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
18
|
-
res.setHeader('Connection', 'keep-alive');
|
|
19
|
-
res.setHeader('X-Accel-Buffering', 'no');
|
|
20
|
-
res.flushHeaders();
|
|
21
|
-
|
|
22
|
-
if (!this.clients.has(executionId)) {
|
|
23
|
-
this.clients.set(executionId, new Set());
|
|
24
|
-
}
|
|
25
|
-
this.clients.get(executionId)!.add(res);
|
|
26
|
-
|
|
27
|
-
log.info(`SSE client connected: ${executionId}`);
|
|
28
|
-
|
|
29
|
-
const pastEvents = this.eventBuffer.get(executionId) ?? [];
|
|
30
|
-
if (pastEvents.length > 0) {
|
|
31
|
-
log.info(`Replaying ${pastEvents.length} past events to late client: ${executionId}`);
|
|
32
|
-
pastEvents.forEach((ev) => this.sendToOne(res, ev));
|
|
33
|
-
} else {
|
|
34
|
-
this.sendToOne(res, {
|
|
35
|
-
type: 'log',
|
|
36
|
-
executionId,
|
|
37
|
-
timestamp: new Date().toISOString(),
|
|
38
|
-
data: { message: 'Connected to execution stream' }
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const hb = setInterval(() => {
|
|
43
|
-
if (!res.writableEnded) res.write(': heartbeat\n\n');
|
|
44
|
-
else clearInterval(hb);
|
|
45
|
-
}, 20000);
|
|
46
|
-
|
|
47
|
-
res.on('close', () => {
|
|
48
|
-
clearInterval(hb);
|
|
49
|
-
this.removeClient(executionId, res);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ─── Emit ──────────────────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
emit(executionId: string, type: SSEEventType, data: any): void {
|
|
56
|
-
const event: SSEEvent = {
|
|
57
|
-
type,
|
|
58
|
-
executionId,
|
|
59
|
-
timestamp: new Date().toISOString(),
|
|
60
|
-
data
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
if (!this.eventBuffer.has(executionId)) {
|
|
64
|
-
this.eventBuffer.set(executionId, []);
|
|
65
|
-
}
|
|
66
|
-
const buf = this.eventBuffer.get(executionId)!;
|
|
67
|
-
if (buf.length < this.MAX_BUFFER) {
|
|
68
|
-
buf.push(event);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const clients = this.clients.get(executionId);
|
|
72
|
-
if (clients && clients.size > 0) {
|
|
73
|
-
clients.forEach((res) => this.sendToOne(res, event));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (type === 'execution_complete') {
|
|
77
|
-
setTimeout(() => this.clients.delete(executionId), 5000);
|
|
78
|
-
setTimeout(() => this.eventBuffer.delete(executionId), this.BUFFER_TTL);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ─── Broadcast ─────────────────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
broadcast(type: SSEEventType, data: any): void {
|
|
85
|
-
const event: SSEEvent = {
|
|
86
|
-
type,
|
|
87
|
-
executionId: 'global',
|
|
88
|
-
timestamp: new Date().toISOString(),
|
|
89
|
-
data
|
|
90
|
-
};
|
|
91
|
-
this.clients.forEach((clients) => {
|
|
92
|
-
clients.forEach((res) => this.sendToOne(res, event));
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ─── Get Buffer ────────────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
getBuffer(executionId: string): SSEEvent[] {
|
|
99
|
-
return this.eventBuffer.get(executionId) ?? [];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ─── Has Clients ───────────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
hasClients(executionId: string): boolean {
|
|
105
|
-
return (this.clients.get(executionId)?.size ?? 0) > 0;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ─── Private: Send To One ──────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
private sendToOne(res: Response, event: SSEEvent): void {
|
|
111
|
-
try {
|
|
112
|
-
if (!res.writableEnded) {
|
|
113
|
-
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
114
|
-
}
|
|
115
|
-
} catch {
|
|
116
|
-
/* client gone */
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ─── Private: Remove Client ────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
private removeClient(executionId: string, res: Response): void {
|
|
123
|
-
this.clients.get(executionId)?.delete(res);
|
|
124
|
-
if (this.clients.get(executionId)?.size === 0) {
|
|
125
|
-
this.clients.delete(executionId);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export const sseManager = new SSEManager();
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { ExecutionState, LiveScriptState, LiveStepState, ExecutionOverallStatus, StepLiveStatus, TestSetResult } from '../types/types';
|
|
2
|
-
|
|
3
|
-
class ExecutionStore {
|
|
4
|
-
private executions: Map<string, ExecutionState> = new Map();
|
|
5
|
-
|
|
6
|
-
// ─── Create ────────────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
createExecution(executionId: string, testSetName: string): void {
|
|
9
|
-
const execution: ExecutionState = {
|
|
10
|
-
execution_id: executionId,
|
|
11
|
-
test_set_name: testSetName,
|
|
12
|
-
status: 'RUNNING',
|
|
13
|
-
start_time: new Date().toISOString(),
|
|
14
|
-
end_time: null,
|
|
15
|
-
scripts: [],
|
|
16
|
-
final_result: null
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
this.executions.set(executionId, execution);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ─── Register Script ───────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
registerScript(executionId: string, testScriptUid: string, testCaseName: string, stepNames: string[]): void {
|
|
25
|
-
const execution = this.executions.get(executionId);
|
|
26
|
-
if (!execution) return;
|
|
27
|
-
|
|
28
|
-
const scriptState: LiveScriptState = {
|
|
29
|
-
test_script_uid: testScriptUid,
|
|
30
|
-
test_case_name: testCaseName,
|
|
31
|
-
status: 'RUNNING',
|
|
32
|
-
video_path: null,
|
|
33
|
-
steps: stepNames.map((name) => ({
|
|
34
|
-
step_name: name,
|
|
35
|
-
status: 'PENDING' as StepLiveStatus,
|
|
36
|
-
expected_result: null,
|
|
37
|
-
comments: null,
|
|
38
|
-
duration: null
|
|
39
|
-
}))
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
execution.scripts.push(scriptState);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ─── Set Video Path ────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
setVideoPath(executionId: string, testScriptUid: string, videoPath: string): void {
|
|
48
|
-
const execution = this.executions.get(executionId);
|
|
49
|
-
if (!execution) return;
|
|
50
|
-
|
|
51
|
-
const script = execution.scripts.find((s) => s.test_script_uid === testScriptUid);
|
|
52
|
-
if (!script) return;
|
|
53
|
-
|
|
54
|
-
script.video_path = videoPath;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ─── Update Step ───────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
updateStep(
|
|
60
|
-
executionId: string,
|
|
61
|
-
testScriptUid: string,
|
|
62
|
-
stepIndex: number,
|
|
63
|
-
status: StepLiveStatus,
|
|
64
|
-
expected_result: string | null = null,
|
|
65
|
-
comments: string | null = null,
|
|
66
|
-
duration: number | null = null
|
|
67
|
-
): void {
|
|
68
|
-
const execution = this.executions.get(executionId);
|
|
69
|
-
if (!execution) return;
|
|
70
|
-
|
|
71
|
-
const script = execution.scripts.find((s) => s.test_script_uid === testScriptUid);
|
|
72
|
-
if (!script || !script.steps[stepIndex]) return;
|
|
73
|
-
|
|
74
|
-
script.steps[stepIndex].status = status;
|
|
75
|
-
if (expected_result !== null) script.steps[stepIndex].expected_result = expected_result;
|
|
76
|
-
if (comments !== null) script.steps[stepIndex].comments = comments;
|
|
77
|
-
if (duration !== null) script.steps[stepIndex].duration = duration;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ─── Update Script Status ──────────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
updateScriptStatus(executionId: string, testScriptUid: string, status: ExecutionOverallStatus): void {
|
|
83
|
-
const execution = this.executions.get(executionId);
|
|
84
|
-
if (!execution) return;
|
|
85
|
-
|
|
86
|
-
const script = execution.scripts.find((s) => s.test_script_uid === testScriptUid);
|
|
87
|
-
if (!script) return;
|
|
88
|
-
|
|
89
|
-
script.status = status;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ─── Complete Execution ────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
completeExecution(executionId: string, status: ExecutionOverallStatus): void {
|
|
95
|
-
const execution = this.executions.get(executionId);
|
|
96
|
-
if (!execution) return;
|
|
97
|
-
|
|
98
|
-
execution.status = status;
|
|
99
|
-
execution.end_time = new Date().toISOString();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ─── Get Execution ─────────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
getExecution(executionId: string): ExecutionState | undefined {
|
|
105
|
-
return this.executions.get(executionId);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ─── Set Final Result ──────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
setFinalResult(executionId: string, result: TestSetResult): void {
|
|
111
|
-
const execution = this.executions.get(executionId);
|
|
112
|
-
if (!execution) return;
|
|
113
|
-
|
|
114
|
-
execution.final_result = result;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ─── List Executions (latest first) ───────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
listExecutions(): { execution_id: string; test_set_name: string; status: ExecutionOverallStatus; start_time: string }[] {
|
|
120
|
-
return Array.from(this.executions.values())
|
|
121
|
-
.map((e) => ({
|
|
122
|
-
execution_id: e.execution_id,
|
|
123
|
-
test_set_name: e.test_set_name,
|
|
124
|
-
status: e.status,
|
|
125
|
-
start_time: e.start_time
|
|
126
|
-
}))
|
|
127
|
-
.reverse();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ─── Queue Stats ───────────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
getQueueStats(): { total: number; queued: number; running: number; done: number; failed: number; cancelled: number; maxConcurrent: number; jobs: { execution_id: string; test_set_name: string; status: string }[] } {
|
|
133
|
-
const all = Array.from(this.executions.values());
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
total: all.length,
|
|
137
|
-
queued: 0,
|
|
138
|
-
running: all.filter((e) => e.status === 'RUNNING').length,
|
|
139
|
-
done: all.filter((e) => e.status === 'PASS').length,
|
|
140
|
-
failed: all.filter((e) => e.status === 'FAIL').length,
|
|
141
|
-
cancelled: 0,
|
|
142
|
-
maxConcurrent: 10,
|
|
143
|
-
jobs: all.map((e) => ({
|
|
144
|
-
execution_id: e.execution_id,
|
|
145
|
-
test_set_name: e.test_set_name,
|
|
146
|
-
status: e.status
|
|
147
|
-
}))
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export const executionStore = new ExecutionStore();
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TestDataStore
|
|
3
|
-
* -------------
|
|
4
|
-
* A simple key-value store shared across all steps of a single script execution.
|
|
5
|
-
* runCustomCode writes values into it; any subsequent step arg that contains
|
|
6
|
-
* testData["key"] has the token replaced with the stored value at call time.
|
|
7
|
-
*/
|
|
8
|
-
export class TestDataStore {
|
|
9
|
-
private store = new Map<string, string>();
|
|
10
|
-
|
|
11
|
-
set(key: string, value: string): void {
|
|
12
|
-
this.store.set(key, String(value));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
get(key: string): string | undefined {
|
|
16
|
-
return this.store.get(key);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
has(key: string): boolean {
|
|
20
|
-
return this.store.has(key);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Resolves a step arg string that may contain testData["key"] tokens.
|
|
25
|
-
* e.g. 'testData["RN_randomEmail"]' → 'abc@xyz.com'
|
|
26
|
-
* Plain strings pass through unchanged.
|
|
27
|
-
*/
|
|
28
|
-
resolve(value: string): string {
|
|
29
|
-
return value.replace(/testData\["([^"]+)"\]/g, (_, key) => {
|
|
30
|
-
const resolved = this.store.get(key);
|
|
31
|
-
if (resolved === undefined) {
|
|
32
|
-
console.warn(`[TestDataStore] Key "${key}" not found in store`);
|
|
33
|
-
return value; // return original token so the error is visible
|
|
34
|
-
}
|
|
35
|
-
return resolved;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getAll(): Record<string, string> {
|
|
40
|
-
return Object.fromEntries(this.store);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
clear(): void {
|
|
44
|
-
this.store.clear();
|
|
45
|
-
}
|
|
46
|
-
}
|