@applitools/eyes-storybook 3.60.0 → 3.61.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,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.61.0](https://github.com/Applitools-Dev/sdk/compare/js/eyes-storybook@3.60.0...js/eyes-storybook@3.61.0) (2025-10-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * storybook addon ([#3104](https://github.com/Applitools-Dev/sdk/issues/3104)) ([16e09cb](https://github.com/Applitools-Dev/sdk/commit/16e09cba8928c3a24b9e0d9d41e0936fbaec2773))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * @applitools/screenshoter bumped to 3.12.6
14
+ #### Bug Fixes
15
+
16
+ * wait after scroll | FLD-3594 ([#3252](https://github.com/Applitools-Dev/sdk/issues/3252)) ([e452422](https://github.com/Applitools-Dev/sdk/commit/e4524229b64e40d9b9596a92bfa94daf5824286a))
17
+ * @applitools/core-base bumped to 1.28.1
18
+ #### Bug Fixes
19
+
20
+ * unexpected concurrency values from server | AD-11465 ([#3248](https://github.com/Applitools-Dev/sdk/issues/3248)) ([0dd28c7](https://github.com/Applitools-Dev/sdk/commit/0dd28c7b297d5ad3aabc6b87e427e3e09a993825))
21
+ * @applitools/nml-client bumped to 1.11.7
22
+
23
+ * @applitools/ec-client bumped to 1.12.9
24
+
25
+ * @applitools/core bumped to 4.49.0
26
+ #### Features
27
+
28
+ * storybook addon ([#3104](https://github.com/Applitools-Dev/sdk/issues/3104)) ([16e09cb](https://github.com/Applitools-Dev/sdk/commit/16e09cba8928c3a24b9e0d9d41e0936fbaec2773))
29
+
30
+
31
+ #### Bug Fixes
32
+
33
+ * duplicate concurrency warnings ([#3255](https://github.com/Applitools-Dev/sdk/issues/3255)) ([ef2f94a](https://github.com/Applitools-Dev/sdk/commit/ef2f94ab4137c78396583f166344285beeb49be7))
34
+
35
+
36
+
37
+ * @applitools/eyes bumped to 1.36.9
38
+
39
+
3
40
  ## [3.60.0](https://github.com/Applitools-Dev/sdk/compare/js/eyes-storybook@3.59.1...js/eyes-storybook@3.60.0) (2025-09-22)
4
41
 
5
42
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ConfigurationPlain, DesktopBrowserInfo, ChromeEmulationInfo, IOSDeviceInfo, IOSMultiDeviceInfo } from '@applitools/eyes';
2
- type irrelevantToStorybook = 'waitBeforeScreenshots' | 'agentId' | 'captureStatusBar' | 'concurrentSessions' | 'connectionTimeout' | 'debugScreenshots' | 'defaultMatchSettings' | 'disableNMLUrlCache' | 'forceFullPageScreenshot' | 'hideCaret' | 'hideScrollbars' | 'hostApp' | 'hostAppInfo' | 'hostOS' | 'hostOSInfo' | 'ignoreBaseline' | 'ignoreCaret' | 'latestCommitInfo' | 'isDisabled' | 'matchTimeout' | 'mobileOptions' | 'removeSession' | 'rotation' | 'scaleRatio' | 'scrollRootElement' | 'sessionType' | 'stitchMode' | 'stitchOverlap' | 'viewportSize';
2
+ type irrelevantToStorybook = 'waitBeforeScreenshots' | 'agentId' | 'captureStatusBar' | 'concurrentSessions' | 'connectionTimeout' | 'debugScreenshots' | 'defaultMatchSettings' | 'disableNMLUrlCache' | 'forceFullPageScreenshot' | 'hideCaret' | 'hideScrollbars' | 'hostApp' | 'hostAppInfo' | 'hostOS' | 'hostOSInfo' | 'ignoreBaseline' | 'ignoreCaret' | 'latestCommitInfo' | 'isDisabled' | 'matchTimeout' | 'mobileOptions' | 'removeSession' | 'rotation' | 'scaleRatio' | 'scrollRootElement' | 'sessionType' | 'stitchMode' | 'stitchOverlap' | 'viewportSize' | 'isAddon';
3
3
  /**
4
4
  * Configuration options for Applitools Eyes Storybook integration.
5
5
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/eyes-storybook",
3
- "version": "3.60.0",
3
+ "version": "3.61.0",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "applitools",
@@ -19,6 +19,7 @@
19
19
  "eyes-setup": "./bin/eyes-setup.js",
20
20
  "eyes-storybook": "./bin/eyes-storybook.js"
21
21
  },
22
+ "main": "./src/main.js",
22
23
  "files": [
23
24
  "src",
24
25
  "bin",
@@ -58,9 +59,9 @@
58
59
  "up:framework": "cd test/fixtures/storybook-versions/${APPLITOOLS_FRAMEWORK_VERSION} && npm ci"
59
60
  },
60
61
  "dependencies": {
61
- "@applitools/core": "4.48.0",
62
+ "@applitools/core": "4.49.0",
62
63
  "@applitools/driver": "1.23.5",
63
- "@applitools/eyes": "1.36.8",
64
+ "@applitools/eyes": "1.36.9",
64
65
  "@applitools/functional-commons": "1.6.0",
65
66
  "@applitools/logger": "2.2.4",
66
67
  "@applitools/monitoring-commons": "1.0.19",
package/src/cli.js CHANGED
@@ -1,22 +1,18 @@
1
1
  'use strict';
2
2
  const yargs = require('yargs');
3
- const {makeLogger} = require('@applitools/logger');
4
- const {configParams: externalConfigParams} = require('./configParams');
5
3
  const VERSION = require('../package.json').version;
6
- const eyesStorybook = require('./eyesStorybook');
7
4
  const processResults = require('./processResults');
8
- const validateAndPopulateConfig = require('./validateAndPopulateConfig');
9
5
  const yargsOptions = require('./yargsOptions');
10
- const {generateConfig} = require('./generateConfig');
11
- const defaultConfig = require('./defaultConfig');
12
- const configDigest = require('./configDigest');
13
6
  const {makeTiming} = require('@applitools/monitoring-commons');
14
7
  const handleJsonFile = require('./handleJsonFile');
15
8
  const handleTapFile = require('./handleTapFile');
16
9
  const handleXmlFile = require('./handleXmlFile');
10
+ const {getConfigAndLogger} = require('./getConfigAndLogger');
17
11
  const {presult} = require('@applitools/functional-commons');
18
12
  const chalk = require('chalk');
19
13
  const utils = require('@applitools/utils');
14
+ const {EyesError} = require('@applitools/eyes');
15
+ const eyesStorybook = require('./eyesStorybook');
20
16
  const {performance, timeItAsync} = makeTiming();
21
17
 
22
18
  (async function () {
@@ -31,7 +27,7 @@ const {performance, timeItAsync} = makeTiming();
31
27
  .options(yargsOptions).argv;
32
28
 
33
29
  console.log(`Using @applitools/eyes-storybook version ${VERSION}.\n`);
34
- const config = generateConfig({argv, defaultConfig, externalConfigParams});
30
+ const {config, logger} = await getConfigAndLogger(argv);
35
31
 
36
32
  if (config.shard) {
37
33
  console.log(`Running with shard: ${config.shard.current}/${config.shard.total}`);
@@ -41,13 +37,6 @@ const {performance, timeItAsync} = makeTiming();
41
37
  }
42
38
  }
43
39
 
44
- const logger = makeLogger({level: config.showLogs ? 'info' : 'silent', label: 'eyes'});
45
- await validateAndPopulateConfig({
46
- config,
47
- logger,
48
- packagePath: process.cwd(),
49
- });
50
- logger.log(`Running with the following config:\n${configDigest(config)}`);
51
40
  const [err, results] = await presult(
52
41
  timeItAsync('eyesStorybook', () => eyesStorybook({config, logger, performance, timeItAsync})),
53
42
  );
@@ -78,7 +67,11 @@ const {performance, timeItAsync} = makeTiming();
78
67
  process.exit(exitCode);
79
68
  }
80
69
  } catch (ex) {
81
- console.log(ex);
70
+ if (utils.types.instanceOf(ex, EyesError)) {
71
+ console.log(ex.message);
72
+ } else {
73
+ console.log(ex);
74
+ }
82
75
  process.exit(1);
83
76
  }
84
77
  })();
@@ -1,14 +1,7 @@
1
1
  'use strict';
2
- const chalk = require('chalk');
3
2
 
4
- const missingApiKeyFailMsg = `
5
- ${chalk.red('Environment variable APPLITOOLS_API_KEY is not set.')}
6
- ${chalk.green(`To fix:
7
- 1. Register for Applitools developer account at www.applitools.com/devreg
8
- 2. Get API key from menu
9
- 3. Set APPLITOOLS_API_KEY environment variable
10
- Mac/Linux: export APPLITOOLS_API_KEY=Your_API_Key_Here
11
- Windows: set APPLITOOLS_API_KEY=Your_API_Key_Here`)}`;
3
+ const chalk = require('chalk');
4
+ const {MissingApiKeyError} = require('@applitools/core');
12
5
 
13
6
  const missingAppNameAndPackageJsonFailMsg = `
14
7
  ${chalk.red(
@@ -47,9 +40,9 @@ function deprecationWarning({deprecatedThing, newThing, isDead}) {
47
40
  }
48
41
 
49
42
  module.exports = {
50
- missingApiKeyFailMsg,
51
43
  missingAppNameAndPackageJsonFailMsg,
52
44
  missingAppNameInPackageJsonFailMsg,
53
45
  refineErrorMessage,
54
46
  deprecationWarning,
47
+ MissingApiKeyError,
55
48
  };
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
  const puppeteer = require('puppeteer');
3
+ const configDigest = require('./configDigest');
3
4
  const getStories = require('../dist/getStories');
4
5
  const {presult} = require('@applitools/functional-commons');
5
6
  const {executeWithRetry} = require('./utils/executeWithRetry');
@@ -31,7 +32,11 @@ async function eyesStorybook({
31
32
  performance,
32
33
  timeItAsync,
33
34
  outputStream = process.stderr,
35
+ eventEmitter,
36
+ signal = new AbortController().signal,
34
37
  }) {
38
+ logger.log(`Running with the following config:\n${configDigest(config)}`);
39
+
35
40
  let renderIE = false;
36
41
  let transitioning = false;
37
42
  logger.log('eyesStorybook started');
@@ -51,7 +56,6 @@ async function eyesStorybook({
51
56
  logger.log(ex);
52
57
  throw new Error(`Storybook URL is not valid: ${storybookUrl}`);
53
58
  }
54
- const agentId = `eyes-storybook/${require('../package.json').version}`;
55
59
  process.env.PUPPETEER_DISABLE_HEADLESS_WARNING = true;
56
60
  const browser = await puppeteer.launch(config.puppeteerOptions);
57
61
  logger.log('browser launched');
@@ -74,7 +78,7 @@ async function eyesStorybook({
74
78
  });
75
79
 
76
80
  const environment = extractEnvironment();
77
- const core = await makeCore({spec, agentId, environment, logger});
81
+ const core = await makeCore({spec, agentId: config.agentId, environment, logger});
78
82
  const manager = await core.makeManager({
79
83
  type: 'ufg',
80
84
  settings: {concurrency: config.testConcurrency, useServerConcurrency: true},
@@ -85,7 +89,7 @@ async function eyesStorybook({
85
89
  settings: {
86
90
  eyesServerUrl: config.eyesServerUrl,
87
91
  apiKey: config.apiKey,
88
- agentId,
92
+ agentId: config.agentId,
89
93
  proxy: config.proxy,
90
94
  useDnsCache: config.useDnsCache,
91
95
  },
@@ -182,6 +186,7 @@ async function eyesStorybook({
182
186
  concurrency: account.serverConcurrency.componentConcurrency,
183
187
  appName: config.appName,
184
188
  serverSettings: account.eyesServer,
189
+ signal,
185
190
  });
186
191
 
187
192
  const renderStories = makeRenderStories({
@@ -192,6 +197,8 @@ async function eyesStorybook({
192
197
  logger,
193
198
  stream: outputStream,
194
199
  pagePool,
200
+ eventEmitter,
201
+ signal,
195
202
  });
196
203
 
197
204
  logger.log('finished creating functions');
@@ -209,6 +216,15 @@ async function eyesStorybook({
209
216
  timeItAsync,
210
217
  }),
211
218
  );
219
+ if (signal.aborted) {
220
+ if (error) {
221
+ const msg = refineErrorMessage({prefix: 'Error in executeRenders:', error});
222
+ logger.log('Error in executeRenders:', error);
223
+ throw new Error(msg);
224
+ } else {
225
+ return {results}; // processResults (which is not used in the addon) doesn't support missing the summary. But it's not a real concern right now.
226
+ }
227
+ }
212
228
  const [errorInGetResults, testResultsSummary] = await presult(
213
229
  manager.getResults({throwErr: false}),
214
230
  );
@@ -219,7 +235,7 @@ async function eyesStorybook({
219
235
 
220
236
  if (error) {
221
237
  const msg = refineErrorMessage({prefix: 'Error in executeRenders:', error});
222
- logger.log(error);
238
+ logger.log('Error in executeRenders:', error);
223
239
  throw new Error(msg);
224
240
  } else {
225
241
  return {summary: testResultsSummary, results};
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const eyesStorybookOrig = require('./eyesStorybook');
4
+ const {EventEmitter} = require('node:events');
5
+
6
+ function eyesStorybookEventEmitter({
7
+ eyesStorybook = eyesStorybookOrig,
8
+ config,
9
+ logger,
10
+ performance,
11
+ timeItAsync,
12
+ signal,
13
+ }) {
14
+ const eventEmitter = new EventEmitter();
15
+ const startedAt = Date.now();
16
+
17
+ eyesStorybook({config, logger, performance, timeItAsync, eventEmitter, signal})
18
+ .then(({results}) => {
19
+ eventEmitter.emit('result', {
20
+ startedAt,
21
+ duration: performance['renderStories'],
22
+ storyResults: results.map(({story, resultsOrErr}) => ({
23
+ story: {id: story.id, queryParams: story.config.queryParams},
24
+ [Array.isArray(resultsOrErr) ? 'results' : 'error']: resultsOrErr,
25
+ })),
26
+ });
27
+ })
28
+ .catch(err => {
29
+ eventEmitter.emit('error', err);
30
+ });
31
+
32
+ return eventEmitter;
33
+ }
34
+
35
+ module.exports = {eyesStorybookEventEmitter};
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const {generateConfig} = require('./generateConfig');
4
+ const defaultConfig = require('./defaultConfig');
5
+ const validateAndPopulateConfig = require('./validateAndPopulateConfig');
6
+ const {makeLogger} = require('@applitools/logger');
7
+ const {configParams: externalConfigParams} = require('./configParams');
8
+
9
+ async function getConfigAndLogger(argv = {}) {
10
+ const config = generateConfig({argv, defaultConfig, externalConfigParams});
11
+ const logger = makeLogger({
12
+ handler: argv.logHandler,
13
+ level: argv.logHandler ? 'all' : config.showLogs ? 'info' : 'silent', // if logHandler is passed, let's pass it all the logs. Otherwise, respect the config
14
+ label: 'eyes',
15
+ });
16
+ await validateAndPopulateConfig({
17
+ config,
18
+ logger,
19
+ packagePath: process.cwd(),
20
+ });
21
+ return {config, logger};
22
+ }
23
+
24
+ module.exports = {getConfigAndLogger};
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ type irrelevantToStorybook = 'waitBeforeScreenshots'
29
29
  | 'stitchMode'
30
30
  | 'stitchOverlap'
31
31
  | 'viewportSize'
32
+ | 'isAddon'
32
33
  ;
33
34
 
34
35
 
package/src/main.js ADDED
@@ -0,0 +1,7 @@
1
+ const {eyesStorybookEventEmitter} = require('./eyesStorybookEventEmitter');
2
+ const {getConfigAndLogger} = require('./getConfigAndLogger');
3
+
4
+ module.exports = {
5
+ eyesStorybookEventEmitter,
6
+ getConfigAndLogger,
7
+ };
@@ -2,6 +2,7 @@
2
2
  const getStoryUrl = require('./getStoryUrl');
3
3
  const getStoryBaselineName = require('./getStoryBaselineName');
4
4
  const ora = require('ora');
5
+ const {EventEmitter} = require('node:events');
5
6
  const {presult} = require('@applitools/functional-commons');
6
7
 
7
8
  function makeRenderStories({
@@ -13,19 +14,23 @@ function makeRenderStories({
13
14
  stream,
14
15
  sanityCheckForPage,
15
16
  maxPageTTL = 60000,
17
+ eventEmitter = new EventEmitter(),
18
+ signal = new AbortController().signal,
16
19
  }) {
17
20
  let newPageIdToAdd;
18
21
 
19
22
  return async function renderStories(stories, isIE) {
20
23
  let doneStories = 0;
24
+ const totalStories = stories.length;
21
25
  const allTestResults = [];
22
26
  let allStoriesPromise = Promise.resolve();
23
27
  let currIndex = 0;
24
28
 
25
29
  const spinner = ora({
26
- text: updateSpinnerText(0, stories.length),
30
+ text: updateSpinnerText(0, totalStories),
27
31
  stream,
28
32
  });
33
+ eventEmitter.emit('progress', {doneStories, totalStories});
29
34
  spinner.start();
30
35
  prepareNewPage();
31
36
 
@@ -35,7 +40,16 @@ function makeRenderStories({
35
40
  return allTestResults;
36
41
 
37
42
  async function processStoryLoop() {
38
- if (currIndex === stories.length) return;
43
+ if (currIndex === totalStories) return;
44
+
45
+ if (signal.aborted) {
46
+ const story = stories[currIndex++];
47
+ const title = getStoryBaselineName(story);
48
+ logger.log('aborting story before processing', title);
49
+ onDoneStory(new Error(`${title} aborted before processing ${signal.reason}`), story);
50
+ return processStoryLoop();
51
+ }
52
+
39
53
  const {page, pageId, markPageAsFree, removePage, getCreatedAt} = await pagePool.getFreePage();
40
54
  const livedTime = Date.now() - getCreatedAt();
41
55
  logger.log(`[prepareNewPage] got free page: ${pageId}, lived time: ${livedTime}`);
@@ -103,6 +117,12 @@ function makeRenderStories({
103
117
  const errMsg = `[page ${pageId}] Failed to get story data for "${title}". ${error}`;
104
118
  logger.log(errMsg);
105
119
  }
120
+
121
+ if (signal.aborted) {
122
+ logger.log('aborting story before open', title);
123
+ return onDoneStory(new Error(`${title} aborted before open ${signal.reason}`), story);
124
+ }
125
+
106
126
  const testResults = await renderStory({
107
127
  snapshots: storyData,
108
128
  url: storyUrl,
@@ -132,10 +152,14 @@ function makeRenderStories({
132
152
  }
133
153
 
134
154
  function onDoneStory(resultsOrErr, story) {
135
- spinner.text = updateSpinnerText(++doneStories, stories.length, story.config);
155
+ spinner.text = updateSpinnerText(++doneStories, totalStories, story.config);
136
156
  const title = getStoryBaselineName(story);
137
- allTestResults.push({title, resultsOrErr});
138
- return {title, resultsOrErr};
157
+ const result = {title, resultsOrErr, story};
158
+ allTestResults.push(result);
159
+
160
+ eventEmitter.emit('progress', {doneStories, totalStories});
161
+
162
+ return result;
139
163
  }
140
164
 
141
165
  async function prepareNewPage() {
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
+
2
3
  const throat = require('throat');
4
+ const {EyesError} = require('@applitools/eyes');
3
5
  const {storyToCheckSettings, storyToOpenSettings} = require('./transformSettings');
4
6
 
5
7
  function makeRenderStory({
@@ -11,6 +13,7 @@ function makeRenderStory({
11
13
  appName,
12
14
  serverSettings,
13
15
  concurrency,
16
+ signal = new AbortController().signal,
14
17
  }) {
15
18
  const throttle = throat(storyDataGap);
16
19
  return function renderStory({story, snapshots, url}) {
@@ -24,7 +27,12 @@ function makeRenderStory({
24
27
 
25
28
  return timeItAsync(baselineName, async () => {
26
29
  const eyes = await openEyes({settings: openParams});
27
- return new Promise((resolve, reject) => {
30
+ return new Promise(async (resolve, reject) => {
31
+ if (signal.aborted) {
32
+ return await abortStory();
33
+ } else {
34
+ signal.addEventListener('abort', abortStory);
35
+ }
28
36
  throttle(async () => {
29
37
  try {
30
38
  if (snapshots) {
@@ -44,6 +52,18 @@ function makeRenderStory({
44
52
  reject(err);
45
53
  }
46
54
  });
55
+
56
+ async function abortStory() {
57
+ logger.log('received abort signal for story', title);
58
+
59
+ // Inside core-base this will cause internal operations to be aborted
60
+ await eyes.abort({settings: {environments: checkParams.environments}});
61
+
62
+ // This will intentionally cause not to wait for results.
63
+ // Therefore there will be a "hanging" promise.
64
+ // But for the purpose of the addon, which is a long living process, it doesn't matter that we didn't stop the operation inside core.
65
+ reject(new EyesError(`${title} aborted after open ${signal.reason}`));
66
+ }
47
67
  });
48
68
  }).then(onDoneStory);
49
69
 
@@ -104,7 +104,6 @@ function storyToCheckSettings({story, url}) {
104
104
  useDom,
105
105
  enablePatterns,
106
106
  ignoreDisplacements,
107
- fully,
108
107
  ignoreCaret,
109
108
  matchLevel,
110
109
  accessibilitySettings: accessibilityValidation
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const detect = require('detect-port');
5
5
  const {version: packageVersion} = require('../package.json');
6
6
  const {
7
- missingApiKeyFailMsg,
7
+ MissingApiKeyError,
8
8
  missingAppNameAndPackageJsonFailMsg,
9
9
  missingAppNameInPackageJsonFailMsg,
10
10
  startStorybookFailMsg,
@@ -18,7 +18,7 @@ const utils = require('@applitools/utils');
18
18
 
19
19
  async function validateAndPopulateConfig({config, packagePath = '', logger = makeLogger()}) {
20
20
  if (!config.apiKey && !utils.general.getEnvValue('API_KEY')) {
21
- throw new Error(missingApiKeyFailMsg);
21
+ throw new MissingApiKeyError();
22
22
  }
23
23
 
24
24
  const packageJsonPath = `${packagePath}/package.json`;
@@ -62,7 +62,8 @@ async function validateAndPopulateConfig({config, packagePath = '', logger = mak
62
62
  }
63
63
  }
64
64
 
65
- config.agentId = `eyes-storybook/${packageVersion}`;
65
+ const agentName = config.isAddon ? 'eyes-storybook-addon' : 'eyes-storybook';
66
+ config.agentId = `${agentName}/${packageVersion}`;
66
67
 
67
68
  if (config.runInDocker) {
68
69
  config.puppeteerOptions = config.puppeteerOptions || {};