creevey 0.10.0-beta.4 → 0.10.0-beta.41

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 (227) hide show
  1. package/README.md +19 -41
  2. package/dist/client/addon/components/Addon.js +17 -7
  3. package/dist/client/addon/components/Addon.js.map +1 -1
  4. package/dist/client/addon/components/Panel.js +2 -2
  5. package/dist/client/addon/components/Panel.js.map +1 -1
  6. package/dist/client/addon/components/Tools.js +17 -7
  7. package/dist/client/addon/components/Tools.js.map +1 -1
  8. package/dist/client/addon/withCreevey.d.ts +2 -1
  9. package/dist/client/addon/withCreevey.js +11 -1
  10. package/dist/client/addon/withCreevey.js.map +1 -1
  11. package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
  12. package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
  13. package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
  14. package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
  15. package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
  16. package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
  17. package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
  18. package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
  19. package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
  20. package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
  21. package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
  22. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  23. package/dist/client/shared/components/PageHeader/ImagePreview.d.ts +1 -1
  24. package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
  25. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  26. package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
  27. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  28. package/dist/client/shared/components/ResultsPage.d.ts +1 -1
  29. package/dist/client/shared/components/ResultsPage.js +43 -13
  30. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  31. package/dist/client/shared/creeveyClientApi.js +8 -1
  32. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  33. package/dist/client/shared/helpers.d.ts +1 -3
  34. package/dist/client/shared/helpers.js +4 -19
  35. package/dist/client/shared/helpers.js.map +1 -1
  36. package/dist/client/web/CreeveyApp.js +42 -14
  37. package/dist/client/web/CreeveyApp.js.map +1 -1
  38. package/dist/client/web/CreeveyContext.d.ts +5 -0
  39. package/dist/client/web/CreeveyContext.js +20 -7
  40. package/dist/client/web/CreeveyContext.js.map +1 -1
  41. package/dist/client/web/CreeveyLoader.js +2 -2
  42. package/dist/client/web/CreeveyLoader.js.map +1 -1
  43. package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
  44. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  45. package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
  46. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  47. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
  48. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  49. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
  50. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  51. package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +2 -2
  52. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
  53. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  54. package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
  55. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  56. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +1 -1
  57. package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
  58. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  59. package/dist/client/web/KeyboardEventsContext.js +79 -64
  60. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  61. package/dist/client/web/assets/index-C47njyZV.js +802 -0
  62. package/dist/client/web/index.html +1 -1
  63. package/dist/client/web/index.js +17 -7
  64. package/dist/client/web/index.js.map +1 -1
  65. package/dist/client/web/themes.d.ts +2 -0
  66. package/dist/client/web/themes.js +22 -0
  67. package/dist/client/web/themes.js.map +1 -0
  68. package/dist/creevey.js +16 -9
  69. package/dist/creevey.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/server/config.d.ts +1 -1
  72. package/dist/server/config.js +30 -7
  73. package/dist/server/config.js.map +1 -1
  74. package/dist/server/connection.d.ts +3 -0
  75. package/dist/server/connection.js +28 -0
  76. package/dist/server/connection.js.map +1 -0
  77. package/dist/server/docker.d.ts +1 -1
  78. package/dist/server/docker.js +56 -32
  79. package/dist/server/docker.js.map +1 -1
  80. package/dist/server/index.js +64 -11
  81. package/dist/server/index.js.map +1 -1
  82. package/dist/server/logger.d.ts +2 -1
  83. package/dist/server/logger.js +7 -3
  84. package/dist/server/logger.js.map +1 -1
  85. package/dist/server/master/api.js +1 -1
  86. package/dist/server/master/api.js.map +1 -1
  87. package/dist/server/master/pool.d.ts +4 -3
  88. package/dist/server/master/pool.js +13 -66
  89. package/dist/server/master/pool.js.map +1 -1
  90. package/dist/server/master/queue.d.ts +13 -0
  91. package/dist/server/master/queue.js +71 -0
  92. package/dist/server/master/queue.js.map +1 -0
  93. package/dist/server/master/runner.d.ts +3 -0
  94. package/dist/server/master/runner.js +78 -10
  95. package/dist/server/master/runner.js.map +1 -1
  96. package/dist/server/master/server.js +1 -1
  97. package/dist/server/master/server.js.map +1 -1
  98. package/dist/server/master/start.js +13 -11
  99. package/dist/server/master/start.js.map +1 -1
  100. package/dist/server/playwright/docker-file.d.ts +1 -1
  101. package/dist/server/playwright/docker-file.js +15 -6
  102. package/dist/server/playwright/docker-file.js.map +1 -1
  103. package/dist/server/playwright/docker.d.ts +2 -1
  104. package/dist/server/playwright/docker.js +10 -2
  105. package/dist/server/playwright/docker.js.map +1 -1
  106. package/dist/server/playwright/index-source.mjs +16 -0
  107. package/dist/server/playwright/internal.d.ts +6 -6
  108. package/dist/server/playwright/internal.js +143 -91
  109. package/dist/server/playwright/internal.js.map +1 -1
  110. package/dist/server/playwright/webdriver.d.ts +1 -1
  111. package/dist/server/playwright/webdriver.js +5 -8
  112. package/dist/server/playwright/webdriver.js.map +1 -1
  113. package/dist/server/providers/browser.js +6 -4
  114. package/dist/server/providers/browser.js.map +1 -1
  115. package/dist/server/providers/hybrid.js +1 -1
  116. package/dist/server/providers/hybrid.js.map +1 -1
  117. package/dist/server/reporters/creevey.d.ts +7 -0
  118. package/dist/server/reporters/creevey.js +63 -0
  119. package/dist/server/reporters/creevey.js.map +1 -0
  120. package/dist/server/reporters/index.d.ts +2 -0
  121. package/dist/server/reporters/index.js +16 -0
  122. package/dist/server/reporters/index.js.map +1 -0
  123. package/dist/server/reporters/junit.d.ts +16 -0
  124. package/dist/server/reporters/junit.js +165 -0
  125. package/dist/server/reporters/junit.js.map +1 -0
  126. package/dist/server/reporters/teamcity.d.ts +7 -0
  127. package/dist/server/reporters/teamcity.js +60 -0
  128. package/dist/server/reporters/teamcity.js.map +1 -0
  129. package/dist/server/selenium/internal.d.ts +3 -4
  130. package/dist/server/selenium/internal.js +127 -108
  131. package/dist/server/selenium/internal.js.map +1 -1
  132. package/dist/server/selenium/selenoid.js +8 -6
  133. package/dist/server/selenium/selenoid.js.map +1 -1
  134. package/dist/server/selenium/webdriver.d.ts +1 -1
  135. package/dist/server/selenium/webdriver.js +5 -9
  136. package/dist/server/selenium/webdriver.js.map +1 -1
  137. package/dist/server/telemetry.js +2 -2
  138. package/dist/server/testsFiles/parser.js +45 -5
  139. package/dist/server/testsFiles/parser.js.map +1 -1
  140. package/dist/server/utils.d.ts +19 -1
  141. package/dist/server/utils.js +87 -8
  142. package/dist/server/utils.js.map +1 -1
  143. package/dist/server/webdriver.d.ts +5 -4
  144. package/dist/server/webdriver.js +23 -10
  145. package/dist/server/webdriver.js.map +1 -1
  146. package/dist/server/worker/chai-image.d.ts +1 -2
  147. package/dist/server/worker/chai-image.js +4 -3
  148. package/dist/server/worker/chai-image.js.map +1 -1
  149. package/dist/server/worker/context.d.ts +3 -0
  150. package/dist/server/worker/context.js +15 -0
  151. package/dist/server/worker/context.js.map +1 -0
  152. package/dist/server/worker/match-image.d.ts +4 -4
  153. package/dist/server/worker/match-image.js +7 -4
  154. package/dist/server/worker/match-image.js.map +1 -1
  155. package/dist/server/worker/start.js +47 -73
  156. package/dist/server/worker/start.js.map +1 -1
  157. package/dist/shared/index.d.ts +1 -1
  158. package/dist/types.d.ts +46 -10
  159. package/dist/types.js +2 -0
  160. package/dist/types.js.map +1 -1
  161. package/docs/cli.md +12 -0
  162. package/docs/config.md +179 -165
  163. package/docs/storybook.md +60 -0
  164. package/docs/tests.md +50 -45
  165. package/package.json +64 -63
  166. package/src/client/addon/components/Panel.tsx +2 -2
  167. package/src/client/addon/withCreevey.ts +10 -2
  168. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  169. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  170. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
  171. package/src/client/shared/components/ResultsPage.tsx +31 -8
  172. package/src/client/shared/creeveyClientApi.ts +9 -1
  173. package/src/client/shared/helpers.ts +4 -24
  174. package/src/client/web/CreeveyApp.tsx +27 -8
  175. package/src/client/web/CreeveyContext.tsx +9 -0
  176. package/src/client/web/CreeveyLoader.tsx +1 -1
  177. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  178. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  179. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  180. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  181. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  182. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  183. package/src/client/web/themes.ts +24 -0
  184. package/src/creevey.ts +16 -10
  185. package/src/server/config.ts +30 -7
  186. package/src/server/connection.ts +26 -0
  187. package/src/server/docker.ts +63 -34
  188. package/src/server/index.ts +72 -14
  189. package/src/server/logger.ts +6 -2
  190. package/src/server/master/api.ts +1 -1
  191. package/src/server/master/pool.ts +23 -59
  192. package/src/server/master/queue.ts +77 -0
  193. package/src/server/master/runner.ts +96 -10
  194. package/src/server/master/server.ts +1 -1
  195. package/src/server/master/start.ts +16 -11
  196. package/src/server/playwright/docker-file.ts +18 -6
  197. package/src/server/playwright/docker.ts +16 -3
  198. package/src/server/playwright/index-source.mjs +16 -0
  199. package/src/server/playwright/internal.ts +182 -111
  200. package/src/server/playwright/webdriver.ts +6 -9
  201. package/src/server/providers/browser.ts +6 -4
  202. package/src/server/providers/hybrid.ts +1 -1
  203. package/src/server/reporters/creevey.ts +71 -0
  204. package/src/server/reporters/index.ts +11 -0
  205. package/src/server/reporters/junit.ts +205 -0
  206. package/src/server/reporters/teamcity.ts +74 -0
  207. package/src/server/selenium/internal.ts +131 -116
  208. package/src/server/selenium/selenoid.ts +8 -6
  209. package/src/server/selenium/webdriver.ts +6 -10
  210. package/src/server/telemetry.ts +2 -2
  211. package/src/server/testsFiles/parser.ts +52 -4
  212. package/src/server/utils.ts +97 -9
  213. package/src/server/webdriver.ts +24 -16
  214. package/src/server/worker/chai-image.ts +4 -4
  215. package/src/server/worker/context.ts +14 -0
  216. package/src/server/worker/match-image.ts +12 -8
  217. package/src/server/worker/start.ts +51 -86
  218. package/src/shared/index.ts +1 -1
  219. package/src/types.ts +50 -11
  220. package/types/global.d.ts +1 -0
  221. package/.yarnrc.yml +0 -1
  222. package/chromatic.config.json +0 -5
  223. package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
  224. package/dist/server/reporter.d.ts +0 -26
  225. package/dist/server/reporter.js +0 -108
  226. package/dist/server/reporter.js.map +0 -1
  227. package/src/server/reporter.ts +0 -138
@@ -1,8 +1,12 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { pathToFileURL } from 'url';
1
3
  import semver from 'semver';
2
4
  import { exec } from 'shelljs';
3
5
 
6
+ const importMetaUrl = pathToFileURL(__filename).href;
7
+
4
8
  // TODO Support custom docker images
5
- export function playwrightDockerFile(browser: string, version: string): string {
9
+ export async function playwrightDockerFile(browser: string, version: string): Promise<string> {
6
10
  const sv = semver.coerce(version);
7
11
 
8
12
  let npmRegistry;
@@ -12,23 +16,31 @@ export function playwrightDockerFile(browser: string, version: string): string {
12
16
  /* noop */
13
17
  }
14
18
 
15
- return `
16
- FROM mcr.microsoft.com/playwright:v${sv?.format() ?? version}
19
+ const indexJs = await readFile(new URL('./index-source.mjs', importMetaUrl), 'utf-8');
20
+
21
+ const dockerfile = `
22
+ FROM node:lts
17
23
 
18
24
  WORKDIR /creevey
19
25
 
20
26
  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 && \\${
27
+ ${indexJs
28
+ .split('\n')
29
+ .map((line) => ` echo "${line.replace(/"/g, '\\"')}" >> index.js && \\`)
30
+ .join('\n')}
31
+ ${
23
32
  npmRegistry
24
33
  ? `
25
34
  echo "registry=${npmRegistry}" > .npmrc && \\`
26
35
  : ''
27
36
  }
28
- npm i playwright-core${sv ? `@${sv.format()}` : ''}
37
+ npm i playwright-core${sv ? `@${sv.format()}` : ''} && \\
38
+ npx -y playwright${sv ? `@${sv.format()}` : ''} install --with-deps ${browser}
29
39
 
30
40
  EXPOSE 4444
31
41
 
32
42
  ENTRYPOINT [ "node", "./index.js" ]
33
43
  `;
44
+
45
+ return dockerfile.replace(/\\\n\s*\\?\n/g, '\\\n');
34
46
  }
@@ -1,9 +1,17 @@
1
+ import assert from 'assert';
1
2
  import { runImage } from '../docker';
2
3
  import { emitWorkerMessage, subscribeOn } from '../messages';
3
- import { isInsideDocker } from '../utils';
4
+ import { getCreeveyCache, isInsideDocker, resolvePlaywrightBrowserType } from '../utils';
4
5
  import { LOCALHOST_REGEXP } from '../webdriver';
6
+ import type { BrowserConfigObject, Config } from '../../types';
5
7
 
6
- export async function startPlaywrightContainer(imageName: string, debug: boolean): Promise<string> {
8
+ export async function startPlaywrightContainer(
9
+ imageName: string,
10
+ browser: string,
11
+ config: Config,
12
+ debug: boolean,
13
+ ): Promise<string> {
14
+ const { browserName, playwrightOptions } = config.browsers[browser] as BrowserConfigObject;
7
15
  const port = await new Promise<number>((resolve) => {
8
16
  subscribeOn('worker', (message) => {
9
17
  if (message.type == 'port') {
@@ -13,13 +21,18 @@ export async function startPlaywrightContainer(imageName: string, debug: boolean
13
21
  emitWorkerMessage({ type: 'port', payload: { port: -1 } });
14
22
  });
15
23
 
24
+ const cacheDir = await getCreeveyCache();
25
+
26
+ assert(cacheDir, "Couldn't get cache directory");
27
+
16
28
  const host = await runImage(
17
29
  imageName,
18
- [],
30
+ [JSON.stringify({ ...playwrightOptions, browser: resolvePlaywrightBrowserType(browserName) })],
19
31
  {
20
32
  ExposedPorts: { [`${port}/tcp`]: {} },
21
33
  HostConfig: {
22
34
  PortBindings: { ['4444/tcp']: [{ HostPort: `${port}` }] },
35
+ Binds: [`${cacheDir}/${process.pid}:/creevey/traces`],
23
36
  },
24
37
  },
25
38
  debug,
@@ -0,0 +1,16 @@
1
+ import { chromium, firefox, webkit } from 'playwright-core';
2
+
3
+ /** @type import("playwright-core").LaunchOptions & { browser: 'chromium' | 'firefox' | 'webkit' } */
4
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
5
+ const config = JSON.parse(process.argv.slice(2)[0]);
6
+
7
+ const browsers = { chromium, firefox, webkit };
8
+
9
+ const ws = await browsers[config.browser].launchServer({
10
+ ...config,
11
+ port: 4444,
12
+ wsPath: 'creevey',
13
+ tracesDir: 'traces',
14
+ });
15
+
16
+ console.log(config.browser, 'browser server launched on:', ws.wsEndpoint());
@@ -1,8 +1,19 @@
1
- import { Browser, BrowserType, Page, chromium, firefox, webkit } from 'playwright-core';
2
- import Logger from 'loglevel';
1
+ import path from 'path';
2
+ import {
3
+ Browser,
4
+ BrowserContext,
5
+ BrowserContextOptions,
6
+ BrowserType,
7
+ Page,
8
+ chromium,
9
+ firefox,
10
+ webkit,
11
+ } from 'playwright-core';
3
12
  import chalk from 'chalk';
4
13
  import { v4 } from 'uuid';
14
+ import Logger from 'loglevel';
5
15
  import prefix from 'loglevel-plugin-prefix';
16
+ import type { Args } from '@storybook/types';
6
17
  import {
7
18
  BrowserConfigObject,
8
19
  Config,
@@ -11,13 +22,24 @@ import {
11
22
  StoryInput,
12
23
  StorybookEvents,
13
24
  StorybookGlobals,
14
- noop,
15
25
  } from '../../types';
16
- import { subscribeOn } from '../messages';
17
- import { appendIframePath, getAddresses, LOCALHOST_REGEXP, resolveStorybookUrl, storybookRootID } from '../webdriver';
18
- import { isShuttingDown, runSequence } from '../utils';
26
+ import {
27
+ appendIframePath,
28
+ getAddresses,
29
+ LOCALHOST_REGEXP,
30
+ openBrowser,
31
+ resolveStorybookUrl,
32
+ storybookRootID,
33
+ } from '../webdriver';
34
+ import { getCreeveyCache, isShuttingDown, resolvePlaywrightBrowserType, runSequence } from '../utils';
19
35
  import { colors, logger } from '../logger';
20
- import { Args } from '@storybook/csf';
36
+ import assert from 'assert';
37
+
38
+ const browsers = {
39
+ chromium,
40
+ firefox,
41
+ webkit,
42
+ };
21
43
 
22
44
  async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser | null> {
23
45
  let timeout: NodeJS.Timeout | null = null;
@@ -28,7 +50,7 @@ async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser |
28
50
  (resolve) =>
29
51
  (timeout = setTimeout(() => {
30
52
  isTimeout = true;
31
- logger.error(`Can't connect to ${type.name()} playwright browser`, error);
53
+ logger().error(`Can't connect to ${type.name()} playwright browser`, error);
32
54
  resolve(null);
33
55
  }, 10000)),
34
56
  ),
@@ -50,25 +72,60 @@ async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser |
50
72
  ]);
51
73
  }
52
74
 
75
+ async function tryCreateBrowserContext(
76
+ browser: Browser,
77
+ options: BrowserContextOptions,
78
+ ): Promise<{ context: BrowserContext; page: Page }> {
79
+ try {
80
+ const context = await browser.newContext(options);
81
+ const page = await context.newPage();
82
+ return { context, page };
83
+ } catch (error) {
84
+ if (error instanceof Error && error.message.includes('ffmpeg')) {
85
+ logger().warn('Failed to create browser context with video recording. Video recording will be disabled.');
86
+ logger().warn(error);
87
+ const context = await browser.newContext({
88
+ ...options,
89
+ recordVideo: undefined,
90
+ });
91
+ const page = await context.newPage();
92
+ return { context, page };
93
+ }
94
+ throw error;
95
+ }
96
+ }
97
+
53
98
  export class InternalBrowser {
54
99
  #isShuttingDown = false;
55
100
  #browser: Browser;
101
+ #context: BrowserContext;
56
102
  #page: Page;
103
+ #traceDir: string;
57
104
  #sessionId: string = v4();
58
105
  #serverHost: string | null = null;
59
106
  #serverPort: number;
60
- #logger: Logger.Logger;
61
- #unsubscribe: () => void = noop;
62
- constructor(browser: Browser, page: Page, port: number) {
107
+ #debug: boolean;
108
+ #storybookGlobals?: StorybookGlobals;
109
+ #closeBrowser = openBrowser();
110
+ constructor(
111
+ browser: Browser,
112
+ context: BrowserContext,
113
+ page: Page,
114
+ traceDir: string,
115
+ port: number,
116
+ debug: boolean,
117
+ storybookGlobals?: StorybookGlobals,
118
+ ) {
63
119
  this.#browser = browser;
120
+ this.#context = context;
64
121
  this.#page = page;
122
+ this.#traceDir = traceDir;
65
123
  this.#serverPort = port;
66
- this.#logger = Logger.getLogger(this.#sessionId);
67
- this.#unsubscribe = subscribeOn('shutdown', () => {
68
- void this.closeBrowser();
69
- });
124
+ this.#debug = debug;
125
+ this.#storybookGlobals = storybookGlobals;
70
126
  }
71
127
 
128
+ // TODO Expose #browser and #context in tests
72
129
  get browser() {
73
130
  return this.#page;
74
131
  }
@@ -81,27 +138,36 @@ export class InternalBrowser {
81
138
  if (this.#isShuttingDown) return;
82
139
 
83
140
  this.#isShuttingDown = true;
84
- this.#unsubscribe();
85
141
 
86
142
  try {
143
+ if (this.#debug) await this.#context.tracing.stop({ path: path.join(this.#traceDir, 'trace.zip') });
87
144
  await this.#page.close();
145
+ if (this.#debug) await this.#page.video()?.saveAs(path.join(this.#traceDir, 'video.webm'));
146
+ await this.#context.close();
88
147
  await this.#browser.close();
89
- } catch (_) {
148
+ } catch {
90
149
  /* noop */
150
+ } finally {
151
+ this.#closeBrowser();
91
152
  }
92
153
  }
93
154
 
94
155
  async takeScreenshot(captureElement?: string | null, ignoreElements?: string | string[] | null): Promise<Buffer> {
95
- // TODO Implement features from selenium `takeScreenshot`
96
- // TODO Do we need scroll bar hack from selenium?
97
156
  const ignore = Array.isArray(ignoreElements) ? ignoreElements : ignoreElements ? [ignoreElements] : [];
98
157
  const mask = ignore.map((el) => this.#page.locator(el));
99
158
  if (captureElement) {
100
159
  const element = await this.#page.$(captureElement);
101
160
  if (!element) throw new Error(`Element with selector ${captureElement} not found`);
102
- return element.screenshot({ animations: 'disabled', mask });
161
+
162
+ logger().debug(`Capturing ${chalk.cyan(captureElement)} element`);
163
+ return element.screenshot({
164
+ animations: 'disabled',
165
+ mask,
166
+ style: ':root { overflow: hidden !important; }',
167
+ });
103
168
  }
104
- return this.#page.screenshot({ animations: 'disabled', mask, fullPage: true });
169
+ logger().debug('Capturing viewport screenshot');
170
+ return this.#page.screenshot({ animations: 'disabled', mask });
105
171
  }
106
172
 
107
173
  waitForComplete(callback: (isCompleted: boolean) => void): void {
@@ -110,10 +176,11 @@ export class InternalBrowser {
110
176
 
111
177
  async selectStory(id: string, waitForReady = false): Promise<boolean> {
112
178
  // NOTE: Global variables might be reset after hot reload. I think it's workaround, maybe we need better solution
179
+ await this.updateStorybookGlobals();
113
180
  await this.updateBrowserGlobalVariables();
114
181
  await this.resetMousePosition();
115
182
 
116
- this.#logger.debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
183
+ logger().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
117
184
 
118
185
  const result = await this.#page.evaluate<
119
186
  [error?: string | null, isCaptureCalled?: boolean] | null,
@@ -152,20 +219,12 @@ export class InternalBrowser {
152
219
  );
153
220
  }
154
221
 
155
- async loadStoriesFromBrowser(retry = false): Promise<StoriesRaw> {
156
- try {
157
- const stories = await this.#page.evaluate<StoriesRaw | undefined>(() => window.__CREEVEY_GET_STORIES__());
222
+ async loadStoriesFromBrowser(): Promise<StoriesRaw> {
223
+ const stories = await this.#page.evaluate<StoriesRaw | undefined>(() => window.__CREEVEY_GET_STORIES__());
158
224
 
159
- if (!stories) throw new Error("Can't get stories, it seems creevey or storybook API isn't available");
225
+ if (!stories) throw new Error("Can't get stories, it seems creevey or storybook API isn't available");
160
226
 
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
- }
227
+ return stories;
169
228
  }
170
229
 
171
230
  static async getBrowser(
@@ -182,56 +241,77 @@ export class InternalBrowser {
182
241
  seleniumCapabilities,
183
242
  playwrightOptions,
184
243
  } = browserConfig;
244
+ const parsedUrl = new URL(gridUrl);
245
+ const tracesDir = path.join(
246
+ playwrightOptions?.tracesDir ?? path.join(config.reportDir, 'traces'),
247
+ process.pid.toString(),
248
+ );
249
+ const cacheDir = await getCreeveyCache();
185
250
 
186
- let browser: Browser | null = null;
251
+ assert(cacheDir, "Couldn't get cache directory");
187
252
 
188
- if (new URL(gridUrl).protocol === 'ws:') {
189
- switch (browserConfig.browserName) {
190
- case 'chromium':
191
- browser = await tryConnect(chromium, gridUrl);
192
- break;
193
- case 'firefox':
194
- browser = await tryConnect(firefox, gridUrl);
195
- break;
196
- case 'webkit':
197
- browser = await tryConnect(webkit, gridUrl);
198
- break;
253
+ let browser: Browser | null = null;
199
254
 
200
- default:
201
- logger.error(
202
- `Unknown browser ${browserConfig.browserName}. Playwright supports browsers: chromium, firefox, webkit`,
203
- );
204
- }
255
+ if (parsedUrl.protocol === 'ws:') {
256
+ browser = await tryConnect(browsers[resolvePlaywrightBrowserType(browserConfig.browserName)], gridUrl);
257
+ } else if (parsedUrl.protocol === 'creevey:') {
258
+ browser = await browsers[resolvePlaywrightBrowserType(browserConfig.browserName)].launch({
259
+ ...playwrightOptions,
260
+ tracesDir: path.join(cacheDir, `${process.pid}`),
261
+ });
205
262
  } else {
206
- if (browserConfig.browserName != 'chrome') {
207
- logger.error("Playwright's Selenium Grid feature supports only chrome browser");
263
+ if (browserConfig.browserName !== 'chrome') {
264
+ logger().error("Playwright's Selenium Grid feature supports only chrome browser");
208
265
  return null;
209
266
  }
210
267
 
211
268
  process.env.SELENIUM_REMOTE_URL = gridUrl;
212
269
  process.env.SELENIUM_REMOTE_CAPABILITIES = JSON.stringify(seleniumCapabilities);
213
270
 
214
- browser = await chromium.launch(playwrightOptions);
271
+ browser = await chromium.launch({ ...playwrightOptions, tracesDir: path.join(cacheDir, `${process.pid}`) });
215
272
  }
216
273
 
217
274
  if (!browser) {
218
275
  return null;
219
276
  }
220
277
 
221
- const page = await browser.newPage();
278
+ const { context, page } = await tryCreateBrowserContext(browser, {
279
+ recordVideo: options.debug
280
+ ? {
281
+ dir: path.join(cacheDir, `${process.pid}`),
282
+ size: viewport,
283
+ }
284
+ : undefined,
285
+ screen: viewport,
286
+ viewport,
287
+ });
288
+ if (options.debug) {
289
+ await context.tracing.start(
290
+ Object.assign({ screenshots: true, snapshots: true, sources: true }, playwrightOptions?.trace),
291
+ );
292
+ }
222
293
 
223
- // TODO Add debug output
294
+ if (logger().getLevel() <= Logger.levels.DEBUG) {
295
+ page.on('console', (msg) => {
296
+ logger().debug(`Console message: ${msg.text()}`);
297
+ });
298
+ }
224
299
 
225
- const internalBrowser = new InternalBrowser(browser, page, options.port);
300
+ const internalBrowser = new InternalBrowser(
301
+ browser,
302
+ context,
303
+ page,
304
+ tracesDir,
305
+ options.port,
306
+ options.debug,
307
+ _storybookGlobals,
308
+ );
226
309
 
227
310
  try {
228
311
  if (isShuttingDown.current) return null;
229
312
  const done = await internalBrowser.init({
230
313
  browserName,
231
- viewport,
232
314
  storybookUrl: address,
233
- storybookGlobals: _storybookGlobals,
234
- resolveStorybookUrl: config.resolveStorybookUrl,
235
315
  });
236
316
 
237
317
  return done ? internalBrowser : null;
@@ -242,90 +322,70 @@ export class InternalBrowser {
242
322
  const error = new Error(`Can't load storybook root page: ${message}`);
243
323
  if (originalError instanceof Error) error.stack = originalError.stack;
244
324
 
245
- logger.error(error);
325
+ logger().error(error);
246
326
 
247
327
  return null;
248
328
  }
249
329
  }
250
330
 
251
- private async init({
252
- browserName,
253
- viewport,
254
- storybookUrl,
255
- storybookGlobals,
256
- resolveStorybookUrl,
257
- }: {
258
- browserName: string;
259
- viewport?: { width: number; height: number };
260
- storybookUrl: string;
261
- storybookGlobals?: StorybookGlobals;
262
- resolveStorybookUrl?: () => Promise<string>;
263
- }) {
331
+ private async init({ browserName, storybookUrl }: { browserName: string; storybookUrl: string }) {
264
332
  const sessionId = this.#sessionId;
265
333
 
266
- prefix.apply(this.#logger, {
334
+ prefix.apply(logger(), {
267
335
  format(level) {
268
336
  const levelColor = colors[level.toUpperCase() as keyof typeof colors];
269
- return `[${browserName}:${chalk.gray(sessionId)}] ${levelColor(level)} =>`;
337
+ return `[${browserName}:${chalk.gray(process.pid)}] ${levelColor(level)} => ${chalk.gray(sessionId)}`;
270
338
  },
271
339
  });
272
340
 
273
- this.#page.setDefaultNavigationTimeout(10000);
274
341
  this.#page.setDefaultTimeout(60000);
275
342
 
276
343
  return await runSequence(
277
344
  [
278
- () => this.openStorybookPage(storybookUrl, resolveStorybookUrl),
345
+ () => this.openStorybookPage(storybookUrl),
279
346
  () => this.waitForStorybook(),
280
- () => this.updateStorybookGlobals(storybookGlobals),
347
+ () => this.triggerViteReload(),
348
+ () => this.updateStorybookGlobals(),
281
349
  () => this.resolveCreeveyHost(),
282
350
  () => this.updateBrowserGlobalVariables(),
283
- () => this.resizeViewport(viewport),
284
351
  ],
285
352
  () => !this.#isShuttingDown,
286
353
  );
287
354
  }
288
355
 
289
- private async openStorybookPage(storybookUrl: string, resolver?: () => Promise<string>): Promise<void> {
356
+ private async openStorybookPage(storybookUrl: string): Promise<void> {
290
357
  if (!LOCALHOST_REGEXP.test(storybookUrl)) {
291
358
  await this.#page.goto(appendIframePath(storybookUrl));
292
359
  return;
293
360
  }
294
361
 
295
362
  try {
296
- if (resolver) {
297
- this.#logger.debug('Resolving storybook url with custom resolver');
298
-
299
- const resolvedUrl = await resolver();
300
-
301
- this.#logger.debug(`Resolver storybook url ${resolvedUrl}`);
302
-
303
- await this.#page.goto(appendIframePath(resolvedUrl));
304
- } else {
305
- await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url), this.#logger);
306
- }
363
+ const resolvedUrl = await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
364
+ await this.#page.goto(resolvedUrl);
307
365
  } catch (error) {
308
- this.#logger.error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
366
+ logger().error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
309
367
  throw error;
310
368
  }
311
369
  }
312
370
 
313
371
  private async checkUrl(url: string): Promise<boolean> {
372
+ const page = await this.#browser.newPage();
314
373
  try {
315
- this.#logger.debug(`Opening ${chalk.magenta(url)} and checking the page source`);
316
- const response = await this.#page.goto(url, { waitUntil: 'commit' });
374
+ logger().debug(`Opening ${chalk.magenta(url)} and checking the page source`);
375
+ const response = await page.goto(url, { waitUntil: 'commit' });
317
376
  const source = await response?.text();
318
377
 
319
- this.#logger.debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
378
+ logger().debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
320
379
  return source?.includes(`id="${storybookRootID}"`) ?? false;
321
380
  } catch {
322
381
  return false;
382
+ } finally {
383
+ await page.close();
323
384
  }
324
385
  }
325
386
 
326
387
  private async waitForStorybook(): Promise<void> {
327
- // TODO Duplicated code with selenium
328
- this.#logger.debug('Waiting for `setStories` event to make sure that storybook is initiated');
388
+ logger().debug('Waiting for Storybook to initiate');
329
389
 
330
390
  const isTimeout = await Promise.race([
331
391
  new Promise<boolean>((resolve) => {
@@ -344,24 +404,38 @@ export class InternalBrowser {
344
404
  return false;
345
405
  }, StorybookEvents.SET_GLOBALS);
346
406
  } catch (e: unknown) {
347
- this.#logger.debug('An error has been caught during the script:', e);
407
+ logger().debug('An error has been caught during the script:', e);
408
+ if (this.#page.isClosed()) throw e;
348
409
  }
410
+ if (wait) await new Promise((resolve) => setTimeout(resolve, 1000));
349
411
  } while (wait);
350
412
  return false;
351
413
  })(),
352
414
  ]);
353
415
 
354
- // TODO Change the message to describe a reason why it might happen
355
- if (isTimeout) throw new Error('Failed to wait `setStories` event');
416
+ if (isTimeout) throw new Error('Failed to wait Storybook init');
356
417
  }
357
418
 
358
- private async updateStorybookGlobals(globals?: StorybookGlobals): Promise<void> {
359
- if (!globals) return;
419
+ // TODO Doesn't work for some reason, maybe because of race-condition
420
+ private async triggerViteReload(): Promise<void> {
421
+ // NOTE: On the first load, Vite might try to optimize some dependencies and reload the page
422
+ // We need to trigger reload earlier to avoid unnecessary reloads further
423
+ try {
424
+ await this.#page.evaluate(async () => {
425
+ await window.__STORYBOOK_PREVIEW__.extract();
426
+ });
427
+ } catch {
428
+ await this.waitForStorybook();
429
+ }
430
+ }
431
+
432
+ private async updateStorybookGlobals(): Promise<void> {
433
+ if (!this.#storybookGlobals) return;
360
434
 
361
- this.#logger.debug('Applying storybook globals');
435
+ logger().debug('Applying storybook globals');
362
436
  await this.#page.evaluate((globals: StorybookGlobals) => {
363
437
  window.__CREEVEY_UPDATE_GLOBALS__(globals);
364
- }, globals);
438
+ }, this.#storybookGlobals);
365
439
  }
366
440
 
367
441
  private async resolveCreeveyHost(): Promise<void> {
@@ -390,8 +464,10 @@ export class InternalBrowser {
390
464
  }
391
465
 
392
466
  private async updateBrowserGlobalVariables() {
467
+ logger().debug('Updating browser global variables');
393
468
  await this.#page.evaluate(
394
469
  ([workerId, creeveyHost, creeveyPort]) => {
470
+ window.__CREEVEY_ENV__ = true;
395
471
  window.__CREEVEY_WORKER_ID__ = workerId;
396
472
  window.__CREEVEY_SERVER_HOST__ = creeveyHost ?? 'localhost';
397
473
  window.__CREEVEY_SERVER_PORT__ = creeveyPort;
@@ -400,13 +476,8 @@ export class InternalBrowser {
400
476
  );
401
477
  }
402
478
 
403
- private async resizeViewport(viewport?: { width: number; height: number }): Promise<void> {
404
- if (!viewport) return;
405
-
406
- await this.#page.setViewportSize(viewport);
407
- }
408
-
409
479
  private async resetMousePosition(): Promise<void> {
480
+ logger().debug('Resetting mouse position to (0, 0)');
410
481
  await this.#page.mouse.move(0, 0);
411
482
  }
412
483
  }
@@ -1,10 +1,11 @@
1
1
  /// <reference types="../../../types/playwright-context" />
2
- import { Args } from '@storybook/csf';
2
+ import type { Args } from '@storybook/types';
3
3
  import { Config, Options, ServerTest, StoriesRaw, StoryInput } from '../../types';
4
4
  import { logger } from '../logger';
5
5
  import { subscribeOn } from '../messages';
6
6
  import { CreeveyWebdriverBase } from '../webdriver';
7
7
  import type { InternalBrowser } from './internal';
8
+ import { removeWorkerContainer } from '../worker/context.js'; // Import container context
8
9
 
9
10
  export class PlaywrightWebdriver extends CreeveyWebdriverBase {
10
11
  #browser: InternalBrowser | null = null;
@@ -21,7 +22,9 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
21
22
  this.#options = options;
22
23
 
23
24
  subscribeOn('shutdown', () => {
24
- void this.#browser?.closeBrowser().finally(() => process.exit());
25
+ void this.#browser?.closeBrowser().finally(() => {
26
+ void removeWorkerContainer().finally(() => process.exit());
27
+ });
25
28
  this.#browser = null;
26
29
  });
27
30
  }
@@ -32,7 +35,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
32
35
 
33
36
  getSessionId(): Promise<string> {
34
37
  if (!this.#browser) {
35
- // TODO Describe the error
36
38
  throw new Error('Browser is not initialized');
37
39
  }
38
40
 
@@ -53,7 +55,7 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
53
55
  try {
54
56
  return await import('./internal.js');
55
57
  } catch (error) {
56
- logger.error(error);
58
+ logger().error(error);
57
59
  return null;
58
60
  }
59
61
  })();
@@ -79,7 +81,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
79
81
 
80
82
  async loadStoriesFromBrowser(): Promise<StoriesRaw> {
81
83
  if (!this.#browser) {
82
- // TODO Describe the error
83
84
  throw new Error('Browser is not initialized');
84
85
  }
85
86
 
@@ -95,7 +96,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
95
96
  ignoreElements?: string | string[] | null,
96
97
  ): Promise<Buffer> {
97
98
  if (!this.#browser) {
98
- // TODO Describe the error
99
99
  throw new Error('Browser is not initialized');
100
100
  }
101
101
 
@@ -104,7 +104,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
104
104
 
105
105
  protected waitForComplete(callback: (isCompleted: boolean) => void): void {
106
106
  if (!this.#browser) {
107
- // TODO Describe the error
108
107
  throw new Error('Browser is not initialized');
109
108
  }
110
109
 
@@ -113,7 +112,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
113
112
 
114
113
  protected async selectStory(id: string, waitForReady?: boolean): Promise<boolean> {
115
114
  if (!this.#browser) {
116
- // TODO Describe the error
117
115
  throw new Error('Browser is not initialized');
118
116
  }
119
117
 
@@ -122,7 +120,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
122
120
 
123
121
  protected async updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void> {
124
122
  if (!this.#browser) {
125
- // TODO Describe the error
126
123
  throw new Error('Browser is not initialized');
127
124
  }
128
125