creevey 0.10.0-beta.3 → 0.10.0-beta.30

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 (186) hide show
  1. package/dist/client/addon/components/Addon.js +17 -7
  2. package/dist/client/addon/components/Addon.js.map +1 -1
  3. package/dist/client/addon/components/Panel.js +2 -2
  4. package/dist/client/addon/components/Panel.js.map +1 -1
  5. package/dist/client/addon/components/Tools.js +17 -7
  6. package/dist/client/addon/components/Tools.js.map +1 -1
  7. package/dist/client/addon/withCreevey.d.ts +1 -0
  8. package/dist/client/addon/withCreevey.js +10 -1
  9. package/dist/client/addon/withCreevey.js.map +1 -1
  10. package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
  11. package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
  12. package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
  13. package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
  14. package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
  15. package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
  16. package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
  17. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  18. package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
  19. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  20. package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
  21. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  22. package/dist/client/shared/components/ResultsPage.js +43 -13
  23. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  24. package/dist/client/shared/creeveyClientApi.js +8 -1
  25. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  26. package/dist/client/shared/helpers.d.ts +1 -3
  27. package/dist/client/shared/helpers.js +4 -19
  28. package/dist/client/shared/helpers.js.map +1 -1
  29. package/dist/client/web/CreeveyApp.js +41 -14
  30. package/dist/client/web/CreeveyApp.js.map +1 -1
  31. package/dist/client/web/CreeveyContext.d.ts +5 -0
  32. package/dist/client/web/CreeveyContext.js +20 -7
  33. package/dist/client/web/CreeveyContext.js.map +1 -1
  34. package/dist/client/web/CreeveyLoader.js +2 -2
  35. package/dist/client/web/CreeveyLoader.js.map +1 -1
  36. package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
  37. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  38. package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
  39. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  40. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
  41. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  42. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
  43. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  44. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
  45. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  46. package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
  47. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  48. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  49. package/dist/client/web/KeyboardEventsContext.js +79 -64
  50. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  51. package/dist/client/web/assets/index-C5QCFtF-.js +595 -0
  52. package/dist/client/web/index.html +1 -1
  53. package/dist/client/web/index.js +17 -7
  54. package/dist/client/web/index.js.map +1 -1
  55. package/dist/client/web/themes.d.ts +2 -0
  56. package/dist/client/web/themes.js +22 -0
  57. package/dist/client/web/themes.js.map +1 -0
  58. package/dist/creevey.js +16 -9
  59. package/dist/creevey.js.map +1 -1
  60. package/dist/index.d.ts +1 -0
  61. package/dist/server/config.d.ts +1 -1
  62. package/dist/server/config.js +29 -7
  63. package/dist/server/config.js.map +1 -1
  64. package/dist/server/connection.d.ts +3 -0
  65. package/dist/server/connection.js +28 -0
  66. package/dist/server/connection.js.map +1 -0
  67. package/dist/server/docker.js +38 -21
  68. package/dist/server/docker.js.map +1 -1
  69. package/dist/server/index.js +63 -11
  70. package/dist/server/index.js.map +1 -1
  71. package/dist/server/logger.d.ts +2 -1
  72. package/dist/server/logger.js +7 -3
  73. package/dist/server/logger.js.map +1 -1
  74. package/dist/server/master/api.js +1 -1
  75. package/dist/server/master/api.js.map +1 -1
  76. package/dist/server/master/pool.d.ts +4 -3
  77. package/dist/server/master/pool.js +12 -63
  78. package/dist/server/master/pool.js.map +1 -1
  79. package/dist/server/master/queue.d.ts +13 -0
  80. package/dist/server/master/queue.js +71 -0
  81. package/dist/server/master/queue.js.map +1 -0
  82. package/dist/server/master/runner.d.ts +1 -0
  83. package/dist/server/master/runner.js +4 -1
  84. package/dist/server/master/runner.js.map +1 -1
  85. package/dist/server/master/server.js +1 -1
  86. package/dist/server/master/server.js.map +1 -1
  87. package/dist/server/master/start.js +13 -11
  88. package/dist/server/master/start.js.map +1 -1
  89. package/dist/server/playwright/docker-file.d.ts +2 -1
  90. package/dist/server/playwright/docker-file.js +7 -5
  91. package/dist/server/playwright/docker-file.js.map +1 -1
  92. package/dist/server/playwright/internal.d.ts +5 -4
  93. package/dist/server/playwright/internal.js +91 -71
  94. package/dist/server/playwright/internal.js.map +1 -1
  95. package/dist/server/playwright/webdriver.d.ts +1 -1
  96. package/dist/server/playwright/webdriver.js +1 -1
  97. package/dist/server/playwright/webdriver.js.map +1 -1
  98. package/dist/server/providers/browser.js +6 -4
  99. package/dist/server/providers/browser.js.map +1 -1
  100. package/dist/server/providers/hybrid.js +1 -1
  101. package/dist/server/providers/hybrid.js.map +1 -1
  102. package/dist/server/reporter.js +13 -9
  103. package/dist/server/reporter.js.map +1 -1
  104. package/dist/server/selenium/internal.d.ts +3 -4
  105. package/dist/server/selenium/internal.js +127 -99
  106. package/dist/server/selenium/internal.js.map +1 -1
  107. package/dist/server/selenium/selenoid.js +9 -6
  108. package/dist/server/selenium/selenoid.js.map +1 -1
  109. package/dist/server/selenium/webdriver.d.ts +1 -1
  110. package/dist/server/selenium/webdriver.js +1 -1
  111. package/dist/server/selenium/webdriver.js.map +1 -1
  112. package/dist/server/telemetry.js +7 -3
  113. package/dist/server/telemetry.js.map +1 -1
  114. package/dist/server/testsFiles/parser.js +44 -2
  115. package/dist/server/testsFiles/parser.js.map +1 -1
  116. package/dist/server/utils.d.ts +20 -1
  117. package/dist/server/utils.js +82 -7
  118. package/dist/server/utils.js.map +1 -1
  119. package/dist/server/webdriver.d.ts +3 -4
  120. package/dist/server/webdriver.js +10 -9
  121. package/dist/server/webdriver.js.map +1 -1
  122. package/dist/server/worker/chai-image.d.ts +1 -2
  123. package/dist/server/worker/chai-image.js +4 -3
  124. package/dist/server/worker/chai-image.js.map +1 -1
  125. package/dist/server/worker/match-image.d.ts +4 -4
  126. package/dist/server/worker/match-image.js +7 -4
  127. package/dist/server/worker/match-image.js.map +1 -1
  128. package/dist/server/worker/start.js +24 -14
  129. package/dist/server/worker/start.js.map +1 -1
  130. package/dist/shared/index.d.ts +1 -1
  131. package/dist/types.d.ts +38 -13
  132. package/dist/types.js.map +1 -1
  133. package/docs/config.md +3 -0
  134. package/package.json +66 -64
  135. package/src/client/addon/components/Panel.tsx +2 -2
  136. package/src/client/addon/withCreevey.ts +8 -1
  137. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  138. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  139. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
  140. package/src/client/shared/components/ResultsPage.tsx +31 -8
  141. package/src/client/shared/creeveyClientApi.ts +9 -1
  142. package/src/client/shared/helpers.ts +4 -24
  143. package/src/client/web/CreeveyApp.tsx +26 -8
  144. package/src/client/web/CreeveyContext.tsx +9 -0
  145. package/src/client/web/CreeveyLoader.tsx +1 -1
  146. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  147. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  148. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  149. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  150. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  151. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  152. package/src/client/web/themes.ts +24 -0
  153. package/src/creevey.ts +16 -10
  154. package/src/server/config.ts +30 -8
  155. package/src/server/connection.ts +26 -0
  156. package/src/server/docker.ts +42 -24
  157. package/src/server/index.ts +73 -14
  158. package/src/server/logger.ts +6 -2
  159. package/src/server/master/api.ts +1 -1
  160. package/src/server/master/pool.ts +22 -56
  161. package/src/server/master/queue.ts +77 -0
  162. package/src/server/master/runner.ts +4 -1
  163. package/src/server/master/server.ts +1 -1
  164. package/src/server/master/start.ts +16 -11
  165. package/src/server/playwright/docker-file.ts +8 -5
  166. package/src/server/playwright/internal.ts +91 -78
  167. package/src/server/playwright/webdriver.ts +2 -2
  168. package/src/server/providers/browser.ts +6 -4
  169. package/src/server/providers/hybrid.ts +1 -1
  170. package/src/server/reporter.ts +15 -9
  171. package/src/server/selenium/internal.ts +131 -107
  172. package/src/server/selenium/selenoid.ts +9 -7
  173. package/src/server/selenium/webdriver.ts +2 -2
  174. package/src/server/telemetry.ts +7 -3
  175. package/src/server/testsFiles/parser.ts +51 -1
  176. package/src/server/utils.ts +87 -8
  177. package/src/server/webdriver.ts +11 -16
  178. package/src/server/worker/chai-image.ts +4 -4
  179. package/src/server/worker/match-image.ts +12 -8
  180. package/src/server/worker/start.ts +25 -16
  181. package/src/shared/index.ts +1 -1
  182. package/src/types.ts +40 -15
  183. package/types/global.d.ts +1 -0
  184. package/.yarnrc.yml +0 -1
  185. package/chromatic.config.json +0 -5
  186. package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
@@ -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> {
@@ -1,13 +1,15 @@
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 assert from 'assert';
12
+ import pidtree from 'pidtree';
11
13
 
12
14
  const importMetaUrl = pathToFileURL(__filename).href;
13
15
 
@@ -15,6 +17,19 @@ export const isShuttingDown = { current: false };
15
17
 
16
18
  export const configExt = ['.js', '.mjs', '.ts', '.cjs', '.mts', '.cts'];
17
19
 
20
+ const browserTypes = {
21
+ chromium: 'chromium',
22
+ 'chromium-headless-shell': 'chromium',
23
+ chrome: 'chromium',
24
+ 'chrome-beta': 'chromium',
25
+ msedge: 'chromium',
26
+ 'msedge-beta': 'chromium',
27
+ 'msedge-dev': 'chromium',
28
+ 'bidi-chromium': 'chromium',
29
+ firefox: 'firefox',
30
+ webkit: 'webkit',
31
+ } as const;
32
+
18
33
  export const skipOptionKeys = ['in', 'kinds', 'stories', 'tests', 'reason'];
19
34
 
20
35
  function matchBy(pattern: string | string[] | RegExp | undefined, value: string): boolean {
@@ -83,7 +98,7 @@ export async function shutdownWorkers(): Promise<void> {
83
98
  (worker) =>
84
99
  new Promise<void>((resolve) => {
85
100
  const timeout = setTimeout(() => {
86
- worker.kill();
101
+ if (worker.process.pid) void killTree(worker.process.pid);
87
102
  }, 10000);
88
103
  worker.on('exit', () => {
89
104
  clearTimeout(timeout);
@@ -96,6 +111,46 @@ export async function shutdownWorkers(): Promise<void> {
96
111
  emitShutdownMessage();
97
112
  }
98
113
 
114
+ export function gracefullyKill(worker: Worker): void {
115
+ worker.isShuttingDown = true;
116
+ const timeout = setTimeout(() => {
117
+ if (worker.process.pid) void killTree(worker.process.pid);
118
+ }, 10000);
119
+ worker.on('exit', () => {
120
+ clearTimeout(timeout);
121
+ });
122
+ sendShutdownMessage(worker);
123
+ }
124
+
125
+ export async function killTree(rootPid: number): Promise<void> {
126
+ const pids = await pidtree(rootPid, { root: true });
127
+
128
+ pids.forEach((pid) => {
129
+ try {
130
+ process.kill(pid, 'SIGKILL');
131
+ } catch {
132
+ /* noop */
133
+ }
134
+ });
135
+ }
136
+
137
+ export function shutdown(): void {
138
+ process.exit();
139
+ }
140
+
141
+ export function shutdownWithError(): void {
142
+ process.exit(1);
143
+ }
144
+
145
+ export function resolvePlaywrightBrowserType(browserName: string): (typeof browserTypes)[keyof typeof browserTypes] {
146
+ assert(
147
+ browserName in browserTypes,
148
+ new Error(`Failed to match browser name "${browserName}" to playwright browserType`),
149
+ );
150
+
151
+ return browserTypes[browserName as keyof typeof browserTypes];
152
+ }
153
+
99
154
  export async function getCreeveyCache(): Promise<string | undefined> {
100
155
  const { default: findCacheDir } = await import('find-cache-dir');
101
156
  return findCacheDir({ name: 'creevey', cwd: dirname(fileURLToPath(importMetaUrl)) });
@@ -131,11 +186,12 @@ export function testsToImages(tests: (TestData | undefined)[]): Set<string> {
131
186
 
132
187
  // https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
133
188
  export const isInsideDocker =
134
- fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf-8').includes('docker');
189
+ (fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf-8').includes('docker')) ||
190
+ process.env.DOCKER === 'true';
135
191
 
136
192
  export const downloadBinary = (downloadUrl: string, destination: string): Promise<void> =>
137
193
  new Promise((resolve, reject) =>
138
- get(downloadUrl, (response) => {
194
+ https.get(downloadUrl, (response) => {
139
195
  if (response.statusCode == 302) {
140
196
  const { location } = response.headers;
141
197
  if (!location) {
@@ -175,10 +231,10 @@ export function readDirRecursive(dirPath: string): string[] {
175
231
  );
176
232
  }
177
233
 
178
- const _require = createRequire(importMetaUrl);
179
234
  export function tryToLoadTestsData(filename: string): Partial<Record<string, ServerTest>> | undefined {
180
235
  try {
181
- return _require(filename) as Partial<Record<string, ServerTest>>;
236
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
237
+ return require(filename) as Partial<Record<string, ServerTest>>;
182
238
  } catch {
183
239
  /* noop */
184
240
  }
@@ -204,3 +260,26 @@ export async function loadThroughTSX<T>(
204
260
 
205
261
  return result;
206
262
  }
263
+
264
+ export function waitOnUrl(url: string, timeout: number, delay: number) {
265
+ const startTime = Date.now();
266
+ return new Promise<void>((resolve, reject) => {
267
+ const interval = setInterval(() => {
268
+ http
269
+ .get(url, (response) => {
270
+ if (response.statusCode === 200) {
271
+ clearInterval(interval);
272
+ resolve();
273
+ }
274
+ })
275
+ .on('error', () => {
276
+ // Ignore HTTP errors
277
+ });
278
+
279
+ if (Date.now() - startTime > timeout) {
280
+ clearInterval(interval);
281
+ reject(new Error(`${url} didn't respond within ${timeout / 1000} seconds`));
282
+ }
283
+ }, delay);
284
+ });
285
+ }
@@ -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/csf';
6
5
  import {
7
6
  isDefined,
8
7
  StoryInput,
@@ -22,15 +21,15 @@ const DOCKER_INTERNAL = 'host.docker.internal';
22
21
  export async function resolveStorybookUrl(
23
22
  storybookUrl: string,
24
23
  checkUrl: (url: string) => Promise<boolean>,
25
- logger: Logger.Logger = defaultLogger,
26
24
  ): Promise<string> {
27
- logger.debug('Resolving storybook url');
25
+ logger().debug('Resolving storybook url');
28
26
  const addresses = getAddresses();
27
+ // TODO Use Promise.race?
29
28
  for (const ip of addresses) {
30
29
  const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
31
- logger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
30
+ logger().debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
32
31
  if (await checkUrl(resolvedUrl)) {
33
- logger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
32
+ logger().debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
34
33
  return resolvedUrl;
35
34
  }
36
35
  }
@@ -74,11 +73,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
74
73
 
75
74
  abstract afterTest(test: ServerTest): Promise<void>;
76
75
 
77
- async switchStory(
78
- story: StoryInput,
79
- context: BaseCreeveyTestContext,
80
- logger: Logger.Logger,
81
- ): Promise<CreeveyTestContext> {
76
+ async switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext> {
82
77
  const { id, title, name, parameters } = story;
83
78
  const {
84
79
  captureElement = `#${storybookRootID}`,
@@ -86,7 +81,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
86
81
  ignoreElements,
87
82
  } = (parameters.creevey ?? {}) as CreeveyStoryParams;
88
83
 
89
- logger.debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
84
+ logger().debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
90
85
 
91
86
  let storyPlayResolver: (isCompleted: boolean) => void;
92
87
  let waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
@@ -107,7 +102,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
107
102
  const isCaptureCalled = await this.selectStory(id, waitForReady);
108
103
 
109
104
  if (isCaptureCalled) {
110
- logger.debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
105
+ logger().debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
111
106
  while (!(await waitForComplete)) {
112
107
  waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
113
108
  }
@@ -115,8 +110,8 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
115
110
 
116
111
  unsubscribe();
117
112
 
118
- if (isCaptureCalled) logger.debug(`Story ${chalk.magenta(id)} completed capturing`);
119
- else logger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
113
+ if (isCaptureCalled) logger().debug(`Story ${chalk.magenta(id)} completed capturing`);
114
+ else logger().debug(`Story ${chalk.magenta(id)} ready for capturing`);
120
115
 
121
116
  return Object.assign(
122
117
  {
@@ -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;
@@ -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,6 +1,4 @@
1
1
  import chai from 'chai';
2
- import chalk from 'chalk';
3
- import Logger from 'loglevel';
4
2
  import EventEmitter from 'events';
5
3
  import {
6
4
  BaseCreeveyTestContext,
@@ -86,7 +84,7 @@ function runHandler(browserName: string, images: Partial<Record<string, Images>>
86
84
 
87
85
  async function setupWebdriver(webdriver: CreeveyWebdriver): Promise<[string, CreeveyWebdriver] | undefined> {
88
86
  if ((await webdriver.openBrowser(true)) == null) {
89
- logger.error('Failed to start browser');
87
+ logger().error('Failed to start browser');
90
88
  emitWorkerMessage({
91
89
  type: 'error',
92
90
  payload: { subtype: 'browser', error: 'Failed to start browser' },
@@ -125,8 +123,6 @@ export async function start(browser: string, gridUrl: string, config: Config, op
125
123
 
126
124
  if (!webdriver || !sessionId) return;
127
125
 
128
- const workerLogger = Logger.getLogger(`${browser}:${chalk.gray(sessionId)}`);
129
-
130
126
  const reporterOptions = {
131
127
  ...config.reporterOptions,
132
128
  creevey: {
@@ -150,13 +146,13 @@ export async function start(browser: string, gridUrl: string, config: Config, op
150
146
  const { matchImage, matchImages } = options.odiff
151
147
  ? getOdiffMatchers(imagesContext, config)
152
148
  : await getMatchers(imagesContext, config);
153
- chai.use(chaiImage(matchImage, matchImages, workerLogger));
149
+ chai.use(chaiImage(matchImage, matchImages));
154
150
 
155
151
  const tests = await (async () => {
156
152
  try {
157
153
  return await getTestsFromStories(config, browser, webdriver);
158
154
  } catch (error) {
159
- workerLogger.error('Failed to get tests from stories:', error);
155
+ logger().error('Failed to get tests from stories:', error);
160
156
  emitWorkerMessage({
161
157
  type: 'error',
162
158
  payload: { subtype: 'browser', error: serializeError(error) },
@@ -174,7 +170,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
174
170
 
175
171
  if (!test) {
176
172
  const error = `Test with id ${message.payload.id} not found`;
177
- workerLogger.error(error);
173
+ logger().error(error);
178
174
  emitWorkerMessage({
179
175
  type: 'error',
180
176
  payload: { subtype: 'test', error },
@@ -226,16 +222,20 @@ export async function start(browser: string, gridUrl: string, config: Config, op
226
222
  runner.emit(TEST_EVENTS.RUN_BEGIN);
227
223
  runner.emit(TEST_EVENTS.TEST_BEGIN, fakeTest);
228
224
 
225
+ let timeout;
226
+ let isRejected = false;
229
227
  const start = Date.now();
230
228
  try {
231
229
  await Promise.race([
232
- new Promise((reject) =>
233
- setTimeout(() => {
234
- reject(`Timeout of ${config.testTimeout}ms exceeded`);
235
- }, config.testTimeout),
230
+ new Promise(
231
+ (_, reject) =>
232
+ (timeout = setTimeout(() => {
233
+ isRejected = true;
234
+ reject(new Error(`Timeout of ${config.testTimeout}ms exceeded`));
235
+ }, config.testTimeout)),
236
236
  ),
237
237
  (async () => {
238
- const context = await webdriver.switchStory(test.story, baseContext, workerLogger);
238
+ const context = await webdriver.switchStory(test.story, baseContext);
239
239
  await test.fn(context);
240
240
  })(),
241
241
  ]);
@@ -244,6 +244,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
244
244
  fakeTest.err = error;
245
245
  }
246
246
  const duration = Date.now() - start;
247
+ clearTimeout(timeout);
247
248
  fakeTest.attachments = imagesContext.attachments;
248
249
  fakeTest.state = error ? 'failed' : 'passed';
249
250
  fakeTest.duration = duration;
@@ -259,9 +260,17 @@ export async function start(browser: string, gridUrl: string, config: Config, op
259
260
 
260
261
  await webdriver.afterTest(test);
261
262
 
262
- runHandler(baseContext.browserName, imagesContext.images, error);
263
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
264
+ if (isRejected) {
265
+ emitWorkerMessage({
266
+ type: 'error',
267
+ payload: { subtype: 'unknown', error: serializeError(error) },
268
+ });
269
+ } else {
270
+ runHandler(baseContext.browserName, imagesContext.images, error);
271
+ }
263
272
  })().catch((error: unknown) => {
264
- workerLogger.error('Unexpected error:', error);
273
+ logger().error('Unexpected error:', error);
265
274
  emitWorkerMessage({
266
275
  type: 'error',
267
276
  payload: { subtype: 'test', error: serializeError(error) },
@@ -269,7 +278,7 @@ export async function start(browser: string, gridUrl: string, config: Config, op
269
278
  });
270
279
  });
271
280
 
272
- workerLogger.info('Browser is ready');
281
+ logger().info('Browser is ready');
273
282
 
274
283
  emitWorkerMessage({ type: 'ready' });
275
284
  }
@@ -1,5 +1,5 @@
1
1
  import _ from 'lodash';
2
- import { Parameters } from '@storybook/csf';
2
+ import type { Parameters } from '@storybook/csf';
3
3
  import { SetStoriesData, StoriesRaw, CreeveyStoryParams, StoryInput } from '../types.js';
4
4
  import { deserializeRegExp, isSerializedRegExp, isRegExp, serializeRegExp } from './serializeRegExp.js';
5
5
 
package/src/types.ts CHANGED
@@ -4,7 +4,7 @@ import type Pixelmatch from 'pixelmatch';
4
4
  import type { ODiffOptions } from 'odiff-bin';
5
5
  import type { expect } from 'chai';
6
6
  import type EventEmitter from 'events';
7
- import type Logger from 'loglevel';
7
+ import type { LaunchOptions } from 'playwright-core';
8
8
  // import type { Browser } from 'playwright-core';
9
9
 
10
10
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -102,15 +102,8 @@ export class ChromeConfig {
102
102
  }
103
103
  */
104
104
  export interface BrowserConfigObject {
105
+ // TODO Restrict browser names for playwright images
105
106
  browserName: string;
106
- /**
107
- * Browser version. Ignored with Playwright webdriver
108
- */
109
- browserVersion?: string;
110
- /**
111
- * Operation system name. Ignored with Playwright webdriver
112
- */
113
- platformName?: string;
114
107
  // customizeBuilder?: (builder: Builder) => Builder;
115
108
  limit?: number;
116
109
  /**
@@ -134,15 +127,32 @@ export interface BrowserConfigObject {
134
127
  * Used only with `useDocker == false`
135
128
  */
136
129
  webdriverCommand?: string[];
137
- // TODO Check version compatibility
138
- // playwrightVersion?: string;
139
130
  // /**
140
131
  // * Use to start standalone playwright browser
141
132
  // */
142
133
  // playwrightBrowser?: () => Promise<Browser>;
143
134
  viewport?: { width: number; height: number };
144
135
 
145
- [name: string]: unknown;
136
+ seleniumCapabilities?: {
137
+ /**
138
+ * Browser version. Ignored with Playwright webdriver
139
+ */
140
+ browserVersion?: string;
141
+ /**
142
+ * Operation system name. Ignored with Playwright webdriver
143
+ */
144
+ platformName?: string;
145
+ [name: string]: unknown;
146
+ };
147
+
148
+ playwrightOptions?: Omit<LaunchOptions, 'logger'> & {
149
+ trace?: {
150
+ screenshots?: boolean;
151
+ snapshots?: boolean;
152
+ sources?: boolean;
153
+ path: string;
154
+ };
155
+ };
146
156
  }
147
157
 
148
158
  export type StorybookGlobals = Record<string, unknown>;
@@ -161,7 +171,7 @@ export interface CreeveyWebdriver {
161
171
  openBrowser(fresh?: boolean): Promise<CreeveyWebdriver | null>;
162
172
  closeBrowser(): Promise<void>;
163
173
  loadStoriesFromBrowser(): Promise<StoriesRaw>;
164
- switchStory(story: StoryInput, context: BaseCreeveyTestContext, logger: Logger.Logger): Promise<CreeveyTestContext>;
174
+ switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext>;
165
175
  afterTest(test: ServerTest): Promise<void>;
166
176
  }
167
177
 
@@ -197,6 +207,11 @@ export interface Config {
197
207
  * Url where storybook hosted on
198
208
  */
199
209
  resolveStorybookUrl?: () => Promise<string>;
210
+ /**
211
+ * Command to automatically start Storybook if it is not running.
212
+ * For example, `npm run storybook`, `yarn run storybook` etc.
213
+ */
214
+ storybookAutorunCmd?: string;
200
215
  /**
201
216
  * Absolute path to directory with reference images
202
217
  * @default path.join(process.cwd(), './images')
@@ -304,6 +319,11 @@ export interface Config {
304
319
  * The `--ui` CLI option ignores this option
305
320
  */
306
321
  failFast: boolean;
322
+ /**
323
+ * Start workers in sequential queue
324
+ * @default false
325
+ */
326
+ useWorkerQueue: boolean;
307
327
  /**
308
328
  * Specify platform for docker images
309
329
  */
@@ -345,9 +365,14 @@ export interface Options {
345
365
  screenDir?: string;
346
366
  reportDir?: string;
347
367
  gridUrl?: string;
368
+ storybookStart?: boolean | string;
348
369
  storybookUrl?: string;
370
+ storybookPort?: string;
371
+ storybookAutorunCmd?: string;
372
+ saveReport: boolean;
349
373
  failFast?: boolean;
350
374
  odiff?: boolean;
375
+ noDocker?: boolean;
351
376
  }
352
377
 
353
378
  export type WorkerError = 'browser' | 'test' | 'unknown';
@@ -434,8 +459,8 @@ export interface BaseCreeveyTestContext {
434
459
  * @internal
435
460
  */
436
461
  screenshots: { imageName?: string; screenshot: Buffer }[];
437
- matchImage: (image: Buffer, imageName?: string) => Promise<void>;
438
- matchImages: (images: Record<string, Buffer>) => Promise<void>;
462
+ matchImage: (image: Buffer | string, imageName?: string) => Promise<void>;
463
+ matchImages: (images: Record<string, Buffer | string>) => Promise<void>;
439
464
  }
440
465
 
441
466
  export interface CreeveyTestContext extends BaseCreeveyTestContext {
package/types/global.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable no-var */
2
2
 
3
+ declare var __CREEVEY_ENV__: boolean;
3
4
  declare var __CREEVEY_SERVER_HOST__: string;
4
5
  declare var __CREEVEY_SERVER_PORT__: number | null;
5
6
  declare var __CREEVEY_CLIENT_PORT__: number | null;
package/.yarnrc.yml DELETED
@@ -1 +0,0 @@
1
- nodeLinker: node-modules
@@ -1,5 +0,0 @@
1
- {
2
- "onlyChanged": true,
3
- "projectId": "Project:5fd73972015e660021b97564",
4
- "zip": true
5
- }