Repository navigation
set_viewport and resize undo --blink-settings hover/pointer overrides since 0.19.0 #664
Description
Disclaimer: This issue was formulated with the help of Claude Code, after I investigated this bug in our CI.
Describe the bug
Since #618 (released in 0.19.0), Page#set_viewport sends Emulation.setTouchEmulationEnabled on every call, so desktop viewports send enabled: false. Page#resize goes through set_viewport as well.
Chrome handles enabled: false by writing the hover and pointer types of the machine's input devices back into the page settings, even if touch emulation was never on. That undoes overrides passed with --blink-settings, such as
--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4
which is the workaround recommended in rubycdp/cuprite#282 and rubycdp/cuprite#268 for headless Chrome on Linux. After set_viewport or resize, (hover: hover) and (pointer: fine) no longer match on the current document. In our runs a later go_to to another document brought them back; on Chrome 141 a reload did not. Client-side navigation doesn't bring them back either (checked with history.pushState, which Inertia and other SPA routers use).
Cuprite 0.19 goes through resize whenever it attaches a page (prepare_page) and in resize_window_to, maximize_window and fullscreen_window. A fresh window is still on about:blank at that point. On Chrome 146 and 155 the first visit re-applies the flag, so a visit-then-hover check like the one in cuprite#282 passes; on Chrome 141 it doesn't, so every new window starts without hover. What breaks on every version we tried is a resize on a loaded page, e.g. a window resize or switch_to_window to a popup that had already loaded. In a Linux CI everything behind @media (hover: hover) (by default all of Tailwind 4's hover: and group-hover: utilities) then stops applying, and hover-revealed elements can't be found.
To Reproduce
Standalone Ferrum, headless Chrome 141 or newer on Linux. Chrome 140 and older also drop the flag on some navigations by themselves (crbug 331757596), which muddies the 0.18.0 baseline. The flag is a launch flag, so let Ferrum start Chrome (no url:).
require "ferrum"
browser = Ferrum::Browser.new(
# dockerize: true, # when running as root in Docker
browser_options: {
"blink-settings" => "primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4"
}
)
page = browser.create_page
media = "[matchMedia('(hover: hover)').matches, matchMedia('(pointer: fine)').matches]"
page.go_to("data:text/html,<p>hello</p>")
p page.evaluate(media) # => [true, true]
page.set_viewport(width: 1024, height: 768) # same with page.resize(width: 1100, height: 800)
p page.evaluate(media) # => [false, false] on 0.19.x, [true, true] on 0.18.0
page.go_to("data:text/html,<p>again</p>")
p page.evaluate(media) # => [true, true]Results with Chrome for Testing in Docker (Ubuntu 24.04.5, linux/amd64):
after go_to |
after set_viewport / resize |
after the next go_to |
|
|---|---|---|---|
| Ferrum 0.18.0, Chrome 155.0.8059.84 | [true, true] |
[true, true] |
[true, true] |
| Ferrum 0.19.1, Chrome 141.0.7390.122, 146.0.7680.165, 155.0.8059.84 | [true, true] |
[false, false] |
[true, true] |
| Ferrum 0.19.1 + the change from the PR, Chrome 141 and 155 | [true, true] |
[true, true] |
[true, true] |
On macOS the script prints [true, true] throughout, because Chrome there hard-codes a fine pointer with hover (pointer_device_default.cc), so the reset writes back what the flag set. The reset itself isn't Linux-specific: with "blink-settings" => "primaryPointerType=2,availablePointerTypes=2", matchMedia('(pointer: coarse)').matches goes from true to false after set_viewport on 0.19.1, and stays true on 0.18.0 and with the change below (macOS arm64 with Chrome 155.0.8059.40, Linux with 155.0.8059.84).
Expected behavior
On a page that was never switched to mobile: true, set_viewport(mobile: false) and resize only change the viewport and leave hover/pointer alone, as in 0.18.0. set_viewport(mobile: true) keeps enabling touch emulation as added in #618.
Desktop (please complete the following information):
- OS: Ubuntu 24.04.5 in Docker (linux/amd64), headless; macOS arm64 for the note above
- Browser: Chrome for Testing 155.0.8059.84, 146.0.7680.165 and 141.0.7390.122; Chrome 155.0.8059.40 on macOS
- Version: Ferrum 0.19.1 (main at e19c24d has the same
set_viewport), 0.18.0 for comparison; Ruby 4.0.6
Additional context
What happens in Chrome:
DevToolsEmulator::SetTouchEventEmulationEnabledsets pointer/hover back to the values the browser provides when called withfalse. On Linux those come from the detected input devices (pointer_device_linux.cc), so headless Chrome gets none.--blink-settingsis applied withSetFromStringsdirectly onto the settings (web_view_impl.cc) and never updates those values, which only change through the emulator-routed setters (web_settings_impl.cc).- The
setTouchEmulationEnabledhandler does this even if touch emulation was never on.
No CDP command sets hover/pointer directly (Emulation.setEmulatedMedia ignores them, as noted in cuprite#282). In our runs the flag came back on navigation to another document and, as a side effect, after page.screenshot(full: true) (captureBeyondViewport).
Playwright launches headless Chromium with this exact flag by default (chromium.ts) and only sends setTouchEmulationEnabled with enabled: true, for contexts with hasTouch (crPage.ts).
Possible fix
Only send setTouchEmulationEnabled when touch emulation is requested, and once with enabled: false to turn off an earlier mobile viewport on the same page. That's the rule Chromium adopted for its own resets in a65c088 ("DevTools: blank sessions should not reset touch emulation on navigation", Chrome 141):
def set_viewport(width: 0, height: 0, scale_factor: 0, mobile: false)
if mobile || @touch_emulation_enabled
command("Emulation.setTouchEmulationEnabled", enabled: mobile, maxTouchPoints: 1)
@touch_emulation_enabled = mobile
end
command(
"Emulation.setDeviceMetricsOverride",
slowmoable: true,
width: width,
height: height,
deviceScaleFactor: scale_factor,
mobile: mobile
)
endWith this, mobile: true still gives (pointer: coarse), no (hover: hover) and navigator.maxTouchPoints == 1 (checked on Chrome 141 and 155). Switching the same page back to a desktop viewport still has to turn touch emulation off, which resets hover/pointer until the page navigates again, but only on pages that were mobile: true before. The one difference to 0.19.x: touch emulation turned on with a raw page.command is no longer turned off by a desktop set_viewport, which matches 0.18.0.
I actually have a working fix already and will prepare a PR in the next days. But if you have additional thoughts or ideas, happy to discuss.
Activity