creevey 0.10.0-beta.3 → 0.10.0-beta.30
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/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 +1 -0
- package/dist/client/addon/withCreevey.js +10 -1
- package/dist/client/addon/withCreevey.js.map +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.js +17 -7
- package/dist/client/shared/components/ImagesView/SideBySideView.js.map +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.js +29 -7
- package/dist/client/shared/components/ImagesView/SwapView.js.map +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.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 +41 -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.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/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-C5QCFtF-.js +595 -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 +29 -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.js +38 -21
- package/dist/server/docker.js.map +1 -1
- package/dist/server/index.js +63 -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 +12 -63
- 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 +1 -0
- package/dist/server/master/runner.js +4 -1
- 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 +2 -1
- package/dist/server/playwright/docker-file.js +7 -5
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/internal.d.ts +5 -4
- package/dist/server/playwright/internal.js +91 -71
- package/dist/server/playwright/internal.js.map +1 -1
- package/dist/server/playwright/webdriver.d.ts +1 -1
- package/dist/server/playwright/webdriver.js +1 -1
- 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/reporter.js +13 -9
- package/dist/server/reporter.js.map +1 -1
- package/dist/server/selenium/internal.d.ts +3 -4
- package/dist/server/selenium/internal.js +127 -99
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +9 -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 +1 -1
- package/dist/server/selenium/webdriver.js.map +1 -1
- package/dist/server/telemetry.js +7 -3
- package/dist/server/telemetry.js.map +1 -1
- package/dist/server/testsFiles/parser.js +44 -2
- package/dist/server/testsFiles/parser.js.map +1 -1
- package/dist/server/utils.d.ts +20 -1
- package/dist/server/utils.js +82 -7
- package/dist/server/utils.js.map +1 -1
- package/dist/server/webdriver.d.ts +3 -4
- package/dist/server/webdriver.js +10 -9
- 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/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 +24 -14
- package/dist/server/worker/start.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/types.d.ts +38 -13
- package/dist/types.js.map +1 -1
- package/docs/config.md +3 -0
- package/package.json +66 -64
- package/src/client/addon/components/Panel.tsx +2 -2
- package/src/client/addon/withCreevey.ts +8 -1
- 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 +26 -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 -8
- package/src/server/connection.ts +26 -0
- package/src/server/docker.ts +42 -24
- package/src/server/index.ts +73 -14
- package/src/server/logger.ts +6 -2
- package/src/server/master/api.ts +1 -1
- package/src/server/master/pool.ts +22 -56
- package/src/server/master/queue.ts +77 -0
- package/src/server/master/runner.ts +4 -1
- package/src/server/master/server.ts +1 -1
- package/src/server/master/start.ts +16 -11
- package/src/server/playwright/docker-file.ts +8 -5
- package/src/server/playwright/internal.ts +91 -78
- package/src/server/playwright/webdriver.ts +2 -2
- package/src/server/providers/browser.ts +6 -4
- package/src/server/providers/hybrid.ts +1 -1
- package/src/server/reporter.ts +15 -9
- package/src/server/selenium/internal.ts +131 -107
- package/src/server/selenium/selenoid.ts +9 -7
- package/src/server/selenium/webdriver.ts +2 -2
- package/src/server/telemetry.ts +7 -3
- package/src/server/testsFiles/parser.ts +51 -1
- package/src/server/utils.ts +87 -8
- package/src/server/webdriver.ts +11 -16
- package/src/server/worker/chai-image.ts +4 -4
- package/src/server/worker/match-image.ts +12 -8
- package/src/server/worker/start.ts +25 -16
- package/src/shared/index.ts +1 -1
- package/src/types.ts +40 -15
- 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
@@ -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> {
|
package/src/server/utils.ts
CHANGED
@@ -1,13 +1,15 @@
|
|
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 assert from 'assert';
|
12
|
+
import pidtree from 'pidtree';
|
11
13
|
|
12
14
|
const importMetaUrl = pathToFileURL(__filename).href;
|
13
15
|
|
@@ -15,6 +17,19 @@ export const isShuttingDown = { current: false };
|
|
15
17
|
|
16
18
|
export const configExt = ['.js', '.mjs', '.ts', '.cjs', '.mts', '.cts'];
|
17
19
|
|
20
|
+
const browserTypes = {
|
21
|
+
chromium: 'chromium',
|
22
|
+
'chromium-headless-shell': 'chromium',
|
23
|
+
chrome: 'chromium',
|
24
|
+
'chrome-beta': 'chromium',
|
25
|
+
msedge: 'chromium',
|
26
|
+
'msedge-beta': 'chromium',
|
27
|
+
'msedge-dev': 'chromium',
|
28
|
+
'bidi-chromium': 'chromium',
|
29
|
+
firefox: 'firefox',
|
30
|
+
webkit: 'webkit',
|
31
|
+
} as const;
|
32
|
+
|
18
33
|
export const skipOptionKeys = ['in', 'kinds', 'stories', 'tests', 'reason'];
|
19
34
|
|
20
35
|
function matchBy(pattern: string | string[] | RegExp | undefined, value: string): boolean {
|
@@ -83,7 +98,7 @@ export async function shutdownWorkers(): Promise<void> {
|
|
83
98
|
(worker) =>
|
84
99
|
new Promise<void>((resolve) => {
|
85
100
|
const timeout = setTimeout(() => {
|
86
|
-
worker.
|
101
|
+
if (worker.process.pid) void killTree(worker.process.pid);
|
87
102
|
}, 10000);
|
88
103
|
worker.on('exit', () => {
|
89
104
|
clearTimeout(timeout);
|
@@ -96,6 +111,46 @@ export async function shutdownWorkers(): Promise<void> {
|
|
96
111
|
emitShutdownMessage();
|
97
112
|
}
|
98
113
|
|
114
|
+
export function gracefullyKill(worker: Worker): void {
|
115
|
+
worker.isShuttingDown = true;
|
116
|
+
const timeout = setTimeout(() => {
|
117
|
+
if (worker.process.pid) void killTree(worker.process.pid);
|
118
|
+
}, 10000);
|
119
|
+
worker.on('exit', () => {
|
120
|
+
clearTimeout(timeout);
|
121
|
+
});
|
122
|
+
sendShutdownMessage(worker);
|
123
|
+
}
|
124
|
+
|
125
|
+
export async function killTree(rootPid: number): Promise<void> {
|
126
|
+
const pids = await pidtree(rootPid, { root: true });
|
127
|
+
|
128
|
+
pids.forEach((pid) => {
|
129
|
+
try {
|
130
|
+
process.kill(pid, 'SIGKILL');
|
131
|
+
} catch {
|
132
|
+
/* noop */
|
133
|
+
}
|
134
|
+
});
|
135
|
+
}
|
136
|
+
|
137
|
+
export function shutdown(): void {
|
138
|
+
process.exit();
|
139
|
+
}
|
140
|
+
|
141
|
+
export function shutdownWithError(): void {
|
142
|
+
process.exit(1);
|
143
|
+
}
|
144
|
+
|
145
|
+
export function resolvePlaywrightBrowserType(browserName: string): (typeof browserTypes)[keyof typeof browserTypes] {
|
146
|
+
assert(
|
147
|
+
browserName in browserTypes,
|
148
|
+
new Error(`Failed to match browser name "${browserName}" to playwright browserType`),
|
149
|
+
);
|
150
|
+
|
151
|
+
return browserTypes[browserName as keyof typeof browserTypes];
|
152
|
+
}
|
153
|
+
|
99
154
|
export async function getCreeveyCache(): Promise<string | undefined> {
|
100
155
|
const { default: findCacheDir } = await import('find-cache-dir');
|
101
156
|
return findCacheDir({ name: 'creevey', cwd: dirname(fileURLToPath(importMetaUrl)) });
|
@@ -131,11 +186,12 @@ export function testsToImages(tests: (TestData | undefined)[]): Set<string> {
|
|
131
186
|
|
132
187
|
// https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
|
133
188
|
export const isInsideDocker =
|
134
|
-
fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf-8').includes('docker')
|
189
|
+
(fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf-8').includes('docker')) ||
|
190
|
+
process.env.DOCKER === 'true';
|
135
191
|
|
136
192
|
export const downloadBinary = (downloadUrl: string, destination: string): Promise<void> =>
|
137
193
|
new Promise((resolve, reject) =>
|
138
|
-
get(downloadUrl, (response) => {
|
194
|
+
https.get(downloadUrl, (response) => {
|
139
195
|
if (response.statusCode == 302) {
|
140
196
|
const { location } = response.headers;
|
141
197
|
if (!location) {
|
@@ -175,10 +231,10 @@ export function readDirRecursive(dirPath: string): string[] {
|
|
175
231
|
);
|
176
232
|
}
|
177
233
|
|
178
|
-
const _require = createRequire(importMetaUrl);
|
179
234
|
export function tryToLoadTestsData(filename: string): Partial<Record<string, ServerTest>> | undefined {
|
180
235
|
try {
|
181
|
-
|
236
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
237
|
+
return require(filename) as Partial<Record<string, ServerTest>>;
|
182
238
|
} catch {
|
183
239
|
/* noop */
|
184
240
|
}
|
@@ -204,3 +260,26 @@ export async function loadThroughTSX<T>(
|
|
204
260
|
|
205
261
|
return result;
|
206
262
|
}
|
263
|
+
|
264
|
+
export function waitOnUrl(url: string, timeout: number, delay: number) {
|
265
|
+
const startTime = Date.now();
|
266
|
+
return new Promise<void>((resolve, reject) => {
|
267
|
+
const interval = setInterval(() => {
|
268
|
+
http
|
269
|
+
.get(url, (response) => {
|
270
|
+
if (response.statusCode === 200) {
|
271
|
+
clearInterval(interval);
|
272
|
+
resolve();
|
273
|
+
}
|
274
|
+
})
|
275
|
+
.on('error', () => {
|
276
|
+
// Ignore HTTP errors
|
277
|
+
});
|
278
|
+
|
279
|
+
if (Date.now() - startTime > timeout) {
|
280
|
+
clearInterval(interval);
|
281
|
+
reject(new Error(`${url} didn't respond within ${timeout / 1000} seconds`));
|
282
|
+
}
|
283
|
+
}, delay);
|
284
|
+
});
|
285
|
+
}
|
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/csf';
|
3
|
+
import { logger } from './logger.js';
|
4
|
+
import type { Args } from '@storybook/csf';
|
6
5
|
import {
|
7
6
|
isDefined,
|
8
7
|
StoryInput,
|
@@ -22,15 +21,15 @@ const DOCKER_INTERNAL = 'host.docker.internal';
|
|
22
21
|
export async function resolveStorybookUrl(
|
23
22
|
storybookUrl: string,
|
24
23
|
checkUrl: (url: string) => Promise<boolean>,
|
25
|
-
logger: Logger.Logger = defaultLogger,
|
26
24
|
): Promise<string> {
|
27
|
-
logger.debug('Resolving storybook url');
|
25
|
+
logger().debug('Resolving storybook url');
|
28
26
|
const addresses = getAddresses();
|
27
|
+
// TODO Use Promise.race?
|
29
28
|
for (const ip of addresses) {
|
30
29
|
const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
|
31
|
-
logger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
30
|
+
logger().debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
32
31
|
if (await checkUrl(resolvedUrl)) {
|
33
|
-
logger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
32
|
+
logger().debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
34
33
|
return resolvedUrl;
|
35
34
|
}
|
36
35
|
}
|
@@ -74,11 +73,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
74
73
|
|
75
74
|
abstract afterTest(test: ServerTest): Promise<void>;
|
76
75
|
|
77
|
-
async switchStory(
|
78
|
-
story: StoryInput,
|
79
|
-
context: BaseCreeveyTestContext,
|
80
|
-
logger: Logger.Logger,
|
81
|
-
): Promise<CreeveyTestContext> {
|
76
|
+
async switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext> {
|
82
77
|
const { id, title, name, parameters } = story;
|
83
78
|
const {
|
84
79
|
captureElement = `#${storybookRootID}`,
|
@@ -86,7 +81,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
86
81
|
ignoreElements,
|
87
82
|
} = (parameters.creevey ?? {}) as CreeveyStoryParams;
|
88
83
|
|
89
|
-
logger.debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
|
84
|
+
logger().debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
|
90
85
|
|
91
86
|
let storyPlayResolver: (isCompleted: boolean) => void;
|
92
87
|
let waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
@@ -107,7 +102,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
107
102
|
const isCaptureCalled = await this.selectStory(id, waitForReady);
|
108
103
|
|
109
104
|
if (isCaptureCalled) {
|
110
|
-
logger.debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
|
105
|
+
logger().debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
|
111
106
|
while (!(await waitForComplete)) {
|
112
107
|
waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
113
108
|
}
|
@@ -115,8 +110,8 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
115
110
|
|
116
111
|
unsubscribe();
|
117
112
|
|
118
|
-
if (isCaptureCalled) logger.debug(`Story ${chalk.magenta(id)} completed capturing`);
|
119
|
-
else logger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
|
113
|
+
if (isCaptureCalled) logger().debug(`Story ${chalk.magenta(id)} completed capturing`);
|
114
|
+
else logger().debug(`Story ${chalk.magenta(id)} ready for capturing`);
|
120
115
|
|
121
116
|
return Object.assign(
|
122
117
|
{
|
@@ -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;
|
@@ -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,6 +1,4 @@
|
|
1
1
|
import chai from 'chai';
|
2
|
-
import chalk from 'chalk';
|
3
|
-
import Logger from 'loglevel';
|
4
2
|
import EventEmitter from 'events';
|
5
3
|
import {
|
6
4
|
BaseCreeveyTestContext,
|
@@ -86,7 +84,7 @@ function runHandler(browserName: string, images: Partial<Record<string, Images>>
|
|
86
84
|
|
87
85
|
async function setupWebdriver(webdriver: CreeveyWebdriver): Promise<[string, CreeveyWebdriver] | undefined> {
|
88
86
|
if ((await webdriver.openBrowser(true)) == null) {
|
89
|
-
logger.error('Failed to start browser');
|
87
|
+
logger().error('Failed to start browser');
|
90
88
|
emitWorkerMessage({
|
91
89
|
type: 'error',
|
92
90
|
payload: { subtype: 'browser', error: 'Failed to start browser' },
|
@@ -125,8 +123,6 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
125
123
|
|
126
124
|
if (!webdriver || !sessionId) return;
|
127
125
|
|
128
|
-
const workerLogger = Logger.getLogger(`${browser}:${chalk.gray(sessionId)}`);
|
129
|
-
|
130
126
|
const reporterOptions = {
|
131
127
|
...config.reporterOptions,
|
132
128
|
creevey: {
|
@@ -150,13 +146,13 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
150
146
|
const { matchImage, matchImages } = options.odiff
|
151
147
|
? getOdiffMatchers(imagesContext, config)
|
152
148
|
: await getMatchers(imagesContext, config);
|
153
|
-
chai.use(chaiImage(matchImage, matchImages
|
149
|
+
chai.use(chaiImage(matchImage, matchImages));
|
154
150
|
|
155
151
|
const tests = await (async () => {
|
156
152
|
try {
|
157
153
|
return await getTestsFromStories(config, browser, webdriver);
|
158
154
|
} catch (error) {
|
159
|
-
|
155
|
+
logger().error('Failed to get tests from stories:', error);
|
160
156
|
emitWorkerMessage({
|
161
157
|
type: 'error',
|
162
158
|
payload: { subtype: 'browser', error: serializeError(error) },
|
@@ -174,7 +170,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
174
170
|
|
175
171
|
if (!test) {
|
176
172
|
const error = `Test with id ${message.payload.id} not found`;
|
177
|
-
|
173
|
+
logger().error(error);
|
178
174
|
emitWorkerMessage({
|
179
175
|
type: 'error',
|
180
176
|
payload: { subtype: 'test', error },
|
@@ -226,16 +222,20 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
226
222
|
runner.emit(TEST_EVENTS.RUN_BEGIN);
|
227
223
|
runner.emit(TEST_EVENTS.TEST_BEGIN, fakeTest);
|
228
224
|
|
225
|
+
let timeout;
|
226
|
+
let isRejected = false;
|
229
227
|
const start = Date.now();
|
230
228
|
try {
|
231
229
|
await Promise.race([
|
232
|
-
new Promise(
|
233
|
-
|
234
|
-
|
235
|
-
|
230
|
+
new Promise(
|
231
|
+
(_, reject) =>
|
232
|
+
(timeout = setTimeout(() => {
|
233
|
+
isRejected = true;
|
234
|
+
reject(new Error(`Timeout of ${config.testTimeout}ms exceeded`));
|
235
|
+
}, config.testTimeout)),
|
236
236
|
),
|
237
237
|
(async () => {
|
238
|
-
const context = await webdriver.switchStory(test.story, baseContext
|
238
|
+
const context = await webdriver.switchStory(test.story, baseContext);
|
239
239
|
await test.fn(context);
|
240
240
|
})(),
|
241
241
|
]);
|
@@ -244,6 +244,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
244
244
|
fakeTest.err = error;
|
245
245
|
}
|
246
246
|
const duration = Date.now() - start;
|
247
|
+
clearTimeout(timeout);
|
247
248
|
fakeTest.attachments = imagesContext.attachments;
|
248
249
|
fakeTest.state = error ? 'failed' : 'passed';
|
249
250
|
fakeTest.duration = duration;
|
@@ -259,9 +260,17 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
259
260
|
|
260
261
|
await webdriver.afterTest(test);
|
261
262
|
|
262
|
-
|
263
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
264
|
+
if (isRejected) {
|
265
|
+
emitWorkerMessage({
|
266
|
+
type: 'error',
|
267
|
+
payload: { subtype: 'unknown', error: serializeError(error) },
|
268
|
+
});
|
269
|
+
} else {
|
270
|
+
runHandler(baseContext.browserName, imagesContext.images, error);
|
271
|
+
}
|
263
272
|
})().catch((error: unknown) => {
|
264
|
-
|
273
|
+
logger().error('Unexpected error:', error);
|
265
274
|
emitWorkerMessage({
|
266
275
|
type: 'error',
|
267
276
|
payload: { subtype: 'test', error: serializeError(error) },
|
@@ -269,7 +278,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
|
|
269
278
|
});
|
270
279
|
});
|
271
280
|
|
272
|
-
|
281
|
+
logger().info('Browser is ready');
|
273
282
|
|
274
283
|
emitWorkerMessage({ type: 'ready' });
|
275
284
|
}
|
package/src/shared/index.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import _ from 'lodash';
|
2
|
-
import { Parameters } from '@storybook/csf';
|
2
|
+
import type { Parameters } from '@storybook/csf';
|
3
3
|
import { SetStoriesData, StoriesRaw, CreeveyStoryParams, StoryInput } from '../types.js';
|
4
4
|
import { deserializeRegExp, isSerializedRegExp, isRegExp, serializeRegExp } from './serializeRegExp.js';
|
5
5
|
|
package/src/types.ts
CHANGED
@@ -4,7 +4,7 @@ import type Pixelmatch from 'pixelmatch';
|
|
4
4
|
import type { ODiffOptions } from 'odiff-bin';
|
5
5
|
import type { expect } from 'chai';
|
6
6
|
import type EventEmitter from 'events';
|
7
|
-
import type
|
7
|
+
import type { LaunchOptions } from 'playwright-core';
|
8
8
|
// import type { Browser } from 'playwright-core';
|
9
9
|
|
10
10
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
@@ -102,15 +102,8 @@ export class ChromeConfig {
|
|
102
102
|
}
|
103
103
|
*/
|
104
104
|
export interface BrowserConfigObject {
|
105
|
+
// TODO Restrict browser names for playwright images
|
105
106
|
browserName: string;
|
106
|
-
/**
|
107
|
-
* Browser version. Ignored with Playwright webdriver
|
108
|
-
*/
|
109
|
-
browserVersion?: string;
|
110
|
-
/**
|
111
|
-
* Operation system name. Ignored with Playwright webdriver
|
112
|
-
*/
|
113
|
-
platformName?: string;
|
114
107
|
// customizeBuilder?: (builder: Builder) => Builder;
|
115
108
|
limit?: number;
|
116
109
|
/**
|
@@ -134,15 +127,32 @@ export interface BrowserConfigObject {
|
|
134
127
|
* Used only with `useDocker == false`
|
135
128
|
*/
|
136
129
|
webdriverCommand?: string[];
|
137
|
-
// TODO Check version compatibility
|
138
|
-
// playwrightVersion?: string;
|
139
130
|
// /**
|
140
131
|
// * Use to start standalone playwright browser
|
141
132
|
// */
|
142
133
|
// playwrightBrowser?: () => Promise<Browser>;
|
143
134
|
viewport?: { width: number; height: number };
|
144
135
|
|
145
|
-
|
136
|
+
seleniumCapabilities?: {
|
137
|
+
/**
|
138
|
+
* Browser version. Ignored with Playwright webdriver
|
139
|
+
*/
|
140
|
+
browserVersion?: string;
|
141
|
+
/**
|
142
|
+
* Operation system name. Ignored with Playwright webdriver
|
143
|
+
*/
|
144
|
+
platformName?: string;
|
145
|
+
[name: string]: unknown;
|
146
|
+
};
|
147
|
+
|
148
|
+
playwrightOptions?: Omit<LaunchOptions, 'logger'> & {
|
149
|
+
trace?: {
|
150
|
+
screenshots?: boolean;
|
151
|
+
snapshots?: boolean;
|
152
|
+
sources?: boolean;
|
153
|
+
path: string;
|
154
|
+
};
|
155
|
+
};
|
146
156
|
}
|
147
157
|
|
148
158
|
export type StorybookGlobals = Record<string, unknown>;
|
@@ -161,7 +171,7 @@ export interface CreeveyWebdriver {
|
|
161
171
|
openBrowser(fresh?: boolean): Promise<CreeveyWebdriver | null>;
|
162
172
|
closeBrowser(): Promise<void>;
|
163
173
|
loadStoriesFromBrowser(): Promise<StoriesRaw>;
|
164
|
-
switchStory(story: StoryInput, context: BaseCreeveyTestContext
|
174
|
+
switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext>;
|
165
175
|
afterTest(test: ServerTest): Promise<void>;
|
166
176
|
}
|
167
177
|
|
@@ -197,6 +207,11 @@ export interface Config {
|
|
197
207
|
* Url where storybook hosted on
|
198
208
|
*/
|
199
209
|
resolveStorybookUrl?: () => Promise<string>;
|
210
|
+
/**
|
211
|
+
* Command to automatically start Storybook if it is not running.
|
212
|
+
* For example, `npm run storybook`, `yarn run storybook` etc.
|
213
|
+
*/
|
214
|
+
storybookAutorunCmd?: string;
|
200
215
|
/**
|
201
216
|
* Absolute path to directory with reference images
|
202
217
|
* @default path.join(process.cwd(), './images')
|
@@ -304,6 +319,11 @@ export interface Config {
|
|
304
319
|
* The `--ui` CLI option ignores this option
|
305
320
|
*/
|
306
321
|
failFast: boolean;
|
322
|
+
/**
|
323
|
+
* Start workers in sequential queue
|
324
|
+
* @default false
|
325
|
+
*/
|
326
|
+
useWorkerQueue: boolean;
|
307
327
|
/**
|
308
328
|
* Specify platform for docker images
|
309
329
|
*/
|
@@ -345,9 +365,14 @@ export interface Options {
|
|
345
365
|
screenDir?: string;
|
346
366
|
reportDir?: string;
|
347
367
|
gridUrl?: string;
|
368
|
+
storybookStart?: boolean | string;
|
348
369
|
storybookUrl?: string;
|
370
|
+
storybookPort?: string;
|
371
|
+
storybookAutorunCmd?: string;
|
372
|
+
saveReport: boolean;
|
349
373
|
failFast?: boolean;
|
350
374
|
odiff?: boolean;
|
375
|
+
noDocker?: boolean;
|
351
376
|
}
|
352
377
|
|
353
378
|
export type WorkerError = 'browser' | 'test' | 'unknown';
|
@@ -434,8 +459,8 @@ export interface BaseCreeveyTestContext {
|
|
434
459
|
* @internal
|
435
460
|
*/
|
436
461
|
screenshots: { imageName?: string; screenshot: Buffer }[];
|
437
|
-
matchImage: (image: Buffer, imageName?: string) => Promise<void>;
|
438
|
-
matchImages: (images: Record<string, Buffer>) => Promise<void>;
|
462
|
+
matchImage: (image: Buffer | string, imageName?: string) => Promise<void>;
|
463
|
+
matchImages: (images: Record<string, Buffer | string>) => Promise<void>;
|
439
464
|
}
|
440
465
|
|
441
466
|
export interface CreeveyTestContext extends BaseCreeveyTestContext {
|
package/types/global.d.ts
CHANGED
package/.yarnrc.yml
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
nodeLinker: node-modules
|