1#![doc = include_str!("../ABOUT.md")]
94#![doc(
95 html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg",
96 html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg"
97)]
98#![cfg_attr(test, allow(clippy::needless_raw_string_hashes))] #![cfg_attr(not(test), forbid(clippy::unwrap_used))]
100#![allow(unused_crate_dependencies)]
102#![allow(
103 clippy::module_name_repetitions,
104 clippy::redundant_pub_crate,
105 clippy::let_unit_value
106)]
107
108pub mod base64;
109pub mod console;
110
111#[doc(inline)]
112pub use console::{Console, ConsoleState, DefaultLogger, Logger, NullLogger};
113
114#[cfg(feature = "fetch")]
115pub mod abort;
116pub mod clone;
117pub mod extensions;
118#[cfg(feature = "fetch")]
119pub mod fetch;
120pub mod interval;
121pub mod message;
122pub mod microtask;
123#[cfg(feature = "process")]
124pub mod process;
125pub mod store;
126#[cfg(feature = "test262")]
128pub mod test262;
129pub mod text;
130#[cfg(feature = "url")]
131pub mod url;
132
133#[cfg(feature = "process")]
134use crate::extensions::ProcessExtension;
135use crate::extensions::{
136 Base64Extension, EncodingExtension, MicrotaskExtension, StructuredCloneExtension,
137 TimeoutExtension,
138};
139pub use extensions::RuntimeExtension;
140
141pub fn register(
147 extensions: impl RuntimeExtension,
148 realm: Option<boa_engine::realm::Realm>,
149 ctx: &mut boa_engine::Context,
150) -> boa_engine::JsResult<()> {
151 (
152 Base64Extension,
153 TimeoutExtension,
154 EncodingExtension,
155 MicrotaskExtension,
156 StructuredCloneExtension,
157 #[cfg(feature = "url")]
158 extensions::UrlExtension,
159 #[cfg(feature = "process")]
160 ProcessExtension,
161 #[cfg(feature = "fetch")]
162 extensions::AbortControllerExtension,
163 extensions,
164 )
165 .register(realm, ctx)?;
166
167 Ok(())
168}
169
170pub fn register_extensions(
176 extensions: impl RuntimeExtension,
177 realm: Option<boa_engine::realm::Realm>,
178 ctx: &mut boa_engine::Context,
179) -> boa_engine::JsResult<()> {
180 extensions.register(realm, ctx)?;
181
182 Ok(())
183}
184
185#[cfg(test)]
186pub(crate) mod test {
187 use crate::extensions::ConsoleExtension;
188 use crate::register;
189 use boa_engine::{Context, JsError, JsResult, JsValue, Source, builtins};
190 use std::borrow::Cow;
191 use std::path::{Path, PathBuf};
192 use std::pin::Pin;
193
194 #[allow(missing_debug_implementations)]
196 pub(crate) struct TestAction(Inner);
197
198 #[allow(dead_code)]
199 #[allow(clippy::type_complexity)]
200 enum Inner {
201 RunHarness,
202 Run {
203 source: Cow<'static, str>,
204 },
205 RunFile {
206 path: PathBuf,
207 },
208 RunJobs,
209 InspectContext {
210 op: Box<dyn FnOnce(&mut Context)>,
211 },
212 InspectContextAsync {
213 op: Box<dyn for<'a> FnOnce(&'a mut Context) -> Pin<Box<dyn Future<Output = ()> + 'a>>>,
214 },
215 Assert {
216 source: Cow<'static, str>,
217 },
218 AssertEq {
219 source: Cow<'static, str>,
220 expected: JsValue,
221 },
222 AssertWithOp {
223 source: Cow<'static, str>,
224 op: fn(JsValue, &mut Context) -> bool,
225 },
226 AssertOpaqueError {
227 source: Cow<'static, str>,
228 expected: JsValue,
229 },
230 AssertNativeError {
231 source: Cow<'static, str>,
232 kind: builtins::error::ErrorKind,
233 message: &'static str,
234 },
235 AssertContext {
236 op: fn(&mut Context) -> bool,
237 },
238 }
239
240 impl TestAction {
241 #[allow(unused)]
242 pub(crate) fn harness() -> Self {
243 Self(Inner::RunHarness)
244 }
245
246 pub(crate) fn run(source: impl Into<Cow<'static, str>>) -> Self {
248 Self(Inner::Run {
249 source: source.into(),
250 })
251 }
252
253 pub(crate) fn inspect_context(op: impl FnOnce(&mut Context) + 'static) -> Self {
257 Self(Inner::InspectContext { op: Box::new(op) })
258 }
259
260 pub(crate) fn inspect_context_async(op: impl AsyncFnOnce(&mut Context) + 'static) -> Self {
262 Self(Inner::InspectContextAsync {
263 op: Box::new(move |ctx| Box::pin(op(ctx))),
264 })
265 }
266 }
267
268 #[track_caller]
270 pub(crate) fn run_test_actions(actions: impl IntoIterator<Item = TestAction>) {
271 let context = &mut Context::default();
272 register(ConsoleExtension::default(), None, context)
273 .expect("failed to register WebAPI objects");
274 run_test_actions_with(actions, context);
275 }
276
277 #[track_caller]
279 #[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
280 pub(crate) fn run_test_actions_with(
281 actions: impl IntoIterator<Item = TestAction>,
282 context: &mut Context,
283 ) {
284 #[track_caller]
285 fn forward_val(context: &mut Context, source: &str) -> JsResult<JsValue> {
286 context.eval(Source::from_bytes(source))
287 }
288
289 #[track_caller]
290 fn forward_file(context: &mut Context, path: impl AsRef<Path>) -> JsResult<JsValue> {
291 let p = path.as_ref();
292 context.eval(Source::from_filepath(p).map_err(JsError::from_rust)?)
293 }
294
295 #[track_caller]
296 fn fmt_test(source: &str, test: usize) -> String {
297 format!(
298 "\n\nTest case {test}: \n```\n{}\n```",
299 textwrap::indent(source, " ")
300 )
301 }
302
303 let mut i = 1;
306 for action in actions.into_iter().map(|a| a.0) {
307 match action {
308 Inner::RunHarness => {
309 if let Err(e) = forward_file(context, "./assets/harness.js") {
310 panic!("Uncaught {e} in the test harness");
311 }
312 }
313 Inner::Run { source } => {
314 if let Err(e) = forward_val(context, &source) {
315 panic!("{}\nUncaught {e}", fmt_test(&source, i));
316 }
317 }
318 Inner::RunFile { path } => {
319 if let Err(e) = forward_file(context, &path) {
320 panic!("Uncaught {e} in file {path:?}");
321 }
322 }
323 Inner::RunJobs => {
324 if let Err(e) = context.run_jobs() {
325 panic!("Uncaught {e} in a job");
326 }
327 }
328 Inner::InspectContext { op } => {
329 op(context);
330 }
331 Inner::InspectContextAsync { op } => futures_lite::future::block_on(op(context)),
332 Inner::Assert { source } => {
333 let val = match forward_val(context, &source) {
334 Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
335 Ok(v) => v,
336 };
337 let Some(val) = val.as_boolean() else {
338 panic!(
339 "{}\nTried to assert with the non-boolean value `{}`",
340 fmt_test(&source, i),
341 val.display()
342 )
343 };
344 assert!(val, "{}", fmt_test(&source, i));
345 i += 1;
346 }
347 Inner::AssertEq { source, expected } => {
348 let val = match forward_val(context, &source) {
349 Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
350 Ok(v) => v,
351 };
352 assert_eq!(val, expected, "{}", fmt_test(&source, i));
353 i += 1;
354 }
355 Inner::AssertWithOp { source, op } => {
356 let val = match forward_val(context, &source) {
357 Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
358 Ok(v) => v,
359 };
360 assert!(op(val, context), "{}", fmt_test(&source, i));
361 i += 1;
362 }
363 Inner::AssertOpaqueError { source, expected } => {
364 let err = match forward_val(context, &source) {
365 Ok(v) => panic!(
366 "{}\nExpected error, got value `{}`",
367 fmt_test(&source, i),
368 v.display()
369 ),
370 Err(e) => e,
371 };
372 let Some(err) = err.as_opaque() else {
373 panic!(
374 "{}\nExpected opaque error, got native error `{}`",
375 fmt_test(&source, i),
376 err
377 )
378 };
379
380 assert_eq!(err, &expected, "{}", fmt_test(&source, i));
381 i += 1;
382 }
383 Inner::AssertNativeError {
384 source,
385 kind,
386 message,
387 } => {
388 let err = match forward_val(context, &source) {
389 Ok(v) => panic!(
390 "{}\nExpected error, got value `{}`",
391 fmt_test(&source, i),
392 v.display()
393 ),
394 Err(e) => e,
395 };
396 let native = match err.try_native(context) {
397 Ok(err) => err,
398 Err(e) => panic!(
399 "{}\nCouldn't obtain a native error: {e}",
400 fmt_test(&source, i)
401 ),
402 };
403
404 assert_eq!(native.kind(), &kind, "{}", fmt_test(&source, i));
405 assert_eq!(native.message(), message, "{}", fmt_test(&source, i));
406 i += 1;
407 }
408 Inner::AssertContext { op } => {
409 assert!(op(context), "Test case {i}");
410 i += 1;
411 }
412 }
413 }
414 }
415}