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.
Files changed (227) 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-C47njyZV.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 +30 -7
  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 +78 -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/reporters/creevey.d.ts +7 -0
  118. package/dist/server/reporters/creevey.js +63 -0
  119. package/dist/server/reporters/creevey.js.map +1 -0
  120. package/dist/server/reporters/index.d.ts +2 -0
  121. package/dist/server/reporters/index.js +16 -0
  122. package/dist/server/reporters/index.js.map +1 -0
  123. package/dist/server/reporters/junit.d.ts +16 -0
  124. package/dist/server/reporters/junit.js +165 -0
  125. package/dist/server/reporters/junit.js.map +1 -0
  126. package/dist/server/reporters/teamcity.d.ts +7 -0
  127. package/dist/server/reporters/teamcity.js +60 -0
  128. package/dist/server/reporters/teamcity.js.map +1 -0
  129. package/dist/server/selenium/internal.d.ts +3 -4
  130. package/dist/server/selenium/internal.js +127 -108
  131. package/dist/server/selenium/internal.js.map +1 -1
  132. package/dist/server/selenium/selenoid.js +8 -6
  133. package/dist/server/selenium/selenoid.js.map +1 -1
  134. package/dist/server/selenium/webdriver.d.ts +1 -1
  135. package/dist/server/selenium/webdriver.js +5 -9
  136. package/dist/server/selenium/webdriver.js.map +1 -1
  137. package/dist/server/telemetry.js +2 -2
  138. package/dist/server/testsFiles/parser.js +45 -5
  139. package/dist/server/testsFiles/parser.js.map +1 -1
  140. package/dist/server/utils.d.ts +19 -1
  141. package/dist/server/utils.js +87 -8
  142. package/dist/server/utils.js.map +1 -1
  143. package/dist/server/webdriver.d.ts +5 -4
  144. package/dist/server/webdriver.js +23 -10
  145. package/dist/server/webdriver.js.map +1 -1
  146. package/dist/server/worker/chai-image.d.ts +1 -2
  147. package/dist/server/worker/chai-image.js +4 -3
  148. package/dist/server/worker/chai-image.js.map +1 -1
  149. package/dist/server/worker/context.d.ts +3 -0
  150. package/dist/server/worker/context.js +15 -0
  151. package/dist/server/worker/context.js.map +1 -0
  152. package/dist/server/worker/match-image.d.ts +4 -4
  153. package/dist/server/worker/match-image.js +7 -4
  154. package/dist/server/worker/match-image.js.map +1 -1
  155. package/dist/server/worker/start.js +47 -73
  156. package/dist/server/worker/start.js.map +1 -1
  157. package/dist/shared/index.d.ts +1 -1
  158. package/dist/types.d.ts +46 -10
  159. package/dist/types.js +2 -0
  160. package/dist/types.js.map +1 -1
  161. package/docs/cli.md +12 -0
  162. package/docs/config.md +179 -165
  163. package/docs/storybook.md +60 -0
  164. package/docs/tests.md +50 -45
  165. package/package.json +64 -63
  166. package/src/client/addon/components/Panel.tsx +2 -2
  167. package/src/client/addon/withCreevey.ts +10 -2
  168. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  169. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  170. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
  171. package/src/client/shared/components/ResultsPage.tsx +31 -8
  172. package/src/client/shared/creeveyClientApi.ts +9 -1
  173. package/src/client/shared/helpers.ts +4 -24
  174. package/src/client/web/CreeveyApp.tsx +27 -8
  175. package/src/client/web/CreeveyContext.tsx +9 -0
  176. package/src/client/web/CreeveyLoader.tsx +1 -1
  177. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  178. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  179. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  180. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  181. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  182. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  183. package/src/client/web/themes.ts +24 -0
  184. package/src/creevey.ts +16 -10
  185. package/src/server/config.ts +30 -7
  186. package/src/server/connection.ts +26 -0
  187. package/src/server/docker.ts +63 -34
  188. package/src/server/index.ts +72 -14
  189. package/src/server/logger.ts +6 -2
  190. package/src/server/master/api.ts +1 -1
  191. package/src/server/master/pool.ts +23 -59
  192. package/src/server/master/queue.ts +77 -0
  193. package/src/server/master/runner.ts +96 -10
  194. package/src/server/master/server.ts +1 -1
  195. package/src/server/master/start.ts +16 -11
  196. package/src/server/playwright/docker-file.ts +18 -6
  197. package/src/server/playwright/docker.ts +16 -3
  198. package/src/server/playwright/index-source.mjs +16 -0
  199. package/src/server/playwright/internal.ts +182 -111
  200. package/src/server/playwright/webdriver.ts +6 -9
  201. package/src/server/providers/browser.ts +6 -4
  202. package/src/server/providers/hybrid.ts +1 -1
  203. package/src/server/reporters/creevey.ts +71 -0
  204. package/src/server/reporters/index.ts +11 -0
  205. package/src/server/reporters/junit.ts +205 -0
  206. package/src/server/reporters/teamcity.ts +74 -0
  207. package/src/server/selenium/internal.ts +131 -116
  208. package/src/server/selenium/selenoid.ts +8 -6
  209. package/src/server/selenium/webdriver.ts +6 -10
  210. package/src/server/telemetry.ts +2 -2
  211. package/src/server/testsFiles/parser.ts +52 -4
  212. package/src/server/utils.ts +97 -9
  213. package/src/server/webdriver.ts +24 -16
  214. package/src/server/worker/chai-image.ts +4 -4
  215. package/src/server/worker/context.ts +14 -0
  216. package/src/server/worker/match-image.ts +12 -8
  217. package/src/server/worker/start.ts +51 -86
  218. package/src/shared/index.ts +1 -1
  219. package/src/types.ts +50 -11
  220. package/types/global.d.ts +1 -0
  221. package/.yarnrc.yml +0 -1
  222. package/chromatic.config.json +0 -5
  223. package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
  224. package/dist/server/reporter.d.ts +0 -26
  225. package/dist/server/reporter.js +0 -108
  226. package/dist/server/reporter.js.map +0 -1
  227. package/src/server/reporter.ts +0 -138
@@ -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
+ }
@@ -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
- if (!test.results) {
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
- if (!test.approved) {
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
- if (!test.approved) {
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);
@@ -96,7 +96,7 @@ export function start(reportDir: string, port: number, ui: boolean): (api: Creev
96
96
  app.use(mount('/report', serve(reportDir)));
97
97
 
98
98
  wss.on('error', (error) => {
99
- logger.error(error);
99
+ logger().error(error);
100
100
  });
101
101
 
102
102
  server.listen(port);
@@ -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)), '../../client/web');
18
- const files = (await readdir(clientDir, { withFileTypes: true }))
19
- .filter((dirent) => dirent.isFile() && !dirent.name.endsWith('.d.ts') && !dirent.name.endsWith('.tsx'))
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
- for (const file of files) {
23
- await copyFile(path.join(clientDir, file), path.join(reportDir, file));
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('We found unnecessary screenshot images, those can be safely removed:\n', unnecessaryImages.join('\n'));
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
- void shutdownWorkers().then(() => process.exit());
111
+ // NOTE: Take some time to kill processes
112
+ void shutdownWorkers().then(() => setTimeout(() => process.exit(), 500));
108
113
  });
109
114
  });
110
115
  // TODO grep