@scenetest/vite-plugin 0.11.0 → 0.13.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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/panels/observer/audio.d.ts +81 -0
- package/dist/panels/observer/audio.d.ts.map +1 -0
- package/dist/panels/observer/audio.js +296 -0
- package/dist/panels/observer/audio.js.map +1 -0
- package/dist/panels/observer/auto.d.ts +10 -0
- package/dist/panels/observer/auto.d.ts.map +1 -0
- package/dist/panels/observer/auto.js +11 -0
- package/dist/panels/observer/auto.js.map +1 -0
- package/dist/panels/observer/fs-viewer.d.ts +20 -0
- package/dist/panels/observer/fs-viewer.d.ts.map +1 -0
- package/dist/panels/observer/fs-viewer.js +536 -0
- package/dist/panels/observer/fs-viewer.js.map +1 -0
- package/dist/panels/observer/fullscreen.d.ts +24 -0
- package/dist/panels/observer/fullscreen.d.ts.map +1 -0
- package/dist/panels/observer/fullscreen.js +701 -0
- package/dist/panels/observer/fullscreen.js.map +1 -0
- package/dist/panels/observer/history.d.ts +41 -0
- package/dist/panels/observer/history.d.ts.map +1 -0
- package/dist/panels/observer/history.js +307 -0
- package/dist/panels/observer/history.js.map +1 -0
- package/dist/panels/observer/index.d.ts +33 -0
- package/dist/panels/observer/index.d.ts.map +1 -0
- package/dist/panels/observer/index.js +128 -0
- package/dist/panels/observer/index.js.map +1 -0
- package/dist/panels/observer/panel.d.ts +12 -0
- package/dist/panels/observer/panel.d.ts.map +1 -0
- package/dist/panels/observer/panel.js +461 -0
- package/dist/panels/observer/panel.js.map +1 -0
- package/dist/panels/observer/render.d.ts +109 -0
- package/dist/panels/observer/render.d.ts.map +1 -0
- package/dist/panels/observer/render.js +760 -0
- package/dist/panels/observer/render.js.map +1 -0
- package/dist/panels/observer/state.d.ts +57 -0
- package/dist/panels/observer/state.d.ts.map +1 -0
- package/dist/panels/observer/state.js +187 -0
- package/dist/panels/observer/state.js.map +1 -0
- package/dist/panels/observer/styles.d.ts +6 -0
- package/dist/panels/observer/styles.d.ts.map +1 -0
- package/dist/panels/observer/styles.js +1706 -0
- package/dist/panels/observer/styles.js.map +1 -0
- package/dist/panels/observer/types.d.ts +102 -0
- package/dist/panels/observer/types.d.ts.map +1 -0
- package/dist/panels/observer/types.js +5 -0
- package/dist/panels/observer/types.js.map +1 -0
- package/dist/panels/observer/utils.d.ts +45 -0
- package/dist/panels/observer/utils.d.ts.map +1 -0
- package/dist/panels/observer/utils.js +101 -0
- package/dist/panels/observer/utils.js.map +1 -0
- package/dist/panels/recorder/auto.d.ts +10 -0
- package/dist/panels/recorder/auto.d.ts.map +1 -0
- package/dist/panels/recorder/auto.js +11 -0
- package/dist/panels/recorder/auto.js.map +1 -0
- package/dist/panels/recorder/capture.d.ts +18 -0
- package/dist/panels/recorder/capture.d.ts.map +1 -0
- package/dist/panels/recorder/capture.js +218 -0
- package/dist/panels/recorder/capture.js.map +1 -0
- package/dist/panels/recorder/index.d.ts +41 -0
- package/dist/panels/recorder/index.d.ts.map +1 -0
- package/dist/panels/recorder/index.js +208 -0
- package/dist/panels/recorder/index.js.map +1 -0
- package/dist/panels/recorder/panel.d.ts +55 -0
- package/dist/panels/recorder/panel.d.ts.map +1 -0
- package/dist/panels/recorder/panel.js +284 -0
- package/dist/panels/recorder/panel.js.map +1 -0
- package/dist/panels/recorder/reverse-selector.d.ts +31 -0
- package/dist/panels/recorder/reverse-selector.d.ts.map +1 -0
- package/dist/panels/recorder/reverse-selector.js +116 -0
- package/dist/panels/recorder/reverse-selector.js.map +1 -0
- package/dist/panels/recorder/styles.d.ts +5 -0
- package/dist/panels/recorder/styles.d.ts.map +1 -0
- package/dist/panels/recorder/styles.js +300 -0
- package/dist/panels/recorder/styles.js.map +1 -0
- package/dist/panels/recorder/types.d.ts +51 -0
- package/dist/panels/recorder/types.d.ts.map +1 -0
- package/dist/panels/recorder/types.js +15 -0
- package/dist/panels/recorder/types.js.map +1 -0
- package/dist/strip.d.ts.map +1 -1
- package/dist/strip.js +6 -3
- package/dist/strip.js.map +1 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +5 -2
- package/dist/transform.js.map +1 -1
- package/package.json +8 -6
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fullscreen window management
|
|
3
|
+
*/
|
|
4
|
+
import { groups, passCount, failCount, fullscreenWindow, filter, viewMode, fileViewMode, sequenceLocationKey, sequenceFromView, locationGroups, setFullscreenWindow, setFilter, setViewMode, setFileViewMode, setSequenceLocation, clearAll, panel, toggleGroupCollapsed, } from './state.js';
|
|
5
|
+
import { filterItems, openInEditor } from './utils.js';
|
|
6
|
+
import { renderFullscreenGroup, renderLocationRow, renderSequenceEntry, renderSequenceHeader, renderChordTooltip, renderPianoRoll, renderBackButton, attachEventListeners, buildFileTree, renderFileTree } from './render.js';
|
|
7
|
+
import { fullscreenStyles } from './styles.js';
|
|
8
|
+
import { updatePanel } from './panel.js';
|
|
9
|
+
import { clearSymphony, playSymphony, stopSymphony, toggleMute, isPlaying, getSymphonyInfo, initAudio, playGroupChord, playAssertionSound, } from './audio.js';
|
|
10
|
+
import { assertions, GROUP_THRESHOLD_MS, toggleFileTreeCollapsed } from './state.js';
|
|
11
|
+
import { mountFsViewer } from './fs-viewer.js';
|
|
12
|
+
// Track filesystem viewer cleanup function so we can tear it down on view mode change
|
|
13
|
+
let fsViewerCleanup = null;
|
|
14
|
+
/**
|
|
15
|
+
* Clean up any active filesystem viewer before switching views
|
|
16
|
+
*/
|
|
17
|
+
function cleanupFsViewer() {
|
|
18
|
+
if (fsViewerCleanup) {
|
|
19
|
+
fsViewerCleanup();
|
|
20
|
+
fsViewerCleanup = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Set up event listeners for fullscreen view rendered content.
|
|
25
|
+
* Uses event delegation via attachEventListeners from render.js.
|
|
26
|
+
*/
|
|
27
|
+
function setupFullscreenEventListeners(_doc, listEl) {
|
|
28
|
+
attachEventListeners(listEl, {
|
|
29
|
+
openInEditorFullscreen: (location) => {
|
|
30
|
+
// In fullscreen, we need to call the opener's function or our own
|
|
31
|
+
const opener = window.opener;
|
|
32
|
+
if (opener && opener.__scenetest_openInEditor) {
|
|
33
|
+
opener.__scenetest_openInEditor(location);
|
|
34
|
+
}
|
|
35
|
+
else if (window.__scenetest_openInEditor) {
|
|
36
|
+
window.__scenetest_openInEditor(location);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
openInEditor(location);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
showSequence: (key) => {
|
|
43
|
+
// Show sequence view - call opener's function or our own
|
|
44
|
+
const opener = window.opener;
|
|
45
|
+
if (opener && opener.__scenetest_showSequence) {
|
|
46
|
+
opener.__scenetest_showSequence(key);
|
|
47
|
+
}
|
|
48
|
+
else if (window.__scenetest_showSequence) {
|
|
49
|
+
window.__scenetest_showSequence(key);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
showSequence(key);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
backToLocationView: () => {
|
|
56
|
+
backFromSequence();
|
|
57
|
+
},
|
|
58
|
+
toggleCollapsed: (groupId) => {
|
|
59
|
+
toggleGroupCollapsed(groupId);
|
|
60
|
+
},
|
|
61
|
+
toggleFileTree: (path) => {
|
|
62
|
+
toggleFileTreeCollapsed(path);
|
|
63
|
+
updateFullscreenWindow();
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the HTML for the fullscreen window
|
|
69
|
+
*/
|
|
70
|
+
function getFullscreenHTML() {
|
|
71
|
+
return `
|
|
72
|
+
<!DOCTYPE html>
|
|
73
|
+
<html>
|
|
74
|
+
<head>
|
|
75
|
+
<title>scenetest - Inline Assertions</title>
|
|
76
|
+
<style>${fullscreenStyles}</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<div id="header">
|
|
80
|
+
<span id="title"><span class="icon"><span>\uD83C\uDFAC</span></span>scenetest</span>
|
|
81
|
+
<div id="controls">
|
|
82
|
+
<div id="counts">
|
|
83
|
+
<span class="count pass" id="pass-count">\u2713 0</span>
|
|
84
|
+
<span class="count fail" id="fail-count">\u2717 0</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div id="view-modes" class="btn-group">
|
|
87
|
+
<button class="btn active" id="view-grouped" title="Group by time">Timeline</button>
|
|
88
|
+
<button class="btn" id="view-byLocation" title="Group by code location">By Location</button>
|
|
89
|
+
</div>
|
|
90
|
+
<div id="filters" class="btn-group">
|
|
91
|
+
<button class="btn active" id="filter-all">All</button>
|
|
92
|
+
<button class="btn" id="filter-fails">Errors</button>
|
|
93
|
+
<button class="btn" id="filter-passes">Passes</button>
|
|
94
|
+
</div>
|
|
95
|
+
<span class="separator"></span>
|
|
96
|
+
<div id="audio-controls" class="btn-group">
|
|
97
|
+
<button class="btn audio-btn" id="audio-mute" title="Toggle sound">\uD83D\uDD0A</button>
|
|
98
|
+
<button class="btn audio-btn" id="audio-play" title="Play symphony">\u25B6</button>
|
|
99
|
+
</div>
|
|
100
|
+
<span class="separator"></span>
|
|
101
|
+
<button class="btn" id="scenetest-clear-full">Clear</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div id="list">
|
|
105
|
+
<div id="empty">
|
|
106
|
+
<div id="empty-icon">\uD83C\uDFAC</div>
|
|
107
|
+
<div>Interact with your app to see inline assertions appear here...</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Set filter from fullscreen and sync to main panel
|
|
116
|
+
*/
|
|
117
|
+
function setFullscreenFilter(newFilter) {
|
|
118
|
+
setFilter(newFilter);
|
|
119
|
+
if (panel) {
|
|
120
|
+
panel.querySelector('#scenetest-filter-all')?.classList.toggle('active', filter === 'all');
|
|
121
|
+
panel.querySelector('#scenetest-filter-fails')?.classList.toggle('active', filter === 'fails');
|
|
122
|
+
panel.querySelector('#scenetest-pass')?.classList.toggle('active', filter === 'passes');
|
|
123
|
+
panel.querySelector('#scenetest-fail')?.classList.toggle('active', filter === 'fails');
|
|
124
|
+
}
|
|
125
|
+
updatePanel();
|
|
126
|
+
updateFullscreenWindow();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Set view mode from fullscreen
|
|
130
|
+
*/
|
|
131
|
+
function setFullscreenViewMode(newMode) {
|
|
132
|
+
setViewMode(newMode);
|
|
133
|
+
updateFullscreenWindow();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Show sequence view for a specific location
|
|
137
|
+
*/
|
|
138
|
+
export function showSequence(locationKey) {
|
|
139
|
+
setSequenceLocation(locationKey);
|
|
140
|
+
updateFullscreenWindow();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Go back from sequence view to the view we came from
|
|
144
|
+
*/
|
|
145
|
+
export function backFromSequence() {
|
|
146
|
+
setViewMode(sequenceFromView);
|
|
147
|
+
updateFullscreenWindow();
|
|
148
|
+
}
|
|
149
|
+
// Track which group to scroll to after fullscreen opens
|
|
150
|
+
let scrollToGroupId = null;
|
|
151
|
+
/**
|
|
152
|
+
* Open the fullscreen window and optionally scroll to a specific group
|
|
153
|
+
*/
|
|
154
|
+
export function openFullscreen(groupId) {
|
|
155
|
+
scrollToGroupId = groupId ?? null;
|
|
156
|
+
if (fullscreenWindow && !fullscreenWindow.closed) {
|
|
157
|
+
fullscreenWindow.focus();
|
|
158
|
+
if (scrollToGroupId !== null) {
|
|
159
|
+
scrollToGroup(scrollToGroupId);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const win = window.open('', 'scenetest-fullscreen', 'width=900,height=700');
|
|
164
|
+
if (!win) {
|
|
165
|
+
alert('Please allow popups for this site to use fullscreen mode.');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setFullscreenWindow(win);
|
|
169
|
+
win.document.write(getFullscreenHTML());
|
|
170
|
+
win.document.close();
|
|
171
|
+
// Set up event handlers
|
|
172
|
+
const doc = win.document;
|
|
173
|
+
doc.getElementById('scenetest-clear-full')?.addEventListener('click', () => {
|
|
174
|
+
clearAll();
|
|
175
|
+
clearSymphony();
|
|
176
|
+
updatePanel();
|
|
177
|
+
updateFullscreenWindow();
|
|
178
|
+
});
|
|
179
|
+
// Audio controls
|
|
180
|
+
doc.getElementById('audio-mute')?.addEventListener('click', () => {
|
|
181
|
+
initAudio();
|
|
182
|
+
const nowMuted = toggleMute();
|
|
183
|
+
const muteBtn = doc.getElementById('audio-mute');
|
|
184
|
+
if (muteBtn) {
|
|
185
|
+
muteBtn.textContent = nowMuted ? '\uD83D\uDD07' : '\uD83D\uDD0A';
|
|
186
|
+
muteBtn.classList.toggle('muted', nowMuted);
|
|
187
|
+
}
|
|
188
|
+
// Sync with main panel
|
|
189
|
+
const panelMuteBtn = panel?.querySelector('#scenetest-mute');
|
|
190
|
+
if (panelMuteBtn) {
|
|
191
|
+
panelMuteBtn.textContent = nowMuted ? '\uD83D\uDD07' : '\uD83D\uDD0A';
|
|
192
|
+
panelMuteBtn.classList.toggle('muted', nowMuted);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
doc.getElementById('audio-play')?.addEventListener('click', () => {
|
|
196
|
+
initAudio();
|
|
197
|
+
const playBtn = doc.getElementById('audio-play');
|
|
198
|
+
if (isPlaying()) {
|
|
199
|
+
stopSymphony();
|
|
200
|
+
if (playBtn) {
|
|
201
|
+
playBtn.textContent = '\u25B6';
|
|
202
|
+
playBtn.classList.remove('playing');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const info = getSymphonyInfo();
|
|
207
|
+
if (info.eventCount === 0)
|
|
208
|
+
return;
|
|
209
|
+
playSymphony();
|
|
210
|
+
if (playBtn) {
|
|
211
|
+
playBtn.textContent = '\u23F9';
|
|
212
|
+
playBtn.classList.add('playing');
|
|
213
|
+
}
|
|
214
|
+
// Set up callback for when symphony completes
|
|
215
|
+
;
|
|
216
|
+
window.__scenetest_symphonyComplete = () => {
|
|
217
|
+
const btn = doc.getElementById('audio-play');
|
|
218
|
+
if (btn) {
|
|
219
|
+
btn.textContent = '\u25B6';
|
|
220
|
+
btn.classList.remove('playing');
|
|
221
|
+
}
|
|
222
|
+
// Also update main panel button
|
|
223
|
+
const panelBtn = panel?.querySelector('#scenetest-play');
|
|
224
|
+
if (panelBtn) {
|
|
225
|
+
panelBtn.textContent = '\u25B6';
|
|
226
|
+
panelBtn.classList.remove('playing');
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
doc.getElementById('filter-all')?.addEventListener('click', () => {
|
|
232
|
+
setFullscreenFilter('all');
|
|
233
|
+
});
|
|
234
|
+
doc.getElementById('filter-fails')?.addEventListener('click', () => {
|
|
235
|
+
setFullscreenFilter('fails');
|
|
236
|
+
});
|
|
237
|
+
doc.getElementById('filter-passes')?.addEventListener('click', () => {
|
|
238
|
+
setFullscreenFilter('passes');
|
|
239
|
+
});
|
|
240
|
+
// View mode handlers
|
|
241
|
+
doc.getElementById('view-grouped')?.addEventListener('click', () => {
|
|
242
|
+
setFullscreenViewMode('grouped');
|
|
243
|
+
});
|
|
244
|
+
doc.getElementById('view-byLocation')?.addEventListener('click', () => {
|
|
245
|
+
setFullscreenViewMode('byLocation');
|
|
246
|
+
});
|
|
247
|
+
// Set up event delegation once on the list element (not on every re-render)
|
|
248
|
+
const listEl = doc.getElementById('list');
|
|
249
|
+
if (listEl) {
|
|
250
|
+
setupFullscreenEventListeners(doc, listEl);
|
|
251
|
+
}
|
|
252
|
+
updateFullscreenWindow();
|
|
253
|
+
// Scroll to group if requested
|
|
254
|
+
if (scrollToGroupId !== null) {
|
|
255
|
+
scrollToGroup(scrollToGroupId);
|
|
256
|
+
scrollToGroupId = null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Scroll to a specific group in the fullscreen window and highlight it
|
|
261
|
+
*/
|
|
262
|
+
function scrollToGroup(groupId) {
|
|
263
|
+
if (!fullscreenWindow || fullscreenWindow.closed)
|
|
264
|
+
return;
|
|
265
|
+
const doc = fullscreenWindow.document;
|
|
266
|
+
const groupEl = doc.querySelector(`[data-group-id="${groupId}"]`);
|
|
267
|
+
if (groupEl) {
|
|
268
|
+
// Expand the group if collapsed
|
|
269
|
+
groupEl.classList.remove('collapsed');
|
|
270
|
+
// Scroll into view
|
|
271
|
+
groupEl.scrollIntoView({ behavior: 'auto', block: 'start' });
|
|
272
|
+
// Clear any previous highlight and add to this group
|
|
273
|
+
const prevHighlighted = doc.querySelector('.group.highlighted');
|
|
274
|
+
if (prevHighlighted)
|
|
275
|
+
prevHighlighted.classList.remove('highlighted');
|
|
276
|
+
groupEl.classList.add('highlighted');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Open fullscreen and scroll to a specific group (called from small panel item clicks)
|
|
281
|
+
*/
|
|
282
|
+
export function openFullscreenToGroup(groupId) {
|
|
283
|
+
openFullscreen(groupId);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Update the fullscreen window content
|
|
287
|
+
*/
|
|
288
|
+
export function updateFullscreenWindow() {
|
|
289
|
+
if (!fullscreenWindow || fullscreenWindow.closed)
|
|
290
|
+
return;
|
|
291
|
+
const doc = fullscreenWindow.document;
|
|
292
|
+
const passCountEl = doc.getElementById('pass-count');
|
|
293
|
+
const failCountEl = doc.getElementById('fail-count');
|
|
294
|
+
if (passCountEl)
|
|
295
|
+
passCountEl.textContent = `\u2713 ${passCount}`;
|
|
296
|
+
if (failCountEl)
|
|
297
|
+
failCountEl.textContent = `\u2717 ${failCount}`;
|
|
298
|
+
// Update filter button states
|
|
299
|
+
doc.getElementById('filter-all')?.classList.toggle('active', filter === 'all');
|
|
300
|
+
doc.getElementById('filter-fails')?.classList.toggle('active', filter === 'fails');
|
|
301
|
+
doc.getElementById('filter-passes')?.classList.toggle('active', filter === 'passes');
|
|
302
|
+
// Update view mode button states - sequence view keeps the tab it came from highlighted
|
|
303
|
+
const activeTab = viewMode === 'sequence' ? sequenceFromView : viewMode;
|
|
304
|
+
doc.getElementById('view-grouped')?.classList.toggle('active', activeTab === 'grouped');
|
|
305
|
+
doc.getElementById('view-byLocation')?.classList.toggle('active', activeTab === 'byLocation');
|
|
306
|
+
const listEl = doc.getElementById('list');
|
|
307
|
+
if (!listEl)
|
|
308
|
+
return;
|
|
309
|
+
// Clean up filesystem viewer when switching away from byLocation or when file view mode changes to tree
|
|
310
|
+
if (viewMode !== 'byLocation' || fileViewMode !== 'graph') {
|
|
311
|
+
cleanupFsViewer();
|
|
312
|
+
}
|
|
313
|
+
// Render based on view mode
|
|
314
|
+
if (viewMode === 'sequence' && sequenceLocationKey) {
|
|
315
|
+
renderSequenceView(doc, listEl);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (viewMode === 'byLocation') {
|
|
319
|
+
renderByLocationView(doc, listEl);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Default: grouped (timeline) view
|
|
323
|
+
renderGroupedView(doc, listEl);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Render the grouped (timeline) view
|
|
327
|
+
*/
|
|
328
|
+
function renderGroupedView(doc, listEl) {
|
|
329
|
+
// Save scroll state before re-rendering
|
|
330
|
+
const scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop;
|
|
331
|
+
const isScrolled = scrollTop > 50;
|
|
332
|
+
let anchorGroupId = null;
|
|
333
|
+
let anchorOffset = 0;
|
|
334
|
+
if (isScrolled) {
|
|
335
|
+
const groupEls = listEl.querySelectorAll('[data-group-id]');
|
|
336
|
+
for (const el of groupEls) {
|
|
337
|
+
const rect = el.getBoundingClientRect();
|
|
338
|
+
if (rect.top >= -rect.height / 2) {
|
|
339
|
+
anchorGroupId = parseInt(el.getAttribute('data-group-id') || '', 10);
|
|
340
|
+
anchorOffset = rect.top;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const filteredGroups = groups
|
|
346
|
+
.map(g => ({
|
|
347
|
+
...g,
|
|
348
|
+
items: filterItems(g.items),
|
|
349
|
+
}))
|
|
350
|
+
.filter(g => g.items.length > 0);
|
|
351
|
+
if (filteredGroups.length === 0) {
|
|
352
|
+
const icon = filter === 'fails' ? '\u2713' : '\uD83C\uDFAC';
|
|
353
|
+
const message = filter === 'fails'
|
|
354
|
+
? 'No errors! All assertions passed.'
|
|
355
|
+
: 'Interact with your app to see inline assertions appear here...';
|
|
356
|
+
listEl.innerHTML = `
|
|
357
|
+
<div id="empty">
|
|
358
|
+
<div id="empty-icon">${icon}</div>
|
|
359
|
+
<div>${message}</div>
|
|
360
|
+
</div>
|
|
361
|
+
`;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
listEl.innerHTML = filteredGroups
|
|
365
|
+
.map(g => renderFullscreenGroup(g))
|
|
366
|
+
.reverse()
|
|
367
|
+
.join('');
|
|
368
|
+
// Restore scroll position
|
|
369
|
+
if (anchorGroupId !== null) {
|
|
370
|
+
const anchorEl = listEl.querySelector(`[data-group-id="${anchorGroupId}"]`);
|
|
371
|
+
if (anchorEl) {
|
|
372
|
+
const newRect = anchorEl.getBoundingClientRect();
|
|
373
|
+
const scrollAdjustment = newRect.top - anchorOffset;
|
|
374
|
+
doc.documentElement.scrollTop += scrollAdjustment;
|
|
375
|
+
doc.body.scrollTop += scrollAdjustment;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Render the "by location" view showing one row per unique code location
|
|
381
|
+
*/
|
|
382
|
+
function renderByLocationView(_doc, listEl) {
|
|
383
|
+
// Clean up graph if switching to tree
|
|
384
|
+
if (fileViewMode !== 'graph') {
|
|
385
|
+
cleanupFsViewer();
|
|
386
|
+
}
|
|
387
|
+
// Get all location groups and sort by most recent activity
|
|
388
|
+
const locations = Array.from(locationGroups.values())
|
|
389
|
+
.sort((a, b) => b.lastTimestamp - a.lastTimestamp);
|
|
390
|
+
// Apply filter
|
|
391
|
+
let filteredLocations = locations;
|
|
392
|
+
if (filter === 'fails') {
|
|
393
|
+
filteredLocations = locations.filter(loc => loc.entries.some(e => !e.result));
|
|
394
|
+
}
|
|
395
|
+
else if (filter === 'passes') {
|
|
396
|
+
filteredLocations = locations.filter(loc => loc.entries.some(e => e.result));
|
|
397
|
+
}
|
|
398
|
+
if (filteredLocations.length === 0) {
|
|
399
|
+
const icon = filter === 'fails' ? '\u2713' : '\uD83C\uDFAC';
|
|
400
|
+
const message = filter === 'fails'
|
|
401
|
+
? 'No errors! All assertions passed.'
|
|
402
|
+
: 'Interact with your app to see inline assertions appear here...';
|
|
403
|
+
listEl.innerHTML = `
|
|
404
|
+
<div id="empty">
|
|
405
|
+
<div id="empty-icon">${icon}</div>
|
|
406
|
+
<div>${message}</div>
|
|
407
|
+
</div>
|
|
408
|
+
`;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
// File viewer toggle (tree vs graph)
|
|
412
|
+
const fileViewerToggle = `
|
|
413
|
+
<div class="file-view-toggle">
|
|
414
|
+
<button class="btn file-view-btn${fileViewMode === 'tree' ? ' active' : ''}" id="fv-tree" title="File tree view">Tree</button>
|
|
415
|
+
<button class="btn file-view-btn${fileViewMode === 'graph' ? ' active' : ''}" id="fv-graph" title="Force-directed graph">Graph</button>
|
|
416
|
+
</div>
|
|
417
|
+
`;
|
|
418
|
+
// Render the file viewer section (tree or graph)
|
|
419
|
+
let fileViewerHTML;
|
|
420
|
+
if (fileViewMode === 'graph') {
|
|
421
|
+
fileViewerHTML = `<div id="fs-viewer-container" class="fs-viewer-container"></div>`;
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
const tree = buildFileTree(filteredLocations);
|
|
425
|
+
fileViewerHTML = renderFileTree(tree);
|
|
426
|
+
}
|
|
427
|
+
listEl.innerHTML = `
|
|
428
|
+
${renderPianoRoll(filteredLocations, assertions)}
|
|
429
|
+
${fileViewerToggle}
|
|
430
|
+
${fileViewerHTML}
|
|
431
|
+
<div class="location-list">
|
|
432
|
+
${filteredLocations.map(loc => renderLocationRow(loc)).join('')}
|
|
433
|
+
</div>
|
|
434
|
+
`;
|
|
435
|
+
// Wire up tree/graph toggle buttons
|
|
436
|
+
_doc.getElementById('fv-tree')?.addEventListener('click', () => {
|
|
437
|
+
cleanupFsViewer();
|
|
438
|
+
setFileViewMode('tree');
|
|
439
|
+
updateFullscreenWindow();
|
|
440
|
+
});
|
|
441
|
+
_doc.getElementById('fv-graph')?.addEventListener('click', () => {
|
|
442
|
+
setFileViewMode('graph');
|
|
443
|
+
updateFullscreenWindow();
|
|
444
|
+
});
|
|
445
|
+
// Mount graph if in graph mode
|
|
446
|
+
if (fileViewMode === 'graph') {
|
|
447
|
+
const container = listEl.querySelector('#fs-viewer-container');
|
|
448
|
+
if (container) {
|
|
449
|
+
cleanupFsViewer();
|
|
450
|
+
fsViewerCleanup = mountFsViewer(container, filteredLocations);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Set up click handlers for piano roll columns and note badges
|
|
454
|
+
setupPianoRollHandlers(_doc, listEl);
|
|
455
|
+
setupNoteClickHandlers(_doc, listEl);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Render the sequence view for a specific location
|
|
459
|
+
*/
|
|
460
|
+
function renderSequenceView(_doc, listEl) {
|
|
461
|
+
const group = locationGroups.get(sequenceLocationKey);
|
|
462
|
+
if (!group) {
|
|
463
|
+
// Location no longer exists, go back
|
|
464
|
+
setViewMode('byLocation');
|
|
465
|
+
renderByLocationView(_doc, listEl);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// Apply filter to entries
|
|
469
|
+
let entries = group.entries;
|
|
470
|
+
if (filter === 'fails') {
|
|
471
|
+
entries = entries.filter(e => !e.result);
|
|
472
|
+
}
|
|
473
|
+
else if (filter === 'passes') {
|
|
474
|
+
entries = entries.filter(e => e.result);
|
|
475
|
+
}
|
|
476
|
+
// Reverse entries so most recent is at top
|
|
477
|
+
const reversedEntries = entries.slice().reverse();
|
|
478
|
+
const entryCount = reversedEntries.length;
|
|
479
|
+
listEl.innerHTML = `
|
|
480
|
+
${renderBackButton(sequenceFromView)}
|
|
481
|
+
${renderSequenceHeader(group)}
|
|
482
|
+
<div class="sequence-direction-hint">
|
|
483
|
+
<span class="direction-arrow">\u2191</span> Most recent at top
|
|
484
|
+
</div>
|
|
485
|
+
<div class="sequence-list">
|
|
486
|
+
${reversedEntries.map((entry, i) => {
|
|
487
|
+
const isFirst = i === 0; // Most recent (top)
|
|
488
|
+
const isLast = i === entryCount - 1; // Oldest (bottom)
|
|
489
|
+
return renderSequenceEntry(entry, group.location, isFirst, isLast);
|
|
490
|
+
}).join('')}
|
|
491
|
+
</div>
|
|
492
|
+
${entryCount > 1 ? '<div class="sequence-end-label">First event</div>' : ''}
|
|
493
|
+
`;
|
|
494
|
+
// Set up chord hover handlers
|
|
495
|
+
setupChordHoverHandlers(_doc, listEl);
|
|
496
|
+
// Set up note click handlers for the header badge
|
|
497
|
+
setupNoteClickHandlers(_doc, listEl);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Find all assertions that fired at approximately the same time (within GROUP_THRESHOLD_MS)
|
|
501
|
+
*/
|
|
502
|
+
function findChordSiblings(timestamp) {
|
|
503
|
+
return assertions.filter(a => Math.abs(a.timestamp - timestamp) <= GROUP_THRESHOLD_MS);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Set up hover handlers for chord triggers in the sequence view
|
|
507
|
+
*/
|
|
508
|
+
function setupChordHoverHandlers(doc, listEl) {
|
|
509
|
+
let tooltipEl = null;
|
|
510
|
+
let hideTimeout = null;
|
|
511
|
+
const chordTriggers = listEl.querySelectorAll('.chord-trigger');
|
|
512
|
+
chordTriggers.forEach(trigger => {
|
|
513
|
+
trigger.addEventListener('mouseenter', (e) => {
|
|
514
|
+
const target = e.target;
|
|
515
|
+
const timestamp = parseInt(target.dataset.timestamp || '0', 10);
|
|
516
|
+
if (!timestamp)
|
|
517
|
+
return;
|
|
518
|
+
// Clear any pending hide
|
|
519
|
+
if (hideTimeout) {
|
|
520
|
+
clearTimeout(hideTimeout);
|
|
521
|
+
hideTimeout = null;
|
|
522
|
+
}
|
|
523
|
+
// Find all assertions in this chord
|
|
524
|
+
const chordMembers = findChordSiblings(timestamp);
|
|
525
|
+
if (chordMembers.length === 0)
|
|
526
|
+
return;
|
|
527
|
+
// Initialize audio and play the chord
|
|
528
|
+
initAudio();
|
|
529
|
+
playGroupChord(chordMembers);
|
|
530
|
+
// Create and show tooltip
|
|
531
|
+
if (tooltipEl) {
|
|
532
|
+
tooltipEl.remove();
|
|
533
|
+
}
|
|
534
|
+
tooltipEl = doc.createElement('div');
|
|
535
|
+
tooltipEl.innerHTML = renderChordTooltip(chordMembers);
|
|
536
|
+
const tooltip = tooltipEl.firstElementChild;
|
|
537
|
+
if (tooltip) {
|
|
538
|
+
doc.body.appendChild(tooltip);
|
|
539
|
+
// Position tooltip near the trigger
|
|
540
|
+
const rect = target.getBoundingClientRect();
|
|
541
|
+
tooltip.style.left = `${rect.right + 12}px`;
|
|
542
|
+
tooltip.style.top = `${rect.top - 8}px`;
|
|
543
|
+
// Adjust if off-screen
|
|
544
|
+
const tooltipRect = tooltip.getBoundingClientRect();
|
|
545
|
+
if (tooltipRect.right > window.innerWidth - 20) {
|
|
546
|
+
tooltip.style.left = `${rect.left - tooltipRect.width - 12}px`;
|
|
547
|
+
}
|
|
548
|
+
if (tooltipRect.bottom > window.innerHeight - 20) {
|
|
549
|
+
tooltip.style.top = `${window.innerHeight - tooltipRect.height - 20}px`;
|
|
550
|
+
}
|
|
551
|
+
tooltipEl = tooltip;
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
trigger.addEventListener('mouseleave', () => {
|
|
555
|
+
// Delay hiding to allow moving to tooltip
|
|
556
|
+
hideTimeout = setTimeout(() => {
|
|
557
|
+
if (tooltipEl) {
|
|
558
|
+
tooltipEl.remove();
|
|
559
|
+
tooltipEl = null;
|
|
560
|
+
}
|
|
561
|
+
}, 200);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
// Also hide tooltip when clicking elsewhere
|
|
565
|
+
doc.addEventListener('click', () => {
|
|
566
|
+
if (tooltipEl) {
|
|
567
|
+
tooltipEl.remove();
|
|
568
|
+
tooltipEl = null;
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Set up click handlers for the piano roll columns
|
|
574
|
+
*/
|
|
575
|
+
function setupPianoRollHandlers(_doc, listEl) {
|
|
576
|
+
const pianoRoll = listEl.querySelector('.piano-roll');
|
|
577
|
+
if (!pianoRoll)
|
|
578
|
+
return;
|
|
579
|
+
// Parse chord data
|
|
580
|
+
const chordDataStr = pianoRoll.dataset.chords;
|
|
581
|
+
if (!chordDataStr)
|
|
582
|
+
return;
|
|
583
|
+
let chordData;
|
|
584
|
+
try {
|
|
585
|
+
chordData = JSON.parse(chordDataStr);
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const cells = pianoRoll.querySelectorAll('.piano-cell');
|
|
591
|
+
const markers = pianoRoll.querySelectorAll('.piano-time-marker');
|
|
592
|
+
// Helper to highlight a column
|
|
593
|
+
const highlightColumn = (colIdx) => {
|
|
594
|
+
pianoRoll.classList.add('col-hover');
|
|
595
|
+
cells.forEach(cell => {
|
|
596
|
+
const cellCol = parseInt(cell.dataset.col || '-1', 10);
|
|
597
|
+
cell.classList.toggle('col-active', cellCol === colIdx);
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
// Helper to clear column highlight
|
|
601
|
+
const clearHighlight = () => {
|
|
602
|
+
pianoRoll.classList.remove('col-hover');
|
|
603
|
+
cells.forEach(cell => cell.classList.remove('col-active'));
|
|
604
|
+
};
|
|
605
|
+
// Helper to play a chord
|
|
606
|
+
const playColumn = (colIdx) => {
|
|
607
|
+
if (colIdx < 0 || colIdx >= chordData.length)
|
|
608
|
+
return;
|
|
609
|
+
const chord = chordData[colIdx];
|
|
610
|
+
initAudio();
|
|
611
|
+
// Create assertion-like objects for playGroupChord
|
|
612
|
+
const assertionLike = chord.notes.map(n => ({
|
|
613
|
+
type: (n.result ? 'pass' : 'fail'),
|
|
614
|
+
description: n.description,
|
|
615
|
+
result: n.result,
|
|
616
|
+
timestamp: chord.timestamp,
|
|
617
|
+
}));
|
|
618
|
+
playGroupChord(assertionLike);
|
|
619
|
+
// Visual feedback on marker
|
|
620
|
+
const marker = markers[colIdx];
|
|
621
|
+
if (marker) {
|
|
622
|
+
marker.classList.add('playing');
|
|
623
|
+
setTimeout(() => marker.classList.remove('playing'), 300);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
// Add hover and click handlers to cells
|
|
627
|
+
cells.forEach(cell => {
|
|
628
|
+
const colIdx = parseInt(cell.dataset.col || '-1', 10);
|
|
629
|
+
cell.addEventListener('mouseenter', () => highlightColumn(colIdx));
|
|
630
|
+
cell.addEventListener('mouseleave', clearHighlight);
|
|
631
|
+
cell.addEventListener('click', (e) => {
|
|
632
|
+
e.stopPropagation();
|
|
633
|
+
playColumn(colIdx);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
// Add hover and click handlers to timeline markers
|
|
637
|
+
markers.forEach((marker, idx) => {
|
|
638
|
+
marker.addEventListener('mouseenter', () => highlightColumn(idx));
|
|
639
|
+
marker.addEventListener('mouseleave', clearHighlight);
|
|
640
|
+
marker.addEventListener('click', (e) => {
|
|
641
|
+
e.stopPropagation();
|
|
642
|
+
playColumn(idx);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Set up click handlers for note badges and piano bars to play sounds
|
|
648
|
+
*/
|
|
649
|
+
function setupNoteClickHandlers(_doc, listEl) {
|
|
650
|
+
// Handle clickable note badges
|
|
651
|
+
const noteBadges = listEl.querySelectorAll('.note-badge.clickable');
|
|
652
|
+
noteBadges.forEach(badge => {
|
|
653
|
+
badge.addEventListener('click', (e) => {
|
|
654
|
+
e.stopPropagation();
|
|
655
|
+
const target = e.currentTarget;
|
|
656
|
+
const description = target.dataset.description || '';
|
|
657
|
+
const result = target.dataset.result === 'true';
|
|
658
|
+
if (!description)
|
|
659
|
+
return;
|
|
660
|
+
// Initialize audio and play the note
|
|
661
|
+
initAudio();
|
|
662
|
+
playAssertionSound({
|
|
663
|
+
type: result ? 'pass' : 'fail',
|
|
664
|
+
description,
|
|
665
|
+
result,
|
|
666
|
+
timestamp: Date.now(),
|
|
667
|
+
});
|
|
668
|
+
// Visual feedback
|
|
669
|
+
target.style.transform = 'scale(1.3)';
|
|
670
|
+
setTimeout(() => {
|
|
671
|
+
target.style.transform = '';
|
|
672
|
+
}, 150);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
// Handle piano bar clicks
|
|
676
|
+
const pianoBars = listEl.querySelectorAll('.piano-bar');
|
|
677
|
+
pianoBars.forEach(bar => {
|
|
678
|
+
bar.addEventListener('click', (e) => {
|
|
679
|
+
e.stopPropagation();
|
|
680
|
+
const target = e.currentTarget;
|
|
681
|
+
const description = target.dataset.description || '';
|
|
682
|
+
const result = target.dataset.result === 'true';
|
|
683
|
+
if (!description)
|
|
684
|
+
return;
|
|
685
|
+
// Initialize audio and play the note
|
|
686
|
+
initAudio();
|
|
687
|
+
playAssertionSound({
|
|
688
|
+
type: result ? 'pass' : 'fail',
|
|
689
|
+
description,
|
|
690
|
+
result,
|
|
691
|
+
timestamp: Date.now(),
|
|
692
|
+
});
|
|
693
|
+
// Visual feedback
|
|
694
|
+
target.style.transform = 'translateX(8px) scale(1.02)';
|
|
695
|
+
setTimeout(() => {
|
|
696
|
+
target.style.transform = '';
|
|
697
|
+
}, 150);
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
//# sourceMappingURL=fullscreen.js.map
|