@testspectra/react 1.1.8-rc.3 → 1.1.8-rc.30

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.
@@ -0,0 +1,227 @@
1
+ // src/vitest_reporter.ts
2
+ import path from "node:path";
3
+
4
+ // src/sanitizer.ts
5
+ var VITE_FS_URL_PATTERN = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+\/@fs\/([A-Za-z]:\/)/g;
6
+ var VITE_FS_UNIX_URL_PATTERN = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+\/@fs\//g;
7
+ var VITE_ROOT_URL_PATTERN = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+\//g;
8
+ var URL_QUERY_PATTERN = /(\.[a-zA-Z0-9]+)\?[^:\s'")]*/g;
9
+ var INTERNAL_FRAME_PATTERNS = [
10
+ /node_modules[\\/]vitest[\\/]/i,
11
+ /node_modules[\\/]@vitest[\\/]/i,
12
+ /node_modules[\\/]vite[\\/]/i,
13
+ /node_modules[\\/]@vitejs[\\/]/i,
14
+ /node_modules[\\/]playwright[\\/]/i,
15
+ /node_modules[\\/]@playwright[\\/]/i,
16
+ /node:internal[\\/]/i,
17
+ /dist[\\/]setup\.js/i,
18
+ /dist[\\/]vitest_reporter\.js/i,
19
+ /tools[\\/]react[\\/]/i,
20
+ /[\\/]vitest\.config\./i,
21
+ /@vitest/i,
22
+ /__vitest__/i
23
+ ];
24
+ function sanitizeViteUrl(text) {
25
+ if (!text) return "";
26
+ let result = text.replace(VITE_FS_URL_PATTERN, "$1");
27
+ result = result.replace(VITE_FS_UNIX_URL_PATTERN, "/");
28
+ result = result.replace(VITE_ROOT_URL_PATTERN, "./");
29
+ result = result.replace(URL_QUERY_PATTERN, "$1");
30
+ return result;
31
+ }
32
+ function sanitizeErrorMessage(msg) {
33
+ if (!msg) return "";
34
+ let sanitized = sanitizeViteUrl(msg);
35
+ sanitized = sanitized.replace(/Failed to fetch dynamically imported module:\s*/gi, "Failed to load module: ");
36
+ sanitized = sanitized.replace(/\[vite\]\s*(?:Internal server error:\s*)?/gi, "[TestSpectra] ");
37
+ sanitized = sanitized.replace(/@vitest\/browser/gi, "TestSpectra Browser");
38
+ sanitized = sanitized.replace(/@vitest\/runner/gi, "TestSpectra Runner");
39
+ sanitized = sanitized.replace(/\bvitest\.config\.[a-z0-9]+/gi, "spectra.config.ts");
40
+ sanitized = sanitized.replace(/\bVitest\b/g, "TestSpectra");
41
+ sanitized = sanitized.replace(/\bvitest\b/g, "testspectra");
42
+ return sanitized;
43
+ }
44
+ function sanitizeStackTrace(stack) {
45
+ if (!stack) return "";
46
+ const lines = stack.split("\n");
47
+ const filtered = [];
48
+ for (let i = 0; i < lines.length; i++) {
49
+ const line = lines[i];
50
+ if (i === 0) {
51
+ filtered.push(sanitizeErrorMessage(line));
52
+ continue;
53
+ }
54
+ const isInternal = INTERNAL_FRAME_PATTERNS.some((pattern) => pattern.test(line));
55
+ if (isInternal) {
56
+ continue;
57
+ }
58
+ filtered.push(sanitizeErrorMessage(line));
59
+ }
60
+ return filtered.join("\n").trim();
61
+ }
62
+ function sanitizeError(err) {
63
+ if (!err) return "";
64
+ if (typeof err === "string") {
65
+ return sanitizeStackTrace(err);
66
+ }
67
+ if (typeof err === "object") {
68
+ const errorObj = err;
69
+ const stack = errorObj.stack;
70
+ const message = errorObj.message || String(err);
71
+ if (stack && typeof stack === "string") {
72
+ const sanitizedStack = sanitizeStackTrace(stack);
73
+ if (sanitizedStack) return sanitizedStack;
74
+ }
75
+ return sanitizeErrorMessage(message);
76
+ }
77
+ return sanitizeErrorMessage(String(err));
78
+ }
79
+ function isVitestNoise(line) {
80
+ const trimmed = line.trim();
81
+ if (!trimmed) return true;
82
+ if (/^RUN\s+v\d+/i.test(trimmed)) return true;
83
+ if (/^DEV\s+v\d+/i.test(trimmed)) return true;
84
+ if (/^Test Files\s+\d+/i.test(trimmed)) return true;
85
+ if (/^Tests\s+\d+/i.test(trimmed)) return true;
86
+ if (/^Start at\s+\d+/i.test(trimmed)) return true;
87
+ if (/^Duration\s+[\d.]+/i.test(trimmed)) return true;
88
+ if (/^⎯{3,}/.test(trimmed)) return true;
89
+ if (/^Re-optimizing dependencies/i.test(trimmed)) return true;
90
+ if (/^JSON report written to/i.test(trimmed)) return true;
91
+ if (/^\(?transform \d+ms/i.test(trimmed)) return true;
92
+ if (/^[·✓❯⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]\s+/.test(trimmed)) return true;
93
+ if (/vite-cjs-node-api-deprecated/i.test(trimmed)) return true;
94
+ if (/CJS build of Vite's Node API is deprecated/i.test(trimmed)) return true;
95
+ if (/^\[?vite\]?\s*(deprecation|warning)/i.test(trimmed)) return true;
96
+ return false;
97
+ }
98
+
99
+ // src/vitest_reporter.ts
100
+ var SpectraVitestReporter = class {
101
+ testRoot;
102
+ /** taskId -> minimal task descriptor, populated in `onCollected` so `onTaskUpdate` can resolve names. */
103
+ tasks = /* @__PURE__ */ new Map();
104
+ /** taskIds seen in the native stream — used to tell "test ran but failed" from "file never loaded". */
105
+ started = /* @__PURE__ */ new Set();
106
+ finished = /* @__PURE__ */ new Set();
107
+ reportedFileFailures = /* @__PURE__ */ new Set();
108
+ constructor(options) {
109
+ this.testRoot = path.resolve(options.testRoot);
110
+ }
111
+ onInit(_ctx) {
112
+ }
113
+ /**
114
+ * Vitest has collected the full task tree. Index every task so `onTaskUpdate` (which only carries
115
+ * task ids) can map back to a human name.
116
+ */
117
+ onCollected(files = []) {
118
+ const index = (task) => {
119
+ this.tasks.set(task.id, { name: task.name, type: task.type });
120
+ const children = task.tasks;
121
+ if (Array.isArray(children)) children.forEach(index);
122
+ };
123
+ for (const file of files) index(file);
124
+ }
125
+ /**
126
+ * The native task-result stream. Each pack is `[taskId, result]`; a test's `result.state` moves
127
+ * to `pass` / `fail` exactly once, carrying its `errors` and `duration`. (`TEST_START` is still
128
+ * emitted in-page by setup.ts's `beforeEach` so it is guaranteed to precede that test's step
129
+ * events on the wire.)
130
+ */
131
+ onTaskUpdate(packs) {
132
+ for (const pack of packs) {
133
+ const id = pack[0];
134
+ const result = pack[1];
135
+ if (!result) continue;
136
+ const task = this.tasks.get(id);
137
+ if (!task || task.type !== "test") continue;
138
+ this.started.add(id);
139
+ const state = result.state;
140
+ if (state !== "pass" && state !== "fail") continue;
141
+ if (this.finished.has(id)) continue;
142
+ this.finished.add(id);
143
+ for (const err of result.errors ?? []) {
144
+ this.emitError(err);
145
+ }
146
+ const duration = typeof result.duration === "number" ? Math.round(result.duration) : 0;
147
+ const tag = state === "pass" ? "TESTSPECTRA_TEST_PASS" : "TESTSPECTRA_TEST_FAIL";
148
+ this.emit(tag, `${task.name} (${duration}ms)`);
149
+ }
150
+ }
151
+ /**
152
+ * Intercepts every console log Vitest forwards from the browser. Step events (emitted in-page by
153
+ * prelude.ts, the one thing the Node-side reporter cannot observe natively) are forwarded as-is;
154
+ * genuine stderr is sanitized into a TestSpectra error so real failures reach the log.
155
+ */
156
+ onUserConsoleLog(log) {
157
+ if (!log?.content) return;
158
+ if (log.content.includes("[TESTSPECTRA_")) {
159
+ for (const line of log.content.split("\n")) {
160
+ const trimmed = line.trim();
161
+ if (trimmed.startsWith("[TESTSPECTRA_")) {
162
+ console.log(trimmed);
163
+ }
164
+ }
165
+ return;
166
+ }
167
+ if (log.type === "stderr") {
168
+ const sanitized = sanitizeError(log.content);
169
+ if (sanitized && !isVitestNoise(sanitized)) {
170
+ for (const line of sanitized.split("\n")) {
171
+ if (line.trim()) this.emit("TESTSPECTRA_TEST_ERROR", line.trim());
172
+ }
173
+ }
174
+ }
175
+ }
176
+ /**
177
+ * Recovery pass for failures that never produced a test task: a spec file that fails to
178
+ * import/transpile (e.g. the Vite dev server returning 500 for a missing scaffolding reference)
179
+ * never enters the `test` state, so `onTaskUpdate` never fires for it. Also surfaces any
180
+ * unhandled runner errors.
181
+ */
182
+ onFinished(files = [], errors = []) {
183
+ for (const file of files) {
184
+ const fileFailed = file.result?.state === "fail";
185
+ if (fileFailed && !this.reportedFileFailures.has(file.filepath) && !this.hasStartedTest(file)) {
186
+ this.reportedFileFailures.add(file.filepath);
187
+ const relativePath = path.relative(this.testRoot, file.filepath).replace(/\\/g, "/");
188
+ const fileErrors = file.result?.errors ?? [];
189
+ this.emit("TESTSPECTRA_TEST_START", JSON.stringify({ title: relativePath }));
190
+ if (fileErrors.length > 0) {
191
+ for (const err of fileErrors) this.emitError(err);
192
+ } else {
193
+ this.emit("TESTSPECTRA_TEST_ERROR", `Failed to load test file: ${relativePath}`);
194
+ }
195
+ this.emit("TESTSPECTRA_TEST_FAIL", `${relativePath} (0ms)`);
196
+ }
197
+ }
198
+ for (const err of errors) {
199
+ const sanitized = sanitizeError(err);
200
+ if (sanitized && !isVitestNoise(sanitized)) {
201
+ for (const line of sanitized.split("\n")) {
202
+ if (line.trim()) this.emit("TESTSPECTRA_TEST_ERROR", line.trim());
203
+ }
204
+ }
205
+ }
206
+ }
207
+ hasStartedTest(file) {
208
+ const check = (task) => {
209
+ if (task.type === "test" && this.started.has(task.id)) return true;
210
+ const children = task.tasks;
211
+ return Array.isArray(children) ? children.some(check) : false;
212
+ };
213
+ return check(file);
214
+ }
215
+ emit(tag, payload) {
216
+ console.log(`[${tag}] ${payload}`);
217
+ }
218
+ emitError(err) {
219
+ const sanitized = sanitizeError(err);
220
+ for (const line of sanitized.split("\n")) {
221
+ if (line.trim()) this.emit("TESTSPECTRA_TEST_ERROR", line.trim());
222
+ }
223
+ }
224
+ };
225
+ export {
226
+ SpectraVitestReporter
227
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testspectra/react",
3
- "version": "1.1.8-rc.3",
3
+ "version": "1.1.8-rc.30",
4
4
  "description": "Bundled Vitest browser-mode runner for React component testing with the TestSpectra Spectra.* DSL",
5
5
  "keywords": [
6
6
  "testspectra",
@@ -27,37 +27,38 @@
27
27
  "import": "./dist/index.js",
28
28
  "default": "./dist/index.js"
29
29
  },
30
- "./vitest.config.ts": "./vitest.config.ts"
30
+ "./package.json": "./package.json"
31
31
  },
32
32
  "files": [
33
33
  "dist",
34
34
  "bin"
35
35
  ],
36
- "scripts": {
37
- "build": "node scripts/build.js",
38
- "typecheck": "tsc --noEmit",
39
- "test": "vitest run --config ./vitest.config.ts",
40
- "prepublishOnly": "npm run build"
41
- },
42
36
  "dependencies": {
43
- "@testspectra/matchers": "workspace:*",
44
37
  "@testing-library/react": "^16.0.0",
45
38
  "@testing-library/user-event": "^14.5.2",
46
39
  "@vitejs/plugin-react": "^4.3.1",
47
40
  "@vitest/browser": "^2.1.8",
41
+ "cross-spawn": "^7.0.6",
42
+ "esbuild": "^0.28.2",
48
43
  "playwright": "^1.48.0",
49
- "vitest": "catalog:"
44
+ "vitest": "^2.1.8",
45
+ "@testspectra/matchers": "1.1.8-rc.30"
50
46
  },
51
47
  "peerDependencies": {
52
48
  "react": ">=18.0.0",
53
49
  "react-dom": ">=18.0.0"
54
50
  },
55
51
  "devDependencies": {
56
- "@types/node": "catalog:",
52
+ "@types/cross-spawn": "^6.0.6",
53
+ "@types/node": "^20.14.0",
57
54
  "@types/react": "^18.3.3",
58
55
  "@types/react-dom": "^18.3.0",
59
- "esbuild": "catalog:",
60
- "typescript": "catalog:"
56
+ "typescript": "^5.6.3"
61
57
  },
62
- "license": "SEE LICENSE IN LICENSE.md"
63
- }
58
+ "license": "SEE LICENSE IN LICENSE.md",
59
+ "scripts": {
60
+ "build": "node scripts/build.js",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "node bin/testspectra-react.js test"
63
+ }
64
+ }