@isentinel/jest-roblox 0.0.1 → 0.0.3

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,4 +1,4 @@
1
- import { t as CliOptions } from "./schema-ryuVGD35.mjs";
1
+ import { t as CliOptions } from "./schema-hhbEFVXy.mjs";
2
2
 
3
3
  //#region src/cli.d.ts
4
4
  declare function main(): Promise<void>;
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-BKBGosEI.mjs";
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";
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.1";
9
+ var version = "0.0.3";
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: open-cloud)
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,10 +157,12 @@ 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
59
164
  --setupFiles <path...> DataModel paths to setup scripts
165
+ --setupFilesAfterEnv <path...> DataModel paths to post-env setup scripts
60
166
  --no-show-luau Hide Luau code in failure output
61
167
  --help Show this help message
62
168
  --version Show version number
@@ -109,6 +215,10 @@ function parseArgs(args) {
109
215
  multiple: true,
110
216
  type: "string"
111
217
  },
218
+ "setupFilesAfterEnv": {
219
+ multiple: true,
220
+ type: "string"
221
+ },
112
222
  "showLuau": { type: "boolean" },
113
223
  "silent": { type: "boolean" },
114
224
  "sourceMap": { type: "boolean" },
@@ -118,6 +228,10 @@ function parseArgs(args) {
118
228
  },
119
229
  "testPathPattern": { type: "string" },
120
230
  "timeout": { type: "string" },
231
+ "updateSnapshot": {
232
+ short: "u",
233
+ type: "boolean"
234
+ },
121
235
  "verbose": { type: "boolean" },
122
236
  "version": {
123
237
  default: false,
@@ -147,12 +261,14 @@ function parseArgs(args) {
147
261
  projects: values.projects,
148
262
  rojoProject: values.rojoProject,
149
263
  setupFiles: values.setupFiles,
264
+ setupFilesAfterEnv: values.setupFilesAfterEnv,
150
265
  showLuau: values["no-show-luau"] === true ? false : values.showLuau,
151
266
  silent: values.silent,
152
267
  sourceMap: values.sourceMap,
153
268
  testNamePattern: values.testNamePattern,
154
269
  testPathPattern: values.testPathPattern,
155
270
  timeout,
271
+ updateSnapshot: values.updateSnapshot,
156
272
  verbose: values.verbose,
157
273
  version: values.version
158
274
  };
@@ -175,6 +291,7 @@ function printError(err) {
175
291
  else console.error("An unknown error occurred");
176
292
  }
177
293
  async function runInner(args) {
294
+ if (args[0] === "patch") return runPatch();
178
295
  const cli = parseArgs(args);
179
296
  if (cli.help === true) {
180
297
  console.log(HELP_TEXT);
@@ -192,10 +309,7 @@ async function runInner(args) {
192
309
  }
193
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`);
194
311
  const result = await execute({
195
- backend: config.backend === "studio" ? createStudioBackend({
196
- port: config.port,
197
- timeout: config.timeout
198
- }) : createOpenCloudBackend(),
312
+ backend: await resolveBackend(config),
199
313
  config,
200
314
  testFiles: discovery.files,
201
315
  version
@@ -267,15 +381,26 @@ function mergeCliWithConfig(cli, config) {
267
381
  projects: cli.projects ?? config.projects,
268
382
  rojoProject: cli.rojoProject ?? config.rojoProject,
269
383
  setupFiles: cli.setupFiles ?? config.setupFiles,
384
+ setupFilesAfterEnv: cli.setupFilesAfterEnv ?? config.setupFilesAfterEnv,
270
385
  showLuau: cli.showLuau ?? config.showLuau,
271
386
  silent: cli.silent ?? config.silent,
272
387
  sourceMap: cli.sourceMap ?? config.sourceMap,
273
388
  testNamePattern: cli.testNamePattern ?? config.testNamePattern,
274
389
  testPathPattern: cli.testPathPattern ?? config.testPathPattern,
275
390
  timeout: cli.timeout ?? config.timeout,
391
+ updateSnapshot: cli.updateSnapshot ?? config.updateSnapshot,
276
392
  verbose: cli.verbose ?? config.verbose
277
393
  };
278
394
  }
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.");
401
+ }
402
+ return 0;
403
+ }
279
404
 
280
405
  //#endregion
281
406
  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 LogService:ClearOutput()\n\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 return result\nend\n\nlocal runSuccess, runValue = pcall(runTests)\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";
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 setupFilesAfterEnv: { 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 if config.setupFilesAfterEnv and #config.setupFilesAfterEnv > 0 then\n local resolved = {}\n\n for _, setupPath in config.setupFilesAfterEnv do\n table.insert(resolved, findInstance(setupPath))\n end\n\n config.setupFilesAfterEnv = 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: "open-cloud",
442
+ backend: "auto",
431
443
  cache: true,
432
444
  color: true,
433
445
  compact: false,
@@ -491,7 +503,7 @@ function findExpectationColumn(lineText) {
491
503
  const expectIndex = lineText.search(/\bexpect\s*\(/);
492
504
  if (expectIndex === -1) return 1;
493
505
  const afterExpect = lineText.slice(expectIndex);
494
- const matcherRegex = /\.\s*([A-Za-z_$][\w$]*)\s*(?=\()/g;
506
+ const matcherRegex = /[.:]\s*([A-Za-z_$][\w$]*)\s*(?=\()/g;
495
507
  let lastMatcher = null;
496
508
  for (const match of afterExpect.matchAll(matcherRegex)) lastMatcher = match;
497
509
  const matcherName = lastMatcher?.[1];
@@ -503,12 +515,12 @@ function findExpectationColumn(lineText) {
503
515
  //#region src/source-mapper/line-mapper.ts
504
516
  function mapLine(luauLineNumber, luauLines, tsLines) {
505
517
  const luauLine = normalize(luauLines[luauLineNumber - 1]);
506
- if (luauLine === void 0) return;
518
+ if (luauLine === void 0 || luauLine === "") return;
507
519
  for (const [index, tsLine] of tsLines.entries()) if (normalize(tsLine) === luauLine) return index + 1;
508
520
  const stripped = luauLine.replace(/\s/g, "");
509
521
  for (const [index, tsLine] of tsLines.entries()) {
510
522
  const tsNormalized = normalize(tsLine);
511
- if (tsNormalized === void 0) continue;
523
+ if (tsNormalized === void 0 || tsNormalized === "") continue;
512
524
  const tsStripped = tsNormalized.replace(/\s/g, "");
513
525
  if (tsStripped.includes(stripped) || stripped.includes(tsStripped)) return index + 1;
514
526
  }
@@ -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} pending`));
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:\s*(.+)/);
936
- const receivedMatch = message.match(/Received:\s*(.+)/);
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-ryuVGD35.mjs";
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-hhbEFVXy.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-BKBGosEI.mjs";
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--EpqHGEr.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 };
@@ -757,8 +757,8 @@ type Except<ObjectType, KeysType extends keyof ObjectType, Options extends Excep
757
757
  type _Except<ObjectType, KeysType extends keyof ObjectType, Options extends Required<ExceptOptions>> = { [KeyType in keyof ObjectType as Filter<KeyType, KeysType>]: ObjectType[KeyType] } & (Options['requireExactProps'] extends true ? Partial<Record<KeysType, never>> : {});
758
758
  //#endregion
759
759
  //#region src/config/schema.d.ts
760
- interface Config extends Except<Argv, "rootDir" | "setupFiles" | "testPathPattern"> {
761
- backend?: "open-cloud" | "studio";
760
+ interface Config extends Except<Argv, "rootDir" | "setupFiles" | "setupFilesAfterEnv" | "testPathPattern"> {
761
+ backend?: Backend;
762
762
  cache?: boolean;
763
763
  compact?: boolean;
764
764
  compactMaxFailures?: number;
@@ -770,13 +770,15 @@ interface Config extends Except<Argv, "rootDir" | "setupFiles" | "testPathPatter
770
770
  rojoProject?: string;
771
771
  rootDir?: string;
772
772
  setupFiles?: Array<string>;
773
+ setupFilesAfterEnv?: Array<string>;
773
774
  showLuau?: boolean;
774
775
  sourceMap?: boolean;
775
776
  testPathPattern?: string;
776
777
  timeout?: number;
778
+ updateSnapshot?: boolean;
777
779
  }
778
780
  interface ResolvedConfig extends Config {
779
- backend: "open-cloud" | "studio";
781
+ backend: "auto" | "open-cloud" | "studio";
780
782
  cache: boolean;
781
783
  color: boolean;
782
784
  compact: boolean;
@@ -795,9 +797,10 @@ interface ResolvedConfig extends Config {
795
797
  timeout: number;
796
798
  verbose: boolean;
797
799
  }
800
+ type Backend = "auto" | "open-cloud" | "studio";
798
801
  declare const DEFAULT_CONFIG: ResolvedConfig;
799
802
  interface CliOptions {
800
- backend?: "open-cloud" | "studio";
803
+ backend?: Backend;
801
804
  cache?: boolean;
802
805
  color?: boolean;
803
806
  compact?: boolean;
@@ -813,12 +816,14 @@ interface CliOptions {
813
816
  projects?: Array<string>;
814
817
  rojoProject?: string;
815
818
  setupFiles?: Array<string>;
819
+ setupFilesAfterEnv?: Array<string>;
816
820
  showLuau?: boolean;
817
821
  silent?: boolean;
818
822
  sourceMap?: boolean;
819
823
  testNamePattern?: string;
820
824
  testPathPattern?: string;
821
825
  timeout?: number;
826
+ updateSnapshot?: boolean;
822
827
  verbose?: boolean;
823
828
  version?: boolean;
824
829
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isentinel/jest-roblox",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Jest-compatible CLI for running roblox-ts tests via Roblox Open Cloud",
5
5
  "keywords": [
6
6
  "jest",
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 = 5
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()
@@ -98,6 +169,15 @@ function module.runTestsAync(
98
169
 
99
170
  config.setupFiles = resolved
100
171
  end
172
+ if config.setupFilesAfterEnv and #config.setupFilesAfterEnv > 0 then
173
+ local resolved = {}
174
+
175
+ for _, setupPath in config.setupFilesAfterEnv do
176
+ table.insert(resolved, module.findInstance(setupPath))
177
+ end
178
+
179
+ config.setupFilesAfterEnv = resolved
180
+ end
101
181
  local t_resolveSetupFiles = os.clock()
102
182
 
103
183
  config._timing = nil
@@ -115,6 +195,7 @@ function module.runTestsAync(
115
195
  result._timing = {
116
196
  configDecode = 0,
117
197
  findJest = t_findJest - t_findJest0,
198
+ patchSnapshot = t_patchSnapshot - t_patchSnapshot0,
118
199
  requireJest = t_requireJest - t_requireJest0,
119
200
  resolveProjects = t_resolveProjects - t_resolveProjects0,
120
201
  resolveSetupFiles = t_resolveSetupFiles - t_resolveSetupFiles0,