creevey 0.10.0-beta.4 → 0.10.0-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -41
- package/dist/client/addon/components/Addon.js +17 -7
- package/dist/client/addon/components/Addon.js.map +1 -1
- package/dist/client/addon/components/Panel.js +2 -2
- package/dist/client/addon/components/Panel.js.map +1 -1
- package/dist/client/addon/components/Tools.js +17 -7
- package/dist/client/addon/components/Tools.js.map +1 -1
- package/dist/client/addon/withCreevey.d.ts +2 -1
- package/dist/client/addon/withCreevey.js +11 -1
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
- package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
- package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
- package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
- package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
- package/dist/client/shared/components/PageHeader/ImagePreview.d.ts +1 -1
- package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
- package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
- package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/components/ResultsPage.d.ts +1 -1
- package/dist/client/shared/components/ResultsPage.js +43 -13
- package/dist/client/shared/components/ResultsPage.js.map +1 -1
- package/dist/client/shared/creeveyClientApi.js +8 -1
- package/dist/client/shared/creeveyClientApi.js.map +1 -1
- package/dist/client/shared/helpers.d.ts +1 -3
- package/dist/client/shared/helpers.js +4 -19
- package/dist/client/shared/helpers.js.map +1 -1
- package/dist/client/web/CreeveyApp.js +42 -14
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyContext.d.ts +5 -0
- package/dist/client/web/CreeveyContext.js +20 -7
- package/dist/client/web/CreeveyContext.js.map +1 -1
- package/dist/client/web/CreeveyLoader.js +2 -2
- package/dist/client/web/CreeveyLoader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
- package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
- package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +2 -2
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
- package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
- package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
- package/dist/client/web/KeyboardEventsContext.js +79 -64
- package/dist/client/web/KeyboardEventsContext.js.map +1 -1
- package/dist/client/web/assets/index-C47njyZV.js +802 -0
- package/dist/client/web/index.html +1 -1
- package/dist/client/web/index.js +17 -7
- package/dist/client/web/index.js.map +1 -1
- package/dist/client/web/themes.d.ts +2 -0
- package/dist/client/web/themes.js +22 -0
- package/dist/client/web/themes.js.map +1 -0
- package/dist/creevey.js +16 -9
- package/dist/creevey.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/server/config.d.ts +1 -1
- package/dist/server/config.js +30 -7
- package/dist/server/config.js.map +1 -1
- package/dist/server/connection.d.ts +3 -0
- package/dist/server/connection.js +28 -0
- package/dist/server/connection.js.map +1 -0
- package/dist/server/docker.d.ts +1 -1
- package/dist/server/docker.js +56 -32
- package/dist/server/docker.js.map +1 -1
- package/dist/server/index.js +64 -11
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +2 -1
- package/dist/server/logger.js +7 -3
- package/dist/server/logger.js.map +1 -1
- package/dist/server/master/api.js +1 -1
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/pool.d.ts +4 -3
- package/dist/server/master/pool.js +13 -66
- package/dist/server/master/pool.js.map +1 -1
- package/dist/server/master/queue.d.ts +13 -0
- package/dist/server/master/queue.js +71 -0
- package/dist/server/master/queue.js.map +1 -0
- package/dist/server/master/runner.d.ts +3 -0
- package/dist/server/master/runner.js +78 -10
- package/dist/server/master/runner.js.map +1 -1
- package/dist/server/master/server.js +1 -1
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/master/start.js +13 -11
- package/dist/server/master/start.js.map +1 -1
- package/dist/server/playwright/docker-file.d.ts +1 -1
- package/dist/server/playwright/docker-file.js +15 -6
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/docker.d.ts +2 -1
- package/dist/server/playwright/docker.js +10 -2
- package/dist/server/playwright/docker.js.map +1 -1
- package/dist/server/playwright/index-source.mjs +16 -0
- package/dist/server/playwright/internal.d.ts +6 -6
- package/dist/server/playwright/internal.js +143 -91
- package/dist/server/playwright/internal.js.map +1 -1
- package/dist/server/playwright/webdriver.d.ts +1 -1
- package/dist/server/playwright/webdriver.js +5 -8
- package/dist/server/playwright/webdriver.js.map +1 -1
- package/dist/server/providers/browser.js +6 -4
- package/dist/server/providers/browser.js.map +1 -1
- package/dist/server/providers/hybrid.js +1 -1
- package/dist/server/providers/hybrid.js.map +1 -1
- package/dist/server/reporters/creevey.d.ts +7 -0
- package/dist/server/reporters/creevey.js +63 -0
- package/dist/server/reporters/creevey.js.map +1 -0
- package/dist/server/reporters/index.d.ts +2 -0
- package/dist/server/reporters/index.js +16 -0
- package/dist/server/reporters/index.js.map +1 -0
- package/dist/server/reporters/junit.d.ts +16 -0
- package/dist/server/reporters/junit.js +165 -0
- package/dist/server/reporters/junit.js.map +1 -0
- package/dist/server/reporters/teamcity.d.ts +7 -0
- package/dist/server/reporters/teamcity.js +60 -0
- package/dist/server/reporters/teamcity.js.map +1 -0
- package/dist/server/selenium/internal.d.ts +3 -4
- package/dist/server/selenium/internal.js +127 -108
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +8 -6
- package/dist/server/selenium/selenoid.js.map +1 -1
- package/dist/server/selenium/webdriver.d.ts +1 -1
- package/dist/server/selenium/webdriver.js +5 -9
- package/dist/server/selenium/webdriver.js.map +1 -1
- package/dist/server/telemetry.js +2 -2
- package/dist/server/testsFiles/parser.js +45 -5
- package/dist/server/testsFiles/parser.js.map +1 -1
- package/dist/server/utils.d.ts +19 -1
- package/dist/server/utils.js +87 -8
- package/dist/server/utils.js.map +1 -1
- package/dist/server/webdriver.d.ts +5 -4
- package/dist/server/webdriver.js +23 -10
- package/dist/server/webdriver.js.map +1 -1
- package/dist/server/worker/chai-image.d.ts +1 -2
- package/dist/server/worker/chai-image.js +4 -3
- package/dist/server/worker/chai-image.js.map +1 -1
- package/dist/server/worker/context.d.ts +3 -0
- package/dist/server/worker/context.js +15 -0
- package/dist/server/worker/context.js.map +1 -0
- package/dist/server/worker/match-image.d.ts +4 -4
- package/dist/server/worker/match-image.js +7 -4
- package/dist/server/worker/match-image.js.map +1 -1
- package/dist/server/worker/start.js +47 -73
- package/dist/server/worker/start.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/types.d.ts +46 -10
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/docs/cli.md +12 -0
- package/docs/config.md +179 -165
- package/docs/storybook.md +60 -0
- package/docs/tests.md +50 -45
- package/package.json +64 -63
- package/src/client/addon/components/Panel.tsx +2 -2
- package/src/client/addon/withCreevey.ts +10 -2
- package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
- package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
- package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
- package/src/client/shared/components/ResultsPage.tsx +31 -8
- package/src/client/shared/creeveyClientApi.ts +9 -1
- package/src/client/shared/helpers.ts +4 -24
- package/src/client/web/CreeveyApp.tsx +27 -8
- package/src/client/web/CreeveyContext.tsx +9 -0
- package/src/client/web/CreeveyLoader.tsx +1 -1
- package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
- package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
- package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
- package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
- package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
- package/src/client/web/KeyboardEventsContext.tsx +61 -73
- package/src/client/web/themes.ts +24 -0
- package/src/creevey.ts +16 -10
- package/src/server/config.ts +30 -7
- package/src/server/connection.ts +26 -0
- package/src/server/docker.ts +63 -34
- package/src/server/index.ts +72 -14
- package/src/server/logger.ts +6 -2
- package/src/server/master/api.ts +1 -1
- package/src/server/master/pool.ts +23 -59
- package/src/server/master/queue.ts +77 -0
- package/src/server/master/runner.ts +96 -10
- package/src/server/master/server.ts +1 -1
- package/src/server/master/start.ts +16 -11
- package/src/server/playwright/docker-file.ts +18 -6
- package/src/server/playwright/docker.ts +16 -3
- package/src/server/playwright/index-source.mjs +16 -0
- package/src/server/playwright/internal.ts +182 -111
- package/src/server/playwright/webdriver.ts +6 -9
- package/src/server/providers/browser.ts +6 -4
- package/src/server/providers/hybrid.ts +1 -1
- package/src/server/reporters/creevey.ts +71 -0
- package/src/server/reporters/index.ts +11 -0
- package/src/server/reporters/junit.ts +205 -0
- package/src/server/reporters/teamcity.ts +74 -0
- package/src/server/selenium/internal.ts +131 -116
- package/src/server/selenium/selenoid.ts +8 -6
- package/src/server/selenium/webdriver.ts +6 -10
- package/src/server/telemetry.ts +2 -2
- package/src/server/testsFiles/parser.ts +52 -4
- package/src/server/utils.ts +97 -9
- package/src/server/webdriver.ts +24 -16
- package/src/server/worker/chai-image.ts +4 -4
- package/src/server/worker/context.ts +14 -0
- package/src/server/worker/match-image.ts +12 -8
- package/src/server/worker/start.ts +51 -86
- package/src/shared/index.ts +1 -1
- package/src/types.ts +50 -11
- package/types/global.d.ts +1 -0
- package/.yarnrc.yml +0 -1
- package/chromatic.config.json +0 -5
- package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
- package/dist/server/reporter.d.ts +0 -26
- package/dist/server/reporter.js +0 -108
- package/dist/server/reporter.js.map +0 -1
- package/src/server/reporter.ts +0 -138
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
|
+
}
|
@@ -11,15 +11,35 @@ import {
|
|
11
11
|
TestStatus,
|
12
12
|
ServerTest,
|
13
13
|
TestMeta,
|
14
|
+
TEST_EVENTS,
|
15
|
+
FakeSuite,
|
16
|
+
FakeTest,
|
14
17
|
} from '../../types.js';
|
15
18
|
import Pool from './pool.js';
|
19
|
+
import { WorkerQueue } from './queue.js';
|
20
|
+
import { getTestPath } from '../utils.js';
|
21
|
+
import { getReporter } from '../reporters/index.js';
|
22
|
+
|
23
|
+
// NOTE: This is workaround to fix parallel tests running with mocha-junit-reporter
|
24
|
+
let isJUnit = false;
|
25
|
+
|
26
|
+
class FakeRunner extends EventEmitter {
|
27
|
+
public stats = {
|
28
|
+
duration: 0,
|
29
|
+
failures: 0,
|
30
|
+
pending: 0,
|
31
|
+
};
|
32
|
+
}
|
16
33
|
|
17
34
|
export default class Runner extends EventEmitter {
|
18
35
|
private failFast: boolean;
|
19
36
|
private screenDir: string;
|
20
37
|
private reportDir: string;
|
21
38
|
private browsers: string[];
|
39
|
+
private scheduler: WorkerQueue;
|
22
40
|
private pools: Record<string, Pool> = {};
|
41
|
+
private fakeRunner: FakeRunner;
|
42
|
+
private config: Config;
|
23
43
|
tests: Partial<Record<string, ServerTest>> = {};
|
24
44
|
public get isRunning(): boolean {
|
25
45
|
return Object.values(this.pools).some((pool) => pool.isRunning);
|
@@ -27,12 +47,25 @@ export default class Runner extends EventEmitter {
|
|
27
47
|
constructor(config: Config, gridUrl?: string) {
|
28
48
|
super();
|
29
49
|
|
50
|
+
this.config = config;
|
30
51
|
this.failFast = config.failFast;
|
31
52
|
this.screenDir = config.screenDir;
|
32
53
|
this.reportDir = config.reportDir;
|
54
|
+
this.scheduler = new WorkerQueue(config.useWorkerQueue);
|
33
55
|
this.browsers = Object.keys(config.browsers);
|
56
|
+
|
57
|
+
const runner = new FakeRunner();
|
58
|
+
const Reporter = getReporter(config.reporter);
|
59
|
+
|
60
|
+
if (Reporter.name == 'MochaJUnitReporter') {
|
61
|
+
isJUnit = true;
|
62
|
+
}
|
63
|
+
|
64
|
+
new Reporter(runner, { reportDir: this.reportDir, reporterOptions: config.reporterOptions });
|
65
|
+
this.fakeRunner = runner;
|
66
|
+
|
34
67
|
this.browsers
|
35
|
-
.map((browser) => (this.pools[browser] = new Pool(config, browser, gridUrl)))
|
68
|
+
.map((browser) => (this.pools[browser] = new Pool(this.scheduler, config, browser, gridUrl)))
|
36
69
|
.map((pool) => pool.on('test', this.handlePoolMessage));
|
37
70
|
}
|
38
71
|
|
@@ -42,22 +75,77 @@ export default class Runner extends EventEmitter {
|
|
42
75
|
|
43
76
|
if (!test) return;
|
44
77
|
const { browser, testName, storyPath, storyId } = test;
|
78
|
+
|
79
|
+
const fakeSuite: FakeSuite = {
|
80
|
+
title: test.storyPath.slice(0, -1).join('/'),
|
81
|
+
fullTitle: () => fakeSuite.title,
|
82
|
+
titlePath: () => [fakeSuite.title],
|
83
|
+
tests: [],
|
84
|
+
};
|
85
|
+
|
86
|
+
const fakeTest: FakeTest = {
|
87
|
+
parent: fakeSuite,
|
88
|
+
title: [test.story.name, testName, browser].filter(isDefined).join('/'),
|
89
|
+
fullTitle: () => getTestPath(test).join('/'),
|
90
|
+
titlePath: () => getTestPath(test),
|
91
|
+
currentRetry: () => result?.retries,
|
92
|
+
retires: () => this.config.maxRetries,
|
93
|
+
slow: () => 1000,
|
94
|
+
err: result?.error,
|
95
|
+
creevey: {
|
96
|
+
sessionId: result?.sessionId ?? id,
|
97
|
+
browserName: result?.browserName ?? browser,
|
98
|
+
workerId: result?.workerId ?? process.pid,
|
99
|
+
willRetry: (result?.retries ?? 0) < this.config.maxRetries,
|
100
|
+
images: result?.images ?? {},
|
101
|
+
},
|
102
|
+
};
|
103
|
+
|
104
|
+
fakeSuite.tests.push(fakeTest);
|
105
|
+
|
45
106
|
// TODO Handle 'retrying' status
|
46
107
|
test.status = status == 'retrying' ? 'failed' : status;
|
47
108
|
if (!result) {
|
48
109
|
// NOTE: Running status
|
110
|
+
this.fakeRunner.emit(TEST_EVENTS.TEST_BEGIN, fakeTest);
|
49
111
|
this.sendUpdate({ tests: { [id]: { id, browser, testName, storyPath, status: test.status, storyId } } });
|
50
112
|
return;
|
51
113
|
}
|
52
|
-
|
53
|
-
test.results = [];
|
54
|
-
}
|
114
|
+
test.results ??= [];
|
55
115
|
test.results.push(result);
|
56
116
|
|
57
117
|
if (status == 'failed') {
|
58
118
|
test.approved = null;
|
59
119
|
}
|
60
120
|
|
121
|
+
const { duration, attachments } = result;
|
122
|
+
|
123
|
+
fakeTest.duration = duration;
|
124
|
+
fakeTest.attachments = attachments;
|
125
|
+
fakeTest.state = result.status === 'failed' ? 'failed' : 'passed';
|
126
|
+
if (duration !== undefined) {
|
127
|
+
fakeTest.speed = duration > fakeTest.slow() ? 'slow' : duration / 2 > fakeTest.slow() ? 'medium' : 'fast';
|
128
|
+
}
|
129
|
+
|
130
|
+
if (isJUnit) {
|
131
|
+
this.fakeRunner.emit(TEST_EVENTS.SUITE_BEGIN, fakeSuite);
|
132
|
+
}
|
133
|
+
|
134
|
+
if (result.status === 'failed') {
|
135
|
+
fakeTest.err = result.error;
|
136
|
+
this.fakeRunner.emit(TEST_EVENTS.TEST_FAIL, fakeTest, result.error);
|
137
|
+
this.fakeRunner.stats.failures++;
|
138
|
+
} else {
|
139
|
+
this.fakeRunner.emit(TEST_EVENTS.TEST_PASS, fakeTest);
|
140
|
+
this.fakeRunner.stats.duration += duration ?? 0;
|
141
|
+
}
|
142
|
+
|
143
|
+
if (isJUnit) {
|
144
|
+
this.fakeRunner.emit(TEST_EVENTS.SUITE_END, fakeSuite);
|
145
|
+
}
|
146
|
+
|
147
|
+
this.fakeRunner.emit(TEST_EVENTS.TEST_END, fakeTest);
|
148
|
+
|
61
149
|
this.sendUpdate({
|
62
150
|
tests: {
|
63
151
|
[id]: {
|
@@ -78,6 +166,7 @@ export default class Runner extends EventEmitter {
|
|
78
166
|
|
79
167
|
private handlePoolStop = (): void => {
|
80
168
|
if (!this.isRunning) {
|
169
|
+
this.fakeRunner.emit(TEST_EVENTS.RUN_END);
|
81
170
|
this.sendUpdate({ isRunning: false });
|
82
171
|
this.emit('stop');
|
83
172
|
}
|
@@ -147,6 +236,7 @@ export default class Runner extends EventEmitter {
|
|
147
236
|
};
|
148
237
|
}, {});
|
149
238
|
|
239
|
+
this.fakeRunner.emit(TEST_EVENTS.RUN_BEGIN);
|
150
240
|
this.browsers.forEach((browser) => {
|
151
241
|
const pool = this.pools[browser];
|
152
242
|
const tests = testsByBrowser[browser];
|
@@ -198,9 +288,7 @@ export default class Runner extends EventEmitter {
|
|
198
288
|
if (!image) continue;
|
199
289
|
await this.copyImage(test, name, image.actual);
|
200
290
|
|
201
|
-
|
202
|
-
test.approved = {};
|
203
|
-
}
|
291
|
+
test.approved ??= {};
|
204
292
|
test.approved[name] = retry;
|
205
293
|
test.status = 'approved';
|
206
294
|
|
@@ -224,9 +312,7 @@ export default class Runner extends EventEmitter {
|
|
224
312
|
if (!result.images) return;
|
225
313
|
const images = result.images[image];
|
226
314
|
if (!images) return;
|
227
|
-
|
228
|
-
test.approved = {};
|
229
|
-
}
|
315
|
+
test.approved ??= {};
|
230
316
|
const { browser, testName, storyPath, storyId } = test;
|
231
317
|
|
232
318
|
await this.copyImage(test, image, images.actual);
|
@@ -14,13 +14,14 @@ import { sendScreenshotsCount } from '../telemetry.js';
|
|
14
14
|
const importMetaUrl = pathToFileURL(__filename).href;
|
15
15
|
|
16
16
|
async function copyStatics(reportDir: string): Promise<void> {
|
17
|
-
const clientDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '
|
18
|
-
const
|
19
|
-
.filter((dirent) => dirent.isFile()
|
17
|
+
const clientDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../../dist/client/web');
|
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,10 +105,11 @@ 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
|
+
// NOTE: Take some time to kill processes
|
112
|
+
void shutdownWorkers().then(() => setTimeout(() => process.exit(), 500));
|
108
113
|
});
|
109
114
|
});
|
110
115
|
// TODO grep
|