//! Phase C1 DOM event mapper tests. //! //! Pure-function coverage — runs natively (no wasm-bindgen-test); the //! C2 listener integration test runs in browsers as part of Phase E //! manual smoke. The Instant-using tests rely on `Instant::now()` //! being available natively; on wasm32-unknown-unknown that call //! panics, which is exactly why the mapper itself takes `timestamp` //! as a parameter (the C2 listener supplies a polyfill). use op_editor_ui::{ ImeKind, KeyCode, KeyLocation, KeyState, KeyValue, Modifiers, NamedKey, ScrollMode, }; use op_host_web::event::{ focus::map_focus, ime::{composition_end, composition_start, composition_update, utf16_selection_to_utf8}, keyboard::map_keyboard_parts, pointer::map_wheel, }; use std::time::Instant; // --------------------------------------------------------------------- // Keyboard // --------------------------------------------------------------------- #[test] fn keyboard_mapping_preserves_key_code_location_and_repeat() { let event = map_keyboard_parts("a", "KeyA", 0, true, true, Modifiers::SHIFT, false); assert_eq!(event.key, KeyValue::Char('a')); assert_eq!(event.code, KeyCode::KeyA); assert_eq!(event.location, KeyLocation::Standard); assert_eq!(event.state, KeyState::Pressed); assert!(event.repeat); assert!(!event.is_composing); assert!(event.modifiers.contains(Modifiers::SHIFT)); } #[test] fn named_key_mapping_handles_escape() { let event = map_keyboard_parts( "Escape", "Escape", 0, false, false, Modifiers::empty(), false, ); assert_eq!(event.key, KeyValue::Named(NamedKey::Escape)); assert_eq!(event.code, KeyCode::Escape); assert_eq!(event.state, KeyState::Released); assert!(!event.repeat); } #[test] fn keyboard_location_maps_left_right_numpad() { let l = map_keyboard_parts( "Shift", "ShiftLeft", 1, false, true, Modifiers::empty(), false, ); assert_eq!(l.location, KeyLocation::Left); let r = map_keyboard_parts( "Shift", "ShiftRight", 2, false, true, Modifiers::empty(), false, ); assert_eq!(r.location, KeyLocation::Right); let n = map_keyboard_parts("1", "Numpad1", 3, false, true, Modifiers::empty(), false); assert_eq!(n.location, KeyLocation::Numpad); } #[test] fn keyboard_is_composing_propagates() { let composing = map_keyboard_parts( "a", "KeyA", 0, false, true, Modifiers::empty(), true, // is_composing ); assert!(composing.is_composing); } #[test] fn keyboard_unidentified_for_multi_char_keys() { // Multi-codepoint key value (e.g. some IME pre-edit handle) falls // through to Unidentified; widget code can decide what to do. let event = map_keyboard_parts( "Compose", "Pause", 0, false, true, Modifiers::empty(), false, ); assert_eq!(event.key, KeyValue::Unidentified("Compose".to_string())); // `Pause` isn't in the table, so the mapper falls back to Unknown. assert_eq!(event.code, KeyCode::Unknown("Pause".to_string())); } #[test] fn keyboard_navigation_keys_mapped() { // Codex Phase C gate Round 3 BLOCK: every NamedKey/KeyCode variant // jian exposes must round-trip through the mapper rather than // falling to Unidentified / Unknown(...). for (k, expected) in [ ("Home", NamedKey::Home), ("End", NamedKey::End), ("PageUp", NamedKey::PageUp), ("PageDown", NamedKey::PageDown), ("CapsLock", NamedKey::CapsLock), ] { let event = map_keyboard_parts(k, k, 0, false, true, Modifiers::empty(), false); assert_eq!(event.key, KeyValue::Named(expected), "key {k}"); } } #[test] fn keyboard_function_keys_mapped() { use op_editor_ui::NamedKey::*; for (k, expected) in [ ("F1", F1), ("F2", F2), ("F3", F3), ("F4", F4), ("F5", F5), ("F6", F6), ("F7", F7), ("F8", F8), ("F9", F9), ("F10", F10), ("F11", F11), ("F12", F12), ] { let event = map_keyboard_parts(k, k, 0, false, true, Modifiers::empty(), false); assert_eq!(event.key, KeyValue::Named(expected), "fn-key {k}"); } } #[test] fn keyboard_modifier_keys_mapped_to_named_values() { // W3C `key` for modifier keys themselves is just the bare name; // `code` carries left/right ("ShiftLeft" etc) which map_key_code // covers. Verify both halves round-trip. for (k, c, expected_key, expected_code) in [ ("Shift", "ShiftLeft", NamedKey::Shift, KeyCode::ShiftLeft), ("Shift", "ShiftRight", NamedKey::Shift, KeyCode::ShiftRight), ( "Control", "ControlLeft", NamedKey::Control, KeyCode::ControlLeft, ), ( "Control", "ControlRight", NamedKey::Control, KeyCode::ControlRight, ), ("Alt", "AltLeft", NamedKey::Alt, KeyCode::AltLeft), ("Alt", "AltRight", NamedKey::Alt, KeyCode::AltRight), ("Meta", "MetaLeft", NamedKey::Meta, KeyCode::MetaLeft), ("Meta", "MetaRight", NamedKey::Meta, KeyCode::MetaRight), ] { let event = map_keyboard_parts(k, c, 0, false, true, Modifiers::empty(), false); assert_eq!(event.key, KeyValue::Named(expected_key), "mod key {k}"); assert_eq!(event.code, expected_code, "mod code {c}"); } } // --------------------------------------------------------------------- // IME // --------------------------------------------------------------------- #[test] fn composition_start_carries_empty_text() { let start = composition_start(); assert_eq!(start.kind, ImeKind::CompositionStart); assert_eq!(start.text, ""); } #[test] fn composition_update_remaps_utf16_selection_to_utf8_for_cjk() { // `你好` = 2 CJK chars; each is 1 UTF-16 code unit + 3 UTF-8 bytes. // Browser-supplied selection 0..2 (UTF-16 code units) must remap // to 0..6 (UTF-8 bytes). let update = composition_update("你好".to_string(), Some(0..2)); match update.kind { ImeKind::CompositionUpdate { selection } => assert_eq!(selection, Some(0..6)), other => panic!("expected CompositionUpdate, got {other:?}"), } } #[test] fn composition_update_remaps_surrogate_pair() { // 🙂 = U+1F642 = 2 UTF-16 code units (surrogate pair) + 4 UTF-8 bytes. // Selection 0..2 (UTF-16) must remap to 0..4 (UTF-8). let update = composition_update("🙂".to_string(), Some(0..2)); match update.kind { ImeKind::CompositionUpdate { selection } => assert_eq!(selection, Some(0..4)), other => panic!("expected CompositionUpdate, got {other:?}"), } } #[test] fn composition_update_handles_partial_selection() { // `aé` = 1 ASCII char (1 UTF-16 / 1 UTF-8) + 1 Latin-1 char (1 UTF-16 / 2 UTF-8). // Selection 1..2 (UTF-16) covers just the `é` — must remap to 1..3 (UTF-8). let update = composition_update("aé".to_string(), Some(1..2)); match update.kind { ImeKind::CompositionUpdate { selection } => assert_eq!(selection, Some(1..3)), other => panic!("expected CompositionUpdate, got {other:?}"), } } #[test] fn composition_update_no_selection_passthrough() { let update = composition_update("hello".to_string(), None); match update.kind { ImeKind::CompositionUpdate { selection } => assert_eq!(selection, None), other => panic!("expected CompositionUpdate, got {other:?}"), } assert_eq!(update.text, "hello"); } #[test] // The reversed `3..1` range is the deliberate test input — the mapper // must reject a misordered selection and return `None`. #[allow(clippy::reversed_empty_ranges)] fn composition_update_misordered_selection_returns_none() { let update = composition_update("hello".to_string(), Some(3..1)); match update.kind { ImeKind::CompositionUpdate { selection } => assert_eq!(selection, None), other => panic!("expected CompositionUpdate, got {other:?}"), } } #[test] fn composition_end_carries_commit_text() { let end = composition_end("你好".to_string()); assert_eq!(end.kind, ImeKind::CompositionEnd); assert_eq!(end.text, "你好"); } #[test] fn utf16_helper_clamps_out_of_range_to_str_len() { // Selection past the end of the string falls back to text.len(). let s = utf16_selection_to_utf8("abc", Some(0..100)); assert_eq!(s, Some(0..3)); } #[test] fn utf16_helper_handles_empty_text_and_zero_length_selection() { // Empty text + 0..0 selection — both bounds clamp to 0 (= text.len()). let s = utf16_selection_to_utf8("", Some(0..0)); assert_eq!(s, Some(0..0)); // Zero-length selection inside non-empty text at offset 1. let mid = utf16_selection_to_utf8("abc", Some(1..1)); assert_eq!(mid, Some(1..1)); } #[test] fn composition_update_zero_length_selection_in_cjk() { // `你好` — caret at UTF-16 offset 1 (between 你 and 好) maps to // UTF-8 byte 3. let update = composition_update("你好".to_string(), Some(1..1)); match update.kind { ImeKind::CompositionUpdate { selection } => assert_eq!(selection, Some(3..3)), other => panic!("expected CompositionUpdate, got {other:?}"), } } #[test] fn keyboard_empty_string_key_falls_to_unidentified() { // The empty string can show up on certain dead-key sequences in // some browsers; Phase C1 routes it to `Unidentified("")` rather // than panicking. let event = map_keyboard_parts("", "", 0, false, true, Modifiers::empty(), false); assert_eq!(event.key, KeyValue::Unidentified(String::new())); assert_eq!(event.code, KeyCode::Unknown(String::new())); } // --------------------------------------------------------------------- // Wheel (pointer.rs) // --------------------------------------------------------------------- #[test] fn wheel_flips_w3c_deltay_sign() { // W3C scroll-down (+120) → Jian winit-positive-up (-120). let pos = jian_core::geometry::Point::new(0.0, 0.0); let event = map_wheel(pos, 0.0, 120.0, 0.0, 0, Modifiers::empty(), Instant::now()); assert_eq!(event.delta.y, -120.0); assert_eq!(event.delta.x, 0.0); assert_eq!(event.mode, ScrollMode::Pixel); } #[test] fn wheel_does_not_flip_x_sign() { // delta_x is positive-right on both W3C and winit — no flip. let pos = jian_core::geometry::Point::new(10.0, 20.0); let event = map_wheel(pos, 5.0, 0.0, 0.0, 0, Modifiers::empty(), Instant::now()); assert_eq!(event.delta.x, 5.0); } #[test] fn wheel_mode_decoding() { let pos = jian_core::geometry::Point::new(0.0, 0.0); let pixel = map_wheel(pos, 0.0, 1.0, 0.0, 0, Modifiers::empty(), Instant::now()); assert_eq!(pixel.mode, ScrollMode::Pixel); let line = map_wheel(pos, 0.0, 1.0, 0.0, 1, Modifiers::empty(), Instant::now()); assert_eq!(line.mode, ScrollMode::Line); let page = map_wheel(pos, 0.0, 1.0, 0.0, 2, Modifiers::empty(), Instant::now()); assert_eq!(page.mode, ScrollMode::Page); } #[test] fn wheel_delta_z_passthrough() { let pos = jian_core::geometry::Point::new(0.0, 0.0); let event = map_wheel(pos, 0.0, 0.0, 1.5, 0, Modifiers::empty(), Instant::now()); assert_eq!(event.delta_z, 1.5); } // --------------------------------------------------------------------- // Focus // --------------------------------------------------------------------- #[test] fn focus_carries_node_hints() { let event = map_focus(true, Some(42), Some(43)); assert!(event.gained); assert_eq!(event.node_id_hint, Some(42)); assert_eq!(event.related_node_id_hint, Some(43)); } #[test] fn focus_loss_with_no_related_target() { // Window-level blur — no relatedTarget, no node hint. let event = map_focus(false, None, None); assert!(!event.gained); assert_eq!(event.node_id_hint, None); assert_eq!(event.related_node_id_hint, None); }