@isentinel/jest-roblox 0.2.7 → 0.3.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.
@@ -1,301 +0,0 @@
1
- --!strict
2
- local HttpService = game:GetService("HttpService")
3
- local LogService = game:GetService("LogService")
4
-
5
- local InstanceResolver = require(script.Parent:FindFirstChild('instance-resolver'))
6
- local SetupTiming = require(script.Parent:FindFirstChild('setup-timing'))
7
- local SnapshotPatch = require(script.Parent:FindFirstChild('snapshot-patch'))
8
-
9
- type Config = {
10
- jestPath: string?,
11
- projects: { string }?,
12
- setupFiles: { string }?,
13
- setupFilesAfterEnv: { string }?,
14
- _coverage: boolean?,
15
- _timing: boolean?,
16
- }
17
-
18
- local function fail(err: string)
19
- return {
20
- success = false,
21
- err = err,
22
- }
23
- end
24
-
25
- type CapturedMessage = { message: string, messageType: number, timestamp: number }
26
-
27
- local function interceptWriteable(writeable: any, buffer: { CapturedMessage }, messageType: number)
28
- local original = writeable._writeFn
29
- if typeof(original) ~= "function" then
30
- return function() end
31
- end
32
-
33
- writeable._writeFn = function(data: string)
34
- table.insert(buffer, {
35
- message = data,
36
- messageType = messageType,
37
- timestamp = os.clock(),
38
- })
39
- original(data)
40
- end
41
-
42
- return function()
43
- writeable._writeFn = original
44
- end
45
- end
46
-
47
- local module = {}
48
-
49
- function module.run(callingScript: LuaSourceContainer, config: Config): (string, string)
50
- local t0 = os.clock()
51
- local timingEnabled = config._timing
52
- local coverageEnabled = config._coverage
53
-
54
- local t_findJest0 = os.clock()
55
- local findSuccess, findValue = pcall(InstanceResolver.getJest, config)
56
- local t_findJest = os.clock()
57
-
58
- if not findSuccess then
59
- local logSuccess, logHistory = pcall(function()
60
- return HttpService:JSONEncode(LogService:GetLogHistory())
61
- end)
62
-
63
- return HttpService:JSONEncode(fail(findValue :: any)), if logSuccess then logHistory else "[]"
64
- end
65
-
66
- LogService:ClearOutput()
67
-
68
- local snapshotWrites: { [string]: string } = {}
69
-
70
- local t_patchSnapshot0 = os.clock()
71
- local patchState = SnapshotPatch.patch(findValue, snapshotWrites)
72
- local t_patchSnapshot = os.clock()
73
-
74
- local t_requireJest0 = os.clock()
75
- local Jest = (require :: any)(findValue)
76
- local t_requireJest = os.clock()
77
-
78
- -- Intercept Jest's stdout/stderr to capture output synchronously.
79
- -- Jest writes via process.stdout/stderr (Writeable objects whose _writeFn
80
- -- defaults to print). Wrapping _writeFn captures messages like
81
- -- "No tests found" that are printed just before exit(1) throws.
82
- local capturedMessages: { CapturedMessage } = {}
83
- local restoreStdout: (() -> ())?
84
- local restoreStderr: (() -> ())?
85
-
86
- local interceptOk = pcall(function()
87
- local nodeModules = findValue.Parent.Parent.Parent :: any
88
- local RobloxShared = (require :: any)(nodeModules["@rbxts-js"].RobloxShared)
89
- local process = RobloxShared.nodeUtils.process
90
-
91
- restoreStdout = interceptWriteable(process.stdout, capturedMessages, 0)
92
- restoreStderr = interceptWriteable(process.stderr, capturedMessages, 1)
93
- end)
94
-
95
- if not interceptOk then
96
- restoreStdout = nil
97
- restoreStderr = nil
98
- end
99
-
100
- local function runTests()
101
- local t_resolveProjects0 = os.clock()
102
- local projects = {}
103
-
104
- assert(
105
- config.projects and #config.projects > 0,
106
- "No projects configured. Set 'projects' in jest.config.ts or pass --projects."
107
- )
108
-
109
- for _, projectPath in config.projects do
110
- table.insert(projects, InstanceResolver.findInstance(projectPath))
111
- end
112
-
113
- config.projects = {}
114
- local t_resolveProjects = os.clock()
115
-
116
- local t_resolveSetupFiles0 = os.clock()
117
- if config.setupFiles and #config.setupFiles > 0 then
118
- local resolved = {}
119
-
120
- for _, setupPath in config.setupFiles do
121
- table.insert(resolved, InstanceResolver.findInstance(setupPath))
122
- end
123
-
124
- config.setupFiles = resolved :: any
125
- end
126
- if config.setupFilesAfterEnv and #config.setupFilesAfterEnv > 0 then
127
- local resolved = {}
128
-
129
- for _, setupPath in config.setupFilesAfterEnv do
130
- table.insert(resolved, InstanceResolver.findInstance(setupPath))
131
- end
132
-
133
- config.setupFilesAfterEnv = resolved :: any
134
- end
135
- local t_resolveSetupFiles = os.clock()
136
-
137
- local setupTimingState = SetupTiming.patch(
138
- findValue,
139
- config.setupFiles :: any,
140
- config.setupFilesAfterEnv :: any
141
- )
142
-
143
- -- Strip private keys before Jest.runCLI (safe: single-task execution per VM)
144
- config._timing = nil :: any
145
- config._coverage = nil :: any
146
-
147
- if coverageEnabled then
148
- _G.__jest_roblox_cov = {}
149
- end
150
-
151
- local t_jestRunCLI0 = os.clock()
152
- local runCLIOk, runCLIValue = pcall(function()
153
- return Jest.runCLI(callingScript, config, projects):expect()
154
- end)
155
- local t_jestRunCLI = os.clock()
156
-
157
- local setupSeconds = SetupTiming.getSeconds(setupTimingState)
158
- SetupTiming.unpatch(setupTimingState)
159
-
160
- if not runCLIOk then
161
- error(runCLIValue, 0)
162
- end
163
-
164
- local jestResult = runCLIValue
165
-
166
- local result: { [string]: any } = {
167
- success = true,
168
- value = jestResult,
169
- }
170
-
171
- if setupSeconds > 0 then
172
- result._setup = setupSeconds
173
- end
174
-
175
- if timingEnabled then
176
- result._timing = {
177
- findJest = t_findJest - t_findJest0,
178
- patchSnapshot = t_patchSnapshot - t_patchSnapshot0,
179
- requireJest = t_requireJest - t_requireJest0,
180
- resolveProjects = t_resolveProjects - t_resolveProjects0,
181
- resolveSetupFiles = t_resolveSetupFiles - t_resolveSetupFiles0,
182
- jestRunCLI = t_jestRunCLI - t_jestRunCLI0,
183
- total = os.clock() - t0,
184
- }
185
- end
186
-
187
- if next(snapshotWrites) then
188
- result._snapshotWrites = snapshotWrites
189
- end
190
-
191
- if coverageEnabled then
192
- result._coverage = _G.__jest_roblox_cov
193
- end
194
-
195
- return result
196
- end
197
-
198
- local jestDone = false
199
- local runSuccess = false
200
- local runValue: any = nil
201
-
202
- task.spawn(function()
203
- local ok, val = pcall(runTests)
204
- jestDone = true
205
- runSuccess = ok
206
- runValue = val
207
- end)
208
-
209
- local infiniteYieldMessage: string? = nil
210
- local watchdogConnection = LogService.MessageOut:Connect(function(message: string, messageType: Enum.MessageType)
211
- if
212
- messageType == Enum.MessageType.MessageWarning
213
- and string.find(message, "Infinite yield possible")
214
- and not infiniteYieldMessage
215
- then
216
- infiniteYieldMessage = message
217
- end
218
- end)
219
-
220
- while not jestDone and not infiniteYieldMessage do
221
- task.wait(0.1)
222
- end
223
-
224
- watchdogConnection:Disconnect()
225
-
226
- if restoreStdout then
227
- restoreStdout()
228
- end
229
-
230
- if restoreStderr then
231
- restoreStderr()
232
- end
233
-
234
- if not jestDone and infiniteYieldMessage then
235
- runSuccess = false
236
- runValue = "Infinite yield detected, aborting tests: " .. infiniteYieldMessage
237
- end
238
-
239
- SnapshotPatch.unpatch(patchState)
240
-
241
- local jestResult
242
- if not runSuccess then
243
- jestResult = HttpService:JSONEncode(fail(runValue :: any))
244
- else
245
- jestResult = HttpService:JSONEncode(runValue)
246
- end
247
-
248
- local logSuccess, logHistory = pcall(function()
249
- return HttpService:JSONEncode(capturedMessages)
250
- end)
251
-
252
- return jestResult, if logSuccess then logHistory else "[]"
253
- end
254
-
255
- type ProjectEntry = {
256
- jestOutput: string,
257
- gameOutput: string,
258
- elapsedMs: number,
259
- }
260
-
261
- local function encodeExecutionError(err: any): string
262
- return HttpService:JSONEncode({
263
- success = true,
264
- value = {
265
- kind = "ExecutionError",
266
- error = tostring(err),
267
- },
268
- })
269
- end
270
-
271
- -- TODO(runner-tests): dogfood harness for Runner.runProjects
272
- function module.runProjects(
273
- callingScript: LuaSourceContainer,
274
- configs: { Config }
275
- ): { ProjectEntry }
276
- local entries: { ProjectEntry } = {}
277
-
278
- for index, cfg in configs do
279
- local start = os.clock()
280
- local ok, jestOutput, gameOutput = pcall(module.run, callingScript, cfg)
281
- local elapsedMs = math.floor((os.clock() - start) * 1000)
282
-
283
- if ok then
284
- entries[index] = {
285
- jestOutput = jestOutput :: string,
286
- gameOutput = gameOutput :: string,
287
- elapsedMs = elapsedMs,
288
- }
289
- else
290
- entries[index] = {
291
- jestOutput = encodeExecutionError(jestOutput),
292
- gameOutput = "[]",
293
- elapsedMs = elapsedMs,
294
- }
295
- end
296
- end
297
-
298
- return entries
299
- end
300
-
301
- return module
@@ -1,89 +0,0 @@
1
- --!strict
2
- local InstanceResolver = require(script.Parent:FindFirstChild('instance-resolver'))
3
-
4
- local module = {}
5
-
6
- export type PatchState = {
7
- Runtime: any,
8
- originalRequireModule: any,
9
- accumulatedSeconds: { value: number },
10
- }
11
-
12
- function module.patch(
13
- jestModule: ModuleScript,
14
- setupFiles: { Instance }?,
15
- setupFilesAfterEnv: { Instance }?
16
- ): PatchState?
17
- local setupSet: { [Instance]: boolean } = {}
18
-
19
- if setupFiles then
20
- for _, inst in setupFiles do
21
- setupSet[inst] = true
22
- end
23
- end
24
-
25
- if setupFilesAfterEnv then
26
- for _, inst in setupFilesAfterEnv do
27
- setupSet[inst] = true
28
- end
29
- end
30
-
31
- if not next(setupSet) then
32
- return nil
33
- end
34
-
35
- local jestRuntimeModule = InstanceResolver.findSiblingPackage(jestModule, "JestRuntime", "jest-runtime")
36
- if not jestRuntimeModule then
37
- warn("Could not find JestRuntime; setup timing unavailable")
38
- return nil
39
- end
40
-
41
- local Runtime = (require :: any)(jestRuntimeModule)
42
- local originalRequireModule = Runtime.requireModule
43
- local accumulated = { value = 0 }
44
- local insideSetupRequire = false
45
-
46
- Runtime.requireModule = function(self: any, moduleName: any, ...): any
47
- if not insideSetupRequire and typeof(moduleName) == "Instance" and setupSet[moduleName] then
48
- insideSetupRequire = true
49
- local t0 = os.clock()
50
- local results = table.pack(pcall(originalRequireModule, self, moduleName, ...))
51
- accumulated.value += os.clock() - t0
52
- insideSetupRequire = false
53
-
54
- if not results[1] then
55
- error(results[2], 0)
56
- end
57
-
58
- return table.unpack(results, 2, results.n)
59
- end
60
-
61
- return originalRequireModule(self, moduleName, ...)
62
- end
63
-
64
- return {
65
- Runtime = Runtime,
66
- originalRequireModule = originalRequireModule,
67
- accumulatedSeconds = accumulated,
68
- }
69
- end
70
-
71
- function module.unpatch(state: PatchState?)
72
- if not state then
73
- return
74
- end
75
-
76
- if state.Runtime and state.originalRequireModule then
77
- state.Runtime.requireModule = state.originalRequireModule
78
- end
79
- end
80
-
81
- function module.getSeconds(state: PatchState?): number
82
- if not state then
83
- return 0
84
- end
85
-
86
- return state.accumulatedSeconds.value
87
- end
88
-
89
- return module
@@ -1,94 +0,0 @@
1
- --!strict
2
- local CoreScriptSyncService = require(script.Parent:FindFirstChild('mock'):FindFirstChild('CoreScriptSyncService'))
3
- local InstanceResolver = require(script.Parent:FindFirstChild('instance-resolver'))
4
-
5
- local module = {}
6
-
7
- function module.createMockGetDataModelService(snapshotWrites: { [string]: string })
8
- local FileSystemService = require(script.Parent:FindFirstChild('mock'):FindFirstChild('FileSystemService'))(snapshotWrites)
9
-
10
- return function(service: string): any
11
- if service == "FileSystemService" then
12
- return FileSystemService
13
- elseif service == "CoreScriptSyncService" then
14
- return CoreScriptSyncService
15
- end
16
-
17
- local success, result = pcall(function()
18
- local service_ = game:GetService(service)
19
- local _ = service_.Name
20
- return service_
21
- end)
22
-
23
- return if success then result else nil
24
- end
25
- end
26
-
27
- export type PatchState = {
28
- robloxSharedExports: any,
29
- originalGetDataModelService: any,
30
- Runtime: any,
31
- originalRequireInternalModule: any,
32
- }
33
-
34
- function module.patch(jestModule: ModuleScript, snapshotWrites: { [string]: string }): PatchState?
35
- local mockGetDataModelService = module.createMockGetDataModelService(snapshotWrites)
36
-
37
- local robloxSharedInstance = InstanceResolver.findRobloxShared(jestModule)
38
- if not robloxSharedInstance then
39
- warn("Could not find RobloxShared; snapshot support unavailable")
40
- return nil
41
- end
42
-
43
- local robloxSharedExports = (require :: any)(robloxSharedInstance)
44
- local originalGetDataModelService = robloxSharedExports.getDataModelService
45
- robloxSharedExports.getDataModelService = mockGetDataModelService
46
-
47
- local getDataModelServiceChild = robloxSharedInstance:FindFirstChild("getDataModelService")
48
-
49
- local jestRuntimeModule = InstanceResolver.findSiblingPackage(jestModule, "JestRuntime", "jest-runtime")
50
- if not jestRuntimeModule then
51
- warn("Could not find JestRuntime; snapshot interception unavailable")
52
- return {
53
- robloxSharedExports = robloxSharedExports,
54
- originalGetDataModelService = originalGetDataModelService,
55
- Runtime = nil,
56
- originalRequireInternalModule = nil,
57
- }
58
- end
59
-
60
- local Runtime = (require :: any)(jestRuntimeModule)
61
- local originalRequireInternalModule = Runtime.requireInternalModule
62
-
63
- Runtime.requireInternalModule = function(self: any, from: any, to: any, ...): any
64
- local target = if to ~= nil then to else from
65
- if getDataModelServiceChild and typeof(target) == "Instance" and target == getDataModelServiceChild then
66
- return mockGetDataModelService
67
- end
68
-
69
- return originalRequireInternalModule(self, from, to, ...)
70
- end
71
-
72
- return {
73
- robloxSharedExports = robloxSharedExports,
74
- originalGetDataModelService = originalGetDataModelService,
75
- Runtime = Runtime,
76
- originalRequireInternalModule = originalRequireInternalModule,
77
- }
78
- end
79
-
80
- function module.unpatch(state: PatchState?)
81
- if not state then
82
- return
83
- end
84
-
85
- if state.robloxSharedExports and state.originalGetDataModelService then
86
- state.robloxSharedExports.getDataModelService = state.originalGetDataModelService
87
- end
88
-
89
- if state.Runtime and state.originalRequireInternalModule then
90
- state.Runtime.requireInternalModule = state.originalRequireInternalModule
91
- end
92
- end
93
-
94
- return module