@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.
- package/CHANGELOG.md +10 -0
- package/package.json +12 -12
- package/src/Shared/Binder.spec.lua +721 -0
- package/src/Shared/BinderGroup.spec.lua +156 -0
- package/src/Shared/BinderGroupProvider.spec.lua +117 -0
- package/src/Shared/BinderProvider.spec.lua +3 -5
- package/src/Shared/BinderUtils.spec.lua +347 -0
- package/src/Shared/Collection/BoundChildCollection.spec.lua +212 -0
- package/src/Shared/Promise/promiseBoundClass.spec.lua +135 -0
- package/src/Shared/Trackers/BoundAncestorTracker.spec.lua +175 -0
- package/src/Shared/Trackers/BoundParentTracker.spec.lua +167 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Comprehensive coverage for Binder.
|
|
4
|
+
|
|
5
|
+
Runtime binders are booted the way production code boots them: registered on a BinderProvider
|
|
6
|
+
that is initialized and started through a ServiceBag, and torn down through serviceBag:Destroy().
|
|
7
|
+
Adornees are parented into a workspace container because CollectionService.GetInstanceAddedSignal
|
|
8
|
+
only fires for instances that live in the DataModel. A bind that happens after :Start() is awaited
|
|
9
|
+
event-driven through `binder:Promise(inst):Yield()`, never a fixed sleep. Tags are global and the
|
|
10
|
+
test place is shared across a batch run, so every test uses a distinct tag and cleans up after
|
|
11
|
+
itself.
|
|
12
|
+
|
|
13
|
+
A handful of tests exercise Binder's standalone lifecycle contract (:new/:Init/:Start guards,
|
|
14
|
+
:Create) and construct the binder directly -- there is no other way to assert, for example, that
|
|
15
|
+
:Start() is idempotent when called twice by hand.
|
|
16
|
+
|
|
17
|
+
@class Binder.spec.lua
|
|
18
|
+
]]
|
|
19
|
+
|
|
20
|
+
local require = require(script.Parent.loader).load(script)
|
|
21
|
+
|
|
22
|
+
local Binder = require("Binder")
|
|
23
|
+
local BinderProvider = require("BinderProvider")
|
|
24
|
+
local Jest = require("Jest")
|
|
25
|
+
local ServiceBag = require("ServiceBag")
|
|
26
|
+
|
|
27
|
+
local describe = Jest.Globals.describe
|
|
28
|
+
local expect = Jest.Globals.expect
|
|
29
|
+
local it = Jest.Globals.it
|
|
30
|
+
|
|
31
|
+
local specCounter = 0
|
|
32
|
+
|
|
33
|
+
-- A minimal bound class that records its instance and whether it was destroyed. It deliberately
|
|
34
|
+
-- does NOT retain its constructor varargs: the ServiceBag is injected as a constructor arg, and its
|
|
35
|
+
-- object graph contains Quenty Signals whose strict __index makes jest's deep-equality traversal
|
|
36
|
+
-- throw. Keeping the class free of them lets toEqual compare instances safely.
|
|
37
|
+
local function makeTrackingClass()
|
|
38
|
+
local Class = {}
|
|
39
|
+
Class.__index = Class
|
|
40
|
+
Class.ClassName = "TrackingClass"
|
|
41
|
+
|
|
42
|
+
function Class.new(inst)
|
|
43
|
+
local self = setmetatable({}, Class)
|
|
44
|
+
self.instance = inst
|
|
45
|
+
self.destroyed = false
|
|
46
|
+
return self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
function Class:Destroy()
|
|
50
|
+
self.destroyed = true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return Class
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
-- Returns once `inst` is no longer bound. CollectionService tag removal is synchronous here, so the
|
|
57
|
+
-- class is usually already gone by the time we check; the guarded wait also covers a deferred case.
|
|
58
|
+
local function awaitUnbound(binder, inst)
|
|
59
|
+
if binder:Get(inst) ~= nil then
|
|
60
|
+
binder:GetClassRemovedSignal():Wait()
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
-- A no-op constructor that still returns a value, so Binder's generic bound type stays inhabited.
|
|
65
|
+
local function noopConstructor()
|
|
66
|
+
return {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
local function setup()
|
|
70
|
+
specCounter += 1
|
|
71
|
+
local suffix = specCounter
|
|
72
|
+
|
|
73
|
+
local serviceBag = ServiceBag.new()
|
|
74
|
+
local serviceBagDestroyed = false
|
|
75
|
+
|
|
76
|
+
local container = Instance.new("Folder")
|
|
77
|
+
container.Name = "BinderSpecContainer"
|
|
78
|
+
container.Parent = workspace
|
|
79
|
+
|
|
80
|
+
local instances = {}
|
|
81
|
+
local pendingBinders: { any } = {}
|
|
82
|
+
local extraTagCounter = 0
|
|
83
|
+
local booted = false
|
|
84
|
+
|
|
85
|
+
local function uniqueTag(): string
|
|
86
|
+
extraTagCounter += 1
|
|
87
|
+
return string.format("BinderSpecTag_%d_%d", suffix, extraTagCounter)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
-- Registers a binder to be booted by boot(); returns the binder so the test can tag/query it.
|
|
91
|
+
-- The constructor is intentionally untyped (any) so per-test generic inference does not leak
|
|
92
|
+
-- into this shared helper.
|
|
93
|
+
local function addBinder(constructor: any, ...): Binder.Binder<any>
|
|
94
|
+
assert(not booted, "Cannot add a binder after boot()")
|
|
95
|
+
local binder = Binder.new(uniqueTag(), constructor, ...)
|
|
96
|
+
table.insert(pendingBinders, binder)
|
|
97
|
+
return binder
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
-- Boots the registered binders through a ServiceBag, exactly as production code does.
|
|
101
|
+
local function boot()
|
|
102
|
+
assert(not booted, "Already booted")
|
|
103
|
+
booted = true
|
|
104
|
+
|
|
105
|
+
local binders = pendingBinders
|
|
106
|
+
local provider = BinderProvider.new(string.format("BinderSpecProvider_%d", suffix), function(self)
|
|
107
|
+
for _, binder in binders do
|
|
108
|
+
self:Add(binder)
|
|
109
|
+
end
|
|
110
|
+
end)
|
|
111
|
+
|
|
112
|
+
serviceBag:GetService(provider)
|
|
113
|
+
serviceBag:Init()
|
|
114
|
+
serviceBag:Start()
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
local function newInstance(parent: Instance?, className: string?): Instance
|
|
118
|
+
local inst = Instance.new(className or "Folder")
|
|
119
|
+
inst.Parent = parent or container
|
|
120
|
+
table.insert(instances, inst)
|
|
121
|
+
return inst
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
local function destroyServiceBag()
|
|
125
|
+
if not serviceBagDestroyed then
|
|
126
|
+
serviceBagDestroyed = true
|
|
127
|
+
serviceBag:Destroy()
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
container = container,
|
|
133
|
+
uniqueTag = uniqueTag,
|
|
134
|
+
addBinder = addBinder,
|
|
135
|
+
boot = boot,
|
|
136
|
+
newInstance = newInstance,
|
|
137
|
+
destroyServiceBag = destroyServiceBag,
|
|
138
|
+
destroy = function()
|
|
139
|
+
destroyServiceBag()
|
|
140
|
+
for _, inst in instances do
|
|
141
|
+
pcall(function()
|
|
142
|
+
inst:Destroy()
|
|
143
|
+
end)
|
|
144
|
+
end
|
|
145
|
+
container:Destroy()
|
|
146
|
+
end,
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe("Binder.new()", function()
|
|
151
|
+
it("constructs a binder", function()
|
|
152
|
+
-- Never Init'd/Started, so it holds no resources and needs no teardown.
|
|
153
|
+
local binder = Binder.new("BinderNewSpecTag", noopConstructor)
|
|
154
|
+
expect(Binder.isBinder(binder)).toEqual(true)
|
|
155
|
+
end)
|
|
156
|
+
|
|
157
|
+
it("derives the ServiceName from the tag", function()
|
|
158
|
+
local binder = Binder.new("BinderNameSpecTag", noopConstructor)
|
|
159
|
+
expect(binder.ServiceName).toEqual("BinderNameSpecTagBinder")
|
|
160
|
+
end)
|
|
161
|
+
|
|
162
|
+
it("throws on a non-string tag name", function()
|
|
163
|
+
expect(function()
|
|
164
|
+
Binder.new(123 :: any, noopConstructor)
|
|
165
|
+
end).toThrow()
|
|
166
|
+
end)
|
|
167
|
+
|
|
168
|
+
it("throws when constructing a binder of a binder", function()
|
|
169
|
+
local inner = Binder.new("BinderOfBinderInner", noopConstructor)
|
|
170
|
+
expect(function()
|
|
171
|
+
Binder.new("BinderOfBinderOuter", inner :: any)
|
|
172
|
+
end).toThrow()
|
|
173
|
+
end)
|
|
174
|
+
|
|
175
|
+
it("captures variadic constructor args", function()
|
|
176
|
+
local env = setup()
|
|
177
|
+
|
|
178
|
+
-- Explicit .new()-time args take precedence over the ServiceBag that Init would otherwise
|
|
179
|
+
-- inject, so this class only ever captures the "a"/"b" passed here.
|
|
180
|
+
local binder = env.addBinder(function(_inst, a, b)
|
|
181
|
+
return { a = a, b = b }
|
|
182
|
+
end, "a", "b")
|
|
183
|
+
env.boot()
|
|
184
|
+
|
|
185
|
+
local inst = env.newInstance()
|
|
186
|
+
binder:Tag(inst)
|
|
187
|
+
|
|
188
|
+
local ok, class = binder:Promise(inst):Yield()
|
|
189
|
+
assert(ok, "Never bound")
|
|
190
|
+
expect(class.a).toEqual("a")
|
|
191
|
+
expect(class.b).toEqual("b")
|
|
192
|
+
|
|
193
|
+
env.destroy()
|
|
194
|
+
end)
|
|
195
|
+
end)
|
|
196
|
+
|
|
197
|
+
describe("Binder.isBinder()", function()
|
|
198
|
+
it("returns false for non-tables", function()
|
|
199
|
+
expect(Binder.isBinder(nil)).toEqual(false)
|
|
200
|
+
expect(Binder.isBinder(5)).toEqual(false)
|
|
201
|
+
expect(Binder.isBinder("binder")).toEqual(false)
|
|
202
|
+
end)
|
|
203
|
+
|
|
204
|
+
it("returns false for tables missing the interface", function()
|
|
205
|
+
expect(Binder.isBinder({ Start = function() end })).toEqual(false)
|
|
206
|
+
end)
|
|
207
|
+
|
|
208
|
+
it("returns true for a real binder", function()
|
|
209
|
+
local binder = Binder.new("BinderIsBinderSpecTag", noopConstructor)
|
|
210
|
+
expect(Binder.isBinder(binder)).toEqual(true)
|
|
211
|
+
end)
|
|
212
|
+
end)
|
|
213
|
+
|
|
214
|
+
describe("Binder metadata", function()
|
|
215
|
+
it("returns the tag from GetTag()", function()
|
|
216
|
+
local binder = Binder.new("BinderGetTagSpecTag", noopConstructor)
|
|
217
|
+
expect(binder:GetTag()).toEqual("BinderGetTagSpecTag")
|
|
218
|
+
end)
|
|
219
|
+
|
|
220
|
+
it("returns the constructor from GetConstructor()", function()
|
|
221
|
+
local constructor = noopConstructor
|
|
222
|
+
local binder = Binder.new("BinderGetConstructorSpecTag", constructor)
|
|
223
|
+
expect(binder:GetConstructor()).toEqual(constructor)
|
|
224
|
+
end)
|
|
225
|
+
end)
|
|
226
|
+
|
|
227
|
+
describe("Binder constructor variants", function()
|
|
228
|
+
it("supports a plain function constructor", function()
|
|
229
|
+
local env = setup()
|
|
230
|
+
|
|
231
|
+
local binder = env.addBinder(function(inst)
|
|
232
|
+
return { instance = inst, kind = "function" }
|
|
233
|
+
end)
|
|
234
|
+
env.boot()
|
|
235
|
+
|
|
236
|
+
local inst = env.newInstance()
|
|
237
|
+
binder:Tag(inst)
|
|
238
|
+
|
|
239
|
+
local ok, class = binder:Promise(inst):Yield()
|
|
240
|
+
assert(ok, "Never bound")
|
|
241
|
+
expect(class.kind).toEqual("function")
|
|
242
|
+
expect(class.instance).toEqual(inst)
|
|
243
|
+
|
|
244
|
+
env.destroy()
|
|
245
|
+
end)
|
|
246
|
+
|
|
247
|
+
it("supports a class table with .new", function()
|
|
248
|
+
local env = setup()
|
|
249
|
+
|
|
250
|
+
local Class = makeTrackingClass()
|
|
251
|
+
local binder = env.addBinder(Class)
|
|
252
|
+
env.boot()
|
|
253
|
+
|
|
254
|
+
local inst = env.newInstance()
|
|
255
|
+
binder:Tag(inst)
|
|
256
|
+
|
|
257
|
+
local ok, class = binder:Promise(inst):Yield()
|
|
258
|
+
assert(ok, "Never bound")
|
|
259
|
+
expect(class.instance).toEqual(inst)
|
|
260
|
+
|
|
261
|
+
env.destroy()
|
|
262
|
+
end)
|
|
263
|
+
|
|
264
|
+
it("supports a provider table with :Create", function()
|
|
265
|
+
local env = setup()
|
|
266
|
+
|
|
267
|
+
local provider = {}
|
|
268
|
+
function provider.Create(_self, inst)
|
|
269
|
+
return { instance = inst, kind = "provider" }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
local binder = env.addBinder(provider :: any)
|
|
273
|
+
env.boot()
|
|
274
|
+
|
|
275
|
+
local inst = env.newInstance()
|
|
276
|
+
binder:Tag(inst)
|
|
277
|
+
|
|
278
|
+
local ok, class = binder:Promise(inst):Yield()
|
|
279
|
+
assert(ok, "Never bound")
|
|
280
|
+
expect(class.kind).toEqual("provider")
|
|
281
|
+
|
|
282
|
+
env.destroy()
|
|
283
|
+
end)
|
|
284
|
+
end)
|
|
285
|
+
|
|
286
|
+
describe("Binder lifecycle guards", function()
|
|
287
|
+
it("Init is idempotent", function()
|
|
288
|
+
local binder = Binder.new("BinderInitIdempotentSpecTag", noopConstructor)
|
|
289
|
+
binder:Init()
|
|
290
|
+
expect(function()
|
|
291
|
+
binder:Init()
|
|
292
|
+
end).never.toThrow()
|
|
293
|
+
binder:Destroy()
|
|
294
|
+
end)
|
|
295
|
+
|
|
296
|
+
it("Start is idempotent", function()
|
|
297
|
+
local binder = Binder.new("BinderStartIdempotentSpecTag", noopConstructor)
|
|
298
|
+
binder:Start()
|
|
299
|
+
expect(function()
|
|
300
|
+
binder:Start()
|
|
301
|
+
end).never.toThrow()
|
|
302
|
+
binder:Destroy()
|
|
303
|
+
end)
|
|
304
|
+
|
|
305
|
+
it("Start initializes implicitly", function()
|
|
306
|
+
local binder = Binder.new("BinderStartInitSpecTag", noopConstructor)
|
|
307
|
+
binder:Start()
|
|
308
|
+
expect(binder:GetAll()).toEqual({})
|
|
309
|
+
binder:Destroy()
|
|
310
|
+
end)
|
|
311
|
+
end)
|
|
312
|
+
|
|
313
|
+
describe("Binder binding via ServiceBag", function()
|
|
314
|
+
it("binds instances tagged before start", function()
|
|
315
|
+
local env = setup()
|
|
316
|
+
|
|
317
|
+
local Class = makeTrackingClass()
|
|
318
|
+
local binder = env.addBinder(Class)
|
|
319
|
+
|
|
320
|
+
local inst = env.newInstance()
|
|
321
|
+
binder:Tag(inst) -- tag before the service bag starts the binder
|
|
322
|
+
|
|
323
|
+
env.boot()
|
|
324
|
+
|
|
325
|
+
-- Pre-tagged instances bind synchronously as the binder starts.
|
|
326
|
+
expect(binder:Get(inst)).never.toBeNil()
|
|
327
|
+
|
|
328
|
+
env.destroy()
|
|
329
|
+
end)
|
|
330
|
+
|
|
331
|
+
it("binds instances tagged after start", function()
|
|
332
|
+
local env = setup()
|
|
333
|
+
|
|
334
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
335
|
+
env.boot()
|
|
336
|
+
|
|
337
|
+
local inst = env.newInstance()
|
|
338
|
+
binder:Tag(inst)
|
|
339
|
+
|
|
340
|
+
local ok = binder:Promise(inst):Yield()
|
|
341
|
+
expect(ok).toEqual(true)
|
|
342
|
+
|
|
343
|
+
env.destroy()
|
|
344
|
+
end)
|
|
345
|
+
end)
|
|
346
|
+
|
|
347
|
+
describe("Binder:Get()", function()
|
|
348
|
+
it("returns nil for an unbound instance", function()
|
|
349
|
+
local env = setup()
|
|
350
|
+
|
|
351
|
+
local binder = env.addBinder(function() end)
|
|
352
|
+
env.boot()
|
|
353
|
+
|
|
354
|
+
expect(binder:Get(env.newInstance())).toBeNil()
|
|
355
|
+
|
|
356
|
+
env.destroy()
|
|
357
|
+
end)
|
|
358
|
+
|
|
359
|
+
it("throws when passed a non-instance", function()
|
|
360
|
+
local env = setup()
|
|
361
|
+
|
|
362
|
+
local binder = env.addBinder(function() end)
|
|
363
|
+
env.boot()
|
|
364
|
+
|
|
365
|
+
expect(function()
|
|
366
|
+
binder:Get(5 :: any)
|
|
367
|
+
end).toThrow()
|
|
368
|
+
|
|
369
|
+
env.destroy()
|
|
370
|
+
end)
|
|
371
|
+
end)
|
|
372
|
+
|
|
373
|
+
describe("Binder tagging", function()
|
|
374
|
+
it("Tag/HasTag/Untag manage the collection service tag", function()
|
|
375
|
+
local env = setup()
|
|
376
|
+
|
|
377
|
+
local binder = env.addBinder(function() end)
|
|
378
|
+
env.boot()
|
|
379
|
+
|
|
380
|
+
local inst = env.newInstance()
|
|
381
|
+
expect(binder:HasTag(inst)).toEqual(false)
|
|
382
|
+
binder:Tag(inst)
|
|
383
|
+
expect(binder:HasTag(inst)).toEqual(true)
|
|
384
|
+
binder:Untag(inst)
|
|
385
|
+
expect(binder:HasTag(inst)).toEqual(false)
|
|
386
|
+
|
|
387
|
+
env.destroy()
|
|
388
|
+
end)
|
|
389
|
+
|
|
390
|
+
it("Bind tags and Unbind untags on the server", function()
|
|
391
|
+
local env = setup()
|
|
392
|
+
|
|
393
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
394
|
+
env.boot()
|
|
395
|
+
|
|
396
|
+
local inst = env.newInstance()
|
|
397
|
+
binder:Bind(inst)
|
|
398
|
+
expect(binder:HasTag(inst)).toEqual(true)
|
|
399
|
+
|
|
400
|
+
local ok = binder:Promise(inst):Yield()
|
|
401
|
+
expect(ok).toEqual(true)
|
|
402
|
+
|
|
403
|
+
binder:Unbind(inst)
|
|
404
|
+
expect(binder:HasTag(inst)).toEqual(false)
|
|
405
|
+
|
|
406
|
+
env.destroy()
|
|
407
|
+
end)
|
|
408
|
+
end)
|
|
409
|
+
|
|
410
|
+
describe("Binder:Create()", function()
|
|
411
|
+
it("creates a tagged, non-archivable instance named after the tag", function()
|
|
412
|
+
local binder = Binder.new("BinderCreateSpecTag", noopConstructor)
|
|
413
|
+
|
|
414
|
+
-- Cast: Create's declared signature marks className required, but it defaults when omitted.
|
|
415
|
+
local inst = (binder :: any):Create()
|
|
416
|
+
expect(inst.Name).toEqual("BinderCreateSpecTag")
|
|
417
|
+
expect(inst.Archivable).toEqual(false)
|
|
418
|
+
expect(binder:HasTag(inst)).toEqual(true)
|
|
419
|
+
expect(inst:IsA("Folder")).toEqual(true)
|
|
420
|
+
|
|
421
|
+
inst:Destroy()
|
|
422
|
+
end)
|
|
423
|
+
|
|
424
|
+
it("honors an explicit class name", function()
|
|
425
|
+
local binder = Binder.new("BinderCreateNamedSpecTag", noopConstructor)
|
|
426
|
+
local inst = binder:Create("BoolValue")
|
|
427
|
+
expect(inst:IsA("BoolValue")).toEqual(true)
|
|
428
|
+
|
|
429
|
+
inst:Destroy()
|
|
430
|
+
end)
|
|
431
|
+
|
|
432
|
+
it("throws on a non-string class name", function()
|
|
433
|
+
local binder = Binder.new("BinderCreateBadSpecTag", noopConstructor)
|
|
434
|
+
expect(function()
|
|
435
|
+
binder:Create(5 :: any)
|
|
436
|
+
end).toThrow()
|
|
437
|
+
end)
|
|
438
|
+
end)
|
|
439
|
+
|
|
440
|
+
describe("Binder:GetAll() / GetAllSet()", function()
|
|
441
|
+
it("tracks every bound class", function()
|
|
442
|
+
local env = setup()
|
|
443
|
+
|
|
444
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
445
|
+
env.boot()
|
|
446
|
+
|
|
447
|
+
local instA = env.newInstance()
|
|
448
|
+
local instB = env.newInstance()
|
|
449
|
+
binder:Tag(instA)
|
|
450
|
+
binder:Tag(instB)
|
|
451
|
+
|
|
452
|
+
assert((binder:Promise(instA):Yield()), "A never bound")
|
|
453
|
+
assert((binder:Promise(instB):Yield()), "B never bound")
|
|
454
|
+
|
|
455
|
+
expect(#binder:GetAll()).toEqual(2)
|
|
456
|
+
|
|
457
|
+
local set = binder:GetAllSet()
|
|
458
|
+
expect(set[binder:Get(instA)]).toEqual(true)
|
|
459
|
+
expect(set[binder:Get(instB)]).toEqual(true)
|
|
460
|
+
|
|
461
|
+
env.destroy()
|
|
462
|
+
end)
|
|
463
|
+
end)
|
|
464
|
+
|
|
465
|
+
describe("Binder signals", function()
|
|
466
|
+
it("memoizes the added/removing/removed signals", function()
|
|
467
|
+
local env = setup()
|
|
468
|
+
|
|
469
|
+
local binder = env.addBinder(function() end)
|
|
470
|
+
env.boot()
|
|
471
|
+
|
|
472
|
+
expect(binder:GetClassAddedSignal()).toEqual(binder:GetClassAddedSignal())
|
|
473
|
+
expect(binder:GetClassRemovingSignal()).toEqual(binder:GetClassRemovingSignal())
|
|
474
|
+
expect(binder:GetClassRemovedSignal()).toEqual(binder:GetClassRemovedSignal())
|
|
475
|
+
|
|
476
|
+
env.destroy()
|
|
477
|
+
end)
|
|
478
|
+
|
|
479
|
+
it("fires the added signal with the class and instance", function()
|
|
480
|
+
local env = setup()
|
|
481
|
+
|
|
482
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
483
|
+
env.boot()
|
|
484
|
+
|
|
485
|
+
local firedClass, firedInst
|
|
486
|
+
binder:GetClassAddedSignal():Connect(function(class, inst)
|
|
487
|
+
firedClass = class
|
|
488
|
+
firedInst = inst
|
|
489
|
+
end)
|
|
490
|
+
|
|
491
|
+
local inst = env.newInstance()
|
|
492
|
+
binder:Tag(inst)
|
|
493
|
+
assert((binder:Promise(inst):Yield()), "Never bound")
|
|
494
|
+
|
|
495
|
+
expect(firedInst).toEqual(inst)
|
|
496
|
+
expect(firedClass).toEqual(binder:Get(inst))
|
|
497
|
+
|
|
498
|
+
env.destroy()
|
|
499
|
+
end)
|
|
500
|
+
|
|
501
|
+
it("fires removing then removed on unbind", function()
|
|
502
|
+
local env = setup()
|
|
503
|
+
|
|
504
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
505
|
+
env.boot()
|
|
506
|
+
|
|
507
|
+
local order = {}
|
|
508
|
+
binder:GetClassRemovingSignal():Connect(function()
|
|
509
|
+
table.insert(order, "removing")
|
|
510
|
+
end)
|
|
511
|
+
binder:GetClassRemovedSignal():Connect(function()
|
|
512
|
+
table.insert(order, "removed")
|
|
513
|
+
end)
|
|
514
|
+
|
|
515
|
+
local inst = env.newInstance()
|
|
516
|
+
binder:Tag(inst)
|
|
517
|
+
assert((binder:Promise(inst):Yield()), "Never bound")
|
|
518
|
+
|
|
519
|
+
binder:Untag(inst)
|
|
520
|
+
awaitUnbound(binder, inst)
|
|
521
|
+
|
|
522
|
+
expect(order[1]).toEqual("removing")
|
|
523
|
+
expect(order[2]).toEqual("removed")
|
|
524
|
+
|
|
525
|
+
env.destroy()
|
|
526
|
+
end)
|
|
527
|
+
|
|
528
|
+
it("destroys the bound class when it is removed", function()
|
|
529
|
+
local env = setup()
|
|
530
|
+
|
|
531
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
532
|
+
env.boot()
|
|
533
|
+
|
|
534
|
+
local inst = env.newInstance()
|
|
535
|
+
binder:Tag(inst)
|
|
536
|
+
local ok, class = binder:Promise(inst):Yield()
|
|
537
|
+
assert(ok, "Never bound")
|
|
538
|
+
|
|
539
|
+
binder:Untag(inst)
|
|
540
|
+
awaitUnbound(binder, inst)
|
|
541
|
+
|
|
542
|
+
expect(class.destroyed).toEqual(true)
|
|
543
|
+
|
|
544
|
+
env.destroy()
|
|
545
|
+
end)
|
|
546
|
+
end)
|
|
547
|
+
|
|
548
|
+
describe("Binder:ObserveInstance()", function()
|
|
549
|
+
it("fires with the class on bind and nil on unbind", function()
|
|
550
|
+
local env = setup()
|
|
551
|
+
|
|
552
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
553
|
+
env.boot()
|
|
554
|
+
|
|
555
|
+
local inst = env.newInstance()
|
|
556
|
+
|
|
557
|
+
-- Wrap each emission so a nil value is still recorded (table.insert(t, nil) is a no-op).
|
|
558
|
+
local emissions = {}
|
|
559
|
+
local cleanup = binder:ObserveInstance(inst, function(class)
|
|
560
|
+
table.insert(emissions, { value = class })
|
|
561
|
+
end)
|
|
562
|
+
|
|
563
|
+
binder:Tag(inst)
|
|
564
|
+
local ok, class = binder:Promise(inst):Yield()
|
|
565
|
+
assert(ok, "Never bound")
|
|
566
|
+
|
|
567
|
+
binder:Untag(inst)
|
|
568
|
+
awaitUnbound(binder, inst)
|
|
569
|
+
|
|
570
|
+
expect(#emissions).toEqual(2)
|
|
571
|
+
expect(emissions[1].value).toEqual(class)
|
|
572
|
+
expect(emissions[2].value).toBeNil()
|
|
573
|
+
|
|
574
|
+
cleanup()
|
|
575
|
+
|
|
576
|
+
env.destroy()
|
|
577
|
+
end)
|
|
578
|
+
|
|
579
|
+
it("returns a cleanup that stops future callbacks", function()
|
|
580
|
+
local env = setup()
|
|
581
|
+
|
|
582
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
583
|
+
env.boot()
|
|
584
|
+
|
|
585
|
+
local inst = env.newInstance()
|
|
586
|
+
|
|
587
|
+
local count = 0
|
|
588
|
+
local cleanup = binder:ObserveInstance(inst, function()
|
|
589
|
+
count += 1
|
|
590
|
+
end)
|
|
591
|
+
cleanup()
|
|
592
|
+
|
|
593
|
+
binder:Tag(inst)
|
|
594
|
+
assert((binder:Promise(inst):Yield()), "Never bound")
|
|
595
|
+
|
|
596
|
+
expect(count).toEqual(0)
|
|
597
|
+
|
|
598
|
+
env.destroy()
|
|
599
|
+
end)
|
|
600
|
+
|
|
601
|
+
it("throws for a non-instance or non-function", function()
|
|
602
|
+
local env = setup()
|
|
603
|
+
|
|
604
|
+
local binder = env.addBinder(function() end)
|
|
605
|
+
env.boot()
|
|
606
|
+
|
|
607
|
+
expect(function()
|
|
608
|
+
binder:ObserveInstance(5 :: any, function() end)
|
|
609
|
+
end).toThrow()
|
|
610
|
+
expect(function()
|
|
611
|
+
binder:ObserveInstance(env.newInstance(), 5 :: any)
|
|
612
|
+
end).toThrow()
|
|
613
|
+
|
|
614
|
+
env.destroy()
|
|
615
|
+
end)
|
|
616
|
+
end)
|
|
617
|
+
|
|
618
|
+
describe("Binder:Observe()", function()
|
|
619
|
+
it("emits the current value on subscribe and updates on bind", function()
|
|
620
|
+
local env = setup()
|
|
621
|
+
|
|
622
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
623
|
+
env.boot()
|
|
624
|
+
|
|
625
|
+
local inst = env.newInstance()
|
|
626
|
+
|
|
627
|
+
-- Wrap each emission so the initial nil value is still recorded.
|
|
628
|
+
local emissions = {}
|
|
629
|
+
local sub = binder:Observe(inst):Subscribe(function(class)
|
|
630
|
+
table.insert(emissions, { value = class })
|
|
631
|
+
end)
|
|
632
|
+
|
|
633
|
+
-- First emission is the (nil) current value.
|
|
634
|
+
expect(#emissions).toEqual(1)
|
|
635
|
+
expect(emissions[1].value).toBeNil()
|
|
636
|
+
|
|
637
|
+
binder:Tag(inst)
|
|
638
|
+
assert((binder:Promise(inst):Yield()), "Never bound")
|
|
639
|
+
|
|
640
|
+
expect(#emissions).toEqual(2)
|
|
641
|
+
expect(emissions[2].value).toEqual(binder:Get(inst))
|
|
642
|
+
|
|
643
|
+
sub:Destroy()
|
|
644
|
+
env.destroy()
|
|
645
|
+
end)
|
|
646
|
+
end)
|
|
647
|
+
|
|
648
|
+
describe("Binder:Promise()", function()
|
|
649
|
+
it("resolves immediately when already bound", function()
|
|
650
|
+
local env = setup()
|
|
651
|
+
|
|
652
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
653
|
+
|
|
654
|
+
local inst = env.newInstance()
|
|
655
|
+
binder:Tag(inst)
|
|
656
|
+
env.boot()
|
|
657
|
+
|
|
658
|
+
local promise = binder:Promise(inst)
|
|
659
|
+
expect(promise:IsFulfilled()).toEqual(true)
|
|
660
|
+
|
|
661
|
+
env.destroy()
|
|
662
|
+
end)
|
|
663
|
+
|
|
664
|
+
it("throws when passed a non-instance", function()
|
|
665
|
+
local env = setup()
|
|
666
|
+
|
|
667
|
+
local binder = env.addBinder(function() end)
|
|
668
|
+
env.boot()
|
|
669
|
+
|
|
670
|
+
expect(function()
|
|
671
|
+
binder:Promise(5 :: any)
|
|
672
|
+
end).toThrow()
|
|
673
|
+
|
|
674
|
+
env.destroy()
|
|
675
|
+
end)
|
|
676
|
+
end)
|
|
677
|
+
|
|
678
|
+
describe("Binder:_add() dedupe", function()
|
|
679
|
+
it("does not rebind an already-bound instance", function()
|
|
680
|
+
local env = setup()
|
|
681
|
+
|
|
682
|
+
local constructed = 0
|
|
683
|
+
local binder = env.addBinder(function(inst)
|
|
684
|
+
constructed += 1
|
|
685
|
+
return { instance = inst }
|
|
686
|
+
end)
|
|
687
|
+
env.boot()
|
|
688
|
+
|
|
689
|
+
local inst = env.newInstance()
|
|
690
|
+
binder:Tag(inst)
|
|
691
|
+
assert((binder:Promise(inst):Yield()), "Never bound")
|
|
692
|
+
|
|
693
|
+
-- Re-tagging an already-tagged instance must not reconstruct.
|
|
694
|
+
binder:Tag(inst)
|
|
695
|
+
task.wait()
|
|
696
|
+
|
|
697
|
+
expect(constructed).toEqual(1)
|
|
698
|
+
|
|
699
|
+
env.destroy()
|
|
700
|
+
end)
|
|
701
|
+
end)
|
|
702
|
+
|
|
703
|
+
describe("Binder teardown", function()
|
|
704
|
+
it("removes and destroys all bound classes when the service bag is destroyed", function()
|
|
705
|
+
local env = setup()
|
|
706
|
+
|
|
707
|
+
local binder = env.addBinder(makeTrackingClass())
|
|
708
|
+
env.boot()
|
|
709
|
+
|
|
710
|
+
local inst = env.newInstance()
|
|
711
|
+
binder:Tag(inst)
|
|
712
|
+
local ok, class = binder:Promise(inst):Yield()
|
|
713
|
+
assert(ok, "Never bound")
|
|
714
|
+
|
|
715
|
+
env.destroyServiceBag()
|
|
716
|
+
|
|
717
|
+
expect(class.destroyed).toEqual(true)
|
|
718
|
+
|
|
719
|
+
env.destroy()
|
|
720
|
+
end)
|
|
721
|
+
end)
|