mod common; #[test] fn broadcast_fanout_clones_once_and_preserves_fifo_tasks() { let output = common::run( r#" const sender = new BroadcastChannel('test'), a = new BroadcastChannel('test'), b = new BroadcastChannel('test'); const other = new BroadcastChannel('other'); sender.onmessage = other.onmessage = () => {throw Error('Wrong recipient');}; let reads = 0; const values = {get value() {reads++; return 1;}}; sender.postMessage(values); sender.postMessage({value: 2}); const seen = []; a.onmessage = event => { if (event.target !== a || !(event instanceof MessageEvent) || !event.isTrusted || event.source !== null || event.ports.length !== 0 || event.origin !== '') throw Error('MessageEvent'); seen.push(event.data.value); event.data.value = 9; queueMicrotask(() => seen.push('microtask')); }; const received = new Promise(resolve => b.onmessage = event => { if (event.data.value !== 1 && event.data.value !== 2) throw Error('Shared clone'); if (event.data.value === 2) resolve(); }); queueMicrotask(() => seen.push('before')); await received; await new Promise(resolve => setTimeout(resolve, 10)); if (reads !== 1 || seen.join() !== 'before,1,microtask,2,microtask') throw Error('Clone/task order ' + seen); for (const channel of [sender,a,b,other]) channel.close(); console.log('broadcast'); "#, &[], ); assert_eq!(output.stdout, b"broadcast\n"); } #[test] fn broadcast_brands_clone_errors_and_close() { let output = common::run( r#" class Sub extends BroadcastChannel {} const a = new Sub({toString() {return 'name';}}), b = new BroadcastChannel('name'); if (a.name !== 'name' || !(a instanceof EventTarget) || !(a instanceof Sub) || Object.prototype.toString.call(a) !== '[object BroadcastChannel]' || a.onmessage !== null || a.onmessageerror !== null) throw Error('Surface'); for (const operation of [() => new BroadcastChannel(), () => new BroadcastChannel(Symbol()), () => BroadcastChannel.prototype.close.call({}), () => a.postMessage()]) { try {operation(); throw Error('Accepted invalid input');} catch (error) {if (!(error instanceof TypeError)) throw error;} } const buffer = new Uint8Array([1]).buffer; for (const value of [() => {}, a, new MessageChannel().port1]) { try {a.postMessage(value); throw Error('Clone accepted');} catch (error) {if (error.name !== 'DataCloneError') throw error;} } a.postMessage(buffer, [buffer]); if (buffer.byteLength !== 1) throw Error('Unexpected transfer'); b.onmessage = () => {throw Error('Closed destination received a message');}; b.close(); b.close(); a.close(); try {a.postMessage(() => {}); throw Error('Closed channel posted');} catch (error) {if (error.name !== 'InvalidStateError') throw error;} const lonely = new BroadcastChannel('lonely'); try {lonely.postMessage(() => {}); throw Error('No-recipient clone skipped');} catch (error) {if (error.name !== 'DataCloneError') throw error;} lonely.close(); await new Promise(resolve => setTimeout(resolve, 10)); console.log('closed'); "#, &[], ); assert_eq!(output.stdout, b"closed\n"); } #[test] fn broadcasts_cross_worker_isolates_and_clone_host_objects() { let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("worker.js"), r#" const channel = new BroadcastChannel('workers'); channel.onmessage = async event => { if (!(event.data.blob instanceof Blob) || await event.data.blob.text() !== 'blob' || event.data.map.get('key') !== 4) throw Error('Host clone'); channel.postMessage('reply'); channel.close(); close(); }; postMessage('ready'); "#).unwrap(); std::fs::write(dir.path().join("main.js"), r#" const channel = new BroadcastChannel('workers'); const worker = new Worker(new URL('./worker.js', import.meta.url), {type:'module'}); await new Promise((resolve, reject) => {worker.onmessage = resolve; worker.onerror = reject;}); const reply = new Promise(resolve => channel.onmessage = event => resolve(event.data)); const post = BroadcastChannel.prototype.postMessage, close = BroadcastChannel.prototype.close; MessagePort.prototype.postMessage = () => {throw Error('Guest port');}; globalThis.structuredClone = () => {throw Error('Guest clone');}; post.call(channel, {blob:new Blob(['blob']), map:new Map([['key',4]])}); if (await reply !== 'reply') throw Error('Worker reply'); close.call(channel); worker.terminate(); console.log('workers'); "#).unwrap(); let output = common::wait( std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .arg(dir.path().join("main.js")) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .unwrap(), ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"workers\n"); } #[test] fn broadcast_names_preserve_exact_domstring_code_units() { let output = common::run( r#" const names = ['\uD800','\uDC00','\uFFFD','valid😀','nul\0name']; const senders = names.map(name => new BroadcastChannel(name)); const receivers = names.map(name => new BroadcastChannel(name)); const counts = names.map(() => 0); const completed = new Promise(resolve => receivers.forEach((channel,i) => { if (channel.name !== names[i]) throw Error('Name conversion'); channel.onmessage = event => { if (event.data !== i || ++counts[i] !== 1) throw Error('Name crosstalk'); if (counts.every(count => count === 1)) resolve(); }; })); senders.forEach((channel,i) => channel.postMessage(i)); await completed; await new Promise(resolve => setTimeout(resolve,10)); [...senders,...receivers].forEach(channel => channel.close()); console.log('names'); "#, &[], ); assert_eq!(output.stdout, b"names\n"); }