Files
Toju/e2e/helpers/multi-device-session.ts
Myx eb51f043ac
All checks were successful
Queue Release Build / prepare (push) Successful in 19s
Deploy Web Apps / deploy (push) Successful in 8m12s
Queue Release Build / build-windows (push) Successful in 27m44s
Queue Release Build / build-linux (push) Successful in 48m1s
Queue Release Build / build-android (push) Successful in 22m7s
Queue Release Build / finalize (push) Successful in 2m42s
fix: Major bug cleanup pass 1
2026-06-09 17:59:54 +02:00

220 lines
7.0 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(() => {
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 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 });
}