creevey 0.8.0-beta.0 → 0.9.0-beta.0

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 (149) hide show
  1. package/CHANGELOG.md +5 -9
  2. package/lib/cjs/client/addon/Manager.js +0 -1
  3. package/lib/cjs/client/addon/preset.js +1 -0
  4. package/lib/cjs/client/addon/readyForCapture.js +12 -0
  5. package/lib/cjs/client/addon/withCreevey.js +313 -41
  6. package/lib/cjs/client/shared/components/ImagesView/BlendView.js +3 -3
  7. package/lib/cjs/client/shared/components/ImagesView/SideBySideView.js +3 -3
  8. package/lib/cjs/client/shared/components/ImagesView/SlideView.js +4 -3
  9. package/lib/cjs/client/shared/components/ImagesView/SwapView.js +3 -3
  10. package/lib/cjs/client/shared/helpers.js +1 -1
  11. package/lib/cjs/client/web/main.js +6 -6
  12. package/lib/cjs/index.js +27 -9
  13. package/lib/cjs/server/config.js +7 -3
  14. package/lib/cjs/server/extract.js +11 -4
  15. package/lib/cjs/server/index.js +2 -4
  16. package/lib/cjs/server/master/index.js +3 -9
  17. package/lib/cjs/server/master/master.js +1 -0
  18. package/lib/cjs/server/master/pool.js +29 -29
  19. package/lib/cjs/server/master/server.js +75 -3
  20. package/lib/cjs/server/messages.js +124 -12
  21. package/lib/cjs/server/parser.js +85 -0
  22. package/lib/cjs/server/selenium/browser.js +119 -21
  23. package/lib/cjs/server/stories.js +49 -46
  24. package/lib/cjs/server/storybook/providers/browser.js +78 -0
  25. package/lib/cjs/server/storybook/providers/hybrid.js +79 -0
  26. package/lib/cjs/server/storybook/{nodejs-provider.js → providers/nodejs.js} +32 -13
  27. package/lib/cjs/server/utils.js +7 -0
  28. package/lib/cjs/server/worker/helpers.js +2 -6
  29. package/lib/cjs/server/worker/worker.js +15 -3
  30. package/lib/cjs/shared.js +78 -6
  31. package/lib/cjs/types.js +5 -0
  32. package/lib/esm/client/addon/Manager.js +0 -1
  33. package/lib/esm/client/addon/preset.js +1 -0
  34. package/lib/esm/client/addon/readyForCapture.js +5 -0
  35. package/lib/esm/client/addon/withCreevey.js +303 -41
  36. package/lib/esm/client/shared/components/ImagesView/BlendView.js +2 -3
  37. package/lib/esm/client/shared/components/ImagesView/SideBySideView.js +2 -3
  38. package/lib/esm/client/shared/components/ImagesView/SlideView.js +3 -3
  39. package/lib/esm/client/shared/components/ImagesView/SwapView.js +2 -3
  40. package/lib/esm/client/shared/helpers.js +1 -1
  41. package/lib/esm/index.js +6 -3
  42. package/lib/esm/server/config.js +7 -5
  43. package/lib/esm/server/extract.js +8 -4
  44. package/lib/esm/server/index.js +2 -3
  45. package/lib/esm/server/master/index.js +4 -10
  46. package/lib/esm/server/master/master.js +1 -0
  47. package/lib/esm/server/master/pool.js +31 -31
  48. package/lib/esm/server/master/server.js +73 -5
  49. package/lib/esm/server/messages.js +118 -12
  50. package/lib/esm/server/parser.js +63 -0
  51. package/lib/esm/server/selenium/browser.js +116 -23
  52. package/lib/esm/server/stories.js +50 -46
  53. package/lib/esm/server/storybook/providers/browser.js +61 -0
  54. package/lib/esm/server/storybook/providers/hybrid.js +63 -0
  55. package/lib/esm/server/storybook/{nodejs-provider.js → providers/nodejs.js} +30 -13
  56. package/lib/esm/server/utils.js +6 -1
  57. package/lib/esm/server/worker/helpers.js +2 -6
  58. package/lib/esm/server/worker/worker.js +16 -4
  59. package/lib/esm/shared.js +59 -5
  60. package/lib/esm/types.js +3 -0
  61. package/lib/types/cli.d.ts +1 -1
  62. package/lib/types/client/addon/Manager.d.ts +37 -37
  63. package/lib/types/client/addon/components/Addon.d.ts +8 -8
  64. package/lib/types/client/addon/components/Icons.d.ts +7 -7
  65. package/lib/types/client/addon/components/Panel.d.ts +9 -9
  66. package/lib/types/client/addon/components/TestSelect.d.ts +9 -9
  67. package/lib/types/client/addon/components/Tools.d.ts +6 -6
  68. package/lib/types/client/addon/decorator.d.ts +1 -1
  69. package/lib/types/client/addon/preset.d.ts +24 -22
  70. package/lib/types/client/addon/readyForCapture.d.ts +6 -0
  71. package/lib/types/client/addon/register.d.ts +3 -3
  72. package/lib/types/client/addon/utils.d.ts +2 -2
  73. package/lib/types/client/addon/withCreevey.d.ts +24 -13
  74. package/lib/types/client/shared/components/ImagesView/BlendView.d.ts +3 -3
  75. package/lib/types/client/shared/components/ImagesView/ImagesView.d.ts +25 -25
  76. package/lib/types/client/shared/components/ImagesView/SideBySideView.d.ts +3 -3
  77. package/lib/types/client/shared/components/ImagesView/SlideView.d.ts +3 -3
  78. package/lib/types/client/shared/components/ImagesView/SwapView.d.ts +3 -3
  79. package/lib/types/client/shared/components/ImagesView/index.d.ts +5 -5
  80. package/lib/types/client/shared/components/PageFooter/PageFooter.d.ts +9 -9
  81. package/lib/types/client/shared/components/PageFooter/Paging.d.ts +8 -8
  82. package/lib/types/client/shared/components/PageHeader/ImagePreview.d.ts +12 -12
  83. package/lib/types/client/shared/components/PageHeader/PageHeader.d.ts +17 -17
  84. package/lib/types/client/shared/components/ResultsPage.d.ts +18 -18
  85. package/lib/types/client/shared/creeveyClientApi.d.ts +9 -9
  86. package/lib/types/client/shared/helpers.d.ts +46 -46
  87. package/lib/types/client/shared/viewMode.d.ts +4 -4
  88. package/lib/types/client/web/CreeveyApp.d.ts +12 -12
  89. package/lib/types/client/web/CreeveyContext.d.ts +11 -11
  90. package/lib/types/client/web/CreeveyLoader.d.ts +3 -3
  91. package/lib/types/client/web/CreeveyView/SideBar/Checkbox.d.ts +19 -19
  92. package/lib/types/client/web/CreeveyView/SideBar/Search.d.ts +6 -6
  93. package/lib/types/client/web/CreeveyView/SideBar/SideBar.d.ts +14 -14
  94. package/lib/types/client/web/CreeveyView/SideBar/SideBarHeader.d.ts +13 -13
  95. package/lib/types/client/web/CreeveyView/SideBar/SuiteLink.d.ts +33 -33
  96. package/lib/types/client/web/CreeveyView/SideBar/TestLink.d.ts +8 -8
  97. package/lib/types/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +10 -10
  98. package/lib/types/client/web/CreeveyView/SideBar/TestsStatus.d.ts +9 -9
  99. package/lib/types/client/web/CreeveyView/SideBar/Toggle.d.ts +6 -6
  100. package/lib/types/client/web/CreeveyView/SideBar/index.d.ts +1 -1
  101. package/lib/types/client/web/KeyboardEventsContext.d.ts +13 -13
  102. package/lib/types/client/web/index.d.ts +4 -4
  103. package/lib/types/creevey.d.ts +1 -1
  104. package/lib/types/index.d.ts +2 -1
  105. package/lib/types/server/config.d.ts +4 -4
  106. package/lib/types/server/docker.d.ts +7 -7
  107. package/lib/types/server/extract.d.ts +2 -2
  108. package/lib/types/server/index.d.ts +2 -2
  109. package/lib/types/server/loaders/babel/creevey-plugin.d.ts +1 -1
  110. package/lib/types/server/loaders/babel/helpers.d.ts +19 -19
  111. package/lib/types/server/loaders/babel/register.d.ts +5 -5
  112. package/lib/types/server/loaders/hooks/mdx.d.ts +1 -1
  113. package/lib/types/server/loaders/hooks/svelte.d.ts +1 -1
  114. package/lib/types/server/loaders/webpack/compile.d.ts +2 -2
  115. package/lib/types/server/loaders/webpack/creevey-loader.d.ts +2 -2
  116. package/lib/types/server/loaders/webpack/dummy-hmr.d.ts +10 -10
  117. package/lib/types/server/loaders/webpack/mdx-loader.d.ts +6 -6
  118. package/lib/types/server/loaders/webpack/start.d.ts +1 -1
  119. package/lib/types/server/logger.d.ts +6 -6
  120. package/lib/types/server/master/api.d.ts +7 -7
  121. package/lib/types/server/master/index.d.ts +3 -3
  122. package/lib/types/server/master/master.d.ts +7 -6
  123. package/lib/types/server/master/pool.d.ts +31 -30
  124. package/lib/types/server/master/runner.d.ts +26 -26
  125. package/lib/types/server/master/server.d.ts +2 -2
  126. package/lib/types/server/messages.d.ts +28 -18
  127. package/lib/types/server/parser.d.ts +12 -0
  128. package/lib/types/server/selenium/browser.d.ts +17 -14
  129. package/lib/types/server/selenium/index.d.ts +2 -2
  130. package/lib/types/server/selenium/selenoid.d.ts +3 -3
  131. package/lib/types/server/stories.d.ts +8 -8
  132. package/lib/types/server/storybook/entry.d.ts +18 -18
  133. package/lib/types/server/storybook/helpers.d.ts +24 -24
  134. package/lib/types/server/storybook/providers/browser.d.ts +4 -0
  135. package/lib/types/server/storybook/providers/hybrid.d.ts +4 -0
  136. package/lib/types/server/storybook/providers/nodejs.d.ts +9 -0
  137. package/lib/types/server/update.d.ts +2 -2
  138. package/lib/types/server/utils.d.ts +20 -19
  139. package/lib/types/server/worker/chai-image.d.ts +6 -6
  140. package/lib/types/server/worker/helpers.d.ts +8 -7
  141. package/lib/types/server/worker/index.d.ts +1 -1
  142. package/lib/types/server/worker/reporter.d.ts +8 -8
  143. package/lib/types/server/worker/worker.d.ts +4 -4
  144. package/lib/types/shared.d.ts +16 -4
  145. package/lib/types/types.d.ts +488 -459
  146. package/package.json +12 -6
  147. package/storybook-static/stories.json +21 -0
  148. package/types/mocha.d.ts +1 -0
  149. package/lib/types/server/storybook/nodejs-provider.d.ts +0 -5
@@ -9,11 +9,14 @@ import { Builder, By, Capabilities, Origin } from 'selenium-webdriver';
9
9
  import { PageLoadStrategy } from 'selenium-webdriver/lib/capabilities';
10
10
  import { isDefined, noop } from '../../types';
11
11
  import { colors, logger } from '../logger';
12
- import { subscribeOn } from '../messages';
12
+ import { emitStoriesMessage, subscribeOn } from '../messages';
13
13
  import { importStorybookCoreEvents, isStorybookVersionLessThan } from '../storybook/helpers';
14
14
  import { isShuttingDown, LOCALHOST_REGEXP, runSequence } from '../utils';
15
15
  const DOCKER_INTERNAL = 'host.docker.internal';
16
16
  let browserLogger = logger;
17
+ let browserName = '';
18
+ let browser = null;
19
+ let creeveyServerHost = null;
17
20
 
18
21
  function getSessionData(grid, sessionId = '') {
19
22
  const gridUrl = new URL(grid);
@@ -40,9 +43,13 @@ function getSessionData(grid, sessionId = '') {
40
43
  }));
41
44
  }
42
45
 
46
+ function getAddresses() {
47
+ return [DOCKER_INTERNAL].concat(...Object.values(networkInterfaces()).filter(isDefined).map(network => network.filter(info => info.family == 'IPv4').map(info => info.address)));
48
+ }
49
+
43
50
  async function resolveStorybookUrl(storybookUrl, checkUrl) {
44
51
  browserLogger.debug('Resolving storybook url');
45
- const addresses = [DOCKER_INTERNAL].concat(...Object.values(networkInterfaces()).filter(isDefined).map(network => network.filter(info => info.family == 'IPv4').map(info => info.address)));
52
+ const addresses = getAddresses();
46
53
 
47
54
  for (const ip of addresses) {
48
55
  const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
@@ -283,7 +290,7 @@ async function takeCompositeScreenshot(browser, windowRect, elementRect) {
283
290
  return PNG.sync.write(compositeImage).toString('base64');
284
291
  }
285
292
 
286
- async function takeScreenshot(browser, captureElement, ignoreElements) {
293
+ export async function takeScreenshot(browser, captureElement, ignoreElements) {
287
294
  let screenshot;
288
295
  const ignoreStyles = await insertIgnoreStyles(browser, ignoreElements);
289
296
 
@@ -341,14 +348,16 @@ async function selectStory(browser, {
341
348
  name
342
349
  }, waitForReady = false) {
343
350
  browserLogger.debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
344
- const errorMessage = await browser.executeAsyncScript(function (id, kind, name, shouldWaitForReady, callback) {
351
+ const result = await browser.executeAsyncScript(function (id, kind, name, shouldWaitForReady, callback) {
345
352
  if (typeof window.__CREEVEY_SELECT_STORY__ == 'undefined') {
346
- return callback("Creevey can't switch story. This may happened if forget to add `creevey` addon to your storybook config, or storybook not loaded in browser due syntax error.");
353
+ return callback(["Creevey can't switch story. This may happened if forget to add `creevey` addon to your storybook config, or storybook not loaded in browser due syntax error."]);
347
354
  }
348
355
 
349
356
  window.__CREEVEY_SELECT_STORY__(id, kind, name, shouldWaitForReady, callback);
350
357
  }, id, kind, name, waitForReady);
358
+ const [errorMessage, isCaptureCalled = false] = result || [];
351
359
  if (errorMessage) throw new Error(errorMessage);
360
+ return isCaptureCalled;
352
361
  }
353
362
 
354
363
  export async function updateStorybookGlobals(browser, globals) {
@@ -388,7 +397,47 @@ async function openStorybookPage(browser, storybookUrl, resolver) {
388
397
  }
389
398
  }
390
399
 
391
- export async function getBrowser(config, browserConfig) {
400
+ async function resolveCreeveyHost(browser, port) {
401
+ if (creeveyServerHost != null) return creeveyServerHost;
402
+ const addresses = getAddresses();
403
+ creeveyServerHost = await browser.executeAsyncScript(function (hosts, port, callback) {
404
+ void Promise.all(hosts.map(function (host) {
405
+ return new Promise(function (resolve, reject) {
406
+ setTimeout(reject, 10000); // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
407
+
408
+ fetch('http://' + host + ':' + port + '/ping').then(resolve).catch(reject);
409
+ }).then(function (response) {
410
+ return response.text();
411
+ }).then(function (pong) {
412
+ return pong == 'pong' ? host : null;
413
+ }).catch(function () {
414
+ return null;
415
+ });
416
+ })).then(function (hosts) {
417
+ callback(hosts.find(function (host) {
418
+ return host != null;
419
+ }));
420
+ });
421
+ }, addresses, port);
422
+ if (creeveyServerHost == null) throw new Error("Can't reach creevey server from a browser");
423
+ return creeveyServerHost;
424
+ }
425
+
426
+ export async function loadStoriesFromBrowser(port) {
427
+ if (!browser) throw new Error("Can't get stories from browser if webdriver isn't connected");
428
+ const host = await resolveCreeveyHost(browser, port);
429
+ const stories = await browser.executeAsyncScript(function (creeveyHost, creeveyPort, callback) {
430
+ window.__CREEVEY_SERVER_HOST__ = creeveyHost;
431
+ window.__CREEVEY_SERVER_PORT__ = creeveyPort;
432
+ void window.__CREEVEY_GET_STORIES__().then(callback);
433
+ }, host, port);
434
+ if (!stories) throw new Error("Can't get stories, it seems creevey or storybook API isn't available");
435
+ return stories;
436
+ }
437
+ export async function getBrowser(config, name) {
438
+ if (browser) return browser;
439
+ browserName = name;
440
+ const browserConfig = config.browsers[browserName];
392
441
  const {
393
442
  gridUrl = config.gridUrl,
394
443
  storybookUrl: address = config.storybookUrl,
@@ -398,14 +447,11 @@ export async function getBrowser(config, browserConfig) {
398
447
  ...userCapabilities
399
448
  } = browserConfig;
400
449
  void limit;
401
- const {
402
- browserName
403
- } = userCapabilities;
404
- const realAddress = address;
405
- let browser = null; // TODO Define some capabilities explicitly and define typings
450
+ const realAddress = address; // TODO Define some capabilities explicitly and define typings
406
451
 
407
- const capabilities = new Capabilities(userCapabilities);
408
- capabilities.setPageLoadStrategy(PageLoadStrategy.NONE);
452
+ const capabilities = new Capabilities({ ...userCapabilities,
453
+ pageLoadStrategy: PageLoadStrategy.NONE
454
+ });
409
455
  subscribeOn('shutdown', () => {
410
456
  var _browser;
411
457
 
@@ -420,7 +466,7 @@ export async function getBrowser(config, browserConfig) {
420
466
  const url = new URL(gridUrl);
421
467
  url.username = url.username ? '********' : '';
422
468
  url.password = url.password ? '********' : '';
423
- browserLogger.debug(`(${browserName}) Connecting to Selenium ${chalk.magenta(url.toString())}`);
469
+ browserLogger.debug(`(${name}) Connecting to Selenium ${chalk.magenta(url.toString())}`);
424
470
  browser = await new Builder().usingServer(gridUrl).withCapabilities(capabilities).build();
425
471
  const sessionId = (_await$browser$getSes = await browser.getSession()) === null || _await$browser$getSes === void 0 ? void 0 : _await$browser$getSes.getId();
426
472
  let browserHost = '';
@@ -434,12 +480,12 @@ export async function getBrowser(config, browserConfig) {
434
480
  /* noop */
435
481
  }
436
482
 
437
- browserLogger.debug(`(${browserName}) Connected successful with ${[chalk.green(browserHost), chalk.magenta(sessionId)].filter(Boolean).join(':')}`);
483
+ browserLogger.debug(`(${name}) Connected successful with ${[chalk.green(browserHost), chalk.magenta(sessionId)].filter(Boolean).join(':')}`);
438
484
  browserLogger = getLogger(sessionId);
439
485
  prefix.apply(browserLogger, {
440
486
  format(level) {
441
487
  const levelColor = colors[level.toUpperCase()];
442
- return `[${browserName}:${chalk.gray(sessionId)}] ${levelColor(level)} =>`;
488
+ return `[${name}:${chalk.gray(sessionId)}] ${levelColor(level)} =>`;
443
489
  }
444
490
 
445
491
  });
@@ -458,6 +504,7 @@ export async function getBrowser(config, browserConfig) {
458
504
  var _browser3;
459
505
 
460
506
  (_browser3 = browser) === null || _browser3 === void 0 ? void 0 : _browser3.quit().catch(noop);
507
+ browser = null;
461
508
  return null;
462
509
  }
463
510
 
@@ -471,6 +518,9 @@ export async function getBrowser(config, browserConfig) {
471
518
  await updateStorybookGlobals(browser, _storybookGlobals);
472
519
  }
473
520
 
521
+ await browser.executeScript(function (workerId) {
522
+ window.__CREEVEY_WORKER_ID__ = workerId;
523
+ }, process.pid);
474
524
  return browser;
475
525
  }
476
526
 
@@ -486,12 +536,22 @@ async function updateStoryArgs(browser, story, updatedArgs) {
486
536
  }, story.id, updatedArgs, Events.UPDATE_STORY_ARGS, Events.STORY_RENDERED);
487
537
  }
488
538
 
539
+ export async function closeBrowser() {
540
+ if (!browser) return;
541
+
542
+ try {
543
+ await browser.quit();
544
+ } finally {
545
+ browser = null;
546
+ }
547
+ }
489
548
  export async function switchStory() {
490
549
  var _this$currentTest, _this$currentTest$ctx, _parameters$creevey;
491
550
 
492
551
  let testOrSuite = this.currentTest;
493
552
  if (!testOrSuite) throw new Error("Can't switch story, because test context doesn't have 'currentTest' field");
494
553
  this.testScope.length = 0;
554
+ this.screenshots.length = 0;
495
555
  this.testScope.push(this.browserName);
496
556
 
497
557
  while ((_testOrSuite = testOrSuite) !== null && _testOrSuite !== void 0 && _testOrSuite.title) {
@@ -515,13 +575,6 @@ export async function switchStory() {
515
575
  ignoreElements
516
576
  } = (_parameters$creevey = parameters.creevey) !== null && _parameters$creevey !== void 0 ? _parameters$creevey : {};
517
577
  browserLogger.debug(`Switching to story ${chalk.cyan(kind)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
518
- await resetMousePosition(this.browser);
519
- await selectStory(this.browser, {
520
- id,
521
- kind,
522
- name
523
- }, waitForReady);
524
- browserLogger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
525
578
  if (captureElement) Object.defineProperty(this, 'captureElement', {
526
579
  enumerable: true,
527
580
  configurable: true,
@@ -533,6 +586,46 @@ export async function switchStory() {
533
586
  this.updateStoryArgs = updatedArgs => updateStoryArgs(this.browser, story, updatedArgs);
534
587
 
535
588
  this.testScope.reverse();
589
+ let storyPlayResolver;
590
+ let waitForComplete = new Promise(resolve => storyPlayResolver = resolve);
591
+ const unsubscribe = subscribeOn('stories', message => {
592
+ var _payload$captureEleme, _payload$ignoreElemen;
593
+
594
+ if (message.type != 'capture') return;
595
+ const {
596
+ payload = {},
597
+ payload: {
598
+ imageName
599
+ } = {}
600
+ } = message;
601
+ void takeScreenshot(this.browser, (_payload$captureEleme = payload.captureElement) !== null && _payload$captureEleme !== void 0 ? _payload$captureEleme : captureElement, (_payload$ignoreElemen = payload.ignoreElements) !== null && _payload$ignoreElemen !== void 0 ? _payload$ignoreElemen : ignoreElements).then(screenshot => {
602
+ this.screenshots.push({
603
+ imageName,
604
+ screenshot
605
+ });
606
+ void this.browser.executeAsyncScript(function (callback) {
607
+ window.__CREEVEY_HAS_PLAY_COMPLETED_YET__(callback);
608
+ }).then(isCompleted => storyPlayResolver(isCompleted));
609
+ emitStoriesMessage({
610
+ type: 'capture'
611
+ });
612
+ });
613
+ });
614
+ await resetMousePosition(this.browser);
615
+ const isCaptureCalled = await selectStory(this.browser, {
616
+ id,
617
+ kind,
618
+ name
619
+ }, waitForReady);
620
+
621
+ if (isCaptureCalled) {
622
+ while (!(await waitForComplete)) {
623
+ waitForComplete = new Promise(resolve => storyPlayResolver = resolve);
624
+ }
625
+ }
626
+
627
+ unsubscribe();
628
+ browserLogger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
536
629
  }
537
630
 
538
631
  async function insertIgnoreStyles(browser, ignoreElements) {
@@ -4,15 +4,19 @@ import { createHash } from 'crypto';
4
4
  import { mapValues, pick } from 'lodash';
5
5
  import { isDefined, isFunction, isObject } from '../types';
6
6
  import { shouldSkip, removeProps } from './utils';
7
- import { isStorybookVersionGreaterThan, isStorybookVersionLessThan } from './storybook/helpers';
8
- import { denormalizeStoryParameters } from '../shared';
7
+ import { isStorybookVersionLessThan } from './storybook/helpers';
9
8
 
10
9
  function storyTestFabric(delay, testFn) {
11
10
  return async function storyTest() {
12
11
  var _testFn$call;
13
12
 
14
13
  delay ? await new Promise(resolve => setTimeout(resolve, delay)) : void 0;
15
- await ((_testFn$call = testFn === null || testFn === void 0 ? void 0 : testFn.call(this)) !== null && _testFn$call !== void 0 ? _testFn$call : this.expect(await this.takeScreenshot()).to.matchImage());
14
+ await ((_testFn$call = testFn === null || testFn === void 0 ? void 0 : testFn.call(this)) !== null && _testFn$call !== void 0 ? _testFn$call : this.screenshots.length > 0 ? this.expect(this.screenshots.reduce((screenshots, {
15
+ imageName,
16
+ screenshot
17
+ }, index) => ({ ...screenshots,
18
+ [imageName !== null && imageName !== void 0 ? imageName : `screenshot_${index}`]: screenshot
19
+ }), {})).to.matchImages() : this.expect(await this.takeScreenshot()).to.matchImage());
16
20
  };
17
21
  }
18
22
 
@@ -38,42 +42,40 @@ function createCreeveyTest(browser, storyMeta, skipOptions, testName) {
38
42
  };
39
43
  }
40
44
 
41
- function convertStories(browsers, stories) {
45
+ function convertStories(browserName, stories) {
42
46
  const tests = {};
43
47
  (Array.isArray(stories) ? stories : Object.values(stories)).forEach(storyMeta => {
48
+ var _storyMeta$parameters;
49
+
44
50
  // TODO Skip docsOnly stories for now
45
51
  if (storyMeta.parameters.docsOnly) return;
46
- browsers.forEach(browserName => {
47
- var _storyMeta$parameters;
48
-
49
- const {
50
- delay: delayParam,
51
- tests: storyTests,
52
- skip
53
- } = (_storyMeta$parameters = storyMeta.parameters.creevey) !== null && _storyMeta$parameters !== void 0 ? _storyMeta$parameters : {};
54
- const delay = typeof delayParam == 'number' ? delayParam : delayParam !== null && delayParam !== void 0 && delayParam.for.includes(browserName) ? delayParam.ms : 0; // typeof tests === "undefined" => rootSuite -> kindSuite -> storyTest -> [browsers.png]
55
- // typeof tests === "function" => rootSuite -> kindSuite -> storyTest -> browser -> [images.png]
56
- // typeof tests === "object" => rootSuite -> kindSuite -> storySuite -> test -> [browsers.png]
57
- // typeof tests === "object" => rootSuite -> kindSuite -> storySuite -> test -> browser -> [images.png]
58
-
59
- if (!storyTests) {
60
- const test = createCreeveyTest(browserName, storyMeta, skip);
61
- tests[test.id] = { ...test,
62
- storyId: storyMeta.id,
63
- story: storyMeta,
64
- fn: storyTestFabric(delay)
65
- };
66
- return;
67
- }
52
+ const {
53
+ delay: delayParam,
54
+ tests: storyTests,
55
+ skip
56
+ } = (_storyMeta$parameters = storyMeta.parameters.creevey) !== null && _storyMeta$parameters !== void 0 ? _storyMeta$parameters : {};
57
+ const delay = typeof delayParam == 'number' ? delayParam : delayParam !== null && delayParam !== void 0 && delayParam.for.includes(browserName) ? delayParam.ms : 0; // typeof tests === "undefined" => rootSuite -> kindSuite -> storyTest -> [browsers.png]
58
+ // typeof tests === "function" => rootSuite -> kindSuite -> storyTest -> browser -> [images.png]
59
+ // typeof tests === "object" => rootSuite -> kindSuite -> storySuite -> test -> [browsers.png]
60
+ // typeof tests === "object" => rootSuite -> kindSuite -> storySuite -> test -> browser -> [images.png]
61
+
62
+ if (!storyTests) {
63
+ const test = createCreeveyTest(browserName, storyMeta, skip);
64
+ tests[test.id] = { ...test,
65
+ storyId: storyMeta.id,
66
+ story: storyMeta,
67
+ fn: storyTestFabric(delay)
68
+ };
69
+ return;
70
+ }
68
71
 
69
- Object.entries(storyTests).forEach(([testName, testFn]) => {
70
- const test = createCreeveyTest(browserName, storyMeta, skip, testName);
71
- tests[test.id] = { ...test,
72
- storyId: storyMeta.id,
73
- story: storyMeta,
74
- fn: storyTestFabric(delay, testFn)
75
- };
76
- });
72
+ Object.entries(storyTests).forEach(([testName, testFn]) => {
73
+ const test = createCreeveyTest(browserName, storyMeta, skip, testName);
74
+ tests[test.id] = { ...test,
75
+ storyId: storyMeta.id,
76
+ story: storyMeta,
77
+ fn: storyTestFabric(delay, testFn)
78
+ };
77
79
  });
78
80
  });
79
81
  return tests;
@@ -81,22 +83,24 @@ function convertStories(browsers, stories) {
81
83
 
82
84
  export async function loadTestsFromStories(browsers, provider, update) {
83
85
  const testIdsByFiles = new Map();
84
- const data = await provider(storiesByFiles => {
86
+ const stories = await provider(storiesByFiles => {
85
87
  const testsDiff = {};
86
- Array.from(storiesByFiles.entries()).forEach(([filename, stories]) => {
87
- var _testIdsByFiles$get$f, _testIdsByFiles$get;
88
-
89
- const tests = convertStories(browsers, stories);
90
- const changed = Object.keys(tests);
91
- const removed = (_testIdsByFiles$get$f = (_testIdsByFiles$get = testIdsByFiles.get(filename)) === null || _testIdsByFiles$get === void 0 ? void 0 : _testIdsByFiles$get.filter(testId => !tests[testId])) !== null && _testIdsByFiles$get$f !== void 0 ? _testIdsByFiles$get$f : [];
92
- if (changed.length == 0) testIdsByFiles.delete(filename);else testIdsByFiles.set(filename, changed);
93
- Object.assign(testsDiff, tests);
94
- removed.forEach(testId => testsDiff[testId] = undefined);
88
+ const tests = {};
89
+ browsers.forEach(browser => {
90
+ Array.from(storiesByFiles.entries()).forEach(([filename, stories]) => {
91
+ var _testIdsByFiles$get$f, _testIdsByFiles$get;
92
+
93
+ Object.assign(tests, convertStories(browser, stories));
94
+ const changed = Object.keys(tests);
95
+ const removed = (_testIdsByFiles$get$f = (_testIdsByFiles$get = testIdsByFiles.get(filename)) === null || _testIdsByFiles$get === void 0 ? void 0 : _testIdsByFiles$get.filter(testId => !tests[testId])) !== null && _testIdsByFiles$get$f !== void 0 ? _testIdsByFiles$get$f : [];
96
+ if (changed.length == 0) testIdsByFiles.delete(filename);else testIdsByFiles.set(filename, changed);
97
+ Object.assign(testsDiff, tests);
98
+ removed.forEach(testId => testsDiff[testId] = undefined);
99
+ });
95
100
  });
96
101
  update === null || update === void 0 ? void 0 : update(testsDiff);
97
102
  });
98
- const stories = isStorybookVersionLessThan(6) || isStorybookVersionGreaterThan(6, 3) ? data.stories : denormalizeStoryParameters(data);
99
- const tests = convertStories(browsers, stories);
103
+ const tests = browsers.reduce((tests, browser) => Object.assign(tests, convertStories(browser, stories)), {});
100
104
  Object.values(tests).filter(isDefined).forEach(({
101
105
  id,
102
106
  story: {
@@ -0,0 +1,61 @@
1
+ import cluster, { isMaster } from 'cluster';
2
+ import { loadStoriesFromBrowser } from '../../selenium';
3
+ import { emitStoriesMessage, sendStoriesMessage, subscribeOn, subscribeOnWorker } from '../../messages';
4
+ import { deserializeRawStories } from '../../../shared';
5
+ import { isDefined } from '../../../types';
6
+ import { logger } from '../../logger';
7
+ export async function loadStories(_config, {
8
+ port
9
+ }, storiesListener) {
10
+ if (isMaster) {
11
+ return new Promise(resolve => {
12
+ const worker = Object.values(cluster.workers).filter(isDefined).find(worker => worker.isConnected());
13
+
14
+ if (worker) {
15
+ const unsubscribe = subscribeOnWorker(worker, 'stories', message => {
16
+ if (message.type == 'set') {
17
+ const {
18
+ stories,
19
+ oldTests
20
+ } = message.payload;
21
+ if (oldTests.length > 0) logger.warn(`If you use browser stories provider of CSFv3 Storybook feature\n` + `Creevey will not load tests defined in story parameters from following stories:\n` + oldTests.join('\n'));
22
+ unsubscribe();
23
+ resolve(stories);
24
+ }
25
+ });
26
+ sendStoriesMessage(worker, {
27
+ type: 'get'
28
+ });
29
+ }
30
+
31
+ subscribeOn('stories', message => {
32
+ // TODO updates only one browser :(
33
+ if (message.type == 'update') storiesListener(new Map(message.payload));
34
+ });
35
+ });
36
+ } else {
37
+ subscribeOn('stories', message => {
38
+ if (message.type == 'get') emitStoriesMessage({
39
+ type: 'set',
40
+ payload: {
41
+ stories,
42
+ oldTests: storiesWithOldTests
43
+ }
44
+ });
45
+ if (message.type == 'update') storiesListener(new Map(message.payload));
46
+ });
47
+ const stories = deserializeRawStories(await loadStoriesFromBrowser(port));
48
+ const storiesWithOldTests = [];
49
+ Object.values(stories).forEach(story => {
50
+ var _parameters, _parameters$creevey;
51
+
52
+ if ((_parameters = story.parameters) !== null && _parameters !== void 0 && (_parameters$creevey = _parameters.creevey) !== null && _parameters$creevey !== void 0 && _parameters$creevey.tests) {
53
+ var _parameters2, _parameters2$creevey;
54
+
55
+ (_parameters2 = story.parameters) === null || _parameters2 === void 0 ? true : (_parameters2$creevey = _parameters2.creevey) === null || _parameters2$creevey === void 0 ? true : delete _parameters2$creevey.tests;
56
+ storiesWithOldTests.push(`${story.kind}/${story.name}`);
57
+ }
58
+ });
59
+ return stories;
60
+ }
61
+ }
@@ -0,0 +1,63 @@
1
+ import chokidar from 'chokidar';
2
+ import { loadStories as browserProvider } from './browser';
3
+ import { logger } from '../../logger';
4
+ import parse from '../../parser';
5
+ import { readDirRecursive } from '../../../server/utils';
6
+ import { combineParameters } from '../../../shared';
7
+ export async function loadStories(_config, {
8
+ port
9
+ }, storiesListener) {
10
+ let creeveyParamsByStoryId = {};
11
+
12
+ const mergeParamsFromTestsToStory = (story, creeveyParams) => {
13
+ if (story.parameters) {
14
+ story.parameters.creevey = combineParameters(story.parameters.creevey || {}, creeveyParams);
15
+ }
16
+ };
17
+
18
+ const stories = await browserProvider(_config, {
19
+ port
20
+ }, updatedStoriesByFiles => {
21
+ Array.from(updatedStoriesByFiles.entries()).forEach(([, storiesArray]) => {
22
+ storiesArray.forEach(story => {
23
+ const creeveyParams = creeveyParamsByStoryId[story.id];
24
+
25
+ if (creeveyParams) {
26
+ mergeParamsFromTestsToStory(story, creeveyParams);
27
+ }
28
+ });
29
+ });
30
+ storiesListener(updatedStoriesByFiles);
31
+ }); // TODO fix test files hot reloading
32
+
33
+ creeveyParamsByStoryId = await parseParams(_config
34
+ /*, (data) => console.log(data) */
35
+ );
36
+ Object.entries(stories).forEach(([storyId, story]) => {
37
+ mergeParamsFromTestsToStory(story, creeveyParamsByStoryId[storyId]);
38
+ });
39
+ return stories;
40
+ }
41
+
42
+ function parseParams(config, listener) {
43
+ if (!config.testDir) {
44
+ return Promise.resolve({});
45
+ }
46
+
47
+ const testFiles = readDirRecursive(config.testDir).filter(file => {
48
+ var _config$testRegex;
49
+
50
+ return (_config$testRegex = config.testRegex) === null || _config$testRegex === void 0 ? void 0 : _config$testRegex.test(file);
51
+ });
52
+
53
+ if (listener) {
54
+ chokidar.watch(testFiles).on('change', filePath => {
55
+ logger.debug(`changed: ${filePath}`); // doesn't work, always returns {} due modules caching
56
+ // see https://github.com/nodejs/modules/issues/307
57
+
58
+ void parse(testFiles).then(data => listener(data));
59
+ });
60
+ }
61
+
62
+ return parse(testFiles);
63
+ }
@@ -2,15 +2,15 @@
2
2
  import path from 'path';
3
3
  import { isWorker, isMaster } from 'cluster';
4
4
  import chokidar from 'chokidar';
5
- import { noop } from '../../types';
6
- import { getCreeveyCache } from '../utils';
7
- import { subscribeOn } from '../messages';
8
- import { importStorybookClientLogger, importStorybookConfig, importStorybookCoreCommon, importStorybookCoreEvents, isStorybookVersionGreaterThan, isStorybookVersionLessThan } from './helpers';
9
- import { logger } from '../logger';
10
- import { denormalizeStoryParameters } from '../../shared';
5
+ import { noop } from '../../../types';
6
+ import { getCreeveyCache } from '../../utils';
7
+ import { subscribeOn } from '../../messages';
8
+ import { importStorybookClientLogger, importStorybookConfig, importStorybookCoreCommon, importStorybookCoreEvents, isStorybookVersionGreaterThan, isStorybookVersionLessThan } from '../helpers';
9
+ import { logger } from '../../logger';
10
+ import { denormalizeStoryParameters } from '../../../shared';
11
11
 
12
12
  async function initStorybookEnvironment() {
13
- // @ts-ignore
13
+ // @ts-expect-error There is no @types/global-jsdom package
14
14
  (await import('global-jsdom')).default(undefined, {
15
15
  url: 'http://localhost'
16
16
  }); // NOTE Cutoff `jsdom` part from userAgent, because storybook check enviroment and create events channel if runs in browser
@@ -28,7 +28,7 @@ async function initStorybookEnvironment() {
28
28
  if (isWorker) logger.warn = noop; // NOTE: disable logger for 5.x storybook
29
29
 
30
30
  logger.debug = noop;
31
- return import('./entry');
31
+ return import('../entry');
32
32
  }
33
33
 
34
34
  function watchStories(channel, watcher, initialFiles) {
@@ -90,8 +90,8 @@ async function loadStoriesDirectly(config, {
90
90
  const {
91
91
  addParameters,
92
92
  configure
93
- } = await import('./entry');
94
- const requireContext = await (await import('../loaders/babel/register')).default(config, debug);
93
+ } = await import('../entry');
94
+ const requireContext = await (await import('../../loaders/babel/register')).default(config, debug);
95
95
 
96
96
  const preview = (() => {
97
97
  try {
@@ -167,7 +167,8 @@ async function loadStoriesDirectly(config, {
167
167
  void startStorybook();
168
168
  });
169
169
  void startStorybook();
170
- }
170
+ } // TODO Do we need to support multiple storybooks here?
171
+
171
172
 
172
173
  export async function loadStories(config, {
173
174
  watch,
@@ -180,7 +181,7 @@ export async function loadStories(config, {
180
181
  } = storybookApi;
181
182
  channel.removeAllListeners(Events.CURRENT_STORY_WAS_SET);
182
183
  channel.on('storiesUpdated', storiesListener);
183
- let watcher = null;
184
+ let watcher;
184
185
  if (watch) watcher = chokidar.watch([], {
185
186
  ignoreInitial: true
186
187
  });
@@ -189,7 +190,7 @@ export async function loadStories(config, {
189
190
  const stories = isStorybookVersionLessThan(6) || isStorybookVersionGreaterThan(6, 3) ? data.stories : denormalizeStoryParameters(data);
190
191
  const files = new Set(Object.values(stories).map(story => story.parameters.fileName));
191
192
  if (watcher) channel.on(Events.SET_STORIES, watchStories(channel, watcher, files));
192
- resolve(data);
193
+ resolve(stories);
193
194
  });
194
195
  });
195
196
  if (config.useWebpackToExtractTests) loadStoriesFromBundle(watch);else void loadStoriesDirectly(config, {
@@ -197,4 +198,20 @@ export async function loadStories(config, {
197
198
  debug
198
199
  });
199
200
  return loadPromise;
201
+ }
202
+ export async function extractStoriesData(config, {
203
+ watch,
204
+ debug
205
+ }) {
206
+ const storybookApi = await initStorybookEnvironment();
207
+ const Events = await importStorybookCoreEvents();
208
+ const {
209
+ channel
210
+ } = storybookApi;
211
+ channel.removeAllListeners(Events.CURRENT_STORY_WAS_SET);
212
+ const loadPromise = new Promise(resolve => channel.once(Events.SET_STORIES, resolve));
213
+ if (config.useWebpackToExtractTests) loadStoriesFromBundle(watch);else void loadStoriesDirectly(config, {
214
+ debug
215
+ });
216
+ return loadPromise;
200
217
  }
@@ -1,4 +1,4 @@
1
- import { createWriteStream, existsSync, readFileSync, unlink } from 'fs';
1
+ import { createWriteStream, existsSync, readFileSync, readdirSync, unlink } from 'fs';
2
2
  import cluster from 'cluster';
3
3
  import { isDefined, noop, isFunction } from '../types';
4
4
  import { emitShutdownMessage, sendShutdownMessage } from './messages';
@@ -138,4 +138,9 @@ export function removeProps(obj, propPath) {
138
138
  if (typeof prop == 'string') delete obj[prop];
139
139
  if (isFunction(prop)) Object.keys(obj).filter(prop).forEach(key => delete obj[key]);
140
140
  }
141
+ }
142
+ export function readDirRecursive(dirPath) {
143
+ return [].concat(...readdirSync(dirPath, {
144
+ withFileTypes: true
145
+ }).map(dirent => dirent.isDirectory() ? readDirRecursive(`${dirPath}/${dirent.name}`) : [`${dirPath}/${dirent.name}`]));
141
146
  }
@@ -47,14 +47,10 @@ function removeTestOrSuite(testOrSuite) {
47
47
 
48
48
  export async function addTestsFromStories(rootSuite, config, {
49
49
  browser,
50
- watch,
51
- debug
50
+ ...options
52
51
  }) {
53
52
  const mochaTestsById = new Map();
54
- const tests = await loadTestsFromStories([browser], listener => config.storiesProvider(config, {
55
- watch,
56
- debug
57
- }, listener), testsDiff => Object.entries(testsDiff).forEach(([id, newTest]) => {
53
+ const tests = await loadTestsFromStories([browser], listener => config.storiesProvider(config, options, listener), testsDiff => Object.entries(testsDiff).forEach(([id, newTest]) => {
58
54
  const oldTest = mochaTestsById.get(id);
59
55
  mochaTestsById.delete(id);
60
56
  if (oldTest) removeTestOrSuite(oldTest);