This commit is contained in:
2026-05-09 16:27:40 -04:00
parent 86f558c65f
commit 3caf207129
31 changed files with 2727 additions and 1597 deletions
+25 -4
View File
@@ -26,15 +26,17 @@ mod web_surface;
mod web_surface_controller;
mod web_surface_frame;
mod web_surface_geometry;
mod web_surface_image;
mod web_surface_keyboard;
mod web_surface_permissions;
mod web_surface_runtime;
mod web_surface_state;
mod web_surface_view;
use std::time::Duration;
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{ProfileId, SpaceId, TabId};
use gpui::{AppContext, Context, Entity, FocusHandle, Subscription, Window};
use gpui::{AppContext, Context, Entity, FocusHandle, Subscription, Timer, Window};
use gpui_component::input::{InputEvent, InputState};
use crate::shortcuts::ShortcutProfile;
@@ -144,7 +146,7 @@ impl ElyShell {
Err(error) => ShellState::StartupError(error.to_string()),
};
Self {
let shell = Self {
state,
focus_handle: cx.focus_handle(),
command_input,
@@ -172,7 +174,9 @@ impl ElyShell {
pending_plugin_uninstall: None,
web_surfaces: WebSurfaceStore::new(),
_command_subscription: command_subscription,
}
};
start_external_web_surface_timer(cx);
shell
}
fn focus_command_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -432,3 +436,20 @@ impl ElyShell {
self.zoom_active_tab_out(cx);
}
}
fn start_external_web_surface_timer(cx: &mut Context<ElyShell>) {
cx.spawn(async move |shell, cx| {
loop {
Timer::after(Duration::from_millis(16)).await;
let result = shell.update(cx, |shell, cx| {
if shell.tick_external_web_surfaces() {
cx.notify();
}
});
if result.is_err() {
break;
}
}
})
.detach();
}
+77 -39
View File
@@ -4,14 +4,14 @@ use gpui::RenderImage;
use image::{ImageBuffer, Rgba};
use thiserror::Error;
use crate::services::servo_sidecar::SidecarSnapshot;
use crate::services::servo_live::ServoLiveFrame;
use super::{
web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollOffset, WebSurfaceSize},
web_surface_image::renderable_image_buffer,
};
#[cfg(all(test, feature = "live-site-smoke"))]
use super::web_surface_geometry::WebSurfaceSize;
use super::web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollOffset};
#[derive(Clone)]
#[cfg_attr(not(all(test, feature = "live-site-smoke")), allow(dead_code))]
pub(super) struct WebSurfaceFrame {
pub(super) requested_url: String,
loaded_url: Option<String>,
@@ -33,62 +33,75 @@ pub(super) struct WebSurfaceFrame {
}
impl WebSurfaceFrame {
pub(super) fn from_snapshot(
pub(super) fn from_live_frame(
requested_url: String,
scroll_offset: WebSurfaceScrollOffset,
zoom_percent: u16,
click_point: Option<WebSurfaceClickPoint>,
typed_text: Option<String>,
snapshot: SidecarSnapshot,
frame: ServoLiveFrame,
) -> Result<Self, WebSurfaceError> {
let width = snapshot.width();
let height = snapshot.height();
let loaded_url = snapshot.loaded_url().map(str::to_string);
let title = snapshot.title().map(str::to_string);
let render_state = snapshot.render_state().to_string();
#[cfg(all(test, feature = "live-site-smoke"))]
let non_white_pixel_count = snapshot.non_white_pixel_count();
#[cfg(all(test, feature = "live-site-smoke"))]
let content_pixel_count = snapshot.content_pixel_count();
#[cfg(all(test, feature = "live-site-smoke"))]
let sample_hash = snapshot.sample_hash();
let rgba_bytes = snapshot.into_rgba_bytes();
let Some(buffer) = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, rgba_bytes) else {
return Err(WebSurfaceError::InvalidFrameBuffer { width, height });
};
let image_buffer = renderable_image_buffer(buffer);
Ok(Self {
Self::from_parts(WebSurfaceFrameParts {
requested_url,
loaded_url,
title,
render_state,
width,
height,
loaded_url: frame.loaded_url().map(str::to_string),
title: frame.title().map(str::to_string),
render_state: frame.render_state().to_string(),
width: frame.width(),
height: frame.height(),
scroll_offset,
zoom_percent,
click_point,
typed_text,
click_point: None,
typed_text: None,
#[cfg(all(test, feature = "live-site-smoke"))]
non_white_pixel_count,
non_white_pixel_count: frame.non_white_pixel_count(),
#[cfg(all(test, feature = "live-site-smoke"))]
content_pixel_count,
content_pixel_count: frame.content_pixel_count(),
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash,
sample_hash: frame.sample_hash(),
rgba_bytes: frame.into_rgba_bytes(),
})
}
fn from_parts(parts: WebSurfaceFrameParts) -> Result<Self, WebSurfaceError> {
let Some(image_buffer) =
ImageBuffer::<Rgba<u8>, _>::from_raw(parts.width, parts.height, parts.rgba_bytes)
else {
return Err(WebSurfaceError::InvalidFrameBuffer {
width: parts.width,
height: parts.height,
});
};
Ok(Self {
requested_url: parts.requested_url,
loaded_url: parts.loaded_url,
title: parts.title,
render_state: parts.render_state,
width: parts.width,
height: parts.height,
scroll_offset: parts.scroll_offset,
zoom_percent: parts.zoom_percent,
click_point: parts.click_point,
typed_text: parts.typed_text,
#[cfg(all(test, feature = "live-site-smoke"))]
non_white_pixel_count: parts.non_white_pixel_count,
#[cfg(all(test, feature = "live-site-smoke"))]
content_pixel_count: parts.content_pixel_count,
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: parts.sample_hash,
image: Arc::new(RenderImage::new([image::Frame::new(image_buffer)])),
})
}
#[cfg(all(test, feature = "live-site-smoke"))]
pub(super) fn title_label(&self) -> String {
self.title.clone().unwrap_or_else(|| self.requested_url.clone())
}
#[cfg(all(test, feature = "live-site-smoke"))]
pub(super) fn url_label(&self) -> &str {
self.loaded_url.as_deref().unwrap_or(self.requested_url.as_str())
}
#[cfg(all(test, feature = "live-site-smoke"))]
pub(super) fn detail_label(&self) -> String {
let mut detail =
format!("{} {}", self.render_state(), self.scroll_offset.detail_label(self.size()));
@@ -104,14 +117,17 @@ impl WebSurfaceFrame {
}
}
#[cfg(all(test, feature = "live-site-smoke"))]
pub(super) fn size(&self) -> WebSurfaceSize {
WebSurfaceSize { width: self.width, height: self.height }
}
#[cfg(all(test, feature = "live-site-smoke"))]
pub(super) fn render_state(&self) -> &str {
self.render_state.as_str()
}
#[cfg(all(test, feature = "live-site-smoke"))]
pub(super) fn scroll_offset(&self) -> WebSurfaceScrollOffset {
self.scroll_offset
}
@@ -120,10 +136,12 @@ impl WebSurfaceFrame {
self.zoom_percent
}
#[cfg(all(test, feature = "live-site-smoke"))]
pub(super) fn click_point(&self) -> Option<WebSurfaceClickPoint> {
self.click_point
}
#[cfg(all(test, feature = "live-site-smoke"))]
pub(super) fn typed_text(&self) -> Option<&str> {
self.typed_text.as_deref()
}
@@ -144,6 +162,26 @@ impl WebSurfaceFrame {
}
}
struct WebSurfaceFrameParts {
requested_url: String,
loaded_url: Option<String>,
title: Option<String>,
render_state: String,
width: u32,
height: u32,
scroll_offset: WebSurfaceScrollOffset,
zoom_percent: u16,
click_point: Option<WebSurfaceClickPoint>,
typed_text: Option<String>,
#[cfg(all(test, feature = "live-site-smoke"))]
non_white_pixel_count: u64,
#[cfg(all(test, feature = "live-site-smoke"))]
content_pixel_count: u64,
#[cfg(all(test, feature = "live-site-smoke"))]
sample_hash: u64,
rgba_bytes: Vec<u8>,
}
#[derive(Debug, Error)]
pub(super) enum WebSurfaceError {
#[error("invalid servo frame buffer for {width}x{height}")]
@@ -1,6 +1,6 @@
use gpui::{Bounds, Pixels, Point};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(super) struct WebSurfaceSize {
pub(super) width: u32,
pub(super) height: u32,
@@ -32,6 +32,7 @@ impl WebSurfaceClickPoint {
})
}
#[cfg(all(test, feature = "live-site-smoke"))]
pub(super) fn detail_label(self) -> String {
format!("click={},{}", self.x, self.y)
}
@@ -59,6 +60,7 @@ impl WebSurfaceScrollOffset {
}
}
#[cfg(all(test, feature = "live-site-smoke"))]
pub(super) fn detail_label(self, size: WebSurfaceSize) -> String {
match (self.x, self.y) {
(0, 0) => format!("{}x{}", size.width, size.height),
@@ -67,10 +69,7 @@ impl WebSurfaceScrollOffset {
}
}
pub(super) fn x(self) -> i32 {
self.x
}
#[cfg_attr(not(test), allow(dead_code))]
pub(super) fn y(self) -> i32 {
self.y
}
@@ -92,6 +91,18 @@ impl WebSurfaceScrollDelta {
Some(Self { x, y })
}
pub(super) fn x(self) -> i32 {
self.x
}
pub(super) fn y(self) -> i32 {
self.y
}
pub(super) fn combined_with(self, next: Self) -> Self {
Self { x: combined_scroll_delta(self.x, next.x), y: combined_scroll_delta(self.y, next.y) }
}
}
fn viewport_dimension(pixels: Pixels) -> Option<u32> {
@@ -133,3 +144,8 @@ fn positive_scroll_component(current: i32, delta: i32) -> i32 {
let clamped = value.clamp(0, i64::from(i32::MAX));
clamped as i32
}
fn combined_scroll_delta(current: i32, next: i32) -> i32 {
let value = i64::from(current) + i64::from(next);
value.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32
}
@@ -1,28 +0,0 @@
use image::{ImageBuffer, Rgba, imageops::FilterType};
const WEB_SURFACE_IMAGE_MAX_EDGE: u32 = 1024;
pub(super) fn renderable_image_buffer(
buffer: ImageBuffer<Rgba<u8>, Vec<u8>>,
) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
let largest_edge = buffer.width().max(buffer.height());
if largest_edge <= WEB_SURFACE_IMAGE_MAX_EDGE {
return buffer;
}
image::imageops::resize(
&buffer,
scaled_image_dimension(buffer.width(), largest_edge),
scaled_image_dimension(buffer.height(), largest_edge),
FilterType::Triangle,
)
}
fn scaled_image_dimension(dimension: u32, largest_edge: u32) -> u32 {
let numerator = u64::from(dimension) * u64::from(WEB_SURFACE_IMAGE_MAX_EDGE);
let rounded = (numerator + u64::from(largest_edge / 2)) / u64::from(largest_edge);
match u32::try_from(rounded.max(1)) {
Ok(value) => value,
Err(_) => WEB_SURFACE_IMAGE_MAX_EDGE,
}
}
@@ -1,13 +1,21 @@
use std::error::Error;
use std::{
env,
error::Error,
process::{Command, Stdio},
thread,
time::{Duration, Instant},
};
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
use gpui::{Bounds, point, px, size};
use crate::{
services::ProfileDataMode,
services::prd_live_sites::{
LiveSiteCase, PRD_REFERENCE_SITE_CASES, PRD_TOP_SITE_CASES,
assert_prd_reference_urls_are_covered,
services::{
ProfileDataMode,
prd_live_sites::{
LiveSiteCase, PRD_REFERENCE_SITE_CASES, PRD_TOP_SITE_CASES,
assert_prd_reference_urls_are_covered,
},
},
shell::{
web_surface_frame::WebSurfaceFrame,
@@ -22,6 +30,9 @@ const LIVE_SURFACE_WIDTH: u32 = 934;
const LIVE_SURFACE_HEIGHT: u32 = 657;
const MINIMUM_CONTENT_PIXELS: u64 = 1_000;
const LIVE_SITE_RENDER_ATTEMPTS: usize = 3;
const LIVE_SITE_WAIT_TIMEOUT: Duration = Duration::from_secs(20);
const LIVE_SITE_WAIT_INTERVAL: Duration = Duration::from_millis(2);
const LIVE_SITE_CHILD_ENV: &str = "ELY_APP_WEB_SURFACE_LIVE_CHILD";
#[test]
fn web_surface_cases_cover_prd_reference_urls() -> Result<(), Box<dyn Error>> {
@@ -30,63 +41,110 @@ fn web_surface_cases_cover_prd_reference_urls() -> Result<(), Box<dyn Error>> {
#[test]
fn web_surface_opens_and_renders_prd_top_sites() -> Result<(), Box<dyn Error>> {
assert_web_surfaces_render(PRD_TOP_SITE_CASES)
run_isolated_live_site_test("web_surface_opens_and_renders_prd_top_sites", || {
assert_web_surfaces_render(PRD_TOP_SITE_CASES)
})
}
#[test]
fn web_surface_opens_and_renders_prd_reference_sites() -> Result<(), Box<dyn Error>> {
assert_web_surfaces_render(PRD_REFERENCE_SITE_CASES)
run_isolated_live_site_test("web_surface_opens_and_renders_prd_reference_sites", || {
assert_web_surfaces_render(PRD_REFERENCE_SITE_CASES)
})
}
fn run_isolated_live_site_test(
test_name: &str,
test: impl FnOnce() -> Result<(), Box<dyn Error>>,
) -> Result<(), Box<dyn Error>> {
if env::var_os(LIVE_SITE_CHILD_ENV).is_some() {
return test();
}
let output = Command::new(env::current_exe()?)
.arg(test_name)
.env(LIVE_SITE_CHILD_ENV, "1")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;
if output.status.success() {
return Ok(());
}
Err(format!(
"isolated web surface live-site test failed\nstatus: {}\nstdout: {}\nstderr: {}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
.into())
}
fn assert_web_surfaces_render(cases: &[LiveSiteCase]) -> Result<(), Box<dyn Error>> {
let mut store = WebSurfaceStore::new();
let profile_id = ProfileId::new();
for case in cases {
let frame = render_web_surface_frame(case)?;
let frame = render_web_surface_frame(&mut store, &profile_id, case)?;
log_prd_frame("web-surface", &frame, case);
}
Ok(())
}
fn render_web_surface_frame(case: &LiveSiteCase) -> Result<WebSurfaceFrame, Box<dyn Error>> {
fn render_web_surface_frame(
store: &mut WebSurfaceStore,
profile_id: &ProfileId,
case: &LiveSiteCase,
) -> Result<WebSurfaceFrame, Box<dyn Error>> {
let mut last_error = String::new();
for attempt in 0..LIVE_SITE_RENDER_ATTEMPTS {
let mut store = WebSurfaceStore::new();
let tab = web_tab(case.url)?;
let tab = web_tab(profile_id.clone(), case.url)?;
assert!(store.record_viewport_size(tab.id(), live_surface_bounds()), "{}", case.url);
let request = store
.prepare_request(&tab, ProfileDataMode::Persistent)
.ok_or_else(|| format!("missing web surface request for {}", case.url))?;
let tab_id = request.tab_id.clone();
let snapshot = request.client.snapshot(request.snapshot_request)?;
let frame = WebSurfaceFrame::from_snapshot(
request.requested_url,
request.scroll_offset,
request.zoom_percent,
request.click_point,
request.typed_text,
snapshot,
)?;
store.ensure_surface(&tab, ProfileDataMode::Transient, &[]);
match validate_prd_frame(&frame, case) {
Ok(()) => {
store.finish(tab_id, WebSurfaceState::Ready(frame.clone()));
let Some(WebSurfaceState::Ready(stored_frame)) = store.state(tab.id()) else {
return Err(format!("web surface state is not ready for {}", case.url).into());
};
validate_prd_frame(stored_frame, case)?;
return Ok(frame);
}
match wait_for_ready_frame(store, tab.id(), case) {
Ok(frame) => return Ok(frame),
Err(error) => last_error = error,
}
if attempt + 1 < LIVE_SITE_RENDER_ATTEMPTS {
std::thread::sleep(std::time::Duration::from_millis(250));
thread::sleep(Duration::from_millis(250));
}
}
Err(last_error.into())
}
fn wait_for_ready_frame(
store: &mut WebSurfaceStore,
tab_id: &TabId,
case: &LiveSiteCase,
) -> Result<WebSurfaceFrame, String> {
let started_at = Instant::now();
loop {
if started_at.elapsed() >= LIVE_SITE_WAIT_TIMEOUT {
return Err(format!("timed out rendering {}", case.url));
}
store.tick();
match store.state(tab_id) {
Some(WebSurfaceState::Ready(frame)) => {
validate_prd_frame(frame, case)?;
return Ok(frame.clone());
}
Some(WebSurfaceState::Failed { message, .. }) => {
return Err(format!("{} failed: {message}", case.url));
}
Some(WebSurfaceState::Loading { .. }) | None => {}
}
thread::sleep(LIVE_SITE_WAIT_INTERVAL);
}
}
fn validate_prd_frame(frame: &WebSurfaceFrame, case: &LiveSiteCase) -> Result<(), String> {
require(
frame.size() == WebSurfaceSize { width: LIVE_SURFACE_WIDTH, height: LIVE_SURFACE_HEIGHT },
@@ -147,8 +205,8 @@ fn live_surface_bounds() -> Bounds<gpui::Pixels> {
)
}
fn web_tab(url: &str) -> Result<BrowserTab, Box<dyn Error>> {
Ok(BrowserTab::new(TabId::new(), SpaceId::new(), ProfileId::new(), "Web", UrlText::parse(url)?))
fn web_tab(profile_id: ProfileId, url: &str) -> Result<BrowserTab, Box<dyn Error>> {
Ok(BrowserTab::new(TabId::new(), SpaceId::new(), profile_id, "Web", UrlText::parse(url)?))
}
fn normalized_url(url: &str) -> &str {
@@ -1,12 +1,39 @@
use ely_browser_core::BrowserSnapshot;
use ely_domain::{BrowserTab, SiteOrigin};
use ely_domain::{BrowserTab, SiteOrigin, SitePermissionDecision, SitePermissionFeature};
use crate::services::servo_sidecar::SidecarSitePermission;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct WebSurfaceSitePermission {
origin: SiteOrigin,
feature: SitePermissionFeature,
decision: SitePermissionDecision,
}
pub(super) fn sidecar_site_permissions_for_tab(
impl WebSurfaceSitePermission {
pub(super) fn new(
origin: SiteOrigin,
feature: SitePermissionFeature,
decision: SitePermissionDecision,
) -> Self {
Self { origin, feature, decision }
}
pub(super) fn origin(&self) -> &SiteOrigin {
&self.origin
}
pub(super) fn feature(&self) -> SitePermissionFeature {
self.feature
}
pub(super) fn decision(&self) -> SitePermissionDecision {
self.decision
}
}
pub(super) fn web_surface_site_permissions_for_tab(
tab: &BrowserTab,
snapshot: &BrowserSnapshot,
) -> Vec<SidecarSitePermission> {
) -> Vec<WebSurfaceSitePermission> {
let Ok(Some(origin)) = SiteOrigin::from_url(tab.url()) else {
return Vec::new();
};
@@ -17,7 +44,7 @@ pub(super) fn sidecar_site_permissions_for_tab(
.filter(|entry| entry.profile_id() == tab.profile_id())
.filter(|entry| entry.origin() == &origin)
.map(|entry| {
SidecarSitePermission::new(entry.origin().clone(), entry.feature(), entry.decision())
WebSurfaceSitePermission::new(entry.origin().clone(), entry.feature(), entry.decision())
})
.collect()
}
@@ -31,7 +58,7 @@ mod tests {
BrowserTab, SiteOrigin, SitePermissionDecision, SitePermissionFeature, UrlText,
};
use super::sidecar_site_permissions_for_tab;
use super::web_surface_site_permissions_for_tab;
#[test]
fn includes_matching_profile_and_origin_permissions() -> Result<(), Box<dyn Error>> {
@@ -45,7 +72,7 @@ mod tests {
let snapshot = core.snapshot()?;
let tab = active_tab(&snapshot)?;
let permissions = sidecar_site_permissions_for_tab(tab, &snapshot);
let permissions = web_surface_site_permissions_for_tab(tab, &snapshot);
assert_eq!(permissions.len(), 1);
assert_eq!(permissions[0].origin().as_str(), "https://example.com");
@@ -66,7 +93,7 @@ mod tests {
let snapshot = core.snapshot()?;
let tab = active_tab(&snapshot)?;
assert!(sidecar_site_permissions_for_tab(tab, &snapshot).is_empty());
assert!(web_surface_site_permissions_for_tab(tab, &snapshot).is_empty());
Ok(())
}
+27 -35
View File
@@ -3,68 +3,60 @@ use std::error::Error;
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
use gpui::{Bounds, point, px, size};
use super::{ProfileDataMode, WebSurfaceStore};
use super::WebSurfaceStore;
#[test]
fn typed_text_enters_snapshot_request_after_clicked_viewport() -> Result<(), Box<dyn Error>> {
fn typed_text_enters_pending_input_after_clicked_viewport() -> Result<(), Box<dyn Error>> {
let mut store = WebSurfaceStore::new();
let tab = web_tab("https://example.com/form")?;
let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)));
assert!(store.record_viewport_size(tab.id(), bounds));
assert!(store.record_viewport_size(tab.id(), web_bounds()));
assert!(store.record_click_point(tab.id(), tab.url().as_str(), point(px(160.0), px(120.0))));
assert!(store.record_typed_text(tab.id(), tab.url().as_str(), "e"));
assert!(store.record_typed_text(tab.id(), tab.url().as_str(), "l"));
let request = store
.prepare_request(&tab, ProfileDataMode::Persistent)
.ok_or("missing web surface request")?;
let input = store.take_pending_input(tab.id(), tab.url().as_str());
assert_eq!(request.typed_text.as_deref(), Some("el"));
assert_eq!(request.snapshot_request.typed_text_for_test(), Some("el"));
assert_eq!(request.snapshot_request.profile_id_for_test(), tab.profile_id());
assert_eq!(request.zoom_percent, ely_domain::DEFAULT_ZOOM_PERCENT);
assert_eq!(
request.snapshot_request.page_zoom_percent_for_test(),
ely_domain::DEFAULT_ZOOM_PERCENT
);
assert_eq!(input.click_point.map(|point| (point.x(), point.y())), Some((160, 120)));
assert_eq!(input.typed_text.as_deref(), Some("el"));
Ok(())
}
#[test]
fn private_profile_enters_transient_snapshot_request() -> Result<(), Box<dyn Error>> {
fn scroll_delta_enters_pending_input_after_wheel() -> Result<(), Box<dyn Error>> {
let mut store = WebSurfaceStore::new();
let tab = web_tab("https://example.com/private")?;
let tab = web_tab("https://example.com/list")?;
let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)));
assert!(store.record_viewport_size(tab.id(), bounds));
assert!(store.record_viewport_size(tab.id(), web_bounds()));
assert!(store.record_scroll_delta(tab.id(), tab.url().as_str(), point(px(0.0), px(140.0))));
assert!(store.record_scroll_delta(tab.id(), tab.url().as_str(), point(px(0.0), px(60.0))));
let request = store
.prepare_request(&tab, ProfileDataMode::Transient)
.ok_or("missing web surface request")?;
let input = store.take_pending_input(tab.id(), tab.url().as_str());
assert_eq!(request.snapshot_request.profile_data_mode_for_test(), ProfileDataMode::Transient);
assert_eq!(input.scroll_offset.y(), 200);
assert_eq!(input.scroll_delta.map(|delta| (delta.x(), delta.y())), Some((0, 200)));
Ok(())
}
#[test]
fn tab_zoom_enters_snapshot_request() -> Result<(), Box<dyn Error>> {
fn viewport_size_changes_after_stable_second_measurement() -> Result<(), Box<dyn Error>> {
let mut store = WebSurfaceStore::new();
let mut tab = web_tab("https://example.com/zoom")?;
tab.set_zoom_percent(125)?;
let tab = web_tab("https://example.com/resize")?;
let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)));
assert!(store.record_viewport_size(tab.id(), bounds));
let request = store
.prepare_request(&tab, ProfileDataMode::Persistent)
.ok_or("missing web surface request")?;
assert_eq!(request.zoom_percent, 125);
assert_eq!(request.snapshot_request.page_zoom_percent_for_test(), 125);
assert!(store.record_viewport_size(tab.id(), web_bounds()));
assert!(!store.record_viewport_size(tab.id(), resized_once_bounds()));
assert!(store.record_viewport_size(tab.id(), resized_once_bounds()));
Ok(())
}
fn web_bounds() -> Bounds<gpui::Pixels> {
Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)))
}
fn resized_once_bounds() -> Bounds<gpui::Pixels> {
Bounds::new(point(px(0.0), px(0.0)), size(px(934.0), px(657.0)))
}
fn web_tab(url: &str) -> Result<BrowserTab, Box<dyn Error>> {
Ok(BrowserTab::new(TabId::new(), SpaceId::new(), ProfileId::new(), "Web", UrlText::parse(url)?))
}