Просмотр исходного кода

test: move the e-mail tests into the TopNav tests

As suggested in review, the window.open mock is created in beforeEach
and restored in afterEach instead of try/finally blocks in each test.
The tests also move from emailTemplateTest.js, which mocha lists last,
into TopNav.js, as describe('showEmailButton & hideEmailButton') and
describe('sendEmail') with test names in the style of the other tests.
The helpers are merged into buildEmailDom, mockWindowOpen and
mailtoParameter, the fixtures use Bootstrap 5 markup, and the property
test resets the mock instead of reloading the environment on each run.
El RIDO 5 дней назад
Родитель
Сommit
d0bcc82f64
2 измененных файлов с 280 добавлено и 397 удалено
  1. 280 9
      js/test/TopNav.js
  2. 0 388
      js/test/emailTemplateTest.js

+ 280 - 9
js/test/TopNav.js

@@ -9,30 +9,301 @@ function query(selector) {
     return document.querySelector(selector);
     return document.querySelector(selector);
 }
 }
 
 
+/**
+ * builds the navigation with the e-mail button and its confirmation dialog, as
+ * in the bootstrap5 template
+ *
+ * @param {boolean} withShortUrl - add the link of a shortened document URL
+ */
+function buildEmailDom(withShortUrl) {
+    document.documentElement.innerHTML =
+        '<nav><div id="navbar"><ul><li>' +
+        '<button id="clonebutton" type="button" class="btn btn-secondary">' +
+        '<svg width="16" height="16" fill="currentColor" aria-hidden="true">' +
+        '<use href="img/bootstrap-icons.svg#copy"></use></svg> Clone</button>' +
+        '</li><li>' +
+        '<button id="emaillink" type="button" class="hidden btn btn-secondary">Email</button>' +
+        '</li></ul></div></nav>' +
+        '<input id="burnafterreadingoption" type="checkbox">' +
+        (withShortUrl ?
+            '<div id="pastelink">Your document is ' +
+            '<a id="pasteurl" href="https://short.example/xYz">https://short.example/xYz</a> ' +
+            '<span id="copyhint">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span></div>' :
+            '') +
+        '<div id="emailconfirmmodal" tabindex="-1" class="modal" role="dialog" aria-hidden="true">' +
+        '<div class="modal-dialog" role="document"><div class="modal-content"><div class="modal-body row">' +
+        '<button id="emailconfirm-timezone-current" type="button">Use Current Timezone</button>' +
+        '<button id="emailconfirm-timezone-utc" type="button">Convert To UTC</button>' +
+        '</div></div></div></div>';
+}
+
+/**
+ * replaces window.open, to record the e-mails sent via mailto: URLs
+ *
+ * @returns {{getUrl: function, getCount: function, reset: function, restore: function}}
+ */
+function mockWindowOpen() {
+    const originalOpen = window.open;
+    let openedUrl = null,
+        openedCount = 0;
+    window.open = function (url) {
+        openedUrl = url;
+        ++openedCount;
+        return {};
+    };
+    return {
+        getUrl: () => openedUrl,
+        getCount: () => openedCount,
+        reset: () => {
+            openedUrl = null;
+            openedCount = 0;
+        },
+        restore: () => {
+            window.open = originalOpen;
+        }
+    };
+}
+
+/**
+ * extracts and decodes a parameter of a mailto: URL
+ *
+ * @param {string} mailtoUrl
+ * @param {string} name - i.e. subject or body
+ * @returns {string}
+ */
+function mailtoParameter(mailtoUrl, name) {
+    assert.match(mailtoUrl, /^mailto:\?/, 'expected a mailto: URL');
+    const match = mailtoUrl.match(new RegExp('[?&]' + name + '=([^&]*)'));
+    assert.ok(match, 'expected the mailto: URL to have a ' + name + '= parameter');
+    return decodeURIComponent(match[1]);
+}
+
 describe('TopNav', function () {
 describe('TopNav', function () {
-    describe('showEmailButton', function () {
+    describe('showEmailButton & hideEmailButton', function () {
+        let windowOpen;
+
+        beforeEach(function () {
+            cleanup();
+            windowOpen = mockWindowOpen();
+        });
+
         afterEach(function () {
         afterEach(function () {
+            windowOpen.restore();
             cleanup();
             cleanup();
         });
         });
 
 
-        it('does nothing when the email button is omitted from the page', function () {
+        it('does nothing when the e-mail button is omitted from the page', function () {
             document.documentElement.innerHTML =
             document.documentElement.innerHTML =
                 '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
                 '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
                 'type="button">New</button></li></ul></div></nav>';
                 'type="button">New</button></li></ul></div></nav>';
             PrivateBin.TopNav.init();
             PrivateBin.TopNav.init();
 
 
-            const originalShowError = PrivateBin.Alert.showError;
+            // cleanup() reloads PrivateBin, dropping this replacement
             let errorShown = false;
             let errorShown = false;
             PrivateBin.Alert.showError = function () {
             PrivateBin.Alert.showError = function () {
                 errorShown = true;
                 errorShown = true;
             };
             };
 
 
-            try {
-                assert.doesNotThrow(() => PrivateBin.TopNav.showEmailButton());
-                assert.strictEqual(errorShown, false);
-            } finally {
-                PrivateBin.Alert.showError = originalShowError;
-            }
+            assert.doesNotThrow(() => PrivateBin.TopNav.showEmailButton());
+            assert.strictEqual(errorShown, false);
+        });
+
+        it('sends a single e-mail after the confirmation dialog was dismissed', function () {
+            buildEmailDom(false);
+            PrivateBin.TopNav.init();
+            PrivateBin.TopNav.showEmailButton(3600);
+
+            // open the dialog and dismiss it without choosing a timezone
+            query('#emaillink').click();
+            assert.strictEqual(windowOpen.getCount(), 0, 'no e-mail before choosing a timezone');
+
+            query('#emaillink').click();
+            query('#emailconfirm-timezone-current').click();
+            assert.strictEqual(windowOpen.getCount(), 1, 'exactly one e-mail with the current timezone');
+
+            query('#emaillink').click();
+            query('#emailconfirm-timezone-utc').click();
+            assert.strictEqual(windowOpen.getCount(), 2, 'exactly one more e-mail with UTC');
+        });
+
+        it('sends a single e-mail per click after the button was shown again', function () {
+            buildEmailDom(false);
+            PrivateBin.TopNav.init();
+            // i.e. sending a second document without reloading the page
+            PrivateBin.TopNav.showEmailButton(0);
+            PrivateBin.TopNav.showEmailButton(0);
+
+            query('#emaillink').click();
+            assert.strictEqual(windowOpen.getCount(), 1, 'exactly one e-mail per click');
+        });
+
+        it('uses the expiration of the document the button was last shown for', function () {
+            buildEmailDom(false);
+            PrivateBin.TopNav.init();
+            // first document expires, the second never does
+            PrivateBin.TopNav.showEmailButton(3600);
+            PrivateBin.TopNav.showEmailButton(0);
+
+            query('#emaillink').click();
+            assert.strictEqual(windowOpen.getCount(), 1, 'sent directly, without confirmation dialog');
+            assert.doesNotMatch(mailtoParameter(windowOpen.getUrl(), 'body'), /expire/i, 'body has no expiration');
+
+            query('#emailconfirm-timezone-current').click();
+            assert.strictEqual(windowOpen.getCount(), 1, 'no e-mail from a stale confirmation dialog');
+        });
+
+        it('sends no e-mail from a confirmation dialog opened for an earlier document', function () {
+            buildEmailDom(false);
+            PrivateBin.TopNav.init();
+            PrivateBin.TopNav.showEmailButton(3600);
+
+            // open the dialog, but start a new document instead of choosing
+            query('#emaillink').click();
+            PrivateBin.TopNav.hideEmailButton();
+            PrivateBin.TopNav.showEmailButton(0);
+
+            query('#emailconfirm-timezone-current').click();
+            query('#emailconfirm-timezone-utc').click();
+            assert.strictEqual(windowOpen.getCount(), 0, 'no e-mail from a stale confirmation dialog');
+        });
+
+        it('sends no e-mail after the button was hidden', function () {
+            buildEmailDom(false);
+            PrivateBin.TopNav.init();
+            PrivateBin.TopNav.showEmailButton(0);
+            PrivateBin.TopNav.hideEmailButton();
+
+            // i.e. a keyboard shortcut or assistive technology activating it
+            query('#emaillink').click();
+            assert.strictEqual(windowOpen.getCount(), 0, 'no e-mail from a hidden button');
+        });
+
+        it('sends exactly one e-mail, regardless of how often the button was shown or the dialog dismissed', function () {
+            fc.assert(fc.property(
+                fc.integer({min: 1, max: 5}),
+                fc.integer({min: 0, max: 5}),
+                fc.constantFrom('current', 'utc'),
+                function (shown, dismissed, timezone) {
+                    // replacing the markup is enough, init binds to the new elements
+                    buildEmailDom(false);
+                    PrivateBin.TopNav.init();
+                    for (let i = 0; i < shown; ++i) {
+                        PrivateBin.TopNav.showEmailButton(3600);
+                    }
+                    windowOpen.reset();
+
+                    for (let i = 0; i < dismissed; ++i) {
+                        query('#emaillink').click();
+                    }
+                    query('#emaillink').click();
+                    query('#emailconfirm-timezone-' + timezone).click();
+                    assert.strictEqual(windowOpen.getCount(), 1);
+                }
+            ));
+        });
+    });
+
+    describe('sendEmail', function () {
+        let windowOpen;
+
+        beforeEach(function () {
+            cleanup();
+            windowOpen = mockWindowOpen();
+        });
+
+        afterEach(function () {
+            windowOpen.restore();
+            cleanup();
+        });
+
+        it('puts the short URL into the mail body, if there is one', function () {
+            buildEmailDom(true);
+            PrivateBin.TopNav.init();
+            PrivateBin.TopNav.showEmailButton(0);
+            assert.ok(!query('#emaillink').classList.contains('hidden'), 'e-mail button is shown');
+
+            query('#emaillink').click();
+            assert.ok(windowOpen.getUrl(), 'an e-mail was sent');
+            const body = mailtoParameter(windowOpen.getUrl(), 'body');
+            assert.match(body, /https:\/\/short\.example\/xYz/, 'body contains the short URL');
+            assert.doesNotMatch(body, /undefined/, 'body never contains "undefined"');
+        });
+
+        it('puts the page URL into the mail body, if there is no short URL', function () {
+            buildEmailDom(false);
+            PrivateBin.TopNav.init();
+            PrivateBin.TopNav.showEmailButton(0);
+            assert.ok(!query('#emaillink').classList.contains('hidden'), 'e-mail button is shown');
+
+            query('#emaillink').click();
+            assert.ok(windowOpen.getUrl(), 'an e-mail was sent');
+            const body = mailtoParameter(windowOpen.getUrl(), 'body');
+            assert.match(body, new RegExp(window.location.href), 'body contains the page URL');
+            assert.doesNotMatch(body, /undefined/, 'body never contains "undefined"');
+        });
+
+        it('uses the burn-after-reading state of the viewed document, not the instance default', function () {
+            buildEmailDom(false);
+            const burnAfterReading = document.createElement('input');
+            burnAfterReading.id = 'burnafterreading';
+            burnAfterReading.type = 'checkbox';
+            burnAfterReading.checked = true;
+            document.body.appendChild(burnAfterReading);
+            PrivateBin.TopNav.init();
+            PrivateBin.TopNav.showEmailButton(0, false);
+
+            query('#emaillink').click();
+            assert.doesNotMatch(
+                mailtoParameter(windowOpen.getUrl(), 'body'),
+                /only be accessed once/,
+                'body uses the metadata of the viewed document'
+            );
+        });
+
+        it('names the instance in the mail subject', function () {
+            buildEmailDom(false);
+            // buildEmailDom() replaces the whole document, including its title
+            document.title = 'My PrivateBin Instance';
+            PrivateBin.TopNav.init();
+            PrivateBin.TopNav.showEmailButton(0);
+
+            query('#emaillink').click();
+            assert.ok(windowOpen.getUrl(), 'an e-mail was sent');
+            assert.match(mailtoParameter(windowOpen.getUrl(), 'subject'), /My PrivateBin Instance/);
+        });
+
+        it('names the instance in the mail subject after the expiration was confirmed', function () {
+            buildEmailDom(true);
+            document.title = 'My PrivateBin Instance';
+            PrivateBin.TopNav.init();
+            // a remaining time asks to confirm the timezone of the expiration
+            PrivateBin.TopNav.showEmailButton(3600);
+
+            query('#emaillink').click();
+            query('#emailconfirm-timezone-current').click();
+            assert.ok(windowOpen.getUrl(), 'an e-mail was sent');
+            assert.match(mailtoParameter(windowOpen.getUrl(), 'subject'), /My PrivateBin Instance/);
+        });
+
+        it('percent-encodes the mail subject, i.e. spaces in the instance name', function () {
+            buildEmailDom(false);
+            document.title = 'My Cool Instance';
+            PrivateBin.TopNav.init();
+            PrivateBin.TopNav.showEmailButton(0);
+
+            query('#emaillink').click();
+            const rawSubjectParam = windowOpen.getUrl().split('&body=')[0];
+            assert.strictEqual(
+                rawSubjectParam,
+                `mailto:?subject=${encodeURIComponent('Encrypted note on My Cool Instance')}`,
+                'raw subject parameter is exactly the encoded translated string'
+            );
+            assert.doesNotMatch(rawSubjectParam, / /, 'raw mailto: URL contains no literal space');
+            assert.strictEqual(
+                mailtoParameter(windowOpen.getUrl(), 'subject'),
+                'Encrypted note on My Cool Instance',
+                'decoded subject round-trips exactly'
+            );
         });
         });
     });
     });
 
 

+ 0 - 388
js/test/emailTemplateTest.js

@@ -1,388 +0,0 @@
-'use strict';
-require('../common');
-const fc = require('fast-check');
-
-// DOM builder that mirrors bootstrap5.php navbar
-function buildEmailDomNoShortUrl() {
-    document.documentElement.innerHTML =
-        // TopNav expects initially hidden #emaillink BUTTON.
-        '<nav><div id="navbar"><ul>' +
-          '<li>' +
-            '<button id="clonebutton" type="button" class="btn btn-warning navbar-btn">' +
-              '<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> Clone' +
-            '</button>' +
-          '</li>' +
-          '<li>' +
-            '<button id="emaillink" type="button" class="hidden btn btn-secondary">Email</button>' +
-          '</li>' +
-        '</ul></div></nav>' +
-        '<input id="burnafterreadingoption" type="checkbox">' +
-        // include dummy email confirm modal for sendEmail
-        '<div id="emailconfirmmodal" class="hidden">' +
-            '<div id="emailconfirm-timezone-current"></div>' +
-            '<div id="emailconfirm-timezone-utc"></div>' +
-        '</div>'
-}
-
-// DOM builder that adds the shortener result block
-function buildEmailDomWithShortUrl() {
-    buildEmailDomNoShortUrl();
-    document.documentElement.innerHTML =
-        // TopNav expectsinitially hidden #emaillink BUTTON.
-        '<nav><div id="navbar"><ul>' +
-          '<li>' +
-            '<button id="clonebutton" type="button" class="btn btn-warning navbar-btn">' +
-              '<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> Clone' +
-            '</button>' +
-          '</li>' +
-          '<li>' +
-            '<button id="emaillink" type="button" class="hidden btn btn-secondary">Email</button>' +
-          '</li>' +
-        '</ul></div></nav>' +
-        '<input id="burnafterreadingoption" type="checkbox">' +
-        '<div id="pastelink">Your document is ' +
-          '<a id="pasteurl" href="https://short.example/xYz">https://short.example/xYz</a> ' +
-          '<span id="copyhint">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span>' +
-        '</div>' +
-        // add a minimal email confirmation modal so sendEmail does not crash
-        '<div id="emailconfirmmodal" class="hidden">' +
-            '<div id="emailconfirm-timezone-current"></div>' +
-            '<div id="emailconfirm-timezone-utc"></div>' +
-        '</div>'
-}
-
-
-function makeWindowOpenMock() {
-    const originalOpen = window.open;
-    let openedUrl = null;
-    let openedCount = 0;
-    let mockRestoreFn = null;
-
-    if (typeof jest !== 'undefined' && typeof jest.spyOn === 'function') {
-        const spy = jest.spyOn(window, 'open').mockImplementation((url) => {
-            openedUrl = url;
-            openedCount++;
-            return {};
-        });
-        mockRestoreFn = () => spy.mockRestore();
-    } else {
-        window.open = function (url) {
-            openedUrl = url;
-            openedCount++;
-            return {};
-        };
-        mockRestoreFn = () => { window.open = originalOpen; };
-    }
-
-    return {
-        getUrl: () => openedUrl,
-        getCount: () => openedCount,
-        restore: () => {
-            if (mockRestoreFn) {
-                mockRestoreFn();
-            }
-        }
-    };
-}
-
-
-// Extract and decode the body from a "mailto:?subject=...&body=..." URL.
-function extractMailtoBody(mailtoUrl) {
-    assert.match(mailtoUrl, /^mailto:\?/, 'expected a mailto: URL');
-    const match = mailtoUrl.match(/[?&]body=([^&]*)/);
-    assert.ok(match, 'expected the mailto: URL to have a body= parameter');
-    return decodeURIComponent(match[1]);
-}
-
-// Extract and decode the subject from a "mailto:?subject=...&body=..." URL.
-function extractMailtoSubject(mailtoUrl) {
-    assert.match(mailtoUrl, /^mailto:\?/, 'expected a mailto: URL');
-    const match = mailtoUrl.match(/[?&]subject=([^&]*)/);
-    assert.ok(match, 'expected the mailto: URL to have a subject= parameter');
-    return decodeURIComponent(match[1]);
-}
-
-describe('Email - mail body content (short URL vs. fallback)', function () {
-    beforeEach(function () {
-        cleanup(); // provided by common
-    });
-
-    it('Uses the short URL when #pasteurl is present and never includes "undefined"', function () {
-        buildEmailDomWithShortUrl(); // with #pastelink/#pasteurl
-        PrivateBin.TopNav.init();
-        PrivateBin.TopNav.showEmailButton(0);
-
-        const emailBtn = document.getElementById('emaillink');
-        assert.ok(!emailBtn.classList.contains('hidden'), '#emaillink should be visible after showEmailButton');
-
-        const { getUrl, restore } = makeWindowOpenMock();
-        try {
-            emailBtn.click();
-            document.getElementById('emailconfirm-timezone-current').click();
-
-            const openedUrl = getUrl();
-            assert.ok(openedUrl, 'window.open should have been called');
-
-            const body = extractMailtoBody(openedUrl);
-            assert.match(body, /https:\/\/short\.example\/xYz/, 'email body should include the short URL');
-            assert.doesNotMatch(body, /undefined/, 'email body must not contain "undefined"');
-        } finally {
-            restore();
-        }
-    });
-
-    it('Falls back to window.location.href when #pasteurl is absent and never includes "undefined"', function () {
-        buildEmailDomNoShortUrl(); // No #pasteurl
-        PrivateBin.TopNav.init();
-        PrivateBin.TopNav.showEmailButton(0);
-
-        const emailBtn = document.getElementById('emaillink');
-        assert.ok(!emailBtn.classList.contains('hidden'), '#emaillink should be visible after showEmailButton');
-
-        const { getUrl, restore } = makeWindowOpenMock();
-        try {
-            emailBtn.click();
-            document.getElementById('emailconfirm-timezone-current').click();
-
-            const openedUrl = getUrl();
-            assert.ok(openedUrl, 'window.open should have been called');
-
-            const body = extractMailtoBody(openedUrl);
-            assert.match(body, new RegExp(window.location.href), 'email body should include the fallback page URL');
-            assert.doesNotMatch(body, /undefined/, 'email body must not contain "undefined"');
-        } finally {
-            restore();
-        }
-    });
-
-    it('Uses the viewed paste burn-after-reading state instead of the instance default', function () {
-        buildEmailDomNoShortUrl();
-        const burnAfterReading = document.createElement('input');
-        burnAfterReading.id = 'burnafterreading';
-        burnAfterReading.type = 'checkbox';
-        burnAfterReading.checked = true;
-        document.body.appendChild(burnAfterReading);
-        PrivateBin.TopNav.init();
-        PrivateBin.TopNav.showEmailButton(0, false);
-
-        const { getUrl, restore } = makeWindowOpenMock();
-        try {
-            document.getElementById('emaillink').click();
-            document.getElementById('emailconfirm-timezone-current').click();
-
-            const body = extractMailtoBody(getUrl());
-            assert.doesNotMatch(
-                body,
-                /only be accessed once/,
-                'email body must use the viewed paste metadata'
-            );
-        } finally {
-            restore();
-        }
-    });
-});
-
-describe('Email - mail subject', function () {
-    beforeEach(function () {
-        cleanup(); // provided by common
-    });
-
-    it('Includes a non-empty subject naming the instance, with no expiration confirmation step', function () {
-        buildEmailDomNoShortUrl();
-        // buildEmailDomNoShortUrl() replaces documentElement.innerHTML (dropping <title>),
-        // so document.title must be (re-)set after calling it, not before.
-        document.title = 'My PrivateBin Instance';
-        PrivateBin.TopNav.init();
-        PrivateBin.TopNav.showEmailButton(0);
-
-        const { getUrl, restore } = makeWindowOpenMock();
-        try {
-            document.getElementById('emaillink').click();
-
-            const openedUrl = getUrl();
-            assert.ok(openedUrl, 'window.open should have been called');
-
-            const subject = extractMailtoSubject(openedUrl);
-            assert.match(subject, /My PrivateBin Instance/, 'subject should name the instance');
-        } finally {
-            restore();
-        }
-    });
-
-    it('Includes the same subject after the expiration confirmation step', function () {
-        buildEmailDomWithShortUrl();
-        document.title = 'My PrivateBin Instance';
-        PrivateBin.TopNav.init();
-        // a non-zero remaining time routes through the timezone confirmation modal
-        PrivateBin.TopNav.showEmailButton(3600);
-
-        const { getUrl, restore } = makeWindowOpenMock();
-        try {
-            document.getElementById('emaillink').click();
-            document.getElementById('emailconfirm-timezone-current').click();
-
-            const openedUrl = getUrl();
-            assert.ok(openedUrl, 'window.open should have been called');
-
-            const subject = extractMailtoSubject(openedUrl);
-            assert.match(subject, /My PrivateBin Instance/, 'subject should name the instance');
-        } finally {
-            restore();
-        }
-    });
-
-    it('Percent-encodes the subject so a space-containing instance name does not break the mailto URL', function () {
-        buildEmailDomNoShortUrl();
-        document.title = 'My Cool Instance';
-        PrivateBin.TopNav.init();
-        PrivateBin.TopNav.showEmailButton(0);
-
-        const { getUrl, restore } = makeWindowOpenMock();
-        try {
-            document.getElementById('emaillink').click();
-
-            const openedUrl = getUrl();
-            const rawSubjectParam = openedUrl.split('&body=')[0];
-            assert.strictEqual(
-                rawSubjectParam,
-                `mailto:?subject=${encodeURIComponent('Encrypted note on My Cool Instance')}`,
-                'raw subject parameter should be exactly the encoded translated string'
-            );
-            assert.doesNotMatch(rawSubjectParam, / /, 'raw mailto URL must not contain a literal space');
-
-            const subject = extractMailtoSubject(openedUrl);
-            assert.strictEqual(subject, 'Encrypted note on My Cool Instance', 'decoded subject should round-trip exactly');
-        } finally {
-            restore();
-        }
-    });
-});
-
-describe('Email - click handlers', function () {
-    beforeEach(function () {
-        cleanup();
-    });
-
-    it('Sends a single e-mail after the confirmation dialog was dismissed', function () {
-        buildEmailDomNoShortUrl();
-        PrivateBin.TopNav.init();
-        PrivateBin.TopNav.showEmailButton(3600);
-
-        const { getCount, restore } = makeWindowOpenMock();
-        try {
-            // open the dialog and dismiss it without choosing a timezone
-            document.getElementById('emaillink').click();
-            assert.strictEqual(getCount(), 0, 'no e-mail before choosing a timezone');
-
-            document.getElementById('emaillink').click();
-            document.getElementById('emailconfirm-timezone-current').click();
-            assert.strictEqual(getCount(), 1, 'exactly one e-mail with the current timezone');
-
-            document.getElementById('emaillink').click();
-            document.getElementById('emailconfirm-timezone-utc').click();
-            assert.strictEqual(getCount(), 2, 'exactly one more e-mail with UTC');
-        } finally {
-            restore();
-        }
-    });
-
-    it('Sends a single e-mail per click after the button was shown again', function () {
-        buildEmailDomNoShortUrl();
-        PrivateBin.TopNav.init();
-        // i.e. sending a second document without reloading the page
-        PrivateBin.TopNav.showEmailButton(0);
-        PrivateBin.TopNav.showEmailButton(0);
-
-        const { getCount, restore } = makeWindowOpenMock();
-        try {
-            document.getElementById('emaillink').click();
-            assert.strictEqual(getCount(), 1, 'exactly one e-mail per click');
-        } finally {
-            restore();
-        }
-    });
-
-    it('Uses the expiration of the document the button was last shown for', function () {
-        buildEmailDomNoShortUrl();
-        PrivateBin.TopNav.init();
-        // first document expires, the second never does
-        PrivateBin.TopNav.showEmailButton(3600);
-        PrivateBin.TopNav.showEmailButton(0);
-
-        const { getUrl, getCount, restore } = makeWindowOpenMock();
-        try {
-            document.getElementById('emaillink').click();
-            assert.strictEqual(getCount(), 1, 'sent directly, without confirmation dialog');
-            assert.doesNotMatch(extractMailtoBody(getUrl()), /expire/i, 'body has no expiration');
-
-            document.getElementById('emailconfirm-timezone-current').click();
-            assert.strictEqual(getCount(), 1, 'no e-mail from a stale confirmation dialog');
-        } finally {
-            restore();
-        }
-    });
-
-    it('Sends no e-mail from a confirmation dialog opened for an earlier document', function () {
-        buildEmailDomNoShortUrl();
-        PrivateBin.TopNav.init();
-        PrivateBin.TopNav.showEmailButton(3600);
-
-        const { getCount, restore } = makeWindowOpenMock();
-        try {
-            // open the dialog, but start a new document instead of choosing
-            document.getElementById('emaillink').click();
-            PrivateBin.TopNav.hideEmailButton();
-            PrivateBin.TopNav.showEmailButton(0);
-
-            document.getElementById('emailconfirm-timezone-current').click();
-            document.getElementById('emailconfirm-timezone-utc').click();
-            assert.strictEqual(getCount(), 0, 'no e-mail from a stale confirmation dialog');
-        } finally {
-            restore();
-        }
-    });
-
-    it('Sends no e-mail after the button was hidden', function () {
-        buildEmailDomNoShortUrl();
-        PrivateBin.TopNav.init();
-        PrivateBin.TopNav.showEmailButton(0);
-        PrivateBin.TopNav.hideEmailButton();
-
-        const { getCount, restore } = makeWindowOpenMock();
-        try {
-            // i.e. a keyboard shortcut or assistive technology activating it
-            document.getElementById('emaillink').click();
-            assert.strictEqual(getCount(), 0, 'no e-mail from a hidden button');
-        } finally {
-            restore();
-        }
-    });
-
-    it('Sends exactly one e-mail, regardless of how often the button was shown or the dialog dismissed', function () {
-        fc.assert(fc.property(
-            fc.integer({min: 1, max: 5}),
-            fc.integer({min: 0, max: 5}),
-            fc.constantFrom('current', 'utc'),
-            function (shown, dismissed, timezone) {
-                cleanup();
-                buildEmailDomNoShortUrl();
-                PrivateBin.TopNav.init();
-                for (let i = 0; i < shown; ++i) {
-                    PrivateBin.TopNav.showEmailButton(3600);
-                }
-
-                const { getCount, restore } = makeWindowOpenMock();
-                try {
-                    for (let i = 0; i < dismissed; ++i) {
-                        document.getElementById('emaillink').click();
-                    }
-                    document.getElementById('emaillink').click();
-                    document.getElementById('emailconfirm-timezone-' + timezone).click();
-                    assert.strictEqual(getCount(), 1);
-                } finally {
-                    restore();
-                }
-            }
-        ));
-    });
-});