squally-reporter 0.5.0 → 0.5.2
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 +7 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +173 -13
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -110,6 +110,13 @@ If Squally is unreachable partway through, results already sent stay in the
|
|
|
110
110
|
dashboard; only what could not be delivered is lost, and your test run's exit
|
|
111
111
|
code is never affected either way.
|
|
112
112
|
|
|
113
|
+
**Only actual test runs appear.** A run shows up in Squally once your first
|
|
114
|
+
test starts, so commands that never run a test create nothing: `--list`,
|
|
115
|
+
a `globalSetup` or `webServer` that fails to start, a `--grep` that matches
|
|
116
|
+
nothing. A run that *does* execute and legitimately contains no tests — an
|
|
117
|
+
empty shard, `--only-changed` with no changed test file, `--pass-with-no-tests`
|
|
118
|
+
— is still reported, as a run with zero results.
|
|
119
|
+
|
|
113
120
|
## Sharding
|
|
114
121
|
|
|
115
122
|
**Handled automatically - no extra setup needed.** Each shard is still a
|
package/dist/index.d.ts
CHANGED
|
@@ -69,10 +69,21 @@ export default class SquallyReporter implements Reporter {
|
|
|
69
69
|
private sendingDisabled;
|
|
70
70
|
private flushTimerHandle;
|
|
71
71
|
private inFlightFlushes;
|
|
72
|
+
private runStarted;
|
|
72
73
|
private runIdPromise;
|
|
73
74
|
private runId;
|
|
74
75
|
constructor(options?: SquallyReporterOptions);
|
|
75
76
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
77
|
+
/**
|
|
78
|
+
* Create the run server-side. Idempotent, and deliberately not called from
|
|
79
|
+
* onBegin.
|
|
80
|
+
*
|
|
81
|
+
* The flush timer is armed here rather than in onBegin for the same reason:
|
|
82
|
+
* nothing can be in the buffer before a test has ended, and a timer that can
|
|
83
|
+
* only ever fire on an empty buffer is a timer with nothing to do.
|
|
84
|
+
*/
|
|
85
|
+
private startRun;
|
|
86
|
+
onTestBegin(): void;
|
|
76
87
|
private resolveRunConfig;
|
|
77
88
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
78
89
|
onEnd: (result: FullResult) => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -295,6 +295,39 @@ function detectAuthorName() {
|
|
|
295
295
|
// the same escape-hatch pattern this file already uses for
|
|
296
296
|
// SAME_REPO_AS_APP/TESTED_REVISION below.
|
|
297
297
|
//
|
|
298
|
+
// Is this invocation `playwright test --list` - "collect all the tests and
|
|
299
|
+
// report them, but do not run" (the flag's own help text)?
|
|
300
|
+
//
|
|
301
|
+
// WHY THIS IS AN OPTIMISATION AND NOT THE MECHANISM. A list run is a customer
|
|
302
|
+
// enumerating tests: in CI, in an editor, or from this repo's own AGENTS.md,
|
|
303
|
+
// which cites `--list` as a check. It is not a test run, and until 0.5.2 the
|
|
304
|
+
// reporter created one anyway - a completed run with zero results, consuming a
|
|
305
|
+
// run number and a slot in the customer's monthly quota.
|
|
306
|
+
//
|
|
307
|
+
// MEASURED, both directions, on 1.62.1 and on 1.30.0 (this package's peer
|
|
308
|
+
// floor):
|
|
309
|
+
// - `config.argv` is documented ("Snapshot of process.argv captured in the
|
|
310
|
+
// runner process") and carries `--list` on 1.62.1. On 1.30.0 the field does
|
|
311
|
+
// not exist at all - not in the types, not on the object. So it cannot be
|
|
312
|
+
// the mechanism for a package whose peer range starts at 1.30.
|
|
313
|
+
// - `process.argv` carries `--list` on BOTH, because a reporter is loaded in
|
|
314
|
+
// the runner process, which is the CLI process. Hence the fallback.
|
|
315
|
+
// - There is no `listOnly` on FullConfig on either version. The only other
|
|
316
|
+
// difference between a list run and a real one is behavioural: onBegin
|
|
317
|
+
// fires with the suite fully populated, and then no test ever begins.
|
|
318
|
+
//
|
|
319
|
+
// That behavioural fingerprint is what the run-creation deferral in the class
|
|
320
|
+
// below relies on, and it is why THIS function is allowed to be imperfect: an
|
|
321
|
+
// invocation that lists tests without `--list` on its argv - an IDE driving the
|
|
322
|
+
// runner through its own entry point, say - is still covered, because it also
|
|
323
|
+
// never begins a test. What this function buys on top is silence: a list run
|
|
324
|
+
// makes no network call at all, not even the rules GET, which matters when an
|
|
325
|
+
// editor lists tests on every keystroke-triggered reload.
|
|
326
|
+
function isListOnlyInvocation(config) {
|
|
327
|
+
const configArgv = config.argv;
|
|
328
|
+
const argv = Array.isArray(configArgv) ? configArgv : process.argv;
|
|
329
|
+
return argv.some((arg) => arg === "--list");
|
|
330
|
+
}
|
|
298
331
|
// env and generateFallback are parameters, not process.env/randomUUID read
|
|
299
332
|
// directly, so this is unit-testable without mocking global state.
|
|
300
333
|
function detectCiRunId(env, generateFallback) {
|
|
@@ -640,10 +673,45 @@ function confirmReasonMessage(reason) {
|
|
|
640
673
|
function artifactLabel(type) {
|
|
641
674
|
return type === "screenshot" ? "Screenshot" : "Trace";
|
|
642
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Wait, WITHOUT letting the process die during the wait.
|
|
678
|
+
*
|
|
679
|
+
* The timer is deliberately NOT unref'd, and that is the whole content of this
|
|
680
|
+
* function. An unref'd timer does not hold Node's event loop open, so a promise
|
|
681
|
+
* that only an unref'd timer can resolve is a promise the process may never
|
|
682
|
+
* wait for: if nothing else is pending, Node treats the loop as empty and exits
|
|
683
|
+
* THERE, with whatever `process.exitCode` happens to be - which is 0.
|
|
684
|
+
*
|
|
685
|
+
* That shipped in 0.5.0 and made a FAILING test suite report a GREEN CI job:
|
|
686
|
+
*
|
|
687
|
+
* 0.4.0 await new Promise((resolve) => setTimeout(resolve, EXIT_DRAIN_MS));
|
|
688
|
+
* 0.5.0 await delay(EXIT_DRAIN_MS); // helper below, which unref'd
|
|
689
|
+
*
|
|
690
|
+
* Playwright computes the exit code only AFTER awaiting every reporter's
|
|
691
|
+
* onEnd - the runner multiplexer awaits `reporter.onEnd(result)` and the CLI
|
|
692
|
+
* then calls `gracefullyProcessExitDoNotHang(status === "passed" ? 0 : 1)`.
|
|
693
|
+
* Exiting during onEnd skips the line that would have set 1. Measured: the same
|
|
694
|
+
* 250 ms wait exits 0 when unref'd and 1 when not, on a suite with a failing
|
|
695
|
+
* test.
|
|
696
|
+
*
|
|
697
|
+
* EVERY caller is awaited from inside onEnd, directly or through
|
|
698
|
+
* drainAndComplete's retries, so every one of them needs this. There is no
|
|
699
|
+
* "this wait is unimportant" case: a wait nobody needs should not be awaited at
|
|
700
|
+
* all. The first fix for the bug above kept an opt-out for the retry backoffs
|
|
701
|
+
* and was still wrong - a server returning 503 on /complete reproduced the
|
|
702
|
+
* green-on-red exit through exactly that path (measured, 10.09.).
|
|
703
|
+
*
|
|
704
|
+
* Ref'ing costs nothing here: Playwright ends the process with `process.exit()`,
|
|
705
|
+
* which does not wait for pending timers, so a still-running backoff cannot
|
|
706
|
+
* delay anyone's CI.
|
|
707
|
+
*
|
|
708
|
+
* NOT to be confused with the two unref'd timers elsewhere in this file, both
|
|
709
|
+
* of which are correct: withDeadline's timer races a real promise, and the
|
|
710
|
+
* periodic flush timer is never awaited by anything.
|
|
711
|
+
*/
|
|
643
712
|
function delay(ms) {
|
|
644
713
|
return new Promise((resolve) => {
|
|
645
|
-
|
|
646
|
-
handle.unref();
|
|
714
|
+
setTimeout(resolve, ms);
|
|
647
715
|
});
|
|
648
716
|
}
|
|
649
717
|
// Retry-After per RFC 9110: either an integer number of seconds, or an
|
|
@@ -711,9 +779,15 @@ class SquallyReporter {
|
|
|
711
779
|
this.sendingDisabled = false;
|
|
712
780
|
this.flushTimerHandle = null;
|
|
713
781
|
this.inFlightFlushes = new Set();
|
|
714
|
-
//
|
|
782
|
+
// Has the run been created server-side? Set by startRun(), which fires on
|
|
783
|
+
// the first test rather than in onBegin (0.5.2). While this is false, this
|
|
784
|
+
// process has sent nothing that could become a run in the dashboard.
|
|
785
|
+
this.runStarted = false;
|
|
786
|
+
// Resolves to the runId once beginRun() succeeds, or to null on any permanent
|
|
715
787
|
// failure (never rejects - every failure path here is a warning, not an
|
|
716
|
-
// exception, same principle as the rest of this file).
|
|
788
|
+
// exception, same principle as the rest of this file). Stays the initial
|
|
789
|
+
// resolved-null promise until startRun() replaces it, which is what makes
|
|
790
|
+
// flush() a no-op for an invocation that never ran a test.
|
|
717
791
|
this.runIdPromise = Promise.resolve(null);
|
|
718
792
|
// Set once runIdPromise resolves to a real id - read by onEnd's artifact
|
|
719
793
|
// upload step, which needs a plain value rather than a promise it would
|
|
@@ -754,6 +828,9 @@ class SquallyReporter {
|
|
|
754
828
|
// See EXIT_DRAIN_MS - closing the dispatcher resolves before the
|
|
755
829
|
// underlying sockets are fully gone, and Playwright exits the process
|
|
756
830
|
// the moment onEnd() resolves.
|
|
831
|
+
// This wait exists precisely to keep the process alive while sockets
|
|
832
|
+
// finish closing - see delay(), and test/exit-code's failing-suite
|
|
833
|
+
// scenario, which is what catches it if the unref ever comes back.
|
|
757
834
|
await delay(EXIT_DRAIN_MS);
|
|
758
835
|
}
|
|
759
836
|
};
|
|
@@ -776,6 +853,15 @@ class SquallyReporter {
|
|
|
776
853
|
onBegin(config, suite) {
|
|
777
854
|
if (!this.enabled)
|
|
778
855
|
return;
|
|
856
|
+
// A list run is not a run. Disabling the reporter outright is what makes
|
|
857
|
+
// every later hook - and onEnd's dispatcher.close() - a no-op, so nothing
|
|
858
|
+
// below this line has to know about the case. Silent on purpose: `--list`
|
|
859
|
+
// output is parsed by tooling, and a warning printed on every editor
|
|
860
|
+
// refresh would be noise about a decision the customer did not make.
|
|
861
|
+
if (isListOnlyInvocation(config)) {
|
|
862
|
+
this.enabled = false;
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
779
865
|
this.startedAt = new Date();
|
|
780
866
|
this.dispatcher = new undici_1.Agent({
|
|
781
867
|
keepAliveTimeout: 4000,
|
|
@@ -799,10 +885,44 @@ class SquallyReporter {
|
|
|
799
885
|
this.runTags =
|
|
800
886
|
tags.length > 0 ? tags.slice(0, MAX_TAGS).map((tag) => truncate(tag, V2_FIELD_LIMITS.tag)) : undefined;
|
|
801
887
|
this.runConfig = this.resolveRunConfig(config);
|
|
802
|
-
|
|
888
|
+
// NO beginRun() HERE ANY MORE (0.5.2). Until 0.5.1 this line created the
|
|
889
|
+
// run server-side the moment Playwright started, which meant every
|
|
890
|
+
// invocation that never executed a test still produced one - measured
|
|
891
|
+
// against a logging mock: `--list`, a failing globalSetup and a webServer
|
|
892
|
+
// that will not start all reach onBegin and all created a completed,
|
|
893
|
+
// empty run. The run is now created by startRun() when the first test
|
|
894
|
+
// actually begins, and by drainAndComplete() for the one case where a run
|
|
895
|
+
// that executed nothing must still be reported (see there).
|
|
896
|
+
//
|
|
897
|
+
// The rules GET stays here: it has to be in flight BEFORE tests run to be
|
|
898
|
+
// of any use, it creates nothing, and it costs no quota.
|
|
803
899
|
void this.fetchRules();
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Create the run server-side. Idempotent, and deliberately not called from
|
|
903
|
+
* onBegin.
|
|
904
|
+
*
|
|
905
|
+
* The flush timer is armed here rather than in onBegin for the same reason:
|
|
906
|
+
* nothing can be in the buffer before a test has ended, and a timer that can
|
|
907
|
+
* only ever fire on an empty buffer is a timer with nothing to do.
|
|
908
|
+
*/
|
|
909
|
+
startRun() {
|
|
910
|
+
if (this.runStarted || !this.enabled)
|
|
911
|
+
return;
|
|
912
|
+
this.runStarted = true;
|
|
913
|
+
this.runIdPromise = this.beginRun();
|
|
804
914
|
this.armFlushTimer();
|
|
805
915
|
}
|
|
916
|
+
// The trigger. onTestBegin fires for every test Playwright actually
|
|
917
|
+
// executes, INCLUDING skipped ones - measured, both the statically skipped
|
|
918
|
+
// (`test.skip("name", fn)`) and the conditionally skipped
|
|
919
|
+
// (`test.skip(true, "reason")`) form - so a suite whose tests are all
|
|
920
|
+
// skipped is still a run, and still reported.
|
|
921
|
+
onTestBegin() {
|
|
922
|
+
if (!this.enabled)
|
|
923
|
+
return;
|
|
924
|
+
this.startRun();
|
|
925
|
+
}
|
|
806
926
|
// config.browsers ({name, version}[]) is deliberately OMITTED here, not
|
|
807
927
|
// sent as an empty/placeholder array. Getting REAL installed browser
|
|
808
928
|
// versions would mean either launching each browser type just to query it
|
|
@@ -827,6 +947,12 @@ class SquallyReporter {
|
|
|
827
947
|
onTestEnd(test, result) {
|
|
828
948
|
if (!this.enabled || this.sendingDisabled)
|
|
829
949
|
return;
|
|
950
|
+
// Belt and braces: onTestBegin has already done this for every test
|
|
951
|
+
// Playwright executes, but a result arriving without its begin would
|
|
952
|
+
// otherwise be buffered against a run that was never created and dropped
|
|
953
|
+
// silently at onEnd. startRun() is idempotent, so the ordinary path pays
|
|
954
|
+
// one boolean check.
|
|
955
|
+
this.startRun();
|
|
830
956
|
// Truncated HERE, once, not when the payload is built: testName/filePath
|
|
831
957
|
// are Squally's test identity, and /api/artifacts/presign resolves each
|
|
832
958
|
// artifact back to a TestResult through exactly these two values plus
|
|
@@ -917,17 +1043,51 @@ class SquallyReporter {
|
|
|
917
1043
|
// (deliberate divergence from v1) Once a runId exists, /complete is ALWAYS
|
|
918
1044
|
// called - there is no "this run had zero results, skip reporting it
|
|
919
1045
|
// entirely" special case, unlike v1 (`if (this.results.length === 0)
|
|
920
|
-
// return;`, guarding the whole of onEnd there). That check cannot be
|
|
1046
|
+
// return;`, guarding the whole of onEnd there). ~~That check cannot be
|
|
921
1047
|
// reproduced here: onBegin already created the run server-side, before a
|
|
922
|
-
// single test could have told us the eventual count would be zero
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
1048
|
+
// single test could have told us the eventual count would be zero.~~
|
|
1049
|
+
// CORRECTED 0.5.2: onBegin no longer creates it, so the decision IS
|
|
1050
|
+
// available here - and it is taken by the block at the top of the method
|
|
1051
|
+
// below, which is a stricter version of v1's check. What stands unchanged is
|
|
1052
|
+
// the reasoning about a run that HAS been created: skipping /complete for it
|
|
1053
|
+
// would not undo its existence, it would leave it stuck in `running` until
|
|
1054
|
+
// the timeout sweep notices (up to 18 hours, see docs/status.md's run-timeout
|
|
1055
|
+
// entry).
|
|
929
1056
|
async drainAndComplete(result) {
|
|
930
1057
|
this.stopFlushTimer();
|
|
1058
|
+
// NOTHING RAN. Two cases that look identical from here and must not be
|
|
1059
|
+
// treated identically (0.5.2):
|
|
1060
|
+
//
|
|
1061
|
+
// 1. The suite the customer asked for was EMPTY, and Playwright ran it
|
|
1062
|
+
// to completion anyway. `--pass-with-no-tests`, `--only-changed` on a
|
|
1063
|
+
// commit that touched no test file, and - the one that matters most -
|
|
1064
|
+
// a shard with nothing in it, which happens whenever there are fewer
|
|
1065
|
+
// tests than shards. All three exit 0 with FullResult.status
|
|
1066
|
+
// "passed", measured. These MUST still be reported: a customer who
|
|
1067
|
+
// filters to a tag with no tests and sees nothing at all concludes
|
|
1068
|
+
// the reporter is broken, and an unreported empty shard is worse than
|
|
1069
|
+
// that - its group would never reach its shard total and would sit
|
|
1070
|
+
// incomplete forever (squally-app docs/shard-ui-spec.md section 5.5).
|
|
1071
|
+
//
|
|
1072
|
+
// 2. Playwright never got as far as running the suite: a globalSetup
|
|
1073
|
+
// that threw, a webServer that would not start, `--grep` matching
|
|
1074
|
+
// nothing without `--pass-with-no-tests`, `--last-failed` with
|
|
1075
|
+
// nothing to repeat. All measured: onBegin fires, an onError follows,
|
|
1076
|
+
// and FullResult.status is "failed" - the run ends in an error state
|
|
1077
|
+
// having executed nothing. Reporting these produced an empty run
|
|
1078
|
+
// whose only content was that it was empty, at the cost of a run
|
|
1079
|
+
// number and a quota slot.
|
|
1080
|
+
//
|
|
1081
|
+
// The line between them is FullResult.status, and that is the whole rule:
|
|
1082
|
+
// Squally reports a run when Playwright ran the suite to completion,
|
|
1083
|
+
// whatever the suite turned out to contain, and does not when Playwright
|
|
1084
|
+
// never got that far. `--list` never reaches here at all - onBegin
|
|
1085
|
+
// disables the reporter outright.
|
|
1086
|
+
if (!this.runStarted) {
|
|
1087
|
+
if (result.status !== "passed")
|
|
1088
|
+
return;
|
|
1089
|
+
this.startRun();
|
|
1090
|
+
}
|
|
931
1091
|
const deadline = Date.now() + DRAIN_BUDGET_MS;
|
|
932
1092
|
const runId = await withDeadline(this.runIdPromise, deadline);
|
|
933
1093
|
if (runId === TIMED_OUT) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squally-reporter",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Playwright reporter that sends test results to your Squally dashboard.",
|
|
5
5
|
"author": "Squally <squally.dev@gmail.com>",
|
|
6
6
|
"keywords": [
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"prepare": "npm run build",
|
|
25
25
|
"test:unit": "node --test \"test/unit/**/*.test.js\"",
|
|
26
26
|
"test:exit-code": "node test/exit-code/run.mjs",
|
|
27
|
-
"test": "
|
|
27
|
+
"test:empty-runs": "node test/empty-runs/run.mjs",
|
|
28
|
+
"test": "npm run build && npm run test:unit && npm run test:exit-code && npm run test:empty-runs"
|
|
28
29
|
},
|
|
29
30
|
"peerDependencies": {
|
|
30
31
|
"@playwright/test": ">=1.30.0"
|