@reportforge/playwright-pdf 0.9.0 → 0.10.1
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 +4 -2
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +118 -13
- package/dist/index.mjs +118 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -211,7 +211,9 @@ 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 test shows its steps as sub-lines beneath its own row, and the PDF is still generated at the end, unchanged.
|
|
215
|
+
|
|
216
|
+
> The watch link is entitlement-gated. If you enable `live` but see no `Live Tracker` line, the reporter logs why (missing `RF_LICENSE_KEY`, or a cached token without the `live` entitlement — delete `~/.reportforge/license.json` to force re-activation).
|
|
215
217
|
|
|
216
218
|
```typescript
|
|
217
219
|
// playwright.config.ts
|
|
@@ -230,7 +232,7 @@ reporter: [
|
|
|
230
232
|
|
|
231
233
|
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
234
|
|
|
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.
|
|
235
|
+
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
236
|
|
|
235
237
|
> **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
238
|
|
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();
|
|
@@ -13712,11 +13753,10 @@ var LiveStreamer = class {
|
|
|
13712
13753
|
this.enabled = false;
|
|
13713
13754
|
this.testBuffer = [];
|
|
13714
13755
|
this.eventBuffer = [];
|
|
13715
|
-
|
|
13756
|
+
logger.debug("Live streaming disabled \u2014 no active license or live entitlement.");
|
|
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) {
|
|
@@ -13776,8 +13816,8 @@ function deriveRunId(opts) {
|
|
|
13776
13816
|
if (fromEnv) return fromEnv;
|
|
13777
13817
|
return detectCiRunId() ?? (0, import_crypto9.randomUUID)();
|
|
13778
13818
|
}
|
|
13779
|
-
function computeWatchToken(runId) {
|
|
13780
|
-
const secret = process.env.RF_LICENSE_KEY?.trim();
|
|
13819
|
+
function computeWatchToken(runId, key) {
|
|
13820
|
+
const secret = key?.trim() || process.env.RF_LICENSE_KEY?.trim();
|
|
13781
13821
|
if (!secret) return null;
|
|
13782
13822
|
return (0, import_crypto8.createHmac)("sha256", secret).update(`live:${runId}`).digest("hex").slice(0, WATCH_TOKEN_HEX_LEN);
|
|
13783
13823
|
}
|
|
@@ -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.1" : "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
|
}
|
|
@@ -14062,19 +14125,61 @@ var PdfReporter = class {
|
|
|
14062
14125
|
this.liveSteps = live.steps;
|
|
14063
14126
|
this.liveConsole = live.console;
|
|
14064
14127
|
void this.license();
|
|
14065
|
-
const token = computeWatchToken(runId);
|
|
14128
|
+
const token = computeWatchToken(runId, this.options.licenseKey);
|
|
14066
14129
|
const watchUrl = token ? buildWatchUrl(serverUrl, runId, token) : void 0;
|
|
14130
|
+
const env = detectCiEnv();
|
|
14131
|
+
if (watchUrl) {
|
|
14132
|
+
void this.announceLive(watchUrl, env, shard).catch(() => {
|
|
14133
|
+
});
|
|
14134
|
+
} else {
|
|
14135
|
+
logger.warn(
|
|
14136
|
+
"Live is enabled but no RF_LICENSE_KEY was found \u2014 cannot mint a watch link or stream. Set RF_LICENSE_KEY (or the `licenseKey` option)."
|
|
14137
|
+
);
|
|
14138
|
+
}
|
|
14067
14139
|
this.liveStreamer = new LiveStreamer({
|
|
14068
14140
|
serverUrl,
|
|
14069
14141
|
runId,
|
|
14070
14142
|
shardKey: this.liveShardKey,
|
|
14071
14143
|
shardTotal: shard?.total ?? null,
|
|
14072
14144
|
license: () => this.license(),
|
|
14073
|
-
env
|
|
14074
|
-
watchUrl,
|
|
14145
|
+
env,
|
|
14075
14146
|
flushMs: live.flushMs
|
|
14076
14147
|
});
|
|
14077
14148
|
}
|
|
14149
|
+
/**
|
|
14150
|
+
* Announces the watch link once the run is confirmed live-entitled. Gating on
|
|
14151
|
+
* the SAME resolved entitlement the streamer requires (`jwt` + the `'live'`
|
|
14152
|
+
* feature) guarantees the link is never printed or posted for a run that will
|
|
14153
|
+
* never stream — no dead links. The chat ping fires from the primary shard only
|
|
14154
|
+
* (unsharded, or `shard.current === 1`) so an N-way matrix posts one message,
|
|
14155
|
+
* not N. The banner still prints from every shard's own log.
|
|
14156
|
+
*/
|
|
14157
|
+
async announceLive(watchUrl, env, shard) {
|
|
14158
|
+
const info = await this.license();
|
|
14159
|
+
if (!info) {
|
|
14160
|
+
logger.warn("Live is enabled but no active license resolved \u2014 no watch link or streaming.");
|
|
14161
|
+
return;
|
|
14162
|
+
}
|
|
14163
|
+
if (!info.jwt) {
|
|
14164
|
+
logger.warn(
|
|
14165
|
+
"Live is enabled but the license has no signed token (offline with no cached token?) \u2014 no streaming."
|
|
14166
|
+
);
|
|
14167
|
+
return;
|
|
14168
|
+
}
|
|
14169
|
+
if (!info.features?.includes("live")) {
|
|
14170
|
+
logger.warn(
|
|
14171
|
+
"Live is enabled but your license token does not include the 'live' entitlement \u2014 no watch link or streaming. This usually means a cached token predates live streaming and could not be refreshed. Ensure network access on the next run, or delete ~/.reportforge/license.json to force re-activation. If it persists on an active subscription, contact support."
|
|
14172
|
+
);
|
|
14173
|
+
return;
|
|
14174
|
+
}
|
|
14175
|
+
logger.info(`Live Tracker: ${watchUrl}`);
|
|
14176
|
+
console.log(formatLiveBanner(watchUrl));
|
|
14177
|
+
const isPrimaryShard = !shard || shard.current === 1;
|
|
14178
|
+
const notify = this.options.notify;
|
|
14179
|
+
if (isPrimaryShard && notify) {
|
|
14180
|
+
await new NotificationSender().sendLiveStarted(notify, watchUrl, env?.branch);
|
|
14181
|
+
}
|
|
14182
|
+
}
|
|
14078
14183
|
resolveProjectName() {
|
|
14079
14184
|
try {
|
|
14080
14185
|
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();
|
|
@@ -13713,11 +13754,10 @@ var LiveStreamer = class {
|
|
|
13713
13754
|
this.enabled = false;
|
|
13714
13755
|
this.testBuffer = [];
|
|
13715
13756
|
this.eventBuffer = [];
|
|
13716
|
-
|
|
13757
|
+
logger.debug("Live streaming disabled \u2014 no active license or live entitlement.");
|
|
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) {
|
|
@@ -13777,8 +13817,8 @@ function deriveRunId(opts) {
|
|
|
13777
13817
|
if (fromEnv) return fromEnv;
|
|
13778
13818
|
return detectCiRunId() ?? randomUUID2();
|
|
13779
13819
|
}
|
|
13780
|
-
function computeWatchToken(runId) {
|
|
13781
|
-
const secret = process.env.RF_LICENSE_KEY?.trim();
|
|
13820
|
+
function computeWatchToken(runId, key) {
|
|
13821
|
+
const secret = key?.trim() || process.env.RF_LICENSE_KEY?.trim();
|
|
13782
13822
|
if (!secret) return null;
|
|
13783
13823
|
return createHmac3("sha256", secret).update(`live:${runId}`).digest("hex").slice(0, WATCH_TOKEN_HEX_LEN);
|
|
13784
13824
|
}
|
|
@@ -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.1" : "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
|
}
|
|
@@ -14063,19 +14126,61 @@ var PdfReporter = class {
|
|
|
14063
14126
|
this.liveSteps = live.steps;
|
|
14064
14127
|
this.liveConsole = live.console;
|
|
14065
14128
|
void this.license();
|
|
14066
|
-
const token = computeWatchToken(runId);
|
|
14129
|
+
const token = computeWatchToken(runId, this.options.licenseKey);
|
|
14067
14130
|
const watchUrl = token ? buildWatchUrl(serverUrl, runId, token) : void 0;
|
|
14131
|
+
const env = detectCiEnv();
|
|
14132
|
+
if (watchUrl) {
|
|
14133
|
+
void this.announceLive(watchUrl, env, shard).catch(() => {
|
|
14134
|
+
});
|
|
14135
|
+
} else {
|
|
14136
|
+
logger.warn(
|
|
14137
|
+
"Live is enabled but no RF_LICENSE_KEY was found \u2014 cannot mint a watch link or stream. Set RF_LICENSE_KEY (or the `licenseKey` option)."
|
|
14138
|
+
);
|
|
14139
|
+
}
|
|
14068
14140
|
this.liveStreamer = new LiveStreamer({
|
|
14069
14141
|
serverUrl,
|
|
14070
14142
|
runId,
|
|
14071
14143
|
shardKey: this.liveShardKey,
|
|
14072
14144
|
shardTotal: shard?.total ?? null,
|
|
14073
14145
|
license: () => this.license(),
|
|
14074
|
-
env
|
|
14075
|
-
watchUrl,
|
|
14146
|
+
env,
|
|
14076
14147
|
flushMs: live.flushMs
|
|
14077
14148
|
});
|
|
14078
14149
|
}
|
|
14150
|
+
/**
|
|
14151
|
+
* Announces the watch link once the run is confirmed live-entitled. Gating on
|
|
14152
|
+
* the SAME resolved entitlement the streamer requires (`jwt` + the `'live'`
|
|
14153
|
+
* feature) guarantees the link is never printed or posted for a run that will
|
|
14154
|
+
* never stream — no dead links. The chat ping fires from the primary shard only
|
|
14155
|
+
* (unsharded, or `shard.current === 1`) so an N-way matrix posts one message,
|
|
14156
|
+
* not N. The banner still prints from every shard's own log.
|
|
14157
|
+
*/
|
|
14158
|
+
async announceLive(watchUrl, env, shard) {
|
|
14159
|
+
const info = await this.license();
|
|
14160
|
+
if (!info) {
|
|
14161
|
+
logger.warn("Live is enabled but no active license resolved \u2014 no watch link or streaming.");
|
|
14162
|
+
return;
|
|
14163
|
+
}
|
|
14164
|
+
if (!info.jwt) {
|
|
14165
|
+
logger.warn(
|
|
14166
|
+
"Live is enabled but the license has no signed token (offline with no cached token?) \u2014 no streaming."
|
|
14167
|
+
);
|
|
14168
|
+
return;
|
|
14169
|
+
}
|
|
14170
|
+
if (!info.features?.includes("live")) {
|
|
14171
|
+
logger.warn(
|
|
14172
|
+
"Live is enabled but your license token does not include the 'live' entitlement \u2014 no watch link or streaming. This usually means a cached token predates live streaming and could not be refreshed. Ensure network access on the next run, or delete ~/.reportforge/license.json to force re-activation. If it persists on an active subscription, contact support."
|
|
14173
|
+
);
|
|
14174
|
+
return;
|
|
14175
|
+
}
|
|
14176
|
+
logger.info(`Live Tracker: ${watchUrl}`);
|
|
14177
|
+
console.log(formatLiveBanner(watchUrl));
|
|
14178
|
+
const isPrimaryShard = !shard || shard.current === 1;
|
|
14179
|
+
const notify = this.options.notify;
|
|
14180
|
+
if (isPrimaryShard && notify) {
|
|
14181
|
+
await new NotificationSender().sendLiveStarted(notify, watchUrl, env?.branch);
|
|
14182
|
+
}
|
|
14183
|
+
}
|
|
14079
14184
|
resolveProjectName() {
|
|
14080
14185
|
try {
|
|
14081
14186
|
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.1",
|
|
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",
|