@capillarytech/cap-ui-dev-tools 1.0.0 → 1.3.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/CAPVISION_USAGE.md +488 -0
- package/README.md +199 -48
- package/package.json +39 -10
- package/src/LibraryWatcherPlugin.js +53 -0
- package/src/capvision-recorder/adapters/WebdriverIOAdapter.js +312 -0
- package/src/capvision-recorder/assets/capvision-player.css +1 -0
- package/src/capvision-recorder/assets/capvision-player.min.js +31 -0
- package/src/capvision-recorder/assets/capvision-plugins/console-record.min.js +93 -0
- package/src/capvision-recorder/assets/capvision-plugins/console-replay.min.js +85 -0
- package/src/capvision-recorder/assets/capvision-plugins/network-record.min.js +542 -0
- package/src/capvision-recorder/assets/capvision-plugins/network-replay.min.js +434 -0
- package/src/capvision-recorder/assets/capvision.min.js +19 -0
- package/src/capvision-recorder/core/CapVisionRecorder.js +1338 -0
- package/src/capvision-recorder/core/ReportEnhancer.js +755 -0
- package/src/capvision-recorder/index.js +58 -0
- package/src/index.js +34 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Report Enhancer Configuration
|
|
6
|
+
* @typedef {Object} ReportEnhancerConfig
|
|
7
|
+
* @property {boolean} [ENABLE_FEATURE] - Master feature flag - if false, replay enhancement is completely disabled (default: true)
|
|
8
|
+
* @property {string} [recordingsDir] - Directory where recordings are stored
|
|
9
|
+
* @property {string} [reportsDir] - Directory where HTML reports are located
|
|
10
|
+
* @property {string} [playerCSSPath] - Path to CapVision player CSS file
|
|
11
|
+
* @property {string} [playerScriptPath] - Path to CapVision player script file
|
|
12
|
+
* @property {string} [consoleReplayPluginPath] - Path to console replay plugin
|
|
13
|
+
* @property {string} [networkReplayPluginPath] - Path to network replay plugin
|
|
14
|
+
* @property {number} [playerWidth] - Player width in pixels
|
|
15
|
+
* @property {number} [playerHeight] - Player height in pixels
|
|
16
|
+
* @property {boolean} [autoPlay] - Auto-play recordings on load
|
|
17
|
+
* @property {string[]} [enabledClusters] - Only enhance for these clusters (empty = all)
|
|
18
|
+
* @property {string[]} [enabledModules] - Only enhance for these modules (empty = all)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Recording Data Interface
|
|
23
|
+
* @typedef {Object} RecordingData
|
|
24
|
+
* @property {string} filename - Recording filename
|
|
25
|
+
* @property {Object} data - Recording data object
|
|
26
|
+
* @property {string} data.testName - Test name
|
|
27
|
+
* @property {number} data.recordingNumber - Recording number/sequence
|
|
28
|
+
* @property {string} data.timestamp - ISO timestamp
|
|
29
|
+
* @property {number} data.totalEvents - Total events in recording
|
|
30
|
+
* @property {Array} data.events - Array of CapVision events
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Default Configuration
|
|
35
|
+
* @type {ReportEnhancerConfig}
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_ENHANCER_CONFIG = {
|
|
38
|
+
ENABLE_FEATURE: true,
|
|
39
|
+
recordingsDir: path.join(process.cwd(), 'reports', 'recordings'),
|
|
40
|
+
reportsDir: path.join(process.cwd(), 'reports', 'html-reports'),
|
|
41
|
+
playerCSSPath: path.join(__dirname, '../assets/capvision-player.css'),
|
|
42
|
+
playerScriptPath: path.join(__dirname, '../assets/capvision-player.min.js'),
|
|
43
|
+
consoleReplayPluginPath: path.join(__dirname, '../assets/capvision-plugins/console-replay.min.js'),
|
|
44
|
+
networkReplayPluginPath: path.join(__dirname, '../assets/capvision-plugins/network-replay.min.js'),
|
|
45
|
+
playerWidth: 800,
|
|
46
|
+
playerHeight: 460,
|
|
47
|
+
autoPlay: false,
|
|
48
|
+
enabledClusters: [],
|
|
49
|
+
enabledModules: []
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Report Enhancer Class
|
|
54
|
+
* Enhances HTML reports with CapVision player functionality
|
|
55
|
+
* @class
|
|
56
|
+
*/
|
|
57
|
+
class ReportEnhancer {
|
|
58
|
+
/**
|
|
59
|
+
* Create a new Report Enhancer instance
|
|
60
|
+
* @param {ReportEnhancerConfig} [config={}] - Enhancer configuration
|
|
61
|
+
*/
|
|
62
|
+
constructor(config = {}) {
|
|
63
|
+
/** @type {ReportEnhancerConfig} */
|
|
64
|
+
this.config = { ...DEFAULT_ENHANCER_CONFIG, ...config };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Update configuration
|
|
69
|
+
* @param {Partial<ReportEnhancerConfig>} config - Configuration updates
|
|
70
|
+
*/
|
|
71
|
+
updateConfig(config) {
|
|
72
|
+
this.config = { ...this.config, ...config };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get current configuration
|
|
77
|
+
* @returns {ReportEnhancerConfig} Current configuration object
|
|
78
|
+
*/
|
|
79
|
+
getConfig() {
|
|
80
|
+
return { ...this.config };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if enhancement should be enabled for current cluster
|
|
85
|
+
* @private
|
|
86
|
+
* @param {string} [cluster] - Override cluster (defaults to process.env.cluster)
|
|
87
|
+
* @returns {boolean} True if enabled for cluster
|
|
88
|
+
*/
|
|
89
|
+
isEnabledForCluster(cluster) {
|
|
90
|
+
if (this.config.enabledClusters.length === 0) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const currentCluster = cluster || process.env.cluster;
|
|
95
|
+
return currentCluster ? this.config.enabledClusters.includes(currentCluster) : false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if enhancement should be enabled for current module
|
|
100
|
+
* @private
|
|
101
|
+
* @param {string} [module] - Override module (defaults to process.env.module)
|
|
102
|
+
* @returns {boolean} True if enabled for module
|
|
103
|
+
*/
|
|
104
|
+
isEnabledForModule(module) {
|
|
105
|
+
if (this.config.enabledModules.length === 0) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const currentModule = module || process.env.module;
|
|
110
|
+
return currentModule ? this.config.enabledModules.includes(currentModule) : false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if enhancement is enabled
|
|
115
|
+
* @param {string} [cluster] - Override cluster check
|
|
116
|
+
* @param {string} [module] - Override module check
|
|
117
|
+
* @returns {boolean} True if enhancement enabled
|
|
118
|
+
*/
|
|
119
|
+
isEnhancementEnabled(cluster, module) {
|
|
120
|
+
return this.isEnabledForCluster(cluster) && this.isEnabledForModule(module);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get available recordings
|
|
125
|
+
* @private
|
|
126
|
+
* @returns {RecordingData[]} Array of recording data objects
|
|
127
|
+
*/
|
|
128
|
+
getAvailableRecordings() {
|
|
129
|
+
try {
|
|
130
|
+
if (!fs.existsSync(this.config.recordingsDir)) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const files = fs.readdirSync(this.config.recordingsDir);
|
|
135
|
+
const recordingFiles = files.filter(file => file.endsWith('.json'));
|
|
136
|
+
|
|
137
|
+
const recordings = [];
|
|
138
|
+
for (const file of recordingFiles) {
|
|
139
|
+
try {
|
|
140
|
+
const filePath = path.join(this.config.recordingsDir, file);
|
|
141
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
142
|
+
recordings.push({
|
|
143
|
+
filename: file,
|
|
144
|
+
data: data
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(`🔴 Failed to read recording file ${file}:`, error.message);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return recordings;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('🔴 Failed to get recordings:', error.message);
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate logo HTML header with toggle button
|
|
160
|
+
* @private
|
|
161
|
+
* @returns {string} Logo HTML
|
|
162
|
+
*/
|
|
163
|
+
generateLogoHTML() {
|
|
164
|
+
// Inline SVG logo - CapVision branded logo (icon size)
|
|
165
|
+
const logoSVG = `
|
|
166
|
+
<svg width="120" height="80" viewBox="0 0 1600 800" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: inline-block; vertical-align: middle;">
|
|
167
|
+
<defs>
|
|
168
|
+
<!-- Neon gradient used for strokes/fills -->
|
|
169
|
+
<linearGradient id="neonGradient" x1="0" x2="1" y1="0" y2="1">
|
|
170
|
+
<stop offset="0%" stop-color="#00D4FF"/>
|
|
171
|
+
<stop offset="25%" stop-color="#3A7CFF"/>
|
|
172
|
+
<stop offset="50%" stop-color="#9A4BFF"/>
|
|
173
|
+
<stop offset="70%" stop-color="#FF4CA3"/>
|
|
174
|
+
<stop offset="85%" stop-color="#FF8E3C"/>
|
|
175
|
+
<stop offset="100%" stop-color="#FFD800"/>
|
|
176
|
+
</linearGradient>
|
|
177
|
+
|
|
178
|
+
<!-- Wordmark gradient (cool blue) -->
|
|
179
|
+
<linearGradient id="wordGradient" x1="0" x2="1">
|
|
180
|
+
<stop offset="0%" stop-color="#19C6FF"/>
|
|
181
|
+
<stop offset="100%" stop-color="#2EA6FF"/>
|
|
182
|
+
</linearGradient>
|
|
183
|
+
|
|
184
|
+
<!-- Soft inner shadow for icon fill -->
|
|
185
|
+
<linearGradient id="iconFill" x1="0" x2="1">
|
|
186
|
+
<stop offset="0%" stop-color="#061022" stop-opacity="0.0"/>
|
|
187
|
+
<stop offset="100%" stop-color="#061022" stop-opacity="0.08"/>
|
|
188
|
+
</linearGradient>
|
|
189
|
+
|
|
190
|
+
<!-- Stroke style for neon lines -->
|
|
191
|
+
<style type="text/css"><![CDATA[
|
|
192
|
+
.neon { stroke: url(#neonGradient); stroke-width:14; stroke-linecap:square; stroke-linejoin:miter; fill: none; }
|
|
193
|
+
.neon-thin { stroke: url(#neonGradient); stroke-width:8; stroke-linecap:square; stroke-linejoin:miter; fill: none; }
|
|
194
|
+
.eye-fill { fill: none; stroke: url(#neonGradient); stroke-width:12; stroke-linecap:square; stroke-linejoin:miter; }
|
|
195
|
+
.circuit { stroke: url(#neonGradient); stroke-width:10; stroke-linecap:square; fill: none; }
|
|
196
|
+
.accent-dot { fill: url(#neonGradient); }
|
|
197
|
+
.word { font-family: "Inter", "Montserrat", sans-serif; font-weight: 700; font-size:120px; fill: url(#wordGradient); letter-spacing: -2px; }
|
|
198
|
+
.bg-texture { opacity: 0.02; }
|
|
199
|
+
]]></style>
|
|
200
|
+
</defs>
|
|
201
|
+
|
|
202
|
+
<!-- Dark background (keeps high contrast for presentation and dashboards) -->
|
|
203
|
+
<rect id="bg" x="0" y="0" width="1600" height="800" fill="#060719" />
|
|
204
|
+
|
|
205
|
+
<!-- subtle grid/texture (very faint) -->
|
|
206
|
+
<g class="bg-texture" transform="translate(0,0)">
|
|
207
|
+
<rect x="0" y="0" width="1600" height="800" fill="url(#iconFill)" />
|
|
208
|
+
</g>
|
|
209
|
+
|
|
210
|
+
<!-- LEFT ICON GROUP -->
|
|
211
|
+
<g id="icon" transform="translate(160,210) scale(1.4)">
|
|
212
|
+
<!-- robot-head-ish rounded rectangle -->
|
|
213
|
+
<rect x="0" y="0" rx="8" ry="8" width="220" height="150" class="neon" />
|
|
214
|
+
|
|
215
|
+
<!-- antenna -->
|
|
216
|
+
<line x1="110" y1="-30" x2="110" y2="0" class="neon-thin" />
|
|
217
|
+
<circle cx="110" cy="-36" r="12" class="accent-dot" />
|
|
218
|
+
|
|
219
|
+
<!-- small scan lines / UI fragments on head (to suggest vision/processing) -->
|
|
220
|
+
<path d="M170,30 h40" class="neon-thin" stroke-linecap="square" />
|
|
221
|
+
<path d="M162,54 h30" class="neon-thin" stroke-linecap="square" />
|
|
222
|
+
<path d="M28,28 h-12" class="neon-thin" />
|
|
223
|
+
<rect x="20" y="20" width="6" height="28" rx="1" class="neon-thin" />
|
|
224
|
+
|
|
225
|
+
<!-- Abstract eye -->
|
|
226
|
+
<g transform="translate(10,50)">
|
|
227
|
+
<!-- eye outline -->
|
|
228
|
+
<path d="M40,60 C20,60 0,44 0,30 C0,16 20,0 40,0 C60,0 80,16 80,30 C80,44 60,60 40,60 Z" class="eye-fill"/>
|
|
229
|
+
<!-- iris concentric rings -->
|
|
230
|
+
<circle cx="40" cy="30" r="16" stroke="url(#neonGradient)" stroke-width="8" fill="none" />
|
|
231
|
+
<circle cx="40" cy="30" r="8" stroke="url(#neonGradient)" stroke-width="6" fill="none" />
|
|
232
|
+
<circle cx="40" cy="30" r="3" fill="#04102A"/>
|
|
233
|
+
|
|
234
|
+
<!-- subtle scan ticks inside eye -->
|
|
235
|
+
<path d="M20,30 h6" class="neon-thin" />
|
|
236
|
+
<path d="M66,30 h6" class="neon-thin" />
|
|
237
|
+
<path d="M40,8 v6" class="neon-thin" />
|
|
238
|
+
<path d="M40,52 v6" class="neon-thin" />
|
|
239
|
+
</g>
|
|
240
|
+
|
|
241
|
+
<!-- Circuit-like legs below eye (AI & automation) -->
|
|
242
|
+
<g transform="translate(15,125)">
|
|
243
|
+
<!-- crossing trace lines -->
|
|
244
|
+
<path d="M20,0 L75,60" class="circuit" />
|
|
245
|
+
<path d="M110,0 L58,60" class="circuit" />
|
|
246
|
+
|
|
247
|
+
<!-- branch nodes -->
|
|
248
|
+
<circle cx="20" cy="0" r="7" class="accent-dot" />
|
|
249
|
+
<circle cx="110" cy="0" r="7" class="accent-dot" />
|
|
250
|
+
|
|
251
|
+
<circle cx="75" cy="60" r="8" class="accent-dot" />
|
|
252
|
+
<circle cx="58" cy="60" r="8" class="accent-dot" />
|
|
253
|
+
|
|
254
|
+
<!-- side traces -->
|
|
255
|
+
<path d="M10,18 L35,38" class="circuit" />
|
|
256
|
+
<path d="M120,18 L95,38" class="circuit" />
|
|
257
|
+
|
|
258
|
+
<circle cx="8" cy="18" r="6" class="accent-dot" />
|
|
259
|
+
<circle cx="122" cy="18" r="6" class="accent-dot" />
|
|
260
|
+
</g>
|
|
261
|
+
</g>
|
|
262
|
+
|
|
263
|
+
<!-- WORDMARK (Right side) -->
|
|
264
|
+
<g id="wordmark" transform="translate(530,360)">
|
|
265
|
+
<!-- Main text -->
|
|
266
|
+
<text class="word" x="0" y="30">CapVision</text>
|
|
267
|
+
|
|
268
|
+
<!-- subtle tech underline / focus ring hint under 'Vision' -->
|
|
269
|
+
<path d="M210,52 h160" stroke="url(#wordGradient)" stroke-width="6" stroke-linecap="square" opacity="0.35" />
|
|
270
|
+
<circle cx="360" cy="28" r="10" fill="none" stroke="url(#wordGradient)" stroke-width="4" opacity="0.45" />
|
|
271
|
+
</g>
|
|
272
|
+
|
|
273
|
+
<!-- Accessibility: invisible title/desc -->
|
|
274
|
+
<title>CapVision — cyberpunk AI vision logo</title>
|
|
275
|
+
<desc>Futuristic neon logo composed of an abstract robotic-eye icon and circuit traces left, and a bold CapVision wordmark right. Gradient neon colors and clean strokes create a cyberpunk high-tech look.</desc>
|
|
276
|
+
</svg>
|
|
277
|
+
`.trim();
|
|
278
|
+
|
|
279
|
+
return `
|
|
280
|
+
<tr class="recording-logo-header">
|
|
281
|
+
<td colspan="2">
|
|
282
|
+
<div style="padding: 20px 15px; background: #fff; border-bottom: 2px solid #e9ecef; display: flex; justify-content: space-between; align-items: center;">
|
|
283
|
+
<span id="capvision-toggle-wrapper"
|
|
284
|
+
onclick="toggleCapVisionRecordings()"
|
|
285
|
+
style="cursor: pointer; padding: 10px; user-select: none; background-color: #C8E6C9; border-radius: 3px; display: inline-block; flex: 1; max-width: calc(100% - 150px); margin-right: 20px;">
|
|
286
|
+
<span id="capvision-toggle-icon" class="collapsed" style="font-size: 12px; display: inline-block;"></span>
|
|
287
|
+
</span>
|
|
288
|
+
<div style="text-align: right;">
|
|
289
|
+
${logoSVG}
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</td>
|
|
293
|
+
</tr>`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Generate wrapper HTML for recordings (table structure)
|
|
298
|
+
* @private
|
|
299
|
+
* @param {string} logoHTML - Logo HTML
|
|
300
|
+
* @param {string} recordingPlayersHTML - Recording players HTML
|
|
301
|
+
* @returns {string} Wrapped HTML
|
|
302
|
+
*/
|
|
303
|
+
generateRecordingsWrapper(logoHTML, recordingPlayersHTML) {
|
|
304
|
+
return `
|
|
305
|
+
<table class="table table-bordered" style="margin-top: 20px;">
|
|
306
|
+
<thead>
|
|
307
|
+
${logoHTML}
|
|
308
|
+
</thead>
|
|
309
|
+
<tbody id="capvision-recordings-container" class="collapsed" style="display: table-row-group;">
|
|
310
|
+
${recordingPlayersHTML}
|
|
311
|
+
</tbody>
|
|
312
|
+
</table>
|
|
313
|
+
<style>
|
|
314
|
+
#capvision-recordings-container {
|
|
315
|
+
transition: opacity 0.3s ease-in-out;
|
|
316
|
+
}
|
|
317
|
+
#capvision-recordings-container.collapsed {
|
|
318
|
+
display: none !important;
|
|
319
|
+
}
|
|
320
|
+
#capvision-toggle-wrapper {
|
|
321
|
+
transition: background-color 0.2s ease;
|
|
322
|
+
}
|
|
323
|
+
#capvision-toggle-wrapper:hover {
|
|
324
|
+
background-color: #C8E6C9;
|
|
325
|
+
}
|
|
326
|
+
#capvision-toggle-wrapper:hover #capvision-toggle-icon {
|
|
327
|
+
color: #333;
|
|
328
|
+
}
|
|
329
|
+
#capvision-toggle-icon {
|
|
330
|
+
display: inline-block;
|
|
331
|
+
}
|
|
332
|
+
#capvision-toggle-icon::before {
|
|
333
|
+
font-family: "Glyphicons Halflings", "Font Awesome", Arial, sans-serif;
|
|
334
|
+
color: #333;
|
|
335
|
+
}
|
|
336
|
+
#capvision-toggle-icon.collapsed::before {
|
|
337
|
+
content: "\\e113";
|
|
338
|
+
}
|
|
339
|
+
#capvision-toggle-icon:not(.collapsed)::before {
|
|
340
|
+
content: "\\e114";
|
|
341
|
+
}
|
|
342
|
+
</style>
|
|
343
|
+
<script>
|
|
344
|
+
function toggleCapVisionRecordings() {
|
|
345
|
+
const container = document.getElementById('capvision-recordings-container');
|
|
346
|
+
const toggleIcon = document.getElementById('capvision-toggle-icon');
|
|
347
|
+
|
|
348
|
+
if (container.classList.contains('collapsed')) {
|
|
349
|
+
container.classList.remove('collapsed');
|
|
350
|
+
toggleIcon.classList.remove('collapsed');
|
|
351
|
+
} else {
|
|
352
|
+
container.classList.add('collapsed');
|
|
353
|
+
toggleIcon.classList.add('collapsed');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
</script>`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Add inline recording players to HTML content
|
|
361
|
+
* @private
|
|
362
|
+
* @param {string} htmlContent - Original HTML content
|
|
363
|
+
* @param {RecordingData[]} recordings - Array of recordings to add
|
|
364
|
+
* @returns {string} Modified HTML content with players
|
|
365
|
+
*/
|
|
366
|
+
addInlineRecordingPlayers(htmlContent, recordings) {
|
|
367
|
+
if (recordings.length === 0) {
|
|
368
|
+
return htmlContent;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Find table with class "table table-bordered table-suite"
|
|
372
|
+
const tableSuiteRegex = /<table[^>]*class=["'][^"']*table[^"']*table-bordered[^"']*table-suite[^"']*["'][^>]*>/i;
|
|
373
|
+
const match = htmlContent.match(tableSuiteRegex);
|
|
374
|
+
|
|
375
|
+
if (!match) {
|
|
376
|
+
// Table not found, return original content
|
|
377
|
+
return htmlContent;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const tableStartIndex = match.index;
|
|
381
|
+
const tableStartTag = match[0];
|
|
382
|
+
|
|
383
|
+
// Find the matching closing </table> tag for this table
|
|
384
|
+
// We need to account for nested tables
|
|
385
|
+
let depth = 1;
|
|
386
|
+
let searchIndex = tableStartIndex + tableStartTag.length;
|
|
387
|
+
let closingTableIndex = -1;
|
|
388
|
+
|
|
389
|
+
while (searchIndex < htmlContent.length && depth > 0) {
|
|
390
|
+
const nextOpen = htmlContent.indexOf('<table', searchIndex);
|
|
391
|
+
const nextClose = htmlContent.indexOf('</table>', searchIndex);
|
|
392
|
+
|
|
393
|
+
if (nextClose === -1) {
|
|
394
|
+
break; // No closing tag found
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
398
|
+
depth++;
|
|
399
|
+
searchIndex = nextOpen + 6;
|
|
400
|
+
} else {
|
|
401
|
+
depth--;
|
|
402
|
+
if (depth === 0) {
|
|
403
|
+
closingTableIndex = nextClose;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
searchIndex = nextClose + 8;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (closingTableIndex === -1) {
|
|
411
|
+
// If we can't find matching closing tag, return original content
|
|
412
|
+
return htmlContent;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Generate logo and players HTML
|
|
416
|
+
const logoHTML = this.generateLogoHTML();
|
|
417
|
+
const recordingPlayersHTML = recordings.map((recording, index) => {
|
|
418
|
+
const testName = recording.data.testName || 'Unknown Test';
|
|
419
|
+
const recordingNumber = recording.data.recordingNumber || (index + 1);
|
|
420
|
+
const displayName = recording.data.testName
|
|
421
|
+
? `${testName} (Recording #${recordingNumber})`
|
|
422
|
+
: recording.filename;
|
|
423
|
+
|
|
424
|
+
return `
|
|
425
|
+
<tr class="test-row recording">
|
|
426
|
+
<td colspan="2">
|
|
427
|
+
<div class="recordingWrapper" style="padding: 15px; background: #f8f9fa; margin: 10px 0;">
|
|
428
|
+
<h4 style="margin: 0 0 10px 0; color: #007bff; font-size: 16px;">
|
|
429
|
+
🎬 Session Recording: ${displayName}
|
|
430
|
+
</h4>
|
|
431
|
+
<div class="recording-info" style="margin-bottom: 10px; font-size: 12px; color: #666;">
|
|
432
|
+
Events: ${recording.data.totalEvents} | Timestamp: ${recording.data.timestamp} | File: ${recording.filename}
|
|
433
|
+
</div>
|
|
434
|
+
<div id="capvision-player-inline-${index}" class="capvision-player-inline" style="width: 100%; max-width: ${this.config.playerWidth + 200}px; height: ${this.config.playerHeight + 100}px; border: 1px solid #ddd; border-radius: 8px; background: #fff;">
|
|
435
|
+
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666;">
|
|
436
|
+
Loading recording player...
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</td>
|
|
441
|
+
</tr>`;
|
|
442
|
+
}).join('');
|
|
443
|
+
|
|
444
|
+
// Wrap logo and players in a new table and inject after the closing </table> tag
|
|
445
|
+
const recordingsWrapper = this.generateRecordingsWrapper(logoHTML, recordingPlayersHTML);
|
|
446
|
+
const insertPosition = closingTableIndex + 8; // 8 = length of '</table>'
|
|
447
|
+
|
|
448
|
+
return htmlContent.substring(0, insertPosition) +
|
|
449
|
+
recordingsWrapper +
|
|
450
|
+
htmlContent.substring(insertPosition);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Generate CapVision player HTML and JavaScript
|
|
455
|
+
* @private
|
|
456
|
+
* @param {RecordingData[]} recordings - Array of recordings to embed
|
|
457
|
+
* @returns {string} HTML/JavaScript string to inject
|
|
458
|
+
*/
|
|
459
|
+
generateCapVisionPlayerHTML(recordings) {
|
|
460
|
+
// Read bundled assets
|
|
461
|
+
let playerCSS = '';
|
|
462
|
+
try {
|
|
463
|
+
playerCSS = fs.readFileSync(this.config.playerCSSPath, 'utf8');
|
|
464
|
+
} catch (error) {
|
|
465
|
+
console.warn('⚠️ Failed to load CapVision player CSS:', error.message);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let playerScript = '';
|
|
469
|
+
try {
|
|
470
|
+
playerScript = fs.readFileSync(this.config.playerScriptPath, 'utf8');
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.warn('⚠️ Failed to load CapVision player script:', error.message);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let consoleReplayPlugin = '';
|
|
476
|
+
try {
|
|
477
|
+
consoleReplayPlugin = fs.readFileSync(this.config.consoleReplayPluginPath, 'utf8');
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.warn('⚠️ Failed to load console replay plugin:', error.message);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Read bundled network replay plugin
|
|
483
|
+
let networkReplayPlugin = '';
|
|
484
|
+
try {
|
|
485
|
+
networkReplayPlugin = fs.readFileSync(this.config.networkReplayPluginPath, 'utf8');
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.warn('⚠️ Failed to load network replay plugin:', error.message);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return `
|
|
491
|
+
<!-- CapVision Player CSS (Bundled) -->
|
|
492
|
+
<style>
|
|
493
|
+
${playerCSS}
|
|
494
|
+
</style>
|
|
495
|
+
|
|
496
|
+
<!-- CapVision Player Script (Bundled) -->
|
|
497
|
+
<script>
|
|
498
|
+
${playerScript}
|
|
499
|
+
</script>
|
|
500
|
+
|
|
501
|
+
<!-- Console Replay Plugin (Bundled) -->
|
|
502
|
+
<script>
|
|
503
|
+
${consoleReplayPlugin}
|
|
504
|
+
</script>
|
|
505
|
+
|
|
506
|
+
<!-- Network Replay Plugin (Bundled) -->
|
|
507
|
+
<script>
|
|
508
|
+
${networkReplayPlugin}
|
|
509
|
+
</script>
|
|
510
|
+
|
|
511
|
+
<script>
|
|
512
|
+
// CapVision Player Integration Script for Inline Players
|
|
513
|
+
(function() {
|
|
514
|
+
const recordings = ${JSON.stringify(recordings)};
|
|
515
|
+
let players = [];
|
|
516
|
+
|
|
517
|
+
// Initialize all inline players
|
|
518
|
+
function initializeInlinePlayers() {
|
|
519
|
+
recordings.forEach((recording, index) => {
|
|
520
|
+
const playerContainer = document.getElementById(\`capvision-player-inline-\${index}\`);
|
|
521
|
+
if (playerContainer) {
|
|
522
|
+
try {
|
|
523
|
+
console.log('🟡 Initializing inline player for recording:', recording.filename);
|
|
524
|
+
|
|
525
|
+
// Clear loading message
|
|
526
|
+
playerContainer.innerHTML = '';
|
|
527
|
+
|
|
528
|
+
// Build plugins array for replay
|
|
529
|
+
const plugins = [];
|
|
530
|
+
|
|
531
|
+
// Add console replay plugin if available
|
|
532
|
+
if (window.rrwebPluginConsoleReplay) {
|
|
533
|
+
try {
|
|
534
|
+
const replayOptions = { level: ['log', 'info', 'warn', 'error'] };
|
|
535
|
+
let consolePlugin;
|
|
536
|
+
|
|
537
|
+
// Handle different export patterns
|
|
538
|
+
if (typeof window.rrwebPluginConsoleReplay === 'function') {
|
|
539
|
+
consolePlugin = window.rrwebPluginConsoleReplay(replayOptions);
|
|
540
|
+
} else if (window.rrwebPluginConsoleReplay.getReplayConsolePlugin) {
|
|
541
|
+
consolePlugin = window.rrwebPluginConsoleReplay.getReplayConsolePlugin(replayOptions);
|
|
542
|
+
} else if (window.rrwebPluginConsoleReplay.default) {
|
|
543
|
+
consolePlugin = window.rrwebPluginConsoleReplay.default(replayOptions);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (consolePlugin) {
|
|
547
|
+
plugins.push(consolePlugin);
|
|
548
|
+
}
|
|
549
|
+
} catch (error) {
|
|
550
|
+
console.warn('⚠️ Failed to initialize console replay plugin:', error.message);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Add network replay plugin if available
|
|
555
|
+
if (window.rrwebPluginNetworkReplay) {
|
|
556
|
+
try {
|
|
557
|
+
console.log('🟡 Initializing network replay plugin...');
|
|
558
|
+
const networkReplayOptions = {
|
|
559
|
+
showPanel: true,
|
|
560
|
+
maxEntries: 100,
|
|
561
|
+
position: 'bottom-right'
|
|
562
|
+
};
|
|
563
|
+
let networkPlugin;
|
|
564
|
+
|
|
565
|
+
// Handle different export patterns
|
|
566
|
+
if (typeof window.rrwebPluginNetworkReplay === 'function') {
|
|
567
|
+
networkPlugin = window.rrwebPluginNetworkReplay(networkReplayOptions);
|
|
568
|
+
console.log('🟢 Network replay plugin initialized (function pattern)');
|
|
569
|
+
} else if (window.rrwebPluginNetworkReplay.getReplayNetworkPlugin) {
|
|
570
|
+
networkPlugin = window.rrwebPluginNetworkReplay.getReplayNetworkPlugin(networkReplayOptions);
|
|
571
|
+
console.log('🟢 Network replay plugin initialized (getReplayNetworkPlugin pattern)');
|
|
572
|
+
} else if (window.rrwebPluginNetworkReplay.default) {
|
|
573
|
+
networkPlugin = window.rrwebPluginNetworkReplay.default(networkReplayOptions);
|
|
574
|
+
console.log('🟢 Network replay plugin initialized (default pattern)');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (networkPlugin) {
|
|
578
|
+
console.log('🟡 Network plugin structure:', {
|
|
579
|
+
hasHandler: typeof networkPlugin.handler === 'function',
|
|
580
|
+
hasOnBuild: typeof networkPlugin.onBuild === 'function',
|
|
581
|
+
keys: Object.keys(networkPlugin)
|
|
582
|
+
});
|
|
583
|
+
plugins.push(networkPlugin);
|
|
584
|
+
console.log('🟢 Network replay plugin added to plugins array');
|
|
585
|
+
} else {
|
|
586
|
+
console.warn('⚠️ Network replay plugin is null or undefined');
|
|
587
|
+
}
|
|
588
|
+
} catch (error) {
|
|
589
|
+
console.error('🔴 Failed to initialize network replay plugin:', error);
|
|
590
|
+
console.error('🔴 Error stack:', error.stack);
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
console.warn('⚠️ window.rrwebPluginNetworkReplay not found');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Create RRWeb player
|
|
597
|
+
const player = new rrwebPlayer({
|
|
598
|
+
target: playerContainer,
|
|
599
|
+
props: {
|
|
600
|
+
events: recording.data.events,
|
|
601
|
+
width: ${this.config.playerWidth},
|
|
602
|
+
height: ${this.config.playerHeight},
|
|
603
|
+
autoPlay: ${this.config.autoPlay},
|
|
604
|
+
speed: 1,
|
|
605
|
+
showController: true,
|
|
606
|
+
mouseTail: true,
|
|
607
|
+
plugins: plugins.length > 0 ? plugins : undefined,
|
|
608
|
+
insertStyleRules: [
|
|
609
|
+
'.replayer-wrapper { border-radius: 8px; width: 100%; height: 100%; }',
|
|
610
|
+
'.replayer-mouse { z-index: 1000; }',
|
|
611
|
+
'.replayer-controller { background: rgba(0,0,0,0.8); border-radius: 4px; }',
|
|
612
|
+
'.replayer-controller button { color: white; }',
|
|
613
|
+
'.replayer-wrapper { background: white; }'
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
players[index] = player;
|
|
619
|
+
|
|
620
|
+
console.log('🟢 Successfully initialized inline player for recording:', recording.filename);
|
|
621
|
+
|
|
622
|
+
} catch (error) {
|
|
623
|
+
console.error('🔴 Failed to initialize inline player for recording:', recording.filename, error);
|
|
624
|
+
playerContainer.innerHTML = \`
|
|
625
|
+
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #dc3545; font-size: 16px; flex-direction: column;">
|
|
626
|
+
<div>❌ Failed to load recording</div>
|
|
627
|
+
<div style="font-size: 12px; margin-top: 5px;">\${error.message}</div>
|
|
628
|
+
</div>
|
|
629
|
+
\`;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Initialize players when DOM is ready
|
|
636
|
+
if (document.readyState === 'loading') {
|
|
637
|
+
document.addEventListener('DOMContentLoaded', initializeInlinePlayers);
|
|
638
|
+
} else {
|
|
639
|
+
initializeInlinePlayers();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Cleanup function
|
|
643
|
+
window.addEventListener('beforeunload', function() {
|
|
644
|
+
players.forEach(player => {
|
|
645
|
+
if (player && player.destroy) {
|
|
646
|
+
player.destroy();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
console.log('🟢 CapVision Player integration loaded with', recordings.length, 'recording(s)');
|
|
652
|
+
|
|
653
|
+
})();
|
|
654
|
+
</script>
|
|
655
|
+
`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Enhance a single HTML report with CapVision player
|
|
660
|
+
* @async
|
|
661
|
+
* @param {string} reportPath - Path to HTML report file
|
|
662
|
+
* @returns {Promise<void>}
|
|
663
|
+
*/
|
|
664
|
+
async enhanceReport(reportPath) {
|
|
665
|
+
try {
|
|
666
|
+
// Check master feature flag first
|
|
667
|
+
if (this.config.ENABLE_FEATURE === false) {
|
|
668
|
+
console.log('🟡 CapVision feature disabled (ENABLE_FEATURE=false), skipping report enhancement');
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
console.log('🟡 Enhancing HTML report with CapVision player:', reportPath);
|
|
673
|
+
|
|
674
|
+
if (!fs.existsSync(reportPath)) {
|
|
675
|
+
console.log('🟡 HTML report not found, skipping enhancement');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const recordings = this.getAvailableRecordings();
|
|
680
|
+
if (recordings.length === 0) {
|
|
681
|
+
console.log('🟡 No CapVision recordings found, skipping enhancement');
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let htmlContent = fs.readFileSync(reportPath, 'utf8');
|
|
686
|
+
|
|
687
|
+
// Add inline players
|
|
688
|
+
htmlContent = this.addInlineRecordingPlayers(htmlContent, recordings);
|
|
689
|
+
|
|
690
|
+
// Add player scripts and CSS
|
|
691
|
+
const capVisionPlayerHTML = this.generateCapVisionPlayerHTML(recordings);
|
|
692
|
+
const bodyEndIndex = htmlContent.lastIndexOf('</body>');
|
|
693
|
+
|
|
694
|
+
if (bodyEndIndex === -1) {
|
|
695
|
+
console.log('🟡 Could not find </body> tag, appending to end');
|
|
696
|
+
htmlContent += capVisionPlayerHTML;
|
|
697
|
+
} else {
|
|
698
|
+
htmlContent = htmlContent.slice(0, bodyEndIndex) + capVisionPlayerHTML + htmlContent.slice(bodyEndIndex);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
fs.writeFileSync(reportPath, htmlContent);
|
|
702
|
+
|
|
703
|
+
console.log('🟢 Successfully enhanced HTML report with CapVision player');
|
|
704
|
+
console.log(`🟢 Found ${recordings.length} recording(s) to integrate`);
|
|
705
|
+
|
|
706
|
+
} catch (error) {
|
|
707
|
+
console.error('🔴 Failed to enhance HTML report:', error.message);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Enhance all HTML reports in the reports directory
|
|
713
|
+
* @async
|
|
714
|
+
* @returns {Promise<void>}
|
|
715
|
+
*/
|
|
716
|
+
async enhanceAllReports() {
|
|
717
|
+
try {
|
|
718
|
+
// Check master feature flag first
|
|
719
|
+
if (this.config.ENABLE_FEATURE === false) {
|
|
720
|
+
console.log('🟡 CapVision feature disabled (ENABLE_FEATURE=false), skipping report enhancement');
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (!this.isEnhancementEnabled()) {
|
|
725
|
+
console.log('🟡 Skipping report enhancement (disabled for current cluster/module)');
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (!fs.existsSync(this.config.reportsDir)) {
|
|
730
|
+
console.log('🟡 Reports directory not found');
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const files = fs.readdirSync(this.config.reportsDir);
|
|
735
|
+
const htmlFiles = files.filter(file => file.endsWith('.html'));
|
|
736
|
+
|
|
737
|
+
for (const htmlFile of htmlFiles) {
|
|
738
|
+
const reportPath = path.join(this.config.reportsDir, htmlFile);
|
|
739
|
+
await this.enhanceReport(reportPath);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
console.log('🟢 Enhanced all HTML reports with CapVision player');
|
|
743
|
+
|
|
744
|
+
} catch (error) {
|
|
745
|
+
console.error('🔴 Failed to enhance reports:', error.message);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Export class and default config
|
|
751
|
+
module.exports = {
|
|
752
|
+
ReportEnhancer,
|
|
753
|
+
DEFAULT_ENHANCER_CONFIG
|
|
754
|
+
};
|
|
755
|
+
|