fix: Chats doesn't sync for multi client users

This commit is contained in:
2026-06-11 00:04:49 +02:00
parent d0aff6319d
commit d174536272
16 changed files with 662 additions and 16 deletions

View File

@@ -114,10 +114,84 @@ export async function expectCrossDeviceMessage(
): Promise<void> {
await sender.sendMessage(message);
await expect.poll(async () => {
return await receiver.getMessageItemByText(message).isVisible()
.catch(() => false);
}, { timeout }).toBe(true);
await expectSyncedMessage(receiver, message, timeout);
}
/** Waits until a message sent elsewhere appears in the local chat history. */
export async function expectSyncedMessage(
receiver: ChatMessagesPage,
message: string,
timeout = 90_000
): Promise<void> {
await receiver.waitForReady();
await expect(receiver.getMessageItemByText(message)).toBeVisible({ timeout });
}
export async function expectSyncedMessageWithResync(
page: Page,
receiver: ChatMessagesPage,
message: string,
timeout = 60_000
): Promise<void> {
await receiver.waitForReady();
const alreadyVisible = await receiver.getMessageItemByText(message)
.isVisible()
.catch(() => false);
if (!alreadyVisible) {
await resyncChannelMessages(page);
}
await expect(receiver.getMessageItemByText(message)).toBeVisible({ timeout });
}
export async function resyncChannelMessages(page: Page, channelName = 'general'): Promise<void> {
const channel = page.locator(`button[data-channel-type="text"][data-channel-name="${channelName}"]`).first();
await expect(channel).toBeVisible({ timeout: 10_000 });
await channel.click({ button: 'right' });
await page.getByRole('button', { name: 'Resync Messages' }).click();
}
export async function closeClient(client: Client): Promise<void> {
await client.context.close();
}
export async function registerGuestAndJoinServer(
page: Page,
credentials: MultiDeviceCredentials,
serverName: string
): Promise<void> {
const registerPage = new RegisterPage(page);
await registerPage.goto();
await registerPage.register(credentials.username, credentials.displayName, credentials.password);
await expect(page).toHaveURL(/\/dashboard/, { timeout: 15_000 });
const search = new ServerSearchPage(page);
await search.joinServerFromSearch(serverName);
await expect(page).toHaveURL(/\/room\//, { timeout: 20_000 });
await expect(page.locator('app-rooms-side-panel').first()).toBeVisible({ timeout: 20_000 });
}
export async function reopenClientInServer(
createClient: () => Promise<Client>,
credentials: MultiDeviceCredentials,
serverName: string
): Promise<{ client: Client; messages: ChatMessagesPage }> {
const client = await createClient();
await warmClientPage(client.page);
await loginSecondDeviceIntoServer(client.page, credentials, serverName);
const messages = new ChatMessagesPage(client.page);
await messages.waitForReady();
return { client, messages };
}
async function warmClientPage(page: Page): Promise<void> {
@@ -181,6 +255,25 @@ export function membersSidePanel(page: Page) {
return page.locator('app-rooms-side-panel').last();
}
export function serverMemberRow(page: Page, displayName: string) {
return membersSidePanel(page)
.locator('[role="button"], button')
.filter({ has: page.getByText(displayName, { exact: true }) })
.first();
}
/**
* Gates cross-user assertions on real presence: the peer must show up in the
* members panel before chat delivery between the two users can be expected.
*/
export async function expectServerPeerVisible(
page: Page,
displayName: string,
timeout = 45_000
): Promise<void> {
await expect(serverMemberRow(page, displayName)).toBeVisible({ timeout });
}
export function passiveVoiceChannelJoinBadge(page: Page, channelName = MULTI_DEVICE_VOICE_CHANNEL) {
return page
.locator(`button[data-channel-type="voice"][data-channel-name="${channelName}"]`)

View File

@@ -0,0 +1,176 @@
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);
});
});
});