@quenty/binder 14.39.0 → 14.41.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 +13 -13
- package/src/Shared/Binder.Loading.spec.lua +54 -0
- package/src/Shared/Binder.ObserveBrio.spec.lua +209 -0
- package/src/Shared/Binder.spec.lua +586 -0
- package/src/Shared/BinderGroup.spec.lua +151 -0
- package/src/Shared/BinderGroupProvider.spec.lua +117 -0
- package/src/Shared/BinderTestUtils.lua +163 -0
- package/src/Shared/BinderUtils.spec.lua +338 -0
- package/src/Shared/Collection/BoundChildCollection.spec.lua +203 -0
- package/src/Shared/Promise/promiseBoundClass.spec.lua +129 -0
- package/src/Shared/Trackers/BoundAncestorTracker.spec.lua +167 -0
- package/src/Shared/Trackers/BoundParentTracker.spec.lua +159 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class BinderGroup.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Binder = require("Binder")
|
|
9
|
+
local BinderGroup = require("BinderGroup")
|
|
10
|
+
local Jest = require("Jest")
|
|
11
|
+
|
|
12
|
+
local describe = Jest.Globals.describe
|
|
13
|
+
local expect = Jest.Globals.expect
|
|
14
|
+
local it = Jest.Globals.it
|
|
15
|
+
|
|
16
|
+
local tagCounter = 0
|
|
17
|
+
|
|
18
|
+
local function setup()
|
|
19
|
+
local binders: { any } = {}
|
|
20
|
+
|
|
21
|
+
local function newBinder(): Binder.Binder<any>
|
|
22
|
+
tagCounter += 1
|
|
23
|
+
local binder = Binder.new(string.format("BinderGroupSpecTag_%d", tagCounter), function()
|
|
24
|
+
return {}
|
|
25
|
+
end)
|
|
26
|
+
table.insert(binders, binder)
|
|
27
|
+
return binder
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
newBinder = newBinder,
|
|
32
|
+
destroy = function()
|
|
33
|
+
for _, binder in binders do
|
|
34
|
+
pcall(function()
|
|
35
|
+
binder:Destroy()
|
|
36
|
+
end)
|
|
37
|
+
end
|
|
38
|
+
end,
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe("BinderGroup.new()", function()
|
|
43
|
+
it("constructs with an initial list of binders", function()
|
|
44
|
+
local controller = setup()
|
|
45
|
+
|
|
46
|
+
local binderA = controller.newBinder()
|
|
47
|
+
local binderB = controller.newBinder()
|
|
48
|
+
local group = BinderGroup.new({ binderA, binderB })
|
|
49
|
+
|
|
50
|
+
expect(#group:GetBinders()).toEqual(2)
|
|
51
|
+
|
|
52
|
+
controller.destroy()
|
|
53
|
+
end)
|
|
54
|
+
|
|
55
|
+
it("constructs empty", function()
|
|
56
|
+
local group = BinderGroup.new({})
|
|
57
|
+
expect(#group:GetBinders()).toEqual(0)
|
|
58
|
+
end)
|
|
59
|
+
end)
|
|
60
|
+
|
|
61
|
+
describe("BinderGroup:Add()", function()
|
|
62
|
+
it("adds a binder and exposes it via GetBinders", function()
|
|
63
|
+
local controller = setup()
|
|
64
|
+
|
|
65
|
+
local group = BinderGroup.new({})
|
|
66
|
+
local binder = controller.newBinder()
|
|
67
|
+
group:Add(binder)
|
|
68
|
+
|
|
69
|
+
expect(group:GetBinders()[1]).toEqual(binder)
|
|
70
|
+
|
|
71
|
+
controller.destroy()
|
|
72
|
+
end)
|
|
73
|
+
|
|
74
|
+
it("fires BinderAdded with the added binder", function()
|
|
75
|
+
local controller = setup()
|
|
76
|
+
|
|
77
|
+
local group = BinderGroup.new({})
|
|
78
|
+
local fired
|
|
79
|
+
group.BinderAdded:Connect(function(binder)
|
|
80
|
+
fired = binder
|
|
81
|
+
end)
|
|
82
|
+
|
|
83
|
+
local binder = controller.newBinder()
|
|
84
|
+
group:Add(binder)
|
|
85
|
+
|
|
86
|
+
expect(fired).toEqual(binder)
|
|
87
|
+
|
|
88
|
+
controller.destroy()
|
|
89
|
+
end)
|
|
90
|
+
|
|
91
|
+
it("throws when the value is not a binder", function()
|
|
92
|
+
local group = BinderGroup.new({})
|
|
93
|
+
expect(function()
|
|
94
|
+
group:Add({} :: any)
|
|
95
|
+
end).toThrow()
|
|
96
|
+
end)
|
|
97
|
+
end)
|
|
98
|
+
|
|
99
|
+
describe("BinderGroup constructor validation", function()
|
|
100
|
+
it("accepts binders whose constructor passes validation", function()
|
|
101
|
+
local controller = setup()
|
|
102
|
+
|
|
103
|
+
local validated = {}
|
|
104
|
+
local group = BinderGroup.new({}, function(constructor)
|
|
105
|
+
table.insert(validated, constructor)
|
|
106
|
+
return true
|
|
107
|
+
end)
|
|
108
|
+
|
|
109
|
+
local binder = controller.newBinder()
|
|
110
|
+
group:Add(binder)
|
|
111
|
+
|
|
112
|
+
expect(validated[1]).toEqual(binder:GetConstructor())
|
|
113
|
+
|
|
114
|
+
controller.destroy()
|
|
115
|
+
end)
|
|
116
|
+
|
|
117
|
+
it("throws when the constructor fails validation", function()
|
|
118
|
+
local controller = setup()
|
|
119
|
+
|
|
120
|
+
local group = BinderGroup.new({}, function()
|
|
121
|
+
return false
|
|
122
|
+
end)
|
|
123
|
+
|
|
124
|
+
local binder = controller.newBinder()
|
|
125
|
+
expect(function()
|
|
126
|
+
group:Add(binder)
|
|
127
|
+
end).toThrow()
|
|
128
|
+
|
|
129
|
+
controller.destroy()
|
|
130
|
+
end)
|
|
131
|
+
end)
|
|
132
|
+
|
|
133
|
+
describe("BinderGroup:AddList()", function()
|
|
134
|
+
it("adds each binder in the list", function()
|
|
135
|
+
local controller = setup()
|
|
136
|
+
|
|
137
|
+
local group = BinderGroup.new({})
|
|
138
|
+
group:AddList({ controller.newBinder(), controller.newBinder() })
|
|
139
|
+
|
|
140
|
+
expect(#group:GetBinders()).toEqual(2)
|
|
141
|
+
|
|
142
|
+
controller.destroy()
|
|
143
|
+
end)
|
|
144
|
+
|
|
145
|
+
it("throws on a non-table argument", function()
|
|
146
|
+
local group = BinderGroup.new({})
|
|
147
|
+
expect(function()
|
|
148
|
+
group:AddList(5 :: any)
|
|
149
|
+
end).toThrow()
|
|
150
|
+
end)
|
|
151
|
+
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)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[=[
|
|
3
|
+
Shared harness for the Binder specs. [BinderTestUtils.setup] boots binders the way production code
|
|
4
|
+
boots them: registered on a [BinderProvider] that is Init/Start'd through a [ServiceBag] and torn
|
|
5
|
+
down through `serviceBag:Destroy()`. Adornees are parented into a workspace container because
|
|
6
|
+
CollectionService.GetInstanceAddedSignal only fires for instances that live in the DataModel.
|
|
7
|
+
|
|
8
|
+
Tags are global and the test place is shared across a batch run, so every binder gets a distinct
|
|
9
|
+
tag from a single module-level counter -- shared across every spec file that requires this module,
|
|
10
|
+
so tags never collide between files -- and each controller cleans up after itself via `destroy()`.
|
|
11
|
+
|
|
12
|
+
@class BinderTestUtils
|
|
13
|
+
]=]
|
|
14
|
+
|
|
15
|
+
local require = require(script.Parent.loader).load(script)
|
|
16
|
+
|
|
17
|
+
local Binder = require("Binder")
|
|
18
|
+
local BinderProvider = require("BinderProvider")
|
|
19
|
+
local ServiceBag = require("ServiceBag")
|
|
20
|
+
|
|
21
|
+
local BinderTestUtils = {}
|
|
22
|
+
|
|
23
|
+
local specCounter = 0
|
|
24
|
+
|
|
25
|
+
--[=[
|
|
26
|
+
A minimal bound class that records its instance and whether it was destroyed. It deliberately does
|
|
27
|
+
NOT retain its constructor varargs: the ServiceBag is injected as a constructor arg, and its object
|
|
28
|
+
graph contains Quenty Signals whose strict __index makes jest's deep-equality traversal throw.
|
|
29
|
+
Keeping the class free of them lets toEqual compare instances safely.
|
|
30
|
+
|
|
31
|
+
@return TrackingClass
|
|
32
|
+
]=]
|
|
33
|
+
function BinderTestUtils.makeTrackingClass()
|
|
34
|
+
local Class = {}
|
|
35
|
+
Class.__index = Class
|
|
36
|
+
Class.ClassName = "TrackingClass"
|
|
37
|
+
|
|
38
|
+
function Class.new(inst)
|
|
39
|
+
local self = setmetatable({}, Class)
|
|
40
|
+
self.instance = inst
|
|
41
|
+
self.destroyed = false
|
|
42
|
+
return self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
function Class:Destroy()
|
|
46
|
+
self.destroyed = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
return Class
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
--[=[
|
|
53
|
+
Returns once `inst` is no longer bound. CollectionService tag removal is synchronous here, so the
|
|
54
|
+
class is usually already gone by the time we check; the guarded wait also covers a deferred case.
|
|
55
|
+
|
|
56
|
+
@param binder Binder
|
|
57
|
+
@param inst Instance
|
|
58
|
+
]=]
|
|
59
|
+
function BinderTestUtils.awaitUnbound(binder, inst)
|
|
60
|
+
if binder:Get(inst) ~= nil then
|
|
61
|
+
binder:GetClassRemovedSignal():Wait()
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
--[=[
|
|
66
|
+
A no-op constructor that still returns a value, so Binder's generic bound type stays inhabited.
|
|
67
|
+
|
|
68
|
+
@return table
|
|
69
|
+
]=]
|
|
70
|
+
function BinderTestUtils.noopConstructor()
|
|
71
|
+
return {}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
--[=[
|
|
75
|
+
Builds the controller the Binder specs share. Register binders with `addBinder`, boot them all at
|
|
76
|
+
once through a ServiceBag with `boot`, create adornees with `newInstance`, and tear everything down
|
|
77
|
+
with `destroy` (or just the service bag with `destroyServiceBag`).
|
|
78
|
+
|
|
79
|
+
Fields: `container`.
|
|
80
|
+
Builders: `addBinder(constructor, ...)` -> Binder, `newInstance(parent?, className?)` -> Instance.
|
|
81
|
+
Lifecycle: `boot()`, `destroyServiceBag()`, `destroy()`.
|
|
82
|
+
Helpers: `uniqueTag()` -> string.
|
|
83
|
+
|
|
84
|
+
@return { ... }
|
|
85
|
+
]=]
|
|
86
|
+
function BinderTestUtils.setup()
|
|
87
|
+
specCounter += 1
|
|
88
|
+
local suffix = specCounter
|
|
89
|
+
|
|
90
|
+
local serviceBag = ServiceBag.new()
|
|
91
|
+
local serviceBagDestroyed = false
|
|
92
|
+
|
|
93
|
+
local container = Instance.new("Folder")
|
|
94
|
+
container.Name = "BinderSpecContainer"
|
|
95
|
+
container.Parent = workspace
|
|
96
|
+
|
|
97
|
+
local instances = {}
|
|
98
|
+
local pendingBinders = {}
|
|
99
|
+
local extraTagCounter = 0
|
|
100
|
+
local booted = false
|
|
101
|
+
|
|
102
|
+
local function uniqueTag()
|
|
103
|
+
extraTagCounter += 1
|
|
104
|
+
return string.format("BinderSpecTag_%d_%d", suffix, extraTagCounter)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
local function addBinder(constructor, ...)
|
|
108
|
+
assert(not booted, "Cannot add a binder after boot()")
|
|
109
|
+
local binder = Binder.new(uniqueTag(), constructor, ...)
|
|
110
|
+
table.insert(pendingBinders, binder)
|
|
111
|
+
return binder
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
local function boot()
|
|
115
|
+
assert(not booted, "Already booted")
|
|
116
|
+
booted = true
|
|
117
|
+
|
|
118
|
+
local binders = pendingBinders
|
|
119
|
+
local provider = BinderProvider.new(string.format("BinderSpecProvider_%d", suffix), function(self)
|
|
120
|
+
for _, binder in binders do
|
|
121
|
+
self:Add(binder)
|
|
122
|
+
end
|
|
123
|
+
end)
|
|
124
|
+
|
|
125
|
+
serviceBag:GetService(provider)
|
|
126
|
+
serviceBag:Init()
|
|
127
|
+
serviceBag:Start()
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
local function newInstance(parent, className)
|
|
131
|
+
local inst = Instance.new(className or "Folder")
|
|
132
|
+
inst.Parent = parent or container
|
|
133
|
+
table.insert(instances, inst)
|
|
134
|
+
return inst
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
local function destroyServiceBag()
|
|
138
|
+
if not serviceBagDestroyed then
|
|
139
|
+
serviceBagDestroyed = true
|
|
140
|
+
serviceBag:Destroy()
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
container = container,
|
|
146
|
+
uniqueTag = uniqueTag,
|
|
147
|
+
addBinder = addBinder,
|
|
148
|
+
boot = boot,
|
|
149
|
+
newInstance = newInstance,
|
|
150
|
+
destroyServiceBag = destroyServiceBag,
|
|
151
|
+
destroy = function()
|
|
152
|
+
destroyServiceBag()
|
|
153
|
+
for _, inst in instances do
|
|
154
|
+
pcall(function()
|
|
155
|
+
inst:Destroy()
|
|
156
|
+
end)
|
|
157
|
+
end
|
|
158
|
+
container:Destroy()
|
|
159
|
+
end,
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
return BinderTestUtils
|