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
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:
20
e2e/helpers/app-menu.ts
Normal file
20
e2e/helpers/app-menu.ts
Normal 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();
|
||||
}
|
||||
@@ -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
11
e2e/helpers/dashboard.ts
Normal 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 });
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
19
e2e/helpers/plugin-store.ts
Normal file
19
e2e/helpers/plugin-store.ts
Normal 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 });
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
test,
|
||||
expect
|
||||
} from '../../fixtures/multi-client';
|
||||
import { test, expect } from '../../fixtures/multi-client';
|
||||
import {
|
||||
MULTI_DEVICE_VOICE_CHANNEL,
|
||||
channelsSidePanel,
|
||||
@@ -57,13 +54,17 @@ test.describe('Multi-device session', () => {
|
||||
await expectPassiveVoiceOnDevice(scenario.clientB.page, {
|
||||
displayName: scenario.credentials.displayName
|
||||
});
|
||||
|
||||
await expect(
|
||||
membersSidePanel(scenario.clientB.page).getByText('In voice on another device', { exact: false })
|
||||
).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
await expect(
|
||||
channelsSidePanel(scenario.clientB.page).locator('.opacity-50').filter({
|
||||
hasText: scenario.credentials.displayName
|
||||
}).first()
|
||||
channelsSidePanel(scenario.clientB.page).locator('.opacity-50')
|
||||
.filter({
|
||||
hasText: scenario.credentials.displayName
|
||||
})
|
||||
.first()
|
||||
).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
|
||||
|
||||
111
e2e/tests/auth/multi-signal-server-auth.spec.ts
Normal file
111
e2e/tests/auth/multi-signal-server-auth.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '../../fixtures/multi-client';
|
||||
import { openSettingsFromMenu } from '../../helpers/app-menu';
|
||||
import { expectDashboardReady } from '../../helpers/dashboard';
|
||||
import { installTestServerEndpoints } from '../../helpers/seed-test-endpoint';
|
||||
import { startTestServer } from '../../helpers/test-server';
|
||||
import {
|
||||
readAuthTokenFromPage,
|
||||
readSignalServerCredentialFromPage,
|
||||
registerTestUser
|
||||
} from '../../helpers/auth-api';
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
|
||||
const PRIMARY_ENDPOINT_ID = 'e2e-multi-auth-primary';
|
||||
const USER_PASSWORD = 'TestPass123!';
|
||||
|
||||
test.describe('Multi-signal-server authentication', () => {
|
||||
test.describe.configure({ timeout: 180_000 });
|
||||
|
||||
test('auto-provisions a foreign signal server when a new endpoint is added', async ({ createClient, request }) => {
|
||||
const primaryServer = await startTestServer();
|
||||
const secondaryServer = await startTestServer();
|
||||
|
||||
try {
|
||||
const client = await createClient();
|
||||
const suffix = `multi_auth_${Date.now()}`;
|
||||
const username = `user_${suffix}`;
|
||||
|
||||
await installTestServerEndpoints(client.context, [
|
||||
{
|
||||
id: PRIMARY_ENDPOINT_ID,
|
||||
name: 'E2E Primary Signal',
|
||||
url: primaryServer.url,
|
||||
isActive: true,
|
||||
status: 'online'
|
||||
}
|
||||
]);
|
||||
|
||||
await test.step('Register on the home signal server', async () => {
|
||||
const register = new RegisterPage(client.page);
|
||||
|
||||
await register.goto();
|
||||
await register.register(username, 'Multi Auth User', USER_PASSWORD);
|
||||
await expectDashboardReady(client.page);
|
||||
});
|
||||
|
||||
await test.step('Add a second signal server in network settings', async () => {
|
||||
await openSettingsFromMenu(client.page);
|
||||
await client.page.getByRole('button', { name: 'Network' }).click();
|
||||
|
||||
await client.page.getByPlaceholder('Server name').fill('E2E Secondary Signal');
|
||||
await client.page.getByPlaceholder('Server URL (e.g., http://localhost:3001)').fill(secondaryServer.url);
|
||||
await client.page.getByTestId('add-signal-server-button').click();
|
||||
|
||||
await expect(client.page.getByText(secondaryServer.url)).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
await test.step('Wait for auto-provisioned credentials on the secondary server', async () => {
|
||||
await expect.poll(async () =>
|
||||
await readSignalServerCredentialFromPage(client.page, secondaryServer.url),
|
||||
{ timeout: 30_000 }
|
||||
).not.toBeNull();
|
||||
|
||||
const homeToken = await readAuthTokenFromPage(client.page, primaryServer.url);
|
||||
const secondaryCredential = await readSignalServerCredentialFromPage(client.page, secondaryServer.url);
|
||||
|
||||
expect(homeToken).toBeTruthy();
|
||||
expect(secondaryCredential?.username).toBe(username);
|
||||
expect(secondaryCredential?.token).toBeTruthy();
|
||||
});
|
||||
|
||||
await test.step('Secondary credential can call authenticated APIs', async () => {
|
||||
const secondaryCredential = await readSignalServerCredentialFromPage(client.page, secondaryServer.url);
|
||||
|
||||
if (!secondaryCredential) {
|
||||
throw new Error('Expected secondary signal-server credential to be provisioned');
|
||||
}
|
||||
|
||||
const response = await request.post(`${secondaryServer.url}/api/servers`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${secondaryCredential.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
name: `Secondary Provisioned Server ${suffix}`,
|
||||
description: 'Created with auto-provisioned credentials',
|
||||
ownerId: secondaryCredential.userId,
|
||||
ownerPublicKey: 'e2e-secondary-owner-key'
|
||||
}
|
||||
});
|
||||
|
||||
expect(response.ok(), `POST /api/servers failed: ${response.status()} ${await response.text()}`).toBe(true);
|
||||
});
|
||||
|
||||
await test.step('Home registration still works independently on the secondary server', async () => {
|
||||
const otherUser = await registerTestUser(
|
||||
request,
|
||||
secondaryServer.url,
|
||||
`other_${suffix}`,
|
||||
USER_PASSWORD,
|
||||
'Other User'
|
||||
);
|
||||
|
||||
expect(otherUser.username).toBe(`other_${suffix}`);
|
||||
});
|
||||
} finally {
|
||||
await primaryServer.stop();
|
||||
await secondaryServer.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
expect,
|
||||
type BrowserContext,
|
||||
type Locator,
|
||||
type Page
|
||||
} from '@playwright/test';
|
||||
@@ -35,6 +36,7 @@ test.describe('Chat notifications', () => {
|
||||
await clearDesktopNotifications(scenario.alice.page);
|
||||
await scenario.bobRoom.joinTextChannel(scenario.channelName);
|
||||
await scenario.bobMessages.sendMessage(message);
|
||||
await expectUnreadCounts(scenario.alice.page, scenario.serverName, scenario.channelName);
|
||||
});
|
||||
|
||||
await test.step('Alice receives a desktop notification with the channel preview', async () => {
|
||||
@@ -67,8 +69,7 @@ test.describe('Chat notifications', () => {
|
||||
});
|
||||
|
||||
await test.step('Alice still sees unread badges for the room and channel', async () => {
|
||||
await expect(getUnreadBadge(getSavedRoomButton(scenario.alice.page, scenario.serverName))).toHaveText('1', { timeout: 20_000 });
|
||||
await expect(getUnreadBadge(getTextChannelButton(scenario.alice.page, scenario.channelName))).toHaveText('1', { timeout: 20_000 });
|
||||
await expectUnreadCounts(scenario.alice.page, scenario.serverName, scenario.channelName);
|
||||
});
|
||||
|
||||
await test.step('Alice does not get a muted desktop popup', async () => {
|
||||
@@ -96,7 +97,7 @@ async function createNotificationScenario(createClient: () => Promise<Client>):
|
||||
const alice = await createClient();
|
||||
const bob = await createClient();
|
||||
|
||||
await installDesktopNotificationSpy(alice.page);
|
||||
await installDesktopNotificationSpy(alice.context);
|
||||
|
||||
await registerUser(alice.page, aliceCredentials.username, aliceCredentials.displayName, aliceCredentials.password);
|
||||
await registerUser(bob.page, bobCredentials.username, bobCredentials.displayName, bobCredentials.password);
|
||||
@@ -143,8 +144,8 @@ async function registerUser(page: Page, username: string, displayName: string, p
|
||||
await expect(page).toHaveURL(/\/dashboard/, { timeout: 15_000 });
|
||||
}
|
||||
|
||||
async function installDesktopNotificationSpy(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
async function installDesktopNotificationSpy(context: BrowserContext): Promise<void> {
|
||||
await context.addInitScript(() => {
|
||||
const notifications: DesktopNotificationRecord[] = [];
|
||||
|
||||
class MockNotification {
|
||||
@@ -250,6 +251,11 @@ function getUnreadBadge(container: Locator): Locator {
|
||||
return container.locator('span.rounded-full').first();
|
||||
}
|
||||
|
||||
async function expectUnreadCounts(page: Page, serverName: string, channelName: string): Promise<void> {
|
||||
await expect(getUnreadBadge(getSavedRoomButton(page, serverName))).toHaveText('1', { timeout: 45_000 });
|
||||
await expect(getUnreadBadge(getTextChannelButton(page, channelName))).toHaveText('1', { timeout: 45_000 });
|
||||
}
|
||||
|
||||
function uniqueName(prefix: string): string {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36)
|
||||
.slice(2, 8)}`;
|
||||
|
||||
@@ -367,11 +367,10 @@ async function launchPersistentSession(
|
||||
});
|
||||
|
||||
await installTestServerEndpoint(context, testServerPort);
|
||||
await installWebRTCTracking(context);
|
||||
|
||||
const page = context.pages()[0] ?? await context.newPage();
|
||||
|
||||
await installWebRTCTracking(page);
|
||||
|
||||
return { context, page };
|
||||
}
|
||||
|
||||
|
||||
@@ -196,11 +196,10 @@ async function launchPersistentSession(userDataDir: string, testServerPort: numb
|
||||
});
|
||||
|
||||
await installTestServerEndpoint(context, testServerPort);
|
||||
await installWebRTCTracking(context);
|
||||
|
||||
const page = context.pages()[0] ?? (await context.newPage());
|
||||
|
||||
await installWebRTCTracking(page);
|
||||
|
||||
return { context, page };
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,20 @@ import {
|
||||
test,
|
||||
type Client
|
||||
} from '../../fixtures/multi-client';
|
||||
import { openPluginStore } from '../../helpers/app-menu';
|
||||
import {
|
||||
addPluginSource,
|
||||
E2E_PLUGIN_SOURCE_URL,
|
||||
E2E_PLUGIN_TITLE
|
||||
} from '../../helpers/plugin-store';
|
||||
import { installWebRTCTracking } from '../../helpers/webrtc-helpers';
|
||||
import { ChatMessagesPage } from '../../pages/chat-messages.page';
|
||||
import { ChatRoomPage } from '../../pages/chat-room.page';
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
import { ServerSearchPage } from '../../pages/server-search.page';
|
||||
|
||||
const PLUGIN_SOURCE_URL = 'http://localhost:4200/plugins/e2e-plugin-source.json';
|
||||
const PLUGIN_TITLE = 'E2E All API Plugin';
|
||||
const PLUGIN_SOURCE_URL = E2E_PLUGIN_SOURCE_URL;
|
||||
const PLUGIN_TITLE = E2E_PLUGIN_TITLE;
|
||||
const EDITED_MESSAGE = 'Plugin API edited message';
|
||||
const ORIGINAL_MESSAGE = 'Plugin API original message';
|
||||
const DELETED_MESSAGE = 'Plugin API deleted message';
|
||||
@@ -87,6 +94,9 @@ async function createPluginApiScenario(createClient: () => Promise<Client>): Pro
|
||||
const alice = await createClient();
|
||||
const bob = await createClient();
|
||||
|
||||
await installWebRTCTracking(alice.page);
|
||||
await installWebRTCTracking(bob.page);
|
||||
|
||||
await registerUser(alice.page, `alice_${suffix}`, 'Alice');
|
||||
await registerUser(bob.page, `bob_${suffix}`, 'Bob');
|
||||
|
||||
@@ -98,13 +108,10 @@ async function createPluginApiScenario(createClient: () => Promise<Client>): Pro
|
||||
const aliceRoom = new ChatRoomPage(alice.page);
|
||||
|
||||
await aliceRoom.ensureVoiceChannelExists(VOICE_CHANNEL);
|
||||
await installGrantAndActivatePlugin(alice.page, true);
|
||||
await closeSettingsModal(alice.page);
|
||||
await expect(soundboardComposerButton(alice.page)).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
const bobSearch = new ServerSearchPage(bob.page);
|
||||
|
||||
await bobSearch.joinServerFromSearch(serverName, { acceptPluginDownloads: true });
|
||||
await bobSearch.joinServerFromSearch(serverName);
|
||||
await expect(bob.page).toHaveURL(/\/room\//, { timeout: 30_000 });
|
||||
|
||||
const bobRoom = new ChatRoomPage(bob.page);
|
||||
@@ -113,6 +120,9 @@ async function createPluginApiScenario(createClient: () => Promise<Client>): Pro
|
||||
await bobRoom.joinVoiceChannel(VOICE_CHANNEL);
|
||||
await expect(aliceRoom.voiceControls).toBeVisible({ timeout: 30_000 });
|
||||
await expect(bobRoom.voiceControls).toBeVisible({ timeout: 30_000 });
|
||||
await installGrantAndActivatePlugin(alice.page, true);
|
||||
await closeSettingsModal(alice.page);
|
||||
await expect(soundboardComposerButton(alice.page)).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
const aliceMessages = new ChatMessagesPage(alice.page);
|
||||
const bobMessages = new ChatMessagesPage(bob.page);
|
||||
@@ -141,14 +151,11 @@ async function registerUser(page: Page, username: string, displayName: string):
|
||||
}
|
||||
|
||||
async function installGrantAndActivatePlugin(page: Page, installFromStore: boolean): Promise<void> {
|
||||
await page.getByRole('button', { name: 'Plugin Store' }).click();
|
||||
await expect(page).toHaveURL(/\/plugin-store/, { timeout: 20_000 });
|
||||
await openPluginStore(page);
|
||||
await expect(page.getByTestId('plugin-store-page')).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
if (installFromStore) {
|
||||
await page.getByLabel('Plugin source manifest URL').fill(PLUGIN_SOURCE_URL);
|
||||
await page.getByRole('button', { name: 'Add Source' }).click();
|
||||
await expect(page.getByRole('heading', { name: PLUGIN_TITLE })).toBeVisible({ timeout: 20_000 });
|
||||
await addPluginSource(page, PLUGIN_SOURCE_URL);
|
||||
await page.locator('article', { hasText: PLUGIN_TITLE }).getByRole('button', { exact: true, name: /^(Install|Install to Server)$/ })
|
||||
.click();
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { expect, test } from '../../fixtures/multi-client';
|
||||
import { openPluginStore } from '../../helpers/app-menu';
|
||||
import { expectDashboardReady } from '../../helpers/dashboard';
|
||||
import { addPluginSource } from '../../helpers/plugin-store';
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
import { ServerSearchPage } from '../../pages/server-search.page';
|
||||
|
||||
@@ -15,7 +18,7 @@ test.describe('Plugin manager UI', () => {
|
||||
await test.step('Register user and create server context', async () => {
|
||||
await register.goto();
|
||||
await register.register(`plugin_${suffix}`, 'Plugin Tester', 'TestPass123!');
|
||||
await expect(page.getByPlaceholder('Search people, servers, or paste an invite...')).toBeVisible({ timeout: 30_000 });
|
||||
await expectDashboardReady(page);
|
||||
await search.createServer(`Plugin API Server ${suffix}`, {
|
||||
description: 'Plugin manager UI E2E coverage'
|
||||
});
|
||||
@@ -23,16 +26,13 @@ test.describe('Plugin manager UI', () => {
|
||||
await expect(page).toHaveURL(/\/room\//, { timeout: 30_000 });
|
||||
});
|
||||
|
||||
await test.step('Open visible Plugin Store button', async () => {
|
||||
await page.getByRole('button', { name: 'Plugin Store' }).click();
|
||||
await expect(page).toHaveURL(/\/plugin-store/, { timeout: 10_000 });
|
||||
await test.step('Open Plugin Store from the title-bar menu', async () => {
|
||||
await openPluginStore(page);
|
||||
await expect(page.getByTestId('plugin-store-page')).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
await test.step('Install fixture plugin from source manifest', async () => {
|
||||
await page.getByLabel('Plugin source manifest URL').fill('http://localhost:4200/plugins/e2e-plugin-source.json');
|
||||
await page.getByRole('button', { name: 'Add Source' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'E2E All API Plugin' })).toBeVisible({ timeout: 15_000 });
|
||||
await addPluginSource(page);
|
||||
const pluginCard = page.locator('article', { hasText: 'E2E All API Plugin' });
|
||||
|
||||
await pluginCard.getByRole('button', { name: 'Readme' }).click();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from '../../fixtures/multi-client';
|
||||
import { expectDashboardReady } from '../../helpers/dashboard';
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
import { ServerSearchPage } from '../../pages/server-search.page';
|
||||
import { ChatRoomPage } from '../../pages/chat-room.page';
|
||||
@@ -88,7 +89,7 @@ test.describe('Connectivity warning', () => {
|
||||
|
||||
await register.goto();
|
||||
await register.register(`alice_${suffix}`, 'Alice', 'TestPass123!');
|
||||
await expect(alice.page.getByPlaceholder('Search people, servers, or paste an invite...')).toBeVisible({ timeout: 30_000 });
|
||||
await expectDashboardReady(alice.page);
|
||||
});
|
||||
|
||||
await test.step('Register Bob', async () => {
|
||||
@@ -96,7 +97,7 @@ test.describe('Connectivity warning', () => {
|
||||
|
||||
await register.goto();
|
||||
await register.register(`bob_${suffix}`, 'Bob', 'TestPass123!');
|
||||
await expect(bob.page.getByPlaceholder('Search people, servers, or paste an invite...')).toBeVisible({ timeout: 30_000 });
|
||||
await expectDashboardReady(bob.page);
|
||||
});
|
||||
|
||||
await test.step('Register Charlie', async () => {
|
||||
@@ -104,7 +105,7 @@ test.describe('Connectivity warning', () => {
|
||||
|
||||
await register.goto();
|
||||
await register.register(`charlie_${suffix}`, 'Charlie', 'TestPass123!');
|
||||
await expect(charlie.page.getByPlaceholder('Search people, servers, or paste an invite...')).toBeVisible({ timeout: 30_000 });
|
||||
await expectDashboardReady(charlie.page);
|
||||
});
|
||||
|
||||
// ── Create server and have everyone join ──
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { test, expect } from '../../fixtures/multi-client';
|
||||
import { openSettingsFromMenu } from '../../helpers/app-menu';
|
||||
import { expectDashboardReady } from '../../helpers/dashboard';
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
|
||||
test.describe('ICE server settings', () => {
|
||||
@@ -9,8 +11,8 @@ test.describe('ICE server settings', () => {
|
||||
|
||||
await register.goto();
|
||||
await register.register(`user_${suffix}`, 'IceTestUser', 'TestPass123!');
|
||||
await expect(page.getByPlaceholder('Search people, servers, or paste an invite...')).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTitle('Settings').click();
|
||||
await expectDashboardReady(page);
|
||||
await openSettingsFromMenu(page);
|
||||
await expect(page.getByRole('button', { name: 'Network' })).toBeVisible({ timeout: 10_000 });
|
||||
await page.getByRole('button', { name: 'Network' }).click();
|
||||
await expect(page.getByTestId('ice-server-settings')).toBeVisible({ timeout: 10_000 });
|
||||
@@ -101,7 +103,7 @@ test.describe('ICE server settings', () => {
|
||||
await expect(page.getByText('stun:persist-test.example.com:3478')).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.getByTitle('Settings').click();
|
||||
await openSettingsFromMenu(page);
|
||||
await expect(page.getByRole('button', { name: 'Network' })).toBeVisible({ timeout: 10_000 });
|
||||
await page.getByRole('button', { name: 'Network' }).click();
|
||||
await expect(page.getByText('stun:persist-test.example.com:3478')).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from '../../fixtures/multi-client';
|
||||
import { expectDashboardReady } from '../../helpers/dashboard';
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
import { ServerSearchPage } from '../../pages/server-search.page';
|
||||
import { ChatRoomPage } from '../../pages/chat-room.page';
|
||||
@@ -89,7 +90,7 @@ test.describe('STUN/TURN fallback behaviour', () => {
|
||||
|
||||
await register.goto();
|
||||
await register.register(`alice_${suffix}`, 'Alice', 'TestPass123!');
|
||||
await expect(alice.page.getByPlaceholder('Search people, servers, or paste an invite...')).toBeVisible({ timeout: 30_000 });
|
||||
await expectDashboardReady(alice.page);
|
||||
});
|
||||
|
||||
await test.step('Register Bob', async () => {
|
||||
@@ -97,7 +98,7 @@ test.describe('STUN/TURN fallback behaviour', () => {
|
||||
|
||||
await register.goto();
|
||||
await register.register(`bob_${suffix}`, 'Bob', 'TestPass123!');
|
||||
await expect(bob.page.getByPlaceholder('Search people, servers, or paste an invite...')).toBeVisible({ timeout: 30_000 });
|
||||
await expectDashboardReady(bob.page);
|
||||
});
|
||||
|
||||
await test.step('Alice creates a server', async () => {
|
||||
|
||||
Reference in New Issue
Block a user