@wdio/visual-service 3.1.0 β†’ 4.0.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,117 @@
1
1
  # @wdio/visual-service
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - b717d9a: # πŸ’₯ Breaking changes
8
+
9
+ - the new element screenshot is producing "smaller" screenshots on certain Android OS versions (not all), but it's more "accurate" so we accept this
10
+
11
+ # πŸš€ New Features
12
+
13
+ ## Add StoryBookπŸ“– support
14
+
15
+ Automatically scan local/remote storybook instances to create element screenshots of each component by adding
16
+
17
+ ```ts
18
+ export const config: Options.Testrunner = {
19
+ // ...
20
+ services: ["visual"],
21
+ // ....
22
+ };
23
+ ```
24
+
25
+ to your `services` and running `npx wdio tests/configs/wdio.local.desktop.storybook.conf.ts --storybook` through the command line.
26
+ It will automatically use Chrome. The following options can be provided through the command line
27
+
28
+ - `--headless`, defaults to `true`
29
+ - `--numShards {number}`, this will be the amount of parallel instances that will be used to run the stories. This will be limited by the `maxInstances` in your `wdio.conf`-file. When running in `headless`-mode then do not increase the number to more than 20 to prevent flakiness
30
+ - `--clip {boolean}`, try to take an element instead of a viewport screenshot, defaults to `true`
31
+ - `--clipSelector {string}`, this is the selector that will be used to:
32
+
33
+ - select the element to take the screenshot of
34
+ - the element to wait for to be visible before a screenshot is taken
35
+
36
+ defaults to `#storybook-root > :first-child` for V7 and `#root > :first-child:not(script):not(style)` for V6
37
+
38
+ - `--version`, the version of storybook, defaults to 7. This is needed to know if the V6 `clipSelector` needs to be used.
39
+ - `--browsers {edge,chrome,safari,firefox}`, defaults to Chrome
40
+ - `--skipStories`, this can be:
41
+ - a string (`example-button--secondary,example-button--small`)
42
+ - or a regex (`"/.*button.*/gm"`) to skip certain stories
43
+
44
+ You can also provide service options
45
+
46
+ ```ts
47
+ export const config: Options.Testrunner = {
48
+ // ...
49
+ services: [
50
+ [
51
+ 'visual',
52
+ {
53
+ // Some default options
54
+ baselineFolder: join(process.cwd(), './__snapshots__/'),
55
+ debug: true,
56
+ // The storybook options
57
+ storybook: {
58
+ clip: false,
59
+ clipSelector: ''#some-id,
60
+ numShards: 4,
61
+ skipStories: ['example-button--secondary', 'example-button--small'],
62
+ url: 'https://www.bbc.co.uk/iplayer/storybook/',
63
+ version: 6,
64
+ },
65
+ },
66
+ ],
67
+ ],
68
+ // ....
69
+ }
70
+ ```
71
+
72
+ The baseline images will be stored in the following structure:
73
+
74
+ ```log
75
+ {projectRoot}
76
+ |_`__snapshots__`
77
+ |_`{category}`
78
+ |_`{componentName}`
79
+ |_{browserName}
80
+ |_`{{component-id}-element-{browser}-{resolution}-dpr-{dprValue}}.png`
81
+ ```
82
+
83
+ which will look like this
84
+
85
+ ![image](https://github.com/webdriverio/visual-testing/assets/11979740/7c41a8b4-2498-4e85-be11-cb1ec601b760)
86
+
87
+ > [!NOTE]
88
+ > Storybook 6.5 or higher is supported
89
+
90
+ # πŸ’… Polish
91
+
92
+ - `hideScrollBars` is disabled by default when using the Storybook runner
93
+ - By default, all element screenshots in the browser, except for iOS, will use the native method to take element screenshots. This will make taking an element screenshot more than 5% faster. If it fails it will fall back to the "viewport" screenshot and create a cropped element screenshot.
94
+ - Taking an element screenshot becomes 70% faster due to removing the fixed scroll delay of 500ms and changing the default scrolling behaviour to an instant scroll
95
+ - refactor web element screenshots and update the screenshots
96
+ - added more UTs to increase the coverage
97
+
98
+ # πŸ› Bug Fixes
99
+
100
+ - When the element has no height or width, we default to the viewport screen size to prevent not cropping any screenshot. An error like below will be logged in red
101
+
102
+ ```logs
103
+
104
+ The element has no width or height. We defaulted to the viewport screen size of width: ${width} and height: ${height}.
105
+
106
+ ```
107
+
108
+ - There were cases where element screenshots were automatically rotated which was not intended
109
+
110
+ ### Patch Changes
111
+
112
+ - Updated dependencies [b717d9a]
113
+ - webdriver-image-comparison@5.0.0
114
+
3
115
  ## 3.1.0
4
116
 
5
117
  ### Minor Changes
@@ -0,0 +1,4 @@
1
+ export declare const V6_CLIP_SELECTOR = "#root > :first-child:not(script):not(style)";
2
+ export declare const CLIP_SELECTOR = "#storybook-root > :first-child:not(script):not(style)";
3
+ export declare const NUM_SHARDS = 1;
4
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,gDAAgD,CAAA;AAC7E,eAAO,MAAM,aAAa,0DAA0D,CAAA;AACpF,eAAO,MAAM,UAAU,IAAI,CAAA"}
@@ -0,0 +1,3 @@
1
+ export const V6_CLIP_SELECTOR = '#root > :first-child:not(script):not(style)';
2
+ export const CLIP_SELECTOR = '#storybook-root > :first-child:not(script):not(style)';
3
+ export const NUM_SHARDS = 1;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /// <reference types="./expect-webdriverio.js" />
2
2
  import type { WicElement } from 'webdriver-image-comparison/dist/commands/element.interfaces.js';
3
3
  import WdioImageComparisonService from './service.js';
4
+ import VisualLauncher from './storybook/launcher.js';
4
5
  import type { Output, Result, WdioCheckFullPageMethodOptions, WdioSaveFullPageMethodOptions, WdioSaveElementMethodOptions, WdioSaveScreenMethodOptions, WdioCheckElementMethodOptions, WdioCheckScreenMethodOptions } from './types.js';
5
6
  declare global {
6
7
  namespace WebdriverIO {
@@ -84,4 +85,5 @@ declare global {
84
85
  }
85
86
  }
86
87
  export default WdioImageComparisonService;
88
+ export declare const launcher: typeof VisualLauncher;
87
89
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  import WdioImageComparisonService from './service.js';
2
+ import VisualLauncher from './storybook/launcher.js';
2
3
  export default WdioImageComparisonService;
4
+ export const launcher = VisualLauncher;
package/dist/matcher.js CHANGED
@@ -2,7 +2,7 @@ import { getBrowserObject } from './utils.js';
2
2
  const DEFAULT_EXPECTED_RESULT = 0;
3
3
  const asymmetricMatcher = typeof Symbol === 'function' && Symbol.for
4
4
  ? Symbol.for('jest.asymmetricMatcher')
5
- : 1267621;
5
+ : 0x13_57_a5;
6
6
  function isAsymmetricMatcher(expected) {
7
7
  return Boolean(expected && typeof expected === 'object' && '$$typeof' in expected && expected.$$typeof === asymmetricMatcher && 'asymmetricMatch' in expected);
8
8
  }
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,aAAa,CAAA;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC9D,OAAO,EACH,SAAS,EAUZ,MAAM,4BAA4B,CAAA;AAqBnC,MAAM,CAAC,OAAO,OAAO,0BAA2B,SAAQ,SAAS;;IAI7D,OAAO,CAAC,QAAQ,CAAC,CAAsD;IACvE,OAAO,CAAC,gBAAgB,CAAqB;gBAEjC,OAAO,EAAE,YAAY,EAAE,CAAC,EAAE,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM;IAM1F;;OAEG;IACG,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,kBAAkB;IAIzE,MAAM,CACR,YAAY,EAAE,WAAW,CAAC,YAAY,EACtC,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,kBAAkB;IA4BjE,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI;IAKhC,YAAY,CAAE,WAAW,EAAC,MAAM,EAAE,KAAK,EAAC,MAAM,EAAE,EAAE,MAAM,EAAC,MAAM,GAAC,MAAM,EAAE,KAAK,EAAC,GAAG;CA+HpF"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAgB,UAAU,EAAE,MAAM,aAAa,CAAA;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC9D,OAAO,EACH,SAAS,EAUZ,MAAM,4BAA4B,CAAA;AAqBnC,MAAM,CAAC,OAAO,OAAO,0BAA2B,SAAQ,SAAS;;IAI7D,OAAO,CAAC,QAAQ,CAAC,CAAsD;IACvE,OAAO,CAAC,gBAAgB,CAAqB;gBAEjC,OAAO,EAAE,YAAY,EAAE,CAAC,EAAE,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM;IAM1F;;OAEG;IACG,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,kBAAkB;IAIzE,MAAM,CACR,YAAY,EAAE,WAAW,CAAC,YAAY,EACtC,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,kBAAkB;IA4BjE,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI;IAKhC,YAAY,CAAE,WAAW,EAAC,MAAM,EAAE,KAAK,EAAC,MAAM,EAAE,EAAE,MAAM,EAAC,MAAM,GAAC,MAAM,EAAE,KAAK,EAAC,GAAG;CAgIpF"}
package/dist/service.js CHANGED
@@ -127,6 +127,7 @@ export default class WdioImageComparisonService extends BaseClass {
127
127
  },
128
128
  getElementRect: this.getElementRect.bind(currentBrowser),
129
129
  screenShot: this.takeScreenshot.bind(currentBrowser),
130
+ takeElementScreenshot: this.takeElementScreenshot.bind(currentBrowser),
130
131
  }, instanceData, getFolders(elementOptions, self.folders, self.#getBaselineFolder()), element, tag, {
131
132
  wic: self.defaultOptions,
132
133
  method: elementOptions,
@@ -0,0 +1,80 @@
1
+ import type { Logger } from '@wdio/logger';
2
+ import type { RemoteCapability } from 'node_modules/@wdio/types/build/Capabilities.js';
3
+ import type { Folders } from 'webdriver-image-comparison';
4
+ export interface StorybookData {
5
+ id: string;
6
+ title: string;
7
+ name: string;
8
+ importPath: string;
9
+ tags: string[];
10
+ storiesImports?: string[];
11
+ type?: 'docs' | 'story';
12
+ kind?: string;
13
+ story?: string;
14
+ parameters?: {
15
+ __id: string;
16
+ docsOnly: boolean;
17
+ fileName: string;
18
+ };
19
+ }
20
+ export interface IndexRes {
21
+ v: number;
22
+ entries: {
23
+ [key: string]: StorybookData;
24
+ };
25
+ }
26
+ export interface StoriesRes {
27
+ v: number;
28
+ stories: {
29
+ [key: string]: StorybookData;
30
+ };
31
+ }
32
+ export type Stories = {
33
+ [key: string]: StorybookData;
34
+ };
35
+ export type CreateTestFileOptions = {
36
+ clip: boolean;
37
+ clipSelector: string;
38
+ directoryPath: string;
39
+ folders: Folders;
40
+ framework: string;
41
+ numShards: number;
42
+ log: Logger;
43
+ skipStories: string[] | RegExp;
44
+ storiesJson: Stories;
45
+ storybookUrl: string;
46
+ };
47
+ export interface CapabilityMap {
48
+ chrome: RemoteCapability;
49
+ firefox: RemoteCapability;
50
+ safari: RemoteCapability;
51
+ edge: RemoteCapability;
52
+ }
53
+ export type CreateTestContent = {
54
+ clip: boolean;
55
+ clipSelector: string;
56
+ folders: Folders;
57
+ framework: string;
58
+ skipStories: string[] | RegExp;
59
+ stories: StorybookData[];
60
+ storybookUrl: string;
61
+ };
62
+ export type CreateItContent = {
63
+ clip: boolean;
64
+ clipSelector: string;
65
+ folders: Folders;
66
+ framework: string;
67
+ skipStories: string[] | RegExp;
68
+ storyData: StorybookData;
69
+ storybookUrl: string;
70
+ };
71
+ export type CategoryComponent = {
72
+ category: string;
73
+ component: string;
74
+ };
75
+ export type ScanStorybookReturnData = {
76
+ storiesJson: Stories;
77
+ storybookUrl: string;
78
+ tempDir: string;
79
+ };
80
+ //# sourceMappingURL=Types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Types.d.ts","sourceRoot":"","sources":["../../src/storybook/Types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gDAAgD,CAAA;AACtF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AAEzD,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,OAAO,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;KACpB,CAAC;CACL;AAED,MAAM,WAAW,QAAQ;IACrB,CAAC,EAAE,MAAM,CAAC;IACV,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,CAAC;CAC7C;AAED,MAAM,WAAW,UAAU;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,CAAC;CAC7C;AAED,MAAM,MAAM,OAAO,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAA;CAAE,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAC,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAC/B,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACxB,CAAA;AAED,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,EAAE,gBAAgB,CAAC;IACzB,IAAI,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAC/B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACxB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAC/B,SAAS,EAAE,aAAa,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACxB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvE,MAAM,MAAM,uBAAuB,GAAG;IAAE,WAAW,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { Capabilities, Options } from '@wdio/types';
2
+ import type { ClassOptions } from 'webdriver-image-comparison';
3
+ import { BaseClass } from 'webdriver-image-comparison';
4
+ export default class VisualLauncher extends BaseClass {
5
+ #private;
6
+ constructor(options: ClassOptions);
7
+ onPrepare(config: Options.Testrunner, capabilities: Capabilities.RemoteCapabilities): Promise<void>;
8
+ onComplete(): Promise<void>;
9
+ }
10
+ //# sourceMappingURL=launcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../../src/storybook/launcher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AActD,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,SAAS;;gBAGrC,OAAO,EAAE,YAAY;IAK3B,SAAS,CAAE,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE,YAAY,EAAE,YAAY,CAAC,kBAAkB;IA0DpF,UAAU;CAenB"}
@@ -0,0 +1,82 @@
1
+ import { rmdirSync } from 'node:fs';
2
+ import logger from '@wdio/logger';
3
+ import { SevereServiceError } from 'webdriverio';
4
+ import { BaseClass } from 'webdriver-image-comparison';
5
+ import { createStorybookCapabilities, createTestFiles, getArgvValue, isCucumberFramework, isStorybookMode, parseSkipStories, scanStorybook, } from './utils.js';
6
+ import { CLIP_SELECTOR, NUM_SHARDS, V6_CLIP_SELECTOR } from '../constants.js';
7
+ const log = logger('@wdio/visual-service');
8
+ export default class VisualLauncher extends BaseClass {
9
+ #options;
10
+ constructor(options) {
11
+ super(options);
12
+ this.#options = options;
13
+ }
14
+ async onPrepare(config, capabilities) {
15
+ const isStorybook = isStorybookMode();
16
+ const framework = config.framework;
17
+ const isCucumber = isCucumberFramework(framework);
18
+ if (isCucumber && isStorybook) {
19
+ throw new SevereServiceError('\n\nRunning Storybook in combination with the cucumber framework adapter is not supported.\nOnly Jasmine and Mocha are supported.\n\n');
20
+ }
21
+ else if (isStorybook) {
22
+ log.info('Running `@wdio/visual-service` in Storybook mode.');
23
+ const { storiesJson, storybookUrl, tempDir } = await scanStorybook(config, log, this.#options);
24
+ // Set an environment variable so it can be used in the onComplete hook
25
+ process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER = tempDir;
26
+ // Determine some run options
27
+ // --version
28
+ const versionOption = this.#options?.storybook?.version;
29
+ const versionArgv = getArgvValue('--version', value => Math.floor(parseFloat(value)));
30
+ const version = versionOption ?? versionArgv ?? 7;
31
+ // --numShards
32
+ const maxInstances = config?.maxInstances ?? 1;
33
+ const numShardsOption = this.#options?.storybook?.numShards;
34
+ const numShardsArgv = getArgvValue('--numShards', value => parseInt(value, 10));
35
+ const numShards = Math.min(numShardsOption || numShardsArgv || NUM_SHARDS, maxInstances);
36
+ // --clip
37
+ const clipOption = this.#options?.storybook?.clip;
38
+ const clipArgv = getArgvValue('--clip', value => value !== 'false');
39
+ const clip = clipOption ?? clipArgv ?? true;
40
+ // --clipSelector
41
+ const clipSelectorOption = this.#options?.storybook?.clipSelector;
42
+ const clipSelectorArgv = getArgvValue('--clipSelector', value => value);
43
+ // V6 has '#root' as the root element, V7 has '#storybook-root'
44
+ const clipSelector = (clipSelectorOption ?? clipSelectorArgv) ?? (version === 6 ? V6_CLIP_SELECTOR : CLIP_SELECTOR);
45
+ // --skipStories
46
+ const skipStoriesOption = this.#options?.storybook?.skipStories;
47
+ const skipStoriesArgv = getArgvValue('--skipStories', value => value);
48
+ const skipStories = skipStoriesOption ?? skipStoriesArgv ?? [];
49
+ const parsedSkipStories = parseSkipStories(skipStories, log);
50
+ // Create the test files
51
+ createTestFiles({
52
+ clip,
53
+ clipSelector,
54
+ directoryPath: tempDir,
55
+ folders: this.folders,
56
+ framework,
57
+ log,
58
+ numShards,
59
+ skipStories: parsedSkipStories,
60
+ storiesJson,
61
+ storybookUrl,
62
+ });
63
+ // Create the capabilities
64
+ createStorybookCapabilities(capabilities, log);
65
+ }
66
+ }
67
+ async onComplete() {
68
+ const tempDir = process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER;
69
+ if (tempDir) {
70
+ log.info(`Cleaning up temporary folder for storybook specs: ${tempDir}`);
71
+ try {
72
+ rmdirSync(tempDir, { recursive: true });
73
+ log.info(`Temporary folder for storybook specs has been removed: ${tempDir}`);
74
+ }
75
+ catch (err) {
76
+ log.error(`Failed to remove temporary folder for storybook specs: ${tempDir} due to: ${err.message}`);
77
+ }
78
+ // Remove the environment variables
79
+ delete process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER;
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,74 @@
1
+ import type { Logger } from '@wdio/logger';
2
+ import type { Capabilities, Options } from '@wdio/types';
3
+ import type { ClassOptions } from 'webdriver-image-comparison';
4
+ import type { CategoryComponent, CreateItContent, CreateTestContent, CreateTestFileOptions, ScanStorybookReturnData, Stories } from './Types.js';
5
+ /**
6
+ * Check if we run for Storybook
7
+ */
8
+ export declare function isStorybookMode(): boolean;
9
+ /**
10
+ * Check if the framework is cucumber
11
+ */
12
+ export declare function isCucumberFramework(framework: string): boolean;
13
+ /**
14
+ * Check if the framework is Jasmine
15
+ */
16
+ export declare function isJasmineFramework(framework: string): boolean;
17
+ /**
18
+ * Check if the framework is Mocha
19
+ */
20
+ export declare function isMochaFramework(framework: string): boolean;
21
+ /**
22
+ * Check if there is an instance of Storybook running
23
+ */
24
+ export declare function checkStorybookIsRunning(url: string): Promise<void>;
25
+ /**
26
+ * Sanitize the URL to ensure it's in a proper format
27
+ */
28
+ export declare function sanitizeURL(url: string): string;
29
+ /**
30
+ * Extract the category and component from the story ID
31
+ */
32
+ export declare function extractCategoryAndComponent(id: string): CategoryComponent;
33
+ /**
34
+ * Get the stories JSON from the Storybook instance
35
+ */
36
+ export declare function getStoriesJson(url: string): Promise<Stories>;
37
+ /**
38
+ * Get arg value from the process.argv
39
+ */
40
+ export declare function getArgvValue(argName: string, parseFunc: (value: string) => any): any;
41
+ /**
42
+ * Creates a it function for the test file
43
+ * @TODO: improve this
44
+ */
45
+ export declare function itFunction({ clip, clipSelector, folders: { baselineFolder }, framework, skipStories, storyData, storybookUrl }: CreateItContent): string;
46
+ /**
47
+ * Write the test file
48
+ */
49
+ export declare function writeTestFile(directoryPath: string, fileID: string, log: Logger, testContent: string): void;
50
+ /**
51
+ * Create the test content
52
+ */
53
+ export declare function createTestContent({ clip, clipSelector, folders, framework, skipStories, stories, storybookUrl }: CreateTestContent, itFunc?: typeof itFunction): string;
54
+ /**
55
+ * Create the file data
56
+ */
57
+ export declare function createFileData(describeTitle: string, testContent: string): string;
58
+ /**
59
+ * Create the test files
60
+ */
61
+ export declare function createTestFiles({ clip, clipSelector, directoryPath, folders, framework, log, numShards, skipStories, storiesJson, storybookUrl }: CreateTestFileOptions, createTestCont?: typeof createTestContent, createFileD?: typeof createFileData, writeTestF?: typeof writeTestFile): void;
62
+ /**
63
+ * Create the storybook capabilities based on the specified browsers
64
+ */
65
+ export declare function createStorybookCapabilities(capabilities: Capabilities.RemoteCapabilities, log: Logger): void;
66
+ /**
67
+ * Scan the storybook instance
68
+ */
69
+ export declare function scanStorybook(config: Options.Testrunner, log: Logger, options: ClassOptions, getArgvVal?: typeof getArgvValue, checkStorybookIsRun?: typeof checkStorybookIsRunning, sanitizeURLFunc?: typeof sanitizeURL, getStoriesJsonFunc?: typeof getStoriesJson): Promise<ScanStorybookReturnData>;
70
+ /**
71
+ * Parse the stories to skip
72
+ */
73
+ export declare function parseSkipStories(skipStories: string | string[], log: Logger): RegExp | string[];
74
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/storybook/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,362 @@
1
+ import fetch from 'node-fetch';
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+ /**
6
+ * Check if we run for Storybook
7
+ */
8
+ export function isStorybookMode() {
9
+ return process.argv.includes('--storybook');
10
+ }
11
+ /**
12
+ * Check if the framework is cucumber
13
+ */
14
+ export function isCucumberFramework(framework) {
15
+ return framework.toLowerCase() === 'cucumber';
16
+ }
17
+ /**
18
+ * Check if the framework is Jasmine
19
+ */
20
+ export function isJasmineFramework(framework) {
21
+ return framework.toLowerCase() === 'jasmine';
22
+ }
23
+ /**
24
+ * Check if the framework is Mocha
25
+ */
26
+ export function isMochaFramework(framework) {
27
+ return framework.toLowerCase() === 'mocha';
28
+ }
29
+ /**
30
+ * Check if there is an instance of Storybook running
31
+ */
32
+ export async function checkStorybookIsRunning(url) {
33
+ try {
34
+ const res = await fetch(url, { method: 'GET', headers: {} });
35
+ if (res.status !== 200) {
36
+ throw new Error(`Unxpected status: ${res.status}`);
37
+ }
38
+ }
39
+ catch (e) {
40
+ console.error(`It seems that the Storybook instance is not running at: ${url}. Are you sure it's running?`);
41
+ process.exit(1);
42
+ }
43
+ }
44
+ /**
45
+ * Sanitize the URL to ensure it's in a proper format
46
+ */
47
+ export function sanitizeURL(url) {
48
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
49
+ url = 'http://' + url;
50
+ }
51
+ url = url.replace(/(iframe\.html|index\.html)\s*$/, '');
52
+ if (!url.endsWith('/')) {
53
+ url += '/';
54
+ }
55
+ return url;
56
+ }
57
+ /**
58
+ * Extract the category and component from the story ID
59
+ */
60
+ export function extractCategoryAndComponent(id) {
61
+ // The ID is in the format of `category-component--storyName`
62
+ const [categoryComponent] = id.split('--');
63
+ const [category, component] = categoryComponent.split('-');
64
+ return { category, component };
65
+ }
66
+ /**
67
+ * Get the stories JSON from the Storybook instance
68
+ */
69
+ export async function getStoriesJson(url) {
70
+ const indexJsonUrl = new URL('index.json', url).toString();
71
+ const storiesJsonUrl = new URL('stories.json', url).toString();
72
+ const fetchOptions = { headers: {} };
73
+ try {
74
+ const [indexRes, storiesRes] = await Promise.all([
75
+ fetch(indexJsonUrl, fetchOptions),
76
+ fetch(storiesJsonUrl, fetchOptions),
77
+ ]);
78
+ for (const response of [storiesRes, indexRes]) {
79
+ if (response.ok) {
80
+ const data = await response.json();
81
+ return data.stories || data.entries;
82
+ }
83
+ }
84
+ }
85
+ catch (err) {
86
+ console.error(err);
87
+ }
88
+ throw new Error(`Failed to fetch index data from the project. Ensure URLs are available with valid data: ${storiesJsonUrl}, ${indexJsonUrl}.`);
89
+ }
90
+ /**
91
+ * Get arg value from the process.argv
92
+ */
93
+ export function getArgvValue(argName, parseFunc) {
94
+ const argWithEqual = argName + '=';
95
+ const argv = process.argv;
96
+ for (let i = 0; i < argv.length; i++) {
97
+ if (argv[i] === argName && i + 1 < argv.length) {
98
+ return parseFunc(argv[i + 1]);
99
+ }
100
+ else if (argv[i].startsWith(argWithEqual)) {
101
+ const value = argv[i].slice(argWithEqual.length);
102
+ return parseFunc(value);
103
+ }
104
+ }
105
+ return undefined;
106
+ }
107
+ /**
108
+ * Creates a it function for the test file
109
+ * @TODO: improve this
110
+ */
111
+ export function itFunction({ clip, clipSelector, folders: { baselineFolder }, framework, skipStories, storyData, storybookUrl }) {
112
+ const { id } = storyData;
113
+ const screenshotType = clip ? 'n element' : ' viewport';
114
+ const DEFAULT_IT_TEXT = 'it';
115
+ let itText = DEFAULT_IT_TEXT;
116
+ if (isJasmineFramework(framework)) {
117
+ itText = 'xit';
118
+ }
119
+ else if (isMochaFramework(framework)) {
120
+ itText = 'it.skip';
121
+ }
122
+ if (Array.isArray(skipStories)) {
123
+ itText = skipStories.includes(id) ? itText : DEFAULT_IT_TEXT;
124
+ }
125
+ else if (skipStories instanceof RegExp) {
126
+ itText = skipStories.test(id) ? itText : DEFAULT_IT_TEXT;
127
+ }
128
+ // Setup the folder structure
129
+ const { category, component } = extractCategoryAndComponent(id);
130
+ const methodOptions = {
131
+ baselineFolder: join(baselineFolder, `./${category}/${component}/`),
132
+ };
133
+ const it = `
134
+ ${itText}(\`should take a${screenshotType} screenshot of ${id}\`, async () => {
135
+ await browser.url(\`${storybookUrl}iframe.html?id=${id}\`);
136
+ await $('${clipSelector}').waitForDisplayed();
137
+ await waitForAllImagesLoaded();
138
+ ${clip
139
+ ? `await expect($('${clipSelector}')).toMatchElementSnapshot('${id}-element', ${JSON.stringify(methodOptions)})`
140
+ : `await expect(browser).toMatchScreenSnapshot('${id}', ${JSON.stringify(methodOptions)})`}
141
+ });
142
+ `;
143
+ return it;
144
+ }
145
+ /**
146
+ * Write the test file
147
+ */
148
+ export function writeTestFile(directoryPath, fileID, log, testContent) {
149
+ const filePath = join(directoryPath, `${fileID}.test.js`);
150
+ try {
151
+ writeFileSync(filePath, testContent);
152
+ log.info(`Test file created at: ${filePath}`);
153
+ }
154
+ catch (err) {
155
+ log.error(`It seems that the writing the file to '${filePath}' didn't succeed due to the following error: ${err}`);
156
+ process.exit(1);
157
+ }
158
+ }
159
+ /**
160
+ * Create the test content
161
+ */
162
+ export function createTestContent({ clip, clipSelector, folders, framework, skipStories, stories, storybookUrl },
163
+ // For testing purposes only
164
+ itFunc = itFunction) {
165
+ const itFunctionOptions = { clip, clipSelector, folders, framework, skipStories, storybookUrl };
166
+ return stories.reduce((acc, storyData) => acc + itFunc({ ...itFunctionOptions, storyData }), '');
167
+ }
168
+ const waitForAllImagesLoaded = `
169
+ async function waitForAllImagesLoaded() {
170
+ await browser.executeAsync(async (done) => {
171
+ const timeout = 11000; // 11 seconds
172
+ let timedOut = false;
173
+
174
+ const timeoutPromise = new Promise((resolve, reject) => {
175
+ setTimeout(() => {
176
+ timedOut = true;
177
+ reject('Timeout: Not all images loaded within 11 seconds');
178
+ }, timeout);
179
+ });
180
+
181
+ const isImageLoaded = (img) => img.complete && img.naturalWidth > 0;
182
+
183
+ // Check for <img> elements
184
+ const imgElements = Array.from(document.querySelectorAll('img'));
185
+ const imgPromises = imgElements.map(img => isImageLoaded(img) ? Promise.resolve() : new Promise(resolve => {
186
+ img.onload = () => !timedOut && resolve();
187
+ img.onerror = () => !timedOut && resolve();
188
+ }));
189
+
190
+ // Check for CSS background images
191
+ const allElements = Array.from(document.querySelectorAll('*'));
192
+ const bgImagePromises = allElements.map(el => {
193
+ const bgImage = window.getComputedStyle(el).backgroundImage;
194
+ if (bgImage && bgImage !== 'none' && bgImage.startsWith('url')) {
195
+ const imageUrl = bgImage.slice(5, -2); // Extract URL from the 'url("")'
196
+ const image = new Image();
197
+ image.src = imageUrl;
198
+ return isImageLoaded(image) ? Promise.resolve() : new Promise(resolve => {
199
+ image.onload = () => !timedOut && resolve();
200
+ image.onerror = () => !timedOut && resolve();
201
+ });
202
+ }
203
+ return Promise.resolve();
204
+ });
205
+
206
+ try {
207
+ await Promise.race([Promise.all([...imgPromises, ...bgImagePromises]), timeoutPromise]);
208
+ done();
209
+ } catch (error) {
210
+ done(error);
211
+ }
212
+ });
213
+ }
214
+ `;
215
+ /**
216
+ * Create the file data
217
+ */
218
+ export function createFileData(describeTitle, testContent) {
219
+ return `
220
+ describe(\`${describeTitle}\`, () => {
221
+ ${testContent}
222
+ });
223
+ ${waitForAllImagesLoaded}
224
+ `;
225
+ }
226
+ /**
227
+ * Create the test files
228
+ */
229
+ export function createTestFiles({ clip, clipSelector, directoryPath, folders, framework, log, numShards, skipStories, storiesJson, storybookUrl },
230
+ // For testing purposes only
231
+ createTestCont = createTestContent, createFileD = createFileData, writeTestF = writeTestFile) {
232
+ const storiesArray = Object.values(storiesJson)
233
+ // By default only keep the stories, not the docs
234
+ .filter((storyData) => storyData?.type === 'story' || !storyData.parameters?.docsOnly);
235
+ const fileNamePrefix = 'visual-storybook';
236
+ const createTestContentData = { clip, clipSelector, folders, framework, skipStories, stories: storiesArray, storybookUrl };
237
+ if (numShards === 1) {
238
+ const testContent = createTestCont(createTestContentData);
239
+ const fileData = createFileD('All stories', testContent);
240
+ writeTestF(directoryPath, `${fileNamePrefix}-1-1`, log, fileData);
241
+ }
242
+ else {
243
+ const totalStories = storiesArray.length;
244
+ const storiesPerShard = Math.ceil(totalStories / numShards);
245
+ for (let shard = 0; shard < numShards; shard++) {
246
+ const startIndex = shard * storiesPerShard;
247
+ const endIndex = Math.min(startIndex + storiesPerShard, totalStories);
248
+ const shardStories = storiesArray.slice(startIndex, endIndex);
249
+ const testContent = createTestCont({ ...createTestContentData, stories: shardStories });
250
+ const fileId = `${fileNamePrefix}-${shard + 1}-${numShards}`;
251
+ const describeTitle = `Shard ${shard + 1} of ${numShards}`;
252
+ const fileData = createFileD(describeTitle, testContent);
253
+ writeTestF(directoryPath, fileId, log, fileData);
254
+ }
255
+ }
256
+ }
257
+ /**
258
+ * Create the storybook capabilities based on the specified browsers
259
+ */
260
+ export function createStorybookCapabilities(capabilities, log) {
261
+ const isHeadless = getArgvValue('--headless', value => value !== 'false') ?? true;
262
+ const browsers = getArgvValue('--browsers', (value) => value.split(',')) ?? ['chrome'];
263
+ if (Array.isArray(capabilities)) {
264
+ const chromeCapability = {
265
+ browserName: 'chrome',
266
+ 'goog:chromeOptions': {
267
+ args: [
268
+ 'disable-infobars',
269
+ ...(isHeadless ? ['--headless'] : []),
270
+ ],
271
+ },
272
+ 'wdio-ics:options': {
273
+ logName: 'local-chrome',
274
+ },
275
+ };
276
+ const firefoxCapability = {
277
+ browserName: 'firefox',
278
+ 'moz:firefoxOptions': {
279
+ args: [...(isHeadless ? ['-headless'] : []),]
280
+ },
281
+ 'wdio-ics:options': {
282
+ logName: 'local-firefox',
283
+ },
284
+ };
285
+ const safariCapability = {
286
+ browserName: 'safari',
287
+ 'wdio-ics:options': {
288
+ logName: 'local-safari',
289
+ },
290
+ };
291
+ const edgeCapability = {
292
+ browserName: 'MicrosoftEdge',
293
+ 'ms:edgeOptions': {
294
+ args: [...(isHeadless ? ['--headless'] : [])]
295
+ },
296
+ 'wdio-ics:options': {
297
+ logName: 'local-edge',
298
+ },
299
+ };
300
+ const capabilityMap = {
301
+ chrome: chromeCapability,
302
+ firefox: firefoxCapability,
303
+ safari: safariCapability,
304
+ edge: edgeCapability,
305
+ };
306
+ const newCapabilities = browsers
307
+ .filter((browser) => browser in capabilityMap)
308
+ .map((browser) => capabilityMap[browser]);
309
+ capabilities.length = 0;
310
+ // Add the new capability to the capabilities array
311
+ capabilities.push(...newCapabilities);
312
+ }
313
+ else {
314
+ log.error('The capabilities are not an array');
315
+ }
316
+ }
317
+ /**
318
+ * Scan the storybook instance
319
+ */
320
+ export async function scanStorybook(config, log, options,
321
+ // For testing purposes only
322
+ getArgvVal = getArgvValue, checkStorybookIsRun = checkStorybookIsRunning, sanitizeURLFunc = sanitizeURL, getStoriesJsonFunc = getStoriesJson) {
323
+ // Prepare storybook scanning
324
+ const cliUrl = getArgvVal('--url', value => value);
325
+ const rawStorybookUrl = cliUrl ?? process.env.STORYBOOK_URL ?? options?.storybook?.url ?? 'http://127.0.0.1:6006';
326
+ await checkStorybookIsRun(rawStorybookUrl);
327
+ const storybookUrl = sanitizeURLFunc(rawStorybookUrl);
328
+ // Create a temporary folder for test files and add that to the specs
329
+ const tempDir = resolve(tmpdir(), `wdio-storybook-tests-${Date.now()}`);
330
+ mkdirSync(tempDir);
331
+ log.info(`Using temporary folder for storybook specs: ${tempDir}`);
332
+ config.specs = [join(tempDir, '*.js')];
333
+ // Get the stories
334
+ const storiesJson = await getStoriesJsonFunc(storybookUrl);
335
+ return {
336
+ storiesJson,
337
+ storybookUrl,
338
+ tempDir,
339
+ };
340
+ }
341
+ /**
342
+ * Parse the stories to skip
343
+ */
344
+ export function parseSkipStories(skipStories, log) {
345
+ if (Array.isArray(skipStories)) {
346
+ return skipStories;
347
+ }
348
+ const regexPattern = /^\/.*\/[gimyus]*$/;
349
+ if (regexPattern.test(skipStories)) {
350
+ try {
351
+ const match = skipStories.match(/^\/(.+)\/([gimyus]*)$/);
352
+ if (match) {
353
+ const [, pattern, flags] = match;
354
+ return new RegExp(pattern, flags);
355
+ }
356
+ }
357
+ catch (error) {
358
+ log.error('Invalid regular expression:', error, '. Not using a regular expression to skip stories.');
359
+ }
360
+ }
361
+ return skipStories.split(',').map(skipped => skipped.trim());
362
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@wdio/visual-service",
3
3
  "author": "Wim Selles - wswebcreation",
4
4
  "description": "Image comparison / visual regression testing for WebdriverIO",
5
- "version": "3.1.0",
5
+ "version": "4.0.0",
6
6
  "license": "MIT",
7
7
  "homepage": "https://webdriver.io/docs/visual-testing",
8
8
  "repository": {
@@ -20,9 +20,10 @@
20
20
  "type": "module",
21
21
  "types": "./dist/index.d.ts",
22
22
  "dependencies": {
23
- "@wdio/logger": "^8.24.12",
24
- "@wdio/types": "^8.26.2",
25
- "webdriver-image-comparison": "^4.1.0"
23
+ "@wdio/logger": "^8.28.0",
24
+ "@wdio/types": "^8.32.4",
25
+ "node-fetch": "^3.3.2",
26
+ "webdriver-image-comparison": "^5.0.0"
26
27
  },
27
28
  "scripts": {
28
29
  "build": "run-s clean build:*",