creevey 0.9.1 → 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 +13 -7
  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 +4 -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 +127 -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 +25 -9
  204. package/src/server/master/runner.ts +4 -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 +162 -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 -212
  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 -240
  259. package/types/mocha.d.ts +0 -20
@@ -1,16 +1,17 @@
1
1
  import path from 'path';
2
2
  import assert from 'assert';
3
- import cluster from 'cluster';
4
3
  import { lstatSync, existsSync } from 'fs';
5
4
  import { mkdir, writeFile, copyFile } from 'fs/promises';
6
5
  import sh from 'shelljs';
7
- import { Octokit } from '@octokit/core';
8
- import { Config, BrowserConfig } from '../../types.js';
6
+ import { Config, BrowserConfigObject } from '../../types.js';
9
7
  import { downloadBinary, getCreeveyCache } from '../utils.js';
10
8
  import { pullImages, runImage } from '../docker.js';
11
9
  import { subscribeOn } from '../messages.js';
12
10
 
13
- async function createSelenoidConfig(browsers: BrowserConfig[], { useDocker }: { useDocker: boolean }): Promise<string> {
11
+ async function createSelenoidConfig(
12
+ browsers: BrowserConfigObject[],
13
+ { useDocker }: { useDocker: boolean },
14
+ ): Promise<string> {
14
15
  const selenoidConfig: Partial<
15
16
  Record<
16
17
  string,
@@ -20,7 +21,7 @@ async function createSelenoidConfig(browsers: BrowserConfig[], { useDocker }: {
20
21
  }
21
22
  >
22
23
  > = {};
23
- const cacheDir = getCreeveyCache();
24
+ const cacheDir = await getCreeveyCache();
24
25
 
25
26
  assert(cacheDir, "Couldn't get cache directory");
26
27
 
@@ -29,9 +30,7 @@ async function createSelenoidConfig(browsers: BrowserConfig[], { useDocker }: {
29
30
  browsers.forEach(
30
31
  ({
31
32
  browserName,
32
- // eslint-disable-next-line @typescript-eslint/no-deprecated
33
- version = 'latest',
34
- browserVersion = version,
33
+ browserVersion = 'latest',
35
34
  dockerImage = `selenoid/${browserName}:${browserVersion}`,
36
35
  webdriverCommand = [],
37
36
  }) => {
@@ -58,6 +57,8 @@ async function downloadSelenoidBinary(destination: string): Promise<void> {
58
57
  linux: 'selenoid_linux_amd64',
59
58
  win32: 'selenoid_windows_amd64.exe',
60
59
  };
60
+ // TODO Replace with `import from`
61
+ const { Octokit } = await import('@octokit/core');
61
62
  const octokit = new Octokit();
62
63
  const response = await octokit.request('GET /repos/{owner}/{repo}/releases/latest', {
63
64
  owner: 'aerokube',
@@ -79,11 +80,7 @@ async function downloadSelenoidBinary(destination: string): Promise<void> {
79
80
  }
80
81
 
81
82
  export async function startSelenoidStandalone(config: Config, debug: boolean): Promise<void> {
82
- config.gridUrl = 'http://localhost:4444/wd/hub';
83
-
84
- if (cluster.isWorker) return;
85
-
86
- const browsers = (Object.values(config.browsers) as BrowserConfig[]).filter((browser) => !browser.gridUrl);
83
+ const browsers = (Object.values(config.browsers) as BrowserConfigObject[]).filter((browser) => !browser.gridUrl);
87
84
  const selenoidConfigDir = await createSelenoidConfig(browsers, { useDocker: false });
88
85
  const binaryPath = path.join(selenoidConfigDir, process.platform == 'win32' ? 'selenoid.exe' : 'selenoid');
89
86
  if (config.selenoidPath) {
@@ -113,16 +110,14 @@ export async function startSelenoidStandalone(config: Config, debug: boolean): P
113
110
  }
114
111
 
115
112
  export async function startSelenoidContainer(config: Config, debug: boolean): Promise<string> {
116
- const browsers = (Object.values(config.browsers) as BrowserConfig[]).filter((browser) => !browser.gridUrl);
113
+ const browsers = (Object.values(config.browsers) as BrowserConfigObject[]).filter((browser) => !browser.gridUrl);
117
114
  const images: string[] = [];
118
115
  let limit = 0;
119
116
 
120
117
  browsers.forEach(
121
118
  ({
122
119
  browserName,
123
- // eslint-disable-next-line @typescript-eslint/no-deprecated
124
- version = 'latest',
125
- browserVersion = version,
120
+ browserVersion = 'latest',
126
121
  limit: browserLimit = 1,
127
122
  dockerImage = `selenoid/${browserName}:${browserVersion}`,
128
123
  }) => {
@@ -0,0 +1,136 @@
1
+ import { Args } from '@storybook/csf';
2
+ import { Config, StorybookGlobals, StoryInput, StoriesRaw, Options } from '../../types.js';
3
+ import { subscribeOn } from '../messages.js';
4
+ import { CreeveyWebdriverBase } from '../webdriver.js';
5
+ import type { InternalBrowser } from './internal.js';
6
+ import { logger } from '../logger.js';
7
+
8
+ declare global {
9
+ interface Window {
10
+ __CREEVEY_RESTORE_SCROLL__?: () => void;
11
+ __CREEVEY_UPDATE_GLOBALS__: (globals: StorybookGlobals) => void;
12
+ __CREEVEY_INSERT_IGNORE_STYLES__: (ignoreElements: string[]) => HTMLStyleElement;
13
+ __CREEVEY_REMOVE_IGNORE_STYLES__: (ignoreStyles: HTMLStyleElement) => void;
14
+ }
15
+ }
16
+
17
+ // TODO Update context interface through references
18
+ export class SeleniumWebdriver extends CreeveyWebdriverBase {
19
+ #browser: InternalBrowser | null = null;
20
+ #browserName: string;
21
+ #gridUrl: string;
22
+ #config: Config;
23
+ #options: Options;
24
+ constructor(browser: string, gridUrl: string, config: Config, options: Options) {
25
+ super();
26
+
27
+ this.#browserName = browser;
28
+ this.#gridUrl = gridUrl;
29
+ this.#config = config;
30
+ this.#options = options;
31
+
32
+ subscribeOn('shutdown', () => {
33
+ void this.#browser?.closeBrowser().finally(() => process.exit());
34
+ this.#browser = null;
35
+ });
36
+ }
37
+
38
+ get browser() {
39
+ return this.#browser?.browser;
40
+ }
41
+
42
+ getSessionId(): Promise<string> {
43
+ if (!this.#browser) {
44
+ // TODO Describe the error
45
+ throw new Error('Browser is not initialized');
46
+ }
47
+
48
+ return this.#browser.browser.getSession().then((session) => session.getId());
49
+ }
50
+
51
+ async openBrowser(fresh = false): Promise<SeleniumWebdriver | null> {
52
+ if (this.#browser) {
53
+ if (fresh) {
54
+ await this.#browser.closeBrowser();
55
+ this.#browser = null;
56
+ } else {
57
+ return this;
58
+ }
59
+ }
60
+
61
+ const internalModule = await (async () => {
62
+ try {
63
+ return await import('./internal.js');
64
+ } catch (error) {
65
+ logger.error(error);
66
+ return null;
67
+ }
68
+ })();
69
+
70
+ if (!internalModule) return null;
71
+
72
+ const { InternalBrowser } = internalModule;
73
+ const browser = await InternalBrowser.getBrowser(this.#browserName, this.#gridUrl, this.#config, this.#options);
74
+
75
+ if (!browser) return null;
76
+
77
+ this.#browser = browser;
78
+
79
+ return this;
80
+ }
81
+
82
+ async closeBrowser(): Promise<void> {
83
+ if (this.#browser) {
84
+ await this.#browser.closeBrowser();
85
+ this.#browser = null;
86
+ }
87
+ }
88
+
89
+ async loadStoriesFromBrowser(): Promise<StoriesRaw> {
90
+ if (!this.#browser) {
91
+ // TODO Describe the error
92
+ throw new Error('Browser is not initialized');
93
+ }
94
+
95
+ return this.#browser.loadStoriesFromBrowser();
96
+ }
97
+
98
+ protected async takeScreenshot(
99
+ captureElement: string | null,
100
+ ignoreElements?: string | string[] | null,
101
+ ): Promise<Buffer> {
102
+ if (!this.#browser) {
103
+ // TODO Describe the error
104
+ throw new Error('Browser is not initialized');
105
+ }
106
+
107
+ return this.#browser.takeScreenshot(captureElement, ignoreElements);
108
+ }
109
+
110
+ protected waitForComplete(callback: (isCompleted: boolean) => void): void {
111
+ if (!this.#browser) {
112
+ // TODO Describe the error
113
+ throw new Error('Browser is not initialized');
114
+ }
115
+
116
+ this.#browser.waitForComplete(callback);
117
+ }
118
+
119
+ protected async selectStory(id: string, waitForReady?: boolean): Promise<boolean> {
120
+ if (!this.#browser) {
121
+ // TODO Describe the error
122
+ throw new Error('Browser is not initialized');
123
+ }
124
+
125
+ return this.#browser.selectStory(id, waitForReady);
126
+ }
127
+
128
+ protected async updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void> {
129
+ if (!this.#browser) {
130
+ // TODO Describe the error
131
+ throw new Error('Browser is not initialized');
132
+ }
133
+
134
+ return this.#browser.updateStoryArgs(story, updatedArgs);
135
+ }
136
+ }
@@ -2,7 +2,6 @@ import path from 'path';
2
2
  import { mkdirSync, writeFileSync } from 'fs';
3
3
  import { createHash } from 'crypto';
4
4
  import _ from 'lodash';
5
- import type { Context } from 'mocha';
6
5
  import type {
7
6
  TestData,
8
7
  CreeveyStoryParams,
@@ -11,26 +10,27 @@ import type {
11
10
  ServerTest,
12
11
  StoryInput,
13
12
  CreeveyTestFunction,
13
+ CreeveyTestContext,
14
14
  } from '../types.js';
15
15
  import { isDefined, isFunction } from '../types.js';
16
16
  import { shouldSkip } from './utils.js';
17
17
 
18
18
  function storyTestFabric(delay?: number, testFn?: CreeveyTestFunction) {
19
- return async function storyTest(this: Context) {
19
+ return async function storyTest(context: CreeveyTestContext) {
20
20
  if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
21
21
  await (testFn
22
- ? testFn.call(this)
23
- : this.screenshots.length > 0
24
- ? this.expect(
25
- this.screenshots.reduce(
22
+ ? testFn(context)
23
+ : context.screenshots.length > 0
24
+ ? context.matchImages(
25
+ context.screenshots.reduce(
26
26
  (screenshots, { imageName, screenshot }, index) => ({
27
27
  ...screenshots,
28
28
  [imageName ?? `screenshot_${index}`]: screenshot,
29
29
  }),
30
30
  {},
31
31
  ),
32
- ).to.matchImages()
33
- : this.expect(await this.takeScreenshot()).to.matchImage());
32
+ )
33
+ : context.matchImage(await context.takeScreenshot()));
34
34
  };
35
35
  }
36
36
 
@@ -41,10 +41,17 @@ function createCreeveyTest(
41
41
  testName?: string,
42
42
  ): TestData {
43
43
  const { title, name, id: storyId } = storyMeta;
44
- const path = [title, name, testName, browser].filter(isDefined);
44
+ const testPath = [title, name, testName, browser].filter(isDefined);
45
45
  const skip = skipOptions ? shouldSkip(browser, { title, name }, skipOptions, testName) : false;
46
- const id = createHash('sha1').update(path.join('/')).digest('hex');
47
- return { id, skip, browser, testName, storyPath: [...title.split('/').map((x) => x.trim()), name], storyId };
46
+ const id = createHash('sha1').update(testPath.join('/')).digest('hex');
47
+ return {
48
+ id,
49
+ skip,
50
+ browser,
51
+ testName,
52
+ storyPath: [...title.split('/').map((x) => x.trim()), name],
53
+ storyId,
54
+ };
48
55
  }
49
56
 
50
57
  function convertStories(browserName: string, stories: StoriesRaw | StoryInput[]): Partial<Record<string, ServerTest>> {
@@ -6,7 +6,7 @@ import { set } from 'lodash';
6
6
  import { v4 } from 'uuid';
7
7
  import { pathToFileURL } from 'url';
8
8
  import { createRequire } from 'module';
9
- import { Config, CreeveyStatus, isDefined, Options } from '../types';
9
+ import { Config, CreeveyStatus, isDefined, Options } from '../types.js';
10
10
 
11
11
  const konturGitHost = 'git.skbkontur.ru';
12
12
 
@@ -151,7 +151,7 @@ export async function sendScreenshotsCount(
151
151
  name,
152
152
  typeof browser === 'object'
153
153
  ? {
154
- name: browser.name,
154
+ name: name,
155
155
  gridUrl: browser.gridUrl ? sanitizeGridUrl(browser.gridUrl) : undefined,
156
156
  browserName: browser.browserName,
157
157
  browserVersion: browser.browserVersion,
@@ -4,7 +4,6 @@ import cluster from 'cluster';
4
4
  import { dirname } from 'path';
5
5
  import { fileURLToPath, pathToFileURL } from 'url';
6
6
  import { createRequire } from 'module';
7
- import findCacheDir from 'find-cache-dir';
8
7
  import { register as esmRegister } from 'tsx/esm/api';
9
8
  import { register as cjsRegister } from 'tsx/cjs/api';
10
9
  import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest } from '../types.js';
@@ -14,8 +13,6 @@ const importMetaUrl = pathToFileURL(__filename).href;
14
13
 
15
14
  export const isShuttingDown = { current: false };
16
15
 
17
- export const LOCALHOST_REGEXP = /(localhost|127\.0\.0\.1)/i;
18
-
19
16
  export const configExt = ['.js', '.mjs', '.ts', '.cjs', '.mts', '.cts'];
20
17
 
21
18
  export const skipOptionKeys = ['in', 'kinds', 'stories', 'tests', 'reason'];
@@ -99,18 +96,20 @@ export async function shutdownWorkers(): Promise<void> {
99
96
  emitShutdownMessage();
100
97
  }
101
98
 
102
- export function shutdown(): void {
103
- process.exit();
104
- }
105
-
106
- export function getCreeveyCache(): string | undefined {
99
+ export async function getCreeveyCache(): Promise<string | undefined> {
100
+ const { default: findCacheDir } = await import('find-cache-dir');
107
101
  return findCacheDir({ name: 'creevey', cwd: dirname(fileURLToPath(importMetaUrl)) });
108
102
  }
109
103
 
110
- export async function runSequence(seq: (() => unknown)[], predicate: () => boolean): Promise<void> {
104
+ export async function runSequence(seq: (() => unknown)[], predicate: () => boolean): Promise<boolean> {
111
105
  for (const fn of seq) {
112
106
  if (predicate()) await fn();
113
107
  }
108
+ return predicate();
109
+ }
110
+
111
+ export function getTestPath(test: ServerTest): string[] {
112
+ return [...test.storyPath, test.testName, test.browser].filter(isDefined);
114
113
  }
115
114
 
116
115
  export function testsToImages(tests: (TestData | undefined)[]): Set<string> {
@@ -189,6 +188,7 @@ const [nodeVersion] = process.versions.node.split('.').map(Number);
189
188
  export async function loadThroughTSX<T>(
190
189
  callback: (load: (modulePath: string) => Promise<T>) => Promise<T>,
191
190
  ): Promise<T> {
191
+ // TODO Check if it work in node18 and type: 'module'
192
192
  const unregister = nodeVersion > 18 ? esmRegister() : cjsRegister();
193
193
 
194
194
  const result = await callback((modulePath) =>
@@ -0,0 +1,127 @@
1
+ import Logger from 'loglevel';
2
+ import chalk from 'chalk';
3
+ import { networkInterfaces } from 'os';
4
+ import { logger as defaultLogger } from './logger.js';
5
+ import { Args } from '@storybook/csf';
6
+ import {
7
+ isDefined,
8
+ StoryInput,
9
+ BaseCreeveyTestContext,
10
+ CreeveyTestContext,
11
+ CreeveyStoryParams,
12
+ StoriesRaw,
13
+ CreeveyWebdriver,
14
+ } from '../types.js';
15
+ import { emitStoriesMessage, subscribeOn } from './messages.js';
16
+
17
+ export const storybookRootID = 'storybook-root';
18
+ export const LOCALHOST_REGEXP = /(localhost|127\.0\.0\.1)/i;
19
+ const DOCKER_INTERNAL = 'host.docker.internal';
20
+
21
+ export async function resolveStorybookUrl(
22
+ storybookUrl: string,
23
+ checkUrl: (url: string) => Promise<boolean>,
24
+ logger: Logger.Logger = defaultLogger,
25
+ ): Promise<string> {
26
+ logger.debug('Resolving storybook url');
27
+ const addresses = getAddresses();
28
+ for (const ip of addresses) {
29
+ const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
30
+ logger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
31
+ if (await checkUrl(resolvedUrl)) {
32
+ logger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
33
+ return resolvedUrl;
34
+ }
35
+ }
36
+ const error = new Error('Please specify `storybookUrl` with IP address that accessible from remote browser');
37
+ error.name = 'ResolveUrlError';
38
+ throw error;
39
+ }
40
+
41
+ export function appendIframePath(url: string): string {
42
+ return `${url.replace(/\/$/, '')}/iframe.html`;
43
+ }
44
+
45
+ export function getAddresses(): string[] {
46
+ // TODO Check if docker is used
47
+ return [DOCKER_INTERNAL].concat(
48
+ ...Object.values(networkInterfaces())
49
+ .filter(isDefined)
50
+ .map((network) => network.filter((info) => info.family == 'IPv4').map((info) => info.address)),
51
+ );
52
+ }
53
+
54
+ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
55
+ protected abstract takeScreenshot(
56
+ captureElement: string | null,
57
+ ignoreElements?: string | string[] | null,
58
+ ): Promise<Buffer>;
59
+
60
+ protected abstract waitForComplete(callback: (isCompleted: boolean) => void): void;
61
+
62
+ protected abstract selectStory(id: string, waitForReady?: boolean): Promise<boolean>;
63
+
64
+ protected abstract updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void>;
65
+
66
+ abstract getSessionId(): Promise<string>;
67
+
68
+ abstract openBrowser(fresh?: boolean): Promise<CreeveyWebdriver | null>;
69
+
70
+ abstract closeBrowser(): Promise<void>;
71
+
72
+ abstract loadStoriesFromBrowser(): Promise<StoriesRaw>;
73
+
74
+ async switchStory(
75
+ story: StoryInput,
76
+ context: BaseCreeveyTestContext,
77
+ logger: Logger.Logger,
78
+ ): Promise<CreeveyTestContext> {
79
+ const { id, title, name, parameters } = story;
80
+ const {
81
+ captureElement = `#${storybookRootID}`,
82
+ waitForReady,
83
+ ignoreElements,
84
+ } = (parameters.creevey ?? {}) as CreeveyStoryParams;
85
+
86
+ logger.debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
87
+
88
+ let storyPlayResolver: (isCompleted: boolean) => void;
89
+ let waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
90
+ const unsubscribe = subscribeOn('stories', (message) => {
91
+ if (message.type != 'capture') return;
92
+ const { payload = {}, payload: { imageName } = {} } = message;
93
+ void this.takeScreenshot(payload.captureElement ?? captureElement, payload.ignoreElements ?? ignoreElements).then(
94
+ (screenshot) => {
95
+ context.screenshots.push({ imageName, screenshot });
96
+
97
+ this.waitForComplete(storyPlayResolver);
98
+
99
+ emitStoriesMessage({ type: 'capture' });
100
+ },
101
+ );
102
+ });
103
+
104
+ const isCaptureCalled = await this.selectStory(id, waitForReady);
105
+
106
+ if (isCaptureCalled) {
107
+ logger.debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
108
+ while (!(await waitForComplete)) {
109
+ waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
110
+ }
111
+ }
112
+
113
+ unsubscribe();
114
+
115
+ if (isCaptureCalled) logger.debug(`Story ${chalk.magenta(id)} completed capturing`);
116
+ else logger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
117
+
118
+ return Object.assign(
119
+ {
120
+ takeScreenshot: () => this.takeScreenshot(captureElement, ignoreElements),
121
+ updateStoryArgs: (updatedArgs: Args) => this.updateStoryArgs(story, updatedArgs),
122
+ captureElement,
123
+ },
124
+ context,
125
+ );
126
+ }
127
+ }
@@ -1,147 +1,35 @@
1
- import { PNG } from 'pngjs';
2
- import pixelmatch from 'pixelmatch';
3
-
4
- import { DiffOptions, ImagesError } from '../../types.js';
5
-
6
- function normalizeImageSize(image: PNG, width: number, height: number): Buffer {
7
- const normalizedImage = Buffer.alloc(4 * width * height);
8
-
9
- for (let y = 0; y < height; y++) {
10
- for (let x = 0; x < width; x++) {
11
- const i = (y * width + x) * 4;
12
- if (x < image.width && y < image.height) {
13
- const j = (y * image.width + x) * 4;
14
- normalizedImage[i + 0] = image.data[j + 0];
15
- normalizedImage[i + 1] = image.data[j + 1];
16
- normalizedImage[i + 2] = image.data[j + 2];
17
- normalizedImage[i + 3] = image.data[j + 3];
18
- } else {
19
- normalizedImage[i + 0] = 0;
20
- normalizedImage[i + 1] = 0;
21
- normalizedImage[i + 2] = 0;
22
- normalizedImage[i + 3] = 0;
23
- }
24
- }
25
- }
26
- return normalizedImage;
27
- }
28
-
29
- function hasDiffPixels(diff: Buffer): boolean {
30
- for (let i = 0; i < diff.length; i += 4) {
31
- if (diff[i + 0] == 255 && diff[i + 1] == 0 && diff[i + 2] == 0 && diff[i + 3] == 255) return true;
32
- }
33
- return false;
34
- }
35
-
36
- function compareImages(expect: Buffer, actual: Buffer, diffOptions: DiffOptions): { isEqual: boolean; diff: Buffer } {
37
- const expectImage = PNG.sync.read(expect);
38
- const actualImage = PNG.sync.read(actual);
39
-
40
- const width = Math.max(actualImage.width, expectImage.width);
41
- const height = Math.max(actualImage.height, expectImage.height);
42
-
43
- const diffImage = new PNG({ width, height });
44
-
45
- let actualImageData = actualImage.data;
46
- if (actualImage.width < width || actualImage.height < height) {
47
- actualImageData = normalizeImageSize(actualImage, width, height);
48
- }
49
-
50
- let expectImageData = expectImage.data;
51
- if (expectImage.width < width || expectImage.height < height) {
52
- expectImageData = normalizeImageSize(expectImage, width, height);
53
- }
54
-
55
- pixelmatch(expectImageData, actualImageData, diffImage.data, width, height, diffOptions);
56
-
57
- return {
58
- isEqual: !hasDiffPixels(diffImage.data),
59
- diff: PNG.sync.write(diffImage),
60
- };
61
- }
62
-
1
+ import Logger from 'loglevel';
63
2
  export default function (
64
- getExpected: (
65
- imageName?: string,
66
- ) => Promise<
67
- | { expected: Buffer | null; onCompare: (actual: Buffer, expect?: Buffer, diff?: Buffer) => Promise<void> }
68
- | Buffer
69
- | null
70
- >,
71
- diffOptions: DiffOptions,
3
+ matchImage: (image: Buffer, imageName?: string) => Promise<void>,
4
+ matchImages: (images: Record<string, Buffer>) => Promise<void>,
5
+ logger: Logger.Logger,
72
6
  ) {
7
+ let isWarningShown = false;
73
8
  return function chaiImage({ Assertion }: Chai.ChaiStatic, utils: Chai.ChaiUtils): void {
74
- async function assertImage(actual: Buffer, imageName?: string): Promise<string | undefined> {
75
- let onCompare: (actual: Buffer, expect?: Buffer, diff?: Buffer) => Promise<void> = () => Promise.resolve();
76
- let expected = await getExpected(imageName);
77
- if (!(expected instanceof Buffer) && expected != null) ({ expected, onCompare } = expected);
78
-
79
- if (expected == null) {
80
- await onCompare(actual);
81
- return imageName ? `Expected image '${imageName}' does not exists` : 'Expected image does not exists';
82
- }
83
-
84
- if (actual.equals(expected)) {
85
- await onCompare(actual);
86
- return;
87
- }
88
-
89
- const { isEqual, diff } = compareImages(expected, actual, diffOptions);
90
-
91
- if (isEqual) {
92
- await onCompare(actual);
93
- return;
94
- }
95
-
96
- await onCompare(actual, expected, diff);
97
-
98
- return imageName ? `Expected image '${imageName}' to match` : 'Expected image to match';
99
- }
100
-
101
9
  utils.addMethod(
102
10
  Assertion.prototype,
103
11
  'matchImage',
104
- async function matchImage(this: Record<string, unknown>, imageName?: string) {
105
- const actual = utils.flag(this, 'object') as string | Buffer;
106
- const errorMessage = await assertImage(
107
- typeof actual == 'string' ? Buffer.from(actual, 'base64') : actual,
108
- imageName,
109
- );
110
- if (errorMessage) {
111
- throw createImageError(imageName ? { [imageName]: errorMessage } : errorMessage);
12
+ async function (this: Record<string, unknown>, imageName?: string) {
13
+ if (!isWarningShown) {
14
+ logger.warn(
15
+ '`expect(...).to.matchImage()` is deprecated and will be removed in the next major release. Please use `context.matchImage()` instead.',
16
+ );
17
+ isWarningShown = true;
112
18
  }
19
+ const image = utils.flag(this, 'object') as Buffer;
20
+ await matchImage(image, imageName);
113
21
  },
114
22
  );
115
23
 
116
- utils.addMethod(Assertion.prototype, 'matchImages', async function matchImages(this: Record<string, unknown>) {
117
- const errors: Record<string, string> = {};
118
- await Promise.all(
119
- Object.entries<string | Buffer>(utils.flag(this, 'object') as Record<string, string | Buffer>).map(
120
- async ([imageName, imageOrBase64]) => {
121
- let errorMessage: string | undefined;
122
- try {
123
- errorMessage = await assertImage(
124
- typeof imageOrBase64 == 'string' ? Buffer.from(imageOrBase64, 'base64') : imageOrBase64,
125
- imageName,
126
- );
127
- } catch (error) {
128
- errorMessage = (error as Error).stack;
129
- }
130
- if (errorMessage) {
131
- errors[imageName] = errorMessage;
132
- }
133
- },
134
- ),
135
- );
136
- if (Object.keys(errors).length > 0) {
137
- throw createImageError(errors);
24
+ utils.addMethod(Assertion.prototype, 'matchImages', async function (this: Record<string, unknown>) {
25
+ if (!isWarningShown) {
26
+ logger.warn(
27
+ '`expect(...).to.matchImages()` is deprecated and will be removed in the next major release. Please use `context.matchImages()` instead.',
28
+ );
29
+ isWarningShown = true;
138
30
  }
31
+ const images = utils.flag(this, 'object') as Record<string, Buffer>;
32
+ await matchImages(images);
139
33
  });
140
34
  };
141
35
  }
142
-
143
- function createImageError(imageErrors: string | Partial<Record<string, string>>): ImagesError {
144
- const error = new Error('Expected image to match') as ImagesError;
145
- error.images = imageErrors;
146
- return error;
147
- }