@justinmiehle/reporter-vitest 0.0.6

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 ADDED
@@ -0,0 +1,90 @@
1
+ # Panoptes Vitest Reporter
2
+
3
+ Custom reporter for Vitest that sends test results directly to Convex.
4
+
5
+ Published as **@justinmiehle/reporter-vitest** on [GitHub Packages](https://github.com/JustinMiehle?tab=packages).
6
+
7
+ ## Installation
8
+
9
+ ### Within the Panoptes Monorepo
10
+
11
+ If you're using this reporter in another package within the Panoptes monorepo, use the workspace protocol:
12
+
13
+ ```bash
14
+ bun add -d @justinmiehle/reporter-vitest@workspace:*
15
+ ```
16
+
17
+ This will reference the local package directly without needing to publish or use a local registry.
18
+
19
+ ### Outside the Monorepo (GitHub Packages)
20
+
21
+ Install from GitHub Packages. You need to point the `@justinmiehle` scope at GitHub's registry and authenticate.
22
+
23
+ **In a GitHub Action** (other repo), add a step before installing dependencies:
24
+
25
+ ```yaml
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: '20'
29
+ registry-url: 'https://npm.pkg.github.com'
30
+ scope: '@justinmiehle'
31
+ - run: bun install # or npm ci
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34
+ ```
35
+
36
+ Create an `.npmrc` in that repo (or inject it in CI):
37
+
38
+ ```
39
+ @justinmiehle:registry=https://npm.pkg.github.com
40
+ ```
41
+
42
+ Then install:
43
+
44
+ ```bash
45
+ bun add -d @justinmiehle/reporter-vitest
46
+ ```
47
+
48
+ **Local / other CI:** Use a [Personal Access Token](https://github.com/settings/tokens) with `read:packages` and set `NODE_AUTH_TOKEN` or use `npm login` against `https://npm.pkg.github.com`.
49
+
50
+ ## Configuration
51
+
52
+ Add the reporter to your `vitest.config.ts`:
53
+
54
+ ```typescript
55
+ import { defineConfig } from 'vitest/config'
56
+ import PanoptesReporter from '@justinmiehle/reporter-vitest'
57
+
58
+ export default defineConfig({
59
+ test: {
60
+ reporters: [
61
+ 'default',
62
+ new PanoptesReporter({
63
+ convexUrl: process.env.CONVEX_URL,
64
+ projectName: process.env.PANOPTES_PROJECT_NAME || 'my-project',
65
+ environment: process.env.NODE_ENV || 'development',
66
+ ci: process.env.CI === 'true',
67
+ }),
68
+ ],
69
+ },
70
+ })
71
+ ```
72
+
73
+ **Important:** Do not add `@justinmiehle/reporter-vitest` or `@justinmiehle/shared` to `ssr.noExternal` in your Vite/Vitest config. The packages are designed to be treated as external dependencies and will be loaded from their compiled JavaScript files in the `dist/` directory.
74
+
75
+ ## Environment Variables
76
+
77
+ - `CONVEX_URL` - Convex deployment URL (required, e.g., `https://xxx.convex.cloud`)
78
+ - `PANOPTES_PROJECT_NAME` - Project name (default: `default-project`)
79
+ - `NODE_ENV` - Environment name
80
+ - `CI` - Set to `true` if running in CI
81
+
82
+ ## Usage
83
+
84
+ Run your tests as usual:
85
+
86
+ ```bash
87
+ vitest run
88
+ ```
89
+
90
+ The reporter will automatically send test results to Convex. Make sure `CONVEX_URL` is set in your environment.
@@ -0,0 +1,20 @@
1
+ import type { Reporter } from "vitest/reporters";
2
+ interface PanoptesReporterOptions {
3
+ convexUrl?: string;
4
+ projectName?: string;
5
+ environment?: string;
6
+ ci?: boolean;
7
+ }
8
+ export default class PanoptesReporter implements Reporter {
9
+ private options;
10
+ private startTime;
11
+ private tests;
12
+ private suites;
13
+ constructor(options?: PanoptesReporterOptions);
14
+ onInit(): void;
15
+ onTestRunStart(): void;
16
+ onTestCaseResult(test: any): void;
17
+ onTestRunEnd(): Promise<void>;
18
+ private mapStatus;
19
+ }
20
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,160 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/index.ts
9
+ function getCommitSha() {
10
+ if (process.env.GITHUB_SHA) {
11
+ return process.env.GITHUB_SHA;
12
+ }
13
+ if (process.env.CIRCLE_SHA1) {
14
+ return process.env.CIRCLE_SHA1;
15
+ }
16
+ if (process.env.GITLAB_CI_COMMIT_SHA) {
17
+ return process.env.GITLAB_CI_COMMIT_SHA;
18
+ }
19
+ try {
20
+ const { execSync } = __require("node:child_process");
21
+ return execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
22
+ } catch {
23
+ return void 0;
24
+ }
25
+ }
26
+ var PanoptesReporter = class {
27
+ options;
28
+ startTime = 0;
29
+ tests = [];
30
+ suites = /* @__PURE__ */ new Map();
31
+ constructor(options = {}) {
32
+ this.options = {
33
+ convexUrl: options.convexUrl || process.env.CONVEX_URL || "",
34
+ projectName: options.projectName || process.env.PANOPTES_PROJECT_NAME || "default-project",
35
+ environment: options.environment || process.env.NODE_ENV || "development",
36
+ ci: options.ci ?? process.env.CI === "true"
37
+ };
38
+ }
39
+ onInit() {
40
+ this.startTime = Date.now();
41
+ }
42
+ onTestRunStart() {
43
+ this.tests = [];
44
+ this.suites.clear();
45
+ }
46
+ // biome-ignore lint/suspicious/noExplicitAny: Vitest reporter interface doesn't provide strict types
47
+ onTestCaseResult(test) {
48
+ const testResult = {
49
+ name: test.name || test.title || "Unknown test",
50
+ file: test.file?.name || test.filepath || "unknown",
51
+ line: test.file?.line,
52
+ column: test.file?.column,
53
+ status: this.mapStatus(test.status),
54
+ duration: test.duration || 0,
55
+ error: test.error?.message,
56
+ errorDetails: test.error?.stack,
57
+ retries: test.retryCount,
58
+ suite: test.suite?.name,
59
+ tags: test.meta?.tags,
60
+ metadata: {
61
+ type: test.type,
62
+ mode: test.mode
63
+ }
64
+ };
65
+ this.tests.push(testResult);
66
+ if (test.suite) {
67
+ const suiteKey = test.suite.name || test.file?.name || "unknown";
68
+ if (!this.suites.has(suiteKey)) {
69
+ this.suites.set(suiteKey, {
70
+ name: test.suite.name || suiteKey,
71
+ file: test.file?.name || test.filepath || "unknown",
72
+ tests: []
73
+ });
74
+ }
75
+ this.suites.get(suiteKey)?.tests.push(testResult);
76
+ }
77
+ }
78
+ async onTestRunEnd() {
79
+ const endTime = Date.now();
80
+ const duration = endTime - this.startTime;
81
+ const passedTests = this.tests.filter((t) => t.status === "passed").length;
82
+ const failedTests = this.tests.filter((t) => t.status === "failed").length;
83
+ const skippedTests = this.tests.filter((t) => t.status === "skipped").length;
84
+ const suiteData = Array.from(this.suites.values()).map((suite) => {
85
+ const suiteTests = suite.tests;
86
+ const passed = suiteTests.filter((t) => t.status === "passed").length;
87
+ const failed = suiteTests.filter((t) => t.status === "failed").length;
88
+ const skipped = suiteTests.filter((t) => t.status === "skipped").length;
89
+ const status = failed > 0 ? "failed" : skipped === suiteTests.length ? "skipped" : "passed";
90
+ return {
91
+ name: suite.name,
92
+ file: suite.file,
93
+ status,
94
+ duration: suiteTests.reduce((sum, t) => sum + t.duration, 0),
95
+ totalTests: suiteTests.length,
96
+ passedTests: passed,
97
+ failedTests: failed,
98
+ skippedTests: skipped
99
+ };
100
+ });
101
+ const testRun = {
102
+ projectName: this.options.projectName,
103
+ framework: "vitest",
104
+ testType: "unit",
105
+ // Vitest is typically unit tests, but could be configured
106
+ startedAt: this.startTime,
107
+ completedAt: endTime,
108
+ duration,
109
+ totalTests: this.tests.length,
110
+ passedTests,
111
+ failedTests,
112
+ skippedTests,
113
+ environment: this.options.environment,
114
+ ci: this.options.ci,
115
+ commitSha: getCommitSha(),
116
+ tests: this.tests,
117
+ suites: suiteData
118
+ };
119
+ if (!this.options.convexUrl) {
120
+ console.warn("[Panoptes] CONVEX_URL not set. Test results will not be sent.");
121
+ return;
122
+ }
123
+ try {
124
+ const response = await fetch(`${this.options.convexUrl}/http/ingestTestRunHttp`, {
125
+ method: "POST",
126
+ headers: {
127
+ "Content-Type": "application/json"
128
+ },
129
+ body: JSON.stringify(testRun)
130
+ });
131
+ if (!response.ok) {
132
+ const error = await response.text();
133
+ console.error(`[Panoptes] Failed to send test results: ${error}`);
134
+ } else {
135
+ const result = await response.json();
136
+ console.log(`[Panoptes] Test results sent successfully. Test Run ID: ${result.testRunId}`);
137
+ }
138
+ } catch (error) {
139
+ console.error("[Panoptes] Error sending test results:", error);
140
+ }
141
+ }
142
+ mapStatus(status) {
143
+ switch (status) {
144
+ case "passed":
145
+ case "pass":
146
+ return "passed";
147
+ case "failed":
148
+ case "fail":
149
+ return "failed";
150
+ case "skipped":
151
+ case "skip":
152
+ return "skipped";
153
+ default:
154
+ return "running";
155
+ }
156
+ }
157
+ };
158
+ export {
159
+ PanoptesReporter as default
160
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@justinmiehle/reporter-vitest",
3
+ "version": "0.0.6",
4
+ "description": "Panoptes reporter for Vitest test results",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist", "src"],
16
+ "scripts": {
17
+ "build": "bunx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@justinmiehle/shared --external:vitest && bunx tsc --emitDeclarationOnly --declaration --outDir dist --skipLibCheck src/index.ts"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/JustinMiehle/panoptes.git",
22
+ "directory": "packages/reporters/vitest"
23
+ },
24
+ "publishConfig": {
25
+ "registry": "https://npm.pkg.github.com",
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "@justinmiehle/shared": "0.0.2"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20.10.0",
33
+ "typescript": "^5.3.3",
34
+ "vitest": "^1.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "vitest": "^1.0.0"
38
+ }
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,181 @@
1
+ import type { TestResult, TestRunIngest } from "@justinmiehle/shared";
2
+ import type { Reporter } from "vitest/reporters";
3
+
4
+ interface PanoptesReporterOptions {
5
+ convexUrl?: string;
6
+ projectName?: string;
7
+ environment?: string;
8
+ ci?: boolean;
9
+ }
10
+
11
+ function getCommitSha(): string | undefined {
12
+ // Check CI environment first (GitHub Actions, etc.)
13
+ if (process.env.GITHUB_SHA) {
14
+ return process.env.GITHUB_SHA;
15
+ }
16
+ if (process.env.CIRCLE_SHA1) {
17
+ return process.env.CIRCLE_SHA1;
18
+ }
19
+ if (process.env.GITLAB_CI_COMMIT_SHA) {
20
+ return process.env.GITLAB_CI_COMMIT_SHA;
21
+ }
22
+ // Try to get from git in local environment
23
+ // Note: This requires git to be available and may fail silently
24
+ try {
25
+ const { execSync } = require("node:child_process");
26
+ return execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
27
+ } catch {
28
+ // Git not available or not in a git repo
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ export default class PanoptesReporter implements Reporter {
34
+ private options: Required<PanoptesReporterOptions>;
35
+ private startTime = 0;
36
+ private tests: TestResult[] = [];
37
+ private suites: Map<string, { name: string; file: string; tests: TestResult[] }> = new Map();
38
+
39
+ constructor(options: PanoptesReporterOptions = {}) {
40
+ this.options = {
41
+ convexUrl: options.convexUrl || process.env.CONVEX_URL || "",
42
+ projectName: options.projectName || process.env.PANOPTES_PROJECT_NAME || "default-project",
43
+ environment: options.environment || process.env.NODE_ENV || "development",
44
+ ci: options.ci ?? process.env.CI === "true",
45
+ };
46
+ }
47
+
48
+ onInit() {
49
+ this.startTime = Date.now();
50
+ }
51
+
52
+ onTestRunStart() {
53
+ this.tests = [];
54
+ this.suites.clear();
55
+ }
56
+
57
+ // biome-ignore lint/suspicious/noExplicitAny: Vitest reporter interface doesn't provide strict types
58
+ onTestCaseResult(test: any) {
59
+ const testResult: TestResult = {
60
+ name: test.name || test.title || "Unknown test",
61
+ file: test.file?.name || test.filepath || "unknown",
62
+ line: test.file?.line,
63
+ column: test.file?.column,
64
+ status: this.mapStatus(test.status),
65
+ duration: test.duration || 0,
66
+ error: test.error?.message,
67
+ errorDetails: test.error?.stack,
68
+ retries: test.retryCount,
69
+ suite: test.suite?.name,
70
+ tags: test.meta?.tags,
71
+ metadata: {
72
+ type: test.type,
73
+ mode: test.mode,
74
+ },
75
+ };
76
+
77
+ this.tests.push(testResult);
78
+
79
+ // Track suites
80
+ if (test.suite) {
81
+ const suiteKey = test.suite.name || test.file?.name || "unknown";
82
+ if (!this.suites.has(suiteKey)) {
83
+ this.suites.set(suiteKey, {
84
+ name: test.suite.name || suiteKey,
85
+ file: test.file?.name || test.filepath || "unknown",
86
+ tests: [],
87
+ });
88
+ }
89
+ this.suites.get(suiteKey)?.tests.push(testResult);
90
+ }
91
+ }
92
+
93
+ async onTestRunEnd() {
94
+ const endTime = Date.now();
95
+ const duration = endTime - this.startTime;
96
+
97
+ const passedTests = this.tests.filter((t) => t.status === "passed").length;
98
+ const failedTests = this.tests.filter((t) => t.status === "failed").length;
99
+ const skippedTests = this.tests.filter((t) => t.status === "skipped").length;
100
+
101
+ // Build suite data
102
+ const suiteData = Array.from(this.suites.values()).map((suite) => {
103
+ const suiteTests = suite.tests;
104
+ const passed = suiteTests.filter((t) => t.status === "passed").length;
105
+ const failed = suiteTests.filter((t) => t.status === "failed").length;
106
+ const skipped = suiteTests.filter((t) => t.status === "skipped").length;
107
+
108
+ const status: "passed" | "failed" | "skipped" =
109
+ failed > 0 ? "failed" : skipped === suiteTests.length ? "skipped" : "passed";
110
+ return {
111
+ name: suite.name,
112
+ file: suite.file,
113
+ status,
114
+ duration: suiteTests.reduce((sum, t) => sum + t.duration, 0),
115
+ totalTests: suiteTests.length,
116
+ passedTests: passed,
117
+ failedTests: failed,
118
+ skippedTests: skipped,
119
+ };
120
+ });
121
+
122
+ const testRun: TestRunIngest = {
123
+ projectName: this.options.projectName,
124
+ framework: "vitest",
125
+ testType: "unit", // Vitest is typically unit tests, but could be configured
126
+ startedAt: this.startTime,
127
+ completedAt: endTime,
128
+ duration,
129
+ totalTests: this.tests.length,
130
+ passedTests,
131
+ failedTests,
132
+ skippedTests,
133
+ environment: this.options.environment,
134
+ ci: this.options.ci,
135
+ commitSha: getCommitSha(),
136
+ tests: this.tests,
137
+ suites: suiteData,
138
+ };
139
+
140
+ if (!this.options.convexUrl) {
141
+ console.warn("[Panoptes] CONVEX_URL not set. Test results will not be sent.");
142
+ return;
143
+ }
144
+
145
+ try {
146
+ const response = await fetch(`${this.options.convexUrl}/http/ingestTestRunHttp`, {
147
+ method: "POST",
148
+ headers: {
149
+ "Content-Type": "application/json",
150
+ },
151
+ body: JSON.stringify(testRun),
152
+ });
153
+
154
+ if (!response.ok) {
155
+ const error = await response.text();
156
+ console.error(`[Panoptes] Failed to send test results: ${error}`);
157
+ } else {
158
+ const result = (await response.json()) as { testRunId?: string };
159
+ console.log(`[Panoptes] Test results sent successfully. Test Run ID: ${result.testRunId}`);
160
+ }
161
+ } catch (error) {
162
+ console.error("[Panoptes] Error sending test results:", error);
163
+ }
164
+ }
165
+
166
+ private mapStatus(status: string): "passed" | "failed" | "skipped" | "running" {
167
+ switch (status) {
168
+ case "passed":
169
+ case "pass":
170
+ return "passed";
171
+ case "failed":
172
+ case "fail":
173
+ return "failed";
174
+ case "skipped":
175
+ case "skip":
176
+ return "skipped";
177
+ default:
178
+ return "running";
179
+ }
180
+ }
181
+ }