creevey 0.10.0-beta.4 → 0.10.0-beta.40
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-B0Xv0lOY.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 +27 -5
- 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 +76 -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/reporter.d.ts +4 -19
- package/dist/server/reporter.js +30 -21
- package/dist/server/reporter.js.map +1 -1
- 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 +45 -73
- package/dist/server/worker/start.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/types.d.ts +40 -8
- 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 +28 -6
- 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 +94 -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/reporter.ts +37 -34
- 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 +49 -86
- package/src/shared/index.ts +1 -1
- package/src/types.ts +44 -8
- 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/server/docker.ts
CHANGED
@@ -1,9 +1,10 @@
|
|
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';
|
5
|
-
import { subscribeOn } from './messages.js';
|
6
6
|
import { logger } from './logger.js';
|
7
|
+
import { setWorkerContainer } from './worker/context.js';
|
7
8
|
|
8
9
|
const docker = new Dockerode();
|
9
10
|
|
@@ -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,18 +51,46 @@ 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
|
});
|
57
58
|
}
|
58
59
|
}
|
59
60
|
|
60
|
-
export async function buildImage(imageName: string, dockerfile: string): Promise<void> {
|
61
|
+
export async function buildImage(imageName: string, version: string, dockerfile: string): Promise<void> {
|
61
62
|
const images = await docker.listImages({ filters: { label: [`creevey=${imageName}`] } });
|
62
63
|
|
63
|
-
|
64
|
-
|
64
|
+
const containers = await docker.listContainers({ all: true, filters: { label: [`creevey=${imageName}`] } });
|
65
|
+
if (containers.length > 0) {
|
66
|
+
await Promise.all(
|
67
|
+
containers.map(async (info) => {
|
68
|
+
const container = docker.getContainer(info.Id);
|
69
|
+
try {
|
70
|
+
await container.remove({ force: true });
|
71
|
+
} catch {
|
72
|
+
/* noop */
|
73
|
+
}
|
74
|
+
}),
|
75
|
+
);
|
76
|
+
}
|
77
|
+
|
78
|
+
const oldImages = images.filter((info) => info.Labels.version !== version);
|
79
|
+
if (oldImages.length > 0) {
|
80
|
+
await Promise.all(
|
81
|
+
oldImages.map(async (info) => {
|
82
|
+
const image = docker.getImage(info.Id);
|
83
|
+
try {
|
84
|
+
await image.remove({ force: true });
|
85
|
+
} catch {
|
86
|
+
/* noop */
|
87
|
+
}
|
88
|
+
}),
|
89
|
+
);
|
90
|
+
}
|
91
|
+
|
92
|
+
if (oldImages.length !== images.length) {
|
93
|
+
logger().info(`Image ${imageName} already exists`);
|
65
94
|
return;
|
66
95
|
}
|
67
96
|
|
@@ -70,15 +99,20 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
|
|
70
99
|
pack.finalize();
|
71
100
|
|
72
101
|
const { default: yoctoSpinner } = await import('yocto-spinner');
|
73
|
-
const spinner = yoctoSpinner({ text: `${imageName}: Build start` })
|
102
|
+
const spinner = yoctoSpinner({ text: `${imageName}: Build start` });
|
103
|
+
if (logger().getLevel() > Logger.levels.DEBUG) {
|
104
|
+
spinner.start();
|
105
|
+
}
|
106
|
+
let isFailed = false;
|
74
107
|
await new Promise<void>((resolve, reject) => {
|
75
108
|
void docker.buildImage(
|
76
109
|
// @ts-expect-error Type incompatibility AsyncIterator and AsyncIterableIterator
|
77
110
|
pack,
|
78
|
-
|
111
|
+
// TODO Support buildkit decode grpc (version: '2')
|
112
|
+
{ t: imageName, labels: { creevey: imageName, version }, version: '1' },
|
79
113
|
(buildError: Error | null, stream) => {
|
80
114
|
if (buildError || !stream) {
|
81
|
-
spinner.error(buildError?.message);
|
115
|
+
// spinner.error(buildError?.message);
|
82
116
|
reject(buildError ?? new Error('Unknown error'));
|
83
117
|
return;
|
84
118
|
}
|
@@ -86,6 +120,8 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
|
|
86
120
|
docker.modem.followProgress(stream, onFinished, onProgress);
|
87
121
|
|
88
122
|
function onFinished(error: Error | null): void {
|
123
|
+
if (isFailed) return;
|
124
|
+
|
89
125
|
if (error) {
|
90
126
|
spinner.error(error.message);
|
91
127
|
reject(error);
|
@@ -95,10 +131,23 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
|
|
95
131
|
resolve();
|
96
132
|
}
|
97
133
|
|
98
|
-
function onProgress(
|
99
|
-
|
100
|
-
|
101
|
-
|
134
|
+
function onProgress(
|
135
|
+
event:
|
136
|
+
| { stream: string }
|
137
|
+
| { errorDetail: { code: number; message: string }; error: string }
|
138
|
+
| { id: string; aux: string }, // NOTE: Only with `version: '2'`
|
139
|
+
): void {
|
140
|
+
if ('stream' in event) {
|
141
|
+
if (logger().getLevel() <= Logger.levels.DEBUG) {
|
142
|
+
logger().debug(event.stream.trim());
|
143
|
+
} else {
|
144
|
+
spinner.text = `${imageName}: [Build] - ${event.stream}`;
|
145
|
+
}
|
146
|
+
} else if ('errorDetail' in event) {
|
147
|
+
isFailed = true;
|
148
|
+
spinner.error(event.error);
|
149
|
+
reject(new Error(event.error));
|
150
|
+
}
|
102
151
|
}
|
103
152
|
},
|
104
153
|
);
|
@@ -111,33 +160,13 @@ export async function runImage(
|
|
111
160
|
options: Record<string, unknown>,
|
112
161
|
debug: boolean,
|
113
162
|
): 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
163
|
const hub = docker.run(image, args, debug ? process.stdout : new DevNull(), options, (error) => {
|
127
164
|
if (error) throw error;
|
128
165
|
});
|
129
166
|
|
130
167
|
return new Promise((resolve) => {
|
131
168
|
hub.once('container', (container: Container) => {
|
132
|
-
|
133
|
-
subscribeOn('shutdown', async () => {
|
134
|
-
try {
|
135
|
-
await container.stop();
|
136
|
-
await container.remove();
|
137
|
-
} catch {
|
138
|
-
/* noop */
|
139
|
-
}
|
140
|
-
});
|
169
|
+
setWorkerContainer(container);
|
141
170
|
});
|
142
171
|
hub.once(
|
143
172
|
'start',
|
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';
|
9
|
+
import { getStorybookUrl, checkIsStorybookConnected } from './connection.js';
|
5
10
|
import { SeleniumWebdriver } from './selenium/webdriver.js';
|
6
11
|
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';
|
12
|
+
import { isInsideDocker, killTree, resolvePlaywrightBrowserType, shutdownWithError } from './utils.js';
|
13
|
+
import { sendWorkerMessage, subscribeOn } from './messages.js';
|
10
14
|
import { buildImage } from './docker.js';
|
15
|
+
import { mkdir, writeFile } from 'fs/promises';
|
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,31 +31,38 @@ 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)}`;
|
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
|
|
41
50
|
const imageName = `creevey/${browserName}:v${version}`;
|
42
|
-
const host = await startPlaywrightContainer(imageName, options.debug);
|
51
|
+
const host = await startPlaywrightContainer(imageName, browser, config, options.debug);
|
43
52
|
|
44
53
|
return host;
|
45
54
|
} else {
|
55
|
+
const { playwrightDockerFile } = await import('./playwright/docker-file.js');
|
56
|
+
const {
|
57
|
+
default: { version: creeveyVersion },
|
58
|
+
} = await import('../../package.json', { with: { type: 'json' } });
|
46
59
|
const browsers = [...new Set(Object.values(config.browsers).map((c) => (c as BrowserConfigObject).browserName))];
|
47
60
|
await Promise.all(
|
48
61
|
browsers.map(async (browserName) => {
|
49
62
|
const imageName = `creevey/${browserName}:v${version}`;
|
50
|
-
const dockerfile = playwrightDockerFile(browserName, version);
|
63
|
+
const dockerfile = await playwrightDockerFile(browserName, version);
|
51
64
|
|
52
|
-
await buildImage(imageName, dockerfile);
|
65
|
+
await buildImage(imageName, creeveyVersion, dockerfile);
|
53
66
|
}),
|
54
67
|
);
|
55
68
|
|
@@ -79,6 +92,10 @@ export default async function (options: Options): Promise<void> {
|
|
79
92
|
const { browser = defaultBrowser, update, ui, port } = options;
|
80
93
|
let gridUrl = cluster.isPrimary ? config.gridUrl : options.gridUrl;
|
81
94
|
|
95
|
+
// TODO Add package.json with `"type": "commonjs"` as workaround for esm packages to load `data.js`
|
96
|
+
await mkdir(config.reportDir, { recursive: true });
|
97
|
+
await writeFile(path.join(config.reportDir, 'package.json'), '{"type": "commonjs"}');
|
98
|
+
|
82
99
|
// NOTE: We don't need docker nor selenoid for update option
|
83
100
|
if (
|
84
101
|
!(gridUrl || (Object.values(config.browsers) as BrowserConfigObject[]).every(({ gridUrl }) => gridUrl)) &&
|
@@ -87,6 +104,47 @@ export default async function (options: Options): Promise<void> {
|
|
87
104
|
gridUrl = await startWebdriverServer(browser, config, options);
|
88
105
|
}
|
89
106
|
|
107
|
+
if (cluster.isPrimary) {
|
108
|
+
const [localUrl, remoteUrl] = getStorybookUrl(config, options);
|
109
|
+
|
110
|
+
if (options.storybookStart) {
|
111
|
+
const pm = getUserAgent();
|
112
|
+
assert(pm, new Error('Failed to detect current package manager'));
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
114
|
+
const { command, args } = resolveCommand(pm, 'run', ['storybook', 'dev'])!;
|
115
|
+
const storybookPort = new URL(localUrl).port;
|
116
|
+
const storybookCommand = `${config.storybookAutorunCmd ?? [command, ...args, '--ci'].join(' ')} -p ${storybookPort}`;
|
117
|
+
|
118
|
+
logger().info(`Start Storybook via \`${storybookCommand}\`, it should be accessible at:`);
|
119
|
+
logger().info(`Local - ${localUrl}`);
|
120
|
+
if (remoteUrl && localUrl != remoteUrl) logger().info(`On your network - ${remoteUrl}`);
|
121
|
+
logger().info('Waiting Storybook...');
|
122
|
+
|
123
|
+
const storybook = exec(storybookCommand, { async: true });
|
124
|
+
subscribeOn('shutdown', () => {
|
125
|
+
if (storybook.pid) void killTree(storybook.pid);
|
126
|
+
});
|
127
|
+
} else {
|
128
|
+
logger().info('Storybook should be started and be accessible at:');
|
129
|
+
logger().info(`Local - ${localUrl}`);
|
130
|
+
if (remoteUrl && localUrl != remoteUrl) logger().info(`On your network - ${remoteUrl}`);
|
131
|
+
logger().info(
|
132
|
+
'Tip: Creevey can start Storybook automatically by using `-s` option at the command line. (e.g., yarn/npm run creevey -s)',
|
133
|
+
);
|
134
|
+
logger().info('Waiting Storybook...');
|
135
|
+
}
|
136
|
+
|
137
|
+
if (options.storybookStart || process.env.CI !== 'true') {
|
138
|
+
const isConnected = await checkIsStorybookConnected(localUrl);
|
139
|
+
if (isConnected) {
|
140
|
+
logger().info('Storybook connected!\n');
|
141
|
+
} else {
|
142
|
+
logger().error('Storybook is not responding. Please start Storybook and restart Creevey');
|
143
|
+
shutdownWithError();
|
144
|
+
}
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
90
148
|
switch (true) {
|
91
149
|
case Boolean(update): {
|
92
150
|
(await import('./update.js')).update(config, typeof update == 'string' ? update : undefined);
|
@@ -97,25 +155,25 @@ export default async function (options: Options): Promise<void> {
|
|
97
155
|
try {
|
98
156
|
await import('selenium-webdriver');
|
99
157
|
} catch {
|
100
|
-
logger.error('Failed to start Creevey, missing required dependency: "selenium-webdriver"');
|
158
|
+
logger().error('Failed to start Creevey, missing required dependency: "selenium-webdriver"');
|
101
159
|
process.exit(-1);
|
102
160
|
}
|
103
161
|
} else {
|
104
162
|
try {
|
105
163
|
await import('playwright-core');
|
106
164
|
} catch {
|
107
|
-
logger.error('Failed to start Creevey, missing required dependency: "playwright-core"');
|
165
|
+
logger().error('Failed to start Creevey, missing required dependency: "playwright-core"');
|
108
166
|
process.exit(-1);
|
109
167
|
}
|
110
168
|
}
|
111
|
-
logger.info('Starting Master Process');
|
169
|
+
logger().info('Starting Master Process');
|
112
170
|
|
113
171
|
const resolveApi = (await import('./master/server.js')).start(config.reportDir, port, ui);
|
114
172
|
|
115
173
|
return (await import('./master/start.js')).start(gridUrl, config, options, resolveApi);
|
116
174
|
}
|
117
175
|
default: {
|
118
|
-
logger.info(`Starting Worker for ${browser}`);
|
176
|
+
logger().info(`Starting Worker for ${browser}`);
|
119
177
|
|
120
178
|
// NOTE: We assume that we pass `gridUrl` to worker CLI options
|
121
179
|
// 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,36 +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
|
-
if (isShuttingDown.current) return { error: 'Master process is shutting down' };
|
125
|
-
|
126
|
-
cluster.setupPrimary({
|
127
|
-
args: ['--browser', this.browser, ...(this.gridUrl ? ['--gridUrl', this.gridUrl] : []), ...process.argv.slice(2)],
|
128
|
-
});
|
129
|
-
const worker = cluster.fork();
|
130
|
-
const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
|
131
|
-
const readyHandler = (message: unknown): void => {
|
132
|
-
if (!isWorkerMessage(message) || message.type == 'port') return;
|
133
|
-
worker.off('message', readyHandler);
|
134
|
-
resolve(message);
|
135
|
-
};
|
136
|
-
worker.on('message', readyHandler);
|
137
|
-
});
|
138
|
-
|
139
|
-
if (message.type != 'error') return worker;
|
140
|
-
|
141
|
-
this.gracefullyKill(worker);
|
142
|
-
|
143
|
-
if (retry == FORK_RETRIES) return message.payload;
|
144
|
-
return this.forkWorker(retry + 1);
|
145
|
-
}
|
146
|
-
|
147
122
|
private exitHandler(worker: Worker): void {
|
148
123
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
149
124
|
worker.once('exit', async () => {
|
150
125
|
if (isShuttingDown.current) return;
|
151
126
|
|
152
|
-
const workerOrError = await this.forkWorker();
|
127
|
+
const workerOrError = await this.scheduler.forkWorker(this.browser, this.storybookUrl, this.gridUrl);
|
153
128
|
|
154
129
|
if (!(workerOrError instanceof ClusterWorker))
|
155
130
|
throw new Error(`Can't instantiate worker for ${this.browser} due many errors`);
|
@@ -160,17 +135,6 @@ export default class Pool extends EventEmitter {
|
|
160
135
|
});
|
161
136
|
}
|
162
137
|
|
163
|
-
private gracefullyKill(worker: Worker): void {
|
164
|
-
worker.isShuttingDown = true;
|
165
|
-
const timeout = setTimeout(() => {
|
166
|
-
worker.kill();
|
167
|
-
}, 10000);
|
168
|
-
worker.on('exit', () => {
|
169
|
-
clearTimeout(timeout);
|
170
|
-
});
|
171
|
-
sendShutdownMessage(worker);
|
172
|
-
}
|
173
|
-
|
174
138
|
private shouldRetry(test: WorkerTest): boolean {
|
175
139
|
return test.retries < this.maxRetries && !this.forcedStop;
|
176
140
|
}
|
@@ -202,10 +166,10 @@ export default class Pool extends EventEmitter {
|
|
202
166
|
});
|
203
167
|
|
204
168
|
if (message.payload.subtype == 'unknown') {
|
205
|
-
|
169
|
+
gracefullyKill(worker);
|
206
170
|
}
|
207
171
|
|
208
|
-
this.handleTestResult(worker, test, { status: 'failed', error: message.payload.error });
|
172
|
+
this.handleTestResult(worker, test, { status: 'failed', error: message.payload.error, retries: test.retries });
|
209
173
|
}),
|
210
174
|
subscribeOnWorker(worker, 'test', (message) => {
|
211
175
|
if (message.type != 'end') return;
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import cluster from 'cluster';
|
2
|
+
import { isWorkerMessage, Worker, WorkerMessage } from '../../types.js';
|
3
|
+
import { gracefullyKill, isShuttingDown } from '../utils.js';
|
4
|
+
|
5
|
+
const FORK_RETRIES = 5;
|
6
|
+
|
7
|
+
type MaybeWorker = Worker | { error: string };
|
8
|
+
|
9
|
+
export class WorkerQueue {
|
10
|
+
private isProcessing = false;
|
11
|
+
private queue: {
|
12
|
+
browser: string;
|
13
|
+
storybookUrl: string;
|
14
|
+
gridUrl?: string;
|
15
|
+
retry: number;
|
16
|
+
resolve: (mw: MaybeWorker) => void;
|
17
|
+
}[] = [];
|
18
|
+
|
19
|
+
// TODO Add concurrency
|
20
|
+
constructor(private useQueue: boolean) {}
|
21
|
+
|
22
|
+
async forkWorker(browser: string, storybookUrl: string, gridUrl?: string, retry = 0): Promise<MaybeWorker> {
|
23
|
+
return new Promise<MaybeWorker>((resolve) => {
|
24
|
+
this.queue.push({ browser, storybookUrl, gridUrl, retry, resolve });
|
25
|
+
|
26
|
+
void this.process();
|
27
|
+
});
|
28
|
+
}
|
29
|
+
|
30
|
+
private async process() {
|
31
|
+
if (this.useQueue && this.isProcessing) return;
|
32
|
+
|
33
|
+
const { browser, storybookUrl, gridUrl, retry, resolve } = this.queue.pop() ?? {};
|
34
|
+
|
35
|
+
if (browser == undefined || storybookUrl == undefined || retry == undefined || resolve == undefined) return;
|
36
|
+
|
37
|
+
if (isShuttingDown.current) {
|
38
|
+
resolve({ error: 'Master process is shutting down' });
|
39
|
+
return;
|
40
|
+
}
|
41
|
+
|
42
|
+
this.isProcessing = true;
|
43
|
+
|
44
|
+
cluster.setupPrimary({
|
45
|
+
args: [
|
46
|
+
'--browser',
|
47
|
+
browser,
|
48
|
+
...(gridUrl ? ['--gridUrl', gridUrl] : []),
|
49
|
+
...process.argv.slice(2),
|
50
|
+
'--storybookUrl',
|
51
|
+
storybookUrl,
|
52
|
+
],
|
53
|
+
});
|
54
|
+
const worker = cluster.fork();
|
55
|
+
const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
|
56
|
+
const readyHandler = (message: unknown): void => {
|
57
|
+
if (!isWorkerMessage(message) || message.type == 'port') return;
|
58
|
+
worker.off('message', readyHandler);
|
59
|
+
resolve(message);
|
60
|
+
};
|
61
|
+
worker.on('message', readyHandler);
|
62
|
+
});
|
63
|
+
|
64
|
+
if (message.type == 'error') {
|
65
|
+
gracefullyKill(worker);
|
66
|
+
|
67
|
+
if (retry == FORK_RETRIES) resolve(message.payload);
|
68
|
+
else this.queue.push({ browser, storybookUrl, gridUrl, retry: retry + 1, resolve });
|
69
|
+
} else {
|
70
|
+
resolve(worker);
|
71
|
+
}
|
72
|
+
|
73
|
+
this.isProcessing = false;
|
74
|
+
|
75
|
+
setImmediate(() => void this.process());
|
76
|
+
}
|
77
|
+
}
|