@reshotdev/screenshot 0.0.1-beta.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.
Files changed (59) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +388 -0
  3. package/package.json +64 -0
  4. package/src/commands/auth.js +259 -0
  5. package/src/commands/chrome.js +140 -0
  6. package/src/commands/ci-run.js +123 -0
  7. package/src/commands/ci-setup.js +288 -0
  8. package/src/commands/drifts.js +423 -0
  9. package/src/commands/import-tests.js +309 -0
  10. package/src/commands/ingest.js +458 -0
  11. package/src/commands/init.js +633 -0
  12. package/src/commands/publish.js +1721 -0
  13. package/src/commands/pull.js +303 -0
  14. package/src/commands/record.js +94 -0
  15. package/src/commands/run.js +476 -0
  16. package/src/commands/setup-wizard.js +740 -0
  17. package/src/commands/setup.js +137 -0
  18. package/src/commands/status.js +275 -0
  19. package/src/commands/sync.js +621 -0
  20. package/src/commands/ui.js +248 -0
  21. package/src/commands/validate-docs.js +529 -0
  22. package/src/index.js +462 -0
  23. package/src/lib/api-client.js +815 -0
  24. package/src/lib/capture-engine.js +1623 -0
  25. package/src/lib/capture-script-runner.js +3120 -0
  26. package/src/lib/ci-detect.js +137 -0
  27. package/src/lib/config.js +1240 -0
  28. package/src/lib/diff-engine.js +642 -0
  29. package/src/lib/hash.js +74 -0
  30. package/src/lib/image-crop.js +396 -0
  31. package/src/lib/matrix.js +89 -0
  32. package/src/lib/output-path-template.js +318 -0
  33. package/src/lib/playwright-runner.js +252 -0
  34. package/src/lib/polished-clip.js +553 -0
  35. package/src/lib/privacy-engine.js +408 -0
  36. package/src/lib/progress-tracker.js +142 -0
  37. package/src/lib/record-browser-injection.js +654 -0
  38. package/src/lib/record-cdp.js +612 -0
  39. package/src/lib/record-clip.js +343 -0
  40. package/src/lib/record-config.js +623 -0
  41. package/src/lib/record-screenshot.js +360 -0
  42. package/src/lib/record-terminal.js +123 -0
  43. package/src/lib/recorder-service.js +781 -0
  44. package/src/lib/secrets.js +51 -0
  45. package/src/lib/selector-strategies.js +859 -0
  46. package/src/lib/standalone-mode.js +400 -0
  47. package/src/lib/storage-providers.js +569 -0
  48. package/src/lib/style-engine.js +684 -0
  49. package/src/lib/ui-api.js +4677 -0
  50. package/src/lib/ui-assets.js +373 -0
  51. package/src/lib/ui-executor.js +587 -0
  52. package/src/lib/variant-injector.js +591 -0
  53. package/src/lib/viewport-presets.js +454 -0
  54. package/src/lib/worker-pool.js +118 -0
  55. package/web/cropper/index.html +436 -0
  56. package/web/manager/dist/assets/index--ZgioErz.js +507 -0
  57. package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
  58. package/web/manager/dist/index.html +27 -0
  59. package/web/subtitle-editor/index.html +295 -0
@@ -0,0 +1,591 @@
1
+ /**
2
+ * Universal Variant Injector
3
+ *
4
+ * Supports multiple injection methods for applying variants to browser contexts:
5
+ * - localStorage: Set values in window.localStorage
6
+ * - sessionStorage: Set values in window.sessionStorage
7
+ * - cookie: Set document.cookie values
8
+ * - urlParam: Append query parameters to navigation URLs
9
+ * - browser: Set Playwright browser context options (locale, timezone)
10
+ * - script: Execute custom JavaScript before page load
11
+ * - header: Set custom HTTP headers (requires route interception)
12
+ *
13
+ * This allows clients to use whatever method their application supports
14
+ * for variant switching (RBAC, i18n, themes, feature flags, etc.)
15
+ */
16
+
17
+ const chalk = require("chalk");
18
+
19
+ /**
20
+ * Injection method types
21
+ * @typedef {'localStorage' | 'sessionStorage' | 'cookie' | 'urlParam' | 'browser' | 'script' | 'header'} InjectionMethod
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} InjectionConfig
26
+ * @property {InjectionMethod} method - The injection method to use
27
+ * @property {string} [key] - Key for storage-based methods
28
+ * @property {string} [value] - Value to inject
29
+ * @property {string} [name] - Name for cookie method
30
+ * @property {string} [locale] - Browser locale for browser method
31
+ * @property {string} [timezone] - Timezone for browser method
32
+ * @property {string} [code] - JavaScript code for script method
33
+ * @property {string} [header] - Header name for header method
34
+ * @property {string} [param] - URL parameter name for urlParam method
35
+ * @property {Object} [options] - Additional options (e.g., cookie settings)
36
+ */
37
+
38
+ /**
39
+ * Resolve variant configuration from scenario and global variants config
40
+ *
41
+ * @param {Object} scenario - The scenario with variant selections
42
+ * @param {Object} variantsConfig - Global variants configuration
43
+ * @returns {Object} Resolved variant with all injection configs
44
+ */
45
+ function resolveVariantConfig(scenario, variantsConfig = {}) {
46
+ const dimensions = variantsConfig.dimensions || {};
47
+ const presets = variantsConfig.presets || {};
48
+
49
+ // Check if scenario uses a preset
50
+ const presetKey = scenario.variantPreset || scenario.preset;
51
+ if (presetKey && presets[presetKey]) {
52
+ const preset = presets[presetKey];
53
+ // Resolve preset values to full variant config
54
+ return resolveVariantValues(preset.values, dimensions);
55
+ }
56
+
57
+ // Check for individual variant dimension selections
58
+ const variantValues = scenario.variant || {};
59
+
60
+ // Also support legacy flat format: scenario.locale, scenario.role
61
+ if (scenario.locale && !variantValues.locale) {
62
+ variantValues.locale = scenario.locale;
63
+ }
64
+ if (scenario.role && !variantValues.role) {
65
+ variantValues.role = scenario.role;
66
+ }
67
+
68
+ if (Object.keys(variantValues).length === 0) {
69
+ return null;
70
+ }
71
+
72
+ return resolveVariantValues(variantValues, dimensions);
73
+ }
74
+
75
+ /**
76
+ * Resolve variant values to full injection configuration
77
+ *
78
+ * @param {Object} values - Map of dimension key to option key (e.g., { locale: 'ko', role: 'admin' })
79
+ * @param {Object} dimensions - Global dimension definitions
80
+ * @returns {Object} Resolved config with injections array and metadata
81
+ */
82
+ function resolveVariantValues(values, dimensions) {
83
+ const result = {
84
+ injections: [],
85
+ browserOptions: {},
86
+ urlParams: {},
87
+ headers: {},
88
+ metadata: {},
89
+ summary: [],
90
+ };
91
+
92
+ for (const [dimensionKey, optionKey] of Object.entries(values)) {
93
+ const dimension = dimensions[dimensionKey];
94
+ if (!dimension) {
95
+ console.warn(
96
+ chalk.yellow(` ⚠ Unknown variant dimension: ${dimensionKey}`)
97
+ );
98
+ continue;
99
+ }
100
+
101
+ const option = dimension.options?.[optionKey];
102
+ if (!option) {
103
+ console.warn(
104
+ chalk.yellow(
105
+ ` ⚠ Unknown option "${optionKey}" for dimension "${dimensionKey}"`
106
+ )
107
+ );
108
+ continue;
109
+ }
110
+
111
+ // Add to summary for logging
112
+ result.summary.push(
113
+ `${dimension.label || dimensionKey}: ${option.name || optionKey}`
114
+ );
115
+
116
+ // Store metadata
117
+ if (option.metadata) {
118
+ result.metadata[dimensionKey] = option.metadata;
119
+ }
120
+
121
+ // Process injections
122
+ const injections = option.inject || [];
123
+ for (const injection of injections) {
124
+ processInjection(injection, result);
125
+ }
126
+ }
127
+
128
+ return result.injections.length > 0 ||
129
+ Object.keys(result.browserOptions).length > 0
130
+ ? result
131
+ : null;
132
+ }
133
+
134
+ /**
135
+ * Process a single injection config and add to result
136
+ */
137
+ function processInjection(injection, result) {
138
+ const { method } = injection;
139
+
140
+ switch (method) {
141
+ case "localStorage":
142
+ case "sessionStorage":
143
+ result.injections.push({
144
+ method,
145
+ key: injection.key,
146
+ value: injection.value,
147
+ });
148
+
149
+ // CRITICAL FIX: Auto-derive colorScheme from theme localStorage value
150
+ // This ensures Playwright's prefers-color-scheme media query matches the theme
151
+ // Match keys that contain 'theme' (case-insensitive) e.g., 'theme', 'reshot-theme', 'app-theme'
152
+ const keyLower = String(injection.key).toLowerCase();
153
+ if (keyLower.includes("theme") && !result.browserOptions.colorScheme) {
154
+ const themeValue = String(injection.value).toLowerCase();
155
+ if (themeValue === "dark" || themeValue === "night" || themeValue.includes("dark")) {
156
+ result.browserOptions.colorScheme = "dark";
157
+ } else if (themeValue === "light" || themeValue === "day" || themeValue.includes("light")) {
158
+ result.browserOptions.colorScheme = "light";
159
+ }
160
+ }
161
+ break;
162
+
163
+ case "cookie":
164
+ result.injections.push({
165
+ method: "cookie",
166
+ name: injection.name,
167
+ value: injection.value,
168
+ options: injection.options || {},
169
+ });
170
+
171
+ // CRITICAL FIX: Also derive colorScheme from theme cookie value
172
+ // Match names that contain 'theme' (case-insensitive)
173
+ const cookieNameLower = String(injection.name).toLowerCase();
174
+ if (cookieNameLower.includes("theme") && !result.browserOptions.colorScheme) {
175
+ const themeValue = String(injection.value).toLowerCase();
176
+ if (themeValue === "dark" || themeValue === "night" || themeValue.includes("dark")) {
177
+ result.browserOptions.colorScheme = "dark";
178
+ } else if (themeValue === "light" || themeValue === "day" || themeValue.includes("light")) {
179
+ result.browserOptions.colorScheme = "light";
180
+ }
181
+ }
182
+ break;
183
+
184
+ case "browser":
185
+ // Browser options get merged for Playwright context
186
+ if (injection.locale) {
187
+ result.browserOptions.locale = injection.locale;
188
+ }
189
+ if (injection.timezone) {
190
+ result.browserOptions.timezoneId = injection.timezone;
191
+ }
192
+ if (injection.colorScheme) {
193
+ result.browserOptions.colorScheme = injection.colorScheme;
194
+ }
195
+ break;
196
+
197
+ case "urlParam":
198
+ result.urlParams[injection.param] = injection.value;
199
+ break;
200
+
201
+ case "header":
202
+ result.headers[injection.header] = injection.value;
203
+ break;
204
+
205
+ case "script":
206
+ result.injections.push({
207
+ method: "script",
208
+ code: injection.code,
209
+ });
210
+ break;
211
+
212
+ default:
213
+ console.warn(chalk.yellow(` ⚠ Unknown injection method: ${method}`));
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Apply variant injections to a Playwright page
219
+ * This should be called BEFORE navigation
220
+ *
221
+ * CRITICAL: For localStorage-based theming, we use addInitScript which runs
222
+ * before any page JavaScript. However, some apps use inline <script> tags
223
+ * that run during HTML parsing (before addInitScript). For these cases,
224
+ * the capture-engine will reload the page after first navigation.
225
+ *
226
+ * @param {import('playwright').Page} page - Playwright page
227
+ * @param {import('playwright').BrowserContext} context - Playwright context (optional, for cookies)
228
+ * @param {Object} variantConfig - Resolved variant configuration
229
+ * @param {Function} [logger] - Optional logger function
230
+ */
231
+ async function applyVariantToPage(page, variantConfig, logger = console.log, context = null) {
232
+ if (!variantConfig || variantConfig.injections.length === 0) {
233
+ return;
234
+ }
235
+
236
+ // Group injections by type for efficient application
237
+ const storageItems = { localStorage: {}, sessionStorage: {} };
238
+ const cookies = [];
239
+ const scripts = [];
240
+
241
+ for (const injection of variantConfig.injections) {
242
+ switch (injection.method) {
243
+ case "localStorage":
244
+ storageItems.localStorage[injection.key] = injection.value;
245
+ break;
246
+ case "sessionStorage":
247
+ storageItems.sessionStorage[injection.key] = injection.value;
248
+ break;
249
+ case "cookie":
250
+ cookies.push(injection);
251
+ break;
252
+ case "script":
253
+ scripts.push(injection.code);
254
+ break;
255
+ }
256
+ }
257
+
258
+ // Apply localStorage items with interception to prevent app overrides
259
+ // This is critical for apps that force a specific theme (e.g., setTheme("dark"))
260
+ if (Object.keys(storageItems.localStorage).length > 0) {
261
+ await page.addInitScript((items) => {
262
+ // Store our desired values and theme override
263
+ window.__RESHOT_LOCKED_STORAGE__ = items;
264
+ window.__RESHOT_THEME_OVERRIDE__ = null;
265
+
266
+ // Helper function to determine theme from value
267
+ const getThemeFromValue = (value) => {
268
+ const v = String(value).toLowerCase();
269
+ if (v === 'dark' || v.includes('dark') || v === 'night') return 'dark';
270
+ if (v === 'light' || v.includes('light') || v === 'day') return 'light';
271
+ return null;
272
+ };
273
+
274
+ // Set our values immediately
275
+ for (const [key, value] of Object.entries(items)) {
276
+ const strValue = typeof value === "string" ? value : JSON.stringify(value);
277
+ localStorage.setItem(key, strValue);
278
+
279
+ // Track theme override for class enforcement (normalized to 'dark' or 'light')
280
+ if (key.toLowerCase().includes('theme')) {
281
+ const theme = getThemeFromValue(value);
282
+ if (theme) {
283
+ window.__RESHOT_THEME_OVERRIDE__ = theme;
284
+ }
285
+ }
286
+ }
287
+
288
+ // Intercept setItem to prevent the app from overriding our values
289
+ const origSetItem = Storage.prototype.setItem;
290
+ Storage.prototype.setItem = function(key, value) {
291
+ if (key.toLowerCase().includes('theme') && window.__RESHOT_THEME_OVERRIDE__) {
292
+ // Force our theme value
293
+ return origSetItem.call(this, key, window.__RESHOT_THEME_OVERRIDE__);
294
+ }
295
+ if (window.__RESHOT_LOCKED_STORAGE__ && key in window.__RESHOT_LOCKED_STORAGE__) {
296
+ const forcedValue = window.__RESHOT_LOCKED_STORAGE__[key];
297
+ return origSetItem.call(this, key,
298
+ typeof forcedValue === "string" ? forcedValue : JSON.stringify(forcedValue)
299
+ );
300
+ }
301
+ return origSetItem.call(this, key, value);
302
+ };
303
+
304
+ // Function to enforce theme class on document
305
+ const enforceTheme = () => {
306
+ if (window.__RESHOT_THEME_OVERRIDE__ && document.documentElement) {
307
+ const wanted = window.__RESHOT_THEME_OVERRIDE__; // Already normalized to 'dark' or 'light'
308
+ const current = document.documentElement.classList.contains('dark') ? 'dark' :
309
+ document.documentElement.classList.contains('light') ? 'light' : null;
310
+ if (current !== wanted) {
311
+ document.documentElement.classList.remove('dark', 'light');
312
+ document.documentElement.classList.add(wanted);
313
+ document.documentElement.style.colorScheme = wanted;
314
+ // Also set data attribute used by some libraries
315
+ document.documentElement.setAttribute('data-theme', wanted);
316
+ }
317
+ }
318
+ };
319
+
320
+ // Set theme class immediately
321
+ enforceTheme();
322
+
323
+ // Use MutationObserver to continuously enforce theme
324
+ // This handles React hydration and next-themes setTheme() calls
325
+ const observer = new MutationObserver((mutations) => {
326
+ for (const mutation of mutations) {
327
+ if (mutation.type === 'attributes' && (mutation.attributeName === 'class' || mutation.attributeName === 'data-theme')) {
328
+ enforceTheme();
329
+ }
330
+ }
331
+ });
332
+
333
+ // Start observing when document is ready
334
+ if (document.documentElement) {
335
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] });
336
+ } else {
337
+ document.addEventListener('DOMContentLoaded', () => {
338
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] });
339
+ enforceTheme();
340
+ });
341
+ }
342
+ }, storageItems.localStorage);
343
+ logger(
344
+ chalk.gray(
345
+ ` → localStorage: ${JSON.stringify(storageItems.localStorage)}`
346
+ )
347
+ );
348
+ }
349
+
350
+ // Apply sessionStorage items
351
+ if (Object.keys(storageItems.sessionStorage).length > 0) {
352
+ await page.addInitScript((items) => {
353
+ for (const [key, value] of Object.entries(items)) {
354
+ sessionStorage.setItem(
355
+ key,
356
+ typeof value === "string" ? value : JSON.stringify(value)
357
+ );
358
+ }
359
+ }, storageItems.sessionStorage);
360
+ logger(
361
+ chalk.gray(
362
+ ` → sessionStorage: ${JSON.stringify(storageItems.sessionStorage)}`
363
+ )
364
+ );
365
+ }
366
+
367
+ // Apply cookies using context.addCookies() for proper timing
368
+ // This ensures cookies are sent with the first HTTP request
369
+ if (cookies.length > 0) {
370
+ // Get the context from the page, or use provided context
371
+ const ctx = context || page.context();
372
+
373
+ // Get a sample URL to determine domain for cookies
374
+ // Default to localhost if we can't determine
375
+ const playwrightCookies = cookies.map((cookie) => {
376
+ return {
377
+ name: cookie.name,
378
+ value: String(cookie.value),
379
+ domain: cookie.options?.domain || "localhost",
380
+ path: cookie.options?.path || "/",
381
+ secure: cookie.options?.secure || false,
382
+ httpOnly: cookie.options?.httpOnly || false,
383
+ sameSite: cookie.options?.sameSite || "Lax",
384
+ };
385
+ });
386
+
387
+ await ctx.addCookies(playwrightCookies);
388
+
389
+ // Also set via addInitScript for document.cookie access
390
+ await page.addInitScript((cookieList) => {
391
+ for (const cookie of cookieList) {
392
+ let cookieStr = `${cookie.name}=${encodeURIComponent(cookie.value)}`;
393
+ if (cookie.options?.path) cookieStr += `; path=${cookie.options.path}`;
394
+ if (cookie.options?.maxAge)
395
+ cookieStr += `; max-age=${cookie.options.maxAge}`;
396
+ if (cookie.options?.secure) cookieStr += "; secure";
397
+ if (cookie.options?.sameSite)
398
+ cookieStr += `; samesite=${cookie.options.sameSite}`;
399
+ document.cookie = cookieStr;
400
+ }
401
+ }, cookies);
402
+ logger(chalk.gray(` → cookies: ${cookies.map((c) => c.name).join(", ")}`));
403
+ }
404
+
405
+ // Apply custom scripts
406
+ for (const code of scripts) {
407
+ await page.addInitScript(code);
408
+ logger(chalk.gray(` → custom script injected`));
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Apply localStorage values AFTER navigation via page.evaluate(), then reload.
414
+ * This is needed for SSR apps that read localStorage in inline <script> tags
415
+ * during HTML parsing (before addInitScript runs).
416
+ *
417
+ * @param {import('playwright').Page} page - Playwright page
418
+ * @param {Object} variantConfig - Resolved variant configuration
419
+ * @param {Function} [logger] - Optional logger function
420
+ * @returns {boolean} - Whether a reload was performed
421
+ */
422
+ async function applyStorageAndReload(page, variantConfig, logger = console.log) {
423
+ if (!variantConfig || variantConfig.injections.length === 0) {
424
+ return false;
425
+ }
426
+
427
+ // Collect localStorage injections
428
+ const localStorageItems = {};
429
+ for (const injection of variantConfig.injections) {
430
+ if (injection.method === "localStorage") {
431
+ localStorageItems[injection.key] = injection.value;
432
+ }
433
+ }
434
+
435
+ if (Object.keys(localStorageItems).length === 0) {
436
+ return false;
437
+ }
438
+
439
+ // Set localStorage via page.evaluate() - this runs immediately
440
+ await page.evaluate((items) => {
441
+ // Store locked items for interception
442
+ window.__RESHOT_LOCKED_STORAGE__ = items;
443
+
444
+ for (const [key, value] of Object.entries(items)) {
445
+ localStorage.setItem(
446
+ key,
447
+ typeof value === "string" ? value : JSON.stringify(value)
448
+ );
449
+ }
450
+
451
+ // Helper function to determine theme from value
452
+ const getThemeFromValue = (value) => {
453
+ const v = String(value).toLowerCase();
454
+ if (v === 'dark' || v.includes('dark') || v === 'night') return 'dark';
455
+ if (v === 'light' || v.includes('light') || v === 'day') return 'light';
456
+ return null;
457
+ };
458
+
459
+ // For theme-related keys, also set the HTML class
460
+ for (const [key, value] of Object.entries(items)) {
461
+ if (key.toLowerCase().includes('theme')) {
462
+ const theme = getThemeFromValue(value);
463
+ if (theme) {
464
+ document.documentElement.classList.remove('dark', 'light');
465
+ document.documentElement.classList.add(theme);
466
+ document.documentElement.style.colorScheme = theme;
467
+ // Also set data attribute used by some libraries
468
+ document.documentElement.setAttribute('data-theme', theme);
469
+ }
470
+ }
471
+ }
472
+ }, localStorageItems);
473
+
474
+ logger(chalk.gray(` → localStorage set via evaluate, reloading page...`));
475
+
476
+ // Reload the page so inline scripts can read the new localStorage values
477
+ await page.reload({ waitUntil: "domcontentloaded", timeout: 30000 });
478
+
479
+ // After reload, force the theme class again (in case React rehydration resets it)
480
+ await page.evaluate((items) => {
481
+ // Helper function to determine theme from value
482
+ const getThemeFromValue = (value) => {
483
+ const v = String(value).toLowerCase();
484
+ if (v === 'dark' || v.includes('dark') || v === 'night') return 'dark';
485
+ if (v === 'light' || v.includes('light') || v === 'day') return 'light';
486
+ return null;
487
+ };
488
+
489
+ for (const [key, value] of Object.entries(items)) {
490
+ if (key.toLowerCase().includes('theme')) {
491
+ const theme = getThemeFromValue(value);
492
+ if (theme) {
493
+ document.documentElement.classList.remove('dark', 'light');
494
+ document.documentElement.classList.add(theme);
495
+ document.documentElement.style.colorScheme = theme;
496
+ // Also set data attribute used by some libraries
497
+ document.documentElement.setAttribute('data-theme', theme);
498
+ }
499
+ }
500
+ }
501
+ }, localStorageItems);
502
+
503
+ return true;
504
+ }
505
+
506
+ /**
507
+ * Set up HTTP header interception for variant headers
508
+ *
509
+ * @param {import('playwright').Page} page - Playwright page
510
+ * @param {Object} headers - Headers to inject
511
+ */
512
+ async function setupHeaderInterception(page, headers) {
513
+ if (!headers || Object.keys(headers).length === 0) {
514
+ return;
515
+ }
516
+
517
+ await page.route("**/*", (route) => {
518
+ const existingHeaders = route.request().headers();
519
+ route.continue({
520
+ headers: {
521
+ ...existingHeaders,
522
+ ...headers,
523
+ },
524
+ });
525
+ });
526
+ }
527
+
528
+ /**
529
+ * Modify URL with variant query parameters
530
+ *
531
+ * @param {string} url - Original URL
532
+ * @param {Object} params - Query parameters to add
533
+ * @returns {string} Modified URL
534
+ */
535
+ function applyUrlParams(url, params) {
536
+ if (!params || Object.keys(params).length === 0) {
537
+ return url;
538
+ }
539
+
540
+ const urlObj = new URL(url);
541
+ for (const [key, value] of Object.entries(params)) {
542
+ urlObj.searchParams.set(key, value);
543
+ }
544
+ return urlObj.toString();
545
+ }
546
+
547
+ /**
548
+ * Get browser context options from variant config
549
+ *
550
+ * @param {Object} variantConfig - Resolved variant configuration
551
+ * @param {Object} defaults - Default browser options
552
+ * @returns {Object} Merged browser options
553
+ */
554
+ function getBrowserOptions(variantConfig, defaults = {}) {
555
+ const merged = {
556
+ ...defaults,
557
+ ...(variantConfig?.browserOptions || {}),
558
+ };
559
+
560
+ // Debug log for colorScheme to help diagnose theme issues
561
+ if (process.env.RESHOT_DEBUG) {
562
+ console.log(`[DEBUG] getBrowserOptions - colorScheme: ${merged.colorScheme || 'not set'}`);
563
+ console.log(`[DEBUG] getBrowserOptions - variantConfig.browserOptions: ${JSON.stringify(variantConfig?.browserOptions || {})}`);
564
+ }
565
+
566
+ return merged;
567
+ }
568
+
569
+ /**
570
+ * Log variant summary
571
+ */
572
+ function logVariantSummary(variantConfig, logger = console.log) {
573
+ if (!variantConfig?.summary?.length) {
574
+ return;
575
+ }
576
+
577
+ for (const item of variantConfig.summary) {
578
+ logger(chalk.gray(` ${item}`));
579
+ }
580
+ }
581
+
582
+ module.exports = {
583
+ resolveVariantConfig,
584
+ resolveVariantValues,
585
+ applyVariantToPage,
586
+ applyStorageAndReload,
587
+ setupHeaderInterception,
588
+ applyUrlParams,
589
+ getBrowserOptions,
590
+ logVariantSummary,
591
+ };