<!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>