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