@isentinel/jest-roblox 0.3.7 → 0.3.9

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.
@@ -819,7 +819,7 @@ interface TypecheckConfig {
819
819
  }
820
820
  //#endregion
821
821
  //#region src/config/schema.d.ts
822
- type Backend = "auto" | "open-cloud" | "studio";
822
+ type Backend = "auto" | "open-cloud" | "studio" | "studio-cli";
823
823
  type CoverageReporter = keyof ReportOptions;
824
824
  type FormatterEntry = [string, Record<string, unknown>] | string;
825
825
  /** pretty-format options controlling how snapshots are serialized. */
@@ -1028,7 +1028,12 @@ interface GlobalTestConfig extends SharedTestConfig {
1028
1028
  expand?: boolean;
1029
1029
  /** A JSON string of globals to expose in every test environment. */
1030
1030
  globals?: string;
1031
- /** Globs selecting Runtime Test files when no `projects` are configured. */
1031
+ /**
1032
+ * Per-project globs selecting Runtime Test files. With no `projects`
1033
+ * configured, the run derives one project per `luauRoots` mount and runtime
1034
+ * discovery uses `testMatch` — this root-level `include` is not consumed
1035
+ * there.
1036
+ */
1032
1037
  include?: Array<string>;
1033
1038
  /**
1034
1039
  * Maximum worker count, or a percentage string like `"50%"`, for parallel
@@ -1081,7 +1086,10 @@ interface Config {
1081
1086
  /**
1082
1087
  * Execution backend. `"auto"` probes for a running Studio then falls back to
1083
1088
  * Open Cloud; `"open-cloud"` uploads and runs via Roblox Open Cloud;
1084
- * `"studio"` drives a locally running Studio. Default `"auto"`.
1089
+ * `"studio"` drives a locally running Studio; `"studio-cli"` launches its own
1090
+ * headless Studio via `--task RunScript` and quits it (pass `--headed` to show
1091
+ * the Studio window during the run). Default `"auto"`. `"studio-cli"` is never
1092
+ * selected by `"auto"` — request it explicitly.
1085
1093
  */
1086
1094
  backend?: Backend;
1087
1095
  /** Force ANSI colour in output. Default `true`. */
@@ -1148,6 +1156,12 @@ interface Config {
1148
1156
  /** Map Luau stack traces back to TypeScript source. Default `true`. */
1149
1157
  sourceMap?: boolean;
1150
1158
  /**
1159
+ * Path to the Roblox Studio executable the `"studio-cli"` backend launches.
1160
+ * Overrides per-OS auto-discovery. Also settable via `--studioPath` or the
1161
+ * `JEST_ROBLOX_STUDIO_PATH` environment variable.
1162
+ */
1163
+ studioPath?: string;
1164
+ /**
1151
1165
  * The Jest options block. Every jest-passthrough setting lives here, kept
1152
1166
  * separate from the CLI/runner keys above.
1153
1167
  */
@@ -1214,6 +1228,11 @@ interface CliOptions {
1214
1228
  files?: Array<string>;
1215
1229
  formatters?: Array<string>;
1216
1230
  gameOutput?: string;
1231
+ /**
1232
+ * Show the Studio window during a `studio-cli` run (`--headed`). CLI-only;
1233
+ * inert for every other backend. Never sourced from config or env.
1234
+ */
1235
+ headed?: boolean;
1217
1236
  help?: boolean;
1218
1237
  outputFile?: string;
1219
1238
  packages?: string;
@@ -1229,6 +1248,7 @@ interface CliOptions {
1229
1248
  showLuau?: boolean;
1230
1249
  silent?: boolean;
1231
1250
  sourceMap?: boolean;
1251
+ studioPath?: string;
1232
1252
  testNamePattern?: string;
1233
1253
  testPathPattern?: string;
1234
1254
  timeout?: number;
@@ -9,7 +9,7 @@ interface ResolveResult {
9
9
  url: string;
10
10
  }
11
11
 
12
- type NextResolve = (specifier: string, context: ResolveContext) => Promise<ResolveResult>;
12
+ type NextResolve = (specifier: string, context: ResolveContext) => ResolveResult;
13
13
 
14
14
  interface LoadContext {
15
15
  conditions?: Array<string>;
@@ -23,12 +23,12 @@ interface LoadResult {
23
23
  source: string;
24
24
  }
25
25
 
26
- type NextLoad = (url: string, context: LoadContext) => Promise<LoadResult>;
26
+ type NextLoad = (url: string, context: LoadContext) => LoadResult;
27
27
 
28
28
  export function resolve(
29
29
  specifier: string,
30
30
  context: ResolveContext,
31
31
  nextResolve: NextResolve,
32
- ): Promise<ResolveResult>;
32
+ ): ResolveResult;
33
33
 
34
- export function load(url: string, context: LoadContext, nextLoad: NextLoad): Promise<LoadResult>;
34
+ export function load(url: string, context: LoadContext, nextLoad: NextLoad): LoadResult;
@@ -1,8 +1,8 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { readFileSync } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
 
4
- export async function resolve(specifier, context, nextResolve) {
5
- const resolved = await nextResolve(specifier, context);
4
+ export function resolve(specifier, context, nextResolve) {
5
+ const resolved = nextResolve(specifier, context);
6
6
 
7
7
  if (resolved.url.endsWith(".luau") || resolved.url.endsWith(".lua")) {
8
8
  return { ...resolved, format: "luau-raw" };
@@ -11,7 +11,7 @@ export async function resolve(specifier, context, nextResolve) {
11
11
  return resolved;
12
12
  }
13
13
 
14
- export async function load(url, context, nextLoad) {
14
+ export function load(url, context, nextLoad) {
15
15
  if (context.format === "luau-raw") {
16
16
  if (url.endsWith(".lua")) {
17
17
  return {
@@ -21,7 +21,7 @@ export async function load(url, context, nextLoad) {
21
21
  };
22
22
  }
23
23
 
24
- const content = await readFile(fileURLToPath(url), "utf-8");
24
+ const content = readFileSync(fileURLToPath(url), "utf-8");
25
25
  return {
26
26
  format: "module",
27
27
  shortCircuit: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isentinel/jest-roblox",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Jest-compatible CLI for running roblox-ts tests via Roblox Open Cloud",
5
5
  "keywords": [
6
6
  "jest",
@@ -66,8 +66,8 @@
66
66
  "ws": "8.20.1"
67
67
  },
68
68
  "devDependencies": {
69
- "@isentinel/eslint-config": "5.2.0",
70
- "@isentinel/roblox-ts": "4.0.7",
69
+ "@isentinel/eslint-config": "6.0.0-beta.3",
70
+ "@isentinel/roblox-ts": "4.0.10",
71
71
  "@isentinel/tsconfig": "1.2.0",
72
72
  "@isentinel/weld": "0.2.0",
73
73
  "@oxc-project/types": "0.123.0",
@@ -86,9 +86,9 @@
86
86
  "@vitest/eslint-plugin": "1.6.19",
87
87
  "better-typescript-lib": "2.12.0",
88
88
  "bumpp": "11.1.0",
89
- "eslint": "9.39.4",
89
+ "eslint": "10.6.0",
90
90
  "eslint-plugin-jest-extended": "3.0.1",
91
- "eslint-plugin-n": "18.0.1",
91
+ "eslint-plugin-n": "18.2.1",
92
92
  "eslint-plugin-pnpm": "1.6.1",
93
93
  "jest-extended": "7.0.0",
94
94
  "memfs": "4.57.2",
Binary file
@@ -12,11 +12,11 @@ local WEBSOCKET_URL = "ws://localhost:3001"
12
12
  local RECONNECT_DELAY = 0.1
13
13
 
14
14
  -- Plugin/CLI protocol version. Must match `STUDIO_PROTOCOL_VERSION` in
15
- -- `src/backends/studio.ts`. Bumped when the runtime contract changes (e.g.
16
- -- runtime-injection payload shape). The plugin rejects mismatched CLI
17
- -- versions with a `version_mismatch` response so users see a clear upgrade
15
+ -- `src/backends/studio.ts`. Bumped when the runtime contract changes — v3 added
16
+ -- the run-mode workspace dispatch + version echo. The plugin rejects mismatched
17
+ -- CLI versions with a `version_mismatch` response so users see a clear upgrade
18
18
  -- message rather than running with stale behaviour or an opaque timeout.
19
- local PROTOCOL_VERSION = 2
19
+ local PROTOCOL_VERSION = 3
20
20
 
21
21
  local IsDebug = ReplicatedStorage:GetAttribute("JEST_ROBLOX_DEBUG") == true
22
22
  ReplicatedStorage:GetAttributeChangedSignal("JEST_ROBLOX_DEBUG"):Connect(function(): ()
@@ -130,11 +130,18 @@ local function connect(): ()
130
130
  local runOk, result = pcall(function(): any
131
131
  return StudioTestService:ExecuteRunModeAsync({
132
132
  test = true,
133
+ -- Echo the negotiated version into the Run-mode payload so
134
+ -- the run-mode runner's own handshake passes (it validates
135
+ -- `protocolVersion`, shared with the studio-cli path).
136
+ protocolVersion = PROTOCOL_VERSION,
133
137
  config = message.config,
134
138
  -- Filtered injection targets (parallel to configs)
135
139
  -- so the Run Mode runner skips mounts that already
136
140
  -- have a user-authored jest.config on disk.
137
141
  runtimeStubMounts = message.runtimeStubMounts,
142
+ -- Present only for workspace runs: the staged entries the
143
+ -- run-mode runner dispatches to the embedded materializer.
144
+ workspace = message.workspace,
138
145
  })
139
146
  end)
140
147
 
@@ -7,11 +7,23 @@ if not RunService:IsRunning() then
7
7
  return
8
8
  end
9
9
 
10
- local function endWithError(err: string): ()
10
+ -- Plugin/CLI protocol version. Must match `STUDIO_PROTOCOL_VERSION` in
11
+ -- `src/backends/studio.ts` and `STUDIO_CLI_PROTOCOL_VERSION` in
12
+ -- `src/backends/studio-cli.ts`. v3 added the run-mode workspace dispatch + this
13
+ -- version echo. The runner validates the version the CLI sent and echoes its own
14
+ -- in `EndTest`, so the CLI surfaces a clean "update the plugin" error against a
15
+ -- mismatched plugin rather than running with stale runtime semantics.
16
+ local PROTOCOL_VERSION = 3
17
+
18
+ -- Every result (success or failure) echoes `protocolVersion` so the studio-cli
19
+ -- host can detect a stale plugin: one predating this echo omits the field and
20
+ -- the host rejects it as a version mismatch.
21
+ local function endTest(jestOutput: string, gameOutput: string?): ()
11
22
  local ok, endErr = pcall(function()
12
23
  StudioTestService:EndTest({
13
- jestOutput = HttpService:JSONEncode({ success = false, err = err }),
14
- gameOutput = "[]",
24
+ jestOutput = jestOutput,
25
+ gameOutput = gameOutput or "[]",
26
+ protocolVersion = PROTOCOL_VERSION,
15
27
  })
16
28
  end)
17
29
  if not ok then
@@ -19,6 +31,10 @@ local function endWithError(err: string): ()
19
31
  end
20
32
  end
21
33
 
34
+ local function endWithError(err: string): ()
35
+ endTest(HttpService:JSONEncode({ success = false, err = err }), "[]")
36
+ end
37
+
22
38
  local testArgs = StudioTestService:GetTestArgs()
23
39
  if testArgs == nil then
24
40
  for _ = 1, 50 do
@@ -34,16 +50,24 @@ if testArgs == nil then
34
50
  return
35
51
  end
36
52
 
37
- if testArgs.config == nil or testArgs.config.configs == nil then
38
- local keysOk, keys = pcall(HttpService.JSONEncode, HttpService, testArgs)
53
+ -- Version handshake: reject a missing or mismatched `protocolVersion` before
54
+ -- running anything, mirroring the WebSocket `version_mismatch` path. The error
55
+ -- envelope still echoes our version (via endTest), so the CLI surfaces the
56
+ -- upgrade message either through this error or the version echo.
57
+ local clientVersion = if typeof(testArgs.protocolVersion) == "number"
58
+ then testArgs.protocolVersion
59
+ else nil
60
+ if clientVersion ~= PROTOCOL_VERSION then
39
61
  endWithError(
40
- "testArgs.config.configs is nil. Keys: " .. (if keysOk then keys else "<unencodable>")
62
+ "Studio plugin protocol version mismatch plugin v"
63
+ .. tostring(PROTOCOL_VERSION)
64
+ .. ", CLI v"
65
+ .. tostring(clientVersion)
66
+ .. ". Update the jest-roblox Studio plugin to match the CLI."
41
67
  )
42
68
  return
43
69
  end
44
70
 
45
- local configs = testArgs.config.configs
46
-
47
71
  local loadStringEnabled = pcall(function()
48
72
  loadstring("return true")
49
73
  end)
@@ -53,6 +77,41 @@ if not loadStringEnabled then
53
77
  return
54
78
  end
55
79
 
80
+ -- Dispatch by payload shape. A `workspace` payload drives the staged
81
+ -- materializer (clone each package from the mega-place's `__pkg_stage`, run,
82
+ -- reset) via the shared single-process embedded runner — the same code OCALE
83
+ -- runs, minus the MemoryStore queue. Otherwise the existing single-/multi-
84
+ -- project configs path runs `Runner.runProjects`.
85
+ if testArgs.workspace ~= nil then
86
+ local embeddedOk, EmbeddedRunner =
87
+ pcall(require, script.Parent.shared.staging["embedded-runner"])
88
+ if not embeddedOk then
89
+ endWithError(
90
+ "Failed to require shared.staging.embedded-runner: " .. tostring(EmbeddedRunner)
91
+ )
92
+ return
93
+ end
94
+
95
+ local runOk, entriesOrErr = pcall(EmbeddedRunner.runEmbedded, script, testArgs.workspace)
96
+ if not runOk then
97
+ endWithError("EmbeddedRunner.runEmbedded threw: " .. tostring(entriesOrErr))
98
+ return
99
+ end
100
+
101
+ endTest(HttpService:JSONEncode({ entries = entriesOrErr }), "[]")
102
+ return
103
+ end
104
+
105
+ if testArgs.config == nil or testArgs.config.configs == nil then
106
+ local keysOk, keys = pcall(HttpService.JSONEncode, HttpService, testArgs)
107
+ endWithError(
108
+ "testArgs.config.configs is nil. Keys: " .. (if keysOk then keys else "<unencodable>")
109
+ )
110
+ return
111
+ end
112
+
113
+ local configs = testArgs.config.configs
114
+
56
115
  local requireOk, Runner = pcall(require, script.Parent.shared.runner)
57
116
  if not requireOk then
58
117
  endWithError("Failed to require shared.runner: " .. tostring(Runner))
@@ -117,11 +176,7 @@ local function injectStubsForConfig(cfg, configIndex: number, injectionPaths: {
117
176
  )
118
177
  local stub = Instance.new("ModuleScript")
119
178
  stub.Name = "jest.config"
120
- stub.Source = string.format(
121
- 'return _G["%s"].configs[%d]',
122
- SESSION_KEY,
123
- configIndex
124
- )
179
+ stub.Source = string.format('return _G["%s"].configs[%d]', SESSION_KEY, configIndex)
125
180
  stub.Parent = leaf
126
181
  -- Record immediately after parenting so a subsequent mount's
127
182
  -- failure does not strand this one.
@@ -133,7 +188,9 @@ local function cleanupStubsForConfig(configIndex: number)
133
188
  local injected = injectedPerConfig[configIndex]
134
189
  if injected ~= nil then
135
190
  for _, inst in injected do
136
- pcall(function() inst:Destroy() end)
191
+ pcall(function()
192
+ inst:Destroy()
193
+ end)
137
194
  end
138
195
  injectedPerConfig[configIndex] = nil
139
196
  end
@@ -159,8 +216,7 @@ end
159
216
  -- nested-project mounts (e.g. `ReplicatedStorage` and `ReplicatedStorage/Foo`)
160
217
  -- never have stubs in DataModel simultaneously — which would let Jest's
161
218
  -- parent-traversal config lookup resolve the wrong project's config.
162
- local runtimeStubMounts: { { string } } = testArgs.runtimeStubMounts
163
- or table.create(#configs, {})
219
+ local runtimeStubMounts: { { string } } = testArgs.runtimeStubMounts or table.create(#configs, {})
164
220
 
165
221
  local hooks = {
166
222
  beforeConfig = function(cfg, index)
@@ -181,7 +237,4 @@ if not runOk then
181
237
  return
182
238
  end
183
239
 
184
- StudioTestService:EndTest({
185
- jestOutput = HttpService:JSONEncode({ entries = entriesOrErr }),
186
- gameOutput = "[]",
187
- })
240
+ endTest(HttpService:JSONEncode({ entries = entriesOrErr }), "[]")