testdossier 0.1.1 → 0.1.2
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 +26 -4
- package/lib/testdossier.mjs +134 -18
- package/package.json +2 -7
package/README.md
CHANGED
|
@@ -1,21 +1,43 @@
|
|
|
1
1
|
# TestDossier CLI
|
|
2
2
|
|
|
3
|
-
Upload a completed test report from a developer laptop, test machine, or CI
|
|
3
|
+
Upload a completed test report from a developer laptop, test machine, or CI
|
|
4
|
+
job. Run these steps in the project containing your automated tests:
|
|
4
5
|
|
|
5
|
-
Create `.env.testdossier
|
|
6
|
+
1. Create `.env.testdossier`:
|
|
6
7
|
|
|
7
8
|
```dotenv
|
|
8
9
|
TD_CI_TOKEN=replace_with_your_td_token
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
Replace the placeholder with the CI-ingestion token shown once by
|
|
12
|
-
|
|
12
|
+
2. Replace the placeholder with the CI-ingestion token shown once by
|
|
13
|
+
TestDossier, add the file to `.gitignore`, and protect it:
|
|
13
14
|
|
|
14
15
|
```bash
|
|
15
16
|
chmod 600 .env.testdossier
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. Run your tests so they create a supported report. For example, with
|
|
20
|
+
Playwright's JSON reporter configured to write `playwright-results.json`:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx playwright test
|
|
24
|
+
test -f playwright-results.json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
4. Upload the completed report:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
16
30
|
npx testdossier upload playwright-results.json --env-file .env.testdossier
|
|
17
31
|
```
|
|
18
32
|
|
|
33
|
+
`playwright-results.json` is an example filename, not a file supplied by
|
|
34
|
+
TestDossier. Use the actual report path produced by your runner. Repeat steps 3
|
|
35
|
+
and 4 whenever you want to publish a new run.
|
|
36
|
+
If tests run in multiple shards or batches, merge all shard reports before
|
|
37
|
+
uploading; selecting one shard's file uploads only that shard.
|
|
38
|
+
After merging, the CLI automatically sends Playwright and generic JSON reports
|
|
39
|
+
larger than 200 tests as sequential pages under one stable Test Run.
|
|
40
|
+
|
|
19
41
|
The CLI auto-detects Playwright JSON, Cypress JSON, Cucumber JSON, JUnit XML,
|
|
20
42
|
and TestDossier's generic JSON format. It has zero runtime dependencies and
|
|
21
43
|
requires Node.js 18 or newer.
|
package/lib/testdossier.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
export const VERSION = "0.1.
|
|
4
|
+
export const VERSION = "0.1.2";
|
|
5
5
|
const DEFAULT_ORIGIN = "https://testdossier.com";
|
|
6
6
|
const MAX_REPORT_BYTES = 25 * 1024 * 1024;
|
|
7
|
+
const MAX_TESTS_PER_PAGE = 200;
|
|
7
8
|
const RETRYABLE_STATUSES = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
|
|
8
9
|
const FORMAT_LABELS = {
|
|
9
10
|
cypress: "Cypress JSON",
|
|
@@ -84,6 +85,76 @@ export function detectReportFormat(text) {
|
|
|
84
85
|
return matches[0];
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
function playwrightTestNodes(suites, found = []) {
|
|
89
|
+
for (const suite of Array.isArray(suites) ? suites : []) {
|
|
90
|
+
if (!suite || typeof suite !== "object") continue;
|
|
91
|
+
for (const spec of Array.isArray(suite.specs) ? suite.specs : []) {
|
|
92
|
+
if (!spec || typeof spec !== "object") continue;
|
|
93
|
+
for (const test of Array.isArray(spec.tests) ? spec.tests : []) {
|
|
94
|
+
found.push(test);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
playwrightTestNodes(suite.suites, found);
|
|
98
|
+
}
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function selectedPlaywrightSuite(suite, selectedTests) {
|
|
103
|
+
if (!suite || typeof suite !== "object") return null;
|
|
104
|
+
const next = { ...suite };
|
|
105
|
+
next.specs = (Array.isArray(suite.specs) ? suite.specs : [])
|
|
106
|
+
.filter((spec) => spec && typeof spec === "object")
|
|
107
|
+
.map((spec) => ({
|
|
108
|
+
...spec,
|
|
109
|
+
tests: (Array.isArray(spec.tests) ? spec.tests : []).filter((test) => selectedTests.has(test)),
|
|
110
|
+
}))
|
|
111
|
+
.filter((spec) => spec.tests.length > 0);
|
|
112
|
+
next.suites = (Array.isArray(suite.suites) ? suite.suites : [])
|
|
113
|
+
.map((child) => selectedPlaywrightSuite(child, selectedTests))
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
return next.specs.length > 0 || next.suites.length > 0 ? next : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function splitReportPages(format, report, maxTests = MAX_TESTS_PER_PAGE) {
|
|
119
|
+
if (!Number.isInteger(maxTests) || maxTests < 1 || maxTests > MAX_TESTS_PER_PAGE) {
|
|
120
|
+
throw new CliError(`page size must be an integer from 1 to ${MAX_TESTS_PER_PAGE}.`);
|
|
121
|
+
}
|
|
122
|
+
if (format !== "playwright" && format !== "generic") {
|
|
123
|
+
return [{ body: report, tests: null }];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const document = parseJson(report.toString("utf8").replace(/^\uFEFF/, ""));
|
|
127
|
+
if (format === "generic") {
|
|
128
|
+
const tests = document.tests;
|
|
129
|
+
if (tests.length <= maxTests) return [{ body: report, tests: tests.length }];
|
|
130
|
+
const pages = [];
|
|
131
|
+
for (let start = 0; start < tests.length; start += maxTests) {
|
|
132
|
+
const slice = tests.slice(start, start + maxTests);
|
|
133
|
+
pages.push({
|
|
134
|
+
body: Buffer.from(JSON.stringify({ ...document, tests: slice })),
|
|
135
|
+
tests: slice.length,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return pages;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const tests = playwrightTestNodes(document.suites);
|
|
142
|
+
if (tests.length <= maxTests) return [{ body: report, tests: tests.length }];
|
|
143
|
+
const pages = [];
|
|
144
|
+
for (let start = 0; start < tests.length; start += maxTests) {
|
|
145
|
+
const slice = tests.slice(start, start + maxTests);
|
|
146
|
+
const selected = new Set(slice);
|
|
147
|
+
const suites = document.suites
|
|
148
|
+
.map((suite) => selectedPlaywrightSuite(suite, selected))
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
pages.push({
|
|
151
|
+
body: Buffer.from(JSON.stringify({ ...document, suites })),
|
|
152
|
+
tests: slice.length,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return pages;
|
|
156
|
+
}
|
|
157
|
+
|
|
87
158
|
function isLoopback(hostname) {
|
|
88
159
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
89
160
|
}
|
|
@@ -350,6 +421,10 @@ Authentication:
|
|
|
350
421
|
Tokens are intentionally not accepted as command-line arguments, keeping
|
|
351
422
|
them out of shell history and process listings.
|
|
352
423
|
|
|
424
|
+
Large reports:
|
|
425
|
+
Playwright and generic JSON reports above 200 tests are automatically sent
|
|
426
|
+
as sequential pages that remain grouped under one TestDossier run.
|
|
427
|
+
|
|
353
428
|
Examples:
|
|
354
429
|
npx testdossier upload playwright-results.json --env-file .env.testdossier
|
|
355
430
|
npx testdossier upload junit-results.xml --env-file .env.testdossier --build abc123
|
|
@@ -369,6 +444,21 @@ function outputSummary(result, stdout, stderr) {
|
|
|
369
444
|
}
|
|
370
445
|
}
|
|
371
446
|
|
|
447
|
+
function aggregatePageResults(results) {
|
|
448
|
+
const aggregate = { counts: {}, warnings: [] };
|
|
449
|
+
for (const result of results) {
|
|
450
|
+
if (result && result.counts && typeof result.counts === "object") {
|
|
451
|
+
for (const [name, value] of Object.entries(result.counts)) {
|
|
452
|
+
if (typeof value === "number") {
|
|
453
|
+
aggregate.counts[name] = (aggregate.counts[name] || 0) + value;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (Array.isArray(result && result.warnings)) aggregate.warnings.push(...result.warnings);
|
|
458
|
+
}
|
|
459
|
+
return aggregate;
|
|
460
|
+
}
|
|
461
|
+
|
|
372
462
|
export async function runCli(
|
|
373
463
|
args,
|
|
374
464
|
{
|
|
@@ -410,10 +500,20 @@ export async function runCli(
|
|
|
410
500
|
const reportText = report.toString("utf8");
|
|
411
501
|
const format = options.format === "auto" ? detectReportFormat(reportText) : options.format;
|
|
412
502
|
assertFormatShape(format, reportText);
|
|
503
|
+
const pages = splitReportPages(format, report);
|
|
504
|
+
const testCount = pages.every((page) => typeof page.tests === "number")
|
|
505
|
+
? pages.reduce((sum, page) => sum + page.tests, 0)
|
|
506
|
+
: null;
|
|
413
507
|
|
|
414
508
|
const endpoint = normalizeEndpoint(options.url || env.TD_URL || DEFAULT_ORIGIN);
|
|
415
509
|
const destination = new URL(endpoint).origin;
|
|
416
|
-
stdout(
|
|
510
|
+
stdout(
|
|
511
|
+
`Validated ${FORMAT_LABELS[format]} (${report.byteLength} bytes` +
|
|
512
|
+
(testCount == null
|
|
513
|
+
? ""
|
|
514
|
+
: `, ${testCount} tests${pages.length > 1 ? ` across ${pages.length} pages` : ""}`) +
|
|
515
|
+
").",
|
|
516
|
+
);
|
|
417
517
|
if (options.dryRun) {
|
|
418
518
|
stdout(`Dry run complete; no data was sent to ${destination}.`);
|
|
419
519
|
return 0;
|
|
@@ -428,29 +528,45 @@ export async function runCli(
|
|
|
428
528
|
if (typeof fetchImpl !== "function") throw new CliError("Node 18 or newer is required (global fetch is unavailable).");
|
|
429
529
|
|
|
430
530
|
const metadata = inferredMetadata(options, env);
|
|
431
|
-
const
|
|
531
|
+
const baseHeaders = {
|
|
432
532
|
Authorization: `Bearer ${token}`,
|
|
433
533
|
"Content-Type": format === "junit" ? "application/xml" : "application/json",
|
|
434
534
|
"User-Agent": `testdossier-cli/${VERSION}`,
|
|
435
535
|
"X-CI-Run-Id": metadata.runId,
|
|
436
|
-
"X-CI-Page-Id": "results",
|
|
437
536
|
"X-CI-Provider": metadata.provider,
|
|
438
537
|
};
|
|
439
|
-
if (format !== "junit" && format !== "generic")
|
|
440
|
-
if (metadata.build)
|
|
441
|
-
if (metadata.branch)
|
|
442
|
-
if (metadata.ciUrl)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
538
|
+
if (format !== "junit" && format !== "generic") baseHeaders["X-CI-Format"] = format;
|
|
539
|
+
if (metadata.build) baseHeaders["X-Build"] = metadata.build;
|
|
540
|
+
if (metadata.branch) baseHeaders["X-Branch"] = metadata.branch;
|
|
541
|
+
if (metadata.ciUrl) baseHeaders["X-CI-URL"] = metadata.ciUrl;
|
|
542
|
+
|
|
543
|
+
if (pages.length > 1) {
|
|
544
|
+
stdout(
|
|
545
|
+
`Uploading ${pages.reduce((sum, page) => sum + (page.tests || 0), 0)} tests in ` +
|
|
546
|
+
`${pages.length} pages (run ${metadata.runId}).`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const pageResults = [];
|
|
550
|
+
for (let index = 0; index < pages.length; index += 1) {
|
|
551
|
+
const page = pages[index];
|
|
552
|
+
const result = await postWithRetry({
|
|
553
|
+
endpoint,
|
|
554
|
+
headers: {
|
|
555
|
+
...baseHeaders,
|
|
556
|
+
"X-CI-Page-Id": pages.length === 1 ? "results" : String(index + 1),
|
|
557
|
+
},
|
|
558
|
+
body: page.body,
|
|
559
|
+
env,
|
|
560
|
+
fetchImpl,
|
|
561
|
+
stderr,
|
|
562
|
+
});
|
|
563
|
+
pageResults.push(result);
|
|
564
|
+
if (pages.length > 1) {
|
|
565
|
+
stdout(`Uploaded page ${index + 1}/${pages.length} (${page.tests} tests).`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
452
568
|
stdout(`Uploaded ${FORMAT_LABELS[format]} to ${destination} (run ${metadata.runId}).`);
|
|
453
|
-
outputSummary(
|
|
569
|
+
outputSummary(aggregatePageResults(pageResults), stdout, stderr);
|
|
454
570
|
return 0;
|
|
455
571
|
} catch (error) {
|
|
456
572
|
const message = error && error.message ? error.message : String(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdossier",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Upload completed test reports to TestDossier from a local machine or CI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,14 +15,9 @@
|
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=18"
|
|
17
17
|
},
|
|
18
|
-
"repository": {
|
|
19
|
-
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/devkalu/test-dossier.git",
|
|
21
|
-
"directory": "cli"
|
|
22
|
-
},
|
|
23
18
|
"homepage": "https://testdossier.com",
|
|
24
19
|
"bugs": {
|
|
25
|
-
"
|
|
20
|
+
"email": "support@testdossier.com"
|
|
26
21
|
},
|
|
27
22
|
"keywords": [
|
|
28
23
|
"testing",
|