@quenty/binder 14.38.0 → 14.40.0

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.
@@ -0,0 +1,156 @@
1
+ --!strict
2
+ --[[
3
+ Coverage for BinderGroup: grouping binders, optional constructor validation, and the
4
+ BinderAdded signal. The binders here are never started, so no CollectionService state is
5
+ touched; each is still destroyed through the setup() controller to avoid leaking into the
6
+ shared test place.
7
+
8
+ @class BinderGroup.spec.lua
9
+ ]]
10
+
11
+ local require = require(script.Parent.loader).load(script)
12
+
13
+ local Binder = require("Binder")
14
+ local BinderGroup = require("BinderGroup")
15
+ local Jest = require("Jest")
16
+
17
+ local describe = Jest.Globals.describe
18
+ local expect = Jest.Globals.expect
19
+ local it = Jest.Globals.it
20
+
21
+ local tagCounter = 0
22
+
23
+ local function setup()
24
+ local binders: { any } = {}
25
+
26
+ local function newBinder(): Binder.Binder<any>
27
+ tagCounter += 1
28
+ local binder = Binder.new(string.format("BinderGroupSpecTag_%d", tagCounter), function()
29
+ return {}
30
+ end)
31
+ table.insert(binders, binder)
32
+ return binder
33
+ end
34
+
35
+ return {
36
+ newBinder = newBinder,
37
+ destroy = function()
38
+ for _, binder in binders do
39
+ pcall(function()
40
+ binder:Destroy()
41
+ end)
42
+ end
43
+ end,
44
+ }
45
+ end
46
+
47
+ describe("BinderGroup.new()", function()
48
+ it("constructs with an initial list of binders", function()
49
+ local env = setup()
50
+
51
+ local binderA = env.newBinder()
52
+ local binderB = env.newBinder()
53
+ local group = BinderGroup.new({ binderA, binderB })
54
+
55
+ expect(#group:GetBinders()).toEqual(2)
56
+
57
+ env.destroy()
58
+ end)
59
+
60
+ it("constructs empty", function()
61
+ local group = BinderGroup.new({})
62
+ expect(#group:GetBinders()).toEqual(0)
63
+ end)
64
+ end)
65
+
66
+ describe("BinderGroup:Add()", function()
67
+ it("adds a binder and exposes it via GetBinders", function()
68
+ local env = setup()
69
+
70
+ local group = BinderGroup.new({})
71
+ local binder = env.newBinder()
72
+ group:Add(binder)
73
+
74
+ expect(group:GetBinders()[1]).toEqual(binder)
75
+
76
+ env.destroy()
77
+ end)
78
+
79
+ it("fires BinderAdded with the added binder", function()
80
+ local env = setup()
81
+
82
+ local group = BinderGroup.new({})
83
+ local fired
84
+ group.BinderAdded:Connect(function(binder)
85
+ fired = binder
86
+ end)
87
+
88
+ local binder = env.newBinder()
89
+ group:Add(binder)
90
+
91
+ expect(fired).toEqual(binder)
92
+
93
+ env.destroy()
94
+ end)
95
+
96
+ it("throws when the value is not a binder", function()
97
+ local group = BinderGroup.new({})
98
+ expect(function()
99
+ group:Add({} :: any)
100
+ end).toThrow()
101
+ end)
102
+ end)
103
+
104
+ describe("BinderGroup constructor validation", function()
105
+ it("accepts binders whose constructor passes validation", function()
106
+ local env = setup()
107
+
108
+ local validated = {}
109
+ local group = BinderGroup.new({}, function(constructor)
110
+ table.insert(validated, constructor)
111
+ return true
112
+ end)
113
+
114
+ local binder = env.newBinder()
115
+ group:Add(binder)
116
+
117
+ expect(validated[1]).toEqual(binder:GetConstructor())
118
+
119
+ env.destroy()
120
+ end)
121
+
122
+ it("throws when the constructor fails validation", function()
123
+ local env = setup()
124
+
125
+ local group = BinderGroup.new({}, function()
126
+ return false
127
+ end)
128
+
129
+ local binder = env.newBinder()
130
+ expect(function()
131
+ group:Add(binder)
132
+ end).toThrow()
133
+
134
+ env.destroy()
135
+ end)
136
+ end)
137
+
138
+ describe("BinderGroup:AddList()", function()
139
+ it("adds each binder in the list", function()
140
+ local env = setup()
141
+
142
+ local group = BinderGroup.new({})
143
+ group:AddList({ env.newBinder(), env.newBinder() })
144
+
145
+ expect(#group:GetBinders()).toEqual(2)
146
+
147
+ env.destroy()
148
+ end)
149
+
150
+ it("throws on a non-table argument", function()
151
+ local group = BinderGroup.new({})
152
+ expect(function()
153
+ group:AddList(5 :: any)
154
+ end).toThrow()
155
+ end)
156
+ end)
@@ -0,0 +1,117 @@
1
+ --!strict
2
+ --[[
3
+ Coverage for BinderGroupProvider: registration before init, the groups-added promise, and
4
+ the custom __index guard that rejects unknown lookups.
5
+
6
+ @class BinderGroupProvider.spec.lua
7
+ ]]
8
+
9
+ local require = require(script.Parent.loader).load(script)
10
+
11
+ local BinderGroup = require("BinderGroup")
12
+ local BinderGroupProvider = require("BinderGroupProvider")
13
+ local Jest = require("Jest")
14
+
15
+ local describe = Jest.Globals.describe
16
+ local expect = Jest.Globals.expect
17
+ local it = Jest.Globals.it
18
+
19
+ describe("BinderGroupProvider.new()", function()
20
+ it("constructs a provider", function()
21
+ local provider = BinderGroupProvider.new(function() end)
22
+ expect((provider :: any).ClassName).toEqual("BinderGroupProvider")
23
+ end)
24
+
25
+ it("throws without an init method", function()
26
+ expect(function()
27
+ BinderGroupProvider.new(nil :: any)
28
+ end).toThrow()
29
+ end)
30
+ end)
31
+
32
+ describe("BinderGroupProvider:Init()", function()
33
+ it("runs the init method and resolves the groups-added promise", function()
34
+ local ran = false
35
+ local provider = BinderGroupProvider.new(function(self)
36
+ ran = true
37
+ self:Add("Group", BinderGroup.new({}))
38
+ end)
39
+
40
+ expect(provider:PromiseGroupsAdded():IsPending()).toEqual(true)
41
+ provider:Init()
42
+
43
+ expect(ran).toEqual(true)
44
+ expect(provider:PromiseGroupsAdded():IsFulfilled()).toEqual(true)
45
+ end)
46
+
47
+ it("passes extra Init args to the init method", function()
48
+ local received
49
+ local provider = BinderGroupProvider.new(function(_self, arg)
50
+ received = arg
51
+ end)
52
+ provider:Init(4321)
53
+ expect(received).toEqual(4321)
54
+ end)
55
+
56
+ it("throws when initialized twice", function()
57
+ local provider = BinderGroupProvider.new(function() end)
58
+ provider:Init()
59
+ expect(function()
60
+ provider:Init()
61
+ end).toThrow()
62
+ end)
63
+ end)
64
+
65
+ describe("BinderGroupProvider:Add() / Get()", function()
66
+ it("retrieves an added group by name", function()
67
+ local group = BinderGroup.new({})
68
+ local provider = BinderGroupProvider.new(function(self)
69
+ self:Add("MyGroup", group)
70
+ end)
71
+ provider:Init()
72
+
73
+ expect(provider:Get("MyGroup")).toEqual(group)
74
+ end)
75
+
76
+ it("returns nil for an unknown group name via Get()", function()
77
+ local provider = BinderGroupProvider.new(function() end)
78
+ provider:Init()
79
+ expect(provider:Get("Missing")).toBeNil()
80
+ end)
81
+
82
+ it("throws when adding a duplicate group name", function()
83
+ local provider = BinderGroupProvider.new(function(self)
84
+ self:Add("Dup", BinderGroup.new({}))
85
+ self:Add("Dup", BinderGroup.new({}))
86
+ end)
87
+
88
+ expect(function()
89
+ provider:Init()
90
+ end).toThrow()
91
+ end)
92
+
93
+ it("throws when adding after initialization", function()
94
+ local provider = BinderGroupProvider.new(function() end)
95
+ provider:Init()
96
+ expect(function()
97
+ provider:Add("Late", BinderGroup.new({}))
98
+ end).toThrow()
99
+ end)
100
+
101
+ it("throws on a non-string group name", function()
102
+ local provider = BinderGroupProvider.new(function() end)
103
+ expect(function()
104
+ provider:Get(5 :: any)
105
+ end).toThrow()
106
+ end)
107
+ end)
108
+
109
+ describe("BinderGroupProvider __index guard", function()
110
+ it("throws when indexing an unknown key", function()
111
+ local provider = BinderGroupProvider.new(function() end)
112
+ provider:Init()
113
+ expect(function()
114
+ return (provider :: any).SomethingInvalid
115
+ end).toThrow()
116
+ end)
117
+ end)
@@ -1,11 +1,9 @@
1
- --!nonstrict
1
+ --!strict
2
2
  --[[
3
3
  @class BinderProvider.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local Binder = require("Binder")
11
9
  local BinderProvider = require("BinderProvider")
@@ -49,7 +47,7 @@ describe("BinderProvider.new()", function()
49
47
 
50
48
  provider:Init()
51
49
 
52
- expect(provider.Test).toEqual(expect.any("table"))
50
+ expect((provider :: any).Test).toEqual(expect.any("table"))
53
51
 
54
52
  provider:Destroy()
55
53
  if binder then
@@ -0,0 +1,347 @@
1
+ --!strict
2
+ --[[
3
+ Coverage for BinderUtils lookup helpers.
4
+
5
+ Binders are booted through a ServiceBag (as production code does) and instances are tagged
6
+ BEFORE the service bag starts, so they bind synchronously and :Get() resolves without waiting.
7
+ Instances live in a workspace container so the binders' added signals fire. Each test tears
8
+ down its service bag and instances because the test place is shared across a batch run.
9
+
10
+ @class BinderUtils.spec.lua
11
+ ]]
12
+
13
+ local require = require(script.Parent.loader).load(script)
14
+
15
+ local Binder = require("Binder")
16
+ local BinderProvider = require("BinderProvider")
17
+ local BinderUtils = require("BinderUtils")
18
+ local Jest = require("Jest")
19
+ local ServiceBag = require("ServiceBag")
20
+
21
+ local describe = Jest.Globals.describe
22
+ local expect = Jest.Globals.expect
23
+ local it = Jest.Globals.it
24
+
25
+ local specCounter = 0
26
+
27
+ local function makeClass()
28
+ local Class = {}
29
+ Class.__index = Class
30
+ Class.ClassName = "BinderUtilsSpecClass"
31
+ function Class.new(inst)
32
+ return setmetatable({ instance = inst }, Class)
33
+ end
34
+ function Class:Destroy() end
35
+ return Class
36
+ end
37
+
38
+ local function setup()
39
+ specCounter += 1
40
+ local suffix = specCounter
41
+
42
+ local serviceBag = ServiceBag.new()
43
+ local container = Instance.new("Folder")
44
+ container.Name = "BinderUtilsSpecContainer"
45
+ container.Parent = workspace
46
+
47
+ local instances = {}
48
+ local pendingBinders: { any } = {}
49
+ local tagCounter = 0
50
+ local booted = false
51
+
52
+ local function newBinder(): Binder.Binder<any>
53
+ assert(not booted, "Cannot add a binder after boot()")
54
+ tagCounter += 1
55
+ local binder = Binder.new(string.format("BinderUtilsSpecTag_%d_%d", suffix, tagCounter), makeClass() :: any)
56
+ table.insert(pendingBinders, binder)
57
+ return binder
58
+ end
59
+
60
+ local function newInstance(parent: Instance?, className: string?): Instance
61
+ local inst = Instance.new(className or "Folder")
62
+ inst.Parent = parent or container
63
+ table.insert(instances, inst)
64
+ return inst
65
+ end
66
+
67
+ -- Tags each instance with the binder, then boots everything through the service bag so the
68
+ -- instances bind synchronously.
69
+ local function boot()
70
+ assert(not booted, "Already booted")
71
+ booted = true
72
+
73
+ local binders = pendingBinders
74
+ local provider = BinderProvider.new(string.format("BinderUtilsSpecProvider_%d", suffix), function(self)
75
+ for _, binder in binders do
76
+ self:Add(binder)
77
+ end
78
+ end)
79
+ serviceBag:GetService(provider)
80
+ serviceBag:Init()
81
+ serviceBag:Start()
82
+ end
83
+
84
+ return {
85
+ container = container,
86
+ newBinder = newBinder,
87
+ newInstance = newInstance,
88
+ boot = boot,
89
+ destroy = function()
90
+ serviceBag:Destroy()
91
+ for _, inst in instances do
92
+ pcall(function()
93
+ inst:Destroy()
94
+ end)
95
+ end
96
+ container:Destroy()
97
+ end,
98
+ }
99
+ end
100
+
101
+ describe("BinderUtils.findFirstAncestor()", function()
102
+ it("returns the nearest bound ancestor, skipping the child itself", function()
103
+ local env = setup()
104
+
105
+ local grandparent = env.newInstance()
106
+ local parent = env.newInstance(grandparent)
107
+ local child = env.newInstance(parent)
108
+
109
+ local binder = env.newBinder()
110
+ binder:Tag(grandparent)
111
+ binder:Tag(child) -- child is bound too, but must be skipped
112
+ env.boot()
113
+
114
+ expect(BinderUtils.findFirstAncestor(binder, child)).toEqual(binder:Get(grandparent))
115
+
116
+ env.destroy()
117
+ end)
118
+
119
+ it("returns nil when no ancestor is bound", function()
120
+ local env = setup()
121
+
122
+ local parent = env.newInstance()
123
+ local child = env.newInstance(parent)
124
+ local binder = env.newBinder()
125
+ env.boot()
126
+
127
+ expect(BinderUtils.findFirstAncestor(binder, child)).toBeNil()
128
+
129
+ env.destroy()
130
+ end)
131
+
132
+ it("throws for a non-instance child", function()
133
+ local env = setup()
134
+
135
+ local binder = env.newBinder()
136
+ env.boot()
137
+ expect(function()
138
+ BinderUtils.findFirstAncestor(binder, 5 :: any)
139
+ end).toThrow()
140
+
141
+ env.destroy()
142
+ end)
143
+ end)
144
+
145
+ describe("BinderUtils.findFirstChild()", function()
146
+ it("returns the first bound child", function()
147
+ local env = setup()
148
+
149
+ local parent = env.newInstance()
150
+ local unboundChild = env.newInstance(parent)
151
+ local boundChild = env.newInstance(parent)
152
+
153
+ local binder = env.newBinder()
154
+ binder:Tag(boundChild)
155
+ env.boot()
156
+
157
+ expect(BinderUtils.findFirstChild(binder, parent)).toEqual(binder:Get(boundChild))
158
+ expect(binder:Get(unboundChild)).toBeNil()
159
+
160
+ env.destroy()
161
+ end)
162
+
163
+ it("returns nil when no child is bound", function()
164
+ local env = setup()
165
+
166
+ local parent = env.newInstance()
167
+ env.newInstance(parent)
168
+ local binder = env.newBinder()
169
+ env.boot()
170
+
171
+ expect(BinderUtils.findFirstChild(binder, parent)).toBeNil()
172
+
173
+ env.destroy()
174
+ end)
175
+ end)
176
+
177
+ describe("BinderUtils.getChildren()", function()
178
+ it("returns every bound child", function()
179
+ local env = setup()
180
+
181
+ local parent = env.newInstance()
182
+ local childA = env.newInstance(parent)
183
+ local childB = env.newInstance(parent)
184
+ env.newInstance(parent) -- unbound
185
+
186
+ local binder = env.newBinder()
187
+ binder:Tag(childA)
188
+ binder:Tag(childB)
189
+ env.boot()
190
+
191
+ expect(#BinderUtils.getChildren(binder, parent)).toEqual(2)
192
+
193
+ env.destroy()
194
+ end)
195
+
196
+ it("does not include bound descendants deeper than one level", function()
197
+ local env = setup()
198
+
199
+ local parent = env.newInstance()
200
+ local child = env.newInstance(parent)
201
+ local grandchild = env.newInstance(child)
202
+
203
+ local binder = env.newBinder()
204
+ binder:Tag(grandchild)
205
+ env.boot()
206
+
207
+ expect(#BinderUtils.getChildren(binder, parent)).toEqual(0)
208
+
209
+ env.destroy()
210
+ end)
211
+ end)
212
+
213
+ describe("BinderUtils.getDescendants()", function()
214
+ it("returns bound instances at any depth", function()
215
+ local env = setup()
216
+
217
+ local parent = env.newInstance()
218
+ local child = env.newInstance(parent)
219
+ local grandchild = env.newInstance(child)
220
+
221
+ local binder = env.newBinder()
222
+ binder:Tag(child)
223
+ binder:Tag(grandchild)
224
+ env.boot()
225
+
226
+ expect(#BinderUtils.getDescendants(binder, parent)).toEqual(2)
227
+
228
+ env.destroy()
229
+ end)
230
+ end)
231
+
232
+ describe("BinderUtils.mapBinderListToTable()", function()
233
+ it("keys binders by their tag", function()
234
+ local env = setup()
235
+
236
+ local binderA = env.newBinder()
237
+ local binderB = env.newBinder()
238
+ env.boot()
239
+
240
+ local map = BinderUtils.mapBinderListToTable({ binderA, binderB })
241
+ expect(map[binderA:GetTag()]).toEqual(binderA)
242
+ expect(map[binderB:GetTag()]).toEqual(binderB)
243
+
244
+ env.destroy()
245
+ end)
246
+ end)
247
+
248
+ describe("BinderUtils.getMappedFromList()", function()
249
+ it("resolves bound values across an instance list by tag", function()
250
+ local env = setup()
251
+
252
+ local instA = env.newInstance()
253
+ local instB = env.newInstance()
254
+ local binderA = env.newBinder()
255
+ local binderB = env.newBinder()
256
+ binderA:Tag(instA)
257
+ binderB:Tag(instB)
258
+ env.boot()
259
+
260
+ local tagsMap = BinderUtils.mapBinderListToTable({ binderA, binderB })
261
+ local objects = BinderUtils.getMappedFromList(tagsMap, { instA, instB, env.newInstance() })
262
+
263
+ expect(#objects).toEqual(2)
264
+
265
+ env.destroy()
266
+ end)
267
+ end)
268
+
269
+ describe("BinderUtils.getChildrenOfBinders()", function()
270
+ it("returns children bound by any binder in the list", function()
271
+ local env = setup()
272
+
273
+ local parent = env.newInstance()
274
+ local childA = env.newInstance(parent)
275
+ local childB = env.newInstance(parent)
276
+
277
+ local binderA = env.newBinder()
278
+ local binderB = env.newBinder()
279
+ binderA:Tag(childA)
280
+ binderB:Tag(childB)
281
+ env.boot()
282
+
283
+ expect(#BinderUtils.getChildrenOfBinders({ binderA, binderB }, parent)).toEqual(2)
284
+
285
+ env.destroy()
286
+ end)
287
+ end)
288
+
289
+ describe("BinderUtils.getLinkedChildren()", function()
290
+ it("resolves bound targets of matching ObjectValue links", function()
291
+ local env = setup()
292
+
293
+ local parent = env.newInstance()
294
+ local target = env.newInstance()
295
+ local binder = env.newBinder()
296
+ binder:Tag(target)
297
+
298
+ local link = env.newInstance(parent, "ObjectValue") :: ObjectValue
299
+ link.Name = "Link"
300
+ link.Value = target
301
+ env.boot()
302
+
303
+ local objects = BinderUtils.getLinkedChildren(binder, "Link", parent)
304
+ expect(#objects).toEqual(1)
305
+ expect(objects[1]).toEqual(binder:Get(target))
306
+
307
+ env.destroy()
308
+ end)
309
+
310
+ it("ignores links whose name does not match", function()
311
+ local env = setup()
312
+
313
+ local parent = env.newInstance()
314
+ local target = env.newInstance()
315
+ local binder = env.newBinder()
316
+ binder:Tag(target)
317
+
318
+ local link = env.newInstance(parent, "ObjectValue") :: ObjectValue
319
+ link.Name = "OtherLink"
320
+ link.Value = target
321
+ env.boot()
322
+
323
+ expect(#BinderUtils.getLinkedChildren(binder, "Link", parent)).toEqual(0)
324
+
325
+ env.destroy()
326
+ end)
327
+
328
+ it("deduplicates when two links point to the same bound target", function()
329
+ local env = setup()
330
+
331
+ local parent = env.newInstance()
332
+ local target = env.newInstance()
333
+ local binder = env.newBinder()
334
+ binder:Tag(target)
335
+
336
+ for _ = 1, 2 do
337
+ local link = env.newInstance(parent, "ObjectValue") :: ObjectValue
338
+ link.Name = "Link"
339
+ link.Value = target
340
+ end
341
+ env.boot()
342
+
343
+ expect(#BinderUtils.getLinkedChildren(binder, "Link", parent)).toEqual(1)
344
+
345
+ env.destroy()
346
+ end)
347
+ end)