Skip to content
File

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

rust359 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 resource `ToJS` and `FromJS` macro-generated implementations.
6//!
7//! These tests verify the automatically derived `ToJS` and `FromJS` implementations
8//! that the `#[jsg_resource]` macro generates for resource types:
9//!
10//! - `ToJS`: allocates a `Ref<R>` and wraps it as a JS object (alloc + wrap)
11//! - `FromJS`: unwraps a JS object back to a `Ref<R>`, returning a `TypeError`
12//! for non-matching values
13 
14use jsg::FromJS;
15use jsg::Number;
16use jsg::ToJS;
17use jsg_macros::jsg_method;
18use jsg_macros::jsg_resource;
19 
20#[jsg_resource]
21struct Greeter {
22 greeting: String,
23}
24 
25#[jsg_resource]
26impl Greeter {
27 #[jsg_method]
28 pub fn greet(&self, name: &str) -> String {
29 format!("{}, {}!", self.greeting, name)
30 }
31}
32 
33#[jsg_resource]
34struct Counter {
35 value: f64,
36}
37 
38#[jsg_resource]
39impl Counter {
40 #[jsg_method]
41 pub fn get_value(&self) -> Number {
42 Number::new(self.value)
43 }
44}
45 
46// =============================================================================
47// ToJS tests
48// =============================================================================
49 
50/// `ToJS` creates a JS object that has the resource's methods.
51#[test]
52fn to_js_creates_object_with_methods() {
53 let harness = crate::Harness::new();
54 harness.run_in_context(|lock, ctx| {
55 let greeter = Greeter {
56 greeting: "Hello".to_owned(),
57 };
58 let js_val = greeter.to_js(lock);
59 ctx.set_global("greeter", js_val);
60 
61 let result: String = ctx.eval(lock, "greeter.greet('World')").unwrap();
62 assert_eq!(result, "Hello, World!");
63 Ok(())
64 });
65}
66 
67/// `ToJS` produces a valid object for different resource types.
68#[test]
69fn to_js_works_for_different_resource_types() {
70 let harness = crate::Harness::new();
71 harness.run_in_context(|lock, ctx| {
72 let counter = Counter { value: 42.0 };
73 let js_val = counter.to_js(lock);
74 ctx.set_global("counter", js_val);
75 
76 let result: Number = ctx.eval(lock, "counter.getValue()").unwrap();
77 assert!((result.value() - 42.0).abs() < f64::EPSILON);
78 Ok(())
79 });
80}
81 
82/// `ToJS` called twice on different values creates distinct JS objects.
83#[test]
84fn to_js_creates_distinct_objects() {
85 let harness = crate::Harness::new();
86 harness.run_in_context(|lock, ctx| {
87 let g1 = Greeter {
88 greeting: "Hi".to_owned(),
89 };
90 let g2 = Greeter {
91 greeting: "Hey".to_owned(),
92 };
93 ctx.set_global("g1", g1.to_js(lock));
94 ctx.set_global("g2", g2.to_js(lock));
95 
96 let same: bool = ctx.eval(lock, "g1 === g2").unwrap();
97 assert!(
98 !same,
99 "distinct resources should produce distinct JS objects"
100 );
101 
102 let r1: String = ctx.eval(lock, "g1.greet('A')").unwrap();
103 let r2: String = ctx.eval(lock, "g2.greet('A')").unwrap();
104 assert_eq!(r1, "Hi, A!");
105 assert_eq!(r2, "Hey, A!");
106 Ok(())
107 });
108}
109 
110// =============================================================================
111// FromJS tests
112// =============================================================================
113 
114/// `FromJS` unwraps a JS-wrapped resource back to a `Ref<R>`.
115#[test]
116fn from_js_unwraps_resource() {
117 let harness = crate::Harness::new();
118 harness.run_in_context(|lock, ctx| {
119 let greeter = Greeter {
120 greeting: "Howdy".to_owned(),
121 };
122 ctx.set_global("greeter", greeter.to_js(lock));
123 
124 // Get the JS value back and use FromJS to unwrap it.
125 let js_val = ctx.eval_raw("greeter").unwrap();
126 let r = Greeter::from_js(lock, js_val).expect("FromJS should succeed");
127 assert_eq!(r.greeting, "Howdy");
128 Ok(())
129 });
130}
131 
132/// `FromJS` returns a `Ref<R>` that keeps the resource alive.
133#[test]
134fn from_js_ref_keeps_resource_alive() {
135 use std::sync::atomic::AtomicUsize;
136 use std::sync::atomic::Ordering;
137 
138 static DROPS: AtomicUsize = AtomicUsize::new(0);
139 
140 #[jsg_resource]
141 struct Tracked {
142 tag: String,
143 }
144 
145 impl Drop for Tracked {
146 fn drop(&mut self) {
147 DROPS.fetch_add(1, Ordering::SeqCst);
148 }
149 }
150 
151 #[jsg_resource]
152 impl Tracked {}
153 
154 DROPS.store(0, Ordering::SeqCst);
155 
156 let harness = crate::Harness::new();
157 harness.run_in_context(|lock, ctx| {
158 let tracked = Tracked {
159 tag: "alive".to_owned(),
160 };
161 ctx.set_global("obj", tracked.to_js(lock));
162 
163 let js_val = ctx.eval_raw("obj").unwrap();
164 let r = Tracked::from_js(lock, js_val).expect("FromJS should succeed");
165 assert_eq!(r.tag, "alive");
166 
167 // The Ref from FromJS keeps the resource alive even after GC.
168 crate::Harness::request_gc(lock);
169 assert_eq!(DROPS.load(Ordering::SeqCst), 0);
170 assert_eq!(r.tag, "alive");
171 
172 std::mem::drop(r);
173 Ok(())
174 });
175}
176 
177/// `FromJS` returns a `TypeError` for a plain JS object.
178#[test]
179fn from_js_rejects_plain_object() {
180 let harness = crate::Harness::new();
181 harness.run_in_context(|lock, ctx| {
182 let js_val = ctx.eval_raw("({})").unwrap();
183 let err = Greeter::from_js(lock, js_val).unwrap_err();
184 assert_eq!(err.name, jsg::ExceptionType::TypeError);
185 assert_eq!(err.message, "expected Greeter, got object");
186 Ok(())
187 });
188}
189 
190/// `FromJS` returns a `TypeError` for a string.
191#[test]
192fn from_js_rejects_string() {
193 let harness = crate::Harness::new();
194 harness.run_in_context(|lock, ctx| {
195 let js_val = ctx.eval_raw("'not a resource'").unwrap();
196 let err = Greeter::from_js(lock, js_val).unwrap_err();
197 assert_eq!(err.name, jsg::ExceptionType::TypeError);
198 assert_eq!(err.message, "expected Greeter, got string");
199 Ok(())
200 });
201}
202 
203/// `FromJS` returns a `TypeError` for a number.
204#[test]
205fn from_js_rejects_number() {
206 let harness = crate::Harness::new();
207 harness.run_in_context(|lock, ctx| {
208 let js_val = ctx.eval_raw("42").unwrap();
209 let err = Greeter::from_js(lock, js_val).unwrap_err();
210 assert_eq!(err.name, jsg::ExceptionType::TypeError);
211 assert_eq!(err.message, "expected Greeter, got number");
212 Ok(())
213 });
214}
215 
216/// `FromJS` returns a `TypeError` for null.
217#[test]
218fn from_js_rejects_null() {
219 let harness = crate::Harness::new();
220 harness.run_in_context(|lock, ctx| {
221 let js_val = ctx.eval_raw("null").unwrap();
222 let err = Greeter::from_js(lock, js_val).unwrap_err();
223 assert_eq!(err.name, jsg::ExceptionType::TypeError);
224 // typeof null === "object" in JavaScript
225 assert_eq!(err.message, "expected Greeter, got object");
226 Ok(())
227 });
228}
229 
230/// `FromJS` returns a `TypeError` for undefined.
231#[test]
232fn from_js_rejects_undefined() {
233 let harness = crate::Harness::new();
234 harness.run_in_context(|lock, ctx| {
235 let js_val = ctx.eval_raw("undefined").unwrap();
236 let err = Greeter::from_js(lock, js_val).unwrap_err();
237 assert_eq!(err.name, jsg::ExceptionType::TypeError);
238 assert_eq!(err.message, "expected Greeter, got undefined");
239 Ok(())
240 });
241}
242 
243// =============================================================================
244// Round-trip tests
245// =============================================================================
246 
247/// `ToJS` followed by `FromJS` preserves the resource data.
248#[test]
249fn round_trip_preserves_data() {
250 let harness = crate::Harness::new();
251 harness.run_in_context(|lock, ctx| {
252 let original = Greeter {
253 greeting: "Bonjour".to_owned(),
254 };
255 ctx.set_global("obj", original.to_js(lock));
256 
257 let js_val = ctx.eval_raw("obj").unwrap();
258 let unwrapped = Greeter::from_js(lock, js_val).expect("round-trip should succeed");
259 assert_eq!(unwrapped.greeting, "Bonjour");
260 Ok(())
261 });
262}
263 
264/// Round-trip through JavaScript — resource passes through JS code and comes back.
265#[test]
266fn round_trip_through_js_identity_function() {
267 let harness = crate::Harness::new();
268 harness.run_in_context(|lock, ctx| {
269 let greeter = Greeter {
270 greeting: "Hola".to_owned(),
271 };
272 ctx.set_global("greeter", greeter.to_js(lock));
273 
274 // Pass through a JS identity function.
275 let js_val = ctx.eval_raw("(x => x)(greeter)").unwrap();
276 let unwrapped = Greeter::from_js(lock, js_val).expect("identity round-trip should work");
277 assert_eq!(unwrapped.greeting, "Hola");
278 Ok(())
279 });
280}
281 
282/// Round-trip preserves object identity — same wrapper object.
283#[test]
284fn round_trip_preserves_js_identity() {
285 let harness = crate::Harness::new();
286 harness.run_in_context(|lock, ctx| {
287 let greeter = Greeter {
288 greeting: "Ciao".to_owned(),
289 };
290 ctx.set_global("g", greeter.to_js(lock));
291 
292 let same: bool = ctx.eval(lock, "g === (x => x)(g)").unwrap();
293 assert!(
294 same,
295 "passing a resource through JS should preserve identity"
296 );
297 Ok(())
298 });
299}
300 
301// =============================================================================
302// FromJS via eval_raw (integration with test harness)
303// =============================================================================
304 
305/// Evaluating a global that holds a resource and calling `FromJS` returns a `Ref<R>`.
306#[test]
307fn eval_raw_returns_ref_via_from_js() {
308 let harness = crate::Harness::new();
309 harness.run_in_context(|lock, ctx| {
310 let greeter = Greeter {
311 greeting: "Salut".to_owned(),
312 };
313 ctx.set_global("g", greeter.to_js(lock));
314 
315 let js_val = ctx.eval_raw("g").unwrap();
316 let r = Greeter::from_js(lock, js_val).expect("FromJS should succeed");
317 assert_eq!(r.greeting, "Salut");
318 Ok(())
319 });
320}
321 
322/// `FromJS` on a non-resource value returns a `TypeError`.
323#[test]
324fn eval_raw_with_wrong_type_gives_type_error() {
325 let harness = crate::Harness::new();
326 harness.run_in_context(|lock, ctx| {
327 let js_val = ctx.eval_raw("42").unwrap();
328 let err = Greeter::from_js(lock, js_val).unwrap_err();
329 assert_eq!(err.name, jsg::ExceptionType::TypeError);
330 assert_eq!(err.message, "expected Greeter, got number");
331 Ok(())
332 });
333}
334 
335/// Resources stored in a JS array can be retrieved individually via `FromJS`.
336#[test]
337fn resource_stored_in_js_array_and_retrieved() {
338 let harness = crate::Harness::new();
339 harness.run_in_context(|lock, ctx| {
340 let g1 = Greeter {
341 greeting: "One".to_owned(),
342 };
343 let g2 = Greeter {
344 greeting: "Two".to_owned(),
345 };
346 ctx.set_global("g1", g1.to_js(lock));
347 ctx.set_global("g2", g2.to_js(lock));
348 
349 let js_val = ctx.eval_raw("([g1, g2])[0]").unwrap();
350 let r = Greeter::from_js(lock, js_val).expect("FromJS should succeed for index 0");
351 assert_eq!(r.greeting, "One");
352 
353 let js_val = ctx.eval_raw("([g1, g2])[1]").unwrap();
354 let r = Greeter::from_js(lock, js_val).expect("FromJS should succeed for index 1");
355 assert_eq!(r.greeting, "Two");
356 Ok(())
357 });
358}