@isentinel/jest-roblox 0.0.2 → 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-CEsyg-FX.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-BDatGA9S.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.2";
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
 
@@ -162,7 +137,11 @@ Options:
162
137
  --pollInterval <ms> Open Cloud poll interval in ms (default: 500)
163
138
  --projects <path...> DataModel paths to search for tests
164
139
  --setupFiles <path...> DataModel paths to setup scripts
140
+ --setupFilesAfterEnv <path...> DataModel paths to post-env setup scripts
165
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
166
145
  --help Show this help message
167
146
  --version Show version number
168
147
 
@@ -178,10 +157,6 @@ Examples:
178
157
  jest-roblox -t "should spawn" Run tests matching pattern
179
158
  jest-roblox --json --outputFile results.json
180
159
  `;
181
- async function main() {
182
- const exitCode = await run(process.argv.slice(2));
183
- process.exit(exitCode);
184
- }
185
160
  function parseArgs(args) {
186
161
  const { positionals, values } = parseArgs$1({
187
162
  allowPositionals: true,
@@ -214,6 +189,10 @@ function parseArgs(args) {
214
189
  multiple: true,
215
190
  type: "string"
216
191
  },
192
+ "setupFilesAfterEnv": {
193
+ multiple: true,
194
+ type: "string"
195
+ },
217
196
  "showLuau": { type: "boolean" },
218
197
  "silent": { type: "boolean" },
219
198
  "sourceMap": { type: "boolean" },
@@ -223,6 +202,9 @@ function parseArgs(args) {
223
202
  },
224
203
  "testPathPattern": { type: "string" },
225
204
  "timeout": { type: "string" },
205
+ "typecheck": { type: "boolean" },
206
+ "typecheckOnly": { type: "boolean" },
207
+ "typecheckTsconfig": { type: "string" },
226
208
  "updateSnapshot": {
227
209
  short: "u",
228
210
  type: "boolean"
@@ -240,7 +222,7 @@ function parseArgs(args) {
240
222
  const port = values.port !== void 0 ? Number.parseInt(values.port, 10) : void 0;
241
223
  const timeout = values.timeout !== void 0 ? Number.parseInt(values.timeout, 10) : void 0;
242
224
  return {
243
- backend: values.backend,
225
+ backend: validateBackend(values.backend),
244
226
  cache: values["no-cache"] === true ? false : values.cache,
245
227
  color: values["no-color"] === true ? false : values.color,
246
228
  compact: values.compact,
@@ -256,12 +238,16 @@ function parseArgs(args) {
256
238
  projects: values.projects,
257
239
  rojoProject: values.rojoProject,
258
240
  setupFiles: values.setupFiles,
241
+ setupFilesAfterEnv: values.setupFilesAfterEnv,
259
242
  showLuau: values["no-show-luau"] === true ? false : values.showLuau,
260
243
  silent: values.silent,
261
244
  sourceMap: values.sourceMap,
262
245
  testNamePattern: values.testNamePattern,
263
246
  testPathPattern: values.testPathPattern,
264
247
  timeout,
248
+ typecheck: values.typecheckOnly === true ? true : values.typecheck,
249
+ typecheckOnly: values.typecheckOnly,
250
+ typecheckTsconfig: values.typecheckTsconfig,
265
251
  updateSnapshot: values.updateSnapshot,
266
252
  verbose: values.verbose,
267
253
  version: values.version
@@ -275,6 +261,10 @@ async function run(args) {
275
261
  return 2;
276
262
  }
277
263
  }
264
+ async function main() {
265
+ const exitCode = await run(process.argv.slice(2));
266
+ process.exit(exitCode);
267
+ }
278
268
  function printError(err) {
279
269
  if (err instanceof LuauScriptError) {
280
270
  const hint = getLuauErrorHint(err.message);
@@ -284,8 +274,33 @@ function printError(err) {
284
274
  } else if (err instanceof Error) console.error(`Error: ${err.message}`);
285
275
  else console.error("An unknown error occurred");
286
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
+ }
287
303
  async function runInner(args) {
288
- if (args[0] === "patch") return runPatch();
289
304
  const cli = parseArgs(args);
290
305
  if (cli.help === true) {
291
306
  console.log(HELP_TEXT);
@@ -301,32 +316,24 @@ async function runInner(args) {
301
316
  console.error("No test files found");
302
317
  return 2;
303
318
  }
304
- 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`);
305
- const result = await execute({
306
- backend: await resolveBackend(config),
307
- config,
308
- testFiles: discovery.files,
309
- version
310
- });
311
- if (result.output) console.log(result.output);
312
- if (config.outputFile !== void 0) await writeJsonFile(result.result, config.outputFile);
313
- writeGameOutputIfConfigured(config, result.gameOutput);
314
- return result.exitCode;
315
- }
316
- function writeGameOutputIfConfigured(config, gameOutput) {
317
- if (config.gameOutput === void 0) return;
318
- const entries = parseGameOutput(gameOutput);
319
- writeGameOutput(config.gameOutput, entries);
320
- if (!config.silent) {
321
- const notice = formatGameOutputNotice(config.gameOutput, entries.length);
322
- 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;
323
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);
324
330
  }
325
331
  const LUAU_ERROR_HINTS = [
326
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\""],
327
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."],
328
334
  [/Failed to find service/, "The first segment of jestPath must be a valid Roblox service name (e.g. ReplicatedStorage, ServerScriptService)."],
329
- [/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."]
330
337
  ];
331
338
  function discoverTestFiles(config, cliFiles) {
332
339
  if (cliFiles && cliFiles.length > 0) {
@@ -356,6 +363,14 @@ function discoverTestFiles(config, cliFiles) {
356
363
  totalFiles
357
364
  };
358
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
+ }
359
374
  function getLuauErrorHint(message) {
360
375
  for (const [pattern, hint] of LUAU_ERROR_HINTS) if (pattern.test(message)) return hint;
361
376
  }
@@ -375,24 +390,48 @@ function mergeCliWithConfig(cli, config) {
375
390
  projects: cli.projects ?? config.projects,
376
391
  rojoProject: cli.rojoProject ?? config.rojoProject,
377
392
  setupFiles: cli.setupFiles ?? config.setupFiles,
393
+ setupFilesAfterEnv: cli.setupFilesAfterEnv ?? config.setupFilesAfterEnv,
378
394
  showLuau: cli.showLuau ?? config.showLuau,
379
395
  silent: cli.silent ?? config.silent,
380
396
  sourceMap: cli.sourceMap ?? config.sourceMap,
381
397
  testNamePattern: cli.testNamePattern ?? config.testNamePattern,
382
398
  testPathPattern: cli.testPathPattern ?? config.testPathPattern,
383
399
  timeout: cli.timeout ?? config.timeout,
400
+ typecheck: cli.typecheck ?? config.typecheck,
401
+ typecheckOnly: cli.typecheckOnly ?? config.typecheckOnly,
402
+ typecheckTsconfig: cli.typecheckTsconfig ?? config.typecheckTsconfig,
384
403
  updateSnapshot: cli.updateSnapshot ?? config.updateSnapshot,
385
404
  verbose: cli.verbose ?? config.verbose
386
405
  };
387
406
  }
388
- function runPatch() {
389
- const result = patchGetDataModelService(process.cwd());
390
- if (result.status === "already-patched") console.log(`Already patched: ${result.filePath}`);
391
- else {
392
- console.log(`Patched: ${result.filePath}`);
393
- 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`);
394
426
  }
395
- 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`);
396
435
  }
397
436
 
398
437
  //#endregion