@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 +234 -46
- package/dist/cli.d.mts +2 -4
- package/dist/cli.mjs +113 -2812
- package/dist/index.d.mts +559 -40
- package/dist/index.mjs +2 -2
- package/dist/run-BEUPi80L.mjs +9834 -0
- package/dist/{executor-COuwZJJX.d.mts → schema-BpjBo-Aw.d.mts} +139 -301
- package/dist/sea-entry.cjs +40211 -40255
- package/package.json +29 -25
- package/plugin/JestRobloxRunner.rbxm +0 -0
- package/plugin/plugin.project.json +1 -1
- package/plugin/src/init.server.luau +39 -0
- package/plugin/src/test-in-run-mode.server.luau +117 -2
- package/dist/game-output-CCPIQMWm.mjs +0 -3643
- package/dist/sea/jest-roblox +0 -0
- package/plugin/out/shared/entry.luau +0 -9
- package/plugin/out/shared/instance-resolver.luau +0 -88
- package/plugin/out/shared/mock/CoreScriptSyncService.luau +0 -19
- package/plugin/out/shared/mock/FileSystemService.luau +0 -30
- package/plugin/out/shared/promise.luau +0 -2006
- package/plugin/out/shared/runner.luau +0 -301
- package/plugin/out/shared/setup-timing.luau +0 -89
- package/plugin/out/shared/snapshot-patch.luau +0 -94
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isentinel/jest-roblox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Jest-compatible CLI for running roblox-ts tests via Roblox Open Cloud",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jest",
|
|
@@ -35,17 +35,18 @@
|
|
|
35
35
|
"dist",
|
|
36
36
|
"loaders",
|
|
37
37
|
"plugin",
|
|
38
|
+
"!dist/sea",
|
|
38
39
|
"!dist/**/*.map",
|
|
39
40
|
"!dist/**/*.tsbuildinfo"
|
|
40
41
|
],
|
|
41
42
|
"dependencies": {
|
|
43
|
+
"@bedrock-rbx/ocale": "0.1.0-beta.15",
|
|
42
44
|
"@jridgewell/trace-mapping": "0.3.31",
|
|
43
|
-
"@roblox-ts/rojo-resolver": "1.1.0",
|
|
44
45
|
"arktype": "2.2.0",
|
|
45
|
-
"c12": "4.0.0-beta.
|
|
46
|
+
"c12": "4.0.0-beta.5",
|
|
46
47
|
"confbox": "0.2.4",
|
|
47
|
-
"defu": "6.1.
|
|
48
|
-
"get-tsconfig": "4.
|
|
48
|
+
"defu": "6.1.7",
|
|
49
|
+
"get-tsconfig": "4.14.0",
|
|
49
50
|
"highlight.js": "11.11.1",
|
|
50
51
|
"istanbul-lib-coverage": "3.2.2",
|
|
51
52
|
"istanbul-lib-report": "3.0.1",
|
|
@@ -54,50 +55,53 @@
|
|
|
54
55
|
"picomatch": "4.0.4",
|
|
55
56
|
"std-env": "4.0.0",
|
|
56
57
|
"tinyrainbow": "3.1.0",
|
|
57
|
-
"ws": "8.20.
|
|
58
|
+
"ws": "8.20.1"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
|
-
"@isentinel/eslint-config": "5.0.0-beta.
|
|
61
|
+
"@isentinel/eslint-config": "5.0.0-beta.11",
|
|
62
|
+
"@isentinel/roblox-ts": "4.0.2",
|
|
61
63
|
"@isentinel/tsconfig": "1.2.0",
|
|
62
|
-
"@
|
|
64
|
+
"@isentinel/weld": "0.2.0",
|
|
65
|
+
"@oxc-project/types": "0.123.0",
|
|
63
66
|
"@rbxts/jest": "3.13.3-ts.1",
|
|
64
|
-
"@rbxts/types": "1.0.
|
|
67
|
+
"@rbxts/types": "1.0.920",
|
|
65
68
|
"@total-typescript/shoehorn": "0.1.2",
|
|
66
|
-
"@tsdown/exe": "0.
|
|
69
|
+
"@tsdown/exe": "0.22.0",
|
|
67
70
|
"@types/istanbul-lib-coverage": "2.0.6",
|
|
68
71
|
"@types/istanbul-lib-report": "3.0.3",
|
|
69
72
|
"@types/istanbul-reports": "3.0.4",
|
|
70
|
-
"@types/node": "24.12.
|
|
71
|
-
"@types/picomatch": "4.0.
|
|
73
|
+
"@types/node": "24.12.3",
|
|
74
|
+
"@types/picomatch": "4.0.3",
|
|
72
75
|
"@types/ws": "8.18.1",
|
|
73
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
74
|
-
"@vitest/coverage-v8": "4.1.
|
|
75
|
-
"@vitest/eslint-plugin": "1.6.
|
|
76
|
+
"@typescript/native-preview": "7.0.0-dev.20260512.1",
|
|
77
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
78
|
+
"@vitest/eslint-plugin": "1.6.17",
|
|
76
79
|
"better-typescript-lib": "2.12.0",
|
|
77
|
-
"bumpp": "11.0
|
|
80
|
+
"bumpp": "11.1.0",
|
|
78
81
|
"eslint": "9.39.4",
|
|
79
82
|
"eslint-plugin-jest-extended": "3.0.1",
|
|
80
83
|
"eslint-plugin-n": "17.24.0",
|
|
81
84
|
"eslint-plugin-pnpm": "1.6.0",
|
|
82
85
|
"jest-extended": "7.0.0",
|
|
83
|
-
"memfs": "4.57.
|
|
84
|
-
"publint": "0.3.
|
|
85
|
-
"tsdown": "0.
|
|
86
|
+
"memfs": "4.57.2",
|
|
87
|
+
"publint": "0.3.21",
|
|
88
|
+
"tsdown": "0.22.0",
|
|
86
89
|
"type-fest": "5.5.0",
|
|
87
|
-
"typescript": "
|
|
88
|
-
"vitest": "4.1.
|
|
90
|
+
"typescript": "6.0.3",
|
|
91
|
+
"vitest": "4.1.5",
|
|
92
|
+
"@isentinel/rojo-utils": "0.1.0",
|
|
89
93
|
"@isentinel/luau-ast": "0.1.0",
|
|
90
|
-
"@isentinel/roblox-runner": "0.1.0"
|
|
91
|
-
"@isentinel/rojo-utils": "0.1.0"
|
|
94
|
+
"@isentinel/roblox-runner": "0.1.0"
|
|
92
95
|
},
|
|
93
96
|
"engines": {
|
|
94
97
|
"node": ">=24.10.0"
|
|
95
98
|
},
|
|
96
99
|
"scripts": {
|
|
100
|
+
"bench": "vitest bench --project unit",
|
|
97
101
|
"build": "pnpm build:packages && pnpm build:bundle && tsdown && pnpm build:plugin",
|
|
98
|
-
"build:bundle": "
|
|
102
|
+
"build:bundle": "weld",
|
|
99
103
|
"build:packages": "pnpm -r --filter !@isentinel/jest-roblox run build",
|
|
100
|
-
"build:plugin": "
|
|
104
|
+
"build:plugin": "rojo build plugin/plugin.project.json -o plugin/JestRobloxRunner.rbxm",
|
|
101
105
|
"lint": "eslint --cache",
|
|
102
106
|
"lint:ci": "eslint --cache --cache-strategy content",
|
|
103
107
|
"release": "bumpp",
|
|
Binary file
|
|
@@ -11,6 +11,13 @@ end
|
|
|
11
11
|
local WEBSOCKET_URL = "ws://localhost:3001"
|
|
12
12
|
local RECONNECT_DELAY = 0.1
|
|
13
13
|
|
|
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
|
|
18
|
+
-- message rather than running with stale behaviour or an opaque timeout.
|
|
19
|
+
local PROTOCOL_VERSION = 2
|
|
20
|
+
|
|
14
21
|
local IsDebug = ReplicatedStorage:GetAttribute("JEST_ROBLOX_DEBUG") == true
|
|
15
22
|
ReplicatedStorage:GetAttributeChangedSignal("JEST_ROBLOX_DEBUG"):Connect(function(): ()
|
|
16
23
|
IsDebug = ReplicatedStorage:GetAttribute("JEST_ROBLOX_DEBUG") == true
|
|
@@ -96,12 +103,38 @@ local function connect(): ()
|
|
|
96
103
|
end
|
|
97
104
|
|
|
98
105
|
if message.action == "run_tests" then
|
|
106
|
+
-- Version handshake: reject a missing or mismatched
|
|
107
|
+
-- `protocolVersion` synchronously so the user sees an
|
|
108
|
+
-- upgrade message rather than stale-semantics behaviour.
|
|
109
|
+
local clientVersion = if typeof(message.protocolVersion) == "number"
|
|
110
|
+
then message.protocolVersion
|
|
111
|
+
else nil
|
|
112
|
+
if clientVersion ~= PROTOCOL_VERSION then
|
|
113
|
+
warn(
|
|
114
|
+
"[jest-roblox] Protocol version mismatch — CLI v"
|
|
115
|
+
.. tostring(clientVersion)
|
|
116
|
+
.. ", plugin v"
|
|
117
|
+
.. tostring(PROTOCOL_VERSION)
|
|
118
|
+
)
|
|
119
|
+
ws:Send(HttpService:JSONEncode({
|
|
120
|
+
type = "version_mismatch",
|
|
121
|
+
actualVersion = PROTOCOL_VERSION,
|
|
122
|
+
expectedVersion = clientVersion or 0,
|
|
123
|
+
request_id = message.request_id,
|
|
124
|
+
}))
|
|
125
|
+
return
|
|
126
|
+
end
|
|
127
|
+
|
|
99
128
|
print("[jest-roblox] Running tests via StudioTestService...")
|
|
100
129
|
|
|
101
130
|
local runOk, result = pcall(function(): any
|
|
102
131
|
return StudioTestService:ExecuteRunModeAsync({
|
|
103
132
|
test = true,
|
|
104
133
|
config = message.config,
|
|
134
|
+
-- Filtered injection targets (parallel to configs)
|
|
135
|
+
-- so the Run Mode runner skips mounts that already
|
|
136
|
+
-- have a user-authored jest.config on disk.
|
|
137
|
+
runtimeStubMounts = message.runtimeStubMounts,
|
|
105
138
|
})
|
|
106
139
|
end)
|
|
107
140
|
|
|
@@ -109,6 +142,7 @@ local function connect(): ()
|
|
|
109
142
|
warn("[jest-roblox] ExecuteRunModeAsync failed: " .. tostring(result))
|
|
110
143
|
ws:Send(HttpService:JSONEncode({
|
|
111
144
|
type = "results",
|
|
145
|
+
protocolVersion = PROTOCOL_VERSION,
|
|
112
146
|
request_id = message.request_id,
|
|
113
147
|
jestOutput = HttpService:JSONEncode({
|
|
114
148
|
success = false,
|
|
@@ -121,6 +155,11 @@ local function connect(): ()
|
|
|
121
155
|
|
|
122
156
|
ws:Send(HttpService:JSONEncode({
|
|
123
157
|
type = "results",
|
|
158
|
+
-- Echo the protocol version. A stale plugin that ignored
|
|
159
|
+
-- the request-side version would also omit the echo, so
|
|
160
|
+
-- the CLI's schema validation rejects it as malformed
|
|
161
|
+
-- rather than treating it as a valid v1 success.
|
|
162
|
+
protocolVersion = PROTOCOL_VERSION,
|
|
124
163
|
request_id = message.request_id,
|
|
125
164
|
jestOutput = result.jestOutput or HttpService:JSONEncode({
|
|
126
165
|
success = false,
|
|
@@ -31,7 +31,6 @@ if testArgs == nil then
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
if testArgs == nil then
|
|
34
|
-
endWithError("StudioTestService:GetTestArgs() returned nil after 50 polls (5s)")
|
|
35
34
|
return
|
|
36
35
|
end
|
|
37
36
|
|
|
@@ -60,7 +59,123 @@ if not requireOk then
|
|
|
60
59
|
return
|
|
61
60
|
end
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
-- Runtime injection of jest.config ModuleScripts per config. Studio +
|
|
63
|
+
-- multi-project does NOT write jest.config.luau into the user's source
|
|
64
|
+
-- tree — the configs travel via the WebSocket payload and we materialize
|
|
65
|
+
-- them into DataModel ModuleScripts just before Jest discovery runs. The
|
|
66
|
+
-- session-UUID `_G` key keeps each Run Mode invocation isolated against
|
|
67
|
+
-- re-entrancy (the assert below fails loud if state from a prior run
|
|
68
|
+
-- somehow persisted).
|
|
69
|
+
local SESSION_KEY = "__JEST_ROBLOX_RT_" .. HttpService:GenerateGUID(false):gsub("-", "")
|
|
70
|
+
assert(_G[SESSION_KEY] == nil, "Session key collision — re-entrant run detected")
|
|
71
|
+
_G[SESSION_KEY] = { configs = {} }
|
|
72
|
+
|
|
73
|
+
local function findInstance(path: string): Instance
|
|
74
|
+
local parts = string.split(path, "/")
|
|
75
|
+
local current: any = game:FindService(parts[1])
|
|
76
|
+
assert(current, "service not found: " .. parts[1])
|
|
77
|
+
for i = 2, #parts do
|
|
78
|
+
current = current:FindFirstChild(parts[i])
|
|
79
|
+
assert(current, "missing segment '" .. parts[i] .. "' in " .. path)
|
|
80
|
+
end
|
|
81
|
+
return current
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
-- Per-config record of injected ModuleScripts so cleanup after each
|
|
85
|
+
-- config's Jest invocation can destroy exactly the stubs that config
|
|
86
|
+
-- created. Per-config (rather than upfront-all) avoids the nested-project
|
|
87
|
+
-- contamination path where configs at e.g. `ReplicatedStorage` and
|
|
88
|
+
-- `ReplicatedStorage/Foo` would both have stubs in DataModel
|
|
89
|
+
-- simultaneously and Jest's parent-traversal could resolve the wrong one.
|
|
90
|
+
local injectedPerConfig: { [number]: { Instance } } = {}
|
|
91
|
+
|
|
92
|
+
-- The CLI sends a filtered `runtimeStubMounts[i]` list per config,
|
|
93
|
+
-- excluding mounts that already have a user-authored `jest.config.luau`
|
|
94
|
+
-- on disk (synced naturally by Rojo). We must iterate this filtered list,
|
|
95
|
+
-- not `cfg.projects`, or we'd inject over the user's canonical config.
|
|
96
|
+
-- Defaults to empty when the field is missing (defensive against
|
|
97
|
+
-- malformed payload from a divergent plugin).
|
|
98
|
+
local function injectStubsForConfig(cfg, configIndex: number, injectionPaths: { string })
|
|
99
|
+
local sessionTable = _G[SESSION_KEY]
|
|
100
|
+
sessionTable.configs[configIndex] = cfg
|
|
101
|
+
-- Register the slot BEFORE parenting any stub. If a later mount
|
|
102
|
+
-- throws (findInstance miss, structural collision), the partially-
|
|
103
|
+
-- parented stubs are already tracked so cleanupStubsForConfig /
|
|
104
|
+
-- cleanupAllStubs can find and destroy them. Without this, an early
|
|
105
|
+
-- stub would linger in DataModel after cleanup.
|
|
106
|
+
local injected: { Instance } = {}
|
|
107
|
+
injectedPerConfig[configIndex] = injected
|
|
108
|
+
|
|
109
|
+
for _, projectPath in injectionPaths do
|
|
110
|
+
local leaf = findInstance(projectPath)
|
|
111
|
+
assert(
|
|
112
|
+
leaf:FindFirstChild("jest.config") == nil,
|
|
113
|
+
"Structural collision: jest.config already exists under "
|
|
114
|
+
.. projectPath
|
|
115
|
+
.. ". This may be a user-authored config not declared as a string entry, "
|
|
116
|
+
.. "a rojo tree mapping, or a model-file mount."
|
|
117
|
+
)
|
|
118
|
+
local stub = Instance.new("ModuleScript")
|
|
119
|
+
stub.Name = "jest.config"
|
|
120
|
+
stub.Source = string.format(
|
|
121
|
+
'return _G["%s"].configs[%d]',
|
|
122
|
+
SESSION_KEY,
|
|
123
|
+
configIndex
|
|
124
|
+
)
|
|
125
|
+
stub.Parent = leaf
|
|
126
|
+
-- Record immediately after parenting so a subsequent mount's
|
|
127
|
+
-- failure does not strand this one.
|
|
128
|
+
table.insert(injected, stub)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
local function cleanupStubsForConfig(configIndex: number)
|
|
133
|
+
local injected = injectedPerConfig[configIndex]
|
|
134
|
+
if injected ~= nil then
|
|
135
|
+
for _, inst in injected do
|
|
136
|
+
pcall(function() inst:Destroy() end)
|
|
137
|
+
end
|
|
138
|
+
injectedPerConfig[configIndex] = nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
local sessionTable = _G[SESSION_KEY]
|
|
142
|
+
if sessionTable ~= nil then
|
|
143
|
+
sessionTable.configs[configIndex] = nil
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
local function cleanupAllStubs()
|
|
148
|
+
for index in injectedPerConfig do
|
|
149
|
+
cleanupStubsForConfig(index)
|
|
150
|
+
end
|
|
151
|
+
_G[SESSION_KEY] = nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
-- Per-config injection target lists, parallel to `configs`. The CLI sends a
|
|
155
|
+
-- pre-filtered list excluding mount paths whose source tree already has a
|
|
156
|
+
-- user-authored `jest.config.luau` synced by Rojo. Empty entry (or missing
|
|
157
|
+
-- field) means: skip injection for this config. The hooks below let
|
|
158
|
+
-- `Runner.runProjects` call our inject/cleanup just-in-time per config, so
|
|
159
|
+
-- nested-project mounts (e.g. `ReplicatedStorage` and `ReplicatedStorage/Foo`)
|
|
160
|
+
-- never have stubs in DataModel simultaneously — which would let Jest's
|
|
161
|
+
-- parent-traversal config lookup resolve the wrong project's config.
|
|
162
|
+
local runtimeStubMounts: { { string } } = testArgs.runtimeStubMounts
|
|
163
|
+
or table.create(#configs, {})
|
|
164
|
+
|
|
165
|
+
local hooks = {
|
|
166
|
+
beforeConfig = function(cfg, index)
|
|
167
|
+
local paths = runtimeStubMounts[index] or {}
|
|
168
|
+
injectStubsForConfig(cfg, index, paths)
|
|
169
|
+
end,
|
|
170
|
+
afterConfig = function(_cfg, index)
|
|
171
|
+
cleanupStubsForConfig(index)
|
|
172
|
+
end,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
local runOk, entriesOrErr = pcall(Runner.runProjects, script, configs, hooks)
|
|
176
|
+
-- Defensive: also clear `_G` even if every afterConfig hook already cleaned
|
|
177
|
+
-- per-config. Belt-and-suspenders against a hook-call that yielded early.
|
|
178
|
+
cleanupAllStubs()
|
|
64
179
|
if not runOk then
|
|
65
180
|
endWithError("Runner.runProjects threw: " .. tostring(entriesOrErr))
|
|
66
181
|
return
|