Update web-platform-tests to revision 60220357131c65146444da1f54624d5b54d0975d

This commit is contained in:
WPT Sync Bot 2018-07-18 15:43:58 +00:00 committed by Tom Servo
parent c45192614c
commit 775b784f79
2144 changed files with 58115 additions and 29658 deletions

View file

@ -0,0 +1,4 @@
spec: https://inexorabletash.github.io/web-locks/
suggested_reviewers:
- inexorabletash
- pwnall

View file

@ -0,0 +1,5 @@
This directory contains a test suite for the proposed Web Locks API.
Explainer: https://github.com/inexorabletash/web-locks
Spec: https://inexorabletash.github.io/web-locks/

View file

@ -0,0 +1,130 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: navigator.locks.request method</title>
<link rel=help href="https://github.com/inexorabletash/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 => {
const res = uniqueName(t);
await promise_rejects(t, new TypeError(), navigator.locks.request());
await promise_rejects(t, new 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(
t, new TypeError(),
navigator.locks.request(res, {mode: 'foo'}, lock => {}));
await promise_rejects(
t, new 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(
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(
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(
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(
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(
t, new TypeError(), navigator.locks.request(res, undefined));
await promise_rejects(
t, new TypeError(), navigator.locks.request(res, null));
await promise_rejects(
t, new TypeError(), navigator.locks.request(res, 123));
await promise_rejects(
t, new TypeError(), navigator.locks.request(res, 'abc'));
await promise_rejects(
t, new TypeError(), navigator.locks.request(res, []));
await promise_rejects(
t, new TypeError(), navigator.locks.request(res, {}));
await promise_rejects(
t, new 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(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(t, test_error, p, 'result should reject');
}, 'Returned Promise rejects if callback throws asynchronously');
</script>

View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: Client IDs in query() vs. Service Worker</title>
<link rel=help href="https://github.com/inexorabletash/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 => {
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_true(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>

View file

@ -0,0 +1,239 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: Frames</title>
<link rel=help href="https://github.com/inexorabletash/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 => {
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 => {
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 => {
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 => {
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 => {
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 => {
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 => {
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');
</script>

View file

@ -0,0 +1,97 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: Lock held until callback result resolves</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
'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');
</script>

View file

@ -0,0 +1,169 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: ifAvailable option</title>
<link rel=help href="https://github.com/inexorabletash/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 => {
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(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(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');
</script>

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: WebIDL tests in service worker</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'use strict';
(async () => {
const scope = 'resources/does/not/exist';
let registration = await navigator.serviceWorker.getRegistration(scope);
if (registration)
await registration.unregister();
registration = await navigator.serviceWorker.register(
'resources/interfaces-serviceworker.js', {scope});
fetch_tests_from_worker(registration.installing);
})();
</script>

View file

@ -0,0 +1,46 @@
enum LockMode { "shared", "exclusive" };
dictionary LockOptions {
LockMode mode = "exclusive";
boolean ifAvailable = false;
};
callback LockGrantedCallback = any (Lock lock);
[Exposed=Window]
partial interface Navigator {
[SecureContext] readonly attribute LockManager locks;
};
[Exposed=Worker]
partial interface WorkerNavigator {
[SecureContext] readonly attribute LockManager locks;
};
[Exposed=(Window,Worker), SecureContext]
interface LockManager {
Promise<any> request(DOMString name,
LockGrantedCallback callback);
Promise<any> request(DOMString name,
LockOptions options,
LockGrantedCallback callback);
Promise<LockManagerSnapshot> query();
};
[Exposed=(Window,Worker), SecureContext]
interface Lock {
readonly attribute DOMString name;
readonly attribute LockMode mode;
};
dictionary LockManagerSnapshot {
sequence<LockInfo> pending;
sequence<LockInfo> held;
};
dictionary LockInfo {
DOMString name;
LockMode mode;
};

View file

@ -0,0 +1,26 @@
// META: script=/resources/WebIDLParser.js
// META: script=/resources/idlharness.js
'use strict';
promise_test(async t => {
const response = await fetch('interfaces.idl');
const idls = await response.text();
const idl_array = new IdlArray();
idl_array.add_untested_idls('[Exposed=Window] interface Navigator {};');
idl_array.add_untested_idls('[Exposed=Worker] interface WorkerNavigator {};');
idl_array.add_idls(idls);
let lock;
await navigator.locks.request('name', l => { lock = l; });
idl_array.add_objects({
LockManager: [navigator.locks],
Lock: [lock],
});
idl_array.test();
});

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: Lock Attributes</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'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)');
</script>

View file

@ -0,0 +1,40 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: Exclusive Mode</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'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');
</script>

View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: Mixed Modes</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'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');
</script>

View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: Shared Mode</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'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');
</script>

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: API not available in non-secure context</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'use strict';
test(t => {
assert_false(window.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');
</script>

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: Opaque origins</title>
<link rel=help href="https://github.com/inexorabletash/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 () => {
try {
await navigator.locks.request('name', lock => {});
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({}, '*');
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({}, '*');
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');
</script>

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: navigator.locks.query method - no locks held</title>
<link rel=help href="https://github.com/inexorabletash/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 => {
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');
</script>

View file

@ -0,0 +1,116 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: navigator.locks.query ordering</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<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(_ => {});
});
}
promise_test(async t => {
const res1 = uniqueName(t);
const res2 = uniqueName(t);
const res3 = uniqueName(t);
// These will never be released.
await Promise.all([
getLockAndHoldUntilReleased(res1),
getLockAndHoldUntilReleased(res2),
getLockAndHoldUntilReleased(res3)
]);
// These requests should be blocked.
navigator.locks.request(res3, {mode: 'shared'}, lock => {});
navigator.locks.request(res2, {mode: 'shared'}, lock => {});
navigator.locks.request(res1, {mode: 'shared'}, lock => {});
const state = await navigator.locks.query();
const relevant_pending_names = state.pending.map(lock => lock.name)
.filter(name => [res1, res2, res3].includes(name));
assert_array_equals(relevant_pending_names, [res3, res2, res1],
'Pending locks should appear in order.');
}, 'Requests appear in state in order made');
promise_test(async t => {
const res1 = uniqueName(t);
const res2 = uniqueName(t);
const res3 = uniqueName(t);
// These should be granted, and will be held until released.
const [release1, release2, release3] = await Promise.all([
getLockAndHoldUntilReleased(res1),
getLockAndHoldUntilReleased(res2),
getLockAndHoldUntilReleased(res3)
]);
// These requests should be blocked.
const requests = [
getLockAndHoldUntilReleased(res1),
getLockAndHoldUntilReleased(res2),
getLockAndHoldUntilReleased(res3)
];
// Ensure the requests have had a chance to get queued by
// waiting for something else to make it through the queue.
await navigator.locks.request(uniqueName(t), lock => {});
// Now release the previous holders.
release2();
release3();
release1();
// Wait until the subsequent requests make it through.
await Promise.all(requests);
const state = await navigator.locks.query();
const relevant_held_names = state.held.map(lock => lock.name)
.filter(name => [res1, res2, res3].includes(name));
assert_array_equals(relevant_held_names, [res2, res3, res1],
'Held locks should appear in granted order.');
}, 'Held locks appear in state in order granted');
promise_test(async t => {
const res1 = uniqueName(t);
const res2 = uniqueName(t);
const res3 = uniqueName(t);
// These should be granted, and will be held until stolen.
await Promise.all([
getLockAndHoldUntilReleased(res1),
getLockAndHoldUntilReleased(res2),
getLockAndHoldUntilReleased(res3)
]);
// Steal in a different order.
await Promise.all([
getLockAndHoldUntilReleased(res3, {steal: true}),
getLockAndHoldUntilReleased(res1, {steal: true}),
getLockAndHoldUntilReleased(res2, {steal: true})
]);
const state = await navigator.locks.query();
const relevant_held_names = state.held.map(lock => lock.name)
.filter(name => [res1, res2, res3].includes(name));
assert_array_equals(relevant_held_names, [res3, res1, res2],
'Held locks should appear in granted order.');
}, 'Held locks appear in state in order granted, including when stolen');
</script>

View file

@ -0,0 +1,234 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: navigator.locks.query method</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
'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');
</script>

View file

@ -0,0 +1,62 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: Resources DOMString edge cases</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'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(
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 "-"');
</script>

View file

@ -0,0 +1,60 @@
// 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 => {
return `${self.location.pathname}-${testCase.name}-${++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);
});
};
})();

View file

@ -0,0 +1,43 @@
<!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;
}
});
</script>

View file

@ -0,0 +1,29 @@
'use strict';
importScripts('/resources/testharness.js',
'/resources/WebIDLParser.js',
'/resources/idlharness.js');
promise_test(async t => {
const response = await fetch('../interfaces.idl');
const idls = await response.text();
const idl_array = new IdlArray();
idl_array.add_untested_idls('[Exposed=Window] interface Navigator {};');
idl_array.add_untested_idls('[Exposed=Worker] interface WorkerNavigator {};');
idl_array.add_idls(idls);
let lock;
await navigator.locks.request('name', l => { lock = l; });
idl_array.add_objects({
LockManager: [navigator.locks],
Lock: [lock],
});
idl_array.test();
}, 'Interface test');
done();

View file

@ -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;
}
});

View file

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

View file

@ -0,0 +1,39 @@
'use strict';
// Map of id => function that releases a lock.
const held = new Map();
let next_lock_id = 1;
self.addEventListener('message', e => {
function respond(data) {
self.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;
}
});

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: API requires secure context</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
'use strict';
test(t => {
assert_true(window.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');
</script>

View file

@ -0,0 +1,202 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: AbortSignal integration</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
'use strict';
function makePromiseAndResolveFunc() {
let resolve;
const promise = new Promise(r => { resolve = r; });
return [promise, resolve];
}
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(
t, new 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(
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);
// Grab a lock and hold it forever.
const never_settled = new Promise(resolve => { /* never */ });
navigator.locks.request(res, lock => never_settled);
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);
assert_equals(state.pending.filter(lock => lock.name === res).length, 1);
const rejected = promise_rejects(
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 forever.
const never_settled = new Promise(resolve => { /* never */ });
navigator.locks.request(res, lock => never_settled);
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);
assert_equals(state.pending.filter(lock => lock.name === res).length, 1);
const rejected = promise_rejects(
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(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');
</script>

View file

@ -0,0 +1,97 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: steal option</title>
<link rel=help href="https://github.com/inexorabletash/web-locks">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/helpers.js"></script>
<script>
'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(
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');
</script>

View file

@ -0,0 +1,118 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>Web Locks API: Workers</title>
<link rel=help href="https://github.com/inexorabletash/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 => {
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 => {
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 => {
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 => {
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>