char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
44fe2c6
main
1mod common; 2 3use common::{ run, spawn, wait}; 4 5# [ test ] 6fn listener_option_boxing_and_abort_arities_are_preserved () { 7let output = common:: run ( 8r#" 9const reads = []; 10for (const key of ['capture', 'once', 'passive']) { 11Object.defineProperty(Number.prototype, key, { 12configurable: true, 13get() {reads.push(key); return false;}, 14}); 15} 16Object.defineProperty(Number.prototype, 'signal', { 17configurable: true, 18get() {throw Error('Primitive signal read');}, 19}); 20const target = new EventTarget(); 21let calls = 0; 22const listener = () => calls++; 23target.addEventListener('ready', listener, 1); 24target.dispatchEvent(new Event('ready')); 25target.removeEventListener('ready', listener, 1); 26target.dispatchEvent(new Event('ready')); 27if (calls !== 1 || reads.join(',') !== 'capture,once,passive,capture') 28throw Error('Listener option conversion: ' + reads); 29if (AbortSignal.abort.length !== 1 || AbortController.prototype.abort.length !== 1) 30throw Error('Abort arity'); 31if (WorkerNavigator.length !== 0 || NavigatorUAData.length !== 0 || Performance.length !== 0) 32throw Error('Runtime constructor arity'); 33const reason = {}; 34const controller = new AbortController(); 35controller.abort(reason); 36if (controller.signal.reason !== reason || AbortSignal.abort(reason).reason !== reason) 37throw Error('Abort reason identity'); 38console.log('conversion and arities'); 39"# , 40& [], 41); 42assert_eq! ( output. stdout , b"conversion and arities\n" ); 43} 44 45# [ test ] 46fn event_interfaces_global_target_and_error_reporting () { 47let output =run ( 48r#" 49const detail = {value: 1}; 50const custom = new CustomEvent('custom', {detail}); 51if (!(custom instanceof Event) || custom.detail !== detail || custom.type !== 'custom') throw Error('CustomEvent state'); 52if (new CustomEvent('empty').detail !== null) throw Error('CustomEvent default'); 53for (const EventType of [CustomEvent, ErrorEvent, PromiseRejectionEvent]) { 54try { new EventType(); throw Error('Missing event type accepted'); } 55catch (error) { if (!(error instanceof TypeError)) throw error; } 56} 57const errorEvent = new ErrorEvent('error', { 58message: 42, filename: 'source.js', lineno: -1, colno: 1.5, error: undefined, 59}); 60if (!(errorEvent instanceof Event) || errorEvent.message !== '42' || errorEvent.filename !== 'source.js' 61|| errorEvent.lineno !== 4294967295 || errorEvent.colno !== 1 || errorEvent.error !== null) { 62throw Error('ErrorEvent state'); 63} 64const conversion = []; 65new Event({[Symbol.toPrimitive]() { conversion.push('type'); return 'conversion'; }}, { 66get bubbles() { conversion.push('bubbles'); return true; }, 67}); 68if (conversion.join() !== 'type,bubbles') throw Error('Event WebIDL conversion order'); 69const promiseEvent = new PromiseRejectionEvent('unhandledrejection', { 70promise: Promise.resolve(), reason: undefined, 71}); 72if (!(promiseEvent.promise instanceof Promise) || promiseEvent.reason !== undefined) { 73throw Error('PromiseRejectionEvent state'); 74} 75const fakePromise = Object.create(Promise.prototype); 76try { new PromiseRejectionEvent('unhandledrejection', {promise: fakePromise}); throw Error('Accepted non-Promise'); } 77catch (error) { if (!(error instanceof TypeError)) throw error; } 78const stamp = new Event('stamp').timeStamp; 79if (stamp < 0 || stamp > performance.now() + 1) throw Error('Event timestamp clock'); 80if (addEventListener.length !== 2 || removeEventListener.length !== 2 || dispatchEvent.length !== 1) { 81throw Error('Global EventTarget method signatures'); 82} 83try { EventTarget.prototype.addEventListener.call({}, 'bad', () => {}); throw Error('Invalid EventTarget receiver'); } 84catch (error) { if (!(error instanceof TypeError)) throw error; } 85let target; 86globalThis.addEventListener('global', function(event) { 87target = this; 88if (event.target !== globalThis || event.currentTarget !== globalThis) throw Error('Global event target'); 89}); 90const globalEvent = new Event('global'); 91if (!globalThis.dispatchEvent(globalEvent) || globalEvent.target !== globalThis || target !== globalThis) { 92throw Error('Global dispatch target'); 93} 94const initialized = new Event('old', {cancelable: true}); 95globalThis.addEventListener('old', event => event.initEvent('changed', true, true)); 96globalThis.dispatchEvent(initialized); 97if (initialized.type !== 'old') throw Error('initEvent changed a dispatching event'); 98initialized.initEvent('new', true, false); 99if (initialized.type !== 'new' || !initialized.bubbles || initialized.cancelable) throw Error('initEvent'); 100const reported = new Error('reported'); 101let expectedError = reported, errorCalls = 0, legacyArgs; 102globalThis.addEventListener('error', event => { 103errorCalls++; 104if (!(event instanceof ErrorEvent) || event.error !== expectedError 105|| event.message !== (expectedError === undefined ? 'undefined' : expectedError.message) 106|| event.target !== globalThis || !event.cancelable) throw Error('Reported ErrorEvent'); 107event.preventDefault(); 108}); 109globalThis.onerror = function(message, filename, line, column, error) { 110legacyArgs = [this === globalThis, message, filename, line, column, error]; 111return true; 112}; 113reportError(reported); 114if (errorCalls !== 1 || legacyArgs[0] !== true || legacyArgs[1] !== 'reported' 115|| legacyArgs[2] !== '' || legacyArgs[3] !== 0 || legacyArgs[4] !== 0 || legacyArgs[5] !== reported) { 116throw Error('Global error handler'); 117} 118expectedError = undefined; 119reportError(undefined); 120if (errorCalls !== 2 || legacyArgs[5] !== undefined || reportError.length !== 1) { 121throw Error('reportError undefined value or function length'); 122} 123const handlerError = new Error('handler'); 124expectedError = handlerError; 125const handlerOrder = []; 126globalThis.onerror = () => handlerOrder.push('old'); 127const orderListener = () => handlerOrder.push('listener'); 128globalThis.addEventListener('error', orderListener); 129globalThis.onerror = () => { handlerOrder.push('new'); return true; }; 130reportError(handlerError); 131if (handlerOrder.join() !== 'new,listener') throw Error('Global handler listener order'); 132globalThis.removeEventListener('error', orderListener); 133globalThis.onerror = null; 134const listenerError = new Error('listener'); 135expectedError = listenerError; 136globalThis.addEventListener('trigger', () => { throw listenerError; }); 137globalThis.dispatchEvent(new Event('trigger')); 138if (errorCalls !== 4) throw Error('Listener exception was not reported'); 139try { reportError(); throw Error('Missing reportError argument accepted'); } 140catch (error) { if (!(error instanceof TypeError)) throw error; } 141try { structuredClone(custom); throw Error('CustomEvent was cloneable'); } 142catch (error) { if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error; } 143console.log('events'); 144"# , 145& [], 146); 147assert_eq! ( output. stdout , b"events\n" ); 148} 149 150# [ test ] 151fn unhandled_rejection_events_preserve_identity_and_report_late_handlers () { 152let output =run ( 153r#" 154const reason = {failure: true}; 155const promise = Promise.reject(reason); 156const sameTurn = Promise.reject('same turn'); 157Promise.resolve().then(() => sameTurn.catch(() => {})); 158let unhandled = 0, handled = 0; 159globalThis.addEventListener('unhandledrejection', event => { 160unhandled++; 161if (!(event instanceof PromiseRejectionEvent) || event.promise !== promise 162|| event.reason !== reason || !event.cancelable || event.target !== globalThis) { 163throw Error('Unhandled rejection event'); 164} 165event.preventDefault(); 166}); 167globalThis.addEventListener('rejectionhandled', event => { 168handled++; 169if (!(event instanceof PromiseRejectionEvent) || event.promise !== promise 170|| event.reason !== reason || event.cancelable || event.target !== globalThis) { 171throw Error('Handled rejection event'); 172} 173}); 174setTimeout(() => promise.catch(() => {}), 0); 175await new Promise(resolve => setTimeout(resolve, 20)); 176if (unhandled !== 1 || handled !== 1) throw Error(`Rejection events: ${unhandled}/${handled}`); 177console.log('rejections'); 178"# , 179& [], 180); 181assert_eq! ( output. stdout , b"rejections\n" ); 182} 183 184# [ test ] 185fn canceled_callback_errors_do_not_stop_queued_work () { 186let output =run ( 187r#" 188let calls = 0; 189globalThis.addEventListener('error', event => { 190if (event.message !== 'canceled') throw Error('Unexpected callback error'); 191event.preventDefault(); 192}); 193setTimeout(() => { throw new Error('canceled'); }, 0); 194setTimeout(() => calls++, 0); 195await new Promise(resolve => setTimeout(resolve, 10)); 196if (calls !== 1) throw Error('Canceled callback stopped later tasks'); 197console.log('continued'); 198"# , 199& [], 200); 201assert_eq! ( output. stdout , b"continued\n" ); 202} 203 204# [ test ] 205fn microtask_error_event_uses_typescript_source_locations () { 206let dir = tempfile:: tempdir (). unwrap (); 207 std:: fs:: write ( 208 dir. path (). join ( "main.ts" ), 209r#" 210globalThis.onerror = (message, filename, line, column, error) => { 211if (message !== 'mapped' || !filename.endsWith('/main.ts') || line < 2 || column < 1 212|| error.message !== 'mapped') throw Error('Incorrect mapped ErrorEvent metadata'); 213console.log(`${filename}:${line}:${column}`); 214return true; 215}; 216queueMicrotask(() => { throw new Error('mapped'); }); 217await new Promise(resolve => setTimeout(resolve, 10)); 218"# , 219) 220. unwrap (); 221let output = std:: process:: Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 222. arg ( "run" ) 223. current_dir ( dir. path ()) 224. arg ( "main.ts" ) 225. output () 226. unwrap (); 227assert! ( 228 output. status . success (), 229"{}" , 230String :: from_utf8_lossy ( & output. stderr ) 231); 232assert! ( String :: from_utf8_lossy ( & output. stdout ). contains ( "/main.ts:" )); 233} 234 235# [ test ] 236fn top_level_await_rejection_reports_error_and_remains_fatal () { 237let ( _dir, child) =spawn ( 238r#" 239globalThis.onerror = (message) => { console.log(`error:${message}`); return true; }; 240globalThis.addEventListener('unhandledrejection', () => console.log('unhandled')); 241await Promise.reject(new Error('module failure')); 242"# , 243); 244let output =wait ( child); 245assert! ( !output. status . success ()); 246assert_eq! ( output. stdout , b"error:module failure\n" ); 247assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "module failure" )); 248} 249 250# [ test ] 251fn uncanceled_unhandled_rejection_remains_fatal () { 252let ( _dir, child) =spawn ( 253r#" 254globalThis.addEventListener('unhandledrejection', event => { 255console.log(`unhandled:${event.reason}`); 256}); 257Promise.reject('fatal rejection'); 258await new Promise(resolve => setTimeout(resolve, 10)); 259"# , 260); 261let output =wait ( child); 262assert! ( !output. status . success ()); 263assert_eq! ( output. stdout , b"unhandled:fatal rejection\n" ); 264assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "fatal rejection" )); 265} 266 267# [ test ] 268fn uncanceled_report_error_remains_fatal () { 269let ( _dir, child) =spawn ( "reportError(32);" ); 270let output =wait ( child); 271assert! ( !output. status . success ()); 272assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "32" )); 273} 274 275# [ test ] 276fn global_event_operations_are_unbound_and_microtask_errors_are_not_dropped () { 277let out =run ( 278r#" 279const seen=[]; 280onerror=(message)=>{seen.push(message);return true;}; 281const listener=event=>seen.push(event.type); 282addEventListener('unbound',listener); 283if(!dispatchEvent(new Event('unbound')))throw Error('Dispatch'); 284removeEventListener('unbound',listener); 285dispatchEvent(new Event('unbound')); 286for(const call of [()=>addEventListener('x'),()=>removeEventListener('x')]){ 287try{call();throw Error('Missing listener accepted');}catch(e){if(!(e instanceof TypeError))throw e;} 288} 289queueMicrotask(()=>{throw 'first';}); 290queueMicrotask(()=>{throw 'second';}); 291await new Promise(r=>setTimeout(r,5)); 292if(seen.join()!=='unbound,first,second')throw Error(seen.join()); 293const custom=new CustomEvent('x',{detail:undefined}); 294const error=new ErrorEvent('x',{message:undefined,filename:undefined,error:undefined}); 295if(custom.detail!==null||error.message!==''||error.filename!==''||error.error!==null)throw Error('Dictionary defaults'); 296const lone='\ud800'; 297if(new Event(lone).type!==lone||new ErrorEvent('x',{message:lone}).message!==lone)throw Error('DOMString replaced'); 298console.log('global events'); 299"# , 300& [], 301); 302assert_eq! ( out. stdout , b"global events\n" ); 303} 304 305# [ test ] 306fn host_events_are_trusted_but_redispatch_is_untrusted () { 307let out =run ( 308r#" 309let original; 310onerror=()=>true; 311addEventListener('error',event=>{original=event;event.preventDefault();},{once:true}); 312reportError('host'); 313if(!original.isTrusted||new Event('synthetic').isTrusted)throw Error('Trusted flag'); 314const getter=Object.getOwnPropertyDescriptor(original,'isTrusted').get; 315if(getter.call(new Event('synthetic')))throw Error('Unforgeable getter receiver'); 316dispatchEvent(original); 317if(original.isTrusted)throw Error('Redispatch retained trusted flag'); 318const signal=AbortSignal.timeout(0); 319signal.addEventListener('abort',event=>{if(!event.isTrusted)throw Error('Untrusted abort');}); 320await new Promise(r=>setTimeout(r,5)); 321console.log('trusted events'); 322"# , 323& [], 324); 325assert_eq! ( out. stdout , b"trusted events\n" ); 326} 327 328# [ test ] 329fn exceptions_in_error_handlers_are_fatal_without_recursive_error_events () { 330let ( _dir, child) =spawn ( 331r#" 332let count=0; 333addEventListener('error',event=>{count++;event.preventDefault();throw new Error('handler failed');}); 334reportError('original'); 335console.log(count); 336"# , 337); 338let output =wait ( child); 339assert! ( !output. status . success ()); 340assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "handler failed" )); 341assert_eq! ( output. stdout , b"1\n" ); 342} 343 344# [ test ] 345fn global_handler_return_values_and_rejection_lifecycle_follow_html () { 346let out =run ( 347r#" 348if(!(globalThis instanceof EventTarget))throw Error('Global EventTarget'); 349let handled=0; 350onrejectionhandled=()=>handled++; 351onunhandledrejection=event=>{event.promise.catch(()=>{});return false;}; 352Promise.reject('handled while reporting'); 353await new Promise(r=>setTimeout(r,5)); 354if(handled!==0)throw Error('Reported handler before outstanding set'); 355onunhandledrejection=()=>false; 356const late=Promise.reject('later handled'); 357await new Promise(r=>setTimeout(r,5)); 358late.catch(()=>{}); 359await new Promise(r=>setTimeout(r,5)); 360if(handled!==1)throw Error('Genuinely late handler not reported'); 361const one=new Event('one'),two=new Event('two'); 362if(Object.getOwnPropertyDescriptor(one,'isTrusted').get!==Object.getOwnPropertyDescriptor(two,'isTrusted').get)throw Error('Trust accessor identity'); 363const stamp=one.timeStamp; 364if(Math.abs(stamp*10-Math.round(stamp*10))>1e-7)throw Error('Timestamp not coarsened'); 365console.log('html handlers'); 366"# , 367& [], 368); 369assert_eq! ( out. stdout , b"html handlers\n" ); 370} 371 372# [ test ] 373fn event_aliases_and_dictionary_members_use_native_state_and_single_reads () { 374let out =run ( 375r#" 376const event=new Event('alias'), target=new EventTarget(); 377if(event.srcElement!==null)throw Error('Initial srcElement'); 378for(const [name,value] of [['NONE',0],['CAPTURING_PHASE',1],['AT_TARGET',2],['BUBBLING_PHASE',3]]){ 379if(Event[name]!==value||event[name]!==value)throw Error('Event phase constants'); 380} 381target.dispatchEvent(event); 382if(event.srcElement!==target||event.srcElement!==event.target)throw Error('srcElement target alias'); 383let calls=0; 384const custom=new CustomEvent('custom',{get detail(){calls++;return calls;}}); 385if(calls!==1||custom.detail!==1)throw Error('detail read twice'); 386const error=new ErrorEvent('error',{get message(){calls++;return 'message';},get filename(){calls++;return 'file';},get error(){calls++;return 'reason';}}); 387if(calls!==4||error.error!=='reason')throw Error('ErrorEvent dictionary reread'); 388const lone='\ud800',exception=new DOMException(lone,lone); 389if(exception.name!==lone||exception.message!==lone)throw Error('DOMException DOMString'); 390for(const key of ['name','message']){ 391if(Object.hasOwn(exception,key))throw Error('DOMException own field'); 392const descriptor=Object.getOwnPropertyDescriptor(DOMException.prototype,key); 393if(!descriptor.get||descriptor.set||!descriptor.enumerable||!descriptor.configurable)throw Error('DOMException attribute shape'); 394try{descriptor.get.call({});throw Error('DOMException unbranded getter');}catch(e){if(!(e instanceof TypeError))throw e;} 395} 396let host; 397onerror=()=>true; 398addEventListener('error',event=>{host=event;},{once:true}); 399reportError('host'); 400if(!host.isTrusted)throw Error('Initial host flag'); 401host.initEvent('reinitialized'); 402if(host.isTrusted)throw Error('initEvent retained trusted flag'); 403console.log('event conversions'); 404"# , 405& [], 406); 407assert_eq! ( out. stdout , b"event conversions\n" ); 408}