creevey 0.10.0-beta.5 → 0.10.0-beta.7

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 (85) hide show
  1. package/dist/client/web/CreeveyView/SideBar/SideBar.js +1 -0
  2. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  3. package/dist/client/web/assets/{index-DkmZfG9C.js → index-DB8lHlJw.js} +2 -2
  4. package/dist/client/web/index.html +1 -1
  5. package/dist/creevey.js +12 -5
  6. package/dist/creevey.js.map +1 -1
  7. package/dist/server/config.js +2 -1
  8. package/dist/server/config.js.map +1 -1
  9. package/dist/server/docker.js +2 -2
  10. package/dist/server/docker.js.map +1 -1
  11. package/dist/server/index.js +4 -4
  12. package/dist/server/index.js.map +1 -1
  13. package/dist/server/logger.d.ts +2 -1
  14. package/dist/server/logger.js +7 -3
  15. package/dist/server/logger.js.map +1 -1
  16. package/dist/server/master/api.js +1 -1
  17. package/dist/server/master/api.js.map +1 -1
  18. package/dist/server/master/pool.d.ts +3 -3
  19. package/dist/server/master/pool.js +10 -65
  20. package/dist/server/master/pool.js.map +1 -1
  21. package/dist/server/master/queue.d.ts +13 -0
  22. package/dist/server/master/queue.js +64 -0
  23. package/dist/server/master/queue.js.map +1 -0
  24. package/dist/server/master/runner.d.ts +1 -0
  25. package/dist/server/master/runner.js +4 -1
  26. package/dist/server/master/runner.js.map +1 -1
  27. package/dist/server/master/server.js +1 -1
  28. package/dist/server/master/server.js.map +1 -1
  29. package/dist/server/master/start.js +4 -4
  30. package/dist/server/master/start.js.map +1 -1
  31. package/dist/server/playwright/internal.js +21 -20
  32. package/dist/server/playwright/internal.js.map +1 -1
  33. package/dist/server/playwright/webdriver.js +1 -1
  34. package/dist/server/playwright/webdriver.js.map +1 -1
  35. package/dist/server/providers/browser.js +2 -1
  36. package/dist/server/providers/browser.js.map +1 -1
  37. package/dist/server/providers/hybrid.js +1 -1
  38. package/dist/server/providers/hybrid.js.map +1 -1
  39. package/dist/server/reporter.js +4 -4
  40. package/dist/server/reporter.js.map +1 -1
  41. package/dist/server/selenium/internal.d.ts +1 -2
  42. package/dist/server/selenium/internal.js +95 -75
  43. package/dist/server/selenium/internal.js.map +1 -1
  44. package/dist/server/selenium/webdriver.js +1 -1
  45. package/dist/server/selenium/webdriver.js.map +1 -1
  46. package/dist/server/utils.d.ts +2 -1
  47. package/dist/server/utils.js +11 -0
  48. package/dist/server/utils.js.map +1 -1
  49. package/dist/server/webdriver.d.ts +2 -3
  50. package/dist/server/webdriver.js +9 -9
  51. package/dist/server/webdriver.js.map +1 -1
  52. package/dist/server/worker/chai-image.d.ts +1 -2
  53. package/dist/server/worker/chai-image.js +4 -3
  54. package/dist/server/worker/chai-image.js.map +1 -1
  55. package/dist/server/worker/start.js +7 -10
  56. package/dist/server/worker/start.js.map +1 -1
  57. package/dist/types.d.ts +6 -2
  58. package/dist/types.js.map +1 -1
  59. package/package.json +30 -30
  60. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  61. package/src/creevey.ts +12 -6
  62. package/src/server/config.ts +2 -1
  63. package/src/server/docker.ts +2 -2
  64. package/src/server/index.ts +4 -4
  65. package/src/server/logger.ts +6 -2
  66. package/src/server/master/api.ts +1 -1
  67. package/src/server/master/pool.ts +18 -58
  68. package/src/server/master/queue.ts +64 -0
  69. package/src/server/master/runner.ts +4 -1
  70. package/src/server/master/server.ts +1 -1
  71. package/src/server/master/start.ts +7 -4
  72. package/src/server/playwright/internal.ts +22 -20
  73. package/src/server/playwright/webdriver.ts +1 -1
  74. package/src/server/providers/browser.ts +2 -1
  75. package/src/server/providers/hybrid.ts +1 -1
  76. package/src/server/reporter.ts +4 -3
  77. package/src/server/selenium/internal.ts +98 -76
  78. package/src/server/selenium/webdriver.ts +1 -1
  79. package/src/server/utils.ts +12 -1
  80. package/src/server/webdriver.ts +9 -15
  81. package/src/server/worker/chai-image.ts +4 -4
  82. package/src/server/worker/start.ts +7 -11
  83. package/src/types.ts +6 -2
  84. package/.yarnrc.yml +0 -1
  85. package/chromatic.config.json +0 -5
@@ -105,7 +105,7 @@ async function buildWebdriver(
105
105
  const url = new URL(gridUrl);
106
106
  url.username = url.username ? '********' : '';
107
107
  url.password = url.password ? '********' : '';
108
- logger.debug(`(${browser}) Connecting to Selenium ${chalk.magenta(url.toString())}`);
108
+ logger().debug(`Connecting to Selenium ${chalk.magenta(url.toString())}`);
109
109
 
110
110
  // TODO Define some capabilities explicitly and define typings
111
111
  const capabilities = new Capabilities({
@@ -125,33 +125,61 @@ async function buildWebdriver(
125
125
  // TODO Validate browsers, versions, and platform
126
126
  // TODO Use `customizeBuilder`
127
127
 
128
- let webdriver: WebDriver;
128
+ let webdriver: WebDriver | null;
129
129
 
130
130
  try {
131
- // const ie = new IeOptions();
132
- // const edge = new EdgeOptions();
133
- // const chrome = new ChromeOptions();
134
- // const safari = new SafariOptions();
135
- // const firefox = new FirefoxOptions();
136
- // edge.enableBidi();
137
- // chrome.enableBidi();
138
- // firefox.enableBidi();
139
-
140
- webdriver = await new Builder()
141
- // .setIeOptions(ie)
142
- // .setEdgeOptions(edge)
143
- // .setChromeOptions(chrome)
144
- // .setSafariOptions(safari)
145
- // .setFirefoxOptions(firefox)
146
- .usingServer(gridUrl)
147
- .withCapabilities(capabilities)
148
- .setLoggingPrefs(prefs) // NOTE: Should go last
149
- .build();
150
-
151
- // const id = await browser.getWindowHandle();
152
- // context = await BrowsingContext(browser, { browsingContextId: id });
131
+ const maxRetries = 5;
132
+ let retries = 0;
133
+ do {
134
+ webdriver = await Promise.race([
135
+ new Promise<null>((resolve) => {
136
+ setTimeout(() => {
137
+ retries += 1;
138
+ resolve(null);
139
+ }, 120_000);
140
+ }),
141
+ (async () => {
142
+ if (retries > 0) {
143
+ logger().debug(`Trying to initialize session to Selenium Grid: retried ${retries} of ${maxRetries}`);
144
+ }
145
+ const retry = retries;
146
+ // const ie = new IeOptions();
147
+ // const edge = new EdgeOptions();
148
+ // const chrome = new ChromeOptions();
149
+ // const safari = new SafariOptions();
150
+ // const firefox = new FirefoxOptions();
151
+ // edge.enableBidi();
152
+ // chrome.enableBidi();
153
+ // firefox.enableBidi();
154
+
155
+ const driver = await new Builder()
156
+ // .setIeOptions(ie)
157
+ // .setEdgeOptions(edge)
158
+ // .setChromeOptions(chrome)
159
+ // .setSafariOptions(safari)
160
+ // .setFirefoxOptions(firefox)
161
+ .usingServer(gridUrl)
162
+ .withCapabilities(capabilities)
163
+ .setLoggingPrefs(prefs) // NOTE: Should go last
164
+ .build();
165
+
166
+ // const id = await driver.getWindowHandle();
167
+ // context = await BrowsingContext(driver, { browsingContextId: id });
168
+
169
+ if (retry != retries) {
170
+ void driver.quit();
171
+ return null;
172
+ }
173
+
174
+ return driver;
175
+ })(),
176
+ ]);
177
+ if (webdriver) break;
178
+ } while (retries < maxRetries);
179
+
180
+ if (!webdriver) throw new Error('Failed to initialize session to Selenium Grid due to many retries');
153
181
  } catch (error) {
154
- logger.error(`(${browser}) Failed to start browser:`, error);
182
+ logger().error(`Failed to start browser:`, error);
155
183
  return null;
156
184
  }
157
185
 
@@ -163,13 +191,11 @@ export class InternalBrowser {
163
191
  #browser: WebDriver;
164
192
  #serverHost: string | null = null;
165
193
  #serverPort: number;
166
- #logger: Logger.Logger;
167
194
  #unsubscribe: () => void = noop;
168
195
  #keepAliveInterval: NodeJS.Timeout | null = null;
169
- constructor(browser: WebDriver, port: number, logger: Logger.Logger) {
196
+ constructor(browser: WebDriver, port: number) {
170
197
  this.#browser = browser;
171
198
  this.#serverPort = port;
172
- this.#logger = logger;
173
199
  this.#unsubscribe = subscribeOn('shutdown', () => {
174
200
  void this.closeBrowser();
175
201
  });
@@ -198,7 +224,7 @@ export class InternalBrowser {
198
224
 
199
225
  const ignoreStyles = await this.insertIgnoreStyles(ignoreElements);
200
226
 
201
- if (this.#logger.getLevel() <= Logger.levels.DEBUG) {
227
+ if (logger().getLevel() <= Logger.levels.DEBUG) {
202
228
  const { innerWidth, innerHeight } = await this.#browser.executeScript<{
203
229
  innerWidth: number;
204
230
  innerHeight: number;
@@ -208,16 +234,16 @@ export class InternalBrowser {
208
234
  innerHeight: window.innerHeight,
209
235
  };
210
236
  });
211
- this.#logger.debug(`Viewport size is: ${innerWidth}x${innerHeight}`);
237
+ logger().debug(`Viewport size is: ${innerWidth}x${innerHeight}`);
212
238
  }
213
239
 
214
240
  try {
215
241
  if (!captureElement) {
216
- this.#logger.debug('Capturing viewport screenshot');
242
+ logger().debug('Capturing viewport screenshot');
217
243
  screenshot = await this.#browser.takeScreenshot();
218
- this.#logger.debug('Viewport screenshot is captured');
244
+ logger().debug('Viewport screenshot is captured');
219
245
  } else {
220
- this.#logger.debug(`Checking is element ${chalk.cyan(captureElement)} fit into viewport`);
246
+ logger().debug(`Checking is element ${chalk.cyan(captureElement)} fit into viewport`);
221
247
  const rects = await this.#browser.executeScript<
222
248
  { elementRect: ElementRect; windowRect: ElementRect } | undefined
223
249
  >(function (selector: string): { elementRect: ElementRect; windowRect: ElementRect } | undefined {
@@ -256,11 +282,11 @@ export class InternalBrowser {
256
282
  elementRect.height + elementRect.top <= windowRect.height;
257
283
 
258
284
  if (isFitIntoViewport) {
259
- this.#logger.debug(
285
+ logger().debug(
260
286
  `Capturing ${chalk.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`,
261
287
  );
262
288
  } else
263
- this.#logger.debug(
289
+ logger().debug(
264
290
  `Capturing composite screenshot image of ${chalk.cyan(captureElement)} with size: ${elementRect.width}x${elementRect.height}`,
265
291
  );
266
292
 
@@ -276,7 +302,7 @@ export class InternalBrowser {
276
302
  : // TODO pointer-events: none, need to research
277
303
  await this.takeCompositeScreenshot(windowRect, elementRect);
278
304
 
279
- this.#logger.debug(`${chalk.cyan(captureElement)} is captured`);
305
+ logger().debug(`${chalk.cyan(captureElement)} is captured`);
280
306
  }
281
307
  } finally {
282
308
  await this.removeIgnoreStyles(ignoreStyles);
@@ -298,7 +324,7 @@ export class InternalBrowser {
298
324
  await this.updateBrowserGlobalVariables();
299
325
  await this.resetMousePosition();
300
326
 
301
- this.#logger.debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
327
+ logger().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
302
328
 
303
329
  const result = await this.#browser.executeAsyncScript<[error?: string | null, isCaptureCalled?: boolean] | null>(
304
330
  function (
@@ -360,14 +386,14 @@ export class InternalBrowser {
360
386
  }
361
387
 
362
388
  async afterTest(test: ServerTest): Promise<void> {
363
- if (this.#logger.getLevel() === Logger.levels.TRACE) {
389
+ if (logger().getLevel() === Logger.levels.TRACE) {
364
390
  const output: string[] = [];
365
391
  const types = await this.#browser.manage().logs().getAvailableLogTypes();
366
392
  for (const type of types) {
367
393
  const logs = await this.#browser.manage().logs().get(type);
368
394
  output.push(logs.map((log) => JSON.stringify(log.toJSON(), null, 2)).join('\n'));
369
395
  }
370
- this.#logger.debug(
396
+ logger().debug(
371
397
  '----------',
372
398
  getTestPath(test).join('/'),
373
399
  '----------\n',
@@ -391,7 +417,7 @@ export class InternalBrowser {
391
417
 
392
418
  if (!browser) return null;
393
419
 
394
- const internalBrowser = new InternalBrowser(browser, options.port, logger);
420
+ const internalBrowser = new InternalBrowser(browser, options.port);
395
421
 
396
422
  try {
397
423
  if (isShuttingDown.current) return null;
@@ -409,11 +435,12 @@ export class InternalBrowser {
409
435
  } catch (originalError) {
410
436
  void internalBrowser.closeBrowser();
411
437
 
412
- const message = originalError instanceof Error ? originalError.message : (originalError as string);
413
- const error = new Error(`Can't load storybook root page by URL ${await browser.getCurrentUrl()}: ${message}`);
438
+ const message =
439
+ originalError instanceof Error ? originalError.message : ((originalError ?? 'Unknown error') as string);
440
+ const error = new Error(`Can't load storybook root page: ${message}`);
414
441
  if (originalError instanceof Error) error.stack = originalError.stack;
415
442
 
416
- logger.error(error);
443
+ logger().error(error);
417
444
 
418
445
  return null;
419
446
  }
@@ -444,16 +471,14 @@ export class InternalBrowser {
444
471
  /* noop */
445
472
  }
446
473
 
447
- this.#logger = Logger.getLogger(sessionId);
448
-
449
- prefix.apply(this.#logger, {
474
+ prefix.apply(logger(), {
450
475
  format(level) {
451
476
  const levelColor = colors[level.toUpperCase() as keyof typeof colors];
452
- return `[${browserName}:${chalk.gray(sessionId)}] ${levelColor(level)} =>`;
477
+ return `[${browserName}:${chalk.gray(process.pid)}] ${levelColor(level)} => ${chalk.gray(sessionId)}`;
453
478
  },
454
479
  });
455
480
 
456
- this.#logger.debug(`Connected successful with ${chalk.green(browserHost)}`);
481
+ logger().debug(`Connected successful with ${chalk.green(browserHost)}`);
457
482
 
458
483
  return await runSequence(
459
484
  [
@@ -482,20 +507,20 @@ export class InternalBrowser {
482
507
 
483
508
  try {
484
509
  if (resolver) {
485
- this.#logger.debug('Resolving storybook url with custom resolver');
510
+ logger().debug('Resolving storybook url with custom resolver');
486
511
 
487
512
  const resolvedUrl = await resolver();
488
513
 
489
- this.#logger.debug(`Resolver storybook url ${resolvedUrl}`);
514
+ logger().debug(`Resolver storybook url ${resolvedUrl}`);
490
515
 
491
516
  await this.#browser.get(appendIframePath(resolvedUrl));
492
517
  } else {
493
518
  // TODO Pageload timeout 10s
494
519
  // NOTE: getUrlChecker already calls `browser.get` so we don't need another one
495
- await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url), this.#logger);
520
+ await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
496
521
  }
497
522
  } catch (error) {
498
- this.#logger.error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
523
+ logger().error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
499
524
  throw error;
500
525
  }
501
526
  }
@@ -503,13 +528,13 @@ export class InternalBrowser {
503
528
  private async checkUrl(url: string): Promise<boolean> {
504
529
  try {
505
530
  // NOTE: Before trying a new url, reset the current one
506
- this.#logger.debug(`Opening ${chalk.magenta('about:blank')} page`);
531
+ logger().debug(`Opening ${chalk.magenta('about:blank')} page`);
507
532
  await openUrlAndWaitForPageSource(
508
533
  this.#browser,
509
534
  'about:blank',
510
535
  (source: string) => !source.includes('<body></body>'),
511
536
  );
512
- this.#logger.debug(`Opening ${chalk.magenta(url)} and checking the page source`);
537
+ logger().debug(`Opening ${chalk.magenta(url)} and checking the page source`);
513
538
  const source = await openUrlAndWaitForPageSource(
514
539
  this.#browser,
515
540
  url,
@@ -521,7 +546,7 @@ export class InternalBrowser {
521
546
  // because other add significant delay and some of them don't work in earlier chrome versions
522
547
  // Browsers always load page successful even it's failed
523
548
  // So we just check `root` element
524
- this.#logger.debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
549
+ logger().debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
525
550
  return source.includes(`id="${storybookRootID}"`);
526
551
  } catch {
527
552
  return false;
@@ -529,7 +554,7 @@ export class InternalBrowser {
529
554
  }
530
555
 
531
556
  private async waitForStorybook(): Promise<void> {
532
- this.#logger.debug('Waiting for `setStories` event to make sure that storybook is initiated');
557
+ logger().debug('Waiting for `setStories` event to make sure that storybook is initiated');
533
558
 
534
559
  const isTimeout = await Promise.race([
535
560
  new Promise<boolean>((resolve) => {
@@ -540,19 +565,15 @@ export class InternalBrowser {
540
565
  (async () => {
541
566
  let wait = true;
542
567
  do {
543
- try {
544
- // TODO Research a different way to ensure storybook is initiated
545
- wait = await this.#browser.executeScript<boolean>(function (SET_GLOBALS: string): boolean {
546
- // TODO Maybe use
547
- // import { global } from '@storybook/global';
548
- // global.IS_STORYBOOK
549
- if (typeof window.__STORYBOOK_ADDONS_CHANNEL__ == 'undefined') return true;
550
- if (window.__STORYBOOK_ADDONS_CHANNEL__.last(SET_GLOBALS) == undefined) return true;
551
- return false;
552
- }, StorybookEvents.SET_GLOBALS);
553
- } catch (e: unknown) {
554
- this.#logger.debug('An error has been caught during the script:', e);
555
- }
568
+ // TODO Research a different way to ensure storybook is initiated
569
+ wait = await this.#browser.executeScript<boolean>(function (SET_GLOBALS: string): boolean {
570
+ // TODO Maybe use
571
+ // import { global } from '@storybook/global';
572
+ // global.IS_STORYBOOK
573
+ if (typeof window.__STORYBOOK_ADDONS_CHANNEL__ == 'undefined') return true;
574
+ if (window.__STORYBOOK_ADDONS_CHANNEL__.last(SET_GLOBALS) == undefined) return true;
575
+ return false;
576
+ }, StorybookEvents.SET_GLOBALS);
556
577
  } while (wait);
557
578
  return false;
558
579
  })(),
@@ -565,7 +586,7 @@ export class InternalBrowser {
565
586
  private async updateStorybookGlobals(globals?: StorybookGlobals): Promise<void> {
566
587
  if (!globals) return;
567
588
 
568
- this.#logger.debug('Applying storybook globals');
589
+ logger().debug('Applying storybook globals');
569
590
  await this.#browser.executeScript(function (globals: StorybookGlobals) {
570
591
  window.__CREEVEY_UPDATE_GLOBALS__(globals);
571
592
  }, globals);
@@ -635,7 +656,7 @@ export class InternalBrowser {
635
656
  },
636
657
  );
637
658
 
638
- this.#logger.debug(`Resizing viewport from ${innerWidth}x${innerHeight} to ${viewport.width}x${viewport.height}`);
659
+ logger().debug(`Resizing viewport from ${innerWidth}x${innerHeight} to ${viewport.width}x${viewport.height}`);
639
660
 
640
661
  const dWidth = windowRect.width - innerWidth;
641
662
  const dHeight = windowRect.height - innerHeight;
@@ -649,7 +670,7 @@ export class InternalBrowser {
649
670
  }
650
671
 
651
672
  private async resetMousePosition(): Promise<void> {
652
- this.#logger.debug('Resetting mouse position to the top-left corner');
673
+ logger().debug('Resetting mouse position to the top-left corner');
653
674
  const browserName = (await this.#browser.getCapabilities()).getBrowserName();
654
675
  const [browserVersion] =
655
676
  (await this.#browser.getCapabilities()).getBrowserVersion()?.split('.') ??
@@ -677,8 +698,9 @@ export class InternalBrowser {
677
698
  y: Math.ceil((-1 * height) / 2) - top,
678
699
  })
679
700
  .perform();
680
- } else if (browserName == 'firefox' && browserVersion == '61') {
701
+ } else if (browserName == 'firefox') {
681
702
  // NOTE Firefox for some reason moving by 0 x 0 move cursor in bottom left corner :sad:
703
+ // NOTE In recent versions (eg 128.0) moving by 0 x 0 doesn't work at all
682
704
  await this.#browser.actions().move({ origin: Origin.VIEWPORT, x: 0, y: 1 }).perform();
683
705
  } else {
684
706
  // NOTE IE don't emit move events until force window focus or connect by RDP on virtual machine
@@ -690,7 +712,7 @@ export class InternalBrowser {
690
712
  const ignoreSelectors = Array.prototype.concat(ignoreElements).filter(Boolean);
691
713
  if (!ignoreSelectors.length) return null;
692
714
 
693
- this.#logger.debug('Hiding ignored elements before capturing');
715
+ logger().debug('Hiding ignored elements before capturing');
694
716
 
695
717
  return await this.#browser.executeScript(function (ignoreSelectors: string[]) {
696
718
  return window.__CREEVEY_INSERT_IGNORE_STYLES__(ignoreSelectors);
@@ -771,7 +793,7 @@ export class InternalBrowser {
771
793
 
772
794
  private async removeIgnoreStyles(ignoreStyles: WebElement | null): Promise<void> {
773
795
  if (ignoreStyles) {
774
- this.#logger.debug('Revert hiding ignored elements');
796
+ logger().debug('Revert hiding ignored elements');
775
797
  await this.#browser.executeScript(function (ignoreStyles: HTMLStyleElement) {
776
798
  window.__CREEVEY_REMOVE_IGNORE_STYLES__(ignoreStyles);
777
799
  }, ignoreStyles);
@@ -810,7 +832,7 @@ export class InternalBrowser {
810
832
  this.#keepAliveInterval = setInterval(() => {
811
833
  // NOTE Simple way to keep session alive
812
834
  void this.#browser.getCurrentUrl().then((url) => {
813
- logger.debug('current url', chalk.magenta(url));
835
+ logger().debug('current url', chalk.magenta(url));
814
836
  });
815
837
  }, 10 * 1000);
816
838
  }
@@ -63,7 +63,7 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
63
63
  try {
64
64
  return await import('./internal.js');
65
65
  } catch (error) {
66
- logger.error(error);
66
+ logger().error(error);
67
67
  return null;
68
68
  }
69
69
  })();
@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
6
6
  import { createRequire } from 'module';
7
7
  import { register as esmRegister } from 'tsx/esm/api';
8
8
  import { register as cjsRegister } from 'tsx/cjs/api';
9
- import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest } from '../types.js';
9
+ import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest, Worker } from '../types.js';
10
10
  import { emitShutdownMessage, sendShutdownMessage } from './messages.js';
11
11
 
12
12
  const importMetaUrl = pathToFileURL(__filename).href;
@@ -96,6 +96,17 @@ export async function shutdownWorkers(): Promise<void> {
96
96
  emitShutdownMessage();
97
97
  }
98
98
 
99
+ export function gracefullyKill(worker: Worker): void {
100
+ worker.isShuttingDown = true;
101
+ const timeout = setTimeout(() => {
102
+ worker.kill();
103
+ }, 10000);
104
+ worker.on('exit', () => {
105
+ clearTimeout(timeout);
106
+ });
107
+ sendShutdownMessage(worker);
108
+ }
109
+
99
110
  export async function getCreeveyCache(): Promise<string | undefined> {
100
111
  const { default: findCacheDir } = await import('find-cache-dir');
101
112
  return findCacheDir({ name: 'creevey', cwd: dirname(fileURLToPath(importMetaUrl)) });
@@ -1,7 +1,6 @@
1
- import Logger from 'loglevel';
2
1
  import chalk from 'chalk';
3
2
  import { networkInterfaces } from 'os';
4
- import { logger as defaultLogger } from './logger.js';
3
+ import { logger } from './logger.js';
5
4
  import { Args } from '@storybook/csf';
6
5
  import {
7
6
  isDefined,
@@ -22,16 +21,15 @@ const DOCKER_INTERNAL = 'host.docker.internal';
22
21
  export async function resolveStorybookUrl(
23
22
  storybookUrl: string,
24
23
  checkUrl: (url: string) => Promise<boolean>,
25
- logger: Logger.Logger = defaultLogger,
26
24
  ): Promise<string> {
27
- logger.debug('Resolving storybook url');
25
+ logger().debug('Resolving storybook url');
28
26
  const addresses = getAddresses();
29
27
  // TODO Use Promise.race?
30
28
  for (const ip of addresses) {
31
29
  const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
32
- logger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
30
+ logger().debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
33
31
  if (await checkUrl(resolvedUrl)) {
34
- logger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
32
+ logger().debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
35
33
  return resolvedUrl;
36
34
  }
37
35
  }
@@ -75,11 +73,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
75
73
 
76
74
  abstract afterTest(test: ServerTest): Promise<void>;
77
75
 
78
- async switchStory(
79
- story: StoryInput,
80
- context: BaseCreeveyTestContext,
81
- logger: Logger.Logger,
82
- ): Promise<CreeveyTestContext> {
76
+ async switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext> {
83
77
  const { id, title, name, parameters } = story;
84
78
  const {
85
79
  captureElement = `#${storybookRootID}`,
@@ -87,7 +81,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
87
81
  ignoreElements,
88
82
  } = (parameters.creevey ?? {}) as CreeveyStoryParams;
89
83
 
90
- logger.debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
84
+ logger().debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
91
85
 
92
86
  let storyPlayResolver: (isCompleted: boolean) => void;
93
87
  let waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
@@ -108,7 +102,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
108
102
  const isCaptureCalled = await this.selectStory(id, waitForReady);
109
103
 
110
104
  if (isCaptureCalled) {
111
- logger.debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
105
+ logger().debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
112
106
  while (!(await waitForComplete)) {
113
107
  waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
114
108
  }
@@ -116,8 +110,8 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
116
110
 
117
111
  unsubscribe();
118
112
 
119
- if (isCaptureCalled) logger.debug(`Story ${chalk.magenta(id)} completed capturing`);
120
- else logger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
113
+ if (isCaptureCalled) logger().debug(`Story ${chalk.magenta(id)} completed capturing`);
114
+ else logger().debug(`Story ${chalk.magenta(id)} ready for capturing`);
121
115
 
122
116
  return Object.assign(
123
117
  {
@@ -1,8 +1,8 @@
1
- import Logger from 'loglevel';
1
+ import { logger } from '../logger';
2
+
2
3
  export default function (
3
4
  matchImage: (image: Buffer, imageName?: string) => Promise<void>,
4
5
  matchImages: (images: Record<string, Buffer>) => Promise<void>,
5
- logger: Logger.Logger,
6
6
  ) {
7
7
  let isWarningShown = false;
8
8
  return function chaiImage({ Assertion }: Chai.ChaiStatic, utils: Chai.ChaiUtils): void {
@@ -11,7 +11,7 @@ export default function (
11
11
  'matchImage',
12
12
  async function (this: Record<string, unknown>, imageName?: string) {
13
13
  if (!isWarningShown) {
14
- logger.warn(
14
+ logger().warn(
15
15
  '`expect(...).to.matchImage()` is deprecated and will be removed in the next major release. Please use `context.matchImage()` instead.',
16
16
  );
17
17
  isWarningShown = true;
@@ -23,7 +23,7 @@ export default function (
23
23
 
24
24
  utils.addMethod(Assertion.prototype, 'matchImages', async function (this: Record<string, unknown>) {
25
25
  if (!isWarningShown) {
26
- logger.warn(
26
+ logger().warn(
27
27
  '`expect(...).to.matchImages()` is deprecated and will be removed in the next major release. Please use `context.matchImages()` instead.',
28
28
  );
29
29
  isWarningShown = true;
@@ -1,6 +1,4 @@
1
1
  import chai from 'chai';
2
- import chalk from 'chalk';
3
- import Logger from 'loglevel';
4
2
  import EventEmitter from 'events';
5
3
  import {
6
4
  BaseCreeveyTestContext,
@@ -86,7 +84,7 @@ function runHandler(browserName: string, images: Partial<Record<string, Images>>
86
84
 
87
85
  async function setupWebdriver(webdriver: CreeveyWebdriver): Promise<[string, CreeveyWebdriver] | undefined> {
88
86
  if ((await webdriver.openBrowser(true)) == null) {
89
- logger.error('Failed to start browser');
87
+ logger().error('Failed to start browser');
90
88
  emitWorkerMessage({
91
89
  type: 'error',
92
90
  payload: { subtype: 'browser', error: 'Failed to start browser' },
@@ -125,8 +123,6 @@ export async function start(browser: string, gridUrl: string, config: Config, op
125
123
 
126
124
  if (!webdriver || !sessionId) return;
127
125
 
128
- const workerLogger = Logger.getLogger(`${browser}:${chalk.gray(sessionId)}`);
129
-
130
126
  const reporterOptions = {
131
127
  ...config.reporterOptions,
132
128
  creevey: {
@@ -150,13 +146,13 @@ export async function start(browser: string, gridUrl: string, config: Config, op
150
146
  const { matchImage, matchImages } = options.odiff
151
147
  ? getOdiffMatchers(imagesContext, config)
152
148
  : await getMatchers(imagesContext, config);
153
- chai.use(chaiImage(matchImage, matchImages, workerLogger));
149
+ chai.use(chaiImage(matchImage, matchImages));
154
150
 
155
151
  const tests = await (async () => {
156
152
  try {
157
153
  return await getTestsFromStories(config, browser, webdriver);
158
154
  } catch (error) {
159
- workerLogger.error('Failed to get tests from stories:', error);
155
+ logger().error('Failed to get tests from stories:', error);
160
156
  emitWorkerMessage({
161
157
  type: 'error',
162
158
  payload: { subtype: 'browser', error: serializeError(error) },
@@ -174,7 +170,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
174
170
 
175
171
  if (!test) {
176
172
  const error = `Test with id ${message.payload.id} not found`;
177
- workerLogger.error(error);
173
+ logger().error(error);
178
174
  emitWorkerMessage({
179
175
  type: 'error',
180
176
  payload: { subtype: 'test', error },
@@ -235,7 +231,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
235
231
  }, config.testTimeout),
236
232
  ),
237
233
  (async () => {
238
- const context = await webdriver.switchStory(test.story, baseContext, workerLogger);
234
+ const context = await webdriver.switchStory(test.story, baseContext);
239
235
  await test.fn(context);
240
236
  })(),
241
237
  ]);
@@ -261,7 +257,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
261
257
 
262
258
  runHandler(baseContext.browserName, imagesContext.images, error);
263
259
  })().catch((error: unknown) => {
264
- workerLogger.error('Unexpected error:', error);
260
+ logger().error('Unexpected error:', error);
265
261
  emitWorkerMessage({
266
262
  type: 'error',
267
263
  payload: { subtype: 'test', error: serializeError(error) },
@@ -269,7 +265,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
269
265
  });
270
266
  });
271
267
 
272
- workerLogger.info('Browser is ready');
268
+ logger().info('Browser is ready');
273
269
 
274
270
  emitWorkerMessage({ type: 'ready' });
275
271
  }
package/src/types.ts CHANGED
@@ -4,7 +4,6 @@ import type Pixelmatch from 'pixelmatch';
4
4
  import type { ODiffOptions } from 'odiff-bin';
5
5
  import type { expect } from 'chai';
6
6
  import type EventEmitter from 'events';
7
- import type Logger from 'loglevel';
8
7
  import { LaunchOptions } from 'playwright-core';
9
8
  // import type { Browser } from 'playwright-core';
10
9
 
@@ -164,7 +163,7 @@ export interface CreeveyWebdriver {
164
163
  openBrowser(fresh?: boolean): Promise<CreeveyWebdriver | null>;
165
164
  closeBrowser(): Promise<void>;
166
165
  loadStoriesFromBrowser(): Promise<StoriesRaw>;
167
- switchStory(story: StoryInput, context: BaseCreeveyTestContext, logger: Logger.Logger): Promise<CreeveyTestContext>;
166
+ switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext>;
168
167
  afterTest(test: ServerTest): Promise<void>;
169
168
  }
170
169
 
@@ -307,6 +306,11 @@ export interface Config {
307
306
  * The `--ui` CLI option ignores this option
308
307
  */
309
308
  failFast: boolean;
309
+ /**
310
+ * Start workers in sequential queue
311
+ * @default false
312
+ */
313
+ useWorkerQueue: boolean;
310
314
  /**
311
315
  * Specify platform for docker images
312
316
  */
package/.yarnrc.yml DELETED
@@ -1 +0,0 @@
1
- nodeLinker: node-modules
@@ -1,5 +0,0 @@
1
- {
2
- "onlyChanged": true,
3
- "projectId": "Project:5fd73972015e660021b97564",
4
- "zip": true
5
- }