| <!DOCTYPE html> |
| <title>Service Worker: Clients.matchAll ordering</title> |
| <meta name=timeout content=long> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="resources/test-helpers.sub.js"></script> |
| <script> |
| |
| // Utility function for URLs this test will open. |
| function makeURL(name, num, type) { |
| let u = new URL('resources/empty.html', location); |
| u.searchParams.set('name', name); |
| if (num !== undefined) { |
| u.searchParams.set('q', num); |
| } |
| if (type === 'nested') { |
| u.searchParams.set('nested', true); |
| } |
| return u.href; |
| } |
| |
| // Non-test URLs that will be open during each test. The harness URLs |
| // are from the WPT harness. The "extra" URL is a final window opened |
| // by the test. |
| const EXTRA_URL = makeURL('extra'); |
| const TEST_HARNESS_URL = location.href; |
| const TOP_HARNESS_URL = new URL('/testharness_runner.html', location).href; |
| |
| // Utility function to open an iframe in the target parent window. We |
| // can't just use with_iframe() because it does not support a configurable |
| // parent window. |
| function openFrame(parentWindow, url) { |
| return new Promise(resolve => { |
| let frame = parentWindow.document.createElement('iframe'); |
| frame.src = url; |
| parentWindow.document.body.appendChild(frame); |
| |
| frame.contentWindow.addEventListener('load', evt => { |
| resolve(frame); |
| }, { once: true }); |
| }); |
| } |
| |
| // Utility function to open a window and wait for it to load. The |
| // window may optionally have a nested iframe as well. Returns |
| // a result like `{ top: <frame ref> nested: <nested frame ref if present> }`. |
| function openFrameConfig(opts) { |
| let url = new URL(opts.url, location.href); |
| return openFrame(window, url.href).then(top => { |
| if (!opts.withNested) { |
| return { top: top }; |
| } |
| |
| url.searchParams.set('nested', true); |
| return openFrame(top.contentWindow, url.href).then(nested => { |
| return { top: top, nested: nested }; |
| }); |
| }); |
| } |
| |
| // Utility function that takes a list of configurations and opens the |
| // corresponding windows in sequence. An array of results is returned. |
| function openFrameConfigList(optList) { |
| let resultList = []; |
| function openNextWindow(optList, nextWindow) { |
| if (nextWindow >= optList.length) { |
| return resultList; |
| } |
| return openFrameConfig(optList[nextWindow]).then(result => { |
| resultList.push(result); |
| return openNextWindow(optList, nextWindow + 1); |
| }); |
| } |
| return openNextWindow(optList, 0); |
| } |
| |
| // Utility function that focuses the given entry in window result list. |
| function executeFocus(frameResultList, opts) { |
| return new Promise(resolve => { |
| let w = frameResultList[opts.index][opts.type]; |
| let target = w.contentWindow ? w.contentWindow : w; |
| target.addEventListener('focus', evt => { |
| resolve(); |
| }, { once: true }); |
| target.focus(); |
| }); |
| } |
| |
| // Utility function that performs a list of focus commands in sequence |
| // based on the window result list. |
| function executeFocusList(frameResultList, optList) { |
| function executeNextCommand(frameResultList, optList, nextCommand) { |
| if (nextCommand >= optList.length) { |
| return; |
| } |
| return executeFocus(frameResultList, optList[nextCommand]).then(_ => { |
| return executeNextCommand(frameResultList, optList, nextCommand + 1); |
| }); |
| } |
| return executeNextCommand(frameResultList, optList, 0); |
| } |
| |
| // Perform a `clients.matchAll()` in the service worker with the given |
| // options dictionary. |
| function doMatchAll(worker, options) { |
| return new Promise(resolve => { |
| let channel = new MessageChannel(); |
| channel.port1.onmessage = evt => { |
| resolve(evt.data); |
| }; |
| worker.postMessage({ port: channel.port2, options: options, disableSort: true }, |
| [channel.port2]); |
| }); |
| } |
| |
| // Function that performs a single test case. It takes a configuration object |
| // describing the windows to open, how to focus them, the matchAll options, |
| // and the resulting expectations. See the test cases for examples of how to |
| // use this. |
| function matchAllOrderTest(t, opts) { |
| let script = 'resources/clients-matchall-worker.js'; |
| let worker; |
| let frameResultList; |
| let extraWindowResult; |
| return service_worker_unregister_and_register(t, script, opts.scope).then(swr => { |
| t.add_cleanup(() => service_worker_unregister(t, opts.scope)); |
| |
| worker = swr.installing; |
| return wait_for_state(t, worker, 'activated'); |
| }).then(_ => { |
| return openFrameConfigList(opts.frameConfigList); |
| }).then(results => { |
| frameResultList = results; |
| return openFrameConfig({ url: EXTRA_URL }); |
| }).then(result => { |
| extraWindowResult = result; |
| return executeFocusList(frameResultList, opts.focusConfigList); |
| }).then(_ => { |
| return doMatchAll(worker, opts.matchAllOptions); |
| }).then(data => { |
| assert_equals(data.length, opts.expected.length); |
| for (let i = 0; i < data.length; ++i) { |
| assert_equals(data[i][2], opts.expected[i], 'expected URL index ' + i); |
| } |
| }).then(_ => { |
| frameResultList.forEach(result => result.top.remove()); |
| extraWindowResult.top.remove(); |
| }).catch(e => { |
| if (frameResultList) { |
| frameResultList.forEach(result => result.top.remove()); |
| } |
| if (extraWindowResult) { |
| extraWindowResult.top.remove(); |
| } |
| throw(e); |
| }); |
| } |
| |
| // ---------- |
| // Test cases |
| // ---------- |
| |
| promise_test(t => { |
| let name = 'no-focus-controlled-windows'; |
| let opts = { |
| scope: makeURL(name), |
| |
| frameConfigList: [ |
| { url: makeURL(name, 0), withNested: false }, |
| { url: makeURL(name, 1), withNested: false }, |
| { url: makeURL(name, 2), withNested: false }, |
| ], |
| |
| focusConfigList: [ |
| // no focus commands |
| ], |
| |
| matchAllOptions: { |
| includeUncontrolled: false |
| }, |
| |
| expected: [ |
| makeURL(name, 0), |
| makeURL(name, 1), |
| makeURL(name, 2), |
| ], |
| }; |
| |
| return matchAllOrderTest(t, opts); |
| }, 'Clients.matchAll() returns non-focused controlled windows in creation order.'); |
| |
| promise_test(t => { |
| let name = 'focus-controlled-windows-1'; |
| let opts = { |
| scope: makeURL(name), |
| |
| frameConfigList: [ |
| { url: makeURL(name, 0), withNested: false }, |
| { url: makeURL(name, 1), withNested: false }, |
| { url: makeURL(name, 2), withNested: false }, |
| ], |
| |
| focusConfigList: [ |
| { index: 0, type: 'top' }, |
| { index: 1, type: 'top' }, |
| { index: 2, type: 'top' }, |
| ], |
| |
| matchAllOptions: { |
| includeUncontrolled: false |
| }, |
| |
| expected: [ |
| makeURL(name, 2), |
| makeURL(name, 1), |
| makeURL(name, 0), |
| ], |
| }; |
| |
| return matchAllOrderTest(t, opts); |
| }, 'Clients.matchAll() returns controlled windows in focus order. Case 1.'); |
| |
| promise_test(t => { |
| let name = 'focus-controlled-windows-2'; |
| let opts = { |
| scope: makeURL(name), |
| |
| frameConfigList: [ |
| { url: makeURL(name, 0), withNested: false }, |
| { url: makeURL(name, 1), withNested: false }, |
| { url: makeURL(name, 2), withNested: false }, |
| ], |
| |
| focusConfigList: [ |
| { index: 2, type: 'top' }, |
| { index: 1, type: 'top' }, |
| { index: 0, type: 'top' }, |
| ], |
| |
| matchAllOptions: { |
| includeUncontrolled: false |
| }, |
| |
| expected: [ |
| makeURL(name, 0), |
| makeURL(name, 1), |
| makeURL(name, 2), |
| ], |
| }; |
| |
| return matchAllOrderTest(t, opts); |
| }, 'Clients.matchAll() returns controlled windows in focus order. Case 2.'); |
| |
| promise_test(t => { |
| let name = 'no-focus-uncontrolled-windows'; |
| let opts = { |
| scope: makeURL(name + '-outofscope'), |
| |
| frameConfigList: [ |
| { url: makeURL(name, 0), withNested: false }, |
| { url: makeURL(name, 1), withNested: false }, |
| { url: makeURL(name, 2), withNested: false }, |
| ], |
| |
| focusConfigList: [ |
| // no focus commands |
| ], |
| |
| matchAllOptions: { |
| includeUncontrolled: true |
| }, |
| |
| expected: [ |
| // The harness windows have been focused, so appear first |
| TEST_HARNESS_URL, |
| TOP_HARNESS_URL, |
| |
| // Test frames have not been focused, so appear in creation order |
| makeURL(name, 0), |
| makeURL(name, 1), |
| makeURL(name, 2), |
| EXTRA_URL, |
| ], |
| }; |
| |
| return matchAllOrderTest(t, opts); |
| }, 'Clients.matchAll() returns non-focused uncontrolled windows in creation order.'); |
| |
| promise_test(t => { |
| let name = 'focus-uncontrolled-windows-1'; |
| let opts = { |
| scope: makeURL(name + '-outofscope'), |
| |
| frameConfigList: [ |
| { url: makeURL(name, 0), withNested: false }, |
| { url: makeURL(name, 1), withNested: false }, |
| { url: makeURL(name, 2), withNested: false }, |
| ], |
| |
| focusConfigList: [ |
| { index: 0, type: 'top' }, |
| { index: 1, type: 'top' }, |
| { index: 2, type: 'top' }, |
| ], |
| |
| matchAllOptions: { |
| includeUncontrolled: true |
| }, |
| |
| expected: [ |
| // The test harness window is a parent of all test frames. It will |
| // always have the same focus time or later as its frames. So it |
| // appears first. |
| TEST_HARNESS_URL, |
| |
| makeURL(name, 2), |
| makeURL(name, 1), |
| makeURL(name, 0), |
| |
| // The overall harness has been focused |
| TOP_HARNESS_URL, |
| |
| // The extra frame was never focused |
| EXTRA_URL, |
| ], |
| }; |
| |
| return matchAllOrderTest(t, opts); |
| }, 'Clients.matchAll() returns uncontrolled windows in focus order. Case 1.'); |
| |
| promise_test(t => { |
| let name = 'focus-uncontrolled-windows-2'; |
| let opts = { |
| scope: makeURL(name + '-outofscope'), |
| |
| frameConfigList: [ |
| { url: makeURL(name, 0), withNested: false }, |
| { url: makeURL(name, 1), withNested: false }, |
| { url: makeURL(name, 2), withNested: false }, |
| ], |
| |
| focusConfigList: [ |
| { index: 2, type: 'top' }, |
| { index: 1, type: 'top' }, |
| { index: 0, type: 'top' }, |
| ], |
| |
| matchAllOptions: { |
| includeUncontrolled: true |
| }, |
| |
| expected: [ |
| // The test harness window is a parent of all test frames. It will |
| // always have the same focus time or later as its frames. So it |
| // appears first. |
| TEST_HARNESS_URL, |
| |
| makeURL(name, 0), |
| makeURL(name, 1), |
| makeURL(name, 2), |
| |
| // The overall harness has been focused |
| TOP_HARNESS_URL, |
| |
| // The extra frame was never focused |
| EXTRA_URL, |
| ], |
| }; |
| |
| return matchAllOrderTest(t, opts); |
| }, 'Clients.matchAll() returns uncontrolled windows in focus order. Case 2.'); |
| |
| promise_test(t => { |
| let name = 'focus-controlled-nested-windows'; |
| let opts = { |
| scope: makeURL(name), |
| |
| frameConfigList: [ |
| { url: makeURL(name, 0), withNested: true }, |
| { url: makeURL(name, 1), withNested: true }, |
| { url: makeURL(name, 2), withNested: true }, |
| ], |
| |
| focusConfigList: [ |
| { index: 0, type: 'top' }, |
| |
| // Note, some browsers don't let programmatic focus of a frame unless |
| // an ancestor window is already focused. So focus the window and |
| // then the frame. |
| { index: 1, type: 'top' }, |
| { index: 1, type: 'nested' }, |
| |
| { index: 2, type: 'top' }, |
| ], |
| |
| matchAllOptions: { |
| includeUncontrolled: false |
| }, |
| |
| expected: [ |
| // Focus order for window 2, but not its frame. We only focused |
| // the window. |
| makeURL(name, 2), |
| |
| // Window 1 is next via focus order, but the window is always |
| // shown first here. The window gets its last focus time updated |
| // when the frame is focused. Since the times match between the |
| // two it falls back to creation order. The window was created |
| // before the frame. This behavior is being discussed in: |
| // https://github.com/w3c/ServiceWorker/issues/1080 |
| makeURL(name, 1), |
| makeURL(name, 1, 'nested'), |
| |
| // Focus order for window 0, but not its frame. We only focused |
| // the window. |
| makeURL(name, 0), |
| |
| // Creation order of the frames since they are not focused by |
| // default when they are created. |
| makeURL(name, 0, 'nested'), |
| makeURL(name, 2, 'nested'), |
| ], |
| }; |
| |
| return matchAllOrderTest(t, opts); |
| }, 'Clients.matchAll() returns controlled windows and frames in focus order.'); |
| </script> |