mod common; use common::{run, spawn, wait}; #[test] fn listener_option_boxing_and_abort_arities_are_preserved() { let output = common::run( r#" const reads = []; for (const key of ['capture', 'once', 'passive']) { Object.defineProperty(Number.prototype, key, { configurable: true, get() {reads.push(key); return false;}, }); } Object.defineProperty(Number.prototype, 'signal', { configurable: true, get() {throw Error('Primitive signal read');}, }); const target = new EventTarget(); let calls = 0; const listener = () => calls++; target.addEventListener('ready', listener, 1); target.dispatchEvent(new Event('ready')); target.removeEventListener('ready', listener, 1); target.dispatchEvent(new Event('ready')); if (calls !== 1 || reads.join(',') !== 'capture,once,passive,capture') throw Error('Listener option conversion: ' + reads); if (AbortSignal.abort.length !== 1 || AbortController.prototype.abort.length !== 1) throw Error('Abort arity'); if (WorkerNavigator.length !== 0 || NavigatorUAData.length !== 0 || Performance.length !== 0) throw Error('Runtime constructor arity'); const reason = {}; const controller = new AbortController(); controller.abort(reason); if (controller.signal.reason !== reason || AbortSignal.abort(reason).reason !== reason) throw Error('Abort reason identity'); console.log('conversion and arities'); "#, &[], ); assert_eq!(output.stdout, b"conversion and arities\n"); } #[test] fn event_interfaces_global_target_and_error_reporting() { let output = run( r#" const detail = {value: 1}; const custom = new CustomEvent('custom', {detail}); if (!(custom instanceof Event) || custom.detail !== detail || custom.type !== 'custom') throw Error('CustomEvent state'); if (new CustomEvent('empty').detail !== null) throw Error('CustomEvent default'); for (const EventType of [CustomEvent, ErrorEvent, PromiseRejectionEvent]) { try { new EventType(); throw Error('Missing event type accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; } } const errorEvent = new ErrorEvent('error', { message: 42, filename: 'source.js', lineno: -1, colno: 1.5, error: undefined, }); if (!(errorEvent instanceof Event) || errorEvent.message !== '42' || errorEvent.filename !== 'source.js' || errorEvent.lineno !== 4294967295 || errorEvent.colno !== 1 || errorEvent.error !== null) { throw Error('ErrorEvent state'); } const conversion = []; new Event({[Symbol.toPrimitive]() { conversion.push('type'); return 'conversion'; }}, { get bubbles() { conversion.push('bubbles'); return true; }, }); if (conversion.join() !== 'type,bubbles') throw Error('Event WebIDL conversion order'); const promiseEvent = new PromiseRejectionEvent('unhandledrejection', { promise: Promise.resolve(), reason: undefined, }); if (!(promiseEvent.promise instanceof Promise) || promiseEvent.reason !== undefined) { throw Error('PromiseRejectionEvent state'); } const fakePromise = Object.create(Promise.prototype); try { new PromiseRejectionEvent('unhandledrejection', {promise: fakePromise}); throw Error('Accepted non-Promise'); } catch (error) { if (!(error instanceof TypeError)) throw error; } const stamp = new Event('stamp').timeStamp; if (stamp < 0 || stamp > performance.now() + 1) throw Error('Event timestamp clock'); if (addEventListener.length !== 2 || removeEventListener.length !== 2 || dispatchEvent.length !== 1) { throw Error('Global EventTarget method signatures'); } try { EventTarget.prototype.addEventListener.call({}, 'bad', () => {}); throw Error('Invalid EventTarget receiver'); } catch (error) { if (!(error instanceof TypeError)) throw error; } let target; globalThis.addEventListener('global', function(event) { target = this; if (event.target !== globalThis || event.currentTarget !== globalThis) throw Error('Global event target'); }); const globalEvent = new Event('global'); if (!globalThis.dispatchEvent(globalEvent) || globalEvent.target !== globalThis || target !== globalThis) { throw Error('Global dispatch target'); } const initialized = new Event('old', {cancelable: true}); globalThis.addEventListener('old', event => event.initEvent('changed', true, true)); globalThis.dispatchEvent(initialized); if (initialized.type !== 'old') throw Error('initEvent changed a dispatching event'); initialized.initEvent('new', true, false); if (initialized.type !== 'new' || !initialized.bubbles || initialized.cancelable) throw Error('initEvent'); const reported = new Error('reported'); let expectedError = reported, errorCalls = 0, legacyArgs; globalThis.addEventListener('error', event => { errorCalls++; if (!(event instanceof ErrorEvent) || event.error !== expectedError || event.message !== (expectedError === undefined ? 'undefined' : expectedError.message) || event.target !== globalThis || !event.cancelable) throw Error('Reported ErrorEvent'); event.preventDefault(); }); globalThis.onerror = function(message, filename, line, column, error) { legacyArgs = [this === globalThis, message, filename, line, column, error]; return true; }; reportError(reported); if (errorCalls !== 1 || legacyArgs[0] !== true || legacyArgs[1] !== 'reported' || legacyArgs[2] !== '' || legacyArgs[3] !== 0 || legacyArgs[4] !== 0 || legacyArgs[5] !== reported) { throw Error('Global error handler'); } expectedError = undefined; reportError(undefined); if (errorCalls !== 2 || legacyArgs[5] !== undefined || reportError.length !== 1) { throw Error('reportError undefined value or function length'); } const handlerError = new Error('handler'); expectedError = handlerError; const handlerOrder = []; globalThis.onerror = () => handlerOrder.push('old'); const orderListener = () => handlerOrder.push('listener'); globalThis.addEventListener('error', orderListener); globalThis.onerror = () => { handlerOrder.push('new'); return true; }; reportError(handlerError); if (handlerOrder.join() !== 'new,listener') throw Error('Global handler listener order'); globalThis.removeEventListener('error', orderListener); globalThis.onerror = null; const listenerError = new Error('listener'); expectedError = listenerError; globalThis.addEventListener('trigger', () => { throw listenerError; }); globalThis.dispatchEvent(new Event('trigger')); if (errorCalls !== 4) throw Error('Listener exception was not reported'); try { reportError(); throw Error('Missing reportError argument accepted'); } catch (error) { if (!(error instanceof TypeError)) throw error; } try { structuredClone(custom); throw Error('CustomEvent was cloneable'); } catch (error) { if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error; } console.log('events'); "#, &[], ); assert_eq!(output.stdout, b"events\n"); } #[test] fn unhandled_rejection_events_preserve_identity_and_report_late_handlers() { let output = run( r#" const reason = {failure: true}; const promise = Promise.reject(reason); const sameTurn = Promise.reject('same turn'); Promise.resolve().then(() => sameTurn.catch(() => {})); let unhandled = 0, handled = 0; globalThis.addEventListener('unhandledrejection', event => { unhandled++; if (!(event instanceof PromiseRejectionEvent) || event.promise !== promise || event.reason !== reason || !event.cancelable || event.target !== globalThis) { throw Error('Unhandled rejection event'); } event.preventDefault(); }); globalThis.addEventListener('rejectionhandled', event => { handled++; if (!(event instanceof PromiseRejectionEvent) || event.promise !== promise || event.reason !== reason || event.cancelable || event.target !== globalThis) { throw Error('Handled rejection event'); } }); setTimeout(() => promise.catch(() => {}), 0); await new Promise(resolve => setTimeout(resolve, 20)); if (unhandled !== 1 || handled !== 1) throw Error(`Rejection events: ${unhandled}/${handled}`); console.log('rejections'); "#, &[], ); assert_eq!(output.stdout, b"rejections\n"); } #[test] fn canceled_callback_errors_do_not_stop_queued_work() { let output = run( r#" let calls = 0; globalThis.addEventListener('error', event => { if (event.message !== 'canceled') throw Error('Unexpected callback error'); event.preventDefault(); }); setTimeout(() => { throw new Error('canceled'); }, 0); setTimeout(() => calls++, 0); await new Promise(resolve => setTimeout(resolve, 10)); if (calls !== 1) throw Error('Canceled callback stopped later tasks'); console.log('continued'); "#, &[], ); assert_eq!(output.stdout, b"continued\n"); } #[test] fn microtask_error_event_uses_typescript_source_locations() { let dir = tempfile::tempdir().unwrap(); std::fs::write( dir.path().join("main.ts"), r#" globalThis.onerror = (message, filename, line, column, error) => { if (message !== 'mapped' || !filename.endsWith('/main.ts') || line < 2 || column < 1 || error.message !== 'mapped') throw Error('Incorrect mapped ErrorEvent metadata'); console.log(`${filename}:${line}:${column}`); return true; }; queueMicrotask(() => { throw new Error('mapped'); }); await new Promise(resolve => setTimeout(resolve, 10)); "#, ) .unwrap(); let output = std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .arg("main.ts") .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert!(String::from_utf8_lossy(&output.stdout).contains("/main.ts:")); } #[test] fn top_level_await_rejection_reports_error_and_remains_fatal() { let (_dir, child) = spawn( r#" globalThis.onerror = (message) => { console.log(`error:${message}`); return true; }; globalThis.addEventListener('unhandledrejection', () => console.log('unhandled')); await Promise.reject(new Error('module failure')); "#, ); let output = wait(child); assert!(!output.status.success()); assert_eq!(output.stdout, b"error:module failure\n"); assert!(String::from_utf8_lossy(&output.stderr).contains("module failure")); } #[test] fn uncanceled_unhandled_rejection_remains_fatal() { let (_dir, child) = spawn( r#" globalThis.addEventListener('unhandledrejection', event => { console.log(`unhandled:${event.reason}`); }); Promise.reject('fatal rejection'); await new Promise(resolve => setTimeout(resolve, 10)); "#, ); let output = wait(child); assert!(!output.status.success()); assert_eq!(output.stdout, b"unhandled:fatal rejection\n"); assert!(String::from_utf8_lossy(&output.stderr).contains("fatal rejection")); } #[test] fn uncanceled_report_error_remains_fatal() { let (_dir, child) = spawn("reportError(32);"); let output = wait(child); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("32")); } #[test] fn global_event_operations_are_unbound_and_microtask_errors_are_not_dropped() { let out = run( r#" const seen=[]; onerror=(message)=>{seen.push(message);return true;}; const listener=event=>seen.push(event.type); addEventListener('unbound',listener); if(!dispatchEvent(new Event('unbound')))throw Error('Dispatch'); removeEventListener('unbound',listener); dispatchEvent(new Event('unbound')); for(const call of [()=>addEventListener('x'),()=>removeEventListener('x')]){ try{call();throw Error('Missing listener accepted');}catch(e){if(!(e instanceof TypeError))throw e;} } queueMicrotask(()=>{throw 'first';}); queueMicrotask(()=>{throw 'second';}); await new Promise(r=>setTimeout(r,5)); if(seen.join()!=='unbound,first,second')throw Error(seen.join()); const custom=new CustomEvent('x',{detail:undefined}); const error=new ErrorEvent('x',{message:undefined,filename:undefined,error:undefined}); if(custom.detail!==null||error.message!==''||error.filename!==''||error.error!==null)throw Error('Dictionary defaults'); const lone='\ud800'; if(new Event(lone).type!==lone||new ErrorEvent('x',{message:lone}).message!==lone)throw Error('DOMString replaced'); console.log('global events'); "#, &[], ); assert_eq!(out.stdout, b"global events\n"); } #[test] fn host_events_are_trusted_but_redispatch_is_untrusted() { let out = run( r#" let original; onerror=()=>true; addEventListener('error',event=>{original=event;event.preventDefault();},{once:true}); reportError('host'); if(!original.isTrusted||new Event('synthetic').isTrusted)throw Error('Trusted flag'); const getter=Object.getOwnPropertyDescriptor(original,'isTrusted').get; if(getter.call(new Event('synthetic')))throw Error('Unforgeable getter receiver'); dispatchEvent(original); if(original.isTrusted)throw Error('Redispatch retained trusted flag'); const signal=AbortSignal.timeout(0); signal.addEventListener('abort',event=>{if(!event.isTrusted)throw Error('Untrusted abort');}); await new Promise(r=>setTimeout(r,5)); console.log('trusted events'); "#, &[], ); assert_eq!(out.stdout, b"trusted events\n"); } #[test] fn exceptions_in_error_handlers_are_fatal_without_recursive_error_events() { let (_dir, child) = spawn( r#" let count=0; addEventListener('error',event=>{count++;event.preventDefault();throw new Error('handler failed');}); reportError('original'); console.log(count); "#, ); let output = wait(child); assert!(!output.status.success()); assert!(String::from_utf8_lossy(&output.stderr).contains("handler failed")); assert_eq!(output.stdout, b"1\n"); } #[test] fn global_handler_return_values_and_rejection_lifecycle_follow_html() { let out = run( r#" if(!(globalThis instanceof EventTarget))throw Error('Global EventTarget'); let handled=0; onrejectionhandled=()=>handled++; onunhandledrejection=event=>{event.promise.catch(()=>{});return false;}; Promise.reject('handled while reporting'); await new Promise(r=>setTimeout(r,5)); if(handled!==0)throw Error('Reported handler before outstanding set'); onunhandledrejection=()=>false; const late=Promise.reject('later handled'); await new Promise(r=>setTimeout(r,5)); late.catch(()=>{}); await new Promise(r=>setTimeout(r,5)); if(handled!==1)throw Error('Genuinely late handler not reported'); const one=new Event('one'),two=new Event('two'); if(Object.getOwnPropertyDescriptor(one,'isTrusted').get!==Object.getOwnPropertyDescriptor(two,'isTrusted').get)throw Error('Trust accessor identity'); const stamp=one.timeStamp; if(Math.abs(stamp*10-Math.round(stamp*10))>1e-7)throw Error('Timestamp not coarsened'); console.log('html handlers'); "#, &[], ); assert_eq!(out.stdout, b"html handlers\n"); } #[test] fn event_aliases_and_dictionary_members_use_native_state_and_single_reads() { let out = run( r#" const event=new Event('alias'), target=new EventTarget(); if(event.srcElement!==null)throw Error('Initial srcElement'); for(const [name,value] of [['NONE',0],['CAPTURING_PHASE',1],['AT_TARGET',2],['BUBBLING_PHASE',3]]){ if(Event[name]!==value||event[name]!==value)throw Error('Event phase constants'); } target.dispatchEvent(event); if(event.srcElement!==target||event.srcElement!==event.target)throw Error('srcElement target alias'); let calls=0; const custom=new CustomEvent('custom',{get detail(){calls++;return calls;}}); if(calls!==1||custom.detail!==1)throw Error('detail read twice'); const error=new ErrorEvent('error',{get message(){calls++;return 'message';},get filename(){calls++;return 'file';},get error(){calls++;return 'reason';}}); if(calls!==4||error.error!=='reason')throw Error('ErrorEvent dictionary reread'); const lone='\ud800',exception=new DOMException(lone,lone); if(exception.name!==lone||exception.message!==lone)throw Error('DOMException DOMString'); for(const key of ['name','message']){ if(Object.hasOwn(exception,key))throw Error('DOMException own field'); const descriptor=Object.getOwnPropertyDescriptor(DOMException.prototype,key); if(!descriptor.get||descriptor.set||!descriptor.enumerable||!descriptor.configurable)throw Error('DOMException attribute shape'); try{descriptor.get.call({});throw Error('DOMException unbranded getter');}catch(e){if(!(e instanceof TypeError))throw e;} } let host; onerror=()=>true; addEventListener('error',event=>{host=event;},{once:true}); reportError('host'); if(!host.isTrusted)throw Error('Initial host flag'); host.initEvent('reinitialized'); if(host.isTrusted)throw Error('initEvent retained trusted flag'); console.log('event conversions'); "#, &[], ); assert_eq!(out.stdout, b"event conversions\n"); }