Hydrate playable media after disk receive, relay file-announce to sibling devices via account_sync, bind DM attachments to pre-allocated message ids, and improve gallery retry/cancel UX with bounded parallel auto-downloads. Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
6.8 KiB
TypeScript
152 lines
6.8 KiB
TypeScript
import { test, expect } 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';
|
|
import {
|
|
MULTI_DEVICE_PASSWORD,
|
|
loginSecondDeviceIntoServer,
|
|
uniqueMultiDeviceName
|
|
} from '../../helpers/multi-device-session';
|
|
|
|
const SHARED_FROM_DEVICE_TEXT = 'Shared from your device';
|
|
|
|
test.describe('Multi-device attachment sharing', () => {
|
|
test.describe.configure({ timeout: 300_000, retries: 1 });
|
|
|
|
test('only the uploading device claims "Shared from your device"; the second same-user device can request it', async ({
|
|
createClient
|
|
}) => {
|
|
const suffix = uniqueMultiDeviceName('attach-share');
|
|
const credentials = {
|
|
username: `share_${suffix}`,
|
|
displayName: 'Multi Device User',
|
|
password: MULTI_DEVICE_PASSWORD
|
|
};
|
|
const serverName = `Attachment Sharing ${suffix}`;
|
|
const fileName = `${suffix}-handoff.bin`;
|
|
const caption = `Uploaded from device A ${suffix}`;
|
|
const fileAttachment = createBinaryFilePayload(fileName, 'application/octet-stream', `binary-body-${suffix}`);
|
|
const clientA = await createClient();
|
|
const messagesA = new ChatMessagesPage(clientA.page);
|
|
|
|
await test.step('device A registers, creates a server, and uploads a generic file', async () => {
|
|
const registerPage = new RegisterPage(clientA.page);
|
|
|
|
await registerPage.goto();
|
|
await registerPage.register(credentials.username, credentials.displayName, credentials.password);
|
|
await expect(clientA.page).toHaveURL(/\/dashboard/, { timeout: 15_000 });
|
|
|
|
const search = new ServerSearchPage(clientA.page);
|
|
|
|
await search.createServer(serverName, { description: 'Multi-device attachment sharing regression coverage' });
|
|
await expect(clientA.page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
|
|
|
await messagesA.waitForReady();
|
|
await messagesA.attachFiles([fileAttachment]);
|
|
await messagesA.sendMessage(caption);
|
|
await expect(messagesA.getMessageItemByText(caption)).toBeVisible({ timeout: 30_000 });
|
|
});
|
|
|
|
await test.step('device A (the uploader) shows "Shared from your device"', async () => {
|
|
const bubbleA = messagesA.getMessageItemByText(caption);
|
|
|
|
await expect(bubbleA.getByText(fileName, { exact: false })).toBeVisible({ timeout: 20_000 });
|
|
await expect(bubbleA.getByText(SHARED_FROM_DEVICE_TEXT, { exact: false })).toBeVisible({ timeout: 20_000 });
|
|
});
|
|
|
|
const clientB = await createClient();
|
|
const messagesB = new ChatMessagesPage(clientB.page);
|
|
|
|
await test.step('device B (same user) logs into the same server after the upload', async () => {
|
|
await loginSecondDeviceIntoServer(clientB.page, credentials, serverName);
|
|
// Keep device A active so it answers device B's account_sync_peer_online push.
|
|
await clientA.page.bringToFront();
|
|
await messagesA.waitForReady();
|
|
await clientB.page.bringToFront();
|
|
await messagesB.waitForReady();
|
|
});
|
|
|
|
await test.step('device B receives the message and its attachment via same-user account sync', async () => {
|
|
await expect(messagesB.getMessageItemByText(caption)).toBeVisible({ timeout: 90_000 });
|
|
await expect(messagesB.getMessageItemByText(caption).getByText(fileName, { exact: false }))
|
|
.toBeVisible({ timeout: 90_000 });
|
|
});
|
|
|
|
await test.step('device B does NOT claim to share it and can request/download the file', async () => {
|
|
const bubbleB = messagesB.getMessageItemByText(caption);
|
|
|
|
// The regression: device B used to render "Shared from your device" and hide the
|
|
// download affordance because the synced metadata carried the uploader's user id.
|
|
await expect(bubbleB.getByText(SHARED_FROM_DEVICE_TEXT, { exact: false })).toHaveCount(0);
|
|
|
|
// Device B must instead be able to fetch the file as any recipient would.
|
|
const getButton = bubbleB.getByRole('button', { name: /request|download/i });
|
|
|
|
await expect(getButton.first()).toBeVisible({ timeout: 20_000 });
|
|
});
|
|
});
|
|
|
|
test('relays file-announce metadata to a sibling device that is already online during upload', async ({
|
|
createClient
|
|
}) => {
|
|
const suffix = uniqueMultiDeviceName('attach-online');
|
|
const credentials = {
|
|
username: `online_${suffix}`,
|
|
displayName: 'Multi Device User',
|
|
password: MULTI_DEVICE_PASSWORD
|
|
};
|
|
const serverName = `Attachment Online Relay ${suffix}`;
|
|
const fileName = `${suffix}-relay.bin`;
|
|
const caption = `Uploaded while device B was online ${suffix}`;
|
|
const fileAttachment = createBinaryFilePayload(fileName, 'application/octet-stream', `relay-body-${suffix}`);
|
|
const clientA = await createClient();
|
|
const clientB = await createClient();
|
|
const messagesA = new ChatMessagesPage(clientA.page);
|
|
const messagesB = new ChatMessagesPage(clientB.page);
|
|
|
|
await test.step('device A registers and creates a server', async () => {
|
|
const registerPage = new RegisterPage(clientA.page);
|
|
|
|
await registerPage.goto();
|
|
await registerPage.register(credentials.username, credentials.displayName, credentials.password);
|
|
await expect(clientA.page).toHaveURL(/\/dashboard/, { timeout: 15_000 });
|
|
|
|
const search = new ServerSearchPage(clientA.page);
|
|
|
|
await search.createServer(serverName, { description: 'Sibling online file-announce relay coverage' });
|
|
await expect(clientA.page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
|
await messagesA.waitForReady();
|
|
});
|
|
|
|
await test.step('device B logs into the same server before the upload starts', async () => {
|
|
await loginSecondDeviceIntoServer(clientB.page, credentials, serverName);
|
|
await clientA.page.bringToFront();
|
|
await messagesA.waitForReady();
|
|
await clientB.page.bringToFront();
|
|
await messagesB.waitForReady();
|
|
});
|
|
|
|
await test.step('device A uploads while device B is already in the room', async () => {
|
|
await clientA.page.bringToFront();
|
|
await messagesA.attachFiles([fileAttachment]);
|
|
await messagesA.sendMessage(caption);
|
|
await expect(messagesA.getMessageItemByText(caption)).toBeVisible({ timeout: 30_000 });
|
|
});
|
|
|
|
await test.step('device B learns attachment metadata without a server-rail click dance', async () => {
|
|
await clientB.page.bringToFront();
|
|
await expect(messagesB.getMessageItemByText(caption)).toBeVisible({ timeout: 90_000 });
|
|
await expect(messagesB.getMessageItemByText(caption).getByText(fileName, { exact: false }))
|
|
.toBeVisible({ timeout: 90_000 });
|
|
});
|
|
});
|
|
});
|
|
|
|
function createBinaryFilePayload(name: string, mimeType: string, content: string): ChatDropFilePayload {
|
|
return {
|
|
name,
|
|
mimeType,
|
|
base64: Buffer.from(content, 'utf8').toString('base64')
|
|
};
|
|
}
|