Skip to content
File

Blob: src/rust/jsg-test/tests/resource_properties.rs

rust2203 lines
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 
8use std::cell::Cell;
9use std::cell::RefCell;
10 
11use jsg::Number;
12use jsg::ToJS;
13use jsg_macros::jsg_inspect_property;
14use jsg_macros::jsg_method;
15use jsg_macros::jsg_property;
16use jsg_macros::jsg_resource;
17use 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]
25struct Counter {
26 value: Cell<f64>,
27 label: RefCell<String>,
28}
29 
30#[jsg_resource]
31impl 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 
70impl 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]
81struct Token {
82 id: RefCell<String>,
83 kind: String,
84}
85 
86#[jsg_resource]
87impl 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 
105impl 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]
116struct MultiWord {
117 first_name: RefCell<String>,
118 last_name: RefCell<String>,
119}
120 
121#[jsg_resource]
122impl 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 
139impl 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]
150struct ExplicitName {
151 x: Cell<f64>,
152}
153 
154#[jsg_resource]
155impl 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 
172impl 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]
180struct MaybeHolder {
181 inner: Cell<Option<f64>>,
182}
183 
184#[jsg_resource]
185impl 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 
197impl 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]
216fn 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]
228fn 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]
242fn 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]
258fn 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]
271fn 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]
284fn 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]
302fn 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]
317fn 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]
335fn 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]
350fn 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]
362fn 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]
380fn 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]
404fn 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]
427fn 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]
449fn 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]
477fn 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]
494fn 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]
516fn 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]
530fn 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]
543fn 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]
566fn 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]
578fn 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]
591fn 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]
609fn 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]
621fn 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]
637fn 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]
654fn 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]
666fn 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]
682fn 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]
695fn 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]
712fn 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]
726fn 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]
757fn 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]
774fn 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]
789fn 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]
810fn 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]
827fn 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]
842fn 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]
859fn 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]
891fn 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]
928fn 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.
961const 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]
975fn 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]
991fn 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]
1007fn 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]
1039fn 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]
1071fn 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]
1097fn 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]
1127fn 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]
1147fn 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]
1171struct Mixed {
1172 proto_val: Cell<f64>,
1173 instance_val: RefCell<String>,
1174}
1175 
1176#[jsg_resource]
1177impl 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 
1199impl 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]
1209fn 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]
1232fn 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]
1246fn 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]
1276struct 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]
1292impl 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 
1370impl 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]
1390fn 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]
1402fn 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]
1415fn 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]
1431fn 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]
1443fn 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]
1458fn 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]
1479fn 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]
1491fn 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]
1504fn 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]
1527fn 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]
1539fn 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]
1556fn 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]
1568fn 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]
1581fn 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]
1595fn 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]
1612fn 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]
1624fn 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]
1639fn 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]
1661fn 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]
1673fn 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]
1686fn 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]
1700fn 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]
1722fn 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]
1734fn 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]
1749fn 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]
1768fn 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]
1781fn 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]
1798fn 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]
1811fn 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]
1835fn 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]
1866fn 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]
1897struct PrefixDetection {
1898 count: Cell<f64>,
1899 readonly_val: Cell<f64>,
1900}
1901 
1902#[jsg_resource]
1903impl 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 
1923impl 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]
1935fn 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]
1952fn 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]
1971fn 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]
1996fn 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]
2014fn 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]
2034fn 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]
2061fn 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]
2085struct DefaultPlacement {
2086 name: RefCell<String>,
2087 version: Cell<f64>,
2088}
2089 
2090#[jsg_resource]
2091impl 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 
2110impl 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]
2122fn 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]
2147fn 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]
2160fn 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]
2175fn 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]
2193fn 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}