fix: Major bug cleanup pass 1
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

This commit is contained in:
2026-06-09 17:59:54 +02:00
parent 80d7728e66
commit eb51f043ac
127 changed files with 2731 additions and 322 deletions

20
e2e/helpers/app-menu.ts Normal file
View File

@@ -0,0 +1,20 @@
import { expect, type Page } from '@playwright/test';
export async function openTitleBarMenu(page: Page): Promise<void> {
const menuButton = page.getByRole('button', { name: 'Menu' });
await expect(menuButton).toBeVisible({ timeout: 15_000 });
await menuButton.click();
await expect(page.locator('app-title-bar .absolute.right-0.top-full').first()).toBeVisible({ timeout: 10_000 });
}
export async function openPluginStore(page: Page): Promise<void> {
await openTitleBarMenu(page);
await page.getByRole('button', { name: 'Plugin Store' }).click();
await expect(page).toHaveURL(/\/plugin-store/, { timeout: 20_000 });
}
export async function openSettingsFromMenu(page: Page): Promise<void> {
await openTitleBarMenu(page);
await page.getByRole('button', { name: 'Settings' }).click();
}

View File

@@ -1,6 +1,7 @@
import { type APIRequestContext, type Page } from '@playwright/test';
export const AUTH_TOKENS_STORAGE_KEY = 'metoyou.authTokens';
export const SIGNAL_SERVER_CREDENTIALS_STORAGE_KEY = 'metoyou.signalServerCredentials';
export interface AuthSession {
id: string;
@@ -56,6 +57,36 @@ export async function loginTestUser(
return await response.json() as AuthSession;
}
export async function readSignalServerCredentialFromPage(
page: Page,
serverUrl: string
): Promise<{ userId: string; token: string; username: string } | null> {
return await page.evaluate(({ storageKey, url }) => {
try {
const store = JSON.parse(localStorage.getItem(storageKey) || '{}') as Record<string, {
userId: string;
token: string;
username: string;
expiresAt: number;
}>;
const normalizedUrl = url.trim().replace(/\/+$/, '');
const entry = store[normalizedUrl];
if (!entry || entry.expiresAt <= Date.now()) {
return null;
}
return {
userId: entry.userId,
token: entry.token,
username: entry.username
};
} catch {
return null;
}
}, { storageKey: SIGNAL_SERVER_CREDENTIALS_STORAGE_KEY, url: serverUrl });
}
export async function readAuthTokenFromPage(page: Page, serverUrl: string): Promise<string | null> {
return await page.evaluate(({ storageKey, url }) => {
try {

11
e2e/helpers/dashboard.ts Normal file
View File

@@ -0,0 +1,11 @@
import { expect, type Page } from '@playwright/test';
/** Dashboard omnibox (desktop placeholder copy changed with i18n refresh). */
export function dashboardSearchInput(page: Page) {
return page.getByRole('textbox', { name: 'Search people, servers, and invites' });
}
export async function expectDashboardReady(page: Page, timeout = 30_000): Promise<void> {
await expect(page).toHaveURL(/\/dashboard/, { timeout });
await expect(dashboardSearchInput(page)).toBeVisible({ timeout });
}

View File

@@ -41,7 +41,6 @@ export async function createMultiDeviceScenario(
password: MULTI_DEVICE_PASSWORD
};
const serverName = `Multi Device Server ${suffix}`;
const clientA = await createClient();
const clientB = await createClient();
@@ -59,6 +58,7 @@ export async function createMultiDeviceScenario(
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);
@@ -115,7 +115,8 @@ export async function expectCrossDeviceMessage(
await sender.sendMessage(message);
await expect.poll(async () => {
return await receiver.getMessageItemByText(message).isVisible().catch(() => false);
return await receiver.getMessageItemByText(message).isVisible()
.catch(() => false);
}, { timeout }).toBe(true);
}
@@ -150,7 +151,15 @@ async function waitForCurrentRoomName(page: Page, roomName: string, timeout = 20
}
export async function readClientInstanceId(page: Page): Promise<string | null> {
return page.evaluate(() => localStorage.getItem('metoyou.clientInstanceId'));
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> {
@@ -191,9 +200,14 @@ export async function expectPassiveVoiceOnDevice(
.getByText('In voice on another device', { exact: false })
.isVisible()
.catch(() => false);
const joinBadge = await passiveVoiceChannelJoinBadge(page, channelName).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)
? await channelsSidePanel(page).locator('.opacity-50')
.filter({ hasText: displayName })
.first()
.isVisible()
.catch(() => false)
: false;
return membersLabel || joinBadge || grayedVoiceUser;

View File

@@ -0,0 +1,19 @@
import { expect, type Page } from '@playwright/test';
export const E2E_PLUGIN_SOURCE_URL = 'http://localhost:4200/plugins/e2e-plugin-source.json';
export const E2E_PLUGIN_TITLE = 'E2E All API Plugin';
export async function addPluginSource(page: Page, sourceUrl = E2E_PLUGIN_SOURCE_URL): Promise<void> {
const sourceInput = page.getByLabel('Plugin source manifest URL');
await expect(sourceInput).toBeVisible({ timeout: 15_000 });
await sourceInput.click();
await sourceInput.fill(sourceUrl);
await expect(sourceInput).toHaveValue(sourceUrl, { timeout: 5_000 });
const addSourceButton = page.getByRole('button', { name: 'Add Source' });
await expect(addSourceButton).toBeEnabled({ timeout: 10_000 });
await addSourceButton.click();
await expect(page.getByRole('heading', { name: E2E_PLUGIN_TITLE })).toBeVisible({ timeout: 20_000 });
}

View File

@@ -1,15 +1,19 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type Page } from '@playwright/test';
import { type BrowserContext, type Page } from '@playwright/test';
/**
* Install RTCPeerConnection monkey-patch on a page BEFORE navigating.
* Tracks all created peer connections and their remote tracks so tests
* can inspect WebRTC state via `page.evaluate()`.
*
* Call immediately after page creation, before any `goto()`.
* Call on the browser context (preferred) or page before any `goto()`.
*/
export async function installWebRTCTracking(page: Page): Promise<void> {
await page.addInitScript(() => {
export async function installWebRTCTracking(target: BrowserContext | Page): Promise<void> {
const addInitScript = 'addInitScript' in target && typeof target.addInitScript === 'function'
? target.addInitScript.bind(target)
: (target as Page).addInitScript.bind(target);
await addInitScript(() => {
const connections: RTCPeerConnection[] = [];
const dataChannels: RTCDataChannel[] = [];
const syntheticMediaResources: {
@@ -197,6 +201,7 @@ export async function waitForPeerConnected(page: Page, timeout = 30_000): Promis
() => (window as any).__rtcConnections?.some(
(pc: RTCPeerConnection) => pc.connectionState === 'connected'
) ?? false,
undefined,
{ timeout }
);
}
@@ -611,6 +616,7 @@ export async function waitForAudioStatsPresent(page: Page, timeout = 15_000): Pr
return false;
},
undefined,
{ timeout }
);
}
@@ -818,6 +824,7 @@ export async function waitForVideoStatsPresent(page: Page, timeout = 15_000): Pr
return false;
},
undefined,
{ timeout }
);
}