test(servo): pin DPR-on-first-apply contract with viewport diff

Lift `apply_viewport`'s three-way diff (resize / set_hidpi / set_zoom)
into a `ViewportChange` value so the decision is testable without a
running `SoftwareServoHost`. Four unit cases pin the contract:

- Retina creation (DPR=2.0): only the hidpi push fires (the bug above).
- Standard-DPI creation (DPR=1.0): nothing to push, session already
  matches Servo's defaults.
- Mid-session zoom change: only `set_page_zoom` fires.
- Cross-monitor DPR change: only the hidpi push fires.

This is a regression guard for the previous commit — any future change
that re-initializes a session from request values instead of Servo's
post-build defaults will fail `fresh_retina_session_pushes_hidpi_only`.
This commit is contained in:
2026-05-18 14:14:04 -04:00
parent a067d62ad3
commit 4cbd1c8870
+148 -14
View File
@@ -166,31 +166,28 @@ impl ServoLiveClient {
let Some(session) = self.sessions.get_mut(&request.tab_id) else {
return Ok(());
};
let change = ViewportChange::between(request, session);
if session.width != request.width || session.height != request.height {
self.host.resize(ResizeRequest {
webview_id: webview_id.clone(),
width: request.width,
height: request.height,
})?;
session.width = request.width;
session.height = request.height;
if let Some((width, height)) = change.resize {
self.host.resize(ResizeRequest { webview_id: webview_id.clone(), width, height })?;
session.width = width;
session.height = height;
}
if session.device_pixel_ratio != request.device_pixel_ratio {
if let Some(scale_factor) = change.set_hidpi {
self.host.set_hidpi_scale(HidpiScaleRequest {
webview_id: webview_id.clone(),
scale_factor: request.device_pixel_ratio,
scale_factor,
})?;
session.device_pixel_ratio = request.device_pixel_ratio;
session.device_pixel_ratio = scale_factor;
}
if session.page_zoom_percent != request.page_zoom_percent {
if let Some(page_zoom_percent) = change.set_page_zoom {
self.host.set_page_zoom(PageZoomRequest {
webview_id: webview_id.clone(),
zoom_factor: f32::from(request.page_zoom_percent) / 100.0,
zoom_factor: f32::from(page_zoom_percent) / 100.0,
})?;
session.page_zoom_percent = request.page_zoom_percent;
session.page_zoom_percent = page_zoom_percent;
}
Ok(())
@@ -316,3 +313,140 @@ struct DirectWebViewSession {
device_pixel_ratio: f32,
native_surface_id: Option<usize>,
}
/// Calls that `apply_viewport` needs to make to bring the underlying
/// Servo WebView state in line with the embedder's request. Extracted
/// as a pure value so the diff decision is testable without a live
/// `SoftwareServoHost` — see `tests` below.
#[derive(Debug, Default, PartialEq)]
struct ViewportChange {
resize: Option<(u32, u32)>,
set_hidpi: Option<f32>,
set_page_zoom: Option<u16>,
}
impl ViewportChange {
fn between(request: &ServoLiveEnsureRequest, session: &DirectWebViewSession) -> Self {
Self {
resize: (session.width != request.width || session.height != request.height)
.then_some((request.width, request.height)),
set_hidpi: (session.device_pixel_ratio != request.device_pixel_ratio)
.then_some(request.device_pixel_ratio),
set_page_zoom: (session.page_zoom_percent != request.page_zoom_percent)
.then_some(request.page_zoom_percent),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ely_domain::WebViewId;
fn session(width: u32, height: u32, dpr: f32, zoom: u16) -> DirectWebViewSession {
DirectWebViewSession {
webview_id: WebViewId::new(),
profile_id: ProfileId::new(),
requested_url: None,
width,
height,
page_zoom_percent: zoom,
device_pixel_ratio: dpr,
native_surface_id: Some(0xC0FFEE),
}
}
fn request(width: u32, height: u32, dpr: f32, zoom: u16) -> ServoLiveEnsureRequest {
ServoLiveEnsureRequest {
tab_id: String::from("tab"),
profile_id: String::from("profile"),
url: String::from("https://example.com/"),
width,
height,
page_zoom_percent: zoom,
device_pixel_ratio: dpr,
native_surface: None,
scroll_delta_x: 0,
scroll_delta_y: 0,
scroll_point_x: None,
scroll_point_y: None,
click_x: None,
click_y: None,
hover_x: None,
hover_y: None,
typed_text: None,
site_permissions: Vec::new(),
}
}
#[test]
fn fresh_retina_session_pushes_hidpi_only() {
// A 1440×900 Retina viewport arrives as physical 2880×1800 with
// DPR=2.0. `ensure_webview` stamps the session with Servo's
// post-build defaults (1.0 DPR, 100 % zoom) and the request's
// physical dimensions, so only the hidpi push should fire.
let change = ViewportChange::between(
&request(2880, 1800, 2.0, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
&session(
2880,
1800,
SERVO_DEFAULT_DEVICE_PIXEL_RATIO,
SERVO_DEFAULT_PAGE_ZOOM_PERCENT,
),
);
assert_eq!(
change,
ViewportChange { resize: None, set_hidpi: Some(2.0), set_page_zoom: None },
);
}
#[test]
fn fresh_standard_dpi_session_pushes_nothing() {
// On a 1.0-DPR display the fresh session already matches the
// request — Servo's defaults are exactly what we asked for.
let change = ViewportChange::between(
&request(
1280,
720,
SERVO_DEFAULT_DEVICE_PIXEL_RATIO,
SERVO_DEFAULT_PAGE_ZOOM_PERCENT,
),
&session(
1280,
720,
SERVO_DEFAULT_DEVICE_PIXEL_RATIO,
SERVO_DEFAULT_PAGE_ZOOM_PERCENT,
),
);
assert_eq!(change, ViewportChange::default());
}
#[test]
fn page_zoom_change_pushes_only_zoom() {
// User toggled zoom to 150 % on a session that's already at the
// right size and DPR — only `set_page_zoom` should fire.
let change = ViewportChange::between(
&request(2880, 1800, 2.0, 150),
&session(2880, 1800, 2.0, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
);
assert_eq!(
change,
ViewportChange { resize: None, set_hidpi: None, set_page_zoom: Some(150) },
);
}
#[test]
fn cross_monitor_dpr_change_pushes_only_hidpi() {
// A window dragged from a 2.0-DPR monitor to a 1.0-DPR one keeps
// the same physical surface (GPUI re-emits the viewport at the
// new scale) — only the DPR push should fire.
let change = ViewportChange::between(
&request(2880, 1800, 1.0, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
&session(2880, 1800, 2.0, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
);
assert_eq!(
change,
ViewportChange { resize: None, set_hidpi: Some(1.0), set_page_zoom: None },
);
}
}