@nsxbet/playwright-orchestrator 0.4.0 → 0.5.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 +22 -5
- package/dist/commands/assign.d.ts.map +1 -1
- package/dist/commands/assign.js +7 -48
- package/dist/commands/assign.js.map +1 -1
- package/dist/commands/extract-timing.d.ts +1 -12
- package/dist/commands/extract-timing.d.ts.map +1 -1
- package/dist/commands/extract-timing.js +15 -84
- package/dist/commands/extract-timing.js.map +1 -1
- package/dist/commands/merge-timing.d.ts +1 -4
- package/dist/commands/merge-timing.d.ts.map +1 -1
- package/dist/commands/merge-timing.js +11 -106
- package/dist/commands/merge-timing.js.map +1 -1
- package/dist/core/estimate.d.ts +9 -9
- package/dist/core/estimate.d.ts.map +1 -1
- package/dist/core/estimate.js +4 -4
- package/dist/core/estimate.js.map +1 -1
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +0 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/test-discovery.d.ts.map +1 -1
- package/dist/core/test-discovery.js +7 -4
- package/dist/core/test-discovery.js.map +1 -1
- package/dist/core/timing-store.d.ts +16 -36
- package/dist/core/timing-store.d.ts.map +1 -1
- package/dist/core/timing-store.js +21 -140
- package/dist/core/timing-store.js.map +1 -1
- package/dist/core/types.d.ts +7 -67
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +1 -32
- package/dist/core/types.js.map +1 -1
- package/dist/reporter.d.ts +40 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +73 -0
- package/dist/reporter.js.map +1 -0
- package/package.json +20 -1
- package/dist/core/grep-pattern.d.ts +0 -61
- package/dist/core/grep-pattern.d.ts.map +0 -1
- package/dist/core/grep-pattern.js +0 -104
- package/dist/core/grep-pattern.js.map +0 -1
package/dist/core/types.d.ts
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Timing data for a single test
|
|
3
|
-
*/
|
|
4
|
-
export interface FileTimingData {
|
|
5
|
-
/** Duration in milliseconds */
|
|
6
|
-
duration: number;
|
|
7
|
-
/** Number of times this file has been measured */
|
|
8
|
-
runs: number;
|
|
9
|
-
/** ISO timestamp of last measurement */
|
|
10
|
-
lastRun: string;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Timing data for a single test (v2 format - test-level)
|
|
2
|
+
* Timing data for a single test
|
|
14
3
|
*/
|
|
15
4
|
export interface TestTimingData {
|
|
16
5
|
/** Source file containing this test */
|
|
@@ -23,31 +12,16 @@ export interface TestTimingData {
|
|
|
23
12
|
lastRun: string;
|
|
24
13
|
}
|
|
25
14
|
/**
|
|
26
|
-
* Complete timing data structure stored in cache (
|
|
15
|
+
* Complete timing data structure stored in cache (test-level)
|
|
27
16
|
*/
|
|
28
|
-
export interface
|
|
29
|
-
/** Schema version
|
|
30
|
-
version: 1;
|
|
31
|
-
/** ISO timestamp of last update */
|
|
32
|
-
updatedAt: string;
|
|
33
|
-
/** Map of file names to their timing data */
|
|
34
|
-
files: Record<string, FileTimingData>;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Complete timing data structure stored in cache (v2 - test-level)
|
|
38
|
-
*/
|
|
39
|
-
export interface TimingDataV2 {
|
|
40
|
-
/** Schema version (2) */
|
|
17
|
+
export interface TimingData {
|
|
18
|
+
/** Schema version */
|
|
41
19
|
version: 2;
|
|
42
20
|
/** ISO timestamp of last update */
|
|
43
21
|
updatedAt: string;
|
|
44
22
|
/** Map of test IDs to their timing data */
|
|
45
23
|
tests: Record<string, TestTimingData>;
|
|
46
24
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Union type for timing data (supports both versions)
|
|
49
|
-
*/
|
|
50
|
-
export type TimingData = TimingDataV1 | TimingDataV2;
|
|
51
25
|
/**
|
|
52
26
|
* Input for the shard assignment algorithm (file-level)
|
|
53
27
|
*/
|
|
@@ -113,10 +87,6 @@ export interface AssignResult {
|
|
|
113
87
|
export interface TestAssignResult {
|
|
114
88
|
/** Map of shard index to list of test IDs */
|
|
115
89
|
shards: Record<number, string[]>;
|
|
116
|
-
/** Map of shard index to grep pattern */
|
|
117
|
-
grepPatterns: Record<number, string>;
|
|
118
|
-
/** Map of shard index to test locations (file:line format) */
|
|
119
|
-
testLocations: Record<number, string[]>;
|
|
120
90
|
/** Expected duration per shard */
|
|
121
91
|
expectedDurations: Record<number, number>;
|
|
122
92
|
/** Total number of tests */
|
|
@@ -127,20 +97,9 @@ export interface TestAssignResult {
|
|
|
127
97
|
isOptimal: boolean;
|
|
128
98
|
}
|
|
129
99
|
/**
|
|
130
|
-
* Per-shard timing artifact uploaded after test run (
|
|
100
|
+
* Per-shard timing artifact uploaded after test run (test-level)
|
|
131
101
|
*/
|
|
132
102
|
export interface ShardTimingArtifact {
|
|
133
|
-
/** Shard index (1-based) */
|
|
134
|
-
shard: number;
|
|
135
|
-
/** Browser project name */
|
|
136
|
-
project: string;
|
|
137
|
-
/** Map of file names to duration in ms */
|
|
138
|
-
files: Record<string, number>;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Per-shard timing artifact uploaded after test run (test-level, v2)
|
|
142
|
-
*/
|
|
143
|
-
export interface TestShardTimingArtifact {
|
|
144
103
|
/** Shard index (1-based) */
|
|
145
104
|
shard: number;
|
|
146
105
|
/** Browser project name */
|
|
@@ -214,26 +173,15 @@ export interface PlaywrightListSpec {
|
|
|
214
173
|
}
|
|
215
174
|
/** Current schema version for timing data */
|
|
216
175
|
export declare const TIMING_DATA_VERSION = 2;
|
|
217
|
-
/** Legacy schema version */
|
|
218
|
-
export declare const TIMING_DATA_VERSION_V1 = 1;
|
|
219
176
|
/**
|
|
220
|
-
* Create an empty timing data structure
|
|
177
|
+
* Create an empty timing data structure
|
|
221
178
|
*/
|
|
222
|
-
export declare function createEmptyTimingData():
|
|
223
|
-
/**
|
|
224
|
-
* Create an empty timing data structure (v1 - file-level, for backwards compatibility)
|
|
225
|
-
*/
|
|
226
|
-
export declare function createEmptyTimingDataV1(): TimingDataV1;
|
|
179
|
+
export declare function createEmptyTimingData(): TimingData;
|
|
227
180
|
/**
|
|
228
181
|
* Build a test ID from file and title path
|
|
229
182
|
* Format: file::describe1::describe2::testTitle
|
|
230
183
|
*/
|
|
231
184
|
export declare function buildTestId(file: string, titlePath: string[]): string;
|
|
232
|
-
/**
|
|
233
|
-
* Build a test location from file and line
|
|
234
|
-
* Format: file:line (used for exact test filtering in Playwright)
|
|
235
|
-
*/
|
|
236
|
-
export declare function buildTestLocation(file: string, line: number): string;
|
|
237
185
|
/**
|
|
238
186
|
* Parse a test ID back to file and title path
|
|
239
187
|
*/
|
|
@@ -241,12 +189,4 @@ export declare function parseTestId(testId: string): {
|
|
|
241
189
|
file: string;
|
|
242
190
|
titlePath: string[];
|
|
243
191
|
};
|
|
244
|
-
/**
|
|
245
|
-
* Check if timing data is v2 (test-level)
|
|
246
|
-
*/
|
|
247
|
-
export declare function isTimingDataV2(data: TimingData): data is TimingDataV2;
|
|
248
|
-
/**
|
|
249
|
-
* Check if timing data is v1 (file-level)
|
|
250
|
-
*/
|
|
251
|
-
export declare function isTimingDataV1(data: TimingData): data is TimingDataV1;
|
|
252
192
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/core/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
|
package/dist/core/types.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/** Current schema version for timing data */
|
|
2
2
|
export const TIMING_DATA_VERSION = 2;
|
|
3
|
-
/** Legacy schema version */
|
|
4
|
-
export const TIMING_DATA_VERSION_V1 = 1;
|
|
5
3
|
/**
|
|
6
|
-
* Create an empty timing data structure
|
|
4
|
+
* Create an empty timing data structure
|
|
7
5
|
*/
|
|
8
6
|
export function createEmptyTimingData() {
|
|
9
7
|
return {
|
|
@@ -12,16 +10,6 @@ export function createEmptyTimingData() {
|
|
|
12
10
|
tests: {},
|
|
13
11
|
};
|
|
14
12
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Create an empty timing data structure (v1 - file-level, for backwards compatibility)
|
|
17
|
-
*/
|
|
18
|
-
export function createEmptyTimingDataV1() {
|
|
19
|
-
return {
|
|
20
|
-
version: TIMING_DATA_VERSION_V1,
|
|
21
|
-
updatedAt: new Date().toISOString(),
|
|
22
|
-
files: {},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
13
|
/**
|
|
26
14
|
* Build a test ID from file and title path
|
|
27
15
|
* Format: file::describe1::describe2::testTitle
|
|
@@ -29,13 +17,6 @@ export function createEmptyTimingDataV1() {
|
|
|
29
17
|
export function buildTestId(file, titlePath) {
|
|
30
18
|
return [file, ...titlePath].join('::');
|
|
31
19
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Build a test location from file and line
|
|
34
|
-
* Format: file:line (used for exact test filtering in Playwright)
|
|
35
|
-
*/
|
|
36
|
-
export function buildTestLocation(file, line) {
|
|
37
|
-
return `${file}:${line}`;
|
|
38
|
-
}
|
|
39
20
|
/**
|
|
40
21
|
* Parse a test ID back to file and title path
|
|
41
22
|
*/
|
|
@@ -46,16 +27,4 @@ export function parseTestId(testId) {
|
|
|
46
27
|
titlePath: parts.slice(1),
|
|
47
28
|
};
|
|
48
29
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Check if timing data is v2 (test-level)
|
|
51
|
-
*/
|
|
52
|
-
export function isTimingDataV2(data) {
|
|
53
|
-
return data.version === 2;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Check if timing data is v1 (file-level)
|
|
57
|
-
*/
|
|
58
|
-
export function isTimingDataV1(data) {
|
|
59
|
-
return data.version === 1;
|
|
60
|
-
}
|
|
61
30
|
//# sourceMappingURL=types.js.map
|
package/dist/core/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AA+LA,6CAA6C;AAC7C,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC;;GAEG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO;QACL,OAAO,EAAE,mBAAmB;QAC5B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,KAAK,EAAE,EAAE;KACV,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,SAAmB;IAC3D,OAAO,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc;IAIxC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE;QACpB,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;KAC1B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright Orchestrator Reporter
|
|
3
|
+
*
|
|
4
|
+
* A custom Playwright reporter that filters tests based on a JSON file
|
|
5
|
+
* containing test IDs. Uses exact Set.has() matching to avoid substring
|
|
6
|
+
* collisions that plague --grep approaches.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* 1. Add to playwright.config.ts:
|
|
10
|
+
* reporter: [['@nsxbet/playwright-orchestrator/reporter'], ['html']]
|
|
11
|
+
* 2. Set ORCHESTRATOR_SHARD_FILE env var to path of JSON file with test IDs
|
|
12
|
+
*
|
|
13
|
+
* Environment variables:
|
|
14
|
+
* - ORCHESTRATOR_SHARD_FILE: Path to JSON file with array of test IDs
|
|
15
|
+
* - ORCHESTRATOR_DEBUG: Set to "1" to enable debug logging
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // shard.json
|
|
19
|
+
* ["e2e/login.spec.ts::Login::should login", "e2e/home.spec.ts::Home::should render"]
|
|
20
|
+
*
|
|
21
|
+
* // Run with filtering
|
|
22
|
+
* ORCHESTRATOR_SHARD_FILE=shard.json npx playwright test
|
|
23
|
+
*
|
|
24
|
+
* @module @nsxbet/playwright-orchestrator/reporter
|
|
25
|
+
*/
|
|
26
|
+
import type { FullConfig, Reporter, Suite, TestCase } from '@playwright/test/reporter';
|
|
27
|
+
export default class OrchestratorReporter implements Reporter {
|
|
28
|
+
private allowedTestIds;
|
|
29
|
+
private debug;
|
|
30
|
+
onBegin(_config: FullConfig, _suite: Suite): void;
|
|
31
|
+
onTestBegin(test: TestCase): void;
|
|
32
|
+
/**
|
|
33
|
+
* Build test ID from TestCase.
|
|
34
|
+
* Format: {relative-file}::{describe}::{test-title}
|
|
35
|
+
*
|
|
36
|
+
* This must match the format generated by the orchestrator.
|
|
37
|
+
*/
|
|
38
|
+
private buildTestId;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACT,MAAM,2BAA2B,CAAC;AAEnC,MAAM,CAAC,OAAO,OAAO,oBAAqB,YAAW,QAAQ;IAC3D,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,KAAK,CAA0C;IAEvD,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK;IAsB1C,WAAW,CAAC,IAAI,EAAE,QAAQ;IAa1B;;;;;OAKG;IACH,OAAO,CAAC,WAAW;CAMpB"}
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright Orchestrator Reporter
|
|
3
|
+
*
|
|
4
|
+
* A custom Playwright reporter that filters tests based on a JSON file
|
|
5
|
+
* containing test IDs. Uses exact Set.has() matching to avoid substring
|
|
6
|
+
* collisions that plague --grep approaches.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* 1. Add to playwright.config.ts:
|
|
10
|
+
* reporter: [['@nsxbet/playwright-orchestrator/reporter'], ['html']]
|
|
11
|
+
* 2. Set ORCHESTRATOR_SHARD_FILE env var to path of JSON file with test IDs
|
|
12
|
+
*
|
|
13
|
+
* Environment variables:
|
|
14
|
+
* - ORCHESTRATOR_SHARD_FILE: Path to JSON file with array of test IDs
|
|
15
|
+
* - ORCHESTRATOR_DEBUG: Set to "1" to enable debug logging
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // shard.json
|
|
19
|
+
* ["e2e/login.spec.ts::Login::should login", "e2e/home.spec.ts::Home::should render"]
|
|
20
|
+
*
|
|
21
|
+
* // Run with filtering
|
|
22
|
+
* ORCHESTRATOR_SHARD_FILE=shard.json npx playwright test
|
|
23
|
+
*
|
|
24
|
+
* @module @nsxbet/playwright-orchestrator/reporter
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
export default class OrchestratorReporter {
|
|
29
|
+
allowedTestIds = null;
|
|
30
|
+
debug = process.env.ORCHESTRATOR_DEBUG === '1';
|
|
31
|
+
onBegin(_config, _suite) {
|
|
32
|
+
const shardFile = process.env.ORCHESTRATOR_SHARD_FILE;
|
|
33
|
+
if (!shardFile || !fs.existsSync(shardFile)) {
|
|
34
|
+
if (this.debug) {
|
|
35
|
+
console.log('[Orchestrator] No shard file, running all tests');
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const testIds = JSON.parse(fs.readFileSync(shardFile, 'utf-8'));
|
|
41
|
+
this.allowedTestIds = new Set(testIds);
|
|
42
|
+
console.log(`[Orchestrator] ${this.allowedTestIds.size} tests for this shard`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error('[Orchestrator] Failed to load shard file:', error);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
onTestBegin(test) {
|
|
50
|
+
if (!this.allowedTestIds)
|
|
51
|
+
return;
|
|
52
|
+
const testId = this.buildTestId(test);
|
|
53
|
+
if (!this.allowedTestIds.has(testId)) {
|
|
54
|
+
test.annotations.push({ type: 'skip', description: 'Not in shard' });
|
|
55
|
+
if (this.debug) {
|
|
56
|
+
console.log(`[Skip] ${testId}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build test ID from TestCase.
|
|
62
|
+
* Format: {relative-file}::{describe}::{test-title}
|
|
63
|
+
*
|
|
64
|
+
* This must match the format generated by the orchestrator.
|
|
65
|
+
*/
|
|
66
|
+
buildTestId(test) {
|
|
67
|
+
const file = path
|
|
68
|
+
.relative(process.cwd(), test.location.file)
|
|
69
|
+
.replace(/\\/g, '/');
|
|
70
|
+
return [file, ...test.titlePath()].join('::');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=reporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporter.js","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAQlC,MAAM,CAAC,OAAO,OAAO,oBAAoB;IAC/B,cAAc,GAAuB,IAAI,CAAC;IAC1C,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,KAAK,GAAG,CAAC;IAEvD,OAAO,CAAC,OAAmB,EAAE,MAAa;QACxC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;QAEtD,IAAI,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5C,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;YACjE,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;YAChE,IAAI,CAAC,cAAc,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;YACvC,OAAO,CAAC,GAAG,CACT,kBAAkB,IAAI,CAAC,cAAc,CAAC,IAAI,uBAAuB,CAClE,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,KAAK,CAAC,CAAC;YAClE,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,WAAW,CAAC,IAAc;QACxB,IAAI,CAAC,IAAI,CAAC,cAAc;YAAE,OAAO;QAEjC,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAEtC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;YACrE,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,GAAG,CAAC,UAAU,MAAM,EAAE,CAAC,CAAC;YAClC,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACK,WAAW,CAAC,IAAc;QAChC,MAAM,IAAI,GAAG,IAAI;aACd,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;aAC3C,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACvB,OAAO,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nsxbet/playwright-orchestrator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Intelligent Playwright test distribution across CI shards using historical timing data",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./reporter": {
|
|
14
|
+
"types": "./dist/reporter.d.ts",
|
|
15
|
+
"import": "./dist/reporter.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
8
18
|
"bin": {
|
|
9
19
|
"playwright-orchestrator": "./bin/run.js"
|
|
10
20
|
},
|
|
@@ -64,7 +74,16 @@
|
|
|
64
74
|
"@biomejs/biome": "2.3.11",
|
|
65
75
|
"@changesets/changelog-github": "^0.5.2",
|
|
66
76
|
"@changesets/cli": "^2.29.8",
|
|
77
|
+
"@playwright/test": "^1.40.0",
|
|
67
78
|
"@types/node": "22.15.29",
|
|
68
79
|
"typescript": "5.9.3"
|
|
80
|
+
},
|
|
81
|
+
"peerDependencies": {
|
|
82
|
+
"@playwright/test": ">=1.20.0"
|
|
83
|
+
},
|
|
84
|
+
"peerDependenciesMeta": {
|
|
85
|
+
"@playwright/test": {
|
|
86
|
+
"optional": true
|
|
87
|
+
}
|
|
69
88
|
}
|
|
70
89
|
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maximum length for a grep pattern before switching to grep-file
|
|
3
|
-
*/
|
|
4
|
-
export declare const MAX_GREP_PATTERN_LENGTH = 4000;
|
|
5
|
-
/**
|
|
6
|
-
* Escape regex special characters in a string
|
|
7
|
-
*
|
|
8
|
-
* @param str - String to escape
|
|
9
|
-
* @returns Escaped string safe for use in regex
|
|
10
|
-
*/
|
|
11
|
-
export declare function escapeRegex(str: string): string;
|
|
12
|
-
/**
|
|
13
|
-
* Extract the test title from a test ID
|
|
14
|
-
* The title is the last part of the titlePath
|
|
15
|
-
*
|
|
16
|
-
* @param testId - Test ID in format file::describe::testTitle
|
|
17
|
-
* @returns The test title
|
|
18
|
-
*/
|
|
19
|
-
export declare function extractTitleFromTestId(testId: string): string;
|
|
20
|
-
/**
|
|
21
|
-
* Generate a grep pattern from a list of test IDs
|
|
22
|
-
*
|
|
23
|
-
* Uses the test title (last part of titlePath) for matching.
|
|
24
|
-
* Escapes regex special characters to ensure exact matching.
|
|
25
|
-
*
|
|
26
|
-
* @param testIds - List of test IDs to include in pattern
|
|
27
|
-
* @returns Grep pattern string that matches any of the tests
|
|
28
|
-
*/
|
|
29
|
-
export declare function generateGrepPattern(testIds: string[]): string;
|
|
30
|
-
/**
|
|
31
|
-
* Generate grep patterns for multiple shards
|
|
32
|
-
*
|
|
33
|
-
* @param shardTests - Map of shard index to list of test IDs
|
|
34
|
-
* @returns Map of shard index to grep pattern
|
|
35
|
-
*/
|
|
36
|
-
export declare function generateGrepPatterns(shardTests: Record<number, string[]>): Record<number, string>;
|
|
37
|
-
/**
|
|
38
|
-
* Check if a grep pattern is too long and should use --grep-file instead
|
|
39
|
-
*
|
|
40
|
-
* @param pattern - Grep pattern to check
|
|
41
|
-
* @returns True if pattern exceeds maximum length
|
|
42
|
-
*/
|
|
43
|
-
export declare function isPatternTooLong(pattern: string): boolean;
|
|
44
|
-
/**
|
|
45
|
-
* Generate content for a grep-file (one pattern per line)
|
|
46
|
-
*
|
|
47
|
-
* @param testIds - List of test IDs
|
|
48
|
-
* @returns File content with one escaped title per line
|
|
49
|
-
*/
|
|
50
|
-
export declare function generateGrepFileContent(testIds: string[]): string;
|
|
51
|
-
/**
|
|
52
|
-
* Determine the best grep strategy for a list of tests
|
|
53
|
-
*
|
|
54
|
-
* @param testIds - List of test IDs
|
|
55
|
-
* @returns Object with strategy ('pattern' or 'file') and content
|
|
56
|
-
*/
|
|
57
|
-
export declare function determineGrepStrategy(testIds: string[]): {
|
|
58
|
-
strategy: 'pattern' | 'file';
|
|
59
|
-
content: string;
|
|
60
|
-
};
|
|
61
|
-
//# sourceMappingURL=grep-pattern.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"grep-pattern.d.ts","sourceRoot":"","sources":["../../src/core/grep-pattern.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAY7D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAOjE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG;IACxD,QAAQ,EAAE,SAAS,GAAG,MAAM,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB,CAeA"}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { parseTestId } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Regex special characters that need escaping in grep patterns
|
|
4
|
-
*/
|
|
5
|
-
const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g;
|
|
6
|
-
/**
|
|
7
|
-
* Maximum length for a grep pattern before switching to grep-file
|
|
8
|
-
*/
|
|
9
|
-
export const MAX_GREP_PATTERN_LENGTH = 4000;
|
|
10
|
-
/**
|
|
11
|
-
* Escape regex special characters in a string
|
|
12
|
-
*
|
|
13
|
-
* @param str - String to escape
|
|
14
|
-
* @returns Escaped string safe for use in regex
|
|
15
|
-
*/
|
|
16
|
-
export function escapeRegex(str) {
|
|
17
|
-
return str.replace(REGEX_SPECIAL_CHARS, '\\$&');
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Extract the test title from a test ID
|
|
21
|
-
* The title is the last part of the titlePath
|
|
22
|
-
*
|
|
23
|
-
* @param testId - Test ID in format file::describe::testTitle
|
|
24
|
-
* @returns The test title
|
|
25
|
-
*/
|
|
26
|
-
export function extractTitleFromTestId(testId) {
|
|
27
|
-
const { titlePath } = parseTestId(testId);
|
|
28
|
-
return titlePath[titlePath.length - 1] || testId;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Generate a grep pattern from a list of test IDs
|
|
32
|
-
*
|
|
33
|
-
* Uses the test title (last part of titlePath) for matching.
|
|
34
|
-
* Escapes regex special characters to ensure exact matching.
|
|
35
|
-
*
|
|
36
|
-
* @param testIds - List of test IDs to include in pattern
|
|
37
|
-
* @returns Grep pattern string that matches any of the tests
|
|
38
|
-
*/
|
|
39
|
-
export function generateGrepPattern(testIds) {
|
|
40
|
-
if (testIds.length === 0) {
|
|
41
|
-
return '';
|
|
42
|
-
}
|
|
43
|
-
const titles = testIds.map((id) => {
|
|
44
|
-
const title = extractTitleFromTestId(id);
|
|
45
|
-
return escapeRegex(title);
|
|
46
|
-
});
|
|
47
|
-
// Use OR operator to match any of the titles
|
|
48
|
-
return titles.join('|');
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Generate grep patterns for multiple shards
|
|
52
|
-
*
|
|
53
|
-
* @param shardTests - Map of shard index to list of test IDs
|
|
54
|
-
* @returns Map of shard index to grep pattern
|
|
55
|
-
*/
|
|
56
|
-
export function generateGrepPatterns(shardTests) {
|
|
57
|
-
const patterns = {};
|
|
58
|
-
for (const [shardIndex, testIds] of Object.entries(shardTests)) {
|
|
59
|
-
patterns[Number(shardIndex)] = generateGrepPattern(testIds);
|
|
60
|
-
}
|
|
61
|
-
return patterns;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Check if a grep pattern is too long and should use --grep-file instead
|
|
65
|
-
*
|
|
66
|
-
* @param pattern - Grep pattern to check
|
|
67
|
-
* @returns True if pattern exceeds maximum length
|
|
68
|
-
*/
|
|
69
|
-
export function isPatternTooLong(pattern) {
|
|
70
|
-
return pattern.length > MAX_GREP_PATTERN_LENGTH;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Generate content for a grep-file (one pattern per line)
|
|
74
|
-
*
|
|
75
|
-
* @param testIds - List of test IDs
|
|
76
|
-
* @returns File content with one escaped title per line
|
|
77
|
-
*/
|
|
78
|
-
export function generateGrepFileContent(testIds) {
|
|
79
|
-
const titles = testIds.map((id) => {
|
|
80
|
-
const title = extractTitleFromTestId(id);
|
|
81
|
-
return escapeRegex(title);
|
|
82
|
-
});
|
|
83
|
-
return titles.join('\n');
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Determine the best grep strategy for a list of tests
|
|
87
|
-
*
|
|
88
|
-
* @param testIds - List of test IDs
|
|
89
|
-
* @returns Object with strategy ('pattern' or 'file') and content
|
|
90
|
-
*/
|
|
91
|
-
export function determineGrepStrategy(testIds) {
|
|
92
|
-
if (testIds.length === 0) {
|
|
93
|
-
return { strategy: 'pattern', content: '' };
|
|
94
|
-
}
|
|
95
|
-
const pattern = generateGrepPattern(testIds);
|
|
96
|
-
if (isPatternTooLong(pattern)) {
|
|
97
|
-
return {
|
|
98
|
-
strategy: 'file',
|
|
99
|
-
content: generateGrepFileContent(testIds),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
return { strategy: 'pattern', content: pattern };
|
|
103
|
-
}
|
|
104
|
-
//# sourceMappingURL=grep-pattern.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"grep-pattern.js","sourceRoot":"","sources":["../../src/core/grep-pattern.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC;;GAEG;AACH,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;AAElD;;GAEG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,OAAO,GAAG,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,MAAM,EAAE,SAAS,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,OAAO,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC;AACnD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAiB;IACnD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QAChC,MAAM,KAAK,GAAG,sBAAsB,CAAC,EAAE,CAAC,CAAC;QACzC,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,6CAA6C;IAC7C,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAAoC;IAEpC,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAE5C,KAAK,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/D,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC9D,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,OAAO,CAAC,MAAM,GAAG,uBAAuB,CAAC;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAiB;IACvD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QAChC,MAAM,KAAK,GAAG,sBAAsB,CAAC,EAAE,CAAC,CAAC;QACzC,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAiB;IAIrD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC9C,CAAC;IAED,MAAM,OAAO,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAE7C,IAAI,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,QAAQ,EAAE,MAAM;YAChB,OAAO,EAAE,uBAAuB,CAAC,OAAO,CAAC;SAC1C,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AACnD,CAAC"}
|