@dionlarson/playwright-orchestrator-core 1.3.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.
Files changed (53) hide show
  1. package/LICENSE.md +190 -0
  2. package/README.md +279 -0
  3. package/dist/adapter.d.ts +20 -0
  4. package/dist/adapter.js +65 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/commands/create-report.d.ts +2 -0
  8. package/dist/commands/create-report.js +18 -0
  9. package/dist/commands/create.d.ts +2 -0
  10. package/dist/commands/create.js +26 -0
  11. package/dist/commands/error-handler.d.ts +1 -0
  12. package/dist/commands/error-handler.js +28 -0
  13. package/dist/commands/init.d.ts +2 -0
  14. package/dist/commands/init.js +14 -0
  15. package/dist/commands/program.d.ts +2 -0
  16. package/dist/commands/program.js +17 -0
  17. package/dist/commands/run.d.ts +2 -0
  18. package/dist/commands/run.js +18 -0
  19. package/dist/helpers/plugin.d.ts +13 -0
  20. package/dist/helpers/plugin.js +23 -0
  21. package/dist/helpers/reporter-tools.d.ts +2 -0
  22. package/dist/helpers/reporter-tools.js +21 -0
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.js +4 -0
  25. package/dist/playwright-tools/annotations.cjs +8 -0
  26. package/dist/playwright-tools/annotations.d.cts +3 -0
  27. package/dist/playwright-tools/run-builder.d.ts +15 -0
  28. package/dist/playwright-tools/run-builder.js +81 -0
  29. package/dist/playwright-tools/run-info-reporter.d.ts +4 -0
  30. package/dist/playwright-tools/run-info-reporter.js +7 -0
  31. package/dist/playwright-tools/test-ats-analyzer.d.ts +21 -0
  32. package/dist/playwright-tools/test-ats-analyzer.js +173 -0
  33. package/dist/playwright-tools/test-result-reporter.d.ts +11 -0
  34. package/dist/playwright-tools/test-result-reporter.js +63 -0
  35. package/dist/plugins-list.d.ts +1 -0
  36. package/dist/plugins-list.js +2 -0
  37. package/dist/reporters/gha-reporter.d.ts +2 -0
  38. package/dist/reporters/gha-reporter.js +42 -0
  39. package/dist/reporters/helpers.d.ts +7 -0
  40. package/dist/reporters/helpers.js +21 -0
  41. package/dist/reporters/reporter-factory.d.ts +4 -0
  42. package/dist/reporters/reporter-factory.js +14 -0
  43. package/dist/reporters/test-execution-reporter.d.ts +17 -0
  44. package/dist/reporters/test-execution-reporter.js +85 -0
  45. package/dist/test-runner.d.ts +18 -0
  46. package/dist/test-runner.js +111 -0
  47. package/dist/types/adapters.d.ts +39 -0
  48. package/dist/types/adapters.js +1 -0
  49. package/dist/types/reporter.d.ts +32 -0
  50. package/dist/types/reporter.js +1 -0
  51. package/dist/types/test-info.d.ts +42 -0
  52. package/dist/types/test-info.js +14 -0
  53. package/package.json +81 -0
@@ -0,0 +1,14 @@
1
+ import { loadPlugins } from '../helpers/plugin.js';
2
+ import { withErrorHandling } from './error-handler.js';
3
+ import { program } from './program.js';
4
+ export default async () => {
5
+ const command = program.command('init').description('Run initialize script for selected storage type.');
6
+ for await (const { factory, subCommand } of loadPlugins(command)) {
7
+ subCommand.action(withErrorHandling(async (options) => {
8
+ const adapter = await factory(options);
9
+ await adapter.initialize();
10
+ await adapter.dispose();
11
+ console.log('Storage initialized');
12
+ }));
13
+ }
14
+ };
@@ -0,0 +1,2 @@
1
+ import { Command } from '@commander-js/extra-typings';
2
+ export declare const program: Command<[], {}, {}>;
@@ -0,0 +1,17 @@
1
+ import { Command } from '@commander-js/extra-typings';
2
+ import init from './init.js';
3
+ import run from './run.js';
4
+ import create from './create.js';
5
+ import createReport from './create-report.js';
6
+ import { readFile } from 'node:fs/promises';
7
+ export const program = new Command();
8
+ const package_json = JSON.parse(await readFile('node_modules/@playwright-orchestrator/core/package.json', 'utf-8'));
9
+ program
10
+ .name('playwright-orchestrator')
11
+ .description('CLI to orchestrate Playwright tests')
12
+ .version(package_json.version);
13
+ await init();
14
+ await run();
15
+ await create();
16
+ await createReport();
17
+ program.parse();
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { loadPlugins } from '../helpers/plugin.js';
2
+ import { TestRunner } from '../test-runner.js';
3
+ import { withErrorHandling } from './error-handler.js';
4
+ import { program } from './program.js';
5
+ export default async () => {
6
+ const command = program.command('run').description('Start test run shard');
7
+ for await (const { factory, subCommand } of loadPlugins(command)) {
8
+ subCommand
9
+ .requiredOption('--run-id <string>', 'Run id generated by create command')
10
+ .option('-o, --output <string>', 'Output folder for blob reports. Existing content is deleted before writing the new report.', 'blob-reports')
11
+ .action(withErrorHandling(async (options) => {
12
+ const adapter = await factory(options);
13
+ const runner = new TestRunner(options, adapter);
14
+ await runner.runTests();
15
+ console.log('Run completed');
16
+ }));
17
+ }
18
+ };
@@ -0,0 +1,13 @@
1
+ import { Command } from '@commander-js/extra-typings';
2
+ import { STORAGES } from '../plugins-list.js';
3
+ import { Adapter } from '../adapter.js';
4
+ export type StorageType = (typeof STORAGES)[number];
5
+ export declare function loadPluginModule(storage: string): Promise<{
6
+ factory: (options: any) => Promise<Adapter>;
7
+ createOptions: (command: Command) => void;
8
+ description?: string;
9
+ } | undefined>;
10
+ export declare function loadPlugins(command: Command<any>): AsyncGenerator<{
11
+ subCommand: Command<[], {}, {}>;
12
+ factory: (options: any) => Promise<Adapter>;
13
+ }, void, unknown>;
@@ -0,0 +1,23 @@
1
+ import { STORAGES } from '../plugins-list.js';
2
+ export async function loadPluginModule(storage) {
3
+ try {
4
+ const a = await import(`@playwright-orchestrator/${storage}`);
5
+ return a;
6
+ }
7
+ catch (error) {
8
+ return;
9
+ }
10
+ }
11
+ export async function* loadPlugins(command) {
12
+ for (const storage of STORAGES) {
13
+ const plugin = await loadPluginModule(storage);
14
+ if (plugin) {
15
+ const subCommand = command.command(storage);
16
+ if (plugin.description) {
17
+ subCommand.description(plugin.description);
18
+ }
19
+ plugin.createOptions(subCommand);
20
+ yield { subCommand, factory: plugin.factory };
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,2 @@
1
+ import { TestRunInfo } from '../types/test-info.js';
2
+ export declare function loadRunInfo(args: string[]): Promise<TestRunInfo>;
@@ -0,0 +1,21 @@
1
+ import child_process from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const exec = promisify(child_process.exec);
4
+ export async function loadRunInfo(args) {
5
+ const { stdout, stderr } = await exec(buildCommand(args), {
6
+ env: { ...process.env, NO_COLOR: '1' },
7
+ });
8
+ if (stderr) {
9
+ throw new Error(stderr);
10
+ }
11
+ // Extract JSON from stdout - skip any non-JSON lines (e.g., dotenvx logs)
12
+ const jsonMatch = stdout.match(/(\{[\s\S]*\})\s*$/);
13
+ if (!jsonMatch) {
14
+ throw new Error(`Failed to parse test run info. Output was: ${stdout}`);
15
+ }
16
+ return JSON.parse(jsonMatch[1]);
17
+ }
18
+ function buildCommand(args) {
19
+ // last param wins, so we need to put our reporter at the end
20
+ return `npx playwright test --list ${args.join(' ')} --reporter "@playwright-orchestrator/core/tests-info-reporter"`;
21
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types/adapters.js';
2
+ export * from './types/reporter.js';
3
+ export * from './types/test-info.js';
4
+ export * from './adapter.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types/adapters.js';
2
+ export * from './types/reporter.js';
3
+ export * from './types/test-info.js';
4
+ export * from './adapter.js';
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ID_TYPE = void 0;
4
+ exports.id = id;
5
+ exports.ID_TYPE = '@playwright-orchestrator/id';
6
+ function id(value) {
7
+ return { type: exports.ID_TYPE, description: value };
8
+ }
@@ -0,0 +1,3 @@
1
+ import { TestDetailsAnnotation } from '@playwright/test';
2
+ export declare const ID_TYPE = "@playwright-orchestrator/id";
3
+ export declare function id(value: string): TestDetailsAnnotation;
@@ -0,0 +1,15 @@
1
+ import { FullConfig, Suite, TestCase } from '@playwright/test/reporter';
2
+ import { TestRunInfo } from '../types/test-info.js';
3
+ export declare class RunBuilder {
4
+ private readonly testRun;
5
+ private config;
6
+ parseEntry(entry: TestCase | Suite): this;
7
+ parseConfig(config: FullConfig): this;
8
+ build(): TestRunInfo;
9
+ private parseSuitesHelper;
10
+ private tryParseEntry;
11
+ private isSerialSuite;
12
+ private getSuiteTimeout;
13
+ private getAnnotations;
14
+ private getFileTests;
15
+ }
@@ -0,0 +1,81 @@
1
+ export class RunBuilder {
2
+ testRun = {};
3
+ config = undefined;
4
+ parseEntry(entry) {
5
+ this.parseSuitesHelper(entry);
6
+ return this;
7
+ }
8
+ parseConfig(config) {
9
+ this.config = {
10
+ workers: config.workers,
11
+ configFile: config.configFile,
12
+ projects: config.projects.map((project) => ({
13
+ name: project.name,
14
+ outputDir: project.outputDir,
15
+ })),
16
+ };
17
+ return this;
18
+ }
19
+ build() {
20
+ return structuredClone({
21
+ config: this.config,
22
+ testRun: this.testRun,
23
+ });
24
+ }
25
+ parseSuitesHelper(entry) {
26
+ if (this.tryParseEntry(entry)) {
27
+ return;
28
+ }
29
+ for (const item of entry.entries()) {
30
+ this.parseSuitesHelper(item);
31
+ }
32
+ }
33
+ tryParseEntry(entry) {
34
+ const [_, project, file] = entry.titlePath();
35
+ if (!file)
36
+ return false;
37
+ const fileTests = this.getFileTests(file);
38
+ const position = `${entry.location?.line}:${entry.location?.column}`;
39
+ // Use position:title as key to support parameterized tests at same location
40
+ const key = `${position}:${entry.title}`;
41
+ if (fileTests[key]) {
42
+ fileTests[key].projects.push(project);
43
+ return true;
44
+ }
45
+ // Check for serial mode using Playwright's internal _parallelMode property
46
+ const isSerial = entry.type !== 'test' && this.isSerialSuite(entry);
47
+ if (entry.type === 'test' || isSerial) {
48
+ if (!fileTests[key]) {
49
+ // For serial suites, sum up all test timeouts
50
+ const timeout = entry.type === 'test'
51
+ ? entry.timeout
52
+ : this.getSuiteTimeout(entry);
53
+ fileTests[key] = {
54
+ timeout,
55
+ projects: [project],
56
+ annotations: this.getAnnotations(entry),
57
+ title: entry.title,
58
+ };
59
+ }
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ isSerialSuite(suite) {
65
+ return suite._parallelMode === 'serial';
66
+ }
67
+ getSuiteTimeout(suite) {
68
+ return suite.allTests().reduce((sum, test) => sum + test.timeout, 0);
69
+ }
70
+ getAnnotations(entry) {
71
+ if (entry.type === 'test') {
72
+ return entry.annotations;
73
+ }
74
+ return entry.allTests()[0]?.annotations;
75
+ }
76
+ getFileTests(file) {
77
+ if (!this.testRun[file])
78
+ this.testRun[file] = {};
79
+ return this.testRun[file];
80
+ }
81
+ }
@@ -0,0 +1,4 @@
1
+ import { FullConfig, Reporter, Suite } from '@playwright/test/reporter';
2
+ export default class RunInfoReporter implements Reporter {
3
+ onBegin(config: FullConfig, suite: Suite): void;
4
+ }
@@ -0,0 +1,7 @@
1
+ import { RunBuilder } from './run-builder.js';
2
+ export default class RunInfoReporter {
3
+ onBegin(config, suite) {
4
+ const testRunInfo = new RunBuilder().parseConfig(config).parseEntry(suite).build();
5
+ console.log(JSON.stringify(testRunInfo, null, 2));
6
+ }
7
+ }
@@ -0,0 +1,21 @@
1
+ import { Suite, TestCase } from '@playwright/test/reporter';
2
+ export declare class TestASTAnalyzer {
3
+ private readonly program;
4
+ private readonly typeChecker;
5
+ private readonly sourceFile;
6
+ private constructor();
7
+ static create(file?: string): TestASTAnalyzer | undefined;
8
+ suiteIsSerial(suite: Suite): boolean;
9
+ getTimeout(entry: TestCase | Suite): number;
10
+ private checkFileSerialStatement;
11
+ private isSerialStatement;
12
+ private getFunctionBodyStatements;
13
+ private extractCallStatementsTestFunc;
14
+ private extractTestCallStatements;
15
+ private isTestStatement;
16
+ private isSlowStatement;
17
+ private getTimeoutFromStatement;
18
+ private getNumberIdentifierValue;
19
+ private findTestTimeout;
20
+ private findNodeAtLocation;
21
+ }
@@ -0,0 +1,173 @@
1
+ import ts from 'typescript';
2
+ const FILTER_PROPERTIES = ['setTimeout', 'slow', 'configure'];
3
+ export class TestASTAnalyzer {
4
+ program;
5
+ typeChecker;
6
+ sourceFile;
7
+ constructor(file) {
8
+ this.program = ts.createProgram([file], {
9
+ allowJs: true,
10
+ noResolve: true, // Don't resolve imports - much faster
11
+ });
12
+ this.typeChecker = this.program.getTypeChecker();
13
+ this.sourceFile = this.program.getSourceFile(file);
14
+ }
15
+ static create(file) {
16
+ if (!file)
17
+ return;
18
+ return new TestASTAnalyzer(file);
19
+ }
20
+ suiteIsSerial(suite) {
21
+ if (!suite.location || (suite.location.line === 0 && suite.location.column == 0)) {
22
+ return this.checkFileSerialStatement();
23
+ }
24
+ const suiteNode = this.findNodeAtLocation(suite.location);
25
+ if (!suiteNode ||
26
+ !ts.isPropertyAccessExpression(suiteNode) ||
27
+ !ts.isIdentifier(suiteNode.expression) ||
28
+ !ts.isCallExpression(suiteNode.parent))
29
+ return false;
30
+ const testNodeText = suiteNode.expression.text;
31
+ var testStatements = this.extractTestCallStatements(this.extractCallStatementsTestFunc(suiteNode.parent), testNodeText);
32
+ // last statement wins
33
+ for (let i = testStatements.length - 1; i >= 0; i--) {
34
+ if (this.isSerialStatement(testStatements[i]))
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+ getTimeout(entry) {
40
+ if (entry.type === 'test')
41
+ return this.findTestTimeout(entry);
42
+ return entry.entries().reduce((timeout, entry) => this.getTimeout(entry) + timeout, 0);
43
+ }
44
+ checkFileSerialStatement() {
45
+ return this.sourceFile.statements
46
+ .filter((statement) => ts.isExpressionStatement(statement))
47
+ .map((statement) => statement.expression)
48
+ .some((statement) => this.isSerialStatement(statement));
49
+ }
50
+ isSerialStatement(statement) {
51
+ const expression = statement.expression;
52
+ if (!ts.isPropertyAccessExpression(expression) ||
53
+ expression.name.text !== 'configure' ||
54
+ !ts.isPropertyAccessExpression(expression.expression) ||
55
+ expression.expression.name.text !== 'describe' ||
56
+ statement.arguments.length === 0)
57
+ return false;
58
+ const arg = statement.arguments[0];
59
+ return (ts.isObjectLiteralExpression(arg) &&
60
+ arg.properties.some((prop) => {
61
+ return (prop.name?.getText() === 'mode' &&
62
+ ts.isPropertyAssignment(prop) &&
63
+ ts.isStringLiteral(prop.initializer) &&
64
+ prop.initializer.text === 'serial');
65
+ }));
66
+ }
67
+ getFunctionBodyStatements(node) {
68
+ if (ts.isFunctionLike(node)) {
69
+ const body = node.body;
70
+ // Handle block body (FunctionDeclaration, FunctionExpression, MethodDeclaration)
71
+ if (ts.isBlock(body)) {
72
+ return [...body.statements];
73
+ }
74
+ // Handle expression body (ArrowFunction)
75
+ if (ts.isExpression(body)) {
76
+ return [ts.factory.createExpressionStatement(body)];
77
+ }
78
+ }
79
+ // Handle function expressions in variable declarations
80
+ if (ts.isVariableDeclaration(node) && node.initializer) {
81
+ if (ts.isFunctionExpression(node.initializer) || ts.isArrowFunction(node.initializer)) {
82
+ return this.getFunctionBodyStatements(node.initializer);
83
+ }
84
+ }
85
+ return [];
86
+ }
87
+ extractCallStatementsTestFunc(node) {
88
+ return this.getFunctionBodyStatements(node.arguments.find((arg) => ts.isFunctionLike(arg)));
89
+ }
90
+ extractTestCallStatements(nodes, testNodeText) {
91
+ return nodes
92
+ .filter((statement) => ts.isExpressionStatement(statement) && this.isTestStatement(statement, testNodeText))
93
+ .map((statement) => statement.expression);
94
+ }
95
+ isTestStatement(statement, testNodeText) {
96
+ if (!ts.isCallExpression(statement.expression))
97
+ return false;
98
+ let node = statement.expression.getChildAt(0);
99
+ if (!ts.isPropertyAccessExpression(node) || !FILTER_PROPERTIES.includes(node.name.text))
100
+ return false;
101
+ while (!ts.isIdentifier(node) && node.getChildCount() > 0)
102
+ node = node.getChildAt(0);
103
+ return node.getText() === testNodeText;
104
+ }
105
+ isSlowStatement(statement) {
106
+ const expression = statement.expression;
107
+ const functionName = expression.getChildAt(expression.getChildCount() - 1);
108
+ return functionName.getText() === 'slow';
109
+ }
110
+ getTimeoutFromStatement(statement) {
111
+ const expression = statement.expression;
112
+ if (!ts.isCallExpression(statement) ||
113
+ statement.arguments.length === 0 ||
114
+ !ts.isPropertyAccessExpression(expression) ||
115
+ expression.name.text !== 'setTimeout')
116
+ return 0;
117
+ return this.getNumberIdentifierValue(statement.arguments[0]);
118
+ }
119
+ getNumberIdentifierValue(node) {
120
+ if (ts.isNumericLiteral(node))
121
+ return parseInt(node.text, 10);
122
+ if (ts.isIdentifier(node)) {
123
+ const symbol = this.typeChecker.getSymbolAtLocation(node);
124
+ if (!symbol)
125
+ return 0;
126
+ const declarations = symbol.declarations;
127
+ if (!declarations || declarations.length === 0)
128
+ return 0;
129
+ const declaration = declarations[0];
130
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
131
+ if (ts.isNumericLiteral(declaration.initializer)) {
132
+ return parseInt(declaration.initializer.text, 10);
133
+ }
134
+ }
135
+ }
136
+ return 0;
137
+ }
138
+ findTestTimeout(test) {
139
+ const { location, timeout } = test;
140
+ const testNode = this.findNodeAtLocation(location);
141
+ if (!testNode || !ts.isIdentifier(testNode))
142
+ return timeout;
143
+ const testNodeText = testNode.text;
144
+ if (!ts.isCallExpression(testNode.parent))
145
+ return timeout;
146
+ var testStatements = this.extractTestCallStatements(this.extractCallStatementsTestFunc(testNode.parent), testNodeText);
147
+ // last statement wins
148
+ for (let i = testStatements.length - 1; i >= 0; i--) {
149
+ const statement = testStatements[i];
150
+ if (this.isSlowStatement(statement))
151
+ return timeout * 3;
152
+ const localTimeout = this.getTimeoutFromStatement(statement);
153
+ if (localTimeout)
154
+ return localTimeout;
155
+ }
156
+ return timeout;
157
+ }
158
+ findNodeAtLocation(location) {
159
+ let position = ts.getPositionOfLineAndCharacter(this.sourceFile, Math.max(0, location.line - 1), // ts using 0-based index location
160
+ Math.max(0, location.column - 1));
161
+ if (position > 0)
162
+ position -= 1;
163
+ const findSmallestContainingNode = (node) => {
164
+ for (const child of node.getChildren()) {
165
+ if (child.getStart() < position && position < child.getEnd()) {
166
+ return findSmallestContainingNode(child);
167
+ }
168
+ }
169
+ return node;
170
+ };
171
+ return findSmallestContainingNode(this.sourceFile);
172
+ }
173
+ }
@@ -0,0 +1,11 @@
1
+ import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
2
+ export default class TestResultReporter implements Reporter {
3
+ private testResults;
4
+ private testCases;
5
+ private commonParent;
6
+ onBegin(config: FullConfig, suite: Suite): void;
7
+ onTestEnd(test: TestCase, result: TestResult): void;
8
+ onEnd(result: FullResult): Promise<{
9
+ status?: FullResult['status'];
10
+ } | undefined | void> | void;
11
+ }
@@ -0,0 +1,63 @@
1
+ export default class TestResultReporter {
2
+ testResults = [];
3
+ testCases = [];
4
+ commonParent;
5
+ onBegin(config, suite) {
6
+ const tests = suite.allTests();
7
+ if (tests.length > 1) {
8
+ const path = [];
9
+ let current = tests[0].parent;
10
+ while (current) {
11
+ path.push(current);
12
+ current = current.parent;
13
+ }
14
+ let lastCommonParent = path.length - 1;
15
+ for (const test of tests.slice(1)) {
16
+ current = test.parent;
17
+ while (test.parent !== this.commonParent) {
18
+ const index = path.indexOf(current);
19
+ if (index !== -1 && index < lastCommonParent) {
20
+ lastCommonParent = index;
21
+ break;
22
+ }
23
+ current = current?.parent;
24
+ }
25
+ }
26
+ this.commonParent = path[lastCommonParent];
27
+ }
28
+ }
29
+ onTestEnd(test, result) {
30
+ this.testResults.push(result);
31
+ this.testCases.push(test);
32
+ }
33
+ onEnd(result) {
34
+ const { status, error, retry } = this.testResults.at(-1);
35
+ const duration = this.testResults.reduce((acc, { duration }) => acc + duration, 0);
36
+ const existingAnnotations = new Set();
37
+ const annotations = this.testCases
38
+ .flatMap((test) => test.annotations)
39
+ .filter(({ type, description }) => {
40
+ if (existingAnnotations.has(type + (description ?? '')))
41
+ return false;
42
+ existingAnnotations.add(type + (description ?? ''));
43
+ return true;
44
+ });
45
+ const { title } = this.commonParent ?? this.testCases.at(-1);
46
+ const output = {
47
+ status,
48
+ duration,
49
+ error,
50
+ title,
51
+ annotations,
52
+ tests: this.testResults.map(({ status, duration, error }, i) => ({
53
+ status,
54
+ duration,
55
+ error,
56
+ annotations: this.testCases[i].annotations,
57
+ title: this.testCases[i].title,
58
+ retry,
59
+ })),
60
+ };
61
+ console.log(JSON.stringify(output));
62
+ }
63
+ }
@@ -0,0 +1 @@
1
+ export declare const STORAGES: readonly ["file", "dynamo-db", "pg", "mysql", "mongo"];
@@ -0,0 +1,2 @@
1
+ // before renaming this file fix GHA workflow
2
+ export const STORAGES = ['file', 'dynamo-db', 'pg', 'mysql', 'mongo'];
@@ -0,0 +1,2 @@
1
+ import { TestRunReport } from '../types/reporter.js';
2
+ export declare function ghaReporter(data: TestRunReport): Promise<void>;
@@ -0,0 +1,42 @@
1
+ import * as core from '@actions/core';
2
+ import { TestStatus } from '../types/test-info.js';
3
+ import { calculateTrend, formatDuration } from './helpers.js';
4
+ export async function ghaReporter(data) {
5
+ const { config, runId, tests } = data;
6
+ await core.summary
7
+ .addHeading(`🏃 Test run summary`)
8
+ .addDetails('Run config', buildConfigData(data))
9
+ .addTable([
10
+ [
11
+ { data: '', header: true },
12
+ { data: '📁 Project', header: true },
13
+ { data: '📝 Title', header: true },
14
+ { data: '⏱️ Duration', header: true },
15
+ { data: '📊 Trend', header: true },
16
+ { data: '✨ Last successful run', header: true },
17
+ { data: '❌ Fails', header: true },
18
+ ],
19
+ ...tests.map((test) => {
20
+ const { percentage, trendIcon } = calculateTrend(test);
21
+ return [
22
+ test.status === TestStatus.Passed ? '✅' : '❌',
23
+ test.project,
24
+ test.title,
25
+ formatDuration(test.duration),
26
+ `${trendIcon} ${percentage}%`,
27
+ test.lastSuccessfulRunTimestamp
28
+ ? new Date(test.lastSuccessfulRunTimestamp).toLocaleString()
29
+ : 'N/A',
30
+ test.fails.toString(),
31
+ ];
32
+ }),
33
+ ])
34
+ .write();
35
+ }
36
+ function buildConfigData({ config, runId }) {
37
+ return `
38
+ <table>
39
+ <tr><td>Run Id</td><td>${runId}</td></tr>
40
+ <tr><td>History Window</td><td>${config.historyWindow}</td></tr>
41
+ </table>`;
42
+ }
@@ -0,0 +1,7 @@
1
+ import { TestReport } from '../types/reporter.js';
2
+ export declare function calculateTrend(test: TestReport): {
3
+ trend: number;
4
+ trendIcon: string;
5
+ percentage: string;
6
+ };
7
+ export declare function formatDuration(ms: number): string;
@@ -0,0 +1,21 @@
1
+ export function calculateTrend(test) {
2
+ const trend = test.averageDuration - test.duration;
3
+ return {
4
+ trend,
5
+ trendIcon: trend > 0 ? '📈' : '📉',
6
+ percentage: ((trend / test.averageDuration) * 100).toFixed(1),
7
+ };
8
+ }
9
+ export function formatDuration(ms) {
10
+ const seconds = Math.floor((ms / 1000) % 60);
11
+ const minutes = Math.floor((ms / (1000 * 60)) % 60);
12
+ const hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
13
+ const parts = [];
14
+ if (hours)
15
+ parts.push(`${hours} ${hours === 1 ? 'h' : 'hs'}`);
16
+ if (minutes)
17
+ parts.push(`${minutes} ${minutes === 1 ? 'min' : 'mins'}`);
18
+ if (parts.length === 0 || seconds)
19
+ parts.push(`${seconds} ${seconds === 1 ? 'sec' : 'secs'}`);
20
+ return parts.join(', ');
21
+ }
@@ -0,0 +1,4 @@
1
+ import { TestRunReport } from '../types/reporter.js';
2
+ export declare const REPORTERS: readonly ["json", "gha"];
3
+ export type ReporterType = (typeof REPORTERS)[number];
4
+ export declare function generateReport(data: TestRunReport, type: ReporterType): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { ghaReporter } from './gha-reporter.js';
2
+ export const REPORTERS = ['json', 'gha'];
3
+ export async function generateReport(data, type) {
4
+ switch (type) {
5
+ case 'json':
6
+ console.log(JSON.stringify(data));
7
+ break;
8
+ case 'gha':
9
+ await ghaReporter(data);
10
+ break;
11
+ default:
12
+ console.error('Unknown reporter type');
13
+ }
14
+ }
@@ -0,0 +1,17 @@
1
+ import { TestItem } from '../types/adapters.js';
2
+ export declare class TestExecutionReporter {
3
+ private readonly failedTests;
4
+ private readonly succeedTests;
5
+ private readonly runningTests;
6
+ private readonly spinner;
7
+ private spinnerIndex;
8
+ private spinnerInterval?;
9
+ constructor();
10
+ addTest(test: TestItem, run: Promise<any>): void;
11
+ finishTest(test: TestItem): void;
12
+ failTest(test: TestItem): void;
13
+ private printTestResult;
14
+ printSummary(): void;
15
+ private redrawRunning;
16
+ private getKey;
17
+ }