@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 +2 -2
- package/dist/cli.mjs +107 -75
- package/dist/{game-output--EpqHGEr.mjs → game-output-DO5fOpW3.mjs} +800 -316
- package/dist/index.d.mts +74 -30
- package/dist/index.mjs +2 -2
- package/dist/{schema-hhbEFVXy.d.mts → schema-ua9HqdbX.d.mts} +23 -3
- package/package.json +36 -6
- package/plugin/JestRobloxRunner.rbxm +0 -0
- package/plugin/out/shared/entry.luau +8 -0
- package/plugin/out/shared/instance-resolver.luau +93 -0
- package/plugin/out/shared/mock/CoreScriptSyncService.luau +19 -0
- package/plugin/out/shared/mock/FileSystemService.luau +30 -0
- package/plugin/out/shared/runner.luau +172 -0
- package/plugin/out/shared/snapshot-patch.luau +99 -0
- package/plugin/plugin.project.json +4 -1
- package/plugin/src/test-in-run-mode.server.luau +2 -15
- package/plugin/sourcemap.json +0 -1
- package/plugin/src/patch/CoreScriptSyncService.luau +0 -19
- package/plugin/src/patch/FileSystemService.luau +0 -27
- package/plugin/src/patch/getDataModelService.luau +0 -19
- package/plugin/src/test-runner.luau +0 -226
package/dist/cli.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as CliOptions } from "./schema-
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|