mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 08:00:59 +00:00
feat(layout): add --hide-scrollbars for fetch and serve
Hide native scrollbars and automatic gutters while preserving scrolling and explicit stable gutters. Require layout mode and retain the setting across navigation, frames, and protocol sessions. Refs #1081
This commit is contained in:
@@ -9,6 +9,7 @@ pub struct BrowserConfig {
|
||||
fetch: FetchConfig,
|
||||
profile_dir: Option<PathBuf>,
|
||||
layout_policy: LayoutPolicy,
|
||||
scrollbars_hidden: bool,
|
||||
optional_resource_fetch_mask: OptionalResourceFetchMask,
|
||||
subframe_loading_enabled: bool,
|
||||
script_execution_disabled: bool,
|
||||
@@ -23,6 +24,7 @@ impl Default for BrowserConfig {
|
||||
fetch: FetchConfig::default(),
|
||||
profile_dir: None,
|
||||
layout_policy: LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
optional_resource_fetch_mask: OptionalResourceFetchMask::NONE,
|
||||
subframe_loading_enabled: true,
|
||||
script_execution_disabled: false,
|
||||
@@ -75,6 +77,19 @@ impl BrowserConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scrollbars_hidden(&self) -> bool {
|
||||
self.scrollbars_hidden
|
||||
}
|
||||
|
||||
pub fn set_scrollbars_hidden(&mut self, hidden: bool) {
|
||||
self.scrollbars_hidden = hidden;
|
||||
}
|
||||
|
||||
pub fn with_scrollbars_hidden(mut self, hidden: bool) -> Self {
|
||||
self.set_scrollbars_hidden(hidden);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn image_fetch_enabled(&self) -> bool {
|
||||
self.optional_resource_fetch_enabled(SubresourceResourceType::Image)
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ impl Browser {
|
||||
JsRuntime::initialize_with_browser_context_owner_access(&browser_context_access)?;
|
||||
js_runtime
|
||||
.renderer_owner_handle()
|
||||
.configure_layout_policy(config.layout_policy())?;
|
||||
.configure_layout(config.layout_policy(), config.scrollbars_hidden())?;
|
||||
let lifetime_owner = Rc::new(BrowserLifetimeOwner {
|
||||
js_runtime: Some(js_runtime.clone()),
|
||||
browser_context_owner,
|
||||
|
||||
@@ -356,6 +356,7 @@ pub struct NavigationRuntimeConfig {
|
||||
optional_resource_fetch_mask: OptionalResourceFetchMask,
|
||||
subframe_loading_enabled: bool,
|
||||
layout_policy: LayoutPolicy,
|
||||
scrollbars_hidden: bool,
|
||||
}
|
||||
|
||||
impl NavigationRuntimeConfig {
|
||||
@@ -370,6 +371,7 @@ impl NavigationRuntimeConfig {
|
||||
optional_resource_fetch_mask,
|
||||
subframe_loading_enabled,
|
||||
layout_policy,
|
||||
scrollbars_hidden: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +394,15 @@ impl NavigationRuntimeConfig {
|
||||
pub fn layout_policy(&self) -> LayoutPolicy {
|
||||
self.layout_policy
|
||||
}
|
||||
|
||||
pub fn scrollbars_hidden(&self) -> bool {
|
||||
self.scrollbars_hidden
|
||||
}
|
||||
|
||||
pub fn with_scrollbars_hidden(mut self, hidden: bool) -> Self {
|
||||
self.scrollbars_hidden = hidden;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NavigationRuntimeConfig {
|
||||
@@ -413,6 +424,7 @@ impl From<&crate::config::BrowserConfig> for NavigationRuntimeConfig {
|
||||
config.subframe_loading_enabled(),
|
||||
config.layout_policy(),
|
||||
)
|
||||
.with_scrollbars_hidden(config.scrollbars_hidden())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +433,7 @@ pub struct NavigationEngine {
|
||||
fetch_config: FetchConfig,
|
||||
page_network_policy: PageNetworkPolicy,
|
||||
layout_policy: LayoutPolicy,
|
||||
scrollbars_hidden: bool,
|
||||
js_runtime: JsRuntime,
|
||||
resource_runtime: Option<BrowserResourceRuntime>,
|
||||
browser_context_access: RendererBrowserContextRuntimeOwnerAccess,
|
||||
@@ -557,6 +570,7 @@ impl NavigationEngine {
|
||||
optional_resource_fetch_mask,
|
||||
subframe_loading_enabled,
|
||||
layout_policy,
|
||||
scrollbars_hidden,
|
||||
} = config;
|
||||
let resource_runtime = browser_context_access
|
||||
.current_browser_resource_runtime()
|
||||
@@ -565,7 +579,7 @@ impl NavigationEngine {
|
||||
JsRuntime::initialize_with_browser_context_owner_access(&browser_context_access)?;
|
||||
js_runtime
|
||||
.renderer_owner_handle()
|
||||
.configure_layout_policy(layout_policy)?;
|
||||
.configure_layout(layout_policy, scrollbars_hidden)?;
|
||||
let standalone_lifetime_owner = standalone_browser_context_owner.map(|owner| {
|
||||
Rc::new(NavigationEngineLifetimeOwner {
|
||||
js_runtime: Some(js_runtime.clone()),
|
||||
@@ -580,6 +594,7 @@ impl NavigationEngine {
|
||||
false,
|
||||
),
|
||||
layout_policy,
|
||||
scrollbars_hidden,
|
||||
js_runtime,
|
||||
resource_runtime: Some(resource_runtime),
|
||||
browser_context_access,
|
||||
@@ -600,7 +615,8 @@ impl NavigationEngine {
|
||||
optional_resource_fetch_mask,
|
||||
subframe_loading_enabled,
|
||||
renderer_owner_source.layout_policy,
|
||||
),
|
||||
)
|
||||
.with_scrollbars_hidden(renderer_owner_source.scrollbars_hidden),
|
||||
renderer_owner_source,
|
||||
)
|
||||
}
|
||||
@@ -614,11 +630,12 @@ impl NavigationEngine {
|
||||
optional_resource_fetch_mask,
|
||||
subframe_loading_enabled,
|
||||
layout_policy,
|
||||
scrollbars_hidden,
|
||||
} = config;
|
||||
renderer_owner_source
|
||||
.js_runtime
|
||||
.renderer_owner_handle()
|
||||
.configure_layout_policy(layout_policy)?;
|
||||
.configure_layout(layout_policy, scrollbars_hidden)?;
|
||||
let resource_runtime = renderer_owner_source
|
||||
.browser_context_access
|
||||
.current_browser_resource_runtime()
|
||||
@@ -631,6 +648,7 @@ impl NavigationEngine {
|
||||
false,
|
||||
),
|
||||
layout_policy,
|
||||
scrollbars_hidden,
|
||||
js_runtime: renderer_owner_source.js_runtime.clone(),
|
||||
resource_runtime: Some(resource_runtime),
|
||||
browser_context_access: renderer_owner_source.browser_context_access.clone(),
|
||||
@@ -698,6 +716,7 @@ impl NavigationEngine {
|
||||
self.subframe_loading_enabled(),
|
||||
self.layout_policy,
|
||||
)
|
||||
.with_scrollbars_hidden(self.scrollbars_hidden)
|
||||
}
|
||||
|
||||
pub fn layout_policy(&self) -> LayoutPolicy {
|
||||
@@ -2531,6 +2550,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_scrollbars_follow_browser_and_shared_navigation_configuration() {
|
||||
let browser_config = BrowserConfig::default()
|
||||
.with_layout_policy(LayoutPolicy::OnDemand)
|
||||
.with_scrollbars_hidden(true);
|
||||
let browser = Browser::new(browser_config.clone()).expect("hidden scrollbar browser");
|
||||
assert!(
|
||||
browser
|
||||
.js_runtime
|
||||
.renderer_owner_handle()
|
||||
.scrollbars_hidden()
|
||||
);
|
||||
|
||||
let source = NavigationEngine::new_with_runtime_config((&browser_config).into());
|
||||
assert!(source.runtime_config().scrollbars_hidden());
|
||||
assert!(
|
||||
source
|
||||
.js_runtime
|
||||
.renderer_owner_handle()
|
||||
.scrollbars_hidden()
|
||||
);
|
||||
let shared = NavigationEngine::new_with_fetch_config_and_shared_renderer_owner(
|
||||
FetchConfig::default(),
|
||||
&source,
|
||||
OptionalResourceFetchMask::NONE,
|
||||
true,
|
||||
)
|
||||
.expect("matching owner configuration");
|
||||
assert!(shared.runtime_config().scrollbars_hidden());
|
||||
assert!(shared.shares_renderer_owner_with(&source));
|
||||
|
||||
let conflicting = source.runtime_config().with_scrollbars_hidden(false);
|
||||
assert!(
|
||||
NavigationEngine::new_with_runtime_config_and_shared_renderer_owner(
|
||||
conflicting,
|
||||
&source
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
source
|
||||
.js_runtime
|
||||
.renderer_owner_handle()
|
||||
.scrollbars_hidden()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_scrollbars_require_real_layout_for_rust_callers() {
|
||||
assert!(Browser::new(BrowserConfig::default().with_scrollbars_hidden(true)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adopting_shared_backend_does_not_share_page_network_policy() {
|
||||
let left = NavigationEngine::new();
|
||||
|
||||
@@ -377,7 +377,8 @@ where
|
||||
root_style.scrollbar_colors(),
|
||||
root_style.uses_horizontal_writing_mode(),
|
||||
self.source.disables_viewport_user_scrolling(),
|
||||
);
|
||||
)
|
||||
.with_scrollbars_hidden(root_style.viewport_scrollbars_hidden());
|
||||
}
|
||||
|
||||
fn principal_box(
|
||||
|
||||
@@ -507,6 +507,7 @@ pub struct ResolvedLayoutStyle {
|
||||
overflow_x: LayoutOverflowMode,
|
||||
overflow_y: LayoutOverflowMode,
|
||||
scrollbar_width: LayoutScrollbarWidth,
|
||||
scrollbars_hidden: bool,
|
||||
scrollbar_gutter: LayoutScrollbarGutter,
|
||||
scrollbar_colors: Option<LayoutScrollbarColors>,
|
||||
revealed_scrollbar_x: bool,
|
||||
@@ -888,6 +889,7 @@ impl ResolvedLayoutStyle {
|
||||
overflow_x,
|
||||
overflow_y,
|
||||
scrollbar_width,
|
||||
scrollbars_hidden: false,
|
||||
scrollbar_gutter,
|
||||
scrollbar_colors,
|
||||
revealed_scrollbar_x: false,
|
||||
@@ -965,6 +967,7 @@ impl ResolvedLayoutStyle {
|
||||
overflow_x,
|
||||
overflow_y,
|
||||
scrollbar_width: LayoutScrollbarWidth::Auto,
|
||||
scrollbars_hidden: false,
|
||||
scrollbar_gutter: LayoutScrollbarGutter::Auto,
|
||||
scrollbar_colors: None,
|
||||
revealed_scrollbar_x: false,
|
||||
@@ -1042,6 +1045,11 @@ impl ResolvedLayoutStyle {
|
||||
self.scrollbar_colors = colors;
|
||||
}
|
||||
|
||||
/// Hides scrollbar UI and automatic gutters, preserving explicit stable gutters.
|
||||
pub fn suppress_scrollbars(&mut self) {
|
||||
self.scrollbars_hidden = true;
|
||||
}
|
||||
|
||||
/// Overrides the alignment keyword for a synthetic inline style.
|
||||
pub fn with_inline_alignment(mut self, alignment: LayoutInlineAlignment) -> Self {
|
||||
self.vertical_align.kind = alignment;
|
||||
@@ -1726,6 +1734,10 @@ impl ResolvedLayoutStyle {
|
||||
self.scrollbar_width
|
||||
}
|
||||
|
||||
pub(crate) const fn viewport_scrollbars_hidden(&self) -> bool {
|
||||
self.scrollbars_hidden
|
||||
}
|
||||
|
||||
pub(crate) const fn viewport_scrollbar_gutter(&self) -> LayoutScrollbarGutter {
|
||||
self.scrollbar_gutter
|
||||
}
|
||||
@@ -1754,8 +1766,10 @@ impl ResolvedLayoutStyle {
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_scrollbar_layout(&mut self, is_root: bool) {
|
||||
self.revealed_scrollbar_x = self.overflow_x == LayoutOverflowMode::Scroll;
|
||||
self.revealed_scrollbar_y = self.overflow_y == LayoutOverflowMode::Scroll;
|
||||
self.revealed_scrollbar_x =
|
||||
!self.scrollbars_hidden && self.overflow_x == LayoutOverflowMode::Scroll;
|
||||
self.revealed_scrollbar_y =
|
||||
!self.scrollbars_hidden && self.overflow_y == LayoutOverflowMode::Scroll;
|
||||
// Moli resolves CSS scrollbar policy to physical edge insets through
|
||||
// `LayoutPartialTree::get_scrollbar_insets`. Keep Taffy's legacy
|
||||
// scalar disabled so no algorithm reserves the same gutter twice.
|
||||
@@ -1806,7 +1820,10 @@ impl ResolvedLayoutStyle {
|
||||
is_root: bool,
|
||||
overflowing: bool,
|
||||
) -> bool {
|
||||
if !overflowing || self.scrollbar_width == LayoutScrollbarWidth::None {
|
||||
if !overflowing
|
||||
|| self.scrollbars_hidden
|
||||
|| self.scrollbar_width == LayoutScrollbarWidth::None
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let mode = self.overflow_mode(axis);
|
||||
@@ -1842,7 +1859,7 @@ impl ResolvedLayoutStyle {
|
||||
is_root: bool,
|
||||
overflowing: bool,
|
||||
) -> bool {
|
||||
if self.scrollbar_width == LayoutScrollbarWidth::None {
|
||||
if self.scrollbars_hidden || self.scrollbar_width == LayoutScrollbarWidth::None {
|
||||
return false;
|
||||
}
|
||||
match self.overflow_mode(axis) {
|
||||
@@ -1867,7 +1884,11 @@ impl ResolvedLayoutStyle {
|
||||
}
|
||||
|
||||
pub(crate) fn scrollbar_control_thickness(&self) -> f32 {
|
||||
self.scrollbar_width.thickness()
|
||||
if self.scrollbars_hidden {
|
||||
0.0
|
||||
} else {
|
||||
self.scrollbar_width.thickness()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scrollbar_gutter_thickness(&self, axis: LayoutScrollbarAxis) -> f32 {
|
||||
@@ -1985,6 +2006,7 @@ impl ResolvedLayoutStyle {
|
||||
overflow_x: LayoutOverflowMode::Visible,
|
||||
overflow_y: LayoutOverflowMode::Visible,
|
||||
scrollbar_width: parent.scrollbar_width,
|
||||
scrollbars_hidden: parent.scrollbars_hidden,
|
||||
scrollbar_gutter: LayoutScrollbarGutter::Auto,
|
||||
scrollbar_colors: parent.scrollbar_colors,
|
||||
revealed_scrollbar_x: false,
|
||||
@@ -2053,6 +2075,7 @@ impl ResolvedLayoutStyle {
|
||||
overflow_x: LayoutOverflowMode::Visible,
|
||||
overflow_y: LayoutOverflowMode::Visible,
|
||||
scrollbar_width: parent.scrollbar_width,
|
||||
scrollbars_hidden: parent.scrollbars_hidden,
|
||||
scrollbar_gutter: LayoutScrollbarGutter::Auto,
|
||||
scrollbar_colors: parent.scrollbar_colors,
|
||||
revealed_scrollbar_x: false,
|
||||
|
||||
@@ -320,6 +320,7 @@ pub(crate) struct ViewportScrollPolicy {
|
||||
scrollbar_style: ViewportScrollbarStyle,
|
||||
embedder_allows_user_scroll: bool,
|
||||
embedder_allows_scrollbar_controls: bool,
|
||||
scrollbars_hidden: bool,
|
||||
revealed_scrollbar_x: bool,
|
||||
revealed_scrollbar_y: bool,
|
||||
}
|
||||
@@ -370,11 +371,17 @@ impl ViewportScrollPolicy {
|
||||
},
|
||||
embedder_allows_user_scroll: !user_scrolling_disabled,
|
||||
embedder_allows_scrollbar_controls: !user_scrolling_disabled,
|
||||
scrollbars_hidden: false,
|
||||
revealed_scrollbar_x: false,
|
||||
revealed_scrollbar_y: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn with_scrollbars_hidden(mut self, hidden: bool) -> Self {
|
||||
self.scrollbars_hidden = hidden;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) const fn defining_body(self) -> Option<LayoutBoxId> {
|
||||
match self.defining_box {
|
||||
ViewportDefiningBox::Root => None,
|
||||
@@ -461,7 +468,8 @@ impl ViewportScrollPolicy {
|
||||
}
|
||||
|
||||
const fn allows_scrollbar_controls(self) -> bool {
|
||||
self.embedder_allows_scrollbar_controls
|
||||
!self.scrollbars_hidden
|
||||
&& self.embedder_allows_scrollbar_controls
|
||||
&& !matches!(self.scrollbar_style.width, LayoutScrollbarWidth::None)
|
||||
}
|
||||
|
||||
|
||||
@@ -1160,6 +1160,77 @@ fn thin_and_hidden_scrollbars_match_chromium_client_geometry() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_scrollbar_policy_preserves_explicit_stable_gutters_without_controls() {
|
||||
let source = Source(vec![
|
||||
Node::element("root", vec![1]),
|
||||
Node::element("scroller", vec![2]),
|
||||
Node::element("content", Vec::new()),
|
||||
]);
|
||||
// Chromium --hide-scrollbars removes automatic gutters, but stable gutters
|
||||
// still use the authored auto/thin width even though no UI is painted.
|
||||
for (width, thickness) in [
|
||||
(LayoutScrollbarWidth::Auto, 15.0),
|
||||
(LayoutScrollbarWidth::Thin, 10.0),
|
||||
(LayoutScrollbarWidth::None, 0.0),
|
||||
] {
|
||||
for (gutter, edges) in [
|
||||
(LayoutScrollbarGutter::Auto, 0.0),
|
||||
(LayoutScrollbarGutter::Stable, 1.0),
|
||||
(LayoutScrollbarGutter::StableBothEdges, 2.0),
|
||||
] {
|
||||
let mut styles = Styles::default();
|
||||
styles
|
||||
.0
|
||||
.insert(0, fixed_size(LayoutDisplay::Block, 320.0, 240.0));
|
||||
let mut scroller = resolved(
|
||||
LayoutDisplay::Block,
|
||||
Style {
|
||||
size: Size {
|
||||
width: length(200.0),
|
||||
height: length(100.0),
|
||||
},
|
||||
overflow: Point {
|
||||
x: Overflow::Scroll,
|
||||
y: Overflow::Scroll,
|
||||
},
|
||||
..Style::default()
|
||||
},
|
||||
);
|
||||
scroller.set_scrollbar_style(width, gutter, None);
|
||||
scroller.suppress_scrollbars();
|
||||
styles.0.insert(1, scroller);
|
||||
styles
|
||||
.0
|
||||
.insert(2, fixed_size(LayoutDisplay::Block, 400.0, 300.0));
|
||||
let output = build(&source, &mut styles);
|
||||
let metrics = output.element_metrics_for_source(1).unwrap();
|
||||
assert_eq!(
|
||||
metrics.client_size,
|
||||
moli_layout::LayoutSize::new(200.0 - edges * thickness, 100.0)
|
||||
);
|
||||
assert_eq!(
|
||||
metrics.client_border.x,
|
||||
if edges == 2.0 { thickness } else { 0.0 }
|
||||
);
|
||||
assert_eq!(
|
||||
metrics.scroll_size,
|
||||
moli_layout::LayoutSize::new(400.0, 300.0)
|
||||
);
|
||||
let box_id = output.source_output(1).unwrap().principal_box.unwrap();
|
||||
let extent = output.scroll_extent(box_id).unwrap();
|
||||
assert!(extent.horizontal_scrollbar.is_none());
|
||||
assert!(extent.vertical_scrollbar.is_none());
|
||||
assert!(extent.scrollbar_corner.is_none());
|
||||
assert!(
|
||||
output
|
||||
.scrollbar_hit_test(LayoutPoint::new(190.0, 30.0))
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_both_edges_reserves_and_offsets_both_chromium_gutters() {
|
||||
let source = Source(vec![
|
||||
|
||||
@@ -751,6 +751,7 @@ struct AppState {
|
||||
optional_resource_fetch_mask: OptionalResourceFetchMask,
|
||||
subframe_loading_enabled: bool,
|
||||
layout_policy: LayoutPolicy,
|
||||
scrollbars_hidden: bool,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -826,6 +827,7 @@ impl AppState {
|
||||
optional_resource_fetch_mask: navigation_runtime_config.optional_resource_fetch_mask(),
|
||||
subframe_loading_enabled: navigation_runtime_config.subframe_loading_enabled(),
|
||||
layout_policy: navigation_runtime_config.layout_policy(),
|
||||
scrollbars_hidden: navigation_runtime_config.scrollbars_hidden(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_bidi_hidden_scrollbars_preserve_server_configuration() {
|
||||
let config = protocol_server_test_runtime_config(
|
||||
protocol_server_test_fetch_config(FetchConfig::default()),
|
||||
OptionalResourceFetchMask::NONE,
|
||||
)
|
||||
.with_scrollbars_hidden(true);
|
||||
let (addr, server) = spawn_test_protocol_server_with_runtime_config(config).await;
|
||||
let (mut socket, context) = bidi_session_with_context(addr).await;
|
||||
let viewport = send_bidi_command(
|
||||
&mut socket,
|
||||
3,
|
||||
"browsingContext.setViewport",
|
||||
json!({
|
||||
"context":context,"viewport":{"width":1280,"height":720}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(viewport["type"], "success", "{viewport}");
|
||||
let navigated = send_bidi_command(&mut socket,4,"browsingContext.navigate",json!({
|
||||
"context":context,"url":"data:text/html,<!doctype html><body style='margin:0'><div style='width:100vw;height:2000px'></div>","wait":"complete"
|
||||
})).await;
|
||||
assert_eq!(navigated["type"], "success", "{navigated}");
|
||||
let captured = send_bidi_command(
|
||||
&mut socket,
|
||||
5,
|
||||
"browsingContext.captureScreenshot",
|
||||
json!({"context":context}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(captured["type"], "success", "{captured}");
|
||||
let metrics = send_bidi_command(&mut socket,6,"script.evaluate",json!({
|
||||
"target":{"context":context},"awaitPromise":false,
|
||||
"expression":"JSON.stringify([innerWidth,document.documentElement.clientWidth,document.documentElement.scrollWidth])"
|
||||
})).await;
|
||||
assert_eq!(metrics["type"], "success", "{metrics}");
|
||||
assert_eq!(metrics["result"]["result"]["value"], "[1280,1280,1280]");
|
||||
let _ = socket.close(None).await;
|
||||
abort_test_cdp_server(server).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_bidi_page_and_box_screenshot_publish_real_png() {
|
||||
// Ported from Chromium/WPT
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn webdriver_classic_hidden_scrollbars_preserve_server_configuration() {
|
||||
let state = AppState::new_with_storage_partition_and_runtime_config(
|
||||
"127.0.0.1:9222".parse().unwrap(),
|
||||
Arc::new(StoragePartitionState::open(None).unwrap()),
|
||||
protocol_server_test_runtime_config(
|
||||
protocol_server_test_fetch_config(FetchConfig::default()),
|
||||
OptionalResourceFetchMask::NONE,
|
||||
)
|
||||
.with_scrollbars_hidden(true),
|
||||
crate::config::DEFAULT_SCREENCAST_INTERVAL_MS,
|
||||
)
|
||||
.unwrap();
|
||||
let app = build_router(state);
|
||||
let session = classic_request_json(app.clone(), Method::POST, "/session").await;
|
||||
let id = session["value"]["sessionId"].as_str().expect("session id");
|
||||
let navigated = classic_request_json_with_body(app.clone(),Method::POST,&format!("/session/{id}/url"),json!({
|
||||
"url":"data:text/html,<!doctype html><body style='margin:0'><div style='width:100vw;height:2000px'></div>"
|
||||
})).await;
|
||||
assert_eq!(navigated, json!({"value":null}));
|
||||
classic_capture_layout(app.clone(), id).await;
|
||||
let metrics = classic_request_json_with_body(app.clone(),Method::POST,&format!("/session/{id}/execute/sync"),json!({
|
||||
"script":"return [innerWidth - document.documentElement.clientWidth,document.documentElement.scrollWidth - innerWidth];",
|
||||
"args":[]
|
||||
})).await;
|
||||
assert_eq!(metrics, json!({"value":[0,0]}));
|
||||
let deleted = classic_request_json(app, Method::DELETE, &format!("/session/{id}")).await;
|
||||
assert_eq!(deleted, json!({"value":null}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn webdriver_classic_element_equality_cases_ported_from_selenium() {
|
||||
// Ported from Selenium's Python element_equality_tests.py:
|
||||
|
||||
@@ -156,6 +156,97 @@ fn pixel(pixels: &[u8], width: u32, x: u32, y: u32) -> [u8; 4] {
|
||||
.expect("one RGBA pixel")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn websocket_cdp_hidden_scrollbars_follow_targets_navigation_and_resize() {
|
||||
let config = protocol_server_test_runtime_config(
|
||||
protocol_server_test_fetch_config(FetchConfig::default()),
|
||||
OptionalResourceFetchMask::NONE,
|
||||
)
|
||||
.with_scrollbars_hidden(true);
|
||||
let (addr, server) = spawn_test_protocol_server_with_runtime_config(config).await;
|
||||
let (mut socket, _) =
|
||||
connect_async(format!("ws://{addr}/devtools/browser/{DEFAULT_BROWSER_ID}"))
|
||||
.await
|
||||
.expect("CDP socket");
|
||||
let url = "data:text/html,<!doctype html><body style='margin:0'><div style='width:100vw;height:2000px;background:rgb(0,255,0)'></div>";
|
||||
for _ in 0..2 {
|
||||
let target = open_screenshot_target(&mut socket, url).await;
|
||||
for width in [1280, 1000] {
|
||||
assert_cdp_success(
|
||||
&send_cdp_command(
|
||||
&mut socket,
|
||||
20,
|
||||
"Emulation.setDeviceMetricsOverride",
|
||||
Some(&target.session_id),
|
||||
json!({"width":width,"height":720,"deviceScaleFactor":1,"mobile":false}),
|
||||
)
|
||||
.await,
|
||||
20,
|
||||
);
|
||||
let png = capture_png(&mut socket, &target.session_id, 21).await;
|
||||
let (image_width, _, pixels) = decode_png(&png);
|
||||
assert_eq!(image_width, width);
|
||||
assert_eq!(pixel(&pixels, width, width - 8, 300), [0, 255, 0, 255]);
|
||||
let result = send_cdp_command(
|
||||
&mut socket, 22, "Runtime.evaluate", Some(&target.session_id),
|
||||
json!({"expression":"JSON.stringify([innerWidth,document.documentElement.clientWidth,document.documentElement.scrollWidth])","returnByValue":true})
|
||||
).await;
|
||||
assert_cdp_success(&result, 22);
|
||||
assert_eq!(
|
||||
response_by_id(&result, 22)["result"]["result"]["value"],
|
||||
json!(format!("[{width},{width},{width}]"))
|
||||
);
|
||||
}
|
||||
assert_cdp_success(
|
||||
&send_cdp_command(
|
||||
&mut socket,
|
||||
23,
|
||||
"Input.dispatchMouseEvent",
|
||||
Some(&target.session_id),
|
||||
json!({"type":"mouseWheel","x":100,"y":300,"deltaX":0,"deltaY":100}),
|
||||
)
|
||||
.await,
|
||||
23,
|
||||
);
|
||||
let scroll = send_cdp_command(
|
||||
&mut socket,
|
||||
24,
|
||||
"Runtime.evaluate",
|
||||
Some(&target.session_id),
|
||||
json!({"expression":"scrollY > 0","returnByValue":true}),
|
||||
)
|
||||
.await;
|
||||
assert_cdp_success(&scroll, 24);
|
||||
assert_eq!(
|
||||
response_by_id(&scroll, 24)["result"]["result"]["value"],
|
||||
true
|
||||
);
|
||||
assert_cdp_success(
|
||||
&cdp_navigate_and_wait_for_load(&mut socket, 25, &target.session_id, url).await,
|
||||
25,
|
||||
);
|
||||
capture_png(&mut socket, &target.session_id, 26).await;
|
||||
let result = send_cdp_command(
|
||||
&mut socket, 27, "Runtime.evaluate", Some(&target.session_id),
|
||||
json!({"expression":"innerWidth === document.documentElement.clientWidth","returnByValue":true})
|
||||
).await;
|
||||
assert_cdp_success(&result, 27);
|
||||
assert_eq!(
|
||||
response_by_id(&result, 27)["result"]["result"]["value"],
|
||||
true
|
||||
);
|
||||
send_cdp_command(
|
||||
&mut socket,
|
||||
28,
|
||||
"Target.closeTarget",
|
||||
None,
|
||||
json!({"targetId":target.target_id}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
abort_test_cdp_server(server).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn websocket_cdp_capture_screenshot_tracks_paint_and_layout_mutations() {
|
||||
let (fixture_addr, _fixture_server) =
|
||||
|
||||
@@ -76,7 +76,8 @@ pub(super) async fn ws_bidi_session_upgrade_handler(
|
||||
state.optional_resource_fetch_mask,
|
||||
state.subframe_loading_enabled,
|
||||
state.layout_policy,
|
||||
);
|
||||
)
|
||||
.with_scrollbars_hidden(state.scrollbars_hidden);
|
||||
ws.on_upgrade(move |socket| {
|
||||
handle_bidi_session_socket(
|
||||
socket,
|
||||
|
||||
@@ -1669,7 +1669,8 @@ pub(super) async fn webdriver_classic_new_session(
|
||||
state.optional_resource_fetch_mask,
|
||||
state.subframe_loading_enabled,
|
||||
state.layout_policy,
|
||||
),
|
||||
)
|
||||
.with_scrollbars_hidden(state.scrollbars_hidden),
|
||||
);
|
||||
let create_context = ClassicDevToolsCommandContext::new(session.session_id.as_str());
|
||||
let target_id = match runtime
|
||||
|
||||
@@ -244,6 +244,36 @@ fn idle_navigation_engine_reset_preserves_mock_layout_policy() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hidden_scrollbars_survive_idle_reset_and_browser_context_removal() {
|
||||
let mut conn = CdpConnection::new_with_initial_storage_partition_and_runtime_config(
|
||||
crate::CdpInitialStoragePartition::memory(),
|
||||
NavigationRuntimeConfig::new(
|
||||
FetchConfig::default(),
|
||||
OptionalResourceFetchMask::NONE,
|
||||
true,
|
||||
LayoutPolicy::OnDemand,
|
||||
)
|
||||
.with_scrollbars_hidden(true),
|
||||
);
|
||||
assert!(conn.release_idle_navigation_engine_memory_if_idle().reset);
|
||||
assert!(
|
||||
conn.standalone_navigation_engine
|
||||
.runtime_config()
|
||||
.scrollbars_hidden()
|
||||
);
|
||||
conn.insert_browser_context(BrowserContext::new("CTX-hidden-scrollbars".to_owned()));
|
||||
let removed = conn
|
||||
.remove_browser_context_by_id_restoring_active_async("CTX-hidden-scrollbars", None)
|
||||
.await;
|
||||
assert!(removed.is_some());
|
||||
assert!(
|
||||
conn.standalone_navigation_engine
|
||||
.runtime_config()
|
||||
.scrollbars_hidden()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn browser_context_install_and_removal_preserve_mock_layout_policy() {
|
||||
let mut conn = CdpConnection::new_with_initial_storage_partition_and_runtime_config(
|
||||
|
||||
@@ -14,6 +14,7 @@ pub(super) struct NativeLayoutStyleResolver<'a> {
|
||||
runtime: &'a JsContextHost,
|
||||
reads: StyleObservation<'a>,
|
||||
scripting_enabled: bool,
|
||||
scrollbars_hidden: bool,
|
||||
profile: Option<NativeLayoutStyleResolverProfile>,
|
||||
}
|
||||
|
||||
@@ -46,6 +47,7 @@ impl<'a> NativeLayoutStyleResolver<'a> {
|
||||
runtime,
|
||||
reads,
|
||||
scripting_enabled: runtime.document_scripting_enabled(document),
|
||||
scrollbars_hidden: runtime.scrollbars_hidden(),
|
||||
profile: moli_trace::cpu_profile_enabled()
|
||||
.then(NativeLayoutStyleResolverProfile::default),
|
||||
}
|
||||
@@ -138,8 +140,14 @@ impl LayoutStyleResolver<DomHandle> for NativeLayoutStyleResolver<'_> {
|
||||
);
|
||||
}
|
||||
let phase_started = self.profile.as_ref().map(|_| Instant::now());
|
||||
let before = before.map(ResolvedLayoutStyle::from_stylo);
|
||||
let after = after.map(ResolvedLayoutStyle::from_stylo);
|
||||
let mut before = before.map(ResolvedLayoutStyle::from_stylo);
|
||||
let mut after = after.map(ResolvedLayoutStyle::from_stylo);
|
||||
if self.scrollbars_hidden {
|
||||
resolved.suppress_scrollbars();
|
||||
for pseudo in before.iter_mut().chain(after.iter_mut()) {
|
||||
pseudo.suppress_scrollbars();
|
||||
}
|
||||
}
|
||||
if let Some(profile) = self.profile.as_mut() {
|
||||
profile.eager_pseudo_count = profile
|
||||
.eager_pseudo_count
|
||||
@@ -200,7 +208,11 @@ impl LayoutStyleResolver<DomHandle> for NativeLayoutStyleResolver<'_> {
|
||||
}
|
||||
let phase_started = self.profile.as_ref().map(|_| Instant::now());
|
||||
let resolved = computed.map(|computed| {
|
||||
ResolvedLayoutPseudoStyle::new(ResolvedLayoutStyle::from_stylo(computed))
|
||||
let mut resolved = ResolvedLayoutStyle::from_stylo(computed);
|
||||
if self.scrollbars_hidden {
|
||||
resolved.suppress_scrollbars();
|
||||
}
|
||||
ResolvedLayoutPseudoStyle::new(resolved)
|
||||
});
|
||||
if let Some(profile) = self.profile.as_mut() {
|
||||
profile.marker_projection_ns = profile.marker_projection_ns.saturating_add(
|
||||
@@ -246,6 +258,9 @@ impl LayoutStyleResolver<DomHandle> for NativeLayoutStyleResolver<'_> {
|
||||
let phase_started = self.profile.as_ref().map(|_| Instant::now());
|
||||
let mut resolved = ResolvedLayoutStyle::from_stylo(computed);
|
||||
resolved.force_layout_display(display);
|
||||
if self.scrollbars_hidden {
|
||||
resolved.suppress_scrollbars();
|
||||
}
|
||||
if let Some(profile) = self.profile.as_mut() {
|
||||
profile.anonymous_projection_ns = profile.anonymous_projection_ns.saturating_add(
|
||||
phase_started
|
||||
|
||||
@@ -199,6 +199,7 @@ impl JsContextHost {
|
||||
pending_history_traversal_admissions: Default::default(),
|
||||
runtime: runtime as *mut DocumentRuntime,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
document_layout_state: RefCell::new(super::layout_state::DocumentLayoutState::default()),
|
||||
layout_pass_active: Cell::new(false),
|
||||
completed_layout_pass_count: Cell::new(0),
|
||||
|
||||
@@ -57,6 +57,14 @@ impl JsContextHost {
|
||||
self.layout_policy
|
||||
}
|
||||
|
||||
pub(crate) fn set_scrollbars_hidden(&mut self, hidden: bool) {
|
||||
self.scrollbars_hidden = hidden;
|
||||
}
|
||||
|
||||
pub(crate) const fn scrollbars_hidden(&self) -> bool {
|
||||
self.scrollbars_hidden
|
||||
}
|
||||
|
||||
pub(crate) fn active_layout_document_handles(&self) -> Vec<DomHandle> {
|
||||
let mut documents = vec![self.document_handle()];
|
||||
documents.extend(
|
||||
|
||||
@@ -807,6 +807,7 @@ pub(crate) struct JsContextHost {
|
||||
super::history_traversal::PendingHistoryTraversalAdmissions,
|
||||
runtime: *mut DocumentRuntime,
|
||||
layout_policy: moli_page_types::LayoutPolicy,
|
||||
scrollbars_hidden: bool,
|
||||
document_layout_state: RefCell<layout_state::DocumentLayoutState>,
|
||||
layout_pass_active: Cell<bool>,
|
||||
completed_layout_pass_count: Cell<u64>,
|
||||
|
||||
@@ -171,6 +171,7 @@ pub struct RendererCreateHtmlPageRequest {
|
||||
pub fetch_subresource_interception_enabled: bool,
|
||||
pub fetch_subresource_interception_resource_type: Option<crate::SubresourceResourceType>,
|
||||
pub layout_policy: LayoutPolicy,
|
||||
pub scrollbars_hidden: bool,
|
||||
pub wpt_extensions_enabled: bool,
|
||||
pub stage: PageVmInitStage,
|
||||
pub reply_boundary: crate::RendererReplyBoundary,
|
||||
@@ -221,6 +222,7 @@ pub struct RendererCreateStreamingRawPageRequest {
|
||||
pub fetch_subresource_interception_enabled: bool,
|
||||
pub fetch_subresource_interception_resource_type: Option<crate::SubresourceResourceType>,
|
||||
pub layout_policy: LayoutPolicy,
|
||||
pub scrollbars_hidden: bool,
|
||||
pub wpt_extensions_enabled: bool,
|
||||
pub stage: PageVmInitStage,
|
||||
pub reply_boundary: crate::RendererReplyBoundary,
|
||||
@@ -1034,7 +1036,7 @@ pub(super) struct RendererOwnerState {
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct RendererOwnerLayoutPolicyState {
|
||||
policy: Option<LayoutPolicy>,
|
||||
configuration: Option<(LayoutPolicy, bool)>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -7,27 +7,52 @@ impl RendererOwnerHandle {
|
||||
/// is constructed. Page construction seals the value so a shared renderer
|
||||
/// owner can never host Pages with conflicting browser-level policies.
|
||||
pub fn configure_layout_policy(&self, policy: LayoutPolicy) -> Result<()> {
|
||||
self.configure_layout(policy, false)
|
||||
}
|
||||
|
||||
/// Fixes both layout policy and scrollbar visibility before Page creation.
|
||||
pub fn configure_layout(&self, policy: LayoutPolicy, scrollbars_hidden: bool) -> Result<()> {
|
||||
ensure!(
|
||||
!scrollbars_hidden || policy.uses_real_layout(),
|
||||
"hiding scrollbars requires real layout"
|
||||
);
|
||||
let mut state = self.state.layout_policy.lock();
|
||||
if let Some(configured) = state.policy {
|
||||
if let Some((configured_policy, configured_hidden)) = state.configuration {
|
||||
ensure!(
|
||||
configured == policy,
|
||||
"renderer owner layout policy is configured as {:?}, cannot change it to {:?}",
|
||||
configured,
|
||||
policy
|
||||
(configured_policy, configured_hidden) == (policy, scrollbars_hidden),
|
||||
"renderer owner layout policy is configured as {:?} (scrollbars hidden: {}), cannot change it to {:?} (scrollbars hidden: {})",
|
||||
configured_policy,
|
||||
configured_hidden,
|
||||
policy,
|
||||
scrollbars_hidden
|
||||
);
|
||||
} else {
|
||||
state.policy = Some(policy);
|
||||
state.configuration = Some((policy, scrollbars_hidden));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn layout_policy(&self) -> LayoutPolicy {
|
||||
self.state.layout_policy.lock().policy.unwrap_or_default()
|
||||
self.state
|
||||
.layout_policy
|
||||
.lock()
|
||||
.configuration
|
||||
.unwrap_or_default()
|
||||
.0
|
||||
}
|
||||
|
||||
pub(super) fn seal_layout_policy_for_page_creation(&self) -> LayoutPolicy {
|
||||
pub fn scrollbars_hidden(&self) -> bool {
|
||||
self.state
|
||||
.layout_policy
|
||||
.lock()
|
||||
.configuration
|
||||
.unwrap_or_default()
|
||||
.1
|
||||
}
|
||||
|
||||
pub(super) fn seal_layout_configuration_for_page_creation(&self) -> (LayoutPolicy, bool) {
|
||||
let mut state = self.state.layout_policy.lock();
|
||||
*state.policy.get_or_insert_with(LayoutPolicy::default)
|
||||
*state.configuration.get_or_insert_with(Default::default)
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
|
||||
@@ -657,6 +657,7 @@ impl RendererOwnerHandle {
|
||||
stage: PageVmInitStage,
|
||||
options: crate::RendererDocumentOptions,
|
||||
) -> RendererCreateHtmlPageRequest {
|
||||
let (layout_policy, scrollbars_hidden) = self.seal_layout_configuration_for_page_creation();
|
||||
RendererCreateHtmlPageRequest {
|
||||
page_reservation,
|
||||
root_frame_id: options.root_frame_id,
|
||||
@@ -694,7 +695,8 @@ impl RendererOwnerHandle {
|
||||
fetch_subresource_interception_enabled: options.fetch_subresource_interception_enabled,
|
||||
fetch_subresource_interception_resource_type: options
|
||||
.fetch_subresource_interception_resource_type,
|
||||
layout_policy: self.seal_layout_policy_for_page_creation(),
|
||||
layout_policy,
|
||||
scrollbars_hidden,
|
||||
wpt_extensions_enabled: false,
|
||||
stage,
|
||||
reply_boundary: crate::RendererReplyBoundary::Stage,
|
||||
@@ -721,6 +723,7 @@ impl RendererOwnerHandle {
|
||||
stage: PageVmInitStage,
|
||||
options: crate::RendererDocumentOptions,
|
||||
) -> RendererCreateStreamingRawPageRequest {
|
||||
let (layout_policy, scrollbars_hidden) = self.seal_layout_configuration_for_page_creation();
|
||||
RendererCreateStreamingRawPageRequest {
|
||||
document_replacement: None,
|
||||
root_frame_id: options.root_frame_id,
|
||||
@@ -758,7 +761,8 @@ impl RendererOwnerHandle {
|
||||
fetch_subresource_interception_enabled: options.fetch_subresource_interception_enabled,
|
||||
fetch_subresource_interception_resource_type: options
|
||||
.fetch_subresource_interception_resource_type,
|
||||
layout_policy: self.seal_layout_policy_for_page_creation(),
|
||||
layout_policy,
|
||||
scrollbars_hidden,
|
||||
wpt_extensions_enabled: false,
|
||||
stage,
|
||||
reply_boundary: crate::RendererReplyBoundary::Stage,
|
||||
@@ -832,6 +836,7 @@ impl RendererOwnerHandle {
|
||||
fetch_subresource_interception_enabled,
|
||||
fetch_subresource_interception_resource_type,
|
||||
layout_policy,
|
||||
scrollbars_hidden,
|
||||
wpt_extensions_enabled,
|
||||
stage,
|
||||
reply_boundary,
|
||||
@@ -921,6 +926,7 @@ impl RendererOwnerHandle {
|
||||
fetch_subresource_interception_enabled,
|
||||
fetch_subresource_interception_resource_type,
|
||||
layout_policy,
|
||||
scrollbars_hidden,
|
||||
wpt_extensions_enabled,
|
||||
root_frame_id,
|
||||
main_document_commit,
|
||||
@@ -1144,6 +1150,7 @@ impl RendererOwnerHandle {
|
||||
fetch_subresource_interception_enabled,
|
||||
fetch_subresource_interception_resource_type,
|
||||
layout_policy,
|
||||
scrollbars_hidden,
|
||||
wpt_extensions_enabled,
|
||||
stage,
|
||||
reply_boundary,
|
||||
@@ -1221,6 +1228,7 @@ impl RendererOwnerHandle {
|
||||
fetch_subresource_interception_enabled,
|
||||
fetch_subresource_interception_resource_type,
|
||||
layout_policy,
|
||||
scrollbars_hidden,
|
||||
wpt_extensions_enabled,
|
||||
root_frame_id,
|
||||
main_document_commit,
|
||||
|
||||
@@ -1207,6 +1207,7 @@ impl PageVm {
|
||||
fetch_subresource_interception_resource_type: self
|
||||
.fetch_subresource_interception_resource_type,
|
||||
layout_policy: self.layout_policy,
|
||||
scrollbars_hidden: self.scrollbars_hidden,
|
||||
wpt_extensions_enabled: self.wpt_extensions_enabled,
|
||||
root_frame_id: self.vm().root_frame_id().map(str::to_owned),
|
||||
top_level_storage_key: None,
|
||||
|
||||
@@ -1095,6 +1095,7 @@ pub(crate) struct PageVmEnvConfig {
|
||||
pub(crate) fetch_subresource_interception_enabled: bool,
|
||||
pub(crate) fetch_subresource_interception_resource_type: Option<crate::SubresourceResourceType>,
|
||||
pub(crate) layout_policy: LayoutPolicy,
|
||||
pub(crate) scrollbars_hidden: bool,
|
||||
pub(crate) wpt_extensions_enabled: bool,
|
||||
pub(crate) navigation_bootstrap_entry: Option<crate::native_bridge::NavigationHistoryEntrySeed>,
|
||||
pub(crate) reserved_service_worker_client_id:
|
||||
@@ -1616,6 +1617,7 @@ pub(crate) struct PageVm {
|
||||
pub(super) fetch_subresource_interception_enabled: bool,
|
||||
pub(super) fetch_subresource_interception_resource_type: Option<crate::SubresourceResourceType>,
|
||||
pub(super) layout_policy: LayoutPolicy,
|
||||
pub(super) scrollbars_hidden: bool,
|
||||
pub(super) wpt_extensions_enabled: bool,
|
||||
pub(crate) runtime_hooks: PageVmRuntimeHooks,
|
||||
pub(super) navigation_response: Option<PageVmNavigationResponse>,
|
||||
@@ -4294,6 +4296,7 @@ impl PageVm {
|
||||
let mut vm = vm_bootstrap.finish()?;
|
||||
vm.set_document_navigator_identity(&env.navigator_identity);
|
||||
vm.set_layout_policy(env.layout_policy);
|
||||
vm.set_scrollbars_hidden(env.scrollbars_hidden);
|
||||
vm.install_page_task_capabilities(page_task_capabilities);
|
||||
vm.set_root_document_lifecycle(document_lifecycle.clone());
|
||||
let dom_agent_state = vm.renderer_dom_agent_state();
|
||||
@@ -4357,6 +4360,7 @@ impl PageVm {
|
||||
fetch_subresource_interception_resource_type: env
|
||||
.fetch_subresource_interception_resource_type,
|
||||
layout_policy: env.layout_policy,
|
||||
scrollbars_hidden: env.scrollbars_hidden,
|
||||
wpt_extensions_enabled: env.wpt_extensions_enabled,
|
||||
runtime_hooks,
|
||||
navigation_response: None,
|
||||
|
||||
@@ -697,6 +697,7 @@ fn minimal_test_page_vm_env_config() -> PageVmEnvConfig {
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: crate::real_layout_test_policy(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
|
||||
@@ -39,6 +39,7 @@ fn default_runtime_hooks_reject_direct_no_owner_page_vm_construction() {
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: crate::real_layout_test_policy(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
|
||||
@@ -689,6 +689,7 @@ fn test_page_vm_with_loader_dom_host_hooks_and_response_referrer_policy(
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: crate::real_layout_test_policy(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
|
||||
@@ -52,6 +52,7 @@ async fn dispatch_main_document_domcontentloaded_for_rendering_test(
|
||||
|
||||
mod layout_geometry;
|
||||
mod painting_tables;
|
||||
mod scrollbars;
|
||||
mod text_styles;
|
||||
mod update_lifecycle;
|
||||
mod visual_output;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn hidden_scrollbars_preserve_geometry_scrolling_and_computed_css_in_all_frames() {
|
||||
run_page_vm_async_test(async move {
|
||||
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?;
|
||||
let mut page = test_page_vm_with_loader_and_document_url(
|
||||
&loader,
|
||||
Vec::new(),
|
||||
Url::parse("https://example.com/hidden-scrollbars.html")?,
|
||||
);
|
||||
page.vm_mut().set_scrollbars_hidden(true);
|
||||
page.vm_mut().set_viewport_surface(Some(crate::protocol_types::ViewportSurface {
|
||||
inner_width: 1280,
|
||||
inner_height: 720,
|
||||
device_pixel_ratio: 1.0,
|
||||
..Default::default()
|
||||
}))?;
|
||||
page.vm_mut().eval(r#"
|
||||
document.head.innerHTML = `<style>
|
||||
html,body { margin:0; padding:0; }
|
||||
html { scrollbar-width:auto !important; }
|
||||
#viewport { width:100vw; height:2000px; background:rgb(0,255,0); }
|
||||
.scroller { position:absolute; top:0; width:200px; height:100px; overflow:scroll;
|
||||
scrollbar-width:auto !important; scrollbar-color:red blue; }
|
||||
.content { width:400px; height:300px; background:red; }
|
||||
#auto { left:0; }
|
||||
#thin { left:220px; direction:rtl; scrollbar-width:thin !important; }
|
||||
#vertical { left:440px; writing-mode:vertical-rl; }
|
||||
#shadow-host { position:absolute; left:660px; top:0; }
|
||||
#pseudo { position:absolute; left:880px; top:0; }
|
||||
#pseudo::before { content:''; display:block; width:200px; height:100px; overflow:scroll;
|
||||
scrollbar-color:blue blue; background:red; }
|
||||
iframe { position:absolute; left:0; top:180px; width:300px; height:200px; border:0; }
|
||||
</style>`;
|
||||
document.body.innerHTML = `<div id=viewport></div>
|
||||
<div id=auto class=scroller><div class=content></div></div>
|
||||
<div id=thin class=scroller><div class=content></div></div>
|
||||
<div id=vertical class=scroller><div class=content></div></div>
|
||||
<div id=shadow-host></div><div id=pseudo></div><iframe id=frame></iframe>`;
|
||||
const shadow = document.getElementById('shadow-host').attachShadow({mode:'open'});
|
||||
shadow.innerHTML = `<div id=scroller style='width:200px;height:100px;overflow:auto'>
|
||||
<div style='width:400px;height:300px;background:red'></div></div>`;
|
||||
const child = document.getElementById('frame').contentDocument;
|
||||
child.documentElement.style.cssText = 'scrollbar-width:thin';
|
||||
child.body.style.margin = '0';
|
||||
child.body.innerHTML = `<div style='width:100vw;height:2000px'></div>
|
||||
<iframe id=nested style='position:absolute;left:0;top:0;width:120px;height:80px;border:0'></iframe>`;
|
||||
const nested = child.getElementById('nested').contentDocument;
|
||||
nested.body.style.margin = '0';
|
||||
nested.body.innerHTML = `<div style='width:100vw;height:2000px'></div>`;
|
||||
'installed'
|
||||
"#)?;
|
||||
page.vm_mut().sync_live_document_style_sources();
|
||||
let snapshot = page.vm_mut().screenshot_layout_snapshot(
|
||||
moli_layout::PaintViewport::new(1280, 720, 1.0),
|
||||
)?.expect("fixture layout");
|
||||
{
|
||||
let host = page.vm().context_host_weak_for_test().upgrade().expect("context host");
|
||||
let host = host.borrow();
|
||||
assert!(host.with_latest_layout_tree_for_document(host.document_handle(), |tree| {
|
||||
for (x, y) in [(190.0, 30.0), (1270.0, 30.0), (1070.0, 30.0)] {
|
||||
assert!(tree.scrollbar_hit_test(moli_layout::LayoutPoint::new(x, y)).is_none());
|
||||
}
|
||||
}).is_some());
|
||||
}
|
||||
let image = moli_paint::raster_snapshot(&snapshot)?;
|
||||
let pixel = |x: u32, y: u32| {
|
||||
let offset = ((y * image.width + x) * 4) as usize;
|
||||
<[u8; 4]>::try_from(&image.rgba[offset..offset + 4]).unwrap()
|
||||
};
|
||||
assert_eq!(pixel(1270, 500), [0, 255, 0, 255], "viewport has no scrollbar paint");
|
||||
assert_eq!(pixel(190, 70), [255, 0, 0, 255], "element has no scrollbar paint");
|
||||
assert_eq!(pixel(1070, 70), [255, 0, 0, 255], "pseudo has no scrollbar paint");
|
||||
|
||||
let metrics: serde_json::Value = serde_json::from_str(&page.vm_mut().eval(r#"JSON.stringify({
|
||||
root: [innerWidth, document.documentElement.clientWidth, document.documentElement.scrollWidth],
|
||||
elements: ['auto','thin','vertical'].map(id => {
|
||||
const e = document.getElementById(id);
|
||||
return [e.clientWidth,e.clientHeight,e.clientLeft,e.clientTop,e.scrollWidth,e.scrollHeight];
|
||||
}),
|
||||
shadow: (() => {
|
||||
const e = document.getElementById('shadow-host').shadowRoot.getElementById('scroller');
|
||||
return [e.clientWidth,e.clientHeight,e.scrollWidth,e.scrollHeight];
|
||||
})(),
|
||||
frames: (() => {
|
||||
const child = document.getElementById('frame').contentDocument;
|
||||
const nested = child.getElementById('nested').contentDocument;
|
||||
return [child.documentElement.clientWidth,child.documentElement.scrollWidth,
|
||||
nested.documentElement.clientWidth,nested.documentElement.scrollWidth];
|
||||
})(),
|
||||
css: [getComputedStyle(document.getElementById('auto')).scrollbarWidth,
|
||||
getComputedStyle(document.getElementById('thin')).scrollbarWidth,
|
||||
getComputedStyle(document.getElementById('auto')).scrollbarGutter]
|
||||
})"#)?)?;
|
||||
assert_eq!(metrics, json!({
|
||||
"root": [1280,1280,1280],
|
||||
"elements": [[200,100,0,0,400,300],[200,100,0,0,400,300],[200,100,0,0,400,300]],
|
||||
"shadow": [200,100,400,300],
|
||||
"frames": [300,300,120,120],
|
||||
"css": ["auto","thin","auto"]
|
||||
}));
|
||||
let scroll = page.vm_mut().eval(r#"
|
||||
const scroller = document.getElementById('auto');
|
||||
scroller.scrollLeft = 20;
|
||||
scroller.scrollTop = 40;
|
||||
window.scrollTo(0,100);
|
||||
JSON.stringify([scroller.scrollLeft,scroller.scrollTop,scrollY])
|
||||
"#)?;
|
||||
assert_eq!(scroll, "[20,40,100]");
|
||||
page.vm_mut().eval("document.documentElement.style.scrollbarGutter = 'stable both-edges'")?;
|
||||
page.vm_mut().publish_layout_for_test()?;
|
||||
// Root clientWidth excludes only painted scrollbar UI, while stable
|
||||
// gutters continue to reduce the root's layout box.
|
||||
assert_eq!(page.vm_mut().eval("JSON.stringify([document.documentElement.clientWidth,document.documentElement.getBoundingClientRect().width])")?, "[1280,1250]", "explicit viewport gutters remain reserved");
|
||||
page.vm_mut().eval("document.documentElement.style.scrollbarGutter = 'auto'")?;
|
||||
page.vm_mut().eval("document.getElementById('viewport').style.width = '1400px'")?;
|
||||
page.vm_mut().publish_layout_for_test()?;
|
||||
assert_eq!(page.vm_mut().eval("JSON.stringify([document.documentElement.clientWidth,document.documentElement.scrollWidth])")?, "[1280,1400]", "real horizontal overflow remains observable");
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}).await.expect("hidden scrollbar layout");
|
||||
}
|
||||
@@ -680,6 +680,7 @@ mod tests {
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
|
||||
@@ -1083,6 +1083,7 @@ mod tests {
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
|
||||
@@ -753,6 +753,7 @@ fn parser_owner_boundary_with_live_backend_queues_document_turn_before_runtime_w
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
@@ -1044,6 +1045,7 @@ fn parser_connected_head_script_does_not_push_later_head_tokens_into_body() {
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
@@ -1223,6 +1225,7 @@ fn parser_connected_external_head_script_with_live_head_and_body_mutation_keeps_
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
@@ -1824,6 +1827,7 @@ fn parser_owner_body_stylesheet_pause_preserves_unconsumed_tail_on_live_page_vm(
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
|
||||
@@ -360,6 +360,7 @@ fn external_async_handoff_marks_parser_stream_already_started_on_live_backend()
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
@@ -486,6 +487,7 @@ fn blocking_classic_handoff_registers_parser_owned_handle_on_live_backend() {
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
@@ -622,6 +624,7 @@ fn non_async_post_parse_handoff_registers_pending_before_source_and_seals_withou
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
@@ -793,6 +796,7 @@ fn parser_owned_external_module_handoff_starts_pending_script_tree_root_fetch()
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
@@ -1110,6 +1114,7 @@ fn parser_owned_inline_importmap_handoff_registers_parser_owned_handle_on_live_b
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
@@ -1277,6 +1282,7 @@ fn parser_owner_style_import_handoff_is_stylesheet_gated_on_live_page_vm() {
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
|
||||
+1
@@ -1154,6 +1154,7 @@ fn parser_connected_head_document_write_keeps_later_head_tokens_in_head() {
|
||||
fetch_subresource_interception_enabled: false,
|
||||
fetch_subresource_interception_resource_type: None,
|
||||
layout_policy: moli_page_types::LayoutPolicy::default(),
|
||||
scrollbars_hidden: false,
|
||||
wpt_extensions_enabled: false,
|
||||
navigation_bootstrap_entry: None,
|
||||
reserved_service_worker_client_id: None,
|
||||
|
||||
@@ -198,6 +198,12 @@ impl ScriptVm {
|
||||
self._context_host.borrow_mut().set_layout_policy(policy);
|
||||
}
|
||||
|
||||
pub(crate) fn set_scrollbars_hidden(&mut self, hidden: bool) {
|
||||
self._context_host
|
||||
.borrow_mut()
|
||||
.set_scrollbars_hidden(hidden);
|
||||
}
|
||||
|
||||
pub(super) fn sync_document_fonts_for_environment(&mut self) {
|
||||
self.renderer_document_isolate
|
||||
.with_renderer_document_isolate_mut(|isolate| {
|
||||
|
||||
@@ -594,6 +594,10 @@ pub struct CommonArgs {
|
||||
)]
|
||||
pub layout: bool,
|
||||
|
||||
/// Hide native scrollbars without disabling scrolling. Requires --layout.
|
||||
#[arg(long, requires = "layout")]
|
||||
pub hide_scrollbars: bool,
|
||||
|
||||
#[arg(short, long = "cookie-file")]
|
||||
pub cookie_file: Vec<String>,
|
||||
|
||||
|
||||
@@ -90,6 +90,10 @@ impl AppConfig {
|
||||
}
|
||||
|
||||
fn apply_common_args(config: &mut AppConfig, common: &CommonArgs) -> Result<()> {
|
||||
if common.hide_scrollbars && !common.layout {
|
||||
bail!("--hide-scrollbars requires --layout or MOLI_LAYOUT=true");
|
||||
}
|
||||
|
||||
if let Some(log_level) = common.log_level {
|
||||
config.log_filter = log_level.as_tracing_filter().to_owned();
|
||||
}
|
||||
@@ -188,6 +192,7 @@ fn apply_common_args(config: &mut AppConfig, common: &CommonArgs) -> Result<()>
|
||||
} else {
|
||||
LayoutPolicy::Mock
|
||||
});
|
||||
config.browser.set_scrollbars_hidden(common.hide_scrollbars);
|
||||
config.fetch.cookie_files = common.cookie_file.clone();
|
||||
config.browser.fetch_mut().set_network_blocking(
|
||||
common.block_private_networks,
|
||||
|
||||
@@ -140,6 +140,7 @@ fn parses_explicit_fetch_command_with_compatibility_flags() {
|
||||
resource: false,
|
||||
disable_subframes: false,
|
||||
layout: false,
|
||||
hide_scrollbars: false,
|
||||
cookie_file: Vec::new(),
|
||||
document_start_script: Vec::new(),
|
||||
document_start_script_file: Vec::new(),
|
||||
@@ -1642,6 +1643,96 @@ fn layout_selects_on_demand_policy_for_fetch_and_serve() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_scrollbars_is_opt_in_for_fetch_and_serve() {
|
||||
for command in ["fetch", "serve"] {
|
||||
for hidden in [false, true] {
|
||||
let mut args = vec!["moli", command, "--layout"];
|
||||
if hidden {
|
||||
args.push("--hide-scrollbars");
|
||||
}
|
||||
if command == "fetch" {
|
||||
args.push("about:blank");
|
||||
}
|
||||
let cli = Cli::try_parse_from(args).unwrap();
|
||||
let config = AppConfig::from_cli(&cli).unwrap();
|
||||
assert_eq!(config.browser.scrollbars_hidden(), hidden);
|
||||
assert_eq!(
|
||||
config.browser.layout_policy(),
|
||||
moli_core::LayoutPolicy::OnDemand
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_scrollbars_requires_enabled_layout_from_cli_or_environment() {
|
||||
for command in ["fetch", "serve"] {
|
||||
for (case, env_value, explicit_layout, expected) in [
|
||||
("missing", None, false, false),
|
||||
("true", Some("true"), false, true),
|
||||
("one", Some("1"), false, true),
|
||||
("false", Some("false"), false, false),
|
||||
("zero", Some("0"), false, false),
|
||||
("cli-overrides", Some("false"), true, true),
|
||||
] {
|
||||
let mut child = Command::new(std::env::current_exe().unwrap());
|
||||
child
|
||||
.args([
|
||||
"--exact",
|
||||
"parse_hide_scrollbars_in_child_process",
|
||||
"--nocapture",
|
||||
])
|
||||
.env_remove("MOLI_LAYOUT")
|
||||
.env("MOLI_TEST_HIDE_SCROLLBARS_COMMAND", command)
|
||||
.env(
|
||||
"MOLI_TEST_HIDE_SCROLLBARS_CLI_LAYOUT",
|
||||
explicit_layout.to_string(),
|
||||
)
|
||||
.env("MOLI_TEST_HIDE_SCROLLBARS_EXPECTED", expected.to_string());
|
||||
if let Some(value) = env_value {
|
||||
child.env("MOLI_LAYOUT", value);
|
||||
}
|
||||
let output = child.output().unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{command}/{case}: {}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_hide_scrollbars_in_child_process() {
|
||||
let Ok(command) = std::env::var("MOLI_TEST_HIDE_SCROLLBARS_COMMAND") else {
|
||||
return;
|
||||
};
|
||||
let explicit_layout = std::env::var("MOLI_TEST_HIDE_SCROLLBARS_CLI_LAYOUT").unwrap() == "true";
|
||||
let expected = std::env::var("MOLI_TEST_HIDE_SCROLLBARS_EXPECTED").unwrap() == "true";
|
||||
let mut args = vec!["moli", command.as_str(), "--hide-scrollbars"];
|
||||
if explicit_layout {
|
||||
args.push("--layout");
|
||||
}
|
||||
if command == "fetch" {
|
||||
args.push("about:blank");
|
||||
}
|
||||
let result = Cli::try_parse_from(args)
|
||||
.map_err(anyhow::Error::from)
|
||||
.and_then(|cli| AppConfig::from_cli(&cli));
|
||||
if expected {
|
||||
let config = result.unwrap();
|
||||
assert!(config.browser.scrollbars_hidden());
|
||||
assert_eq!(
|
||||
config.browser.layout_policy(),
|
||||
moli_core::LayoutPolicy::OnDemand
|
||||
);
|
||||
} else {
|
||||
assert!(result.unwrap_err().to_string().contains("--layout"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screencast_interval_requires_layout_and_accepts_positive_milliseconds() {
|
||||
for interval_ms in ["1", "16", "17", "33", "250", "1000", "1500", "4294967295"] {
|
||||
|
||||
@@ -173,6 +173,53 @@ fn eval_with_layout_publishes_static_geometry() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_hide_scrollbars_removes_gutters_and_preserves_scrolling() -> Result<()> {
|
||||
let url = "data:text/html,<!doctype html><body style='margin:0'><div style='width:100vw;height:2000px'><div id=scroller style='width:200px;height:100px;overflow:scroll;scrollbar-width:auto!important;scrollbar-gutter:stable both-edges'><div style='width:400px;height:300px'></div></div></div>";
|
||||
let script = r#"(() => {
|
||||
const scroller = document.getElementById('scroller');
|
||||
scroller.scrollLeft = 20;
|
||||
scroller.scrollTop = 40;
|
||||
window.scrollTo(0, 100);
|
||||
return {
|
||||
inner: innerWidth,
|
||||
client: document.documentElement.clientWidth,
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
elementClient: [scroller.clientWidth, scroller.clientHeight, scroller.clientLeft],
|
||||
elementScroll: [scroller.scrollWidth, scroller.scrollHeight],
|
||||
offsets: [scroller.scrollLeft, scroller.scrollTop, scrollY],
|
||||
css: [getComputedStyle(scroller).scrollbarWidth, getComputedStyle(scroller).scrollbarGutter]
|
||||
};
|
||||
})()"#;
|
||||
for hidden in [false, true] {
|
||||
let mut args = vec!["--layout"];
|
||||
if hidden {
|
||||
args.push("--hide-scrollbars");
|
||||
}
|
||||
let output = run_eval(url, script, &args)?;
|
||||
assert!(output.status.success(), "{}", clean_output(&output.stderr));
|
||||
let value: serde_json::Value = serde_json::from_slice(&output.stdout)?;
|
||||
let inner = value["inner"].as_u64().expect("viewport width");
|
||||
assert_eq!(value["client"], inner - if hidden { 0 } else { 15 });
|
||||
assert_eq!(value["scrollWidth"], inner);
|
||||
assert_eq!(
|
||||
value["elementClient"],
|
||||
serde_json::json!(if hidden {
|
||||
[170, 100, 15]
|
||||
} else {
|
||||
[170, 85, 15]
|
||||
})
|
||||
);
|
||||
assert_eq!(value["elementScroll"], serde_json::json!([400, 300]));
|
||||
assert_eq!(value["offsets"], serde_json::json!([20, 40, 100]));
|
||||
assert_eq!(
|
||||
value["css"],
|
||||
serde_json::json!(["auto", "stable both-edges"])
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_computed_sizes_follow_layout_option_with_or_without_css() -> Result<()> {
|
||||
let url = "data:text/html,<!doctype html><div id=target style=\"max-width:20px\"></div><canvas id=canvas style=\"max-width:40px\"></canvas>";
|
||||
|
||||
Reference in New Issue
Block a user