creevey 0.9.2 → 0.10.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 (259) hide show
  1. package/chromatic.config.json +5 -0
  2. package/dist/client/addon/components/Addon.d.ts +1 -0
  3. package/dist/client/addon/components/Addon.js.map +1 -1
  4. package/dist/client/addon/components/Icons.d.ts +1 -0
  5. package/dist/client/addon/components/Icons.js.map +1 -1
  6. package/dist/client/addon/components/Panel.d.ts +1 -0
  7. package/dist/client/addon/components/Panel.js.map +1 -1
  8. package/dist/client/addon/components/TestSelect.d.ts +1 -0
  9. package/dist/client/addon/components/TestSelect.js +4 -3
  10. package/dist/client/addon/components/TestSelect.js.map +1 -1
  11. package/dist/client/addon/components/Tools.d.ts +1 -0
  12. package/dist/client/addon/components/Tools.js +7 -8
  13. package/dist/client/addon/components/Tools.js.map +1 -1
  14. package/dist/client/addon/controller.d.ts +1 -1
  15. package/dist/client/addon/controller.js.map +1 -1
  16. package/dist/client/addon/decorator.d.ts +1 -1
  17. package/dist/client/addon/manager.js +3 -2
  18. package/dist/client/addon/manager.js.map +1 -1
  19. package/dist/client/addon/preview.d.ts +1 -1
  20. package/dist/client/addon/withCreevey.d.ts +6 -8
  21. package/dist/client/addon/withCreevey.js +21 -19
  22. package/dist/client/addon/withCreevey.js.map +1 -1
  23. package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
  24. package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
  25. package/dist/client/shared/components/ImagesView/ImagesView.d.ts +1 -0
  26. package/dist/client/shared/components/ImagesView/ImagesView.js.map +1 -1
  27. package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
  28. package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
  29. package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
  30. package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
  31. package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
  32. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  33. package/dist/client/shared/components/PageFooter/PageFooter.d.ts +1 -0
  34. package/dist/client/shared/components/PageFooter/PageFooter.js +1 -1
  35. package/dist/client/shared/components/PageFooter/PageFooter.js.map +1 -1
  36. package/dist/client/shared/components/PageFooter/Paging.d.ts +2 -2
  37. package/dist/client/shared/components/PageFooter/Paging.js +8 -6
  38. package/dist/client/shared/components/PageFooter/Paging.js.map +1 -1
  39. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  40. package/dist/client/shared/components/PageHeader/PageHeader.d.ts +1 -0
  41. package/dist/client/shared/components/PageHeader/PageHeader.js +2 -1
  42. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  43. package/dist/client/shared/components/ResultsPage.d.ts +2 -2
  44. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  45. package/dist/client/web/CreeveyApp.d.ts +1 -0
  46. package/dist/client/web/CreeveyApp.js.map +1 -1
  47. package/dist/client/web/CreeveyLoader.d.ts +1 -0
  48. package/dist/client/web/CreeveyLoader.js.map +1 -1
  49. package/dist/client/web/CreeveyView/SideBar/Checkbox.d.ts +1 -1
  50. package/dist/client/web/CreeveyView/SideBar/Checkbox.js +4 -4
  51. package/dist/client/web/CreeveyView/SideBar/Checkbox.js.map +1 -1
  52. package/dist/client/web/CreeveyView/SideBar/Search.d.ts +1 -0
  53. package/dist/client/web/CreeveyView/SideBar/Search.js +4 -4
  54. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  55. package/dist/client/web/CreeveyView/SideBar/SideBar.d.ts +1 -1
  56. package/dist/client/web/CreeveyView/SideBar/SideBar.js +1 -7
  57. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  58. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.d.ts +1 -0
  59. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +5 -4
  60. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  61. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.d.ts +1 -0
  62. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +4 -3
  63. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  64. package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +3 -7
  65. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +6 -5
  66. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  67. package/dist/client/web/CreeveyView/SideBar/TestLink.d.ts +1 -0
  68. package/dist/client/web/CreeveyView/SideBar/TestLink.js +5 -1
  69. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  70. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js +15 -8
  71. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js.map +1 -1
  72. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +5 -4
  73. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
  74. package/dist/client/web/CreeveyView/SideBar/Toggle.d.ts +1 -0
  75. package/dist/client/web/CreeveyView/SideBar/Toggle.js.map +1 -1
  76. package/dist/client/web/KeyboardEventsContext.d.ts +3 -4
  77. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  78. package/dist/client/web/assets/index-DkmZfG9C.js +591 -0
  79. package/dist/client/web/index.html +1 -1
  80. package/dist/client/web/index.js +5 -6
  81. package/dist/client/web/index.js.map +1 -1
  82. package/dist/creevey.js +21 -9
  83. package/dist/creevey.js.map +1 -1
  84. package/dist/index.js +7 -3
  85. package/dist/index.js.map +1 -1
  86. package/dist/server/config.d.ts +1 -1
  87. package/dist/server/config.js +9 -5
  88. package/dist/server/config.js.map +1 -1
  89. package/dist/server/docker.d.ts +2 -2
  90. package/dist/server/docker.js +46 -40
  91. package/dist/server/docker.js.map +1 -1
  92. package/dist/server/index.js +54 -15
  93. package/dist/server/index.js.map +1 -1
  94. package/dist/server/master/master.d.ts +1 -5
  95. package/dist/server/master/master.js +3 -3
  96. package/dist/server/master/master.js.map +1 -1
  97. package/dist/server/master/pool.d.ts +2 -1
  98. package/dist/server/master/pool.js +9 -5
  99. package/dist/server/master/pool.js.map +1 -1
  100. package/dist/server/master/runner.d.ts +1 -1
  101. package/dist/server/master/runner.js +2 -2
  102. package/dist/server/master/runner.js.map +1 -1
  103. package/dist/server/master/server.js +1 -0
  104. package/dist/server/master/server.js.map +1 -1
  105. package/dist/server/master/start.d.ts +3 -0
  106. package/dist/server/master/{index.js → start.js} +6 -9
  107. package/dist/server/master/start.js.map +1 -0
  108. package/dist/server/messages.d.ts +4 -10
  109. package/dist/server/messages.js +4 -58
  110. package/dist/server/messages.js.map +1 -1
  111. package/dist/server/playwright/docker-file.d.ts +1 -0
  112. package/dist/server/playwright/docker-file.js +26 -0
  113. package/dist/server/playwright/docker-file.js.map +1 -0
  114. package/dist/server/playwright/docker.d.ts +1 -0
  115. package/dist/server/playwright/docker.js +31 -0
  116. package/dist/server/playwright/docker.js.map +1 -0
  117. package/dist/server/playwright/internal.d.ts +25 -0
  118. package/dist/server/playwright/internal.js +319 -0
  119. package/dist/server/playwright/internal.js.map +1 -0
  120. package/dist/server/playwright/webdriver.d.ts +16 -0
  121. package/dist/server/playwright/webdriver.js +105 -0
  122. package/dist/server/playwright/webdriver.js.map +1 -0
  123. package/dist/server/providers/browser.d.ts +2 -0
  124. package/dist/server/{storybook/providers → providers}/browser.js +6 -7
  125. package/dist/server/providers/browser.js.map +1 -0
  126. package/dist/server/providers/hybrid.d.ts +2 -0
  127. package/dist/server/{storybook/providers → providers}/hybrid.js +8 -8
  128. package/dist/server/providers/hybrid.js.map +1 -0
  129. package/dist/server/reporter.d.ts +26 -0
  130. package/dist/server/{worker/reporter.js → reporter.js} +34 -56
  131. package/dist/server/reporter.js.map +1 -0
  132. package/dist/server/selenium/internal.d.ts +31 -0
  133. package/dist/server/selenium/internal.js +606 -0
  134. package/dist/server/selenium/internal.js.map +1 -0
  135. package/dist/server/selenium/selenoid.js +6 -13
  136. package/dist/server/selenium/selenoid.js.map +1 -1
  137. package/dist/server/selenium/webdriver.d.ts +24 -0
  138. package/dist/server/selenium/webdriver.js +106 -0
  139. package/dist/server/selenium/webdriver.js.map +1 -0
  140. package/dist/server/stories.js +16 -9
  141. package/dist/server/stories.js.map +1 -1
  142. package/dist/server/telemetry.d.ts +1 -1
  143. package/dist/server/telemetry.js +4 -4
  144. package/dist/server/telemetry.js.map +1 -1
  145. package/dist/server/utils.d.ts +3 -4
  146. package/dist/server/utils.js +10 -9
  147. package/dist/server/utils.js.map +1 -1
  148. package/dist/server/webdriver.d.ts +19 -0
  149. package/dist/server/webdriver.js +79 -0
  150. package/dist/server/webdriver.js.map +1 -0
  151. package/dist/server/worker/chai-image.d.ts +2 -5
  152. package/dist/server/worker/chai-image.js +14 -102
  153. package/dist/server/worker/chai-image.js.map +1 -1
  154. package/dist/server/worker/match-image.d.ts +14 -0
  155. package/dist/server/worker/match-image.js +231 -0
  156. package/dist/server/worker/match-image.js.map +1 -0
  157. package/dist/server/worker/start.d.ts +2 -0
  158. package/dist/server/worker/start.js +258 -0
  159. package/dist/server/worker/start.js.map +1 -0
  160. package/dist/types.d.ts +126 -64
  161. package/dist/types.js +15 -9
  162. package/dist/types.js.map +1 -1
  163. package/package.json +108 -110
  164. package/src/client/addon/components/Addon.tsx +1 -1
  165. package/src/client/addon/components/Icons.tsx +1 -1
  166. package/src/client/addon/components/Panel.tsx +1 -1
  167. package/src/client/addon/components/TestSelect.tsx +5 -5
  168. package/src/client/addon/components/Tools.tsx +9 -9
  169. package/src/client/addon/controller.ts +1 -1
  170. package/src/client/addon/manager.ts +4 -4
  171. package/src/client/addon/withCreevey.ts +26 -28
  172. package/src/client/shared/components/ImagesView/BlendView.tsx +1 -1
  173. package/src/client/shared/components/ImagesView/ImagesView.tsx +2 -2
  174. package/src/client/shared/components/ImagesView/SideBySideView.tsx +1 -1
  175. package/src/client/shared/components/ImagesView/SlideView.tsx +1 -1
  176. package/src/client/shared/components/ImagesView/SwapView.tsx +1 -1
  177. package/src/client/shared/components/PageFooter/PageFooter.tsx +2 -2
  178. package/src/client/shared/components/PageFooter/Paging.tsx +13 -13
  179. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -1
  180. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -3
  181. package/src/client/shared/components/ResultsPage.tsx +1 -1
  182. package/src/client/web/CreeveyApp.tsx +1 -1
  183. package/src/client/web/CreeveyLoader.tsx +1 -1
  184. package/src/client/web/CreeveyView/SideBar/Checkbox.tsx +6 -7
  185. package/src/client/web/CreeveyView/SideBar/Search.tsx +4 -4
  186. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +3 -10
  187. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +7 -6
  188. package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +7 -6
  189. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +8 -6
  190. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -3
  191. package/src/client/web/CreeveyView/SideBar/TestStatusIcon.tsx +18 -10
  192. package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +7 -10
  193. package/src/client/web/CreeveyView/SideBar/Toggle.tsx +1 -2
  194. package/src/client/web/KeyboardEventsContext.tsx +3 -4
  195. package/src/client/web/index.html +1 -1
  196. package/src/client/web/index.tsx +4 -3
  197. package/src/creevey.ts +25 -8
  198. package/src/index.ts +4 -2
  199. package/src/server/config.ts +12 -8
  200. package/src/server/docker.ts +58 -44
  201. package/src/server/index.ts +57 -18
  202. package/src/server/master/master.ts +3 -6
  203. package/src/server/master/pool.ts +20 -7
  204. package/src/server/master/runner.ts +2 -2
  205. package/src/server/master/server.ts +1 -0
  206. package/src/server/master/{index.ts → start.ts} +13 -11
  207. package/src/server/messages.ts +11 -75
  208. package/src/server/playwright/docker-file.ts +21 -0
  209. package/src/server/playwright/docker.ts +41 -0
  210. package/src/server/playwright/internal.ts +387 -0
  211. package/src/server/playwright/webdriver.ts +126 -0
  212. package/src/server/{storybook/providers → providers}/browser.ts +7 -8
  213. package/src/server/{storybook/providers → providers}/hybrid.ts +19 -19
  214. package/src/server/{worker/reporter.ts → reporter.ts} +40 -72
  215. package/src/server/selenium/internal.ts +785 -0
  216. package/src/server/selenium/selenoid.ts +12 -17
  217. package/src/server/selenium/webdriver.ts +136 -0
  218. package/src/server/stories.ts +18 -11
  219. package/src/server/telemetry.ts +2 -2
  220. package/src/server/utils.ts +9 -9
  221. package/src/server/webdriver.ts +127 -0
  222. package/src/server/worker/chai-image.ts +21 -133
  223. package/src/server/worker/match-image.ts +303 -0
  224. package/src/server/worker/start.ts +303 -0
  225. package/src/types.ts +161 -60
  226. package/dist/client/web/202.js +0 -1
  227. package/dist/client/web/270.js +0 -43
  228. package/dist/client/web/752.js +0 -1
  229. package/dist/client/web/main.js +0 -79
  230. package/dist/client/web/main.js.LICENSE.txt +0 -34
  231. package/dist/server/master/index.d.ts +0 -3
  232. package/dist/server/master/index.js.map +0 -1
  233. package/dist/server/selenium/browser.d.ts +0 -19
  234. package/dist/server/selenium/browser.js +0 -640
  235. package/dist/server/selenium/browser.js.map +0 -1
  236. package/dist/server/selenium/index.d.ts +0 -2
  237. package/dist/server/selenium/index.js +0 -19
  238. package/dist/server/selenium/index.js.map +0 -1
  239. package/dist/server/storybook/providers/browser.d.ts +0 -2
  240. package/dist/server/storybook/providers/browser.js.map +0 -1
  241. package/dist/server/storybook/providers/hybrid.d.ts +0 -2
  242. package/dist/server/storybook/providers/hybrid.js.map +0 -1
  243. package/dist/server/worker/helpers.d.ts +0 -8
  244. package/dist/server/worker/helpers.js +0 -57
  245. package/dist/server/worker/helpers.js.map +0 -1
  246. package/dist/server/worker/index.d.ts +0 -1
  247. package/dist/server/worker/index.js +0 -6
  248. package/dist/server/worker/index.js.map +0 -1
  249. package/dist/server/worker/reporter.d.ts +0 -8
  250. package/dist/server/worker/reporter.js.map +0 -1
  251. package/dist/server/worker/worker.d.ts +0 -4
  252. package/dist/server/worker/worker.js +0 -217
  253. package/dist/server/worker/worker.js.map +0 -1
  254. package/src/server/selenium/browser.ts +0 -840
  255. package/src/server/selenium/index.ts +0 -2
  256. package/src/server/worker/helpers.ts +0 -61
  257. package/src/server/worker/index.ts +0 -1
  258. package/src/server/worker/worker.ts +0 -245
  259. package/types/mocha.d.ts +0 -20
@@ -0,0 +1,41 @@
1
+ import { buildImage, runImage } from '../docker';
2
+ import { emitWorkerMessage, subscribeOn } from '../messages';
3
+ import { isInsideDocker } from '../utils';
4
+ import { LOCALHOST_REGEXP } from '../webdriver';
5
+ import { playwrightDockerFile } from './docker-file';
6
+
7
+ export async function startPlaywrightContainer(browserName: string, debug: boolean): Promise<string> {
8
+ const {
9
+ default: { version },
10
+ } = await import('playwright-core/package.json', { with: { type: 'json' } });
11
+
12
+ const imageName = `creevey/${browserName}:v${version}`;
13
+ const dockerfile = playwrightDockerFile(browserName, version);
14
+
15
+ await buildImage(imageName, dockerfile);
16
+
17
+ const port = await new Promise<number>((resolve) => {
18
+ subscribeOn('worker', (message) => {
19
+ if (message.type == 'port') {
20
+ resolve(message.payload.port);
21
+ }
22
+ });
23
+ emitWorkerMessage({ type: 'port', payload: { port: -1 } });
24
+ });
25
+
26
+ const host = await runImage(
27
+ imageName,
28
+ [],
29
+ {
30
+ ExposedPorts: { [`${port}/tcp`]: {} },
31
+ HostConfig: {
32
+ PortBindings: { ['4444/tcp']: [{ HostPort: `${port}` }] },
33
+ },
34
+ },
35
+ debug,
36
+ );
37
+
38
+ const gridUrl = `ws://localhost:${port}/creevey`;
39
+
40
+ return isInsideDocker ? gridUrl.replace(LOCALHOST_REGEXP, host) : gridUrl;
41
+ }
@@ -0,0 +1,387 @@
1
+ import { Browser, BrowserType, Page, chromium, firefox, webkit } from 'playwright-core';
2
+ import Logger from 'loglevel';
3
+ import chalk from 'chalk';
4
+ import { v4 } from 'uuid';
5
+ import prefix from 'loglevel-plugin-prefix';
6
+ import { SET_GLOBALS, STORY_RENDERED, UPDATE_STORY_ARGS } from '@storybook/core-events';
7
+ import { BrowserConfigObject, Config, Options, StoriesRaw, StoryInput, StorybookGlobals, noop } from '../../types';
8
+ import { subscribeOn } from '../messages';
9
+ import { appendIframePath, getAddresses, LOCALHOST_REGEXP, resolveStorybookUrl, storybookRootID } from '../webdriver';
10
+ import { isShuttingDown, runSequence } from '../utils';
11
+ import { colors, logger } from '../logger';
12
+ import { Args } from '@storybook/csf';
13
+
14
+ async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser | null> {
15
+ let timeout: NodeJS.Timeout | null = null;
16
+ let isTimeout = false;
17
+ let error: unknown = null;
18
+ return Promise.race([
19
+ new Promise<null>(
20
+ (resolve) =>
21
+ (timeout = setTimeout(() => {
22
+ isTimeout = true;
23
+ logger.error(`Can't connect to ${type.name()} playwright browser`, error);
24
+ resolve(null);
25
+ }, 10000)),
26
+ ),
27
+ (async () => {
28
+ let browser: Browser | null = null;
29
+ do {
30
+ try {
31
+ browser = await type.connect(gridUrl);
32
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
33
+ if (timeout) clearTimeout(timeout);
34
+ break;
35
+ } catch (e: unknown) {
36
+ error = e;
37
+ }
38
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
39
+ } while (!isTimeout);
40
+ return browser;
41
+ })(),
42
+ ]);
43
+ }
44
+
45
+ export class InternalBrowser {
46
+ #isShuttingDown = false;
47
+ #browser: Browser;
48
+ #page: Page;
49
+ #sessionId: string = v4();
50
+ #serverHost: string | null = null;
51
+ #serverPort: number;
52
+ #logger: Logger.Logger;
53
+ #unsubscribe: () => void = noop;
54
+ constructor(browser: Browser, page: Page, port: number) {
55
+ this.#browser = browser;
56
+ this.#page = page;
57
+ this.#serverPort = port;
58
+ this.#logger = Logger.getLogger(this.#sessionId);
59
+ this.#unsubscribe = subscribeOn('shutdown', () => {
60
+ void this.closeBrowser();
61
+ });
62
+ }
63
+
64
+ get browser() {
65
+ return this.#page;
66
+ }
67
+
68
+ get sessionId() {
69
+ return this.#sessionId;
70
+ }
71
+
72
+ async closeBrowser(): Promise<void> {
73
+ if (this.#isShuttingDown) return;
74
+
75
+ this.#isShuttingDown = true;
76
+ this.#unsubscribe();
77
+
78
+ try {
79
+ await this.#page.close();
80
+ await this.#browser.close();
81
+ } catch (_) {
82
+ /* noop */
83
+ }
84
+ }
85
+
86
+ async takeScreenshot(captureElement?: string | null, ignoreElements?: string | string[] | null): Promise<Buffer> {
87
+ // TODO Implement features from selenium `takeScreenshot`
88
+ // TODO Do we need scroll bar hack from selenium?
89
+ const ignore = Array.isArray(ignoreElements) ? ignoreElements : ignoreElements ? [ignoreElements] : [];
90
+ const mask = ignore.map((el) => this.#page.locator(el));
91
+ if (captureElement) {
92
+ const element = await this.#page.$(captureElement);
93
+ if (!element) throw new Error(`Element with selector ${captureElement} not found`);
94
+ return element.screenshot({ animations: 'disabled', mask });
95
+ }
96
+ return this.#page.screenshot({ animations: 'disabled', mask, fullPage: true });
97
+ }
98
+
99
+ waitForComplete(callback: (isCompleted: boolean) => void): void {
100
+ void this.#page.evaluate<boolean>(() => window.__CREEVEY_HAS_PLAY_COMPLETED_YET__()).then(callback);
101
+ }
102
+
103
+ async selectStory(id: string, waitForReady = false): Promise<boolean> {
104
+ // NOTE: Global variables might be reset after hot reload. I think it's workaround, maybe we need better solution
105
+ await this.updateBrowserGlobalVariables();
106
+ await this.resetMousePosition();
107
+
108
+ this.#logger.debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
109
+
110
+ const result = await this.#page.evaluate<
111
+ [error?: string | null, isCaptureCalled?: boolean] | null,
112
+ [id: string, shouldWaitForReady: boolean]
113
+ >(
114
+ ([id, shouldWaitForReady]) => {
115
+ if (typeof window.__CREEVEY_SELECT_STORY__ == 'undefined') {
116
+ return [
117
+ "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.",
118
+ ];
119
+ }
120
+ return window.__CREEVEY_SELECT_STORY__(id, shouldWaitForReady);
121
+ },
122
+ [id, waitForReady],
123
+ );
124
+
125
+ const [errorMessage, isCaptureCalled = false] = result ?? [];
126
+
127
+ if (errorMessage) throw new Error(errorMessage);
128
+
129
+ return isCaptureCalled;
130
+ }
131
+
132
+ async updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void> {
133
+ await this.#page.evaluate(
134
+ ([storyId, updatedArgs, UPDATE_STORY_ARGS, STORY_RENDERED]) => {
135
+ return new Promise((resolve) => {
136
+ window.__STORYBOOK_ADDONS_CHANNEL__.once(STORY_RENDERED, resolve);
137
+ window.__STORYBOOK_ADDONS_CHANNEL__.emit(UPDATE_STORY_ARGS, {
138
+ storyId,
139
+ updatedArgs,
140
+ });
141
+ });
142
+ },
143
+ [story.id, updatedArgs, UPDATE_STORY_ARGS, STORY_RENDERED] as const,
144
+ );
145
+ }
146
+
147
+ async loadStoriesFromBrowser(retry = false): Promise<StoriesRaw> {
148
+ try {
149
+ const stories = await this.#page.evaluate<StoriesRaw | undefined>(() => window.__CREEVEY_GET_STORIES__());
150
+
151
+ if (!stories) throw new Error("Can't get stories, it seems creevey or storybook API isn't available");
152
+
153
+ return stories;
154
+ } catch (error) {
155
+ // TODO Check how other solutions with playwright get stories from storybook
156
+ if (retry) throw error;
157
+ await new Promise((resolve) => setTimeout(resolve, 1000));
158
+ // NOTE: Try one more time because of dynamic nature of vite and storybook
159
+ return this.loadStoriesFromBrowser(true);
160
+ }
161
+ }
162
+
163
+ static async getBrowser(
164
+ browserName: string,
165
+ gridUrl: string,
166
+ config: Config,
167
+ options: Options,
168
+ ): Promise<InternalBrowser | null> {
169
+ const browserConfig = config.browsers[browserName] as BrowserConfigObject;
170
+ const { storybookUrl: address = config.storybookUrl, viewport, _storybookGlobals } = browserConfig;
171
+
172
+ let browser: Browser | null = null;
173
+
174
+ // TODO Support Selenium Grid 4
175
+ switch (browserConfig.browserName) {
176
+ case 'chromium':
177
+ browser = await tryConnect(chromium, gridUrl);
178
+ break;
179
+ case 'firefox':
180
+ browser = await tryConnect(firefox, gridUrl);
181
+ break;
182
+ case 'webkit':
183
+ browser = await tryConnect(webkit, gridUrl);
184
+ break;
185
+
186
+ default:
187
+ throw new Error(
188
+ `Unknown browser ${browserConfig.browserName}. Playwright supports browsers: chromium, firefox, webkit`,
189
+ );
190
+ }
191
+
192
+ if (!browser) {
193
+ return null;
194
+ }
195
+
196
+ const page = await browser.newPage();
197
+
198
+ // TODO Add debug output
199
+
200
+ const internalBrowser = new InternalBrowser(browser, page, options.port);
201
+
202
+ try {
203
+ if (isShuttingDown.current) return null;
204
+ const done = await internalBrowser.init({
205
+ browserName,
206
+ viewport,
207
+ storybookUrl: address,
208
+ storybookGlobals: _storybookGlobals,
209
+ resolveStorybookUrl: config.resolveStorybookUrl,
210
+ });
211
+
212
+ return done ? internalBrowser : null;
213
+ } catch (originalError) {
214
+ void internalBrowser.closeBrowser();
215
+
216
+ const message = originalError instanceof Error ? originalError.message : (originalError as string);
217
+ const error = new Error(`Can't load storybook root page: ${message}`);
218
+ if (originalError instanceof Error) error.stack = originalError.stack;
219
+
220
+ logger.error(error);
221
+
222
+ return null;
223
+ }
224
+ }
225
+
226
+ private async init({
227
+ browserName,
228
+ viewport,
229
+ storybookUrl,
230
+ storybookGlobals,
231
+ resolveStorybookUrl,
232
+ }: {
233
+ browserName: string;
234
+ viewport?: { width: number; height: number };
235
+ storybookUrl: string;
236
+ storybookGlobals?: StorybookGlobals;
237
+ resolveStorybookUrl?: () => Promise<string>;
238
+ }) {
239
+ const sessionId = this.#sessionId;
240
+
241
+ prefix.apply(this.#logger, {
242
+ format(level) {
243
+ const levelColor = colors[level.toUpperCase() as keyof typeof colors];
244
+ return `[${browserName}:${chalk.gray(sessionId)}] ${levelColor(level)} =>`;
245
+ },
246
+ });
247
+
248
+ this.#page.setDefaultNavigationTimeout(10000);
249
+ this.#page.setDefaultTimeout(60000);
250
+
251
+ return await runSequence(
252
+ [
253
+ () => this.openStorybookPage(storybookUrl, resolveStorybookUrl),
254
+ () => this.waitForStorybook(),
255
+ () => this.updateStorybookGlobals(storybookGlobals),
256
+ () => this.resolveCreeveyHost(),
257
+ () => this.updateBrowserGlobalVariables(),
258
+ () => this.resizeViewport(viewport),
259
+ ],
260
+ () => !this.#isShuttingDown,
261
+ );
262
+ }
263
+
264
+ private async openStorybookPage(storybookUrl: string, resolver?: () => Promise<string>): Promise<void> {
265
+ if (!LOCALHOST_REGEXP.test(storybookUrl)) {
266
+ await this.#page.goto(appendIframePath(storybookUrl));
267
+ return;
268
+ }
269
+
270
+ try {
271
+ if (resolver) {
272
+ this.#logger.debug('Resolving storybook url with custom resolver');
273
+
274
+ const resolvedUrl = await resolver();
275
+
276
+ this.#logger.debug(`Resolver storybook url ${resolvedUrl}`);
277
+
278
+ await this.#page.goto(appendIframePath(resolvedUrl));
279
+ } else {
280
+ await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url), this.#logger);
281
+ }
282
+ } catch (error) {
283
+ this.#logger.error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
284
+ throw error;
285
+ }
286
+ }
287
+
288
+ private async checkUrl(url: string): Promise<boolean> {
289
+ try {
290
+ this.#logger.debug(`Opening ${chalk.magenta(url)} and checking the page source`);
291
+ const response = await this.#page.goto(url, { waitUntil: 'commit' });
292
+ const source = await response?.text();
293
+
294
+ this.#logger.debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
295
+ return source?.includes(`id="${storybookRootID}"`) ?? false;
296
+ } catch {
297
+ return false;
298
+ }
299
+ }
300
+
301
+ private async waitForStorybook(): Promise<void> {
302
+ // TODO Duplicated code with selenium
303
+ this.#logger.debug('Waiting for `setStories` event to make sure that storybook is initiated');
304
+
305
+ const isTimeout = await Promise.race([
306
+ new Promise<boolean>((resolve) => {
307
+ setTimeout(() => {
308
+ resolve(true);
309
+ }, 60000);
310
+ }),
311
+ (async () => {
312
+ let wait = true;
313
+ do {
314
+ try {
315
+ // TODO Research a different way to ensure storybook is initiated
316
+ wait = await this.#page.evaluate((SET_GLOBALS: string) => {
317
+ if (typeof window.__STORYBOOK_ADDONS_CHANNEL__ == 'undefined') return true;
318
+ if (window.__STORYBOOK_ADDONS_CHANNEL__.last(SET_GLOBALS) == undefined) return true;
319
+ return false;
320
+ }, SET_GLOBALS);
321
+ } catch (e: unknown) {
322
+ this.#logger.debug('An error has been caught during the script:', e);
323
+ }
324
+ } while (wait);
325
+ return false;
326
+ })(),
327
+ ]);
328
+
329
+ // TODO Change the message to describe a reason why it might happen
330
+ if (isTimeout) throw new Error('Failed to wait `setStories` event');
331
+ }
332
+
333
+ private async updateStorybookGlobals(globals?: StorybookGlobals): Promise<void> {
334
+ if (!globals) return;
335
+
336
+ this.#logger.debug('Applying storybook globals');
337
+ await this.#page.evaluate((globals: StorybookGlobals) => {
338
+ window.__CREEVEY_UPDATE_GLOBALS__(globals);
339
+ }, globals);
340
+ }
341
+
342
+ private async resolveCreeveyHost(): Promise<void> {
343
+ const addresses = getAddresses();
344
+
345
+ this.#serverHost = await this.#page.evaluate(
346
+ ([hosts, port]) => {
347
+ return Promise.all(
348
+ hosts.map((host) => {
349
+ return Promise.race([
350
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
351
+ fetch('http://' + host + ':' + port + '/ping').then((response) => response.text()),
352
+ new Promise((_resolve, reject) => {
353
+ setTimeout(reject, 5000);
354
+ }),
355
+ ])
356
+ .then((pong) => (pong == 'pong' ? host : null))
357
+ .catch(() => null);
358
+ }),
359
+ ).then((hosts) => hosts.find((host) => host != null) ?? null);
360
+ },
361
+ [addresses, this.#serverPort] as const,
362
+ );
363
+
364
+ if (this.#serverHost == null) throw new Error("Can't reach creevey server from a browser");
365
+ }
366
+
367
+ private async updateBrowserGlobalVariables() {
368
+ await this.#page.evaluate(
369
+ ([workerId, creeveyHost, creeveyPort]) => {
370
+ window.__CREEVEY_WORKER_ID__ = workerId;
371
+ window.__CREEVEY_SERVER_HOST__ = creeveyHost ?? 'localhost';
372
+ window.__CREEVEY_SERVER_PORT__ = creeveyPort;
373
+ },
374
+ [process.pid, this.#serverHost, this.#serverPort] as const,
375
+ );
376
+ }
377
+
378
+ private async resizeViewport(viewport?: { width: number; height: number }): Promise<void> {
379
+ if (!viewport) return;
380
+
381
+ await this.#page.setViewportSize(viewport);
382
+ }
383
+
384
+ private async resetMousePosition(): Promise<void> {
385
+ await this.#page.mouse.move(0, 0);
386
+ }
387
+ }
@@ -0,0 +1,126 @@
1
+ import { Args } from '@storybook/csf';
2
+ import { Config, Options, StoriesRaw, StoryInput } from '../../types';
3
+ import { logger } from '../logger';
4
+ import { subscribeOn } from '../messages';
5
+ import { CreeveyWebdriverBase } from '../webdriver';
6
+ import type { InternalBrowser } from './internal';
7
+
8
+ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
9
+ #browser: InternalBrowser | null = null;
10
+ #browserName: string;
11
+ #gridUrl: string;
12
+ #config: Config;
13
+ #options: Options;
14
+ constructor(browser: string, gridUrl: string, config: Config, options: Options) {
15
+ super();
16
+
17
+ this.#browserName = browser;
18
+ this.#gridUrl = gridUrl;
19
+ this.#config = config;
20
+ this.#options = options;
21
+
22
+ subscribeOn('shutdown', () => {
23
+ void this.#browser?.closeBrowser().finally(() => process.exit());
24
+ this.#browser = null;
25
+ });
26
+ }
27
+
28
+ get browser() {
29
+ return this.#browser?.browser;
30
+ }
31
+
32
+ getSessionId(): Promise<string> {
33
+ if (!this.#browser) {
34
+ // TODO Describe the error
35
+ throw new Error('Browser is not initialized');
36
+ }
37
+
38
+ return Promise.resolve(this.#browser.sessionId);
39
+ }
40
+
41
+ async openBrowser(fresh = false): Promise<PlaywrightWebdriver | null> {
42
+ if (this.#browser) {
43
+ if (fresh) {
44
+ await this.#browser.closeBrowser();
45
+ this.#browser = null;
46
+ } else {
47
+ return this;
48
+ }
49
+ }
50
+
51
+ const internalModule = await (async () => {
52
+ try {
53
+ return await import('./internal.js');
54
+ } catch (error) {
55
+ logger.error(error);
56
+ return null;
57
+ }
58
+ })();
59
+
60
+ if (!internalModule) return null;
61
+
62
+ const { InternalBrowser } = internalModule;
63
+ const browser = await InternalBrowser.getBrowser(this.#browserName, this.#gridUrl, this.#config, this.#options);
64
+
65
+ if (!browser) return null;
66
+
67
+ this.#browser = browser;
68
+
69
+ return this;
70
+ }
71
+
72
+ async closeBrowser(): Promise<void> {
73
+ if (this.#browser) {
74
+ await this.#browser.closeBrowser();
75
+ this.#browser = null;
76
+ }
77
+ }
78
+
79
+ async loadStoriesFromBrowser(): Promise<StoriesRaw> {
80
+ if (!this.#browser) {
81
+ // TODO Describe the error
82
+ throw new Error('Browser is not initialized');
83
+ }
84
+
85
+ return this.#browser.loadStoriesFromBrowser();
86
+ }
87
+
88
+ protected async takeScreenshot(
89
+ captureElement: string | null,
90
+ ignoreElements?: string | string[] | null,
91
+ ): Promise<Buffer> {
92
+ if (!this.#browser) {
93
+ // TODO Describe the error
94
+ throw new Error('Browser is not initialized');
95
+ }
96
+
97
+ return this.#browser.takeScreenshot(captureElement, ignoreElements);
98
+ }
99
+
100
+ protected waitForComplete(callback: (isCompleted: boolean) => void): void {
101
+ if (!this.#browser) {
102
+ // TODO Describe the error
103
+ throw new Error('Browser is not initialized');
104
+ }
105
+
106
+ this.#browser.waitForComplete(callback);
107
+ }
108
+
109
+ protected async selectStory(id: string, waitForReady?: boolean): Promise<boolean> {
110
+ if (!this.#browser) {
111
+ // TODO Describe the error
112
+ throw new Error('Browser is not initialized');
113
+ }
114
+
115
+ return this.#browser.selectStory(id, waitForReady);
116
+ }
117
+
118
+ protected async updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void> {
119
+ if (!this.#browser) {
120
+ // TODO Describe the error
121
+ throw new Error('Browser is not initialized');
122
+ }
123
+
124
+ return this.#browser.updateStoryArgs(story, updatedArgs);
125
+ }
126
+ }
@@ -1,12 +1,11 @@
1
1
  import cluster from 'cluster';
2
- import type { CreeveyStory, StoriesProvider, StoriesRaw } from '../../../types.js';
3
- import { loadStoriesFromBrowser } from '../../selenium/index.js';
4
- import { emitStoriesMessage, sendStoriesMessage, subscribeOn, subscribeOnWorker } from '../../messages.js';
5
- import { isDefined } from '../../../types.js';
6
- import { logger } from '../../logger.js';
7
- import { deserializeRawStories } from '../../../shared/index.js';
2
+ import type { CreeveyStory, StoriesProvider, StoriesRaw } from '../../types.js';
3
+ import { emitStoriesMessage, sendStoriesMessage, subscribeOn, subscribeOnWorker } from '../messages.js';
4
+ import { isDefined } from '../../types.js';
5
+ import { logger } from '../logger.js';
6
+ import { deserializeRawStories } from '../../shared/index.js';
8
7
 
9
- export const loadStories: StoriesProvider = async (_config, _options, storiesListener) => {
8
+ export const loadStories: StoriesProvider = async (_config, storiesListener, webdriver) => {
10
9
  if (cluster.isPrimary) {
11
10
  return new Promise<StoriesRaw>((resolve) => {
12
11
  const worker = Object.values(cluster.workers ?? {})
@@ -40,7 +39,7 @@ export const loadStories: StoriesProvider = async (_config, _options, storiesLis
40
39
  emitStoriesMessage({ type: 'set', payload: { stories, oldTests: storiesWithOldTests } });
41
40
  if (message.type == 'update') storiesListener(new Map(message.payload));
42
41
  });
43
- const stories = deserializeRawStories(await loadStoriesFromBrowser());
42
+ const stories = deserializeRawStories((await webdriver?.loadStoriesFromBrowser()) ?? {});
44
43
 
45
44
  const storiesWithOldTests: string[] = [];
46
45
 
@@ -1,17 +1,13 @@
1
1
  import chokidar from 'chokidar';
2
2
 
3
3
  import { loadStories as browserProvider } from './browser.js';
4
- import type { Config, StoryInput, CreeveyStoryParams, CreeveyStory, StoriesProvider } from '../../../types.js';
5
- import { logger } from '../../logger.js';
6
- import parse, { CreeveyParamsByStoryId } from '../../testsFiles/parser.js';
7
- import { readDirRecursive } from '../../utils.js';
8
- import { combineParameters } from '../../../shared/index.js';
4
+ import type { Config, CreeveyStoryParams, CreeveyStory, StoriesProvider } from '../../types.js';
5
+ import { logger } from '../logger.js';
6
+ import parse, { CreeveyParamsByStoryId } from '../testsFiles/parser.js';
7
+ import { readDirRecursive } from '../utils.js';
8
+ import { combineParameters } from '../../shared/index.js';
9
9
 
10
- export const loadStories: StoriesProvider = async (
11
- _config: Config,
12
- _options,
13
- storiesListener: (stories: Map<string, StoryInput[]>) => void,
14
- ) => {
10
+ export const loadStories: StoriesProvider = async (config, storiesListener, webdriver) => {
15
11
  let creeveyParamsByStoryId: Partial<CreeveyParamsByStoryId> = {};
16
12
 
17
13
  const mergeParamsFromTestsToStory = (story: CreeveyStory, creeveyParams: CreeveyStoryParams): void => {
@@ -20,18 +16,22 @@ export const loadStories: StoriesProvider = async (
20
16
  }
21
17
  };
22
18
 
23
- const stories = await browserProvider(_config, {}, (updatedStoriesByFiles) => {
24
- Array.from(updatedStoriesByFiles.entries()).forEach(([, storiesArray]) => {
25
- storiesArray.forEach((story) => {
26
- const creeveyParams = creeveyParamsByStoryId[story.id];
27
- if (creeveyParams) mergeParamsFromTestsToStory(story, creeveyParams);
19
+ const stories = await browserProvider(
20
+ config,
21
+ (updatedStoriesByFiles) => {
22
+ Array.from(updatedStoriesByFiles.entries()).forEach(([, storiesArray]) => {
23
+ storiesArray.forEach((story) => {
24
+ const creeveyParams = creeveyParamsByStoryId[story.id];
25
+ if (creeveyParams) mergeParamsFromTestsToStory(story, creeveyParams);
26
+ });
28
27
  });
29
- });
30
- storiesListener(updatedStoriesByFiles);
31
- });
28
+ storiesListener(updatedStoriesByFiles);
29
+ },
30
+ webdriver,
31
+ );
32
32
 
33
33
  // TODO fix test files hot reloading
34
- creeveyParamsByStoryId = await parseParams(_config /*, (data) => console.log(data) */);
34
+ creeveyParamsByStoryId = await parseParams(config /*, (data) => console.log(data) */);
35
35
 
36
36
  Object.entries(stories).forEach(([storyId, story]) => {
37
37
  const creeveyParams = creeveyParamsByStoryId[storyId];