creevey 0.10.0-beta.4 → 0.10.0-beta.40

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 (211) 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-B0Xv0lOY.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 +27 -5
  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 +76 -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/reporter.d.ts +4 -19
  118. package/dist/server/reporter.js +30 -21
  119. package/dist/server/reporter.js.map +1 -1
  120. package/dist/server/selenium/internal.d.ts +3 -4
  121. package/dist/server/selenium/internal.js +127 -108
  122. package/dist/server/selenium/internal.js.map +1 -1
  123. package/dist/server/selenium/selenoid.js +8 -6
  124. package/dist/server/selenium/selenoid.js.map +1 -1
  125. package/dist/server/selenium/webdriver.d.ts +1 -1
  126. package/dist/server/selenium/webdriver.js +5 -9
  127. package/dist/server/selenium/webdriver.js.map +1 -1
  128. package/dist/server/telemetry.js +2 -2
  129. package/dist/server/testsFiles/parser.js +45 -5
  130. package/dist/server/testsFiles/parser.js.map +1 -1
  131. package/dist/server/utils.d.ts +19 -1
  132. package/dist/server/utils.js +87 -8
  133. package/dist/server/utils.js.map +1 -1
  134. package/dist/server/webdriver.d.ts +5 -4
  135. package/dist/server/webdriver.js +23 -10
  136. package/dist/server/webdriver.js.map +1 -1
  137. package/dist/server/worker/chai-image.d.ts +1 -2
  138. package/dist/server/worker/chai-image.js +4 -3
  139. package/dist/server/worker/chai-image.js.map +1 -1
  140. package/dist/server/worker/context.d.ts +3 -0
  141. package/dist/server/worker/context.js +15 -0
  142. package/dist/server/worker/context.js.map +1 -0
  143. package/dist/server/worker/match-image.d.ts +4 -4
  144. package/dist/server/worker/match-image.js +7 -4
  145. package/dist/server/worker/match-image.js.map +1 -1
  146. package/dist/server/worker/start.js +45 -73
  147. package/dist/server/worker/start.js.map +1 -1
  148. package/dist/shared/index.d.ts +1 -1
  149. package/dist/types.d.ts +40 -8
  150. package/dist/types.js +2 -0
  151. package/dist/types.js.map +1 -1
  152. package/docs/cli.md +12 -0
  153. package/docs/config.md +179 -165
  154. package/docs/storybook.md +60 -0
  155. package/docs/tests.md +50 -45
  156. package/package.json +64 -63
  157. package/src/client/addon/components/Panel.tsx +2 -2
  158. package/src/client/addon/withCreevey.ts +10 -2
  159. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  160. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  161. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
  162. package/src/client/shared/components/ResultsPage.tsx +31 -8
  163. package/src/client/shared/creeveyClientApi.ts +9 -1
  164. package/src/client/shared/helpers.ts +4 -24
  165. package/src/client/web/CreeveyApp.tsx +27 -8
  166. package/src/client/web/CreeveyContext.tsx +9 -0
  167. package/src/client/web/CreeveyLoader.tsx +1 -1
  168. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  169. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  170. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  171. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  172. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  173. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  174. package/src/client/web/themes.ts +24 -0
  175. package/src/creevey.ts +16 -10
  176. package/src/server/config.ts +28 -6
  177. package/src/server/connection.ts +26 -0
  178. package/src/server/docker.ts +63 -34
  179. package/src/server/index.ts +72 -14
  180. package/src/server/logger.ts +6 -2
  181. package/src/server/master/api.ts +1 -1
  182. package/src/server/master/pool.ts +23 -59
  183. package/src/server/master/queue.ts +77 -0
  184. package/src/server/master/runner.ts +94 -10
  185. package/src/server/master/server.ts +1 -1
  186. package/src/server/master/start.ts +16 -11
  187. package/src/server/playwright/docker-file.ts +18 -6
  188. package/src/server/playwright/docker.ts +16 -3
  189. package/src/server/playwright/index-source.mjs +16 -0
  190. package/src/server/playwright/internal.ts +182 -111
  191. package/src/server/playwright/webdriver.ts +6 -9
  192. package/src/server/providers/browser.ts +6 -4
  193. package/src/server/providers/hybrid.ts +1 -1
  194. package/src/server/reporter.ts +37 -34
  195. package/src/server/selenium/internal.ts +131 -116
  196. package/src/server/selenium/selenoid.ts +8 -6
  197. package/src/server/selenium/webdriver.ts +6 -10
  198. package/src/server/telemetry.ts +2 -2
  199. package/src/server/testsFiles/parser.ts +52 -4
  200. package/src/server/utils.ts +97 -9
  201. package/src/server/webdriver.ts +24 -16
  202. package/src/server/worker/chai-image.ts +4 -4
  203. package/src/server/worker/context.ts +14 -0
  204. package/src/server/worker/match-image.ts +12 -8
  205. package/src/server/worker/start.ts +49 -86
  206. package/src/shared/index.ts +1 -1
  207. package/src/types.ts +44 -8
  208. package/types/global.d.ts +1 -0
  209. package/.yarnrc.yml +0 -1
  210. package/chromatic.config.json +0 -5
  211. package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
@@ -2,9 +2,9 @@ import path from 'path';
2
2
  import assert from 'assert';
3
3
  import { lstatSync, existsSync } from 'fs';
4
4
  import { mkdir, writeFile, copyFile } from 'fs/promises';
5
- import sh from 'shelljs';
5
+ import { exec, chmod } from 'shelljs';
6
6
  import { Config, BrowserConfigObject } from '../../types.js';
7
- import { downloadBinary, getCreeveyCache } from '../utils.js';
7
+ import { downloadBinary, getCreeveyCache, killTree } from '../utils.js';
8
8
  import { pullImages, runImage } from '../docker.js';
9
9
  import { subscribeOn } from '../messages.js';
10
10
 
@@ -34,7 +34,7 @@ async function createSelenoidConfig(
34
34
  dockerImage = `selenoid/${browserName}:${browserVersion}`,
35
35
  webdriverCommand = [],
36
36
  }) => {
37
- if (!selenoidConfig[browserName]) selenoidConfig[browserName] = { default: browserVersion, versions: {} };
37
+ selenoidConfig[browserName] ??= { default: browserVersion, versions: {} };
38
38
  if (!useDocker && webdriverCommand.length == 0)
39
39
  throw new Error('Please specify "webdriverCommand" browser option with path to browser webdriver');
40
40
  selenoidConfig[browserName].versions[browserVersion] = {
@@ -91,12 +91,12 @@ export async function startSelenoidStandalone(config: Config, debug: boolean): P
91
91
 
92
92
  // TODO Download browser webdrivers
93
93
  try {
94
- if (process.platform != 'win32') sh.chmod('+x', binaryPath);
94
+ if (process.platform != 'win32') chmod('+x', binaryPath);
95
95
  } catch {
96
96
  /* noop */
97
97
  }
98
98
 
99
- const selenoidProcess = sh.exec(`${binaryPath} -conf ./browsers.json -disable-docker`, {
99
+ const selenoidProcess = exec(`${binaryPath} -conf ./browsers.json -disable-docker`, {
100
100
  async: true,
101
101
  cwd: selenoidConfigDir,
102
102
  });
@@ -106,7 +106,9 @@ export async function startSelenoidStandalone(config: Config, debug: boolean): P
106
106
  selenoidProcess.stderr?.pipe(process.stderr);
107
107
  }
108
108
 
109
- subscribeOn('shutdown', () => selenoidProcess.kill());
109
+ subscribeOn('shutdown', () => {
110
+ if (selenoidProcess.pid) void killTree(selenoidProcess.pid);
111
+ });
110
112
  }
111
113
 
112
114
  export async function startSelenoidContainer(config: Config, debug: boolean): Promise<string> {
@@ -1,10 +1,11 @@
1
1
  /// <reference types="../../../types/selenium-context" />
2
- import { Args } from '@storybook/csf';
2
+ import type { Args } from '@storybook/types';
3
3
  import { Config, StorybookGlobals, StoryInput, StoriesRaw, Options, ServerTest } from '../../types.js';
4
4
  import { subscribeOn } from '../messages.js';
5
5
  import { CreeveyWebdriverBase } from '../webdriver.js';
6
6
  import type { InternalBrowser } from './internal.js';
7
7
  import { logger } from '../logger.js';
8
+ import { removeWorkerContainer } from '../worker/context.js';
8
9
 
9
10
  declare global {
10
11
  interface Window {
@@ -31,7 +32,9 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
31
32
  this.#options = options;
32
33
 
33
34
  subscribeOn('shutdown', () => {
34
- void this.#browser?.closeBrowser().finally(() => process.exit());
35
+ void this.#browser?.closeBrowser().finally(() => {
36
+ void removeWorkerContainer().finally(() => process.exit());
37
+ });
35
38
  this.#browser = null;
36
39
  });
37
40
  }
@@ -42,7 +45,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
42
45
 
43
46
  getSessionId(): Promise<string> {
44
47
  if (!this.#browser) {
45
- // TODO Describe the error
46
48
  throw new Error('Browser is not initialized');
47
49
  }
48
50
 
@@ -63,7 +65,7 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
63
65
  try {
64
66
  return await import('./internal.js');
65
67
  } catch (error) {
66
- logger.error(error);
68
+ logger().error(error);
67
69
  return null;
68
70
  }
69
71
  })();
@@ -89,7 +91,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
89
91
 
90
92
  async loadStoriesFromBrowser(): Promise<StoriesRaw> {
91
93
  if (!this.#browser) {
92
- // TODO Describe the error
93
94
  throw new Error('Browser is not initialized');
94
95
  }
95
96
 
@@ -98,7 +99,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
98
99
 
99
100
  afterTest(test: ServerTest): Promise<void> {
100
101
  if (!this.#browser) {
101
- // TODO Describe the error
102
102
  throw new Error('Browser is not initialized');
103
103
  }
104
104
 
@@ -110,7 +110,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
110
110
  ignoreElements?: string | string[] | null,
111
111
  ): Promise<Buffer> {
112
112
  if (!this.#browser) {
113
- // TODO Describe the error
114
113
  throw new Error('Browser is not initialized');
115
114
  }
116
115
 
@@ -119,7 +118,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
119
118
 
120
119
  protected waitForComplete(callback: (isCompleted: boolean) => void): void {
121
120
  if (!this.#browser) {
122
- // TODO Describe the error
123
121
  throw new Error('Browser is not initialized');
124
122
  }
125
123
 
@@ -128,7 +126,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
128
126
 
129
127
  protected async selectStory(id: string, waitForReady?: boolean): Promise<boolean> {
130
128
  if (!this.#browser) {
131
- // TODO Describe the error
132
129
  throw new Error('Browser is not initialized');
133
130
  }
134
131
 
@@ -137,7 +134,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
137
134
 
138
135
  protected async updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void> {
139
136
  if (!this.#browser) {
140
- // TODO Describe the error
141
137
  throw new Error('Browser is not initialized');
142
138
  }
143
139
 
@@ -181,8 +181,8 @@ export async function sendScreenshotsCount(
181
181
  const testsMeta = { runId: uuid, tests };
182
182
 
183
183
  const fullPathname = buildPathname('tests', testsMeta);
184
- // NOTE: Keep request path shorter than 32k symbols
185
- const chunksCount = Math.ceil(fullPathname.length / 32_000);
184
+ // NOTE: Keep request path shorter than 24k symbols
185
+ const chunksCount = Math.ceil(fullPathname.length / 24_000);
186
186
  let chunks: string[] = [];
187
187
  if (chunksCount > 1) {
188
188
  const testsString = JSON.stringify(tests);
@@ -1,8 +1,58 @@
1
1
  import { pathToFileURL } from 'url';
2
- import { toId, storyNameFromExport } from '@storybook/csf';
3
2
  import { CreeveyStoryParams, CreeveyTestFunction } from '../../types.js';
4
3
  import { loadThroughTSX } from '../utils.js';
5
4
 
5
+ // NOTE: Copy-pasted from @storybook/csf
6
+ function toStartCaseStr(str: string) {
7
+ return str
8
+ .replace(/_/g, ' ')
9
+ .replace(/-/g, ' ')
10
+ .replace(/\./g, ' ')
11
+ .replace(/([^\n])([A-Z])([a-z])/g, (_, $1, $2, $3) => `${$1} ${$2}${$3}`)
12
+ .replace(/([a-z])([A-Z])/g, (_, $1, $2) => `${$1} ${$2}`)
13
+ .replace(/([a-z])([0-9])/gi, (_, $1, $2) => `${$1} ${$2}`)
14
+ .replace(/([0-9])([a-z])/gi, (_, $1, $2) => `${$1} ${$2}`)
15
+ .replace(/(\s|^)(\w)/g, (_, $1, $2: string) => `${$1}${$2.toUpperCase()}`)
16
+ .replace(/ +/g, ' ')
17
+ .trim();
18
+ }
19
+
20
+ /**
21
+ * Remove punctuation and illegal characters from a story ID.
22
+ *
23
+ * See https://gist.github.com/davidjrice/9d2af51100e41c6c4b4a
24
+ */
25
+ const sanitize = (string: string) => {
26
+ return (
27
+ string
28
+ .toLowerCase()
29
+ // eslint-disable-next-line no-useless-escape
30
+ .replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '-')
31
+ .replace(/-+/g, '-')
32
+ .replace(/^-+/, '')
33
+ .replace(/-+$/, '')
34
+ );
35
+ };
36
+
37
+ const sanitizeSafe = (string: string, part: string) => {
38
+ const sanitized = sanitize(string);
39
+ if (sanitized === '') {
40
+ throw new Error(`Invalid ${part} '${string}', must include alphanumeric characters`);
41
+ }
42
+ return sanitized;
43
+ };
44
+
45
+ /**
46
+ * Generate a storybook ID from a component/kind and story name.
47
+ */
48
+ const toId = (kind: string, name?: string) =>
49
+ `${sanitizeSafe(kind, 'kind')}${name ? `--${sanitizeSafe(name, 'name')}` : ''}`;
50
+
51
+ /**
52
+ * Transform a CSF named export into a readable story name
53
+ */
54
+ const storyNameFromExport = (key: string) => toStartCaseStr(key);
55
+
6
56
  export type CreeveyParamsByStoryId = Record<string, CreeveyStoryParams>;
7
57
 
8
58
  export default async function parse(files: string[]): Promise<CreeveyParamsByStoryId> {
@@ -55,8 +105,6 @@ export const story = (
55
105
 
56
106
  export const test = (title: string, testFn: CreeveyTestFunction): void => {
57
107
  const storyId = getStoryId(kindTitle, storyTitle);
58
- if (!result[storyId]) {
59
- result[storyId] = {};
60
- }
108
+ result[storyId] ??= {};
61
109
  result[storyId].tests = Object.assign({}, result[storyId].tests, { [title]: testFn });
62
110
  };
@@ -1,13 +1,16 @@
1
1
  import fs from 'fs';
2
- import { get } from 'https';
2
+ import https from 'https';
3
+ import http from 'http';
3
4
  import cluster from 'cluster';
4
5
  import { dirname } from 'path';
5
6
  import { fileURLToPath, pathToFileURL } from 'url';
6
- import { createRequire } from 'module';
7
7
  import { register as esmRegister } from 'tsx/esm/api';
8
8
  import { register as cjsRegister } from 'tsx/cjs/api';
9
- import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest } from '../types.js';
9
+ import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest, Worker } from '../types.js';
10
10
  import { emitShutdownMessage, sendShutdownMessage } from './messages.js';
11
+ import { LOCALHOST_REGEXP } from './webdriver.js';
12
+ import assert from 'assert';
13
+ import pidtree from 'pidtree';
11
14
 
12
15
  const importMetaUrl = pathToFileURL(__filename).href;
13
16
 
@@ -15,6 +18,19 @@ export const isShuttingDown = { current: false };
15
18
 
16
19
  export const configExt = ['.js', '.mjs', '.ts', '.cjs', '.mts', '.cts'];
17
20
 
21
+ const browserTypes = {
22
+ chromium: 'chromium',
23
+ 'chromium-headless-shell': 'chromium',
24
+ chrome: 'chromium',
25
+ 'chrome-beta': 'chromium',
26
+ msedge: 'chromium',
27
+ 'msedge-beta': 'chromium',
28
+ 'msedge-dev': 'chromium',
29
+ 'bidi-chromium': 'chromium',
30
+ firefox: 'firefox',
31
+ webkit: 'webkit',
32
+ } as const;
33
+
18
34
  export const skipOptionKeys = ['in', 'kinds', 'stories', 'tests', 'reason'];
19
35
 
20
36
  function matchBy(pattern: string | string[] | RegExp | undefined, value: string): boolean {
@@ -83,19 +99,56 @@ export async function shutdownWorkers(): Promise<void> {
83
99
  (worker) =>
84
100
  new Promise<void>((resolve) => {
85
101
  const timeout = setTimeout(() => {
86
- worker.kill();
87
- }, 10000);
102
+ if (worker.process.pid) void killTree(worker.process.pid);
103
+ }, 10_000);
88
104
  worker.on('exit', () => {
89
105
  clearTimeout(timeout);
90
106
  resolve();
91
107
  });
92
108
  sendShutdownMessage(worker);
109
+ worker.disconnect();
93
110
  }),
94
111
  ),
95
112
  );
96
113
  emitShutdownMessage();
97
114
  }
98
115
 
116
+ export function gracefullyKill(worker: Worker): void {
117
+ worker.isShuttingDown = true;
118
+ const timeout = setTimeout(() => {
119
+ if (worker.process.pid) void killTree(worker.process.pid);
120
+ }, 10000);
121
+ worker.on('exit', () => {
122
+ clearTimeout(timeout);
123
+ });
124
+ sendShutdownMessage(worker);
125
+ }
126
+
127
+ export async function killTree(rootPid: number): Promise<void> {
128
+ const pids = await pidtree(rootPid, { root: true });
129
+
130
+ pids.forEach((pid) => {
131
+ try {
132
+ process.kill(pid, 'SIGKILL');
133
+ } catch {
134
+ /* noop */
135
+ }
136
+ });
137
+ }
138
+
139
+ export function shutdownWithError(): void {
140
+ process.exit(1);
141
+ }
142
+
143
+ export function resolvePlaywrightBrowserType(browserName: string): (typeof browserTypes)[keyof typeof browserTypes] {
144
+ assert(
145
+ browserName in browserTypes,
146
+ new Error(`Failed to match browser name "${browserName}" to playwright browserType`),
147
+ );
148
+
149
+ return browserTypes[browserName as keyof typeof browserTypes];
150
+ }
151
+
99
152
  export async function getCreeveyCache(): Promise<string | undefined> {
100
153
  const { default: findCacheDir } = await import('find-cache-dir');
101
154
  return findCacheDir({ name: 'creevey', cwd: dirname(fileURLToPath(importMetaUrl)) });
@@ -131,11 +184,12 @@ export function testsToImages(tests: (TestData | undefined)[]): Set<string> {
131
184
 
132
185
  // https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
133
186
  export const isInsideDocker =
134
- fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf-8').includes('docker');
187
+ (fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf-8').includes('docker')) ||
188
+ process.env.DOCKER === 'true';
135
189
 
136
190
  export const downloadBinary = (downloadUrl: string, destination: string): Promise<void> =>
137
191
  new Promise((resolve, reject) =>
138
- get(downloadUrl, (response) => {
192
+ https.get(downloadUrl, (response) => {
139
193
  if (response.statusCode == 302) {
140
194
  const { location } = response.headers;
141
195
  if (!location) {
@@ -175,10 +229,10 @@ export function readDirRecursive(dirPath: string): string[] {
175
229
  );
176
230
  }
177
231
 
178
- const _require = createRequire(importMetaUrl);
179
232
  export function tryToLoadTestsData(filename: string): Partial<Record<string, ServerTest>> | undefined {
180
233
  try {
181
- return _require(filename) as Partial<Record<string, ServerTest>>;
234
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
235
+ return require(filename) as Partial<Record<string, ServerTest>>;
182
236
  } catch {
183
237
  /* noop */
184
238
  }
@@ -204,3 +258,37 @@ export async function loadThroughTSX<T>(
204
258
 
205
259
  return result;
206
260
  }
261
+
262
+ export function waitOnUrl(waitUrl: string, timeout: number, delay: number) {
263
+ const urls = [waitUrl];
264
+ if (!LOCALHOST_REGEXP.test(waitUrl)) {
265
+ const parsedUrl = new URL(waitUrl);
266
+ parsedUrl.host = 'localhost';
267
+ urls.push(parsedUrl.toString());
268
+ }
269
+ const startTime = Date.now();
270
+ return Promise.race(
271
+ urls.map(
272
+ (url) =>
273
+ new Promise<void>((resolve, reject) => {
274
+ const interval = setInterval(() => {
275
+ http
276
+ .get(url, (response) => {
277
+ if (response.statusCode === 200) {
278
+ clearInterval(interval);
279
+ resolve();
280
+ }
281
+ })
282
+ .on('error', () => {
283
+ // Ignore HTTP errors
284
+ });
285
+
286
+ if (Date.now() - startTime > timeout) {
287
+ clearInterval(interval);
288
+ reject(new Error(`${url} didn't respond within ${timeout / 1000} seconds`));
289
+ }
290
+ }, delay);
291
+ }),
292
+ ),
293
+ );
294
+ }
@@ -1,8 +1,7 @@
1
- import Logger from 'loglevel';
2
1
  import chalk from 'chalk';
3
2
  import { networkInterfaces } from 'os';
4
- import { logger as defaultLogger } from './logger.js';
5
- import { Args } from '@storybook/csf';
3
+ import { logger } from './logger.js';
4
+ import type { Args } from '@storybook/types';
6
5
  import {
7
6
  isDefined,
8
7
  StoryInput,
@@ -19,18 +18,31 @@ export const storybookRootID = 'storybook-root';
19
18
  export const LOCALHOST_REGEXP = /(localhost|127\.0\.0\.1)/i;
20
19
  const DOCKER_INTERNAL = 'host.docker.internal';
21
20
 
21
+ let browserClosePromise: Promise<void> | null = null;
22
+
23
+ export const openBrowser = () => {
24
+ let resolve: () => void;
25
+ browserClosePromise = new Promise((r) => (resolve = r));
26
+ return () => {
27
+ resolve();
28
+ browserClosePromise = null;
29
+ };
30
+ };
31
+
32
+ export const waitForBrowserClose = () => browserClosePromise;
33
+
22
34
  export async function resolveStorybookUrl(
23
35
  storybookUrl: string,
24
36
  checkUrl: (url: string) => Promise<boolean>,
25
- logger: Logger.Logger = defaultLogger,
26
37
  ): Promise<string> {
27
- logger.debug('Resolving storybook url');
38
+ logger().debug('Resolving storybook url');
28
39
  const addresses = getAddresses();
40
+ // TODO Use Promise.race?
29
41
  for (const ip of addresses) {
30
42
  const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
31
- logger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
43
+ logger().debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
32
44
  if (await checkUrl(resolvedUrl)) {
33
- logger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
45
+ logger().debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
34
46
  return resolvedUrl;
35
47
  }
36
48
  }
@@ -74,11 +86,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
74
86
 
75
87
  abstract afterTest(test: ServerTest): Promise<void>;
76
88
 
77
- async switchStory(
78
- story: StoryInput,
79
- context: BaseCreeveyTestContext,
80
- logger: Logger.Logger,
81
- ): Promise<CreeveyTestContext> {
89
+ async switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext> {
82
90
  const { id, title, name, parameters } = story;
83
91
  const {
84
92
  captureElement = `#${storybookRootID}`,
@@ -86,7 +94,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
86
94
  ignoreElements,
87
95
  } = (parameters.creevey ?? {}) as CreeveyStoryParams;
88
96
 
89
- logger.debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
97
+ logger().debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
90
98
 
91
99
  let storyPlayResolver: (isCompleted: boolean) => void;
92
100
  let waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
@@ -107,7 +115,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
107
115
  const isCaptureCalled = await this.selectStory(id, waitForReady);
108
116
 
109
117
  if (isCaptureCalled) {
110
- logger.debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
118
+ logger().debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
111
119
  while (!(await waitForComplete)) {
112
120
  waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
113
121
  }
@@ -115,8 +123,8 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
115
123
 
116
124
  unsubscribe();
117
125
 
118
- if (isCaptureCalled) logger.debug(`Story ${chalk.magenta(id)} completed capturing`);
119
- else logger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
126
+ if (isCaptureCalled) logger().debug(`Story ${chalk.magenta(id)} completed capturing`);
127
+ else logger().debug(`Story ${chalk.magenta(id)} ready for capturing`);
120
128
 
121
129
  return Object.assign(
122
130
  {
@@ -1,8 +1,8 @@
1
- import Logger from 'loglevel';
1
+ import { logger } from '../logger';
2
+
2
3
  export default function (
3
4
  matchImage: (image: Buffer, imageName?: string) => Promise<void>,
4
5
  matchImages: (images: Record<string, Buffer>) => Promise<void>,
5
- logger: Logger.Logger,
6
6
  ) {
7
7
  let isWarningShown = false;
8
8
  return function chaiImage({ Assertion }: Chai.ChaiStatic, utils: Chai.ChaiUtils): void {
@@ -11,7 +11,7 @@ export default function (
11
11
  'matchImage',
12
12
  async function (this: Record<string, unknown>, imageName?: string) {
13
13
  if (!isWarningShown) {
14
- logger.warn(
14
+ logger().warn(
15
15
  '`expect(...).to.matchImage()` is deprecated and will be removed in the next major release. Please use `context.matchImage()` instead.',
16
16
  );
17
17
  isWarningShown = true;
@@ -23,7 +23,7 @@ export default function (
23
23
 
24
24
  utils.addMethod(Assertion.prototype, 'matchImages', async function (this: Record<string, unknown>) {
25
25
  if (!isWarningShown) {
26
- logger.warn(
26
+ logger().warn(
27
27
  '`expect(...).to.matchImages()` is deprecated and will be removed in the next major release. Please use `context.matchImages()` instead.',
28
28
  );
29
29
  isWarningShown = true;
@@ -0,0 +1,14 @@
1
+ import type { Container } from 'dockerode';
2
+
3
+ let workerContainer: Container | null = null;
4
+
5
+ export function setWorkerContainer(container: Container): void {
6
+ workerContainer = container;
7
+ }
8
+
9
+ export async function removeWorkerContainer(): Promise<void> {
10
+ if (workerContainer) {
11
+ await workerContainer.remove({ force: true });
12
+ workerContainer = null;
13
+ }
14
+ }
@@ -21,6 +21,10 @@ interface ImagePaths {
21
21
  reportImageDir: string;
22
22
  }
23
23
 
24
+ function toBuffer(bufferOrBase64: Buffer | string) {
25
+ return typeof bufferOrBase64 === 'string' ? Buffer.from(bufferOrBase64, 'base64') : bufferOrBase64;
26
+ }
27
+
24
28
  async function getStat(filePath: string): Promise<Stats | null> {
25
29
  try {
26
30
  return await stat(filePath);
@@ -228,17 +232,17 @@ export async function getMatchers(ctx: ImageContext, config: Config) {
228
232
  }
229
233
 
230
234
  return {
231
- matchImage: async (image: Buffer, imageName?: string) => {
232
- const errorMessage = await assertImage(image, imageName);
235
+ matchImage: async (image: Buffer | string, imageName?: string) => {
236
+ const errorMessage = await assertImage(toBuffer(image), imageName);
233
237
  if (errorMessage) {
234
238
  throw createImageError(imageName ? { [imageName]: errorMessage } : errorMessage);
235
239
  }
236
240
  },
237
- matchImages: async (images: Record<string, Buffer>) => {
241
+ matchImages: async (images: Record<string, Buffer | string>) => {
238
242
  const errors: Record<string, string> = {};
239
243
  await Promise.all(
240
244
  Object.entries(images).map(async ([imageName, image]) => {
241
- const errorMessage = await assertImage(image, imageName);
245
+ const errorMessage = await assertImage(toBuffer(image), imageName);
242
246
  if (errorMessage) {
243
247
  errors[imageName] = errorMessage;
244
248
  }
@@ -279,17 +283,17 @@ export function getOdiffMatchers(ctx: ImageContext, config: Config) {
279
283
  }
280
284
 
281
285
  return {
282
- matchImage: async (image: Buffer, imageName?: string) => {
283
- const errorMessage = await assertImage(image, imageName);
286
+ matchImage: async (image: Buffer | string, imageName?: string) => {
287
+ const errorMessage = await assertImage(toBuffer(image), imageName);
284
288
  if (errorMessage) {
285
289
  throw createImageError(imageName ? { [imageName]: errorMessage } : errorMessage);
286
290
  }
287
291
  },
288
- matchImages: async (images: Record<string, Buffer>) => {
292
+ matchImages: async (images: Record<string, Buffer | string>) => {
289
293
  const errors: Record<string, string> = {};
290
294
  await Promise.all(
291
295
  Object.entries(images).map(async ([imageName, image]) => {
292
- const errorMessage = await assertImage(image, imageName);
296
+ const errorMessage = await assertImage(toBuffer(image), imageName);
293
297
  if (errorMessage) {
294
298
  errors[imageName] = errorMessage;
295
299
  }