@debugg-ai/debugg-ai-mcp 1.0.22 → 1.0.25
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/handlers/liveSessionHandlers.js +4 -4
- package/dist/handlers/testPageChangesHandler.js +29 -38
- package/dist/services/index.js +9 -0
- package/dist/services/workflows.js +14 -3
- package/package.json +1 -1
- package/dist/e2e-agents/e2eRunner.js +0 -145
- package/dist/e2e-agents/recordingHandler.js +0 -57
- package/dist/e2e-agents/resultsFormatter.js +0 -102
|
@@ -76,12 +76,12 @@ export async function startLiveSessionHandler(input, context, progressCallback)
|
|
|
76
76
|
if (!session) {
|
|
77
77
|
throw new Error('Failed to start browser session: No session returned');
|
|
78
78
|
}
|
|
79
|
-
// If we need a tunnel, create it now using the
|
|
79
|
+
// If we need a tunnel, create it now using the tunnel key from the session response
|
|
80
80
|
if (isLocalhost && tunnelId) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new Error('No tunnel key provided by backend - tunnels not available for localhost URLs');
|
|
81
|
+
if (!session.tunnelKey) {
|
|
82
|
+
throw new Error('Backend did not return a tunnel key for localhost session');
|
|
84
83
|
}
|
|
84
|
+
const tunnelAuthToken = session.tunnelKey;
|
|
85
85
|
logger.info(`Creating tunnel with backend-provided key for ${input.url} -> ${sessionUrl}`);
|
|
86
86
|
// Create the tunnel using the original localhost URL and the generated tunnel ID
|
|
87
87
|
const port = extractLocalhostPort(input.url);
|
|
@@ -35,44 +35,13 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
35
35
|
const client = new DebuggAIServerClient(config.api.key);
|
|
36
36
|
await client.init();
|
|
37
37
|
let tunnelId;
|
|
38
|
+
let ngrokKeyId;
|
|
38
39
|
try {
|
|
39
40
|
const targetUrlRaw = resolveTargetUrl(input);
|
|
40
|
-
|
|
41
|
-
// --- Localhost tunneling ---
|
|
42
|
-
if (isLocalhostUrl(targetUrlRaw)) {
|
|
43
|
-
if (progressCallback) {
|
|
44
|
-
await progressCallback({ progress: 1, total: 10, message: 'Creating secure tunnel for localhost...' });
|
|
45
|
-
}
|
|
46
|
-
const port = extractLocalhostPort(targetUrlRaw);
|
|
47
|
-
if (!port) {
|
|
48
|
-
throw new Error(`Could not extract port from localhost URL: ${targetUrlRaw}`);
|
|
49
|
-
}
|
|
50
|
-
const { v4: uuidv4 } = await import('uuid');
|
|
51
|
-
tunnelId = uuidv4();
|
|
52
|
-
const tunnelPublicUrl = `https://${tunnelId}.ngrok.debugg.ai`;
|
|
53
|
-
// Start a minimal browser session to obtain the ngrok auth token
|
|
54
|
-
const session = await client.browserSessions.startSession({
|
|
55
|
-
url: tunnelPublicUrl,
|
|
56
|
-
originalUrl: targetUrlRaw,
|
|
57
|
-
localPort: port,
|
|
58
|
-
sessionName: `Tunnel provisioning for ${targetUrlRaw}`,
|
|
59
|
-
monitorConsole: false,
|
|
60
|
-
monitorNetwork: false,
|
|
61
|
-
takeScreenshots: false,
|
|
62
|
-
isLocalhost: true,
|
|
63
|
-
tunnelId,
|
|
64
|
-
});
|
|
65
|
-
const tunnelAuthToken = session.tunnelKey;
|
|
66
|
-
if (!tunnelAuthToken) {
|
|
67
|
-
throw new Error('Browser sessions service did not return a tunnel auth token');
|
|
68
|
-
}
|
|
69
|
-
const tunnelResult = await tunnelManager.processUrl(targetUrlRaw, tunnelAuthToken, tunnelId);
|
|
70
|
-
targetUrl = tunnelResult.url;
|
|
71
|
-
logger.info(`Tunnel ready: ${targetUrl}`);
|
|
72
|
-
}
|
|
41
|
+
const isLocalhost = isLocalhostUrl(targetUrlRaw);
|
|
73
42
|
// --- Find workflow template ---
|
|
74
43
|
if (progressCallback) {
|
|
75
|
-
await progressCallback({ progress:
|
|
44
|
+
await progressCallback({ progress: 1, total: 10, message: 'Locating evaluation workflow template...' });
|
|
76
45
|
}
|
|
77
46
|
if (!cachedTemplateUuid) {
|
|
78
47
|
const template = await client.workflows.findEvaluationTemplate();
|
|
@@ -85,15 +54,33 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
85
54
|
}
|
|
86
55
|
// --- Build context data ---
|
|
87
56
|
const contextData = {
|
|
88
|
-
targetUrl:
|
|
57
|
+
targetUrl: targetUrlRaw,
|
|
89
58
|
question: input.description,
|
|
90
59
|
};
|
|
91
60
|
// --- Execute ---
|
|
92
61
|
if (progressCallback) {
|
|
93
|
-
await progressCallback({ progress:
|
|
62
|
+
await progressCallback({ progress: 2, total: 10, message: 'Queuing workflow execution...' });
|
|
94
63
|
}
|
|
95
|
-
const
|
|
64
|
+
const executeResponse = await client.workflows.executeWorkflow(cachedTemplateUuid, contextData);
|
|
65
|
+
const executionUuid = executeResponse.executionUuid;
|
|
66
|
+
ngrokKeyId = executeResponse.ngrokKeyId ?? undefined;
|
|
96
67
|
logger.info(`Execution queued: ${executionUuid}`);
|
|
68
|
+
// --- Localhost tunneling (after execute, using tunnel_key + executionUuid as subdomain) ---
|
|
69
|
+
if (isLocalhost) {
|
|
70
|
+
if (progressCallback) {
|
|
71
|
+
await progressCallback({ progress: 3, total: 10, message: 'Creating secure tunnel for localhost...' });
|
|
72
|
+
}
|
|
73
|
+
const port = extractLocalhostPort(targetUrlRaw);
|
|
74
|
+
if (!port) {
|
|
75
|
+
throw new Error(`Could not extract port from localhost URL: ${targetUrlRaw}`);
|
|
76
|
+
}
|
|
77
|
+
if (!executeResponse.tunnelKey) {
|
|
78
|
+
throw new Error('Backend did not return a tunnel key for localhost execution');
|
|
79
|
+
}
|
|
80
|
+
tunnelId = executionUuid;
|
|
81
|
+
const tunnelResult = await tunnelManager.processUrl(targetUrlRaw, executeResponse.tunnelKey, tunnelId);
|
|
82
|
+
logger.info(`Tunnel ready: ${tunnelResult.url}`);
|
|
83
|
+
}
|
|
97
84
|
// --- Poll ---
|
|
98
85
|
let lastSteps = 0;
|
|
99
86
|
const finalExecution = await client.workflows.pollExecution(executionUuid, async (exec) => {
|
|
@@ -120,7 +107,7 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
120
107
|
success: finalExecution.state?.success ?? false,
|
|
121
108
|
status: finalExecution.status,
|
|
122
109
|
stepsTaken: finalExecution.state?.stepsTaken ?? surferNode?.outputData?.stepsTaken ?? 0,
|
|
123
|
-
targetUrl,
|
|
110
|
+
targetUrl: targetUrlRaw,
|
|
124
111
|
executionId: executionUuid,
|
|
125
112
|
durationMs: finalExecution.durationMs ?? duration,
|
|
126
113
|
};
|
|
@@ -157,6 +144,10 @@ export async function testPageChangesHandler(input, context, progressCallback) {
|
|
|
157
144
|
throw handleExternalServiceError(error, 'DebuggAI', 'test execution');
|
|
158
145
|
}
|
|
159
146
|
finally {
|
|
147
|
+
// Revoke the short-lived ngrok key
|
|
148
|
+
if (ngrokKeyId) {
|
|
149
|
+
client.revokeNgrokKey(ngrokKeyId).catch(err => logger.warn(`Failed to revoke ngrok key ${ngrokKeyId}: ${err}`));
|
|
150
|
+
}
|
|
160
151
|
// Clean up tunnel if we created one
|
|
161
152
|
if (tunnelId) {
|
|
162
153
|
tunnelManager.stopTunnel(tunnelId).catch(err => logger.warn(`Failed to stop tunnel ${tunnelId}: ${err}`));
|
package/dist/services/index.js
CHANGED
|
@@ -50,6 +50,15 @@ export class DebuggAIServerClient {
|
|
|
50
50
|
this.browserSessions = createBrowserSessionsService(this.tx);
|
|
51
51
|
this.workflows = createWorkflowsService(this.tx);
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Revoke an ngrok API key by its key ID.
|
|
55
|
+
* Call this after workflow execution completes to clean up the short-lived key.
|
|
56
|
+
*/
|
|
57
|
+
async revokeNgrokKey(ngrokKeyId) {
|
|
58
|
+
if (!this.tx)
|
|
59
|
+
throw new Error('Client not initialized — call init() first');
|
|
60
|
+
await this.tx.post('api/v1/ngrok/revoke/', { ngrokKeyId });
|
|
61
|
+
}
|
|
53
62
|
}
|
|
54
63
|
/**
|
|
55
64
|
* Create and initialize a service client
|
|
@@ -13,12 +13,23 @@ export const createWorkflowsService = (tx) => {
|
|
|
13
13
|
const evalTemplate = templates.find(t => t.name.toLowerCase().includes('app evaluation'));
|
|
14
14
|
return evalTemplate ?? templates[0] ?? null;
|
|
15
15
|
},
|
|
16
|
-
async executeWorkflow(workflowUuid, contextData) {
|
|
17
|
-
const
|
|
16
|
+
async executeWorkflow(workflowUuid, contextData, env) {
|
|
17
|
+
const body = { contextData };
|
|
18
|
+
if (env && Object.keys(env).length > 0) {
|
|
19
|
+
body.env = env;
|
|
20
|
+
}
|
|
21
|
+
const response = await tx.post(`api/v1/workflows/${workflowUuid}/execute/`, body);
|
|
18
22
|
if (!response?.resourceUuid) {
|
|
19
23
|
throw new Error('Workflow execution failed: no execution UUID returned');
|
|
20
24
|
}
|
|
21
|
-
return
|
|
25
|
+
return {
|
|
26
|
+
executionUuid: response.resourceUuid,
|
|
27
|
+
tunnelKey: response.tunnelKey ?? null,
|
|
28
|
+
ngrokKeyId: response.ngrokKeyId ?? null,
|
|
29
|
+
ngrokExpiresAt: response.ngrokExpiresAt ?? null,
|
|
30
|
+
resolvedEnvironmentId: response.resolvedEnvironmentId ?? null,
|
|
31
|
+
resolvedCredentialId: response.resolvedCredentialId ?? null,
|
|
32
|
+
};
|
|
22
33
|
},
|
|
23
34
|
async getExecution(executionUuid) {
|
|
24
35
|
const response = await tx.get(`api/v1/workflows/executions/${executionUuid}/`);
|
package/package.json
CHANGED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
-
import { createRequire } from 'module';
|
|
3
|
-
const require = createRequire(import.meta.url);
|
|
4
|
-
let ngrokModule = null;
|
|
5
|
-
async function getNgrok() {
|
|
6
|
-
if (!ngrokModule) {
|
|
7
|
-
try {
|
|
8
|
-
ngrokModule = require('ngrok');
|
|
9
|
-
}
|
|
10
|
-
catch (error) {
|
|
11
|
-
throw new Error(`Failed to load ngrok module: ${error}`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return ngrokModule;
|
|
15
|
-
}
|
|
16
|
-
async function startTunnel(authToken, localPort, domain) {
|
|
17
|
-
try {
|
|
18
|
-
const ngrok = await getNgrok();
|
|
19
|
-
if (process.env.DOCKER_CONTAINER === "true") {
|
|
20
|
-
const url = await ngrok.connect({ proto: 'http', addr: `host.docker.internal:${localPort}`, hostname: domain, authtoken: authToken });
|
|
21
|
-
return url;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
const url = await ngrok.connect({ proto: 'http', addr: localPort, hostname: domain, authtoken: authToken });
|
|
25
|
-
return url;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
console.error('Error starting ngrok tunnel:', err);
|
|
30
|
-
throw err;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
async function stopTunnel(url) {
|
|
34
|
-
try {
|
|
35
|
-
const ngrok = await getNgrok();
|
|
36
|
-
if (url) {
|
|
37
|
-
await ngrok.disconnect(url);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
await ngrok.disconnect();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
console.error('Error stopping ngrok tunnel:', err);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const POLL_INTERVAL_MS = 1500;
|
|
48
|
-
const TIMEOUT_MS = 900_000; // 15 minutes
|
|
49
|
-
export class E2eTestRunner {
|
|
50
|
-
client;
|
|
51
|
-
constructor(client) {
|
|
52
|
-
this.client = client;
|
|
53
|
-
}
|
|
54
|
-
async setup() {
|
|
55
|
-
await this.configureNgrok();
|
|
56
|
-
}
|
|
57
|
-
async configureNgrok() {
|
|
58
|
-
// ngrok binary is downloaded automatically by the ngrok package
|
|
59
|
-
}
|
|
60
|
-
async startTunnel(authToken, port, url) {
|
|
61
|
-
await startTunnel(authToken, port, url);
|
|
62
|
-
console.error(`Tunnel started at: ${url}`);
|
|
63
|
-
return url;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Create a new E2E test and run it.
|
|
67
|
-
*/
|
|
68
|
-
async createNewE2eTest(testPort, testDescription, repoName, branchName, repoPath, filePath) {
|
|
69
|
-
console.error(`Creating new E2E test with description: ${testDescription}`);
|
|
70
|
-
const key = uuidv4();
|
|
71
|
-
const e2eTest = await this.client.e2es?.createE2eTest(testDescription, { filePath: filePath ?? "", repoName, branchName, repoPath, key });
|
|
72
|
-
console.error("E2E test creation response:", JSON.stringify(e2eTest, null, 2));
|
|
73
|
-
const authToken = e2eTest?.tunnelKey;
|
|
74
|
-
if (!authToken) {
|
|
75
|
-
console.error("Failed to get auth token. E2E test response:", e2eTest);
|
|
76
|
-
console.error("Available keys in response:", e2eTest ? Object.keys(e2eTest) : 'null response');
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
await startTunnel(authToken, testPort, `${key}.ngrok.debugg.ai`);
|
|
80
|
-
console.error(`E2E test created - ${e2eTest}`);
|
|
81
|
-
if (!e2eTest) {
|
|
82
|
-
console.error("Failed to create E2E test.");
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
if (!e2eTest.curRun) {
|
|
86
|
-
console.error("Failed to create E2E test run.");
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
return e2eTest.curRun;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Poll an E2E run until it completes or times out.
|
|
93
|
-
*
|
|
94
|
-
* Uses a safe async loop — no setInterval race conditions.
|
|
95
|
-
* The tunnel is stopped in a finally block so cleanup always runs
|
|
96
|
-
* regardless of how the loop exits (completion, timeout, or error).
|
|
97
|
-
*
|
|
98
|
-
* onUpdate is called on every poll tick so progress notifications
|
|
99
|
-
* fire at a steady cadence and keep the MCP connection alive.
|
|
100
|
-
*/
|
|
101
|
-
async handleE2eRun(e2eRun, onUpdate) {
|
|
102
|
-
const tunnelUrl = `https://${e2eRun.key}.ngrok.debugg.ai`;
|
|
103
|
-
const startTime = Date.now();
|
|
104
|
-
let updatedRun = e2eRun;
|
|
105
|
-
console.error(`🔧 Handling E2E run - ${e2eRun.uuid}`);
|
|
106
|
-
console.error(`🌐 Tunnel: ${tunnelUrl}`);
|
|
107
|
-
try {
|
|
108
|
-
while (true) {
|
|
109
|
-
if (Date.now() - startTime >= TIMEOUT_MS) {
|
|
110
|
-
console.error('⏰ E2E test timed out after 15 minutes');
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
await this._sleep(POLL_INTERVAL_MS);
|
|
114
|
-
try {
|
|
115
|
-
const latestRun = await this.client.e2es?.getE2eRun(e2eRun.uuid);
|
|
116
|
-
if (latestRun) {
|
|
117
|
-
updatedRun = latestRun;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (pollError) {
|
|
121
|
-
console.error(`⚠️ Poll error (continuing): ${pollError}`);
|
|
122
|
-
}
|
|
123
|
-
console.error(`📡 Polled E2E run status: ${updatedRun.status}`);
|
|
124
|
-
// Always fire onUpdate — keeps MCP progress notifications alive
|
|
125
|
-
// even when the run is loading or the poll returned null
|
|
126
|
-
await onUpdate(updatedRun);
|
|
127
|
-
if (updatedRun.status === 'completed') {
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
finally {
|
|
133
|
-
await this._stopTunnel(tunnelUrl);
|
|
134
|
-
}
|
|
135
|
-
return updatedRun;
|
|
136
|
-
}
|
|
137
|
-
// Overridable in tests
|
|
138
|
-
async _sleep(ms) {
|
|
139
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
140
|
-
}
|
|
141
|
-
async _stopTunnel(url) {
|
|
142
|
-
await stopTunnel(url);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
export default E2eTestRunner;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as http from "http";
|
|
3
|
-
import * as https from "https";
|
|
4
|
-
import * as path from "path";
|
|
5
|
-
import { URL } from "url";
|
|
6
|
-
export async function fetchAndOpenGif(projectRoot, recordingUrl, testName, testId) {
|
|
7
|
-
const cacheDir = path.join(projectRoot, ".debugg-ai", "e2e-runs");
|
|
8
|
-
console.error('....downloading gif....');
|
|
9
|
-
console.error('cacheDir', cacheDir);
|
|
10
|
-
console.error('testId', testId);
|
|
11
|
-
console.error('recordingUrl', recordingUrl);
|
|
12
|
-
let localUrl = recordingUrl.replace('localhost', 'localhost:8002');
|
|
13
|
-
console.error('localUrl', localUrl);
|
|
14
|
-
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
15
|
-
const filePath = path.join(cacheDir, `${testName.replace(/[^a-zA-Z0-9]/g, '-')}-${testId.slice(0, 4)}.gif`);
|
|
16
|
-
const fileUrl = new URL(localUrl);
|
|
17
|
-
const file = fs.createWriteStream(filePath);
|
|
18
|
-
console.error(`⬇️ Downloading test recording...`);
|
|
19
|
-
await new Promise((resolve, reject) => {
|
|
20
|
-
console.error('fetching gif', fileUrl);
|
|
21
|
-
if (fileUrl.protocol === 'https:') {
|
|
22
|
-
https.get(localUrl, (response) => {
|
|
23
|
-
if (response.statusCode !== 200) {
|
|
24
|
-
reject(new Error(`Failed to download file: ${response.statusCode}`));
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
response.pipe(file);
|
|
28
|
-
file.on("finish", () => {
|
|
29
|
-
file.close();
|
|
30
|
-
resolve();
|
|
31
|
-
});
|
|
32
|
-
}).on("error", (err) => {
|
|
33
|
-
fs.unlinkSync(filePath);
|
|
34
|
-
reject(err);
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
http.get(localUrl, (response) => {
|
|
39
|
-
if (response.statusCode !== 200) {
|
|
40
|
-
reject(new Error(`Failed to download file: ${response.statusCode}`));
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
response.pipe(file);
|
|
44
|
-
file.on("finish", () => {
|
|
45
|
-
file.close();
|
|
46
|
-
resolve();
|
|
47
|
-
});
|
|
48
|
-
}).on("error", (err) => {
|
|
49
|
-
fs.unlinkSync(filePath);
|
|
50
|
-
reject(err);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
console.error(`📂 Opening test recording`);
|
|
55
|
-
// const fileUri = vscode.Uri.file(filePath);
|
|
56
|
-
// await vscode.commands.executeCommand('vscode.open', fileUri);
|
|
57
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
export class RunResultFormatter {
|
|
2
|
-
steps = [];
|
|
3
|
-
passed(result) {
|
|
4
|
-
return result.status === "completed" && result.outcome === "pass";
|
|
5
|
-
}
|
|
6
|
-
formatFailures(result) {
|
|
7
|
-
if (this.passed(result) || !result.outcome)
|
|
8
|
-
return "";
|
|
9
|
-
return "\n\n❌ Failures:" + "\n" + `> ${result.outcome}`;
|
|
10
|
-
}
|
|
11
|
-
formatStepsAsMarkdown() {
|
|
12
|
-
if (this.steps.length === 0)
|
|
13
|
-
return "";
|
|
14
|
-
return ("\n\n" +
|
|
15
|
-
this.steps
|
|
16
|
-
.map((s, idx) => {
|
|
17
|
-
const num = `Step ${idx + 1}:`;
|
|
18
|
-
const label = s.label.padEnd(30);
|
|
19
|
-
const icon = "✅ Success";
|
|
20
|
-
// s.status === "pending"
|
|
21
|
-
// ? chalk.yellow("⏳ Pending")
|
|
22
|
-
// : s.status === "success"
|
|
23
|
-
// ? chalk.green("✅ Success")
|
|
24
|
-
// : chalk.red("❌ Failed");
|
|
25
|
-
return `${num} ${label} ${icon}`;
|
|
26
|
-
})
|
|
27
|
-
.join("\n\n"));
|
|
28
|
-
}
|
|
29
|
-
updateStep(label, status) {
|
|
30
|
-
const existing = this.steps.find((s) => s.label === label);
|
|
31
|
-
if (existing) {
|
|
32
|
-
existing.status = status;
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
this.steps.push({ label, status });
|
|
36
|
-
}
|
|
37
|
-
console.error('updating step. steps ->', this.steps);
|
|
38
|
-
// Clear terminal and redraw
|
|
39
|
-
console.error("\x1Bc"); // ANSI clear screen
|
|
40
|
-
console.error("🧪 E2E Test Progress" +
|
|
41
|
-
`\r\n${this.steps
|
|
42
|
-
.map((s, i) => {
|
|
43
|
-
const icon = s.status === "pending"
|
|
44
|
-
? "⏳"
|
|
45
|
-
: s.status === "success"
|
|
46
|
-
? "✅"
|
|
47
|
-
: "❌";
|
|
48
|
-
return `${`Step ${i + 1}:`} ${s.label.padEnd(30)} ${icon}`;
|
|
49
|
-
})
|
|
50
|
-
.join("\r\n")}`);
|
|
51
|
-
}
|
|
52
|
-
formatTerminalBox(result) {
|
|
53
|
-
const header = this.passed(result)
|
|
54
|
-
? "✅ Test Passed"
|
|
55
|
-
: "❌ Test Failed";
|
|
56
|
-
const body = [
|
|
57
|
-
"Test: " + result.test?.name,
|
|
58
|
-
"Description: " + (result.test?.description ?? "None"),
|
|
59
|
-
"Duration: " + `${result.metrics?.executionTime ?? 0}s`,
|
|
60
|
-
"Status: " + result.status,
|
|
61
|
-
"Outcome: " + result.outcome,
|
|
62
|
-
this.formatStepsAsMarkdown(),
|
|
63
|
-
this.passed(result) ? "" : this.formatFailures(result),
|
|
64
|
-
]
|
|
65
|
-
.filter(Boolean)
|
|
66
|
-
.join("\n");
|
|
67
|
-
return `${header}\n${body}`;
|
|
68
|
-
}
|
|
69
|
-
formatMarkdownSummary(result) {
|
|
70
|
-
return [
|
|
71
|
-
`🧪 **Test Name:** ${result.test?.name ?? "Unknown"}`,
|
|
72
|
-
`📄 **Description:** ${result.test?.description ?? "None"}`,
|
|
73
|
-
`⏱ **Duration:** ${result.metrics?.executionTime ?? 0}s`,
|
|
74
|
-
`🔎 **Status:** ${result.status}`,
|
|
75
|
-
`📊 **Outcome:** ${result.outcome}`,
|
|
76
|
-
this.formatStepsAsMarkdown(),
|
|
77
|
-
this.formatFailures(result),
|
|
78
|
-
]
|
|
79
|
-
.filter(Boolean)
|
|
80
|
-
.join("\n")
|
|
81
|
-
.trim();
|
|
82
|
-
}
|
|
83
|
-
/*
|
|
84
|
-
Terminal uses different formatting than markdown.
|
|
85
|
-
*/
|
|
86
|
-
terminalSummary(result) {
|
|
87
|
-
return [
|
|
88
|
-
`🧪 Test Name: ${result.test?.name ?? "Unknown"}`,
|
|
89
|
-
`📄 Description: ${result.test?.description ?? "None"}`,
|
|
90
|
-
`⏱ Duration: ${result.metrics?.executionTime ?? 0}s`,
|
|
91
|
-
`🔎 Status: ${result.status}`,
|
|
92
|
-
`📊 Outcome: ${result.outcome}`,
|
|
93
|
-
this.formatFailures(result),
|
|
94
|
-
]
|
|
95
|
-
.filter(Boolean)
|
|
96
|
-
.join("\r\n")
|
|
97
|
-
.trim();
|
|
98
|
-
}
|
|
99
|
-
appendToTestRun(result) {
|
|
100
|
-
console.error(this.terminalSummary(result));
|
|
101
|
-
}
|
|
102
|
-
}
|