@isentinel/jest-roblox 0.0.1 → 0.0.2
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 +1 -1
- package/dist/cli.mjs +125 -7
- package/dist/{game-output-BKBGosEI.mjs → game-output-BDatGA9S.mjs} +93 -10
- package/dist/index.d.mts +13 -10
- package/dist/index.mjs +1 -1
- package/dist/{schema-ryuVGD35.d.mts → schema-CEsyg-FX.d.mts} +6 -3
- package/package.json +1 -1
- package/plugin/JestRobloxRunner.rbxm +0 -0
- package/plugin/src/init.server.luau +1 -1
- package/plugin/src/patch/CoreScriptSyncService.luau +19 -0
- package/plugin/src/patch/FileSystemService.luau +27 -0
- package/plugin/src/patch/getDataModelService.luau +19 -0
- package/plugin/src/test-in-run-mode.server.luau +13 -0
- package/plugin/src/test-runner.luau +72 -0
package/dist/cli.d.mts
CHANGED
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,115 @@
|
|
|
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-
|
|
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";
|
|
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";
|
|
5
|
+
import { WebSocketServer } from "ws";
|
|
5
6
|
import * as fs from "node:fs";
|
|
6
7
|
|
|
7
8
|
//#region package.json
|
|
8
|
-
var version = "0.0.
|
|
9
|
+
var version = "0.0.2";
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/backends/auto.ts
|
|
13
|
+
var StudioWithFallback = class {
|
|
14
|
+
studio;
|
|
15
|
+
constructor(studio) {
|
|
16
|
+
this.studio = studio;
|
|
17
|
+
}
|
|
18
|
+
async runTests(options) {
|
|
19
|
+
try {
|
|
20
|
+
return await this.studio.runTests(options);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (isStudioBusyError(err)) {
|
|
23
|
+
process.stderr.write("Studio busy, falling back to Open Cloud\n");
|
|
24
|
+
return createOpenCloudBackend().runTests(options);
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function isStudioBusyError(error) {
|
|
31
|
+
if (error instanceof LuauScriptError) return /previous call to start play session/i.test(error.message);
|
|
32
|
+
return error.code === "EADDRINUSE";
|
|
33
|
+
}
|
|
34
|
+
async function probeStudioPlugin(port, timeoutMs, createServer = (wsPort) => {
|
|
35
|
+
return new WebSocketServer({ port: wsPort });
|
|
36
|
+
}) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const wss = createServer(port);
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
wss.close();
|
|
41
|
+
resolve(false);
|
|
42
|
+
}, timeoutMs);
|
|
43
|
+
wss.on("connection", () => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
wss.close();
|
|
46
|
+
resolve(true);
|
|
47
|
+
});
|
|
48
|
+
wss.on("error", () => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
wss.close();
|
|
51
|
+
resolve(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function resolveBackend(config, probe = probeStudioPlugin) {
|
|
56
|
+
if (config.backend === "studio") return createStudioBackend({
|
|
57
|
+
port: config.port,
|
|
58
|
+
timeout: config.timeout
|
|
59
|
+
});
|
|
60
|
+
if (config.backend === "open-cloud") return createOpenCloudBackend();
|
|
61
|
+
if (await probe(config.port, 500)) {
|
|
62
|
+
process.stderr.write("Backend: studio (plugin detected)\n");
|
|
63
|
+
const studio = createStudioBackend({
|
|
64
|
+
port: config.port,
|
|
65
|
+
timeout: config.timeout
|
|
66
|
+
});
|
|
67
|
+
if (hasOpenCloudCredentials()) return new StudioWithFallback(studio);
|
|
68
|
+
return studio;
|
|
69
|
+
}
|
|
70
|
+
if (hasOpenCloudCredentials()) {
|
|
71
|
+
process.stderr.write("Backend: open-cloud (no plugin, using Open Cloud)\n");
|
|
72
|
+
return createOpenCloudBackend();
|
|
73
|
+
}
|
|
74
|
+
throw new Error("No backend available: Studio plugin not detected and Open Cloud env vars (ROBLOX_OPEN_CLOUD_API_KEY, ROBLOX_UNIVERSE_ID, ROBLOX_PLACE_ID) are missing");
|
|
75
|
+
}
|
|
76
|
+
function hasOpenCloudCredentials() {
|
|
77
|
+
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
|
+
}
|
|
79
|
+
|
|
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
|
+
}
|
|
9
113
|
|
|
10
114
|
//#endregion
|
|
11
115
|
//#region src/utils/glob.ts
|
|
@@ -38,7 +142,7 @@ const HELP_TEXT = `
|
|
|
38
142
|
Usage: jest-roblox [options] [files...]
|
|
39
143
|
|
|
40
144
|
Options:
|
|
41
|
-
--backend <type> Backend: "open-cloud" or "studio" (default:
|
|
145
|
+
--backend <type> Backend: "auto", "open-cloud", or "studio" (default: auto)
|
|
42
146
|
--port <number> WebSocket port for studio backend (default: 3001)
|
|
43
147
|
--config <path> Path to config file
|
|
44
148
|
--testPathPattern <regex> Filter test files by path pattern
|
|
@@ -53,6 +157,7 @@ Options:
|
|
|
53
157
|
--verbose Show individual test results
|
|
54
158
|
--silent Suppress output
|
|
55
159
|
--no-color Disable colored output
|
|
160
|
+
-u, --updateSnapshot Update snapshot files
|
|
56
161
|
--no-cache Force re-upload place file (skip cache)
|
|
57
162
|
--pollInterval <ms> Open Cloud poll interval in ms (default: 500)
|
|
58
163
|
--projects <path...> DataModel paths to search for tests
|
|
@@ -118,6 +223,10 @@ function parseArgs(args) {
|
|
|
118
223
|
},
|
|
119
224
|
"testPathPattern": { type: "string" },
|
|
120
225
|
"timeout": { type: "string" },
|
|
226
|
+
"updateSnapshot": {
|
|
227
|
+
short: "u",
|
|
228
|
+
type: "boolean"
|
|
229
|
+
},
|
|
121
230
|
"verbose": { type: "boolean" },
|
|
122
231
|
"version": {
|
|
123
232
|
default: false,
|
|
@@ -153,6 +262,7 @@ function parseArgs(args) {
|
|
|
153
262
|
testNamePattern: values.testNamePattern,
|
|
154
263
|
testPathPattern: values.testPathPattern,
|
|
155
264
|
timeout,
|
|
265
|
+
updateSnapshot: values.updateSnapshot,
|
|
156
266
|
verbose: values.verbose,
|
|
157
267
|
version: values.version
|
|
158
268
|
};
|
|
@@ -175,6 +285,7 @@ function printError(err) {
|
|
|
175
285
|
else console.error("An unknown error occurred");
|
|
176
286
|
}
|
|
177
287
|
async function runInner(args) {
|
|
288
|
+
if (args[0] === "patch") return runPatch();
|
|
178
289
|
const cli = parseArgs(args);
|
|
179
290
|
if (cli.help === true) {
|
|
180
291
|
console.log(HELP_TEXT);
|
|
@@ -192,10 +303,7 @@ async function runInner(args) {
|
|
|
192
303
|
}
|
|
193
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`);
|
|
194
305
|
const result = await execute({
|
|
195
|
-
backend:
|
|
196
|
-
port: config.port,
|
|
197
|
-
timeout: config.timeout
|
|
198
|
-
}) : createOpenCloudBackend(),
|
|
306
|
+
backend: await resolveBackend(config),
|
|
199
307
|
config,
|
|
200
308
|
testFiles: discovery.files,
|
|
201
309
|
version
|
|
@@ -273,9 +381,19 @@ function mergeCliWithConfig(cli, config) {
|
|
|
273
381
|
testNamePattern: cli.testNamePattern ?? config.testNamePattern,
|
|
274
382
|
testPathPattern: cli.testPathPattern ?? config.testPathPattern,
|
|
275
383
|
timeout: cli.timeout ?? config.timeout,
|
|
384
|
+
updateSnapshot: cli.updateSnapshot ?? config.updateSnapshot,
|
|
276
385
|
verbose: cli.verbose ?? config.verbose
|
|
277
386
|
};
|
|
278
387
|
}
|
|
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.");
|
|
394
|
+
}
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
279
397
|
|
|
280
398
|
//#endregion
|
|
281
399
|
export { main, parseArgs, run };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import * as path$1 from "node:path";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
5
|
+
import { type } from "arktype";
|
|
4
6
|
import * as fs from "node:fs";
|
|
5
7
|
import { existsSync } from "node:fs";
|
|
6
|
-
import { type } from "arktype";
|
|
7
8
|
import { homedir, tmpdir } from "node:os";
|
|
8
9
|
import * as crypto from "node:crypto";
|
|
9
10
|
import { randomUUID } from "node:crypto";
|
|
10
11
|
import buffer from "node:buffer";
|
|
11
|
-
import { WebSocketServer } from "ws";
|
|
12
12
|
import { createJiti } from "jiti";
|
|
13
13
|
import color from "tinyrainbow";
|
|
14
14
|
import hljs from "highlight.js/lib/core";
|
|
@@ -55,6 +55,7 @@ function parseJestOutput(output) {
|
|
|
55
55
|
if (trimmed.startsWith("{")) {
|
|
56
56
|
const parsed = JSON.parse(trimmed);
|
|
57
57
|
const luauTiming = extractLuauTiming(parsed);
|
|
58
|
+
const snapshotWrites = extractSnapshotWrites(parsed);
|
|
58
59
|
const unwrapped = unwrapResult(parsed);
|
|
59
60
|
if (unwrapped["kind"] === "ExecutionError") {
|
|
60
61
|
const errorMessage = extractExecutionError(unwrapped);
|
|
@@ -62,12 +63,14 @@ function parseJestOutput(output) {
|
|
|
62
63
|
}
|
|
63
64
|
if (unwrapped["results"] !== void 0 && typeof unwrapped["results"] === "object") return {
|
|
64
65
|
luauTiming,
|
|
65
|
-
result: validateJestResult(unwrapped["results"])
|
|
66
|
+
result: validateJestResult(unwrapped["results"]),
|
|
67
|
+
snapshotWrites
|
|
66
68
|
};
|
|
67
69
|
try {
|
|
68
70
|
return {
|
|
69
71
|
luauTiming,
|
|
70
|
-
result: validateJestResult(unwrapped)
|
|
72
|
+
result: validateJestResult(unwrapped),
|
|
73
|
+
snapshotWrites
|
|
71
74
|
};
|
|
72
75
|
} catch {}
|
|
73
76
|
}
|
|
@@ -95,6 +98,13 @@ function extractLuauTiming(parsed) {
|
|
|
95
98
|
for (const [key, value] of Object.entries(timing)) if (typeof value === "number") record[key] = value;
|
|
96
99
|
return Object.keys(record).length > 0 ? record : void 0;
|
|
97
100
|
}
|
|
101
|
+
function extractSnapshotWrites(parsed) {
|
|
102
|
+
const writes = parsed["_snapshotWrites"];
|
|
103
|
+
if (writes === void 0 || writes === null || typeof writes !== "object") return;
|
|
104
|
+
const record = {};
|
|
105
|
+
for (const [key, value] of Object.entries(writes)) if (typeof value === "string") record[key] = value;
|
|
106
|
+
return Object.keys(record).length > 0 ? record : void 0;
|
|
107
|
+
}
|
|
98
108
|
function isValidJson(text) {
|
|
99
109
|
try {
|
|
100
110
|
JSON.parse(text);
|
|
@@ -116,7 +126,7 @@ function validateJestResult(value) {
|
|
|
116
126
|
|
|
117
127
|
//#endregion
|
|
118
128
|
//#region src/test-runner.luau
|
|
119
|
-
var test_runner_default = "local HttpService = game:GetService(\"HttpService\")\nlocal LogService = game:GetService(\"LogService\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal t0 = os.clock()\nlocal baseConfig = HttpService:JSONDecode([[__CONFIG_JSON__]])\nlocal t_configDecode = os.clock()\n\ntype Config = {\n jestPath: string?,\n projects: { string }?,\n setupFiles: { string }?,\n _timing: boolean?,\n}\n\nlocal function fail(err: string)\n return {\n success = false,\n err = err,\n }\nend\n\nlocal config = table.clone(baseConfig) :: Config\nlocal timingEnabled = config._timing\n\nlocal function findInstance(path: string, class: string?): Instance\n local parts = string.split(path, \"/\")\n\n local result, current: Instance? = pcall(function()\n return game:FindService(parts[1])\n end)\n assert(result, `Failed to find service {parts[1]}: {current}`)\n\n for i = 2, #parts do\n assert(current, `Failed to find instance at {parts[i]}`)\n current = current:FindFirstChild(parts[i])\n end\n\n assert(current, `Failed to find instance at path {path}: {current}`)\n\n return current\nend\n\nlocal function getJest(): ModuleScript\n local jestPath = config.jestPath\n if jestPath then\n local instance = findInstance(jestPath)\n assert(instance, `Failed to find Jest instance at path ${jestPath}`)\n assert(instance:IsA(\"ModuleScript\"), `Instance at path ${jestPath} is not a ModuleScript`)\n return instance\n end\n\n local jestInstance = ReplicatedStorage:FindFirstChild(\"Jest\", true)\n assert(jestInstance, \"Failed to find Jest instance in ReplicatedStorage\")\n assert(\n jestInstance:IsA(\"ModuleScript\"),\n \"Jest instance in ReplicatedStorage is not a ModuleScript\"\n )\n return jestInstance\nend\n\nlocal t_findJest0 = os.clock()\nlocal findSuccess, findValue = pcall(getJest)\nlocal t_findJest = os.clock()\n\nif not findSuccess then\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(LogService:GetLogHistory())\n end)\n\n return HttpService:JSONEncode(fail(findValue :: any)), if logSuccess then logHistory else \"[]\"\nend\n\nlocal t_requireJest0 = os.clock()\nlocal Jest = (require :: any)(findValue)\nlocal t_requireJest = os.clock()\n\nlocal function runTests()\n
|
|
129
|
+
var test_runner_default = "local HttpService = game:GetService(\"HttpService\")\nlocal LogService = game:GetService(\"LogService\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal t0 = os.clock()\nlocal baseConfig = HttpService:JSONDecode([[__CONFIG_JSON__]])\nlocal t_configDecode = os.clock()\n\ntype Config = {\n jestPath: string?,\n projects: { string }?,\n setupFiles: { string }?,\n _timing: boolean?,\n}\n\nlocal function fail(err: string)\n return {\n success = false,\n err = err,\n }\nend\n\nlocal config = table.clone(baseConfig) :: Config\nlocal timingEnabled = config._timing\n\nlocal function findInstance(path: string, class: string?): Instance\n local parts = string.split(path, \"/\")\n\n local result, current: Instance? = pcall(function()\n return game:FindService(parts[1])\n end)\n assert(result, `Failed to find service {parts[1]}: {current}`)\n\n for i = 2, #parts do\n assert(current, `Failed to find instance at {parts[i]}`)\n current = current:FindFirstChild(parts[i])\n end\n\n assert(current, `Failed to find instance at path {path}: {current}`)\n\n return current\nend\n\nlocal function getJest(): ModuleScript\n local jestPath = config.jestPath\n if jestPath then\n local instance = findInstance(jestPath)\n assert(instance, `Failed to find Jest instance at path ${jestPath}`)\n assert(instance:IsA(\"ModuleScript\"), `Instance at path ${jestPath} is not a ModuleScript`)\n return instance\n end\n\n local jestInstance = ReplicatedStorage:FindFirstChild(\"Jest\", true)\n assert(jestInstance, \"Failed to find Jest instance in ReplicatedStorage\")\n assert(\n jestInstance:IsA(\"ModuleScript\"),\n \"Jest instance in ReplicatedStorage is not a ModuleScript\"\n )\n return jestInstance\nend\n\nlocal t_findJest0 = os.clock()\nlocal findSuccess, findValue = pcall(getJest)\nlocal t_findJest = os.clock()\n\nif not findSuccess then\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(LogService:GetLogHistory())\n end)\n\n return HttpService:JSONEncode(fail(findValue :: any)), if logSuccess then logHistory else \"[]\"\nend\n\nLogService:ClearOutput()\n\n-- Snapshot support: patch jest-roblox-shared before requiring Jest\nlocal snapshotWrites: { [string]: string } = {}\n\nlocal function getInstancePath(instance: Instance): string\n local parts = {} :: { string }\n local curr: Instance? = instance\n while curr and curr ~= game do\n table.insert(parts, 1, curr.Name)\n curr = curr.Parent\n end\n\n return table.concat(parts, \"/\")\nend\n\nlocal mockFileSystemService = {\n WriteFile = function(self: any, path: string, contents: string)\n -- Rewrite .snap.lua → .snap.luau\n snapshotWrites[string.gsub(path, \"%.snap%.lua$\", \".snap.luau\")] = contents\n end,\n CreateDirectories = function(self: any, _path: string) end,\n Exists = function(self: any, path: string): boolean\n return snapshotWrites[string.gsub(path, \"%.snap%.lua$\", \".snap.luau\")] ~= nil\n end,\n Remove = function(self: any, path: string)\n snapshotWrites[string.gsub(path, \"%.snap%.lua$\", \".snap.luau\")] = nil\n end,\n IsRegularFile = function(self: any, path: string): boolean\n return snapshotWrites[string.gsub(path, \"%.snap%.lua$\", \".snap.luau\")] ~= nil\n end,\n}\n\nlocal mockCoreScriptSyncService = {}\nfunction mockCoreScriptSyncService:GetScriptFilePath(script_: Instance): string\n return getInstancePath(script_)\nend\n\n-- Use _G so the mock survives across Jest's separate module loader context.\n-- Requires getDataModelService.lua to be patched (jest-roblox patch).\n_G.__mockGetDataModelService = function(service: string): any\n if service == \"FileSystemService\" then\n return mockFileSystemService\n elseif service == \"CoreScriptSyncService\" then\n return mockCoreScriptSyncService\n end\n\n local success, result = pcall(function()\n local svc = game:GetService(service)\n local _ = svc.Name\n return svc\n end)\n\n return if success then result else nil\nend\n\nlocal t_requireJest0 = os.clock()\nlocal Jest = (require :: any)(findValue)\nlocal t_requireJest = os.clock()\n\nlocal function runTests()\n local t_resolveProjects0 = os.clock()\n local projects = {}\n\n assert(\n config.projects and #config.projects > 0,\n \"No projects configured. Set 'projects' in jest.config.ts or pass --projects.\"\n )\n\n for _, projectPath in config.projects do\n table.insert(projects, findInstance(projectPath))\n end\n\n config.projects = {}\n local t_resolveProjects = os.clock()\n\n local t_resolveSetupFiles0 = os.clock()\n if config.setupFiles and #config.setupFiles > 0 then\n local resolved = {}\n\n for _, setupPath in config.setupFiles do\n table.insert(resolved, findInstance(setupPath))\n end\n\n config.setupFiles = resolved :: any\n end\n local t_resolveSetupFiles = os.clock()\n\n config._timing = nil :: any\n\n local t_jestRunCLI0 = os.clock()\n local jestResult = Jest.runCLI(script, config, projects):expect()\n local t_jestRunCLI = os.clock()\n\n local result: { [string]: any } = {\n success = true,\n value = jestResult,\n }\n\n if timingEnabled then\n result._timing = {\n configDecode = t_configDecode - t0,\n findJest = t_findJest - t_findJest0,\n requireJest = t_requireJest - t_requireJest0,\n resolveProjects = t_resolveProjects - t_resolveProjects0,\n resolveSetupFiles = t_resolveSetupFiles - t_resolveSetupFiles0,\n jestRunCLI = t_jestRunCLI - t_jestRunCLI0,\n }\n end\n\n if next(snapshotWrites) then\n result._snapshotWrites = snapshotWrites\n end\n\n return result\nend\n\nlocal runSuccess, runValue = pcall(runTests)\n\n_G.__mockGetDataModelService = nil\n\nlocal jestResult\nif not runSuccess then\n jestResult = HttpService:JSONEncode(fail(runValue :: any))\nelse\n jestResult = HttpService:JSONEncode(runValue)\nend\n\nlocal logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(LogService:GetLogHistory())\nend)\n\nreturn jestResult, if logSuccess then logHistory else \"[]\"\n";
|
|
120
130
|
|
|
121
131
|
//#endregion
|
|
122
132
|
//#region src/test-script.ts
|
|
@@ -265,6 +275,7 @@ var OpenCloudBackend = class {
|
|
|
265
275
|
gameOutput,
|
|
266
276
|
luauTiming: parsed.luauTiming,
|
|
267
277
|
result: parsed.result,
|
|
278
|
+
snapshotWrites: parsed.snapshotWrites,
|
|
268
279
|
timing: {
|
|
269
280
|
executionMs,
|
|
270
281
|
uploadCached,
|
|
@@ -381,6 +392,7 @@ var StudioBackend = class {
|
|
|
381
392
|
gameOutput: message.gameOutput,
|
|
382
393
|
luauTiming: parsed.luauTiming,
|
|
383
394
|
result: parsed.result,
|
|
395
|
+
snapshotWrites: parsed.snapshotWrites,
|
|
384
396
|
timing: { executionMs }
|
|
385
397
|
};
|
|
386
398
|
}
|
|
@@ -427,7 +439,7 @@ function createStudioBackend(options) {
|
|
|
427
439
|
//#endregion
|
|
428
440
|
//#region src/config/schema.ts
|
|
429
441
|
const DEFAULT_CONFIG = {
|
|
430
|
-
backend: "
|
|
442
|
+
backend: "auto",
|
|
431
443
|
cache: true,
|
|
432
444
|
color: true,
|
|
433
445
|
compact: false,
|
|
@@ -910,7 +922,7 @@ function formatTestSummary(result, timing, styles) {
|
|
|
910
922
|
const testParts = [];
|
|
911
923
|
if (result.numPassedTests > 0) testParts.push(st.summary.passed(`${result.numPassedTests} passed`));
|
|
912
924
|
if (result.numFailedTests > 0) testParts.push(st.summary.failed(`${result.numFailedTests} failed`));
|
|
913
|
-
if (result.numPendingTests > 0) testParts.push(st.summary.pending(`${result.numPendingTests}
|
|
925
|
+
if (result.numPendingTests > 0) testParts.push(st.summary.pending(`${result.numPendingTests} skipped`));
|
|
914
926
|
const testTotalLabel = st.dim(`(${result.numTotalTests})`);
|
|
915
927
|
const testsLabel = labelStyle(" Tests");
|
|
916
928
|
lines.push(`${testsLabel} ${testParts.join(" | ")} ${testTotalLabel}`);
|
|
@@ -932,8 +944,8 @@ function formatTestSummary(result, timing, styles) {
|
|
|
932
944
|
}
|
|
933
945
|
function parseErrorMessage(message) {
|
|
934
946
|
const lines = message.split("\n");
|
|
935
|
-
const expectedMatch = message.match(/Expected
|
|
936
|
-
const receivedMatch = message.match(/Received
|
|
947
|
+
const expectedMatch = message.match(/Expected\b.*?:\s*(.+)/);
|
|
948
|
+
const receivedMatch = message.match(/Received\b.*?:\s*(.+)/);
|
|
937
949
|
return {
|
|
938
950
|
expected: expectedMatch?.[1],
|
|
939
951
|
message: lines[0] ?? message,
|
|
@@ -1251,6 +1263,32 @@ async function writeJsonFile(result, filePath) {
|
|
|
1251
1263
|
fs.writeFileSync(absolutePath, formatJson(result), "utf8");
|
|
1252
1264
|
}
|
|
1253
1265
|
|
|
1266
|
+
//#endregion
|
|
1267
|
+
//#region src/snapshot/path-resolver.ts
|
|
1268
|
+
function createSnapshotPathResolver(config) {
|
|
1269
|
+
const mappings = buildMappings(config.rojoProject.tree, "");
|
|
1270
|
+
return { resolve(virtualPath) {
|
|
1271
|
+
const normalized = virtualPath.replaceAll("\\", "/");
|
|
1272
|
+
for (const [prefix, basePath] of mappings) {
|
|
1273
|
+
if (!normalized.startsWith(`${prefix}/`) && normalized !== prefix) continue;
|
|
1274
|
+
let result = `${basePath}/${normalized.slice(prefix.length + 1)}`;
|
|
1275
|
+
if (config.outDir !== void 0 && config.rootDir !== void 0 && result.startsWith(`${config.outDir}/`)) result = config.rootDir + result.slice(config.outDir.length);
|
|
1276
|
+
return result;
|
|
1277
|
+
}
|
|
1278
|
+
} };
|
|
1279
|
+
}
|
|
1280
|
+
function buildMappings(tree, prefix) {
|
|
1281
|
+
const mappings = [];
|
|
1282
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
1283
|
+
if (key.startsWith("$") || typeof value !== "object") continue;
|
|
1284
|
+
const dataModelPath = prefix ? `${prefix}/${key}` : key;
|
|
1285
|
+
if (value.$path !== void 0) mappings.push([dataModelPath, value.$path]);
|
|
1286
|
+
mappings.push(...buildMappings(value, dataModelPath));
|
|
1287
|
+
}
|
|
1288
|
+
mappings.sort((a, b) => b[0].length - a[0].length);
|
|
1289
|
+
return mappings;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1254
1292
|
//#endregion
|
|
1255
1293
|
//#region src/executor.ts
|
|
1256
1294
|
const rojoProjectSchema = type({ tree: "object" });
|
|
@@ -1260,10 +1298,11 @@ const tsconfigSchema = type({ "compilerOptions?": {
|
|
|
1260
1298
|
} });
|
|
1261
1299
|
async function execute(options) {
|
|
1262
1300
|
const startTime = Date.now();
|
|
1263
|
-
const { gameOutput, luauTiming, result, timing: backendTiming } = await options.backend.runTests({
|
|
1301
|
+
const { gameOutput, luauTiming, result, snapshotWrites, timing: backendTiming } = await options.backend.runTests({
|
|
1264
1302
|
config: options.config,
|
|
1265
1303
|
testFiles: options.testFiles
|
|
1266
1304
|
});
|
|
1305
|
+
if (snapshotWrites !== void 0) writeSnapshots(snapshotWrites, options.config);
|
|
1267
1306
|
const testsMs = calculateTestsMs(result.testResults);
|
|
1268
1307
|
const sourceMapper = options.config.sourceMap ? buildSourceMapper(options.config) : void 0;
|
|
1269
1308
|
const totalMs = Date.now() - startTime;
|
|
@@ -1336,6 +1375,50 @@ function printLuauTiming(timing) {
|
|
|
1336
1375
|
}
|
|
1337
1376
|
process.stderr.write(`[TIMING] total: ${String(total)}ms\n`);
|
|
1338
1377
|
}
|
|
1378
|
+
function writeSnapshots(snapshotWrites, config) {
|
|
1379
|
+
const rojoProjectPath = config.rojoProject ?? findRojoProject(config.rootDir);
|
|
1380
|
+
if (rojoProjectPath === void 0 || !fs.existsSync(rojoProjectPath)) {
|
|
1381
|
+
process.stderr.write("Warning: Cannot write snapshots - no rojo project found\n");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
try {
|
|
1385
|
+
const rojoResult = rojoProjectSchema(JSON.parse(fs.readFileSync(rojoProjectPath, "utf-8")));
|
|
1386
|
+
if (rojoResult instanceof type.errors) {
|
|
1387
|
+
process.stderr.write("Warning: Cannot write snapshots - invalid rojo project\n");
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const tsconfigPath = path$1.join(config.rootDir, "tsconfig.json");
|
|
1391
|
+
let outDirectory;
|
|
1392
|
+
let rootDirectory;
|
|
1393
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
1394
|
+
const tsconfigResult = tsconfigSchema(JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")));
|
|
1395
|
+
if (!(tsconfigResult instanceof type.errors)) {
|
|
1396
|
+
outDirectory = tsconfigResult.compilerOptions?.outDir ?? "out";
|
|
1397
|
+
rootDirectory = tsconfigResult.compilerOptions?.rootDir ?? "src";
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const resolver = createSnapshotPathResolver({
|
|
1401
|
+
outDir: outDirectory,
|
|
1402
|
+
rojoProject: rojoResult,
|
|
1403
|
+
rootDir: rootDirectory
|
|
1404
|
+
});
|
|
1405
|
+
let written = 0;
|
|
1406
|
+
for (const [virtualPath, content] of Object.entries(snapshotWrites)) {
|
|
1407
|
+
const fsPath = resolver.resolve(virtualPath);
|
|
1408
|
+
if (fsPath === void 0) {
|
|
1409
|
+
process.stderr.write(`Warning: Cannot resolve snapshot path: ${virtualPath}\n`);
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
const absolutePath = path$1.resolve(config.rootDir, fsPath);
|
|
1413
|
+
fs.mkdirSync(path$1.dirname(absolutePath), { recursive: true });
|
|
1414
|
+
fs.writeFileSync(absolutePath, content);
|
|
1415
|
+
written++;
|
|
1416
|
+
}
|
|
1417
|
+
if (written > 0) process.stderr.write(`Wrote ${String(written)} snapshot file${written === 1 ? "" : "s"}\n`);
|
|
1418
|
+
} catch (err) {
|
|
1419
|
+
process.stderr.write(`Warning: Failed to write snapshot files: ${String(err)}\n`);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1339
1422
|
|
|
1340
1423
|
//#endregion
|
|
1341
1424
|
//#region src/utils/game-output.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as Argv, i as ResolvedConfig, n as Config, r as DEFAULT_CONFIG, t as CliOptions } from "./schema-
|
|
2
|
-
import buffer from "node:buffer";
|
|
1
|
+
import { a as Argv, i as ResolvedConfig, n as Config, r as DEFAULT_CONFIG, t as CliOptions } from "./schema-CEsyg-FX.mjs";
|
|
3
2
|
import { WebSocketServer } from "ws";
|
|
3
|
+
import buffer from "node:buffer";
|
|
4
4
|
|
|
5
5
|
//#region src/types/jest-result.d.ts
|
|
6
6
|
interface JestResult {
|
|
@@ -29,6 +29,16 @@ interface TestFileResult {
|
|
|
29
29
|
}
|
|
30
30
|
type TestStatus = "failed" | "passed" | "pending" | "skipped";
|
|
31
31
|
//#endregion
|
|
32
|
+
//#region src/reporter/parser.d.ts
|
|
33
|
+
interface ParseResult {
|
|
34
|
+
luauTiming?: Record<string, number>;
|
|
35
|
+
result: JestResult;
|
|
36
|
+
snapshotWrites?: SnapshotWrites;
|
|
37
|
+
}
|
|
38
|
+
type SnapshotWrites = Record<string, string>;
|
|
39
|
+
declare function extractJsonFromOutput(output: string): string | undefined;
|
|
40
|
+
declare function parseJestOutput(output: string): ParseResult;
|
|
41
|
+
//#endregion
|
|
32
42
|
//#region src/backends/interface.d.ts
|
|
33
43
|
interface Backend {
|
|
34
44
|
runTests(options: BackendOptions): Promise<BackendResult>;
|
|
@@ -41,6 +51,7 @@ interface BackendResult {
|
|
|
41
51
|
gameOutput?: string;
|
|
42
52
|
luauTiming?: Record<string, number>;
|
|
43
53
|
result: JestResult;
|
|
54
|
+
snapshotWrites?: SnapshotWrites;
|
|
44
55
|
timing: BackendTiming;
|
|
45
56
|
}
|
|
46
57
|
interface BackendTiming {
|
|
@@ -213,14 +224,6 @@ declare function formatTestSummary(result: JestResult, timing: TimingResult, sty
|
|
|
213
224
|
declare function formatJson(result: JestResult): string;
|
|
214
225
|
declare function writeJsonFile(result: JestResult, filePath: string): Promise<void>;
|
|
215
226
|
//#endregion
|
|
216
|
-
//#region src/reporter/parser.d.ts
|
|
217
|
-
interface ParseResult {
|
|
218
|
-
luauTiming?: Record<string, number>;
|
|
219
|
-
result: JestResult;
|
|
220
|
-
}
|
|
221
|
-
declare function extractJsonFromOutput(output: string): string | undefined;
|
|
222
|
-
declare function parseJestOutput(output: string): ParseResult;
|
|
223
|
-
//#endregion
|
|
224
227
|
//#region src/test-script.d.ts
|
|
225
228
|
type JestArgv = Argv & {
|
|
226
229
|
testMatch: Array<string>;
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { _ as buildJestArgv, a as formatJson, b as extractJsonFromOutput, c as formatResult, d as resolveConfig, f as DEFAULT_CONFIG, g as createOpenCloudBackend, h as OpenCloudBackend, i as execute, l as formatTestSummary, m as createStudioBackend, n as parseGameOutput, o as writeJsonFile, p as StudioBackend, r as writeGameOutput, s as formatFailure, t as formatGameOutputNotice, u as loadConfig, v as generateTestScript, x as parseJestOutput } from "./game-output-
|
|
1
|
+
import { _ as buildJestArgv, a as formatJson, b as extractJsonFromOutput, c as formatResult, d as resolveConfig, f as DEFAULT_CONFIG, g as createOpenCloudBackend, h as OpenCloudBackend, i as execute, l as formatTestSummary, m as createStudioBackend, n as parseGameOutput, o as writeJsonFile, p as StudioBackend, r as writeGameOutput, s as formatFailure, t as formatGameOutputNotice, u as loadConfig, v as generateTestScript, x as parseJestOutput } from "./game-output-BDatGA9S.mjs";
|
|
2
2
|
|
|
3
3
|
export { DEFAULT_CONFIG, OpenCloudBackend, StudioBackend, buildJestArgv, createOpenCloudBackend, createStudioBackend, execute, extractJsonFromOutput, formatFailure, formatGameOutputNotice, formatJson, formatResult, formatTestSummary, generateTestScript, loadConfig, parseGameOutput, parseJestOutput, resolveConfig, writeGameOutput, writeJsonFile };
|
|
@@ -758,7 +758,7 @@ type _Except<ObjectType, KeysType extends keyof ObjectType, Options extends Requ
|
|
|
758
758
|
//#endregion
|
|
759
759
|
//#region src/config/schema.d.ts
|
|
760
760
|
interface Config extends Except<Argv, "rootDir" | "setupFiles" | "testPathPattern"> {
|
|
761
|
-
backend?:
|
|
761
|
+
backend?: Backend;
|
|
762
762
|
cache?: boolean;
|
|
763
763
|
compact?: boolean;
|
|
764
764
|
compactMaxFailures?: number;
|
|
@@ -774,9 +774,10 @@ interface Config extends Except<Argv, "rootDir" | "setupFiles" | "testPathPatter
|
|
|
774
774
|
sourceMap?: boolean;
|
|
775
775
|
testPathPattern?: string;
|
|
776
776
|
timeout?: number;
|
|
777
|
+
updateSnapshot?: boolean;
|
|
777
778
|
}
|
|
778
779
|
interface ResolvedConfig extends Config {
|
|
779
|
-
backend: "open-cloud" | "studio";
|
|
780
|
+
backend: "auto" | "open-cloud" | "studio";
|
|
780
781
|
cache: boolean;
|
|
781
782
|
color: boolean;
|
|
782
783
|
compact: boolean;
|
|
@@ -795,9 +796,10 @@ interface ResolvedConfig extends Config {
|
|
|
795
796
|
timeout: number;
|
|
796
797
|
verbose: boolean;
|
|
797
798
|
}
|
|
799
|
+
type Backend = "auto" | "open-cloud" | "studio";
|
|
798
800
|
declare const DEFAULT_CONFIG: ResolvedConfig;
|
|
799
801
|
interface CliOptions {
|
|
800
|
-
backend?:
|
|
802
|
+
backend?: Backend;
|
|
801
803
|
cache?: boolean;
|
|
802
804
|
color?: boolean;
|
|
803
805
|
compact?: boolean;
|
|
@@ -819,6 +821,7 @@ interface CliOptions {
|
|
|
819
821
|
testNamePattern?: string;
|
|
820
822
|
testPathPattern?: string;
|
|
821
823
|
timeout?: number;
|
|
824
|
+
updateSnapshot?: boolean;
|
|
822
825
|
verbose?: boolean;
|
|
823
826
|
version?: boolean;
|
|
824
827
|
}
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -9,7 +9,7 @@ if RunService:IsRunning() then
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
local WEBSOCKET_URL = "ws://localhost:3001"
|
|
12
|
-
local RECONNECT_DELAY =
|
|
12
|
+
local RECONNECT_DELAY = 0.1
|
|
13
13
|
|
|
14
14
|
local IsDebug = ReplicatedStorage:GetAttribute("JEST_ROBLOX_DEBUG") == true
|
|
15
15
|
ReplicatedStorage:GetAttributeChangedSignal("JEST_ROBLOX_DEBUG"):Connect(function(): ()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Mock CoreScriptSyncService: builds instance-to-path mapping
|
|
2
|
+
-- Uses custom getInstancePath that stops at game (excludes DataModel name)
|
|
3
|
+
local function getInstancePath(instance: Instance): string
|
|
4
|
+
local parts = {}
|
|
5
|
+
local curr: Instance? = instance
|
|
6
|
+
while curr and curr ~= game do
|
|
7
|
+
table.insert(parts, 1, (curr :: Instance).Name)
|
|
8
|
+
curr = (curr :: Instance).Parent
|
|
9
|
+
end
|
|
10
|
+
return table.concat(parts, "/")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
local CoreScriptSyncService = {}
|
|
14
|
+
|
|
15
|
+
function CoreScriptSyncService:GetScriptFilePath(script_: Instance): string
|
|
16
|
+
return getInstancePath(script_)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
return CoreScriptSyncService
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Mock FileSystemService: collects snapshot writes in-memory Uses
|
|
2
|
+
-- _G.__snapshotWrites so all module load contexts (standard Luau + Jest
|
|
3
|
+
-- runtime) share the same table. The test-runner creates this before patching.
|
|
4
|
+
local snapshotWrites: { [string]: string } = _G.__snapshotWrites or {}
|
|
5
|
+
|
|
6
|
+
local FileSystemService = {}
|
|
7
|
+
|
|
8
|
+
function FileSystemService:WriteFile(path: string, contents: string)
|
|
9
|
+
-- Rewrite .snap.lua -> .snap.luau for Rojo compatibility
|
|
10
|
+
snapshotWrites[string.gsub(path, "%.snap%.lua$", ".snap.luau")] = contents
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
function FileSystemService:CreateDirectories(_path: string) end
|
|
14
|
+
|
|
15
|
+
function FileSystemService:Exists(path: string): boolean
|
|
16
|
+
return snapshotWrites[string.gsub(path, "%.snap%.lua$", ".snap.luau")] ~= nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
function FileSystemService:Remove(path: string)
|
|
20
|
+
snapshotWrites[string.gsub(path, "%.snap%.lua$", ".snap.luau")] = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
function FileSystemService:IsRegularFile(path: string): boolean
|
|
24
|
+
return snapshotWrites[string.gsub(path, "%.snap%.lua$", ".snap.luau")] ~= nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
return FileSystemService
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Patched getDataModelService: returns mock services for snapshot support
|
|
2
|
+
local CoreScriptSyncService = require(script.Parent.CoreScriptSyncService)
|
|
3
|
+
local FileSystemService = require(script.Parent.FileSystemService)
|
|
4
|
+
|
|
5
|
+
return function(service: string)
|
|
6
|
+
if service == "FileSystemService" then
|
|
7
|
+
return FileSystemService
|
|
8
|
+
elseif service == "CoreScriptSyncService" then
|
|
9
|
+
return CoreScriptSyncService
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
local success, result = pcall(function()
|
|
13
|
+
local svc = game:GetService(service)
|
|
14
|
+
local _ = svc.Name
|
|
15
|
+
return svc
|
|
16
|
+
end)
|
|
17
|
+
|
|
18
|
+
return success and result or nil
|
|
19
|
+
end
|
|
@@ -30,6 +30,19 @@ end
|
|
|
30
30
|
local TestRunner = require(script.Parent["test-runner"])
|
|
31
31
|
local jestOutput, gameOutput = TestRunner.runTestsAync(script, testArgs.config or {})
|
|
32
32
|
|
|
33
|
+
-- Inject snapshot writes from shared global table into jestOutput
|
|
34
|
+
local snapshotWrites = _G.__snapshotWrites
|
|
35
|
+
if snapshotWrites and next(snapshotWrites) then
|
|
36
|
+
local ok, decoded = pcall(function()
|
|
37
|
+
return HttpService:JSONDecode(jestOutput)
|
|
38
|
+
end)
|
|
39
|
+
|
|
40
|
+
if ok and type(decoded) == "table" then
|
|
41
|
+
decoded._snapshotWrites = snapshotWrites
|
|
42
|
+
jestOutput = HttpService:JSONEncode(decoded)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
33
46
|
StudioTestService:EndTest({
|
|
34
47
|
jestOutput = jestOutput,
|
|
35
48
|
gameOutput = gameOutput,
|
|
@@ -41,6 +41,66 @@ function module.getJest(config: { jestPath: string? }): ModuleScript
|
|
|
41
41
|
return jestInstance
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
-- Find RobloxShared relative to Jest module
|
|
45
|
+
local function findRobloxShared(jestModule: ModuleScript): Instance?
|
|
46
|
+
local parent = jestModule.Parent
|
|
47
|
+
if parent then
|
|
48
|
+
local found = parent:FindFirstChild("RobloxShared")
|
|
49
|
+
or parent:FindFirstChild("jest-roblox-shared")
|
|
50
|
+
if found then
|
|
51
|
+
return found
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if parent.Parent then
|
|
55
|
+
found = parent.Parent:FindFirstChild("RobloxShared")
|
|
56
|
+
or parent.Parent:FindFirstChild("jest-roblox-shared")
|
|
57
|
+
if found then
|
|
58
|
+
return found
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
return game:FindFirstChild("RobloxShared", true)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
-- Replace a child ModuleScript inside RobloxShared with a patched clone
|
|
67
|
+
local function replaceChild(parent: Instance, name: string, patchSource: ModuleScript)
|
|
68
|
+
local original = parent:FindFirstChild(name)
|
|
69
|
+
if original then
|
|
70
|
+
original:Destroy()
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
local clone = patchSource:Clone()
|
|
74
|
+
clone.Name = name
|
|
75
|
+
clone.Parent = parent
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
-- Patch RobloxShared by replacing ModuleScript children
|
|
79
|
+
local function patchRobloxShared(jestModule: ModuleScript, patchFolder: Instance): boolean
|
|
80
|
+
local robloxShared = findRobloxShared(jestModule)
|
|
81
|
+
if not robloxShared then
|
|
82
|
+
warn("[jest-roblox-cli] Could not find RobloxShared; snapshot support disabled")
|
|
83
|
+
return false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
local patchGetDataModelService = patchFolder:FindFirstChild("getDataModelService")
|
|
87
|
+
local patchFileSystemService = patchFolder:FindFirstChild("FileSystemService")
|
|
88
|
+
local patchCoreScriptSyncService = patchFolder:FindFirstChild("CoreScriptSyncService")
|
|
89
|
+
|
|
90
|
+
if not (patchGetDataModelService and patchFileSystemService and patchCoreScriptSyncService) then
|
|
91
|
+
warn("[jest-roblox-cli] Missing patch modules; snapshot support disabled")
|
|
92
|
+
return false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
-- Replace children inside RobloxShared
|
|
96
|
+
replaceChild(robloxShared, "FileSystemService", patchFileSystemService :: ModuleScript)
|
|
97
|
+
replaceChild(robloxShared, "CoreScriptSyncService", patchCoreScriptSyncService :: ModuleScript)
|
|
98
|
+
replaceChild(robloxShared, "getDataModelService", patchGetDataModelService :: ModuleScript)
|
|
99
|
+
|
|
100
|
+
warn("[jest-roblox-cli] Patched RobloxShared at:", robloxShared:GetFullName())
|
|
101
|
+
return true
|
|
102
|
+
end
|
|
103
|
+
|
|
44
104
|
function module.runTestsAync(
|
|
45
105
|
callingScript: LuaSourceContainer,
|
|
46
106
|
config: {
|
|
@@ -66,6 +126,17 @@ function module.runTestsAync(
|
|
|
66
126
|
if logSuccess then logHistory else "[]"
|
|
67
127
|
end
|
|
68
128
|
|
|
129
|
+
-- Snapshot support: create shared table BEFORE patching so all module
|
|
130
|
+
-- load contexts (standard Luau + Jest runtime) reference the same table
|
|
131
|
+
_G.__snapshotWrites = {}
|
|
132
|
+
|
|
133
|
+
local t_patchSnapshot0 = os.clock()
|
|
134
|
+
local patchFolder = (callingScript :: any).Parent.patch
|
|
135
|
+
if patchFolder then
|
|
136
|
+
patchRobloxShared(findValue, patchFolder)
|
|
137
|
+
end
|
|
138
|
+
local t_patchSnapshot = os.clock()
|
|
139
|
+
|
|
69
140
|
local t_requireJest0 = os.clock()
|
|
70
141
|
local Jest = (require :: any)(findValue)
|
|
71
142
|
local t_requireJest = os.clock()
|
|
@@ -115,6 +186,7 @@ function module.runTestsAync(
|
|
|
115
186
|
result._timing = {
|
|
116
187
|
configDecode = 0,
|
|
117
188
|
findJest = t_findJest - t_findJest0,
|
|
189
|
+
patchSnapshot = t_patchSnapshot - t_patchSnapshot0,
|
|
118
190
|
requireJest = t_requireJest - t_requireJest0,
|
|
119
191
|
resolveProjects = t_resolveProjects - t_resolveProjects0,
|
|
120
192
|
resolveSetupFiles = t_resolveSetupFiles - t_resolveSetupFiles0,
|