diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index 88ec8080aab..f650178e950 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -875,7 +875,8 @@ const MessageTimelineBase = React.forwardRef< )} - {!isAtBottom ? ( + {/* Physical bottom may be the frozen tail, not the newest message. */} + {!isAtBottom || bufferedTimeline.pendingCount > 0 ? (
window.getSelection()?.toString()), + ).toBe("@Scout hello"); + await page.keyboard.insertText("@Scout"); + await page.getByTestId(`mention-suggestion-${FIRST}`).click(); + await page.keyboard.type("hello"); + await expect(input).toHaveText("@Scout hello"); + await page.getByTestId("send-message").click(); + await expect + .poll(() => recipients(page, "@Scout hello")) + .toEqual([[FIRST]]); + await expect(input).toHaveText(""); + } }); } diff --git a/desktop/tests/e2e/messaging.spec.ts b/desktop/tests/e2e/messaging.spec.ts index 67641d1c38e..66f3f6ccc12 100644 --- a/desktop/tests/e2e/messaging.spec.ts +++ b/desktop/tests/e2e/messaging.spec.ts @@ -12,6 +12,25 @@ const LINK_PREVIEW_IMAGE = readFileSync( ); const LINK_PREVIEW_IMAGE_DATA_URL = `data:image/png;base64,${LINK_PREVIEW_IMAGE.toString("base64")}`; +async function replaceThreadEdit( + input: Locator, + source: string, + replacement: string, +) { + // The Edit action must seed this scoped composer and deliver its initial focus. + // Focus is not an idle fence: later PM focus callbacks project the current + // selection. Mod+A commits AllSelection through Tiptap, unlike DOM-only fill. + await expect(input).toHaveText(source); + await expect(input).toBeFocused(); + await input.press("ControlOrMeta+a"); + expect(await input.evaluate(() => window.getSelection()?.toString())).toBe( + source, + ); + await input.page().keyboard.insertText(replacement); + // Observe the first replacement before any refused-navigation action. + expect(await input.textContent()).toBe(replacement); +} + async function waitForReadyComposerSnapshots( page: import("@playwright/test").Page, count = 1, @@ -1707,13 +1726,12 @@ test("send multiple messages in sequence", async ({ page }) => { test("copy a rendered code block and paste it back as code", async ({ page, }) => { - await page.context().grantPermissions(["clipboard-read", "clipboard-write"], { - origin: "http://127.0.0.1:4173", - }); - const code = "# not a heading\nconst answer = 42;\n indented();"; await page.goto("/"); + await page.context().grantPermissions(["clipboard-read", "clipboard-write"], { + origin: new URL(page.url()).origin, + }); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); @@ -1750,16 +1768,15 @@ test("copy a rendered code block and paste it back as code", async ({ test("pasting a long copied code block scrolls composer to cursor", async ({ page, }) => { - await page.context().grantPermissions(["clipboard-read", "clipboard-write"], { - origin: "http://127.0.0.1:4173", - }); - const longCode = Array.from( { length: 48 }, (_, index) => `const line${index} = ${index};`, ).join("\n"); await page.goto("/"); + await page.context().grantPermissions(["clipboard-read", "clipboard-write"], { + origin: new URL(page.url()).origin, + }); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); @@ -1794,11 +1811,10 @@ test("pasting a long copied code block scrolls composer to cursor", async ({ test("code block shows language label when language is specified", async ({ page, }) => { + await page.goto("/"); await page.context().grantPermissions(["clipboard-read", "clipboard-write"], { - origin: "http://127.0.0.1:4173", + origin: new URL(page.url()).origin, }); - - await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); @@ -2434,285 +2450,354 @@ test("send message to DM channel p-tags the recipient", async ({ page }) => { .toContainEqual(["p", TEST_IDENTITIES.alice.pubkey]); }); -test("sends a thread message to its parent channel with a root-thread link", async ({ - page, -}) => { - const timestamp = Date.now(); - const rootContent = `🧵 Share source thread ${timestamp}`; - const priorChannelMessage = `Prior channel message ${timestamp}`; - const replySummary = `Share this reply ${timestamp}`; - const attachmentSha = "d".repeat(64); - const attachmentUrl = `http://localhost:3000/media/${attachmentSha}.txt`; - const customEmojiUrl = "https://example.com/send-to-channel-party.svg"; - const previewUrl = "https://github.com/block/buzz/pull/5305"; - const ownReplyContent = [ - `${replySummary} with @alice :party:`, - `[launch-notes.txt](${attachmentUrl})`, - previewUrl, - ].join("\n\n"); - const imetaTag = [ - "imeta", - `url ${attachmentUrl}`, - "m text/plain", - `x ${attachmentSha}`, - "size 42", - "filename launch-notes.txt", - ]; - const emojiTag = ["emoji", "party", customEmojiUrl]; - const mentionTag = ["mention", TEST_IDENTITIES.alice.pubkey]; - const linkPreviewTag = [ - "link-preview", - "snapshot", - "1", - previewUrl, - "Add Send to channel for thread messages", - "GitHub", - "A shared link preview preserved from the source thread message.", - "", - "", - "", - "", - ]; +for (const recovery of [false, true]) { + test( + recovery + ? "wheel return exposes stranded shared reply recovery before panel close" + : "sends a thread message to its parent channel with a root-thread link", + async ({ page }) => { + const timestamp = Date.now(); + const rootContent = `🧵 Share source thread ${timestamp}`; + const priorChannelMessage = `Prior channel message ${timestamp}`; + const replySummary = `Share this reply ${timestamp}`; + const attachmentSha = "d".repeat(64); + const attachmentUrl = `http://localhost:3000/media/${attachmentSha}.txt`; + const customEmojiUrl = "https://example.com/send-to-channel-party.svg"; + const previewUrl = "https://github.com/block/buzz/pull/5305"; + const ownReplyContent = [ + `${replySummary} with @alice :party:`, + `[launch-notes.txt](${attachmentUrl})`, + previewUrl, + ].join("\n\n"); + const imetaTag = [ + "imeta", + `url ${attachmentUrl}`, + "m text/plain", + `x ${attachmentSha}`, + "size 42", + "filename launch-notes.txt", + ]; + const emojiTag = ["emoji", "party", customEmojiUrl]; + const mentionTag = ["mention", TEST_IDENTITIES.alice.pubkey]; + const linkPreviewTag = [ + "link-preview", + "snapshot", + "1", + previewUrl, + "Add Send to channel for thread messages", + "GitHub", + "A shared link preview preserved from the source thread message.", + "", + "", + "", + "", + ]; + + await page.route(customEmojiUrl, (route) => + route.fulfill({ + body: '', + contentType: "image/svg+xml", + }), + ); - await page.route(customEmojiUrl, (route) => - route.fulfill({ - body: '', - contentType: "image/svg+xml", - }), - ); + await page.goto("/"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + await page.waitForFunction( + () => + typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function" && + (window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({ + channelName: "general", + }) ?? + false), + ); - await page.goto("/"); - await page.getByTestId("channel-general").click(); - await expect(page.getByTestId("chat-title")).toHaveText("general"); - await page.waitForFunction( - () => - typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function" && - (window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({ - channelName: "general", - }) ?? - false), - ); + const { ownReplyId, rootId } = await page.evaluate( + ({ alicePubkey, ownReply, root, semanticTags }) => { + const emit = window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__; + if (!emit) throw new Error("Mock message emitter is unavailable."); + const rootEvent = emit({ + channelName: "general", + content: root, + pubkey: alicePubkey, + }); + const ownReplyEvent = emit({ + channelName: "general", + content: ownReply, + extraTags: semanticTags, + mentionPubkeys: [alicePubkey], + parentEventId: rootEvent.id, + }); + return { + ownReplyId: ownReplyEvent.id, + rootId: rootEvent.id, + }; + }, + { + alicePubkey: TEST_IDENTITIES.alice.pubkey, + ownReply: ownReplyContent, + root: rootContent, + semanticTags: [imetaTag, emojiTag, mentionTag, linkPreviewTag], + }, + ); - const { ownReplyId, rootId } = await page.evaluate( - ({ alicePubkey, ownReply, root, semanticTags }) => { - const emit = window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__; - if (!emit) throw new Error("Mock message emitter is unavailable."); - const rootEvent = emit({ - channelName: "general", - content: root, - pubkey: alicePubkey, - }); - const ownReplyEvent = emit({ - channelName: "general", - content: ownReply, - extraTags: semanticTags, - mentionPubkeys: [alicePubkey], - parentEventId: rootEvent.id, + const timeline = page.getByTestId("message-timeline"); + const rootRow = timeline.locator(`[data-message-id="${rootId}"]`); + await expect(rootRow).toContainText(rootContent); + + await page.getByTestId("message-input").fill(priorChannelMessage); + await page.getByTestId("send-message").click(); + const priorChannelRow = timeline + .getByTestId("message-row") + .filter({ hasText: priorChannelMessage }); + await expect(priorChannelRow).toBeVisible(); + await expect( + priorChannelRow.getByTestId("message-send-status"), + ).toHaveCount(0); + + await timeline + .locator( + `[data-testid="message-thread-summary"][data-thread-head-id="${rootId}"]`, + ) + .click(); + const threadPanel = page.getByTestId("message-thread-panel"); + const threadRootRow = threadPanel.locator( + `[data-message-id="${rootId}"]`, + ); + const rootMoreActions = threadRootRow.getByTestId( + `more-actions-${rootId}`, + ); + await threadRootRow.hover(); + await rootMoreActions.click(); + await expect(page.getByRole("menu")).toBeVisible(); + await expect( + page.getByRole("menuitem", { name: "Send to channel" }), + ).toHaveCount(0); + await page.keyboard.press("Escape"); + await expect(page.getByRole("menu")).toHaveCount(0); + + let scrollBox: { x: number; y: number } | null = null; + if (recovery) { + // Read history before sharing, then return with native input rather than + // relying on panel-close reflow to release the semantic tail buffer. + await page.setViewportSize({ width: 1000, height: 500 }); + scrollBox = await timeline.boundingBox(); + if (!scrollBox) throw new Error("Missing timeline geometry"); + await page.mouse.move(scrollBox.x + 40, scrollBox.y + 60); + await page.mouse.wheel(0, -600); + await expect( + page.getByTestId("message-scroll-to-latest"), + ).toBeVisible(); + } + if (!recovery) { + await expect + .poll(() => + timeline.evaluate( + (element) => + element.scrollHeight - element.clientHeight - element.scrollTop, + ), + ) + .toBeLessThanOrEqual(32); + } + const ownReplyRow = threadPanel.locator( + `[data-message-id="${ownReplyId}"]`, + ); + await expect(ownReplyRow).toContainText(replySummary); + await ownReplyRow + .getByTestId(`more-actions-${ownReplyId}`) + .press("Enter"); + const sendToChannelItem = page.getByRole("menuitem", { + name: "Send to channel", }); - return { - ownReplyId: ownReplyEvent.id, - rootId: rootEvent.id, - }; - }, - { - alicePubkey: TEST_IDENTITIES.alice.pubkey, - ownReply: ownReplyContent, - root: rootContent, - semanticTags: [imetaTag, emojiTag, mentionTag, linkPreviewTag], - }, - ); - - const timeline = page.getByTestId("message-timeline"); - const rootRow = timeline.locator(`[data-message-id="${rootId}"]`); - await expect(rootRow).toContainText(rootContent); - - await page.getByTestId("message-input").fill(priorChannelMessage); - await page.getByTestId("send-message").click(); - const priorChannelRow = timeline - .getByTestId("message-row") - .filter({ hasText: priorChannelMessage }); - await expect(priorChannelRow).toBeVisible(); - await expect(priorChannelRow.getByTestId("message-send-status")).toHaveCount( - 0, - ); - - await timeline - .locator( - `[data-testid="message-thread-summary"][data-thread-head-id="${rootId}"]`, - ) - .click(); - const threadPanel = page.getByTestId("message-thread-panel"); - const threadRootRow = threadPanel.locator(`[data-message-id="${rootId}"]`); - const rootMoreActions = threadRootRow.getByTestId(`more-actions-${rootId}`); - await rootMoreActions.click({ force: true }); - await expect(page.getByRole("menu")).toBeVisible(); - await expect( - page.getByRole("menuitem", { name: "Send to channel" }), - ).toHaveCount(0); - await page.keyboard.press("Escape"); - await expect(page.getByRole("menu")).toHaveCount(0); - - const ownReplyRow = threadPanel.locator(`[data-message-id="${ownReplyId}"]`); - await expect(ownReplyRow).toContainText(replySummary); - await ownReplyRow - .getByTestId(`more-actions-${ownReplyId}`) - .click({ force: true }); - const sendToChannelItem = page.getByRole("menuitem", { - name: "Send to channel", - }); - const sendToChannelIcon = sendToChannelItem.getByTestId( - "send-to-channel-icon", - ); - await expect(sendToChannelIcon).toBeVisible(); - await expect(sendToChannelIcon).toHaveAttribute("aria-hidden", "true"); - await expect(sendToChannelIcon).toHaveClass(/lucide-hash-arrow-in/); - await expect - .poll(async () => { - const box = await sendToChannelIcon.boundingBox(); - return box ? [box.width, box.height] : null; - }) - .toEqual([16, 16]); - await sendToChannelItem.click(); - - await expect( - page.locator("[data-sonner-toast]").filter({ hasText: "Sent to channel" }), - ).toBeVisible(); - await expect - .poll(() => - page.evaluate((content) => { - return Boolean( + const sendToChannelIcon = sendToChannelItem.getByTestId( + "send-to-channel-icon", + ); + await expect(sendToChannelIcon).toBeVisible(); + await expect(sendToChannelIcon).toHaveAttribute("aria-hidden", "true"); + await expect(sendToChannelIcon).toHaveClass(/lucide-hash-arrow-in/); + await expect + .poll(async () => { + const box = await sendToChannelIcon.boundingBox(); + return box ? [box.width, box.height] : null; + }) + .toEqual([16, 16]); + await sendToChannelItem.click(); + + await expect( + page + .locator("[data-sonner-toast]") + .filter({ hasText: "Sent to channel" }), + ).toBeVisible(); + await expect + .poll(() => + page.evaluate((content) => { + return Boolean( + (window.__BUZZ_E2E_COMMAND_LOG__ ?? []).findLast( + (entry) => + entry.command === "send_channel_message" && + (entry.payload as { content?: string } | undefined) + ?.content === content, + ), + ); + }, ownReplyContent), + ) + .toBe(true); + const sentPayload = await page.evaluate( + (content) => (window.__BUZZ_E2E_COMMAND_LOG__ ?? []).findLast( (entry) => entry.command === "send_channel_message" && (entry.payload as { content?: string } | undefined)?.content === content, - ), - ); - }, ownReplyContent), - ) - .toBe(true); - const sentPayload = await page.evaluate( - (content) => - (window.__BUZZ_E2E_COMMAND_LOG__ ?? []).findLast( - (entry) => - entry.command === "send_channel_message" && - (entry.payload as { content?: string } | undefined)?.content === - content, - )?.payload as Record | undefined, - ownReplyContent, - ); - expect(sentPayload).toMatchObject({ - channelId: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", - content: ownReplyContent, - emojiTags: [emojiTag], - linkPreviewTags: [linkPreviewTag], - mediaTags: [imetaTag], - mentionPubkeys: [TEST_IDENTITIES.alice.pubkey], - mentionTags: [mentionTag], - parentEventId: null, - sentFromThreadTag: ["buzz:sent-from-thread", rootId, rootContent], - }); - - await page.getByTestId("auxiliary-panel-close").click(); - - const sharedRow = timeline - .getByTestId("message-row") - .filter({ hasText: replySummary }) - .last(); - await expect - .poll(async () => { - if (await sharedRow.isVisible()) return true; - const scrollToLatest = page.getByTestId("message-scroll-to-latest"); - if (await scrollToLatest.isVisible()) await scrollToLatest.click(); - return false; - }) - .toBe(true); - await expect(sharedRow.getByTestId("message-author")).toHaveText( - "npub1mock...", - ); - await expect(sharedRow.getByTestId("message-avatar-fallback")).toBeVisible(); - await expect(sharedRow.locator('[data-mention=""]')).toContainText("alice"); - await expect(sharedRow.locator("img[data-custom-emoji]")).toHaveAttribute( - "src", - customEmojiUrl, - ); - await expect(sharedRow.getByTestId("file-card")).toContainText( - "launch-notes.txt", - ); - await expect( - sharedRow.locator('[data-link-preview="github-pull-request"]'), - ).toContainText("Add Send to channel for thread messages"); - const sourceLine = sharedRow.getByTestId("sent-from-thread"); - await expect(sourceLine).toContainText("Sent from thread:"); - await expect(sourceLine).toHaveClass(/message-markdown/); - await expect(sourceLine).toHaveClass(/pt-0\.5/); - await expect(sourceLine).toHaveClass(/text-sm/); - await expect(sourceLine).toHaveClass(/font-normal/); - await expect(sourceLine).toHaveClass(/leading-4/); - await expect(sourceLine).toHaveClass(/text-muted-foreground\/70/); - const rootLink = sourceLine.locator("[data-message-link]"); - const sourcePrefix = sourceLine.locator("span").first(); - const rootLinkLabel = rootContent; - await expect(rootLink).toHaveText(rootLinkLabel); - await expect(rootLink).toHaveAttribute( - "aria-label", - "Open thread in general", - ); - await expect(rootLink).toHaveAttribute("title", rootLinkLabel); - await expect(rootLink).toHaveClass(/max-w-80/); - await expect(rootLink).toHaveClass(/truncate/); - await expect(rootLink).toHaveClass(/inline-block/); - await expect(rootLink).toHaveClass(/font-medium/); - await expect(rootLink).not.toHaveClass(/mention-chip/); - await expect(rootLink).not.toHaveClass(/border-b/); - const rootLinkText = rootLink.locator("[data-message-link-text]"); - const rootLinkEmoji = rootLink.locator("[data-message-link-emoji]"); - await expect(rootLinkText).toHaveText(` Share source thread ${timestamp}`); - await expect(rootLinkText).not.toHaveClass(/border-b/); - await expect(rootLinkEmoji).toHaveText("🧵"); - await expect(rootLinkEmoji).not.toHaveClass(/border-b/); - await expect(rootLink).not.toHaveAttribute("data-hovered"); - const [prefixColor, linkColorBeforeHover] = await Promise.all([ - sourcePrefix.evaluate((element) => getComputedStyle(element).color), - rootLink.evaluate((element) => getComputedStyle(element).color), - ]); - expect(linkColorBeforeHover).not.toBe(prefixColor); - await expect - .poll(() => - rootLink.evaluate((element) => getComputedStyle(element).backgroundColor), - ) - .toBe("rgba(0, 0, 0, 0)"); - await expect - .poll(() => - rootLinkText.evaluate((element) => getComputedStyle(element).boxShadow), - ) - .toBe("none"); - - await rootLink.hover(); - await expect(rootLink).toHaveAttribute("data-hovered", ""); - await expect - .poll(() => rootLink.evaluate((element) => getComputedStyle(element).color)) - .toBe(linkColorBeforeHover); - await expect - .poll(() => - rootLinkText.evaluate( - (element) => getComputedStyle(element).boxShadow !== "none", - ), - ) - .toBe(true); + )?.payload as Record | undefined, + ownReplyContent, + ); + expect(sentPayload).toMatchObject({ + channelId: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", + content: ownReplyContent, + emojiTags: [emojiTag], + linkPreviewTags: [linkPreviewTag], + mediaTags: [imetaTag], + mentionPubkeys: [TEST_IDENTITIES.alice.pubkey], + mentionTags: [mentionTag], + parentEventId: null, + sentFromThreadTag: ["buzz:sent-from-thread", rootId, rootContent], + }); - await expect - .poll(() => - rootLinkEmoji.evaluate((element) => getComputedStyle(element).boxShadow), - ) - .toBe("none"); + if (recovery) { + if (!scrollBox) throw new Error("Missing recovery timeline geometry"); + const latest = page.getByTestId("message-scroll-to-latest"); + await expect(latest).toBeVisible(); + await page.mouse.move(scrollBox.x + 40, scrollBox.y + 60); + await page.mouse.wheel(0, 600); + // A real return to the frozen model's floor must not hide pending work. + await expect + .poll(() => + timeline.evaluate( + (element) => + element.scrollHeight - element.clientHeight - element.scrollTop, + ), + ) + .toBe(0); + await expect(latest).toBeVisible(); + await latest.click(); + } + await page.getByTestId("auxiliary-panel-close").click(); + + const sharedRow = timeline + .getByTestId("message-row") + .filter({ hasText: replySummary }) + .last(); + await expect + .poll(async () => { + if (await sharedRow.isVisible()) return true; + const scrollToLatest = page.getByTestId("message-scroll-to-latest"); + if (await scrollToLatest.isVisible()) await scrollToLatest.click(); + return false; + }) + .toBe(true); + await expect(sharedRow.getByTestId("message-author")).toHaveText( + "npub1mock...", + ); + await expect( + sharedRow.getByTestId("message-avatar-fallback"), + ).toBeVisible(); + await expect(sharedRow.locator('[data-mention=""]')).toContainText( + "alice", + ); + await expect(sharedRow.locator("img[data-custom-emoji]")).toHaveAttribute( + "src", + customEmojiUrl, + ); + await expect(sharedRow.getByTestId("file-card")).toContainText( + "launch-notes.txt", + ); + await expect( + sharedRow.locator('[data-link-preview="github-pull-request"]'), + ).toContainText("Add Send to channel for thread messages"); + const sourceLine = sharedRow.getByTestId("sent-from-thread"); + await expect(sourceLine).toContainText("Sent from thread:"); + await expect(sourceLine).toHaveClass(/message-markdown/); + await expect(sourceLine).toHaveClass(/pt-0\.5/); + await expect(sourceLine).toHaveClass(/text-sm/); + await expect(sourceLine).toHaveClass(/font-normal/); + await expect(sourceLine).toHaveClass(/leading-4/); + await expect(sourceLine).toHaveClass(/text-muted-foreground\/70/); + const rootLink = sourceLine.locator("[data-message-link]"); + const sourcePrefix = sourceLine.locator("span").first(); + const rootLinkLabel = rootContent; + await expect(rootLink).toHaveText(rootLinkLabel); + await expect(rootLink).toHaveAttribute( + "aria-label", + "Open thread in general", + ); + await expect(rootLink).toHaveAttribute("title", rootLinkLabel); + await expect(rootLink).toHaveClass(/max-w-80/); + await expect(rootLink).toHaveClass(/truncate/); + await expect(rootLink).toHaveClass(/inline-block/); + await expect(rootLink).toHaveClass(/font-medium/); + await expect(rootLink).not.toHaveClass(/mention-chip/); + await expect(rootLink).not.toHaveClass(/border-b/); + const rootLinkText = rootLink.locator("[data-message-link-text]"); + const rootLinkEmoji = rootLink.locator("[data-message-link-emoji]"); + await expect(rootLinkText).toHaveText( + ` Share source thread ${timestamp}`, + ); + await expect(rootLinkText).not.toHaveClass(/border-b/); + await expect(rootLinkEmoji).toHaveText("🧵"); + await expect(rootLinkEmoji).not.toHaveClass(/border-b/); + await expect(rootLink).not.toHaveAttribute("data-hovered"); + const [prefixColor, linkColorBeforeHover] = await Promise.all([ + sourcePrefix.evaluate((element) => getComputedStyle(element).color), + rootLink.evaluate((element) => getComputedStyle(element).color), + ]); + expect(linkColorBeforeHover).not.toBe(prefixColor); + await expect + .poll(() => + rootLink.evaluate( + (element) => getComputedStyle(element).backgroundColor, + ), + ) + .toBe("rgba(0, 0, 0, 0)"); + await expect + .poll(() => + rootLinkText.evaluate( + (element) => getComputedStyle(element).boxShadow, + ), + ) + .toBe("none"); + + await rootLink.hover(); + await expect(rootLink).toHaveAttribute("data-hovered", ""); + await expect + .poll(() => + rootLink.evaluate((element) => getComputedStyle(element).color), + ) + .toBe(linkColorBeforeHover); + await expect + .poll(() => + rootLinkText.evaluate( + (element) => getComputedStyle(element).boxShadow !== "none", + ), + ) + .toBe(true); - await rootLink.click(); - await expect(threadPanel).toBeVisible(); - await expect(threadPanel.getByTestId("message-thread-head")).toContainText( - rootContent, + await expect + .poll(() => + rootLinkEmoji.evaluate( + (element) => getComputedStyle(element).boxShadow, + ), + ) + .toBe("none"); + + await rootLink.click(); + await expect(threadPanel).toBeVisible(); + await expect( + threadPanel.getByTestId("message-thread-head"), + ).toContainText(rootContent); + }, ); -}); +} test("shows your avatar on your own message when profile avatar is set", async ({ page, @@ -3825,7 +3910,7 @@ test("closing a thread while editing a reply preserves the typed edit", async ({ await threadReply.hover(); await threadReply.getByRole("button", { name: "More actions" }).click(); await page.getByRole("menuitem", { name: "Edit message" }).click(); - await threadInput.fill(edited); + await replaceThreadEdit(threadInput, reply, edited); await threadPanel.getByTestId("auxiliary-panel-close").click(); @@ -4031,7 +4116,7 @@ test("a refused message deep link retries after the thread edit is canceled", as await threadReply.hover(); await threadReply.getByRole("button", { name: "More actions" }).click(); await page.getByRole("menuitem", { name: "Edit message" }).click(); - await threadInput.fill(`${reply} unsaved`); + await replaceThreadEdit(threadInput, reply, `${reply} unsaved`); const threadUrl = page.url(); expect(threadUrl).toContain( @@ -4115,7 +4200,7 @@ test("a refused sent-from-thread link preserves the edit and retries after cance await reply.hover(); await reply.getByRole("button", { name: "More actions" }).click(); await page.getByRole("menuitem", { name: "Edit message" }).click(); - await threadInput.fill(dirtyReply); + await replaceThreadEdit(threadInput, sourceReply, dirtyReply); const threadUrl = page.url(); expect(threadUrl).toContain(`thread=${sourceRootId}`); @@ -4189,7 +4274,7 @@ test("a refused search result preserves the edit and retries after cancel", asyn await reply.hover(); await reply.getByRole("button", { name: "More actions" }).click(); await page.getByRole("menuitem", { name: "Edit message" }).click(); - await threadInput.fill(dirtyReply); + await replaceThreadEdit(threadInput, sourceReply, dirtyReply); const threadUrl = page.url(); expect(threadUrl).toContain(`thread=${sourceRootId}`); @@ -4263,7 +4348,7 @@ test("a refused forum search result preserves the edit and retries after cancel" await reply.hover(); await reply.getByRole("button", { name: "More actions" }).click(); await page.getByRole("menuitem", { name: "Edit message" }).click(); - await threadInput.fill(dirtyReply); + await replaceThreadEdit(threadInput, sourceReply, dirtyReply); const threadUrl = page.url(); const editTarget = threadPanel.getByTestId("edit-target"); @@ -4346,7 +4431,7 @@ for (const targetKind of ["reply", "root"] as const) { await reply.hover(); await reply.getByRole("button", { name: "More actions" }).click(); await page.getByRole("menuitem", { name: "Edit message" }).click(); - await threadInput.fill(dirtyReply); + await replaceThreadEdit(threadInput, sourceReply, dirtyReply); const targetLink = timeline .getByTestId("message-row") @@ -4440,7 +4525,7 @@ test("a refused channel switch preserves the reply edit and retries after cancel await reply.hover(); await reply.getByRole("button", { name: "More actions" }).click(); await page.getByRole("menuitem", { name: "Edit message" }).click(); - await threadInput.fill(dirtyReply); + await replaceThreadEdit(threadInput, sourceReply, dirtyReply); const navigationBefore = await page.evaluate(() => ({ historyLength: history.length, @@ -4524,7 +4609,7 @@ for (const backInput of ["button", "keyboard"] as const) { await reply.hover(); await reply.getByRole("button", { name: "More actions" }).click(); await page.getByRole("menuitem", { name: "Edit message" }).click(); - await threadInput.fill(dirtyReply); + await replaceThreadEdit(threadInput, sourceReply, dirtyReply); const navigationBefore = await page.evaluate(() => ({ historyLength: history.length, diff --git a/desktop/tests/e2e/remote-owned-mentions.spec.ts b/desktop/tests/e2e/remote-owned-mentions.spec.ts index b9d5e9efb8d..15b711958ca 100644 --- a/desktop/tests/e2e/remote-owned-mentions.spec.ts +++ b/desktop/tests/e2e/remote-owned-mentions.spec.ts @@ -590,7 +590,19 @@ for (const { incoming, savedFirst } of [ }), }; }, roots[0]); - await input.fill(""); + await input.press("ControlOrMeta+a"); + expect( + await input.evaluate((el) => { + const editor = ( + el as HTMLElement & { editor: import("@tiptap/core").Editor } + ).editor; + return { + selection: editor.state.selection.toJSON(), + text: window.getSelection()?.toString(), + }; + }), + ).toEqual({ selection: { type: "all" }, text: "new authored text" }); + await input.press("Backspace"); const deleted = await deletion.evaluate(({ completed }) => completed); // No assertion/poll between action completion and outgoing-key cleanup. await navigate(roots[1]);