import { test, expect } from '../../fixtures/multi-client'; import { RegisterPage } from '../../pages/register.page'; import { ServerSearchPage } from '../../pages/server-search.page'; import { ChatMessagesPage } from '../../pages/chat-messages.page'; import { MULTI_DEVICE_PASSWORD, closeClient, expectCrossDeviceMessage, expectSyncedMessage, expectSyncedMessageWithResync, expectServerPeerVisible, loginSecondDeviceIntoServer, reopenClientInServer, uniqueMultiDeviceName } from '../../helpers/multi-device-session'; test.describe('Multi-client chat sync', () => { test.describe.configure({ timeout: 360_000, retries: 1 }); test('syncs messages between same-user devices and late-joining users after offline gaps', async ({ createClient }) => { const suffix = uniqueMultiDeviceName('multi-chat-sync'); const hostCredentials = { username: `ludde_${suffix}`, displayName: 'Ludde', password: MULTI_DEVICE_PASSWORD }; const guestCredentials = { username: `azaaxin_${suffix}`, displayName: 'Azaaxin', password: MULTI_DEVICE_PASSWORD }; const serverName = `Multi Client Chat Sync ${suffix}`; const sharedBaselineMessage = `Shared baseline ${suffix}`; const soloHostMessage = `Solo host message ${suffix}`; const liveGuestProbeMessage = `Live guest probe ${suffix}`; const offlineGapMessage = `Offline gap message ${suffix}`; const client1 = await createClient(); const client2 = await createClient(); const client3 = await createClient(); await test.step('client 1: host registers and creates the shared server', async () => { const registerPage = new RegisterPage(client1.page); await registerPage.goto(); await registerPage.register( hostCredentials.username, hostCredentials.displayName, hostCredentials.password ); await expect(client1.page).toHaveURL(/\/dashboard/, { timeout: 15_000 }); const search = new ServerSearchPage(client1.page); await search.createServer(serverName, { description: 'Multi-client chat sync regression coverage' }); await expect(client1.page).toHaveURL(/\/room\//, { timeout: 15_000 }); }); const messages1 = new ChatMessagesPage(client1.page); await messages1.waitForReady(); await test.step('client 2: second host device joins the same server', async () => { await loginSecondDeviceIntoServer(client2.page, hostCredentials, serverName); }); const messages2 = new ChatMessagesPage(client2.page); await messages2.waitForReady(); await test.step('both host devices exchange chat while online together', async () => { await expectCrossDeviceMessage(messages1, messages2, sharedBaselineMessage); }); await test.step('close the second host browser (client 2)', async () => { await closeClient(client2); }); await test.step('client 1 sends chat while the second host device is offline', async () => { await client1.page.bringToFront(); await messages1.sendMessage(soloHostMessage); await expect(messages1.getMessageItemByText(soloHostMessage)).toBeVisible({ timeout: 20_000 }); }); await test.step('guest account registers ahead of joining the server', async () => { const registerPage = new RegisterPage(client3.page); await registerPage.goto(); await registerPage.register( guestCredentials.username, guestCredentials.displayName, guestCredentials.password ); await expect(client3.page).toHaveURL(/\/dashboard/, { timeout: 15_000 }); }); let messages3 = new ChatMessagesPage(client3.page); await test.step('client 3: guest joins and receives existing chat history', async () => { // Keep the host tab active so its websocket + peer negotiation stay alive. await client1.page.bringToFront(); await messages1.waitForReady(); const search = new ServerSearchPage(client3.page); await search.joinServerFromSearch(serverName); await expect(client3.page).toHaveURL(/\/room\//, { timeout: 20_000 }); messages3 = new ChatMessagesPage(client3.page); await messages3.waitForReady(); // Presence gate: both users must see each other in the members panel // before cross-user chat delivery can be expected. await client1.page.bringToFront(); await expectServerPeerVisible(client1.page, guestCredentials.displayName); await client3.page.bringToFront(); await expectServerPeerVisible(client3.page, hostCredentials.displayName); // Live delivery first - proves host <-> guest transport is actually up. await expectCrossDeviceMessage(messages1, messages3, liveGuestProbeMessage); // History only replicates over P2P inventory once the peer link exists. await client1.page.bringToFront(); await expectSyncedMessageWithResync(client3.page, messages3, sharedBaselineMessage); await expectSyncedMessageWithResync(client3.page, messages3, soloHostMessage); }); await test.step('close the guest browser (client 3)', async () => { await closeClient(client3); }); await test.step('reopen client 2 and send a message while client 1 stays online', async () => { await client1.page.bringToFront(); const reopened = await reopenClientInServer(createClient, hostCredentials, serverName); // Same-user catch-up uses account_sync, not P2P between own devices. await expectSyncedMessageWithResync( reopened.client.page, reopened.messages, soloHostMessage ); await reopened.messages.sendMessage(offlineGapMessage); await expect(reopened.messages.getMessageItemByText(offlineGapMessage)).toBeVisible({ timeout: 20_000 }); }); await test.step('reopened guest client receives the offline-gap message from host device 2', async () => { await client1.page.bringToFront(); await messages1.waitForReady(); const reopenedGuest = await reopenClientInServer(createClient, guestCredentials, serverName); // Presence gate before relying on cross-user delivery again. await client1.page.bringToFront(); await expectServerPeerVisible(client1.page, guestCredentials.displayName); await reopenedGuest.client.page.bringToFront(); await expectServerPeerVisible(reopenedGuest.client.page, hostCredentials.displayName); await expectCrossDeviceMessage(messages1, reopenedGuest.messages, `Guest wake ${suffix}`); await expectSyncedMessageWithResync( reopenedGuest.client.page, reopenedGuest.messages, offlineGapMessage ); }); await test.step('primary host device still receives the message from its second device', async () => { await expectSyncedMessage(messages1, offlineGapMessage); }); }); });