use std::cell::Cell; use std::io; use std::rc::Rc; use avril_core::{Error, Extension, Op, Runtime, RuntimeOptions}; #[test] fn live_initializers_run_after_all_static_entries_and_keep_export_identity() { let native = Rc::new(Cell::new(10)); let read = native.clone(); let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![ Extension { name: "ext:consumer", dependencies: &["ext:producer"], ops: vec![Op::new("setNative", move |value: u32| { native.set(value); Ok::<_, io::Error>(()) })], modules: &[( "ext:consumer/main.js", r#" import {onRuntimeStart, startRuntime} from 'ext:core/bootstrap'; import {setNative} from 'ext:core/ops'; import {value} from 'ext:producer/main.js'; if (value.native !== undefined) throw Error('Early initialization'); await Promise.resolve(); events.push('static consumer'); setNative(42); globalThis.captured = value; globalThis.startAgain = startRuntime; globalThis.registerLate = () => onRuntimeStart(() => {}); onRuntimeStart(() => { if (value.native !== 42) throw Error('Stale native state'); events.push('live consumer'); }); "#, )], entry_point: Some("ext:consumer/main.js"), }, Extension { name: "ext:producer", dependencies: &[], ops: vec![Op::new("readNative", move |(): ()| { Ok::<_, io::Error>(read.get()) })], modules: &[( "ext:producer/main.js", r#" import {onRuntimeStart} from 'ext:core/bootstrap'; import {readNative} from 'ext:core/ops'; globalThis.events = ['static producer']; export const value = {native: undefined}; onRuntimeStart(() => { value.native = readNative(); Object.freeze(value); events.push('live producer'); }); "#, )], entry_point: Some("ext:producer/main.js"), }, ], ..Default::default() }) .unwrap(); assert_eq!( runtime .execute_script::>("events.js", "events") .unwrap(), [ "static producer", "static consumer", "live producer", "live consumer" ], ); runtime .register_extension_module("test:producer", "ext:producer/main.js") .unwrap(); runtime .execute_script::<()>( "identity.js", r#" import('test:producer').then(({value}) => { if (value !== captured || value.native !== 42 || !Object.isFrozen(value)) { throw Error('Export identity or descriptors changed'); } }); for (const action of [startAgain, registerLate]) { try { action(); throw Error('Expected rejection'); } catch (error) { if (error.message !== 'Runtime already started') throw error; } } "#, ) .unwrap(); runtime.run_event_loop().unwrap(); } #[test] fn failing_live_initializers_abort_construction_without_running_later_initializers() { let called = Rc::new(Cell::new(false)); let observed = called.clone(); let result = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![Op::new("later", move |(): ()| { called.set(true); Ok::<_, io::Error>(()) })], modules: &[( "ext:test/main.js", r#" import {onRuntimeStart} from 'ext:core/bootstrap'; import {later} from 'ext:core/ops'; onRuntimeStart(() => { throw Error('failed live initialization'); }); onRuntimeStart(() => { later(); }); "#, )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }); assert!( matches!(result, Err(Error::JavaScript(message)) if message.contains("failed live initialization")) ); assert!(!observed.get()); } #[test] fn live_initializers_cannot_escape_the_startup_boundary_with_promises() { let result = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![], modules: &[( "ext:test/main.js", r#" import {onRuntimeStart} from 'ext:core/bootstrap'; onRuntimeStart(async () => {}); "#, )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }); assert!( matches!(result, Err(Error::JavaScript(message)) if message.contains("Runtime initializers must return undefined")) ); } #[test] fn live_initializers_ignore_poisoned_array_protocols() { let mut runtime = Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![], modules: &[( "ext:test/main.js", r#" import {onRuntimeStart} from 'ext:core/bootstrap'; const push = Array.prototype.push; const iterator = Array.prototype[Symbol.iterator]; let initialized = 0; onRuntimeStart(function () { if (this !== undefined) throw Error('Leaked initializer receiver'); initialized++; }); const poison = () => { throw Error('Mutable array protocol'); }; Array.prototype.push = poison; Array.prototype[Symbol.iterator] = poison; onRuntimeStart(() => { Array.prototype.push = push; Array.prototype[Symbol.iterator] = iterator; globalThis.initialized = initialized; }); "#, )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }) .unwrap(); assert_eq!( runtime .execute_script::("initialized.js", "initialized") .unwrap(), 1 ); }