diff --git a/.github/workflows/playwright-mock.yml b/.github/workflows/playwright-mock.yml index 3baae0e307..db3a275b25 100644 --- a/.github/workflows/playwright-mock.yml +++ b/.github/workflows/playwright-mock.yml @@ -144,6 +144,16 @@ jobs: - name: Verify Chrome is present run: google-chrome --version + # `video: 'on-first-retry'` needs ffmpeg; without it the first retry dies in + # browserContext.newPage before the test body runs, so a flaky test loses the + # retry that would have recovered it. The CLI can hang after the download + # finishes on these runners, so bound it and keep it non-fatal — worst case is + # today's behaviour of retrying without video. + - name: Install Playwright ffmpeg (best effort) + timeout-minutes: 3 + continue-on-error: true + run: timeout -k 10 90 npx playwright install ffmpeg + # The runner's Chrome is an apt package, so its real library dependencies are # already satisfied; all `install-deps` adds here are optional CJK/Thai/Cyrillic # font packages (~21MB from azure.archive.ubuntu.com). Nothing in CI asserts on @@ -235,6 +245,12 @@ jobs: - name: Verify Chrome is present run: google-chrome --version + # ffmpeg for retry video — see the note in the e2e_shards job. + - name: Install Playwright ffmpeg (best effort) + timeout-minutes: 3 + continue-on-error: true + run: timeout -k 10 90 npx playwright install ffmpeg + # Optional fonts only — see the note in the e2e_shards job. - name: Install optional Playwright font dependencies (best effort) timeout-minutes: 4 diff --git a/e2e/specs/mock/quotes.spec.ts b/e2e/specs/mock/quotes.spec.ts index 4e524b9551..37e2cba038 100644 --- a/e2e/specs/mock/quotes.spec.ts +++ b/e2e/specs/mock/quotes.spec.ts @@ -98,6 +98,33 @@ async function doubleClickWord(page: Page, needle: string) { await page.mouse.dblclick(point.x, point.y); } +/** + * Wait for the reply to stop re-rendering before selecting text inside it. + * + * Distinct from the settle window `QuoteButton` applies to a *selection*: this + * is the reply itself still streaming. A markdown re-render swaps out the text + * node the selection points at, which collapses it — so a double-click landing + * mid-stream loses its selection before the popup can be clicked, and every + * `toPass` retry loses the same race rather than recovering from it. + * + * `sendMessage` resolves on the stream *response*, not on the final render, so + * the wait has to be explicit. + */ +async function waitForReplyToSettle(page: Page, needle: string) { + const readReply = () => + page.evaluate((text) => { + const renders = Array.from(document.querySelectorAll('.message-render')); + const host = [...renders].reverse().find((el) => (el.textContent ?? '').includes(text)); + return host?.textContent ?? ''; + }, needle); + + await expect(async () => { + const before = await readReply(); + await page.waitForTimeout(250); + expect(await readReply()).toBe(before); + }).toPass({ timeout: 20000 }); +} + /** * Triple-click the block containing `needle` with native mouse events, which * makes Chromium select that whole block and park the selection's far boundary @@ -311,6 +338,7 @@ test.describe('quote references', () => { const response = await sendMessage(page, 'seed for dblclick'); expect(response.ok()).toBeTruthy(); await expect(mockReply(page)).toBeVisible({ timeout: 20000 }); + await waitForReplyToSettle(page, MOCK_REPLY_TEXT); // A real double-click selects the word under the cursor. Chromium commits // that selection on `dblclick`, AFTER `mouseup` fires, so only a `dblclick`