squally-reporter 0.5.1 → 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 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) {
@@ -746,9 +779,15 @@ class SquallyReporter {
746
779
  this.sendingDisabled = false;
747
780
  this.flushTimerHandle = null;
748
781
  this.inFlightFlushes = new Set();
749
- // Resolves to the runId once onBegin succeeds, or to null on any permanent
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
750
787
  // failure (never rejects - every failure path here is a warning, not an
751
- // 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.
752
791
  this.runIdPromise = Promise.resolve(null);
753
792
  // Set once runIdPromise resolves to a real id - read by onEnd's artifact
754
793
  // upload step, which needs a plain value rather than a promise it would
@@ -814,6 +853,15 @@ class SquallyReporter {
814
853
  onBegin(config, suite) {
815
854
  if (!this.enabled)
816
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
+ }
817
865
  this.startedAt = new Date();
818
866
  this.dispatcher = new undici_1.Agent({
819
867
  keepAliveTimeout: 4000,
@@ -837,10 +885,44 @@ class SquallyReporter {
837
885
  this.runTags =
838
886
  tags.length > 0 ? tags.slice(0, MAX_TAGS).map((tag) => truncate(tag, V2_FIELD_LIMITS.tag)) : undefined;
839
887
  this.runConfig = this.resolveRunConfig(config);
840
- this.runIdPromise = this.beginRun();
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.
841
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();
842
914
  this.armFlushTimer();
843
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
+ }
844
926
  // config.browsers ({name, version}[]) is deliberately OMITTED here, not
845
927
  // sent as an empty/placeholder array. Getting REAL installed browser
846
928
  // versions would mean either launching each browser type just to query it
@@ -865,6 +947,12 @@ class SquallyReporter {
865
947
  onTestEnd(test, result) {
866
948
  if (!this.enabled || this.sendingDisabled)
867
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();
868
956
  // Truncated HERE, once, not when the payload is built: testName/filePath
869
957
  // are Squally's test identity, and /api/artifacts/presign resolves each
870
958
  // artifact back to a TestResult through exactly these two values plus
@@ -955,17 +1043,51 @@ class SquallyReporter {
955
1043
  // (deliberate divergence from v1) Once a runId exists, /complete is ALWAYS
956
1044
  // called - there is no "this run had zero results, skip reporting it
957
1045
  // entirely" special case, unlike v1 (`if (this.results.length === 0)
958
- // return;`, guarding the whole of onEnd there). That check cannot be
1046
+ // return;`, guarding the whole of onEnd there). ~~That check cannot be
959
1047
  // reproduced here: onBegin already created the run server-side, before a
960
- // single test could have told us the eventual count would be zero.
961
- // Skipping /complete at this point would not undo the run's existence - it
962
- // would leave an already-created run stuck in `running` until the timeout
963
- // sweep notices (currently up to 18 hours, see docs/status.md's run-timeout
964
- // entry). Completing it immediately with zero results is strictly better: a
965
- // short-lived empty run in the dashboard costs less than an
966
- // apparently-stuck one for the better part of a day.
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).
967
1056
  async drainAndComplete(result) {
968
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
+ }
969
1091
  const deadline = Date.now() + DRAIN_BUDGET_MS;
970
1092
  const runId = await withDeadline(this.runIdPromise, deadline);
971
1093
  if (runId === TIMED_OUT) {
package/package.json CHANGED
@@ -1,41 +1,42 @@
1
- {
2
- "name": "squally-reporter",
3
- "version": "0.5.1",
4
- "description": "Playwright reporter that sends test results to your Squally dashboard.",
5
- "author": "Squally <squally.dev@gmail.com>",
6
- "keywords": [
7
- "playwright",
8
- "playwright-reporter",
9
- "flaky-tests",
10
- "test-results",
11
- "ci"
12
- ],
13
- "main": "dist/index.js",
14
- "types": "dist/index.d.ts",
15
- "files": [
16
- "dist"
17
- ],
18
- "engines": {
19
- "node": ">=22.19.0"
20
- },
21
- "scripts": {
22
- "build": "tsc",
23
- "prepublishOnly": "npm run build",
24
- "prepare": "npm run build",
25
- "test:unit": "node --test \"test/unit/**/*.test.js\"",
26
- "test:exit-code": "node test/exit-code/run.mjs",
27
- "test": "npm run build && npm run test:unit && npm run test:exit-code"
28
- },
29
- "peerDependencies": {
30
- "@playwright/test": ">=1.30.0"
31
- },
32
- "devDependencies": {
33
- "@playwright/test": "^1.48.0",
34
- "@types/node": "^20.14.0",
35
- "typescript": "^5.6.0"
36
- },
37
- "license": "MIT",
38
- "dependencies": {
39
- "undici": "^8.10.0"
40
- }
41
- }
1
+ {
2
+ "name": "squally-reporter",
3
+ "version": "0.5.2",
4
+ "description": "Playwright reporter that sends test results to your Squally dashboard.",
5
+ "author": "Squally <squally.dev@gmail.com>",
6
+ "keywords": [
7
+ "playwright",
8
+ "playwright-reporter",
9
+ "flaky-tests",
10
+ "test-results",
11
+ "ci"
12
+ ],
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "engines": {
19
+ "node": ">=22.19.0"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "prepublishOnly": "npm run build",
24
+ "prepare": "npm run build",
25
+ "test:unit": "node --test \"test/unit/**/*.test.js\"",
26
+ "test:exit-code": "node test/exit-code/run.mjs",
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"
29
+ },
30
+ "peerDependencies": {
31
+ "@playwright/test": ">=1.30.0"
32
+ },
33
+ "devDependencies": {
34
+ "@playwright/test": "^1.48.0",
35
+ "@types/node": "^20.14.0",
36
+ "typescript": "^5.6.0"
37
+ },
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "undici": "^8.10.0"
41
+ }
42
+ }