fix: Chats doesn't sync for multi client users
This commit is contained in:
@@ -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}"]`)
|
||||
|
||||
176
e2e/tests/chat/multi-client-chat-sync.spec.ts
Normal file
176
e2e/tests/chat/multi-client-chat-sync.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user