capybara-simulated 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,408 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>capybara-simulated trace</title>
7
+ <!--
8
+ Self-contained trace viewer. The `capybara-simulated trace <file.json>`
9
+ CLI replaces the data token in the script block below with the trace
10
+ JSON (with `</` escaped to `<\/` so an embedded `</script>` in a DOM
11
+ snapshot can't close the data block). Inline embedding — not fetch /
12
+ `import … with { type: 'json' }` — is what lets the file open straight
13
+ from `file://` without a server (module/fetch loads are CORS-blocked
14
+ for `file://` origins). The Load button / drag-and-drop reads any other
15
+ trace JSON via FileReader, which is also CORS-free.
16
+ -->
17
+ <script id="csim-trace" type="application/json">__CSIM_TRACE_DATA__</script>
18
+ <style>
19
+ :root {
20
+ --bg: #f6f7f9; --panel: #fff; --border: #e2e5ea; --text: #1f2329;
21
+ --muted: #6b7280; --accent: #2563eb; --accent-soft: #eaf0fe;
22
+ --error: #dc2626; --error-soft: #fdecec; --ok: #15803d;
23
+ --warn: #b45309; --code: #f3f4f6;
24
+ }
25
+ * { box-sizing: border-box; }
26
+ html, body { height: 100%; margin: 0; }
27
+ body {
28
+ font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
29
+ color: var(--text); background: var(--bg); display: flex; flex-direction: column;
30
+ }
31
+ header {
32
+ display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
33
+ padding: 10px 16px; background: var(--panel); border-bottom: 1px solid var(--border);
34
+ }
35
+ header h1 { font-size: 15px; margin: 0; font-weight: 600; }
36
+ header .meta { color: var(--muted); font-size: 12px; display: flex; gap: 14px; flex-wrap: wrap; }
37
+ header .spacer { flex: 1; }
38
+ .badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }
39
+ .badge.passed { background: #e6f4ea; color: var(--ok); }
40
+ .badge.failed { background: var(--error-soft); color: var(--error); }
41
+ button, label.btn {
42
+ font: inherit; font-size: 13px; padding: 5px 12px; border: 1px solid var(--border);
43
+ background: var(--panel); border-radius: 6px; cursor: pointer; color: var(--text);
44
+ }
45
+ button:hover, label.btn:hover { background: var(--code); }
46
+ main { flex: 1; display: flex; min-height: 0; }
47
+ #steps { width: 320px; flex: none; overflow-y: auto; border-right: 1px solid var(--border); background: var(--panel); }
48
+ .step {
49
+ display: flex; align-items: baseline; gap: 8px; padding: 8px 12px;
50
+ border-bottom: 1px solid var(--border); cursor: pointer; border-left: 3px solid transparent;
51
+ }
52
+ .step:hover { background: var(--bg); }
53
+ .step.sel { background: var(--accent-soft); border-left-color: var(--accent); }
54
+ .step.err { border-left-color: var(--error); }
55
+ .step .idx { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 12px; min-width: 22px; }
56
+ .step .kind { font-weight: 600; font-size: 12px; }
57
+ .step .desc { flex: 1; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
58
+ .step .dur { color: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; }
59
+ .step.err .kind { color: var(--error); }
60
+ #detail { flex: 1; overflow-y: auto; padding: 18px 22px; min-width: 0; }
61
+ #detail h2 { margin: 0 0 2px; font-size: 17px; }
62
+ #detail .sub { color: var(--muted); font-size: 12px; margin-bottom: 16px; }
63
+ section.block { margin: 18px 0; }
64
+ section.block > h3 {
65
+ font-size: 12px; text-transform: uppercase; letter-spacing: .04em;
66
+ color: var(--muted); margin: 0 0 8px; font-weight: 600;
67
+ }
68
+ code, pre, .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
69
+ .kv { display: grid; grid-template-columns: auto 1fr; gap: 2px 14px; }
70
+ .kv dt { color: var(--muted); }
71
+ .kv dd { margin: 0; word-break: break-all; }
72
+ .url-to { font-weight: 600; }
73
+ .panel-error { background: var(--error-soft); border: 1px solid #f3c4c4; border-radius: 6px; padding: 10px 12px; }
74
+ .panel-error .cls { font-weight: 600; color: var(--error); }
75
+ .panel-error .msg { white-space: pre-wrap; margin-top: 4px; }
76
+ .logs { border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
77
+ .log { display: flex; gap: 10px; padding: 4px 10px; border-top: 1px solid var(--border); font-size: 12.5px; }
78
+ .log:first-child { border-top: none; }
79
+ .log .sev { flex: none; width: 52px; font-weight: 600; font-size: 11px; text-transform: uppercase; }
80
+ .log .sev.error { color: var(--error); } .log .sev.warn, .log .sev.warning { color: var(--warn); }
81
+ .log .sev.debug { color: var(--muted); }
82
+ .log .msg { white-space: pre-wrap; word-break: break-word; }
83
+ .net-hint { font-size: 11px; margin-bottom: 6px; }
84
+ .net { width: 100%; border-collapse: collapse; font-size: 12.5px; }
85
+ .net th, .net td { text-align: left; padding: 4px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
86
+ .net th { color: var(--muted); font-weight: 600; }
87
+ .net td.method { font-weight: 600; } .net td.url { word-break: break-all; }
88
+ .net td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
89
+ .net td.status { font-variant-numeric: tabular-nums; }
90
+ .net td.status.ok { color: var(--ok); } .net td.status.redir { color: var(--warn); } .net td.status.err { color: var(--error); }
91
+ .net-row { cursor: pointer; }
92
+ .net-row:hover { background: var(--bg); }
93
+ .net-row.open { background: var(--accent-soft); }
94
+ .redir-tag { display: inline-block; font-size: 10px; font-weight: 600; color: var(--warn);
95
+ background: #fff5e6; border-radius: 8px; padding: 0 6px; vertical-align: middle; }
96
+ .net-detail td { background: var(--bg); }
97
+ .net-sub { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase;
98
+ letter-spacing: .03em; margin: 8px 0 4px; }
99
+ .net-detail .net-sub:first-child { margin-top: 0; }
100
+ .kv.hdr { font-size: 12px; grid-template-columns: max-content 1fr; gap: 1px 12px; }
101
+ .kv.hdr dt { color: var(--muted); }
102
+ .net-body { max-height: 260px; overflow: auto; background: var(--code); border: 1px solid var(--border);
103
+ border-radius: 6px; padding: 8px 10px; font-size: 12px; margin: 0; white-space: pre-wrap; word-break: break-word; }
104
+ .tabs { display: flex; gap: 6px; margin-bottom: 8px; }
105
+ .tabs button.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
106
+ iframe.dom { width: 100%; height: 460px; border: 1px solid var(--border); border-radius: 6px; background: #fff; }
107
+ pre.dom { max-height: 460px; overflow: auto; background: var(--code); border: 1px solid var(--border);
108
+ border-radius: 6px; padding: 12px; font-size: 12px; margin: 0; }
109
+ .muted { color: var(--muted); }
110
+ #empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; color: var(--muted); text-align: center; }
111
+ #empty.hide { display: none; }
112
+ #drop-veil { position: fixed; inset: 0; background: rgba(37,99,235,.08); border: 3px dashed var(--accent);
113
+ display: none; align-items: center; justify-content: center; font-size: 18px; color: var(--accent); z-index: 10; }
114
+ #drop-veil.on { display: flex; }
115
+ </style>
116
+ </head>
117
+ <body>
118
+ <header>
119
+ <h1>capybara-simulated trace</h1>
120
+ <div class="meta" id="meta"></div>
121
+ <span class="spacer"></span>
122
+ <label class="btn">Load JSON…<input id="file" type="file" accept=".json,application/json" hidden></label>
123
+ </header>
124
+ <main>
125
+ <div id="steps"></div>
126
+ <div id="detail"></div>
127
+ <div id="empty">
128
+ <div style="font-size:40px">📊</div>
129
+ <div>No trace loaded.<br>Drop a <code>*.json</code> trace here or use <b>Load JSON…</b>.</div>
130
+ </div>
131
+ </main>
132
+ <div id="drop-veil">Drop trace JSON to load</div>
133
+
134
+ <script>
135
+ (function () {
136
+ 'use strict';
137
+ var stepsEl = document.getElementById('steps');
138
+ var detailEl = document.getElementById('detail');
139
+ var emptyEl = document.getElementById('empty');
140
+ var metaEl = document.getElementById('meta');
141
+ var trace = null, selected = 0;
142
+
143
+ // Escapes both text and double-quoted-attribute contexts (severity /
144
+ // outcome land in class="…"), so a quote in trace data can't break out
145
+ // of an attribute and inject markup.
146
+ function esc(s) {
147
+ return String(s == null ? '' : s)
148
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
149
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
150
+ }
151
+ function readEmbedded() {
152
+ var el = document.getElementById('csim-trace');
153
+ try {
154
+ var d = JSON.parse(el.textContent);
155
+ return (d && Array.isArray(d.steps)) ? d : null;
156
+ } catch (_) { return null; }
157
+ }
158
+
159
+ function load(data) {
160
+ if (!data || !Array.isArray(data.steps)) { return; }
161
+ trace = data; selected = 0;
162
+ emptyEl.classList.add('hide');
163
+ stepsEl.style.display = ''; detailEl.style.display = '';
164
+ renderMeta(); renderSteps(); renderDetail();
165
+ }
166
+
167
+ function renderMeta() {
168
+ var m = trace.metadata || {}, parts = [];
169
+ if (m.title) parts.push('<span>' + esc(m.title) + '</span>');
170
+ if (m.file) parts.push('<span class="mono">' + esc(m.file) + '</span>');
171
+ if (m.outcome) parts.push('<span class="badge ' + esc(m.outcome) + '">' + esc(m.outcome) + '</span>');
172
+ parts.push('<span>' + trace.steps.length + ' steps</span>');
173
+ var total = trace.steps.reduce(function (a, s) { return a + (s.duration_ms || 0); }, 0);
174
+ parts.push('<span>' + total + ' ms total</span>');
175
+ metaEl.innerHTML = parts.join('');
176
+ }
177
+
178
+ function renderSteps() {
179
+ stepsEl.innerHTML = '';
180
+ trace.steps.forEach(function (s, i) {
181
+ var row = document.createElement('div');
182
+ row.className = 'step' + (i === selected ? ' sel' : '') + (s.error ? ' err' : '');
183
+ row.innerHTML =
184
+ '<span class="idx">' + i + '</span>' +
185
+ '<span class="kind">' + esc(s.kind) + '</span>' +
186
+ '<span class="desc">' + esc(s.description) + '</span>' +
187
+ '<span class="dur">' + (s.duration_ms == null ? '' : s.duration_ms + 'ms') + '</span>';
188
+ row.onclick = function () { selected = i; renderSteps(); renderDetail(); };
189
+ stepsEl.appendChild(row);
190
+ });
191
+ }
192
+
193
+ function section(title, bodyHtml) {
194
+ return '<section class="block"><h3>' + title + '</h3>' + bodyHtml + '</section>';
195
+ }
196
+
197
+ function fmtBytes(n) {
198
+ if (n == null) { return ''; }
199
+ if (n < 1024) { return n + ' B'; }
200
+ if (n < 1048576) { return (n / 1024).toFixed(1) + ' KB'; }
201
+ return (n / 1048576).toFixed(1) + ' MB';
202
+ }
203
+ function statusClass(st) {
204
+ var c = Math.floor((+st || 0) / 100);
205
+ return c === 2 ? 'ok' : c === 3 ? 'redir' : (c >= 4 ? 'err' : '');
206
+ }
207
+ function headerTable(h) {
208
+ var keys = h ? Object.keys(h) : [];
209
+ if (!keys.length) { return '<div class="muted">—</div>'; }
210
+ return '<dl class="kv hdr">' + keys.map(function (k) {
211
+ return '<dt>' + esc(k) + '</dt><dd>' + esc(h[k]) + '</dd>';
212
+ }).join('') + '</dl>';
213
+ }
214
+ function bodyBlock(label, body) {
215
+ if (body == null || body === '') { return ''; }
216
+ return '<div class="net-sub">' + label + '</div><pre class="net-body">' + esc(body) + '</pre>';
217
+ }
218
+ function wireNetwork() {
219
+ detailEl.querySelectorAll('tr.net-row').forEach(function (row) {
220
+ row.onclick = function () {
221
+ var d = detailEl.querySelector('tr.net-detail[data-i="' + row.getAttribute('data-i') + '"]');
222
+ if (d) { d.hidden = !d.hidden; row.classList.toggle('open'); }
223
+ };
224
+ });
225
+ }
226
+
227
+ function renderDetail() {
228
+ var s = trace.steps[selected];
229
+ if (!s) { detailEl.innerHTML = ''; return; }
230
+ var html = '<h2>#' + selected + ' · ' + esc(s.kind) + '</h2>';
231
+ html += '<div class="sub mono">' + esc(s.description) + '</div>';
232
+
233
+ var kv = '<dl class="kv">';
234
+ kv += '<dt>elapsed</dt><dd>' + (s.elapsed_ms || 0) + ' ms</dd>';
235
+ kv += '<dt>duration</dt><dd>' + (s.duration_ms || 0) + ' ms</dd>';
236
+ if (s.url_before != null || s.url_after != null) {
237
+ var changed = s.url_before !== s.url_after;
238
+ kv += '<dt>url</dt><dd class="mono">' + esc(s.url_before || '∅') +
239
+ (changed ? ' → <span class="url-to">' + esc(s.url_after || '∅') + '</span>' : '') + '</dd>';
240
+ }
241
+ kv += '</dl>';
242
+ html += section('Step', kv);
243
+
244
+ if (s.error) {
245
+ html += section('Error',
246
+ '<div class="panel-error"><div class="cls mono">' + esc(s.error.class || 'Error') +
247
+ '</div><div class="msg mono">' + esc(s.error.message) + '</div></div>');
248
+ }
249
+
250
+ if (s.console && s.console.length) {
251
+ var logs = s.console.map(function (c) {
252
+ var sev = String(c.severity || 'log').toLowerCase();
253
+ return '<div class="log"><span class="sev ' + esc(sev) + '">' + esc(sev) +
254
+ '</span><span class="msg">' + esc(c.message) + '</span></div>';
255
+ }).join('');
256
+ html += section('Console (' + s.console.length + ')', '<div class="logs">' + logs + '</div>');
257
+ }
258
+
259
+ if (s.network && s.network.length) {
260
+ var rows = s.network.map(function (n, i) {
261
+ var head =
262
+ '<tr class="net-row" data-i="' + i + '">' +
263
+ '<td class="method">' + esc(n.method) + '</td>' +
264
+ '<td class="url">' + esc(n.url) +
265
+ (n.redirected ? ' <span class="redir-tag">redirect</span>' : '') + '</td>' +
266
+ '<td class="status ' + statusClass(n.status) + '">' + esc(n.status) + '</td>' +
267
+ '<td>' + esc(n.content_type || '') + '</td>' +
268
+ '<td class="num">' + fmtBytes(n.size) + '</td>' +
269
+ '<td class="num">' + (n.duration_ms == null ? '' : n.duration_ms + ' ms') + '</td>' +
270
+ '</tr>';
271
+ var detail =
272
+ '<tr class="net-detail" data-i="' + i + '" hidden><td colspan="6">' +
273
+ '<div class="net-sub">Request headers</div>' + headerTable(n.request_headers) +
274
+ bodyBlock('Request body', n.request_body) +
275
+ '<div class="net-sub">Response headers</div>' + headerTable(n.response_headers) +
276
+ bodyBlock('Response body', n.response_body) +
277
+ '</td></tr>';
278
+ return head + detail;
279
+ }).join('');
280
+ html += section('Network (' + s.network.length + ')',
281
+ '<div class="muted net-hint">Click a request for headers and bodies.</div>' +
282
+ '<table class="net"><tr><th>Method</th><th>URL</th><th>Status</th>' +
283
+ '<th>Type</th><th>Size</th><th>Time</th></tr>' + rows + '</table>');
284
+ }
285
+
286
+ if (s.dom_after) {
287
+ html += section('DOM snapshot',
288
+ '<div class="tabs"><button id="t-prev" class="on">Preview</button>' +
289
+ '<button id="t-src">HTML</button></div><div id="dom-host"></div>');
290
+ } else {
291
+ html += section('DOM snapshot',
292
+ '<div class="muted">Not captured for this step — snapshots are taken only on an action error, ' +
293
+ 'or for every step under <code>CSIM_TRACE=full</code>.</div>');
294
+ }
295
+
296
+ detailEl.innerHTML = html;
297
+ detailEl.scrollTop = 0;
298
+ if (s.network && s.network.length) { wireNetwork(); }
299
+ if (s.dom_after) { wireDomTabs(s.dom_after); }
300
+ }
301
+
302
+ // Pretty-print a DOM-snapshot HTML string for the source view: the
303
+ // driver serializes flat (one line), so we re-parse with the browser's
304
+ // own DOMParser and re-emit indented. Returns null on any parse trouble
305
+ // → caller falls back to the raw string. Raw-text / preformatted
306
+ // elements (pre/textarea/script/style) keep their content verbatim. The
307
+ // result is raw HTML text — the caller esc()s it once for display.
308
+ var VOID = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
309
+ var RAWEL = new Set(['pre','textarea','script','style']);
310
+ function formatHtml(html) {
311
+ var doc;
312
+ try { doc = new DOMParser().parseFromString(html, 'text/html'); }
313
+ catch (_) { return null; }
314
+ if (!doc || !doc.documentElement) { return null; }
315
+ var out = [];
316
+ var pad = function (d) { return ' '.repeat(d); };
317
+ var attrs = function (el) {
318
+ return Array.prototype.map.call(el.attributes, function (a) {
319
+ return a.value === '' ? ' ' + a.name : ' ' + a.name + '="' + a.value + '"';
320
+ }).join('');
321
+ };
322
+ function emit(el, depth) {
323
+ var tag = el.tagName.toLowerCase();
324
+ var open = '<' + tag + attrs(el) + '>';
325
+ if (VOID.has(tag)) { out.push(pad(depth) + open); return; }
326
+ if (RAWEL.has(tag)) {
327
+ var text = el.textContent;
328
+ if (text.indexOf('\n') === -1) {
329
+ out.push(pad(depth) + open + text + '</' + tag + '>');
330
+ } else {
331
+ out.push(pad(depth) + open);
332
+ text.split('\n').forEach(function (l) { out.push(l); }); // verbatim, no re-indent
333
+ out.push(pad(depth) + '</' + tag + '>');
334
+ }
335
+ return;
336
+ }
337
+ var kids = Array.prototype.filter.call(el.childNodes, function (n) {
338
+ return n.nodeType === 1 || n.nodeType === 8 || (n.nodeType === 3 && /\S/.test(n.nodeValue));
339
+ });
340
+ if (kids.length === 0) {
341
+ out.push(pad(depth) + open + '</' + tag + '>');
342
+ } else if (kids.length === 1 && kids[0].nodeType === 3) {
343
+ out.push(pad(depth) + open + kids[0].nodeValue.trim() + '</' + tag + '>');
344
+ } else {
345
+ out.push(pad(depth) + open);
346
+ kids.forEach(function (k) {
347
+ if (k.nodeType === 3) { out.push(pad(depth + 1) + k.nodeValue.trim()); }
348
+ else if (k.nodeType === 8) { out.push(pad(depth + 1) + '<!--' + k.nodeValue + '-->'); }
349
+ else { emit(k, depth + 1); }
350
+ });
351
+ out.push(pad(depth) + '</' + tag + '>');
352
+ }
353
+ }
354
+ if (doc.doctype) { out.push('<!DOCTYPE ' + doc.doctype.name + '>'); }
355
+ emit(doc.documentElement, 0);
356
+ return out.join('\n');
357
+ }
358
+
359
+ function wireDomTabs(domHtml) {
360
+ var host = document.getElementById('dom-host');
361
+ var bPrev = document.getElementById('t-prev');
362
+ var bSrc = document.getElementById('t-src');
363
+ function preview() {
364
+ bPrev.classList.add('on'); bSrc.classList.remove('on');
365
+ var f = document.createElement('iframe');
366
+ f.className = 'dom'; f.setAttribute('sandbox', ''); // no scripts: a static visual snapshot
367
+ host.innerHTML = ''; host.appendChild(f);
368
+ f.srcdoc = domHtml;
369
+ }
370
+ function source() {
371
+ bSrc.classList.add('on'); bPrev.classList.remove('on');
372
+ var pretty = formatHtml(domHtml);
373
+ host.innerHTML = '<pre class="dom">' + esc(pretty != null ? pretty : domHtml) + '</pre>';
374
+ }
375
+ bPrev.onclick = preview; bSrc.onclick = source;
376
+ preview();
377
+ }
378
+
379
+ // ── loading other traces (FileReader → CORS-free, works on file://) ──
380
+ function readFile(file) {
381
+ var r = new FileReader();
382
+ r.onload = function () {
383
+ try { load(JSON.parse(r.result)); }
384
+ catch (e) { alert('Not a valid trace JSON: ' + e.message); }
385
+ };
386
+ r.readAsText(file);
387
+ }
388
+ document.getElementById('file').addEventListener('change', function (e) {
389
+ if (e.target.files[0]) { readFile(e.target.files[0]); }
390
+ });
391
+ var veil = document.getElementById('drop-veil');
392
+ window.addEventListener('dragover', function (e) { e.preventDefault(); veil.classList.add('on'); });
393
+ window.addEventListener('dragleave', function (e) { if (e.relatedTarget === null) { veil.classList.remove('on'); } });
394
+ window.addEventListener('drop', function (e) {
395
+ e.preventDefault(); veil.classList.remove('on');
396
+ if (e.dataTransfer.files[0]) { readFile(e.dataTransfer.files[0]); }
397
+ });
398
+ window.addEventListener('keydown', function (e) {
399
+ if (!trace) { return; }
400
+ if (e.key === 'ArrowDown' || e.key === 'j') { selected = Math.min(selected + 1, trace.steps.length - 1); renderSteps(); renderDetail(); }
401
+ if (e.key === 'ArrowUp' || e.key === 'k') { selected = Math.max(selected - 1, 0); renderSteps(); renderDetail(); }
402
+ });
403
+
404
+ load(readEmbedded());
405
+ })();
406
+ </script>
407
+ </body>
408
+ </html>