@reportforge/playwright-pdf 0.9.0 → 0.10.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 +2 -2
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +93 -9
- package/dist/index.mjs +93 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ Full reference: [reportforge.org/docs/configuration](https://reportforge.org/doc
|
|
|
211
211
|
|
|
212
212
|
### Live Runs
|
|
213
213
|
|
|
214
|
-
Watch a run as it happens. With `live` enabled, the reporter prints an unguessable watch link to your CI logs at run start and streams per-test progress to a hosted dashboard while tests execute
|
|
214
|
+
Watch a run as it happens. With `live` enabled, the reporter prints an unguessable watch link — boxed under a `Live Tracker` heading — to your CI logs at run start and streams per-test progress to a hosted dashboard while tests execute. All shards of one CI run converge on a single live view, each running test shows the step it's currently executing, and the PDF is still generated at the end, unchanged.
|
|
215
215
|
|
|
216
216
|
```typescript
|
|
217
217
|
// playwright.config.ts
|
|
@@ -230,7 +230,7 @@ reporter: [
|
|
|
230
230
|
|
|
231
231
|
Sharded runs need no extra config — the run id is derived from the run-shared CI identifier (e.g. `GITHUB_RUN_ID`), identical across every matrix shard. Set `RF_LIVE_RUN_ID` to override when your CI is not auto-detected.
|
|
232
232
|
|
|
233
|
-
Live streaming is best-effort and entitlement-gated: if the server is unreachable the run is unaffected, and an active subscription with the `live` feature is required.
|
|
233
|
+
Live streaming is best-effort and entitlement-gated: if the server is unreachable the run is unaffected, and an active subscription with the `live` feature is required. If you've configured Slack/Teams/Discord notifications, the watch link is also posted to those channels when the run starts.
|
|
234
234
|
|
|
235
235
|
> **Security:** the watch link exposes test titles + statuses (and step titles when `steps` is on). Leaving `console: false` is recommended — enabling it streams your tests' stdout/stderr to anyone holding the link. Full details at [reportforge.org/docs/advanced/live](https://reportforge.org/docs/advanced/live).
|
|
236
236
|
|
package/dist/index.d.mts
CHANGED
|
@@ -442,6 +442,15 @@ declare class PdfReporter implements Reporter {
|
|
|
442
442
|
* link to the CI logs. Throws are caught by the onBegin try/catch.
|
|
443
443
|
*/
|
|
444
444
|
private setupLive;
|
|
445
|
+
/**
|
|
446
|
+
* Announces the watch link once the run is confirmed live-entitled. Gating on
|
|
447
|
+
* the SAME resolved entitlement the streamer requires (`jwt` + the `'live'`
|
|
448
|
+
* feature) guarantees the link is never printed or posted for a run that will
|
|
449
|
+
* never stream — no dead links. The chat ping fires from the primary shard only
|
|
450
|
+
* (unsharded, or `shard.current === 1`) so an N-way matrix posts one message,
|
|
451
|
+
* not N. The banner still prints from every shard's own log.
|
|
452
|
+
*/
|
|
453
|
+
private announceLive;
|
|
445
454
|
private resolveProjectName;
|
|
446
455
|
private openPdf;
|
|
447
456
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -442,6 +442,15 @@ declare class PdfReporter implements Reporter {
|
|
|
442
442
|
* link to the CI logs. Throws are caught by the onBegin try/catch.
|
|
443
443
|
*/
|
|
444
444
|
private setupLive;
|
|
445
|
+
/**
|
|
446
|
+
* Announces the watch link once the run is confirmed live-entitled. Gating on
|
|
447
|
+
* the SAME resolved entitlement the streamer requires (`jwt` + the `'live'`
|
|
448
|
+
* feature) guarantees the link is never printed or posted for a run that will
|
|
449
|
+
* never stream — no dead links. The chat ping fires from the primary shard only
|
|
450
|
+
* (unsharded, or `shard.current === 1`) so an N-way matrix posts one message,
|
|
451
|
+
* not N. The banner still prints from every shard's own log.
|
|
452
|
+
*/
|
|
453
|
+
private announceLive;
|
|
445
454
|
private resolveProjectName;
|
|
446
455
|
private openPdf;
|
|
447
456
|
}
|
package/dist/index.js
CHANGED
|
@@ -13319,6 +13319,22 @@ _${name}_`
|
|
|
13319
13319
|
|
|
13320
13320
|
// src/notify/formatters/teams.ts
|
|
13321
13321
|
init_cjs_shims();
|
|
13322
|
+
function buildTeamsSimpleMessage(text) {
|
|
13323
|
+
return {
|
|
13324
|
+
type: "message",
|
|
13325
|
+
attachments: [
|
|
13326
|
+
{
|
|
13327
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
13328
|
+
content: {
|
|
13329
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
13330
|
+
type: "AdaptiveCard",
|
|
13331
|
+
version: "1.4",
|
|
13332
|
+
body: [{ type: "TextBlock", text, wrap: true }]
|
|
13333
|
+
}
|
|
13334
|
+
}
|
|
13335
|
+
]
|
|
13336
|
+
};
|
|
13337
|
+
}
|
|
13322
13338
|
function buildTeamsPayload(stats, pdfPaths, reportTitle) {
|
|
13323
13339
|
const label = statusLabel(stats.verdict);
|
|
13324
13340
|
const name = reportName(pdfPaths, reportTitle);
|
|
@@ -13525,6 +13541,30 @@ var NotificationSender = class {
|
|
|
13525
13541
|
}
|
|
13526
13542
|
return result;
|
|
13527
13543
|
}
|
|
13544
|
+
/**
|
|
13545
|
+
* Fire-and-forget "live run started" ping to the enabled chat channels, carrying
|
|
13546
|
+
* the watch URL so teammates can open the live view without hunting through CI
|
|
13547
|
+
* logs. Only channels with `on: 'always'` receive it — a start ping has no
|
|
13548
|
+
* pass/fail outcome, so a `failure`/`success`-only channel is left alone. Never
|
|
13549
|
+
* touches the email channel (too heavy for a start signal). Resolves even if
|
|
13550
|
+
* every post fails; live is best-effort.
|
|
13551
|
+
*/
|
|
13552
|
+
async sendLiveStarted(notifyConfig, watchUrl, branch) {
|
|
13553
|
+
if (!notifyConfig) return;
|
|
13554
|
+
const line = `ReportForge: live test run started${branch ? ` on ${branch}` : ""}. Watch: ${watchUrl}`;
|
|
13555
|
+
const payloads = {
|
|
13556
|
+
slack: { text: line },
|
|
13557
|
+
teams: buildTeamsSimpleMessage(line),
|
|
13558
|
+
discord: { content: line }
|
|
13559
|
+
};
|
|
13560
|
+
await Promise.allSettled(
|
|
13561
|
+
Object.keys(payloads).map((name) => {
|
|
13562
|
+
const channel = notifyConfig[name];
|
|
13563
|
+
if (!channel || !channel.enabled || channel.on !== "always") return Promise.resolve();
|
|
13564
|
+
return this.post(`${name} (live-start)`, channel.url, payloads[name]);
|
|
13565
|
+
})
|
|
13566
|
+
);
|
|
13567
|
+
}
|
|
13528
13568
|
async post(name, url, payload, result) {
|
|
13529
13569
|
const controller = new AbortController();
|
|
13530
13570
|
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -13537,10 +13577,10 @@ var NotificationSender = class {
|
|
|
13537
13577
|
});
|
|
13538
13578
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
13539
13579
|
logger.info(`[notify] ${name}: sent`);
|
|
13540
|
-
result
|
|
13580
|
+
result?.sent.push(name);
|
|
13541
13581
|
} catch (err) {
|
|
13542
13582
|
logger.warn(`[notify] ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13543
|
-
result
|
|
13583
|
+
result?.failed.push(name);
|
|
13544
13584
|
} finally {
|
|
13545
13585
|
clearTimeout(timeoutId);
|
|
13546
13586
|
}
|
|
@@ -13582,7 +13622,8 @@ var LiveStreamer = class {
|
|
|
13582
13622
|
}
|
|
13583
13623
|
enqueueTest(update) {
|
|
13584
13624
|
if (!this.enabled) return;
|
|
13585
|
-
this.testStates.
|
|
13625
|
+
const prevRetry = this.testStates.get(update.id)?.retry;
|
|
13626
|
+
this.testStates.set(update.id, { status: update.status, retry: update.retry ?? prevRetry });
|
|
13586
13627
|
this.testBuffer.push(update);
|
|
13587
13628
|
this.capPreLicenseBuffer();
|
|
13588
13629
|
this.scheduleFlush();
|
|
@@ -13716,7 +13757,6 @@ var LiveStreamer = class {
|
|
|
13716
13757
|
return false;
|
|
13717
13758
|
}
|
|
13718
13759
|
this.jwt = info.jwt;
|
|
13719
|
-
if (this.config.watchUrl) logger.info(`Live results: ${this.config.watchUrl}`);
|
|
13720
13760
|
return true;
|
|
13721
13761
|
}
|
|
13722
13762
|
async send(body) {
|
|
@@ -13785,6 +13825,13 @@ function buildWatchUrl(serverUrl, runId, token) {
|
|
|
13785
13825
|
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
13786
13826
|
return `${base}/live/${encodeURIComponent(runId)}?t=${encodeURIComponent(token)}`;
|
|
13787
13827
|
}
|
|
13828
|
+
function formatLiveBanner(url) {
|
|
13829
|
+
const label = "Live Tracker";
|
|
13830
|
+
const link = `Watch live: ${url}`;
|
|
13831
|
+
const width = Math.max(label.length, link.length);
|
|
13832
|
+
const rule = "\u2500".repeat(width + 2);
|
|
13833
|
+
return [rule, ` ${label.padEnd(width)} `, ` ${link.padEnd(width)} `, rule].join("\n");
|
|
13834
|
+
}
|
|
13788
13835
|
function shardKeyOf(shard) {
|
|
13789
13836
|
if (!shard || !shard.total) return "1/1";
|
|
13790
13837
|
return `${shard.current}/${shard.total}`;
|
|
@@ -13819,6 +13866,17 @@ function shouldEmitStep(step, mode) {
|
|
|
13819
13866
|
if (step.error) return true;
|
|
13820
13867
|
return step.category === "test.step" || step.category === "expect";
|
|
13821
13868
|
}
|
|
13869
|
+
function isCurrentStep(step) {
|
|
13870
|
+
return step.category === "test.step" && !!step.title;
|
|
13871
|
+
}
|
|
13872
|
+
function mapCurrentStep(testId, step, shardKey) {
|
|
13873
|
+
return {
|
|
13874
|
+
id: testId,
|
|
13875
|
+
status: "running",
|
|
13876
|
+
shardKey,
|
|
13877
|
+
currentStep: clampWithEllipsis(step.title ?? "", MAX_STEP_TITLE_CHARS)
|
|
13878
|
+
};
|
|
13879
|
+
}
|
|
13822
13880
|
function mapStep(testId, step, phase, shardKey) {
|
|
13823
13881
|
return {
|
|
13824
13882
|
kind: "step",
|
|
@@ -13853,7 +13911,7 @@ var PdfReporter = class {
|
|
|
13853
13911
|
this.liveConsole = false;
|
|
13854
13912
|
this.options = parseOptions(rawOptions);
|
|
13855
13913
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
13856
|
-
const version = true ? "0.
|
|
13914
|
+
const version = true ? "0.10.0" : "0.x";
|
|
13857
13915
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13858
13916
|
this.licenseClient = new LicenseClient({
|
|
13859
13917
|
licenseKey: this.options.licenseKey,
|
|
@@ -13887,9 +13945,14 @@ var PdfReporter = class {
|
|
|
13887
13945
|
}
|
|
13888
13946
|
}
|
|
13889
13947
|
onStepBegin(test, _result, step) {
|
|
13890
|
-
if (!this.liveStreamer
|
|
13948
|
+
if (!this.liveStreamer) return;
|
|
13891
13949
|
try {
|
|
13892
|
-
this.
|
|
13950
|
+
if (this.liveSteps !== "none" && isCurrentStep(step)) {
|
|
13951
|
+
this.liveStreamer.enqueueTest(mapCurrentStep(test.id, step, this.liveShardKey));
|
|
13952
|
+
}
|
|
13953
|
+
if (shouldEmitStep(step, this.liveSteps)) {
|
|
13954
|
+
this.liveStreamer.enqueueEvent(mapStep(test.id, step, "begin", this.liveShardKey));
|
|
13955
|
+
}
|
|
13893
13956
|
} catch {
|
|
13894
13957
|
}
|
|
13895
13958
|
}
|
|
@@ -14064,17 +14127,38 @@ var PdfReporter = class {
|
|
|
14064
14127
|
void this.license();
|
|
14065
14128
|
const token = computeWatchToken(runId);
|
|
14066
14129
|
const watchUrl = token ? buildWatchUrl(serverUrl, runId, token) : void 0;
|
|
14130
|
+
const env = detectCiEnv();
|
|
14131
|
+
if (watchUrl) void this.announceLive(watchUrl, env, shard).catch(() => {
|
|
14132
|
+
});
|
|
14067
14133
|
this.liveStreamer = new LiveStreamer({
|
|
14068
14134
|
serverUrl,
|
|
14069
14135
|
runId,
|
|
14070
14136
|
shardKey: this.liveShardKey,
|
|
14071
14137
|
shardTotal: shard?.total ?? null,
|
|
14072
14138
|
license: () => this.license(),
|
|
14073
|
-
env
|
|
14074
|
-
watchUrl,
|
|
14139
|
+
env,
|
|
14075
14140
|
flushMs: live.flushMs
|
|
14076
14141
|
});
|
|
14077
14142
|
}
|
|
14143
|
+
/**
|
|
14144
|
+
* Announces the watch link once the run is confirmed live-entitled. Gating on
|
|
14145
|
+
* the SAME resolved entitlement the streamer requires (`jwt` + the `'live'`
|
|
14146
|
+
* feature) guarantees the link is never printed or posted for a run that will
|
|
14147
|
+
* never stream — no dead links. The chat ping fires from the primary shard only
|
|
14148
|
+
* (unsharded, or `shard.current === 1`) so an N-way matrix posts one message,
|
|
14149
|
+
* not N. The banner still prints from every shard's own log.
|
|
14150
|
+
*/
|
|
14151
|
+
async announceLive(watchUrl, env, shard) {
|
|
14152
|
+
const info = await this.license();
|
|
14153
|
+
if (!info?.jwt || !info.features?.includes("live")) return;
|
|
14154
|
+
logger.info(`Live Tracker: ${watchUrl}`);
|
|
14155
|
+
console.log(formatLiveBanner(watchUrl));
|
|
14156
|
+
const isPrimaryShard = !shard || shard.current === 1;
|
|
14157
|
+
const notify = this.options.notify;
|
|
14158
|
+
if (isPrimaryShard && notify) {
|
|
14159
|
+
await new NotificationSender().sendLiveStarted(notify, watchUrl, env?.branch);
|
|
14160
|
+
}
|
|
14161
|
+
}
|
|
14078
14162
|
resolveProjectName() {
|
|
14079
14163
|
try {
|
|
14080
14164
|
const pkgPath = import_path12.default.resolve(process.cwd(), "package.json");
|
package/dist/index.mjs
CHANGED
|
@@ -13320,6 +13320,22 @@ _${name}_`
|
|
|
13320
13320
|
|
|
13321
13321
|
// src/notify/formatters/teams.ts
|
|
13322
13322
|
init_esm_shims();
|
|
13323
|
+
function buildTeamsSimpleMessage(text) {
|
|
13324
|
+
return {
|
|
13325
|
+
type: "message",
|
|
13326
|
+
attachments: [
|
|
13327
|
+
{
|
|
13328
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
13329
|
+
content: {
|
|
13330
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
13331
|
+
type: "AdaptiveCard",
|
|
13332
|
+
version: "1.4",
|
|
13333
|
+
body: [{ type: "TextBlock", text, wrap: true }]
|
|
13334
|
+
}
|
|
13335
|
+
}
|
|
13336
|
+
]
|
|
13337
|
+
};
|
|
13338
|
+
}
|
|
13323
13339
|
function buildTeamsPayload(stats, pdfPaths, reportTitle) {
|
|
13324
13340
|
const label = statusLabel(stats.verdict);
|
|
13325
13341
|
const name = reportName(pdfPaths, reportTitle);
|
|
@@ -13526,6 +13542,30 @@ var NotificationSender = class {
|
|
|
13526
13542
|
}
|
|
13527
13543
|
return result;
|
|
13528
13544
|
}
|
|
13545
|
+
/**
|
|
13546
|
+
* Fire-and-forget "live run started" ping to the enabled chat channels, carrying
|
|
13547
|
+
* the watch URL so teammates can open the live view without hunting through CI
|
|
13548
|
+
* logs. Only channels with `on: 'always'` receive it — a start ping has no
|
|
13549
|
+
* pass/fail outcome, so a `failure`/`success`-only channel is left alone. Never
|
|
13550
|
+
* touches the email channel (too heavy for a start signal). Resolves even if
|
|
13551
|
+
* every post fails; live is best-effort.
|
|
13552
|
+
*/
|
|
13553
|
+
async sendLiveStarted(notifyConfig, watchUrl, branch) {
|
|
13554
|
+
if (!notifyConfig) return;
|
|
13555
|
+
const line = `ReportForge: live test run started${branch ? ` on ${branch}` : ""}. Watch: ${watchUrl}`;
|
|
13556
|
+
const payloads = {
|
|
13557
|
+
slack: { text: line },
|
|
13558
|
+
teams: buildTeamsSimpleMessage(line),
|
|
13559
|
+
discord: { content: line }
|
|
13560
|
+
};
|
|
13561
|
+
await Promise.allSettled(
|
|
13562
|
+
Object.keys(payloads).map((name) => {
|
|
13563
|
+
const channel = notifyConfig[name];
|
|
13564
|
+
if (!channel || !channel.enabled || channel.on !== "always") return Promise.resolve();
|
|
13565
|
+
return this.post(`${name} (live-start)`, channel.url, payloads[name]);
|
|
13566
|
+
})
|
|
13567
|
+
);
|
|
13568
|
+
}
|
|
13529
13569
|
async post(name, url, payload, result) {
|
|
13530
13570
|
const controller = new AbortController();
|
|
13531
13571
|
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -13538,10 +13578,10 @@ var NotificationSender = class {
|
|
|
13538
13578
|
});
|
|
13539
13579
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
13540
13580
|
logger.info(`[notify] ${name}: sent`);
|
|
13541
|
-
result
|
|
13581
|
+
result?.sent.push(name);
|
|
13542
13582
|
} catch (err) {
|
|
13543
13583
|
logger.warn(`[notify] ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13544
|
-
result
|
|
13584
|
+
result?.failed.push(name);
|
|
13545
13585
|
} finally {
|
|
13546
13586
|
clearTimeout(timeoutId);
|
|
13547
13587
|
}
|
|
@@ -13583,7 +13623,8 @@ var LiveStreamer = class {
|
|
|
13583
13623
|
}
|
|
13584
13624
|
enqueueTest(update) {
|
|
13585
13625
|
if (!this.enabled) return;
|
|
13586
|
-
this.testStates.
|
|
13626
|
+
const prevRetry = this.testStates.get(update.id)?.retry;
|
|
13627
|
+
this.testStates.set(update.id, { status: update.status, retry: update.retry ?? prevRetry });
|
|
13587
13628
|
this.testBuffer.push(update);
|
|
13588
13629
|
this.capPreLicenseBuffer();
|
|
13589
13630
|
this.scheduleFlush();
|
|
@@ -13717,7 +13758,6 @@ var LiveStreamer = class {
|
|
|
13717
13758
|
return false;
|
|
13718
13759
|
}
|
|
13719
13760
|
this.jwt = info.jwt;
|
|
13720
|
-
if (this.config.watchUrl) logger.info(`Live results: ${this.config.watchUrl}`);
|
|
13721
13761
|
return true;
|
|
13722
13762
|
}
|
|
13723
13763
|
async send(body) {
|
|
@@ -13786,6 +13826,13 @@ function buildWatchUrl(serverUrl, runId, token) {
|
|
|
13786
13826
|
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
13787
13827
|
return `${base}/live/${encodeURIComponent(runId)}?t=${encodeURIComponent(token)}`;
|
|
13788
13828
|
}
|
|
13829
|
+
function formatLiveBanner(url) {
|
|
13830
|
+
const label = "Live Tracker";
|
|
13831
|
+
const link = `Watch live: ${url}`;
|
|
13832
|
+
const width = Math.max(label.length, link.length);
|
|
13833
|
+
const rule = "\u2500".repeat(width + 2);
|
|
13834
|
+
return [rule, ` ${label.padEnd(width)} `, ` ${link.padEnd(width)} `, rule].join("\n");
|
|
13835
|
+
}
|
|
13789
13836
|
function shardKeyOf(shard) {
|
|
13790
13837
|
if (!shard || !shard.total) return "1/1";
|
|
13791
13838
|
return `${shard.current}/${shard.total}`;
|
|
@@ -13820,6 +13867,17 @@ function shouldEmitStep(step, mode) {
|
|
|
13820
13867
|
if (step.error) return true;
|
|
13821
13868
|
return step.category === "test.step" || step.category === "expect";
|
|
13822
13869
|
}
|
|
13870
|
+
function isCurrentStep(step) {
|
|
13871
|
+
return step.category === "test.step" && !!step.title;
|
|
13872
|
+
}
|
|
13873
|
+
function mapCurrentStep(testId, step, shardKey) {
|
|
13874
|
+
return {
|
|
13875
|
+
id: testId,
|
|
13876
|
+
status: "running",
|
|
13877
|
+
shardKey,
|
|
13878
|
+
currentStep: clampWithEllipsis(step.title ?? "", MAX_STEP_TITLE_CHARS)
|
|
13879
|
+
};
|
|
13880
|
+
}
|
|
13823
13881
|
function mapStep(testId, step, phase, shardKey) {
|
|
13824
13882
|
return {
|
|
13825
13883
|
kind: "step",
|
|
@@ -13854,7 +13912,7 @@ var PdfReporter = class {
|
|
|
13854
13912
|
this.liveConsole = false;
|
|
13855
13913
|
this.options = parseOptions(rawOptions);
|
|
13856
13914
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
13857
|
-
const version = true ? "0.
|
|
13915
|
+
const version = true ? "0.10.0" : "0.x";
|
|
13858
13916
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13859
13917
|
this.licenseClient = new LicenseClient({
|
|
13860
13918
|
licenseKey: this.options.licenseKey,
|
|
@@ -13888,9 +13946,14 @@ var PdfReporter = class {
|
|
|
13888
13946
|
}
|
|
13889
13947
|
}
|
|
13890
13948
|
onStepBegin(test, _result, step) {
|
|
13891
|
-
if (!this.liveStreamer
|
|
13949
|
+
if (!this.liveStreamer) return;
|
|
13892
13950
|
try {
|
|
13893
|
-
this.
|
|
13951
|
+
if (this.liveSteps !== "none" && isCurrentStep(step)) {
|
|
13952
|
+
this.liveStreamer.enqueueTest(mapCurrentStep(test.id, step, this.liveShardKey));
|
|
13953
|
+
}
|
|
13954
|
+
if (shouldEmitStep(step, this.liveSteps)) {
|
|
13955
|
+
this.liveStreamer.enqueueEvent(mapStep(test.id, step, "begin", this.liveShardKey));
|
|
13956
|
+
}
|
|
13894
13957
|
} catch {
|
|
13895
13958
|
}
|
|
13896
13959
|
}
|
|
@@ -14065,17 +14128,38 @@ var PdfReporter = class {
|
|
|
14065
14128
|
void this.license();
|
|
14066
14129
|
const token = computeWatchToken(runId);
|
|
14067
14130
|
const watchUrl = token ? buildWatchUrl(serverUrl, runId, token) : void 0;
|
|
14131
|
+
const env = detectCiEnv();
|
|
14132
|
+
if (watchUrl) void this.announceLive(watchUrl, env, shard).catch(() => {
|
|
14133
|
+
});
|
|
14068
14134
|
this.liveStreamer = new LiveStreamer({
|
|
14069
14135
|
serverUrl,
|
|
14070
14136
|
runId,
|
|
14071
14137
|
shardKey: this.liveShardKey,
|
|
14072
14138
|
shardTotal: shard?.total ?? null,
|
|
14073
14139
|
license: () => this.license(),
|
|
14074
|
-
env
|
|
14075
|
-
watchUrl,
|
|
14140
|
+
env,
|
|
14076
14141
|
flushMs: live.flushMs
|
|
14077
14142
|
});
|
|
14078
14143
|
}
|
|
14144
|
+
/**
|
|
14145
|
+
* Announces the watch link once the run is confirmed live-entitled. Gating on
|
|
14146
|
+
* the SAME resolved entitlement the streamer requires (`jwt` + the `'live'`
|
|
14147
|
+
* feature) guarantees the link is never printed or posted for a run that will
|
|
14148
|
+
* never stream — no dead links. The chat ping fires from the primary shard only
|
|
14149
|
+
* (unsharded, or `shard.current === 1`) so an N-way matrix posts one message,
|
|
14150
|
+
* not N. The banner still prints from every shard's own log.
|
|
14151
|
+
*/
|
|
14152
|
+
async announceLive(watchUrl, env, shard) {
|
|
14153
|
+
const info = await this.license();
|
|
14154
|
+
if (!info?.jwt || !info.features?.includes("live")) return;
|
|
14155
|
+
logger.info(`Live Tracker: ${watchUrl}`);
|
|
14156
|
+
console.log(formatLiveBanner(watchUrl));
|
|
14157
|
+
const isPrimaryShard = !shard || shard.current === 1;
|
|
14158
|
+
const notify = this.options.notify;
|
|
14159
|
+
if (isPrimaryShard && notify) {
|
|
14160
|
+
await new NotificationSender().sendLiveStarted(notify, watchUrl, env?.branch);
|
|
14161
|
+
}
|
|
14162
|
+
}
|
|
14079
14163
|
resolveProjectName() {
|
|
14080
14164
|
try {
|
|
14081
14165
|
const pkgPath = path14.resolve(process.cwd(), "package.json");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|