@quenty/steputils 3.7.0 → 3.7.2
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/CHANGELOG.md +10 -0
- package/deploy.nevermore.json +10 -0
- package/package.json +7 -2
- package/src/Shared/StepUtils.lua +25 -0
- package/src/Shared/StepUtils.spec.lua +635 -0
- package/src/Shared/onRenderStepFrame.spec.lua +98 -0
- package/src/Shared/onSteppedFrame.spec.lua +85 -0
- package/src/jest.config.lua +3 -0
- package/src/node_modules.project.json +7 -0
- package/test/default.project.json +25 -0
- package/test/scripts/Server/ServerMain.server.lua +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [3.7.2](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.7.1...@quenty/steputils@3.7.2) (2026-08-14)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @quenty/steputils
|
|
9
|
+
|
|
10
|
+
## [3.7.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.7.0...@quenty/steputils@3.7.1) (2026-07-25)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- Fix things ([de8a3b5](https://github.com/Quenty/NevermoreEngine/commit/de8a3b5fbe2aa839f6fc928d4052405d3a13cd71))
|
|
15
|
+
|
|
6
16
|
# [3.7.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.6.3...@quenty/steputils@3.7.0) (2026-07-23)
|
|
7
17
|
|
|
8
18
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/steputils",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.2",
|
|
4
4
|
"description": "Binds animations into step, where the animation only runs as needed",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -27,8 +27,13 @@
|
|
|
27
27
|
"contributors": [
|
|
28
28
|
"Quenty"
|
|
29
29
|
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@quenty/loader": "10.11.1",
|
|
32
|
+
"@quenty/nevermore-test-runner": "1.5.1",
|
|
33
|
+
"@quentystudios/jest-lua": "3.10.0-quenty.2"
|
|
34
|
+
},
|
|
30
35
|
"publishConfig": {
|
|
31
36
|
"access": "public"
|
|
32
37
|
},
|
|
33
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "f084360c3729ea424d49d9bc42f8f0d1c128fa82"
|
|
34
39
|
}
|
package/src/Shared/StepUtils.lua
CHANGED
|
@@ -36,6 +36,31 @@ function StepUtils.getAnimationStepSignal(): RBXScriptSignal
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
--[=[
|
|
40
|
+
Returns the signal per-frame bookkeeping should run on: [RunService.Stepped] (the physics
|
|
41
|
+
pre-step) on a running DataModel, falling back to [RunService.Heartbeat] on a non-running one
|
|
42
|
+
(headless test runs, edit mode) where Stepped never fires.
|
|
43
|
+
|
|
44
|
+
Unlike [StepUtils.getAnimationStepSignal] this does not branch to RenderStepped on the client,
|
|
45
|
+
so both realms step at the same point in the frame. Prefer it for work that is not driving
|
|
46
|
+
visuals -- rescoring, polling, cache invalidation -- and that would otherwise silently stop
|
|
47
|
+
running outside a live game.
|
|
48
|
+
|
|
49
|
+
:::caution
|
|
50
|
+
Stepped fires `(time, deltaTime)` but Heartbeat fires `(deltaTime)`. Do not assume the first
|
|
51
|
+
argument is deltaTime when connecting directly to this signal.
|
|
52
|
+
:::
|
|
53
|
+
|
|
54
|
+
@return RBXScriptSignal
|
|
55
|
+
]=]
|
|
56
|
+
function StepUtils.getSteppedSignal(): RBXScriptSignal
|
|
57
|
+
if RunService:IsRunning() then
|
|
58
|
+
return RunService.Stepped
|
|
59
|
+
else
|
|
60
|
+
return RunService.Heartbeat
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
39
64
|
--[=[
|
|
40
65
|
Binds the given update function to [StepUtils.getAnimationStepSignal].
|
|
41
66
|
|
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class StepUtils.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local RunService = game:GetService("RunService")
|
|
9
|
+
|
|
10
|
+
local Jest = require("Jest")
|
|
11
|
+
local StepUtils = require("StepUtils")
|
|
12
|
+
|
|
13
|
+
local describe = Jest.Globals.describe
|
|
14
|
+
local expect = Jest.Globals.expect
|
|
15
|
+
local it = Jest.Globals.it
|
|
16
|
+
|
|
17
|
+
type Controller = {
|
|
18
|
+
newBindable: () -> BindableEvent,
|
|
19
|
+
bind: (RBXScriptSignal, (...any) -> boolean) -> ((...any) -> (), () -> ()),
|
|
20
|
+
bindRenderStep: ((...any) -> boolean) -> ((...any) -> (), () -> ()),
|
|
21
|
+
bindStepped: ((...any) -> boolean) -> ((...any) -> (), () -> ()),
|
|
22
|
+
track: (() -> ()) -> () -> (),
|
|
23
|
+
stepFrames: (number) -> (),
|
|
24
|
+
destroy: () -> (),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
local function setup(): Controller
|
|
28
|
+
local instances: { Instance } = {}
|
|
29
|
+
local cleanups: { () -> () } = {}
|
|
30
|
+
|
|
31
|
+
local function track(cleanup: () -> ()): () -> ()
|
|
32
|
+
table.insert(cleanups, cleanup)
|
|
33
|
+
return cleanup
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
local controller: Controller = {
|
|
37
|
+
newBindable = function()
|
|
38
|
+
local bindable = Instance.new("BindableEvent")
|
|
39
|
+
table.insert(instances, bindable)
|
|
40
|
+
return bindable
|
|
41
|
+
end,
|
|
42
|
+
|
|
43
|
+
bind = function(signal, update)
|
|
44
|
+
local connect, disconnect = StepUtils.bindToSignal(signal, update)
|
|
45
|
+
track(disconnect)
|
|
46
|
+
return connect, disconnect
|
|
47
|
+
end,
|
|
48
|
+
|
|
49
|
+
bindRenderStep = function(update)
|
|
50
|
+
local connect, disconnect = StepUtils.bindToRenderStep(update)
|
|
51
|
+
track(disconnect)
|
|
52
|
+
return connect, disconnect
|
|
53
|
+
end,
|
|
54
|
+
|
|
55
|
+
bindStepped = function(update)
|
|
56
|
+
local connect, disconnect = StepUtils.bindToStepped(update)
|
|
57
|
+
track(disconnect)
|
|
58
|
+
return connect, disconnect
|
|
59
|
+
end,
|
|
60
|
+
|
|
61
|
+
track = track,
|
|
62
|
+
|
|
63
|
+
stepFrames = function(count: number)
|
|
64
|
+
for _ = 1, count do
|
|
65
|
+
task.wait()
|
|
66
|
+
end
|
|
67
|
+
end,
|
|
68
|
+
|
|
69
|
+
destroy = function()
|
|
70
|
+
for _, cleanup in cleanups do
|
|
71
|
+
cleanup()
|
|
72
|
+
end
|
|
73
|
+
for _, inst in instances do
|
|
74
|
+
inst:Destroy()
|
|
75
|
+
end
|
|
76
|
+
end,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return controller
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe("StepUtils.getAnimationStepSignal", function()
|
|
83
|
+
it("returns a signal", function()
|
|
84
|
+
local controller = setup()
|
|
85
|
+
|
|
86
|
+
expect(typeof(StepUtils.getAnimationStepSignal())).toBe("RBXScriptSignal")
|
|
87
|
+
|
|
88
|
+
controller.destroy()
|
|
89
|
+
end)
|
|
90
|
+
|
|
91
|
+
it("falls back to Heartbeat off a running client", function()
|
|
92
|
+
local controller = setup()
|
|
93
|
+
|
|
94
|
+
expect(StepUtils.getAnimationStepSignal()).toBe(RunService.Heartbeat)
|
|
95
|
+
|
|
96
|
+
controller.destroy()
|
|
97
|
+
end)
|
|
98
|
+
|
|
99
|
+
it("returns a signal that actually fires here", function()
|
|
100
|
+
local controller = setup()
|
|
101
|
+
|
|
102
|
+
local fired = false
|
|
103
|
+
local conn = StepUtils.getAnimationStepSignal():Connect(function()
|
|
104
|
+
fired = true
|
|
105
|
+
end)
|
|
106
|
+
controller.track(function()
|
|
107
|
+
conn:Disconnect()
|
|
108
|
+
end)
|
|
109
|
+
|
|
110
|
+
controller.stepFrames(2)
|
|
111
|
+
|
|
112
|
+
expect(fired).toBe(true)
|
|
113
|
+
|
|
114
|
+
controller.destroy()
|
|
115
|
+
end)
|
|
116
|
+
end)
|
|
117
|
+
|
|
118
|
+
describe("StepUtils.getSteppedSignal", function()
|
|
119
|
+
it("returns a signal", function()
|
|
120
|
+
local controller = setup()
|
|
121
|
+
|
|
122
|
+
expect(typeof(StepUtils.getSteppedSignal())).toBe("RBXScriptSignal")
|
|
123
|
+
|
|
124
|
+
controller.destroy()
|
|
125
|
+
end)
|
|
126
|
+
|
|
127
|
+
it("falls back to Heartbeat on a non-running DataModel", function()
|
|
128
|
+
local controller = setup()
|
|
129
|
+
|
|
130
|
+
expect(StepUtils.getSteppedSignal()).toBe(RunService.Heartbeat)
|
|
131
|
+
expect(StepUtils.getSteppedSignal()).never.toBe(RunService.Stepped)
|
|
132
|
+
|
|
133
|
+
controller.destroy()
|
|
134
|
+
end)
|
|
135
|
+
|
|
136
|
+
it("returns a signal that actually fires here", function()
|
|
137
|
+
local controller = setup()
|
|
138
|
+
|
|
139
|
+
local fired = false
|
|
140
|
+
local conn = StepUtils.getSteppedSignal():Connect(function()
|
|
141
|
+
fired = true
|
|
142
|
+
end)
|
|
143
|
+
controller.track(function()
|
|
144
|
+
conn:Disconnect()
|
|
145
|
+
end)
|
|
146
|
+
|
|
147
|
+
controller.stepFrames(2)
|
|
148
|
+
|
|
149
|
+
expect(fired).toBe(true)
|
|
150
|
+
|
|
151
|
+
controller.destroy()
|
|
152
|
+
end)
|
|
153
|
+
end)
|
|
154
|
+
|
|
155
|
+
describe("StepUtils.bindToSignal", function()
|
|
156
|
+
it("invokes the update immediately on connect", function()
|
|
157
|
+
local controller = setup()
|
|
158
|
+
local bindable = controller.newBindable()
|
|
159
|
+
|
|
160
|
+
local calls = 0
|
|
161
|
+
local connect = controller.bind(bindable.Event, function()
|
|
162
|
+
calls += 1
|
|
163
|
+
return false
|
|
164
|
+
end)
|
|
165
|
+
|
|
166
|
+
connect()
|
|
167
|
+
|
|
168
|
+
expect(calls).toBe(1)
|
|
169
|
+
|
|
170
|
+
controller.destroy()
|
|
171
|
+
end)
|
|
172
|
+
|
|
173
|
+
it("does not connect when the first update returns false", function()
|
|
174
|
+
local controller = setup()
|
|
175
|
+
local bindable = controller.newBindable()
|
|
176
|
+
|
|
177
|
+
local calls = 0
|
|
178
|
+
local connect = controller.bind(bindable.Event, function()
|
|
179
|
+
calls += 1
|
|
180
|
+
return false
|
|
181
|
+
end)
|
|
182
|
+
|
|
183
|
+
connect()
|
|
184
|
+
bindable:Fire()
|
|
185
|
+
bindable:Fire()
|
|
186
|
+
|
|
187
|
+
expect(calls).toBe(1)
|
|
188
|
+
|
|
189
|
+
controller.destroy()
|
|
190
|
+
end)
|
|
191
|
+
|
|
192
|
+
it("keeps invoking the update while it returns true", function()
|
|
193
|
+
local controller = setup()
|
|
194
|
+
local bindable = controller.newBindable()
|
|
195
|
+
|
|
196
|
+
local calls = 0
|
|
197
|
+
local connect = controller.bind(bindable.Event, function()
|
|
198
|
+
calls += 1
|
|
199
|
+
return calls < 3
|
|
200
|
+
end)
|
|
201
|
+
|
|
202
|
+
connect()
|
|
203
|
+
bindable:Fire()
|
|
204
|
+
bindable:Fire()
|
|
205
|
+
|
|
206
|
+
expect(calls).toBe(3)
|
|
207
|
+
|
|
208
|
+
controller.destroy()
|
|
209
|
+
end)
|
|
210
|
+
|
|
211
|
+
it("stops invoking the update once it returns false", function()
|
|
212
|
+
local controller = setup()
|
|
213
|
+
local bindable = controller.newBindable()
|
|
214
|
+
|
|
215
|
+
local calls = 0
|
|
216
|
+
local connect = controller.bind(bindable.Event, function()
|
|
217
|
+
calls += 1
|
|
218
|
+
return calls < 2
|
|
219
|
+
end)
|
|
220
|
+
|
|
221
|
+
connect()
|
|
222
|
+
bindable:Fire()
|
|
223
|
+
bindable:Fire()
|
|
224
|
+
bindable:Fire()
|
|
225
|
+
|
|
226
|
+
expect(calls).toBe(2)
|
|
227
|
+
|
|
228
|
+
controller.destroy()
|
|
229
|
+
end)
|
|
230
|
+
|
|
231
|
+
it("forwards the connect arguments to every later update", function()
|
|
232
|
+
local controller = setup()
|
|
233
|
+
local bindable = controller.newBindable()
|
|
234
|
+
|
|
235
|
+
local seen = {}
|
|
236
|
+
local connect = controller.bind(bindable.Event, function(arg)
|
|
237
|
+
table.insert(seen, arg)
|
|
238
|
+
return #seen < 3
|
|
239
|
+
end)
|
|
240
|
+
|
|
241
|
+
connect("self")
|
|
242
|
+
bindable:Fire()
|
|
243
|
+
bindable:Fire()
|
|
244
|
+
|
|
245
|
+
expect(seen).toEqual({ "self", "self", "self" })
|
|
246
|
+
|
|
247
|
+
controller.destroy()
|
|
248
|
+
end)
|
|
249
|
+
|
|
250
|
+
it("ignores the signal's own arguments", function()
|
|
251
|
+
local controller = setup()
|
|
252
|
+
local bindable = controller.newBindable()
|
|
253
|
+
|
|
254
|
+
local seen = {}
|
|
255
|
+
local connect = controller.bind(bindable.Event, function(arg)
|
|
256
|
+
table.insert(seen, { value = arg })
|
|
257
|
+
return #seen < 2
|
|
258
|
+
end)
|
|
259
|
+
|
|
260
|
+
connect()
|
|
261
|
+
bindable:Fire("fromSignal")
|
|
262
|
+
|
|
263
|
+
expect(seen).toEqual({ { value = nil }, { value = nil } })
|
|
264
|
+
|
|
265
|
+
controller.destroy()
|
|
266
|
+
end)
|
|
267
|
+
|
|
268
|
+
it("ignores a repeated connect while already connected", function()
|
|
269
|
+
local controller = setup()
|
|
270
|
+
local bindable = controller.newBindable()
|
|
271
|
+
|
|
272
|
+
local calls = 0
|
|
273
|
+
local connect = controller.bind(bindable.Event, function()
|
|
274
|
+
calls += 1
|
|
275
|
+
return true
|
|
276
|
+
end)
|
|
277
|
+
|
|
278
|
+
connect()
|
|
279
|
+
connect()
|
|
280
|
+
|
|
281
|
+
expect(calls).toBe(1)
|
|
282
|
+
|
|
283
|
+
controller.destroy()
|
|
284
|
+
end)
|
|
285
|
+
|
|
286
|
+
it("reconnects after an explicit disconnect", function()
|
|
287
|
+
local controller = setup()
|
|
288
|
+
local bindable = controller.newBindable()
|
|
289
|
+
|
|
290
|
+
local calls = 0
|
|
291
|
+
local connect, disconnect = controller.bind(bindable.Event, function()
|
|
292
|
+
calls += 1
|
|
293
|
+
return true
|
|
294
|
+
end)
|
|
295
|
+
|
|
296
|
+
connect()
|
|
297
|
+
disconnect()
|
|
298
|
+
bindable:Fire()
|
|
299
|
+
expect(calls).toBe(1)
|
|
300
|
+
|
|
301
|
+
connect()
|
|
302
|
+
bindable:Fire()
|
|
303
|
+
|
|
304
|
+
expect(calls).toBe(3)
|
|
305
|
+
|
|
306
|
+
controller.destroy()
|
|
307
|
+
end)
|
|
308
|
+
|
|
309
|
+
it("tolerates disconnect before connect and repeated disconnects", function()
|
|
310
|
+
local controller = setup()
|
|
311
|
+
local bindable = controller.newBindable()
|
|
312
|
+
|
|
313
|
+
local _connect, disconnect = controller.bind(bindable.Event, function()
|
|
314
|
+
return true
|
|
315
|
+
end)
|
|
316
|
+
|
|
317
|
+
expect(function()
|
|
318
|
+
disconnect()
|
|
319
|
+
disconnect()
|
|
320
|
+
end).never.toThrow()
|
|
321
|
+
|
|
322
|
+
controller.destroy()
|
|
323
|
+
end)
|
|
324
|
+
|
|
325
|
+
it("throws on something that is not a signal", function()
|
|
326
|
+
local controller = setup()
|
|
327
|
+
|
|
328
|
+
expect(function()
|
|
329
|
+
(StepUtils :: any).bindToSignal({}, function()
|
|
330
|
+
return false
|
|
331
|
+
end)
|
|
332
|
+
end).toThrow()
|
|
333
|
+
|
|
334
|
+
controller.destroy()
|
|
335
|
+
end)
|
|
336
|
+
|
|
337
|
+
it("throws on a non-function update", function()
|
|
338
|
+
local controller = setup()
|
|
339
|
+
local bindable = controller.newBindable()
|
|
340
|
+
|
|
341
|
+
expect(function()
|
|
342
|
+
(StepUtils :: any).bindToSignal(bindable.Event, "update")
|
|
343
|
+
end).toThrow()
|
|
344
|
+
|
|
345
|
+
controller.destroy()
|
|
346
|
+
end)
|
|
347
|
+
end)
|
|
348
|
+
|
|
349
|
+
describe("StepUtils.bindToRenderStep", function()
|
|
350
|
+
it("drives the update off the animation step signal", function()
|
|
351
|
+
local controller = setup()
|
|
352
|
+
|
|
353
|
+
local calls = 0
|
|
354
|
+
local connect = controller.bindRenderStep(function()
|
|
355
|
+
calls += 1
|
|
356
|
+
return calls < 3
|
|
357
|
+
end)
|
|
358
|
+
|
|
359
|
+
connect()
|
|
360
|
+
controller.stepFrames(5)
|
|
361
|
+
|
|
362
|
+
expect(calls).toBe(3)
|
|
363
|
+
|
|
364
|
+
controller.destroy()
|
|
365
|
+
end)
|
|
366
|
+
|
|
367
|
+
it("does not bind when the first update returns false", function()
|
|
368
|
+
local controller = setup()
|
|
369
|
+
|
|
370
|
+
local calls = 0
|
|
371
|
+
local connect = controller.bindRenderStep(function()
|
|
372
|
+
calls += 1
|
|
373
|
+
return false
|
|
374
|
+
end)
|
|
375
|
+
|
|
376
|
+
connect()
|
|
377
|
+
controller.stepFrames(3)
|
|
378
|
+
|
|
379
|
+
expect(calls).toBe(1)
|
|
380
|
+
|
|
381
|
+
controller.destroy()
|
|
382
|
+
end)
|
|
383
|
+
|
|
384
|
+
it("stops driving the update after disconnect", function()
|
|
385
|
+
local controller = setup()
|
|
386
|
+
|
|
387
|
+
local calls = 0
|
|
388
|
+
local connect, disconnect = controller.bindRenderStep(function()
|
|
389
|
+
calls += 1
|
|
390
|
+
return true
|
|
391
|
+
end)
|
|
392
|
+
|
|
393
|
+
connect()
|
|
394
|
+
controller.stepFrames(2)
|
|
395
|
+
disconnect()
|
|
396
|
+
|
|
397
|
+
local afterDisconnect = calls
|
|
398
|
+
controller.stepFrames(3)
|
|
399
|
+
|
|
400
|
+
expect(calls).toBe(afterDisconnect)
|
|
401
|
+
|
|
402
|
+
controller.destroy()
|
|
403
|
+
end)
|
|
404
|
+
end)
|
|
405
|
+
|
|
406
|
+
describe("StepUtils.bindToStepped", function()
|
|
407
|
+
it("invokes the update immediately on connect", function()
|
|
408
|
+
local controller = setup()
|
|
409
|
+
|
|
410
|
+
local calls = 0
|
|
411
|
+
local connect = controller.bindStepped(function()
|
|
412
|
+
calls += 1
|
|
413
|
+
return false
|
|
414
|
+
end)
|
|
415
|
+
|
|
416
|
+
connect()
|
|
417
|
+
|
|
418
|
+
expect(calls).toBe(1)
|
|
419
|
+
|
|
420
|
+
controller.destroy()
|
|
421
|
+
end)
|
|
422
|
+
|
|
423
|
+
it("tolerates disconnect before connect", function()
|
|
424
|
+
local controller = setup()
|
|
425
|
+
|
|
426
|
+
local _connect, disconnect = controller.bindStepped(function()
|
|
427
|
+
return true
|
|
428
|
+
end)
|
|
429
|
+
|
|
430
|
+
expect(function()
|
|
431
|
+
disconnect()
|
|
432
|
+
end).never.toThrow()
|
|
433
|
+
|
|
434
|
+
controller.destroy()
|
|
435
|
+
end)
|
|
436
|
+
end)
|
|
437
|
+
|
|
438
|
+
describe("StepUtils.deferWait", function()
|
|
439
|
+
it("resumes only after already-queued deferrals run", function()
|
|
440
|
+
local controller = setup()
|
|
441
|
+
|
|
442
|
+
local order = {}
|
|
443
|
+
task.defer(function()
|
|
444
|
+
table.insert(order, "deferred")
|
|
445
|
+
end)
|
|
446
|
+
|
|
447
|
+
StepUtils.deferWait()
|
|
448
|
+
table.insert(order, "resumed")
|
|
449
|
+
|
|
450
|
+
expect(order).toEqual({ "deferred", "resumed" })
|
|
451
|
+
|
|
452
|
+
controller.destroy()
|
|
453
|
+
end)
|
|
454
|
+
end)
|
|
455
|
+
|
|
456
|
+
describe("StepUtils.onceAtEvent", function()
|
|
457
|
+
it("invokes the function on the next event", function()
|
|
458
|
+
local controller = setup()
|
|
459
|
+
local bindable = controller.newBindable()
|
|
460
|
+
|
|
461
|
+
local calls = 0
|
|
462
|
+
controller.track(StepUtils.onceAtEvent(bindable.Event, function()
|
|
463
|
+
calls += 1
|
|
464
|
+
end))
|
|
465
|
+
|
|
466
|
+
bindable:Fire()
|
|
467
|
+
|
|
468
|
+
expect(calls).toBe(1)
|
|
469
|
+
|
|
470
|
+
controller.destroy()
|
|
471
|
+
end)
|
|
472
|
+
|
|
473
|
+
it("passes the event arguments through", function()
|
|
474
|
+
local controller = setup()
|
|
475
|
+
local bindable = controller.newBindable()
|
|
476
|
+
|
|
477
|
+
local seen: any = nil
|
|
478
|
+
controller.track(StepUtils.onceAtEvent(bindable.Event, function(value: any)
|
|
479
|
+
seen = value
|
|
480
|
+
end))
|
|
481
|
+
|
|
482
|
+
bindable:Fire("payload")
|
|
483
|
+
|
|
484
|
+
expect(seen).toBe("payload")
|
|
485
|
+
|
|
486
|
+
controller.destroy()
|
|
487
|
+
end)
|
|
488
|
+
|
|
489
|
+
it("only invokes the function once", function()
|
|
490
|
+
local controller = setup()
|
|
491
|
+
local bindable = controller.newBindable()
|
|
492
|
+
|
|
493
|
+
local calls = 0
|
|
494
|
+
controller.track(StepUtils.onceAtEvent(bindable.Event, function()
|
|
495
|
+
calls += 1
|
|
496
|
+
end))
|
|
497
|
+
|
|
498
|
+
bindable:Fire()
|
|
499
|
+
bindable:Fire()
|
|
500
|
+
|
|
501
|
+
expect(calls).toBe(1)
|
|
502
|
+
|
|
503
|
+
controller.destroy()
|
|
504
|
+
end)
|
|
505
|
+
|
|
506
|
+
it("does not invoke the function once cancelled", function()
|
|
507
|
+
local controller = setup()
|
|
508
|
+
local bindable = controller.newBindable()
|
|
509
|
+
|
|
510
|
+
local calls = 0
|
|
511
|
+
local cancel = StepUtils.onceAtEvent(bindable.Event, function()
|
|
512
|
+
calls += 1
|
|
513
|
+
end)
|
|
514
|
+
|
|
515
|
+
cancel()
|
|
516
|
+
bindable:Fire()
|
|
517
|
+
|
|
518
|
+
expect(calls).toBe(0)
|
|
519
|
+
|
|
520
|
+
controller.destroy()
|
|
521
|
+
end)
|
|
522
|
+
|
|
523
|
+
it("tolerates cancelling after the function ran", function()
|
|
524
|
+
local controller = setup()
|
|
525
|
+
local bindable = controller.newBindable()
|
|
526
|
+
|
|
527
|
+
local cancel = StepUtils.onceAtEvent(bindable.Event, function() end)
|
|
528
|
+
bindable:Fire()
|
|
529
|
+
|
|
530
|
+
expect(function()
|
|
531
|
+
cancel()
|
|
532
|
+
cancel()
|
|
533
|
+
end).never.toThrow()
|
|
534
|
+
|
|
535
|
+
controller.destroy()
|
|
536
|
+
end)
|
|
537
|
+
|
|
538
|
+
it("throws on a non-function", function()
|
|
539
|
+
local controller = setup()
|
|
540
|
+
local bindable = controller.newBindable()
|
|
541
|
+
|
|
542
|
+
expect(function()
|
|
543
|
+
(StepUtils :: any).onceAtEvent(bindable.Event, "func")
|
|
544
|
+
end).toThrow()
|
|
545
|
+
|
|
546
|
+
controller.destroy()
|
|
547
|
+
end)
|
|
548
|
+
end)
|
|
549
|
+
|
|
550
|
+
describe("StepUtils.onceAtRenderStepped", function()
|
|
551
|
+
it("invokes the function once on the animation step signal", function()
|
|
552
|
+
local controller = setup()
|
|
553
|
+
|
|
554
|
+
local calls = 0
|
|
555
|
+
controller.track(StepUtils.onceAtRenderStepped(function()
|
|
556
|
+
calls += 1
|
|
557
|
+
end))
|
|
558
|
+
|
|
559
|
+
controller.stepFrames(3)
|
|
560
|
+
|
|
561
|
+
expect(calls).toBe(1)
|
|
562
|
+
|
|
563
|
+
controller.destroy()
|
|
564
|
+
end)
|
|
565
|
+
|
|
566
|
+
it("does not invoke the function once cancelled", function()
|
|
567
|
+
local controller = setup()
|
|
568
|
+
|
|
569
|
+
local calls = 0
|
|
570
|
+
local cancel = StepUtils.onceAtRenderStepped(function()
|
|
571
|
+
calls += 1
|
|
572
|
+
end)
|
|
573
|
+
|
|
574
|
+
cancel()
|
|
575
|
+
controller.stepFrames(3)
|
|
576
|
+
|
|
577
|
+
expect(calls).toBe(0)
|
|
578
|
+
|
|
579
|
+
controller.destroy()
|
|
580
|
+
end)
|
|
581
|
+
end)
|
|
582
|
+
|
|
583
|
+
describe("StepUtils.onceAtStepped", function()
|
|
584
|
+
it("returns a cancel function that is safe to call twice", function()
|
|
585
|
+
local controller = setup()
|
|
586
|
+
|
|
587
|
+
local calls = 0
|
|
588
|
+
local cancel = StepUtils.onceAtStepped(function()
|
|
589
|
+
calls += 1
|
|
590
|
+
end)
|
|
591
|
+
|
|
592
|
+
expect(function()
|
|
593
|
+
cancel()
|
|
594
|
+
cancel()
|
|
595
|
+
end).never.toThrow()
|
|
596
|
+
expect(calls).toBe(0)
|
|
597
|
+
|
|
598
|
+
controller.destroy()
|
|
599
|
+
end)
|
|
600
|
+
end)
|
|
601
|
+
|
|
602
|
+
describe("StepUtils.onceAtRenderPriority", function()
|
|
603
|
+
it("returns a cancel function that is safe to call twice", function()
|
|
604
|
+
local controller = setup()
|
|
605
|
+
|
|
606
|
+
local cancel = StepUtils.onceAtRenderPriority(Enum.RenderPriority.Last.Value, function() end)
|
|
607
|
+
|
|
608
|
+
expect(function()
|
|
609
|
+
cancel()
|
|
610
|
+
cancel()
|
|
611
|
+
end).never.toThrow()
|
|
612
|
+
|
|
613
|
+
controller.destroy()
|
|
614
|
+
end)
|
|
615
|
+
|
|
616
|
+
it("throws on a non-number priority", function()
|
|
617
|
+
local controller = setup()
|
|
618
|
+
|
|
619
|
+
expect(function()
|
|
620
|
+
(StepUtils :: any).onceAtRenderPriority("high", function() end)
|
|
621
|
+
end).toThrow()
|
|
622
|
+
|
|
623
|
+
controller.destroy()
|
|
624
|
+
end)
|
|
625
|
+
|
|
626
|
+
it("throws on a non-function", function()
|
|
627
|
+
local controller = setup()
|
|
628
|
+
|
|
629
|
+
expect(function()
|
|
630
|
+
(StepUtils :: any).onceAtRenderPriority(100, "func")
|
|
631
|
+
end).toThrow()
|
|
632
|
+
|
|
633
|
+
controller.destroy()
|
|
634
|
+
end)
|
|
635
|
+
end)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class onRenderStepFrame.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local onRenderStepFrame = require("onRenderStepFrame")
|
|
10
|
+
|
|
11
|
+
local describe = Jest.Globals.describe
|
|
12
|
+
local expect = Jest.Globals.expect
|
|
13
|
+
local it = Jest.Globals.it
|
|
14
|
+
|
|
15
|
+
type Controller = {
|
|
16
|
+
bind: (number, () -> ()) -> () -> (),
|
|
17
|
+
destroy: () -> (),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
local function setup(): Controller
|
|
21
|
+
local unbinds: { () -> () } = {}
|
|
22
|
+
|
|
23
|
+
local controller: Controller = {
|
|
24
|
+
bind = function(priority: number, callback: () -> ())
|
|
25
|
+
local unbind = onRenderStepFrame(priority, callback)
|
|
26
|
+
table.insert(unbinds, unbind)
|
|
27
|
+
return unbind
|
|
28
|
+
end,
|
|
29
|
+
|
|
30
|
+
destroy = function()
|
|
31
|
+
for _, unbind in unbinds do
|
|
32
|
+
unbind()
|
|
33
|
+
end
|
|
34
|
+
end,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return controller
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe("onRenderStepFrame", function()
|
|
41
|
+
it("returns an unbind function", function()
|
|
42
|
+
local controller = setup()
|
|
43
|
+
|
|
44
|
+
local unbind = controller.bind(Enum.RenderPriority.Last.Value, function() end)
|
|
45
|
+
|
|
46
|
+
expect(type(unbind)).toBe("function")
|
|
47
|
+
|
|
48
|
+
controller.destroy()
|
|
49
|
+
end)
|
|
50
|
+
|
|
51
|
+
it("tolerates a repeated unbind", function()
|
|
52
|
+
local controller = setup()
|
|
53
|
+
|
|
54
|
+
local unbind = controller.bind(Enum.RenderPriority.Last.Value, function() end)
|
|
55
|
+
|
|
56
|
+
expect(function()
|
|
57
|
+
unbind()
|
|
58
|
+
unbind()
|
|
59
|
+
end).never.toThrow()
|
|
60
|
+
|
|
61
|
+
controller.destroy()
|
|
62
|
+
end)
|
|
63
|
+
|
|
64
|
+
it("binds each call under its own key", function()
|
|
65
|
+
local controller = setup()
|
|
66
|
+
|
|
67
|
+
local first = controller.bind(Enum.RenderPriority.Last.Value, function() end)
|
|
68
|
+
local second = controller.bind(Enum.RenderPriority.Last.Value, function() end)
|
|
69
|
+
|
|
70
|
+
expect(second).never.toBe(first)
|
|
71
|
+
expect(function()
|
|
72
|
+
first()
|
|
73
|
+
second()
|
|
74
|
+
end).never.toThrow()
|
|
75
|
+
|
|
76
|
+
controller.destroy()
|
|
77
|
+
end)
|
|
78
|
+
|
|
79
|
+
it("throws on a non-number priority", function()
|
|
80
|
+
local controller = setup()
|
|
81
|
+
|
|
82
|
+
expect(function()
|
|
83
|
+
(onRenderStepFrame :: any)("high", function() end)
|
|
84
|
+
end).toThrow()
|
|
85
|
+
|
|
86
|
+
controller.destroy()
|
|
87
|
+
end)
|
|
88
|
+
|
|
89
|
+
it("throws on a non-function callback", function()
|
|
90
|
+
local controller = setup()
|
|
91
|
+
|
|
92
|
+
expect(function()
|
|
93
|
+
(onRenderStepFrame :: any)(100, "callback")
|
|
94
|
+
end).toThrow()
|
|
95
|
+
|
|
96
|
+
controller.destroy()
|
|
97
|
+
end)
|
|
98
|
+
end)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class onSteppedFrame.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local onSteppedFrame = require("onSteppedFrame")
|
|
10
|
+
|
|
11
|
+
local describe = Jest.Globals.describe
|
|
12
|
+
local expect = Jest.Globals.expect
|
|
13
|
+
local it = Jest.Globals.it
|
|
14
|
+
|
|
15
|
+
type Controller = {
|
|
16
|
+
bind: (() -> ()) -> RBXScriptConnection,
|
|
17
|
+
destroy: () -> (),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
local function setup(): Controller
|
|
21
|
+
local connections: { RBXScriptConnection } = {}
|
|
22
|
+
|
|
23
|
+
local controller: Controller = {
|
|
24
|
+
bind = function(callback)
|
|
25
|
+
local conn = onSteppedFrame(callback)
|
|
26
|
+
table.insert(connections, conn)
|
|
27
|
+
return conn
|
|
28
|
+
end,
|
|
29
|
+
|
|
30
|
+
destroy = function()
|
|
31
|
+
for _, conn in connections do
|
|
32
|
+
conn:Disconnect()
|
|
33
|
+
end
|
|
34
|
+
end,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return controller
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe("onSteppedFrame", function()
|
|
41
|
+
it("returns a live connection", function()
|
|
42
|
+
local controller = setup()
|
|
43
|
+
|
|
44
|
+
local conn = controller.bind(function() end)
|
|
45
|
+
|
|
46
|
+
expect(typeof(conn)).toBe("RBXScriptConnection")
|
|
47
|
+
expect(conn.Connected).toBe(true)
|
|
48
|
+
|
|
49
|
+
controller.destroy()
|
|
50
|
+
end)
|
|
51
|
+
|
|
52
|
+
it("goes dead once disconnected", function()
|
|
53
|
+
local controller = setup()
|
|
54
|
+
|
|
55
|
+
local conn = controller.bind(function() end)
|
|
56
|
+
conn:Disconnect()
|
|
57
|
+
|
|
58
|
+
expect(conn.Connected).toBe(false)
|
|
59
|
+
|
|
60
|
+
controller.destroy()
|
|
61
|
+
end)
|
|
62
|
+
|
|
63
|
+
it("tolerates a repeated disconnect", function()
|
|
64
|
+
local controller = setup()
|
|
65
|
+
|
|
66
|
+
local conn = controller.bind(function() end)
|
|
67
|
+
|
|
68
|
+
expect(function()
|
|
69
|
+
conn:Disconnect()
|
|
70
|
+
conn:Disconnect()
|
|
71
|
+
end).never.toThrow()
|
|
72
|
+
|
|
73
|
+
controller.destroy()
|
|
74
|
+
end)
|
|
75
|
+
|
|
76
|
+
it("throws on a non-function", function()
|
|
77
|
+
local controller = setup()
|
|
78
|
+
|
|
79
|
+
expect(function()
|
|
80
|
+
(onSteppedFrame :: any)("func")
|
|
81
|
+
end).toThrow()
|
|
82
|
+
|
|
83
|
+
controller.destroy()
|
|
84
|
+
end)
|
|
85
|
+
end)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "StepUtilsTest",
|
|
3
|
+
"globIgnorePaths": [
|
|
4
|
+
"**/.package-lock.json",
|
|
5
|
+
"**/.pnpm",
|
|
6
|
+
"**/.pnpm-workspace-state-v1.json",
|
|
7
|
+
"**/.modules.yaml",
|
|
8
|
+
"**/.ignored",
|
|
9
|
+
"**/.ignored_*"
|
|
10
|
+
],
|
|
11
|
+
"tree": {
|
|
12
|
+
"$className": "DataModel",
|
|
13
|
+
"ServerScriptService": {
|
|
14
|
+
"$properties": {
|
|
15
|
+
"LoadStringEnabled": true
|
|
16
|
+
},
|
|
17
|
+
"steputils": {
|
|
18
|
+
"$path": ".."
|
|
19
|
+
},
|
|
20
|
+
"Script": {
|
|
21
|
+
"$path": "scripts/Server"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
@class ServerMain
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local ServerScriptService = game:GetService("ServerScriptService")
|
|
7
|
+
|
|
8
|
+
local root = ServerScriptService.steputils
|
|
9
|
+
local loader = root:FindFirstChild("LoaderUtils", true).Parent
|
|
10
|
+
local require = require(loader).bootstrapGame(root)
|
|
11
|
+
|
|
12
|
+
local NevermoreTestRunnerUtils = require("NevermoreTestRunnerUtils")
|
|
13
|
+
|
|
14
|
+
NevermoreTestRunnerUtils.runTestsIfNeededAsync(root)
|