@epikodelabs/testify 1.0.16

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/esm-loader.mjs ADDED
@@ -0,0 +1,332 @@
1
+ // ESM loader for running TS/JS specs with:
2
+ // - ts-node/esm (TypeScript at runtime)
3
+ // - tsconfig "paths" + "baseUrl" (tsconfig-paths)
4
+ //
5
+ // Works on Windows by converting absolute `C:\...` specifiers to `file://` URLs.
6
+ //
7
+ // Usage example:
8
+ // node --loader @epikodelabs/testify/esm-loader.mjs ./node_modules/@epikodelabs/testify/bin/jasmine --spec ./path/to/test.spec.ts
9
+ //
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { createRequire } from 'module';
13
+ import { fileURLToPath, pathToFileURL } from 'url';
14
+ import { createMatchPath } from 'tsconfig-paths';
15
+ import { load as loadTs, resolve as resolveTs } from 'ts-node/esm';
16
+
17
+ const require = createRequire(import.meta.url);
18
+
19
+ const WINDOWS_ABS_PATH_RE = /^[A-Za-z]:[\\/]/;
20
+
21
+ function isFileUrl(url) {
22
+ return typeof url === 'string' && url.startsWith('file:');
23
+ }
24
+
25
+ function isWindowsAbsPath(specifier) {
26
+ return typeof specifier === 'string' && WINDOWS_ABS_PATH_RE.test(specifier);
27
+ }
28
+
29
+ function cleanJsonLike(text) {
30
+ // Removes // and /* */ comments and trailing commas; good enough for typical tsconfig.json.
31
+ // This is intentionally lightweight to avoid extra deps in the published package.
32
+ let out = '';
33
+ let i = 0;
34
+ let inString = false;
35
+ let stringChar = null;
36
+ let escapeNext = false;
37
+
38
+ while (i < text.length) {
39
+ const ch = text[i];
40
+ const next = text[i + 1];
41
+
42
+ if (escapeNext) {
43
+ out += ch;
44
+ escapeNext = false;
45
+ i += 1;
46
+ continue;
47
+ }
48
+
49
+ if (inString && ch === '\\') {
50
+ out += ch;
51
+ escapeNext = true;
52
+ i += 1;
53
+ continue;
54
+ }
55
+
56
+ if ((ch === '"' || ch === "'") && !escapeNext) {
57
+ if (!inString) {
58
+ inString = true;
59
+ stringChar = ch;
60
+ } else if (ch === stringChar) {
61
+ inString = false;
62
+ stringChar = null;
63
+ }
64
+ out += ch;
65
+ i += 1;
66
+ continue;
67
+ }
68
+
69
+ if (!inString && ch === '/' && next === '/') {
70
+ i += 2;
71
+ while (i < text.length && text[i] !== '\n' && text[i] !== '\r') i += 1;
72
+ continue;
73
+ }
74
+
75
+ if (!inString && ch === '/' && next === '*') {
76
+ i += 2;
77
+ while (i < text.length - 1) {
78
+ if (text[i] === '*' && text[i + 1] === '/') {
79
+ i += 2;
80
+ break;
81
+ }
82
+ i += 1;
83
+ }
84
+ continue;
85
+ }
86
+
87
+ out += ch;
88
+ i += 1;
89
+ }
90
+
91
+ // Remove trailing commas (outside strings).
92
+ let out2 = '';
93
+ i = 0;
94
+ inString = false;
95
+ stringChar = null;
96
+ escapeNext = false;
97
+
98
+ while (i < out.length) {
99
+ const ch = out[i];
100
+
101
+ if (escapeNext) {
102
+ out2 += ch;
103
+ escapeNext = false;
104
+ i += 1;
105
+ continue;
106
+ }
107
+
108
+ if (inString && ch === '\\') {
109
+ out2 += ch;
110
+ escapeNext = true;
111
+ i += 1;
112
+ continue;
113
+ }
114
+
115
+ if ((ch === '"' || ch === "'") && !escapeNext) {
116
+ if (!inString) {
117
+ inString = true;
118
+ stringChar = ch;
119
+ } else if (ch === stringChar) {
120
+ inString = false;
121
+ stringChar = null;
122
+ }
123
+ out2 += ch;
124
+ i += 1;
125
+ continue;
126
+ }
127
+
128
+ if (!inString && ch === ',') {
129
+ let j = i + 1;
130
+ while (j < out.length && /\s/.test(out[j])) j += 1;
131
+ if (j < out.length && (out[j] === ']' || out[j] === '}')) {
132
+ i += 1;
133
+ continue;
134
+ }
135
+ }
136
+
137
+ out2 += ch;
138
+ i += 1;
139
+ }
140
+
141
+ return out2;
142
+ }
143
+
144
+ function readTsconfig(tsconfigPath, visited = new Set()) {
145
+ const resolved = path.resolve(tsconfigPath);
146
+ if (visited.has(resolved)) return {};
147
+ visited.add(resolved);
148
+
149
+ if (!fs.existsSync(resolved)) return {};
150
+
151
+ const raw = fs.readFileSync(resolved, 'utf8');
152
+ const parsed = JSON.parse(cleanJsonLike(raw));
153
+ const tsconfigDir = path.dirname(resolved);
154
+
155
+ let base = {};
156
+ const ext = parsed.extends;
157
+ if (typeof ext === 'string' && ext.length > 0) {
158
+ const tryResolve = (specifier) => {
159
+ // Relative / absolute
160
+ if (
161
+ specifier.startsWith('.') ||
162
+ specifier.startsWith('/') ||
163
+ isWindowsAbsPath(specifier)
164
+ ) {
165
+ const candidate = path.resolve(tsconfigDir, specifier);
166
+ if (fs.existsSync(candidate)) return candidate;
167
+ if (fs.existsSync(candidate + '.json')) return candidate + '.json';
168
+ return null;
169
+ }
170
+
171
+ // Package-based (e.g. "@tsconfig/node20/tsconfig.json")
172
+ try {
173
+ return require.resolve(specifier, { paths: [tsconfigDir] });
174
+ } catch {
175
+ try {
176
+ return require.resolve(specifier + '.json', { paths: [tsconfigDir] });
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+ };
182
+
183
+ const resolvedExt = tryResolve(ext);
184
+ if (resolvedExt) {
185
+ base = readTsconfig(resolvedExt, visited);
186
+ }
187
+ }
188
+
189
+ const merged = {
190
+ ...base,
191
+ ...parsed,
192
+ compilerOptions: {
193
+ ...(base.compilerOptions ?? {}),
194
+ ...(parsed.compilerOptions ?? {}),
195
+ paths: {
196
+ ...((base.compilerOptions ?? {}).paths ?? {}),
197
+ ...((parsed.compilerOptions ?? {}).paths ?? {}),
198
+ },
199
+ },
200
+ };
201
+
202
+ // Preserve tsconfig directory (used for baseUrl resolution).
203
+ merged.__tsconfigDir = tsconfigDir;
204
+ return merged;
205
+ }
206
+
207
+ function findUp(startDir, filename) {
208
+ let current = path.resolve(startDir);
209
+ while (true) {
210
+ const candidate = path.join(current, filename);
211
+ if (fs.existsSync(candidate)) return candidate;
212
+ const parent = path.dirname(current);
213
+ if (parent === current) return null;
214
+ current = parent;
215
+ }
216
+ }
217
+
218
+ function getTsconfigFor(startDir) {
219
+ const envProject = process.env.TS_NODE_PROJECT;
220
+ if (envProject) {
221
+ const fromCwd = path.resolve(process.cwd(), envProject);
222
+ if (fs.existsSync(fromCwd)) return fromCwd;
223
+ }
224
+ return findUp(startDir, 'tsconfig.json') ?? findUp(process.cwd(), 'tsconfig.json');
225
+ }
226
+
227
+ const matchPathCache = new Map();
228
+
229
+ function getMatchPath(startDir) {
230
+ const tsconfigPath = getTsconfigFor(startDir);
231
+ if (!tsconfigPath) return null;
232
+
233
+ const key = tsconfigPath;
234
+ const cached = matchPathCache.get(key);
235
+ if (cached) return cached;
236
+
237
+ const tsconfig = readTsconfig(tsconfigPath);
238
+ const compilerOptions = tsconfig.compilerOptions ?? {};
239
+ const tsconfigDir = tsconfig.__tsconfigDir ?? path.dirname(tsconfigPath);
240
+
241
+ const baseUrl = compilerOptions.baseUrl
242
+ ? path.resolve(tsconfigDir, compilerOptions.baseUrl)
243
+ : tsconfigDir;
244
+ const pathsMap = compilerOptions.paths ?? {};
245
+
246
+ const matchPath = createMatchPath(baseUrl, pathsMap);
247
+ matchPathCache.set(key, matchPath);
248
+ return matchPath;
249
+ }
250
+
251
+ const EXTENSIONS = [
252
+ '.ts',
253
+ '.tsx',
254
+ '.mts',
255
+ '.cts',
256
+ '.js',
257
+ '.jsx',
258
+ '.mjs',
259
+ '.cjs',
260
+ '.json',
261
+ ];
262
+
263
+ function tryResolveWithExtensions(absPath) {
264
+ for (const ext of EXTENSIONS) {
265
+ const candidate = absPath.endsWith(ext) ? absPath : absPath + ext;
266
+ if (fs.existsSync(candidate)) return candidate;
267
+ }
268
+ for (const ext of EXTENSIONS) {
269
+ const idx = path.join(absPath, 'index' + ext);
270
+ if (fs.existsSync(idx)) return idx;
271
+ }
272
+ return null;
273
+ }
274
+
275
+ export async function resolve(specifier, context, defaultResolve) {
276
+ // Windows: absolute paths must be file:// URLs for ESM.
277
+ if (isWindowsAbsPath(specifier)) {
278
+ return resolveTs(pathToFileURL(specifier).href, context, defaultResolve);
279
+ }
280
+
281
+ const parentUrl = context?.parentURL;
282
+ const startDir =
283
+ isFileUrl(parentUrl) ? path.dirname(fileURLToPath(parentUrl)) : process.cwd();
284
+
285
+ // 1) Try tsconfig paths mapping.
286
+ const matchPath = getMatchPath(startDir);
287
+ if (matchPath) {
288
+ const resolved = matchPath(specifier, undefined, undefined, EXTENSIONS);
289
+ if (resolved) {
290
+ return resolveTs(pathToFileURL(resolved).href, context, defaultResolve);
291
+ }
292
+ }
293
+
294
+ // 2) Relative specifiers without extension.
295
+ if (
296
+ (specifier.startsWith('./') || specifier.startsWith('../')) &&
297
+ isFileUrl(parentUrl)
298
+ ) {
299
+ const parentFile = fileURLToPath(parentUrl);
300
+ const candidate = path.resolve(path.dirname(parentFile), specifier);
301
+ const withExt = tryResolveWithExtensions(candidate);
302
+ if (withExt) {
303
+ return resolveTs(pathToFileURL(withExt).href, context, defaultResolve);
304
+ }
305
+ }
306
+
307
+ // 3) Fallback to ts-node/esm resolver.
308
+ return resolveTs(specifier, context, defaultResolve);
309
+ }
310
+
311
+ export async function load(url, context, defaultLoad) {
312
+ // ts-node's loader returns `format: null` for extensionless files, which breaks running
313
+ // our (extensionless) bin entry under `--loader` on Windows.
314
+ if (typeof url === 'string' && url.startsWith('file:')) {
315
+ const filePath = fileURLToPath(url);
316
+ const base = path.basename(filePath);
317
+ const parent = path.basename(path.dirname(filePath));
318
+
319
+ if (
320
+ parent === 'bin' &&
321
+ (base === 'jasmine') &&
322
+ path.extname(filePath) === '' &&
323
+ fs.existsSync(filePath)
324
+ ) {
325
+ const raw = fs.readFileSync(filePath, 'utf8');
326
+ const source = raw.startsWith('#!') ? raw.replace(/^#!.*\r?\n/, '') : raw;
327
+ return { format: 'module', source, shortCircuit: true };
328
+ }
329
+ }
330
+
331
+ return loadTs(url, context, defaultLoad);
332
+ }
package/lib/index.js ADDED
@@ -0,0 +1,231 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+ class JasmineConsoleReporter {
4
+ static {
5
+ __name(this, "JasmineConsoleReporter");
6
+ }
7
+ print = /* @__PURE__ */ __name((message) => process.stdout.write(message), "print");
8
+ showColors = true;
9
+ specCount = 0;
10
+ executableSpecCount = 0;
11
+ failureCount = 0;
12
+ failedSpecs = [];
13
+ pendingSpecs = [];
14
+ alwaysListPendingSpecs = true;
15
+ ansi = {
16
+ green: "\x1B[32m",
17
+ red: "\x1B[31m",
18
+ yellow: "\x1B[33m",
19
+ none: "\x1B[0m"
20
+ };
21
+ failedSuites = [];
22
+ stackFilter = /* @__PURE__ */ __name((stack) => stack, "stackFilter");
23
+ randomSeedReproductionCmd(seed) {
24
+ return "jasmine --random=true --seed=" + seed;
25
+ }
26
+ /**
27
+ * Configures the reporter.
28
+ */
29
+ setOptions(options) {
30
+ if (options.print) {
31
+ this.print = options.print;
32
+ }
33
+ this.showColors = options.showColors || false;
34
+ if (options.stackFilter) {
35
+ this.stackFilter = options.stackFilter;
36
+ }
37
+ if (options.randomSeedReproductionCmd) {
38
+ this.randomSeedReproductionCmd = options.randomSeedReproductionCmd;
39
+ }
40
+ if (options.alwaysListPendingSpecs !== void 0) {
41
+ this.alwaysListPendingSpecs = options.alwaysListPendingSpecs;
42
+ }
43
+ }
44
+ jasmineStarted(options) {
45
+ this.specCount = 0;
46
+ this.executableSpecCount = 0;
47
+ this.failureCount = 0;
48
+ if (options?.order?.random) {
49
+ this.print("Randomized with seed " + options.order.seed);
50
+ this.printNewline();
51
+ }
52
+ this.print("Started");
53
+ this.printNewline();
54
+ }
55
+ jasmineDone(result) {
56
+ if (result.failedExpectations) {
57
+ this.failureCount += result.failedExpectations.length;
58
+ }
59
+ this.printNewline();
60
+ this.printNewline();
61
+ if (this.failedSpecs.length > 0) {
62
+ this.print("Failures:");
63
+ }
64
+ for (let i = 0; i < this.failedSpecs.length; i++) {
65
+ this.specFailureDetails(this.failedSpecs[i], i + 1);
66
+ }
67
+ for (let i = 0; i < this.failedSuites.length; i++) {
68
+ this.suiteFailureDetails(this.failedSuites[i]);
69
+ }
70
+ if (result.failedExpectations?.length > 0) {
71
+ this.suiteFailureDetails({
72
+ fullName: "top suite",
73
+ failedExpectations: result.failedExpectations
74
+ });
75
+ }
76
+ if (this.alwaysListPendingSpecs || result.overallStatus === "passed") {
77
+ if (this.pendingSpecs.length > 0) {
78
+ this.print("Pending:");
79
+ }
80
+ for (let i = 0; i < this.pendingSpecs.length; i++) {
81
+ this.pendingSpecDetails(this.pendingSpecs[i], i + 1);
82
+ }
83
+ }
84
+ if (this.specCount > 0) {
85
+ this.printNewline();
86
+ if (this.executableSpecCount !== this.specCount) {
87
+ this.print(
88
+ "Ran " + this.executableSpecCount + " of " + this.specCount + this.plural(" spec", this.specCount)
89
+ );
90
+ this.printNewline();
91
+ }
92
+ let specCounts = this.executableSpecCount + " " + this.plural("spec", this.executableSpecCount) + ", " + this.failureCount + " " + this.plural("failure", this.failureCount);
93
+ if (this.pendingSpecs.length) {
94
+ specCounts += ", " + this.pendingSpecs.length + " pending " + this.plural("spec", this.pendingSpecs.length);
95
+ }
96
+ this.print(specCounts);
97
+ } else {
98
+ this.print("No specs found");
99
+ }
100
+ this.printNewline();
101
+ const seconds = result ? result.totalTime / 1e3 : 0;
102
+ this.print("Finished in " + seconds + " " + this.plural("second", seconds));
103
+ this.printNewline();
104
+ if (result && result.overallStatus === "incomplete") {
105
+ this.print("Incomplete: " + result.incompleteReason);
106
+ this.printNewline();
107
+ }
108
+ if (result.order?.random) {
109
+ this.print("Randomized with seed " + result.order.seed);
110
+ this.print(" (" + this.randomSeedReproductionCmd(result.order.seed) + ")");
111
+ this.printNewline();
112
+ }
113
+ }
114
+ specDone(result) {
115
+ this.specCount++;
116
+ if (result.status == "pending") {
117
+ this.pendingSpecs.push(result);
118
+ this.executableSpecCount++;
119
+ this.print(this.colored("yellow", "*"));
120
+ return;
121
+ }
122
+ if (result.status == "passed") {
123
+ this.executableSpecCount++;
124
+ this.print(this.colored("green", "."));
125
+ return;
126
+ }
127
+ if (result.status == "failed") {
128
+ this.failureCount++;
129
+ this.failedSpecs.push(result);
130
+ this.executableSpecCount++;
131
+ this.print(this.colored("red", "F"));
132
+ }
133
+ }
134
+ suiteDone(result) {
135
+ if (result.failedExpectations && result.failedExpectations.length > 0) {
136
+ this.failureCount++;
137
+ this.failedSuites.push(result);
138
+ }
139
+ }
140
+ reporterCapabilities = { parallel: true };
141
+ printNewline() {
142
+ this.print("\n");
143
+ }
144
+ colored(color, str) {
145
+ return this.showColors ? this.ansi[color] + str + this.ansi.none : str;
146
+ }
147
+ plural(str, count) {
148
+ return count == 1 ? str : str + "s";
149
+ }
150
+ repeat(thing, times) {
151
+ return Array.from({ length: times }, () => thing);
152
+ }
153
+ indent(str, spaces) {
154
+ const lines = (str || "").split("\n");
155
+ return lines.map((line) => this.repeat(" ", spaces).join("") + line).join("\n");
156
+ }
157
+ specFailureDetails(result, failedSpecNumber) {
158
+ this.printNewline();
159
+ this.print(failedSpecNumber + ") ");
160
+ this.print(result.fullName);
161
+ this.printFailedExpectations(result);
162
+ if (result.debugLogs?.length) {
163
+ this.printNewline();
164
+ this.print(this.indent("Debug logs:", 2));
165
+ this.printNewline();
166
+ for (const entry of result.debugLogs) {
167
+ this.print(this.indent(`${entry.timestamp}ms: ${entry.message}`, 4));
168
+ this.printNewline();
169
+ }
170
+ }
171
+ }
172
+ suiteFailureDetails(result) {
173
+ this.printNewline();
174
+ this.print("Suite error: " + result.fullName);
175
+ this.printFailedExpectations(result);
176
+ }
177
+ printFailedExpectations(result) {
178
+ for (let i = 0; i < result.failedExpectations.length; i++) {
179
+ const failedExpectation = result.failedExpectations[i];
180
+ this.printNewline();
181
+ this.print(this.indent("Message:", 2));
182
+ this.printNewline();
183
+ this.print(this.colored("red", this.indent(failedExpectation.message, 4)));
184
+ this.printNewline();
185
+ this.print(this.indent("Stack:", 2));
186
+ this.printNewline();
187
+ this.print(this.indent(this.stackFilter(failedExpectation.stack), 4));
188
+ }
189
+ if (result.failedExpectations.length === 0 && Array.isArray(result.passedExpectations) && result.passedExpectations.length === 0) {
190
+ this.printNewline();
191
+ this.print(this.indent("Message:", 2));
192
+ this.printNewline();
193
+ this.print(this.colored("red", this.indent("Spec has no expectations", 4)));
194
+ }
195
+ this.printNewline();
196
+ }
197
+ pendingSpecDetails(result, pendingSpecNumber) {
198
+ this.printNewline();
199
+ this.printNewline();
200
+ this.print(pendingSpecNumber + ") ");
201
+ this.print(result.fullName);
202
+ this.printNewline();
203
+ let pendingReason = "No reason given";
204
+ if (result.pendingReason && result.pendingReason !== "") {
205
+ pendingReason = result.pendingReason;
206
+ }
207
+ this.print(this.indent(this.colored("yellow", pendingReason), 2));
208
+ this.printNewline();
209
+ }
210
+ }
211
+ class AwaitableJasmineConsoleReporter extends JasmineConsoleReporter {
212
+ static {
213
+ __name(this, "AwaitableJasmineConsoleReporter");
214
+ }
215
+ resolveComplete;
216
+ complete;
217
+ constructor() {
218
+ super();
219
+ this.complete = new Promise((resolve) => {
220
+ this.resolveComplete = resolve;
221
+ });
222
+ }
223
+ jasmineDone(result) {
224
+ super.jasmineDone(result);
225
+ this.resolveComplete?.(result);
226
+ }
227
+ }
228
+ export {
229
+ AwaitableJasmineConsoleReporter,
230
+ JasmineConsoleReporter
231
+ };
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@epikodelabs/testify",
3
+ "version": "1.0.16",
4
+ "description": "Serve and run your Jasmine specs in a browser",
5
+ "type": "module",
6
+ "bin": {
7
+ "jasmine": "bin/jasmine",
8
+ "testify": "bin/testify"
9
+ },
10
+ "files": [
11
+ "README.md",
12
+ "LICENSE",
13
+ "esm-loader.mjs",
14
+ "package.json",
15
+ "postinstall.mjs",
16
+ "assets/",
17
+ "bin/",
18
+ "lib/",
19
+ "node_modules/"
20
+ ],
21
+ "scripts": {
22
+ "postinstall": "node postinstall.mjs"
23
+ },
24
+ "keywords": [
25
+ "Jasmine",
26
+ "Testing",
27
+ "TDD",
28
+ "Browser",
29
+ "Node",
30
+ "Coverage",
31
+ "HMR"
32
+ ],
33
+ "author": "Oleksii Shepel",
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "chokidar": "^5.0.0",
37
+ "chromium-bidi": "^13.1.1",
38
+ "deasync": "^0.1.31",
39
+ "esbuild": "^0.25.11",
40
+ "fdir": "^6.5.0",
41
+ "glob": "^13.0.1",
42
+ "istanbul-api": "^3.0.0",
43
+ "istanbul-lib-coverage": "^3.2.2",
44
+ "istanbul-lib-instrument": "^6.0.3",
45
+ "istanbul-lib-report": "^3.0.1",
46
+ "istanbul-lib-source-maps": "^5.0.6",
47
+ "istanbul-reports": "^3.2.0",
48
+ "jasmine-core": "^6.0.1",
49
+ "module-alias": "^2.3.4",
50
+ "path-scurry": "^2.0.1",
51
+ "picomatch": "^4.0.3",
52
+ "playwright": "^1.58.2",
53
+ "rollup": "^4.57.1",
54
+ "tinyglobby": "^0.2.15",
55
+ "ts-node": "^10.9.2",
56
+ "tsconfig-paths": "^4.2.0",
57
+ "vite": "^7.3.1",
58
+ "word-wrap": "^1.2.5",
59
+ "ws": "^8.19.0"
60
+ },
61
+ "bundleDependencies": [
62
+ "chokidar",
63
+ "chromium-bidi",
64
+ "deasync",
65
+ "esbuild",
66
+ "fdir",
67
+ "glob",
68
+ "istanbul-api",
69
+ "istanbul-lib-coverage",
70
+ "istanbul-lib-instrument",
71
+ "istanbul-lib-report",
72
+ "istanbul-lib-source-maps",
73
+ "istanbul-reports",
74
+ "jasmine-core",
75
+ "module-alias",
76
+ "path-scurry",
77
+ "picomatch",
78
+ "playwright",
79
+ "rollup",
80
+ "tinyglobby",
81
+ "ts-node",
82
+ "tsconfig-paths",
83
+ "vite",
84
+ "word-wrap",
85
+ "ws"
86
+ ],
87
+ "peerDependencies": {},
88
+ "overrides": {}
89
+ }