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.
Files changed (211) hide show
  1. package/README.md +19 -41
  2. package/dist/client/addon/components/Addon.js +17 -7
  3. package/dist/client/addon/components/Addon.js.map +1 -1
  4. package/dist/client/addon/components/Panel.js +2 -2
  5. package/dist/client/addon/components/Panel.js.map +1 -1
  6. package/dist/client/addon/components/Tools.js +17 -7
  7. package/dist/client/addon/components/Tools.js.map +1 -1
  8. package/dist/client/addon/withCreevey.d.ts +2 -1
  9. package/dist/client/addon/withCreevey.js +11 -1
  10. package/dist/client/addon/withCreevey.js.map +1 -1
  11. package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
  12. package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
  13. package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
  14. package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
  15. package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
  16. package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
  17. package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
  18. package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
  19. package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
  20. package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
  21. package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
  22. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  23. package/dist/client/shared/components/PageHeader/ImagePreview.d.ts +1 -1
  24. package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
  25. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  26. package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
  27. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  28. package/dist/client/shared/components/ResultsPage.d.ts +1 -1
  29. package/dist/client/shared/components/ResultsPage.js +43 -13
  30. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  31. package/dist/client/shared/creeveyClientApi.js +8 -1
  32. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  33. package/dist/client/shared/helpers.d.ts +1 -3
  34. package/dist/client/shared/helpers.js +4 -19
  35. package/dist/client/shared/helpers.js.map +1 -1
  36. package/dist/client/web/CreeveyApp.js +42 -14
  37. package/dist/client/web/CreeveyApp.js.map +1 -1
  38. package/dist/client/web/CreeveyContext.d.ts +5 -0
  39. package/dist/client/web/CreeveyContext.js +20 -7
  40. package/dist/client/web/CreeveyContext.js.map +1 -1
  41. package/dist/client/web/CreeveyLoader.js +2 -2
  42. package/dist/client/web/CreeveyLoader.js.map +1 -1
  43. package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
  44. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  45. package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
  46. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  47. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
  48. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  49. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
  50. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  51. package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +2 -2
  52. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
  53. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  54. package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
  55. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  56. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +1 -1
  57. package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
  58. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  59. package/dist/client/web/KeyboardEventsContext.js +79 -64
  60. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  61. package/dist/client/web/assets/index-B0Xv0lOY.js +802 -0
  62. package/dist/client/web/index.html +1 -1
  63. package/dist/client/web/index.js +17 -7
  64. package/dist/client/web/index.js.map +1 -1
  65. package/dist/client/web/themes.d.ts +2 -0
  66. package/dist/client/web/themes.js +22 -0
  67. package/dist/client/web/themes.js.map +1 -0
  68. package/dist/creevey.js +16 -9
  69. package/dist/creevey.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/server/config.d.ts +1 -1
  72. package/dist/server/config.js +27 -5
  73. package/dist/server/config.js.map +1 -1
  74. package/dist/server/connection.d.ts +3 -0
  75. package/dist/server/connection.js +28 -0
  76. package/dist/server/connection.js.map +1 -0
  77. package/dist/server/docker.d.ts +1 -1
  78. package/dist/server/docker.js +56 -32
  79. package/dist/server/docker.js.map +1 -1
  80. package/dist/server/index.js +64 -11
  81. package/dist/server/index.js.map +1 -1
  82. package/dist/server/logger.d.ts +2 -1
  83. package/dist/server/logger.js +7 -3
  84. package/dist/server/logger.js.map +1 -1
  85. package/dist/server/master/api.js +1 -1
  86. package/dist/server/master/api.js.map +1 -1
  87. package/dist/server/master/pool.d.ts +4 -3
  88. package/dist/server/master/pool.js +13 -66
  89. package/dist/server/master/pool.js.map +1 -1
  90. package/dist/server/master/queue.d.ts +13 -0
  91. package/dist/server/master/queue.js +71 -0
  92. package/dist/server/master/queue.js.map +1 -0
  93. package/dist/server/master/runner.d.ts +3 -0
  94. package/dist/server/master/runner.js +76 -10
  95. package/dist/server/master/runner.js.map +1 -1
  96. package/dist/server/master/server.js +1 -1
  97. package/dist/server/master/server.js.map +1 -1
  98. package/dist/server/master/start.js +13 -11
  99. package/dist/server/master/start.js.map +1 -1
  100. package/dist/server/playwright/docker-file.d.ts +1 -1
  101. package/dist/server/playwright/docker-file.js +15 -6
  102. package/dist/server/playwright/docker-file.js.map +1 -1
  103. package/dist/server/playwright/docker.d.ts +2 -1
  104. package/dist/server/playwright/docker.js +10 -2
  105. package/dist/server/playwright/docker.js.map +1 -1
  106. package/dist/server/playwright/index-source.mjs +16 -0
  107. package/dist/server/playwright/internal.d.ts +6 -6
  108. package/dist/server/playwright/internal.js +143 -91
  109. package/dist/server/playwright/internal.js.map +1 -1
  110. package/dist/server/playwright/webdriver.d.ts +1 -1
  111. package/dist/server/playwright/webdriver.js +5 -8
  112. package/dist/server/playwright/webdriver.js.map +1 -1
  113. package/dist/server/providers/browser.js +6 -4
  114. package/dist/server/providers/browser.js.map +1 -1
  115. package/dist/server/providers/hybrid.js +1 -1
  116. package/dist/server/providers/hybrid.js.map +1 -1
  117. package/dist/server/reporter.d.ts +4 -19
  118. package/dist/server/reporter.js +30 -21
  119. package/dist/server/reporter.js.map +1 -1
  120. package/dist/server/selenium/internal.d.ts +3 -4
  121. package/dist/server/selenium/internal.js +127 -108
  122. package/dist/server/selenium/internal.js.map +1 -1
  123. package/dist/server/selenium/selenoid.js +8 -6
  124. package/dist/server/selenium/selenoid.js.map +1 -1
  125. package/dist/server/selenium/webdriver.d.ts +1 -1
  126. package/dist/server/selenium/webdriver.js +5 -9
  127. package/dist/server/selenium/webdriver.js.map +1 -1
  128. package/dist/server/telemetry.js +2 -2
  129. package/dist/server/testsFiles/parser.js +45 -5
  130. package/dist/server/testsFiles/parser.js.map +1 -1
  131. package/dist/server/utils.d.ts +19 -1
  132. package/dist/server/utils.js +87 -8
  133. package/dist/server/utils.js.map +1 -1
  134. package/dist/server/webdriver.d.ts +5 -4
  135. package/dist/server/webdriver.js +23 -10
  136. package/dist/server/webdriver.js.map +1 -1
  137. package/dist/server/worker/chai-image.d.ts +1 -2
  138. package/dist/server/worker/chai-image.js +4 -3
  139. package/dist/server/worker/chai-image.js.map +1 -1
  140. package/dist/server/worker/context.d.ts +3 -0
  141. package/dist/server/worker/context.js +15 -0
  142. package/dist/server/worker/context.js.map +1 -0
  143. package/dist/server/worker/match-image.d.ts +4 -4
  144. package/dist/server/worker/match-image.js +7 -4
  145. package/dist/server/worker/match-image.js.map +1 -1
  146. package/dist/server/worker/start.js +45 -73
  147. package/dist/server/worker/start.js.map +1 -1
  148. package/dist/shared/index.d.ts +1 -1
  149. package/dist/types.d.ts +40 -8
  150. package/dist/types.js +2 -0
  151. package/dist/types.js.map +1 -1
  152. package/docs/cli.md +12 -0
  153. package/docs/config.md +179 -165
  154. package/docs/storybook.md +60 -0
  155. package/docs/tests.md +50 -45
  156. package/package.json +64 -63
  157. package/src/client/addon/components/Panel.tsx +2 -2
  158. package/src/client/addon/withCreevey.ts +10 -2
  159. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  160. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  161. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
  162. package/src/client/shared/components/ResultsPage.tsx +31 -8
  163. package/src/client/shared/creeveyClientApi.ts +9 -1
  164. package/src/client/shared/helpers.ts +4 -24
  165. package/src/client/web/CreeveyApp.tsx +27 -8
  166. package/src/client/web/CreeveyContext.tsx +9 -0
  167. package/src/client/web/CreeveyLoader.tsx +1 -1
  168. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  169. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  170. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  171. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  172. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  173. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  174. package/src/client/web/themes.ts +24 -0
  175. package/src/creevey.ts +16 -10
  176. package/src/server/config.ts +28 -6
  177. package/src/server/connection.ts +26 -0
  178. package/src/server/docker.ts +63 -34
  179. package/src/server/index.ts +72 -14
  180. package/src/server/logger.ts +6 -2
  181. package/src/server/master/api.ts +1 -1
  182. package/src/server/master/pool.ts +23 -59
  183. package/src/server/master/queue.ts +77 -0
  184. package/src/server/master/runner.ts +94 -10
  185. package/src/server/master/server.ts +1 -1
  186. package/src/server/master/start.ts +16 -11
  187. package/src/server/playwright/docker-file.ts +18 -6
  188. package/src/server/playwright/docker.ts +16 -3
  189. package/src/server/playwright/index-source.mjs +16 -0
  190. package/src/server/playwright/internal.ts +182 -111
  191. package/src/server/playwright/webdriver.ts +6 -9
  192. package/src/server/providers/browser.ts +6 -4
  193. package/src/server/providers/hybrid.ts +1 -1
  194. package/src/server/reporter.ts +37 -34
  195. package/src/server/selenium/internal.ts +131 -116
  196. package/src/server/selenium/selenoid.ts +8 -6
  197. package/src/server/selenium/webdriver.ts +6 -10
  198. package/src/server/telemetry.ts +2 -2
  199. package/src/server/testsFiles/parser.ts +52 -4
  200. package/src/server/utils.ts +97 -9
  201. package/src/server/webdriver.ts +24 -16
  202. package/src/server/worker/chai-image.ts +4 -4
  203. package/src/server/worker/context.ts +14 -0
  204. package/src/server/worker/match-image.ts +12 -8
  205. package/src/server/worker/start.ts +49 -86
  206. package/src/shared/index.ts +1 -1
  207. package/src/types.ts +44 -8
  208. package/types/global.d.ts +1 -0
  209. package/.yarnrc.yml +0 -1
  210. package/chromatic.config.json +0 -5
  211. package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
@@ -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 ? 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
- if (images.at(0)) {
64
- logger.info(`Image ${imageName} already exists`);
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` }).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
- { t: imageName, labels: { creevey: imageName } },
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(event: { id: string; status: string; progress?: string }): void {
99
- if (!/^[a-z0-9]{12}$/i.test(event.id)) return;
100
-
101
- spinner.text = `${imageName}: [${event.id}] ${event.status} ${event.progress ? event.progress : ''}`;
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
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
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',
@@ -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
- // TODO start standalone playwright server (useDocker == false)
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
@@ -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 = 'Creevey') {
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 logger = Logger.getLogger('Creevey');
23
+ export const setRootName = (newName: string) => (rootName = newName);
24
+
25
+ export const logger = () => Logger.getLogger(rootName);
@@ -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 cluster, { Worker as ClusterWorker } from 'cluster';
1
+ import { Worker as ClusterWorker } from 'cluster';
2
2
  import { EventEmitter } from 'events';
3
- import {
4
- Worker,
5
- Config,
6
- TestResult,
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
- // TODO Init queue for workers to smooth browser starting load
50
- this.workers = (await Promise.all(Array.from({ length: poolSize }).map(() => this.forkWorker()))).filter(
51
- (workerOrError): workerOrError is Worker => workerOrError instanceof ClusterWorker,
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(): void {
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(): void {
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
- this.process();
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
- this.gracefullyKill(worker);
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
+ }