diff options
Diffstat (limited to 'testing/web-platform/tests/webmessaging/broadcastchannel')
19 files changed, 1766 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/WEB_FEATURES.yml b/testing/web-platform/tests/webmessaging/broadcastchannel/WEB_FEATURES.yml new file mode 100644 index 0000000000..378ed57dc5 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/WEB_FEATURES.yml @@ -0,0 +1,3 @@ +features: +- name: broadcast-channel + files: "**" diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/basics.any.js b/testing/web-platform/tests/webmessaging/broadcastchannel/basics.any.js new file mode 100644 index 0000000000..eec09d65a3 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/basics.any.js @@ -0,0 +1,128 @@ +test(function() { + assert_throws_js( + TypeError, + () => BroadcastChannel(""), + "Calling BroadcastChannel constructor without 'new' must throw" + ); +}, "BroadcastChannel constructor called as normal function"); + +async_test(t => { + let c1 = new BroadcastChannel('eventType'); + let c2 = new BroadcastChannel('eventType'); + + c2.onmessage = t.step_func(e => { + assert_true(e instanceof MessageEvent); + assert_equals(e.target, c2); + assert_equals(e.type, 'message'); + assert_equals(e.origin, location.origin, 'origin'); + assert_equals(e.data, 'hello world'); + assert_equals(e.source, null, 'source'); + t.done(); + }); + c1.postMessage('hello world'); + }, 'postMessage results in correct event'); + +async_test(t => { + let c1 = new BroadcastChannel('order'); + let c2 = new BroadcastChannel('order'); + let c3 = new BroadcastChannel('order'); + + let events = []; + let doneCount = 0; + let handler = t.step_func(e => { + events.push(e); + if (e.data == 'done') { + doneCount++; + if (doneCount == 2) { + assert_equals(events.length, 6); + assert_equals(events[0].target, c2, 'target for event 0'); + assert_equals(events[0].data, 'from c1'); + assert_equals(events[1].target, c3, 'target for event 1'); + assert_equals(events[1].data, 'from c1'); + assert_equals(events[2].target, c1, 'target for event 2'); + assert_equals(events[2].data, 'from c3'); + assert_equals(events[3].target, c2, 'target for event 3'); + assert_equals(events[3].data, 'from c3'); + assert_equals(events[4].target, c1, 'target for event 4'); + assert_equals(events[4].data, 'done'); + assert_equals(events[5].target, c3, 'target for event 5'); + assert_equals(events[5].data, 'done'); + t.done(); + } + } + }); + c1.onmessage = handler; + c2.onmessage = handler; + c3.onmessage = handler; + + c1.postMessage('from c1'); + c3.postMessage('from c3'); + c2.postMessage('done'); + }, 'messages are delivered in port creation order'); + +async_test(t => { + let c1 = new BroadcastChannel('closed'); + let c2 = new BroadcastChannel('closed'); + let c3 = new BroadcastChannel('closed'); + + c2.onmessage = t.unreached_func(); + c2.close(); + c3.onmessage = t.step_func(() => t.done()); + c1.postMessage('test'); + }, 'messages aren\'t delivered to a closed port'); + + async_test(t => { + let c1 = new BroadcastChannel('closed'); + let c2 = new BroadcastChannel('closed'); + let c3 = new BroadcastChannel('closed'); + + c2.onmessage = t.unreached_func(); + c3.onmessage = t.step_func(() => t.done()); + c1.postMessage('test'); + c2.close(); +}, 'messages aren\'t delivered to a port closed after calling postMessage.'); + +async_test(t => { + let c1 = new BroadcastChannel('create-in-onmessage'); + let c2 = new BroadcastChannel('create-in-onmessage'); + + c2.onmessage = t.step_func(e => { + assert_equals(e.data, 'first'); + c2.close(); + let c3 = new BroadcastChannel('create-in-onmessage'); + c3.onmessage = t.step_func(event => { + assert_equals(event.data, 'done'); + t.done(); + }); + c1.postMessage('done'); + }); + c1.postMessage('first'); + c2.postMessage('second'); + }, 'closing and creating channels during message delivery works correctly'); + +async_test(t => { + let c1 = new BroadcastChannel('close-in-onmessage'); + let c2 = new BroadcastChannel('close-in-onmessage'); + let c3 = new BroadcastChannel('close-in-onmessage'); + let events = []; + c1.onmessage = e => events.push('c1: ' + e.data); + c2.onmessage = e => events.push('c2: ' + e.data); + c3.onmessage = e => events.push('c3: ' + e.data); + + // c2 closes itself when it receives the first message + c2.addEventListener('message', e => { + c2.close(); + }); + + c3.addEventListener('message', t.step_func(e => { + if (e.data == 'done') { + assert_array_equals(events, [ + 'c2: first', + 'c3: first', + 'c3: done']); + t.done(); + } + })); + c1.postMessage('first'); + c1.postMessage('done'); + }, 'Closing a channel in onmessage prevents already queued tasks from firing onmessage events'); diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/blobs.html b/testing/web-platform/tests/webmessaging/broadcastchannel/blobs.html new file mode 100644 index 0000000000..ab5096b63c --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/blobs.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/gc.js"></script> +<script> +async_test(t => { + const c1 = new BroadcastChannel('blob'); + const c2 = new BroadcastChannel('blob'); + const c3 = new BroadcastChannel('blob'); + + let readCount = 0; + c2.onmessage = t.step_func(e => { + // check blob + assert_true('blob' in e.data); + assert_true(e.data.blob instanceof Blob); + assert_equals(e.data.blob.size, 6); + const reader = new FileReader(); + reader.onerror = t.unreached_func(); + reader.onload = t.step_func(() => { + assert_equals(reader.result, 'foobar'); + if (++readCount == 2) + t.done(); + }); + reader.readAsText(e.data.blob); + }); + c3.onmessage = c2.onmessage; + (() => { + c1.postMessage({blob: new Blob(['foo', 'bar'])}); + })(); + garbageCollect(); + }, 'Blobs work on BroadcastChannel'); + +async_test(t => { + const c1 = new BroadcastChannel('blobworker'); + const c2 = new BroadcastChannel('blobworker'); + const events = []; + + const verifyEvents = function() { + assert_equals(events.length, 5); + assert_equals(events[0], 'from worker'); + assert_equals(events[1], 'from worker'); + assert_true(events[2].blob instanceof Blob); + assert_equals(events[2].blob.size, 11); + assert_true(events[3].blob instanceof Blob); + assert_equals(events[3].blob.size, 11); + assert_equals(events[4], 'done'); + const reader = new FileReader(); + reader.onerror = t.unreached_func(); + reader.onload = t.step_func(() => { + assert_equals(reader.result, 'hello-world'); + t.done(); + }); + reader.readAsText(events[3].blob); + }; + + let receivedDone = false; + let receivedWorkerDone = false; + + c1.onmessage = e => events.push(e.data); + c2.onmessage = e => events.push(e.data); + c2.addEventListener('message', t.step_func(e => { + if (e.data.blob) + c1.postMessage('done'); + if (e.data === 'done') + receivedDone = true; + if (receivedDone && receivedWorkerDone) + verifyEvents(); + })); + + const worker = new Worker('resources/worker.js'); + worker.onmessage = t.step_func(e => { + receivedWorkerDone = true; + if (receivedDone && receivedWorkerDone) + verifyEvents(); + }); + worker.postMessage({channel: 'blobworker'}); + worker.postMessage({blob: ['hello-world']}); + + }, 'Blobs work with workers on BroadcastChannel'); + +</script> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/cross-origin.html b/testing/web-platform/tests/webmessaging/broadcastchannel/cross-origin.html new file mode 100644 index 0000000000..ee4b2f21c8 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/cross-origin.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<!-- Pull in the with_iframe helper function from the service worker tests --> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<body> +<script> + +const events = []; + +function testCompletion(t) { + return new Promise((resolve) => { + window.addEventListener("message", t.step_func(e => { + if (e.data == 'done') { + assert_equals(events.length, 0); + resolve(); + } + })); + }); +} + +promise_test(async t => { + + const bc0 = new BroadcastChannel('no-cross-origin-messages'); + bc0.onmessage = e => {window.events.push(e);}; + + const testResults = testCompletion(t); + const url = get_host_info().HTTPS_NOTSAMESITE_ORIGIN + + '/webmessaging/broadcastchannel/resources/cross-origin.html'; + await with_iframe(url); + + return testResults; +}, "Messages aren't delivered across origins"); + +</script> +</body> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/cross-partition.https.tentative.html b/testing/web-platform/tests/webmessaging/broadcastchannel/cross-partition.https.tentative.html new file mode 100644 index 0000000000..4e91da5546 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/cross-partition.https.tentative.html @@ -0,0 +1,356 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<!-- Pull in executor_path needed by newPopup / newIframe --> +<script src="/html/cross-origin-embedder-policy/credentialless/resources/common.js"></script> +<!-- Pull in importScript / newPopup / newIframe --> +<script src="/html/anonymous-iframe/resources/common.js"></script> +<body> +<script> + +const bc_postmessage_js = (channel_name, message, done_queue_name) => ` + const bc = new BroadcastChannel("${channel_name}"); + bc.postMessage("${message}"); + send("${done_queue_name}", "done"); +`; + +const add_iframe_js = (iframe_origin, response_queue_uuid) => ` + const importScript = ${importScript}; + await importScript("/html/cross-origin-embedder-policy/credentialless" + + "/resources/common.js"); + await importScript("/html/anonymous-iframe/resources/common.js"); + await importScript("/common/utils.js"); + send("${response_queue_uuid}", newIframe("${iframe_origin}")); +`; + +const same_site_origin = get_host_info().HTTPS_ORIGIN; +const cross_site_origin = get_host_info().HTTPS_NOTSAMESITE_ORIGIN; + +async function create_test_iframes(t, response_queue_uuid) { + + // Create a same-origin iframe in a cross-site popup. + const not_same_site_popup_uuid = newPopup(t, cross_site_origin); + send(not_same_site_popup_uuid, + add_iframe_js(same_site_origin, response_queue_uuid)); + const iframe_1_uuid = await receive(response_queue_uuid); + + // Create a same-origin iframe in a same-site popup. + const same_origin_popup_uuid = newPopup(t, same_site_origin); + send(same_origin_popup_uuid, + add_iframe_js(same_site_origin, response_queue_uuid)); + const iframe_2_uuid = await receive(response_queue_uuid); + + return [iframe_1_uuid, iframe_2_uuid]; +} + +promise_test(t => { + return new Promise(async (resolve, reject) => { + const response_queue_uuid = token(); + + const [iframe_1_uuid, iframe_2_uuid] = + await create_test_iframes(t, response_queue_uuid); + + const channel_name = token(); + const bc = new BroadcastChannel(channel_name); + bc.onmessage = resolve; + + // Instruct the not-same-top-level-site iframe to send a message on the BC + // channel we are listening on. This message should not be received since + // the iframe should be in a different partition. + send(iframe_1_uuid, + bc_postmessage_js(channel_name, "iframe1 msg", response_queue_uuid)); + if (await receive(response_queue_uuid) != "done") { + reject("Unable to trigger iframe1 BroadcastChannel message creation"); + } + + // Now instruct the same-top-level-site iframe to send a BC message. By + // the time we send the script to execute, have it send the BC message, + // and then receive the BC message in our BC instance, it should be + // reasonable to assume that the message from the first iframe would have + // been delivered if it was going to be. + send(iframe_2_uuid, + bc_postmessage_js(channel_name, "iframe2 msg", response_queue_uuid)); + if (await receive(response_queue_uuid) != "done") { + reject("Unable to trigger iframe2 BroadcastChannel message creation"); + } + + }).then(event => { + assert_equals(event.data, "iframe2 msg"); + }); + +}, "BroadcastChannel messages aren't received from a cross-partition iframe"); + +// Optional Test: Checking for partitioned BroadcastChannels in an A->B->A +// (nested-iframe with cross-site ancestor chain) scenario. +promise_test(t => { + return new Promise(async (resolve, reject) => { + const response_queue_uuid = token(); + + const [cross_site_iframe_uuid, all_same_parent_iframe_uuid] = + await create_test_iframes(t, response_queue_uuid); + + // Create a same-origin iframe in a cross-site iframe in a same-site popup. + // Create the same-site child iframe of the cross-site iframe (A->B->A). + send(cross_site_iframe_uuid, + add_iframe_js(same_site_origin, response_queue_uuid)); + const same_site_iframe_uuid = await receive(response_queue_uuid); + + // Create a same-origin iframe in a same-site iframe in a same-site popup. + // Create the same-site child iframe of the same-site parent iframe (A->A->A). + send(all_same_parent_iframe_uuid, + add_iframe_js(same_site_origin, response_queue_uuid)); + const all_same_child_iframe_uuid = await receive(response_queue_uuid); + + const channel_name = token(); + const bc = new BroadcastChannel(channel_name); + bc.onmessage = resolve; + + // Instruct the A->B->A child iframe to send a message on the BC + // channel we are listening on. This message should not be received since + // the iframe should be in a different partition. + send(same_site_iframe_uuid, + bc_postmessage_js(channel_name, "iframe1 msg", response_queue_uuid)); + if (await receive(response_queue_uuid) != "done") { + reject("Unable to trigger A->B->A BroadcastChannel message creation"); + } + + // Now instruct the A->A->A child iframe to send a BC message. By + // the time we send the script to execute, send the BC message, + // and receive the BC message in our BC instance, it should be + // reasonable to assume that the message from the first iframe would have + // been delivered if it was going to be. + send(all_same_child_iframe_uuid, + bc_postmessage_js(channel_name, "iframe2 msg", response_queue_uuid)); + if (await receive(response_queue_uuid) != "done") { + reject("Unable to trigger A->A->A BroadcastChannel message creation"); + } + + }).then(event => { + assert_equals(event.data, "iframe2 msg"); + }); + +}, "BroadcastChannel messages aren't received from a nested iframe with a cross-site ancestor"); + +const newWorker = (origin) => { + const worker_token = token(); + const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`; + const worker = new Worker(worker_url); + return worker_token; +} + +promise_test(t => { + return new Promise(async (resolve, reject) => { + const response_queue_uuid = token(); + + const create_worker_js = (origin) => ` + const importScript = ${importScript}; + await importScript("/html/cross-origin-embedder-policy/credentialless" + + "/resources/common.js"); + await importScript("/html/anonymous-iframe/resources/common.js"); + await importScript("/common/utils.js"); + const newWorker = ${newWorker}; + send("${response_queue_uuid}", newWorker("${origin}")); + `; + + const [iframe_1_uuid, iframe_2_uuid] = + await create_test_iframes(t, response_queue_uuid); + + // Create a dedicated worker in the cross-top-level-site iframe. + send(iframe_1_uuid, create_worker_js(same_site_origin)); + const worker_1_uuid = await receive(response_queue_uuid); + + // Create a dedicated worker in the same-top-level-site iframe. + send(iframe_2_uuid, create_worker_js(same_site_origin)); + const worker_2_uuid = await receive(response_queue_uuid); + + const channel_name = token(); + const bc = new BroadcastChannel(channel_name); + bc.onmessage = async (e) => { + await send(worker_1_uuid, "self.close();"); + await send(worker_2_uuid, "self.close();"); + resolve(e); + }; + + // Instruct the not-same-top-level-site worker to send a message on the BC + // channel we are listening on. This message should not be received since + // the worker should be in a different partition. + send(worker_1_uuid, + bc_postmessage_js(channel_name, "worker1 msg", response_queue_uuid)); + if (await receive(response_queue_uuid) != "done") { + reject("Unable to trigger worker1 BroadcastChannel message creation"); + } + + // Now instruct the same-top-level-site worker to send a BC message. By + // the time we send the script to execute, have it send the BC message, + // and then receive the BC message in our BC instance, it should be + // reasonable to assume that the message from the first worker would have + // been delivered if it was going to be. + send(worker_2_uuid, + bc_postmessage_js(channel_name, "worker2 msg", response_queue_uuid)); + if (await receive(response_queue_uuid) != "done") { + reject("Unable to trigger worker2 BroadcastChannel message creation"); + } + + }).then(event => { + assert_equals(event.data, "worker2 msg"); + }); + +}, "BroadcastChannel messages aren't received from a cross-partition dedicated worker"); + +const newSharedWorker = (origin) => { + const worker_token = token(); + const worker_url = origin + executor_worker_path + `&uuid=${worker_token}`; + const worker = new SharedWorker(worker_url, worker_token); + return worker_token; +} + +promise_test(t => { + return new Promise(async (resolve, reject) => { + const response_queue_uuid = token(); + + const create_worker_js = (origin) => ` + const importScript = ${importScript}; + await importScript("/html/cross-origin-embedder-policy/credentialless" + + "/resources/common.js"); + await importScript("/html/anonymous-iframe/resources/common.js"); + await importScript("/common/utils.js"); + const newSharedWorker = ${newSharedWorker}; + send("${response_queue_uuid}", newSharedWorker("${origin}")); + `; + + const [iframe_1_uuid, iframe_2_uuid] = + await create_test_iframes(t, response_queue_uuid); + + // Create a shared worker in the cross-top-level-site iframe. + send(iframe_1_uuid, create_worker_js(same_site_origin)); + const worker_1_uuid = await receive(response_queue_uuid); + + // Create a shared worker in the same-top-level-site iframe. + send(iframe_2_uuid, create_worker_js(same_site_origin)); + const worker_2_uuid = await receive(response_queue_uuid); + + const channel_name = token(); + const bc = new BroadcastChannel(channel_name); + bc.onmessage = async (e) => { + await send(worker_1_uuid, "self.close();"); + await send(worker_2_uuid, "self.close();"); + resolve(e); + }; + + // Instruct the not-same-top-level-site worker to send a message on the BC + // channel we are listening on. This message should not be received since + // the worker should be in a different partition. + send(worker_1_uuid, + bc_postmessage_js(channel_name, "worker1 msg", response_queue_uuid)); + if (await receive(response_queue_uuid) != "done") { + reject("Unable to trigger worker1 BroadcastChannel message creation"); + } + + // Now instruct the same-top-level-site worker to send a BC message. By + // the time we send the script to execute, have it send the BC message, + // and then receive the BC message in our BC instance, it should be + // reasonable to assume that the message from the first worker would have + // been delivered if it was going to be. + send(worker_2_uuid, + bc_postmessage_js(channel_name, "worker2 msg", response_queue_uuid)); + if (await receive(response_queue_uuid) != "done") { + reject("Unable to trigger worker2 BroadcastChannel message creation"); + } + + }).then(event => { + assert_equals(event.data, "worker2 msg"); + }); + +}, "BroadcastChannel messages aren't received from a cross-partition shared worker"); + +const newServiceWorker = async (origin) => { + const worker_token = token(); + const worker_url = origin + executor_service_worker_path + + `&uuid=${worker_token}`; + const worker_url_path = executor_service_worker_path.substring(0, + executor_service_worker_path.lastIndexOf('/')); + const scope = worker_url_path + "/not-used/"; + const reg = await navigator.serviceWorker.register(worker_url, + {'scope': scope}); + return worker_token; +} + +promise_test(t => { + return new Promise(async (resolve, reject) => { + const response_queue_uuid = token(); + + const create_worker_js = (origin) => ` + const importScript = ${importScript}; + await importScript("/html/cross-origin-embedder-policy/credentialless" + + "/resources/common.js"); + await importScript("/html/anonymous-iframe/resources/common.js"); + await importScript("/common/utils.js"); + const newServiceWorker = ${newServiceWorker}; + send("${response_queue_uuid}", await newServiceWorker("${origin}")); + `; + + const [iframe_1_uuid, iframe_2_uuid] = + await create_test_iframes(t, response_queue_uuid); + + // Create a service worker in the cross-top-level-site iframe. + send(iframe_1_uuid, create_worker_js(same_site_origin)); + var worker_1_uuid = await receive(response_queue_uuid); + + const channel_name = token(); + const bc = new BroadcastChannel(channel_name); + bc.onmessage = async (e) => { + if (worker_1_uuid) { + await send(worker_1_uuid, "self.registration.unregister();"); + } + if (worker_2_uuid) { + await send(worker_2_uuid, "self.registration.unregister();"); + } + resolve(e); + }; + + // Instruct the not-same-top-level-site worker to send a message on the BC + // channel we are listening on. This message should not be received since + // the worker should be in a different partition. + send(worker_1_uuid, + bc_postmessage_js(channel_name, "worker1 msg", response_queue_uuid)); + if (await receive(response_queue_uuid) != "done") { + reject("Unable to trigger worker1 BroadcastChannel message creation"); + } + + await send(worker_1_uuid, "await self.registration.unregister();"); + worker_1_uuid = undefined; + + // Create a service worker in the same-top-level-site iframe. Note that + // if service workers are unpartitioned then this new service worker would + // replace the one created above. This is why we wait to create the second + // service worker until after we are done with the first one. + send(iframe_2_uuid, create_worker_js(same_site_origin)); + var worker_2_uuid = await receive(response_queue_uuid); + + // Now instruct the same-top-level-site worker to send a BC message. By + // the time we send the script to execute, have it send the BC message, + // and then receive the BC message in our BC instance, it should be + // reasonable to assume that the message from the first worker would have + // been delivered if it was going to be. + send(worker_2_uuid, + bc_postmessage_js(channel_name, "worker2 msg", response_queue_uuid)); + if (await receive(response_queue_uuid) != "done") { + reject("Unable to trigger worker2 BroadcastChannel message creation"); + } + + await send(worker_2_uuid, "await self.registration.unregister();"); + worker_2_uuid = undefined; + + }).then(event => { + assert_equals(event.data, "worker2 msg"); + }); + +}, "BroadcastChannel messages aren't received from a cross-partition service worker"); + +</script> +</body> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/detached-iframe.html b/testing/web-platform/tests/webmessaging/broadcastchannel/detached-iframe.html new file mode 100644 index 0000000000..b9b06c3a46 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/detached-iframe.html @@ -0,0 +1,174 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<!-- Pull in the with_iframe helper function from the service worker tests --> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<body> +<script> +const TEST_IFRAME_CLASS_NAME = 'test-iframe'; +const events = []; +var bc1; +const DONE_MSG = 'done'; + +function DetachedIframeTestCheckForOneMessage(t) { + return new Promise((resolve) => { + bc1.onmessage = t.step_func(e => { + events.push(e); + if (e.data == DONE_MSG) { + assert_equals(events.length, 1); + resolve(); + } + }); + }); +} + +const IframeAction = { + REMOVE_BEFORE_CREATION: 'remove-before-creation', + REMOVE_AFTER_CREATION: 'remove-after-creation', +}; + +async function doMessageSentTest(t, channelName, action) { + await with_iframe('about:blank'); + const iframe = document.getElementsByClassName(TEST_IFRAME_CLASS_NAME)[0]; + const iframe_BroadcastChannel = iframe.contentWindow.BroadcastChannel; + + if (action === IframeAction.REMOVE_BEFORE_CREATION) { + iframe.remove(); + } + + events.length = 0; + + bc1 = new BroadcastChannel(channelName); + const bc2 = new BroadcastChannel(channelName); + const iframe_bc = new iframe_BroadcastChannel(channelName); + + if (action === IframeAction.REMOVE_AFTER_CREATION) { + iframe.remove(); + } + + const testResultsPromise = DetachedIframeTestCheckForOneMessage(t); + + iframe_bc.postMessage('test'); + bc2.postMessage(DONE_MSG); + + bc2.close(); + iframe_bc.close(); + t.add_cleanup(() => bc1.close()); + + return testResultsPromise; +} + +promise_test(async t => { + return doMessageSentTest( + t, 'postMessage-from-detached-iframe-pre', + IframeAction.REMOVE_AFTER_CREATION); +}, 'BroadcastChannel messages from detached iframe to parent should be ignored (BC created before detaching)'); + +promise_test(async t => { + return doMessageSentTest( + t, 'postMessage-from-detached-iframe-post', + IframeAction.REMOVE_BEFORE_CREATION); +}, 'BroadcastChannel messages from detached iframe to parent should be ignored (BC created after detaching)'); + + +async function doMessageReceivedTest(t, channelName, action) { + await with_iframe('about:blank'); + const iframe = document.getElementsByClassName(TEST_IFRAME_CLASS_NAME)[0]; + const iframe_BroadcastChannel = iframe.contentWindow.BroadcastChannel; + + if (action === IframeAction.REMOVE_BEFORE_CREATION) { + iframe.remove(); + } + + events.length = 0; + + // `iframe_bc` must be created first so that it receives messages before + // `bc1`. That way we can tell whether `iframe_bc` received a message by + // inspecting `events` in the `bc1` message handler. + const iframe_bc = new iframe_BroadcastChannel(channelName); + iframe_bc.onmessage = e => { + events.push(e) + }; + bc1 = new BroadcastChannel(channelName); + const bc2 = new BroadcastChannel(channelName); + + if (action === IframeAction.REMOVE_AFTER_CREATION) { + iframe.remove(); + } + + const testResultsPromise = DetachedIframeTestCheckForOneMessage(t); + bc2.postMessage(DONE_MSG); + + bc2.close(); + iframe_bc.close(); + t.add_cleanup(() => bc1.close()); +} + +promise_test(async t => { + return doMessageReceivedTest( + t, 'postMessage-to-detached-iframe-pre', + IframeAction.REMOVE_AFTER_CREATION); +}, 'BroadcastChannel messages from parent to detached iframe should be ignored (BC created before detaching)'); + +promise_test(async t => { + return doMessageReceivedTest( + t, 'postMessage-to-detached-iframe-post', + IframeAction.REMOVE_BEFORE_CREATION); +}, 'BroadcastChannel messages from parent to detached iframe should be ignored (BC created after detaching)'); + + +async function doMessageSendReceiveTest(t, channelName, action) { + await with_iframe('about:blank'); + const iframe = document.getElementsByClassName(TEST_IFRAME_CLASS_NAME)[0]; + const iframe_BroadcastChannel = iframe.contentWindow.BroadcastChannel; + + if (action === IframeAction.REMOVE_BEFORE_CREATION) { + iframe.remove(); + } + + const iframe_bc1 = new iframe_BroadcastChannel(channelName); + const iframe_bc2 = new iframe_BroadcastChannel(channelName); + iframe_bc1.onmessage = t.unreached_func( + 'Detached iframe BroadcastChannel instance received message unexpectedly'); + + if (action === IframeAction.REMOVE_AFTER_CREATION) { + iframe.remove(); + } + + iframe_bc2.postMessage(DONE_MSG); + + iframe_bc2.close(); + t.add_cleanup(() => iframe_bc1.close()); + + // To avoid calling t.step_timeout here, instead just create two new + // BroadcastChannel instances and complete the test when a message is passed + // between them. Per the spec, all "BroadcastChannel objects whose relevant + // agents are the same" must have messages delivered to them in creation + // order, so if we get this message then it's safe to assume the earlier + // message would have been delivered if it was going to be. + const bc1 = new BroadcastChannel(channelName); + const bc2 = new BroadcastChannel(channelName); + return new Promise((resolve) => { + bc1.onmessage = t.step_func(e => { + resolve(); + }); + bc2.postMessage(DONE_MSG); + }); +} + +promise_test(async t => { + return doMessageSendReceiveTest( + t, 'postMessage-within-detached-iframe-pre', + IframeAction.REMOVE_AFTER_CREATION); +}, 'BroadcastChannel messages within detached iframe should be ignored (BCs created before detaching)'); + +promise_test(async t => { + return doMessageSendReceiveTest( + t, 'postMessage-within-detached-iframe-post', + IframeAction.REMOVE_BEFORE_CREATION); +}, 'BroadcastChannel messages within detached iframe should be ignored (BCs created after detaching)'); + +</script> +</body> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/interface.any.js b/testing/web-platform/tests/webmessaging/broadcastchannel/interface.any.js new file mode 100644 index 0000000000..35e09d34b4 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/interface.any.js @@ -0,0 +1,65 @@ +test(() => assert_throws_js(TypeError, () => new BroadcastChannel()), + 'Should throw if no name is provided'); + +test(() => { + let c = new BroadcastChannel(null); + assert_equals(c.name, 'null'); + }, 'Null name should not throw'); + +test(() => { + let c = new BroadcastChannel(undefined); + assert_equals(c.name, 'undefined'); + }, 'Undefined name should not throw'); + +test(() => { + let c = new BroadcastChannel('fooBar'); + assert_equals(c.name, 'fooBar'); + }, 'Non-empty name should not throw'); + +test(() => { + let c = new BroadcastChannel(123); + assert_equals(c.name, '123'); + }, 'Non-string name should not throw'); + +test(() => { + let c = new BroadcastChannel(''); + assert_throws_js(TypeError, () => c.postMessage()); + }, 'postMessage without parameters should throw'); + +test(() => { + let c = new BroadcastChannel(''); + c.postMessage(null); + }, 'postMessage with null should not throw'); + +test(() => { + let c = new BroadcastChannel(''); + c.close(); + }, 'close should not throw'); + +test(() => { + let c = new BroadcastChannel(''); + c.close(); + c.close(); + }, 'close should not throw when called multiple times'); + +test(() => { + let c = new BroadcastChannel(''); + c.close(); + assert_throws_dom('InvalidStateError', () => c.postMessage('')); + }, 'postMessage after close should throw'); + +test(() => { + let c = new BroadcastChannel(''); + assert_not_equals(c.onmessage, undefined); + }, 'BroadcastChannel should have an onmessage event'); + +test(() => { + let c = new BroadcastChannel(''); + assert_throws_dom('DataCloneError', () => c.postMessage(Symbol())); + }, 'postMessage should throw with uncloneable data'); + +test(() => { + let c = new BroadcastChannel(''); + c.close(); + assert_throws_dom('InvalidStateError', () => c.postMessage(Symbol())); + }, 'postMessage should throw InvalidStateError after close, even with uncloneable data'); diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/opaque-origin.html b/testing/web-platform/tests/webmessaging/broadcastchannel/opaque-origin.html new file mode 100644 index 0000000000..e09d935244 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/opaque-origin.html @@ -0,0 +1,193 @@ +<!doctype html> +<html> +<head> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<title></title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +</head> +<body> +<script> +<!-- +promise_test(t => { + return new Promise((resolve) => { + let ifr = document.createElement("iframe"); + ifr.src = + "data:text/html,<script> let bc = new BroadcastChannel(\"test\");" + + "bc.onmessage = (e) => {" + + " if (e.data == \"ping\") bc.postMessage('pong');"+ + " else parent.postMessage({workerMessageOrigin: e.data, messageOrigin: e.origin}, \"*\"); };" + + "new Worker(URL.createObjectURL(new Blob([\"" + + "let bc2 = new BroadcastChannel('test'); bc2.postMessage('ping'); " + + "bc2.onmessage = e => bc2.postMessage(e.origin);" + + "\"], {type: 'text/javascript'}))); </script>"; + window.addEventListener("message", t.step_func(e => { + assert_equals(e.data.workerMessageOrigin, "null"); + assert_equals(e.data.messageOrigin, "null"); + resolve(); + }), {once: true}); + t.add_cleanup(() => { document.body.removeChild(ifr) }); + document.body.appendChild(ifr); + }); +}, "Opaque origin should be serialized to \"null\""); + + +const iframe_src = (channel_name, iframe_name) => `data:text/html,<script> +let bc2 = new BroadcastChannel("${channel_name}"); +bc2.onmessage = (e) => { + if (e.data == "from-${iframe_name}") { + parent.postMessage("${iframe_name}-done", "*"); + } else { + parent.postMessage("fail", "*"); + } +}; +let bc3 = new BroadcastChannel("${channel_name}"); +bc3.postMessage("from-${iframe_name}"); +</script>`; + +promise_test(t => { + return new Promise((resolve, reject) => { + const channel_name = "opaque-origin-test-2"; + const bc1 = new BroadcastChannel(channel_name); + bc1.onmessage = t.unreached_func("Received message from an opaque origin"); + + // We'll create an iframe and have it send a BroadcastChannel message + // between two instances. Once the message is received, it will postMessage + // back and we'll repeat this with another iframe. If the first + // BroadcastChannel message is received by `bc1`, or if the second + // BroadcastChannel message is received by `bc1` or `bc2` in the first + // iframe, then the test should fail. + + window.addEventListener("message", e => { + if(e.data == "iframe1-done") { + let iframe2 = document.createElement("iframe"); + iframe2.src = iframe_src(channel_name, "iframe2"); + t.add_cleanup(() => { document.body.removeChild(iframe2) }); + document.body.appendChild(iframe2); + } else if(e.data == "iframe2-done") { + resolve(); + } else if(e.data == "fail") { + reject("One opaque origin received a message from the other"); + } else { + reject("An unexpected error occurred"); + } + }); + + let iframe1 = document.createElement("iframe"); + iframe1.src = iframe_src(channel_name, "iframe1"); + t.add_cleanup(() => { document.body.removeChild(iframe1) }); + document.body.appendChild(iframe1); + }); +}, "BroadcastChannel messages from opaque origins should be self-contained"); + +const data_url_worker_src = (channel_name, worker_name) => { + const source = ` +const handler = (reply) => { + let bc2 = new BroadcastChannel("${channel_name}"); + bc2.onmessage = (e) => { + if (e.data == "from-${worker_name}") { + reply("${worker_name}-done"); + } else { + reply("fail"); + } + }; + let bc3 = new BroadcastChannel("${channel_name}"); + bc3.postMessage("from-${worker_name}"); +}; +// For dedicated workers: +self.addEventListener("message", () => handler(self.postMessage)); +// For shared workers: +self.addEventListener("connect", (e) => { + var port = e.ports[0]; + port.onmessage = () => handler(msg => port.postMessage(msg)); + port.start(); + +}); +`; + return "data:,".concat(encodeURIComponent(source)); +} + +promise_test(t => { + return new Promise((resolve, reject) => { + const channel_name = "opaque-origin-test-3"; + const bc1 = new BroadcastChannel(channel_name); + bc1.onmessage = e => { reject("Received message from an opaque origin"); }; + + // Same as the previous test but with data URL dedicated workers (which + // should have opaque origins per the HTML spec). + const worker_name_prefix = "data-url-dedicated-worker"; + const worker_1_name = `${worker_name_prefix}-1`; + const worker_2_name = `${worker_name_prefix}-2`; + + const handler = e => { + if(e.data == `${worker_1_name}-done`) { + const worker2 = new Worker(data_url_worker_src(channel_name, worker_2_name)); + t.add_cleanup(() => worker2.terminate()); + worker2.addEventListener("message", handler); + worker2.postMessage("go!"); + } else if(e.data == `${worker_2_name}-done`) { + resolve(); + } else if(e.data == "fail") { + reject("One opaque origin received a message from the other"); + } else { + reject("An unexpected error occurred"); + } + }; + + let worker1 = new Worker(data_url_worker_src(channel_name, worker_1_name)); + t.add_cleanup(() => worker1.terminate()); + worker1.addEventListener("message", handler); + worker1.postMessage("go!"); + }); +}, "BroadcastChannel messages from data URL dedicated workers should be self-contained"); + +promise_test(() => { + return new Promise((resolve, reject) => { + const channel_name = "opaque-origin-test-4"; + const bc1 = new BroadcastChannel(channel_name); + + // Same as the previous test but with data URL shared workers (which + // should have opaque origins per the HTML spec). + const worker_name_prefix = "data-url-shared-worker"; + const worker_1_name = `${worker_name_prefix}-1`; + const worker_2_name = `${worker_name_prefix}-2`; + + const handler = e => { + if (e.data == `${worker_1_name}-done`) { + const worker_script = data_url_worker_src(channel_name, worker_2_name); + const worker2 = new SharedWorker(worker_script, worker_2_name); + worker2.port.addEventListener("message", handler); + worker2.port.start(); + worker2.port.postMessage("go!"); + } else if(e.data == `${worker_2_name}-done`) { + resolve(); + } else if(e.data == "fail") { + reject("One opaque origin received a message from the other"); + } else { + reject("An unexpected error occurred"); + } + }; + + bc1.onmessage = e => { + if (e.data == "go!") { + const worker_script = data_url_worker_src(channel_name, worker_1_name); + const worker1 = new SharedWorker(worker_script, worker_1_name); + worker1.port.addEventListener("message", handler); + worker1.port.start(); + worker1.port.postMessage("go!"); + } else { + reject("Received message from an opaque origin"); + } + }; + + // Ensure that the BroadcastChannel instance above can receive messages + // before we create the first shared worker. + const bc2 = new BroadcastChannel(channel_name); + bc2.postMessage("go!"); + }); +}, "BroadcastChannel messages from data URL shared workers should be self-contained"); +//--> +</script> +</body> +</html> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/ordering.html b/testing/web-platform/tests/webmessaging/broadcastchannel/ordering.html new file mode 100644 index 0000000000..2d521b9e0c --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/ordering.html @@ -0,0 +1,116 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<!-- Pull in the with_iframe helper function from the service worker tests --> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<body> +<script> + +const BC0_FIRST_MSG = 'from BC0 - first'; +const BC1_FIRST_MSG = 'from BC1 - first'; +const BC2_FIRST_MSG = 'from BC2 - first'; +const BC3_FIRST_MSG = 'from BC3 - first'; +const BC0_SECOND_MSG = 'from BC0 - second'; +const BC1_SECOND_MSG = 'from BC1 - second'; +const BC2_SECOND_MSG = 'from BC2 - second'; +const BC3_SECOND_MSG = 'done'; +const BC0_TARGET_NAME = 'BC1'; +const BC1_TARGET_NAME = 'BC1'; +const BC2_TARGET_NAME = 'BC2'; +const BC3_TARGET_NAME = 'BC3'; +const MULTI_FRAME_ORDERING_TEST_CHANNEL_NAME = 'multi-frame-order'; + +const bc0 = new BroadcastChannel(MULTI_FRAME_ORDERING_TEST_CHANNEL_NAME); +const messages = []; + +function logReceivedMessage(targetname, e) { + messages.push({'target': targetname, 'data': e.data}); +} + +function postMessagesToChannel() { + return new Promise((resolve) => { + bc0.postMessage(BC0_FIRST_MSG); + bc0.postMessage(BC0_SECOND_MSG); + resolve(); + }); +} + +// Expected flow of messages between the BroadcastChannel objects (based on +// the requirement that messages get delivered to BroadcastChannel objects +// "in creation order, oldest first") and comments describing the actions +// taken in response to each event +const EXPECTED_RESULTS = [ + // -> BC0 sends two messages, BC1 and BC2 are connected to the channel + + {'data': BC0_FIRST_MSG, 'target': BC1_TARGET_NAME}, + // -> BC1 Creates BC3 and sends first message + + {'data': BC0_FIRST_MSG, 'target': BC2_TARGET_NAME}, + // -> BC2 sends two messages + + // BC3 isn't expected to receive the messages sent before it was created, so + // no corresponding entries here for messages from BC0. + + {'data': BC0_SECOND_MSG, 'target': BC1_TARGET_NAME}, + // -> BC1 sends second message + + {'data': BC0_SECOND_MSG, 'target': BC2_TARGET_NAME}, + // -> BC2 closes + + {'data': BC1_FIRST_MSG, 'target': BC0_TARGET_NAME}, + + {'data': BC1_FIRST_MSG, 'target': BC3_TARGET_NAME}, + // -> BC3 sends first message + + {'data': BC2_FIRST_MSG, 'target': BC0_TARGET_NAME}, + + {'data': BC2_FIRST_MSG, 'target': BC1_TARGET_NAME}, + // -> BC1 closes + + {'data': BC2_FIRST_MSG, 'target': BC3_TARGET_NAME}, + // -> BC3 sends second message + + {'data': BC2_SECOND_MSG, 'target': BC0_TARGET_NAME}, + + {'data': BC2_SECOND_MSG, 'target': BC3_TARGET_NAME}, + // -> BC3 closes + + {'data': BC1_SECOND_MSG, 'target': BC0_TARGET_NAME}, + + {'data': BC3_FIRST_MSG, 'target': BC0_TARGET_NAME}, + + {'data': BC3_SECOND_MSG, 'target': BC0_TARGET_NAME}, +]; + +function testCompletion(t) { + return new Promise((resolve) => { + bc0.onmessage = t.step_func(e => { + logReceivedMessage(BC0_TARGET_NAME, e); + if (e.data == BC3_SECOND_MSG) { + assert_equals(messages.length, EXPECTED_RESULTS.length); + for(var i = 0; i < messages.length; i++) { + assert_equals(messages[i].target, EXPECTED_RESULTS[i].target, `Message ${i+1} has unexpected target`); + assert_equals(messages[i].data, EXPECTED_RESULTS[i].data, `Message ${i+1} has unexpected message contents`); + } + resolve(); + } + }); + }); +} + +promise_test(async t => { + + const testResults = testCompletion(t); + // Await them sequentially because we need the BroadcastChannel object in + // iframe1 to be created first, we need the BroadcastChannel object in + // iframe2 to be created second, and then we only want to call + // postMessagesToChannel once both BroadcastChannels have been created. + await with_iframe('resources/ordering.html?id=iframe1'); + await with_iframe('resources/ordering.html?id=iframe2'); + await postMessagesToChannel(); + return testResults; +}, "Messages are delivered in port creation order across multiple frames"); + +</script> +</body> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/origin.window.js b/testing/web-platform/tests/webmessaging/broadcastchannel/origin.window.js new file mode 100644 index 0000000000..7e9d602af1 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/origin.window.js @@ -0,0 +1,10 @@ +async_test(t => { + const crossOriginURL = new URL("resources/origin.html", self.location.href).href.replace("://", "://天気の良い日."), + frame = document.createElement("iframe"); + frame.src = crossOriginURL; + document.body.appendChild(frame); + t.add_cleanup(() => frame.remove()); + self.onmessage = t.step_func_done(e => { + assert_equals(e.data, self.origin.replace("://", "://xn--n8j6ds53lwwkrqhv28a.")); + }); +}, "Serialization of BroadcastChannel origin"); diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/resources/cross-origin.html b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/cross-origin.html new file mode 100644 index 0000000000..5078b6fc8e --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/cross-origin.html @@ -0,0 +1,15 @@ +<body></body> +<script> + window.onload = function() { + bc1 = new BroadcastChannel('no-cross-origin-messages'); + bc2 = new BroadcastChannel('no-cross-origin-messages'); + bc2.onmessage = e => { + parent.postMessage('done', "*"); + }; + // Post a message on bc1 and once we receive it in bc2, we know that the + // message should have been sent to bc0 if messages were being passed + // across origin (assuming compliance with the spec regarding message + // delivery in port creation order). + bc1.postMessage('ignition'); + } +</script> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/resources/ordering.html b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/ordering.html new file mode 100644 index 0000000000..b7f12d865a --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/ordering.html @@ -0,0 +1,78 @@ +<body></body> +<script> + const BC0_FIRST_MSG = 'from BC0 - first'; + const BC1_FIRST_MSG = 'from BC1 - first'; + const BC2_FIRST_MSG = 'from BC2 - first'; + const BC3_FIRST_MSG = 'from BC3 - first'; + const BC0_SECOND_MSG = 'from BC0 - second'; + const BC1_SECOND_MSG = 'from BC1 - second'; + const BC2_SECOND_MSG = 'from BC2 - second'; + const BC3_SECOND_MSG = 'done'; + const BC0_TARGET_NAME = 'BC1'; + const BC1_TARGET_NAME = 'BC1'; + const BC2_TARGET_NAME = 'BC2'; + const BC3_TARGET_NAME = 'BC3'; + const MULTI_FRAME_ORDERING_TEST_CHANNEL_NAME = 'multi-frame-order'; + + var bc1, bc2, bc3; + var sentMessageCountForBc1 = 0; + var sentMessageCountForBc2 = 0; + var sentMessageCountForBc3 = 0; + + var bc1_handler = e => { + window.top.logReceivedMessage(BC1_TARGET_NAME, e); + switch(sentMessageCountForBc1) { + case 0: + bc3 = new BroadcastChannel(MULTI_FRAME_ORDERING_TEST_CHANNEL_NAME); + bc3.onmessage = bc3_handler; + bc1.postMessage(BC1_FIRST_MSG); + break; + case 1: + bc1.postMessage(BC1_SECOND_MSG); + break; + case 2: + bc1.close(); + return; + } + sentMessageCountForBc1 += 1; + } + var bc2_handler = e => { + window.top.logReceivedMessage(BC2_TARGET_NAME, e); + switch(sentMessageCountForBc2) { + case 0: + bc2.postMessage(BC2_FIRST_MSG); + bc2.postMessage(BC2_SECOND_MSG); + sentMessageCountForBc2 += 2; + break; + case 2: + bc2.close(); + return; + } + }; + var bc3_handler = e => { + window.top.logReceivedMessage(BC3_TARGET_NAME, e); + switch(sentMessageCountForBc3) { + case 0: + bc3.postMessage(BC3_FIRST_MSG); + break; + case 1: + bc3.postMessage(BC3_SECOND_MSG); + break; + case 2: + bc3.close(); + return; + } + sentMessageCountForBc3 += 1; + }; + + window.onload = function() { + const params = new URLSearchParams(window.location.search); + if (params.get('id') === 'iframe1') { + bc1 = new BroadcastChannel(MULTI_FRAME_ORDERING_TEST_CHANNEL_NAME); + bc1.onmessage = bc1_handler; + } else if (params.get('id') === 'iframe2') { + bc2 = new BroadcastChannel(MULTI_FRAME_ORDERING_TEST_CHANNEL_NAME); + bc2.onmessage = bc2_handler; + } + } +</script> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/resources/origin.html b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/origin.html new file mode 100644 index 0000000000..f57d582bbb --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/origin.html @@ -0,0 +1,8 @@ +<script> +const bc1 = new BroadcastChannel("ladila"), + bc2 = new BroadcastChannel("ladila"); +bc2.onmessage = e => { + parent.postMessage(e.origin, "*"); +} +bc1.postMessage("does not matter"); +</script> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/resources/sandboxed.html b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/sandboxed.html new file mode 100644 index 0000000000..e32962cdfd --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/sandboxed.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<script> +try { + let c = new BroadcastChannel('foo'); + parent.postMessage('Created', '*'); +} catch (e) { + parent.postMessage('Exception: ' + e.name, '*'); +} +</script> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/resources/service-worker.js b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/service-worker.js new file mode 100644 index 0000000000..a3d17b9c65 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/service-worker.js @@ -0,0 +1,15 @@ +let promise_func = null; +let promise = new Promise(resolve => promise_func = resolve); + +const SERVICE_WORKER_TEST_CHANNEL_NAME = 'service worker'; +const bc3 = new BroadcastChannel(SERVICE_WORKER_TEST_CHANNEL_NAME); +bc3.onmessage = e => { + bc3.postMessage('done'); + promise_func(); +}; +bc3.postMessage('from worker'); + +// Ensure that the worker stays alive for the duration of the test +self.addEventListener('install', evt => { + evt.waitUntil(promise); +}); diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/resources/worker.js b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/worker.js new file mode 100644 index 0000000000..ee2d51a254 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/resources/worker.js @@ -0,0 +1,37 @@ +importScripts("/common/gc.js"); + +var c; + +async function handler(e, reply) { + if (e.data.ping) { + c.postMessage(e.data.ping); + return; + } + if (e.data.blob) { + (() => { + c.postMessage({blob: new Blob(e.data.blob)}); + })(); + await garbageCollect(); + } + c = new BroadcastChannel(e.data.channel); + let messages = []; + c.onmessage = e => { + if (e.data === 'ready') { + // Ignore any 'ready' messages from the other thread since there could + // be some race conditions between this BroadcastChannel instance + // being created / ready to receive messages and the message being sent. + return; + } + messages.push(e.data); + if (e.data == 'done') + reply(messages); + }; + c.postMessage('from worker'); +} + +onmessage = e => handler(e, postMessage); + +onconnect = e => { + let port = e.ports[0]; + port.onmessage = e => handler(e, msg => port.postMessage(msg)); +}; diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/sandbox.html b/testing/web-platform/tests/webmessaging/broadcastchannel/sandbox.html new file mode 100644 index 0000000000..aedf3c0d6f --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/sandbox.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Creating BroadcastChannel in an opaque origin</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +async_test(t => { + self.onmessage = t.step_func(e => { + assert_equals(e.data, 'Created'); + t.done(); + }); + }); +</script> +<iframe sandbox="allow-scripts" src="resources/sandboxed.html"></iframe> +</body> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/service-worker.https.html b/testing/web-platform/tests/webmessaging/broadcastchannel/service-worker.https.html new file mode 100644 index 0000000000..d605434ae1 --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/service-worker.https.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +const SERVICE_WORKER_TEST_CHANNEL_NAME = 'service worker'; +const events = []; +const c1 = new BroadcastChannel(SERVICE_WORKER_TEST_CHANNEL_NAME); +const c2 = new BroadcastChannel(SERVICE_WORKER_TEST_CHANNEL_NAME); +c1.onmessage = e => events.push(e); +c2.onmessage = e => events.push(e); + +function testCompletion(t) { + return new Promise((resolve) => { + c2.addEventListener("message", t.step_func(e => { + if (e.data == 'from worker') { + c2.postMessage('from c2'); + } else if (e.data == 'done') { + assert_equals(events.length, 5); + assert_equals(events[0].data, 'from worker'); + assert_equals(events[0].target, c1); + assert_equals(events[1].data, 'from worker'); + assert_equals(events[1].target, c2); + assert_equals(events[2].data, 'from c2'); + assert_equals(events[3].data, 'done'); + assert_equals(events[3].target, c1); + assert_equals(events[4].data, 'done'); + assert_equals(events[4].target, c2); + resolve(); + } + })); + }); +} + +promise_test(async t => { + + const testResults = testCompletion(t); + const SCRIPT = "resources/service-worker.js"; + const SCOPE = "/webmessaging/broadcastchannel/resources/not-used/"; + + const reg = await navigator.serviceWorker.register(SCRIPT, {'scope': SCOPE}); + t.add_cleanup(() => reg.unregister()); + + return testResults; + }, 'BroadcastChannel works in service workers'); +</script> diff --git a/testing/web-platform/tests/webmessaging/broadcastchannel/workers.html b/testing/web-platform/tests/webmessaging/broadcastchannel/workers.html new file mode 100644 index 0000000000..8b55492f3c --- /dev/null +++ b/testing/web-platform/tests/webmessaging/broadcastchannel/workers.html @@ -0,0 +1,375 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +async_test(t => { + let c1 = new BroadcastChannel('worker'); + let c2 = new BroadcastChannel('worker'); + let events = []; + + c1.onmessage = e => events.push(e); + c2.onmessage = e => events.push(e); + + let doneCount = 0; + c2.addEventListener('message', t.step_func(e => { + if (e.data == 'from worker') { + c2.postMessage('from c2'); + c1.postMessage('done'); + } else if (e.data == 'done') { + assert_equals(events.length, 4); + assert_equals(events[0].data, 'from worker'); + assert_equals(events[0].target, c1); + assert_equals(events[1].data, 'from worker'); + assert_equals(events[1].target, c2); + assert_equals(events[2].data, 'from c2'); + assert_equals(events[3].data, 'done'); + if (++doneCount == 2) t.done(); + } + })); + + let worker = new Worker('resources/worker.js'); + worker.onmessage = t.step_func(e => { + assert_array_equals(e.data, ['from c2', 'done']); + if (++doneCount == 2) t.done(); + }); + worker.postMessage({channel: 'worker'}); + + }, 'BroadcastChannel works in workers'); + +async_test(t => { + let c1 = new BroadcastChannel('shared worker'); + let c2 = new BroadcastChannel('shared worker'); + let events = []; + + c1.onmessage = e => events.push(e); + c2.onmessage = e => events.push(e); + + let doneCount = 0; + c2.addEventListener('message', t.step_func(e => { + if (e.data == 'from worker') { + c2.postMessage('from c2'); + c1.postMessage('done'); + } else if (e.data == 'done') { + assert_equals(events.length, 4); + assert_equals(events[0].data, 'from worker'); + assert_equals(events[0].target, c1); + assert_equals(events[1].data, 'from worker'); + assert_equals(events[1].target, c2); + assert_equals(events[2].data, 'from c2'); + assert_equals(events[3].data, 'done'); + if (++doneCount == 2) t.done(); + } + })); + + let worker = new SharedWorker('resources/worker.js'); + worker.port.onmessage = t.step_func(e => { + assert_array_equals(e.data, ['from c2', 'done']); + if (++doneCount == 2) t.done(); + }); + worker.port.postMessage({channel: 'shared worker'}); + + }, 'BroadcastChannel works in shared workers'); + +async_test(t => { + let c = new BroadcastChannel('worker-close'); + let events = []; + + c.onmessage = e => events.push('c1: ' + e.data); + + let worker = new Worker('resources/worker.js'); + worker.onmessage = t.step_func(e => { + assert_array_equals(events, + ['c1: from worker', 'c2: ready', 'c2: echo'], + 'messages in document'); + assert_array_equals(e.data, ['done'], 'messages in worker'); + t.done(); + }); + worker.onmessagerror = + t.unreached_func('Worker\'s onmessageerror handler called'); + + c.addEventListener('message', e => { + if (e.data == 'from worker') { + c.close(); + if (self.gc) self.gc(); + window.setTimeout(() => { + let c2 = new BroadcastChannel('worker-close'); + c2.onmessage = e => { + events.push('c2: ' + e.data); + if (e.data === 'ready') { + worker.postMessage({ping: 'echo'}); + } else { + c2.postMessage('done'); + c2.close(); + } + }; + // For some implementations there may be a race condition between + // when the BroadcastChannel instance above is created / ready to + // receive messages and when the worker calls postMessage on it's + // BroadcastChannel instance. To avoid this, confirm that our + // instance can receive a message before indicating to the other + // thread that we are ready. For more details, see: + // https://github.com/whatwg/html/issues/7267 + let c3 = new BroadcastChannel('worker-close'); + c3.postMessage('ready'); + c3.close(); + }, 1); + } + }); + + worker.postMessage({channel: 'worker-close'}); + t.add_cleanup(() => worker.terminate()); + + }, 'Closing and re-opening a channel works.'); + +async_test(t => { + function workerCode() { + close(); + try { + var bc = new BroadcastChannel('worker-create-after-close'); + } catch (e) { + postMessage(e); + return; + } + postMessage(true); + } + + var workerBlob = new Blob( + [workerCode.toString() + ';workerCode();'], + {type: 'application/javascript'}); + + var w = new Worker(URL.createObjectURL(workerBlob)); + w.onmessage = t.step_func_done(function(e) { + assert_equals( + e.data, true, + 'BroadcastChannel creation in closed worker triggered exception: ' + + e.data.message); + }); + t.add_cleanup(() => w.terminate()); +}, 'BroadcastChannel created after a worker self.close()'); + + +function postMessageFromWorkerWorkerCode(workerName, channelName) { + if (workerName === 'close-before-create-worker') { + close(); + } + let bc = new BroadcastChannel(channelName); + if (workerName === 'close-after-create-worker') { + close(); + } + bc.postMessage(workerName + ' done'); + postMessage(true); +} + +function doPostMessageFromWorkerTest(t, workerName, channelName) { + var bc = new BroadcastChannel(channelName); + bc.onmessage = t.step_func_done(function(e) { + assert_equals( + e.data, 'done-worker done', + 'BroadcastChannel message should only be received from the second worker'); + }); + t.add_cleanup(() => bc.close()); + + var testMessageHandler = t.step_func(function(e) { + assert_equals( + e.data, true, + 'Worker sent postMessage indicating it sent a BroadcastChannel message'); + + var w = createWorker( + postMessageFromWorkerWorkerCode, 'done-worker', channelName); + t.add_cleanup(() => w.terminate()); + }); + createWorker( + postMessageFromWorkerWorkerCode, workerName, channelName, + testMessageHandler); + + // To avoid calling t.step_timeout here, have the worker postMessage(true) + // once it is finished and then we'll instantiate another worker that + // performs the same test steps but doesn't close. By the time the + // BroadcastChannel message in that worker gets sent successfully it should + // be safe to assume that any BroadcastChannel messages from the previous + // worker would have been sent if they were going to be. +} + +function createWorker(workerCode, workerName, channelName, handler = null) { + var workerCodeStr = workerCode.toString() + + `;${workerCode.name}("${workerName}", "${channelName}");`; + var workerBlob = new Blob([workerCodeStr], {type: 'application/javascript'}); + var w = new Worker(URL.createObjectURL(workerBlob)); + if (handler !== null) { + w.onmessage = handler; + } + return w; +} + +async_test(t => { + const workerName = 'close-after-create-worker'; + const channelName = workerName + '-postmessage-from-worker'; + doPostMessageFromWorkerTest(t, workerName, channelName); +}, 'BroadcastChannel messages from closed worker to parent should be ignored (BC created before closing)'); + +async_test(t => { + const workerName = 'close-before-create-worker'; + const channelName = workerName + '-postmessage-from-worker'; + doPostMessageFromWorkerTest(t, workerName, channelName); +}, 'BroadcastChannel messages from closed worker to parent should be ignored (BC created after closing)'); + + +function postMessageToWorkerWorkerCode(workerName, channelName) { + self.addEventListener('message', () => { + if (workerName === 'close-before-create-worker') { + close(); + } + try { + let bc1 = new BroadcastChannel(channelName); + bc1.onmessage = e => { + if (e.data === 'ready') { + postMessage(e.data); + } else if (e.data === 'test') { + postMessage(workerName + ' done'); + } + }; + bc1.onmessageerror = () => { + postMessage('onmessageerror called from worker BroadcastChannel'); + }; + if (workerName === 'close-after-create-worker') { + close(); + } + } catch (e) { + postMessage(e); + return; + } + + if (workerName === 'done-worker') { + // For some implementations there may be a race condition between when + // the BroadcastChannel instance above is created / ready to receive + // messages and when the parent calls postMessage on it's + // BroadcastChannel instance. To avoid this, confirm that our instance + // can receive a message before indicating to the other thread that we + // are ready. For more details, see: + // https://github.com/whatwg/html/issues/7267 + let bc2 = new BroadcastChannel(channelName); + bc2.postMessage('ready'); + bc2.close(); + } else { + // Since the worker has closed, it's not expected that the + // BroadcastChannel will receive messages (there's a separate test for + // that), so just indicate directly that it's ready to test receiving + // a message from the parent dispite the possibility of a race condition. + postMessage('ready'); + } + }); + self.addEventListener('messageerror', () => { + postMessage('onmessageerror called from worker'); + }); +} + +function doPostMessageToWorkerTest(t, workerName, channelName) { + var bc = new BroadcastChannel(channelName); + t.add_cleanup(() => bc.close()); + + var doneMessageHandler = t.step_func(function(e) { + if (e.data === 'ready') { + bc.postMessage('test'); + } else if (e.data === 'done-worker done') { + t.done(); + } else { + assert_unreached( + 'BroadcastChannel.postMessage triggered exception within second worker: ' + + e.data.message); + } + }); + var testMessageHandler = t.step_func(function(e) { + assert_equals( + e.data, 'ready', + 'Worker sent postMessage indicating its BroadcastChannel instance is ready'); + bc.postMessage('test'); + + var doneWorker = createWorker( + postMessageToWorkerWorkerCode, 'done-worker', channelName, + doneMessageHandler); + t.add_cleanup(() => { + doneWorker.terminate(); + }); + doneWorker.postMessage('start'); + }); + var testWorker = createWorker( + postMessageToWorkerWorkerCode, workerName, channelName, + testMessageHandler); + testWorker.postMessage('start'); +} + +async_test(t => { + const workerName = 'close-after-create-worker'; + const channelName = workerName + '-postmessage-to-worker'; + doPostMessageToWorkerTest(t, workerName, channelName); +}, 'BroadcastChannel messages from parent to closed worker should be ignored (BC created before closing)'); + +async_test(t => { + const workerName = 'close-before-create-worker'; + const channelName = workerName + '-postmessage-to-worker'; + doPostMessageToWorkerTest(t, workerName, channelName); +}, 'BroadcastChannel messages from parent to closed worker should be ignored (BC created after closing)'); + + +function postMessageWithinWorkerWorkerCode(workerName, channelName) { + if (workerName === 'close-before-create-worker') { + close(); + } + try { + let bc1 = new BroadcastChannel(channelName); + let bc2 = new BroadcastChannel(channelName); + bc1.onmessage = e => { + postMessage(workerName + ' done') + }; + if (workerName === 'close-after-create-worker') { + close(); + } + bc2.postMessage(true); + postMessage(true); + } catch (e) { + postMessage(e); + } +} + +function doPostMessageWithinWorkerTest(t, workerName, channelName) { + var doneMessageHandler = t.step_func(function(e) { + if (e.data === true) { + // Done worker has finished - no action needed + } else if (e.data === 'done-worker done') { + t.done(); + } else { + assert_unreached( + 'BroadcastChannel.postMessage triggered exception within second worker: ' + + e.data.message); + } + }); + var testMessageHandler = t.step_func(function(e) { + assert_equals( + e.data, true, + 'Worker indicated that the test procedures were executed successfully'); + + var w = createWorker( + postMessageWithinWorkerWorkerCode, 'done-worker', channelName, + doneMessageHandler); + t.add_cleanup(() => w.terminate()); + }); + createWorker( + postMessageWithinWorkerWorkerCode, workerName, channelName, + testMessageHandler); +} + +async_test(t => { + const workerName = 'close-after-create-worker'; + const channelName = workerName + '-postmessage-within-worker'; + doPostMessageWithinWorkerTest(t, workerName, channelName); +}, 'BroadcastChannel messages within closed worker should be ignored (BCs created before closing)'); + +async_test(t => { + const workerName = 'close-before-create-worker'; + const channelName = workerName + '-postmessage-within-worker'; + doPostMessageWithinWorkerTest(t, workerName, channelName); +}, 'BroadcastChannel messages within closed worker should be ignored (BCs created after closing)'); + +</script> |