fix: improve plugins functionality with server management
This commit is contained in:
@@ -69,6 +69,7 @@ function applySeededEndpointStorageState(storageState: SeededEndpointStorageStat
|
||||
'toju-primary',
|
||||
'toju-sweden'
|
||||
]));
|
||||
|
||||
storage.setItem('metoyou_general_settings', generalSettings);
|
||||
|
||||
if (currentUserId) {
|
||||
|
||||
@@ -10,7 +10,9 @@ export class LoginPage {
|
||||
readonly registerLink: Locator;
|
||||
|
||||
constructor(private page: Page) {
|
||||
this.form = page.locator('#login-username').locator('xpath=ancestor::div[contains(@class, "space-y-3")]').first();
|
||||
this.form = page.locator('#login-username').locator('xpath=ancestor::div[contains(@class, "space-y-3")]')
|
||||
.first();
|
||||
|
||||
this.usernameInput = page.locator('#login-username');
|
||||
this.passwordInput = page.locator('#login-password');
|
||||
this.serverSelect = page.locator('#login-server');
|
||||
|
||||
@@ -79,12 +79,19 @@ export class ServerSearchPage {
|
||||
await this.page.getByRole('button', { name }).click();
|
||||
}
|
||||
|
||||
async joinServerFromSearch(name: string) {
|
||||
async joinServerFromSearch(name: string, options: { acceptPluginDownloads?: boolean } = {}) {
|
||||
await this.searchInput.fill(name);
|
||||
|
||||
const serverCard = this.page.locator('div[title]', { hasText: name }).first();
|
||||
|
||||
await expect(serverCard).toBeVisible({ timeout: 15_000 });
|
||||
await serverCard.dblclick();
|
||||
|
||||
if (options.acceptPluginDownloads) {
|
||||
const pluginConsentDialog = this.page.getByRole('dialog', { name: /uses plugins/ });
|
||||
|
||||
await expect(pluginConsentDialog).toBeVisible({ timeout: 20_000 });
|
||||
await pluginConsentDialog.getByRole('button', { name: 'Accept and join' }).click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,7 @@ interface PersistentClient {
|
||||
userDataDir: string;
|
||||
}
|
||||
|
||||
const CLIENT_LAUNCH_ARGS = [
|
||||
'--use-fake-device-for-media-stream',
|
||||
'--use-fake-ui-for-media-stream'
|
||||
];
|
||||
const CLIENT_LAUNCH_ARGS = ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'];
|
||||
|
||||
test.describe('User session data isolation', () => {
|
||||
test.describe.configure({ timeout: 240_000 });
|
||||
@@ -43,6 +40,7 @@ test.describe('User session data isolation', () => {
|
||||
};
|
||||
const aliceServerName = `Alice Session Server ${suffix}`;
|
||||
const aliceMessage = `Alice persisted message ${suffix}`;
|
||||
|
||||
let client: PersistentClient | null = null;
|
||||
|
||||
try {
|
||||
@@ -82,6 +80,7 @@ test.describe('User session data isolation', () => {
|
||||
const bobServerName = `Bob Private Server ${suffix}`;
|
||||
const aliceMessage = `Alice history ${suffix}`;
|
||||
const bobMessage = `Bob history ${suffix}`;
|
||||
|
||||
let client: PersistentClient | null = null;
|
||||
|
||||
try {
|
||||
@@ -136,7 +135,7 @@ async function launchPersistentClient(userDataDir: string, testServerPort: numbe
|
||||
|
||||
await installTestServerEndpoint(context, testServerPort);
|
||||
|
||||
const page = context.pages()[0] ?? await context.newPage();
|
||||
const page = context.pages()[0] ?? (await context.newPage());
|
||||
|
||||
return {
|
||||
context,
|
||||
@@ -202,6 +201,7 @@ async function createServerAndSendMessage(page: Page, serverName: string, messag
|
||||
await searchPage.createServer(serverName, {
|
||||
description: `User session isolation coverage for ${serverName}`
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
||||
|
||||
await messagesPage.sendMessage(messageText);
|
||||
@@ -209,11 +209,15 @@ async function createServerAndSendMessage(page: Page, serverName: string, messag
|
||||
}
|
||||
|
||||
async function expectSavedRoomAndHistory(page: Page, roomName: string, messageText: string): Promise<void> {
|
||||
const roomButton = getSavedRoomButton(page, roomName);
|
||||
const railRoomButton = getRailSavedRoomButton(page, roomName);
|
||||
const messagesPage = new ChatMessagesPage(page);
|
||||
|
||||
await expect(roomButton).toBeVisible({ timeout: 20_000 });
|
||||
await roomButton.click();
|
||||
await expect(railRoomButton).toBeVisible({ timeout: 20_000 });
|
||||
await page.goto('/search', { waitUntil: 'domcontentloaded' });
|
||||
const searchRoomButton = getSearchSavedRoomButton(page, roomName);
|
||||
|
||||
await expect(searchRoomButton).toBeVisible({ timeout: 20_000 });
|
||||
await searchRoomButton.click();
|
||||
await expect(page).toHaveURL(/\/room\//, { timeout: 20_000 });
|
||||
await expect(messagesPage.getMessageItemByText(messageText)).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
@@ -230,17 +234,29 @@ async function expectBlankSlate(page: Page, hiddenRoomNames: string[]): Promise<
|
||||
}
|
||||
|
||||
async function expectSavedRoomVisible(page: Page, roomName: string): Promise<void> {
|
||||
await expect(getSavedRoomButton(page, roomName)).toBeVisible({ timeout: 20_000 });
|
||||
await expect(getRailSavedRoomButton(page, roomName)).toBeVisible({ timeout: 20_000 });
|
||||
await page.goto('/search', { waitUntil: 'domcontentloaded' });
|
||||
await expect(getSearchSavedRoomButton(page, roomName)).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
async function expectSavedRoomHidden(page: Page, roomName: string): Promise<void> {
|
||||
await expect(getSavedRoomButton(page, roomName)).toHaveCount(0);
|
||||
await expect(getRailSavedRoomButton(page, roomName)).toHaveCount(0);
|
||||
|
||||
if (!page.url().includes('/search')) {
|
||||
await page.goto('/search', { waitUntil: 'domcontentloaded' });
|
||||
}
|
||||
|
||||
await expect(getSearchSavedRoomButton(page, roomName)).toHaveCount(0);
|
||||
}
|
||||
|
||||
function getSavedRoomButton(page: Page, roomName: string) {
|
||||
function getRailSavedRoomButton(page: Page, roomName: string) {
|
||||
return page.locator(`button[title="${roomName}"]`).first();
|
||||
}
|
||||
|
||||
function getSearchSavedRoomButton(page: Page, roomName: string) {
|
||||
return page.locator('app-server-search').getByRole('button', { name: roomName, exact: true });
|
||||
}
|
||||
|
||||
async function retryTransientNavigation<T>(navigate: () => Promise<T>, attempts = 4): Promise<T> {
|
||||
let lastError: unknown;
|
||||
|
||||
@@ -259,11 +275,10 @@ async function retryTransientNavigation<T>(navigate: () => Promise<T>, attempts
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError instanceof Error
|
||||
? lastError
|
||||
: new Error(`Navigation failed after ${attempts} attempts`);
|
||||
throw lastError instanceof Error ? lastError : new Error(`Navigation failed after ${attempts} attempts`);
|
||||
}
|
||||
|
||||
function uniqueName(prefix: string): string {
|
||||
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36)
|
||||
.slice(2, 8)}`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { chromium, type BrowserContext, type Locator, type Page, type Route } from '@playwright/test';
|
||||
import {
|
||||
chromium,
|
||||
type BrowserContext,
|
||||
type Locator,
|
||||
type Page,
|
||||
type Route
|
||||
} from '@playwright/test';
|
||||
import { test, expect } from '../../fixtures/multi-client';
|
||||
import { installTestServerEndpoint } from '../../helpers/seed-test-endpoint';
|
||||
import { installWebRTCTracking } from '../../helpers/webrtc-helpers';
|
||||
@@ -31,7 +37,11 @@ interface PersistentClient {
|
||||
}
|
||||
|
||||
const STATIC_GIF_BASE64 = 'R0lGODlhAQABAPAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
|
||||
const GIF_FRAME_MARKER = Buffer.from([0x21, 0xf9, 0x04]);
|
||||
const GIF_FRAME_MARKER = Buffer.from([
|
||||
0x21,
|
||||
0xf9,
|
||||
0x04
|
||||
]);
|
||||
const CLIENT_LAUNCH_ARGS = ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'];
|
||||
const SERVER_ICON_SYNC_TIMEOUT_MS = 45_000;
|
||||
|
||||
@@ -77,6 +87,7 @@ test.describe('Server icon sync', () => {
|
||||
await new ServerSearchPage(alice.page).createServer(serverName, {
|
||||
description: 'Server icon synchronization E2E coverage'
|
||||
});
|
||||
|
||||
await expect(alice.page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
||||
|
||||
await joinServerFromSearch(bob.page, serverName);
|
||||
@@ -263,15 +274,15 @@ async function openServerSettings(page: Page, serverName: string): Promise<void>
|
||||
|
||||
async function openSettingsModalThroughAngularDevMode(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
type SettingsModalComponentHandle = {
|
||||
interface SettingsModalComponentHandle {
|
||||
modal?: {
|
||||
open: (page: string) => void;
|
||||
};
|
||||
};
|
||||
type AngularDebugApi = {
|
||||
}
|
||||
interface AngularDebugApi {
|
||||
getComponent: (element: Element) => SettingsModalComponentHandle;
|
||||
applyChanges?: (component: SettingsModalComponentHandle) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const host = document.querySelector('app-settings-modal');
|
||||
const debugApi = (window as Window & { ng?: AngularDebugApi }).ng;
|
||||
@@ -373,33 +384,33 @@ async function retryTransientNavigation<T>(navigate: () => Promise<T>, attempts
|
||||
|
||||
async function expectServerSettingsIcon(page: Page, serverName: string, expectedDataUrl: string): Promise<void> {
|
||||
const settingsPanel = page.locator('app-server-settings');
|
||||
const image = settingsPanel.locator(`img[alt="${serverName} icon"]`).first();
|
||||
const image = settingsPanel.locator('[style*="background-image"]').first();
|
||||
|
||||
await expectImageLoadedWithSrc(image, expectedDataUrl, 'settings server icon');
|
||||
await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'settings server icon');
|
||||
}
|
||||
|
||||
async function expectRoomHeaderIcon(page: Page, serverName: string, expectedDataUrl: string): Promise<void> {
|
||||
const channelsPanel = page.locator('app-rooms-side-panel').first();
|
||||
const image = channelsPanel.locator(`img[alt="${serverName} icon"]`).first();
|
||||
const image = channelsPanel.locator('[style*="background-image"]').first();
|
||||
|
||||
await expectImageLoadedWithSrc(image, expectedDataUrl, 'room header server icon');
|
||||
await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'room header server icon');
|
||||
}
|
||||
|
||||
async function expectRailIcon(page: Page, serverName: string, expectedDataUrl: string): Promise<void> {
|
||||
const image = page.locator(`app-servers-rail img[alt="${serverName} icon"]`).first();
|
||||
const image = page.locator(`app-servers-rail button[title="${serverName}"] [style*="background-image"]`).first();
|
||||
|
||||
await expectImageLoadedWithSrc(image, expectedDataUrl, 'servers rail icon');
|
||||
await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'servers rail icon');
|
||||
}
|
||||
|
||||
async function expectSearchResultIcon(page: Page, serverName: string, expectedDataUrl: string): Promise<void> {
|
||||
const serverCard = page.locator('app-server-search div[title]', { hasText: serverName }).first();
|
||||
const image = serverCard.locator(`img[alt="${serverName} icon"]`).first();
|
||||
const image = serverCard.locator('[style*="background-image"]').first();
|
||||
|
||||
await expect(serverCard).toBeVisible({ timeout: 20_000 });
|
||||
await expectImageLoadedWithSrc(image, expectedDataUrl, 'search result server icon');
|
||||
await expectBackgroundImageLoadedWithUrl(image, expectedDataUrl, 'search result server icon');
|
||||
}
|
||||
|
||||
async function expectImageLoadedWithSrc(image: Locator, expectedDataUrl: string, label: string): Promise<void> {
|
||||
async function expectBackgroundImageLoadedWithUrl(image: Locator, expectedDataUrl: string, label: string): Promise<void> {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
@@ -407,14 +418,14 @@ async function expectImageLoadedWithSrc(image: Locator, expectedDataUrl: string,
|
||||
return null;
|
||||
}
|
||||
|
||||
return image.getAttribute('src');
|
||||
return image.evaluate((element) => getComputedStyle(element).backgroundImage);
|
||||
},
|
||||
{
|
||||
timeout: SERVER_ICON_SYNC_TIMEOUT_MS,
|
||||
message: `${label} src should update`
|
||||
message: `${label} background should update`
|
||||
}
|
||||
)
|
||||
.toBe(expectedDataUrl);
|
||||
.toContain(expectedDataUrl);
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
@@ -423,11 +434,23 @@ async function expectImageLoadedWithSrc(image: Locator, expectedDataUrl: string,
|
||||
return false;
|
||||
}
|
||||
|
||||
return image.evaluate((element) => {
|
||||
const img = element as HTMLImageElement;
|
||||
return image.evaluate(
|
||||
(element) =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
const backgroundImage = getComputedStyle(element).backgroundImage;
|
||||
const match = /^url\("?(.*?)"?\)$/.exec(backgroundImage);
|
||||
const img = new Image();
|
||||
|
||||
return img.complete && img.naturalWidth > 0 && img.naturalHeight > 0;
|
||||
});
|
||||
if (!match?.[1]) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
img.onload = () => resolve(img.naturalWidth > 0 && img.naturalHeight > 0);
|
||||
img.onerror = () => resolve(false);
|
||||
img.src = match[1];
|
||||
})
|
||||
);
|
||||
},
|
||||
{
|
||||
timeout: SERVER_ICON_SYNC_TIMEOUT_MS,
|
||||
@@ -448,8 +471,22 @@ function buildGifUpload(label: string): ImageUploadPayload {
|
||||
const header = baseGif.subarray(0, frameStart);
|
||||
const frame = baseGif.subarray(frameStart, baseGif.length - 1);
|
||||
const commentData = Buffer.from(label, 'ascii');
|
||||
const commentExtension = Buffer.concat([Buffer.from([0x21, 0xfe, commentData.length]), commentData, Buffer.from([0x00])]);
|
||||
const buffer = Buffer.concat([header, commentExtension, frame, Buffer.from([0x3b])]);
|
||||
const commentExtension = Buffer.concat([
|
||||
Buffer.from([
|
||||
0x21,
|
||||
0xfe,
|
||||
commentData.length
|
||||
]),
|
||||
commentData,
|
||||
Buffer.from([0x00])
|
||||
]);
|
||||
const buffer = Buffer.concat([
|
||||
header,
|
||||
commentExtension,
|
||||
frame,
|
||||
frame,
|
||||
Buffer.from([0x3b])
|
||||
]);
|
||||
const base64 = buffer.toString('base64');
|
||||
|
||||
return {
|
||||
@@ -461,5 +498,6 @@ function buildGifUpload(label: string): ImageUploadPayload {
|
||||
}
|
||||
|
||||
function uniqueName(prefix: string): string {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36)
|
||||
.slice(2, 8)}`;
|
||||
}
|
||||
|
||||
@@ -28,9 +28,7 @@ test.describe('Plugin API multi-user runtime', () => {
|
||||
test('runs chat, embed, soundboard, and profile APIs between two users', async ({ createClient }) => {
|
||||
const scenario = await createPluginApiScenario(createClient);
|
||||
|
||||
await test.step('Install the server plugin as Alice', async () => {
|
||||
await installGrantAndActivatePlugin(scenario.alice.page, true);
|
||||
await closeSettingsModal(scenario.alice.page);
|
||||
await test.step('Alice has the server plugin active', async () => {
|
||||
await expect(soundboardComposerButton(scenario.alice.page)).toBeVisible({ timeout: 20_000 });
|
||||
await expect(scenario.alice.page.getByText(SOUND_BOARD_TEXT, { exact: true })).toBeVisible({ timeout: 20_000 });
|
||||
await expect(scenario.alice.page.getByTestId('e2e-plugin-owned-dom')).toHaveAttribute('data-plugin-owner', 'e2e.all-api-plugin');
|
||||
@@ -101,10 +99,13 @@ 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);
|
||||
await bobSearch.joinServerFromSearch(serverName, { acceptPluginDownloads: true });
|
||||
await expect(bob.page).toHaveURL(/\/room\//, { timeout: 30_000 });
|
||||
|
||||
const bobRoom = new ChatRoomPage(bob.page);
|
||||
|
||||
Reference in New Issue
Block a user