@quenty/steputils 3.6.3 → 3.7.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/CHANGELOG.md CHANGED
@@ -3,187 +3,119 @@
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.6.3](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.6.2...@quenty/steputils@3.6.3) (2026-01-12)
7
-
6
+ ## [3.7.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.7.0...@quenty/steputils@3.7.1) (2026-07-25)
8
7
 
9
8
  ### Bug Fixes
10
9
 
11
- * Fix dependency installation process for pnpm ([9651b09](https://github.com/Quenty/NevermoreEngine/commit/9651b09af34f252ef3f9c7c539793cc878dd8cba))
10
+ - Fix things ([de8a3b5](https://github.com/Quenty/NevermoreEngine/commit/de8a3b5fbe2aa839f6fc928d4052405d3a13cd71))
12
11
 
12
+ # [3.7.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.6.3...@quenty/steputils@3.7.0) (2026-07-23)
13
13
 
14
+ ### Bug Fixes
14
15
 
16
+ - Cleanup code a bit ([fa8c596](https://github.com/Quenty/NevermoreEngine/commit/fa8c596544e45610e180f5ae3797154f35930ccd))
15
17
 
18
+ ### Features
16
19
 
17
- ## [3.6.2](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.6.1...@quenty/steputils@3.6.2) (2026-01-10)
20
+ - Add baseline player-mock and support across Nevermore for mocked players. ([567d121](https://github.com/Quenty/NevermoreEngine/commit/567d121ffc014b42391554088189a1a6296dda83))
18
21
 
19
- **Note:** Version bump only for package @quenty/steputils
22
+ ## [3.6.3](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.6.2...@quenty/steputils@3.6.3) (2026-01-12)
20
23
 
24
+ ### Bug Fixes
21
25
 
26
+ - Fix dependency installation process for pnpm ([9651b09](https://github.com/Quenty/NevermoreEngine/commit/9651b09af34f252ef3f9c7c539793cc878dd8cba))
22
27
 
28
+ ## [3.6.2](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.6.1...@quenty/steputils@3.6.2) (2026-01-10)
23
29
 
30
+ **Note:** Version bump only for package @quenty/steputils
24
31
 
25
32
  ## [3.6.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.6.0...@quenty/steputils@3.6.1) (2026-01-03)
26
33
 
27
34
  **Note:** Version bump only for package @quenty/steputils
28
35
 
29
-
30
-
31
-
32
-
33
36
  # [3.6.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.5.5...@quenty/steputils@3.6.0) (2025-05-10)
34
37
 
35
38
  **Note:** Version bump only for package @quenty/steputils
36
39
 
37
-
38
-
39
-
40
-
41
40
  ## [3.5.5](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.5.3...@quenty/steputils@3.5.5) (2025-04-07)
42
41
 
43
-
44
42
  ### Bug Fixes
45
43
 
46
- * Add types to packages ([2374fb2](https://github.com/Quenty/NevermoreEngine/commit/2374fb2b043cfbe0e9b507b3316eec46a4e353a0))
47
- * Bump package versions for republishing ([ba47c62](https://github.com/Quenty/NevermoreEngine/commit/ba47c62e32170bf74377b0c658c60b84306dc294))
48
-
49
-
50
-
51
-
44
+ - Add types to packages ([2374fb2](https://github.com/Quenty/NevermoreEngine/commit/2374fb2b043cfbe0e9b507b3316eec46a4e353a0))
45
+ - Bump package versions for republishing ([ba47c62](https://github.com/Quenty/NevermoreEngine/commit/ba47c62e32170bf74377b0c658c60b84306dc294))
52
46
 
53
47
  ## [3.5.4](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.5.3...@quenty/steputils@3.5.4) (2025-04-07)
54
48
 
55
-
56
49
  ### Bug Fixes
57
50
 
58
- * Add types to packages ([2374fb2](https://github.com/Quenty/NevermoreEngine/commit/2374fb2b043cfbe0e9b507b3316eec46a4e353a0))
59
-
60
-
61
-
62
-
51
+ - Add types to packages ([2374fb2](https://github.com/Quenty/NevermoreEngine/commit/2374fb2b043cfbe0e9b507b3316eec46a4e353a0))
63
52
 
64
53
  ## [3.5.3](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.5.2...@quenty/steputils@3.5.3) (2025-03-21)
65
54
 
66
55
  **Note:** Version bump only for package @quenty/steputils
67
56
 
68
-
69
-
70
-
71
-
72
57
  ## [3.5.2](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.5.1...@quenty/steputils@3.5.2) (2024-10-09)
73
58
 
74
-
75
59
  ### Bug Fixes
76
60
 
77
- * Deprecate a large set of APIs ([#506](https://github.com/Quenty/NevermoreEngine/issues/506)) ([d6bdd65](https://github.com/Quenty/NevermoreEngine/commit/d6bdd6567668f238c45d3bd0c85bedb763d30e5c))
78
-
79
-
80
-
81
-
61
+ - Deprecate a large set of APIs ([#506](https://github.com/Quenty/NevermoreEngine/issues/506)) ([d6bdd65](https://github.com/Quenty/NevermoreEngine/commit/d6bdd6567668f238c45d3bd0c85bedb763d30e5c))
82
62
 
83
63
  ## [3.5.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.5.0...@quenty/steputils@3.5.1) (2024-10-04)
84
64
 
85
65
  **Note:** Version bump only for package @quenty/steputils
86
66
 
87
-
88
-
89
-
90
-
91
67
  # [3.5.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.4.0...@quenty/steputils@3.5.0) (2024-09-25)
92
68
 
93
69
  **Note:** Version bump only for package @quenty/steputils
94
70
 
95
-
96
-
97
-
98
-
99
71
  # [3.4.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.3.0...@quenty/steputils@3.4.0) (2024-05-09)
100
72
 
101
-
102
73
  ### Bug Fixes
103
74
 
104
- * Fix .package-lock.json replicating in packages ([75d0efe](https://github.com/Quenty/NevermoreEngine/commit/75d0efeef239f221d93352af71a5b3e930ec23c5))
105
-
106
-
107
-
108
-
75
+ - Fix .package-lock.json replicating in packages ([75d0efe](https://github.com/Quenty/NevermoreEngine/commit/75d0efeef239f221d93352af71a5b3e930ec23c5))
109
76
 
110
77
  # [3.3.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.2.0...@quenty/steputils@3.3.0) (2023-09-04)
111
78
 
112
-
113
79
  ### Features
114
80
 
115
- * Add StepUtils.deferWait() ([caac896](https://github.com/Quenty/NevermoreEngine/commit/caac896fc8439d0de87245cf4bdcb534ba2a9efe))
116
-
117
-
118
-
119
-
81
+ - Add StepUtils.deferWait() ([caac896](https://github.com/Quenty/NevermoreEngine/commit/caac896fc8439d0de87245cf4bdcb534ba2a9efe))
120
82
 
121
83
  # [3.2.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.1.0...@quenty/steputils@3.2.0) (2022-10-11)
122
84
 
123
-
124
85
  ### Features
125
86
 
126
- * Add StepUtils.bindToStepped ([f49f70d](https://github.com/Quenty/NevermoreEngine/commit/f49f70d870fa9fd8276a9cb0056a40ebeb85c049))
127
-
128
-
129
-
130
-
87
+ - Add StepUtils.bindToStepped ([f49f70d](https://github.com/Quenty/NevermoreEngine/commit/f49f70d870fa9fd8276a9cb0056a40ebeb85c049))
131
88
 
132
89
  # [3.1.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.0.1...@quenty/steputils@3.1.0) (2022-03-27)
133
90
 
134
91
  **Note:** Version bump only for package @quenty/steputils
135
92
 
136
-
137
-
138
-
139
-
140
93
  ## [3.0.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@3.0.0...@quenty/steputils@3.0.1) (2021-12-30)
141
94
 
142
95
  **Note:** Version bump only for package @quenty/steputils
143
96
 
144
-
145
-
146
-
147
-
148
97
  # [3.0.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@2.0.0...@quenty/steputils@3.0.0) (2021-09-11)
149
98
 
150
-
151
99
  ### Features
152
100
 
153
- * Add new methods to StepUtils ([738712a](https://github.com/Quenty/NevermoreEngine/commit/738712a3fea2ccf5ba1d479749d6db73d28a0902))
154
-
155
-
156
-
157
-
101
+ - Add new methods to StepUtils ([738712a](https://github.com/Quenty/NevermoreEngine/commit/738712a3fea2ccf5ba1d479749d6db73d28a0902))
158
102
 
159
103
  # [2.0.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/steputils@1.2.0...@quenty/steputils@2.0.0) (2021-09-05)
160
104
 
161
-
162
105
  ### Bug Fixes
163
106
 
164
- * Remove peer dependencies. This is because lerna doesn't really support peer dependencies being linked and getting a new version on build, which is unfortunate. ([5f5aeee](https://github.com/Quenty/NevermoreEngine/commit/5f5aeeea8de9975435309e53679f0ef7064f9dd0))
165
-
166
-
167
-
168
-
107
+ - Remove peer dependencies. This is because lerna doesn't really support peer dependencies being linked and getting a new version on build, which is unfortunate. ([5f5aeee](https://github.com/Quenty/NevermoreEngine/commit/5f5aeeea8de9975435309e53679f0ef7064f9dd0))
169
108
 
170
109
  # 1.2.0 (2021-07-31)
171
110
 
172
-
173
111
  ### Bug Fixes
174
112
 
175
- * Add CI and CD ([47513e9](https://github.com/Quenty/NevermoreEngine/commit/47513e9b568162707534af132396dd8756947dd3))
176
- * Adjust CI badge to show automatic build and release state ([5a55d3f](https://github.com/Quenty/NevermoreEngine/commit/5a55d3f19bf8d66a760d67da9b56ed47fab74656))
177
- * Fix selene linting ([45fc074](https://github.com/Quenty/NevermoreEngine/commit/45fc07489ee59127ac6582689f19a0e87c1e5b5a))
178
-
179
-
113
+ - Add CI and CD ([47513e9](https://github.com/Quenty/NevermoreEngine/commit/47513e9b568162707534af132396dd8756947dd3))
114
+ - Adjust CI badge to show automatic build and release state ([5a55d3f](https://github.com/Quenty/NevermoreEngine/commit/5a55d3f19bf8d66a760d67da9b56ed47fab74656))
115
+ - Fix selene linting ([45fc074](https://github.com/Quenty/NevermoreEngine/commit/45fc07489ee59127ac6582689f19a0e87c1e5b5a))
180
116
 
181
117
  ## 1.0.2 (2021-07-25)
182
118
 
183
-
184
-
185
119
  ## 1.0.1 (2021-07-25)
186
120
 
187
-
188
-
189
121
  # 1.0.0 (2021-07-24)
@@ -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.6.3",
3
+ "version": "3.7.1",
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.0",
32
+ "@quenty/nevermore-test-runner": "1.5.0",
33
+ "@quentystudios/jest-lua": "3.10.0-quenty.2"
34
+ },
30
35
  "publishConfig": {
31
36
  "access": "public"
32
37
  },
33
- "gitHead": "55f4a7407de7247aceea574de4d2f57dab70cb91"
38
+ "gitHead": "f276b791b8eb4e88b952b5babaf0f4189c1a11d0"
34
39
  }
@@ -10,7 +10,59 @@ local RunService = game:GetService("RunService")
10
10
  local StepUtils = {}
11
11
 
12
12
  --[=[
13
- Binds the given update function to [RunService.RenderStepped].
13
+ Returns the signal animation should step on: [RunService.RenderStepped] on the client,
14
+ [RunService.Stepped] (the physics pre-step) on a running server so that writes land before
15
+ the physics step -- springs that drive CFrames stay in lockstep with constraints, characters,
16
+ and replication. Heartbeat is only the fallback for a non-running DataModel (headless test
17
+ runs, edit mode), where Stepped never fires.
18
+
19
+ Use this instead of hand-rolling the realm branch at call sites.
20
+
21
+ :::caution
22
+ The argument shape is realm-dependent: RenderStepped fires `(deltaTime)` but Stepped fires
23
+ `(time, deltaTime)`. Shared code connecting directly to this signal must not assume the
24
+ first argument is deltaTime.
25
+ :::
26
+
27
+ @return RBXScriptSignal
28
+ ]=]
29
+ function StepUtils.getAnimationStepSignal(): RBXScriptSignal
30
+ if RunService:IsClient() then
31
+ return RunService.RenderStepped
32
+ elseif RunService:IsRunning() then
33
+ return RunService.Stepped
34
+ else
35
+ return RunService.Heartbeat
36
+ end
37
+ end
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
+
64
+ --[=[
65
+ Binds the given update function to [StepUtils.getAnimationStepSignal].
14
66
 
15
67
  ```lua
16
68
  local spring = Spring.new(0)
@@ -37,7 +89,7 @@ local StepUtils = {}
37
89
  @return () -> () -- Disconnect function
38
90
  ]=]
39
91
  function StepUtils.bindToRenderStep(update: () -> boolean): (() -> (), () -> ())
40
- return StepUtils.bindToSignal(RunService.RenderStepped, update)
92
+ return StepUtils.bindToSignal(StepUtils.getAnimationStepSignal(), update)
41
93
  end
42
94
 
43
95
  --[=[
@@ -183,7 +235,7 @@ end
183
235
  @return function -- Call this function to cancel call
184
236
  ]=]
185
237
  function StepUtils.onceAtRenderStepped(func: () -> ()): () -> ()
186
- local conn = RunService.RenderStepped:Once(func)
238
+ local conn = StepUtils.getAnimationStepSignal():Once(func)
187
239
  return function()
188
240
  conn:Disconnect()
189
241
  end
@@ -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)