@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.
@@ -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