summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/presentation-api/receiving-ua/support
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/presentation-api/receiving-ua/support')
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnectionList_onconnectionavailable_receiving-ua.html92
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_onclose_receiving-ua.html89
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_onmessage_receiving-ua.html108
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_send_receiving-ua.html45
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_terminate_receiving-ua.html23
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_create_receiving-ua.html328
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_create_receiving-ua_child.html171
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_unreached_receiving-ua.html21
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/idlharness_receiving-ua.html41
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/iframe.html22
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/serviceworker.js9
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/stash.js98
-rw-r--r--testing/web-platform/tests/presentation-api/receiving-ua/support/stash.py10
13 files changed, 1057 insertions, 0 deletions
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnectionList_onconnectionavailable_receiving-ua.html b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnectionList_onconnectionavailable_receiving-ua.html
new file mode 100644
index 0000000000..74d8dfa0ed
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnectionList_onconnectionavailable_receiving-ua.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8">
+<title>Monitoring incoming presentation connections</title>
+<link rel="author" title="Tomoyuki Shimizu" href="https://github.com/tomoyukilabs">
+<link rel="help" href="https://w3c.github.io/presentation-api/#monitoring-incoming-presentation-connections">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../common.js"></script>
+<script src="stash.js"></script>
+
+<script>
+ const stash = new Stash(stashIds.toReceiver, stashIds.toController);
+
+ add_completion_callback((tests, status) => {
+ const log = document.getElementById('log');
+ stash.send(JSON.stringify({ type: 'result', tests: tests, status: status, log: log.innerHTML }));
+ });
+
+ promise_test(t => {
+ const receiver = navigator.presentation.receiver;
+ return receiver.connectionList.then(list => {
+ let connections = list.connections;
+ let number = connections.length;
+
+ const checkConnectionList = (connection, action) => {
+ assert_equals(connections.length, number, 'PresentationConnectionList.connections is a frozen array.');
+
+ // Note: When a presentation is terminated, a receiving user agent unloads a document
+ // without firing a "terminate" event.
+ return receiver.connectionList.then(list => {
+ connections = list.connections;
+ if (action === 'close') {
+ assert_true(connections.length === number - 1 && !connections.includes(connection),
+ 'A closed presentation connection is removed from the set of presentation controllers.');
+ } else if (action === 'connect') {
+ assert_true(connections.length === number + 1 && connections.includes(connection),
+ 'A new presentation connection is added to the set of presentation controllers.');
+ }
+ number = connections.length;
+ });
+ };
+
+ const checkEvent = evt => {
+ assert_true(evt instanceof PresentationConnectionAvailableEvent, 'An event using PresentationConnectionAvailableEvent is fired.');
+ assert_true(evt.isTrusted, 'The event is a trusted event.');
+ assert_false(evt.bubbles, 'The event does not bubbles.');
+ assert_false(evt.cancelable, 'The event is not cancelable.');
+ assert_equals(evt.type, 'connectionavailable', 'The event name is "connectionavailable".');
+ assert_equals(evt.target, list, 'event.target is the presentation connection list.');
+ assert_true(evt.connection instanceof PresentationConnection, 'event.connection is a presentation connection.');
+
+ return checkConnectionList(evt.connection, 'connect');
+ };
+
+ const watchEvent = (obj, watcher, type) => {
+ const watchHandler = new Promise(resolve => {
+ obj['on' + type] = evt => { resolve(evt); };
+ });
+ return Promise.all([ watchHandler, watcher.wait_for(type) ]).then(results => {
+ assert_equals(results[0], results[1], 'Both on' + type + ' and addEventListener pass the same event object.');
+ return results[0];
+ });
+ };
+
+ // Step 1: check the first connection in "connected" state
+ let connection = list.connections[0];
+ assert_equals(number, 1, 'A presentation connection list is populated with a first presentation connection.');
+ assert_true(list instanceof PresentationConnectionList, 'navigator.presentation.receiver.connectionList is resolved with a presentation connection list.');
+ assert_true(list.connections instanceof Array, 'A presentation connection list is an array.');
+ assert_true(connection instanceof PresentationConnection, 'A presentation connection is added to a presentation connection list.');
+
+ // Step 2: check the connection in "closed" state
+ stash.send(JSON.stringify({ type: 'ok' }));
+ let eventWatcher = new EventWatcher(t, connection, 'close');
+ return eventWatcher.wait_for('close').then(() => {
+ return checkConnectionList(connection, 'close');
+ })
+ // Step 3: check the first connection when reconnected
+ .then(() => {
+ stash.send(JSON.stringify({ type: 'ok' }));
+ eventWatcher = new EventWatcher(t, list, 'connectionavailable');
+ return watchEvent(list, eventWatcher, 'connectionavailable')
+ }).then(checkEvent)
+ // Step 4: check the second connection (with <iframe>) in "connected" state
+ .then(() => {
+ stash.send(JSON.stringify({ type: 'ok' }));
+ return watchEvent(list, eventWatcher, 'connectionavailable')
+ }).then(checkEvent);
+ });
+ });
+</script>
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_onclose_receiving-ua.html b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_onclose_receiving-ua.html
new file mode 100644
index 0000000000..7194c6f4d9
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_onclose_receiving-ua.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8">
+<title>Closing a PresentationConnection</title>
+<link rel="author" title="Tomoyuki Shimizu" href="https://github.com/tomoyukilabs">
+<link rel="help" href="https://w3c.github.io/presentation-api/#closing-a-presentationconnection">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../common.js"></script>
+<script src="stash.js"></script>
+
+<script>
+ const stash = new Stash(stashIds.toReceiver, stashIds.toController);
+
+ add_completion_callback((tests, status) => {
+ const log = document.getElementById('log');
+ stash.send(JSON.stringify({ type: 'result', tests: tests, status: status, log: log.innerHTML }));
+ });
+
+ const checkCloseEvent = (evt, connection, reason) => {
+ assert_true(evt instanceof PresentationConnectionCloseEvent, 'An event using PresentationConnectionCloseEvent is fired.');
+ assert_true(evt.isTrusted, 'The event is a trusted event.');
+ assert_false(evt.bubbles, 'The event does not bubbles.');
+ assert_false(evt.cancelable, 'The event is not cancelable.');
+ assert_equals(evt.type, 'close', 'The event name is "close".');
+ assert_equals(evt.target, connection, 'event.target is the presentation connection.');
+ assert_equals(connection.state, 'closed', 'State of the presentation connection is "closed".');
+ assert_equals(evt.reason, reason, 'The reason for closing the presentation connection is "' + reason + '".');
+ };
+
+ const watchEvent = (obj, watcher, type) => {
+ const watchHandler = new Promise(resolve => {
+ obj['on' + type] = evt => { resolve(evt); };
+ });
+ return Promise.all([ watchHandler, watcher.wait_for(type) ]).then(results => {
+ assert_equals(results[0], results[1], 'Both on' + type + ' and addEventListener pass the same event object.');
+ return results[0];
+ });
+ };
+
+ promise_test(t => {
+ return navigator.presentation.receiver.connectionList.then(list => {
+ let connection = list.connections[0];
+ assert_equals(list.connections.length, 1, 'A presentation connection list is populated with a first presentation connection.');
+
+ // Step 1: close the presentation connection in "connected" state
+ connection.close();
+ let watchClose = new EventWatcher(t, connection, 'close');
+ const watchNewConnection = new EventWatcher(t, list, 'connectionavailable');
+ return watchEvent(connection, watchClose, 'close').then(evt => {
+ checkCloseEvent(evt, connection, 'closed');
+
+ // Step 2: check a connection closed by the controlling user agent
+ stash.send(JSON.stringify({ type: 'ok' }));
+ return watchNewConnection.wait_for('connectionavailable');
+ }).then(evt => {
+ connection = evt.connection;
+ watchClose = new EventWatcher(t, connection, 'close');
+ return watchEvent(connection, watchClose, 'close');
+ }).then(evt => {
+ checkCloseEvent(evt, connection, 'closed');
+
+ // Step 3: try to close the presentation connection in "closed" state (nothing should happen)
+ connection.close();
+ return Promise.race([
+ new Promise(resolve => { t.step_timeout(resolve, 1000); }),
+ watchEvent(connection, watchClose, 'close').then(() => {
+ assert_unreached('Invoking PresentationConnection.close() in the "closed" state causes nothing.'); })
+ ]);
+ }).then(() => {
+
+ // Step 4: check a connection closed due to aborting the nested browsing context
+ stash.send(JSON.stringify({ type: 'ok' }));
+ return watchNewConnection.wait_for('connectionavailable');
+ }).then(evt => {
+ connection = evt.connection;
+ stash.send(JSON.stringify({ type: 'ok' }));
+ watchClose = new EventWatcher(t, connection, 'close');
+ return Promise.race([
+ watchEvent(connection, watchClose, 'close'),
+ new Promise(resolve => { t.step_timeout(() => { resolve(null); }, 3000); })
+ ]);
+ }).then(evt => {
+ assert_true(!!evt, 'A presentation connection is closed when its controller\'s browsing context is discarded.');
+ checkCloseEvent(evt, connection, 'wentaway');
+ });
+ });
+ });
+</script>
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_onmessage_receiving-ua.html b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_onmessage_receiving-ua.html
new file mode 100644
index 0000000000..0734a5570c
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_onmessage_receiving-ua.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Receiving a message through PresentationConnection</title>
+<link rel="author" title="Tomoyuki Shimizu" href="https://github.com/tomoyukilabs/">
+<link rel="help" href="http://w3c.github.io/presentation-api/#receiving-a-message-through-presentationconnection">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../common.js"></script>
+<script src="stash.js"></script>
+
+<script>
+setup({explicit_timeout: true});
+
+const message1 = '1st';
+const message2 = '2nd';
+const message3 = new Uint8Array([51, 114, 100]); // "3rd"
+const message4 = new Uint8Array([52, 116, 104]); // "4th"
+const message5 = new Uint8Array([108, 97, 115, 116]); // "last"
+
+const toUint8Array = buf => {
+ return buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf;
+}
+
+// compare two ArrayBuffer or Uint8Array
+const compare = (a, b) => {
+ const p = toUint8Array(a);
+ const q = toUint8Array(b);
+ return !!p && !!q && p.every((item, index) => { return item === q[index]; });
+};
+
+const stash = new Stash(stashIds.toReceiver, stashIds.toController);
+
+add_completion_callback((tests, status) => {
+ const log = document.getElementById('log');
+ stash.send(JSON.stringify({ type: 'result', tests: tests, status: status, log: log.innerHTML }));
+});
+
+navigator.presentation.receiver.connectionList.then(list => {
+ const checkEvent = event => {
+ assert_true(event.isTrusted, 'a trusted event is fired');
+ assert_true(event instanceof MessageEvent, 'The event uses the MessageEvent interface');
+ assert_false(event.bubbles, 'the event does not bubble');
+ assert_false(event.cancelable, 'the event is not cancelable');
+ };
+
+ const watchEvent = (obj, watcher, type) => {
+ const watchHandler = new Promise(resolve => {
+ obj['on' + type] = evt => { resolve(evt); };
+ });
+ return Promise.all([ watchHandler, watcher.wait_for(type) ]).then(results => {
+ assert_equals(results[0], results[1], 'Both on' + type + ' and addEventListener pass the same event object.');
+ return results[0];
+ });
+ };
+
+ const connection = list.connections[0];
+
+ promise_test(t => {
+ t.step_timeout(() => {
+ t.force_timeout();
+ t.done();
+ }, 3000);
+
+ assert_equals(connection.binaryType, 'arraybuffer', 'the default value of binaryType is "arraybuffer"');
+ const eventWatcher = new EventWatcher(t, connection, 'message');
+ return eventWatcher.wait_for('message').then(event => {
+ checkEvent(event);
+ assert_equals(event.data, message1, 'receive a string correctly');
+ return watchEvent(connection, eventWatcher, 'message');
+ }).then(event => {
+ checkEvent(event);
+ assert_equals(event.data, message2, 'receive a string correctly');
+ return watchEvent(connection, eventWatcher, 'message');
+ }).then(event => {
+ checkEvent(event);
+ assert_true(event.data instanceof ArrayBuffer, 'receive binary data as ArrayBuffer');
+ assert_true(compare(event.data, message3), 'receive an ArrayBuffer correctly (originally a Blob at a controlling user agent)');
+ return watchEvent(connection, eventWatcher, 'message');
+ }).then(event => {
+ checkEvent(event);
+ assert_true(event.data instanceof ArrayBuffer, 'receive binary data as ArrayBuffer');
+ assert_true(compare(event.data, message4), 'receive an ArrayBuffer correctly (originally an ArrayBuffer at a controlling user agent)');
+ return watchEvent(connection, eventWatcher, 'message');
+ }).then(event => {
+ checkEvent(event);
+ assert_true(event.data instanceof ArrayBuffer, 'receive binary data as ArrayBuffer');
+ assert_true(compare(event.data, message5), 'receive an ArrayBuffer correctly (originally an ArrayBufferView at a controlling user agent)');
+
+ connection.binaryType = 'blob';
+ return Promise.all([
+ stash.send(JSON.stringify({ type: 'blob' })),
+ watchEvent(connection, eventWatcher, 'message')
+ ]).then(results => results[1]);
+ }).then(event => {
+ assert_true(event.data instanceof Blob, 'receive binary data as Blob');
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = resolve;
+ reader.onerror = reject;
+ reader.readAsArrayBuffer(event.data);
+ });
+ }).then(event => {
+ assert_true(compare(event.target.result, message5), 'receive a Blob correctly');
+ connection.terminate();
+ });
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_send_receiving-ua.html b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_send_receiving-ua.html
new file mode 100644
index 0000000000..bc482e9dfd
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_send_receiving-ua.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8">
+<title>Sending a message through PresentationConnection</title>
+<link rel="author" title="Tomoyuki Shimizu" href="https://github.com/tomoyukilabs/">
+<link rel="help" href="http://w3c.github.io/presentation-api/#sending-a-message-through-presentationconnection">
+<script src="../common.js"></script>
+<script src="stash.js"></script>
+
+<script>
+const stash = new Stash(stashIds.toReceiver, stashIds.toController);
+
+navigator.presentation.receiver.connectionList.then(list => {
+ const connection = list.connections[0];
+
+ const message1 = '1st';
+ const message2 = '2nd';
+ const message3 = new Uint8Array([51, 114, 100]); // "3rd"
+ const message4 = new Uint8Array([52, 116, 104]); // "4th"
+ const message5 = new Uint8Array([108, 97, 115, 116]); // "last"
+
+ // send messages
+ return stash.receive().then(() => {
+ connection.send(message1); // string
+ connection.send(message2); // string
+ connection.send(new Blob([message3])); // Blob
+ connection.send(message4.buffer); // ArrayBuffer
+ connection.send(message5); // ArrayBufferView
+
+ return stash.receive();
+ }).then(() => {
+ // try to send a message in the "closed" state
+ // Note: a receiving user agent does not fire a "terminate" event
+ connection.close();
+ connection.onclose = () => {
+ try {
+ connection.send(message1);
+ stash.send(JSON.stringify({ type: 'ok' }));
+ } catch (e) {
+ stash.send(JSON.stringify({ type: 'error', name: e.name, message: e.message }));
+ }
+ };
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_terminate_receiving-ua.html b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_terminate_receiving-ua.html
new file mode 100644
index 0000000000..3a234aaa83
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationConnection_terminate_receiving-ua.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8">
+<title>Terminating a presentation in a receiving browsing context</title>
+<link rel="author" title="Tomoyuki Shimizu" href="https://github.com/tomoyukilabs/">
+<link rel="help" href="https://w3c.github.io/presentation-api/#terminating-a-presentation-in-a-receiving-browsing-context">
+<script src="../common.js"></script>
+<script src="stash.js"></script>
+
+<script>
+ const stash = new Stash(stashIds.toReceiver, stashIds.toController);
+
+ navigator.presentation.receiver.connectionList.then(list => {
+ const connection = list.connections[0];
+
+ // terminate a presentation when two presentation connections become "connected"
+ list.onconnectionavailable = evt => {
+ connection.terminate();
+
+ stash.sendBeacon({ type: (window.closed ? 'ok' : 'error') });
+ };
+ });
+</script>
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_create_receiving-ua.html b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_create_receiving-ua.html
new file mode 100644
index 0000000000..53f7601442
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_create_receiving-ua.html
@@ -0,0 +1,328 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8">
+<title>Creating a receiving browsing context</title>
+<link rel="author" title="Tomoyuki Shimizu" href="https://github.com/tomoyukilabs/">
+<link rel="help" href="https://w3c.github.io/presentation-api/#creating-a-receiving-browsing-context">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../common.js"></script>
+<script src="stash.js"></script>
+<style>
+a { visibility: hidden; }
+iframe { width: 100%; }
+</style>
+
+<iframe id="child" src="PresentationReceiver_create_receiving-ua_child.html"></iframe>
+<p id="notice">Checking <code id="modal"></code>: if you see this message, please wait for test to time out.</p>
+<a href="PresentationReceiver_unreached_receiving-ua.html">do not navigate</a>
+<script>
+let finished = false;
+
+const sendResult = (test, status) => {
+ if(!finished) {
+ finished = true;
+ const stash = new Stash(stashIds.toReceiver, stashIds.toController);
+ stash.send(JSON.stringify({ test: test, status: status }));
+ }
+};
+
+add_completion_callback((tests, status) => {
+ // note: a single test result is supposed to appear here.
+ sendResult(tests[0], status);
+});
+
+const child = document.getElementById('child');
+child.addEventListener('load', () => {
+ const notice = document.getElementById('notice');
+ const modal = document.getElementById('modal');
+
+ const dbName = {
+ controller: 'db-presentation-api-controlling-ua',
+ receiver: 'db-presentation-api-receiving-ua'
+ };
+
+ promise_test(t => {
+ t.add_cleanup(() => {
+ document.cookie = cookieName + '=False;Expires=' + new Date().toUTCString();
+ document.cookie = cookieNameChild + '=False;Expires=' + new Date().toUTCString();
+ sessionStorage.removeItem(storageName);
+ localStorage.removeItem(storageName);
+ sessionStorage.removeItem(storageNameChild);
+ localStorage.removeItem(storageNameChild);
+
+ Object.values(dbName).forEach(name => {
+ indexedDB.deleteDatabase(name);
+ });
+
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.getRegistrations().then(registrations => {
+ return Promise.all(registrations.map(reg => reg.unregister()));
+ });
+ }
+ if ('caches' in window) {
+ caches.keys().then(keys => {
+ return Promise.all(keys.map(key => caches.delete(key)));
+ });
+ }
+ });
+
+ // Session history
+ assert_equals(window.history.length, 1, 'Session history consists of the current page only.');
+
+ // The Sandboxed auxiliary navigation browsing context flag
+ assert_equals(window.open('PresentationReceiver_unreached_receiving-ua.html'), null, 'Sandboxed auxiliary navigation browsing context flag is set.');
+
+ // The sandboxed top-level navigation browsing context flag (codes below are expected to be ignored)
+ window.close();
+ document.querySelector('a').click();
+ location.href = 'PresentationReceiver_unreached_receiving-ua.html';
+
+ // The sandboxed modals flag (codes below are expected to be ignored):
+ // If user agent prompts user, a timeout will occur and the test will eventually fail
+ let message = 'If you see this prompt, do not dismiss it and wait for test to time out.';
+ notice.style.display = 'block';
+ modal.textContent = 'alert()';
+ alert(message);
+ modal.textContent = 'confirm()';
+ confirm(message);
+ modal.textContent = 'print()';
+ print();
+ modal.textContent = 'prompt()';
+ prompt(message);
+ notice.style.display = 'none';
+
+ // Permissions
+ const checkPermission = query => {
+ return navigator.permissions ? navigator.permissions.query(query).then(status => {
+ assert_equals(status.state, 'denied', 'The state of the "' + query.name + '" permission is set to "denied".');
+ }, () => { /* skip this assertion if the specified permission is not implemented */ }) : Promise.resolve();
+ }
+
+ // Cookie
+ assert_equals(document.cookie, '', 'A cookie store is set to an empty store.')
+
+ // Indexed Database
+ const checkIndexedDB = () => {
+ if ('indexedDB' in window) {
+ // The test would fail when the database is already created by the controlling UA
+ const req = indexedDB.open(dbName.controller);
+ const upgradeneededWatcher = new EventWatcher(t, req, 'upgradeneeded');
+ const successWatcher = new EventWatcher(t, req, 'success');
+ return Promise.race([
+ upgradeneededWatcher.wait_for('upgradeneeded').then(evt => {
+ evt.target.result.close();
+ const req = indexedDB.open(dbName.receiver, 2);
+ const eventWatcher = new EventWatcher(t, req, 'upgradeneeded');
+ return eventWatcher.wait_for('upgradeneeded');
+ }).then(evt => {
+ evt.target.result.close();
+ }),
+ successWatcher.wait_for('success').then(evt => {
+ evt.target.result.close();
+ // This would fail if the database created by the controlling UA is visible to the receiving UA
+ assert_unreached('Indexed Database is set to an empty storage.');
+ })
+ ]);
+ }
+ else
+ return Promise.resolve();
+ };
+
+ // Web Storage
+ assert_equals(sessionStorage.length, 0, 'Session storage is set to an empty storage.');
+ assert_equals(localStorage.length, 0, 'Local storage is set to an empty storage.');
+
+ // Service Workers
+ const checkServiceWorkers = () => {
+ return 'serviceWorker' in navigator ? navigator.serviceWorker.getRegistrations().then(registrations => {
+ assert_equals(registrations.length, 0, 'List of registered service worker registrations is empty.');
+ }) : Promise.resolve();
+ };
+ const checkCaches = () => {
+ return 'caches' in window ? caches.keys().then(keys => {
+ assert_equals(keys.length, 0, 'Cache storage is empty.')
+ }) : Promise.resolve();
+ };
+
+ // Navigation
+ const checkNavigation = () => {
+ return navigator.presentation.receiver.connectionList.then(connectionList => {
+ assert_equals(connectionList.connections.length, 1, 'The initial number of presentation connections is one');
+ assert_equals(location.href, connectionList.connections[0].url, 'A receiving browsing context is navigated to the presentation URL.');
+ });
+ };
+
+ // Update storages and service workers shared with the top-level brosing context
+ const cookieName = 'PresentationApiTest';
+ const cookieValue = 'Receiving-UA';
+ const storageName = 'presentation_api_test';
+ const storageValue = 'receiving-ua';
+ document.cookie = cookieName + '=' + cookieValue;
+ sessionStorage.setItem(storageName, storageValue);
+ localStorage.setItem(storageName, storageValue);
+
+ // Service Workers and Caches
+ const cacheName = 'receiving-ua';
+ const getClientUrls = () => {
+ return new Promise(resolve => {
+ navigator.serviceWorker.getRegistration().then(reg => {
+ const channel = new MessageChannel();
+ channel.port1.onmessage = event => {
+ resolve(event.data);
+ };
+ reg.active.postMessage('test', [channel.port2]);
+ });
+ });
+ };
+
+ const registerServiceWorker = () => {
+ return ('serviceWorker' in navigator ?
+ navigator.serviceWorker.register('../serviceworker.js').then(registration => {
+ return new Promise((resolve, reject) => {
+ if (registration.installing) {
+ registration.installing.addEventListener('statechange', event => {
+ if(event.target.state === 'installed')
+ resolve();
+ });
+ }
+ else
+ resolve();
+ });
+ }) : Promise.resolve()).then(getClientUrls).then(urls => {
+ assert_true(urls.every(url => { return url !== new Request('../PresentationReceiver_create-manual.https.html').url }),
+ 'A window client in a controlling user agent is not accessible to a service worker on a receiving user agent.');
+ });
+ };
+
+ const openCaches = () => {
+ return 'caches' in window ? caches.open(cacheName).then(cache => cache.add('../cache.txt')) : Promise.resolve();
+ };
+
+ const getChildFrameResult = () => {
+ return new Promise(resolve => {
+ window.addEventListener('message', t.step_func(event => {
+ const result = event.data;
+ if (result.type === 'presentation-api') {
+ // if the test in iframe failed, report the result and abort the test
+ if (result.test.status === 0)
+ resolve();
+ else {
+ sendResult(result.test, result.status);
+ assert_unreached('A test for a nested browsing context failed.');
+ }
+ }
+ }));
+ child.contentWindow.postMessage('start', location.origin);
+ });
+ };
+
+ // check the results from updates by iframe
+ const cookieNameChild = 'NestedBrowsingContext';
+ const cookieValueChild = 'True';
+ const storageNameChild = 'nested_browsing_context';
+ const storageValueChild = 'yes';
+
+ const checkUpdatedResult = () => {
+ // cookie
+ const cookies = document.cookie.split(/;\s*/).reduce((result, item) => {
+ const t = item.split('=');
+ result[t[0]] = t[1];
+ return result;
+ }, {});
+ message = 'A cookie store is shared by top-level and nested browsing contexts.'
+ assert_equals(Object.keys(cookies).length, 2, message);
+ assert_equals(cookies[cookieName], cookieValue, message);
+ assert_equals(cookies[cookieNameChild], cookieValueChild, message);
+
+ // Web Storage
+ message = 'Session storage is shared by top-level and nested browsing contexts.';
+ assert_equals(sessionStorage.length, 2, message);
+ assert_equals(sessionStorage.getItem(storageName), storageValue, message);
+ assert_equals(sessionStorage.getItem(storageNameChild), storageValueChild, message);
+ message = 'Local storage is shared by top-level and nested browsing contexts.';
+ assert_equals(localStorage.length, 2, message);
+ assert_equals(localStorage.getItem(storageName), storageValue, message);
+ assert_equals(localStorage.getItem(storageNameChild), storageValueChild, message);
+ };
+
+ // Indexed Database
+ const checkUpdatedIndexedDB = () => {
+ if ('indexedDB' in window) {
+ message = 'Indexed Database is shared by top-level and nested browsing contexts.';
+ const req = indexedDB.open(dbName.receiver);
+ const upgradeneededWatcher = new EventWatcher(t, req, 'upgradeneeded');
+ const successWatcher = new EventWatcher(t, req, 'success');
+ return Promise.race([
+ upgradeneededWatcher.wait_for('upgradeneeded').then(evt => {
+ evt.target.result.close();
+ // Check if the version of the database is upgraded to 3 by the nested browsing context
+ assert_unreached(message);
+ }),
+ successWatcher.wait_for('success').then(evt => {
+ const db = evt.target.result;
+ const version = db.version;
+ db.close();
+ // Check if the version of the database is upgraded to 3 by the nested browsing context
+ assert_equals(version, 3, message);
+ })
+ ]);
+ }
+ else
+ return Promise.resolve();
+ };
+
+ // Service Workers
+ const checkUpdatedServiceWorkers = () => {
+ return 'serviceWorker' in window ? navigator.serviceWorker.getRegistrations().then(registrations => {
+ message = 'List of registered service worker registrations is shared by top-level and nested browsing contexts.';
+ assert_equals(registrations.length, 2, message);
+ const scriptURLs = registrations.map(reg => { return reg.active.scriptURL; }).sort();
+ assert_equals(scriptURLs[0], new Request('../serviceworker.js').url, message);
+ assert_equals(scriptURLs[1], new Request('serviceworker.js').url, message);
+ }) : Promise.resolve();
+ };
+ const cacheNameChild = 'nested-browsing-context';
+ const checkUpdatedCaches = () => {
+ message = 'Cache storage is shared by top-level and nested browsing contexts.';
+ return 'caches' in window ? caches.keys().then(keys => {
+ assert_equals(keys.length, 2, message);
+ const cacheKeys = keys.sort();
+ assert_equals(cacheKeys[0], cacheNameChild, message);
+ assert_equals(cacheKeys[1], cacheName, message);
+ return Promise.all(keys.map(
+ key => caches.open(key)
+ .then(cache => cache.matchAll())
+ .then(responses => {
+ assert_equals(responses.length, 1, message);
+ assert_equals(responses[0].url, new Request('../cache.txt').url, message);
+ })));
+ }) : Promise.resolve();
+ };
+
+ // Asynchronous tests
+ const permissionList = [
+ { name: 'geolocation' },
+ { name: 'notifications' },
+ { name: 'push', userVisibleOnly: true },
+ { name: 'midi' },
+ { name: 'camera' },
+ { name: 'microphone' },
+ { name: 'speaker' },
+ { name: 'background-sync' }
+ ];
+ return Promise.all(permissionList.map(perm => { return checkPermission(perm); }))
+ .then(checkIndexedDB)
+ .then(checkServiceWorkers)
+ .then(checkCaches)
+ .then(checkNavigation)
+ .then(registerServiceWorker)
+ .then(openCaches)
+ .then(getChildFrameResult)
+ .then(checkUpdatedResult)
+ .then(checkUpdatedIndexedDB)
+ .then(checkUpdatedServiceWorkers)
+ .then(checkUpdatedCaches);
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_create_receiving-ua_child.html b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_create_receiving-ua_child.html
new file mode 100644
index 0000000000..2f67f7fac0
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_create_receiving-ua_child.html
@@ -0,0 +1,171 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8">
+<title>Creating a receiving browsing context</title>
+<link rel="author" title="Tomoyuki Shimizu" href="https://github.com/tomoyukilabs/">
+<link rel="help" href="https://w3c.github.io/presentation-api/#creating-a-receiving-browsing-context">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p id="notice">Checking <code id="modal"></code>: if you see this message, please wait for test to time out.</p>
+
+<script>
+add_completion_callback((tests, status) => {
+ // remove unserializable attributes, then send the result to the parent window
+ // note: a single test result is supposed to appear here.
+ window.parent.postMessage(JSON.parse(JSON.stringify({
+ type: 'presentation-api', test: tests[0], status: status
+ })), location.origin);
+});
+
+window.addEventListener('message', event => {
+ if(event.data !== 'start')
+ return;
+
+ promise_test(t => {
+ const notice = document.getElementById('notice');
+ const modal = document.getElementById('modal');
+
+ // Presentation and PresentationReceiver
+ assert_true(navigator.presentation instanceof Presentation, 'navigator.presentation in a nested browsing context is an instanceof Presentation.');
+ assert_equals(navigator.presentation.receiver, null, 'navigator.presentation.receiver in a nested browsing context is set to null.');
+
+ // Session history
+ assert_equals(window.history.length, 1, 'Session history consists of the current page only.');
+
+ // The Sandboxed auxiliary navigation browsing context flag (codes below are expected to be ignored)
+ assert_equals(window.open('./idlharness_receiving-ua.html'), null, 'Sandboxed auxiliary navigation browsing context flag is set.');
+
+ // The sandboxed modals flag (codes below are expected to be ignored):
+ // If user agent prompts user, a timeout will occur and the test will eventually fail
+ let message = 'If you see this prompt, do not dismiss it and wait for test to time out.';
+ notice.style.display = 'block';
+ modal.textContent = 'alert()';
+ alert(message);
+ modal.textContent = 'confirm()';
+ confirm(message);
+ modal.textContent = 'print()';
+ print();
+ modal.textContent = 'prompt()';
+ prompt(message);
+ notice.style.display = 'none';
+
+ // Permissions
+ const checkPermission = query => {
+ return navigator.permissions ? navigator.permissions.query(query).then(status => {
+ assert_equals(status.state, 'denied', 'The state of the "' + query.name + '" permission in a nested browsing context is set to "denied".');
+ }, () => { /* skip this assertion if the specified permission is not implemented */ }) : Promise.resolve();
+ };
+
+ // Cookie
+ assert_equals(document.cookie, 'PresentationApiTest=Receiving-UA', 'A cookie store is shared by top-level and nested browsing contexts.');
+
+ // Indexed Database
+ const dbName = 'db-presentation-api-receiving-ua';
+ const checkIndexedDB = () => {
+ if ('indexedDB' in window) {
+ message = 'Indexed Database is shared by top-level and nested browsing contexts.';
+
+ const req = indexedDB.open(dbName);
+ const upgradeneededWatcher = new EventWatcher(t, req, 'upgradeneeded');
+ const successWatcher = new EventWatcher(t, req, 'success');
+ return Promise.race([
+ upgradeneededWatcher.wait_for('upgradeneeded').then(evt => {
+ evt.target.result.close();
+ // This would fail if the database is not created by the top-level browsing context
+ assert_unreached(message);
+ }),
+ successWatcher.wait_for('success').then(evt => {
+ evt.target.result.close();
+ const db = evt.target.result;
+ const version = db.version;
+ db.close();
+ // Check if the version of the database is 2 as specified by the top-level browsing context
+ assert_equals(version, 2, message);
+
+ // Upgrade the version
+ const req = indexedDB.open(dbName, 3);
+ const eventWatcher = new EventWatcher(t, req, 'upgradeneeded');
+ return eventWatcher.wait_for('upgradeneeded');
+ }).then(evt => {
+ evt.target.result.close();
+ })
+ ]);
+ }
+ else
+ return Promise.resolve();
+ };
+
+ // Web Storage
+ assert_equals(sessionStorage.length, 1, 'Session storage is shared by top-level and nested browsing contexts.');
+ assert_equals(sessionStorage.getItem('presentation_api_test'), 'receiving-ua', 'Session storage is shared by top-level and nested browsing contexts.');
+ assert_equals(localStorage.length, 1, 'Local storage is shared by top-level and nested browsing contexts.');
+ assert_equals(localStorage.getItem('presentation_api_test'), 'receiving-ua', 'Local storage is shared by top-level and nested browsing contexts.');
+
+ // Service Workers
+ const checkServiceWorkers = () => {
+ return 'serviceWorker' in navigator ? navigator.serviceWorker.getRegistrations().then(registrations => {
+ message = 'List of registered service worker registrations is shared by top-level and nested browsing contexts.';
+ assert_equals(registrations.length, 1, message);
+ assert_equals(registrations[0].active.scriptURL, new Request('../serviceworker.js').url, message);
+ }) : Promise.resolve();
+ };
+ const checkCaches = () => {
+ message = 'Cache storage is shared by top-level and nested browsing contexts.';
+ return 'caches' in window ? caches.keys().then(keys => {
+ assert_equals(keys.length, 1, message);
+ assert_equals(keys[0], 'receiving-ua', message);
+ return caches.open(keys[0]);
+ }).then(cache => cache.matchAll())
+ .then(responses => {
+ assert_equals(responses.length, 1, message);
+ assert_equals(responses[0].url, new Request('../cache.txt').url, message);
+ }) : Promise.resolve();
+ };
+
+ // Update storages and service workers shared with the top-level brosing context
+ document.cookie = 'NestedBrowsingContext=True';
+ sessionStorage.setItem('nested_browsing_context', 'yes');
+ localStorage.setItem('nested_browsing_context', 'yes');
+
+ // register a service worker with different scope from that of the top-level browsing context
+ const registerServiceWorker = () => {
+ return 'serviceWorker' in navigator ?
+ navigator.serviceWorker.register('serviceworker.js').then(registration => {
+ return new Promise((resolve, reject) => {
+ if (registration.installing) {
+ registration.installing.addEventListener('statechange', event => {
+ if(event.target.state === 'installed')
+ resolve();
+ });
+ }
+ else
+ resolve();
+ });
+ }) : Promise.resolve();
+ };
+
+ const openCaches = () => {
+ return 'caches' in window
+ ? caches.open('nested-browsing-context').then(cache => cache.add('../cache.txt')) : Promise.resolve();
+ };
+
+ // Asynchronous tests
+ const permissionList = [
+ { name: 'geolocation' },
+ { name: 'notifications' },
+ { name: 'push', userVisibleOnly: true },
+ { name: 'midi' },
+ { name: 'camera' },
+ { name: 'microphone' },
+ { name: 'speaker' },
+ { name: 'background-sync' }
+ ];
+ return Promise.all(permissionList.map(perm => { return checkPermission(perm); }))
+ .then(checkIndexedDB)
+ .then(checkServiceWorkers)
+ .then(checkCaches)
+ .then(registerServiceWorker)
+ .then(openCaches);
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_unreached_receiving-ua.html b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_unreached_receiving-ua.html
new file mode 100644
index 0000000000..2b2a8b315e
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/PresentationReceiver_unreached_receiving-ua.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8">
+<title>Creating a receiving browsing context</title>
+<link rel="author" title="Tomoyuki Shimizu" href="https://github.com/tomoyukilabs/">
+<link rel="help" href="https://w3c.github.io/presentation-api/#creating-a-receiving-browsing-context">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../common.js"></script>
+<script src="stash.js"></script>
+<script>
+add_completion_callback((tests, status) => {
+ const stash = new Stash(stashIds.toReceiver, stashIds.toController);
+ const log = document.getElementById('log');
+ stash.send(JSON.stringify({ test: tests[0], status: status, log: log.innerHTML }));
+});
+
+test(() => {
+ assert_unreached('Top-level navigation is disabled.');
+});
+</script>
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/idlharness_receiving-ua.html b/testing/web-platform/tests/presentation-api/receiving-ua/support/idlharness_receiving-ua.html
new file mode 100644
index 0000000000..2d5a5b17f1
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/idlharness_receiving-ua.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Presentation API IDL tests for Receiving User Agent</title>
+<link rel="author" title="Louay Bassbouss" href="http://www.fokus.fraunhofer.de">
+<link rel="author" title="Tomoyuki Shimizu" href="https://github.com/tomoyukilabs">
+<link rel="help" href="http://w3c.github.io/presentation-api/#dfn-receiving-user-agent">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../common.js"></script>
+<script src="stash.js"></script>
+
+<script>
+ 'use strict';
+ (async () => {
+ const srcs = ['presentation-api', 'dom', 'html'];
+ const [idl, dom, html] = await Promise.all(
+ srcs.map(i => fetch(`/interfaces/${i}.idl`).then(r => r.text())));
+
+ const idl_array = new IdlArray();
+ idl_array.add_idls(idl, {
+ except: [
+ 'PresentationRequest',
+ 'PresentationAvailability',
+ ]
+ });
+ idl_array.add_dependency_idls(dom);
+ idl_array.add_dependency_idls(html);
+ idl_array.add_objects({
+ Presentation: ['navigator.presentation'],
+ PresentationReceiver: ['navigator.presentation.receiver']
+ });
+ add_completion_callback((tests, status) => {
+ const stash = new Stash(stashIds.toReceiver, stashIds.toController);
+ const log = document.getElementById('log');
+ stash.send(JSON.stringify({ tests: tests, status: status, log: log.innerHTML }));
+ });
+ idl_array.test();
+ })();
+</script>
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/iframe.html b/testing/web-platform/tests/presentation-api/receiving-ua/support/iframe.html
new file mode 100644
index 0000000000..61f18b8912
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/iframe.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Helper functions invoked by a nested browsing context</title>
+<link rel="author" title="Tomoyuki Shimizu" href="https://github.com/tomoyukilabs">
+<link rel="help" href="https://w3c.github.io/presentation-api/#monitoring-incoming-presentation-connections">
+
+<script>
+ window.addEventListener('message', event => {
+ const data = event.data;
+
+ if (!data.type)
+ return;
+ else if (data.type === 'connect') {
+ const request = new PresentationRequest(data.url);
+ request.reconnect(data.id).then(c => {
+ c.onterminate = () => {
+ window.parent.postMessage({ type: 'terminated' }, '*');
+ };
+ });
+ }
+ });
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/serviceworker.js b/testing/web-platform/tests/presentation-api/receiving-ua/support/serviceworker.js
new file mode 100644
index 0000000000..d2d4449ded
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/serviceworker.js
@@ -0,0 +1,9 @@
+self.addEventListener('install', () => {
+ // activate this service worker immediately
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', event => {
+ // let this service worker control window clients immediately
+ event.waitUntil(self.clients.claim());
+});
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/stash.js b/testing/web-platform/tests/presentation-api/receiving-ua/support/stash.js
new file mode 100644
index 0000000000..eb63bb8623
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/stash.js
@@ -0,0 +1,98 @@
+var Stash = function(inbound, outbound) {
+ this.stashPath = '/presentation-api/receiving-ua/support/stash.py?id=';
+ this.inbound = inbound;
+ this.outbound = outbound;
+}
+
+// initialize a stash on wptserve
+Stash.prototype.init = function() {
+ return Promise.all([
+ fetch(this.stashPath + this.inbound).then(response => {
+ return response.text();
+ }),
+ fetch(this.stashPath + this.outbound).then(response => {
+ return response.text();
+ })
+ ]);
+}
+
+// upload a test result to a stash on wptserve
+Stash.prototype.send = function(result) {
+ return fetch(this.stashPath + this.outbound, {
+ method: 'POST',
+ body: JSON.stringify({ type: 'data', data: result })
+ }).then(response => {
+ return response.text();
+ }).then(text => {
+ return text === 'ok' ? null : Promise.reject();
+ })
+};
+
+// upload a test result to a stash on wptserve via navigator.sendBeacon
+Stash.prototype.sendBeacon = function(result) {
+ if ('sendBeacon' in navigator) {
+ navigator.sendBeacon(this.stashPath + this.outbound, JSON.stringify({ type: 'data', data: result }));
+ }
+ // Note: The following could be discarded, since XHR in synchronous mode is now being deprecated.
+ else {
+ return new Promise(resolve, reject => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', this.stashPath + this.outbound, false);
+ xhr.send(JSON.stringify({ type: 'data', data: result }));
+ });
+ }
+};
+
+// wait until a test result is uploaded to a stash on wptserve
+Stash.prototype.receive = function() {
+ return new Promise((resolve, reject) => {
+ let intervalId;
+ const interval = 500; // msec
+ const polling = () => {
+ return fetch(this.stashPath + this.inbound).then(response => {
+ return response.text();
+ }).then(text => {
+ if (text) {
+ try {
+ const json = JSON.parse(text);
+ if (json.type === 'data')
+ resolve(json.data);
+ else
+ reject();
+ } catch(e) {
+ resolve(text);
+ }
+ clearInterval(intervalId);
+ }
+ });
+ };
+ intervalId = setInterval(polling, interval);
+ });
+};
+
+// reset a stash on wptserve
+Stash.prototype.stop = function() {
+ return Promise.all([
+ fetch(this.stashPath + this.inbound).then(response => {
+ return response.text();
+ }),
+ fetch(this.stashPath + this.outbound).then(response => {
+ return response.text();
+ })
+ ]).then(() => {
+ return Promise.all([
+ fetch(this.stashPath + this.inbound, {
+ method: 'POST',
+ body: JSON.stringify({ type: 'stop' })
+ }).then(response => {
+ return response.text();
+ }),
+ fetch(this.stashPath + this.outbound, {
+ method: 'POST',
+ body: JSON.stringify({ type: 'stop' })
+ }).then(response => {
+ return response.text();
+ })
+ ]);
+ });
+}
diff --git a/testing/web-platform/tests/presentation-api/receiving-ua/support/stash.py b/testing/web-platform/tests/presentation-api/receiving-ua/support/stash.py
new file mode 100644
index 0000000000..83653d365a
--- /dev/null
+++ b/testing/web-platform/tests/presentation-api/receiving-ua/support/stash.py
@@ -0,0 +1,10 @@
+def main(request, response):
+ key = request.GET.first(b"id")
+
+ if request.method == u"POST":
+ request.server.stash.put(key, request.body)
+ return b"ok"
+ else:
+ value = request.server.stash.take(key)
+ assert request.server.stash.take(key) is None
+ return value \ No newline at end of file