squally-reporter 0.1.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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +182 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Squally <squally.dev@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
```ts
|
|
45
|
+
reporter: [
|
|
46
|
+
["squally-reporter", { apiUrl: "https://squally-app.vercel.app", apiKey: "..." }],
|
|
47
|
+
],
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's it - run your tests as usual (`npx playwright test`) and results will
|
|
51
|
+
be sent to Squally after the run finishes.
|
|
52
|
+
|
|
53
|
+
## Tested revision (optional)
|
|
54
|
+
|
|
55
|
+
If your test-automation code lives in the same repo as the application it
|
|
56
|
+
tests, Squally can already tell which commit was tested from the CI
|
|
57
|
+
provider's own commit env vars. If they're **separate repos** (e.g. a
|
|
58
|
+
dedicated E2E-test repo pointed at a deployed app), set one of the following
|
|
59
|
+
so Squally knows which application state was actually tested:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Same repo as the application under test - Squally will trust this CI
|
|
63
|
+
# job's own commit (GITHUB_SHA / CI_COMMIT_SHA) as the tested revision.
|
|
64
|
+
SAME_REPO_AS_APP=true
|
|
65
|
+
|
|
66
|
+
# Separate repos - pass the tested revision explicitly (a commit SHA, tag,
|
|
67
|
+
# or branch name/ref).
|
|
68
|
+
TESTED_REVISION=v1.2.0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Neither variable is required - without them, test results are still sent
|
|
72
|
+
and stored exactly as before.
|
|
73
|
+
|
|
74
|
+
## Support
|
|
75
|
+
|
|
76
|
+
Found a bug or have a question? Please open an issue on
|
|
77
|
+
[GitHub Issues](https://github.com/squallydev-admin/squally-reporter/issues) -
|
|
78
|
+
that's the primary way to report problems or ask questions.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT - see [LICENSE](./LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from "@playwright/test/reporter";
|
|
2
|
+
interface SquallyReporterOptions {
|
|
3
|
+
apiUrl?: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
}
|
|
6
|
+
export default class SquallyReporter implements Reporter {
|
|
7
|
+
private apiUrl;
|
|
8
|
+
private apiKey;
|
|
9
|
+
private startedAt;
|
|
10
|
+
private results;
|
|
11
|
+
constructor(options?: SquallyReporterOptions);
|
|
12
|
+
onBegin(_config: FullConfig, _suite: Suite): void;
|
|
13
|
+
onTestEnd(test: TestCase, result: TestResult): void;
|
|
14
|
+
onEnd(result: FullResult): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const undici_1 = require("undici");
|
|
9
|
+
// Maps Playwright's more granular statuses down to the three
|
|
10
|
+
// statuses Squally stores (passed | failed | skipped).
|
|
11
|
+
// "timedOut" and "interrupted" both count as failures for reporting purposes.
|
|
12
|
+
function mapStatus(status) {
|
|
13
|
+
if (status === "passed")
|
|
14
|
+
return "passed";
|
|
15
|
+
if (status === "skipped")
|
|
16
|
+
return "skipped";
|
|
17
|
+
return "failed";
|
|
18
|
+
}
|
|
19
|
+
// CI systems expose branch/commit via env vars - checking the common ones
|
|
20
|
+
// first avoids shelling out to git, which may not have full history in CI.
|
|
21
|
+
function detectCiProvider() {
|
|
22
|
+
if (process.env.GITHUB_ACTIONS)
|
|
23
|
+
return "github-actions";
|
|
24
|
+
if (process.env.GITLAB_CI)
|
|
25
|
+
return "gitlab-ci";
|
|
26
|
+
if (process.env.CIRCLECI)
|
|
27
|
+
return "circleci";
|
|
28
|
+
if (process.env.CI)
|
|
29
|
+
return "unknown-ci";
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function detectBranch() {
|
|
33
|
+
if (process.env.GITHUB_REF_NAME)
|
|
34
|
+
return process.env.GITHUB_REF_NAME;
|
|
35
|
+
if (process.env.CI_COMMIT_BRANCH)
|
|
36
|
+
return process.env.CI_COMMIT_BRANCH;
|
|
37
|
+
try {
|
|
38
|
+
return (0, node_child_process_1.execSync)("git rev-parse --abbrev-ref HEAD").toString().trim();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function detectCommitSha() {
|
|
45
|
+
if (process.env.GITHUB_SHA)
|
|
46
|
+
return process.env.GITHUB_SHA;
|
|
47
|
+
if (process.env.CI_COMMIT_SHA)
|
|
48
|
+
return process.env.CI_COMMIT_SHA;
|
|
49
|
+
try {
|
|
50
|
+
return (0, node_child_process_1.execSync)("git rev-parse HEAD").toString().trim();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// --- Flakiness Engine v2 (Phase A): tested application revision ---
|
|
57
|
+
//
|
|
58
|
+
// commitSha above is the test-automation repo's own commit. For teams whose
|
|
59
|
+
// test suite lives in a separate repo from the application it tests, that's
|
|
60
|
+
// not the same thing as "what application state did this run actually
|
|
61
|
+
// test" - testedRevision fills that gap.
|
|
62
|
+
// Applied to every resolved revision value before it's sent, regardless of
|
|
63
|
+
// which pattern below produced it.
|
|
64
|
+
function normalizeRevision(raw) {
|
|
65
|
+
let value = raw.toLowerCase();
|
|
66
|
+
if (value.startsWith("refs/heads/")) {
|
|
67
|
+
value = value.slice("refs/heads/".length);
|
|
68
|
+
}
|
|
69
|
+
if (/^v\d/.test(value)) {
|
|
70
|
+
value = value.slice(1);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
// Fallback chain:
|
|
75
|
+
// 1. ci_native - only trusted when the caller has explicitly confirmed (via
|
|
76
|
+
// SAME_REPO_AS_APP) that this CI job's own commit *is* the application
|
|
77
|
+
// commit, i.e. test-automation and application share one repo.
|
|
78
|
+
// 2. passed_param - the caller passes the tested revision directly, for the
|
|
79
|
+
// separate-repos case where nothing in this job's own env describes the
|
|
80
|
+
// application under test.
|
|
81
|
+
// Neither found: returns null, no error thrown - existing users without
|
|
82
|
+
// these new env vars must keep working exactly as before.
|
|
83
|
+
function resolveTestedRevision() {
|
|
84
|
+
if (process.env.SAME_REPO_AS_APP === "true") {
|
|
85
|
+
const revision = process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA;
|
|
86
|
+
if (revision) {
|
|
87
|
+
return { revision: normalizeRevision(revision), source: "ci_native" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (process.env.TESTED_REVISION) {
|
|
91
|
+
return { revision: normalizeRevision(process.env.TESTED_REVISION), source: "passed_param" };
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
// Resolved independently of resolveTestedRevision()'s outcome - e.g. a repo
|
|
96
|
+
// using TESTED_REVISION (pattern 2, separate repos) can still be running
|
|
97
|
+
// under GitHub Actions/GitLab CI and have this available.
|
|
98
|
+
function detectEventType() {
|
|
99
|
+
if (process.env.GITHUB_EVENT_NAME)
|
|
100
|
+
return process.env.GITHUB_EVENT_NAME;
|
|
101
|
+
if (process.env.CI_PIPELINE_SOURCE)
|
|
102
|
+
return process.env.CI_PIPELINE_SOURCE;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
class SquallyReporter {
|
|
106
|
+
constructor(options = {}) {
|
|
107
|
+
this.startedAt = new Date();
|
|
108
|
+
this.results = [];
|
|
109
|
+
const apiUrl = options.apiUrl ?? process.env.SQUALLY_API_URL;
|
|
110
|
+
const apiKey = options.apiKey ?? process.env.SQUALLY_API_KEY;
|
|
111
|
+
if (!apiUrl || !apiKey) {
|
|
112
|
+
throw new Error("[squally-reporter] Missing config: set SQUALLY_API_URL and " +
|
|
113
|
+
"SQUALLY_API_KEY as env vars, or pass apiUrl/apiKey in playwright.config.ts");
|
|
114
|
+
}
|
|
115
|
+
this.apiUrl = apiUrl.replace(/\/$/, ""); // strip trailing slash if present
|
|
116
|
+
this.apiKey = apiKey;
|
|
117
|
+
}
|
|
118
|
+
onBegin(_config, _suite) {
|
|
119
|
+
this.startedAt = new Date();
|
|
120
|
+
}
|
|
121
|
+
onTestEnd(test, result) {
|
|
122
|
+
const error = result.errors[0];
|
|
123
|
+
this.results.push({
|
|
124
|
+
testName: test.titlePath().slice(1).join(" > "),
|
|
125
|
+
filePath: node_path_1.default.relative(process.cwd(), test.location.file),
|
|
126
|
+
status: mapStatus(result.status),
|
|
127
|
+
durationMs: result.duration,
|
|
128
|
+
retryCount: result.retry,
|
|
129
|
+
errorMessage: error?.message ?? null,
|
|
130
|
+
errorStack: error?.stack ?? null,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async onEnd(result) {
|
|
134
|
+
const resolvedRevision = resolveTestedRevision();
|
|
135
|
+
const payload = {
|
|
136
|
+
branch: detectBranch(),
|
|
137
|
+
commitSha: detectCommitSha(),
|
|
138
|
+
ciProvider: detectCiProvider(),
|
|
139
|
+
status: result.status === "passed" ? "passed" : "failed",
|
|
140
|
+
startedAt: this.startedAt.toISOString(),
|
|
141
|
+
finishedAt: new Date().toISOString(),
|
|
142
|
+
testResults: this.results,
|
|
143
|
+
testedRevision: resolvedRevision?.revision ?? null,
|
|
144
|
+
revisionSource: resolvedRevision?.source ?? null,
|
|
145
|
+
eventType: detectEventType(),
|
|
146
|
+
};
|
|
147
|
+
// Node's global fetch() pools connections through a shared, long-lived
|
|
148
|
+
// keep-alive dispatcher by default. In a short-lived CLI process like
|
|
149
|
+
// this reporter, that pooled socket can still be mid-teardown when
|
|
150
|
+
// Playwright exits the process, which crashes with a libuv assertion on
|
|
151
|
+
// Windows ("UV_HANDLE_CLOSING", src/win/async.c). Using our own Agent
|
|
152
|
+
// for this one request and closing it explicitly guarantees no open
|
|
153
|
+
// handle is left behind by the time onEnd() resolves. Importing fetch
|
|
154
|
+
// from undici too (not Node's global one) avoids a possible instanceof
|
|
155
|
+
// mismatch between Node's internal bundled undici and this installed
|
|
156
|
+
// copy when passing our Agent as the dispatcher.
|
|
157
|
+
const dispatcher = new undici_1.Agent({ keepAliveTimeout: 1, keepAliveMaxTimeout: 1 });
|
|
158
|
+
try {
|
|
159
|
+
const response = await (0, undici_1.fetch)(`${this.apiUrl}/api/ingest`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify(payload),
|
|
166
|
+
dispatcher,
|
|
167
|
+
});
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
// Never throw here - a failing reporter upload should not fail the
|
|
170
|
+
// whole CI run just because the dashboard was unreachable.
|
|
171
|
+
console.error(`[squally-reporter] Ingest failed: ${response.status} ${response.statusText}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
console.error("[squally-reporter] Failed to reach Squally API:", err);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await dispatcher.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.default = SquallyReporter;
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "squally-reporter",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@playwright/test": ">=1.30.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@playwright/test": "^1.48.0",
|
|
31
|
+
"@types/node": "^20.14.0",
|
|
32
|
+
"typescript": "^5.6.0"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"undici": "^8.10.0"
|
|
37
|
+
}
|
|
38
|
+
}
|