creevey 0.9.2 → 0.10.0-beta.1

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 (271) hide show
  1. package/chromatic.config.json +5 -0
  2. package/dist/client/addon/components/Addon.d.ts +1 -0
  3. package/dist/client/addon/components/Addon.js.map +1 -1
  4. package/dist/client/addon/components/Icons.d.ts +1 -0
  5. package/dist/client/addon/components/Icons.js.map +1 -1
  6. package/dist/client/addon/components/Panel.d.ts +1 -0
  7. package/dist/client/addon/components/Panel.js.map +1 -1
  8. package/dist/client/addon/components/TestSelect.d.ts +1 -0
  9. package/dist/client/addon/components/TestSelect.js +4 -3
  10. package/dist/client/addon/components/TestSelect.js.map +1 -1
  11. package/dist/client/addon/components/Tools.d.ts +1 -0
  12. package/dist/client/addon/components/Tools.js +7 -8
  13. package/dist/client/addon/components/Tools.js.map +1 -1
  14. package/dist/client/addon/controller.d.ts +1 -1
  15. package/dist/client/addon/controller.js.map +1 -1
  16. package/dist/client/addon/decorator.d.ts +1 -1
  17. package/dist/client/addon/manager.js +3 -2
  18. package/dist/client/addon/manager.js.map +1 -1
  19. package/dist/client/addon/preview.d.ts +1 -1
  20. package/dist/client/addon/withCreevey.d.ts +6 -8
  21. package/dist/client/addon/withCreevey.js +21 -19
  22. package/dist/client/addon/withCreevey.js.map +1 -1
  23. package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
  24. package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
  25. package/dist/client/shared/components/ImagesView/ImagesView.d.ts +1 -0
  26. package/dist/client/shared/components/ImagesView/ImagesView.js.map +1 -1
  27. package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
  28. package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
  29. package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
  30. package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
  31. package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
  32. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  33. package/dist/client/shared/components/PageFooter/PageFooter.d.ts +1 -0
  34. package/dist/client/shared/components/PageFooter/PageFooter.js +1 -1
  35. package/dist/client/shared/components/PageFooter/PageFooter.js.map +1 -1
  36. package/dist/client/shared/components/PageFooter/Paging.d.ts +2 -2
  37. package/dist/client/shared/components/PageFooter/Paging.js +8 -6
  38. package/dist/client/shared/components/PageFooter/Paging.js.map +1 -1
  39. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  40. package/dist/client/shared/components/PageHeader/PageHeader.d.ts +1 -0
  41. package/dist/client/shared/components/PageHeader/PageHeader.js +2 -1
  42. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  43. package/dist/client/shared/components/ResultsPage.d.ts +2 -2
  44. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  45. package/dist/client/web/CreeveyApp.d.ts +1 -0
  46. package/dist/client/web/CreeveyApp.js.map +1 -1
  47. package/dist/client/web/CreeveyLoader.d.ts +1 -0
  48. package/dist/client/web/CreeveyLoader.js.map +1 -1
  49. package/dist/client/web/CreeveyView/SideBar/Checkbox.d.ts +1 -1
  50. package/dist/client/web/CreeveyView/SideBar/Checkbox.js +4 -4
  51. package/dist/client/web/CreeveyView/SideBar/Checkbox.js.map +1 -1
  52. package/dist/client/web/CreeveyView/SideBar/Search.d.ts +1 -0
  53. package/dist/client/web/CreeveyView/SideBar/Search.js +4 -4
  54. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  55. package/dist/client/web/CreeveyView/SideBar/SideBar.d.ts +1 -1
  56. package/dist/client/web/CreeveyView/SideBar/SideBar.js +1 -7
  57. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  58. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.d.ts +1 -0
  59. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +5 -4
  60. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  61. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.d.ts +1 -0
  62. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +4 -3
  63. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  64. package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +3 -7
  65. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +6 -5
  66. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  67. package/dist/client/web/CreeveyView/SideBar/TestLink.d.ts +1 -0
  68. package/dist/client/web/CreeveyView/SideBar/TestLink.js +5 -1
  69. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  70. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js +15 -8
  71. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js.map +1 -1
  72. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +5 -4
  73. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
  74. package/dist/client/web/CreeveyView/SideBar/Toggle.d.ts +1 -0
  75. package/dist/client/web/CreeveyView/SideBar/Toggle.js.map +1 -1
  76. package/dist/client/web/KeyboardEventsContext.d.ts +3 -4
  77. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  78. package/dist/client/web/assets/index-DkmZfG9C.js +591 -0
  79. package/dist/client/web/index.html +1 -1
  80. package/dist/client/web/index.js +5 -6
  81. package/dist/client/web/index.js.map +1 -1
  82. package/dist/creevey.js +21 -9
  83. package/dist/creevey.js.map +1 -1
  84. package/dist/index.js +2 -2
  85. package/dist/index.js.map +1 -1
  86. package/dist/playwright.d.ts +2 -0
  87. package/dist/playwright.js +6 -0
  88. package/dist/playwright.js.map +1 -0
  89. package/dist/selenium.d.ts +2 -0
  90. package/dist/selenium.js +6 -0
  91. package/dist/selenium.js.map +1 -0
  92. package/dist/server/config.d.ts +1 -1
  93. package/dist/server/config.js +16 -6
  94. package/dist/server/config.js.map +1 -1
  95. package/dist/server/docker.d.ts +2 -2
  96. package/dist/server/docker.js +46 -40
  97. package/dist/server/docker.js.map +1 -1
  98. package/dist/server/index.js +64 -15
  99. package/dist/server/index.js.map +1 -1
  100. package/dist/server/master/master.d.ts +1 -5
  101. package/dist/server/master/master.js +3 -3
  102. package/dist/server/master/master.js.map +1 -1
  103. package/dist/server/master/pool.d.ts +2 -1
  104. package/dist/server/master/pool.js +9 -5
  105. package/dist/server/master/pool.js.map +1 -1
  106. package/dist/server/master/runner.d.ts +1 -1
  107. package/dist/server/master/runner.js +2 -2
  108. package/dist/server/master/runner.js.map +1 -1
  109. package/dist/server/master/server.js +1 -0
  110. package/dist/server/master/server.js.map +1 -1
  111. package/dist/server/master/start.d.ts +3 -0
  112. package/dist/server/master/{index.js → start.js} +6 -9
  113. package/dist/server/master/start.js.map +1 -0
  114. package/dist/server/messages.d.ts +4 -10
  115. package/dist/server/messages.js +4 -58
  116. package/dist/server/messages.js.map +1 -1
  117. package/dist/server/playwright/docker-file.d.ts +1 -0
  118. package/dist/server/playwright/docker-file.js +27 -0
  119. package/dist/server/playwright/docker-file.js.map +1 -0
  120. package/dist/server/playwright/docker.d.ts +1 -0
  121. package/dist/server/playwright/docker.js +26 -0
  122. package/dist/server/playwright/docker.js.map +1 -0
  123. package/dist/server/playwright/internal.d.ts +25 -0
  124. package/dist/server/playwright/internal.js +319 -0
  125. package/dist/server/playwright/internal.js.map +1 -0
  126. package/dist/server/playwright/webdriver.d.ts +17 -0
  127. package/dist/server/playwright/webdriver.js +108 -0
  128. package/dist/server/playwright/webdriver.js.map +1 -0
  129. package/dist/server/providers/browser.d.ts +2 -0
  130. package/dist/server/{storybook/providers → providers}/browser.js +6 -7
  131. package/dist/server/providers/browser.js.map +1 -0
  132. package/dist/server/providers/hybrid.d.ts +2 -0
  133. package/dist/server/{storybook/providers → providers}/hybrid.js +8 -8
  134. package/dist/server/providers/hybrid.js.map +1 -0
  135. package/dist/server/reporter.d.ts +26 -0
  136. package/dist/server/{worker/reporter.js → reporter.js} +34 -56
  137. package/dist/server/reporter.js.map +1 -0
  138. package/dist/server/selenium/internal.d.ts +32 -0
  139. package/dist/server/selenium/internal.js +617 -0
  140. package/dist/server/selenium/internal.js.map +1 -0
  141. package/dist/server/selenium/selenoid.js +6 -13
  142. package/dist/server/selenium/selenoid.js.map +1 -1
  143. package/dist/server/selenium/webdriver.d.ts +25 -0
  144. package/dist/server/selenium/webdriver.js +113 -0
  145. package/dist/server/selenium/webdriver.js.map +1 -0
  146. package/dist/server/stories.js +16 -9
  147. package/dist/server/stories.js.map +1 -1
  148. package/dist/server/telemetry.d.ts +1 -1
  149. package/dist/server/telemetry.js +4 -4
  150. package/dist/server/telemetry.js.map +1 -1
  151. package/dist/server/utils.d.ts +3 -4
  152. package/dist/server/utils.js +10 -9
  153. package/dist/server/utils.js.map +1 -1
  154. package/dist/server/webdriver.d.ts +20 -0
  155. package/dist/server/webdriver.js +79 -0
  156. package/dist/server/webdriver.js.map +1 -0
  157. package/dist/server/worker/chai-image.d.ts +2 -5
  158. package/dist/server/worker/chai-image.js +14 -102
  159. package/dist/server/worker/chai-image.js.map +1 -1
  160. package/dist/server/worker/match-image.d.ts +14 -0
  161. package/dist/server/worker/match-image.js +231 -0
  162. package/dist/server/worker/match-image.js.map +1 -0
  163. package/dist/server/worker/start.d.ts +2 -0
  164. package/dist/server/worker/start.js +236 -0
  165. package/dist/server/worker/start.js.map +1 -0
  166. package/dist/types.d.ts +119 -65
  167. package/dist/types.js +15 -9
  168. package/dist/types.js.map +1 -1
  169. package/package.json +123 -112
  170. package/playwright.d.ts +2 -0
  171. package/selenium.d.ts +2 -0
  172. package/src/client/addon/components/Addon.tsx +1 -1
  173. package/src/client/addon/components/Icons.tsx +1 -1
  174. package/src/client/addon/components/Panel.tsx +1 -1
  175. package/src/client/addon/components/TestSelect.tsx +5 -5
  176. package/src/client/addon/components/Tools.tsx +9 -9
  177. package/src/client/addon/controller.ts +1 -1
  178. package/src/client/addon/manager.ts +4 -4
  179. package/src/client/addon/withCreevey.ts +26 -28
  180. package/src/client/shared/components/ImagesView/BlendView.tsx +1 -1
  181. package/src/client/shared/components/ImagesView/ImagesView.tsx +2 -2
  182. package/src/client/shared/components/ImagesView/SideBySideView.tsx +1 -1
  183. package/src/client/shared/components/ImagesView/SlideView.tsx +1 -1
  184. package/src/client/shared/components/ImagesView/SwapView.tsx +1 -1
  185. package/src/client/shared/components/PageFooter/PageFooter.tsx +2 -2
  186. package/src/client/shared/components/PageFooter/Paging.tsx +13 -13
  187. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -1
  188. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -3
  189. package/src/client/shared/components/ResultsPage.tsx +1 -1
  190. package/src/client/web/CreeveyApp.tsx +1 -1
  191. package/src/client/web/CreeveyLoader.tsx +1 -1
  192. package/src/client/web/CreeveyView/SideBar/Checkbox.tsx +6 -7
  193. package/src/client/web/CreeveyView/SideBar/Search.tsx +4 -4
  194. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +3 -10
  195. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +7 -6
  196. package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +7 -6
  197. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +8 -6
  198. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -3
  199. package/src/client/web/CreeveyView/SideBar/TestStatusIcon.tsx +18 -10
  200. package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +7 -10
  201. package/src/client/web/CreeveyView/SideBar/Toggle.tsx +1 -2
  202. package/src/client/web/KeyboardEventsContext.tsx +3 -4
  203. package/src/client/web/index.html +1 -1
  204. package/src/client/web/index.tsx +4 -3
  205. package/src/creevey.ts +25 -8
  206. package/src/index.ts +2 -2
  207. package/src/playwright.ts +1 -0
  208. package/src/selenium.ts +1 -0
  209. package/src/server/config.ts +25 -10
  210. package/src/server/docker.ts +58 -44
  211. package/src/server/index.ts +75 -18
  212. package/src/server/master/master.ts +3 -6
  213. package/src/server/master/pool.ts +20 -7
  214. package/src/server/master/runner.ts +2 -2
  215. package/src/server/master/server.ts +1 -0
  216. package/src/server/master/{index.ts → start.ts} +13 -11
  217. package/src/server/messages.ts +11 -75
  218. package/src/server/playwright/docker-file.ts +22 -0
  219. package/src/server/playwright/docker.ts +31 -0
  220. package/src/server/playwright/internal.ts +387 -0
  221. package/src/server/playwright/webdriver.ts +131 -0
  222. package/src/server/{storybook/providers → providers}/browser.ts +7 -8
  223. package/src/server/{storybook/providers → providers}/hybrid.ts +19 -19
  224. package/src/server/{worker/reporter.ts → reporter.ts} +40 -72
  225. package/src/server/selenium/internal.ts +812 -0
  226. package/src/server/selenium/selenoid.ts +12 -17
  227. package/src/server/selenium/webdriver.ts +146 -0
  228. package/src/server/stories.ts +18 -11
  229. package/src/server/telemetry.ts +2 -2
  230. package/src/server/utils.ts +9 -9
  231. package/src/server/webdriver.ts +130 -0
  232. package/src/server/worker/chai-image.ts +21 -133
  233. package/src/server/worker/match-image.ts +303 -0
  234. package/src/server/worker/start.ts +275 -0
  235. package/src/types.ts +155 -61
  236. package/types/playwright-context.d.ts +7 -0
  237. package/types/selenium-context.d.ts +7 -0
  238. package/dist/client/web/202.js +0 -1
  239. package/dist/client/web/270.js +0 -43
  240. package/dist/client/web/752.js +0 -1
  241. package/dist/client/web/main.js +0 -79
  242. package/dist/client/web/main.js.LICENSE.txt +0 -34
  243. package/dist/server/master/index.d.ts +0 -3
  244. package/dist/server/master/index.js.map +0 -1
  245. package/dist/server/selenium/browser.d.ts +0 -19
  246. package/dist/server/selenium/browser.js +0 -640
  247. package/dist/server/selenium/browser.js.map +0 -1
  248. package/dist/server/selenium/index.d.ts +0 -2
  249. package/dist/server/selenium/index.js +0 -19
  250. package/dist/server/selenium/index.js.map +0 -1
  251. package/dist/server/storybook/providers/browser.d.ts +0 -2
  252. package/dist/server/storybook/providers/browser.js.map +0 -1
  253. package/dist/server/storybook/providers/hybrid.d.ts +0 -2
  254. package/dist/server/storybook/providers/hybrid.js.map +0 -1
  255. package/dist/server/worker/helpers.d.ts +0 -8
  256. package/dist/server/worker/helpers.js +0 -57
  257. package/dist/server/worker/helpers.js.map +0 -1
  258. package/dist/server/worker/index.d.ts +0 -1
  259. package/dist/server/worker/index.js +0 -6
  260. package/dist/server/worker/index.js.map +0 -1
  261. package/dist/server/worker/reporter.d.ts +0 -8
  262. package/dist/server/worker/reporter.js.map +0 -1
  263. package/dist/server/worker/worker.d.ts +0 -4
  264. package/dist/server/worker/worker.js +0 -217
  265. package/dist/server/worker/worker.js.map +0 -1
  266. package/src/server/selenium/browser.ts +0 -840
  267. package/src/server/selenium/index.ts +0 -2
  268. package/src/server/worker/helpers.ts +0 -61
  269. package/src/server/worker/index.ts +0 -1
  270. package/src/server/worker/worker.ts +0 -245
  271. package/types/mocha.d.ts +0 -20
@@ -1,13 +1,15 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { pathToFileURL } from 'url';
4
- import { loadStories as browserStoriesProvider } from './storybook/providers/browser.js';
5
- import { Config, Browser, BrowserConfig, Options, isDefined } from '../types.js';
4
+ import { loadStories as browserStoriesProvider } from './providers/browser.js';
5
+ import { Config, BrowserConfig, BrowserConfigObject, Options, isDefined } from '../types.js';
6
6
  import { configExt, loadThroughTSX } from './utils.js';
7
+ import { CreeveyReporter, TeamcityReporter } from './reporter.js';
8
+ import { logger } from './logger.js';
7
9
 
8
10
  export const defaultBrowser = 'chrome';
9
11
 
10
- export const defaultConfig: Omit<Config, 'gridUrl' | 'storiesProvider' | 'testsDir' | 'tsConfig'> = {
12
+ export const defaultConfig: Omit<Config, 'gridUrl' | 'testsDir' | 'tsConfig' | 'webdriver'> = {
11
13
  disableTelemetry: false,
12
14
  useDocker: true,
13
15
  dockerImage: 'aerokube/selenoid:latest-release',
@@ -17,15 +19,18 @@ export const defaultConfig: Omit<Config, 'gridUrl' | 'storiesProvider' | 'testsD
17
19
  storybookUrl: 'http://localhost:6006',
18
20
  screenDir: path.resolve('images'),
19
21
  reportDir: path.resolve('report'),
22
+ reporter: process.env.TEAMCITY_VERSION ? TeamcityReporter : CreeveyReporter,
23
+ storiesProvider: browserStoriesProvider,
20
24
  maxRetries: 0,
21
- diffOptions: { threshold: 0, includeAA: true },
25
+ testTimeout: 30000,
26
+ diffOptions: { threshold: 0.05, includeAA: false },
27
+ odiffOptions: { threshold: 0.05, antialiasing: true },
22
28
  browsers: { [defaultBrowser]: true },
23
29
  hooks: {},
24
- babelOptions: (_) => _,
25
30
  testsRegex: /\.creevey\.(t|j)s$/,
26
31
  };
27
32
 
28
- function normalizeBrowserConfig(name: string, config: Browser): BrowserConfig {
33
+ function normalizeBrowserConfig(name: string, config: BrowserConfig): BrowserConfigObject {
29
34
  if (typeof config == 'boolean') return { browserName: name };
30
35
  if (typeof config == 'string') return { browserName: config };
31
36
  return config;
@@ -56,17 +61,27 @@ export async function readConfig(options: Options): Promise<Config> {
56
61
  const userConfig: typeof defaultConfig & Partial<Pick<Config, 'gridUrl' | 'storiesProvider'>> = { ...defaultConfig };
57
62
 
58
63
  if (isDefined(configPath)) {
59
- const configModule = await loadThroughTSX<{ default: Partial<Config> } | Partial<Config>>((load) => {
64
+ const configModule = await loadThroughTSX<
65
+ { default: { default: Partial<Config> } | Partial<Config> } | Partial<Config>
66
+ >((load) => {
60
67
  const configFileUrl = pathToFileURL(configPath).toString();
61
68
  return load(configFileUrl);
62
69
  });
63
- const configData = 'default' in configModule ? configModule.default : configModule;
70
+ let configData = 'default' in configModule ? configModule.default : configModule;
71
+ // NOTE In node > 18 with commonjs project and esm config with tsconfig moduleResolution nodeNext there is additional 'default'
72
+ configData = 'default' in configData ? configData.default : configData;
73
+
74
+ if (!configData.webdriver) {
75
+ const { SeleniumWebdriver } = await import('./selenium/webdriver.js');
76
+ logger.warn(
77
+ "Creevey supports `Selenium` and `Playwright` webdrivers. For backward compatibility `Selenium` is used by default, but it might changed in the future. Please explicitly specify one of webdrivers in your Creevey's config",
78
+ );
79
+ configData.webdriver = SeleniumWebdriver;
80
+ }
64
81
 
65
82
  Object.assign(userConfig, configData);
66
83
  }
67
84
 
68
- if (!userConfig.storiesProvider) userConfig.storiesProvider = browserStoriesProvider;
69
-
70
85
  if (options.failFast != undefined) userConfig.failFast = Boolean(options.failFast);
71
86
  if (options.reportDir) userConfig.reportDir = path.resolve(options.reportDir);
72
87
  if (options.screenDir) userConfig.screenDir = path.resolve(options.screenDir);
@@ -1,16 +1,14 @@
1
- import cluster from 'cluster';
2
- import ora from 'ora';
1
+ import tar from 'tar-stream';
3
2
  import { Writable } from 'stream';
4
3
  import Dockerode, { Container } from 'dockerode';
5
- import { Config, BrowserConfig, isDockerMessage, DockerAuth } from '../types.js';
6
- import { subscribeOn, sendDockerMessage, emitDockerMessage } from './messages.js';
7
- import { isInsideDocker, LOCALHOST_REGEXP } from './utils.js';
4
+ import { DockerAuth } from '../types.js';
5
+ import { subscribeOn } from './messages.js';
8
6
  import { logger } from './logger.js';
9
7
 
10
8
  const docker = new Dockerode();
11
9
 
12
10
  class DevNull extends Writable {
13
- _write(_chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null | undefined) => void): void {
11
+ _write(_chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
14
12
  setImmediate(callback);
15
13
  }
16
14
  }
@@ -24,13 +22,15 @@ export async function pullImages(
24
22
  if (platform) args.platform = platform;
25
23
 
26
24
  logger.info('Pull docker images');
25
+ // TODO Replace with `import from`
26
+ const { default: yoctoSpinner } = await import('yocto-spinner');
27
27
  for (const image of images) {
28
28
  await new Promise<void>((resolve, reject) => {
29
- const spinner = ora(`${image}: Pull start`).start();
29
+ const spinner = yoctoSpinner({ text: `${image}: Pull start` }).start();
30
30
 
31
31
  docker.pull(image, args, (pullError: Error | null, stream?: NodeJS.ReadableStream) => {
32
32
  if (pullError || !stream) {
33
- spinner.fail();
33
+ spinner.error(pullError?.message);
34
34
  reject(pullError ?? new Error('Unknown error'));
35
35
  return;
36
36
  }
@@ -39,11 +39,11 @@ export async function pullImages(
39
39
 
40
40
  function onFinished(error: Error | null): void {
41
41
  if (error) {
42
- spinner.fail();
42
+ spinner.error(error.message);
43
43
  reject(error);
44
44
  return;
45
45
  }
46
- spinner.succeed(`${image}: Pull complete`);
46
+ spinner.success(`${image}: Pull complete`);
47
47
  resolve();
48
48
  }
49
49
 
@@ -57,6 +57,54 @@ export async function pullImages(
57
57
  }
58
58
  }
59
59
 
60
+ export async function buildImage(imageName: string, dockerfile: string): Promise<void> {
61
+ const images = await docker.listImages({ filters: { label: [`creevey=${imageName}`] } });
62
+
63
+ if (images.at(0)) {
64
+ logger.info(`Image ${imageName} already exists`);
65
+ return;
66
+ }
67
+
68
+ const pack = tar.pack();
69
+ pack.entry({ name: 'Dockerfile' }, dockerfile);
70
+ pack.finalize();
71
+
72
+ const { default: yoctoSpinner } = await import('yocto-spinner');
73
+ const spinner = yoctoSpinner({ text: `${imageName}: Build start` }).start();
74
+ await new Promise<void>((resolve, reject) => {
75
+ void docker.buildImage(
76
+ // @ts-expect-error Type incompatibility AsyncIterator and AsyncIterableIterator
77
+ pack,
78
+ { t: imageName, labels: { creevey: imageName } },
79
+ (buildError: Error | null, stream) => {
80
+ if (buildError || !stream) {
81
+ spinner.error(buildError?.message);
82
+ reject(buildError ?? new Error('Unknown error'));
83
+ return;
84
+ }
85
+
86
+ docker.modem.followProgress(stream, onFinished, onProgress);
87
+
88
+ function onFinished(error: Error | null): void {
89
+ if (error) {
90
+ spinner.error(error.message);
91
+ reject(error);
92
+ return;
93
+ }
94
+ spinner.success(`${imageName}: Build complete`);
95
+ resolve();
96
+ }
97
+
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 : ''}`;
102
+ }
103
+ },
104
+ );
105
+ });
106
+ }
107
+
60
108
  export async function runImage(
61
109
  image: string,
62
110
  args: string[],
@@ -100,37 +148,3 @@ export async function runImage(
100
148
  );
101
149
  });
102
150
  }
103
-
104
- export async function initDocker(
105
- config: Config,
106
- browser: string | undefined,
107
- startContainer: () => Promise<string>,
108
- ): Promise<void> {
109
- if (cluster.isPrimary) {
110
- const host = await startContainer();
111
- let gridUrl = 'http://localhost:4444/wd/hub';
112
- gridUrl = isInsideDocker ? gridUrl.replace(LOCALHOST_REGEXP, host) : gridUrl;
113
- cluster.on('message', (worker, message: unknown) => {
114
- if (!isDockerMessage(message)) return;
115
-
116
- const dockerMessage = message;
117
- if (dockerMessage.type != 'start') return;
118
-
119
- sendDockerMessage(worker, {
120
- type: 'success',
121
- payload: { gridUrl },
122
- });
123
- });
124
- } else {
125
- if (browser && (config.browsers[browser] as BrowserConfig).gridUrl) return Promise.resolve();
126
- return new Promise((resolve) => {
127
- subscribeOn('docker', (message) => {
128
- if (message.type == 'success') {
129
- config.gridUrl = message.payload.gridUrl;
130
- resolve();
131
- }
132
- });
133
- emitDockerMessage({ type: 'start' });
134
- });
135
- }
136
- }
@@ -1,30 +1,88 @@
1
1
  import cluster from 'cluster';
2
2
  import { readConfig, defaultBrowser } from './config.js';
3
- import { Options, Config, BrowserConfig } from '../types.js';
3
+ import { Options, Config, BrowserConfigObject, isWorkerMessage } from '../types.js';
4
4
  import { logger } from './logger.js';
5
+ import { SeleniumWebdriver } from './selenium/webdriver.js';
6
+ 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';
10
+ import { buildImage } from './docker.js';
5
11
 
6
- // NOTE: Impure function, mutate config by adding gridUrl prop
7
- async function startWebdriverServer(config: Config, options: Options): Promise<void> {
8
- if (config.useDocker) {
9
- return (await import('./docker.js')).initDocker(config, options.browser, async () =>
10
- (await import('./selenium/selenoid.js')).startSelenoidContainer(config, options.debug),
11
- );
12
+ async function startWebdriverServer(browser: string, config: Config, options: Options): Promise<string | undefined> {
13
+ if (config.webdriver === SeleniumWebdriver) {
14
+ if (cluster.isPrimary) {
15
+ // TODO Get random free port
16
+ const { startSelenoidContainer, startSelenoidStandalone } = await import('./selenium/selenoid.js');
17
+ const gridUrl = 'http://localhost:4444/wd/hub';
18
+ if (config.useDocker) {
19
+ const host = await startSelenoidContainer(config, options.debug);
20
+ return isInsideDocker ? gridUrl.replace(LOCALHOST_REGEXP, host) : gridUrl;
21
+ }
22
+ await startSelenoidStandalone(config, options.debug);
23
+ return gridUrl;
24
+ }
25
+ // TODO Worker might want to use docker image of browser or start standalone selenium
12
26
  } else {
13
- return (await import('./selenium/selenoid.js')).startSelenoidStandalone(config, options.debug);
27
+ // TODO start standalone playwright server (useDocker == false)
28
+
29
+ const {
30
+ default: { version },
31
+ } = await import('playwright-core/package.json', { with: { type: 'json' } });
32
+
33
+ if (cluster.isWorker) {
34
+ // TODO Re-use dockerImage
35
+
36
+ const { startPlaywrightContainer } = await import('./playwright/docker.js');
37
+ const { browserName } = config.browsers[browser] as BrowserConfigObject;
38
+
39
+ const imageName = `creevey/${browserName}:v${version}`;
40
+ const host = await startPlaywrightContainer(imageName, options.debug);
41
+
42
+ return host;
43
+ } else {
44
+ const browsers = [...new Set(Object.values(config.browsers).map((c) => (c as BrowserConfigObject).browserName))];
45
+ await Promise.all(
46
+ browsers.map(async (browserName) => {
47
+ const imageName = `creevey/${browserName}:v${version}`;
48
+ const dockerfile = playwrightDockerFile(browserName, version);
49
+
50
+ await buildImage(imageName, dockerfile);
51
+ }),
52
+ );
53
+
54
+ const { default: getPort } = await import('get-port');
55
+
56
+ cluster.on('message', (worker, message: unknown) => {
57
+ if (!isWorkerMessage(message)) return;
58
+
59
+ const workerMessage = message;
60
+ if (workerMessage.type != 'port') return;
61
+
62
+ void getPort().then((port) => {
63
+ sendWorkerMessage(worker, {
64
+ type: 'port',
65
+ payload: { port },
66
+ });
67
+ });
68
+ });
69
+ }
70
+ // TODO Support gridUrl for playwright
71
+ // NOTE: There is no grid for playwright right now
14
72
  }
15
73
  }
16
74
 
17
75
  export default async function (options: Options): Promise<void> {
18
76
  const config = await readConfig(options);
19
- const { browser = defaultBrowser, tests, update, ui, port } = options;
77
+ const { browser = defaultBrowser, update, ui, port } = options;
78
+ let gridUrl = cluster.isPrimary ? config.gridUrl : options.gridUrl;
20
79
 
21
- // NOTE: We don't need docker nor selenoid for webpack or update options
80
+ // NOTE: We don't need docker nor selenoid for update option
22
81
  if (
23
- !(config.gridUrl || (Object.values(config.browsers) as BrowserConfig[]).every(({ gridUrl }) => gridUrl)) &&
24
- !tests &&
82
+ !(gridUrl || (Object.values(config.browsers) as BrowserConfigObject[]).every(({ gridUrl }) => gridUrl)) &&
25
83
  !update
26
84
  ) {
27
- await startWebdriverServer(config, options);
85
+ gridUrl = await startWebdriverServer(browser, config, options);
28
86
  }
29
87
 
30
88
  switch (true) {
@@ -37,15 +95,14 @@ export default async function (options: Options): Promise<void> {
37
95
 
38
96
  const resolveApi = (await import('./master/server.js')).start(config.reportDir, port, ui);
39
97
 
40
- return (await import('./master/index.js')).start(config, options, resolveApi);
98
+ return (await import('./master/start.js')).start(gridUrl, config, options, resolveApi);
41
99
  }
42
100
  default: {
43
101
  logger.info(`Starting Worker for ${browser}`);
44
102
 
45
- return (await import('./worker/index.js')).start(config, {
46
- ...options,
47
- browser,
48
- });
103
+ // NOTE: We assume that we pass `gridUrl` to worker CLI options
104
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
105
+ return (await import('./worker/start.js')).start(browser, gridUrl!, config, options);
49
106
  }
50
107
  }
51
108
  }
@@ -21,11 +21,8 @@ function mergeTests(
21
21
  return testsFromStories;
22
22
  }
23
23
 
24
- export default async function master(
25
- config: Config,
26
- options: { watch: boolean; debug: boolean; port: number },
27
- ): Promise<Runner> {
28
- const runner = new Runner(config);
24
+ export default async function master(config: Config, gridUrl?: string): Promise<Runner> {
25
+ const runner = new Runner(config, gridUrl);
29
26
  const reportDataPath = path.join(config.reportDir, 'data.js');
30
27
  const testsFromReport = tryToLoadTestsData(reportDataPath) ?? {};
31
28
 
@@ -33,7 +30,7 @@ export default async function master(
33
30
 
34
31
  const tests = await loadTestsFromStories(
35
32
  Object.keys(config.browsers),
36
- (listener) => config.storiesProvider(config, options, listener),
33
+ (listener) => config.storiesProvider(config, listener),
37
34
  (testsDiff) => {
38
35
  runner.updateTests(testsDiff);
39
36
  saveTestsJson(runner.tests, config.reportDir);
@@ -1,6 +1,14 @@
1
1
  import cluster, { Worker as ClusterWorker } from 'cluster';
2
2
  import { EventEmitter } from 'events';
3
- import { Worker, Config, TestResult, BrowserConfig, WorkerMessage, TestStatus, isWorkerMessage } from '../../types.js';
3
+ import {
4
+ Worker,
5
+ Config,
6
+ TestResult,
7
+ BrowserConfigObject,
8
+ WorkerMessage,
9
+ TestStatus,
10
+ isWorkerMessage,
11
+ } from '../../types.js';
4
12
  import { sendTestMessage, sendShutdownMessage, subscribeOnWorker } from '../messages.js';
5
13
  import { isShuttingDown } from '../utils.js';
6
14
 
@@ -14,23 +22,26 @@ interface WorkerTest {
14
22
 
15
23
  export default class Pool extends EventEmitter {
16
24
  private maxRetries: number;
17
- private config: BrowserConfig;
25
+ private config: BrowserConfigObject;
18
26
  private workers: Worker[] = [];
19
27
  private queue: WorkerTest[] = [];
20
28
  private forcedStop = false;
21
29
  private failFast: boolean;
30
+ private gridUrl?: string;
22
31
  public get isRunning(): boolean {
23
32
  return this.workers.length !== this.freeWorkers.length;
24
33
  }
25
34
  constructor(
26
35
  config: Config,
27
36
  private browser: string,
37
+ gridUrl?: string,
28
38
  ) {
29
39
  super();
30
40
 
31
41
  this.failFast = config.failFast;
32
42
  this.maxRetries = config.maxRetries;
33
- this.config = config.browsers[browser] as BrowserConfig;
43
+ this.config = config.browsers[browser] as BrowserConfigObject;
44
+ this.gridUrl = this.config.gridUrl ?? gridUrl;
34
45
  }
35
46
 
36
47
  async init(): Promise<void> {
@@ -111,12 +122,12 @@ export default class Pool extends EventEmitter {
111
122
 
112
123
  private async forkWorker(retry = 0): Promise<Worker | { error: string }> {
113
124
  cluster.setupPrimary({
114
- args: ['--browser', this.browser, ...process.argv.slice(2)],
125
+ args: ['--browser', this.browser, ...(this.gridUrl ? ['--gridUrl', this.gridUrl] : []), ...process.argv.slice(2)],
115
126
  });
116
127
  const worker = cluster.fork();
117
128
  const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
118
129
  const readyHandler = (message: unknown): void => {
119
- if (!isWorkerMessage(message)) return;
130
+ if (!isWorkerMessage(message) || message.type == 'port') return;
120
131
  worker.off('message', readyHandler);
121
132
  resolve(message);
122
133
  };
@@ -188,9 +199,11 @@ export default class Pool extends EventEmitter {
188
199
  unsubscribe();
189
200
  });
190
201
 
191
- this.gracefullyKill(worker);
202
+ if (message.payload.subtype == 'unknown') {
203
+ this.gracefullyKill(worker);
204
+ }
192
205
 
193
- this.handleTestResult(worker, test, { status: 'failed', ...message.payload });
206
+ this.handleTestResult(worker, test, { status: 'failed', error: message.payload.error });
194
207
  }),
195
208
  subscribeOnWorker(worker, 'test', (message) => {
196
209
  if (message.type != 'end') return;
@@ -24,7 +24,7 @@ export default class Runner extends EventEmitter {
24
24
  public get isRunning(): boolean {
25
25
  return Object.values(this.pools).some((pool) => pool.isRunning);
26
26
  }
27
- constructor(config: Config) {
27
+ constructor(config: Config, gridUrl?: string) {
28
28
  super();
29
29
 
30
30
  this.failFast = config.failFast;
@@ -32,7 +32,7 @@ export default class Runner extends EventEmitter {
32
32
  this.reportDir = config.reportDir;
33
33
  this.browsers = Object.keys(config.browsers);
34
34
  this.browsers
35
- .map((browser) => (this.pools[browser] = new Pool(config, browser)))
35
+ .map((browser) => (this.pools[browser] = new Pool(config, browser, gridUrl)))
36
36
  .map((pool) => pool.on('test', this.handlePoolMessage));
37
37
  }
38
38
 
@@ -21,6 +21,7 @@ export function start(reportDir: string, port: number, ui: boolean): (api: Creev
21
21
  let setStoriesCounter = 0;
22
22
  const creeveyApi = new Promise<CreeveyApi>((resolve) => (resolveApi = resolve));
23
23
  const app = new Koa();
24
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
24
25
  const server = http.createServer(app.callback());
25
26
  const wss = new WebSocket.Server({ server });
26
27
 
@@ -5,7 +5,7 @@ import { copyFile, readdir, mkdir, writeFile } from 'fs/promises';
5
5
  import master from './master.js';
6
6
  import creeveyApi, { CreeveyApi } from './api.js';
7
7
  import { Config, Options, TestData, isDefined } from '../../types.js';
8
- import { shutdown, shutdownWorkers, testsToImages, readDirRecursive } from '../utils.js';
8
+ import { shutdownWorkers, testsToImages, readDirRecursive } from '../utils.js';
9
9
  import { subscribeOn } from '../messages.js';
10
10
  import Runner from './runner.js';
11
11
  import { logger } from '../logger.js';
@@ -46,13 +46,17 @@ function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
46
46
  }
47
47
  }
48
48
 
49
- export async function start(config: Config, options: Options, resolveApi: (api: CreeveyApi) => void): Promise<void> {
49
+ export async function start(
50
+ gridUrl: string | undefined,
51
+ config: Config,
52
+ options: Options,
53
+ resolveApi: (api: CreeveyApi) => void,
54
+ ): Promise<void> {
50
55
  let runner: Runner | null = null;
51
56
  if (config.hooks.before) {
52
57
  await config.hooks.before();
53
58
  }
54
59
  subscribeOn('shutdown', () => config.hooks.after?.());
55
- process.removeListener('SIGINT', shutdown);
56
60
  process.on('SIGINT', () => {
57
61
  runner?.removeAllListeners('stop');
58
62
  if (runner?.isRunning) {
@@ -67,15 +71,13 @@ export async function start(config: Config, options: Options, resolveApi: (api:
67
71
  }
68
72
  });
69
73
 
70
- runner = await master(config, { watch: options.ui, debug: options.debug, port: options.port });
74
+ runner = await master(config, gridUrl);
71
75
 
72
- if (options.saveReport) {
73
- runner.on('stop', () => {
74
- void copyStatics(config.reportDir).then(() =>
75
- writeFile(path.join(config.reportDir, 'data.js'), reportDataModule(runner.status.tests)),
76
- );
77
- });
78
- }
76
+ runner.on('stop', () => {
77
+ void copyStatics(config.reportDir).then(() =>
78
+ writeFile(path.join(config.reportDir, 'data.js'), reportDataModule(runner.status.tests)),
79
+ );
80
+ });
79
81
 
80
82
  if (options.ui) {
81
83
  resolveApi(creeveyApi(runner));