'use strict'; require('../common'); const fc = require('fast-check'); function query(selector) { if (selector.startsWith('#')) { return document.getElementById(selector.slice(1)); } return document.querySelector(selector); } describe('TopNav', function () { describe('showEmailButton', function () { afterEach(function () { cleanup(); }); it('does nothing when the email button is omitted from the page', function () { document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); const originalShowError = PrivateBin.Alert.showError; let errorShown = false; PrivateBin.Alert.showError = function () { errorShown = true; }; try { assert.doesNotThrow(() => PrivateBin.TopNav.showEmailButton()); assert.strictEqual(errorShown, false); } finally { PrivateBin.Alert.showError = originalShowError; } }); }); describe('hideBurnAfterReadingButtons', function () { it( 'hides buttons when the QR code button is disabled', function () { document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); assert.doesNotThrow(function () { PrivateBin.TopNav.hideBurnAfterReadingButtons(); }); assert.ok(query('#clonebutton').classList.contains('hidden')); cleanup(); } ); }); describe('showViewButtons & hideViewButtons', function () { before(function () { cleanup(); }); it( 'shows and hides navigation elements when the QR code button is disabled', function () { document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); assert.doesNotThrow(function () { PrivateBin.TopNav.showViewButtons(); }); assert.ok(!query('#newbutton').classList.contains('hidden')); assert.ok(!query('#clonebutton').classList.contains('hidden')); assert.ok(!query('#rawtextbutton').classList.contains('hidden')); assert.ok(!query('#downloadtextbutton').classList.contains('hidden')); assert.doesNotThrow(function () { PrivateBin.TopNav.hideViewButtons(); }); assert.ok(query('#newbutton').classList.contains('hidden')); assert.ok(query('#clonebutton').classList.contains('hidden')); assert.ok(query('#rawtextbutton').classList.contains('hidden')); assert.ok(query('#downloadtextbutton').classList.contains('hidden')); cleanup(); } ); it( 'displays & hides navigation elements for viewing an existing document', function () { let results = []; document.documentElement.innerHTML = ``; PrivateBin.TopNav.init(); results.push( query('#newbutton').classList.contains('hidden') && query('#clonebutton').classList.contains('hidden') && query('#rawtextbutton').classList.contains('hidden') && query('#qrcodelink').classList.contains('hidden') ); PrivateBin.TopNav.showViewButtons(); results.push( !query('#newbutton').classList.contains('hidden') && !query('#clonebutton').classList.contains('hidden') && !query('#rawtextbutton').classList.contains('hidden') && !query('#qrcodelink').classList.contains('hidden') ); PrivateBin.TopNav.hideViewButtons(); results.push( query('#newbutton').classList.contains('hidden') && query('#clonebutton').classList.contains('hidden') && query('#rawtextbutton').classList.contains('hidden') && query('#qrcodelink').classList.contains('hidden') ); cleanup(); const result = results.every(element => element); if (!result) { console.log(results); } assert.ok(result); } ); }); describe('showCreateButtons & hideCreateButtons', function () { before(function () { cleanup(); }); it( 'displays & hides navigation elements for creating a document', function () { // eslint-disable-line complexity let results = []; document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); results.push( query('#sendbutton').classList.contains('hidden') && query('#expiration').classList.contains('hidden') && query('#formatter').classList.contains('hidden') && query('#burnafterreadingoption').classList.contains('hidden') && query('#opendiscussionoption').classList.contains('hidden') && query('#newbutton').classList.contains('hidden') && query('#password').classList.contains('hidden') && query('#attach').classList.contains('hidden') ); PrivateBin.TopNav.showCreateButtons(); results.push( !query('#sendbutton').classList.contains('hidden') && !query('#expiration').classList.contains('hidden') && !query('#formatter').classList.contains('hidden') && !query('#burnafterreadingoption').classList.contains('hidden') && !query('#opendiscussionoption').classList.contains('hidden') && !query('#newbutton').classList.contains('hidden') && !query('#password').classList.contains('hidden') && !query('#attach').classList.contains('hidden') ); PrivateBin.TopNav.hideCreateButtons(); results.push( query('#sendbutton').classList.contains('hidden') && query('#expiration').classList.contains('hidden') && query('#formatter').classList.contains('hidden') && query('#burnafterreadingoption').classList.contains('hidden') && query('#opendiscussionoption').classList.contains('hidden') && query('#newbutton').classList.contains('hidden') && query('#password').classList.contains('hidden') && query('#attach').classList.contains('hidden') ); cleanup(); const result = results.every(element => element); if (!result) { console.log(results); } assert.ok(result); } ); it( 'tolerates missing optional elements (discussion & password disabled)', function () { // with discussion = false and password = false in the // configuration, the template omits these elements entirely document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); assert.doesNotThrow(() => PrivateBin.TopNav.showCreateButtons()); assert.ok(!query('#sendbutton').classList.contains('hidden')); assert.ok(!query('#burnafterreadingoption').classList.contains('hidden')); assert.doesNotThrow(() => PrivateBin.TopNav.hideCreateButtons()); assert.ok(query('#sendbutton').classList.contains('hidden')); assert.ok(query('#burnafterreadingoption').classList.contains('hidden')); cleanup(); } ); }); describe('send shortcut', function () { let sendCount; // builds the create-document navbar and stubs the actual send, so no // server interaction happens function setUpCreateMode() { cleanup(); sendCount = 0; document.documentElement.innerHTML = ''; PrivateBin.PasteEncrypter.sendPaste = function () { sendCount++; }; PrivateBin.TopNav.init(); PrivateBin.TopNav.showCreateButtons(); } // dispatches a keydown on the document, returns whether the default was prevented function pressKey(options) { const event = new window.KeyboardEvent('keydown', Object.assign( {bubbles: true, cancelable: true}, options )); document.dispatchEvent(event); return event.defaultPrevented; } afterEach(function () { cleanup(); }); it('sends the document on Ctrl+S and suppresses the browser save dialog', function () { setUpCreateMode(); const prevented = pressKey({key: 's', code: 'KeyS', ctrlKey: true}); assert.strictEqual(sendCount, 1); assert.ok(prevented); }); it('sends the document on Cmd+S (macOS)', function () { setUpCreateMode(); const prevented = pressKey({key: 's', code: 'KeyS', metaKey: true}); assert.strictEqual(sendCount, 1); assert.ok(prevented); }); it('sends on the physical S key of non-Latin keyboard layouts', function () { setUpCreateMode(); // on a Russian layout the S key produces "ы" pressKey({key: 'ы', code: 'KeyS', ctrlKey: true}); assert.strictEqual(sendCount, 1); }); it('leaves Ctrl+S to the browser when viewing a document', function () { setUpCreateMode(); PrivateBin.TopNav.hideCreateButtons(); const prevented = pressKey({key: 's', code: 'KeyS', ctrlKey: true}); assert.strictEqual(sendCount, 0); assert.ok(!prevented); }); it('ignores S without Ctrl/Cmd, with AltGr (Ctrl+Alt), with Shift, or other keys', function () { setUpCreateMode(); const prevented = [ pressKey({key: 's', code: 'KeyS'}), pressKey({key: 's', code: 'KeyS', ctrlKey: true, altKey: true}), pressKey({key: 'S', code: 'KeyS', ctrlKey: true, shiftKey: true}), pressKey({key: 'd', code: 'KeyD', ctrlKey: true}) ]; assert.strictEqual(sendCount, 0); assert.deepStrictEqual(prevented, [false, false, false, false]); }); }); describe('showNewPasteButton', function () { before(function () { cleanup(); }); it( 'displays the button for creating a document', function () { let results = []; document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); results.push( query('#newbutton').classList.contains('hidden') ); PrivateBin.TopNav.showNewPasteButton(); results.push( !query('#newbutton').classList.contains('hidden') ); cleanup(); const result = results.every(element => element); if (!result) { console.log(results); } assert.ok(result); } ); }); describe('hideCloneButton', function () { before(function () { cleanup(); }); it( 'hides the button for cloning a document', function () { let results = []; document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); results.push( !query('#clonebutton').classList.contains('hidden') ); PrivateBin.TopNav.hideCloneButton(); results.push( query('#clonebutton').classList.contains('hidden') ); cleanup(); const result = results.every(element => element); if (!result) { console.log(results); } assert.ok(result); } ); }); describe('hideRawButton', function () { before(function () { cleanup(); }); it( 'hides the raw text button', function () { let results = []; document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); results.push( !query('#rawtextbutton').classList.contains('hidden') ); PrivateBin.TopNav.hideRawButton(); results.push( query('#rawtextbutton').classList.contains('hidden') ); cleanup(); const result = results.every(element => element); if (!result) { console.log(results); } assert.ok(result); } ); }); describe('hideFileSelector', function () { before(function () { cleanup(); }); it( 'hides the file attachment selection button', function () { let results = []; document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); results.push( !query('#filewrap').classList.contains('hidden') ); PrivateBin.TopNav.hideFileSelector(); results.push( query('#filewrap').classList.contains('hidden') ); cleanup(); const result = results.every(element => element); if (!result) { console.log(results); } assert.ok(result); } ); }); describe('highlightFileupload', function () { afterEach(function () { cleanup(); }); it( 'opens the attachment dropdown and highlights the file selection', function () { cleanup(); document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); const errors = []; const consoleError = console.error; console.error = function (...args) { errors.push(args.join(' ')); }; try { PrivateBin.TopNav.highlightFileupload(); assert.strictEqual(query('.dropdown-toggle').getAttribute('aria-expanded'), 'true'); assert.ok(query('.dropdown-menu').classList.contains('show'), 'dropdown is open'); assert.ok(query('#filewrap').classList.contains('highlight'), 'file selection is highlighted'); // a second upload, while the dropdown is still open PrivateBin.TopNav.highlightFileupload(); assert.ok(query('.dropdown-menu').classList.contains('show'), 'dropdown stays open'); // a third upload, after the user closed the dropdown query('.dropdown-toggle').click(); assert.strictEqual(query('.dropdown-toggle').getAttribute('aria-expanded'), 'false'); PrivateBin.TopNav.highlightFileupload(); assert.ok(query('.dropdown-menu').classList.contains('show'), 'dropdown is open again'); } finally { console.error = consoleError; } assert.deepStrictEqual(errors, []); } ); }); describe('showCustomAttachment', function () { before(function () { cleanup(); }); it( 'display the custom file attachment', function () { let results = []; document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); results.push( query('#customattachment').classList.contains('hidden') ); PrivateBin.TopNav.showCustomAttachment(); results.push( !query('#customattachment').classList.contains('hidden') ); cleanup(); const result = results.every(element => element); if (!result) { console.log(results); } assert.ok(result); } ); }); describe('collapseBar', function () { before(function () { cleanup(); }); /** * navigation bar markup of the bootstrap5 template, with the * collapsible navigation either expanded or collapsed * * @param {boolean} isExpanded * @returns {string} */ function navbar(isExpanded) { return ''; } it( 'collapses the expanded navigation when displayed on a small screen', function () { document.documentElement.innerHTML = navbar(true); PrivateBin.TopNav.init(); let clicks = 0; query('.navbar-toggler').addEventListener('click', function () { ++clicks; }); PrivateBin.TopNav.collapseBar(); assert.strictEqual(clicks, 1, 'toggler is clicked once'); assert.ok(!query('#navbar').classList.contains('show'), 'navigation is collapsing'); } ); it( 'leaves the collapsed navigation alone', function () { document.documentElement.innerHTML = navbar(false); PrivateBin.TopNav.init(); let clicks = 0; query('.navbar-toggler').addEventListener('click', function () { ++clicks; }); PrivateBin.TopNav.collapseBar(); assert.strictEqual(clicks, 0, 'toggler is not clicked'); assert.ok(!query('#navbar').classList.contains('collapsing'), 'navigation is not toggled'); } ); it( 'ignores pages without a navigation', function () { document.documentElement.innerHTML = '
'; PrivateBin.TopNav.collapseBar(); } ); }); describe('resetInput', function () { before(function () { cleanup(); }); it( 'reset inputs to defaults (options off)', function () { let results = []; document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); results.push( !PrivateBin.TopNav.getBurnAfterReading() ); results.push( !PrivateBin.TopNav.getOpenDiscussion() ); query('#burnafterreading').checked = true; query('#opendiscussion').checked = true; results.push( PrivateBin.TopNav.getBurnAfterReading() ); results.push( PrivateBin.TopNav.getOpenDiscussion() ); PrivateBin.TopNav.resetInput(); results.push( !PrivateBin.TopNav.getBurnAfterReading() ); results.push( !PrivateBin.TopNav.getOpenDiscussion() ); cleanup(); const result = results.every(element => element); if (!result) { console.log(results); } assert.ok(result); } ); it( 'reset inputs to defaults (burnafterreading on)', function () { let results = []; document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); results.push( PrivateBin.TopNav.getBurnAfterReading() ); results.push( !PrivateBin.TopNav.getOpenDiscussion() ); query('#burnafterreading').checked = false; query('#burnafterreading').removeAttribute('checked'); results.push( !PrivateBin.TopNav.getBurnAfterReading() ); results.push( !PrivateBin.TopNav.getOpenDiscussion() ); PrivateBin.TopNav.resetInput(); results.push( PrivateBin.TopNav.getBurnAfterReading() ); results.push( !PrivateBin.TopNav.getOpenDiscussion() ); cleanup(); const result = results.every(element => element); if (!result) { console.log(results); } assert.ok(result); } ); it( 'reset inputs to defaults (opendiscussion on)', function () { let results = []; document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); results.push( !PrivateBin.TopNav.getBurnAfterReading() ); results.push( PrivateBin.TopNav.getOpenDiscussion() ); query('#opendiscussion').checked = false; query('#opendiscussion').removeAttribute('checked'); query('#burnafterreading').checked = true; query('#burnafterreading').setAttribute('checked', 'checked'); results.push( PrivateBin.TopNav.getBurnAfterReading() ); results.push( !PrivateBin.TopNav.getOpenDiscussion() ); PrivateBin.TopNav.resetInput(); results.push( !PrivateBin.TopNav.getBurnAfterReading() ); results.push( PrivateBin.TopNav.getOpenDiscussion() ); cleanup(); const result = results.every(element => element); if (!result) { console.log(results); } assert.ok(result); } ); it( 'tolerates a missing discussion option (discussion disabled)', function () { document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); assert.doesNotThrow(() => PrivateBin.TopNav.resetInput()); assert.ok(!query('#burnafterreadingoption').classList.contains('buttondisabled')); cleanup(); } ); }); describe('getExpiration', function () { before(function () { cleanup(); }); it( 'returns the currently selected expiration date', function () { document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); assert.strictEqual(PrivateBin.TopNav.getExpiration(), '1day'); cleanup(); } ); it( 'returns the expiration date the user selected', function () { document.documentElement.innerHTML = ''; PrivateBin.TopNav.init(); const select = query('#pasteExpiration'); select.value = 'never'; select.dispatchEvent(new window.Event('change')); assert.strictEqual(PrivateBin.TopNav.getExpiration(), 'never'); cleanup(); } ); }); describe('setFormat', function () { before(function () { cleanup(); }); const formatter = '