@piwitests/reporter 0.14.0 → 0.16.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 +46 -0
- package/dist/global-setup-module.js +7 -2
- package/dist/index.d.ts +19 -0
- package/dist/index.js +146 -29
- package/dist/internal/capture/capture-fixtures.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ export default defineConfig({
|
|
|
92
92
|
| `collectScmInfo` | boolean | `true` | Auto-collect git commit, branch, author |
|
|
93
93
|
| `collectCiInfo` | boolean | `true` | Auto-collect CI environment info |
|
|
94
94
|
| `collectPerformanceMetrics` | boolean | `true` | Collect step timings, network requests and web vitals from the fixture |
|
|
95
|
+
| `outputFile` | string | — | Write a JSON file with the run URL/id/status so CI can consume it (see below) |
|
|
95
96
|
| `apiKey` | string | — | API key for authentication (preferred for CI) |
|
|
96
97
|
| `username` | string | — | Username for dashboard login (use `apiKey` instead when possible) |
|
|
97
98
|
| `password` | string | — | Password for dashboard login (used with `username`) |
|
|
@@ -209,6 +210,51 @@ When `collectCiInfo` is enabled (default), the reporter auto-detects:
|
|
|
209
210
|
- **Travis CI** — build number/URL, job number
|
|
210
211
|
- **Azure Pipelines** — build number, build ID/URL, job name
|
|
211
212
|
|
|
213
|
+
## Publishing the run URL to CI
|
|
214
|
+
|
|
215
|
+
After a run is submitted, the reporter surfaces the dashboard run URL so a later
|
|
216
|
+
CI step (a custom email, a Slack message, a deploy gate) can pick it up without
|
|
217
|
+
scraping the log. The URL is always printed as `View run: <url>`, and in
|
|
218
|
+
addition:
|
|
219
|
+
|
|
220
|
+
- **Any CI — JSON output file.** Set `outputFile` (or `PIWI_OUTPUT_FILE`) and the
|
|
221
|
+
reporter writes a small JSON file when the run lands:
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{ "runUrl": "https://piwi.example.com/test-runs/1234", "runId": 1234, "projectId": 5, "projectName": "checkout", "status": "passed", "ciBuildUrl": "https://ci.example.com/build/9" }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Read it from any pipeline, e.g. `node -e "console.log(require('./piwi-run.json').runUrl)"`
|
|
228
|
+
(portable) or `cat piwi-run.json` and parse it in your email step. In Jenkins,
|
|
229
|
+
`def run = readJSON file: 'piwi-run.json'` then use `run.runUrl`.
|
|
230
|
+
|
|
231
|
+
- **GitHub Actions (automatic).** When `GITHUB_ACTIONS` is set, the reporter
|
|
232
|
+
appends step outputs to `$GITHUB_OUTPUT` (`piwi_run_url`, `piwi_run_id`,
|
|
233
|
+
`piwi_project_id`, `piwi_run_status`), writes a markdown link to the job
|
|
234
|
+
summary, and prints a `::notice::` annotation. Give the test step an `id` and a
|
|
235
|
+
downstream step can read it:
|
|
236
|
+
|
|
237
|
+
```yaml
|
|
238
|
+
- id: tests
|
|
239
|
+
run: npx playwright test
|
|
240
|
+
- run: echo "Results at ${{ steps.tests.outputs.piwi_run_url }}"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- **GitLab CI (automatic).** When `GITLAB_CI` is set, the reporter writes a
|
|
244
|
+
dotenv report (`piwi.env` by default, override with `PIWI_DOTENV_FILE`).
|
|
245
|
+
Declare it so later jobs inherit `$PIWI_RUN_URL`:
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
test:
|
|
249
|
+
script: npx playwright test
|
|
250
|
+
artifacts:
|
|
251
|
+
reports:
|
|
252
|
+
dotenv: piwi.env
|
|
253
|
+
email:
|
|
254
|
+
needs: [test]
|
|
255
|
+
script: ./send-email.sh "$PIWI_RUN_URL"
|
|
256
|
+
```
|
|
257
|
+
|
|
212
258
|
## How It Works
|
|
213
259
|
|
|
214
260
|
1. When tests start, the reporter creates a run on the server (streaming mode) or collects results locally (batch mode)
|
|
@@ -57,6 +57,7 @@ var DEFAULTS = {
|
|
|
57
57
|
collectPerformanceMetrics: true,
|
|
58
58
|
captureLocators: true,
|
|
59
59
|
capturePageState: true,
|
|
60
|
+
captureServerTraces: true,
|
|
60
61
|
streaming: true,
|
|
61
62
|
streamingBatchSize: 5,
|
|
62
63
|
streamingBatchDelay: 2e3,
|
|
@@ -83,8 +84,10 @@ var PIWI_ENV_KEYS = {
|
|
|
83
84
|
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
84
85
|
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
85
86
|
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
87
|
+
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
86
88
|
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
87
|
-
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL"
|
|
89
|
+
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
90
|
+
outputFile: "PIWI_OUTPUT_FILE"
|
|
88
91
|
};
|
|
89
92
|
function readBool(val) {
|
|
90
93
|
if (val === void 0) return void 0;
|
|
@@ -107,8 +110,10 @@ var ENV_FALLBACK_SPECS = [
|
|
|
107
110
|
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
108
111
|
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
109
112
|
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
113
|
+
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
110
114
|
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
111
|
-
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" }
|
|
115
|
+
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
116
|
+
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
112
117
|
];
|
|
113
118
|
function resolveOptions(raw) {
|
|
114
119
|
const env = process.env;
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,16 @@ interface PiwiDashboardOptions extends PlaywrightTestConfig {
|
|
|
41
41
|
* also be forced off with `PIWI_CAPTURE_PAGE_STATE=false`.
|
|
42
42
|
*/
|
|
43
43
|
capturePageState?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Capture server-side spans for each API/document request the test makes,
|
|
46
|
+
* read from the `X-Piwi-Trace` response header emitted by a Piwi
|
|
47
|
+
* instrumentation plugin (e.g. `@piwitests/instrumentation`). The spans show
|
|
48
|
+
* up next to the network request in the dashboard and feed AI diagnosis. Free
|
|
49
|
+
* when no instrumentation is present (the header is simply absent). Defaults
|
|
50
|
+
* to `true`; automatically disabled when `collectPerformanceMetrics` is
|
|
51
|
+
* `false`. Can also be forced off with `PIWI_CAPTURE_SERVER_TRACES=false`.
|
|
52
|
+
*/
|
|
53
|
+
captureServerTraces?: boolean;
|
|
44
54
|
/**
|
|
45
55
|
* Open Piwi's own failure-time overlay on the failing page — for inspecting
|
|
46
56
|
* the page and picking a locator for any element (click an element → confirm
|
|
@@ -97,6 +107,15 @@ interface PiwiDashboardOptions extends PlaywrightTestConfig {
|
|
|
97
107
|
tags?: string[];
|
|
98
108
|
/** Additional custom metadata as key-value pairs */
|
|
99
109
|
customData?: Record<string, unknown>;
|
|
110
|
+
/**
|
|
111
|
+
* Write a JSON file with the submitted run's dashboard URL, id, project id and
|
|
112
|
+
* status after the run lands, so a CI pipeline can consume it (e.g. feed the
|
|
113
|
+
* run URL into a custom email step). Any CI can read the file. GitHub Actions
|
|
114
|
+
* step outputs / job summary and GitLab dotenv reports are emitted
|
|
115
|
+
* automatically when running under those systems, regardless of this option.
|
|
116
|
+
* Can also be set with `PIWI_OUTPUT_FILE`.
|
|
117
|
+
*/
|
|
118
|
+
outputFile?: string;
|
|
100
119
|
/** Enable verbose logging for debugging. Defaults to `false`. */
|
|
101
120
|
verbose?: boolean;
|
|
102
121
|
}
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ __export(index_exports, {
|
|
|
40
40
|
module.exports = __toCommonJS(index_exports);
|
|
41
41
|
|
|
42
42
|
// src/public/reporter.ts
|
|
43
|
-
var
|
|
43
|
+
var path13 = __toESM(require("path"));
|
|
44
44
|
|
|
45
45
|
// src/internal/config/env.ts
|
|
46
46
|
var DEFAULTS = {
|
|
@@ -53,6 +53,7 @@ var DEFAULTS = {
|
|
|
53
53
|
collectPerformanceMetrics: true,
|
|
54
54
|
captureLocators: true,
|
|
55
55
|
capturePageState: true,
|
|
56
|
+
captureServerTraces: true,
|
|
56
57
|
streaming: true,
|
|
57
58
|
streamingBatchSize: 5,
|
|
58
59
|
streamingBatchDelay: 2e3,
|
|
@@ -79,8 +80,10 @@ var PIWI_ENV_KEYS = {
|
|
|
79
80
|
uploadReport: "PIWI_UPLOAD_REPORT",
|
|
80
81
|
captureLocators: "PIWI_CAPTURE_LOCATORS",
|
|
81
82
|
capturePageState: "PIWI_CAPTURE_PAGE_STATE",
|
|
83
|
+
captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
|
|
82
84
|
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
83
|
-
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL"
|
|
85
|
+
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
86
|
+
outputFile: "PIWI_OUTPUT_FILE"
|
|
84
87
|
};
|
|
85
88
|
function readBool(val) {
|
|
86
89
|
if (val === void 0) return void 0;
|
|
@@ -103,8 +106,10 @@ var ENV_FALLBACK_SPECS = [
|
|
|
103
106
|
{ option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
|
|
104
107
|
{ option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
|
|
105
108
|
{ option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
|
|
109
|
+
{ option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
|
|
106
110
|
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
107
|
-
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" }
|
|
111
|
+
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
112
|
+
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
108
113
|
];
|
|
109
114
|
function resolveOptions(raw) {
|
|
110
115
|
const env = process.env;
|
|
@@ -139,6 +144,9 @@ function applyOptionsToEnv(options) {
|
|
|
139
144
|
if (options.capturePageState === false || options.collectPerformanceMetrics === false)
|
|
140
145
|
env[PIWI_ENV_KEYS.capturePageState] = "false";
|
|
141
146
|
else if (options.capturePageState === true) env[PIWI_ENV_KEYS.capturePageState] = "true";
|
|
147
|
+
if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
|
|
148
|
+
env[PIWI_ENV_KEYS.captureServerTraces] = "false";
|
|
149
|
+
else if (options.captureServerTraces === true) env[PIWI_ENV_KEYS.captureServerTraces] = "true";
|
|
142
150
|
if (options.inspectOnFailure !== void 0) env[PIWI_ENV_KEYS.inspectOnFailure] = String(options.inspectOnFailure);
|
|
143
151
|
if (options.pickLocatorOnFailure !== void 0)
|
|
144
152
|
env[PIWI_ENV_KEYS.pickLocatorOnFailure] = String(options.pickLocatorOnFailure);
|
|
@@ -353,6 +361,7 @@ function toWireTestCase(tc) {
|
|
|
353
361
|
location: rest.location,
|
|
354
362
|
status: rest.status,
|
|
355
363
|
duration: rest.duration,
|
|
364
|
+
timeout: rest.timeout ?? null,
|
|
356
365
|
error: rest.error,
|
|
357
366
|
retries: rest.retries,
|
|
358
367
|
workerIndex: rest.workerIndex ?? null,
|
|
@@ -407,11 +416,6 @@ function serializeRun(payload, opts) {
|
|
|
407
416
|
return body;
|
|
408
417
|
}
|
|
409
418
|
|
|
410
|
-
// src/internal/support/run-url.ts
|
|
411
|
-
function runUrl(serverUrl, runId) {
|
|
412
|
-
return `${serverUrl.replace(/\/+$/, "")}/test-runs/${runId}`;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
419
|
// src/internal/submit/uploader.ts
|
|
416
420
|
var Uploader = class {
|
|
417
421
|
/**
|
|
@@ -434,7 +438,6 @@ var Uploader = class {
|
|
|
434
438
|
this.logger.info(`Successfully uploaded test results`);
|
|
435
439
|
if (response.testRunId) {
|
|
436
440
|
this.logger.info(`Test Run ID: ${response.testRunId}, Project ID: ${response.projectId}`);
|
|
437
|
-
this.logger.info(`View run: ${runUrl(this.httpClient.baseUrl, response.testRunId)}`);
|
|
438
441
|
}
|
|
439
442
|
return response;
|
|
440
443
|
}
|
|
@@ -450,7 +453,6 @@ var Uploader = class {
|
|
|
450
453
|
this.logger.info(`Successfully uploaded test results with files`);
|
|
451
454
|
if (response.testRunId) {
|
|
452
455
|
this.logger.info(`Test Run ID: ${response.testRunId}, Project ID: ${response.projectId}`);
|
|
453
|
-
this.logger.info(`View run: ${runUrl(this.httpClient.baseUrl, response.testRunId)}`);
|
|
454
456
|
}
|
|
455
457
|
if (response.reports) {
|
|
456
458
|
for (const r of response.reports) this.logger.info(`${r.label}: ${r.path}`);
|
|
@@ -1157,6 +1159,11 @@ function readSetupInfo(projectName) {
|
|
|
1157
1159
|
return null;
|
|
1158
1160
|
}
|
|
1159
1161
|
|
|
1162
|
+
// src/internal/support/run-url.ts
|
|
1163
|
+
function runUrl(serverUrl, runId) {
|
|
1164
|
+
return `${serverUrl.replace(/\/+$/, "")}/test-runs/${runId}`;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1160
1167
|
// src/internal/streaming/stream-manager.ts
|
|
1161
1168
|
var StreamManager = class {
|
|
1162
1169
|
/**
|
|
@@ -2001,6 +2008,90 @@ function buildErrorText(result) {
|
|
|
2001
2008
|
return text;
|
|
2002
2009
|
}
|
|
2003
2010
|
|
|
2011
|
+
// src/internal/support/ci-output.ts
|
|
2012
|
+
var fs12 = __toESM(require("fs"));
|
|
2013
|
+
var path12 = __toESM(require("path"));
|
|
2014
|
+
function emitRunOutputs(output, logger, outputFile, env = process.env) {
|
|
2015
|
+
logger.info(`View run: ${output.runUrl}`);
|
|
2016
|
+
if (outputFile) writeOutputFile(outputFile, output, logger);
|
|
2017
|
+
if (env.GITHUB_ACTIONS) emitGitHubActions(output, env, logger);
|
|
2018
|
+
if (env.GITLAB_CI) emitGitLabDotenv(output, env, logger);
|
|
2019
|
+
}
|
|
2020
|
+
function writeOutputFile(file, output, logger) {
|
|
2021
|
+
try {
|
|
2022
|
+
const dir = path12.dirname(file);
|
|
2023
|
+
if (dir && dir !== ".") fs12.mkdirSync(dir, { recursive: true });
|
|
2024
|
+
fs12.writeFileSync(
|
|
2025
|
+
file,
|
|
2026
|
+
JSON.stringify(
|
|
2027
|
+
{
|
|
2028
|
+
runUrl: output.runUrl,
|
|
2029
|
+
runId: output.runId,
|
|
2030
|
+
projectId: output.projectId ?? null,
|
|
2031
|
+
projectName: output.projectName,
|
|
2032
|
+
status: output.status,
|
|
2033
|
+
ciBuildUrl: output.ciBuildUrl ?? null
|
|
2034
|
+
},
|
|
2035
|
+
null,
|
|
2036
|
+
2
|
|
2037
|
+
) + "\n"
|
|
2038
|
+
);
|
|
2039
|
+
logger.info(`Wrote run output to ${file}`);
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
logger.warn(`Failed to write run output file '${file}': ${errorMessage(error)}`);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function emitGitHubActions(output, env, logger) {
|
|
2045
|
+
const pairs = [
|
|
2046
|
+
["piwi_run_url", output.runUrl],
|
|
2047
|
+
["piwi_run_id", String(output.runId)],
|
|
2048
|
+
["piwi_run_status", output.status]
|
|
2049
|
+
];
|
|
2050
|
+
if (output.projectId != null) pairs.push(["piwi_project_id", String(output.projectId)]);
|
|
2051
|
+
if (env.GITHUB_OUTPUT) {
|
|
2052
|
+
appendFileLines(
|
|
2053
|
+
env.GITHUB_OUTPUT,
|
|
2054
|
+
pairs.map(([k, v]) => `${k}=${v}`),
|
|
2055
|
+
logger,
|
|
2056
|
+
"step output"
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
if (env.GITHUB_STEP_SUMMARY) {
|
|
2060
|
+
appendFileLines(
|
|
2061
|
+
env.GITHUB_STEP_SUMMARY,
|
|
2062
|
+
["### Piwi test run", "", `[View run](${output.runUrl}) \u2014 **${output.status}**`, ""],
|
|
2063
|
+
logger,
|
|
2064
|
+
"step summary"
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
process.stdout.write(`::notice title=Piwi test run::${output.runUrl}
|
|
2068
|
+
`);
|
|
2069
|
+
}
|
|
2070
|
+
function emitGitLabDotenv(output, env, logger) {
|
|
2071
|
+
const file = env.PIWI_DOTENV_FILE || "piwi.env";
|
|
2072
|
+
const lines = [`PIWI_RUN_URL=${output.runUrl}`, `PIWI_RUN_ID=${output.runId}`, `PIWI_RUN_STATUS=${output.status}`];
|
|
2073
|
+
if (output.projectId != null) lines.push(`PIWI_PROJECT_ID=${output.projectId}`);
|
|
2074
|
+
if (output.ciBuildUrl) lines.push(`PIWI_CI_BUILD_URL=${output.ciBuildUrl}`);
|
|
2075
|
+
try {
|
|
2076
|
+
fs12.writeFileSync(file, lines.join("\n") + "\n");
|
|
2077
|
+
logger.info(`Wrote GitLab dotenv report to ${file} (declare it as artifacts:reports:dotenv)`);
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
function appendFileLines(file, lines, logger, label) {
|
|
2083
|
+
try {
|
|
2084
|
+
fs12.appendFileSync(file, lines.join("\n") + "\n");
|
|
2085
|
+
} catch (error) {
|
|
2086
|
+
logger.warn(`Failed to write GitHub Actions ${label}: ${errorMessage(error)}`);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
function ciBuildUrlFromMetadata(metadata) {
|
|
2090
|
+
const ci = metadata?.ci;
|
|
2091
|
+
if (!ci) return void 0;
|
|
2092
|
+
return ci.buildUrl || ci.pipelineUrl || ci.jobUrl || void 0;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2004
2095
|
// src/internal/submit/run-submitter.ts
|
|
2005
2096
|
var RunSubmitter = class {
|
|
2006
2097
|
/**
|
|
@@ -2045,13 +2136,29 @@ var RunSubmitter = class {
|
|
|
2045
2136
|
this.logger.error(`Authentication failed: ${errorMessage(error)}`);
|
|
2046
2137
|
throw error;
|
|
2047
2138
|
}
|
|
2139
|
+
let outcome = { done: false, output: null };
|
|
2048
2140
|
if (sm?.enabled && sm?.runId != null) {
|
|
2049
|
-
|
|
2141
|
+
outcome = await this.tryFinishStreaming(run, overallStatus, duration, auth);
|
|
2142
|
+
}
|
|
2143
|
+
if (!outcome.done && (this.hasReports(run) || run.options.uploadTraces)) {
|
|
2144
|
+
outcome = await this.tryUploadWithFiles(run, overallStatus, duration, auth);
|
|
2050
2145
|
}
|
|
2051
|
-
if (
|
|
2052
|
-
|
|
2146
|
+
if (!outcome.done) {
|
|
2147
|
+
outcome = await this.tryUploadJSON(run, overallStatus, duration, auth);
|
|
2053
2148
|
}
|
|
2054
|
-
|
|
2149
|
+
if (outcome.output) emitRunOutputs(outcome.output, this.logger, run.options.outputFile);
|
|
2150
|
+
}
|
|
2151
|
+
/** Assemble a CI-facing run output, or `null` when the server returned no run id. */
|
|
2152
|
+
buildOutput(runId, projectId, run, status) {
|
|
2153
|
+
if (runId == null) return null;
|
|
2154
|
+
return {
|
|
2155
|
+
runUrl: runUrl(this.httpClient.baseUrl, runId),
|
|
2156
|
+
runId,
|
|
2157
|
+
projectId,
|
|
2158
|
+
projectName: run.options.projectName,
|
|
2159
|
+
status,
|
|
2160
|
+
ciBuildUrl: ciBuildUrlFromMetadata(run.metadata)
|
|
2161
|
+
};
|
|
2055
2162
|
}
|
|
2056
2163
|
hasReports(run) {
|
|
2057
2164
|
return !!run.options.uploadReport || (run.options.reports?.length ?? 0) > 0;
|
|
@@ -2122,9 +2229,6 @@ var RunSubmitter = class {
|
|
|
2122
2229
|
}
|
|
2123
2230
|
await this.httpClient.postJSON(`/api/test-runs/${sm.runId}/finish`, finishBody, auth);
|
|
2124
2231
|
this.logger.info(`Successfully finalized streaming run #${sm.runId}`);
|
|
2125
|
-
if (run.options.serverUrl) {
|
|
2126
|
-
this.logger.info(`View run: ${runUrl(run.options.serverUrl, sm.runId)}`);
|
|
2127
|
-
}
|
|
2128
2232
|
this.recovery.clear();
|
|
2129
2233
|
if (this.hasReports(run)) {
|
|
2130
2234
|
try {
|
|
@@ -2139,22 +2243,22 @@ var RunSubmitter = class {
|
|
|
2139
2243
|
this.logger.warn(`Failed to upload reports for streaming run: ${errorMessage(error)}`);
|
|
2140
2244
|
}
|
|
2141
2245
|
}
|
|
2142
|
-
return true;
|
|
2246
|
+
return { done: true, output: this.buildOutput(sm.runId, void 0, run, overallStatus) };
|
|
2143
2247
|
} catch (error) {
|
|
2144
2248
|
this.logger.warn(`Failed to finalize streaming run: ${errorMessage(error)}`);
|
|
2145
2249
|
this.logger.info("Falling back to batch upload...");
|
|
2146
|
-
return false;
|
|
2250
|
+
return { done: false, output: null };
|
|
2147
2251
|
}
|
|
2148
2252
|
}
|
|
2149
2253
|
async tryUploadWithFiles(run, overallStatus, duration, auth) {
|
|
2150
2254
|
try {
|
|
2151
|
-
await this.uploader.uploadWithFiles(
|
|
2255
|
+
const response = await this.uploader.uploadWithFiles(
|
|
2152
2256
|
this.buildRunPayload(run, overallStatus, duration),
|
|
2153
2257
|
this.reportOptions(run),
|
|
2154
2258
|
auth
|
|
2155
2259
|
);
|
|
2156
2260
|
this.recovery.clear();
|
|
2157
|
-
return true;
|
|
2261
|
+
return { done: true, output: this.buildOutput(response?.testRunId, response?.projectId, run, overallStatus) };
|
|
2158
2262
|
} catch (error) {
|
|
2159
2263
|
if (error instanceof HttpError && error.status === 401 && !auth) {
|
|
2160
2264
|
this.logAuthRequired(run.options.serverUrl);
|
|
@@ -2162,14 +2266,15 @@ var RunSubmitter = class {
|
|
|
2162
2266
|
}
|
|
2163
2267
|
this.logger.warn(`Failed to upload with files: ${errorMessage(error)}`);
|
|
2164
2268
|
this.logger.info("Falling back to JSON upload...");
|
|
2165
|
-
return false;
|
|
2269
|
+
return { done: false, output: null };
|
|
2166
2270
|
}
|
|
2167
2271
|
}
|
|
2168
2272
|
async tryUploadJSON(run, overallStatus, duration, auth) {
|
|
2169
2273
|
const payload = this.buildRunPayload(run, overallStatus, duration);
|
|
2170
2274
|
try {
|
|
2171
|
-
await this.uploader.uploadJSON(payload, auth);
|
|
2275
|
+
const response = await this.uploader.uploadJSON(payload, auth);
|
|
2172
2276
|
this.recovery.clear();
|
|
2277
|
+
return { done: true, output: this.buildOutput(response?.testRunId, response?.projectId, run, overallStatus) };
|
|
2173
2278
|
} catch (error) {
|
|
2174
2279
|
if (error instanceof HttpError && error.status === 401 && !auth) {
|
|
2175
2280
|
this.logAuthRequired(run.options.serverUrl);
|
|
@@ -2180,6 +2285,7 @@ var RunSubmitter = class {
|
|
|
2180
2285
|
`Saved a local recovery copy \u2014 it will be uploaded automatically on your next test run. If this keeps happening, check that serverUrl (${run.options.serverUrl ?? "not set"}) is correct and reachable.`
|
|
2181
2286
|
);
|
|
2182
2287
|
this.recovery.save(serializeRun(payload, { includeTestCases: true }));
|
|
2288
|
+
return { done: true, output: null };
|
|
2183
2289
|
}
|
|
2184
2290
|
}
|
|
2185
2291
|
/** Log one actionable line explaining how to fix a 401 caused by a missing credential. */
|
|
@@ -2192,7 +2298,7 @@ var RunSubmitter = class {
|
|
|
2192
2298
|
|
|
2193
2299
|
// src/public/reporter.ts
|
|
2194
2300
|
function testLocation(test) {
|
|
2195
|
-
const relativeFilePath =
|
|
2301
|
+
const relativeFilePath = path13.relative(process.cwd(), test.location.file).split(path13.sep).join("/");
|
|
2196
2302
|
return `${relativeFilePath}:${test.location.line}:${test.location.column}`;
|
|
2197
2303
|
}
|
|
2198
2304
|
var PiwiDashboardReporter = class {
|
|
@@ -2373,6 +2479,9 @@ var PiwiDashboardReporter = class {
|
|
|
2373
2479
|
location: testLocation(test),
|
|
2374
2480
|
status,
|
|
2375
2481
|
duration: result.duration,
|
|
2482
|
+
// Effective per-test timeout (reflects project config + describe-level
|
|
2483
|
+
// overrides). `0` means unbounded; kept as-is so the dashboard can flag it.
|
|
2484
|
+
timeout: test.timeout ?? null,
|
|
2376
2485
|
error: buildErrorText(result),
|
|
2377
2486
|
retries: result.retry,
|
|
2378
2487
|
workerIndex: workerIndexOf(result),
|
|
@@ -2448,6 +2557,7 @@ var PiwiDashboardReporter = class {
|
|
|
2448
2557
|
location: testLocation(test),
|
|
2449
2558
|
status: "didnotrun",
|
|
2450
2559
|
duration: 0,
|
|
2560
|
+
timeout: test.timeout ?? null,
|
|
2451
2561
|
error: null,
|
|
2452
2562
|
retries: 0,
|
|
2453
2563
|
workerIndex: null,
|
|
@@ -2500,7 +2610,7 @@ var PiwiDashboardReporter = class {
|
|
|
2500
2610
|
var import_node_zlib = require("zlib");
|
|
2501
2611
|
|
|
2502
2612
|
// src/internal/capture/locator-healing.ts
|
|
2503
|
-
var
|
|
2613
|
+
var path14 = __toESM(require("path"));
|
|
2504
2614
|
|
|
2505
2615
|
// ../packages/core/src/locator-fingerprint.ts
|
|
2506
2616
|
var PRESENT_SIMILARITY = 0.8;
|
|
@@ -3057,10 +3167,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
|
|
|
3057
3167
|
}
|
|
3058
3168
|
let rel = file;
|
|
3059
3169
|
try {
|
|
3060
|
-
rel =
|
|
3170
|
+
rel = path14.relative(process.cwd(), file);
|
|
3061
3171
|
} catch {
|
|
3062
3172
|
}
|
|
3063
|
-
rel = rel.split(
|
|
3173
|
+
rel = rel.split(path14.sep).join("/");
|
|
3064
3174
|
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
3065
3175
|
return `${rel}:${m[3]}:${m[4]}`;
|
|
3066
3176
|
}
|
|
@@ -3103,7 +3213,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
|
|
|
3103
3213
|
}
|
|
3104
3214
|
|
|
3105
3215
|
// src/internal/capture/pick-on-failure.ts
|
|
3106
|
-
var
|
|
3216
|
+
var path15 = __toESM(require("path"));
|
|
3107
3217
|
var ANSI_RE = /\[[0-9;]*m/g;
|
|
3108
3218
|
function endOfString(s, start) {
|
|
3109
3219
|
const q = s[start];
|
|
@@ -3197,7 +3307,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE, "");
|
|
|
3197
3307
|
const parsed = parseLeafLocatorExpression(line[1].trim());
|
|
3198
3308
|
if (!parsed) continue;
|
|
3199
3309
|
const loc = err.location;
|
|
3200
|
-
const location = loc ? `${
|
|
3310
|
+
const location = loc ? `${path15.relative(process.cwd(), loc.file).split(path15.sep).join("/")}:${loc.line}:${loc.column}` : null;
|
|
3201
3311
|
return { method: parsed.method, args: parsed.args, location };
|
|
3202
3312
|
}
|
|
3203
3313
|
return null;
|
|
@@ -4528,6 +4638,13 @@ function instrumentPage(page) {
|
|
|
4528
4638
|
} catch {
|
|
4529
4639
|
}
|
|
4530
4640
|
}
|
|
4641
|
+
const traceHeader = headers["x-piwi-trace"];
|
|
4642
|
+
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
4643
|
+
try {
|
|
4644
|
+
entry.serverTraces = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
4645
|
+
} catch {
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4531
4648
|
}
|
|
4532
4649
|
sink.networkRequests.push(entry);
|
|
4533
4650
|
} catch {
|
|
@@ -2085,6 +2085,13 @@ function instrumentPage(page) {
|
|
|
2085
2085
|
} catch {
|
|
2086
2086
|
}
|
|
2087
2087
|
}
|
|
2088
|
+
const traceHeader = headers["x-piwi-trace"];
|
|
2089
|
+
if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
|
|
2090
|
+
try {
|
|
2091
|
+
entry.serverTraces = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
|
|
2092
|
+
} catch {
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2088
2095
|
}
|
|
2089
2096
|
sink.networkRequests.push(entry);
|
|
2090
2097
|
} catch {
|
package/package.json
CHANGED