@reportforge/playwright-pdf 0.8.1 → 0.9.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 +28 -0
- package/dist/cli/index.js +17 -1
- package/dist/index.d.mts +62 -1
- package/dist/index.d.ts +62 -1
- package/dist/index.js +459 -12
- package/dist/index.mjs +455 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10484,6 +10484,21 @@ var captureConfigSchema = external_exports.object({
|
|
|
10484
10484
|
maxSteps: external_exports.number().int().min(1).max(500).default(50),
|
|
10485
10485
|
maxConsoleLines: external_exports.number().int().min(1).max(1e3).default(50)
|
|
10486
10486
|
}).default({});
|
|
10487
|
+
var liveConfigSchema = external_exports.object({
|
|
10488
|
+
enabled: external_exports.boolean().default(false),
|
|
10489
|
+
// Override the auto-derived run id. All shards of one run must share it.
|
|
10490
|
+
runId: external_exports.string().optional(),
|
|
10491
|
+
// Override the streaming/server base URL (defaults to the license serverUrl).
|
|
10492
|
+
serverUrl: external_exports.string().url().optional(),
|
|
10493
|
+
// Step verbosity: 'none' skips steps, 'failed' streams intent + assertions +
|
|
10494
|
+
// errored steps, 'all' streams every step.
|
|
10495
|
+
steps: external_exports.enum(["none", "failed", "all"]).default("failed"),
|
|
10496
|
+
// Stream test stdout/stderr tails. Off by default — may leak secrets printed
|
|
10497
|
+
// by tests to a token-holder.
|
|
10498
|
+
console: external_exports.boolean().default(false),
|
|
10499
|
+
// Debounce window for batched flushes (ms).
|
|
10500
|
+
flushMs: external_exports.number().int().min(500).max(1e4).default(2e3)
|
|
10501
|
+
}).default({});
|
|
10487
10502
|
var sectionFlagsShape = Object.fromEntries(
|
|
10488
10503
|
SECTION_KEYS.map((k) => [k, external_exports.boolean().optional()])
|
|
10489
10504
|
);
|
|
@@ -10525,6 +10540,7 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10525
10540
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10526
10541
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
10527
10542
|
capture: captureConfigSchema,
|
|
10543
|
+
live: liveConfigSchema,
|
|
10528
10544
|
templatePath: external_exports.union([
|
|
10529
10545
|
external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
|
|
10530
10546
|
external_exports.array(
|
|
@@ -10890,13 +10906,21 @@ var LicenseClient = class {
|
|
|
10890
10906
|
this.autoUpdateModel = opts.autoUpdateModel ?? true;
|
|
10891
10907
|
this.modelVersionForServer = this.autoUpdateModel ? opts.currentModelVersion ?? 0 : Number.MAX_SAFE_INTEGER;
|
|
10892
10908
|
}
|
|
10893
|
-
|
|
10909
|
+
/**
|
|
10910
|
+
* @param opts.requireFeature When set, a fresh cached JWT that does NOT carry
|
|
10911
|
+
* this feature is bypassed in favour of a network refresh — so a token issued
|
|
10912
|
+
* before the feature (e.g. `'live'`) was added to entitlements doesn't gate it
|
|
10913
|
+
* off until natural TTL expiry. If the refresh also lacks it, the caller gets
|
|
10914
|
+
* a LicenseInfo without the feature (genuinely not entitled).
|
|
10915
|
+
*/
|
|
10916
|
+
async resolve(opts = {}) {
|
|
10894
10917
|
const key = this.rawKey || process.env.RF_LICENSE_KEY?.trim();
|
|
10895
10918
|
const init = this._validateKey(key);
|
|
10896
10919
|
if (!init) return null;
|
|
10897
10920
|
const { normalized, verifier } = init;
|
|
10898
10921
|
const { cacheValid, cached, cacheFresh, nowMs } = this._readCache(verifier);
|
|
10899
|
-
|
|
10922
|
+
const cacheHasFeature = !opts.requireFeature || (cacheValid?.features?.includes(opts.requireFeature) ?? false);
|
|
10923
|
+
if (cacheFresh && cacheValid && cached && cacheHasFeature) {
|
|
10900
10924
|
logger.debug("license path=jwt-cache", {
|
|
10901
10925
|
exp: new Date(cacheValid.exp * 1e3).toISOString(),
|
|
10902
10926
|
machinesUsed: cached.machinesUsed,
|
|
@@ -10961,7 +10985,8 @@ var LicenseClient = class {
|
|
|
10961
10985
|
lastRefresh: Math.floor(Date.now() / 1e3),
|
|
10962
10986
|
fingerprintHash: this.fingerprint.hash,
|
|
10963
10987
|
machinesUsed: result.machinesUsed,
|
|
10964
|
-
machineLimit: result.machineLimit
|
|
10988
|
+
machineLimit: result.machineLimit,
|
|
10989
|
+
features: payload.features
|
|
10965
10990
|
};
|
|
10966
10991
|
writeCache(entry);
|
|
10967
10992
|
logger.debug(`license path=${cached ? "refresh" : "activate"} response`, {
|
|
@@ -10979,7 +11004,8 @@ var LicenseClient = class {
|
|
|
10979
11004
|
jwt: result.jwt,
|
|
10980
11005
|
expiry: new Date(payload.exp * 1e3),
|
|
10981
11006
|
machinesUsed: result.machinesUsed,
|
|
10982
|
-
machineLimit: result.machineLimit
|
|
11007
|
+
machineLimit: result.machineLimit,
|
|
11008
|
+
features: payload.features
|
|
10983
11009
|
};
|
|
10984
11010
|
} catch (err) {
|
|
10985
11011
|
const msg = err.message;
|
|
@@ -11067,7 +11093,11 @@ function toLicenseInfo(payload, cached, normalizedKey, source) {
|
|
|
11067
11093
|
// caches written by older reporter versions before these fields were
|
|
11068
11094
|
// persisted (the next refresh will populate them).
|
|
11069
11095
|
machinesUsed: cached.machinesUsed,
|
|
11070
|
-
machineLimit: cached.machineLimit
|
|
11096
|
+
machineLimit: cached.machineLimit,
|
|
11097
|
+
// Prefer the signed `features` claim (authenticated) over the unauthenticated
|
|
11098
|
+
// sidecar — falls back to the sidecar only for caches written before the
|
|
11099
|
+
// payload was threaded through here.
|
|
11100
|
+
features: payload.features ?? cached.features
|
|
11071
11101
|
};
|
|
11072
11102
|
}
|
|
11073
11103
|
function normalizeUrl(url) {
|
|
@@ -11686,6 +11716,7 @@ function sortBySeverity(failures) {
|
|
|
11686
11716
|
// src/utils/env.ts
|
|
11687
11717
|
init_cjs_shims();
|
|
11688
11718
|
var import_child_process = require("child_process");
|
|
11719
|
+
var import_crypto5 = require("crypto");
|
|
11689
11720
|
var COMMIT_HASH_LENGTH = 8;
|
|
11690
11721
|
function detectCiEnv() {
|
|
11691
11722
|
const env = process.env;
|
|
@@ -11756,6 +11787,34 @@ function detectCiEnv() {
|
|
|
11756
11787
|
ciProvider: "local"
|
|
11757
11788
|
};
|
|
11758
11789
|
}
|
|
11790
|
+
var CI_RUN_SOURCES = [
|
|
11791
|
+
{ provider: "github.com", repoKeys: ["GITHUB_REPOSITORY"], runKeys: ["GITHUB_RUN_ID"], attemptKeys: ["GITHUB_RUN_ATTEMPT"] },
|
|
11792
|
+
{ provider: "gitlab.com", repoKeys: ["CI_PROJECT_PATH"], runKeys: ["CI_PIPELINE_ID"] },
|
|
11793
|
+
{ provider: "circleci.com", repoKeys: ["CIRCLE_PROJECT_REPONAME"], runKeys: ["CIRCLE_WORKFLOW_ID"] },
|
|
11794
|
+
{ provider: "buildkite.com", repoKeys: ["BUILDKITE_REPO"], runKeys: ["BUILDKITE_BUILD_ID"] },
|
|
11795
|
+
{ provider: "dev.azure.com", repoKeys: ["BUILD_REPOSITORY_NAME"], runKeys: ["BUILD_BUILDID"] },
|
|
11796
|
+
{ provider: "bitbucket.org", repoKeys: ["BITBUCKET_REPO_FULL_NAME"], runKeys: ["BITBUCKET_PIPELINE_UUID"] },
|
|
11797
|
+
{ provider: "jenkins", repoKeys: ["JOB_NAME"], runKeys: ["BUILD_NUMBER"] }
|
|
11798
|
+
];
|
|
11799
|
+
var RUN_ID_FALLBACK_SECRET = "rf-live-runid";
|
|
11800
|
+
function detectCiRunId() {
|
|
11801
|
+
for (const src of CI_RUN_SOURCES) {
|
|
11802
|
+
const runId = firstEnv(src.runKeys);
|
|
11803
|
+
if (!runId) continue;
|
|
11804
|
+
const repo = firstEnv(src.repoKeys) ?? "unknown";
|
|
11805
|
+
const attempt = src.attemptKeys ? firstEnv(src.attemptKeys) ?? "1" : "1";
|
|
11806
|
+
const secret = process.env.RF_LICENSE_KEY?.trim() || RUN_ID_FALLBACK_SECRET;
|
|
11807
|
+
return (0, import_crypto5.createHmac)("sha256", secret).update(`${src.provider}:${repo}:${runId}:${attempt}`).digest("hex").slice(0, 32);
|
|
11808
|
+
}
|
|
11809
|
+
return null;
|
|
11810
|
+
}
|
|
11811
|
+
function firstEnv(keys) {
|
|
11812
|
+
for (const k of keys) {
|
|
11813
|
+
const v = process.env[k]?.trim();
|
|
11814
|
+
if (v) return v;
|
|
11815
|
+
}
|
|
11816
|
+
return void 0;
|
|
11817
|
+
}
|
|
11759
11818
|
function gitBranch() {
|
|
11760
11819
|
try {
|
|
11761
11820
|
return (0, import_child_process.execSync)("git rev-parse --abbrev-ref HEAD", { stdio: "pipe" }).toString().trim();
|
|
@@ -12678,11 +12737,11 @@ init_cjs_shims();
|
|
|
12678
12737
|
var import_fs7 = __toESM(require("fs"));
|
|
12679
12738
|
var import_os4 = __toESM(require("os"));
|
|
12680
12739
|
var import_path4 = __toESM(require("path"));
|
|
12681
|
-
var
|
|
12740
|
+
var import_crypto6 = __toESM(require("crypto"));
|
|
12682
12741
|
var HtmlWriter = class {
|
|
12683
12742
|
async write(html) {
|
|
12684
12743
|
const tmpDir = import_os4.default.tmpdir();
|
|
12685
|
-
const filename = `rf-report-${
|
|
12744
|
+
const filename = `rf-report-${import_crypto6.default.randomBytes(8).toString("hex")}.html`;
|
|
12686
12745
|
this.tempPath = import_path4.default.join(tmpDir, filename);
|
|
12687
12746
|
await import_fs7.default.promises.writeFile(this.tempPath, html, "utf8");
|
|
12688
12747
|
return this.tempPath;
|
|
@@ -12726,7 +12785,7 @@ var import_child_process3 = require("child_process");
|
|
|
12726
12785
|
var import_fs8 = __toESM(require("fs"));
|
|
12727
12786
|
var import_path5 = __toESM(require("path"));
|
|
12728
12787
|
var import_os5 = __toESM(require("os"));
|
|
12729
|
-
var
|
|
12788
|
+
var import_crypto7 = __toESM(require("crypto"));
|
|
12730
12789
|
var PdfEncryptor = class {
|
|
12731
12790
|
async encrypt(pdfPath, password) {
|
|
12732
12791
|
if (!password) return;
|
|
@@ -12737,7 +12796,7 @@ var PdfEncryptor = class {
|
|
|
12737
12796
|
return;
|
|
12738
12797
|
}
|
|
12739
12798
|
const tmpPath = import_path5.default.join(import_os5.default.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
|
|
12740
|
-
const pwFile = import_path5.default.join(import_os5.default.tmpdir(), `rf-pw-${
|
|
12799
|
+
const pwFile = import_path5.default.join(import_os5.default.tmpdir(), `rf-pw-${import_crypto7.default.randomBytes(8).toString("hex")}`);
|
|
12741
12800
|
try {
|
|
12742
12801
|
import_fs8.default.writeFileSync(pwFile, `${password}
|
|
12743
12802
|
${password}`, { mode: 384 });
|
|
@@ -13488,14 +13547,313 @@ var NotificationSender = class {
|
|
|
13488
13547
|
}
|
|
13489
13548
|
};
|
|
13490
13549
|
|
|
13550
|
+
// src/live/LiveStreamer.ts
|
|
13551
|
+
init_cjs_shims();
|
|
13552
|
+
var NETWORK_TIMEOUT_MS2 = 8e3;
|
|
13553
|
+
var LIVE_DRAIN_DEADLINE_MS = 1e4;
|
|
13554
|
+
var MAX_FLUSH_MS = 3e4;
|
|
13555
|
+
var WARN_THROTTLE_MS = 3e4;
|
|
13556
|
+
var LiveStreamer = class {
|
|
13557
|
+
constructor(config) {
|
|
13558
|
+
this.config = config;
|
|
13559
|
+
this.testBuffer = [];
|
|
13560
|
+
this.eventBuffer = [];
|
|
13561
|
+
/** Monotonic per-shard event counter → stable `{shardKey}:{seq}` event ids. */
|
|
13562
|
+
this.eventSeq = 0;
|
|
13563
|
+
/**
|
|
13564
|
+
* Latest status per test id (last write wins), so retries don't inflate the
|
|
13565
|
+
* tallies: a flaky test that fails then passes is one entry, counted once.
|
|
13566
|
+
* Counts are derived from this map, matching the collector/PDF semantics.
|
|
13567
|
+
*/
|
|
13568
|
+
this.testStates = /* @__PURE__ */ new Map();
|
|
13569
|
+
this.flushTimer = null;
|
|
13570
|
+
this.inFlight = /* @__PURE__ */ new Set();
|
|
13571
|
+
this.enabled = true;
|
|
13572
|
+
this.gated = false;
|
|
13573
|
+
this.gatePromise = null;
|
|
13574
|
+
this.jwt = null;
|
|
13575
|
+
this.envSent = false;
|
|
13576
|
+
this.lastWarnMs = 0;
|
|
13577
|
+
this.serverUrl = config.serverUrl.endsWith("/") ? config.serverUrl.slice(0, -1) : config.serverUrl;
|
|
13578
|
+
this.flushMs = config.flushMs ?? 2e3;
|
|
13579
|
+
this.flushMaxBatch = config.maxBatch ?? 100;
|
|
13580
|
+
this.maxInFlight = config.maxInFlight ?? 4;
|
|
13581
|
+
this.maxPreLicenseBuffer = config.maxPreLicenseBuffer ?? 2e3;
|
|
13582
|
+
}
|
|
13583
|
+
enqueueTest(update) {
|
|
13584
|
+
if (!this.enabled) return;
|
|
13585
|
+
this.testStates.set(update.id, { status: update.status, retry: update.retry });
|
|
13586
|
+
this.testBuffer.push(update);
|
|
13587
|
+
this.capPreLicenseBuffer();
|
|
13588
|
+
this.scheduleFlush();
|
|
13589
|
+
}
|
|
13590
|
+
enqueueEvent(event) {
|
|
13591
|
+
if (!this.enabled) return;
|
|
13592
|
+
event.id = `${this.config.shardKey}:${this.eventSeq++}`;
|
|
13593
|
+
this.eventBuffer.push(event);
|
|
13594
|
+
this.capPreLicenseBuffer();
|
|
13595
|
+
this.scheduleFlush();
|
|
13596
|
+
}
|
|
13597
|
+
/**
|
|
13598
|
+
* Final flush: cancels the pending timer, emits the terminal `finished` batch,
|
|
13599
|
+
* then awaits in-flight requests bounded by LIVE_DRAIN_DEADLINE_MS. Called from
|
|
13600
|
+
* the reporter's `onExit`. The bound is the #16644 fix — a hung server cannot
|
|
13601
|
+
* hold the process open past the deadline.
|
|
13602
|
+
*/
|
|
13603
|
+
async drain() {
|
|
13604
|
+
if (this.flushTimer) {
|
|
13605
|
+
clearTimeout(this.flushTimer);
|
|
13606
|
+
this.flushTimer = null;
|
|
13607
|
+
}
|
|
13608
|
+
await this.flush("finished");
|
|
13609
|
+
await Promise.race([
|
|
13610
|
+
Promise.allSettled([...this.inFlight]),
|
|
13611
|
+
sleep(LIVE_DRAIN_DEADLINE_MS)
|
|
13612
|
+
]);
|
|
13613
|
+
}
|
|
13614
|
+
// ─── internals ────────────────────────────────────────────────────────────
|
|
13615
|
+
snapshotCounts() {
|
|
13616
|
+
const c = {
|
|
13617
|
+
total: this.testStates.size,
|
|
13618
|
+
running: 0,
|
|
13619
|
+
passed: 0,
|
|
13620
|
+
failed: 0,
|
|
13621
|
+
timedOut: 0,
|
|
13622
|
+
skipped: 0,
|
|
13623
|
+
flaky: 0
|
|
13624
|
+
};
|
|
13625
|
+
for (const { status, retry } of this.testStates.values()) {
|
|
13626
|
+
switch (status) {
|
|
13627
|
+
case "running":
|
|
13628
|
+
c.running++;
|
|
13629
|
+
break;
|
|
13630
|
+
case "passed":
|
|
13631
|
+
retry && retry > 0 ? c.flaky++ : c.passed++;
|
|
13632
|
+
break;
|
|
13633
|
+
case "failed":
|
|
13634
|
+
c.failed++;
|
|
13635
|
+
break;
|
|
13636
|
+
case "timedOut":
|
|
13637
|
+
c.timedOut++;
|
|
13638
|
+
break;
|
|
13639
|
+
case "skipped":
|
|
13640
|
+
c.skipped++;
|
|
13641
|
+
break;
|
|
13642
|
+
case "interrupted":
|
|
13643
|
+
c.failed++;
|
|
13644
|
+
break;
|
|
13645
|
+
}
|
|
13646
|
+
}
|
|
13647
|
+
return c;
|
|
13648
|
+
}
|
|
13649
|
+
/** Drop-oldest cap on buffered items while the license gate is still pending. */
|
|
13650
|
+
capPreLicenseBuffer() {
|
|
13651
|
+
if (this.gated) return;
|
|
13652
|
+
let overflow = this.testBuffer.length + this.eventBuffer.length - this.maxPreLicenseBuffer;
|
|
13653
|
+
if (overflow <= 0) return;
|
|
13654
|
+
const dropEvents = Math.min(overflow, this.eventBuffer.length);
|
|
13655
|
+
if (dropEvents > 0) this.eventBuffer.splice(0, dropEvents);
|
|
13656
|
+
overflow -= dropEvents;
|
|
13657
|
+
if (overflow > 0) this.testBuffer.splice(0, overflow);
|
|
13658
|
+
}
|
|
13659
|
+
scheduleFlush() {
|
|
13660
|
+
if (this.testBuffer.length + this.eventBuffer.length >= this.flushMaxBatch) {
|
|
13661
|
+
void this.flush("running");
|
|
13662
|
+
return;
|
|
13663
|
+
}
|
|
13664
|
+
if (this.flushTimer) return;
|
|
13665
|
+
this.flushTimer = setTimeout(() => {
|
|
13666
|
+
this.flushTimer = null;
|
|
13667
|
+
void this.flush("running");
|
|
13668
|
+
}, this.flushMs);
|
|
13669
|
+
if (typeof this.flushTimer.unref === "function") this.flushTimer.unref();
|
|
13670
|
+
}
|
|
13671
|
+
async flush(status) {
|
|
13672
|
+
if (!this.enabled) return;
|
|
13673
|
+
const entitled = await this.ensureGate();
|
|
13674
|
+
if (!entitled) return;
|
|
13675
|
+
if (status !== "finished" && this.inFlight.size >= this.maxInFlight) return;
|
|
13676
|
+
const tests = this.testBuffer;
|
|
13677
|
+
const events = this.eventBuffer;
|
|
13678
|
+
if (status !== "finished" && tests.length === 0 && events.length === 0) return;
|
|
13679
|
+
this.testBuffer = [];
|
|
13680
|
+
this.eventBuffer = [];
|
|
13681
|
+
const body = {
|
|
13682
|
+
runId: this.config.runId,
|
|
13683
|
+
shardKey: this.config.shardKey,
|
|
13684
|
+
shardTotal: this.config.shardTotal,
|
|
13685
|
+
status,
|
|
13686
|
+
counts: this.snapshotCounts(),
|
|
13687
|
+
tests,
|
|
13688
|
+
events,
|
|
13689
|
+
// Keep attaching env until a send actually succeeds (envSent flips in
|
|
13690
|
+
// send()), so a failed first request doesn't lose the run's CI context.
|
|
13691
|
+
...this.envSent ? {} : { env: this.config.env }
|
|
13692
|
+
};
|
|
13693
|
+
const p = this.send(body).finally(() => {
|
|
13694
|
+
this.inFlight.delete(p);
|
|
13695
|
+
});
|
|
13696
|
+
this.inFlight.add(p);
|
|
13697
|
+
}
|
|
13698
|
+
/** Runs the license entitlement check exactly once, memoized. */
|
|
13699
|
+
ensureGate() {
|
|
13700
|
+
return this.gatePromise ?? (this.gatePromise = this.runGate());
|
|
13701
|
+
}
|
|
13702
|
+
async runGate() {
|
|
13703
|
+
let info = null;
|
|
13704
|
+
try {
|
|
13705
|
+
info = await this.config.license();
|
|
13706
|
+
} catch {
|
|
13707
|
+
info = null;
|
|
13708
|
+
}
|
|
13709
|
+
this.gated = true;
|
|
13710
|
+
const features = info?.features ?? [];
|
|
13711
|
+
if (!info || !info.jwt || !features.includes("live")) {
|
|
13712
|
+
this.enabled = false;
|
|
13713
|
+
this.testBuffer = [];
|
|
13714
|
+
this.eventBuffer = [];
|
|
13715
|
+
this.warn("Live streaming disabled \u2014 no active license or live entitlement.");
|
|
13716
|
+
return false;
|
|
13717
|
+
}
|
|
13718
|
+
this.jwt = info.jwt;
|
|
13719
|
+
if (this.config.watchUrl) logger.info(`Live results: ${this.config.watchUrl}`);
|
|
13720
|
+
return true;
|
|
13721
|
+
}
|
|
13722
|
+
async send(body) {
|
|
13723
|
+
if (!this.jwt) return;
|
|
13724
|
+
const controller = new AbortController();
|
|
13725
|
+
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS2);
|
|
13726
|
+
try {
|
|
13727
|
+
const res = await fetch(`${this.serverUrl}/api/live/ingest`, {
|
|
13728
|
+
method: "POST",
|
|
13729
|
+
headers: {
|
|
13730
|
+
"content-type": "application/json",
|
|
13731
|
+
authorization: `Bearer ${this.jwt}`
|
|
13732
|
+
},
|
|
13733
|
+
body: JSON.stringify(body),
|
|
13734
|
+
signal: controller.signal
|
|
13735
|
+
});
|
|
13736
|
+
await res.body?.cancel().catch(() => {
|
|
13737
|
+
});
|
|
13738
|
+
if (res.status === 429) {
|
|
13739
|
+
this.flushMs = Math.min(this.flushMs * 2, MAX_FLUSH_MS);
|
|
13740
|
+
return;
|
|
13741
|
+
}
|
|
13742
|
+
if (!res.ok) {
|
|
13743
|
+
this.warn(`Live ingest returned HTTP ${res.status}.`);
|
|
13744
|
+
return;
|
|
13745
|
+
}
|
|
13746
|
+
if (body.env) this.envSent = true;
|
|
13747
|
+
} catch (err) {
|
|
13748
|
+
this.warn(`Live ingest request failed: ${err.message}`);
|
|
13749
|
+
} finally {
|
|
13750
|
+
clearTimeout(timeout);
|
|
13751
|
+
}
|
|
13752
|
+
}
|
|
13753
|
+
warn(msg) {
|
|
13754
|
+
const now = Date.now();
|
|
13755
|
+
if (now - this.lastWarnMs < WARN_THROTTLE_MS) return;
|
|
13756
|
+
this.lastWarnMs = now;
|
|
13757
|
+
logger.warn(msg);
|
|
13758
|
+
}
|
|
13759
|
+
};
|
|
13760
|
+
function sleep(ms) {
|
|
13761
|
+
return new Promise((resolve) => {
|
|
13762
|
+
const t = setTimeout(resolve, ms);
|
|
13763
|
+
if (typeof t.unref === "function") t.unref();
|
|
13764
|
+
});
|
|
13765
|
+
}
|
|
13766
|
+
|
|
13767
|
+
// src/live/runId.ts
|
|
13768
|
+
init_cjs_shims();
|
|
13769
|
+
var import_crypto8 = require("crypto");
|
|
13770
|
+
var import_crypto9 = require("crypto");
|
|
13771
|
+
var WATCH_TOKEN_HEX_LEN = 32;
|
|
13772
|
+
function deriveRunId(opts) {
|
|
13773
|
+
const explicit = opts.runId?.trim();
|
|
13774
|
+
if (explicit) return explicit;
|
|
13775
|
+
const fromEnv = process.env.RF_LIVE_RUN_ID?.trim();
|
|
13776
|
+
if (fromEnv) return fromEnv;
|
|
13777
|
+
return detectCiRunId() ?? (0, import_crypto9.randomUUID)();
|
|
13778
|
+
}
|
|
13779
|
+
function computeWatchToken(runId) {
|
|
13780
|
+
const secret = process.env.RF_LICENSE_KEY?.trim();
|
|
13781
|
+
if (!secret) return null;
|
|
13782
|
+
return (0, import_crypto8.createHmac)("sha256", secret).update(`live:${runId}`).digest("hex").slice(0, WATCH_TOKEN_HEX_LEN);
|
|
13783
|
+
}
|
|
13784
|
+
function buildWatchUrl(serverUrl, runId, token) {
|
|
13785
|
+
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
13786
|
+
return `${base}/live/${encodeURIComponent(runId)}?t=${encodeURIComponent(token)}`;
|
|
13787
|
+
}
|
|
13788
|
+
function shardKeyOf(shard) {
|
|
13789
|
+
if (!shard || !shard.total) return "1/1";
|
|
13790
|
+
return `${shard.current}/${shard.total}`;
|
|
13791
|
+
}
|
|
13792
|
+
|
|
13793
|
+
// src/live/event-map.ts
|
|
13794
|
+
init_cjs_shims();
|
|
13795
|
+
var MAX_TITLE_CHARS2 = 200;
|
|
13796
|
+
var MAX_STEP_TITLE_CHARS = 200;
|
|
13797
|
+
var MAX_CONSOLE_CHARS = 2048;
|
|
13798
|
+
function mapTestBegin(test, shardKey) {
|
|
13799
|
+
return {
|
|
13800
|
+
id: test.id,
|
|
13801
|
+
title: clampWithEllipsis(test.title ?? "", MAX_TITLE_CHARS2),
|
|
13802
|
+
status: "running",
|
|
13803
|
+
shardKey
|
|
13804
|
+
};
|
|
13805
|
+
}
|
|
13806
|
+
function mapTestEnd(test, result, shardKey) {
|
|
13807
|
+
return {
|
|
13808
|
+
id: test.id,
|
|
13809
|
+
status: result.status,
|
|
13810
|
+
durationMs: result.duration,
|
|
13811
|
+
retry: result.retry,
|
|
13812
|
+
shardKey
|
|
13813
|
+
};
|
|
13814
|
+
}
|
|
13815
|
+
function shouldEmitStep(step, mode) {
|
|
13816
|
+
if (mode === "none") return false;
|
|
13817
|
+
if (!step.title) return false;
|
|
13818
|
+
if (mode === "all") return true;
|
|
13819
|
+
if (step.error) return true;
|
|
13820
|
+
return step.category === "test.step" || step.category === "expect";
|
|
13821
|
+
}
|
|
13822
|
+
function mapStep(testId, step, phase, shardKey) {
|
|
13823
|
+
return {
|
|
13824
|
+
kind: "step",
|
|
13825
|
+
testId,
|
|
13826
|
+
title: clampWithEllipsis(step.title ?? "", MAX_STEP_TITLE_CHARS),
|
|
13827
|
+
status: phase === "begin" ? "running" : step.error ? "failed" : "passed",
|
|
13828
|
+
shardKey,
|
|
13829
|
+
at: Date.now()
|
|
13830
|
+
};
|
|
13831
|
+
}
|
|
13832
|
+
function mapConsole(kind, chunk, testId, shardKey) {
|
|
13833
|
+
return {
|
|
13834
|
+
kind,
|
|
13835
|
+
testId,
|
|
13836
|
+
text: clampWithEllipsis(chunkToString2(chunk), MAX_CONSOLE_CHARS),
|
|
13837
|
+
shardKey,
|
|
13838
|
+
at: Date.now()
|
|
13839
|
+
};
|
|
13840
|
+
}
|
|
13841
|
+
function chunkToString2(chunk) {
|
|
13842
|
+
return typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
13843
|
+
}
|
|
13844
|
+
|
|
13491
13845
|
// src/reporter.ts
|
|
13846
|
+
var DEFAULT_SERVER_URL2 = "https://reportforge.org";
|
|
13492
13847
|
var PdfReporter = class {
|
|
13493
13848
|
constructor(rawOptions = {}) {
|
|
13494
13849
|
this.cwd = process.cwd();
|
|
13495
13850
|
this.pdfGenerator = new PdfGenerator();
|
|
13851
|
+
this.liveShardKey = "1/1";
|
|
13852
|
+
this.liveSteps = "failed";
|
|
13853
|
+
this.liveConsole = false;
|
|
13496
13854
|
this.options = parseOptions(rawOptions);
|
|
13497
13855
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
13498
|
-
const version = true ? "0.
|
|
13856
|
+
const version = true ? "0.9.0" : "0.x";
|
|
13499
13857
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13500
13858
|
this.licenseClient = new LicenseClient({
|
|
13501
13859
|
licenseKey: this.options.licenseKey,
|
|
@@ -13506,12 +13864,65 @@ var PdfReporter = class {
|
|
|
13506
13864
|
}
|
|
13507
13865
|
onBegin(config, suite) {
|
|
13508
13866
|
this.dataCollector.onBegin(config, suite);
|
|
13867
|
+
if (!this.options.live.enabled) return;
|
|
13868
|
+
try {
|
|
13869
|
+
this.setupLive(config);
|
|
13870
|
+
} catch (err) {
|
|
13871
|
+
logger.warn(`Live setup failed \u2014 streaming disabled: ${err.message}`);
|
|
13872
|
+
}
|
|
13873
|
+
}
|
|
13874
|
+
onTestBegin(test) {
|
|
13875
|
+
if (!this.liveStreamer) return;
|
|
13876
|
+
try {
|
|
13877
|
+
this.liveStreamer.enqueueTest(mapTestBegin(test, this.liveShardKey));
|
|
13878
|
+
} catch {
|
|
13879
|
+
}
|
|
13509
13880
|
}
|
|
13510
13881
|
onTestEnd(test, result) {
|
|
13511
13882
|
this.dataCollector.onTestEnd(test, result);
|
|
13883
|
+
if (!this.liveStreamer) return;
|
|
13884
|
+
try {
|
|
13885
|
+
this.liveStreamer.enqueueTest(mapTestEnd(test, result, this.liveShardKey));
|
|
13886
|
+
} catch {
|
|
13887
|
+
}
|
|
13888
|
+
}
|
|
13889
|
+
onStepBegin(test, _result, step) {
|
|
13890
|
+
if (!this.liveStreamer || !shouldEmitStep(step, this.liveSteps)) return;
|
|
13891
|
+
try {
|
|
13892
|
+
this.liveStreamer.enqueueEvent(mapStep(test.id, step, "begin", this.liveShardKey));
|
|
13893
|
+
} catch {
|
|
13894
|
+
}
|
|
13895
|
+
}
|
|
13896
|
+
onStepEnd(test, _result, step) {
|
|
13897
|
+
if (!this.liveStreamer || !shouldEmitStep(step, this.liveSteps)) return;
|
|
13898
|
+
try {
|
|
13899
|
+
this.liveStreamer.enqueueEvent(mapStep(test.id, step, "end", this.liveShardKey));
|
|
13900
|
+
} catch {
|
|
13901
|
+
}
|
|
13902
|
+
}
|
|
13903
|
+
onStdOut(chunk, test) {
|
|
13904
|
+
if (!this.liveStreamer || !this.liveConsole) return;
|
|
13905
|
+
try {
|
|
13906
|
+
this.liveStreamer.enqueueEvent(mapConsole("stdout", chunk, test?.id, this.liveShardKey));
|
|
13907
|
+
} catch {
|
|
13908
|
+
}
|
|
13909
|
+
}
|
|
13910
|
+
onStdErr(chunk, test) {
|
|
13911
|
+
if (!this.liveStreamer || !this.liveConsole) return;
|
|
13912
|
+
try {
|
|
13913
|
+
this.liveStreamer.enqueueEvent(mapConsole("stderr", chunk, test?.id, this.liveShardKey));
|
|
13914
|
+
} catch {
|
|
13915
|
+
}
|
|
13916
|
+
}
|
|
13917
|
+
async onExit() {
|
|
13918
|
+
if (!this.liveStreamer) return;
|
|
13919
|
+
try {
|
|
13920
|
+
await this.liveStreamer.drain();
|
|
13921
|
+
} catch {
|
|
13922
|
+
}
|
|
13512
13923
|
}
|
|
13513
13924
|
async onEnd(result) {
|
|
13514
|
-
const licenseInfo = await this.
|
|
13925
|
+
const licenseInfo = await this.license();
|
|
13515
13926
|
if (!licenseInfo) return;
|
|
13516
13927
|
const collected = this._collectData(result);
|
|
13517
13928
|
if (!collected) return;
|
|
@@ -13618,9 +14029,18 @@ var PdfReporter = class {
|
|
|
13618
14029
|
printsToStdio() {
|
|
13619
14030
|
return false;
|
|
13620
14031
|
}
|
|
14032
|
+
/**
|
|
14033
|
+
* Memoized license resolution shared by the live stream (kicked early in
|
|
14034
|
+
* onBegin) and PDF generation (awaited in onEnd) — at most one network trip.
|
|
14035
|
+
*/
|
|
14036
|
+
license() {
|
|
14037
|
+
return this.licensePromise ?? (this.licensePromise = this.resolveLicense());
|
|
14038
|
+
}
|
|
13621
14039
|
async resolveLicense() {
|
|
13622
14040
|
try {
|
|
13623
|
-
return await this.licenseClient.resolve(
|
|
14041
|
+
return await this.licenseClient.resolve(
|
|
14042
|
+
this.options.live.enabled ? { requireFeature: "live" } : {}
|
|
14043
|
+
);
|
|
13624
14044
|
} catch (err) {
|
|
13625
14045
|
logger.warn(
|
|
13626
14046
|
`License resolution errored: ${err.message}. PDF generation skipped.`
|
|
@@ -13628,6 +14048,33 @@ var PdfReporter = class {
|
|
|
13628
14048
|
return null;
|
|
13629
14049
|
}
|
|
13630
14050
|
}
|
|
14051
|
+
/**
|
|
14052
|
+
* Wires up live streaming: derives the run id (shared by all shards), kicks
|
|
14053
|
+
* the license resolution early, builds the streamer, and prints the watch
|
|
14054
|
+
* link to the CI logs. Throws are caught by the onBegin try/catch.
|
|
14055
|
+
*/
|
|
14056
|
+
setupLive(config) {
|
|
14057
|
+
const live = this.options.live;
|
|
14058
|
+
const runId = deriveRunId({ runId: live.runId });
|
|
14059
|
+
const serverUrl = live.serverUrl ?? this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL2;
|
|
14060
|
+
const shard = config.shard ?? null;
|
|
14061
|
+
this.liveShardKey = shardKeyOf(shard);
|
|
14062
|
+
this.liveSteps = live.steps;
|
|
14063
|
+
this.liveConsole = live.console;
|
|
14064
|
+
void this.license();
|
|
14065
|
+
const token = computeWatchToken(runId);
|
|
14066
|
+
const watchUrl = token ? buildWatchUrl(serverUrl, runId, token) : void 0;
|
|
14067
|
+
this.liveStreamer = new LiveStreamer({
|
|
14068
|
+
serverUrl,
|
|
14069
|
+
runId,
|
|
14070
|
+
shardKey: this.liveShardKey,
|
|
14071
|
+
shardTotal: shard?.total ?? null,
|
|
14072
|
+
license: () => this.license(),
|
|
14073
|
+
env: detectCiEnv(),
|
|
14074
|
+
watchUrl,
|
|
14075
|
+
flushMs: live.flushMs
|
|
14076
|
+
});
|
|
14077
|
+
}
|
|
13631
14078
|
resolveProjectName() {
|
|
13632
14079
|
try {
|
|
13633
14080
|
const pkgPath = import_path12.default.resolve(process.cwd(), "package.json");
|