@isentinel/jest-roblox 0.2.6 → 0.3.0

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
@@ -58,7 +58,9 @@ import { defineConfig } from "@isentinel/jest-roblox";
58
58
 
59
59
  export default defineConfig({
60
60
  placeFile: "./game.rbxl",
61
- projects: ["ReplicatedStorage/shared"],
61
+ test: {
62
+ projects: ["ReplicatedStorage/shared"],
63
+ },
62
64
  });
63
65
  ```
64
66
 
@@ -85,7 +87,8 @@ jest-roblox -t "should spawn"
85
87
  jest-roblox --testPathPattern player
86
88
  jest-roblox --testPathPattern="modifiers|define\\.spec|triggers"
87
89
 
88
- # Use a specific backend
90
+ # Use a specific backend (default "auto" picks Studio if the plugin is
91
+ # connected, else Open Cloud if credentials are set — see Backends below)
89
92
  jest-roblox --backend studio
90
93
  jest-roblox --backend open-cloud
91
94
 
@@ -110,37 +113,77 @@ Configs can extend a shared base with `extends`:
110
113
  ```typescript
111
114
  export default defineConfig({
112
115
  extends: "../../jest.shared.ts",
113
- projects: ["ReplicatedStorage/shared"],
116
+ test: {
117
+ projects: ["ReplicatedStorage/shared"],
118
+ },
114
119
  });
115
120
  ```
116
121
 
117
122
  Precedence: CLI flags > config file > extended config > defaults.
118
123
 
119
- ### Config fields
124
+ ### Root config fields
125
+
126
+ Two distinct buckets live at the root level. Jest passthrough fields live
127
+ under `test:` (see "Test fields" below).
128
+
129
+ #### Workspace Run Options
130
+
131
+ Atomic to one invocation — these describe what the run targets and how the
132
+ CLI presents output, not how any individual package runs. In `--workspace`
133
+ mode they resolve as: CLI flag > unanimous per-package declaration >
134
+ default. Mixed per-package declarations error loudly.
135
+
136
+ | Field | What it does | Default |
137
+ |---|---|---|
138
+ | `backend` | `"auto"`, `"open-cloud"`, or `"studio"` | `"auto"` |
139
+ | `color` | Use ANSI colors in console output | `true` |
140
+ | `formatters` | Output formatters (`"default"`, `"agent"`, `"json"`, `"github-actions"`) | `["default"]` |
141
+ | `gameOutput` | Write Game Output to a file — a path, or `true` for `game-output.log` under the root. In `--workspace` mode this is one grouped aggregate file across every package | — |
142
+ | `outputFile` | Write the Jest result JSON — a path, or `true` for `jest-output.log` under the root. In `--workspace` mode this is the single merged result across every package | — |
143
+ | `workspace.gameOutput` | `true` to also emit per-package Game Output files under `.jest-roblox/output/` (`--workspace` only) | — |
144
+ | `workspace.outputFile` | `true` to also emit per-package result files under `.jest-roblox/output/` (`--workspace` only) | — |
145
+ | `parallel` | Number of concurrent Open Cloud sessions, or `"auto"` (= `min(jobs, 3)`) | — |
146
+ | `placeId` | Open Cloud place ID | — |
147
+ | `port` | WebSocket port for Studio backend | `3001` |
148
+ | `silent` | Suppress console output | `false` |
149
+ | `universeId` | Open Cloud universe ID | — |
150
+
151
+ #### Per-package fields
152
+
153
+ Loaded per package (directly or via `extends: "../jest.shared.ts"`). The
154
+ workspace-root config is NOT a source of truth for these — declare them in
155
+ each package's own jest.config or in a shared config that every package
156
+ extends.
120
157
 
121
158
  | Field | What it does | Default |
122
159
  |---|---|---|
123
- | `projects` | Where to look for tests in the DataModel | **required** |
124
- | `backend` | `"open-cloud"` or `"studio"` | — |
125
160
  | `placeFile` | Path to your `.rbxl` file | `"./game.rbxl"` |
126
161
  | `timeout` | Max time for tests to run (ms) | `300000` (5 min) |
127
162
  | `sourceMap` | Map Luau errors back to TypeScript (roblox-ts only) | `true` |
128
- | `port` | WebSocket port for Studio backend | `3001` |
129
- | `testMatch` | Glob patterns that find test files | `**/*.spec.ts`, `**/*.test.ts`, etc. |
130
- | `testPathIgnorePatterns` | Patterns to skip | `/node_modules/`, `/dist/`, `/out/` |
131
163
  | `rojoProject` | Path to your Rojo project file | auto |
132
164
  | `jestPath` | Where Jest lives in the DataModel | auto |
165
+ | `showLuau` | Show Luau code snippets in failure output | `true` |
166
+ | `coverageCache` | Reuse incrementally-instrumented coverage shadow dir between runs | `true` |
167
+ | `luauRoots` | Where Luau files live for coverage instrumentation | auto from tsconfig `outDir` |
168
+
169
+ ### Test fields
170
+
171
+ Put these under `test: { ... }`.
172
+
173
+ | Field | What it does | Default |
174
+ |---|---|---|
175
+ | `projects` | Where to look for tests in the DataModel | **required** |
176
+ | `testMatch` | Glob patterns that find test files | `**/*.spec.ts`, `**/*.test.ts`, etc. |
177
+ | `testPathIgnorePatterns` | Patterns to skip | `/node_modules/`, `/dist/`, `/out/` |
133
178
  | `setupFiles` | Scripts to run before the test environment loads | — |
134
179
  | `setupFilesAfterEnv` | Scripts to run after the test environment loads | — |
135
- | `formatters` | Output formatters (`"default"`, `"agent"`, `"json"`, `"github-actions"`) | `["default"]` |
136
- | `gameOutput` | Path to write game print/warn/error output | |
137
- | `showLuau` | Show Luau code snippets in failure output | `true` |
138
- | `cache` | Cache place file uploads by content hash | `true` |
139
- | `pollInterval` | How often to poll for results in ms (Open Cloud) | `500` |
140
- | `parallel` | Number of concurrent Open Cloud sessions, or `"auto"` (= `min(jobs, 3)`) | — |
180
+ | `verbose` | Show individual test results | `false` |
181
+ | `silent` | Suppress console output | `false` |
141
182
 
142
183
  ### Coverage fields
143
184
 
185
+ Put these under `test: { ... }`.
186
+
144
187
  > [!IMPORTANT]
145
188
  > Coverage requires [Lute](https://github.com/luau-lang/lute) to be installed and
146
189
  > on your `PATH`. Lute parses Luau ASTs so the CLI can insert coverage probes.
@@ -153,7 +196,6 @@ Precedence: CLI flags > config file > extended config > defaults.
153
196
  | `coverageThreshold` | Minimum coverage to pass | — |
154
197
  | `coveragePathIgnorePatterns` | Files to leave out of coverage | test files, `node_modules`, `rbxts_include` |
155
198
  | `collectCoverageFrom` | Globs for files to include in coverage | — |
156
- | `luauRoots` | Where Luau files live (auto from tsconfig `outDir` for roblox-ts, or set by hand for pure Luau) | auto |
157
199
 
158
200
  ### Project-level config
159
201
 
@@ -165,23 +207,25 @@ import { defineConfig, defineProject } from "@isentinel/jest-roblox";
165
207
 
166
208
  export default defineConfig({
167
209
  placeFile: "./game.rbxl",
168
- projects: [
169
- {
170
- test: defineProject({
171
- displayName: { name: "client", color: "magenta" },
172
- include: ["**/*.spec.ts"],
173
- mockDataModel: true,
174
- outDir: "out/src/client",
210
+ test: {
211
+ projects: [
212
+ defineProject({
213
+ test: {
214
+ displayName: { name: "client", color: "magenta" },
215
+ include: ["**/*.spec.ts"],
216
+ mockDataModel: true,
217
+ outDir: "out/src/client",
218
+ },
175
219
  }),
176
- },
177
- {
178
- test: defineProject({
179
- displayName: { name: "server", color: "white" },
180
- include: ["**/*.spec.ts"],
181
- outDir: "out/src/server",
220
+ defineProject({
221
+ test: {
222
+ displayName: { name: "server", color: "white" },
223
+ include: ["**/*.spec.ts"],
224
+ outDir: "out/src/server",
225
+ },
182
226
  }),
183
- },
184
- ],
227
+ ],
228
+ },
185
229
  });
186
230
  ```
187
231
 
@@ -192,22 +236,31 @@ import { defineConfig } from "@isentinel/jest-roblox";
192
236
 
193
237
  export default defineConfig({
194
238
  backend: "open-cloud",
195
- collectCoverage: true,
196
- coverageThreshold: {
197
- branches: 70,
198
- functions: 80,
199
- statements: 80,
200
- },
201
239
  jestPath: "ReplicatedStorage/Packages/Jest",
202
240
  placeFile: "./game.rbxl",
203
- projects: ["ReplicatedStorage/client", "ServerScriptService/server"],
241
+ test: {
242
+ collectCoverage: true,
243
+ coverageThreshold: {
244
+ branches: 70,
245
+ functions: 80,
246
+ statements: 80,
247
+ },
248
+ projects: ["ReplicatedStorage/client", "ServerScriptService/server"],
249
+ },
204
250
  timeout: 60000,
205
251
  });
206
252
  ```
207
253
 
208
254
  ## Backends
209
255
 
210
- Two ways to run tests:
256
+ Two ways to run tests, plus an auto-pick:
257
+
258
+ ### Auto (default)
259
+
260
+ `--backend auto` (the default) probes for a connected Studio plugin first.
261
+ If detected, runs via Studio; otherwise falls back to Open Cloud — but only
262
+ if credentials are available (see Open Cloud below). With no plugin and no
263
+ credentials, the run errors instead of silently falling back.
211
264
 
212
265
  ### Open Cloud (remote)
213
266
 
@@ -221,6 +274,39 @@ You need these environment variables:
221
274
  | `ROBLOX_UNIVERSE_ID` | The universe to run tests in |
222
275
  | `ROBLOX_PLACE_ID` | The place to run tests in |
223
276
 
277
+ > Prefix any of the above with `JEST_` (e.g. `JEST_ROBLOX_PLACE_ID`) to override the unprefixed value. Use the `JEST_`-prefixed form when the generic names collide with other tooling.
278
+
279
+ #### Required scopes
280
+
281
+ Create the API key in the Creator Dashboard against the target universe, then
282
+ grant it the scopes below. A `403` at runtime surfaces as a `PermissionError`
283
+ with the missing scope name.
284
+
285
+ Always required:
286
+
287
+ | Scope | What it's for |
288
+ |---|---|
289
+ | `universe-places:write` | Publish the built `.rbxl` as a new place version |
290
+ | `universe.place.luau-execution-session:write` | Start the Luau session that runs the tests |
291
+
292
+ `--workspace --parallel >1` additionally requires the queue scopes for
293
+ work-stealing across concurrent sessions:
294
+
295
+ | Scope | What it's for |
296
+ |---|---|
297
+ | `memory-store.queue:add` / `:dequeue` / `:discard` | Work-stealing queue across concurrent sessions |
298
+
299
+ `--workspace --parallel >1` with a streaming formatter additionally requires:
300
+
301
+ | Scope | What it's for |
302
+ |---|---|
303
+ | `memory-store.sorted-map:read` / `:write` | Stream live per-package results back as packages finish |
304
+
305
+ Streaming is enabled by default and disabled only for `--silent`,
306
+ `--formatters json`, and `--formatters agent` (without `--verbose`).
307
+ `--formatters agent --verbose` re-enables streaming and therefore still
308
+ needs the sorted-map scopes; `--formatters github-actions` also streams.
309
+
224
310
  ### Studio (local)
225
311
 
226
312
  Connects to Roblox Studio over WebSocket. Faster than Open Cloud (no upload
@@ -229,7 +315,7 @@ multiple concurrent projects aren't supported yet.
229
315
 
230
316
  > [!NOTE]
231
317
  > 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
318
+ > built to a separate output under `.jest-roblox/coverage/` that is likely not
233
319
  > the studio place being served.
234
320
 
235
321
  Install the plugin with [Drillbit](https://github.com/jacktabscode/drillbit):
@@ -240,7 +326,7 @@ Create a file named drillbit.toml in your project's directory.
240
326
 
241
327
  ```toml
242
328
  [plugins.jest_roblox]
243
- github = "https://github.com/christopher-buss/jest-roblox-cli/releases/download/v0.2.4/JestRobloxRunner.rbxm"
329
+ github = "https://github.com/christopher-buss/jest-roblox-cli/releases/download/v0.3.0/JestRobloxRunner.rbxm"
244
330
  ```
245
331
 
246
332
  Then run `drillbit` and it will download the plugin and install it in Studio for you.
@@ -249,11 +335,107 @@ Or download `JestRobloxRunner.rbxm` from the
249
335
  [latest release](https://github.com/christopher-buss/jest-roblox-cli/releases)
250
336
  and drop it into your Studio plugins folder.
251
337
 
338
+ ## Workspace mode
339
+
340
+ Run tests across multiple packages in a pnpm workspace in a single
341
+ invocation. Open Cloud only — Studio backend is not supported.
342
+
343
+ > [!NOTE]
344
+ > Package discovery uses one of two sources. By default it reads
345
+ > `pnpm-workspace.yaml` at the workspace root. Alternatively, declare a
346
+ > `workspace` block in your jest config (see
347
+ > [Workspaces without pnpm](#workspaces-without-pnpm)) to enumerate packages
348
+ > by glob — this works in Luau-only, npm, and yarn repos. `--affected-since`
349
+ > always delegates change detection to `turbo` or `nx` and is not yet wired
350
+ > for the `workspace.packages` source. When using Nx, each project's Nx name
351
+ > must match the `package.json` `name` field — `--affected-since` returns Nx
352
+ > project names and looks them up against the package list, so a mismatch
353
+ > surfaces as `Package "<name>" not found in workspace`.
354
+
355
+ Pick packages explicitly or by what changed:
356
+
357
+ ```bash
358
+ # Specific packages
359
+ jest-roblox --workspace --packages @scope/pkg-a,@scope/pkg-b
360
+
361
+ # Everything changed since a git ref (via turbo/nx affected)
362
+ jest-roblox --workspace --affected-since main
363
+ ```
364
+
365
+ `--workspace` must be combined with `--packages` or `--affected-since` —
366
+ the two are mutually exclusive, and either flag requires `--workspace`.
367
+
368
+ ### Workspaces without pnpm
369
+
370
+ `pnpm-workspace.yaml` isn't required. Declare a `workspace` block in a shared
371
+ config and have every package extend it:
372
+
373
+ ```ts
374
+ // packages/testing/jest.shared.ts
375
+ import { defineConfig } from "@isentinel/jest-roblox";
376
+
377
+ export default defineConfig({
378
+ workspace: {
379
+ packages: ["packages/*"], // globs relative to root
380
+ root: "../..", // relative to THIS file; resolved at load
381
+ },
382
+ // shared jest options…
383
+ });
384
+ ```
385
+
386
+ ```ts
387
+ // packages/foo/jest.config.ts
388
+ export default { extends: "../testing/jest.shared.ts" };
389
+ ```
390
+
391
+ `workspace.root` and `workspace.packages` must be declared together. `root` is
392
+ resolved to an absolute path relative to the file that declares it (the shared
393
+ config), so it points at the same directory no matter which package you run
394
+ from. Each glob in `packages` selects directories that contain a
395
+ `jest.config.*`; the package name comes from `package.json#name`, falling back
396
+ to the directory name (so Luau-only packages need no `package.json`). Every
397
+ selected package must resolve the same `workspace.packages`/`root` — inheriting
398
+ from one shared config guarantees this, and a package that overrides or omits
399
+ it fails the run.
400
+
401
+ Run from inside any package as usual. To run from a directory with no
402
+ resolvable jest config (e.g. the repo root), either point at the shared config
403
+ with `--workspace-root`:
404
+
405
+ ```bash
406
+ jest-roblox --workspace --packages foo --workspace-root packages/testing
407
+ ```
408
+
409
+ or add a re-export at the repo root so the config is discovered there:
410
+
411
+ ```ts
412
+ // jest.config.ts (repo root)
413
+ export { default } from "./packages/testing/jest.shared.ts";
414
+ ```
415
+
416
+ Per-package coverage is aggregated into a single report under
417
+ `<rootDir>/<coverageDirectory>`. `rootDir` defaults to the current working
418
+ directory, so run from the workspace root (or set `rootDir`) if you want
419
+ the report to land there.
420
+
421
+ Game Output has two independent sinks. Setting `gameOutput` (a path, or
422
+ `true`) writes one **grouped** aggregate file at the workspace root —
423
+ `[{ package, project, entries }]`, one group per (package, project) that ran.
424
+ Setting `workspace.gameOutput: true` writes a **per-package** file per
425
+ (package, project) under `.jest-roblox/output/`. Either, both, or neither
426
+ may be set; with both, humans see the aggregate announced and agents see the
427
+ per-package paths.
428
+
429
+ `outputFile` (the Jest result JSON) follows the same two-sink model:
430
+ `outputFile` (a path, or `true`) writes one merged result at the workspace
431
+ root, and `workspace.outputFile: true` writes a per-package result file per
432
+ (package, project) under `.jest-roblox/output/`.
433
+
252
434
  ## CLI flags
253
435
 
254
436
  | Flag | What it does |
255
437
  |---|---|
256
- | `--backend <type>` | Choose `open-cloud` or `studio` |
438
+ | `--backend <type>` | Choose `auto`, `open-cloud`, or `studio` |
257
439
  | `--port <n>` | WebSocket port for Studio |
258
440
  | `--config <path>` | Path to config file |
259
441
  | `--testPathPattern <regex>` | Filter test files by path |
@@ -264,24 +446,30 @@ and drop it into your Studio plugins folder.
264
446
  | `--coverage` | Collect coverage |
265
447
  | `--coverageDirectory <path>` | Where to put coverage reports |
266
448
  | `--coverageReporters <r...>` | Which report formats to use |
267
- | `--luauRoots <path...>` | Where compiled Luau files live |
449
+ | `--collectCoverageFrom <glob>` | Globs for files to include in coverage (repeatable) |
268
450
  | `--no-show-luau` | Hide Luau code in failure output |
269
451
  | `-u, --updateSnapshot` | Update snapshot files |
270
452
  | `--sourceMap` | Map Luau errors to TypeScript (roblox-ts only) |
271
453
  | `--rojoProject <path>` | Path to Rojo project file |
454
+ | `--timeout <ms>` | Max time for tests to run |
455
+ | `--passWithNoTests` | Exit `0` when no test files are found |
272
456
  | `--verbose` | Show each test result |
273
457
  | `--silent` | Hide all output |
274
458
  | `--no-color` | Turn off colors |
275
- | `--no-cache` | Force a fresh place file upload |
276
- | `--pollInterval <ms>` | How often to check for results (Open Cloud) |
459
+ | `--no-coverage-cache` | Force a clean coverage re-instrumentation |
277
460
  | `--parallel [n]` | Open Cloud concurrent sessions, or `auto` (= `min(jobs, 3)`) |
278
461
  | `--project <name...>` | Filter which named projects to run |
279
- | `--projects <path...>` | DataModel paths that hold tests |
280
462
  | `--setupFiles <path...>` | Scripts to run before env |
281
463
  | `--setupFilesAfterEnv <path...>` | Scripts to run after env |
282
464
  | `--typecheck` | Run type tests too |
283
465
  | `--typecheckOnly` | Run only type tests |
284
466
  | `--typecheckTsconfig <path>` | tsconfig for type tests |
467
+ | `--workspace` | Enable workspace mode (pair with `--packages` or `--affected-since`; see [Workspace mode](#workspace-mode)) |
468
+ | `--packages <names>` | Comma-separated package names (workspace mode) |
469
+ | `--affected-since <ref>` | Run only packages affected since a git ref (workspace mode) |
470
+ | `--apiKey <key>` | Open Cloud API key (prefer env vars in CI — visible in process listings) |
471
+ | `--universeId <id>` | Target universe ID (Open Cloud) |
472
+ | `--placeId <id>` | Target place ID (Open Cloud) |
285
473
 
286
474
  ## How it works
287
475
 
package/dist/cli.d.mts CHANGED
@@ -1,10 +1,8 @@
1
- import { _ as ResolvedProjectConfig, n as ExecuteResult, v as CliOptions } from "./executor-COuwZJJX.mjs";
1
+ import { t as CliOptions } from "./schema-BpjBo-Aw.mjs";
2
2
 
3
3
  //#region src/cli.d.ts
4
4
  declare function parseArgs(args: Array<string>): CliOptions;
5
- declare function filterByName(projects: Array<ResolvedProjectConfig>, names: Array<string>): Array<ResolvedProjectConfig>;
6
- declare function mergeProjectResults(results: Array<ExecuteResult>): ExecuteResult;
7
5
  declare function run(args: Array<string>): Promise<number>;
8
6
  declare function main(): Promise<void>;
9
7
  //#endregion
10
- export { filterByName, main, mergeProjectResults, parseArgs, run };
8
+ export { main, parseArgs, run };