we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement DOM event system: addEventListener, dispatch, bubbling

Implements the W3C DOM event model connecting JavaScript event handlers
to the DOM tree with full capture/target/bubble phase propagation.

- Event constructor: new Event(type, options) with bubbles/cancelable
- Event methods: preventDefault, stopPropagation, stopImmediatePropagation
- EventTarget methods on all node wrappers and document:
addEventListener, removeEventListener, dispatchEvent
- Event propagation: capture phase -> at-target -> bubble phase
- Support for capture option (boolean or options object)
- Support for once option (auto-remove after first invocation)
- Event listener GcRefs registered as GC roots
- 20 new tests covering all acceptance criteria

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+1083
+1004
crates/js/src/dom_bridge.rs
··· 522 522 ("replaceChild", node_replace_child), 523 523 ("cloneNode", node_clone_node), 524 524 ("hasChildNodes", node_has_child_nodes), 525 + ("addEventListener", event_target_add_listener), 526 + ("removeEventListener", event_target_remove_listener), 527 + ("dispatchEvent", event_target_dispatch_event), 525 528 ]; 526 529 for &(name, callback) in node_methods { 527 530 let func = make_native(gc, name, callback); ··· 1498 1501 result 1499 1502 } 1500 1503 1504 + // ── Event system ──────────────────────────────────────────────────── 1505 + 1506 + // Internal property keys for Event objects. 1507 + const EVENT_TYPE_KEY: &str = "__event_type__"; 1508 + const EVENT_BUBBLES_KEY: &str = "__event_bubbles__"; 1509 + const EVENT_CANCELABLE_KEY: &str = "__event_cancelable__"; 1510 + const EVENT_STOP_PROP_KEY: &str = "__event_stop_prop__"; 1511 + const EVENT_STOP_IMMEDIATE_KEY: &str = "__event_stop_immediate__"; 1512 + const EVENT_DEFAULT_PREVENTED_KEY: &str = "__event_default_prevented__"; 1513 + const EVENT_PHASE_KEY: &str = "__event_phase__"; 1514 + 1515 + /// Create an Event JS object with the given type string and options. 1516 + fn create_event_object( 1517 + gc: &mut Gc<HeapObject>, 1518 + event_type: &str, 1519 + bubbles: bool, 1520 + cancelable: bool, 1521 + ) -> GcRef { 1522 + let mut data = ObjectData::new(); 1523 + 1524 + // Public properties. 1525 + data.properties.insert( 1526 + "type".to_string(), 1527 + Property::data(Value::String(event_type.to_string())), 1528 + ); 1529 + data.properties.insert( 1530 + "bubbles".to_string(), 1531 + Property::data(Value::Boolean(bubbles)), 1532 + ); 1533 + data.properties.insert( 1534 + "cancelable".to_string(), 1535 + Property::data(Value::Boolean(cancelable)), 1536 + ); 1537 + data.properties.insert( 1538 + "defaultPrevented".to_string(), 1539 + Property::data(Value::Boolean(false)), 1540 + ); 1541 + data.properties 1542 + .insert("eventPhase".to_string(), Property::data(Value::Number(0.0))); 1543 + data.properties 1544 + .insert("target".to_string(), Property::data(Value::Null)); 1545 + data.properties 1546 + .insert("currentTarget".to_string(), Property::data(Value::Null)); 1547 + data.properties 1548 + .insert("timeStamp".to_string(), Property::data(Value::Number(0.0))); 1549 + 1550 + // Internal state. 1551 + data.properties.insert( 1552 + EVENT_TYPE_KEY.to_string(), 1553 + Property::builtin(Value::String(event_type.to_string())), 1554 + ); 1555 + data.properties.insert( 1556 + EVENT_BUBBLES_KEY.to_string(), 1557 + Property::builtin(Value::Boolean(bubbles)), 1558 + ); 1559 + data.properties.insert( 1560 + EVENT_CANCELABLE_KEY.to_string(), 1561 + Property::builtin(Value::Boolean(cancelable)), 1562 + ); 1563 + data.properties.insert( 1564 + EVENT_STOP_PROP_KEY.to_string(), 1565 + Property::builtin(Value::Boolean(false)), 1566 + ); 1567 + data.properties.insert( 1568 + EVENT_STOP_IMMEDIATE_KEY.to_string(), 1569 + Property::builtin(Value::Boolean(false)), 1570 + ); 1571 + data.properties.insert( 1572 + EVENT_DEFAULT_PREVENTED_KEY.to_string(), 1573 + Property::builtin(Value::Boolean(false)), 1574 + ); 1575 + data.properties.insert( 1576 + EVENT_PHASE_KEY.to_string(), 1577 + Property::builtin(Value::Number(0.0)), 1578 + ); 1579 + 1580 + // Event phase constants. 1581 + data.properties 1582 + .insert("NONE".to_string(), Property::builtin(Value::Number(0.0))); 1583 + data.properties.insert( 1584 + "CAPTURING_PHASE".to_string(), 1585 + Property::builtin(Value::Number(1.0)), 1586 + ); 1587 + data.properties.insert( 1588 + "AT_TARGET".to_string(), 1589 + Property::builtin(Value::Number(2.0)), 1590 + ); 1591 + data.properties.insert( 1592 + "BUBBLING_PHASE".to_string(), 1593 + Property::builtin(Value::Number(3.0)), 1594 + ); 1595 + 1596 + let event_ref = gc.alloc(HeapObject::Object(data)); 1597 + 1598 + // Register methods. 1599 + let methods: &[NativeMethod] = &[ 1600 + ("preventDefault", event_prevent_default), 1601 + ("stopPropagation", event_stop_propagation), 1602 + ("stopImmediatePropagation", event_stop_immediate_propagation), 1603 + ]; 1604 + for &(name, callback) in methods { 1605 + let func = make_native(gc, name, callback); 1606 + set_builtin_prop(gc, event_ref, name, Value::Function(func)); 1607 + } 1608 + 1609 + event_ref 1610 + } 1611 + 1612 + /// `Event` constructor: `new Event(type, options)`. 1613 + fn event_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1614 + let event_type = args 1615 + .first() 1616 + .map(|v| v.to_js_string(ctx.gc)) 1617 + .unwrap_or_default(); 1618 + 1619 + let mut bubbles = false; 1620 + let mut cancelable = false; 1621 + 1622 + // Parse options object if provided. 1623 + if let Some(Value::Object(opts_ref)) = args.get(1) { 1624 + if let Some(HeapObject::Object(opts)) = ctx.gc.get(*opts_ref) { 1625 + if let Some(prop) = opts.properties.get("bubbles") { 1626 + bubbles = prop.value.to_boolean(); 1627 + } 1628 + if let Some(prop) = opts.properties.get("cancelable") { 1629 + cancelable = prop.value.to_boolean(); 1630 + } 1631 + } 1632 + } 1633 + 1634 + let event_ref = create_event_object(ctx.gc, &event_type, bubbles, cancelable); 1635 + Ok(Value::Object(event_ref)) 1636 + } 1637 + 1638 + fn event_prevent_default(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1639 + if let Value::Object(r) = &ctx.this { 1640 + // Only prevent if cancelable. 1641 + let cancelable = match ctx.gc.get(*r) { 1642 + Some(HeapObject::Object(data)) => data 1643 + .properties 1644 + .get(EVENT_CANCELABLE_KEY) 1645 + .map(|p| p.value.to_boolean()) 1646 + .unwrap_or(false), 1647 + _ => false, 1648 + }; 1649 + if cancelable { 1650 + set_builtin_prop( 1651 + ctx.gc, 1652 + *r, 1653 + EVENT_DEFAULT_PREVENTED_KEY, 1654 + Value::Boolean(true), 1655 + ); 1656 + set_builtin_prop(ctx.gc, *r, "defaultPrevented", Value::Boolean(true)); 1657 + } 1658 + } 1659 + Ok(Value::Undefined) 1660 + } 1661 + 1662 + fn event_stop_propagation(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 1663 + if let Value::Object(r) = &ctx.this { 1664 + set_builtin_prop(ctx.gc, *r, EVENT_STOP_PROP_KEY, Value::Boolean(true)); 1665 + } 1666 + Ok(Value::Undefined) 1667 + } 1668 + 1669 + fn event_stop_immediate_propagation( 1670 + _args: &[Value], 1671 + ctx: &mut NativeContext, 1672 + ) -> Result<Value, RuntimeError> { 1673 + if let Value::Object(r) = &ctx.this { 1674 + set_builtin_prop(ctx.gc, *r, EVENT_STOP_PROP_KEY, Value::Boolean(true)); 1675 + set_builtin_prop(ctx.gc, *r, EVENT_STOP_IMMEDIATE_KEY, Value::Boolean(true)); 1676 + } 1677 + Ok(Value::Undefined) 1678 + } 1679 + 1680 + // ── EventTarget methods ───────────────────────────────────────────── 1681 + 1682 + fn event_target_add_listener( 1683 + args: &[Value], 1684 + ctx: &mut NativeContext, 1685 + ) -> Result<Value, RuntimeError> { 1686 + let bridge = ctx 1687 + .dom_bridge 1688 + .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 1689 + let node_id = match &ctx.this { 1690 + Value::Object(r) => get_node_id(ctx.gc, *r), 1691 + _ => None, 1692 + } 1693 + .ok_or_else(|| RuntimeError::type_error("addEventListener called on non-node"))?; 1694 + 1695 + let event_type = args 1696 + .first() 1697 + .map(|v| v.to_js_string(ctx.gc)) 1698 + .unwrap_or_default(); 1699 + 1700 + let callback_ref = match args.get(1) { 1701 + Some(Value::Function(r)) => *r, 1702 + _ => return Ok(Value::Undefined), // Silently ignore non-function 1703 + }; 1704 + 1705 + let mut capture = false; 1706 + let mut once = false; 1707 + 1708 + // Third arg: boolean (capture) or options object. 1709 + if let Some(arg) = args.get(2) { 1710 + match arg { 1711 + Value::Boolean(b) => capture = *b, 1712 + Value::Object(opts_ref) => { 1713 + if let Some(HeapObject::Object(opts)) = ctx.gc.get(*opts_ref) { 1714 + if let Some(prop) = opts.properties.get("capture") { 1715 + capture = prop.value.to_boolean(); 1716 + } 1717 + if let Some(prop) = opts.properties.get("once") { 1718 + once = prop.value.to_boolean(); 1719 + } 1720 + } 1721 + } 1722 + _ => {} 1723 + } 1724 + } 1725 + 1726 + let idx = node_id.index(); 1727 + let mut listeners = bridge.event_listeners.borrow_mut(); 1728 + let list = listeners.entry(idx).or_default(); 1729 + 1730 + // Don't add duplicate: same type, same callback ref, same capture. 1731 + let already_exists = list 1732 + .iter() 1733 + .any(|l| l.event_type == event_type && l.callback == callback_ref && l.capture == capture); 1734 + if !already_exists { 1735 + list.push(EventListener { 1736 + event_type, 1737 + callback: callback_ref, 1738 + capture, 1739 + once, 1740 + }); 1741 + } 1742 + 1743 + Ok(Value::Undefined) 1744 + } 1745 + 1746 + fn event_target_remove_listener( 1747 + args: &[Value], 1748 + ctx: &mut NativeContext, 1749 + ) -> Result<Value, RuntimeError> { 1750 + let bridge = ctx 1751 + .dom_bridge 1752 + .ok_or_else(|| RuntimeError::type_error("no document attached"))?; 1753 + let node_id = match &ctx.this { 1754 + Value::Object(r) => get_node_id(ctx.gc, *r), 1755 + _ => None, 1756 + } 1757 + .ok_or_else(|| RuntimeError::type_error("removeEventListener called on non-node"))?; 1758 + 1759 + let event_type = args 1760 + .first() 1761 + .map(|v| v.to_js_string(ctx.gc)) 1762 + .unwrap_or_default(); 1763 + 1764 + let callback_ref = match args.get(1) { 1765 + Some(Value::Function(r)) => *r, 1766 + _ => return Ok(Value::Undefined), 1767 + }; 1768 + 1769 + let mut capture = false; 1770 + if let Some(arg) = args.get(2) { 1771 + match arg { 1772 + Value::Boolean(b) => capture = *b, 1773 + Value::Object(opts_ref) => { 1774 + if let Some(HeapObject::Object(opts)) = ctx.gc.get(*opts_ref) { 1775 + if let Some(prop) = opts.properties.get("capture") { 1776 + capture = prop.value.to_boolean(); 1777 + } 1778 + } 1779 + } 1780 + _ => {} 1781 + } 1782 + } 1783 + 1784 + let idx = node_id.index(); 1785 + let mut listeners = bridge.event_listeners.borrow_mut(); 1786 + if let Some(list) = listeners.get_mut(&idx) { 1787 + list.retain(|l| { 1788 + !(l.event_type == event_type && l.callback == callback_ref && l.capture == capture) 1789 + }); 1790 + } 1791 + 1792 + Ok(Value::Undefined) 1793 + } 1794 + 1795 + /// Native `dispatchEvent` — returns a marker for the VM to handle. 1796 + fn event_target_dispatch_event( 1797 + args: &[Value], 1798 + ctx: &mut NativeContext, 1799 + ) -> Result<Value, RuntimeError> { 1800 + let node_id = match &ctx.this { 1801 + Value::Object(r) => get_node_id(ctx.gc, *r), 1802 + _ => None, 1803 + } 1804 + .ok_or_else(|| RuntimeError::type_error("dispatchEvent called on non-node"))?; 1805 + 1806 + let event_ref = match args.first() { 1807 + Some(Value::Object(r)) => *r, 1808 + _ => { 1809 + return Err(RuntimeError::type_error( 1810 + "dispatchEvent requires an Event argument", 1811 + )) 1812 + } 1813 + }; 1814 + 1815 + // Build a marker object for the VM to process. 1816 + let mut marker = ObjectData::new(); 1817 + marker.properties.insert( 1818 + "__event_dispatch__".to_string(), 1819 + Property::builtin(Value::Boolean(true)), 1820 + ); 1821 + marker.properties.insert( 1822 + "__target_id__".to_string(), 1823 + Property::builtin(Value::Number(node_id.index() as f64)), 1824 + ); 1825 + marker.properties.insert( 1826 + "__event_ref__".to_string(), 1827 + Property::builtin(Value::Object(event_ref)), 1828 + ); 1829 + 1830 + Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(marker)))) 1831 + } 1832 + 1833 + /// Register the `Event` constructor and event target methods on the VM. 1834 + pub fn init_event_system(vm: &mut Vm) { 1835 + // Register `Event` as a global constructor. 1836 + let ctor = make_native(&mut vm.gc, "Event", event_constructor); 1837 + vm.set_global("Event", Value::Function(ctor)); 1838 + 1839 + // Add event target methods to the document object. 1840 + if let Some(Value::Object(doc_ref)) = vm.get_global("document").cloned() { 1841 + let methods: &[NativeMethod] = &[ 1842 + ("addEventListener", event_target_add_listener), 1843 + ("removeEventListener", event_target_remove_listener), 1844 + ("dispatchEvent", event_target_dispatch_event), 1845 + ]; 1846 + for &(name, callback) in methods { 1847 + let func = make_native(&mut vm.gc, name, callback); 1848 + set_builtin_prop(&mut vm.gc, doc_ref, name, Value::Function(func)); 1849 + } 1850 + 1851 + // Ensure the document object has __node_id__ set to the root node. 1852 + if let Some(bridge) = &vm.dom_bridge { 1853 + let root_idx = bridge.document.borrow().root().index(); 1854 + set_builtin_prop( 1855 + &mut vm.gc, 1856 + doc_ref, 1857 + NODE_ID_KEY, 1858 + Value::Number(root_idx as f64), 1859 + ); 1860 + // Also cache the document wrapper. 1861 + bridge.node_wrappers.borrow_mut().insert(root_idx, doc_ref); 1862 + } 1863 + } 1864 + } 1865 + 1866 + /// Run the event dispatch algorithm. Called from the VM when it detects an 1867 + /// event dispatch marker returned by the native `dispatchEvent`. 1868 + /// 1869 + /// Returns the result of `!event.defaultPrevented`. 1870 + pub fn run_event_dispatch(vm: &mut Vm, target_idx: usize, event_ref: GcRef) -> Value { 1871 + let bridge = match vm.dom_bridge.clone() { 1872 + Some(b) => b, 1873 + None => return Value::Boolean(true), 1874 + }; 1875 + 1876 + let target_id = NodeId::from_index(target_idx); 1877 + 1878 + // Build propagation path: target -> ... -> root. 1879 + let path = { 1880 + let doc = bridge.document.borrow(); 1881 + let mut path = vec![target_id]; 1882 + let mut current = target_id; 1883 + while let Some(parent) = doc.parent(current) { 1884 + path.push(parent); 1885 + current = parent; 1886 + } 1887 + path.reverse(); // Now root -> ... -> target. 1888 + path 1889 + }; 1890 + 1891 + // Set event.target to the target wrapper. 1892 + let target_wrapper = get_or_create_wrapper(target_id, &mut vm.gc, &bridge, vm.object_prototype); 1893 + set_builtin_prop( 1894 + &mut vm.gc, 1895 + event_ref, 1896 + "target", 1897 + Value::Object(target_wrapper), 1898 + ); 1899 + 1900 + let target_pos = path.len() - 1; 1901 + 1902 + // --- Capture phase (root to target, excluding target) --- 1903 + set_builtin_prop(&mut vm.gc, event_ref, "eventPhase", Value::Number(1.0)); 1904 + set_builtin_prop(&mut vm.gc, event_ref, EVENT_PHASE_KEY, Value::Number(1.0)); 1905 + 1906 + for &node_id in &path[..target_pos] { 1907 + if is_propagation_stopped(&vm.gc, event_ref) { 1908 + break; 1909 + } 1910 + let wrapper = get_or_create_wrapper(node_id, &mut vm.gc, &bridge, vm.object_prototype); 1911 + set_builtin_prop( 1912 + &mut vm.gc, 1913 + event_ref, 1914 + "currentTarget", 1915 + Value::Object(wrapper), 1916 + ); 1917 + invoke_listeners(vm, &bridge, node_id, event_ref, wrapper, true); 1918 + } 1919 + 1920 + // --- At-target phase --- 1921 + if !is_propagation_stopped(&vm.gc, event_ref) { 1922 + set_builtin_prop(&mut vm.gc, event_ref, "eventPhase", Value::Number(2.0)); 1923 + set_builtin_prop(&mut vm.gc, event_ref, EVENT_PHASE_KEY, Value::Number(2.0)); 1924 + set_builtin_prop( 1925 + &mut vm.gc, 1926 + event_ref, 1927 + "currentTarget", 1928 + Value::Object(target_wrapper), 1929 + ); 1930 + // At target, fire BOTH capture and bubble listeners in registration order. 1931 + invoke_listeners(vm, &bridge, target_id, event_ref, target_wrapper, false); 1932 + } 1933 + 1934 + // --- Bubble phase (target to root, excluding target) --- 1935 + let bubbles = match vm.gc.get(event_ref) { 1936 + Some(HeapObject::Object(data)) => data 1937 + .properties 1938 + .get(EVENT_BUBBLES_KEY) 1939 + .map(|p| p.value.to_boolean()) 1940 + .unwrap_or(false), 1941 + _ => false, 1942 + }; 1943 + 1944 + if bubbles { 1945 + set_builtin_prop(&mut vm.gc, event_ref, "eventPhase", Value::Number(3.0)); 1946 + set_builtin_prop(&mut vm.gc, event_ref, EVENT_PHASE_KEY, Value::Number(3.0)); 1947 + 1948 + for &node_id in path[..target_pos].iter().rev() { 1949 + if is_propagation_stopped(&vm.gc, event_ref) { 1950 + break; 1951 + } 1952 + let wrapper = get_or_create_wrapper(node_id, &mut vm.gc, &bridge, vm.object_prototype); 1953 + set_builtin_prop( 1954 + &mut vm.gc, 1955 + event_ref, 1956 + "currentTarget", 1957 + Value::Object(wrapper), 1958 + ); 1959 + invoke_listeners(vm, &bridge, node_id, event_ref, wrapper, false); 1960 + } 1961 + } 1962 + 1963 + // Reset event state. 1964 + set_builtin_prop(&mut vm.gc, event_ref, "eventPhase", Value::Number(0.0)); 1965 + set_builtin_prop(&mut vm.gc, event_ref, EVENT_PHASE_KEY, Value::Number(0.0)); 1966 + set_builtin_prop(&mut vm.gc, event_ref, "currentTarget", Value::Null); 1967 + 1968 + // Return !defaultPrevented. 1969 + let default_prevented = match vm.gc.get(event_ref) { 1970 + Some(HeapObject::Object(data)) => data 1971 + .properties 1972 + .get(EVENT_DEFAULT_PREVENTED_KEY) 1973 + .map(|p| p.value.to_boolean()) 1974 + .unwrap_or(false), 1975 + _ => false, 1976 + }; 1977 + Value::Boolean(!default_prevented) 1978 + } 1979 + 1980 + fn is_propagation_stopped(gc: &Gc<HeapObject>, event_ref: GcRef) -> bool { 1981 + match gc.get(event_ref) { 1982 + Some(HeapObject::Object(data)) => data 1983 + .properties 1984 + .get(EVENT_STOP_PROP_KEY) 1985 + .map(|p| p.value.to_boolean()) 1986 + .unwrap_or(false), 1987 + _ => false, 1988 + } 1989 + } 1990 + 1991 + fn is_immediate_stopped(gc: &Gc<HeapObject>, event_ref: GcRef) -> bool { 1992 + match gc.get(event_ref) { 1993 + Some(HeapObject::Object(data)) => data 1994 + .properties 1995 + .get(EVENT_STOP_IMMEDIATE_KEY) 1996 + .map(|p| p.value.to_boolean()) 1997 + .unwrap_or(false), 1998 + _ => false, 1999 + } 2000 + } 2001 + 2002 + /// Invoke matching listeners on a node for the given event. 2003 + /// 2004 + /// When `capture_only` is true (capture phase), only capture listeners fire. 2005 + /// When false (at-target or bubble), at-target fires all listeners and 2006 + /// bubble fires only non-capture listeners. We distinguish by the event phase: 2007 + /// at-target (phase 2) fires all, bubble (phase 3) fires only non-capture. 2008 + fn invoke_listeners( 2009 + vm: &mut Vm, 2010 + bridge: &DomBridge, 2011 + node_id: NodeId, 2012 + event_ref: GcRef, 2013 + current_target_wrapper: GcRef, 2014 + capture_only: bool, 2015 + ) { 2016 + let event_type = match vm.gc.get(event_ref) { 2017 + Some(HeapObject::Object(data)) => match data.properties.get(EVENT_TYPE_KEY) { 2018 + Some(Property { 2019 + value: Value::String(s), 2020 + .. 2021 + }) => s.clone(), 2022 + _ => return, 2023 + }, 2024 + _ => return, 2025 + }; 2026 + 2027 + let phase = match vm.gc.get(event_ref) { 2028 + Some(HeapObject::Object(data)) => match data.properties.get(EVENT_PHASE_KEY) { 2029 + Some(Property { 2030 + value: Value::Number(n), 2031 + .. 2032 + }) => *n as u8, 2033 + _ => 0, 2034 + }, 2035 + _ => 0, 2036 + }; 2037 + 2038 + let idx = node_id.index(); 2039 + 2040 + // Collect matching listeners (snapshot the list to avoid borrow issues). 2041 + let matching: Vec<(GcRef, bool)> = { 2042 + let listeners = bridge.event_listeners.borrow(); 2043 + match listeners.get(&idx) { 2044 + Some(list) => list 2045 + .iter() 2046 + .filter(|l| { 2047 + if l.event_type != event_type { 2048 + return false; 2049 + } 2050 + if capture_only { 2051 + // Capture phase: only capture listeners. 2052 + l.capture 2053 + } else if phase == 2 { 2054 + // At-target: all listeners fire. 2055 + true 2056 + } else { 2057 + // Bubble phase: only non-capture listeners. 2058 + !l.capture 2059 + } 2060 + }) 2061 + .map(|l| (l.callback, l.once)) 2062 + .collect(), 2063 + None => return, 2064 + } 2065 + }; 2066 + 2067 + // Invoke each listener. 2068 + for (callback_ref, once) in &matching { 2069 + if is_immediate_stopped(&vm.gc, event_ref) { 2070 + break; 2071 + } 2072 + if is_propagation_stopped(&vm.gc, event_ref) && capture_only { 2073 + break; 2074 + } 2075 + 2076 + // Set `this` to currentTarget for the callback. 2077 + let old_this = vm.get_global("this").cloned(); 2078 + vm.set_global("this", Value::Object(current_target_wrapper)); 2079 + 2080 + let _ = vm.call_function(*callback_ref, &[Value::Object(event_ref)]); 2081 + 2082 + // Restore `this`. 2083 + match old_this { 2084 + Some(v) => { 2085 + vm.set_global("this", v); 2086 + } 2087 + None => { 2088 + vm.remove_global("this"); 2089 + } 2090 + } 2091 + 2092 + // Remove if once. 2093 + if *once { 2094 + let mut listeners = bridge.event_listeners.borrow_mut(); 2095 + if let Some(list) = listeners.get_mut(&idx) { 2096 + list.retain(|l| { 2097 + !(l.event_type == event_type && l.callback == *callback_ref && l.once) 2098 + }); 2099 + } 2100 + } 2101 + } 2102 + } 2103 + 1501 2104 // ── Tests ─────────────────────────────────────────────────────────── 1502 2105 1503 2106 #[cfg(test)] ··· 2334 2937 var c = document.getElementById("c"); 2335 2938 p.removeChild(c); 2336 2939 c.parentNode === null 2940 + "#, 2941 + ) 2942 + .unwrap(); 2943 + assert!(matches!(result, Value::Boolean(true))); 2944 + } 2945 + 2946 + // ── Event system tests ────────────────────────────────── 2947 + 2948 + #[test] 2949 + fn test_event_constructor() { 2950 + let result = eval_with_doc( 2951 + "<html><body></body></html>", 2952 + r#" 2953 + var e = new Event("click"); 2954 + e.type 2955 + "#, 2956 + ) 2957 + .unwrap(); 2958 + match result { 2959 + Value::String(s) => assert_eq!(s, "click"), 2960 + v => panic!("expected 'click', got {v:?}"), 2961 + } 2962 + } 2963 + 2964 + #[test] 2965 + fn test_event_constructor_options() { 2966 + let result = eval_with_doc( 2967 + "<html><body></body></html>", 2968 + r#" 2969 + var e = new Event("click", { bubbles: true, cancelable: true }); 2970 + e.bubbles + "," + e.cancelable 2971 + "#, 2972 + ) 2973 + .unwrap(); 2974 + match result { 2975 + Value::String(s) => assert_eq!(s, "true,true"), 2976 + v => panic!("expected 'true,true', got {v:?}"), 2977 + } 2978 + } 2979 + 2980 + #[test] 2981 + fn test_event_default_properties() { 2982 + let result = eval_with_doc( 2983 + "<html><body></body></html>", 2984 + r#" 2985 + var e = new Event("test"); 2986 + e.bubbles + "," + e.cancelable + "," + e.defaultPrevented + "," + e.eventPhase 2987 + "#, 2988 + ) 2989 + .unwrap(); 2990 + match result { 2991 + Value::String(s) => assert_eq!(s, "false,false,false,0"), 2992 + v => panic!("expected 'false,false,false,0', got {v:?}"), 2993 + } 2994 + } 2995 + 2996 + #[test] 2997 + fn test_add_event_listener_and_dispatch() { 2998 + let result = eval_with_doc( 2999 + r#"<html><body><div id="d"></div></body></html>"#, 3000 + r#" 3001 + var called = false; 3002 + var d = document.getElementById("d"); 3003 + d.addEventListener("click", function(e) { 3004 + called = true; 3005 + }); 3006 + d.dispatchEvent(new Event("click")); 3007 + called 3008 + "#, 3009 + ) 3010 + .unwrap(); 3011 + assert!(matches!(result, Value::Boolean(true))); 3012 + } 3013 + 3014 + #[test] 3015 + fn test_event_handler_receives_event_object() { 3016 + let result = eval_with_doc( 3017 + r#"<html><body><div id="d"></div></body></html>"#, 3018 + r#" 3019 + var eventType = ""; 3020 + var d = document.getElementById("d"); 3021 + d.addEventListener("myevent", function(e) { 3022 + eventType = e.type; 3023 + }); 3024 + d.dispatchEvent(new Event("myevent")); 3025 + eventType 3026 + "#, 3027 + ) 3028 + .unwrap(); 3029 + match result { 3030 + Value::String(s) => assert_eq!(s, "myevent"), 3031 + v => panic!("expected 'myevent', got {v:?}"), 3032 + } 3033 + } 3034 + 3035 + #[test] 3036 + fn test_event_target_set_correctly() { 3037 + let result = eval_with_doc( 3038 + r#"<html><body><div id="d"></div></body></html>"#, 3039 + r#" 3040 + var targetTag = ""; 3041 + var d = document.getElementById("d"); 3042 + d.addEventListener("click", function(e) { 3043 + targetTag = e.target.tagName; 3044 + }); 3045 + d.dispatchEvent(new Event("click")); 3046 + targetTag 3047 + "#, 3048 + ) 3049 + .unwrap(); 3050 + match result { 3051 + Value::String(s) => assert_eq!(s, "DIV"), 3052 + v => panic!("expected 'DIV', got {v:?}"), 3053 + } 3054 + } 3055 + 3056 + #[test] 3057 + fn test_event_bubbling() { 3058 + let result = eval_with_doc( 3059 + r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3060 + r#" 3061 + var order = ""; 3062 + var parent = document.getElementById("parent"); 3063 + var child = document.getElementById("child"); 3064 + parent.addEventListener("click", function(e) { 3065 + order = order + "parent"; 3066 + }); 3067 + child.addEventListener("click", function(e) { 3068 + order = order + "child,"; 3069 + }); 3070 + child.dispatchEvent(new Event("click", { bubbles: true })); 3071 + order 3072 + "#, 3073 + ) 3074 + .unwrap(); 3075 + match result { 3076 + Value::String(s) => assert_eq!(s, "child,parent"), 3077 + v => panic!("expected 'child,parent', got {v:?}"), 3078 + } 3079 + } 3080 + 3081 + #[test] 3082 + fn test_event_no_bubbling_by_default() { 3083 + let result = eval_with_doc( 3084 + r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3085 + r#" 3086 + var parentCalled = false; 3087 + var parent = document.getElementById("parent"); 3088 + var child = document.getElementById("child"); 3089 + parent.addEventListener("test", function(e) { 3090 + parentCalled = true; 3091 + }); 3092 + child.dispatchEvent(new Event("test")); 3093 + parentCalled 3094 + "#, 3095 + ) 3096 + .unwrap(); 3097 + assert!(matches!(result, Value::Boolean(false))); 3098 + } 3099 + 3100 + #[test] 3101 + fn test_event_capture_phase() { 3102 + let result = eval_with_doc( 3103 + r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3104 + r#" 3105 + var order = ""; 3106 + var parent = document.getElementById("parent"); 3107 + var child = document.getElementById("child"); 3108 + parent.addEventListener("click", function(e) { 3109 + order = order + "capture,"; 3110 + }, true); 3111 + parent.addEventListener("click", function(e) { 3112 + order = order + "bubble"; 3113 + }); 3114 + child.addEventListener("click", function(e) { 3115 + order = order + "target,"; 3116 + }); 3117 + child.dispatchEvent(new Event("click", { bubbles: true })); 3118 + order 3119 + "#, 3120 + ) 3121 + .unwrap(); 3122 + match result { 3123 + Value::String(s) => assert_eq!(s, "capture,target,bubble"), 3124 + v => panic!("expected 'capture,target,bubble', got {v:?}"), 3125 + } 3126 + } 3127 + 3128 + #[test] 3129 + fn test_stop_propagation() { 3130 + let result = eval_with_doc( 3131 + r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3132 + r#" 3133 + var parentCalled = false; 3134 + var parent = document.getElementById("parent"); 3135 + var child = document.getElementById("child"); 3136 + parent.addEventListener("click", function(e) { 3137 + parentCalled = true; 3138 + }); 3139 + child.addEventListener("click", function(e) { 3140 + e.stopPropagation(); 3141 + }); 3142 + child.dispatchEvent(new Event("click", { bubbles: true })); 3143 + parentCalled 3144 + "#, 3145 + ) 3146 + .unwrap(); 3147 + assert!(matches!(result, Value::Boolean(false))); 3148 + } 3149 + 3150 + #[test] 3151 + fn test_stop_immediate_propagation() { 3152 + let result = eval_with_doc( 3153 + r#"<html><body><div id="d"></div></body></html>"#, 3154 + r#" 3155 + var count = 0; 3156 + var d = document.getElementById("d"); 3157 + d.addEventListener("click", function(e) { 3158 + count = count + 1; 3159 + e.stopImmediatePropagation(); 3160 + }); 3161 + d.addEventListener("click", function(e) { 3162 + count = count + 1; 3163 + }); 3164 + d.dispatchEvent(new Event("click")); 3165 + count 3166 + "#, 3167 + ) 3168 + .unwrap(); 3169 + match result { 3170 + Value::Number(n) => assert_eq!(n, 1.0), 3171 + v => panic!("expected 1, got {v:?}"), 3172 + } 3173 + } 3174 + 3175 + #[test] 3176 + fn test_prevent_default() { 3177 + let result = eval_with_doc( 3178 + r#"<html><body><div id="d"></div></body></html>"#, 3179 + r#" 3180 + var d = document.getElementById("d"); 3181 + d.addEventListener("click", function(e) { 3182 + e.preventDefault(); 3183 + }); 3184 + var result = d.dispatchEvent(new Event("click", { cancelable: true })); 3185 + result 3186 + "#, 3187 + ) 3188 + .unwrap(); 3189 + // dispatchEvent returns false when preventDefault was called. 3190 + assert!(matches!(result, Value::Boolean(false))); 3191 + } 3192 + 3193 + #[test] 3194 + fn test_prevent_default_not_cancelable() { 3195 + let result = eval_with_doc( 3196 + r#"<html><body><div id="d"></div></body></html>"#, 3197 + r#" 3198 + var d = document.getElementById("d"); 3199 + d.addEventListener("click", function(e) { 3200 + e.preventDefault(); 3201 + }); 3202 + var result = d.dispatchEvent(new Event("click")); 3203 + result 3204 + "#, 3205 + ) 3206 + .unwrap(); 3207 + // Non-cancelable: preventDefault has no effect, returns true. 3208 + assert!(matches!(result, Value::Boolean(true))); 3209 + } 3210 + 3211 + #[test] 3212 + fn test_remove_event_listener() { 3213 + let result = eval_with_doc( 3214 + r#"<html><body><div id="d"></div></body></html>"#, 3215 + r#" 3216 + var count = 0; 3217 + var d = document.getElementById("d"); 3218 + var handler = function(e) { count = count + 1; }; 3219 + d.addEventListener("click", handler); 3220 + d.dispatchEvent(new Event("click")); 3221 + d.removeEventListener("click", handler); 3222 + d.dispatchEvent(new Event("click")); 3223 + count 3224 + "#, 3225 + ) 3226 + .unwrap(); 3227 + match result { 3228 + Value::Number(n) => assert_eq!(n, 1.0), 3229 + v => panic!("expected 1, got {v:?}"), 3230 + } 3231 + } 3232 + 3233 + #[test] 3234 + fn test_once_option() { 3235 + let result = eval_with_doc( 3236 + r#"<html><body><div id="d"></div></body></html>"#, 3237 + r#" 3238 + var count = 0; 3239 + var d = document.getElementById("d"); 3240 + d.addEventListener("click", function(e) { 3241 + count = count + 1; 3242 + }, { once: true }); 3243 + d.dispatchEvent(new Event("click")); 3244 + d.dispatchEvent(new Event("click")); 3245 + count 3246 + "#, 3247 + ) 3248 + .unwrap(); 3249 + match result { 3250 + Value::Number(n) => assert_eq!(n, 1.0), 3251 + v => panic!("expected 1, got {v:?}"), 3252 + } 3253 + } 3254 + 3255 + #[test] 3256 + fn test_dispatch_event_returns_true_normally() { 3257 + let result = eval_with_doc( 3258 + r#"<html><body><div id="d"></div></body></html>"#, 3259 + r#" 3260 + var d = document.getElementById("d"); 3261 + d.dispatchEvent(new Event("click")) 3262 + "#, 3263 + ) 3264 + .unwrap(); 3265 + assert!(matches!(result, Value::Boolean(true))); 3266 + } 3267 + 3268 + #[test] 3269 + fn test_event_current_target() { 3270 + let result = eval_with_doc( 3271 + r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3272 + r#" 3273 + var currentTag = ""; 3274 + var parent = document.getElementById("parent"); 3275 + var child = document.getElementById("child"); 3276 + parent.addEventListener("click", function(e) { 3277 + currentTag = e.currentTarget.tagName; 3278 + }); 3279 + child.dispatchEvent(new Event("click", { bubbles: true })); 3280 + currentTag 3281 + "#, 3282 + ) 3283 + .unwrap(); 3284 + match result { 3285 + Value::String(s) => assert_eq!(s, "DIV"), 3286 + v => panic!("expected 'DIV', got {v:?}"), 3287 + } 3288 + } 3289 + 3290 + #[test] 3291 + fn test_multiple_listeners_same_type() { 3292 + let result = eval_with_doc( 3293 + r#"<html><body><div id="d"></div></body></html>"#, 3294 + r#" 3295 + var order = ""; 3296 + var d = document.getElementById("d"); 3297 + d.addEventListener("click", function(e) { order = order + "a,"; }); 3298 + d.addEventListener("click", function(e) { order = order + "b"; }); 3299 + d.dispatchEvent(new Event("click")); 3300 + order 3301 + "#, 3302 + ) 3303 + .unwrap(); 3304 + match result { 3305 + Value::String(s) => assert_eq!(s, "a,b"), 3306 + v => panic!("expected 'a,b', got {v:?}"), 3307 + } 3308 + } 3309 + 3310 + #[test] 3311 + fn test_capture_option_object() { 3312 + let result = eval_with_doc( 3313 + r#"<html><body><div id="parent"><span id="child"></span></div></body></html>"#, 3314 + r#" 3315 + var capturePhase = false; 3316 + var parent = document.getElementById("parent"); 3317 + var child = document.getElementById("child"); 3318 + parent.addEventListener("click", function(e) { 3319 + capturePhase = (e.eventPhase === 1); 3320 + }, { capture: true }); 3321 + child.dispatchEvent(new Event("click", { bubbles: true })); 3322 + capturePhase 3323 + "#, 3324 + ) 3325 + .unwrap(); 3326 + assert!(matches!(result, Value::Boolean(true))); 3327 + } 3328 + 3329 + #[test] 3330 + fn test_document_add_event_listener() { 3331 + let result = eval_with_doc( 3332 + r#"<html><body><div id="d"></div></body></html>"#, 3333 + r#" 3334 + var docCalled = false; 3335 + document.addEventListener("click", function(e) { 3336 + docCalled = true; 3337 + }); 3338 + var d = document.getElementById("d"); 3339 + d.dispatchEvent(new Event("click", { bubbles: true })); 3340 + docCalled 2337 3341 "#, 2338 3342 ) 2339 3343 .unwrap();
+79
crates/js/src/vm.rs
··· 221 221 } 222 222 } 223 223 224 + /// A single event listener registered on a DOM node. 225 + pub struct EventListener { 226 + pub event_type: String, 227 + pub callback: GcRef, 228 + pub capture: bool, 229 + pub once: bool, 230 + } 231 + 224 232 /// Bridge between JS and the DOM. Holds a shared document and a cache 225 233 /// mapping `NodeId` indices to their JS wrapper `GcRef` so that the same 226 234 /// DOM node always returns the same JS object (identity). 227 235 pub struct DomBridge { 228 236 pub document: RefCell<Document>, 229 237 pub node_wrappers: RefCell<HashMap<usize, GcRef>>, 238 + /// Event listeners keyed by NodeId index. 239 + pub event_listeners: RefCell<HashMap<usize, Vec<EventListener>>>, 230 240 } 231 241 232 242 /// Context passed to native functions, providing GC access and `this` binding. ··· 829 839 let bridge = Rc::new(DomBridge { 830 840 document: RefCell::new(doc), 831 841 node_wrappers: RefCell::new(HashMap::new()), 842 + event_listeners: RefCell::new(HashMap::new()), 832 843 }); 833 844 self.dom_bridge = Some(bridge); 834 845 crate::dom_bridge::init_document_object(self); 846 + crate::dom_bridge::init_event_system(self); 835 847 } 836 848 837 849 /// Set an instruction limit. The VM will return a RuntimeError after ··· 1006 1018 } 1007 1019 return Ok(Value::Object(promise)); 1008 1020 } 1021 + 1022 + // Check for event dispatch marker. 1023 + let is_event_dispatch = matches!( 1024 + gc_get_property(&self.gc, *r, "__event_dispatch__"), 1025 + Value::Boolean(true) 1026 + ); 1027 + if is_event_dispatch { 1028 + let target_idx = match gc_get_property(&self.gc, *r, "__target_id__") { 1029 + Value::Number(n) => n as usize, 1030 + _ => return Ok(Value::Boolean(true)), 1031 + }; 1032 + let evt_ref = match gc_get_property(&self.gc, *r, "__event_ref__") { 1033 + Value::Object(er) => er, 1034 + _ => return Ok(Value::Boolean(true)), 1035 + }; 1036 + return Ok(crate::dom_bridge::run_event_dispatch( 1037 + self, target_idx, evt_ref, 1038 + )); 1039 + } 1009 1040 } 1010 1041 1011 1042 Ok(result) ··· 2023 2054 for &wrapper_ref in bridge.node_wrappers.borrow().values() { 2024 2055 roots.push(wrapper_ref); 2025 2056 } 2057 + // Event listener callbacks must also be GC roots. 2058 + for listeners in bridge.event_listeners.borrow().values() { 2059 + for listener in listeners { 2060 + roots.push(listener.callback); 2061 + } 2062 + } 2026 2063 } 2027 2064 roots 2028 2065 } ··· 2651 2688 Value::Object(promise); 2652 2689 continue; 2653 2690 } 2691 + 2692 + // Check for event dispatch marker. 2693 + let is_event_dispatch = matches!( 2694 + gc_get_property(&self.gc, *r, "__event_dispatch__"), 2695 + Value::Boolean(true) 2696 + ); 2697 + if is_event_dispatch { 2698 + let target_idx = match gc_get_property( 2699 + &self.gc, 2700 + *r, 2701 + "__target_id__", 2702 + ) { 2703 + Value::Number(n) => n as usize, 2704 + _ => { 2705 + self.registers[base + dst as usize] = 2706 + Value::Boolean(true); 2707 + continue; 2708 + } 2709 + }; 2710 + let evt_ref = match gc_get_property( 2711 + &self.gc, 2712 + *r, 2713 + "__event_ref__", 2714 + ) { 2715 + Value::Object(er) => er, 2716 + _ => { 2717 + self.registers[base + dst as usize] = 2718 + Value::Boolean(true); 2719 + continue; 2720 + } 2721 + }; 2722 + let result = crate::dom_bridge::run_event_dispatch( 2723 + self, target_idx, evt_ref, 2724 + ); 2725 + self.registers[base + dst as usize] = result; 2726 + continue; 2727 + } 2654 2728 } 2655 2729 self.registers[base + dst as usize] = val; 2656 2730 } ··· 3412 3486 /// Set a global variable. 3413 3487 pub fn set_global(&mut self, name: &str, val: Value) { 3414 3488 self.globals.insert(name.to_string(), val); 3489 + } 3490 + 3491 + /// Remove a global variable. 3492 + pub fn remove_global(&mut self, name: &str) { 3493 + self.globals.remove(name); 3415 3494 } 3416 3495 } 3417 3496