import { type Page } from '@playwright/test'; import { expect, test, type Client } from '../../fixtures/multi-client'; 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 EDITED_MESSAGE = 'Plugin API edited message'; const ORIGINAL_MESSAGE = 'Plugin API original message'; const DELETED_MESSAGE = 'Plugin API deleted message'; const DELETED_MESSAGE_CONTENT = '[Message deleted]'; const PLUGIN_BOT_MESSAGE = 'Plugin bot message from all-api fixture'; const CUSTOM_EMBED_TEXT = 'E2E custom embed: Plugin API custom embed'; const SOUND_BOARD_TEXT = 'E2E soundboard ready'; const SOUND_BOARD_LABEL = 'E2E Soundboard'; const SOUND_BOARD_PLAYED_MESSAGE = 'E2E soundboard played Airhorn to voice channel'; const VOICE_CHANNEL = 'Plugin Voice'; test.describe('Plugin API multi-user runtime', () => { test.describe.configure({ timeout: 180_000 }); 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 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'); }); await test.step('Activate the server plugin for Bob as the embed/soundboard receiver', async () => { await installGrantAndActivatePlugin(scenario.bob.page, false); await closeSettingsModal(scenario.bob.page); await expect(soundboardComposerButton(scenario.bob.page)).toBeVisible({ timeout: 20_000 }); await expect(scenario.bob.page.getByText(SOUND_BOARD_TEXT, { exact: true })).toBeVisible({ timeout: 20_000 }); await expect(scenario.bob.page.getByTestId('e2e-plugin-owned-dom')).toHaveAttribute('data-plugin-owner', 'e2e.all-api-plugin'); }); await test.step('Alice opens the plugin soundboard modal and plays a sound to voice', async () => { await soundboardComposerButton(scenario.alice.page).click(); await expect(scenario.alice.page.getByRole('dialog', { name: SOUND_BOARD_LABEL })).toBeVisible({ timeout: 20_000 }); await expect(scenario.alice.page.getByTestId('e2e-soundboard-modal')).toHaveAttribute('data-plugin-owner', 'e2e.all-api-plugin'); await scenario.alice.page.getByRole('button', { name: 'Play airhorn to voice' }).click(); await expect(scenario.alice.page.getByTestId('e2e-soundboard-status')).toHaveText(SOUND_BOARD_PLAYED_MESSAGE, { timeout: 20_000 }); }); await test.step('Bob receives messages sent and edited by Alice through the plugin API', async () => { await expect(scenario.bobMessages.getMessageItemByText(EDITED_MESSAGE)).toBeVisible({ timeout: 30_000 }); await expect(scenario.bobMessages.getMessageItemByText(ORIGINAL_MESSAGE)).toHaveCount(0); await expect(scenario.bob.page.getByText('(edited)')).toBeVisible({ timeout: 20_000 }); }); await test.step('Bob sees plugin API deletion state and plugin-user messages', async () => { await expect(scenario.bobMessages.getMessageItemByText(DELETED_MESSAGE_CONTENT)).toBeVisible({ timeout: 30_000 }); await expect(scenario.bobMessages.getMessageItemByText(DELETED_MESSAGE)).toHaveCount(0); await expect(scenario.bobMessages.getMessageItemByText(PLUGIN_BOT_MESSAGE)).toBeVisible({ timeout: 30_000 }); await expect(scenario.bobMessages.getMessageItemByText(SOUND_BOARD_PLAYED_MESSAGE)).toBeVisible({ timeout: 30_000 }); }); await test.step('Bob renders Alice custom embed through the plugin embed API', async () => { await expect(scenario.bob.page.getByTestId('plugin-message-embeds')).toContainText(CUSTOM_EMBED_TEXT, { timeout: 30_000 }); }); await test.step('Bob sees Alice profile name changed by the plugin API', async () => { await expect(scenario.bobMessages.getMessageItemByText(EDITED_MESSAGE)).toContainText('Alice Plugin Renamed', { timeout: 30_000 }); }); }); }); interface PluginApiScenario { alice: Client; aliceRoom: ChatRoomPage; bob: Client; bobRoom: ChatRoomPage; aliceMessages: ChatMessagesPage; bobMessages: ChatMessagesPage; } async function createPluginApiScenario(createClient: () => Promise): Promise { const suffix = uniqueName('plugin-api'); const serverName = `Plugin API Server ${suffix}`; const alice = await createClient(); const bob = await createClient(); await registerUser(alice.page, `alice_${suffix}`, 'Alice'); await registerUser(bob.page, `bob_${suffix}`, 'Bob'); const aliceSearch = new ServerSearchPage(alice.page); await aliceSearch.createServer(serverName, { description: 'Two-user plugin API E2E coverage' }); await expect(alice.page).toHaveURL(/\/room\//, { timeout: 30_000 }); const aliceRoom = new ChatRoomPage(alice.page); await aliceRoom.ensureVoiceChannelExists(VOICE_CHANNEL); const bobSearch = new ServerSearchPage(bob.page); await bobSearch.joinServerFromSearch(serverName); await expect(bob.page).toHaveURL(/\/room\//, { timeout: 30_000 }); const bobRoom = new ChatRoomPage(bob.page); await aliceRoom.joinVoiceChannel(VOICE_CHANNEL); await bobRoom.joinVoiceChannel(VOICE_CHANNEL); await expect(aliceRoom.voiceControls).toBeVisible({ timeout: 30_000 }); await expect(bobRoom.voiceControls).toBeVisible({ timeout: 30_000 }); const aliceMessages = new ChatMessagesPage(alice.page); const bobMessages = new ChatMessagesPage(bob.page); await aliceMessages.waitForReady(); await bobMessages.waitForReady(); await expect(alice.page.locator('[data-testid^="room-user-card-"]', { hasText: 'Bob' })).toBeVisible({ timeout: 30_000 }); await expect(bob.page.locator('[data-testid^="room-user-card-"]', { hasText: 'Alice' })).toBeVisible({ timeout: 30_000 }); return { alice, aliceRoom, bob, bobRoom, aliceMessages, bobMessages }; } async function registerUser(page: Page, username: string, displayName: string): Promise { const registerPage = new RegisterPage(page); await registerPage.goto(); await registerPage.register(username, displayName, 'TestPass123!'); await expect(page).toHaveURL(/\/search/, { timeout: 30_000 }); } async function installGrantAndActivatePlugin(page: Page, installFromStore: boolean): Promise { await page.getByRole('button', { name: 'Plugins' }).click(); await expect(page).toHaveURL(/\/plugin-store/, { timeout: 20_000 }); 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 page.getByRole('button', { exact: true, name: /^(Install|Install to Server)$/ }).click(); await expect(page.getByRole('dialog', { name: PLUGIN_TITLE })).toBeVisible({ timeout: 10_000 }); await page.getByRole('button', { name: 'Install and Activate' }).click(); await expect(page.locator('article', { hasText: PLUGIN_TITLE }).getByText('Installed')).toBeVisible({ timeout: 20_000 }); } await page.getByRole('button', { name: 'Manage Plugins' }).click(); await expect(page.getByTestId('plugin-manager')).toBeVisible({ timeout: 20_000 }); await expect(page.locator('article', { hasText: PLUGIN_TITLE })).toBeVisible({ timeout: 20_000 }); await page.locator('article', { hasText: PLUGIN_TITLE }) .getByRole('button', { name: 'Select' }) .click(); await page.getByRole('button', { name: 'Grant all requested' }).click(); await page.getByRole('button', { name: 'Activate ready plugins' }).click(); await expect(page.locator('article', { hasText: PLUGIN_TITLE }).getByText('ready', { exact: true })).toBeVisible({ timeout: 30_000 }); await page.getByRole('button', { name: 'Logs' }).click(); await expect(page.getByText('all-api plugin completed')).toBeVisible({ timeout: 30_000 }); } async function closeSettingsModal(page: Page): Promise { await page.keyboard.press('Escape'); await expect(page.getByTestId('plugin-manager')).toHaveCount(0); } function uniqueName(prefix: string): string { return `${prefix}-${Date.now()}-${Math.random().toString(36) .slice(2, 8)}`; } function soundboardComposerButton(page: Page) { return page.locator('app-chat-message-composer') .getByRole('button', { exact: true, name: SOUND_BOARD_LABEL }); }