From b86965f3948cb830b084a53133ec949af0d11ff7 Mon Sep 17 00:00:00 2001 From: Ms2ger Date: Mon, 28 Nov 2016 10:23:04 +0100 Subject: [PATCH] Implement synchronous about:blank loading. Based on initial work by jdm in . --- components/constellation/constellation.rs | 77 ++- components/constellation/pipeline.rs | 24 +- components/script/dom/htmliframeelement.rs | 148 ++-- components/script/script_thread.rs | 43 +- components/script_traits/lib.rs | 21 +- components/script_traits/script_msg.rs | 6 +- .../xhtml1print/cssimportrule.xht.ini | 6 + .../es-exceptions/exceptions.html.ini | 18 +- .../dom/nodes/Comment-constructor.html.ini | 5 - .../dom/nodes/Text-constructor.html.ini | 5 - .../navigate-child-function-parent.html.ini | 4 +- .../navigate-child-src-about-blank.html.ini | 4 +- .../cross-origin-objects.html.ini | 2 +- .../browsing-context-first-created.xhtml.ini | 6 - .../window-top-001.html.ini | 8 - .../wpt/metadata/html/dom/interfaces.html.ini | 639 ------------------ ...lection-invoke-insert-into-iframe.html.ini | 5 - .../pause-move-to-other-document.html.ini | 5 - .../iframe-append-to-child-document.html.ini | 4 +- .../iframe-load-event.html.ini | 3 - .../move_iframe_in_dom_01.html.ini | 3 - .../viewport-change.html.ini | 46 +- .../sizes/parse-a-sizes-attribute.html.ini | 218 ++++-- .../node-document-changes.html.ini | 6 +- ...nsupported-csp-referrer-directive.html.ini | 1 - .../metadata/webstorage/event_basic.html.ini | 1 - .../webstorage/event_body_attribute.html.ini | 9 - .../webstorage/event_case_sensitive.html.ini | 1 - .../webstorage/event_setattribute.html.ini | 9 - .../mozilla/htmllabel-activation.html.ini | 1 - .../mozbrowser/mozbrowser_loadevents.html.ini | 5 + .../mozilla/mozbrowser/iframe_goback.html | 6 +- .../mozbrowserlocationchange_event.html | 11 +- .../tests/mozilla/mozbrowser/redirect.html | 12 +- 34 files changed, 456 insertions(+), 906 deletions(-) delete mode 100644 tests/wpt/metadata/dom/nodes/Comment-constructor.html.ini delete mode 100644 tests/wpt/metadata/dom/nodes/Text-constructor.html.ini delete mode 100644 tests/wpt/metadata/html/browsers/windows/nested-browsing-contexts/window-top-001.html.ini delete mode 100644 tests/wpt/metadata/html/semantics/embedded-content/media-elements/loading-the-media-resource/resource-selection-invoke-insert-into-iframe.html.ini delete mode 100644 tests/wpt/metadata/html/semantics/embedded-content/media-elements/playing-the-media-resource/pause-move-to-other-document.html.ini delete mode 100644 tests/wpt/metadata/html/semantics/embedded-content/the-iframe-element/move_iframe_in_dom_01.html.ini delete mode 100644 tests/wpt/metadata/webstorage/event_body_attribute.html.ini delete mode 100644 tests/wpt/metadata/webstorage/event_setattribute.html.ini create mode 100644 tests/wpt/mozilla/meta/mozilla/mozbrowser/mozbrowser_loadevents.html.ini diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 8cb82a5cdd5..19e70c90bfc 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -42,7 +42,7 @@ use rand::{Rng, SeedableRng, StdRng, random}; use script_traits::{AnimationState, AnimationTickType, CompositorEvent}; use script_traits::{ConstellationControlMsg, ConstellationMsg as FromCompositorMsg}; use script_traits::{DocumentState, LayoutControlMsg, LoadData}; -use script_traits::{IFrameLoadInfo, IFrameSandboxState, TimerEventRequest}; +use script_traits::{IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSandboxState, TimerEventRequest}; use script_traits::{LayoutMsg as FromLayoutMsg, ScriptMsg as FromScriptMsg, ScriptThreadFactory}; use script_traits::{LogEntry, ServiceWorkerMsg, webdriver_msg}; use script_traits::{MozBrowserErrorType, MozBrowserEvent, WebDriverCommandMsg, WindowSizeData}; @@ -914,11 +914,17 @@ impl Constellation } FromScriptMsg::ScriptLoadedURLInIFrame(load_info) => { debug!("constellation got iframe URL load message {:?} {:?} {:?}", - load_info.parent_pipeline_id, + load_info.info.parent_pipeline_id, load_info.old_pipeline_id, - load_info.new_pipeline_id); + load_info.info.new_pipeline_id); self.handle_script_loaded_url_in_iframe_msg(load_info); } + FromScriptMsg::ScriptLoadedAboutBlankInIFrame(load_info, lc) => { + debug!("constellation got loaded `about:blank` in iframe message {:?} {:?}", + load_info.parent_pipeline_id, + load_info.new_pipeline_id); + self.handle_script_loaded_about_blank_in_iframe_msg(load_info, lc); + } FromScriptMsg::ChangeRunningAnimationsState(pipeline_id, animation_state) => { self.handle_change_running_animations_state(pipeline_id, animation_state) } @@ -1363,14 +1369,14 @@ impl Constellation // will result in a new pipeline being spawned and a frame tree being added to // parent_pipeline_id's frame tree's children. This message is never the result of a // page navigation. - fn handle_script_loaded_url_in_iframe_msg(&mut self, load_info: IFrameLoadInfo) { + fn handle_script_loaded_url_in_iframe_msg(&mut self, load_info: IFrameLoadInfoWithData) { let (load_data, window_size, is_private) = { let old_pipeline = load_info.old_pipeline_id .and_then(|old_pipeline_id| self.pipelines.get(&old_pipeline_id)); - let source_pipeline = match self.pipelines.get(&load_info.parent_pipeline_id) { + let source_pipeline = match self.pipelines.get(&load_info.info.parent_pipeline_id) { Some(source_pipeline) => source_pipeline, - None => return warn!("Script loaded url in closed iframe {}.", load_info.parent_pipeline_id), + None => return warn!("Script loaded url in closed iframe {}.", load_info.info.parent_pipeline_id), }; // If no url is specified, reload. @@ -1384,7 +1390,7 @@ impl Constellation LoadData::new(url, None, None) }); - let is_private = load_info.is_private || source_pipeline.is_private; + let is_private = load_info.info.is_private || source_pipeline.is_private; let window_size = old_pipeline.and_then(|old_pipeline| old_pipeline.size); @@ -1396,20 +1402,65 @@ impl Constellation }; // Create the new pipeline, attached to the parent and push to pending frames - self.new_pipeline(load_info.new_pipeline_id, - load_info.frame_id, - Some((load_info.parent_pipeline_id, load_info.frame_type)), + self.new_pipeline(load_info.info.new_pipeline_id, + load_info.info.frame_id, + Some((load_info.info.parent_pipeline_id, load_info.info.frame_type)), window_size, load_data, load_info.sandbox, is_private); self.pending_frames.push(FrameChange { - frame_id: load_info.frame_id, + frame_id: load_info.info.frame_id, old_pipeline_id: load_info.old_pipeline_id, - new_pipeline_id: load_info.new_pipeline_id, + new_pipeline_id: load_info.info.new_pipeline_id, document_ready: false, - replace: load_info.replace, + replace: load_info.info.replace, + }); + } + + fn handle_script_loaded_about_blank_in_iframe_msg(&mut self, + load_info: IFrameLoadInfo, + layout_sender: IpcSender) { + let IFrameLoadInfo { + parent_pipeline_id, + new_pipeline_id, + frame_type, + replace, + frame_id, + is_private, + } = load_info; + + let pipeline = { + let parent_pipeline = match self.pipelines.get(&parent_pipeline_id) { + Some(parent_pipeline) => parent_pipeline, + None => return warn!("Script loaded url in closed iframe {}.", parent_pipeline_id), + }; + + let script_sender = parent_pipeline.script_chan.clone(); + + let url = ServoUrl::parse("about:blank").expect("infallible"); + Pipeline::new(new_pipeline_id, + frame_id, + Some((parent_pipeline_id, frame_type)), + script_sender, + layout_sender, + self.compositor_proxy.clone_compositor_proxy(), + is_private || parent_pipeline.is_private, + url, + None, + parent_pipeline.visible) + }; + + assert!(!self.pipelines.contains_key(&new_pipeline_id)); + self.pipelines.insert(new_pipeline_id, pipeline); + + self.pending_frames.push(FrameChange { + frame_id: frame_id, + old_pipeline_id: None, + new_pipeline_id: new_pipeline_id, + document_ready: false, + replace: replace, }); } diff --git a/components/constellation/pipeline.rs b/components/constellation/pipeline.rs index 53c2a58e091..890476e5010 100644 --- a/components/constellation/pipeline.rs +++ b/components/constellation/pipeline.rs @@ -256,17 +256,19 @@ impl Pipeline { Ok((pipeline, child_process)) } - fn new(id: PipelineId, - frame_id: FrameId, - parent_info: Option<(PipelineId, FrameType)>, - script_chan: Rc, - layout_chan: IpcSender, - compositor_proxy: Box, - is_private: bool, - url: ServoUrl, - size: Option>, - visible: bool) - -> Pipeline { + /// Creates a new `Pipeline`, after the script and layout threads have been + /// spawned. + pub fn new(id: PipelineId, + frame_id: FrameId, + parent_info: Option<(PipelineId, FrameType)>, + script_chan: Rc, + layout_chan: IpcSender, + compositor_proxy: Box, + is_private: bool, + url: ServoUrl, + size: Option>, + visible: bool) + -> Pipeline { let pipeline = Pipeline { id: id, frame_id: frame_id, diff --git a/components/script/dom/htmliframeelement.rs b/components/script/dom/htmliframeelement.rs index a31baa9f045..c322b96a555 100644 --- a/components/script/dom/htmliframeelement.rs +++ b/components/script/dom/htmliframeelement.rs @@ -20,6 +20,7 @@ use dom::bindings::conversions::ToJSValConvertible; use dom::bindings::error::{Error, ErrorResult, Fallible}; use dom::bindings::inheritance::Castable; use dom::bindings::js::{JS, LayoutJS, MutNullableHeap, Root}; +use dom::bindings::refcounted::Trusted; use dom::bindings::reflector::Reflectable; use dom::bindings::str::DOMString; use dom::browsingcontext::BrowsingContext; @@ -42,14 +43,16 @@ use js::jsval::{NullValue, UndefinedValue}; use msg::constellation_msg::{FrameType, FrameId, PipelineId, TraversalDirection}; use net_traits::response::HttpsState; use script_layout_interface::message::ReflowQueryType; -use script_thread::ScriptThread; -use script_traits::{IFrameLoadInfo, LoadData, MozBrowserEvent, ScriptMsg as ConstellationMsg}; +use script_thread::{ScriptThread, Runnable}; +use script_traits::{IFrameLoadInfo, IFrameLoadInfoWithData, LoadData}; +use script_traits::{MozBrowserEvent, NewLayoutInfo, ScriptMsg as ConstellationMsg}; use script_traits::IFrameSandboxState::{IFrameSandboxed, IFrameUnsandboxed}; use servo_atoms::Atom; use servo_url::ServoUrl; use std::cell::Cell; use style::attr::{AttrValue, LengthOrPercentageOrAuto}; use style::context::ReflowGoal; +use task_source::TaskSource; use util::prefs::PREFS; use util::servo_version; @@ -66,6 +69,12 @@ bitflags! { } } +#[derive(PartialEq)] +enum ProcessingMode { + FirstTime, + NotFirstTime, +} + #[dom_struct] pub struct HTMLIFrameElement { htmlelement: HTMLElement, @@ -131,20 +140,46 @@ impl HTMLIFrameElement { let global_scope = window.upcast::(); let load_info = IFrameLoadInfo { - load_data: load_data, parent_pipeline_id: global_scope.pipeline_id(), frame_id: self.frame_id, - old_pipeline_id: old_pipeline_id, new_pipeline_id: new_pipeline_id, - sandbox: sandboxed, is_private: private_iframe, frame_type: frame_type, replace: replace, }; - global_scope - .constellation_chan() - .send(ConstellationMsg::ScriptLoadedURLInIFrame(load_info)) - .unwrap(); + + if load_data.as_ref().map_or(false, |d| d.url.as_str() == "about:blank") { + let (pipeline_sender, pipeline_receiver) = ipc::channel().unwrap(); + + global_scope + .constellation_chan() + .send(ConstellationMsg::ScriptLoadedAboutBlankInIFrame(load_info, pipeline_sender)) + .unwrap(); + + let new_layout_info = NewLayoutInfo { + parent_info: Some((global_scope.pipeline_id(), frame_type)), + new_pipeline_id: new_pipeline_id, + frame_id: self.frame_id, + load_data: load_data.unwrap(), + pipeline_port: pipeline_receiver, + content_process_shutdown_chan: None, + window_size: None, + layout_threads: PREFS.get("layout.threads").as_u64().expect("count") as usize, + }; + + ScriptThread::process_attach_layout(new_layout_info); + } else { + let load_info = IFrameLoadInfoWithData { + info: load_info, + load_data: load_data, + old_pipeline_id: old_pipeline_id, + sandbox: sandboxed, + }; + global_scope + .constellation_chan() + .send(ConstellationMsg::ScriptLoadedURLInIFrame(load_info)) + .unwrap(); + } if PREFS.is_mozbrowser_enabled() { // https://developer.mozilla.org/en-US/docs/Web/Events/mozbrowserloadstart @@ -152,9 +187,23 @@ impl HTMLIFrameElement { } } - pub fn process_the_iframe_attributes(&self) { + /// https://html.spec.whatwg.org/multipage/#process-the-iframe-attributes + fn process_the_iframe_attributes(&self, mode: ProcessingMode) { + // TODO: srcdoc + + // https://github.com/whatwg/html/issues/490 + if mode == ProcessingMode::FirstTime && !self.upcast::().has_attribute(&local_name!("src")) { + let window = window_from_node(self); + let event_loop = window.dom_manipulation_task_source(); + let _ = event_loop.queue(box IframeLoadEventSteps::new(self), + window.upcast()); + return; + } + let url = self.get_url(); + // TODO: check ancestor browsing contexts for same URL + let document = document_from_node(self); self.navigate_or_reload_child_browsing_context( Some(LoadData::new(url, document.get_referrer_policy(), Some(document.url()))), false); @@ -171,6 +220,16 @@ impl HTMLIFrameElement { } } + fn create_nested_browsing_context(&self) { + // Synchronously create a new context and navigate it to about:blank. + let url = ServoUrl::parse("about:blank").unwrap(); + let document = document_from_node(self); + let load_data = LoadData::new(url, + document.get_referrer_policy(), + Some(document.url().clone())); + self.navigate_or_reload_child_browsing_context(Some(load_data), false); + } + pub fn update_pipeline_id(&self, new_pipeline_id: PipelineId) { self.pipeline_id.set(Some(new_pipeline_id)); @@ -272,7 +331,11 @@ impl HTMLIFrameElement { self.pipeline_id.get() .and_then(|pipeline_id| ScriptThread::find_document(pipeline_id)) .and_then(|document| { - if self.global().get_url().origin() == document.global().get_url().origin() { + // FIXME(#10964): this should use the Document's origin and the + // origin of the incumbent settings object. + let contained_url = document.global().get_url(); + if self.global().get_url().origin() == contained_url.origin() || + contained_url.as_str() == "about:blank" { Some(Root::from_ref(document.window())) } else { None @@ -458,18 +521,7 @@ impl HTMLIFrameElementMethods for HTMLIFrameElement { // https://html.spec.whatwg.org/multipage/#dom-iframe-contentdocument fn GetContentDocument(&self) -> Option> { - self.get_content_window().and_then(|window| { - // FIXME(#10964): this should use the Document's origin and the - // origin of the incumbent settings object. - let self_url = self.get_url(); - let win_url = window_from_node(self).get_url(); - - if UrlHelper::SameOrigin(&self_url, &win_url) { - Some(window.Document()) - } else { - None - } - }) + self.get_content_window().map(|window| window.Document()) } // Experimental mozbrowser implementation is based on the webidl @@ -601,19 +653,17 @@ impl VirtualMethods for HTMLIFrameElement { })); }, &local_name!("src") => { - if let AttributeMutation::Set(_) = mutation { - // https://html.spec.whatwg.org/multipage/#the-iframe-element - // "Similarly, whenever an iframe element with a non-null nested browsing context - // but with no srcdoc attribute specified has its src attribute set, changed, or removed, - // the user agent must process the iframe attributes," - // but we can't check that directly, since the child browsing context - // may be in a different script thread. Instread, we check to see if the parent - // is in a document tree and has a browsing context, which is what causes - // the child browsing context to be created. - if self.upcast::().is_in_doc_with_browsing_context() { - debug!("iframe {} src set while in browsing context.", self.frame_id); - self.process_the_iframe_attributes(); - } + // https://html.spec.whatwg.org/multipage/#the-iframe-element + // "Similarly, whenever an iframe element with a non-null nested browsing context + // but with no srcdoc attribute specified has its src attribute set, changed, or removed, + // the user agent must process the iframe attributes," + // but we can't check that directly, since the child browsing context + // may be in a different script thread. Instread, we check to see if the parent + // is in a document tree and has a browsing context, which is what causes + // the child browsing context to be created. + if self.upcast::().is_in_doc_with_browsing_context() { + debug!("iframe {} src set while in browsing context.", self.frame_id); + self.process_the_iframe_attributes(ProcessingMode::NotFirstTime); } }, _ => {}, @@ -642,7 +692,8 @@ impl VirtualMethods for HTMLIFrameElement { // iframe attributes for the "first time"." if self.upcast::().is_in_doc_with_browsing_context() { debug!("iframe {} bound to browsing context.", self.frame_id); - self.process_the_iframe_attributes(); + self.create_nested_browsing_context(); + self.process_the_iframe_attributes(ProcessingMode::FirstTime); } } @@ -667,7 +718,7 @@ impl VirtualMethods for HTMLIFrameElement { // HTMLIFrameElement::contentDocument. let self_url = self.get_url(); let win_url = window_from_node(self).get_url(); - UrlHelper::SameOrigin(&self_url, &win_url) + UrlHelper::SameOrigin(&self_url, &win_url) || self_url.as_str() == "about:blank" }; let (sender, receiver) = if same_origin { (None, None) @@ -690,3 +741,24 @@ impl VirtualMethods for HTMLIFrameElement { } } } + +struct IframeLoadEventSteps { + frame_element: Trusted, + pipeline_id: PipelineId, +} + +impl IframeLoadEventSteps { + fn new(frame_element: &HTMLIFrameElement) -> IframeLoadEventSteps { + IframeLoadEventSteps { + frame_element: Trusted::new(frame_element), + pipeline_id: frame_element.pipeline_id().unwrap(), + } + } +} + +impl Runnable for IframeLoadEventSteps { + fn handler(self: Box) { + let this = self.frame_element.root(); + this.iframe_load_event_steps(self.pipeline_id); + } +} diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index e7a1f95ec9d..140d57ec754 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -71,7 +71,8 @@ use js::rust::Runtime; use layout_wrapper::ServoLayoutNode; use mem::heap_size_of_self_and_children; use msg::constellation_msg::{FrameId, FrameType, PipelineId, PipelineNamespace}; -use net_traits::{CoreResourceMsg, IpcSend, Metadata, ReferrerPolicy, ResourceThreads}; +use net_traits::{CoreResourceMsg, FetchMetadata, FetchResponseListener}; +use net_traits::{IpcSend, Metadata, ReferrerPolicy, ResourceThreads}; use net_traits::image_cache_thread::{ImageCacheChan, ImageCacheResult, ImageCacheThread}; use net_traits::request::{CredentialsMode, Destination, RequestInit}; use net_traits::storage_thread::StorageType; @@ -605,6 +606,17 @@ impl ScriptThread { }); } + pub fn process_attach_layout(new_layout_info: NewLayoutInfo) { + SCRIPT_THREAD_ROOT.with(|root| { + if let Some(script_thread) = root.get() { + let script_thread = unsafe { &*script_thread }; + script_thread.profile_event(ScriptThreadEventCategory::AttachLayout, || { + script_thread.handle_new_layout(new_layout_info); + }) + } + }); + } + pub fn find_document(id: PipelineId) -> Option> { SCRIPT_THREAD_ROOT.with(|root| root.get().and_then(|script_thread| { let script_thread = unsafe { &*script_thread }; @@ -1219,7 +1231,11 @@ impl ScriptThread { let new_load = InProgressLoad::new(new_pipeline_id, frame_id, parent_info, layout_chan, window_size, load_data.url.clone()); - self.start_page_load(new_load, load_data); + if load_data.url.as_str() == "about:blank" { + self.start_page_load_about_blank(new_load); + } else { + self.start_page_load(new_load, load_data); + } } fn handle_loads_complete(&self, pipeline: PipelineId) { @@ -1645,7 +1661,8 @@ impl ScriptThread { /// Notify the containing document of a child frame that has completed loading. fn handle_frame_load_event(&self, parent_id: PipelineId, frame_id: FrameId, child_id: PipelineId) { - match self.documents.borrow().find_iframe(parent_id, frame_id) { + let iframe = self.documents.borrow().find_iframe(parent_id, frame_id); + match iframe { Some(iframe) => iframe.iframe_load_event_steps(child_id), None => warn!("Message sent to closed pipeline {}.", parent_id), } @@ -2018,7 +2035,8 @@ impl ScriptThread { replace: bool) { match frame_id { Some(frame_id) => { - if let Some(iframe) = self.documents.borrow().find_iframe(parent_pipeline_id, frame_id) { + let iframe = self.documents.borrow().find_iframe(parent_pipeline_id, frame_id); + if let Some(iframe) = iframe { iframe.navigate_or_reload_child_browsing_context(Some(load_data), replace); } } @@ -2096,6 +2114,23 @@ impl ScriptThread { self.incomplete_loads.borrow_mut().push(incomplete); } + /// Synchronously fetch `about:blank`. Stores the `InProgressLoad` + /// argument until a notification is received that the fetch is complete. + fn start_page_load_about_blank(&self, incomplete: InProgressLoad) { + let id = incomplete.pipeline_id; + + self.incomplete_loads.borrow_mut().push(incomplete); + + let url = ServoUrl::parse("about:blank").unwrap(); + let mut context = ParserContext::new(id, url.clone()); + + let mut meta = Metadata::default(url); + meta.set_content_type(Some(&mime!(Text / Html))); + context.process_response(Ok(FetchMetadata::Unfiltered(meta))); + context.process_response_chunk(vec![]); + context.process_response_eof(Ok(())); + } + fn handle_parsing_complete(&self, id: PipelineId) { let document = match self.documents.borrow().find_document(id) { Some(document) => document, diff --git a/components/script_traits/lib.rs b/components/script_traits/lib.rs index 8c51aa22841..0494f44c70d 100644 --- a/components/script_traits/lib.rs +++ b/components/script_traits/lib.rs @@ -492,21 +492,15 @@ pub enum IFrameSandboxState { IFrameUnsandboxed } -/// Specifies the information required to load a URL in an iframe. +/// Specifies the information required to load an iframe. #[derive(Deserialize, Serialize)] pub struct IFrameLoadInfo { - /// Load data containing the url to load - pub load_data: Option, /// Pipeline ID of the parent of this iframe pub parent_pipeline_id: PipelineId, /// The ID for this iframe. pub frame_id: FrameId, - /// The old pipeline ID for this iframe, if a page was previously loaded. - pub old_pipeline_id: Option, /// The new pipeline ID that the iframe has generated. pub new_pipeline_id: PipelineId, - /// Sandbox type of this iframe - pub sandbox: IFrameSandboxState, /// Whether this iframe should be considered private pub is_private: bool, /// Whether this iframe is a mozbrowser iframe @@ -516,6 +510,19 @@ pub struct IFrameLoadInfo { pub replace: bool, } +/// Specifies the information required to load a URL in an iframe. +#[derive(Deserialize, Serialize)] +pub struct IFrameLoadInfoWithData { + /// The information required to load an iframe. + pub info: IFrameLoadInfo, + /// Load data containing the url to load + pub load_data: Option, + /// The old pipeline ID for this iframe, if a page was previously loaded. + pub old_pipeline_id: Option, + /// Sandbox type of this iframe + pub sandbox: IFrameSandboxState, +} + // https://developer.mozilla.org/en-US/docs/Web/API/Using_the_Browser_API#Events /// The events fired in a Browser API context (`