@applitools/eyes-storybook 3.61.3 → 3.62.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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.62.0](https://github.com/Applitools-Dev/sdk/compare/js/eyes-storybook@3.61.4...js/eyes-storybook@3.62.0) (2025-10-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * add a retry mechanism for inconsistent timeouts | AD-11191 ([#3305](https://github.com/Applitools-Dev/sdk/issues/3305)) ([2871d59](https://github.com/Applitools-Dev/sdk/commit/2871d59e41eeb4f078a6ec2e3e717b82b3f064bd))
9
+
10
+ ## [3.61.4](https://github.com/Applitools-Dev/sdk/compare/js/eyes-storybook@3.61.3...js/eyes-storybook@3.61.4) (2025-10-22)
11
+
12
+
13
+ ### Dependencies
14
+
15
+ * @applitools/dom-snapshot bumped to 4.13.12
16
+ #### Bug Fixes
17
+
18
+ * upgrade from @applitools/css-tree fork to official css-tree v3.1.0 | AD-11642 ([#3286](https://github.com/Applitools-Dev/sdk/issues/3286)) ([187ac4b](https://github.com/Applitools-Dev/sdk/commit/187ac4bbca0921ed692b2a676200c6a967c0fb33))
19
+ * @applitools/ufg-client bumped to 1.17.5
20
+ #### Bug Fixes
21
+
22
+ * upgrade from @applitools/css-tree fork to official css-tree v3.1.0 | AD-11642 ([#3286](https://github.com/Applitools-Dev/sdk/issues/3286)) ([187ac4b](https://github.com/Applitools-Dev/sdk/commit/187ac4bbca0921ed692b2a676200c6a967c0fb33))
23
+ * @applitools/core bumped to 4.50.4
24
+ #### Bug Fixes
25
+
26
+ * upgrade from @applitools/css-tree fork to official css-tree v3.1.0 | AD-11642 ([#3286](https://github.com/Applitools-Dev/sdk/issues/3286)) ([187ac4b](https://github.com/Applitools-Dev/sdk/commit/187ac4bbca0921ed692b2a676200c6a967c0fb33))
27
+
28
+
29
+
30
+ * @applitools/eyes bumped to 1.36.13
31
+
32
+
3
33
  ## [3.61.3](https://github.com/Applitools-Dev/sdk/compare/js/eyes-storybook@3.61.2...js/eyes-storybook@3.61.3) (2025-10-21)
4
34
 
5
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/eyes-storybook",
3
- "version": "3.61.3",
3
+ "version": "3.62.0",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "applitools",
@@ -59,14 +59,14 @@
59
59
  "up:framework": "cd test/fixtures/storybook-versions/${APPLITOOLS_FRAMEWORK_VERSION} && npm ci"
60
60
  },
61
61
  "dependencies": {
62
- "@applitools/core": "4.50.3",
62
+ "@applitools/core": "4.50.4",
63
63
  "@applitools/driver": "1.24.0",
64
- "@applitools/eyes": "1.36.12",
64
+ "@applitools/eyes": "1.36.13",
65
65
  "@applitools/functional-commons": "1.6.0",
66
66
  "@applitools/logger": "2.2.4",
67
67
  "@applitools/monitoring-commons": "1.0.19",
68
68
  "@applitools/spec-driver-puppeteer": "1.6.6",
69
- "@applitools/ufg-client": "1.17.4",
69
+ "@applitools/ufg-client": "1.17.5",
70
70
  "@applitools/utils": "1.12.0",
71
71
  "@inquirer/prompts": "7.0.1",
72
72
  "boxen": "4.2.0",
@@ -25,6 +25,7 @@ const {extractEnvironment} = require('./extractEnvironment');
25
25
  const {makeCore} = require('@applitools/core');
26
26
  const makeGetStoriesWithConfig = require('./getStoriesWithConfig');
27
27
  const {makeNetworkUtils} = require('./utils/pageNetworkUtils');
28
+ const {readStoriesTimeout: defaultReadStoriesTimeout} = require('./defaultConfig');
28
29
 
29
30
  async function eyesStorybook({
30
31
  config,
@@ -48,6 +49,8 @@ async function eyesStorybook({
48
49
  logger.log(`Running with ${CONCURRENT_TABS} concurrent tabs`);
49
50
 
50
51
  const {storybookUrl, readStoriesTimeout, reloadPagePerStory, navigationWaitUntil} = config;
52
+ const enableTimeoutRetryMechanism = readStoriesTimeout === defaultReadStoriesTimeout;
53
+ const timeoutRetry = 30_000;
51
54
 
52
55
  let iframeUrl;
53
56
  try {
@@ -60,23 +63,6 @@ async function eyesStorybook({
60
63
  process.env.PUPPETEER_DISABLE_HEADLESS_WARNING = true;
61
64
  const browser = await puppeteer.launch(config.puppeteerOptions);
62
65
  logger.log('browser launched');
63
- const page = await browser.newPage();
64
-
65
- const {startInterception} = makeNetworkUtils({
66
- page,
67
- logger,
68
- });
69
- // we send http headers here and in init page
70
- if (config.puppeteerExtraHTTPHeaders) {
71
- await page.setExtraHTTPHeaders(config.puppeteerExtraHTTPHeaders);
72
- }
73
-
74
- await startInterception({
75
- timeout: config.browserRequestsTimeout,
76
- blockPatterns: config.networkBlockPatterns,
77
- browserHeadersOverride: config.browserHeadersOverride,
78
- cache: config.browserCacheRequests,
79
- });
80
66
 
81
67
  const environment = extractEnvironment(addonVersion);
82
68
  const core = await makeCore({spec, agentId: config.agentId, environment, logger});
@@ -113,6 +99,8 @@ async function eyesStorybook({
113
99
  });
114
100
  const pagePool = createPagePool({initPage, logger});
115
101
 
102
+ const {stories, page} = await getStoriesWithSpinner();
103
+
116
104
  const doTakeDomSnapshots = async ({page, ...settings}) => {
117
105
  const driver = await new Driver({spec, driver: page, logger});
118
106
  return await core.takeSnapshots({
@@ -131,8 +119,6 @@ async function eyesStorybook({
131
119
  },
132
120
  });
133
121
  try {
134
- const stories = await getStoriesWithSpinner();
135
-
136
122
  const filteredStories = filterStories({stories, config});
137
123
 
138
124
  // Log filtering and sharding results
@@ -239,63 +225,146 @@ async function eyesStorybook({
239
225
  await browser.close();
240
226
  }
241
227
 
242
- async function getStoriesWithSpinner() {
243
- let hasConsoleErr;
244
- page.on('console', msg => {
245
- hasConsoleErr =
246
- msg.args()[0] &&
247
- msg.args()[0]._remoteObject &&
248
- msg.args()[0]._remoteObject.subtype === 'error';
249
- });
228
+ async function createContext({browser, config, logger}) {
229
+ const context = await browser.createBrowserContext();
230
+ const page = await context.newPage();
250
231
 
251
- logger.log('Getting stories from storybook');
252
- const spinner = ora({text: 'Reading stories', stream: outputStream});
253
- spinner.start();
254
- logger.log('navigating to storybook url:', storybookUrl);
255
- const [navigateErr] = await presult(
256
- page.goto(storybookUrl, {timeout: readStoriesTimeout, waitUntil: navigationWaitUntil}),
257
- );
258
- if (navigateErr) {
259
- logger.log('Error when loading storybook', navigateErr);
260
- const failMsg = refineErrorMessage({
261
- prefix: 'Error when loading storybook.',
262
- error: navigateErr,
263
- });
264
- spinner.fail(failMsg);
265
- throw new Error();
232
+ logger.log('A new context and page created');
233
+
234
+ // Set up interception and headers, but do not navigate
235
+ const {startInterception} = makeNetworkUtils({page, logger});
236
+ if (config.puppeteerExtraHTTPHeaders) {
237
+ await page.setExtraHTTPHeaders(config.puppeteerExtraHTTPHeaders);
266
238
  }
239
+ await startInterception({
240
+ timeout: config.browserRequestsTimeout,
241
+ blockPatterns: config.networkBlockPatterns,
242
+ browserHeadersOverride: config.browserHeadersOverride,
243
+ cache: config.browserCacheRequests,
244
+ });
267
245
 
268
- const [getStoriesErr, stories] = await presult(readStoriesWithRetry());
246
+ return {context, page};
247
+ }
269
248
 
270
- if (getStoriesErr) {
271
- logger.log('Error when reading stories:', getStoriesErr);
272
- const failMsg = refineErrorMessage({
273
- prefix: 'Error when reading stories:',
274
- error: getStoriesErr,
275
- });
276
- spinner.fail(failMsg);
277
- throw new Error();
249
+ async function getStoriesWithSpinner() {
250
+ let firstAttemptContext;
251
+ let secondAttemptContext;
252
+ async function attemptNavigate(timeout, waitUntil, isSecondAttempt = false) {
253
+ const contextObj = await createContext({browser, config, logger});
254
+ if (isSecondAttempt) secondAttemptContext = contextObj;
255
+ else firstAttemptContext = contextObj;
256
+ const page = contextObj.page;
257
+ logger.log(
258
+ 'Attempting navigation to storybook url:',
259
+ storybookUrl,
260
+ 'timeout:',
261
+ timeout,
262
+ 'waitUntil:',
263
+ waitUntil,
264
+ );
265
+ const [navigateErr] = await presult(page.goto(storybookUrl, {timeout, waitUntil}));
266
+ if (navigateErr) {
267
+ logger.log('Error when loading storybook', navigateErr);
268
+ return {ok: false, error: navigateErr, page};
269
+ }
270
+ return {ok: true, page};
278
271
  }
279
-
280
- if (!stories.length && hasConsoleErr) {
281
- return [
282
- new Error(
272
+ const spinner = ora({text: 'Reading stories', stream: outputStream});
273
+ spinner.start();
274
+ // Start first attempt immediately with original timeout/waitUntil
275
+ const firstAttempt = attemptNavigate(readStoriesTimeout, navigationWaitUntil);
276
+ logger.log('Started first navigation attempt');
277
+ // Start second attempt after 30 seconds with 2x timeout
278
+ const secondAttempt = enableTimeoutRetryMechanism
279
+ ? new Promise(resolve => {
280
+ setTimeout(() => {
281
+ attemptNavigate(readStoriesTimeout * 2, navigationWaitUntil, true).then(resolve);
282
+ }, timeoutRetry);
283
+ })
284
+ : Promise.resolve({ok: false});
285
+
286
+ // Helper for reading stories after successful navigation
287
+ async function handleReadStories(page) {
288
+ let hasConsoleErr;
289
+ page.on('console', msg => {
290
+ hasConsoleErr =
291
+ msg.args()[0] &&
292
+ msg.args()[0]._remoteObject &&
293
+ msg.args()[0]._remoteObject.subtype === 'error';
294
+ });
295
+ logger.log('Getting stories from storybook');
296
+
297
+ const [getStoriesErr, stories] = await presult(readStoriesWithRetry(page));
298
+ if (getStoriesErr) {
299
+ logger.log('Error when reading stories:', getStoriesErr);
300
+ const failMsg = refineErrorMessage({
301
+ prefix: 'Error when reading stories:',
302
+ error: getStoriesErr,
303
+ });
304
+ spinner.fail(failMsg);
305
+ throw new Error();
306
+ }
307
+ if (!stories.length && hasConsoleErr) {
308
+ throw new Error(
283
309
  'Could not load stories, make sure your storybook renders correctly. Perhaps no stories were rendered?',
284
- ),
285
- ];
310
+ );
311
+ }
312
+ const badParamsError = stories
313
+ .map(s => s.error)
314
+ .filter(Boolean)
315
+ .join('\n');
316
+ if (badParamsError) {
317
+ console.log(chalk.red(`\n${badParamsError}`));
318
+ }
319
+ spinner.succeed();
320
+ logger.log(`got ${stories.length} stories:`, JSON.stringify(stories));
321
+ return {stories, page};
286
322
  }
287
323
 
288
- const badParamsError = stories
289
- .map(s => s.error)
290
- .filter(Boolean)
291
- .join('\n');
292
- if (badParamsError) {
293
- console.log(chalk.red(`\n${badParamsError}`));
324
+ async function cleanupContext(winnerPage) {
325
+ if (winnerPage === firstAttemptContext?.page && secondAttemptContext) {
326
+ try {
327
+ await secondAttemptContext.page.close();
328
+ await secondAttemptContext.context.close();
329
+ logger.log('Closed second attempt context/page after first attempt succeeded');
330
+ } catch (err) {
331
+ logger.log('Error closing second attempt context/page:', err);
332
+ }
333
+ } else if (winnerPage === secondAttemptContext?.page && firstAttemptContext) {
334
+ try {
335
+ await firstAttemptContext.page.close();
336
+ await firstAttemptContext.context.close();
337
+ logger.log('Closed first attempt context/page after second attempt succeeded');
338
+ } catch (err) {
339
+ logger.log('Error closing first attempt context/page:', err);
340
+ }
341
+ }
294
342
  }
295
343
 
296
- spinner.succeed();
297
- logger.log(`got ${stories.length} stories:`, JSON.stringify(stories));
298
- return stories;
344
+ // Wait for the first successful navigation
345
+ return Promise.race([firstAttempt, secondAttempt]).then(async result => {
346
+ if (result.ok) {
347
+ await cleanupContext(result.page);
348
+ return handleReadStories(result.page);
349
+ }
350
+ // If first finished with error, wait for the other
351
+ return Promise.all([firstAttempt, secondAttempt]).then(async results => {
352
+ const success = results.find(r => r.ok);
353
+ if (success) {
354
+ await cleanupContext(success.page);
355
+ return handleReadStories(success.page);
356
+ }
357
+
358
+ const lastError = results[1].error || results[0].error;
359
+ logger.log('Error when loading storybook', lastError);
360
+ const failMsg = refineErrorMessage({
361
+ prefix: 'Error when loading storybook.',
362
+ error: lastError,
363
+ });
364
+ spinner.fail(failMsg);
365
+ throw new Error();
366
+ });
367
+ });
299
368
  }
300
369
 
301
370
  function getRenderIE() {
@@ -314,7 +383,7 @@ async function eyesStorybook({
314
383
  return transitioning;
315
384
  }
316
385
 
317
- async function readStoriesWithRetry() {
386
+ async function readStoriesWithRetry(page) {
318
387
  return executeWithRetry(
319
388
  _readStoriesWithRetry,
320
389
  {