@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.
- package/LICENSE +190 -0
- package/README.md +388 -0
- package/package.json +64 -0
- package/src/commands/auth.js +259 -0
- package/src/commands/chrome.js +140 -0
- package/src/commands/ci-run.js +123 -0
- package/src/commands/ci-setup.js +288 -0
- package/src/commands/drifts.js +423 -0
- package/src/commands/import-tests.js +309 -0
- package/src/commands/ingest.js +458 -0
- package/src/commands/init.js +633 -0
- package/src/commands/publish.js +1721 -0
- package/src/commands/pull.js +303 -0
- package/src/commands/record.js +94 -0
- package/src/commands/run.js +476 -0
- package/src/commands/setup-wizard.js +740 -0
- package/src/commands/setup.js +137 -0
- package/src/commands/status.js +275 -0
- package/src/commands/sync.js +621 -0
- package/src/commands/ui.js +248 -0
- package/src/commands/validate-docs.js +529 -0
- package/src/index.js +462 -0
- package/src/lib/api-client.js +815 -0
- package/src/lib/capture-engine.js +1623 -0
- package/src/lib/capture-script-runner.js +3120 -0
- package/src/lib/ci-detect.js +137 -0
- package/src/lib/config.js +1240 -0
- package/src/lib/diff-engine.js +642 -0
- package/src/lib/hash.js +74 -0
- package/src/lib/image-crop.js +396 -0
- package/src/lib/matrix.js +89 -0
- package/src/lib/output-path-template.js +318 -0
- package/src/lib/playwright-runner.js +252 -0
- package/src/lib/polished-clip.js +553 -0
- package/src/lib/privacy-engine.js +408 -0
- package/src/lib/progress-tracker.js +142 -0
- package/src/lib/record-browser-injection.js +654 -0
- package/src/lib/record-cdp.js +612 -0
- package/src/lib/record-clip.js +343 -0
- package/src/lib/record-config.js +623 -0
- package/src/lib/record-screenshot.js +360 -0
- package/src/lib/record-terminal.js +123 -0
- package/src/lib/recorder-service.js +781 -0
- package/src/lib/secrets.js +51 -0
- package/src/lib/selector-strategies.js +859 -0
- package/src/lib/standalone-mode.js +400 -0
- package/src/lib/storage-providers.js +569 -0
- package/src/lib/style-engine.js +684 -0
- package/src/lib/ui-api.js +4677 -0
- package/src/lib/ui-assets.js +373 -0
- package/src/lib/ui-executor.js +587 -0
- package/src/lib/variant-injector.js +591 -0
- package/src/lib/viewport-presets.js +454 -0
- package/src/lib/worker-pool.js +118 -0
- package/web/cropper/index.html +436 -0
- package/web/manager/dist/assets/index--ZgioErz.js +507 -0
- package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
- package/web/manager/dist/index.html +27 -0
- 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
|
+
};
|