@recursica/adapter-tester 1.1.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/README.md +164 -0
- package/dist/config-B0Eop8Az.cjs +2 -0
- package/dist/config-B0Eop8Az.cjs.map +1 -0
- package/dist/config-CKgyDyNF.js +23 -0
- package/dist/config-CKgyDyNF.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/extractDomNode.d.ts +11 -0
- package/dist/extractDomNode.d.ts.map +1 -0
- package/dist/harness/mantineSourceOfTruth.d.ts +42 -0
- package/dist/harness/mantineSourceOfTruth.d.ts.map +1 -0
- package/dist/index.cjs +54 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.html +461 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/runVisualRegression.d.ts +10 -0
- package/dist/testing/runVisualRegression.d.ts.map +1 -0
- package/dist/testing.cjs +2 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +143 -0
- package/dist/testing.js.map +1 -0
- package/package.json +70 -0
package/dist/index.html
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Recursica UI Kit - Adapter Dev Mode</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
margin: 0;
|
|
9
|
+
display: flex;
|
|
10
|
+
height: 100vh;
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
12
|
+
background-color: #1e1e1e;
|
|
13
|
+
color: #fff;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
.pane {
|
|
17
|
+
flex: 1;
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
position: relative;
|
|
21
|
+
}
|
|
22
|
+
.header {
|
|
23
|
+
padding: 10px 15px;
|
|
24
|
+
background-color: #252526;
|
|
25
|
+
border-bottom: 1px solid #333;
|
|
26
|
+
font-size: 13px;
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
text-transform: uppercase;
|
|
29
|
+
letter-spacing: 1px;
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
align-items: center;
|
|
33
|
+
}
|
|
34
|
+
.status {
|
|
35
|
+
font-size: 11px;
|
|
36
|
+
color: #aaa;
|
|
37
|
+
text-transform: none;
|
|
38
|
+
letter-spacing: normal;
|
|
39
|
+
font-weight: normal;
|
|
40
|
+
}
|
|
41
|
+
.status.connected {
|
|
42
|
+
color: #4CAF50;
|
|
43
|
+
}
|
|
44
|
+
iframe {
|
|
45
|
+
flex: 1;
|
|
46
|
+
border: none;
|
|
47
|
+
width: 100%;
|
|
48
|
+
background: white;
|
|
49
|
+
}
|
|
50
|
+
.divider {
|
|
51
|
+
width: 4px;
|
|
52
|
+
background: #333;
|
|
53
|
+
cursor: col-resize;
|
|
54
|
+
z-index: 10;
|
|
55
|
+
transition: background 0.2s;
|
|
56
|
+
}
|
|
57
|
+
.divider:hover {
|
|
58
|
+
background: #007acc;
|
|
59
|
+
}
|
|
60
|
+
.h-divider {
|
|
61
|
+
height: 4px;
|
|
62
|
+
background: #333;
|
|
63
|
+
cursor: row-resize;
|
|
64
|
+
z-index: 10;
|
|
65
|
+
transition: background 0.2s;
|
|
66
|
+
}
|
|
67
|
+
.h-divider:hover {
|
|
68
|
+
background: #007acc;
|
|
69
|
+
}
|
|
70
|
+
.right-top {
|
|
71
|
+
flex: 0 0 70%;
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
position: relative;
|
|
75
|
+
}
|
|
76
|
+
.right-bottom {
|
|
77
|
+
flex: 1;
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
position: relative;
|
|
81
|
+
background-color: #1e1e1e;
|
|
82
|
+
}
|
|
83
|
+
.notes-area {
|
|
84
|
+
flex: 1;
|
|
85
|
+
padding: 15px;
|
|
86
|
+
background: transparent;
|
|
87
|
+
border: none;
|
|
88
|
+
color: #d4d4d4;
|
|
89
|
+
font-family: monospace;
|
|
90
|
+
font-size: 13px;
|
|
91
|
+
resize: none;
|
|
92
|
+
outline: none;
|
|
93
|
+
}
|
|
94
|
+
.btn {
|
|
95
|
+
background: #0e639c;
|
|
96
|
+
color: white;
|
|
97
|
+
border: none;
|
|
98
|
+
padding: 4px 10px;
|
|
99
|
+
border-radius: 3px;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
font-size: 11px;
|
|
102
|
+
font-weight: bold;
|
|
103
|
+
text-transform: uppercase;
|
|
104
|
+
}
|
|
105
|
+
.btn:hover {
|
|
106
|
+
background: #1177bb;
|
|
107
|
+
}
|
|
108
|
+
/* Modal Styles */
|
|
109
|
+
.modal-overlay {
|
|
110
|
+
display: none;
|
|
111
|
+
position: fixed;
|
|
112
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
113
|
+
background: rgba(0,0,0,0.7);
|
|
114
|
+
z-index: 1000;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
align-items: center;
|
|
117
|
+
}
|
|
118
|
+
.modal-content {
|
|
119
|
+
background: #252526;
|
|
120
|
+
border: 1px solid #444;
|
|
121
|
+
width: 60%;
|
|
122
|
+
height: 70%;
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
border-radius: 6px;
|
|
126
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
|
127
|
+
}
|
|
128
|
+
.modal-header {
|
|
129
|
+
padding: 15px;
|
|
130
|
+
border-bottom: 1px solid #444;
|
|
131
|
+
display: flex;
|
|
132
|
+
justify-content: space-between;
|
|
133
|
+
align-items: center;
|
|
134
|
+
}
|
|
135
|
+
.modal-header h2 {
|
|
136
|
+
margin: 0;
|
|
137
|
+
font-size: 16px;
|
|
138
|
+
}
|
|
139
|
+
.close-btn {
|
|
140
|
+
background: transparent;
|
|
141
|
+
border: none;
|
|
142
|
+
color: #ccc;
|
|
143
|
+
font-size: 20px;
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
}
|
|
146
|
+
.close-btn:hover { color: white; }
|
|
147
|
+
.report-area {
|
|
148
|
+
flex: 1;
|
|
149
|
+
padding: 15px;
|
|
150
|
+
background: #1e1e1e;
|
|
151
|
+
color: #d4d4d4;
|
|
152
|
+
border: none;
|
|
153
|
+
font-family: monospace;
|
|
154
|
+
resize: none;
|
|
155
|
+
outline: none;
|
|
156
|
+
}
|
|
157
|
+
.modal-footer {
|
|
158
|
+
padding: 10px 15px;
|
|
159
|
+
border-top: 1px solid #444;
|
|
160
|
+
display: flex;
|
|
161
|
+
justify-content: flex-end;
|
|
162
|
+
}
|
|
163
|
+
</style>
|
|
164
|
+
</head>
|
|
165
|
+
<body>
|
|
166
|
+
<div class="pane" id="leftPane">
|
|
167
|
+
<div class="header">
|
|
168
|
+
Mantine Adapter (Source of Truth)
|
|
169
|
+
<span class="status connected" id="mantineStatus">Connected & Syncing</span>
|
|
170
|
+
</div>
|
|
171
|
+
<!-- Load Mantine via the SAME ORIGIN proxy -->
|
|
172
|
+
<iframe id="mantine" src="/?path=/story/ui-kit-slider--default"></iframe>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="divider" id="vDivider"></div>
|
|
176
|
+
|
|
177
|
+
<div class="pane" id="rightPane">
|
|
178
|
+
<div class="right-top" id="rightTop">
|
|
179
|
+
<div class="header">
|
|
180
|
+
MUI Adapter (Syncing)
|
|
181
|
+
<span class="status connected" id="muiStatus">Ready</span>
|
|
182
|
+
</div>
|
|
183
|
+
<!-- Spacer to align the view with Mantine's Storybook header (40px) -->
|
|
184
|
+
<div style="height: 40px; flex: 0 0 40px; background-color: #fff; border-bottom: 1px solid #e6e6e6; box-sizing: border-box;"></div>
|
|
185
|
+
<!-- Load the bare MUI component preview -->
|
|
186
|
+
<iframe id="mui" src="http://localhost:6012/iframe.html?id=ui-kit-slider--default&viewMode=story"></iframe>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div class="h-divider" id="hDivider"></div>
|
|
190
|
+
|
|
191
|
+
<div class="right-bottom" id="rightBottom">
|
|
192
|
+
<div class="header">
|
|
193
|
+
<span>Component Notes <span id="currentNoteId" style="color:#aaa; text-transform:none; font-weight:normal; margin-left: 10px;"></span></span>
|
|
194
|
+
<button class="btn" id="fullReportBtn">Full Report</button>
|
|
195
|
+
</div>
|
|
196
|
+
<textarea id="notesArea" class="notes-area" placeholder="Take notes on visual differences here... They will be automatically saved locally for this specific component state."></textarea>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<!-- Full Report Modal -->
|
|
201
|
+
<div class="modal-overlay" id="reportModal">
|
|
202
|
+
<div class="modal-content">
|
|
203
|
+
<div class="modal-header">
|
|
204
|
+
<h2>Component Discrepancy Report</h2>
|
|
205
|
+
<button class="close-btn" id="closeModalBtn">×</button>
|
|
206
|
+
</div>
|
|
207
|
+
<textarea id="reportArea" class="report-area" readonly></textarea>
|
|
208
|
+
<div class="modal-footer">
|
|
209
|
+
<button class="btn" id="clearNotesBtn" style="padding: 8px 15px; margin-right: 10px; background-color: #d32f2f;">Clear All Notes</button>
|
|
210
|
+
<button class="btn" id="copyReportBtn" style="padding: 8px 15px;">Copy to Clipboard</button>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<script>
|
|
216
|
+
const muiIframe = document.getElementById('mui');
|
|
217
|
+
const mantineIframe = document.getElementById('mantine');
|
|
218
|
+
const muiStatus = document.getElementById('muiStatus');
|
|
219
|
+
|
|
220
|
+
// Sync initial iframe sources with outer URL if params exist
|
|
221
|
+
const outerParams = new URLSearchParams(window.location.search);
|
|
222
|
+
const initialId = outerParams.get('id');
|
|
223
|
+
const initialArgs = outerParams.get('args');
|
|
224
|
+
if (initialId) {
|
|
225
|
+
let mSrc = `/?path=/story/${initialId}`;
|
|
226
|
+
if (initialArgs) {
|
|
227
|
+
mSrc += `&args=${initialArgs}`;
|
|
228
|
+
}
|
|
229
|
+
mantineIframe.src = mSrc;
|
|
230
|
+
|
|
231
|
+
let uSrc = `http://localhost:6012/iframe.html?id=${initialId}&viewMode=story`;
|
|
232
|
+
if (initialArgs) {
|
|
233
|
+
uSrc += `&args=${initialArgs}`;
|
|
234
|
+
}
|
|
235
|
+
muiIframe.src = uSrc;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Resizers
|
|
239
|
+
const vDivider = document.getElementById('vDivider');
|
|
240
|
+
const leftPane = document.getElementById('leftPane');
|
|
241
|
+
const hDivider = document.getElementById('hDivider');
|
|
242
|
+
const rightTop = document.getElementById('rightTop');
|
|
243
|
+
|
|
244
|
+
// Notes
|
|
245
|
+
const notesArea = document.getElementById('notesArea');
|
|
246
|
+
const currentNoteIdLabel = document.getElementById('currentNoteId');
|
|
247
|
+
let currentStoryId = '';
|
|
248
|
+
|
|
249
|
+
// Modal
|
|
250
|
+
const reportModal = document.getElementById('reportModal');
|
|
251
|
+
const fullReportBtn = document.getElementById('fullReportBtn');
|
|
252
|
+
const closeModalBtn = document.getElementById('closeModalBtn');
|
|
253
|
+
const reportArea = document.getElementById('reportArea');
|
|
254
|
+
const copyReportBtn = document.getElementById('copyReportBtn');
|
|
255
|
+
|
|
256
|
+
// Resizing Logic - Vertical
|
|
257
|
+
let isVResizing = false;
|
|
258
|
+
vDivider.addEventListener('mousedown', (e) => {
|
|
259
|
+
isVResizing = true;
|
|
260
|
+
document.body.style.cursor = 'col-resize';
|
|
261
|
+
mantineIframe.style.pointerEvents = 'none';
|
|
262
|
+
muiIframe.style.pointerEvents = 'none';
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
document.addEventListener('mousemove', (e) => {
|
|
266
|
+
if (isVResizing) {
|
|
267
|
+
const totalWidth = document.body.offsetWidth;
|
|
268
|
+
const percentage = (e.clientX / totalWidth) * 100;
|
|
269
|
+
if (percentage > 20 && percentage < 80) {
|
|
270
|
+
leftPane.style.flex = `0 0 ${percentage}%`;
|
|
271
|
+
}
|
|
272
|
+
} else if (isHResizing) {
|
|
273
|
+
const totalHeight = document.body.offsetHeight;
|
|
274
|
+
const percentage = (e.clientY / totalHeight) * 100;
|
|
275
|
+
if (percentage > 20 && percentage < 90) {
|
|
276
|
+
rightTop.style.flex = `0 0 ${percentage}%`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
document.addEventListener('mouseup', () => {
|
|
282
|
+
if (isVResizing || isHResizing) {
|
|
283
|
+
isVResizing = false;
|
|
284
|
+
isHResizing = false;
|
|
285
|
+
document.body.style.cursor = 'default';
|
|
286
|
+
mantineIframe.style.pointerEvents = 'auto';
|
|
287
|
+
muiIframe.style.pointerEvents = 'auto';
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Resizing Logic - Horizontal
|
|
292
|
+
let isHResizing = false;
|
|
293
|
+
hDivider.addEventListener('mousedown', (e) => {
|
|
294
|
+
isHResizing = true;
|
|
295
|
+
document.body.style.cursor = 'row-resize';
|
|
296
|
+
mantineIframe.style.pointerEvents = 'none';
|
|
297
|
+
muiIframe.style.pointerEvents = 'none';
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Auto-save notes logic
|
|
301
|
+
notesArea.addEventListener('input', (e) => {
|
|
302
|
+
if (currentStoryId) {
|
|
303
|
+
const val = e.target.value;
|
|
304
|
+
if (val.trim() === '') {
|
|
305
|
+
localStorage.removeItem(`notes_${currentStoryId}`);
|
|
306
|
+
} else {
|
|
307
|
+
localStorage.setItem(`notes_${currentStoryId}`, val);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
function loadNotes(storyId) {
|
|
313
|
+
if (currentStoryId !== storyId) {
|
|
314
|
+
currentStoryId = storyId;
|
|
315
|
+
currentNoteIdLabel.textContent = `(${storyId})`;
|
|
316
|
+
const savedNote = localStorage.getItem(`notes_${storyId}`);
|
|
317
|
+
notesArea.value = savedNote || '';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Modal Logic
|
|
322
|
+
const clearNotesBtn = document.getElementById('clearNotesBtn');
|
|
323
|
+
|
|
324
|
+
fullReportBtn.addEventListener('click', async () => {
|
|
325
|
+
let reportText = '';
|
|
326
|
+
try {
|
|
327
|
+
const response = await fetch('/report-header.txt');
|
|
328
|
+
reportText = await response.text();
|
|
329
|
+
} catch (err) {
|
|
330
|
+
reportText = '# UI Component Discrepancy Report\n\n*(Failed to load custom header)*\n\n';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let hasNotes = false;
|
|
334
|
+
|
|
335
|
+
// Iterate through local storage
|
|
336
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
337
|
+
const key = localStorage.key(i);
|
|
338
|
+
if (key && key.startsWith('notes_')) {
|
|
339
|
+
hasNotes = true;
|
|
340
|
+
const configState = key.replace('notes_', '');
|
|
341
|
+
const note = localStorage.getItem(key);
|
|
342
|
+
|
|
343
|
+
// Parse a human readable name from the config string (e.g., ui-kit-slider--default)
|
|
344
|
+
const baseId = configState.split(' | ')[0];
|
|
345
|
+
const parts = baseId.split('--');
|
|
346
|
+
let compName = (parts[0] || '').replace('ui-kit-', '').split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
347
|
+
let storyName = (parts[1] || '').split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
348
|
+
|
|
349
|
+
const humanReadableHeader = `${compName}${storyName ? ` (${storyName})` : ''}`;
|
|
350
|
+
|
|
351
|
+
reportText += `## Component: ${humanReadableHeader}\n**Storybook Config State:** \`${configState}\`\n\n${note}\n\n---\n\n`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!hasNotes) {
|
|
356
|
+
reportText += '*No notes taken yet.*';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
reportArea.value = reportText;
|
|
360
|
+
reportModal.style.display = 'flex';
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
closeModalBtn.addEventListener('click', () => {
|
|
364
|
+
reportModal.style.display = 'none';
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Close modal on click outside
|
|
368
|
+
reportModal.addEventListener('click', (e) => {
|
|
369
|
+
if (e.target === reportModal) {
|
|
370
|
+
reportModal.style.display = 'none';
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
copyReportBtn.addEventListener('click', () => {
|
|
375
|
+
reportArea.select();
|
|
376
|
+
document.execCommand('copy');
|
|
377
|
+
const originalText = copyReportBtn.textContent;
|
|
378
|
+
copyReportBtn.textContent = 'Copied!';
|
|
379
|
+
setTimeout(() => {
|
|
380
|
+
copyReportBtn.textContent = originalText;
|
|
381
|
+
}, 2000);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
clearNotesBtn.addEventListener('click', () => {
|
|
385
|
+
if (confirm('Are you sure you want to delete all your component notes? This cannot be undone.')) {
|
|
386
|
+
const keysToRemove = [];
|
|
387
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
388
|
+
const key = localStorage.key(i);
|
|
389
|
+
if (key && key.startsWith('notes_')) {
|
|
390
|
+
keysToRemove.push(key);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
keysToRemove.forEach(k => localStorage.removeItem(k));
|
|
394
|
+
|
|
395
|
+
notesArea.value = '';
|
|
396
|
+
reportArea.value = '*No notes taken yet.*';
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Same-Origin Polling Syncing
|
|
401
|
+
let lastUrl = '';
|
|
402
|
+
setInterval(() => {
|
|
403
|
+
try {
|
|
404
|
+
const currentUrl = mantineIframe.contentWindow.location.href;
|
|
405
|
+
|
|
406
|
+
if (currentUrl && currentUrl !== lastUrl && currentUrl !== 'about:blank') {
|
|
407
|
+
lastUrl = currentUrl;
|
|
408
|
+
|
|
409
|
+
const url = new URL(currentUrl);
|
|
410
|
+
const args = url.searchParams.get('args');
|
|
411
|
+
const id = url.searchParams.get('id') || url.searchParams.get('path')?.replace('/story/', '');
|
|
412
|
+
|
|
413
|
+
if (id) {
|
|
414
|
+
// Determine the full state string to use as the notes key
|
|
415
|
+
const stateId = args ? `${id} | ${args}` : id;
|
|
416
|
+
loadNotes(stateId);
|
|
417
|
+
|
|
418
|
+
// Sync search parameters to the outer browser URL in the address bar
|
|
419
|
+
const outerUrl = new URL(window.location.href);
|
|
420
|
+
let urlChanged = false;
|
|
421
|
+
if (outerUrl.searchParams.get('id') !== id) {
|
|
422
|
+
outerUrl.searchParams.set('id', id);
|
|
423
|
+
urlChanged = true;
|
|
424
|
+
}
|
|
425
|
+
if (args) {
|
|
426
|
+
if (outerUrl.searchParams.get('args') !== args) {
|
|
427
|
+
outerUrl.searchParams.set('args', args);
|
|
428
|
+
urlChanged = true;
|
|
429
|
+
}
|
|
430
|
+
} else if (outerUrl.searchParams.has('args')) {
|
|
431
|
+
outerUrl.searchParams.delete('args');
|
|
432
|
+
urlChanged = true;
|
|
433
|
+
}
|
|
434
|
+
if (urlChanged) {
|
|
435
|
+
window.history.replaceState({}, '', outerUrl.pathname + outerUrl.search);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let muiUrl = `http://localhost:6012/iframe.html?id=${id}&viewMode=story`;
|
|
439
|
+
if (args) {
|
|
440
|
+
muiUrl += `&args=${args}`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (muiIframe.src !== muiUrl) {
|
|
444
|
+
muiStatus.textContent = 'Updating...';
|
|
445
|
+
muiStatus.classList.remove('connected');
|
|
446
|
+
muiIframe.src = muiUrl;
|
|
447
|
+
|
|
448
|
+
setTimeout(() => {
|
|
449
|
+
muiStatus.textContent = 'Synced';
|
|
450
|
+
muiStatus.classList.add('connected');
|
|
451
|
+
}, 500);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} catch (e) {
|
|
456
|
+
// Ignore cross-origin errors if they briefly occur during navigation
|
|
457
|
+
}
|
|
458
|
+
}, 100);
|
|
459
|
+
</script>
|
|
460
|
+
</body>
|
|
461
|
+
</html>
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { d as E, g as P } from "./config-CKgyDyNF.js";
|
|
2
|
+
import { mkdirSync as m, writeFileSync as t } from "node:fs";
|
|
3
|
+
import { join as o } from "node:path";
|
|
4
|
+
const i = "^8.0.0", s = "^10.3.3", a = "^19.0.0", p = {
|
|
5
|
+
"@storybook/addon-docs": s,
|
|
6
|
+
"@storybook/addon-a11y": s,
|
|
7
|
+
"storybook-dark-mode": "^5.0.0"
|
|
8
|
+
}, u = {
|
|
9
|
+
"react-markdown": "^10.1.0"
|
|
10
|
+
};
|
|
11
|
+
function f(e) {
|
|
12
|
+
return {
|
|
13
|
+
name: "adapter-tester-mantine-source-of-truth-harness",
|
|
14
|
+
private: !0,
|
|
15
|
+
type: "module",
|
|
16
|
+
scripts: {
|
|
17
|
+
storybook: `storybook dev -p ${e.port}`
|
|
18
|
+
},
|
|
19
|
+
dependencies: {
|
|
20
|
+
"@recursica/mantine-adapter": e.mantineAdapterVersion,
|
|
21
|
+
"@recursica/storybook-template": e.storybookTemplateVersion,
|
|
22
|
+
"@recursica/official-release": "latest",
|
|
23
|
+
"@recursica/adapter-common": "latest",
|
|
24
|
+
"@mantine/core": i,
|
|
25
|
+
"@mantine/dates": i,
|
|
26
|
+
react: a,
|
|
27
|
+
"react-dom": a,
|
|
28
|
+
storybook: s,
|
|
29
|
+
"@storybook/react-vite": s,
|
|
30
|
+
...p,
|
|
31
|
+
...u
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const l = `import { createMainConfig } from "@recursica/storybook-template/main";
|
|
36
|
+
|
|
37
|
+
const config = createMainConfig({
|
|
38
|
+
stories: [
|
|
39
|
+
"../node_modules/@recursica/mantine-adapter/src/**/*.stories.@(js|jsx|mjs|ts|tsx)",
|
|
40
|
+
],
|
|
41
|
+
enableCORS: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// react-docgen-typescript can't resolve a TS project for a config file living
|
|
45
|
+
// in .storybook/ when the component source it's docgen'ing lives three
|
|
46
|
+
// directories down inside node_modules — it throws "Cannot read properties
|
|
47
|
+
// of undefined (reading 'fileExists')", which surfaces as a plain 404 on
|
|
48
|
+
// preview.tsx. Docgen only powers Storybook's Controls/Docs tables, which
|
|
49
|
+
// this harness never renders, so disabling it is a safe workaround (see
|
|
50
|
+
// PROPOSAL-installed-package-harness.md, gap 3).
|
|
51
|
+
config.typescript = { ...config.typescript, reactDocgen: false };
|
|
52
|
+
|
|
53
|
+
export default config;
|
|
54
|
+
`, y = `import type { Preview } from "@storybook/react-vite";
|
|
55
|
+
import { createPreviewConfig } from "@recursica/storybook-template/preview";
|
|
56
|
+
import { MantineProvider } from "@mantine/core";
|
|
57
|
+
import "@mantine/core/styles.css";
|
|
58
|
+
import "@mantine/dates/styles.css";
|
|
59
|
+
import "@recursica/adapter-common/style.css";
|
|
60
|
+
import "@recursica/official-release/recursica_variables_scoped.css";
|
|
61
|
+
import recursicaTokens from "@recursica/official-release/recursica_tokens.json";
|
|
62
|
+
import recursicaBrand from "@recursica/official-release/recursica_brand.json";
|
|
63
|
+
import recursicaUIKit from "@recursica/official-release/recursica_ui-kit.json";
|
|
64
|
+
|
|
65
|
+
const basePreview = createPreviewConfig({
|
|
66
|
+
defaultTheme: "light",
|
|
67
|
+
recursicaTokensJsonPath: recursicaTokens,
|
|
68
|
+
recursicaBrandJsonPath: recursicaBrand,
|
|
69
|
+
recursicaUIKitJsonPath: recursicaUIKit,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const preview: Preview = {
|
|
73
|
+
...basePreview,
|
|
74
|
+
decorators: [
|
|
75
|
+
(Story) => (
|
|
76
|
+
<MantineProvider>
|
|
77
|
+
<Story />
|
|
78
|
+
</MantineProvider>
|
|
79
|
+
),
|
|
80
|
+
...(basePreview.decorators || []),
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default preview;
|
|
85
|
+
`;
|
|
86
|
+
function k(e) {
|
|
87
|
+
const {
|
|
88
|
+
dir: r,
|
|
89
|
+
port: n,
|
|
90
|
+
mantineAdapterVersion: c = "latest",
|
|
91
|
+
storybookTemplateVersion: d = "latest"
|
|
92
|
+
} = e;
|
|
93
|
+
return m(o(r, ".storybook"), { recursive: !0 }), t(
|
|
94
|
+
o(r, "package.json"),
|
|
95
|
+
JSON.stringify(
|
|
96
|
+
f({ mantineAdapterVersion: c, storybookTemplateVersion: d, port: n }),
|
|
97
|
+
null,
|
|
98
|
+
2
|
|
99
|
+
) + `
|
|
100
|
+
`
|
|
101
|
+
), t(o(r, ".storybook/main.ts"), l), t(o(r, ".storybook/preview.tsx"), y), t(o(r, ".gitignore"), `node_modules
|
|
102
|
+
`), r;
|
|
103
|
+
}
|
|
104
|
+
function v(e) {
|
|
105
|
+
const r = k(e);
|
|
106
|
+
return {
|
|
107
|
+
command: "npm install --no-audit --no-fund && npm run storybook",
|
|
108
|
+
port: e.port,
|
|
109
|
+
cwd: r,
|
|
110
|
+
reuseExistingServer: !process.env.CI,
|
|
111
|
+
timeout: 180 * 1e3
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
E as defineAdapterTesterConfig,
|
|
116
|
+
P as getSourceOfTruth,
|
|
117
|
+
v as mantineSourceOfTruthWebServer,
|
|
118
|
+
k as scaffoldMantineSourceOfTruthHarness
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/harness/mantineSourceOfTruth.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Generates a small, throwaway Storybook project that installs\n * `@recursica/mantine-adapter` as a real npm dependency (not a workspace\n * link) and boots a real Storybook from its published `src/**\\/*.stories.tsx`\n * files, using `@recursica/storybook-template`'s exported factories.\n *\n * This lets any repo — including ones that never checked out the Recursica\n * monorepo — run adapter-tester's visual regression suite against Mantine\n * (Recursica's source-of-truth adapter) as one side of the comparison, while\n * the other side is that repo's own already-running local Storybook.\n *\n * See PROPOSAL-installed-package-harness.md for the verified prototype this\n * is built from, and the three upstream gaps it works around.\n */\n\nexport interface MantineSourceOfTruthHarnessOptions {\n /**\n * Directory the harness project is scaffolded into. Regenerated on every\n * call — add it to your .gitignore rather than committing it.\n */\n dir: string;\n /** Port the harness's Storybook dev server boots on. */\n port: number;\n /** npm version/range for @recursica/mantine-adapter. Defaults to \"latest\". */\n mantineAdapterVersion?: string;\n /** npm version/range for @recursica/storybook-template. Defaults to \"latest\". */\n storybookTemplateVersion?: string;\n}\n\nexport interface HarnessWebServerConfig {\n command: string;\n port: number;\n cwd: string;\n reuseExistingServer: boolean;\n timeout: number;\n}\n\n// Peer/dev ranges pinned to what @recursica/mantine-adapter and\n// @recursica/storybook-template themselves require, so the harness can't\n// drift onto an incompatible Mantine or Storybook major version.\nconst MANTINE_CORE_RANGE = \"^8.0.0\";\nconst STORYBOOK_RANGE = \"^10.3.3\";\nconst REACT_RANGE = \"^19.0.0\";\n\n// storybook-template's createMainConfig() defaults its addons list to these\n// three but doesn't declare them as peerDependencies (proposal gap 2) — a\n// harness that skips installing any of them gets a silent \"could not\n// resolve addon\" warning at boot, then a hard runtime crash later when Vite\n// pre-bundles preview.tsx's dependency graph. Installed explicitly here.\nconst DEFAULT_ADDON_DEPENDENCIES = {\n \"@storybook/addon-docs\": STORYBOOK_RANGE,\n \"@storybook/addon-a11y\": STORYBOOK_RANGE,\n \"storybook-dark-mode\": \"^5.0.0\",\n};\n\n// mantine-adapter's Introduction.stories.tsx (Version.tsx/OverStyling.tsx)\n// needs react-markdown, but it's a devDependency there — Storybook-only,\n// never bundled into dist — so an external `npm install` of the published\n// package won't pull it in. The harness boots a real Storybook against\n// src/, so it must provide this itself. Installed explicitly here.\nconst WORKAROUND_DEPENDENCIES = {\n \"react-markdown\": \"^10.1.0\",\n};\n\nfunction harnessPackageJson(options: {\n mantineAdapterVersion: string;\n storybookTemplateVersion: string;\n port: number;\n}) {\n return {\n name: \"adapter-tester-mantine-source-of-truth-harness\",\n private: true,\n type: \"module\",\n scripts: {\n storybook: `storybook dev -p ${options.port}`,\n },\n dependencies: {\n \"@recursica/mantine-adapter\": options.mantineAdapterVersion,\n \"@recursica/storybook-template\": options.storybookTemplateVersion,\n \"@recursica/official-release\": \"latest\",\n \"@recursica/adapter-common\": \"latest\",\n \"@mantine/core\": MANTINE_CORE_RANGE,\n \"@mantine/dates\": MANTINE_CORE_RANGE,\n react: REACT_RANGE,\n \"react-dom\": REACT_RANGE,\n storybook: STORYBOOK_RANGE,\n \"@storybook/react-vite\": STORYBOOK_RANGE,\n ...DEFAULT_ADDON_DEPENDENCIES,\n ...WORKAROUND_DEPENDENCIES,\n },\n };\n}\n\nconst MAIN_TS = `import { createMainConfig } from \"@recursica/storybook-template/main\";\n\nconst config = createMainConfig({\n stories: [\n \"../node_modules/@recursica/mantine-adapter/src/**/*.stories.@(js|jsx|mjs|ts|tsx)\",\n ],\n enableCORS: true,\n});\n\n// react-docgen-typescript can't resolve a TS project for a config file living\n// in .storybook/ when the component source it's docgen'ing lives three\n// directories down inside node_modules — it throws \"Cannot read properties\n// of undefined (reading 'fileExists')\", which surfaces as a plain 404 on\n// preview.tsx. Docgen only powers Storybook's Controls/Docs tables, which\n// this harness never renders, so disabling it is a safe workaround (see\n// PROPOSAL-installed-package-harness.md, gap 3).\nconfig.typescript = { ...config.typescript, reactDocgen: false };\n\nexport default config;\n`;\n\nconst PREVIEW_TSX = `import type { Preview } from \"@storybook/react-vite\";\nimport { createPreviewConfig } from \"@recursica/storybook-template/preview\";\nimport { MantineProvider } from \"@mantine/core\";\nimport \"@mantine/core/styles.css\";\nimport \"@mantine/dates/styles.css\";\nimport \"@recursica/adapter-common/style.css\";\nimport \"@recursica/official-release/recursica_variables_scoped.css\";\nimport recursicaTokens from \"@recursica/official-release/recursica_tokens.json\";\nimport recursicaBrand from \"@recursica/official-release/recursica_brand.json\";\nimport recursicaUIKit from \"@recursica/official-release/recursica_ui-kit.json\";\n\nconst basePreview = createPreviewConfig({\n defaultTheme: \"light\",\n recursicaTokensJsonPath: recursicaTokens,\n recursicaBrandJsonPath: recursicaBrand,\n recursicaUIKitJsonPath: recursicaUIKit,\n});\n\nconst preview: Preview = {\n ...basePreview,\n decorators: [\n (Story) => (\n <MantineProvider>\n <Story />\n </MantineProvider>\n ),\n ...(basePreview.decorators || []),\n ],\n};\n\nexport default preview;\n`;\n\n/** Writes the harness project's files to `options.dir` without booting it. */\nexport function scaffoldMantineSourceOfTruthHarness(\n options: MantineSourceOfTruthHarnessOptions,\n): string {\n const {\n dir,\n port,\n mantineAdapterVersion = \"latest\",\n storybookTemplateVersion = \"latest\",\n } = options;\n\n mkdirSync(join(dir, \".storybook\"), { recursive: true });\n writeFileSync(\n join(dir, \"package.json\"),\n JSON.stringify(\n harnessPackageJson({ mantineAdapterVersion, storybookTemplateVersion, port }),\n null,\n 2,\n ) + \"\\n\",\n );\n writeFileSync(join(dir, \".storybook/main.ts\"), MAIN_TS);\n writeFileSync(join(dir, \".storybook/preview.tsx\"), PREVIEW_TSX);\n writeFileSync(join(dir, \".gitignore\"), \"node_modules\\n\");\n\n return dir;\n}\n\n/**\n * Scaffolds the harness and returns a Playwright `webServer` entry for it.\n * Spread the result directly into `playwright.config.ts`'s `webServer` array.\n */\nexport function mantineSourceOfTruthWebServer(\n options: MantineSourceOfTruthHarnessOptions,\n): HarnessWebServerConfig {\n const dir = scaffoldMantineSourceOfTruthHarness(options);\n return {\n command: \"npm install --no-audit --no-fund && npm run storybook\",\n port: options.port,\n cwd: dir,\n reuseExistingServer: !process.env.CI,\n timeout: 180 * 1000,\n };\n}\n"],"names":["MANTINE_CORE_RANGE","STORYBOOK_RANGE","REACT_RANGE","DEFAULT_ADDON_DEPENDENCIES","WORKAROUND_DEPENDENCIES","harnessPackageJson","options","MAIN_TS","PREVIEW_TSX","scaffoldMantineSourceOfTruthHarness","dir","port","mantineAdapterVersion","storybookTemplateVersion","mkdirSync","join","writeFileSync","mantineSourceOfTruthWebServer"],"mappings":";;;AA2CA,MAAMA,IAAqB,UACrBC,IAAkB,WAClBC,IAAc,WAOdC,IAA6B;AAAA,EACjC,yBAAyBF;AAAA,EACzB,yBAAyBA;AAAA,EACzB,uBAAuB;AACzB,GAOMG,IAA0B;AAAA,EAC9B,kBAAkB;AACpB;AAEA,SAASC,EAAmBC,GAIzB;AACD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,MAAM;AAAA,IACN,SAAS;AAAA,MACP,WAAW,oBAAoBA,EAAQ,IAAI;AAAA,IAAA;AAAA,IAE7C,cAAc;AAAA,MACZ,8BAA8BA,EAAQ;AAAA,MACtC,iCAAiCA,EAAQ;AAAA,MACzC,+BAA+B;AAAA,MAC/B,6BAA6B;AAAA,MAC7B,iBAAiBN;AAAA,MACjB,kBAAkBA;AAAA,MAClB,OAAOE;AAAA,MACP,aAAaA;AAAA,MACb,WAAWD;AAAA,MACX,yBAAyBA;AAAA,MACzB,GAAGE;AAAA,MACH,GAAGC;AAAA,IAAA;AAAA,EACL;AAEJ;AAEA,MAAMG,IAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAqBVC,IAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkCb,SAASC,EACdH,GACQ;AACR,QAAM;AAAA,IACJ,KAAAI;AAAA,IACA,MAAAC;AAAA,IACA,uBAAAC,IAAwB;AAAA,IACxB,0BAAAC,IAA2B;AAAA,EAAA,IACzBP;AAEJ,SAAAQ,EAAUC,EAAKL,GAAK,YAAY,GAAG,EAAE,WAAW,IAAM,GACtDM;AAAA,IACED,EAAKL,GAAK,cAAc;AAAA,IACxB,KAAK;AAAA,MACHL,EAAmB,EAAE,uBAAAO,GAAuB,0BAAAC,GAA0B,MAAAF,GAAM;AAAA,MAC5E;AAAA,MACA;AAAA,IAAA,IACE;AAAA;AAAA,EAAA,GAENK,EAAcD,EAAKL,GAAK,oBAAoB,GAAGH,CAAO,GACtDS,EAAcD,EAAKL,GAAK,wBAAwB,GAAGF,CAAW,GAC9DQ,EAAcD,EAAKL,GAAK,YAAY,GAAG;AAAA,CAAgB,GAEhDA;AACT;AAMO,SAASO,EACdX,GACwB;AACxB,QAAMI,IAAMD,EAAoCH,CAAO;AACvD,SAAO;AAAA,IACL,SAAS;AAAA,IACT,MAAMA,EAAQ;AAAA,IACd,KAAKI;AAAA,IACL,qBAAqB,CAAC,QAAQ,IAAI;AAAA,IAClC,SAAS,MAAM;AAAA,EAAA;AAEnB;"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AdapterTesterConfig } from '../config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Defines a Playwright suite that diffs every target in `config.targets`
|
|
4
|
+
* against `config.targets`' single `sourceOfTruth` entry, story by story.
|
|
5
|
+
* Call this with a top-level `await` from a Playwright `*.spec.ts` file — it
|
|
6
|
+
* calls `test.describe` at module scope, so it must run inside Playwright's
|
|
7
|
+
* test runner during test-graph compilation.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runVisualRegression(config: AdapterTesterConfig): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=runVisualRegression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runVisualRegression.d.ts","sourceRoot":"","sources":["../../src/testing/runVisualRegression.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAiB,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAyEvE;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAkJf"}
|
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const b=require("@playwright/test"),q=require("pixelmatch"),u=require("pngjs"),h=require("node:fs"),J=require("./config-B0Eop8Az.cjs"),_=["Theme","Tokens","Introduction"],v=["color","background-color","padding-top","padding-right","padding-bottom","padding-left","margin-top","margin-right","margin-bottom","margin-left","border-top-left-radius","border-top-right-radius","border-bottom-left-radius","border-bottom-right-radius","font-size","font-weight","display","flex-direction","gap","height"];function D(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"-")}async function G(t,r){let c;try{const i=await fetch(`${t.url}/index.json`);if(!i.ok)throw new Error(`Failed to fetch Storybook index: ${i.statusText}`);const g=(await i.json()).entries||{};c=Object.values(g).filter(a=>a.type==="story"&&!r.some(e=>a.title===e||a.title.startsWith(`${e}/`))),c.sort((a,e)=>a.id.localeCompare(e.id))}catch(i){throw console.error("Failed to load Storybook index from",`${t.url}/index.json`,i),new Error(`Storybook source of truth "${t.name}" is not responsive or index.json is missing. Please ensure its Storybook is running.`)}return c}async function R(t){const r=J.getSourceOfTruth(t),c=t.targets.filter(e=>!e.sourceOfTruth),i=t.excludeTitlePrefixes??_,P=t.relaxedThresholdStoryIds??[],g=t.relaxedThresholdPixels,a=await G(r,i);for(const e of c)b.test.describe(`${r.name} vs ${e.name} — Dynamic Visual Regression Suite`,()=>{for(const o of a)b.test(`Visual regression for: ${o.title} - ${o.name} (${o.id})`,async({browser:$},l)=>{const s=await $.newPage(),n=await $.newPage();await s.setViewportSize({width:800,height:600}),await n.setViewportSize({width:800,height:600}),await Promise.all([s.goto(`${r.url}/iframe.html?id=${o.id}&viewMode=story`,{waitUntil:"networkidle"}),n.goto(`${e.url}/iframe.html?id=${o.id}&viewMode=story`,{waitUntil:"networkidle"})]),await Promise.all([s.waitForSelector("#storybook-root"),n.waitForSelector("#storybook-root")]),await Promise.all([s.waitForTimeout(300),n.waitForTimeout(300)]);const x=p=>{function j(S){const T={tag:S.tagName.toLowerCase(),styles:{},children:[]},L=window.getComputedStyle(S);for(const w of p)T.styles[w]=L.getPropertyValue(w);for(const w of Array.from(S.children))T.children.push(j(w));return T}const M=document.querySelector("#storybook-root");return M?j(M):null},O=await s.evaluate(x,v),F=await n.evaluate(x,v);await l.attach(`${r.name} DOM Tree JSON`,{body:JSON.stringify(O,null,2),contentType:"application/json"}),await l.attach(`${e.name} DOM Tree JSON`,{body:JSON.stringify(F,null,2),contentType:"application/json"});const N=D(r.name),k=D(e.name);h.writeFileSync(`/tmp/${o.id}-${N}DOM.json`,JSON.stringify(O,null,2)),h.writeFileSync(`/tmp/${o.id}-${k}DOM.json`,JSON.stringify(F,null,2));const m=await s.screenshot(),y=await n.screenshot(),d=u.PNG.sync.read(m),E=u.PNG.sync.read(y),f=new u.PNG({width:d.width,height:d.height}),V=q(d.data,E.data,f.data,d.width,d.height,{threshold:.1});h.writeFileSync(`/tmp/${o.id}-diff.png`,u.PNG.sync.write(f)),h.writeFileSync(`/tmp/${o.id}-${N}.png`,m),h.writeFileSync(`/tmp/${o.id}-${k}.png`,y),await l.attach("Visual Diff Overlay",{body:u.PNG.sync.write(f),contentType:"image/png"}),await l.attach(`${r.name} Screenshot (Source of Truth)`,{body:m,contentType:"image/png"}),await l.attach(`${e.name} Screenshot (Target)`,{body:y,contentType:"image/png"});const C=g!==void 0&&P.some(p=>o.id.startsWith(p))?g:t.diffThresholdPixels;b.expect.soft(V).toBeLessThan(C)})})}exports.runVisualRegression=R;
|
|
2
|
+
//# sourceMappingURL=testing.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.cjs","sources":["../src/testing/runVisualRegression.ts"],"sourcesContent":["import { test, expect } from \"@playwright/test\";\nimport pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\nimport fs from \"node:fs\";\nimport type { AdapterTarget, AdapterTesterConfig } from \"../config.js\";\nimport { getSourceOfTruth } from \"../config.js\";\n\nconst DEFAULT_EXCLUDE_TITLE_PREFIXES = [\"Theme\", \"Tokens\", \"Introduction\"];\n\nconst COMMON_STYLES_TO_CAPTURE = [\n \"color\",\n \"background-color\",\n \"padding-top\",\n \"padding-right\",\n \"padding-bottom\",\n \"padding-left\",\n \"margin-top\",\n \"margin-right\",\n \"margin-bottom\",\n \"margin-left\",\n \"border-top-left-radius\",\n \"border-top-right-radius\",\n \"border-bottom-left-radius\",\n \"border-bottom-right-radius\",\n \"font-size\",\n \"font-weight\",\n \"display\",\n \"flex-direction\",\n \"gap\",\n \"height\",\n];\n\ninterface StorybookEntry {\n type: string;\n id: string;\n name: string;\n title: string;\n}\n\nfunction slug(name: string): string {\n return name.toLowerCase().replace(/[^a-z0-9]+/g, \"-\");\n}\n\nasync function fetchStories(\n sourceOfTruth: AdapterTarget,\n excludeTitlePrefixes: string[],\n): Promise<StorybookEntry[]> {\n let stories: StorybookEntry[];\n try {\n const response = await fetch(`${sourceOfTruth.url}/index.json`);\n if (!response.ok) {\n throw new Error(`Failed to fetch Storybook index: ${response.statusText}`);\n }\n const data = (await response.json()) as any;\n const entries = data.entries || {};\n stories = Object.values(entries).filter(\n (entry: any) =>\n entry.type === \"story\" &&\n !excludeTitlePrefixes.some(\n (prefix) =>\n entry.title === prefix || entry.title.startsWith(`${prefix}/`),\n ),\n ) as StorybookEntry[];\n stories.sort((a, b) => a.id.localeCompare(b.id));\n } catch (error) {\n console.error(\n \"Failed to load Storybook index from\",\n `${sourceOfTruth.url}/index.json`,\n error,\n );\n throw new Error(\n `Storybook source of truth \"${sourceOfTruth.name}\" is not responsive or index.json is missing. Please ensure its Storybook is running.`,\n );\n }\n return stories;\n}\n\n/**\n * Defines a Playwright suite that diffs every target in `config.targets`\n * against `config.targets`' single `sourceOfTruth` entry, story by story.\n * Call this with a top-level `await` from a Playwright `*.spec.ts` file — it\n * calls `test.describe` at module scope, so it must run inside Playwright's\n * test runner during test-graph compilation.\n */\nexport async function runVisualRegression(\n config: AdapterTesterConfig,\n): Promise<void> {\n const sourceOfTruth = getSourceOfTruth(config);\n const targets = config.targets.filter((target) => !target.sourceOfTruth);\n const excludeTitlePrefixes =\n config.excludeTitlePrefixes ?? DEFAULT_EXCLUDE_TITLE_PREFIXES;\n const relaxedThresholdStoryIds = config.relaxedThresholdStoryIds ?? [];\n const relaxedThresholdPixels = config.relaxedThresholdPixels;\n\n // Fetched once at test-graph compilation time and shared by every target's\n // describe block below.\n const stories = await fetchStories(sourceOfTruth, excludeTitlePrefixes);\n\n for (const target of targets) {\n test.describe(\n `${sourceOfTruth.name} vs ${target.name} — Dynamic Visual Regression Suite`,\n () => {\n for (const story of stories) {\n test(`Visual regression for: ${story.title} - ${story.name} (${story.id})`, async (\n { browser },\n testInfo,\n ) => {\n const sourcePage = await browser.newPage();\n const targetPage = await browser.newPage();\n\n await sourcePage.setViewportSize({ width: 800, height: 600 });\n await targetPage.setViewportSize({ width: 800, height: 600 });\n\n await Promise.all([\n sourcePage.goto(\n `${sourceOfTruth.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n ),\n targetPage.goto(\n `${target.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n ),\n ]);\n\n await Promise.all([\n sourcePage.waitForSelector(\"#storybook-root\"),\n targetPage.waitForSelector(\"#storybook-root\"),\n ]);\n\n await Promise.all([\n sourcePage.waitForTimeout(300),\n targetPage.waitForTimeout(300),\n ]);\n\n // 1. EXTRACT DOM STRUCTURE AND STYLES\n const extractFn = (stylesToCapture: string[]) => {\n function crawl(el: Element): any {\n const data: any = {\n tag: el.tagName.toLowerCase(),\n styles: {},\n children: [],\n };\n const computed = window.getComputedStyle(el);\n for (const prop of stylesToCapture) {\n data.styles[prop] = computed.getPropertyValue(prop);\n }\n for (const child of Array.from(el.children)) {\n data.children.push(crawl(child));\n }\n return data;\n }\n const root = document.querySelector(\"#storybook-root\");\n return root ? crawl(root) : null;\n };\n\n const sourceDOM = await sourcePage.evaluate(\n extractFn,\n COMMON_STYLES_TO_CAPTURE,\n );\n const targetDOM = await targetPage.evaluate(\n extractFn,\n COMMON_STYLES_TO_CAPTURE,\n );\n\n await testInfo.attach(`${sourceOfTruth.name} DOM Tree JSON`, {\n body: JSON.stringify(sourceDOM, null, 2),\n contentType: \"application/json\",\n });\n await testInfo.attach(`${target.name} DOM Tree JSON`, {\n body: JSON.stringify(targetDOM, null, 2),\n contentType: \"application/json\",\n });\n\n const sourceSlug = slug(sourceOfTruth.name);\n const targetSlug = slug(target.name);\n fs.writeFileSync(\n `/tmp/${story.id}-${sourceSlug}DOM.json`,\n JSON.stringify(sourceDOM, null, 2),\n );\n fs.writeFileSync(\n `/tmp/${story.id}-${targetSlug}DOM.json`,\n JSON.stringify(targetDOM, null, 2),\n );\n\n // 2. CAPTURE Headless Viewport Screenshots\n const sourceBuffer = await sourcePage.screenshot();\n const targetBuffer = await targetPage.screenshot();\n\n // 3. PIXEL-BY-PIXEL COMPARISON\n const img1 = PNG.sync.read(sourceBuffer);\n const img2 = PNG.sync.read(targetBuffer);\n const diff = new PNG({ width: img1.width, height: img1.height });\n\n const diffPixels = pixelmatch(\n img1.data,\n img2.data,\n diff.data,\n img1.width,\n img1.height,\n { threshold: 0.1 },\n );\n\n fs.writeFileSync(`/tmp/${story.id}-diff.png`, PNG.sync.write(diff));\n fs.writeFileSync(`/tmp/${story.id}-${sourceSlug}.png`, sourceBuffer);\n fs.writeFileSync(`/tmp/${story.id}-${targetSlug}.png`, targetBuffer);\n\n await testInfo.attach(\"Visual Diff Overlay\", {\n body: PNG.sync.write(diff),\n contentType: \"image/png\",\n });\n await testInfo.attach(`${sourceOfTruth.name} Screenshot (Source of Truth)`, {\n body: sourceBuffer,\n contentType: \"image/png\",\n });\n await testInfo.attach(`${target.name} Screenshot (Target)`, {\n body: targetBuffer,\n contentType: \"image/png\",\n });\n\n const threshold =\n relaxedThresholdPixels !== undefined &&\n relaxedThresholdStoryIds.some((id) => story.id.startsWith(id))\n ? relaxedThresholdPixels\n : config.diffThresholdPixels;\n\n // Perform soft assertions so failures are logged but other stories continue\n expect.soft(diffPixels).toBeLessThan(threshold);\n });\n }\n },\n );\n }\n}\n"],"names":["DEFAULT_EXCLUDE_TITLE_PREFIXES","COMMON_STYLES_TO_CAPTURE","slug","name","fetchStories","sourceOfTruth","excludeTitlePrefixes","stories","response","entries","entry","prefix","b","error","runVisualRegression","config","getSourceOfTruth","targets","target","relaxedThresholdStoryIds","relaxedThresholdPixels","test","story","browser","testInfo","sourcePage","targetPage","extractFn","stylesToCapture","crawl","el","data","computed","prop","child","root","sourceDOM","targetDOM","sourceSlug","targetSlug","fs","sourceBuffer","targetBuffer","img1","PNG","img2","diff","diffPixels","pixelmatch","threshold","id","expect"],"mappings":"uNAOMA,EAAiC,CAAC,QAAS,SAAU,cAAc,EAEnEC,EAA2B,CAC/B,QACA,mBACA,cACA,gBACA,iBACA,eACA,aACA,eACA,gBACA,cACA,yBACA,0BACA,4BACA,6BACA,YACA,cACA,UACA,iBACA,MACA,QACF,EASA,SAASC,EAAKC,EAAsB,CAClC,OAAOA,EAAK,YAAA,EAAc,QAAQ,cAAe,GAAG,CACtD,CAEA,eAAeC,EACbC,EACAC,EAC2B,CAC3B,IAAIC,EACJ,GAAI,CACF,MAAMC,EAAW,MAAM,MAAM,GAAGH,EAAc,GAAG,aAAa,EAC9D,GAAI,CAACG,EAAS,GACZ,MAAM,IAAI,MAAM,oCAAoCA,EAAS,UAAU,EAAE,EAG3E,MAAMC,GADQ,MAAMD,EAAS,KAAA,GACR,SAAW,CAAA,EAChCD,EAAU,OAAO,OAAOE,CAAO,EAAE,OAC9BC,GACCA,EAAM,OAAS,SACf,CAACJ,EAAqB,KACnBK,GACCD,EAAM,QAAUC,GAAUD,EAAM,MAAM,WAAW,GAAGC,CAAM,GAAG,CAAA,CACjE,EAEJJ,EAAQ,KAAK,CAAC,EAAGK,IAAM,EAAE,GAAG,cAAcA,EAAE,EAAE,CAAC,CACjD,OAASC,EAAO,CACd,cAAQ,MACN,sCACA,GAAGR,EAAc,GAAG,cACpBQ,CAAA,EAEI,IAAI,MACR,8BAA8BR,EAAc,IAAI,uFAAA,CAEpD,CACA,OAAOE,CACT,CASA,eAAsBO,EACpBC,EACe,CACf,MAAMV,EAAgBW,EAAAA,iBAAiBD,CAAM,EACvCE,EAAUF,EAAO,QAAQ,OAAQG,GAAW,CAACA,EAAO,aAAa,EACjEZ,EACJS,EAAO,sBAAwBf,EAC3BmB,EAA2BJ,EAAO,0BAA4B,CAAA,EAC9DK,EAAyBL,EAAO,uBAIhCR,EAAU,MAAMH,EAAaC,EAAeC,CAAoB,EAEtE,UAAWY,KAAUD,EACnBI,EAAAA,KAAK,SACH,GAAGhB,EAAc,IAAI,OAAOa,EAAO,IAAI,qCACvC,IAAM,CACJ,UAAWI,KAASf,EAClBc,EAAAA,KAAK,0BAA0BC,EAAM,KAAK,MAAMA,EAAM,IAAI,KAAKA,EAAM,EAAE,IAAK,MAC1E,CAAE,QAAAC,CAAA,EACFC,IACG,CACH,MAAMC,EAAa,MAAMF,EAAQ,QAAA,EAC3BG,EAAa,MAAMH,EAAQ,QAAA,EAEjC,MAAME,EAAW,gBAAgB,CAAE,MAAO,IAAK,OAAQ,IAAK,EAC5D,MAAMC,EAAW,gBAAgB,CAAE,MAAO,IAAK,OAAQ,IAAK,EAE5D,MAAM,QAAQ,IAAI,CAChBD,EAAW,KACT,GAAGpB,EAAc,GAAG,mBAAmBiB,EAAM,EAAE,kBAC/C,CAAE,UAAW,aAAA,CAAc,EAE7BI,EAAW,KACT,GAAGR,EAAO,GAAG,mBAAmBI,EAAM,EAAE,kBACxC,CAAE,UAAW,aAAA,CAAc,CAC7B,CACD,EAED,MAAM,QAAQ,IAAI,CAChBG,EAAW,gBAAgB,iBAAiB,EAC5CC,EAAW,gBAAgB,iBAAiB,CAAA,CAC7C,EAED,MAAM,QAAQ,IAAI,CAChBD,EAAW,eAAe,GAAG,EAC7BC,EAAW,eAAe,GAAG,CAAA,CAC9B,EAGD,MAAMC,EAAaC,GAA8B,CAC/C,SAASC,EAAMC,EAAkB,CAC/B,MAAMC,EAAY,CAChB,IAAKD,EAAG,QAAQ,YAAA,EAChB,OAAQ,CAAA,EACR,SAAU,CAAA,CAAC,EAEPE,EAAW,OAAO,iBAAiBF,CAAE,EAC3C,UAAWG,KAAQL,EACjBG,EAAK,OAAOE,CAAI,EAAID,EAAS,iBAAiBC,CAAI,EAEpD,UAAWC,KAAS,MAAM,KAAKJ,EAAG,QAAQ,EACxCC,EAAK,SAAS,KAAKF,EAAMK,CAAK,CAAC,EAEjC,OAAOH,CACT,CACA,MAAMI,EAAO,SAAS,cAAc,iBAAiB,EACrD,OAAOA,EAAON,EAAMM,CAAI,EAAI,IAC9B,EAEMC,EAAY,MAAMX,EAAW,SACjCE,EACA1B,CAAA,EAEIoC,EAAY,MAAMX,EAAW,SACjCC,EACA1B,CAAA,EAGF,MAAMuB,EAAS,OAAO,GAAGnB,EAAc,IAAI,iBAAkB,CAC3D,KAAM,KAAK,UAAU+B,EAAW,KAAM,CAAC,EACvC,YAAa,kBAAA,CACd,EACD,MAAMZ,EAAS,OAAO,GAAGN,EAAO,IAAI,iBAAkB,CACpD,KAAM,KAAK,UAAUmB,EAAW,KAAM,CAAC,EACvC,YAAa,kBAAA,CACd,EAED,MAAMC,EAAapC,EAAKG,EAAc,IAAI,EACpCkC,EAAarC,EAAKgB,EAAO,IAAI,EACnCsB,EAAG,cACD,QAAQlB,EAAM,EAAE,IAAIgB,CAAU,WAC9B,KAAK,UAAUF,EAAW,KAAM,CAAC,CAAA,EAEnCI,EAAG,cACD,QAAQlB,EAAM,EAAE,IAAIiB,CAAU,WAC9B,KAAK,UAAUF,EAAW,KAAM,CAAC,CAAA,EAInC,MAAMI,EAAe,MAAMhB,EAAW,WAAA,EAChCiB,EAAe,MAAMhB,EAAW,WAAA,EAGhCiB,EAAOC,EAAAA,IAAI,KAAK,KAAKH,CAAY,EACjCI,EAAOD,EAAAA,IAAI,KAAK,KAAKF,CAAY,EACjCI,EAAO,IAAIF,EAAAA,IAAI,CAAE,MAAOD,EAAK,MAAO,OAAQA,EAAK,OAAQ,EAEzDI,EAAaC,EACjBL,EAAK,KACLE,EAAK,KACLC,EAAK,KACLH,EAAK,MACLA,EAAK,OACL,CAAE,UAAW,EAAA,CAAI,EAGnBH,EAAG,cAAc,QAAQlB,EAAM,EAAE,YAAasB,MAAI,KAAK,MAAME,CAAI,CAAC,EAClEN,EAAG,cAAc,QAAQlB,EAAM,EAAE,IAAIgB,CAAU,OAAQG,CAAY,EACnED,EAAG,cAAc,QAAQlB,EAAM,EAAE,IAAIiB,CAAU,OAAQG,CAAY,EAEnE,MAAMlB,EAAS,OAAO,sBAAuB,CAC3C,KAAMoB,EAAAA,IAAI,KAAK,MAAME,CAAI,EACzB,YAAa,WAAA,CACd,EACD,MAAMtB,EAAS,OAAO,GAAGnB,EAAc,IAAI,gCAAiC,CAC1E,KAAMoC,EACN,YAAa,WAAA,CACd,EACD,MAAMjB,EAAS,OAAO,GAAGN,EAAO,IAAI,uBAAwB,CAC1D,KAAMwB,EACN,YAAa,WAAA,CACd,EAED,MAAMO,EACJ7B,IAA2B,QAC3BD,EAAyB,KAAM+B,GAAO5B,EAAM,GAAG,WAAW4B,CAAE,CAAC,EACzD9B,EACAL,EAAO,oBAGboC,EAAAA,OAAO,KAAKJ,CAAU,EAAE,aAAaE,CAAS,CAChD,CAAC,CAEL,CAAA,CAGN"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC"}
|