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.
- package/README.md +19 -41
- package/dist/client/addon/components/Addon.js +17 -7
- package/dist/client/addon/components/Addon.js.map +1 -1
- package/dist/client/addon/components/Panel.js +2 -2
- package/dist/client/addon/components/Panel.js.map +1 -1
- package/dist/client/addon/components/Tools.js +17 -7
- package/dist/client/addon/components/Tools.js.map +1 -1
- package/dist/client/addon/withCreevey.d.ts +2 -1
- package/dist/client/addon/withCreevey.js +11 -1
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
- package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
- package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
- package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
- package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
- package/dist/client/shared/components/PageHeader/ImagePreview.d.ts +1 -1
- package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
- package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
- package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/components/ResultsPage.d.ts +1 -1
- package/dist/client/shared/components/ResultsPage.js +43 -13
- package/dist/client/shared/components/ResultsPage.js.map +1 -1
- package/dist/client/shared/creeveyClientApi.js +8 -1
- package/dist/client/shared/creeveyClientApi.js.map +1 -1
- package/dist/client/shared/helpers.d.ts +1 -3
- package/dist/client/shared/helpers.js +4 -19
- package/dist/client/shared/helpers.js.map +1 -1
- package/dist/client/web/CreeveyApp.js +42 -14
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyContext.d.ts +5 -0
- package/dist/client/web/CreeveyContext.js +20 -7
- package/dist/client/web/CreeveyContext.js.map +1 -1
- package/dist/client/web/CreeveyLoader.js +2 -2
- package/dist/client/web/CreeveyLoader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
- package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
- package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +2 -2
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
- package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
- package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
- package/dist/client/web/KeyboardEventsContext.js +79 -64
- package/dist/client/web/KeyboardEventsContext.js.map +1 -1
- package/dist/client/web/assets/index-C47njyZV.js +802 -0
- package/dist/client/web/index.html +1 -1
- package/dist/client/web/index.js +17 -7
- package/dist/client/web/index.js.map +1 -1
- package/dist/client/web/themes.d.ts +2 -0
- package/dist/client/web/themes.js +22 -0
- package/dist/client/web/themes.js.map +1 -0
- package/dist/creevey.js +16 -9
- package/dist/creevey.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/server/config.d.ts +1 -1
- package/dist/server/config.js +30 -7
- package/dist/server/config.js.map +1 -1
- package/dist/server/connection.d.ts +3 -0
- package/dist/server/connection.js +28 -0
- package/dist/server/connection.js.map +1 -0
- package/dist/server/docker.d.ts +1 -1
- package/dist/server/docker.js +56 -32
- package/dist/server/docker.js.map +1 -1
- package/dist/server/index.js +64 -11
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +2 -1
- package/dist/server/logger.js +7 -3
- package/dist/server/logger.js.map +1 -1
- package/dist/server/master/api.js +1 -1
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/pool.d.ts +4 -3
- package/dist/server/master/pool.js +13 -66
- package/dist/server/master/pool.js.map +1 -1
- package/dist/server/master/queue.d.ts +13 -0
- package/dist/server/master/queue.js +71 -0
- package/dist/server/master/queue.js.map +1 -0
- package/dist/server/master/runner.d.ts +3 -0
- package/dist/server/master/runner.js +78 -10
- package/dist/server/master/runner.js.map +1 -1
- package/dist/server/master/server.js +1 -1
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/master/start.js +13 -11
- package/dist/server/master/start.js.map +1 -1
- package/dist/server/playwright/docker-file.d.ts +1 -1
- package/dist/server/playwright/docker-file.js +15 -6
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/docker.d.ts +2 -1
- package/dist/server/playwright/docker.js +10 -2
- package/dist/server/playwright/docker.js.map +1 -1
- package/dist/server/playwright/index-source.mjs +16 -0
- package/dist/server/playwright/internal.d.ts +6 -6
- package/dist/server/playwright/internal.js +143 -91
- package/dist/server/playwright/internal.js.map +1 -1
- package/dist/server/playwright/webdriver.d.ts +1 -1
- package/dist/server/playwright/webdriver.js +5 -8
- package/dist/server/playwright/webdriver.js.map +1 -1
- package/dist/server/providers/browser.js +6 -4
- package/dist/server/providers/browser.js.map +1 -1
- package/dist/server/providers/hybrid.js +1 -1
- package/dist/server/providers/hybrid.js.map +1 -1
- package/dist/server/reporters/creevey.d.ts +7 -0
- package/dist/server/reporters/creevey.js +63 -0
- package/dist/server/reporters/creevey.js.map +1 -0
- package/dist/server/reporters/index.d.ts +2 -0
- package/dist/server/reporters/index.js +16 -0
- package/dist/server/reporters/index.js.map +1 -0
- package/dist/server/reporters/junit.d.ts +16 -0
- package/dist/server/reporters/junit.js +165 -0
- package/dist/server/reporters/junit.js.map +1 -0
- package/dist/server/reporters/teamcity.d.ts +7 -0
- package/dist/server/reporters/teamcity.js +60 -0
- package/dist/server/reporters/teamcity.js.map +1 -0
- package/dist/server/selenium/internal.d.ts +3 -4
- package/dist/server/selenium/internal.js +127 -108
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +8 -6
- package/dist/server/selenium/selenoid.js.map +1 -1
- package/dist/server/selenium/webdriver.d.ts +1 -1
- package/dist/server/selenium/webdriver.js +5 -9
- package/dist/server/selenium/webdriver.js.map +1 -1
- package/dist/server/telemetry.js +2 -2
- package/dist/server/testsFiles/parser.js +45 -5
- package/dist/server/testsFiles/parser.js.map +1 -1
- package/dist/server/utils.d.ts +19 -1
- package/dist/server/utils.js +87 -8
- package/dist/server/utils.js.map +1 -1
- package/dist/server/webdriver.d.ts +5 -4
- package/dist/server/webdriver.js +23 -10
- package/dist/server/webdriver.js.map +1 -1
- package/dist/server/worker/chai-image.d.ts +1 -2
- package/dist/server/worker/chai-image.js +4 -3
- package/dist/server/worker/chai-image.js.map +1 -1
- package/dist/server/worker/context.d.ts +3 -0
- package/dist/server/worker/context.js +15 -0
- package/dist/server/worker/context.js.map +1 -0
- package/dist/server/worker/match-image.d.ts +4 -4
- package/dist/server/worker/match-image.js +7 -4
- package/dist/server/worker/match-image.js.map +1 -1
- package/dist/server/worker/start.js +47 -73
- package/dist/server/worker/start.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/types.d.ts +46 -10
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/docs/cli.md +12 -0
- package/docs/config.md +179 -165
- package/docs/storybook.md +60 -0
- package/docs/tests.md +50 -45
- package/package.json +64 -63
- package/src/client/addon/components/Panel.tsx +2 -2
- package/src/client/addon/withCreevey.ts +10 -2
- package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
- package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
- package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
- package/src/client/shared/components/ResultsPage.tsx +31 -8
- package/src/client/shared/creeveyClientApi.ts +9 -1
- package/src/client/shared/helpers.ts +4 -24
- package/src/client/web/CreeveyApp.tsx +27 -8
- package/src/client/web/CreeveyContext.tsx +9 -0
- package/src/client/web/CreeveyLoader.tsx +1 -1
- package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
- package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
- package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
- package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
- package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
- package/src/client/web/KeyboardEventsContext.tsx +61 -73
- package/src/client/web/themes.ts +24 -0
- package/src/creevey.ts +16 -10
- package/src/server/config.ts +30 -7
- package/src/server/connection.ts +26 -0
- package/src/server/docker.ts +63 -34
- package/src/server/index.ts +72 -14
- package/src/server/logger.ts +6 -2
- package/src/server/master/api.ts +1 -1
- package/src/server/master/pool.ts +23 -59
- package/src/server/master/queue.ts +77 -0
- package/src/server/master/runner.ts +96 -10
- package/src/server/master/server.ts +1 -1
- package/src/server/master/start.ts +16 -11
- package/src/server/playwright/docker-file.ts +18 -6
- package/src/server/playwright/docker.ts +16 -3
- package/src/server/playwright/index-source.mjs +16 -0
- package/src/server/playwright/internal.ts +182 -111
- package/src/server/playwright/webdriver.ts +6 -9
- package/src/server/providers/browser.ts +6 -4
- package/src/server/providers/hybrid.ts +1 -1
- package/src/server/reporters/creevey.ts +71 -0
- package/src/server/reporters/index.ts +11 -0
- package/src/server/reporters/junit.ts +205 -0
- package/src/server/reporters/teamcity.ts +74 -0
- package/src/server/selenium/internal.ts +131 -116
- package/src/server/selenium/selenoid.ts +8 -6
- package/src/server/selenium/webdriver.ts +6 -10
- package/src/server/telemetry.ts +2 -2
- package/src/server/testsFiles/parser.ts +52 -4
- package/src/server/utils.ts +97 -9
- package/src/server/webdriver.ts +24 -16
- package/src/server/worker/chai-image.ts +4 -4
- package/src/server/worker/context.ts +14 -0
- package/src/server/worker/match-image.ts +12 -8
- package/src/server/worker/start.ts +51 -86
- package/src/shared/index.ts +1 -1
- package/src/types.ts +50 -11
- package/types/global.d.ts +1 -0
- package/.yarnrc.yml +0 -1
- package/chromatic.config.json +0 -5
- package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
- package/dist/server/reporter.d.ts +0 -26
- package/dist/server/reporter.js +0 -108
- package/dist/server/reporter.js.map +0 -1
- 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
|
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, '&')
|
187
|
+
.replace(/"/g, '"')
|
188
|
+
.replace(/'/g, ''')
|
189
|
+
.replace(/</g, '<')
|
190
|
+
.replace(/>/g, '>'),
|
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
|
+
}
|