@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 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
@@ -0,0 +1,10 @@
1
+ {
2
+ "targets": {
3
+ "test": {
4
+ "universeId": 9716264427,
5
+ "placeId": 80583370987435,
6
+ "project": "test/default.project.json",
7
+ "scriptTemplate": "test/scripts/Server/ServerMain.server.lua"
8
+ }
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/steputils",
3
- "version": "3.7.0",
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": "1de37218a2bedb8e3f8614a2e09bba9eddc812da"
38
+ "gitHead": "f084360c3729ea424d49d9bc42f8f0d1c128fa82"
34
39
  }
@@ -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,3 @@
1
+ return {
2
+ testMatch = { "**/*.spec" },
3
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "node_modules",
3
+ "globIgnorePaths": [ "**/.package-lock.json" ],
4
+ "tree": {
5
+ "$path": { "optional": "../node_modules" }
6
+ }
7
+ }
@@ -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)