@isentinel/jest-roblox 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -227,6 +227,11 @@ Connects to Roblox Studio over WebSocket. Faster than Open Cloud (no upload
227
227
  step), but Studio must be open with the plugin running. Studio doesn't expose which place is open, so
228
228
  multiple concurrent projects aren't supported yet.
229
229
 
230
+ > [!NOTE]
231
+ > For `--coverage`, prefer `--backend open-cloud` since the coverage output is
232
+ > built to a separate output under `.jest-roblox-coverage/` that is likely not
233
+ > the studio place being served.
234
+
230
235
  Install the plugin with [Drillbit](https://github.com/jacktabscode/drillbit):
231
236
 
232
237
  #### Configuration file
@@ -235,7 +240,7 @@ Create a file named drillbit.toml in your project's directory.
235
240
 
236
241
  ```toml
237
242
  [plugins.jest_roblox]
238
- github = "https://github.com/christopher-buss/jest-roblox-cli/releases/download/v0.2.1/JestRobloxRunner.rbxm"
243
+ github = "https://github.com/christopher-buss/jest-roblox-cli/releases/download/v0.2.4/JestRobloxRunner.rbxm"
239
244
  ```
240
245
 
241
246
  Then run `drillbit` and it will download the plugin and install it in Studio for you.
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as formatBanner, C as writeJsonFile, D as formatResult, E as formatMultiProjectResult, F as createStudioBackend, G as isValidBackend, H as VALID_BACKENDS, K as LuauScriptError, L as createOpenCloudBackend, M as loadConfig$1, V as ROOT_ONLY_KEYS, _ as resolveTsconfigDirectories, a as formatAnnotations, b as rojoProjectSchema, c as visitBlock, d as buildProjectJob, f as execute, g as processProjectResult, h as loadCoverageManifest, i as runTypecheck, j as combineSourceMappers, k as formatTypecheckSummary, m as formatExecuteOutput, n as parseGameOutput, o as formatJobSummary, p as executeBackend, r as writeGameOutput, s as resolveGitHubActionsOptions, t as formatGameOutputNotice, v as collectPaths$1, w as formatAgentMultiProject, x as findFormatterOptions, y as resolveNestedProjects } from "./game-output-CwmtpYhn.mjs";
1
+ import { A as collectPaths, D as formatTypecheckSummary, H as ROOT_ONLY_KEYS, I as createStudioBackend, J as LuauScriptError, K as isValidBackend, M as resolveNestedProjects, N as loadConfig$1, O as formatBanner, R as createOpenCloudBackend, S as formatAgentMultiProject, T as formatResult, U as VALID_BACKENDS, _ as resolveTsconfigDirectories, a as formatAnnotations, c as visitBlock, d as buildProjectJob, f as execute, g as processProjectResult, h as loadCoverageManifest, i as runTypecheck, j as findInTree, k as combineSourceMappers, m as formatExecuteOutput, n as parseGameOutput, o as formatJobSummary, p as executeBackend, q as hashBuffer, r as writeGameOutput, s as resolveGitHubActionsOptions, t as formatGameOutputNotice, v as rojoProjectSchema, w as formatMultiProjectResult, x as writeJsonFile, y as findFormatterOptions } from "./game-output-BtWj32M8.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { type } from "arktype";
4
4
  import assert from "node:assert";
@@ -11,21 +11,19 @@ import { parseArgs as parseArgs$1 } from "node:util";
11
11
  import { isAgent } from "std-env";
12
12
  import color from "tinyrainbow";
13
13
  import { WebSocketServer } from "ws";
14
- import { hashBuffer as hashBuffer$1 } from "@isentinel/roblox-runner";
14
+ import * as os from "node:os";
15
+ import { Buffer } from "node:buffer";
15
16
  import { loadConfig } from "c12";
16
- import { collectPaths, findInTree } from "@isentinel/rojo-utils";
17
17
  import { getTsconfig } from "get-tsconfig";
18
18
  import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
19
19
  import * as cp from "node:child_process";
20
- import * as os from "node:os";
21
20
  import { RojoResolver } from "@roblox-ts/rojo-resolver";
22
- import { Buffer } from "node:buffer";
23
21
  import picomatch from "picomatch";
24
22
  import istanbulCoverage from "istanbul-lib-coverage";
25
23
  import istanbulReport from "istanbul-lib-report";
26
24
  import istanbulReports from "istanbul-reports";
27
25
  //#region package.json
28
- var version = "0.2.2";
26
+ var version = "0.2.4";
29
27
  //#endregion
30
28
  //#region src/backends/auto.ts
31
29
  var StudioWithFallback = class {
@@ -1473,7 +1471,7 @@ function instrumentRoot(options) {
1473
1471
  functionCount: collectorResult.functions.length,
1474
1472
  instrumentedLuauPath,
1475
1473
  originalLuauPath,
1476
- sourceHash: hashBuffer$1(sourceBuffer),
1474
+ sourceHash: hashBuffer(sourceBuffer),
1477
1475
  sourceMapPath,
1478
1476
  statementCount: collectorResult.statements.length
1479
1477
  };
@@ -1582,7 +1580,7 @@ const previousManifestSchema = type({
1582
1580
  }).as();
1583
1581
  function collectLuauRootsFromRojo(project, config) {
1584
1582
  const paths = [];
1585
- collectPaths$1(project.tree, paths);
1583
+ collectPaths(project.tree, paths);
1586
1584
  const ignorePatterns = config.coveragePathIgnorePatterns;
1587
1585
  const isIgnored = picomatch(ignorePatterns, { contains: true });
1588
1586
  return paths.filter((directoryPath) => {
@@ -1732,7 +1730,7 @@ function syncNonInstrumentedFiles(luauRoot, shadowDirectory, previousNonInstrume
1732
1730
  for (const relativePath of discovered) {
1733
1731
  const sourcePath = `${posixRoot}/${relativePath}`;
1734
1732
  const shadowPath = `${shadowDirectory}/${relativePath}`;
1735
- const currentHash = hashBuffer$1(fs$1.readFileSync(path$1.resolve(sourcePath)));
1733
+ const currentHash = hashBuffer(fs$1.readFileSync(path$1.resolve(sourcePath)));
1736
1734
  const previousRecord = previousNonInstrumented?.[sourcePath];
1737
1735
  if (previousRecord?.sourceHash === currentHash) {
1738
1736
  files[sourcePath] = previousRecord;
@@ -1762,7 +1760,7 @@ function computeSkipFiles(luauRoot, previousManifest) {
1762
1760
  const relativePath = fileKey.slice(posixRoot.length + 1);
1763
1761
  const sourcePath = path$1.resolve(record.originalLuauPath);
1764
1762
  if (!fs$1.existsSync(sourcePath)) continue;
1765
- if (hashBuffer$1(fs$1.readFileSync(sourcePath)) === record.sourceHash) skipFiles.add(relativePath);
1763
+ if (hashBuffer(fs$1.readFileSync(sourcePath)) === record.sourceHash) skipFiles.add(relativePath);
1766
1764
  }
1767
1765
  return skipFiles;
1768
1766
  }
@@ -4,15 +4,16 @@ import assert from "node:assert";
4
4
  import * as fs$1 from "node:fs";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import * as path$1 from "node:path";
7
- import path from "node:path";
7
+ import path, { dirname, join, relative } from "node:path";
8
8
  import process from "node:process";
9
9
  import color from "tinyrainbow";
10
10
  import { WebSocketServer } from "ws";
11
- import { createFetchClient, getCacheDirectory, getCacheKey, hashBuffer, isUploaded, markUploaded, readCache, writeCache } from "@isentinel/roblox-runner";
12
- import { createDefineConfig, loadConfig } from "c12";
11
+ import { homedir, tmpdir } from "node:os";
12
+ import * as crypto from "node:crypto";
13
13
  import { randomUUID } from "node:crypto";
14
+ import buffer from "node:buffer";
15
+ import { createDefineConfig, loadConfig } from "c12";
14
16
  import { defuFn } from "defu";
15
- import { collectPaths as collectPaths$1, resolveNestedProjects } from "@isentinel/rojo-utils";
16
17
  import { getTsconfig } from "get-tsconfig";
17
18
  import { TraceMap, originalPositionFor, sourceContentFor } from "@jridgewell/trace-mapping";
18
19
  import hljs from "highlight.js/lib/core";
@@ -201,6 +202,82 @@ function extractSetupSeconds(parsed) {
201
202
  return setup;
202
203
  }
203
204
  //#endregion
205
+ //#region packages/roblox-runner/dist/index.mjs
206
+ const CACHE_DIR_NAME = "jest-roblox";
207
+ function getCacheDirectory() {
208
+ const xdgCacheHome = process.env["XDG_CACHE_HOME"];
209
+ if (xdgCacheHome !== void 0 && xdgCacheHome !== "") return path$1.join(xdgCacheHome, CACHE_DIR_NAME);
210
+ if (process.platform === "win32") {
211
+ const localAppData = process.env["LOCALAPPDATA"];
212
+ if (localAppData !== void 0 && localAppData !== "") return path$1.join(localAppData, CACHE_DIR_NAME);
213
+ return path$1.join(tmpdir(), CACHE_DIR_NAME);
214
+ }
215
+ return path$1.join(homedir(), ".cache", CACHE_DIR_NAME);
216
+ }
217
+ function getCacheKey(universeId, placeId) {
218
+ return `${universeId}:${placeId}`;
219
+ }
220
+ function isUploaded(cache, key, fileHash) {
221
+ return cache[key]?.fileHash === fileHash;
222
+ }
223
+ function markUploaded(cache, key, fileHash) {
224
+ cache[key] = {
225
+ fileHash,
226
+ uploadedAt: Date.now()
227
+ };
228
+ }
229
+ function readCache(cacheFilePath) {
230
+ try {
231
+ const data = fs$1.readFileSync(cacheFilePath, "utf-8");
232
+ return JSON.parse(data);
233
+ } catch {
234
+ return {};
235
+ }
236
+ }
237
+ function writeCache(cacheFilePath, cache) {
238
+ const cacheDirectory = path$1.dirname(cacheFilePath);
239
+ fs$1.mkdirSync(cacheDirectory, { recursive: true });
240
+ fs$1.writeFileSync(cacheFilePath, JSON.stringify(cache, null, 2));
241
+ }
242
+ function hashBuffer(data) {
243
+ return crypto.createHash("sha256").update(data).digest("hex");
244
+ }
245
+ function createFetchClient(defaultHeaders) {
246
+ return { async request(method, url, options) {
247
+ const headers = {
248
+ ...defaultHeaders,
249
+ ...options?.headers
250
+ };
251
+ const fetchOptions = {
252
+ headers,
253
+ method
254
+ };
255
+ if (options?.body !== void 0) if (options.body instanceof buffer.Buffer) fetchOptions.body = options.body;
256
+ else {
257
+ fetchOptions.body = JSON.stringify(options.body);
258
+ headers["Content-Type"] = "application/json";
259
+ }
260
+ const response = await fetch(url, fetchOptions);
261
+ return {
262
+ body: await (response.headers.get("content-type")?.includes("application/json") ?? false ? response.json() : response.text()),
263
+ headers: { "retry-after": response.headers.get("retry-after") ?? void 0 },
264
+ ok: response.ok,
265
+ status: response.status
266
+ };
267
+ } };
268
+ }
269
+ type({ path: "string" });
270
+ type({
271
+ "error?": { "message?": "string" },
272
+ "output?": { "results?": "string[]" },
273
+ "state": "'CANCELLED' | 'COMPLETE' | 'FAILED' | 'PROCESSING'"
274
+ });
275
+ type({
276
+ outputs: "string[]",
277
+ request_id: "string",
278
+ type: "'results'"
279
+ });
280
+ //#endregion
204
281
  //#region src/config/schema.ts
205
282
  const ROOT_ONLY_KEYS = new Set([
206
283
  "backend",
@@ -866,6 +943,77 @@ function resolveFunctionValues(config) {
866
943
  return resolved;
867
944
  }
868
945
  //#endregion
946
+ //#region packages/rojo-utils/dist/index.mjs
947
+ function resolveNestedProjects(tree, rootDirectory) {
948
+ return resolveTree(tree, rootDirectory, rootDirectory, /* @__PURE__ */ new Set());
949
+ }
950
+ function collectPaths(node, result) {
951
+ for (const [key, value] of Object.entries(node)) if (key === "$path" && typeof value === "string") result.push(value.replaceAll("\\", "/"));
952
+ else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) collectPaths(value, result);
953
+ }
954
+ function inlineNestedProject(projectPath, currentDirectory, originalRoot, visited) {
955
+ const chain = new Set(visited);
956
+ chain.add(projectPath);
957
+ let content;
958
+ try {
959
+ content = readFileSync(projectPath, "utf-8");
960
+ } catch (err) {
961
+ const relativePath = relative(currentDirectory, projectPath);
962
+ throw new Error(`Could not read nested Rojo project: ${relativePath}`, { cause: err });
963
+ }
964
+ let project;
965
+ try {
966
+ project = JSON.parse(content);
967
+ } catch (err) {
968
+ const relativePath = relative(currentDirectory, projectPath);
969
+ throw new Error(`Failed to parse nested Rojo project: ${relativePath}`, { cause: err });
970
+ }
971
+ return resolveTree(project.tree, dirname(projectPath), originalRoot, chain);
972
+ }
973
+ function resolveRootRelativePath(currentDirectory, value, originalRoot) {
974
+ return relative(originalRoot, join(currentDirectory, value)).replaceAll("\\", "/");
975
+ }
976
+ function resolveTree(node, currentDirectory, originalRoot, visited) {
977
+ const resolved = {};
978
+ for (const [key, value] of Object.entries(node)) {
979
+ if (key === "$path" && typeof value === "string" && value.endsWith(".project.json")) {
980
+ const projectPath = join(currentDirectory, value);
981
+ if (visited.has(projectPath)) throw new Error(`Circular project reference: ${value}`);
982
+ const innerTree = inlineNestedProject(projectPath, currentDirectory, originalRoot, visited);
983
+ for (const [innerKey, innerValue] of Object.entries(innerTree)) resolved[innerKey] = innerValue;
984
+ continue;
985
+ }
986
+ if (key === "$path" && typeof value === "string") {
987
+ resolved[key] = resolveRootRelativePath(currentDirectory, value, originalRoot);
988
+ continue;
989
+ }
990
+ if (key.startsWith("$") || typeof value !== "object" || Array.isArray(value)) {
991
+ resolved[key] = value;
992
+ continue;
993
+ }
994
+ resolved[key] = resolveTree(value, currentDirectory, originalRoot, visited);
995
+ }
996
+ return resolved;
997
+ }
998
+ function matchNodePath(childNode, targetPath, childDataModelPath) {
999
+ const nodePath = childNode.$path;
1000
+ if (typeof nodePath !== "string") return;
1001
+ const normalizedNodePath = nodePath.replace(/\/$/, "");
1002
+ if (normalizedNodePath === targetPath) return childDataModelPath;
1003
+ if (targetPath.startsWith(`${normalizedNodePath}/`)) return `${childDataModelPath}/${targetPath.slice(normalizedNodePath.length + 1)}`;
1004
+ }
1005
+ function findInTree(node, targetPath, currentDataModelPath) {
1006
+ for (const [key, value] of Object.entries(node)) {
1007
+ if (key.startsWith("$") || typeof value !== "object") continue;
1008
+ const childNode = value;
1009
+ const childDataModelPath = currentDataModelPath === "" ? key : `${currentDataModelPath}/${key}`;
1010
+ const pathMatch = matchNodePath(childNode, targetPath, childDataModelPath);
1011
+ if (pathMatch !== void 0) return pathMatch;
1012
+ const found = findInTree(childNode, targetPath, childDataModelPath);
1013
+ if (found !== void 0) return found;
1014
+ }
1015
+ }
1016
+ //#endregion
869
1017
  //#region src/utils/normalize-windows-path.ts
870
1018
  const DRIVE_LETTER_START_REGEX = /^[A-Za-z]:\//;
871
1019
  function normalizeWindowsPath(input = "") {
@@ -3456,4 +3604,4 @@ function writeGameOutput(filePath, entries) {
3456
3604
  fs$1.writeFileSync(absolutePath, JSON.stringify(entries, null, 2));
3457
3605
  }
3458
3606
  //#endregion
3459
- export { formatBanner as A, DEFAULT_CONFIG as B, writeJsonFile as C, formatResult as D, formatMultiProjectResult as E, createStudioBackend as F, isValidBackend as G, VALID_BACKENDS as H, OpenCloudBackend as I, parseJestOutput as J, LuauScriptError as K, createOpenCloudBackend as L, loadConfig$1 as M, resolveConfig as N, formatTestSummary as O, StudioBackend as P, buildJestArgv as R, formatJson as S, formatFailure as T, defineConfig as U, ROOT_ONLY_KEYS as V, defineProject as W, resolveTsconfigDirectories as _, formatAnnotations as a, rojoProjectSchema as b, visitBlock as c, buildProjectJob as d, execute as f, processProjectResult as g, loadCoverageManifest as h, runTypecheck as i, combineSourceMappers as j, formatTypecheckSummary as k, visitExpression as l, formatExecuteOutput as m, parseGameOutput as n, formatJobSummary as o, executeBackend as p, extractJsonFromOutput as q, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, visitStatement as u, collectPaths$1 as v, formatAgentMultiProject as w, findFormatterOptions as x, resolveNestedProjects as y, generateTestScript as z };
3607
+ export { collectPaths as A, generateTestScript as B, formatFailure as C, formatTypecheckSummary as D, formatTestSummary as E, StudioBackend as F, defineProject as G, ROOT_ONLY_KEYS as H, createStudioBackend as I, LuauScriptError as J, isValidBackend as K, OpenCloudBackend as L, resolveNestedProjects as M, loadConfig$1 as N, formatBanner as O, resolveConfig as P, createOpenCloudBackend as R, formatAgentMultiProject as S, formatResult as T, VALID_BACKENDS as U, DEFAULT_CONFIG as V, defineConfig as W, parseJestOutput as X, extractJsonFromOutput as Y, resolveTsconfigDirectories as _, formatAnnotations as a, formatJson as b, visitBlock as c, buildProjectJob as d, execute as f, processProjectResult as g, loadCoverageManifest as h, runTypecheck as i, findInTree as j, combineSourceMappers as k, visitExpression as l, formatExecuteOutput as m, parseGameOutput as n, formatJobSummary as o, executeBackend as p, hashBuffer as q, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, visitStatement as u, rojoProjectSchema as v, formatMultiProjectResult as w, writeJsonFile as x, findFormatterOptions as y, buildJestArgv as z };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,23 @@
1
1
  import { A as defineConfig, C as FormatterEntry, D as ROOT_ONLY_KEYS, E as ProjectTestConfig, M as Argv, O as ResolvedConfig, S as DisplayName, T as ProjectEntry, _ as ResolvedProjectConfig, a as formatExecuteOutput, b as ConfigInput, c as Backend, d as extractJsonFromOutput, f as parseJestOutput, g as TestStatus, h as TestFileResult, i as execute, j as defineProject, k as SnapshotFormatOptions, l as BackendOptions, m as TestCaseResult, n as ExecuteResult, o as TimingResult, p as JestResult, r as FormatOutputOptions, s as SourceMapper, t as ExecuteOptions, u as BackendResult, v as CliOptions, w as InlineProjectConfig, x as DEFAULT_CONFIG, y as Config } from "./executor-B2IDh6bH.mjs";
2
2
  import { WebSocket, WebSocketServer } from "ws";
3
- import { HttpClient } from "@isentinel/roblox-runner";
4
3
  import buffer from "node:buffer";
5
4
 
5
+ //#region packages/roblox-runner/dist/index.d.mts
6
+ //#region src/http-client.d.ts
7
+ interface HttpResponse {
8
+ body: unknown;
9
+ headers?: Record<string, string | undefined>;
10
+ ok: boolean;
11
+ status: number;
12
+ }
13
+ interface RequestOptions {
14
+ body?: unknown;
15
+ headers?: Record<string, string>;
16
+ }
17
+ interface HttpClient {
18
+ request(method: string, url: string, options?: RequestOptions): Promise<HttpResponse>;
19
+ }
20
+ //#endregion
6
21
  //#region src/backends/open-cloud.d.ts
7
22
  interface OpenCloudCredentials {
8
23
  apiKey: string;
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { B as DEFAULT_CONFIG, C as writeJsonFile, D as formatResult, F as createStudioBackend, I as OpenCloudBackend, J as parseJestOutput, L as createOpenCloudBackend, M as loadConfig, N as resolveConfig, O as formatTestSummary, P as StudioBackend, R as buildJestArgv, S as formatJson, T as formatFailure, U as defineConfig, V as ROOT_ONLY_KEYS, W as defineProject, a as formatAnnotations, c as visitBlock, f as execute, i as runTypecheck, l as visitExpression, m as formatExecuteOutput, n as parseGameOutput, o as formatJobSummary, q as extractJsonFromOutput, r as writeGameOutput, t as formatGameOutputNotice, u as visitStatement, z as generateTestScript } from "./game-output-CwmtpYhn.mjs";
1
+ import { B as generateTestScript, C as formatFailure, E as formatTestSummary, F as StudioBackend, G as defineProject, H as ROOT_ONLY_KEYS, I as createStudioBackend, L as OpenCloudBackend, N as loadConfig, P as resolveConfig, R as createOpenCloudBackend, T as formatResult, V as DEFAULT_CONFIG, W as defineConfig, X as parseJestOutput, Y as extractJsonFromOutput, a as formatAnnotations, b as formatJson, c as visitBlock, f as execute, i as runTypecheck, l as visitExpression, m as formatExecuteOutput, n as parseGameOutput, o as formatJobSummary, r as writeGameOutput, t as formatGameOutputNotice, u as visitStatement, x as writeJsonFile, z as buildJestArgv } from "./game-output-BtWj32M8.mjs";
2
2
  export { DEFAULT_CONFIG, OpenCloudBackend, ROOT_ONLY_KEYS, StudioBackend, buildJestArgv, createOpenCloudBackend, createStudioBackend, defineConfig, defineProject, execute, extractJsonFromOutput, formatAnnotations, formatExecuteOutput, formatFailure, formatGameOutputNotice, formatJobSummary, formatJson, formatResult, formatTestSummary, generateTestScript, loadConfig, parseGameOutput, parseJestOutput, resolveConfig, runTypecheck, visitBlock, visitExpression, visitStatement, writeGameOutput, writeJsonFile };
Binary file
@@ -7633,7 +7633,7 @@ function C$5({ force: e } = {}) {
7633
7633
  var y$4 = C$5();
7634
7634
  //#endregion
7635
7635
  //#region package.json
7636
- var version = "0.2.2";
7636
+ var version = "0.2.4";
7637
7637
  //#endregion
7638
7638
  //#region node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/constants.js
7639
7639
  var require_constants$2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isentinel/jest-roblox",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Jest-compatible CLI for running roblox-ts tests via Roblox Open Cloud",
5
5
  "keywords": [
6
6
  "jest",
@@ -54,10 +54,7 @@
54
54
  "picomatch": "4.0.4",
55
55
  "std-env": "4.0.0",
56
56
  "tinyrainbow": "3.1.0",
57
- "ws": "8.20.0",
58
- "@isentinel/luau-ast": "0.1.0",
59
- "@isentinel/rojo-utils": "0.1.0",
60
- "@isentinel/roblox-runner": "0.1.0"
57
+ "ws": "8.20.0"
61
58
  },
62
59
  "devDependencies": {
63
60
  "@isentinel/eslint-config": "5.0.0-beta.9",
@@ -88,7 +85,10 @@
88
85
  "tsdown": "0.21.7",
89
86
  "type-fest": "5.5.0",
90
87
  "typescript": "5.9.3",
91
- "vitest": "4.1.2"
88
+ "vitest": "4.1.2",
89
+ "@isentinel/luau-ast": "0.1.0",
90
+ "@isentinel/rojo-utils": "0.1.0",
91
+ "@isentinel/roblox-runner": "0.1.0"
92
92
  },
93
93
  "engines": {
94
94
  "node": ">=24.10.0"
Binary file
@@ -7,6 +7,18 @@ if not RunService:IsRunning() then
7
7
  return
8
8
  end
9
9
 
10
+ local function endWithError(err: string): ()
11
+ local ok, endErr = pcall(function()
12
+ StudioTestService:EndTest({
13
+ jestOutput = HttpService:JSONEncode({ success = false, err = err }),
14
+ gameOutput = "[]",
15
+ })
16
+ end)
17
+ if not ok then
18
+ warn("[jest-roblox] EndTest failed: " .. tostring(endErr))
19
+ end
20
+ end
21
+
10
22
  local testArgs = StudioTestService:GetTestArgs()
11
23
  if testArgs == nil then
12
24
  for _ = 1, 50 do
@@ -18,29 +30,43 @@ if testArgs == nil then
18
30
  end
19
31
  end
20
32
 
21
- if testArgs == nil or testArgs.configs == nil then
33
+ if testArgs == nil then
34
+ endWithError("StudioTestService:GetTestArgs() returned nil after 50 polls (5s)")
35
+ return
36
+ end
37
+
38
+ if testArgs.config == nil or testArgs.config.configs == nil then
39
+ local keysOk, keys = pcall(HttpService.JSONEncode, HttpService, testArgs)
40
+ endWithError(
41
+ "testArgs.config.configs is nil. Keys: " .. (if keysOk then keys else "<unencodable>")
42
+ )
22
43
  return
23
44
  end
24
45
 
46
+ local configs = testArgs.config.configs
47
+
25
48
  local loadStringEnabled = pcall(function()
26
49
  loadstring("return true")
27
50
  end)
28
51
 
29
52
  if not loadStringEnabled then
30
- StudioTestService:EndTest({
31
- jestOutput = HttpService:JSONEncode({
32
- success = false,
33
- err = "LoadString must be enabled in ServerScriptService to run tests",
34
- }),
35
- gameOutput = "[]",
36
- })
53
+ endWithError("LoadString must be enabled in ServerScriptService to run tests")
54
+ return
55
+ end
56
+
57
+ local requireOk, Runner = pcall(require, script.Parent.shared.runner)
58
+ if not requireOk then
59
+ endWithError("Failed to require shared.runner: " .. tostring(Runner))
37
60
  return
38
61
  end
39
62
 
40
- local Runner = require(script.Parent.shared.runner)
41
- local entries = Runner.runProjects(script, testArgs.configs)
63
+ local runOk, entriesOrErr = pcall(Runner.runProjects, script, configs)
64
+ if not runOk then
65
+ endWithError("Runner.runProjects threw: " .. tostring(entriesOrErr))
66
+ return
67
+ end
42
68
 
43
69
  StudioTestService:EndTest({
44
- jestOutput = HttpService:JSONEncode({ entries = entries }),
70
+ jestOutput = HttpService:JSONEncode({ entries = entriesOrErr }),
45
71
  gameOutput = "[]",
46
72
  })