@quenty/friendutils 12.32.0 → 12.33.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 +6 -0
- package/deploy.nevermore.json +10 -0
- package/package.json +10 -6
- package/src/Shared/FriendUtils.lua +9 -0
- package/src/Shared/FriendUtils.spec.lua +121 -0
- package/src/Shared/RxFriendUtils.lua +85 -21
- package/src/Shared/RxFriendUtils.spec.lua +133 -0
- package/src/jest.config.lua +3 -0
- package/test/default.project.json +17 -0
- package/test/scripts/Server/ServerMain.server.lua +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [12.33.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/friendutils@12.32.0...@quenty/friendutils@12.33.0) (2026-07-23)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- Add baseline player-mock and support across Nevermore for mocked players. ([567d121](https://github.com/Quenty/NevermoreEngine/commit/567d121ffc014b42391554088189a1a6296dda83))
|
|
11
|
+
|
|
6
12
|
# [12.32.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/friendutils@12.31.0...@quenty/friendutils@12.32.0) (2026-07-18)
|
|
7
13
|
|
|
8
14
|
**Note:** Version bump only for package @quenty/friendutils
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/friendutils",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.33.0",
|
|
4
4
|
"description": "Utlity functions to help find friends of a user. Also contains utility to make testing in studio easier.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -28,14 +28,18 @@
|
|
|
28
28
|
"Quenty"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@quenty/brio": "14.
|
|
31
|
+
"@quenty/brio": "14.33.0",
|
|
32
32
|
"@quenty/loader": "10.11.0",
|
|
33
|
-
"@quenty/maid": "3.
|
|
34
|
-
"@quenty/
|
|
35
|
-
"@quenty/
|
|
33
|
+
"@quenty/maid": "3.11.0",
|
|
34
|
+
"@quenty/nevermore-test-runner": "1.5.0",
|
|
35
|
+
"@quenty/pagesutils": "5.22.0",
|
|
36
|
+
"@quenty/playermock": "1.1.0",
|
|
37
|
+
"@quenty/promise": "10.21.0",
|
|
38
|
+
"@quenty/rx": "13.31.0",
|
|
39
|
+
"@quentystudios/jest-lua": "3.10.0-quenty.2"
|
|
36
40
|
},
|
|
37
41
|
"publishConfig": {
|
|
38
42
|
"access": "public"
|
|
39
43
|
},
|
|
40
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "1de37218a2bedb8e3f8614a2e09bba9eddc812da"
|
|
41
45
|
}
|
|
@@ -8,6 +8,9 @@ local require = require(script.Parent.loader).load(script)
|
|
|
8
8
|
|
|
9
9
|
local Players = game:GetService("Players")
|
|
10
10
|
|
|
11
|
+
local PagesDatabase = require("PagesDatabase")
|
|
12
|
+
local PagesProxy = require("PagesProxy")
|
|
13
|
+
local PlayerMock = require("PlayerMock")
|
|
11
14
|
local Promise = require("Promise")
|
|
12
15
|
|
|
13
16
|
local FriendUtils = {}
|
|
@@ -113,6 +116,12 @@ function FriendUtils.promiseFriendPages(userId: number): Promise.Promise<FriendP
|
|
|
113
116
|
assert(type(userId) == "number", "Bad userId")
|
|
114
117
|
|
|
115
118
|
return Promise.spawn(function(resolve, reject)
|
|
119
|
+
local mockPlayer = PlayerMock.getMockByUserId(userId)
|
|
120
|
+
if mockPlayer ~= nil then
|
|
121
|
+
local friends = PlayerMock.readLookup(mockPlayer, "Players.GetFriendsAsync", 0)
|
|
122
|
+
return resolve(PagesProxy.new(PagesDatabase.fromPageData({ friends })) :: any)
|
|
123
|
+
end
|
|
124
|
+
|
|
116
125
|
local pages
|
|
117
126
|
local ok, err = pcall(function()
|
|
118
127
|
pages = Players:GetFriendsAsync(userId)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class FriendUtils.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Workspace = game:GetService("Workspace")
|
|
9
|
+
|
|
10
|
+
local FriendUtils = require("FriendUtils")
|
|
11
|
+
local Jest = require("Jest")
|
|
12
|
+
local PagesDatabase = require("PagesDatabase")
|
|
13
|
+
local PagesProxy = require("PagesProxy")
|
|
14
|
+
local PlayerMock = require("PlayerMock")
|
|
15
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
16
|
+
|
|
17
|
+
local describe = Jest.Globals.describe
|
|
18
|
+
local expect = Jest.Globals.expect
|
|
19
|
+
local it = Jest.Globals.it
|
|
20
|
+
|
|
21
|
+
local function makeFriendData(userId: number, isOnline: boolean)
|
|
22
|
+
return {
|
|
23
|
+
Id = userId,
|
|
24
|
+
Username = string.format("friend_%d", userId),
|
|
25
|
+
DisplayName = string.format("Friend %d", userId),
|
|
26
|
+
IsOnline = isOnline,
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe("FriendUtils.promiseAllFriends against a mock", function()
|
|
31
|
+
it("resolves the injected friends list", function()
|
|
32
|
+
local player = PlayerMock.new({ UserId = 90041001 })
|
|
33
|
+
player.Parent = Workspace
|
|
34
|
+
PlayerMock.writeLookup(player, "Players.GetFriendsAsync", 0, {
|
|
35
|
+
makeFriendData(90041002, true),
|
|
36
|
+
makeFriendData(90041003, false),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
local outcome, friends = PromiseTestUtils.awaitOutcome(FriendUtils.promiseAllFriends(90041001))
|
|
40
|
+
|
|
41
|
+
expect(outcome).toBe("resolved")
|
|
42
|
+
expect(#friends).toBe(2)
|
|
43
|
+
expect(friends[1].Id).toBe(90041002)
|
|
44
|
+
expect(friends[1].Username).toBe("friend_90041002")
|
|
45
|
+
expect(friends[2].IsOnline).toBe(false)
|
|
46
|
+
|
|
47
|
+
player:Destroy()
|
|
48
|
+
end)
|
|
49
|
+
|
|
50
|
+
it("resolves an empty list for a mock with no injected friends", function()
|
|
51
|
+
local player = PlayerMock.new({ UserId = 90041004 })
|
|
52
|
+
player.Parent = Workspace
|
|
53
|
+
|
|
54
|
+
local outcome, friends = PromiseTestUtils.awaitOutcome(FriendUtils.promiseAllFriends(90041004))
|
|
55
|
+
|
|
56
|
+
expect(outcome).toBe("resolved")
|
|
57
|
+
expect(friends).toEqual({})
|
|
58
|
+
|
|
59
|
+
player:Destroy()
|
|
60
|
+
end)
|
|
61
|
+
|
|
62
|
+
it("stops at limitMaxFriends", function()
|
|
63
|
+
local player = PlayerMock.new({ UserId = 90041005 })
|
|
64
|
+
player.Parent = Workspace
|
|
65
|
+
PlayerMock.writeLookup(player, "Players.GetFriendsAsync", 0, {
|
|
66
|
+
makeFriendData(90041006, true),
|
|
67
|
+
makeFriendData(90041007, true),
|
|
68
|
+
makeFriendData(90041008, true),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
local outcome, friends = PromiseTestUtils.awaitOutcome(FriendUtils.promiseAllFriends(90041005, 2))
|
|
72
|
+
|
|
73
|
+
expect(outcome).toBe("resolved")
|
|
74
|
+
expect(#friends).toBe(2)
|
|
75
|
+
|
|
76
|
+
player:Destroy()
|
|
77
|
+
end)
|
|
78
|
+
end)
|
|
79
|
+
|
|
80
|
+
describe("FriendUtils.iterateFriendsYielding", function()
|
|
81
|
+
it("iterates across page boundaries", function()
|
|
82
|
+
local pages = PagesProxy.new(PagesDatabase.fromPageData({
|
|
83
|
+
{ makeFriendData(90041009, true) },
|
|
84
|
+
{ makeFriendData(90041010, false) },
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
local seen = {}
|
|
88
|
+
for userData in FriendUtils.iterateFriendsYielding(pages :: any) do
|
|
89
|
+
table.insert(seen, userData.Id)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
expect(seen).toEqual({ 90041009, 90041010 })
|
|
93
|
+
end)
|
|
94
|
+
end)
|
|
95
|
+
|
|
96
|
+
describe("FriendUtils.onlineFriends", function()
|
|
97
|
+
it("filters to online friends only", function()
|
|
98
|
+
local friends = {
|
|
99
|
+
makeFriendData(90041011, true),
|
|
100
|
+
makeFriendData(90041012, false),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
local online = FriendUtils.onlineFriends(friends)
|
|
104
|
+
|
|
105
|
+
expect(#online).toBe(1)
|
|
106
|
+
expect(online[1].Id).toBe(90041011)
|
|
107
|
+
end)
|
|
108
|
+
end)
|
|
109
|
+
|
|
110
|
+
describe("FriendUtils.friendsNotInGame", function()
|
|
111
|
+
it("filters out friends whose player is in the game", function()
|
|
112
|
+
-- Mocks are not in Players:GetPlayers(), so all injected friends count as not in game
|
|
113
|
+
local friends = {
|
|
114
|
+
makeFriendData(90041013, true),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
local notInGame = FriendUtils.friendsNotInGame(friends)
|
|
118
|
+
|
|
119
|
+
expect(#notInGame).toBe(1)
|
|
120
|
+
end)
|
|
121
|
+
end)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
local require = require(script.Parent.loader).load(script)
|
|
9
9
|
|
|
10
|
+
local CollectionService = game:GetService("CollectionService")
|
|
10
11
|
local Players = game:GetService("Players")
|
|
11
12
|
local RunService = game:GetService("RunService")
|
|
12
13
|
local StarterGui = game:GetService("StarterGui")
|
|
@@ -14,14 +15,24 @@ local StarterGui = game:GetService("StarterGui")
|
|
|
14
15
|
local Brio = require("Brio")
|
|
15
16
|
local Maid = require("Maid")
|
|
16
17
|
local Observable = require("Observable")
|
|
18
|
+
local PlayerMock = require("PlayerMock")
|
|
17
19
|
|
|
18
20
|
local RxFriendUtils = {}
|
|
19
21
|
|
|
22
|
+
local function getUserId(player: Player): number
|
|
23
|
+
return if PlayerMock.isMock(player) then PlayerMock.read(player, "UserId") else player.UserId
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
--[=[
|
|
21
27
|
Observe friends in the current server (not including the LocalPlayer!), useful for social GUIs.
|
|
22
28
|
The lifetimes exist for the whole duration another player is a friend and in your server.
|
|
23
29
|
This means if a player is unfriended + friended multiple times per session, they will have emitted multiple friend lifetimes.
|
|
24
30
|
|
|
31
|
+
Works against [PlayerMock] players on either side: mocks in the DataModel are enumerated
|
|
32
|
+
alongside real players, friendship resolves from the mock's "Player.IsFriendsWithAsync" lookup
|
|
33
|
+
domain, and a mid-test [PlayerMock.writeLookup] to that domain stands in for the CoreGui
|
|
34
|
+
friended/unfriended events -- the observable re-reads and emits/kills lifetimes accordingly.
|
|
35
|
+
|
|
25
36
|
@param player Player?
|
|
26
37
|
@return Observable<Brio<Player>>
|
|
27
38
|
]=]
|
|
@@ -30,6 +41,8 @@ function RxFriendUtils.observeFriendsInServerAsBrios(player: Player?): Observabl
|
|
|
30
41
|
|
|
31
42
|
assert(typeof(player) == "Instance", "Bad player")
|
|
32
43
|
|
|
44
|
+
local observedPlayer: Player = player :: Player
|
|
45
|
+
|
|
33
46
|
-- Note that 'PlayerFriendedEvent' and 'PlayerUnfriendedEvent' are currently unreliable.
|
|
34
47
|
-- See: https://devforum.roblox.com/t/getcores-playerfriendedevent-and-playerunfriendedevent-bindableevents-firing-at-inappropriate-times/570403/4
|
|
35
48
|
-- Due to players initially starting with an 'unknown' friend value, they fire for all players in the game at launch, and on the first time another player that joins the server.
|
|
@@ -38,6 +51,11 @@ function RxFriendUtils.observeFriendsInServerAsBrios(player: Player?): Observabl
|
|
|
38
51
|
return Observable.new(function(sub)
|
|
39
52
|
local maid = Maid.new()
|
|
40
53
|
|
|
54
|
+
-- Per-otherPlayer wiring (mock friendship-changed connections). Separate from the brio keyed
|
|
55
|
+
-- under maid[otherPlayer]: an unfriend kills the brio but the wiring must survive so a
|
|
56
|
+
-- re-friend emits a new lifetime. Both die together when the other player leaves.
|
|
57
|
+
local listenMaid = maid:Add(Maid.new())
|
|
58
|
+
|
|
41
59
|
local function handleFriendState(otherPlayer: Player, isFriendsWith: boolean)
|
|
42
60
|
if otherPlayer == Players.LocalPlayer then
|
|
43
61
|
return
|
|
@@ -57,45 +75,91 @@ function RxFriendUtils.observeFriendsInServerAsBrios(player: Player?): Observabl
|
|
|
57
75
|
end
|
|
58
76
|
|
|
59
77
|
local function handleNewPlayerAsync(otherPlayer: Player)
|
|
60
|
-
if otherPlayer
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
isFriendsWith = player:IsFriendsWithAsync(otherPlayer.UserId)
|
|
64
|
-
end)
|
|
65
|
-
if not ok then
|
|
66
|
-
warn(
|
|
67
|
-
string.format(
|
|
68
|
-
"[RxFriendUtils.observeFriendsInServerAsBrios] Couldn't get friendship status with %q!",
|
|
69
|
-
otherPlayer.Name
|
|
70
|
-
)
|
|
71
|
-
)
|
|
78
|
+
if otherPlayer == observedPlayer then
|
|
79
|
+
return
|
|
80
|
+
end
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
-- Friendship is symmetric, so with only one side mocked the injected state lives on
|
|
83
|
+
-- whichever player is the mock; with both mocked, on the observed player.
|
|
84
|
+
local lookupMock: Player? = nil
|
|
85
|
+
local lookupKey: number? = nil
|
|
86
|
+
if PlayerMock.isMock(observedPlayer) then
|
|
87
|
+
lookupMock = observedPlayer
|
|
88
|
+
lookupKey = getUserId(otherPlayer)
|
|
89
|
+
elseif PlayerMock.isMock(otherPlayer) then
|
|
90
|
+
lookupMock = otherPlayer
|
|
91
|
+
lookupKey = getUserId(observedPlayer)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if lookupMock ~= nil and lookupKey ~= nil then
|
|
95
|
+
local mock: Player, key: number = lookupMock, lookupKey
|
|
96
|
+
|
|
97
|
+
local function readFriendState()
|
|
98
|
+
handleFriendState(otherPlayer, PlayerMock.readLookup(mock, "Player.IsFriendsWithAsync", key))
|
|
77
99
|
end
|
|
78
100
|
|
|
79
|
-
|
|
101
|
+
-- The lookup attribute's changed signal stands in for the CoreGui friended /
|
|
102
|
+
-- unfriended events below, which only the engine can fire.
|
|
103
|
+
listenMaid[otherPlayer] = PlayerMock.getLookupChangedSignal(mock, "Player.IsFriendsWithAsync", key)
|
|
104
|
+
:Connect(readFriendState)
|
|
105
|
+
|
|
106
|
+
readFriendState()
|
|
107
|
+
return
|
|
80
108
|
end
|
|
109
|
+
|
|
110
|
+
local isFriendsWith = false
|
|
111
|
+
local ok = pcall(function()
|
|
112
|
+
isFriendsWith = observedPlayer:IsFriendsWithAsync(getUserId(otherPlayer))
|
|
113
|
+
end)
|
|
114
|
+
if not ok then
|
|
115
|
+
warn(
|
|
116
|
+
string.format(
|
|
117
|
+
"[RxFriendUtils.observeFriendsInServerAsBrios] Couldn't get friendship status with %q!",
|
|
118
|
+
otherPlayer.Name
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
-- If the call failed, then 'isFriendsWith' will be nil.
|
|
123
|
+
-- We'll assume that this player isn't a friend on failure.
|
|
124
|
+
handleFriendState(otherPlayer, false)
|
|
125
|
+
return
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
handleFriendState(otherPlayer, isFriendsWith)
|
|
81
129
|
end
|
|
82
130
|
|
|
83
|
-
|
|
84
|
-
|
|
131
|
+
local function handlePlayerRemoving(otherPlayer: Player)
|
|
132
|
+
listenMaid[otherPlayer] = nil
|
|
85
133
|
maid[otherPlayer] = nil
|
|
86
|
-
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
-- Handle players leaving / joining.
|
|
137
|
+
maid:GiveTask(Players.PlayerRemoving:Connect(handlePlayerRemoving))
|
|
87
138
|
maid:GiveTask(Players.PlayerAdded:Connect(handleNewPlayerAsync))
|
|
88
139
|
|
|
140
|
+
-- Mocks never appear in Players:GetPlayers()/PlayerAdded; enumerate them through their tag,
|
|
141
|
+
-- mirroring the DataModel-scoped engine calls (see [PlayerMock.TAG]).
|
|
142
|
+
maid:GiveTask(CollectionService:GetInstanceAddedSignal(PlayerMock.TAG):Connect(function(instance)
|
|
143
|
+
handleNewPlayerAsync((instance :: any) :: Player)
|
|
144
|
+
end))
|
|
145
|
+
maid:GiveTask(CollectionService:GetInstanceRemovedSignal(PlayerMock.TAG):Connect(function(instance)
|
|
146
|
+
handlePlayerRemoving((instance :: any) :: Player)
|
|
147
|
+
end))
|
|
148
|
+
|
|
89
149
|
for _, otherPlayer in Players:GetPlayers() do
|
|
90
150
|
task.spawn(handleNewPlayerAsync, otherPlayer)
|
|
91
151
|
end
|
|
92
152
|
|
|
153
|
+
for _, tagged in CollectionService:GetTagged(PlayerMock.TAG) do
|
|
154
|
+
task.spawn(handleNewPlayerAsync, (tagged :: any) :: Player)
|
|
155
|
+
end
|
|
156
|
+
|
|
93
157
|
-- Handle changes for players already in this server.
|
|
94
158
|
-- There's a non-zero chance these get removed someday... :(
|
|
95
159
|
-- https://devforum.roblox.com/t/playerfriendedevent-was-deleted-from-corescripts/696683
|
|
96
160
|
-- So just incase these connections throw, use a new thread so we don't error out the whole observable.
|
|
97
161
|
-- Only allow this while the game is running too
|
|
98
|
-
if
|
|
162
|
+
if observedPlayer == Players.LocalPlayer and RunService:IsRunning() then
|
|
99
163
|
task.spawn(function()
|
|
100
164
|
maid:GiveTask(StarterGui:GetCore("PlayerFriendedEvent").Event:Connect(function(otherPlayer: Player)
|
|
101
165
|
handleFriendState(otherPlayer, true)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class RxFriendUtils.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Workspace = game:GetService("Workspace")
|
|
9
|
+
|
|
10
|
+
local Brio = require("Brio")
|
|
11
|
+
local Jest = require("Jest")
|
|
12
|
+
local Maid = require("Maid")
|
|
13
|
+
local PlayerMock = require("PlayerMock")
|
|
14
|
+
local RxFriendUtils = require("RxFriendUtils")
|
|
15
|
+
|
|
16
|
+
local afterEach = Jest.Globals.afterEach
|
|
17
|
+
local describe = Jest.Globals.describe
|
|
18
|
+
local expect = Jest.Globals.expect
|
|
19
|
+
local it = Jest.Globals.it
|
|
20
|
+
|
|
21
|
+
describe("RxFriendUtils.observeFriendsInServerAsBrios", function()
|
|
22
|
+
local maid = Maid.new()
|
|
23
|
+
|
|
24
|
+
afterEach(function()
|
|
25
|
+
maid:DoCleaning()
|
|
26
|
+
end)
|
|
27
|
+
|
|
28
|
+
local function makeMock(userId: number): Player
|
|
29
|
+
local player = PlayerMock.new({ UserId = userId })
|
|
30
|
+
player.Parent = Workspace
|
|
31
|
+
maid:GiveTask(player)
|
|
32
|
+
return player
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
local function trackBrios(observer: Player): { Brio.Brio<Player> }
|
|
36
|
+
local emissions: { Brio.Brio<Player> } = {}
|
|
37
|
+
maid:GiveTask(RxFriendUtils.observeFriendsInServerAsBrios(observer):Subscribe(function(brio)
|
|
38
|
+
table.insert(emissions, brio)
|
|
39
|
+
end))
|
|
40
|
+
return emissions
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it("emits an initial lifetime for a mock that is already a friend", function()
|
|
44
|
+
local observer = makeMock(90051001)
|
|
45
|
+
local friend = makeMock(90051002)
|
|
46
|
+
PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051002, true)
|
|
47
|
+
|
|
48
|
+
local emissions = trackBrios(observer)
|
|
49
|
+
|
|
50
|
+
expect(#emissions).toBe(1)
|
|
51
|
+
expect(emissions[1]:GetValue()).toBe(friend)
|
|
52
|
+
expect(emissions[1]:IsDead()).toBe(false)
|
|
53
|
+
end)
|
|
54
|
+
|
|
55
|
+
it("emits nothing when no mock is a friend", function()
|
|
56
|
+
local observer = makeMock(90051003)
|
|
57
|
+
makeMock(90051004)
|
|
58
|
+
|
|
59
|
+
local emissions = trackBrios(observer)
|
|
60
|
+
|
|
61
|
+
expect(#emissions).toBe(0)
|
|
62
|
+
end)
|
|
63
|
+
|
|
64
|
+
it("emits when friendship is written mid-test and kills the lifetime on unfriend", function()
|
|
65
|
+
local observer = makeMock(90051005)
|
|
66
|
+
local friend = makeMock(90051006)
|
|
67
|
+
|
|
68
|
+
local emissions = trackBrios(observer)
|
|
69
|
+
expect(#emissions).toBe(0)
|
|
70
|
+
|
|
71
|
+
PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051006, true)
|
|
72
|
+
expect(#emissions).toBe(1)
|
|
73
|
+
expect(emissions[1]:GetValue()).toBe(friend)
|
|
74
|
+
|
|
75
|
+
PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051006, false)
|
|
76
|
+
expect(emissions[1]:IsDead()).toBe(true)
|
|
77
|
+
end)
|
|
78
|
+
|
|
79
|
+
it("emits a fresh lifetime per re-friend", function()
|
|
80
|
+
local observer = makeMock(90051007)
|
|
81
|
+
makeMock(90051008)
|
|
82
|
+
|
|
83
|
+
local emissions = trackBrios(observer)
|
|
84
|
+
|
|
85
|
+
PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051008, true)
|
|
86
|
+
PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051008, false)
|
|
87
|
+
PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051008, true)
|
|
88
|
+
|
|
89
|
+
expect(#emissions).toBe(2)
|
|
90
|
+
expect(emissions[1]:IsDead()).toBe(true)
|
|
91
|
+
expect(emissions[2]:IsDead()).toBe(false)
|
|
92
|
+
end)
|
|
93
|
+
|
|
94
|
+
it("does not emit a duplicate lifetime for a repeated friended signal", function()
|
|
95
|
+
local observer = makeMock(90051009)
|
|
96
|
+
makeMock(90051010)
|
|
97
|
+
PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051010, true)
|
|
98
|
+
|
|
99
|
+
local emissions = trackBrios(observer)
|
|
100
|
+
|
|
101
|
+
-- The CoreGui friended event this wiring stands in for can fire repeatedly; a re-write of
|
|
102
|
+
-- the same state must not open a second lifetime.
|
|
103
|
+
PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051010, true)
|
|
104
|
+
|
|
105
|
+
expect(#emissions).toBe(1)
|
|
106
|
+
end)
|
|
107
|
+
|
|
108
|
+
it("emits for a friend mock that joins after subscription", function()
|
|
109
|
+
local observer = makeMock(90051011)
|
|
110
|
+
PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051012, true)
|
|
111
|
+
|
|
112
|
+
local emissions = trackBrios(observer)
|
|
113
|
+
expect(#emissions).toBe(0)
|
|
114
|
+
|
|
115
|
+
local friend = makeMock(90051012)
|
|
116
|
+
|
|
117
|
+
expect(#emissions).toBe(1)
|
|
118
|
+
expect(emissions[1]:GetValue()).toBe(friend)
|
|
119
|
+
end)
|
|
120
|
+
|
|
121
|
+
it("kills the lifetime when the friend mock leaves", function()
|
|
122
|
+
local observer = makeMock(90051013)
|
|
123
|
+
local friend = makeMock(90051014)
|
|
124
|
+
PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051014, true)
|
|
125
|
+
|
|
126
|
+
local emissions = trackBrios(observer)
|
|
127
|
+
expect(#emissions).toBe(1)
|
|
128
|
+
|
|
129
|
+
friend:Destroy()
|
|
130
|
+
|
|
131
|
+
expect(emissions[1]:IsDead()).toBe(true)
|
|
132
|
+
end)
|
|
133
|
+
end)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "FriendUtilsTest",
|
|
3
|
+
"tree": {
|
|
4
|
+
"$className": "DataModel",
|
|
5
|
+
"ServerScriptService": {
|
|
6
|
+
"$properties": {
|
|
7
|
+
"LoadStringEnabled": true
|
|
8
|
+
},
|
|
9
|
+
"friendutils": {
|
|
10
|
+
"$path": ".."
|
|
11
|
+
},
|
|
12
|
+
"Script": {
|
|
13
|
+
"$path": "scripts/Server"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
local ServerScriptService = game:GetService("ServerScriptService")
|
|
3
|
+
|
|
4
|
+
local root = ServerScriptService.friendutils
|
|
5
|
+
local loader = root:FindFirstChild("LoaderUtils", true).Parent
|
|
6
|
+
local require = require(loader).bootstrapGame(root)
|
|
7
|
+
|
|
8
|
+
local NevermoreTestRunnerUtils = require("NevermoreTestRunnerUtils")
|
|
9
|
+
|
|
10
|
+
if NevermoreTestRunnerUtils.runTestsIfNeededAsync(root) then
|
|
11
|
+
return
|
|
12
|
+
end
|