import { test, expect, type Client } from '../../fixtures/multi-client'; import { RegisterPage } from '../../pages/register.page'; import { ServerSearchPage } from '../../pages/server-search.page'; import { ChatMessagesPage, type ChatDropFilePayload } from '../../pages/chat-messages.page'; /** * Regression coverage for: "Video attachment on android gets sent in the * message bubble above with no preview image." * * Root cause was platform-agnostic: caption-less media was bound to a message * re-discovered by matching `content` (always '' for attachment-only sends), * which raced the async create-effect and grouped a second attachment onto the * previous bubble - leaving an empty message behind. The fix pre-allocates the * message id, dispatches it, and binds attachments to that exact id. This test * proves each caption-less attachment lands in its own bubble and renders. */ test.describe('Attachment-only message grouping', () => { test.describe.configure({ timeout: 180_000 }); test('each caption-less attachment keeps its own message bubble and preview', async ({ createClient }) => { const scenario = await createSingleClientChatScenario(createClient); const serverName = `Attachment Group ${uniqueName('srv')}`; const introText = `Intro line ${uniqueName('intro')}`; const firstImageName = `${uniqueName('first')}.svg`; const secondImageName = `${uniqueName('second')}.svg`; const firstImage = createSvgFilePayload(firstImageName); const secondImage = createSvgFilePayload(secondImageName); await test.step('Create a server and open its room', async () => { await scenario.search.createServer(serverName, { description: 'Attachment grouping regression server' }); await expect(scenario.client.page).toHaveURL(/\/room\//, { timeout: 15_000 }); await scenario.messages.waitForReady(); }); await test.step('Send a normal text message first', async () => { await scenario.messages.sendMessage(introText); await expect(scenario.messages.getMessageItemByText(introText)).toBeVisible({ timeout: 20_000 }); }); await test.step('Send two caption-less attachments back-to-back', async () => { // Fire them rapidly (no render wait between) to mirror the reported // rapid-upload repro and stress the message-create vs. attach ordering. await scenario.messages.attachFiles([firstImage]); await scenario.messages.sendPendingAttachments(); await scenario.messages.attachFiles([secondImage]); await scenario.messages.sendPendingAttachments(); await scenario.messages.expectMessageImageLoaded(firstImageName); await scenario.messages.expectMessageImageLoaded(secondImageName); }); await test.step('Each attachment lives in its own bubble (no grouping, no blank message)', async () => { const firstMessageId = await scenario.messages.getMessageIdContainingImage(firstImageName); const secondMessageId = await scenario.messages.getMessageIdContainingImage(secondImageName); expect(firstMessageId).toBeTruthy(); expect(secondMessageId).toBeTruthy(); // The bug grouped both onto one bubble; distinct ids prove they did not. expect(firstMessageId).not.toBe(secondMessageId); // Exactly two bubbles carry an image, and neither carries both. await expect(scenario.messages.messageItems.filter({ has: scenario.client.page.locator('img[alt$=".svg"]') })) .toHaveCount(2, { timeout: 20_000 }); await expect(scenario.messages.getMessageItemContainingImage(firstImageName).locator('img[alt$=".svg"]')) .toHaveCount(1); await expect(scenario.messages.getMessageItemContainingImage(secondImageName).locator('img[alt$=".svg"]')) .toHaveCount(1); }); }); }); interface SingleClientChatScenario { client: Client; messages: ChatMessagesPage; search: ServerSearchPage; } async function createSingleClientChatScenario(createClient: () => Promise): Promise { const suffix = uniqueName('solo'); const client = await createClient(); const credentials = { username: `solo_${suffix}`, displayName: 'Solo', password: 'TestPass123!' }; const registerPage = new RegisterPage(client.page); await registerPage.goto(); await registerPage.register(credentials.username, credentials.displayName, credentials.password); await expect(client.page).toHaveURL(/\/dashboard/, { timeout: 15_000 }); return { client, messages: new ChatMessagesPage(client.page), search: new ServerSearchPage(client.page) }; } function createSvgFilePayload(name: string): ChatDropFilePayload { const markup = [ '', '', '', `${name}`, '' ].join(''); return { name, mimeType: 'image/svg+xml', base64: Buffer.from(markup, 'utf8').toString('base64') }; } function uniqueName(prefix: string): string { return `${prefix}-${Date.now()}-${Math.random().toString(36) .slice(2, 8)}`; }