summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/web-locks
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/web-locks')
-rw-r--r--testing/web-platform/tests/web-locks/META.yml5
-rw-r--r--testing/web-platform/tests/web-locks/README.md5
-rw-r--r--testing/web-platform/tests/web-locks/acquire.tentative.https.any.js136
-rw-r--r--testing/web-platform/tests/web-locks/bfcache/abort.tentative.https.html64
-rw-r--r--testing/web-platform/tests/web-locks/bfcache/held.tentative.https.html45
-rw-r--r--testing/web-platform/tests/web-locks/bfcache/helpers.js15
-rw-r--r--testing/web-platform/tests/web-locks/bfcache/release-across-thread.tentative.https.html46
-rw-r--r--testing/web-platform/tests/web-locks/bfcache/release.tentative.https.html48
-rw-r--r--testing/web-platform/tests/web-locks/bfcache/sharedworker-multiple.tentative.https.html69
-rw-r--r--testing/web-platform/tests/web-locks/clientids.tentative.https.html45
-rw-r--r--testing/web-platform/tests/web-locks/crashtests/after-worker-termination.https.html20
-rw-r--r--testing/web-platform/tests/web-locks/crashtests/iframe-append-2.https.html18
-rw-r--r--testing/web-platform/tests/web-locks/crashtests/iframe-append.https.html21
-rw-r--r--testing/web-platform/tests/web-locks/crashtests/settle-after-steal.https.html10
-rw-r--r--testing/web-platform/tests/web-locks/crashtests/worker-termination.https.html16
-rw-r--r--testing/web-platform/tests/web-locks/frames.tentative.https.html265
-rw-r--r--testing/web-platform/tests/web-locks/held.tentative.https.any.js91
-rw-r--r--testing/web-platform/tests/web-locks/idlharness.tentative.https.any.js29
-rw-r--r--testing/web-platform/tests/web-locks/ifAvailable.tentative.https.any.js163
-rw-r--r--testing/web-platform/tests/web-locks/lock-attributes.tentative.https.any.js18
-rw-r--r--testing/web-platform/tests/web-locks/mode-exclusive.tentative.https.any.js34
-rw-r--r--testing/web-platform/tests/web-locks/mode-mixed.tentative.https.any.js98
-rw-r--r--testing/web-platform/tests/web-locks/mode-shared.tentative.https.any.js38
-rw-r--r--testing/web-platform/tests/web-locks/non-fully-active.tentative.https.html73
-rw-r--r--testing/web-platform/tests/web-locks/non-secure-context.tentative.any.js14
-rw-r--r--testing/web-platform/tests/web-locks/opaque-origin.tentative.https.html87
-rw-r--r--testing/web-platform/tests/web-locks/partitioned-web-locks.tentative.https.html167
-rw-r--r--testing/web-platform/tests/web-locks/query-empty.tentative.https.any.js18
-rw-r--r--testing/web-platform/tests/web-locks/query-ordering.tentative.https.html131
-rw-r--r--testing/web-platform/tests/web-locks/query.tentative.https.any.js227
-rw-r--r--testing/web-platform/tests/web-locks/resource-names.tentative.https.any.js56
-rw-r--r--testing/web-platform/tests/web-locks/resources/helpers.js83
-rw-r--r--testing/web-platform/tests/web-locks/resources/iframe-parent.html37
-rw-r--r--testing/web-platform/tests/web-locks/resources/iframe.html52
-rw-r--r--testing/web-platform/tests/web-locks/resources/parentworker.js10
-rw-r--r--testing/web-platform/tests/web-locks/resources/partitioned-parent.html30
-rw-r--r--testing/web-platform/tests/web-locks/resources/service-worker.js7
-rw-r--r--testing/web-platform/tests/web-locks/resources/sw-controlled-iframe.html35
-rw-r--r--testing/web-platform/tests/web-locks/resources/worker.js56
-rw-r--r--testing/web-platform/tests/web-locks/secure-context.tentative.https.any.js14
-rw-r--r--testing/web-platform/tests/web-locks/signal.tentative.https.any.js261
-rw-r--r--testing/web-platform/tests/web-locks/steal.tentative.https.any.js91
-rw-r--r--testing/web-platform/tests/web-locks/storage-buckets.tentative.https.any.js56
-rw-r--r--testing/web-platform/tests/web-locks/workers.tentative.https.html122
44 files changed, 2926 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.tentative.https.any.js b/testing/web-platform/tests/web-locks/acquire.tentative.https.any.js
new file mode 100644
index 0000000000..54ae6f30e7
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/acquire.tentative.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.tentative.https.html b/testing/web-platform/tests/web-locks/clientids.tentative.https.html
new file mode 100644
index 0000000000..7a3ffe45e1
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/clientids.tentative.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.tentative.https.html b/testing/web-platform/tests/web-locks/frames.tentative.https.html
new file mode 100644
index 0000000000..980b2fe747
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/frames.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/held.tentative.https.any.js
new file mode 100644
index 0000000000..7fc4c73540
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/held.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/idlharness.tentative.https.any.js
new file mode 100644
index 0000000000..2df7831c34
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/idlharness.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/ifAvailable.tentative.https.any.js
new file mode 100644
index 0000000000..63e6d568fe
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/ifAvailable.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/lock-attributes.tentative.https.any.js
new file mode 100644
index 0000000000..d1c03103e0
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/lock-attributes.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/mode-exclusive.tentative.https.any.js
new file mode 100644
index 0000000000..8450e237c3
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/mode-exclusive.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/mode-mixed.tentative.https.any.js
new file mode 100644
index 0000000000..5f34588693
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/mode-mixed.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/mode-shared.tentative.https.any.js
new file mode 100644
index 0000000000..fc4a6012fa
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/mode-shared.tentative.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.tentative.https.html b/testing/web-platform/tests/web-locks/non-fully-active.tentative.https.html
new file mode 100644
index 0000000000..56a5372044
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/non-fully-active.tentative.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.tentative.any.js b/testing/web-platform/tests/web-locks/non-secure-context.tentative.any.js
new file mode 100644
index 0000000000..94553591f3
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/non-secure-context.tentative.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.tentative.https.html b/testing/web-platform/tests/web-locks/opaque-origin.tentative.https.html
new file mode 100644
index 0000000000..3ba636ecd8
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/opaque-origin.tentative.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..2f166e67bc
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/partitioned-web-locks.tentative.https.html
@@ -0,0 +1,167 @@
+
+<!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): 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.
+// Step 8 (top-frame): clean up.
+
+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.
+ navigator.locks.request('testLock', {mode: 'exclusive', ifAvailable: true},
+ lock => {
+ if (lock === null) {
+ assert_true(false)
+ return;
+ }
+ let lock_id = next_lock_id++;
+ let release;
+ const promise = new Promise(r => { release = r; });
+ held.set(lock_id, release);
+ return promise;
+ });
+
+ // Step 2.
+ const w = window.open(target_url);
+ const result = await new Promise(resolve => window.onmessage = resolve);
+
+ // Step 7.
+ // When 3rd party storage partitioning is enabled, the iframe should be able
+ // to aquire 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');
+
+ // Step 8.
+ t.add_cleanup(() => {
+ w.close()
+ for(let i = 1; i < next_lock_id; i++){
+ held.get(i)();
+ }
+ });
+}
+
+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): 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.
+// Nested Step 11 (top frame): clean up.
+
+// 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.
+ navigator.locks.request('testLock', {mode: 'exclusive', ifAvailable: true},
+ lock => {
+ if (lock === null) {
+ assert_true(false)
+ return;
+ }
+ // Obtain and store the release functions for clean-up.
+ let lock_id = next_lock_id_2++;
+ let release;
+ const promise = new Promise(r => { release = r; });
+ held_2.set(lock_id, release);
+ return promise;
+ }).catch(error => alert(error.message));
+
+ // 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.
+ // 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');
+
+ // Nested Step 11.
+ t.add_cleanup(() => {
+ w.close()
+ for(let i = 1; i < next_lock_id_2; i++){
+ held_2.get(i)();
+ }
+ });
+}
+
+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.tentative.https.any.js b/testing/web-platform/tests/web-locks/query-empty.tentative.https.any.js
new file mode 100644
index 0000000000..88ffdb7f81
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/query-empty.tentative.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.tentative.https.html b/testing/web-platform/tests/web-locks/query-ordering.tentative.https.html
new file mode 100644
index 0000000000..d5e722baf7
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/query-ordering.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/query.tentative.https.any.js
new file mode 100644
index 0000000000..14fdeca7a4
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/query.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/resource-names.tentative.https.any.js
new file mode 100644
index 0000000000..1031b3f7ba
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/resource-names.tentative.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..4b3311eee6
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/resources/helpers.js
@@ -0,0 +1,83 @@
+// 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 = {}) => {
+ return navigator.locks.request(name, options, () => {
+ return new Promise(resolve => t.add_cleanup(resolve));
+ });
+ };
+
+ 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.tentative.https.any.js b/testing/web-platform/tests/web-locks/secure-context.tentative.https.any.js
new file mode 100644
index 0000000000..29ae7aea47
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/secure-context.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/signal.tentative.https.any.js
new file mode 100644
index 0000000000..5a37e3ae87
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/signal.tentative.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.tentative.https.any.js b/testing/web-platform/tests/web-locks/steal.tentative.https.any.js
new file mode 100644
index 0000000000..d165b9d179
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/steal.tentative.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.tentative.https.html b/testing/web-platform/tests/web-locks/workers.tentative.https.html
new file mode 100644
index 0000000000..9fe38dbe38
--- /dev/null
+++ b/testing/web-platform/tests/web-locks/workers.tentative.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>