creevey 0.10.0-beta.3 → 0.10.0-beta.30

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 (186) hide show
  1. package/dist/client/addon/components/Addon.js +17 -7
  2. package/dist/client/addon/components/Addon.js.map +1 -1
  3. package/dist/client/addon/components/Panel.js +2 -2
  4. package/dist/client/addon/components/Panel.js.map +1 -1
  5. package/dist/client/addon/components/Tools.js +17 -7
  6. package/dist/client/addon/components/Tools.js.map +1 -1
  7. package/dist/client/addon/withCreevey.d.ts +1 -0
  8. package/dist/client/addon/withCreevey.js +10 -1
  9. package/dist/client/addon/withCreevey.js.map +1 -1
  10. package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
  11. package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
  12. package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
  13. package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
  14. package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
  15. package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
  16. package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
  17. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  18. package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
  19. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  20. package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
  21. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  22. package/dist/client/shared/components/ResultsPage.js +43 -13
  23. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  24. package/dist/client/shared/creeveyClientApi.js +8 -1
  25. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  26. package/dist/client/shared/helpers.d.ts +1 -3
  27. package/dist/client/shared/helpers.js +4 -19
  28. package/dist/client/shared/helpers.js.map +1 -1
  29. package/dist/client/web/CreeveyApp.js +41 -14
  30. package/dist/client/web/CreeveyApp.js.map +1 -1
  31. package/dist/client/web/CreeveyContext.d.ts +5 -0
  32. package/dist/client/web/CreeveyContext.js +20 -7
  33. package/dist/client/web/CreeveyContext.js.map +1 -1
  34. package/dist/client/web/CreeveyLoader.js +2 -2
  35. package/dist/client/web/CreeveyLoader.js.map +1 -1
  36. package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
  37. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  38. package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
  39. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  40. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
  41. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  42. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
  43. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  44. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
  45. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  46. package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
  47. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  48. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  49. package/dist/client/web/KeyboardEventsContext.js +79 -64
  50. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  51. package/dist/client/web/assets/index-C5QCFtF-.js +595 -0
  52. package/dist/client/web/index.html +1 -1
  53. package/dist/client/web/index.js +17 -7
  54. package/dist/client/web/index.js.map +1 -1
  55. package/dist/client/web/themes.d.ts +2 -0
  56. package/dist/client/web/themes.js +22 -0
  57. package/dist/client/web/themes.js.map +1 -0
  58. package/dist/creevey.js +16 -9
  59. package/dist/creevey.js.map +1 -1
  60. package/dist/index.d.ts +1 -0
  61. package/dist/server/config.d.ts +1 -1
  62. package/dist/server/config.js +29 -7
  63. package/dist/server/config.js.map +1 -1
  64. package/dist/server/connection.d.ts +3 -0
  65. package/dist/server/connection.js +28 -0
  66. package/dist/server/connection.js.map +1 -0
  67. package/dist/server/docker.js +38 -21
  68. package/dist/server/docker.js.map +1 -1
  69. package/dist/server/index.js +63 -11
  70. package/dist/server/index.js.map +1 -1
  71. package/dist/server/logger.d.ts +2 -1
  72. package/dist/server/logger.js +7 -3
  73. package/dist/server/logger.js.map +1 -1
  74. package/dist/server/master/api.js +1 -1
  75. package/dist/server/master/api.js.map +1 -1
  76. package/dist/server/master/pool.d.ts +4 -3
  77. package/dist/server/master/pool.js +12 -63
  78. package/dist/server/master/pool.js.map +1 -1
  79. package/dist/server/master/queue.d.ts +13 -0
  80. package/dist/server/master/queue.js +71 -0
  81. package/dist/server/master/queue.js.map +1 -0
  82. package/dist/server/master/runner.d.ts +1 -0
  83. package/dist/server/master/runner.js +4 -1
  84. package/dist/server/master/runner.js.map +1 -1
  85. package/dist/server/master/server.js +1 -1
  86. package/dist/server/master/server.js.map +1 -1
  87. package/dist/server/master/start.js +13 -11
  88. package/dist/server/master/start.js.map +1 -1
  89. package/dist/server/playwright/docker-file.d.ts +2 -1
  90. package/dist/server/playwright/docker-file.js +7 -5
  91. package/dist/server/playwright/docker-file.js.map +1 -1
  92. package/dist/server/playwright/internal.d.ts +5 -4
  93. package/dist/server/playwright/internal.js +91 -71
  94. package/dist/server/playwright/internal.js.map +1 -1
  95. package/dist/server/playwright/webdriver.d.ts +1 -1
  96. package/dist/server/playwright/webdriver.js +1 -1
  97. package/dist/server/playwright/webdriver.js.map +1 -1
  98. package/dist/server/providers/browser.js +6 -4
  99. package/dist/server/providers/browser.js.map +1 -1
  100. package/dist/server/providers/hybrid.js +1 -1
  101. package/dist/server/providers/hybrid.js.map +1 -1
  102. package/dist/server/reporter.js +13 -9
  103. package/dist/server/reporter.js.map +1 -1
  104. package/dist/server/selenium/internal.d.ts +3 -4
  105. package/dist/server/selenium/internal.js +127 -99
  106. package/dist/server/selenium/internal.js.map +1 -1
  107. package/dist/server/selenium/selenoid.js +9 -6
  108. package/dist/server/selenium/selenoid.js.map +1 -1
  109. package/dist/server/selenium/webdriver.d.ts +1 -1
  110. package/dist/server/selenium/webdriver.js +1 -1
  111. package/dist/server/selenium/webdriver.js.map +1 -1
  112. package/dist/server/telemetry.js +7 -3
  113. package/dist/server/telemetry.js.map +1 -1
  114. package/dist/server/testsFiles/parser.js +44 -2
  115. package/dist/server/testsFiles/parser.js.map +1 -1
  116. package/dist/server/utils.d.ts +20 -1
  117. package/dist/server/utils.js +82 -7
  118. package/dist/server/utils.js.map +1 -1
  119. package/dist/server/webdriver.d.ts +3 -4
  120. package/dist/server/webdriver.js +10 -9
  121. package/dist/server/webdriver.js.map +1 -1
  122. package/dist/server/worker/chai-image.d.ts +1 -2
  123. package/dist/server/worker/chai-image.js +4 -3
  124. package/dist/server/worker/chai-image.js.map +1 -1
  125. package/dist/server/worker/match-image.d.ts +4 -4
  126. package/dist/server/worker/match-image.js +7 -4
  127. package/dist/server/worker/match-image.js.map +1 -1
  128. package/dist/server/worker/start.js +24 -14
  129. package/dist/server/worker/start.js.map +1 -1
  130. package/dist/shared/index.d.ts +1 -1
  131. package/dist/types.d.ts +38 -13
  132. package/dist/types.js.map +1 -1
  133. package/docs/config.md +3 -0
  134. package/package.json +66 -64
  135. package/src/client/addon/components/Panel.tsx +2 -2
  136. package/src/client/addon/withCreevey.ts +8 -1
  137. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  138. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  139. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
  140. package/src/client/shared/components/ResultsPage.tsx +31 -8
  141. package/src/client/shared/creeveyClientApi.ts +9 -1
  142. package/src/client/shared/helpers.ts +4 -24
  143. package/src/client/web/CreeveyApp.tsx +26 -8
  144. package/src/client/web/CreeveyContext.tsx +9 -0
  145. package/src/client/web/CreeveyLoader.tsx +1 -1
  146. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  147. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  148. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  149. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  150. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  151. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  152. package/src/client/web/themes.ts +24 -0
  153. package/src/creevey.ts +16 -10
  154. package/src/server/config.ts +30 -8
  155. package/src/server/connection.ts +26 -0
  156. package/src/server/docker.ts +42 -24
  157. package/src/server/index.ts +73 -14
  158. package/src/server/logger.ts +6 -2
  159. package/src/server/master/api.ts +1 -1
  160. package/src/server/master/pool.ts +22 -56
  161. package/src/server/master/queue.ts +77 -0
  162. package/src/server/master/runner.ts +4 -1
  163. package/src/server/master/server.ts +1 -1
  164. package/src/server/master/start.ts +16 -11
  165. package/src/server/playwright/docker-file.ts +8 -5
  166. package/src/server/playwright/internal.ts +91 -78
  167. package/src/server/playwright/webdriver.ts +2 -2
  168. package/src/server/providers/browser.ts +6 -4
  169. package/src/server/providers/hybrid.ts +1 -1
  170. package/src/server/reporter.ts +15 -9
  171. package/src/server/selenium/internal.ts +131 -107
  172. package/src/server/selenium/selenoid.ts +9 -7
  173. package/src/server/selenium/webdriver.ts +2 -2
  174. package/src/server/telemetry.ts +7 -3
  175. package/src/server/testsFiles/parser.ts +51 -1
  176. package/src/server/utils.ts +87 -8
  177. package/src/server/webdriver.ts +11 -16
  178. package/src/server/worker/chai-image.ts +4 -4
  179. package/src/server/worker/match-image.ts +12 -8
  180. package/src/server/worker/start.ts +25 -16
  181. package/src/shared/index.ts +1 -1
  182. package/src/types.ts +40 -15
  183. package/types/global.d.ts +1 -0
  184. package/.yarnrc.yml +0 -1
  185. package/chromatic.config.json +0 -5
  186. package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
@@ -0,0 +1,77 @@
1
+ import cluster from 'cluster';
2
+ import { isWorkerMessage, Worker, WorkerMessage } from '../../types.js';
3
+ import { gracefullyKill, isShuttingDown } from '../utils.js';
4
+
5
+ const FORK_RETRIES = 5;
6
+
7
+ type MaybeWorker = Worker | { error: string };
8
+
9
+ export class WorkerQueue {
10
+ private isProcessing = false;
11
+ private queue: {
12
+ browser: string;
13
+ storybookUrl: string;
14
+ gridUrl?: string;
15
+ retry: number;
16
+ resolve: (mw: MaybeWorker) => void;
17
+ }[] = [];
18
+
19
+ // TODO Add concurrency
20
+ constructor(private useQueue: boolean) {}
21
+
22
+ async forkWorker(browser: string, storybookUrl: string, gridUrl?: string, retry = 0): Promise<MaybeWorker> {
23
+ return new Promise<MaybeWorker>((resolve) => {
24
+ this.queue.push({ browser, storybookUrl, gridUrl, retry, resolve });
25
+
26
+ void this.process();
27
+ });
28
+ }
29
+
30
+ private async process() {
31
+ if (this.useQueue && this.isProcessing) return;
32
+
33
+ const { browser, storybookUrl, gridUrl, retry, resolve } = this.queue.pop() ?? {};
34
+
35
+ if (browser == undefined || storybookUrl == undefined || retry == undefined || resolve == undefined) return;
36
+
37
+ if (isShuttingDown.current) {
38
+ resolve({ error: 'Master process is shutting down' });
39
+ return;
40
+ }
41
+
42
+ this.isProcessing = true;
43
+
44
+ cluster.setupPrimary({
45
+ args: [
46
+ '--browser',
47
+ browser,
48
+ ...(gridUrl ? ['--gridUrl', gridUrl] : []),
49
+ ...process.argv.slice(2),
50
+ '--storybookUrl',
51
+ storybookUrl,
52
+ ],
53
+ });
54
+ const worker = cluster.fork();
55
+ const message = await new Promise((resolve: (value: WorkerMessage) => void) => {
56
+ const readyHandler = (message: unknown): void => {
57
+ if (!isWorkerMessage(message) || message.type == 'port') return;
58
+ worker.off('message', readyHandler);
59
+ resolve(message);
60
+ };
61
+ worker.on('message', readyHandler);
62
+ });
63
+
64
+ if (message.type == 'error') {
65
+ gracefullyKill(worker);
66
+
67
+ if (retry == FORK_RETRIES) resolve(message.payload);
68
+ else this.queue.push({ browser, storybookUrl, gridUrl, retry: retry + 1, resolve });
69
+ } else {
70
+ resolve(worker);
71
+ }
72
+
73
+ this.isProcessing = false;
74
+
75
+ setImmediate(() => void this.process());
76
+ }
77
+ }
@@ -13,12 +13,14 @@ import {
13
13
  TestMeta,
14
14
  } from '../../types.js';
15
15
  import Pool from './pool.js';
16
+ import { WorkerQueue } from './queue.js';
16
17
 
17
18
  export default class Runner extends EventEmitter {
18
19
  private failFast: boolean;
19
20
  private screenDir: string;
20
21
  private reportDir: string;
21
22
  private browsers: string[];
23
+ private scheduler: WorkerQueue;
22
24
  private pools: Record<string, Pool> = {};
23
25
  tests: Partial<Record<string, ServerTest>> = {};
24
26
  public get isRunning(): boolean {
@@ -30,9 +32,10 @@ export default class Runner extends EventEmitter {
30
32
  this.failFast = config.failFast;
31
33
  this.screenDir = config.screenDir;
32
34
  this.reportDir = config.reportDir;
35
+ this.scheduler = new WorkerQueue(config.useWorkerQueue);
33
36
  this.browsers = Object.keys(config.browsers);
34
37
  this.browsers
35
- .map((browser) => (this.pools[browser] = new Pool(config, browser, gridUrl)))
38
+ .map((browser) => (this.pools[browser] = new Pool(this.scheduler, config, browser, gridUrl)))
36
39
  .map((pool) => pool.on('test', this.handlePoolMessage));
37
40
  }
38
41
 
@@ -96,7 +96,7 @@ export function start(reportDir: string, port: number, ui: boolean): (api: Creev
96
96
  app.use(mount('/report', serve(reportDir)));
97
97
 
98
98
  wss.on('error', (error) => {
99
- logger.error(error);
99
+ logger().error(error);
100
100
  });
101
101
 
102
102
  server.listen(port);
@@ -14,13 +14,14 @@ import { sendScreenshotsCount } from '../telemetry.js';
14
14
  const importMetaUrl = pathToFileURL(__filename).href;
15
15
 
16
16
  async function copyStatics(reportDir: string): Promise<void> {
17
- const clientDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../client/web');
18
- const files = (await readdir(clientDir, { withFileTypes: true }))
19
- .filter((dirent) => dirent.isFile() && !dirent.name.endsWith('.d.ts') && !dirent.name.endsWith('.tsx'))
17
+ const clientDir = path.join(path.dirname(fileURLToPath(importMetaUrl)), '../../../dist/client/web');
18
+ const assets = (await readdir(path.join(clientDir, 'assets'), { withFileTypes: true }))
19
+ .filter((dirent) => dirent.isFile())
20
20
  .map((dirent) => dirent.name);
21
- await mkdir(reportDir, { recursive: true });
22
- for (const file of files) {
23
- await copyFile(path.join(clientDir, file), path.join(reportDir, file));
21
+ await mkdir(path.join(reportDir, 'assets'), { recursive: true });
22
+ await copyFile(path.join(clientDir, 'index.html'), path.join(reportDir, 'index.html'));
23
+ for (const asset of assets) {
24
+ await copyFile(path.join(clientDir, 'assets', asset), path.join(reportDir, 'assets', asset));
24
25
  }
25
26
  }
26
27
 
@@ -42,7 +43,10 @@ function outputUnnecessaryImages(imagesDir: string, images: Set<string>): void {
42
43
  .map((imagePath) => path.posix.relative(imagesDir, imagePath))
43
44
  .filter((imagePath) => !images.has(imagePath));
44
45
  if (unnecessaryImages.length > 0) {
45
- logger.warn('We found unnecessary screenshot images, those can be safely removed:\n', unnecessaryImages.join('\n'));
46
+ logger().warn(
47
+ 'We found unnecessary screenshot images, those can be safely removed:\n',
48
+ unnecessaryImages.join('\n'),
49
+ );
46
50
  }
47
51
  }
48
52
 
@@ -81,10 +85,10 @@ export async function start(
81
85
 
82
86
  if (options.ui) {
83
87
  resolveApi(creeveyApi(runner));
84
- logger.info(`Started on http://localhost:${options.port}`);
88
+ logger().info(`Started on http://localhost:${options.port}`);
85
89
  } else {
86
90
  if (Object.values(runner.status.tests).filter((test) => test && !test.skip).length == 0) {
87
- logger.warn("Don't have any tests to run");
91
+ logger().warn("Don't have any tests to run");
88
92
 
89
93
  void shutdownWorkers().then(() => process.exit());
90
94
  return;
@@ -101,10 +105,11 @@ export async function start(
101
105
  void sendScreenshotsCount(config, options, runner.status)
102
106
  .catch((reason: unknown) => {
103
107
  const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
104
- logger.warn(`Can't send telemetry: ${error}`);
108
+ logger().warn(`Can't send telemetry: ${error}`);
105
109
  })
106
110
  .finally(() => {
107
- void shutdownWorkers().then(() => process.exit());
111
+ // NOTE: Take some time to kill processes
112
+ void shutdownWorkers().then(() => setTimeout(() => process.exit(), 500));
108
113
  });
109
114
  });
110
115
  // TODO grep
@@ -1,8 +1,10 @@
1
1
  import semver from 'semver';
2
2
  import { exec } from 'shelljs';
3
+ import { LaunchOptions } from 'playwright-core';
4
+ import { resolvePlaywrightBrowserType } from '../utils';
3
5
 
4
6
  // TODO Support custom docker images
5
- export function playwrightDockerFile(browser: string, version: string): string {
7
+ export function playwrightDockerFile(browser: string, version: string, serverOptions?: LaunchOptions): string {
6
8
  const sv = semver.coerce(version);
7
9
 
8
10
  let npmRegistry;
@@ -13,19 +15,20 @@ export function playwrightDockerFile(browser: string, version: string): string {
13
15
  }
14
16
 
15
17
  return `
16
- FROM mcr.microsoft.com/playwright:v${sv?.format() ?? version}
18
+ FROM node:lts
17
19
 
18
20
  WORKDIR /creevey
19
21
 
20
22
  RUN echo "{ \\"type\\": \\"module\\" }" > package.json && \\
21
- echo "import { ${browser} as browser } from 'playwright-core';" >> index.js && \\
22
- echo "const ws = await browser.launchServer({ port: 4444, wsPath: 'creevey' })" >> index.js && \\${
23
+ echo "import { ${resolvePlaywrightBrowserType(browser)} as browser } from 'playwright-core';" >> index.js && \\
24
+ echo "const ws = await browser.launchServer({ ...${JSON.stringify(serverOptions)}, port: 4444, wsPath: 'creevey' })" >> index.js && \\${
23
25
  npmRegistry
24
26
  ? `
25
27
  echo "registry=${npmRegistry}" > .npmrc && \\`
26
28
  : ''
27
29
  }
28
- npm i playwright-core${sv ? `@${sv.format()}` : ''}
30
+ npm i playwright-core${sv ? `@${sv.format()}` : ''} && \\
31
+ npx -y playwright${sv ? `@${sv.format()}` : ''} install --with-deps ${browser}
29
32
 
30
33
  EXPOSE 4444
31
34
 
@@ -1,7 +1,7 @@
1
1
  import { Browser, BrowserType, Page, chromium, firefox, webkit } from 'playwright-core';
2
- import Logger from 'loglevel';
3
2
  import chalk from 'chalk';
4
3
  import { v4 } from 'uuid';
4
+ import Logger from 'loglevel';
5
5
  import prefix from 'loglevel-plugin-prefix';
6
6
  import {
7
7
  BrowserConfigObject,
@@ -15,9 +15,15 @@ import {
15
15
  } from '../../types';
16
16
  import { subscribeOn } from '../messages';
17
17
  import { appendIframePath, getAddresses, LOCALHOST_REGEXP, resolveStorybookUrl, storybookRootID } from '../webdriver';
18
- import { isShuttingDown, runSequence } from '../utils';
18
+ import { isShuttingDown, resolvePlaywrightBrowserType, runSequence } from '../utils';
19
19
  import { colors, logger } from '../logger';
20
- import { Args } from '@storybook/csf';
20
+ import type { Args } from '@storybook/csf';
21
+
22
+ const browsers = {
23
+ chromium,
24
+ firefox,
25
+ webkit,
26
+ };
21
27
 
22
28
  async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser | null> {
23
29
  let timeout: NodeJS.Timeout | null = null;
@@ -28,7 +34,7 @@ async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser |
28
34
  (resolve) =>
29
35
  (timeout = setTimeout(() => {
30
36
  isTimeout = true;
31
- logger.error(`Can't connect to ${type.name()} playwright browser`, error);
37
+ logger().error(`Can't connect to ${type.name()} playwright browser`, error);
32
38
  resolve(null);
33
39
  }, 10000)),
34
40
  ),
@@ -57,13 +63,13 @@ export class InternalBrowser {
57
63
  #sessionId: string = v4();
58
64
  #serverHost: string | null = null;
59
65
  #serverPort: number;
60
- #logger: Logger.Logger;
66
+ #storybookGlobals?: StorybookGlobals;
61
67
  #unsubscribe: () => void = noop;
62
- constructor(browser: Browser, page: Page, port: number) {
68
+ constructor(browser: Browser, page: Page, port: number, storybookGlobals?: StorybookGlobals) {
63
69
  this.#browser = browser;
64
70
  this.#page = page;
65
71
  this.#serverPort = port;
66
- this.#logger = Logger.getLogger(this.#sessionId);
72
+ this.#storybookGlobals = storybookGlobals;
67
73
  this.#unsubscribe = subscribeOn('shutdown', () => {
68
74
  void this.closeBrowser();
69
75
  });
@@ -99,7 +105,12 @@ export class InternalBrowser {
99
105
  if (captureElement) {
100
106
  const element = await this.#page.$(captureElement);
101
107
  if (!element) throw new Error(`Element with selector ${captureElement} not found`);
102
- return element.screenshot({ animations: 'disabled', mask });
108
+
109
+ return element.screenshot({
110
+ animations: 'disabled',
111
+ mask,
112
+ style: ':root { overflow: hidden !important; }',
113
+ });
103
114
  }
104
115
  return this.#page.screenshot({ animations: 'disabled', mask, fullPage: true });
105
116
  }
@@ -110,10 +121,11 @@ export class InternalBrowser {
110
121
 
111
122
  async selectStory(id: string, waitForReady = false): Promise<boolean> {
112
123
  // NOTE: Global variables might be reset after hot reload. I think it's workaround, maybe we need better solution
124
+ await this.updateStorybookGlobals();
113
125
  await this.updateBrowserGlobalVariables();
114
126
  await this.resetMousePosition();
115
127
 
116
- this.#logger.debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
128
+ logger().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
117
129
 
118
130
  const result = await this.#page.evaluate<
119
131
  [error?: string | null, isCaptureCalled?: boolean] | null,
@@ -152,20 +164,12 @@ export class InternalBrowser {
152
164
  );
153
165
  }
154
166
 
155
- async loadStoriesFromBrowser(retry = false): Promise<StoriesRaw> {
156
- try {
157
- const stories = await this.#page.evaluate<StoriesRaw | undefined>(() => window.__CREEVEY_GET_STORIES__());
167
+ async loadStoriesFromBrowser(): Promise<StoriesRaw> {
168
+ const stories = await this.#page.evaluate<StoriesRaw | undefined>(() => window.__CREEVEY_GET_STORIES__());
158
169
 
159
- if (!stories) throw new Error("Can't get stories, it seems creevey or storybook API isn't available");
170
+ if (!stories) throw new Error("Can't get stories, it seems creevey or storybook API isn't available");
160
171
 
161
- return stories;
162
- } catch (error) {
163
- // TODO Check how other solutions with playwright get stories from storybook
164
- if (retry) throw error;
165
- await new Promise((resolve) => setTimeout(resolve, 1000));
166
- // NOTE: Try one more time because of dynamic nature of vite and storybook
167
- return this.loadStoriesFromBrowser(true);
168
- }
172
+ return stories;
169
173
  }
170
174
 
171
175
  static async getBrowser(
@@ -179,49 +183,50 @@ export class InternalBrowser {
179
183
  storybookUrl: address = config.storybookUrl,
180
184
  viewport,
181
185
  _storybookGlobals,
182
- ...userCapabilities
186
+ seleniumCapabilities,
187
+ playwrightOptions,
183
188
  } = browserConfig;
184
189
 
185
190
  let browser: Browser | null = null;
186
191
 
187
- if (new URL(gridUrl).protocol === 'ws:') {
188
- switch (browserConfig.browserName) {
189
- case 'chromium':
190
- browser = await tryConnect(chromium, gridUrl);
191
- break;
192
- case 'firefox':
193
- browser = await tryConnect(firefox, gridUrl);
194
- break;
195
- case 'webkit':
196
- browser = await tryConnect(webkit, gridUrl);
197
- break;
198
-
199
- default:
200
- logger.error(
201
- `Unknown browser ${browserConfig.browserName}. Playwright supports browsers: chromium, firefox, webkit`,
202
- );
203
- }
192
+ const parsedUrl = new URL(gridUrl);
193
+ if (parsedUrl.protocol === 'ws:') {
194
+ browser = await tryConnect(browsers[resolvePlaywrightBrowserType(browserConfig.browserName)], gridUrl);
195
+ } else if (parsedUrl.protocol === 'creevey:') {
196
+ browser = await browsers[resolvePlaywrightBrowserType(browserConfig.browserName)].launch(playwrightOptions);
204
197
  } else {
205
- if (browserConfig.browserName != 'chrome') {
206
- logger.error("Playwright's Selenium Grid feature supports only chrome browser");
198
+ if (browserConfig.browserName !== 'chrome') {
199
+ logger().error("Playwright's Selenium Grid feature supports only chrome browser");
207
200
  return null;
208
201
  }
209
202
 
210
203
  process.env.SELENIUM_REMOTE_URL = gridUrl;
211
- process.env.SELENIUM_REMOTE_CAPABILITIES = JSON.stringify(userCapabilities);
204
+ process.env.SELENIUM_REMOTE_CAPABILITIES = JSON.stringify(seleniumCapabilities);
212
205
 
213
- browser = await chromium.launch();
206
+ browser = await chromium.launch(playwrightOptions);
214
207
  }
215
208
 
216
209
  if (!browser) {
217
210
  return null;
218
211
  }
219
212
 
213
+ // TODO Record video
220
214
  const page = await browser.newPage();
215
+ // TODO Support tracing
216
+ // if (playwrightOptions?.trace) {
217
+ // const context = page.context();
218
+ // await context.tracing.start(playwrightOptions.trace);
219
+ // }
220
+
221
+ if (logger().getLevel() <= Logger.levels.DEBUG) {
222
+ page.on('console', (msg) => {
223
+ logger().debug(`Console message: ${msg.text()}`);
224
+ });
225
+ }
221
226
 
222
227
  // TODO Add debug output
223
228
 
224
- const internalBrowser = new InternalBrowser(browser, page, options.port);
229
+ const internalBrowser = new InternalBrowser(browser, page, options.port, _storybookGlobals);
225
230
 
226
231
  try {
227
232
  if (isShuttingDown.current) return null;
@@ -229,8 +234,6 @@ export class InternalBrowser {
229
234
  browserName,
230
235
  viewport,
231
236
  storybookUrl: address,
232
- storybookGlobals: _storybookGlobals,
233
- resolveStorybookUrl: config.resolveStorybookUrl,
234
237
  });
235
238
 
236
239
  return done ? internalBrowser : null;
@@ -241,7 +244,7 @@ export class InternalBrowser {
241
244
  const error = new Error(`Can't load storybook root page: ${message}`);
242
245
  if (originalError instanceof Error) error.stack = originalError.stack;
243
246
 
244
- logger.error(error);
247
+ logger().error(error);
245
248
 
246
249
  return null;
247
250
  }
@@ -251,32 +254,28 @@ export class InternalBrowser {
251
254
  browserName,
252
255
  viewport,
253
256
  storybookUrl,
254
- storybookGlobals,
255
- resolveStorybookUrl,
256
257
  }: {
257
258
  browserName: string;
258
259
  viewport?: { width: number; height: number };
259
260
  storybookUrl: string;
260
- storybookGlobals?: StorybookGlobals;
261
- resolveStorybookUrl?: () => Promise<string>;
262
261
  }) {
263
262
  const sessionId = this.#sessionId;
264
263
 
265
- prefix.apply(this.#logger, {
264
+ prefix.apply(logger(), {
266
265
  format(level) {
267
266
  const levelColor = colors[level.toUpperCase() as keyof typeof colors];
268
- return `[${browserName}:${chalk.gray(sessionId)}] ${levelColor(level)} =>`;
267
+ return `[${browserName}:${chalk.gray(process.pid)}] ${levelColor(level)} => ${chalk.gray(sessionId)}`;
269
268
  },
270
269
  });
271
270
 
272
- this.#page.setDefaultNavigationTimeout(10000);
273
271
  this.#page.setDefaultTimeout(60000);
274
272
 
275
273
  return await runSequence(
276
274
  [
277
- () => this.openStorybookPage(storybookUrl, resolveStorybookUrl),
275
+ () => this.openStorybookPage(storybookUrl),
278
276
  () => this.waitForStorybook(),
279
- () => this.updateStorybookGlobals(storybookGlobals),
277
+ () => this.triggerViteReload(),
278
+ () => this.updateStorybookGlobals(),
280
279
  () => this.resolveCreeveyHost(),
281
280
  () => this.updateBrowserGlobalVariables(),
282
281
  () => this.resizeViewport(viewport),
@@ -285,46 +284,41 @@ export class InternalBrowser {
285
284
  );
286
285
  }
287
286
 
288
- private async openStorybookPage(storybookUrl: string, resolver?: () => Promise<string>): Promise<void> {
287
+ private async openStorybookPage(storybookUrl: string): Promise<void> {
289
288
  if (!LOCALHOST_REGEXP.test(storybookUrl)) {
290
289
  await this.#page.goto(appendIframePath(storybookUrl));
291
290
  return;
292
291
  }
293
292
 
294
293
  try {
295
- if (resolver) {
296
- this.#logger.debug('Resolving storybook url with custom resolver');
297
-
298
- const resolvedUrl = await resolver();
299
-
300
- this.#logger.debug(`Resolver storybook url ${resolvedUrl}`);
301
-
302
- await this.#page.goto(appendIframePath(resolvedUrl));
303
- } else {
304
- await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url), this.#logger);
305
- }
294
+ // TODO this.#page.setDefaultNavigationTimeout(10000);
295
+ const resolvedUrl = await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
296
+ await this.#page.goto(resolvedUrl);
306
297
  } catch (error) {
307
- this.#logger.error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
298
+ logger().error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
308
299
  throw error;
309
300
  }
310
301
  }
311
302
 
312
303
  private async checkUrl(url: string): Promise<boolean> {
304
+ const page = await this.#browser.newPage();
313
305
  try {
314
- this.#logger.debug(`Opening ${chalk.magenta(url)} and checking the page source`);
315
- const response = await this.#page.goto(url, { waitUntil: 'commit' });
306
+ logger().debug(`Opening ${chalk.magenta(url)} and checking the page source`);
307
+ const response = await page.goto(url, { waitUntil: 'commit' });
316
308
  const source = await response?.text();
317
309
 
318
- this.#logger.debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
310
+ logger().debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
319
311
  return source?.includes(`id="${storybookRootID}"`) ?? false;
320
312
  } catch {
321
313
  return false;
314
+ } finally {
315
+ await page.close();
322
316
  }
323
317
  }
324
318
 
325
319
  private async waitForStorybook(): Promise<void> {
326
320
  // TODO Duplicated code with selenium
327
- this.#logger.debug('Waiting for `setStories` event to make sure that storybook is initiated');
321
+ logger().debug('Waiting for `setStories` event to make sure that storybook is initiated');
328
322
 
329
323
  const isTimeout = await Promise.race([
330
324
  new Promise<boolean>((resolve) => {
@@ -337,14 +331,17 @@ export class InternalBrowser {
337
331
  do {
338
332
  try {
339
333
  // TODO Research a different way to ensure storybook is initiated
334
+ // TODO Maybe use `__STORYBOOK_PREVIEW__.extract()`
340
335
  wait = await this.#page.evaluate((SET_GLOBALS: string) => {
341
336
  if (typeof window.__STORYBOOK_ADDONS_CHANNEL__ == 'undefined') return true;
342
337
  if (window.__STORYBOOK_ADDONS_CHANNEL__.last(SET_GLOBALS) == undefined) return true;
343
338
  return false;
344
339
  }, StorybookEvents.SET_GLOBALS);
345
340
  } catch (e: unknown) {
346
- this.#logger.debug('An error has been caught during the script:', e);
341
+ logger().debug('An error has been caught during the script:', e);
342
+ if (this.#page.isClosed()) throw e;
347
343
  }
344
+ if (wait) await new Promise((resolve) => setTimeout(resolve, 1000));
348
345
  } while (wait);
349
346
  return false;
350
347
  })(),
@@ -354,13 +351,25 @@ export class InternalBrowser {
354
351
  if (isTimeout) throw new Error('Failed to wait `setStories` event');
355
352
  }
356
353
 
357
- private async updateStorybookGlobals(globals?: StorybookGlobals): Promise<void> {
358
- if (!globals) return;
354
+ private async triggerViteReload(): Promise<void> {
355
+ // NOTE: On the first load, Vite might try to optimize some dependencies and reload the page
356
+ // We need to trigger reload earlier to avoid unnecessary reloads further
357
+ try {
358
+ await this.#page.evaluate(async () => {
359
+ await window.__STORYBOOK_PREVIEW__.extract();
360
+ });
361
+ } catch {
362
+ await this.waitForStorybook();
363
+ }
364
+ }
365
+
366
+ private async updateStorybookGlobals(): Promise<void> {
367
+ if (!this.#storybookGlobals) return;
359
368
 
360
- this.#logger.debug('Applying storybook globals');
369
+ logger().debug('Applying storybook globals');
361
370
  await this.#page.evaluate((globals: StorybookGlobals) => {
362
371
  window.__CREEVEY_UPDATE_GLOBALS__(globals);
363
- }, globals);
372
+ }, this.#storybookGlobals);
364
373
  }
365
374
 
366
375
  private async resolveCreeveyHost(): Promise<void> {
@@ -389,8 +398,10 @@ export class InternalBrowser {
389
398
  }
390
399
 
391
400
  private async updateBrowserGlobalVariables() {
401
+ logger().debug('Updating browser global variables');
392
402
  await this.#page.evaluate(
393
403
  ([workerId, creeveyHost, creeveyPort]) => {
404
+ window.__CREEVEY_ENV__ = true;
394
405
  window.__CREEVEY_WORKER_ID__ = workerId;
395
406
  window.__CREEVEY_SERVER_HOST__ = creeveyHost ?? 'localhost';
396
407
  window.__CREEVEY_SERVER_PORT__ = creeveyPort;
@@ -402,10 +413,12 @@ export class InternalBrowser {
402
413
  private async resizeViewport(viewport?: { width: number; height: number }): Promise<void> {
403
414
  if (!viewport) return;
404
415
 
416
+ logger().debug('Resizing viewport to', viewport);
405
417
  await this.#page.setViewportSize(viewport);
406
418
  }
407
419
 
408
420
  private async resetMousePosition(): Promise<void> {
421
+ logger().debug('Resetting mouse position to (0, 0)');
409
422
  await this.#page.mouse.move(0, 0);
410
423
  }
411
424
  }
@@ -1,5 +1,5 @@
1
1
  /// <reference types="../../../types/playwright-context" />
2
- import { Args } from '@storybook/csf';
2
+ import type { Args } from '@storybook/csf';
3
3
  import { Config, Options, ServerTest, StoriesRaw, StoryInput } from '../../types';
4
4
  import { logger } from '../logger';
5
5
  import { subscribeOn } from '../messages';
@@ -53,7 +53,7 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
53
53
  try {
54
54
  return await import('./internal.js');
55
55
  } catch (error) {
56
- logger.error(error);
56
+ logger().error(error);
57
57
  return null;
58
58
  }
59
59
  })();
@@ -5,6 +5,7 @@ import { isDefined } from '../../types.js';
5
5
  import { logger } from '../logger.js';
6
6
  import { deserializeRawStories } from '../../shared/index.js';
7
7
 
8
+ // TODO Don't have updates from stories
8
9
  export const loadStories: StoriesProvider = async (_config, storiesListener, webdriver) => {
9
10
  if (cluster.isPrimary) {
10
11
  return new Promise<StoriesRaw>((resolve) => {
@@ -17,13 +18,13 @@ export const loadStories: StoriesProvider = async (_config, storiesListener, web
17
18
  if (message.type == 'set') {
18
19
  const { stories, oldTests } = message.payload;
19
20
  if (oldTests.length > 0)
20
- logger.warn(
21
+ logger().warn(
21
22
  `If you use browser stories provider of CSFv3 Storybook feature\n` +
22
23
  `Creevey will not load tests defined in story parameters from following stories:\n` +
23
24
  oldTests.join('\n'),
24
25
  );
25
26
  unsubscribe();
26
- resolve(stories);
27
+ resolve(deserializeRawStories(stories));
27
28
  }
28
29
  });
29
30
  sendStoriesMessage(worker, { type: 'get' });
@@ -36,10 +37,11 @@ export const loadStories: StoriesProvider = async (_config, storiesListener, web
36
37
  } else {
37
38
  subscribeOn('stories', (message) => {
38
39
  if (message.type == 'get')
39
- emitStoriesMessage({ type: 'set', payload: { stories, oldTests: storiesWithOldTests } });
40
+ emitStoriesMessage({ type: 'set', payload: { stories: rawStories, oldTests: storiesWithOldTests } });
40
41
  if (message.type == 'update') storiesListener(new Map(message.payload));
41
42
  });
42
- const stories = deserializeRawStories((await webdriver?.loadStoriesFromBrowser()) ?? {});
43
+ const rawStories = (await webdriver?.loadStoriesFromBrowser()) ?? {};
44
+ const stories = deserializeRawStories(rawStories);
43
45
 
44
46
  const storiesWithOldTests: string[] = [];
45
47
 
@@ -54,7 +54,7 @@ async function parseParams(
54
54
 
55
55
  if (listener) {
56
56
  chokidar.watch(testFiles).on('change', (filePath) => {
57
- logger.debug(`changed: ${filePath}`);
57
+ logger().debug(`changed: ${filePath}`);
58
58
 
59
59
  // doesn't work, always returns {} due modules caching
60
60
  // see https://github.com/nodejs/modules/issues/307