norn-cli 1.3.17 → 1.3.19
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/AGENTS.md +72 -0
- package/CHANGELOG.md +39 -1
- package/README.md +7 -3
- package/dist/cli.js +113 -54
- package/out/assertionRunner.js +537 -0
- package/out/cli/colors.js +129 -0
- package/out/cli/formatters/assertion.js +75 -0
- package/out/cli/formatters/index.js +23 -0
- package/out/cli/formatters/response.js +106 -0
- package/out/cli/formatters/summary.js +187 -0
- package/out/cli/redaction.js +237 -0
- package/out/cli/reporters/html.js +634 -0
- package/out/cli/reporters/index.js +22 -0
- package/out/cli/reporters/junit.js +211 -0
- package/out/cli.js +926 -0
- package/out/codeLensProvider.js +254 -0
- package/out/compareContentProvider.js +85 -0
- package/out/completionProvider.js +1886 -0
- package/out/contractDecorationProvider.js +243 -0
- package/out/coverageCalculator.js +756 -0
- package/out/coveragePanel.js +542 -0
- package/out/diagnosticProvider.js +980 -0
- package/out/environmentProvider.js +373 -0
- package/out/extension.js +1025 -0
- package/out/httpClient.js +269 -0
- package/out/jsonFileReader.js +320 -0
- package/out/nornapiParser.js +326 -0
- package/out/parser.js +725 -0
- package/out/responsePanel.js +4674 -0
- package/out/schemaGenerator.js +393 -0
- package/out/scriptRunner.js +419 -0
- package/out/sequenceRunner.js +3046 -0
- package/out/swaggerParser.js +339 -0
- package/out/test/extension.test.js +48 -0
- package/out/testProvider.js +658 -0
- package/out/validationCache.js +245 -0
- package/package.json +1 -1
|
@@ -0,0 +1,4674 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ResponsePanel = void 0;
|
|
37
|
+
const vscode = __importStar(require("vscode"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const compareContentProvider_1 = require("./compareContentProvider");
|
|
40
|
+
const schemaGenerator_1 = require("./schemaGenerator");
|
|
41
|
+
class ResponsePanel {
|
|
42
|
+
static currentPanel;
|
|
43
|
+
_panel;
|
|
44
|
+
_disposables = [];
|
|
45
|
+
_sequenceName = '';
|
|
46
|
+
_totalSteps = 0;
|
|
47
|
+
_startTime = 0;
|
|
48
|
+
_baselineResponse = null;
|
|
49
|
+
_sourceFilePath = '';
|
|
50
|
+
constructor(panel) {
|
|
51
|
+
this._panel = panel;
|
|
52
|
+
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
|
|
53
|
+
// Handle messages from the webview for response comparison
|
|
54
|
+
this._panel.webview.onDidReceiveMessage(async (message) => {
|
|
55
|
+
switch (message.type) {
|
|
56
|
+
case 'setBaseline':
|
|
57
|
+
this._baselineResponse = message.body;
|
|
58
|
+
break;
|
|
59
|
+
case 'compare':
|
|
60
|
+
if (this._baselineResponse) {
|
|
61
|
+
await this._openDiff(this._baselineResponse, message.body);
|
|
62
|
+
this._baselineResponse = null;
|
|
63
|
+
// Tell webview to reset compare state
|
|
64
|
+
this._panel.webview.postMessage({ type: 'resetCompare' });
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
case 'assertSchema':
|
|
68
|
+
await this._generateSchemaAndInsertAssertion(message.body, message.method, message.url, message.variableName);
|
|
69
|
+
break;
|
|
70
|
+
case 'assertProperty':
|
|
71
|
+
await this._insertPropertyAssertion(message.path, message.value, message.variableName);
|
|
72
|
+
break;
|
|
73
|
+
case 'openContractView':
|
|
74
|
+
// Open the Contract View with the schema validation data
|
|
75
|
+
vscode.commands.executeCommand('norn.openContractView', {
|
|
76
|
+
schemaPath: message.schemaPath,
|
|
77
|
+
errors: message.errors,
|
|
78
|
+
responseBody: message.responseBody,
|
|
79
|
+
schema: message.schema,
|
|
80
|
+
sourceFile: this._sourceFilePath
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}, null, this._disposables);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Generate a JSON Schema from response body and insert assertion into the source file
|
|
88
|
+
*/
|
|
89
|
+
async _generateSchemaAndInsertAssertion(body, method, url, variableName) {
|
|
90
|
+
if (!this._sourceFilePath) {
|
|
91
|
+
vscode.window.showErrorMessage('Cannot generate schema: source file path not available');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
// Parse body if it's a string
|
|
96
|
+
const bodyObj = typeof body === 'string' ? JSON.parse(body) : body;
|
|
97
|
+
// Generate schema file name from endpoint
|
|
98
|
+
const schemaFileName = (0, schemaGenerator_1.generateSchemaFileName)(method, url);
|
|
99
|
+
// Create schemas directory path
|
|
100
|
+
const sourceDir = path.dirname(this._sourceFilePath);
|
|
101
|
+
const schemasDir = path.join(sourceDir, 'schemas');
|
|
102
|
+
const schemaFilePath = path.join(schemasDir, schemaFileName);
|
|
103
|
+
const relativeSchemaPath = `./schemas/${schemaFileName}`;
|
|
104
|
+
// Generate schema from body
|
|
105
|
+
const schema = (0, schemaGenerator_1.generateJsonSchema)(bodyObj, `${method} ${url} response`);
|
|
106
|
+
// Save schema file
|
|
107
|
+
(0, schemaGenerator_1.saveSchema)(schema, schemaFilePath);
|
|
108
|
+
// Build the assertion line
|
|
109
|
+
const assertLine = `assert ${variableName}.body matchesSchema "${relativeSchemaPath}"`;
|
|
110
|
+
// Insert assertion at the end of the sequence in the source file
|
|
111
|
+
await this._insertAssertionInSequence(assertLine);
|
|
112
|
+
// Show success message
|
|
113
|
+
vscode.window.showInformationMessage(`Schema generated: ${relativeSchemaPath}`);
|
|
114
|
+
// Open the schema file for review
|
|
115
|
+
const schemaDoc = await vscode.workspace.openTextDocument(schemaFilePath);
|
|
116
|
+
await vscode.window.showTextDocument(schemaDoc, { viewColumn: vscode.ViewColumn.Beside, preview: true });
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
120
|
+
vscode.window.showErrorMessage(`Failed to generate schema: ${error}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Insert an assertion line at the end of the current sequence in the source file
|
|
125
|
+
*/
|
|
126
|
+
async _insertAssertionInSequence(assertLine) {
|
|
127
|
+
if (!this._sourceFilePath || !this._sequenceName) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const doc = await vscode.workspace.openTextDocument(this._sourceFilePath);
|
|
131
|
+
const text = doc.getText();
|
|
132
|
+
const lines = text.split('\n');
|
|
133
|
+
// Find the sequence by name
|
|
134
|
+
let sequenceStartLine = -1;
|
|
135
|
+
let sequenceEndLine = -1;
|
|
136
|
+
for (let i = 0; i < lines.length; i++) {
|
|
137
|
+
const line = lines[i].trim();
|
|
138
|
+
// Match sequence header (with optional tags)
|
|
139
|
+
if (line.match(new RegExp(`^(test\\s+)?sequence\\s+${this._escapeRegex(this._sequenceName)}(\\s|$)`))) {
|
|
140
|
+
sequenceStartLine = i;
|
|
141
|
+
// Find the end of the sequence (next sequence definition or end of file)
|
|
142
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
143
|
+
const nextLine = lines[j].trim();
|
|
144
|
+
// Only another sequence definition ends the current sequence
|
|
145
|
+
if (nextLine.match(/^(test\s+)?sequence\s+\S/)) {
|
|
146
|
+
// Found next sequence - current sequence ends before this
|
|
147
|
+
sequenceEndLine = j - 1;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (sequenceEndLine === -1) {
|
|
152
|
+
sequenceEndLine = lines.length - 1;
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (sequenceStartLine === -1) {
|
|
158
|
+
vscode.window.showWarningMessage(`Could not find sequence "${this._sequenceName}" to insert assertion`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Look for explicit "end sequence" marker within the sequence bounds
|
|
162
|
+
let endSequenceLine = -1;
|
|
163
|
+
for (let j = sequenceStartLine + 1; j <= sequenceEndLine; j++) {
|
|
164
|
+
if (lines[j].trim().match(/^end\s+sequence\s*$/i)) {
|
|
165
|
+
endSequenceLine = j;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let insertLine;
|
|
170
|
+
if (endSequenceLine !== -1) {
|
|
171
|
+
// Insert before "end sequence"
|
|
172
|
+
insertLine = endSequenceLine;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Find the last non-empty line within the sequence
|
|
176
|
+
insertLine = sequenceEndLine;
|
|
177
|
+
while (insertLine > sequenceStartLine && lines[insertLine].trim() === '') {
|
|
178
|
+
insertLine--;
|
|
179
|
+
}
|
|
180
|
+
// Insert after the last non-empty line
|
|
181
|
+
insertLine = insertLine + 1;
|
|
182
|
+
}
|
|
183
|
+
const insertPosition = new vscode.Position(insertLine, 0);
|
|
184
|
+
const edit = new vscode.WorkspaceEdit();
|
|
185
|
+
edit.insert(doc.uri, insertPosition, ` ${assertLine}\n`);
|
|
186
|
+
await vscode.workspace.applyEdit(edit);
|
|
187
|
+
// Save the document
|
|
188
|
+
await doc.save();
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Escape special regex characters in a string
|
|
192
|
+
*/
|
|
193
|
+
_escapeRegex(str) {
|
|
194
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Insert an assertion for a specific property value clicked in the response panel
|
|
198
|
+
*/
|
|
199
|
+
async _insertPropertyAssertion(path, value, variableName) {
|
|
200
|
+
// Format the value appropriately for the assertion
|
|
201
|
+
let valueString;
|
|
202
|
+
if (typeof value === 'string') {
|
|
203
|
+
valueString = `"${value.replace(/"/g, '\\"')}"`;
|
|
204
|
+
}
|
|
205
|
+
else if (value === null) {
|
|
206
|
+
valueString = 'null';
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
valueString = String(value);
|
|
210
|
+
}
|
|
211
|
+
const assertLine = `assert ${variableName}.${path} == ${valueString}`;
|
|
212
|
+
await this._insertAssertionInSequence(assertLine);
|
|
213
|
+
vscode.window.showInformationMessage(`Assertion added: ${path}`);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Open VS Code diff view comparing two response bodies
|
|
217
|
+
*/
|
|
218
|
+
async _openDiff(baseline, current) {
|
|
219
|
+
const provider = (0, compareContentProvider_1.getCompareContentProvider)();
|
|
220
|
+
// Pretty-print JSON if possible
|
|
221
|
+
const formatJson = (str) => {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = JSON.parse(str);
|
|
224
|
+
return JSON.stringify(parsed, null, 2);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return str;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const baselineUri = provider.setContent('baseline.json', formatJson(baseline));
|
|
231
|
+
const currentUri = provider.setContent('current.json', formatJson(current));
|
|
232
|
+
await vscode.commands.executeCommand('vscode.diff', baselineUri, currentUri, 'Response Comparison (Baseline ↔ Current)');
|
|
233
|
+
}
|
|
234
|
+
static show(extensionUri, method, url, response) {
|
|
235
|
+
const column = vscode.ViewColumn.Beside;
|
|
236
|
+
// If we already have a panel, show it
|
|
237
|
+
if (ResponsePanel.currentPanel) {
|
|
238
|
+
ResponsePanel.currentPanel._panel.reveal(column);
|
|
239
|
+
ResponsePanel.currentPanel._update(method, url, response);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// Otherwise, create a new panel
|
|
243
|
+
const panel = vscode.window.createWebviewPanel('nornResponse', 'Response', column, {
|
|
244
|
+
enableScripts: true,
|
|
245
|
+
retainContextWhenHidden: true,
|
|
246
|
+
});
|
|
247
|
+
ResponsePanel.currentPanel = new ResponsePanel(panel);
|
|
248
|
+
ResponsePanel.currentPanel._update(method, url, response);
|
|
249
|
+
}
|
|
250
|
+
static showError(extensionUri, method, url, errorMessage) {
|
|
251
|
+
const column = vscode.ViewColumn.Beside;
|
|
252
|
+
// If we already have a panel, show it
|
|
253
|
+
if (ResponsePanel.currentPanel) {
|
|
254
|
+
ResponsePanel.currentPanel._panel.reveal(column);
|
|
255
|
+
ResponsePanel.currentPanel._updateError(method, url, errorMessage);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Otherwise, create a new panel
|
|
259
|
+
const panel = vscode.window.createWebviewPanel('nornResponse', 'Error', column, {
|
|
260
|
+
enableScripts: true,
|
|
261
|
+
retainContextWhenHidden: true,
|
|
262
|
+
});
|
|
263
|
+
ResponsePanel.currentPanel = new ResponsePanel(panel);
|
|
264
|
+
ResponsePanel.currentPanel._updateError(method, url, errorMessage);
|
|
265
|
+
}
|
|
266
|
+
static showSequenceResult(extensionUri, result) {
|
|
267
|
+
const column = vscode.ViewColumn.Beside;
|
|
268
|
+
// If we already have a panel, show it
|
|
269
|
+
if (ResponsePanel.currentPanel) {
|
|
270
|
+
ResponsePanel.currentPanel._panel.reveal(column);
|
|
271
|
+
ResponsePanel.currentPanel._updateSequence(result);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Otherwise, create a new panel
|
|
275
|
+
const panel = vscode.window.createWebviewPanel('nornResponse', 'Sequence Result', column, {
|
|
276
|
+
enableScripts: true,
|
|
277
|
+
retainContextWhenHidden: true,
|
|
278
|
+
});
|
|
279
|
+
ResponsePanel.currentPanel = new ResponsePanel(panel);
|
|
280
|
+
ResponsePanel.currentPanel._updateSequence(result);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Show aggregated results for multiple runs of the same sequence (e.g. @data/@theory cases).
|
|
284
|
+
*/
|
|
285
|
+
static showMultiSequenceResult(extensionUri, sequenceName, runs) {
|
|
286
|
+
const column = vscode.ViewColumn.Beside;
|
|
287
|
+
const normalizedRuns = runs.map((run, index) => ({
|
|
288
|
+
label: run.label || `Case ${index + 1}`,
|
|
289
|
+
result: run.result
|
|
290
|
+
}));
|
|
291
|
+
// If we already have a panel, show it
|
|
292
|
+
if (ResponsePanel.currentPanel) {
|
|
293
|
+
ResponsePanel.currentPanel._panel.reveal(column);
|
|
294
|
+
ResponsePanel.currentPanel._updateMultiSequence(sequenceName, normalizedRuns);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// Otherwise, create a new panel
|
|
298
|
+
const panel = vscode.window.createWebviewPanel('nornResponse', 'Sequence Result', column, {
|
|
299
|
+
enableScripts: true,
|
|
300
|
+
retainContextWhenHidden: true,
|
|
301
|
+
});
|
|
302
|
+
ResponsePanel.currentPanel = new ResponsePanel(panel);
|
|
303
|
+
ResponsePanel.currentPanel._updateMultiSequence(sequenceName, normalizedRuns);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Start a sequence run - shows the panel with running state and prepares for incremental updates
|
|
307
|
+
*/
|
|
308
|
+
static startSequenceRun(extensionUri, sequenceName, totalSteps, sourceFilePath) {
|
|
309
|
+
const column = vscode.ViewColumn.Beside;
|
|
310
|
+
// If we already have a panel, reuse it
|
|
311
|
+
if (ResponsePanel.currentPanel) {
|
|
312
|
+
ResponsePanel.currentPanel._panel.reveal(column);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Create a new panel
|
|
316
|
+
const panel = vscode.window.createWebviewPanel('nornResponse', `Running: ${sequenceName}`, column, {
|
|
317
|
+
enableScripts: true,
|
|
318
|
+
retainContextWhenHidden: true,
|
|
319
|
+
});
|
|
320
|
+
ResponsePanel.currentPanel = new ResponsePanel(panel);
|
|
321
|
+
}
|
|
322
|
+
ResponsePanel.currentPanel._sequenceName = sequenceName;
|
|
323
|
+
ResponsePanel.currentPanel._totalSteps = totalSteps;
|
|
324
|
+
ResponsePanel.currentPanel._startTime = Date.now();
|
|
325
|
+
ResponsePanel.currentPanel._sourceFilePath = sourceFilePath || '';
|
|
326
|
+
ResponsePanel.currentPanel._panel.title = `⏳ ${sequenceName}`;
|
|
327
|
+
ResponsePanel.currentPanel._panel.webview.html = ResponsePanel.currentPanel._getStreamingSequenceHtml(sequenceName, totalSteps);
|
|
328
|
+
// Send reset message to clear any previous state (handles retainContextWhenHidden)
|
|
329
|
+
ResponsePanel.currentPanel._panel.webview.postMessage({
|
|
330
|
+
type: 'reset',
|
|
331
|
+
totalSteps: totalSteps
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Add a step result to the running sequence display
|
|
336
|
+
*/
|
|
337
|
+
static addSequenceStep(stepResult, progressInfo) {
|
|
338
|
+
if (!ResponsePanel.currentPanel) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Send the step to the webview via postMessage
|
|
342
|
+
ResponsePanel.currentPanel._panel.webview.postMessage({
|
|
343
|
+
type: 'addStep',
|
|
344
|
+
step: stepResult,
|
|
345
|
+
progress: progressInfo,
|
|
346
|
+
elapsed: Date.now() - ResponsePanel.currentPanel._startTime
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Finalize the sequence run - update header to show success/failure
|
|
351
|
+
*/
|
|
352
|
+
static finalizeSequence(result) {
|
|
353
|
+
if (!ResponsePanel.currentPanel) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
ResponsePanel.currentPanel._panel.title = `${result.success ? '✓' : '✗'} ${result.name}`;
|
|
357
|
+
ResponsePanel.currentPanel._panel.webview.postMessage({
|
|
358
|
+
type: 'finalize',
|
|
359
|
+
success: result.success,
|
|
360
|
+
errors: result.errors,
|
|
361
|
+
duration: result.duration
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Show the Contract View for schema validation errors
|
|
366
|
+
* Provides a rich UI for navigating validation failures
|
|
367
|
+
*/
|
|
368
|
+
static showContractView(extensionUri, data) {
|
|
369
|
+
const column = vscode.ViewColumn.Beside;
|
|
370
|
+
// If we already have a panel, reuse it
|
|
371
|
+
if (ResponsePanel.currentPanel) {
|
|
372
|
+
ResponsePanel.currentPanel._panel.reveal(column);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
// Create a new panel
|
|
376
|
+
const panel = vscode.window.createWebviewPanel('nornResponse', 'Contract View', column, {
|
|
377
|
+
enableScripts: true,
|
|
378
|
+
retainContextWhenHidden: true,
|
|
379
|
+
});
|
|
380
|
+
ResponsePanel.currentPanel = new ResponsePanel(panel);
|
|
381
|
+
}
|
|
382
|
+
const errorCount = data.errors.length;
|
|
383
|
+
const schemaName = path.basename(data.schemaPath);
|
|
384
|
+
ResponsePanel.currentPanel._panel.title = `⚠ ${errorCount} violation${errorCount !== 1 ? 's' : ''} - ${schemaName}`;
|
|
385
|
+
ResponsePanel.currentPanel._sourceFilePath = data.sourceFile || '';
|
|
386
|
+
ResponsePanel.currentPanel._panel.webview.html = ResponsePanel.currentPanel._getContractViewHtml(data);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Show the Contract Report - a visual comparison of response against schema
|
|
390
|
+
* Shows matched properties with checkmarks and highlights differences
|
|
391
|
+
*/
|
|
392
|
+
static showContractReport(extensionUri, data) {
|
|
393
|
+
const column = vscode.ViewColumn.Beside;
|
|
394
|
+
// If we already have a panel, reuse it
|
|
395
|
+
if (ResponsePanel.currentPanel) {
|
|
396
|
+
ResponsePanel.currentPanel._panel.reveal(column);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
// Create a new panel
|
|
400
|
+
const panel = vscode.window.createWebviewPanel('nornResponse', 'Contract Report', column, {
|
|
401
|
+
enableScripts: true,
|
|
402
|
+
retainContextWhenHidden: true,
|
|
403
|
+
});
|
|
404
|
+
ResponsePanel.currentPanel = new ResponsePanel(panel);
|
|
405
|
+
}
|
|
406
|
+
const errorCount = data.errors.length;
|
|
407
|
+
const schemaName = path.basename(data.schemaPath);
|
|
408
|
+
const titleIcon = errorCount === 0 ? '✓' : '⚠';
|
|
409
|
+
const titleText = errorCount === 0 ? 'All OK' : `${errorCount} issue${errorCount !== 1 ? 's' : ''}`;
|
|
410
|
+
ResponsePanel.currentPanel._panel.title = `${titleIcon} ${titleText} - ${schemaName}`;
|
|
411
|
+
ResponsePanel.currentPanel._sourceFilePath = data.sourceFile || '';
|
|
412
|
+
ResponsePanel.currentPanel._panel.webview.html = ResponsePanel.currentPanel._getContractReportHtml(data);
|
|
413
|
+
}
|
|
414
|
+
_update(method, url, response) {
|
|
415
|
+
this._panel.title = `${response.status} ${method} Response`;
|
|
416
|
+
this._panel.webview.html = this._getHtmlContent(method, url, response);
|
|
417
|
+
}
|
|
418
|
+
_updateSequence(result) {
|
|
419
|
+
this._panel.title = `${result.success ? '✓' : '✗'} ${result.name}`;
|
|
420
|
+
this._panel.webview.html = this._getSequenceHtmlContent(result);
|
|
421
|
+
}
|
|
422
|
+
_updateMultiSequence(sequenceName, runs) {
|
|
423
|
+
const passedRuns = runs.filter(r => r.result.success).length;
|
|
424
|
+
const allPassed = passedRuns === runs.length;
|
|
425
|
+
this._panel.title = `${allPassed ? '✓' : '✗'} ${sequenceName} (${runs.length} runs)`;
|
|
426
|
+
this._panel.webview.html = this._getMultiSequenceHtmlContent(sequenceName, runs);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Get the HTML for streaming sequence results
|
|
430
|
+
*/
|
|
431
|
+
_getStreamingSequenceHtml(sequenceName, totalSteps) {
|
|
432
|
+
return /*html*/ `
|
|
433
|
+
<!DOCTYPE html>
|
|
434
|
+
<html lang="en">
|
|
435
|
+
<head>
|
|
436
|
+
<meta charset="UTF-8">
|
|
437
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
438
|
+
<title>Sequence Result</title>
|
|
439
|
+
<style>
|
|
440
|
+
* { box-sizing: border-box; }
|
|
441
|
+
|
|
442
|
+
body {
|
|
443
|
+
font-family: var(--vscode-font-family);
|
|
444
|
+
font-size: var(--vscode-font-size);
|
|
445
|
+
color: var(--vscode-foreground);
|
|
446
|
+
background-color: var(--vscode-editor-background);
|
|
447
|
+
padding: 0;
|
|
448
|
+
margin: 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.container { padding: 16px; }
|
|
452
|
+
|
|
453
|
+
.sequence-header {
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
gap: 16px;
|
|
457
|
+
padding: 16px;
|
|
458
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
459
|
+
border-radius: 6px;
|
|
460
|
+
margin-bottom: 16px;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.sequence-name {
|
|
464
|
+
font-size: 1.2em;
|
|
465
|
+
font-weight: bold;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.sequence-status {
|
|
469
|
+
padding: 4px 12px;
|
|
470
|
+
border-radius: 4px;
|
|
471
|
+
font-weight: bold;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.status-running { background: #569cd6; color: #fff; }
|
|
475
|
+
.status-success { background: #4ec9b0; color: #000; }
|
|
476
|
+
.status-warning { background: #cca700; color: #000; }
|
|
477
|
+
.status-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
|
|
478
|
+
.status-redirect { background: #dcdcaa; color: #000; }
|
|
479
|
+
.status-client-error { background: #ce9178; color: #000; }
|
|
480
|
+
.status-server-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
|
|
481
|
+
|
|
482
|
+
.meta { color: var(--vscode-descriptionForeground); }
|
|
483
|
+
|
|
484
|
+
.progress-info {
|
|
485
|
+
display: flex;
|
|
486
|
+
align-items: center;
|
|
487
|
+
gap: 8px;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.spinner {
|
|
491
|
+
width: 16px;
|
|
492
|
+
height: 16px;
|
|
493
|
+
border: 2px solid var(--vscode-descriptionForeground);
|
|
494
|
+
border-top-color: #569cd6;
|
|
495
|
+
border-radius: 50%;
|
|
496
|
+
animation: spin 1s linear infinite;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
@keyframes spin {
|
|
500
|
+
to { transform: rotate(360deg); }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* Contract View button for schema validation failures */
|
|
504
|
+
.contract-view-actions {
|
|
505
|
+
margin-top: 12px;
|
|
506
|
+
}
|
|
507
|
+
.contract-view-btn {
|
|
508
|
+
padding: 8px 16px;
|
|
509
|
+
font-size: 13px;
|
|
510
|
+
font-weight: 500;
|
|
511
|
+
border: 1px solid var(--vscode-button-border, var(--vscode-focusBorder));
|
|
512
|
+
background-color: var(--vscode-button-secondaryBackground);
|
|
513
|
+
color: var(--vscode-button-secondaryForeground);
|
|
514
|
+
border-radius: 4px;
|
|
515
|
+
cursor: pointer;
|
|
516
|
+
transition: background-color 0.15s;
|
|
517
|
+
}
|
|
518
|
+
.contract-view-btn:hover {
|
|
519
|
+
background-color: var(--vscode-button-secondaryHoverBackground);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.response-item {
|
|
523
|
+
border: 1px solid var(--vscode-panel-border);
|
|
524
|
+
border-radius: 6px;
|
|
525
|
+
margin-bottom: 8px;
|
|
526
|
+
overflow: hidden;
|
|
527
|
+
animation: slideIn 0.2s ease-out;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
@keyframes slideIn {
|
|
531
|
+
from {
|
|
532
|
+
opacity: 0;
|
|
533
|
+
transform: translateY(-10px);
|
|
534
|
+
}
|
|
535
|
+
to {
|
|
536
|
+
opacity: 1;
|
|
537
|
+
transform: translateY(0);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.response-header {
|
|
542
|
+
display: flex;
|
|
543
|
+
align-items: center;
|
|
544
|
+
gap: 12px;
|
|
545
|
+
padding: 12px;
|
|
546
|
+
cursor: pointer;
|
|
547
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.response-header:hover {
|
|
551
|
+
background: var(--vscode-list-hoverBackground);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.response-index {
|
|
555
|
+
font-family: var(--vscode-editor-font-family);
|
|
556
|
+
font-weight: bold;
|
|
557
|
+
color: var(--vscode-symbolIcon-variableForeground, #9cdcfe);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.status-code {
|
|
561
|
+
padding: 2px 8px;
|
|
562
|
+
border-radius: 3px;
|
|
563
|
+
font-size: 0.9em;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.expand-icon {
|
|
567
|
+
margin-left: auto;
|
|
568
|
+
transition: transform 0.2s;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.expand-icon.expanded {
|
|
572
|
+
transform: rotate(90deg);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.response-body {
|
|
576
|
+
padding: 12px;
|
|
577
|
+
border-top: 1px solid var(--vscode-panel-border);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
pre {
|
|
581
|
+
margin: 0;
|
|
582
|
+
background: transparent;
|
|
583
|
+
font-family: var(--vscode-editor-font-family);
|
|
584
|
+
font-size: var(--vscode-editor-font-size);
|
|
585
|
+
line-height: 1.5;
|
|
586
|
+
overflow-x: auto;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
pre code { background: none; }
|
|
590
|
+
|
|
591
|
+
.errors-section {
|
|
592
|
+
margin-top: 16px;
|
|
593
|
+
padding: 12px 16px;
|
|
594
|
+
background: linear-gradient(135deg, rgba(201, 85, 90, 0.2) 0%, rgba(168, 67, 71, 0.15) 100%);
|
|
595
|
+
border-radius: 6px;
|
|
596
|
+
border-left: 3px solid #c9555a;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.errors-section h3 { margin-top: 0; color: var(--vscode-foreground); }
|
|
600
|
+
.errors-section ul { margin: 0; padding-left: 20px; }
|
|
601
|
+
.errors-section .error { color: var(--vscode-foreground); white-space: pre-wrap; line-height: 1.5; }
|
|
602
|
+
.errors-section .warning { color: #cca700; }
|
|
603
|
+
|
|
604
|
+
/* JSON Syntax Highlighting */
|
|
605
|
+
.json-key { color: #9cdcfe; background: none; }
|
|
606
|
+
.json-string { color: #ce9178; background: none; }
|
|
607
|
+
.json-number { color: #b5cea8; background: none; }
|
|
608
|
+
.json-boolean { color: #569cd6; background: none; }
|
|
609
|
+
.json-null { color: #569cd6; background: none; }
|
|
610
|
+
|
|
611
|
+
/* Clickable JSON values */
|
|
612
|
+
.json-clickable {
|
|
613
|
+
cursor: pointer;
|
|
614
|
+
border-radius: 3px;
|
|
615
|
+
padding: 1px 3px;
|
|
616
|
+
margin: -1px -3px;
|
|
617
|
+
position: relative;
|
|
618
|
+
transition: background-color 0.15s, box-shadow 0.15s;
|
|
619
|
+
}
|
|
620
|
+
.json-clickable:hover {
|
|
621
|
+
background: var(--vscode-editor-selectionBackground);
|
|
622
|
+
box-shadow: 0 0 0 1px var(--vscode-focusBorder);
|
|
623
|
+
}
|
|
624
|
+
.json-clickable::after {
|
|
625
|
+
content: '+';
|
|
626
|
+
position: absolute;
|
|
627
|
+
right: -16px;
|
|
628
|
+
top: 50%;
|
|
629
|
+
transform: translateY(-50%);
|
|
630
|
+
font-size: 10px;
|
|
631
|
+
font-weight: bold;
|
|
632
|
+
color: var(--vscode-button-background);
|
|
633
|
+
background: var(--vscode-button-foreground);
|
|
634
|
+
border-radius: 50%;
|
|
635
|
+
width: 14px;
|
|
636
|
+
height: 14px;
|
|
637
|
+
line-height: 14px;
|
|
638
|
+
text-align: center;
|
|
639
|
+
opacity: 0;
|
|
640
|
+
transition: opacity 0.15s;
|
|
641
|
+
}
|
|
642
|
+
.json-clickable:hover::after {
|
|
643
|
+
opacity: 1;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/* Script Result Styling */
|
|
647
|
+
.script-item { border-left: 3px solid #dcdcaa; }
|
|
648
|
+
.script-icon { font-size: 1.1em; }
|
|
649
|
+
.script-type {
|
|
650
|
+
font-family: var(--vscode-editor-font-family);
|
|
651
|
+
padding: 2px 8px;
|
|
652
|
+
background: #dcdcaa22;
|
|
653
|
+
border-radius: 3px;
|
|
654
|
+
color: #dcdcaa;
|
|
655
|
+
font-size: 0.85em;
|
|
656
|
+
text-transform: uppercase;
|
|
657
|
+
}
|
|
658
|
+
.script-path {
|
|
659
|
+
font-family: var(--vscode-editor-font-family);
|
|
660
|
+
color: var(--vscode-descriptionForeground);
|
|
661
|
+
}
|
|
662
|
+
.capture-var {
|
|
663
|
+
font-family: var(--vscode-editor-font-family);
|
|
664
|
+
color: #9cdcfe;
|
|
665
|
+
font-weight: bold;
|
|
666
|
+
}
|
|
667
|
+
.script-output {
|
|
668
|
+
margin-bottom: 12px;
|
|
669
|
+
}
|
|
670
|
+
.script-output:last-child { margin-bottom: 0; }
|
|
671
|
+
.output-label {
|
|
672
|
+
font-size: 0.85em;
|
|
673
|
+
color: var(--vscode-descriptionForeground);
|
|
674
|
+
margin-bottom: 4px;
|
|
675
|
+
text-transform: uppercase;
|
|
676
|
+
}
|
|
677
|
+
.script-output.stderr .output-label { color: #f14c4c; }
|
|
678
|
+
.script-output.stderr pre { color: #f14c4c; }
|
|
679
|
+
|
|
680
|
+
.step-number {
|
|
681
|
+
font-family: var(--vscode-editor-font-family);
|
|
682
|
+
font-weight: bold;
|
|
683
|
+
color: var(--vscode-descriptionForeground);
|
|
684
|
+
min-width: 24px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/* Print styling */
|
|
688
|
+
.print-item {
|
|
689
|
+
border-left: 3px solid #569cd6;
|
|
690
|
+
}
|
|
691
|
+
.print-item .response-header {
|
|
692
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
693
|
+
}
|
|
694
|
+
.print-content {
|
|
695
|
+
display: flex;
|
|
696
|
+
align-items: center;
|
|
697
|
+
gap: 12px;
|
|
698
|
+
padding: 12px;
|
|
699
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
700
|
+
}
|
|
701
|
+
.print-icon {
|
|
702
|
+
font-size: 1em;
|
|
703
|
+
}
|
|
704
|
+
.print-title {
|
|
705
|
+
font-family: var(--vscode-editor-font-family);
|
|
706
|
+
color: #569cd6;
|
|
707
|
+
flex: 1;
|
|
708
|
+
}
|
|
709
|
+
.print-body {
|
|
710
|
+
background: var(--vscode-editor-background);
|
|
711
|
+
}
|
|
712
|
+
.print-body pre {
|
|
713
|
+
margin: 0;
|
|
714
|
+
white-space: pre-wrap;
|
|
715
|
+
word-wrap: break-word;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/* Assertion styling */
|
|
719
|
+
.assertion-item {
|
|
720
|
+
border-left: 3px solid var(--vscode-descriptionForeground);
|
|
721
|
+
}
|
|
722
|
+
.assertion-item.assertion-passed {
|
|
723
|
+
border-left-color: #4ec9b0;
|
|
724
|
+
}
|
|
725
|
+
.assertion-item.assertion-failed {
|
|
726
|
+
border-left-color: #f14c4c;
|
|
727
|
+
}
|
|
728
|
+
.assertion-icon {
|
|
729
|
+
font-size: 1.1em;
|
|
730
|
+
font-weight: bold;
|
|
731
|
+
}
|
|
732
|
+
.assertion-icon.status-success { color: #4ec9b0; }
|
|
733
|
+
.assertion-icon.status-error { color: #f14c4c; }
|
|
734
|
+
.assertion-expr {
|
|
735
|
+
font-family: var(--vscode-editor-font-family);
|
|
736
|
+
flex: 1;
|
|
737
|
+
}
|
|
738
|
+
.assertion-expr code {
|
|
739
|
+
background: var(--vscode-textCodeBlock-background);
|
|
740
|
+
padding: 2px 6px;
|
|
741
|
+
border-radius: 3px;
|
|
742
|
+
}
|
|
743
|
+
.assertion-body {
|
|
744
|
+
padding: 12px 16px;
|
|
745
|
+
background: var(--vscode-editor-background);
|
|
746
|
+
}
|
|
747
|
+
.assertion-message {
|
|
748
|
+
color: var(--vscode-descriptionForeground);
|
|
749
|
+
margin-bottom: 8px;
|
|
750
|
+
font-style: italic;
|
|
751
|
+
}
|
|
752
|
+
.assertion-details {
|
|
753
|
+
font-family: var(--vscode-editor-font-family);
|
|
754
|
+
font-size: 0.9em;
|
|
755
|
+
}
|
|
756
|
+
.assertion-expected, .assertion-actual {
|
|
757
|
+
margin: 4px 0;
|
|
758
|
+
}
|
|
759
|
+
.assertion-expected code, .assertion-actual code {
|
|
760
|
+
background: var(--vscode-textCodeBlock-background);
|
|
761
|
+
padding: 2px 6px;
|
|
762
|
+
border-radius: 3px;
|
|
763
|
+
}
|
|
764
|
+
.assertion-error {
|
|
765
|
+
color: #f14c4c;
|
|
766
|
+
font-family: var(--vscode-editor-font-family);
|
|
767
|
+
}
|
|
768
|
+
.response-header.no-expand {
|
|
769
|
+
cursor: default;
|
|
770
|
+
}
|
|
771
|
+
.response-header.no-expand:hover {
|
|
772
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/* Inline response preview for failed assertions */
|
|
776
|
+
.assertion-response-preview {
|
|
777
|
+
margin-top: 12px;
|
|
778
|
+
border: 1px solid var(--vscode-panel-border);
|
|
779
|
+
border-radius: 4px;
|
|
780
|
+
overflow: hidden;
|
|
781
|
+
}
|
|
782
|
+
.assertion-response-preview .preview-header {
|
|
783
|
+
display: flex;
|
|
784
|
+
align-items: center;
|
|
785
|
+
gap: 8px;
|
|
786
|
+
padding: 8px 12px;
|
|
787
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
788
|
+
cursor: pointer;
|
|
789
|
+
font-family: var(--vscode-editor-font-family);
|
|
790
|
+
font-size: 0.9em;
|
|
791
|
+
}
|
|
792
|
+
.assertion-response-preview .preview-header:hover {
|
|
793
|
+
background: var(--vscode-list-hoverBackground);
|
|
794
|
+
}
|
|
795
|
+
.assertion-response-preview .preview-body {
|
|
796
|
+
max-height: 300px;
|
|
797
|
+
overflow: auto;
|
|
798
|
+
background: var(--vscode-editor-background);
|
|
799
|
+
padding: 8px 12px;
|
|
800
|
+
}
|
|
801
|
+
.assertion-response-preview .preview-body pre {
|
|
802
|
+
margin: 0;
|
|
803
|
+
font-size: 0.85em;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/* JSON path highlighting */
|
|
807
|
+
.json-highlighted {
|
|
808
|
+
background: rgba(255, 200, 0, 0.25);
|
|
809
|
+
border-radius: 2px;
|
|
810
|
+
padding: 1px 2px;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.step-description {
|
|
814
|
+
color: var(--vscode-descriptionForeground);
|
|
815
|
+
font-family: var(--vscode-editor-font-family);
|
|
816
|
+
font-size: 0.9em;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/* Sub-sequence grouping */
|
|
820
|
+
.sequence-group {
|
|
821
|
+
border: 1px solid var(--vscode-panel-border);
|
|
822
|
+
border-radius: 6px;
|
|
823
|
+
margin-bottom: 8px;
|
|
824
|
+
overflow: hidden;
|
|
825
|
+
animation: slideIn 0.2s ease-out;
|
|
826
|
+
}
|
|
827
|
+
.sequence-group-header {
|
|
828
|
+
display: flex;
|
|
829
|
+
align-items: center;
|
|
830
|
+
gap: 12px;
|
|
831
|
+
padding: 10px 12px;
|
|
832
|
+
cursor: pointer;
|
|
833
|
+
background: linear-gradient(135deg, rgba(86, 156, 214, 0.15) 0%, rgba(78, 201, 176, 0.1) 100%);
|
|
834
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
835
|
+
width: 100%;
|
|
836
|
+
box-sizing: border-box;
|
|
837
|
+
}
|
|
838
|
+
.sequence-group-header .expand-icon {
|
|
839
|
+
margin-left: 0;
|
|
840
|
+
}
|
|
841
|
+
.sequence-group-header:hover {
|
|
842
|
+
background: linear-gradient(135deg, rgba(86, 156, 214, 0.25) 0%, rgba(78, 201, 176, 0.15) 100%);
|
|
843
|
+
}
|
|
844
|
+
.sequence-group-icon {
|
|
845
|
+
font-size: 1em;
|
|
846
|
+
}
|
|
847
|
+
.sequence-group-name {
|
|
848
|
+
font-family: var(--vscode-editor-font-family);
|
|
849
|
+
font-weight: bold;
|
|
850
|
+
color: #569cd6;
|
|
851
|
+
}
|
|
852
|
+
.sequence-group-status {
|
|
853
|
+
font-size: 0.85em;
|
|
854
|
+
padding: 2px 8px;
|
|
855
|
+
border-radius: 3px;
|
|
856
|
+
background: rgba(86, 156, 214, 0.2);
|
|
857
|
+
color: #569cd6;
|
|
858
|
+
margin-left: auto;
|
|
859
|
+
}
|
|
860
|
+
.sequence-group-status.running {
|
|
861
|
+
background: rgba(86, 156, 214, 0.2);
|
|
862
|
+
color: #569cd6;
|
|
863
|
+
}
|
|
864
|
+
.sequence-group-status.completed {
|
|
865
|
+
background: rgba(78, 201, 176, 0.2);
|
|
866
|
+
color: #4ec9b0;
|
|
867
|
+
}
|
|
868
|
+
.sequence-group-content {
|
|
869
|
+
padding: 8px;
|
|
870
|
+
background: var(--vscode-editor-background);
|
|
871
|
+
}
|
|
872
|
+
.sequence-group-content .response-item {
|
|
873
|
+
margin-bottom: 6px;
|
|
874
|
+
}
|
|
875
|
+
.sequence-group-content .response-item:last-child {
|
|
876
|
+
margin-bottom: 0;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.toggle-all-btn {
|
|
880
|
+
background: var(--vscode-button-secondaryBackground);
|
|
881
|
+
color: var(--vscode-button-secondaryForeground);
|
|
882
|
+
border: none;
|
|
883
|
+
padding: 4px 10px;
|
|
884
|
+
border-radius: 4px;
|
|
885
|
+
cursor: pointer;
|
|
886
|
+
font-size: 0.85em;
|
|
887
|
+
margin-left: auto;
|
|
888
|
+
}
|
|
889
|
+
.toggle-all-btn:hover {
|
|
890
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/* Sequence Request Details */
|
|
894
|
+
.seq-request-url {
|
|
895
|
+
padding: 6px 12px;
|
|
896
|
+
background: var(--vscode-textCodeBlock-background);
|
|
897
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
898
|
+
font-family: var(--vscode-editor-font-family);
|
|
899
|
+
font-size: 0.9em;
|
|
900
|
+
color: var(--vscode-foreground);
|
|
901
|
+
display: flex;
|
|
902
|
+
align-items: center;
|
|
903
|
+
gap: 8px;
|
|
904
|
+
}
|
|
905
|
+
.seq-request-url .method {
|
|
906
|
+
font-weight: bold;
|
|
907
|
+
padding: 2px 6px;
|
|
908
|
+
border-radius: 3px;
|
|
909
|
+
font-size: 0.85em;
|
|
910
|
+
}
|
|
911
|
+
.seq-request-url .method.GET { background: #4ec9b022; color: #4ec9b0; }
|
|
912
|
+
.seq-request-url .method.POST { background: #dcdcaa22; color: #dcdcaa; }
|
|
913
|
+
.seq-request-url .method.PUT { background: #569cd622; color: #569cd6; }
|
|
914
|
+
.seq-request-url .method.PATCH { background: #c586c022; color: #c586c0; }
|
|
915
|
+
.seq-request-url .method.DELETE { background: #f14c4c22; color: #f14c4c; }
|
|
916
|
+
.seq-tabs {
|
|
917
|
+
display: flex;
|
|
918
|
+
gap: 0;
|
|
919
|
+
padding: 0 12px;
|
|
920
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
921
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
922
|
+
}
|
|
923
|
+
.seq-tab {
|
|
924
|
+
padding: 6px 12px;
|
|
925
|
+
background: transparent;
|
|
926
|
+
border: none;
|
|
927
|
+
color: var(--vscode-foreground);
|
|
928
|
+
cursor: pointer;
|
|
929
|
+
font-size: 0.85em;
|
|
930
|
+
border-bottom: 2px solid transparent;
|
|
931
|
+
transition: all 0.15s;
|
|
932
|
+
}
|
|
933
|
+
.seq-tab:hover {
|
|
934
|
+
background: var(--vscode-list-hoverBackground);
|
|
935
|
+
}
|
|
936
|
+
.seq-tab.active {
|
|
937
|
+
border-bottom-color: var(--vscode-focusBorder);
|
|
938
|
+
color: var(--vscode-textLink-foreground);
|
|
939
|
+
}
|
|
940
|
+
.seq-tab-content {
|
|
941
|
+
display: none;
|
|
942
|
+
padding: 12px;
|
|
943
|
+
}
|
|
944
|
+
.seq-tab-content.active {
|
|
945
|
+
display: block;
|
|
946
|
+
}
|
|
947
|
+
.seq-body-container {
|
|
948
|
+
position: relative;
|
|
949
|
+
}
|
|
950
|
+
.seq-copy-btn {
|
|
951
|
+
position: absolute;
|
|
952
|
+
top: 0;
|
|
953
|
+
right: 0;
|
|
954
|
+
background: var(--vscode-button-secondaryBackground);
|
|
955
|
+
color: var(--vscode-button-secondaryForeground);
|
|
956
|
+
border: none;
|
|
957
|
+
padding: 4px 8px;
|
|
958
|
+
cursor: pointer;
|
|
959
|
+
font-size: 0.8em;
|
|
960
|
+
border-radius: 3px;
|
|
961
|
+
z-index: 1;
|
|
962
|
+
}
|
|
963
|
+
.seq-copy-btn:hover {
|
|
964
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
965
|
+
}
|
|
966
|
+
.seq-compare-btn {
|
|
967
|
+
position: absolute;
|
|
968
|
+
top: 0;
|
|
969
|
+
right: 60px;
|
|
970
|
+
background: var(--vscode-button-secondaryBackground);
|
|
971
|
+
color: var(--vscode-button-secondaryForeground);
|
|
972
|
+
border: none;
|
|
973
|
+
padding: 4px 8px;
|
|
974
|
+
cursor: pointer;
|
|
975
|
+
font-size: 0.8em;
|
|
976
|
+
border-radius: 3px;
|
|
977
|
+
z-index: 1;
|
|
978
|
+
}
|
|
979
|
+
.seq-compare-btn:hover {
|
|
980
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
981
|
+
}
|
|
982
|
+
.seq-compare-btn.comparing {
|
|
983
|
+
background: var(--vscode-inputValidation-infoBackground);
|
|
984
|
+
border: 1px solid var(--vscode-inputValidation-infoBorder);
|
|
985
|
+
}
|
|
986
|
+
.norn-toast {
|
|
987
|
+
position: fixed;
|
|
988
|
+
bottom: 20px;
|
|
989
|
+
left: 50%;
|
|
990
|
+
transform: translateX(-50%);
|
|
991
|
+
background: var(--vscode-notifications-background);
|
|
992
|
+
color: var(--vscode-notifications-foreground);
|
|
993
|
+
border: 1px solid var(--vscode-notifications-border);
|
|
994
|
+
padding: 10px 20px;
|
|
995
|
+
border-radius: 4px;
|
|
996
|
+
z-index: 1000;
|
|
997
|
+
opacity: 0;
|
|
998
|
+
transition: opacity 0.3s;
|
|
999
|
+
pointer-events: none;
|
|
1000
|
+
}
|
|
1001
|
+
.norn-toast.visible {
|
|
1002
|
+
opacity: 1;
|
|
1003
|
+
}
|
|
1004
|
+
.seq-schema-btn {
|
|
1005
|
+
position: absolute;
|
|
1006
|
+
top: 0;
|
|
1007
|
+
right: 140px;
|
|
1008
|
+
background: var(--vscode-button-secondaryBackground);
|
|
1009
|
+
color: var(--vscode-button-secondaryForeground);
|
|
1010
|
+
border: none;
|
|
1011
|
+
padding: 4px 8px;
|
|
1012
|
+
cursor: pointer;
|
|
1013
|
+
font-size: 0.8em;
|
|
1014
|
+
border-radius: 3px;
|
|
1015
|
+
z-index: 1;
|
|
1016
|
+
}
|
|
1017
|
+
.seq-schema-btn:hover {
|
|
1018
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
1019
|
+
}
|
|
1020
|
+
.seq-headers-table,
|
|
1021
|
+
.seq-cookies-table {
|
|
1022
|
+
width: 100%;
|
|
1023
|
+
border-collapse: collapse;
|
|
1024
|
+
font-family: var(--vscode-editor-font-family);
|
|
1025
|
+
font-size: 0.9em;
|
|
1026
|
+
}
|
|
1027
|
+
.seq-headers-table th,
|
|
1028
|
+
.seq-headers-table td,
|
|
1029
|
+
.seq-cookies-table th,
|
|
1030
|
+
.seq-cookies-table td {
|
|
1031
|
+
padding: 6px 8px;
|
|
1032
|
+
text-align: left;
|
|
1033
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
1034
|
+
}
|
|
1035
|
+
.seq-headers-table th,
|
|
1036
|
+
.seq-cookies-table th {
|
|
1037
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
1038
|
+
color: var(--vscode-descriptionForeground);
|
|
1039
|
+
font-weight: normal;
|
|
1040
|
+
font-size: 0.85em;
|
|
1041
|
+
}
|
|
1042
|
+
.seq-no-data {
|
|
1043
|
+
color: var(--vscode-descriptionForeground);
|
|
1044
|
+
font-style: italic;
|
|
1045
|
+
padding: 12px;
|
|
1046
|
+
}
|
|
1047
|
+
</style>
|
|
1048
|
+
</head>
|
|
1049
|
+
<body>
|
|
1050
|
+
<div class="container">
|
|
1051
|
+
<div class="sequence-header" id="sequence-header">
|
|
1052
|
+
<span class="sequence-status status-running" id="status-badge">RUNNING</span>
|
|
1053
|
+
<span class="sequence-name">${this._escapeHtml(sequenceName)}</span>
|
|
1054
|
+
<span class="meta" id="step-count">0/${totalSteps} steps</span>
|
|
1055
|
+
<span class="meta" id="elapsed-time">⏱ 0ms</span>
|
|
1056
|
+
<button class="toggle-all-btn" id="toggle-all-btn" onclick="toggleAll()">Expand All</button>
|
|
1057
|
+
<div class="progress-info" id="spinner">
|
|
1058
|
+
<div class="spinner"></div>
|
|
1059
|
+
</div>
|
|
1060
|
+
</div>
|
|
1061
|
+
|
|
1062
|
+
<div class="steps-list" id="steps-list">
|
|
1063
|
+
<!-- Steps will be added here dynamically -->
|
|
1064
|
+
</div>
|
|
1065
|
+
|
|
1066
|
+
<div class="errors-section" id="errors-section" style="display: none;">
|
|
1067
|
+
<h3>Errors & Warnings</h3>
|
|
1068
|
+
<ul id="errors-list"></ul>
|
|
1069
|
+
</div>
|
|
1070
|
+
|
|
1071
|
+
<div id="norn-toast" class="norn-toast"></div>
|
|
1072
|
+
</div>
|
|
1073
|
+
|
|
1074
|
+
<script>
|
|
1075
|
+
const vscode = acquireVsCodeApi();
|
|
1076
|
+
let stepCount = 0;
|
|
1077
|
+
let requestCounter = 0;
|
|
1078
|
+
let allExpanded = false;
|
|
1079
|
+
const totalSteps = ${totalSteps};
|
|
1080
|
+
|
|
1081
|
+
// Track sequence group stack for nested sequences
|
|
1082
|
+
let sequenceStack = [];
|
|
1083
|
+
let sequenceGroupCounter = 0;
|
|
1084
|
+
|
|
1085
|
+
// Response comparison state
|
|
1086
|
+
let compareBaseline = null;
|
|
1087
|
+
|
|
1088
|
+
function showToast(message) {
|
|
1089
|
+
const toast = document.getElementById('norn-toast');
|
|
1090
|
+
toast.textContent = message;
|
|
1091
|
+
toast.classList.add('visible');
|
|
1092
|
+
setTimeout(() => toast.classList.remove('visible'), 3000);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function compareResponse(stepId) {
|
|
1096
|
+
const bodyEl = document.getElementById(stepId + '-body-text');
|
|
1097
|
+
if (!bodyEl) return;
|
|
1098
|
+
|
|
1099
|
+
const body = bodyEl.value || bodyEl.textContent || '';
|
|
1100
|
+
|
|
1101
|
+
if (!compareBaseline) {
|
|
1102
|
+
// First click - set baseline
|
|
1103
|
+
compareBaseline = { stepId, body };
|
|
1104
|
+
showToast('Select another response to compare');
|
|
1105
|
+
// Highlight the button
|
|
1106
|
+
document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
|
|
1107
|
+
const btn = document.querySelector('[data-compare-step="' + stepId + '"]');
|
|
1108
|
+
if (btn) btn.classList.add('comparing');
|
|
1109
|
+
} else {
|
|
1110
|
+
// Second click - send compare request
|
|
1111
|
+
vscode.postMessage({ type: 'setBaseline', body: compareBaseline.body });
|
|
1112
|
+
vscode.postMessage({ type: 'compare', body: body });
|
|
1113
|
+
compareBaseline = null;
|
|
1114
|
+
document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Listen for reset compare message from extension
|
|
1119
|
+
window.addEventListener('message', event => {
|
|
1120
|
+
const message = event.data;
|
|
1121
|
+
if (message.type === 'resetCompare') {
|
|
1122
|
+
compareBaseline = null;
|
|
1123
|
+
document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
function assertSchema(stepId, method, url, variableName) {
|
|
1128
|
+
const bodyEl = document.getElementById(stepId + '-body-text');
|
|
1129
|
+
if (!bodyEl) return;
|
|
1130
|
+
|
|
1131
|
+
const body = bodyEl.value || bodyEl.textContent || '';
|
|
1132
|
+
vscode.postMessage({
|
|
1133
|
+
type: 'assertSchema',
|
|
1134
|
+
body: body,
|
|
1135
|
+
method: method,
|
|
1136
|
+
url: url,
|
|
1137
|
+
variableName: variableName
|
|
1138
|
+
});
|
|
1139
|
+
showToast('Generating schema...');
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function assertProperty(path, value, variableName) {
|
|
1143
|
+
vscode.postMessage({
|
|
1144
|
+
type: 'assertProperty',
|
|
1145
|
+
path: path,
|
|
1146
|
+
value: value,
|
|
1147
|
+
variableName: variableName
|
|
1148
|
+
});
|
|
1149
|
+
showToast('Assertion added for ' + path);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Global store for contract data to avoid escaping issues
|
|
1153
|
+
const contractDataStore = {};
|
|
1154
|
+
|
|
1155
|
+
function openContractView(contractId) {
|
|
1156
|
+
const contractData = contractDataStore[contractId];
|
|
1157
|
+
if (!contractData) {
|
|
1158
|
+
console.error('Contract data not found for ID:', contractId);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
vscode.postMessage({
|
|
1162
|
+
type: 'openContractView',
|
|
1163
|
+
schemaPath: contractData.schemaPath,
|
|
1164
|
+
errors: contractData.errors,
|
|
1165
|
+
responseBody: contractData.responseBody,
|
|
1166
|
+
schema: contractData.schema
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function openContractViewFromButton(button) {
|
|
1171
|
+
const contractDataBase64 = button.getAttribute('data-contract');
|
|
1172
|
+
if (!contractDataBase64) {
|
|
1173
|
+
console.error('No contract data on button');
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
try {
|
|
1177
|
+
const contractData = JSON.parse(decodeURIComponent(atob(contractDataBase64)));
|
|
1178
|
+
vscode.postMessage({
|
|
1179
|
+
type: 'openContractView',
|
|
1180
|
+
schemaPath: contractData.schemaPath,
|
|
1181
|
+
errors: contractData.errors,
|
|
1182
|
+
responseBody: contractData.responseBody,
|
|
1183
|
+
schema: contractData.schema
|
|
1184
|
+
});
|
|
1185
|
+
} catch (e) {
|
|
1186
|
+
console.error('Failed to parse contract data:', e);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function storeContractData(id, data) {
|
|
1191
|
+
contractDataStore[id] = data;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function escapeHtml(text) {
|
|
1195
|
+
const div = document.createElement('div');
|
|
1196
|
+
div.textContent = text;
|
|
1197
|
+
return div.innerHTML;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function escapeJsString(text) {
|
|
1201
|
+
return String(text || '').replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function getStatusClass(status) {
|
|
1205
|
+
if (status >= 200 && status < 300) return 'status-success';
|
|
1206
|
+
if (status >= 300 && status < 400) return 'status-redirect';
|
|
1207
|
+
if (status >= 400 && status < 500) return 'status-client-error';
|
|
1208
|
+
if (status >= 500) return 'status-server-error';
|
|
1209
|
+
return '';
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function formatJson(obj) {
|
|
1213
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
1214
|
+
return escapeHtml(String(obj));
|
|
1215
|
+
}
|
|
1216
|
+
try {
|
|
1217
|
+
const json = JSON.stringify(obj, null, 2);
|
|
1218
|
+
return json.replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
|
|
1219
|
+
.replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
|
|
1220
|
+
.replace(/: (\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>')
|
|
1221
|
+
.replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
|
|
1222
|
+
.replace(/: (null)/g, ': <span class="json-null">$1</span>');
|
|
1223
|
+
} catch {
|
|
1224
|
+
return escapeHtml(String(obj));
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function formatJsonWithHighlight(obj, highlightPath) {
|
|
1229
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
1230
|
+
return escapeHtml(String(obj));
|
|
1231
|
+
}
|
|
1232
|
+
try {
|
|
1233
|
+
const json = JSON.stringify(obj, null, 2);
|
|
1234
|
+
let result = json.replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
|
|
1235
|
+
.replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
|
|
1236
|
+
.replace(/: (\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>')
|
|
1237
|
+
.replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
|
|
1238
|
+
.replace(/: (null)/g, ': <span class="json-null">$1</span>');
|
|
1239
|
+
|
|
1240
|
+
// Highlight the specific path if provided (e.g., "body.status" -> highlight "status")
|
|
1241
|
+
if (highlightPath) {
|
|
1242
|
+
const pathParts = highlightPath.replace(/^body\\.?/, '').split('.');
|
|
1243
|
+
const leafKey = pathParts[pathParts.length - 1];
|
|
1244
|
+
if (leafKey) {
|
|
1245
|
+
// Highlight the key that was checked
|
|
1246
|
+
const keyRegex = new RegExp('(<span class="json-key">"' + leafKey + '"</span>:)', 'g');
|
|
1247
|
+
result = result.replace(keyRegex, '<span class="json-highlighted">$1</span>');
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return result;
|
|
1251
|
+
} catch {
|
|
1252
|
+
return escapeHtml(String(obj));
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function formatJsonInteractive(obj, variableName, basePath) {
|
|
1257
|
+
basePath = basePath || 'body';
|
|
1258
|
+
|
|
1259
|
+
function encodeData(val) {
|
|
1260
|
+
// Base64 encode JSON to avoid all escaping issues
|
|
1261
|
+
return btoa(JSON.stringify(val));
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function renderValue(val, path) {
|
|
1265
|
+
const dataAttrs = 'data-path="' + escapeHtml(path) + '" data-var="' + escapeHtml(variableName) + '"';
|
|
1266
|
+
|
|
1267
|
+
if (val === null) {
|
|
1268
|
+
return '<span class="json-null json-clickable" ' + dataAttrs + ' data-value="' + encodeData(null) + '">null</span>';
|
|
1269
|
+
}
|
|
1270
|
+
if (typeof val === 'boolean') {
|
|
1271
|
+
return '<span class="json-boolean json-clickable" ' + dataAttrs + ' data-value="' + encodeData(val) + '">' + val + '</span>';
|
|
1272
|
+
}
|
|
1273
|
+
if (typeof val === 'number') {
|
|
1274
|
+
return '<span class="json-number json-clickable" ' + dataAttrs + ' data-value="' + encodeData(val) + '">' + val + '</span>';
|
|
1275
|
+
}
|
|
1276
|
+
if (typeof val === 'string') {
|
|
1277
|
+
return '<span class="json-string json-clickable" ' + dataAttrs + ' data-value="' + encodeData(val) + '">"' + escapeHtml(val) + '"</span>';
|
|
1278
|
+
}
|
|
1279
|
+
if (Array.isArray(val)) {
|
|
1280
|
+
if (val.length === 0) return '[]';
|
|
1281
|
+
const items = val.map((item, i) => renderValue(item, path + '[' + i + ']'));
|
|
1282
|
+
return '[\\n' + items.map(item => ' ' + item.split('\\n').join('\\n ')).join(',\\n') + '\\n]';
|
|
1283
|
+
}
|
|
1284
|
+
if (typeof val === 'object') {
|
|
1285
|
+
const keys = Object.keys(val);
|
|
1286
|
+
if (keys.length === 0) return '{}';
|
|
1287
|
+
const pairs = keys.map(k => {
|
|
1288
|
+
const newPath = path + '.' + k;
|
|
1289
|
+
const renderedVal = renderValue(val[k], newPath);
|
|
1290
|
+
return ' <span class="json-key">"' + escapeHtml(k) + '"</span>: ' + renderedVal.split('\\n').join('\\n ');
|
|
1291
|
+
});
|
|
1292
|
+
return '{\\n' + pairs.join(',\\n') + '\\n}';
|
|
1293
|
+
}
|
|
1294
|
+
return escapeHtml(String(val));
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
try {
|
|
1298
|
+
return renderValue(obj, basePath);
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
console.error('formatJsonInteractive error:', e);
|
|
1301
|
+
return formatJson(obj);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Event delegation for clickable JSON values
|
|
1306
|
+
document.addEventListener('click', function(e) {
|
|
1307
|
+
const target = e.target;
|
|
1308
|
+
if (target && target.classList && target.classList.contains('json-clickable')) {
|
|
1309
|
+
const path = target.getAttribute('data-path');
|
|
1310
|
+
const varName = target.getAttribute('data-var');
|
|
1311
|
+
const encodedValue = target.getAttribute('data-value');
|
|
1312
|
+
if (path && varName && encodedValue) {
|
|
1313
|
+
try {
|
|
1314
|
+
const value = JSON.parse(atob(encodedValue));
|
|
1315
|
+
assertProperty(path, value, varName);
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
console.error('Failed to parse clickable JSON value:', err);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
function toggleResponsePreview(headerEl) {
|
|
1324
|
+
const previewBody = headerEl.nextElementSibling;
|
|
1325
|
+
const icon = headerEl.querySelector('.expand-icon');
|
|
1326
|
+
if (previewBody) {
|
|
1327
|
+
if (previewBody.style.display === 'none') {
|
|
1328
|
+
previewBody.style.display = 'block';
|
|
1329
|
+
if (icon) icon.classList.add('expanded');
|
|
1330
|
+
} else {
|
|
1331
|
+
previewBody.style.display = 'none';
|
|
1332
|
+
if (icon) icon.classList.remove('expanded');
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function formatHeaders(headers) {
|
|
1338
|
+
if (!headers || Object.keys(headers).length === 0) {
|
|
1339
|
+
return '<p class="seq-no-data">No headers</p>';
|
|
1340
|
+
}
|
|
1341
|
+
const rows = Object.entries(headers).map(([key, value]) =>
|
|
1342
|
+
'<tr><td>' + escapeHtml(key) + '</td><td>' + escapeHtml(String(value)) + '</td></tr>'
|
|
1343
|
+
).join('');
|
|
1344
|
+
return '<table class="seq-headers-table"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function formatCookies(cookies) {
|
|
1348
|
+
if (!cookies || cookies.length === 0) {
|
|
1349
|
+
return '<p class="seq-no-data">No cookies</p>';
|
|
1350
|
+
}
|
|
1351
|
+
const rows = cookies.map(c =>
|
|
1352
|
+
'<tr><td>' + escapeHtml(c.name) + '</td><td title="' + escapeHtml(c.value) + '">' +
|
|
1353
|
+
escapeHtml(c.value.length > 30 ? c.value.substring(0, 30) + '...' : c.value) +
|
|
1354
|
+
'</td><td>' + escapeHtml(c.domain || '') + '</td><td>' + escapeHtml(c.path || '/') + '</td></tr>'
|
|
1355
|
+
).join('');
|
|
1356
|
+
return '<table class="seq-cookies-table"><thead><tr><th>Name</th><th>Value</th><th>Domain</th><th>Path</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function switchTab(stepId, tabName) {
|
|
1360
|
+
const container = document.getElementById(stepId);
|
|
1361
|
+
if (!container) return;
|
|
1362
|
+
|
|
1363
|
+
// Update tab buttons
|
|
1364
|
+
const tabs = container.querySelectorAll('.seq-tab');
|
|
1365
|
+
tabs.forEach(t => {
|
|
1366
|
+
if (t.dataset.tab === tabName) {
|
|
1367
|
+
t.classList.add('active');
|
|
1368
|
+
} else {
|
|
1369
|
+
t.classList.remove('active');
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
// Update tab contents
|
|
1374
|
+
const contents = container.querySelectorAll('.seq-tab-content');
|
|
1375
|
+
contents.forEach(c => {
|
|
1376
|
+
if (c.id === stepId + '-' + tabName) {
|
|
1377
|
+
c.classList.add('active');
|
|
1378
|
+
} else {
|
|
1379
|
+
c.classList.remove('active');
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function copyResponseBody(stepId) {
|
|
1385
|
+
const bodyEl = document.getElementById(stepId + '-body-text');
|
|
1386
|
+
if (bodyEl) {
|
|
1387
|
+
navigator.clipboard.writeText(bodyEl.value || bodyEl.textContent || '');
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function toggleResponse(id) {
|
|
1392
|
+
const body = document.getElementById(id);
|
|
1393
|
+
const icon = document.getElementById('icon-' + id);
|
|
1394
|
+
|
|
1395
|
+
if (!body) return;
|
|
1396
|
+
|
|
1397
|
+
if (body.style.display === 'none') {
|
|
1398
|
+
body.style.display = 'block';
|
|
1399
|
+
if (icon) icon.classList.add('expanded');
|
|
1400
|
+
} else {
|
|
1401
|
+
body.style.display = 'none';
|
|
1402
|
+
if (icon) icon.classList.remove('expanded');
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function toggleSequenceGroup(id) {
|
|
1407
|
+
const content = document.getElementById('seq-content-' + id);
|
|
1408
|
+
const icon = document.getElementById('seq-icon-' + id);
|
|
1409
|
+
|
|
1410
|
+
if (!content) return;
|
|
1411
|
+
|
|
1412
|
+
if (content.style.display === 'none') {
|
|
1413
|
+
content.style.display = 'block';
|
|
1414
|
+
if (icon) icon.classList.add('expanded');
|
|
1415
|
+
} else {
|
|
1416
|
+
content.style.display = 'none';
|
|
1417
|
+
if (icon) icon.classList.remove('expanded');
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function startSequenceGroup(sequenceName) {
|
|
1422
|
+
const groupId = 'seq-' + (++sequenceGroupCounter);
|
|
1423
|
+
const container = sequenceStack.length > 0
|
|
1424
|
+
? document.getElementById('seq-content-' + sequenceStack[sequenceStack.length - 1])
|
|
1425
|
+
: document.getElementById('steps-list');
|
|
1426
|
+
|
|
1427
|
+
const html = \`
|
|
1428
|
+
<div class="sequence-group" id="\${groupId}">
|
|
1429
|
+
<div class="sequence-group-header" onclick="toggleSequenceGroup('\${groupId}')">
|
|
1430
|
+
<span class="expand-icon" id="seq-icon-\${groupId}">▶</span>
|
|
1431
|
+
<span class="sequence-group-icon">📦</span>
|
|
1432
|
+
<span class="sequence-group-name">\${escapeHtml(sequenceName)}</span>
|
|
1433
|
+
<span class="sequence-group-status running" id="seq-status-\${groupId}">running...</span>
|
|
1434
|
+
</div>
|
|
1435
|
+
<div class="sequence-group-content" id="seq-content-\${groupId}" style="display: none;">
|
|
1436
|
+
</div>
|
|
1437
|
+
</div>
|
|
1438
|
+
\`;
|
|
1439
|
+
|
|
1440
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
1441
|
+
sequenceStack.push(groupId);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function endSequenceGroup() {
|
|
1445
|
+
if (sequenceStack.length > 0) {
|
|
1446
|
+
const groupId = sequenceStack.pop();
|
|
1447
|
+
const statusEl = document.getElementById('seq-status-' + groupId);
|
|
1448
|
+
if (statusEl) {
|
|
1449
|
+
statusEl.textContent = 'completed';
|
|
1450
|
+
statusEl.classList.remove('running');
|
|
1451
|
+
statusEl.classList.add('completed');
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function getCurrentContainer() {
|
|
1457
|
+
if (sequenceStack.length > 0) {
|
|
1458
|
+
return document.getElementById('seq-content-' + sequenceStack[sequenceStack.length - 1]);
|
|
1459
|
+
}
|
|
1460
|
+
return document.getElementById('steps-list');
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function toggleAll() {
|
|
1464
|
+
const bodies = document.querySelectorAll('.response-body');
|
|
1465
|
+
const seqContents = document.querySelectorAll('.sequence-group-content');
|
|
1466
|
+
const icons = document.querySelectorAll('.expand-icon');
|
|
1467
|
+
const btn = document.getElementById('toggle-all-btn');
|
|
1468
|
+
|
|
1469
|
+
allExpanded = !allExpanded;
|
|
1470
|
+
|
|
1471
|
+
bodies.forEach(body => {
|
|
1472
|
+
body.style.display = allExpanded ? 'block' : 'none';
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
seqContents.forEach(content => {
|
|
1476
|
+
content.style.display = allExpanded ? 'block' : 'none';
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
icons.forEach(icon => {
|
|
1480
|
+
if (allExpanded) {
|
|
1481
|
+
icon.classList.add('expanded');
|
|
1482
|
+
} else {
|
|
1483
|
+
icon.classList.remove('expanded');
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
btn.textContent = allExpanded ? 'Collapse All' : 'Expand All';
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function addStepHtml(step, stepIndex, elapsed, progress) {
|
|
1491
|
+
const container = getCurrentContainer();
|
|
1492
|
+
const stepId = 'step-' + stepIndex + '-' + Date.now();
|
|
1493
|
+
|
|
1494
|
+
let html = '';
|
|
1495
|
+
|
|
1496
|
+
if (step.type === 'print' && step.print) {
|
|
1497
|
+
const hasBody = step.print.body && step.print.body.trim();
|
|
1498
|
+
|
|
1499
|
+
if (hasBody) {
|
|
1500
|
+
html = \`
|
|
1501
|
+
<div class="response-item print-item">
|
|
1502
|
+
<div class="response-header" onclick="toggleResponse('\${stepId}')">
|
|
1503
|
+
<span class="step-number">\${stepIndex + 1}.</span>
|
|
1504
|
+
<span class="print-icon">📝</span>
|
|
1505
|
+
<span class="print-title">\${escapeHtml(step.print.title)}</span>
|
|
1506
|
+
<span class="meta">@ \${elapsed}ms</span>
|
|
1507
|
+
<span class="expand-icon" id="icon-\${stepId}">▶</span>
|
|
1508
|
+
</div>
|
|
1509
|
+
<div class="response-body print-body" id="\${stepId}" style="display: none;">
|
|
1510
|
+
<pre><code>\${escapeHtml(step.print.body)}</code></pre>
|
|
1511
|
+
</div>
|
|
1512
|
+
</div>
|
|
1513
|
+
\`;
|
|
1514
|
+
} else {
|
|
1515
|
+
html = \`
|
|
1516
|
+
<div class="response-item print-item">
|
|
1517
|
+
<div class="print-content">
|
|
1518
|
+
<span class="step-number">\${stepIndex + 1}.</span>
|
|
1519
|
+
<span class="print-icon">📝</span>
|
|
1520
|
+
<span class="print-title">\${escapeHtml(step.print.title)}</span>
|
|
1521
|
+
<span class="meta">@ \${elapsed}ms</span>
|
|
1522
|
+
</div>
|
|
1523
|
+
</div>
|
|
1524
|
+
\`;
|
|
1525
|
+
}
|
|
1526
|
+
} else if (step.type === 'request' && step.response) {
|
|
1527
|
+
requestCounter++;
|
|
1528
|
+
const response = step.response;
|
|
1529
|
+
const statusClass = getStatusClass(response.status);
|
|
1530
|
+
// Show variable name if available, otherwise fall back to $N
|
|
1531
|
+
const responseLabel = step.variableName || ('$' + requestCounter);
|
|
1532
|
+
const bodyContent = typeof response.body === 'object'
|
|
1533
|
+
? formatJsonInteractive(response.body, responseLabel, 'body')
|
|
1534
|
+
: escapeHtml(String(response.body || ''));
|
|
1535
|
+
const rawBody = typeof response.body === 'object'
|
|
1536
|
+
? JSON.stringify(response.body, null, 2)
|
|
1537
|
+
: String(response.body || '');
|
|
1538
|
+
const method = step.requestMethod || 'GET';
|
|
1539
|
+
const url = step.requestUrl || '';
|
|
1540
|
+
const headersHtml = formatHeaders(response.headers);
|
|
1541
|
+
const cookiesHtml = formatCookies(response.cookies);
|
|
1542
|
+
|
|
1543
|
+
// Show retry info if request was retried
|
|
1544
|
+
const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1
|
|
1545
|
+
? \`<span class="retry-info">🔄 retried \${response.retryInfo.attemptsMade - 1}x</span>\`
|
|
1546
|
+
: '';
|
|
1547
|
+
|
|
1548
|
+
html = \`
|
|
1549
|
+
<div class="response-item">
|
|
1550
|
+
<div class="response-header" onclick="toggleResponse('\${stepId}')">
|
|
1551
|
+
<span class="step-number">\${stepIndex + 1}.</span>
|
|
1552
|
+
<span class="response-index">\${responseLabel}</span>
|
|
1553
|
+
<span class="status-code \${statusClass}">\${response.status} \${escapeHtml(response.statusText || '')}</span>
|
|
1554
|
+
<span class="meta">⏱ \${response.duration}ms</span>
|
|
1555
|
+
\${retryInfo}
|
|
1556
|
+
<span class="expand-icon" id="icon-\${stepId}">▶</span>
|
|
1557
|
+
</div>
|
|
1558
|
+
<div class="response-body" id="\${stepId}" style="display: none;">
|
|
1559
|
+
<div class="seq-request-url">
|
|
1560
|
+
<span class="method \${method}">\${method}</span>
|
|
1561
|
+
<span>\${escapeHtml(url)}</span>
|
|
1562
|
+
</div>
|
|
1563
|
+
<div class="seq-tabs">
|
|
1564
|
+
<button class="seq-tab active" data-tab="body" onclick="switchTab('\${stepId}', 'body')">Body</button>
|
|
1565
|
+
<button class="seq-tab" data-tab="headers" onclick="switchTab('\${stepId}', 'headers')">Headers</button>
|
|
1566
|
+
<button class="seq-tab" data-tab="cookies" onclick="switchTab('\${stepId}', 'cookies')">Cookies</button>
|
|
1567
|
+
</div>
|
|
1568
|
+
<div id="\${stepId}-body" class="seq-tab-content active">
|
|
1569
|
+
<div class="seq-body-container">
|
|
1570
|
+
<button class="seq-schema-btn" onclick="assertSchema('\${stepId}', '\${escapeJsString(method)}', '\${escapeJsString(url)}', '\${escapeJsString(responseLabel)}')">📐 Assert Schema</button>
|
|
1571
|
+
<button class="seq-compare-btn" data-compare-step="\${stepId}" onclick="compareResponse('\${stepId}')">⇄ Compare</button>
|
|
1572
|
+
<button class="seq-copy-btn" onclick="copyResponseBody('\${stepId}')">📋 Copy</button>
|
|
1573
|
+
<pre><code>\${bodyContent}</code></pre>
|
|
1574
|
+
<textarea id="\${stepId}-body-text" style="display:none;">\${escapeHtml(rawBody)}</textarea>
|
|
1575
|
+
</div>
|
|
1576
|
+
</div>
|
|
1577
|
+
<div id="\${stepId}-headers" class="seq-tab-content">
|
|
1578
|
+
\${headersHtml}
|
|
1579
|
+
</div>
|
|
1580
|
+
<div id="\${stepId}-cookies" class="seq-tab-content">
|
|
1581
|
+
\${cookiesHtml}
|
|
1582
|
+
</div>
|
|
1583
|
+
</div>
|
|
1584
|
+
</div>
|
|
1585
|
+
\`;
|
|
1586
|
+
} else if (step.type === 'script' && step.script) {
|
|
1587
|
+
const script = step.script;
|
|
1588
|
+
const hasStderr = script.error && script.error.trim();
|
|
1589
|
+
const scriptStatusClass = !script.success ? 'status-error' : (hasStderr ? 'status-warning' : 'status-success');
|
|
1590
|
+
const scriptStatusText = !script.success ? 'Exit ' + script.exitCode : (hasStderr ? 'Warning' : 'OK');
|
|
1591
|
+
const hasOutput = (script.output && script.output.trim()) || (script.error && script.error.trim());
|
|
1592
|
+
|
|
1593
|
+
html = \`
|
|
1594
|
+
<div class="response-item script-item">
|
|
1595
|
+
<div class="response-header" onclick="toggleResponse('\${stepId}')">
|
|
1596
|
+
<span class="step-number">\${stepIndex + 1}.</span>
|
|
1597
|
+
<span class="script-icon">⚡</span>
|
|
1598
|
+
<span class="script-type">\${escapeHtml(script.type)}</span>
|
|
1599
|
+
<span class="script-path">\${escapeHtml(script.scriptPath)}</span>
|
|
1600
|
+
\${script.captureVar ? '<span class="capture-var">→ ' + escapeHtml(script.captureVar) + '</span>' : ''}
|
|
1601
|
+
<span class="status-code \${scriptStatusClass}">\${scriptStatusText}</span>
|
|
1602
|
+
<span class="meta">⏱ \${script.duration}ms</span>
|
|
1603
|
+
<span class="expand-icon" id="icon-\${stepId}">\${hasOutput ? '▶' : ''}</span>
|
|
1604
|
+
</div>
|
|
1605
|
+
\${hasOutput ? \`
|
|
1606
|
+
<div class="response-body" id="\${stepId}" style="display: none;">
|
|
1607
|
+
\${script.output && script.output.trim() ? '<div class="script-output"><div class="output-label">stdout:</div><pre><code>' + escapeHtml(script.output) + '</code></pre></div>' : ''}
|
|
1608
|
+
\${script.error && script.error.trim() ? '<div class="script-output stderr"><div class="output-label">stderr:</div><pre><code>' + escapeHtml(script.error) + '</code></pre></div>' : ''}
|
|
1609
|
+
</div>
|
|
1610
|
+
\` : ''}
|
|
1611
|
+
</div>
|
|
1612
|
+
\`;
|
|
1613
|
+
} else if (step.type === 'assertion' && step.assertion) {
|
|
1614
|
+
const assertion = step.assertion;
|
|
1615
|
+
const assertionClass = assertion.passed ? 'status-success' : 'status-error';
|
|
1616
|
+
const assertionIcon = assertion.passed ? '✓' : '✗';
|
|
1617
|
+
const hasDetails = !assertion.passed || assertion.message;
|
|
1618
|
+
const isFailed = !assertion.passed;
|
|
1619
|
+
const isSchemaAssertion = assertion.operator === 'matchesSchema';
|
|
1620
|
+
|
|
1621
|
+
// Build display expression with friendly name when available
|
|
1622
|
+
let displayExpr = assertion.expression;
|
|
1623
|
+
if (assertion.friendlyName && assertion.responseIndex) {
|
|
1624
|
+
// Replace $N with the friendly variable name
|
|
1625
|
+
displayExpr = displayExpr.replace('$' + assertion.responseIndex, assertion.friendlyName);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
let detailsHtml = '';
|
|
1629
|
+
if (hasDetails) {
|
|
1630
|
+
if (assertion.message) {
|
|
1631
|
+
detailsHtml = '<div class="assertion-message">' + escapeHtml(assertion.message) + '</div>';
|
|
1632
|
+
}
|
|
1633
|
+
if (assertion.error) {
|
|
1634
|
+
detailsHtml += '<div class="assertion-error">' + escapeHtml(assertion.error) + '</div>';
|
|
1635
|
+
|
|
1636
|
+
// Add Contract View button for failed schema assertions
|
|
1637
|
+
if (isSchemaAssertion && isFailed && assertion.schemaErrors) {
|
|
1638
|
+
const contractId = 'contract-' + stepId;
|
|
1639
|
+
const contractData = {
|
|
1640
|
+
schemaPath: assertion.schemaPath || assertion.rightValue,
|
|
1641
|
+
errors: assertion.schemaErrors,
|
|
1642
|
+
responseBody: assertion.leftValue,
|
|
1643
|
+
schema: assertion.schema
|
|
1644
|
+
};
|
|
1645
|
+
// Store contract data using base64 encoding in data attribute to avoid escaping issues
|
|
1646
|
+
const contractDataBase64 = btoa(encodeURIComponent(JSON.stringify(contractData)));
|
|
1647
|
+
detailsHtml += \`
|
|
1648
|
+
<div class="contract-view-actions">
|
|
1649
|
+
<button class="contract-view-btn" data-contract-id="\${contractId}" data-contract="\${contractDataBase64}" onclick="openContractViewFromButton(this)">
|
|
1650
|
+
📋 View Contract Details
|
|
1651
|
+
</button>
|
|
1652
|
+
</div>
|
|
1653
|
+
\`;
|
|
1654
|
+
}
|
|
1655
|
+
} else if (isFailed) {
|
|
1656
|
+
detailsHtml += '<div class="assertion-details">' +
|
|
1657
|
+
'<div class="assertion-expected">Expected: <code>' + escapeHtml(assertion.rightExpression || assertion.operator) + '</code></div>' +
|
|
1658
|
+
'<div class="assertion-actual">Actual: <code>' + escapeHtml(JSON.stringify(assertion.leftValue)) + '</code></div>' +
|
|
1659
|
+
'</div>';
|
|
1660
|
+
|
|
1661
|
+
// Add inline response preview for failures
|
|
1662
|
+
if (assertion.relatedResponse) {
|
|
1663
|
+
const resp = assertion.relatedResponse;
|
|
1664
|
+
const respStatusClass = getStatusClass(resp.status);
|
|
1665
|
+
const bodyPreview = typeof resp.body === 'object'
|
|
1666
|
+
? formatJsonWithHighlight(resp.body, assertion.jsonPath)
|
|
1667
|
+
: escapeHtml(String(resp.body || ''));
|
|
1668
|
+
const varLabel = assertion.friendlyName || ('$' + assertion.responseIndex);
|
|
1669
|
+
|
|
1670
|
+
detailsHtml += \`
|
|
1671
|
+
<div class="assertion-response-preview">
|
|
1672
|
+
<div class="preview-header" onclick="toggleResponsePreview(this)">
|
|
1673
|
+
<span class="expand-icon">▶</span>
|
|
1674
|
+
<span>\${varLabel} Response</span>
|
|
1675
|
+
<span class="status-code \${respStatusClass}">\${resp.status}</span>
|
|
1676
|
+
</div>
|
|
1677
|
+
<div class="preview-body" style="display: none;">
|
|
1678
|
+
<pre><code>\${bodyPreview}</code></pre>
|
|
1679
|
+
</div>
|
|
1680
|
+
</div>
|
|
1681
|
+
\`;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// Auto-expand failed assertions
|
|
1687
|
+
const expandedByDefault = isFailed;
|
|
1688
|
+
|
|
1689
|
+
html = \`
|
|
1690
|
+
<div class="response-item assertion-item \${assertion.passed ? 'assertion-passed' : 'assertion-failed'}">
|
|
1691
|
+
<div class="response-header\${hasDetails ? '' : ' no-expand'}" \${hasDetails ? 'onclick="toggleResponse(\\'' + stepId + '\\')"' : ''}>
|
|
1692
|
+
<span class="step-number">\${stepIndex + 1}.</span>
|
|
1693
|
+
<span class="assertion-icon \${assertionClass}">\${assertionIcon}</span>
|
|
1694
|
+
<span class="assertion-expr">assert <code>\${escapeHtml(displayExpr)}</code></span>
|
|
1695
|
+
\${hasDetails ? '<span class="expand-icon' + (expandedByDefault ? ' expanded' : '') + '" id="icon-' + stepId + '">▶</span>' : ''}
|
|
1696
|
+
</div>
|
|
1697
|
+
\${hasDetails ? \`
|
|
1698
|
+
<div class="response-body assertion-body" id="\${stepId}" style="display: \${expandedByDefault ? 'block' : 'none'};">
|
|
1699
|
+
\${detailsHtml}
|
|
1700
|
+
</div>
|
|
1701
|
+
\` : ''}
|
|
1702
|
+
</div>
|
|
1703
|
+
\`;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (html && container) {
|
|
1707
|
+
container.insertAdjacentHTML('beforeend', html);
|
|
1708
|
+
}
|
|
1709
|
+
stepCount++;
|
|
1710
|
+
document.getElementById('step-count').textContent = stepCount + '/${totalSteps} steps';
|
|
1711
|
+
document.getElementById('elapsed-time').textContent = '⏱ ' + elapsed + 'ms';
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function finalize(success, errors, duration) {
|
|
1715
|
+
// Update status badge
|
|
1716
|
+
const badge = document.getElementById('status-badge');
|
|
1717
|
+
badge.textContent = success ? 'PASSED' : 'FAILED';
|
|
1718
|
+
badge.className = 'sequence-status ' + (success ? 'status-success' : 'status-error');
|
|
1719
|
+
|
|
1720
|
+
// Hide spinner
|
|
1721
|
+
document.getElementById('spinner').style.display = 'none';
|
|
1722
|
+
|
|
1723
|
+
// Update elapsed time
|
|
1724
|
+
document.getElementById('elapsed-time').textContent = '⏱ ' + duration + 'ms total';
|
|
1725
|
+
|
|
1726
|
+
// Show errors if any
|
|
1727
|
+
if (errors && errors.length > 0) {
|
|
1728
|
+
const errorsSection = document.getElementById('errors-section');
|
|
1729
|
+
const errorsList = document.getElementById('errors-list');
|
|
1730
|
+
errorsSection.style.display = 'block';
|
|
1731
|
+
|
|
1732
|
+
errors.forEach(err => {
|
|
1733
|
+
const li = document.createElement('li');
|
|
1734
|
+
li.className = err.startsWith('Warning') ? 'warning' : 'error';
|
|
1735
|
+
li.textContent = err;
|
|
1736
|
+
errorsList.appendChild(li);
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Listen for messages from the extension
|
|
1742
|
+
window.addEventListener('message', event => {
|
|
1743
|
+
const message = event.data;
|
|
1744
|
+
|
|
1745
|
+
if (message.type === 'reset') {
|
|
1746
|
+
// Clear all state for a fresh run
|
|
1747
|
+
stepCount = 0;
|
|
1748
|
+
requestCounter = 0;
|
|
1749
|
+
allExpanded = false;
|
|
1750
|
+
sequenceStack = [];
|
|
1751
|
+
sequenceGroupCounter = 0;
|
|
1752
|
+
document.getElementById('steps-list').innerHTML = '';
|
|
1753
|
+
document.getElementById('step-count').textContent = '0/' + message.totalSteps + ' steps';
|
|
1754
|
+
document.getElementById('elapsed-time').textContent = '⏱ 0ms';
|
|
1755
|
+
document.getElementById('status-badge').textContent = 'RUNNING';
|
|
1756
|
+
document.getElementById('status-badge').className = 'sequence-status status-running';
|
|
1757
|
+
document.getElementById('spinner').style.display = 'flex';
|
|
1758
|
+
document.getElementById('toggle-all-btn').textContent = 'Expand All';
|
|
1759
|
+
document.getElementById('errors-section').style.display = 'none';
|
|
1760
|
+
document.getElementById('errors-list').innerHTML = '';
|
|
1761
|
+
} else if (message.type === 'addStep') {
|
|
1762
|
+
const progress = message.progress || {};
|
|
1763
|
+
|
|
1764
|
+
// Handle sequence start/end
|
|
1765
|
+
if (progress.stepType === 'sequenceStart' && progress.sequenceName) {
|
|
1766
|
+
startSequenceGroup(progress.sequenceName);
|
|
1767
|
+
} else if (progress.stepType === 'sequenceEnd') {
|
|
1768
|
+
endSequenceGroup();
|
|
1769
|
+
} else if (message.step) {
|
|
1770
|
+
// Regular step
|
|
1771
|
+
addStepHtml(message.step, message.step.stepIndex, message.elapsed, progress);
|
|
1772
|
+
}
|
|
1773
|
+
} else if (message.type === 'finalize') {
|
|
1774
|
+
finalize(message.success, message.errors, message.duration);
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
</script>
|
|
1778
|
+
</body>
|
|
1779
|
+
</html>`;
|
|
1780
|
+
}
|
|
1781
|
+
_getMultiSequenceHtmlContent(sequenceName, runs) {
|
|
1782
|
+
const passedRuns = runs.filter(r => r.result.success).length;
|
|
1783
|
+
const totalDuration = runs.reduce((sum, run) => sum + run.result.duration, 0);
|
|
1784
|
+
const allPassed = passedRuns === runs.length;
|
|
1785
|
+
const statusClass = allPassed ? 'status-success' : 'status-error';
|
|
1786
|
+
const statusText = allPassed ? 'PASSED' : 'FAILED';
|
|
1787
|
+
const runsHtml = runs.map((run, index) => {
|
|
1788
|
+
const runId = `run-${index}`;
|
|
1789
|
+
const runStatusClass = run.result.success ? 'status-success' : 'status-error';
|
|
1790
|
+
const runStatusText = run.result.success ? 'PASSED' : 'FAILED';
|
|
1791
|
+
const stepCount = run.result.steps?.length || run.result.responses.length;
|
|
1792
|
+
const expandedByDefault = !run.result.success || index === 0;
|
|
1793
|
+
const runStepsHtml = run.result.steps && run.result.steps.length > 0
|
|
1794
|
+
? this._renderOrderedSteps(run.result.steps, `${runId}-step`)
|
|
1795
|
+
: this._renderGroupedResults(run.result, `${runId}-legacy`);
|
|
1796
|
+
const runErrorsHtml = run.result.errors.length > 0 ? `
|
|
1797
|
+
<div class="errors-section">
|
|
1798
|
+
<h3>Errors & Warnings</h3>
|
|
1799
|
+
<ul>
|
|
1800
|
+
${run.result.errors.map(e => `<li class="${e.startsWith('Warning') ? 'warning' : 'error'}">${this._escapeHtml(e)}</li>`).join('')}
|
|
1801
|
+
</ul>
|
|
1802
|
+
</div>
|
|
1803
|
+
` : '';
|
|
1804
|
+
return `
|
|
1805
|
+
<div class="run-group">
|
|
1806
|
+
<div class="run-group-header" onclick="toggleRunGroup('${runId}')">
|
|
1807
|
+
<span class="expand-icon ${expandedByDefault ? 'expanded' : ''}" id="icon-${runId}">▶</span>
|
|
1808
|
+
<span class="run-group-name">Run ${index + 1}: ${this._escapeHtml(run.label)}</span>
|
|
1809
|
+
<span class="run-group-meta">${stepCount} step${stepCount !== 1 ? 's' : ''}</span>
|
|
1810
|
+
<span class="status-code ${runStatusClass}">${runStatusText}</span>
|
|
1811
|
+
<span class="run-group-meta">⏱ ${run.result.duration}ms</span>
|
|
1812
|
+
</div>
|
|
1813
|
+
<div class="run-group-content" id="${runId}" style="display: ${expandedByDefault ? 'block' : 'none'};">
|
|
1814
|
+
<div class="steps-list">
|
|
1815
|
+
${runStepsHtml}
|
|
1816
|
+
</div>
|
|
1817
|
+
${runErrorsHtml}
|
|
1818
|
+
</div>
|
|
1819
|
+
</div>
|
|
1820
|
+
`;
|
|
1821
|
+
}).join('');
|
|
1822
|
+
return /*html*/ `
|
|
1823
|
+
<!DOCTYPE html>
|
|
1824
|
+
<html lang="en">
|
|
1825
|
+
<head>
|
|
1826
|
+
<meta charset="UTF-8">
|
|
1827
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1828
|
+
<title>Sequence Result</title>
|
|
1829
|
+
<style>
|
|
1830
|
+
* { box-sizing: border-box; }
|
|
1831
|
+
|
|
1832
|
+
body {
|
|
1833
|
+
font-family: var(--vscode-font-family);
|
|
1834
|
+
font-size: var(--vscode-font-size);
|
|
1835
|
+
color: var(--vscode-foreground);
|
|
1836
|
+
background-color: var(--vscode-editor-background);
|
|
1837
|
+
padding: 0;
|
|
1838
|
+
margin: 0;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
.container { padding: 16px; }
|
|
1842
|
+
|
|
1843
|
+
.sequence-header {
|
|
1844
|
+
display: flex;
|
|
1845
|
+
align-items: center;
|
|
1846
|
+
gap: 16px;
|
|
1847
|
+
padding: 16px;
|
|
1848
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
1849
|
+
border-radius: 6px;
|
|
1850
|
+
margin-bottom: 16px;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
.sequence-name {
|
|
1854
|
+
font-size: 1.2em;
|
|
1855
|
+
font-weight: bold;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
.sequence-status {
|
|
1859
|
+
padding: 4px 12px;
|
|
1860
|
+
border-radius: 4px;
|
|
1861
|
+
font-weight: bold;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
.status-success { background: #4ec9b0; color: #000; }
|
|
1865
|
+
.status-warning { background: #cca700; color: #000; }
|
|
1866
|
+
.status-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
|
|
1867
|
+
.status-redirect { background: #dcdcaa; color: #000; }
|
|
1868
|
+
.status-client-error { background: #ce9178; color: #000; }
|
|
1869
|
+
.status-server-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
|
|
1870
|
+
|
|
1871
|
+
.meta { color: var(--vscode-descriptionForeground); }
|
|
1872
|
+
|
|
1873
|
+
.run-group {
|
|
1874
|
+
border: 1px solid var(--vscode-panel-border);
|
|
1875
|
+
border-radius: 6px;
|
|
1876
|
+
margin-bottom: 12px;
|
|
1877
|
+
overflow: hidden;
|
|
1878
|
+
}
|
|
1879
|
+
.run-group-header {
|
|
1880
|
+
display: flex;
|
|
1881
|
+
align-items: center;
|
|
1882
|
+
gap: 12px;
|
|
1883
|
+
padding: 10px 12px;
|
|
1884
|
+
cursor: pointer;
|
|
1885
|
+
background: linear-gradient(135deg, rgba(86, 156, 214, 0.15) 0%, rgba(78, 201, 176, 0.1) 100%);
|
|
1886
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
1887
|
+
}
|
|
1888
|
+
.run-group-header:hover {
|
|
1889
|
+
background: linear-gradient(135deg, rgba(86, 156, 214, 0.25) 0%, rgba(78, 201, 176, 0.15) 100%);
|
|
1890
|
+
}
|
|
1891
|
+
.run-group-name {
|
|
1892
|
+
font-family: var(--vscode-editor-font-family);
|
|
1893
|
+
font-weight: bold;
|
|
1894
|
+
color: #569cd6;
|
|
1895
|
+
flex: 1;
|
|
1896
|
+
}
|
|
1897
|
+
.run-group-meta {
|
|
1898
|
+
color: var(--vscode-descriptionForeground);
|
|
1899
|
+
font-size: 0.9em;
|
|
1900
|
+
}
|
|
1901
|
+
.run-group-content {
|
|
1902
|
+
padding: 8px;
|
|
1903
|
+
background: var(--vscode-editor-background);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
.retry-info {
|
|
1907
|
+
color: #dcdcaa;
|
|
1908
|
+
font-size: 0.85em;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
.response-item {
|
|
1912
|
+
border: 1px solid var(--vscode-panel-border);
|
|
1913
|
+
border-radius: 6px;
|
|
1914
|
+
margin-bottom: 8px;
|
|
1915
|
+
overflow: hidden;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
.response-header {
|
|
1919
|
+
display: flex;
|
|
1920
|
+
align-items: center;
|
|
1921
|
+
gap: 12px;
|
|
1922
|
+
padding: 12px;
|
|
1923
|
+
cursor: pointer;
|
|
1924
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
.response-header:hover {
|
|
1928
|
+
background: var(--vscode-list-hoverBackground);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
.response-index {
|
|
1932
|
+
font-family: var(--vscode-editor-font-family);
|
|
1933
|
+
font-weight: bold;
|
|
1934
|
+
color: var(--vscode-symbolIcon-variableForeground, #9cdcfe);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
.status-code {
|
|
1938
|
+
padding: 2px 8px;
|
|
1939
|
+
border-radius: 3px;
|
|
1940
|
+
font-size: 0.9em;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
.expand-icon {
|
|
1944
|
+
margin-left: auto;
|
|
1945
|
+
transition: transform 0.2s;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
.expand-icon.expanded {
|
|
1949
|
+
transform: rotate(90deg);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
.response-body {
|
|
1953
|
+
padding: 12px;
|
|
1954
|
+
border-top: 1px solid var(--vscode-panel-border);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
.seq-request-url {
|
|
1958
|
+
padding: 6px 12px;
|
|
1959
|
+
background: var(--vscode-textCodeBlock-background);
|
|
1960
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
1961
|
+
font-family: var(--vscode-editor-font-family);
|
|
1962
|
+
font-size: 0.9em;
|
|
1963
|
+
color: var(--vscode-foreground);
|
|
1964
|
+
display: flex;
|
|
1965
|
+
align-items: center;
|
|
1966
|
+
gap: 8px;
|
|
1967
|
+
margin: -12px -12px 12px -12px;
|
|
1968
|
+
}
|
|
1969
|
+
.seq-request-url .method {
|
|
1970
|
+
font-weight: bold;
|
|
1971
|
+
padding: 2px 6px;
|
|
1972
|
+
border-radius: 3px;
|
|
1973
|
+
font-size: 0.85em;
|
|
1974
|
+
}
|
|
1975
|
+
.seq-request-url .method.GET { background: #4ec9b022; color: #4ec9b0; }
|
|
1976
|
+
.seq-request-url .method.POST { background: #dcdcaa22; color: #dcdcaa; }
|
|
1977
|
+
.seq-request-url .method.PUT { background: #569cd622; color: #569cd6; }
|
|
1978
|
+
.seq-request-url .method.PATCH { background: #c586c022; color: #c586c0; }
|
|
1979
|
+
.seq-request-url .method.DELETE { background: #f14c4c22; color: #f14c4c; }
|
|
1980
|
+
|
|
1981
|
+
.seq-body-container {
|
|
1982
|
+
position: relative;
|
|
1983
|
+
}
|
|
1984
|
+
.seq-copy-btn {
|
|
1985
|
+
position: absolute;
|
|
1986
|
+
top: 0;
|
|
1987
|
+
right: 0;
|
|
1988
|
+
background: var(--vscode-button-secondaryBackground);
|
|
1989
|
+
color: var(--vscode-button-secondaryForeground);
|
|
1990
|
+
border: none;
|
|
1991
|
+
padding: 4px 8px;
|
|
1992
|
+
cursor: pointer;
|
|
1993
|
+
font-size: 0.8em;
|
|
1994
|
+
border-radius: 3px;
|
|
1995
|
+
z-index: 1;
|
|
1996
|
+
}
|
|
1997
|
+
.seq-copy-btn:hover {
|
|
1998
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
1999
|
+
}
|
|
2000
|
+
.seq-compare-btn {
|
|
2001
|
+
position: absolute;
|
|
2002
|
+
top: 0;
|
|
2003
|
+
right: 60px;
|
|
2004
|
+
background: var(--vscode-button-secondaryBackground);
|
|
2005
|
+
color: var(--vscode-button-secondaryForeground);
|
|
2006
|
+
border: none;
|
|
2007
|
+
padding: 4px 8px;
|
|
2008
|
+
cursor: pointer;
|
|
2009
|
+
font-size: 0.8em;
|
|
2010
|
+
border-radius: 3px;
|
|
2011
|
+
z-index: 1;
|
|
2012
|
+
}
|
|
2013
|
+
.seq-compare-btn:hover {
|
|
2014
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
2015
|
+
}
|
|
2016
|
+
.seq-compare-btn.comparing {
|
|
2017
|
+
background: var(--vscode-inputValidation-infoBackground);
|
|
2018
|
+
border: 1px solid var(--vscode-inputValidation-infoBorder);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
.norn-toast {
|
|
2022
|
+
position: fixed;
|
|
2023
|
+
bottom: 20px;
|
|
2024
|
+
left: 50%;
|
|
2025
|
+
transform: translateX(-50%);
|
|
2026
|
+
background: var(--vscode-notifications-background);
|
|
2027
|
+
color: var(--vscode-notifications-foreground);
|
|
2028
|
+
border: 1px solid var(--vscode-notifications-border);
|
|
2029
|
+
padding: 10px 20px;
|
|
2030
|
+
border-radius: 4px;
|
|
2031
|
+
z-index: 1000;
|
|
2032
|
+
opacity: 0;
|
|
2033
|
+
transition: opacity 0.3s;
|
|
2034
|
+
pointer-events: none;
|
|
2035
|
+
}
|
|
2036
|
+
.norn-toast.visible {
|
|
2037
|
+
opacity: 1;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
pre {
|
|
2041
|
+
margin: 0;
|
|
2042
|
+
background: transparent;
|
|
2043
|
+
font-family: var(--vscode-editor-font-family);
|
|
2044
|
+
font-size: var(--vscode-editor-font-size);
|
|
2045
|
+
line-height: 1.5;
|
|
2046
|
+
overflow-x: auto;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
pre code { background: none; }
|
|
2050
|
+
|
|
2051
|
+
.errors-section {
|
|
2052
|
+
margin-top: 12px;
|
|
2053
|
+
padding: 12px 16px;
|
|
2054
|
+
background: linear-gradient(135deg, rgba(201, 85, 90, 0.2) 0%, rgba(168, 67, 71, 0.15) 100%);
|
|
2055
|
+
border-radius: 6px;
|
|
2056
|
+
border-left: 3px solid #c9555a;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
.errors-section h3 { margin-top: 0; color: var(--vscode-foreground); }
|
|
2060
|
+
.errors-section ul { margin: 0; padding-left: 20px; }
|
|
2061
|
+
.errors-section .error { color: var(--vscode-foreground); white-space: pre-wrap; line-height: 1.5; }
|
|
2062
|
+
.errors-section .warning { color: #cca700; }
|
|
2063
|
+
|
|
2064
|
+
.json-key { color: #9cdcfe; background: none; }
|
|
2065
|
+
.json-string { color: #ce9178; background: none; }
|
|
2066
|
+
.json-number { color: #b5cea8; background: none; }
|
|
2067
|
+
.json-boolean { color: #569cd6; background: none; }
|
|
2068
|
+
.json-null { color: #569cd6; background: none; }
|
|
2069
|
+
|
|
2070
|
+
.script-item { border-left: 3px solid #dcdcaa; }
|
|
2071
|
+
.script-icon { font-size: 1.1em; }
|
|
2072
|
+
.script-type {
|
|
2073
|
+
font-family: var(--vscode-editor-font-family);
|
|
2074
|
+
padding: 2px 8px;
|
|
2075
|
+
background: #dcdcaa22;
|
|
2076
|
+
border-radius: 3px;
|
|
2077
|
+
color: #dcdcaa;
|
|
2078
|
+
font-size: 0.85em;
|
|
2079
|
+
text-transform: uppercase;
|
|
2080
|
+
}
|
|
2081
|
+
.script-path {
|
|
2082
|
+
font-family: var(--vscode-editor-font-family);
|
|
2083
|
+
color: var(--vscode-descriptionForeground);
|
|
2084
|
+
}
|
|
2085
|
+
.capture-var {
|
|
2086
|
+
font-family: var(--vscode-editor-font-family);
|
|
2087
|
+
color: #9cdcfe;
|
|
2088
|
+
font-weight: bold;
|
|
2089
|
+
}
|
|
2090
|
+
.script-output {
|
|
2091
|
+
margin-bottom: 12px;
|
|
2092
|
+
}
|
|
2093
|
+
.script-output:last-child { margin-bottom: 0; }
|
|
2094
|
+
.output-label {
|
|
2095
|
+
font-size: 0.85em;
|
|
2096
|
+
color: var(--vscode-descriptionForeground);
|
|
2097
|
+
margin-bottom: 4px;
|
|
2098
|
+
text-transform: uppercase;
|
|
2099
|
+
}
|
|
2100
|
+
.script-output.stderr .output-label { color: #f14c4c; }
|
|
2101
|
+
.script-output.stderr pre { color: #f14c4c; }
|
|
2102
|
+
|
|
2103
|
+
.step-number {
|
|
2104
|
+
font-family: var(--vscode-editor-font-family);
|
|
2105
|
+
font-weight: bold;
|
|
2106
|
+
color: var(--vscode-descriptionForeground);
|
|
2107
|
+
min-width: 24px;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
.print-item {
|
|
2111
|
+
border-left: 3px solid #569cd6;
|
|
2112
|
+
}
|
|
2113
|
+
.print-item .response-header {
|
|
2114
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
2115
|
+
}
|
|
2116
|
+
.print-content {
|
|
2117
|
+
display: flex;
|
|
2118
|
+
align-items: center;
|
|
2119
|
+
gap: 12px;
|
|
2120
|
+
padding: 12px;
|
|
2121
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
2122
|
+
}
|
|
2123
|
+
.print-icon {
|
|
2124
|
+
font-size: 1em;
|
|
2125
|
+
}
|
|
2126
|
+
.print-title {
|
|
2127
|
+
font-family: var(--vscode-editor-font-family);
|
|
2128
|
+
color: #569cd6;
|
|
2129
|
+
flex: 1;
|
|
2130
|
+
}
|
|
2131
|
+
.print-body {
|
|
2132
|
+
background: var(--vscode-editor-background);
|
|
2133
|
+
}
|
|
2134
|
+
.print-body pre {
|
|
2135
|
+
margin: 0;
|
|
2136
|
+
white-space: pre-wrap;
|
|
2137
|
+
word-wrap: break-word;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
.assertion-item {
|
|
2141
|
+
border-left: 3px solid var(--vscode-descriptionForeground);
|
|
2142
|
+
}
|
|
2143
|
+
.assertion-item.assertion-passed {
|
|
2144
|
+
border-left-color: #4ec9b0;
|
|
2145
|
+
}
|
|
2146
|
+
.assertion-item.assertion-failed {
|
|
2147
|
+
border-left-color: #f14c4c;
|
|
2148
|
+
}
|
|
2149
|
+
.assertion-icon {
|
|
2150
|
+
font-size: 1.1em;
|
|
2151
|
+
font-weight: bold;
|
|
2152
|
+
}
|
|
2153
|
+
.assertion-icon.status-success { color: #4ec9b0; }
|
|
2154
|
+
.assertion-icon.status-error { color: #f14c4c; }
|
|
2155
|
+
.assertion-expr {
|
|
2156
|
+
font-family: var(--vscode-editor-font-family);
|
|
2157
|
+
flex: 1;
|
|
2158
|
+
}
|
|
2159
|
+
.assertion-expr code {
|
|
2160
|
+
background: var(--vscode-textCodeBlock-background);
|
|
2161
|
+
padding: 2px 6px;
|
|
2162
|
+
border-radius: 3px;
|
|
2163
|
+
}
|
|
2164
|
+
.assertion-body {
|
|
2165
|
+
padding: 12px 16px;
|
|
2166
|
+
background: var(--vscode-editor-background);
|
|
2167
|
+
}
|
|
2168
|
+
.assertion-message {
|
|
2169
|
+
color: var(--vscode-descriptionForeground);
|
|
2170
|
+
margin-bottom: 8px;
|
|
2171
|
+
font-style: italic;
|
|
2172
|
+
}
|
|
2173
|
+
.assertion-details {
|
|
2174
|
+
font-family: var(--vscode-editor-font-family);
|
|
2175
|
+
font-size: 0.9em;
|
|
2176
|
+
}
|
|
2177
|
+
.assertion-expected, .assertion-actual {
|
|
2178
|
+
margin: 4px 0;
|
|
2179
|
+
}
|
|
2180
|
+
.assertion-expected code, .assertion-actual code {
|
|
2181
|
+
background: var(--vscode-textCodeBlock-background);
|
|
2182
|
+
padding: 2px 6px;
|
|
2183
|
+
border-radius: 3px;
|
|
2184
|
+
}
|
|
2185
|
+
.assertion-error {
|
|
2186
|
+
color: #f14c4c;
|
|
2187
|
+
font-family: var(--vscode-editor-font-family);
|
|
2188
|
+
}
|
|
2189
|
+
.response-header.no-expand {
|
|
2190
|
+
cursor: default;
|
|
2191
|
+
}
|
|
2192
|
+
.response-header.no-expand:hover {
|
|
2193
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
2194
|
+
}
|
|
2195
|
+
</style>
|
|
2196
|
+
</head>
|
|
2197
|
+
<body>
|
|
2198
|
+
<div class="container">
|
|
2199
|
+
<div class="sequence-header">
|
|
2200
|
+
<span class="sequence-status ${statusClass}">${statusText}</span>
|
|
2201
|
+
<span class="sequence-name">${this._escapeHtml(sequenceName)}</span>
|
|
2202
|
+
<span class="meta">${runs.length} run${runs.length !== 1 ? 's' : ''}</span>
|
|
2203
|
+
<span class="meta">${passedRuns}/${runs.length} passed</span>
|
|
2204
|
+
<span class="meta">⏱ ${totalDuration}ms total</span>
|
|
2205
|
+
</div>
|
|
2206
|
+
|
|
2207
|
+
${runsHtml}
|
|
2208
|
+
|
|
2209
|
+
<div id="norn-toast" class="norn-toast"></div>
|
|
2210
|
+
</div>
|
|
2211
|
+
|
|
2212
|
+
<script>
|
|
2213
|
+
const vscode = acquireVsCodeApi();
|
|
2214
|
+
let compareBaseline = null;
|
|
2215
|
+
|
|
2216
|
+
function showToast(message) {
|
|
2217
|
+
const toast = document.getElementById('norn-toast');
|
|
2218
|
+
if (!toast) return;
|
|
2219
|
+
toast.textContent = message;
|
|
2220
|
+
toast.classList.add('visible');
|
|
2221
|
+
setTimeout(() => toast.classList.remove('visible'), 3000);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function copyResponseBody(stepId) {
|
|
2225
|
+
const bodyEl = document.getElementById(stepId + '-body-text');
|
|
2226
|
+
if (!bodyEl) return;
|
|
2227
|
+
const body = bodyEl.value || bodyEl.textContent || '';
|
|
2228
|
+
navigator.clipboard.writeText(body).then(() => {
|
|
2229
|
+
showToast('Copied response body');
|
|
2230
|
+
}).catch(() => {
|
|
2231
|
+
showToast('Copy failed');
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
function compareResponse(stepId) {
|
|
2236
|
+
const bodyEl = document.getElementById(stepId + '-body-text');
|
|
2237
|
+
if (!bodyEl) return;
|
|
2238
|
+
const body = bodyEl.value || bodyEl.textContent || '';
|
|
2239
|
+
|
|
2240
|
+
if (!compareBaseline) {
|
|
2241
|
+
compareBaseline = { stepId, body };
|
|
2242
|
+
document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
|
|
2243
|
+
const btn = document.querySelector('[data-compare-step="' + stepId + '"]');
|
|
2244
|
+
if (btn) btn.classList.add('comparing');
|
|
2245
|
+
showToast('Select another response to compare');
|
|
2246
|
+
} else {
|
|
2247
|
+
vscode.postMessage({ type: 'setBaseline', body: compareBaseline.body });
|
|
2248
|
+
vscode.postMessage({ type: 'compare', body: body });
|
|
2249
|
+
compareBaseline = null;
|
|
2250
|
+
document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
function toggleResponse(id) {
|
|
2255
|
+
const body = document.getElementById(id);
|
|
2256
|
+
const icon = document.getElementById('icon-' + id);
|
|
2257
|
+
|
|
2258
|
+
if (!body) return;
|
|
2259
|
+
|
|
2260
|
+
if (body.style.display === 'none') {
|
|
2261
|
+
body.style.display = 'block';
|
|
2262
|
+
if (icon) icon.classList.add('expanded');
|
|
2263
|
+
} else {
|
|
2264
|
+
body.style.display = 'none';
|
|
2265
|
+
if (icon) icon.classList.remove('expanded');
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
function toggleRunGroup(id) {
|
|
2270
|
+
const body = document.getElementById(id);
|
|
2271
|
+
const icon = document.getElementById('icon-' + id);
|
|
2272
|
+
|
|
2273
|
+
if (!body) return;
|
|
2274
|
+
|
|
2275
|
+
if (body.style.display === 'none') {
|
|
2276
|
+
body.style.display = 'block';
|
|
2277
|
+
if (icon) icon.classList.add('expanded');
|
|
2278
|
+
} else {
|
|
2279
|
+
body.style.display = 'none';
|
|
2280
|
+
if (icon) icon.classList.remove('expanded');
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
window.addEventListener('message', event => {
|
|
2285
|
+
const message = event.data;
|
|
2286
|
+
if (message.type === 'resetCompare') {
|
|
2287
|
+
compareBaseline = null;
|
|
2288
|
+
document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
</script>
|
|
2292
|
+
</body>
|
|
2293
|
+
</html>`;
|
|
2294
|
+
}
|
|
2295
|
+
_getSequenceHtmlContent(result) {
|
|
2296
|
+
const statusClass = result.success ? 'status-success' : 'status-error';
|
|
2297
|
+
const statusText = result.success ? 'PASSED' : 'FAILED';
|
|
2298
|
+
// Use ordered steps if available, otherwise fall back to grouped display
|
|
2299
|
+
const stepsHtml = result.steps && result.steps.length > 0
|
|
2300
|
+
? this._renderOrderedSteps(result.steps)
|
|
2301
|
+
: this._renderGroupedResults(result);
|
|
2302
|
+
const errorsHtml = result.errors.length > 0 ? `
|
|
2303
|
+
<div class="errors-section">
|
|
2304
|
+
<h3>Errors & Warnings</h3>
|
|
2305
|
+
<ul>
|
|
2306
|
+
${result.errors.map(e => `<li class="${e.startsWith('Warning') ? 'warning' : 'error'}">${this._escapeHtml(e)}</li>`).join('')}
|
|
2307
|
+
</ul>
|
|
2308
|
+
</div>
|
|
2309
|
+
` : '';
|
|
2310
|
+
return /*html*/ `
|
|
2311
|
+
<!DOCTYPE html>
|
|
2312
|
+
<html lang="en">
|
|
2313
|
+
<head>
|
|
2314
|
+
<meta charset="UTF-8">
|
|
2315
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2316
|
+
<title>Sequence Result</title>
|
|
2317
|
+
<style>
|
|
2318
|
+
* { box-sizing: border-box; }
|
|
2319
|
+
|
|
2320
|
+
body {
|
|
2321
|
+
font-family: var(--vscode-font-family);
|
|
2322
|
+
font-size: var(--vscode-font-size);
|
|
2323
|
+
color: var(--vscode-foreground);
|
|
2324
|
+
background-color: var(--vscode-editor-background);
|
|
2325
|
+
padding: 0;
|
|
2326
|
+
margin: 0;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
.container { padding: 16px; }
|
|
2330
|
+
|
|
2331
|
+
.sequence-header {
|
|
2332
|
+
display: flex;
|
|
2333
|
+
align-items: center;
|
|
2334
|
+
gap: 16px;
|
|
2335
|
+
padding: 16px;
|
|
2336
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
2337
|
+
border-radius: 6px;
|
|
2338
|
+
margin-bottom: 16px;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
.sequence-name {
|
|
2342
|
+
font-size: 1.2em;
|
|
2343
|
+
font-weight: bold;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
.sequence-status {
|
|
2347
|
+
padding: 4px 12px;
|
|
2348
|
+
border-radius: 4px;
|
|
2349
|
+
font-weight: bold;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
.status-success { background: #4ec9b0; color: #000; }
|
|
2353
|
+
.status-warning { background: #cca700; color: #000; }
|
|
2354
|
+
.status-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
|
|
2355
|
+
.status-redirect { background: #dcdcaa; color: #000; }
|
|
2356
|
+
.status-client-error { background: #ce9178; color: #000; }
|
|
2357
|
+
.status-server-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
|
|
2358
|
+
|
|
2359
|
+
.meta { color: var(--vscode-descriptionForeground); }
|
|
2360
|
+
|
|
2361
|
+
.retry-info {
|
|
2362
|
+
color: #dcdcaa;
|
|
2363
|
+
font-size: 0.85em;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
.response-item {
|
|
2367
|
+
border: 1px solid var(--vscode-panel-border);
|
|
2368
|
+
border-radius: 6px;
|
|
2369
|
+
margin-bottom: 8px;
|
|
2370
|
+
overflow: hidden;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
.response-header {
|
|
2374
|
+
display: flex;
|
|
2375
|
+
align-items: center;
|
|
2376
|
+
gap: 12px;
|
|
2377
|
+
padding: 12px;
|
|
2378
|
+
cursor: pointer;
|
|
2379
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
.response-header:hover {
|
|
2383
|
+
background: var(--vscode-list-hoverBackground);
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
.response-index {
|
|
2387
|
+
font-family: var(--vscode-editor-font-family);
|
|
2388
|
+
font-weight: bold;
|
|
2389
|
+
color: var(--vscode-symbolIcon-variableForeground, #9cdcfe);
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
.status-code {
|
|
2393
|
+
padding: 2px 8px;
|
|
2394
|
+
border-radius: 3px;
|
|
2395
|
+
font-size: 0.9em;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
.expand-icon {
|
|
2399
|
+
margin-left: auto;
|
|
2400
|
+
transition: transform 0.2s;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
.expand-icon.expanded {
|
|
2404
|
+
transform: rotate(90deg);
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
.response-body {
|
|
2408
|
+
padding: 12px;
|
|
2409
|
+
border-top: 1px solid var(--vscode-panel-border);
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
.seq-request-url {
|
|
2413
|
+
padding: 6px 12px;
|
|
2414
|
+
background: var(--vscode-textCodeBlock-background);
|
|
2415
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
2416
|
+
font-family: var(--vscode-editor-font-family);
|
|
2417
|
+
font-size: 0.9em;
|
|
2418
|
+
color: var(--vscode-foreground);
|
|
2419
|
+
display: flex;
|
|
2420
|
+
align-items: center;
|
|
2421
|
+
gap: 8px;
|
|
2422
|
+
margin: -12px -12px 12px -12px;
|
|
2423
|
+
}
|
|
2424
|
+
.seq-request-url .method {
|
|
2425
|
+
font-weight: bold;
|
|
2426
|
+
padding: 2px 6px;
|
|
2427
|
+
border-radius: 3px;
|
|
2428
|
+
font-size: 0.85em;
|
|
2429
|
+
}
|
|
2430
|
+
.seq-request-url .method.GET { background: #4ec9b022; color: #4ec9b0; }
|
|
2431
|
+
.seq-request-url .method.POST { background: #dcdcaa22; color: #dcdcaa; }
|
|
2432
|
+
.seq-request-url .method.PUT { background: #569cd622; color: #569cd6; }
|
|
2433
|
+
.seq-request-url .method.PATCH { background: #c586c022; color: #c586c0; }
|
|
2434
|
+
.seq-request-url .method.DELETE { background: #f14c4c22; color: #f14c4c; }
|
|
2435
|
+
|
|
2436
|
+
.seq-body-container {
|
|
2437
|
+
position: relative;
|
|
2438
|
+
}
|
|
2439
|
+
.seq-copy-btn {
|
|
2440
|
+
position: absolute;
|
|
2441
|
+
top: 0;
|
|
2442
|
+
right: 0;
|
|
2443
|
+
background: var(--vscode-button-secondaryBackground);
|
|
2444
|
+
color: var(--vscode-button-secondaryForeground);
|
|
2445
|
+
border: none;
|
|
2446
|
+
padding: 4px 8px;
|
|
2447
|
+
cursor: pointer;
|
|
2448
|
+
font-size: 0.8em;
|
|
2449
|
+
border-radius: 3px;
|
|
2450
|
+
z-index: 1;
|
|
2451
|
+
}
|
|
2452
|
+
.seq-copy-btn:hover {
|
|
2453
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
2454
|
+
}
|
|
2455
|
+
.seq-compare-btn {
|
|
2456
|
+
position: absolute;
|
|
2457
|
+
top: 0;
|
|
2458
|
+
right: 60px;
|
|
2459
|
+
background: var(--vscode-button-secondaryBackground);
|
|
2460
|
+
color: var(--vscode-button-secondaryForeground);
|
|
2461
|
+
border: none;
|
|
2462
|
+
padding: 4px 8px;
|
|
2463
|
+
cursor: pointer;
|
|
2464
|
+
font-size: 0.8em;
|
|
2465
|
+
border-radius: 3px;
|
|
2466
|
+
z-index: 1;
|
|
2467
|
+
}
|
|
2468
|
+
.seq-compare-btn:hover {
|
|
2469
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
2470
|
+
}
|
|
2471
|
+
.seq-compare-btn.comparing {
|
|
2472
|
+
background: var(--vscode-inputValidation-infoBackground);
|
|
2473
|
+
border: 1px solid var(--vscode-inputValidation-infoBorder);
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
.norn-toast {
|
|
2477
|
+
position: fixed;
|
|
2478
|
+
bottom: 20px;
|
|
2479
|
+
left: 50%;
|
|
2480
|
+
transform: translateX(-50%);
|
|
2481
|
+
background: var(--vscode-notifications-background);
|
|
2482
|
+
color: var(--vscode-notifications-foreground);
|
|
2483
|
+
border: 1px solid var(--vscode-notifications-border);
|
|
2484
|
+
padding: 10px 20px;
|
|
2485
|
+
border-radius: 4px;
|
|
2486
|
+
z-index: 1000;
|
|
2487
|
+
opacity: 0;
|
|
2488
|
+
transition: opacity 0.3s;
|
|
2489
|
+
pointer-events: none;
|
|
2490
|
+
}
|
|
2491
|
+
.norn-toast.visible {
|
|
2492
|
+
opacity: 1;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
pre {
|
|
2496
|
+
margin: 0;
|
|
2497
|
+
background: transparent;
|
|
2498
|
+
font-family: var(--vscode-editor-font-family);
|
|
2499
|
+
font-size: var(--vscode-editor-font-size);
|
|
2500
|
+
line-height: 1.5;
|
|
2501
|
+
overflow-x: auto;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
pre code { background: none; }
|
|
2505
|
+
|
|
2506
|
+
.errors-section {
|
|
2507
|
+
margin-top: 16px;
|
|
2508
|
+
padding: 12px 16px;
|
|
2509
|
+
background: linear-gradient(135deg, rgba(201, 85, 90, 0.2) 0%, rgba(168, 67, 71, 0.15) 100%);
|
|
2510
|
+
border-radius: 6px;
|
|
2511
|
+
border-left: 3px solid #c9555a;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
.errors-section h3 { margin-top: 0; color: var(--vscode-foreground); }
|
|
2515
|
+
.errors-section ul { margin: 0; padding-left: 20px; }
|
|
2516
|
+
.errors-section .error { color: var(--vscode-foreground); white-space: pre-wrap; line-height: 1.5; }
|
|
2517
|
+
.errors-section .warning { color: #cca700; }
|
|
2518
|
+
|
|
2519
|
+
/* JSON Syntax Highlighting */
|
|
2520
|
+
.json-key { color: #9cdcfe; background: none; }
|
|
2521
|
+
.json-string { color: #ce9178; background: none; }
|
|
2522
|
+
.json-number { color: #b5cea8; background: none; }
|
|
2523
|
+
.json-boolean { color: #569cd6; background: none; }
|
|
2524
|
+
.json-null { color: #569cd6; background: none; }
|
|
2525
|
+
|
|
2526
|
+
/* Script Result Styling */
|
|
2527
|
+
.script-item { border-left: 3px solid #dcdcaa; }
|
|
2528
|
+
.script-icon { font-size: 1.1em; }
|
|
2529
|
+
.script-type {
|
|
2530
|
+
font-family: var(--vscode-editor-font-family);
|
|
2531
|
+
padding: 2px 8px;
|
|
2532
|
+
background: #dcdcaa22;
|
|
2533
|
+
border-radius: 3px;
|
|
2534
|
+
color: #dcdcaa;
|
|
2535
|
+
font-size: 0.85em;
|
|
2536
|
+
text-transform: uppercase;
|
|
2537
|
+
}
|
|
2538
|
+
.script-path {
|
|
2539
|
+
font-family: var(--vscode-editor-font-family);
|
|
2540
|
+
color: var(--vscode-descriptionForeground);
|
|
2541
|
+
}
|
|
2542
|
+
.capture-var {
|
|
2543
|
+
font-family: var(--vscode-editor-font-family);
|
|
2544
|
+
color: #9cdcfe;
|
|
2545
|
+
font-weight: bold;
|
|
2546
|
+
}
|
|
2547
|
+
.script-output {
|
|
2548
|
+
margin-bottom: 12px;
|
|
2549
|
+
}
|
|
2550
|
+
.script-output:last-child { margin-bottom: 0; }
|
|
2551
|
+
.output-label {
|
|
2552
|
+
font-size: 0.85em;
|
|
2553
|
+
color: var(--vscode-descriptionForeground);
|
|
2554
|
+
margin-bottom: 4px;
|
|
2555
|
+
text-transform: uppercase;
|
|
2556
|
+
}
|
|
2557
|
+
.script-output.stderr .output-label { color: #f14c4c; }
|
|
2558
|
+
.script-output.stderr pre { color: #f14c4c; }
|
|
2559
|
+
|
|
2560
|
+
.step-number {
|
|
2561
|
+
font-family: var(--vscode-editor-font-family);
|
|
2562
|
+
font-weight: bold;
|
|
2563
|
+
color: var(--vscode-descriptionForeground);
|
|
2564
|
+
min-width: 24px;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
/* Print styling */
|
|
2568
|
+
.print-item {
|
|
2569
|
+
border-left: 3px solid #569cd6;
|
|
2570
|
+
}
|
|
2571
|
+
.print-item .response-header {
|
|
2572
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
2573
|
+
}
|
|
2574
|
+
.print-content {
|
|
2575
|
+
display: flex;
|
|
2576
|
+
align-items: center;
|
|
2577
|
+
gap: 12px;
|
|
2578
|
+
padding: 12px;
|
|
2579
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
2580
|
+
}
|
|
2581
|
+
.print-icon {
|
|
2582
|
+
font-size: 1em;
|
|
2583
|
+
}
|
|
2584
|
+
.print-title {
|
|
2585
|
+
font-family: var(--vscode-editor-font-family);
|
|
2586
|
+
color: #569cd6;
|
|
2587
|
+
flex: 1;
|
|
2588
|
+
}
|
|
2589
|
+
.print-body {
|
|
2590
|
+
background: var(--vscode-editor-background);
|
|
2591
|
+
}
|
|
2592
|
+
.print-body pre {
|
|
2593
|
+
margin: 0;
|
|
2594
|
+
white-space: pre-wrap;
|
|
2595
|
+
word-wrap: break-word;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
/* Assertion styling */
|
|
2599
|
+
.assertion-item {
|
|
2600
|
+
border-left: 3px solid var(--vscode-descriptionForeground);
|
|
2601
|
+
}
|
|
2602
|
+
.assertion-item.assertion-passed {
|
|
2603
|
+
border-left-color: #4ec9b0;
|
|
2604
|
+
}
|
|
2605
|
+
.assertion-item.assertion-failed {
|
|
2606
|
+
border-left-color: #f14c4c;
|
|
2607
|
+
}
|
|
2608
|
+
.assertion-icon {
|
|
2609
|
+
font-size: 1.1em;
|
|
2610
|
+
font-weight: bold;
|
|
2611
|
+
}
|
|
2612
|
+
.assertion-icon.status-success { color: #4ec9b0; }
|
|
2613
|
+
.assertion-icon.status-error { color: #f14c4c; }
|
|
2614
|
+
.assertion-expr {
|
|
2615
|
+
font-family: var(--vscode-editor-font-family);
|
|
2616
|
+
flex: 1;
|
|
2617
|
+
}
|
|
2618
|
+
.assertion-expr code {
|
|
2619
|
+
background: var(--vscode-textCodeBlock-background);
|
|
2620
|
+
padding: 2px 6px;
|
|
2621
|
+
border-radius: 3px;
|
|
2622
|
+
}
|
|
2623
|
+
.assertion-body {
|
|
2624
|
+
padding: 12px 16px;
|
|
2625
|
+
background: var(--vscode-editor-background);
|
|
2626
|
+
}
|
|
2627
|
+
.assertion-message {
|
|
2628
|
+
color: var(--vscode-descriptionForeground);
|
|
2629
|
+
margin-bottom: 8px;
|
|
2630
|
+
font-style: italic;
|
|
2631
|
+
}
|
|
2632
|
+
.assertion-details {
|
|
2633
|
+
font-family: var(--vscode-editor-font-family);
|
|
2634
|
+
font-size: 0.9em;
|
|
2635
|
+
}
|
|
2636
|
+
.assertion-expected, .assertion-actual {
|
|
2637
|
+
margin: 4px 0;
|
|
2638
|
+
}
|
|
2639
|
+
.assertion-expected code, .assertion-actual code {
|
|
2640
|
+
background: var(--vscode-textCodeBlock-background);
|
|
2641
|
+
padding: 2px 6px;
|
|
2642
|
+
border-radius: 3px;
|
|
2643
|
+
}
|
|
2644
|
+
.assertion-error {
|
|
2645
|
+
color: #f14c4c;
|
|
2646
|
+
font-family: var(--vscode-editor-font-family);
|
|
2647
|
+
}
|
|
2648
|
+
.response-header.no-expand {
|
|
2649
|
+
cursor: default;
|
|
2650
|
+
}
|
|
2651
|
+
.response-header.no-expand:hover {
|
|
2652
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
2653
|
+
}
|
|
2654
|
+
</style>
|
|
2655
|
+
</head>
|
|
2656
|
+
<body>
|
|
2657
|
+
<div class="container">
|
|
2658
|
+
<div class="sequence-header">
|
|
2659
|
+
<span class="sequence-status ${statusClass}">${statusText}</span>
|
|
2660
|
+
<span class="sequence-name">${this._escapeHtml(result.name)}</span>
|
|
2661
|
+
<span class="meta">${result.steps?.length || result.responses.length} step${(result.steps?.length || result.responses.length) !== 1 ? 's' : ''}</span>
|
|
2662
|
+
<span class="meta">⏱ ${result.duration}ms total</span>
|
|
2663
|
+
</div>
|
|
2664
|
+
|
|
2665
|
+
<div class="steps-list">
|
|
2666
|
+
${stepsHtml}
|
|
2667
|
+
</div>
|
|
2668
|
+
|
|
2669
|
+
${errorsHtml}
|
|
2670
|
+
|
|
2671
|
+
<div id="norn-toast" class="norn-toast"></div>
|
|
2672
|
+
</div>
|
|
2673
|
+
|
|
2674
|
+
<script>
|
|
2675
|
+
const vscode = acquireVsCodeApi();
|
|
2676
|
+
let compareBaseline = null;
|
|
2677
|
+
|
|
2678
|
+
function showToast(message) {
|
|
2679
|
+
const toast = document.getElementById('norn-toast');
|
|
2680
|
+
if (!toast) return;
|
|
2681
|
+
toast.textContent = message;
|
|
2682
|
+
toast.classList.add('visible');
|
|
2683
|
+
setTimeout(() => toast.classList.remove('visible'), 3000);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
function copyResponseBody(stepId) {
|
|
2687
|
+
const bodyEl = document.getElementById(stepId + '-body-text');
|
|
2688
|
+
if (!bodyEl) return;
|
|
2689
|
+
const body = bodyEl.value || bodyEl.textContent || '';
|
|
2690
|
+
navigator.clipboard.writeText(body).then(() => {
|
|
2691
|
+
showToast('Copied response body');
|
|
2692
|
+
}).catch(() => {
|
|
2693
|
+
showToast('Copy failed');
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
function compareResponse(stepId) {
|
|
2698
|
+
const bodyEl = document.getElementById(stepId + '-body-text');
|
|
2699
|
+
if (!bodyEl) return;
|
|
2700
|
+
const body = bodyEl.value || bodyEl.textContent || '';
|
|
2701
|
+
|
|
2702
|
+
if (!compareBaseline) {
|
|
2703
|
+
compareBaseline = { stepId, body };
|
|
2704
|
+
document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
|
|
2705
|
+
const btn = document.querySelector('[data-compare-step="' + stepId + '"]');
|
|
2706
|
+
if (btn) btn.classList.add('comparing');
|
|
2707
|
+
showToast('Select another response to compare');
|
|
2708
|
+
} else {
|
|
2709
|
+
vscode.postMessage({ type: 'setBaseline', body: compareBaseline.body });
|
|
2710
|
+
vscode.postMessage({ type: 'compare', body: body });
|
|
2711
|
+
compareBaseline = null;
|
|
2712
|
+
document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function toggleResponse(id) {
|
|
2717
|
+
const body = document.getElementById(id);
|
|
2718
|
+
const icon = document.getElementById('icon-' + id);
|
|
2719
|
+
|
|
2720
|
+
if (!body) return;
|
|
2721
|
+
|
|
2722
|
+
if (body.style.display === 'none') {
|
|
2723
|
+
body.style.display = 'block';
|
|
2724
|
+
if (icon) icon.classList.add('expanded');
|
|
2725
|
+
} else {
|
|
2726
|
+
body.style.display = 'none';
|
|
2727
|
+
if (icon) icon.classList.remove('expanded');
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
window.addEventListener('message', event => {
|
|
2732
|
+
const message = event.data;
|
|
2733
|
+
if (message.type === 'resetCompare') {
|
|
2734
|
+
compareBaseline = null;
|
|
2735
|
+
document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
|
|
2736
|
+
}
|
|
2737
|
+
});
|
|
2738
|
+
</script>
|
|
2739
|
+
</body>
|
|
2740
|
+
</html>`;
|
|
2741
|
+
}
|
|
2742
|
+
_renderOrderedSteps(steps, stepIdPrefix = 'step') {
|
|
2743
|
+
let requestCounter = 0;
|
|
2744
|
+
return steps.map((step, index) => {
|
|
2745
|
+
const stepId = `${stepIdPrefix}-${index}`;
|
|
2746
|
+
if (step.type === 'print' && step.print) {
|
|
2747
|
+
const hasBody = step.print.body && step.print.body.trim();
|
|
2748
|
+
if (hasBody) {
|
|
2749
|
+
// Print with expandable body
|
|
2750
|
+
return `
|
|
2751
|
+
<div class="response-item print-item">
|
|
2752
|
+
<div class="response-header" onclick="toggleResponse('${stepId}')">
|
|
2753
|
+
<span class="step-number">${index + 1}.</span>
|
|
2754
|
+
<span class="print-icon">📝</span>
|
|
2755
|
+
<span class="print-title">${this._escapeHtml(step.print.title)}</span>
|
|
2756
|
+
<span class="meta">@ ${step.print.timestamp}ms</span>
|
|
2757
|
+
<span class="expand-icon" id="icon-${stepId}">▶</span>
|
|
2758
|
+
</div>
|
|
2759
|
+
<div class="response-body print-body" id="${stepId}" style="display: none;">
|
|
2760
|
+
<pre><code>${this._escapeHtml(step.print.body)}</code></pre>
|
|
2761
|
+
</div>
|
|
2762
|
+
</div>
|
|
2763
|
+
`;
|
|
2764
|
+
}
|
|
2765
|
+
else {
|
|
2766
|
+
// Print without body - simple display
|
|
2767
|
+
return `
|
|
2768
|
+
<div class="response-item print-item">
|
|
2769
|
+
<div class="print-content">
|
|
2770
|
+
<span class="step-number">${index + 1}.</span>
|
|
2771
|
+
<span class="print-icon">📝</span>
|
|
2772
|
+
<span class="print-title">${this._escapeHtml(step.print.title)}</span>
|
|
2773
|
+
<span class="meta">@ ${step.print.timestamp}ms</span>
|
|
2774
|
+
</div>
|
|
2775
|
+
</div>
|
|
2776
|
+
`;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
else if (step.type === 'request' && step.response) {
|
|
2780
|
+
requestCounter++;
|
|
2781
|
+
const response = step.response;
|
|
2782
|
+
const respStatusClass = this._getStatusClass(response.status);
|
|
2783
|
+
const bodyContent = this._formatBody(response.body);
|
|
2784
|
+
const rawBody = typeof response.body === 'object'
|
|
2785
|
+
? JSON.stringify(response.body, null, 2)
|
|
2786
|
+
: String(response.body ?? '');
|
|
2787
|
+
const method = step.requestMethod || 'GET';
|
|
2788
|
+
const url = step.requestUrl || '';
|
|
2789
|
+
return `
|
|
2790
|
+
<div class="response-item">
|
|
2791
|
+
<div class="response-header" onclick="toggleResponse('${stepId}')">
|
|
2792
|
+
<span class="step-number">${index + 1}.</span>
|
|
2793
|
+
<span class="response-index">$${requestCounter}</span>
|
|
2794
|
+
<span class="status-code ${respStatusClass}">${response.status} ${response.statusText}</span>
|
|
2795
|
+
<span class="meta">⏱ ${response.duration}ms</span>
|
|
2796
|
+
<span class="expand-icon" id="icon-${stepId}">▶</span>
|
|
2797
|
+
</div>
|
|
2798
|
+
<div class="response-body" id="${stepId}" style="display: none;">
|
|
2799
|
+
<div class="seq-request-url">
|
|
2800
|
+
<span class="method ${method}">${method}</span>
|
|
2801
|
+
<span>${this._escapeHtml(url)}</span>
|
|
2802
|
+
</div>
|
|
2803
|
+
<div class="seq-body-container">
|
|
2804
|
+
<button class="seq-compare-btn" data-compare-step="${stepId}" onclick="compareResponse('${stepId}')">⇄ Compare</button>
|
|
2805
|
+
<button class="seq-copy-btn" onclick="copyResponseBody('${stepId}')">📋 Copy</button>
|
|
2806
|
+
<pre><code>${bodyContent}</code></pre>
|
|
2807
|
+
<textarea id="${stepId}-body-text" style="display:none;">${this._escapeHtml(rawBody)}</textarea>
|
|
2808
|
+
</div>
|
|
2809
|
+
</div>
|
|
2810
|
+
</div>
|
|
2811
|
+
`;
|
|
2812
|
+
}
|
|
2813
|
+
else if (step.type === 'script' && step.script) {
|
|
2814
|
+
const script = step.script;
|
|
2815
|
+
const hasStderr = script.error && script.error.trim();
|
|
2816
|
+
// Show error status if exit code non-zero, or warning if there's stderr output
|
|
2817
|
+
const scriptStatusClass = !script.success ? 'status-error' : (hasStderr ? 'status-warning' : 'status-success');
|
|
2818
|
+
const scriptStatusText = !script.success ? `Exit ${script.exitCode}` : (hasStderr ? 'Warning' : 'OK');
|
|
2819
|
+
const hasOutput = script.output.trim() || script.error.trim();
|
|
2820
|
+
return `
|
|
2821
|
+
<div class="response-item script-item">
|
|
2822
|
+
<div class="response-header" onclick="toggleResponse('${stepId}')">
|
|
2823
|
+
<span class="step-number">${index + 1}.</span>
|
|
2824
|
+
<span class="script-icon">⚡</span>
|
|
2825
|
+
<span class="script-type">${script.type}</span>
|
|
2826
|
+
<span class="script-path">${this._escapeHtml(script.scriptPath)}</span>
|
|
2827
|
+
${script.captureVar ? `<span class="capture-var">→ ${this._escapeHtml(script.captureVar)}</span>` : ''}
|
|
2828
|
+
<span class="status-code ${scriptStatusClass}">${scriptStatusText}</span>
|
|
2829
|
+
<span class="meta">⏱ ${script.duration}ms</span>
|
|
2830
|
+
<span class="expand-icon" id="icon-${stepId}">${hasOutput ? '▶' : ''}</span>
|
|
2831
|
+
</div>
|
|
2832
|
+
${hasOutput ? `
|
|
2833
|
+
<div class="response-body" id="${stepId}" style="display: none;">
|
|
2834
|
+
${script.output.trim() ? `<div class="script-output"><div class="output-label">stdout:</div><pre><code>${this._escapeHtml(script.output)}</code></pre></div>` : ''}
|
|
2835
|
+
${script.error.trim() ? `<div class="script-output stderr"><div class="output-label">stderr:</div><pre><code>${this._escapeHtml(script.error)}</code></pre></div>` : ''}
|
|
2836
|
+
</div>
|
|
2837
|
+
` : ''}
|
|
2838
|
+
</div>
|
|
2839
|
+
`;
|
|
2840
|
+
}
|
|
2841
|
+
else if (step.type === 'assertion' && step.assertion) {
|
|
2842
|
+
const assertion = step.assertion;
|
|
2843
|
+
const assertionClass = assertion.passed ? 'status-success' : 'status-error';
|
|
2844
|
+
const assertionIcon = assertion.passed ? '✓' : '✗';
|
|
2845
|
+
const hasDetails = !assertion.passed || assertion.message;
|
|
2846
|
+
let detailsHtml = '';
|
|
2847
|
+
if (hasDetails) {
|
|
2848
|
+
if (assertion.error) {
|
|
2849
|
+
detailsHtml = `<div class="assertion-error">${this._escapeHtml(assertion.error)}</div>`;
|
|
2850
|
+
}
|
|
2851
|
+
else if (!assertion.passed) {
|
|
2852
|
+
detailsHtml = `<div class="assertion-details">` +
|
|
2853
|
+
`<div class="assertion-expected">Expected: <code>${this._escapeHtml(assertion.rightExpression || assertion.operator)}</code></div>` +
|
|
2854
|
+
`<div class="assertion-actual">Actual: <code>${this._escapeHtml(JSON.stringify(assertion.leftValue))}</code></div>` +
|
|
2855
|
+
`</div>`;
|
|
2856
|
+
}
|
|
2857
|
+
if (assertion.message) {
|
|
2858
|
+
detailsHtml = `<div class="assertion-message">${this._escapeHtml(assertion.message)}</div>` + detailsHtml;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
return `
|
|
2862
|
+
<div class="response-item assertion-item ${assertion.passed ? 'assertion-passed' : 'assertion-failed'}">
|
|
2863
|
+
<div class="response-header${hasDetails ? '' : ' no-expand'}"${hasDetails ? ` onclick="toggleResponse('${stepId}')"` : ''}>
|
|
2864
|
+
<span class="step-number">${index + 1}.</span>
|
|
2865
|
+
<span class="assertion-icon ${assertionClass}">${assertionIcon}</span>
|
|
2866
|
+
<span class="assertion-expr">assert <code>${this._escapeHtml(assertion.expression)}</code></span>
|
|
2867
|
+
${hasDetails ? `<span class="expand-icon" id="icon-${stepId}">▶</span>` : ''}
|
|
2868
|
+
</div>
|
|
2869
|
+
${hasDetails ? `
|
|
2870
|
+
<div class="response-body assertion-body" id="${stepId}" style="display: none;">
|
|
2871
|
+
${detailsHtml}
|
|
2872
|
+
</div>
|
|
2873
|
+
` : ''}
|
|
2874
|
+
</div>
|
|
2875
|
+
`;
|
|
2876
|
+
}
|
|
2877
|
+
return '';
|
|
2878
|
+
}).join('');
|
|
2879
|
+
}
|
|
2880
|
+
_renderGroupedResults(result, idPrefix = 'grouped') {
|
|
2881
|
+
// Fallback for older results without ordered steps
|
|
2882
|
+
const responsesHtml = result.responses.map((response, index) => {
|
|
2883
|
+
const responseId = `${idPrefix}-resp-${index}`;
|
|
2884
|
+
const respStatusClass = this._getStatusClass(response.status);
|
|
2885
|
+
const bodyContent = this._formatBody(response.body);
|
|
2886
|
+
const rawBody = typeof response.body === 'object'
|
|
2887
|
+
? JSON.stringify(response.body, null, 2)
|
|
2888
|
+
: String(response.body ?? '');
|
|
2889
|
+
return `
|
|
2890
|
+
<div class="response-item">
|
|
2891
|
+
<div class="response-header" onclick="toggleResponse('${responseId}')">
|
|
2892
|
+
<span class="response-index">$${index + 1}</span>
|
|
2893
|
+
<span class="status-code ${respStatusClass}">${response.status} ${response.statusText}</span>
|
|
2894
|
+
<span class="meta">⏱ ${response.duration}ms</span>
|
|
2895
|
+
<span class="expand-icon" id="icon-${responseId}">▶</span>
|
|
2896
|
+
</div>
|
|
2897
|
+
<div class="response-body" id="${responseId}" style="display: none;">
|
|
2898
|
+
<div class="seq-body-container">
|
|
2899
|
+
<button class="seq-compare-btn" data-compare-step="${responseId}" onclick="compareResponse('${responseId}')">⇄ Compare</button>
|
|
2900
|
+
<button class="seq-copy-btn" onclick="copyResponseBody('${responseId}')">📋 Copy</button>
|
|
2901
|
+
<pre><code>${bodyContent}</code></pre>
|
|
2902
|
+
<textarea id="${responseId}-body-text" style="display:none;">${this._escapeHtml(rawBody)}</textarea>
|
|
2903
|
+
</div>
|
|
2904
|
+
</div>
|
|
2905
|
+
</div>
|
|
2906
|
+
`;
|
|
2907
|
+
}).join('');
|
|
2908
|
+
const scriptResultsHtml = result.scriptResults && result.scriptResults.length > 0 ? result.scriptResults.map((script, index) => {
|
|
2909
|
+
const scriptId = `${idPrefix}-script-${index}`;
|
|
2910
|
+
const hasStderr = script.error && script.error.trim();
|
|
2911
|
+
// Show error status if exit code non-zero, or warning if there's stderr output
|
|
2912
|
+
const scriptStatusClass = !script.success ? 'status-error' : (hasStderr ? 'status-warning' : 'status-success');
|
|
2913
|
+
const scriptStatusText = !script.success ? `Exit ${script.exitCode}` : (hasStderr ? 'Warning' : 'OK');
|
|
2914
|
+
const hasOutput = script.output.trim() || script.error.trim();
|
|
2915
|
+
return `
|
|
2916
|
+
<div class="response-item script-item">
|
|
2917
|
+
<div class="response-header" onclick="toggleResponse('${scriptId}')">
|
|
2918
|
+
<span class="script-icon">⚡</span>
|
|
2919
|
+
<span class="script-type">${script.type}</span>
|
|
2920
|
+
<span class="script-path">${this._escapeHtml(script.scriptPath)}</span>
|
|
2921
|
+
${script.captureVar ? `<span class="capture-var">→ ${this._escapeHtml(script.captureVar)}</span>` : ''}
|
|
2922
|
+
<span class="status-code ${scriptStatusClass}">${scriptStatusText}</span>
|
|
2923
|
+
<span class="meta">⏱ ${script.duration}ms</span>
|
|
2924
|
+
<span class="expand-icon" id="icon-${scriptId}">${hasOutput ? '▶' : ''}</span>
|
|
2925
|
+
</div>
|
|
2926
|
+
${hasOutput ? `
|
|
2927
|
+
<div class="response-body" id="${scriptId}" style="display: none;">
|
|
2928
|
+
${script.output.trim() ? `<div class="script-output"><div class="output-label">stdout:</div><pre><code>${this._escapeHtml(script.output)}</code></pre></div>` : ''}
|
|
2929
|
+
${script.error.trim() ? `<div class="script-output stderr"><div class="output-label">stderr:</div><pre><code>${this._escapeHtml(script.error)}</code></pre></div>` : ''}
|
|
2930
|
+
</div>
|
|
2931
|
+
` : ''}
|
|
2932
|
+
</div>
|
|
2933
|
+
`;
|
|
2934
|
+
}).join('') : '';
|
|
2935
|
+
return responsesHtml + scriptResultsHtml;
|
|
2936
|
+
}
|
|
2937
|
+
_updateError(method, url, errorMessage) {
|
|
2938
|
+
this._panel.title = `Error - ${method}`;
|
|
2939
|
+
this._panel.webview.html = this._getErrorHtmlContent(method, url, errorMessage);
|
|
2940
|
+
}
|
|
2941
|
+
_getErrorHtmlContent(method, url, errorMessage) {
|
|
2942
|
+
return /*html*/ `
|
|
2943
|
+
<!DOCTYPE html>
|
|
2944
|
+
<html lang="en">
|
|
2945
|
+
<head>
|
|
2946
|
+
<meta charset="UTF-8">
|
|
2947
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2948
|
+
<title>Error</title>
|
|
2949
|
+
<style>
|
|
2950
|
+
* {
|
|
2951
|
+
box-sizing: border-box;
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
body {
|
|
2955
|
+
font-family: var(--vscode-font-family);
|
|
2956
|
+
font-size: var(--vscode-font-size);
|
|
2957
|
+
color: var(--vscode-foreground);
|
|
2958
|
+
background-color: var(--vscode-editor-background);
|
|
2959
|
+
padding: 0;
|
|
2960
|
+
margin: 0;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
.container {
|
|
2964
|
+
padding: 16px;
|
|
2965
|
+
max-width: 100%;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
.error-bar {
|
|
2969
|
+
display: flex;
|
|
2970
|
+
align-items: center;
|
|
2971
|
+
gap: 16px;
|
|
2972
|
+
padding: 12px 16px;
|
|
2973
|
+
background: linear-gradient(135deg, #c9555a 0%, #a84347 100%);
|
|
2974
|
+
border: none;
|
|
2975
|
+
border-radius: 6px;
|
|
2976
|
+
margin-bottom: 16px;
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
.error-icon {
|
|
2980
|
+
font-size: 1.5em;
|
|
2981
|
+
filter: brightness(0) invert(1);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
.error-title {
|
|
2985
|
+
font-weight: bold;
|
|
2986
|
+
color: #ffffff;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
.request-info {
|
|
2990
|
+
font-family: var(--vscode-editor-font-family);
|
|
2991
|
+
font-size: 0.85em;
|
|
2992
|
+
color: var(--vscode-descriptionForeground);
|
|
2993
|
+
word-break: break-all;
|
|
2994
|
+
margin-bottom: 16px;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
.method {
|
|
2998
|
+
font-weight: bold;
|
|
2999
|
+
color: var(--vscode-symbolIcon-methodForeground, #dcdcaa);
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
.error-message {
|
|
3003
|
+
background: rgba(201, 85, 90, 0.15);
|
|
3004
|
+
padding: 16px;
|
|
3005
|
+
border-radius: 6px;
|
|
3006
|
+
font-family: var(--vscode-editor-font-family);
|
|
3007
|
+
font-size: var(--vscode-editor-font-size);
|
|
3008
|
+
line-height: 1.6;
|
|
3009
|
+
color: var(--vscode-foreground);
|
|
3010
|
+
border-left: 3px solid #c9555a;
|
|
3011
|
+
white-space: pre-wrap;
|
|
3012
|
+
word-break: break-word;
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
.help-section {
|
|
3016
|
+
margin-top: 24px;
|
|
3017
|
+
padding: 16px;
|
|
3018
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
3019
|
+
border-radius: 6px;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
.help-title {
|
|
3023
|
+
font-weight: bold;
|
|
3024
|
+
margin-bottom: 8px;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
.help-list {
|
|
3028
|
+
margin: 0;
|
|
3029
|
+
padding-left: 20px;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
.help-list li {
|
|
3033
|
+
margin-bottom: 4px;
|
|
3034
|
+
}
|
|
3035
|
+
</style>
|
|
3036
|
+
</head>
|
|
3037
|
+
<body>
|
|
3038
|
+
<div class="container">
|
|
3039
|
+
<div class="error-bar">
|
|
3040
|
+
<span class="error-icon">⚠️</span>
|
|
3041
|
+
<span class="error-title">Request Failed</span>
|
|
3042
|
+
</div>
|
|
3043
|
+
|
|
3044
|
+
<div class="request-info">
|
|
3045
|
+
<span class="method">${method}</span> ${this._escapeHtml(url)}
|
|
3046
|
+
</div>
|
|
3047
|
+
|
|
3048
|
+
<div class="error-message">
|
|
3049
|
+
${this._escapeHtml(errorMessage)}
|
|
3050
|
+
</div>
|
|
3051
|
+
|
|
3052
|
+
<div class="help-section">
|
|
3053
|
+
<div class="help-title">Common causes:</div>
|
|
3054
|
+
<ul class="help-list">
|
|
3055
|
+
<li>Invalid or malformed URL</li>
|
|
3056
|
+
<li>Network connectivity issues</li>
|
|
3057
|
+
<li>Server is unreachable or down</li>
|
|
3058
|
+
<li>DNS resolution failed</li>
|
|
3059
|
+
<li>Request timed out</li>
|
|
3060
|
+
<li>SSL/TLS certificate issues</li>
|
|
3061
|
+
</ul>
|
|
3062
|
+
</div>
|
|
3063
|
+
</div>
|
|
3064
|
+
</body>
|
|
3065
|
+
</html>`;
|
|
3066
|
+
}
|
|
3067
|
+
_getHtmlContent(method, url, response) {
|
|
3068
|
+
const statusClass = this._getStatusClass(response.status);
|
|
3069
|
+
const bodyContent = this._formatBody(response.body);
|
|
3070
|
+
const headersHtml = this._formatHeaders(response.headers);
|
|
3071
|
+
const cookiesHtml = this._formatCookies(response.cookies);
|
|
3072
|
+
const bodySize = this._getBodySize(response.body);
|
|
3073
|
+
return /*html*/ `
|
|
3074
|
+
<!DOCTYPE html>
|
|
3075
|
+
<html lang="en">
|
|
3076
|
+
<head>
|
|
3077
|
+
<meta charset="UTF-8">
|
|
3078
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3079
|
+
<title>Response</title>
|
|
3080
|
+
<style>
|
|
3081
|
+
:root {
|
|
3082
|
+
--success-color: #4ec9b0;
|
|
3083
|
+
--redirect-color: #dcdcaa;
|
|
3084
|
+
--client-error-color: #ce9178;
|
|
3085
|
+
--server-error-color: #f14c4c;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
* {
|
|
3089
|
+
box-sizing: border-box;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
body {
|
|
3093
|
+
font-family: var(--vscode-font-family);
|
|
3094
|
+
font-size: var(--vscode-font-size);
|
|
3095
|
+
color: var(--vscode-foreground);
|
|
3096
|
+
background-color: var(--vscode-editor-background);
|
|
3097
|
+
padding: 0;
|
|
3098
|
+
margin: 0;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
.container {
|
|
3102
|
+
padding: 16px;
|
|
3103
|
+
max-width: 100%;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
.status-bar {
|
|
3107
|
+
display: flex;
|
|
3108
|
+
align-items: center;
|
|
3109
|
+
gap: 16px;
|
|
3110
|
+
padding: 12px 16px;
|
|
3111
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
3112
|
+
border-radius: 6px;
|
|
3113
|
+
margin-bottom: 16px;
|
|
3114
|
+
flex-wrap: wrap;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
.status-code {
|
|
3118
|
+
font-weight: bold;
|
|
3119
|
+
font-size: 1.2em;
|
|
3120
|
+
padding: 4px 12px;
|
|
3121
|
+
border-radius: 4px;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
.status-success { background: var(--success-color); color: #000; }
|
|
3125
|
+
.status-warning { background: #cca700; color: #000; }
|
|
3126
|
+
.status-redirect { background: var(--redirect-color); color: #000; }
|
|
3127
|
+
.status-client-error { background: var(--client-error-color); color: #000; }
|
|
3128
|
+
.status-server-error { background: var(--server-error-color); color: #000; }
|
|
3129
|
+
|
|
3130
|
+
.meta {
|
|
3131
|
+
color: var(--vscode-descriptionForeground);
|
|
3132
|
+
font-size: 0.9em;
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
.request-info {
|
|
3136
|
+
font-family: var(--vscode-editor-font-family);
|
|
3137
|
+
font-size: 0.85em;
|
|
3138
|
+
color: var(--vscode-descriptionForeground);
|
|
3139
|
+
word-break: break-all;
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
.method {
|
|
3143
|
+
font-weight: bold;
|
|
3144
|
+
color: var(--vscode-symbolIcon-methodForeground, #dcdcaa);
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
.tabs {
|
|
3148
|
+
display: flex;
|
|
3149
|
+
gap: 0;
|
|
3150
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
3151
|
+
margin-bottom: 16px;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
.tab {
|
|
3155
|
+
padding: 8px 16px;
|
|
3156
|
+
cursor: pointer;
|
|
3157
|
+
border: none;
|
|
3158
|
+
background: transparent;
|
|
3159
|
+
color: var(--vscode-foreground);
|
|
3160
|
+
opacity: 0.7;
|
|
3161
|
+
border-bottom: 2px solid transparent;
|
|
3162
|
+
font-size: inherit;
|
|
3163
|
+
font-family: inherit;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
.tab:hover {
|
|
3167
|
+
opacity: 1;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
.tab.active {
|
|
3171
|
+
opacity: 1;
|
|
3172
|
+
border-bottom-color: var(--vscode-focusBorder);
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
.tab-content {
|
|
3176
|
+
display: none;
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
.tab-content.active {
|
|
3180
|
+
display: block;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
.body-container {
|
|
3184
|
+
position: relative;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
.copy-btn {
|
|
3188
|
+
position: absolute;
|
|
3189
|
+
top: 8px;
|
|
3190
|
+
right: 8px;
|
|
3191
|
+
padding: 6px 12px;
|
|
3192
|
+
background: var(--vscode-button-background);
|
|
3193
|
+
color: var(--vscode-button-foreground);
|
|
3194
|
+
border: none;
|
|
3195
|
+
border-radius: 4px;
|
|
3196
|
+
cursor: pointer;
|
|
3197
|
+
font-size: 0.85em;
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
.copy-btn:hover {
|
|
3201
|
+
background: var(--vscode-button-hoverBackground);
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
.compare-btn {
|
|
3205
|
+
position: absolute;
|
|
3206
|
+
top: 8px;
|
|
3207
|
+
right: 80px;
|
|
3208
|
+
padding: 6px 12px;
|
|
3209
|
+
background: var(--vscode-button-secondaryBackground);
|
|
3210
|
+
color: var(--vscode-button-secondaryForeground);
|
|
3211
|
+
border: none;
|
|
3212
|
+
border-radius: 4px;
|
|
3213
|
+
cursor: pointer;
|
|
3214
|
+
font-size: 0.85em;
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
.compare-btn:hover {
|
|
3218
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
.compare-btn.comparing {
|
|
3222
|
+
background: var(--vscode-inputValidation-infoBackground);
|
|
3223
|
+
border: 1px solid var(--vscode-inputValidation-infoBorder);
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
.norn-toast {
|
|
3227
|
+
position: fixed;
|
|
3228
|
+
bottom: 20px;
|
|
3229
|
+
left: 50%;
|
|
3230
|
+
transform: translateX(-50%);
|
|
3231
|
+
background: var(--vscode-notifications-background);
|
|
3232
|
+
color: var(--vscode-notifications-foreground);
|
|
3233
|
+
border: 1px solid var(--vscode-notifications-border);
|
|
3234
|
+
padding: 10px 20px;
|
|
3235
|
+
border-radius: 4px;
|
|
3236
|
+
z-index: 1000;
|
|
3237
|
+
opacity: 0;
|
|
3238
|
+
transition: opacity 0.3s;
|
|
3239
|
+
pointer-events: none;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
.norn-toast.visible {
|
|
3243
|
+
opacity: 1;
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
pre {
|
|
3247
|
+
background: transparent;
|
|
3248
|
+
padding: 16px;
|
|
3249
|
+
border-radius: 6px;
|
|
3250
|
+
overflow-x: auto;
|
|
3251
|
+
margin: 0;
|
|
3252
|
+
font-family: var(--vscode-editor-font-family);
|
|
3253
|
+
font-size: var(--vscode-editor-font-size);
|
|
3254
|
+
line-height: 1.5;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
pre code {
|
|
3258
|
+
background: none;
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
.headers-table {
|
|
3262
|
+
width: 100%;
|
|
3263
|
+
border-collapse: collapse;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
.headers-table th,
|
|
3267
|
+
.headers-table td {
|
|
3268
|
+
padding: 8px 12px;
|
|
3269
|
+
text-align: left;
|
|
3270
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
.headers-table th {
|
|
3274
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
3275
|
+
font-weight: 600;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
.header-name {
|
|
3279
|
+
color: var(--vscode-symbolIcon-propertyForeground, #9cdcfe);
|
|
3280
|
+
font-family: var(--vscode-editor-font-family);
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
.header-value {
|
|
3284
|
+
color: var(--vscode-foreground);
|
|
3285
|
+
font-family: var(--vscode-editor-font-family);
|
|
3286
|
+
word-break: break-all;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
/* Cookies */
|
|
3290
|
+
.cookies-table td {
|
|
3291
|
+
font-size: 0.9em;
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
.cookie-name {
|
|
3295
|
+
color: var(--vscode-symbolIcon-propertyForeground, #9cdcfe);
|
|
3296
|
+
font-family: var(--vscode-editor-font-family);
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
.cookie-value {
|
|
3300
|
+
color: var(--vscode-foreground);
|
|
3301
|
+
font-family: var(--vscode-editor-font-family);
|
|
3302
|
+
max-width: 200px;
|
|
3303
|
+
overflow: hidden;
|
|
3304
|
+
text-overflow: ellipsis;
|
|
3305
|
+
cursor: help;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
.cookie-flags {
|
|
3309
|
+
text-align: center;
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
.cookies-container {
|
|
3313
|
+
padding: 8px 0;
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
.cookie-summary {
|
|
3317
|
+
color: var(--vscode-foreground);
|
|
3318
|
+
font-weight: 500;
|
|
3319
|
+
margin-bottom: 16px;
|
|
3320
|
+
padding: 8px 12px;
|
|
3321
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
3322
|
+
border-radius: 4px;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
.cookie-domain-section {
|
|
3326
|
+
margin-bottom: 16px;
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
.cookie-domain-header {
|
|
3330
|
+
color: var(--vscode-foreground);
|
|
3331
|
+
font-size: 1em;
|
|
3332
|
+
margin: 0 0 8px 0;
|
|
3333
|
+
padding: 4px 0;
|
|
3334
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
.cookie-count {
|
|
3338
|
+
color: var(--vscode-descriptionForeground);
|
|
3339
|
+
font-weight: normal;
|
|
3340
|
+
font-size: 0.9em;
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
.no-cookies {
|
|
3344
|
+
color: var(--vscode-descriptionForeground);
|
|
3345
|
+
font-style: italic;
|
|
3346
|
+
padding: 16px;
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
.cookie-info {
|
|
3350
|
+
color: var(--vscode-descriptionForeground);
|
|
3351
|
+
font-size: 0.9em;
|
|
3352
|
+
margin-top: 16px;
|
|
3353
|
+
padding: 8px;
|
|
3354
|
+
background: var(--vscode-editor-inactiveSelectionBackground);
|
|
3355
|
+
border-radius: 4px;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
.cookie-info code {
|
|
3359
|
+
background: var(--vscode-textCodeBlock-background);
|
|
3360
|
+
padding: 2px 6px;
|
|
3361
|
+
border-radius: 3px;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
/* JSON Syntax Highlighting */
|
|
3365
|
+
.json-key { color: #9cdcfe; background: none; }
|
|
3366
|
+
.json-string { color: #ce9178; background: none; }
|
|
3367
|
+
.json-number { color: #b5cea8; background: none; }
|
|
3368
|
+
.json-boolean { color: #569cd6; background: none; }
|
|
3369
|
+
.json-null { color: #569cd6; background: none; }
|
|
3370
|
+
</style>
|
|
3371
|
+
</head>
|
|
3372
|
+
<body>
|
|
3373
|
+
<div class="container">
|
|
3374
|
+
<div class="status-bar">
|
|
3375
|
+
<span class="status-code ${statusClass}">${response.status} ${response.statusText}</span>
|
|
3376
|
+
<span class="meta">⏱ ${response.duration}ms</span>
|
|
3377
|
+
<span class="meta">📦 ${bodySize}</span>
|
|
3378
|
+
</div>
|
|
3379
|
+
|
|
3380
|
+
<div class="request-info">
|
|
3381
|
+
<span class="method">${method}</span> ${this._escapeHtml(url)}
|
|
3382
|
+
</div>
|
|
3383
|
+
|
|
3384
|
+
<div class="tabs">
|
|
3385
|
+
<button class="tab active" data-tab="body">Body</button>
|
|
3386
|
+
<button class="tab" data-tab="headers">Headers</button>
|
|
3387
|
+
<button class="tab" data-tab="cookies">Cookies</button>
|
|
3388
|
+
</div>
|
|
3389
|
+
|
|
3390
|
+
<div id="body" class="tab-content active">
|
|
3391
|
+
<div class="body-container">
|
|
3392
|
+
<button class="compare-btn" onclick="compareBody()">⇄ Compare</button>
|
|
3393
|
+
<button class="copy-btn" onclick="copyBody()">📋 Copy</button>
|
|
3394
|
+
<pre><code>${bodyContent}</code></pre>
|
|
3395
|
+
</div>
|
|
3396
|
+
</div>
|
|
3397
|
+
|
|
3398
|
+
<div id="headers" class="tab-content">
|
|
3399
|
+
${headersHtml}
|
|
3400
|
+
</div>
|
|
3401
|
+
|
|
3402
|
+
<div id="cookies" class="tab-content">
|
|
3403
|
+
${cookiesHtml}
|
|
3404
|
+
</div>
|
|
3405
|
+
|
|
3406
|
+
<div id="norn-toast" class="norn-toast"></div>
|
|
3407
|
+
</div>
|
|
3408
|
+
|
|
3409
|
+
<script>
|
|
3410
|
+
const vscode = acquireVsCodeApi();
|
|
3411
|
+
const tabs = document.querySelectorAll('.tab');
|
|
3412
|
+
const contents = document.querySelectorAll('.tab-content');
|
|
3413
|
+
|
|
3414
|
+
tabs.forEach(tab => {
|
|
3415
|
+
tab.addEventListener('click', () => {
|
|
3416
|
+
tabs.forEach(t => t.classList.remove('active'));
|
|
3417
|
+
contents.forEach(c => c.classList.remove('active'));
|
|
3418
|
+
|
|
3419
|
+
tab.classList.add('active');
|
|
3420
|
+
document.getElementById(tab.dataset.tab).classList.add('active');
|
|
3421
|
+
});
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
const rawBody = decodeURIComponent("${encodeURIComponent(typeof response.body === 'object' ? JSON.stringify(response.body, null, 2) : String(response.body))}");
|
|
3425
|
+
|
|
3426
|
+
// Response comparison state
|
|
3427
|
+
let compareBaseline = null;
|
|
3428
|
+
|
|
3429
|
+
function showToast(message) {
|
|
3430
|
+
const toast = document.getElementById('norn-toast');
|
|
3431
|
+
toast.textContent = message;
|
|
3432
|
+
toast.classList.add('visible');
|
|
3433
|
+
setTimeout(() => toast.classList.remove('visible'), 3000);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
function copyBody() {
|
|
3437
|
+
navigator.clipboard.writeText(rawBody).then(() => {
|
|
3438
|
+
const btn = document.querySelector('.copy-btn');
|
|
3439
|
+
btn.textContent = '✓ Copied!';
|
|
3440
|
+
setTimeout(() => btn.textContent = '📋 Copy', 2000);
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
function compareBody() {
|
|
3445
|
+
const btn = document.querySelector('.compare-btn');
|
|
3446
|
+
|
|
3447
|
+
if (!compareBaseline) {
|
|
3448
|
+
// First click - set baseline
|
|
3449
|
+
compareBaseline = rawBody;
|
|
3450
|
+
btn.classList.add('comparing');
|
|
3451
|
+
showToast('Select another response to compare');
|
|
3452
|
+
} else {
|
|
3453
|
+
// Second click - send compare request
|
|
3454
|
+
vscode.postMessage({ type: 'setBaseline', body: compareBaseline });
|
|
3455
|
+
vscode.postMessage({ type: 'compare', body: rawBody });
|
|
3456
|
+
compareBaseline = null;
|
|
3457
|
+
btn.classList.remove('comparing');
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// Listen for reset compare message from extension
|
|
3462
|
+
window.addEventListener('message', event => {
|
|
3463
|
+
const message = event.data;
|
|
3464
|
+
if (message.type === 'resetCompare') {
|
|
3465
|
+
compareBaseline = null;
|
|
3466
|
+
const btn = document.querySelector('.compare-btn');
|
|
3467
|
+
if (btn) btn.classList.remove('comparing');
|
|
3468
|
+
}
|
|
3469
|
+
});
|
|
3470
|
+
</script>
|
|
3471
|
+
</body>
|
|
3472
|
+
</html>`;
|
|
3473
|
+
}
|
|
3474
|
+
_getStatusClass(status) {
|
|
3475
|
+
if (status >= 200 && status < 300) {
|
|
3476
|
+
return 'status-success';
|
|
3477
|
+
}
|
|
3478
|
+
if (status >= 300 && status < 400) {
|
|
3479
|
+
return 'status-redirect';
|
|
3480
|
+
}
|
|
3481
|
+
if (status >= 400 && status < 500) {
|
|
3482
|
+
return 'status-client-error';
|
|
3483
|
+
}
|
|
3484
|
+
return 'status-server-error';
|
|
3485
|
+
}
|
|
3486
|
+
_formatBody(body) {
|
|
3487
|
+
if (typeof body === 'object') {
|
|
3488
|
+
return this._syntaxHighlightJson(JSON.stringify(body, null, 2));
|
|
3489
|
+
}
|
|
3490
|
+
const stringBody = String(body);
|
|
3491
|
+
// Check if it looks like HTML
|
|
3492
|
+
if (stringBody.trim().startsWith('<') && stringBody.includes('>')) {
|
|
3493
|
+
return this._escapeHtml(stringBody);
|
|
3494
|
+
}
|
|
3495
|
+
return this._escapeHtml(stringBody);
|
|
3496
|
+
}
|
|
3497
|
+
_syntaxHighlightJson(json) {
|
|
3498
|
+
return json
|
|
3499
|
+
.replace(/&/g, '&')
|
|
3500
|
+
.replace(/</g, '<')
|
|
3501
|
+
.replace(/>/g, '>')
|
|
3502
|
+
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)/g, (match) => {
|
|
3503
|
+
let cls = 'json-string';
|
|
3504
|
+
if (match.endsWith(':')) {
|
|
3505
|
+
cls = 'json-key';
|
|
3506
|
+
}
|
|
3507
|
+
return `<span class="${cls}">${match}</span>`;
|
|
3508
|
+
})
|
|
3509
|
+
.replace(/\b(true|false)\b/g, '<span class="json-boolean">$1</span>')
|
|
3510
|
+
.replace(/\bnull\b/g, '<span class="json-null">null</span>')
|
|
3511
|
+
.replace(/\b(-?\d+\.?\d*)\b/g, '<span class="json-number">$1</span>');
|
|
3512
|
+
}
|
|
3513
|
+
_formatHeaders(headers) {
|
|
3514
|
+
const rows = Object.entries(headers)
|
|
3515
|
+
.map(([key, value]) => `
|
|
3516
|
+
<tr>
|
|
3517
|
+
<td class="header-name">${this._escapeHtml(key)}</td>
|
|
3518
|
+
<td class="header-value">${this._escapeHtml(String(value))}</td>
|
|
3519
|
+
</tr>
|
|
3520
|
+
`).join('');
|
|
3521
|
+
return `
|
|
3522
|
+
<table class="headers-table">
|
|
3523
|
+
<thead>
|
|
3524
|
+
<tr>
|
|
3525
|
+
<th>Name</th>
|
|
3526
|
+
<th>Value</th>
|
|
3527
|
+
</tr>
|
|
3528
|
+
</thead>
|
|
3529
|
+
<tbody>
|
|
3530
|
+
${rows}
|
|
3531
|
+
</tbody>
|
|
3532
|
+
</table>
|
|
3533
|
+
`;
|
|
3534
|
+
}
|
|
3535
|
+
_getBodySize(body) {
|
|
3536
|
+
const str = typeof body === 'object' ? JSON.stringify(body) : String(body);
|
|
3537
|
+
const bytes = new TextEncoder().encode(str).length;
|
|
3538
|
+
if (bytes < 1024) {
|
|
3539
|
+
return `${bytes} B`;
|
|
3540
|
+
}
|
|
3541
|
+
if (bytes < 1024 * 1024) {
|
|
3542
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
3543
|
+
}
|
|
3544
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
3545
|
+
}
|
|
3546
|
+
_formatCookies(cookies) {
|
|
3547
|
+
if (!cookies || cookies.length === 0) {
|
|
3548
|
+
return '<p class="no-cookies">No cookies stored in session</p>';
|
|
3549
|
+
}
|
|
3550
|
+
// Group cookies by domain
|
|
3551
|
+
const byDomain = {};
|
|
3552
|
+
for (const cookie of cookies) {
|
|
3553
|
+
const domain = cookie.domain || 'unknown';
|
|
3554
|
+
if (!byDomain[domain]) {
|
|
3555
|
+
byDomain[domain] = [];
|
|
3556
|
+
}
|
|
3557
|
+
byDomain[domain].push(cookie);
|
|
3558
|
+
}
|
|
3559
|
+
const sections = Object.entries(byDomain).map(([domain, domainCookies]) => {
|
|
3560
|
+
const rows = domainCookies.map(cookie => `
|
|
3561
|
+
<tr>
|
|
3562
|
+
<td class="cookie-name">${this._escapeHtml(cookie.name)}</td>
|
|
3563
|
+
<td class="cookie-value" title="${this._escapeHtml(cookie.value)}">${this._escapeHtml(cookie.value.length > 40 ? cookie.value.substring(0, 40) + '...' : cookie.value)}</td>
|
|
3564
|
+
<td>${this._escapeHtml(cookie.path)}</td>
|
|
3565
|
+
<td>${cookie.expires ? new Date(cookie.expires).toLocaleDateString() : 'Session'}</td>
|
|
3566
|
+
<td class="cookie-flags">${cookie.httpOnly ? '🔒' : ''}${cookie.secure ? '🔐' : ''}</td>
|
|
3567
|
+
</tr>
|
|
3568
|
+
`).join('');
|
|
3569
|
+
return `
|
|
3570
|
+
<div class="cookie-domain-section">
|
|
3571
|
+
<h4 class="cookie-domain-header">🌐 ${this._escapeHtml(domain)} <span class="cookie-count">(${domainCookies.length})</span></h4>
|
|
3572
|
+
<table class="headers-table cookies-table">
|
|
3573
|
+
<thead>
|
|
3574
|
+
<tr>
|
|
3575
|
+
<th>Name</th>
|
|
3576
|
+
<th>Value</th>
|
|
3577
|
+
<th>Path</th>
|
|
3578
|
+
<th>Expires</th>
|
|
3579
|
+
<th>Flags</th>
|
|
3580
|
+
</tr>
|
|
3581
|
+
</thead>
|
|
3582
|
+
<tbody>
|
|
3583
|
+
${rows}
|
|
3584
|
+
</tbody>
|
|
3585
|
+
</table>
|
|
3586
|
+
</div>
|
|
3587
|
+
`;
|
|
3588
|
+
}).join('');
|
|
3589
|
+
return `
|
|
3590
|
+
<div class="cookies-container">
|
|
3591
|
+
<p class="cookie-summary">🍪 ${cookies.length} cookie${cookies.length !== 1 ? 's' : ''} stored across ${Object.keys(byDomain).length} domain${Object.keys(byDomain).length !== 1 ? 's' : ''}</p>
|
|
3592
|
+
${sections}
|
|
3593
|
+
<p class="cookie-info">💡 Cookies persist across requests. Use <code>Norn: Clear Cookies</code> command to reset.</p>
|
|
3594
|
+
</div>
|
|
3595
|
+
`;
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* Generate the HTML for the Contract View panel
|
|
3599
|
+
* Shows schema validation errors in a rich, navigable UI
|
|
3600
|
+
*/
|
|
3601
|
+
_getContractViewHtml(data) {
|
|
3602
|
+
const errorCount = data.errors.length;
|
|
3603
|
+
const schemaName = path.basename(data.schemaPath);
|
|
3604
|
+
// Group errors by severity
|
|
3605
|
+
const errorErrors = data.errors.filter(e => e.severity === 'error');
|
|
3606
|
+
const warningErrors = data.errors.filter(e => e.severity === 'warning');
|
|
3607
|
+
const infoErrors = data.errors.filter(e => e.severity === 'info');
|
|
3608
|
+
// Build errorsByPath map for annotated JSON
|
|
3609
|
+
const errorsByPath = new Map();
|
|
3610
|
+
for (const err of data.errors) {
|
|
3611
|
+
const path = err.instancePath || '/';
|
|
3612
|
+
if (!errorsByPath.has(path)) {
|
|
3613
|
+
errorsByPath.set(path, []);
|
|
3614
|
+
}
|
|
3615
|
+
errorsByPath.get(path).push(err);
|
|
3616
|
+
}
|
|
3617
|
+
// Generate annotated JSON view (same as Contract Report)
|
|
3618
|
+
const annotatedJson = this._generateAnnotatedJson(data.responseBody, errorsByPath, data.schema);
|
|
3619
|
+
// Format schema for display (schema doesn't need error annotations)
|
|
3620
|
+
const schemaStr = data.schema
|
|
3621
|
+
? JSON.stringify(data.schema, null, 2)
|
|
3622
|
+
: '(Schema not available)';
|
|
3623
|
+
// For raw copy functionality
|
|
3624
|
+
const responseBodyStr = typeof data.responseBody === 'string'
|
|
3625
|
+
? data.responseBody
|
|
3626
|
+
: JSON.stringify(data.responseBody, null, 2);
|
|
3627
|
+
// Build violations list HTML
|
|
3628
|
+
const violationsHtml = data.errors.map((err, idx) => {
|
|
3629
|
+
const severityClass = `severity-${err.severity}`;
|
|
3630
|
+
const severityBadge = err.severity === 'error' ? '❌' : err.severity === 'warning' ? '⚠️' : 'ℹ️';
|
|
3631
|
+
const actualDisplay = err.actual !== undefined
|
|
3632
|
+
? this._escapeHtml(typeof err.actual === 'object' ? JSON.stringify(err.actual) : String(err.actual))
|
|
3633
|
+
: '(missing)';
|
|
3634
|
+
const expectedDisplay = err.expected ? this._escapeHtml(err.expected) : '';
|
|
3635
|
+
return `
|
|
3636
|
+
<div class="violation-item ${severityClass}" data-index="${idx}">
|
|
3637
|
+
<div class="violation-header">
|
|
3638
|
+
<span class="severity-badge">${severityBadge}</span>
|
|
3639
|
+
<span class="violation-path">${this._escapeHtml(err.instancePath)}</span>
|
|
3640
|
+
<span class="violation-keyword">${this._escapeHtml(err.keyword)}</span>
|
|
3641
|
+
</div>
|
|
3642
|
+
<div class="violation-message">${this._escapeHtml(err.message)}</div>
|
|
3643
|
+
<div class="violation-details">
|
|
3644
|
+
${expectedDisplay ? `<div class="violation-expected"><strong>Expected:</strong> ${expectedDisplay}</div>` : ''}
|
|
3645
|
+
<div class="violation-actual"><strong>Actual:</strong> <code>${actualDisplay}</code></div>
|
|
3646
|
+
</div>
|
|
3647
|
+
<div class="violation-actions">
|
|
3648
|
+
<button class="action-btn" onclick="jumpToResponse('${this._escapeHtml(err.instancePath)}')">Jump to Response</button>
|
|
3649
|
+
<button class="action-btn" onclick="jumpToSchema('${this._escapeHtml(err.schemaPath)}')">Jump to Schema</button>
|
|
3650
|
+
</div>
|
|
3651
|
+
</div>
|
|
3652
|
+
`;
|
|
3653
|
+
}).join('');
|
|
3654
|
+
return /*html*/ `
|
|
3655
|
+
<!DOCTYPE html>
|
|
3656
|
+
<html lang="en">
|
|
3657
|
+
<head>
|
|
3658
|
+
<meta charset="UTF-8">
|
|
3659
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3660
|
+
<title>Contract View</title>
|
|
3661
|
+
<style>
|
|
3662
|
+
:root {
|
|
3663
|
+
--vscode-font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
body {
|
|
3667
|
+
font-family: var(--vscode-font-family);
|
|
3668
|
+
padding: 16px;
|
|
3669
|
+
color: var(--vscode-foreground);
|
|
3670
|
+
background-color: var(--vscode-editor-background);
|
|
3671
|
+
line-height: 1.5;
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
.contract-header {
|
|
3675
|
+
display: flex;
|
|
3676
|
+
align-items: center;
|
|
3677
|
+
gap: 12px;
|
|
3678
|
+
margin-bottom: 16px;
|
|
3679
|
+
padding-bottom: 12px;
|
|
3680
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
.contract-icon {
|
|
3684
|
+
font-size: 24px;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
.contract-title {
|
|
3688
|
+
flex: 1;
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
.contract-title h1 {
|
|
3692
|
+
margin: 0;
|
|
3693
|
+
font-size: 18px;
|
|
3694
|
+
font-weight: 600;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
.contract-title .schema-path {
|
|
3698
|
+
font-size: 12px;
|
|
3699
|
+
color: var(--vscode-descriptionForeground);
|
|
3700
|
+
margin-top: 4px;
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
.summary {
|
|
3704
|
+
display: flex;
|
|
3705
|
+
gap: 16px;
|
|
3706
|
+
margin-bottom: 16px;
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
.summary-item {
|
|
3710
|
+
padding: 8px 12px;
|
|
3711
|
+
border-radius: 4px;
|
|
3712
|
+
font-size: 13px;
|
|
3713
|
+
font-weight: 500;
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
.summary-errors {
|
|
3717
|
+
background-color: rgba(244, 67, 54, 0.15);
|
|
3718
|
+
color: #f44336;
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
.summary-warnings {
|
|
3722
|
+
background-color: rgba(255, 193, 7, 0.15);
|
|
3723
|
+
color: #ffc107;
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
.summary-info {
|
|
3727
|
+
background-color: rgba(33, 150, 243, 0.15);
|
|
3728
|
+
color: #2196f3;
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
.tabs {
|
|
3732
|
+
display: flex;
|
|
3733
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
3734
|
+
margin-bottom: 16px;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
.tab {
|
|
3738
|
+
padding: 8px 16px;
|
|
3739
|
+
cursor: pointer;
|
|
3740
|
+
border-bottom: 2px solid transparent;
|
|
3741
|
+
color: var(--vscode-foreground);
|
|
3742
|
+
opacity: 0.7;
|
|
3743
|
+
transition: opacity 0.2s, border-color 0.2s;
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
.tab:hover {
|
|
3747
|
+
opacity: 1;
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
.tab.active {
|
|
3751
|
+
opacity: 1;
|
|
3752
|
+
border-bottom-color: var(--vscode-focusBorder);
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
.tab-content {
|
|
3756
|
+
display: none;
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
.tab-content.active {
|
|
3760
|
+
display: block;
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
.violation-item {
|
|
3764
|
+
margin-bottom: 12px;
|
|
3765
|
+
padding: 12px;
|
|
3766
|
+
border-radius: 6px;
|
|
3767
|
+
border-left: 4px solid;
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
.violation-item.severity-error {
|
|
3771
|
+
background-color: rgba(244, 67, 54, 0.08);
|
|
3772
|
+
border-left-color: #f44336;
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
.violation-item.severity-warning {
|
|
3776
|
+
background-color: rgba(255, 193, 7, 0.08);
|
|
3777
|
+
border-left-color: #ffc107;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
.violation-item.severity-info {
|
|
3781
|
+
background-color: rgba(33, 150, 243, 0.08);
|
|
3782
|
+
border-left-color: #2196f3;
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
.violation-header {
|
|
3786
|
+
display: flex;
|
|
3787
|
+
align-items: center;
|
|
3788
|
+
gap: 8px;
|
|
3789
|
+
margin-bottom: 8px;
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
.severity-badge {
|
|
3793
|
+
font-size: 14px;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
.violation-path {
|
|
3797
|
+
font-family: var(--vscode-editor-font-family);
|
|
3798
|
+
font-weight: 600;
|
|
3799
|
+
color: var(--vscode-textLink-foreground);
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
.violation-keyword {
|
|
3803
|
+
font-size: 11px;
|
|
3804
|
+
padding: 2px 6px;
|
|
3805
|
+
border-radius: 3px;
|
|
3806
|
+
background-color: var(--vscode-badge-background);
|
|
3807
|
+
color: var(--vscode-badge-foreground);
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
.violation-message {
|
|
3811
|
+
margin-bottom: 8px;
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
.violation-details {
|
|
3815
|
+
font-size: 12px;
|
|
3816
|
+
color: var(--vscode-descriptionForeground);
|
|
3817
|
+
margin-bottom: 8px;
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
.violation-details code {
|
|
3821
|
+
background-color: var(--vscode-textCodeBlock-background);
|
|
3822
|
+
padding: 2px 4px;
|
|
3823
|
+
border-radius: 3px;
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
.violation-actions {
|
|
3827
|
+
display: flex;
|
|
3828
|
+
gap: 8px;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
.action-btn {
|
|
3832
|
+
padding: 4px 8px;
|
|
3833
|
+
font-size: 11px;
|
|
3834
|
+
border: 1px solid var(--vscode-button-secondaryBorder, var(--vscode-button-border));
|
|
3835
|
+
background-color: var(--vscode-button-secondaryBackground);
|
|
3836
|
+
color: var(--vscode-button-secondaryForeground);
|
|
3837
|
+
border-radius: 4px;
|
|
3838
|
+
cursor: pointer;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
.action-btn:hover {
|
|
3842
|
+
background-color: var(--vscode-button-secondaryHoverBackground);
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
.code-container {
|
|
3846
|
+
border: 1px solid var(--vscode-panel-border);
|
|
3847
|
+
border-radius: 4px;
|
|
3848
|
+
background: var(--vscode-editor-background);
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
.code-header {
|
|
3852
|
+
display: flex;
|
|
3853
|
+
justify-content: space-between;
|
|
3854
|
+
align-items: center;
|
|
3855
|
+
padding: 8px 12px;
|
|
3856
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
3857
|
+
background: var(--vscode-sideBar-background);
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
.code-label {
|
|
3861
|
+
font-size: 12px;
|
|
3862
|
+
font-weight: 500;
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
.copy-btn {
|
|
3866
|
+
padding: 4px 8px;
|
|
3867
|
+
font-size: 11px;
|
|
3868
|
+
border: none;
|
|
3869
|
+
background-color: transparent;
|
|
3870
|
+
color: var(--vscode-foreground);
|
|
3871
|
+
cursor: pointer;
|
|
3872
|
+
opacity: 0.7;
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
.copy-btn:hover {
|
|
3876
|
+
opacity: 1;
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
pre {
|
|
3880
|
+
margin: 0;
|
|
3881
|
+
padding: 12px;
|
|
3882
|
+
overflow: auto;
|
|
3883
|
+
max-height: 400px;
|
|
3884
|
+
font-size: 12px;
|
|
3885
|
+
line-height: 1.4;
|
|
3886
|
+
background: transparent !important;
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
pre code {
|
|
3890
|
+
font-family: var(--vscode-editor-font-family);
|
|
3891
|
+
background: transparent !important;
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
.json-formatted {
|
|
3895
|
+
background: transparent !important;
|
|
3896
|
+
} /* JSON syntax highlighting - explicit colors with !important for reliability */
|
|
3897
|
+
.json-key { color: #9cdcfe !important; }
|
|
3898
|
+
.json-string { color: #ce9178 !important; }
|
|
3899
|
+
.json-number { color: #b5cea8 !important; }
|
|
3900
|
+
.json-boolean { color: #569cd6 !important; }
|
|
3901
|
+
.json-null { color: #569cd6 !important; }
|
|
3902
|
+
.json-punctuation { color: var(--vscode-foreground); }
|
|
3903
|
+
|
|
3904
|
+
/* Annotated JSON styles (same as Contract Report) */
|
|
3905
|
+
.annotated-json {
|
|
3906
|
+
font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
|
|
3907
|
+
font-size: 13px;
|
|
3908
|
+
line-height: 1.5;
|
|
3909
|
+
background: var(--vscode-editor-background);
|
|
3910
|
+
border: 1px solid var(--vscode-panel-border);
|
|
3911
|
+
border-radius: 4px;
|
|
3912
|
+
padding: 12px;
|
|
3913
|
+
overflow-x: auto;
|
|
3914
|
+
white-space: pre;
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
.json-line {
|
|
3918
|
+
display: block;
|
|
3919
|
+
white-space: pre;
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
/* Property status highlights */
|
|
3923
|
+
.prop-status {
|
|
3924
|
+
border-radius: 3px;
|
|
3925
|
+
padding: 1px 3px;
|
|
3926
|
+
margin: -1px -3px;
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
.prop-pass {
|
|
3930
|
+
/* No highlight for passing properties */
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
.prop-fail {
|
|
3934
|
+
background: rgba(244, 67, 54, 0.25);
|
|
3935
|
+
box-shadow: 0 0 0 1px rgba(244, 67, 54, 0.6);
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
.prop-warn {
|
|
3939
|
+
background: rgba(255, 193, 7, 0.25);
|
|
3940
|
+
box-shadow: 0 0 0 1px rgba(255, 193, 7, 0.6);
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
/* JSON formatted in pre code blocks (for schema) */
|
|
3944
|
+
.json-formatted {
|
|
3945
|
+
font-family: var(--vscode-editor-font-family);
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
.highlight-path {
|
|
3949
|
+
background-color: rgba(255, 235, 59, 0.3);
|
|
3950
|
+
border-radius: 2px;
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
.empty-state {
|
|
3954
|
+
text-align: center;
|
|
3955
|
+
padding: 40px;
|
|
3956
|
+
color: var(--vscode-descriptionForeground);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
.empty-state-icon {
|
|
3960
|
+
font-size: 48px;
|
|
3961
|
+
margin-bottom: 16px;
|
|
3962
|
+
}
|
|
3963
|
+
</style>
|
|
3964
|
+
</head>
|
|
3965
|
+
<body>
|
|
3966
|
+
<div class="contract-header">
|
|
3967
|
+
<span class="contract-icon">📋</span>
|
|
3968
|
+
<div class="contract-title">
|
|
3969
|
+
<h1>Contract Validation</h1>
|
|
3970
|
+
<div class="schema-path">${this._escapeHtml(data.schemaPath)}</div>
|
|
3971
|
+
</div>
|
|
3972
|
+
</div>
|
|
3973
|
+
|
|
3974
|
+
<div class="summary">
|
|
3975
|
+
${errorErrors.length > 0 ? `<div class="summary-item summary-errors">${errorErrors.length} error${errorErrors.length !== 1 ? 's' : ''}</div>` : ''}
|
|
3976
|
+
${warningErrors.length > 0 ? `<div class="summary-item summary-warnings">${warningErrors.length} warning${warningErrors.length !== 1 ? 's' : ''}</div>` : ''}
|
|
3977
|
+
${infoErrors.length > 0 ? `<div class="summary-item summary-info">${infoErrors.length} info</div>` : ''}
|
|
3978
|
+
</div>
|
|
3979
|
+
|
|
3980
|
+
<div class="tabs">
|
|
3981
|
+
<div class="tab active" onclick="switchTab('violations')">Violations (${errorCount})</div>
|
|
3982
|
+
<div class="tab" onclick="switchTab('response')">Response</div>
|
|
3983
|
+
<div class="tab" onclick="switchTab('schema')">Schema</div>
|
|
3984
|
+
</div>
|
|
3985
|
+
|
|
3986
|
+
<div id="tab-violations" class="tab-content active">
|
|
3987
|
+
${errorCount > 0 ? violationsHtml : `
|
|
3988
|
+
<div class="empty-state">
|
|
3989
|
+
<div class="empty-state-icon">✅</div>
|
|
3990
|
+
<p>No violations found</p>
|
|
3991
|
+
</div>
|
|
3992
|
+
`}
|
|
3993
|
+
</div>
|
|
3994
|
+
|
|
3995
|
+
<div id="tab-response" class="tab-content">
|
|
3996
|
+
<div class="code-container">
|
|
3997
|
+
<div class="code-header">
|
|
3998
|
+
<span class="code-label">Response Body</span>
|
|
3999
|
+
<button class="copy-btn" onclick="copyCode('response')">📋 Copy</button>
|
|
4000
|
+
</div>
|
|
4001
|
+
<div class="annotated-json" id="response-code">${annotatedJson}</div>
|
|
4002
|
+
</div>
|
|
4003
|
+
<textarea id="response-raw" style="display:none;">${this._escapeHtml(responseBodyStr)}</textarea>
|
|
4004
|
+
</div>
|
|
4005
|
+
|
|
4006
|
+
<div id="tab-schema" class="tab-content">
|
|
4007
|
+
<div class="code-container">
|
|
4008
|
+
<div class="code-header">
|
|
4009
|
+
<span class="code-label">JSON Schema</span>
|
|
4010
|
+
<button class="copy-btn" onclick="copyCode('schema')">📋 Copy</button>
|
|
4011
|
+
</div>
|
|
4012
|
+
<pre><code id="schema-code" class="json-formatted">${this._formatJsonHtml(schemaStr)}</code></pre>
|
|
4013
|
+
</div>
|
|
4014
|
+
<textarea id="schema-raw" style="display:none;">${this._escapeHtml(schemaStr)}</textarea>
|
|
4015
|
+
</div>
|
|
4016
|
+
|
|
4017
|
+
<script>
|
|
4018
|
+
function switchTab(tabName) {
|
|
4019
|
+
// Update tab buttons
|
|
4020
|
+
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
|
4021
|
+
event.target.classList.add('active');
|
|
4022
|
+
|
|
4023
|
+
// Update tab content
|
|
4024
|
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
4025
|
+
document.getElementById('tab-' + tabName).classList.add('active');
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
function copyCode(type) {
|
|
4029
|
+
const textarea = document.getElementById(type + '-raw');
|
|
4030
|
+
const text = textarea.value || textarea.textContent || '';
|
|
4031
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
4032
|
+
const btn = event.target;
|
|
4033
|
+
const originalText = btn.textContent;
|
|
4034
|
+
btn.textContent = '✓ Copied!';
|
|
4035
|
+
setTimeout(() => btn.textContent = originalText, 1500);
|
|
4036
|
+
});
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
function jumpToResponse(jsonPath) {
|
|
4040
|
+
switchTab('response');
|
|
4041
|
+
// Highlight the path in the response (basic implementation)
|
|
4042
|
+
const codeEl = document.getElementById('response-code');
|
|
4043
|
+
// For now, just switch to the tab - advanced highlighting could be added later
|
|
4044
|
+
codeEl.scrollIntoView({ behavior: 'smooth' });
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
function jumpToSchema(schemaPath) {
|
|
4048
|
+
switchTab('schema');
|
|
4049
|
+
const codeEl = document.getElementById('schema-code');
|
|
4050
|
+
codeEl.scrollIntoView({ behavior: 'smooth' });
|
|
4051
|
+
}
|
|
4052
|
+
</script>
|
|
4053
|
+
</body>
|
|
4054
|
+
</html>`;
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* Generate HTML for the Contract Report view
|
|
4058
|
+
* Shows response body with inline status indicators for each property
|
|
4059
|
+
*/
|
|
4060
|
+
_getContractReportHtml(data) {
|
|
4061
|
+
const errorCount = data.errors.length;
|
|
4062
|
+
const schemaName = path.basename(data.schemaPath);
|
|
4063
|
+
// Build a map of paths to errors for quick lookup
|
|
4064
|
+
const errorsByPath = new Map();
|
|
4065
|
+
for (const err of data.errors) {
|
|
4066
|
+
const path = err.instancePath || '/';
|
|
4067
|
+
if (!errorsByPath.has(path)) {
|
|
4068
|
+
errorsByPath.set(path, []);
|
|
4069
|
+
}
|
|
4070
|
+
errorsByPath.get(path).push(err);
|
|
4071
|
+
}
|
|
4072
|
+
// Get list of all paths that have been validated (from schema properties)
|
|
4073
|
+
const schemaPaths = this._extractSchemaPaths(data.schema);
|
|
4074
|
+
const matchedCount = schemaPaths.length - errorCount;
|
|
4075
|
+
// Format response body for display
|
|
4076
|
+
const responseBodyStr = typeof data.responseBody === 'string'
|
|
4077
|
+
? data.responseBody
|
|
4078
|
+
: JSON.stringify(data.responseBody, null, 2);
|
|
4079
|
+
// Format schema for display
|
|
4080
|
+
const schemaStr = data.schema
|
|
4081
|
+
? JSON.stringify(data.schema, null, 2)
|
|
4082
|
+
: '(Schema not available)';
|
|
4083
|
+
// Generate annotated JSON view
|
|
4084
|
+
const annotatedJson = this._generateAnnotatedJson(data.responseBody, errorsByPath, data.schema);
|
|
4085
|
+
// Group errors by severity for summary
|
|
4086
|
+
const breakingErrors = data.errors.filter(e => e.keyword === 'required' ||
|
|
4087
|
+
e.keyword === 'type' ||
|
|
4088
|
+
e.keyword === 'additionalProperties');
|
|
4089
|
+
const warningErrors = data.errors.filter(e => e.keyword !== 'required' &&
|
|
4090
|
+
e.keyword !== 'type' &&
|
|
4091
|
+
e.keyword !== 'additionalProperties');
|
|
4092
|
+
return /*html*/ `
|
|
4093
|
+
<!DOCTYPE html>
|
|
4094
|
+
<html lang="en">
|
|
4095
|
+
<head>
|
|
4096
|
+
<meta charset="UTF-8">
|
|
4097
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4098
|
+
<title>Contract Report</title>
|
|
4099
|
+
<style>
|
|
4100
|
+
:root {
|
|
4101
|
+
--vscode-font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
body {
|
|
4105
|
+
font-family: var(--vscode-font-family);
|
|
4106
|
+
padding: 16px;
|
|
4107
|
+
color: var(--vscode-foreground);
|
|
4108
|
+
background-color: var(--vscode-editor-background);
|
|
4109
|
+
line-height: 1.5;
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
.report-header {
|
|
4113
|
+
display: flex;
|
|
4114
|
+
align-items: center;
|
|
4115
|
+
gap: 16px;
|
|
4116
|
+
margin-bottom: 20px;
|
|
4117
|
+
padding-bottom: 16px;
|
|
4118
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
.status-icon {
|
|
4122
|
+
font-size: 48px;
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
.status-icon.pass { color: #4caf50; }
|
|
4126
|
+
.status-icon.fail { color: #f44336; }
|
|
4127
|
+
|
|
4128
|
+
.report-info h1 {
|
|
4129
|
+
margin: 0;
|
|
4130
|
+
font-size: 20px;
|
|
4131
|
+
font-weight: 600;
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
.report-info .schema-path {
|
|
4135
|
+
font-size: 12px;
|
|
4136
|
+
color: var(--vscode-descriptionForeground);
|
|
4137
|
+
margin-top: 4px;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
.summary-cards {
|
|
4141
|
+
display: flex;
|
|
4142
|
+
gap: 12px;
|
|
4143
|
+
margin-bottom: 20px;
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
.summary-card {
|
|
4147
|
+
padding: 12px 16px;
|
|
4148
|
+
border-radius: 6px;
|
|
4149
|
+
flex: 1;
|
|
4150
|
+
text-align: center;
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
.summary-card.matched {
|
|
4154
|
+
background-color: rgba(76, 175, 80, 0.15);
|
|
4155
|
+
border: 1px solid rgba(76, 175, 80, 0.3);
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
.summary-card.breaking {
|
|
4159
|
+
background-color: rgba(244, 67, 54, 0.15);
|
|
4160
|
+
border: 1px solid rgba(244, 67, 54, 0.3);
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
.summary-card.warnings {
|
|
4164
|
+
background-color: rgba(255, 193, 7, 0.15);
|
|
4165
|
+
border: 1px solid rgba(255, 193, 7, 0.3);
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4168
|
+
.summary-card .number {
|
|
4169
|
+
font-size: 28px;
|
|
4170
|
+
font-weight: bold;
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
.summary-card.matched .number { color: #4caf50; }
|
|
4174
|
+
.summary-card.breaking .number { color: #f44336; }
|
|
4175
|
+
.summary-card.warnings .number { color: #ffc107; }
|
|
4176
|
+
|
|
4177
|
+
.summary-card .label {
|
|
4178
|
+
font-size: 12px;
|
|
4179
|
+
color: var(--vscode-descriptionForeground);
|
|
4180
|
+
margin-top: 4px;
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
.tabs {
|
|
4184
|
+
display: flex;
|
|
4185
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
4186
|
+
margin-bottom: 16px;
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
.tab {
|
|
4190
|
+
padding: 8px 16px;
|
|
4191
|
+
cursor: pointer;
|
|
4192
|
+
border-bottom: 2px solid transparent;
|
|
4193
|
+
color: var(--vscode-foreground);
|
|
4194
|
+
opacity: 0.7;
|
|
4195
|
+
transition: opacity 0.2s, border-color 0.2s;
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
.tab:hover { opacity: 1; }
|
|
4199
|
+
.tab.active {
|
|
4200
|
+
opacity: 1;
|
|
4201
|
+
border-bottom-color: var(--vscode-focusBorder);
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
.tab-content { display: none; }
|
|
4205
|
+
.tab-content.active { display: block; }
|
|
4206
|
+
|
|
4207
|
+
/* Annotated JSON styles */
|
|
4208
|
+
.annotated-json {
|
|
4209
|
+
font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
|
|
4210
|
+
font-size: 13px;
|
|
4211
|
+
line-height: 1.5;
|
|
4212
|
+
background: var(--vscode-editor-background);
|
|
4213
|
+
border: 1px solid var(--vscode-panel-border);
|
|
4214
|
+
border-radius: 4px;
|
|
4215
|
+
padding: 12px;
|
|
4216
|
+
overflow-x: auto;
|
|
4217
|
+
white-space: pre;
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4220
|
+
.json-line {
|
|
4221
|
+
display: block;
|
|
4222
|
+
white-space: pre;
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
/* Property status highlights - similar to assert hover */
|
|
4226
|
+
.prop-status {
|
|
4227
|
+
border-radius: 3px;
|
|
4228
|
+
padding: 1px 3px;
|
|
4229
|
+
margin: -1px -3px;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
.prop-pass {
|
|
4233
|
+
/* No highlight for passing properties - clean look */
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
.prop-fail {
|
|
4237
|
+
background: rgba(244, 67, 54, 0.25);
|
|
4238
|
+
box-shadow: 0 0 0 1px rgba(244, 67, 54, 0.6);
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
.prop-warn {
|
|
4242
|
+
background: rgba(255, 193, 7, 0.25);
|
|
4243
|
+
box-shadow: 0 0 0 1px rgba(255, 193, 7, 0.6);
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4246
|
+
.prop-error {
|
|
4247
|
+
background: rgba(244, 67, 54, 0.15);
|
|
4248
|
+
border-radius: 3px;
|
|
4249
|
+
padding: 0 4px;
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
.prop-warning {
|
|
4253
|
+
background: rgba(255, 193, 7, 0.15);
|
|
4254
|
+
border-radius: 3px;
|
|
4255
|
+
padding: 0 4px;
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
.error-tooltip {
|
|
4259
|
+
display: none;
|
|
4260
|
+
position: absolute;
|
|
4261
|
+
background: var(--vscode-editorWidget-background);
|
|
4262
|
+
border: 1px solid var(--vscode-editorWidget-border);
|
|
4263
|
+
padding: 8px 12px;
|
|
4264
|
+
border-radius: 4px;
|
|
4265
|
+
font-size: 12px;
|
|
4266
|
+
max-width: 400px;
|
|
4267
|
+
z-index: 100;
|
|
4268
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
.json-line:hover .error-tooltip {
|
|
4272
|
+
display: block;
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
/* Issues list styles */
|
|
4276
|
+
.issue-item {
|
|
4277
|
+
margin-bottom: 12px;
|
|
4278
|
+
padding: 12px;
|
|
4279
|
+
border-radius: 6px;
|
|
4280
|
+
border-left: 4px solid;
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
.issue-item.breaking {
|
|
4284
|
+
background-color: rgba(244, 67, 54, 0.08);
|
|
4285
|
+
border-left-color: #f44336;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
.issue-item.warning {
|
|
4289
|
+
background-color: rgba(255, 193, 7, 0.08);
|
|
4290
|
+
border-left-color: #ffc107;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
.issue-header {
|
|
4294
|
+
display: flex;
|
|
4295
|
+
align-items: center;
|
|
4296
|
+
gap: 8px;
|
|
4297
|
+
font-weight: 500;
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4300
|
+
.issue-path {
|
|
4301
|
+
font-family: var(--vscode-font-family);
|
|
4302
|
+
color: var(--vscode-textLink-foreground);
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
.issue-message {
|
|
4306
|
+
margin-top: 4px;
|
|
4307
|
+
color: var(--vscode-descriptionForeground);
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
.issue-details {
|
|
4311
|
+
margin-top: 8px;
|
|
4312
|
+
font-size: 12px;
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
.issue-expected, .issue-actual {
|
|
4316
|
+
margin-top: 4px;
|
|
4317
|
+
}
|
|
4318
|
+
|
|
4319
|
+
/* JSON syntax highlighting - explicit colors for reliability */
|
|
4320
|
+
.json-key { color: #9cdcfe !important; }
|
|
4321
|
+
.json-string { color: #ce9178 !important; }
|
|
4322
|
+
.json-number { color: #b5cea8 !important; }
|
|
4323
|
+
.json-boolean { color: #569cd6 !important; }
|
|
4324
|
+
.json-null { color: #569cd6 !important; }
|
|
4325
|
+
.json-punctuation { color: var(--vscode-foreground); }
|
|
4326
|
+
|
|
4327
|
+
.code-container {
|
|
4328
|
+
border: 1px solid var(--vscode-panel-border);
|
|
4329
|
+
border-radius: 4px;
|
|
4330
|
+
background: var(--vscode-editor-background);
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4333
|
+
.code-header {
|
|
4334
|
+
display: flex;
|
|
4335
|
+
justify-content: space-between;
|
|
4336
|
+
align-items: center;
|
|
4337
|
+
padding: 8px 12px;
|
|
4338
|
+
border-bottom: 1px solid var(--vscode-panel-border);
|
|
4339
|
+
background: var(--vscode-sideBar-background);
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
.code-label {
|
|
4343
|
+
font-weight: 500;
|
|
4344
|
+
font-size: 12px;
|
|
4345
|
+
color: var(--vscode-descriptionForeground);
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
.copy-btn {
|
|
4349
|
+
background: transparent;
|
|
4350
|
+
border: 1px solid var(--vscode-button-secondaryBackground);
|
|
4351
|
+
color: var(--vscode-foreground);
|
|
4352
|
+
padding: 4px 8px;
|
|
4353
|
+
border-radius: 3px;
|
|
4354
|
+
cursor: pointer;
|
|
4355
|
+
font-size: 11px;
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
.copy-btn:hover {
|
|
4359
|
+
background: var(--vscode-button-secondaryHoverBackground);
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
pre {
|
|
4363
|
+
margin: 0;
|
|
4364
|
+
padding: 12px;
|
|
4365
|
+
overflow-x: auto;
|
|
4366
|
+
background: transparent;
|
|
4367
|
+
white-space: pre;
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
code {
|
|
4371
|
+
font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
|
|
4372
|
+
font-size: 13px;
|
|
4373
|
+
background: transparent;
|
|
4374
|
+
white-space: pre;
|
|
4375
|
+
line-height: 1.5;
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
/* Formatted JSON in schema tab */
|
|
4379
|
+
code.json-formatted {
|
|
4380
|
+
display: block;
|
|
4381
|
+
white-space: pre;
|
|
4382
|
+
line-height: 1.5;
|
|
4383
|
+
background: transparent;
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
/* Ensure spans don't have background */
|
|
4387
|
+
code span {
|
|
4388
|
+
background: transparent;
|
|
4389
|
+
}
|
|
4390
|
+
</style>
|
|
4391
|
+
</head>
|
|
4392
|
+
<body>
|
|
4393
|
+
<!-- Header -->
|
|
4394
|
+
<div class="report-header">
|
|
4395
|
+
<div class="status-icon ${errorCount === 0 ? 'pass' : 'fail'}">
|
|
4396
|
+
${errorCount === 0 ? '✓' : '⚠'}
|
|
4397
|
+
</div>
|
|
4398
|
+
<div class="report-info">
|
|
4399
|
+
<h1>${errorCount === 0 ? 'Contract Validation Passed' : `${errorCount} Issue${errorCount !== 1 ? 's' : ''} Found`}</h1>
|
|
4400
|
+
<div class="schema-path">${this._escapeHtml(data.schemaPath)}</div>
|
|
4401
|
+
</div>
|
|
4402
|
+
</div>
|
|
4403
|
+
|
|
4404
|
+
<!-- Summary Cards -->
|
|
4405
|
+
<div class="summary-cards">
|
|
4406
|
+
<div class="summary-card matched">
|
|
4407
|
+
<div class="number">${matchedCount >= 0 ? matchedCount : '?'}</div>
|
|
4408
|
+
<div class="label">Matched</div>
|
|
4409
|
+
</div>
|
|
4410
|
+
<div class="summary-card breaking">
|
|
4411
|
+
<div class="number">${breakingErrors.length}</div>
|
|
4412
|
+
<div class="label">Breaking</div>
|
|
4413
|
+
</div>
|
|
4414
|
+
<div class="summary-card warnings">
|
|
4415
|
+
<div class="number">${warningErrors.length}</div>
|
|
4416
|
+
<div class="label">Warnings</div>
|
|
4417
|
+
</div>
|
|
4418
|
+
</div>
|
|
4419
|
+
|
|
4420
|
+
<!-- Tabs -->
|
|
4421
|
+
<div class="tabs">
|
|
4422
|
+
<div class="tab active" onclick="switchTab('annotated')">Annotated Response</div>
|
|
4423
|
+
<div class="tab" onclick="switchTab('issues')">Issues (${errorCount})</div>
|
|
4424
|
+
<div class="tab" onclick="switchTab('schema')">Schema</div>
|
|
4425
|
+
</div>
|
|
4426
|
+
|
|
4427
|
+
<!-- Annotated Response Tab -->
|
|
4428
|
+
<div id="tab-annotated" class="tab-content active">
|
|
4429
|
+
<div class="annotated-json">
|
|
4430
|
+
${annotatedJson}
|
|
4431
|
+
</div>
|
|
4432
|
+
</div>
|
|
4433
|
+
|
|
4434
|
+
<!-- Issues Tab -->
|
|
4435
|
+
<div id="tab-issues" class="tab-content">
|
|
4436
|
+
${errorCount === 0 ? '<p style="color: var(--vscode-descriptionForeground);">No issues found - all properties match the schema!</p>' : ''}
|
|
4437
|
+
|
|
4438
|
+
${breakingErrors.length > 0 ? `
|
|
4439
|
+
<h3 style="color: #f44336; margin-top: 0;">Breaking Changes</h3>
|
|
4440
|
+
${breakingErrors.map(err => `
|
|
4441
|
+
<div class="issue-item breaking">
|
|
4442
|
+
<div class="issue-header">
|
|
4443
|
+
<span>❌</span>
|
|
4444
|
+
<span class="issue-path">${this._escapeHtml(err.instancePath || '/')}</span>
|
|
4445
|
+
<span style="color: var(--vscode-descriptionForeground);">(${err.keyword})</span>
|
|
4446
|
+
</div>
|
|
4447
|
+
<div class="issue-message">${this._escapeHtml(err.message)}</div>
|
|
4448
|
+
<div class="issue-details">
|
|
4449
|
+
${err.expected ? `<div class="issue-expected"><strong>Expected:</strong> ${this._escapeHtml(err.expected)}</div>` : ''}
|
|
4450
|
+
${err.actual !== undefined ? `<div class="issue-actual"><strong>Actual:</strong> <code>${this._escapeHtml(typeof err.actual === 'object' ? JSON.stringify(err.actual) : String(err.actual))}</code></div>` : ''}
|
|
4451
|
+
</div>
|
|
4452
|
+
</div>
|
|
4453
|
+
`).join('')}
|
|
4454
|
+
` : ''}
|
|
4455
|
+
|
|
4456
|
+
${warningErrors.length > 0 ? `
|
|
4457
|
+
<h3 style="color: #ffc107; margin-top: ${breakingErrors.length > 0 ? '24px' : '0'};">Warnings</h3>
|
|
4458
|
+
${warningErrors.map(err => `
|
|
4459
|
+
<div class="issue-item warning">
|
|
4460
|
+
<div class="issue-header">
|
|
4461
|
+
<span>⚠️</span>
|
|
4462
|
+
<span class="issue-path">${this._escapeHtml(err.instancePath || '/')}</span>
|
|
4463
|
+
<span style="color: var(--vscode-descriptionForeground);">(${err.keyword})</span>
|
|
4464
|
+
</div>
|
|
4465
|
+
<div class="issue-message">${this._escapeHtml(err.message)}</div>
|
|
4466
|
+
<div class="issue-details">
|
|
4467
|
+
${err.expected ? `<div class="issue-expected"><strong>Expected:</strong> ${this._escapeHtml(err.expected)}</div>` : ''}
|
|
4468
|
+
${err.actual !== undefined ? `<div class="issue-actual"><strong>Actual:</strong> <code>${this._escapeHtml(typeof err.actual === 'object' ? JSON.stringify(err.actual) : String(err.actual))}</code></div>` : ''}
|
|
4469
|
+
</div>
|
|
4470
|
+
</div>
|
|
4471
|
+
`).join('')}
|
|
4472
|
+
` : ''}
|
|
4473
|
+
</div>
|
|
4474
|
+
|
|
4475
|
+
<!-- Schema Tab -->
|
|
4476
|
+
<div id="tab-schema" class="tab-content">
|
|
4477
|
+
<div class="code-container">
|
|
4478
|
+
<div class="code-header">
|
|
4479
|
+
<span class="code-label">JSON Schema</span>
|
|
4480
|
+
<button class="copy-btn" onclick="copySchema()">📋 Copy</button>
|
|
4481
|
+
</div>
|
|
4482
|
+
<pre><code class="json-formatted">${this._formatJsonHtml(schemaStr)}</code></pre>
|
|
4483
|
+
</div>
|
|
4484
|
+
<textarea id="schema-raw" style="display:none;">${this._escapeHtml(schemaStr)}</textarea>
|
|
4485
|
+
</div>
|
|
4486
|
+
|
|
4487
|
+
<script>
|
|
4488
|
+
function switchTab(tabName) {
|
|
4489
|
+
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
|
4490
|
+
event.target.classList.add('active');
|
|
4491
|
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
4492
|
+
document.getElementById('tab-' + tabName).classList.add('active');
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
function copySchema() {
|
|
4496
|
+
const text = document.getElementById('schema-raw').value;
|
|
4497
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
4498
|
+
const btn = event.target;
|
|
4499
|
+
const originalText = btn.textContent;
|
|
4500
|
+
btn.textContent = '✓ Copied!';
|
|
4501
|
+
setTimeout(() => btn.textContent = originalText, 1500);
|
|
4502
|
+
});
|
|
4503
|
+
}
|
|
4504
|
+
</script>
|
|
4505
|
+
</body>
|
|
4506
|
+
</html>`;
|
|
4507
|
+
}
|
|
4508
|
+
/**
|
|
4509
|
+
* Extract all property paths from a JSON Schema
|
|
4510
|
+
*/
|
|
4511
|
+
_extractSchemaPaths(schema, prefix = '') {
|
|
4512
|
+
if (!schema || typeof schema !== 'object')
|
|
4513
|
+
return [];
|
|
4514
|
+
const paths = [];
|
|
4515
|
+
if (schema.properties) {
|
|
4516
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
4517
|
+
const path = prefix ? `${prefix}/${key}` : `/${key}`;
|
|
4518
|
+
paths.push(path);
|
|
4519
|
+
paths.push(...this._extractSchemaPaths(prop, path));
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
if (schema.items) {
|
|
4523
|
+
paths.push(...this._extractSchemaPaths(schema.items, `${prefix}/0`));
|
|
4524
|
+
}
|
|
4525
|
+
return paths;
|
|
4526
|
+
}
|
|
4527
|
+
/**
|
|
4528
|
+
* Generate annotated JSON HTML with status indicators per line
|
|
4529
|
+
*/
|
|
4530
|
+
_generateAnnotatedJson(data, errorsByPath, schema) {
|
|
4531
|
+
if (!data || typeof data !== 'object') {
|
|
4532
|
+
const hasError = errorsByPath.has('/') || errorsByPath.has('');
|
|
4533
|
+
const lineClass = hasError ? 'line-fail' : 'line-pass';
|
|
4534
|
+
return `<div class="json-line ${lineClass}">${this._escapeHtml(JSON.stringify(data))}</div>`;
|
|
4535
|
+
}
|
|
4536
|
+
const lines = [];
|
|
4537
|
+
this._annotateJsonRecursive(data, errorsByPath, '', 0, lines);
|
|
4538
|
+
return lines.join('');
|
|
4539
|
+
}
|
|
4540
|
+
/**
|
|
4541
|
+
* Recursively annotate JSON object with status indicators
|
|
4542
|
+
*/
|
|
4543
|
+
_annotateJsonRecursive(data, errorsByPath, currentPath, indent, lines) {
|
|
4544
|
+
const indentStr = ' '.repeat(indent);
|
|
4545
|
+
if (Array.isArray(data)) {
|
|
4546
|
+
lines.push(this._createJsonLine(currentPath, errorsByPath, `${indentStr}<span class="json-punctuation">[</span>`, true, ''));
|
|
4547
|
+
for (let i = 0; i < data.length; i++) {
|
|
4548
|
+
const itemPath = `${currentPath}/${i}`;
|
|
4549
|
+
const item = data[i];
|
|
4550
|
+
const isLast = i === data.length - 1;
|
|
4551
|
+
if (typeof item === 'object' && item !== null) {
|
|
4552
|
+
this._annotateJsonRecursive(item, errorsByPath, itemPath, indent + 1, lines);
|
|
4553
|
+
// Add comma if not last
|
|
4554
|
+
if (!isLast) {
|
|
4555
|
+
const lastLine = lines[lines.length - 1];
|
|
4556
|
+
lines[lines.length - 1] = lastLine.replace('</span></div>', ',</span></div>');
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
else {
|
|
4560
|
+
const valueStr = this._formatJsonValue(item);
|
|
4561
|
+
const itemIndent = ' '.repeat(indent + 1);
|
|
4562
|
+
lines.push(this._createJsonLine(itemPath, errorsByPath, `${valueStr}${isLast ? '' : ','}`, false, itemIndent));
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
lines.push(this._createJsonLine(currentPath, errorsByPath, `${indentStr}<span class="json-punctuation">]</span>`, true, ''));
|
|
4566
|
+
}
|
|
4567
|
+
else if (typeof data === 'object' && data !== null) {
|
|
4568
|
+
lines.push(this._createJsonLine(currentPath, errorsByPath, `${indentStr}<span class="json-punctuation">{</span>`, true, ''));
|
|
4569
|
+
const keys = Object.keys(data);
|
|
4570
|
+
for (let i = 0; i < keys.length; i++) {
|
|
4571
|
+
const key = keys[i];
|
|
4572
|
+
const value = data[key];
|
|
4573
|
+
const propPath = `${currentPath}/${key}`;
|
|
4574
|
+
const isLast = i === keys.length - 1;
|
|
4575
|
+
const propIndent = ' '.repeat(indent + 1);
|
|
4576
|
+
if (typeof value === 'object' && value !== null) {
|
|
4577
|
+
// Nested object - key line is structural, no highlight
|
|
4578
|
+
lines.push(this._createJsonLine(propPath, errorsByPath, `${propIndent}<span class="json-key">"${this._escapeHtml(key)}"</span><span class="json-punctuation">:</span> `, true, ''));
|
|
4579
|
+
this._annotateJsonRecursive(value, errorsByPath, propPath, indent + 1, lines);
|
|
4580
|
+
if (!isLast) {
|
|
4581
|
+
const lastLine = lines[lines.length - 1];
|
|
4582
|
+
lines[lines.length - 1] = lastLine.replace('</span></div>', ',</span></div>');
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
else {
|
|
4586
|
+
// Primitive value - highlight the key:value pair
|
|
4587
|
+
const valueStr = this._formatJsonValue(value);
|
|
4588
|
+
lines.push(this._createJsonLine(propPath, errorsByPath, `<span class="json-key">"${this._escapeHtml(key)}"</span><span class="json-punctuation">:</span> ${valueStr}${isLast ? '' : ','}`, false, propIndent));
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
lines.push(this._createJsonLine(currentPath, errorsByPath, `${indentStr}<span class="json-punctuation">}</span>`, true, ''));
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
/**
|
|
4595
|
+
* Create a single annotated JSON line with property highlight
|
|
4596
|
+
*/
|
|
4597
|
+
_createJsonLine(path, errorsByPath, content, isBracket = false, indent = '') {
|
|
4598
|
+
// For brackets, just return the line without any highlight
|
|
4599
|
+
if (isBracket) {
|
|
4600
|
+
return `<div class="json-line">${content}</div>`;
|
|
4601
|
+
}
|
|
4602
|
+
const errors = errorsByPath.get(path);
|
|
4603
|
+
let propClass = 'prop-pass';
|
|
4604
|
+
let tooltip = '';
|
|
4605
|
+
if (errors && errors.length > 0) {
|
|
4606
|
+
const isBreaking = errors.some(e => e.keyword === 'required' ||
|
|
4607
|
+
e.keyword === 'type' ||
|
|
4608
|
+
e.keyword === 'additionalProperties');
|
|
4609
|
+
propClass = isBreaking ? 'prop-fail' : 'prop-warn';
|
|
4610
|
+
tooltip = errors.map((e) => `${e.keyword}: ${e.message}`).join('\n');
|
|
4611
|
+
}
|
|
4612
|
+
// Indent outside the highlighted span, property content inside
|
|
4613
|
+
return `<div class="json-line">${indent}<span class="prop-status ${propClass}" ${tooltip ? `title="${this._escapeHtml(tooltip)}"` : ''}>${content}</span></div>`;
|
|
4614
|
+
}
|
|
4615
|
+
/**
|
|
4616
|
+
* Format a primitive JSON value with syntax highlighting
|
|
4617
|
+
*/
|
|
4618
|
+
_formatJsonValue(value) {
|
|
4619
|
+
if (value === null) {
|
|
4620
|
+
return '<span class="json-null">null</span>';
|
|
4621
|
+
}
|
|
4622
|
+
if (typeof value === 'boolean') {
|
|
4623
|
+
return `<span class="json-boolean">${value}</span>`;
|
|
4624
|
+
}
|
|
4625
|
+
if (typeof value === 'number') {
|
|
4626
|
+
return `<span class="json-number">${value}</span>`;
|
|
4627
|
+
}
|
|
4628
|
+
if (typeof value === 'string') {
|
|
4629
|
+
return `<span class="json-string">"${this._escapeHtml(value)}"</span>`;
|
|
4630
|
+
}
|
|
4631
|
+
return this._escapeHtml(String(value));
|
|
4632
|
+
}
|
|
4633
|
+
/**
|
|
4634
|
+
* Format JSON string with syntax highlighting HTML
|
|
4635
|
+
*/
|
|
4636
|
+
_formatJsonHtml(json) {
|
|
4637
|
+
// Do syntax highlighting first, then escape only the values
|
|
4638
|
+
// This preserves the HTML tags we're adding
|
|
4639
|
+
return json
|
|
4640
|
+
// First, escape any < > & in the actual content
|
|
4641
|
+
.replace(/&/g, '&')
|
|
4642
|
+
.replace(/</g, '<')
|
|
4643
|
+
.replace(/>/g, '>')
|
|
4644
|
+
// Now apply syntax highlighting (quotes are still intact)
|
|
4645
|
+
.replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
|
|
4646
|
+
.replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
|
|
4647
|
+
.replace(/: (-?\d+\.?\d*)\b/g, ': <span class="json-number">$1</span>')
|
|
4648
|
+
.replace(/: (true|false)\b/g, ': <span class="json-boolean">$1</span>')
|
|
4649
|
+
.replace(/: (null)\b/g, ': <span class="json-null">$1</span>')
|
|
4650
|
+
// Handle array values (not just after colon)
|
|
4651
|
+
.replace(/\[\s*"([^"]*)"/g, '[ <span class="json-string">"$1"</span>')
|
|
4652
|
+
.replace(/,\s*"([^"]*)"(?=\s*[,\]])/g, ', <span class="json-string">"$1"</span>');
|
|
4653
|
+
}
|
|
4654
|
+
_escapeHtml(text) {
|
|
4655
|
+
return text
|
|
4656
|
+
.replace(/&/g, '&')
|
|
4657
|
+
.replace(/</g, '<')
|
|
4658
|
+
.replace(/>/g, '>')
|
|
4659
|
+
.replace(/"/g, '"')
|
|
4660
|
+
.replace(/'/g, ''');
|
|
4661
|
+
}
|
|
4662
|
+
dispose() {
|
|
4663
|
+
ResponsePanel.currentPanel = undefined;
|
|
4664
|
+
this._panel.dispose();
|
|
4665
|
+
while (this._disposables.length) {
|
|
4666
|
+
const disposable = this._disposables.pop();
|
|
4667
|
+
if (disposable) {
|
|
4668
|
+
disposable.dispose();
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
exports.ResponsePanel = ResponsePanel;
|
|
4674
|
+
//# sourceMappingURL=responsePanel.js.map
|