creevey 0.10.0-beta.2 → 0.10.0-beta.21
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/controller.js +4 -5
- package/dist/client/addon/controller.js.map +1 -1
- package/dist/client/addon/withCreevey.d.ts +1 -0
- package/dist/client/addon/withCreevey.js +19 -34
- 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 +17 -7
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/components/ResultsPage.js +40 -12
- 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 +0 -2
- package/dist/client/shared/helpers.js +0 -17
- 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-DkmZfG9C.js → index-iytWuaD6.js} +104 -104
- 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 +13 -5
- package/dist/creevey.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/server/config.js +5 -4
- package/dist/server/config.js.map +1 -1
- package/dist/server/docker.js +37 -20
- package/dist/server/docker.js.map +1 -1
- package/dist/server/index.js +36 -7
- 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 +3 -3
- package/dist/server/master/pool.js +10 -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 +64 -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 +10 -9
- 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 +6 -4
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/internal.d.ts +3 -3
- package/dist/server/playwright/internal.js +50 -44
- 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 -91
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +2 -2
- 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/utils.d.ts +16 -1
- package/dist/server/utils.js +31 -3
- 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 +32 -13
- package/dist/types.js +13 -1
- package/dist/types.js.map +1 -1
- package/package.json +55 -59
- package/src/client/addon/components/Panel.tsx +2 -2
- package/src/client/addon/controller.ts +13 -6
- package/src/client/addon/withCreevey.ts +27 -13
- 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/ResultsPage.tsx +28 -7
- package/src/client/shared/creeveyClientApi.ts +9 -1
- package/src/client/shared/helpers.ts +0 -22
- 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 +13 -6
- package/src/server/config.ts +5 -4
- package/src/server/docker.ts +41 -23
- package/src/server/index.ts +39 -9
- package/src/server/logger.ts +6 -2
- package/src/server/master/api.ts +1 -1
- package/src/server/master/pool.ts +18 -56
- package/src/server/master/queue.ts +64 -0
- package/src/server/master/runner.ts +4 -1
- package/src/server/master/server.ts +1 -1
- package/src/server/master/start.ts +13 -9
- package/src/server/playwright/docker-file.ts +7 -4
- package/src/server/playwright/internal.ts +70 -51
- 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 +133 -96
- package/src/server/selenium/selenoid.ts +2 -2
- package/src/server/selenium/webdriver.ts +2 -2
- package/src/server/telemetry.ts +7 -3
- package/src/server/utils.ts +37 -4
- 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 +35 -15
- package/types/global.d.ts +1 -0
- package/.yarnrc.yml +0 -1
- package/chromatic.config.json +0 -5
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) {
|
@@ -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,4 +1,5 @@
|
|
1
1
|
import cluster from 'cluster';
|
2
|
+
import path from 'path';
|
2
3
|
import { readConfig, defaultBrowser } from './config.js';
|
3
4
|
import { Options, Config, BrowserConfigObject, isWorkerMessage } from '../types.js';
|
4
5
|
import { logger } from './logger.js';
|
@@ -6,13 +7,12 @@ import { SeleniumWebdriver } from './selenium/webdriver.js';
|
|
6
7
|
import { LOCALHOST_REGEXP } from './webdriver.js';
|
7
8
|
import { isInsideDocker } from './utils.js';
|
8
9
|
import { sendWorkerMessage } from './messages.js';
|
9
|
-
import { playwrightDockerFile } from './playwright/docker-file.js';
|
10
10
|
import { buildImage } from './docker.js';
|
11
|
+
import { mkdir, writeFile } from 'fs/promises';
|
11
12
|
|
12
13
|
async function startWebdriverServer(browser: string, config: Config, options: Options): Promise<string | undefined> {
|
13
14
|
if (config.webdriver === SeleniumWebdriver) {
|
14
15
|
if (cluster.isPrimary) {
|
15
|
-
// TODO Get random free port
|
16
16
|
const { startSelenoidContainer, startSelenoidStandalone } = await import('./selenium/selenoid.js');
|
17
17
|
const gridUrl = 'http://localhost:4444/wd/hub';
|
18
18
|
if (config.useDocker) {
|
@@ -24,15 +24,19 @@ async function startWebdriverServer(browser: string, config: Config, options: Op
|
|
24
24
|
}
|
25
25
|
// TODO Worker might want to use docker image of browser or start standalone selenium
|
26
26
|
} else {
|
27
|
-
|
27
|
+
if (config.gridUrl) return undefined;
|
28
|
+
|
29
|
+
if (!config.useDocker) {
|
30
|
+
// TODO Missing @storybook/csf dep
|
31
|
+
}
|
28
32
|
|
33
|
+
// TODO start standalone playwright server (useDocker == false)
|
29
34
|
const {
|
30
35
|
default: { version },
|
31
36
|
} = await import('playwright-core/package.json', { with: { type: 'json' } });
|
32
37
|
|
33
38
|
if (cluster.isWorker) {
|
34
39
|
// TODO Re-use dockerImage
|
35
|
-
|
36
40
|
const { startPlaywrightContainer } = await import('./playwright/docker.js');
|
37
41
|
const { browserName } = config.browsers[browser] as BrowserConfigObject;
|
38
42
|
|
@@ -41,11 +45,18 @@ async function startWebdriverServer(browser: string, config: Config, options: Op
|
|
41
45
|
|
42
46
|
return host;
|
43
47
|
} else {
|
44
|
-
const
|
48
|
+
const { playwrightDockerFile } = await import('./playwright/docker-file.js');
|
49
|
+
const browsers = [
|
50
|
+
...new Set(
|
51
|
+
Object.values(config.browsers).map(
|
52
|
+
(c) => [(c as BrowserConfigObject).browserName, (c as BrowserConfigObject).playwrightOptions] as const,
|
53
|
+
),
|
54
|
+
),
|
55
|
+
];
|
45
56
|
await Promise.all(
|
46
|
-
browsers.map(async (browserName) => {
|
57
|
+
browsers.map(async ([browserName, launchOptions]) => {
|
47
58
|
const imageName = `creevey/${browserName}:v${version}`;
|
48
|
-
const dockerfile = playwrightDockerFile(browserName, version);
|
59
|
+
const dockerfile = playwrightDockerFile(browserName, version, launchOptions);
|
49
60
|
|
50
61
|
await buildImage(imageName, dockerfile);
|
51
62
|
}),
|
@@ -77,6 +88,10 @@ export default async function (options: Options): Promise<void> {
|
|
77
88
|
const { browser = defaultBrowser, update, ui, port } = options;
|
78
89
|
let gridUrl = cluster.isPrimary ? config.gridUrl : options.gridUrl;
|
79
90
|
|
91
|
+
// TODO Add package.json with `"type": "commonjs"` as workaround for esm packages to load `data.js`
|
92
|
+
await mkdir(config.reportDir, { recursive: true });
|
93
|
+
await writeFile(path.join(config.reportDir, 'package.json'), '{"type": "commonjs"}');
|
94
|
+
|
80
95
|
// NOTE: We don't need docker nor selenoid for update option
|
81
96
|
if (
|
82
97
|
!(gridUrl || (Object.values(config.browsers) as BrowserConfigObject[]).every(({ gridUrl }) => gridUrl)) &&
|
@@ -91,14 +106,29 @@ export default async function (options: Options): Promise<void> {
|
|
91
106
|
return;
|
92
107
|
}
|
93
108
|
case cluster.isPrimary: {
|
94
|
-
|
109
|
+
if (config.webdriver === SeleniumWebdriver) {
|
110
|
+
try {
|
111
|
+
await import('selenium-webdriver');
|
112
|
+
} catch {
|
113
|
+
logger().error('Failed to start Creevey, missing required dependency: "selenium-webdriver"');
|
114
|
+
process.exit(-1);
|
115
|
+
}
|
116
|
+
} else {
|
117
|
+
try {
|
118
|
+
await import('playwright-core');
|
119
|
+
} catch {
|
120
|
+
logger().error('Failed to start Creevey, missing required dependency: "playwright-core"');
|
121
|
+
process.exit(-1);
|
122
|
+
}
|
123
|
+
}
|
124
|
+
logger().info('Starting Master Process');
|
95
125
|
|
96
126
|
const resolveApi = (await import('./master/server.js')).start(config.reportDir, port, ui);
|
97
127
|
|
98
128
|
return (await import('./master/start.js')).start(gridUrl, config, options, resolveApi);
|
99
129
|
}
|
100
130
|
default: {
|
101
|
-
logger.info(`Starting Worker for ${browser}`);
|
131
|
+
logger().info(`Starting Worker for ${browser}`);
|
102
132
|
|
103
133
|
// NOTE: We assume that we pass `gridUrl` to worker CLI options
|
104
134
|
// 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;
|
@@ -32,6 +23,7 @@ export default class Pool extends EventEmitter {
|
|
32
23
|
return this.workers.length !== this.freeWorkers.length;
|
33
24
|
}
|
34
25
|
constructor(
|
26
|
+
public scheduler: WorkerQueue,
|
35
27
|
config: Config,
|
36
28
|
private browser: string,
|
37
29
|
gridUrl?: string,
|
@@ -46,10 +38,11 @@ export default class Pool extends EventEmitter {
|
|
46
38
|
|
47
39
|
async init(): Promise<void> {
|
48
40
|
const poolSize = Math.max(1, this.config.limit ?? 1);
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
41
|
+
this.workers = (
|
42
|
+
await Promise.all(
|
43
|
+
Array.from({ length: poolSize }).map(() => this.scheduler.forkWorker(this.browser, this.gridUrl)),
|
44
|
+
)
|
45
|
+
).filter((workerOrError): workerOrError is Worker => workerOrError instanceof ClusterWorker);
|
53
46
|
if (this.workers.length != poolSize)
|
54
47
|
throw new Error(`Can't instantiate workers for ${this.browser} due many errors`);
|
55
48
|
this.workers.forEach((worker) => {
|
@@ -66,7 +59,7 @@ export default class Pool extends EventEmitter {
|
|
66
59
|
return true;
|
67
60
|
}
|
68
61
|
|
69
|
-
stop()
|
62
|
+
stop() {
|
70
63
|
if (!this.isRunning) {
|
71
64
|
this.emit('stop');
|
72
65
|
return;
|
@@ -76,7 +69,7 @@ export default class Pool extends EventEmitter {
|
|
76
69
|
this.queue = [];
|
77
70
|
}
|
78
71
|
|
79
|
-
process()
|
72
|
+
process() {
|
80
73
|
const worker = this.getFreeWorker();
|
81
74
|
const test = this.queue.at(0);
|
82
75
|
|
@@ -99,7 +92,9 @@ export default class Pool extends EventEmitter {
|
|
99
92
|
|
100
93
|
sendTestMessage(worker, { type: 'start', payload: test });
|
101
94
|
|
102
|
-
|
95
|
+
setImmediate(() => {
|
96
|
+
this.process();
|
97
|
+
});
|
103
98
|
}
|
104
99
|
|
105
100
|
private sendStatus(message: { id: string; status: TestStatus; result?: TestResult }): void {
|
@@ -120,34 +115,12 @@ export default class Pool extends EventEmitter {
|
|
120
115
|
return this.aliveWorkers.filter((worker) => !worker.isRunning);
|
121
116
|
}
|
122
117
|
|
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
118
|
private exitHandler(worker: Worker): void {
|
146
119
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
147
120
|
worker.once('exit', async () => {
|
148
121
|
if (isShuttingDown.current) return;
|
149
122
|
|
150
|
-
const workerOrError = await this.forkWorker();
|
123
|
+
const workerOrError = await this.scheduler.forkWorker(this.browser, this.gridUrl);
|
151
124
|
|
152
125
|
if (!(workerOrError instanceof ClusterWorker))
|
153
126
|
throw new Error(`Can't instantiate worker for ${this.browser} due many errors`);
|
@@ -158,17 +131,6 @@ export default class Pool extends EventEmitter {
|
|
158
131
|
});
|
159
132
|
}
|
160
133
|
|
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
134
|
private shouldRetry(test: WorkerTest): boolean {
|
173
135
|
return test.retries < this.maxRetries && !this.forcedStop;
|
174
136
|
}
|
@@ -200,7 +162,7 @@ export default class Pool extends EventEmitter {
|
|
200
162
|
});
|
201
163
|
|
202
164
|
if (message.payload.subtype == 'unknown') {
|
203
|
-
|
165
|
+
gracefullyKill(worker);
|
204
166
|
}
|
205
167
|
|
206
168
|
this.handleTestResult(worker, test, { status: 'failed', error: message.payload.error });
|
@@ -0,0 +1,64 @@
|
|
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: { browser: string; gridUrl?: string; retry: number; resolve: (mw: MaybeWorker) => void }[] = [];
|
12
|
+
|
13
|
+
// TODO Add concurrency
|
14
|
+
constructor(private useQueue: boolean) {}
|
15
|
+
|
16
|
+
async forkWorker(browser: string, gridUrl?: string, retry = 0): Promise<MaybeWorker> {
|
17
|
+
return new Promise<MaybeWorker>((resolve) => {
|
18
|
+
this.queue.push({ browser, gridUrl, retry, resolve });
|
19
|
+
|
20
|
+
void this.process();
|
21
|
+
});
|
22
|
+
}
|
23
|
+
|
24
|
+
private async process() {
|
25
|
+
if (this.useQueue && this.isProcessing) return;
|
26
|
+
|
27
|
+
const { browser, gridUrl, retry, resolve } = this.queue.pop() ?? {};
|
28
|
+
|
29
|
+
if (browser == undefined || retry == undefined || resolve == undefined) return;
|
30
|
+
|
31
|
+
if (isShuttingDown.current) {
|
32
|
+
resolve({ error: 'Master process is shutting down' });
|
33
|
+
return;
|
34
|
+
}
|
35
|
+
|
36
|
+
this.isProcessing = true;
|
37
|
+
|
38
|
+
cluster.setupPrimary({
|
39
|
+
args: ['--browser', browser, ...(gridUrl ? ['--gridUrl', gridUrl] : []), ...process.argv.slice(2)],
|
40
|
+
});
|
41
|
+
const worker = cluster.fork();
|
42
|
+
const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
|
43
|
+
const readyHandler = (message: unknown): void => {
|
44
|
+
if (!isWorkerMessage(message) || message.type == 'port') return;
|
45
|
+
worker.off('message', readyHandler);
|
46
|
+
resolve(message);
|
47
|
+
};
|
48
|
+
worker.on('message', readyHandler);
|
49
|
+
});
|
50
|
+
|
51
|
+
if (message.type == 'error') {
|
52
|
+
gracefullyKill(worker);
|
53
|
+
|
54
|
+
if (retry == FORK_RETRIES) resolve(message.payload);
|
55
|
+
else this.queue.push({ browser, gridUrl, retry: retry + 1, resolve });
|
56
|
+
} else {
|
57
|
+
resolve(worker);
|
58
|
+
}
|
59
|
+
|
60
|
+
this.isProcessing = false;
|
61
|
+
|
62
|
+
setImmediate(() => void this.process());
|
63
|
+
}
|
64
|
+
}
|
@@ -13,12 +13,14 @@ import {
|
|
13
13
|
TestMeta,
|
14
14
|
} from '../../types.js';
|
15
15
|
import Pool from './pool.js';
|
16
|
+
import { WorkerQueue } from './queue.js';
|
16
17
|
|
17
18
|
export default class Runner extends EventEmitter {
|
18
19
|
private failFast: boolean;
|
19
20
|
private screenDir: string;
|
20
21
|
private reportDir: string;
|
21
22
|
private browsers: string[];
|
23
|
+
private scheduler: WorkerQueue;
|
22
24
|
private pools: Record<string, Pool> = {};
|
23
25
|
tests: Partial<Record<string, ServerTest>> = {};
|
24
26
|
public get isRunning(): boolean {
|
@@ -30,9 +32,10 @@ export default class Runner extends EventEmitter {
|
|
30
32
|
this.failFast = config.failFast;
|
31
33
|
this.screenDir = config.screenDir;
|
32
34
|
this.reportDir = config.reportDir;
|
35
|
+
this.scheduler = new WorkerQueue(config.useWorkerQueue);
|
33
36
|
this.browsers = Object.keys(config.browsers);
|
34
37
|
this.browsers
|
35
|
-
.map((browser) => (this.pools[browser] = new Pool(config, browser, gridUrl)))
|
38
|
+
.map((browser) => (this.pools[browser] = new Pool(this.scheduler, config, browser, gridUrl)))
|
36
39
|
.map((pool) => pool.on('test', this.handlePoolMessage));
|
37
40
|
}
|
38
41
|
|
@@ -15,12 +15,13 @@ const importMetaUrl = pathToFileURL(__filename).href;
|
|
15
15
|
|
16
16
|
async function copyStatics(reportDir: string): Promise<void> {
|
17
17
|
const clientDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../client/web');
|
18
|
-
const
|
19
|
-
.filter((dirent) => dirent.isFile()
|
18
|
+
const assets = (await readdir(path.join(clientDir, 'assets'), { withFileTypes: true }))
|
19
|
+
.filter((dirent) => dirent.isFile())
|
20
20
|
.map((dirent) => dirent.name);
|
21
|
-
await mkdir(reportDir, { recursive: true });
|
22
|
-
|
23
|
-
|
21
|
+
await mkdir(path.join(reportDir, 'assets'), { recursive: true });
|
22
|
+
await copyFile(path.join(clientDir, 'index.html'), path.join(reportDir, 'index.html'));
|
23
|
+
for (const asset of assets) {
|
24
|
+
await copyFile(path.join(clientDir, 'assets', asset), path.join(reportDir, 'assets', asset));
|
24
25
|
}
|
25
26
|
}
|
26
27
|
|
@@ -42,7 +43,10 @@ function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
|
|
42
43
|
.map((imagePath) => path.posix.relative(imagesDir, imagePath))
|
43
44
|
.filter((imagePath) => !images.has(imagePath));
|
44
45
|
if (unnecessaryImages.length > 0) {
|
45
|
-
logger.warn(
|
46
|
+
logger().warn(
|
47
|
+
'We found unnecessary screenshot images, those can be safely removed:\n',
|
48
|
+
unnecessaryImages.join('\n'),
|
49
|
+
);
|
46
50
|
}
|
47
51
|
}
|
48
52
|
|
@@ -81,10 +85,10 @@ export async function start(
|
|
81
85
|
|
82
86
|
if (options.ui) {
|
83
87
|
resolveApi(creeveyApi(runner));
|
84
|
-
logger.info(`Started on http://localhost:${options.port}`);
|
88
|
+
logger().info(`Started on http://localhost:${options.port}`);
|
85
89
|
} else {
|
86
90
|
if (Object.values(runner.status.tests).filter((test) => test && !test.skip).length == 0) {
|
87
|
-
logger.warn("Don't have any tests to run");
|
91
|
+
logger().warn("Don't have any tests to run");
|
88
92
|
|
89
93
|
void shutdownWorkers().then(() => process.exit());
|
90
94
|
return;
|
@@ -101,7 +105,7 @@ export async function start(
|
|
101
105
|
void sendScreenshotsCount(config, options, runner.status)
|
102
106
|
.catch((reason: unknown) => {
|
103
107
|
const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
|
104
|
-
logger.warn(`Can't send telemetry: ${error}`);
|
108
|
+
logger().warn(`Can't send telemetry: ${error}`);
|
105
109
|
})
|
106
110
|
.finally(() => {
|
107
111
|
void shutdownWorkers().then(() => process.exit());
|
@@ -1,8 +1,10 @@
|
|
1
1
|
import semver from 'semver';
|
2
2
|
import { exec } from 'shelljs';
|
3
|
+
import { LaunchOptions } from 'playwright-core';
|
4
|
+
import { resolvePlaywrightBrowserType } from '../utils';
|
3
5
|
|
4
6
|
// TODO Support custom docker images
|
5
|
-
export function playwrightDockerFile(browser: string, version: string): string {
|
7
|
+
export function playwrightDockerFile(browser: string, version: string, serverOptions?: LaunchOptions): string {
|
6
8
|
const sv = semver.coerce(version);
|
7
9
|
|
8
10
|
let npmRegistry;
|
@@ -13,18 +15,19 @@ export function playwrightDockerFile(browser: string, version: string): string {
|
|
13
15
|
}
|
14
16
|
|
15
17
|
return `
|
16
|
-
FROM
|
18
|
+
FROM node:lts
|
17
19
|
|
18
20
|
WORKDIR /creevey
|
19
21
|
|
20
22
|
RUN echo "{ \\"type\\": \\"module\\" }" > package.json && \\
|
21
|
-
echo "import { ${browser} as browser } from 'playwright-core';" >> index.js && \\
|
22
|
-
echo "const ws = await browser.launchServer({ port: 4444, wsPath: 'creevey' })" >> index.js && \\${
|
23
|
+
echo "import { ${resolvePlaywrightBrowserType(browser)} as browser } from 'playwright-core';" >> index.js && \\
|
24
|
+
echo "const ws = await browser.launchServer({ ...${JSON.stringify(serverOptions)}, port: 4444, wsPath: 'creevey' })" >> index.js && \\${
|
23
25
|
npmRegistry
|
24
26
|
? `
|
25
27
|
echo "registry=${npmRegistry}" > .npmrc && \\`
|
26
28
|
: ''
|
27
29
|
}
|
30
|
+
npx -y playwright${sv ? `@${sv.format()}` : ''} install --with-deps ${browser} && \\
|
28
31
|
npm i playwright-core${sv ? `@${sv.format()}` : ''}
|
29
32
|
|
30
33
|
EXPOSE 4444
|