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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/README.md +19 -41
  2. package/dist/client/addon/components/Addon.js +17 -7
  3. package/dist/client/addon/components/Addon.js.map +1 -1
  4. package/dist/client/addon/components/Panel.js +2 -2
  5. package/dist/client/addon/components/Panel.js.map +1 -1
  6. package/dist/client/addon/components/Tools.js +17 -7
  7. package/dist/client/addon/components/Tools.js.map +1 -1
  8. package/dist/client/addon/withCreevey.d.ts +2 -1
  9. package/dist/client/addon/withCreevey.js +11 -1
  10. package/dist/client/addon/withCreevey.js.map +1 -1
  11. package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
  12. package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
  13. package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
  14. package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
  15. package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
  16. package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
  17. package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
  18. package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
  19. package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
  20. package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
  21. package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
  22. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  23. package/dist/client/shared/components/PageHeader/ImagePreview.d.ts +1 -1
  24. package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
  25. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  26. package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
  27. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  28. package/dist/client/shared/components/ResultsPage.d.ts +1 -1
  29. package/dist/client/shared/components/ResultsPage.js +43 -13
  30. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  31. package/dist/client/shared/creeveyClientApi.js +8 -1
  32. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  33. package/dist/client/shared/helpers.d.ts +1 -3
  34. package/dist/client/shared/helpers.js +4 -19
  35. package/dist/client/shared/helpers.js.map +1 -1
  36. package/dist/client/web/CreeveyApp.js +42 -14
  37. package/dist/client/web/CreeveyApp.js.map +1 -1
  38. package/dist/client/web/CreeveyContext.d.ts +5 -0
  39. package/dist/client/web/CreeveyContext.js +20 -7
  40. package/dist/client/web/CreeveyContext.js.map +1 -1
  41. package/dist/client/web/CreeveyLoader.js +2 -2
  42. package/dist/client/web/CreeveyLoader.js.map +1 -1
  43. package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
  44. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  45. package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
  46. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  47. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
  48. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  49. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
  50. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  51. package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +2 -2
  52. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
  53. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  54. package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
  55. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  56. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +1 -1
  57. package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
  58. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  59. package/dist/client/web/KeyboardEventsContext.js +79 -64
  60. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  61. package/dist/client/web/assets/index-C47njyZV.js +802 -0
  62. package/dist/client/web/index.html +1 -1
  63. package/dist/client/web/index.js +17 -7
  64. package/dist/client/web/index.js.map +1 -1
  65. package/dist/client/web/themes.d.ts +2 -0
  66. package/dist/client/web/themes.js +22 -0
  67. package/dist/client/web/themes.js.map +1 -0
  68. package/dist/creevey.js +16 -9
  69. package/dist/creevey.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/server/config.d.ts +1 -1
  72. package/dist/server/config.js +30 -7
  73. package/dist/server/config.js.map +1 -1
  74. package/dist/server/connection.d.ts +3 -0
  75. package/dist/server/connection.js +28 -0
  76. package/dist/server/connection.js.map +1 -0
  77. package/dist/server/docker.d.ts +1 -1
  78. package/dist/server/docker.js +56 -32
  79. package/dist/server/docker.js.map +1 -1
  80. package/dist/server/index.js +64 -11
  81. package/dist/server/index.js.map +1 -1
  82. package/dist/server/logger.d.ts +2 -1
  83. package/dist/server/logger.js +7 -3
  84. package/dist/server/logger.js.map +1 -1
  85. package/dist/server/master/api.js +1 -1
  86. package/dist/server/master/api.js.map +1 -1
  87. package/dist/server/master/pool.d.ts +4 -3
  88. package/dist/server/master/pool.js +13 -66
  89. package/dist/server/master/pool.js.map +1 -1
  90. package/dist/server/master/queue.d.ts +13 -0
  91. package/dist/server/master/queue.js +71 -0
  92. package/dist/server/master/queue.js.map +1 -0
  93. package/dist/server/master/runner.d.ts +3 -0
  94. package/dist/server/master/runner.js +78 -10
  95. package/dist/server/master/runner.js.map +1 -1
  96. package/dist/server/master/server.js +1 -1
  97. package/dist/server/master/server.js.map +1 -1
  98. package/dist/server/master/start.js +13 -11
  99. package/dist/server/master/start.js.map +1 -1
  100. package/dist/server/playwright/docker-file.d.ts +1 -1
  101. package/dist/server/playwright/docker-file.js +15 -6
  102. package/dist/server/playwright/docker-file.js.map +1 -1
  103. package/dist/server/playwright/docker.d.ts +2 -1
  104. package/dist/server/playwright/docker.js +10 -2
  105. package/dist/server/playwright/docker.js.map +1 -1
  106. package/dist/server/playwright/index-source.mjs +16 -0
  107. package/dist/server/playwright/internal.d.ts +6 -6
  108. package/dist/server/playwright/internal.js +143 -91
  109. package/dist/server/playwright/internal.js.map +1 -1
  110. package/dist/server/playwright/webdriver.d.ts +1 -1
  111. package/dist/server/playwright/webdriver.js +5 -8
  112. package/dist/server/playwright/webdriver.js.map +1 -1
  113. package/dist/server/providers/browser.js +6 -4
  114. package/dist/server/providers/browser.js.map +1 -1
  115. package/dist/server/providers/hybrid.js +1 -1
  116. package/dist/server/providers/hybrid.js.map +1 -1
  117. package/dist/server/reporters/creevey.d.ts +7 -0
  118. package/dist/server/reporters/creevey.js +63 -0
  119. package/dist/server/reporters/creevey.js.map +1 -0
  120. package/dist/server/reporters/index.d.ts +2 -0
  121. package/dist/server/reporters/index.js +16 -0
  122. package/dist/server/reporters/index.js.map +1 -0
  123. package/dist/server/reporters/junit.d.ts +16 -0
  124. package/dist/server/reporters/junit.js +165 -0
  125. package/dist/server/reporters/junit.js.map +1 -0
  126. package/dist/server/reporters/teamcity.d.ts +7 -0
  127. package/dist/server/reporters/teamcity.js +60 -0
  128. package/dist/server/reporters/teamcity.js.map +1 -0
  129. package/dist/server/selenium/internal.d.ts +3 -4
  130. package/dist/server/selenium/internal.js +127 -108
  131. package/dist/server/selenium/internal.js.map +1 -1
  132. package/dist/server/selenium/selenoid.js +8 -6
  133. package/dist/server/selenium/selenoid.js.map +1 -1
  134. package/dist/server/selenium/webdriver.d.ts +1 -1
  135. package/dist/server/selenium/webdriver.js +5 -9
  136. package/dist/server/selenium/webdriver.js.map +1 -1
  137. package/dist/server/telemetry.js +2 -2
  138. package/dist/server/testsFiles/parser.js +45 -5
  139. package/dist/server/testsFiles/parser.js.map +1 -1
  140. package/dist/server/utils.d.ts +19 -1
  141. package/dist/server/utils.js +87 -8
  142. package/dist/server/utils.js.map +1 -1
  143. package/dist/server/webdriver.d.ts +5 -4
  144. package/dist/server/webdriver.js +23 -10
  145. package/dist/server/webdriver.js.map +1 -1
  146. package/dist/server/worker/chai-image.d.ts +1 -2
  147. package/dist/server/worker/chai-image.js +4 -3
  148. package/dist/server/worker/chai-image.js.map +1 -1
  149. package/dist/server/worker/context.d.ts +3 -0
  150. package/dist/server/worker/context.js +15 -0
  151. package/dist/server/worker/context.js.map +1 -0
  152. package/dist/server/worker/match-image.d.ts +4 -4
  153. package/dist/server/worker/match-image.js +7 -4
  154. package/dist/server/worker/match-image.js.map +1 -1
  155. package/dist/server/worker/start.js +47 -73
  156. package/dist/server/worker/start.js.map +1 -1
  157. package/dist/shared/index.d.ts +1 -1
  158. package/dist/types.d.ts +46 -10
  159. package/dist/types.js +2 -0
  160. package/dist/types.js.map +1 -1
  161. package/docs/cli.md +12 -0
  162. package/docs/config.md +179 -165
  163. package/docs/storybook.md +60 -0
  164. package/docs/tests.md +50 -45
  165. package/package.json +64 -63
  166. package/src/client/addon/components/Panel.tsx +2 -2
  167. package/src/client/addon/withCreevey.ts +10 -2
  168. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  169. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  170. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
  171. package/src/client/shared/components/ResultsPage.tsx +31 -8
  172. package/src/client/shared/creeveyClientApi.ts +9 -1
  173. package/src/client/shared/helpers.ts +4 -24
  174. package/src/client/web/CreeveyApp.tsx +27 -8
  175. package/src/client/web/CreeveyContext.tsx +9 -0
  176. package/src/client/web/CreeveyLoader.tsx +1 -1
  177. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  178. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  179. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  180. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  181. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  182. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  183. package/src/client/web/themes.ts +24 -0
  184. package/src/creevey.ts +16 -10
  185. package/src/server/config.ts +30 -7
  186. package/src/server/connection.ts +26 -0
  187. package/src/server/docker.ts +63 -34
  188. package/src/server/index.ts +72 -14
  189. package/src/server/logger.ts +6 -2
  190. package/src/server/master/api.ts +1 -1
  191. package/src/server/master/pool.ts +23 -59
  192. package/src/server/master/queue.ts +77 -0
  193. package/src/server/master/runner.ts +96 -10
  194. package/src/server/master/server.ts +1 -1
  195. package/src/server/master/start.ts +16 -11
  196. package/src/server/playwright/docker-file.ts +18 -6
  197. package/src/server/playwright/docker.ts +16 -3
  198. package/src/server/playwright/index-source.mjs +16 -0
  199. package/src/server/playwright/internal.ts +182 -111
  200. package/src/server/playwright/webdriver.ts +6 -9
  201. package/src/server/providers/browser.ts +6 -4
  202. package/src/server/providers/hybrid.ts +1 -1
  203. package/src/server/reporters/creevey.ts +71 -0
  204. package/src/server/reporters/index.ts +11 -0
  205. package/src/server/reporters/junit.ts +205 -0
  206. package/src/server/reporters/teamcity.ts +74 -0
  207. package/src/server/selenium/internal.ts +131 -116
  208. package/src/server/selenium/selenoid.ts +8 -6
  209. package/src/server/selenium/webdriver.ts +6 -10
  210. package/src/server/telemetry.ts +2 -2
  211. package/src/server/testsFiles/parser.ts +52 -4
  212. package/src/server/utils.ts +97 -9
  213. package/src/server/webdriver.ts +24 -16
  214. package/src/server/worker/chai-image.ts +4 -4
  215. package/src/server/worker/context.ts +14 -0
  216. package/src/server/worker/match-image.ts +12 -8
  217. package/src/server/worker/start.ts +51 -86
  218. package/src/shared/index.ts +1 -1
  219. package/src/types.ts +50 -11
  220. package/types/global.d.ts +1 -0
  221. package/.yarnrc.yml +0 -1
  222. package/chromatic.config.json +0 -5
  223. package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
  224. package/dist/server/reporter.d.ts +0 -26
  225. package/dist/server/reporter.js +0 -108
  226. package/dist/server/reporter.js.map +0 -1
  227. package/src/server/reporter.ts +0 -138
@@ -1,8 +1,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
  }
@@ -1,18 +1,12 @@
1
1
  import chai from 'chai';
2
- import chalk from 'chalk';
3
- import Logger from 'loglevel';
4
- import EventEmitter from 'events';
5
2
  import {
6
3
  BaseCreeveyTestContext,
7
4
  Config,
8
5
  CreeveyWebdriver,
9
- FakeSuite,
10
- FakeTest,
11
- Images,
12
6
  Options,
13
7
  ServerTest,
14
- TEST_EVENTS,
15
8
  TestMessage,
9
+ TestResult,
16
10
  isDefined,
17
11
  isImageError,
18
12
  } from '../../types.js';
@@ -47,9 +41,10 @@ async function getTestsFromStories(
47
41
  return testsById;
48
42
  }
49
43
 
50
- function runHandler(browserName: string, images: Partial<Record<string, Images>>, error?: unknown): void {
44
+ function runHandler(browserName: string, result: Omit<TestResult, 'status'>, error?: unknown): void {
51
45
  // TODO How handle browser corruption?
52
- if (isImageError(error)) {
46
+ const { images } = result;
47
+ if (images != null && isImageError(error)) {
53
48
  if (typeof error.images == 'string') {
54
49
  const image = images[browserName];
55
50
  if (image) image.error = error.images;
@@ -62,31 +57,37 @@ function runHandler(browserName: string, images: Partial<Record<string, Images>>
62
57
  }
63
58
  }
64
59
 
65
- if (error || Object.values(images).some((image) => image?.error != null)) {
66
- const errorMessage = serializeError(error);
60
+ if (error || (images != null && Object.values(images).some((image) => image?.error != null))) {
61
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
62
+ const errorMessage = result.error!;
67
63
 
68
64
  const isUnexpectedError =
69
65
  hasTimeout(errorMessage) ||
70
66
  hasDisconnected(errorMessage) ||
71
- Object.values(images).some((image) => hasTimeout(image?.error));
67
+ (images != null && Object.values(images).some((image) => hasTimeout(image?.error)));
72
68
  if (isUnexpectedError) emitWorkerMessage({ type: 'error', payload: { subtype: 'unknown', error: errorMessage } });
73
69
  else
74
70
  emitTestMessage({
75
71
  type: 'end',
76
72
  payload: {
77
73
  status: 'failed',
78
- images,
79
- error: errorMessage,
74
+ ...result,
80
75
  },
81
76
  });
82
77
  } else {
83
- emitTestMessage({ type: 'end', payload: { status: 'success', images } });
78
+ emitTestMessage({
79
+ type: 'end',
80
+ payload: {
81
+ status: 'success',
82
+ ...result,
83
+ },
84
+ });
84
85
  }
85
86
  }
86
87
 
87
88
  async function setupWebdriver(webdriver: CreeveyWebdriver): Promise<[string, CreeveyWebdriver] | undefined> {
88
89
  if ((await webdriver.openBrowser(true)) == null) {
89
- logger.error('Failed to start browser');
90
+ logger().error('Failed to start browser');
90
91
  emitWorkerMessage({
91
92
  type: 'error',
92
93
  payload: { subtype: 'browser', error: 'Failed to start browser' },
@@ -114,7 +115,6 @@ function hasTimeout(str: string | null | undefined): boolean {
114
115
  }
115
116
 
116
117
  export async function start(browser: string, gridUrl: string, config: Config, options: Options): Promise<void> {
117
- let retries = 0;
118
118
  const imagesContext: ImageContext = {
119
119
  attachments: [],
120
120
  testFullPath: [],
@@ -125,38 +125,16 @@ export async function start(browser: string, gridUrl: string, config: Config, op
125
125
 
126
126
  if (!webdriver || !sessionId) return;
127
127
 
128
- const workerLogger = Logger.getLogger(`${browser}:${chalk.gray(sessionId)}`);
129
-
130
- const reporterOptions = {
131
- ...config.reporterOptions,
132
- creevey: {
133
- sessionId,
134
- reportDir: config.reportDir,
135
- browserName: browser,
136
- get willRetry() {
137
- return retries < config.maxRetries;
138
- },
139
- get images() {
140
- return imagesContext.images;
141
- },
142
- },
143
- };
144
-
145
- class FakeRunner extends EventEmitter {}
146
- const runner = new FakeRunner();
147
- const Reporter = config.reporter;
148
- new Reporter(runner, { reporterOptions });
149
-
150
128
  const { matchImage, matchImages } = options.odiff
151
129
  ? getOdiffMatchers(imagesContext, config)
152
130
  : await getMatchers(imagesContext, config);
153
- chai.use(chaiImage(matchImage, matchImages, workerLogger));
131
+ chai.use(chaiImage(matchImage, matchImages));
154
132
 
155
133
  const tests = await (async () => {
156
134
  try {
157
135
  return await getTestsFromStories(config, browser, webdriver);
158
136
  } catch (error) {
159
- workerLogger.error('Failed to get tests from stories:', error);
137
+ logger().error('Failed to get tests from stories:', error);
160
138
  emitWorkerMessage({
161
139
  type: 'error',
162
140
  payload: { subtype: 'browser', error: serializeError(error) },
@@ -174,7 +152,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
174
152
 
175
153
  if (!test) {
176
154
  const error = `Test with id ${message.payload.id} not found`;
177
- workerLogger.error(error);
155
+ logger().error(error);
178
156
  emitWorkerMessage({
179
157
  type: 'error',
180
158
  payload: { subtype: 'test', error },
@@ -200,68 +178,55 @@ export async function start(browser: string, gridUrl: string, config: Config, op
200
178
  imagesContext.testFullPath = getTestPath(test);
201
179
  imagesContext.images = {};
202
180
 
203
- retries = message.payload.retries;
204
181
  let error = undefined;
205
182
 
206
- const fakeSuite: FakeSuite = {
207
- title: test.storyPath.slice(0, -1).join('/'),
208
- fullTitle: () => fakeSuite.title,
209
- titlePath: () => [fakeSuite.title],
210
- tests: [],
211
- };
212
-
213
- const fakeTest: FakeTest = {
214
- parent: fakeSuite,
215
- title: [test.story.name, test.testName, test.browser].filter(isDefined).join('/'),
216
- fullTitle: () => getTestPath(test).join('/'),
217
- titlePath: () => getTestPath(test),
218
- currentRetry: () => retries,
219
- retires: () => config.maxRetries,
220
- slow: () => 1000,
221
- };
222
-
223
- fakeSuite.tests.push(fakeTest);
224
-
225
183
  void (async () => {
226
- runner.emit(TEST_EVENTS.RUN_BEGIN);
227
- runner.emit(TEST_EVENTS.TEST_BEGIN, fakeTest);
228
-
184
+ let timeout;
185
+ let isRejected = false;
229
186
  const start = Date.now();
230
187
  try {
231
188
  await Promise.race([
232
- new Promise((reject) =>
233
- setTimeout(() => {
234
- reject(`Timeout of ${config.testTimeout}ms exceeded`);
235
- }, config.testTimeout),
189
+ new Promise(
190
+ (_, reject) =>
191
+ (timeout = setTimeout(() => {
192
+ isRejected = true;
193
+ reject(new Error(`Timeout of ${config.testTimeout}ms exceeded`));
194
+ }, config.testTimeout)),
236
195
  ),
237
196
  (async () => {
238
- const context = await webdriver.switchStory(test.story, baseContext, workerLogger);
197
+ const context = await webdriver.switchStory(test.story, baseContext);
239
198
  await test.fn(context);
240
199
  })(),
241
200
  ]);
242
201
  } catch (testError) {
243
202
  error = testError;
244
- fakeTest.err = error;
245
203
  }
246
204
  const duration = Date.now() - start;
247
- fakeTest.attachments = imagesContext.attachments;
248
- fakeTest.state = error ? 'failed' : 'passed';
249
- fakeTest.duration = duration;
250
- fakeTest.speed = duration > fakeTest.slow() ? 'slow' : duration / 2 > fakeTest.slow() ? 'medium' : 'fast';
251
-
252
- if (error) {
253
- runner.emit(TEST_EVENTS.TEST_FAIL, fakeTest, error);
254
- } else {
255
- runner.emit(TEST_EVENTS.TEST_PASS, fakeTest);
256
- }
257
- runner.emit(TEST_EVENTS.TEST_END, fakeTest);
258
- runner.emit(TEST_EVENTS.RUN_END);
205
+ clearTimeout(timeout);
259
206
 
260
207
  await webdriver.afterTest(test);
261
208
 
262
- runHandler(baseContext.browserName, imagesContext.images, error);
209
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
210
+ if (isRejected) {
211
+ emitWorkerMessage({
212
+ type: 'error',
213
+ payload: { subtype: 'unknown', error: serializeError(error) },
214
+ });
215
+ } else {
216
+ const result = {
217
+ sessionId,
218
+ browserName: baseContext.browserName,
219
+ workerId: process.pid,
220
+ images: imagesContext.images,
221
+ error: serializeError(error),
222
+ duration,
223
+ attachments: imagesContext.attachments,
224
+ retries: message.payload.retries,
225
+ };
226
+ runHandler(baseContext.browserName, result, error);
227
+ }
263
228
  })().catch((error: unknown) => {
264
- workerLogger.error('Unexpected error:', error);
229
+ logger().error('Unexpected error:', error);
265
230
  emitWorkerMessage({
266
231
  type: 'error',
267
232
  payload: { subtype: 'test', error: serializeError(error) },
@@ -269,7 +234,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
269
234
  });
270
235
  });
271
236
 
272
- workerLogger.info('Browser is ready');
237
+ logger().info('Browser is ready');
273
238
 
274
239
  emitWorkerMessage({ type: 'ready' });
275
240
  }
@@ -1,5 +1,5 @@
1
1
  import _ from 'lodash';
2
- import { Parameters } from '@storybook/csf';
2
+ import type { Parameters } from '@storybook/types';
3
3
  import { SetStoriesData, StoriesRaw, CreeveyStoryParams, StoryInput } from '../types.js';
4
4
  import { deserializeRegExp, isSerializedRegExp, isRegExp, serializeRegExp } from './serializeRegExp.js';
5
5