fix: Bug - Video attachment on android gets sent in the message bubble above with no preview image
This commit is contained in:
@@ -94,6 +94,25 @@ export class ChatMessagesPage {
|
||||
}, files);
|
||||
}
|
||||
|
||||
/** Sends the currently-attached files with no text caption (attachment-only message). */
|
||||
async sendPendingAttachments(): Promise<void> {
|
||||
await this.waitForReady();
|
||||
await expect(this.sendButton).toBeEnabled({ timeout: 10_000 });
|
||||
await this.sendButton.click();
|
||||
}
|
||||
|
||||
/** The message bubble that contains the rendered image with the given alt text. */
|
||||
getMessageItemContainingImage(altText: string): Locator {
|
||||
return this.messageItems.filter({
|
||||
has: this.page.locator(`img[alt="${altText}"]`)
|
||||
}).last();
|
||||
}
|
||||
|
||||
/** Resolves the stable data-message-id of the bubble holding the given image. */
|
||||
async getMessageIdContainingImage(altText: string): Promise<string | null> {
|
||||
return this.getMessageItemContainingImage(altText).getAttribute('data-message-id');
|
||||
}
|
||||
|
||||
async openGifPicker(): Promise<void> {
|
||||
await this.waitForReady();
|
||||
await this.gifButton.click();
|
||||
|
||||
125
e2e/tests/chat/attachment-only-message-grouping.spec.ts
Normal file
125
e2e/tests/chat/attachment-only-message-grouping.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
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)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user