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
@@ -5,6 +5,7 @@ import { isDefined } from '../../types.js';
5
5
  import { logger } from '../logger.js';
6
6
  import { deserializeRawStories } from '../../shared/index.js';
7
7
 
8
+ // TODO Don't have updates from stories
8
9
  export const loadStories: StoriesProvider = async (_config, storiesListener, webdriver) => {
9
10
  if (cluster.isPrimary) {
10
11
  return new Promise<StoriesRaw>((resolve) => {
@@ -17,13 +18,13 @@ export const loadStories: StoriesProvider = async (_config, storiesListener, web
17
18
  if (message.type == 'set') {
18
19
  const { stories, oldTests } = message.payload;
19
20
  if (oldTests.length > 0)
20
- logger.warn(
21
+ logger().warn(
21
22
  `If you use browser stories provider of CSFv3 Storybook feature\n` +
22
23
  `Creevey will not load tests defined in story parameters from following stories:\n` +
23
24
  oldTests.join('\n'),
24
25
  );
25
26
  unsubscribe();
26
- resolve(stories);
27
+ resolve(deserializeRawStories(stories));
27
28
  }
28
29
  });
29
30
  sendStoriesMessage(worker, { type: 'get' });
@@ -36,10 +37,11 @@ export const loadStories: StoriesProvider = async (_config, storiesListener, web
36
37
  } else {
37
38
  subscribeOn('stories', (message) => {
38
39
  if (message.type == 'get')
39
- emitStoriesMessage({ type: 'set', payload: { stories, oldTests: storiesWithOldTests } });
40
+ emitStoriesMessage({ type: 'set', payload: { stories: rawStories, oldTests: storiesWithOldTests } });
40
41
  if (message.type == 'update') storiesListener(new Map(message.payload));
41
42
  });
42
- const stories = deserializeRawStories((await webdriver?.loadStoriesFromBrowser()) ?? {});
43
+ const rawStories = (await webdriver?.loadStoriesFromBrowser()) ?? {};
44
+ const stories = deserializeRawStories(rawStories);
43
45
 
44
46
  const storiesWithOldTests: string[] = [];
45
47
 
@@ -54,7 +54,7 @@ async function parseParams(
54
54
 
55
55
  if (listener) {
56
56
  chokidar.watch(testFiles).on('change', (filePath) => {
57
- logger.debug(`changed: ${filePath}`);
57
+ logger().debug(`changed: ${filePath}`);
58
58
 
59
59
  // doesn't work, always returns {} due modules caching
60
60
  // see https://github.com/nodejs/modules/issues/307
@@ -0,0 +1,71 @@
1
+ import chalk from 'chalk';
2
+ import Logger from 'loglevel';
3
+ import prefix from 'loglevel-plugin-prefix';
4
+ import { FakeTest, isImageError, TEST_EVENTS } from '../../types.js';
5
+ import EventEmitter from 'events';
6
+
7
+ const testLevels: Record<string, string> = {
8
+ INFO: chalk.green('PASS'),
9
+ WARN: chalk.yellow('START'),
10
+ ERROR: chalk.red('FAIL'),
11
+ };
12
+
13
+ export class CreeveyReporter {
14
+ private logger: Logger.Logger | null = null;
15
+ // TODO Output in better way, like vitest, maybe
16
+ constructor(runner: EventEmitter) {
17
+ runner.on(TEST_EVENTS.TEST_BEGIN, (test: FakeTest) => {
18
+ this.getLogger(test.creevey).warn(chalk.cyan(test.fullTitle()));
19
+ });
20
+ runner.on(TEST_EVENTS.TEST_PASS, (test: FakeTest) => {
21
+ this.getLogger(test.creevey).info(chalk.cyan(test.fullTitle()), chalk.gray(`(${test.duration} ms)`));
22
+ });
23
+ runner.on(TEST_EVENTS.TEST_FAIL, (test: FakeTest, error) => {
24
+ this.getLogger(test.creevey).error(
25
+ chalk.cyan(test.fullTitle()),
26
+ chalk.gray(`(${test.duration} ms)`),
27
+ '\n ',
28
+ this.getErrors(
29
+ error,
30
+ (error, imageName) => `${chalk.bold(imageName ?? test.creevey.browserName)}:${error}`,
31
+ (error) => error.stack ?? error.message,
32
+ ).join('\n '),
33
+ );
34
+ });
35
+ }
36
+
37
+ private getLogger(options: { sessionId: string; browserName: string }) {
38
+ if (this.logger) return this.logger;
39
+ const { sessionId, browserName } = options;
40
+ const testLogger = Logger.getLogger(sessionId);
41
+
42
+ this.logger = prefix.apply(testLogger, {
43
+ format(level) {
44
+ return `[${browserName}:${chalk.gray(process.pid)}] ${testLevels[level]} => ${chalk.gray(sessionId)}`;
45
+ },
46
+ });
47
+
48
+ return this.logger;
49
+ }
50
+
51
+ private getErrors(
52
+ error: unknown,
53
+ imageErrorToString: (error: string, imageName?: string) => string,
54
+ errorToString: (error: Error) => string,
55
+ ): string[] {
56
+ const errors = [];
57
+ if (!(error instanceof Error)) {
58
+ errors.push(error as string);
59
+ } else if (!isImageError(error)) {
60
+ errors.push(errorToString(error));
61
+ } else if (typeof error.images == 'string') {
62
+ errors.push(imageErrorToString(error.images));
63
+ } else {
64
+ const imageErrors = error.images ?? {};
65
+ Object.keys(imageErrors).forEach((imageName) => {
66
+ errors.push(imageErrorToString(imageErrors[imageName] ?? '', imageName));
67
+ });
68
+ }
69
+ return errors;
70
+ }
71
+ }
@@ -0,0 +1,11 @@
1
+ import { BaseReporter } from '../../types.js';
2
+ import { CreeveyReporter } from './creevey.js';
3
+ import { JUnitReporter } from './junit.js';
4
+ import { TeamcityReporter } from './teamcity.js';
5
+
6
+ export function getReporter(reporter: BaseReporter | 'creevey' | 'teamcity' | 'junit'): BaseReporter {
7
+ if (reporter === 'creevey') return CreeveyReporter;
8
+ if (reporter === 'teamcity') return TeamcityReporter;
9
+ if (reporter === 'junit') return JUnitReporter;
10
+ return reporter;
11
+ }
@@ -0,0 +1,205 @@
1
+ import EventEmitter from 'events';
2
+ import { dirname, resolve } from 'path';
3
+ import { closeSync, existsSync, mkdirSync, openSync, writeFileSync } from 'fs';
4
+ import { TEST_EVENTS, FakeTest } from '../../types.js';
5
+ import { logger } from '../logger.js';
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ class IndentedLogger<T = any> {
9
+ private currentIndent = '';
10
+
11
+ constructor(private baseLog: (text: string) => T) {}
12
+
13
+ indent(): void {
14
+ this.currentIndent += ' ';
15
+ }
16
+
17
+ unindent(): void {
18
+ this.currentIndent = this.currentIndent.substring(0, this.currentIndent.length - 4);
19
+ }
20
+
21
+ log(text: string): T {
22
+ return this.baseLog(this.currentIndent + text);
23
+ }
24
+ }
25
+
26
+ // NOTE: This is a reworked copy of the JUnitReporter class from Vitest.
27
+ export class JUnitReporter {
28
+ private reportFile: string;
29
+ private fileFd?: number;
30
+ private logger: IndentedLogger<void>;
31
+ private suites: Record<string, FakeTest[]> = {};
32
+ // TODO classnameTemplate
33
+ constructor(runner: EventEmitter, options: { reportDir: string; reporterOptions: { outputFile?: string } }) {
34
+ const { reportDir, reporterOptions } = options;
35
+
36
+ this.reportFile = reporterOptions.outputFile ?? resolve(reportDir, 'junit.xml');
37
+
38
+ this.logger = new IndentedLogger((text) => {
39
+ this.fileFd ??= openSync(this.reportFile, 'w+');
40
+
41
+ writeFileSync(this.fileFd, `${text}\n`);
42
+ });
43
+
44
+ runner.on(TEST_EVENTS.RUN_BEGIN, () => {
45
+ this.suites = {};
46
+
47
+ const outputDirectory = dirname(this.reportFile);
48
+ if (!existsSync(outputDirectory)) {
49
+ mkdirSync(outputDirectory, { recursive: true });
50
+ }
51
+
52
+ this.fileFd = openSync(this.reportFile, 'w+');
53
+ });
54
+ runner.on(TEST_EVENTS.TEST_PASS, (test: FakeTest) => {
55
+ const suite = this.suites[test.parent.title] ?? [];
56
+ suite.push(test);
57
+ });
58
+ runner.on(TEST_EVENTS.TEST_FAIL, (test: FakeTest) => {
59
+ const suite = this.suites[test.parent.title] ?? [];
60
+ suite.push(test);
61
+ });
62
+ runner.on(TEST_EVENTS.RUN_END, () => {
63
+ this.onFinished();
64
+ });
65
+ }
66
+
67
+ private writeElement(name: string, attrs: Record<string, string | number | undefined>, children?: () => void): void {
68
+ const pairs: string[] = [];
69
+ for (const key in attrs) {
70
+ const attr = attrs[key];
71
+ if (attr === undefined) {
72
+ continue;
73
+ }
74
+
75
+ pairs.push(`${key}="${escapeXML(attr)}"`);
76
+ }
77
+
78
+ this.logger.log(`<${name}${pairs.length ? ` ${pairs.join(' ')}` : ''}>`);
79
+ this.logger.indent();
80
+ children?.call(this);
81
+ this.logger.unindent();
82
+
83
+ this.logger.log(`</${name}>`);
84
+ }
85
+
86
+ private writeTasks(tests: FakeTest[]): void {
87
+ for (const test of tests) {
88
+ const classname = test.parent.title;
89
+
90
+ this.writeElement(
91
+ 'testcase',
92
+ {
93
+ classname,
94
+ name: test.title,
95
+ time: getDuration(test),
96
+ },
97
+ () => {
98
+ if (test.state === 'failed') {
99
+ const error = test.err;
100
+ this.writeElement('failure', { message: error });
101
+ }
102
+ },
103
+ );
104
+ }
105
+ }
106
+
107
+ private onFinished(): void {
108
+ this.logger.log('<?xml version="1.0" encoding="UTF-8" ?>');
109
+
110
+ const suites = Object.entries(this.suites).map(([name, tests]) => {
111
+ return {
112
+ name,
113
+ tests,
114
+ failures: tests.filter((test) => test.state === 'failed').length,
115
+ time: tests.reduce((acc, test) => acc + (test.duration ?? 0), 0),
116
+ };
117
+ });
118
+ const stats = suites.reduce(
119
+ (s, { tests, failures, time }) => {
120
+ s.tests += tests.length;
121
+ s.failures += failures;
122
+ s.time += time;
123
+ return s;
124
+ },
125
+ { name: 'creevey tests', tests: 0, failures: 0, time: 0 },
126
+ );
127
+
128
+ this.writeElement('testsuites', { ...stats, time: executionTime(stats.time) }, () => {
129
+ suites.forEach(({ name, tests, failures, time }) => {
130
+ this.writeElement(
131
+ 'testsuite',
132
+ {
133
+ name,
134
+ tests: tests.length,
135
+ failures,
136
+ time: executionTime(time),
137
+ },
138
+ () => {
139
+ this.writeTasks(tests);
140
+ },
141
+ );
142
+ });
143
+ });
144
+
145
+ if (this.reportFile) {
146
+ logger().info(`JUNIT report written to ${this.reportFile}`);
147
+ }
148
+
149
+ if (this.fileFd) {
150
+ closeSync(this.fileFd);
151
+ this.fileFd = undefined;
152
+ }
153
+ }
154
+ }
155
+
156
+ // https://gist.github.com/john-doherty/b9195065884cdbfd2017a4756e6409cc
157
+ function removeInvalidXMLCharacters(value: string, removeDiscouragedChars: boolean): string {
158
+ let regex =
159
+ // eslint-disable-next-line no-control-regex
160
+ /([\0-\x08\v\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g;
161
+ value = String(value).replace(regex, '');
162
+
163
+ if (removeDiscouragedChars) {
164
+ // remove everything discouraged by XML 1.0 specifications
165
+ regex = new RegExp(
166
+ '([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|\\uD83F[\\uDFFE\\uDFFF]|(?:\\uD87F[\\uDF' +
167
+ 'FE\\uDFFF])|\\uD8BF[\\uDFFE\\uDFFF]|\\uD8FF[\\uDFFE\\uDFFF]|(?:\\uD93F[\\uDFFE\\uD' +
168
+ 'FFF])|\\uD97F[\\uDFFE\\uDFFF]|\\uD9BF[\\uDFFE\\uDFFF]|\\uD9FF[\\uDFFE\\uDFFF]' +
169
+ '|\\uDA3F[\\uDFFE\\uDFFF]|\\uDA7F[\\uDFFE\\uDFFF]|\\uDABF[\\uDFFE\\uDFFF]|(?:\\' +
170
+ 'uDAFF[\\uDFFE\\uDFFF])|\\uDB3F[\\uDFFE\\uDFFF]|\\uDB7F[\\uDFFE\\uDFFF]|(?:\\uDBBF' +
171
+ '[\\uDFFE\\uDFFF])|\\uDBFF[\\uDFFE\\uDFFF](?:[\\0-\\t\\v\\f\\x0E-\\u2027\\u202A-\\uD7FF\\' +
172
+ 'uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|' +
173
+ '(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))',
174
+ 'g',
175
+ );
176
+
177
+ value = value.replace(regex, '');
178
+ }
179
+
180
+ return value;
181
+ }
182
+
183
+ function escapeXML(value: string | number): string {
184
+ return removeInvalidXMLCharacters(
185
+ String(value)
186
+ .replace(/&/g, '&amp;')
187
+ .replace(/"/g, '&quot;')
188
+ .replace(/'/g, '&apos;')
189
+ .replace(/</g, '&lt;')
190
+ .replace(/>/g, '&gt;'),
191
+ true,
192
+ );
193
+ }
194
+
195
+ function executionTime(durationMS: number) {
196
+ return (durationMS / 1000).toLocaleString('en-US', {
197
+ useGrouping: false,
198
+ maximumFractionDigits: 10,
199
+ });
200
+ }
201
+
202
+ function getDuration(task: FakeTest): string | undefined {
203
+ const duration = task.duration ?? 0;
204
+ return executionTime(duration);
205
+ }
@@ -0,0 +1,74 @@
1
+ import { FakeTest, Images, isDefined, TEST_EVENTS } from '../../types.js';
2
+ import EventEmitter from 'events';
3
+
4
+ export class TeamcityReporter {
5
+ constructor(runner: EventEmitter, options: { reportDir: string }) {
6
+ const { reportDir } = options;
7
+
8
+ runner.on(TEST_EVENTS.TEST_BEGIN, (test: FakeTest) => {
9
+ console.log(`##teamcity[testStarted name='${this.escape(test.fullTitle())}' flowId='${test.creevey.workerId}']`);
10
+ });
11
+
12
+ runner.on(TEST_EVENTS.TEST_PASS, (test: FakeTest) => {
13
+ console.log(`##teamcity[testFinished name='${this.escape(test.fullTitle())}' flowId='${test.creevey.workerId}']`);
14
+ });
15
+
16
+ runner.on(TEST_EVENTS.TEST_FAIL, (test: FakeTest, error: Error) => {
17
+ const browserName = this.escape(test.creevey.browserName);
18
+ Object.entries(test.creevey.images).forEach(([name, image]) => {
19
+ if (!image) return;
20
+ const filePath = test
21
+ .titlePath()
22
+ .slice(0, -1)
23
+ .concat(name == browserName ? [] : [browserName])
24
+ .map(this.escape)
25
+ .join('/');
26
+
27
+ const { error: _, ...rest } = image;
28
+ Object.values(rest as Partial<Images>)
29
+ .filter(isDefined)
30
+ .forEach((fileName) => {
31
+ console.log(`##teamcity[publishArtifacts '${reportDir}/${filePath}/${fileName} => report/${filePath}']`);
32
+ console.log(
33
+ `##teamcity[testMetadata testName='${this.escape(
34
+ test.fullTitle(),
35
+ )}' type='image' value='report/${filePath}/${fileName}' flowId='${test.creevey.workerId}']`,
36
+ );
37
+ });
38
+ });
39
+
40
+ // Output failed test as passed due TC don't support retry mechanic
41
+ // https://teamcity-support.jetbrains.com/hc/en-us/community/posts/207216829-Count-test-as-successful-if-at-least-one-try-is-successful?page=1#community_comment_207394125
42
+
43
+ if (test.creevey.willRetry)
44
+ console.log(
45
+ `##teamcity[testFinished name='${this.escape(test.fullTitle())}' flowId='${test.creevey.workerId}']`,
46
+ );
47
+ else
48
+ console.log(
49
+ `##teamcity[testFailed name='${this.escape(test.fullTitle())}' message='${this.escape(
50
+ error.message,
51
+ )}' details='${this.escape(error.stack ?? '')}' flowId='${test.creevey.workerId}']`,
52
+ );
53
+ });
54
+ }
55
+
56
+ private escape = (str: string): string => {
57
+ if (!str) return '';
58
+ return (
59
+ str
60
+ .toString()
61
+ // eslint-disable-next-line no-control-regex
62
+ .replace(/\x1B.*?m/g, '')
63
+ .replace(/\|/g, '||')
64
+ .replace(/\n/g, '|n')
65
+ .replace(/\r/g, '|r')
66
+ .replace(/\[/g, '|[')
67
+ .replace(/\]/g, '|]')
68
+ .replace(/\u0085/g, '|x')
69
+ .replace(/\u2028/g, '|l')
70
+ .replace(/\u2029/g, '|p')
71
+ .replace(/'/g, "|'")
72
+ );
73
+ };
74
+ }