@capillarytech/cap-ui-dev-tools 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,312 @@
1
+ const { CapVisionRecorder } = require('../core/CapVisionRecorder');
2
+ const { ReportEnhancer } = require('../core/ReportEnhancer');
3
+
4
+ /**
5
+ * WebdriverIO Browser Executor Adapter
6
+ * Adapts WebdriverIO's browser object to the BrowserExecutor interface
7
+ * @class
8
+ */
9
+ class WebdriverIOBrowserExecutor {
10
+ /**
11
+ * Create a new WebdriverIO Browser Executor
12
+ * @param {Object} browser - WebdriverIO browser instance
13
+ */
14
+ constructor(browser) {
15
+ /** @type {Object} */
16
+ this.browser = browser;
17
+ }
18
+
19
+ /**
20
+ * Execute script in browser context
21
+ * @template T
22
+ * @param {string|Function} script - Script to execute
23
+ * @param {...*} args - Arguments to pass to script
24
+ * @returns {Promise<T>} Result of script execution
25
+ */
26
+ async execute(script, ...args) {
27
+ return this.browser.execute(script, ...args);
28
+ }
29
+
30
+ /**
31
+ * Pause execution for specified milliseconds
32
+ * @param {number} ms - Milliseconds to pause
33
+ * @returns {Promise<void>}
34
+ */
35
+ async pause(ms) {
36
+ return this.browser.pause(ms);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * WebdriverIO CapVision Recorder Adapter
42
+ * Provides WebdriverIO-specific helpers and integration
43
+ * @class
44
+ */
45
+ class WebdriverIOCapVisionRecorder {
46
+ /**
47
+ * Create a new WebdriverIO CapVision Recorder
48
+ * @param {import('../core/CapVisionRecorder').CapVisionRecorderConfig} [recorderConfig={}] - Recorder configuration
49
+ * @param {import('../core/ReportEnhancer').ReportEnhancerConfig} [enhancerConfig={}] - Enhancer configuration
50
+ */
51
+ constructor(recorderConfig = {}, enhancerConfig = {}) {
52
+ /** @type {import('../core/CapVisionRecorder').CapVisionRecorder} */
53
+ this.recorder = new CapVisionRecorder(recorderConfig);
54
+
55
+ /** @type {import('../core/ReportEnhancer').ReportEnhancer} */
56
+ this.enhancer = new ReportEnhancer(enhancerConfig);
57
+
58
+ /** @type {WebdriverIOBrowserExecutor|null} */
59
+ this.browserExecutor = null;
60
+ }
61
+
62
+ /**
63
+ * Initialize with browser object
64
+ * Call this in beforeTest or beforeSuite hook
65
+ * @param {Object} browser - WebdriverIO browser instance
66
+ */
67
+ init(browser) {
68
+ this.browserExecutor = new WebdriverIOBrowserExecutor(browser);
69
+ this.recorder.setBrowserExecutor(this.browserExecutor);
70
+ }
71
+
72
+ /**
73
+ * Get recorder instance
74
+ * @returns {import('../core/CapVisionRecorder').CapVisionRecorder} Recorder instance
75
+ */
76
+ getRecorder() {
77
+ return this.recorder;
78
+ }
79
+
80
+ /**
81
+ * Get enhancer instance
82
+ * @returns {import('../core/ReportEnhancer').ReportEnhancer} Enhancer instance
83
+ */
84
+ getEnhancer() {
85
+ return this.enhancer;
86
+ }
87
+
88
+ /**
89
+ * Update recorder configuration
90
+ * @param {Partial<import('../core/CapVisionRecorder').CapVisionRecorderConfig>} config - Configuration updates
91
+ */
92
+ updateRecorderConfig(config) {
93
+ this.recorder.updateConfig(config);
94
+ }
95
+
96
+ /**
97
+ * Update enhancer configuration
98
+ * @param {Partial<import('../core/ReportEnhancer').ReportEnhancerConfig>} config - Configuration updates
99
+ */
100
+ updateEnhancerConfig(config) {
101
+ this.enhancer.updateConfig(config);
102
+ }
103
+
104
+ /**
105
+ * Set test type from test file path
106
+ * @param {string} testFilePath - Path to test file
107
+ * @returns {string|null} Detected test type or null
108
+ */
109
+ setTestTypeFromPath(testFilePath) {
110
+ return this.recorder.setTestTypeFromPath(testFilePath);
111
+ }
112
+
113
+ /**
114
+ * Check if recording is enabled
115
+ * @param {string} [cluster] - Override cluster check
116
+ * @param {string} [module] - Override module check
117
+ * @returns {boolean} True if recording enabled
118
+ */
119
+ isCapVisionEnabled(cluster, module) {
120
+ return this.recorder.isCapVisionEnabled(cluster, module);
121
+ }
122
+
123
+ /**
124
+ * Clear recordings folder
125
+ */
126
+ clearRecordingsFolder() {
127
+ this.recorder.clearRecordingsFolder();
128
+ }
129
+
130
+ /**
131
+ * Start recording
132
+ * @async
133
+ * @returns {Promise<void>}
134
+ * @throws {Error} If browser executor not initialized
135
+ */
136
+ async startRecording() {
137
+ if (!this.browserExecutor) {
138
+ throw new Error('Browser executor not initialized. Call init(browser) first.');
139
+ }
140
+ return this.recorder.startRecording();
141
+ }
142
+
143
+ /**
144
+ * Stop recording and save
145
+ * @async
146
+ * @param {string} [testName='test'] - Name of the test
147
+ * @param {boolean} [testPassed=true] - Whether the test passed. If true, recording is not saved permanently but temp file is still deleted
148
+ * @returns {Promise<import('../core/CapVisionRecorder').RecordingMetadata>} Recording metadata
149
+ * @throws {Error} If browser executor not initialized
150
+ */
151
+ async stopRecordingAndSave(testName = 'test', testPassed = true) {
152
+ if (!this.browserExecutor) {
153
+ throw new Error('Browser executor not initialized. Call init(browser) first.');
154
+ }
155
+ return this.recorder.stopRecordingAndSave(testName, testPassed);
156
+ }
157
+
158
+ /**
159
+ * Ensure CapVision is active after page refresh
160
+ * @async
161
+ * @returns {Promise<void>}
162
+ * @throws {Error} If browser executor not initialized
163
+ */
164
+ async ensureCapVisionIsActive() {
165
+ if (!this.browserExecutor) {
166
+ throw new Error('Browser executor not initialized. Call init(browser) first.');
167
+ }
168
+ return this.recorder.ensureCapVisionIsActive();
169
+ }
170
+
171
+ /**
172
+ * Enhance all reports
173
+ * @async
174
+ * @returns {Promise<void>}
175
+ */
176
+ async enhanceAllReports() {
177
+ return this.enhancer.enhanceAllReports();
178
+ }
179
+
180
+ /**
181
+ * Enhance a specific report
182
+ * @async
183
+ * @param {string} reportPath - Path to HTML report file
184
+ * @returns {Promise<void>}
185
+ */
186
+ async enhanceReport(reportPath) {
187
+ return this.enhancer.enhanceReport(reportPath);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Create WebdriverIO hooks for easy integration
193
+ * Usage: Add these hooks to your wdio.conf.js
194
+ *
195
+ * @param {import('../core/CapVisionRecorder').CapVisionRecorderConfig} [recorderConfig={}] - Recorder configuration
196
+ * @param {import('../core/ReportEnhancer').ReportEnhancerConfig} [enhancerConfig={}] - Enhancer configuration
197
+ * @returns {Object} Hooks object with onPrepare, beforeTest, afterTest, onComplete, and getRecorder methods
198
+ *
199
+ * @example
200
+ * const { createWDIOCapVisionHooks } = require('@capillarytech/cap-ui-dev-tools/capvision');
201
+ *
202
+ * const hooks = createWDIOCapVisionHooks({
203
+ * enabledClusters: ['staging'],
204
+ * recordingsOutputDir: './reports/recordings'
205
+ * });
206
+ *
207
+ * exports.config = {
208
+ * onPrepare: hooks.onPrepare,
209
+ * beforeTest: hooks.beforeTest,
210
+ * afterTest: hooks.afterTest,
211
+ * onComplete: hooks.onComplete
212
+ * };
213
+ */
214
+ function createWDIOCapVisionHooks(recorderConfig = {}, enhancerConfig = {}) {
215
+ const capVisionRecorder = new WebdriverIOCapVisionRecorder(recorderConfig, enhancerConfig);
216
+
217
+ return {
218
+ /**
219
+ * Call in onPrepare hook to clear old recordings
220
+ * @returns {void}
221
+ */
222
+ onPrepare: () => {
223
+ capVisionRecorder.clearRecordingsFolder();
224
+ },
225
+
226
+ /**
227
+ * Call in beforeTest hook to start recording
228
+ * @async
229
+ * @param {Object} test - Test object
230
+ * @param {Object} context - Test context
231
+ * @param {Object} browser - WebdriverIO browser instance
232
+ * @returns {Promise<void>}
233
+ */
234
+ beforeTest: async (test, context, browser) => {
235
+ // Initialize browser executor
236
+ capVisionRecorder.init(browser);
237
+
238
+ // Detect test type from file path
239
+ if (test.file) {
240
+ capVisionRecorder.setTestTypeFromPath(test.file);
241
+ }
242
+
243
+ // Start recording if enabled
244
+ if (capVisionRecorder.isCapVisionEnabled()) {
245
+ try {
246
+ console.log('🟡 Starting CapVision recording for test:', test.title);
247
+ await capVisionRecorder.startRecording();
248
+ } catch (error) {
249
+ console.error('🔴 Failed to start CapVision recording:', error.message);
250
+ }
251
+ }
252
+ },
253
+
254
+ /**
255
+ * Call in afterTest hook to stop recording and save
256
+ * @async
257
+ * @param {Object} test - Test object
258
+ * @param {Object} context - Test context
259
+ * @param {Object} result - Test result object
260
+ * @returns {Promise<void>}
261
+ */
262
+ afterTest: async (test, context, result) => {
263
+ if (capVisionRecorder.isCapVisionEnabled()) {
264
+ try {
265
+ const testName = test.title || 'unknown_test';
266
+ console.log('🟡 Test result:', result.passed);
267
+ const testPassed = result.passed !== false; // Default to true if not specified
268
+ const metadata = await capVisionRecorder.stopRecordingAndSave(testName, testPassed);
269
+
270
+ if (metadata && metadata.totalEvents > 0) {
271
+ if (metadata.filename) {
272
+ console.log(`🟢 CapVision recording saved: ${metadata.totalEvents} events -> ${metadata.filename}`);
273
+ } else {
274
+ console.log(`🟡 CapVision recording discarded (test passed): ${metadata.totalEvents} events`);
275
+ }
276
+ } else {
277
+ console.log('🟨 No CapVision events were captured during test execution');
278
+ }
279
+ } catch (error) {
280
+ console.error('🔴 Failed to save CapVision recording:', error.message);
281
+ }
282
+ }
283
+ },
284
+
285
+ /**
286
+ * Call in onComplete hook to enhance reports
287
+ * @async
288
+ * @returns {Promise<void>}
289
+ */
290
+ onComplete: async () => {
291
+ try {
292
+ await capVisionRecorder.enhanceAllReports();
293
+ } catch (error) {
294
+ console.error('🔴 Failed to enhance reports:', error.message);
295
+ }
296
+ },
297
+
298
+ /**
299
+ * Get recorder instance for manual control
300
+ * @returns {WebdriverIOCapVisionRecorder} Recorder instance
301
+ */
302
+ getRecorder: () => capVisionRecorder
303
+ };
304
+ }
305
+
306
+ // Export all components
307
+ module.exports = {
308
+ WebdriverIOBrowserExecutor,
309
+ WebdriverIOCapVisionRecorder,
310
+ createWDIOCapVisionHooks
311
+ };
312
+
@@ -0,0 +1 @@
1
+ .replayer-wrapper{position:relative}.replayer-mouse{position:absolute;width:20px;height:20px;transition:left .05s linear,top .05s linear;background-size:contain;background-position:50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMwMCIgd2lkdGg9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBkYXRhLW5hbWU9IkxheWVyIDEiIHZpZXdCb3g9IjAgMCA1MCA1MCI+PHBhdGggZD0iTTQ4LjcxIDQyLjkxTDM0LjA4IDI4LjI5IDQ0LjMzIDE4YTEgMSAwIDAwLS4zMy0xLjYxTDIuMzUgMS4wNmExIDEgMCAwMC0xLjI5IDEuMjlMMTYuMzkgNDRhMSAxIDAgMDAxLjY1LjM2bDEwLjI1LTEwLjI4IDE0LjYyIDE0LjYzYTEgMSAwIDAwMS40MSAwbDQuMzgtNC4zOGExIDEgMCAwMC4wMS0xLjQyem0tNS4wOSAzLjY3TDI5IDMyYTEgMSAwIDAwLTEuNDEgMGwtOS44NSA5Ljg1TDMuNjkgMy42OWwzOC4xMiAxNEwzMiAyNy41OEExIDEgMCAwMDMyIDI5bDE0LjU5IDE0LjYyeiIvPjwvc3ZnPg==");border-color:transparent}.replayer-mouse:after{content:"";display:inline-block;width:20px;height:20px;background:#4950f6;border-radius:100%;transform:translate(-50%,-50%);opacity:.3}.replayer-mouse.active:after{animation:click .2s ease-in-out 1}.replayer-mouse.touch-device{background-image:none;width:70px;height:70px;border-radius:100%;margin-left:-37px;margin-top:-37px;border:4px solid rgba(73,80,246,0);transition:left 0s linear,top 0s linear,border-color .2s ease-in-out}.replayer-mouse.touch-device.touch-active{border-color:#4950f6;transition:left .25s linear,top .25s linear,border-color .2s ease-in-out}.replayer-mouse.touch-device:after{opacity:0}.replayer-mouse.touch-device.active:after{animation:touch-click .2s ease-in-out 1}.replayer-mouse-tail{position:absolute;pointer-events:none}@keyframes click{0%{opacity:.3;width:20px;height:20px}50%{opacity:.5;width:10px;height:10px}}@keyframes touch-click{0%{opacity:0;width:20px;height:20px}50%{opacity:.5;width:10px;height:10px}}.rr-player{position:relative;background:white;float:left;border-radius:5px;box-shadow:0 24px 48px rgba(17, 16, 62, 0.12)}.rr-player__frame{overflow:hidden}.replayer-wrapper{float:left;clear:both;transform-origin:top left;left:50%;top:50%}.replayer-wrapper>iframe{border:none}.rr-controller.svelte-19ke1iv.svelte-19ke1iv{width:100%;height:80px;background:#fff;display:flex;flex-direction:column;justify-content:space-around;align-items:center;border-radius:0 0 5px 5px}.rr-timeline.svelte-19ke1iv.svelte-19ke1iv{width:80%;display:flex;align-items:center}.rr-timeline__time.svelte-19ke1iv.svelte-19ke1iv{display:inline-block;width:100px;text-align:center;color:#11103e}.rr-progress.svelte-19ke1iv.svelte-19ke1iv{flex:1;height:12px;background:#eee;position:relative;border-radius:3px;cursor:pointer;box-sizing:border-box;border-top:solid 4px #fff;border-bottom:solid 4px #fff}.rr-progress.disabled.svelte-19ke1iv.svelte-19ke1iv{cursor:not-allowed}.rr-progress__step.svelte-19ke1iv.svelte-19ke1iv{height:100%;position:absolute;left:0;top:0;background:#e0e1fe}.rr-progress__handler.svelte-19ke1iv.svelte-19ke1iv{width:20px;height:20px;border-radius:10px;position:absolute;top:2px;transform:translate(-50%, -50%);background:rgb(73, 80, 246)}.rr-controller__btns.svelte-19ke1iv.svelte-19ke1iv{display:flex;align-items:center;justify-content:center;font-size:13px}.rr-controller__btns.svelte-19ke1iv button.svelte-19ke1iv{width:32px;height:32px;display:flex;padding:0;align-items:center;justify-content:center;background:none;border:none;border-radius:50%;cursor:pointer}.rr-controller__btns.svelte-19ke1iv button.svelte-19ke1iv:active{background:#e0e1fe}.rr-controller__btns.svelte-19ke1iv button.active.svelte-19ke1iv{color:#fff;background:rgb(73, 80, 246)}.rr-controller__btns.svelte-19ke1iv button.svelte-19ke1iv:disabled{cursor:not-allowed}.switch.svelte-9brlez.svelte-9brlez.svelte-9brlez{height:1em;display:flex;align-items:center}.switch.disabled.svelte-9brlez.svelte-9brlez.svelte-9brlez{opacity:0.5}.label.svelte-9brlez.svelte-9brlez.svelte-9brlez{margin:0 8px}.switch.svelte-9brlez input[type='checkbox'].svelte-9brlez.svelte-9brlez{position:absolute;opacity:0}.switch.svelte-9brlez label.svelte-9brlez.svelte-9brlez{width:2em;height:1em;position:relative;cursor:pointer;display:block}.switch.disabled.svelte-9brlez label.svelte-9brlez.svelte-9brlez{cursor:not-allowed}.switch.svelte-9brlez label.svelte-9brlez.svelte-9brlez:before{content:'';position:absolute;width:2em;height:1em;left:0.1em;transition:background 0.1s ease;background:rgba(73, 80, 246, 0.5);border-radius:50px}.switch.svelte-9brlez label.svelte-9brlez.svelte-9brlez:after{content:'';position:absolute;width:1em;height:1em;border-radius:50px;left:0;transition:all 0.2s ease;box-shadow:0px 2px 5px 0px rgba(0, 0, 0, 0.3);background:#fcfff4;animation:switch-off 0.2s ease-out;z-index:2}.switch.svelte-9brlez input[type='checkbox'].svelte-9brlez:checked+label.svelte-9brlez:before{background:rgb(73, 80, 246)}.switch.svelte-9brlez input[type='checkbox'].svelte-9brlez:checked+label.svelte-9brlez:after{animation:switch-on 0.2s ease-out;left:1.1em}