126 lines
5.2 KiB
TypeScript
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)}`;
|
|
}
|