@isentinel/jest-roblox 0.0.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/bin/jest-roblox.js +4 -0
- package/dist/cli.d.mts +8 -0
- package/dist/cli.mjs +281 -0
- package/dist/game-output-BKBGosEI.mjs +1362 -0
- package/dist/index.d.mts +243 -0
- package/dist/index.mjs +3 -0
- package/dist/schema-ryuVGD35.d.mts +826 -0
- package/package.json +80 -0
- package/plugin/JestRobloxRunner.rbxm +0 -0
- package/plugin/plugin.project.json +6 -0
- package/plugin/sourcemap.json +1 -0
- package/plugin/src/init.server.luau +193 -0
- package/plugin/src/test-in-run-mode.server.luau +36 -0
- package/plugin/src/test-runner.luau +145 -0
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@isentinel/jest-roblox",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Jest-compatible CLI for running roblox-ts tests via Roblox Open Cloud",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jest",
|
|
7
|
+
"roblox",
|
|
8
|
+
"rbxts",
|
|
9
|
+
"testing",
|
|
10
|
+
"cli"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Christopher Buss <christopher.buss@pm.me> (https://github.com/christopher-buss)",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./dist/index.mjs",
|
|
18
|
+
"./cli": "./dist/cli.mjs",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"bin": {
|
|
23
|
+
"jest-roblox": "./bin/jest-roblox.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"bin",
|
|
27
|
+
"dist",
|
|
28
|
+
"plugin",
|
|
29
|
+
"!dist/**/*.map",
|
|
30
|
+
"!dist/**/*.tsbuildinfo"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"arktype": "2.1.29",
|
|
34
|
+
"highlight.js": "11.11.1",
|
|
35
|
+
"jiti": "2.6.1",
|
|
36
|
+
"tinyrainbow": "3.0.3",
|
|
37
|
+
"ws": "8.18.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@isentinel/eslint-config": "4.7.6",
|
|
41
|
+
"@rbxts/jest": "3.13.3-ts.1",
|
|
42
|
+
"@rbxts/types": "1.0.896",
|
|
43
|
+
"@types/node": "24.10.4",
|
|
44
|
+
"@types/ws": "8.5.13",
|
|
45
|
+
"better-typescript-lib": "2.12.0",
|
|
46
|
+
"bumpp": "10.4.1",
|
|
47
|
+
"publint": "0.3.15",
|
|
48
|
+
"tsdown": "0.20.1",
|
|
49
|
+
"type-fest": "5.2.0",
|
|
50
|
+
"vitest": "4.0.16"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=20.0.0"
|
|
54
|
+
},
|
|
55
|
+
"nx": {
|
|
56
|
+
"tags": [
|
|
57
|
+
"scope:tooling",
|
|
58
|
+
"type:cli"
|
|
59
|
+
],
|
|
60
|
+
"projectType": "application",
|
|
61
|
+
"targets": {
|
|
62
|
+
"lint": {
|
|
63
|
+
"command": "eslint",
|
|
64
|
+
"hasTypeAwareRules": true
|
|
65
|
+
},
|
|
66
|
+
"test": {
|
|
67
|
+
"command": "vitest run",
|
|
68
|
+
"options": {
|
|
69
|
+
"cwd": "tools/jest-roblox-cli"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"scripts": {
|
|
75
|
+
"build": "tsdown",
|
|
76
|
+
"build:plugin": "rojo build plugin/plugin.project.json -o plugin/JestRobloxRunner.rbxm",
|
|
77
|
+
"release": "bumpp",
|
|
78
|
+
"watch": "tsdown --watch"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"JestRobloxRunner","className":"Script","filePaths":["src\\init.server.luau","plugin.project.json"],"children":[{"name":"test-in-run-mode","className":"Script","filePaths":["src\\test-in-run-mode.server.luau"]},{"name":"test-runner","className":"ModuleScript","filePaths":["src\\test-runner.luau"]}]}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
local HttpService = game:GetService("HttpService")
|
|
3
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
4
|
+
local RunService = game:GetService("RunService")
|
|
5
|
+
local StudioTestService = game:GetService("StudioTestService")
|
|
6
|
+
|
|
7
|
+
if RunService:IsRunning() then
|
|
8
|
+
return
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
local WEBSOCKET_URL = "ws://localhost:3001"
|
|
12
|
+
local RECONNECT_DELAY = 5
|
|
13
|
+
|
|
14
|
+
local IsDebug = ReplicatedStorage:GetAttribute("JEST_ROBLOX_DEBUG") == true
|
|
15
|
+
ReplicatedStorage:GetAttributeChangedSignal("JEST_ROBLOX_DEBUG"):Connect(function(): ()
|
|
16
|
+
IsDebug = ReplicatedStorage:GetAttribute("JEST_ROBLOX_DEBUG") == true
|
|
17
|
+
end)
|
|
18
|
+
|
|
19
|
+
local toolbar = plugin:CreateToolbar("Jest-Roblox")
|
|
20
|
+
local startButton =
|
|
21
|
+
toolbar:CreateButton("Jest Roblox", "Connect to jest-roblox CLI", "rbxassetid://4458901886")
|
|
22
|
+
local stopButton =
|
|
23
|
+
toolbar:CreateButton("Stop", "Disconnect from jest-roblox CLI", "rbxassetid://4458901886")
|
|
24
|
+
startButton.Enabled = false
|
|
25
|
+
|
|
26
|
+
local autoReconnect = true
|
|
27
|
+
local ws: WebStreamClient? = nil
|
|
28
|
+
local connections: { RBXScriptConnection } = {}
|
|
29
|
+
|
|
30
|
+
local _print = print
|
|
31
|
+
local _warn = warn
|
|
32
|
+
|
|
33
|
+
local function print(...: any): ()
|
|
34
|
+
if IsDebug then
|
|
35
|
+
_print("[jest-roblox]", ...)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
local function warn(...: any): ()
|
|
40
|
+
if IsDebug then
|
|
41
|
+
_warn("[jest-roblox]", ...)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
local function clearConnections(): ()
|
|
46
|
+
for _, connection in connections do
|
|
47
|
+
connection:Disconnect()
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
table.clear(connections)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
local function disconnect(): ()
|
|
54
|
+
clearConnections()
|
|
55
|
+
|
|
56
|
+
if ws then
|
|
57
|
+
pcall(function(): ()
|
|
58
|
+
assert(ws, "Luau")
|
|
59
|
+
ws:Close()
|
|
60
|
+
end)
|
|
61
|
+
ws = nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local function connect(): ()
|
|
66
|
+
clearConnections()
|
|
67
|
+
|
|
68
|
+
print("[jest-roblox] Connecting to " .. WEBSOCKET_URL .. "...")
|
|
69
|
+
|
|
70
|
+
local success, socket = pcall(function(): WebStreamClient
|
|
71
|
+
return HttpService:CreateWebStreamClient(
|
|
72
|
+
Enum.WebStreamClientType.WebSocket,
|
|
73
|
+
{ Url = WEBSOCKET_URL }
|
|
74
|
+
)
|
|
75
|
+
end)
|
|
76
|
+
|
|
77
|
+
if not success then
|
|
78
|
+
warn("[jest-roblox] Failed to connect: " .. tostring(socket))
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
ws = socket
|
|
83
|
+
assert(ws, "Luau")
|
|
84
|
+
|
|
85
|
+
table.insert(
|
|
86
|
+
connections,
|
|
87
|
+
ws.MessageReceived:Connect(function(rawMessage: string): ()
|
|
88
|
+
assert(ws, "Luau")
|
|
89
|
+
|
|
90
|
+
local ok, message = pcall(function(): any
|
|
91
|
+
return HttpService:JSONDecode(rawMessage)
|
|
92
|
+
end)
|
|
93
|
+
if not ok then
|
|
94
|
+
warn("[jest-roblox] Failed to decode message:", rawMessage)
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if message.action == "run_tests" then
|
|
99
|
+
print("[jest-roblox] Running tests via StudioTestService...")
|
|
100
|
+
|
|
101
|
+
local runOk, result = pcall(function(): any
|
|
102
|
+
return StudioTestService:ExecuteRunModeAsync({
|
|
103
|
+
test = true,
|
|
104
|
+
config = message.config,
|
|
105
|
+
})
|
|
106
|
+
end)
|
|
107
|
+
|
|
108
|
+
if not runOk then
|
|
109
|
+
warn("[jest-roblox] ExecuteRunModeAsync failed: " .. tostring(result))
|
|
110
|
+
ws:Send(HttpService:JSONEncode({
|
|
111
|
+
type = "results",
|
|
112
|
+
request_id = message.request_id,
|
|
113
|
+
jestOutput = HttpService:JSONEncode({
|
|
114
|
+
success = false,
|
|
115
|
+
err = tostring(result),
|
|
116
|
+
}),
|
|
117
|
+
gameOutput = "[]",
|
|
118
|
+
}))
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
ws:Send(HttpService:JSONEncode({
|
|
123
|
+
type = "results",
|
|
124
|
+
request_id = message.request_id,
|
|
125
|
+
jestOutput = result.jestOutput or HttpService:JSONEncode({
|
|
126
|
+
success = false,
|
|
127
|
+
err = "No jestOutput in run-mode result",
|
|
128
|
+
}),
|
|
129
|
+
gameOutput = result.gameOutput or "[]",
|
|
130
|
+
}))
|
|
131
|
+
|
|
132
|
+
print("[jest-roblox] Results sent")
|
|
133
|
+
end
|
|
134
|
+
end)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
local opened = ws.Opened:Once(function(): ()
|
|
138
|
+
print("[jest-roblox] Connected to CLI")
|
|
139
|
+
end)
|
|
140
|
+
table.insert(connections, opened)
|
|
141
|
+
|
|
142
|
+
local err = ws.Error:Once(function(_statusCode: number, errorMessage: string): ()
|
|
143
|
+
warn("[jest-roblox] Connection error: " .. errorMessage)
|
|
144
|
+
end)
|
|
145
|
+
table.insert(connections, err)
|
|
146
|
+
|
|
147
|
+
local closed = ws.Closed:Once(function(): ()
|
|
148
|
+
ws = nil
|
|
149
|
+
if autoReconnect then
|
|
150
|
+
print("[jest-roblox] Disconnected, reconnecting in " .. RECONNECT_DELAY .. "s...")
|
|
151
|
+
local thread = task.delay(RECONNECT_DELAY, function(): ()
|
|
152
|
+
connect()
|
|
153
|
+
end)
|
|
154
|
+
table.insert(connections :: any, {
|
|
155
|
+
Disconnect = function(): ()
|
|
156
|
+
if coroutine.running() then
|
|
157
|
+
local wasCancelled = pcall(function()
|
|
158
|
+
task.cancel(thread)
|
|
159
|
+
end)
|
|
160
|
+
|
|
161
|
+
if not wasCancelled then
|
|
162
|
+
task.defer(function()
|
|
163
|
+
task.cancel(thread)
|
|
164
|
+
end)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end,
|
|
168
|
+
})
|
|
169
|
+
end
|
|
170
|
+
end)
|
|
171
|
+
table.insert(connections, closed)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
startButton.Click:Connect(function(): ()
|
|
175
|
+
startButton.Enabled = false
|
|
176
|
+
stopButton.Enabled = true
|
|
177
|
+
autoReconnect = true
|
|
178
|
+
connect()
|
|
179
|
+
end)
|
|
180
|
+
|
|
181
|
+
stopButton.Click:Connect(function(): ()
|
|
182
|
+
stopButton.Enabled = false
|
|
183
|
+
startButton.Enabled = true
|
|
184
|
+
autoReconnect = false
|
|
185
|
+
disconnect()
|
|
186
|
+
end)
|
|
187
|
+
|
|
188
|
+
plugin.Unloading:Connect(function(): ()
|
|
189
|
+
autoReconnect = false
|
|
190
|
+
disconnect()
|
|
191
|
+
end)
|
|
192
|
+
|
|
193
|
+
task.defer(connect)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
local HttpService = game:GetService("HttpService")
|
|
3
|
+
local RunService = game:GetService("RunService")
|
|
4
|
+
local StudioTestService = game:GetService("StudioTestService")
|
|
5
|
+
|
|
6
|
+
if not RunService:IsRunning() then
|
|
7
|
+
return
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
local testArgs = StudioTestService:GetTestArgs()
|
|
11
|
+
if not testArgs or not testArgs.test then
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
local loadStringEnabled = pcall(function()
|
|
16
|
+
loadstring("return true")
|
|
17
|
+
end)
|
|
18
|
+
|
|
19
|
+
if not loadStringEnabled then
|
|
20
|
+
StudioTestService:EndTest({
|
|
21
|
+
jestOutput = HttpService:JSONEncode({
|
|
22
|
+
success = false,
|
|
23
|
+
err = "LoadString must be enabled in ServerScriptService to run tests",
|
|
24
|
+
}),
|
|
25
|
+
gameOutput = "[]",
|
|
26
|
+
})
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
local TestRunner = require(script.Parent["test-runner"])
|
|
31
|
+
local jestOutput, gameOutput = TestRunner.runTestsAync(script, testArgs.config or {})
|
|
32
|
+
|
|
33
|
+
StudioTestService:EndTest({
|
|
34
|
+
jestOutput = jestOutput,
|
|
35
|
+
gameOutput = gameOutput,
|
|
36
|
+
})
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
local HttpService = game:GetService("HttpService")
|
|
3
|
+
local LogService = game:GetService("LogService")
|
|
4
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
5
|
+
|
|
6
|
+
local module = {}
|
|
7
|
+
|
|
8
|
+
function module.findInstance(path: string): Instance
|
|
9
|
+
local parts = string.split(path, "/")
|
|
10
|
+
|
|
11
|
+
local result, current: Instance? = pcall(function(): Instance
|
|
12
|
+
return game:FindService(parts[1])
|
|
13
|
+
end)
|
|
14
|
+
assert(result, `Failed to find service {parts[1]}: {current}`)
|
|
15
|
+
|
|
16
|
+
for i = 2, #parts do
|
|
17
|
+
assert(current, `Failed to find instance at {parts[i]}`)
|
|
18
|
+
current = current:FindFirstChild(parts[i])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
assert(current, `Failed to find instance at path {path}: {current}`)
|
|
22
|
+
|
|
23
|
+
return current
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
function module.getJest(config: { jestPath: string? }): ModuleScript
|
|
27
|
+
local jestPath = config.jestPath
|
|
28
|
+
if jestPath then
|
|
29
|
+
local instance = module.findInstance(jestPath)
|
|
30
|
+
assert(instance, `Failed to find Jest instance at path {jestPath}`)
|
|
31
|
+
assert(instance:IsA("ModuleScript"), `Instance at path {jestPath} is not a ModuleScript`)
|
|
32
|
+
return instance
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
local jestInstance = ReplicatedStorage:FindFirstChild("Jest", true)
|
|
36
|
+
assert(jestInstance, "Failed to find Jest instance in ReplicatedStorage")
|
|
37
|
+
assert(
|
|
38
|
+
jestInstance:IsA("ModuleScript"),
|
|
39
|
+
"Jest instance in ReplicatedStorage is not a ModuleScript"
|
|
40
|
+
)
|
|
41
|
+
return jestInstance
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
function module.runTestsAync(
|
|
45
|
+
callingScript: LuaSourceContainer,
|
|
46
|
+
config: {
|
|
47
|
+
[string]: any,
|
|
48
|
+
}
|
|
49
|
+
): (string, string)
|
|
50
|
+
warn("Running tests with config:", config)
|
|
51
|
+
local t0 = os.clock()
|
|
52
|
+
local timingEnabled = config._timing
|
|
53
|
+
|
|
54
|
+
local t_findJest0 = os.clock()
|
|
55
|
+
local findSuccess, findValue = pcall(function(): ModuleScript
|
|
56
|
+
return module.getJest(config)
|
|
57
|
+
end)
|
|
58
|
+
local t_findJest = os.clock()
|
|
59
|
+
|
|
60
|
+
if not findSuccess then
|
|
61
|
+
local logSuccess, logHistory = pcall(function(): string
|
|
62
|
+
return HttpService:JSONEncode(LogService:GetLogHistory())
|
|
63
|
+
end)
|
|
64
|
+
|
|
65
|
+
return HttpService:JSONEncode({ success = false, err = findValue }),
|
|
66
|
+
if logSuccess then logHistory else "[]"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
local t_requireJest0 = os.clock()
|
|
70
|
+
local Jest = (require :: any)(findValue)
|
|
71
|
+
local t_requireJest = os.clock()
|
|
72
|
+
|
|
73
|
+
local function run(): string
|
|
74
|
+
LogService:ClearOutput()
|
|
75
|
+
|
|
76
|
+
local t_resolveProjects0 = os.clock()
|
|
77
|
+
local projects = {}
|
|
78
|
+
|
|
79
|
+
assert(
|
|
80
|
+
config.projects and #config.projects > 0,
|
|
81
|
+
"No projects configured. Set 'projects' in jest.config.ts or pass --projects."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
for _, projectPath in config.projects do
|
|
85
|
+
table.insert(projects, module.findInstance(projectPath))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
config.projects = {}
|
|
89
|
+
local t_resolveProjects = os.clock()
|
|
90
|
+
|
|
91
|
+
local t_resolveSetupFiles0 = os.clock()
|
|
92
|
+
if config.setupFiles and #config.setupFiles > 0 then
|
|
93
|
+
local resolved = {}
|
|
94
|
+
|
|
95
|
+
for _, setupPath in config.setupFiles do
|
|
96
|
+
table.insert(resolved, module.findInstance(setupPath))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
config.setupFiles = resolved
|
|
100
|
+
end
|
|
101
|
+
local t_resolveSetupFiles = os.clock()
|
|
102
|
+
|
|
103
|
+
config._timing = nil
|
|
104
|
+
|
|
105
|
+
local t_jestRunCLI0 = os.clock()
|
|
106
|
+
local jestResult = Jest.runCLI(callingScript, config, projects):expect()
|
|
107
|
+
local t_jestRunCLI = os.clock()
|
|
108
|
+
|
|
109
|
+
local result: { [string]: any } = {
|
|
110
|
+
success = true,
|
|
111
|
+
value = jestResult,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if timingEnabled then
|
|
115
|
+
result._timing = {
|
|
116
|
+
configDecode = 0,
|
|
117
|
+
findJest = t_findJest - t_findJest0,
|
|
118
|
+
requireJest = t_requireJest - t_requireJest0,
|
|
119
|
+
resolveProjects = t_resolveProjects - t_resolveProjects0,
|
|
120
|
+
resolveSetupFiles = t_resolveSetupFiles - t_resolveSetupFiles0,
|
|
121
|
+
jestRunCLI = t_jestRunCLI - t_jestRunCLI0,
|
|
122
|
+
total = os.clock() - t0,
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
return HttpService:JSONEncode(result)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
local runSuccess, runValue = pcall(run)
|
|
130
|
+
|
|
131
|
+
local jestResult: string
|
|
132
|
+
if not runSuccess then
|
|
133
|
+
jestResult = HttpService:JSONEncode({ success = false, err = runValue })
|
|
134
|
+
else
|
|
135
|
+
jestResult = runValue
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
local logSuccess, logHistory = pcall(function(): string
|
|
139
|
+
return HttpService:JSONEncode(LogService:GetLogHistory())
|
|
140
|
+
end)
|
|
141
|
+
|
|
142
|
+
return jestResult, if logSuccess then logHistory else "[]"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
return module
|