This commit is contained in:
2026-05-21 15:44:26 -04:00
parent 7fc2967793
commit aa8182bec4
11 changed files with 282 additions and 97 deletions
+47 -11
View File
@@ -20,6 +20,7 @@ use gpui_component_assets::Assets;
use shell::ElyShell;
use shell::chrome::{TRAFFIC_LIGHT_ORIGIN_X, TRAFFIC_LIGHT_ORIGIN_Y};
use shortcuts::bind_shortcuts;
use url::Url;
use crate::brand::{DEEP_LINK_PREFIX, PRODUCT_NAME};
@@ -55,7 +56,7 @@ actions!(
fn main() {
init_tracing();
let pending_deep_links = PendingDeepLinks::default();
pending_deep_links.push(startup_deep_links(env::args().skip(1)));
pending_deep_links.push(startup_open_urls(env::args().skip(1)));
let open_url_queue = pending_deep_links.clone();
let application = Application::new().with_assets(Assets);
@@ -248,7 +249,7 @@ fn open_deep_links(
current_window: &Rc<RefCell<Option<BrowserWindowTarget>>>,
cx: &mut App,
) {
let urls = urls.into_iter().filter_map(|url| parse_ely_deep_link(&url)).collect::<Vec<_>>();
let urls = urls.into_iter().filter_map(|url| parse_open_url(&url)).collect::<Vec<_>>();
if urls.is_empty() {
return;
@@ -314,9 +315,19 @@ fn parse_ely_deep_link(value: &str) -> Option<UrlText> {
.flatten()
}
fn startup_deep_links(args: impl IntoIterator<Item = String>) -> Vec<String> {
fn parse_open_url(value: &str) -> Option<UrlText> {
parse_ely_deep_link(value).or_else(|| parse_external_open_url(value))
}
fn parse_external_open_url(value: &str) -> Option<UrlText> {
let parsed = UrlText::from_address_text(value).ok()?;
let url = Url::parse(parsed.as_str()).ok()?;
matches!(url.scheme(), "http" | "https").then(|| UrlText::parse(url.to_string()).ok()).flatten()
}
fn startup_open_urls(args: impl IntoIterator<Item = String>) -> Vec<String> {
args.into_iter()
.filter_map(|arg| parse_ely_deep_link(&arg).map(|url| url.as_str().to_string()))
.filter_map(|arg| parse_open_url(&arg).map(|url| url.as_str().to_string()))
.collect()
}
@@ -346,7 +357,7 @@ fn open_private_window(_: &OpenPrivateWindow, cx: &mut App) {
#[cfg(test)]
mod tests {
use super::{PendingDeepLinks, parse_ely_deep_link, startup_deep_links};
use super::{PendingDeepLinks, parse_ely_deep_link, parse_open_url, startup_open_urls};
#[test]
fn pending_deep_links_drains_urls_in_order() {
@@ -381,16 +392,41 @@ mod tests {
}
#[test]
fn startup_deep_links_filter_and_normalize_args() {
let links = startup_deep_links(
["--ignored", " ELY://history ", "https://example.com", "ely://auth/callback?code=abc"]
.into_iter()
.map(str::to_string),
fn parse_open_url_accepts_external_pages() {
let url = parse_open_url(" HTTPS://example.com ");
assert_eq!(url.as_ref().map(|url| url.as_str()), Some("https://example.com/"));
}
#[test]
fn parse_open_url_accepts_address_text_domains() {
let url = parse_open_url("servo.org");
assert_eq!(url.as_ref().map(|url| url.as_str()), Some("https://servo.org/"));
}
#[test]
fn startup_open_urls_filter_and_normalize_args() {
let links = startup_open_urls(
[
"--ignored",
" ELY://history ",
"https://example.com",
"servo.org",
"ely://auth/callback?code=abc",
]
.into_iter()
.map(str::to_string),
);
assert_eq!(
links,
vec!["ely://history".to_string(), "ely://auth/callback?code=abc".to_string()]
vec![
"ely://history".to_string(),
"https://example.com/".to_string(),
"https://servo.org/".to_string(),
"ely://auth/callback?code=abc".to_string(),
]
);
}
}
+51 -43
View File
@@ -6,7 +6,7 @@ use ely_domain::{
use ely_servo_host::{
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseHoverRequest,
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, ResizeRequest,
ScrollRequest, ServoHost, ServoSurfaceSize, SoftwareServoHost,
ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost,
};
#[path = "servo_live_types.rs"]
@@ -54,24 +54,18 @@ impl ServoLiveClient {
self.apply_navigation(&request, &webview_id, tab_id, requested_url)?;
self.apply_input(&request, &webview_id)?;
// Match Servo's `examples/winit_minimal.rs`: spin the event loop on
// the embedder-side hot path, never paint. Painting is reactive in
// Servo — `notify_new_frame_ready` on the delegate flags the
// session, and the next `poll` (gated on `has_pending_frame`)
// performs the single paint + present for that frame. Forcing a
// paint here would clear the surface to the WebRender background
// and present it *before* Servo has composited the navigated
// page, which is what produced the per-redirect white-flash on
// sites that perform a chain of redirects (google.com → /
// → /?zx=…). The `ServoLiveFrame` we return only carries the
// snapshot metadata; the on-screen surface is owned by Servo via
// the native NSView and updated through `poll`.
// the embedder-side hot path, never paint. Servo's public
// rendering contract says `notify_new_frame_ready` is the signal
// for `WebView::paint`; URL/title/load-status callbacks are
// metadata updates and travel through snapshots. Painting here
// would present before Servo has composited the navigated page,
// which produced per-redirect white flashes.
self.host.tick();
if !self.session_uses_native_surface(&request.tab_id) {
return Err(ServoLiveError::NativeSurfaceUnavailable);
if self.session_uses_native_surface(&request.tab_id) {
return self.presented_frame_from_session(&request.tab_id, &webview_id).map(Some);
}
let frame = self.frame_from_session(&request.tab_id, &webview_id)?;
Ok(Some(frame))
Ok(None)
}
pub fn poll(&mut self, tab_id: String) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
@@ -82,15 +76,22 @@ impl ServoLiveClient {
let uses_native_surface = session.native_surface_id.is_some();
self.host.tick();
if !self.host.snapshot(&webview_id)?.has_pending_frame() {
let snapshot = self.host.snapshot(&webview_id)?;
if !snapshot.has_pending_frame() && !snapshot.has_pending_metadata() {
return Ok(None);
}
if !uses_native_surface {
return Err(ServoLiveError::NativeSurfaceUnavailable);
if uses_native_surface {
if snapshot.has_pending_frame() {
self.host.paint_without_readback_with_completion(&webview_id, false)?;
}
return self.presented_frame_from_session(&tab_id, &webview_id).map(Some);
}
self.host.paint_without_readback_with_completion(&webview_id, false)?;
self.frame_from_session(&tab_id, &webview_id).map(Some)
if snapshot.has_pending_frame() {
self.host.paint(&webview_id)?;
}
self.rendered_frame_from_session(&tab_id, &webview_id)
}
pub fn close(&mut self, tab_id: String) -> Result<(), ServoLiveError> {
@@ -252,8 +253,7 @@ impl ServoLiveClient {
// `set_history`) is the source of truth — if it already
// matches the requested URL, this URL change came *from*
// Servo and only needs an embedder-side bookkeeping sync.
let servo_current_url =
self.host.snapshot(webview_id)?.url().map(str::to_string);
let servo_current_url = self.host.snapshot(webview_id)?.url().map(str::to_string);
if servo_current_url.as_deref() == Some(requested_url.as_str()) {
if let Some(session) = self.sessions.get_mut(&request.tab_id) {
session.requested_url = Some(requested_url.as_str().to_string());
@@ -314,7 +314,7 @@ impl ServoLiveClient {
Ok(())
}
fn frame_from_session(
fn presented_frame_from_session(
&self,
tab_id: &str,
webview_id: &WebViewId,
@@ -325,7 +325,7 @@ impl ServoLiveClient {
}));
};
if session.native_surface_id.is_some() {
let snapshot = self.host.snapshot(webview_id)?;
let snapshot = self.host.snapshot_and_mark_metadata_observed(webview_id)?;
return Ok(ServoLiveFrame::from_presented(
snapshot,
session.width,
@@ -336,6 +336,29 @@ impl ServoLiveClient {
Err(ServoLiveError::NativeSurfaceUnavailable)
}
fn rendered_frame_from_session(
&self,
tab_id: &str,
webview_id: &WebViewId,
) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
let Some(session) = self.sessions.get(tab_id) else {
return Err(ServoLiveError::Host(ServoHostError::WebViewNotFound {
id: webview_id.clone(),
}));
};
let rendered_frame = match self.host.last_rendered_frame() {
Ok(frame) => frame,
Err(ServoHostError::RenderedFrameUnavailable) => return Ok(None),
Err(error) => return Err(ServoLiveError::Host(error)),
};
let snapshot = self.host.snapshot_and_mark_metadata_observed(webview_id)?;
Ok(Some(ServoLiveFrame::from_rendered(
snapshot,
rendered_frame,
session.device_pixel_ratio,
)))
}
fn session_uses_native_surface(&self, tab_id: &str) -> bool {
self.sessions.get(tab_id).is_some_and(|session| session.native_surface_id.is_some())
}
@@ -425,12 +448,7 @@ mod tests {
// physical dimensions, so only the hidpi push should fire.
let change = ViewportChange::between(
&request(2880, 1800, 2.0, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
&session(
2880,
1800,
SERVO_DEFAULT_DEVICE_PIXEL_RATIO,
SERVO_DEFAULT_PAGE_ZOOM_PERCENT,
),
&session(2880, 1800, SERVO_DEFAULT_DEVICE_PIXEL_RATIO, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
);
assert_eq!(
change,
@@ -443,18 +461,8 @@ mod tests {
// On a 1.0-DPR display the fresh session already matches the
// request — Servo's defaults are exactly what we asked for.
let change = ViewportChange::between(
&request(
1280,
720,
SERVO_DEFAULT_DEVICE_PIXEL_RATIO,
SERVO_DEFAULT_PAGE_ZOOM_PERCENT,
),
&session(
1280,
720,
SERVO_DEFAULT_DEVICE_PIXEL_RATIO,
SERVO_DEFAULT_PAGE_ZOOM_PERCENT,
),
&request(1280, 720, SERVO_DEFAULT_DEVICE_PIXEL_RATIO, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
&session(1280, 720, SERVO_DEFAULT_DEVICE_PIXEL_RATIO, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
);
assert_eq!(change, ViewportChange::default());
}
@@ -1,5 +1,5 @@
use ely_domain::SitePermissionDecision;
use ely_servo_host::{ServoHostError, WebViewSnapshot, WebViewState};
use ely_servo_host::{RenderedFrame, ServoHostError, WebViewSnapshot, WebViewState};
use gpui::NativeSurfaceHandle;
use serde::Serialize;
use thiserror::Error;
@@ -90,6 +90,34 @@ impl ServoLiveFrame {
}
}
pub(super) fn from_rendered(
snapshot: WebViewSnapshot,
rendered_frame: RenderedFrame,
device_pixel_ratio: f32,
) -> Self {
let width = rendered_frame.width();
let height = rendered_frame.height();
let (css_viewport_width, css_viewport_height) =
css_viewport_size(width, height, device_pixel_ratio);
Self {
loaded_url: snapshot.url().map(str::to_string),
title: snapshot.title().map(str::to_string),
render_state: render_state_label(snapshot.state()).to_string(),
width,
height,
device_pixel_ratio,
css_viewport_width,
css_viewport_height,
#[cfg(all(test, feature = "live-site-smoke"))]
non_white_pixel_count: rendered_frame.non_white_pixel_count(),
#[cfg(all(test, feature = "live-site-smoke"))]
content_pixel_count: rendered_frame.content_pixel_count(),
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: rendered_frame.sample_hash(),
rgba_bytes: Some(rendered_frame.rgba_bytes().to_vec()),
}
}
#[must_use]
pub fn loaded_url(&self) -> Option<&str> {
self.loaded_url.as_deref()
@@ -155,6 +183,9 @@ impl ServoLiveFrame {
#[cfg(test)]
pub(crate) fn for_test(width: u32, height: u32, rgba_bytes: Vec<u8>) -> Self {
#[cfg(all(test, feature = "live-site-smoke"))]
let summary =
ely_servo_host::RenderedFrameSummary::from_rgba_bytes(width, height, &rgba_bytes);
Self {
loaded_url: Some("https://example.com/".to_string()),
title: Some("Example".to_string()),
@@ -165,11 +196,11 @@ impl ServoLiveFrame {
css_viewport_width: width,
css_viewport_height: height,
#[cfg(all(test, feature = "live-site-smoke"))]
non_white_pixel_count: 0,
non_white_pixel_count: summary.non_white_pixel_count(),
#[cfg(all(test, feature = "live-site-smoke"))]
content_pixel_count: 0,
content_pixel_count: summary.content_pixel_count(),
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: 0,
sample_hash: summary.sample_hash(),
rgba_bytes: Some(rgba_bytes),
}
}
+5 -7
View File
@@ -79,10 +79,8 @@ impl WebSurfaceStore {
// which destroys and reallocates the framebuffer — the source of
// the per-frame blank flash. The first ensure (when no prior
// `last_ensure_key` is set) always fires so the page can load.
let already_ensured = self
.surfaces
.get(tab.id())
.is_some_and(|surface| surface.last_ensure_key.is_some());
let already_ensured =
self.surfaces.get(tab.id()).is_some_and(|surface| surface.last_ensure_key.is_some());
if already_ensured
&& self
.surfaces
@@ -211,9 +209,9 @@ impl WebSurfaceStore {
// one frame of the gesture settling. Without this boost the
// idle 80 ms polling adds a noticeable lag between letting go
// of a resize and the page re-laying-out at the final size.
let any_settling = visible_tab_ids
.iter()
.any(|tab_id| self.surfaces.get(tab_id).is_some_and(|s| s.viewport_size_is_settling(now)));
let any_settling = visible_tab_ids.iter().any(|tab_id| {
self.surfaces.get(tab_id).is_some_and(|s| s.viewport_size_is_settling(now))
});
if any_settling {
return ACTIVE_POLL_INTERVAL;
}
@@ -201,7 +201,7 @@ fn assert_web_surface_resizes_prd_site() -> Result<(), Box<dyn Error>> {
store.ensure_surface(&tab, ProfileDataMode::Transient, &[]);
let _ = wait_for_ready_frame_at_size(
&mut store,
tab.id(),
&tab,
case,
LIVE_SURFACE_WIDTH,
LIVE_SURFACE_HEIGHT,
@@ -216,7 +216,7 @@ fn assert_web_surface_resizes_prd_site() -> Result<(), Box<dyn Error>> {
store.ensure_surface(&tab, ProfileDataMode::Transient, &[]);
wait_for_ready_frame_at_size(
&mut store,
tab.id(),
&tab,
case,
RESIZED_LIVE_SURFACE_WIDTH,
RESIZED_LIVE_SURFACE_HEIGHT,
@@ -377,7 +377,7 @@ fn wait_for_ready_frame_at_scroll(
fn wait_for_ready_frame_at_size(
store: &mut WebSurfaceStore,
tab_id: &TabId,
tab: &BrowserTab,
case: &LiveSiteCase,
expected_width: u32,
expected_height: u32,
@@ -386,8 +386,9 @@ fn wait_for_ready_frame_at_size(
let mut last_error = None;
loop {
store.tick(std::slice::from_ref(tab_id));
match store.state(tab_id) {
store.ensure_surface(tab, ProfileDataMode::Transient, &[]);
store.tick(std::slice::from_ref(tab.id()));
match store.state(tab.id()) {
Some(WebSurfaceState::Ready(frame))
if frame.size().width == expected_width
&& frame.size().height == expected_height =>
@@ -395,7 +395,9 @@ impl WebSurfaceRuntime {
let Some(scoped) = self.worker.take() else {
return;
};
if let Some(path) = scoped.transient_profile_data_dir {
let ScopedWorker { worker, transient_profile_data_dir } = scoped;
drop(worker);
if let Some(path) = transient_profile_data_dir {
let _ = fs::remove_dir_all(path);
}
}
@@ -404,7 +406,9 @@ impl WebSurfaceRuntime {
let Some(scoped) = self.direct_client.take() else {
return;
};
if let Some(path) = scoped.transient_profile_data_dir {
let ScopedDirectClient { client, transient_profile_data_dir } = scoped;
drop(client);
if let Some(path) = transient_profile_data_dir {
let _ = fs::remove_dir_all(path);
}
}
+34 -4
View File
@@ -147,6 +147,29 @@ impl RenderedFrame {
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebViewSnapshotPending {
has_pending_frame: bool,
has_pending_metadata: bool,
}
impl WebViewSnapshotPending {
#[must_use]
pub fn new(has_pending_frame: bool, has_pending_metadata: bool) -> Self {
Self { has_pending_frame, has_pending_metadata }
}
#[must_use]
pub fn has_pending_frame(&self) -> bool {
self.has_pending_frame
}
#[must_use]
pub fn has_pending_metadata(&self) -> bool {
self.has_pending_metadata
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WebViewSnapshot {
webview_id: WebViewId,
@@ -155,7 +178,7 @@ pub struct WebViewSnapshot {
state: WebViewState,
url: Option<String>,
title: Option<String>,
has_pending_frame: bool,
pending: WebViewSnapshotPending,
}
impl WebViewSnapshot {
@@ -167,9 +190,9 @@ impl WebViewSnapshot {
state: WebViewState,
url: Option<String>,
title: Option<String>,
has_pending_frame: bool,
pending: WebViewSnapshotPending,
) -> Self {
Self { webview_id, tab_id, profile_id, state, url, title, has_pending_frame }
Self { webview_id, tab_id, profile_id, state, url, title, pending }
}
#[must_use]
@@ -204,7 +227,14 @@ impl WebViewSnapshot {
#[must_use]
pub fn has_pending_frame(&self) -> bool {
self.has_pending_frame
self.pending.has_pending_frame()
}
/// Returns true when URL, title, or load-state changed since the
/// last embedder snapshot observation.
#[must_use]
pub fn has_pending_metadata(&self) -> bool {
self.pending.has_pending_metadata()
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ pub use host::{
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest,
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderedFrame,
RenderedFrameSummary, ResizeRequest, ScrollRequest, ServoHost, TouchTapRequest,
WebViewSnapshot, WebViewState,
WebViewSnapshot, WebViewSnapshotPending, WebViewState,
};
#[cfg(feature = "servo-engine")]
pub use runtime::{RenderingContextKind, ServoSurfaceSize, SoftwareServoHost};
+49 -1
View File
@@ -45,6 +45,9 @@ pub struct SoftwareServoHost {
default_surface_size: ServoSurfaceSize,
rendering_context_kind: RenderingContextKind,
webviews: HashMap<WebViewId, HostWebView>,
// Servo 0.1.0 can still run script tasks after a remote page moves away.
// Retain hidden about:blank WebViews until host shutdown releases them together.
retired_webviews: Vec<HostWebView>,
permissions: PermissionStore,
wake_requested: Arc<AtomicBool>,
last_rendered_frame: Option<RenderedFrame>,
@@ -120,7 +123,27 @@ impl SoftwareServoHost {
}
pub fn close_webview(&mut self, webview_id: &WebViewId) -> bool {
self.webviews.remove(webview_id).is_some()
let Some(webview) = self.webviews.remove(webview_id) else {
return false;
};
self.retire_webview(webview);
true
}
fn retire_webview(&mut self, webview: HostWebView) {
webview.webview.hide();
if let Ok(blank_url) = Url::parse("about:blank") {
webview.webview.load(blank_url);
}
for _ in 0..32 {
self.servo.spin_event_loop();
if webview.current_url().as_deref() == Some("about:blank")
&& matches!(webview.state(), WebViewState::Complete)
{
break;
}
}
self.retired_webviews.push(webview);
}
fn new_started(
@@ -143,6 +166,7 @@ impl SoftwareServoHost {
default_surface_size: size,
rendering_context_kind,
webviews: HashMap::new(),
retired_webviews: Vec::new(),
permissions: Rc::new(RefCell::new(HashMap::new())),
wake_requested,
last_rendered_frame: None,
@@ -190,6 +214,24 @@ impl SoftwareServoHost {
self.last_rendered_frame = Some(rendered_frame);
Ok(())
}
/// Returns the current snapshot and acknowledges metadata-only
/// updates without clearing Servo's frame-ready signal.
pub fn snapshot_and_mark_metadata_observed(
&self,
webview_id: &WebViewId,
) -> Result<WebViewSnapshot, ServoHostError> {
let webview = self.webview(webview_id)?;
let snapshot = webview.snapshot(webview_id);
webview.delegate.mark_metadata_observed();
Ok(snapshot)
}
fn drain_after_webview_close(&self) {
for _ in 0..16 {
self.servo.spin_event_loop();
}
}
}
fn install_rustls_provider() {
@@ -395,6 +437,12 @@ impl ServoHost for SoftwareServoHost {
impl Drop for SoftwareServoHost {
fn drop(&mut self) {
let webviews = std::mem::take(&mut self.webviews);
for webview in webviews.into_values() {
self.retire_webview(webview);
}
self.retired_webviews.clear();
self.drain_after_webview_close();
SERVO_RUNTIME_STARTED.store(false, Ordering::Release);
}
}
+27 -10
View File
@@ -5,7 +5,7 @@ use servo::{LoadStatus, RenderingContext, WebView, WebViewDelegate};
use url::Url;
use crate::{
PermissionDecision, WebViewSnapshot, WebViewState,
PermissionDecision, WebViewSnapshot, WebViewSnapshotPending, WebViewState,
runtime_permissions::{PermissionStore, permission_decision_for_webview},
};
@@ -27,7 +27,10 @@ impl HostWebView {
self.state(),
self.current_url(),
self.current_title(),
self.delegate.has_pending_frame(),
WebViewSnapshotPending::new(
self.delegate.has_pending_frame(),
self.delegate.has_pending_metadata(),
),
)
}
@@ -62,6 +65,7 @@ pub(super) struct HostWebViewDelegate {
url: RefCell<Option<String>>,
title: RefCell<Option<String>>,
has_pending_frame: Cell<bool>,
has_pending_metadata: Cell<bool>,
}
impl HostWebViewDelegate {
@@ -73,6 +77,7 @@ impl HostWebViewDelegate {
url: RefCell::new(None),
title: RefCell::new(None),
has_pending_frame: Cell::new(false),
has_pending_metadata: Cell::new(false),
}
}
@@ -94,12 +99,12 @@ impl HostWebViewDelegate {
fn record_url_change(&self, url: String) {
self.url.replace(Some(url));
self.has_pending_frame.set(true);
self.has_pending_metadata.set(true);
}
fn record_title_change(&self, title: Option<String>) {
self.title.replace(title);
self.has_pending_frame.set(true);
self.has_pending_metadata.set(true);
}
fn record_load_status(&self, status: LoadStatus) {
@@ -108,16 +113,24 @@ impl HostWebViewDelegate {
LoadStatus::Complete => WebViewState::Complete,
};
self.set_state(state);
self.has_pending_frame.set(true);
self.has_pending_metadata.set(true);
}
pub(super) fn has_pending_frame(&self) -> bool {
self.has_pending_frame.get()
}
pub(super) fn has_pending_metadata(&self) -> bool {
self.has_pending_metadata.get()
}
pub(super) fn mark_frame_presented(&self) {
self.has_pending_frame.set(false);
}
pub(super) fn mark_metadata_observed(&self) {
self.has_pending_metadata.set(false);
}
}
impl WebViewDelegate for HostWebViewDelegate {
@@ -172,21 +185,25 @@ mod tests {
use super::HostWebViewDelegate;
#[test]
fn metadata_changes_mark_pending_frame() {
fn metadata_changes_are_separate_from_pending_frame() {
let delegate =
HostWebViewDelegate::new(ProfileId::new(), Rc::new(RefCell::new(HashMap::new())));
assert!(!delegate.has_pending_frame());
assert!(!delegate.has_pending_metadata());
delegate.record_title_change(Some("Example Domain".to_string()));
assert_eq!(delegate.title().as_deref(), Some("Example Domain"));
assert!(delegate.has_pending_frame());
delegate.mark_frame_presented();
assert!(!delegate.has_pending_frame());
assert!(delegate.has_pending_metadata());
delegate.mark_metadata_observed();
assert!(!delegate.has_pending_frame());
assert!(!delegate.has_pending_metadata());
delegate.record_url_change("https://example.com/".to_string());
assert_eq!(delegate.url().as_deref(), Some("https://example.com/"));
assert!(delegate.has_pending_frame());
assert!(!delegate.has_pending_frame());
assert!(delegate.has_pending_metadata());
}
}