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
package/src/creevey.ts
CHANGED
@@ -5,14 +5,14 @@ import { Options } from './types.js';
|
|
5
5
|
import { emitWorkerMessage } from './server/messages.js';
|
6
6
|
import { isShuttingDown, shutdownWorkers } from './server/utils.js';
|
7
7
|
import Logger from 'loglevel';
|
8
|
-
import { logger } from './server/logger.js';
|
8
|
+
import { logger, setRootName } from './server/logger.js';
|
9
9
|
|
10
10
|
function shutdownOnException(reason: unknown): void {
|
11
11
|
if (isShuttingDown.current) return;
|
12
12
|
|
13
13
|
const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
|
14
14
|
|
15
|
-
logger.error(error);
|
15
|
+
logger().error(error);
|
16
16
|
|
17
17
|
process.exitCode = -1;
|
18
18
|
if (cluster.isWorker) emitWorkerMessage({ type: 'error', payload: { subtype: 'unknown', error } });
|
@@ -23,19 +23,25 @@ process.on('uncaughtException', shutdownOnException);
|
|
23
23
|
process.on('unhandledRejection', shutdownOnException);
|
24
24
|
// TODO SIGINT Stuck with selenium
|
25
25
|
process.on('SIGINT', () => {
|
26
|
+
if (isShuttingDown.current) {
|
27
|
+
process.exit(-1);
|
28
|
+
}
|
26
29
|
isShuttingDown.current = true;
|
27
30
|
});
|
28
31
|
|
29
32
|
const argv = minimist<Options>(process.argv.slice(2), {
|
30
|
-
string: ['browser', 'config', 'reporter', 'reportDir', 'screenDir', 'gridUrl', 'storybookUrl'],
|
31
|
-
boolean: ['debug', 'trace', 'ui', 'odiff'],
|
32
|
-
default: { port: 3000 },
|
33
|
-
alias: { port: 'p', config: 'c', debug: 'd', update: 'u' },
|
33
|
+
string: ['browser', 'config', 'reporter', 'reportDir', 'screenDir', 'gridUrl', 'storybookUrl', 'storybookPort'],
|
34
|
+
boolean: ['debug', 'trace', 'ui', 'odiff', 'noDocker'],
|
35
|
+
default: { port: '3000' },
|
36
|
+
alias: { port: 'p', config: 'c', debug: 'd', update: 'u', storybookStart: 's' },
|
34
37
|
});
|
35
38
|
|
39
|
+
if ('port' in argv && !isNaN(argv.port)) argv.port = Number(argv.port);
|
40
|
+
if ('browser' in argv && argv.browser) setRootName(argv.browser);
|
41
|
+
|
36
42
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
37
43
|
if (cluster.isPrimary && argv.reporter) {
|
38
|
-
logger.warn(`--reporter option has been removed please describe reporter in config file:
|
44
|
+
logger().warn(`--reporter option has been removed please describe reporter in config file:
|
39
45
|
import { reporters } from 'mocha';
|
40
46
|
|
41
47
|
const config = {
|
@@ -52,13 +58,13 @@ if (cluster.isPrimary && argv.reporter) {
|
|
52
58
|
// @ts-expect-error: define log level for storybook
|
53
59
|
global.LOGLEVEL = argv.trace ? 'trace' : argv.debug ? 'debug' : 'warn';
|
54
60
|
if (argv.trace) {
|
55
|
-
logger.setDefaultLevel(Logger.levels.TRACE);
|
61
|
+
logger().setDefaultLevel(Logger.levels.TRACE);
|
56
62
|
Logger.setDefaultLevel(Logger.levels.TRACE);
|
57
63
|
} else if (argv.debug) {
|
58
|
-
logger.setDefaultLevel(Logger.levels.DEBUG);
|
64
|
+
logger().setDefaultLevel(Logger.levels.DEBUG);
|
59
65
|
Logger.setDefaultLevel(Logger.levels.DEBUG);
|
60
66
|
} else {
|
61
|
-
logger.setDefaultLevel(Logger.levels.INFO);
|
67
|
+
logger().setDefaultLevel(Logger.levels.INFO);
|
62
68
|
Logger.setDefaultLevel(Logger.levels.INFO);
|
63
69
|
}
|
64
70
|
|
package/src/server/config.ts
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
import fs from 'fs';
|
2
2
|
import path from 'path';
|
3
|
+
import cluster from 'cluster';
|
3
4
|
import { pathToFileURL } from 'url';
|
4
|
-
import { loadStories as
|
5
|
+
import { loadStories as hybridStoriesProvider } from './providers/hybrid.js';
|
5
6
|
import { Config, BrowserConfig, BrowserConfigObject, Options, isDefined } from '../types.js';
|
6
7
|
import { configExt, loadThroughTSX } from './utils.js';
|
7
8
|
import { CreeveyReporter, TeamcityReporter } from './reporter.js';
|
@@ -9,25 +10,27 @@ import { logger } from './logger.js';
|
|
9
10
|
|
10
11
|
export const defaultBrowser = 'chrome';
|
11
12
|
|
12
|
-
export const defaultConfig: Omit<Config, 'gridUrl' | '
|
13
|
+
export const defaultConfig: Omit<Config, 'gridUrl' | 'tsConfig' | 'webdriver'> = {
|
13
14
|
disableTelemetry: false,
|
15
|
+
useWorkerQueue: false,
|
14
16
|
useDocker: true,
|
15
|
-
dockerImage: 'aerokube/selenoid:latest-release',
|
17
|
+
dockerImage: 'aerokube/selenoid:latest-release', // TODO What about playwright?
|
16
18
|
dockerImagePlatform: '',
|
17
19
|
pullImages: true,
|
18
20
|
failFast: false,
|
19
21
|
storybookUrl: 'http://localhost:6006',
|
20
22
|
screenDir: path.resolve('images'),
|
21
23
|
reportDir: path.resolve('report'),
|
24
|
+
testsDir: path.resolve('src'),
|
22
25
|
reporter: process.env.TEAMCITY_VERSION ? TeamcityReporter : CreeveyReporter,
|
23
|
-
storiesProvider:
|
26
|
+
storiesProvider: hybridStoriesProvider,
|
24
27
|
maxRetries: 0,
|
25
28
|
testTimeout: 30000,
|
26
|
-
diffOptions: { threshold: 0.
|
27
|
-
odiffOptions: { threshold: 0.
|
29
|
+
diffOptions: { threshold: 0.1, includeAA: false },
|
30
|
+
odiffOptions: { threshold: 0.1, antialiasing: true },
|
28
31
|
browsers: { [defaultBrowser]: true },
|
29
32
|
hooks: {},
|
30
|
-
testsRegex: /\.creevey\.(t|j)s$/,
|
33
|
+
testsRegex: /\.creevey\.(m|c)?(t|j)s$/,
|
31
34
|
};
|
32
35
|
|
33
36
|
function normalizeBrowserConfig(name: string, config: BrowserConfig): BrowserConfigObject {
|
@@ -73,7 +76,7 @@ export async function readConfig(options: Options): Promise<Config> {
|
|
73
76
|
|
74
77
|
if (!configData.webdriver) {
|
75
78
|
const { SeleniumWebdriver } = await import('./selenium/webdriver.js');
|
76
|
-
logger.warn(
|
79
|
+
logger().warn(
|
77
80
|
"Creevey supports `Selenium` and `Playwright` webdrivers. For backward compatibility `Selenium` is used by default, but it might changed in the future. Please explicitly specify one of webdrivers in your Creevey's config",
|
78
81
|
);
|
79
82
|
configData.webdriver = SeleniumWebdriver;
|
@@ -82,10 +85,29 @@ export async function readConfig(options: Options): Promise<Config> {
|
|
82
85
|
Object.assign(userConfig, configData);
|
83
86
|
}
|
84
87
|
|
88
|
+
if (userConfig.resolveStorybookUrl && !options.storybookUrl) {
|
89
|
+
userConfig.storybookUrl = await userConfig.resolveStorybookUrl();
|
90
|
+
}
|
91
|
+
|
92
|
+
if (options.noDocker) userConfig.useDocker = false;
|
85
93
|
if (options.failFast != undefined) userConfig.failFast = Boolean(options.failFast);
|
86
94
|
if (options.reportDir) userConfig.reportDir = path.resolve(options.reportDir);
|
87
95
|
if (options.screenDir) userConfig.screenDir = path.resolve(options.screenDir);
|
88
96
|
if (options.storybookUrl) userConfig.storybookUrl = options.storybookUrl;
|
97
|
+
if (options.storybookPort && cluster.isPrimary) {
|
98
|
+
const url = new URL(userConfig.storybookUrl);
|
99
|
+
url.port = options.storybookPort;
|
100
|
+
userConfig.storybookUrl = url.toString();
|
101
|
+
}
|
102
|
+
if (typeof options.storybookStart === 'string') userConfig.storybookAutorunCmd = options.storybookStart;
|
103
|
+
|
104
|
+
if (options.storybookStart && cluster.isPrimary) {
|
105
|
+
const { default: getPort } = await import('get-port');
|
106
|
+
const url = new URL(userConfig.storybookUrl);
|
107
|
+
const port = await getPort({ port: Number(url.port) });
|
108
|
+
url.port = `${port}`;
|
109
|
+
userConfig.storybookUrl = url.toString();
|
110
|
+
}
|
89
111
|
|
90
112
|
// NOTE: Hack to pass typescript checking
|
91
113
|
const config = userConfig as Config;
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import type { Config, Options } from '../types';
|
2
|
+
import { waitOnUrl } from './utils.js';
|
3
|
+
import { logger } from './logger.js';
|
4
|
+
|
5
|
+
const RESPONSE_CHECK_TIMEOUT_MS = 10000;
|
6
|
+
const RESPONSE_CHECK_INTERVAL_MS = 200;
|
7
|
+
|
8
|
+
export function getStorybookUrl({ storybookUrl }: Config, { storybookStart }: Options): [string, string | undefined] {
|
9
|
+
if (storybookStart) {
|
10
|
+
const url = new URL(storybookUrl);
|
11
|
+
url.hostname = 'localhost';
|
12
|
+
return [url.toString(), storybookUrl];
|
13
|
+
}
|
14
|
+
return [storybookUrl, undefined];
|
15
|
+
}
|
16
|
+
|
17
|
+
export async function checkIsStorybookConnected(url: string) {
|
18
|
+
try {
|
19
|
+
await waitOnUrl(url, RESPONSE_CHECK_TIMEOUT_MS, RESPONSE_CHECK_INTERVAL_MS);
|
20
|
+
return true;
|
21
|
+
} catch (reason: unknown) {
|
22
|
+
const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
|
23
|
+
logger().error(error);
|
24
|
+
return false;
|
25
|
+
}
|
26
|
+
}
|
package/src/server/docker.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import tar from 'tar-stream';
|
2
|
+
import Logger from 'loglevel';
|
2
3
|
import { Writable } from 'stream';
|
3
4
|
import Dockerode, { Container } from 'dockerode';
|
4
5
|
import { DockerAuth } from '../types.js';
|
@@ -21,7 +22,7 @@ export async function pullImages(
|
|
21
22
|
if (auth) args.authconfig = auth;
|
22
23
|
if (platform) args.platform = platform;
|
23
24
|
|
24
|
-
logger.info('Pull docker images');
|
25
|
+
logger().info('Pull docker images');
|
25
26
|
// TODO Replace with `import from`
|
26
27
|
const { default: yoctoSpinner } = await import('yocto-spinner');
|
27
28
|
for (const image of images) {
|
@@ -50,7 +51,7 @@ export async function pullImages(
|
|
50
51
|
function onProgress(event: { id: string; status: string; progress?: string }): void {
|
51
52
|
if (!/^[a-z0-9]{12}$/i.test(event.id)) return;
|
52
53
|
|
53
|
-
spinner.text = `${image}: [${event.id}] ${event.status} ${event.progress
|
54
|
+
spinner.text = `${image}: [${event.id}] ${event.status} ${event.progress ?? ''}`;
|
54
55
|
}
|
55
56
|
});
|
56
57
|
});
|
@@ -61,7 +62,17 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
|
|
61
62
|
const images = await docker.listImages({ filters: { label: [`creevey=${imageName}`] } });
|
62
63
|
|
63
64
|
if (images.at(0)) {
|
64
|
-
|
65
|
+
await Promise.all(
|
66
|
+
(await docker.listContainers({ all: true, filters: { label: [`creevey=${imageName}`] } })).map(async (info) => {
|
67
|
+
const container = docker.getContainer(info.Id);
|
68
|
+
try {
|
69
|
+
await container.remove({ force: true });
|
70
|
+
} catch {
|
71
|
+
/* noop */
|
72
|
+
}
|
73
|
+
}),
|
74
|
+
);
|
75
|
+
logger().info(`Image ${imageName} already exists`);
|
65
76
|
return;
|
66
77
|
}
|
67
78
|
|
@@ -70,15 +81,20 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
|
|
70
81
|
pack.finalize();
|
71
82
|
|
72
83
|
const { default: yoctoSpinner } = await import('yocto-spinner');
|
73
|
-
const spinner = yoctoSpinner({ text: `${imageName}: Build start` })
|
84
|
+
const spinner = yoctoSpinner({ text: `${imageName}: Build start` });
|
85
|
+
if (logger().getLevel() > Logger.levels.DEBUG) {
|
86
|
+
spinner.start();
|
87
|
+
}
|
88
|
+
let isFailed = false;
|
74
89
|
await new Promise<void>((resolve, reject) => {
|
75
90
|
void docker.buildImage(
|
76
91
|
// @ts-expect-error Type incompatibility AsyncIterator and AsyncIterableIterator
|
77
92
|
pack,
|
78
|
-
|
93
|
+
// TODO Support buildkit decode grpc (version: '2')
|
94
|
+
{ t: imageName, labels: { creevey: imageName }, version: '1' },
|
79
95
|
(buildError: Error | null, stream) => {
|
80
96
|
if (buildError || !stream) {
|
81
|
-
spinner.error(buildError?.message);
|
97
|
+
// spinner.error(buildError?.message);
|
82
98
|
reject(buildError ?? new Error('Unknown error'));
|
83
99
|
return;
|
84
100
|
}
|
@@ -86,6 +102,8 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
|
|
86
102
|
docker.modem.followProgress(stream, onFinished, onProgress);
|
87
103
|
|
88
104
|
function onFinished(error: Error | null): void {
|
105
|
+
if (isFailed) return;
|
106
|
+
|
89
107
|
if (error) {
|
90
108
|
spinner.error(error.message);
|
91
109
|
reject(error);
|
@@ -95,10 +113,23 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
|
|
95
113
|
resolve();
|
96
114
|
}
|
97
115
|
|
98
|
-
function onProgress(
|
99
|
-
|
100
|
-
|
101
|
-
|
116
|
+
function onProgress(
|
117
|
+
event:
|
118
|
+
| { stream: string }
|
119
|
+
| { errorDetail: { code: number; message: string }; error: string }
|
120
|
+
| { id: string; aux: string }, // NOTE: Only with `version: '2'`
|
121
|
+
): void {
|
122
|
+
if ('stream' in event) {
|
123
|
+
if (logger().getLevel() <= Logger.levels.DEBUG) {
|
124
|
+
logger().debug(event.stream.trim());
|
125
|
+
} else {
|
126
|
+
spinner.text = `${imageName}: [Build] - ${event.stream}`;
|
127
|
+
}
|
128
|
+
} else if ('errorDetail' in event) {
|
129
|
+
isFailed = true;
|
130
|
+
spinner.error(event.error);
|
131
|
+
reject(new Error(event.error));
|
132
|
+
}
|
102
133
|
}
|
103
134
|
},
|
104
135
|
);
|
@@ -111,18 +142,6 @@ export async function runImage(
|
|
111
142
|
options: Record<string, unknown>,
|
112
143
|
debug: boolean,
|
113
144
|
): Promise<string> {
|
114
|
-
await Promise.all(
|
115
|
-
(await docker.listContainers({ all: true, filters: { ancestor: [image] } })).map(async (info) => {
|
116
|
-
const container = docker.getContainer(info.Id);
|
117
|
-
try {
|
118
|
-
await container.stop();
|
119
|
-
} catch {
|
120
|
-
/* noop */
|
121
|
-
}
|
122
|
-
await container.remove();
|
123
|
-
}),
|
124
|
-
);
|
125
|
-
|
126
145
|
const hub = docker.run(image, args, debug ? process.stdout : new DevNull(), options, (error) => {
|
127
146
|
if (error) throw error;
|
128
147
|
});
|
@@ -132,8 +151,7 @@ export async function runImage(
|
|
132
151
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
133
152
|
subscribeOn('shutdown', async () => {
|
134
153
|
try {
|
135
|
-
await container.
|
136
|
-
await container.remove();
|
154
|
+
await container.remove({ force: true });
|
137
155
|
} catch {
|
138
156
|
/* noop */
|
139
157
|
}
|
package/src/server/index.ts
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
import cluster from 'cluster';
|
2
|
+
import path from 'path';
|
3
|
+
import { exec } from 'shelljs';
|
4
|
+
import { getUserAgent } from 'package-manager-detector/detect';
|
5
|
+
import { resolveCommand } from 'package-manager-detector/commands';
|
2
6
|
import { readConfig, defaultBrowser } from './config.js';
|
3
7
|
import { Options, Config, BrowserConfigObject, isWorkerMessage } from '../types.js';
|
4
8
|
import { logger } from './logger.js';
|
5
9
|
import { SeleniumWebdriver } from './selenium/webdriver.js';
|
6
10
|
import { LOCALHOST_REGEXP } from './webdriver.js';
|
7
|
-
import { isInsideDocker } from './utils.js';
|
8
|
-
import { sendWorkerMessage } from './messages.js';
|
9
|
-
import { playwrightDockerFile } from './playwright/docker-file.js';
|
11
|
+
import { isInsideDocker, killTree, resolvePlaywrightBrowserType, shutdownWithError } from './utils.js';
|
12
|
+
import { sendWorkerMessage, subscribeOn } from './messages.js';
|
10
13
|
import { buildImage } from './docker.js';
|
14
|
+
import { mkdir, writeFile } from 'fs/promises';
|
15
|
+
import { getStorybookUrl, checkIsStorybookConnected } from './connection.js';
|
16
|
+
import assert from 'assert';
|
11
17
|
|
12
18
|
async function startWebdriverServer(browser: string, config: Config, options: Options): Promise<string | undefined> {
|
13
19
|
if (config.webdriver === SeleniumWebdriver) {
|
@@ -25,16 +31,19 @@ async function startWebdriverServer(browser: string, config: Config, options: Op
|
|
25
31
|
} else {
|
26
32
|
if (config.gridUrl) return undefined;
|
27
33
|
|
28
|
-
|
34
|
+
if (!config.useDocker) {
|
35
|
+
if (cluster.isPrimary) return undefined;
|
36
|
+
|
37
|
+
const { browserName } = config.browsers[browser] as BrowserConfigObject;
|
38
|
+
return `creevey://${resolvePlaywrightBrowserType(browserName)}.playwright`;
|
39
|
+
}
|
40
|
+
|
29
41
|
const {
|
30
42
|
default: { version },
|
31
43
|
} = await import('playwright-core/package.json', { with: { type: 'json' } });
|
32
44
|
|
33
45
|
if (cluster.isWorker) {
|
34
46
|
// TODO Re-use dockerImage
|
35
|
-
|
36
|
-
// TODO Use https://hub.docker.com/r/playwright/chrome
|
37
|
-
// NOTE It will be possible to use `chrome` browserName
|
38
47
|
const { startPlaywrightContainer } = await import('./playwright/docker.js');
|
39
48
|
const { browserName } = config.browsers[browser] as BrowserConfigObject;
|
40
49
|
|
@@ -43,11 +52,18 @@ async function startWebdriverServer(browser: string, config: Config, options: Op
|
|
43
52
|
|
44
53
|
return host;
|
45
54
|
} else {
|
46
|
-
const
|
55
|
+
const { playwrightDockerFile } = await import('./playwright/docker-file.js');
|
56
|
+
const browsers = [
|
57
|
+
...new Set(
|
58
|
+
Object.values(config.browsers).map(
|
59
|
+
(c) => [(c as BrowserConfigObject).browserName, (c as BrowserConfigObject).playwrightOptions] as const,
|
60
|
+
),
|
61
|
+
),
|
62
|
+
];
|
47
63
|
await Promise.all(
|
48
|
-
browsers.map(async (browserName) => {
|
64
|
+
browsers.map(async ([browserName, launchOptions]) => {
|
49
65
|
const imageName = `creevey/${browserName}:v${version}`;
|
50
|
-
const dockerfile = playwrightDockerFile(browserName, version);
|
66
|
+
const dockerfile = playwrightDockerFile(browserName, version, launchOptions);
|
51
67
|
|
52
68
|
await buildImage(imageName, dockerfile);
|
53
69
|
}),
|
@@ -79,6 +95,10 @@ export default async function (options: Options): Promise<void> {
|
|
79
95
|
const { browser = defaultBrowser, update, ui, port } = options;
|
80
96
|
let gridUrl = cluster.isPrimary ? config.gridUrl : options.gridUrl;
|
81
97
|
|
98
|
+
// TODO Add package.json with `"type": "commonjs"` as workaround for esm packages to load `data.js`
|
99
|
+
await mkdir(config.reportDir, { recursive: true });
|
100
|
+
await writeFile(path.join(config.reportDir, 'package.json'), '{"type": "commonjs"}');
|
101
|
+
|
82
102
|
// NOTE: We don't need docker nor selenoid for update option
|
83
103
|
if (
|
84
104
|
!(gridUrl || (Object.values(config.browsers) as BrowserConfigObject[]).every(({ gridUrl }) => gridUrl)) &&
|
@@ -87,6 +107,45 @@ export default async function (options: Options): Promise<void> {
|
|
87
107
|
gridUrl = await startWebdriverServer(browser, config, options);
|
88
108
|
}
|
89
109
|
|
110
|
+
if (cluster.isPrimary) {
|
111
|
+
const [localUrl, remoteUrl] = getStorybookUrl(config, options);
|
112
|
+
const pm = getUserAgent();
|
113
|
+
assert(pm, new Error('Failed to detect current package manager'));
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
115
|
+
const { command, args } = resolveCommand(pm, 'run', ['storybook', 'dev'])!;
|
116
|
+
|
117
|
+
if (options.storybookStart) {
|
118
|
+
const storybookPort = new URL(localUrl).port;
|
119
|
+
const storybookCommand = `${config.storybookAutorunCmd ?? [command, ...args, '--ci'].join(' ')} -p ${storybookPort}`;
|
120
|
+
|
121
|
+
logger().info(`Start Storybook via \`${storybookCommand}\`, it should be accessible at:`);
|
122
|
+
logger().info(`Local - ${localUrl}`);
|
123
|
+
if (remoteUrl && localUrl != remoteUrl) logger().info(`On your network - ${remoteUrl}`);
|
124
|
+
logger().info('Waiting Storybook...');
|
125
|
+
|
126
|
+
const storybook = exec(storybookCommand, { async: true });
|
127
|
+
subscribeOn('shutdown', () => {
|
128
|
+
if (storybook.pid) void killTree(storybook.pid);
|
129
|
+
});
|
130
|
+
} else {
|
131
|
+
logger().info('Storybook should be started and be accessible at:');
|
132
|
+
logger().info(`Local - ${localUrl}`);
|
133
|
+
if (remoteUrl && localUrl != remoteUrl) logger().info(`On your network - ${remoteUrl}`);
|
134
|
+
logger().info(
|
135
|
+
'Tip: Creevey can start Storybook automatically by using `-s` option at the command line. (e.g., yarn/npm run creevey -s)',
|
136
|
+
);
|
137
|
+
logger().info('Waiting Storybook...');
|
138
|
+
}
|
139
|
+
|
140
|
+
const isConnected = await checkIsStorybookConnected(localUrl);
|
141
|
+
if (isConnected) {
|
142
|
+
logger().info('Storybook connected!\n');
|
143
|
+
} else {
|
144
|
+
logger().error('Storybook is not responding. Please start Storybook and restart Creevey');
|
145
|
+
shutdownWithError();
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
90
149
|
switch (true) {
|
91
150
|
case Boolean(update): {
|
92
151
|
(await import('./update.js')).update(config, typeof update == 'string' ? update : undefined);
|
@@ -97,25 +156,25 @@ export default async function (options: Options): Promise<void> {
|
|
97
156
|
try {
|
98
157
|
await import('selenium-webdriver');
|
99
158
|
} catch {
|
100
|
-
logger.error('Failed to start Creevey, missing required dependency: "selenium-webdriver"');
|
159
|
+
logger().error('Failed to start Creevey, missing required dependency: "selenium-webdriver"');
|
101
160
|
process.exit(-1);
|
102
161
|
}
|
103
162
|
} else {
|
104
163
|
try {
|
105
164
|
await import('playwright-core');
|
106
165
|
} catch {
|
107
|
-
logger.error('Failed to start Creevey, missing required dependency: "playwright-core"');
|
166
|
+
logger().error('Failed to start Creevey, missing required dependency: "playwright-core"');
|
108
167
|
process.exit(-1);
|
109
168
|
}
|
110
169
|
}
|
111
|
-
logger.info('Starting Master Process');
|
170
|
+
logger().info('Starting Master Process');
|
112
171
|
|
113
172
|
const resolveApi = (await import('./master/server.js')).start(config.reportDir, port, ui);
|
114
173
|
|
115
174
|
return (await import('./master/start.js')).start(gridUrl, config, options, resolveApi);
|
116
175
|
}
|
117
176
|
default: {
|
118
|
-
logger.info(`Starting Worker for ${browser}`);
|
177
|
+
logger().info(`Starting Worker for ${browser}`);
|
119
178
|
|
120
179
|
// NOTE: We assume that we pass `gridUrl` to worker CLI options
|
121
180
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
package/src/server/logger.ts
CHANGED
@@ -10,12 +10,16 @@ export const colors = {
|
|
10
10
|
ERROR: chalk.red,
|
11
11
|
};
|
12
12
|
|
13
|
+
let rootName = 'Creevey';
|
14
|
+
|
13
15
|
prefix.reg(Logger);
|
14
16
|
prefix.apply(Logger, {
|
15
|
-
format(level, name =
|
17
|
+
format(level, name = rootName) {
|
16
18
|
const levelColor = colors[level.toUpperCase() as keyof typeof colors];
|
17
19
|
return `[${name}:${chalk.gray(process.pid)}] ${levelColor(level)} =>`;
|
18
20
|
},
|
19
21
|
});
|
20
22
|
|
21
|
-
export const
|
23
|
+
export const setRootName = (newName: string) => (rootName = newName);
|
24
|
+
|
25
|
+
export const logger = () => Logger.getLogger(rootName);
|
package/src/server/master/api.ts
CHANGED
@@ -26,7 +26,7 @@ export default function creeveyApi(runner: Runner): CreeveyApi {
|
|
26
26
|
|
27
27
|
handleMessage(ws: WebSocket, message: WebSocket.Data) {
|
28
28
|
if (typeof message != 'string') {
|
29
|
-
logger.info('unhandled message', message);
|
29
|
+
logger().info('unhandled message', message);
|
30
30
|
return;
|
31
31
|
}
|
32
32
|
|
@@ -1,18 +1,9 @@
|
|
1
|
-
import
|
1
|
+
import { Worker as ClusterWorker } from 'cluster';
|
2
2
|
import { EventEmitter } from 'events';
|
3
|
-
import {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
BrowserConfigObject,
|
8
|
-
WorkerMessage,
|
9
|
-
TestStatus,
|
10
|
-
isWorkerMessage,
|
11
|
-
} from '../../types.js';
|
12
|
-
import { sendTestMessage, sendShutdownMessage, subscribeOnWorker } from '../messages.js';
|
13
|
-
import { isShuttingDown } from '../utils.js';
|
14
|
-
|
15
|
-
const FORK_RETRIES = 5;
|
3
|
+
import { Worker, Config, TestResult, BrowserConfigObject, TestStatus } from '../../types.js';
|
4
|
+
import { sendTestMessage, subscribeOnWorker } from '../messages.js';
|
5
|
+
import { gracefullyKill, isShuttingDown } from '../utils.js';
|
6
|
+
import { WorkerQueue } from './queue.js';
|
16
7
|
|
17
8
|
interface WorkerTest {
|
18
9
|
id: string;
|
@@ -28,10 +19,12 @@ export default class Pool extends EventEmitter {
|
|
28
19
|
private forcedStop = false;
|
29
20
|
private failFast: boolean;
|
30
21
|
private gridUrl?: string;
|
22
|
+
private storybookUrl: string;
|
31
23
|
public get isRunning(): boolean {
|
32
24
|
return this.workers.length !== this.freeWorkers.length;
|
33
25
|
}
|
34
26
|
constructor(
|
27
|
+
public scheduler: WorkerQueue,
|
35
28
|
config: Config,
|
36
29
|
private browser: string,
|
37
30
|
gridUrl?: string,
|
@@ -42,14 +35,18 @@ export default class Pool extends EventEmitter {
|
|
42
35
|
this.maxRetries = config.maxRetries;
|
43
36
|
this.config = config.browsers[browser] as BrowserConfigObject;
|
44
37
|
this.gridUrl = this.config.gridUrl ?? gridUrl;
|
38
|
+
this.storybookUrl = this.config.storybookUrl ?? config.storybookUrl;
|
45
39
|
}
|
46
40
|
|
47
41
|
async init(): Promise<void> {
|
48
42
|
const poolSize = Math.max(1, this.config.limit ?? 1);
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
43
|
+
this.workers = (
|
44
|
+
await Promise.all(
|
45
|
+
Array.from({ length: poolSize }).map(() =>
|
46
|
+
this.scheduler.forkWorker(this.browser, this.storybookUrl, this.gridUrl),
|
47
|
+
),
|
48
|
+
)
|
49
|
+
).filter((workerOrError): workerOrError is Worker => workerOrError instanceof ClusterWorker);
|
53
50
|
if (this.workers.length != poolSize)
|
54
51
|
throw new Error(`Can't instantiate workers for ${this.browser} due many errors`);
|
55
52
|
this.workers.forEach((worker) => {
|
@@ -66,7 +63,7 @@ export default class Pool extends EventEmitter {
|
|
66
63
|
return true;
|
67
64
|
}
|
68
65
|
|
69
|
-
stop()
|
66
|
+
stop() {
|
70
67
|
if (!this.isRunning) {
|
71
68
|
this.emit('stop');
|
72
69
|
return;
|
@@ -76,7 +73,7 @@ export default class Pool extends EventEmitter {
|
|
76
73
|
this.queue = [];
|
77
74
|
}
|
78
75
|
|
79
|
-
process()
|
76
|
+
process() {
|
80
77
|
const worker = this.getFreeWorker();
|
81
78
|
const test = this.queue.at(0);
|
82
79
|
|
@@ -99,7 +96,9 @@ export default class Pool extends EventEmitter {
|
|
99
96
|
|
100
97
|
sendTestMessage(worker, { type: 'start', payload: test });
|
101
98
|
|
102
|
-
|
99
|
+
setImmediate(() => {
|
100
|
+
this.process();
|
101
|
+
});
|
103
102
|
}
|
104
103
|
|
105
104
|
private sendStatus(message: { id: string; status: TestStatus; result?: TestResult }): void {
|
@@ -120,34 +119,12 @@ export default class Pool extends EventEmitter {
|
|
120
119
|
return this.aliveWorkers.filter((worker) => !worker.isRunning);
|
121
120
|
}
|
122
121
|
|
123
|
-
private async forkWorker(retry = 0): Promise<Worker | { error: string }> {
|
124
|
-
cluster.setupPrimary({
|
125
|
-
args: ['--browser', this.browser, ...(this.gridUrl ? ['--gridUrl', this.gridUrl] : []), ...process.argv.slice(2)],
|
126
|
-
});
|
127
|
-
const worker = cluster.fork();
|
128
|
-
const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
|
129
|
-
const readyHandler = (message: unknown): void => {
|
130
|
-
if (!isWorkerMessage(message) || message.type == 'port') return;
|
131
|
-
worker.off('message', readyHandler);
|
132
|
-
resolve(message);
|
133
|
-
};
|
134
|
-
worker.on('message', readyHandler);
|
135
|
-
});
|
136
|
-
|
137
|
-
if (message.type != 'error') return worker;
|
138
|
-
|
139
|
-
this.gracefullyKill(worker);
|
140
|
-
|
141
|
-
if (retry == FORK_RETRIES) return message.payload;
|
142
|
-
return this.forkWorker(retry + 1);
|
143
|
-
}
|
144
|
-
|
145
122
|
private exitHandler(worker: Worker): void {
|
146
123
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
147
124
|
worker.once('exit', async () => {
|
148
125
|
if (isShuttingDown.current) return;
|
149
126
|
|
150
|
-
const workerOrError = await this.forkWorker();
|
127
|
+
const workerOrError = await this.scheduler.forkWorker(this.browser, this.storybookUrl, this.gridUrl);
|
151
128
|
|
152
129
|
if (!(workerOrError instanceof ClusterWorker))
|
153
130
|
throw new Error(`Can't instantiate worker for ${this.browser} due many errors`);
|
@@ -158,17 +135,6 @@ export default class Pool extends EventEmitter {
|
|
158
135
|
});
|
159
136
|
}
|
160
137
|
|
161
|
-
private gracefullyKill(worker: Worker): void {
|
162
|
-
worker.isShuttingDown = true;
|
163
|
-
const timeout = setTimeout(() => {
|
164
|
-
worker.kill();
|
165
|
-
}, 10000);
|
166
|
-
worker.on('exit', () => {
|
167
|
-
clearTimeout(timeout);
|
168
|
-
});
|
169
|
-
sendShutdownMessage(worker);
|
170
|
-
}
|
171
|
-
|
172
138
|
private shouldRetry(test: WorkerTest): boolean {
|
173
139
|
return test.retries < this.maxRetries && !this.forcedStop;
|
174
140
|
}
|
@@ -200,7 +166,7 @@ export default class Pool extends EventEmitter {
|
|
200
166
|
});
|
201
167
|
|
202
168
|
if (message.payload.subtype == 'unknown') {
|
203
|
-
|
169
|
+
gracefullyKill(worker);
|
204
170
|
}
|
205
171
|
|
206
172
|
this.handleTestResult(worker, test, { status: 'failed', error: message.payload.error });
|