Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions shared/chat/conversation/info-panel/add-to-channel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,57 @@
/// <reference types="jest" />
import * as T from '@/constants/types'
import {addMembersToChannel} from './add-to-channel'
import {installFakeEngine, type FakeEngine} from '@/test/fake-engine'

const conversationIDKey = T.Chat.conversationIDToKey(new Uint8Array([1, 2, 3, 4]))
const convID = T.Chat.keyToConversationID(conversationIDKey)

let engine: FakeEngine
let onBulkAdd: () => void = () => {}
let onRefresh: () => void = () => {}

beforeEach(() => {
onBulkAdd = () => {}
onRefresh = () => {}
engine = installFakeEngine({
'chat.1.local.bulkAddToConv': () => {
onBulkAdd()
},
'chat.1.local.refreshParticipants': () => {
onRefresh()
},
})
})

afterEach(() => {
engine.uninstall()
jest.restoreAllMocks()
})

test('adding members refreshes the conversation participants', async () => {
jest.spyOn(T.RPCChat, 'localBulkAddToConvRpcPromise').mockResolvedValue(undefined)
jest.spyOn(T.RPCChat, 'localRefreshParticipantsRpcPromise').mockResolvedValue(undefined)

await addMembersToChannel(conversationIDKey, ['testuser', 'testuser-mac'])

expect(T.RPCChat.localBulkAddToConvRpcPromise).toHaveBeenCalledWith({
convID,
usernames: ['testuser', 'testuser-mac'],
})
expect(T.RPCChat.localRefreshParticipantsRpcPromise).toHaveBeenCalledWith({convID})
expect(engine.calls('chat.1.local.bulkAddToConv')).toEqual([
{method: 'chat.1.local.bulkAddToConv', params: {convID, usernames: ['testuser', 'testuser-mac']}},
])
expect(engine.calls('chat.1.local.refreshParticipants')).toEqual([
{method: 'chat.1.local.refreshParticipants', params: {convID}},
])
})

test('a failed add never claims the participants are fresh', async () => {
jest.spyOn(T.RPCChat, 'localBulkAddToConvRpcPromise').mockRejectedValue(new Error('nope'))
jest.spyOn(T.RPCChat, 'localRefreshParticipantsRpcPromise').mockResolvedValue(undefined)
onBulkAdd = () => {
throw new Error('nope')
}

await expect(addMembersToChannel(conversationIDKey, ['testuser'])).rejects.toThrow('nope')
expect(T.RPCChat.localRefreshParticipantsRpcPromise).not.toHaveBeenCalled()
expect(engine.callCount('chat.1.local.refreshParticipants')).toBe(0)
})

test('a failed refresh does not fail the add', async () => {
jest.spyOn(T.RPCChat, 'localBulkAddToConvRpcPromise').mockResolvedValue(undefined)
jest.spyOn(T.RPCChat, 'localRefreshParticipantsRpcPromise').mockRejectedValue(new Error('offline'))
onRefresh = () => {
throw new Error('offline')
}

await expect(addMembersToChannel(conversationIDKey, ['testuser'])).resolves.toBeUndefined()
})
38 changes: 25 additions & 13 deletions shared/chat/conversation/messages/reset-user.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,45 @@
/// <reference types="jest" />
import * as T from '@/constants/types'
import {addTeamMemberAfterReset} from './reset-user'
import {installFakeEngine, type FakeEngine} from '@/test/fake-engine'

const conversationIDKey = T.Chat.conversationIDToKey(new Uint8Array([1, 2, 3, 4]))
const convID = T.Chat.keyToConversationID(conversationIDKey)

let engine: FakeEngine
let onAdd: () => void = () => {}

beforeEach(() => {
onAdd = () => {}
engine = installFakeEngine({
'chat.1.local.addTeamMemberAfterReset': () => {
onAdd()
},
'chat.1.local.refreshParticipants': () => {},
})
})

afterEach(() => {
engine.uninstall()
jest.restoreAllMocks()
})

test('letting a reset user back in refreshes the conversation participants', async () => {
jest.spyOn(T.RPCChat, 'localAddTeamMemberAfterResetRpcPromise').mockResolvedValue(undefined)
jest.spyOn(T.RPCChat, 'localRefreshParticipantsRpcPromise').mockResolvedValue(undefined)

await addTeamMemberAfterReset(conversationIDKey, 'testuser')

expect(T.RPCChat.localAddTeamMemberAfterResetRpcPromise).toHaveBeenCalledWith({
convID,
username: 'testuser',
})
expect(T.RPCChat.localRefreshParticipantsRpcPromise).toHaveBeenCalledWith({convID})
expect(engine.calls('chat.1.local.addTeamMemberAfterReset')).toEqual([
{method: 'chat.1.local.addTeamMemberAfterReset', params: {convID, username: 'testuser'}},
])
expect(engine.calls('chat.1.local.refreshParticipants')).toEqual([
{method: 'chat.1.local.refreshParticipants', params: {convID}},
])
})

test('a failed re-add never claims the participants are fresh', async () => {
jest
.spyOn(T.RPCChat, 'localAddTeamMemberAfterResetRpcPromise')
.mockRejectedValue(new Error('still reset'))
jest.spyOn(T.RPCChat, 'localRefreshParticipantsRpcPromise').mockResolvedValue(undefined)
onAdd = () => {
throw new Error('still reset')
}

await expect(addTeamMemberAfterReset(conversationIDKey, 'testuser')).rejects.toThrow('still reset')
expect(T.RPCChat.localRefreshParticipantsRpcPromise).not.toHaveBeenCalled()
expect(engine.callCount('chat.1.local.refreshParticipants')).toBe(0)
})
36 changes: 26 additions & 10 deletions shared/chat/conversation/status-actions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/// <reference types="jest" />
import * as T from '@/constants/types'
import {joinConversation} from './status-actions'
import {installFakeEngine, type FakeEngine} from '@/test/fake-engine'

const conversationIDKey = T.Chat.conversationIDToKey(new Uint8Array([1, 2, 3, 4]))
const convID = T.Chat.keyToConversationID(conversationIDKey)
Expand All @@ -12,29 +13,44 @@ const flushPromises = async () => {
}
}

let engine: FakeEngine
let onJoin: () => void = () => {}

beforeEach(() => {
onJoin = () => {}
engine = installFakeEngine({
'chat.1.local.joinConversationByIDLocal': () => {
onJoin()
return {} as never
},
'chat.1.local.refreshParticipants': () => {},
})
})

afterEach(() => {
engine.uninstall()
jest.restoreAllMocks()
})

test('joining a conversation refreshes its participants', async () => {
jest.spyOn(T.RPCChat, 'localJoinConversationByIDLocalRpcPromise').mockResolvedValue({} as never)
jest.spyOn(T.RPCChat, 'localRefreshParticipantsRpcPromise').mockResolvedValue(undefined)

joinConversation(conversationIDKey)
await flushPromises()

expect(T.RPCChat.localJoinConversationByIDLocalRpcPromise).toHaveBeenCalledWith({convID})
expect(T.RPCChat.localRefreshParticipantsRpcPromise).toHaveBeenCalledWith({convID})
expect(engine.calls('chat.1.local.joinConversationByIDLocal')).toEqual([
{method: 'chat.1.local.joinConversationByIDLocal', params: {convID}},
])
expect(engine.calls('chat.1.local.refreshParticipants')).toEqual([
{method: 'chat.1.local.refreshParticipants', params: {convID}},
])
})

test('a failed join never claims the participants are fresh', async () => {
jest
.spyOn(T.RPCChat, 'localJoinConversationByIDLocalRpcPromise')
.mockRejectedValue(new Error('cannot join'))
jest.spyOn(T.RPCChat, 'localRefreshParticipantsRpcPromise').mockResolvedValue(undefined)
onJoin = () => {
throw new Error('cannot join')
}

joinConversation(conversationIDKey)
await flushPromises()

expect(T.RPCChat.localRefreshParticipantsRpcPromise).not.toHaveBeenCalled()
expect(engine.callCount('chat.1.local.refreshParticipants')).toBe(0)
})
41 changes: 26 additions & 15 deletions shared/chat/conversation/team-hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type * as React from 'react'
import * as T from '@/constants/types'
import {resetAllStores} from '@/util/zustand'
import {ChatTeamProvider, useChatTeamMemberRole, useChatTeamMembers} from './team-hooks'
import {installFakeEngine, type FakeEngine} from '@/test/fake-engine'

// The provider reads the conversation's team off the thread meta.
let mockThreadMeta = {teamID: '' as T.Teams.TeamID, teamType: 'big' as T.Chat.TeamType, teamname: ''}
Expand All @@ -30,26 +31,36 @@ const flushPromises = async () => {
}
}

const mockGetMembers = (members: ReadonlyArray<T.RPCGen.TeamMemberDetails>) =>
jest.spyOn(T.RPCGen, 'teamsTeamGetMembersByIDRpcPromise').mockResolvedValue(members)
let engine: FakeEngine

const installMembers = (
members: ReadonlyArray<T.RPCGen.TeamMemberDetails>,
extra: Parameters<typeof installFakeEngine>[0] = {}
) => {
engine = installFakeEngine({'keybase.1.teams.teamGetMembersByID': () => members, ...extra})
return engine
}

const getMembersCalls = () => engine.callCount('keybase.1.teams.teamGetMembersByID')

afterEach(() => {
cleanup()
engine.uninstall()
jest.restoreAllMocks()
resetAllStores()
})

test('useChatTeamMembers serves cached members on remount so roles render without a refetch', async () => {
const teamID = makeTeamID(1)
const rpc = mockGetMembers([memberDetails('testuser', T.RPCGen.TeamRole.owner)])
installMembers([memberDetails('testuser', T.RPCGen.TeamRole.owner)])

const first = renderHook(() => useChatTeamMembers(teamID))
expect(first.result.current.loading).toBe(true)
await act(async () => {
await flushPromises()
})
expect(first.result.current.members.get('testuser')?.type).toBe('owner')
expect(rpc).toHaveBeenCalledTimes(1)
expect(getMembersCalls()).toBe(1)
first.unmount()

// Reopening the same team must have the roles on the very first render.
Expand All @@ -59,19 +70,19 @@ test('useChatTeamMembers serves cached members on remount so roles render withou
await act(async () => {
await flushPromises()
})
expect(rpc).toHaveBeenCalledTimes(1)
expect(getMembersCalls()).toBe(1)
})

test('useChatTeamMemberRole resolves from cache on the first render under a remounted provider', async () => {
const teamID = makeTeamID(2)
mockThreadMeta = {teamID, teamType: 'big', teamname: 'keybase'}
const annotatedTeamRPC = jest
.spyOn(T.RPCGen, 'teamsGetAnnotatedTeamRpcPromise')
.mockResolvedValue({} as T.RPCGen.AnnotatedTeam)
const rpc = mockGetMembers([
memberDetails('testuser', T.RPCGen.TeamRole.admin),
memberDetails('testuser-mac', T.RPCGen.TeamRole.reader),
])
installMembers(
[
memberDetails('testuser', T.RPCGen.TeamRole.admin),
memberDetails('testuser-mac', T.RPCGen.TeamRole.reader),
],
{'keybase.1.teams.getAnnotatedTeam': () => ({}) as T.RPCGen.AnnotatedTeam}
)

const wrapper = ({children}: {children: React.ReactNode}) => (
<ChatTeamProvider>{children}</ChatTeamProvider>
Expand All @@ -88,14 +99,14 @@ test('useChatTeamMemberRole resolves from cache on the first render under a remo
await act(async () => {
await flushPromises()
})
expect(rpc).toHaveBeenCalledTimes(1)
expect(annotatedTeamRPC).not.toHaveBeenCalled()
expect(getMembersCalls()).toBe(1)
expect(engine.callCount('keybase.1.teams.getAnnotatedTeam')).toBe(0)
})

test('a disabled shadow useChatTeamMembers does not clobber the provider cache', async () => {
const teamID = makeTeamID(3)
mockThreadMeta = {teamID, teamType: 'big', teamname: 'keybase'}
mockGetMembers([memberDetails('testuser', T.RPCGen.TeamRole.owner)])
installMembers([memberDetails('testuser', T.RPCGen.TeamRole.owner)])

const wrapper = ({children}: {children: React.ReactNode}) => (
<ChatTeamProvider>{children}</ChatTeamProvider>
Expand Down
Loading