creevey 0.9.1 → 0.9.3

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 (60) hide show
  1. package/dist/creevey.js +8 -4
  2. package/dist/creevey.js.map +1 -1
  3. package/dist/server/config.js +1 -0
  4. package/dist/server/config.js.map +1 -1
  5. package/dist/server/docker.js +1 -1
  6. package/dist/server/docker.js.map +1 -1
  7. package/dist/server/index.js +2 -2
  8. package/dist/server/index.js.map +1 -1
  9. package/dist/server/logger.d.ts +2 -1
  10. package/dist/server/logger.js +7 -3
  11. package/dist/server/logger.js.map +1 -1
  12. package/dist/server/master/api.js +1 -1
  13. package/dist/server/master/api.js.map +1 -1
  14. package/dist/server/master/index.js +4 -4
  15. package/dist/server/master/index.js.map +1 -1
  16. package/dist/server/master/pool.d.ts +3 -3
  17. package/dist/server/master/pool.js +13 -64
  18. package/dist/server/master/pool.js.map +1 -1
  19. package/dist/server/master/queue.d.ts +13 -0
  20. package/dist/server/master/queue.js +60 -0
  21. package/dist/server/master/queue.js.map +1 -0
  22. package/dist/server/master/runner.d.ts +1 -0
  23. package/dist/server/master/runner.js +6 -1
  24. package/dist/server/master/runner.js.map +1 -1
  25. package/dist/server/master/server.js +1 -1
  26. package/dist/server/master/server.js.map +1 -1
  27. package/dist/server/selenium/browser.js +116 -90
  28. package/dist/server/selenium/browser.js.map +1 -1
  29. package/dist/server/storybook/providers/browser.js +1 -1
  30. package/dist/server/storybook/providers/browser.js.map +1 -1
  31. package/dist/server/storybook/providers/hybrid.js +1 -1
  32. package/dist/server/storybook/providers/hybrid.js.map +1 -1
  33. package/dist/server/utils.d.ts +2 -1
  34. package/dist/server/utils.js +11 -0
  35. package/dist/server/utils.js.map +1 -1
  36. package/dist/server/worker/reporter.js +2 -2
  37. package/dist/server/worker/reporter.js.map +1 -1
  38. package/dist/server/worker/worker.js +22 -15
  39. package/dist/server/worker/worker.js.map +1 -1
  40. package/dist/types.d.ts +6 -0
  41. package/dist/types.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/creevey.ts +8 -5
  44. package/src/server/config.ts +1 -0
  45. package/src/server/docker.ts +1 -1
  46. package/src/server/index.ts +2 -2
  47. package/src/server/logger.ts +6 -2
  48. package/src/server/master/api.ts +1 -1
  49. package/src/server/master/index.ts +7 -4
  50. package/src/server/master/pool.ts +20 -49
  51. package/src/server/master/queue.ts +59 -0
  52. package/src/server/master/runner.ts +6 -1
  53. package/src/server/master/server.ts +1 -1
  54. package/src/server/selenium/browser.ts +129 -97
  55. package/src/server/storybook/providers/browser.ts +1 -1
  56. package/src/server/storybook/providers/hybrid.ts +1 -1
  57. package/src/server/utils.ts +12 -1
  58. package/src/server/worker/reporter.ts +2 -2
  59. package/src/server/worker/worker.ts +21 -15
  60. package/src/types.ts +6 -0
@@ -29,6 +29,7 @@ import {
29
29
  import { colors, logger } from '../logger.js';
30
30
  import { emitStoriesMessage, subscribeOn } from '../messages.js';
31
31
  import { isShuttingDown, LOCALHOST_REGEXP, runSequence } from '../utils.js';
32
+ import { Preferences } from 'selenium-webdriver/lib/logging.js';
32
33
 
33
34
  interface ElementRect {
34
35
  top: number;
@@ -50,7 +51,6 @@ declare global {
50
51
 
51
52
  const storybookRootID = 'storybook-root';
52
53
  const DOCKER_INTERNAL = 'host.docker.internal';
53
- let browserLogger = logger;
54
54
  let browserName = '';
55
55
  let browser: WebDriver | null = null;
56
56
  // let context: UnPromise<ReturnType<typeof BrowsingContext>> | null = null;
@@ -99,19 +99,17 @@ function getAddresses(): string[] {
99
99
  }
100
100
 
101
101
  async function resolveStorybookUrl(storybookUrl: string, checkUrl: (url: string) => Promise<boolean>): Promise<string> {
102
- browserLogger.debug('Resolving storybook url');
102
+ logger().debug('Resolving storybook url');
103
103
  const addresses = getAddresses();
104
104
  for (const ip of addresses) {
105
105
  const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
106
- browserLogger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
106
+ logger().debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
107
107
  if (await checkUrl(resolvedUrl)) {
108
- browserLogger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
108
+ logger().debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
109
109
  return resolvedUrl;
110
110
  }
111
111
  }
112
- const error = new Error('Please specify `storybookUrl` with IP address that accessible from remote browser');
113
- error.name = 'ResolveUrlError';
114
- throw error;
112
+ throw new Error('Please specify `storybookUrl` with IP address that accessible from remote browser');
115
113
  }
116
114
 
117
115
  async function openUrlAndWaitForPageSource(
@@ -135,9 +133,9 @@ function getUrlChecker(browser: WebDriver): (url: string) => Promise<boolean> {
135
133
  return async (url: string): Promise<boolean> => {
136
134
  try {
137
135
  // NOTE: Before trying a new url, reset the current one
138
- browserLogger.debug(`Opening ${chalk.magenta('about:blank')} page`);
136
+ logger().debug(`Opening ${chalk.magenta('about:blank')} page`);
139
137
  await openUrlAndWaitForPageSource(browser, 'about:blank', (source: string) => !source.includes('<body></body>'));
140
- browserLogger.debug(`Opening ${chalk.magenta(url)} and checking the page source`);
138
+ logger().debug(`Opening ${chalk.magenta(url)} and checking the page source`);
141
139
  const source = await openUrlAndWaitForPageSource(
142
140
  browser,
143
141
  url,
@@ -149,7 +147,7 @@ function getUrlChecker(browser: WebDriver): (url: string) => Promise<boolean> {
149
147
  // because other add significant delay and some of them don't work in earlier chrome versions
150
148
  // Browsers always load page successful even it's failed
151
149
  // So we just check `root` element
152
- browserLogger.debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
150
+ logger().debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
153
151
  return source.includes(`id="${storybookRootID}"`);
154
152
  } catch {
155
153
  return false;
@@ -157,8 +155,70 @@ function getUrlChecker(browser: WebDriver): (url: string) => Promise<boolean> {
157
155
  };
158
156
  }
159
157
 
158
+ async function buildWebdriver(
159
+ gridUrl: string,
160
+ capabilities: Capabilities,
161
+ prefs: Preferences,
162
+ ): Promise<readonly [string, WebDriver]> {
163
+ const maxRetries = 5;
164
+ let maybeResult = null;
165
+ let retries = 0;
166
+ do {
167
+ maybeResult = await Promise.race([
168
+ new Promise<null>((resolve) => {
169
+ setTimeout(() => {
170
+ retries += 1;
171
+ resolve(null);
172
+ }, 120_000);
173
+ }),
174
+ (async () => {
175
+ if (retries > 0) {
176
+ logger().debug(`Trying to initialize session to Selenium Grid: retried ${retries} of ${maxRetries}`);
177
+ }
178
+ const retry = retries;
179
+ // const ie = new IeOptions();
180
+ // const edge = new EdgeOptions();
181
+ // const chrome = new ChromeOptions();
182
+ // const safari = new SafariOptions();
183
+ // const firefox = new FirefoxOptions();
184
+ // edge.enableBidi();
185
+ // chrome.enableBidi();
186
+ // firefox.enableBidi();
187
+
188
+ const browser = await new Builder()
189
+ // .setIeOptions(ie)
190
+ // .setEdgeOptions(edge)
191
+ // .setChromeOptions(chrome)
192
+ // .setSafariOptions(safari)
193
+ // .setFirefoxOptions(firefox)
194
+ .usingServer(gridUrl)
195
+ .withCapabilities(capabilities)
196
+ .setLoggingPrefs(prefs) // NOTE: Should go last
197
+ .build();
198
+
199
+ // const id = await browser.getWindowHandle();
200
+ // context = await BrowsingContext(browser, { browsingContextId: id });
201
+
202
+ const sessionId = (await browser.getSession()).getId();
203
+
204
+ if (retry != retries) {
205
+ void browser.quit();
206
+ return null;
207
+ }
208
+
209
+ return [sessionId, browser] as const;
210
+ })(),
211
+ ]);
212
+ if (maybeResult) break;
213
+ } while (retries < maxRetries);
214
+
215
+ if (!maybeResult) throw new Error('Failed to initialize session to Selenium Grid due to many retries');
216
+
217
+ return maybeResult;
218
+ }
219
+
160
220
  async function waitForStorybook(browser: WebDriver): Promise<void> {
161
- browserLogger.debug('Waiting for `setStories` event to make sure that storybook is initiated');
221
+ logger().debug('Waiting for `setStories` event to make sure that storybook is initiated');
162
222
 
163
223
  const isTimeout = await Promise.race([
164
224
  new Promise<boolean>((resolve) => {
@@ -176,7 +236,7 @@ async function waitForStorybook(browser: WebDriver): Promise<void> {
176
236
  return false;
177
237
  }, SET_GLOBALS);
178
238
  } catch (e: unknown) {
179
- browserLogger.debug('An error has been caught during the script:', e);
239
+ logger().debug('An error has been caught during the script:', e);
180
240
  }
181
241
  } while (wait);
182
242
  return false;
@@ -188,7 +248,7 @@ async function waitForStorybook(browser: WebDriver): Promise<void> {
188
248
  }
189
249
 
190
250
  async function resetMousePosition(browser: WebDriver): Promise<void> {
191
- browserLogger.debug('Resetting mouse position to the top-left corner');
251
+ logger().debug('Resetting mouse position to the top-left corner');
192
252
  const browserName = (await browser.getCapabilities()).getBrowserName();
193
253
  const [browserVersion] =
194
254
  (await browser.getCapabilities()).getBrowserVersion()?.split('.') ??
@@ -236,7 +296,7 @@ async function resizeViewport(browser: WebDriver, viewport: { width: number; hei
236
296
  },
237
297
  );
238
298
 
239
- browserLogger.debug(`Resizing viewport from ${innerWidth}x${innerHeight} to ${viewport.width}x${viewport.height}`);
299
+ logger().debug(`Resizing viewport from ${innerWidth}x${innerHeight} to ${viewport.width}x${viewport.height}`);
240
300
 
241
301
  const dWidth = windowRect.width - innerWidth;
242
302
  const dHeight = windowRect.height - innerHeight;
@@ -367,7 +427,7 @@ export async function takeScreenshot(
367
427
 
368
428
  const ignoreStyles = await insertIgnoreStyles(browser, ignoreElements);
369
429
 
370
- if (browserLogger.getLevel() <= Logger.levels.DEBUG) {
430
+ if (logger().getLevel() <= Logger.levels.DEBUG) {
371
431
  const { innerWidth, innerHeight } = await browser.executeScript<{ innerWidth: number; innerHeight: number }>(
372
432
  function () {
373
433
  return {
@@ -376,16 +436,16 @@ export async function takeScreenshot(
376
436
  };
377
437
  },
378
438
  );
379
- browserLogger.debug(`Viewport size is: ${innerWidth}x${innerHeight}`);
439
+ logger().debug(`Viewport size is: ${innerWidth}x${innerHeight}`);
380
440
  }
381
441
 
382
442
  try {
383
443
  if (!captureElement) {
384
- browserLogger.debug('Capturing viewport screenshot');
444
+ logger().debug('Capturing viewport screenshot');
385
445
  screenshot = await browser.takeScreenshot();
386
- browserLogger.debug('Viewport screenshot is captured');
446
+ logger().debug('Viewport screenshot is captured');
387
447
  } else {
388
- browserLogger.debug(`Checking is element ${chalk.cyan(captureElement)} fit into viewport`);
448
+ logger().debug(`Checking is element ${chalk.cyan(captureElement)} fit into viewport`);
389
449
  const rects = await browser.executeScript<{ elementRect: ElementRect; windowRect: ElementRect } | undefined>(
390
450
  function (selector: string): { elementRect: ElementRect; windowRect: ElementRect } | undefined {
391
451
  window.scrollTo(0, 0); // TODO Maybe we should remove same code from `resetMousePosition`
@@ -425,11 +485,9 @@ export async function takeScreenshot(
425
485
  elementRect.height + elementRect.top <= windowRect.height;
426
486
 
427
487
  if (isFitIntoViewport) {
428
- browserLogger.debug(
429
- `Capturing ${chalk.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`,
430
- );
488
+ logger().debug(`Capturing ${chalk.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`);
431
489
  } else
432
- browserLogger.debug(
490
+ logger().debug(
433
491
  `Capturing composite screenshot image of ${chalk.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`,
434
492
  );
435
493
 
@@ -445,7 +503,7 @@ export async function takeScreenshot(
445
503
  : // TODO pointer-events: none, need to research
446
504
  await takeCompositeScreenshot(browser, windowRect, elementRect);
447
505
 
448
- browserLogger.debug(`${chalk.cyan(captureElement)} is captured`);
506
+ logger().debug(`${chalk.cyan(captureElement)} is captured`);
449
507
  }
450
508
  } finally {
451
509
  await removeIgnoreStyles(browser, ignoreStyles);
@@ -455,7 +513,7 @@ export async function takeScreenshot(
455
513
  }
456
514
 
457
515
  async function selectStory(browser: WebDriver, storyId: string, waitForReady = false): Promise<boolean> {
458
- browserLogger.debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(storyId)}`);
516
+ logger().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(storyId)}`);
459
517
 
460
518
  const result = await browser.executeAsyncScript<[error?: string | null, isCaptureCalled?: boolean] | null>(
461
519
  function (
@@ -483,7 +541,7 @@ async function selectStory(browser: WebDriver, storyId: string, waitForReady = f
483
541
  }
484
542
 
485
543
  export async function updateStorybookGlobals(browser: WebDriver, globals: StorybookGlobals): Promise<void> {
486
- browserLogger.debug('Applying storybook globals');
544
+ logger().debug('Applying storybook globals');
487
545
  await browser.executeScript(function (globals: StorybookGlobals) {
488
546
  window.__CREEVEY_UPDATE_GLOBALS__(globals);
489
547
  }, globals);
@@ -504,11 +562,11 @@ async function openStorybookPage(
504
562
 
505
563
  try {
506
564
  if (resolver) {
507
- browserLogger.debug('Resolving storybook url with custom resolver');
565
+ logger().debug('Resolving storybook url with custom resolver');
508
566
 
509
567
  const resolvedUrl = await resolver();
510
568
 
511
- browserLogger.debug(`Resolver storybook url ${resolvedUrl}`);
569
+ logger().debug(`Resolver storybook url ${resolvedUrl}`);
512
570
 
513
571
  await browser.get(appendIframePath(resolvedUrl));
514
572
  } else {
@@ -516,7 +574,7 @@ async function openStorybookPage(
516
574
  await resolveStorybookUrl(appendIframePath(storybookUrl), getUrlChecker(browser));
517
575
  }
518
576
  } catch (error) {
519
- browserLogger.error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
577
+ logger().error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
520
578
  throw error;
521
579
  }
522
580
  }
@@ -598,73 +656,53 @@ export async function getBrowser(config: Config, options: Options & { browser: s
598
656
  browser = null;
599
657
  });
600
658
 
601
- try {
602
- const url = new URL(gridUrl);
603
- url.username = url.username ? '********' : '';
604
- url.password = url.password ? '********' : '';
605
- browserLogger.debug(`(${browserName}) Connecting to Selenium ${chalk.magenta(url.toString())}`);
659
+ const url = new URL(gridUrl);
660
+ url.username = url.username ? '********' : '';
661
+ url.password = url.password ? '********' : '';
662
+ logger().debug(`(${browserName}) Connecting to Selenium ${chalk.magenta(url.toString())}`);
606
663
 
607
- const prefs = new logging.Preferences();
664
+ const prefs = new logging.Preferences();
608
665
 
609
- if (options.trace) {
610
- for (const type of Object.values(logging.Type)) {
611
- prefs.setLevel(type as string, logging.Level.ALL);
612
- }
666
+ if (options.trace) {
667
+ for (const type of Object.values(logging.Type)) {
668
+ prefs.setLevel(type as string, logging.Level.ALL);
613
669
  }
670
+ }
614
671
 
615
- // const ie = new IeOptions();
616
- // const edge = new EdgeOptions();
617
- // const chrome = new ChromeOptions();
618
- // const safari = new SafariOptions();
619
- // const firefox = new FirefoxOptions();
620
- // edge.enableBidi();
621
- // chrome.enableBidi();
622
- // firefox.enableBidi();
623
-
624
- browser = await new Builder()
625
- // .setIeOptions(ie)
626
- // .setEdgeOptions(edge)
627
- // .setChromeOptions(chrome)
628
- // .setSafariOptions(safari)
629
- // .setFirefoxOptions(firefox)
630
- .usingServer(gridUrl)
631
- .withCapabilities(capabilities)
632
- .setLoggingPrefs(prefs) // NOTE: Should go last
633
- .build();
634
-
635
- // const id = await browser.getWindowHandle();
636
- // context = await BrowsingContext(browser, { browsingContextId: id });
637
-
638
- const sessionId = (await browser.getSession()).getId();
639
- let browserHost = '';
672
+ let sessionId;
673
+ let browserHost = '';
640
674
 
641
- try {
642
- const { Name } = await getSessionData(gridUrl, sessionId);
643
- if (typeof Name == 'string') browserHost = Name;
644
- } catch {
645
- /* noop */
646
- }
675
+ [sessionId, browser] = await buildWebdriver(gridUrl, capabilities, prefs);
647
676
 
648
- browserLogger.debug(
649
- `(${browserName}) Connected successful with ${[chalk.green(browserHost), chalk.magenta(sessionId)]
650
- .filter(Boolean)
651
- .join(':')}`,
652
- );
677
+ try {
678
+ const { Name } = await getSessionData(gridUrl, sessionId);
679
+ if (typeof Name == 'string') browserHost = Name;
680
+ } catch {
681
+ /* noop */
682
+ }
653
683
 
654
- browserLogger = Logger.getLogger(sessionId);
684
+ logger().debug(
685
+ `(${browserName}) Connected successful with ${[chalk.green(browserHost), chalk.magenta(sessionId)]
686
+ .filter(Boolean)
687
+ .join(':')}`,
688
+ );
655
689
 
656
- prefix.apply(browserLogger, {
657
- format(level) {
658
- const levelColor = colors[level.toUpperCase() as keyof typeof colors];
659
- return `[${browserName}:${chalk.gray(sessionId)}] ${levelColor(level)} =>`;
660
- },
661
- });
690
+ prefix.apply(logger(), {
691
+ format(level) {
692
+ const levelColor = colors[level.toUpperCase() as keyof typeof colors];
693
+ return `[${browserName}:${chalk.gray(process.pid)}] ${levelColor(level)} => ${chalk.gray(sessionId)}`;
694
+ },
695
+ });
662
696
 
697
+ try {
663
698
  await runSequence(
664
699
  [
665
700
  () => browser?.manage().setTimeouts({ pageLoad: 10000, script: 60000 }),
666
701
  () => browser && openStorybookPage(browser, realAddress, config.resolveStorybookUrl),
667
702
  () => browser && waitForStorybook(browser),
703
+ () => browser && resolveCreeveyHost(browser, options.port),
704
+ () => browser && updateBrowserGlobalVariables(browser),
705
+ () => _storybookGlobals && browser && updateStorybookGlobals(browser, _storybookGlobals),
668
706
  // NOTE: Selenium draws automation toolbar with some delay after webdriver initialization
669
707
  // NOTE: So if we resize window right after getting webdriver instance we might get situation
670
708
  // NOTE: When the toolbar appears after resize and final viewport size become smaller than we set
@@ -674,24 +712,18 @@ export async function getBrowser(config: Config, options: Options & { browser: s
674
712
  );
675
713
  } catch (originalError) {
676
714
  if (isShuttingDown.current) {
677
- browser?.quit().catch(noop);
715
+ browser.quit().catch(noop);
678
716
  browser = null;
679
717
  return null;
680
718
  }
681
- if (originalError instanceof Error && originalError.name == 'ResolveUrlError') throw originalError;
682
- const error = new Error(`Can't load storybook root page by URL ${(await browser?.getCurrentUrl()) ?? realAddress}`);
719
+ const currentUrl = await browser.getCurrentUrl();
720
+ const error = new Error(
721
+ `Can't load storybook root page${currentUrl ? ` by URL ${currentUrl}` : ''}: ${originalError instanceof Error ? originalError.message : ((originalError ?? 'Unknown error') as string)}`,
722
+ );
683
723
  if (originalError instanceof Error) error.stack = originalError.stack;
684
724
  throw error;
685
725
  }
686
726
 
687
- if (_storybookGlobals) {
688
- await updateStorybookGlobals(browser, _storybookGlobals);
689
- }
690
-
691
- await resolveCreeveyHost(browser, options.port);
692
-
693
- await updateBrowserGlobalVariables(browser);
694
-
695
727
  return browser;
696
728
  }
697
729
 
@@ -762,7 +794,7 @@ export async function switchStory(this: Context): Promise<void> {
762
794
  ignoreElements,
763
795
  } = (parameters.creevey ?? {}) as CreeveyStoryParams;
764
796
 
765
- browserLogger.debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
797
+ logger().debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
766
798
 
767
799
  if (captureElement)
768
800
  Object.defineProperty(this, 'captureElement', {
@@ -813,7 +845,7 @@ export async function switchStory(this: Context): Promise<void> {
813
845
 
814
846
  unsubscribe();
815
847
 
816
- browserLogger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
848
+ logger().debug(`Story ${chalk.magenta(id)} ready for capturing`);
817
849
  }
818
850
 
819
851
  async function insertIgnoreStyles(
@@ -823,7 +855,7 @@ async function insertIgnoreStyles(
823
855
  const ignoreSelectors = Array.prototype.concat(ignoreElements).filter(Boolean);
824
856
  if (!ignoreSelectors.length) return null;
825
857
 
826
- browserLogger.debug('Hiding ignored elements before capturing');
858
+ logger().debug('Hiding ignored elements before capturing');
827
859
 
828
860
  return await browser.executeScript(function (ignoreSelectors: string[]) {
829
861
  return window.__CREEVEY_INSERT_IGNORE_STYLES__(ignoreSelectors);
@@ -832,7 +864,7 @@ async function insertIgnoreStyles(
832
864
 
833
865
  async function removeIgnoreStyles(browser: WebDriver, ignoreStyles: WebElement | null): Promise<void> {
834
866
  if (ignoreStyles) {
835
- browserLogger.debug('Revert hiding ignored elements');
867
+ logger().debug('Revert hiding ignored elements');
836
868
  await browser.executeScript(function (ignoreStyles: HTMLStyleElement) {
837
869
  window.__CREEVEY_REMOVE_IGNORE_STYLES__(ignoreStyles);
838
870
  }, ignoreStyles);
@@ -18,7 +18,7 @@ export const loadStories: StoriesProvider = async (_config, _options, storiesLis
18
18
  if (message.type == 'set') {
19
19
  const { stories, oldTests } = message.payload;
20
20
  if (oldTests.length > 0)
21
- logger.warn(
21
+ logger().warn(
22
22
  `If you use browser stories provider of CSFv3 Storybook feature\n` +
23
23
  `Creevey will not load tests defined in story parameters from following stories:\n` +
24
24
  oldTests.join('\n'),
@@ -54,7 +54,7 @@ async function parseParams(
54
54
 
55
55
  if (listener) {
56
56
  chokidar.watch(testFiles).on('change', (filePath) => {
57
- logger.debug(`changed: ${filePath}`);
57
+ logger().debug(`changed: ${filePath}`);
58
58
 
59
59
  // doesn't work, always returns {} due modules caching
60
60
  // see https://github.com/nodejs/modules/issues/307
@@ -7,7 +7,7 @@ import { createRequire } from 'module';
7
7
  import findCacheDir from 'find-cache-dir';
8
8
  import { register as esmRegister } from 'tsx/esm/api';
9
9
  import { register as cjsRegister } from 'tsx/cjs/api';
10
- import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest } from '../types.js';
10
+ import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest, Worker } from '../types.js';
11
11
  import { emitShutdownMessage, sendShutdownMessage } from './messages.js';
12
12
 
13
13
  const importMetaUrl = pathToFileURL(__filename).href;
@@ -99,6 +99,17 @@ export async function shutdownWorkers(): Promise<void> {
99
99
  emitShutdownMessage();
100
100
  }
101
101
 
102
+ export function gracefullyKill(worker: Worker): void {
103
+ worker.isShuttingDown = true;
104
+ const timeout = setTimeout(() => {
105
+ worker.kill();
106
+ }, 10000);
107
+ worker.on('exit', () => {
108
+ clearTimeout(timeout);
109
+ });
110
+ sendShutdownMessage(worker);
111
+ }
112
+
102
113
  export function shutdown(): void {
103
114
  process.exit();
104
115
  }
@@ -24,11 +24,11 @@ export class CreeveyReporter extends reporters.Base {
24
24
  super(runner);
25
25
 
26
26
  const { sessionId, topLevelSuite } = options.reporterOptions as ReporterOptions;
27
- const testLogger = Logger.getLogger(topLevelSuite);
27
+ const testLogger = Logger.getLogger(sessionId);
28
28
 
29
29
  prefix.apply(testLogger, {
30
30
  format(level) {
31
- return `${testLevels[level]} => (${topLevelSuite}:${chalk.gray(sessionId)})`;
31
+ return `[${topLevelSuite}:${chalk.gray(process.pid)}] ${testLevels[level]} => ${chalk.gray(sessionId)}`;
32
32
  },
33
33
  });
34
34
 
@@ -9,7 +9,7 @@ import { Key, until } from 'selenium-webdriver';
9
9
  import { Config, Images, Options, TestMessage, isImageError } from '../../types.js';
10
10
  import { subscribeOn, emitTestMessage, emitWorkerMessage } from '../messages.js';
11
11
  import chaiImage from './chai-image.js';
12
- import { closeBrowser, getBrowser, switchStory } from '../selenium/index.js';
12
+ import { getBrowser, switchStory } from '../selenium/index.js';
13
13
  import { CreeveyReporter, TeamcityReporter } from './reporter.js';
14
14
  import { addTestsFromStories } from './helpers.js';
15
15
  import { logger } from '../logger.js';
@@ -136,7 +136,21 @@ export async function start(config: Config, options: Options & { browser: string
136
136
 
137
137
  chai.use(chaiImage(getExpected, config.diffOptions));
138
138
 
139
- if ((await getBrowser(config, options)) == null) return;
139
+ const browser = await (async () => {
140
+ try {
141
+ return await getBrowser(config, options);
142
+ } catch (error) {
143
+ const errorMessage = error instanceof Error ? error.message : ((error ?? 'Unknown error') as string);
144
+ logger().error('Failed to initiate webdriver:', errorMessage);
145
+ emitWorkerMessage({
146
+ type: 'error',
147
+ payload: { error: errorMessage },
148
+ });
149
+ return null;
150
+ }
151
+ })();
152
+
153
+ if (browser == null) return;
140
154
 
141
155
  await addTestsFromStories(mocha.suite, config, {
142
156
  browser: options.browser,
@@ -145,20 +159,13 @@ export async function start(config: Config, options: Options & { browser: string
145
159
  port: options.port,
146
160
  });
147
161
 
148
- try {
149
- await (await getBrowser(config, options))?.getCurrentUrl();
150
- } catch {
151
- await closeBrowser();
152
- }
153
- const browser = await getBrowser(config, options);
154
- const sessionId = (await browser?.getSession())?.getId();
155
-
156
- if (browser == null) return;
162
+ const sessionId = (await browser.getSession()).getId();
157
163
 
158
164
  const interval = setInterval(
159
165
  () =>
166
+ // NOTE Simple way to keep session alive
160
167
  void browser.getCurrentUrl().then((url) => {
161
- logger.debug(`${options.browser}:${chalk.gray(sessionId)}`, 'current url', chalk.magenta(url));
168
+ logger().debug('current url', chalk.magenta(url));
162
169
  }),
163
170
  10 * 1000,
164
171
  );
@@ -186,9 +193,8 @@ export async function start(config: Config, options: Options & { browser: string
186
193
  const logs = await browser.manage().logs().get(type);
187
194
  output.push(logs.map((log) => JSON.stringify(log.toJSON(), null, 2)).join('\n'));
188
195
  }
189
- logger.debug(
196
+ logger().debug(
190
197
  '----------',
191
- sessionId,
192
198
  this.currentTest?.titlePath().join('/'),
193
199
  '----------\n',
194
200
  output.join('\n'),
@@ -230,7 +236,7 @@ export async function start(config: Config, options: Options & { browser: string
230
236
  });
231
237
  });
232
238
 
233
- logger.info(`${options.browser}:${chalk.gray(sessionId)} is ready`);
239
+ logger().info('Worker is ready');
234
240
 
235
241
  emitWorkerMessage({ type: 'ready' });
236
242
  }
package/src/types.ts CHANGED
@@ -234,6 +234,11 @@ export interface Config {
234
234
  * The `--ui` CLI option ignores this option
235
235
  */
236
236
  failFast: boolean;
237
+ /**
238
+ * Start workers in sequential queue
239
+ * @default false
240
+ */
241
+ useWorkerQueue: boolean;
237
242
  /**
238
243
  * Specify platform for docker images
239
244
  */
@@ -314,6 +319,7 @@ export type ShutdownHandler = (message: ShutdownMessage) => void;
314
319
 
315
320
  export interface Worker extends ClusterWorker {
316
321
  isRunning?: boolean;
322
+ isShuttingDown?: boolean;
317
323
  }
318
324
 
319
325
  export interface Images {