creevey 0.7.39 → 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 (102) hide show
  1. package/CHANGELOG.md +12 -2
  2. package/README.md +1 -1
  3. package/docs/config.md +37 -5
  4. package/docs/grid.md +2 -1
  5. package/lib/cjs/client/addon/Manager.js +3 -2
  6. package/lib/cjs/client/addon/preset.js +1 -0
  7. package/lib/cjs/client/addon/readyForCapture.js +12 -0
  8. package/lib/cjs/client/addon/utils.js +1 -41
  9. package/lib/cjs/client/addon/withCreevey.js +313 -41
  10. package/lib/cjs/client/shared/components/ImagesView/BlendView.js +3 -3
  11. package/lib/cjs/client/shared/components/ImagesView/SideBySideView.js +3 -3
  12. package/lib/cjs/client/shared/components/ImagesView/SlideView.js +4 -3
  13. package/lib/cjs/client/shared/components/ImagesView/SwapView.js +3 -3
  14. package/lib/cjs/client/shared/helpers.js +1 -1
  15. package/lib/cjs/client/web/1.js +2 -2
  16. package/lib/cjs/client/web/2.js +1 -1
  17. package/lib/cjs/client/web/main.js +6 -6
  18. package/lib/cjs/index.js +27 -9
  19. package/lib/cjs/server/config.js +7 -3
  20. package/lib/cjs/server/extract.js +11 -4
  21. package/lib/cjs/server/index.js +2 -4
  22. package/lib/cjs/server/loaders/babel/register.js +2 -1
  23. package/lib/cjs/server/master/index.js +3 -9
  24. package/lib/cjs/server/master/master.js +1 -0
  25. package/lib/cjs/server/master/pool.js +29 -29
  26. package/lib/cjs/server/master/server.js +75 -3
  27. package/lib/cjs/server/messages.js +124 -12
  28. package/lib/cjs/server/parser.js +85 -0
  29. package/lib/cjs/server/selenium/browser.js +119 -21
  30. package/lib/cjs/server/selenium/selenoid.js +1 -1
  31. package/lib/cjs/server/stories.js +49 -58
  32. package/lib/cjs/server/storybook/entry.js +5 -4
  33. package/lib/cjs/server/storybook/helpers.js +11 -3
  34. package/lib/cjs/server/storybook/providers/browser.js +78 -0
  35. package/lib/cjs/server/storybook/providers/hybrid.js +79 -0
  36. package/lib/cjs/server/storybook/{nodejs-provider.js → providers/nodejs.js} +42 -18
  37. package/lib/cjs/server/utils.js +32 -2
  38. package/lib/cjs/server/worker/helpers.js +2 -6
  39. package/lib/cjs/server/worker/worker.js +15 -3
  40. package/lib/cjs/shared.js +107 -0
  41. package/lib/cjs/types.js +5 -0
  42. package/lib/esm/client/addon/Manager.js +3 -3
  43. package/lib/esm/client/addon/preset.js +1 -0
  44. package/lib/esm/client/addon/readyForCapture.js +5 -0
  45. package/lib/esm/client/addon/utils.js +1 -33
  46. package/lib/esm/client/addon/withCreevey.js +303 -41
  47. package/lib/esm/client/shared/components/ImagesView/BlendView.js +2 -3
  48. package/lib/esm/client/shared/components/ImagesView/SideBySideView.js +2 -3
  49. package/lib/esm/client/shared/components/ImagesView/SlideView.js +3 -3
  50. package/lib/esm/client/shared/components/ImagesView/SwapView.js +2 -3
  51. package/lib/esm/client/shared/helpers.js +1 -1
  52. package/lib/esm/index.js +6 -3
  53. package/lib/esm/server/config.js +7 -5
  54. package/lib/esm/server/extract.js +8 -4
  55. package/lib/esm/server/index.js +2 -3
  56. package/lib/esm/server/loaders/babel/register.js +3 -2
  57. package/lib/esm/server/master/index.js +4 -10
  58. package/lib/esm/server/master/master.js +1 -0
  59. package/lib/esm/server/master/pool.js +31 -31
  60. package/lib/esm/server/master/server.js +73 -5
  61. package/lib/esm/server/messages.js +118 -12
  62. package/lib/esm/server/parser.js +63 -0
  63. package/lib/esm/server/selenium/browser.js +116 -23
  64. package/lib/esm/server/selenium/selenoid.js +1 -1
  65. package/lib/esm/server/stories.js +51 -58
  66. package/lib/esm/server/storybook/entry.js +4 -4
  67. package/lib/esm/server/storybook/helpers.js +9 -3
  68. package/lib/esm/server/storybook/providers/browser.js +61 -0
  69. package/lib/esm/server/storybook/providers/hybrid.js +63 -0
  70. package/lib/esm/server/storybook/{nodejs-provider.js → providers/nodejs.js} +40 -18
  71. package/lib/esm/server/utils.js +29 -2
  72. package/lib/esm/server/worker/helpers.js +2 -6
  73. package/lib/esm/server/worker/worker.js +16 -4
  74. package/lib/esm/shared.js +76 -0
  75. package/lib/esm/types.js +3 -0
  76. package/lib/types/client/addon/preset.d.ts +2 -0
  77. package/lib/types/client/addon/readyForCapture.d.ts +6 -0
  78. package/lib/types/client/addon/utils.d.ts +1 -5
  79. package/lib/types/client/addon/withCreevey.d.ts +13 -2
  80. package/lib/types/client/web/CreeveyView/SideBar/SuiteLink.d.ts +2 -2
  81. package/lib/types/index.d.ts +2 -1
  82. package/lib/types/server/config.d.ts +1 -1
  83. package/lib/types/server/master/master.d.ts +1 -0
  84. package/lib/types/server/master/pool.d.ts +1 -0
  85. package/lib/types/server/master/server.d.ts +1 -1
  86. package/lib/types/server/messages.d.ts +12 -2
  87. package/lib/types/server/parser.d.ts +12 -0
  88. package/lib/types/server/selenium/browser.d.ts +5 -2
  89. package/lib/types/server/stories.d.ts +1 -2
  90. package/lib/types/server/storybook/entry.d.ts +13 -9
  91. package/lib/types/server/storybook/helpers.d.ts +1 -0
  92. package/lib/types/server/storybook/providers/browser.d.ts +4 -0
  93. package/lib/types/server/storybook/providers/hybrid.d.ts +4 -0
  94. package/lib/types/server/storybook/providers/nodejs.d.ts +9 -0
  95. package/lib/types/server/utils.d.ts +2 -0
  96. package/lib/types/server/worker/helpers.d.ts +2 -1
  97. package/lib/types/shared.d.ts +16 -0
  98. package/lib/types/types.d.ts +33 -4
  99. package/package.json +28 -18
  100. package/storybook-static/stories.json +4 -513
  101. package/types/mocha.d.ts +1 -0
  102. 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) {
@@ -30,7 +30,7 @@ async function createSelenoidConfig(browsers, {
30
30
  selenoidConfig[browserName].versions[version] = {
31
31
  image: useDocker ? dockerImage : webdriverCommand,
32
32
  port: '4444',
33
- path: !useDocker || ['chrome', 'opera', 'webkit'].includes(browserName) ? '/' : '/wd/hub'
33
+ path: !useDocker || ['chrome', 'opera', 'webkit', 'MicrosoftEdge'].includes(browserName) ? '/' : '/wd/hub'
34
34
  };
35
35
  });
36
36
  await mkdirAsync(selenoidConfigDir, {
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import { mkdirSync, writeFileSync } from 'fs';
3
3
  import { createHash } from 'crypto';
4
- import { mapValues, mergeWith, pick } from 'lodash';
4
+ import { mapValues, pick } from 'lodash';
5
5
  import { isDefined, isFunction, isObject } from '../types';
6
6
  import { shouldSkip, removeProps } from './utils';
7
7
  import { isStorybookVersionLessThan } from './storybook/helpers';
@@ -11,7 +11,12 @@ function storyTestFabric(delay, testFn) {
11
11
  var _testFn$call;
12
12
 
13
13
  delay ? await new Promise(resolve => setTimeout(resolve, delay)) : void 0;
14
- 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());
15
20
  };
16
21
  }
17
22
 
@@ -37,77 +42,65 @@ function createCreeveyTest(browser, storyMeta, skipOptions, testName) {
37
42
  };
38
43
  }
39
44
 
40
- function convertStories(browsers, stories) {
45
+ function convertStories(browserName, stories) {
41
46
  const tests = {};
42
47
  (Array.isArray(stories) ? stories : Object.values(stories)).forEach(storyMeta => {
48
+ var _storyMeta$parameters;
49
+
43
50
  // TODO Skip docsOnly stories for now
44
51
  if (storyMeta.parameters.docsOnly) return;
45
- browsers.forEach(browserName => {
46
- var _storyMeta$parameters;
47
-
48
- const {
49
- delay: delayParam,
50
- tests: storyTests,
51
- skip
52
- } = (_storyMeta$parameters = storyMeta.parameters.creevey) !== null && _storyMeta$parameters !== void 0 ? _storyMeta$parameters : {};
53
- 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]
54
- // typeof tests === "function" => rootSuite -> kindSuite -> storyTest -> browser -> [images.png]
55
- // typeof tests === "object" => rootSuite -> kindSuite -> storySuite -> test -> [browsers.png]
56
- // typeof tests === "object" => rootSuite -> kindSuite -> storySuite -> test -> browser -> [images.png]
57
-
58
- if (!storyTests) {
59
- const test = createCreeveyTest(browserName, storyMeta, skip);
60
- tests[test.id] = { ...test,
61
- storyId: storyMeta.id,
62
- story: storyMeta,
63
- fn: storyTestFabric(delay)
64
- };
65
- return;
66
- }
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
+ }
67
71
 
68
- Object.entries(storyTests).forEach(([testName, testFn]) => {
69
- const test = createCreeveyTest(browserName, storyMeta, skip, testName);
70
- tests[test.id] = { ...test,
71
- storyId: storyMeta.id,
72
- story: storyMeta,
73
- fn: storyTestFabric(delay, testFn)
74
- };
75
- });
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
+ };
76
79
  });
77
80
  });
78
81
  return tests;
79
- } // TODO use the storybook version, after the fix of skip option API
80
-
81
-
82
- export function flatStories({
83
- globalParameters,
84
- kindParameters,
85
- stories
86
- }) {
87
- Object.values(stories).forEach(story => {
88
- // NOTE: Copy-paste merge parameters from storybook
89
- story.parameters = mergeWith({}, globalParameters, kindParameters[story.kind], story.parameters, (objValue, srcValue) => Array.isArray(objValue) ? objValue.concat(srcValue) : undefined);
90
- });
91
- return stories;
92
82
  }
83
+
93
84
  export async function loadTestsFromStories(browsers, provider, update) {
94
85
  const testIdsByFiles = new Map();
95
- const data = await provider(storiesByFiles => {
86
+ const stories = await provider(storiesByFiles => {
96
87
  const testsDiff = {};
97
- Array.from(storiesByFiles.entries()).forEach(([filename, stories]) => {
98
- var _testIdsByFiles$get$f, _testIdsByFiles$get;
99
-
100
- const tests = convertStories(browsers, stories);
101
- const changed = Object.keys(tests);
102
- 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 : [];
103
- if (changed.length == 0) testIdsByFiles.delete(filename);else testIdsByFiles.set(filename, changed);
104
- Object.assign(testsDiff, tests);
105
- 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
+ });
106
100
  });
107
101
  update === null || update === void 0 ? void 0 : update(testsDiff);
108
102
  });
109
- const stories = isStorybookVersionLessThan(6) ? data.stories : flatStories(data);
110
- const tests = convertStories(browsers, stories);
103
+ const tests = browsers.reduce((tests, browser) => Object.assign(tests, convertStories(browser, stories)), {});
111
104
  Object.values(tests).filter(isDefined).forEach(({
112
105
  id,
113
106
  story: {
@@ -1,5 +1,6 @@
1
1
  var _api$channel, _api$context;
2
2
 
3
+ import { addons } from '@storybook/addons';
3
4
  import { getStorybookFramework, isStorybookVersionLessThan, resolveFromStorybook } from './helpers';
4
5
  const framework = getStorybookFramework(); // eslint-disable-next-line @typescript-eslint/no-var-requires
5
6
 
@@ -8,10 +9,8 @@ const core = require(resolveFromStorybook('@storybook/core')); //@ts-expect-erro
8
9
 
9
10
 
10
11
  const start = isStorybookVersionLessThan(6, 2) ? core.default.start : core.start;
11
- const api = start(() => void 0); //@ts-expect-error: 6.x has { channel }, but 5.x has { context: { channel } }
12
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
13
-
14
- export const channel = (_api$channel = api.channel) !== null && _api$channel !== void 0 ? _api$channel : (_api$context = api.context) === null || _api$context === void 0 ? void 0 : _api$context.channel;
12
+ const api = start(() => void 0);
13
+ export const channel = isStorybookVersionLessThan(6, 4) ? (_api$channel = api.channel) !== null && _api$channel !== void 0 ? _api$channel : (_api$context = api.context) === null || _api$context === void 0 ? void 0 : _api$context.channel : addons.getChannel();
15
14
  export const clientApi = api.clientApi;
16
15
  export const forceReRender = api.forceReRender;
17
16
  export const storiesOf = (kind, m) => {
@@ -23,6 +22,7 @@ export const configure = (...args) => {
23
22
  if (isStorybookVersionLessThan(5, 2)) {
24
23
  //NOTE: Storybook <= 5.1 pass args as is
25
24
  //@ts-expect-error: ignore it
25
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
26
26
  return api.configApi.configure(...args);
27
27
  }
28
28
 
@@ -45,16 +45,22 @@ export function isStorybookVersionLessThan(major, minor) {
45
45
  const [sbMajor, sbMinor] = ((_process$env$__CREEVE = process.env.__CREEVEY_STORYBOOK_VERSION__) !== null && _process$env$__CREEVE !== void 0 ? _process$env$__CREEVE : getStorybookVersion()).split('.');
46
46
  return Number(sbMajor) < major || minor != undefined && Number(sbMajor) == major && Number(sbMinor) < minor;
47
47
  }
48
- export function isStorybookVersion(major, minor) {
48
+ export function isStorybookVersionGreaterThan(major, minor) {
49
49
  var _process$env$__CREEVE2;
50
50
 
51
51
  const [sbMajor, sbMinor] = ((_process$env$__CREEVE2 = process.env.__CREEVEY_STORYBOOK_VERSION__) !== null && _process$env$__CREEVE2 !== void 0 ? _process$env$__CREEVE2 : getStorybookVersion()).split('.');
52
+ return Number(sbMajor) > major || minor != undefined && Number(sbMajor) == major && Number(sbMinor) > minor;
53
+ }
54
+ export function isStorybookVersion(major, minor) {
55
+ var _process$env$__CREEVE3;
56
+
57
+ const [sbMajor, sbMinor] = ((_process$env$__CREEVE3 = process.env.__CREEVEY_STORYBOOK_VERSION__) !== null && _process$env$__CREEVE3 !== void 0 ? _process$env$__CREEVE3 : getStorybookVersion()).split('.');
52
58
  return Number(sbMajor) == major || minor != undefined && Number(sbMajor) == major && Number(sbMinor) == minor;
53
59
  }
54
60
  export function getStorybookFramework() {
55
- var _process$env$__CREEVE3;
61
+ var _process$env$__CREEVE4;
56
62
 
57
- const framework = (_process$env$__CREEVE3 = process.env.__CREEVEY_STORYBOOK_FRAMEWORK__) !== null && _process$env$__CREEVE3 !== void 0 ? _process$env$__CREEVE3 : supportedFrameworks.find(framework => {
63
+ const framework = (_process$env$__CREEVE4 = process.env.__CREEVEY_STORYBOOK_FRAMEWORK__) !== null && _process$env$__CREEVE4 !== void 0 ? _process$env$__CREEVE4 : supportedFrameworks.find(framework => {
58
64
  try {
59
65
  return require.resolve(resolveFromStorybook(`@storybook/${framework}`));
60
66
  } catch (_) {
@@ -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
+ }