313 lines
9.8 KiB
TypeScript
313 lines
9.8 KiB
TypeScript
import { expect, type Page } from '@playwright/test';
|
|
import { type Client } from '../fixtures/multi-client';
|
|
import { LoginPage } from '../pages/login.page';
|
|
import { RegisterPage } from '../pages/register.page';
|
|
import { ServerSearchPage } from '../pages/server-search.page';
|
|
import { ChatRoomPage } from '../pages/chat-room.page';
|
|
import { ChatMessagesPage } from '../pages/chat-messages.page';
|
|
|
|
export const MULTI_DEVICE_PASSWORD = 'TestPass123!';
|
|
export const MULTI_DEVICE_VOICE_CHANNEL = 'General';
|
|
|
|
export interface MultiDeviceCredentials {
|
|
username: string;
|
|
displayName: string;
|
|
password: string;
|
|
}
|
|
|
|
export interface MultiDeviceScenario {
|
|
clientA: Client;
|
|
clientB: Client;
|
|
credentials: MultiDeviceCredentials;
|
|
serverName: string;
|
|
messagesA: ChatMessagesPage;
|
|
messagesB: ChatMessagesPage;
|
|
roomA: ChatRoomPage;
|
|
roomB: ChatRoomPage;
|
|
}
|
|
|
|
export function uniqueMultiDeviceName(prefix: string): string {
|
|
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 10_000)}`;
|
|
}
|
|
|
|
export async function createMultiDeviceScenario(
|
|
createClient: () => Promise<Client>,
|
|
options: { suffix?: string; serverDescription?: string } = {}
|
|
): Promise<MultiDeviceScenario> {
|
|
const suffix = options.suffix ?? uniqueMultiDeviceName('multi-device');
|
|
const credentials: MultiDeviceCredentials = {
|
|
username: `multi_${suffix}`,
|
|
displayName: 'Multi Device User',
|
|
password: MULTI_DEVICE_PASSWORD
|
|
};
|
|
const serverName = `Multi Device Server ${suffix}`;
|
|
const clientA = await createClient();
|
|
const clientB = await createClient();
|
|
|
|
await warmClientPage(clientA.page);
|
|
await warmClientPage(clientB.page);
|
|
|
|
const registerPage = new RegisterPage(clientA.page);
|
|
|
|
await registerPage.goto();
|
|
await registerPage.register(credentials.username, credentials.displayName, credentials.password);
|
|
await expect(clientA.page).toHaveURL(/\/dashboard/, { timeout: 15_000 });
|
|
|
|
const searchA = new ServerSearchPage(clientA.page);
|
|
|
|
await searchA.createServer(serverName, {
|
|
description: options.serverDescription ?? 'Multi-device session coverage'
|
|
});
|
|
|
|
await expect(clientA.page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
|
await waitForCurrentRoomName(clientA.page, serverName);
|
|
|
|
const roomA = new ChatRoomPage(clientA.page);
|
|
|
|
await roomA.ensureVoiceChannelExists(MULTI_DEVICE_VOICE_CHANNEL);
|
|
|
|
await loginSecondDeviceIntoServer(clientB.page, credentials, serverName);
|
|
await waitForCurrentRoomName(clientB.page, serverName);
|
|
|
|
const messagesA = new ChatMessagesPage(clientA.page);
|
|
const messagesB = new ChatMessagesPage(clientB.page);
|
|
const roomB = new ChatRoomPage(clientB.page);
|
|
|
|
await messagesA.waitForReady();
|
|
await messagesB.waitForReady();
|
|
|
|
return {
|
|
clientA,
|
|
clientB,
|
|
credentials,
|
|
serverName,
|
|
messagesA,
|
|
messagesB,
|
|
roomA,
|
|
roomB
|
|
};
|
|
}
|
|
|
|
export async function loginSecondDeviceIntoServer(
|
|
page: Page,
|
|
credentials: MultiDeviceCredentials,
|
|
serverName: string
|
|
): Promise<void> {
|
|
const loginPage = new LoginPage(page);
|
|
|
|
await loginPage.goto();
|
|
await loginPage.login(credentials.username, 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 expectCrossDeviceMessage(
|
|
sender: ChatMessagesPage,
|
|
receiver: ChatMessagesPage,
|
|
message: string,
|
|
timeout = 60_000
|
|
): Promise<void> {
|
|
await sender.sendMessage(message);
|
|
|
|
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> {
|
|
await page.goto('/dashboard', { waitUntil: 'domcontentloaded' });
|
|
await page.waitForLoadState('networkidle').catch(() => undefined);
|
|
}
|
|
|
|
async function waitForCurrentRoomName(page: Page, roomName: string, timeout = 20_000): Promise<void> {
|
|
await page.waitForFunction(
|
|
(expectedRoomName) => {
|
|
interface RoomShape { name?: string }
|
|
interface AngularDebugApi {
|
|
getComponent: (element: Element) => Record<string, unknown>;
|
|
}
|
|
|
|
const host = document.querySelector('app-rooms-side-panel');
|
|
const debugApi = (window as { ng?: AngularDebugApi }).ng;
|
|
|
|
if (!host || !debugApi?.getComponent) {
|
|
return false;
|
|
}
|
|
|
|
const component = debugApi.getComponent(host);
|
|
const currentRoom = (component['currentRoom'] as (() => RoomShape | null) | undefined)?.() ?? null;
|
|
|
|
return currentRoom?.name === expectedRoomName;
|
|
},
|
|
roomName,
|
|
{ timeout }
|
|
);
|
|
}
|
|
|
|
export async function readClientInstanceId(page: Page): Promise<string | null> {
|
|
return page.evaluate(() => {
|
|
const sessionId = sessionStorage.getItem('metoyou.clientInstanceId')?.trim();
|
|
|
|
if (sessionId) {
|
|
return sessionId;
|
|
}
|
|
|
|
return localStorage.getItem('metoyou.clientInstanceId')?.trim() ?? null;
|
|
});
|
|
}
|
|
|
|
export async function logoutFromMenu(page: Page): Promise<void> {
|
|
const menuButton = page.getByRole('button', { name: 'Menu' });
|
|
const logoutButton = page.getByRole('button', { name: 'Logout' });
|
|
|
|
await expect(menuButton).toBeVisible({ timeout: 10_000 });
|
|
await menuButton.click();
|
|
await expect(logoutButton).toBeVisible({ timeout: 10_000 });
|
|
await logoutButton.click();
|
|
await expect(page).toHaveURL(/\/login/, { timeout: 15_000 });
|
|
}
|
|
|
|
export function channelsSidePanel(page: Page) {
|
|
return page.locator('app-rooms-side-panel').first();
|
|
}
|
|
|
|
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}"]`)
|
|
.getByText('Join', { exact: true });
|
|
}
|
|
|
|
export async function expectPassiveVoiceOnDevice(
|
|
page: Page,
|
|
options: { timeout?: number; displayName?: string; channelName?: string } = {}
|
|
): Promise<void> {
|
|
const timeout = options.timeout ?? 45_000;
|
|
const channelName = options.channelName ?? MULTI_DEVICE_VOICE_CHANNEL;
|
|
const displayName = options.displayName;
|
|
|
|
await expect.poll(async () => {
|
|
const membersLabel = await membersSidePanel(page)
|
|
.getByText('In voice on another device', { exact: false })
|
|
.isVisible()
|
|
.catch(() => false);
|
|
const joinBadge = await passiveVoiceChannelJoinBadge(page, channelName).isVisible()
|
|
.catch(() => false);
|
|
const grayedVoiceUser = displayName
|
|
? await channelsSidePanel(page).locator('.opacity-50')
|
|
.filter({ hasText: displayName })
|
|
.first()
|
|
.isVisible()
|
|
.catch(() => false)
|
|
: false;
|
|
|
|
return membersLabel || joinBadge || grayedVoiceUser;
|
|
}, { timeout }).toBe(true);
|
|
}
|
|
|
|
export async function expectActiveVoiceOnDevice(page: Page, timeout = 20_000): Promise<void> {
|
|
await expect(page.locator('app-voice-controls, app-voice-workspace').first()).toBeVisible({ timeout });
|
|
}
|