206 lines
6.8 KiB
TypeScript
206 lines
6.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 expect.poll(async () => {
|
|
return await receiver.getMessageItemByText(message).isVisible().catch(() => false);
|
|
}, { timeout }).toBe(true);
|
|
}
|
|
|
|
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(() => localStorage.getItem('metoyou.clientInstanceId'));
|
|
}
|
|
|
|
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 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 });
|
|
}
|