creevey 0.10.0-beta.1 → 0.10.0-beta.10

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 (137) hide show
  1. package/dist/client/addon/components/Panel.js +2 -2
  2. package/dist/client/addon/components/Panel.js.map +1 -1
  3. package/dist/client/addon/controller.js +4 -5
  4. package/dist/client/addon/controller.js.map +1 -1
  5. package/dist/client/addon/withCreevey.js +18 -34
  6. package/dist/client/addon/withCreevey.js.map +1 -1
  7. package/dist/client/shared/components/ImagesView/SwapView.js +12 -0
  8. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  9. package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
  10. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  11. package/dist/client/shared/components/ResultsPage.js +23 -5
  12. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  13. package/dist/client/web/CreeveyApp.js +22 -6
  14. package/dist/client/web/CreeveyApp.js.map +1 -1
  15. package/dist/client/web/CreeveyContext.d.ts +5 -0
  16. package/dist/client/web/CreeveyContext.js +3 -0
  17. package/dist/client/web/CreeveyContext.js.map +1 -1
  18. package/dist/client/web/CreeveyView/SideBar/Search.js +2 -2
  19. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  20. package/dist/client/web/CreeveyView/SideBar/SideBar.js +1 -0
  21. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  22. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +49 -6
  23. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  24. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +1 -3
  25. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  26. package/dist/client/web/CreeveyView/SideBar/TestLink.js +1 -3
  27. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  28. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  29. package/dist/client/web/KeyboardEventsContext.js +62 -57
  30. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  31. package/dist/client/web/assets/{index-DkmZfG9C.js → index-BE9CL5_G.js} +94 -94
  32. package/dist/client/web/index.html +1 -1
  33. package/dist/creevey.js +13 -5
  34. package/dist/creevey.js.map +1 -1
  35. package/dist/server/config.js +4 -3
  36. package/dist/server/config.js.map +1 -1
  37. package/dist/server/docker.js +2 -2
  38. package/dist/server/docker.js.map +1 -1
  39. package/dist/server/index.js +29 -3
  40. package/dist/server/index.js.map +1 -1
  41. package/dist/server/logger.d.ts +2 -1
  42. package/dist/server/logger.js +7 -3
  43. package/dist/server/logger.js.map +1 -1
  44. package/dist/server/master/api.js +1 -1
  45. package/dist/server/master/api.js.map +1 -1
  46. package/dist/server/master/pool.d.ts +3 -3
  47. package/dist/server/master/pool.js +10 -63
  48. package/dist/server/master/pool.js.map +1 -1
  49. package/dist/server/master/queue.d.ts +13 -0
  50. package/dist/server/master/queue.js +64 -0
  51. package/dist/server/master/queue.js.map +1 -0
  52. package/dist/server/master/runner.d.ts +1 -0
  53. package/dist/server/master/runner.js +4 -1
  54. package/dist/server/master/runner.js.map +1 -1
  55. package/dist/server/master/server.js +1 -1
  56. package/dist/server/master/server.js.map +1 -1
  57. package/dist/server/master/start.js +4 -4
  58. package/dist/server/master/start.js.map +1 -1
  59. package/dist/server/playwright/docker-file.js +12 -2
  60. package/dist/server/playwright/docker-file.js.map +1 -1
  61. package/dist/server/playwright/internal.d.ts +2 -2
  62. package/dist/server/playwright/internal.js +56 -44
  63. package/dist/server/playwright/internal.js.map +1 -1
  64. package/dist/server/playwright/webdriver.js +1 -1
  65. package/dist/server/playwright/webdriver.js.map +1 -1
  66. package/dist/server/providers/browser.js +2 -1
  67. package/dist/server/providers/browser.js.map +1 -1
  68. package/dist/server/providers/hybrid.js +1 -1
  69. package/dist/server/providers/hybrid.js.map +1 -1
  70. package/dist/server/reporter.js +4 -4
  71. package/dist/server/reporter.js.map +1 -1
  72. package/dist/server/selenium/internal.d.ts +2 -3
  73. package/dist/server/selenium/internal.js +116 -90
  74. package/dist/server/selenium/internal.js.map +1 -1
  75. package/dist/server/selenium/selenoid.js +2 -2
  76. package/dist/server/selenium/selenoid.js.map +1 -1
  77. package/dist/server/selenium/webdriver.js +1 -1
  78. package/dist/server/selenium/webdriver.js.map +1 -1
  79. package/dist/server/telemetry.js +7 -3
  80. package/dist/server/telemetry.js.map +1 -1
  81. package/dist/server/utils.d.ts +2 -1
  82. package/dist/server/utils.js +13 -3
  83. package/dist/server/utils.js.map +1 -1
  84. package/dist/server/webdriver.d.ts +2 -3
  85. package/dist/server/webdriver.js +10 -9
  86. package/dist/server/webdriver.js.map +1 -1
  87. package/dist/server/worker/chai-image.d.ts +1 -2
  88. package/dist/server/worker/chai-image.js +4 -3
  89. package/dist/server/worker/chai-image.js.map +1 -1
  90. package/dist/server/worker/start.js +24 -14
  91. package/dist/server/worker/start.js.map +1 -1
  92. package/dist/types.d.ts +30 -11
  93. package/dist/types.js +13 -1
  94. package/dist/types.js.map +1 -1
  95. package/package.json +36 -42
  96. package/src/client/addon/components/Panel.tsx +2 -2
  97. package/src/client/addon/controller.ts +13 -6
  98. package/src/client/addon/withCreevey.ts +25 -13
  99. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  100. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  101. package/src/client/shared/components/ResultsPage.tsx +28 -7
  102. package/src/client/web/CreeveyApp.tsx +25 -7
  103. package/src/client/web/CreeveyContext.tsx +9 -0
  104. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  105. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  106. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  107. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  108. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  109. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  110. package/src/creevey.ts +13 -6
  111. package/src/server/config.ts +4 -3
  112. package/src/server/docker.ts +2 -2
  113. package/src/server/index.ts +27 -4
  114. package/src/server/logger.ts +6 -2
  115. package/src/server/master/api.ts +1 -1
  116. package/src/server/master/pool.ts +18 -56
  117. package/src/server/master/queue.ts +64 -0
  118. package/src/server/master/runner.ts +4 -1
  119. package/src/server/master/server.ts +1 -1
  120. package/src/server/master/start.ts +7 -4
  121. package/src/server/playwright/docker-file.ts +14 -2
  122. package/src/server/playwright/internal.ts +76 -49
  123. package/src/server/playwright/webdriver.ts +1 -1
  124. package/src/server/providers/browser.ts +2 -1
  125. package/src/server/providers/hybrid.ts +1 -1
  126. package/src/server/reporter.ts +4 -3
  127. package/src/server/selenium/internal.ts +119 -92
  128. package/src/server/selenium/selenoid.ts +2 -2
  129. package/src/server/selenium/webdriver.ts +1 -1
  130. package/src/server/telemetry.ts +7 -3
  131. package/src/server/utils.ts +14 -4
  132. package/src/server/webdriver.ts +10 -15
  133. package/src/server/worker/chai-image.ts +4 -4
  134. package/src/server/worker/start.ts +25 -16
  135. package/src/types.ts +32 -13
  136. package/.yarnrc.yml +0 -1
  137. package/chromatic.config.json +0 -5
@@ -1,20 +1,7 @@
1
- import React, { JSX, useState, useEffect, useCallback, useContext, PropsWithChildren } from 'react';
2
- import { CreeveySuite, isTest, noop } from '../../types.js';
1
+ import React, { JSX, useEffect, useCallback, PropsWithChildren } from 'react';
2
+ import { CreeveySuite, isTest } from '../../types.js';
3
3
  import { CreeveyViewFilter, filterTests, flattenSuite, getSuiteByPath, getTestPath } from '../shared/helpers.js';
4
- import { CreeveyContext } from './CreeveyContext.js';
5
-
6
- export type SuitePath = string[];
7
- export type FocusableItem = null | SuitePath;
8
-
9
- export interface KeyboardEventsContextType {
10
- sidebarFocusedItem: FocusableItem;
11
- setSidebarFocusedItem: (item: FocusableItem) => void;
12
- }
13
-
14
- export const KeyboardEventsContext = React.createContext<KeyboardEventsContextType>({
15
- sidebarFocusedItem: [],
16
- setSidebarFocusedItem: noop,
17
- });
4
+ import { useCreeveyContext } from './CreeveyContext.js';
18
5
 
19
6
  export const KeyboardEvents = ({
20
7
  children,
@@ -24,9 +11,7 @@ export const KeyboardEvents = ({
24
11
  rootSuite: CreeveySuite;
25
12
  filter: CreeveyViewFilter;
26
13
  }>): JSX.Element => {
27
- const [sidebarFocusedItem, setSidebarFocusedItem] = useState<FocusableItem>([]);
28
-
29
- const { onSuiteOpen, onSuiteToggle } = useContext(CreeveyContext);
14
+ const { onSuiteOpen, onSuiteToggle, sidebarFocusedItem, setSidebarFocusedItem } = useCreeveyContext();
30
15
 
31
16
  const suiteList = flattenSuite(filterTests(rootSuite, filter));
32
17
 
@@ -45,63 +30,70 @@ export const KeyboardEvents = ({
45
30
  (e: KeyboardEvent) => {
46
31
  if (sidebarFocusedItem === null) return;
47
32
 
48
- if (e.code === 'Enter') {
49
- if (sidebarFocusedItem.length === 0) return;
50
-
51
- const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
52
- if (!focusedSuite) return;
53
- if (!isTest(focusedSuite)) {
54
- e.preventDefault();
55
- onSuiteOpen(focusedSuite.path, !focusedSuite.opened);
33
+ switch (e.code) {
34
+ case 'Enter': {
35
+ if (sidebarFocusedItem.length === 0) return;
36
+
37
+ const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
38
+ if (!focusedSuite) return;
39
+ if (!isTest(focusedSuite)) {
40
+ e.preventDefault();
41
+ onSuiteOpen(focusedSuite.path, !focusedSuite.opened);
42
+ }
43
+ if (isTest(focusedSuite) && focusedSuite.results?.length == 0) {
44
+ e.preventDefault();
45
+ }
46
+ return;
56
47
  }
57
- if (isTest(focusedSuite) && focusedSuite.results?.length == 0) {
48
+ case 'Space': {
58
49
  e.preventDefault();
50
+ // TODO handle keys in one place
51
+ if (e.altKey) return;
52
+ const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
53
+ if (!focusedSuite) return;
54
+ const path = isTest(focusedSuite) ? getTestPath(focusedSuite) : focusedSuite.path;
55
+ onSuiteToggle(path, !focusedSuite.checked);
56
+ return;
59
57
  }
60
- return;
61
- }
62
- if (e.code === 'Space') {
63
- e.preventDefault();
64
- const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
65
- if (!focusedSuite) return;
66
- const path = isTest(focusedSuite) ? getTestPath(focusedSuite) : focusedSuite.path;
67
- onSuiteToggle(path, !focusedSuite.checked);
68
- }
69
- if (e.code === 'ArrowDown') {
70
- const currentIndex = sidebarFocusedItem.length === 0 ? -1 : getFocusedItemIndex(sidebarFocusedItem);
71
- if (currentIndex === suiteList.length - 1) return;
72
- const nextSuite = suiteList[currentIndex + 1];
73
- const nextPath = isTest(nextSuite.suite) ? getTestPath(nextSuite.suite) : nextSuite.suite.path;
74
- setSidebarFocusedItem(nextPath);
75
- }
76
- if (e.code === 'ArrowUp') {
77
- const currentIndex = sidebarFocusedItem.length === 0 ? 0 : getFocusedItemIndex(sidebarFocusedItem);
78
- const nextSuite = currentIndex > 0 ? suiteList[currentIndex - 1].suite : rootSuite;
79
- const nextPath = isTest(nextSuite) ? getTestPath(nextSuite) : nextSuite.path;
80
- setSidebarFocusedItem(nextPath);
81
- }
82
-
83
- if (e.code === 'ArrowRight') {
84
- if (sidebarFocusedItem.length === 0) return;
85
- const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
86
-
87
- if (!focusedSuite || isTest(focusedSuite)) return;
88
- onSuiteOpen(focusedSuite.path, true);
89
- }
90
-
91
- if (e.code === 'ArrowLeft') {
92
- if (sidebarFocusedItem.length === 0) return;
93
- const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
94
- if (!focusedSuite) return;
95
- if (!isTest(focusedSuite) && focusedSuite.opened) {
96
- onSuiteOpen(focusedSuite.path, false);
58
+ case 'ArrowDown': {
59
+ const currentIndex = sidebarFocusedItem.length === 0 ? -1 : getFocusedItemIndex(sidebarFocusedItem);
60
+ if (currentIndex === suiteList.length - 1) return;
61
+ const nextSuite = suiteList[currentIndex + 1];
62
+ const nextPath = isTest(nextSuite.suite) ? getTestPath(nextSuite.suite) : nextSuite.suite.path;
63
+ setSidebarFocusedItem(nextPath);
64
+ return;
65
+ }
66
+ case 'ArrowUp': {
67
+ const currentIndex = sidebarFocusedItem.length === 0 ? 0 : getFocusedItemIndex(sidebarFocusedItem);
68
+ const nextSuite = currentIndex > 0 ? suiteList[currentIndex - 1].suite : rootSuite;
69
+ const nextPath = isTest(nextSuite) ? getTestPath(nextSuite) : nextSuite.path;
70
+ setSidebarFocusedItem(nextPath);
97
71
  return;
98
72
  }
73
+ case 'ArrowRight': {
74
+ if (sidebarFocusedItem.length === 0) return;
75
+ const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
99
76
 
100
- const path = isTest(focusedSuite) ? getTestPath(focusedSuite) : focusedSuite.path;
101
- setSidebarFocusedItem(path.slice(0, -1));
77
+ if (!focusedSuite || isTest(focusedSuite)) return;
78
+ onSuiteOpen(focusedSuite.path, true);
79
+ return;
80
+ }
81
+ case 'ArrowLeft': {
82
+ if (sidebarFocusedItem.length === 0) return;
83
+ const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
84
+ if (!focusedSuite) return;
85
+ if (!isTest(focusedSuite) && focusedSuite.opened) {
86
+ onSuiteOpen(focusedSuite.path, false);
87
+ return;
88
+ }
89
+
90
+ const path = isTest(focusedSuite) ? getTestPath(focusedSuite) : focusedSuite.path;
91
+ setSidebarFocusedItem(path.slice(0, -1));
92
+ return;
93
+ }
102
94
  }
103
95
  },
104
- [onSuiteOpen, onSuiteToggle, rootSuite, suiteList, getFocusedItemIndex, sidebarFocusedItem],
96
+ [onSuiteOpen, onSuiteToggle, rootSuite, suiteList, getFocusedItemIndex, sidebarFocusedItem, setSidebarFocusedItem],
105
97
  );
106
98
 
107
99
  useEffect(() => {
@@ -112,9 +104,5 @@ export const KeyboardEvents = ({
112
104
  };
113
105
  }, [handleKeyDown]);
114
106
 
115
- return (
116
- <KeyboardEventsContext.Provider value={{ sidebarFocusedItem, setSidebarFocusedItem }}>
117
- {children}
118
- </KeyboardEventsContext.Provider>
119
- );
107
+ return <>{children}</>;
120
108
  };
package/src/creevey.ts CHANGED
@@ -5,14 +5,14 @@ import { Options } from './types.js';
5
5
  import { emitWorkerMessage } from './server/messages.js';
6
6
  import { isShuttingDown, shutdownWorkers } from './server/utils.js';
7
7
  import Logger from 'loglevel';
8
- import { logger } from './server/logger.js';
8
+ import { logger, setRootName } from './server/logger.js';
9
9
 
10
10
  function shutdownOnException(reason: unknown): void {
11
11
  if (isShuttingDown.current) return;
12
12
 
13
13
  const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
14
14
 
15
- logger.error(error);
15
+ logger().error(error);
16
16
 
17
17
  process.exitCode = -1;
18
18
  if (cluster.isWorker) emitWorkerMessage({ type: 'error', payload: { subtype: 'unknown', error } });
@@ -21,7 +21,11 @@ function shutdownOnException(reason: unknown): void {
21
21
 
22
22
  process.on('uncaughtException', shutdownOnException);
23
23
  process.on('unhandledRejection', shutdownOnException);
24
+ // TODO SIGINT Stuck with selenium
24
25
  process.on('SIGINT', () => {
26
+ if (isShuttingDown.current) {
27
+ process.exit(-1);
28
+ }
25
29
  isShuttingDown.current = true;
26
30
  });
27
31
 
@@ -32,9 +36,12 @@ const argv = minimist<Options>(process.argv.slice(2), {
32
36
  alias: { port: 'p', config: 'c', debug: 'd', update: 'u' },
33
37
  });
34
38
 
39
+ if ('port' in argv && !isNaN(argv.port)) argv.port = Number(argv.port);
40
+ if ('browser' in argv && argv.browser) setRootName(argv.browser);
41
+
35
42
  // eslint-disable-next-line @typescript-eslint/no-deprecated
36
43
  if (cluster.isPrimary && argv.reporter) {
37
- logger.warn(`--reporter option has been removed please describe reporter in config file:
44
+ logger().warn(`--reporter option has been removed please describe reporter in config file:
38
45
  import { reporters } from 'mocha';
39
46
 
40
47
  const config = {
@@ -51,13 +58,13 @@ if (cluster.isPrimary && argv.reporter) {
51
58
  // @ts-expect-error: define log level for storybook
52
59
  global.LOGLEVEL = argv.trace ? 'trace' : argv.debug ? 'debug' : 'warn';
53
60
  if (argv.trace) {
54
- logger.setDefaultLevel(Logger.levels.TRACE);
61
+ logger().setDefaultLevel(Logger.levels.TRACE);
55
62
  Logger.setDefaultLevel(Logger.levels.TRACE);
56
63
  } else if (argv.debug) {
57
- logger.setDefaultLevel(Logger.levels.DEBUG);
64
+ logger().setDefaultLevel(Logger.levels.DEBUG);
58
65
  Logger.setDefaultLevel(Logger.levels.DEBUG);
59
66
  } else {
60
- logger.setDefaultLevel(Logger.levels.INFO);
67
+ logger().setDefaultLevel(Logger.levels.INFO);
61
68
  Logger.setDefaultLevel(Logger.levels.INFO);
62
69
  }
63
70
 
@@ -11,6 +11,7 @@ export const defaultBrowser = 'chrome';
11
11
 
12
12
  export const defaultConfig: Omit<Config, 'gridUrl' | 'testsDir' | 'tsConfig' | 'webdriver'> = {
13
13
  disableTelemetry: false,
14
+ useWorkerQueue: false,
14
15
  useDocker: true,
15
16
  dockerImage: 'aerokube/selenoid:latest-release',
16
17
  dockerImagePlatform: '',
@@ -23,8 +24,8 @@ export const defaultConfig: Omit<Config, 'gridUrl' | 'testsDir' | 'tsConfig' | '
23
24
  storiesProvider: browserStoriesProvider,
24
25
  maxRetries: 0,
25
26
  testTimeout: 30000,
26
- diffOptions: { threshold: 0.05, includeAA: false },
27
- odiffOptions: { threshold: 0.05, antialiasing: true },
27
+ diffOptions: { threshold: 0.1, includeAA: false },
28
+ odiffOptions: { threshold: 0.1, antialiasing: true },
28
29
  browsers: { [defaultBrowser]: true },
29
30
  hooks: {},
30
31
  testsRegex: /\.creevey\.(t|j)s$/,
@@ -73,7 +74,7 @@ export async function readConfig(options: Options): Promise<Config> {
73
74
 
74
75
  if (!configData.webdriver) {
75
76
  const { SeleniumWebdriver } = await import('./selenium/webdriver.js');
76
- logger.warn(
77
+ logger().warn(
77
78
  "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
  );
79
80
  configData.webdriver = SeleniumWebdriver;
@@ -21,7 +21,7 @@ export async function pullImages(
21
21
  if (auth) args.authconfig = auth;
22
22
  if (platform) args.platform = platform;
23
23
 
24
- logger.info('Pull docker images');
24
+ logger().info('Pull docker images');
25
25
  // TODO Replace with `import from`
26
26
  const { default: yoctoSpinner } = await import('yocto-spinner');
27
27
  for (const image of images) {
@@ -61,7 +61,7 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
61
61
  const images = await docker.listImages({ filters: { label: [`creevey=${imageName}`] } });
62
62
 
63
63
  if (images.at(0)) {
64
- logger.info(`Image ${imageName} already exists`);
64
+ logger().info(`Image ${imageName} already exists`);
65
65
  return;
66
66
  }
67
67
 
@@ -1,4 +1,5 @@
1
1
  import cluster from 'cluster';
2
+ import path from 'path';
2
3
  import { readConfig, defaultBrowser } from './config.js';
3
4
  import { Options, Config, BrowserConfigObject, isWorkerMessage } from '../types.js';
4
5
  import { logger } from './logger.js';
@@ -8,11 +9,11 @@ import { isInsideDocker } from './utils.js';
8
9
  import { sendWorkerMessage } from './messages.js';
9
10
  import { playwrightDockerFile } from './playwright/docker-file.js';
10
11
  import { buildImage } from './docker.js';
12
+ import { mkdir, writeFile } from 'fs/promises';
11
13
 
12
14
  async function startWebdriverServer(browser: string, config: Config, options: Options): Promise<string | undefined> {
13
15
  if (config.webdriver === SeleniumWebdriver) {
14
16
  if (cluster.isPrimary) {
15
- // TODO Get random free port
16
17
  const { startSelenoidContainer, startSelenoidStandalone } = await import('./selenium/selenoid.js');
17
18
  const gridUrl = 'http://localhost:4444/wd/hub';
18
19
  if (config.useDocker) {
@@ -24,8 +25,9 @@ async function startWebdriverServer(browser: string, config: Config, options: Op
24
25
  }
25
26
  // TODO Worker might want to use docker image of browser or start standalone selenium
26
27
  } else {
27
- // TODO start standalone playwright server (useDocker == false)
28
+ if (config.gridUrl) return undefined;
28
29
 
30
+ // TODO start standalone playwright server (useDocker == false)
29
31
  const {
30
32
  default: { version },
31
33
  } = await import('playwright-core/package.json', { with: { type: 'json' } });
@@ -33,6 +35,8 @@ async function startWebdriverServer(browser: string, config: Config, options: Op
33
35
  if (cluster.isWorker) {
34
36
  // TODO Re-use dockerImage
35
37
 
38
+ // TODO Use https://hub.docker.com/r/playwright/chrome
39
+ // NOTE It will be possible to use `chrome` browserName
36
40
  const { startPlaywrightContainer } = await import('./playwright/docker.js');
37
41
  const { browserName } = config.browsers[browser] as BrowserConfigObject;
38
42
 
@@ -77,6 +81,10 @@ export default async function (options: Options): Promise<void> {
77
81
  const { browser = defaultBrowser, update, ui, port } = options;
78
82
  let gridUrl = cluster.isPrimary ? config.gridUrl : options.gridUrl;
79
83
 
84
+ // TODO Add package.json with `"type": "commonjs"` as workaround for esm packages to load `data.js`
85
+ await mkdir(config.reportDir, { recursive: true });
86
+ await writeFile(path.join(config.reportDir, 'package.json'), '{"type": "commonjs"}');
87
+
80
88
  // NOTE: We don't need docker nor selenoid for update option
81
89
  if (
82
90
  !(gridUrl || (Object.values(config.browsers) as BrowserConfigObject[]).every(({ gridUrl }) => gridUrl)) &&
@@ -91,14 +99,29 @@ export default async function (options: Options): Promise<void> {
91
99
  return;
92
100
  }
93
101
  case cluster.isPrimary: {
94
- logger.info('Starting Master Process');
102
+ if (config.webdriver === SeleniumWebdriver) {
103
+ try {
104
+ await import('selenium-webdriver');
105
+ } catch {
106
+ logger().error('Failed to start Creevey, missing required dependency: "selenium-webdriver"');
107
+ process.exit(-1);
108
+ }
109
+ } else {
110
+ try {
111
+ await import('playwright-core');
112
+ } catch {
113
+ logger().error('Failed to start Creevey, missing required dependency: "playwright-core"');
114
+ process.exit(-1);
115
+ }
116
+ }
117
+ logger().info('Starting Master Process');
95
118
 
96
119
  const resolveApi = (await import('./master/server.js')).start(config.reportDir, port, ui);
97
120
 
98
121
  return (await import('./master/start.js')).start(gridUrl, config, options, resolveApi);
99
122
  }
100
123
  default: {
101
- logger.info(`Starting Worker for ${browser}`);
124
+ logger().info(`Starting Worker for ${browser}`);
102
125
 
103
126
  // NOTE: We assume that we pass `gridUrl` to worker CLI options
104
127
  // 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;
@@ -32,6 +23,7 @@ export default class Pool extends EventEmitter {
32
23
  return this.workers.length !== this.freeWorkers.length;
33
24
  }
34
25
  constructor(
26
+ public scheduler: WorkerQueue,
35
27
  config: Config,
36
28
  private browser: string,
37
29
  gridUrl?: string,
@@ -46,10 +38,11 @@ export default class Pool extends EventEmitter {
46
38
 
47
39
  async init(): Promise<void> {
48
40
  const poolSize = Math.max(1, this.config.limit ?? 1);
49
- // 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
- );
41
+ this.workers = (
42
+ await Promise.all(
43
+ Array.from({ length: poolSize }).map(() => this.scheduler.forkWorker(this.browser, this.gridUrl)),
44
+ )
45
+ ).filter((workerOrError): workerOrError is Worker => workerOrError instanceof ClusterWorker);
53
46
  if (this.workers.length != poolSize)
54
47
  throw new Error(`Can't instantiate workers for ${this.browser} due many errors`);
55
48
  this.workers.forEach((worker) => {
@@ -66,7 +59,7 @@ export default class Pool extends EventEmitter {
66
59
  return true;
67
60
  }
68
61
 
69
- stop(): void {
62
+ stop() {
70
63
  if (!this.isRunning) {
71
64
  this.emit('stop');
72
65
  return;
@@ -76,7 +69,7 @@ export default class Pool extends EventEmitter {
76
69
  this.queue = [];
77
70
  }
78
71
 
79
- process(): void {
72
+ process() {
80
73
  const worker = this.getFreeWorker();
81
74
  const test = this.queue.at(0);
82
75
 
@@ -99,7 +92,9 @@ export default class Pool extends EventEmitter {
99
92
 
100
93
  sendTestMessage(worker, { type: 'start', payload: test });
101
94
 
102
- this.process();
95
+ setImmediate(() => {
96
+ this.process();
97
+ });
103
98
  }
104
99
 
105
100
  private sendStatus(message: { id: string; status: TestStatus; result?: TestResult }): void {
@@ -120,34 +115,12 @@ export default class Pool extends EventEmitter {
120
115
  return this.aliveWorkers.filter((worker) => !worker.isRunning);
121
116
  }
122
117
 
123
- private async forkWorker(retry = 0): Promise<Worker | { error: string }> {
124
- cluster.setupPrimary({
125
- args: ['--browser', this.browser, ...(this.gridUrl ? ['--gridUrl', this.gridUrl] : []), ...process.argv.slice(2)],
126
- });
127
- const worker = cluster.fork();
128
- const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
129
- const readyHandler = (message: unknown): void => {
130
- if (!isWorkerMessage(message) || message.type == 'port') return;
131
- worker.off('message', readyHandler);
132
- resolve(message);
133
- };
134
- worker.on('message', readyHandler);
135
- });
136
-
137
- if (message.type != 'error') return worker;
138
-
139
- this.gracefullyKill(worker);
140
-
141
- if (retry == FORK_RETRIES) return message.payload;
142
- return this.forkWorker(retry + 1);
143
- }
144
-
145
118
  private exitHandler(worker: Worker): void {
146
119
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
147
120
  worker.once('exit', async () => {
148
121
  if (isShuttingDown.current) return;
149
122
 
150
- const workerOrError = await this.forkWorker();
123
+ const workerOrError = await this.scheduler.forkWorker(this.browser, this.gridUrl);
151
124
 
152
125
  if (!(workerOrError instanceof ClusterWorker))
153
126
  throw new Error(`Can't instantiate worker for ${this.browser} due many errors`);
@@ -158,17 +131,6 @@ export default class Pool extends EventEmitter {
158
131
  });
159
132
  }
160
133
 
161
- private gracefullyKill(worker: Worker): void {
162
- worker.isShuttingDown = true;
163
- const timeout = setTimeout(() => {
164
- worker.kill();
165
- }, 10000);
166
- worker.on('exit', () => {
167
- clearTimeout(timeout);
168
- });
169
- sendShutdownMessage(worker);
170
- }
171
-
172
134
  private shouldRetry(test: WorkerTest): boolean {
173
135
  return test.retries < this.maxRetries && !this.forcedStop;
174
136
  }
@@ -200,7 +162,7 @@ export default class Pool extends EventEmitter {
200
162
  });
201
163
 
202
164
  if (message.payload.subtype == 'unknown') {
203
- this.gracefullyKill(worker);
165
+ gracefullyKill(worker);
204
166
  }
205
167
 
206
168
  this.handleTestResult(worker, test, { status: 'failed', error: message.payload.error });
@@ -0,0 +1,64 @@
1
+ import cluster from 'cluster';
2
+ import { isWorkerMessage, Worker, WorkerMessage } from '../../types.js';
3
+ import { gracefullyKill, isShuttingDown } from '../utils.js';
4
+
5
+ const FORK_RETRIES = 5;
6
+
7
+ type MaybeWorker = Worker | { error: string };
8
+
9
+ export class WorkerQueue {
10
+ private isProcessing = false;
11
+ private queue: { browser: string; gridUrl?: string; retry: number; resolve: (mw: MaybeWorker) => void }[] = [];
12
+
13
+ // TODO Add concurrency
14
+ constructor(private useQueue: boolean) {}
15
+
16
+ async forkWorker(browser: string, gridUrl?: string, retry = 0): Promise<MaybeWorker> {
17
+ return new Promise<MaybeWorker>((resolve) => {
18
+ this.queue.push({ browser, gridUrl, retry, resolve });
19
+
20
+ void this.process();
21
+ });
22
+ }
23
+
24
+ private async process() {
25
+ if (this.useQueue && this.isProcessing) return;
26
+
27
+ const { browser, gridUrl, retry, resolve } = this.queue.pop() ?? {};
28
+
29
+ if (browser == undefined || retry == undefined || resolve == undefined) return;
30
+
31
+ if (isShuttingDown.current) {
32
+ resolve({ error: 'Master process is shutting down' });
33
+ return;
34
+ }
35
+
36
+ this.isProcessing = true;
37
+
38
+ cluster.setupPrimary({
39
+ args: ['--browser', browser, ...(gridUrl ? ['--gridUrl', gridUrl] : []), ...process.argv.slice(2)],
40
+ });
41
+ const worker = cluster.fork();
42
+ const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
43
+ const readyHandler = (message: unknown): void => {
44
+ if (!isWorkerMessage(message) || message.type == 'port') return;
45
+ worker.off('message', readyHandler);
46
+ resolve(message);
47
+ };
48
+ worker.on('message', readyHandler);
49
+ });
50
+
51
+ if (message.type == 'error') {
52
+ gracefullyKill(worker);
53
+
54
+ if (retry == FORK_RETRIES) resolve(message.payload);
55
+ else this.queue.push({ browser, gridUrl, retry: retry + 1, resolve });
56
+ } else {
57
+ resolve(worker);
58
+ }
59
+
60
+ this.isProcessing = false;
61
+
62
+ setImmediate(() => void this.process());
63
+ }
64
+ }
@@ -13,12 +13,14 @@ import {
13
13
  TestMeta,
14
14
  } from '../../types.js';
15
15
  import Pool from './pool.js';
16
+ import { WorkerQueue } from './queue.js';
16
17
 
17
18
  export default class Runner extends EventEmitter {
18
19
  private failFast: boolean;
19
20
  private screenDir: string;
20
21
  private reportDir: string;
21
22
  private browsers: string[];
23
+ private scheduler: WorkerQueue;
22
24
  private pools: Record<string, Pool> = {};
23
25
  tests: Partial<Record<string, ServerTest>> = {};
24
26
  public get isRunning(): boolean {
@@ -30,9 +32,10 @@ export default class Runner extends EventEmitter {
30
32
  this.failFast = config.failFast;
31
33
  this.screenDir = config.screenDir;
32
34
  this.reportDir = config.reportDir;
35
+ this.scheduler = new WorkerQueue(config.useWorkerQueue);
33
36
  this.browsers = Object.keys(config.browsers);
34
37
  this.browsers
35
- .map((browser) => (this.pools[browser] = new Pool(config, browser, gridUrl)))
38
+ .map((browser) => (this.pools[browser] = new Pool(this.scheduler, config, browser, gridUrl)))
36
39
  .map((pool) => pool.on('test', this.handlePoolMessage));
37
40
  }
38
41
 
@@ -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);
@@ -42,7 +42,10 @@ function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
42
42
  .map((imagePath) => path.posix.relative(imagesDir, imagePath))
43
43
  .filter((imagePath) => !images.has(imagePath));
44
44
  if (unnecessaryImages.length > 0) {
45
- logger.warn('We found unnecessary screenshot images, those can be safely removed:\n', unnecessaryImages.join('\n'));
45
+ logger().warn(
46
+ 'We found unnecessary screenshot images, those can be safely removed:\n',
47
+ unnecessaryImages.join('\n'),
48
+ );
46
49
  }
47
50
  }
48
51
 
@@ -81,10 +84,10 @@ export async function start(
81
84
 
82
85
  if (options.ui) {
83
86
  resolveApi(creeveyApi(runner));
84
- logger.info(`Started on http://localhost:${options.port}`);
87
+ logger().info(`Started on http://localhost:${options.port}`);
85
88
  } else {
86
89
  if (Object.values(runner.status.tests).filter((test) => test && !test.skip).length == 0) {
87
- logger.warn("Don't have any tests to run");
90
+ logger().warn("Don't have any tests to run");
88
91
 
89
92
  void shutdownWorkers().then(() => process.exit());
90
93
  return;
@@ -101,7 +104,7 @@ export async function start(
101
104
  void sendScreenshotsCount(config, options, runner.status)
102
105
  .catch((reason: unknown) => {
103
106
  const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
104
- logger.warn(`Can't send telemetry: ${error}`);
107
+ logger().warn(`Can't send telemetry: ${error}`);
105
108
  })
106
109
  .finally(() => {
107
110
  void shutdownWorkers().then(() => process.exit());