use avril_core::{Extension, Runtime, RuntimeOptions, v8}; fn collect_garbage( scope: &mut v8::PinScope, _args: v8::FunctionCallbackArguments, _result: v8::ReturnValue, ) { scope.low_memory_notification(); } fn runtime() -> Runtime { Runtime::new(RuntimeOptions { extensions: vec![Extension { name: "ext:test", dependencies: &[], ops: vec![], modules: &[( "ext:test/main.js", r#" import {setEventHandlers} from 'ext:core/event_hooks'; import {enqueueMicrotask} from 'ext:core/microtasks'; globalThis.testQueueMicrotask = enqueueMicrotask; globalThis.reported = []; globalThis.cancelErrors = true; globalThis.captureRejections = true; globalThis.cancelRejections = true; globalThis.catchDuringUnhandled = false; setEventHandlers( (error, filename, line, column) => { reported.push(['error', error, filename, line, column]); return cancelErrors; }, (promise, reason) => { if (captureRejections) reported.push(['unhandled', promise, reason]); if (catchDuringUnhandled) { catchDuringUnhandled = false; promise.catch(() => {}); } return cancelRejections; }, (promise, reason) => reported.push(['handled', promise, reason]), ); "#, )], entry_point: Some("ext:test/main.js"), }], ..Default::default() }) .unwrap() } #[test] fn native_event_hooks_report_exceptions_and_late_rejections() { let mut runtime = runtime(); assert!( runtime .execute_script::<()>("direct.js", "throw 'direct exception';") .is_err() ); assert!(runtime .execute_script::( "direct-reported.js", "reported.length === 1 && reported[0][0] === 'error' && reported[0][1] === 'direct exception'", ) .unwrap()); runtime .execute_script::<()>( "same-turn.js", r#" const handledBeforeReport = Promise.reject('same turn'); Promise.resolve().then(() => handledBeforeReport.catch(() => {})); testQueueMicrotask(() => {throw 'reported exception';}); testQueueMicrotask(() => {throw 'second reported exception';}); "#, ) .unwrap(); assert!( runtime .execute_script::( "first.js", "reported.length === 3 && reported[1][0] === 'error' && reported[1][1] === 'reported exception' && reported[2][1] === 'second reported exception'", ) .unwrap() ); let error = runtime .execute_script::<()>( "fatal-errors.js", "cancelErrors = false; testQueueMicrotask(() => {throw 'fatal first';}); testQueueMicrotask(() => {throw 'fatal second';});", ) .unwrap_err(); assert!(error.to_string().contains("fatal first"), "{error}"); assert!(runtime .execute_script::( "fatal-errors-reported.js", "reported.length === 5 && reported[3][1] === 'fatal first' && reported[4][1] === 'fatal second'", ) .unwrap()); runtime .execute_script::<()>("enable-cancellation.js", "cancelErrors = true;") .unwrap(); runtime .execute_script::<()>( "caught-in-unhandled-listener.js", "catchDuringUnhandled = true; Promise.reject('caught in listener');", ) .unwrap(); assert!(runtime .execute_script::( "caught-in-unhandled-listener-reported.js", "reported.length === 6 && reported[5][0] === 'unhandled' && reported.filter(event => event[0] === 'handled').length === 0", ) .unwrap()); runtime .execute_script::<()>( "late.js", "globalThis.reason = {}; globalThis.promise = Promise.reject(reason);", ) .unwrap(); assert!( runtime .execute_script::( "unhandled.js", "reported.length === 7 && reported[6][0] === 'unhandled' && reported[6][1] === promise && reported[6][2] === reason", ) .unwrap() ); runtime .execute_script::<()>("catch.js", "promise.catch(() => {});") .unwrap(); assert!( runtime .execute_script::( "handled.js", "reported.length === 8 && reported[7][0] === 'handled' && reported[7][1] === promise && reported[7][2] === reason", ) .unwrap() ); let error = runtime .execute_script::<()>( "fatal-rejection.js", "cancelRejections = false; Promise.reject('fatal rejection');", ) .unwrap_err(); assert!(error.to_string().contains("fatal rejection"), "{error}"); assert!(runtime .execute_script::( "fatal-rejection-reported.js", "reported.length === 9 && reported[8][0] === 'unhandled' && reported[8][2] === 'fatal rejection'", ) .unwrap()); runtime .execute_script::<()>( "enable-rejection-cancellation.js", "cancelRejections = true;", ) .unwrap(); runtime .register_module("test:gc", |scope| { let global = scope.get_current_context().global(scope); let collect = v8::Function::builder(collect_garbage).build(scope)?; let key = v8::String::new(scope, "collectGarbage")?; global.set(scope, key.into(), collect.into()); Some(v8::Object::new(scope)) }) .unwrap(); runtime .execute_script::<()>( "unretained-rejections.js", "captureRejections = false; globalThis.promiseRefs = []; globalThis.reasonRefs = []; for (let i = 0; i < 256; i++) { const reason = {}; const promise = Promise.reject(reason); promiseRefs.push(new WeakRef(promise)); reasonRefs.push(new WeakRef(reason)); }", ) .unwrap(); runtime .execute_script::<()>("collect.js", "collectGarbage();") .unwrap(); let collected = runtime .execute_script::( "collected.js", "promiseRefs.every(reference => reference.deref() === undefined) && reasonRefs.every(reference => reference.deref() === undefined)", ) .unwrap(); assert!(collected); }