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.
- checksums.yaml +4 -4
- data/README.md +37 -5
- data/exe/capybara-simulated +143 -0
- data/lib/capybara/simulated/browser.rb +570 -53
- data/lib/capybara/simulated/driver.rb +52 -6
- data/lib/capybara/simulated/js/bridge.bundle.js +11575 -5716
- data/lib/capybara/simulated/js/snapshot_stubs.js +34 -0
- data/lib/capybara/simulated/minitest.rb +65 -0
- data/lib/capybara/simulated/quickjs_runtime.rb +9 -0
- data/lib/capybara/simulated/rspec.rb +32 -0
- data/lib/capybara/simulated/runtime_shared.rb +26 -2
- data/lib/capybara/simulated/trace.rb +35 -2
- data/lib/capybara/simulated/trace_persistence.rb +48 -0
- data/lib/capybara/simulated/trace_viewer.html +408 -0
- data/lib/capybara/simulated/v8_runtime.rb +182 -17
- data/lib/capybara/simulated/version.rb +1 -1
- data/vendor/js/vendor.bundle.js +21 -10
- metadata +23 -3
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
149
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
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>
|