@isentinel/jest-roblox 0.0.8 → 0.1.1
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/LICENSE.md +21 -0
- package/README.md +131 -130
- package/bin/jest-roblox.js +14 -2
- package/dist/cli.d.mts +4 -2
- package/dist/cli.mjs +920 -183
- package/dist/{schema-DcDQmTyn.d.mts → executor-DqZE3wME.d.mts} +236 -31
- package/dist/{game-output-M8du29nj.mjs → game-output-C0_-YIAY.mjs} +1095 -468
- package/dist/index.d.mts +6 -145
- package/dist/index.mjs +2 -3
- package/dist/sea/jest-roblox +0 -0
- package/dist/sea-entry.cjs +61580 -0
- package/package.json +17 -42
- package/plugin/JestRobloxRunner.rbxm +0 -0
- package/plugin/out/shared/instance-resolver.luau +3 -8
- package/plugin/out/shared/runner.luau +62 -13
- package/plugin/out/shared/snapshot-patch.luau +2 -7
|
@@ -1,27 +1,30 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { type } from "arktype";
|
|
1
3
|
import assert from "node:assert";
|
|
2
|
-
import * as fs from "node:fs";
|
|
4
|
+
import * as fs$1 from "node:fs";
|
|
3
5
|
import { existsSync } from "node:fs";
|
|
4
6
|
import * as path$1 from "node:path";
|
|
5
7
|
import path from "node:path";
|
|
6
8
|
import process from "node:process";
|
|
7
9
|
import color from "tinyrainbow";
|
|
8
10
|
import { WebSocketServer } from "ws";
|
|
9
|
-
import {
|
|
11
|
+
import { createDefineConfig, loadConfig } from "c12";
|
|
10
12
|
import { homedir, tmpdir } from "node:os";
|
|
11
13
|
import * as crypto from "node:crypto";
|
|
12
14
|
import { randomUUID } from "node:crypto";
|
|
13
15
|
import buffer from "node:buffer";
|
|
14
|
-
import {
|
|
16
|
+
import { defuFn } from "defu";
|
|
15
17
|
import { TraceMap, originalPositionFor, sourceContentFor } from "@jridgewell/trace-mapping";
|
|
16
18
|
import { getTsconfig } from "get-tsconfig";
|
|
17
|
-
import {
|
|
19
|
+
import { execFileSync } from "node:child_process";
|
|
18
20
|
import hljs from "highlight.js/lib/core";
|
|
19
21
|
import typescript from "highlight.js/lib/languages/typescript";
|
|
22
|
+
import { parseJSONC } from "confbox";
|
|
20
23
|
import { Visitor, parseSync } from "oxc-parser";
|
|
21
|
-
|
|
22
24
|
//#region src/reporter/parser.ts
|
|
23
25
|
const TASK_SCRIPT_PREFIX = /^TaskScript:\d+:\s*/;
|
|
24
26
|
var LuauScriptError = class extends Error {
|
|
27
|
+
gameOutput;
|
|
25
28
|
constructor(rawMessage) {
|
|
26
29
|
super(rawMessage.replace(TASK_SCRIPT_PREFIX, ""));
|
|
27
30
|
}
|
|
@@ -175,6 +178,7 @@ function extractSnapshotWrites(parsed) {
|
|
|
175
178
|
function stringifyError(err) {
|
|
176
179
|
if (typeof err === "string") return err;
|
|
177
180
|
if (typeof err === "object" && err !== null && "message" in err && typeof err.message === "string") return err.message;
|
|
181
|
+
if (typeof err === "object" && err !== null && "kind" in err && err["kind"] === "ExecutionError") return extractExecutionError(err);
|
|
178
182
|
const serialized = JSON.stringify(err);
|
|
179
183
|
assert(serialized !== void 0, "JSON-parsed values are always serializable");
|
|
180
184
|
return serialized;
|
|
@@ -189,26 +193,21 @@ function validateJestResult(value) {
|
|
|
189
193
|
if (result instanceof type.errors) throw new Error(`Invalid Jest result: ${result.summary}`);
|
|
190
194
|
return result;
|
|
191
195
|
}
|
|
192
|
-
|
|
193
196
|
//#endregion
|
|
194
|
-
//#region src/
|
|
195
|
-
var test_runner_bundled_default = "type PatchState__DARKLUA_TYPE_a = {\n robloxSharedExports: any,\n originalGetDataModelService: any,\n Runtime: any,\n originalRequireInternalModule: any,\n}\n\ntype Config__DARKLUA_TYPE_b = {\n jestPath: string?,\n projects: { string }?,\n setupFiles: { string }?,\n setupFilesAfterEnv: { string }?,\n _coverage: boolean?,\n _timing: boolean?,\n}\nlocal __JEST_MODULES __JEST_MODULES={cache={}, load=function(m)if not __JEST_MODULES.cache[m]then __JEST_MODULES.cache[m]={c=__JEST_MODULES[m]()}end return __JEST_MODULES.cache[m].c end}do function __JEST_MODULES.a()--!strict\n\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal module = {}\n\nfunction module.findInstance(path: string): Instance\n local parts = string.split(path, \"/\")\n\n local success, current = pcall(function()\n return game:FindService(parts[1])\n end)\n assert(success, `Failed to find service {parts[1]}: {current}`)\n\n for i = 2, #parts do\n assert(current, `Failed to find '{parts[i - 1]}' in path {path}`)\n current = current:FindFirstChild(parts[i])\n end\n\n assert(current, `Failed to find instance at path {path}`)\n\n return current\nend\n\nfunction module.getJest(config: { jestPath: string? }): ModuleScript\n local jestPath = config.jestPath\n if jestPath then\n local instance = module.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 :: ModuleScript\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\nfunction module.findRobloxShared(jestModule: ModuleScript): Instance?\n local parent = jestModule.Parent\n if parent then\n local found = parent:FindFirstChild(\"RobloxShared\")\n or parent:FindFirstChild(\"jest-roblox-shared\")\n if found then\n return found\n end\n\n if parent.Parent then\n found = parent.Parent:FindFirstChild(\"RobloxShared\")\n or parent.Parent:FindFirstChild(\"jest-roblox-shared\")\n if found then\n return found\n end\n end\n end\n\n return game:FindFirstChild(\"RobloxShared\", true)\nend\n\nfunction module.findSiblingPackage(jestModule: ModuleScript, ...: string): Instance?\n local parent = jestModule.Parent\n if parent then\n for _, name in { ... } do\n local found = parent:FindFirstChild(name)\n if found then\n return found\n end\n end\n\n if parent.Parent then\n for _, name in { ... } do\n local found = parent.Parent:FindFirstChild(name)\n if found then\n return found\n end\n end\n end\n end\n\n for _, name in { ... } do\n local found = game:FindFirstChild(name, true)\n if found then\n return found\n end\n end\n\n return nil\nend\n\nreturn module\nend function __JEST_MODULES.b()--!strict\n\nlocal function getInstancePath(instance: Instance): string\n local parts = {} :: { string }\n local current: Instance? = instance\n while current and current ~= game do\n table.insert(parts, 1, current.Name)\n current = current.Parent\n end\n\n return table.concat(parts, \"/\")\nend\n\nlocal CoreScriptSyncService = {}\n\nfunction CoreScriptSyncService:GetScriptFilePath(instance: Instance): string\n return getInstancePath(instance)\nend\n\nreturn CoreScriptSyncService\nend function __JEST_MODULES.c()--!strict\n\nlocal function normalizeSnapPath(path: string): string\n return (string.gsub(path, \"%.snap%.lua$\", \".snap.luau\"))\nend\n\nlocal function create(snapshotWrites: { [string]: string })\n local FileSystemService = {}\n\n function FileSystemService:WriteFile(path: string, contents: string)\n snapshotWrites[normalizeSnapPath(path)] = contents\n end\n\n function FileSystemService:CreateDirectories(_path: string) end\n\n function FileSystemService:Exists(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n function FileSystemService:Remove(path: string)\n snapshotWrites[normalizeSnapPath(path)] = nil\n end\n\n function FileSystemService:IsRegularFile(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n return FileSystemService\nend\n\nreturn create\nend function __JEST_MODULES.d()--!strict\n\nlocal CoreScriptSyncService = __JEST_MODULES.load('b')\nlocal InstanceResolver = __JEST_MODULES.load('a')\n\nlocal module = {}\n\nfunction module.createMockGetDataModelService(snapshotWrites: { [string]: string })\n local FileSystemService = __JEST_MODULES.load('c')(snapshotWrites)\n\n return function(service: string): any\n if service == \"FileSystemService\" then\n return FileSystemService\n elseif service == \"CoreScriptSyncService\" then\n return CoreScriptSyncService\n end\n\n local success, result = pcall(function()\n local service_ = game:GetService(service)\n local _ = service_.Name\n return service_\n end)\n\n return if success then result else nil\n end\nend\n\n\n\n\n\n\n\n\nfunction module.patch(jestModule: ModuleScript, snapshotWrites: { [string]: string }): PatchState__DARKLUA_TYPE_a?\n local mockGetDataModelService = module.createMockGetDataModelService(snapshotWrites)\n\n local robloxSharedInstance = InstanceResolver.findRobloxShared(jestModule)\n if not robloxSharedInstance then\n warn(\"Could not find RobloxShared; snapshot support unavailable\")\n return nil\n end\n\n local robloxSharedExports = (require :: any)(robloxSharedInstance)\n local originalGetDataModelService = robloxSharedExports.getDataModelService\n robloxSharedExports.getDataModelService = mockGetDataModelService\n\n local getDataModelServiceChild = robloxSharedInstance:FindFirstChild(\"getDataModelService\")\n\n local jestRuntimeModule =\n InstanceResolver.findSiblingPackage(jestModule, \"JestRuntime\", \"jest-runtime\")\n if not jestRuntimeModule then\n warn(\"Could not find JestRuntime; snapshot interception unavailable\")\n return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = nil,\n originalRequireInternalModule = nil,\n }\n end\n\n local Runtime = (require :: any)(jestRuntimeModule)\n local originalRequireInternalModule = Runtime.requireInternalModule\n\n Runtime.requireInternalModule = function(self: any, from: any, to: any, ...): any\n local target = if to ~= nil then to else from\n if\n getDataModelServiceChild\n and typeof(target) == \"Instance\"\n and target == getDataModelServiceChild\n then\n return mockGetDataModelService\n end\n\n return originalRequireInternalModule(self, from, to, ...)\n end\n\n return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = Runtime,\n originalRequireInternalModule = originalRequireInternalModule,\n }\nend\n\nfunction module.unpatch(state: PatchState__DARKLUA_TYPE_a?)\n if not state then\n return\n end\n\n if state.robloxSharedExports and state.originalGetDataModelService then\n state.robloxSharedExports.getDataModelService = state.originalGetDataModelService\n end\n\n if state.Runtime and state.originalRequireInternalModule then\n state.Runtime.requireInternalModule = state.originalRequireInternalModule\n end\nend\n\nreturn module\nend function __JEST_MODULES.e()--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\nlocal LogService = game:GetService(\"LogService\")\n\nlocal InstanceResolver = __JEST_MODULES.load('a')\nlocal SnapshotPatch = __JEST_MODULES.load('d')\n\n\n\n\n\n\n\n\n\n\nlocal function fail(err: string)\n return {\n success = false,\n err = err,\n }\nend\n\nlocal module = {}\n\nfunction module.run(callingScript: LuaSourceContainer, config: Config__DARKLUA_TYPE_b): (string, string)\n local t0 = os.clock()\n local timingEnabled = config._timing\n local coverageEnabled = config._coverage\n\n local t_findJest0 = os.clock()\n local findSuccess, findValue = pcall(InstanceResolver.getJest, config)\n local t_findJest = os.clock()\n\n if 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)),\n if logSuccess then logHistory else \"[]\"\n end\n\n LogService:ClearOutput()\n\n local snapshotWrites: { [string]: string } = {}\n\n local t_patchSnapshot0 = os.clock()\n local patchState = SnapshotPatch.patch(findValue, snapshotWrites)\n local t_patchSnapshot = os.clock()\n\n local t_requireJest0 = os.clock()\n local Jest = (require :: any)(findValue)\n local t_requireJest = os.clock()\n\n local 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, InstanceResolver.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, InstanceResolver.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, InstanceResolver.findInstance(setupPath))\n end\n\n config.setupFilesAfterEnv = resolved :: any\n end\n local t_resolveSetupFiles = os.clock()\n\n -- Strip private keys before Jest.runCLI (safe: single-task execution per VM)\n config._timing = nil :: any\n config._coverage = nil :: any\n\n if coverageEnabled then\n _G.__jest_roblox_cov = {}\n end\n\n local t_jestRunCLI0 = os.clock()\n local jestResult = Jest.runCLI(callingScript, 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 findJest = t_findJest - t_findJest0,\n patchSnapshot = t_patchSnapshot - t_patchSnapshot0,\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 total = os.clock() - t0,\n }\n end\n\n if next(snapshotWrites) then\n result._snapshotWrites = snapshotWrites\n end\n\n if coverageEnabled then\n result._coverage = _G.__jest_roblox_cov\n end\n\n return result\n end\n\n local jestDone = false\n local runSuccess = false\n local runValue: any = nil\n\n task.spawn(function()\n local ok, val = pcall(runTests)\n jestDone = true\n runSuccess = ok\n runValue = val\n end)\n\n local infiniteYieldMessage: string? = nil\n local watchdogConnection = LogService.MessageOut:Connect(\n function(message: string, messageType: Enum.MessageType)\n if\n messageType == Enum.MessageType.MessageWarning\n and string.find(message, \"Infinite yield possible\")\n and not infiniteYieldMessage\n then\n infiniteYieldMessage = message\n end\n end\n )\n\n while not jestDone and not infiniteYieldMessage do\n task.wait(0.1)\n end\n\n watchdogConnection:Disconnect()\n\n if not jestDone and infiniteYieldMessage then\n runSuccess = false\n runValue = \"Infinite yield detected, aborting tests: \" .. infiniteYieldMessage\n end\n\n SnapshotPatch.unpatch(patchState)\n\n local jestResult\n if not runSuccess then\n jestResult = HttpService:JSONEncode(fail(runValue :: any))\n else\n jestResult = HttpService:JSONEncode(runValue)\n end\n\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(LogService:GetLogHistory())\n end)\n\n return jestResult, if logSuccess then logHistory else \"[]\"\nend\n\nreturn module\nend end--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\n\nlocal Runner = __JEST_MODULES.load('e')\n\nlocal config = HttpService:JSONDecode([=[__CONFIG_JSON__]=])\n\nreturn Runner.run(script, config)\n";
|
|
196
|
-
|
|
197
|
-
//#endregion
|
|
198
|
-
//#region src/test-script.ts
|
|
199
|
-
const CLI_KEYS = new Set([
|
|
197
|
+
//#region src/config/schema.ts
|
|
198
|
+
const ROOT_ONLY_KEYS = new Set([
|
|
200
199
|
"backend",
|
|
201
200
|
"cache",
|
|
202
201
|
"collectCoverage",
|
|
203
202
|
"collectCoverageFrom",
|
|
204
|
-
"compact",
|
|
205
|
-
"compactMaxFailures",
|
|
206
203
|
"coverageDirectory",
|
|
207
204
|
"coveragePathIgnorePatterns",
|
|
208
205
|
"coverageReporters",
|
|
209
206
|
"coverageThreshold",
|
|
207
|
+
"formatters",
|
|
210
208
|
"gameOutput",
|
|
211
209
|
"jestPath",
|
|
210
|
+
"luauRoots",
|
|
212
211
|
"placeFile",
|
|
213
212
|
"pollInterval",
|
|
214
213
|
"port",
|
|
@@ -216,11 +215,208 @@ const CLI_KEYS = new Set([
|
|
|
216
215
|
"rootDir",
|
|
217
216
|
"showLuau",
|
|
218
217
|
"sourceMap",
|
|
219
|
-
"timeout"
|
|
218
|
+
"timeout",
|
|
219
|
+
"typecheck",
|
|
220
|
+
"typecheckOnly",
|
|
221
|
+
"typecheckTsconfig"
|
|
220
222
|
]);
|
|
223
|
+
const VALID_BACKENDS = new Set([
|
|
224
|
+
"auto",
|
|
225
|
+
"open-cloud",
|
|
226
|
+
"studio"
|
|
227
|
+
]);
|
|
228
|
+
function isValidBackend(value) {
|
|
229
|
+
return VALID_BACKENDS.has(value);
|
|
230
|
+
}
|
|
231
|
+
const DEFAULT_CONFIG = {
|
|
232
|
+
backend: "auto",
|
|
233
|
+
cache: true,
|
|
234
|
+
collectCoverage: false,
|
|
235
|
+
color: true,
|
|
236
|
+
coverageDirectory: "coverage",
|
|
237
|
+
coveragePathIgnorePatterns: [
|
|
238
|
+
"**/*.spec.lua",
|
|
239
|
+
"**/*.spec.luau",
|
|
240
|
+
"**/*.test.lua",
|
|
241
|
+
"**/*.test.luau",
|
|
242
|
+
"**/node_modules/**",
|
|
243
|
+
"**/rbxts_include/**"
|
|
244
|
+
],
|
|
245
|
+
coverageReporters: ["text", "lcov"],
|
|
246
|
+
placeFile: "./game.rbxl",
|
|
247
|
+
pollInterval: 500,
|
|
248
|
+
port: 3001,
|
|
249
|
+
rootDir: process.cwd(),
|
|
250
|
+
showLuau: true,
|
|
251
|
+
silent: false,
|
|
252
|
+
sourceMap: true,
|
|
253
|
+
testMatch: [
|
|
254
|
+
"**/*.spec.ts",
|
|
255
|
+
"**/*.spec.tsx",
|
|
256
|
+
"**/*.test.ts",
|
|
257
|
+
"**/*.test.tsx",
|
|
258
|
+
"**/*.spec-d.ts",
|
|
259
|
+
"**/*.test-d.ts",
|
|
260
|
+
"**/*.spec.lua",
|
|
261
|
+
"**/*.spec.luau",
|
|
262
|
+
"**/*.test.lua",
|
|
263
|
+
"**/*.test.luau"
|
|
264
|
+
],
|
|
265
|
+
testPathIgnorePatterns: [
|
|
266
|
+
"/node_modules/",
|
|
267
|
+
"/dist/",
|
|
268
|
+
"/out/"
|
|
269
|
+
],
|
|
270
|
+
timeout: 3e5,
|
|
271
|
+
typecheck: false,
|
|
272
|
+
typecheckOnly: false,
|
|
273
|
+
verbose: false
|
|
274
|
+
};
|
|
275
|
+
const snapshotFormatSchema = type({
|
|
276
|
+
"+": "reject",
|
|
277
|
+
"callToJSON?": "boolean",
|
|
278
|
+
"escapeRegex?": "boolean",
|
|
279
|
+
"escapeString?": "boolean",
|
|
280
|
+
"indent?": "number",
|
|
281
|
+
"maxDepth?": "number",
|
|
282
|
+
"min?": "boolean",
|
|
283
|
+
"printBasicPrototype?": "boolean",
|
|
284
|
+
"printFunctionName?": "boolean"
|
|
285
|
+
});
|
|
286
|
+
const coverageThresholdSchema = type({
|
|
287
|
+
"+": "reject",
|
|
288
|
+
"branches?": "number",
|
|
289
|
+
"functions?": "number",
|
|
290
|
+
"lines?": "number",
|
|
291
|
+
"statements?": "number"
|
|
292
|
+
});
|
|
293
|
+
const displayNameSchema = type({
|
|
294
|
+
"name": "string",
|
|
295
|
+
"+": "reject",
|
|
296
|
+
"color": "string"
|
|
297
|
+
});
|
|
298
|
+
const projectTestConfigSchema = type({
|
|
299
|
+
"+": "reject",
|
|
300
|
+
"automock?": "boolean",
|
|
301
|
+
"clearMocks?": "boolean",
|
|
302
|
+
"displayName": type("string").or(displayNameSchema),
|
|
303
|
+
"exclude?": "string[]",
|
|
304
|
+
"include": "string[]",
|
|
305
|
+
"injectGlobals?": "boolean",
|
|
306
|
+
"mockDataModel?": "boolean",
|
|
307
|
+
"outDir?": "string",
|
|
308
|
+
"resetMocks?": "boolean",
|
|
309
|
+
"resetModules?": "boolean",
|
|
310
|
+
"restoreMocks?": "boolean",
|
|
311
|
+
"root?": "string",
|
|
312
|
+
"setupFiles?": "string[]",
|
|
313
|
+
"setupFilesAfterEnv?": "string[]",
|
|
314
|
+
"slowTestThreshold?": "number",
|
|
315
|
+
"snapshotFormat?": snapshotFormatSchema,
|
|
316
|
+
"snapshotSerializers?": "string[]",
|
|
317
|
+
"testEnvironment?": "string",
|
|
318
|
+
"testEnvironmentOptions?": type("string").or(type("object")),
|
|
319
|
+
"testMatch?": "string[]",
|
|
320
|
+
"testPathIgnorePatterns?": "string[]",
|
|
321
|
+
"testRegex?": type("string").or(type("string[]")),
|
|
322
|
+
"testTimeout?": "number"
|
|
323
|
+
});
|
|
324
|
+
const inlineProjectSchema = type({
|
|
325
|
+
"+": "reject",
|
|
326
|
+
"test": projectTestConfigSchema
|
|
327
|
+
});
|
|
328
|
+
const formatterEntrySchema = type("string").or(type(["string", type("object")]));
|
|
329
|
+
const projectEntrySchema = type("string").or(inlineProjectSchema);
|
|
330
|
+
const configSchema = type({
|
|
331
|
+
"+": "reject",
|
|
332
|
+
"all?": "boolean",
|
|
333
|
+
"automock?": "boolean",
|
|
334
|
+
"backend?": type("'auto'|'open-cloud'|'studio'"),
|
|
335
|
+
"bail?": type("boolean").or(type("number")),
|
|
336
|
+
"cache?": "boolean",
|
|
337
|
+
"changedSince?": "string",
|
|
338
|
+
"ci?": "boolean",
|
|
339
|
+
"clearCache?": "boolean",
|
|
340
|
+
"clearMocks?": "boolean",
|
|
341
|
+
"collectCoverage?": "boolean",
|
|
342
|
+
"collectCoverageFrom?": "string[]",
|
|
343
|
+
"color?": "boolean",
|
|
344
|
+
"colors?": "boolean",
|
|
345
|
+
"config?": "string",
|
|
346
|
+
"coverage?": "boolean",
|
|
347
|
+
"coverageDirectory?": "string",
|
|
348
|
+
"coveragePathIgnorePatterns?": "string[]",
|
|
349
|
+
"coverageReporters?": "string[]",
|
|
350
|
+
"coverageThreshold?": coverageThresholdSchema,
|
|
351
|
+
"debug?": "boolean",
|
|
352
|
+
"env?": "string",
|
|
353
|
+
"expand?": "boolean",
|
|
354
|
+
"formatters?": formatterEntrySchema.array(),
|
|
355
|
+
"gameOutput?": "string",
|
|
356
|
+
"globals?": "string",
|
|
357
|
+
"init?": "boolean",
|
|
358
|
+
"injectGlobals?": "boolean",
|
|
359
|
+
"jestPath?": "string",
|
|
360
|
+
"luauRoots?": "string[]",
|
|
361
|
+
"maxWorkers?": type("number").or(type("string")),
|
|
362
|
+
"noStackTrace?": "boolean",
|
|
363
|
+
"outputFile?": "string",
|
|
364
|
+
"placeFile?": "string",
|
|
365
|
+
"pollInterval?": "number",
|
|
366
|
+
"port?": "number",
|
|
367
|
+
"preset?": "string",
|
|
368
|
+
"projects?": projectEntrySchema.array(),
|
|
369
|
+
"reporters?": "string[]",
|
|
370
|
+
"resetMocks?": "boolean",
|
|
371
|
+
"resetModules?": "boolean",
|
|
372
|
+
"restoreMocks?": "boolean",
|
|
373
|
+
"rojoProject?": "string",
|
|
374
|
+
"rootDir?": "string",
|
|
375
|
+
"roots?": "string[]",
|
|
376
|
+
"runInBand?": "boolean",
|
|
377
|
+
"selectProjects?": "string[]",
|
|
378
|
+
"setupFiles?": "string[]",
|
|
379
|
+
"setupFilesAfterEnv?": "string[]",
|
|
380
|
+
"showConfig?": "boolean",
|
|
381
|
+
"showLuau?": "boolean",
|
|
382
|
+
"silent?": "boolean",
|
|
383
|
+
"snapshotFormat?": snapshotFormatSchema,
|
|
384
|
+
"snapshotSerializers?": "string[]",
|
|
385
|
+
"sourceMap?": "boolean",
|
|
386
|
+
"testEnvironment?": "string",
|
|
387
|
+
"testEnvironmentOptions?": type("string").or(type("object")),
|
|
388
|
+
"testFailureExitCode?": "string",
|
|
389
|
+
"testMatch?": "string[]",
|
|
390
|
+
"testNamePattern?": "string",
|
|
391
|
+
"testPathIgnorePatterns?": "string[]",
|
|
392
|
+
"testPathPattern?": "string",
|
|
393
|
+
"testRegex?": type("string").or(type("string[]")),
|
|
394
|
+
"testTimeout?": "number",
|
|
395
|
+
"timeout?": "number",
|
|
396
|
+
"timers?": "string",
|
|
397
|
+
"typecheck?": "boolean",
|
|
398
|
+
"typecheckOnly?": "boolean",
|
|
399
|
+
"typecheckTsconfig?": "string",
|
|
400
|
+
"updateSnapshot?": "boolean",
|
|
401
|
+
"verbose?": "boolean",
|
|
402
|
+
"version?": "boolean"
|
|
403
|
+
}).as();
|
|
404
|
+
function validateConfig(raw) {
|
|
405
|
+
const result = configSchema(raw);
|
|
406
|
+
if (result instanceof type.errors) throw new Error(`Invalid config: ${result.summary}`);
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
const defineConfig = createDefineConfig();
|
|
410
|
+
const defineProject = createDefineConfig();
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/test-runner.bundled.luau
|
|
413
|
+
var test_runner_bundled_default = "type PatchState__DARKLUA_TYPE_a = {\n robloxSharedExports: any,\n originalGetDataModelService: any,\n Runtime: any,\n originalRequireInternalModule: any,\n}\n\ntype Config__DARKLUA_TYPE_b = {\n jestPath: string?,\n projects: { string }?,\n setupFiles: { string }?,\n setupFilesAfterEnv: { string }?,\n _coverage: boolean?,\n _timing: boolean?,\n}\n\ntype CapturedMessage__DARKLUA_TYPE_c = { message: string, messageType: number, timestamp: number }\nlocal __JEST_MODULES __JEST_MODULES={cache={}, load=function(m)if not __JEST_MODULES.cache[m]then __JEST_MODULES.cache[m]={c=__JEST_MODULES[m]()}end return __JEST_MODULES.cache[m].c end}do function __JEST_MODULES.a()--!strict\n\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal module = {}\n\nfunction module.findInstance(path: string): Instance\n local parts = string.split(path, \"/\")\n\n local success, current = pcall(function()\n return game:FindService(parts[1])\n end)\n assert(success, `Failed to find service {parts[1]}: {current}`)\n\n for i = 2, #parts do\n assert(current, `Failed to find '{parts[i - 1]}' in path {path}`)\n current = current:FindFirstChild(parts[i])\n end\n\n assert(current, `Failed to find instance at path {path}`)\n\n return current\nend\n\nfunction module.getJest(config: { jestPath: string? }): ModuleScript\n local jestPath = config.jestPath\n if jestPath then\n local instance = module.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 :: ModuleScript\n end\n\n local jestInstance = ReplicatedStorage:FindFirstChild(\"Jest\", true)\n assert(jestInstance, \"Failed to find Jest instance in ReplicatedStorage\")\n assert(jestInstance:IsA(\"ModuleScript\"), \"Jest instance in ReplicatedStorage is not a ModuleScript\")\n return jestInstance\nend\n\nfunction module.findRobloxShared(jestModule: ModuleScript): Instance?\n local parent = jestModule.Parent\n if parent then\n local found = parent:FindFirstChild(\"RobloxShared\") or parent:FindFirstChild(\"jest-roblox-shared\")\n if found then\n return found\n end\n\n if parent.Parent then\n found = parent.Parent:FindFirstChild(\"RobloxShared\") or parent.Parent:FindFirstChild(\"jest-roblox-shared\")\n if found then\n return found\n end\n end\n end\n\n return game:FindFirstChild(\"RobloxShared\", true)\nend\n\nfunction module.findSiblingPackage(jestModule: ModuleScript, ...: string): Instance?\n local parent = jestModule.Parent\n if parent then\n for _, name in { ... } do\n local found = parent:FindFirstChild(name)\n if found then\n return found\n end\n end\n\n if parent.Parent then\n for _, name in { ... } do\n local found = parent.Parent:FindFirstChild(name)\n if found then\n return found\n end\n end\n end\n end\n\n for _, name in { ... } do\n local found = game:FindFirstChild(name, true)\n if found then\n return found\n end\n end\n\n return nil\nend\n\nreturn module\nend function __JEST_MODULES.b()--!strict\n\nlocal function getInstancePath(instance: Instance): string\n local parts = {} :: { string }\n local current: Instance? = instance\n while current and current ~= game do\n table.insert(parts, 1, current.Name)\n current = current.Parent\n end\n\n return table.concat(parts, \"/\")\nend\n\nlocal CoreScriptSyncService = {}\n\nfunction CoreScriptSyncService:GetScriptFilePath(instance: Instance): string\n return getInstancePath(instance)\nend\n\nreturn CoreScriptSyncService\nend function __JEST_MODULES.c()--!strict\n\nlocal function normalizeSnapPath(path: string): string\n return (string.gsub(path, \"%.snap%.lua$\", \".snap.luau\"))\nend\n\nlocal function create(snapshotWrites: { [string]: string })\n local FileSystemService = {}\n\n function FileSystemService:WriteFile(path: string, contents: string)\n snapshotWrites[normalizeSnapPath(path)] = contents\n end\n\n function FileSystemService:CreateDirectories(_path: string) end\n\n function FileSystemService:Exists(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n function FileSystemService:Remove(path: string)\n snapshotWrites[normalizeSnapPath(path)] = nil\n end\n\n function FileSystemService:IsRegularFile(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n return FileSystemService\nend\n\nreturn create\nend function __JEST_MODULES.d()--!strict\n\nlocal CoreScriptSyncService = __JEST_MODULES.load('b')\nlocal InstanceResolver = __JEST_MODULES.load('a')\n\nlocal module = {}\n\nfunction module.createMockGetDataModelService(snapshotWrites: { [string]: string })\n local FileSystemService = __JEST_MODULES.load('c')(snapshotWrites)\n\n return function(service: string): any\n if service == \"FileSystemService\" then\n return FileSystemService\n elseif service == \"CoreScriptSyncService\" then\n return CoreScriptSyncService\n end\n\n local success, result = pcall(function()\n local service_ = game:GetService(service)\n local _ = service_.Name\n return service_\n end)\n\n return if success then result else nil\n end\nend\n\n\n\n\n\n\n\n\nfunction module.patch(jestModule: ModuleScript, snapshotWrites: { [string]: string }): PatchState__DARKLUA_TYPE_a?\n local mockGetDataModelService = module.createMockGetDataModelService(snapshotWrites)\n\n local robloxSharedInstance = InstanceResolver.findRobloxShared(jestModule)\n if not robloxSharedInstance then\n warn(\"Could not find RobloxShared; snapshot support unavailable\")\n return nil\n end\n\n local robloxSharedExports = (require :: any)(robloxSharedInstance)\n local originalGetDataModelService = robloxSharedExports.getDataModelService\n robloxSharedExports.getDataModelService = mockGetDataModelService\n\n local getDataModelServiceChild = robloxSharedInstance:FindFirstChild(\"getDataModelService\")\n\n local jestRuntimeModule = InstanceResolver.findSiblingPackage(jestModule, \"JestRuntime\", \"jest-runtime\")\n if not jestRuntimeModule then\n warn(\"Could not find JestRuntime; snapshot interception unavailable\")\n return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = nil,\n originalRequireInternalModule = nil,\n }\n end\n\n local Runtime = (require :: any)(jestRuntimeModule)\n local originalRequireInternalModule = Runtime.requireInternalModule\n\n Runtime.requireInternalModule = function(self: any, from: any, to: any, ...): any\n local target = if to ~= nil then to else from\n if getDataModelServiceChild and typeof(target) == \"Instance\" and target == getDataModelServiceChild then\n return mockGetDataModelService\n end\n\n return originalRequireInternalModule(self, from, to, ...)\n end\n\n return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = Runtime,\n originalRequireInternalModule = originalRequireInternalModule,\n }\nend\n\nfunction module.unpatch(state: PatchState__DARKLUA_TYPE_a?)\n if not state then\n return\n end\n\n if state.robloxSharedExports and state.originalGetDataModelService then\n state.robloxSharedExports.getDataModelService = state.originalGetDataModelService\n end\n\n if state.Runtime and state.originalRequireInternalModule then\n state.Runtime.requireInternalModule = state.originalRequireInternalModule\n end\nend\n\nreturn module\nend function __JEST_MODULES.e()--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\nlocal LogService = game:GetService(\"LogService\")\n\nlocal InstanceResolver = __JEST_MODULES.load('a')\nlocal SnapshotPatch = __JEST_MODULES.load('d')\n\n\n\n\n\n\n\n\n\n\nlocal function fail(err: string)\n return {\n success = false,\n err = err,\n }\nend\n\n\n\nlocal function interceptWriteable(writeable: any, buffer: { CapturedMessage__DARKLUA_TYPE_c }, messageType: number)\n local original = writeable._writeFn\n if typeof(original) ~= \"function\" then\n return function() end\n end\n\n writeable._writeFn = function(data: string)\n table.insert(buffer, {\n message = data,\n messageType = messageType,\n timestamp = os.clock(),\n })\n original(data)\n end\n\n return function()\n writeable._writeFn = original\n end\nend\n\nlocal module = {}\n\nfunction module.run(callingScript: LuaSourceContainer, config: Config__DARKLUA_TYPE_b): (string, string)\n local t0 = os.clock()\n local timingEnabled = config._timing\n local coverageEnabled = config._coverage\n\n local t_findJest0 = os.clock()\n local findSuccess, findValue = pcall(InstanceResolver.getJest, config)\n local t_findJest = os.clock()\n\n if 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 \"[]\"\n end\n\n LogService:ClearOutput()\n\n local snapshotWrites: { [string]: string } = {}\n\n local t_patchSnapshot0 = os.clock()\n local patchState = SnapshotPatch.patch(findValue, snapshotWrites)\n local t_patchSnapshot = os.clock()\n\n local t_requireJest0 = os.clock()\n local Jest = (require :: any)(findValue)\n local t_requireJest = os.clock()\n\n -- Intercept Jest's stdout/stderr to capture output synchronously.\n -- Jest writes via process.stdout/stderr (Writeable objects whose _writeFn\n -- defaults to print). Wrapping _writeFn captures messages like\n -- \"No tests found\" that are printed just before exit(1) throws.\n local capturedMessages: { CapturedMessage__DARKLUA_TYPE_c } = {}\n local restoreStdout: (() -> ())?\n local restoreStderr: (() -> ())?\n\n local interceptOk = pcall(function()\n local nodeModules = findValue.Parent.Parent.Parent :: any\n local RobloxShared = (require :: any)(nodeModules[\"@rbxts-js\"].RobloxShared)\n local process = RobloxShared.nodeUtils.process\n\n restoreStdout = interceptWriteable(process.stdout, capturedMessages, 0)\n restoreStderr = interceptWriteable(process.stderr, capturedMessages, 1)\n end)\n\n if not interceptOk then\n restoreStdout = nil\n restoreStderr = nil\n end\n\n local 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, InstanceResolver.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, InstanceResolver.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, InstanceResolver.findInstance(setupPath))\n end\n\n config.setupFilesAfterEnv = resolved :: any\n end\n local t_resolveSetupFiles = os.clock()\n\n -- Strip private keys before Jest.runCLI (safe: single-task execution per VM)\n config._timing = nil :: any\n config._coverage = nil :: any\n\n if coverageEnabled then\n _G.__jest_roblox_cov = {}\n end\n\n local t_jestRunCLI0 = os.clock()\n local jestResult = Jest.runCLI(callingScript, 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 findJest = t_findJest - t_findJest0,\n patchSnapshot = t_patchSnapshot - t_patchSnapshot0,\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 total = os.clock() - t0,\n }\n end\n\n if next(snapshotWrites) then\n result._snapshotWrites = snapshotWrites\n end\n\n if coverageEnabled then\n result._coverage = _G.__jest_roblox_cov\n end\n\n return result\n end\n\n local jestDone = false\n local runSuccess = false\n local runValue: any = nil\n\n task.spawn(function()\n local ok, val = pcall(runTests)\n jestDone = true\n runSuccess = ok\n runValue = val\n end)\n\n local infiniteYieldMessage: string? = nil\n local watchdogConnection = LogService.MessageOut:Connect(function(message: string, messageType: Enum.MessageType)\n if\n messageType == Enum.MessageType.MessageWarning\n and string.find(message, \"Infinite yield possible\")\n and not infiniteYieldMessage\n then\n infiniteYieldMessage = message\n end\n end)\n\n while not jestDone and not infiniteYieldMessage do\n task.wait(0.1)\n end\n\n watchdogConnection:Disconnect()\n\n if restoreStdout then\n restoreStdout()\n end\n\n if restoreStderr then\n restoreStderr()\n end\n\n if not jestDone and infiniteYieldMessage then\n runSuccess = false\n runValue = \"Infinite yield detected, aborting tests: \" .. infiniteYieldMessage\n end\n\n SnapshotPatch.unpatch(patchState)\n\n local jestResult\n if not runSuccess then\n jestResult = HttpService:JSONEncode(fail(runValue :: any))\n else\n jestResult = HttpService:JSONEncode(runValue)\n end\n\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(capturedMessages)\n end)\n\n return jestResult, if logSuccess then logHistory else \"[]\"\nend\n\nreturn module\nend end--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\n\nlocal Runner = __JEST_MODULES.load('e')\n\nlocal config = HttpService:JSONDecode([=[__CONFIG_JSON__]=])\n\nreturn Runner.run(script, config)\n";
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/test-script.ts
|
|
221
416
|
function buildJestArgv(options) {
|
|
222
417
|
const argv = {};
|
|
223
|
-
for (const [key, value] of Object.entries(options.config)) if (!
|
|
418
|
+
for (const [key, value] of Object.entries(options.config)) if (!ROOT_ONLY_KEYS.has(key) && value !== void 0) argv[key] = value;
|
|
419
|
+
if (options.config.jestPath !== void 0) argv["jestPath"] = options.config.jestPath;
|
|
224
420
|
if (process.env["TIMING"] !== void 0) argv["_timing"] = true;
|
|
225
421
|
if (options.config.collectCoverage) argv["_coverage"] = true;
|
|
226
422
|
return {
|
|
@@ -233,7 +429,6 @@ function generateTestScript(options) {
|
|
|
233
429
|
const config = buildJestArgv(options);
|
|
234
430
|
return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify(config));
|
|
235
431
|
}
|
|
236
|
-
|
|
237
432
|
//#endregion
|
|
238
433
|
//#region src/utils/cache.ts
|
|
239
434
|
const CACHE_DIR_NAME = "jest-roblox";
|
|
@@ -261,7 +456,7 @@ function markUploaded(cache, key, fileHash) {
|
|
|
261
456
|
}
|
|
262
457
|
function readCache(cacheFilePath) {
|
|
263
458
|
try {
|
|
264
|
-
const data = fs.readFileSync(cacheFilePath, "utf-8");
|
|
459
|
+
const data = fs$1.readFileSync(cacheFilePath, "utf-8");
|
|
265
460
|
return JSON.parse(data);
|
|
266
461
|
} catch {
|
|
267
462
|
return {};
|
|
@@ -269,16 +464,14 @@ function readCache(cacheFilePath) {
|
|
|
269
464
|
}
|
|
270
465
|
function writeCache(cacheFilePath, cache) {
|
|
271
466
|
const cacheDirectory = path$1.dirname(cacheFilePath);
|
|
272
|
-
fs.mkdirSync(cacheDirectory, { recursive: true });
|
|
273
|
-
fs.writeFileSync(cacheFilePath, JSON.stringify(cache, null, 2));
|
|
467
|
+
fs$1.mkdirSync(cacheDirectory, { recursive: true });
|
|
468
|
+
fs$1.writeFileSync(cacheFilePath, JSON.stringify(cache, null, 2));
|
|
274
469
|
}
|
|
275
|
-
|
|
276
470
|
//#endregion
|
|
277
471
|
//#region src/utils/hash.ts
|
|
278
472
|
function hashBuffer(data) {
|
|
279
473
|
return crypto.createHash("sha256").update(data).digest("hex");
|
|
280
474
|
}
|
|
281
|
-
|
|
282
475
|
//#endregion
|
|
283
476
|
//#region src/backends/http-client.ts
|
|
284
477
|
function createFetchClient(defaultHeaders) {
|
|
@@ -305,7 +498,6 @@ function createFetchClient(defaultHeaders) {
|
|
|
305
498
|
};
|
|
306
499
|
} };
|
|
307
500
|
}
|
|
308
|
-
|
|
309
501
|
//#endregion
|
|
310
502
|
//#region src/backends/open-cloud.ts
|
|
311
503
|
const OPEN_CLOUD_BASE_URL = "https://apis.roblox.com";
|
|
@@ -325,7 +517,7 @@ var OpenCloudBackend = class {
|
|
|
325
517
|
constructor(credentials, options) {
|
|
326
518
|
this.credentials = credentials;
|
|
327
519
|
this.http = options?.http ?? createFetchClient({ "x-api-key": credentials.apiKey });
|
|
328
|
-
this.readFile = options?.readFile ?? ((filePath) => fs.readFileSync(filePath));
|
|
520
|
+
this.readFile = options?.readFile ?? ((filePath) => fs$1.readFileSync(filePath));
|
|
329
521
|
this.sleepFn = options?.sleep ?? (async (ms) => {
|
|
330
522
|
return new Promise((resolve) => {
|
|
331
523
|
setTimeout(resolve, ms);
|
|
@@ -354,7 +546,13 @@ var OpenCloudBackend = class {
|
|
|
354
546
|
const taskPath = await this.createExecutionTask(options);
|
|
355
547
|
const { gameOutput, jestOutput } = await this.pollForCompletion(taskPath, options.config.timeout, options.config.pollInterval);
|
|
356
548
|
const executionMs = Date.now() - executionStart;
|
|
357
|
-
|
|
549
|
+
let parsed;
|
|
550
|
+
try {
|
|
551
|
+
parsed = parseJestOutput(jestOutput);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
if (err instanceof LuauScriptError) err.gameOutput = gameOutput;
|
|
554
|
+
throw err;
|
|
555
|
+
}
|
|
358
556
|
return {
|
|
359
557
|
coverageData: parsed.coverageData,
|
|
360
558
|
gameOutput,
|
|
@@ -447,7 +645,6 @@ function parseRetryAfter(headers) {
|
|
|
447
645
|
if (Number.isNaN(seconds) || seconds <= 0) return RATE_LIMIT_DEFAULT_WAIT_MS;
|
|
448
646
|
return seconds * 1e3;
|
|
449
647
|
}
|
|
450
|
-
|
|
451
648
|
//#endregion
|
|
452
649
|
//#region src/backends/studio.ts
|
|
453
650
|
const DEFAULT_STUDIO_TIMEOUT = 3e5;
|
|
@@ -484,7 +681,13 @@ var StudioBackend = class {
|
|
|
484
681
|
const executionStart = Date.now();
|
|
485
682
|
const message = await this.waitForResult(wss, requestId, config, existingSocket);
|
|
486
683
|
const executionMs = Date.now() - executionStart;
|
|
487
|
-
|
|
684
|
+
let parsed;
|
|
685
|
+
try {
|
|
686
|
+
parsed = parseJestOutput(message.jestOutput);
|
|
687
|
+
} catch (err) {
|
|
688
|
+
if (err instanceof LuauScriptError) err.gameOutput = message.gameOutput;
|
|
689
|
+
throw err;
|
|
690
|
+
}
|
|
488
691
|
return {
|
|
489
692
|
coverageData: parsed.coverageData,
|
|
490
693
|
gameOutput: message.gameOutput,
|
|
@@ -540,73 +743,8 @@ var StudioBackend = class {
|
|
|
540
743
|
function createStudioBackend(options) {
|
|
541
744
|
return new StudioBackend(options);
|
|
542
745
|
}
|
|
543
|
-
|
|
544
|
-
//#endregion
|
|
545
|
-
//#region src/config/schema.ts
|
|
546
|
-
const VALID_BACKENDS = new Set([
|
|
547
|
-
"auto",
|
|
548
|
-
"open-cloud",
|
|
549
|
-
"studio"
|
|
550
|
-
]);
|
|
551
|
-
function isValidBackend(value) {
|
|
552
|
-
return VALID_BACKENDS.has(value);
|
|
553
|
-
}
|
|
554
|
-
const DEFAULT_CONFIG = {
|
|
555
|
-
backend: "auto",
|
|
556
|
-
cache: true,
|
|
557
|
-
collectCoverage: false,
|
|
558
|
-
color: true,
|
|
559
|
-
compact: false,
|
|
560
|
-
compactMaxFailures: 10,
|
|
561
|
-
coverageDirectory: "coverage",
|
|
562
|
-
coveragePathIgnorePatterns: [
|
|
563
|
-
"**/*.spec.lua",
|
|
564
|
-
"**/*.spec.luau",
|
|
565
|
-
"**/*.test.lua",
|
|
566
|
-
"**/*.test.luau",
|
|
567
|
-
"**/node_modules/**",
|
|
568
|
-
"**/rbxts_include/**"
|
|
569
|
-
],
|
|
570
|
-
coverageReporters: ["text", "lcov"],
|
|
571
|
-
json: false,
|
|
572
|
-
placeFile: "./game.rbxl",
|
|
573
|
-
pollInterval: 500,
|
|
574
|
-
port: 3001,
|
|
575
|
-
rootDir: process.cwd(),
|
|
576
|
-
showLuau: true,
|
|
577
|
-
silent: false,
|
|
578
|
-
sourceMap: true,
|
|
579
|
-
testMatch: [
|
|
580
|
-
"**/*.spec.ts",
|
|
581
|
-
"**/*.spec.tsx",
|
|
582
|
-
"**/*.test.ts",
|
|
583
|
-
"**/*.test.tsx",
|
|
584
|
-
"**/*.spec-d.ts",
|
|
585
|
-
"**/*.test-d.ts",
|
|
586
|
-
"**/*.spec.lua",
|
|
587
|
-
"**/*.spec.luau",
|
|
588
|
-
"**/*.test.lua",
|
|
589
|
-
"**/*.test.luau"
|
|
590
|
-
],
|
|
591
|
-
testPathIgnorePatterns: [
|
|
592
|
-
"/node_modules/",
|
|
593
|
-
"/dist/",
|
|
594
|
-
"/out/"
|
|
595
|
-
],
|
|
596
|
-
timeout: 3e5,
|
|
597
|
-
typecheck: false,
|
|
598
|
-
typecheckOnly: false,
|
|
599
|
-
verbose: false
|
|
600
|
-
};
|
|
601
|
-
const defineConfig = createDefineConfig();
|
|
602
|
-
|
|
603
746
|
//#endregion
|
|
604
747
|
//#region src/config/loader.ts
|
|
605
|
-
function validateBackend(config) {
|
|
606
|
-
if (config.backend === void 0 || isValidBackend(config.backend)) return;
|
|
607
|
-
const valid = [...VALID_BACKENDS].join(", ");
|
|
608
|
-
throw new Error(`Invalid backend "${config.backend}" in config file. Must be one of: ${valid}`);
|
|
609
|
-
}
|
|
610
748
|
function applySnapshotFormatDefaults(config, isLuauProject) {
|
|
611
749
|
if (config.snapshotFormat?.printBasicPrototype !== void 0) return config;
|
|
612
750
|
return {
|
|
@@ -617,19 +755,24 @@ function applySnapshotFormatDefaults(config, isLuauProject) {
|
|
|
617
755
|
}
|
|
618
756
|
};
|
|
619
757
|
}
|
|
620
|
-
/**
|
|
621
|
-
* Merges a partial Config with DEFAULT_CONFIG. Useful for external consumers
|
|
622
|
-
* who build a Config manually (e.g. from CLI args) without going through
|
|
623
|
-
* file-based loadConfig.
|
|
624
|
-
*/
|
|
625
758
|
function resolveConfig(config) {
|
|
626
|
-
|
|
759
|
+
validateConfig(config);
|
|
627
760
|
const defined = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
|
|
628
761
|
return Object.assign({}, DEFAULT_CONFIG, defined);
|
|
629
762
|
}
|
|
630
763
|
async function loadConfig$1(configPath, cwd = process.cwd()) {
|
|
631
764
|
let result;
|
|
765
|
+
const extendWarnings = [];
|
|
766
|
+
const originalWarn = console.warn;
|
|
632
767
|
try {
|
|
768
|
+
console.warn = (...args) => {
|
|
769
|
+
const message = args.join(" ");
|
|
770
|
+
if (typeof message === "string" && message.includes("Cannot extend config")) {
|
|
771
|
+
extendWarnings.push(message);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
originalWarn.apply(console, args);
|
|
775
|
+
};
|
|
633
776
|
result = await loadConfig({
|
|
634
777
|
name: "jest",
|
|
635
778
|
configFile: configPath,
|
|
@@ -637,6 +780,7 @@ async function loadConfig$1(configPath, cwd = process.cwd()) {
|
|
|
637
780
|
cwd,
|
|
638
781
|
dotenv: false,
|
|
639
782
|
globalRc: false,
|
|
783
|
+
merger,
|
|
640
784
|
omit$Keys: true,
|
|
641
785
|
packageJson: false,
|
|
642
786
|
rcFile: false
|
|
@@ -644,11 +788,28 @@ async function loadConfig$1(configPath, cwd = process.cwd()) {
|
|
|
644
788
|
} catch (err) {
|
|
645
789
|
if (configPath !== void 0) throw new Error(`Config file not found: ${configPath}`, { cause: err });
|
|
646
790
|
throw err;
|
|
791
|
+
} finally {
|
|
792
|
+
console.warn = originalWarn;
|
|
793
|
+
}
|
|
794
|
+
if (extendWarnings.length > 0) {
|
|
795
|
+
const extendsPath = extendWarnings[0]?.match(/`([^`]+)`/)?.[1];
|
|
796
|
+
throw new Error(`Failed to resolve extends: "${extendsPath}". If the file exists, try adding the file extension (e.g. ".ts").`);
|
|
647
797
|
}
|
|
648
|
-
result.config
|
|
649
|
-
|
|
798
|
+
const config = resolveFunctionValues(result.config);
|
|
799
|
+
config.rootDir ??= cwd;
|
|
800
|
+
return resolveConfig(config);
|
|
801
|
+
}
|
|
802
|
+
function merger(...sources) {
|
|
803
|
+
return defuFn(...sources.filter(Boolean));
|
|
804
|
+
}
|
|
805
|
+
function isMergerFunction(value) {
|
|
806
|
+
return typeof value === "function";
|
|
807
|
+
}
|
|
808
|
+
function resolveFunctionValues(config) {
|
|
809
|
+
const resolved = {};
|
|
810
|
+
for (const [key, value] of Object.entries(config)) resolved[key] = isMergerFunction(value) ? value(void 0) : value;
|
|
811
|
+
return resolved;
|
|
650
812
|
}
|
|
651
|
-
|
|
652
813
|
//#endregion
|
|
653
814
|
//#region src/types/rojo.ts
|
|
654
815
|
const rojoProjectSchema = type({
|
|
@@ -656,7 +817,6 @@ const rojoProjectSchema = type({
|
|
|
656
817
|
"servePort?": "number.integer",
|
|
657
818
|
"tree": "object"
|
|
658
819
|
}).as();
|
|
659
|
-
|
|
660
820
|
//#endregion
|
|
661
821
|
//#region src/utils/normalize-windows-path.ts
|
|
662
822
|
const DRIVE_LETTER_START_REGEX = /^[A-Za-z]:\//;
|
|
@@ -664,7 +824,25 @@ function normalizeWindowsPath(input = "") {
|
|
|
664
824
|
if (!input) return input;
|
|
665
825
|
return input.replace(/\\/g, "/").replace(DRIVE_LETTER_START_REGEX, (driveLetterMatch) => driveLetterMatch.toUpperCase());
|
|
666
826
|
}
|
|
667
|
-
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/utils/tsconfig-mapping.ts
|
|
829
|
+
function findMapping(filePath, mappings, key = "outDir") {
|
|
830
|
+
let best;
|
|
831
|
+
let bestLength = -1;
|
|
832
|
+
for (const mapping of mappings) {
|
|
833
|
+
const prefix = mapping[key];
|
|
834
|
+
if ((filePath === prefix || filePath.startsWith(`${prefix}/`)) && prefix.length > bestLength) {
|
|
835
|
+
best = mapping;
|
|
836
|
+
bestLength = prefix.length;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return best;
|
|
840
|
+
}
|
|
841
|
+
function replacePrefix(filePath, from, to) {
|
|
842
|
+
if (filePath === from) return to;
|
|
843
|
+
if (filePath.startsWith(`${from}/`)) return `${to}${filePath.slice(from.length)}`;
|
|
844
|
+
return filePath;
|
|
845
|
+
}
|
|
668
846
|
//#endregion
|
|
669
847
|
//#region src/source-mapper/column-finder.ts
|
|
670
848
|
/**
|
|
@@ -683,29 +861,32 @@ function findExpectationColumn(lineText) {
|
|
|
683
861
|
if (lastMatcher === null || matcherName === void 0) return;
|
|
684
862
|
return expectIndex + (lastMatcher.index + lastMatcher[0].indexOf(matcherName)) + 1;
|
|
685
863
|
}
|
|
686
|
-
|
|
687
864
|
//#endregion
|
|
688
865
|
//#region src/source-mapper/path-resolver.ts
|
|
689
866
|
function createPathResolver(rojoProject, config) {
|
|
690
|
-
const
|
|
867
|
+
const rojoMappings = /* @__PURE__ */ new Map();
|
|
691
868
|
function walkTree(tree, prefix) {
|
|
692
869
|
for (const [key, value] of Object.entries(tree)) {
|
|
693
870
|
if (key.startsWith("$") || typeof value !== "object") continue;
|
|
694
871
|
const dataModelPath = prefix ? `${prefix}.${key}` : key;
|
|
695
872
|
const node = value;
|
|
696
|
-
if (typeof node.$path === "string")
|
|
873
|
+
if (typeof node.$path === "string") rojoMappings.set(dataModelPath, node.$path);
|
|
697
874
|
walkTree(node, dataModelPath);
|
|
698
875
|
}
|
|
699
876
|
}
|
|
700
877
|
walkTree(rojoProject.tree, "");
|
|
701
|
-
const
|
|
702
|
-
const
|
|
703
|
-
const useTypeScript = outDirectory !== void 0 && rootDirectory !== void 0;
|
|
878
|
+
const tsconfigMappings = config?.mappings ?? [];
|
|
879
|
+
const sortedRojoMappings = [...rojoMappings.entries()].sort(([a], [b]) => b.length - a.length);
|
|
704
880
|
return { resolve(dataModelPath) {
|
|
705
|
-
for (const [prefix, basePath] of
|
|
881
|
+
for (const [prefix, basePath] of sortedRojoMappings) {
|
|
882
|
+
if (dataModelPath !== prefix && !dataModelPath.startsWith(`${prefix}.`)) continue;
|
|
706
883
|
const result = `${basePath}/${convertToFilePath(dataModelPath.slice(prefix.length + 1))}`;
|
|
707
|
-
|
|
708
|
-
|
|
884
|
+
const mapping = findMapping(result, tsconfigMappings);
|
|
885
|
+
if (mapping !== void 0) return {
|
|
886
|
+
filePath: `${luauInitToIndex(replacePrefix(result, mapping.outDir, mapping.rootDir).replace(/^\.\//, ""))}.ts`,
|
|
887
|
+
mapping
|
|
888
|
+
};
|
|
889
|
+
return { filePath: findLuaFile(result) };
|
|
709
890
|
}
|
|
710
891
|
} };
|
|
711
892
|
}
|
|
@@ -722,6 +903,10 @@ function convertToFilePath(suffix) {
|
|
|
722
903
|
}
|
|
723
904
|
return result.join("/");
|
|
724
905
|
}
|
|
906
|
+
/** roblox-ts compiles index.ts → init.luau; reverse the rename for TS paths. */
|
|
907
|
+
function luauInitToIndex(filePath) {
|
|
908
|
+
return filePath.replace(/(^|\/)(init)(\.|\/)/, "$1index$3");
|
|
909
|
+
}
|
|
725
910
|
function findLuaFile(basePath) {
|
|
726
911
|
const luauPath = `${basePath}.luau`;
|
|
727
912
|
if (existsSync(luauPath)) return luauPath;
|
|
@@ -729,7 +914,6 @@ function findLuaFile(basePath) {
|
|
|
729
914
|
if (existsSync(luaPath)) return luaPath;
|
|
730
915
|
return luauPath;
|
|
731
916
|
}
|
|
732
|
-
|
|
733
917
|
//#endregion
|
|
734
918
|
//#region src/source-mapper/stack-parser.ts
|
|
735
919
|
const FRAME_REGEX = /\[string "([^"]+)"\]:(\d+)(?::(\d+))?/g;
|
|
@@ -749,7 +933,6 @@ function parseStack(input) {
|
|
|
749
933
|
message: input.slice(0, firstMatchIndex).trim()
|
|
750
934
|
};
|
|
751
935
|
}
|
|
752
|
-
|
|
753
936
|
//#endregion
|
|
754
937
|
//#region src/source-mapper/v3-mapper.ts
|
|
755
938
|
const mapCache = /* @__PURE__ */ new Map();
|
|
@@ -772,34 +955,31 @@ function getTraceMap(luauPath) {
|
|
|
772
955
|
let traced = mapCache.get(luauPath);
|
|
773
956
|
if (traced !== void 0) return traced;
|
|
774
957
|
const mapPath = `${luauPath}.map`;
|
|
775
|
-
if (!fs.existsSync(mapPath)) return;
|
|
776
|
-
traced = new TraceMap(fs.readFileSync(mapPath, "utf-8"));
|
|
958
|
+
if (!fs$1.existsSync(mapPath)) return;
|
|
959
|
+
traced = new TraceMap(fs$1.readFileSync(mapPath, "utf-8"));
|
|
777
960
|
mapCache.set(luauPath, traced);
|
|
778
961
|
return traced;
|
|
779
962
|
}
|
|
780
|
-
|
|
781
963
|
//#endregion
|
|
782
964
|
//#region src/source-mapper/index.ts
|
|
783
965
|
function createSourceMapper(config) {
|
|
784
|
-
const pathResolver = createPathResolver(config.rojoProject, {
|
|
785
|
-
outDir: config.outDir,
|
|
786
|
-
rootDir: config.rootDir
|
|
787
|
-
});
|
|
966
|
+
const pathResolver = createPathResolver(config.rojoProject, { mappings: config.mappings });
|
|
788
967
|
function mapFrame(frame) {
|
|
789
|
-
const
|
|
790
|
-
if (
|
|
791
|
-
if (
|
|
792
|
-
luauPath:
|
|
968
|
+
const resolved = pathResolver.resolve(frame.dataModelPath);
|
|
969
|
+
if (resolved === void 0) return;
|
|
970
|
+
if (resolved.mapping === void 0) return {
|
|
971
|
+
luauPath: resolved.filePath,
|
|
793
972
|
mapped: void 0
|
|
794
973
|
};
|
|
795
|
-
const
|
|
974
|
+
const { filePath, mapping } = resolved;
|
|
975
|
+
const luauPath = replacePrefix(filePath, mapping.rootDir, mapping.outDir).replace(/\.ts$/, ".luau");
|
|
796
976
|
const v3Result = mapFromSourceMap(luauPath, frame.line, frame.column);
|
|
797
977
|
if (v3Result !== void 0 && v3Result.source !== null && v3Result.line !== null) {
|
|
798
978
|
const mapDirectory = path$1.dirname(luauPath);
|
|
799
979
|
const resolvedTsPath = normalizeWindowsPath(path$1.resolve(mapDirectory, v3Result.source));
|
|
800
980
|
const tsLine = v3Result.line;
|
|
801
981
|
const embeddedContent = getSourceContent(luauPath, v3Result.source) ?? void 0;
|
|
802
|
-
const tsContent = embeddedContent ?? (fs.existsSync(resolvedTsPath) ? fs.readFileSync(resolvedTsPath, "utf-8") : void 0);
|
|
982
|
+
const tsContent = embeddedContent ?? (fs$1.existsSync(resolvedTsPath) ? fs$1.readFileSync(resolvedTsPath, "utf-8") : void 0);
|
|
803
983
|
return {
|
|
804
984
|
luauPath,
|
|
805
985
|
mapped: {
|
|
@@ -868,12 +1048,12 @@ function createSourceMapper(config) {
|
|
|
868
1048
|
},
|
|
869
1049
|
resolveTestFilePath(testFilePath) {
|
|
870
1050
|
const dataModelPath = testFilePath.replace(/^\//, "").replaceAll("/", ".");
|
|
871
|
-
return pathResolver.resolve(dataModelPath);
|
|
1051
|
+
return pathResolver.resolve(dataModelPath)?.filePath;
|
|
872
1052
|
}
|
|
873
1053
|
};
|
|
874
1054
|
}
|
|
875
1055
|
function getSourceSnippet({ column, context = 2, filePath, line, sourceContent }) {
|
|
876
|
-
const content = sourceContent ?? (fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : void 0);
|
|
1056
|
+
const content = sourceContent ?? (fs$1.existsSync(filePath) ? fs$1.readFileSync(filePath, "utf-8") : void 0);
|
|
877
1057
|
if (content === void 0) return;
|
|
878
1058
|
const allLines = content.split("\n");
|
|
879
1059
|
const startLine = Math.max(1, line - context);
|
|
@@ -894,13 +1074,11 @@ function getSourceSnippet({ column, context = 2, filePath, line, sourceContent }
|
|
|
894
1074
|
lines
|
|
895
1075
|
};
|
|
896
1076
|
}
|
|
897
|
-
|
|
898
1077
|
//#endregion
|
|
899
1078
|
//#region src/types/jest-result.ts
|
|
900
1079
|
function hasExecError(file) {
|
|
901
1080
|
return file.failureMessage !== void 0 && file.failureMessage !== "" && file.testResults.length === 0;
|
|
902
1081
|
}
|
|
903
|
-
|
|
904
1082
|
//#endregion
|
|
905
1083
|
//#region src/utils/banner.ts
|
|
906
1084
|
const SEPARATOR$1 = "⎯";
|
|
@@ -938,7 +1116,6 @@ function formatBanner({ body, level, termWidth, title }) {
|
|
|
938
1116
|
function getDefaultWidth() {
|
|
939
1117
|
return process.stderr.columns || 80;
|
|
940
1118
|
}
|
|
941
|
-
|
|
942
1119
|
//#endregion
|
|
943
1120
|
//#region src/highlighter/luau-grammar.ts
|
|
944
1121
|
const OPENING_LONG_BRACKET = "\\[=*\\[";
|
|
@@ -1003,7 +1180,6 @@ function luauGrammar(hljs) {
|
|
|
1003
1180
|
}
|
|
1004
1181
|
};
|
|
1005
1182
|
}
|
|
1006
|
-
|
|
1007
1183
|
//#endregion
|
|
1008
1184
|
//#region src/utils/colors.ts
|
|
1009
1185
|
hljs.registerLanguage("luau", luauGrammar);
|
|
@@ -1068,7 +1244,6 @@ function highlightLuau(source) {
|
|
|
1068
1244
|
function highlightTypeScript(source) {
|
|
1069
1245
|
return convertHljsToAnsi(hljs.highlight(source, { language: "typescript" }).value).replace(/=>/g, color.yellow("=>"));
|
|
1070
1246
|
}
|
|
1071
|
-
|
|
1072
1247
|
//#endregion
|
|
1073
1248
|
//#region src/formatters/formatter.ts
|
|
1074
1249
|
const EXEC_ERROR_HINTS = [[/loadstring\(\) is not available/, "loadstring() must be enabled for Jest to run. Add to your project.json:\n\n \"ServerScriptService\": {\n \"$properties\": {\n \"LoadStringEnabled\": true\n }\n }"]];
|
|
@@ -1200,7 +1375,7 @@ function formatRunHeader(options, styles) {
|
|
|
1200
1375
|
if (options.collectCoverage === true) return `${header}\n${`${st.dim(" Coverage enabled with")} ${st.status.pending("istanbul")}`}\n`;
|
|
1201
1376
|
return `${header}\n`;
|
|
1202
1377
|
}
|
|
1203
|
-
function formatTestSummary(result, timing, styles) {
|
|
1378
|
+
function formatTestSummary(result, timing, styles, options) {
|
|
1204
1379
|
const st = styles ?? createStyles(true);
|
|
1205
1380
|
const lines = [];
|
|
1206
1381
|
const execErrorFiles = result.testResults.filter(hasExecError).length;
|
|
@@ -1222,14 +1397,21 @@ function formatTestSummary(result, timing, styles) {
|
|
|
1222
1397
|
if (result.numPendingTests > 0) testParts.push(st.summary.pending(`${result.numPendingTests} skipped`));
|
|
1223
1398
|
const testTotalLabel = st.dim(`(${result.numTotalTests})`);
|
|
1224
1399
|
lines.push(`${st.dim(" Tests")} ${testParts.join(" | ")} ${testTotalLabel}`);
|
|
1400
|
+
if (options?.typeErrors !== void 0) {
|
|
1401
|
+
const typeErrorLabel = st.dim("Type Errors");
|
|
1402
|
+
const typeErrorValue = options.typeErrors > 0 ? st.summary.failed(`${options.typeErrors} failed`) : st.dim("no errors");
|
|
1403
|
+
lines.push(`${typeErrorLabel} ${typeErrorValue}`);
|
|
1404
|
+
}
|
|
1225
1405
|
const startAtStr = new Date(timing.startTime).toLocaleTimeString("en-GB", { hour12: false });
|
|
1226
1406
|
lines.push(`${st.dim(" Start at")} ${startAtStr}`);
|
|
1227
1407
|
const environmentMs = timing.executionMs - timing.testsMs;
|
|
1228
1408
|
const uploadMs = timing.uploadMs ?? 0;
|
|
1229
|
-
const
|
|
1409
|
+
const coverageMs = timing.coverageMs ?? 0;
|
|
1410
|
+
const cliMs = Math.max(0, timing.totalMs - uploadMs - timing.executionMs - coverageMs);
|
|
1230
1411
|
const breakdownParts = [];
|
|
1231
1412
|
if (timing.uploadMs !== void 0) breakdownParts.push(timing.uploadCached === true ? `upload ${timing.uploadMs}ms (cached)` : `upload ${timing.uploadMs}ms`);
|
|
1232
1413
|
breakdownParts.push(`environment ${environmentMs}ms`, `tests ${timing.testsMs}ms`, `cli ${cliMs}ms`);
|
|
1414
|
+
if (coverageMs > 0) breakdownParts.push(`coverage ${coverageMs}ms`);
|
|
1233
1415
|
const breakdown = st.dim(`(${breakdownParts.join(", ")})`);
|
|
1234
1416
|
lines.push(`${st.dim(" Duration")} ${timing.totalMs}ms ${breakdown}`);
|
|
1235
1417
|
return lines.join("\n");
|
|
@@ -1255,15 +1437,136 @@ function formatResult(result, timing, options) {
|
|
|
1255
1437
|
}
|
|
1256
1438
|
for (const file of execErrors) lines.push(formatExecErrorDetail(file, styles, failureCtx, options.sourceMapper));
|
|
1257
1439
|
}
|
|
1258
|
-
lines.push("", formatTestSummary(result, timing, styles));
|
|
1440
|
+
lines.push("", formatTestSummary(result, timing, styles, { typeErrors: options.typeErrors }));
|
|
1259
1441
|
if (!result.success) {
|
|
1260
1442
|
const hints = formatLogHints(options, styles);
|
|
1261
1443
|
if (hints !== "") lines.push("", hints);
|
|
1262
1444
|
}
|
|
1263
1445
|
return lines.join("\n");
|
|
1264
1446
|
}
|
|
1265
|
-
function
|
|
1266
|
-
|
|
1447
|
+
function formatTypecheckFailures(result, useColor = true) {
|
|
1448
|
+
const styles = createStyles(useColor);
|
|
1449
|
+
const lines = [];
|
|
1450
|
+
for (const file of result.testResults) for (const test of file.testResults) {
|
|
1451
|
+
if (test.status !== "failed") continue;
|
|
1452
|
+
const badge = styles.failBadge(" FAIL ");
|
|
1453
|
+
lines.push(` ${badge} ${styles.status.fail(test.fullName)}`);
|
|
1454
|
+
for (const message of test.failureMessages) lines.push(` ${styles.dim(message)}`);
|
|
1455
|
+
}
|
|
1456
|
+
return lines.join("\n");
|
|
1457
|
+
}
|
|
1458
|
+
function formatTypecheckSummary(result, useColor = true) {
|
|
1459
|
+
const styles = createStyles(useColor);
|
|
1460
|
+
const passed = result.numPassedTests;
|
|
1461
|
+
const failed = result.numFailedTests;
|
|
1462
|
+
const total = result.numTotalTests;
|
|
1463
|
+
const parts = [];
|
|
1464
|
+
if (failed > 0) parts.push(formatTypecheckFailures(result, useColor));
|
|
1465
|
+
const failedLabel = styles.summary.failed(`${String(failed)} failed`);
|
|
1466
|
+
const failedPart = failed > 0 ? `${failedLabel}, ` : "";
|
|
1467
|
+
const passedPart = styles.summary.passed(`${String(passed)} passed`);
|
|
1468
|
+
const label = styles.dim("Type Tests:");
|
|
1469
|
+
parts.push(`\n${label} ${failedPart}${passedPart}, ${String(total)} total\n`);
|
|
1470
|
+
return parts.join("\n");
|
|
1471
|
+
}
|
|
1472
|
+
const PROJECT_BADGE_COLORS = [
|
|
1473
|
+
(text) => color.bgYellow(color.black(text)),
|
|
1474
|
+
(text) => color.bgCyan(color.black(text)),
|
|
1475
|
+
(text) => color.bgGreen(color.black(text)),
|
|
1476
|
+
(text) => color.bgMagenta(color.black(text))
|
|
1477
|
+
];
|
|
1478
|
+
const NAMED_BADGE_COLORS = {
|
|
1479
|
+
blue: (text) => color.bgBlue(color.white(text)),
|
|
1480
|
+
cyan: (text) => color.bgCyan(color.black(text)),
|
|
1481
|
+
green: (text) => color.bgGreen(color.black(text)),
|
|
1482
|
+
magenta: (text) => color.bgMagenta(color.black(text)),
|
|
1483
|
+
red: (text) => color.bgRed(color.white(text)),
|
|
1484
|
+
white: (text) => color.bgWhite(color.black(text)),
|
|
1485
|
+
yellow: (text) => color.bgYellow(color.black(text))
|
|
1486
|
+
};
|
|
1487
|
+
function formatProjectBadge(displayName, useColor, displayColor) {
|
|
1488
|
+
if (!useColor) return `▶ ${displayName}`;
|
|
1489
|
+
return `▶ ${resolveBadgeColor(displayName, displayColor)(` ${displayName} `)}`;
|
|
1490
|
+
}
|
|
1491
|
+
function formatProjectHeader(options) {
|
|
1492
|
+
const { displayColor, displayName, result, styles: headerStyles, useColor = true } = options;
|
|
1493
|
+
const resolved = headerStyles ?? createStyles(useColor);
|
|
1494
|
+
const stats = computeProjectStats(result);
|
|
1495
|
+
const parts = [];
|
|
1496
|
+
if (stats.passedFiles > 0) parts.push(resolved.summary.passed(`${stats.passedFiles} passed`));
|
|
1497
|
+
if (stats.failedFiles > 0) parts.push(resolved.summary.failed(`${stats.failedFiles} failed`));
|
|
1498
|
+
if (stats.skippedFiles > 0) parts.push(resolved.summary.pending(`${stats.skippedFiles} skipped`));
|
|
1499
|
+
const duration = stats.durationMs > 0 ? ` - ${stats.durationMs}ms` : "";
|
|
1500
|
+
const meta = resolved.dim(`(${stats.totalTests} tests${duration})`);
|
|
1501
|
+
const fileStats = parts.join(" | ");
|
|
1502
|
+
return `${formatProjectBadge(displayName, useColor, displayColor)} ${fileStats} ${meta}`;
|
|
1503
|
+
}
|
|
1504
|
+
function formatProjectSection(section) {
|
|
1505
|
+
const { displayColor, displayName, failureCtx, options, result, styles: sectionStyles } = section;
|
|
1506
|
+
const resolved = sectionStyles ?? createStyles(options.color);
|
|
1507
|
+
const lines = [formatProjectHeader({
|
|
1508
|
+
displayColor,
|
|
1509
|
+
displayName,
|
|
1510
|
+
result,
|
|
1511
|
+
styles: resolved,
|
|
1512
|
+
useColor: options.color
|
|
1513
|
+
})];
|
|
1514
|
+
for (const file of result.testResults) {
|
|
1515
|
+
if (options.failuresOnly === true && file.numFailingTests === 0 && !hasExecError(file)) continue;
|
|
1516
|
+
lines.push(formatFileSummary(file, options, resolved));
|
|
1517
|
+
}
|
|
1518
|
+
const execErrors = result.testResults.filter(hasExecError);
|
|
1519
|
+
if (result.numFailedTests + execErrors.length > 0) {
|
|
1520
|
+
for (const file of result.testResults) {
|
|
1521
|
+
const failures = formatFileFailures(file, options, resolved, failureCtx);
|
|
1522
|
+
if (failures !== "") lines.push(failures);
|
|
1523
|
+
}
|
|
1524
|
+
for (const file of execErrors) lines.push(formatExecErrorDetail(file, resolved, failureCtx, options.sourceMapper));
|
|
1525
|
+
}
|
|
1526
|
+
return lines.join("\n");
|
|
1527
|
+
}
|
|
1528
|
+
function formatMultiProjectResult(projects, timing, options) {
|
|
1529
|
+
const styles = createStyles(options.color);
|
|
1530
|
+
let totalFailures = 0;
|
|
1531
|
+
for (const { result } of projects) {
|
|
1532
|
+
const execErrors = result.testResults.filter(hasExecError).length;
|
|
1533
|
+
totalFailures += result.numFailedTests + execErrors;
|
|
1534
|
+
}
|
|
1535
|
+
const failureCtx = {
|
|
1536
|
+
currentIndex: 1,
|
|
1537
|
+
totalFailures
|
|
1538
|
+
};
|
|
1539
|
+
const sections = [];
|
|
1540
|
+
for (const { displayColor, displayName, result } of projects) sections.push(formatProjectSection({
|
|
1541
|
+
displayColor,
|
|
1542
|
+
displayName,
|
|
1543
|
+
failureCtx,
|
|
1544
|
+
options,
|
|
1545
|
+
result,
|
|
1546
|
+
styles
|
|
1547
|
+
}));
|
|
1548
|
+
const lines = [formatRunHeader(options, styles), sections.join("\n\n")];
|
|
1549
|
+
const mergedResult = mergeJestResults(projects.map((project) => project.result));
|
|
1550
|
+
lines.push("", formatTestSummary(mergedResult, timing, styles, { typeErrors: options.typeErrors }));
|
|
1551
|
+
if (!mergedResult.success) {
|
|
1552
|
+
const hints = formatLogHints(options, styles);
|
|
1553
|
+
if (hints !== "") lines.push("", hints);
|
|
1554
|
+
}
|
|
1555
|
+
return lines.join("\n");
|
|
1556
|
+
}
|
|
1557
|
+
function hashProjectName(name) {
|
|
1558
|
+
let hash = 0;
|
|
1559
|
+
for (let index = 0; index < name.length; index++) hash += name.charCodeAt(index) + index;
|
|
1560
|
+
return hash % PROJECT_BADGE_COLORS.length;
|
|
1561
|
+
}
|
|
1562
|
+
function resolveBadgeColor(displayName, displayColor) {
|
|
1563
|
+
if (displayColor !== void 0) {
|
|
1564
|
+
const named = NAMED_BADGE_COLORS[displayColor];
|
|
1565
|
+
if (named !== void 0) return named;
|
|
1566
|
+
}
|
|
1567
|
+
const hashed = PROJECT_BADGE_COLORS[hashProjectName(displayName)];
|
|
1568
|
+
assert(hashed !== void 0, "hash always returns valid index");
|
|
1569
|
+
return hashed;
|
|
1267
1570
|
}
|
|
1268
1571
|
function identity(text) {
|
|
1269
1572
|
return text;
|
|
@@ -1320,171 +1623,51 @@ function createStyles(useColor) {
|
|
|
1320
1623
|
}
|
|
1321
1624
|
};
|
|
1322
1625
|
}
|
|
1323
|
-
function
|
|
1324
|
-
let
|
|
1325
|
-
for (const
|
|
1326
|
-
|
|
1327
|
-
result += " ".repeat(spaces);
|
|
1328
|
-
} else result += char;
|
|
1329
|
-
return result;
|
|
1626
|
+
function sumFileDuration(file) {
|
|
1627
|
+
let total = 0;
|
|
1628
|
+
for (const test of file.testResults) if (test.duration !== void 0) total += test.duration;
|
|
1629
|
+
return total;
|
|
1330
1630
|
}
|
|
1331
|
-
function
|
|
1332
|
-
|
|
1333
|
-
|
|
1631
|
+
function computeProjectStats(result) {
|
|
1632
|
+
let durationMs = 0;
|
|
1633
|
+
let failedFiles = 0;
|
|
1634
|
+
let passedFiles = 0;
|
|
1635
|
+
let skippedFiles = 0;
|
|
1636
|
+
for (const file of result.testResults) {
|
|
1637
|
+
if (file.numFailingTests > 0 || hasExecError(file)) failedFiles++;
|
|
1638
|
+
else if (file.numPassingTests === 0 && file.numPendingTests > 0) skippedFiles++;
|
|
1639
|
+
else passedFiles++;
|
|
1640
|
+
durationMs += sumFileDuration(file);
|
|
1641
|
+
}
|
|
1642
|
+
return {
|
|
1643
|
+
durationMs,
|
|
1644
|
+
failedFiles,
|
|
1645
|
+
passedFiles,
|
|
1646
|
+
skippedFiles,
|
|
1647
|
+
totalTests: result.numTotalTests
|
|
1648
|
+
};
|
|
1334
1649
|
}
|
|
1335
|
-
function
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1650
|
+
function formatFileFailures(file, options, styles, failureCtx) {
|
|
1651
|
+
const lines = [];
|
|
1652
|
+
const displayPath = resolveDisplayPath(file.testFilePath, options.sourceMapper);
|
|
1653
|
+
for (const testCase of file.testResults) if (testCase.status === "failed") {
|
|
1654
|
+
const index = failureCtx.currentIndex;
|
|
1655
|
+
failureCtx.currentIndex++;
|
|
1656
|
+
lines.push(formatFailure({
|
|
1657
|
+
failureIndex: index,
|
|
1658
|
+
filePath: displayPath,
|
|
1659
|
+
showLuau: options.showLuau,
|
|
1660
|
+
sourceMapper: options.sourceMapper,
|
|
1661
|
+
styles,
|
|
1662
|
+
test: testCase,
|
|
1663
|
+
totalFailures: failureCtx.totalFailures,
|
|
1664
|
+
useColor: options.color
|
|
1665
|
+
}));
|
|
1342
1666
|
}
|
|
1343
|
-
|
|
1344
|
-
"",
|
|
1345
|
-
styles.diff.expected("- Expected"),
|
|
1346
|
-
styles.diff.received("+ Received"),
|
|
1347
|
-
"",
|
|
1348
|
-
styles.diff.expected(`- ${parsed.expected}`),
|
|
1349
|
-
styles.diff.received(`+ ${parsed.received}`)
|
|
1350
|
-
];
|
|
1351
|
-
return [];
|
|
1667
|
+
return lines.join("\n");
|
|
1352
1668
|
}
|
|
1353
|
-
function
|
|
1354
|
-
|
|
1355
|
-
return styles.status.fail(parsed.message);
|
|
1356
|
-
}
|
|
1357
|
-
function formatFallbackSnippet(message, styles, useColor) {
|
|
1358
|
-
const location = parseSourceLocation(message);
|
|
1359
|
-
if (location === void 0) return [];
|
|
1360
|
-
const snippet = getSourceSnippet({
|
|
1361
|
-
column: location.column,
|
|
1362
|
-
context: 2,
|
|
1363
|
-
filePath: location.path,
|
|
1364
|
-
line: location.line
|
|
1365
|
-
});
|
|
1366
|
-
if (snippet === void 0) return [];
|
|
1367
|
-
return ["", formatSourceSnippet(snippet, location.path, {
|
|
1368
|
-
styles,
|
|
1369
|
-
useColor
|
|
1370
|
-
})];
|
|
1371
|
-
}
|
|
1372
|
-
function formatMappedLocationSnippets(loc, showLuau, styles, useColor) {
|
|
1373
|
-
const snippets = [];
|
|
1374
|
-
if (loc.tsPath !== void 0 && loc.tsLine !== void 0) {
|
|
1375
|
-
const tsSnippet = getSourceSnippet({
|
|
1376
|
-
column: loc.tsColumn,
|
|
1377
|
-
context: 2,
|
|
1378
|
-
filePath: loc.tsPath,
|
|
1379
|
-
line: loc.tsLine,
|
|
1380
|
-
sourceContent: loc.sourceContent
|
|
1381
|
-
});
|
|
1382
|
-
if (tsSnippet !== void 0) {
|
|
1383
|
-
const label = showLuau ? "TypeScript" : void 0;
|
|
1384
|
-
snippets.push("", formatSourceSnippet(tsSnippet, loc.tsPath, {
|
|
1385
|
-
language: label,
|
|
1386
|
-
styles,
|
|
1387
|
-
useColor
|
|
1388
|
-
}));
|
|
1389
|
-
}
|
|
1390
|
-
if (showLuau) {
|
|
1391
|
-
const luauSnippet = getSourceSnippet({
|
|
1392
|
-
context: 2,
|
|
1393
|
-
filePath: loc.luauPath,
|
|
1394
|
-
line: loc.luauLine
|
|
1395
|
-
});
|
|
1396
|
-
if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
|
|
1397
|
-
language: "Luau",
|
|
1398
|
-
styles,
|
|
1399
|
-
useColor
|
|
1400
|
-
}));
|
|
1401
|
-
}
|
|
1402
|
-
} else {
|
|
1403
|
-
const luauSnippet = getSourceSnippet({
|
|
1404
|
-
context: 2,
|
|
1405
|
-
filePath: loc.luauPath,
|
|
1406
|
-
line: loc.luauLine
|
|
1407
|
-
});
|
|
1408
|
-
if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
|
|
1409
|
-
styles,
|
|
1410
|
-
useColor
|
|
1411
|
-
}));
|
|
1412
|
-
}
|
|
1413
|
-
return snippets;
|
|
1414
|
-
}
|
|
1415
|
-
function formatSnapshotCallSnippet(filePath, styles, useColor) {
|
|
1416
|
-
if (!fs.existsSync(filePath)) return [];
|
|
1417
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
1418
|
-
const snapshotIndices = content.split("\n").reduce((accumulator, fileLine, index) => {
|
|
1419
|
-
if (fileLine.includes("toMatchSnapshot")) accumulator.push(index);
|
|
1420
|
-
return accumulator;
|
|
1421
|
-
}, []);
|
|
1422
|
-
if (snapshotIndices.length !== 1) return [];
|
|
1423
|
-
const snippet = getSourceSnippet({
|
|
1424
|
-
context: 2,
|
|
1425
|
-
filePath,
|
|
1426
|
-
line: snapshotIndices[0] + 1,
|
|
1427
|
-
sourceContent: content
|
|
1428
|
-
});
|
|
1429
|
-
if (snippet === void 0) return [];
|
|
1430
|
-
return ["", formatSourceSnippet(snippet, filePath, {
|
|
1431
|
-
styles,
|
|
1432
|
-
useColor
|
|
1433
|
-
})];
|
|
1434
|
-
}
|
|
1435
|
-
function resolveSourceSnippets(options) {
|
|
1436
|
-
const { filePath, hasSnapshotDiff, mappedLocations, message, showLuau, sourceMapper, styles, useColor } = options;
|
|
1437
|
-
if (mappedLocations.length > 0) return mappedLocations.flatMap((loc) => {
|
|
1438
|
-
return formatMappedLocationSnippets(loc, showLuau, styles, useColor);
|
|
1439
|
-
});
|
|
1440
|
-
const fallback = formatFallbackSnippet(message, styles, useColor);
|
|
1441
|
-
if (fallback.length > 0) return fallback;
|
|
1442
|
-
if (hasSnapshotDiff && filePath !== void 0) return formatSnapshotCallSnippet(resolveDisplayPath(filePath, sourceMapper), styles, useColor);
|
|
1443
|
-
return [];
|
|
1444
|
-
}
|
|
1445
|
-
function formatFailureMessage(originalMessage, options) {
|
|
1446
|
-
const { filePath, showLuau, sourceMapper, styles, useColor } = options;
|
|
1447
|
-
let mappedLocations = [];
|
|
1448
|
-
let message = originalMessage;
|
|
1449
|
-
if (sourceMapper !== void 0) ({locations: mappedLocations, message} = sourceMapper.mapFailureWithLocations(originalMessage));
|
|
1450
|
-
const parsed = parseErrorMessage(originalMessage);
|
|
1451
|
-
return [
|
|
1452
|
-
formatErrorLine(parsed, styles, useColor),
|
|
1453
|
-
...formatDiffBlock(parsed, styles),
|
|
1454
|
-
...resolveSourceSnippets({
|
|
1455
|
-
filePath,
|
|
1456
|
-
hasSnapshotDiff: parsed.snapshotDiff !== void 0,
|
|
1457
|
-
mappedLocations,
|
|
1458
|
-
message,
|
|
1459
|
-
showLuau,
|
|
1460
|
-
sourceMapper,
|
|
1461
|
-
styles,
|
|
1462
|
-
useColor
|
|
1463
|
-
})
|
|
1464
|
-
];
|
|
1465
|
-
}
|
|
1466
|
-
function formatSnapshotLine(snapshot, styles) {
|
|
1467
|
-
if (snapshot === void 0 || snapshot.unmatched === 0) return;
|
|
1468
|
-
return `${styles.dim(" Snapshots")} ${styles.summary.failed(`${snapshot.unmatched} failed`)}`;
|
|
1469
|
-
}
|
|
1470
|
-
function formatFileFailures(file, options, styles, failureCtx) {
|
|
1471
|
-
const lines = [];
|
|
1472
|
-
const displayPath = resolveDisplayPath(file.testFilePath, options.sourceMapper);
|
|
1473
|
-
for (const testCase of file.testResults) if (testCase.status === "failed") {
|
|
1474
|
-
const index = failureCtx.currentIndex;
|
|
1475
|
-
failureCtx.currentIndex++;
|
|
1476
|
-
lines.push(formatFailure({
|
|
1477
|
-
failureIndex: index,
|
|
1478
|
-
filePath: displayPath,
|
|
1479
|
-
showLuau: options.showLuau,
|
|
1480
|
-
sourceMapper: options.sourceMapper,
|
|
1481
|
-
styles,
|
|
1482
|
-
test: testCase,
|
|
1483
|
-
totalFailures: failureCtx.totalFailures,
|
|
1484
|
-
useColor: options.color
|
|
1485
|
-
}));
|
|
1486
|
-
}
|
|
1487
|
-
return lines.join("\n");
|
|
1669
|
+
function getTerminalWidth() {
|
|
1670
|
+
return "columns" in process.stdout && process.stdout.columns || 80;
|
|
1488
1671
|
}
|
|
1489
1672
|
function formatExecErrorDetail(file, styles, failureCtx, sourceMapper) {
|
|
1490
1673
|
const lines = [];
|
|
@@ -1503,12 +1686,6 @@ function formatExecErrorDetail(file, styles, failureCtx, sourceMapper) {
|
|
|
1503
1686
|
lines.push("", ` ${separator}`);
|
|
1504
1687
|
return lines.join("\n");
|
|
1505
1688
|
}
|
|
1506
|
-
function formatLogHints(options, styles) {
|
|
1507
|
-
const lines = [];
|
|
1508
|
-
if (options.outputFile !== void 0) lines.push(styles.dim(` View ${options.outputFile} for full Jest output`));
|
|
1509
|
-
if (options.gameOutput !== void 0) lines.push(styles.dim(` View ${options.gameOutput} for Roblox game logs`));
|
|
1510
|
-
return lines.join("\n");
|
|
1511
|
-
}
|
|
1512
1689
|
function formatTestInGroup(testCase, styles) {
|
|
1513
1690
|
const duration = testCase.duration !== void 0 ? styles.lineNumber(` ${testCase.duration}ms`) : "";
|
|
1514
1691
|
if (testCase.status === "passed") return `${styles.status.pass(" ✓")}${styles.status.fail(` ${testCase.title}`)}${duration}`;
|
|
@@ -1572,11 +1749,6 @@ function formatPass(test, styles) {
|
|
|
1572
1749
|
const duration = test.duration !== void 0 ? styles.dim(` ${test.duration}ms`) : "";
|
|
1573
1750
|
return styles.status.pass(` ✓ ${test.fullName}`) + duration;
|
|
1574
1751
|
}
|
|
1575
|
-
function sumFileDuration(file) {
|
|
1576
|
-
let total = 0;
|
|
1577
|
-
for (const test of file.testResults) if (test.duration !== void 0) total += test.duration;
|
|
1578
|
-
return total;
|
|
1579
|
-
}
|
|
1580
1752
|
function formatPassedFileSummary(file, ctx) {
|
|
1581
1753
|
const lines = [];
|
|
1582
1754
|
const fileMs = sumFileDuration(file);
|
|
@@ -1603,51 +1775,323 @@ function formatFileSummary(file, options, styles) {
|
|
|
1603
1775
|
verbose: options.verbose
|
|
1604
1776
|
}).join("\n");
|
|
1605
1777
|
}
|
|
1606
|
-
|
|
1778
|
+
function formatLogHints(options, styles) {
|
|
1779
|
+
const lines = [];
|
|
1780
|
+
if (options.outputFile !== void 0) lines.push(styles.dim(` View ${options.outputFile} for full Jest output`));
|
|
1781
|
+
if (options.gameOutput !== void 0) lines.push(styles.dim(` View ${options.gameOutput} for Roblox game logs`));
|
|
1782
|
+
return lines.join("\n");
|
|
1783
|
+
}
|
|
1784
|
+
function mergeJestResults(results) {
|
|
1785
|
+
let numberFailedTests = 0;
|
|
1786
|
+
let numberPassedTests = 0;
|
|
1787
|
+
let numberPendingTests = 0;
|
|
1788
|
+
let numberTodoTests = 0;
|
|
1789
|
+
let numberTotalTests = 0;
|
|
1790
|
+
let startTime = Number.POSITIVE_INFINITY;
|
|
1791
|
+
let success = true;
|
|
1792
|
+
let snapshotAdded = 0;
|
|
1793
|
+
let snapshotMatched = 0;
|
|
1794
|
+
let snapshotTotal = 0;
|
|
1795
|
+
let snapshotUnmatched = 0;
|
|
1796
|
+
let snapshotUpdated = 0;
|
|
1797
|
+
let hasSnapshot = false;
|
|
1798
|
+
const testResults = [];
|
|
1799
|
+
for (const result of results) {
|
|
1800
|
+
numberFailedTests += result.numFailedTests;
|
|
1801
|
+
numberPassedTests += result.numPassedTests;
|
|
1802
|
+
numberPendingTests += result.numPendingTests;
|
|
1803
|
+
numberTodoTests += result.numTodoTests ?? 0;
|
|
1804
|
+
numberTotalTests += result.numTotalTests;
|
|
1805
|
+
startTime = Math.min(startTime, result.startTime);
|
|
1806
|
+
success &&= result.success;
|
|
1807
|
+
testResults.push(...result.testResults);
|
|
1808
|
+
if (result.snapshot !== void 0) {
|
|
1809
|
+
hasSnapshot = true;
|
|
1810
|
+
snapshotAdded += result.snapshot.added;
|
|
1811
|
+
snapshotMatched += result.snapshot.matched;
|
|
1812
|
+
snapshotTotal += result.snapshot.total;
|
|
1813
|
+
snapshotUnmatched += result.snapshot.unmatched;
|
|
1814
|
+
snapshotUpdated += result.snapshot.updated;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return {
|
|
1818
|
+
numFailedTests: numberFailedTests,
|
|
1819
|
+
numPassedTests: numberPassedTests,
|
|
1820
|
+
numPendingTests: numberPendingTests,
|
|
1821
|
+
numTodoTests: numberTodoTests > 0 ? numberTodoTests : void 0,
|
|
1822
|
+
numTotalTests: numberTotalTests,
|
|
1823
|
+
snapshot: hasSnapshot ? {
|
|
1824
|
+
added: snapshotAdded,
|
|
1825
|
+
matched: snapshotMatched,
|
|
1826
|
+
total: snapshotTotal,
|
|
1827
|
+
unmatched: snapshotUnmatched,
|
|
1828
|
+
updated: snapshotUpdated
|
|
1829
|
+
} : void 0,
|
|
1830
|
+
startTime,
|
|
1831
|
+
success,
|
|
1832
|
+
testResults
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
function expandTabs(text, tabWidth = 4) {
|
|
1836
|
+
let result = "";
|
|
1837
|
+
for (const char of text) if (char === " ") {
|
|
1838
|
+
const spaces = tabWidth - result.length % tabWidth;
|
|
1839
|
+
result += " ".repeat(spaces);
|
|
1840
|
+
} else result += char;
|
|
1841
|
+
return result;
|
|
1842
|
+
}
|
|
1843
|
+
function highlightSyntax(filePath, code, useColor) {
|
|
1844
|
+
if (!useColor) return code;
|
|
1845
|
+
return highlightCode(filePath, code);
|
|
1846
|
+
}
|
|
1847
|
+
function formatDiffBlock(parsed, styles) {
|
|
1848
|
+
if (parsed.snapshotDiff !== void 0) {
|
|
1849
|
+
const lines = [""];
|
|
1850
|
+
for (const diffLine of parsed.snapshotDiff.split("\n")) if (diffLine.startsWith("- ")) lines.push(styles.diff.expected(diffLine));
|
|
1851
|
+
else if (diffLine.startsWith("+ ")) lines.push(styles.diff.received(diffLine));
|
|
1852
|
+
else lines.push(styles.dim(diffLine));
|
|
1853
|
+
return lines;
|
|
1854
|
+
}
|
|
1855
|
+
if (parsed.expected !== void 0 && parsed.received !== void 0) return [
|
|
1856
|
+
"",
|
|
1857
|
+
styles.diff.expected("- Expected"),
|
|
1858
|
+
styles.diff.received("+ Received"),
|
|
1859
|
+
"",
|
|
1860
|
+
styles.diff.expected(`- ${parsed.expected}`),
|
|
1861
|
+
styles.diff.received(`+ ${parsed.received}`)
|
|
1862
|
+
];
|
|
1863
|
+
return [];
|
|
1864
|
+
}
|
|
1865
|
+
function formatErrorLine(parsed, styles, useColor) {
|
|
1866
|
+
if (useColor && parsed.message.startsWith("Error:")) return styles.status.fail(color.bold("Error:") + parsed.message.slice(6));
|
|
1867
|
+
return styles.status.fail(parsed.message);
|
|
1868
|
+
}
|
|
1869
|
+
function formatFallbackSnippet(message, styles, useColor) {
|
|
1870
|
+
const location = parseSourceLocation(message);
|
|
1871
|
+
if (location === void 0) return [];
|
|
1872
|
+
const snippet = getSourceSnippet({
|
|
1873
|
+
column: location.column,
|
|
1874
|
+
context: 2,
|
|
1875
|
+
filePath: location.path,
|
|
1876
|
+
line: location.line
|
|
1877
|
+
});
|
|
1878
|
+
if (snippet === void 0) return [];
|
|
1879
|
+
return ["", formatSourceSnippet(snippet, location.path, {
|
|
1880
|
+
styles,
|
|
1881
|
+
useColor
|
|
1882
|
+
})];
|
|
1883
|
+
}
|
|
1884
|
+
function formatMappedLocationSnippets(loc, showLuau, styles, useColor) {
|
|
1885
|
+
const snippets = [];
|
|
1886
|
+
if (loc.tsPath !== void 0 && loc.tsLine !== void 0) {
|
|
1887
|
+
const tsSnippet = getSourceSnippet({
|
|
1888
|
+
column: loc.tsColumn,
|
|
1889
|
+
context: 2,
|
|
1890
|
+
filePath: loc.tsPath,
|
|
1891
|
+
line: loc.tsLine,
|
|
1892
|
+
sourceContent: loc.sourceContent
|
|
1893
|
+
});
|
|
1894
|
+
if (tsSnippet !== void 0) {
|
|
1895
|
+
const label = showLuau ? "TypeScript" : void 0;
|
|
1896
|
+
snippets.push("", formatSourceSnippet(tsSnippet, loc.tsPath, {
|
|
1897
|
+
language: label,
|
|
1898
|
+
styles,
|
|
1899
|
+
useColor
|
|
1900
|
+
}));
|
|
1901
|
+
}
|
|
1902
|
+
if (showLuau) {
|
|
1903
|
+
const luauSnippet = getSourceSnippet({
|
|
1904
|
+
context: 2,
|
|
1905
|
+
filePath: loc.luauPath,
|
|
1906
|
+
line: loc.luauLine
|
|
1907
|
+
});
|
|
1908
|
+
if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
|
|
1909
|
+
language: "Luau",
|
|
1910
|
+
styles,
|
|
1911
|
+
useColor
|
|
1912
|
+
}));
|
|
1913
|
+
}
|
|
1914
|
+
} else {
|
|
1915
|
+
const luauSnippet = getSourceSnippet({
|
|
1916
|
+
context: 2,
|
|
1917
|
+
filePath: loc.luauPath,
|
|
1918
|
+
line: loc.luauLine
|
|
1919
|
+
});
|
|
1920
|
+
if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
|
|
1921
|
+
styles,
|
|
1922
|
+
useColor
|
|
1923
|
+
}));
|
|
1924
|
+
}
|
|
1925
|
+
return snippets;
|
|
1926
|
+
}
|
|
1927
|
+
function formatSnapshotCallSnippet(filePath, styles, useColor) {
|
|
1928
|
+
if (!fs$1.existsSync(filePath)) return [];
|
|
1929
|
+
const content = fs$1.readFileSync(filePath, "utf-8");
|
|
1930
|
+
const snapshotIndices = content.split("\n").reduce((accumulator, fileLine, index) => {
|
|
1931
|
+
if (fileLine.includes("toMatchSnapshot")) accumulator.push(index);
|
|
1932
|
+
return accumulator;
|
|
1933
|
+
}, []);
|
|
1934
|
+
if (snapshotIndices.length !== 1) return [];
|
|
1935
|
+
const snippet = getSourceSnippet({
|
|
1936
|
+
context: 2,
|
|
1937
|
+
filePath,
|
|
1938
|
+
line: snapshotIndices[0] + 1,
|
|
1939
|
+
sourceContent: content
|
|
1940
|
+
});
|
|
1941
|
+
if (snippet === void 0) return [];
|
|
1942
|
+
return ["", formatSourceSnippet(snippet, filePath, {
|
|
1943
|
+
styles,
|
|
1944
|
+
useColor
|
|
1945
|
+
})];
|
|
1946
|
+
}
|
|
1947
|
+
function resolveSourceSnippets(options) {
|
|
1948
|
+
const { filePath, hasSnapshotDiff, mappedLocations, message, showLuau, sourceMapper, styles, useColor } = options;
|
|
1949
|
+
if (mappedLocations.length > 0) return mappedLocations.flatMap((loc) => {
|
|
1950
|
+
return formatMappedLocationSnippets(loc, showLuau, styles, useColor);
|
|
1951
|
+
});
|
|
1952
|
+
const fallback = formatFallbackSnippet(message, styles, useColor);
|
|
1953
|
+
if (fallback.length > 0) return fallback;
|
|
1954
|
+
if (hasSnapshotDiff && filePath !== void 0) return formatSnapshotCallSnippet(resolveDisplayPath(filePath, sourceMapper), styles, useColor);
|
|
1955
|
+
return [];
|
|
1956
|
+
}
|
|
1957
|
+
function formatFailureMessage(originalMessage, options) {
|
|
1958
|
+
const { filePath, showLuau, sourceMapper, styles, useColor } = options;
|
|
1959
|
+
let mappedLocations = [];
|
|
1960
|
+
let message = originalMessage;
|
|
1961
|
+
if (sourceMapper !== void 0) ({locations: mappedLocations, message} = sourceMapper.mapFailureWithLocations(originalMessage));
|
|
1962
|
+
const parsed = parseErrorMessage(originalMessage);
|
|
1963
|
+
return [
|
|
1964
|
+
formatErrorLine(parsed, styles, useColor),
|
|
1965
|
+
...formatDiffBlock(parsed, styles),
|
|
1966
|
+
...resolveSourceSnippets({
|
|
1967
|
+
filePath,
|
|
1968
|
+
hasSnapshotDiff: parsed.snapshotDiff !== void 0,
|
|
1969
|
+
mappedLocations,
|
|
1970
|
+
message,
|
|
1971
|
+
showLuau,
|
|
1972
|
+
sourceMapper,
|
|
1973
|
+
styles,
|
|
1974
|
+
useColor
|
|
1975
|
+
})
|
|
1976
|
+
];
|
|
1977
|
+
}
|
|
1978
|
+
function formatSnapshotLine(snapshot, styles) {
|
|
1979
|
+
if (snapshot === void 0 || snapshot.unmatched === 0) return;
|
|
1980
|
+
return `${styles.dim(" Snapshots")} ${styles.summary.failed(`${snapshot.unmatched} failed`)}`;
|
|
1981
|
+
}
|
|
1607
1982
|
//#endregion
|
|
1608
1983
|
//#region src/formatters/compact.ts
|
|
1609
|
-
function formatCompactSummary(result) {
|
|
1610
|
-
const parts = [];
|
|
1611
|
-
const execErrorCount = result.testResults.filter(hasExecError).length;
|
|
1612
|
-
const failCount = result.numFailedTests + execErrorCount;
|
|
1613
|
-
if (result.numPassedTests > 0) parts.push(`PASS ${result.numPassedTests}`);
|
|
1614
|
-
if (failCount > 0) parts.push(`FAIL ${failCount}`);
|
|
1615
|
-
if (result.numPendingTests > 0) parts.push(`SKIP ${result.numPendingTests}`);
|
|
1616
|
-
return parts.join(" | ");
|
|
1617
|
-
}
|
|
1618
1984
|
function formatCompact(result, options) {
|
|
1619
|
-
const lines = [
|
|
1985
|
+
const lines = [];
|
|
1620
1986
|
const execErrors = result.testResults.filter(hasExecError);
|
|
1621
1987
|
if (result.numFailedTests > 0 || execErrors.length > 0) {
|
|
1622
|
-
lines.push("");
|
|
1988
|
+
lines.push(...formatFileHeaders(result, options), "");
|
|
1989
|
+
const totalFailures = result.numFailedTests + execErrors.length;
|
|
1990
|
+
lines.push(`${"⎯".repeat(3)} Failed Tests ${totalFailures} ${"⎯".repeat(3)}`, "");
|
|
1623
1991
|
if (result.numFailedTests > 0) {
|
|
1624
|
-
const failureLines = formatFailures(result, options);
|
|
1992
|
+
const failureLines = formatFailures(result, result.numFailedTests, options);
|
|
1625
1993
|
lines.push(...failureLines);
|
|
1626
1994
|
}
|
|
1627
|
-
for (const file of execErrors)
|
|
1628
|
-
const relativePath = makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir);
|
|
1629
|
-
assert(file.failureMessage !== void 0, "exec error files have failureMessage");
|
|
1630
|
-
const errorMessage = cleanExecErrorMessage(file.failureMessage);
|
|
1631
|
-
lines.push(`[FAIL] ${relativePath} - suite failed to run`, errorMessage);
|
|
1632
|
-
const hint = getExecErrorHint(errorMessage);
|
|
1633
|
-
if (hint !== void 0) lines.push(`Hint: ${hint}`);
|
|
1634
|
-
lines.push("");
|
|
1635
|
-
}
|
|
1995
|
+
for (const file of execErrors) lines.push(...formatExecError(file, options));
|
|
1636
1996
|
const hints = formatCompactLogHints(options);
|
|
1637
1997
|
if (hints !== "") lines.push(hints);
|
|
1638
1998
|
}
|
|
1999
|
+
lines.push(...formatSummarySection(result, options));
|
|
1639
2000
|
return lines.join("\n");
|
|
1640
2001
|
}
|
|
1641
|
-
function
|
|
2002
|
+
function formatCompactMultiProject(projects, options) {
|
|
1642
2003
|
const lines = [];
|
|
1643
|
-
|
|
1644
|
-
|
|
2004
|
+
for (const { displayName, result } of projects) lines.push(...formatCompactProjectHeader(displayName, result, options));
|
|
2005
|
+
const stats = collectMultiProjectStats(projects);
|
|
2006
|
+
if (stats.totalFailed + stats.allExecErrors.length > 0) lines.push(...formatMultiProjectFailures(projects, stats, options));
|
|
2007
|
+
lines.push(...formatMultiProjectSummary(stats, options));
|
|
1645
2008
|
return lines.join("\n");
|
|
1646
2009
|
}
|
|
2010
|
+
function formatTypeErrorLabel(count) {
|
|
2011
|
+
if (count === 0) return "no errors";
|
|
2012
|
+
return `${count} error${count === 1 ? "" : "s"}`;
|
|
2013
|
+
}
|
|
2014
|
+
function formatSummarySection(result, options) {
|
|
2015
|
+
const lines = [];
|
|
2016
|
+
const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
|
|
2017
|
+
const passedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && !hasExecError(file)).length;
|
|
2018
|
+
const totalFiles = failedFiles + passedFiles;
|
|
2019
|
+
const fileParts = [];
|
|
2020
|
+
if (failedFiles > 0) fileParts.push(`${failedFiles} failed`);
|
|
2021
|
+
if (passedFiles > 0) fileParts.push(`${passedFiles} passed`);
|
|
2022
|
+
lines.push(` Test Files ${fileParts.join(" | ")} (${totalFiles})`);
|
|
2023
|
+
const testParts = [];
|
|
2024
|
+
if (result.numFailedTests > 0) testParts.push(`${result.numFailedTests} failed`);
|
|
2025
|
+
if (result.numPassedTests > 0) testParts.push(`${result.numPassedTests} passed`);
|
|
2026
|
+
if (result.numPendingTests > 0) testParts.push(`${result.numPendingTests} skipped`);
|
|
2027
|
+
const totalTests = result.numTotalTests;
|
|
2028
|
+
lines.push(` Tests ${testParts.join(" | ")} (${totalTests})`);
|
|
2029
|
+
if (options.typeErrorCount !== void 0) {
|
|
2030
|
+
const typeLabel = formatTypeErrorLabel(options.typeErrorCount);
|
|
2031
|
+
lines.push(`Type Errors ${typeLabel}`);
|
|
2032
|
+
}
|
|
2033
|
+
return lines;
|
|
2034
|
+
}
|
|
1647
2035
|
function makeRelative$1(filePath, rootDirectory) {
|
|
1648
|
-
|
|
2036
|
+
const normalizedPath = filePath.replaceAll("\\", "/");
|
|
2037
|
+
const normalizedRoot = rootDirectory.replaceAll("\\", "/");
|
|
2038
|
+
if (normalizedPath.startsWith(normalizedRoot)) return path.relative(normalizedRoot, normalizedPath).replaceAll("\\", "/");
|
|
1649
2039
|
return filePath;
|
|
1650
2040
|
}
|
|
2041
|
+
function formatFileHeaderExecError(file, options) {
|
|
2042
|
+
return [` ❯ ${makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir)} (suite failed to run)`];
|
|
2043
|
+
}
|
|
2044
|
+
function formatFileHeaderFailures(file, options) {
|
|
2045
|
+
const lines = [];
|
|
2046
|
+
const relativePath = makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir);
|
|
2047
|
+
const totalTests = file.numFailingTests + file.numPassingTests + file.numPendingTests;
|
|
2048
|
+
const testWord = totalTests === 1 ? "test" : "tests";
|
|
2049
|
+
lines.push(` ❯ ${relativePath} (${totalTests} ${testWord} | ${file.numFailingTests} failed)`);
|
|
2050
|
+
for (const test of file.testResults) if (test.status === "failed") {
|
|
2051
|
+
const duration = test.duration !== void 0 ? ` ${String(test.duration)}ms` : "";
|
|
2052
|
+
lines.push(` × ${test.title}${duration}`);
|
|
2053
|
+
}
|
|
2054
|
+
return lines;
|
|
2055
|
+
}
|
|
2056
|
+
function formatFileHeaders(result, options) {
|
|
2057
|
+
const lines = [];
|
|
2058
|
+
for (const file of result.testResults) {
|
|
2059
|
+
if (hasExecError(file)) {
|
|
2060
|
+
lines.push(...formatFileHeaderExecError(file, options));
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
if (file.numFailingTests === 0) continue;
|
|
2064
|
+
lines.push(...formatFileHeaderFailures(file, options));
|
|
2065
|
+
}
|
|
2066
|
+
return lines;
|
|
2067
|
+
}
|
|
2068
|
+
function formatExecError(file, options) {
|
|
2069
|
+
const lines = [];
|
|
2070
|
+
const relativePath = makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir);
|
|
2071
|
+
assert(file.failureMessage !== void 0, "exec error files have failureMessage");
|
|
2072
|
+
const errorMessage = cleanExecErrorMessage(file.failureMessage);
|
|
2073
|
+
lines.push(` FAIL ${relativePath}`, errorMessage);
|
|
2074
|
+
const hint = getExecErrorHint(errorMessage);
|
|
2075
|
+
if (hint !== void 0) lines.push(`Hint: ${hint}`);
|
|
2076
|
+
lines.push("");
|
|
2077
|
+
return lines;
|
|
2078
|
+
}
|
|
2079
|
+
function formatSize(bytes) {
|
|
2080
|
+
if (bytes < 1024) return `${bytes}b`;
|
|
2081
|
+
return `${Math.round(bytes / 1024)}kb`;
|
|
2082
|
+
}
|
|
2083
|
+
function formatCompactLogHints(options) {
|
|
2084
|
+
const lines = [];
|
|
2085
|
+
if (options.outputFile !== void 0) {
|
|
2086
|
+
const size = options.outputFileSize !== void 0 ? ` (${formatSize(options.outputFileSize)})` : "";
|
|
2087
|
+
lines.push(`View ${options.outputFile} for full Jest output${size}`);
|
|
2088
|
+
}
|
|
2089
|
+
if (options.gameOutput !== void 0) {
|
|
2090
|
+
const size = options.gameOutputSize !== void 0 ? ` (${formatSize(options.gameOutputSize)})` : "";
|
|
2091
|
+
lines.push(`View ${options.gameOutput} for Roblox game logs${size}`);
|
|
2092
|
+
}
|
|
2093
|
+
return lines.join("\n");
|
|
2094
|
+
}
|
|
1651
2095
|
function collectFailedTests(result, sourceMapper) {
|
|
1652
2096
|
const failures = [];
|
|
1653
2097
|
for (const file of result.testResults) {
|
|
@@ -1659,6 +2103,11 @@ function collectFailedTests(result, sourceMapper) {
|
|
|
1659
2103
|
}
|
|
1660
2104
|
return failures;
|
|
1661
2105
|
}
|
|
2106
|
+
function getSnippetLevel(totalFailures) {
|
|
2107
|
+
if (totalFailures <= 2) return "both";
|
|
2108
|
+
if (totalFailures <= 5) return "ts-only";
|
|
2109
|
+
return "none";
|
|
2110
|
+
}
|
|
1662
2111
|
function findFailureLocation(mappedLocations, message) {
|
|
1663
2112
|
if (mappedLocations.length > 0) {
|
|
1664
2113
|
const loc = mappedLocations[0];
|
|
@@ -1674,37 +2123,74 @@ function findFailureLocation(mappedLocations, message) {
|
|
|
1674
2123
|
}
|
|
1675
2124
|
return parseSourceLocation(message);
|
|
1676
2125
|
}
|
|
1677
|
-
function
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
2126
|
+
function formatSnippetBlock(snippetResult) {
|
|
2127
|
+
if (snippetResult === void 0) return;
|
|
2128
|
+
const lines = [];
|
|
2129
|
+
for (const line of snippetResult.lines) {
|
|
2130
|
+
const prefix = line.num === snippetResult.failureLine ? ">" : " ";
|
|
2131
|
+
lines.push(`${prefix} ${line.num}| ${line.content}`);
|
|
2132
|
+
}
|
|
2133
|
+
return lines.join("\n");
|
|
2134
|
+
}
|
|
2135
|
+
function getTsSnippets(loc, snippetLevel, rootDirectory) {
|
|
2136
|
+
assert(loc.tsPath !== void 0 && loc.tsLine !== void 0, "caller checked ts fields");
|
|
2137
|
+
const result = [];
|
|
2138
|
+
const tsSnippet = formatSnippetBlock(getSourceSnippet({
|
|
2139
|
+
column: loc.tsColumn,
|
|
2140
|
+
context: 1,
|
|
2141
|
+
filePath: loc.tsPath,
|
|
2142
|
+
line: loc.tsLine,
|
|
2143
|
+
sourceContent: loc.sourceContent
|
|
2144
|
+
}));
|
|
2145
|
+
if (tsSnippet !== void 0) {
|
|
2146
|
+
const relativeTsPath = makeRelative$1(loc.tsPath, rootDirectory);
|
|
2147
|
+
const label = snippetLevel === "both" ? `TS ${relativeTsPath}:${loc.tsLine}\n` : "";
|
|
2148
|
+
result.push(`${label}${tsSnippet}`);
|
|
2149
|
+
}
|
|
2150
|
+
if (snippetLevel === "both") {
|
|
2151
|
+
const luauSnippet = formatSnippetBlock(getSourceSnippet({
|
|
1690
2152
|
context: 1,
|
|
1691
2153
|
filePath: loc.luauPath,
|
|
1692
2154
|
line: loc.luauLine
|
|
1693
|
-
});
|
|
1694
|
-
|
|
2155
|
+
}));
|
|
2156
|
+
if (luauSnippet !== void 0) {
|
|
2157
|
+
const relativeLuauPath = makeRelative$1(loc.luauPath, rootDirectory);
|
|
2158
|
+
result.push(`Luau ${relativeLuauPath}:${loc.luauLine}\n${luauSnippet}`);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return result;
|
|
2162
|
+
}
|
|
2163
|
+
function getLuauOnlySnippet(loc) {
|
|
2164
|
+
const snippet = formatSnippetBlock(getSourceSnippet({
|
|
2165
|
+
context: 1,
|
|
2166
|
+
filePath: loc.luauPath,
|
|
2167
|
+
line: loc.luauLine
|
|
2168
|
+
}));
|
|
2169
|
+
return snippet !== void 0 ? [snippet] : [];
|
|
2170
|
+
}
|
|
2171
|
+
function getMappedSnippets(loc, snippetLevel, rootDirectory) {
|
|
2172
|
+
if (loc.tsPath !== void 0 && loc.tsLine !== void 0) return getTsSnippets(loc, snippetLevel, rootDirectory);
|
|
2173
|
+
return getLuauOnlySnippet(loc);
|
|
2174
|
+
}
|
|
2175
|
+
function getFallbackSnippet(location) {
|
|
2176
|
+
const snippet = formatSnippetBlock(getSourceSnippet({
|
|
1695
2177
|
context: 1,
|
|
1696
2178
|
filePath: location.path,
|
|
1697
2179
|
line: location.line
|
|
1698
|
-
});
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2180
|
+
}));
|
|
2181
|
+
return snippet !== void 0 ? [snippet] : [];
|
|
2182
|
+
}
|
|
2183
|
+
function getFailureSnippets(mappedLocations, location, snippetLevel, rootDirectory) {
|
|
2184
|
+
if (snippetLevel === "none") return [];
|
|
2185
|
+
if (mappedLocations.length > 0) {
|
|
2186
|
+
const loc = mappedLocations[0];
|
|
2187
|
+
assert(loc !== void 0, "array with length > 0 has element 0");
|
|
2188
|
+
return getMappedSnippets(loc, snippetLevel, rootDirectory);
|
|
1704
2189
|
}
|
|
1705
|
-
return
|
|
2190
|
+
if (location !== void 0) return getFallbackSnippet(location);
|
|
2191
|
+
return [];
|
|
1706
2192
|
}
|
|
1707
|
-
function formatCompactFailure(test, filePath, options) {
|
|
2193
|
+
function formatCompactFailure(test, filePath, options, snippetLevel) {
|
|
1708
2194
|
const lines = [];
|
|
1709
2195
|
for (const originalMessage of test.failureMessages) {
|
|
1710
2196
|
let mappedLocations = [];
|
|
@@ -1714,28 +2200,97 @@ function formatCompactFailure(test, filePath, options) {
|
|
|
1714
2200
|
const location = findFailureLocation(mappedLocations, message);
|
|
1715
2201
|
const relativePath = makeRelative$1(location?.path ?? filePath, options.rootDir);
|
|
1716
2202
|
const lineInfo = location?.line !== void 0 ? `:${location.line}` : "";
|
|
1717
|
-
|
|
2203
|
+
const ancestors = test.ancestorTitles.length > 0 ? ` > ${test.ancestorTitles.join(" > ")}` : "";
|
|
2204
|
+
lines.push(` FAIL ${relativePath}${lineInfo}${ancestors} > ${test.title}`);
|
|
1718
2205
|
if (parsed.snapshotDiff !== void 0) lines.push(parsed.snapshotDiff);
|
|
1719
|
-
else if (parsed.expected !== void 0 && parsed.received !== void 0) lines.push(`
|
|
1720
|
-
const
|
|
1721
|
-
|
|
2206
|
+
else if (parsed.expected !== void 0 && parsed.received !== void 0) lines.push(`Expected: ${parsed.expected}`, `Received: ${parsed.received}`);
|
|
2207
|
+
const snippets = getFailureSnippets(mappedLocations, location, snippetLevel, options.rootDir);
|
|
2208
|
+
for (const snippet of snippets) lines.push(snippet);
|
|
1722
2209
|
lines.push("");
|
|
1723
2210
|
}
|
|
1724
2211
|
return lines.join("\n");
|
|
1725
2212
|
}
|
|
1726
|
-
function formatFailures(result, options) {
|
|
2213
|
+
function formatFailures(result, totalFailures, options) {
|
|
1727
2214
|
const lines = [];
|
|
1728
2215
|
const failures = collectFailedTests(result, options.sourceMapper);
|
|
2216
|
+
const snippetLevel = getSnippetLevel(totalFailures);
|
|
1729
2217
|
for (const [index, { filePath, test }] of failures.entries()) {
|
|
1730
2218
|
if (index >= options.maxFailures) {
|
|
1731
|
-
lines.push(
|
|
2219
|
+
lines.push(`... ${result.numFailedTests - index} more failures omitted`, "");
|
|
1732
2220
|
break;
|
|
1733
2221
|
}
|
|
1734
|
-
lines.push(formatCompactFailure(test, filePath, options));
|
|
2222
|
+
lines.push(formatCompactFailure(test, filePath, options, snippetLevel));
|
|
1735
2223
|
}
|
|
1736
2224
|
return lines;
|
|
1737
2225
|
}
|
|
1738
|
-
|
|
2226
|
+
function formatCompactProjectHeader(displayName, result, options) {
|
|
2227
|
+
const execErrors = result.testResults.filter(hasExecError);
|
|
2228
|
+
const hasFailures = result.numFailedTests > 0 || execErrors.length > 0;
|
|
2229
|
+
const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
|
|
2230
|
+
const skippedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && file.numPassingTests === 0 && !hasExecError(file)).length;
|
|
2231
|
+
const passedFiles = result.testResults.length - failedFiles - skippedFiles;
|
|
2232
|
+
const fileParts = [];
|
|
2233
|
+
if (passedFiles > 0) fileParts.push(`${passedFiles} passed`);
|
|
2234
|
+
if (failedFiles > 0) fileParts.push(`${failedFiles} failed`);
|
|
2235
|
+
if (skippedFiles > 0) fileParts.push(`${skippedFiles} skipped`);
|
|
2236
|
+
const lines = [`▶ ${displayName} ${fileParts.join(" | ")} (${result.numTotalTests} tests)`];
|
|
2237
|
+
if (hasFailures) lines.push(...formatFileHeaders(result, options));
|
|
2238
|
+
return lines;
|
|
2239
|
+
}
|
|
2240
|
+
function collectMultiProjectStats(projects) {
|
|
2241
|
+
const stats = {
|
|
2242
|
+
allExecErrors: [],
|
|
2243
|
+
totalFailed: 0,
|
|
2244
|
+
totalFailedFiles: 0,
|
|
2245
|
+
totalPassed: 0,
|
|
2246
|
+
totalPassedFiles: 0,
|
|
2247
|
+
totalPending: 0,
|
|
2248
|
+
totalSkippedFiles: 0,
|
|
2249
|
+
totalTests: 0
|
|
2250
|
+
};
|
|
2251
|
+
for (const { result } of projects) {
|
|
2252
|
+
const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
|
|
2253
|
+
const skippedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && file.numPassingTests === 0 && !hasExecError(file)).length;
|
|
2254
|
+
stats.totalFailed += result.numFailedTests;
|
|
2255
|
+
stats.totalPassed += result.numPassedTests;
|
|
2256
|
+
stats.totalPending += result.numPendingTests;
|
|
2257
|
+
stats.totalTests += result.numTotalTests;
|
|
2258
|
+
stats.totalFailedFiles += failedFiles;
|
|
2259
|
+
stats.totalSkippedFiles += skippedFiles;
|
|
2260
|
+
stats.totalPassedFiles += result.testResults.length - failedFiles - skippedFiles;
|
|
2261
|
+
stats.allExecErrors.push(...result.testResults.filter(hasExecError));
|
|
2262
|
+
}
|
|
2263
|
+
return stats;
|
|
2264
|
+
}
|
|
2265
|
+
function formatMultiProjectFailures(projects, stats, options) {
|
|
2266
|
+
const totalFailures = stats.totalFailed + stats.allExecErrors.length;
|
|
2267
|
+
const lines = [
|
|
2268
|
+
"",
|
|
2269
|
+
`${"⎯".repeat(3)} Failed Tests ${totalFailures} ${"⎯".repeat(3)}`,
|
|
2270
|
+
""
|
|
2271
|
+
];
|
|
2272
|
+
for (const { result } of projects) if (result.numFailedTests > 0) lines.push(...formatFailures(result, totalFailures, options));
|
|
2273
|
+
for (const file of stats.allExecErrors) lines.push(...formatExecError(file, options));
|
|
2274
|
+
const hints = formatCompactLogHints(options);
|
|
2275
|
+
if (hints !== "") lines.push(hints);
|
|
2276
|
+
return lines;
|
|
2277
|
+
}
|
|
2278
|
+
function formatMultiProjectSummary(stats, options) {
|
|
2279
|
+
const lines = [];
|
|
2280
|
+
const fileParts = [];
|
|
2281
|
+
if (stats.totalFailedFiles > 0) fileParts.push(`${stats.totalFailedFiles} failed`);
|
|
2282
|
+
if (stats.totalPassedFiles > 0) fileParts.push(`${stats.totalPassedFiles} passed`);
|
|
2283
|
+
if (stats.totalSkippedFiles > 0) fileParts.push(`${stats.totalSkippedFiles} skipped`);
|
|
2284
|
+
const totalFiles = stats.totalFailedFiles + stats.totalPassedFiles + stats.totalSkippedFiles;
|
|
2285
|
+
lines.push(` Test Files ${fileParts.join(" | ")} (${totalFiles})`);
|
|
2286
|
+
const testParts = [];
|
|
2287
|
+
if (stats.totalFailed > 0) testParts.push(`${stats.totalFailed} failed`);
|
|
2288
|
+
if (stats.totalPassed > 0) testParts.push(`${stats.totalPassed} passed`);
|
|
2289
|
+
if (stats.totalPending > 0) testParts.push(`${stats.totalPending} skipped`);
|
|
2290
|
+
lines.push(` Tests ${testParts.join(" | ")} (${stats.totalTests})`);
|
|
2291
|
+
if (options.typeErrorCount !== void 0) lines.push(`Type Errors ${formatTypeErrorLabel(options.typeErrorCount)}`);
|
|
2292
|
+
return lines;
|
|
2293
|
+
}
|
|
1739
2294
|
//#endregion
|
|
1740
2295
|
//#region src/formatters/json.ts
|
|
1741
2296
|
function formatJson(result) {
|
|
@@ -1744,21 +2299,37 @@ function formatJson(result) {
|
|
|
1744
2299
|
async function writeJsonFile(result, filePath) {
|
|
1745
2300
|
const absolutePath = path$1.resolve(filePath);
|
|
1746
2301
|
const directoryPath = path$1.dirname(absolutePath);
|
|
1747
|
-
if (!fs.existsSync(directoryPath)) fs.mkdirSync(directoryPath, { recursive: true });
|
|
1748
|
-
fs.writeFileSync(absolutePath, formatJson(result), "utf8");
|
|
2302
|
+
if (!fs$1.existsSync(directoryPath)) fs$1.mkdirSync(directoryPath, { recursive: true });
|
|
2303
|
+
fs$1.writeFileSync(absolutePath, formatJson(result), "utf8");
|
|
2304
|
+
}
|
|
2305
|
+
//#endregion
|
|
2306
|
+
//#region src/formatters/utils.ts
|
|
2307
|
+
/**
|
|
2308
|
+
* Find the options object for a named formatter in a resolved formatter list.
|
|
2309
|
+
* Returns `{}` if the formatter is present without options, or `undefined` if absent.
|
|
2310
|
+
*/
|
|
2311
|
+
function findFormatterOptions(formatters, name) {
|
|
2312
|
+
for (const entry of formatters) {
|
|
2313
|
+
if (entry === name) return {};
|
|
2314
|
+
if (Array.isArray(entry) && entry[0] === name) return entry[1];
|
|
2315
|
+
}
|
|
1749
2316
|
}
|
|
1750
|
-
|
|
1751
2317
|
//#endregion
|
|
1752
2318
|
//#region src/snapshot/path-resolver.ts
|
|
1753
2319
|
function createSnapshotPathResolver(config) {
|
|
1754
|
-
const
|
|
2320
|
+
const rojoMappings = buildMappings(config.rojoProject.tree, "");
|
|
2321
|
+
const tsconfigMappings = config.mappings ?? [];
|
|
1755
2322
|
return { resolve(virtualPath) {
|
|
1756
2323
|
const normalized = virtualPath.replaceAll("\\", "/");
|
|
1757
|
-
for (const [prefix, basePath] of
|
|
2324
|
+
for (const [prefix, basePath] of rojoMappings) {
|
|
1758
2325
|
if (!normalized.startsWith(`${prefix}/`) && normalized !== prefix) continue;
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
return
|
|
2326
|
+
const result = `${basePath}/${normalized.slice(prefix.length + 1)}`;
|
|
2327
|
+
const mapping = findMapping(result, tsconfigMappings);
|
|
2328
|
+
if (mapping !== void 0) return {
|
|
2329
|
+
filePath: replacePrefix(result, mapping.outDir, mapping.rootDir).replace(/^\.\//, ""),
|
|
2330
|
+
mapping
|
|
2331
|
+
};
|
|
2332
|
+
return { filePath: result };
|
|
1762
2333
|
}
|
|
1763
2334
|
} };
|
|
1764
2335
|
}
|
|
@@ -1774,41 +2345,76 @@ function buildMappings(tree, prefix) {
|
|
|
1774
2345
|
mappings.sort((a, b) => b[0].length - a[0].length);
|
|
1775
2346
|
return mappings;
|
|
1776
2347
|
}
|
|
1777
|
-
|
|
1778
2348
|
//#endregion
|
|
1779
2349
|
//#region src/executor.ts
|
|
1780
|
-
function isLuauProject(testFiles,
|
|
1781
|
-
if (
|
|
2350
|
+
function isLuauProject(testFiles, tsconfigMappings) {
|
|
2351
|
+
if (tsconfigMappings.length > 0) return false;
|
|
1782
2352
|
if (testFiles.some((file) => /\.tsx?$/.test(file))) return false;
|
|
1783
2353
|
return true;
|
|
1784
2354
|
}
|
|
1785
|
-
function
|
|
1786
|
-
const tsconfig = getTsconfig(projectRoot);
|
|
1787
|
-
const tsconfigDirectory = tsconfig !== null ? path$1.dirname(path$1.resolve(tsconfig.path)) : void 0;
|
|
2355
|
+
function resolveAllTsconfigMappings(projectRoot) {
|
|
1788
2356
|
const resolvedRoot = path$1.resolve(projectRoot);
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2357
|
+
let files;
|
|
2358
|
+
try {
|
|
2359
|
+
files = fs$1.readdirSync(resolvedRoot).filter((file) => /^tsconfig.*\.json$/i.test(file));
|
|
2360
|
+
} catch {
|
|
2361
|
+
return [];
|
|
2362
|
+
}
|
|
2363
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2364
|
+
const mappings = [];
|
|
2365
|
+
for (const file of files) {
|
|
2366
|
+
const compilerOptions = getTsconfig(resolvedRoot, file)?.config.compilerOptions;
|
|
2367
|
+
if (compilerOptions?.outDir === void 0) continue;
|
|
2368
|
+
const parsed = parseTsconfigMappings(compilerOptions);
|
|
2369
|
+
for (const entry of parsed) {
|
|
2370
|
+
const key = `${entry.outDir}:${entry.rootDir}`;
|
|
2371
|
+
if (!seen.has(key)) {
|
|
2372
|
+
seen.add(key);
|
|
2373
|
+
mappings.push(entry);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
mappings.sort((a, b) => b.outDir.length - a.outDir.length);
|
|
2378
|
+
return mappings;
|
|
2379
|
+
}
|
|
2380
|
+
function formatExecuteOutput(options) {
|
|
2381
|
+
const { config, result, sourceMapper, timing, version } = options;
|
|
2382
|
+
if (config.silent) return "";
|
|
2383
|
+
const resolvedOutputFile = config.outputFile !== void 0 ? path$1.resolve(config.outputFile) : void 0;
|
|
2384
|
+
const resolvedGameOutput = config.gameOutput !== void 0 ? path$1.resolve(config.gameOutput) : void 0;
|
|
2385
|
+
const agentOptions = findFormatterOptions(config.formatters ?? [], "agent");
|
|
2386
|
+
if (agentOptions !== void 0 && !config.verbose) return formatCompact(result, {
|
|
2387
|
+
gameOutput: resolvedGameOutput,
|
|
2388
|
+
maxFailures: agentOptions.maxFailures ?? 10,
|
|
2389
|
+
outputFile: resolvedOutputFile,
|
|
2390
|
+
rootDir: config.rootDir,
|
|
2391
|
+
sourceMapper
|
|
2392
|
+
});
|
|
2393
|
+
if (findFormatterOptions(config.formatters ?? [], "json") !== void 0) return formatJson(result);
|
|
2394
|
+
return formatResult(result, timing, {
|
|
2395
|
+
collectCoverage: config.collectCoverage,
|
|
2396
|
+
color: config.color,
|
|
2397
|
+
gameOutput: resolvedGameOutput,
|
|
2398
|
+
outputFile: resolvedOutputFile,
|
|
2399
|
+
rootDir: config.rootDir,
|
|
2400
|
+
showLuau: config.showLuau,
|
|
2401
|
+
sourceMapper,
|
|
2402
|
+
verbose: config.verbose,
|
|
2403
|
+
version
|
|
2404
|
+
});
|
|
1799
2405
|
}
|
|
1800
2406
|
async function execute(options) {
|
|
1801
2407
|
const startTime = Date.now();
|
|
1802
|
-
const
|
|
1803
|
-
const luauProject = isLuauProject(options.testFiles,
|
|
2408
|
+
const tsconfigMappings = resolveAllTsconfigMappings(options.config.rootDir);
|
|
2409
|
+
const luauProject = isLuauProject(options.testFiles, tsconfigMappings);
|
|
1804
2410
|
const config = applySnapshotFormatDefaults(options.config, luauProject);
|
|
1805
2411
|
const { coverageData, gameOutput, luauTiming, result, snapshotWrites, timing: backendTiming } = await options.backend.runTests({
|
|
1806
2412
|
config,
|
|
1807
2413
|
testFiles: options.testFiles
|
|
1808
2414
|
});
|
|
1809
|
-
if (snapshotWrites !== void 0) writeSnapshots(snapshotWrites, config,
|
|
2415
|
+
if (snapshotWrites !== void 0) writeSnapshots(snapshotWrites, config, tsconfigMappings);
|
|
1810
2416
|
const testsMs = calculateTestsMs(result.testResults);
|
|
1811
|
-
const sourceMapper = config.sourceMap ? buildSourceMapper(config,
|
|
2417
|
+
const sourceMapper = config.sourceMap ? buildSourceMapper(config, tsconfigMappings) : void 0;
|
|
1812
2418
|
resolveTestFilePaths(result, sourceMapper);
|
|
1813
2419
|
const totalMs = Date.now() - startTime;
|
|
1814
2420
|
const timing = {
|
|
@@ -1819,29 +2425,13 @@ async function execute(options) {
|
|
|
1819
2425
|
uploadCached: backendTiming.uploadCached,
|
|
1820
2426
|
uploadMs: backendTiming.uploadMs
|
|
1821
2427
|
};
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
if (!config.silent) if (config.compact && !config.verbose) output = formatCompact(result, {
|
|
1826
|
-
gameOutput: resolvedGameOutput,
|
|
1827
|
-
maxFailures: config.compactMaxFailures,
|
|
1828
|
-
outputFile: resolvedOutputFile,
|
|
1829
|
-
rootDir: config.rootDir,
|
|
1830
|
-
sourceMapper
|
|
1831
|
-
});
|
|
1832
|
-
else if (config.json) output = formatJson(result);
|
|
1833
|
-
else output = formatResult(result, timing, {
|
|
1834
|
-
collectCoverage: config.collectCoverage,
|
|
1835
|
-
color: config.color,
|
|
1836
|
-
failuresOnly: config.compact && config.verbose,
|
|
1837
|
-
gameOutput: resolvedGameOutput,
|
|
1838
|
-
outputFile: resolvedOutputFile,
|
|
1839
|
-
rootDir: config.rootDir,
|
|
1840
|
-
showLuau: config.showLuau,
|
|
2428
|
+
const output = options.deferFormatting !== true ? formatExecuteOutput({
|
|
2429
|
+
config,
|
|
2430
|
+
result,
|
|
1841
2431
|
sourceMapper,
|
|
1842
|
-
|
|
2432
|
+
timing,
|
|
1843
2433
|
version: options.version
|
|
1844
|
-
});
|
|
2434
|
+
}) : "";
|
|
1845
2435
|
if (luauTiming !== void 0) printLuauTiming(luauTiming);
|
|
1846
2436
|
return {
|
|
1847
2437
|
coverageData,
|
|
@@ -1849,28 +2439,46 @@ async function execute(options) {
|
|
|
1849
2439
|
gameOutput,
|
|
1850
2440
|
output,
|
|
1851
2441
|
result,
|
|
1852
|
-
sourceMapper
|
|
2442
|
+
sourceMapper,
|
|
2443
|
+
timing
|
|
1853
2444
|
};
|
|
1854
2445
|
}
|
|
1855
2446
|
function normalizeDirectoryPath(directory) {
|
|
1856
2447
|
return path$1.normalize(directory).replaceAll("\\", "/");
|
|
1857
2448
|
}
|
|
2449
|
+
function parseTsconfigMappings(options) {
|
|
2450
|
+
const outDirectory = normalizeDirectoryPath(options.outDir ?? "out");
|
|
2451
|
+
if (options.rootDirs !== void 0 && options.rootDirs.length > 0) return [{
|
|
2452
|
+
outDir: outDirectory,
|
|
2453
|
+
rootDir: options.rootDirs.map((directory) => normalizeDirectoryPath(directory)).reduce((ancestor, directory) => {
|
|
2454
|
+
const parts = ancestor.split("/");
|
|
2455
|
+
const directoryParts = directory.split("/");
|
|
2456
|
+
let common = 0;
|
|
2457
|
+
while (common < parts.length && common < directoryParts.length && parts[common] === directoryParts[common]) common++;
|
|
2458
|
+
return parts.slice(0, common).join("/");
|
|
2459
|
+
}) || "."
|
|
2460
|
+
}];
|
|
2461
|
+
if (options.rootDir === null) return [];
|
|
2462
|
+
return [{
|
|
2463
|
+
outDir: outDirectory,
|
|
2464
|
+
rootDir: normalizeDirectoryPath(options.rootDir ?? "src")
|
|
2465
|
+
}];
|
|
2466
|
+
}
|
|
1858
2467
|
function findRojoProject(rootDirectory) {
|
|
1859
2468
|
const defaultPath = path$1.join(rootDirectory, "default.project.json");
|
|
1860
|
-
if (fs.existsSync(defaultPath)) return defaultPath;
|
|
1861
|
-
const projectFile = fs.readdirSync(rootDirectory).find((file) => file.endsWith(".project.json"));
|
|
2469
|
+
if (fs$1.existsSync(defaultPath)) return defaultPath;
|
|
2470
|
+
const projectFile = fs$1.readdirSync(rootDirectory).find((file) => file.endsWith(".project.json"));
|
|
1862
2471
|
return projectFile !== void 0 ? path$1.join(rootDirectory, projectFile) : void 0;
|
|
1863
2472
|
}
|
|
1864
|
-
function buildSourceMapper(config,
|
|
2473
|
+
function buildSourceMapper(config, tsconfigMappings) {
|
|
1865
2474
|
const rojoProjectPath = config.rojoProject ?? findRojoProject(config.rootDir);
|
|
1866
|
-
if (rojoProjectPath === void 0 || !fs.existsSync(rojoProjectPath)) return;
|
|
2475
|
+
if (rojoProjectPath === void 0 || !fs$1.existsSync(rojoProjectPath)) return;
|
|
1867
2476
|
try {
|
|
1868
|
-
const rojoResult = rojoProjectSchema(JSON.parse(fs.readFileSync(rojoProjectPath, "utf-8")));
|
|
2477
|
+
const rojoResult = rojoProjectSchema(JSON.parse(fs$1.readFileSync(rojoProjectPath, "utf-8")));
|
|
1869
2478
|
if (rojoResult instanceof type.errors) return;
|
|
1870
2479
|
return createSourceMapper({
|
|
1871
|
-
|
|
1872
|
-
rojoProject: rojoResult
|
|
1873
|
-
rootDir: tsconfigDirectories.rootDir
|
|
2480
|
+
mappings: tsconfigMappings,
|
|
2481
|
+
rojoProject: rojoResult
|
|
1874
2482
|
});
|
|
1875
2483
|
} catch {
|
|
1876
2484
|
return;
|
|
@@ -1924,7 +2532,7 @@ const coverageManifestSchema = type({
|
|
|
1924
2532
|
function loadCoverageManifest(rootDirectory) {
|
|
1925
2533
|
const manifestPath = path$1.join(rootDirectory, ".jest-roblox-coverage", "manifest.json");
|
|
1926
2534
|
try {
|
|
1927
|
-
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
2535
|
+
const raw = fs$1.readFileSync(manifestPath, "utf-8");
|
|
1928
2536
|
const parsed = coverageManifestSchema(JSON.parse(raw));
|
|
1929
2537
|
if (parsed instanceof type.errors) {
|
|
1930
2538
|
process.stderr.write(`Warning: Coverage manifest is invalid (re-run \`jest-roblox instrument\`): ${parsed.summary}\n`);
|
|
@@ -1936,39 +2544,38 @@ function loadCoverageManifest(rootDirectory) {
|
|
|
1936
2544
|
return;
|
|
1937
2545
|
}
|
|
1938
2546
|
}
|
|
1939
|
-
function writeSnapshots(snapshotWrites, config,
|
|
2547
|
+
function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
|
|
1940
2548
|
const rojoProjectPath = config.rojoProject ?? findRojoProject(config.rootDir);
|
|
1941
|
-
if (rojoProjectPath === void 0 || !fs.existsSync(rojoProjectPath)) {
|
|
2549
|
+
if (rojoProjectPath === void 0 || !fs$1.existsSync(rojoProjectPath)) {
|
|
1942
2550
|
process.stderr.write("Warning: Cannot write snapshots - no rojo project found\n");
|
|
1943
2551
|
return;
|
|
1944
2552
|
}
|
|
1945
2553
|
try {
|
|
1946
|
-
const rojoResult = rojoProjectSchema(JSON.parse(fs.readFileSync(rojoProjectPath, "utf-8")));
|
|
2554
|
+
const rojoResult = rojoProjectSchema(JSON.parse(fs$1.readFileSync(rojoProjectPath, "utf-8")));
|
|
1947
2555
|
if (rojoResult instanceof type.errors) {
|
|
1948
2556
|
process.stderr.write("Warning: Cannot write snapshots - invalid rojo project\n");
|
|
1949
2557
|
return;
|
|
1950
2558
|
}
|
|
1951
|
-
const { outDir: outDirectory, rootDir: rootDirectory } = tsconfigDirectories;
|
|
1952
2559
|
const resolver = createSnapshotPathResolver({
|
|
1953
|
-
|
|
1954
|
-
rojoProject: rojoResult
|
|
1955
|
-
rootDir: rootDirectory
|
|
2560
|
+
mappings: tsconfigMappings,
|
|
2561
|
+
rojoProject: rojoResult
|
|
1956
2562
|
});
|
|
1957
2563
|
let written = 0;
|
|
1958
2564
|
for (const [virtualPath, content] of Object.entries(snapshotWrites)) {
|
|
1959
|
-
const
|
|
1960
|
-
if (
|
|
2565
|
+
const resolved = resolver.resolve(virtualPath);
|
|
2566
|
+
if (resolved === void 0) {
|
|
1961
2567
|
process.stderr.write(`Warning: Cannot resolve snapshot path: ${virtualPath}\n`);
|
|
1962
2568
|
continue;
|
|
1963
2569
|
}
|
|
1964
|
-
const absolutePath = path$1.resolve(config.rootDir,
|
|
1965
|
-
fs.mkdirSync(path$1.dirname(absolutePath), { recursive: true });
|
|
1966
|
-
fs.writeFileSync(absolutePath, content);
|
|
1967
|
-
|
|
1968
|
-
|
|
2570
|
+
const absolutePath = path$1.resolve(config.rootDir, resolved.filePath);
|
|
2571
|
+
fs$1.mkdirSync(path$1.dirname(absolutePath), { recursive: true });
|
|
2572
|
+
fs$1.writeFileSync(absolutePath, content);
|
|
2573
|
+
const { filePath, mapping } = resolved;
|
|
2574
|
+
if (mapping !== void 0) {
|
|
2575
|
+
const outPath = mapping.outDir + filePath.slice(mapping.rootDir.length);
|
|
1969
2576
|
const absoluteOutPath = path$1.resolve(config.rootDir, outPath);
|
|
1970
|
-
fs.mkdirSync(path$1.dirname(absoluteOutPath), { recursive: true });
|
|
1971
|
-
fs.writeFileSync(absoluteOutPath, content);
|
|
2577
|
+
fs$1.mkdirSync(path$1.dirname(absoluteOutPath), { recursive: true });
|
|
2578
|
+
fs$1.writeFileSync(absoluteOutPath, content);
|
|
1972
2579
|
}
|
|
1973
2580
|
written++;
|
|
1974
2581
|
}
|
|
@@ -1982,7 +2589,6 @@ function writeSnapshots(snapshotWrites, config, tsconfigDirectories) {
|
|
|
1982
2589
|
else process.stderr.write(`Warning: Failed to write snapshot files: ${String(err)}\n`);
|
|
1983
2590
|
}
|
|
1984
2591
|
}
|
|
1985
|
-
|
|
1986
2592
|
//#endregion
|
|
1987
2593
|
//#region src/formatters/github-actions.ts
|
|
1988
2594
|
const SEPARATOR = " · ";
|
|
@@ -2130,7 +2736,6 @@ function createFileLink(options) {
|
|
|
2130
2736
|
if (serverUrl === void 0 || repository === void 0 || sha === void 0) return (_filePath) => {};
|
|
2131
2737
|
return (filePath) => `${serverUrl}/${repository}/blob/${sha}/${filePath}`;
|
|
2132
2738
|
}
|
|
2133
|
-
|
|
2134
2739
|
//#endregion
|
|
2135
2740
|
//#region src/typecheck/collect.ts
|
|
2136
2741
|
const TEST_FUNCTIONS = new Set(["it", "test"]);
|
|
@@ -2198,7 +2803,6 @@ function extractDefinition(node, source) {
|
|
|
2198
2803
|
type: TEST_FUNCTIONS.has(name) ? "test" : "suite"
|
|
2199
2804
|
};
|
|
2200
2805
|
}
|
|
2201
|
-
|
|
2202
2806
|
//#endregion
|
|
2203
2807
|
//#region src/typecheck/parse.ts
|
|
2204
2808
|
const errorCodeRegExp = /error TS(?<errorCode>\d+)/;
|
|
@@ -2242,7 +2846,6 @@ function parseTscOutput(stdout) {
|
|
|
2242
2846
|
}
|
|
2243
2847
|
return map;
|
|
2244
2848
|
}
|
|
2245
|
-
|
|
2246
2849
|
//#endregion
|
|
2247
2850
|
//#region src/typecheck/runner.ts
|
|
2248
2851
|
function createLocationsIndexMap(source) {
|
|
@@ -2260,7 +2863,7 @@ function createLocationsIndexMap(source) {
|
|
|
2260
2863
|
}
|
|
2261
2864
|
return map;
|
|
2262
2865
|
}
|
|
2263
|
-
function mapErrorsToTests(errors, files) {
|
|
2866
|
+
function mapErrorsToTests(errors, files, startTime) {
|
|
2264
2867
|
const testResults = [];
|
|
2265
2868
|
let numberFailed = 0;
|
|
2266
2869
|
let numberPassed = 0;
|
|
@@ -2275,19 +2878,33 @@ function mapErrorsToTests(errors, files) {
|
|
|
2275
2878
|
numPassedTests: numberPassed,
|
|
2276
2879
|
numPendingTests: 0,
|
|
2277
2880
|
numTotalTests: numberFailed + numberPassed,
|
|
2278
|
-
startTime
|
|
2881
|
+
startTime,
|
|
2279
2882
|
success: numberFailed === 0,
|
|
2280
2883
|
testResults
|
|
2281
2884
|
};
|
|
2282
2885
|
}
|
|
2886
|
+
function isCompositeProject(rootDirectory, tsconfig) {
|
|
2887
|
+
const tsconfigPath = tsconfig !== void 0 ? path$1.resolve(rootDirectory, tsconfig) : path$1.join(rootDirectory, "tsconfig.json");
|
|
2888
|
+
try {
|
|
2889
|
+
return parseJSONC(fs$1.readFileSync(tsconfigPath, "utf-8"))["compilerOptions"]?.["composite"] === true;
|
|
2890
|
+
} catch (err) {
|
|
2891
|
+
if (tsconfig !== void 0) {
|
|
2892
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2893
|
+
process.stderr.write(`Warning: could not read tsconfig "${tsconfigPath}": ${message}\n`);
|
|
2894
|
+
}
|
|
2895
|
+
return false;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2283
2898
|
function runTypecheck(options) {
|
|
2899
|
+
const startTime = Date.now();
|
|
2284
2900
|
const errors = parseTscOutput(spawnTsgo(options));
|
|
2285
2901
|
const files = /* @__PURE__ */ new Map();
|
|
2286
2902
|
for (const filePath of options.files) {
|
|
2287
|
-
const source = fs.readFileSync(filePath, "utf-8");
|
|
2903
|
+
const source = fs$1.readFileSync(filePath, "utf-8");
|
|
2288
2904
|
const definitions = collectTestDefinitions(source);
|
|
2289
2905
|
const resolvedPath = path$1.resolve(filePath);
|
|
2290
|
-
|
|
2906
|
+
const key = normalizeWindowsPath(path$1.relative(options.rootDir, resolvedPath));
|
|
2907
|
+
files.set(key, {
|
|
2291
2908
|
definitions,
|
|
2292
2909
|
source
|
|
2293
2910
|
});
|
|
@@ -2295,9 +2912,10 @@ function runTypecheck(options) {
|
|
|
2295
2912
|
const resolvedErrors = /* @__PURE__ */ new Map();
|
|
2296
2913
|
for (const [errorPath, errorList] of errors) {
|
|
2297
2914
|
const resolved = path$1.resolve(options.rootDir, errorPath);
|
|
2298
|
-
|
|
2915
|
+
const key = normalizeWindowsPath(path$1.relative(options.rootDir, resolved));
|
|
2916
|
+
resolvedErrors.set(key, errorList);
|
|
2299
2917
|
}
|
|
2300
|
-
return mapErrorsToTests(resolvedErrors, files);
|
|
2918
|
+
return mapErrorsToTests(resolvedErrors, files, startTime);
|
|
2301
2919
|
}
|
|
2302
2920
|
function buildFileResult(filePath, fileInfo, errors) {
|
|
2303
2921
|
const indexMap = createLocationsIndexMap(fileInfo.source);
|
|
@@ -2341,16 +2959,27 @@ function buildFileResult(filePath, fileInfo, errors) {
|
|
|
2341
2959
|
testResults: testCases
|
|
2342
2960
|
};
|
|
2343
2961
|
}
|
|
2962
|
+
function isExecSyncError(err) {
|
|
2963
|
+
return typeof err === "object" && err !== null && ("stdout" in err || "stderr" in err);
|
|
2964
|
+
}
|
|
2965
|
+
function resolveTsgoScript() {
|
|
2966
|
+
const packageJsonPath = createRequire(import.meta.url).resolve("@typescript/native-preview/package.json");
|
|
2967
|
+
return path$1.join(path$1.dirname(packageJsonPath), "bin", "tsgo.js");
|
|
2968
|
+
}
|
|
2344
2969
|
function spawnTsgo(options) {
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2970
|
+
const composite = isCompositeProject(options.rootDir, options.tsconfig);
|
|
2971
|
+
const args = [];
|
|
2972
|
+
if (composite) args.push("--build", "--emitDeclarationOnly");
|
|
2973
|
+
else args.push("--noEmit");
|
|
2974
|
+
args.push("--pretty", "false");
|
|
2975
|
+
if (options.tsconfig !== void 0) {
|
|
2976
|
+
const resolvedTsconfig = path$1.resolve(options.rootDir, options.tsconfig);
|
|
2977
|
+
if (composite) args.push(resolvedTsconfig);
|
|
2978
|
+
else args.push("-p", resolvedTsconfig);
|
|
2979
|
+
}
|
|
2980
|
+
const tsgoScript = resolveTsgoScript();
|
|
2352
2981
|
try {
|
|
2353
|
-
return
|
|
2982
|
+
return execFileSync(process.execPath, [tsgoScript, ...args], {
|
|
2354
2983
|
cwd: options.rootDir,
|
|
2355
2984
|
encoding: "utf-8",
|
|
2356
2985
|
stdio: [
|
|
@@ -2360,11 +2989,10 @@ function spawnTsgo(options) {
|
|
|
2360
2989
|
]
|
|
2361
2990
|
});
|
|
2362
2991
|
} catch (err) {
|
|
2363
|
-
|
|
2364
|
-
return
|
|
2992
|
+
if (!isExecSyncError(err)) throw err;
|
|
2993
|
+
return err.stdout ?? err.stderr ?? "";
|
|
2365
2994
|
}
|
|
2366
2995
|
}
|
|
2367
|
-
|
|
2368
2996
|
//#endregion
|
|
2369
2997
|
//#region src/utils/game-output.ts
|
|
2370
2998
|
function formatGameOutputNotice(filePath, entryCount) {
|
|
@@ -2384,9 +3012,8 @@ function parseGameOutput(raw) {
|
|
|
2384
3012
|
function writeGameOutput(filePath, entries) {
|
|
2385
3013
|
const absolutePath = path$1.resolve(filePath);
|
|
2386
3014
|
const directoryPath = path$1.dirname(absolutePath);
|
|
2387
|
-
if (!fs.existsSync(directoryPath)) fs.mkdirSync(directoryPath, { recursive: true });
|
|
2388
|
-
fs.writeFileSync(absolutePath, JSON.stringify(entries, null, 2));
|
|
3015
|
+
if (!fs$1.existsSync(directoryPath)) fs$1.mkdirSync(directoryPath, { recursive: true });
|
|
3016
|
+
fs$1.writeFileSync(absolutePath, JSON.stringify(entries, null, 2));
|
|
2389
3017
|
}
|
|
2390
|
-
|
|
2391
3018
|
//#endregion
|
|
2392
|
-
export {
|
|
3019
|
+
export { generateTestScript as A, resolveConfig as C, createOpenCloudBackend as D, OpenCloudBackend as E, defineProject as F, isValidBackend as I, LuauScriptError as L, ROOT_ONLY_KEYS as M, VALID_BACKENDS as N, hashBuffer as O, defineConfig as P, extractJsonFromOutput as R, loadConfig$1 as S, createStudioBackend as T, formatResult as _, formatAnnotations as a, formatBanner as b, execute as c, findFormatterOptions as d, formatJson as f, formatMultiProjectResult as g, formatFailure as h, runTypecheck as i, DEFAULT_CONFIG as j, buildJestArgv as k, formatExecuteOutput as l, formatCompactMultiProject as m, parseGameOutput as n, formatJobSummary as o, writeJsonFile as p, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, loadCoverageManifest as u, formatTestSummary as v, StudioBackend as w, rojoProjectSchema as x, formatTypecheckSummary as y, parseJestOutput as z };
|