Files
Toju/e2e/tests/chat/attachment-only-message-grouping.spec.ts

126 lines
5.2 KiB
TypeScript

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<Client>): Promise<SingleClientChatScenario> {
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 = [
'<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120" viewBox="0 0 160 120">',
'<rect width="160" height="120" rx="18" fill="#0f172a" />',
'<circle cx="38" cy="36" r="18" fill="#38bdf8" />',
`<text x="24" y="104" fill="#e2e8f0" font-size="12" font-family="Arial, sans-serif">${name}</text>`,
'</svg>'
].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)}`;
}