@lokalise/playwright-reporters 1.5.0 → 1.7.0
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 +36 -0
- package/dist/analytics/transform.d.ts +2 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +13 -4
- package/dist/permissions/elastic.d.ts +13 -0
- package/dist/permissions/elastic.js +28 -0
- package/dist/permissions/reporter.d.ts +25 -0
- package/dist/permissions/reporter.js +58 -0
- package/dist/stepDuration/reporter.d.ts +15 -0
- package/dist/stepDuration/reporter.js +56 -0
- package/dist/testSimilarity/reporter.d.ts +1 -0
- package/dist/testSimilarity/reporter.js +72 -0
- package/dist/timeline/reporter.d.ts +47 -0
- package/dist/timeline/reporter.js +97 -0
- package/package.json +33 -16
package/README.md
CHANGED
|
@@ -42,6 +42,42 @@ export default defineConfig({
|
|
|
42
42
|
});
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Steps Duration Reporter
|
|
46
|
+
|
|
47
|
+
This reporter aggregates the duration of each discrete test step and reports their total duration at the end of the test run. The reporter uses the custom [Playwright Reporter](https://playwright.dev/docs/test-reporters#custom-reporters) implementation to collect the step durations. Follow the steps in the [official Playwright documentation](https://playwright.dev/docs/test-reporters#multiple-reporters) to add the custom reporter to your test suite.
|
|
48
|
+
|
|
49
|
+
### Usage
|
|
50
|
+
|
|
51
|
+
Once the reporter is enabled, it will output a `JSON` file `./reporters/results/steps.json` containing the duration of each step in the test suite.
|
|
52
|
+
|
|
53
|
+
## Test Similarity Reporter
|
|
54
|
+
|
|
55
|
+
This reporter calculates the similarity between tests based on the test steps. The reporter does not use the custom [Playwright Reporter](https://playwright.dev/docs/test-reporters#custom-reporters) implementation. Instead, it takes the simplified reporter from the undocumented `timeline` reporter.
|
|
56
|
+
|
|
57
|
+
### Usage
|
|
58
|
+
|
|
59
|
+
1. Run the test suite with the `timeline` reporter enabled. This will generate a `JSON` file `./reporters/results/timeline.json`
|
|
60
|
+
1. Run the similarity reporter with the following command: `npx ts-node reporters/testSimilarity.ts`
|
|
61
|
+
1. After analysing the results, the reporter will output a `JSON` file `./reporters/results/distance.json` containing the similarity between tests based on the test steps.
|
|
62
|
+
|
|
63
|
+
### Output
|
|
64
|
+
|
|
65
|
+
The report has the following format:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
[
|
|
69
|
+
{
|
|
70
|
+
"title": "Title of the test being compared",
|
|
71
|
+
"relatedTests": [
|
|
72
|
+
["Title of the first related test", 0],
|
|
73
|
+
["Title of the second related test", 50]
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `relatedTests` array contains the title of the related test and the distance score. The similarity percentage is calculated based on the [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) between the test steps. The lower the distance, the higher the similarity between the tests.
|
|
80
|
+
|
|
45
81
|
## Husky
|
|
46
82
|
|
|
47
83
|
By default, pre-commit hook will run `npm run lint:fix`. Feel free to remove that if it's undesirable or add your own
|
|
@@ -15,7 +15,7 @@ export declare const formFailedTestData: (test: TestCase, result: TestResult, te
|
|
|
15
15
|
name: string;
|
|
16
16
|
team: string | null;
|
|
17
17
|
testRunId: string;
|
|
18
|
-
status:
|
|
18
|
+
status: "failed" | "interrupted" | "passed" | "timedOut" | "skipped";
|
|
19
19
|
hooksDuration: number;
|
|
20
20
|
beforeHookDuration: number;
|
|
21
21
|
afterHookDuration: number;
|
|
@@ -32,7 +32,7 @@ export declare const formFlakyTestData: (test: TestCase, result: TestResult, tes
|
|
|
32
32
|
name: string;
|
|
33
33
|
team: string | null;
|
|
34
34
|
testRunId: string;
|
|
35
|
-
status:
|
|
35
|
+
status: "failed" | "interrupted" | "passed" | "timedOut" | "skipped";
|
|
36
36
|
hooksDuration: number;
|
|
37
37
|
beforeHookDuration: number;
|
|
38
38
|
afterHookDuration: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import AnalyticsReporter, { defineAnalyticsReporterConfig } from './analytics/reporter';
|
|
2
|
+
import PermissionsReporter, { definePermissionsReporterConfig } from './permissions/reporter';
|
|
2
3
|
import RetryReporter, { defineRetryReporterConfig } from './retry/reporter';
|
|
3
|
-
|
|
4
|
+
import StepDurationReporter, { defineStepDurationReporterConfig } from './stepDuration/reporter';
|
|
5
|
+
import TimelineReporter, { defineTimelineReporterConfig } from './timeline/reporter';
|
|
6
|
+
export { AnalyticsReporter, defineAnalyticsReporterConfig, RetryReporter, defineRetryReporterConfig, TimelineReporter, defineTimelineReporterConfig, StepDurationReporter, defineStepDurationReporterConfig, PermissionsReporter, definePermissionsReporterConfig };
|
package/dist/index.js
CHANGED
|
@@ -23,10 +23,19 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.defineRetryReporterConfig = exports.RetryReporter = exports.defineAnalyticsReporterConfig = exports.AnalyticsReporter = void 0;
|
|
26
|
+
exports.definePermissionsReporterConfig = exports.PermissionsReporter = exports.defineStepDurationReporterConfig = exports.StepDurationReporter = exports.defineTimelineReporterConfig = exports.TimelineReporter = exports.defineRetryReporterConfig = exports.RetryReporter = exports.defineAnalyticsReporterConfig = exports.AnalyticsReporter = void 0;
|
|
27
27
|
const reporter_1 = __importStar(require("./analytics/reporter"));
|
|
28
28
|
exports.AnalyticsReporter = reporter_1.default;
|
|
29
29
|
Object.defineProperty(exports, "defineAnalyticsReporterConfig", { enumerable: true, get: function () { return reporter_1.defineAnalyticsReporterConfig; } });
|
|
30
|
-
const reporter_2 = __importStar(require("./
|
|
31
|
-
exports.
|
|
32
|
-
Object.defineProperty(exports, "
|
|
30
|
+
const reporter_2 = __importStar(require("./permissions/reporter"));
|
|
31
|
+
exports.PermissionsReporter = reporter_2.default;
|
|
32
|
+
Object.defineProperty(exports, "definePermissionsReporterConfig", { enumerable: true, get: function () { return reporter_2.definePermissionsReporterConfig; } });
|
|
33
|
+
const reporter_3 = __importStar(require("./retry/reporter"));
|
|
34
|
+
exports.RetryReporter = reporter_3.default;
|
|
35
|
+
Object.defineProperty(exports, "defineRetryReporterConfig", { enumerable: true, get: function () { return reporter_3.defineRetryReporterConfig; } });
|
|
36
|
+
const reporter_4 = __importStar(require("./stepDuration/reporter"));
|
|
37
|
+
exports.StepDurationReporter = reporter_4.default;
|
|
38
|
+
Object.defineProperty(exports, "defineStepDurationReporterConfig", { enumerable: true, get: function () { return reporter_4.defineStepDurationReporterConfig; } });
|
|
39
|
+
const reporter_5 = __importStar(require("./timeline/reporter"));
|
|
40
|
+
exports.TimelineReporter = reporter_5.default;
|
|
41
|
+
Object.defineProperty(exports, "defineTimelineReporterConfig", { enumerable: true, get: function () { return reporter_5.defineTimelineReporterConfig; } });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PermissionsReportData } from "./reporter";
|
|
2
|
+
export type ElasticOptions = {
|
|
3
|
+
permissionsIndex: string;
|
|
4
|
+
elasticUrl: string;
|
|
5
|
+
elasticToken: string;
|
|
6
|
+
};
|
|
7
|
+
export declare class Elastic {
|
|
8
|
+
private readonly options;
|
|
9
|
+
private request;
|
|
10
|
+
constructor(options: ElasticOptions);
|
|
11
|
+
setRequestContext(): Promise<void>;
|
|
12
|
+
savePermissionsData(testData: PermissionsReportData): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Elastic = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
class Elastic {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
async setRequestContext() {
|
|
10
|
+
this.request = await test_1.request.newContext({
|
|
11
|
+
baseURL: this.options.elasticUrl,
|
|
12
|
+
ignoreHTTPSErrors: true,
|
|
13
|
+
...(this.options.elasticToken && {
|
|
14
|
+
extraHTTPHeaders: { Authorization: `ApiKey ${this.options.elasticToken}` },
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async savePermissionsData(testData) {
|
|
19
|
+
try {
|
|
20
|
+
await this.request.post(`/${this.options.permissionsIndex}/_doc`, { data: testData });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error("Failed to save permissions data");
|
|
24
|
+
console.error(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.Elastic = Elastic;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TestCase, TestResult, Reporter, TestStep } from '@playwright/test/reporter';
|
|
2
|
+
import { type ElasticOptions } from './elastic';
|
|
3
|
+
type ReporterOptions = {
|
|
4
|
+
annotationName: string;
|
|
5
|
+
stepName: string;
|
|
6
|
+
debug: boolean;
|
|
7
|
+
} & ElasticOptions;
|
|
8
|
+
export type PermissionsReportData = {
|
|
9
|
+
title: string;
|
|
10
|
+
permissions: Record<string, boolean>;
|
|
11
|
+
failedStep: string;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
};
|
|
14
|
+
export default class PermissionsReporter implements Reporter {
|
|
15
|
+
private readonly options;
|
|
16
|
+
private readonly elastic;
|
|
17
|
+
private readonly debugLogging;
|
|
18
|
+
private failedTestData;
|
|
19
|
+
constructor(options: ReporterOptions);
|
|
20
|
+
onBegin(): Promise<void>;
|
|
21
|
+
onTestEnd(test: TestCase): Promise<void>;
|
|
22
|
+
onStepEnd(test: TestCase, _result: TestResult, step: TestStep): void;
|
|
23
|
+
}
|
|
24
|
+
export declare const definePermissionsReporterConfig: (options: ConstructorParameters<typeof PermissionsReporter>[0]) => ReporterOptions;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.definePermissionsReporterConfig = void 0;
|
|
4
|
+
const elastic_1 = require("./elastic");
|
|
5
|
+
// eslint-disable-next-line import/no-default-export
|
|
6
|
+
class PermissionsReporter {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.elastic = new elastic_1.Elastic(options);
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.failedTestData = {};
|
|
11
|
+
this.debugLogging = options.debug ? console.info : () => { };
|
|
12
|
+
}
|
|
13
|
+
async onBegin() {
|
|
14
|
+
await this.elastic.setRequestContext();
|
|
15
|
+
}
|
|
16
|
+
async onTestEnd(test) {
|
|
17
|
+
// Omit the results if the test is passing
|
|
18
|
+
if (test.outcome() === 'expected') {
|
|
19
|
+
this.debugLogging('Not reporting test - test passed');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Omit the results if the test does not contain the annotation
|
|
23
|
+
if (!test.annotations.find((annotation) => annotation.type === this.options.annotationName)) {
|
|
24
|
+
this.debugLogging('Not reporting test - no permission annotation');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Test Title
|
|
28
|
+
const testTitle = test.title;
|
|
29
|
+
// Permissions as object
|
|
30
|
+
const testPermissions = test.annotations.find((annotation) => annotation.type === this.options.annotationName);
|
|
31
|
+
const permissionContents = testPermissions?.description ? JSON.parse(testPermissions.description) : {};
|
|
32
|
+
// Which step failed
|
|
33
|
+
const failedStep = this.failedTestData[test.id];
|
|
34
|
+
// Send payload body
|
|
35
|
+
const reportData = {
|
|
36
|
+
title: testTitle,
|
|
37
|
+
permissions: permissionContents,
|
|
38
|
+
failedStep,
|
|
39
|
+
timestamp: Date.now()
|
|
40
|
+
};
|
|
41
|
+
await this.elastic.savePermissionsData(reportData);
|
|
42
|
+
}
|
|
43
|
+
onStepEnd(test, _result, step) {
|
|
44
|
+
if (!step.title.includes(this.options.stepName)) {
|
|
45
|
+
this.debugLogging('Not reporting step - no step annotation', step.title);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!step.error) {
|
|
49
|
+
this.debugLogging("Not reporting step - no error");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.debugLogging('Captured step name with error:', step.title);
|
|
53
|
+
this.failedTestData[test.id] = step.title;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.default = PermissionsReporter;
|
|
57
|
+
const definePermissionsReporterConfig = (options) => options;
|
|
58
|
+
exports.definePermissionsReporterConfig = definePermissionsReporterConfig;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Reporter, TestCase, TestResult, TestStep } from '@playwright/test/reporter';
|
|
2
|
+
interface ReporterOptions {
|
|
3
|
+
writeToFile: boolean;
|
|
4
|
+
filePath?: string;
|
|
5
|
+
}
|
|
6
|
+
declare class StepDurationReporter implements Reporter {
|
|
7
|
+
private stepStorage;
|
|
8
|
+
private readonly ignoredSteps;
|
|
9
|
+
private readonly options;
|
|
10
|
+
constructor(options: ReporterOptions);
|
|
11
|
+
onStepEnd(_test: TestCase, _result: TestResult, step: TestStep): void;
|
|
12
|
+
onEnd(): void;
|
|
13
|
+
}
|
|
14
|
+
export default StepDurationReporter;
|
|
15
|
+
export declare const defineStepDurationReporterConfig: (options: ConstructorParameters<typeof StepDurationReporter>[0]) => ReporterOptions;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.defineStepDurationReporterConfig = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
class StepDurationReporter {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.stepStorage = {};
|
|
12
|
+
this.ignoredSteps = [
|
|
13
|
+
'expect.',
|
|
14
|
+
/attach.*(response|request|trace|login)/i,
|
|
15
|
+
/hook(s?)$/i,
|
|
16
|
+
/tracing\.(start|stop)/,
|
|
17
|
+
/apiResponse\.(json|text)/,
|
|
18
|
+
/sending .* to/,
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
onStepEnd(_test, _result, step) {
|
|
22
|
+
const stepName = step.title;
|
|
23
|
+
const shouldIgnoreStep = this.ignoredSteps.some((ignoredStep) => {
|
|
24
|
+
if (typeof ignoredStep === 'string') {
|
|
25
|
+
return stepName.toLowerCase().includes(ignoredStep);
|
|
26
|
+
}
|
|
27
|
+
return ignoredStep.test(stepName.toLowerCase());
|
|
28
|
+
});
|
|
29
|
+
if (shouldIgnoreStep) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!(stepName in this.stepStorage)) {
|
|
33
|
+
this.stepStorage[stepName] = { totalTime: 0, minTime: 0, maxTime: 0, count: 0 };
|
|
34
|
+
}
|
|
35
|
+
const stepDuration = Date.now() - step.startTime.getTime();
|
|
36
|
+
this.stepStorage[stepName].totalTime += stepDuration;
|
|
37
|
+
this.stepStorage[stepName].count += 1;
|
|
38
|
+
this.stepStorage[stepName].minTime = Math.min(this.stepStorage[stepName].minTime === 0 ? stepDuration : this.stepStorage[stepName].minTime, stepDuration);
|
|
39
|
+
this.stepStorage[stepName].maxTime = Math.max(this.stepStorage[stepName].maxTime, stepDuration);
|
|
40
|
+
}
|
|
41
|
+
onEnd() {
|
|
42
|
+
const sortedSteps = Object.fromEntries(Object.entries(this.stepStorage).sort((a, b) => b[1].totalTime - a[1].totalTime));
|
|
43
|
+
const reportString = JSON.stringify(sortedSteps, null, 2);
|
|
44
|
+
if (this.options.writeToFile) {
|
|
45
|
+
const filePath = this.options.filePath ?? './reporters/results/steps.json';
|
|
46
|
+
node_fs_1.default.writeFileSync(filePath, reportString);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(reportString);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// eslint-disable-next-line import/no-default-export
|
|
54
|
+
exports.default = StepDurationReporter;
|
|
55
|
+
const defineStepDurationReporterConfig = (options) => options;
|
|
56
|
+
exports.defineStepDurationReporterConfig = defineStepDurationReporterConfig;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const fastest_levenshtein_1 = require("fastest-levenshtein");
|
|
9
|
+
const replaceTitleParameters = (title) => {
|
|
10
|
+
const numberInUrl = /\/(\d+)(?:\/|$)/gm;
|
|
11
|
+
const projectId = /[a-z0-9]+\.[a-z0-9]+/gm;
|
|
12
|
+
return title.replace(numberInUrl, '/:id/').replace(projectId, ':projectId');
|
|
13
|
+
};
|
|
14
|
+
const updateLog = (message) => {
|
|
15
|
+
process.stdout.clearLine(0);
|
|
16
|
+
process.stdout.cursorTo(0);
|
|
17
|
+
process.stdout.write(message);
|
|
18
|
+
};
|
|
19
|
+
const stepsToIgnore = [
|
|
20
|
+
'Create Browser',
|
|
21
|
+
'Sending post to /signup/test-user',
|
|
22
|
+
'open and match condition at Application',
|
|
23
|
+
];
|
|
24
|
+
const pathVariable = process.env.REPORTS_PATH;
|
|
25
|
+
const timelinePath = node_path_1.default.resolve(node_path_1.default.join(pathVariable, './results/timeline.json'));
|
|
26
|
+
const timeline = JSON.parse(node_fs_1.default.readFileSync(timelinePath, 'utf-8'));
|
|
27
|
+
const timelineWithoutEmptySteps = timeline.tests.filter((test) => {
|
|
28
|
+
return test.steps.length > 0;
|
|
29
|
+
});
|
|
30
|
+
const simplifiedTimeline = timelineWithoutEmptySteps.map((test) => {
|
|
31
|
+
return {
|
|
32
|
+
title: test.title,
|
|
33
|
+
steps: test.steps
|
|
34
|
+
.filter((step) => step.category === 'test.step')
|
|
35
|
+
.filter((step) => !stepsToIgnore.includes(step.title))
|
|
36
|
+
.map((step) => replaceTitleParameters(step.title)),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
const timelineAsText = simplifiedTimeline.map((test) => {
|
|
40
|
+
return {
|
|
41
|
+
...test,
|
|
42
|
+
stepsAsText: test.steps.join(' '),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
const distances = {};
|
|
46
|
+
const iterationCount = (timelineAsText.length * (timelineAsText.length - 1)) / 2;
|
|
47
|
+
let currentIteration = 0;
|
|
48
|
+
for (let testIndex = 0; testIndex < timelineAsText.length; testIndex++) {
|
|
49
|
+
updateLog(`Processing iteration ${currentIteration} of ${iterationCount} (${Math.floor((currentIteration / iterationCount) * 100)}%)`);
|
|
50
|
+
currentIteration += timelineAsText.length - testIndex;
|
|
51
|
+
const keyName = timelineAsText[testIndex].title;
|
|
52
|
+
if (!(keyName in distances)) {
|
|
53
|
+
distances[keyName] = [];
|
|
54
|
+
}
|
|
55
|
+
for (let compareIndex = testIndex + 1; compareIndex < timelineAsText.length; compareIndex++) {
|
|
56
|
+
const currentTest = timelineAsText[testIndex];
|
|
57
|
+
const compareTest = timelineAsText[compareIndex];
|
|
58
|
+
const distanceValue = (0, fastest_levenshtein_1.distance)(currentTest.stepsAsText, compareTest.stepsAsText);
|
|
59
|
+
distances[keyName].push([compareTest.title, distanceValue]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const sortedDistances = Object.entries(distances)
|
|
63
|
+
.map(([key, value]) => {
|
|
64
|
+
return {
|
|
65
|
+
title: key,
|
|
66
|
+
relatedTests: value.sort((a, b) => a[1] - b[1]),
|
|
67
|
+
};
|
|
68
|
+
})
|
|
69
|
+
.sort((a, b) => a.relatedTests[0][1] - b.relatedTests[0][1]);
|
|
70
|
+
const resultsPath = node_path_1.default.join(pathVariable, './results/distances.json');
|
|
71
|
+
node_fs_1.default.writeFileSync(resultsPath, JSON.stringify(sortedDistances, null, 2));
|
|
72
|
+
console.log(`\nDone. Check the results in ${resultsPath}`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FullResult, Reporter, TestCase, TestResult, TestStatus, TestStep } from '@playwright/test/reporter';
|
|
2
|
+
export interface TestRunData {
|
|
3
|
+
startTime: number;
|
|
4
|
+
endTime: number;
|
|
5
|
+
duration: number;
|
|
6
|
+
tests: TestData[];
|
|
7
|
+
}
|
|
8
|
+
export interface TestData {
|
|
9
|
+
title: string;
|
|
10
|
+
startTime: number;
|
|
11
|
+
endTime: number;
|
|
12
|
+
status: TestStatus | 'running';
|
|
13
|
+
parallelIndex: number;
|
|
14
|
+
id: string;
|
|
15
|
+
retry: number;
|
|
16
|
+
steps: TestStepData[];
|
|
17
|
+
}
|
|
18
|
+
type StepCategory = 'hook' | 'expect' | 'pw:api' | 'test.step' | 'fixture' | 'attach';
|
|
19
|
+
export interface TestStepData {
|
|
20
|
+
title: string;
|
|
21
|
+
id: string;
|
|
22
|
+
category: StepCategory;
|
|
23
|
+
isWithinHook?: boolean;
|
|
24
|
+
startTime: number;
|
|
25
|
+
endTime: number;
|
|
26
|
+
depth: number;
|
|
27
|
+
hasError: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface ReporterOptions {
|
|
30
|
+
writeToFile: boolean;
|
|
31
|
+
filePath?: string;
|
|
32
|
+
}
|
|
33
|
+
declare class TimelineReporter implements Reporter {
|
|
34
|
+
private readonly testData;
|
|
35
|
+
private readonly options;
|
|
36
|
+
constructor(options?: ReporterOptions);
|
|
37
|
+
private findMatchingTest;
|
|
38
|
+
onBegin(): void;
|
|
39
|
+
onTestBegin(test: TestCase, result: TestResult): void;
|
|
40
|
+
onTestEnd(test: TestCase, result: TestResult): void;
|
|
41
|
+
onStepEnd(test: TestCase, result: TestResult, step: TestStep): void;
|
|
42
|
+
onEnd(result: FullResult): void;
|
|
43
|
+
private writeReportToFile;
|
|
44
|
+
getReport(): TestRunData;
|
|
45
|
+
}
|
|
46
|
+
export default TimelineReporter;
|
|
47
|
+
export declare const defineTimelineReporterConfig: (options: ConstructorParameters<typeof TimelineReporter>[0]) => ReporterOptions | undefined;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.defineTimelineReporterConfig = void 0;
|
|
7
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const getStepDepthFromStepPath = (step) => {
|
|
10
|
+
const stepPath = step.titlePath();
|
|
11
|
+
const stepDepth = stepPath.length - 1;
|
|
12
|
+
return stepDepth;
|
|
13
|
+
};
|
|
14
|
+
const isStepWithinHook = (step) => {
|
|
15
|
+
if (step === undefined) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (step.category === 'hook') {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return isStepWithinHook(step?.parent);
|
|
22
|
+
};
|
|
23
|
+
class TimelineReporter {
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.options = { writeToFile: true, ...options };
|
|
26
|
+
this.testData = {
|
|
27
|
+
startTime: 0,
|
|
28
|
+
endTime: 0,
|
|
29
|
+
duration: 0,
|
|
30
|
+
tests: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
findMatchingTest(test, result) {
|
|
34
|
+
const matchingTest = this.testData.tests.find((testData) => {
|
|
35
|
+
const matchesId = testData.id === test.id;
|
|
36
|
+
const matchesRetry = testData.retry === (result?.retry ?? 0);
|
|
37
|
+
return matchesId && matchesRetry;
|
|
38
|
+
});
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
40
|
+
return matchingTest;
|
|
41
|
+
}
|
|
42
|
+
onBegin() {
|
|
43
|
+
this.testData.startTime = Date.now();
|
|
44
|
+
}
|
|
45
|
+
onTestBegin(test, result) {
|
|
46
|
+
this.testData.tests.push({
|
|
47
|
+
title: test.title,
|
|
48
|
+
startTime: result.startTime.getTime(),
|
|
49
|
+
endTime: 0,
|
|
50
|
+
retry: result?.retry ?? 0,
|
|
51
|
+
status: 'running',
|
|
52
|
+
parallelIndex: result.parallelIndex,
|
|
53
|
+
id: test.id,
|
|
54
|
+
steps: [],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
onTestEnd(test, result) {
|
|
58
|
+
const finishedTest = this.findMatchingTest(test, result);
|
|
59
|
+
finishedTest.endTime = finishedTest.startTime + result.duration;
|
|
60
|
+
finishedTest.status = result.status;
|
|
61
|
+
}
|
|
62
|
+
onStepEnd(test, result, step) {
|
|
63
|
+
const matchingTest = this.findMatchingTest(test, result);
|
|
64
|
+
const startTime = step.startTime.getTime();
|
|
65
|
+
const endTime = startTime + step.duration;
|
|
66
|
+
matchingTest.steps.push({
|
|
67
|
+
category: step.category,
|
|
68
|
+
isWithinHook: isStepWithinHook(step),
|
|
69
|
+
title: step.title,
|
|
70
|
+
id: node_crypto_1.default.randomUUID(),
|
|
71
|
+
startTime,
|
|
72
|
+
endTime,
|
|
73
|
+
depth: getStepDepthFromStepPath(step),
|
|
74
|
+
hasError: step.error !== undefined,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
onEnd(result) {
|
|
78
|
+
this.testData.endTime = Date.now();
|
|
79
|
+
this.testData.duration = result.duration;
|
|
80
|
+
if (this?.options?.writeToFile === true) {
|
|
81
|
+
this.writeReportToFile();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
writeReportToFile() {
|
|
85
|
+
const filePath = this.options?.filePath ?? 'customResults.json';
|
|
86
|
+
const report = this.getReport();
|
|
87
|
+
const reportString = JSON.stringify(report, null, 2);
|
|
88
|
+
node_fs_1.default.writeFileSync(filePath, reportString);
|
|
89
|
+
}
|
|
90
|
+
getReport() {
|
|
91
|
+
return this.testData;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// eslint-disable-next-line import/no-default-export
|
|
95
|
+
exports.default = TimelineReporter;
|
|
96
|
+
const defineTimelineReporterConfig = (options) => options;
|
|
97
|
+
exports.defineTimelineReporterConfig = defineTimelineReporterConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lokalise/playwright-reporters",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"lint:eslint": "eslint --cache . --ext .js,.cjs,.ts",
|
|
6
6
|
"lint:ts": "tsc --noEmit",
|
|
@@ -29,35 +29,52 @@
|
|
|
29
29
|
"./analytics": {
|
|
30
30
|
"import": "./dist/analytics/reporter.js",
|
|
31
31
|
"require": "./dist/analytics/reporter.js"
|
|
32
|
+
},
|
|
33
|
+
"./timeline": {
|
|
34
|
+
"import": "./dist/timeline/reporter.js",
|
|
35
|
+
"require": "./dist/timeline/reporter.js"
|
|
36
|
+
},
|
|
37
|
+
"./stepDuration": {
|
|
38
|
+
"import": "./dist/stepDuration/reporter.js",
|
|
39
|
+
"require": "./dist/stepDuration/reporter.js"
|
|
40
|
+
},
|
|
41
|
+
"./testSimilarity": {
|
|
42
|
+
"import": "./dist/testSimilarity/reporter.js",
|
|
43
|
+
"require": "./dist/testSimilarity/reporter.js"
|
|
44
|
+
},
|
|
45
|
+
"./permissions": {
|
|
46
|
+
"import": "./dist/permissions/reporter.js",
|
|
47
|
+
"require": "./dist/permissions/reporter.js"
|
|
32
48
|
}
|
|
33
49
|
},
|
|
34
50
|
"publishConfig": {
|
|
35
51
|
"access": "public"
|
|
36
52
|
},
|
|
37
53
|
"devDependencies": {
|
|
38
|
-
"@commitlint/cli": "19.
|
|
39
|
-
"@commitlint/config-conventional": "19.
|
|
40
|
-
"@commitlint/prompt-cli": "19.1
|
|
41
|
-
"@lokalise/eslint-config-frontend": "^4.
|
|
42
|
-
"@lokalise/prettier-config": "^1.0.
|
|
54
|
+
"@commitlint/cli": "19.3.0",
|
|
55
|
+
"@commitlint/config-conventional": "19.2.2",
|
|
56
|
+
"@commitlint/prompt-cli": "19.3.1",
|
|
57
|
+
"@lokalise/eslint-config-frontend": "^4.6.0",
|
|
58
|
+
"@lokalise/prettier-config": "^1.0.1",
|
|
43
59
|
"@semantic-release/changelog": "6.0.3",
|
|
44
|
-
"@semantic-release/commit-analyzer": "
|
|
60
|
+
"@semantic-release/commit-analyzer": "13.0.0",
|
|
45
61
|
"@semantic-release/git": "10.0.1",
|
|
46
|
-
"@semantic-release/github": "10.0.
|
|
47
|
-
"@semantic-release/npm": "
|
|
48
|
-
"@semantic-release/release-notes-generator": "
|
|
49
|
-
"@types/lodash": "^4.17.
|
|
50
|
-
"@types/node": "^20.
|
|
62
|
+
"@semantic-release/github": "10.0.6",
|
|
63
|
+
"@semantic-release/npm": "12.0.1",
|
|
64
|
+
"@semantic-release/release-notes-generator": "13.0.0",
|
|
65
|
+
"@types/lodash": "^4.17.5",
|
|
66
|
+
"@types/node": "^20.14.2",
|
|
51
67
|
"eslint-config-prettier": "^9.1.0",
|
|
52
68
|
"eslint-plugin-prettier": "^5.1.3",
|
|
53
69
|
"husky": "9.0.11",
|
|
54
|
-
"prettier": "^3.2
|
|
55
|
-
"semantic-release": "23.
|
|
56
|
-
"typescript": "5.4.
|
|
70
|
+
"prettier": "^3.3.2",
|
|
71
|
+
"semantic-release": "23.1.1",
|
|
72
|
+
"typescript": "5.4.5"
|
|
57
73
|
},
|
|
58
74
|
"dependencies": {
|
|
59
75
|
"@faker-js/faker": "^8.4.1",
|
|
60
|
-
"@playwright/test": "^1.
|
|
76
|
+
"@playwright/test": "^1.44.1",
|
|
77
|
+
"fastest-levenshtein": "^1.0.16",
|
|
61
78
|
"lodash": "^4.17.21"
|
|
62
79
|
},
|
|
63
80
|
"prettier": "@lokalise/prettier-config"
|