@isentinel/jest-roblox 0.0.3 → 0.0.4

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/dist/cli.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { t as CliOptions } from "./schema-hhbEFVXy.mjs";
1
+ import { t as CliOptions } from "./schema-ua9HqdbX.mjs";
2
2
 
3
3
  //#region src/cli.d.ts
4
- declare function main(): Promise<void>;
5
4
  declare function parseArgs(args: Array<string>): CliOptions;
6
5
  declare function run(args: Array<string>): Promise<number>;
6
+ declare function main(): Promise<void>;
7
7
  //#endregion
8
8
  export { main, parseArgs, run };
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { g as createOpenCloudBackend, i as execute, m as createStudioBackend, n as parseGameOutput, o as writeJsonFile, r as writeGameOutput, t as formatGameOutputNotice, u as loadConfig, y as LuauScriptError } from "./game-output--EpqHGEr.mjs";
1
+ import { S as LuauScriptError, _ as createStudioBackend, a as execute, d as loadConfig, h as isValidBackend, i as runTypecheck, m as VALID_BACKENDS, n as parseGameOutput, r as writeGameOutput, s as writeJsonFile, t as formatGameOutputNotice, y as createOpenCloudBackend } from "./game-output-DO5fOpW3.mjs";
2
2
  import * as path$1 from "node:path";
3
3
  import process from "node:process";
4
4
  import { parseArgs as parseArgs$1 } from "node:util";
@@ -6,7 +6,7 @@ import { WebSocketServer } from "ws";
6
6
  import * as fs from "node:fs";
7
7
 
8
8
  //#region package.json
9
- var version = "0.0.3";
9
+ var version = "0.0.4";
10
10
 
11
11
  //#endregion
12
12
  //#region src/backends/auto.ts
@@ -38,17 +38,20 @@ async function probeStudioPlugin(port, timeoutMs, createServer = (wsPort) => {
38
38
  const wss = createServer(port);
39
39
  const timer = setTimeout(() => {
40
40
  wss.close();
41
- resolve(false);
41
+ resolve({ detected: false });
42
42
  }, timeoutMs);
43
- wss.on("connection", () => {
43
+ wss.on("connection", (ws) => {
44
44
  clearTimeout(timer);
45
- wss.close();
46
- resolve(true);
45
+ resolve({
46
+ detected: true,
47
+ server: wss,
48
+ socket: ws
49
+ });
47
50
  });
48
51
  wss.on("error", () => {
49
52
  clearTimeout(timer);
50
53
  wss.close();
51
- resolve(false);
54
+ resolve({ detected: false });
52
55
  });
53
56
  });
54
57
  }
@@ -58,10 +61,15 @@ async function resolveBackend(config, probe = probeStudioPlugin) {
58
61
  timeout: config.timeout
59
62
  });
60
63
  if (config.backend === "open-cloud") return createOpenCloudBackend();
61
- if (await probe(config.port, 500)) {
64
+ const probeResult = await probe(config.port, 500);
65
+ if (probeResult.detected) {
62
66
  process.stderr.write("Backend: studio (plugin detected)\n");
63
67
  const studio = createStudioBackend({
64
68
  port: config.port,
69
+ preConnected: {
70
+ server: probeResult.server,
71
+ socket: probeResult.socket
72
+ },
65
73
  timeout: config.timeout
66
74
  });
67
75
  if (hasOpenCloudCredentials()) return new StudioWithFallback(studio);
@@ -77,40 +85,6 @@ function hasOpenCloudCredentials() {
77
85
  return process.env["ROBLOX_OPEN_CLOUD_API_KEY"] !== void 0 && process.env["ROBLOX_UNIVERSE_ID"] !== void 0 && process.env["ROBLOX_PLACE_ID"] !== void 0;
78
86
  }
79
87
 
80
- //#endregion
81
- //#region src/patch.ts
82
- const PATCH_MARKER = "-- jest-roblox-cli patch: enable snapshot mock override";
83
- const PATCH_LINES = `${PATCH_MARKER}
84
- if _G.__mockGetDataModelService then
85
- \treturn _G.__mockGetDataModelService
86
- end
87
-
88
- `;
89
- const PACKAGE_SUBPATH = path$1.join("node_modules", "@rbxts-js", "roblox-shared", "src", "getDataModelService.lua");
90
- function patchGetDataModelService(rootDirectory) {
91
- const filePath = findSourceFile(rootDirectory);
92
- const content = fs.readFileSync(filePath, "utf-8");
93
- if (content.includes(PATCH_MARKER)) return {
94
- filePath,
95
- status: "already-patched"
96
- };
97
- fs.writeFileSync(filePath, PATCH_LINES + content);
98
- return {
99
- filePath,
100
- status: "patched"
101
- };
102
- }
103
- function findSourceFile(rootDirectory) {
104
- let current = path$1.resolve(rootDirectory);
105
- const { root } = path$1.parse(current);
106
- while (current !== root) {
107
- const candidate = path$1.join(current, PACKAGE_SUBPATH);
108
- if (fs.existsSync(candidate)) return candidate;
109
- current = path$1.dirname(current);
110
- }
111
- throw new Error(`Cannot find @rbxts-js/roblox-shared. Searched from ${rootDirectory} to filesystem root.`);
112
- }
113
-
114
88
  //#endregion
115
89
  //#region src/utils/glob.ts
116
90
  function globSync(pattern, options = {}) {
@@ -138,6 +112,7 @@ function walkDirectory(directoryPath, baseDirectory) {
138
112
 
139
113
  //#endregion
140
114
  //#region src/cli.ts
115
+ const TYPE_TEST_PATTERN = /\.(test-d|spec-d)\.ts$/;
141
116
  const HELP_TEXT = `
142
117
  Usage: jest-roblox [options] [files...]
143
118
 
@@ -164,6 +139,9 @@ Options:
164
139
  --setupFiles <path...> DataModel paths to setup scripts
165
140
  --setupFilesAfterEnv <path...> DataModel paths to post-env setup scripts
166
141
  --no-show-luau Hide Luau code in failure output
142
+ --typecheck Enable type testing (*.test-d.ts, *.spec-d.ts)
143
+ --typecheckOnly Run only type tests, skip runtime tests
144
+ --typecheckTsconfig <path> tsconfig for type testing
167
145
  --help Show this help message
168
146
  --version Show version number
169
147
 
@@ -179,10 +157,6 @@ Examples:
179
157
  jest-roblox -t "should spawn" Run tests matching pattern
180
158
  jest-roblox --json --outputFile results.json
181
159
  `;
182
- async function main() {
183
- const exitCode = await run(process.argv.slice(2));
184
- process.exit(exitCode);
185
- }
186
160
  function parseArgs(args) {
187
161
  const { positionals, values } = parseArgs$1({
188
162
  allowPositionals: true,
@@ -228,6 +202,9 @@ function parseArgs(args) {
228
202
  },
229
203
  "testPathPattern": { type: "string" },
230
204
  "timeout": { type: "string" },
205
+ "typecheck": { type: "boolean" },
206
+ "typecheckOnly": { type: "boolean" },
207
+ "typecheckTsconfig": { type: "string" },
231
208
  "updateSnapshot": {
232
209
  short: "u",
233
210
  type: "boolean"
@@ -245,7 +222,7 @@ function parseArgs(args) {
245
222
  const port = values.port !== void 0 ? Number.parseInt(values.port, 10) : void 0;
246
223
  const timeout = values.timeout !== void 0 ? Number.parseInt(values.timeout, 10) : void 0;
247
224
  return {
248
- backend: values.backend,
225
+ backend: validateBackend(values.backend),
249
226
  cache: values["no-cache"] === true ? false : values.cache,
250
227
  color: values["no-color"] === true ? false : values.color,
251
228
  compact: values.compact,
@@ -268,6 +245,9 @@ function parseArgs(args) {
268
245
  testNamePattern: values.testNamePattern,
269
246
  testPathPattern: values.testPathPattern,
270
247
  timeout,
248
+ typecheck: values.typecheckOnly === true ? true : values.typecheck,
249
+ typecheckOnly: values.typecheckOnly,
250
+ typecheckTsconfig: values.typecheckTsconfig,
271
251
  updateSnapshot: values.updateSnapshot,
272
252
  verbose: values.verbose,
273
253
  version: values.version
@@ -281,6 +261,10 @@ async function run(args) {
281
261
  return 2;
282
262
  }
283
263
  }
264
+ async function main() {
265
+ const exitCode = await run(process.argv.slice(2));
266
+ process.exit(exitCode);
267
+ }
284
268
  function printError(err) {
285
269
  if (err instanceof LuauScriptError) {
286
270
  const hint = getLuauErrorHint(err.message);
@@ -290,8 +274,33 @@ function printError(err) {
290
274
  } else if (err instanceof Error) console.error(`Error: ${err.message}`);
291
275
  else console.error("An unknown error occurred");
292
276
  }
277
+ async function executeRuntimeTests(config, testFiles, totalFiles) {
278
+ if (!config.silent && (!config.compact || config.verbose) && !config.json && testFiles.length !== totalFiles) process.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
279
+ return execute({
280
+ backend: await resolveBackend(config),
281
+ config,
282
+ testFiles,
283
+ version
284
+ });
285
+ }
286
+ function writeGameOutputIfConfigured(config, gameOutput, options) {
287
+ if (config.gameOutput === void 0) return;
288
+ const entries = parseGameOutput(gameOutput);
289
+ writeGameOutput(config.gameOutput, entries);
290
+ if (!config.silent && options.hintsShown !== true) {
291
+ const notice = formatGameOutputNotice(config.gameOutput, entries.length);
292
+ if (notice) console.error(notice);
293
+ }
294
+ }
295
+ async function outputResults(config, typecheckResult, runtimeResult) {
296
+ const mergedResult = mergeResults(typecheckResult, runtimeResult?.result);
297
+ if (runtimeResult !== void 0 && runtimeResult.output !== "") console.log(runtimeResult.output);
298
+ if (typecheckResult !== void 0 && !config.silent) printTypecheckSummary(typecheckResult);
299
+ if (config.outputFile !== void 0) await writeJsonFile(mergedResult, config.outputFile);
300
+ if (runtimeResult !== void 0) writeGameOutputIfConfigured(config, runtimeResult.gameOutput, { hintsShown: !mergedResult.success });
301
+ return mergedResult.success ? 0 : 1;
302
+ }
293
303
  async function runInner(args) {
294
- if (args[0] === "patch") return runPatch();
295
304
  const cli = parseArgs(args);
296
305
  if (cli.help === true) {
297
306
  console.log(HELP_TEXT);
@@ -307,32 +316,24 @@ async function runInner(args) {
307
316
  console.error("No test files found");
308
317
  return 2;
309
318
  }
310
- if (!config.silent && !config.compact && !config.json && discovery.files.length !== discovery.totalFiles) process.stderr.write(`Running ${discovery.files.length} of ${discovery.totalFiles} test files\n`);
311
- const result = await execute({
312
- backend: await resolveBackend(config),
313
- config,
314
- testFiles: discovery.files,
315
- version
316
- });
317
- if (result.output) console.log(result.output);
318
- if (config.outputFile !== void 0) await writeJsonFile(result.result, config.outputFile);
319
- writeGameOutputIfConfigured(config, result.gameOutput);
320
- return result.exitCode;
321
- }
322
- function writeGameOutputIfConfigured(config, gameOutput) {
323
- if (config.gameOutput === void 0) return;
324
- const entries = parseGameOutput(gameOutput);
325
- writeGameOutput(config.gameOutput, entries);
326
- if (!config.silent) {
327
- const notice = formatGameOutputNotice(config.gameOutput, entries.length);
328
- if (notice) console.error(notice);
319
+ const typeTestFiles = config.typecheck ? discovery.files.filter((file) => TYPE_TEST_PATTERN.test(file)) : [];
320
+ const runtimeTestFiles = config.typecheckOnly ? [] : discovery.files.filter((file) => !TYPE_TEST_PATTERN.test(file));
321
+ if (typeTestFiles.length === 0 && runtimeTestFiles.length === 0) {
322
+ console.error("No test files found for the selected mode");
323
+ return 2;
329
324
  }
325
+ return outputResults(config, typeTestFiles.length > 0 ? runTypecheck({
326
+ files: typeTestFiles,
327
+ rootDir: config.rootDir,
328
+ tsconfig: config.typecheckTsconfig
329
+ }) : void 0, runtimeTestFiles.length > 0 ? await executeRuntimeTests(config, runtimeTestFiles, discovery.totalFiles) : void 0);
330
330
  }
331
331
  const LUAU_ERROR_HINTS = [
332
332
  [/Failed to find Jest instance in ReplicatedStorage/, "Set \"jestPath\" in your config to specify the Jest module location, e.g. \"ReplicatedStorage/rbxts_include/node_modules/@rbxts/jest/src\""],
333
333
  [/Failed to find Jest instance at path/, "The configured jestPath does not resolve to a valid instance. Verify the path matches your Rojo project tree."],
334
334
  [/Failed to find service/, "The first segment of jestPath must be a valid Roblox service name (e.g. ReplicatedStorage, ServerScriptService)."],
335
- [/No projects configured/, "Set \"projects\" in jest.config.ts (e.g. [\"ReplicatedStorage/client\", \"ServerScriptService/server\"]) or pass --projects."]
335
+ [/No projects configured/, "Set \"projects\" in jest.config.ts (e.g. [\"ReplicatedStorage/client\", \"ServerScriptService/server\"]) or pass --projects."],
336
+ [/Infinite yield detected/, "A :WaitForChild() call is waiting for an instance that doesn't exist. Check your DataModel paths and Rojo project configuration."]
336
337
  ];
337
338
  function discoverTestFiles(config, cliFiles) {
338
339
  if (cliFiles && cliFiles.length > 0) {
@@ -362,6 +363,14 @@ function discoverTestFiles(config, cliFiles) {
362
363
  totalFiles
363
364
  };
364
365
  }
366
+ function validateBackend(value) {
367
+ if (value === void 0) return;
368
+ if (!isValidBackend(value)) {
369
+ const valid = [...VALID_BACKENDS].join(", ");
370
+ throw new Error(`Invalid backend "${value}". Must be one of: ${valid}`);
371
+ }
372
+ return value;
373
+ }
365
374
  function getLuauErrorHint(message) {
366
375
  for (const [pattern, hint] of LUAU_ERROR_HINTS) if (pattern.test(message)) return hint;
367
376
  }
@@ -388,18 +397,41 @@ function mergeCliWithConfig(cli, config) {
388
397
  testNamePattern: cli.testNamePattern ?? config.testNamePattern,
389
398
  testPathPattern: cli.testPathPattern ?? config.testPathPattern,
390
399
  timeout: cli.timeout ?? config.timeout,
400
+ typecheck: cli.typecheck ?? config.typecheck,
401
+ typecheckOnly: cli.typecheckOnly ?? config.typecheckOnly,
402
+ typecheckTsconfig: cli.typecheckTsconfig ?? config.typecheckTsconfig,
391
403
  updateSnapshot: cli.updateSnapshot ?? config.updateSnapshot,
392
404
  verbose: cli.verbose ?? config.verbose
393
405
  };
394
406
  }
395
- function runPatch() {
396
- const result = patchGetDataModelService(process.cwd());
397
- if (result.status === "already-patched") console.log(`Already patched: ${result.filePath}`);
398
- else {
399
- console.log(`Patched: ${result.filePath}`);
400
- console.log("Rebuild your .rbxl to include the patch.");
407
+ function mergeResults(typecheck, runtime) {
408
+ if (typecheck !== void 0 && runtime !== void 0) return {
409
+ numFailedTests: typecheck.numFailedTests + runtime.numFailedTests,
410
+ numPassedTests: typecheck.numPassedTests + runtime.numPassedTests,
411
+ numPendingTests: typecheck.numPendingTests + runtime.numPendingTests,
412
+ numTotalTests: typecheck.numTotalTests + runtime.numTotalTests,
413
+ startTime: Math.min(typecheck.startTime, runtime.startTime),
414
+ success: typecheck.success && runtime.success,
415
+ testResults: [...typecheck.testResults, ...runtime.testResults]
416
+ };
417
+ if (typecheck !== void 0) return typecheck;
418
+ if (runtime !== void 0) return runtime;
419
+ throw new Error("mergeResults requires at least one result");
420
+ }
421
+ function printTypecheckFailures(result) {
422
+ for (const file of result.testResults) for (const test of file.testResults) {
423
+ if (test.status !== "failed") continue;
424
+ process.stderr.write(` FAIL ${test.fullName}\n`);
425
+ for (const message of test.failureMessages) process.stderr.write(` ${message}\n`);
401
426
  }
402
- return 0;
427
+ }
428
+ function printTypecheckSummary(result) {
429
+ const passed = result.numPassedTests;
430
+ const failed = result.numFailedTests;
431
+ const total = result.numTotalTests;
432
+ if (failed > 0) printTypecheckFailures(result);
433
+ const failedPart = failed > 0 ? `${String(failed)} failed, ` : "";
434
+ process.stderr.write(`\nType Tests: ${failedPart}${String(passed)} passed, ${String(total)} total\n`);
403
435
  }
404
436
 
405
437
  //#endregion