creevey 0.10.0-beta.4 → 0.10.0-beta.41
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/README.md +19 -41
- package/dist/client/addon/components/Addon.js +17 -7
- package/dist/client/addon/components/Addon.js.map +1 -1
- package/dist/client/addon/components/Panel.js +2 -2
- package/dist/client/addon/components/Panel.js.map +1 -1
- package/dist/client/addon/components/Tools.js +17 -7
- package/dist/client/addon/components/Tools.js.map +1 -1
- package/dist/client/addon/withCreevey.d.ts +2 -1
- package/dist/client/addon/withCreevey.js +11 -1
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
- package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
- package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
- package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
- package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
- package/dist/client/shared/components/PageHeader/ImagePreview.d.ts +1 -1
- package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
- package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
- package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/components/ResultsPage.d.ts +1 -1
- package/dist/client/shared/components/ResultsPage.js +43 -13
- package/dist/client/shared/components/ResultsPage.js.map +1 -1
- package/dist/client/shared/creeveyClientApi.js +8 -1
- package/dist/client/shared/creeveyClientApi.js.map +1 -1
- package/dist/client/shared/helpers.d.ts +1 -3
- package/dist/client/shared/helpers.js +4 -19
- package/dist/client/shared/helpers.js.map +1 -1
- package/dist/client/web/CreeveyApp.js +42 -14
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyContext.d.ts +5 -0
- package/dist/client/web/CreeveyContext.js +20 -7
- package/dist/client/web/CreeveyContext.js.map +1 -1
- package/dist/client/web/CreeveyLoader.js +2 -2
- package/dist/client/web/CreeveyLoader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
- package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
- package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +2 -2
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
- package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
- package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
- package/dist/client/web/KeyboardEventsContext.js +79 -64
- package/dist/client/web/KeyboardEventsContext.js.map +1 -1
- package/dist/client/web/assets/index-C47njyZV.js +802 -0
- package/dist/client/web/index.html +1 -1
- package/dist/client/web/index.js +17 -7
- package/dist/client/web/index.js.map +1 -1
- package/dist/client/web/themes.d.ts +2 -0
- package/dist/client/web/themes.js +22 -0
- package/dist/client/web/themes.js.map +1 -0
- package/dist/creevey.js +16 -9
- package/dist/creevey.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/server/config.d.ts +1 -1
- package/dist/server/config.js +30 -7
- package/dist/server/config.js.map +1 -1
- package/dist/server/connection.d.ts +3 -0
- package/dist/server/connection.js +28 -0
- package/dist/server/connection.js.map +1 -0
- package/dist/server/docker.d.ts +1 -1
- package/dist/server/docker.js +56 -32
- package/dist/server/docker.js.map +1 -1
- package/dist/server/index.js +64 -11
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +2 -1
- package/dist/server/logger.js +7 -3
- package/dist/server/logger.js.map +1 -1
- package/dist/server/master/api.js +1 -1
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/pool.d.ts +4 -3
- package/dist/server/master/pool.js +13 -66
- package/dist/server/master/pool.js.map +1 -1
- package/dist/server/master/queue.d.ts +13 -0
- package/dist/server/master/queue.js +71 -0
- package/dist/server/master/queue.js.map +1 -0
- package/dist/server/master/runner.d.ts +3 -0
- package/dist/server/master/runner.js +78 -10
- package/dist/server/master/runner.js.map +1 -1
- package/dist/server/master/server.js +1 -1
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/master/start.js +13 -11
- package/dist/server/master/start.js.map +1 -1
- package/dist/server/playwright/docker-file.d.ts +1 -1
- package/dist/server/playwright/docker-file.js +15 -6
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/docker.d.ts +2 -1
- package/dist/server/playwright/docker.js +10 -2
- package/dist/server/playwright/docker.js.map +1 -1
- package/dist/server/playwright/index-source.mjs +16 -0
- package/dist/server/playwright/internal.d.ts +6 -6
- package/dist/server/playwright/internal.js +143 -91
- package/dist/server/playwright/internal.js.map +1 -1
- package/dist/server/playwright/webdriver.d.ts +1 -1
- package/dist/server/playwright/webdriver.js +5 -8
- package/dist/server/playwright/webdriver.js.map +1 -1
- package/dist/server/providers/browser.js +6 -4
- package/dist/server/providers/browser.js.map +1 -1
- package/dist/server/providers/hybrid.js +1 -1
- package/dist/server/providers/hybrid.js.map +1 -1
- package/dist/server/reporters/creevey.d.ts +7 -0
- package/dist/server/reporters/creevey.js +63 -0
- package/dist/server/reporters/creevey.js.map +1 -0
- package/dist/server/reporters/index.d.ts +2 -0
- package/dist/server/reporters/index.js +16 -0
- package/dist/server/reporters/index.js.map +1 -0
- package/dist/server/reporters/junit.d.ts +16 -0
- package/dist/server/reporters/junit.js +165 -0
- package/dist/server/reporters/junit.js.map +1 -0
- package/dist/server/reporters/teamcity.d.ts +7 -0
- package/dist/server/reporters/teamcity.js +60 -0
- package/dist/server/reporters/teamcity.js.map +1 -0
- package/dist/server/selenium/internal.d.ts +3 -4
- package/dist/server/selenium/internal.js +127 -108
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +8 -6
- package/dist/server/selenium/selenoid.js.map +1 -1
- package/dist/server/selenium/webdriver.d.ts +1 -1
- package/dist/server/selenium/webdriver.js +5 -9
- package/dist/server/selenium/webdriver.js.map +1 -1
- package/dist/server/telemetry.js +2 -2
- package/dist/server/testsFiles/parser.js +45 -5
- package/dist/server/testsFiles/parser.js.map +1 -1
- package/dist/server/utils.d.ts +19 -1
- package/dist/server/utils.js +87 -8
- package/dist/server/utils.js.map +1 -1
- package/dist/server/webdriver.d.ts +5 -4
- package/dist/server/webdriver.js +23 -10
- package/dist/server/webdriver.js.map +1 -1
- package/dist/server/worker/chai-image.d.ts +1 -2
- package/dist/server/worker/chai-image.js +4 -3
- package/dist/server/worker/chai-image.js.map +1 -1
- package/dist/server/worker/context.d.ts +3 -0
- package/dist/server/worker/context.js +15 -0
- package/dist/server/worker/context.js.map +1 -0
- package/dist/server/worker/match-image.d.ts +4 -4
- package/dist/server/worker/match-image.js +7 -4
- package/dist/server/worker/match-image.js.map +1 -1
- package/dist/server/worker/start.js +47 -73
- package/dist/server/worker/start.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/types.d.ts +46 -10
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/docs/cli.md +12 -0
- package/docs/config.md +179 -165
- package/docs/storybook.md +60 -0
- package/docs/tests.md +50 -45
- package/package.json +64 -63
- package/src/client/addon/components/Panel.tsx +2 -2
- package/src/client/addon/withCreevey.ts +10 -2
- package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
- package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
- package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
- package/src/client/shared/components/ResultsPage.tsx +31 -8
- package/src/client/shared/creeveyClientApi.ts +9 -1
- package/src/client/shared/helpers.ts +4 -24
- package/src/client/web/CreeveyApp.tsx +27 -8
- package/src/client/web/CreeveyContext.tsx +9 -0
- package/src/client/web/CreeveyLoader.tsx +1 -1
- package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
- package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
- package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
- package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
- package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
- package/src/client/web/KeyboardEventsContext.tsx +61 -73
- package/src/client/web/themes.ts +24 -0
- package/src/creevey.ts +16 -10
- package/src/server/config.ts +30 -7
- package/src/server/connection.ts +26 -0
- package/src/server/docker.ts +63 -34
- package/src/server/index.ts +72 -14
- package/src/server/logger.ts +6 -2
- package/src/server/master/api.ts +1 -1
- package/src/server/master/pool.ts +23 -59
- package/src/server/master/queue.ts +77 -0
- package/src/server/master/runner.ts +96 -10
- package/src/server/master/server.ts +1 -1
- package/src/server/master/start.ts +16 -11
- package/src/server/playwright/docker-file.ts +18 -6
- package/src/server/playwright/docker.ts +16 -3
- package/src/server/playwright/index-source.mjs +16 -0
- package/src/server/playwright/internal.ts +182 -111
- package/src/server/playwright/webdriver.ts +6 -9
- package/src/server/providers/browser.ts +6 -4
- package/src/server/providers/hybrid.ts +1 -1
- package/src/server/reporters/creevey.ts +71 -0
- package/src/server/reporters/index.ts +11 -0
- package/src/server/reporters/junit.ts +205 -0
- package/src/server/reporters/teamcity.ts +74 -0
- package/src/server/selenium/internal.ts +131 -116
- package/src/server/selenium/selenoid.ts +8 -6
- package/src/server/selenium/webdriver.ts +6 -10
- package/src/server/telemetry.ts +2 -2
- package/src/server/testsFiles/parser.ts +52 -4
- package/src/server/utils.ts +97 -9
- package/src/server/webdriver.ts +24 -16
- package/src/server/worker/chai-image.ts +4 -4
- package/src/server/worker/context.ts +14 -0
- package/src/server/worker/match-image.ts +12 -8
- package/src/server/worker/start.ts +51 -86
- package/src/shared/index.ts +1 -1
- package/src/types.ts +50 -11
- package/types/global.d.ts +1 -0
- package/.yarnrc.yml +0 -1
- package/chromatic.config.json +0 -5
- package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
- package/dist/server/reporter.d.ts +0 -26
- package/dist/server/reporter.js +0 -108
- package/dist/server/reporter.js.map +0 -1
- package/src/server/reporter.ts +0 -138
@@ -1,8 +1,58 @@
|
|
1
1
|
import { pathToFileURL } from 'url';
|
2
|
-
import { toId, storyNameFromExport } from '@storybook/csf';
|
3
2
|
import { CreeveyStoryParams, CreeveyTestFunction } from '../../types.js';
|
4
3
|
import { loadThroughTSX } from '../utils.js';
|
5
4
|
|
5
|
+
// NOTE: Copy-pasted from @storybook/csf
|
6
|
+
function toStartCaseStr(str: string) {
|
7
|
+
return str
|
8
|
+
.replace(/_/g, ' ')
|
9
|
+
.replace(/-/g, ' ')
|
10
|
+
.replace(/\./g, ' ')
|
11
|
+
.replace(/([^\n])([A-Z])([a-z])/g, (_, $1, $2, $3) => `${$1} ${$2}${$3}`)
|
12
|
+
.replace(/([a-z])([A-Z])/g, (_, $1, $2) => `${$1} ${$2}`)
|
13
|
+
.replace(/([a-z])([0-9])/gi, (_, $1, $2) => `${$1} ${$2}`)
|
14
|
+
.replace(/([0-9])([a-z])/gi, (_, $1, $2) => `${$1} ${$2}`)
|
15
|
+
.replace(/(\s|^)(\w)/g, (_, $1, $2: string) => `${$1}${$2.toUpperCase()}`)
|
16
|
+
.replace(/ +/g, ' ')
|
17
|
+
.trim();
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Remove punctuation and illegal characters from a story ID.
|
22
|
+
*
|
23
|
+
* See https://gist.github.com/davidjrice/9d2af51100e41c6c4b4a
|
24
|
+
*/
|
25
|
+
const sanitize = (string: string) => {
|
26
|
+
return (
|
27
|
+
string
|
28
|
+
.toLowerCase()
|
29
|
+
// eslint-disable-next-line no-useless-escape
|
30
|
+
.replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '-')
|
31
|
+
.replace(/-+/g, '-')
|
32
|
+
.replace(/^-+/, '')
|
33
|
+
.replace(/-+$/, '')
|
34
|
+
);
|
35
|
+
};
|
36
|
+
|
37
|
+
const sanitizeSafe = (string: string, part: string) => {
|
38
|
+
const sanitized = sanitize(string);
|
39
|
+
if (sanitized === '') {
|
40
|
+
throw new Error(`Invalid ${part} '${string}', must include alphanumeric characters`);
|
41
|
+
}
|
42
|
+
return sanitized;
|
43
|
+
};
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Generate a storybook ID from a component/kind and story name.
|
47
|
+
*/
|
48
|
+
const toId = (kind: string, name?: string) =>
|
49
|
+
`${sanitizeSafe(kind, 'kind')}${name ? `--${sanitizeSafe(name, 'name')}` : ''}`;
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Transform a CSF named export into a readable story name
|
53
|
+
*/
|
54
|
+
const storyNameFromExport = (key: string) => toStartCaseStr(key);
|
55
|
+
|
6
56
|
export type CreeveyParamsByStoryId = Record<string, CreeveyStoryParams>;
|
7
57
|
|
8
58
|
export default async function parse(files: string[]): Promise<CreeveyParamsByStoryId> {
|
@@ -55,8 +105,6 @@ export const story = (
|
|
55
105
|
|
56
106
|
export const test = (title: string, testFn: CreeveyTestFunction): void => {
|
57
107
|
const storyId = getStoryId(kindTitle, storyTitle);
|
58
|
-
|
59
|
-
result[storyId] = {};
|
60
|
-
}
|
108
|
+
result[storyId] ??= {};
|
61
109
|
result[storyId].tests = Object.assign({}, result[storyId].tests, { [title]: testFn });
|
62
110
|
};
|
package/src/server/utils.ts
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
import fs from 'fs';
|
2
|
-
import
|
2
|
+
import https from 'https';
|
3
|
+
import http from 'http';
|
3
4
|
import cluster from 'cluster';
|
4
5
|
import { dirname } from 'path';
|
5
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
6
|
-
import { createRequire } from 'module';
|
7
7
|
import { register as esmRegister } from 'tsx/esm/api';
|
8
8
|
import { register as cjsRegister } from 'tsx/cjs/api';
|
9
|
-
import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest } from '../types.js';
|
9
|
+
import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest, Worker } from '../types.js';
|
10
10
|
import { emitShutdownMessage, sendShutdownMessage } from './messages.js';
|
11
|
+
import { LOCALHOST_REGEXP } from './webdriver.js';
|
12
|
+
import assert from 'assert';
|
13
|
+
import pidtree from 'pidtree';
|
11
14
|
|
12
15
|
const importMetaUrl = pathToFileURL(__filename).href;
|
13
16
|
|
@@ -15,6 +18,19 @@ export const isShuttingDown = { current: false };
|
|
15
18
|
|
16
19
|
export const configExt = ['.js', '.mjs', '.ts', '.cjs', '.mts', '.cts'];
|
17
20
|
|
21
|
+
const browserTypes = {
|
22
|
+
chromium: 'chromium',
|
23
|
+
'chromium-headless-shell': 'chromium',
|
24
|
+
chrome: 'chromium',
|
25
|
+
'chrome-beta': 'chromium',
|
26
|
+
msedge: 'chromium',
|
27
|
+
'msedge-beta': 'chromium',
|
28
|
+
'msedge-dev': 'chromium',
|
29
|
+
'bidi-chromium': 'chromium',
|
30
|
+
firefox: 'firefox',
|
31
|
+
webkit: 'webkit',
|
32
|
+
} as const;
|
33
|
+
|
18
34
|
export const skipOptionKeys = ['in', 'kinds', 'stories', 'tests', 'reason'];
|
19
35
|
|
20
36
|
function matchBy(pattern: string | string[] | RegExp | undefined, value: string): boolean {
|
@@ -83,19 +99,56 @@ export async function shutdownWorkers(): Promise<void> {
|
|
83
99
|
(worker) =>
|
84
100
|
new Promise<void>((resolve) => {
|
85
101
|
const timeout = setTimeout(() => {
|
86
|
-
worker.
|
87
|
-
},
|
102
|
+
if (worker.process.pid) void killTree(worker.process.pid);
|
103
|
+
}, 10_000);
|
88
104
|
worker.on('exit', () => {
|
89
105
|
clearTimeout(timeout);
|
90
106
|
resolve();
|
91
107
|
});
|
92
108
|
sendShutdownMessage(worker);
|
109
|
+
worker.disconnect();
|
93
110
|
}),
|
94
111
|
),
|
95
112
|
);
|
96
113
|
emitShutdownMessage();
|
97
114
|
}
|
98
115
|
|
116
|
+
export function gracefullyKill(worker: Worker): void {
|
117
|
+
worker.isShuttingDown = true;
|
118
|
+
const timeout = setTimeout(() => {
|
119
|
+
if (worker.process.pid) void killTree(worker.process.pid);
|
120
|
+
}, 10000);
|
121
|
+
worker.on('exit', () => {
|
122
|
+
clearTimeout(timeout);
|
123
|
+
});
|
124
|
+
sendShutdownMessage(worker);
|
125
|
+
}
|
126
|
+
|
127
|
+
export async function killTree(rootPid: number): Promise<void> {
|
128
|
+
const pids = await pidtree(rootPid, { root: true });
|
129
|
+
|
130
|
+
pids.forEach((pid) => {
|
131
|
+
try {
|
132
|
+
process.kill(pid, 'SIGKILL');
|
133
|
+
} catch {
|
134
|
+
/* noop */
|
135
|
+
}
|
136
|
+
});
|
137
|
+
}
|
138
|
+
|
139
|
+
export function shutdownWithError(): void {
|
140
|
+
process.exit(1);
|
141
|
+
}
|
142
|
+
|
143
|
+
export function resolvePlaywrightBrowserType(browserName: string): (typeof browserTypes)[keyof typeof browserTypes] {
|
144
|
+
assert(
|
145
|
+
browserName in browserTypes,
|
146
|
+
new Error(`Failed to match browser name "${browserName}" to playwright browserType`),
|
147
|
+
);
|
148
|
+
|
149
|
+
return browserTypes[browserName as keyof typeof browserTypes];
|
150
|
+
}
|
151
|
+
|
99
152
|
export async function getCreeveyCache(): Promise<string | undefined> {
|
100
153
|
const { default: findCacheDir } = await import('find-cache-dir');
|
101
154
|
return findCacheDir({ name: 'creevey', cwd: dirname(fileURLToPath(importMetaUrl)) });
|
@@ -131,11 +184,12 @@ export function testsToImages(tests: (TestData | undefined)[]): Set<string> {
|
|
131
184
|
|
132
185
|
// https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
|
133
186
|
export const isInsideDocker =
|
134
|
-
fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf-8').includes('docker')
|
187
|
+
(fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf-8').includes('docker')) ||
|
188
|
+
process.env.DOCKER === 'true';
|
135
189
|
|
136
190
|
export const downloadBinary = (downloadUrl: string, destination: string): Promise<void> =>
|
137
191
|
new Promise((resolve, reject) =>
|
138
|
-
get(downloadUrl, (response) => {
|
192
|
+
https.get(downloadUrl, (response) => {
|
139
193
|
if (response.statusCode == 302) {
|
140
194
|
const { location } = response.headers;
|
141
195
|
if (!location) {
|
@@ -175,10 +229,10 @@ export function readDirRecursive(dirPath: string): string[] {
|
|
175
229
|
);
|
176
230
|
}
|
177
231
|
|
178
|
-
const _require = createRequire(importMetaUrl);
|
179
232
|
export function tryToLoadTestsData(filename: string): Partial<Record<string, ServerTest>> | undefined {
|
180
233
|
try {
|
181
|
-
|
234
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
235
|
+
return require(filename) as Partial<Record<string, ServerTest>>;
|
182
236
|
} catch {
|
183
237
|
/* noop */
|
184
238
|
}
|
@@ -204,3 +258,37 @@ export async function loadThroughTSX<T>(
|
|
204
258
|
|
205
259
|
return result;
|
206
260
|
}
|
261
|
+
|
262
|
+
export function waitOnUrl(waitUrl: string, timeout: number, delay: number) {
|
263
|
+
const urls = [waitUrl];
|
264
|
+
if (!LOCALHOST_REGEXP.test(waitUrl)) {
|
265
|
+
const parsedUrl = new URL(waitUrl);
|
266
|
+
parsedUrl.host = 'localhost';
|
267
|
+
urls.push(parsedUrl.toString());
|
268
|
+
}
|
269
|
+
const startTime = Date.now();
|
270
|
+
return Promise.race(
|
271
|
+
urls.map(
|
272
|
+
(url) =>
|
273
|
+
new Promise<void>((resolve, reject) => {
|
274
|
+
const interval = setInterval(() => {
|
275
|
+
http
|
276
|
+
.get(url, (response) => {
|
277
|
+
if (response.statusCode === 200) {
|
278
|
+
clearInterval(interval);
|
279
|
+
resolve();
|
280
|
+
}
|
281
|
+
})
|
282
|
+
.on('error', () => {
|
283
|
+
// Ignore HTTP errors
|
284
|
+
});
|
285
|
+
|
286
|
+
if (Date.now() - startTime > timeout) {
|
287
|
+
clearInterval(interval);
|
288
|
+
reject(new Error(`${url} didn't respond within ${timeout / 1000} seconds`));
|
289
|
+
}
|
290
|
+
}, delay);
|
291
|
+
}),
|
292
|
+
),
|
293
|
+
);
|
294
|
+
}
|
package/src/server/webdriver.ts
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
import Logger from 'loglevel';
|
2
1
|
import chalk from 'chalk';
|
3
2
|
import { networkInterfaces } from 'os';
|
4
|
-
import { logger
|
5
|
-
import { Args } from '@storybook/
|
3
|
+
import { logger } from './logger.js';
|
4
|
+
import type { Args } from '@storybook/types';
|
6
5
|
import {
|
7
6
|
isDefined,
|
8
7
|
StoryInput,
|
@@ -19,18 +18,31 @@ export const storybookRootID = 'storybook-root';
|
|
19
18
|
export const LOCALHOST_REGEXP = /(localhost|127\.0\.0\.1)/i;
|
20
19
|
const DOCKER_INTERNAL = 'host.docker.internal';
|
21
20
|
|
21
|
+
let browserClosePromise: Promise<void> | null = null;
|
22
|
+
|
23
|
+
export const openBrowser = () => {
|
24
|
+
let resolve: () => void;
|
25
|
+
browserClosePromise = new Promise((r) => (resolve = r));
|
26
|
+
return () => {
|
27
|
+
resolve();
|
28
|
+
browserClosePromise = null;
|
29
|
+
};
|
30
|
+
};
|
31
|
+
|
32
|
+
export const waitForBrowserClose = () => browserClosePromise;
|
33
|
+
|
22
34
|
export async function resolveStorybookUrl(
|
23
35
|
storybookUrl: string,
|
24
36
|
checkUrl: (url: string) => Promise<boolean>,
|
25
|
-
logger: Logger.Logger = defaultLogger,
|
26
37
|
): Promise<string> {
|
27
|
-
logger.debug('Resolving storybook url');
|
38
|
+
logger().debug('Resolving storybook url');
|
28
39
|
const addresses = getAddresses();
|
40
|
+
// TODO Use Promise.race?
|
29
41
|
for (const ip of addresses) {
|
30
42
|
const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
|
31
|
-
logger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
43
|
+
logger().debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
32
44
|
if (await checkUrl(resolvedUrl)) {
|
33
|
-
logger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
45
|
+
logger().debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
34
46
|
return resolvedUrl;
|
35
47
|
}
|
36
48
|
}
|
@@ -74,11 +86,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
74
86
|
|
75
87
|
abstract afterTest(test: ServerTest): Promise<void>;
|
76
88
|
|
77
|
-
async switchStory(
|
78
|
-
story: StoryInput,
|
79
|
-
context: BaseCreeveyTestContext,
|
80
|
-
logger: Logger.Logger,
|
81
|
-
): Promise<CreeveyTestContext> {
|
89
|
+
async switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext> {
|
82
90
|
const { id, title, name, parameters } = story;
|
83
91
|
const {
|
84
92
|
captureElement = `#${storybookRootID}`,
|
@@ -86,7 +94,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
86
94
|
ignoreElements,
|
87
95
|
} = (parameters.creevey ?? {}) as CreeveyStoryParams;
|
88
96
|
|
89
|
-
logger.debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
|
97
|
+
logger().debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
|
90
98
|
|
91
99
|
let storyPlayResolver: (isCompleted: boolean) => void;
|
92
100
|
let waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
@@ -107,7 +115,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
107
115
|
const isCaptureCalled = await this.selectStory(id, waitForReady);
|
108
116
|
|
109
117
|
if (isCaptureCalled) {
|
110
|
-
logger.debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
|
118
|
+
logger().debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
|
111
119
|
while (!(await waitForComplete)) {
|
112
120
|
waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
113
121
|
}
|
@@ -115,8 +123,8 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
115
123
|
|
116
124
|
unsubscribe();
|
117
125
|
|
118
|
-
if (isCaptureCalled) logger.debug(`Story ${chalk.magenta(id)} completed capturing`);
|
119
|
-
else logger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
|
126
|
+
if (isCaptureCalled) logger().debug(`Story ${chalk.magenta(id)} completed capturing`);
|
127
|
+
else logger().debug(`Story ${chalk.magenta(id)} ready for capturing`);
|
120
128
|
|
121
129
|
return Object.assign(
|
122
130
|
{
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import
|
1
|
+
import { logger } from '../logger';
|
2
|
+
|
2
3
|
export default function (
|
3
4
|
matchImage: (image: Buffer, imageName?: string) => Promise<void>,
|
4
5
|
matchImages: (images: Record<string, Buffer>) => Promise<void>,
|
5
|
-
logger: Logger.Logger,
|
6
6
|
) {
|
7
7
|
let isWarningShown = false;
|
8
8
|
return function chaiImage({ Assertion }: Chai.ChaiStatic, utils: Chai.ChaiUtils): void {
|
@@ -11,7 +11,7 @@ export default function (
|
|
11
11
|
'matchImage',
|
12
12
|
async function (this: Record<string, unknown>, imageName?: string) {
|
13
13
|
if (!isWarningShown) {
|
14
|
-
logger.warn(
|
14
|
+
logger().warn(
|
15
15
|
'`expect(...).to.matchImage()` is deprecated and will be removed in the next major release. Please use `context.matchImage()` instead.',
|
16
16
|
);
|
17
17
|
isWarningShown = true;
|
@@ -23,7 +23,7 @@ export default function (
|
|
23
23
|
|
24
24
|
utils.addMethod(Assertion.prototype, 'matchImages', async function (this: Record<string, unknown>) {
|
25
25
|
if (!isWarningShown) {
|
26
|
-
logger.warn(
|
26
|
+
logger().warn(
|
27
27
|
'`expect(...).to.matchImages()` is deprecated and will be removed in the next major release. Please use `context.matchImages()` instead.',
|
28
28
|
);
|
29
29
|
isWarningShown = true;
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import type { Container } from 'dockerode';
|
2
|
+
|
3
|
+
let workerContainer: Container | null = null;
|
4
|
+
|
5
|
+
export function setWorkerContainer(container: Container): void {
|
6
|
+
workerContainer = container;
|
7
|
+
}
|
8
|
+
|
9
|
+
export async function removeWorkerContainer(): Promise<void> {
|
10
|
+
if (workerContainer) {
|
11
|
+
await workerContainer.remove({ force: true });
|
12
|
+
workerContainer = null;
|
13
|
+
}
|
14
|
+
}
|
@@ -21,6 +21,10 @@ interface ImagePaths {
|
|
21
21
|
reportImageDir: string;
|
22
22
|
}
|
23
23
|
|
24
|
+
function toBuffer(bufferOrBase64: Buffer | string) {
|
25
|
+
return typeof bufferOrBase64 === 'string' ? Buffer.from(bufferOrBase64, 'base64') : bufferOrBase64;
|
26
|
+
}
|
27
|
+
|
24
28
|
async function getStat(filePath: string): Promise<Stats | null> {
|
25
29
|
try {
|
26
30
|
return await stat(filePath);
|
@@ -228,17 +232,17 @@ export async function getMatchers(ctx: ImageContext, config: Config) {
|
|
228
232
|
}
|
229
233
|
|
230
234
|
return {
|
231
|
-
matchImage: async (image: Buffer, imageName?: string) => {
|
232
|
-
const errorMessage = await assertImage(image, imageName);
|
235
|
+
matchImage: async (image: Buffer | string, imageName?: string) => {
|
236
|
+
const errorMessage = await assertImage(toBuffer(image), imageName);
|
233
237
|
if (errorMessage) {
|
234
238
|
throw createImageError(imageName ? { [imageName]: errorMessage } : errorMessage);
|
235
239
|
}
|
236
240
|
},
|
237
|
-
matchImages: async (images: Record<string, Buffer>) => {
|
241
|
+
matchImages: async (images: Record<string, Buffer | string>) => {
|
238
242
|
const errors: Record<string, string> = {};
|
239
243
|
await Promise.all(
|
240
244
|
Object.entries(images).map(async ([imageName, image]) => {
|
241
|
-
const errorMessage = await assertImage(image, imageName);
|
245
|
+
const errorMessage = await assertImage(toBuffer(image), imageName);
|
242
246
|
if (errorMessage) {
|
243
247
|
errors[imageName] = errorMessage;
|
244
248
|
}
|
@@ -279,17 +283,17 @@ export function getOdiffMatchers(ctx: ImageContext, config: Config) {
|
|
279
283
|
}
|
280
284
|
|
281
285
|
return {
|
282
|
-
matchImage: async (image: Buffer, imageName?: string) => {
|
283
|
-
const errorMessage = await assertImage(image, imageName);
|
286
|
+
matchImage: async (image: Buffer | string, imageName?: string) => {
|
287
|
+
const errorMessage = await assertImage(toBuffer(image), imageName);
|
284
288
|
if (errorMessage) {
|
285
289
|
throw createImageError(imageName ? { [imageName]: errorMessage } : errorMessage);
|
286
290
|
}
|
287
291
|
},
|
288
|
-
matchImages: async (images: Record<string, Buffer>) => {
|
292
|
+
matchImages: async (images: Record<string, Buffer | string>) => {
|
289
293
|
const errors: Record<string, string> = {};
|
290
294
|
await Promise.all(
|
291
295
|
Object.entries(images).map(async ([imageName, image]) => {
|
292
|
-
const errorMessage = await assertImage(image, imageName);
|
296
|
+
const errorMessage = await assertImage(toBuffer(image), imageName);
|
293
297
|
if (errorMessage) {
|
294
298
|
errors[imageName] = errorMessage;
|
295
299
|
}
|
@@ -1,18 +1,12 @@
|
|
1
1
|
import chai from 'chai';
|
2
|
-
import chalk from 'chalk';
|
3
|
-
import Logger from 'loglevel';
|
4
|
-
import EventEmitter from 'events';
|
5
2
|
import {
|
6
3
|
BaseCreeveyTestContext,
|
7
4
|
Config,
|
8
5
|
CreeveyWebdriver,
|
9
|
-
FakeSuite,
|
10
|
-
FakeTest,
|
11
|
-
Images,
|
12
6
|
Options,
|
13
7
|
ServerTest,
|
14
|
-
TEST_EVENTS,
|
15
8
|
TestMessage,
|
9
|
+
TestResult,
|
16
10
|
isDefined,
|
17
11
|
isImageError,
|
18
12
|
} from '../../types.js';
|
@@ -47,9 +41,10 @@ async function getTestsFromStories(
|
|
47
41
|
return testsById;
|
48
42
|
}
|
49
43
|
|
50
|
-
function runHandler(browserName: string,
|
44
|
+
function runHandler(browserName: string, result: Omit<TestResult, 'status'>, error?: unknown): void {
|
51
45
|
// TODO How handle browser corruption?
|
52
|
-
|
46
|
+
const { images } = result;
|
47
|
+
if (images != null && isImageError(error)) {
|
53
48
|
if (typeof error.images == 'string') {
|
54
49
|
const image = images[browserName];
|
55
50
|
if (image) image.error = error.images;
|
@@ -62,31 +57,37 @@ function runHandler(browserName: string, images: Partial<Record<string, Images>>
|
|
62
57
|
}
|
63
58
|
}
|
64
59
|
|
65
|
-
if (error || Object.values(images).some((image) => image?.error != null)) {
|
66
|
-
|
60
|
+
if (error || (images != null && Object.values(images).some((image) => image?.error != null))) {
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
62
|
+
const errorMessage = result.error!;
|
67
63
|
|
68
64
|
const isUnexpectedError =
|
69
65
|
hasTimeout(errorMessage) ||
|
70
66
|
hasDisconnected(errorMessage) ||
|
71
|
-
Object.values(images).some((image) => hasTimeout(image?.error));
|
67
|
+
(images != null && Object.values(images).some((image) => hasTimeout(image?.error)));
|
72
68
|
if (isUnexpectedError) emitWorkerMessage({ type: 'error', payload: { subtype: 'unknown', error: errorMessage } });
|
73
69
|
else
|
74
70
|
emitTestMessage({
|
75
71
|
type: 'end',
|
76
72
|
payload: {
|
77
73
|
status: 'failed',
|
78
|
-
|
79
|
-
error: errorMessage,
|
74
|
+
...result,
|
80
75
|
},
|
81
76
|
});
|
82
77
|
} else {
|
83
|
-
emitTestMessage({
|
78
|
+
emitTestMessage({
|
79
|
+
type: 'end',
|
80
|
+
payload: {
|
81
|
+
status: 'success',
|
82
|
+
...result,
|
83
|
+
},
|
84
|
+
});
|
84
85
|
}
|
85
86
|
}
|
86
87
|
|
87
88
|
async function setupWebdriver(webdriver: CreeveyWebdriver): Promise<[string, CreeveyWebdriver] | undefined> {
|
88
89
|
if ((await webdriver.openBrowser(true)) == null) {
|
89
|
-
logger.error('Failed to start browser');
|
90
|
+
logger().error('Failed to start browser');
|
90
91
|
emitWorkerMessage({
|
91
92
|
type: 'error',
|
92
93
|
payload: { subtype: 'browser', error: 'Failed to start browser' },
|
@@ -114,7 +115,6 @@ function hasTimeout(str: string | null | undefined): boolean {
|
|
114
115
|
}
|
115
116
|
|
116
117
|
export async function start(browser: string, gridUrl: string, config: Config, options: Options): Promise<void> {
|
117
|
-
let retries = 0;
|
118
118
|
const imagesContext: ImageContext = {
|
119
119
|
attachments: [],
|
120
120
|
testFullPath: [],
|
@@ -125,38 +125,16 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
125
125
|
|
126
126
|
if (!webdriver || !sessionId) return;
|
127
127
|
|
128
|
-
const workerLogger = Logger.getLogger(`${browser}:${chalk.gray(sessionId)}`);
|
129
|
-
|
130
|
-
const reporterOptions = {
|
131
|
-
...config.reporterOptions,
|
132
|
-
creevey: {
|
133
|
-
sessionId,
|
134
|
-
reportDir: config.reportDir,
|
135
|
-
browserName: browser,
|
136
|
-
get willRetry() {
|
137
|
-
return retries < config.maxRetries;
|
138
|
-
},
|
139
|
-
get images() {
|
140
|
-
return imagesContext.images;
|
141
|
-
},
|
142
|
-
},
|
143
|
-
};
|
144
|
-
|
145
|
-
class FakeRunner extends EventEmitter {}
|
146
|
-
const runner = new FakeRunner();
|
147
|
-
const Reporter = config.reporter;
|
148
|
-
new Reporter(runner, { reporterOptions });
|
149
|
-
|
150
128
|
const { matchImage, matchImages } = options.odiff
|
151
129
|
? getOdiffMatchers(imagesContext, config)
|
152
130
|
: await getMatchers(imagesContext, config);
|
153
|
-
chai.use(chaiImage(matchImage, matchImages
|
131
|
+
chai.use(chaiImage(matchImage, matchImages));
|
154
132
|
|
155
133
|
const tests = await (async () => {
|
156
134
|
try {
|
157
135
|
return await getTestsFromStories(config, browser, webdriver);
|
158
136
|
} catch (error) {
|
159
|
-
|
137
|
+
logger().error('Failed to get tests from stories:', error);
|
160
138
|
emitWorkerMessage({
|
161
139
|
type: 'error',
|
162
140
|
payload: { subtype: 'browser', error: serializeError(error) },
|
@@ -174,7 +152,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
174
152
|
|
175
153
|
if (!test) {
|
176
154
|
const error = `Test with id ${message.payload.id} not found`;
|
177
|
-
|
155
|
+
logger().error(error);
|
178
156
|
emitWorkerMessage({
|
179
157
|
type: 'error',
|
180
158
|
payload: { subtype: 'test', error },
|
@@ -200,68 +178,55 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
200
178
|
imagesContext.testFullPath = getTestPath(test);
|
201
179
|
imagesContext.images = {};
|
202
180
|
|
203
|
-
retries = message.payload.retries;
|
204
181
|
let error = undefined;
|
205
182
|
|
206
|
-
const fakeSuite: FakeSuite = {
|
207
|
-
title: test.storyPath.slice(0, -1).join('/'),
|
208
|
-
fullTitle: () => fakeSuite.title,
|
209
|
-
titlePath: () => [fakeSuite.title],
|
210
|
-
tests: [],
|
211
|
-
};
|
212
|
-
|
213
|
-
const fakeTest: FakeTest = {
|
214
|
-
parent: fakeSuite,
|
215
|
-
title: [test.story.name, test.testName, test.browser].filter(isDefined).join('/'),
|
216
|
-
fullTitle: () => getTestPath(test).join('/'),
|
217
|
-
titlePath: () => getTestPath(test),
|
218
|
-
currentRetry: () => retries,
|
219
|
-
retires: () => config.maxRetries,
|
220
|
-
slow: () => 1000,
|
221
|
-
};
|
222
|
-
|
223
|
-
fakeSuite.tests.push(fakeTest);
|
224
|
-
|
225
183
|
void (async () => {
|
226
|
-
|
227
|
-
|
228
|
-
|
184
|
+
let timeout;
|
185
|
+
let isRejected = false;
|
229
186
|
const start = Date.now();
|
230
187
|
try {
|
231
188
|
await Promise.race([
|
232
|
-
new Promise(
|
233
|
-
|
234
|
-
|
235
|
-
|
189
|
+
new Promise(
|
190
|
+
(_, reject) =>
|
191
|
+
(timeout = setTimeout(() => {
|
192
|
+
isRejected = true;
|
193
|
+
reject(new Error(`Timeout of ${config.testTimeout}ms exceeded`));
|
194
|
+
}, config.testTimeout)),
|
236
195
|
),
|
237
196
|
(async () => {
|
238
|
-
const context = await webdriver.switchStory(test.story, baseContext
|
197
|
+
const context = await webdriver.switchStory(test.story, baseContext);
|
239
198
|
await test.fn(context);
|
240
199
|
})(),
|
241
200
|
]);
|
242
201
|
} catch (testError) {
|
243
202
|
error = testError;
|
244
|
-
fakeTest.err = error;
|
245
203
|
}
|
246
204
|
const duration = Date.now() - start;
|
247
|
-
|
248
|
-
fakeTest.state = error ? 'failed' : 'passed';
|
249
|
-
fakeTest.duration = duration;
|
250
|
-
fakeTest.speed = duration > fakeTest.slow() ? 'slow' : duration / 2 > fakeTest.slow() ? 'medium' : 'fast';
|
251
|
-
|
252
|
-
if (error) {
|
253
|
-
runner.emit(TEST_EVENTS.TEST_FAIL, fakeTest, error);
|
254
|
-
} else {
|
255
|
-
runner.emit(TEST_EVENTS.TEST_PASS, fakeTest);
|
256
|
-
}
|
257
|
-
runner.emit(TEST_EVENTS.TEST_END, fakeTest);
|
258
|
-
runner.emit(TEST_EVENTS.RUN_END);
|
205
|
+
clearTimeout(timeout);
|
259
206
|
|
260
207
|
await webdriver.afterTest(test);
|
261
208
|
|
262
|
-
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
210
|
+
if (isRejected) {
|
211
|
+
emitWorkerMessage({
|
212
|
+
type: 'error',
|
213
|
+
payload: { subtype: 'unknown', error: serializeError(error) },
|
214
|
+
});
|
215
|
+
} else {
|
216
|
+
const result = {
|
217
|
+
sessionId,
|
218
|
+
browserName: baseContext.browserName,
|
219
|
+
workerId: process.pid,
|
220
|
+
images: imagesContext.images,
|
221
|
+
error: serializeError(error),
|
222
|
+
duration,
|
223
|
+
attachments: imagesContext.attachments,
|
224
|
+
retries: message.payload.retries,
|
225
|
+
};
|
226
|
+
runHandler(baseContext.browserName, result, error);
|
227
|
+
}
|
263
228
|
})().catch((error: unknown) => {
|
264
|
-
|
229
|
+
logger().error('Unexpected error:', error);
|
265
230
|
emitWorkerMessage({
|
266
231
|
type: 'error',
|
267
232
|
payload: { subtype: 'test', error: serializeError(error) },
|
@@ -269,7 +234,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
269
234
|
});
|
270
235
|
});
|
271
236
|
|
272
|
-
|
237
|
+
logger().info('Browser is ready');
|
273
238
|
|
274
239
|
emitWorkerMessage({ type: 'ready' });
|
275
240
|
}
|
package/src/shared/index.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import _ from 'lodash';
|
2
|
-
import { Parameters } from '@storybook/
|
2
|
+
import type { Parameters } from '@storybook/types';
|
3
3
|
import { SetStoriesData, StoriesRaw, CreeveyStoryParams, StoryInput } from '../types.js';
|
4
4
|
import { deserializeRegExp, isSerializedRegExp, isRegExp, serializeRegExp } from './serializeRegExp.js';
|
5
5
|
|