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 window.location Location object (Phase 17)

Add a proper Location object with URL property getters (href, protocol,
host, hostname, port, pathname, search, hash, origin), property setters
that trigger navigation, and methods (assign, replace, reload, toString).

- New crates/js/src/location.rs with Location object, dynamic property
resolution, setter-driven navigation queue, and comprehensive tests
- Replace window.location stub in iframe_bridge with real Location object
- Wire document.location as alias via resolve_document_get
- Hook location getters/setters into VM property resolution chain
- Initialize window object and document URL in script_loader
- Thread-local NavigationCommand queue for browser to consume

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

+640 -2
+6
crates/browser/src/script_loader.rs
··· 221 221 we_js::fetch::set_document_origin(&origin_str); 222 222 vm.set_document_origin(&origin_str); 223 223 224 + // Set the document URL so window.location properties work correctly. 225 + vm.set_document_url(base_url.clone()); 226 + 227 + // Initialize the window global object (includes window.location). 228 + we_js::iframe_bridge::init_window_object(&mut vm, "main", &origin_str); 229 + 224 230 // Separate into immediate (sync + async) and deferred scripts. 225 231 let mut deferred_sources: Vec<(String, String)> = Vec::new(); 226 232
+4
crates/js/src/dom_bridge.rs
··· 1784 1784 } 1785 1785 } 1786 1786 } 1787 + "location" => { 1788 + // document.location is the same object as window.location. 1789 + bridge.location_object.borrow().map(Value::Object) 1790 + } 1787 1791 _ => None, 1788 1792 } 1789 1793 }
+7 -2
crates/js/src/iframe_bridge.rs
··· 90 90 Value::Object(window_ref), 91 91 ); 92 92 93 - // window.location (basic stub). 93 + // window.location — proper Location object. 94 + let location_ref = crate::location::create_location_object(&mut vm.gc, &mut vm.shapes); 94 95 set_builtin_prop( 95 96 &mut vm.gc, 96 97 &mut vm.shapes, 97 98 window_ref, 98 99 "location", 99 - Value::Object(window_ref), 100 + Value::Object(location_ref), 100 101 ); 102 + // Store on the DomBridge so document.location can return the same object. 103 + if let Some(bridge) = &vm.dom_bridge { 104 + *bridge.location_object.borrow_mut() = Some(location_ref); 105 + } 101 106 102 107 // window.origin 103 108 set_builtin_prop(
+1
crates/js/src/lib.rs
··· 11 11 pub mod indexeddb; 12 12 pub mod jit; 13 13 pub mod lexer; 14 + pub mod location; 14 15 pub mod parser; 15 16 pub mod regex; 16 17 pub mod shape;
+602
crates/js/src/location.rs
··· 1 + //! `window.location` / `document.location` — the Location object. 2 + //! 3 + //! Exposes URL components of the current document and provides navigation 4 + //! methods (`assign`, `replace`, `reload`). Property getters are resolved 5 + //! dynamically from `DomBridge.document_url` so they always reflect the 6 + //! current page URL. 7 + 8 + use std::cell::RefCell; 9 + use std::rc::Rc; 10 + 11 + use crate::builtins::{make_native, set_builtin_prop}; 12 + use crate::gc::{Gc, GcRef}; 13 + use crate::shape::ShapeTable; 14 + use crate::vm::*; 15 + 16 + /// Internal key marking an object as a Location object. 17 + pub(crate) const LOCATION_MARKER_KEY: &str = "__location__"; 18 + 19 + // --------------------------------------------------------------------------- 20 + // Navigation command queue 21 + // --------------------------------------------------------------------------- 22 + 23 + /// A navigation command queued by Location setters/methods. 24 + pub enum NavigationCommand { 25 + /// Navigate to a new URL. `replace` = true means replace current history 26 + /// entry (for `location.replace()`), false means add a new entry. 27 + Navigate { url: String, replace: bool }, 28 + /// Reload the current page. 29 + Reload, 30 + } 31 + 32 + thread_local! { 33 + static NAVIGATION_QUEUE: RefCell<Vec<NavigationCommand>> = const { RefCell::new(Vec::new()) }; 34 + } 35 + 36 + /// Drain all pending navigation commands from the queue. 37 + pub fn drain_navigation_queue() -> Vec<NavigationCommand> { 38 + NAVIGATION_QUEUE.with(|q| { 39 + let mut queue = q.borrow_mut(); 40 + std::mem::take(&mut *queue) 41 + }) 42 + } 43 + 44 + fn push_navigate(url: String, replace: bool) { 45 + NAVIGATION_QUEUE.with(|q| { 46 + q.borrow_mut() 47 + .push(NavigationCommand::Navigate { url, replace }); 48 + }); 49 + } 50 + 51 + fn push_reload() { 52 + NAVIGATION_QUEUE.with(|q| { 53 + q.borrow_mut().push(NavigationCommand::Reload); 54 + }); 55 + } 56 + 57 + // --------------------------------------------------------------------------- 58 + // Location object creation 59 + // --------------------------------------------------------------------------- 60 + 61 + /// Create a Location object with native methods. 62 + /// 63 + /// The object is marked with `LOCATION_MARKER_KEY` so that the dynamic 64 + /// property resolver can identify it. URL property getters are resolved 65 + /// dynamically from `DomBridge.document_url`, not stored statically. 66 + pub fn create_location_object(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable) -> GcRef { 67 + let mut data = ObjectData::new(); 68 + data.insert_property( 69 + LOCATION_MARKER_KEY.to_string(), 70 + Property::builtin(Value::Boolean(true)), 71 + shapes, 72 + ); 73 + 74 + let loc_ref = gc.alloc(HeapObject::Object(data)); 75 + 76 + // assign(url) — navigate, add history entry 77 + let assign_fn = make_native(gc, "assign", location_assign); 78 + set_builtin_prop(gc, shapes, loc_ref, "assign", Value::Function(assign_fn)); 79 + 80 + // replace(url) — navigate, replace history entry 81 + let replace_fn = make_native(gc, "replace", location_replace); 82 + set_builtin_prop(gc, shapes, loc_ref, "replace", Value::Function(replace_fn)); 83 + 84 + // reload() — reload current page 85 + let reload_fn = make_native(gc, "reload", location_reload); 86 + set_builtin_prop(gc, shapes, loc_ref, "reload", Value::Function(reload_fn)); 87 + 88 + // toString() — returns href 89 + let to_string_fn = make_native(gc, "toString", location_to_string); 90 + set_builtin_prop( 91 + gc, 92 + shapes, 93 + loc_ref, 94 + "toString", 95 + Value::Function(to_string_fn), 96 + ); 97 + 98 + loc_ref 99 + } 100 + 101 + // --------------------------------------------------------------------------- 102 + // Native method implementations 103 + // --------------------------------------------------------------------------- 104 + 105 + /// `location.assign(url)` — navigate to the given URL (adds history entry). 106 + fn location_assign(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 107 + let url = args 108 + .first() 109 + .map(|v| v.to_js_string(ctx.gc)) 110 + .unwrap_or_default(); 111 + 112 + let resolved = resolve_url_against_current(&url, ctx.dom_bridge); 113 + push_navigate(resolved, false); 114 + Ok(Value::Undefined) 115 + } 116 + 117 + /// `location.replace(url)` — navigate to the given URL (replaces history entry). 118 + fn location_replace(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 119 + let url = args 120 + .first() 121 + .map(|v| v.to_js_string(ctx.gc)) 122 + .unwrap_or_default(); 123 + 124 + let resolved = resolve_url_against_current(&url, ctx.dom_bridge); 125 + push_navigate(resolved, true); 126 + Ok(Value::Undefined) 127 + } 128 + 129 + /// `location.reload()` — reload the current page. 130 + fn location_reload(_args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 131 + push_reload(); 132 + Ok(Value::Undefined) 133 + } 134 + 135 + /// `location.toString()` — returns the href. 136 + fn location_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> { 137 + let href = ctx 138 + .dom_bridge 139 + .and_then(|b| b.document_url.borrow().as_ref().map(|u| u.serialize())) 140 + .unwrap_or_default(); 141 + Ok(Value::String(href)) 142 + } 143 + 144 + /// Resolve a (possibly relative) URL against the current document URL. 145 + fn resolve_url_against_current(input: &str, bridge: Option<&DomBridge>) -> String { 146 + let base = bridge.and_then(|b| b.document_url.borrow().clone()); 147 + match base { 148 + Some(base_url) => we_url::Url::parse_with_base(input, &base_url) 149 + .map(|u| u.serialize()) 150 + .unwrap_or_else(|_| input.to_string()), 151 + None => input.to_string(), 152 + } 153 + } 154 + 155 + // --------------------------------------------------------------------------- 156 + // Dynamic property resolution (getters) 157 + // --------------------------------------------------------------------------- 158 + 159 + /// Check whether a GcRef is a Location object. 160 + pub fn is_location_object(gc: &Gc<HeapObject>, shapes: &ShapeTable, gc_ref: GcRef) -> bool { 161 + if let Some(HeapObject::Object(data)) = gc.get(gc_ref) { 162 + data.contains_key(LOCATION_MARKER_KEY, shapes) 163 + } else { 164 + false 165 + } 166 + } 167 + 168 + /// Resolve a dynamic property on a Location object. 169 + /// 170 + /// Reads URL components from `DomBridge.document_url` so that getters always 171 + /// reflect the current page URL. 172 + pub fn resolve_location_get( 173 + gc: &Gc<HeapObject>, 174 + shapes: &ShapeTable, 175 + bridge: &Rc<DomBridge>, 176 + gc_ref: GcRef, 177 + key: &str, 178 + ) -> Option<Value> { 179 + if !is_location_object(gc, shapes, gc_ref) { 180 + return None; 181 + } 182 + 183 + let url_ref = bridge.document_url.borrow(); 184 + let url = url_ref.as_ref(); 185 + 186 + match key { 187 + "href" => { 188 + let href = url.map(|u| u.serialize()).unwrap_or_default(); 189 + Some(Value::String(href)) 190 + } 191 + "protocol" => { 192 + let proto = url.map(|u| format!("{}:", u.scheme())).unwrap_or_default(); 193 + Some(Value::String(proto)) 194 + } 195 + "host" => { 196 + let host = url.map(format_host_with_port).unwrap_or_default(); 197 + Some(Value::String(host)) 198 + } 199 + "hostname" => { 200 + let hostname = url.and_then(|u| u.host_str()).unwrap_or_default(); 201 + Some(Value::String(hostname)) 202 + } 203 + "port" => { 204 + // Return the explicit port as a string, or "" if default/absent. 205 + let port = url 206 + .and_then(|u| u.port()) 207 + .map(|p| p.to_string()) 208 + .unwrap_or_default(); 209 + Some(Value::String(port)) 210 + } 211 + "pathname" => { 212 + let path = url.map(|u| u.path()).unwrap_or_else(|| "/".to_string()); 213 + Some(Value::String(path)) 214 + } 215 + "search" => { 216 + let search = url 217 + .and_then(|u| u.query().map(|q| format!("?{q}"))) 218 + .unwrap_or_default(); 219 + Some(Value::String(search)) 220 + } 221 + "hash" => { 222 + let hash = url 223 + .and_then(|u| u.fragment().map(|f| format!("#{f}"))) 224 + .unwrap_or_default(); 225 + Some(Value::String(hash)) 226 + } 227 + "origin" => { 228 + let origin = url.map(|u| u.origin().serialize()).unwrap_or_default(); 229 + Some(Value::String(origin)) 230 + } 231 + _ => None, 232 + } 233 + } 234 + 235 + /// Format `host:port` — omit port if it is the default for the scheme. 236 + fn format_host_with_port(url: &we_url::Url) -> String { 237 + let hostname = match url.host_str() { 238 + Some(h) => h, 239 + None => return String::new(), 240 + }; 241 + match url.port() { 242 + Some(p) => format!("{hostname}:{p}"), 243 + None => hostname, 244 + } 245 + } 246 + 247 + // --------------------------------------------------------------------------- 248 + // Dynamic property setter 249 + // --------------------------------------------------------------------------- 250 + 251 + /// Handle a property set on a Location object. 252 + /// 253 + /// Setting `href` triggers full navigation. Setting `hash` triggers 254 + /// fragment navigation. Other URL component setters modify the URL and 255 + /// navigate. 256 + /// 257 + /// Returns `true` if the property was handled. 258 + pub fn handle_location_set( 259 + bridge: &Rc<DomBridge>, 260 + gc_ref: GcRef, 261 + key: &str, 262 + val: &Value, 263 + gc: &Gc<HeapObject>, 264 + shapes: &ShapeTable, 265 + ) -> bool { 266 + if !is_location_object(gc, shapes, gc_ref) { 267 + return false; 268 + } 269 + 270 + let val_str = val.to_js_string(gc); 271 + 272 + match key { 273 + "href" => { 274 + let resolved = resolve_url_against_current(&val_str, Some(bridge_ref(bridge))); 275 + push_navigate(resolved, false); 276 + true 277 + } 278 + "hash" => { 279 + // Modify fragment and navigate (fragment-only = no page reload in 280 + // a full implementation, but we queue a navigate so the browser 281 + // can decide). 282 + if let Some(new_url) = modify_url_component(bridge, |url| { 283 + let frag = val_str.strip_prefix('#').unwrap_or(&val_str); 284 + url.fragment = Some(frag.to_string()); 285 + }) { 286 + push_navigate(new_url, false); 287 + } 288 + true 289 + } 290 + "search" => { 291 + if let Some(new_url) = modify_url_component(bridge, |url| { 292 + let q = val_str.strip_prefix('?').unwrap_or(&val_str); 293 + url.query = if q.is_empty() { 294 + None 295 + } else { 296 + Some(q.to_string()) 297 + }; 298 + }) { 299 + push_navigate(new_url, false); 300 + } 301 + true 302 + } 303 + "pathname" => { 304 + // Build a new URL with the modified path by re-serializing. 305 + let url_ref = bridge.document_url.borrow(); 306 + if let Some(url) = url_ref.as_ref() { 307 + let path = if val_str.starts_with('/') { 308 + val_str.clone() 309 + } else { 310 + format!("/{val_str}") 311 + }; 312 + let host = format_host_with_port(url); 313 + let mut new_url = format!("{}://{}{}", url.scheme(), host, path); 314 + if let Some(q) = url.query() { 315 + new_url.push('?'); 316 + new_url.push_str(q); 317 + } 318 + if let Some(f) = url.fragment() { 319 + new_url.push('#'); 320 + new_url.push_str(f); 321 + } 322 + push_navigate(new_url, false); 323 + } 324 + true 325 + } 326 + "protocol" => { 327 + if let Some(new_url) = modify_url_component(bridge, |url| { 328 + let scheme = val_str.strip_suffix(':').unwrap_or(&val_str); 329 + url.scheme = scheme.to_lowercase(); 330 + }) { 331 + push_navigate(new_url, false); 332 + } 333 + true 334 + } 335 + "hostname" => { 336 + if let Some(new_url) = modify_url_component(bridge, |url| { 337 + url.host = Some(we_url::Host::Domain(val_str.clone())); 338 + }) { 339 + push_navigate(new_url, false); 340 + } 341 + true 342 + } 343 + "host" => { 344 + if let Some(new_url) = modify_url_component(bridge, |url| { 345 + // host can include port: "example.com:8080" 346 + if let Some((hostname, port_str)) = val_str.rsplit_once(':') { 347 + url.host = Some(we_url::Host::Domain(hostname.to_string())); 348 + url.port = port_str.parse().ok(); 349 + } else { 350 + url.host = Some(we_url::Host::Domain(val_str.clone())); 351 + url.port = None; 352 + } 353 + }) { 354 + push_navigate(new_url, false); 355 + } 356 + true 357 + } 358 + "port" => { 359 + if let Some(new_url) = modify_url_component(bridge, |url| { 360 + url.port = if val_str.is_empty() { 361 + None 362 + } else { 363 + val_str.parse().ok() 364 + }; 365 + }) { 366 + push_navigate(new_url, false); 367 + } 368 + true 369 + } 370 + _ => false, 371 + } 372 + } 373 + 374 + fn bridge_ref(bridge: &Rc<DomBridge>) -> &DomBridge { 375 + bridge 376 + } 377 + 378 + /// Clone the current URL, apply a mutation, and return the serialized result. 379 + /// Returns `None` if there is no current URL. 380 + fn modify_url_component( 381 + bridge: &Rc<DomBridge>, 382 + mutate: impl FnOnce(&mut we_url::Url), 383 + ) -> Option<String> { 384 + let url_ref = bridge.document_url.borrow(); 385 + let mut url = url_ref.as_ref()?.clone(); 386 + drop(url_ref); 387 + mutate(&mut url); 388 + Some(url.serialize()) 389 + } 390 + 391 + // --------------------------------------------------------------------------- 392 + // Tests 393 + // --------------------------------------------------------------------------- 394 + 395 + #[cfg(test)] 396 + mod tests { 397 + use super::*; 398 + use crate::gc::Gc; 399 + 400 + #[test] 401 + fn location_object_has_marker() { 402 + let mut gc = Gc::<HeapObject>::new(); 403 + let mut shapes = ShapeTable::new(); 404 + let loc = create_location_object(&mut gc, &mut shapes); 405 + assert!(is_location_object(&gc, &shapes, loc)); 406 + } 407 + 408 + #[test] 409 + fn location_object_has_methods() { 410 + let mut gc = Gc::<HeapObject>::new(); 411 + let mut shapes = ShapeTable::new(); 412 + let loc = create_location_object(&mut gc, &mut shapes); 413 + if let Some(HeapObject::Object(data)) = gc.get(loc) { 414 + assert!(data.get_property("assign", &shapes).is_some()); 415 + assert!(data.get_property("replace", &shapes).is_some()); 416 + assert!(data.get_property("reload", &shapes).is_some()); 417 + assert!(data.get_property("toString", &shapes).is_some()); 418 + } else { 419 + panic!("expected Object"); 420 + } 421 + } 422 + 423 + #[test] 424 + fn navigation_queue_drain() { 425 + // Clear any leftover state from other tests. 426 + drain_navigation_queue(); 427 + 428 + push_navigate("https://example.com".to_string(), false); 429 + push_navigate("https://other.com".to_string(), true); 430 + push_reload(); 431 + 432 + let cmds = drain_navigation_queue(); 433 + assert_eq!(cmds.len(), 3); 434 + match &cmds[0] { 435 + NavigationCommand::Navigate { url, replace } => { 436 + assert_eq!(url, "https://example.com"); 437 + assert!(!replace); 438 + } 439 + _ => panic!("expected Navigate"), 440 + } 441 + match &cmds[1] { 442 + NavigationCommand::Navigate { url, replace } => { 443 + assert_eq!(url, "https://other.com"); 444 + assert!(*replace); 445 + } 446 + _ => panic!("expected Navigate"), 447 + } 448 + assert!(matches!(cmds[2], NavigationCommand::Reload)); 449 + 450 + // Queue should be empty after drain. 451 + assert!(drain_navigation_queue().is_empty()); 452 + } 453 + 454 + /// Helper: extract a string from a Value, panicking if not a string. 455 + fn assert_str(val: Option<Value>, expected: &str) { 456 + match val { 457 + Some(Value::String(s)) => assert_eq!(s, expected), 458 + other => panic!("expected String(\"{expected}\"), got {other:?}"), 459 + } 460 + } 461 + 462 + /// Helper: create a test DomBridge with an optional URL. 463 + fn test_bridge(url: Option<we_url::Url>) -> Rc<DomBridge> { 464 + Rc::new(DomBridge { 465 + document: RefCell::new(we_dom::Document::new()), 466 + node_wrappers: RefCell::new(std::collections::HashMap::new()), 467 + event_listeners: RefCell::new(std::collections::HashMap::new()), 468 + origin: RefCell::new(String::new()), 469 + cookie_jar: RefCell::new(we_net::cookie::CookieJar::new()), 470 + document_url: RefCell::new(url), 471 + local_storage: RefCell::new(crate::storage::StorageArea::new()), 472 + session_storage: RefCell::new(crate::storage::StorageArea::new()), 473 + indexeddb: RefCell::new(crate::indexeddb::IndexedDbState::new()), 474 + iframe_windows: RefCell::new(std::collections::HashMap::new()), 475 + location_object: RefCell::new(None), 476 + }) 477 + } 478 + 479 + #[test] 480 + fn resolve_getters_no_url() { 481 + let mut gc = Gc::<HeapObject>::new(); 482 + let shapes = ShapeTable::new(); 483 + let bridge = test_bridge(None); 484 + 485 + // A plain (non-Location) object should return None. 486 + let plain = gc.alloc(HeapObject::Object(ObjectData::new())); 487 + assert!(resolve_location_get(&gc, &shapes, &bridge, plain, "href").is_none()); 488 + } 489 + 490 + #[test] 491 + fn resolve_getters_with_url() { 492 + let mut gc = Gc::<HeapObject>::new(); 493 + let mut shapes = ShapeTable::new(); 494 + let loc = create_location_object(&mut gc, &mut shapes); 495 + 496 + let url = we_url::Url::parse("https://example.com:8080/path?q=1#frag").unwrap(); 497 + let bridge = test_bridge(Some(url)); 498 + 499 + let get = |key: &str| resolve_location_get(&gc, &shapes, &bridge, loc, key); 500 + 501 + assert_str(get("href"), "https://example.com:8080/path?q=1#frag"); 502 + assert_str(get("protocol"), "https:"); 503 + assert_str(get("host"), "example.com:8080"); 504 + assert_str(get("hostname"), "example.com"); 505 + assert_str(get("port"), "8080"); 506 + assert_str(get("pathname"), "/path"); 507 + assert_str(get("search"), "?q=1"); 508 + assert_str(get("hash"), "#frag"); 509 + assert_str(get("origin"), "https://example.com:8080"); 510 + // Unknown key returns None. 511 + assert!(get("nonexistent").is_none()); 512 + } 513 + 514 + #[test] 515 + fn resolve_getters_default_port() { 516 + let mut gc = Gc::<HeapObject>::new(); 517 + let mut shapes = ShapeTable::new(); 518 + let loc = create_location_object(&mut gc, &mut shapes); 519 + 520 + let url = we_url::Url::parse("https://example.com/page").unwrap(); 521 + let bridge = test_bridge(Some(url)); 522 + 523 + let get = |key: &str| resolve_location_get(&gc, &shapes, &bridge, loc, key); 524 + 525 + // Port should be empty for default port. 526 + assert_str(get("port"), ""); 527 + // Host should not include port when it's the default. 528 + assert_str(get("host"), "example.com"); 529 + // Search/hash empty when absent. 530 + assert_str(get("search"), ""); 531 + assert_str(get("hash"), ""); 532 + } 533 + 534 + #[test] 535 + fn setter_href_queues_navigate() { 536 + drain_navigation_queue(); // Clear. 537 + 538 + let mut gc = Gc::<HeapObject>::new(); 539 + let mut shapes = ShapeTable::new(); 540 + let loc = create_location_object(&mut gc, &mut shapes); 541 + 542 + let url = we_url::Url::parse("https://example.com/old").unwrap(); 543 + let bridge = test_bridge(Some(url)); 544 + 545 + let val = Value::String("https://other.com/new".to_string()); 546 + assert!(handle_location_set( 547 + &bridge, loc, "href", &val, &gc, &shapes 548 + )); 549 + 550 + let cmds = drain_navigation_queue(); 551 + assert_eq!(cmds.len(), 1); 552 + match &cmds[0] { 553 + NavigationCommand::Navigate { url, replace } => { 554 + assert_eq!(url, "https://other.com/new"); 555 + assert!(!replace); 556 + } 557 + _ => panic!("expected Navigate"), 558 + } 559 + } 560 + 561 + #[test] 562 + fn setter_hash_queues_navigate() { 563 + drain_navigation_queue(); 564 + 565 + let mut gc = Gc::<HeapObject>::new(); 566 + let mut shapes = ShapeTable::new(); 567 + let loc = create_location_object(&mut gc, &mut shapes); 568 + 569 + let url = we_url::Url::parse("https://example.com/page").unwrap(); 570 + let bridge = test_bridge(Some(url)); 571 + 572 + let val = Value::String("#section2".to_string()); 573 + assert!(handle_location_set( 574 + &bridge, loc, "hash", &val, &gc, &shapes 575 + )); 576 + 577 + let cmds = drain_navigation_queue(); 578 + assert_eq!(cmds.len(), 1); 579 + match &cmds[0] { 580 + NavigationCommand::Navigate { url, replace } => { 581 + assert_eq!(url, "https://example.com/page#section2"); 582 + assert!(!replace); 583 + } 584 + _ => panic!("expected Navigate"), 585 + } 586 + } 587 + 588 + #[test] 589 + fn non_location_object_not_handled() { 590 + let mut gc = Gc::<HeapObject>::new(); 591 + let shapes = ShapeTable::new(); 592 + // Create a plain object (not a Location). 593 + let plain = gc.alloc(HeapObject::Object(ObjectData::new())); 594 + let bridge = test_bridge(None); 595 + 596 + assert!(resolve_location_get(&gc, &shapes, &bridge, plain, "href").is_none()); 597 + let val = Value::String("https://x.com".to_string()); 598 + assert!(!handle_location_set( 599 + &bridge, plain, "href", &val, &gc, &shapes 600 + )); 601 + } 602 + }
+20
crates/js/src/vm.rs
··· 535 535 /// Window proxies for iframe elements, keyed by the iframe's NodeId index. 536 536 /// Used to implement `contentWindow` / `contentDocument`. 537 537 pub iframe_windows: RefCell<HashMap<usize, GcRef>>, 538 + /// The Location object (`window.location` / `document.location`). 539 + pub location_object: RefCell<Option<GcRef>>, 538 540 } 539 541 540 542 /// Context passed to native functions, providing GC access and `this` binding. ··· 1211 1213 session_storage: RefCell::new(crate::storage::StorageArea::new()), 1212 1214 indexeddb: RefCell::new(crate::indexeddb::IndexedDbState::new()), 1213 1215 iframe_windows: RefCell::new(HashMap::new()), 1216 + location_object: RefCell::new(None), 1214 1217 }); 1215 1218 self.dom_bridge = Some(bridge); 1216 1219 crate::dom_bridge::init_document_object(self); ··· 2634 2637 { 2635 2638 return Some(val); 2636 2639 } 2640 + // Try Location object properties (href, protocol, host, etc.). 2641 + if let Some(val) = 2642 + crate::location::resolve_location_get(&self.gc, &self.shapes, &bridge, gc_ref, key) 2643 + { 2644 + return Some(val); 2645 + } 2637 2646 // Try window properties (parent, top, frames, length). 2638 2647 if let Some(val) = 2639 2648 crate::iframe_bridge::resolve_window_property(&self.gc, &self.shapes, gc_ref, key) ··· 2672 2681 /// Returns `true` if the property was handled (caller should skip normal set). 2673 2682 fn handle_dom_property_set(&mut self, gc_ref: GcRef, key: &str, val: &Value) -> bool { 2674 2683 if let Some(bridge) = self.dom_bridge.clone() { 2684 + // Check for Location object property sets (location.href = "..."). 2685 + if crate::location::handle_location_set( 2686 + &bridge, 2687 + gc_ref, 2688 + key, 2689 + val, 2690 + &self.gc, 2691 + &self.shapes, 2692 + ) { 2693 + return true; 2694 + } 2675 2695 // Check for Storage proxy sets (localStorage["key"] = "val"). 2676 2696 if crate::dom_bridge::handle_storage_set( 2677 2697 &bridge,