mod common;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, Mutex};
use dpi::PhysicalSize;
use embedder_traits::{RefreshDriver, UrlRequest};
use euclid::default::Size2D as UntypedSize2D;
use euclid::{Point2D, Size2D};
use http::{HeaderMap, HeaderName, HeaderValue};
use http_body_util::combinators::BoxBody;
use hyper::body::{Bytes, Incoming};
use hyper::{Request as HyperRequest, Response as HyperResponse};
use image::RgbaImage;
use itertools::Itertools;
use net::test_util::{make_body, make_server, replace_host_table};
use servo::profile_traits::mem::MemoryReportResult;
use servo::{
CreateNewWebViewRequest, Cursor, EmbedderControl, InputEvent, InputMethodType, JSValue,
LoadStatus, MouseButton, MouseLeftViewportEvent, MouseMoveEvent, PrefValue, RenderingContext,
Scroll, SimpleDialog, Theme, WebView, WebViewBuilder, WebViewDelegate, WebViewPoint,
WebViewVector,
};
use servo_base::generic_channel::GenericCallback;
use servo_config::prefs::Preferences;
use servo_url::ServoUrl;
use surfman::{Error, Surface, SurfaceTexture};
use url::Url;
use webrender_api::units::{DeviceIntRect, DeviceIntSize, DevicePoint, DeviceVector2D};
use crate::common::{
ServoTest, WebViewDelegateImpl, click_at_point, evaluate_javascript,
show_webview_and_wait_for_rendering_to_be_ready, wait_for_webview_scene_to_be_up_to_date,
};
fn retained_display_list_bytes(servo_test: &ServoTest) -> usize {
let report: Arc<Mutex<Option<MemoryReportResult>>> = Arc::default();
let report_from_callback = report.clone();
let callback = GenericCallback::new(move |result| {
if let Ok(result) = result {
*report_from_callback.lock().unwrap() = Some(result);
}
})
.expect("Should be able to create a memory report callback");
servo_test.servo().create_memory_report(callback);
let waiting = report.clone();
servo_test.spin(move || waiting.lock().unwrap().is_none());
let result = report
.lock()
.unwrap()
.take()
.expect("Should have a memory report once the spin loop ends");
result
.results
.iter()
.flat_map(|process| process.reports.iter())
.filter(|report| report.path == ["webrender", "display-list"])
.map(|report| report.size)
.sum()
}
#[test]
fn test_create_webview() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.build();
servo_test.spin(move || !delegate.url_changed.get());
let url = webview.url();
assert!(url.is_some());
assert_eq!(url.unwrap().to_string(), "about:blank");
}
struct ConnectionlessRenderingContext(Rc<dyn RenderingContext>);
impl RenderingContext for ConnectionlessRenderingContext {
fn prepare_for_rendering(&self) {
self.0.prepare_for_rendering();
}
fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
self.0.read_to_image(source_rectangle)
}
fn size(&self) -> PhysicalSize<u32> {
self.0.size()
}
fn resize(&self, size: PhysicalSize<u32>) {
self.0.resize(size);
}
fn present(&self) {
self.0.present();
}
fn make_current(&self) -> Result<(), Error> {
self.0.make_current()
}
fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
self.0.gleam_gl_api()
}
fn glow_gl_api(&self) -> Arc<glow::Context> {
self.0.glow_gl_api()
}
fn create_texture(
&self,
surface: Surface,
) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
self.0.create_texture(surface)
}
fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
self.0.destroy_texture(surface_texture)
}
fn refresh_driver(&self) -> Option<Rc<dyn RefreshDriver>> {
self.0.refresh_driver()
}
}
#[test]
fn test_create_webview_without_surfman_connection() {
let servo_test = ServoTest::new();
let rendering_context = Rc::new(ConnectionlessRenderingContext(
servo_test.rendering_context.clone(),
));
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), rendering_context)
.delegate(delegate.clone())
.build();
servo_test.spin(move || !delegate.url_changed.get());
let url = webview.url();
assert!(url.is_some());
assert_eq!(url.unwrap().to_string(), "about:blank");
}
#[test]
fn test_create_webview_http() {
let servo_test = ServoTest::new();
static MESSAGE: &'static [u8] = b"<!DOCTYPE html>\nHello";
let handler =
move |_: HyperRequest<Incoming>,
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
*response.body_mut() = make_body(MESSAGE.to_vec());
};
let (server, url) = make_server(handler);
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(url.as_url().clone())
.build();
servo_test.spin(move || !delegate.url_changed.get());
let _ = server.close();
let url = webview.url();
assert!(url.is_some());
let url = url.unwrap();
assert_eq!(url.scheme(), "http");
let host = url.host_str();
assert!(host.is_some());
assert_eq!(host.unwrap(), "localhost");
}
#[test]
fn test_create_webview_http_custom_host() {
let servo_test = ServoTest::new();
static MESSAGE: &'static [u8] = b"<!DOCTYPE html>\n<title>Hello</title>";
let handler =
move |_: HyperRequest<Incoming>,
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
*response.body_mut() = make_body(MESSAGE.to_vec());
};
let (server, url) = make_server(handler);
let port = url.port().unwrap();
let ip = "127.0.0.1".parse().unwrap();
let mut host_table = HashMap::new();
host_table.insert("www.example.com".to_owned(), ip);
replace_host_table(host_table);
let custom_url = ServoUrl::parse(&format!("http://www.example.com:{}", port)).unwrap();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(custom_url.clone().into_url())
.build();
servo_test.spin(move || !delegate.load_status_changed.get() || !delegate.url_changed.get());
let _ = server.close();
let page_title = webview.page_title();
assert!(page_title.is_some());
assert_eq!(page_title.unwrap(), "Hello");
let url = webview.url();
assert!(url.is_some());
assert_eq!(url.unwrap(), custom_url.into_url());
}
#[test]
fn test_create_webview_and_immediately_drop_webview_before_shutdown() {
let servo_test = ServoTest::new();
WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone()).build();
}
#[test]
#[cfg_attr(
target_os = "windows",
ignore = "the scene is only observable through a memory report, which aborts \
the process on Windows (#37238)"
)]
fn test_closing_a_webview_removes_its_pipelines_from_the_scene() {
let servo_test = ServoTest::new();
let page_url = Url::parse(
"data:text/html,<!DOCTYPE html>\
<style>div{width:100px;height:20px;background:red;margin:2px}</style>\
<div></div><div></div><div></div><div></div><div></div>",
)
.unwrap();
let kept_delegate = Rc::new(WebViewDelegateImpl::default());
let kept = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(kept_delegate.clone())
.url(page_url.clone())
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &kept, &kept_delegate);
wait_for_webview_scene_to_be_up_to_date(&servo_test, &kept);
let baseline = retained_display_list_bytes(&servo_test);
let closed_delegate = Rc::new(WebViewDelegateImpl::default());
let closed = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(closed_delegate.clone())
.url(page_url)
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &closed, &closed_delegate);
wait_for_webview_scene_to_be_up_to_date(&servo_test, &closed);
let with_both = retained_display_list_bytes(&servo_test);
assert!(
with_both > baseline,
"A second loaded WebView should add retained display lists \
({baseline} bytes to {with_both} bytes)"
);
drop(closed);
wait_for_webview_scene_to_be_up_to_date(&servo_test, &kept);
let after = retained_display_list_bytes(&servo_test);
assert!(
after.abs_diff(baseline) < with_both.abs_diff(baseline) / 2,
"Closing a WebView should return the scene to its single-WebView size \
(baseline {baseline}, with both {with_both}, after closing {after} bytes)"
);
}
#[test]
fn test_theme_change() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(Url::parse("data:text/html,page one").unwrap())
.build();
let is_dark_theme_script = "window.matchMedia('(prefers-color-scheme: dark)').matches";
let result = evaluate_javascript(&servo_test, webview.clone(), is_dark_theme_script);
assert_eq!(result, Ok(JSValue::Boolean(false)));
webview.notify_theme_change(Theme::Dark);
let result = evaluate_javascript(&servo_test, webview.clone(), is_dark_theme_script);
assert_eq!(result, Ok(JSValue::Boolean(true)));
delegate.reset();
webview.load(Url::parse("data:text/html,page two").unwrap());
servo_test.spin(move || !delegate.url_changed.get());
let result = evaluate_javascript(&servo_test, webview.clone(), is_dark_theme_script);
assert_eq!(result, Ok(JSValue::Boolean(true)));
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(Url::parse("data:text/html,page one").unwrap())
.build();
webview.notify_theme_change(Theme::Dark);
let result = evaluate_javascript(&servo_test, webview.clone(), is_dark_theme_script);
assert_eq!(result, Ok(JSValue::Boolean(true)));
}
#[test]
fn test_theme_change_media_query_event() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(
Url::parse(
r#"data:text/html,<!DOCTYPE html><html><script>
let mediaQueryChanges = 0;
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
mediaQueryList.addEventListener('change', () => {
mediaQueryChanges += 1;
console.log(mediaQueryChanges);
});
</script></html>"#,
)
.unwrap(),
)
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
let result = evaluate_javascript(&servo_test, webview.clone(), "mediaQueryChanges");
assert_eq!(result, Ok(JSValue::Number(0.0)));
webview.notify_theme_change(Theme::Dark);
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
let result = evaluate_javascript(&servo_test, webview.clone(), "mediaQueryChanges");
assert_eq!(result, Ok(JSValue::Number(1.0)));
webview.notify_theme_change(Theme::Light);
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
let result = evaluate_javascript(&servo_test, webview.clone(), "mediaQueryChanges");
assert_eq!(result, Ok(JSValue::Number(2.0)));
}
#[test]
fn test_cursor_change() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(
Url::parse(
"data:text/html,<!DOCTYPE html><style> html { cursor: crosshair; margin: 0}</style><body>hello</body>",
)
.unwrap(),
)
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(
DevicePoint::new(10., 10.).into(),
)));
let captured_delegate = delegate.clone();
servo_test.spin(move || !captured_delegate.cursor_changed.get());
assert_eq!(webview.cursor(), Cursor::Crosshair);
delegate.reset();
webview.notify_input_event(InputEvent::MouseLeftViewport(
MouseLeftViewportEvent::default(),
));
let captured_delegate = delegate.clone();
servo_test.spin(move || !captured_delegate.cursor_changed.get());
assert_eq!(webview.cursor(), Cursor::Default);
}
#[test]
fn test_cursor_unchanged_input_color() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(
Url::parse(
"data:text/html,<!DOCTYPE html><body><input type=\"color\"><p>Test text for Cursor change</p></body>",
)
.unwrap(),
)
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(
DevicePoint::new(20., 65.).into(),
)));
let captured_delegate = delegate.clone();
servo_test.spin(move || !captured_delegate.cursor_changed.get());
assert_eq!(webview.cursor(), Cursor::Text);
delegate.reset();
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(
DevicePoint::new(20., 25.).into(),
)));
let captured_delegate = delegate.clone();
servo_test.spin(move || !captured_delegate.cursor_changed.get());
assert_eq!(webview.cursor(), Cursor::Default);
}
#[test]
fn test_negative_resize_to_request() {
let servo_test = ServoTest::new();
struct WebViewResizeTestDelegate {
rendering_context: Rc<dyn RenderingContext>,
popup: RefCell<Option<WebView>>,
resize_request: Cell<Option<DeviceIntSize>>,
}
impl WebViewDelegate for WebViewResizeTestDelegate {
fn request_create_new(&self, parent_webview: WebView, request: CreateNewWebViewRequest) {
let webview = request
.builder(self.rendering_context.clone())
.delegate(parent_webview.delegate())
.build();
self.popup.borrow_mut().replace(webview.clone());
}
fn request_resize_to(&self, _: WebView, requested_outer_size: DeviceIntSize) {
self.resize_request.set(Some(requested_outer_size));
}
}
let delegate = Rc::new(WebViewResizeTestDelegate {
rendering_context: servo_test.rendering_context.clone(),
popup: None.into(),
resize_request: None.into(),
});
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(
Url::parse(
"data:text/html,<!DOCTYPE html><script>\
let popup = window.open('about:blank');\
popup.resizeTo(-100, -100);\
</script></body>",
)
.unwrap(),
)
.build();
let load_webview = webview.clone();
let _ = servo_test.spin(move || load_webview.load_status() != LoadStatus::Complete);
let popup = delegate
.popup
.borrow()
.clone()
.expect("Should have created popup");
let load_webview = popup.clone();
let _ = servo_test.spin(move || load_webview.load_status() != LoadStatus::Complete);
assert_eq!(
delegate.resize_request.get(),
Some(DeviceIntSize::new(1, 1))
);
*delegate.popup.borrow_mut() = None;
}
#[test]
fn test_resize_webview_zero() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(Url::parse("data:text/html,<!DOCTYPE html><body>hello</body>").unwrap())
.build();
webview.resize(PhysicalSize::new(0, 0));
let load_webview = webview.clone();
let _ = servo_test.spin(move || load_webview.load_status() != LoadStatus::Complete);
webview.resize(PhysicalSize::new(500, 500));
}
#[test]
fn test_webview_resize_interactivity() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(
Url::parse(
"data:text/html,<!DOCTYPE html>\
<style>\
html { margin: 0; }\
div { margin-top: 500px; width: 100px; height: 100px; cursor: crosshair; }\
</style>\
<body><div></div></body>",
)
.unwrap(),
)
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
assert_eq!(webview.size(), Size2D::new(500., 500.));
webview.resize(PhysicalSize::new(600, 600));
wait_for_webview_scene_to_be_up_to_date(&servo_test, &webview);
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(
DevicePoint::new(20., 520.).into(),
)));
let captured_delegate = delegate.clone();
servo_test.spin(move || !captured_delegate.cursor_changed.get());
assert_eq!(webview.cursor(), Cursor::Crosshair);
}
#[test]
fn test_control_show_and_hide() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(
Url::parse(
"data:text/html,\
<!DOCTYPE html>\
<select id=select style=\"width: 500px; height: 500px\">\
<option>one</option>\
</select>
<script>\
select.addEventListener('click', () => {\
setTimeout(() => select.parentNode.removeChild(select), 100);\
});\
</script>",
)
.unwrap(),
)
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
click_at_point(&webview, Point2D::new(50., 50.), MouseButton::Primary);
let captured_delegate = delegate.clone();
servo_test.spin(move || captured_delegate.number_of_controls_shown.get() != 1);
let captured_delegate = delegate.clone();
servo_test.spin(move || captured_delegate.number_of_controls_hidden.get() != 1);
}
#[test]
fn test_page_zoom() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.build();
assert_eq!(webview.page_zoom(), 1.0);
webview.set_page_zoom(1.5);
assert_eq!(webview.page_zoom(), 1.5);
webview.set_page_zoom(0.5);
assert_eq!(webview.page_zoom(), 0.5);
webview.set_page_zoom(-1.0);
assert_eq!(webview.page_zoom(), 0.1);
webview.set_page_zoom(100.0);
assert_eq!(webview.page_zoom(), 10.0);
}
#[test]
fn test_viewport_meta_tag_initial_scale() {
let servo_test = ServoTest::new_with_builder(|builder| {
let mut preferences = Preferences::default();
preferences.viewport_meta_enabled = true;
preferences.dom_visual_viewport_enabled = true;
builder.preferences(preferences)
});
let initial_scale: f64 = 2.0;
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(
Url::parse(
format!(
"data:text/html,\
<!DOCTYPE html>\
<meta name=viewport content=\"initial-scale={}\">",
initial_scale
)
.as_str(),
)
.unwrap(),
)
.build();
assert_eq!(webview.hidpi_scale_factor().get(), 1.0);
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
assert_eq!(webview.hidpi_scale_factor().get(), 1.0);
assert_eq!(
Ok(JSValue::Number(webview.hidpi_scale_factor().get() as f64)),
evaluate_javascript(&servo_test, webview.clone(), "window.devicePixelRatio;")
);
assert_eq!(
Ok(JSValue::Number(initial_scale)),
evaluate_javascript(&servo_test, webview.clone(), "window.visualViewport.scale;")
);
}
#[test]
fn test_show_and_hide_ime() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(
Url::parse(
"data:text/html,<!DOCTYPE html> \
<input type=\"text\" value=\"servo\" style=\"width: 200px; height: 200px;\">",
)
.unwrap(),
)
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
click_at_point(&webview, Point2D::new(150., 150.), MouseButton::Primary);
let captured_delegate = delegate.clone();
servo_test.spin(move || captured_delegate.number_of_controls_shown.get() != 1);
{
let controls = delegate.controls_shown.borrow();
assert_eq!(controls.len(), 1);
let EmbedderControl::InputMethod(ime) = &controls[0] else {
unreachable!("Expected embedder control to be an IME");
};
assert_eq!(ime.input_method_type(), InputMethodType::Text);
assert_eq!(ime.text(), "servo");
assert_eq!(ime.insertion_point(), Some(5));
}
click_at_point(&webview, Point2D::new(300., 300.), MouseButton::Primary);
let captured_delegate = delegate.clone();
servo_test.spin(move || captured_delegate.number_of_controls_hidden.get() != 1);
}
#[test]
fn test_alert_dialog() {
test_simple_dialog("window.alert('Alert');", |dialog| {
let SimpleDialog::Alert(..) = dialog else {
unreachable!("Expected dialog to be a SimpleDialog::Alert");
};
assert_eq!(dialog.message(), "Alert");
});
}
#[test]
fn test_prompt_dialog() {
test_simple_dialog("window.prompt('Prompt');", |dialog| {
let SimpleDialog::Prompt(..) = dialog else {
unreachable!("Expected dialog to be a SimpleDialog::Prompt");
};
assert_eq!(dialog.message(), "Prompt");
});
}
#[test]
fn test_confirm_dialog() {
test_simple_dialog("window.confirm('Confirm');", |dialog| {
let SimpleDialog::Confirm(..) = dialog else {
unreachable!("Expected dialog to be a SimpleDialog::Confirm");
};
assert_eq!(dialog.message(), "Confirm");
});
}
fn test_simple_dialog(prompt: &str, validate: impl Fn(&SimpleDialog)) {
let make_test_html = |prompt: &str| {
let html = format!(
"data:text/html,<!DOCTYPE html>\
<input type=\"button\" value=\"click\" style=\"width: 200px; height: 200px;\"\
onclick=\"{}\">",
prompt
);
Url::parse(&html).unwrap()
};
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(make_test_html(prompt))
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
assert!(delegate.active_dialog.borrow().is_none());
click_at_point(&webview, Point2D::new(100., 100.), MouseButton::Primary);
let captured_delegate = delegate.clone();
servo_test.spin(move || captured_delegate.active_dialog.borrow().is_none());
let active_dialog = delegate.active_dialog.borrow();
validate(
active_dialog
.as_ref()
.expect("the spin call above ensures this is not None"),
);
}
#[test]
fn test_can_go_forward_and_can_go_back() {
let servo_test = ServoTest::new();
let page_1_url = Url::parse("data:text/html,<!DOCTYPE html> page 1").unwrap();
let page_2_url = Url::parse("data:text/html,<!DOCTYPE html> page 2").unwrap();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(page_1_url.clone())
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
assert!(!webview.can_go_forward());
assert!(!webview.can_go_back());
let load_webview = webview.clone();
webview.load(page_2_url.clone());
servo_test.spin(move || load_webview.url() != Some(page_2_url.clone()));
assert!(!webview.can_go_forward());
assert!(webview.can_go_back());
webview.go_back(1);
let load_webview = webview.clone();
servo_test.spin(move || load_webview.url() != Some(page_1_url.clone()));
assert!(webview.can_go_forward());
assert!(!webview.can_go_back());
}
#[test]
fn test_pinch_zoom_update_dom_visual_viewport() {
let servo_test = ServoTest::new_with_builder(|builder| {
let mut preferences = Preferences::default();
preferences.dom_visual_viewport_enabled = true;
builder.preferences(preferences)
});
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(Url::parse("data:text/html,<!DOCTYPE html><body>Hello world!</body>").unwrap())
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
let eval_visual_viewport = |attribute: &str| match evaluate_javascript(
&servo_test,
webview.clone(),
format!("window.visualViewport.{}", attribute),
) {
Ok(JSValue::Number(number)) => Some(number),
_ => None,
};
assert_eq!(eval_visual_viewport("scale"), Some(1.));
assert_eq!(eval_visual_viewport("width"), Some(500.));
assert_eq!(eval_visual_viewport("height"), Some(500.));
assert_eq!(eval_visual_viewport("offsetLeft"), Some(0.));
assert_eq!(eval_visual_viewport("offsetTop"), Some(0.));
webview.adjust_pinch_zoom(5., DevicePoint::new(100., 100.));
wait_for_webview_scene_to_be_up_to_date(&servo_test, &webview);
assert_eq!(eval_visual_viewport("scale"), Some(5.));
assert_eq!(eval_visual_viewport("width"), Some(100.));
assert_eq!(eval_visual_viewport("height"), Some(100.));
assert_eq!(eval_visual_viewport("offsetLeft"), Some(80.));
assert_eq!(eval_visual_viewport("offsetTop"), Some(80.));
webview.notify_scroll_event(
Scroll::Delta(WebViewVector::Device(DeviceVector2D::new(100., 100.))),
WebViewPoint::Device(DevicePoint::zero()),
);
wait_for_webview_scene_to_be_up_to_date(&servo_test, &webview);
assert_eq!(eval_visual_viewport("scale"), Some(5.));
assert_eq!(eval_visual_viewport("width"), Some(100.));
assert_eq!(eval_visual_viewport("height"), Some(100.));
assert_eq!(eval_visual_viewport("offsetLeft"), Some(100.));
assert_eq!(eval_visual_viewport("offsetTop"), Some(100.));
}
#[test]
fn test_webview_load_with_headers() {
let servo_test = ServoTest::new();
let request_count = Arc::new(AtomicUsize::new(0));
let request_count_clone = request_count.clone();
let mut headers = HeaderMap::new();
headers.append(HeaderName::from_static("world"), "hello".parse().unwrap());
headers.append(HeaderName::from_static("second"), "header".parse().unwrap());
headers.append(
HeaderName::from_static("accept-encoding"),
"customzip".parse().unwrap(),
);
static MESSAGE: &'static [u8] = b"<!DOCTYPE html>\nHello";
let handler =
move |req: HyperRequest<Incoming>,
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
if request_count_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst) == 2 {
let headers = req.headers();
assert_eq!(
headers.get("world"),
Some(&HeaderValue::from_static("hello"))
);
assert_eq!(
headers.get("second"),
Some(&HeaderValue::from_static("header"))
);
assert!(
headers
.get_all("accept-encoding")
.iter()
.contains(&HeaderValue::from_static("customzip"))
);
assert!(headers.get_all("accept-encoding").iter().count() > 1);
} else {
*response.body_mut() = make_body(MESSAGE.to_vec());
}
};
let (server, url) = make_server(handler);
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(url.url().into_url())
.build();
{
let delegate = delegate.clone();
servo_test.spin(move || !delegate.url_changed.get());
}
let urlrequest = UrlRequest::new(url.url().into_url()).headers(headers);
webview.load_request(urlrequest);
{
let request_count = request_count.clone();
servo_test.spin(move || request_count.load(std::sync::atomic::Ordering::SeqCst) < 2);
}
assert_eq!(request_count.load(std::sync::atomic::Ordering::SeqCst), 2);
server.close();
}
#[test]
fn test_console_log_and_error_ordering() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let _webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(
Url::parse(
"data:text/html,<!doctype html><script>\
console.log('Hello');\
DocumentFragment.prototype.querySelector.call(document, 'body');\
</script>",
)
.unwrap(),
)
.build();
let captured = delegate.clone();
servo_test.spin(move || captured.console_messages.borrow().len() < 2);
let messages = delegate.console_messages.borrow();
assert_eq!(
messages.len(),
2,
"Expected exactly 2 console messages, got: {messages:?}"
);
assert!(
messages[0].1.contains("Hello"),
"Expected first message to contain 'Hello', got: {:?}",
messages[0].1
);
assert!(
messages[1].1.contains("DocumentFragment") || messages[1].1.contains("querySelector"),
"Expected second message to contain error info, got: {:?}",
messages[1].1
);
}
#[test]
fn test_preferences_change() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let test_page =
Url::parse("data:text/html,<div id=\"target\" style=\"backdrop-filter: sepia(1)\"></div>")
.expect("Data URL failed to build");
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(test_page.clone())
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
assert_eq!(
Ok(JSValue::Array(vec![
JSValue::String(String::new()),
JSValue::String(String::new())
])),
evaluate_javascript(
&servo_test,
webview.clone(),
"let old_value = target.style.getPropertyValue('backdrop-filter');
target.style.backdropFilter = 'sepia(1)';
let new_value = target.style.getPropertyValue('backdrop-filter');
[old_value, new_value]"
)
);
servo_test
.servo()
.set_preference("layout_unimplemented", PrefValue::Bool(true));
webview.reload();
assert_eq!(
Ok(JSValue::Array(vec![
JSValue::String("sepia(1)".to_string()),
JSValue::String("opacity(0.5)".to_string())
])),
evaluate_javascript(
&servo_test,
webview,
"let old_value = target.style.getPropertyValue('backdrop-filter');
target.style.backdropFilter = 'opacity(0.5)';
let new_value = target.style.getPropertyValue('backdrop-filter');
[old_value, new_value]"
)
);
}
#[test]
fn test_fullscreen() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let test_page = Url::parse(
"data:text/html,\
<button onclick='this.requestFullscreen()'>click</button>\
<a href=about:blank>click</a>",
)
.expect("Data URL failed to build");
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(test_page.clone())
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
click_at_point(&webview, Point2D::new(10., 10.), MouseButton::Primary);
let captured = delegate.clone();
servo_test.spin(move || !captured.fullscreen.get());
assert!(
evaluate_javascript(
&servo_test,
webview.clone(),
"document.querySelector('a').click();"
)
.is_ok()
);
let captured = delegate.clone();
servo_test.spin(move || captured.fullscreen.get());
}
#[test]
fn test_webview_title_updates_when_document_title_is_updated() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let test_page = Url::parse(
"data:text/html,\
<script>document.title='Success';</script>",
)
.expect("Data URL failed to build");
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(test_page.clone())
.build();
let load_webview = webview.clone();
servo_test.spin(move || load_webview.load_status() != LoadStatus::Complete);
assert_eq!(webview.page_title().as_deref(), Some("Success"));
}
#[test]
fn test_webview_title_updates_when_title_element_is_created_from_javascript() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let test_page = Url::parse(
"data:text/html,\
<body>\
<script>\
let title = document.createElement('title');\
title.textContent = 'Success';\
document.body.appendChild(title);\
</script>",
)
.expect("Data URL failed to build");
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(test_page.clone())
.build();
let load_webview = webview.clone();
servo_test.spin(move || load_webview.load_status() != LoadStatus::Complete);
assert_eq!(webview.page_title().as_deref(), Some("Success"));
}
#[test]
fn test_webview_clear_history() {
let session_history_changed = Rc::new(Cell::new(false));
let entries = Rc::new(RefCell::new(vec![]));
struct MyDelegate {
session_history_changed: Rc<Cell<bool>>,
entries: Rc<RefCell<Vec<Url>>>,
}
impl WebViewDelegate for MyDelegate {
fn notify_history_changed(&self, _webview: WebView, entries: Vec<Url>, _current: usize) {
self.session_history_changed.set(true);
*self.entries.borrow_mut() = entries;
}
}
let servo_test = ServoTest::new();
let delegate = Rc::new(MyDelegate {
session_history_changed: session_history_changed.clone(),
entries: entries.clone(),
});
let url_1 = Url::parse("data:text/html,<body><title>Success</title></body>").unwrap();
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(url_1)
.build();
{
let webview = webview.clone();
servo_test.spin(move || webview.page_title() != Some("Success".into()));
}
let second_url =
Url::parse("data:text/html,<script>document.title='Success2';</script>").unwrap();
webview.load(second_url.clone());
{
let webview = webview.clone();
servo_test.spin(move || webview.page_title() != Some("Success2".into()));
}
webview.load(Url::parse("data:text/html,<script>document.title='Success3';</script>").unwrap());
{
let webview = webview.clone();
servo_test.spin(move || webview.page_title() != Some("Success3".into()));
}
assert!(webview.can_go_back());
webview.go_back(1);
{
let webview = webview.clone();
servo_test.spin(move || webview.page_title() != Some("Success2".into()));
}
session_history_changed.set(false);
entries.borrow_mut().clear();
webview.clear_session_history();
servo_test.spin(move || !session_history_changed.get());
assert!(!webview.can_go_back());
assert!(!webview.can_go_forward());
assert_eq!(&*entries.borrow(), &vec![second_url]);
}
#[test]
fn test_hide_animating_webview() {
let servo_test = ServoTest::new();
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo(), servo_test.rendering_context.clone())
.delegate(delegate.clone())
.url(Url::parse("data:text/html,<script>(function frame() { requestAnimationFrame(frame) })();</script>").unwrap())
.build();
show_webview_and_wait_for_rendering_to_be_ready(&servo_test, &webview, &delegate);
servo_test.spin(|| !webview.animating());
webview.hide();
servo_test.spin(|| webview.animating());
}