@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,85 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+ import { cursorSavePosition, cursorRestorePosition, cursorLeft, cursorDown, eraseDown } from 'ansi-escapes';
4
+ export class TestExecutionReporter {
5
+ failedTests = [];
6
+ succeedTests = [];
7
+ runningTests = [];
8
+ spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
9
+ spinnerIndex = 0;
10
+ spinnerInterval;
11
+ constructor() {
12
+ // disable spinner in CI
13
+ if (process.env.CI)
14
+ return;
15
+ process.stdout.write(cursorSavePosition);
16
+ this.spinnerInterval = setInterval(() => {
17
+ this.spinnerIndex = (this.spinnerIndex + 1) % this.spinner.length;
18
+ this.redrawRunning();
19
+ }, 80);
20
+ }
21
+ addTest(test, run) {
22
+ run.then(() => this.finishTest(test)).catch(() => this.failTest(test));
23
+ this.runningTests.push(test);
24
+ this.redrawRunning(true);
25
+ }
26
+ finishTest(test) {
27
+ this.succeedTests.push(test);
28
+ const message = `${chalk.green('✓')} ${this.getKey(test)}`;
29
+ this.printTestResult(test, message);
30
+ }
31
+ failTest(test) {
32
+ this.failedTests.push(test);
33
+ const message = `${chalk.red('✗')} ${this.getKey(test)}`;
34
+ this.printTestResult(test, message);
35
+ }
36
+ printTestResult(test, message) {
37
+ this.runningTests.splice(this.runningTests.indexOf(test), 1);
38
+ if (process.env.CI) {
39
+ console.log(message);
40
+ }
41
+ else {
42
+ process.stdout.write(cursorRestorePosition);
43
+ process.stdout.write(eraseDown);
44
+ process.stdout.write(message);
45
+ process.stdout.write('\n');
46
+ process.stdout.write(cursorSavePosition);
47
+ this.redrawRunning(true);
48
+ }
49
+ }
50
+ printSummary() {
51
+ clearTimeout(this.spinnerInterval);
52
+ process.stdout.write(cursorRestorePosition);
53
+ process.stdout.write(eraseDown);
54
+ const lines = [
55
+ chalk.green(`Succeed: ${this.succeedTests.length}`),
56
+ chalk.red(`Failed: ${this.failedTests.length}:`),
57
+ ...this.failedTests.map((test) => chalk.red(` - ${this.getKey(test)}`)),
58
+ ];
59
+ console.log(boxen(lines.join('\n'), {
60
+ title: 'Test shard results',
61
+ titleAlignment: 'left',
62
+ borderColor: 'yellow',
63
+ padding: 1,
64
+ }));
65
+ }
66
+ redrawRunning(full = false) {
67
+ if (process.env.CI)
68
+ return;
69
+ process.stdout.write(cursorRestorePosition);
70
+ for (let i = 0; i < this.runningTests.length; i++) {
71
+ const spinner = chalk.yellow(process.env.CI ? '*' : this.spinner[this.spinnerIndex]);
72
+ if (full) {
73
+ process.stdout.write(`${spinner} ${this.getKey(this.runningTests[i])}\n`);
74
+ }
75
+ else {
76
+ process.stdout.write(spinner);
77
+ process.stdout.write(cursorLeft);
78
+ process.stdout.write(cursorDown());
79
+ }
80
+ }
81
+ }
82
+ getKey(test) {
83
+ return `[${test.project}] ${test.file}:${test.position}`;
84
+ }
85
+ }
@@ -0,0 +1,18 @@
1
+ import { Adapter } from './adapter.js';
2
+ export declare class TestRunner {
3
+ private readonly adapter;
4
+ private readonly runId;
5
+ private readonly outputFolder;
6
+ private readonly reporter;
7
+ constructor(options: {
8
+ runId: string;
9
+ output: string;
10
+ }, adapter: Adapter);
11
+ runTests(): Promise<void>;
12
+ private runTestsUntilAvailable;
13
+ private removePreviousReports;
14
+ private runTest;
15
+ private parseTestResult;
16
+ private buildParams;
17
+ private createTempConfig;
18
+ }
@@ -0,0 +1,111 @@
1
+ import { createHash } from 'node:crypto';
2
+ import child_process from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { rm, writeFile } from 'node:fs/promises';
5
+ import { TestExecutionReporter } from './reporters/test-execution-reporter.js';
6
+ import path from 'node:path';
7
+ import * as uuid from 'uuid';
8
+ const exec = promisify(child_process.exec);
9
+ export class TestRunner {
10
+ adapter;
11
+ runId;
12
+ outputFolder;
13
+ reporter = new TestExecutionReporter();
14
+ constructor(options, adapter) {
15
+ this.adapter = adapter;
16
+ this.runId = options.runId;
17
+ this.outputFolder = options.output;
18
+ }
19
+ async runTests() {
20
+ await this.removePreviousReports();
21
+ const config = await this.adapter.startShard(this.runId);
22
+ config.configFile = await this.createTempConfig(config.configFile);
23
+ try {
24
+ await this.runTestsUntilAvailable(config);
25
+ await this.adapter.finishShard(this.runId);
26
+ await this.adapter.dispose();
27
+ this.reporter.printSummary();
28
+ }
29
+ finally {
30
+ if (config.configFile)
31
+ await rm(config.configFile);
32
+ }
33
+ }
34
+ async runTestsUntilAvailable(config) {
35
+ const runningTests = new Set();
36
+ let next = await this.adapter.getNextTest(this.runId, config);
37
+ while (next || runningTests.size > 0) {
38
+ if (next && runningTests.size < config.workers) {
39
+ const testPromise = this.runTest(next, config).then(() => {
40
+ runningTests.delete(testPromise);
41
+ });
42
+ runningTests.add(testPromise);
43
+ next = await this.adapter.getNextTest(this.runId, config);
44
+ }
45
+ else {
46
+ await Promise.race(runningTests);
47
+ }
48
+ }
49
+ await Promise.all(runningTests);
50
+ }
51
+ async removePreviousReports() {
52
+ await rm(`./${this.outputFolder}`, { recursive: true, force: true });
53
+ }
54
+ async runTest(test, config) {
55
+ const testPosition = `${test.file}:${test.position}`;
56
+ const testName = `[${test.project}] > ${testPosition}`;
57
+ const testHash = createHash('md5').update(testName).digest('hex');
58
+ try {
59
+ const run = exec(`npx playwright test ${testPosition} ${this.buildParams(test, config, testHash)}`, {
60
+ env: {
61
+ ...process.env,
62
+ PLAYWRIGHT_BLOB_OUTPUT_FILE: `${this.outputFolder}/${testHash}.zip`,
63
+ },
64
+ });
65
+ this.reporter.addTest(test, run);
66
+ const { stdout } = await run;
67
+ await this.adapter.finishTest({
68
+ runId: this.runId,
69
+ test,
70
+ testResult: this.parseTestResult(stdout),
71
+ config,
72
+ });
73
+ }
74
+ catch (error) {
75
+ if (!error.stdout)
76
+ throw error;
77
+ await this.adapter.failTest({
78
+ runId: this.runId,
79
+ test,
80
+ testResult: this.parseTestResult(error.stdout),
81
+ config,
82
+ });
83
+ }
84
+ }
85
+ parseTestResult(stdout) {
86
+ return JSON.parse(stdout);
87
+ }
88
+ buildParams(test, config, testHash) {
89
+ const args = [...config.args];
90
+ args.push('--workers', '1');
91
+ args.push('--reporter', 'blob,@playwright-orchestrator/core/test-result-reporter');
92
+ args.push('--project', `"${test.project}"`);
93
+ args.push('--output', `"${this.outputFolder}/${testHash}"`);
94
+ if (config.configFile) {
95
+ args.push('--config', `"${config.configFile}"`);
96
+ }
97
+ return args.join(' ');
98
+ }
99
+ async createTempConfig(file) {
100
+ if (!file)
101
+ return;
102
+ // Remove webServer from the config. Not supported in the orchestrator
103
+ const content = `
104
+ import config from '${path.resolve(file)}';
105
+ delete config.webServer;
106
+ export default config;`;
107
+ const tempFile = `.playwright-${uuid.v7()}.config.tmp.ts`;
108
+ await writeFile(tempFile, content);
109
+ return tempFile;
110
+ }
111
+ }
@@ -0,0 +1,39 @@
1
+ import { TestReportResult } from './reporter.js';
2
+ import { TestRunConfig, TestRunInfo } from './test-info.js';
3
+ import { TestDetailsAnnotation } from '@playwright/test';
4
+ export interface TestItem {
5
+ file: string;
6
+ position: string;
7
+ project: string;
8
+ order: number;
9
+ timeout: number;
10
+ }
11
+ export interface ReporterTestItem extends TestItem {
12
+ testId: string;
13
+ }
14
+ export interface ResultTestParams {
15
+ runId: string;
16
+ test: TestItem;
17
+ testResult: TestReportResult;
18
+ config: TestRunConfig;
19
+ }
20
+ export interface SaveTestRunParams {
21
+ runId: string;
22
+ testRun: TestRunInfo;
23
+ args: string[];
24
+ historyWindow: number;
25
+ }
26
+ export interface GetTestIdParams {
27
+ project: string;
28
+ file: string;
29
+ title: string;
30
+ annotations: TestDetailsAnnotation[];
31
+ }
32
+ export interface SortTestsOptions {
33
+ historyWindow: number;
34
+ reverse?: boolean;
35
+ }
36
+ export interface TestSortItem {
37
+ ema: number;
38
+ fails: number;
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { TestResult } from '@playwright/test/reporter';
2
+ import { TestDetailsAnnotation } from '@playwright/test';
3
+ import { TestRunConfig, TestStatus } from './test-info.js';
4
+ export interface TestReport {
5
+ file: string;
6
+ position: string;
7
+ project: string;
8
+ status: TestStatus;
9
+ duration: number;
10
+ averageDuration: number;
11
+ title: string;
12
+ fails: number;
13
+ lastSuccessfulRunTimestamp?: number;
14
+ }
15
+ export interface TestRunReport {
16
+ runId: string;
17
+ config: TestRunConfig;
18
+ tests: TestReport[];
19
+ }
20
+ export interface BaseTestResult {
21
+ annotations: TestDetailsAnnotation[];
22
+ duration: number;
23
+ title: string;
24
+ status: TestResult['status'];
25
+ }
26
+ export interface TestReportResult extends BaseTestResult {
27
+ error: TestResult['error'];
28
+ tests: TestInfoResult[];
29
+ }
30
+ export interface TestInfoResult extends BaseTestResult {
31
+ retry: number;
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { TestDetailsAnnotation } from '@playwright/test';
2
+ export interface Project {
3
+ name: string;
4
+ outputDir: string;
5
+ }
6
+ export interface TestConfig {
7
+ workers: number;
8
+ configFile?: string;
9
+ projects: Project[];
10
+ }
11
+ export interface TestRunConfig extends TestConfig {
12
+ historyWindow: number;
13
+ args: string[];
14
+ status: RunStatus;
15
+ updated: number;
16
+ }
17
+ export interface TestRunInfo {
18
+ testRun: TestRun;
19
+ config: TestConfig;
20
+ }
21
+ export interface TestRun {
22
+ [file: string]: {
23
+ [position: string]: {
24
+ timeout: number;
25
+ projects: string[];
26
+ title: string;
27
+ annotations: TestDetailsAnnotation[];
28
+ };
29
+ };
30
+ }
31
+ export declare enum RunStatus {
32
+ Created = 0,
33
+ Run = 10,
34
+ RepeatRun = 20,
35
+ Finished = 30
36
+ }
37
+ export declare enum TestStatus {
38
+ Ready = 0,
39
+ Ongoing = 10,
40
+ Failed = 20,
41
+ Passed = 30
42
+ }
@@ -0,0 +1,14 @@
1
+ export var RunStatus;
2
+ (function (RunStatus) {
3
+ RunStatus[RunStatus["Created"] = 0] = "Created";
4
+ RunStatus[RunStatus["Run"] = 10] = "Run";
5
+ RunStatus[RunStatus["RepeatRun"] = 20] = "RepeatRun";
6
+ RunStatus[RunStatus["Finished"] = 30] = "Finished";
7
+ })(RunStatus || (RunStatus = {}));
8
+ export var TestStatus;
9
+ (function (TestStatus) {
10
+ TestStatus[TestStatus["Ready"] = 0] = "Ready";
11
+ TestStatus[TestStatus["Ongoing"] = 10] = "Ongoing";
12
+ TestStatus[TestStatus["Failed"] = 20] = "Failed";
13
+ TestStatus[TestStatus["Passed"] = 30] = "Passed";
14
+ })(TestStatus || (TestStatus = {}));
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@dionlarson/playwright-orchestrator-core",
3
+ "version": "1.3.0",
4
+ "description": "Core lib and cli for Playwright test orchestration",
5
+ "keywords": [
6
+ "playwright",
7
+ "testing",
8
+ "automation",
9
+ "orchestration",
10
+ "e2e"
11
+ ],
12
+ "author": "Rostyslav Kudrevatykh",
13
+ "license": "Apache-2.0",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/dionlarson/playwright-orchestrator-private.git"
17
+ },
18
+ "module": "dist/index.js",
19
+ "types": "dist/index.d.ts",
20
+ "bin": {
21
+ "playwright-orchestrator": "./dist/cli.js"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "dependencies": {
27
+ "@actions/core": "^1.11.1",
28
+ "@commander-js/extra-typings": "^13.0.0",
29
+ "ansi-escapes": "^7.0.0",
30
+ "chalk": "^5.4.1",
31
+ "commander": "^13.0.0",
32
+ "boxen": "^8.0.1",
33
+ "typescript": "^5.0.0",
34
+ "uuid": "^11.0.3"
35
+ },
36
+ "devDependencies": {},
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "type": "module",
41
+ "main": "dist/index.js",
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/index.d.ts",
45
+ "default": "./dist/index.js"
46
+ },
47
+ "./cli": "./dist/cli.js",
48
+ "./package.json": "./package.json",
49
+ "./tests-info-reporter": "./dist/playwright-tools/run-info-reporter.js",
50
+ "./test-result-reporter": "./dist/playwright-tools/test-result-reporter.js",
51
+ "./annotations": "./dist/playwright-tools/annotations.cjs"
52
+ },
53
+ "peerDependencies": {
54
+ "@playwright/test": "^1.44.0",
55
+ "@dionlarson/playwright-orchestrator-file": "^1.0.0",
56
+ "@dionlarson/playwright-orchestrator-dynamo-db": "^1.0.0",
57
+ "@dionlarson/playwright-orchestrator-pg": "^1.0.0",
58
+ "@dionlarson/playwright-orchestrator-mysql": "^1.0.0",
59
+ "@dionlarson/playwright-orchestrator-mongo": "^1.0.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "@dionlarson/playwright-orchestrator-file": {
63
+ "optional": true
64
+ },
65
+ "@dionlarson/playwright-orchestrator-dynamo-db": {
66
+ "optional": true
67
+ },
68
+ "@dionlarson/playwright-orchestrator-pg": {
69
+ "optional": true
70
+ },
71
+ "@dionlarson/playwright-orchestrator-mysql": {
72
+ "optional": true
73
+ },
74
+ "@dionlarson/playwright-orchestrator-mongo": {
75
+ "optional": true
76
+ }
77
+ },
78
+ "scripts": {
79
+ "prepare": "cp ../../README.md ./ && cp ../../LICENSE.md ./"
80
+ }
81
+ }