tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

postmessage-to-client-message-queue.https.html (9118B)


      1 <!DOCTYPE html>
      2 <title>Service Worker: postMessage to Client (message queue)</title>
      3 <script src="/resources/testharness.js"></script>
      4 <script src="/resources/testharnessreport.js"></script>
      5 <script src="/common/get-host-info.sub.js"></script>
      6 <script src="resources/test-helpers.sub.js"></script>
      7 <script>
      8 // This function creates a message listener that captures all messages
      9 // sent to this window and matches them with corresponding requests.
     10 // This frees test code from having to use clunky constructs just to
     11 // avoid race conditions, since the relative order of message and
     12 // request arrival doesn't matter.
     13 function create_message_listener(t) {
     14    const listener = {
     15        messages: new Set(),
     16        requests: new Set(),
     17        waitFor: function(predicate) {
     18            for (const event of this.messages) {
     19                // If a message satisfying the predicate has already
     20                // arrived, it gets matched to this request.
     21                if (predicate(event)) {
     22                    this.messages.delete(event);
     23                    return Promise.resolve(event);
     24                }
     25            }
     26 
     27            // If no match was found, the request is stored and a
     28            // promise is returned.
     29            const request = { predicate };
     30            const promise = new Promise(resolve => request.resolve = resolve);
     31            this.requests.add(request);
     32            return promise;
     33        }
     34    };
     35    window.onmessage = t.step_func(event => {
     36        for (const request of listener.requests) {
     37            // If the new message matches a stored request's
     38            // predicate, the request's promise is resolved with this
     39            // message.
     40            if (request.predicate(event)) {
     41                listener.requests.delete(request);
     42                request.resolve(event);
     43                return;
     44            }
     45        };
     46 
     47        // No outstanding request for this message, store it in case
     48        // it's requested later.
     49        listener.messages.add(event);
     50    });
     51    return listener;
     52 }
     53 
     54 async function service_worker_register_and_activate(t, script, scope) {
     55    const registration = await service_worker_unregister_and_register(t, script, scope);
     56    t.add_cleanup(() => registration.unregister());
     57    const worker = registration.installing;
     58    await wait_for_state(t, worker, 'activated');
     59    return worker;
     60 }
     61 
     62 // Add an iframe (parent) whose document contains a nested iframe
     63 // (child), then set the child's src attribute to child_url and return
     64 // its Window (without waiting for it to finish loading).
     65 async function with_nested_iframes(t, child_url) {
     66    const parent = await with_iframe('resources/nested-iframe-parent.html?role=parent');
     67    t.add_cleanup(() => parent.remove());
     68    const child = parent.contentWindow.document.getElementById('child');
     69    child.setAttribute('src', child_url);
     70    return child.contentWindow;
     71 }
     72 
     73 // Returns a predicate matching a fetch message with the specified
     74 // key.
     75 function fetch_message(key) {
     76    return event => event.data.type === 'fetch' && event.data.key === key;
     77 }
     78 
     79 // Returns a predicate matching a ping message with the specified
     80 // payload.
     81 function ping_message(data) {
     82    return event => event.data.type === 'ping' && event.data.data === data;
     83 }
     84 
     85 // A client message queue test is a testharness.js test with some
     86 // additional setup:
     87 // 1. A listener (see create_message_listener)
     88 // 2. An active service worker
     89 // 3. Two nested iframes
     90 // 4. A state transition function that controls the order of events
     91 //    during the test
     92 function client_message_queue_test(url, test_function, description) {
     93    promise_test(async t => {
     94        t.listener = create_message_listener(t);
     95 
     96        const script = 'resources/stalling-service-worker.js';
     97        const scope = 'resources/';
     98        t.service_worker = await service_worker_register_and_activate(t, script, scope);
     99 
    100        // We create two nested iframes such that both are controlled by
    101        // the newly installed service worker.
    102        const child_url = url + '?role=child';
    103        t.frame = await with_nested_iframes(t, child_url);
    104 
    105        t.state_transition = async function(from, to, scripts) {
    106            // A state transition begins with the child's parser
    107            // fetching a script due to a <script> tag. The request
    108            // arrives at the service worker, which notifies the
    109            // parent, which in turn notifies the test. Note that the
    110            // event loop keeps spinning while the parser is waiting.
    111            const request = await this.listener.waitFor(fetch_message(to));
    112 
    113            // The test instructs the service worker to send two ping
    114            // messages through the Client interface: first to the
    115            // child, then to the parent.
    116            this.service_worker.postMessage(from);
    117 
    118            // When the parent receives the ping message, it forwards
    119            // it to the test. Assuming that messages to both child
    120            // and parent are mapped to the same task queue (this is
    121            // not [yet] required by the spec), receiving this message
    122            // guarantees that the child has already dispatched its
    123            // message if it was allowed to do so.
    124            await this.listener.waitFor(ping_message(from));
    125 
    126            // Finally, reply to the service worker's fetch
    127            // notification with the script it should use as the fetch
    128            // request's response. This is a defensive mechanism that
    129            // ensures the child's parser really is blocked until the
    130            // test is ready to continue.
    131            request.ports[0].postMessage([`state = '${to}';`].concat(scripts));
    132        };
    133 
    134        await test_function(t);
    135    }, description);
    136 }
    137 
    138 function client_message_queue_enable_test(
    139    install_script,
    140    start_script,
    141    earliest_dispatch,
    142    description)
    143 {
    144    function assert_state_less_than_equal(state1, state2, explanation) {
    145        const states = ['init', 'install', 'start', 'finish', 'loaded'];
    146        const index1 = states.indexOf(state1);
    147        const index2 = states.indexOf(state2);
    148        if (index1 > index2)
    149          assert_unreached(explanation);
    150    }
    151 
    152    client_message_queue_test('enable-client-message-queue.html', async t => {
    153        // While parsing the child's document, the child transitions
    154        // from the 'init' state all the way to the 'finish' state.
    155        // Once parsing is finished it would enter the final 'loaded'
    156        // state. All but the last transition require assitance from
    157        // the test.
    158        await t.state_transition('init', 'install', [install_script]);
    159        await t.state_transition('install', 'start', [start_script]);
    160        await t.state_transition('start', 'finish', []);
    161 
    162        // Wait for all messages to get dispatched on the child's
    163        // ServiceWorkerContainer and then verify that each message
    164        // was dispatched after |earliest_dispatch|.
    165        const report = await t.frame.report;
    166        ['init', 'install', 'start'].forEach(state => {
    167            const explanation = `Message sent in state '${state}' was dispatched in '${report[state]}', should be dispatched no earlier than '${earliest_dispatch}'`;
    168            assert_state_less_than_equal(earliest_dispatch,
    169                                         report[state],
    170                                         explanation);
    171        });
    172    }, description);
    173 }
    174 
    175 const empty_script = ``;
    176 
    177 const add_event_listener =
    178    `navigator.serviceWorker.addEventListener('message', handle_message);`;
    179 
    180 const set_onmessage = `navigator.serviceWorker.onmessage = handle_message;`;
    181 
    182 const start_messages = `navigator.serviceWorker.startMessages();`;
    183 
    184 client_message_queue_enable_test(add_event_listener, empty_script, 'loaded',
    185    'Messages from ServiceWorker to Client only received after DOMContentLoaded event.');
    186 
    187 client_message_queue_enable_test(add_event_listener, start_messages, 'start',
    188    'Messages from ServiceWorker to Client only received after calling startMessages().');
    189 
    190 client_message_queue_enable_test(set_onmessage, empty_script, 'install',
    191    'Messages from ServiceWorker to Client only received after setting onmessage.');
    192 
    193 const resolve_manual_promise = `resolve_manual_promise();`
    194 
    195 async function test_microtasks_when_client_message_queue_enabled(t, scripts) {
    196    await t.state_transition('init', 'start', scripts.concat([resolve_manual_promise]));
    197    let result = await t.frame.result;
    198    assert_equals(result[0], 'microtask', 'The microtask was executed first.');
    199    assert_equals(result[1], 'message', 'The message was dispatched.');
    200 }
    201 
    202 client_message_queue_test('message-vs-microtask.html', t => {
    203    return test_microtasks_when_client_message_queue_enabled(t, [
    204        add_event_listener,
    205        start_messages,
    206    ]);
    207 }, 'Microtasks run before dispatching messages after calling startMessages().');
    208 
    209 client_message_queue_test('message-vs-microtask.html', t => {
    210    return test_microtasks_when_client_message_queue_enabled(t, [set_onmessage]);
    211 }, 'Microtasks run before dispatching messages after setting onmessage.');
    212 </script>