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 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).
@@ -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
+ }