<!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="support/stash.js"></script>

<p id="notice">
  Click the button below and select the available presentation display, to start the manual test. The test passes if a "PASS" result appears.<br>
  This test asks you to click the button twice, unless the test fails.<br>
  <button id="presentBtn">Start Presentation Test</button>
</p>

<script>
    setup({explicit_timeout: true});

    let receiverStack;
    add_completion_callback(() => {
        // overwrite a stack written in the test result
        if (receiverStack) {
            document.querySelector('#log pre').textContent = receiverStack;
        }
    });

    let connection;
    const presentBtn = document.getElementById('presentBtn');

    const dbName = {
        controller: 'db-presentation-api-controlling-ua',
        receiver: 'db-presentation-api-receiving-ua'
    };

    const main = () => {
        promise_test(t => {
            presentBtn.disabled = true;
            const stash = new Stash(stashIds.toController, stashIds.toReceiver);
            const request = new PresentationRequest('support/PresentationReceiver_create_receiving-ua.html');

            t.add_cleanup(() => {
                const notice = document.getElementById('notice');
                notice.parentNode.removeChild(notice);
                stash.stop();

                // history.back();
                document.cookie = 'PresentationApiTest=true; Expires=' + new Date().toUTCString();
                sessionStorage.removeItem('presentation_api_test');
                localStorage.removeItem('presentation_api_test');

                Object.values(dbName).forEach(name => {
                    indexedDB.deleteDatabase(name);
                });

                if (connection) {
                    connection.onconnect = () => { connection.terminate(); };
                    if (connection.state === 'closed')
                        request.reconnect(connection.id);
                    else
                        connection.terminate();
                }

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

            history.pushState(null, 'test', 'PresentationReceiver_create-manual.https.html');
            document.cookie = 'PresentationApiTest=Controlling-UA';

            const storageName = 'presentation_api_test';
            const storageValue = 'receiving-ua';
            sessionStorage.setItem(storageName, storageValue);
            localStorage.setItem(storageName, storageValue);

            const openIndexedDB = () => {
                if ('indexedDB' in window) {
                    const req = indexedDB.open(dbName.controller, 1);
                    const eventWatcher = new EventWatcher(t, req, 'upgradeneeded');
                    return eventWatcher.wait_for('upgradeneeded').then(evt => {
                        evt.target.result.close();
                    });
                }
                else
                    return Promise.resolve();
            };

            const cacheName = 'controlling-ua';
            let clientUrls;
            const getClientUrls = () => {
                return new Promise(resolve => {
                    navigator.serviceWorker.getRegistration().then(reg => {
                        if (reg) {
                            const channel = new MessageChannel();
                            channel.port1.onmessage = event => {
                                resolve(event.data);
                            };
                            reg.active.postMessage('', [channel.port2]);
                        }
                        else
                            resolve([]);
                    });
                });
            };
            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 => {
                        clientUrls = urls;
                    });
            };
            const openCaches = () => {
                return 'caches' in window ? caches.open(cacheName).then(cache => cache.add('cache.txt')) : Promise.resolve();
            };

            const checkUpdates = () => {
                // Cookie
                assert_equals(document.cookie, 'PresentationApiTest=Controlling-UA', 'A cookie store is not shared with a receiving user agent.');

                // Web Storage
                assert_equals(sessionStorage.length, 1, 'Session storage is not shared with a receiving user agent.');
                assert_equals(sessionStorage.getItem(storageName), storageValue, 'Session storage is not shared with a receiving user agent.');
                assert_equals(localStorage.length, 1, 'Local storage is not shared with a receiving user agent.');
                assert_equals(localStorage.getItem(storageName), storageValue, 'Local storage is not shared with a receiving user agent.');
            };

            // Indexed Database
            const checkIndexedDB = t => {
                if ('indexedDB' in window) {
                    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();
                        }),
                        successWatcher.wait_for('success').then(evt => {
                            evt.target.result.close();
                            // This would fail if the database created by the receiving UA is visible to the controlling UA
                            assert_unreached('Indexed Database is not shared with a receiving user agent.');
                        })
                    ]);
                }
                else
                    return Promise.resolve();
            };

            // Service Workers
            const checkServiceWorkers = () => {
                return 'serviceWorker' in navigator ? navigator.serviceWorker.getRegistrations().then(registrations => {
                    const message = 'List of registered service worker registrations is not shared with a receiving user agent.';
                    assert_equals(registrations.length, 1, message);
                    assert_equals(registrations[0].active.scriptURL, new Request('serviceworker.js').url, message);
                }) : Promise.resolve();
            };
            const checkCaches = () => {
                const message = 'Cache storage is not shared with a receiving user agent.';
                return 'caches' in window ? caches.keys().then(keys => {
                    assert_equals(keys.length, 1, message);
                    assert_equals(keys[0], cacheName, 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();
            };

            let timeout;
            const enableTimeout = () => {
                timeout = t.step_timeout(() => {
                    t.force_timeout();
                    t.done();
                }, 5000);
            };
            const disableTimeout = () => {
                clearTimeout(timeout);
            };
            const cancelWait = () => {
                connection.removeEventListener('close', onTerminate);
                connection.removeEventListener('terminate', onTerminate);
            };
            const onTerminate = (reject, event) => {
                cancelWait();
                reject('A receiving user agent unexpectedly ' + event.type + 'd a presentation. ');
            };
            const waitForTerminate = () => {
                return new Promise((resolve, reject) => {
                    connection.addEventListener('close', onTerminate.bind(this, reject));
                    connection.addEventListener('terminate', onTerminate.bind(this, reject));
                });
            };

            // Start a presentation for receiving user agent tests
            return request.start().then(c => {
                connection = c;
                enableTimeout();

                // This Promise.race will be rejected if a receiving side terminates/closes the connection when window.close() is invoked
                return Promise.race([
                    openIndexedDB()
                        .then(registerServiceWorker)
                        .then(openCaches)
                        .then(() => { return stash.init(); })
                        .then(() => { return stash.receive(); }),
                    waitForTerminate()]);
            }).then(result => {
                // terminate and connect again if the result is PASS
                cancelWait();
                const json = JSON.parse(result);
                if (json.test.status !== 0)
                    return json;

                // Check accessibility to window clients before terminating a presentation
                return getClientUrls().then(urls => {
                    assert_true(urls.length === clientUrls.length && urls.every((value, index) => { return clientUrls[index] === value}),
                        'A window client in a receiving user agent is not accessible to a service worker on a controlling user agent.');
                    const eventWatcher = new EventWatcher(t, connection, 'terminate');
                    connection.terminate();
                    return eventWatcher.wait_for('terminate');
                }).then(() => {
                    connection = null;
                    disableTimeout();
                    presentBtn.removeEventListener('click', main);
                    presentBtn.textContent = 'Continue Presentation Test';
                    presentBtn.disabled = false;
                    const eventWatcher = new EventWatcher(t, presentBtn, 'click');
                    return eventWatcher.wait_for('click');
                }).then(() => {
                    presentBtn.disabled = true;
                    return request.start();
                }).then(c => {
                    connection = c;
                    enableTimeout();
                    return stash.receive();
                }).then(result => {
                    return JSON.parse(result);
                });
            }).then(json => {
                if (json.test.status === 0) {
                    checkUpdates();
                    return checkIndexedDB(t)
                            .then(checkServiceWorkers)
                            .then(checkCaches);
                }
                else {
                    receiverStack = json.test.stack;
                    parseResult(json.test.message);
                }
            });
        });
    };
    presentBtn.addEventListener('click', main);
</script>
