squally-reporter 0.3.0 → 0.5.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 +182 -121
- package/dist/index.d.ts +95 -4
- package/dist/index.js +940 -115
- package/package.json +41 -38
package/README.md
CHANGED
|
@@ -1,121 +1,182 @@
|
|
|
1
|
-
# squally-reporter
|
|
2
|
-
|
|
3
|
-
Playwright reporter that sends test results to your Squally dashboard.
|
|
4
|
-
|
|
5
|
-
Learn more at [squally-app.vercel.app](https://squally-app.vercel.app).
|
|
6
|
-
|
|
7
|
-
## Requirements
|
|
8
|
-
|
|
9
|
-
- Node.js >= 22.19.0 (required by the `undici` dependency)
|
|
10
|
-
- `@playwright/test` >= 1.30.0
|
|
11
|
-
|
|
12
|
-
## Install
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
npm install squally-reporter
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Config
|
|
19
|
-
|
|
20
|
-
Set two env vars (e.g. in CI secrets or `.env`):
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
SQUALLY_API_URL=https://squally-app.vercel.app
|
|
24
|
-
SQUALLY_API_KEY=your-project-api-key
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Find your API key on your project's Settings page in the Squally dashboard.
|
|
28
|
-
|
|
29
|
-
Then add the reporter to `playwright.config.ts`:
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
import { defineConfig } from "@playwright/test";
|
|
33
|
-
|
|
34
|
-
export default defineConfig({
|
|
35
|
-
reporter: [
|
|
36
|
-
["list"],
|
|
37
|
-
["squally-reporter"],
|
|
38
|
-
],
|
|
39
|
-
});
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Alternatively, pass config explicitly instead of using env vars
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
##
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
1
|
+
# squally-reporter
|
|
2
|
+
|
|
3
|
+
Playwright reporter that sends test results to your Squally dashboard.
|
|
4
|
+
|
|
5
|
+
Learn more at [squally-app.vercel.app](https://squally-app.vercel.app).
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js >= 22.19.0 (required by the `undici` dependency)
|
|
10
|
+
- `@playwright/test` >= 1.30.0
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -D squally-reporter
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Config
|
|
19
|
+
|
|
20
|
+
Set two env vars (e.g. in CI secrets or `.env`):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
SQUALLY_API_URL=https://squally-app.vercel.app
|
|
24
|
+
SQUALLY_API_KEY=your-project-api-key
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Find your API key on your project's Settings page in the Squally dashboard.
|
|
28
|
+
|
|
29
|
+
Then add the reporter to `playwright.config.ts`:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { defineConfig } from "@playwright/test";
|
|
33
|
+
|
|
34
|
+
export default defineConfig({
|
|
35
|
+
reporter: [
|
|
36
|
+
["list"],
|
|
37
|
+
["squally-reporter"],
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Alternatively, pass config explicitly instead of using env vars - **only if
|
|
43
|
+
`apiKey` itself comes from an env var or secret store at runtime, never as a
|
|
44
|
+
literal string in a committed file:**
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
reporter: [
|
|
48
|
+
["squally-reporter", { apiUrl: "https://squally-app.vercel.app", apiKey: process.env.SQUALLY_API_KEY }],
|
|
49
|
+
],
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That's it - run your tests as usual (`npx playwright test`) and results will
|
|
53
|
+
be sent to Squally after the run finishes.
|
|
54
|
+
|
|
55
|
+
## Screenshot/trace upload
|
|
56
|
+
|
|
57
|
+
If your `playwright.config.ts` captures screenshots or traces, the reporter
|
|
58
|
+
uploads them to Squally so they show up next to the failing test in the
|
|
59
|
+
dashboard - no separate setup beyond the two env vars above.
|
|
60
|
+
|
|
61
|
+
**Only from attempts worth looking at:** screenshots/traces are uploaded
|
|
62
|
+
only for attempts that **failed**, or that **needed a retry** (even if that
|
|
63
|
+
retry then passed) - never for an attempt that passed on its first try,
|
|
64
|
+
regardless of how `screenshot`/`trace` are configured. Nobody looks at a
|
|
65
|
+
screenshot of a test that just passed, and uploading one anyway would only
|
|
66
|
+
eat into your organization's storage for no benefit.
|
|
67
|
+
|
|
68
|
+
**You need to actually enable capture in `playwright.config.ts`** - without
|
|
69
|
+
it, Playwright doesn't produce any attachments, so there's nothing for the
|
|
70
|
+
reporter to find or upload (no error either way, it's a silent no-op):
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
export default defineConfig({
|
|
74
|
+
use: {
|
|
75
|
+
screenshot: "only-on-failure",
|
|
76
|
+
trace: "retain-on-failure",
|
|
77
|
+
},
|
|
78
|
+
// ...
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Limits:**
|
|
83
|
+
- 50 MB per individual file. Larger files are skipped (logged as a
|
|
84
|
+
`console.warn`, the test result itself is still reported as usual).
|
|
85
|
+
- A total storage cap per organization, depending on your Squally plan.
|
|
86
|
+
Once reached, further uploads are skipped the same way until you either
|
|
87
|
+
free up space or upgrade.
|
|
88
|
+
- Two minutes per upload, and 30 seconds for the calls that surround it. An
|
|
89
|
+
unreachable or very slow server means a skipped upload and a `console.warn`,
|
|
90
|
+
not a CI job waiting indefinitely.
|
|
91
|
+
|
|
92
|
+
Upload is best-effort and fully non-blocking, same principle as the rest of
|
|
93
|
+
this reporter - a failed or skipped upload never fails your test run or
|
|
94
|
+
changes its exit code, it only logs a `console.warn` explaining what
|
|
95
|
+
happened.
|
|
96
|
+
|
|
97
|
+
## How results reach Squally
|
|
98
|
+
|
|
99
|
+
Results stream to Squally as your suite runs, not all at once at the end:
|
|
100
|
+
each test's result is sent in a small batch (at most every 25 results or 2
|
|
101
|
+
seconds, whichever comes first), so the "running" view in the dashboard stays
|
|
102
|
+
close to real time even for a long suite. There is no longer a single-request
|
|
103
|
+
size limit that could discard a whole run - a very large suite simply takes
|
|
104
|
+
more, smaller requests. As a courtesy the reporter still warns if a run's
|
|
105
|
+
result count climbs well past 5,000, since that usually means retries are
|
|
106
|
+
configured higher than intended rather than a suite that's actually that
|
|
107
|
+
large.
|
|
108
|
+
|
|
109
|
+
If Squally is unreachable partway through, results already sent stay in the
|
|
110
|
+
dashboard; only what could not be delivered is lost, and your test run's exit
|
|
111
|
+
code is never affected either way.
|
|
112
|
+
|
|
113
|
+
## Sharding
|
|
114
|
+
|
|
115
|
+
**Handled automatically - no extra setup needed.** Each shard is still a
|
|
116
|
+
separate process with its own reporter instance, but Squally now correlates
|
|
117
|
+
them itself: all shards of one CI run are grouped together and shown as one
|
|
118
|
+
run once every shard has finished, with the flip side of that guarantee being
|
|
119
|
+
that the run only completes once every shard *has* reported.
|
|
120
|
+
|
|
121
|
+
Correlation is based on an id read from your CI provider's own environment,
|
|
122
|
+
so it costs you nothing on GitHub Actions, GitLab CI or CircleCI. If your
|
|
123
|
+
provider is not one of those (or you shard locally across several separate
|
|
124
|
+
`--shard` invocations), set `SQUALLY_CI_RUN_ID` to the same value for every
|
|
125
|
+
shard of one run - anything unique per CI run and shared by its shards works,
|
|
126
|
+
e.g. a build number your CI system already exposes.
|
|
127
|
+
|
|
128
|
+
**`playwright merge-reports` is not part of this and is not recommended** for
|
|
129
|
+
getting a single run in Squally - the automatic correlation above is the
|
|
130
|
+
supported path, and produces correct per-shard timing/branch/commit
|
|
131
|
+
information that a merge job cannot (see the historical note below). Nothing
|
|
132
|
+
stops you from using `merge-reports` for Playwright's own HTML report if you
|
|
133
|
+
want that separately; just don't point this reporter at the merged result
|
|
134
|
+
expecting Squally to treat it specially.
|
|
135
|
+
|
|
136
|
+
<details>
|
|
137
|
+
<summary>Why not just use `merge-reports`, historically</summary>
|
|
138
|
+
|
|
139
|
+
An earlier version of this reporter recommended `playwright merge-reports` as
|
|
140
|
+
the way to get one Squally run out of a sharded job. That path has a real
|
|
141
|
+
problem: branch, commit, commit message and author are all read on whichever
|
|
142
|
+
machine the reporter actually runs on - during a merge that is the merge job,
|
|
143
|
+
not the shards - and without a checkout there (and without the CI provider's
|
|
144
|
+
own commit variables) they all come out empty. A run with no branch is never
|
|
145
|
+
counted as a run on your stable branch, which is what Squally's broken/healthy
|
|
146
|
+
verdicts are based on - so a "merged" run could exist in the dashboard while
|
|
147
|
+
silently contributing nothing to flakiness detection. The automatic
|
|
148
|
+
correlation above reads that information from each shard's own environment
|
|
149
|
+
instead, so this problem does not arise.
|
|
150
|
+
|
|
151
|
+
</details>
|
|
152
|
+
|
|
153
|
+
## Tested revision (optional)
|
|
154
|
+
|
|
155
|
+
If your test-automation code lives in the same repo as the application it
|
|
156
|
+
tests, Squally can already tell which commit was tested from the CI
|
|
157
|
+
provider's own commit env vars. If they're **separate repos** (e.g. a
|
|
158
|
+
dedicated E2E-test repo pointed at a deployed app), set one of the following
|
|
159
|
+
so Squally knows which application state was actually tested:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Same repo as the application under test - Squally will trust this CI
|
|
163
|
+
# job's own commit (GITHUB_SHA / CI_COMMIT_SHA) as the tested revision.
|
|
164
|
+
SAME_REPO_AS_APP=true
|
|
165
|
+
|
|
166
|
+
# Separate repos - pass the tested revision explicitly (a commit SHA, tag,
|
|
167
|
+
# or branch name/ref).
|
|
168
|
+
TESTED_REVISION=v1.2.0
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Neither variable is required - without them, test results are still sent
|
|
172
|
+
and stored exactly as before.
|
|
173
|
+
|
|
174
|
+
## Support
|
|
175
|
+
|
|
176
|
+
Found a bug or have a question? Please open an issue on
|
|
177
|
+
[GitHub Issues](https://github.com/squallydev-admin/squally-reporter/issues) -
|
|
178
|
+
that's the primary way to report problems or ask questions.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT - see [LICENSE](./LICENSE).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,111 @@
|
|
|
1
|
-
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from "@playwright/test/reporter";
|
|
1
|
+
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestError, TestResult } from "@playwright/test/reporter";
|
|
2
2
|
interface SquallyReporterOptions {
|
|
3
3
|
apiUrl?: string;
|
|
4
4
|
apiKey?: string;
|
|
5
5
|
}
|
|
6
|
+
interface AppendErrorEntry {
|
|
7
|
+
message: string;
|
|
8
|
+
stack?: string;
|
|
9
|
+
snippet?: string;
|
|
10
|
+
location?: {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
column: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
interface AppendResultEntry {
|
|
17
|
+
testName: string;
|
|
18
|
+
filePath: string | null;
|
|
19
|
+
status: "passed" | "failed" | "skipped";
|
|
20
|
+
retryCount: number;
|
|
21
|
+
repeatIndex: number;
|
|
22
|
+
durationMs: number | null;
|
|
23
|
+
errors?: AppendErrorEntry[];
|
|
24
|
+
consoleOutput: string | null;
|
|
25
|
+
tags?: string[];
|
|
26
|
+
browserName: string | null;
|
|
27
|
+
errorContext: string | null;
|
|
28
|
+
}
|
|
29
|
+
type ArtifactType = "screenshot" | "trace";
|
|
30
|
+
declare function truncate(value: string, max: number): string;
|
|
31
|
+
declare function truncateOrNull(value: string | null, max: number): string | null;
|
|
32
|
+
declare function mapStatus(status: TestResult["status"]): AppendResultEntry["status"];
|
|
33
|
+
declare function detectCiRunId(env: NodeJS.ProcessEnv, generateFallback: () => string): string;
|
|
34
|
+
interface ProjectSuiteLike {
|
|
35
|
+
project(): {
|
|
36
|
+
name: string;
|
|
37
|
+
} | undefined;
|
|
38
|
+
}
|
|
39
|
+
declare function resolveProjectName(projectSuites: readonly ProjectSuiteLike[]): string | null;
|
|
40
|
+
declare function normalizeRevision(raw: string): string;
|
|
41
|
+
interface ResolvedRevision {
|
|
42
|
+
revision: string;
|
|
43
|
+
source: "ci_native" | "passed_param";
|
|
44
|
+
}
|
|
45
|
+
declare function resolveTestedRevision(): ResolvedRevision | null;
|
|
46
|
+
declare function toPosixPath(p: string): string;
|
|
47
|
+
declare function stripAnsi(value: string): string;
|
|
48
|
+
declare function maskSecrets(value: string): string;
|
|
49
|
+
declare function prepareConsoleOutput(value: string | null): string | null;
|
|
50
|
+
declare function mapErrors(errors: readonly TestError[]): AppendErrorEntry[] | undefined;
|
|
51
|
+
declare function attemptQualifiesForArtifacts(mappedStatus: AppendResultEntry["status"], retryCount: number): boolean;
|
|
52
|
+
declare function artifactKey(testName: string, filePath: string, retryCount: number, type: ArtifactType): string;
|
|
53
|
+
declare function parseRetryAfterMs(headerValue: string | null, nowMs: number): number | null;
|
|
54
|
+
declare function computeBackoffMs(retryNumber: number): number;
|
|
6
55
|
export default class SquallyReporter implements Reporter {
|
|
7
56
|
private apiUrl;
|
|
8
57
|
private apiKey;
|
|
9
58
|
private enabled;
|
|
10
59
|
private startedAt;
|
|
11
|
-
private
|
|
60
|
+
private dispatcher;
|
|
61
|
+
private ciRunId;
|
|
62
|
+
private shardIndex;
|
|
63
|
+
private shardTotal;
|
|
64
|
+
private projectName;
|
|
65
|
+
private runTags;
|
|
66
|
+
private runConfig;
|
|
67
|
+
private buffer;
|
|
12
68
|
private artifacts;
|
|
69
|
+
private sendingDisabled;
|
|
70
|
+
private flushTimerHandle;
|
|
71
|
+
private inFlightFlushes;
|
|
72
|
+
private runIdPromise;
|
|
73
|
+
private runId;
|
|
13
74
|
constructor(options?: SquallyReporterOptions);
|
|
14
|
-
onBegin(
|
|
75
|
+
onBegin(config: FullConfig, suite: Suite): void;
|
|
76
|
+
private resolveRunConfig;
|
|
15
77
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
16
|
-
onEnd(result: FullResult)
|
|
78
|
+
onEnd: (result: FullResult) => Promise<void>;
|
|
79
|
+
private drainAndComplete;
|
|
80
|
+
private armFlushTimer;
|
|
81
|
+
private stopFlushTimer;
|
|
82
|
+
private giveUp;
|
|
83
|
+
private triggerFlush;
|
|
84
|
+
private flush;
|
|
85
|
+
private sendBatch;
|
|
86
|
+
private beginRun;
|
|
87
|
+
private completeRun;
|
|
88
|
+
private fetchRules;
|
|
89
|
+
private requestJson;
|
|
17
90
|
private uploadArtifacts;
|
|
18
91
|
private presignAndUpload;
|
|
19
92
|
}
|
|
93
|
+
export declare const __internal: {
|
|
94
|
+
truncate: typeof truncate;
|
|
95
|
+
truncateOrNull: typeof truncateOrNull;
|
|
96
|
+
mapStatus: typeof mapStatus;
|
|
97
|
+
stripAnsi: typeof stripAnsi;
|
|
98
|
+
normalizeRevision: typeof normalizeRevision;
|
|
99
|
+
resolveTestedRevision: typeof resolveTestedRevision;
|
|
100
|
+
toPosixPath: typeof toPosixPath;
|
|
101
|
+
detectCiRunId: typeof detectCiRunId;
|
|
102
|
+
resolveProjectName: typeof resolveProjectName;
|
|
103
|
+
maskSecrets: typeof maskSecrets;
|
|
104
|
+
prepareConsoleOutput: typeof prepareConsoleOutput;
|
|
105
|
+
mapErrors: typeof mapErrors;
|
|
106
|
+
parseRetryAfterMs: typeof parseRetryAfterMs;
|
|
107
|
+
computeBackoffMs: typeof computeBackoffMs;
|
|
108
|
+
attemptQualifiesForArtifacts: typeof attemptQualifiesForArtifacts;
|
|
109
|
+
artifactKey: typeof artifactKey;
|
|
110
|
+
};
|
|
20
111
|
export {};
|