mirror of
https://github.com/lexmount/moli.git
synced 2026-09-30 16:01:42 +00:00
fix(renderer): derive popup policy from current input
This commit is contained in:
@@ -107,11 +107,15 @@ auto-attach reconciliation without duplicate sessions, stable Tab-to-Page
|
||||
ownership across foreground changes, `Target.createTarget` foreground and
|
||||
background behavior, Page visibility changes, screencast visibility
|
||||
`true -> false -> true` across demotion and close-driven promotion, and a real
|
||||
default discovery target that is not recreated after close. The complete
|
||||
12-contract group passed three consecutive fresh Chromium processes and five
|
||||
consecutive fresh Moli processes. Chromium source revision `a03603fe9af6` was
|
||||
also inspected to explain the access-mode and auto-attach machinery; the
|
||||
executable probe remains the behavioral authority.
|
||||
default discovery target that is not recreated after close. It also drives a
|
||||
real middle-button press/release through `Input.dispatchMouseEvent` and checks
|
||||
that the trusted `auxclick` anchor default action leaves the source visible and
|
||||
creates a hidden background Page. The original 12-contract matrix passed three
|
||||
consecutive fresh Chromium processes and five consecutive fresh Moli processes;
|
||||
the middle-click contract was then run unchanged against both engines.
|
||||
Chromium source revision `a03603fe9af6` was also inspected to explain the
|
||||
access-mode and auto-attach machinery; the executable probe remains the
|
||||
behavioral authority.
|
||||
|
||||
## Current Coverage
|
||||
|
||||
|
||||
@@ -106,6 +106,21 @@ def _raw_semantic_contracts() -> tuple[RawSemanticContract, ...]:
|
||||
],
|
||||
_noopener_popup_devtools_attribution,
|
||||
),
|
||||
RawSemanticContract(
|
||||
"raw_cdp_contract_middle_click_background_popup",
|
||||
"A real middle-button press/release dispatches an uncanceled auxclick whose anchor default action opens a background Page target while the source target stays foreground.",
|
||||
"Chromium CurrentInputEvent, NavigationPolicyFromEvent, and HTMLAnchorElement auxclick default action",
|
||||
[
|
||||
"Target.createBrowserContext",
|
||||
"Target.setDiscoverTargets",
|
||||
"Target.createTarget",
|
||||
"Target.attachToTarget x2",
|
||||
"Runtime.evaluate",
|
||||
"Input.dispatchMouseEvent x2",
|
||||
"Target.getTargetInfo",
|
||||
],
|
||||
_middle_click_background_popup,
|
||||
),
|
||||
RawSemanticContract(
|
||||
"raw_cdp_contract_target_multi_attach_independence",
|
||||
"Each flattened attachment has a distinct session and detaching one session leaves the other attachment usable.",
|
||||
@@ -1375,6 +1390,202 @@ async def _noopener_popup_devtools_attribution(
|
||||
await client.websocket.close()
|
||||
|
||||
|
||||
async def _middle_click_background_popup(
|
||||
endpoint: str,
|
||||
fixture: str,
|
||||
) -> dict[str, Any]:
|
||||
client = await connect_raw_cdp(endpoint)
|
||||
browser_context_id: str | None = None
|
||||
source_target_id: str | None = None
|
||||
popup_target_id: str | None = None
|
||||
stage = "create browser context"
|
||||
try:
|
||||
context_result, _ = await _raw_command(client, "Target.createBrowserContext")
|
||||
browser_context_id = context_result["browserContextId"]
|
||||
await _raw_command(client, "Target.setDiscoverTargets", {"discover": True})
|
||||
|
||||
source_url = f"{fixture}/plain?middle-click-source=1"
|
||||
popup_url = f"{fixture}/plain?middle-click-popup=1"
|
||||
stage = "create middle-click source target"
|
||||
source_result, _ = await _raw_command(
|
||||
client,
|
||||
"Target.createTarget",
|
||||
{"url": source_url, "browserContextId": browser_context_id},
|
||||
)
|
||||
source_target_id = source_result["targetId"]
|
||||
attached, _ = await _raw_command(
|
||||
client,
|
||||
"Target.attachToTarget",
|
||||
{"targetId": source_target_id, "flatten": True},
|
||||
)
|
||||
source_session_id = attached["sessionId"]
|
||||
|
||||
source_document: dict[str, Any] | None = None
|
||||
|
||||
async def source_document_loaded() -> bool:
|
||||
nonlocal source_document
|
||||
evaluation, _ = await _raw_command(
|
||||
client,
|
||||
"Runtime.evaluate",
|
||||
{
|
||||
"expression": "({url: location.href, readyState: document.readyState})",
|
||||
"returnByValue": True,
|
||||
},
|
||||
session_id=source_session_id,
|
||||
)
|
||||
candidate = evaluation["result"].get("value")
|
||||
source_document = candidate if isinstance(candidate, dict) else None
|
||||
return source_document == {"url": source_url, "readyState": "complete"}
|
||||
|
||||
stage = "wait for middle-click source document"
|
||||
await wait_until(source_document_loaded, "middle-click source document complete")
|
||||
|
||||
stage = "install middle-click anchor"
|
||||
fixture_result, _ = await _raw_command(
|
||||
client,
|
||||
"Runtime.evaluate",
|
||||
{
|
||||
"expression": (
|
||||
"(() => {"
|
||||
"document.body.replaceChildren();"
|
||||
"document.body.style.margin = '0';"
|
||||
"const anchor = document.createElement('a');"
|
||||
f"anchor.href = {json.dumps(popup_url)};"
|
||||
"anchor.textContent = 'open';"
|
||||
"anchor.style.cssText = 'position:fixed;left:0;top:0;width:120px;height:80px;display:block';"
|
||||
"window.__middleAuxclickEvents = [];"
|
||||
"anchor.addEventListener('auxclick', event => {"
|
||||
"window.__middleAuxclickEvents.push(`${event.type}:${event.button}:${event.isTrusted}`);"
|
||||
"});"
|
||||
"document.body.append(anchor);"
|
||||
"return anchor.getBoundingClientRect().toJSON();"
|
||||
"})()"
|
||||
),
|
||||
"returnByValue": True,
|
||||
},
|
||||
session_id=source_session_id,
|
||||
)
|
||||
fixture_rect = fixture_result["result"].get("value")
|
||||
_require(isinstance(fixture_rect, dict), "middle-click anchor has no layout rect")
|
||||
|
||||
stage = "dispatch real middle-button input"
|
||||
await _raw_command(
|
||||
client,
|
||||
"Input.dispatchMouseEvent",
|
||||
{
|
||||
"type": "mousePressed",
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
"button": "middle",
|
||||
"buttons": 4,
|
||||
"clickCount": 1,
|
||||
},
|
||||
session_id=source_session_id,
|
||||
)
|
||||
_, release_seen = await _raw_command(
|
||||
client,
|
||||
"Input.dispatchMouseEvent",
|
||||
{
|
||||
"type": "mouseReleased",
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
"button": "middle",
|
||||
"buttons": 0,
|
||||
"clickCount": 1,
|
||||
},
|
||||
session_id=source_session_id,
|
||||
)
|
||||
|
||||
auxclick_result, auxclick_seen = await _raw_command(
|
||||
client,
|
||||
"Runtime.evaluate",
|
||||
{
|
||||
"expression": "window.__middleAuxclickEvents.join('|')",
|
||||
"returnByValue": True,
|
||||
},
|
||||
session_id=source_session_id,
|
||||
)
|
||||
assert_equal(
|
||||
auxclick_result["result"].get("value"),
|
||||
"auxclick:1:true",
|
||||
"real middle-button auxclick",
|
||||
)
|
||||
|
||||
stage = "wait for middle-click popup target"
|
||||
popup_event = await _raw_wait_for_event(
|
||||
client,
|
||||
"Target.targetCreated",
|
||||
lambda params: params.get("targetInfo", {}).get("browserContextId")
|
||||
== browser_context_id
|
||||
and params.get("targetInfo", {}).get("targetId") != source_target_id,
|
||||
[*release_seen, *auxclick_seen],
|
||||
)
|
||||
popup_target_id = popup_event["params"]["targetInfo"]["targetId"]
|
||||
|
||||
popup_info: dict[str, Any] | None = None
|
||||
|
||||
async def popup_navigation_committed() -> bool:
|
||||
nonlocal popup_info
|
||||
result, _ = await _raw_command(
|
||||
client,
|
||||
"Target.getTargetInfo",
|
||||
{"targetId": popup_target_id},
|
||||
)
|
||||
candidate = result.get("targetInfo")
|
||||
popup_info = candidate if isinstance(candidate, dict) else None
|
||||
return popup_info is not None and popup_info.get("url") == popup_url
|
||||
|
||||
stage = "wait for middle-click popup navigation"
|
||||
await wait_until(popup_navigation_committed, "middle-click popup navigation")
|
||||
|
||||
popup_attach, _ = await _raw_command(
|
||||
client,
|
||||
"Target.attachToTarget",
|
||||
{"targetId": popup_target_id, "flatten": True},
|
||||
)
|
||||
|
||||
async def visibility(session_id: str) -> str | None:
|
||||
result, _ = await _raw_command(
|
||||
client,
|
||||
"Runtime.evaluate",
|
||||
{
|
||||
"expression": "document.visibilityState",
|
||||
"returnByValue": True,
|
||||
},
|
||||
session_id=session_id,
|
||||
)
|
||||
return result.get("result", {}).get("value")
|
||||
|
||||
source_visibility = await visibility(source_session_id)
|
||||
popup_visibility = await visibility(popup_attach["sessionId"])
|
||||
assert_equal(source_visibility, "visible", "middle-click source visibility")
|
||||
assert_equal(popup_visibility, "hidden", "middle-click popup visibility")
|
||||
|
||||
return {
|
||||
"auxclick": auxclick_result["result"].get("value"),
|
||||
"popupUrl": popup_info.get("url") if popup_info is not None else None,
|
||||
"sourceVisibility": source_visibility,
|
||||
"popupVisibility": popup_visibility,
|
||||
}
|
||||
except Exception as error:
|
||||
raise SmokeError(f"{stage}: {type(error).__name__}: {error}") from error
|
||||
finally:
|
||||
for target_id in (popup_target_id, source_target_id):
|
||||
if target_id is not None:
|
||||
await _ignore_raw_error(
|
||||
client,
|
||||
"Target.closeTarget",
|
||||
{"targetId": target_id},
|
||||
)
|
||||
if browser_context_id is not None:
|
||||
await _ignore_raw_error(
|
||||
client,
|
||||
"Target.disposeBrowserContext",
|
||||
{"browserContextId": browser_context_id},
|
||||
)
|
||||
await client.websocket.close()
|
||||
|
||||
|
||||
async def _target_close_lifecycle(
|
||||
endpoint: str,
|
||||
_fixture: str,
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
context_bootstrap::CHILD_BROWSING_CONTEXT_HANDLE_SLOT,
|
||||
document_runtime::{DocumentPolicyContainer, DomHandle},
|
||||
native_bridge::{
|
||||
child_window_handle_from_marker_data,
|
||||
InputNavigationPolicy, child_window_handle_from_marker_data,
|
||||
element::{
|
||||
SpecialBrowsingContextTarget, navigate_existing_browsing_context_target,
|
||||
navigate_named_iframe_target,
|
||||
@@ -221,6 +221,21 @@ pub(crate) fn window_open_callback<'s>(
|
||||
let opener = (!suppress_opener).then_some(entered_window);
|
||||
let opener_child_handle =
|
||||
opener.and_then(|opener| window_open_receiver_child_handle(scope, opener));
|
||||
let popup_disposition = match host
|
||||
.current_input_event()
|
||||
.map(crate::native_bridge::CurrentInputEvent::navigation_policy)
|
||||
{
|
||||
Some(InputNavigationPolicy::NewBackgroundSurface) => {
|
||||
crate::RendererPopupDisposition::Background
|
||||
}
|
||||
Some(
|
||||
InputNavigationPolicy::Current
|
||||
| InputNavigationPolicy::Download
|
||||
| InputNavigationPolicy::NewWindow
|
||||
| InputNavigationPolicy::NewForegroundSurface,
|
||||
)
|
||||
| None => crate::RendererPopupDisposition::Foreground,
|
||||
};
|
||||
if popup_target_can_use_lightweight_window(&parsed.target_name, &url)
|
||||
&& let Some(opened_popup) = host.open_lightweight_popup_window(
|
||||
scope,
|
||||
@@ -240,9 +255,6 @@ pub(crate) fn window_open_callback<'s>(
|
||||
let window_open_event = opened_popup
|
||||
.created_new_browsing_context
|
||||
.then_some(window_open_event);
|
||||
let popup_disposition = host
|
||||
.current_input_popup_disposition()
|
||||
.unwrap_or(crate::RendererPopupDisposition::Foreground);
|
||||
host.record_pending_popup_activation(
|
||||
RendererPendingPopupActivation::window(
|
||||
root_document,
|
||||
@@ -266,9 +278,6 @@ pub(crate) fn window_open_callback<'s>(
|
||||
}
|
||||
return;
|
||||
}
|
||||
let popup_disposition = host
|
||||
.current_input_popup_disposition()
|
||||
.unwrap_or(crate::RendererPopupDisposition::Foreground);
|
||||
host.record_pending_popup_activation(
|
||||
RendererPendingPopupActivation::window(
|
||||
root_document,
|
||||
|
||||
@@ -267,7 +267,7 @@ impl JsContextHost {
|
||||
timezone_override: None,
|
||||
idle_override: None,
|
||||
protocol_user_gesture_activation_depth: 0,
|
||||
current_input_popup_disposition: None,
|
||||
current_input_event: None,
|
||||
webdriver_bidi_file_prompt_handler_stack: Vec::new(),
|
||||
emulated_media: crate::protocol_types::EmulatedMediaOverrides::default(),
|
||||
viewport_surface: None,
|
||||
|
||||
@@ -747,17 +747,15 @@ impl JsContextHost {
|
||||
self.protocol_user_gesture_activation_depth > 0
|
||||
}
|
||||
|
||||
pub(crate) fn replace_current_input_popup_disposition(
|
||||
pub(crate) fn replace_current_input_event(
|
||||
&mut self,
|
||||
disposition: Option<crate::RendererPopupDisposition>,
|
||||
) -> Option<crate::RendererPopupDisposition> {
|
||||
std::mem::replace(&mut self.current_input_popup_disposition, disposition)
|
||||
event: Option<crate::native_bridge::CurrentInputEvent>,
|
||||
) -> Option<crate::native_bridge::CurrentInputEvent> {
|
||||
std::mem::replace(&mut self.current_input_event, event)
|
||||
}
|
||||
|
||||
pub(crate) fn current_input_popup_disposition(
|
||||
&self,
|
||||
) -> Option<crate::RendererPopupDisposition> {
|
||||
self.current_input_popup_disposition
|
||||
pub(crate) fn current_input_event(&self) -> Option<crate::native_bridge::CurrentInputEvent> {
|
||||
self.current_input_event
|
||||
}
|
||||
|
||||
pub(crate) fn begin_webdriver_bidi_file_prompt_handler(&mut self, handler: &str) {
|
||||
|
||||
@@ -869,7 +869,7 @@ pub(crate) struct JsContextHost {
|
||||
timezone_override: Option<String>,
|
||||
idle_override: Option<crate::protocol_types::EmulatedIdleOverride>,
|
||||
protocol_user_gesture_activation_depth: usize,
|
||||
current_input_popup_disposition: Option<crate::RendererPopupDisposition>,
|
||||
current_input_event: Option<crate::native_bridge::CurrentInputEvent>,
|
||||
webdriver_bidi_file_prompt_handler_stack: Vec<String>,
|
||||
emulated_media: crate::protocol_types::EmulatedMediaOverrides,
|
||||
viewport_surface: Option<crate::protocol_types::ViewportSurface>,
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use super::JsContextHost;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct InputModifiers(u8);
|
||||
|
||||
impl InputModifiers {
|
||||
const ALT: u8 = 1;
|
||||
const CONTROL: u8 = 2;
|
||||
const META: u8 = 4;
|
||||
const SHIFT: u8 = 8;
|
||||
|
||||
const fn from_bits(bits: u8) -> Self {
|
||||
Self(bits)
|
||||
}
|
||||
|
||||
const fn alt(self) -> bool {
|
||||
self.0 & Self::ALT != 0
|
||||
}
|
||||
|
||||
const fn control(self) -> bool {
|
||||
self.0 & Self::CONTROL != 0
|
||||
}
|
||||
|
||||
const fn meta(self) -> bool {
|
||||
self.0 & Self::META != 0
|
||||
}
|
||||
|
||||
const fn shift(self) -> bool {
|
||||
self.0 & Self::SHIFT != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum CurrentInputEventKind {
|
||||
MouseUp { button: i32 },
|
||||
EnterKey,
|
||||
Other,
|
||||
}
|
||||
|
||||
/// The navigation-relevant subset of the real platform input currently being
|
||||
/// handled. DOM events are deliberately not stored here: synthetic events may
|
||||
/// describe modifiers, but cannot manufacture the corresponding user intent.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct CurrentInputEvent {
|
||||
kind: CurrentInputEventKind,
|
||||
modifiers: InputModifiers,
|
||||
}
|
||||
|
||||
impl CurrentInputEvent {
|
||||
pub(crate) fn mouse(event_name: &str, button: i32, modifiers: u8) -> Self {
|
||||
Self {
|
||||
kind: if event_name == "mouseup" {
|
||||
CurrentInputEventKind::MouseUp { button }
|
||||
} else {
|
||||
CurrentInputEventKind::Other
|
||||
},
|
||||
modifiers: InputModifiers::from_bits(modifiers),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn keyboard(key: &str, modifiers: u8) -> Self {
|
||||
Self {
|
||||
kind: if key.eq_ignore_ascii_case("enter") {
|
||||
CurrentInputEventKind::EnterKey
|
||||
} else {
|
||||
CurrentInputEventKind::Other
|
||||
},
|
||||
modifiers: InputModifiers::from_bits(modifiers),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn navigation_policy(self) -> InputNavigationPolicy {
|
||||
let button = match self.kind {
|
||||
CurrentInputEventKind::MouseUp { button } => button,
|
||||
CurrentInputEventKind::EnterKey => 0,
|
||||
CurrentInputEventKind::Other => return InputNavigationPolicy::Current,
|
||||
};
|
||||
navigation_policy_from_modifiers(button, self.modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
/// Chromium-shaped navigation intent before Moli folds window and tab chrome
|
||||
/// into its foreground/background surface model.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum InputNavigationPolicy {
|
||||
Current,
|
||||
Download,
|
||||
NewWindow,
|
||||
NewForegroundSurface,
|
||||
NewBackgroundSurface,
|
||||
}
|
||||
|
||||
/// Computes a DOM event's requested policy, then applies Blink's synthetic
|
||||
/// download and tab-under protections against the ambient real input.
|
||||
pub(crate) fn navigation_policy_from_event(
|
||||
button: i32,
|
||||
modifiers: u8,
|
||||
current_input: Option<CurrentInputEvent>,
|
||||
) -> InputNavigationPolicy {
|
||||
let event_policy =
|
||||
navigation_policy_from_modifiers(button, InputModifiers::from_bits(modifiers));
|
||||
let input_policy = current_input
|
||||
.map(CurrentInputEvent::navigation_policy)
|
||||
.unwrap_or(InputNavigationPolicy::Current);
|
||||
|
||||
match event_policy {
|
||||
InputNavigationPolicy::Download if input_policy != InputNavigationPolicy::Download => {
|
||||
InputNavigationPolicy::Current
|
||||
}
|
||||
InputNavigationPolicy::NewBackgroundSurface
|
||||
if input_policy != InputNavigationPolicy::NewBackgroundSurface =>
|
||||
{
|
||||
InputNavigationPolicy::NewForegroundSurface
|
||||
}
|
||||
_ => event_policy,
|
||||
}
|
||||
}
|
||||
|
||||
fn navigation_policy_from_modifiers(
|
||||
button: i32,
|
||||
modifiers: InputModifiers,
|
||||
) -> InputNavigationPolicy {
|
||||
let platform_new_tab_modifier = if cfg!(target_os = "macos") {
|
||||
modifiers.meta()
|
||||
} else {
|
||||
modifiers.control()
|
||||
};
|
||||
|
||||
let requests_new_tab = button == 1 || platform_new_tab_modifier;
|
||||
if !requests_new_tab && !modifiers.shift() && !modifiers.alt() {
|
||||
InputNavigationPolicy::Current
|
||||
} else if requests_new_tab {
|
||||
if modifiers.shift() {
|
||||
InputNavigationPolicy::NewForegroundSurface
|
||||
} else {
|
||||
InputNavigationPolicy::NewBackgroundSurface
|
||||
}
|
||||
} else if modifiers.shift() {
|
||||
InputNavigationPolicy::NewWindow
|
||||
} else {
|
||||
InputNavigationPolicy::Download
|
||||
}
|
||||
}
|
||||
|
||||
/// Restores the ambient real input automatically across early returns,
|
||||
/// nested input dispatch, and unwinding.
|
||||
#[must_use = "dropping the scope immediately would discard the current input event"]
|
||||
pub(crate) struct CurrentInputEventScope {
|
||||
context_host: Rc<RefCell<JsContextHost>>,
|
||||
previous: Option<CurrentInputEvent>,
|
||||
installed: CurrentInputEvent,
|
||||
}
|
||||
|
||||
impl CurrentInputEventScope {
|
||||
pub(crate) fn enter(
|
||||
context_host: Rc<RefCell<JsContextHost>>,
|
||||
event: CurrentInputEvent,
|
||||
) -> Self {
|
||||
let previous = context_host
|
||||
.borrow_mut()
|
||||
.replace_current_input_event(Some(event));
|
||||
Self {
|
||||
context_host,
|
||||
previous,
|
||||
installed: event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CurrentInputEventScope {
|
||||
fn drop(&mut self) {
|
||||
let replaced = self
|
||||
.context_host
|
||||
.borrow_mut()
|
||||
.replace_current_input_event(self.previous);
|
||||
assert_eq!(
|
||||
replaced,
|
||||
Some(self.installed),
|
||||
"the current input event must remain scoped to its real input dispatch"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const ALT: u8 = 1;
|
||||
const CONTROL: u8 = 2;
|
||||
const META: u8 = 4;
|
||||
const SHIFT: u8 = 8;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const PLATFORM_NEW_TAB_MODIFIER: u8 = META;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const PLATFORM_NEW_TAB_MODIFIER: u8 = CONTROL;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const NON_PLATFORM_NEW_TAB_MODIFIER: u8 = CONTROL;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const NON_PLATFORM_NEW_TAB_MODIFIER: u8 = META;
|
||||
|
||||
#[test]
|
||||
fn modifier_policy_matches_chromium_surface_selection() {
|
||||
assert_eq!(
|
||||
navigation_policy_from_modifiers(0, InputModifiers::from_bits(0)),
|
||||
InputNavigationPolicy::Current
|
||||
);
|
||||
assert_eq!(
|
||||
navigation_policy_from_modifiers(
|
||||
0,
|
||||
InputModifiers::from_bits(NON_PLATFORM_NEW_TAB_MODIFIER),
|
||||
),
|
||||
InputNavigationPolicy::Current
|
||||
);
|
||||
assert_eq!(
|
||||
navigation_policy_from_modifiers(1, InputModifiers::from_bits(0)),
|
||||
InputNavigationPolicy::NewBackgroundSurface
|
||||
);
|
||||
assert_eq!(
|
||||
navigation_policy_from_modifiers(
|
||||
0,
|
||||
InputModifiers::from_bits(PLATFORM_NEW_TAB_MODIFIER),
|
||||
),
|
||||
InputNavigationPolicy::NewBackgroundSurface
|
||||
);
|
||||
assert_eq!(
|
||||
navigation_policy_from_modifiers(0, InputModifiers::from_bits(SHIFT)),
|
||||
InputNavigationPolicy::NewWindow
|
||||
);
|
||||
assert_eq!(
|
||||
navigation_policy_from_modifiers(1, InputModifiers::from_bits(SHIFT)),
|
||||
InputNavigationPolicy::NewForegroundSurface
|
||||
);
|
||||
assert_eq!(
|
||||
navigation_policy_from_modifiers(0, InputModifiers::from_bits(ALT)),
|
||||
InputNavigationPolicy::Download
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthesized_events_cannot_request_tab_unders_or_downloads() {
|
||||
assert_eq!(
|
||||
navigation_policy_from_event(1, 0, None),
|
||||
InputNavigationPolicy::NewForegroundSurface
|
||||
);
|
||||
assert_eq!(
|
||||
navigation_policy_from_event(0, PLATFORM_NEW_TAB_MODIFIER, None),
|
||||
InputNavigationPolicy::NewForegroundSurface
|
||||
);
|
||||
assert_eq!(
|
||||
navigation_policy_from_event(0, ALT, None),
|
||||
InputNavigationPolicy::Current
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_navigation_capable_real_input_supplies_an_input_policy() {
|
||||
assert_eq!(
|
||||
CurrentInputEvent::mouse("mousedown", 0, PLATFORM_NEW_TAB_MODIFIER).navigation_policy(),
|
||||
InputNavigationPolicy::Current
|
||||
);
|
||||
assert_eq!(
|
||||
CurrentInputEvent::mouse("mouseup", 0, PLATFORM_NEW_TAB_MODIFIER).navigation_policy(),
|
||||
InputNavigationPolicy::NewBackgroundSurface
|
||||
);
|
||||
assert_eq!(
|
||||
CurrentInputEvent::keyboard("Enter", PLATFORM_NEW_TAB_MODIFIER).navigation_policy(),
|
||||
InputNavigationPolicy::NewBackgroundSurface
|
||||
);
|
||||
assert_eq!(
|
||||
CurrentInputEvent::keyboard(" ", PLATFORM_NEW_TAB_MODIFIER).navigation_policy(),
|
||||
InputNavigationPolicy::Current
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ pub(crate) use activation::{
|
||||
pub(crate) use activation::{
|
||||
activate_handle_via_click, activate_handle_via_click_with_detail_and_modifiers,
|
||||
activate_handle_via_synthetic_click, dispatched_click_activation_target,
|
||||
finish_legacy_activation_for_dispatched_click,
|
||||
finish_legacy_activation_for_dispatched_click, perform_auxiliary_link_default_action,
|
||||
perform_click_default_action_for_dispatched_event, perform_drop_default_action,
|
||||
prepare_legacy_activation_for_dispatched_click, replace_contenteditable_selection,
|
||||
scroll_to_url_fragment_or_top, select_contenteditable_contents,
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
document_runtime::{DomHandle, EventTargetHandle},
|
||||
frame_owner_model::DocumentId,
|
||||
native_bridge::context_host::ChildBrowsingContextBootstrap,
|
||||
native_bridge::{CurrentInputEvent, InputNavigationPolicy, navigation_policy_from_event},
|
||||
runtime::RendererDocumentLifecycleIdentity,
|
||||
};
|
||||
|
||||
@@ -730,45 +731,8 @@ fn dispatch_click_event(
|
||||
Some(dispatch_public_event(scope, runtime_ptr, handle, event))
|
||||
}
|
||||
|
||||
fn click_handle_internal(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
handle: DomHandle,
|
||||
x: f64,
|
||||
y: f64,
|
||||
button: i32,
|
||||
buttons: i32,
|
||||
click_detail: i32,
|
||||
modifiers: u8,
|
||||
user_initiated: bool,
|
||||
) -> RendererInputDispatchOutcome {
|
||||
let popup_disposition = current_click_popup_disposition(button, modifiers, user_initiated);
|
||||
let previous_popup_disposition = unsafe { &mut *runtime_ptr }
|
||||
.replace_current_input_popup_disposition(Some(popup_disposition));
|
||||
let outcome = click_handle_internal_with_current_input(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
x,
|
||||
y,
|
||||
button,
|
||||
buttons,
|
||||
click_detail,
|
||||
modifiers,
|
||||
user_initiated,
|
||||
);
|
||||
let replaced_popup_disposition = unsafe { &mut *runtime_ptr }
|
||||
.replace_current_input_popup_disposition(previous_popup_disposition);
|
||||
assert_eq!(
|
||||
replaced_popup_disposition,
|
||||
Some(popup_disposition),
|
||||
"the current click popup disposition must remain scoped to its activation"
|
||||
);
|
||||
outcome
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn click_handle_internal_with_current_input(
|
||||
fn click_handle_internal(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
handle: DomHandle,
|
||||
@@ -1045,6 +1009,23 @@ fn click_activation_default_action_handle(
|
||||
None
|
||||
}
|
||||
|
||||
fn hyperlink_activation_default_action_handle(
|
||||
runtime: &JsContextHost,
|
||||
target: DomHandle,
|
||||
) -> Option<DomHandle> {
|
||||
let mut current = Some(target);
|
||||
while let Some(handle) = current {
|
||||
if is_disabled_form_control(runtime, handle) {
|
||||
return None;
|
||||
}
|
||||
if element_is_anchor_with_href(runtime, handle) {
|
||||
return Some(handle);
|
||||
}
|
||||
current = runtime.dom_host().parent_node(handle);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
enum ClickActivationDefaultAction {
|
||||
Element(DomHandle),
|
||||
LabelControl(DomHandle),
|
||||
@@ -1203,6 +1184,26 @@ pub(crate) fn perform_click_default_action_for_dispatched_event(
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn perform_auxiliary_link_default_action(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
target: DomHandle,
|
||||
button: i32,
|
||||
modifiers: u8,
|
||||
pending_child_navigations_before_default: &[(DomHandle, ChildBrowsingContextBootstrap)],
|
||||
) -> Option<RendererPendingDownloadActivation> {
|
||||
let handle = hyperlink_activation_default_action_handle(unsafe { &*runtime_ptr }, target)?;
|
||||
anchor_click_default_action(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
button,
|
||||
modifiers,
|
||||
true,
|
||||
pending_child_navigations_before_default,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) enum DispatchedClickLegacyActivation {
|
||||
Checkbox {
|
||||
handle: DomHandle,
|
||||
@@ -1855,7 +1856,8 @@ fn anchor_click_default_action(
|
||||
suggested_filename,
|
||||
);
|
||||
}
|
||||
let navigation_policy = hyperlink_navigation_policy(button, modifiers, user_initiated);
|
||||
let navigation_policy =
|
||||
hyperlink_navigation_policy(button, modifiers, runtime.current_input_event());
|
||||
if navigation_policy == HyperlinkNavigationPolicy::Download {
|
||||
return anchor_download_activation(
|
||||
scope,
|
||||
@@ -2015,47 +2017,19 @@ enum HyperlinkNavigationPolicy {
|
||||
fn hyperlink_navigation_policy(
|
||||
button: i32,
|
||||
modifiers: u8,
|
||||
user_initiated: bool,
|
||||
current_input: Option<CurrentInputEvent>,
|
||||
) -> HyperlinkNavigationPolicy {
|
||||
const ALT: u8 = 1;
|
||||
const SHIFT: u8 = 8;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let platform_new_tab_modifier = 4;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let platform_new_tab_modifier = 2;
|
||||
|
||||
let requests_new_tab = button == 1 || modifiers & platform_new_tab_modifier != 0;
|
||||
let shift = modifiers & SHIFT != 0;
|
||||
let alt = modifiers & ALT != 0;
|
||||
if requests_new_tab {
|
||||
let disposition = if shift || !user_initiated {
|
||||
RendererPopupDisposition::Foreground
|
||||
} else {
|
||||
RendererPopupDisposition::Background
|
||||
};
|
||||
HyperlinkNavigationPolicy::Auxiliary(disposition)
|
||||
} else if shift {
|
||||
// The renderer target model has no separate window chrome. Preserve
|
||||
// Chromium's selected-surface behavior by folding a new window into a
|
||||
// foreground auxiliary target.
|
||||
HyperlinkNavigationPolicy::Auxiliary(RendererPopupDisposition::Foreground)
|
||||
} else if alt && user_initiated {
|
||||
HyperlinkNavigationPolicy::Download
|
||||
} else {
|
||||
HyperlinkNavigationPolicy::Current
|
||||
}
|
||||
}
|
||||
|
||||
fn current_click_popup_disposition(
|
||||
button: i32,
|
||||
modifiers: u8,
|
||||
user_initiated: bool,
|
||||
) -> RendererPopupDisposition {
|
||||
match hyperlink_navigation_policy(button, modifiers, user_initiated) {
|
||||
HyperlinkNavigationPolicy::Auxiliary(disposition) => disposition,
|
||||
HyperlinkNavigationPolicy::Current | HyperlinkNavigationPolicy::Download => {
|
||||
RendererPopupDisposition::Foreground
|
||||
match navigation_policy_from_event(button, modifiers, current_input) {
|
||||
InputNavigationPolicy::Current => HyperlinkNavigationPolicy::Current,
|
||||
InputNavigationPolicy::Download => HyperlinkNavigationPolicy::Download,
|
||||
InputNavigationPolicy::NewBackgroundSurface => {
|
||||
HyperlinkNavigationPolicy::Auxiliary(RendererPopupDisposition::Background)
|
||||
}
|
||||
InputNavigationPolicy::NewWindow | InputNavigationPolicy::NewForegroundSurface => {
|
||||
// The renderer target model has no separate window chrome. Preserve
|
||||
// Chromium's selected-surface behavior by folding a new window into a
|
||||
// foreground auxiliary target.
|
||||
HyperlinkNavigationPolicy::Auxiliary(RendererPopupDisposition::Foreground)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2394,11 +2368,11 @@ mod hyperlink_popup_disposition_tests {
|
||||
#[test]
|
||||
fn ordinary_click_uses_the_declared_target() {
|
||||
assert_eq!(
|
||||
hyperlink_navigation_policy(0, 0, true),
|
||||
hyperlink_navigation_policy(0, 0, None),
|
||||
HyperlinkNavigationPolicy::Current
|
||||
);
|
||||
assert_eq!(
|
||||
hyperlink_navigation_policy(0, NON_PLATFORM_NEW_TAB_MODIFIER, true),
|
||||
hyperlink_navigation_policy(0, NON_PLATFORM_NEW_TAB_MODIFIER, None),
|
||||
HyperlinkNavigationPolicy::Current
|
||||
);
|
||||
}
|
||||
@@ -2406,19 +2380,35 @@ mod hyperlink_popup_disposition_tests {
|
||||
#[test]
|
||||
fn trusted_new_context_input_matches_chromium_surface_selection() {
|
||||
assert_eq!(
|
||||
hyperlink_navigation_policy(1, 0, true),
|
||||
hyperlink_navigation_policy(1, 0, Some(CurrentInputEvent::mouse("mouseup", 1, 0)),),
|
||||
HyperlinkNavigationPolicy::Auxiliary(RendererPopupDisposition::Background)
|
||||
);
|
||||
assert_eq!(
|
||||
hyperlink_navigation_policy(0, PLATFORM_NEW_TAB_MODIFIER, true),
|
||||
hyperlink_navigation_policy(
|
||||
0,
|
||||
PLATFORM_NEW_TAB_MODIFIER,
|
||||
Some(CurrentInputEvent::mouse(
|
||||
"mouseup",
|
||||
0,
|
||||
PLATFORM_NEW_TAB_MODIFIER,
|
||||
)),
|
||||
),
|
||||
HyperlinkNavigationPolicy::Auxiliary(RendererPopupDisposition::Background)
|
||||
);
|
||||
assert_eq!(
|
||||
hyperlink_navigation_policy(0, SHIFT, true),
|
||||
hyperlink_navigation_policy(
|
||||
0,
|
||||
SHIFT,
|
||||
Some(CurrentInputEvent::mouse("mouseup", 0, SHIFT)),
|
||||
),
|
||||
HyperlinkNavigationPolicy::Auxiliary(RendererPopupDisposition::Foreground)
|
||||
);
|
||||
assert_eq!(
|
||||
hyperlink_navigation_policy(1, SHIFT, true),
|
||||
hyperlink_navigation_policy(
|
||||
1,
|
||||
SHIFT,
|
||||
Some(CurrentInputEvent::mouse("mouseup", 1, SHIFT)),
|
||||
),
|
||||
HyperlinkNavigationPolicy::Auxiliary(RendererPopupDisposition::Foreground)
|
||||
);
|
||||
}
|
||||
@@ -2426,11 +2416,11 @@ mod hyperlink_popup_disposition_tests {
|
||||
#[test]
|
||||
fn synthetic_new_tab_input_cannot_create_a_background_tab_under() {
|
||||
assert_eq!(
|
||||
hyperlink_navigation_policy(1, 0, false),
|
||||
hyperlink_navigation_policy(1, 0, None),
|
||||
HyperlinkNavigationPolicy::Auxiliary(RendererPopupDisposition::Foreground)
|
||||
);
|
||||
assert_eq!(
|
||||
hyperlink_navigation_policy(0, PLATFORM_NEW_TAB_MODIFIER, false),
|
||||
hyperlink_navigation_policy(0, PLATFORM_NEW_TAB_MODIFIER, None),
|
||||
HyperlinkNavigationPolicy::Auxiliary(RendererPopupDisposition::Foreground)
|
||||
);
|
||||
}
|
||||
@@ -2438,11 +2428,11 @@ mod hyperlink_popup_disposition_tests {
|
||||
#[test]
|
||||
fn only_trusted_alt_click_requests_a_download() {
|
||||
assert_eq!(
|
||||
hyperlink_navigation_policy(0, ALT, true),
|
||||
hyperlink_navigation_policy(0, ALT, Some(CurrentInputEvent::mouse("mouseup", 0, ALT)),),
|
||||
HyperlinkNavigationPolicy::Download
|
||||
);
|
||||
assert_eq!(
|
||||
hyperlink_navigation_policy(0, ALT, false),
|
||||
hyperlink_navigation_policy(0, ALT, None),
|
||||
HyperlinkNavigationPolicy::Current
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ pub(in crate::native_bridge) use default_action::navigate_form_target_browsing_c
|
||||
pub(crate) use default_action::{
|
||||
activate_handle_via_click, activate_handle_via_click_with_detail_and_modifiers,
|
||||
activate_handle_via_synthetic_click, dispatched_click_activation_target,
|
||||
finish_legacy_activation_for_dispatched_click,
|
||||
finish_legacy_activation_for_dispatched_click, perform_auxiliary_link_default_action,
|
||||
perform_click_default_action_for_dispatched_event, perform_drop_default_action,
|
||||
prepare_legacy_activation_for_dispatched_click, replace_contenteditable_selection,
|
||||
scroll_to_url_fragment_or_top, select_contenteditable_contents,
|
||||
|
||||
@@ -5,10 +5,14 @@ mod bridge;
|
||||
mod child_window_surface;
|
||||
mod collections;
|
||||
mod context_host;
|
||||
mod current_input;
|
||||
pub(crate) use context_host::{
|
||||
JsContextHost, JsContextHostPageTaskCapabilities, PendingScrollObservableEffects,
|
||||
PostParseAutofocusAdmission, ServiceWorkerWindowOwner,
|
||||
};
|
||||
pub(crate) use current_input::{
|
||||
CurrentInputEvent, CurrentInputEventScope, InputNavigationPolicy, navigation_policy_from_event,
|
||||
};
|
||||
pub(crate) mod document;
|
||||
pub(crate) mod element;
|
||||
mod helpers;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::input_dispatch_outcome;
|
||||
use super::input_helpers::{
|
||||
@@ -11,7 +12,6 @@ use super::inspector::{
|
||||
use super::{ActiveDragSession, ActiveScrollbarDrag, ActiveTouchPoint, ScriptVm};
|
||||
use crate::document_runtime::DomHandle;
|
||||
use crate::dom::native::{Node, SelectedFile};
|
||||
use crate::native_bridge::PointerCaptureDispatchEvent;
|
||||
use crate::native_bridge::element::{
|
||||
TouchEventPoint, activate_handle_via_click,
|
||||
activate_handle_via_click_with_detail_and_modifiers, cache_input_files_from_selected_files,
|
||||
@@ -22,13 +22,16 @@ use crate::native_bridge::element::{
|
||||
construct_pointer_event_with_related_target_and_modifiers, construct_simple_event,
|
||||
construct_touch_event, construct_touch_event_with_points, construct_wheel_event,
|
||||
contenteditable_editing_host, dispatch_public_event, observable_input_hit_test,
|
||||
observable_input_surface_hit_test, perform_drop_default_action,
|
||||
perform_mouse_focus_default_action, perform_scrollbar_scroll_default_action,
|
||||
perform_wheel_scroll_default_action, replace_contenteditable_selection,
|
||||
replace_text_control_selection, select_contenteditable_contents,
|
||||
text_control_set_selection_range_internal,
|
||||
observable_input_surface_hit_test, perform_auxiliary_link_default_action,
|
||||
perform_drop_default_action, perform_mouse_focus_default_action,
|
||||
perform_scrollbar_scroll_default_action, perform_wheel_scroll_default_action,
|
||||
replace_contenteditable_selection, replace_text_control_selection,
|
||||
select_contenteditable_contents, text_control_set_selection_range_internal,
|
||||
text_control_set_selection_range_with_direction_internal, text_control_value, update_focus,
|
||||
};
|
||||
use crate::native_bridge::{
|
||||
CurrentInputEvent, CurrentInputEventScope, PointerCaptureDispatchEvent,
|
||||
};
|
||||
use crate::runtime::{
|
||||
RendererDragData, RendererInputDispatchOutcome, RendererPointerEventProperties,
|
||||
RendererTouchPoint,
|
||||
@@ -72,6 +75,12 @@ fn can_suppress_compat_mouse_event(event_name: &str) -> bool {
|
||||
const MOUSE_POINTER_ID: i32 = 1;
|
||||
const TOUCH_POINTER_ID: i32 = 2;
|
||||
|
||||
struct PreparedMouseInputDispatch {
|
||||
button: i32,
|
||||
buttons: i32,
|
||||
released_press: Option<PendingMousePress>,
|
||||
}
|
||||
|
||||
fn touch_pointer_id(touch_id: i32) -> i32 {
|
||||
TOUCH_POINTER_ID.saturating_add(touch_id.max(0))
|
||||
}
|
||||
@@ -362,19 +371,22 @@ impl ScriptVm {
|
||||
pointer: RendererPointerEventProperties,
|
||||
modifiers: u8,
|
||||
) -> Result<RendererInputDispatchOutcome> {
|
||||
let result = self
|
||||
.dispatch_mouse_event_at_point_with_pointer_and_modifiers_without_checkpoint(
|
||||
x,
|
||||
y,
|
||||
event_name,
|
||||
button,
|
||||
buttons,
|
||||
click_count,
|
||||
delta_x,
|
||||
delta_y,
|
||||
pointer,
|
||||
modifiers,
|
||||
);
|
||||
let prepared = self.prepare_mouse_input_dispatch(event_name, button, buttons);
|
||||
let _current_input_event = CurrentInputEventScope::enter(
|
||||
Rc::clone(&self._context_host),
|
||||
CurrentInputEvent::mouse(event_name, prepared.button, modifiers),
|
||||
);
|
||||
let result = self.dispatch_mouse_event_at_point_with_pointer_and_modifiers_body(
|
||||
x,
|
||||
y,
|
||||
event_name,
|
||||
prepared,
|
||||
click_count,
|
||||
delta_x,
|
||||
delta_y,
|
||||
pointer,
|
||||
modifiers,
|
||||
);
|
||||
self.finish_input_event_dispatch_turn(result)
|
||||
}
|
||||
|
||||
@@ -428,6 +440,30 @@ impl ScriptVm {
|
||||
pointer: RendererPointerEventProperties,
|
||||
modifiers: u8,
|
||||
) -> Result<RendererInputDispatchOutcome> {
|
||||
let prepared = self.prepare_mouse_input_dispatch(event_name, button, buttons);
|
||||
let _current_input_event = CurrentInputEventScope::enter(
|
||||
Rc::clone(&self._context_host),
|
||||
CurrentInputEvent::mouse(event_name, prepared.button, modifiers),
|
||||
);
|
||||
self.dispatch_mouse_event_at_point_with_pointer_and_modifiers_body(
|
||||
x,
|
||||
y,
|
||||
event_name,
|
||||
prepared,
|
||||
click_count,
|
||||
delta_x,
|
||||
delta_y,
|
||||
pointer,
|
||||
modifiers,
|
||||
)
|
||||
}
|
||||
|
||||
fn prepare_mouse_input_dispatch(
|
||||
&mut self,
|
||||
event_name: &str,
|
||||
button: i32,
|
||||
buttons: Option<i32>,
|
||||
) -> PreparedMouseInputDispatch {
|
||||
let previous_pressed_buttons = self.pressed_mouse_buttons;
|
||||
let released_press = if event_name == "mouseup" {
|
||||
self.pending_mouse_press.take()
|
||||
@@ -474,6 +510,31 @@ impl ScriptVm {
|
||||
}
|
||||
}
|
||||
let buttons = buttons.unwrap_or(self.pressed_mouse_buttons);
|
||||
PreparedMouseInputDispatch {
|
||||
button,
|
||||
buttons,
|
||||
released_press,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn dispatch_mouse_event_at_point_with_pointer_and_modifiers_body(
|
||||
&mut self,
|
||||
x: f64,
|
||||
y: f64,
|
||||
event_name: &str,
|
||||
prepared: PreparedMouseInputDispatch,
|
||||
click_count: i32,
|
||||
delta_x: f64,
|
||||
delta_y: f64,
|
||||
pointer: RendererPointerEventProperties,
|
||||
modifiers: u8,
|
||||
) -> Result<RendererInputDispatchOutcome> {
|
||||
let PreparedMouseInputDispatch {
|
||||
button,
|
||||
buttons,
|
||||
released_press,
|
||||
} = prepared;
|
||||
|
||||
if let Some(outcome) =
|
||||
self.dispatch_active_scrollbar_mouse_event(x, y, event_name, button, buttons)?
|
||||
@@ -967,6 +1028,11 @@ impl ScriptVm {
|
||||
if event_name == "mouseup" {
|
||||
suppress_compat_mouse_events = false;
|
||||
}
|
||||
let had_pending_top_level_navigation_before_event =
|
||||
unsafe { &*runtime_ptr }.has_pending_location_navigation();
|
||||
let pending_child_navigations_before_event = unsafe { &*runtime_ptr }
|
||||
.pending_live_child_browsing_context_navigation_snapshot();
|
||||
let mut pending_download = None;
|
||||
if let Some(event) = construct_mouse_event_with_modifiers(
|
||||
scope,
|
||||
follow_up_event_name,
|
||||
@@ -976,7 +1042,27 @@ impl ScriptVm {
|
||||
buttons,
|
||||
modifiers,
|
||||
) {
|
||||
let _ = dispatch_public_event(scope, runtime_ptr, handle, event);
|
||||
let dispatched = dispatch_public_event(scope, runtime_ptr, handle, event);
|
||||
if follow_up_event_name == "auxclick" && dispatched.allows_default() {
|
||||
pending_download = perform_auxiliary_link_default_action(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
button,
|
||||
modifiers,
|
||||
&pending_child_navigations_before_event,
|
||||
);
|
||||
}
|
||||
}
|
||||
if follow_up_event_name == "auxclick" {
|
||||
return Ok(RendererInputDispatchOutcome {
|
||||
handled: true,
|
||||
triggered_top_level_navigation:
|
||||
!had_pending_top_level_navigation_before_event
|
||||
&& unsafe { &*runtime_ptr }.has_pending_location_navigation(),
|
||||
pending_download,
|
||||
pending_file_chooser: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
@@ -1708,6 +1794,10 @@ impl ScriptVm {
|
||||
auto_repeat: bool,
|
||||
should_insert_text: bool,
|
||||
) -> Result<RendererInputDispatchOutcome> {
|
||||
let _current_input_event = CurrentInputEventScope::enter(
|
||||
Rc::clone(&self._context_host),
|
||||
CurrentInputEvent::keyboard(key, modifiers),
|
||||
);
|
||||
let handle = self
|
||||
.document_runtime
|
||||
.active_element_handle()
|
||||
|
||||
@@ -3727,7 +3727,61 @@ fn trusted_alt_click_downloads_an_anchor_without_a_download_attribute() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_open_in_click_listener_uses_current_input_popup_disposition() {
|
||||
fn alt_input_does_not_turn_window_open_into_a_download_or_background_popup() {
|
||||
let mut vm = new_storage_test_vm("https://alt-window-open.test/path/index.html");
|
||||
vm.eval(
|
||||
r#"const html = document.createElement('html');
|
||||
const body = document.createElement('body');
|
||||
body.style.margin = '0';
|
||||
const button = document.createElement('button');
|
||||
button.style.cssText = 'display:block;width:100px;height:100px';
|
||||
button.addEventListener('click', () => window.open('/target', '_blank'));
|
||||
body.appendChild(button);
|
||||
html.appendChild(body);
|
||||
document.appendChild(html);
|
||||
'installed'"#,
|
||||
)
|
||||
.expect("Alt window.open fixture should initialize");
|
||||
|
||||
vm.dispatch_mouse_event_at_point_with_pointer_and_modifiers(
|
||||
20.0,
|
||||
20.0,
|
||||
"mousedown",
|
||||
0,
|
||||
None,
|
||||
1,
|
||||
0.0,
|
||||
0.0,
|
||||
crate::runtime::RendererPointerEventProperties::default(),
|
||||
1,
|
||||
)
|
||||
.expect("Alt mousedown should dispatch");
|
||||
let outcome = vm
|
||||
.dispatch_mouse_event_at_point_with_pointer_and_modifiers(
|
||||
20.0,
|
||||
20.0,
|
||||
"mouseup",
|
||||
0,
|
||||
None,
|
||||
1,
|
||||
0.0,
|
||||
0.0,
|
||||
crate::runtime::RendererPointerEventProperties::default(),
|
||||
1,
|
||||
)
|
||||
.expect("Alt mouseup should dispatch");
|
||||
|
||||
assert!(outcome.pending_download.is_none());
|
||||
let popups = vm.take_pending_popup_activations();
|
||||
assert_eq!(popups.len(), 1);
|
||||
assert_eq!(
|
||||
popups[0].disposition(),
|
||||
crate::RendererPopupDisposition::Foreground
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_open_in_click_listener_uses_current_input_event() {
|
||||
#[cfg(target_os = "macos")]
|
||||
let (platform_modifier, synthetic_modifier) = (4, "metaKey: true");
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -3821,6 +3875,211 @@ fn window_open_in_click_listener_uses_current_input_popup_disposition() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_mouseup_input_covers_pointer_mouse_click_and_microtask_callbacks() {
|
||||
#[cfg(target_os = "macos")]
|
||||
let platform_modifier = 4;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let platform_modifier = 2;
|
||||
|
||||
let mut vm = new_storage_test_vm("https://window-open-input-scope.test/path/index.html");
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const html = document.createElement('html');
|
||||
const body = document.createElement('body');
|
||||
body.style.margin = '0';
|
||||
const button = document.createElement('button');
|
||||
button.style.cssText = 'display:block;width:100px;height:100px';
|
||||
const nested = document.createElement('button');
|
||||
nested.addEventListener('click', () => window.open('/nested-click', '_blank'));
|
||||
button.addEventListener('pointerup', () => window.open('/pointerup', '_blank'));
|
||||
button.addEventListener('mouseup', () => window.open('/mouseup', '_blank'));
|
||||
button.addEventListener('click', () => {
|
||||
window.open('/click', '_blank');
|
||||
nested.click();
|
||||
Promise.resolve().then(() => window.open('/microtask', '_blank'));
|
||||
});
|
||||
body.append(button, nested);
|
||||
html.appendChild(body);
|
||||
document.appendChild(html);
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("current input scope fixture should initialize");
|
||||
|
||||
vm.dispatch_mouse_event_at_point_with_pointer_and_modifiers(
|
||||
20.0,
|
||||
20.0,
|
||||
"mousedown",
|
||||
0,
|
||||
None,
|
||||
1,
|
||||
0.0,
|
||||
0.0,
|
||||
crate::runtime::RendererPointerEventProperties::default(),
|
||||
platform_modifier,
|
||||
)
|
||||
.expect("new-tab-modified mousedown should dispatch");
|
||||
vm.dispatch_mouse_event_at_point_with_pointer_and_modifiers(
|
||||
20.0,
|
||||
20.0,
|
||||
"mouseup",
|
||||
0,
|
||||
None,
|
||||
1,
|
||||
0.0,
|
||||
0.0,
|
||||
crate::runtime::RendererPointerEventProperties::default(),
|
||||
platform_modifier,
|
||||
)
|
||||
.expect("new-tab-modified mouseup should dispatch");
|
||||
|
||||
let popups = vm.take_pending_popup_activations();
|
||||
assert_eq!(
|
||||
popups.iter().map(|popup| popup.url()).collect::<Vec<_>>(),
|
||||
vec![
|
||||
"https://window-open-input-scope.test/pointerup",
|
||||
"https://window-open-input-scope.test/mouseup",
|
||||
"https://window-open-input-scope.test/click",
|
||||
"https://window-open-input-scope.test/nested-click",
|
||||
"https://window-open-input-scope.test/microtask",
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
popups
|
||||
.iter()
|
||||
.all(|popup| { popup.disposition() == crate::RendererPopupDisposition::Background })
|
||||
);
|
||||
|
||||
vm.eval("window.open('/after-input', '_blank'); 'done'")
|
||||
.expect("post-input window.open should evaluate");
|
||||
let after_input = vm.take_pending_popup_activations();
|
||||
assert_eq!(after_input.len(), 1);
|
||||
assert_eq!(
|
||||
after_input[0].disposition(),
|
||||
crate::RendererPopupDisposition::Foreground,
|
||||
"the real input policy must be restored after its dispatch turn"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_auxclick_runs_the_anchor_default_action_in_the_background() {
|
||||
let mut vm = new_storage_test_vm("https://middle-auxclick.test/path/index.html");
|
||||
vm.eval(
|
||||
r#"const html = document.createElement('html');
|
||||
const body = document.createElement('body');
|
||||
body.style.margin = '0';
|
||||
const link = document.createElement('a');
|
||||
link.href = '/anchor-target';
|
||||
link.style.cssText = 'display:block;width:100px;height:100px';
|
||||
body.appendChild(link);
|
||||
html.appendChild(body);
|
||||
document.appendChild(html);
|
||||
window.__auxclickEvents = [];
|
||||
link.addEventListener('auxclick', event => {
|
||||
__auxclickEvents.push(`${event.type}:${event.button}:${event.isTrusted}`);
|
||||
if (event.button === 1) window.open('/listener-target', '_blank');
|
||||
}); 'installed'"#,
|
||||
)
|
||||
.expect("auxclick listener should install");
|
||||
|
||||
vm.dispatch_mouse_event_at_point(20.0, 20.0, "mousedown", 1, None, 0.0, 0.0)
|
||||
.expect("middle mousedown should dispatch");
|
||||
let outcome = vm
|
||||
.dispatch_mouse_event_at_point(20.0, 20.0, "mouseup", 1, None, 0.0, 0.0)
|
||||
.expect("middle mouseup should dispatch");
|
||||
assert!(outcome.handled);
|
||||
assert!(!outcome.triggered_top_level_navigation);
|
||||
assert_eq!(
|
||||
vm.eval("__auxclickEvents.join('|')")
|
||||
.expect("auxclick event log should evaluate"),
|
||||
"auxclick:1:true"
|
||||
);
|
||||
|
||||
let popups = vm.take_pending_popup_activations();
|
||||
assert_eq!(
|
||||
popups.iter().map(|popup| popup.url()).collect::<Vec<_>>(),
|
||||
vec![
|
||||
"https://middle-auxclick.test/listener-target",
|
||||
"https://middle-auxclick.test/anchor-target",
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
popups
|
||||
.iter()
|
||||
.all(|popup| { popup.disposition() == crate::RendererPopupDisposition::Background })
|
||||
);
|
||||
assert!(vm.take_pending_location_navigation_with_seed().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canceling_middle_auxclick_suppresses_the_anchor_default_action() {
|
||||
let mut vm = new_storage_test_vm("https://middle-auxclick-canceled.test/path/index.html");
|
||||
vm.eval(
|
||||
r#"const html = document.createElement('html');
|
||||
const body = document.createElement('body');
|
||||
body.style.margin = '0';
|
||||
const link = document.createElement('a');
|
||||
link.href = '/anchor-target';
|
||||
link.style.cssText = 'display:block;width:100px;height:100px';
|
||||
link.addEventListener('auxclick', event => event.preventDefault());
|
||||
body.appendChild(link);
|
||||
html.appendChild(body);
|
||||
document.appendChild(html);
|
||||
'installed'"#,
|
||||
)
|
||||
.expect("canceling auxclick listener should install");
|
||||
|
||||
vm.dispatch_mouse_event_at_point(20.0, 20.0, "mousedown", 1, None, 0.0, 0.0)
|
||||
.expect("middle mousedown should dispatch");
|
||||
vm.dispatch_mouse_event_at_point(20.0, 20.0, "mouseup", 1, None, 0.0, 0.0)
|
||||
.expect("middle mouseup should dispatch");
|
||||
|
||||
assert!(vm.take_pending_popup_activations().is_empty());
|
||||
assert!(vm.take_pending_location_navigation_with_seed().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_key_window_open_inherits_the_real_input_modifiers() {
|
||||
#[cfg(target_os = "macos")]
|
||||
let platform_modifier = 4;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let platform_modifier = 2;
|
||||
|
||||
let mut vm = new_storage_test_vm("https://enter-window-open.test/path/index.html");
|
||||
vm.eval(
|
||||
r#"const html = document.createElement('html');
|
||||
const body = document.createElement('body');
|
||||
const button = document.createElement('button');
|
||||
body.appendChild(button);
|
||||
html.appendChild(body);
|
||||
document.appendChild(html);
|
||||
button.addEventListener('click', () => window.open('/target', '_blank'));
|
||||
button.focus();
|
||||
'installed'"#,
|
||||
)
|
||||
.expect("Enter activation fixture should initialize");
|
||||
|
||||
vm.dispatch_key_event(
|
||||
"keydown",
|
||||
"Enter",
|
||||
"Enter",
|
||||
"",
|
||||
platform_modifier,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.expect("new-tab-modified Enter should dispatch");
|
||||
|
||||
let popups = vm.take_pending_popup_activations();
|
||||
assert_eq!(popups.len(), 1);
|
||||
assert_eq!(
|
||||
popups[0].disposition(),
|
||||
crate::RendererPopupDisposition::Background
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyboard_activation_click_preserves_modifiers() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
|
||||
Reference in New Issue
Block a user