import { test, expect } from '../../fixtures/multi-client'; import { MULTI_DEVICE_VOICE_CHANNEL, channelsSidePanel, createMultiDeviceScenario, expectCrossDeviceMessage, expectActiveVoiceOnDevice, expectPassiveVoiceOnDevice, logoutFromMenu, membersSidePanel, passiveVoiceChannelJoinBadge, readClientInstanceId, uniqueMultiDeviceName } from '../../helpers/multi-device-session'; test.describe('Multi-device session', () => { test.describe.configure({ timeout: 300_000, retries: 1 }); test('covers identity, chat sync, typing exclusion, and voice exclusivity', async ({ createClient }) => { const scenario = await createMultiDeviceScenario(createClient); const messageAtoB = `Cross-device A to B ${uniqueMultiDeviceName('msg')}`; const messageBtoA = `Cross-device B to A ${uniqueMultiDeviceName('msg')}`; const typingDraft = `Typing draft ${uniqueMultiDeviceName('draft')}`; await test.step('assigns distinct clientInstanceId per browser context', async () => { const instanceA = await readClientInstanceId(scenario.clientA.page); const instanceB = await readClientInstanceId(scenario.clientB.page); expect(instanceA).toBeTruthy(); expect(instanceB).toBeTruthy(); expect(instanceA).not.toEqual(instanceB); }); await test.step('syncs chat from device A to device B', async () => { await expectCrossDeviceMessage(scenario.messagesA, scenario.messagesB, messageAtoB); }); await test.step('syncs chat from device B to device A', async () => { await expectCrossDeviceMessage(scenario.messagesB, scenario.messagesA, messageBtoA); }); await test.step('does not show own typing indicator on the other device for the same user', async () => { await scenario.messagesA.typeDraftWithTypingEvents(typingDraft); await expect( scenario.clientB.page.getByText(`${scenario.credentials.displayName} is typing`, { exact: false }) ).toHaveCount(0, { timeout: 5_000 }); }); await test.step('shows passive in-voice UI on the second device when the first joins voice', async () => { await scenario.roomA.joinVoiceChannel(MULTI_DEVICE_VOICE_CHANNEL); await expectActiveVoiceOnDevice(scenario.clientA.page); await expectPassiveVoiceOnDevice(scenario.clientB.page, { displayName: scenario.credentials.displayName }); await expect( membersSidePanel(scenario.clientB.page).getByText('In voice on another device', { exact: false }) ).toBeVisible({ timeout: 20_000 }); await expect( channelsSidePanel(scenario.clientB.page).locator('.opacity-50') .filter({ hasText: scenario.credentials.displayName }) .first() ).toBeVisible({ timeout: 20_000 }); }); await test.step('shows Join takeover affordance on passive device voice channel', async () => { await expect(passiveVoiceChannelJoinBadge(scenario.clientB.page)).toBeVisible({ timeout: 20_000 }); }); await test.step('transfers voice ownership when the passive device takes over', async () => { await scenario.roomB.joinVoiceChannel(MULTI_DEVICE_VOICE_CHANNEL); await expectActiveVoiceOnDevice(scenario.clientB.page); await expectPassiveVoiceOnDevice(scenario.clientA.page, { displayName: scenario.credentials.displayName }); }); await test.step('keeps the second device logged in when the first device logs out', async () => { const message = `Still logged in ${uniqueMultiDeviceName('logout')}`; await logoutFromMenu(scenario.clientA.page); await scenario.messagesB.sendMessage(message); await expect(scenario.messagesB.getMessageItemByText(message)).toBeVisible({ timeout: 20_000 }); await expect(scenario.clientB.page).toHaveURL(/\/room\//, { timeout: 10_000 }); }); }); });