viewgate-mcp 1.0.61 → 1.0.63
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/index.js +464 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -81,7 +81,11 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
81
81
|
return "sync";
|
|
82
82
|
case "get_synced_endpoints":
|
|
83
83
|
case "get_ai_resolved_tickets":
|
|
84
|
+
case "get_e2e_flows":
|
|
85
|
+
case "export_e2e_flow":
|
|
84
86
|
return "idle";
|
|
87
|
+
case "process_e2e_events":
|
|
88
|
+
return "e2e";
|
|
85
89
|
default:
|
|
86
90
|
return "idle";
|
|
87
91
|
}
|
|
@@ -126,6 +130,10 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
126
130
|
guard.flow = "sync";
|
|
127
131
|
guard.step = 1;
|
|
128
132
|
break;
|
|
133
|
+
case "process_e2e_events":
|
|
134
|
+
guard.flow = "e2e";
|
|
135
|
+
guard.step = 1;
|
|
136
|
+
break;
|
|
129
137
|
default:
|
|
130
138
|
if (guard.flow === "idle") {
|
|
131
139
|
throw new Error(`TOOL_CALL_BLOCKED: tool '${toolName}' not allowed in idle. Please start a workflow with 'get_annotations', 'get_ui_components', or 'get_ui_improvements'.`);
|
|
@@ -148,10 +156,8 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
148
156
|
}
|
|
149
157
|
else if (toolName === "generate_ui_components") {
|
|
150
158
|
guard.step = 2;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Allow finishing if it was started in this flow
|
|
154
|
-
resetGuard();
|
|
159
|
+
// Step 3 is now terminal, but we reset after success in the handler
|
|
160
|
+
guard.step = 3;
|
|
155
161
|
}
|
|
156
162
|
else {
|
|
157
163
|
throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
|
|
@@ -165,8 +171,8 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
165
171
|
else if (toolName === "mark_annotations_as_live") {
|
|
166
172
|
if (guard.step !== 1)
|
|
167
173
|
throw new Error("TOOL_CALL_BLOCKED: unexpected step");
|
|
168
|
-
//
|
|
169
|
-
|
|
174
|
+
// Reset moved to success handler
|
|
175
|
+
guard.step = 2;
|
|
170
176
|
}
|
|
171
177
|
else {
|
|
172
178
|
throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
|
|
@@ -179,7 +185,7 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
179
185
|
else if (toolName === "mark_annotations_as_live") {
|
|
180
186
|
if (guard.step !== 1)
|
|
181
187
|
throw new Error("TOOL_CALL_BLOCKED: unexpected step");
|
|
182
|
-
|
|
188
|
+
guard.step = 2;
|
|
183
189
|
}
|
|
184
190
|
else {
|
|
185
191
|
throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
|
|
@@ -198,7 +204,8 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
198
204
|
else {
|
|
199
205
|
if (guard.step !== 1)
|
|
200
206
|
throw new Error("TOOL_CALL_BLOCKED: unexpected step");
|
|
201
|
-
|
|
207
|
+
// Reset moved to success handler
|
|
208
|
+
guard.step = 2;
|
|
202
209
|
}
|
|
203
210
|
}
|
|
204
211
|
else if (guard.flow === "sync") {
|
|
@@ -215,6 +222,14 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
215
222
|
throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active flow");
|
|
216
223
|
}
|
|
217
224
|
}
|
|
225
|
+
else if (guard.flow === "e2e") {
|
|
226
|
+
if (toolName === "process_e2e_events") {
|
|
227
|
+
guard.step = 1;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
throw new Error("TOOL_CALL_BLOCKED: tool not allowed in active e2e flow");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
218
233
|
guard.lastTool = toolName;
|
|
219
234
|
guard.lastActivityAt = now;
|
|
220
235
|
}
|
|
@@ -369,6 +384,41 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
369
384
|
name: "get_ai_resolved_tickets",
|
|
370
385
|
description: "Retrieves the list of ticket IDs that have already been resolved by the AI in the current sprint.",
|
|
371
386
|
inputSchema: { type: "object", properties: {} },
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "get_e2e_flows",
|
|
390
|
+
description: "Fetch all E2E test flows for a project. Returns generated test flows from user interactions.",
|
|
391
|
+
inputSchema: {
|
|
392
|
+
type: "object",
|
|
393
|
+
properties: {
|
|
394
|
+
projectId: { type: "string", description: "Project ID to filter flows." },
|
|
395
|
+
status: { type: "string", description: "Filter by status (active, degraded, needs_review, archived).", default: "active" }
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "process_e2e_events",
|
|
401
|
+
description: "Process raw DOM events and generate a Test Flow. Transforms captured user interactions into structured E2E tests.",
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: "object",
|
|
404
|
+
properties: {
|
|
405
|
+
sessionId: { type: "string", description: "Session ID containing the events to process." },
|
|
406
|
+
flowName: { type: "string", description: "Suggested name for the flow (e.g., 'Login Flow')." },
|
|
407
|
+
projectId: { type: "string", description: "Project ID to associate with the flow." }
|
|
408
|
+
},
|
|
409
|
+
required: ["sessionId", "flowName"]
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "export_e2e_flow",
|
|
414
|
+
description: "Export a test flow as Playwright code. Generates executable TypeScript test code.",
|
|
415
|
+
inputSchema: {
|
|
416
|
+
type: "object",
|
|
417
|
+
properties: {
|
|
418
|
+
flowId: { type: "string", description: "ID of the flow to export." }
|
|
419
|
+
},
|
|
420
|
+
required: ["flowId"]
|
|
421
|
+
}
|
|
372
422
|
}
|
|
373
423
|
],
|
|
374
424
|
};
|
|
@@ -398,6 +448,7 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
398
448
|
const errorBody = await response.text();
|
|
399
449
|
throw new Error(`Backend responded with ${response.status}: ${errorBody}`);
|
|
400
450
|
}
|
|
451
|
+
resetGuard();
|
|
401
452
|
const data = (await response.json());
|
|
402
453
|
const styleHandler = data?.settings?.styleHandler || 'unknown';
|
|
403
454
|
const language = data?.settings?.language || 'en';
|
|
@@ -489,6 +540,7 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
489
540
|
'MUST use Tailwind standard "primary" class (e.g. bg-primary, text-primary-foreground) as these are dynamically injected in the dashboard.',
|
|
490
541
|
'The theme prop MUST be optional and typed in the component interface.',
|
|
491
542
|
'Micro-animations MUST be implemented with Framer Motion (use whileHover, whileTap, etc.)',
|
|
543
|
+
'AESTHETIC PERFECTION: The generated component MUST be an EXACT visual match of the source design. Pay extreme attention to spacing, shadows, and subtle gradients.',
|
|
492
544
|
]
|
|
493
545
|
},
|
|
494
546
|
constraints: {
|
|
@@ -501,7 +553,9 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
501
553
|
mustBeProductionReady: true,
|
|
502
554
|
mustUseFramerMotionForAnimations: true,
|
|
503
555
|
themingIsMandatory: true,
|
|
504
|
-
stylingHandlerIsStrict: true
|
|
556
|
+
stylingHandlerIsStrict: true,
|
|
557
|
+
mustBeAestheticallyIdentical: true,
|
|
558
|
+
visualFidelityInstruction: "You MUST ensure 100% visual parity. The output component must be PREMIUM and indistinguishable from the provided Figma design in terms of layout, spacing, and styling tokens."
|
|
505
559
|
}
|
|
506
560
|
};
|
|
507
561
|
results.push({
|
|
@@ -528,6 +582,7 @@ function createMcpServer(apiKey, personalKey) {
|
|
|
528
582
|
2) REGISTRAR LA PREVIEW usando 'mark_ui_component_generated'.
|
|
529
583
|
|
|
530
584
|
REQUISITO TECNOLÓGICO OBLIGATORIO: Usar ${styleLabel} para TODOS los estilos.
|
|
585
|
+
REQUISITO DE FIDELIDAD ESTÉTICA: El componente debe ser VISUALMENTE IDÉNTICO al original. Respeta cada píxel de espaciado, sombras y pesos de fuente.
|
|
531
586
|
REQUISITO DE THEMING: DEBES respetar las themingTokens. NUNCA hardcodees colores (#hex, etc). Usa SIEMPRE la prop 'theme' o el fallback 'var(--vg-*)'.
|
|
532
587
|
NOTA: Si styleHandler es '${styleHandler}', NO utilices otra tecnología distinta aunque los archivos originales (Figma HTML/CSS) digan lo contrario.`,
|
|
533
588
|
results
|
|
@@ -710,6 +765,7 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
|
|
|
710
765
|
});
|
|
711
766
|
if (!response.ok)
|
|
712
767
|
throw new Error(`Backend responded with ${response.status}`);
|
|
768
|
+
resetGuard();
|
|
713
769
|
const data = (await response.json());
|
|
714
770
|
return {
|
|
715
771
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
@@ -730,6 +786,9 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
|
|
|
730
786
|
});
|
|
731
787
|
if (!response.ok)
|
|
732
788
|
throw new Error(`Backend responded with ${response.status}`);
|
|
789
|
+
if (args.results) {
|
|
790
|
+
resetGuard();
|
|
791
|
+
}
|
|
733
792
|
const data = (await response.json());
|
|
734
793
|
// Point: Version Storing
|
|
735
794
|
if (data.backlogVersion)
|
|
@@ -829,6 +888,114 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
|
|
|
829
888
|
: (data.preferredLanguage === 'en' ? "\n*** [INSTRUCTION: Provide all comments and analysis in English.] ***\n\n\n\n" : "");
|
|
830
889
|
return { content: [{ type: "text", text: langHint + JSON.stringify(data, null, 2) }] };
|
|
831
890
|
}
|
|
891
|
+
case "get_e2e_flows": {
|
|
892
|
+
const args = argsAny;
|
|
893
|
+
const fetchUrl = new URL(`${BACKEND_URL}/api/e2e/flows`);
|
|
894
|
+
if (args.projectId)
|
|
895
|
+
fetchUrl.searchParams.append("projectId", args.projectId);
|
|
896
|
+
if (args.status)
|
|
897
|
+
fetchUrl.searchParams.append("status", args.status);
|
|
898
|
+
const response = await fetch(fetchUrl, {
|
|
899
|
+
headers: { 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
|
|
900
|
+
});
|
|
901
|
+
if (!response.ok)
|
|
902
|
+
throw new Error(`Backend responded with ${response.status}`);
|
|
903
|
+
const data = (await response.json());
|
|
904
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
905
|
+
}
|
|
906
|
+
case "process_e2e_events": {
|
|
907
|
+
const args = argsAny;
|
|
908
|
+
// Fetch UNPROCESSED events for the session (only new events)
|
|
909
|
+
const eventsResponse = await fetch(`${BACKEND_URL}/api/e2e/sessions/${args.sessionId}/events?unprocessed=true&limit=${args.limit || 50}`, {
|
|
910
|
+
headers: { 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
|
|
911
|
+
});
|
|
912
|
+
if (!eventsResponse.ok)
|
|
913
|
+
throw new Error(`Failed to fetch events: ${eventsResponse.status}`);
|
|
914
|
+
const { events, totalUnprocessed } = await eventsResponse.json();
|
|
915
|
+
if (!events || events.length === 0) {
|
|
916
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
917
|
+
message: "No new events to process - all events already in flow",
|
|
918
|
+
sessionId: args.sessionId,
|
|
919
|
+
processed: true
|
|
920
|
+
}, null, 2) }] };
|
|
921
|
+
}
|
|
922
|
+
console.log(`[MCP E2E] Processing ${events.length} unprocessed events (${totalUnprocessed} total pending)`);
|
|
923
|
+
const workspaceId = args.workspaceId || events[0]?.workspace?.toString() || '';
|
|
924
|
+
const projectId = args.projectId || events[0]?.project?.toString() || '';
|
|
925
|
+
if (!workspaceId || !projectId) {
|
|
926
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Could not determine workspace/project ID" }, null, 2) }] };
|
|
927
|
+
}
|
|
928
|
+
// Transform ONLY new events to steps
|
|
929
|
+
const newSteps = events.map(event => transformSingleEventToStep(event));
|
|
930
|
+
// Update flow with just the new steps
|
|
931
|
+
const updateResponse = await fetch(`${BACKEND_URL}/api/e2e/flows`, {
|
|
932
|
+
method: 'POST',
|
|
933
|
+
headers: {
|
|
934
|
+
'Content-Type': 'application/json',
|
|
935
|
+
'x-api-key': apiKey,
|
|
936
|
+
'x-internal-key': process.env.INTERNAL_API_KEY || ''
|
|
937
|
+
},
|
|
938
|
+
body: JSON.stringify({
|
|
939
|
+
workspaceId,
|
|
940
|
+
projectId,
|
|
941
|
+
name: args.flowName || `E2E Flow - ${new Date().toLocaleString()}`,
|
|
942
|
+
steps: newSteps,
|
|
943
|
+
metadata: {
|
|
944
|
+
processedAt: new Date().toISOString(),
|
|
945
|
+
eventsProcessed: events.length,
|
|
946
|
+
mcpGenerated: true
|
|
947
|
+
},
|
|
948
|
+
sessionId: args.sessionId,
|
|
949
|
+
markAsProcessed: true // Tell backend to mark these events as processed
|
|
950
|
+
})
|
|
951
|
+
});
|
|
952
|
+
if (!updateResponse.ok) {
|
|
953
|
+
const error = await updateResponse.text();
|
|
954
|
+
throw new Error(`Failed to update flow: ${error}`);
|
|
955
|
+
}
|
|
956
|
+
const result = (await updateResponse.json());
|
|
957
|
+
resetGuard();
|
|
958
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
959
|
+
success: true,
|
|
960
|
+
message: `Added ${newSteps.length} new steps to E2E flow (processed ${events.length} events)`,
|
|
961
|
+
flow: result.flow,
|
|
962
|
+
newStepsCount: newSteps.length,
|
|
963
|
+
remainingUnprocessed: Math.max(0, totalUnprocessed - events.length)
|
|
964
|
+
}, null, 2) }] };
|
|
965
|
+
}
|
|
966
|
+
case "export_e2e_flow": {
|
|
967
|
+
const args = argsAny;
|
|
968
|
+
// Fetch project settings to get default testing framework if not specified
|
|
969
|
+
let framework = args.testingFramework;
|
|
970
|
+
if (!framework) {
|
|
971
|
+
try {
|
|
972
|
+
const projectResponse = await fetch(`${BACKEND_URL}/api/projects/current`, {
|
|
973
|
+
headers: { 'x-api-key': apiKey, 'x-mcp-tool-name': toolName, ...(personalKey ? { 'x-personal-key': personalKey } : {}) }
|
|
974
|
+
});
|
|
975
|
+
if (projectResponse.ok) {
|
|
976
|
+
const projectData = await projectResponse.json();
|
|
977
|
+
framework = projectData.project?.settings?.testingFramework || 'playwright';
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
catch (e) {
|
|
981
|
+
framework = 'playwright';
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const response = await fetch(`${BACKEND_URL}/api/e2e/flows/${args.flowId}/export`, {
|
|
985
|
+
method: 'POST',
|
|
986
|
+
headers: {
|
|
987
|
+
'Content-Type': 'application/json',
|
|
988
|
+
'x-api-key': apiKey,
|
|
989
|
+
'x-mcp-tool-name': toolName,
|
|
990
|
+
...(personalKey ? { 'x-personal-key': personalKey } : {})
|
|
991
|
+
},
|
|
992
|
+
body: JSON.stringify({ testingFramework: framework })
|
|
993
|
+
});
|
|
994
|
+
if (!response.ok)
|
|
995
|
+
throw new Error(`Backend responded with ${response.status}`);
|
|
996
|
+
const data = (await response.json());
|
|
997
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...data, testingFramework: framework }, null, 2) }] };
|
|
998
|
+
}
|
|
832
999
|
default:
|
|
833
1000
|
throw new Error("Unknown tool");
|
|
834
1001
|
}
|
|
@@ -840,6 +1007,294 @@ Lang: ${rawData.preferredLanguage === 'es' ? 'ES' : 'EN'}
|
|
|
840
1007
|
});
|
|
841
1008
|
return server;
|
|
842
1009
|
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Transform raw DOM events into a structured Test Flow (IR)
|
|
1012
|
+
* Following the priority: data-testid > getByRole > getByLabel > text > CSS > XPath
|
|
1013
|
+
*/
|
|
1014
|
+
function transformEventsToTestFlow(events, flowName, workspaceId) {
|
|
1015
|
+
const steps = [];
|
|
1016
|
+
let lastUrl = '';
|
|
1017
|
+
for (const event of events) {
|
|
1018
|
+
const step = transformEventToStep(event, lastUrl);
|
|
1019
|
+
if (step) {
|
|
1020
|
+
steps.push(step);
|
|
1021
|
+
if (event.type === 'navigation' || step.type === 'goto') {
|
|
1022
|
+
lastUrl = event.url;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
// Add automatic assertions for state changes
|
|
1027
|
+
const stepsWithAsserts = addImplicitAssertions(steps);
|
|
1028
|
+
// Calculate confidence based on selector quality
|
|
1029
|
+
const confidence = calculateFlowConfidence(stepsWithAsserts);
|
|
1030
|
+
return {
|
|
1031
|
+
name: flowName,
|
|
1032
|
+
workspaceId: workspaceId,
|
|
1033
|
+
steps: stepsWithAsserts,
|
|
1034
|
+
metadata: {
|
|
1035
|
+
createdAt: new Date().toISOString(),
|
|
1036
|
+
updatedAt: new Date().toISOString(),
|
|
1037
|
+
confidence,
|
|
1038
|
+
urlPattern: extractUrlPattern(events.map(e => e.url)),
|
|
1039
|
+
eventCount: events.length
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Infer step type from user description using keyword analysis
|
|
1045
|
+
* Supports English and Spanish keywords
|
|
1046
|
+
*/
|
|
1047
|
+
function inferStepTypeFromDescription(description) {
|
|
1048
|
+
if (!description)
|
|
1049
|
+
return null;
|
|
1050
|
+
const desc = description.toLowerCase();
|
|
1051
|
+
// Assertion keywords (verify, validate, check, assert)
|
|
1052
|
+
if (/\b(verificar|validar|comprobar|asegurar|chequear|confirmar|assert|verify|validate|check|ensure|see|expect|should|must)\b/.test(desc)) {
|
|
1053
|
+
if (/\b(texto|text|contiene|contains|dice|says|muestra|shows|display)\b/.test(desc)) {
|
|
1054
|
+
return 'assertText';
|
|
1055
|
+
}
|
|
1056
|
+
return 'assertVisible';
|
|
1057
|
+
}
|
|
1058
|
+
// Fill/Type keywords
|
|
1059
|
+
if (/\b(escribir|type|rellenar|fill|ingresar|enter|input|escribe|completar)\b/.test(desc)) {
|
|
1060
|
+
return 'fill';
|
|
1061
|
+
}
|
|
1062
|
+
// Navigation keywords
|
|
1063
|
+
if (/\b(navegar|navega|ir a|go to|acceder|accede|entrar|entra|visit|open|abrir)\b/.test(desc)) {
|
|
1064
|
+
return 'goto';
|
|
1065
|
+
}
|
|
1066
|
+
// Wait keywords
|
|
1067
|
+
if (/\b(esperar|wait|pausa|pause|delay|sleep|detener|stop)\b/.test(desc)) {
|
|
1068
|
+
return 'wait';
|
|
1069
|
+
}
|
|
1070
|
+
// Select keywords
|
|
1071
|
+
if (/\b(seleccionar|select|selecciona|elegir|choose|dropdown|opción|option)\b/.test(desc)) {
|
|
1072
|
+
return 'select';
|
|
1073
|
+
}
|
|
1074
|
+
// Press key keywords
|
|
1075
|
+
if (/\b(presionar|press|presiona|tecla|key|enter|escape|tab|enviar|submit)\b/.test(desc)) {
|
|
1076
|
+
return 'press';
|
|
1077
|
+
}
|
|
1078
|
+
// Hover keywords
|
|
1079
|
+
if (/\b(hover|pasar|sobre|encima|mouse over|cursor)\b/.test(desc)) {
|
|
1080
|
+
return 'assertVisible'; // Hover usually implies checking visibility
|
|
1081
|
+
}
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
function transformEventToStep(event, lastUrl) {
|
|
1085
|
+
const { type, element, value, url } = event;
|
|
1086
|
+
const userDescription = event.description || '';
|
|
1087
|
+
// Handle navigation
|
|
1088
|
+
if (type === 'navigation' || url !== lastUrl) {
|
|
1089
|
+
return {
|
|
1090
|
+
type: 'goto',
|
|
1091
|
+
selector: url,
|
|
1092
|
+
selectorType: 'css',
|
|
1093
|
+
value: url,
|
|
1094
|
+
description: userDescription || `Navigate to ${url}`,
|
|
1095
|
+
confidence: 1.0,
|
|
1096
|
+
originalEvent: event
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
// Generate selector following priority order
|
|
1100
|
+
const selectorInfo = generateBestSelector(element);
|
|
1101
|
+
// Try to infer step type from user description
|
|
1102
|
+
const inferredType = inferStepTypeFromDescription(userDescription);
|
|
1103
|
+
// Log the inference for debugging
|
|
1104
|
+
if (inferredType && userDescription) {
|
|
1105
|
+
console.log(`[MCP E2E] Inferred step type "${inferredType}" from description: "${userDescription}"`);
|
|
1106
|
+
}
|
|
1107
|
+
// Use inferred type or fall back to event type
|
|
1108
|
+
const effectiveType = inferredType || type;
|
|
1109
|
+
switch (effectiveType) {
|
|
1110
|
+
case 'click':
|
|
1111
|
+
return {
|
|
1112
|
+
type: 'click',
|
|
1113
|
+
selector: selectorInfo.selector,
|
|
1114
|
+
selectorType: selectorInfo.type,
|
|
1115
|
+
description: userDescription || `Click on ${element.text || element.tag || 'element'}`,
|
|
1116
|
+
confidence: selectorInfo.confidence,
|
|
1117
|
+
originalEvent: event
|
|
1118
|
+
};
|
|
1119
|
+
case 'fill':
|
|
1120
|
+
return {
|
|
1121
|
+
type: 'fill',
|
|
1122
|
+
selector: selectorInfo.selector,
|
|
1123
|
+
selectorType: selectorInfo.type,
|
|
1124
|
+
value: value || '',
|
|
1125
|
+
description: userDescription || `Fill "${value}" in ${element.aria?.label || element.attributes?.placeholder || 'input'}`,
|
|
1126
|
+
confidence: selectorInfo.confidence,
|
|
1127
|
+
originalEvent: event
|
|
1128
|
+
};
|
|
1129
|
+
case 'press':
|
|
1130
|
+
return {
|
|
1131
|
+
type: 'press',
|
|
1132
|
+
selector: value || 'Enter',
|
|
1133
|
+
selectorType: 'css',
|
|
1134
|
+
value: value || 'Enter',
|
|
1135
|
+
description: userDescription || `Press ${value || 'Enter'} key`,
|
|
1136
|
+
confidence: 1.0,
|
|
1137
|
+
originalEvent: event
|
|
1138
|
+
};
|
|
1139
|
+
case 'assertVisible':
|
|
1140
|
+
return {
|
|
1141
|
+
type: 'assertVisible',
|
|
1142
|
+
selector: selectorInfo.selector,
|
|
1143
|
+
selectorType: selectorInfo.type,
|
|
1144
|
+
description: userDescription || `Assert ${element.text || 'element'} is visible`,
|
|
1145
|
+
confidence: selectorInfo.confidence,
|
|
1146
|
+
originalEvent: event
|
|
1147
|
+
};
|
|
1148
|
+
case 'assertText':
|
|
1149
|
+
return {
|
|
1150
|
+
type: 'assertText',
|
|
1151
|
+
selector: selectorInfo.selector,
|
|
1152
|
+
selectorType: selectorInfo.type,
|
|
1153
|
+
value: value || '',
|
|
1154
|
+
description: userDescription || `Assert text is "${value}"`,
|
|
1155
|
+
confidence: selectorInfo.confidence,
|
|
1156
|
+
originalEvent: event
|
|
1157
|
+
};
|
|
1158
|
+
case 'wait':
|
|
1159
|
+
return {
|
|
1160
|
+
type: 'wait',
|
|
1161
|
+
selector: 'body',
|
|
1162
|
+
selectorType: 'css',
|
|
1163
|
+
value: value || '1000',
|
|
1164
|
+
description: userDescription || `Wait ${value || '1000'}ms`,
|
|
1165
|
+
confidence: 1.0,
|
|
1166
|
+
originalEvent: event
|
|
1167
|
+
};
|
|
1168
|
+
case 'select':
|
|
1169
|
+
return {
|
|
1170
|
+
type: 'select',
|
|
1171
|
+
selector: selectorInfo.selector,
|
|
1172
|
+
selectorType: selectorInfo.type,
|
|
1173
|
+
value: value || '',
|
|
1174
|
+
description: userDescription || `Select "${value}"`,
|
|
1175
|
+
confidence: selectorInfo.confidence,
|
|
1176
|
+
originalEvent: event
|
|
1177
|
+
};
|
|
1178
|
+
default:
|
|
1179
|
+
// Default to click for unknown types
|
|
1180
|
+
return {
|
|
1181
|
+
type: 'click',
|
|
1182
|
+
selector: selectorInfo.selector,
|
|
1183
|
+
selectorType: selectorInfo.type,
|
|
1184
|
+
description: userDescription || `Click on ${element.text || element.tag || 'element'}`,
|
|
1185
|
+
confidence: selectorInfo.confidence * 0.8, // Lower confidence for defaulted steps
|
|
1186
|
+
originalEvent: event
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
// Wrapper for processing individual events without navigation context
|
|
1191
|
+
function transformSingleEventToStep(event) {
|
|
1192
|
+
// For single event processing, we treat navigation as goto if URL changed
|
|
1193
|
+
const lastUrl = '';
|
|
1194
|
+
return transformEventToStep(event, lastUrl);
|
|
1195
|
+
}
|
|
1196
|
+
function generateBestSelector(element) {
|
|
1197
|
+
const attrs = element.attributes || {};
|
|
1198
|
+
// Priority 1: data-testid (most stable)
|
|
1199
|
+
if (attrs['data-testid']) {
|
|
1200
|
+
return { selector: attrs['data-testid'], type: 'data-testid', confidence: 1.0 };
|
|
1201
|
+
}
|
|
1202
|
+
// Priority 2: ARIA role
|
|
1203
|
+
if (element.aria?.role && element.aria?.label) {
|
|
1204
|
+
return { selector: `${element.aria.role}[name="${element.aria.label}"]`, type: 'role', confidence: 0.95 };
|
|
1205
|
+
}
|
|
1206
|
+
if (element.aria?.role) {
|
|
1207
|
+
return { selector: element.aria.role, type: 'role', confidence: 0.9 };
|
|
1208
|
+
}
|
|
1209
|
+
// Priority 3: ARIA label
|
|
1210
|
+
if (element.aria?.label) {
|
|
1211
|
+
return { selector: element.aria.label, type: 'label', confidence: 0.9 };
|
|
1212
|
+
}
|
|
1213
|
+
// Priority 4: ID (if stable)
|
|
1214
|
+
if (attrs.id && !attrs.id.match(/\d+/)) {
|
|
1215
|
+
return { selector: `#${attrs.id}`, type: 'css', confidence: 0.85 };
|
|
1216
|
+
}
|
|
1217
|
+
// Priority 5: Text content (for buttons/links)
|
|
1218
|
+
if (element.text && element.text.length < 50 && ['button', 'a', 'span'].includes(element.tag || '')) {
|
|
1219
|
+
return { selector: element.text.trim(), type: 'text', confidence: 0.8 };
|
|
1220
|
+
}
|
|
1221
|
+
// Priority 6: CSS stable classes (avoiding dynamic classes)
|
|
1222
|
+
if (attrs.class) {
|
|
1223
|
+
const stableClasses = attrs.class
|
|
1224
|
+
.split(' ')
|
|
1225
|
+
.filter(c => !c.match(/\d+/) && c.length > 3)
|
|
1226
|
+
.slice(0, 3)
|
|
1227
|
+
.join('.');
|
|
1228
|
+
if (stableClasses) {
|
|
1229
|
+
return { selector: `.${stableClasses}`, type: 'css', confidence: 0.7 };
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
// Priority 7: XPath (last resort)
|
|
1233
|
+
if (element.xpath) {
|
|
1234
|
+
return { selector: element.xpath, type: 'xpath', confidence: 0.5 };
|
|
1235
|
+
}
|
|
1236
|
+
// Fallback: tag name
|
|
1237
|
+
return { selector: element.tag || 'body', type: 'css', confidence: 0.4 };
|
|
1238
|
+
}
|
|
1239
|
+
function addImplicitAssertions(steps) {
|
|
1240
|
+
const result = [];
|
|
1241
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1242
|
+
const step = steps[i];
|
|
1243
|
+
result.push(step);
|
|
1244
|
+
// After navigation, assert page loaded
|
|
1245
|
+
if (step.type === 'goto') {
|
|
1246
|
+
result.push({
|
|
1247
|
+
type: 'assertVisible',
|
|
1248
|
+
selector: 'body',
|
|
1249
|
+
selectorType: 'css',
|
|
1250
|
+
description: 'Assert page is loaded',
|
|
1251
|
+
confidence: 1.0
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
// After fill on input, assert value changed
|
|
1255
|
+
if (step.type === 'fill' && i < steps.length - 1) {
|
|
1256
|
+
const nextStep = steps[i + 1];
|
|
1257
|
+
if (nextStep.type === 'click' || nextStep.type === 'press') {
|
|
1258
|
+
result.push({
|
|
1259
|
+
type: 'assertVisible',
|
|
1260
|
+
selector: step.selector,
|
|
1261
|
+
selectorType: step.selectorType,
|
|
1262
|
+
description: `Assert input has value "${step.value}"`,
|
|
1263
|
+
confidence: 0.9
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return result;
|
|
1269
|
+
}
|
|
1270
|
+
function calculateFlowConfidence(steps) {
|
|
1271
|
+
if (steps.length === 0)
|
|
1272
|
+
return 0;
|
|
1273
|
+
const totalConfidence = steps.reduce((sum, step) => sum + (step.confidence || 0.5), 0);
|
|
1274
|
+
return Math.round((totalConfidence / steps.length) * 100) / 100;
|
|
1275
|
+
}
|
|
1276
|
+
function extractUrlPattern(urls) {
|
|
1277
|
+
if (urls.length === 0)
|
|
1278
|
+
return '';
|
|
1279
|
+
// Find common pattern
|
|
1280
|
+
const uniqueUrls = [...new Set(urls)];
|
|
1281
|
+
if (uniqueUrls.length === 1)
|
|
1282
|
+
return uniqueUrls[0];
|
|
1283
|
+
// Try to extract route pattern (e.g., /user/:id)
|
|
1284
|
+
const patterns = uniqueUrls.map(url => {
|
|
1285
|
+
try {
|
|
1286
|
+
const pathname = new URL(url).pathname;
|
|
1287
|
+
return pathname.replace(/\/[0-9a-f]{24}/g, '/:id').replace(/\/\d+/g, '/:id');
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
return url;
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
return [...new Set(patterns)].join(' -> ');
|
|
1294
|
+
}
|
|
1295
|
+
// ============================================
|
|
1296
|
+
// END E2E TRANSFORMATION FUNCTIONS
|
|
1297
|
+
// ============================================
|
|
843
1298
|
const useSSE = process.argv.includes("--sse") || process.env.MCP_TRANSPORT === "sse";
|
|
844
1299
|
if (!useSSE) {
|
|
845
1300
|
const apiKey = process.env.VIEWGATE_API_KEY || process.env.API_KEY || "";
|