lexxit-automation-framework 2.0.19 → 3.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/README.md +246 -57
- package/npmignore +8 -0
- package/package.json +32 -1
- package/public/dashboard.html +591 -0
- package/src/actions/baseHandler.ts +351 -0
- package/src/actions/browserManager.ts +432 -0
- package/src/actions/checkboxHandler.ts +276 -0
- package/src/actions/clickHandler.ts +513 -0
- package/src/actions/customcodehandler.ts +251 -0
- package/src/actions/dropdownHandler.ts +501 -0
- package/src/actions/radiobuttonHandler.ts +286 -0
- package/src/actions/textHandler.ts +498 -0
- package/src/api/server.ts +210 -0
- package/src/config.ts +7 -0
- package/src/executor/functionMap.ts +153 -0
- package/src/executor/mapping.ts +70 -0
- package/src/executor/scriptExecutor.ts +162 -0
- package/src/executor/stepExecutor.ts +289 -0
- package/src/runner/testRunner.ts +78 -0
- package/src/sse/sseManager.ts +130 -0
- package/src/store/executionStore.ts +152 -0
- package/src/store/testDataStore.ts +46 -0
- package/src/types/types.ts +159 -0
- package/src/utils/healingService.ts +210 -0
- package/src/utils/locatorService.ts +73 -0
- package/src/utils/logger.ts +27 -0
- package/src/utils/metadataService.ts +137 -0
- package/src/utils/waitConditions.ts +141 -0
- package/src/validator/validator.ts +140 -0
- package/tsconfig.json +16 -0
- package/bin/lexxit-automation-framework.js +0 -224
- package/dist-obf/app.js +0 -1
- package/dist-obf/controllers/controller.js +0 -1
- package/dist-obf/core/BrowserManager.js +0 -1
- package/dist-obf/core/PlaywrightEngine.js +0 -1
- package/dist-obf/core/ScreenshotManager.js +0 -1
- package/dist-obf/core/TestData.js +0 -1
- package/dist-obf/core/TestExecutor.js +0 -1
- package/dist-obf/core/handlers/AllHandlers.js +0 -1
- package/dist-obf/core/handlers/BaseHandler.js +0 -1
- package/dist-obf/core/handlers/ClickHandler.js +0 -1
- package/dist-obf/core/handlers/CustomCodeHandler.js +0 -1
- package/dist-obf/core/handlers/DropdownHandler.js +0 -1
- package/dist-obf/core/handlers/InputHandler.js +0 -1
- package/dist-obf/core/registry/ActionRegistry.js +0 -1
- package/dist-obf/installer/frameworkLauncher.js +0 -1
- package/dist-obf/public/dashboard.html +0 -591
- package/dist-obf/public/execution.html +0 -510
- package/dist-obf/public/queue-monitor.html +0 -408
- package/dist-obf/queue/ExecutionQueue.js +0 -1
- package/dist-obf/routes/api.routes.js +0 -1
- package/dist-obf/server.js +0 -1
- package/dist-obf/types/types.js +0 -1
- package/dist-obf/utils/chromefinder.js +0 -1
- package/dist-obf/utils/elementHighlight.js +0 -1
- package/dist-obf/utils/locatorHelper.js +0 -1
- package/dist-obf/utils/logger.js +0 -1
- package/dist-obf/utils/response.js +0 -1
- package/dist-obf/utils/responseFormatter.js +0 -1
- package/dist-obf/utils/sseManager.js +0 -1
- package/scripts/postinstall.js +0 -52
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// ─── Execution State (for live dashboard) ─────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export type ExecutionOverallStatus = 'RUNNING' | 'PASS' | 'FAIL';
|
|
4
|
+
export type StepLiveStatus = 'PENDING' | 'RUNNING' | 'PASS' | 'FAIL' | 'SKIP';
|
|
5
|
+
|
|
6
|
+
export type ScreenshotMode = 'on_failure' | 'always' | 'never';
|
|
7
|
+
|
|
8
|
+
export interface LiveStepState {
|
|
9
|
+
step_name: string;
|
|
10
|
+
status: StepLiveStatus;
|
|
11
|
+
expected_result: string | null;
|
|
12
|
+
comments: string | null;
|
|
13
|
+
duration: number | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LiveScriptState {
|
|
17
|
+
test_script_uid: string;
|
|
18
|
+
test_case_name: string;
|
|
19
|
+
status: ExecutionOverallStatus;
|
|
20
|
+
video_path: string | null;
|
|
21
|
+
steps: LiveStepState[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ExecutionState {
|
|
25
|
+
execution_id: string;
|
|
26
|
+
test_set_name: string;
|
|
27
|
+
status: ExecutionOverallStatus;
|
|
28
|
+
start_time: string;
|
|
29
|
+
end_time: string | null;
|
|
30
|
+
scripts: LiveScriptState[];
|
|
31
|
+
final_result: TestSetResult | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Step Result ──────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export interface StepResult {
|
|
37
|
+
uid: string | null;
|
|
38
|
+
obj_uid: string | null;
|
|
39
|
+
page_uid: string | null;
|
|
40
|
+
step_name: string;
|
|
41
|
+
step_script: string;
|
|
42
|
+
status: 'PASS' | 'FAIL' | 'SKIP';
|
|
43
|
+
expected_result: string;
|
|
44
|
+
comments: string;
|
|
45
|
+
duration: number; // milliseconds
|
|
46
|
+
start_time: string; // ISO string e.g. 2026-06-23T11:25:32.487Z
|
|
47
|
+
screenshot?: string;
|
|
48
|
+
pageSource?: string;
|
|
49
|
+
autoHealed?: boolean;
|
|
50
|
+
failureType?: 'ELEMENT_NOT_FOUND' | 'TIMEOUT' | 'NAVIGATION_TIMEOUT' | 'OTHER';
|
|
51
|
+
comparisonCode?: any;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Failure Metadata for API ─────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export interface FailureMetadata {
|
|
57
|
+
test_script_uid: string;
|
|
58
|
+
test_case_name: string;
|
|
59
|
+
step_name: string;
|
|
60
|
+
step_index: number;
|
|
61
|
+
failure_reason: string;
|
|
62
|
+
screenshot?: string;
|
|
63
|
+
pageSource?: string;
|
|
64
|
+
browser_type: string;
|
|
65
|
+
timestamp: string;
|
|
66
|
+
app_id?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Step Request ─────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export interface StepRequest {
|
|
72
|
+
step_name: string;
|
|
73
|
+
step_script: string;
|
|
74
|
+
label: string;
|
|
75
|
+
locators?: string[];
|
|
76
|
+
obj_uid?: string | null;
|
|
77
|
+
page_uid?: string | null;
|
|
78
|
+
status?: string;
|
|
79
|
+
value?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Test Script Request ──────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export interface TestScriptRequest {
|
|
85
|
+
test_script_uid: string;
|
|
86
|
+
test_case_name: string;
|
|
87
|
+
voice_enabled: boolean;
|
|
88
|
+
app_id: string;
|
|
89
|
+
browser: 'chromium' | 'chrome' | 'firefox' | 'edge';
|
|
90
|
+
headless: boolean;
|
|
91
|
+
screenshot_mode: 'on_failure' | 'always' | 'never';
|
|
92
|
+
stop_on_failure: boolean;
|
|
93
|
+
steps: StepRequest[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Test Set Request ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export interface TestSetRequest {
|
|
99
|
+
test_set_name: string;
|
|
100
|
+
open_dashboard: boolean;
|
|
101
|
+
parallel: boolean;
|
|
102
|
+
stop_on_failure: boolean;
|
|
103
|
+
video_enabled: boolean;
|
|
104
|
+
exec_mode?: ExecMode;
|
|
105
|
+
scripts: TestScriptRequest[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Test Script Result ───────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export interface TestScriptResult {
|
|
111
|
+
test_script_uid: string;
|
|
112
|
+
test_case_name: string;
|
|
113
|
+
app_id: string;
|
|
114
|
+
browser: string;
|
|
115
|
+
status: 'PASS' | 'FAIL';
|
|
116
|
+
results: StepResult[];
|
|
117
|
+
// flat counts — no summary object
|
|
118
|
+
total_steps: number;
|
|
119
|
+
passed_steps: number;
|
|
120
|
+
failed_steps: number;
|
|
121
|
+
skipped_steps: number;
|
|
122
|
+
duration: number; // milliseconds
|
|
123
|
+
start_time: string; // ISO string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Test Set Result ──────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export interface TestSetResult {
|
|
129
|
+
executionId: string;
|
|
130
|
+
status: 'PASS' | 'FAIL';
|
|
131
|
+
total_scripts: number;
|
|
132
|
+
passed: number;
|
|
133
|
+
failed: number;
|
|
134
|
+
duration: number; // milliseconds
|
|
135
|
+
results: TestScriptResult[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ExecMode {
|
|
139
|
+
mode: 'fast' | 'medium' | 'slow';
|
|
140
|
+
delay_ms?: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── SSE ─────────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export type SSEEventType =
|
|
146
|
+
| 'execution_start'
|
|
147
|
+
| 'script_start'
|
|
148
|
+
| 'step_start'
|
|
149
|
+
| 'step_complete'
|
|
150
|
+
| 'script_complete'
|
|
151
|
+
| 'execution_complete'
|
|
152
|
+
| 'log';
|
|
153
|
+
|
|
154
|
+
export interface SSEEvent {
|
|
155
|
+
type: SSEEventType;
|
|
156
|
+
executionId: string;
|
|
157
|
+
timestamp: string;
|
|
158
|
+
data: any;
|
|
159
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { StepResult } from '../types/types';
|
|
2
|
+
import { TRIAGE, COMPARE } from '../config';
|
|
3
|
+
/**
|
|
4
|
+
* Shape of a single healed entry from api/compare response
|
|
5
|
+
*/
|
|
6
|
+
export interface HealedEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
obj_uid: string;
|
|
9
|
+
step_script: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Service for handling advanced auto-healing via triage + compare APIs
|
|
14
|
+
*/
|
|
15
|
+
export class HealingService {
|
|
16
|
+
|
|
17
|
+
private triageApiUrl = TRIAGE;
|
|
18
|
+
private compareApiUrl = COMPARE;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Classify failure type from error message
|
|
22
|
+
*/
|
|
23
|
+
static classifyFailure(comments: string, result?: any): string {
|
|
24
|
+
const msg = comments.toLowerCase();
|
|
25
|
+
|
|
26
|
+
if (msg.includes('element not found') || msg.includes('not visible')) {
|
|
27
|
+
return 'ELEMENT_NOT_FOUND';
|
|
28
|
+
}
|
|
29
|
+
if (msg.includes('timeout') && msg.includes('navigation')) {
|
|
30
|
+
return 'NAVIGATION_TIMEOUT';
|
|
31
|
+
}
|
|
32
|
+
if (msg.includes('timeout')) {
|
|
33
|
+
return 'TIMEOUT';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return 'OTHER';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Call triage API to determine if auto-healing should be attempted
|
|
41
|
+
*/
|
|
42
|
+
async callTriageAPI(triagePayload: any): Promise<{ callCompareScript: boolean; rootCause?: string; confidence?: number; reason?: string }> {
|
|
43
|
+
try {
|
|
44
|
+
console.log(`🩺 Calling triage API at ${this.triageApiUrl}...`);
|
|
45
|
+
const response = await fetch(this.triageApiUrl, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
body: JSON.stringify(triagePayload)
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
console.warn(`⚠️ Triage API returned ${response.status} — skipping compare`);
|
|
53
|
+
return { callCompareScript: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = await response.json();
|
|
57
|
+
console.log(`🩺 Triage result:`, JSON.stringify(result));
|
|
58
|
+
console.log(` Root cause: ${result?.root_cause} | Confidence: ${result?.confidence} | Call compare: ${result?.call_compare_script}`);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
callCompareScript: result?.call_compare_script === true,
|
|
62
|
+
rootCause: result?.root_cause,
|
|
63
|
+
confidence: result?.confidence,
|
|
64
|
+
reason: result?.reason
|
|
65
|
+
};
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
console.error(`❌ Triage API call failed: ${error.message} — skipping compare`);
|
|
68
|
+
return { callCompareScript: false };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Call compare API to get healed selectors/scripts
|
|
74
|
+
*/
|
|
75
|
+
async callCompareAPI(comparePayload: any): Promise<any> {
|
|
76
|
+
try {
|
|
77
|
+
console.log(`📡 Calling compare API at ${this.compareApiUrl}...`);
|
|
78
|
+
const response = await fetch(this.compareApiUrl, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
body: JSON.stringify(comparePayload)
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
console.warn(`⚠️ Compare API returned ${response.status}`);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = await response.json();
|
|
90
|
+
console.log(`📦 Compare result:`, JSON.stringify(result));
|
|
91
|
+
|
|
92
|
+
const healedChanged: HealedEntry[] = result?.diff_report?.self_healing?.healed_changed ?? [];
|
|
93
|
+
const healedRemoved: HealedEntry[] = result?.diff_report?.self_healing?.healed_removed ?? [];
|
|
94
|
+
const allHealed: HealedEntry[] = [...healedChanged, ...healedRemoved];
|
|
95
|
+
|
|
96
|
+
console.log(`🔬 healed_changed: ${healedChanged.length} | healed_removed: ${healedRemoved.length} | total: ${allHealed.length}`);
|
|
97
|
+
|
|
98
|
+
if (allHealed.length > 0) {
|
|
99
|
+
console.log(`🗃️ Found ${allHealed.length} healed entr${allHealed.length === 1 ? 'y' : 'ies'}: [${allHealed.map(e => e.name).join(', ')}]`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
console.error(`❌ Compare API call failed: ${error.message}`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build triage payload from execution context
|
|
111
|
+
*/
|
|
112
|
+
static buildTriagePayload(
|
|
113
|
+
stepResults: StepResult[],
|
|
114
|
+
currentStep: any,
|
|
115
|
+
stepIndex: number,
|
|
116
|
+
scriptUid: string,
|
|
117
|
+
appId?: string
|
|
118
|
+
): any {
|
|
119
|
+
const comments = currentStep.comments || 'Unknown error';
|
|
120
|
+
const failureType = HealingService.classifyFailure(comments);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
status: 'fail',
|
|
124
|
+
app_id: appId,
|
|
125
|
+
total_scripts: 1,
|
|
126
|
+
results: [
|
|
127
|
+
{
|
|
128
|
+
test_script_uid: scriptUid,
|
|
129
|
+
app_id: appId,
|
|
130
|
+
status: 'fail',
|
|
131
|
+
results: [
|
|
132
|
+
// All completed steps
|
|
133
|
+
...stepResults.map((r, idx) => ({
|
|
134
|
+
uid: r.uid ?? null,
|
|
135
|
+
obj_uid: r.obj_uid ?? null,
|
|
136
|
+
step_name: r.step_name,
|
|
137
|
+
step_script: r.expected_result || '',
|
|
138
|
+
page_uid: r.page_uid ?? null,
|
|
139
|
+
comments: r.comments,
|
|
140
|
+
screenshot: r.screenshot ?? '',
|
|
141
|
+
duration: r.duration,
|
|
142
|
+
start_time: r.start_time.replace('T', ' ').substring(0, 19),
|
|
143
|
+
expected_result: r.expected_result ?? '',
|
|
144
|
+
status: r.status.toLowerCase()
|
|
145
|
+
})),
|
|
146
|
+
// Current failing step
|
|
147
|
+
{
|
|
148
|
+
uid: currentStep.uid ?? null,
|
|
149
|
+
obj_uid: currentStep.obj_uid ?? null,
|
|
150
|
+
step_name: currentStep.step_name,
|
|
151
|
+
step_script: currentStep.step_script,
|
|
152
|
+
page_uid: currentStep.page_uid ?? null,
|
|
153
|
+
comments: comments,
|
|
154
|
+
screenshot: currentStep.screenshot ?? '',
|
|
155
|
+
duration: currentStep.duration || '0 seconds',
|
|
156
|
+
start_time: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
|
157
|
+
expected_result: currentStep.expected_result ?? '',
|
|
158
|
+
status: 'fail',
|
|
159
|
+
data: {
|
|
160
|
+
failure_type: failureType,
|
|
161
|
+
error: comments
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build compare payload from page context
|
|
172
|
+
*/
|
|
173
|
+
static buildComparePayload(
|
|
174
|
+
appId: string,
|
|
175
|
+
pageUid: string,
|
|
176
|
+
objUid: string,
|
|
177
|
+
pageSource: string,
|
|
178
|
+
currentUrl: string,
|
|
179
|
+
stepIndex: number
|
|
180
|
+
): any {
|
|
181
|
+
return {
|
|
182
|
+
app_id: appId,
|
|
183
|
+
page_uid: pageUid,
|
|
184
|
+
obj_uid: objUid,
|
|
185
|
+
pgContent: pageSource,
|
|
186
|
+
sub_url: currentUrl,
|
|
187
|
+
mode: 'html',
|
|
188
|
+
failed_step_index: stepIndex
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Replace $variable placeholders with actual values
|
|
194
|
+
*/
|
|
195
|
+
static replaceVariables(healedScript: string, stepValue?: string): string {
|
|
196
|
+
if (!healedScript.includes("'$")) {
|
|
197
|
+
return healedScript;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (stepValue !== undefined && stepValue !== null && stepValue !== '') {
|
|
201
|
+
const replaced = healedScript.replace(/'(\$[^']+)'/, `'${stepValue}'`);
|
|
202
|
+
console.log(`🔧 $variable replaced with value: "${stepValue}"`);
|
|
203
|
+
console.log(`📝 Final healed script: ${replaced}`);
|
|
204
|
+
return replaced;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log(`⚠️ Step value is empty — $variable left unreplaced`);
|
|
208
|
+
return healedScript;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Page, Locator } from 'playwright';
|
|
2
|
+
|
|
3
|
+
export interface ResolvedElement {
|
|
4
|
+
element: Locator;
|
|
5
|
+
matchedIndex: number;
|
|
6
|
+
matchedXPath: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class LocatorService {
|
|
10
|
+
/**
|
|
11
|
+
* Resolves an element using a list of xpath locators.
|
|
12
|
+
* Combines all xpaths into a single Playwright locator using OR-combined xpath expressions,
|
|
13
|
+
* waits for the first visible + attached match, then determines which specific
|
|
14
|
+
* locator (by position) actually matched.
|
|
15
|
+
*/
|
|
16
|
+
static async resolveElement(page: Page, locators: string[], timeout: number = 10000): Promise<ResolvedElement> {
|
|
17
|
+
if (!locators || locators.length === 0) {
|
|
18
|
+
throw new Error('No locators provided');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const xpathLocators = locators.filter(l => l.trim().startsWith('/') || l.trim().startsWith('('));
|
|
22
|
+
const combinedXPath = xpathLocators.join(' | ');
|
|
23
|
+
const element = page.locator(`xpath=${combinedXPath}`).first();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await element.waitFor({ state: 'visible', timeout });
|
|
27
|
+
} catch (error: any) {
|
|
28
|
+
throw new Error(`Element not found or not visible using locators: [${locators.join(', ')}] - ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const matchedIndex = await this.findMatchedIndex(page, element, xpathLocators);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
element,
|
|
35
|
+
matchedIndex,
|
|
36
|
+
matchedXPath: matchedIndex >= 0 ? xpathLocators[matchedIndex] : combinedXPath
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Determines which specific locator (by position) matches the resolved element,
|
|
42
|
+
* by checking each individual xpath separately and comparing element identity.
|
|
43
|
+
*/
|
|
44
|
+
private static async findMatchedIndex(page: Page, resolvedElement: Locator, locators: string[]): Promise<number> {
|
|
45
|
+
try {
|
|
46
|
+
const resolvedHandle = await resolvedElement.elementHandle();
|
|
47
|
+
if (!resolvedHandle) return -1;
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < locators.length; i++) {
|
|
50
|
+
const candidate = page.locator(`xpath=${locators[i]}`).first();
|
|
51
|
+
const count = await candidate.count().catch(() => 0);
|
|
52
|
+
|
|
53
|
+
if (count === 0) continue;
|
|
54
|
+
|
|
55
|
+
const candidateHandle = await candidate.elementHandle().catch(() => null);
|
|
56
|
+
if (!candidateHandle) continue;
|
|
57
|
+
|
|
58
|
+
const isSameElement = await page.evaluate(
|
|
59
|
+
({ a, b }) => a === b,
|
|
60
|
+
{ a: resolvedHandle, b: candidateHandle }
|
|
61
|
+
).catch(() => false);
|
|
62
|
+
|
|
63
|
+
if (isSameElement) {
|
|
64
|
+
return i;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return -1;
|
|
69
|
+
} catch {
|
|
70
|
+
return -1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class Logger {
|
|
2
|
+
private context: string;
|
|
3
|
+
|
|
4
|
+
private constructor(context: string) {
|
|
5
|
+
this.context = context;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
static create(context: string): Logger {
|
|
9
|
+
return new Logger(context);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
info(msg: string): void {
|
|
13
|
+
console.log(`[${this.timestamp()}] [INFO] [${this.context}] ${msg}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
error(msg: string): void {
|
|
17
|
+
console.error(`[${this.timestamp()}] [ERROR] [${this.context}] ${msg}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
warn(msg: string): void {
|
|
21
|
+
console.warn(`[${this.timestamp()}] [WARN] [${this.context}] ${msg}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private timestamp(): string {
|
|
25
|
+
return new Date().toISOString().replace('T', ' ').split('.')[0];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { FailureMetadata, StepResult } from '../types/types';
|
|
2
|
+
|
|
3
|
+
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:5501';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Service for handling failure metadata collection and transmission to API
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class MetadataService {
|
|
10
|
+
/**
|
|
11
|
+
* Capture page source from Playwright page
|
|
12
|
+
*/
|
|
13
|
+
static async capturePageSource(page: any): Promise<string | undefined> {
|
|
14
|
+
try {
|
|
15
|
+
const content = await page.content();
|
|
16
|
+
return content;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.warn('Failed to capture page source:', error);
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
static buildFailureMetadata(
|
|
25
|
+
testScriptUid: string,
|
|
26
|
+
testCaseName: string,
|
|
27
|
+
stepIndex: number,
|
|
28
|
+
step: StepResult,
|
|
29
|
+
browserType: string,
|
|
30
|
+
appId?: string
|
|
31
|
+
): FailureMetadata {
|
|
32
|
+
return {
|
|
33
|
+
test_script_uid: testScriptUid,
|
|
34
|
+
test_case_name: testCaseName,
|
|
35
|
+
step_name: step.step_name,
|
|
36
|
+
step_index: stepIndex,
|
|
37
|
+
failure_reason: step.comments,
|
|
38
|
+
screenshot: step.screenshot,
|
|
39
|
+
pageSource: step.pageSource,
|
|
40
|
+
browser_type: browserType,
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
app_id: appId
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Send failure metadata to API for analysis and auto-healing
|
|
48
|
+
*/
|
|
49
|
+
static async sendFailureMetadata(metadata: FailureMetadata): Promise<{ autoHealSuggestion?: string; success: boolean }> {
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(`${API_BASE_URL}/api/failure-metadata`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify(metadata)
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
console.warn(`Failed to send metadata: ${response.statusText}`);
|
|
59
|
+
return { success: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
return { autoHealSuggestion: data.autoHealSuggestion, success: true };
|
|
64
|
+
} catch (error: any) {
|
|
65
|
+
console.warn('Error sending failure metadata:', error.message);
|
|
66
|
+
return { success: false };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Apply auto-healing suggestion if available
|
|
72
|
+
* This could involve retrying with adjusted selectors, different wait conditions, etc.
|
|
73
|
+
*/
|
|
74
|
+
static async applyAutoHealing(
|
|
75
|
+
suggestion: string,
|
|
76
|
+
originalStep: any,
|
|
77
|
+
page: any
|
|
78
|
+
): Promise<boolean> {
|
|
79
|
+
try {
|
|
80
|
+
// Parse and apply healing suggestion
|
|
81
|
+
// Example: suggestion could be "{\"action\": \"retry\", \"adjustedSelector\": \"...\"}"
|
|
82
|
+
const healingData = JSON.parse(suggestion);
|
|
83
|
+
|
|
84
|
+
if (healingData.action === 'retry') {
|
|
85
|
+
console.log('Auto-healing: Retrying step with adjusted selector');
|
|
86
|
+
// Implementation would depend on step type
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (healingData.action === 'navigate') {
|
|
91
|
+
console.log('Auto-healing: Navigating to URL:', healingData.url);
|
|
92
|
+
await page.goto(healingData.url, { waitUntil: 'networkidle' });
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
console.warn('Failed to apply auto-healing:', error.message);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
static async processScriptFailure(
|
|
105
|
+
results: StepResult[],
|
|
106
|
+
testScriptUid: string,
|
|
107
|
+
testCaseName: string,
|
|
108
|
+
browserType: string,
|
|
109
|
+
appId?: string
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const failedSteps = results.filter((r) => r.status === 'FAIL');
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < failedSteps.length; i++) {
|
|
114
|
+
const step = failedSteps[i];
|
|
115
|
+
const stepIndex = results.indexOf(step);
|
|
116
|
+
|
|
117
|
+
const metadata = this.buildFailureMetadata(
|
|
118
|
+
testScriptUid,
|
|
119
|
+
testCaseName,
|
|
120
|
+
stepIndex,
|
|
121
|
+
step,
|
|
122
|
+
browserType,
|
|
123
|
+
appId
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const response = await this.sendFailureMetadata(metadata);
|
|
127
|
+
|
|
128
|
+
if (response.success) {
|
|
129
|
+
console.log(`Failure metadata sent for step: ${step.step_name}`);
|
|
130
|
+
if (response.autoHealSuggestion) {
|
|
131
|
+
console.log('Auto-healing suggestion received:', response.autoHealSuggestion);
|
|
132
|
+
// Auto-healing would be applied during re-execution
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|