char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport web extension to typescript44fe2c6

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