Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/runtime/src/clone/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use boa_engine::realm::Realm;
use boa_engine::value::TryFromJs;
use boa_engine::{Context, JsResult, JsValue, boa_module};

#[cfg(test)]
mod tests;

/// Options used by `structuredClone`. This is currently unused.
#[derive(Debug, Clone, TryFromJs)]
pub struct StructuredCloneOptions {
Expand Down
74 changes: 74 additions & 0 deletions core/runtime/src/clone/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! Tests for the `structuredClone` extension.

use crate::test::{TestAction, run_test_actions};

#[test]
fn clones_error_objects() {
run_test_actions([
TestAction::harness(),
TestAction::run(
r#"
const original = new Error("boom");
const cloned = structuredClone(original);

assert(cloned instanceof Error);
assert(cloned !== original);
assertEq(cloned.name, "Error");
assertEq(cloned.message, "boom");
"#,
),
]);
}

#[test]
fn clones_error_object_cause() {
run_test_actions([
TestAction::harness(),
TestAction::run(
r#"
const original = new Error("boom", { cause: { code: 7 } });
const cloned = structuredClone(original);

assert(cloned instanceof Error);
assert(cloned.cause !== original.cause);
assertEq(cloned.cause.code, 7);
"#,
),
]);
}

#[test]
fn clones_aggregate_error_entries() {
run_test_actions([
TestAction::harness(),
TestAction::run(
r#"
const original = new AggregateError([new Error("inner")], "agg");
const cloned = structuredClone(original);

assert(cloned instanceof AggregateError);
assertEq(cloned.message, "agg");
assertEq(cloned.errors.length, 1);
assert(cloned.errors[0] instanceof Error);
assertEq(cloned.errors[0].message, "inner");
"#,
),
]);
}

#[test]
fn clones_error_with_undefined_cause_property() {
run_test_actions([
TestAction::harness(),
TestAction::run(
r#"
const original = new Error("boom", { cause: undefined });
const cloned = structuredClone(original);

assert(Object.hasOwn(original, "cause"));
assert(Object.hasOwn(cloned, "cause"));
assertEq(cloned.cause, undefined);
"#,
),
]);
}
129 changes: 129 additions & 0 deletions core/runtime/src/message/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,135 @@ fn basic() {
);
}

#[test]
fn basic_error_message() {
let context = &mut Context::default();

let sender = OnMessageQueueSender::create(context);
message::register(sender, None, context).unwrap();

run_test_actions_with(
[
TestAction::harness(),
TestAction::run(
r#"
let latestMessage = null;
function onMessageQueue(message) {
latestMessage = message;
}

const message = new Error("boom");
postMessage(message);
assert(latestMessage === null);
"#,
),
TestAction::inspect_context(move |context| {
drop(future::block_on(future::poll_once(
context
.downcast_job_executor::<SimpleJobExecutor>()
.expect("")
.run_jobs_async(&RefCell::new(context)),
)));
}),
TestAction::run(
r#"
assert(latestMessage instanceof Error);
assert(latestMessage !== message);
assertEq(latestMessage.name, "Error");
assertEq(latestMessage.message, "boom");
"#,
),
],
context,
);
}

#[test]
fn basic_error_with_object_cause() {
let context = &mut Context::default();

let sender = OnMessageQueueSender::create(context);
message::register(sender, None, context).unwrap();

run_test_actions_with(
[
TestAction::harness(),
TestAction::run(
r#"
let latestMessage = null;
function onMessageQueue(message) {
latestMessage = message;
}

const message = new Error("boom", { cause: { code: 7 } });
postMessage(message);
assert(latestMessage === null);
"#,
),
TestAction::inspect_context(move |context| {
drop(future::block_on(future::poll_once(
context
.downcast_job_executor::<SimpleJobExecutor>()
.expect("")
.run_jobs_async(&RefCell::new(context)),
)));
}),
TestAction::run(
r#"
assert(latestMessage instanceof Error);
assert(latestMessage !== message);
assertEq(latestMessage.message, "boom");
assertEq(latestMessage.cause.code, 7);
"#,
),
],
context,
);
}

#[test]
fn basic_error_with_undefined_cause_property() {
let context = &mut Context::default();

let sender = OnMessageQueueSender::create(context);
message::register(sender, None, context).unwrap();

run_test_actions_with(
[
TestAction::harness(),
TestAction::run(
r#"
let latestMessage = null;
function onMessageQueue(message) {
latestMessage = message;
}

const message = new Error("boom", { cause: undefined });
postMessage(message);
assert(latestMessage === null);
"#,
),
TestAction::inspect_context(move |context| {
drop(future::block_on(future::poll_once(
context
.downcast_job_executor::<SimpleJobExecutor>()
.expect("")
.run_jobs_async(&RefCell::new(context)),
)));
}),
TestAction::run(
r#"
assert(latestMessage instanceof Error);
assert(latestMessage !== message);
assert(Object.hasOwn(latestMessage, "cause"));
assertEq(latestMessage.cause, undefined);
"#,
),
],
context,
);
}

#[test]
fn shared_multi_thread() {
let (sender, receiver) = std::sync::mpsc::channel::<OnMessageQueueSender>();
Expand Down
93 changes: 89 additions & 4 deletions core/runtime/src/store/from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

use crate::store::{JsValueStore, StringStore, ValueStoreInner, unsupported_type};
use boa_engine::builtins::array_buffer::{AlignedVec, ArrayBuffer};
use boa_engine::builtins::error::Error;
use boa_engine::builtins::error::{Error, ErrorKind};
use boa_engine::object::builtins::{
JsArray, JsArrayBuffer, JsDataView, JsDate, JsMap, JsRegExp, JsSet, JsSharedArrayBuffer,
JsTypedArray,
};
use boa_engine::property::PropertyKey;
use boa_engine::{Context, JsError, JsObject, JsResult, JsString, JsValue, JsVariant, js_error};
use boa_engine::{
Context, JsError, JsNativeErrorKind, JsObject, JsResult, JsString, JsValue, JsVariant,
js_error, js_string,
};
use std::collections::{HashMap, HashSet};

/// A Map of seen objects when walking through the value. We use the address
Expand Down Expand Up @@ -181,6 +184,88 @@ fn clone_regexp(
Ok(stored)
}

fn clone_error(
original: &JsObject,
transfer: &HashSet<JsObject>,
seen: &mut SeenMap,
context: &mut Context,
) -> JsResult<JsValueStore> {
let mut store = JsValueStore::empty();
seen.insert(original, store.clone());

let native = JsError::from_opaque(JsValue::from(original.clone()))
.try_native(context)
.map_err(|_| unsupported_type())?;

let kind = match native.kind() {
JsNativeErrorKind::Aggregate(_) => ErrorKind::Aggregate,
JsNativeErrorKind::Eval => ErrorKind::Eval,
JsNativeErrorKind::Type => ErrorKind::Type,
JsNativeErrorKind::Range => ErrorKind::Range,
JsNativeErrorKind::Reference => ErrorKind::Reference,
JsNativeErrorKind::Syntax => ErrorKind::Syntax,
JsNativeErrorKind::Uri => ErrorKind::Uri,
_ => ErrorKind::Error,
};

let to_optional_string = |key: &str, context: &mut Context| -> JsResult<JsString> {
let value = original.get(js_string!(key), context)?;
if value.is_undefined() {
Ok(js_string!())
} else {
value.to_string(context)
}
};

let name = to_optional_string("name", context)?;
let stack = to_optional_string("stack", context)?;

let cause = if original.has_own_property(js_string!("cause"), context)? {
let cause = original.get(js_string!("cause"), context)?;
Some(try_from_js_value(&cause, transfer, seen, context)?)
} else {
None
};

let errors = if matches!(kind, ErrorKind::Aggregate) {
let errors = original.get(js_string!("errors"), context)?;
if errors.is_undefined() {
Vec::new()
} else {
let Some(errors) = errors.as_object() else {
return Err(unsupported_type());
};

let errors = JsArray::from_object(errors).map_err(|_| unsupported_type())?;

let length = errors.length(context)?;
let length = usize::try_from(length).map_err(JsError::from_rust)?;
let mut values = Vec::with_capacity(length);
for i in 0..length {
let value = errors.get(i, context)?;
values.push(try_from_js_value(&value, transfer, seen, context)?);
}
values
}
} else {
Vec::new()
};

// SAFETY: This is safe as this function is the sole owner of the store.
unsafe {
store.replace(ValueStoreInner::Error {
kind,
name: name.into(),
message: JsString::from(native.message()).into(),
stack: stack.into(),
cause,
errors,
});
}

Ok(store)
}

fn try_from_map(
original: &JsObject,
map: &JsMap,
Expand Down Expand Up @@ -258,8 +343,8 @@ fn try_from_js_object_clone(
return clone_typed_array(object, typed_array, transfer, seen, context);
} else if let Ok(ref date) = JsDate::from_object(object.clone()) {
return clone_date(object, date, seen, context);
} else if let Ok(_error) = object.clone().downcast::<Error>() {
return Err(js_error!(TypeError: "Errors are not supported yet."));
} else if object.downcast_ref::<Error>().is_some() {
return clone_error(object, transfer, seen, context);
} else if let Ok(ref regexp) = JsRegExp::from_object(object.clone()) {
return clone_regexp(object, regexp, seen, context);
} else if let Ok(_dataview) = JsDataView::from_object(object.clone()) {
Expand Down
4 changes: 2 additions & 2 deletions core/runtime/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ enum ValueStoreInner {
Date(f64),

/// Allowed error types (see the structured clone algorithm page).
#[expect(unused)]
Error {
kind: ErrorKind,
name: StringStore,
message: StringStore,
stack: StringStore,
cause: StringStore,
cause: Option<JsValueStore>,
errors: Vec<JsValueStore>,
},

/// Regular expression. We store the expression and its flags. Everything else
Expand Down
Loading