File
Blob: src/rust/jsg-test/tests/resource_properties.rs
| 1 | // Copyright (c) 2026 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | //! Tests for `#[jsg_property(prototype)]`, `#[jsg_property(instance)]`, and |
| 6 | //! `#[jsg_inspect_property]`. |
| 7 | |
| 8 | use std::cell::Cell; |
| 9 | use std::cell::RefCell; |
| 10 | |
| 11 | use jsg::Number; |
| 12 | use jsg::ToJS; |
| 13 | use jsg_macros::jsg_inspect_property; |
| 14 | use jsg_macros::jsg_method; |
| 15 | use jsg_macros::jsg_property; |
| 16 | use jsg_macros::jsg_resource; |
| 17 | use jsg_macros::jsg_static_constant; |
| 18 | |
| 19 | // ============================================================================= |
| 20 | // Shared test resources |
| 21 | // ============================================================================= |
| 22 | |
| 23 | /// A counter with a read/write value, a read-only label, and an inspect property. |
| 24 | #[jsg_resource] |
| 25 | struct Counter { |
| 26 | value: Cell<f64>, |
| 27 | label: RefCell<String>, |
| 28 | } |
| 29 | |
| 30 | #[jsg_resource] |
| 31 | impl Counter { |
| 32 | // Read/write prototype property — detected from get_/set_ prefix. |
| 33 | #[jsg_property(prototype)] |
| 34 | pub fn get_value(&self) -> Number { |
| 35 | Number::new(self.value.get()) |
| 36 | } |
| 37 | |
| 38 | #[jsg_property(prototype)] |
| 39 | pub fn set_value(&self, v: Number) { |
| 40 | self.value.set(v.value()); |
| 41 | } |
| 42 | |
| 43 | // Read-only prototype property (no matching set_). |
| 44 | #[jsg_property(prototype, readonly)] |
| 45 | pub fn get_label(&self) -> String { |
| 46 | self.label.borrow().clone() |
| 47 | } |
| 48 | |
| 49 | // A regular method coexisting with properties. |
| 50 | #[jsg_method] |
| 51 | pub fn reset(&self) { |
| 52 | self.value.set(0.0); |
| 53 | } |
| 54 | |
| 55 | // Inspect property — hidden from normal enumeration. |
| 56 | #[jsg_inspect_property] |
| 57 | pub fn debug_info(&self) -> String { |
| 58 | format!( |
| 59 | "Counter(value={}, label={})", |
| 60 | self.value.get(), |
| 61 | self.label.borrow() |
| 62 | ) |
| 63 | } |
| 64 | |
| 65 | // Static constant coexisting with properties. |
| 66 | #[jsg_static_constant] |
| 67 | pub const MAX_VALUE: f64 = 1_000_000.0; |
| 68 | } |
| 69 | |
| 70 | impl Counter { |
| 71 | fn new(value: f64, label: impl Into<String>) -> Self { |
| 72 | Self { |
| 73 | value: Cell::new(value), |
| 74 | label: RefCell::new(label.into()), |
| 75 | } |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | /// A token with a read/write instance property and a read-only one. |
| 80 | #[jsg_resource] |
| 81 | struct Token { |
| 82 | id: RefCell<String>, |
| 83 | kind: String, |
| 84 | } |
| 85 | |
| 86 | #[jsg_resource] |
| 87 | impl Token { |
| 88 | #[jsg_property(instance)] |
| 89 | pub fn get_id(&self) -> String { |
| 90 | self.id.borrow().clone() |
| 91 | } |
| 92 | |
| 93 | #[jsg_property(instance)] |
| 94 | pub fn set_id(&self, v: String) { |
| 95 | *self.id.borrow_mut() = v; |
| 96 | } |
| 97 | |
| 98 | // Read-only instance property. |
| 99 | #[jsg_property(instance, readonly)] |
| 100 | pub fn get_kind(&self) -> String { |
| 101 | self.kind.clone() |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | impl Token { |
| 106 | fn new(id: impl Into<String>, kind: impl Into<String>) -> Self { |
| 107 | Self { |
| 108 | id: RefCell::new(id.into()), |
| 109 | kind: kind.into(), |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | /// A resource with multi-word `snake_case` property names to test camelCase conversion. |
| 115 | #[jsg_resource] |
| 116 | struct MultiWord { |
| 117 | first_name: RefCell<String>, |
| 118 | last_name: RefCell<String>, |
| 119 | } |
| 120 | |
| 121 | #[jsg_resource] |
| 122 | impl MultiWord { |
| 123 | #[jsg_property(prototype)] |
| 124 | pub fn get_first_name(&self) -> String { |
| 125 | self.first_name.borrow().clone() |
| 126 | } |
| 127 | |
| 128 | #[jsg_property(prototype)] |
| 129 | pub fn set_first_name(&self, v: String) { |
| 130 | *self.first_name.borrow_mut() = v; |
| 131 | } |
| 132 | |
| 133 | #[jsg_property(prototype, readonly)] |
| 134 | pub fn get_last_name(&self) -> String { |
| 135 | self.last_name.borrow().clone() |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | impl MultiWord { |
| 140 | fn new(first: impl Into<String>, last: impl Into<String>) -> Self { |
| 141 | Self { |
| 142 | first_name: RefCell::new(first.into()), |
| 143 | last_name: RefCell::new(last.into()), |
| 144 | } |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | /// A resource with an explicit name override. |
| 149 | #[jsg_resource] |
| 150 | struct ExplicitName { |
| 151 | x: Cell<f64>, |
| 152 | } |
| 153 | |
| 154 | #[jsg_resource] |
| 155 | impl ExplicitName { |
| 156 | #[jsg_property(prototype, name = "myValue")] |
| 157 | pub fn get_something(&self) -> Number { |
| 158 | Number::new(self.x.get()) |
| 159 | } |
| 160 | |
| 161 | #[jsg_property(prototype, name = "myValue")] |
| 162 | pub fn set_something(&self, v: Number) { |
| 163 | self.x.set(v.value()); |
| 164 | } |
| 165 | |
| 166 | #[jsg_inspect_property(name = "debugX")] |
| 167 | pub fn internal_debug(&self) -> Number { |
| 168 | Number::new(self.x.get()) |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | impl ExplicitName { |
| 173 | fn new(x: f64) -> Self { |
| 174 | Self { x: Cell::new(x) } |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | /// A resource where a property returns `Option<T>` (can be null in JS). |
| 179 | #[jsg_resource] |
| 180 | struct MaybeHolder { |
| 181 | inner: Cell<Option<f64>>, |
| 182 | } |
| 183 | |
| 184 | #[jsg_resource] |
| 185 | impl MaybeHolder { |
| 186 | #[jsg_property(prototype)] |
| 187 | pub fn get_value(&self) -> Option<Number> { |
| 188 | self.inner.get().map(Number::new) |
| 189 | } |
| 190 | |
| 191 | #[jsg_property(prototype)] |
| 192 | pub fn set_value(&self, v: Number) { |
| 193 | self.inner.set(Some(v.value())); |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | impl MaybeHolder { |
| 198 | fn with_value(v: f64) -> Self { |
| 199 | Self { |
| 200 | inner: Cell::new(Some(v)), |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | fn empty() -> Self { |
| 205 | Self { |
| 206 | inner: Cell::new(None), |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | // ============================================================================= |
| 212 | // Prototype property — getter |
| 213 | // ============================================================================= |
| 214 | |
| 215 | #[test] |
| 216 | fn prototype_getter_returns_initial_value() { |
| 217 | let harness = crate::Harness::new(); |
| 218 | harness.run_in_context(|lock, ctx| { |
| 219 | let r = jsg::Rc::new(Counter::new(42.0, "x")); |
| 220 | ctx.set_global("c", r.to_js(lock)); |
| 221 | let v: Number = ctx.eval(lock, "c.value").unwrap(); |
| 222 | assert!((v.value() - 42.0).abs() < f64::EPSILON); |
| 223 | Ok(()) |
| 224 | }); |
| 225 | } |
| 226 | |
| 227 | #[test] |
| 228 | fn prototype_getter_reflects_rust_side_mutation() { |
| 229 | let harness = crate::Harness::new(); |
| 230 | harness.run_in_context(|lock, ctx| { |
| 231 | let r = jsg::Rc::new(Counter::new(1.0, "x")); |
| 232 | let r2 = r.clone(); |
| 233 | ctx.set_global("c", r.to_js(lock)); |
| 234 | r2.value.set(99.0); |
| 235 | let v: Number = ctx.eval(lock, "c.value").unwrap(); |
| 236 | assert!((v.value() - 99.0).abs() < f64::EPSILON); |
| 237 | Ok(()) |
| 238 | }); |
| 239 | } |
| 240 | |
| 241 | #[test] |
| 242 | fn prototype_getter_returns_string_value() { |
| 243 | let harness = crate::Harness::new(); |
| 244 | harness.run_in_context(|lock, ctx| { |
| 245 | let r = jsg::Rc::new(Counter::new(0.0, "hello")); |
| 246 | ctx.set_global("c", r.to_js(lock)); |
| 247 | let v: String = ctx.eval(lock, "c.label").unwrap(); |
| 248 | assert_eq!(v, "hello"); |
| 249 | Ok(()) |
| 250 | }); |
| 251 | } |
| 252 | |
| 253 | // ============================================================================= |
| 254 | // Prototype property — setter |
| 255 | // ============================================================================= |
| 256 | |
| 257 | #[test] |
| 258 | fn prototype_setter_updates_value_visible_via_getter() { |
| 259 | let harness = crate::Harness::new(); |
| 260 | harness.run_in_context(|lock, ctx| { |
| 261 | let r = jsg::Rc::new(Counter::new(0.0, "x")); |
| 262 | ctx.set_global("c", r.to_js(lock)); |
| 263 | ctx.eval_raw("c.value = 77").unwrap(); |
| 264 | let v: Number = ctx.eval(lock, "c.value").unwrap(); |
| 265 | assert!((v.value() - 77.0).abs() < f64::EPSILON); |
| 266 | Ok(()) |
| 267 | }); |
| 268 | } |
| 269 | |
| 270 | #[test] |
| 271 | fn prototype_setter_mutation_visible_from_rust() { |
| 272 | let harness = crate::Harness::new(); |
| 273 | harness.run_in_context(|lock, ctx| { |
| 274 | let r = jsg::Rc::new(Counter::new(0.0, "x")); |
| 275 | let r2 = r.clone(); |
| 276 | ctx.set_global("c", r.to_js(lock)); |
| 277 | ctx.eval_raw("c.value = 55").unwrap(); |
| 278 | assert!((r2.value.get() - 55.0).abs() < f64::EPSILON); |
| 279 | Ok(()) |
| 280 | }); |
| 281 | } |
| 282 | |
| 283 | #[test] |
| 284 | fn prototype_setter_called_multiple_times() { |
| 285 | let harness = crate::Harness::new(); |
| 286 | harness.run_in_context(|lock, ctx| { |
| 287 | let r = jsg::Rc::new(Counter::new(0.0, "x")); |
| 288 | ctx.set_global("c", r.to_js(lock)); |
| 289 | ctx.eval_raw("c.value = 1; c.value = 2; c.value = 3") |
| 290 | .unwrap(); |
| 291 | let v: Number = ctx.eval(lock, "c.value").unwrap(); |
| 292 | assert!((v.value() - 3.0).abs() < f64::EPSILON); |
| 293 | Ok(()) |
| 294 | }); |
| 295 | } |
| 296 | |
| 297 | // ============================================================================= |
| 298 | // Prototype property — read-only enforcement |
| 299 | // ============================================================================= |
| 300 | |
| 301 | #[test] |
| 302 | fn prototype_readonly_property_throws_in_strict_mode() { |
| 303 | let harness = crate::Harness::new(); |
| 304 | harness.run_in_context(|lock, ctx| { |
| 305 | let r = jsg::Rc::new(Counter::new(1.0, "lbl")); |
| 306 | ctx.set_global("c", r.to_js(lock)); |
| 307 | let result = ctx.eval_raw("'use strict'; c.label = 'changed'"); |
| 308 | assert!( |
| 309 | result.is_err(), |
| 310 | "expected TypeError assigning to read-only prototype property" |
| 311 | ); |
| 312 | Ok(()) |
| 313 | }); |
| 314 | } |
| 315 | |
| 316 | #[test] |
| 317 | fn prototype_readonly_property_silently_ignored_in_sloppy_mode() { |
| 318 | let harness = crate::Harness::new(); |
| 319 | harness.run_in_context(|lock, ctx| { |
| 320 | let r = jsg::Rc::new(Counter::new(1.0, "original")); |
| 321 | ctx.set_global("c", r.to_js(lock)); |
| 322 | // In sloppy mode, the assignment is silently ignored; value unchanged. |
| 323 | ctx.eval_raw("c.label = 'changed'").unwrap(); |
| 324 | let v: String = ctx.eval(lock, "c.label").unwrap(); |
| 325 | assert_eq!(v, "original"); |
| 326 | Ok(()) |
| 327 | }); |
| 328 | } |
| 329 | |
| 330 | // ============================================================================= |
| 331 | // Prototype property — enumerability / prototype chain |
| 332 | // ============================================================================= |
| 333 | |
| 334 | #[test] |
| 335 | fn prototype_property_not_in_object_keys() { |
| 336 | let harness = crate::Harness::new(); |
| 337 | harness.run_in_context(|lock, ctx| { |
| 338 | let r = jsg::Rc::new(Counter::new(1.0, "x")); |
| 339 | ctx.set_global("c", r.to_js(lock)); |
| 340 | let keys: String = ctx.eval(lock, "Object.keys(c).join(',')").unwrap(); |
| 341 | assert_eq!( |
| 342 | keys, "", |
| 343 | "prototype properties must not appear in Object.keys()" |
| 344 | ); |
| 345 | Ok(()) |
| 346 | }); |
| 347 | } |
| 348 | |
| 349 | #[test] |
| 350 | fn prototype_property_found_by_in_operator() { |
| 351 | let harness = crate::Harness::new(); |
| 352 | harness.run_in_context(|lock, ctx| { |
| 353 | let r = jsg::Rc::new(Counter::new(1.0, "x")); |
| 354 | ctx.set_global("c", r.to_js(lock)); |
| 355 | let found: bool = ctx.eval(lock, "'value' in c").unwrap(); |
| 356 | assert!(found, "'in' must find prototype properties"); |
| 357 | Ok(()) |
| 358 | }); |
| 359 | } |
| 360 | |
| 361 | #[test] |
| 362 | fn prototype_property_not_an_own_property() { |
| 363 | let harness = crate::Harness::new(); |
| 364 | harness.run_in_context(|lock, ctx| { |
| 365 | let r = jsg::Rc::new(Counter::new(1.0, "x")); |
| 366 | ctx.set_global("c", r.to_js(lock)); |
| 367 | let own: bool = ctx |
| 368 | .eval(lock, "Object.prototype.hasOwnProperty.call(c, 'value')") |
| 369 | .unwrap(); |
| 370 | assert!(!own, "prototype property must NOT be an own property"); |
| 371 | Ok(()) |
| 372 | }); |
| 373 | } |
| 374 | |
| 375 | // ============================================================================= |
| 376 | // Prototype property — enumerability (matches C++ JSG_PROTOTYPE_PROPERTY) |
| 377 | // ============================================================================= |
| 378 | |
| 379 | #[test] |
| 380 | fn prototype_property_is_enumerable() { |
| 381 | // C++ registerPrototypeProperty uses v8::PropertyAttribute::None for normal |
| 382 | // (non-Unimplemented) properties, making them enumerable. Verify the Rust |
| 383 | // layer matches this behaviour. |
| 384 | let harness = crate::Harness::new(); |
| 385 | harness.run_in_context(|lock, ctx| { |
| 386 | let r = jsg::Rc::new(Counter::new(1.0, "x")); |
| 387 | ctx.set_global("c", r.to_js(lock)); |
| 388 | // value is a read/write prototype property. |
| 389 | let enumerable: bool = ctx |
| 390 | .eval( |
| 391 | lock, |
| 392 | "Object.getOwnPropertyDescriptor(Object.getPrototypeOf(c), 'value').enumerable", |
| 393 | ) |
| 394 | .unwrap(); |
| 395 | assert!( |
| 396 | enumerable, |
| 397 | "read-write prototype property must be enumerable (matching C++ JSG)" |
| 398 | ); |
| 399 | Ok(()) |
| 400 | }); |
| 401 | } |
| 402 | |
| 403 | #[test] |
| 404 | fn prototype_readonly_property_is_enumerable() { |
| 405 | // C++ registerReadonlyPrototypeProperty uses v8::PropertyAttribute::ReadOnly |
| 406 | // (without DontEnum) for normal properties, making them enumerable. |
| 407 | let harness = crate::Harness::new(); |
| 408 | harness.run_in_context(|lock, ctx| { |
| 409 | let r = jsg::Rc::new(Counter::new(1.0, "x")); |
| 410 | ctx.set_global("c", r.to_js(lock)); |
| 411 | // label is a read-only prototype property. |
| 412 | let enumerable: bool = ctx |
| 413 | .eval( |
| 414 | lock, |
| 415 | "Object.getOwnPropertyDescriptor(Object.getPrototypeOf(c), 'label').enumerable", |
| 416 | ) |
| 417 | .unwrap(); |
| 418 | assert!( |
| 419 | enumerable, |
| 420 | "read-only prototype property must be enumerable (matching C++ JSG)" |
| 421 | ); |
| 422 | Ok(()) |
| 423 | }); |
| 424 | } |
| 425 | |
| 426 | #[test] |
| 427 | fn prototype_property_appears_in_for_in() { |
| 428 | // Enumerable prototype properties must appear in for...in loops. |
| 429 | let harness = crate::Harness::new(); |
| 430 | harness.run_in_context(|lock, ctx| { |
| 431 | let r = jsg::Rc::new(Counter::new(1.0, "x")); |
| 432 | ctx.set_global("c", r.to_js(lock)); |
| 433 | let found: bool = ctx |
| 434 | .eval( |
| 435 | lock, |
| 436 | "(function() { for (var k in c) { if (k === 'value') return true; } return false; })()", |
| 437 | ) |
| 438 | .unwrap(); |
| 439 | assert!(found, "prototype property 'value' must appear in for...in"); |
| 440 | Ok(()) |
| 441 | }); |
| 442 | } |
| 443 | |
| 444 | // ============================================================================= |
| 445 | // Prototype property — multiple instances are independent |
| 446 | // ============================================================================= |
| 447 | |
| 448 | #[test] |
| 449 | fn prototype_property_independent_across_instances() { |
| 450 | let harness = crate::Harness::new(); |
| 451 | harness.run_in_context(|lock, ctx| { |
| 452 | let a = jsg::Rc::new(Counter::new(10.0, "a")); |
| 453 | let b = jsg::Rc::new(Counter::new(20.0, "b")); |
| 454 | ctx.set_global("a", a.to_js(lock)); |
| 455 | ctx.set_global("b", b.to_js(lock)); |
| 456 | let va: Number = ctx.eval(lock, "a.value").unwrap(); |
| 457 | let vb: Number = ctx.eval(lock, "b.value").unwrap(); |
| 458 | assert!((va.value() - 10.0).abs() < f64::EPSILON); |
| 459 | assert!((vb.value() - 20.0).abs() < f64::EPSILON); |
| 460 | ctx.eval_raw("a.value = 99").unwrap(); |
| 461 | let va2: Number = ctx.eval(lock, "a.value").unwrap(); |
| 462 | let vb2: Number = ctx.eval(lock, "b.value").unwrap(); |
| 463 | assert!((va2.value() - 99.0).abs() < f64::EPSILON); |
| 464 | assert!( |
| 465 | (vb2.value() - 20.0).abs() < f64::EPSILON, |
| 466 | "setting a.value must not affect b" |
| 467 | ); |
| 468 | Ok(()) |
| 469 | }); |
| 470 | } |
| 471 | |
| 472 | // ============================================================================= |
| 473 | // Prototype property — coexistence with methods and constants |
| 474 | // ============================================================================= |
| 475 | |
| 476 | #[test] |
| 477 | fn prototype_property_coexists_with_method() { |
| 478 | let harness = crate::Harness::new(); |
| 479 | harness.run_in_context(|lock, ctx| { |
| 480 | let r = jsg::Rc::new(Counter::new(5.0, "x")); |
| 481 | ctx.set_global("c", r.to_js(lock)); |
| 482 | ctx.eval_raw("c.value = 7").unwrap(); |
| 483 | ctx.eval_raw("c.reset()").unwrap(); |
| 484 | let v: Number = ctx.eval(lock, "c.value").unwrap(); |
| 485 | assert!( |
| 486 | (v.value()).abs() < f64::EPSILON, |
| 487 | "reset() must set value back to 0" |
| 488 | ); |
| 489 | Ok(()) |
| 490 | }); |
| 491 | } |
| 492 | |
| 493 | #[test] |
| 494 | fn prototype_property_coexists_with_static_constant() { |
| 495 | let harness = crate::Harness::new(); |
| 496 | harness.run_in_context(|lock, ctx| { |
| 497 | let constructor = jsg::resource::function_template_of::<Counter>(lock); |
| 498 | ctx.set_global("Counter", constructor.into()); |
| 499 | let r = jsg::Rc::new(Counter::new(1.0, "x")); |
| 500 | ctx.set_global("c", r.to_js(lock)); |
| 501 | |
| 502 | let v: Number = ctx.eval(lock, "c.value").unwrap(); |
| 503 | assert!((v.value() - 1.0).abs() < f64::EPSILON); |
| 504 | |
| 505 | let max: Number = ctx.eval(lock, "Counter.MAX_VALUE").unwrap(); |
| 506 | assert!((max.value() - 1_000_000.0).abs() < f64::EPSILON); |
| 507 | Ok(()) |
| 508 | }); |
| 509 | } |
| 510 | |
| 511 | // ============================================================================= |
| 512 | // Prototype property — camelCase name derivation |
| 513 | // ============================================================================= |
| 514 | |
| 515 | #[test] |
| 516 | fn prototype_property_multi_word_camel_case() { |
| 517 | let harness = crate::Harness::new(); |
| 518 | harness.run_in_context(|lock, ctx| { |
| 519 | let r = jsg::Rc::new(MultiWord::new("Alice", "Smith")); |
| 520 | ctx.set_global("p", r.to_js(lock)); |
| 521 | let first: String = ctx.eval(lock, "p.firstName").unwrap(); |
| 522 | let last: String = ctx.eval(lock, "p.lastName").unwrap(); |
| 523 | assert_eq!(first, "Alice"); |
| 524 | assert_eq!(last, "Smith"); |
| 525 | Ok(()) |
| 526 | }); |
| 527 | } |
| 528 | |
| 529 | #[test] |
| 530 | fn prototype_property_multi_word_setter() { |
| 531 | let harness = crate::Harness::new(); |
| 532 | harness.run_in_context(|lock, ctx| { |
| 533 | let r = jsg::Rc::new(MultiWord::new("Alice", "Smith")); |
| 534 | ctx.set_global("p", r.to_js(lock)); |
| 535 | ctx.eval_raw("p.firstName = 'Bob'").unwrap(); |
| 536 | let first: String = ctx.eval(lock, "p.firstName").unwrap(); |
| 537 | assert_eq!(first, "Bob"); |
| 538 | Ok(()) |
| 539 | }); |
| 540 | } |
| 541 | |
| 542 | #[test] |
| 543 | fn prototype_property_original_rust_name_not_exposed() { |
| 544 | let harness = crate::Harness::new(); |
| 545 | harness.run_in_context(|lock, ctx| { |
| 546 | let r = jsg::Rc::new(MultiWord::new("Alice", "Smith")); |
| 547 | ctx.set_global("p", r.to_js(lock)); |
| 548 | // The raw Rust names (get_first_name, set_first_name) must not be visible. |
| 549 | let undef: bool = ctx |
| 550 | .eval(lock, "typeof p.getFirstName === 'undefined'") |
| 551 | .unwrap(); |
| 552 | assert!(undef, "raw getter name must not be exposed"); |
| 553 | let undef2: bool = ctx |
| 554 | .eval(lock, "typeof p.setFirstName === 'undefined'") |
| 555 | .unwrap(); |
| 556 | assert!(undef2, "raw setter name must not be exposed"); |
| 557 | Ok(()) |
| 558 | }); |
| 559 | } |
| 560 | |
| 561 | // ============================================================================= |
| 562 | // Prototype property — explicit name override |
| 563 | // ============================================================================= |
| 564 | |
| 565 | #[test] |
| 566 | fn explicit_name_override_getter() { |
| 567 | let harness = crate::Harness::new(); |
| 568 | harness.run_in_context(|lock, ctx| { |
| 569 | let r = jsg::Rc::new(ExplicitName::new(42.0)); |
| 570 | ctx.set_global("obj", r.to_js(lock)); |
| 571 | let v: Number = ctx.eval(lock, "obj.myValue").unwrap(); |
| 572 | assert!((v.value() - 42.0).abs() < f64::EPSILON); |
| 573 | Ok(()) |
| 574 | }); |
| 575 | } |
| 576 | |
| 577 | #[test] |
| 578 | fn explicit_name_override_setter() { |
| 579 | let harness = crate::Harness::new(); |
| 580 | harness.run_in_context(|lock, ctx| { |
| 581 | let r = jsg::Rc::new(ExplicitName::new(0.0)); |
| 582 | ctx.set_global("obj", r.to_js(lock)); |
| 583 | ctx.eval_raw("obj.myValue = 7").unwrap(); |
| 584 | let v: Number = ctx.eval(lock, "obj.myValue").unwrap(); |
| 585 | assert!((v.value() - 7.0).abs() < f64::EPSILON); |
| 586 | Ok(()) |
| 587 | }); |
| 588 | } |
| 589 | |
| 590 | #[test] |
| 591 | fn explicit_name_original_rust_name_not_exposed() { |
| 592 | let harness = crate::Harness::new(); |
| 593 | harness.run_in_context(|lock, ctx| { |
| 594 | let r = jsg::Rc::new(ExplicitName::new(0.0)); |
| 595 | ctx.set_global("obj", r.to_js(lock)); |
| 596 | let undef: bool = ctx |
| 597 | .eval(lock, "typeof obj.getSomething === 'undefined'") |
| 598 | .unwrap(); |
| 599 | assert!(undef); |
| 600 | Ok(()) |
| 601 | }); |
| 602 | } |
| 603 | |
| 604 | // ============================================================================= |
| 605 | // Prototype property — Option<T> return (null) |
| 606 | // ============================================================================= |
| 607 | |
| 608 | #[test] |
| 609 | fn prototype_option_property_returns_value_when_some() { |
| 610 | let harness = crate::Harness::new(); |
| 611 | harness.run_in_context(|lock, ctx| { |
| 612 | let r = jsg::Rc::new(MaybeHolder::with_value(std::f64::consts::PI)); |
| 613 | ctx.set_global("h", r.to_js(lock)); |
| 614 | let v: Number = ctx.eval(lock, "h.value").unwrap(); |
| 615 | assert!((v.value() - std::f64::consts::PI).abs() < 1e-10); |
| 616 | Ok(()) |
| 617 | }); |
| 618 | } |
| 619 | |
| 620 | #[test] |
| 621 | fn prototype_option_property_returns_undefined_when_none() { |
| 622 | let harness = crate::Harness::new(); |
| 623 | harness.run_in_context(|lock, ctx| { |
| 624 | let r = jsg::Rc::new(MaybeHolder::empty()); |
| 625 | ctx.set_global("h", r.to_js(lock)); |
| 626 | // Option<T>::None maps to `undefined` via jsg::ToJS (same as a missing value). |
| 627 | let is_nullish: bool = ctx.eval(lock, "h.value == null").unwrap(); |
| 628 | assert!( |
| 629 | is_nullish, |
| 630 | "None getter return must be nullish (null or undefined) in JS" |
| 631 | ); |
| 632 | Ok(()) |
| 633 | }); |
| 634 | } |
| 635 | |
| 636 | #[test] |
| 637 | fn prototype_option_property_setter_makes_it_some() { |
| 638 | let harness = crate::Harness::new(); |
| 639 | harness.run_in_context(|lock, ctx| { |
| 640 | let r = jsg::Rc::new(MaybeHolder::empty()); |
| 641 | ctx.set_global("h", r.to_js(lock)); |
| 642 | ctx.eval_raw("h.value = 2.5").unwrap(); |
| 643 | let v: Number = ctx.eval(lock, "h.value").unwrap(); |
| 644 | assert!((v.value() - 2.5).abs() < f64::EPSILON); |
| 645 | Ok(()) |
| 646 | }); |
| 647 | } |
| 648 | |
| 649 | // ============================================================================= |
| 650 | // Instance property — getter |
| 651 | // ============================================================================= |
| 652 | |
| 653 | #[test] |
| 654 | fn instance_getter_returns_initial_value() { |
| 655 | let harness = crate::Harness::new(); |
| 656 | harness.run_in_context(|lock, ctx| { |
| 657 | let r = jsg::Rc::new(Token::new("tok-123", "bearer")); |
| 658 | ctx.set_global("t", r.to_js(lock)); |
| 659 | let id: String = ctx.eval(lock, "t.id").unwrap(); |
| 660 | assert_eq!(id, "tok-123"); |
| 661 | Ok(()) |
| 662 | }); |
| 663 | } |
| 664 | |
| 665 | #[test] |
| 666 | fn instance_readonly_getter_returns_value() { |
| 667 | let harness = crate::Harness::new(); |
| 668 | harness.run_in_context(|lock, ctx| { |
| 669 | let r = jsg::Rc::new(Token::new("abc", "jwt")); |
| 670 | ctx.set_global("t", r.to_js(lock)); |
| 671 | let kind: String = ctx.eval(lock, "t.kind").unwrap(); |
| 672 | assert_eq!(kind, "jwt"); |
| 673 | Ok(()) |
| 674 | }); |
| 675 | } |
| 676 | |
| 677 | // ============================================================================= |
| 678 | // Instance property — setter |
| 679 | // ============================================================================= |
| 680 | |
| 681 | #[test] |
| 682 | fn instance_setter_updates_value() { |
| 683 | let harness = crate::Harness::new(); |
| 684 | harness.run_in_context(|lock, ctx| { |
| 685 | let r = jsg::Rc::new(Token::new("old", "bearer")); |
| 686 | ctx.set_global("t", r.to_js(lock)); |
| 687 | ctx.eval_raw("t.id = 'new-id'").unwrap(); |
| 688 | let id: String = ctx.eval(lock, "t.id").unwrap(); |
| 689 | assert_eq!(id, "new-id"); |
| 690 | Ok(()) |
| 691 | }); |
| 692 | } |
| 693 | |
| 694 | #[test] |
| 695 | fn instance_setter_visible_from_rust() { |
| 696 | let harness = crate::Harness::new(); |
| 697 | harness.run_in_context(|lock, ctx| { |
| 698 | let r = jsg::Rc::new(Token::new("orig", "bearer")); |
| 699 | let r2 = r.clone(); |
| 700 | ctx.set_global("t", r.to_js(lock)); |
| 701 | ctx.eval_raw("t.id = 'updated'").unwrap(); |
| 702 | assert_eq!(*r2.id.borrow(), "updated"); |
| 703 | Ok(()) |
| 704 | }); |
| 705 | } |
| 706 | |
| 707 | // ============================================================================= |
| 708 | // Instance property — own-property semantics |
| 709 | // ============================================================================= |
| 710 | |
| 711 | #[test] |
| 712 | fn instance_property_is_own_property() { |
| 713 | let harness = crate::Harness::new(); |
| 714 | harness.run_in_context(|lock, ctx| { |
| 715 | let r = jsg::Rc::new(Token::new("tok", "bearer")); |
| 716 | ctx.set_global("t", r.to_js(lock)); |
| 717 | let own: bool = ctx |
| 718 | .eval(lock, "Object.prototype.hasOwnProperty.call(t, 'id')") |
| 719 | .unwrap(); |
| 720 | assert!(own, "instance property must be an own property"); |
| 721 | Ok(()) |
| 722 | }); |
| 723 | } |
| 724 | |
| 725 | #[test] |
| 726 | fn instance_property_has_accessor_descriptor() { |
| 727 | let harness = crate::Harness::new(); |
| 728 | harness.run_in_context(|lock, ctx| { |
| 729 | let r = jsg::Rc::new(Token::new("tok", "bearer")); |
| 730 | ctx.set_global("t", r.to_js(lock)); |
| 731 | // An accessor descriptor has `get`/`set` keys, not `value`/`writable`. |
| 732 | let has_get: bool = ctx |
| 733 | .eval( |
| 734 | lock, |
| 735 | "typeof Object.getOwnPropertyDescriptor(t, 'id').get === 'function'", |
| 736 | ) |
| 737 | .unwrap(); |
| 738 | let has_set: bool = ctx |
| 739 | .eval( |
| 740 | lock, |
| 741 | "typeof Object.getOwnPropertyDescriptor(t, 'id').set === 'function'", |
| 742 | ) |
| 743 | .unwrap(); |
| 744 | assert!( |
| 745 | has_get, |
| 746 | "instance property must have a getter in its descriptor" |
| 747 | ); |
| 748 | assert!( |
| 749 | has_set, |
| 750 | "read-write instance property must have a setter in its descriptor" |
| 751 | ); |
| 752 | Ok(()) |
| 753 | }); |
| 754 | } |
| 755 | |
| 756 | #[test] |
| 757 | fn instance_readonly_property_has_no_setter_in_descriptor() { |
| 758 | let harness = crate::Harness::new(); |
| 759 | harness.run_in_context(|lock, ctx| { |
| 760 | let r = jsg::Rc::new(Token::new("tok", "bearer")); |
| 761 | ctx.set_global("t", r.to_js(lock)); |
| 762 | let set_undef: bool = ctx |
| 763 | .eval( |
| 764 | lock, |
| 765 | "typeof Object.getOwnPropertyDescriptor(t, 'kind').set === 'undefined'", |
| 766 | ) |
| 767 | .unwrap(); |
| 768 | assert!(set_undef, "read-only instance property must have no setter"); |
| 769 | Ok(()) |
| 770 | }); |
| 771 | } |
| 772 | |
| 773 | #[test] |
| 774 | fn instance_readonly_property_throws_in_strict_mode() { |
| 775 | let harness = crate::Harness::new(); |
| 776 | harness.run_in_context(|lock, ctx| { |
| 777 | let r = jsg::Rc::new(Token::new("tok", "bearer")); |
| 778 | ctx.set_global("t", r.to_js(lock)); |
| 779 | let result = ctx.eval_raw("'use strict'; t.kind = 'other'"); |
| 780 | assert!( |
| 781 | result.is_err(), |
| 782 | "expected error assigning to read-only instance property" |
| 783 | ); |
| 784 | Ok(()) |
| 785 | }); |
| 786 | } |
| 787 | |
| 788 | #[test] |
| 789 | fn instance_property_independent_across_instances() { |
| 790 | let harness = crate::Harness::new(); |
| 791 | harness.run_in_context(|lock, ctx| { |
| 792 | let a = jsg::Rc::new(Token::new("id-a", "bearer")); |
| 793 | let b = jsg::Rc::new(Token::new("id-b", "bearer")); |
| 794 | ctx.set_global("a", a.to_js(lock)); |
| 795 | ctx.set_global("b", b.to_js(lock)); |
| 796 | ctx.eval_raw("a.id = 'changed-a'").unwrap(); |
| 797 | let id_a: String = ctx.eval(lock, "a.id").unwrap(); |
| 798 | let id_b: String = ctx.eval(lock, "b.id").unwrap(); |
| 799 | assert_eq!(id_a, "changed-a"); |
| 800 | assert_eq!(id_b, "id-b", "mutating a.id must not affect b.id"); |
| 801 | Ok(()) |
| 802 | }); |
| 803 | } |
| 804 | |
| 805 | // ============================================================================= |
| 806 | // Inspect property |
| 807 | // ============================================================================= |
| 808 | |
| 809 | #[test] |
| 810 | fn inspect_property_not_accessible_by_string_key() { |
| 811 | let harness = crate::Harness::new(); |
| 812 | harness.run_in_context(|lock, ctx| { |
| 813 | let r = jsg::Rc::new(Counter::new(3.0, "dbg")); |
| 814 | ctx.set_global("c", r.to_js(lock)); |
| 815 | let undef: bool = ctx |
| 816 | .eval(lock, "typeof c.debugInfo === 'undefined'") |
| 817 | .unwrap(); |
| 818 | assert!( |
| 819 | undef, |
| 820 | "inspect property must not be accessible via string key" |
| 821 | ); |
| 822 | Ok(()) |
| 823 | }); |
| 824 | } |
| 825 | |
| 826 | #[test] |
| 827 | fn inspect_property_not_in_object_keys() { |
| 828 | let harness = crate::Harness::new(); |
| 829 | harness.run_in_context(|lock, ctx| { |
| 830 | let r = jsg::Rc::new(Counter::new(3.0, "dbg")); |
| 831 | ctx.set_global("c", r.to_js(lock)); |
| 832 | let keys: String = ctx.eval(lock, "Object.keys(c).join(',')").unwrap(); |
| 833 | assert!( |
| 834 | !keys.contains("debugInfo"), |
| 835 | "inspect property must not appear in Object.keys()" |
| 836 | ); |
| 837 | Ok(()) |
| 838 | }); |
| 839 | } |
| 840 | |
| 841 | #[test] |
| 842 | fn inspect_property_not_in_own_string_names() { |
| 843 | let harness = crate::Harness::new(); |
| 844 | harness.run_in_context(|lock, ctx| { |
| 845 | let r = jsg::Rc::new(Counter::new(3.0, "dbg")); |
| 846 | ctx.set_global("c", r.to_js(lock)); |
| 847 | let names: String = ctx |
| 848 | .eval(lock, "Object.getOwnPropertyNames(c).join(',')") |
| 849 | .unwrap(); |
| 850 | assert!( |
| 851 | !names.contains("debugInfo"), |
| 852 | "inspect property must not appear in getOwnPropertyNames(), got: {names}" |
| 853 | ); |
| 854 | Ok(()) |
| 855 | }); |
| 856 | } |
| 857 | |
| 858 | #[test] |
| 859 | fn inspect_property_registered_under_a_symbol() { |
| 860 | let harness = crate::Harness::new(); |
| 861 | harness.run_in_context(|lock, ctx| { |
| 862 | let r = jsg::Rc::new(Counter::new(3.0, "dbg")); |
| 863 | ctx.set_global("c", r.to_js(lock)); |
| 864 | // The property is registered under a v8::Symbol; at least one symbol must exist on |
| 865 | // the prototype chain that has an accessor descriptor. |
| 866 | let has_symbol_accessor: bool = ctx |
| 867 | .eval( |
| 868 | lock, |
| 869 | r" |
| 870 | (function() { |
| 871 | let proto = Object.getPrototypeOf(c); |
| 872 | let syms = Object.getOwnPropertySymbols(proto); |
| 873 | for (let sym of syms) { |
| 874 | let d = Object.getOwnPropertyDescriptor(proto, sym); |
| 875 | if (d && typeof d.get === 'function') return true; |
| 876 | } |
| 877 | return false; |
| 878 | })() |
| 879 | ", |
| 880 | ) |
| 881 | .unwrap(); |
| 882 | assert!( |
| 883 | has_symbol_accessor, |
| 884 | "inspect property must be accessible via its symbol" |
| 885 | ); |
| 886 | Ok(()) |
| 887 | }); |
| 888 | } |
| 889 | |
| 890 | #[test] |
| 891 | fn inspect_property_getter_returns_correct_value() { |
| 892 | let harness = crate::Harness::new(); |
| 893 | harness.run_in_context(|lock, ctx| { |
| 894 | let r = jsg::Rc::new(Counter::new(7.0, "test")); |
| 895 | ctx.set_global("c", r.to_js(lock)); |
| 896 | // Retrieve the symbol-keyed getter from the prototype and call it. |
| 897 | let result: String = ctx |
| 898 | .eval( |
| 899 | lock, |
| 900 | r" |
| 901 | (function() { |
| 902 | let proto = Object.getPrototypeOf(c); |
| 903 | let syms = Object.getOwnPropertySymbols(proto); |
| 904 | for (let sym of syms) { |
| 905 | let d = Object.getOwnPropertyDescriptor(proto, sym); |
| 906 | if (d && typeof d.get === 'function') { |
| 907 | return d.get.call(c); |
| 908 | } |
| 909 | } |
| 910 | return 'NOT FOUND'; |
| 911 | })() |
| 912 | ", |
| 913 | ) |
| 914 | .unwrap(); |
| 915 | assert!( |
| 916 | result.contains("Counter"), |
| 917 | "inspect getter must return debug string, got: {result}" |
| 918 | ); |
| 919 | assert!( |
| 920 | result.contains('7'), |
| 921 | "debug string must contain current value" |
| 922 | ); |
| 923 | Ok(()) |
| 924 | }); |
| 925 | } |
| 926 | |
| 927 | #[test] |
| 928 | fn inspect_explicit_name_override() { |
| 929 | let harness = crate::Harness::new(); |
| 930 | harness.run_in_context(|lock, ctx| { |
| 931 | let r = jsg::Rc::new(ExplicitName::new(5.0)); |
| 932 | ctx.set_global("obj", r.to_js(lock)); |
| 933 | // The inspect property must not be accessible as a string key "debugX". |
| 934 | let undef: bool = ctx.eval(lock, "typeof obj.debugX === 'undefined'").unwrap(); |
| 935 | assert!( |
| 936 | undef, |
| 937 | "explicitly-named inspect property must still be hidden under a symbol" |
| 938 | ); |
| 939 | Ok(()) |
| 940 | }); |
| 941 | } |
| 942 | |
| 943 | // ============================================================================= |
| 944 | // inspectProperties dictionary — kResourceTypeInspect integration |
| 945 | // |
| 946 | // node:util's inspect() relies on a `kResourceTypeInspect` API symbol being set |
| 947 | // on the prototype, whose value is an object (the inspectProperties template) |
| 948 | // mapping string names to unique symbols for each inspect property. |
| 949 | // |
| 950 | // From JavaScript the kResourceTypeInspect symbol is not accessible via |
| 951 | // Symbol.for() (it lives in the ForApi registry, not the global symbol table). |
| 952 | // However it IS returned by Object.getOwnPropertySymbols(), and we can identify |
| 953 | // it because its descriptor is a data property whose value is a plain object |
| 954 | // (the dictionary), distinguishing it from the per-property symbols whose |
| 955 | // descriptors are accessor properties (get/set). |
| 956 | // ============================================================================= |
| 957 | |
| 958 | /// Finds the inspectProperties dictionary installed on the prototype under |
| 959 | /// the kResourceTypeInspect API symbol. Returns the dictionary object or |
| 960 | /// `null` if not found. |
| 961 | const FIND_INSPECT_DICT: &str = r" |
| 962 | (function(obj) { |
| 963 | let proto = Object.getPrototypeOf(obj); |
| 964 | for (let sym of Object.getOwnPropertySymbols(proto)) { |
| 965 | let desc = Object.getOwnPropertyDescriptor(proto, sym); |
| 966 | if (desc && typeof desc.value === 'object' && desc.value !== null) { |
| 967 | return desc.value; |
| 968 | } |
| 969 | } |
| 970 | return null; |
| 971 | }) |
| 972 | "; |
| 973 | |
| 974 | #[test] |
| 975 | fn inspect_properties_dict_exists_on_prototype() { |
| 976 | let harness = crate::Harness::new(); |
| 977 | harness.run_in_context(|lock, ctx| { |
| 978 | let r = jsg::Rc::new(Counter::new(3.0, "dbg")); |
| 979 | ctx.set_global("c", r.to_js(lock)); |
| 980 | ctx.set_global("findInspectDict", ctx.eval_raw(FIND_INSPECT_DICT).unwrap()); |
| 981 | let found: bool = ctx.eval(lock, "findInspectDict(c) !== null").unwrap(); |
| 982 | assert!( |
| 983 | found, |
| 984 | "kResourceTypeInspect dictionary must be present on the prototype" |
| 985 | ); |
| 986 | Ok(()) |
| 987 | }); |
| 988 | } |
| 989 | |
| 990 | #[test] |
| 991 | fn inspect_properties_dict_contains_property_name() { |
| 992 | let harness = crate::Harness::new(); |
| 993 | harness.run_in_context(|lock, ctx| { |
| 994 | let r = jsg::Rc::new(Counter::new(3.0, "dbg")); |
| 995 | ctx.set_global("c", r.to_js(lock)); |
| 996 | ctx.set_global("findInspectDict", ctx.eval_raw(FIND_INSPECT_DICT).unwrap()); |
| 997 | let has_key: bool = ctx.eval(lock, "'debugInfo' in findInspectDict(c)").unwrap(); |
| 998 | assert!( |
| 999 | has_key, |
| 1000 | "inspectProperties dictionary must contain the camelCase property name" |
| 1001 | ); |
| 1002 | Ok(()) |
| 1003 | }); |
| 1004 | } |
| 1005 | |
| 1006 | #[test] |
| 1007 | fn inspect_properties_dict_value_is_the_getter_symbol() { |
| 1008 | let harness = crate::Harness::new(); |
| 1009 | harness.run_in_context(|lock, ctx| { |
| 1010 | let r = jsg::Rc::new(Counter::new(3.0, "dbg")); |
| 1011 | ctx.set_global("c", r.to_js(lock)); |
| 1012 | ctx.set_global("findInspectDict", ctx.eval_raw(FIND_INSPECT_DICT).unwrap()); |
| 1013 | // The value in the dictionary must be a symbol, and that same symbol must be the key |
| 1014 | // under which the getter accessor is registered on the prototype. |
| 1015 | let valid: bool = ctx |
| 1016 | .eval( |
| 1017 | lock, |
| 1018 | r" |
| 1019 | (function() { |
| 1020 | let dict = findInspectDict(c); |
| 1021 | let sym = dict['debugInfo']; |
| 1022 | if (typeof sym !== 'symbol') return false; |
| 1023 | let proto = Object.getPrototypeOf(c); |
| 1024 | let d = Object.getOwnPropertyDescriptor(proto, sym); |
| 1025 | return d !== undefined && typeof d.get === 'function'; |
| 1026 | })() |
| 1027 | ", |
| 1028 | ) |
| 1029 | .unwrap(); |
| 1030 | assert!( |
| 1031 | valid, |
| 1032 | "inspectProperties dictionary value must be the symbol under which the getter lives" |
| 1033 | ); |
| 1034 | Ok(()) |
| 1035 | }); |
| 1036 | } |
| 1037 | |
| 1038 | #[test] |
| 1039 | fn inspect_properties_dict_getter_callable_via_symbol() { |
| 1040 | let harness = crate::Harness::new(); |
| 1041 | harness.run_in_context(|lock, ctx| { |
| 1042 | let r = jsg::Rc::new(Counter::new(7.0, "test")); |
| 1043 | ctx.set_global("c", r.to_js(lock)); |
| 1044 | ctx.set_global("findInspectDict", ctx.eval_raw(FIND_INSPECT_DICT).unwrap()); |
| 1045 | // Invoking the getter via the symbol from the dictionary should return the value. |
| 1046 | let result: String = ctx |
| 1047 | .eval( |
| 1048 | lock, |
| 1049 | r" |
| 1050 | (function() { |
| 1051 | let sym = findInspectDict(c)['debugInfo']; |
| 1052 | let proto = Object.getPrototypeOf(c); |
| 1053 | return Object.getOwnPropertyDescriptor(proto, sym).get.call(c); |
| 1054 | })() |
| 1055 | ", |
| 1056 | ) |
| 1057 | .unwrap(); |
| 1058 | assert!( |
| 1059 | result.contains("Counter"), |
| 1060 | "getter invoked via dictionary symbol must return debug string, got: {result}" |
| 1061 | ); |
| 1062 | assert!( |
| 1063 | result.contains('7'), |
| 1064 | "debug string must contain current value" |
| 1065 | ); |
| 1066 | Ok(()) |
| 1067 | }); |
| 1068 | } |
| 1069 | |
| 1070 | #[test] |
| 1071 | fn inspect_properties_dict_present_even_without_inspect_props() { |
| 1072 | // The kResourceTypeInspect symbol must be on the prototype of every JSG resource type |
| 1073 | // (even those with no #[jsg_inspect_property] fields) so node:util can correctly |
| 1074 | // identify the object as a JSG resource and walk its prototype chain. |
| 1075 | let harness = crate::Harness::new(); |
| 1076 | harness.run_in_context(|lock, ctx| { |
| 1077 | // Token has only instance properties, no inspect properties. |
| 1078 | let r = jsg::Rc::new(Token::new("abc", "bearer")); |
| 1079 | ctx.set_global("t", r.to_js(lock)); |
| 1080 | ctx.set_global("findInspectDict", ctx.eval_raw(FIND_INSPECT_DICT).unwrap()); |
| 1081 | let found: bool = ctx |
| 1082 | .eval(lock, "findInspectDict(t) !== null") |
| 1083 | .unwrap(); |
| 1084 | assert!( |
| 1085 | found, |
| 1086 | "kResourceTypeInspect dictionary must be present even on types with no inspect properties" |
| 1087 | ); |
| 1088 | let empty: bool = ctx |
| 1089 | .eval(lock, "Object.keys(findInspectDict(t)).length === 0") |
| 1090 | .unwrap(); |
| 1091 | assert!(empty, "dictionary must be empty when there are no inspect properties"); |
| 1092 | Ok(()) |
| 1093 | }); |
| 1094 | } |
| 1095 | |
| 1096 | #[test] |
| 1097 | fn inspect_properties_dict_explicit_name_override() { |
| 1098 | // When #[jsg_inspect_property(name = "...")] is used, the dictionary key must be |
| 1099 | // the overridden JS name, not the Rust method name. |
| 1100 | let harness = crate::Harness::new(); |
| 1101 | harness.run_in_context(|lock, ctx| { |
| 1102 | let r = jsg::Rc::new(ExplicitName::new(5.0)); |
| 1103 | ctx.set_global("obj", r.to_js(lock)); |
| 1104 | ctx.set_global("findInspectDict", ctx.eval_raw(FIND_INSPECT_DICT).unwrap()); |
| 1105 | // Must contain "debugX" (the overridden name), not "internalDebug" (Rust method). |
| 1106 | let has_override: bool = ctx.eval(lock, "'debugX' in findInspectDict(obj)").unwrap(); |
| 1107 | let has_raw: bool = ctx |
| 1108 | .eval(lock, "'internalDebug' in findInspectDict(obj)") |
| 1109 | .unwrap(); |
| 1110 | assert!( |
| 1111 | has_override, |
| 1112 | "inspectProperties must use the overridden name 'debugX'" |
| 1113 | ); |
| 1114 | assert!( |
| 1115 | !has_raw, |
| 1116 | "inspectProperties must not use the raw Rust method name 'internalDebug'" |
| 1117 | ); |
| 1118 | Ok(()) |
| 1119 | }); |
| 1120 | } |
| 1121 | |
| 1122 | // ============================================================================= |
| 1123 | // Receiver guard — prototype properties must enforce correct `this` |
| 1124 | // ============================================================================= |
| 1125 | |
| 1126 | #[test] |
| 1127 | fn prototype_property_getter_throws_on_wrong_receiver() { |
| 1128 | let harness = crate::Harness::new(); |
| 1129 | harness.run_in_context(|lock, ctx| { |
| 1130 | let r = jsg::Rc::new(Counter::new(1.0, "x")); |
| 1131 | ctx.set_global("c", r.to_js(lock)); |
| 1132 | // Extract the getter from the descriptor and call it with a plain object receiver. |
| 1133 | let result = ctx.eval_raw( |
| 1134 | r" |
| 1135 | 'use strict'; |
| 1136 | let proto = Object.getPrototypeOf(c); |
| 1137 | let desc = Object.getOwnPropertyDescriptor(proto, 'value'); |
| 1138 | desc.get.call({}); |
| 1139 | ", |
| 1140 | ); |
| 1141 | assert!(result.is_err(), "getter with wrong receiver must throw"); |
| 1142 | Ok(()) |
| 1143 | }); |
| 1144 | } |
| 1145 | |
| 1146 | #[test] |
| 1147 | fn prototype_property_setter_throws_on_wrong_receiver() { |
| 1148 | let harness = crate::Harness::new(); |
| 1149 | harness.run_in_context(|lock, ctx| { |
| 1150 | let r = jsg::Rc::new(Counter::new(1.0, "x")); |
| 1151 | ctx.set_global("c", r.to_js(lock)); |
| 1152 | let result = ctx.eval_raw( |
| 1153 | r" |
| 1154 | 'use strict'; |
| 1155 | let proto = Object.getPrototypeOf(c); |
| 1156 | let desc = Object.getOwnPropertyDescriptor(proto, 'value'); |
| 1157 | desc.set.call({}, 99); |
| 1158 | ", |
| 1159 | ); |
| 1160 | assert!(result.is_err(), "setter with wrong receiver must throw"); |
| 1161 | Ok(()) |
| 1162 | }); |
| 1163 | } |
| 1164 | |
| 1165 | // ============================================================================= |
| 1166 | // Interplay: prototype vs instance properties on the same resource |
| 1167 | // ============================================================================= |
| 1168 | |
| 1169 | /// A resource that mixes both kinds of properties. |
| 1170 | #[jsg_resource] |
| 1171 | struct Mixed { |
| 1172 | proto_val: Cell<f64>, |
| 1173 | instance_val: RefCell<String>, |
| 1174 | } |
| 1175 | |
| 1176 | #[jsg_resource] |
| 1177 | impl Mixed { |
| 1178 | #[jsg_property(prototype)] |
| 1179 | pub fn get_proto_val(&self) -> Number { |
| 1180 | Number::new(self.proto_val.get()) |
| 1181 | } |
| 1182 | |
| 1183 | #[jsg_property(prototype)] |
| 1184 | pub fn set_proto_val(&self, v: Number) { |
| 1185 | self.proto_val.set(v.value()); |
| 1186 | } |
| 1187 | |
| 1188 | #[jsg_property(instance)] |
| 1189 | pub fn get_instance_val(&self) -> String { |
| 1190 | self.instance_val.borrow().clone() |
| 1191 | } |
| 1192 | |
| 1193 | #[jsg_property(instance)] |
| 1194 | pub fn set_instance_val(&self, v: String) { |
| 1195 | *self.instance_val.borrow_mut() = v; |
| 1196 | } |
| 1197 | } |
| 1198 | |
| 1199 | impl Mixed { |
| 1200 | fn new(p: f64, i: impl Into<String>) -> Self { |
| 1201 | Self { |
| 1202 | proto_val: Cell::new(p), |
| 1203 | instance_val: RefCell::new(i.into()), |
| 1204 | } |
| 1205 | } |
| 1206 | } |
| 1207 | |
| 1208 | #[test] |
| 1209 | fn mixed_prototype_not_own_instance_is_own() { |
| 1210 | let harness = crate::Harness::new(); |
| 1211 | harness.run_in_context(|lock, ctx| { |
| 1212 | let r = jsg::Rc::new(Mixed::new(1.0, "hello")); |
| 1213 | ctx.set_global("m", r.to_js(lock)); |
| 1214 | |
| 1215 | let proto_own: bool = ctx |
| 1216 | .eval(lock, "Object.prototype.hasOwnProperty.call(m, 'protoVal')") |
| 1217 | .unwrap(); |
| 1218 | let inst_own: bool = ctx |
| 1219 | .eval( |
| 1220 | lock, |
| 1221 | "Object.prototype.hasOwnProperty.call(m, 'instanceVal')", |
| 1222 | ) |
| 1223 | .unwrap(); |
| 1224 | |
| 1225 | assert!(!proto_own, "protoVal must NOT be an own property"); |
| 1226 | assert!(inst_own, "instanceVal MUST be an own property"); |
| 1227 | Ok(()) |
| 1228 | }); |
| 1229 | } |
| 1230 | |
| 1231 | #[test] |
| 1232 | fn mixed_both_properties_readable() { |
| 1233 | let harness = crate::Harness::new(); |
| 1234 | harness.run_in_context(|lock, ctx| { |
| 1235 | let r = jsg::Rc::new(Mixed::new(3.0, "world")); |
| 1236 | ctx.set_global("m", r.to_js(lock)); |
| 1237 | let pv: Number = ctx.eval(lock, "m.protoVal").unwrap(); |
| 1238 | let iv: String = ctx.eval(lock, "m.instanceVal").unwrap(); |
| 1239 | assert!((pv.value() - 3.0).abs() < f64::EPSILON); |
| 1240 | assert_eq!(iv, "world"); |
| 1241 | Ok(()) |
| 1242 | }); |
| 1243 | } |
| 1244 | |
| 1245 | #[test] |
| 1246 | fn mixed_both_properties_writable() { |
| 1247 | let harness = crate::Harness::new(); |
| 1248 | harness.run_in_context(|lock, ctx| { |
| 1249 | let r = jsg::Rc::new(Mixed::new(0.0, "old")); |
| 1250 | ctx.set_global("m", r.to_js(lock)); |
| 1251 | ctx.eval_raw("m.protoVal = 9; m.instanceVal = 'new'") |
| 1252 | .unwrap(); |
| 1253 | let pv: Number = ctx.eval(lock, "m.protoVal").unwrap(); |
| 1254 | let iv: String = ctx.eval(lock, "m.instanceVal").unwrap(); |
| 1255 | assert!((pv.value() - 9.0).abs() < f64::EPSILON); |
| 1256 | assert_eq!(iv, "new"); |
| 1257 | Ok(()) |
| 1258 | }); |
| 1259 | } |
| 1260 | |
| 1261 | // ============================================================================= |
| 1262 | // Combination matrix: every jsg_property flag permutation |
| 1263 | // |
| 1264 | // placement : prototype | instance |
| 1265 | // name : omitted | name = "..." |
| 1266 | // readonly : omitted | readonly |
| 1267 | // rw : getter-only | getter + setter |
| 1268 | // |
| 1269 | // That gives 2 × 2 × 2 × 2 = 16 logical combinations, but readonly + rw |
| 1270 | // is always a compile error (tested separately in compile-fail tests), so |
| 1271 | // we cover the 12 valid runtime variants below. |
| 1272 | // ============================================================================= |
| 1273 | |
| 1274 | /// Fixture covering all valid `#[jsg_property]` combinations in one resource. |
| 1275 | #[jsg_resource] |
| 1276 | struct AllCombinations { |
| 1277 | // backing values for each property |
| 1278 | a: Cell<f64>, // prototype, no-name, rw |
| 1279 | b: Cell<f64>, // prototype, no-name, readonly |
| 1280 | c: Cell<f64>, // prototype, name, rw |
| 1281 | d: Cell<f64>, // prototype, name, readonly |
| 1282 | e: RefCell<String>, // instance, no-name, rw |
| 1283 | f: RefCell<String>, // instance, no-name, readonly |
| 1284 | g: RefCell<String>, // instance, name, rw |
| 1285 | h: RefCell<String>, // instance, name, readonly |
| 1286 | // extra: name before readonly (attribute order independence) |
| 1287 | i: Cell<f64>, // prototype, name + readonly (name first) |
| 1288 | j: Cell<f64>, // instance, readonly + name (readonly first) |
| 1289 | } |
| 1290 | |
| 1291 | #[jsg_resource] |
| 1292 | impl AllCombinations { |
| 1293 | // ---- prototype, no name, rw ------------------------------------------------ |
| 1294 | #[jsg_property(prototype)] |
| 1295 | pub fn get_a(&self) -> Number { |
| 1296 | Number::new(self.a.get()) |
| 1297 | } |
| 1298 | #[jsg_property(prototype)] |
| 1299 | pub fn set_a(&self, v: Number) { |
| 1300 | self.a.set(v.value()); |
| 1301 | } |
| 1302 | |
| 1303 | // ---- prototype, no name, readonly ------------------------------------------ |
| 1304 | #[jsg_property(prototype, readonly)] |
| 1305 | pub fn get_b(&self) -> Number { |
| 1306 | Number::new(self.b.get()) |
| 1307 | } |
| 1308 | |
| 1309 | // ---- prototype, with name, rw ---------------------------------------------- |
| 1310 | #[jsg_property(prototype, name = "namedC")] |
| 1311 | pub fn get_c(&self) -> Number { |
| 1312 | Number::new(self.c.get()) |
| 1313 | } |
| 1314 | #[jsg_property(prototype, name = "namedC")] |
| 1315 | pub fn set_c(&self, v: Number) { |
| 1316 | self.c.set(v.value()); |
| 1317 | } |
| 1318 | |
| 1319 | // ---- prototype, with name, readonly ---------------------------------------- |
| 1320 | #[jsg_property(prototype, name = "namedD", readonly)] |
| 1321 | pub fn get_d(&self) -> Number { |
| 1322 | Number::new(self.d.get()) |
| 1323 | } |
| 1324 | |
| 1325 | // ---- instance, no name, rw ------------------------------------------------- |
| 1326 | #[jsg_property(instance)] |
| 1327 | pub fn get_e(&self) -> String { |
| 1328 | self.e.borrow().clone() |
| 1329 | } |
| 1330 | #[jsg_property(instance)] |
| 1331 | pub fn set_e(&self, v: String) { |
| 1332 | *self.e.borrow_mut() = v; |
| 1333 | } |
| 1334 | |
| 1335 | // ---- instance, no name, readonly ------------------------------------------- |
| 1336 | #[jsg_property(instance, readonly)] |
| 1337 | pub fn get_f(&self) -> String { |
| 1338 | self.f.borrow().clone() |
| 1339 | } |
| 1340 | |
| 1341 | // ---- instance, with name, rw ----------------------------------------------- |
| 1342 | #[jsg_property(instance, name = "namedG")] |
| 1343 | pub fn get_g(&self) -> String { |
| 1344 | self.g.borrow().clone() |
| 1345 | } |
| 1346 | #[jsg_property(instance, name = "namedG")] |
| 1347 | pub fn set_g(&self, v: String) { |
| 1348 | *self.g.borrow_mut() = v; |
| 1349 | } |
| 1350 | |
| 1351 | // ---- instance, with name, readonly ----------------------------------------- |
| 1352 | #[jsg_property(instance, name = "namedH", readonly)] |
| 1353 | pub fn get_h(&self) -> String { |
| 1354 | self.h.borrow().clone() |
| 1355 | } |
| 1356 | |
| 1357 | // ---- attribute order: name before readonly (prototype) --------------------- |
| 1358 | #[jsg_property(prototype, name = "namedI", readonly)] |
| 1359 | pub fn get_i(&self) -> Number { |
| 1360 | Number::new(self.i.get()) |
| 1361 | } |
| 1362 | |
| 1363 | // ---- attribute order: readonly before name (instance) ---------------------- |
| 1364 | #[jsg_property(instance, readonly, name = "namedJ")] |
| 1365 | pub fn get_j(&self) -> Number { |
| 1366 | Number::new(self.j.get()) |
| 1367 | } |
| 1368 | } |
| 1369 | |
| 1370 | impl AllCombinations { |
| 1371 | fn new() -> Self { |
| 1372 | Self { |
| 1373 | a: Cell::new(1.0), |
| 1374 | b: Cell::new(2.0), |
| 1375 | c: Cell::new(3.0), |
| 1376 | d: Cell::new(4.0), |
| 1377 | e: RefCell::new("e".into()), |
| 1378 | f: RefCell::new("f".into()), |
| 1379 | g: RefCell::new("g".into()), |
| 1380 | h: RefCell::new("h".into()), |
| 1381 | i: Cell::new(9.0), |
| 1382 | j: Cell::new(10.0), |
| 1383 | } |
| 1384 | } |
| 1385 | } |
| 1386 | |
| 1387 | // ---- prototype, no-name, rw --------------------------------------------------- |
| 1388 | |
| 1389 | #[test] |
| 1390 | fn combo_prototype_noname_rw_get() { |
| 1391 | let harness = crate::Harness::new(); |
| 1392 | harness.run_in_context(|lock, ctx| { |
| 1393 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1394 | ctx.set_global("o", r.to_js(lock)); |
| 1395 | let v: Number = ctx.eval(lock, "o.a").unwrap(); |
| 1396 | assert!((v.value() - 1.0).abs() < f64::EPSILON); |
| 1397 | Ok(()) |
| 1398 | }); |
| 1399 | } |
| 1400 | |
| 1401 | #[test] |
| 1402 | fn combo_prototype_noname_rw_set() { |
| 1403 | let harness = crate::Harness::new(); |
| 1404 | harness.run_in_context(|lock, ctx| { |
| 1405 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1406 | ctx.set_global("o", r.to_js(lock)); |
| 1407 | ctx.eval_raw("o.a = 99").unwrap(); |
| 1408 | let v: Number = ctx.eval(lock, "o.a").unwrap(); |
| 1409 | assert!((v.value() - 99.0).abs() < f64::EPSILON); |
| 1410 | Ok(()) |
| 1411 | }); |
| 1412 | } |
| 1413 | |
| 1414 | #[test] |
| 1415 | fn combo_prototype_noname_rw_not_own_property() { |
| 1416 | let harness = crate::Harness::new(); |
| 1417 | harness.run_in_context(|lock, ctx| { |
| 1418 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1419 | ctx.set_global("o", r.to_js(lock)); |
| 1420 | let own: bool = ctx |
| 1421 | .eval(lock, "Object.prototype.hasOwnProperty.call(o, 'a')") |
| 1422 | .unwrap(); |
| 1423 | assert!(!own, "prototype property must not be an own property"); |
| 1424 | Ok(()) |
| 1425 | }); |
| 1426 | } |
| 1427 | |
| 1428 | // ---- prototype, no-name, readonly --------------------------------------------- |
| 1429 | |
| 1430 | #[test] |
| 1431 | fn combo_prototype_noname_readonly_get() { |
| 1432 | let harness = crate::Harness::new(); |
| 1433 | harness.run_in_context(|lock, ctx| { |
| 1434 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1435 | ctx.set_global("o", r.to_js(lock)); |
| 1436 | let v: Number = ctx.eval(lock, "o.b").unwrap(); |
| 1437 | assert!((v.value() - 2.0).abs() < f64::EPSILON); |
| 1438 | Ok(()) |
| 1439 | }); |
| 1440 | } |
| 1441 | |
| 1442 | #[test] |
| 1443 | fn combo_prototype_noname_readonly_throws_in_strict() { |
| 1444 | let harness = crate::Harness::new(); |
| 1445 | harness.run_in_context(|lock, ctx| { |
| 1446 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1447 | ctx.set_global("o", r.to_js(lock)); |
| 1448 | let result = ctx.eval_raw("'use strict'; o.b = 5"); |
| 1449 | assert!( |
| 1450 | result.is_err(), |
| 1451 | "readonly prototype property must throw in strict mode" |
| 1452 | ); |
| 1453 | Ok(()) |
| 1454 | }); |
| 1455 | } |
| 1456 | |
| 1457 | #[test] |
| 1458 | fn combo_prototype_noname_readonly_no_setter_in_descriptor() { |
| 1459 | let harness = crate::Harness::new(); |
| 1460 | harness.run_in_context(|lock, ctx| { |
| 1461 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1462 | ctx.set_global("o", r.to_js(lock)); |
| 1463 | let proto = ctx.eval_raw("Object.getPrototypeOf(o)").unwrap(); |
| 1464 | let _ = proto; |
| 1465 | let set_undef: bool = ctx |
| 1466 | .eval( |
| 1467 | lock, |
| 1468 | "typeof Object.getOwnPropertyDescriptor(Object.getPrototypeOf(o), 'b').set === 'undefined'", |
| 1469 | ) |
| 1470 | .unwrap(); |
| 1471 | assert!(set_undef, "readonly prototype property must have no setter"); |
| 1472 | Ok(()) |
| 1473 | }); |
| 1474 | } |
| 1475 | |
| 1476 | // ---- prototype, with name, rw ------------------------------------------------- |
| 1477 | |
| 1478 | #[test] |
| 1479 | fn combo_prototype_named_rw_get() { |
| 1480 | let harness = crate::Harness::new(); |
| 1481 | harness.run_in_context(|lock, ctx| { |
| 1482 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1483 | ctx.set_global("o", r.to_js(lock)); |
| 1484 | let v: Number = ctx.eval(lock, "o.namedC").unwrap(); |
| 1485 | assert!((v.value() - 3.0).abs() < f64::EPSILON); |
| 1486 | Ok(()) |
| 1487 | }); |
| 1488 | } |
| 1489 | |
| 1490 | #[test] |
| 1491 | fn combo_prototype_named_rw_set() { |
| 1492 | let harness = crate::Harness::new(); |
| 1493 | harness.run_in_context(|lock, ctx| { |
| 1494 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1495 | ctx.set_global("o", r.to_js(lock)); |
| 1496 | ctx.eval_raw("o.namedC = 33").unwrap(); |
| 1497 | let v: Number = ctx.eval(lock, "o.namedC").unwrap(); |
| 1498 | assert!((v.value() - 33.0).abs() < f64::EPSILON); |
| 1499 | Ok(()) |
| 1500 | }); |
| 1501 | } |
| 1502 | |
| 1503 | #[test] |
| 1504 | fn combo_prototype_named_rw_raw_rust_name_hidden() { |
| 1505 | let harness = crate::Harness::new(); |
| 1506 | harness.run_in_context(|lock, ctx| { |
| 1507 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1508 | ctx.set_global("o", r.to_js(lock)); |
| 1509 | // The Rust method name "getC" / "setC" must not be visible. |
| 1510 | let hidden: bool = ctx |
| 1511 | .eval( |
| 1512 | lock, |
| 1513 | "typeof o.getC === 'undefined' && typeof o.c === 'undefined'", |
| 1514 | ) |
| 1515 | .unwrap(); |
| 1516 | assert!( |
| 1517 | hidden, |
| 1518 | "raw getter name and camelCase default must be hidden when name is overridden" |
| 1519 | ); |
| 1520 | Ok(()) |
| 1521 | }); |
| 1522 | } |
| 1523 | |
| 1524 | // ---- prototype, with name, readonly ------------------------------------------- |
| 1525 | |
| 1526 | #[test] |
| 1527 | fn combo_prototype_named_readonly_get() { |
| 1528 | let harness = crate::Harness::new(); |
| 1529 | harness.run_in_context(|lock, ctx| { |
| 1530 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1531 | ctx.set_global("o", r.to_js(lock)); |
| 1532 | let v: Number = ctx.eval(lock, "o.namedD").unwrap(); |
| 1533 | assert!((v.value() - 4.0).abs() < f64::EPSILON); |
| 1534 | Ok(()) |
| 1535 | }); |
| 1536 | } |
| 1537 | |
| 1538 | #[test] |
| 1539 | fn combo_prototype_named_readonly_throws_in_strict() { |
| 1540 | let harness = crate::Harness::new(); |
| 1541 | harness.run_in_context(|lock, ctx| { |
| 1542 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1543 | ctx.set_global("o", r.to_js(lock)); |
| 1544 | let result = ctx.eval_raw("'use strict'; o.namedD = 0"); |
| 1545 | assert!( |
| 1546 | result.is_err(), |
| 1547 | "named readonly prototype property must throw in strict mode" |
| 1548 | ); |
| 1549 | Ok(()) |
| 1550 | }); |
| 1551 | } |
| 1552 | |
| 1553 | // ---- instance, no-name, rw ---------------------------------------------------- |
| 1554 | |
| 1555 | #[test] |
| 1556 | fn combo_instance_noname_rw_get() { |
| 1557 | let harness = crate::Harness::new(); |
| 1558 | harness.run_in_context(|lock, ctx| { |
| 1559 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1560 | ctx.set_global("o", r.to_js(lock)); |
| 1561 | let v: String = ctx.eval(lock, "o.e").unwrap(); |
| 1562 | assert_eq!(v, "e"); |
| 1563 | Ok(()) |
| 1564 | }); |
| 1565 | } |
| 1566 | |
| 1567 | #[test] |
| 1568 | fn combo_instance_noname_rw_set() { |
| 1569 | let harness = crate::Harness::new(); |
| 1570 | harness.run_in_context(|lock, ctx| { |
| 1571 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1572 | ctx.set_global("o", r.to_js(lock)); |
| 1573 | ctx.eval_raw("o.e = 'updated'").unwrap(); |
| 1574 | let v: String = ctx.eval(lock, "o.e").unwrap(); |
| 1575 | assert_eq!(v, "updated"); |
| 1576 | Ok(()) |
| 1577 | }); |
| 1578 | } |
| 1579 | |
| 1580 | #[test] |
| 1581 | fn combo_instance_noname_rw_is_own_property() { |
| 1582 | let harness = crate::Harness::new(); |
| 1583 | harness.run_in_context(|lock, ctx| { |
| 1584 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1585 | ctx.set_global("o", r.to_js(lock)); |
| 1586 | let own: bool = ctx |
| 1587 | .eval(lock, "Object.prototype.hasOwnProperty.call(o, 'e')") |
| 1588 | .unwrap(); |
| 1589 | assert!(own, "instance property must be an own property"); |
| 1590 | Ok(()) |
| 1591 | }); |
| 1592 | } |
| 1593 | |
| 1594 | #[test] |
| 1595 | fn combo_instance_noname_rw_in_object_keys() { |
| 1596 | let harness = crate::Harness::new(); |
| 1597 | harness.run_in_context(|lock, ctx| { |
| 1598 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1599 | ctx.set_global("o", r.to_js(lock)); |
| 1600 | let keys: String = ctx.eval(lock, "Object.keys(o).sort().join(',')").unwrap(); |
| 1601 | assert!( |
| 1602 | keys.contains('e'), |
| 1603 | "instance property must appear in Object.keys(), got: {keys}" |
| 1604 | ); |
| 1605 | Ok(()) |
| 1606 | }); |
| 1607 | } |
| 1608 | |
| 1609 | // ---- instance, no-name, readonly ---------------------------------------------- |
| 1610 | |
| 1611 | #[test] |
| 1612 | fn combo_instance_noname_readonly_get() { |
| 1613 | let harness = crate::Harness::new(); |
| 1614 | harness.run_in_context(|lock, ctx| { |
| 1615 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1616 | ctx.set_global("o", r.to_js(lock)); |
| 1617 | let v: String = ctx.eval(lock, "o.f").unwrap(); |
| 1618 | assert_eq!(v, "f"); |
| 1619 | Ok(()) |
| 1620 | }); |
| 1621 | } |
| 1622 | |
| 1623 | #[test] |
| 1624 | fn combo_instance_noname_readonly_throws_in_strict() { |
| 1625 | let harness = crate::Harness::new(); |
| 1626 | harness.run_in_context(|lock, ctx| { |
| 1627 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1628 | ctx.set_global("o", r.to_js(lock)); |
| 1629 | let result = ctx.eval_raw("'use strict'; o.f = 'x'"); |
| 1630 | assert!( |
| 1631 | result.is_err(), |
| 1632 | "readonly instance property must throw in strict mode" |
| 1633 | ); |
| 1634 | Ok(()) |
| 1635 | }); |
| 1636 | } |
| 1637 | |
| 1638 | #[test] |
| 1639 | fn combo_instance_noname_readonly_no_setter_in_descriptor() { |
| 1640 | let harness = crate::Harness::new(); |
| 1641 | harness.run_in_context(|lock, ctx| { |
| 1642 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1643 | ctx.set_global("o", r.to_js(lock)); |
| 1644 | let set_undef: bool = ctx |
| 1645 | .eval( |
| 1646 | lock, |
| 1647 | "typeof Object.getOwnPropertyDescriptor(o, 'f').set === 'undefined'", |
| 1648 | ) |
| 1649 | .unwrap(); |
| 1650 | assert!( |
| 1651 | set_undef, |
| 1652 | "readonly instance property must have no setter in descriptor" |
| 1653 | ); |
| 1654 | Ok(()) |
| 1655 | }); |
| 1656 | } |
| 1657 | |
| 1658 | // ---- instance, with name, rw -------------------------------------------------- |
| 1659 | |
| 1660 | #[test] |
| 1661 | fn combo_instance_named_rw_get() { |
| 1662 | let harness = crate::Harness::new(); |
| 1663 | harness.run_in_context(|lock, ctx| { |
| 1664 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1665 | ctx.set_global("o", r.to_js(lock)); |
| 1666 | let v: String = ctx.eval(lock, "o.namedG").unwrap(); |
| 1667 | assert_eq!(v, "g"); |
| 1668 | Ok(()) |
| 1669 | }); |
| 1670 | } |
| 1671 | |
| 1672 | #[test] |
| 1673 | fn combo_instance_named_rw_set() { |
| 1674 | let harness = crate::Harness::new(); |
| 1675 | harness.run_in_context(|lock, ctx| { |
| 1676 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1677 | ctx.set_global("o", r.to_js(lock)); |
| 1678 | ctx.eval_raw("o.namedG = 'ggg'").unwrap(); |
| 1679 | let v: String = ctx.eval(lock, "o.namedG").unwrap(); |
| 1680 | assert_eq!(v, "ggg"); |
| 1681 | Ok(()) |
| 1682 | }); |
| 1683 | } |
| 1684 | |
| 1685 | #[test] |
| 1686 | fn combo_instance_named_rw_is_own_property() { |
| 1687 | let harness = crate::Harness::new(); |
| 1688 | harness.run_in_context(|lock, ctx| { |
| 1689 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1690 | ctx.set_global("o", r.to_js(lock)); |
| 1691 | let own: bool = ctx |
| 1692 | .eval(lock, "Object.prototype.hasOwnProperty.call(o, 'namedG')") |
| 1693 | .unwrap(); |
| 1694 | assert!(own, "named instance property must be an own property"); |
| 1695 | Ok(()) |
| 1696 | }); |
| 1697 | } |
| 1698 | |
| 1699 | #[test] |
| 1700 | fn combo_instance_named_rw_raw_rust_name_hidden() { |
| 1701 | let harness = crate::Harness::new(); |
| 1702 | harness.run_in_context(|lock, ctx| { |
| 1703 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1704 | ctx.set_global("o", r.to_js(lock)); |
| 1705 | let hidden: bool = ctx |
| 1706 | .eval( |
| 1707 | lock, |
| 1708 | "typeof o.getG === 'undefined' && typeof o.g === 'undefined'", |
| 1709 | ) |
| 1710 | .unwrap(); |
| 1711 | assert!( |
| 1712 | hidden, |
| 1713 | "camelCase default name must be hidden when name is overridden" |
| 1714 | ); |
| 1715 | Ok(()) |
| 1716 | }); |
| 1717 | } |
| 1718 | |
| 1719 | // ---- instance, with name, readonly -------------------------------------------- |
| 1720 | |
| 1721 | #[test] |
| 1722 | fn combo_instance_named_readonly_get() { |
| 1723 | let harness = crate::Harness::new(); |
| 1724 | harness.run_in_context(|lock, ctx| { |
| 1725 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1726 | ctx.set_global("o", r.to_js(lock)); |
| 1727 | let v: String = ctx.eval(lock, "o.namedH").unwrap(); |
| 1728 | assert_eq!(v, "h"); |
| 1729 | Ok(()) |
| 1730 | }); |
| 1731 | } |
| 1732 | |
| 1733 | #[test] |
| 1734 | fn combo_instance_named_readonly_throws_in_strict() { |
| 1735 | let harness = crate::Harness::new(); |
| 1736 | harness.run_in_context(|lock, ctx| { |
| 1737 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1738 | ctx.set_global("o", r.to_js(lock)); |
| 1739 | let result = ctx.eval_raw("'use strict'; o.namedH = 'x'"); |
| 1740 | assert!( |
| 1741 | result.is_err(), |
| 1742 | "named readonly instance property must throw in strict mode" |
| 1743 | ); |
| 1744 | Ok(()) |
| 1745 | }); |
| 1746 | } |
| 1747 | |
| 1748 | #[test] |
| 1749 | fn combo_instance_named_readonly_is_own_property() { |
| 1750 | let harness = crate::Harness::new(); |
| 1751 | harness.run_in_context(|lock, ctx| { |
| 1752 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1753 | ctx.set_global("o", r.to_js(lock)); |
| 1754 | let own: bool = ctx |
| 1755 | .eval(lock, "Object.prototype.hasOwnProperty.call(o, 'namedH')") |
| 1756 | .unwrap(); |
| 1757 | assert!( |
| 1758 | own, |
| 1759 | "named readonly instance property must still be an own property" |
| 1760 | ); |
| 1761 | Ok(()) |
| 1762 | }); |
| 1763 | } |
| 1764 | |
| 1765 | // ---- attribute ordering: name then readonly (prototype) ----------------------- |
| 1766 | |
| 1767 | #[test] |
| 1768 | fn combo_attr_order_name_then_readonly_prototype_get() { |
| 1769 | let harness = crate::Harness::new(); |
| 1770 | harness.run_in_context(|lock, ctx| { |
| 1771 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1772 | ctx.set_global("o", r.to_js(lock)); |
| 1773 | // #[jsg_property(prototype, name = "namedI", readonly)] |
| 1774 | let v: Number = ctx.eval(lock, "o.namedI").unwrap(); |
| 1775 | assert!((v.value() - 9.0).abs() < f64::EPSILON); |
| 1776 | Ok(()) |
| 1777 | }); |
| 1778 | } |
| 1779 | |
| 1780 | #[test] |
| 1781 | fn combo_attr_order_name_then_readonly_prototype_is_readonly() { |
| 1782 | let harness = crate::Harness::new(); |
| 1783 | harness.run_in_context(|lock, ctx| { |
| 1784 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1785 | ctx.set_global("o", r.to_js(lock)); |
| 1786 | let result = ctx.eval_raw("'use strict'; o.namedI = 0"); |
| 1787 | assert!( |
| 1788 | result.is_err(), |
| 1789 | "name-before-readonly prototype property must be read-only" |
| 1790 | ); |
| 1791 | Ok(()) |
| 1792 | }); |
| 1793 | } |
| 1794 | |
| 1795 | // ---- attribute ordering: readonly then name (instance) ------------------------ |
| 1796 | |
| 1797 | #[test] |
| 1798 | fn combo_attr_order_readonly_then_name_instance_get() { |
| 1799 | let harness = crate::Harness::new(); |
| 1800 | harness.run_in_context(|lock, ctx| { |
| 1801 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1802 | ctx.set_global("o", r.to_js(lock)); |
| 1803 | // #[jsg_property(instance, readonly, name = "namedJ")] |
| 1804 | let v: Number = ctx.eval(lock, "o.namedJ").unwrap(); |
| 1805 | assert!((v.value() - 10.0).abs() < f64::EPSILON); |
| 1806 | Ok(()) |
| 1807 | }); |
| 1808 | } |
| 1809 | |
| 1810 | #[test] |
| 1811 | fn combo_attr_order_readonly_then_name_instance_is_own_and_readonly() { |
| 1812 | let harness = crate::Harness::new(); |
| 1813 | harness.run_in_context(|lock, ctx| { |
| 1814 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1815 | ctx.set_global("o", r.to_js(lock)); |
| 1816 | let own: bool = ctx |
| 1817 | .eval(lock, "Object.prototype.hasOwnProperty.call(o, 'namedJ')") |
| 1818 | .unwrap(); |
| 1819 | assert!( |
| 1820 | own, |
| 1821 | "readonly-before-name instance property must be an own property" |
| 1822 | ); |
| 1823 | let result = ctx.eval_raw("'use strict'; o.namedJ = 0"); |
| 1824 | assert!( |
| 1825 | result.is_err(), |
| 1826 | "readonly-before-name instance property must be read-only" |
| 1827 | ); |
| 1828 | Ok(()) |
| 1829 | }); |
| 1830 | } |
| 1831 | |
| 1832 | // ---- instance props enumerable, prototype props not --------------------------- |
| 1833 | |
| 1834 | #[test] |
| 1835 | fn combo_object_keys_contains_only_instance_props() { |
| 1836 | let harness = crate::Harness::new(); |
| 1837 | harness.run_in_context(|lock, ctx| { |
| 1838 | let r = jsg::Rc::new(AllCombinations::new()); |
| 1839 | ctx.set_global("o", r.to_js(lock)); |
| 1840 | |
| 1841 | // Instance props that MUST appear in Object.keys(). |
| 1842 | for key in &["e", "f", "namedG", "namedH", "namedJ"] { |
| 1843 | let found: bool = ctx |
| 1844 | .eval(lock, &format!("Object.keys(o).includes({key:?})")) |
| 1845 | .unwrap(); |
| 1846 | assert!(found, "instance key '{key}' must appear in Object.keys()"); |
| 1847 | } |
| 1848 | |
| 1849 | // Prototype props that must NOT appear in Object.keys(). |
| 1850 | for key in &["a", "b", "namedC", "namedD", "namedI"] { |
| 1851 | let found: bool = ctx |
| 1852 | .eval(lock, &format!("Object.keys(o).includes({key:?})")) |
| 1853 | .unwrap(); |
| 1854 | assert!( |
| 1855 | !found, |
| 1856 | "prototype key '{key}' must NOT appear in Object.keys()" |
| 1857 | ); |
| 1858 | } |
| 1859 | Ok(()) |
| 1860 | }); |
| 1861 | } |
| 1862 | |
| 1863 | // ---- multiple instances are independent (instance properties) ----------------- |
| 1864 | |
| 1865 | #[test] |
| 1866 | fn combo_instance_props_independent_across_instances() { |
| 1867 | let harness = crate::Harness::new(); |
| 1868 | harness.run_in_context(|lock, ctx| { |
| 1869 | let a = jsg::Rc::new(AllCombinations::new()); |
| 1870 | let b = jsg::Rc::new(AllCombinations::new()); |
| 1871 | ctx.set_global("a", a.to_js(lock)); |
| 1872 | ctx.set_global("b", b.to_js(lock)); |
| 1873 | ctx.eval_raw("a.e = 'alpha'").unwrap(); |
| 1874 | let ea: String = ctx.eval(lock, "a.e").unwrap(); |
| 1875 | let eb: String = ctx.eval(lock, "b.e").unwrap(); |
| 1876 | assert_eq!(ea, "alpha"); |
| 1877 | assert_eq!(eb, "e", "mutating a.e must not affect b.e"); |
| 1878 | Ok(()) |
| 1879 | }); |
| 1880 | } |
| 1881 | |
| 1882 | // ============================================================================= |
| 1883 | // get_/set_ prefix detection for getter/setter pairing |
| 1884 | // |
| 1885 | // These tests explicitly verify that: |
| 1886 | // - Methods named `get_<stem>` are registered as getters. |
| 1887 | // - Methods named `set_<stem>` are registered as setters. |
| 1888 | // - A getter and setter with the same `<stem>` are paired into a single |
| 1889 | // read-write property whose JS name is the camelCase form of `<stem>`. |
| 1890 | // - The raw Rust method names (`getX` / `setX`) are never exposed to JS. |
| 1891 | // - A `get_`-only method produces a read-only property. |
| 1892 | // ============================================================================= |
| 1893 | |
| 1894 | /// Resource with a rw property (`get_count`/`set_count`) and a getter-only |
| 1895 | /// property (`get_readonly_val`) to exercise prefix-based detection directly. |
| 1896 | #[jsg_resource] |
| 1897 | struct PrefixDetection { |
| 1898 | count: Cell<f64>, |
| 1899 | readonly_val: Cell<f64>, |
| 1900 | } |
| 1901 | |
| 1902 | #[jsg_resource] |
| 1903 | impl PrefixDetection { |
| 1904 | /// `get_count` → getter for JS property "count" |
| 1905 | #[jsg_property(prototype)] |
| 1906 | pub fn get_count(&self) -> Number { |
| 1907 | Number::new(self.count.get()) |
| 1908 | } |
| 1909 | |
| 1910 | /// `set_count` → setter for JS property "count" (same stem as `get_count`) |
| 1911 | #[jsg_property(prototype)] |
| 1912 | pub fn set_count(&self, v: Number) { |
| 1913 | self.count.set(v.value()); |
| 1914 | } |
| 1915 | |
| 1916 | /// `get_readonly_val` → getter-only for JS property "readonlyVal" |
| 1917 | #[jsg_property(prototype, readonly)] |
| 1918 | pub fn get_readonly_val(&self) -> Number { |
| 1919 | Number::new(self.readonly_val.get()) |
| 1920 | } |
| 1921 | } |
| 1922 | |
| 1923 | impl PrefixDetection { |
| 1924 | fn new(count: f64, readonly_val: f64) -> Self { |
| 1925 | Self { |
| 1926 | count: Cell::new(count), |
| 1927 | readonly_val: Cell::new(readonly_val), |
| 1928 | } |
| 1929 | } |
| 1930 | } |
| 1931 | |
| 1932 | /// The `get_` prefix identifies `get_count` as the getter; the value is |
| 1933 | /// accessible as the JS property `"count"` (prefix stripped, camelCased). |
| 1934 | #[test] |
| 1935 | fn get_prefix_identifies_getter() { |
| 1936 | let harness = crate::Harness::new(); |
| 1937 | harness.run_in_context(|lock, ctx| { |
| 1938 | let r = jsg::Rc::new(PrefixDetection::new(42.0, 0.0)); |
| 1939 | ctx.set_global("obj", r.to_js(lock)); |
| 1940 | let v: Number = ctx.eval(lock, "obj.count").unwrap(); |
| 1941 | assert!( |
| 1942 | (v.value() - 42.0).abs() < f64::EPSILON, |
| 1943 | "get_count must be accessible as JS property 'count'" |
| 1944 | ); |
| 1945 | Ok(()) |
| 1946 | }); |
| 1947 | } |
| 1948 | |
| 1949 | /// The `set_` prefix identifies `set_count` as the setter for the same |
| 1950 | /// JS property `"count"` (matching stem with `get_count`). |
| 1951 | #[test] |
| 1952 | fn set_prefix_identifies_setter() { |
| 1953 | let harness = crate::Harness::new(); |
| 1954 | harness.run_in_context(|lock, ctx| { |
| 1955 | let r = jsg::Rc::new(PrefixDetection::new(0.0, 0.0)); |
| 1956 | ctx.set_global("obj", r.to_js(lock)); |
| 1957 | ctx.eval_raw("obj.count = 99").unwrap(); |
| 1958 | let v: Number = ctx.eval(lock, "obj.count").unwrap(); |
| 1959 | assert!( |
| 1960 | (v.value() - 99.0).abs() < f64::EPSILON, |
| 1961 | "set_count must write to JS property 'count'" |
| 1962 | ); |
| 1963 | Ok(()) |
| 1964 | }); |
| 1965 | } |
| 1966 | |
| 1967 | /// `get_count` and `set_count` share the same JS name `"count"` because both |
| 1968 | /// have the same stem (`count`) after prefix stripping — they form a single |
| 1969 | /// read-write property. |
| 1970 | #[test] |
| 1971 | fn getter_setter_paired_by_matching_stem() { |
| 1972 | let harness = crate::Harness::new(); |
| 1973 | harness.run_in_context(|lock, ctx| { |
| 1974 | let r = jsg::Rc::new(PrefixDetection::new(5.0, 0.0)); |
| 1975 | ctx.set_global("obj", r.to_js(lock)); |
| 1976 | // Both getter and setter must share the single JS name. |
| 1977 | let descriptor: bool = ctx |
| 1978 | .eval( |
| 1979 | lock, |
| 1980 | "(function() { |
| 1981 | var d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), 'count'); |
| 1982 | return typeof d.get === 'function' && typeof d.set === 'function'; |
| 1983 | })()", |
| 1984 | ) |
| 1985 | .unwrap(); |
| 1986 | assert!( |
| 1987 | descriptor, |
| 1988 | "get_count and set_count must be paired as getter+setter on JS property 'count'" |
| 1989 | ); |
| 1990 | Ok(()) |
| 1991 | }); |
| 1992 | } |
| 1993 | |
| 1994 | /// The raw Rust getter name (`getCount`) must NOT be exposed as a JS property. |
| 1995 | #[test] |
| 1996 | fn getter_raw_rust_name_not_exposed() { |
| 1997 | let harness = crate::Harness::new(); |
| 1998 | harness.run_in_context(|lock, ctx| { |
| 1999 | let r = jsg::Rc::new(PrefixDetection::new(1.0, 0.0)); |
| 2000 | ctx.set_global("obj", r.to_js(lock)); |
| 2001 | let undef: bool = ctx |
| 2002 | .eval(lock, "typeof obj.getCount === 'undefined'") |
| 2003 | .unwrap(); |
| 2004 | assert!( |
| 2005 | undef, |
| 2006 | "raw getter name 'getCount' must not be exposed to JS" |
| 2007 | ); |
| 2008 | Ok(()) |
| 2009 | }); |
| 2010 | } |
| 2011 | |
| 2012 | /// The raw Rust setter name (`setCount`) must NOT be exposed as a JS property. |
| 2013 | #[test] |
| 2014 | fn setter_raw_rust_name_not_exposed() { |
| 2015 | let harness = crate::Harness::new(); |
| 2016 | harness.run_in_context(|lock, ctx| { |
| 2017 | let r = jsg::Rc::new(PrefixDetection::new(1.0, 0.0)); |
| 2018 | ctx.set_global("obj", r.to_js(lock)); |
| 2019 | let undef: bool = ctx |
| 2020 | .eval(lock, "typeof obj.setCount === 'undefined'") |
| 2021 | .unwrap(); |
| 2022 | assert!( |
| 2023 | undef, |
| 2024 | "raw setter name 'setCount' must not be exposed to JS" |
| 2025 | ); |
| 2026 | Ok(()) |
| 2027 | }); |
| 2028 | } |
| 2029 | |
| 2030 | /// A method annotated with only `get_` (no matching `set_`) produces a |
| 2031 | /// getter-only (read-only) property; the JS property descriptor has a getter |
| 2032 | /// but no setter. |
| 2033 | #[test] |
| 2034 | fn getter_only_method_produces_readonly_property() { |
| 2035 | let harness = crate::Harness::new(); |
| 2036 | harness.run_in_context(|lock, ctx| { |
| 2037 | let r = jsg::Rc::new(PrefixDetection::new(0.0, 7.0)); |
| 2038 | ctx.set_global("obj", r.to_js(lock)); |
| 2039 | let v: Number = ctx.eval(lock, "obj.readonlyVal").unwrap(); |
| 2040 | assert!( |
| 2041 | (v.value() - 7.0).abs() < f64::EPSILON, |
| 2042 | "get_readonly_val must be readable as 'readonlyVal'" |
| 2043 | ); |
| 2044 | // No setter in the descriptor. |
| 2045 | let no_setter: bool = ctx |
| 2046 | .eval( |
| 2047 | lock, |
| 2048 | "(function() { |
| 2049 | var d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), 'readonlyVal'); |
| 2050 | return typeof d.set === 'undefined'; |
| 2051 | })()", |
| 2052 | ) |
| 2053 | .unwrap(); |
| 2054 | assert!(no_setter, "getter-only property must have no setter in descriptor"); |
| 2055 | Ok(()) |
| 2056 | }); |
| 2057 | } |
| 2058 | |
| 2059 | /// In strict mode, assigning to a getter-only property throws a `TypeError`. |
| 2060 | #[test] |
| 2061 | fn getter_only_throws_on_write_in_strict_mode() { |
| 2062 | let harness = crate::Harness::new(); |
| 2063 | harness.run_in_context(|lock, ctx| { |
| 2064 | let r = jsg::Rc::new(PrefixDetection::new(0.0, 3.0)); |
| 2065 | ctx.set_global("obj", r.to_js(lock)); |
| 2066 | let result = ctx.eval_raw("'use strict'; obj.readonlyVal = 99"); |
| 2067 | assert!( |
| 2068 | result.is_err(), |
| 2069 | "assigning to getter-only property must throw in strict mode" |
| 2070 | ); |
| 2071 | Ok(()) |
| 2072 | }); |
| 2073 | } |
| 2074 | |
| 2075 | // ============================================================================= |
| 2076 | // Default placement (`#[jsg_property]` without explicit placement argument) |
| 2077 | // |
| 2078 | // When no placement is specified, the macro defaults to `prototype` placement. |
| 2079 | // These tests verify this default matches the behaviour of explicit |
| 2080 | // `#[jsg_property(prototype)]`. |
| 2081 | // ============================================================================= |
| 2082 | |
| 2083 | /// Resource using `#[jsg_property]` (no placement arg) to exercise the default. |
| 2084 | #[jsg_resource] |
| 2085 | struct DefaultPlacement { |
| 2086 | name: RefCell<String>, |
| 2087 | version: Cell<f64>, |
| 2088 | } |
| 2089 | |
| 2090 | #[jsg_resource] |
| 2091 | impl DefaultPlacement { |
| 2092 | /// No placement arg → defaults to `prototype`. |
| 2093 | #[jsg_property] |
| 2094 | pub fn get_name(&self) -> String { |
| 2095 | self.name.borrow().clone() |
| 2096 | } |
| 2097 | |
| 2098 | #[jsg_property] |
| 2099 | pub fn set_name(&self, v: String) { |
| 2100 | *self.name.borrow_mut() = v; |
| 2101 | } |
| 2102 | |
| 2103 | /// Getter-only with default placement. |
| 2104 | #[jsg_property(readonly)] |
| 2105 | pub fn get_version(&self) -> Number { |
| 2106 | Number::new(self.version.get()) |
| 2107 | } |
| 2108 | } |
| 2109 | |
| 2110 | impl DefaultPlacement { |
| 2111 | fn new(name: impl Into<String>, version: f64) -> Self { |
| 2112 | Self { |
| 2113 | name: RefCell::new(name.into()), |
| 2114 | version: Cell::new(version), |
| 2115 | } |
| 2116 | } |
| 2117 | } |
| 2118 | |
| 2119 | /// Default placement (`#[jsg_property]` with no args) places the property on |
| 2120 | /// the prototype, not the instance — `Object.keys()` is empty. |
| 2121 | #[test] |
| 2122 | fn default_placement_is_prototype_not_instance() { |
| 2123 | let harness = crate::Harness::new(); |
| 2124 | harness.run_in_context(|lock, ctx| { |
| 2125 | let r = jsg::Rc::new(DefaultPlacement::new("hello", 1.0)); |
| 2126 | ctx.set_global("obj", r.to_js(lock)); |
| 2127 | // Prototype property → not an own property. |
| 2128 | let own: bool = ctx |
| 2129 | .eval(lock, "Object.prototype.hasOwnProperty.call(obj, 'name')") |
| 2130 | .unwrap(); |
| 2131 | assert!( |
| 2132 | !own, |
| 2133 | "default-placement property must NOT be an own property" |
| 2134 | ); |
| 2135 | // Prototype property → not in Object.keys(). |
| 2136 | let keys: String = ctx.eval(lock, "Object.keys(obj).join(',')").unwrap(); |
| 2137 | assert_eq!( |
| 2138 | keys, "", |
| 2139 | "default-placement property must not appear in Object.keys()" |
| 2140 | ); |
| 2141 | Ok(()) |
| 2142 | }); |
| 2143 | } |
| 2144 | |
| 2145 | /// The getter works with default placement. |
| 2146 | #[test] |
| 2147 | fn default_placement_getter_returns_value() { |
| 2148 | let harness = crate::Harness::new(); |
| 2149 | harness.run_in_context(|lock, ctx| { |
| 2150 | let r = jsg::Rc::new(DefaultPlacement::new("world", 2.0)); |
| 2151 | ctx.set_global("obj", r.to_js(lock)); |
| 2152 | let v: String = ctx.eval(lock, "obj.name").unwrap(); |
| 2153 | assert_eq!(v, "world"); |
| 2154 | Ok(()) |
| 2155 | }); |
| 2156 | } |
| 2157 | |
| 2158 | /// The setter works with default placement. |
| 2159 | #[test] |
| 2160 | fn default_placement_setter_updates_value() { |
| 2161 | let harness = crate::Harness::new(); |
| 2162 | harness.run_in_context(|lock, ctx| { |
| 2163 | let r = jsg::Rc::new(DefaultPlacement::new("before", 0.0)); |
| 2164 | ctx.set_global("obj", r.to_js(lock)); |
| 2165 | ctx.eval_raw("obj.name = 'after'").unwrap(); |
| 2166 | let v: String = ctx.eval(lock, "obj.name").unwrap(); |
| 2167 | assert_eq!(v, "after"); |
| 2168 | Ok(()) |
| 2169 | }); |
| 2170 | } |
| 2171 | |
| 2172 | /// A readonly property with default placement is accessible but rejects writes |
| 2173 | /// in strict mode. |
| 2174 | #[test] |
| 2175 | fn default_placement_readonly_throws_on_write() { |
| 2176 | let harness = crate::Harness::new(); |
| 2177 | harness.run_in_context(|lock, ctx| { |
| 2178 | let r = jsg::Rc::new(DefaultPlacement::new("x", 42.0)); |
| 2179 | ctx.set_global("obj", r.to_js(lock)); |
| 2180 | let v: Number = ctx.eval(lock, "obj.version").unwrap(); |
| 2181 | assert!((v.value() - 42.0).abs() < f64::EPSILON); |
| 2182 | let result = ctx.eval_raw("'use strict'; obj.version = 0"); |
| 2183 | assert!( |
| 2184 | result.is_err(), |
| 2185 | "readonly default-placement property must throw on write" |
| 2186 | ); |
| 2187 | Ok(()) |
| 2188 | }); |
| 2189 | } |
| 2190 | |
| 2191 | /// Default placement is on the prototype chain — `"name" in obj` is true. |
| 2192 | #[test] |
| 2193 | fn default_placement_found_via_in_operator() { |
| 2194 | let harness = crate::Harness::new(); |
| 2195 | harness.run_in_context(|lock, ctx| { |
| 2196 | let r = jsg::Rc::new(DefaultPlacement::new("hi", 1.0)); |
| 2197 | ctx.set_global("obj", r.to_js(lock)); |
| 2198 | let found: bool = ctx.eval(lock, "'name' in obj").unwrap(); |
| 2199 | assert!(found, "prototype property must be found by 'in' operator"); |
| 2200 | Ok(()) |
| 2201 | }); |
| 2202 | } |