mirror of
https://github.com/servo/servo.git
synced 2025-07-22 23:03:42 +01:00
libservo: Allow embedders to execute JavaScript scripts via the API (#35720)
This change adds a new `WebView` API `evaluate_javascript()`, which allows embedders to execute JavaScript code and wait for a reply asynchronously. Ongoing script execution is tracked by a libservo `JavaScriptEvaluator` struct, which maps an id to the callback passed to the `evaluate_javascript()` method. The id is used to track the script and its execution through the other parts of Servo. Testing: This changes includes `WebView` unit tests. --------- Signed-off-by: Narfinger <Narfinger@users.noreply.github.com> Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
parent
91c4c7b998
commit
991be359a3
12 changed files with 391 additions and 18 deletions
|
@ -129,9 +129,10 @@ use embedder_traits::resources::{self, Resource};
|
||||||
use embedder_traits::user_content_manager::UserContentManager;
|
use embedder_traits::user_content_manager::UserContentManager;
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
AnimationState, CompositorHitTestResult, Cursor, EmbedderMsg, EmbedderProxy,
|
AnimationState, CompositorHitTestResult, Cursor, EmbedderMsg, EmbedderProxy,
|
||||||
FocusSequenceNumber, ImeEvent, InputEvent, MediaSessionActionType, MediaSessionEvent,
|
FocusSequenceNumber, ImeEvent, InputEvent, JSValue, JavaScriptEvaluationError,
|
||||||
MediaSessionPlaybackState, MouseButton, MouseButtonAction, MouseButtonEvent, Theme,
|
JavaScriptEvaluationId, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState,
|
||||||
ViewportDetails, WebDriverCommandMsg, WebDriverLoadStatus,
|
MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg,
|
||||||
|
WebDriverLoadStatus,
|
||||||
};
|
};
|
||||||
use euclid::Size2D;
|
use euclid::Size2D;
|
||||||
use euclid::default::Size2D as UntypedSize2D;
|
use euclid::default::Size2D as UntypedSize2D;
|
||||||
|
@ -1477,6 +1478,52 @@ where
|
||||||
EmbedderToConstellationMessage::PaintMetric(pipeline_id, paint_metric_event) => {
|
EmbedderToConstellationMessage::PaintMetric(pipeline_id, paint_metric_event) => {
|
||||||
self.handle_paint_metric(pipeline_id, paint_metric_event);
|
self.handle_paint_metric(pipeline_id, paint_metric_event);
|
||||||
},
|
},
|
||||||
|
EmbedderToConstellationMessage::EvaluateJavaScript(
|
||||||
|
webview_id,
|
||||||
|
evaluation_id,
|
||||||
|
script,
|
||||||
|
) => {
|
||||||
|
self.handle_evaluate_javascript(webview_id, evaluation_id, script);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "tracing",
|
||||||
|
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
||||||
|
)]
|
||||||
|
fn handle_evaluate_javascript(
|
||||||
|
&mut self,
|
||||||
|
webview_id: WebViewId,
|
||||||
|
evaluation_id: JavaScriptEvaluationId,
|
||||||
|
script: String,
|
||||||
|
) {
|
||||||
|
let browsing_context_id = BrowsingContextId::from(webview_id);
|
||||||
|
let Some(pipeline) = self
|
||||||
|
.browsing_contexts
|
||||||
|
.get(&browsing_context_id)
|
||||||
|
.and_then(|browsing_context| self.pipelines.get(&browsing_context.pipeline_id))
|
||||||
|
else {
|
||||||
|
self.handle_finish_javascript_evaluation(
|
||||||
|
evaluation_id,
|
||||||
|
Err(JavaScriptEvaluationError::InternalError),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if pipeline
|
||||||
|
.event_loop
|
||||||
|
.send(ScriptThreadMessage::EvaluateJavaScript(
|
||||||
|
pipeline.id,
|
||||||
|
evaluation_id,
|
||||||
|
script,
|
||||||
|
))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
self.handle_finish_javascript_evaluation(
|
||||||
|
evaluation_id,
|
||||||
|
Err(JavaScriptEvaluationError::InternalError),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1817,6 +1864,9 @@ where
|
||||||
self.mem_profiler_chan
|
self.mem_profiler_chan
|
||||||
.send(mem::ProfilerMsg::Report(sender));
|
.send(mem::ProfilerMsg::Report(sender));
|
||||||
},
|
},
|
||||||
|
ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result) => {
|
||||||
|
self.handle_finish_javascript_evaluation(evaluation_id, result)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3178,6 +3228,22 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "tracing",
|
||||||
|
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
||||||
|
)]
|
||||||
|
fn handle_finish_javascript_evaluation(
|
||||||
|
&mut self,
|
||||||
|
evaluation_id: JavaScriptEvaluationId,
|
||||||
|
result: Result<JSValue, JavaScriptEvaluationError>,
|
||||||
|
) {
|
||||||
|
self.embedder_proxy
|
||||||
|
.send(EmbedderMsg::FinishJavaScriptEvaluation(
|
||||||
|
evaluation_id,
|
||||||
|
result,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "tracing",
|
feature = "tracing",
|
||||||
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
||||||
|
@ -4691,6 +4757,7 @@ where
|
||||||
NavigationHistoryBehavior::Replace,
|
NavigationHistoryBehavior::Replace,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
// TODO: This should use the ScriptThreadMessage::EvaluateJavaScript command
|
||||||
WebDriverCommandMsg::ScriptCommand(browsing_context_id, cmd) => {
|
WebDriverCommandMsg::ScriptCommand(browsing_context_id, cmd) => {
|
||||||
let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) {
|
let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) {
|
||||||
Some(browsing_context) => browsing_context.pipeline_id,
|
Some(browsing_context) => browsing_context.pipeline_id,
|
||||||
|
|
|
@ -77,6 +77,7 @@ mod from_compositor {
|
||||||
Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"),
|
Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"),
|
||||||
Self::SetScrollStates(..) => target!("SetScrollStates"),
|
Self::SetScrollStates(..) => target!("SetScrollStates"),
|
||||||
Self::PaintMetric(..) => target!("PaintMetric"),
|
Self::PaintMetric(..) => target!("PaintMetric"),
|
||||||
|
Self::EvaluateJavaScript(..) => target!("EvaluateJavaScript"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,6 +177,7 @@ mod from_script {
|
||||||
Self::TitleChanged(..) => target!("TitleChanged"),
|
Self::TitleChanged(..) => target!("TitleChanged"),
|
||||||
Self::IFrameSizes(..) => target!("IFrameSizes"),
|
Self::IFrameSizes(..) => target!("IFrameSizes"),
|
||||||
Self::ReportMemory(..) => target!("ReportMemory"),
|
Self::ReportMemory(..) => target!("ReportMemory"),
|
||||||
|
Self::FinishJavaScriptEvaluation(..) => target!("FinishJavaScriptEvaluation"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,6 +240,9 @@ mod from_script {
|
||||||
Self::ShutdownComplete => target_variant!("ShutdownComplete"),
|
Self::ShutdownComplete => target_variant!("ShutdownComplete"),
|
||||||
Self::ShowNotification(..) => target_variant!("ShowNotification"),
|
Self::ShowNotification(..) => target_variant!("ShowNotification"),
|
||||||
Self::ShowSelectElementMenu(..) => target_variant!("ShowSelectElementMenu"),
|
Self::ShowSelectElementMenu(..) => target_variant!("ShowSelectElementMenu"),
|
||||||
|
Self::FinishJavaScriptEvaluation(..) => {
|
||||||
|
target_variant!("FinishJavaScriptEvaluation")
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,6 +91,7 @@ impl MixedMessage {
|
||||||
#[cfg(feature = "webgpu")]
|
#[cfg(feature = "webgpu")]
|
||||||
ScriptThreadMessage::SetWebGPUPort(..) => None,
|
ScriptThreadMessage::SetWebGPUPort(..) => None,
|
||||||
ScriptThreadMessage::SetScrollStates(id, ..) => Some(*id),
|
ScriptThreadMessage::SetScrollStates(id, ..) => Some(*id),
|
||||||
|
ScriptThreadMessage::EvaluateJavaScript(id, _, _) => Some(*id),
|
||||||
},
|
},
|
||||||
MixedMessage::FromScript(inner_msg) => match inner_msg {
|
MixedMessage::FromScript(inner_msg) => match inner_msg {
|
||||||
MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => {
|
MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => {
|
||||||
|
|
|
@ -50,9 +50,9 @@ use devtools_traits::{
|
||||||
};
|
};
|
||||||
use embedder_traits::user_content_manager::UserContentManager;
|
use embedder_traits::user_content_manager::UserContentManager;
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
CompositorHitTestResult, EmbedderMsg, FocusSequenceNumber, InputEvent, MediaSessionActionType,
|
CompositorHitTestResult, EmbedderMsg, FocusSequenceNumber, InputEvent,
|
||||||
MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails,
|
JavaScriptEvaluationError, JavaScriptEvaluationId, MediaSessionActionType, MouseButton,
|
||||||
WebDriverScriptCommand,
|
MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverScriptCommand,
|
||||||
};
|
};
|
||||||
use euclid::Point2D;
|
use euclid::Point2D;
|
||||||
use euclid::default::Rect;
|
use euclid::default::Rect;
|
||||||
|
@ -156,6 +156,7 @@ use crate::script_runtime::{
|
||||||
};
|
};
|
||||||
use crate::task_queue::TaskQueue;
|
use crate::task_queue::TaskQueue;
|
||||||
use crate::task_source::{SendableTaskSource, TaskSourceName};
|
use crate::task_source::{SendableTaskSource, TaskSourceName};
|
||||||
|
use crate::webdriver_handlers::jsval_to_webdriver;
|
||||||
use crate::{devtools, webdriver_handlers};
|
use crate::{devtools, webdriver_handlers};
|
||||||
|
|
||||||
thread_local!(static SCRIPT_THREAD_ROOT: Cell<Option<*const ScriptThread>> = const { Cell::new(None) });
|
thread_local!(static SCRIPT_THREAD_ROOT: Cell<Option<*const ScriptThread>> = const { Cell::new(None) });
|
||||||
|
@ -1878,6 +1879,9 @@ impl ScriptThread {
|
||||||
ScriptThreadMessage::SetScrollStates(pipeline_id, scroll_states) => {
|
ScriptThreadMessage::SetScrollStates(pipeline_id, scroll_states) => {
|
||||||
self.handle_set_scroll_states(pipeline_id, scroll_states)
|
self.handle_set_scroll_states(pipeline_id, scroll_states)
|
||||||
},
|
},
|
||||||
|
ScriptThreadMessage::EvaluateJavaScript(pipeline_id, evaluation_id, script) => {
|
||||||
|
self.handle_evaluate_javascript(pipeline_id, evaluation_id, script, can_gc);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3815,6 +3819,53 @@ impl ScriptThread {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_evaluate_javascript(
|
||||||
|
&self,
|
||||||
|
pipeline_id: PipelineId,
|
||||||
|
evaluation_id: JavaScriptEvaluationId,
|
||||||
|
script: String,
|
||||||
|
can_gc: CanGc,
|
||||||
|
) {
|
||||||
|
let Some(window) = self.documents.borrow().find_window(pipeline_id) else {
|
||||||
|
let _ = self.senders.pipeline_to_constellation_sender.send((
|
||||||
|
pipeline_id,
|
||||||
|
ScriptToConstellationMessage::FinishJavaScriptEvaluation(
|
||||||
|
evaluation_id,
|
||||||
|
Err(JavaScriptEvaluationError::WebViewNotReady),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let global_scope = window.as_global_scope();
|
||||||
|
let realm = enter_realm(global_scope);
|
||||||
|
let context = window.get_cx();
|
||||||
|
|
||||||
|
rooted!(in(*context) let mut return_value = UndefinedValue());
|
||||||
|
global_scope.evaluate_js_on_global_with_result(
|
||||||
|
&script,
|
||||||
|
return_value.handle_mut(),
|
||||||
|
ScriptFetchOptions::default_classic_script(global_scope),
|
||||||
|
global_scope.api_base_url(),
|
||||||
|
can_gc,
|
||||||
|
);
|
||||||
|
let result = match jsval_to_webdriver(
|
||||||
|
context,
|
||||||
|
global_scope,
|
||||||
|
return_value.handle(),
|
||||||
|
(&realm).into(),
|
||||||
|
can_gc,
|
||||||
|
) {
|
||||||
|
Ok(ref value) => Ok(value.into()),
|
||||||
|
Err(_) => Err(JavaScriptEvaluationError::SerializationError),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = self.senders.pipeline_to_constellation_sender.send((
|
||||||
|
pipeline_id,
|
||||||
|
ScriptToConstellationMessage::FinishJavaScriptEvaluation(evaluation_id, result),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for ScriptThread {
|
impl Drop for ScriptThread {
|
||||||
|
|
65
components/servo/javascript_evaluator.rs
Normal file
65
components/servo/javascript_evaluator.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use base::id::WebViewId;
|
||||||
|
use constellation_traits::EmbedderToConstellationMessage;
|
||||||
|
use embedder_traits::{JSValue, JavaScriptEvaluationError, JavaScriptEvaluationId};
|
||||||
|
|
||||||
|
use crate::ConstellationProxy;
|
||||||
|
|
||||||
|
struct PendingEvaluation {
|
||||||
|
callback: Box<dyn FnOnce(Result<JSValue, JavaScriptEvaluationError>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct JavaScriptEvaluator {
|
||||||
|
current_id: JavaScriptEvaluationId,
|
||||||
|
constellation_proxy: ConstellationProxy,
|
||||||
|
pending_evaluations: HashMap<JavaScriptEvaluationId, PendingEvaluation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JavaScriptEvaluator {
|
||||||
|
pub(crate) fn new(constellation_proxy: ConstellationProxy) -> Self {
|
||||||
|
Self {
|
||||||
|
current_id: JavaScriptEvaluationId(0),
|
||||||
|
constellation_proxy,
|
||||||
|
pending_evaluations: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_id(&mut self) -> JavaScriptEvaluationId {
|
||||||
|
let next_id = JavaScriptEvaluationId(self.current_id.0 + 1);
|
||||||
|
std::mem::replace(&mut self.current_id, next_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn evaluate(
|
||||||
|
&mut self,
|
||||||
|
webview_id: WebViewId,
|
||||||
|
script: String,
|
||||||
|
callback: Box<dyn FnOnce(Result<JSValue, JavaScriptEvaluationError>)>,
|
||||||
|
) {
|
||||||
|
let evaluation_id = self.generate_id();
|
||||||
|
self.constellation_proxy
|
||||||
|
.send(EmbedderToConstellationMessage::EvaluateJavaScript(
|
||||||
|
webview_id,
|
||||||
|
evaluation_id,
|
||||||
|
script,
|
||||||
|
));
|
||||||
|
self.pending_evaluations
|
||||||
|
.insert(evaluation_id, PendingEvaluation { callback });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn finish_evaluation(
|
||||||
|
&mut self,
|
||||||
|
evaluation_id: JavaScriptEvaluationId,
|
||||||
|
result: Result<JSValue, JavaScriptEvaluationError>,
|
||||||
|
) {
|
||||||
|
(self
|
||||||
|
.pending_evaluations
|
||||||
|
.remove(&evaluation_id)
|
||||||
|
.expect("Received request to finish unknown JavaScript evaluation.")
|
||||||
|
.callback)(result)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@
|
||||||
//! `WindowMethods` trait.
|
//! `WindowMethods` trait.
|
||||||
|
|
||||||
mod clipboard_delegate;
|
mod clipboard_delegate;
|
||||||
|
mod javascript_evaluator;
|
||||||
mod proxies;
|
mod proxies;
|
||||||
mod responders;
|
mod responders;
|
||||||
mod servo_delegate;
|
mod servo_delegate;
|
||||||
|
@ -82,6 +83,7 @@ pub use gleam::gl;
|
||||||
use gleam::gl::RENDERER;
|
use gleam::gl::RENDERER;
|
||||||
use ipc_channel::ipc::{self, IpcSender};
|
use ipc_channel::ipc::{self, IpcSender};
|
||||||
use ipc_channel::router::ROUTER;
|
use ipc_channel::router::ROUTER;
|
||||||
|
use javascript_evaluator::JavaScriptEvaluator;
|
||||||
pub use keyboard_types::*;
|
pub use keyboard_types::*;
|
||||||
use layout::LayoutFactoryImpl;
|
use layout::LayoutFactoryImpl;
|
||||||
use log::{Log, Metadata, Record, debug, warn};
|
use log::{Log, Metadata, Record, debug, warn};
|
||||||
|
@ -196,6 +198,9 @@ pub struct Servo {
|
||||||
compositor: Rc<RefCell<IOCompositor>>,
|
compositor: Rc<RefCell<IOCompositor>>,
|
||||||
constellation_proxy: ConstellationProxy,
|
constellation_proxy: ConstellationProxy,
|
||||||
embedder_receiver: Receiver<EmbedderMsg>,
|
embedder_receiver: Receiver<EmbedderMsg>,
|
||||||
|
/// A struct that tracks ongoing JavaScript evaluations and is responsible for
|
||||||
|
/// calling the callback when the evaluation is complete.
|
||||||
|
javascript_evaluator: Rc<RefCell<JavaScriptEvaluator>>,
|
||||||
/// Tracks whether we are in the process of shutting down, or have shut down.
|
/// Tracks whether we are in the process of shutting down, or have shut down.
|
||||||
/// This is shared with `WebView`s and the `ServoRenderer`.
|
/// This is shared with `WebView`s and the `ServoRenderer`.
|
||||||
shutdown_state: Rc<Cell<ShutdownState>>,
|
shutdown_state: Rc<Cell<ShutdownState>>,
|
||||||
|
@ -487,10 +492,14 @@ impl Servo {
|
||||||
opts.debug.convert_mouse_to_touch,
|
opts.debug.convert_mouse_to_touch,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let constellation_proxy = ConstellationProxy::new(constellation_chan);
|
||||||
Self {
|
Self {
|
||||||
delegate: RefCell::new(Rc::new(DefaultServoDelegate)),
|
delegate: RefCell::new(Rc::new(DefaultServoDelegate)),
|
||||||
compositor: Rc::new(RefCell::new(compositor)),
|
compositor: Rc::new(RefCell::new(compositor)),
|
||||||
constellation_proxy: ConstellationProxy::new(constellation_chan),
|
javascript_evaluator: Rc::new(RefCell::new(JavaScriptEvaluator::new(
|
||||||
|
constellation_proxy.clone(),
|
||||||
|
))),
|
||||||
|
constellation_proxy,
|
||||||
embedder_receiver,
|
embedder_receiver,
|
||||||
shutdown_state,
|
shutdown_state,
|
||||||
webviews: Default::default(),
|
webviews: Default::default(),
|
||||||
|
@ -738,6 +747,11 @@ impl Servo {
|
||||||
webview.delegate().request_unload(webview, request);
|
webview.delegate().request_unload(webview, request);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
EmbedderMsg::FinishJavaScriptEvaluation(evaluation_id, result) => {
|
||||||
|
self.javascript_evaluator
|
||||||
|
.borrow_mut()
|
||||||
|
.finish_evaluation(evaluation_id, result);
|
||||||
|
},
|
||||||
EmbedderMsg::Keyboard(webview_id, keyboard_event) => {
|
EmbedderMsg::Keyboard(webview_id, keyboard_event) => {
|
||||||
if let Some(webview) = self.get_webview_handle(webview_id) {
|
if let Some(webview) = self.get_webview_handle(webview_id) {
|
||||||
webview
|
webview
|
||||||
|
|
|
@ -11,12 +11,14 @@
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use anyhow::ensure;
|
use anyhow::ensure;
|
||||||
use common::{ServoTest, run_api_tests};
|
use common::{ServoTest, run_api_tests};
|
||||||
use servo::{WebViewBuilder, WebViewDelegate};
|
use servo::{
|
||||||
|
JSValue, JavaScriptEvaluationError, LoadStatus, WebView, WebViewBuilder, WebViewDelegate,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct WebViewDelegateImpl {
|
struct WebViewDelegateImpl {
|
||||||
|
@ -44,6 +46,81 @@ fn test_create_webview(servo_test: &ServoTest) -> Result<(), anyhow::Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn evaluate_javascript(
|
||||||
|
servo_test: &ServoTest,
|
||||||
|
webview: WebView,
|
||||||
|
script: impl ToString,
|
||||||
|
) -> Result<JSValue, JavaScriptEvaluationError> {
|
||||||
|
let load_webview = webview.clone();
|
||||||
|
let _ = servo_test.spin(move || Ok(load_webview.load_status() != LoadStatus::Complete));
|
||||||
|
|
||||||
|
let saved_result = Rc::new(RefCell::new(None));
|
||||||
|
let callback_result = saved_result.clone();
|
||||||
|
webview.evaluate_javascript(script, move |result| {
|
||||||
|
*callback_result.borrow_mut() = Some(result)
|
||||||
|
});
|
||||||
|
|
||||||
|
let spin_result = saved_result.clone();
|
||||||
|
let _ = servo_test.spin(move || Ok(spin_result.borrow().is_none()));
|
||||||
|
|
||||||
|
(*saved_result.borrow())
|
||||||
|
.clone()
|
||||||
|
.expect("Should have waited until value available")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_evaluate_javascript_basic(servo_test: &ServoTest) -> Result<(), anyhow::Error> {
|
||||||
|
let delegate = Rc::new(WebViewDelegateImpl::default());
|
||||||
|
let webview = WebViewBuilder::new(servo_test.servo())
|
||||||
|
.delegate(delegate.clone())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let result = evaluate_javascript(servo_test, webview.clone(), "undefined");
|
||||||
|
ensure!(result == Ok(JSValue::Undefined));
|
||||||
|
|
||||||
|
let result = evaluate_javascript(servo_test, webview.clone(), "null");
|
||||||
|
ensure!(result == Ok(JSValue::Null));
|
||||||
|
|
||||||
|
let result = evaluate_javascript(servo_test, webview.clone(), "42");
|
||||||
|
ensure!(result == Ok(JSValue::Number(42.0)));
|
||||||
|
|
||||||
|
let result = evaluate_javascript(servo_test, webview.clone(), "3 + 4");
|
||||||
|
ensure!(result == Ok(JSValue::Number(7.0)));
|
||||||
|
|
||||||
|
let result = evaluate_javascript(servo_test, webview.clone(), "'abc' + 'def'");
|
||||||
|
ensure!(result == Ok(JSValue::String("abcdef".into())));
|
||||||
|
|
||||||
|
let result = evaluate_javascript(servo_test, webview.clone(), "let foo = {blah: 123}; foo");
|
||||||
|
ensure!(matches!(result, Ok(JSValue::Object(_))));
|
||||||
|
if let Ok(JSValue::Object(values)) = result {
|
||||||
|
ensure!(values.len() == 1);
|
||||||
|
ensure!(values.get("blah") == Some(&JSValue::Number(123.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = evaluate_javascript(servo_test, webview.clone(), "[1, 2, 3, 4]");
|
||||||
|
let expected = JSValue::Array(vec![
|
||||||
|
JSValue::Number(1.0),
|
||||||
|
JSValue::Number(2.0),
|
||||||
|
JSValue::Number(3.0),
|
||||||
|
JSValue::Number(4.0),
|
||||||
|
]);
|
||||||
|
ensure!(result == Ok(expected));
|
||||||
|
|
||||||
|
let result = evaluate_javascript(servo_test, webview.clone(), "window");
|
||||||
|
ensure!(matches!(result, Ok(JSValue::Window(..))));
|
||||||
|
|
||||||
|
let result = evaluate_javascript(servo_test, webview.clone(), "document.body");
|
||||||
|
ensure!(matches!(result, Ok(JSValue::Element(..))));
|
||||||
|
|
||||||
|
let result = evaluate_javascript(
|
||||||
|
servo_test,
|
||||||
|
webview.clone(),
|
||||||
|
"document.body.innerHTML += '<iframe>'; frames[0]",
|
||||||
|
);
|
||||||
|
ensure!(matches!(result, Ok(JSValue::Frame(..))));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn test_create_webview_and_immediately_drop_webview_before_shutdown(
|
fn test_create_webview_and_immediately_drop_webview_before_shutdown(
|
||||||
servo_test: &ServoTest,
|
servo_test: &ServoTest,
|
||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
|
@ -54,6 +131,7 @@ fn test_create_webview_and_immediately_drop_webview_before_shutdown(
|
||||||
fn main() {
|
fn main() {
|
||||||
run_api_tests!(
|
run_api_tests!(
|
||||||
test_create_webview,
|
test_create_webview,
|
||||||
|
test_evaluate_javascript_basic,
|
||||||
// This test needs to be last, as it tests creating and dropping
|
// This test needs to be last, as it tests creating and dropping
|
||||||
// a WebView right before shutdown.
|
// a WebView right before shutdown.
|
||||||
test_create_webview_and_immediately_drop_webview_before_shutdown
|
test_create_webview_and_immediately_drop_webview_before_shutdown
|
||||||
|
|
|
@ -13,8 +13,8 @@ use compositing_traits::WebViewTrait;
|
||||||
use constellation_traits::{EmbedderToConstellationMessage, TraversalDirection};
|
use constellation_traits::{EmbedderToConstellationMessage, TraversalDirection};
|
||||||
use dpi::PhysicalSize;
|
use dpi::PhysicalSize;
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
Cursor, InputEvent, LoadStatus, MediaSessionActionType, ScreenGeometry, Theme, TouchEventType,
|
Cursor, InputEvent, JSValue, JavaScriptEvaluationError, LoadStatus, MediaSessionActionType,
|
||||||
ViewportDetails,
|
ScreenGeometry, Theme, TouchEventType, ViewportDetails,
|
||||||
};
|
};
|
||||||
use euclid::{Point2D, Scale, Size2D};
|
use euclid::{Point2D, Scale, Size2D};
|
||||||
use servo_geometry::DeviceIndependentPixel;
|
use servo_geometry::DeviceIndependentPixel;
|
||||||
|
@ -23,6 +23,7 @@ use webrender_api::ScrollLocation;
|
||||||
use webrender_api::units::{DeviceIntPoint, DevicePixel, DeviceRect};
|
use webrender_api::units::{DeviceIntPoint, DevicePixel, DeviceRect};
|
||||||
|
|
||||||
use crate::clipboard_delegate::{ClipboardDelegate, DefaultClipboardDelegate};
|
use crate::clipboard_delegate::{ClipboardDelegate, DefaultClipboardDelegate};
|
||||||
|
use crate::javascript_evaluator::JavaScriptEvaluator;
|
||||||
use crate::webview_delegate::{DefaultWebViewDelegate, WebViewDelegate};
|
use crate::webview_delegate::{DefaultWebViewDelegate, WebViewDelegate};
|
||||||
use crate::{ConstellationProxy, Servo, WebRenderDebugOption};
|
use crate::{ConstellationProxy, Servo, WebRenderDebugOption};
|
||||||
|
|
||||||
|
@ -75,6 +76,7 @@ pub(crate) struct WebViewInner {
|
||||||
pub(crate) compositor: Rc<RefCell<IOCompositor>>,
|
pub(crate) compositor: Rc<RefCell<IOCompositor>>,
|
||||||
pub(crate) delegate: Rc<dyn WebViewDelegate>,
|
pub(crate) delegate: Rc<dyn WebViewDelegate>,
|
||||||
pub(crate) clipboard_delegate: Rc<dyn ClipboardDelegate>,
|
pub(crate) clipboard_delegate: Rc<dyn ClipboardDelegate>,
|
||||||
|
javascript_evaluator: Rc<RefCell<JavaScriptEvaluator>>,
|
||||||
|
|
||||||
rect: DeviceRect,
|
rect: DeviceRect,
|
||||||
hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
|
hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
|
||||||
|
@ -117,9 +119,10 @@ impl WebView {
|
||||||
compositor: servo.compositor.clone(),
|
compositor: servo.compositor.clone(),
|
||||||
delegate: builder.delegate,
|
delegate: builder.delegate,
|
||||||
clipboard_delegate: Rc::new(DefaultClipboardDelegate),
|
clipboard_delegate: Rc::new(DefaultClipboardDelegate),
|
||||||
|
javascript_evaluator: servo.javascript_evaluator.clone(),
|
||||||
rect: DeviceRect::from_origin_and_size(Point2D::origin(), size),
|
rect: DeviceRect::from_origin_and_size(Point2D::origin(), size),
|
||||||
hidpi_scale_factor: builder.hidpi_scale_factor,
|
hidpi_scale_factor: builder.hidpi_scale_factor,
|
||||||
load_status: LoadStatus::Complete,
|
load_status: LoadStatus::Started,
|
||||||
url: None,
|
url: None,
|
||||||
status_text: None,
|
status_text: None,
|
||||||
page_title: None,
|
page_title: None,
|
||||||
|
@ -549,6 +552,20 @@ impl WebView {
|
||||||
pub fn paint(&self) -> bool {
|
pub fn paint(&self) -> bool {
|
||||||
self.inner().compositor.borrow_mut().render()
|
self.inner().compositor.borrow_mut().render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Evaluate the specified string of JavaScript code. Once execution is complete or an error
|
||||||
|
/// occurs, Servo will call `callback`.
|
||||||
|
pub fn evaluate_javascript<T: ToString>(
|
||||||
|
&self,
|
||||||
|
script: T,
|
||||||
|
callback: impl FnOnce(Result<JSValue, JavaScriptEvaluationError>) + 'static,
|
||||||
|
) {
|
||||||
|
self.inner().javascript_evaluator.borrow_mut().evaluate(
|
||||||
|
self.id(),
|
||||||
|
script.to_string(),
|
||||||
|
Box::new(callback),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A structure used to expose a view of the [`WebView`] to the Servo
|
/// A structure used to expose a view of the [`WebView`] to the Servo
|
||||||
|
|
|
@ -15,8 +15,8 @@ use base::id::{
|
||||||
use canvas_traits::canvas::{CanvasId, CanvasMsg};
|
use canvas_traits::canvas::{CanvasId, CanvasMsg};
|
||||||
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId};
|
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId};
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
AnimationState, EmbedderMsg, FocusSequenceNumber, MediaSessionEvent, TouchEventResult,
|
AnimationState, EmbedderMsg, FocusSequenceNumber, JSValue, JavaScriptEvaluationError,
|
||||||
ViewportDetails,
|
JavaScriptEvaluationId, MediaSessionEvent, TouchEventResult, ViewportDetails,
|
||||||
};
|
};
|
||||||
use euclid::default::Size2D as UntypedSize2D;
|
use euclid::default::Size2D as UntypedSize2D;
|
||||||
use http::{HeaderMap, Method};
|
use http::{HeaderMap, Method};
|
||||||
|
@ -644,6 +644,11 @@ pub enum ScriptToConstellationMessage {
|
||||||
IFrameSizes(Vec<IFrameSizeMsg>),
|
IFrameSizes(Vec<IFrameSizeMsg>),
|
||||||
/// Request results from the memory reporter.
|
/// Request results from the memory reporter.
|
||||||
ReportMemory(IpcSender<MemoryReportResult>),
|
ReportMemory(IpcSender<MemoryReportResult>),
|
||||||
|
/// Return the result of the evaluated JavaScript with the given [`JavaScriptEvaluationId`].
|
||||||
|
FinishJavaScriptEvaluation(
|
||||||
|
JavaScriptEvaluationId,
|
||||||
|
Result<JSValue, JavaScriptEvaluationError>,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for ScriptToConstellationMessage {
|
impl fmt::Debug for ScriptToConstellationMessage {
|
||||||
|
|
|
@ -19,8 +19,8 @@ use base::Epoch;
|
||||||
use base::cross_process_instant::CrossProcessInstant;
|
use base::cross_process_instant::CrossProcessInstant;
|
||||||
use base::id::{MessagePortId, PipelineId, WebViewId};
|
use base::id::{MessagePortId, PipelineId, WebViewId};
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
CompositorHitTestResult, Cursor, InputEvent, MediaSessionActionType, Theme, ViewportDetails,
|
CompositorHitTestResult, Cursor, InputEvent, JavaScriptEvaluationId, MediaSessionActionType,
|
||||||
WebDriverCommandMsg,
|
Theme, ViewportDetails, WebDriverCommandMsg,
|
||||||
};
|
};
|
||||||
use euclid::Vector2D;
|
use euclid::Vector2D;
|
||||||
pub use from_script_message::*;
|
pub use from_script_message::*;
|
||||||
|
@ -92,6 +92,9 @@ pub enum EmbedderToConstellationMessage {
|
||||||
SetScrollStates(PipelineId, Vec<ScrollState>),
|
SetScrollStates(PipelineId, Vec<ScrollState>),
|
||||||
/// Notify the constellation that a particular paint metric event has happened for the given pipeline.
|
/// Notify the constellation that a particular paint metric event has happened for the given pipeline.
|
||||||
PaintMetric(PipelineId, PaintMetricEvent),
|
PaintMetric(PipelineId, PaintMetricEvent),
|
||||||
|
/// Evaluate a JavaScript string in the context of a `WebView`. When execution is complete or an
|
||||||
|
/// error is encountered, a correpsonding message will be sent to the embedding layer.
|
||||||
|
EvaluateJavaScript(WebViewId, JavaScriptEvaluationId, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A description of a paint metric that is sent from the Servo renderer to the
|
/// A description of a paint metric that is sent from the Servo renderer to the
|
||||||
|
|
|
@ -13,8 +13,10 @@ pub mod resources;
|
||||||
pub mod user_content_manager;
|
pub mod user_content_manager;
|
||||||
mod webdriver;
|
mod webdriver;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::fmt::{Debug, Display, Error, Formatter};
|
use std::fmt::{Debug, Display, Error, Formatter};
|
||||||
|
use std::hash::Hash;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -372,6 +374,12 @@ pub enum EmbedderMsg {
|
||||||
DeviceIntRect,
|
DeviceIntRect,
|
||||||
IpcSender<Option<usize>>,
|
IpcSender<Option<usize>>,
|
||||||
),
|
),
|
||||||
|
/// Inform the embedding layer that a JavaScript evaluation has
|
||||||
|
/// finished with the given result.
|
||||||
|
FinishJavaScriptEvaluation(
|
||||||
|
JavaScriptEvaluationId,
|
||||||
|
Result<JSValue, JavaScriptEvaluationError>,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for EmbedderMsg {
|
impl Debug for EmbedderMsg {
|
||||||
|
@ -857,3 +865,59 @@ impl Display for FocusSequenceNumber {
|
||||||
Display::fmt(&self.0, f)
|
Display::fmt(&self.0, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An identifier for a particular JavaScript evaluation that is used to track the
|
||||||
|
/// evaluation from the embedding layer to the script layer and then back.
|
||||||
|
#[derive(Clone, Copy, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
pub struct JavaScriptEvaluationId(pub usize);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
|
pub enum JSValue {
|
||||||
|
Undefined,
|
||||||
|
Null,
|
||||||
|
Boolean(bool),
|
||||||
|
Number(f64),
|
||||||
|
String(String),
|
||||||
|
Element(String),
|
||||||
|
Frame(String),
|
||||||
|
Window(String),
|
||||||
|
Array(Vec<JSValue>),
|
||||||
|
Object(HashMap<String, JSValue>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&WebDriverJSValue> for JSValue {
|
||||||
|
fn from(value: &WebDriverJSValue) -> Self {
|
||||||
|
match value {
|
||||||
|
WebDriverJSValue::Undefined => Self::Undefined,
|
||||||
|
WebDriverJSValue::Null => Self::Null,
|
||||||
|
WebDriverJSValue::Boolean(value) => Self::Boolean(*value),
|
||||||
|
WebDriverJSValue::Int(value) => Self::Number(*value as f64),
|
||||||
|
WebDriverJSValue::Number(value) => Self::Number(*value),
|
||||||
|
WebDriverJSValue::String(value) => Self::String(value.clone()),
|
||||||
|
WebDriverJSValue::Element(web_element) => Self::Element(web_element.0.clone()),
|
||||||
|
WebDriverJSValue::Frame(web_frame) => Self::Frame(web_frame.0.clone()),
|
||||||
|
WebDriverJSValue::Window(web_window) => Self::Window(web_window.0.clone()),
|
||||||
|
WebDriverJSValue::ArrayLike(vector) => {
|
||||||
|
Self::Array(vector.iter().map(Into::into).collect())
|
||||||
|
},
|
||||||
|
WebDriverJSValue::Object(map) => Self::Object(
|
||||||
|
map.iter()
|
||||||
|
.map(|(key, value)| (key.clone(), value.into()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
|
pub enum JavaScriptEvaluationError {
|
||||||
|
/// An internal Servo error prevented the JavaSript evaluation from completing properly.
|
||||||
|
/// This indicates a bug in Servo.
|
||||||
|
InternalError,
|
||||||
|
/// The `WebView` on which this evaluation request was triggered is not ready. This might
|
||||||
|
/// happen if the `WebView`'s `Document` is changing due to ongoing load events, for instance.
|
||||||
|
WebViewNotReady,
|
||||||
|
/// The script executed successfully, but Servo could not serialize the JavaScript return
|
||||||
|
/// value into a [`JSValue`].
|
||||||
|
SerializationError,
|
||||||
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ use crossbeam_channel::{RecvTimeoutError, Sender};
|
||||||
use devtools_traits::ScriptToDevtoolsControlMsg;
|
use devtools_traits::ScriptToDevtoolsControlMsg;
|
||||||
use embedder_traits::user_content_manager::UserContentManager;
|
use embedder_traits::user_content_manager::UserContentManager;
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
CompositorHitTestResult, FocusSequenceNumber, InputEvent, MediaSessionActionType, Theme,
|
CompositorHitTestResult, FocusSequenceNumber, InputEvent, JavaScriptEvaluationId,
|
||||||
ViewportDetails, WebDriverScriptCommand,
|
MediaSessionActionType, Theme, ViewportDetails, WebDriverScriptCommand,
|
||||||
};
|
};
|
||||||
use euclid::{Rect, Scale, Size2D, UnknownUnit};
|
use euclid::{Rect, Scale, Size2D, UnknownUnit};
|
||||||
use ipc_channel::ipc::{IpcReceiver, IpcSender};
|
use ipc_channel::ipc::{IpcReceiver, IpcSender};
|
||||||
|
@ -245,6 +245,9 @@ pub enum ScriptThreadMessage {
|
||||||
/// The compositor scrolled and is updating the scroll states of the nodes in the given
|
/// The compositor scrolled and is updating the scroll states of the nodes in the given
|
||||||
/// pipeline via the Constellation.
|
/// pipeline via the Constellation.
|
||||||
SetScrollStates(PipelineId, Vec<ScrollState>),
|
SetScrollStates(PipelineId, Vec<ScrollState>),
|
||||||
|
/// Evaluate the given JavaScript and return a result via a corresponding message
|
||||||
|
/// to the Constellation.
|
||||||
|
EvaluateJavaScript(PipelineId, JavaScriptEvaluationId, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for ScriptThreadMessage {
|
impl fmt::Debug for ScriptThreadMessage {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue