@quenty/friendutils 12.40.0 → 12.42.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 CHANGED
@@ -3,6 +3,14 @@
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.42.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/friendutils@12.41.0...@quenty/friendutils@12.42.0) (2026-09-02)
7
+
8
+ **Note:** Version bump only for package @quenty/friendutils
9
+
10
+ # [12.41.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/friendutils@12.40.0...@quenty/friendutils@12.41.0) (2026-08-28)
11
+
12
+ **Note:** Version bump only for package @quenty/friendutils
13
+
6
14
  # [12.40.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/friendutils@12.39.0...@quenty/friendutils@12.40.0) (2026-08-28)
7
15
 
8
16
  **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.40.0",
3
+ "version": "12.42.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,13 +28,13 @@
28
28
  "Quenty"
29
29
  ],
30
30
  "dependencies": {
31
- "@quenty/brio": "14.36.0",
32
- "@quenty/coreguiutils": "10.25.0",
31
+ "@quenty/brio": "14.37.0",
32
+ "@quenty/coreguiutils": "10.26.0",
33
33
  "@quenty/loader": "10.11.1",
34
34
  "@quenty/maid": "3.12.0",
35
35
  "@quenty/nevermore-test-runner": "1.6.0",
36
36
  "@quenty/pagesutils": "5.25.0",
37
- "@quenty/playermock": "1.7.0",
37
+ "@quenty/playermock": "1.9.0",
38
38
  "@quenty/promise": "10.24.0",
39
39
  "@quenty/rx": "13.34.0",
40
40
  "@quentystudios/jest-lua": "3.10.0-quenty.2"
@@ -42,5 +42,5 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "gitHead": "f75f93d5b2ac51cf7da3b7599788dcce31fd8a29"
45
+ "gitHead": "278f4ca25486a8ba0601704b27a11da8c77499c9"
46
46
  }
@@ -118,7 +118,7 @@ function FriendUtils.promiseFriendPages(userId: number): Promise.Promise<FriendP
118
118
  return Promise.spawn(function(resolve, reject)
119
119
  local mockPlayer = PlayerMock.getMockByUserId(userId)
120
120
  if mockPlayer ~= nil then
121
- local friends = PlayerMock.readLookup(mockPlayer, "Players.GetFriendsAsync", 0)
121
+ local friends = PlayerMock.callMethod(mockPlayer, "Players.GetFriendsAsync", 0)
122
122
  return resolve(PagesProxy.new(PagesDatabase.fromPageData({ friends })) :: any)
123
123
  end
124
124
 
@@ -31,10 +31,10 @@ describe("FriendUtils.promiseAllFriends against a mock", function()
31
31
  it("resolves the injected friends list", function()
32
32
  local player = PlayerMock.new({ UserId = 90041001 })
33
33
  player.Parent = Workspace
34
- PlayerMock.writeLookup(player, "Players.GetFriendsAsync", 0, {
34
+ PlayerMock.writeLookup(player, "Players.GetFriendsAsync", {
35
35
  makeFriendData(90041002, true),
36
36
  makeFriendData(90041003, false),
37
- })
37
+ }, 0)
38
38
 
39
39
  local outcome, friends = PromiseTestUtils.awaitOutcome(FriendUtils.promiseAllFriends(90041001))
40
40
 
@@ -62,11 +62,11 @@ describe("FriendUtils.promiseAllFriends against a mock", function()
62
62
  it("stops at limitMaxFriends", function()
63
63
  local player = PlayerMock.new({ UserId = 90041005 })
64
64
  player.Parent = Workspace
65
- PlayerMock.writeLookup(player, "Players.GetFriendsAsync", 0, {
65
+ PlayerMock.writeLookup(player, "Players.GetFriendsAsync", {
66
66
  makeFriendData(90041006, true),
67
67
  makeFriendData(90041007, true),
68
68
  makeFriendData(90041008, true),
69
- })
69
+ }, 0)
70
70
 
71
71
  local outcome, friends = PromiseTestUtils.awaitOutcome(FriendUtils.promiseAllFriends(90041005, 2))
72
72
 
@@ -26,15 +26,29 @@ local function getUserId(player: Player): number
26
26
  return if PlayerMock.isMock(player) then PlayerMock.read(player, "UserId") else player.UserId
27
27
  end
28
28
 
29
+ local function readFriendship(observedPlayer: Player, otherPlayer: Player): boolean
30
+ if PlayerMock.isMock(observedPlayer) then
31
+ return PlayerMock.callMethod(observedPlayer, "Player.IsFriendsWithAsync", getUserId(otherPlayer))
32
+ elseif PlayerMock.isMock(otherPlayer) then
33
+ return PlayerMock.callMethod(otherPlayer, "Player.IsFriendsWithAsync", getUserId(observedPlayer))
34
+ end
35
+
36
+ return observedPlayer:IsFriendsWithAsync(getUserId(otherPlayer))
37
+ end
38
+
29
39
  --[=[
30
40
  Observe friends in the current server (not including the LocalPlayer!), useful for social GUIs.
31
41
  The lifetimes exist for the whole duration another player is a friend and in your server.
32
42
  This means if a player is unfriended + friended multiple times per session, they will have emitted multiple friend lifetimes.
33
43
 
34
- Works against [PlayerMock] players on either side: mocks in the DataModel are enumerated
35
- alongside real players, friendship resolves from the mock's "Player.IsFriendsWithAsync" lookup
36
- domain, and a mid-test [PlayerMock.writeLookup] to that domain stands in for the CoreGui
37
- friended/unfriended events -- the observable re-reads and emits/kills lifetimes accordingly.
44
+ Works against [PlayerMock] players on either side: mocks in the DataModel are enumerated alongside
45
+ real players, and friendship resolves through the mock's `"Player.IsFriendsWithAsync"` domain. A
46
+ mock local player answers `StarterGui:GetCore` with its own stand-in for the CoreGui events below,
47
+ so a test fires a friending the way the CoreGui does:
48
+
49
+ ```lua
50
+ PlayerMock.callMethod(player, "StarterGui.GetCore", "PlayerFriendedEvent"):Fire(otherPlayer)
51
+ ```
38
52
 
39
53
  @param player Player?
40
54
  @return Observable<Brio<Player>>
@@ -54,11 +68,6 @@ function RxFriendUtils.observeFriendsInServerAsBrios(player: Player?): Observabl
54
68
  return Observable.new(function(sub)
55
69
  local maid = Maid.new()
56
70
 
57
- -- Per-otherPlayer wiring (mock friendship-changed connections). Separate from the brio keyed
58
- -- under maid[otherPlayer]: an unfriend kills the brio but the wiring must survive so a
59
- -- re-friend emits a new lifetime. Both die together when the other player leaves.
60
- local listenMaid = maid:Add(Maid.new())
61
-
62
71
  local function handleFriendState(otherPlayer: Player, isFriendsWith: boolean)
63
72
  if otherPlayer == Players.LocalPlayer then
64
73
  return
@@ -82,37 +91,9 @@ function RxFriendUtils.observeFriendsInServerAsBrios(player: Player?): Observabl
82
91
  return
83
92
  end
84
93
 
85
- -- Friendship is symmetric, so with only one side mocked the injected state lives on
86
- -- whichever player is the mock; with both mocked, on the observed player.
87
- local lookupMock: Player? = nil
88
- local lookupKey: number? = nil
89
- if PlayerMock.isMock(observedPlayer) then
90
- lookupMock = observedPlayer
91
- lookupKey = getUserId(otherPlayer)
92
- elseif PlayerMock.isMock(otherPlayer) then
93
- lookupMock = otherPlayer
94
- lookupKey = getUserId(observedPlayer)
95
- end
96
-
97
- if lookupMock ~= nil and lookupKey ~= nil then
98
- local mock: Player, key: number = lookupMock, lookupKey
99
-
100
- local function readFriendState()
101
- handleFriendState(otherPlayer, PlayerMock.readLookup(mock, "Player.IsFriendsWithAsync", key))
102
- end
103
-
104
- -- The lookup attribute's changed signal stands in for the CoreGui friended /
105
- -- unfriended events below, which only the engine can fire.
106
- listenMaid[otherPlayer] = PlayerMock.getLookupChangedSignal(mock, "Player.IsFriendsWithAsync", key)
107
- :Connect(readFriendState)
108
-
109
- readFriendState()
110
- return
111
- end
112
-
113
94
  local isFriendsWith = false
114
95
  local ok = pcall(function()
115
- isFriendsWith = observedPlayer:IsFriendsWithAsync(getUserId(otherPlayer))
96
+ isFriendsWith = readFriendship(observedPlayer, otherPlayer)
116
97
  end)
117
98
  if not ok then
118
99
  warn(
@@ -132,7 +113,6 @@ function RxFriendUtils.observeFriendsInServerAsBrios(player: Player?): Observabl
132
113
  end
133
114
 
134
115
  local function handlePlayerRemoving(otherPlayer: Player)
135
- listenMaid[otherPlayer] = nil
136
116
  maid[otherPlayer] = nil
137
117
  end
138
118
 
@@ -162,7 +142,8 @@ function RxFriendUtils.observeFriendsInServerAsBrios(player: Player?): Observabl
162
142
  -- https://devforum.roblox.com/t/playerfriendedevent-was-deleted-from-corescripts/696683
163
143
  -- So just incase these connections throw, retrieve them off-thread so we don't error out the whole observable.
164
144
  -- Only allow this while the game is running too
165
- if observedPlayer == Players.LocalPlayer and RunService:IsRunning() then
145
+ local localPlayer = Players.LocalPlayer or PlayerMock.getMockedLocalPlayer()
146
+ if observedPlayer == localPlayer and (RunService:IsRunning() or PlayerMock.isMock(observedPlayer)) then
166
147
  -- Getting the core yields, so the subscription may be cleaned up before it resolves.
167
148
  local cancelled = false
168
149
  maid:GiveTask(function()
@@ -32,6 +32,17 @@ describe("RxFriendUtils.observeFriendsInServerAsBrios", function()
32
32
  return player
33
33
  end
34
34
 
35
+ local function makeLocalMock(userId: number): Player
36
+ local player = makeMock(userId)
37
+ maid:GiveTask(PlayerMock.setMockedLocalPlayer(player))
38
+ return player
39
+ end
40
+
41
+ local function fireCoreFriendship(observer: Player, otherPlayer: Player, isFriend: boolean): ()
42
+ local coreName = if isFriend then "PlayerFriendedEvent" else "PlayerUnfriendedEvent"
43
+ PlayerMock.callMethod(observer, "StarterGui.GetCore", coreName):Fire(otherPlayer)
44
+ end
45
+
35
46
  local function trackBrios(observer: Player): { Brio.Brio<Player> }
36
47
  local emissions: { Brio.Brio<Player> } = {}
37
48
  maid:GiveTask(RxFriendUtils.observeFriendsInServerAsBrios(observer):Subscribe(function(brio)
@@ -43,7 +54,7 @@ describe("RxFriendUtils.observeFriendsInServerAsBrios", function()
43
54
  it("emits an initial lifetime for a mock that is already a friend", function()
44
55
  local observer = makeMock(90051001)
45
56
  local friend = makeMock(90051002)
46
- PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051002, true)
57
+ PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", true, 90051002)
47
58
 
48
59
  local emissions = trackBrios(observer)
49
60
 
@@ -61,30 +72,30 @@ describe("RxFriendUtils.observeFriendsInServerAsBrios", function()
61
72
  expect(#emissions).toBe(0)
62
73
  end)
63
74
 
64
- it("emits when friendship is written mid-test and kills the lifetime on unfriend", function()
65
- local observer = makeMock(90051005)
75
+ it("emits when the CoreGui reports a friending and kills the lifetime on unfriend", function()
76
+ local observer = makeLocalMock(90051005)
66
77
  local friend = makeMock(90051006)
67
78
 
68
79
  local emissions = trackBrios(observer)
69
80
  expect(#emissions).toBe(0)
70
81
 
71
- PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051006, true)
82
+ fireCoreFriendship(observer, friend, true)
72
83
  expect(#emissions).toBe(1)
73
84
  expect(emissions[1]:GetValue()).toBe(friend)
74
85
 
75
- PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051006, false)
86
+ fireCoreFriendship(observer, friend, false)
76
87
  expect(emissions[1]:IsDead()).toBe(true)
77
88
  end)
78
89
 
79
90
  it("emits a fresh lifetime per re-friend", function()
80
- local observer = makeMock(90051007)
81
- makeMock(90051008)
91
+ local observer = makeLocalMock(90051007)
92
+ local friend = makeMock(90051008)
82
93
 
83
94
  local emissions = trackBrios(observer)
84
95
 
85
- PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051008, true)
86
- PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051008, false)
87
- PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051008, true)
96
+ fireCoreFriendship(observer, friend, true)
97
+ fireCoreFriendship(observer, friend, false)
98
+ fireCoreFriendship(observer, friend, true)
88
99
 
89
100
  expect(#emissions).toBe(2)
90
101
  expect(emissions[1]:IsDead()).toBe(true)
@@ -92,22 +103,20 @@ describe("RxFriendUtils.observeFriendsInServerAsBrios", function()
92
103
  end)
93
104
 
94
105
  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)
106
+ local observer = makeLocalMock(90051009)
107
+ local friend = makeMock(90051010)
108
+ PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", true, 90051010)
98
109
 
99
110
  local emissions = trackBrios(observer)
100
111
 
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)
112
+ fireCoreFriendship(observer, friend, true)
104
113
 
105
114
  expect(#emissions).toBe(1)
106
115
  end)
107
116
 
108
117
  it("emits for a friend mock that joins after subscription", function()
109
118
  local observer = makeMock(90051011)
110
- PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051012, true)
119
+ PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", true, 90051012)
111
120
 
112
121
  local emissions = trackBrios(observer)
113
122
  expect(#emissions).toBe(0)
@@ -121,7 +130,7 @@ describe("RxFriendUtils.observeFriendsInServerAsBrios", function()
121
130
  it("kills the lifetime when the friend mock leaves", function()
122
131
  local observer = makeMock(90051013)
123
132
  local friend = makeMock(90051014)
124
- PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", 90051014, true)
133
+ PlayerMock.writeLookup(observer, "Player.IsFriendsWithAsync", true, 90051014)
125
134
 
126
135
  local emissions = trackBrios(observer)
127
136
  expect(#emissions).toBe(1)