@reportforge/playwright-pdf 0.12.0 → 0.13.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 +5 -2
- package/dist/cli/index.js +5 -2
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +56 -6
- package/dist/index.mjs +52 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ Three templates, one reporter. Every PDF is fully offline — fonts, charts, and
|
|
|
55
55
|
- **CI/CD snippets** — GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins, Azure DevOps
|
|
56
56
|
- **Live runs** — opt-in `live` streams per-test progress to an unguessable watch link printed at run start; all shards of one CI run converge on a single live dashboard while tests execute. The PDF is still produced at the end, unchanged
|
|
57
57
|
- **Shard merging** — combine N Playwright JSON shard reports into one PDF via `shardResults`; accepts glob patterns or explicit paths
|
|
58
|
-
- **Notifications** — post pass/fail summaries to Slack, Teams, Discord, or email after each run; configurable trigger (`always` / `failure` / `success`) per channel; email
|
|
58
|
+
- **Notifications** — post pass/fail summaries to Slack, Teams, Discord, or email after each run; configurable trigger (`always` / `failure` / `success`) per channel; email and Discord support optional PDF attachment via `attachPdf`
|
|
59
59
|
- **Flakiness trend** — top-N flakiest tests table with per-test dot sparkline across stored history runs in the `detailed` template; `flakinessTopN` option (default 5, `0` disables)
|
|
60
60
|
- **Offline failure analysis** — sorts each failure into one of 7 root-cause buckets and renders a per-test root-cause chip inline under the failed test in the `detailed` PDF (dominant-cause one-liner in `executive`). No network, no AI service — a small embedded classifier that updates itself safely over your existing license refresh; pin it any time. Full details at [reportforge.org/docs/advanced/failure-analysis](https://reportforge.org/docs/advanced/failure-analysis).
|
|
61
61
|
- `npx @reportforge/playwright-pdf export-feedback` — exports locally-collected unrecognized failures (tokenized, local-only) for optional manual sharing.
|
|
@@ -194,7 +194,7 @@ All options are the second element of the reporter tuple.
|
|
|
194
194
|
| `maxInlineFailures` | `number` | derived from `compressionLevel` | Cap on failure entries rendered inline in the PDF. Overflow is written to a sibling `{basename}-failures.json` sidecar file. |
|
|
195
195
|
| `maxFileSizeMb` | `number` | `8` | Soft cap on the final PDF size in MB. If exceeded, the report is re-rendered once with the `'max'` compression preset. |
|
|
196
196
|
| `shardResults` | `string \| string[]` | — | Glob or path array of Playwright JSON shard report files to merge into one PDF. See the Shard Merging docs. |
|
|
197
|
-
| `notify` | `object` | — | Notification channels: `slack`, `teams
|
|
197
|
+
| `notify` | `object` | — | Notification channels: `slack`, `teams` (each `{ url, enabled, on }`), `discord` (`{ url, enabled, on, attachPdf }`), `email` (`{ to, enabled, on, attachPdf }`). See the Notifications docs. |
|
|
198
198
|
| `failureAnalysis` | `object` | `{ enabled: true }` | Offline failure root-cause analysis (embedded Naive Bayes; no runtime network). Fields: `enabled` (default `true`), `maxClusters` (default `10`), `minStrength` (`weak`\|`moderate`\|`strong`, default `weak`), `maxFailuresToAnalyse` (default `500`), `collectUnclassified` (default `true`), `autoUpdateModel` (default `true`). See the Failure Analysis docs. |
|
|
199
199
|
| `capture` | `object` | `{}` (off) | Opt-in rich execution capture for the defect log (reporter-side; no fixtures, no test-code changes). Fields: `steps` (a copy-pasteable "Steps to Reproduce" outline built from the Playwright step tree — each row a Markdown list line indented under its parent `test.step`, default `false`), `apiSteps` (include every top-level `pw:api` action (click/fill/check/goto) in the outline for a full action trail; off by default the outline shows only `test.step` intent + `expect` assertions, default `false`), `console` (Node stdout/stderr tail, default `false`), `evidence` (trace/video file links, default `false`), `maxSteps` (default `50`), `maxConsoleLines` (default `50`). Renders in the Defect Log section of the `detailed` template. |
|
|
200
200
|
| `live` | `object` | `{}` (off) | Opt-in live test-execution streaming. When `enabled`, the reporter prints an unguessable watch link to the CI logs at run start and streams per-test progress to the hosted dashboard while tests run — all shards of one CI run converge on a single live view. Fields: `enabled` (default `false`), `runId` (override the auto-derived run id), `serverUrl` (override the streaming server), `steps` (`none`\|`failed`\|`intent`\|`all`, default `failed`; `intent` shows only your `test.step` names plus any failing step), `console` (stream stdout/stderr tails, default `false`), `flushMs` (batch debounce 500–10000, default `2000`). The PDF at the end of the run is unaffected. Requires an active subscription with the `live` entitlement. See the Live Runs docs. |
|
|
@@ -282,6 +282,7 @@ notify: {
|
|
|
282
282
|
url: process.env.DISCORD_WEBHOOK_URL,
|
|
283
283
|
enabled: true,
|
|
284
284
|
on: 'failure',
|
|
285
|
+
attachPdf: true, // upload the PDF with the message (default: false)
|
|
285
286
|
},
|
|
286
287
|
email: {
|
|
287
288
|
to: ['team@company.com', 'qa@company.com'],
|
|
@@ -304,6 +305,8 @@ The `on` trigger controls when the message fires:
|
|
|
304
305
|
|
|
305
306
|
**Email** requires `RESEND_API_KEY` in the environment (get one at [resend.com](https://resend.com)). Sender defaults to `noreply@reportforge.org`; override with `RESEND_FROM`. Store webhook URLs and API keys as CI secrets — never commit them.
|
|
306
307
|
|
|
308
|
+
**PDF attachment** (`attachPdf: true`) is available on `email` and `discord` — Slack and Teams webhooks cannot carry file uploads. On Discord the PDF is uploaded with the message; if the upload fails or the PDF exceeds Discord's file-size cap, the summary still posts without the attachment. With multiple templates, the first PDF is attached.
|
|
309
|
+
|
|
307
310
|
The summary includes pass rate, test counts, duration, and the report filename. Notifications require a valid license and fire after PDF generation (or after a PDF failure — you still get the ping).
|
|
308
311
|
|
|
309
312
|
### Test History Trending
|
package/dist/cli/index.js
CHANGED
|
@@ -11921,7 +11921,7 @@ ${issues}`);
|
|
|
11921
11921
|
}
|
|
11922
11922
|
return result.data;
|
|
11923
11923
|
}
|
|
11924
|
-
var hexColor, channelConfigSchema, emailChannelConfigSchema, notifyConfigSchema, failureAnalysisConfigSchema, captureConfigSchema, liveConfigSchema, sectionFlagsShape, sectionFlagsSchema, sectionsSchema, ReporterOptionsSchema;
|
|
11924
|
+
var hexColor, channelConfigSchema, discordChannelConfigSchema, emailChannelConfigSchema, notifyConfigSchema, failureAnalysisConfigSchema, captureConfigSchema, liveConfigSchema, sectionFlagsShape, sectionFlagsSchema, sectionsSchema, ReporterOptionsSchema;
|
|
11925
11925
|
var init_schema = __esm({
|
|
11926
11926
|
"src/config/schema.ts"() {
|
|
11927
11927
|
"use strict";
|
|
@@ -11938,6 +11938,9 @@ var init_schema = __esm({
|
|
|
11938
11938
|
on: external_exports.enum(["always", "failure", "success"]).default("always"),
|
|
11939
11939
|
enabled: external_exports.boolean().default(false)
|
|
11940
11940
|
});
|
|
11941
|
+
discordChannelConfigSchema = channelConfigSchema.extend({
|
|
11942
|
+
attachPdf: external_exports.boolean().default(false)
|
|
11943
|
+
});
|
|
11941
11944
|
emailChannelConfigSchema = external_exports.object({
|
|
11942
11945
|
to: external_exports.array(external_exports.string().email('Each "to" entry must be a valid email')).min(1, '"to" must have at least one address'),
|
|
11943
11946
|
on: external_exports.enum(["always", "failure", "success"]).default("always"),
|
|
@@ -11947,7 +11950,7 @@ var init_schema = __esm({
|
|
|
11947
11950
|
notifyConfigSchema = external_exports.object({
|
|
11948
11951
|
slack: channelConfigSchema.optional(),
|
|
11949
11952
|
teams: channelConfigSchema.optional(),
|
|
11950
|
-
discord:
|
|
11953
|
+
discord: discordChannelConfigSchema.optional(),
|
|
11951
11954
|
email: emailChannelConfigSchema.optional()
|
|
11952
11955
|
}).optional();
|
|
11953
11956
|
failureAnalysisConfigSchema = external_exports.object({
|
package/dist/index.d.mts
CHANGED
|
@@ -158,7 +158,12 @@ interface ReporterOptions {
|
|
|
158
158
|
* `on` controls the trigger: `'always'` (default), `'failure'`, or `'success'`.
|
|
159
159
|
*
|
|
160
160
|
* - `slack` / `teams` / `discord`: webhook URL channels
|
|
161
|
+
* - `discord`: `attachPdf: true` uploads the PDF with the message (falls back
|
|
162
|
+
* to the summary alone if the upload fails or the PDF exceeds Discord's cap)
|
|
161
163
|
* - `email`: sends via Resend; `to` is required; `attachPdf: true` attaches the PDF
|
|
164
|
+
*
|
|
165
|
+
* Slack and Teams webhooks cannot carry file uploads, so `attachPdf` does not
|
|
166
|
+
* exist on those channels.
|
|
162
167
|
*/
|
|
163
168
|
notify?: {
|
|
164
169
|
slack?: {
|
|
@@ -175,6 +180,7 @@ interface ReporterOptions {
|
|
|
175
180
|
url: string;
|
|
176
181
|
on?: 'always' | 'failure' | 'success';
|
|
177
182
|
enabled?: boolean;
|
|
183
|
+
attachPdf?: boolean;
|
|
178
184
|
};
|
|
179
185
|
email?: {
|
|
180
186
|
to: string[];
|
package/dist/index.d.ts
CHANGED
|
@@ -158,7 +158,12 @@ interface ReporterOptions {
|
|
|
158
158
|
* `on` controls the trigger: `'always'` (default), `'failure'`, or `'success'`.
|
|
159
159
|
*
|
|
160
160
|
* - `slack` / `teams` / `discord`: webhook URL channels
|
|
161
|
+
* - `discord`: `attachPdf: true` uploads the PDF with the message (falls back
|
|
162
|
+
* to the summary alone if the upload fails or the PDF exceeds Discord's cap)
|
|
161
163
|
* - `email`: sends via Resend; `to` is required; `attachPdf: true` attaches the PDF
|
|
164
|
+
*
|
|
165
|
+
* Slack and Teams webhooks cannot carry file uploads, so `attachPdf` does not
|
|
166
|
+
* exist on those channels.
|
|
162
167
|
*/
|
|
163
168
|
notify?: {
|
|
164
169
|
slack?: {
|
|
@@ -175,6 +180,7 @@ interface ReporterOptions {
|
|
|
175
180
|
url: string;
|
|
176
181
|
on?: 'always' | 'failure' | 'success';
|
|
177
182
|
enabled?: boolean;
|
|
183
|
+
attachPdf?: boolean;
|
|
178
184
|
};
|
|
179
185
|
email?: {
|
|
180
186
|
to: string[];
|
package/dist/index.js
CHANGED
|
@@ -10454,6 +10454,9 @@ var channelConfigSchema = external_exports.object({
|
|
|
10454
10454
|
on: external_exports.enum(["always", "failure", "success"]).default("always"),
|
|
10455
10455
|
enabled: external_exports.boolean().default(false)
|
|
10456
10456
|
});
|
|
10457
|
+
var discordChannelConfigSchema = channelConfigSchema.extend({
|
|
10458
|
+
attachPdf: external_exports.boolean().default(false)
|
|
10459
|
+
});
|
|
10457
10460
|
var emailChannelConfigSchema = external_exports.object({
|
|
10458
10461
|
to: external_exports.array(external_exports.string().email('Each "to" entry must be a valid email')).min(1, '"to" must have at least one address'),
|
|
10459
10462
|
on: external_exports.enum(["always", "failure", "success"]).default("always"),
|
|
@@ -10463,7 +10466,7 @@ var emailChannelConfigSchema = external_exports.object({
|
|
|
10463
10466
|
var notifyConfigSchema = external_exports.object({
|
|
10464
10467
|
slack: channelConfigSchema.optional(),
|
|
10465
10468
|
teams: channelConfigSchema.optional(),
|
|
10466
|
-
discord:
|
|
10469
|
+
discord: discordChannelConfigSchema.optional(),
|
|
10467
10470
|
email: emailChannelConfigSchema.optional()
|
|
10468
10471
|
}).optional();
|
|
10469
10472
|
var failureAnalysisConfigSchema = external_exports.object({
|
|
@@ -13199,7 +13202,7 @@ function fmtMb(bytes) {
|
|
|
13199
13202
|
}
|
|
13200
13203
|
|
|
13201
13204
|
// src/reporter.ts
|
|
13202
|
-
var
|
|
13205
|
+
var import_path13 = __toESM(require("path"));
|
|
13203
13206
|
|
|
13204
13207
|
// src/history/HistoryManager.ts
|
|
13205
13208
|
init_cjs_shims();
|
|
@@ -13280,6 +13283,8 @@ init_cjs_shims();
|
|
|
13280
13283
|
|
|
13281
13284
|
// src/notify/NotificationSender.ts
|
|
13282
13285
|
init_cjs_shims();
|
|
13286
|
+
var import_promises2 = require("fs/promises");
|
|
13287
|
+
var import_path12 = require("path");
|
|
13283
13288
|
|
|
13284
13289
|
// src/notify/formatters/slack.ts
|
|
13285
13290
|
init_cjs_shims();
|
|
@@ -13529,6 +13534,7 @@ var FORMATTERS = {
|
|
|
13529
13534
|
teams: buildTeamsPayload,
|
|
13530
13535
|
discord: buildDiscordPayload
|
|
13531
13536
|
};
|
|
13537
|
+
var DISCORD_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
13532
13538
|
var NotificationSender = class {
|
|
13533
13539
|
async sendAll(reportData, notifyConfig, pdfPaths) {
|
|
13534
13540
|
const result = { sent: [], skipped: [], failed: [] };
|
|
@@ -13541,6 +13547,9 @@ var NotificationSender = class {
|
|
|
13541
13547
|
return Promise.resolve();
|
|
13542
13548
|
}
|
|
13543
13549
|
const payload = FORMATTERS[name](reportData.stats, pdfPaths, reportTitle);
|
|
13550
|
+
if (name === "discord" && notifyConfig.discord?.attachPdf && pdfPaths.length > 0) {
|
|
13551
|
+
return this.postDiscordWithPdf(channel.url, payload, pdfPaths[0], result);
|
|
13552
|
+
}
|
|
13544
13553
|
return this.post(name, channel.url, payload, result);
|
|
13545
13554
|
})
|
|
13546
13555
|
);
|
|
@@ -13582,6 +13591,47 @@ var NotificationSender = class {
|
|
|
13582
13591
|
})
|
|
13583
13592
|
);
|
|
13584
13593
|
}
|
|
13594
|
+
/**
|
|
13595
|
+
* Posts the summary to a Discord webhook with the PDF as a multipart file
|
|
13596
|
+
* upload (`payload_json` + `files[0]`). Any problem — unreadable file, PDF
|
|
13597
|
+
* over Discord's upload cap, or a failed upload — falls back to the plain
|
|
13598
|
+
* JSON summary post so the notification still lands; only the fallback's
|
|
13599
|
+
* outcome is recorded in the result. Attaches the first PDF only (matches
|
|
13600
|
+
* the email channel's multi-template behaviour).
|
|
13601
|
+
*/
|
|
13602
|
+
async postDiscordWithPdf(url, payload, pdfPath, result) {
|
|
13603
|
+
let file;
|
|
13604
|
+
try {
|
|
13605
|
+
file = await (0, import_promises2.readFile)(pdfPath);
|
|
13606
|
+
if (file.byteLength > DISCORD_MAX_UPLOAD_BYTES) {
|
|
13607
|
+
const mb = (file.byteLength / 1024 / 1024).toFixed(1);
|
|
13608
|
+
throw new Error(`PDF is ${mb}MB \u2014 over Discord's upload limit`);
|
|
13609
|
+
}
|
|
13610
|
+
} catch (err) {
|
|
13611
|
+
logger.warn(
|
|
13612
|
+
`[notify] discord: ${err instanceof Error ? err.message : String(err)} \u2014 sending summary without the PDF`
|
|
13613
|
+
);
|
|
13614
|
+
return this.post("discord", url, payload, result);
|
|
13615
|
+
}
|
|
13616
|
+
const form = new FormData();
|
|
13617
|
+
form.append("payload_json", JSON.stringify(payload));
|
|
13618
|
+
form.append("files[0]", new Blob([new Uint8Array(file)], { type: "application/pdf" }), (0, import_path12.basename)(pdfPath));
|
|
13619
|
+
const controller = new AbortController();
|
|
13620
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
13621
|
+
try {
|
|
13622
|
+
const res = await fetch(url, { method: "POST", body: form, signal: controller.signal });
|
|
13623
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
13624
|
+
logger.info("[notify] discord: sent (PDF attached)");
|
|
13625
|
+
result.sent.push("discord");
|
|
13626
|
+
} catch (err) {
|
|
13627
|
+
logger.warn(
|
|
13628
|
+
`[notify] discord: upload failed (${err instanceof Error ? err.message : String(err)}) \u2014 retrying without the PDF`
|
|
13629
|
+
);
|
|
13630
|
+
return this.post("discord", url, payload, result);
|
|
13631
|
+
} finally {
|
|
13632
|
+
clearTimeout(timeoutId);
|
|
13633
|
+
}
|
|
13634
|
+
}
|
|
13585
13635
|
async post(name, url, payload, result) {
|
|
13586
13636
|
const controller = new AbortController();
|
|
13587
13637
|
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -13951,7 +14001,7 @@ var PdfReporter = class {
|
|
|
13951
14001
|
this.liveConsole = false;
|
|
13952
14002
|
this.options = parseOptions(rawOptions);
|
|
13953
14003
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
13954
|
-
const version = true ? "0.
|
|
14004
|
+
const version = true ? "0.13.0" : "0.x";
|
|
13955
14005
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13956
14006
|
this.licenseClient = new LicenseClient({
|
|
13957
14007
|
licenseKey: this.options.licenseKey,
|
|
@@ -14219,7 +14269,7 @@ var PdfReporter = class {
|
|
|
14219
14269
|
}
|
|
14220
14270
|
resolveProjectName() {
|
|
14221
14271
|
try {
|
|
14222
|
-
const pkgPath =
|
|
14272
|
+
const pkgPath = import_path13.default.resolve(process.cwd(), "package.json");
|
|
14223
14273
|
const pkg = require(pkgPath);
|
|
14224
14274
|
return pkg.name ?? "Playwright Tests";
|
|
14225
14275
|
} catch {
|
|
@@ -14269,9 +14319,9 @@ function deriveVerdict(stats) {
|
|
|
14269
14319
|
}
|
|
14270
14320
|
function resolveHistoryPath(options, cwd) {
|
|
14271
14321
|
if (options.historyFile) {
|
|
14272
|
-
return
|
|
14322
|
+
return import_path13.default.isAbsolute(options.historyFile) ? options.historyFile : import_path13.default.resolve(cwd, options.historyFile);
|
|
14273
14323
|
}
|
|
14274
|
-
return
|
|
14324
|
+
return import_path13.default.join(resolveProjectDir(options, cwd), "history.json");
|
|
14275
14325
|
}
|
|
14276
14326
|
function flattenSuiteNodes(suites) {
|
|
14277
14327
|
return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);
|
package/dist/index.mjs
CHANGED
|
@@ -10455,6 +10455,9 @@ var channelConfigSchema = external_exports.object({
|
|
|
10455
10455
|
on: external_exports.enum(["always", "failure", "success"]).default("always"),
|
|
10456
10456
|
enabled: external_exports.boolean().default(false)
|
|
10457
10457
|
});
|
|
10458
|
+
var discordChannelConfigSchema = channelConfigSchema.extend({
|
|
10459
|
+
attachPdf: external_exports.boolean().default(false)
|
|
10460
|
+
});
|
|
10458
10461
|
var emailChannelConfigSchema = external_exports.object({
|
|
10459
10462
|
to: external_exports.array(external_exports.string().email('Each "to" entry must be a valid email')).min(1, '"to" must have at least one address'),
|
|
10460
10463
|
on: external_exports.enum(["always", "failure", "success"]).default("always"),
|
|
@@ -10464,7 +10467,7 @@ var emailChannelConfigSchema = external_exports.object({
|
|
|
10464
10467
|
var notifyConfigSchema = external_exports.object({
|
|
10465
10468
|
slack: channelConfigSchema.optional(),
|
|
10466
10469
|
teams: channelConfigSchema.optional(),
|
|
10467
|
-
discord:
|
|
10470
|
+
discord: discordChannelConfigSchema.optional(),
|
|
10468
10471
|
email: emailChannelConfigSchema.optional()
|
|
10469
10472
|
}).optional();
|
|
10470
10473
|
var failureAnalysisConfigSchema = external_exports.object({
|
|
@@ -13281,6 +13284,8 @@ init_esm_shims();
|
|
|
13281
13284
|
|
|
13282
13285
|
// src/notify/NotificationSender.ts
|
|
13283
13286
|
init_esm_shims();
|
|
13287
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
13288
|
+
import { basename as basename3 } from "path";
|
|
13284
13289
|
|
|
13285
13290
|
// src/notify/formatters/slack.ts
|
|
13286
13291
|
init_esm_shims();
|
|
@@ -13530,6 +13535,7 @@ var FORMATTERS = {
|
|
|
13530
13535
|
teams: buildTeamsPayload,
|
|
13531
13536
|
discord: buildDiscordPayload
|
|
13532
13537
|
};
|
|
13538
|
+
var DISCORD_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
13533
13539
|
var NotificationSender = class {
|
|
13534
13540
|
async sendAll(reportData, notifyConfig, pdfPaths) {
|
|
13535
13541
|
const result = { sent: [], skipped: [], failed: [] };
|
|
@@ -13542,6 +13548,9 @@ var NotificationSender = class {
|
|
|
13542
13548
|
return Promise.resolve();
|
|
13543
13549
|
}
|
|
13544
13550
|
const payload = FORMATTERS[name](reportData.stats, pdfPaths, reportTitle);
|
|
13551
|
+
if (name === "discord" && notifyConfig.discord?.attachPdf && pdfPaths.length > 0) {
|
|
13552
|
+
return this.postDiscordWithPdf(channel.url, payload, pdfPaths[0], result);
|
|
13553
|
+
}
|
|
13545
13554
|
return this.post(name, channel.url, payload, result);
|
|
13546
13555
|
})
|
|
13547
13556
|
);
|
|
@@ -13583,6 +13592,47 @@ var NotificationSender = class {
|
|
|
13583
13592
|
})
|
|
13584
13593
|
);
|
|
13585
13594
|
}
|
|
13595
|
+
/**
|
|
13596
|
+
* Posts the summary to a Discord webhook with the PDF as a multipart file
|
|
13597
|
+
* upload (`payload_json` + `files[0]`). Any problem — unreadable file, PDF
|
|
13598
|
+
* over Discord's upload cap, or a failed upload — falls back to the plain
|
|
13599
|
+
* JSON summary post so the notification still lands; only the fallback's
|
|
13600
|
+
* outcome is recorded in the result. Attaches the first PDF only (matches
|
|
13601
|
+
* the email channel's multi-template behaviour).
|
|
13602
|
+
*/
|
|
13603
|
+
async postDiscordWithPdf(url, payload, pdfPath, result) {
|
|
13604
|
+
let file;
|
|
13605
|
+
try {
|
|
13606
|
+
file = await readFile2(pdfPath);
|
|
13607
|
+
if (file.byteLength > DISCORD_MAX_UPLOAD_BYTES) {
|
|
13608
|
+
const mb = (file.byteLength / 1024 / 1024).toFixed(1);
|
|
13609
|
+
throw new Error(`PDF is ${mb}MB \u2014 over Discord's upload limit`);
|
|
13610
|
+
}
|
|
13611
|
+
} catch (err) {
|
|
13612
|
+
logger.warn(
|
|
13613
|
+
`[notify] discord: ${err instanceof Error ? err.message : String(err)} \u2014 sending summary without the PDF`
|
|
13614
|
+
);
|
|
13615
|
+
return this.post("discord", url, payload, result);
|
|
13616
|
+
}
|
|
13617
|
+
const form = new FormData();
|
|
13618
|
+
form.append("payload_json", JSON.stringify(payload));
|
|
13619
|
+
form.append("files[0]", new Blob([new Uint8Array(file)], { type: "application/pdf" }), basename3(pdfPath));
|
|
13620
|
+
const controller = new AbortController();
|
|
13621
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
13622
|
+
try {
|
|
13623
|
+
const res = await fetch(url, { method: "POST", body: form, signal: controller.signal });
|
|
13624
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
13625
|
+
logger.info("[notify] discord: sent (PDF attached)");
|
|
13626
|
+
result.sent.push("discord");
|
|
13627
|
+
} catch (err) {
|
|
13628
|
+
logger.warn(
|
|
13629
|
+
`[notify] discord: upload failed (${err instanceof Error ? err.message : String(err)}) \u2014 retrying without the PDF`
|
|
13630
|
+
);
|
|
13631
|
+
return this.post("discord", url, payload, result);
|
|
13632
|
+
} finally {
|
|
13633
|
+
clearTimeout(timeoutId);
|
|
13634
|
+
}
|
|
13635
|
+
}
|
|
13586
13636
|
async post(name, url, payload, result) {
|
|
13587
13637
|
const controller = new AbortController();
|
|
13588
13638
|
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -13952,7 +14002,7 @@ var PdfReporter = class {
|
|
|
13952
14002
|
this.liveConsole = false;
|
|
13953
14003
|
this.options = parseOptions(rawOptions);
|
|
13954
14004
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
13955
|
-
const version = true ? "0.
|
|
14005
|
+
const version = true ? "0.13.0" : "0.x";
|
|
13956
14006
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13957
14007
|
this.licenseClient = new LicenseClient({
|
|
13958
14008
|
licenseKey: this.options.licenseKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Enterprise-ready PDF reports for Playwright Test — minimal, detailed, and executive templates with CI/CD integrations",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ReportForge",
|