#![allow(clippy::expect_used, clippy::unwrap_used)]
use ferridriver::protocol::{
PropertyEntry, RegExpValue, SerializationContext, SerializedValue, SpecialValue, TypedArrayKind, TypedArrayValue,
};
use ferridriver_script::bindings::convert::{quickjs_arg_to_serialized, serialized_value_to_quickjs};
use rquickjs::{AsyncContext, AsyncRuntime, async_with};
fn install_url(ctx: &rquickjs::Ctx<'_>) {
ferridriver_script::bindings::webapi::install(ctx).expect("install webapi globals");
}
fn arr(items: Vec<SerializedValue>) -> SerializedValue {
let mut alloc = SerializationContext::default();
SerializedValue::Array {
id: alloc.alloc_id(),
items,
}
}
fn obj(entries: Vec<(&str, SerializedValue)>) -> SerializedValue {
let mut alloc = SerializationContext::default();
SerializedValue::Object {
id: alloc.alloc_id(),
entries: entries
.into_iter()
.map(|(k, v)| PropertyEntry { k: k.to_string(), v })
.collect(),
}
}
async fn run<F>(f: F)
where
F: FnOnce(&rquickjs::Ctx<'_>) + Send + 'static,
{
let rt = AsyncRuntime::new().expect("runtime");
let ctx = AsyncContext::full(&rt).await.expect("context");
async_with!(ctx => |ctx| {
f(&ctx);
})
.await;
}
#[tokio::test]
async fn json_expressible_values_round_trip() {
let cases = vec![
SerializedValue::Bool(true),
SerializedValue::Bool(false),
SerializedValue::Number(42.0),
SerializedValue::Number(-1.5),
SerializedValue::Str("hello".to_string()),
SerializedValue::Special(SpecialValue::Null),
arr(vec![
SerializedValue::Number(1.0),
SerializedValue::Str("two".to_string()),
SerializedValue::Bool(false),
]),
obj(vec![
("a", SerializedValue::Number(1.0)),
("b", SerializedValue::Str("x".to_string())),
(
"c",
arr(vec![
SerializedValue::Bool(true),
SerializedValue::Special(SpecialValue::Null),
]),
),
]),
arr(vec![arr(vec![arr(vec![obj(vec![(
"deep",
SerializedValue::Number(7.0),
)])])])]),
];
run(move |ctx| {
for case in &cases {
let js = serialized_value_to_quickjs(ctx, case).expect("rehydrate");
let back = quickjs_arg_to_serialized(ctx, Some(js)).expect("re-serialize");
assert_eq!(
normalize(&back.value),
normalize(case),
"round-trip mismatch for {case:?}"
);
assert!(back.handles.is_empty());
}
})
.await;
}
fn normalize(v: &SerializedValue) -> SerializedValue {
match v {
SerializedValue::Array { items, .. } => SerializedValue::Array {
id: 0,
items: items.iter().map(normalize).collect(),
},
SerializedValue::Object { entries, .. } => SerializedValue::Object {
id: 0,
entries: entries
.iter()
.map(|e| PropertyEntry {
k: e.k.clone(),
v: normalize(&e.v),
})
.collect(),
},
other => other.clone(),
}
}
#[tokio::test]
async fn shared_subgraph_back_reference_preserves_js_identity() {
let shared = obj(vec![("k", SerializedValue::Number(9.0))]);
let shared_id = match &shared {
SerializedValue::Object { id, .. } => *id,
_ => unreachable!(),
};
let tree = SerializedValue::Array {
id: 100,
items: vec![shared, SerializedValue::Reference(shared_id)],
};
run(move |ctx| {
let js = serialized_value_to_quickjs(ctx, &tree).expect("rehydrate");
ctx.globals().set("__v", js).expect("set global");
let same: bool = ctx
.eval("Array.isArray(__v) && __v.length === 2 && __v[0] === __v[1] && __v[0].k === 9".as_bytes())
.expect("identity probe");
assert!(same, "back-reference must resolve to the same JS object");
})
.await;
}
#[tokio::test]
async fn rich_types_rehydrate_to_native_prototypes() {
let probes: Vec<(SerializedValue, &str, &str)> = vec![
(
SerializedValue::Date("2020-01-02T03:04:05.000Z".to_string()),
"__v instanceof Date && __v.toISOString()",
"2020-01-02T03:04:05.000Z",
),
(
SerializedValue::RegExp(RegExpValue {
p: "ab+c".to_string(),
f: "i".to_string(),
}),
"(__v instanceof RegExp) + '|' + __v.source + '|' + __v.flags",
"true|ab+c|i",
),
(
SerializedValue::BigInt("123456789012345".to_string()),
"typeof __v === 'bigint' && (__v === 123456789012345n)",
"true",
),
(
SerializedValue::Url("https://example.com/p?q=1".to_string()),
"__v instanceof URL && __v.href",
"https://example.com/p?q=1",
),
(
SerializedValue::TypedArray(TypedArrayValue {
k: TypedArrayKind::U8,
b: vec![1, 2, 3],
}),
"(__v instanceof Uint8Array) + '|' + __v.length + '|' + __v[2]",
"true|3|3",
),
];
run(move |ctx| {
install_url(ctx);
for (value, expr, expected) in &probes {
let js = serialized_value_to_quickjs(ctx, value).expect("rehydrate");
ctx.globals().set("__v", js).expect("set global");
let got: String = ctx.eval(format!("String({expr})").as_bytes()).expect("eval probe");
assert_eq!(&got, expected, "probe `{expr}` for {value:?}");
}
})
.await;
}
#[tokio::test]
async fn special_numbers_rehydrate_correctly() {
let probes: Vec<(SerializedValue, &str)> = vec![
(SerializedValue::Special(SpecialValue::NaN), "Number.isNaN(__v)"),
(SerializedValue::Special(SpecialValue::Infinity), "__v === Infinity"),
(SerializedValue::Special(SpecialValue::NegInfinity), "__v === -Infinity"),
(SerializedValue::Special(SpecialValue::NegZero), "__v === 0"),
(SerializedValue::Special(SpecialValue::Undefined), "__v === undefined"),
];
run(move |ctx| {
for (value, expr) in &probes {
let js = serialized_value_to_quickjs(ctx, value).expect("rehydrate");
ctx.globals().set("__v", js).expect("set global");
let ok: bool = ctx.eval(expr.as_bytes()).expect("eval probe");
assert!(ok, "probe `{expr}` for {value:?}");
}
})
.await;
}