@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 +112 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/matcher.js +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +1 -0
- package/dist/storybook/Types.d.ts +80 -0
- package/dist/storybook/Types.d.ts.map +1 -0
- package/dist/storybook/Types.js +1 -0
- package/dist/storybook/launcher.d.ts +10 -0
- package/dist/storybook/launcher.d.ts.map +1 -0
- package/dist/storybook/launcher.js +82 -0
- package/dist/storybook/utils.d.ts +74 -0
- package/dist/storybook/utils.d.ts.map +1 -0
- package/dist/storybook/utils.js +362 -0
- package/package.json +5 -4
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
|
+

|
|
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 @@
|
|
|
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"}
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
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
|
-
:
|
|
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
|
}
|
package/dist/service.d.ts.map
CHANGED
|
@@ -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;
|
|
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":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAKxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC9D,OAAO,KAAK,EACR,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EAErB,uBAAuB,EACvB,OAAO,EAGV,MAAM,YAAY,CAAA;AAEnB;;GAEG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,MAAM,iBAUxD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAY/C;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,CAMzE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAsBlE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,GAAG,GAAG,GAAG,CAcpF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,eAAe,UAmC/I;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,QASpG;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC7B,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,iBAAiB,EAEjG,MAAM,oBAAa,GACpB,MAAM,CAIR;AAkDD;;GAEG;AACH,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAOjF;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC3B,EAAE,IAAI,EAAE,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,qBAAqB,EAExI,cAAc,2BAAoB,EAClC,WAAW,wBAAiB,EAC5B,UAAU,uBAAgB,QA4B7B;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,YAAY,CAAC,kBAAkB,EAAE,GAAG,EAAE,MAAM,QA8DrG;AAED;;GAEG;AACH,wBAAsB,aAAa,CAC/B,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EAErB,UAAU,sBAAe,EACzB,mBAAmB,iCAA0B,EAC7C,eAAe,qBAAc,EAC7B,kBAAkB,wBAAiB,GACpC,OAAO,CAAC,uBAAuB,CAAC,CAqBlC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,CAmB/F"}
|
|
@@ -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": "
|
|
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
|
-
"@wdio/types": "^8.
|
|
25
|
-
"
|
|
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:*",
|