diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/web-locks | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/web-locks')
44 files changed, 2939 insertions, 0 deletions
diff --git a/testing/web-platform/tests/web-locks/META.yml b/testing/web-platform/tests/web-locks/META.yml new file mode 100644 index 0000000000..e74e4150f3 --- /dev/null +++ b/testing/web-platform/tests/web-locks/META.yml @@ -0,0 +1,5 @@ +spec: https://w3c.github.io/web-locks/ +suggested_reviewers: + - inexorabletash + - pwnall + - saschanaz diff --git a/testing/web-platform/tests/web-locks/README.md b/testing/web-platform/tests/web-locks/README.md new file mode 100644 index 0000000000..84af41baa8 --- /dev/null +++ b/testing/web-platform/tests/web-locks/README.md @@ -0,0 +1,5 @@ +This directory contains a test suite for the proposed Web Locks API. + +Explainer: https://github.com/w3c/web-locks/ + +Spec: https://w3c.github.io/web-locks/ diff --git a/testing/web-platform/tests/web-locks/acquire.https.any.js b/testing/web-platform/tests/web-locks/acquire.https.any.js new file mode 100644 index 0000000000..54ae6f30e7 --- /dev/null +++ b/testing/web-platform/tests/web-locks/acquire.https.any.js @@ -0,0 +1,136 @@ +// META: title=Web Locks API: navigator.locks.request method +// META: script=resources/helpers.js +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +promise_test(async t => { + const res = uniqueName(t); + await promise_rejects_js(t, TypeError, navigator.locks.request()); + await promise_rejects_js(t, TypeError, navigator.locks.request(res)); +}, 'navigator.locks.request requires a name and a callback'); + +promise_test(async t => { + const res = uniqueName(t); + await promise_rejects_js( + t, TypeError, + navigator.locks.request(res, {mode: 'foo'}, lock => {})); + await promise_rejects_js( + t, TypeError, + navigator.locks.request(res, {mode: null }, lock => {})); + assert_equals(await navigator.locks.request( + res, {mode: 'exclusive'}, lock => lock.mode), 'exclusive', + 'mode is exclusive'); + assert_equals(await navigator.locks.request( + res, {mode: 'shared'}, lock => lock.mode), 'shared', + 'mode is shared'); +}, 'mode must be "shared" or "exclusive"'); + +promise_test(async t => { + const res = uniqueName(t); + await promise_rejects_dom( + t, 'NotSupportedError', + navigator.locks.request( + res, {steal: true, ifAvailable: true}, lock => {}), + "A NotSupportedError should be thrown if both " + + "'steal' and 'ifAvailable' are specified."); +}, "The 'steal' and 'ifAvailable' options are mutually exclusive"); + +promise_test(async t => { + const res = uniqueName(t); + await promise_rejects_dom( + t, 'NotSupportedError', + navigator.locks.request(res, {mode: 'shared', steal: true}, lock => {}), + 'Request with mode=shared and steal=true should fail'); +}, "The 'steal' option must be used with exclusive locks"); + +promise_test(async t => { + const res = uniqueName(t); + const controller = new AbortController(); + await promise_rejects_dom( + t, 'NotSupportedError', + navigator.locks.request( + res, {signal: controller.signal, steal: true}, lock => {}), + 'Request with signal and steal=true should fail'); +}, "The 'signal' and 'steal' options are mutually exclusive"); + +promise_test(async t => { + const res = uniqueName(t); + const controller = new AbortController(); + await promise_rejects_dom( + t, 'NotSupportedError', + navigator.locks.request( + res, {signal: controller.signal, ifAvailable: true}, lock => {}), + 'Request with signal and ifAvailable=true should fail'); +}, "The 'signal' and 'ifAvailable' options are mutually exclusive"); + +promise_test(async t => { + const res = uniqueName(t); + await promise_rejects_js( + t, TypeError, navigator.locks.request(res, undefined)); + await promise_rejects_js( + t, TypeError, navigator.locks.request(res, null)); + await promise_rejects_js( + t, TypeError, navigator.locks.request(res, 123)); + await promise_rejects_js( + t, TypeError, navigator.locks.request(res, 'abc')); + await promise_rejects_js( + t, TypeError, navigator.locks.request(res, [])); + await promise_rejects_js( + t, TypeError, navigator.locks.request(res, {})); + await promise_rejects_js( + t, TypeError, navigator.locks.request(res, new Promise(r => {}))); +}, 'callback must be a function'); + +promise_test(async t => { + const res = uniqueName(t); + let release; + const promise = new Promise(r => { release = r; }); + + let returned = navigator.locks.request(res, lock => { return promise; }); + + const order = []; + + returned.then(() => { order.push('returned'); }); + promise.then(() => { order.push('holding'); }); + + release(); + + await Promise.all([returned, promise]); + + assert_array_equals(order, ['holding', 'returned']); + +}, 'navigator.locks.request\'s returned promise resolves after' + + ' lock is released'); + +promise_test(async t => { + const res = uniqueName(t); + const test_error = {name: 'test'}; + const p = navigator.locks.request(res, lock => { + throw test_error; + }); + assert_equals(Promise.resolve(p), p, 'request() result is a Promise'); + await promise_rejects_exactly(t, test_error, p, 'result should reject'); +}, 'Returned Promise rejects if callback throws synchronously'); + +promise_test(async t => { + const res = uniqueName(t); + const test_error = {name: 'test'}; + const p = navigator.locks.request(res, async lock => { + throw test_error; + }); + assert_equals(Promise.resolve(p), p, 'request() result is a Promise'); + await promise_rejects_exactly(t, test_error, p, 'result should reject'); +}, 'Returned Promise rejects if callback throws asynchronously'); + +promise_test(async t => { + const res = uniqueName(t); + let then_invoked = false; + const test_error = { then: _ => { then_invoked = true; } }; + const p = navigator.locks.request(res, async lock => { + throw test_error; + }); + assert_equals(Promise.resolve(p), p, 'request() result is a Promise'); + await promise_rejects_exactly(t, test_error, p, 'result should reject'); + assert_false(then_invoked, 'then() should not be invoked'); +}, 'If callback throws a thenable, its then() should not be invoked'); diff --git a/testing/web-platform/tests/web-locks/bfcache/abort.tentative.https.html b/testing/web-platform/tests/web-locks/bfcache/abort.tentative.https.html new file mode 100644 index 0000000000..35d9b11270 --- /dev/null +++ b/testing/web-platform/tests/web-locks/bfcache/abort.tentative.https.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<title>Web Locks API: bfcache</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script> +<script type="module"> + import { runWebLocksBfcacheTest } from "./helpers.js"; + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + const controller = new AbortController(); + const promise = navigator.locks.request( + uniqueNameByQuery(), + { signal: controller.signal }, + () => new Promise(() => { }) + ); + controller.abort(); + await promise.catch(() => { }); + }, + shouldBeCached: true + }, "An immediately aborted lock on main thread should not prevent bfcache"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + window.worker = new Worker("/web-locks/resources/worker.js"); + await postToWorkerAndWait(worker, { + op: "request", + name: uniqueNameByQuery(), + abortImmediately: true + }); + }, + shouldBeCached: true + }, "An immediately aborted lock on a worker should not prevent bfcache"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + window.worker = new Worker("/web-locks/resources/parentworker.js"); + await postToWorkerAndWait(worker, { + op: "request", + name: uniqueNameByQuery(), + abortImmediately: true + }); + }, + shouldBeCached: true + }, "An immediately aborted lock on a nested worker should not prevent bfcache"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + window.worker = new SharedWorker("/web-locks/resources/worker.js"); + worker.port.start(); + await postToWorkerAndWait(worker.port, { + op: "request", + name: uniqueNameByQuery(), + abortImmediately: true + }); + }, + shouldBeCached: true + }, "An immediately aborted lock on a shared worker should not prevent bfcache"); +</script> diff --git a/testing/web-platform/tests/web-locks/bfcache/held.tentative.https.html b/testing/web-platform/tests/web-locks/bfcache/held.tentative.https.html new file mode 100644 index 0000000000..9d670c5889 --- /dev/null +++ b/testing/web-platform/tests/web-locks/bfcache/held.tentative.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<title>Web Locks API: bfcache</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script> +<script type="module"> + import { runWebLocksBfcacheTest } from "./helpers.js"; + + runWebLocksBfcacheTest({ + funcBeforeNavigation: () => { + navigator.locks.request(uniqueNameByQuery(), () => new Promise(() => { })); + }, + shouldBeCached: false + }, "A held lock on the main thread must prevent bfcache"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + window.worker = new Worker("/web-locks/resources/worker.js"); + await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() }); + }, + shouldBeCached: false + }, "A held lock on a worker must prevent bfcache"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + window.worker = new Worker("/web-locks/resources/parentworker.js"); + await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() }); + }, + shouldBeCached: false + }, "A held lock on a nested worker must prevent bfcache"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + window.worker = new SharedWorker("/web-locks/resources/worker.js"); + worker.port.start(); + await postToWorkerAndWait(worker.port, { op: "request", name: uniqueNameByQuery() }); + }, + shouldBeCached: false + }, "A held lock on a shared worker must prevent bfcache"); +</script> diff --git a/testing/web-platform/tests/web-locks/bfcache/helpers.js b/testing/web-platform/tests/web-locks/bfcache/helpers.js new file mode 100644 index 0000000000..7997f982a8 --- /dev/null +++ b/testing/web-platform/tests/web-locks/bfcache/helpers.js @@ -0,0 +1,15 @@ +export function runWebLocksBfcacheTest(params, description) { + runBfcacheTest( + { + scripts: ["/web-locks/resources/helpers.js"], + openFunc: url => + window.open( + url + `&prefix=${location.pathname}-${description}`, + "_blank", + "noopener" + ), + ...params, + }, + description + ); +} diff --git a/testing/web-platform/tests/web-locks/bfcache/release-across-thread.tentative.https.html b/testing/web-platform/tests/web-locks/bfcache/release-across-thread.tentative.https.html new file mode 100644 index 0000000000..374cee8b6d --- /dev/null +++ b/testing/web-platform/tests/web-locks/bfcache/release-across-thread.tentative.https.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<title>Web Locks API: bfcache</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script> +<script type="module"> + import { runWebLocksBfcacheTest } from "./helpers.js"; + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + navigator.locks.request(uniqueNameByQuery(), () => new Promise(() => { })); + window.worker = new Worker("/web-locks/resources/worker.js"); + const { lock_id } = await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() }); + await postToWorkerAndWait(worker, { op: "release", lock_id }); + }, + shouldBeCached: false, + }, "A held lock on main thread must prevent bfcache even after worker releases locks"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + const controller = new AbortController(); + navigator.locks.request(uniqueNameByQuery(), { signal: controller.signal }, () => new Promise(() => { })); + window.worker = new Worker("/web-locks/resources/worker.js"); + await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() }); + controller.abort(); + }, + shouldBeCached: false, + }, "A held lock on worker must prevent bfcache even after main thread releases locks"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + const controller = new AbortController(); + navigator.locks.request(uniqueNameByQuery(), { signal: controller.signal }, () => new Promise(() => { })); + window.worker = new SharedWorker("/web-locks/resources/worker.js"); + worker.port.start(); + await postToWorkerAndWait(worker.port, { op: "request", name: uniqueNameByQuery() }); + controller.abort(); + }, + shouldBeCached: false, + }, "A held lock on shared worker must prevent bfcache even after main thread releases locks"); +</script> diff --git a/testing/web-platform/tests/web-locks/bfcache/release.tentative.https.html b/testing/web-platform/tests/web-locks/bfcache/release.tentative.https.html new file mode 100644 index 0000000000..97f542fc2e --- /dev/null +++ b/testing/web-platform/tests/web-locks/bfcache/release.tentative.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<title>Web Locks API: bfcache</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script> +<script type="module"> + import { runWebLocksBfcacheTest } from "./helpers.js"; + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + await navigator.locks.request(uniqueNameByQuery(), () => { }); + }, + shouldBeCached: true, + }, "A released lock on the main thread should not prevent bfcache"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + window.worker = new Worker("/web-locks/resources/worker.js"); + const { lock_id } = await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() }); + await postToWorkerAndWait(worker, { op: "release", lock_id }); + }, + shouldBeCached: true, + }, "A released lock on a worker should not prevent bfcache"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + window.worker = new Worker("/web-locks/resources/parentworker.js"); + const { lock_id } = await postToWorkerAndWait(worker, { op: "request", name: uniqueNameByQuery() }); + await postToWorkerAndWait(worker, { op: "release", lock_id }); + }, + shouldBeCached: true, + }, "A released lock on a nested worker should not prevent bfcache"); + + runWebLocksBfcacheTest({ + funcBeforeNavigation: async () => { + window.worker = new SharedWorker("/web-locks/resources/worker.js"); + worker.port.start(); + const { lock_id } = await postToWorkerAndWait(worker.port, { op: "request", name: uniqueNameByQuery() }); + await postToWorkerAndWait(worker.port, { op: "release", lock_id }); + }, + shouldBeCached: true, + }, "A released lock on a shared worker should not prevent bfcache"); +</script> diff --git a/testing/web-platform/tests/web-locks/bfcache/sharedworker-multiple.tentative.https.html b/testing/web-platform/tests/web-locks/bfcache/sharedworker-multiple.tentative.https.html new file mode 100644 index 0000000000..71a79bc5fa --- /dev/null +++ b/testing/web-platform/tests/web-locks/bfcache/sharedworker-multiple.tentative.https.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<title>Web Locks API: bfcache</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js"></script> +<script> + const connectToSharedWorker = async () => { + await window.pageShowPromise; + window.worker = new SharedWorker("/web-locks/resources/worker.js"); + worker.port.start(); + } + function double_docs_test(func, description) { + promise_test(async t => { + const pageA1 = new RemoteContext(token()); + const pageA2 = new RemoteContext(token()); + const pageB1 = new RemoteContext(token()); + const pageB2 = new RemoteContext(token()); + + const urlA1 = executorPath + pageA1.context_id; + const urlA2 = executorPath + pageA2.context_id; + const urlB1 = originCrossSite + executorPath + pageB1.context_id; + const urlB2 = originCrossSite + executorPath + pageB2.context_id; + + window.open(urlA1, "_blank", "noopener"); + window.open(urlA2, "_blank", "noopener"); + + await func(pageA1, pageA2); + + await Promise.all([ + navigateAndThenBack(pageA1, pageB1, urlB1), + navigateAndThenBack(pageA2, pageB2, urlB2), + ]); + + await assert_not_bfcached(pageA1); + await assert_not_bfcached(pageA2); + }, description); + } + + double_docs_test(async (pageA1, pageA2) => { + await Promise.all([ + pageA1.execute_script(connectToSharedWorker), + pageA2.execute_script(connectToSharedWorker), + ]); + await pageA1.execute_script(async () => { + const script = document.createElement("script"); + script.src = "/web-locks/resources/helpers.js"; + document.head.append(script); + await new Promise(resolve => script.onload = resolve); + await postToWorkerAndWait(worker.port, { op: "request", name: uniqueNameByQuery() }); + }); + }, "A new held lock must prevent bfcache on all connected documents"); + + double_docs_test(async (pageA1, pageA2) => { + await pageA1.execute_script(connectToSharedWorker); + await pageA1.execute_script(async () => { + const script = document.createElement("script"); + script.src = "/web-locks/resources/helpers.js"; + document.head.append(script); + await new Promise(resolve => script.onload = resolve); + await postToWorkerAndWait(worker.port, { op: "request", name: uniqueNameByQuery() }); + }); + await pageA2.execute_script(connectToSharedWorker); + }, "An existing held lock must prevent bfcache on all connected documents"); +</script> diff --git a/testing/web-platform/tests/web-locks/clientids.https.html b/testing/web-platform/tests/web-locks/clientids.https.html new file mode 100644 index 0000000000..7a3ffe45e1 --- /dev/null +++ b/testing/web-platform/tests/web-locks/clientids.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Web Locks API: Client IDs in query() vs. Service Worker</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> + +// Returns a promise resolved by the next message event. +function nextMessage() { + return new Promise(resolve => { + window.addEventListener('message', event => { + resolve(event.data); + }, {once: true}); + }); +} + +promise_test(async t => { + assert_implements(navigator.locks); + const iframe_url = 'resources/sw-controlled-iframe.html'; + + // Register a service worker that will control an iframe. + const registration = await service_worker_unregister_and_register( + t, 'resources/service-worker.js', iframe_url); + await wait_for_state(t, registration.installing, 'activated'); + + const iframe = await with_iframe(iframe_url); + + iframe.contentWindow.postMessage('get_sw_client_id', '*'); + const sw_client_id = await nextMessage(); + + iframe.contentWindow.postMessage('get_lock_client_id', '*'); + const lock_client_id = await nextMessage(); + + // NOTE: Not assert_equals(), as we don't want log the randomly generated + // clientIds, since they would not match any failure expectation files. + assert_equals(lock_client_id, sw_client_id, + 'clientIds should match, but are different'); + + await registration.unregister(); + +}, 'Client IDs match between Locks API and Service Workers'); + +</script> diff --git a/testing/web-platform/tests/web-locks/crashtests/after-worker-termination.https.html b/testing/web-platform/tests/web-locks/crashtests/after-worker-termination.https.html new file mode 100644 index 0000000000..e113cbdf6a --- /dev/null +++ b/testing/web-platform/tests/web-locks/crashtests/after-worker-termination.https.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html class="test-wait"> +<meta charset="utf-8"> +<script> + const script = ` + postMessage("hi"); + // This line runs until worker.terminate() happens, which terminates this function too. + self.reportError(new Int16Array(2147483648)) + // And thus this line runs after the termination. + navigator.locks.request("weblock_0", () => {}); + `; + const worker = new Worker(URL.createObjectURL(new Blob([script]))); + worker.onmessage = () => { + worker.terminate(); + + // We want to wait for the full termination but there is no API for that + // So, just wait for a random time + setTimeout(() => document.documentElement.classList.remove("test-wait"), 100); + } +</script> diff --git a/testing/web-platform/tests/web-locks/crashtests/iframe-append-2.https.html b/testing/web-platform/tests/web-locks/crashtests/iframe-append-2.https.html new file mode 100644 index 0000000000..06aee518fe --- /dev/null +++ b/testing/web-platform/tests/web-locks/crashtests/iframe-append-2.https.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<iframe id="id_0"></iframe> +<script> + window.addEventListener("load", () => { + const iframe = document.getElementById("id_0") + // Discards the previous document + document.documentElement.appendChild(iframe) + const xhr = new XMLHttpRequest() + // LockManager is created after discarding + // At this point the new document is not there yet + iframe.contentWindow.navigator.locks.request("weblock_0", () => { + xhr.open("GET", "FOOBAR", false) + xhr.send() + // Now there is a new document + }) + }) +</script> diff --git a/testing/web-platform/tests/web-locks/crashtests/iframe-append.https.html b/testing/web-platform/tests/web-locks/crashtests/iframe-append.https.html new file mode 100644 index 0000000000..507a40d272 --- /dev/null +++ b/testing/web-platform/tests/web-locks/crashtests/iframe-append.https.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html class="test-wait"> +<meta charset="utf-8"> +<iframe id="id_0"></iframe> +<script> + /** @param {HTMLIFrameElement} iframe */ + function waitForLoad(iframe) { + // iframe is initialized immediately on Chrome while it needs some time on Firefox + if (iframe.contentDocument.readyState === "complete") { + return; + } + return new Promise(r => iframe.onload = r); + } + + const iframe = document.getElementById("id_0"); + iframe.contentWindow.navigator.locks.request("weblock_0", async () => { + await waitForLoad(iframe); + document.body.append(iframe); // discards the document and destroys locks + document.documentElement.classList.remove("test-wait"); + }); +</script> diff --git a/testing/web-platform/tests/web-locks/crashtests/settle-after-steal.https.html b/testing/web-platform/tests/web-locks/crashtests/settle-after-steal.https.html new file mode 100644 index 0000000000..6b337e72e9 --- /dev/null +++ b/testing/web-platform/tests/web-locks/crashtests/settle-after-steal.https.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<html class="test-wait"> +<script> + navigator.locks.request("foo", async () => { + await new Promise(queueMicrotask); + document.documentElement.classList.remove("test-wait"); + }); + navigator.locks.request("foo", { steal: true }, () => {}); +</script> diff --git a/testing/web-platform/tests/web-locks/crashtests/worker-termination.https.html b/testing/web-platform/tests/web-locks/crashtests/worker-termination.https.html new file mode 100644 index 0000000000..b9b988b36c --- /dev/null +++ b/testing/web-platform/tests/web-locks/crashtests/worker-termination.https.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html class="test-wait"> +<meta charset="utf-8"> +<script> + var worker = new Worker(URL.createObjectURL(new Blob([` + postMessage("hi"); + (async () => { + const abort = new AbortController() + await navigator.locks.request("weblock_0", { signal: abort.signal }, () => {}) + })() + `]))); + worker.onmessage = () => { + worker.terminate(); + document.documentElement.classList.remove("test-wait"); + }; +</script> diff --git a/testing/web-platform/tests/web-locks/frames.https.html b/testing/web-platform/tests/web-locks/frames.https.html new file mode 100644 index 0000000000..980b2fe747 --- /dev/null +++ b/testing/web-platform/tests/web-locks/frames.https.html @@ -0,0 +1,265 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Web Locks API: Frames</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/helpers.js"></script> +<style>iframe { display: none; }</style> +<script> +'use strict'; + +promise_test(async t => { + assert_implements(navigator.locks); + const res = uniqueName(t); + + const frame = await iframe('resources/iframe.html'); + t.add_cleanup(() => { frame.remove(); }); + + const lock_id = (await postToFrameAndWait( + frame, {op: 'request', name: res, mode: 'shared'})).lock_id; + + await navigator.locks.request(res, {mode: 'shared'}, async lock => { + await postToFrameAndWait(frame, {op: 'release', lock_id}); + }); + +}, 'Window and Frame - shared mode'); + +promise_test(async t => { + assert_implements(navigator.locks); + const res = uniqueName(t); + + const frame = await iframe('resources/iframe.html'); + t.add_cleanup(() => { frame.remove(); }); + + // frame acquires the lock. + const lock_id = (await postToFrameAndWait( + frame, {op: 'request', name: res})).lock_id; + + // This request should be blocked. + let lock_granted = false; + const blocked = navigator.locks.request(res, lock => { lock_granted = true; }); + + // Verify that we can't get it. + let available = undefined; + await navigator.locks.request( + res, {ifAvailable: true}, lock => { available = lock !== null; }); + assert_false(available); + assert_false(lock_granted); + + // Ask the frame to release it. + await postToFrameAndWait(frame, {op: 'release', lock_id}); + + await blocked; + // Now we've got it. + assert_true(lock_granted); +}, 'Window and Frame - exclusive mode'); + +promise_test(async t => { + assert_implements(navigator.locks); + const res = uniqueName(t); + + const frame1 = await iframe('resources/iframe.html'); + const frame2 = await iframe('resources/iframe.html'); + + // frame1 acquires the lock. + const lock_id = (await postToFrameAndWait( + frame1, {op: 'request', name: res})).lock_id; + + // frame2's request should be blocked. + let lock_granted = false; + const blocked = postToFrameAndWait( + frame2, {op: 'request', name: res}); + blocked.then(f => { lock_granted = true; }); + + // Verify that frame2 can't get it. + assert_true((await postToFrameAndWait(frame2, { + op: 'request', name: res, ifAvailable: true + })).failed, 'Lock request should have failed'); + assert_false(lock_granted); + + // Ask frame1 to release it. + await postToFrameAndWait(frame1, {op: 'release', lock_id}); + + await blocked; + // Now frame2 can get it. + assert_true(lock_granted); + frame1.parentElement.removeChild(frame1); + frame2.parentElement.removeChild(frame2); +}, 'Frame and Frame - exclusive mode'); + +promise_test(async t => { + assert_implements(navigator.locks); + const res = uniqueName(t); + + const frame = await iframe('resources/iframe.html'); + + // Frame acquires the lock. + await postToFrameAndWait(frame, {op: 'request', name: res}); + + // This request should be blocked. + let lock_granted = false; + const blocked = navigator.locks.request( + res, lock => { lock_granted = true; }); + + // Verify that we can't get it. + let available = undefined; + await navigator.locks.request( + res, {ifAvailable: true}, lock => { available = lock !== null; }); + assert_false(available); + assert_false(lock_granted); + + // Implicitly release it by terminating the frame. + frame.remove(); + await blocked; + // Now we've got it. + assert_true(lock_granted); + +}, 'Terminated Frame with held lock'); + +promise_test(async t => { + assert_implements(navigator.locks); + const res = uniqueName(t); + + const frame = await iframe('resources/iframe.html'); + + // Frame acquires the lock. + await postToFrameAndWait(frame, {op: 'request', name: res}); + + // This request should be blocked. + let lock_granted = false; + const blocked = navigator.locks.request( + res, lock => { lock_granted = true; }); + + // Verify that we can't get it. + let available = undefined; + await navigator.locks.request( + res, {ifAvailable: true}, lock => { available = lock !== null; }); + assert_false(available); + assert_false(lock_granted); + + // Implicitly release it by navigating the frame. + frame.src = 'about:blank'; + await blocked; + // Now we've got it. + assert_true(lock_granted); + +}, 'Navigated Frame with held lock'); + +promise_test(async t => { + assert_implements(navigator.locks); + const res = uniqueName(t); + + // frame1 requests and holds res - should be granted immediately. + // frame2 requests res - should be blocked. + // frame3 requests res - should be blocked. + // frame2 is navigated. + // frame1 releases res. + // frame3's request should be granted. + + const frame1 = await iframe('resources/iframe.html'); + const frame2 = await iframe('resources/iframe.html'); + const frame3 = await iframe('resources/iframe.html'); + t.add_cleanup(() => { frame1.remove(); }); + t.add_cleanup(() => { frame2.remove(); }); + t.add_cleanup(() => { frame3.remove(); }); + + // frame1 requests and holds res - should be granted immediately. + const lock_id = (await postToFrameAndWait( + frame1, {op: 'request', name: res})).lock_id; + + // frame2 requests res - should be blocked. + // (don't attach listeners as they will keep the frame alive) + frame2.contentWindow.postMessage({op: 'request', name: res}, '*'); + + // frame3 requests res - should be blocked. + let lock_granted = false; + const blocked = postToFrameAndWait(frame3, {op: 'request', name: res}); + blocked.then(f => { lock_granted = true; }); + + // Verify that frame3 can't get it. + assert_true((await postToFrameAndWait(frame3, { + op: 'request', name: res, ifAvailable: true + })).failed, 'Lock request should have failed'); + assert_false(lock_granted); + + // Navigate frame2. + frame2.src = 'about:blank'; + + // frame1 releases lock + await postToFrameAndWait(frame1, {op: 'release', lock_id}); + + // frame3's request should be granted. + await blocked; + assert_true(lock_granted); + +}, 'Navigated Frame with pending request'); + +promise_test(async t => { + assert_implements(navigator.locks); + const res = uniqueName(t); + + // frame1 requests and holds res - should be granted immediately. + // frame2 requests res - should be blocked. + // frame3 requests res - should be blocked. + // frame2 is removed. + // frame1 drops lock. + // frame3's request should be granted. + + const frame1 = await iframe('resources/iframe.html'); + const frame2 = await iframe('resources/iframe.html'); + const frame3 = await iframe('resources/iframe.html'); + t.add_cleanup(() => { frame1.remove(); }); + t.add_cleanup(() => { frame3.remove(); }); + + // frame1 requests and holds res - should be granted immediately. + const lock_id = (await postToFrameAndWait( + frame1, {op: 'request', name: res})).lock_id; + + // frame2 requests res - should be blocked. + // (don't attach listeners as they will keep the frame alive) + frame2.contentWindow.postMessage({op: 'request', name: res}, '*'); + + // frame3 requests res - should be blocked. + let lock_granted = false; + const blocked = postToFrameAndWait(frame3, {op: 'request', name: res}); + blocked.then(f => { lock_granted = true; }); + + // So frame3 can't get it + assert_true((await postToFrameAndWait(frame3, { + op: 'request', name: res, ifAvailable: true + })).failed, 'Lock request should have failed'); + assert_false(lock_granted); + + // Remove frame2. + frame2.remove(); + + // frame1 releases lock + await postToFrameAndWait(frame1, {op: 'release', lock_id}); + + // frame3's request should be granted. + await blocked; + assert_true(lock_granted); + +}, 'Removed Frame with pending request'); + +promise_test(async t => { + assert_implements(navigator.locks); + const res = uniqueName(t); + + const frame = await iframe('about:blank'); + + // Remove a frame while it is in the process of requesting a lock. + // The promise returned by `request` will never resolve since its frame no + // longer exists, but the lock should still be released. + await new Promise(resolve => { + frame.contentWindow.navigator.locks.request(res, () => { + frame.remove(); + resolve(); + }); + }); + + assert_false((await navigator.locks.query()).held.includes(res)); +}, 'Removed Frame as lock is granted'); + +</script> diff --git a/testing/web-platform/tests/web-locks/held.https.any.js b/testing/web-platform/tests/web-locks/held.https.any.js new file mode 100644 index 0000000000..7fc4c73540 --- /dev/null +++ b/testing/web-platform/tests/web-locks/held.https.any.js @@ -0,0 +1,91 @@ +// META: title=Web Locks API: Lock held until callback result resolves +// META: script=resources/helpers.js +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +// For uncaught rejections. +setup({allow_uncaught_exception: true}); + +function snooze(t, ms) { return new Promise(r => t.step_timeout(r, ms)); } + +promise_test(async t => { + const res = uniqueName(t); + const p = navigator.locks.request(res, lock => 123); + assert_equals(Promise.resolve(p), p, 'request() result is a Promise'); + assert_equals(await p, 123, 'promise resolves to the returned value'); +}, 'callback\'s result is promisified if not async'); + +promise_test(async t => { + const res = uniqueName(t); + // Resolved when the lock is granted. + let granted; + const lock_granted_promise = new Promise(r => { granted = r; }); + + // Lock is held until this is resolved. + let resolve; + const lock_release_promise = new Promise(r => { resolve = r; }); + + const order = []; + + navigator.locks.request(res, lock => { + granted(lock); + return lock_release_promise; + }); + await lock_granted_promise; + + await Promise.all([ + snooze(t, 50).then(() => { + order.push('1st lock released'); + resolve(); + }), + navigator.locks.request(res, () => { + order.push('2nd lock granted'); + }) + ]); + + assert_array_equals(order, ['1st lock released', '2nd lock granted']); +}, 'lock is held until callback\'s returned promise resolves'); + +promise_test(async t => { + const res = uniqueName(t); + // Resolved when the lock is granted. + let granted; + const lock_granted_promise = new Promise(r => { granted = r; }); + + // Lock is held until this is rejected. + let reject; + const lock_release_promise = new Promise((_, r) => { reject = r; }); + + const order = []; + + navigator.locks.request(res, lock => { + granted(lock); + return lock_release_promise; + }); + await lock_granted_promise; + + await Promise.all([ + snooze(t, 50).then(() => { + order.push('reject'); + reject(new Error('this uncaught rejection is expected')); + }), + navigator.locks.request(res, () => { + order.push('2nd lock granted'); + }) + ]); + + assert_array_equals(order, ['reject', '2nd lock granted']); +}, 'lock is held until callback\'s returned promise rejects'); + +promise_test(async t => { + const res = uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, async lock => { + await navigator.locks.request(res, {ifAvailable: true}, lock => { + callback_called = true; + assert_equals(lock, null, 'lock request should fail if held'); + }); + }); + assert_true(callback_called, 'callback should have executed'); +}, 'held lock prevents the same client from acquiring it'); diff --git a/testing/web-platform/tests/web-locks/idlharness.https.any.js b/testing/web-platform/tests/web-locks/idlharness.https.any.js new file mode 100644 index 0000000000..2df7831c34 --- /dev/null +++ b/testing/web-platform/tests/web-locks/idlharness.https.any.js @@ -0,0 +1,29 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: global=window,dedicatedworker,sharedworker,serviceworker +// META: timeout=long + +'use strict'; + +idl_test( + ['web-locks'], + ['html'], + async idl_array => { + idl_array.add_objects({ + LockManager: ['navigator.locks'], + Lock: ['lock'], + }); + + if (self.Window) { + idl_array.add_objects({ Navigator: ['navigator'] }); + } else { + idl_array.add_objects({ WorkerNavigator: ['navigator'] }); + } + + try { + await navigator.locks.request('name', l => { self.lock = l; }); + } catch (e) { + // Surfaced in idlharness.js's test_object below. + } + } +); diff --git a/testing/web-platform/tests/web-locks/ifAvailable.https.any.js b/testing/web-platform/tests/web-locks/ifAvailable.https.any.js new file mode 100644 index 0000000000..63e6d568fe --- /dev/null +++ b/testing/web-platform/tests/web-locks/ifAvailable.https.any.js @@ -0,0 +1,163 @@ +// META: title=Web Locks API: ifAvailable option +// META: script=resources/helpers.js +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +promise_test(async t => { + const res = self.uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, {ifAvailable: true}, async lock => { + callback_called = true; + assert_not_equals(lock, null, 'lock should be granted'); + }); + assert_true(callback_called, 'callback should be called'); +}, 'Lock request with ifAvailable - lock available'); + +promise_test(async t => { + const res = self.uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, async lock => { + // Request would time out if |ifAvailable| was not specified. + const result = await navigator.locks.request( + res, {ifAvailable: true}, async lock => { + callback_called = true; + assert_equals(lock, null, 'lock should not be granted'); + return 123; + }); + assert_equals(result, 123, 'result should be value returned by callback'); + }); + assert_true(callback_called, 'callback should be called'); +}, 'Lock request with ifAvailable - lock not available'); + +promise_test(async t => { + const res = self.uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, async lock => { + try { + // Request would time out if |ifAvailable| was not specified. + await navigator.locks.request(res, {ifAvailable: true}, async lock => { + callback_called = true; + assert_equals(lock, null, 'lock should not be granted'); + throw 123; + }); + assert_unreached('call should throw'); + } catch (ex) { + assert_equals(ex, 123, 'ex should be value thrown by callback'); + } + }); + assert_true(callback_called, 'callback should be called'); +}, 'Lock request with ifAvailable - lock not available, callback throws'); + +promise_test(async t => { + const res = self.uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, async lock => { + // Request with a different name - should be grantable. + await navigator.locks.request('different', {ifAvailable: true}, async lock => { + callback_called = true; + assert_not_equals(lock, null, 'lock should be granted'); + }); + }); + assert_true(callback_called, 'callback should be called'); +}, 'Lock request with ifAvailable - unrelated lock held'); + +promise_test(async t => { + const res = self.uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, {mode: 'shared'}, async lock => { + await navigator.locks.request( + res, {mode: 'shared', ifAvailable: true}, async lock => { + callback_called = true; + assert_not_equals(lock, null, 'lock should be granted'); + }); + }); + assert_true(callback_called, 'callback should be called'); +}, 'Shared lock request with ifAvailable - shared lock held'); + +promise_test(async t => { + const res = self.uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, {mode: 'shared'}, async lock => { + // Request would time out if |ifAvailable| was not specified. + await navigator.locks.request(res, {ifAvailable: true}, async lock => { + callback_called = true; + assert_equals(lock, null, 'lock should not be granted'); + }); + }); + assert_true(callback_called, 'callback should be called'); +}, 'Exclusive lock request with ifAvailable - shared lock held'); + +promise_test(async t => { + const res = self.uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, async lock => { + // Request would time out if |ifAvailable| was not specified. + await navigator.locks.request( + res, {mode: 'shared', ifAvailable: true}, async lock => { + callback_called = true; + assert_equals(lock, null, 'lock should not be granted'); + }); + }); + assert_true(callback_called, 'callback should be called'); +}, 'Shared lock request with ifAvailable - exclusive lock held'); + +promise_test(async t => { + const res = self.uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, async lock => { + callback_called = true; + const test_error = {name: 'test'}; + const p = navigator.locks.request( + res, {ifAvailable: true}, lock => { + assert_equals(lock, null, 'lock should not be available'); + throw test_error; + }); + assert_equals(Promise.resolve(p), p, 'request() result is a Promise'); + await promise_rejects_exactly(t, test_error, p, 'result should reject'); + }); + assert_true(callback_called, 'callback should be called'); +}, 'Returned Promise rejects if callback throws synchronously'); + +promise_test(async t => { + const res = self.uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, async lock => { + callback_called = true; + const test_error = {name: 'test'}; + const p = navigator.locks.request( + res, {ifAvailable: true}, async lock => { + assert_equals(lock, null, 'lock should not be available'); + throw test_error; + }); + assert_equals(Promise.resolve(p), p, 'request() result is a Promise'); + await promise_rejects_exactly(t, test_error, p, 'result should reject'); + }); + assert_true(callback_called, 'callback should be called'); +}, 'Returned Promise rejects if async callback yields rejected promise'); + +// Regression test for: https://crbug.com/840994 +promise_test(async t => { + const res1 = self.uniqueName(t); + const res2 = self.uniqueName(t); + let callback1_called = false; + await navigator.locks.request(res1, async lock => { + callback1_called = true; + let callback2_called = false; + await navigator.locks.request(res2, async lock => { + callback2_called = true; + }); + assert_true(callback2_called, 'callback2 should be called'); + + let callback3_called = false; + await navigator.locks.request(res2, {ifAvailable: true}, async lock => { + callback3_called = true; + // This request would fail if the "is this grantable?" test + // failed, e.g. due to the release without a pending request + // skipping steps. + assert_not_equals(lock, null, 'Lock should be available'); + }); + assert_true(callback3_called, 'callback2 should be called'); + }); + assert_true(callback1_called, 'callback1 should be called'); +}, 'Locks are available once previous release is processed'); diff --git a/testing/web-platform/tests/web-locks/lock-attributes.https.any.js b/testing/web-platform/tests/web-locks/lock-attributes.https.any.js new file mode 100644 index 0000000000..d1c03103e0 --- /dev/null +++ b/testing/web-platform/tests/web-locks/lock-attributes.https.any.js @@ -0,0 +1,18 @@ +// META: title=Web Locks API: Lock Attributes +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +promise_test(async t => { + await navigator.locks.request('resource', lock => { + assert_equals(lock.name, 'resource'); + assert_equals(lock.mode, 'exclusive'); + }); +}, 'Lock attributes reflect requested properties (exclusive)'); + +promise_test(async t => { + await navigator.locks.request('resource', {mode: 'shared'}, lock => { + assert_equals(lock.name, 'resource'); + assert_equals(lock.mode, 'shared'); + }); +}, 'Lock attributes reflect requested properties (shared)'); diff --git a/testing/web-platform/tests/web-locks/mode-exclusive.https.any.js b/testing/web-platform/tests/web-locks/mode-exclusive.https.any.js new file mode 100644 index 0000000000..8450e237c3 --- /dev/null +++ b/testing/web-platform/tests/web-locks/mode-exclusive.https.any.js @@ -0,0 +1,34 @@ +// META: title=Web Locks API: Exclusive Mode +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +promise_test(async t => { + const granted = []; + function log_grant(n) { return () => { granted.push(n); }; } + + await Promise.all([ + navigator.locks.request('a', log_grant(1)), + navigator.locks.request('a', log_grant(2)), + navigator.locks.request('a', log_grant(3)) + ]); + assert_array_equals(granted, [1, 2, 3]); +}, 'Lock requests are granted in order'); + +promise_test(async t => { + const granted = []; + function log_grant(n) { return () => { granted.push(n); }; } + + let inner_promise; + await navigator.locks.request('a', async lock => { + inner_promise = Promise.all([ + // This will be blocked. + navigator.locks.request('a', log_grant(1)), + // But this should be grantable immediately. + navigator.locks.request('b', log_grant(2)) + ]); + }); + + await inner_promise; + assert_array_equals(granted, [2, 1]); +}, 'Requests for distinct resources can be granted'); diff --git a/testing/web-platform/tests/web-locks/mode-mixed.https.any.js b/testing/web-platform/tests/web-locks/mode-mixed.https.any.js new file mode 100644 index 0000000000..5f34588693 --- /dev/null +++ b/testing/web-platform/tests/web-locks/mode-mixed.https.any.js @@ -0,0 +1,98 @@ +// META: title=Web Locks API: Mixed Modes +// META: script=resources/helpers.js +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +promise_test(async t => { + let unblock; + const blocked = new Promise(r => { unblock = r; }); + + const granted = []; + + // These should be granted immediately, and held until unblocked. + navigator.locks.request('a', {mode: 'shared'}, async lock => { + granted.push('a-shared-1'); await blocked; }); + navigator.locks.request('a', {mode: 'shared'}, async lock => { + granted.push('a-shared-2'); await blocked; }); + navigator.locks.request('a', {mode: 'shared'}, async lock => { + granted.push('a-shared-3'); await blocked; }); + + // This should be blocked. + let exclusive_lock; + const exclusive_request = navigator.locks.request('a', async lock => { + granted.push('a-exclusive'); + exclusive_lock = lock; + }); + + // This should be granted immediately (different name). + await navigator.locks.request('b', {mode: 'exclusive'}, lock => { + granted.push('b-exclusive'); }); + + assert_array_equals( + granted, ['a-shared-1', 'a-shared-2', 'a-shared-3', 'b-exclusive']); + + // Release the shared locks granted above. + unblock(); + + // Now the blocked request can be granted. + await exclusive_request; + assert_equals(exclusive_lock.mode, 'exclusive'); + + assert_array_equals( + granted, + ['a-shared-1', 'a-shared-2', 'a-shared-3', 'b-exclusive', 'a-exclusive']); + +}, 'Lock requests are granted in order'); + +promise_test(async t => { + const res = uniqueName(t); + + let [promise, resolve] = makePromiseAndResolveFunc(); + + const exclusive = navigator.locks.request(res, () => promise); + for (let i = 0; i < 5; i++) { + requestLockAndHold(t, res, { mode: "shared" }); + } + + let answer = await navigator.locks.query(); + assert_equals(answer.held.length, 1, "An exclusive lock is held"); + assert_equals(answer.pending.length, 5, "Requests for shared locks are pending"); + resolve(); + await exclusive; + + answer = await navigator.locks.query(); + assert_equals(answer.held.length, 5, "Shared locks are held"); + assert_true(answer.held.every(l => l.mode === "shared"), "All held locks are shared ones"); +}, 'Releasing exclusive lock grants multiple shared locks'); + +promise_test(async t => { + const res = uniqueName(t); + + let [sharedPromise, sharedResolve] = makePromiseAndResolveFunc(); + let [exclusivePromise, exclusiveResolve] = makePromiseAndResolveFunc(); + + const sharedReleasedPromise = Promise.all(new Array(5).fill(0).map( + () => navigator.locks.request(res, { mode: "shared" }, () => sharedPromise)) + ); + const exclusiveReleasedPromise = navigator.locks.request(res, () => exclusivePromise); + for (let i = 0; i < 5; i++) { + requestLockAndHold(t, res, { mode: "shared" }); + } + + let answer = await navigator.locks.query(); + assert_equals(answer.held.length, 5, "Shared locks are held"); + assert_true(answer.held.every(l => l.mode === "shared"), "All held locks are shared ones"); + sharedResolve(); + await sharedReleasedPromise; + + answer = await navigator.locks.query(); + assert_equals(answer.held.length, 1, "An exclusive lock is held"); + assert_equals(answer.held[0].mode, "exclusive"); + exclusiveResolve(); + await exclusiveReleasedPromise; + + answer = await navigator.locks.query(); + assert_equals(answer.held.length, 5, "The next shared locks are held"); + assert_true(answer.held.every(l => l.mode === "shared"), "All held locks are shared ones"); +}, 'An exclusive lock between shared locks'); diff --git a/testing/web-platform/tests/web-locks/mode-shared.https.any.js b/testing/web-platform/tests/web-locks/mode-shared.https.any.js new file mode 100644 index 0000000000..fc4a6012fa --- /dev/null +++ b/testing/web-platform/tests/web-locks/mode-shared.https.any.js @@ -0,0 +1,38 @@ +// META: title=Web Locks API: Shared Mode +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +promise_test(async t => { + const granted = []; + function log_grant(n) { return () => { granted.push(n); }; } + + await Promise.all([ + navigator.locks.request('a', {mode: 'shared'}, log_grant(1)), + navigator.locks.request('b', {mode: 'shared'}, log_grant(2)), + navigator.locks.request('c', {mode: 'shared'}, log_grant(3)), + navigator.locks.request('a', {mode: 'shared'}, log_grant(4)), + navigator.locks.request('b', {mode: 'shared'}, log_grant(5)), + navigator.locks.request('c', {mode: 'shared'}, log_grant(6)), + ]); + + assert_array_equals(granted, [1, 2, 3, 4, 5, 6]); +}, 'Lock requests are granted in order'); + +promise_test(async t => { + let a_acquired = false, a_acquired_again = false; + + await navigator.locks.request('a', {mode: 'shared'}, async lock => { + a_acquired = true; + + // Since lock is held, this request would be blocked if the + // lock was not 'shared', causing this test to time out. + + await navigator.locks.request('a', {mode: 'shared'}, lock => { + a_acquired_again = true; + }); + }); + + assert_true(a_acquired, 'first lock acquired'); + assert_true(a_acquired_again, 'second lock acquired'); +}, 'Shared locks are not exclusive'); diff --git a/testing/web-platform/tests/web-locks/non-fully-active.https.html b/testing/web-platform/tests/web-locks/non-fully-active.https.html new file mode 100644 index 0000000000..56a5372044 --- /dev/null +++ b/testing/web-platform/tests/web-locks/non-fully-active.https.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Web Locks API: Non-fully-active documents</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/helpers.js"></script> + +<div></div> + +<script> + function createNonFullyActiveIframe(src) { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + const { navigator, DOMException, postMessage } = iframe.contentWindow; + iframe.remove(); + return { iframe, navigator, DOMException, postMessage }; + } + + promise_test(async t => { + const { navigator, DOMException } = createNonFullyActiveIframe(); + const p = navigator.locks.request("foo", t.unreached_func()); + await promise_rejects_dom(t, "InvalidStateError", DOMException, p, "Request should explicitly fail"); + }, "request() on non-fully-active document must fail"); + + promise_test(async t => { + const { navigator, DOMException } = createNonFullyActiveIframe(); + const p = navigator.locks.query(); + await promise_rejects_dom(t, "InvalidStateError", DOMException, p, "Query should explicitly fail"); + }, "query() on a non-fully-active document must fail"); + + promise_test(async t => { + const { navigator, DOMException, postMessage } = createNonFullyActiveIframe(); + + const p = navigator.locks.request("-", t.unreached_func()); + await promise_rejects_dom(t, "InvalidStateError", DOMException, p, "Request should explicitly fail"); + }, "request()'s fully-active check happens earlier than name validation"); + + promise_test(async t => { + const { iframe, navigator, DOMException } = createNonFullyActiveIframe(); + document.body.append(iframe); + t.add_cleanup(() => iframe.remove()); + + // Appending should create a new browsing context with a new Navigator object + // https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element:insert-an-element-into-a-document + // https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object:associated-navigator + assert_not_equals(navigator, iframe.contentWindow.navigator, "Navigator object changes"); + assert_not_equals(navigator.locks, iframe.contentWindow.navigator.locks, "LockManager object changes"); + + const p = navigator.locks.request("foo", t.unreached_func()); + await promise_rejects_dom(t, "InvalidStateError", DOMException, p, "Request on the previous LockManager still must fail"); + }, "Reactivated iframe must not reuse the previous LockManager"); + + promise_test(async t => { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + const worker = new iframe.contentWindow.Worker("resources/worker.js"); + + const name = uniqueName(t); + await postToWorkerAndWait(worker, { op: 'request', name }); + + let query = await navigator.locks.query(); + assert_equals(query.held.length, 1, "One lock is present"); + + iframe.remove(); + + const lock = await navigator.locks.request(name, lock => lock); + assert_equals(lock.name, name, "The following lock should be processed"); + + query = await navigator.locks.query(); + assert_equals(query.held.length, 0, "No lock is present"); + }, "Workers owned by an unloaded iframe must release their locks"); +</script> diff --git a/testing/web-platform/tests/web-locks/non-secure-context.any.js b/testing/web-platform/tests/web-locks/non-secure-context.any.js new file mode 100644 index 0000000000..94553591f3 --- /dev/null +++ b/testing/web-platform/tests/web-locks/non-secure-context.any.js @@ -0,0 +1,14 @@ +// META: title=Web Locks API: API not available in non-secure context +// META: global=window,dedicatedworker,sharedworker + +'use strict'; + +test(t => { + assert_false(self.isSecureContext); + assert_false('locks' in navigator, + 'navigator.locks is only present in secure contexts'); + assert_false('LockManager' in self, + 'LockManager is only present in secure contexts'); + assert_false('Lock' in self, + 'Lock interface is only present in secure contexts'); +}, 'API presence in non-secure contexts'); diff --git a/testing/web-platform/tests/web-locks/opaque-origin.https.html b/testing/web-platform/tests/web-locks/opaque-origin.https.html new file mode 100644 index 0000000000..3ba636ecd8 --- /dev/null +++ b/testing/web-platform/tests/web-locks/opaque-origin.https.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Web Locks API: Opaque origins</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +function load_iframe(src, sandbox) { + return new Promise(resolve => { + const iframe = document.createElement('iframe'); + iframe.onload = () => { resolve(iframe); }; + if (sandbox) + iframe.sandbox = sandbox; + iframe.srcdoc = src; + iframe.style.display = 'none'; + document.documentElement.appendChild(iframe); + }); +} + +function wait_for_message(iframe) { + return new Promise(resolve => { + self.addEventListener('message', function listener(e) { + if (e.source === iframe.contentWindow) { + resolve(e.data); + self.removeEventListener('message', listener); + } + }); + }); +} + +const script = ` +<script> + "use strict"; + window.onmessage = async (ev) => { + try { + switch (ev.data) { + case "request": + await navigator.locks.request('name', lock => {}); + break; + case "query": + await navigator.locks.query(); + break; + default: + window.parent.postMessage({result: "unexpected message"}, "*"); + return; + } + window.parent.postMessage({result: "no exception"}, "*"); + } catch (ex) { + window.parent.postMessage({result: ex.name}, "*"); + }; + }; +<\/script> +`; + +promise_test(async t => { + const iframe = await load_iframe(script); + iframe.contentWindow.postMessage("request", '*'); + const message = await wait_for_message(iframe); + assert_equals(message.result, 'no exception', + 'navigator.locks.request() should not throw'); +}, 'navigator.locks.request() in non-sandboxed iframe should not throw'); + +promise_test(async t => { + const iframe = await load_iframe(script, 'allow-scripts'); + iframe.contentWindow.postMessage("request", '*'); + const message = await wait_for_message(iframe); + assert_equals(message.result, 'SecurityError', + 'Exception should be SecurityError'); +}, 'navigator.locks.request() in sandboxed iframe should throw SecurityError'); + +promise_test(async t => { + const iframe = await load_iframe(script); + iframe.contentWindow.postMessage("query", '*'); + const message = await wait_for_message(iframe); + assert_equals(message.result, 'no exception', + 'navigator.locks.request() should not throw'); +}, 'navigator.locks.query() in non-sandboxed iframe should not throw'); + +promise_test(async t => { + const iframe = await load_iframe(script, 'allow-scripts'); + iframe.contentWindow.postMessage("query", '*'); + const message = await wait_for_message(iframe); + assert_equals(message.result, 'SecurityError', + 'Exception should be SecurityError'); +}, 'navigator.locks.query() in sandboxed iframe should throw SecurityError'); +</script> diff --git a/testing/web-platform/tests/web-locks/partitioned-web-locks.tentative.https.html b/testing/web-platform/tests/web-locks/partitioned-web-locks.tentative.https.html new file mode 100644 index 0000000000..aa9c9b6984 --- /dev/null +++ b/testing/web-platform/tests/web-locks/partitioned-web-locks.tentative.https.html @@ -0,0 +1,172 @@ + +<!DOCTYPE html> +<meta charset="utf-8"/> +<title>Web Locks API: Partitioned WebLocks</title> + +<!-- Pull in get_host_info() --> +<script src="/common/get-host-info.sub.js"></script> +<script src="/common/utils.js"></script> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="resources/helpers.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + +const { HTTPS_ORIGIN, HTTPS_NOTSAMESITE_ORIGIN } = get_host_info(); +// Map of lock_id => function that releases a lock. +const held = new Map(); +let next_lock_id = 1; + +// How this test works: +// Step 1 (top-frame): request an exclusive web-lock and store its id +// and release for clean-up. +// Step 2 (top-frame): open a pop-up window and load a not-same-site +// ./web-locks/resources/partitioned-parent.html +// Step 3 (pop-up): load a same-site iframe inside the pop-up. +// Step 4 (pop-up): send a web-lock request to the same-site iframe. +// Step 5 (iframe): process the web-lock request and message the result +// back to the pop-up. +// Step 6 (pop-up): intercept the result message from the iframe and +// send it to the top-frame. +// Step 7 (top-frame): add cleanup hook. +// Step 8 (top-frame): ensure that the same-site iframe's web-lock +// request succeeds since it and the top-level site are successfully +// partitioned and each can hold an exclusive lock. + +async function third_party_test(t) { + let target_url = HTTPS_ORIGIN + '/web-locks/resources/iframe.html'; + target_url = new URL( + `/web-locks/resources/partitioned-parent.html?target=${encodeURIComponent(target_url)}`, + HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname); + + // Step 1. + let lock_id = next_lock_id++; + let [ promise, release ] = makePromiseAndResolveFunc(); + let released = navigator.locks.request('testLock', {mode: 'exclusive', ifAvailable: true}, + lock => { + if (lock === null) { + assert_true(false) + return; + } + return promise; + }); + held.set(lock_id, { release, released }); + + // Step 2. + const w = window.open(target_url); + const result = await new Promise(resolve => window.onmessage = resolve); + + // Step 7. + t.add_cleanup(() => { + w.close(); + let released = []; + for(let i = 1; i < next_lock_id; i++){ + let h = held.get(i); + h.release(); + released.push(h.released); + } + return Promise.allSettled(released); + }); + + // Step 8. + // When 3rd party storage partitioning is enabled, the iframe should be able + // to acquire a lock with the same name as one exclusively held by the opener + // of its top window, even when that opener has the same origin. + assert_equals(result.data.failed, undefined, + 'The 1p iframe failed to acquire the lock'); +} + +promise_test(t => { + return third_party_test(t); +}, 'WebLocks of an iframe under a 3rd-party site are partitioned'); + + +// Optional Test: Checking for partitioned web locks in an A->B->A +// (nested-iframe with cross-site ancestor chain) scenario. +// +// How this test works: +// Nested Step 1 (top frame): request an exclusive web-lock and +// store its id and release for clean-up. +// Nested Step 2 (top frame): open a pop-up window and load a +// same-site /web-locks/resources/partitioned-parent.html. +// Nested Step 3 (pop-up): load a not-same-site "parent" iframe (A->B) +// (/web-locks/resources/iframe-parent.html) inside the pop-up. +// Nested Step 4 (pop-up): send a web-lock request to the parent iframe. +// Nested Step 5 (parent iframe): load a "child" iframe (A->B->A) +// (/web-locks/resources/iframe.html) that is same-site with the +// pop-up inside the "parent" iframe. +// Nested Step 6 (parent iframe): pass on the web-lock request message to +// the "child" iframe. +// Nested Step 7 (child iframe): process the web-lock request and message +// the result to the parent iframe. +// Nested Step 8 (parent iframe): intercept the result message from the +// child iframe and send it to the pop-up. +// Nested Step 9 (pop-up): intercept the result message from the parent +// iframe and send it to the top frame. +// Nested Step 10 (top frame): add cleanup hook +// Nested Step 11 (top frame): ensure that the same-site iframe's web-lock +// request succeeds since it and the top-level are successfully +// partitioned and each can hold an exclusive lock. + +// Map of lock_id => function that releases a lock. +const held_2 = new Map(); +let next_lock_id_2 = 1; + +async function nested_iframe_test(t) { + // Create innermost child iframe (leaf). + let leaf_url = HTTPS_ORIGIN + '/web-locks/resources/iframe.html'; + // Wrap the child iframe in its cross-origin parent (middle). + let middle_url = new URL( + `/web-locks/resources/iframe-parent.html?target=${encodeURIComponent(leaf_url)}`, + HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname); + // Embed the parent iframe in the top-level site (top). + let top_url = new URL( + `/web-locks/resources/partitioned-parent.html?target=${encodeURIComponent(middle_url)}`, + HTTPS_ORIGIN + self.location.pathname); + + // Nested Step 1. + // Request the weblock for the top-level site. + let lock_id = next_lock_id_2++; + let [ promise, release ] = makePromiseAndResolveFunc(); + let released = navigator.locks.request('testLock', {mode: 'exclusive', ifAvailable: true}, + lock => { + if (lock === null) { + assert_true(false) + return; + } + return promise; + }).catch(error => alert(error.message)); + held_2.set(lock_id, { release, released }); + + // Nested Step 2. + // Open the nested iframes. The script in the innermost child iframe + // will attempt to obtain the same weblock as above. + const w = window.open(top_url); + const result = await new Promise(resolve => window.onmessage = resolve); + + // Nested Step 10. + t.add_cleanup(() => { + w.close(); + let released = []; + for(let i = 1; i < next_lock_id; i++){ + let h = held_2.get(i); + h.release(); + released.push(h.released); + } + return Promise.allSettled(released); + }); + + // Nested Step 11. + // With third-party storage partitioning enabled, the same-site iframe + // should be able to acquire the lock as it has a cross-site ancestor + // and is partitioned separately from the top-level site. + assert_equals(result.data.failed, undefined, + 'The 1p iframe failed to acquire the lock'); +} + +promise_test(t => { + return nested_iframe_test(t); +}, 'WebLocks of a nested iframe with a cross-site ancestor are partitioned'); +</script> +</body> diff --git a/testing/web-platform/tests/web-locks/query-empty.https.any.js b/testing/web-platform/tests/web-locks/query-empty.https.any.js new file mode 100644 index 0000000000..88ffdb7f81 --- /dev/null +++ b/testing/web-platform/tests/web-locks/query-empty.https.any.js @@ -0,0 +1,18 @@ +// META: title=Web Locks API: navigator.locks.query method - no locks held +// META: script=resources/helpers.js +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +promise_test(async t => { + const state = await navigator.locks.query(); + + assert_own_property(state, 'pending', 'State has `pending` property'); + assert_true(Array.isArray(state.pending), + 'State `pending` property is an array'); + assert_array_equals(state.pending, [], 'Pending array is empty'); + + assert_own_property(state, 'held', 'State has `held` property'); + assert_true(Array.isArray(state.held), 'State `held` property is an array'); + assert_array_equals(state.held, [], 'Held array is empty'); +}, 'query() returns dictionary with empty arrays when no locks are held'); diff --git a/testing/web-platform/tests/web-locks/query-ordering.https.html b/testing/web-platform/tests/web-locks/query-ordering.https.html new file mode 100644 index 0000000000..d5e722baf7 --- /dev/null +++ b/testing/web-platform/tests/web-locks/query-ordering.https.html @@ -0,0 +1,131 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Web Locks API: navigator.locks.query ordering</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/helpers.js"></script> +<style>iframe { display: none; }</style> +<script> +'use strict'; + +// Grab a lock and hold until a release function is called. Resolves +// to a release function. +function getLockAndHoldUntilReleased(name, options) { + let release; + const promise = new Promise(resolve => { release = resolve; }); + return new Promise(resolve => { + navigator.locks.request(name, options || {}, lock => { + resolve(release); + return promise; + }).catch(_ => {}); + }); +} + +// Returns a promise resolved by the next message event. +function nextMessage() { + return new Promise(resolve => { + window.addEventListener('message', event => { + resolve(event.data); + }, {once: true}); + }); +} + +// Tests the ordering constraints on the requested lock state returned by +// navigator.locks.query(). Three separate iframes are instantiated to make +// lock requests on the same resource, first in one order and then in another, +// different order. For each set of requests, it is verified that the requests +// appear in the result of navigator.locks.query() in the same order in which +// they were made. +// +// It is necessary to use separate iframes here so that the lock requests have +// distinguishable client_ids (otherwise it would not be possible to +// distinguish the requests and thus impossible to verify ordering). +promise_test(async testCase => { + assert_implements(navigator.locks); + const resourceName = uniqueName(testCase); + + // Set up clients. + const frame1 = await iframe('resources/iframe.html'); + const frame2 = await iframe('resources/iframe.html'); + const frame3 = await iframe('resources/iframe.html'); + testCase.add_cleanup(() => { frame1.remove(); }); + testCase.add_cleanup(() => { frame2.remove(); }); + testCase.add_cleanup(() => { frame3.remove(); }); + + // Collect the client ids. + const clientId1 = + (await postToFrameAndWait(frame1, {op: 'client_id', + name: resourceName})).client_id; + const clientId2 = + (await postToFrameAndWait(frame2, {op: 'client_id', + name: resourceName})).client_id; + const clientId3 = + (await postToFrameAndWait(frame3, {op: 'client_id', + name: resourceName})).client_id; + + // Preemptively take the lock. + const firstRequestGroupReleaseFunction = + await getLockAndHoldUntilReleased(resourceName); + + // Queue the first group of lock requests from the different clients. These + // will be blocked until firstRequestGroupReleaseFunction() is called. + let lockId1; + let lockId2; + const lockPromise1 = + postToFrameAndWait(frame1, {op: 'request', name: resourceName}) + .then(val => {lockId1 = val.lock_id;}); + const lockPromise2 = + postToFrameAndWait(frame2, {op: 'request', name: resourceName}) + .then(val => {lockId2 = val.lock_id;}); + + // This third request will later be granted and held in order to block a + // second group of requests to test a different client ordering. It is not + // meant to be released. + postToFrameAndWait(frame3, {op: 'request', name: resourceName}); + + // Request and wait for the release of a separate lock to ensure all previous + // requests are processed. + const checkpointName = uniqueName(testCase, 'checkpoint'); + const checkpointId = (await postToFrameAndWait( + frame3, + {op: 'request', name: checkpointName})).lock_id; + await postToFrameAndWait(frame3, {op: 'release', lock_id: checkpointId}); + + // Query the state and test the ordering of requested locks. + const state = await navigator.locks.query(); + const relevant_pending_ids = state.pending + .filter(lock => [clientId1, clientId2, clientId3].includes(lock.clientId)) + .map(lock => lock.clientId); + assert_array_equals( + [clientId1, clientId2, clientId3], + relevant_pending_ids, + 'Querying the state should return requested locks in the order they were ' + + 'requested.'); + + // Add the second group of requests from the clients in a new order. + postToFrameAndWait(frame3, {op: 'request', name: resourceName}); + postToFrameAndWait(frame1, {op: 'request', name: resourceName}); + postToFrameAndWait(frame2, {op: 'request', name: resourceName}); + + // Release locks such that only the newly added locks are requested. This + // acts like a checkpoint for the newly queued requests. + firstRequestGroupReleaseFunction(); + await lockPromise1; + await postToFrameAndWait(frame1, {op: 'release', lock_id: lockId1}); + await lockPromise2; + await postToFrameAndWait(frame2, {op: 'release', lock_id: lockId2}); + + // Query the state and test the new ordering. + const state2 = await navigator.locks.query(); + const relevant_pending_ids2 = state2.pending + .filter(lock => [clientId1, clientId2, clientId3].includes(lock.clientId)) + .map(lock => lock.clientId); + assert_array_equals( + [clientId3, clientId1, clientId2], + relevant_pending_ids2, + 'Querying the state should return requested locks in the order they were ' + + 'requested.'); + +}, 'Requests appear in state in order made.'); +</script> diff --git a/testing/web-platform/tests/web-locks/query.https.any.js b/testing/web-platform/tests/web-locks/query.https.any.js new file mode 100644 index 0000000000..14fdeca7a4 --- /dev/null +++ b/testing/web-platform/tests/web-locks/query.https.any.js @@ -0,0 +1,227 @@ +// META: title=Web Locks API: navigator.locks.query method +// META: script=resources/helpers.js + +'use strict'; + +// Returns an array of the modes for the locks with matching name. +function modes(list, name) { + return list.filter(item => item.name === name).map(item => item.mode); +} +// Returns an array of the clientIds for the locks with matching name. +function clients(list, name) { + return list.filter(item => item.name === name).map(item => item.clientId); +} + +promise_test(async t => { + const res = uniqueName(t); + + await navigator.locks.request(res, async lock1 => { + // Attempt to request this again - should be blocked. + let lock2_acquired = false; + navigator.locks.request(res, lock2 => { lock2_acquired = true; }); + + // Verify that it was blocked. + await navigator.locks.request(res, {ifAvailable: true}, async lock3 => { + assert_false(lock2_acquired, 'second request should be blocked'); + assert_equals(lock3, null, 'third request should have failed'); + + const state = await navigator.locks.query(); + + assert_own_property(state, 'pending', 'State has `pending` property'); + assert_true(Array.isArray(state.pending), + 'State `pending` property is an array'); + const pending_info = state.pending[0]; + assert_own_property(pending_info, 'name', + 'Pending info dictionary has `name` property'); + assert_own_property(pending_info, 'mode', + 'Pending info dictionary has `mode` property'); + assert_own_property(pending_info, 'clientId', + 'Pending info dictionary has `clientId` property'); + + assert_own_property(state, 'held', 'State has `held` property'); + assert_true(Array.isArray(state.held), + 'State `held` property is an array'); + const held_info = state.held[0]; + assert_own_property(held_info, 'name', + 'Held info dictionary has `name` property'); + assert_own_property(held_info, 'mode', + 'Held info dictionary has `mode` property'); + assert_own_property(held_info, 'clientId', + 'Held info dictionary has `clientId` property'); + }); + }); +}, 'query() returns dictionaries with expected properties'); + + + +promise_test(async t => { + const res = uniqueName(t); + + await navigator.locks.request(res, async lock1 => { + const state = await navigator.locks.query(); + assert_array_equals(modes(state.held, res), ['exclusive'], + 'Held lock should appear once'); + }); + + await navigator.locks.request(res, {mode: 'shared'}, async lock1 => { + const state = await navigator.locks.query(); + assert_array_equals(modes(state.held, res), ['shared'], + 'Held lock should appear once'); + }); +}, 'query() reports individual held locks'); + +promise_test(async t => { + const res1 = uniqueName(t); + const res2 = uniqueName(t); + + await navigator.locks.request(res1, async lock1 => { + await navigator.locks.request(res2, {mode: 'shared'}, async lock2 => { + const state = await navigator.locks.query(); + assert_array_equals(modes(state.held, res1), ['exclusive'], + 'Held lock should appear once'); + assert_array_equals(modes(state.held, res2), ['shared'], + 'Held lock should appear once'); + }); + }); +}, 'query() reports multiple held locks'); + +promise_test(async t => { + const res = uniqueName(t); + + await navigator.locks.request(res, async lock1 => { + // Attempt to request this again - should be blocked. + let lock2_acquired = false; + navigator.locks.request(res, lock2 => { lock2_acquired = true; }); + + // Verify that it was blocked. + await navigator.locks.request(res, {ifAvailable: true}, async lock3 => { + assert_false(lock2_acquired, 'second request should be blocked'); + assert_equals(lock3, null, 'third request should have failed'); + + const state = await navigator.locks.query(); + assert_array_equals(modes(state.pending, res), ['exclusive'], + 'Pending lock should appear once'); + assert_array_equals(modes(state.held, res), ['exclusive'], + 'Held lock should appear once'); + }); + }); +}, 'query() reports pending and held locks'); + +promise_test(async t => { + const res = uniqueName(t); + + await navigator.locks.request(res, {mode: 'shared'}, async lock1 => { + await navigator.locks.request(res, {mode: 'shared'}, async lock2 => { + const state = await navigator.locks.query(); + assert_array_equals(modes(state.held, res), ['shared', 'shared'], + 'Held lock should appear twice'); + }); + }); +}, 'query() reports held shared locks with appropriate count'); + +promise_test(async t => { + const res = uniqueName(t); + + await navigator.locks.request(res, async lock1 => { + let lock2_acquired = false, lock3_acquired = false; + navigator.locks.request(res, {mode: 'shared'}, + lock2 => { lock2_acquired = true; }); + navigator.locks.request(res, {mode: 'shared'}, + lock3 => { lock3_acquired = true; }); + + await navigator.locks.request(res, {ifAvailable: true}, async lock4 => { + assert_equals(lock4, null, 'lock should not be available'); + assert_false(lock2_acquired, 'second attempt should be blocked'); + assert_false(lock3_acquired, 'third attempt should be blocked'); + + const state = await navigator.locks.query(); + assert_array_equals(modes(state.held, res), ['exclusive'], + 'Held lock should appear once'); + + assert_array_equals(modes(state.pending, res), ['shared', 'shared'], + 'Pending lock should appear twice'); + }); + }); +}, 'query() reports pending shared locks with appropriate count'); + +promise_test(async t => { + const res1 = uniqueName(t); + const res2 = uniqueName(t); + + await navigator.locks.request(res1, async lock1 => { + await navigator.locks.request(res2, async lock2 => { + const state = await navigator.locks.query(); + + const res1_clients = clients(state.held, res1); + const res2_clients = clients(state.held, res2); + + assert_equals(res1_clients.length, 1, 'Each lock should have one holder'); + assert_equals(res2_clients.length, 1, 'Each lock should have one holder'); + + assert_array_equals(res1_clients, res2_clients, + 'Both locks should have same clientId'); + }); + }); +}, 'query() reports the same clientId for held locks from the same context'); + +promise_test(async t => { + const res = uniqueName(t); + + const worker = new Worker('resources/worker.js'); + t.add_cleanup(() => { worker.terminate(); }); + + await postToWorkerAndWait( + worker, {op: 'request', name: res, mode: 'shared'}); + + await navigator.locks.request(res, {mode: 'shared'}, async lock => { + const state = await navigator.locks.query(); + const res_clients = clients(state.held, res); + assert_equals(res_clients.length, 2, 'Clients should have same resource'); + assert_not_equals(res_clients[0], res_clients[1], + 'Clients should have different ids'); + }); +}, 'query() reports different ids for held locks from different contexts'); + +promise_test(async t => { + const res1 = uniqueName(t); + const res2 = uniqueName(t); + + const worker = new Worker('resources/worker.js'); + t.add_cleanup(() => { worker.terminate(); }); + + // Acquire 1 in the worker. + await postToWorkerAndWait(worker, {op: 'request', name: res1}) + + // Acquire 2 here. + await new Promise(resolve => { + navigator.locks.request(res2, lock => { + resolve(); + return new Promise(() => {}); // Never released. + }); + }); + + // Request 2 in the worker. + postToWorkerAndWait(worker, {op: 'request', name: res2}); + assert_true((await postToWorkerAndWait(worker, { + op: 'request', name: res2, ifAvailable: true + })).failed, 'Lock request should have failed'); + + // Request 1 here. + navigator.locks.request( + res1, t.unreached_func('Lock should not be acquired')); + + // Verify that we're seeing a deadlock. + const state = await navigator.locks.query(); + const res1_held_clients = clients(state.held, res1); + const res2_held_clients = clients(state.held, res2); + const res1_pending_clients = clients(state.pending, res1); + const res2_pending_clients = clients(state.pending, res2); + + assert_equals(res1_held_clients.length, 1); + assert_equals(res2_held_clients.length, 1); + assert_equals(res1_pending_clients.length, 1); + assert_equals(res2_pending_clients.length, 1); + + assert_equals(res1_held_clients[0], res2_pending_clients[0]); + assert_equals(res2_held_clients[0], res1_pending_clients[0]); +}, 'query() can observe a deadlock'); diff --git a/testing/web-platform/tests/web-locks/resource-names.https.any.js b/testing/web-platform/tests/web-locks/resource-names.https.any.js new file mode 100644 index 0000000000..1031b3f7ba --- /dev/null +++ b/testing/web-platform/tests/web-locks/resource-names.https.any.js @@ -0,0 +1,56 @@ +// META: title=Web Locks API: Resources DOMString edge cases +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +function code_points(s) { + return [...s] + .map(c => '0x' + c.charCodeAt(0).toString(16).toUpperCase()) + .join(' '); +} + +[ + '', // Empty strings + 'abc\x00def', // Embedded NUL + '\uD800', // Unpaired low surrogage + '\uDC00', // Unpaired high surrogage + '\uDC00\uD800', // Swapped surrogate pair + '\uFFFF' // Non-character +].forEach(string => { + promise_test(async t => { + await navigator.locks.request(string, lock => { + assert_equals(lock.name, string, + 'Requested name matches granted name'); + }); + }, 'DOMString: ' + code_points(string)); +}); + +promise_test(async t => { + // '\uD800' treated as a USVString would become '\uFFFD'. + await navigator.locks.request('\uD800', async lock => { + assert_equals(lock.name, '\uD800'); + + // |lock| is held for the duration of this name. It + // Should not block acquiring |lock2| with a distinct + // DOMString. + await navigator.locks.request('\uFFFD', lock2 => { + assert_equals(lock2.name, '\uFFFD'); + }); + + // If we did not time out, this passed. + }); +}, 'Resource names that are not valid UTF-16 are not mangled'); + +promise_test(async t => { + for (const name of ['-', '-foo']) { + await promise_rejects_dom( + t, 'NotSupportedError', + navigator.locks.request(name, lock => {}), + 'Names starting with "-" should be rejected'); + } + let got_lock = false; + await navigator.locks.request('x-anything', lock => { + got_lock = true; + }); + assert_true(got_lock, 'Names with embedded "-" should be accepted'); +}, 'Names cannot start with "-"'); diff --git a/testing/web-platform/tests/web-locks/resources/helpers.js b/testing/web-platform/tests/web-locks/resources/helpers.js new file mode 100644 index 0000000000..3fb89711ab --- /dev/null +++ b/testing/web-platform/tests/web-locks/resources/helpers.js @@ -0,0 +1,91 @@ +// Test helpers used by multiple Web Locks API tests. +(() => { + + // Generate a unique resource identifier, using the script path and + // test case name. This is useful to avoid lock interference between + // test cases. + let res_num = 0; + self.uniqueName = (testCase, prefix) => { + return `${self.location.pathname}-${prefix}-${testCase.name}-${++res_num}`; + }; + self.uniqueNameByQuery = () => { + const prefix = new URL(location.href).searchParams.get('prefix'); + return `${prefix}-${++res_num}`; + } + + // Inject an iframe showing the given url into the page, and resolve + // the returned promise when the frame is loaded. + self.iframe = url => new Promise(resolve => { + const element = document.createElement('iframe'); + element.addEventListener( + 'load', () => { resolve(element); }, { once: true }); + element.src = url; + document.documentElement.appendChild(element); + }); + + // Post a message to the target frame, and resolve the returned + // promise when a response comes back. The posted data is annotated + // with unique id to track the response. This assumes the use of + // 'iframe.html' as the frame, which implements this protocol. + let next_request_id = 0; + self.postToFrameAndWait = (frame, data) => { + const iframe_window = frame.contentWindow; + data.rqid = next_request_id++; + iframe_window.postMessage(data, '*'); + return new Promise(resolve => { + const listener = event => { + if (event.source !== iframe_window || event.data.rqid !== data.rqid) + return; + self.removeEventListener('message', listener); + resolve(event.data); + }; + self.addEventListener('message', listener); + }); + }; + + // Post a message to the target worker, and resolve the returned + // promise when a response comes back. The posted data is annotated + // with unique id to track the response. This assumes the use of + // 'worker.js' as the worker, which implements this protocol. + self.postToWorkerAndWait = (worker, data) => { + return new Promise(resolve => { + data.rqid = next_request_id++; + worker.postMessage(data); + const listener = event => { + if (event.data.rqid !== data.rqid) + return; + worker.removeEventListener('message', listener); + resolve(event.data); + }; + worker.addEventListener('message', listener); + }); + }; + + /** + * Request a lock and hold it until the subtest ends. + * @param {*} t test runner object + * @param {string} name lock name + * @param {LockOptions=} options lock options + * @returns + */ + self.requestLockAndHold = (t, name, options = {}) => { + let [promise, resolve] = self.makePromiseAndResolveFunc(); + const released = navigator.locks.request(name, options, () => promise); + // Add a cleanup function that releases the lock by resolving the promise, + // and then waits until the lock is really released, to avoid contaminating + // following tests with temporarily held locks. + t.add_cleanup(() => { + resolve(); + // Cleanup shouldn't fail if the request is aborted. + return released.catch(() => undefined); + }); + return released; + }; + + self.makePromiseAndResolveFunc = () => { + let resolve; + const promise = new Promise(r => { resolve = r; }); + return [promise, resolve]; + }; + +})(); diff --git a/testing/web-platform/tests/web-locks/resources/iframe-parent.html b/testing/web-platform/tests/web-locks/resources/iframe-parent.html new file mode 100644 index 0000000000..ec63045b4a --- /dev/null +++ b/testing/web-platform/tests/web-locks/resources/iframe-parent.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<title>Helper IFrame</title> +<script> +'use strict'; + +async function onLoad() { + // Nested Step 5: wpt/web-locks/partitioned-web-locks.tentative.https.html + // Load the innermost child iframe and its content. + const params = new URLSearchParams(self.location.search); + const frame = document.createElement('iframe'); + frame.src = params.get('target'); + document.body.appendChild(frame); + + self.addEventListener('message', evt => { + // Nested Step 6: wpt/web-locks/partitioned-web-locks.tentative.https.html + // Pass any operations request messages to the + // innermost child iframe. + if (evt.data.op){ + // Ensure that the iframe has loaded before passing + // on the message. + frame.addEventListener('load', function(){ + frame.contentWindow.postMessage(evt.data, '*'); + }); + } + // Nested Step 8: wpt/web-locks/partitioned-web-locks.tentative.https.html + else { + // All other messages, should be sent back to the + // top-level site. + if (self.opener) + self.opener.postMessage(evt.data, '*'); + else + self.top.postMessage(evt.data, '*'); + } + }); +} +self.addEventListener('load', onLoad); +</script> diff --git a/testing/web-platform/tests/web-locks/resources/iframe.html b/testing/web-platform/tests/web-locks/resources/iframe.html new file mode 100644 index 0000000000..ba63c77bae --- /dev/null +++ b/testing/web-platform/tests/web-locks/resources/iframe.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<title>Helper IFrame</title> +<script> +'use strict'; + +// Map of lock_id => function that releases a lock. + +const held = new Map(); +let next_lock_id = 1; + +self.addEventListener('message', e => { + function respond(data) { + parent.postMessage(Object.assign(data, {rqid: e.data.rqid}), '*'); + } + + switch (e.data.op) { + case 'request': + navigator.locks.request( + e.data.name, { + mode: e.data.mode || 'exclusive', + ifAvailable: e.data.ifAvailable || false + }, lock => { + if (lock === null) { + respond({ack: 'request', failed: true}); + return; + } + let lock_id = next_lock_id++; + let release; + const promise = new Promise(r => { release = r; }); + held.set(lock_id, release); + respond({ack: 'request', lock_id: lock_id}); + return promise + }); + break; + + case 'release': + held.get(e.data.lock_id)(); + held.delete(e.data.lock_id); + respond({ack: 'release', lock_id: e.data.lock_id}); + break; + + case 'client_id': + navigator.locks.request(e.data.name, async lock => { + const lock_state = await navigator.locks.query(); + const held_lock = + lock_state.held.filter(l => l.name === lock.name)[0]; + respond({ack: 'client_id', client_id: held_lock.clientId}); + }); + break; + } +}); +</script> diff --git a/testing/web-platform/tests/web-locks/resources/parentworker.js b/testing/web-platform/tests/web-locks/resources/parentworker.js new file mode 100644 index 0000000000..2b2b2c2028 --- /dev/null +++ b/testing/web-platform/tests/web-locks/resources/parentworker.js @@ -0,0 +1,10 @@ +// Just transparently forwards things to the child worker + +importScripts("/web-locks/resources/helpers.js"); +const worker = new Worker("/web-locks/resources/worker.js"); + +self.addEventListener("message", async ev => { + const data = await postToWorkerAndWait(worker, ev.data); + data.rqid = ev.data.rqid; + postMessage(data); +}); diff --git a/testing/web-platform/tests/web-locks/resources/partitioned-parent.html b/testing/web-platform/tests/web-locks/resources/partitioned-parent.html new file mode 100644 index 0000000000..ec19c8dbaa --- /dev/null +++ b/testing/web-platform/tests/web-locks/resources/partitioned-parent.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta charset="utf-8"/> +<meta name="referrer" content="origin"> +<script> +async function onLoad() { + // Step 6 and Nested Step 9: + // wpt/web-locks/partitioned-web-locks.tentative.https.html + self.addEventListener('message', evt => { + if (self.opener) + self.opener.postMessage(evt.data, '*'); + else + self.top.postMessage(evt.data, '*'); + }, { once: true }); + + // Step 3 and Nested Step 3: + // wpt/web-locks/partitioned-web-locks.tentative.https.html + const params = new URLSearchParams(self.location.search); + const frame = document.createElement('iframe'); + frame.src = params.get('target'); + document.body.appendChild(frame); + + // Step 4 and Nested Step 4: + // wpt/web-locks/partitioned-web-locks.tentative.https.html + frame.addEventListener('load', function(){ + frame.contentWindow.postMessage({op: 'request', + name: 'testLock', ifAvailable: true}, '*'); + }); +} +self.addEventListener('load', onLoad); +</script> diff --git a/testing/web-platform/tests/web-locks/resources/service-worker.js b/testing/web-platform/tests/web-locks/resources/service-worker.js new file mode 100644 index 0000000000..027863e33e --- /dev/null +++ b/testing/web-platform/tests/web-locks/resources/service-worker.js @@ -0,0 +1,7 @@ +// Responds to '/clientId' with the request's clientId. +self.addEventListener('fetch', e => { + if (new URL(e.request.url).pathname === '/clientId') { + e.respondWith(new Response(JSON.stringify({clientId: e.clientId}))); + return; + } +}); diff --git a/testing/web-platform/tests/web-locks/resources/sw-controlled-iframe.html b/testing/web-platform/tests/web-locks/resources/sw-controlled-iframe.html new file mode 100644 index 0000000000..bc5c9bdb83 --- /dev/null +++ b/testing/web-platform/tests/web-locks/resources/sw-controlled-iframe.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>iframe used in clientId test</title> +<script> + +self.onmessage = async event => { + try { + if (event.data === 'get_sw_client_id') { + // Use the controlling service worker to determine + // this client's id according to the Service Worker. + const response = await fetch('/clientId'); + const data = await response.json(); + window.parent.postMessage(data.clientId, '*'); + return; + } + + if (event.data === 'get_lock_client_id') { + // Grab a lock, then query the lock manager for state to + // determine this client's id according to the lock manager. + await navigator.locks.request('lock-name', async lock => { + const lock_state = await navigator.locks.query(); + const held_lock = lock_state.held.filter(l => l.name === lock.name)[0]; + window.parent.postMessage(held_lock.clientId, '*'); + }); + return; + } + + window.parent.postMessage(`unknown request: ${event.data}`, '*'); + } catch (ex) { + // In case of test failure, don't leave parent window hanging. + window.parent.postMessage(`${ex.name}: ${ex.message}`, '*'); + } +}; + +</script> diff --git a/testing/web-platform/tests/web-locks/resources/worker.js b/testing/web-platform/tests/web-locks/resources/worker.js new file mode 100644 index 0000000000..cc71631ba6 --- /dev/null +++ b/testing/web-platform/tests/web-locks/resources/worker.js @@ -0,0 +1,56 @@ +'use strict'; + +// Map of id => function that releases a lock. + +const held = new Map(); +let next_lock_id = 1; + +function processMessage(e) { + const target = this; + + function respond(data) { + target.postMessage(Object.assign(data, {rqid: e.data.rqid})); + } + + switch (e.data.op) { + case 'request': { + const controller = new AbortController(); + navigator.locks.request( + e.data.name, { + mode: e.data.mode || 'exclusive', + ifAvailable: e.data.ifAvailable || false, + signal: e.data.abortImmediately ? controller.signal : undefined, + }, lock => { + if (lock === null) { + respond({ack: 'request', failed: true}); + return; + } + let lock_id = next_lock_id++; + let release; + const promise = new Promise(r => { release = r; }); + held.set(lock_id, release); + respond({ack: 'request', lock_id: lock_id}); + return promise; + }).catch(e => { + respond({ack: 'request', error: e.name}); + }); + if (e.data.abortImmediately) { + controller.abort(); + } + break; + } + + case 'release': + held.get(e.data.lock_id)(); + held.delete(e.data.lock_id); + respond({ack: 'release', lock_id: e.data.lock_id}); + break; + } +} + +self.addEventListener('message', processMessage); + +self.addEventListener('connect', ev => { + // Shared worker case + ev.ports[0].onmessage = processMessage; +}); diff --git a/testing/web-platform/tests/web-locks/secure-context.https.any.js b/testing/web-platform/tests/web-locks/secure-context.https.any.js new file mode 100644 index 0000000000..29ae7aea47 --- /dev/null +++ b/testing/web-platform/tests/web-locks/secure-context.https.any.js @@ -0,0 +1,14 @@ +// META: title=Web Locks API: API requires secure context +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +test(t => { + assert_true(self.isSecureContext); + assert_idl_attribute(navigator, 'locks', + 'navigator.locks exists in secure context'); + assert_true('LockManager' in self, + 'LockManager is present in secure contexts'); + assert_true('Lock' in self, + 'Lock interface is present in secure contexts'); +}, 'API presence in secure contexts'); diff --git a/testing/web-platform/tests/web-locks/signal.https.any.js b/testing/web-platform/tests/web-locks/signal.https.any.js new file mode 100644 index 0000000000..5a37e3ae87 --- /dev/null +++ b/testing/web-platform/tests/web-locks/signal.https.any.js @@ -0,0 +1,261 @@ +// META: title=Web Locks API: AbortSignal integration +// META: script=resources/helpers.js +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +promise_test(async t => { + const res = uniqueName(t); + + // These cases should not work: + for (const signal of ['string', 12.34, false, {}, Symbol(), () => {}, self]) { + await promise_rejects_js( + t, TypeError, + navigator.locks.request( + res, {signal}, t.unreached_func('callback should not run')), + 'Bindings should throw if the signal option is a not an AbortSignal'); + } +}, 'The signal option must be an AbortSignal'); + +promise_test(async t => { + const res = uniqueName(t); + const controller = new AbortController(); + controller.abort(); + + await promise_rejects_dom( + t, 'AbortError', + navigator.locks.request(res, {signal: controller.signal}, + t.unreached_func('callback should not run')), + 'Request should reject with AbortError'); +}, 'Passing an already aborted signal aborts'); + +promise_test(async t => { + const res = uniqueName(t); + + const controller = new AbortController(); + const reason = 'My dog ate it.'; + controller.abort(reason); + + const promise = + navigator.locks.request(res, {signal: controller.signal}, + t.unreached_func('callback should not run')); + + await promise_rejects_exactly( + t, reason, promise, "Rejection should give the abort reason"); +}, 'Passing an already aborted signal rejects with the custom abort reason.'); + +promise_test(async t => { + const res = uniqueName(t); + + const controller = new AbortController(); + controller.abort(); + + const promise = + navigator.locks.request(res, {signal: controller.signal}, + t.unreached_func('callback should not run')); + + await promise_rejects_exactly( + t, controller.signal.reason, promise, + "Rejection should give the abort reason"); +}, 'Passing an already aborted signal rejects with the default abort reason.'); + +promise_test(async t => { + const res = uniqueName(t); + + // Grab a lock and hold it until this subtest completes. + requestLockAndHold(t, res); + + const controller = new AbortController(); + + const promise = + navigator.locks.request(res, {signal: controller.signal}, + t.unreached_func('callback should not run')); + + // Verify the request is enqueued: + const state = await navigator.locks.query(); + assert_equals(state.held.filter(lock => lock.name === res).length, 1, + 'Number of held locks'); + assert_equals(state.pending.filter(lock => lock.name === res).length, 1, + 'Number of pending locks'); + + const rejected = promise_rejects_dom( + t, 'AbortError', promise, 'Request should reject with AbortError'); + + controller.abort(); + + await rejected; + +}, 'An aborted request results in AbortError'); + +promise_test(async t => { + const res = uniqueName(t); + + // Grab a lock and hold it until this subtest completes. + requestLockAndHold(t, res); + + const controller = new AbortController(); + + const promise = + navigator.locks.request(res, {signal: controller.signal}, lock => {}); + + // Verify the request is enqueued: + const state = await navigator.locks.query(); + assert_equals(state.held.filter(lock => lock.name === res).length, 1, + 'Number of held locks'); + assert_equals(state.pending.filter(lock => lock.name === res).length, 1, + 'Number of pending locks'); + + const rejected = promise_rejects_dom( + t, 'AbortError', promise, 'Request should reject with AbortError'); + + let callback_called = false; + t.step_timeout(() => { + callback_called = true; + controller.abort(); + }, 10); + + await rejected; + assert_true(callback_called, 'timeout should have caused the abort'); + +}, 'Abort after a timeout'); + +promise_test(async t => { + const res = uniqueName(t); + + const controller = new AbortController(); + + let got_lock = false; + await navigator.locks.request( + res, {signal: controller.signal}, async lock => { got_lock = true; }); + + assert_true(got_lock, 'Lock should be acquired if abort is not signaled.'); + +}, 'Signal that is not aborted'); + +promise_test(async t => { + const res = uniqueName(t); + + const controller = new AbortController(); + + let got_lock = false; + const p = navigator.locks.request( + res, {signal: controller.signal}, lock => { got_lock = true; }); + + // Even though lock is grantable, this abort should be processed synchronously. + controller.abort(); + + await promise_rejects_dom(t, 'AbortError', p, 'Request should abort'); + + assert_false(got_lock, 'Request should be aborted if signal is synchronous'); + + await navigator.locks.request(res, lock => { got_lock = true; }); + assert_true(got_lock, 'Subsequent request should not be blocked'); + +}, 'Synchronously signaled abort'); + +promise_test(async t => { + const res = uniqueName(t); + + const controller = new AbortController(); + + // Make a promise that resolves when the lock is acquired. + const [acquired_promise, acquired_func] = makePromiseAndResolveFunc(); + + // Request the lock. + let release_func; + const released_promise = navigator.locks.request( + res, {signal: controller.signal}, lock => { + acquired_func(); + + // Hold lock until release_func is called. + const [waiting_promise, waiting_func] = makePromiseAndResolveFunc(); + release_func = waiting_func; + return waiting_promise; + }); + + // Wait for the lock to be acquired. + await acquired_promise; + + // Signal an abort. + controller.abort(); + + // Release the lock. + release_func('resolved ok'); + + assert_equals(await released_promise, 'resolved ok', + 'Lock released promise should not reject'); + +}, 'Abort signaled after lock granted'); + +promise_test(async t => { + const res = uniqueName(t); + + const controller = new AbortController(); + + // Make a promise that resolves when the lock is acquired. + const [acquired_promise, acquired_func] = makePromiseAndResolveFunc(); + + // Request the lock. + let release_func; + const released_promise = navigator.locks.request( + res, {signal: controller.signal}, lock => { + acquired_func(); + + // Hold lock until release_func is called. + const [waiting_promise, waiting_func] = makePromiseAndResolveFunc(); + release_func = waiting_func; + return waiting_promise; + }); + + // Wait for the lock to be acquired. + await acquired_promise; + + // Release the lock. + release_func('resolved ok'); + + // Signal an abort. + controller.abort(); + + assert_equals(await released_promise, 'resolved ok', + 'Lock released promise should not reject'); + +}, 'Abort signaled after lock released'); + +promise_test(async t => { + const res = uniqueName(t); + + const controller = new AbortController(); + const first = requestLockAndHold(t, res, { signal: controller.signal }); + const next = navigator.locks.request(res, () => "resolved"); + controller.abort(); + + await promise_rejects_dom(t, "AbortError", first, "Request should abort"); + assert_equals( + await next, + "resolved", + "The next request is processed after abort" + ); +}, "Abort should process the next pending lock request"); + +promise_test(async t => { + const res = uniqueName(t); + + const controller = new AbortController(); + const promise = requestLockAndHold(t, res, { signal: controller.signal }); + + const reason = "My cat handled it"; + controller.abort(reason); + + await promise_rejects_exactly(t, reason, promise, "Rejection should give the abort reason"); +}, "Aborted promise should reject with the custom abort reason"); + +promise_test(async t => { + const res = uniqueName(t); + + const controller = new AbortController(); + const promise = requestLockAndHold(t, res, { signal: controller.signal }); + + controller.abort(); + + await promise_rejects_exactly(t, controller.signal.reason, promise, "Should be the same reason"); +}, "Aborted promise should reject with the default abort reason"); diff --git a/testing/web-platform/tests/web-locks/steal.https.any.js b/testing/web-platform/tests/web-locks/steal.https.any.js new file mode 100644 index 0000000000..d165b9d179 --- /dev/null +++ b/testing/web-platform/tests/web-locks/steal.https.any.js @@ -0,0 +1,91 @@ +// META: title=Web Locks API: steal option +// META: script=resources/helpers.js +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +const never_settled = new Promise(resolve => { /* never */ }); + +promise_test(async t => { + const res = uniqueName(t); + let callback_called = false; + await navigator.locks.request(res, {steal: true}, lock => { + callback_called = true; + assert_not_equals(lock, null, 'Lock should be granted'); + }); + assert_true(callback_called, 'Callback should be called'); +}, 'Lock available'); + +promise_test(async t => { + const res = uniqueName(t); + let callback_called = false; + + // Grab and hold the lock. + navigator.locks.request(res, lock => never_settled).catch(_ => {}); + + // Steal it. + await navigator.locks.request(res, {steal: true}, lock => { + callback_called = true; + assert_not_equals(lock, null, 'Lock should be granted'); + }); + + assert_true(callback_called, 'Callback should be called'); +}, 'Lock not available'); + +promise_test(async t => { + const res = uniqueName(t); + + // Grab and hold the lock. + const promise = navigator.locks.request(res, lock => never_settled); + const assertion = promise_rejects_dom( + t, 'AbortError', promise, `Initial request's promise should reject`); + + // Steal it. + await navigator.locks.request(res, {steal: true}, lock => {}); + + await assertion; + +}, `Broken lock's release promise rejects`); + +promise_test(async t => { + const res = uniqueName(t); + + // Grab and hold the lock. + navigator.locks.request(res, lock => never_settled).catch(_ => {}); + + // Make a request for it. + let request_granted = false; + const promise = navigator.locks.request(res, lock => { + request_granted = true; + }); + + // Steal it. + await navigator.locks.request(res, {steal: true}, lock => { + assert_false(request_granted, 'Steal should override request'); + }); + + await promise; + assert_true(request_granted, 'Request should eventually be granted'); + +}, `Requested lock's release promise is deferred`); + +promise_test(async t => { + const res = uniqueName(t); + + // Grab and hold the lock. + navigator.locks.request(res, lock => never_settled).catch(_ => {}); + + // Steal it. + let saw_abort = false; + const first_steal = navigator.locks.request( + res, {steal: true}, lock => never_settled).catch(error => { + saw_abort = true; + }); + + // Steal it again. + await navigator.locks.request(res, {steal: true}, lock => {}); + + await first_steal; + assert_true(saw_abort, 'First steal should have aborted'); + +}, 'Last caller wins'); diff --git a/testing/web-platform/tests/web-locks/storage-buckets.tentative.https.any.js b/testing/web-platform/tests/web-locks/storage-buckets.tentative.https.any.js new file mode 100644 index 0000000000..a6b4f59a95 --- /dev/null +++ b/testing/web-platform/tests/web-locks/storage-buckets.tentative.https.any.js @@ -0,0 +1,56 @@ +// META: title=Web Locks API: Storage Buckets have independent lock sets +// META: script=resources/helpers.js +// META: script=/storage/buckets/resources/util.js +// META: global=window,dedicatedworker,sharedworker,serviceworker + +'use strict'; + +/** + * Returns whether bucket1 and bucket2 share locks + * @param {*} t test runner object + * @param {*} bucket1 Storage bucket + * @param {*} bucket2 Storage bucket + */ +async function locksAreShared(t, bucket1, bucket2) { + const lock_name = self.uniqueName(t); + let callback_called = false; + let locks_are_shared; + await bucket1.locks.request(lock_name, async lock => { + await bucket2.locks.request( + lock_name, { ifAvailable: true }, async lock => { + callback_called = true; + locks_are_shared = lock == null; + }); + }); + assert_true(callback_called, 'callback should be called'); + return locks_are_shared; +} + +promise_test(async t => { + await prepareForBucketTest(t); + + const inboxBucket = await navigator.storageBuckets.open('inbox'); + const draftsBucket = await navigator.storageBuckets.open('drafts'); + + assert_true( + await locksAreShared(t, navigator, navigator), + 'The default bucket should share locks with itself'); + + assert_true( + await locksAreShared(t, inboxBucket, inboxBucket), + 'A non default bucket should share locks with itself'); + + assert_false( + await locksAreShared(t, navigator, inboxBucket), + 'The default bucket shouldn\'t share locks with a non default bucket'); + + assert_false( + await locksAreShared(t, draftsBucket, inboxBucket), + 'Two different non default buckets shouldn\'t share locks'); + + const inboxBucket2 = await navigator.storageBuckets.open('inbox'); + + assert_true( + await self.locksAreShared(t, inboxBucket, inboxBucket2), + 'A two instances of the same non default bucket should share locks with theirselves'); +}, 'Storage buckets have independent locks'); diff --git a/testing/web-platform/tests/web-locks/workers.https.html b/testing/web-platform/tests/web-locks/workers.https.html new file mode 100644 index 0000000000..9fe38dbe38 --- /dev/null +++ b/testing/web-platform/tests/web-locks/workers.https.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Web Locks API: Workers</title> +<link rel=help href="https://w3c.github.io/web-locks/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/helpers.js"></script> +<script> +'use strict'; + +promise_test(async t => { + assert_implements(navigator.locks); + const worker = new Worker('resources/worker.js'); + t.add_cleanup(() => { worker.terminate(); }); + + const res = 'shared resource 1'; + + const lock_id = (await postToWorkerAndWait( + worker, {op: 'request', name: res, mode: 'shared'})).lock_id; + + await navigator.locks.request(res, {mode: 'shared'}, async lock => { + await postToWorkerAndWait(worker, {op: 'release', lock_id}); + }); + +}, 'Window and Worker - shared mode'); + +promise_test(async t => { + assert_implements(navigator.locks); + const worker = new Worker('resources/worker.js'); + t.add_cleanup(() => { worker.terminate(); }); + + const res = 'exclusive resource 1'; + + // worker acquires the lock. + const lock_id = (await postToWorkerAndWait( + worker, {op: 'request', name: res})).lock_id; + + // This request should be blocked. + let lock_granted = false; + const blocked = navigator.locks.request( + res, lock => { lock_granted = true; }); + + // Verify we can't get it. + let available = undefined; + await navigator.locks.request( + res, {ifAvailable: true}, lock => { available = lock !== null; }); + assert_false(available); + assert_false(lock_granted); + + // Ask the worker to release it. + await postToWorkerAndWait(worker, {op: 'release', lock_id}); + + // Now we've got it. + const lock2 = await blocked; + assert_true(lock_granted); + +}, 'Window and Worker - exclusive mode'); + +promise_test(async t => { + assert_implements(navigator.locks); + const worker1 = new Worker('resources/worker.js'); + const worker2 = new Worker('resources/worker.js'); + t.add_cleanup(() => { worker1.terminate(); worker2.terminate(); }); + + const res = 'exclusive resource 2'; + + // worker1 acquires the lock. + const lock_id = (await postToWorkerAndWait( + worker1, {op: 'request', name: res})).lock_id; + + // This request should be blocked. + let lock_granted = false; + const blocked = postToWorkerAndWait( + worker2, {op: 'request', name: res}); + blocked.then(f => { lock_granted = true; }); + + // Verify worker2 can't get it. + assert_true((await postToWorkerAndWait(worker2, { + op: 'request', name: res, ifAvailable: true + })).failed, 'Lock request should have failed'); + assert_false(lock_granted); + + // Ask worker1 to release it. + await postToWorkerAndWait(worker1, {op: 'release', lock_id}); + + // Now worker2 can get it. + const lock = await blocked; + assert_true(lock_granted); + +}, 'Worker and Worker - exclusive mode'); + +promise_test(async t => { + assert_implements(navigator.locks); + const worker = new Worker('resources/worker.js'); + + const res = 'exclusive resource 3'; + + // Worker acquires the lock. + await postToWorkerAndWait(worker, {op: 'request', name: res}); + + // This request should be blocked. + let lock_granted = false; + const blocked = navigator.locks.request( + res, lock => { lock_granted = true; }); + + // Verify we can't get it. + let available = undefined; + await navigator.locks.request( + res, {ifAvailable: true}, lock => { available = lock !== null; }); + assert_false(available); + assert_false(lock_granted); + + // Implicitly release it by terminating the worker. + worker.terminate(); + + // Now we've got it. + const lock = await blocked; + assert_true(lock_granted); + +}, 'Terminated Worker - exclusive mode'); + +</script> |