diff --git a/playwright-tests/helpers/attachmentMenu.js b/playwright-tests/helpers/attachmentMenu.js new file mode 100644 index 0000000..c8af0cb --- /dev/null +++ b/playwright-tests/helpers/attachmentMenu.js @@ -0,0 +1,53 @@ +const { expect } = require('@playwright/test'); + +class AttachmentMenu { + static async open(attachmentLink) { + await attachmentLink.scrollIntoViewIfNeeded(); + await attachmentLink.evaluate(AttachmentMenu.waitForScrollSettled); + await attachmentLink.click(); + + const saveOption = attachmentLink.page().locator( + '#imageAttachmentContextMenu .context-menu-option[data-action="save"]' + ); + await expect(saveOption).toBeVisible(); + return saveOption; + } + + // Runs in the page: allow queued scroll events and layout changes to settle + // before opening a menu that intentionally closes whenever its container scrolls. + static waitForScrollSettled(attachment) { + const container = attachment.closest('.messages-container'); + if (!container) throw new Error('Attachment is not inside the message container'); + + return new Promise((resolve, reject) => { + let frame; + let previousPosition; + let stableSince = performance.now(); + const timeout = setTimeout(() => { + cancelAnimationFrame(frame); + reject(new Error('Attachment scrolling did not settle within 5 seconds')); + }, 5_000); + + const sample = (now) => { + const rect = attachment.getBoundingClientRect(); + const position = [ + container.scrollTop, container.scrollLeft, + rect.top, rect.left, rect.width, rect.height + ].join(','); + if (position !== previousPosition) { + previousPosition = position; + stableSince = now; + } + if (now - stableSince >= 100) { + clearTimeout(timeout); + resolve(); + return; + } + frame = requestAnimationFrame(sample); + }; + frame = requestAnimationFrame(sample); + }); + } +} + +module.exports = { AttachmentMenu }; diff --git a/playwright-tests/tests/attachments.e2e.test.js b/playwright-tests/tests/attachments.e2e.test.js index f9689cb..cb8c02d 100644 --- a/playwright-tests/tests/attachments.e2e.test.js +++ b/playwright-tests/tests/attachments.e2e.test.js @@ -1,6 +1,7 @@ const { test, expect } = require('../fixtures/newUserFixture'); const { createAndSignInUser, generateUsername } = require('../helpers/userHelpers'); const { newContext } = require('../helpers/toastHelpers'); +const { AttachmentMenu } = require('../helpers/attachmentMenu'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); @@ -553,16 +554,11 @@ test.describe('File Attachment Tests', () => { const attachmentLink = recipientPage.locator('.message.received .attachment-label', { hasText: attachment.fileName }); await expect(attachmentLink).toBeVisible({ timeout: 15000 }); - // Set up download listener for this specific attachment - const downloadPromise = waitForNamedDownload(recipientPage, attachment.fileName); - - // Click to download - await attachmentLink.click(); - await expect(recipientPage.locator('#imageAttachmentContextMenu .context-menu-option[data-action="save"]')).toBeVisible(); - await recipientPage.click('#imageAttachmentContextMenu .context-menu-option[data-action="save"]'); - - // Wait for download to start - const download = await downloadPromise; + const saveOption = await AttachmentMenu.open(attachmentLink); + const [download] = await Promise.all([ + waitForNamedDownload(recipientPage, attachment.fileName), + saveOption.click(), + ]); expect(download.suggestedFilename()).toBe(attachment.fileName); // Define path for the downloaded file with unique ID to avoid conflicts