@applitools/eyes-storybook 3.26.1 → 3.27.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
@@ -3,6 +3,11 @@
3
3
  ## Unreleased
4
4
 
5
5
 
6
+ ## 3.27.0 - 2021/12/6
7
+
8
+ - support custom properties and query params
9
+ - remove `queryParams` api from story and global config
10
+
6
11
  ## 3.26.1 - 2021/11/28
7
12
 
8
13
  - add custom test properties to stories with query params
package/README.md CHANGED
@@ -28,7 +28,6 @@ Applitools Eyes SDK for [Storybook](http://storybook.js.org).
28
28
  - [global](#global)
29
29
  - [component](#component)
30
30
  - [`variations`](#variations)
31
- - [`queryParams`](#queryparams)
32
31
  - [`waitBeforeCapture`](#waitbeforecapture)
33
32
  - [`properties`](#properties)
34
33
  - [`ignoreRegions`](#ignoreregions)
@@ -181,7 +180,6 @@ In addition to command-line arguments, it's possible to define the following con
181
180
  | `waitBeforeCapture` | undefined | Selector, function or timeout.<br/>If ```number``` then the argument is treated as time in milliseconds to wait before all screenshots.<br/>If ```string``` then the argument is treated as a selector for elements to wait for before all screenshots.<br/>If ```function```, then the argument is treated as a predicate to wait for before all screenshots.<br/><hr/>For per component configuration see [waitBeforeCapture.](#waitBeforeCapture)<br/>Note that we use Puppeteer's [page.waitForTimeout()](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pagewaitfortimeoutmilliseconds), [page.waitForSelector()](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pagewaitforselectorselector-options), [page.waitForXPath()](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pagewaitforxpathxpath-options) and [page.waitForFunction()](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pagewaitforfunctionpagefunction-options-args), checkout it's API for more details. |
182
181
  | `include` | true | A predicate function, a string or a regular expression specifying which stories should be visually tested.<br/>Visual baselines will be created only for the components specified.<br/>The function receives an object with ```name```, ```kind```, ```storyTitle``` and ```parameters``` properties.<br/>For example (exclude all stories with a name that start with [SKIP]):<br/>```({name, kind, storyTitle, parameters}) => !/^\[SKIP\]/.test(name)```<br/>For more information, see [per component configuration - include](#include). |
183
182
  | `variations` | undefined | Specifies additional variations for all or some of the stories. For example, RTL. For more information, see [per component configuration - variations](#variations).|
184
- | `queryParam` | undefined | Specifies additional query parameters for all or some of the stories. For more information, see [per component configuration - queryParams](#queryParams).|
185
183
  | `notifyOnCompletion` | false | If `true` batch completion notifications are sent. |
186
184
  | `dontCloseBatches` | false | If true, batches are not closed for notifyOnCompletion.|
187
185
  | `testConcurrency` | 5 | The maximum number of tests that can run concurrently. The default value is the allowed amount for free accounts. For paid accounts, set this number to the quota set for your account. |
@@ -425,10 +423,9 @@ storiesOf('Some kind', module)
425
423
 
426
424
  ### `variations`
427
425
 
428
- An array of string values, which specifies which variations to add for this story. For each value, an additional visual test will be executed for the component. It will have the same name only with a `[<variation name>]` suffix, and when the component is loaded, the URL will have an additional param: `eyes-variation=<variation name>`.
426
+ An array of object values, which specifies which variations to add for this story. For each value, an additional visual test will be executed for the component. Each variation could contain `queryParams` and `properties` fields to specifies custom query parameters and eyes properties respectfully. Variation tests will have the same name.
429
427
 
430
- This can accommodate many use cases, for example RTL (right to left).
431
- It's now possible for the component to render its variation version when the relevant URL param is present. For Example, here's a storybook that handles an RTL variation:
428
+ This can accommodate many use cases, for example `@storybook/addon-contexts`. With addons like this it is possible to render components in a different way depends on query parameters in URL. For Example, here's a storybook that handles an RTL variation:
432
429
 
433
430
  ```js
434
431
  const isRTL = new URL(window.location).searchParams.get('eyes-variation') === 'RTL';
@@ -445,17 +442,10 @@ storiesOf('Components that support RTL', module)
445
442
  <span>I am visually perfect!</span>
446
443
  <span>{isRTL ? ' and rendered right to left as well :)' : ''}</span>
447
444
  </div>,
448
- {eyes: {variations: ['RTL']}}
445
+ {eyes: {variations: [{queryParams: {'eyes-variation': 'RTL'}, properties: {name: 'isRTL', value: 'true'}, {properties: {name: 'isRTL', value: 'false'}}]}}
449
446
  )
450
447
  ```
451
448
 
452
- ### `queryParams`
453
-
454
- An array of object values, which specifies `name` and `value` of query parameter which will be added to the story. For each value, an additional visual test will be executed for the component. It will have the same name display name, but still generate a different baseline.
455
-
456
- This can accommodate many use cases, for example `@storybook/addon-contexts`.
457
- With addons like this it is possible to render components in a different way depends on query parameters in URL.
458
-
459
449
  ### `waitBeforeCapture`
460
450
 
461
451
  Selector or timeout, see [advanced configuration](#advanced-configuration) for more details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/eyes-storybook",
3
- "version": "3.26.1",
3
+ "version": "3.27.0",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=8.6.0"
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ function addVariationStories({stories, config}) {
4
+ const defaultStories = [];
5
+ const variationStories = {};
6
+ for (const story of stories) {
7
+ const variations = getStoryVariations(story, config);
8
+ if (variations) {
9
+ for (const variation of variations) {
10
+ const variationKey = stringifyVariation(variation);
11
+ const variationStoriesArray =
12
+ variationStories[variationKey] || (variationStories[variationKey] = []);
13
+ const variationStory = {...story};
14
+ if (variation.queryParams || variation.properties) {
15
+ variationStory.parameters = addVariation(story.parameters, variation);
16
+ }
17
+ variationStoriesArray.push(variationStory);
18
+ }
19
+ } else {
20
+ defaultStories.push(story);
21
+ }
22
+ }
23
+
24
+ return Object.values(variationStories).reduce((allStories, variationStories) => {
25
+ return allStories.concat(variationStories);
26
+ }, defaultStories);
27
+ }
28
+
29
+ function getStoryVariations(story, config) {
30
+ if (story.parameters && story.parameters.eyes && story.parameters.eyes.variations) {
31
+ if (!Array.isArray(story.parameters.eyes.variations)) {
32
+ throw new Error('variations should be an array');
33
+ }
34
+ return normalizeVariations(story.parameters.eyes.variations);
35
+ }
36
+
37
+ if (config.variations) {
38
+ if (Array.isArray(config.variations)) {
39
+ return normalizeVariations(config.variations);
40
+ } else if (typeof config.variations === 'function') {
41
+ const variations = config.variations(story);
42
+ if (variations) {
43
+ if (!Array.isArray(variations)) {
44
+ throw new Error('global variations should be a function that returns array');
45
+ }
46
+ return normalizeVariations(variations);
47
+ }
48
+ } else {
49
+ throw new Error('global variations should be an array or a function that returns array');
50
+ }
51
+ }
52
+ }
53
+
54
+ function normalizeVariations(variations) {
55
+ let shouldHasDefault = false; // default variation is one without query params
56
+ const normalizedVariations = variations.map(variation => {
57
+ if (typeof variation === 'string') {
58
+ shouldHasDefault = true;
59
+ return {
60
+ queryParams: {'eyes-variation': variation},
61
+ properties: [{name: 'eyes-variation', value: variation}],
62
+ };
63
+ }
64
+ if (variation.queryParams && (!variation.properties || variation.properties.length === 0)) {
65
+ return {
66
+ ...variation,
67
+ properties: Object.entries(variation.queryParams).map(([name, value]) => ({name, value})),
68
+ };
69
+ }
70
+ return variation;
71
+ });
72
+
73
+ // if it should has default variation and it is not already there
74
+ if (shouldHasDefault && !normalizedVariations.some(variation => !variation.queryParams)) {
75
+ normalizedVariations.unshift({});
76
+ }
77
+
78
+ return normalizedVariations;
79
+ }
80
+
81
+ function stringifyVariation(variation) {
82
+ if (!variation.queryParams) return '';
83
+ return Object.keys(variation.queryParams)
84
+ .sort()
85
+ .map(name => `${name}=${variation.queryParams[name]}`)
86
+ .join('&');
87
+ }
88
+
89
+ function addVariation(parameters, variation) {
90
+ parameters = {...parameters};
91
+ parameters.eyes = {...parameters.eyes};
92
+ if (variation.queryParams) {
93
+ parameters.eyes.queryParams = variation.queryParams;
94
+ }
95
+ if (variation.properties) {
96
+ parameters.eyes.properties = [
97
+ ...(parameters.eyes.properties || []),
98
+ ...(variation.properties || []),
99
+ ];
100
+ }
101
+ return parameters;
102
+ }
103
+
104
+ module.exports = addVariationStories;
@@ -10,7 +10,7 @@ const makeRenderStories = require('./renderStories');
10
10
  const makeGetStoryData = require('./getStoryData');
11
11
  const ora = require('ora');
12
12
  const filterStories = require('./filterStories');
13
- const addParametrizedStories = require('./addParametrizedStories');
13
+ const addVariationStories = require('./addVariationStories');
14
14
  const browserLog = require('./browserLog');
15
15
  const memoryLog = require('./memoryLog');
16
16
  const getIframeUrl = require('./getIframeUrl');
@@ -116,12 +116,12 @@ async function eyesStorybook({
116
116
  );
117
117
 
118
118
  const filteredStories = filterStories({stories, config});
119
- const storiesIncludingParametrizedVariations = addParametrizedStories({
119
+ const storiesIncludingVariations = addVariationStories({
120
120
  stories: filteredStories,
121
121
  config,
122
122
  });
123
123
 
124
- logger.log(`starting to run ${storiesIncludingParametrizedVariations.length} stories`);
124
+ logger.log(`starting to run ${storiesIncludingVariations.length} stories`);
125
125
 
126
126
  const getStoryData = makeGetStoryData({
127
127
  logger,
@@ -158,7 +158,7 @@ async function eyesStorybook({
158
158
  setRenderIE,
159
159
  setTransitioningIntoIE,
160
160
  configs,
161
- stories: storiesIncludingParametrizedVariations,
161
+ stories: storiesIncludingVariations,
162
162
  pagePool,
163
163
  logger,
164
164
  timeItAsync,
@@ -3,11 +3,17 @@ const getStoryTitle = require('./getStoryTitle');
3
3
 
4
4
  function getStoryBaselineName({name, kind, parameters}) {
5
5
  const storyTitle = getStoryTitle({name, kind, parameters});
6
- const queryParam = parameters && parameters.eyes && parameters.eyes.queryParam;
6
+ const queryParams = parameters && parameters.eyes && parameters.eyes.queryParams;
7
+ if (!queryParams) return storyTitle;
8
+ const customQueryParams = {...queryParams};
9
+ delete customQueryParams['eyes-variation'];
10
+ if (Object.keys(customQueryParams).length === 0) return storyTitle;
7
11
 
8
- if (!queryParam || queryParam.name === 'eyes-variation') return storyTitle;
12
+ const queryParamsString = Object.entries(customQueryParams)
13
+ .map(([name, value]) => `${name}=${value}`)
14
+ .join('&');
9
15
 
10
- return `${storyTitle} [${queryParam.name}=${queryParam.value}]`;
16
+ return `${storyTitle} [${queryParamsString}]`;
11
17
  }
12
18
 
13
19
  module.exports = getStoryBaselineName;
@@ -15,19 +15,9 @@ function makeGetStoryData({logger, takeDomSnapshots, waitBeforeCapture, reloadPa
15
15
 
16
16
  const eyesParameters = story.parameters && story.parameters.eyes;
17
17
  if (story.isApi && !reloadPagePerStory) {
18
- const expectedQueryParam = eyesParameters ? eyesParameters.queryParam : undefined;
19
- const actualQueryParam = await getQueryParam(
20
- eyesParameters && eyesParameters.queryParams
21
- ? eyesParameters.queryParams.map(queryParam => queryParam.name)
22
- : undefined,
23
- );
24
- if (
25
- (!actualQueryParam && !expectedQueryParam) ||
26
- (actualQueryParam &&
27
- expectedQueryParam &&
28
- actualQueryParam.value === expectedQueryParam.name &&
29
- actualQueryParam.value === expectedQueryParam.value)
30
- ) {
18
+ const currentUrl = page.url();
19
+ const expectedQueryParams = eyesParameters ? eyesParameters.queryParams : undefined;
20
+ if (urlQueryParamsEquals(currentUrl, expectedQueryParams)) {
31
21
  const err = await page.evaluate(renderStoryWithClientAPI, story.index);
32
22
  err && handleRenderStoryError(err);
33
23
  } else {
@@ -84,19 +74,28 @@ function makeGetStoryData({logger, takeDomSnapshots, waitBeforeCapture, reloadPa
84
74
  }
85
75
  }
86
76
 
87
- async function getQueryParam(queryParamNames = []) {
77
+ function urlQueryParamsEquals(url, expectedQueryParams) {
88
78
  try {
89
- const url = new URL(await page.url());
90
- queryParamNames = [...queryParamNames, 'eyes-variation'];
91
- const queryParamName = queryParamNames.find(queryParamName =>
92
- url.searchParams.has(queryParamName),
93
- );
94
- if (queryParamName) {
95
- return {name: queryParamName, value: url.searchParams.get(queryParamName)};
79
+ url = new URL(url);
80
+ } catch (err) {
81
+ logger.error('Error during parsing page url', err);
82
+ return false;
83
+ }
84
+
85
+ expectedQueryParams = expectedQueryParams || {};
86
+ const expectedQueryParamNames = Object.keys(expectedQueryParams);
87
+ const actualQueryParamNames = [];
88
+ for (const [name, value] of url.searchParams) {
89
+ if (['eyes-storybook', 'selectedKind', 'selectedStory'].includes(name)) continue;
90
+ if (!expectedQueryParams.hasOwnProperty(name) || !expectedQueryParams[name] === value) {
91
+ return false;
96
92
  }
97
- } catch (ex) {
98
- logger.log('failed to get url from page (in need of eyes-variation param)');
93
+ actualQueryParamNames.push(name);
99
94
  }
95
+
96
+ if (actualQueryParamNames.length !== expectedQueryParamNames.length) return false;
97
+
98
+ return true;
100
99
  }
101
100
 
102
101
  // TODO (amit): handle this error in the caller (probably renderStories)
@@ -1,11 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  function getStoryTitle({name, kind, parameters}) {
4
- const queryParam = parameters && parameters.eyes && parameters.eyes.queryParam;
5
- const variation =
6
- queryParam && queryParam.name === 'eyes-variation' ? queryParam.value : undefined;
4
+ const queryParams = (parameters && parameters.eyes && parameters.eyes.queryParams) || {};
5
+ const eyesVariation = queryParams['eyes-variation'];
7
6
 
8
- return `${kind}: ${name}${variation ? ` [${variation}]` : ''}`;
7
+ return `${kind}: ${name}${eyesVariation ? ` [${eyesVariation}]` : ''}`;
9
8
  }
10
9
 
11
10
  module.exports = getStoryTitle;
@@ -2,12 +2,15 @@
2
2
  const getIframeUrl = require('./getIframeUrl');
3
3
 
4
4
  function getStoryUrl({name, kind, parameters}, baseUrl) {
5
- const queryParam = parameters && parameters.eyes && parameters.eyes.queryParam;
6
- const queryParamString = queryParam ? `&${queryParam.name}=${queryParam.value}` : '';
5
+ const queryParams = (parameters && parameters.eyes && parameters.eyes.queryParams) || {};
6
+ const queryString = Object.entries(queryParams).reduce(
7
+ (queryString, [name, value]) => queryString + `&${name}=${value}`,
8
+ '',
9
+ );
7
10
 
8
11
  return `${getIframeUrl(baseUrl)}&selectedKind=${encodeURIComponent(
9
12
  kind,
10
- )}&selectedStory=${encodeURIComponent(name)}${queryParamString}`;
13
+ )}&selectedStory=${encodeURIComponent(name)}${queryString}`;
11
14
  }
12
15
 
13
16
  module.exports = getStoryUrl;
@@ -8,7 +8,12 @@ function makeRenderStory({logger, testWindow, performance, timeItAsync}) {
8
8
  const {name, kind, parameters} = story;
9
9
  const baselineName = getStoryBaselineName({name, kind, parameters});
10
10
  const title = getStoryTitle({name, kind, parameters});
11
- const eyesOptions = Object.assign({}, config, (parameters && parameters.eyes) || {});
11
+ const eyesParameters = (parameters && parameters.eyes) || {};
12
+ const eyesOptions = {
13
+ ...config,
14
+ ...eyesParameters,
15
+ properties: [...(config.properties || []), ...(eyesParameters.properties || [])],
16
+ };
12
17
  const {
13
18
  ignoreDisplacements,
14
19
  ignoreRegions,
@@ -31,7 +36,6 @@ function makeRenderStory({logger, testWindow, performance, timeItAsync}) {
31
36
  visualGridOptions,
32
37
  useDom,
33
38
  enablePatterns,
34
- queryParam,
35
39
  } = eyesOptions;
36
40
 
37
41
  if (sizeMode) {
@@ -52,10 +56,6 @@ function makeRenderStory({logger, testWindow, performance, timeItAsync}) {
52
56
  ...(properties || []),
53
57
  ];
54
58
 
55
- if (queryParam) {
56
- storyProperties.push(queryParam);
57
- }
58
-
59
59
  const openParams = {
60
60
  testName: baselineName,
61
61
  displayName: title,
@@ -1,81 +0,0 @@
1
- 'use strict';
2
-
3
- function addParametrizedStories({stories, config}) {
4
- const parametrizedStories = {};
5
- for (const story of stories) {
6
- const queryParams = getStoryQueryParams(story, config);
7
- if (!queryParams) continue;
8
- for (const queryParam of queryParams) {
9
- const queryParamString = `${queryParam.name}=${queryParam.value}`;
10
- const parametrizeStoriesArray =
11
- parametrizedStories[queryParamString] || (parametrizedStories[queryParamString] = []);
12
- parametrizeStoriesArray.push({
13
- ...story,
14
- parameters: addQueryParam(story.parameters, queryParam),
15
- });
16
- }
17
- }
18
-
19
- return Object.values(parametrizedStories).reduce((allStories, parametrizedStories) => {
20
- return allStories.concat(parametrizedStories);
21
- }, stories);
22
- }
23
-
24
- function getStoryQueryParams(story, config) {
25
- let queryParams;
26
- if (story.parameters && story.parameters.eyes && story.parameters.eyes.queryParams) {
27
- if (!Array.isArray(story.parameters.eyes.queryParams)) {
28
- throw new Error('queryParams should be an array');
29
- }
30
- queryParams = [...story.parameters.eyes.queryParams];
31
- }
32
- if (story.parameters && story.parameters.eyes && story.parameters.eyes.variations) {
33
- if (!Array.isArray(story.parameters.eyes.variations)) {
34
- throw new Error('variations should be an array');
35
- }
36
- queryParams = [
37
- ...(queryParams || []),
38
- ...variationsToQueryParams(story.parameters.eyes.variations),
39
- ];
40
- }
41
-
42
- if (queryParams) return queryParams;
43
-
44
- if (config.queryParams) {
45
- if (!Array.isArray(config.queryParams)) {
46
- throw new Error('global queryParams should be an array');
47
- }
48
- queryParams = [...config.queryParams];
49
- }
50
-
51
- if (config.variations) {
52
- if (Array.isArray(config.variations)) {
53
- queryParams = [...(queryParams || []), ...variationsToQueryParams(config.variations)];
54
- } else if (typeof config.variations === 'function') {
55
- const variations = config.variations(story);
56
- if (variations) {
57
- if (!Array.isArray(variations)) {
58
- throw new Error('global variations should be a function that returns array');
59
- }
60
- queryParams = [...(queryParams || []), ...variationsToQueryParams(variations)];
61
- }
62
- } else {
63
- throw new Error('global variations should be an array or a function that returns array');
64
- }
65
- }
66
-
67
- if (queryParams) return queryParams;
68
- }
69
-
70
- function variationsToQueryParams(variations) {
71
- return variations.map(variation => ({name: 'eyes-variation', value: variation}));
72
- }
73
-
74
- function addQueryParam(parameters, queryParam) {
75
- parameters = {...parameters};
76
- parameters.eyes = {...parameters.eyes};
77
- parameters.eyes.queryParam = queryParam;
78
- return parameters;
79
- }
80
-
81
- module.exports = addParametrizedStories;