@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,212 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Coverage for BoundChildCollection, which tracks the bound children of a parent instance.
|
|
4
|
+
|
|
5
|
+
The binder is booted through a ServiceBag (as production code does); the collection itself is a
|
|
6
|
+
plain BaseObject and is constructed directly. Everything lives under a workspace container so
|
|
7
|
+
CollectionService and ChildAdded/ChildRemoved signals fire. Children tagged before the service
|
|
8
|
+
bag starts bind synchronously; changes made after construction are awaited event-driven via the
|
|
9
|
+
collection's own ClassAdded/ClassRemoved signals rather than a fixed sleep.
|
|
10
|
+
|
|
11
|
+
@class BoundChildCollection.spec.lua
|
|
12
|
+
]]
|
|
13
|
+
|
|
14
|
+
local require = require(script.Parent.loader).load(script)
|
|
15
|
+
|
|
16
|
+
local Binder = require("Binder")
|
|
17
|
+
local BinderProvider = require("BinderProvider")
|
|
18
|
+
local BoundChildCollection = require("BoundChildCollection")
|
|
19
|
+
local Jest = require("Jest")
|
|
20
|
+
local ServiceBag = require("ServiceBag")
|
|
21
|
+
|
|
22
|
+
local describe = Jest.Globals.describe
|
|
23
|
+
local expect = Jest.Globals.expect
|
|
24
|
+
local it = Jest.Globals.it
|
|
25
|
+
|
|
26
|
+
local specCounter = 0
|
|
27
|
+
|
|
28
|
+
local function makeClass()
|
|
29
|
+
local Class = {}
|
|
30
|
+
Class.__index = Class
|
|
31
|
+
Class.ClassName = "BoundChildCollectionSpecClass"
|
|
32
|
+
function Class.new(inst)
|
|
33
|
+
return setmetatable({ instance = inst }, Class)
|
|
34
|
+
end
|
|
35
|
+
function Class:Destroy() end
|
|
36
|
+
return Class
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
local function setup()
|
|
40
|
+
specCounter += 1
|
|
41
|
+
local suffix = specCounter
|
|
42
|
+
|
|
43
|
+
local serviceBag = ServiceBag.new()
|
|
44
|
+
local container = Instance.new("Folder")
|
|
45
|
+
container.Name = "BoundChildCollectionSpecContainer"
|
|
46
|
+
container.Parent = workspace
|
|
47
|
+
|
|
48
|
+
local instances = {}
|
|
49
|
+
local cleanups = {}
|
|
50
|
+
local booted = false
|
|
51
|
+
|
|
52
|
+
local binder = Binder.new(string.format("BoundChildCollectionSpecTag_%d", suffix), makeClass() :: any)
|
|
53
|
+
|
|
54
|
+
local function newInstance(parent: Instance?): Instance
|
|
55
|
+
local inst = Instance.new("Folder")
|
|
56
|
+
inst.Parent = parent or container
|
|
57
|
+
table.insert(instances, inst)
|
|
58
|
+
return inst
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
local function track(item: any): any
|
|
62
|
+
table.insert(cleanups, item)
|
|
63
|
+
return item
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
local function boot()
|
|
67
|
+
assert(not booted, "Already booted")
|
|
68
|
+
booted = true
|
|
69
|
+
|
|
70
|
+
local provider = BinderProvider.new(string.format("BoundChildCollectionSpecProvider_%d", suffix), function(self)
|
|
71
|
+
self:Add(binder)
|
|
72
|
+
end)
|
|
73
|
+
serviceBag:GetService(provider)
|
|
74
|
+
serviceBag:Init()
|
|
75
|
+
serviceBag:Start()
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
container = container,
|
|
80
|
+
binder = binder,
|
|
81
|
+
newInstance = newInstance,
|
|
82
|
+
track = track,
|
|
83
|
+
boot = boot,
|
|
84
|
+
destroy = function()
|
|
85
|
+
for _, item in cleanups do
|
|
86
|
+
pcall(function()
|
|
87
|
+
item:Destroy()
|
|
88
|
+
end)
|
|
89
|
+
end
|
|
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("BoundChildCollection construction", function()
|
|
102
|
+
it("counts children bound before construction without firing ClassAdded", function()
|
|
103
|
+
local env = setup()
|
|
104
|
+
|
|
105
|
+
local parent = env.newInstance()
|
|
106
|
+
local childA = env.newInstance(parent)
|
|
107
|
+
local childB = env.newInstance(parent)
|
|
108
|
+
env.newInstance(parent) -- unbound child
|
|
109
|
+
|
|
110
|
+
env.binder:Tag(childA)
|
|
111
|
+
env.binder:Tag(childB)
|
|
112
|
+
env.boot()
|
|
113
|
+
|
|
114
|
+
local fired = 0
|
|
115
|
+
local collection = env.track(BoundChildCollection.new(env.binder, parent))
|
|
116
|
+
collection.ClassAdded:Connect(function()
|
|
117
|
+
fired += 1
|
|
118
|
+
end)
|
|
119
|
+
|
|
120
|
+
expect(collection:GetSize()).toEqual(2)
|
|
121
|
+
expect(fired).toEqual(0)
|
|
122
|
+
expect(#collection:GetClasses()).toEqual(2)
|
|
123
|
+
expect(collection:HasClass(env.binder:Get(childA))).toEqual(true)
|
|
124
|
+
|
|
125
|
+
env.destroy()
|
|
126
|
+
end)
|
|
127
|
+
end)
|
|
128
|
+
|
|
129
|
+
describe("BoundChildCollection dynamic updates", function()
|
|
130
|
+
it("fires ClassAdded when a bound child is reparented in", function()
|
|
131
|
+
local env = setup()
|
|
132
|
+
|
|
133
|
+
local parent = env.newInstance()
|
|
134
|
+
env.boot()
|
|
135
|
+
|
|
136
|
+
local collection = env.track(BoundChildCollection.new(env.binder, parent))
|
|
137
|
+
|
|
138
|
+
-- Bind an instance living elsewhere, then move it under the tracked parent.
|
|
139
|
+
local child = env.newInstance()
|
|
140
|
+
env.binder:Tag(child)
|
|
141
|
+
local ok, class = env.binder:Promise(child):Yield()
|
|
142
|
+
assert(ok, "child never bound")
|
|
143
|
+
|
|
144
|
+
local addedClass
|
|
145
|
+
local conn = collection.ClassAdded:Connect(function(c)
|
|
146
|
+
addedClass = c
|
|
147
|
+
end)
|
|
148
|
+
|
|
149
|
+
child.Parent = parent
|
|
150
|
+
if collection:GetSize() == 0 then
|
|
151
|
+
collection.ClassAdded:Wait()
|
|
152
|
+
end
|
|
153
|
+
conn:Disconnect()
|
|
154
|
+
|
|
155
|
+
expect(addedClass).toEqual(class)
|
|
156
|
+
expect(collection:GetSize()).toEqual(1)
|
|
157
|
+
|
|
158
|
+
env.destroy()
|
|
159
|
+
end)
|
|
160
|
+
|
|
161
|
+
it("fires ClassRemoved when a tracked child is reparented out", function()
|
|
162
|
+
local env = setup()
|
|
163
|
+
|
|
164
|
+
local parent = env.newInstance()
|
|
165
|
+
local child = env.newInstance(parent)
|
|
166
|
+
env.binder:Tag(child)
|
|
167
|
+
env.boot()
|
|
168
|
+
|
|
169
|
+
local collection = env.track(BoundChildCollection.new(env.binder, parent))
|
|
170
|
+
expect(collection:GetSize()).toEqual(1)
|
|
171
|
+
|
|
172
|
+
local removedClass
|
|
173
|
+
local conn = collection.ClassRemoved:Connect(function(c)
|
|
174
|
+
removedClass = c
|
|
175
|
+
end)
|
|
176
|
+
|
|
177
|
+
local class = env.binder:Get(child)
|
|
178
|
+
child.Parent = env.container
|
|
179
|
+
if collection:GetSize() == 1 then
|
|
180
|
+
collection.ClassRemoved:Wait()
|
|
181
|
+
end
|
|
182
|
+
conn:Disconnect()
|
|
183
|
+
|
|
184
|
+
expect(removedClass).toEqual(class)
|
|
185
|
+
expect(collection:GetSize()).toEqual(0)
|
|
186
|
+
|
|
187
|
+
env.destroy()
|
|
188
|
+
end)
|
|
189
|
+
|
|
190
|
+
it("fires ClassRemoved when a tracked child is unbound", function()
|
|
191
|
+
local env = setup()
|
|
192
|
+
|
|
193
|
+
local parent = env.newInstance()
|
|
194
|
+
local child = env.newInstance(parent)
|
|
195
|
+
env.binder:Tag(child)
|
|
196
|
+
env.boot()
|
|
197
|
+
|
|
198
|
+
local collection = env.track(BoundChildCollection.new(env.binder, parent))
|
|
199
|
+
expect(collection:GetSize()).toEqual(1)
|
|
200
|
+
|
|
201
|
+
local conn = collection.ClassRemoved:Connect(function() end)
|
|
202
|
+
env.binder:Untag(child)
|
|
203
|
+
if collection:GetSize() == 1 then
|
|
204
|
+
collection.ClassRemoved:Wait()
|
|
205
|
+
end
|
|
206
|
+
conn:Disconnect()
|
|
207
|
+
|
|
208
|
+
expect(collection:GetSize()).toEqual(0)
|
|
209
|
+
|
|
210
|
+
env.destroy()
|
|
211
|
+
end)
|
|
212
|
+
end)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Coverage for promiseBoundClass, a thin wrapper that delegates to Binder:Promise after
|
|
4
|
+
validating its arguments.
|
|
5
|
+
|
|
6
|
+
The binder is booted through a ServiceBag; the instance is tagged before start so it binds
|
|
7
|
+
synchronously and the promise resolves immediately.
|
|
8
|
+
|
|
9
|
+
@class promiseBoundClass.spec.lua
|
|
10
|
+
]]
|
|
11
|
+
|
|
12
|
+
local require = require(script.Parent.loader).load(script)
|
|
13
|
+
|
|
14
|
+
local Binder = require("Binder")
|
|
15
|
+
local BinderProvider = require("BinderProvider")
|
|
16
|
+
local Jest = require("Jest")
|
|
17
|
+
local ServiceBag = require("ServiceBag")
|
|
18
|
+
local promiseBoundClass = require("promiseBoundClass")
|
|
19
|
+
|
|
20
|
+
local describe = Jest.Globals.describe
|
|
21
|
+
local expect = Jest.Globals.expect
|
|
22
|
+
local it = Jest.Globals.it
|
|
23
|
+
|
|
24
|
+
local specCounter = 0
|
|
25
|
+
|
|
26
|
+
local function makeClass()
|
|
27
|
+
local Class = {}
|
|
28
|
+
Class.__index = Class
|
|
29
|
+
Class.ClassName = "PromiseBoundClassSpecClass"
|
|
30
|
+
function Class.new(inst)
|
|
31
|
+
return setmetatable({ instance = inst }, Class)
|
|
32
|
+
end
|
|
33
|
+
function Class:Destroy() end
|
|
34
|
+
return Class
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
local function setup()
|
|
38
|
+
specCounter += 1
|
|
39
|
+
local suffix = specCounter
|
|
40
|
+
|
|
41
|
+
local serviceBag = ServiceBag.new()
|
|
42
|
+
local container = Instance.new("Folder")
|
|
43
|
+
container.Name = "PromiseBoundClassSpecContainer"
|
|
44
|
+
container.Parent = workspace
|
|
45
|
+
|
|
46
|
+
local instances = {}
|
|
47
|
+
local booted = false
|
|
48
|
+
|
|
49
|
+
local binder = Binder.new(string.format("PromiseBoundClassSpecTag_%d", suffix), makeClass() :: any)
|
|
50
|
+
|
|
51
|
+
local function newInstance(): Instance
|
|
52
|
+
local inst = Instance.new("Folder")
|
|
53
|
+
inst.Parent = container
|
|
54
|
+
table.insert(instances, inst)
|
|
55
|
+
return inst
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
local function boot()
|
|
59
|
+
assert(not booted, "Already booted")
|
|
60
|
+
booted = true
|
|
61
|
+
|
|
62
|
+
local provider = BinderProvider.new(string.format("PromiseBoundClassSpecProvider_%d", suffix), function(self)
|
|
63
|
+
self:Add(binder)
|
|
64
|
+
end)
|
|
65
|
+
serviceBag:GetService(provider)
|
|
66
|
+
serviceBag:Init()
|
|
67
|
+
serviceBag:Start()
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
binder = binder,
|
|
72
|
+
newInstance = newInstance,
|
|
73
|
+
boot = boot,
|
|
74
|
+
destroy = function()
|
|
75
|
+
serviceBag:Destroy()
|
|
76
|
+
for _, inst in instances do
|
|
77
|
+
pcall(function()
|
|
78
|
+
inst:Destroy()
|
|
79
|
+
end)
|
|
80
|
+
end
|
|
81
|
+
container:Destroy()
|
|
82
|
+
end,
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe("promiseBoundClass()", function()
|
|
87
|
+
it("resolves with the bound class", function()
|
|
88
|
+
local env = setup()
|
|
89
|
+
|
|
90
|
+
local inst = env.newInstance()
|
|
91
|
+
env.binder:Tag(inst)
|
|
92
|
+
env.boot()
|
|
93
|
+
|
|
94
|
+
local ok, class = promiseBoundClass(env.binder, inst):Yield()
|
|
95
|
+
assert(ok, "Never bound")
|
|
96
|
+
expect(class).toEqual(env.binder:Get(inst))
|
|
97
|
+
|
|
98
|
+
env.destroy()
|
|
99
|
+
end)
|
|
100
|
+
|
|
101
|
+
it("resolves once an instance is bound after start", function()
|
|
102
|
+
local env = setup()
|
|
103
|
+
|
|
104
|
+
env.boot()
|
|
105
|
+
local inst = env.newInstance()
|
|
106
|
+
env.binder:Tag(inst)
|
|
107
|
+
|
|
108
|
+
local ok = promiseBoundClass(env.binder, inst):Yield()
|
|
109
|
+
expect(ok).toEqual(true)
|
|
110
|
+
|
|
111
|
+
env.destroy()
|
|
112
|
+
end)
|
|
113
|
+
|
|
114
|
+
it("throws when the binder is not a binder", function()
|
|
115
|
+
local env = setup()
|
|
116
|
+
env.boot()
|
|
117
|
+
|
|
118
|
+
expect(function()
|
|
119
|
+
promiseBoundClass({} :: any, env.newInstance())
|
|
120
|
+
end).toThrow()
|
|
121
|
+
|
|
122
|
+
env.destroy()
|
|
123
|
+
end)
|
|
124
|
+
|
|
125
|
+
it("throws when the instance is not an Instance", function()
|
|
126
|
+
local env = setup()
|
|
127
|
+
env.boot()
|
|
128
|
+
|
|
129
|
+
expect(function()
|
|
130
|
+
promiseBoundClass(env.binder, 5 :: any)
|
|
131
|
+
end).toThrow()
|
|
132
|
+
|
|
133
|
+
env.destroy()
|
|
134
|
+
end)
|
|
135
|
+
end)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Coverage for BoundAncestorTracker, which exposes the bound class of a child's nearest bound
|
|
4
|
+
ancestor.
|
|
5
|
+
|
|
6
|
+
The binder is booted through a ServiceBag; the tracker is a plain BaseObject constructed
|
|
7
|
+
directly. Instances live under a workspace container so ancestry and CollectionService signals
|
|
8
|
+
fire. Ancestry changes and late binds are awaited event-driven via the tracker's Class.Changed
|
|
9
|
+
signal rather than a fixed sleep.
|
|
10
|
+
|
|
11
|
+
@class BoundAncestorTracker.spec.lua
|
|
12
|
+
]]
|
|
13
|
+
|
|
14
|
+
local require = require(script.Parent.loader).load(script)
|
|
15
|
+
|
|
16
|
+
local Binder = require("Binder")
|
|
17
|
+
local BinderProvider = require("BinderProvider")
|
|
18
|
+
local BoundAncestorTracker = require("BoundAncestorTracker")
|
|
19
|
+
local Jest = require("Jest")
|
|
20
|
+
local ServiceBag = require("ServiceBag")
|
|
21
|
+
|
|
22
|
+
local describe = Jest.Globals.describe
|
|
23
|
+
local expect = Jest.Globals.expect
|
|
24
|
+
local it = Jest.Globals.it
|
|
25
|
+
|
|
26
|
+
local specCounter = 0
|
|
27
|
+
|
|
28
|
+
local function makeClass()
|
|
29
|
+
local Class = {}
|
|
30
|
+
Class.__index = Class
|
|
31
|
+
Class.ClassName = "BoundAncestorTrackerSpecClass"
|
|
32
|
+
function Class.new(inst)
|
|
33
|
+
return setmetatable({ instance = inst }, Class)
|
|
34
|
+
end
|
|
35
|
+
function Class:Destroy() end
|
|
36
|
+
return Class
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
local function setup()
|
|
40
|
+
specCounter += 1
|
|
41
|
+
local suffix = specCounter
|
|
42
|
+
|
|
43
|
+
local serviceBag = ServiceBag.new()
|
|
44
|
+
local container = Instance.new("Folder")
|
|
45
|
+
container.Name = "BoundAncestorTrackerSpecContainer"
|
|
46
|
+
container.Parent = workspace
|
|
47
|
+
|
|
48
|
+
local instances = {}
|
|
49
|
+
local cleanups = {}
|
|
50
|
+
local booted = false
|
|
51
|
+
|
|
52
|
+
local binder = Binder.new(string.format("BoundAncestorTrackerSpecTag_%d", suffix), makeClass() :: any)
|
|
53
|
+
|
|
54
|
+
local function newInstance(parent: Instance?): Instance
|
|
55
|
+
local inst = Instance.new("Folder")
|
|
56
|
+
inst.Parent = parent or container
|
|
57
|
+
table.insert(instances, inst)
|
|
58
|
+
return inst
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
local function track(item: any): any
|
|
62
|
+
table.insert(cleanups, item)
|
|
63
|
+
return item
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
local function boot()
|
|
67
|
+
assert(not booted, "Already booted")
|
|
68
|
+
booted = true
|
|
69
|
+
|
|
70
|
+
local provider = BinderProvider.new(string.format("BoundAncestorTrackerSpecProvider_%d", suffix), function(self)
|
|
71
|
+
self:Add(binder)
|
|
72
|
+
end)
|
|
73
|
+
serviceBag:GetService(provider)
|
|
74
|
+
serviceBag:Init()
|
|
75
|
+
serviceBag:Start()
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
local function awaitChange(tracker, previous)
|
|
79
|
+
if tracker.Class.Value == previous then
|
|
80
|
+
tracker.Class.Changed:Wait()
|
|
81
|
+
end
|
|
82
|
+
return tracker.Class.Value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
container = container,
|
|
87
|
+
binder = binder,
|
|
88
|
+
newInstance = newInstance,
|
|
89
|
+
track = track,
|
|
90
|
+
boot = boot,
|
|
91
|
+
awaitChange = awaitChange,
|
|
92
|
+
destroy = function()
|
|
93
|
+
for _, item in cleanups do
|
|
94
|
+
pcall(function()
|
|
95
|
+
item:Destroy()
|
|
96
|
+
end)
|
|
97
|
+
end
|
|
98
|
+
serviceBag:Destroy()
|
|
99
|
+
for _, inst in instances do
|
|
100
|
+
pcall(function()
|
|
101
|
+
inst:Destroy()
|
|
102
|
+
end)
|
|
103
|
+
end
|
|
104
|
+
container:Destroy()
|
|
105
|
+
end,
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe("BoundAncestorTracker tracking", function()
|
|
110
|
+
it("exposes the nearest bound ancestor's class", function()
|
|
111
|
+
local env = setup()
|
|
112
|
+
|
|
113
|
+
local grandparent = env.newInstance()
|
|
114
|
+
local parent = env.newInstance(grandparent)
|
|
115
|
+
local child = env.newInstance(parent)
|
|
116
|
+
env.binder:Tag(grandparent)
|
|
117
|
+
env.boot()
|
|
118
|
+
|
|
119
|
+
local tracker = env.track(BoundAncestorTracker.new(env.binder, child))
|
|
120
|
+
expect(tracker.Class.Value).toEqual(env.binder:Get(grandparent))
|
|
121
|
+
|
|
122
|
+
env.destroy()
|
|
123
|
+
end)
|
|
124
|
+
|
|
125
|
+
it("has no value when no ancestor is bound", function()
|
|
126
|
+
local env = setup()
|
|
127
|
+
|
|
128
|
+
local parent = env.newInstance()
|
|
129
|
+
local child = env.newInstance(parent)
|
|
130
|
+
env.boot()
|
|
131
|
+
|
|
132
|
+
local tracker = env.track(BoundAncestorTracker.new(env.binder, child))
|
|
133
|
+
expect(tracker.Class.Value).toBeNil()
|
|
134
|
+
|
|
135
|
+
env.destroy()
|
|
136
|
+
end)
|
|
137
|
+
|
|
138
|
+
it("updates when an ancestor becomes bound", function()
|
|
139
|
+
local env = setup()
|
|
140
|
+
|
|
141
|
+
-- The tracker resolves ancestors above the child's direct parent, so bind the grandparent.
|
|
142
|
+
local ancestor = env.newInstance()
|
|
143
|
+
local parent = env.newInstance(ancestor)
|
|
144
|
+
local child = env.newInstance(parent)
|
|
145
|
+
env.boot()
|
|
146
|
+
|
|
147
|
+
local tracker = env.track(BoundAncestorTracker.new(env.binder, child))
|
|
148
|
+
expect(tracker.Class.Value).toBeNil()
|
|
149
|
+
|
|
150
|
+
env.binder:Tag(ancestor)
|
|
151
|
+
local value = env.awaitChange(tracker, nil)
|
|
152
|
+
expect(value).toEqual(env.binder:Get(ancestor))
|
|
153
|
+
|
|
154
|
+
env.destroy()
|
|
155
|
+
end)
|
|
156
|
+
|
|
157
|
+
it("clears the value when the child leaves the bound ancestry", function()
|
|
158
|
+
local env = setup()
|
|
159
|
+
|
|
160
|
+
local ancestor = env.newInstance()
|
|
161
|
+
local parent = env.newInstance(ancestor)
|
|
162
|
+
local child = env.newInstance(parent)
|
|
163
|
+
env.binder:Tag(ancestor)
|
|
164
|
+
env.boot()
|
|
165
|
+
|
|
166
|
+
local tracker = env.track(BoundAncestorTracker.new(env.binder, child))
|
|
167
|
+
local class = env.binder:Get(ancestor)
|
|
168
|
+
expect(tracker.Class.Value).toEqual(class)
|
|
169
|
+
|
|
170
|
+
child.Parent = env.container
|
|
171
|
+
expect(env.awaitChange(tracker, class)).toBeNil()
|
|
172
|
+
|
|
173
|
+
env.destroy()
|
|
174
|
+
end)
|
|
175
|
+
end)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Coverage for BoundParentTracker, which exposes the bound class on a child's direct parent.
|
|
4
|
+
|
|
5
|
+
The binder is booted through a ServiceBag; the tracker is a plain BaseObject constructed
|
|
6
|
+
directly. Instances live under a workspace container so parent-change and CollectionService
|
|
7
|
+
signals fire. Parent changes and unbinds are awaited event-driven via the tracker's
|
|
8
|
+
Class.Changed signal rather than a fixed sleep.
|
|
9
|
+
|
|
10
|
+
@class BoundParentTracker.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 BoundParentTracker = require("BoundParentTracker")
|
|
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 = "BoundParentTrackerSpecClass"
|
|
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 = "BoundParentTrackerSpecContainer"
|
|
45
|
+
container.Parent = workspace
|
|
46
|
+
|
|
47
|
+
local instances = {}
|
|
48
|
+
local cleanups = {}
|
|
49
|
+
local booted = false
|
|
50
|
+
|
|
51
|
+
local binder = Binder.new(string.format("BoundParentTrackerSpecTag_%d", suffix), makeClass() :: any)
|
|
52
|
+
|
|
53
|
+
local function newInstance(parent: Instance?): Instance
|
|
54
|
+
local inst = Instance.new("Folder")
|
|
55
|
+
inst.Parent = parent or container
|
|
56
|
+
table.insert(instances, inst)
|
|
57
|
+
return inst
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
local function track(item: any): any
|
|
61
|
+
table.insert(cleanups, item)
|
|
62
|
+
return item
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local function boot()
|
|
66
|
+
assert(not booted, "Already booted")
|
|
67
|
+
booted = true
|
|
68
|
+
|
|
69
|
+
local provider = BinderProvider.new(string.format("BoundParentTrackerSpecProvider_%d", suffix), function(self)
|
|
70
|
+
self:Add(binder)
|
|
71
|
+
end)
|
|
72
|
+
serviceBag:GetService(provider)
|
|
73
|
+
serviceBag:Init()
|
|
74
|
+
serviceBag:Start()
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
-- Blocks until the tracker's value moves off `previous`.
|
|
78
|
+
local function awaitChange(tracker, previous)
|
|
79
|
+
if tracker.Class.Value == previous then
|
|
80
|
+
tracker.Class.Changed:Wait()
|
|
81
|
+
end
|
|
82
|
+
return tracker.Class.Value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
container = container,
|
|
87
|
+
binder = binder,
|
|
88
|
+
newInstance = newInstance,
|
|
89
|
+
track = track,
|
|
90
|
+
boot = boot,
|
|
91
|
+
awaitChange = awaitChange,
|
|
92
|
+
destroy = function()
|
|
93
|
+
for _, item in cleanups do
|
|
94
|
+
pcall(function()
|
|
95
|
+
item:Destroy()
|
|
96
|
+
end)
|
|
97
|
+
end
|
|
98
|
+
serviceBag:Destroy()
|
|
99
|
+
for _, inst in instances do
|
|
100
|
+
pcall(function()
|
|
101
|
+
inst:Destroy()
|
|
102
|
+
end)
|
|
103
|
+
end
|
|
104
|
+
container:Destroy()
|
|
105
|
+
end,
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe("BoundParentTracker.new()", function()
|
|
110
|
+
it("throws without a binder or child", function()
|
|
111
|
+
expect(function()
|
|
112
|
+
BoundParentTracker.new(nil :: any, Instance.new("Folder"))
|
|
113
|
+
end).toThrow()
|
|
114
|
+
end)
|
|
115
|
+
end)
|
|
116
|
+
|
|
117
|
+
describe("BoundParentTracker tracking", function()
|
|
118
|
+
it("exposes the bound class of the direct parent", function()
|
|
119
|
+
local env = setup()
|
|
120
|
+
|
|
121
|
+
local parent = env.newInstance()
|
|
122
|
+
local child = env.newInstance(parent)
|
|
123
|
+
env.binder:Tag(parent)
|
|
124
|
+
env.boot()
|
|
125
|
+
|
|
126
|
+
local tracker = env.track(BoundParentTracker.new(env.binder, child))
|
|
127
|
+
expect(tracker.Class.Value).toEqual(env.binder:Get(parent))
|
|
128
|
+
|
|
129
|
+
env.destroy()
|
|
130
|
+
end)
|
|
131
|
+
|
|
132
|
+
it("clears the value when the child is reparented off the bound parent", function()
|
|
133
|
+
local env = setup()
|
|
134
|
+
|
|
135
|
+
local parent = env.newInstance()
|
|
136
|
+
local child = env.newInstance(parent)
|
|
137
|
+
env.binder:Tag(parent)
|
|
138
|
+
env.boot()
|
|
139
|
+
|
|
140
|
+
local tracker = env.track(BoundParentTracker.new(env.binder, child))
|
|
141
|
+
local class = env.binder:Get(parent)
|
|
142
|
+
expect(tracker.Class.Value).toEqual(class)
|
|
143
|
+
|
|
144
|
+
child.Parent = env.container
|
|
145
|
+
expect(env.awaitChange(tracker, class)).toBeNil()
|
|
146
|
+
|
|
147
|
+
env.destroy()
|
|
148
|
+
end)
|
|
149
|
+
|
|
150
|
+
it("clears the value when the parent's class is unbound", function()
|
|
151
|
+
local env = setup()
|
|
152
|
+
|
|
153
|
+
local parent = env.newInstance()
|
|
154
|
+
local child = env.newInstance(parent)
|
|
155
|
+
env.binder:Tag(parent)
|
|
156
|
+
env.boot()
|
|
157
|
+
|
|
158
|
+
local tracker = env.track(BoundParentTracker.new(env.binder, child))
|
|
159
|
+
local class = env.binder:Get(parent)
|
|
160
|
+
expect(tracker.Class.Value).toEqual(class)
|
|
161
|
+
|
|
162
|
+
env.binder:Untag(parent)
|
|
163
|
+
expect(env.awaitChange(tracker, class)).toBeNil()
|
|
164
|
+
|
|
165
|
+
env.destroy()
|
|
166
|
+
end)
|
|
167
|
+
end)
|