From e6f0298f90f702a2c3ab06817cf8d9c2a671e054 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sun, 4 Oct 2026 18:49:45 +0530 Subject: [PATCH] Wait for stable reader touch targets in browser checks --- apps/extensions/tests/auth.browser.mjs | 2 +- .../extensions/tests/reader-touch.browser.mjs | 51 +++++++++++++++++++ apps/web/tests/browser/feed.mjs | 5 +- .../tests/browser}/reader-interactions.mjs | 25 +++++---- 4 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 apps/extensions/tests/reader-touch.browser.mjs rename {scripts/testing => apps/web/tests/browser}/reader-interactions.mjs (89%) diff --git a/apps/extensions/tests/auth.browser.mjs b/apps/extensions/tests/auth.browser.mjs index aa8518432..95b0e76da 100644 --- a/apps/extensions/tests/auth.browser.mjs +++ b/apps/extensions/tests/auth.browser.mjs @@ -5,7 +5,7 @@ import { checkReadingStreak } from "../../../scripts/testing/reading-streak.mjs" import { checkReaderInteractions, notificationFixture, -} from "../../../scripts/testing/reader-interactions.mjs"; +} from "../../web/tests/browser/reader-interactions.mjs"; import { checkDevCardPromo, checkUnclaimedDevCardPromo, diff --git a/apps/extensions/tests/reader-touch.browser.mjs b/apps/extensions/tests/reader-touch.browser.mjs new file mode 100644 index 000000000..84f53e738 --- /dev/null +++ b/apps/extensions/tests/reader-touch.browser.mjs @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { chromium } from "playwright"; +import { touchReaderTarget } from "../../web/tests/browser/reader-interactions.mjs"; + +test("reader touch re-resolves a target replaced during scrolling", async () => { + const browser = await chromium.launch({ + executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, + channel: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH + ? undefined + : process.env.DEVFEED_EXTENSION_BROWSER === "edge" + ? "msedge" + : "chromium", + headless: true, + }); + try { + const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); + const cdp = await page.context().newCDPSession(page); + await cdp.send("Emulation.setTouchEmulationEnabled", { + enabled: true, + maxTouchPoints: 1, + }); + await page.setContent(` + + + + `); + await touchReaderTarget(cdp, page.getByRole("button", { name: "Open article" })); + await page.waitForFunction(() => window.result.clicks === 1); + assert.deepEqual(await page.evaluate(() => window.result), { + replacements: 1, + touches: 1, + clicks: 1, + }); + } finally { + await browser.close(); + } +}); diff --git a/apps/web/tests/browser/feed.mjs b/apps/web/tests/browser/feed.mjs index a1a4083c0..a282c54ac 100644 --- a/apps/web/tests/browser/feed.mjs +++ b/apps/web/tests/browser/feed.mjs @@ -3,10 +3,7 @@ import { dailyFixture, checkMustReads } from "../../../../scripts/testing/must-r import { checkLeaderboard, leaderboardProfile } from "../../../../scripts/testing/leaderboard.mjs"; import { checkMcp, testMcpEndpoint } from "../../../../scripts/testing/mcp.mjs"; import { checkReadingStreak } from "../../../../scripts/testing/reading-streak.mjs"; -import { - checkReaderInteractions, - notificationFixture, -} from "../../../../scripts/testing/reader-interactions.mjs"; +import { checkReaderInteractions, notificationFixture } from "./reader-interactions.mjs"; import { checkSidebarGitHub } from "../../../../scripts/testing/sidebar-github.mjs"; import { engagementFeed, diff --git a/scripts/testing/reader-interactions.mjs b/apps/web/tests/browser/reader-interactions.mjs similarity index 89% rename from scripts/testing/reader-interactions.mjs rename to apps/web/tests/browser/reader-interactions.mjs index d74677656..e495f1888 100644 --- a/scripts/testing/reader-interactions.mjs +++ b/apps/web/tests/browser/reader-interactions.mjs @@ -17,6 +17,19 @@ export function notificationFixture() { }; } +/** Wait for a stable target before dispatching native touch events. */ +export async function touchReaderTarget(cdp, locator) { + // Trial actions re-resolve a detached locator without sending a desktop click. + await locator.click({ trial: true }); + const box = await locator.boundingBox(); + assert.ok(box, "touch target has visible bounds"); + await cdp.send("Input.dispatchTouchEvent", { + type: "touchStart", + touchPoints: [{ x: box.x + box.width / 2, y: box.y + box.height / 2 }], + }); + await cdp.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] }); +} + /** Exercise actual touch events, not desktop clicks in a narrow viewport. */ export async function checkReaderInteractions( page, @@ -29,17 +42,7 @@ export async function checkReaderInteractions( await page.setViewportSize({ width: 390, height: 844 }); await cdp.send("Emulation.setTouchEmulationEnabled", { enabled: true, maxTouchPoints: 1 }); await page.emulateMedia({ reducedMotion: "no-preference" }); - async function tap(locator) { - await locator.waitFor(); - await locator.scrollIntoViewIfNeeded(); - const box = await locator.boundingBox(); - assert.ok(box, "touch target has visible bounds"); - await cdp.send("Input.dispatchTouchEvent", { - type: "touchStart", - touchPoints: [{ x: box.x + box.width / 2, y: box.y + box.height / 2 }], - }); - await cdp.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] }); - } + const tap = (locator) => touchReaderTarget(cdp, locator); const nav = page.locator(".mobile-nav"); const free = async () => { assert.notEqual(