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

refactor(js): convert tests to fast-check

Fixes https://github.com/PrivateBin/PrivateBin/issues/1812

Co-authored-by: Junie <junie@jetbrains.com>
rugk 2 месяцев назад
Родитель
Сommit
b590cddfdb
15 измененных файлов с 1794 добавлено и 1692 удалено
  1. 27 27
      js/common.js
  2. 309 294
      js/test/Alert.js
  3. 112 110
      js/test/AttachmentViewer.js
  4. 67 62
      js/test/Check.js
  5. 61 54
      js/test/CopyToClipboard.js
  6. 83 81
      js/test/DiscussionViewer.js
  7. 70 68
      js/test/Editor.js
  8. 292 257
      js/test/Helper.js
  9. 207 200
      js/test/I18n.js
  10. 206 195
      js/test/Model.js
  11. 155 148
      js/test/PasteStatus.js
  12. 114 112
      js/test/PasteViewer.js
  13. 24 22
      js/test/Prompt.js
  14. 33 31
      js/test/TopNav.js
  15. 34 31
      js/test/UiHelper.js

+ 27 - 27
js/common.js

@@ -2,7 +2,7 @@
 
 // testing prerequisites
 global.assert = require('assert');
-global.jsc = require('jsverify');
+const fc = require('fast-check');
 global.jsdom = require('jsdom-global');
 // initial DOM environment created by jsdom-global
 let currentCleanup = global.jsdom();
@@ -136,68 +136,68 @@ exports.btoa = function(text) {
 };
 
 // provides random lowercase characters from a to z
-exports.jscA2zString = function() {
-    return jsc.elements(a2zString);
+exports.fcA2zString = function() {
+    return fc.constantFrom(...a2zString);
 };
 
 // provides random lowercase alpha numeric characters (a to z and 0 to 9)
-exports.jscAlnumString = function() {
-    return jsc.elements(alnumString);
+exports.fcAlnumString = function() {
+    return fc.constantFrom(...alnumString);
 };
 
 //provides random characters allowed in hexadecimal notation
-exports.jscHexString = function() {
-    return jsc.elements(hexString);
+exports.fcHexString = function() {
+    return fc.constantFrom(...hexString);
 };
 
 // provides random characters allowed in GET queries
-exports.jscQueryString = function() {
-    return jsc.elements(queryString);
+exports.fcQueryString = function() {
+    return fc.constantFrom(...queryString);
 };
 
 // provides random characters allowed in hash queries
-exports.jscHashString = function() {
-    return jsc.elements(hashString);
+exports.fcHashString = function() {
+    return fc.constantFrom(...hashString);
 };
 
 // provides random characters allowed in base64 encoded strings
-exports.jscBase64String = function() {
-    return jsc.elements(base64String);
+exports.fcBase64String = function() {
+    return fc.constantFrom(...base64String);
 };
 
 // provides a random URL schema supported by the whatwg-url library
-exports.jscSchemas = function(withFtp = true) {
-    return jsc.elements(withFtp ? schemas : schemas.slice(1));
+exports.fcSchemas = function(withFtp = true) {
+    return fc.constantFrom(...(withFtp ? schemas : schemas.slice(1)));
 };
 
 // provides a random supported language string
-exports.jscSupportedLanguages = function() {
-    return jsc.elements(supportedLanguages);
+exports.fcSupportedLanguages = function() {
+    return fc.constantFrom(...supportedLanguages);
 };
 
 // provides a random mime type
-exports.jscMimeTypes = function() {
-    return jsc.elements(mimeTypes);
+exports.fcMimeTypes = function() {
+    return fc.constantFrom(...mimeTypes);
 };
 
 // provides a random PrivateBin document formatter
-exports.jscFormats = function() {
-    return jsc.elements(formats);
+exports.fcFormats = function() {
+    return fc.constantFrom(...formats);
 };
 
 // provides random URLs
-exports.jscUrl = function(withFragment = true, withQuery = true) {
+exports.fcUrl = function(withFragment = true, withQuery = true) {
     let url = {
-        schema: exports.jscSchemas(),
-        address: jsc.nearray(exports.jscA2zString()),
+        schema: exports.fcSchemas(),
+        address: fc.array(exports.fcA2zString(), {minLength: 1}),
     };
     if (withFragment) {
-        url.fragment = jsc.string;
+        url.fragment = fc.string();
     }
     if(withQuery) {
-        url.query = jsc.array(exports.jscQueryString());
+        url.query = fc.array(exports.fcQueryString());
     }
-    return jsc.record(url);
+    return fc.record(url);
 };
 
 exports.urlToString = function (url) {

+ 309 - 294
js/test/Alert.js

@@ -1,283 +1,297 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('Alert', function () {
     describe('showStatus', function () {
-        jsc.property(
-            'shows a status message (basic)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="status">' + message + '</div>';
-                document.body.innerHTML =
-                    '<div id="status"></div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showStatus(message, icon);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows a status message (basic)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="status">' + message + '</div>';
+                    document.body.innerHTML =
+                        '<div id="status"></div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showStatus(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows a status message (bootstrap)',
-            jsc.array(common.jscAlnumString()),
-            function (message) {
-                message = message.join('');
-                const expected = '<div id="status" role="alert" ' +
-                    'class="statusmessage alert alert-info"><span ' +
-                    'class="glyphicon glyphicon-info-sign" ' +
-                    'aria-hidden="true"></span> <span>' + message + '</span></div>';
-                document.body.innerHTML =
-                    '<div id="status" role="alert" class="statusmessage ' +
-                    'alert alert-info hidden"><span class="glyphicon ' +
-                    'glyphicon-info-sign" aria-hidden="true"></span> </div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showStatus(message);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows a status message (bootstrap)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                function (message) {
+                    message = message.join('');
+                    const expected = '<div id="status" role="alert" ' +
+                        'class="statusmessage alert alert-info"><span ' +
+                        'class="glyphicon glyphicon-info-sign" ' +
+                        'aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="status" role="alert" class="statusmessage ' +
+                        'alert alert-info hidden"><span class="glyphicon ' +
+                        'glyphicon-info-sign" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showStatus(message);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows a status message (bootstrap, custom icon)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="status" role="alert" ' +
-                    'class="statusmessage alert alert-info"><span ' +
-                    'class="glyphicon glyphicon-' + icon +
-                    '" aria-hidden="true"></span> <span>' + message + '</span></div>';
-                document.body.innerHTML =
-                    '<div id="status" role="alert" class="statusmessage ' +
-                    'alert alert-info hidden"><span class="glyphicon ' +
-                    'glyphicon-info-sign" aria-hidden="true"></span> </div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showStatus(message, icon);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows a status message (bootstrap, custom icon)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="status" role="alert" ' +
+                        'class="statusmessage alert alert-info"><span ' +
+                        'class="glyphicon glyphicon-' + icon +
+                        '" aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="status" role="alert" class="statusmessage ' +
+                        'alert alert-info hidden"><span class="glyphicon ' +
+                        'glyphicon-info-sign" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showStatus(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('showWarning', function () {
-        jsc.property(
-            'shows a warning message (basic)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="errormessage">' + message + '</div>';
-                document.body.innerHTML =
-                    '<div id="errormessage"></div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showWarning(message, icon);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows a warning message (basic)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="errormessage">' + message + '</div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage"></div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showWarning(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows a warning message (bootstrap)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (message) {
-                message = message.join('');
-                const expected = '<div id="errormessage" role="alert" ' +
-                    'class="statusmessage alert alert-danger"><span ' +
-                    'class="glyphicon glyphicon-warning-sign" ' +
-                    'aria-hidden="true"></span> <span>' + message + '</span></div>';
-                document.body.innerHTML =
-                    '<div id="errormessage" role="alert" class="statusmessage ' +
-                    'alert alert-danger hidden"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showWarning(message);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows a warning message (bootstrap)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (message) {
+                    message = message.join('');
+                    const expected = '<div id="errormessage" role="alert" ' +
+                        'class="statusmessage alert alert-danger"><span ' +
+                        'class="glyphicon glyphicon-warning-sign" ' +
+                        'aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage" role="alert" class="statusmessage ' +
+                        'alert alert-danger hidden"><span class="glyphicon ' +
+                        'glyphicon-alert" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showWarning(message);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows a warning message (bootstrap, custom icon)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="errormessage" role="alert" ' +
-                    'class="statusmessage alert alert-danger"><span ' +
-                    'class="glyphicon glyphicon-' + icon +
-                    '" aria-hidden="true"></span> <span>' + message + '</span></div>';
-                document.body.innerHTML =
-                    '<div id="errormessage" role="alert" class="statusmessage ' +
-                    'alert alert-danger hidden"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showWarning(message, icon);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows a warning message (bootstrap, custom icon)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="errormessage" role="alert" ' +
+                        'class="statusmessage alert alert-danger"><span ' +
+                        'class="glyphicon glyphicon-' + icon +
+                        '" aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage" role="alert" class="statusmessage ' +
+                        'alert alert-danger hidden"><span class="glyphicon ' +
+                        'glyphicon-alert" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showWarning(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('showError', function () {
-        jsc.property(
-            'shows an error message (basic)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="errormessage">' + message + '</div>';
-                document.body.innerHTML =
-                    '<div id="errormessage"></div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showError(message, icon);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows an error message (basic)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="errormessage">' + message + '</div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage"></div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showError(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows an error message (bootstrap)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                message = message.join('');
-                const expected = '<div id="errormessage" role="alert" ' +
-                    'class="statusmessage alert alert-danger"><span ' +
-                    'class="glyphicon glyphicon-alert" ' +
-                    'aria-hidden="true"></span> <span>' + message + '</span></div>';
-                document.body.innerHTML =
-                    '<div id="errormessage" role="alert" class="statusmessage ' +
-                    'alert alert-danger hidden"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showError(message);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows an error message (bootstrap)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    message = message.join('');
+                    const expected = '<div id="errormessage" role="alert" ' +
+                        'class="statusmessage alert alert-danger"><span ' +
+                        'class="glyphicon glyphicon-alert" ' +
+                        'aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage" role="alert" class="statusmessage ' +
+                        'alert alert-danger hidden"><span class="glyphicon ' +
+                        'glyphicon-alert" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showError(message);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows an error message (bootstrap, custom icon)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (icon, message) {
-                icon = icon.join('');
-                message = message.join('');
-                const expected = '<div id="errormessage" role="alert" ' +
-                    'class="statusmessage alert alert-danger"><span ' +
-                    'class="glyphicon glyphicon-' + icon +
-                    '" aria-hidden="true"></span> <span>' + message + '</span></div>';
-                document.body.innerHTML =
-                    '<div id="errormessage" role="alert" class="statusmessage ' +
-                    'alert alert-danger hidden"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showError(message, icon);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows an error message (bootstrap, custom icon)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (icon, message) {
+                    icon = icon.join('');
+                    message = message.join('');
+                    const expected = '<div id="errormessage" role="alert" ' +
+                        'class="statusmessage alert alert-danger"><span ' +
+                        'class="glyphicon glyphicon-' + icon +
+                        '" aria-hidden="true"></span> <span>' + message + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="errormessage" role="alert" class="statusmessage ' +
+                        'alert alert-danger hidden"><span class="glyphicon ' +
+                        'glyphicon-alert" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showError(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('showRemaining', function () {
-        jsc.property(
-            'shows remaining time (basic)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            'integer',
-            function (message, string, number) {
-                message = message.join('');
-                string = string.join('');
-                const expected = '<div id="remainingtime" class="">' + string + message + number + '</div>';
-                document.body.innerHTML =
-                    '<div id="remainingtime" class="hidden"></div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows remaining time (basic)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                fc.integer(),
+                function (message, string, number) {
+                    message = message.join('');
+                    string = string.join('');
+                    const expected = '<div id="remainingtime" class="">' + string + message + number + '</div>';
+                    document.body.innerHTML =
+                        '<div id="remainingtime" class="hidden"></div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'shows remaining time (bootstrap)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            'integer',
-            function (message, string, number) {
-                message = message.join('');
-                string = string.join('');
-                const expected = '<div id="remainingtime" role="alert" ' +
-                    'class="alert alert-info"><span ' +
-                    'class="glyphicon glyphicon-fire" aria-hidden="true">' +
-                    '</span> <span>' + string + message + number + '</span></div>';
-                document.body.innerHTML =
-                    '<div id="remainingtime" role="alert" class="hidden ' +
-                    'alert alert-info"><span class="glyphicon ' +
-                    'glyphicon-fire" aria-hidden="true"></span> </div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+        it('shows remaining time (bootstrap)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                fc.integer(),
+                function (message, string, number) {
+                    message = message.join('');
+                    string = string.join('');
+                    const expected = '<div id="remainingtime" role="alert" ' +
+                        'class="alert alert-info"><span ' +
+                        'class="glyphicon glyphicon-fire" aria-hidden="true">' +
+                        '</span> <span>' + string + message + number + '</span></div>';
+                    document.body.innerHTML =
+                        '<div id="remainingtime" role="alert" class="hidden ' +
+                        'alert alert-info"><span class="glyphicon ' +
+                        'glyphicon-fire" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]);
+                    const result = document.body.innerHTML;
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('showLoading', function () {
-        jsc.property(
-            'shows a loading message (basic)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (message, icon) {
-                message = message.join('');
-                icon = icon.join('');
-                const defaultMessage = 'Loading…';
-                if (message.length === 0) {
-                    message = defaultMessage;
+        it('shows a loading message (basic)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (message, icon) {
+                    message = message.join('');
+                    icon = icon.join('');
+                    const defaultMessage = 'Loading…';
+                    if (message.length === 0) {
+                        message = defaultMessage;
+                    }
+                    const expected = '<div id="loadingindicator" class="">' + message + '</div>';
+                    document.body.innerHTML =
+                        '<div id="loadingindicator" class="hidden">' + defaultMessage + '</div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showLoading(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
                 }
-                const expected = '<div id="loadingindicator" class="">' + message + '</div>';
-                document.body.innerHTML =
-                    '<div id="loadingindicator" class="hidden">' + defaultMessage + '</div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showLoading(message, icon);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+            ));
+        });
 
-        jsc.property(
-            'shows a loading message (bootstrap)',
-            jsc.array(common.jscAlnumString()),
-            jsc.array(common.jscAlnumString()),
-            function (message, icon) {
-                message = message.join('');
-                icon = icon.join('');
-                const defaultMessage = 'Loading…';
-                if (message.length === 0) {
-                    message = defaultMessage;
+        it('shows a loading message (bootstrap)', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString()),
+                fc.array(common.fcAlnumString()),
+                function (message, icon) {
+                    message = message.join('');
+                    icon = icon.join('');
+                    const defaultMessage = 'Loading…';
+                    if (message.length === 0) {
+                        message = defaultMessage;
+                    }
+                    const expected = '<ul class="nav navbar-nav"><li ' +
+                        'id="loadingindicator" class="navbar-text"><span ' +
+                        'class="glyphicon glyphicon-' + icon +
+                        '" aria-hidden="true"></span> <span>' + message + '</span></li></ul>';
+                    document.body.innerHTML =
+                        '<ul class="nav navbar-nav"><li id="loadingindicator" ' +
+                        'class="navbar-text hidden"><span class="glyphicon ' +
+                        'glyphicon-time" aria-hidden="true"></span> ' +
+                        defaultMessage + '</li></ul>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.showLoading(message, icon);
+                    const result = document.body.innerHTML;
+                    return expected === result;
                 }
-                const expected = '<ul class="nav navbar-nav"><li ' +
-                    'id="loadingindicator" class="navbar-text"><span ' +
-                    'class="glyphicon glyphicon-' + icon +
-                    '" aria-hidden="true"></span> <span>' + message + '</span></li></ul>';
-                document.body.innerHTML =
-                    '<ul class="nav navbar-nav"><li id="loadingindicator" ' +
-                    'class="navbar-text hidden"><span class="glyphicon ' +
-                    'glyphicon-time" aria-hidden="true"></span> ' +
-                    defaultMessage + '</li></ul>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.showLoading(message, icon);
-                const result = document.body.innerHTML;
-                return expected === result;
-            }
-        );
+            ));
+        });
     });
 
     describe('hideLoading', function () {
@@ -318,46 +332,47 @@ describe('Alert', function () {
     });
 
     describe('setCustomHandler', function () {
-        jsc.property(
-            'calls a given handler function',
-            'nat 3',
-            jsc.array(common.jscAlnumString()),
-            function (trigger, message) {
-                message = message.join('');
-                let handlerCalled = false,
-                    defaultMessage = 'Loading…',
-                    functions = [
-                        PrivateBin.Alert.showStatus,
-                        PrivateBin.Alert.showError,
-                        PrivateBin.Alert.showRemaining,
-                        PrivateBin.Alert.showLoading
-                    ];
-                if (message.length === 0) {
-                    message = defaultMessage;
+        it('calls a given handler function', () => {
+            fc.assert(fc.property(
+                fc.integer({min: 0, max: 3}),
+                fc.array(common.fcAlnumString()),
+                function (trigger, message) {
+                    message = message.join('');
+                    let handlerCalled = false,
+                        defaultMessage = 'Loading…',
+                        functions = [
+                            PrivateBin.Alert.showStatus,
+                            PrivateBin.Alert.showError,
+                            PrivateBin.Alert.showRemaining,
+                            PrivateBin.Alert.showLoading
+                        ];
+                    if (message.length === 0) {
+                        message = defaultMessage;
+                    }
+                    document.body.innerHTML =
+                        '<ul class="nav navbar-nav"><li id="loadingindicator" ' +
+                        'class="navbar-text hidden"><span class="glyphicon ' +
+                        'glyphicon-time" aria-hidden="true"></span> ' +
+                        defaultMessage + '</li></ul>' +
+                        '<div id="remainingtime" role="alert" class="hidden ' +
+                        'alert alert-info"><span class="glyphicon ' +
+                        'glyphicon-fire" aria-hidden="true"></span> </div>' +
+                        '<div id="status" role="alert" class="statusmessage ' +
+                        'alert alert-info"><span class="glyphicon ' +
+                        'glyphicon-info-sign" aria-hidden="true"></span> </div>' +
+                        '<div id="errormessage" role="alert" class="statusmessage ' +
+                        'alert alert-danger"><span class="glyphicon ' +
+                        'glyphicon-alert" aria-hidden="true"></span> </div>';
+                    PrivateBin.Alert.init();
+                    PrivateBin.Alert.setCustomHandler(function(id, element) {
+                        handlerCalled = true;
+                        return Math.random() > 0.5 ? true : element;
+                    });
+                    functions[trigger](message);
+                    PrivateBin.Alert.setCustomHandler(null);
+                    return handlerCalled;
                 }
-                document.body.innerHTML =
-                    '<ul class="nav navbar-nav"><li id="loadingindicator" ' +
-                    'class="navbar-text hidden"><span class="glyphicon ' +
-                    'glyphicon-time" aria-hidden="true"></span> ' +
-                    defaultMessage + '</li></ul>' +
-                    '<div id="remainingtime" role="alert" class="hidden ' +
-                    'alert alert-info"><span class="glyphicon ' +
-                    'glyphicon-fire" aria-hidden="true"></span> </div>' +
-                    '<div id="status" role="alert" class="statusmessage ' +
-                    'alert alert-info"><span class="glyphicon ' +
-                    'glyphicon-info-sign" aria-hidden="true"></span> </div>' +
-                    '<div id="errormessage" role="alert" class="statusmessage ' +
-                    'alert alert-danger"><span class="glyphicon ' +
-                    'glyphicon-alert" aria-hidden="true"></span> </div>';
-                PrivateBin.Alert.init();
-                PrivateBin.Alert.setCustomHandler(function(id, element) {
-                    handlerCalled = true;
-                    return jsc.random(0, 1) ? true : element;
-                });
-                functions[trigger](message);
-                PrivateBin.Alert.setCustomHandler(null);
-                return handlerCalled;
-            }
-        );
+            ));
+        });
     });
 });

+ 112 - 110
js/test/AttachmentViewer.js

@@ -1,5 +1,6 @@
 'use strict';
 const common = require('../common');
+const fc = require('fast-check');
 
 describe('AttachmentViewer', function () {
     beforeEach(() => {
@@ -13,120 +14,121 @@ describe('AttachmentViewer', function () {
     describe('whole run (setAttachment, showAttachment, removeAttachment, hideAttachment, hideAttachmentPreview, hasAttachment, getAttachment & moveAttachmentTo)', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'displays & hides data as requested',
-            common.jscMimeTypes(),
-            'string',
-            'string',
-            'string',
-            'string',
-             // eslint-disable-next-line complexity
-            function (mimeType, rawdata, filename, prefix, postfix) {
-                let data = 'data:' + mimeType + ';base64,' + common.btoa(rawdata),
-                    mimePrefix = mimeType.substring(0, 6),
-                    previewSupported = (
-                        mimePrefix === 'image/' ||
-                        mimePrefix === 'audio/' ||
-                        mimePrefix === 'video/' ||
-                        mimeType.match(/\/pdf/i)
-                    ),
-                    results = [],
-                    result = '';
-                // text node of attachment will truncate at null byte
-                if (filename === '\u0000') {
-                    filename = '';
-                }
-                prefix  = prefix.replace(/%(s|d)/g, '%%');
-                postfix = postfix.replace(/%(s|d)/g, '%%').replace(/<|>/g, '');
-                document.body.innerHTML = (
-                    '<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>' +
-                    '<div id="attachment" class="hidden"></div>' +
-                    '<div id="templates">' +
-                        '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
-                            '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
-                            '<a class="alert-link">Download attachment</a>' +
-                        '</div>' +
-                    '</div>'
-                );
-                PrivateBin.AttachmentViewer.init();
-                PrivateBin.Model.init();
-                results.push(
-                    !PrivateBin.AttachmentViewer.hasAttachment() &&
-                    document.getElementById('attachment').classList.contains('hidden') &&
-                    document.getElementById('attachment').children.length === 0 &&
-                    document.getElementById('attachmenttemplate').classList.contains('hidden') &&
-                    document.getElementById('attachmentPreview').classList.contains('hidden')
-                );
-                global.atob = common.atob;
-                if (filename.length) {
-                    PrivateBin.AttachmentViewer.setAttachment(data, filename);
-                } else {
-                    PrivateBin.AttachmentViewer.setAttachment(data);
-                }
-                // // beyond this point we will get the blob URL instead of the data
-                data = window.URL.createObjectURL(data);
-                const attachment = PrivateBin.AttachmentViewer.getAttachments();
-                results.push(
-                    PrivateBin.AttachmentViewer.hasAttachment() &&
-                    document.getElementById('attachment').classList.contains('hidden') &&
-                    document.getElementById('attachment').children.length > 0 &&
-                    document.getElementById('attachmentPreview').classList.contains('hidden') &&
-                    attachment[0][0] === data &&
-                    attachment[0][1] === filename
-                );
-                PrivateBin.AttachmentViewer.showAttachment();
-                results.push(
-                    !document.getElementById('attachment').classList.contains('hidden') &&
-                    document.getElementById('attachment').children.length > 0 &&
-                    (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
-                );
-                PrivateBin.AttachmentViewer.hideAttachment();
-                results.push(
-                    document.getElementById('attachment').classList.contains('hidden') &&
-                    (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
-                );
-                if (previewSupported) {
-                    PrivateBin.AttachmentViewer.hideAttachmentPreview();
-                    results.push(document.getElementById('attachmentPreview').classList.contains('hidden'));
-                }
-                PrivateBin.AttachmentViewer.showAttachment();
-                results.push(
-                    !document.getElementById('attachment').classList.contains('hidden') &&
-                    (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
-                );
-                let element = document.createElement('div');
-                PrivateBin.AttachmentViewer.moveAttachmentTo(element, attachment[0], prefix + '%s' + postfix);
-                // messageIDs with links get a relaxed treatment
-                if (prefix.indexOf('<a') === -1 && postfix.indexOf('<a') === -1) {
-                    const tempTA = document.createElement('textarea');
-                    tempTA.textContent = (prefix + filename + postfix);
-                    result = tempTA.textContent;
-                } else {
-                    result = DOMPurify.sanitize(
-                        prefix + PrivateBin.Helper.htmlEntities(filename) + postfix, {
-                            ALLOWED_TAGS: ['a', 'i', 'span'],
-                            ALLOWED_ATTR: ['href', 'id']
-                        }
+        it('displays & hides data as requested', () => {
+            fc.assert(fc.property(
+                common.fcMimeTypes(),
+                fc.string(),
+                fc.string(),
+                fc.string(),
+                fc.string(),
+                // eslint-disable-next-line complexity
+                function (mimeType, rawdata, filename, prefix, postfix) {
+                    let data = 'data:' + mimeType + ';base64,' + common.btoa(rawdata),
+                        mimePrefix = mimeType.substring(0, 6),
+                        previewSupported = (
+                            mimePrefix === 'image/' ||
+                            mimePrefix === 'audio/' ||
+                            mimePrefix === 'video/' ||
+                            mimeType.match(/\/pdf/i)
+                        ),
+                        results = [],
+                        result = '';
+                    // text node of attachment will truncate at null byte
+                    if (filename === '\u0000') {
+                        filename = '';
+                    }
+                    prefix  = prefix.replace(/%(s|d)/g, '%%');
+                    postfix = postfix.replace(/%(s|d)/g, '%%').replace(/<|>/g, '');
+                    document.body.innerHTML = (
+                        '<div id="attachmentPreview" class="col-md-12 text-center hidden"></div>' +
+                        '<div id="attachment" class="hidden"></div>' +
+                        '<div id="templates">' +
+                            '<div id="attachmenttemplate" role="alert" class="attachment hidden alert alert-info">' +
+                                '<span class="glyphicon glyphicon-download-alt" aria-hidden="true"></span>' +
+                                '<a class="alert-link">Download attachment</a>' +
+                            '</div>' +
+                        '</div>'
                     );
-                }
-                if (filename.length) {
+                    PrivateBin.AttachmentViewer.init();
+                    PrivateBin.Model.init();
+                    results.push(
+                        !PrivateBin.AttachmentViewer.hasAttachment() &&
+                        document.getElementById('attachment').classList.contains('hidden') &&
+                        document.getElementById('attachment').children.length === 0 &&
+                        document.getElementById('attachmenttemplate').classList.contains('hidden') &&
+                        document.getElementById('attachmentPreview').classList.contains('hidden')
+                    );
+                    global.atob = common.atob;
+                    if (filename.length) {
+                        PrivateBin.AttachmentViewer.setAttachment(data, filename);
+                    } else {
+                        PrivateBin.AttachmentViewer.setAttachment(data);
+                    }
+                    // // beyond this point we will get the blob URL instead of the data
+                    data = window.URL.createObjectURL(data);
+                    const attachment = PrivateBin.AttachmentViewer.getAttachments();
+                    results.push(
+                        PrivateBin.AttachmentViewer.hasAttachment() &&
+                        document.getElementById('attachment').classList.contains('hidden') &&
+                        document.getElementById('attachment').children.length > 0 &&
+                        document.getElementById('attachmentPreview').classList.contains('hidden') &&
+                        attachment[0][0] === data &&
+                        attachment[0][1] === filename
+                    );
+                    PrivateBin.AttachmentViewer.showAttachment();
+                    results.push(
+                        !document.getElementById('attachment').classList.contains('hidden') &&
+                        document.getElementById('attachment').children.length > 0 &&
+                        (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
+                    );
+                    PrivateBin.AttachmentViewer.hideAttachment();
                     results.push(
-                        element.querySelector('a').href === data &&
-                        element.querySelector('a').getAttribute('download') === filename &&
-                        element.querySelector('a').textContent === result
+                        document.getElementById('attachment').classList.contains('hidden') &&
+                        (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
                     );
-                } else {
-                    results.push(element.querySelector('a').href === data);
+                    if (previewSupported) {
+                        PrivateBin.AttachmentViewer.hideAttachmentPreview();
+                        results.push(document.getElementById('attachmentPreview').classList.contains('hidden'));
+                    }
+                    PrivateBin.AttachmentViewer.showAttachment();
+                    results.push(
+                        !document.getElementById('attachment').classList.contains('hidden') &&
+                        (previewSupported ? !document.getElementById('attachmentPreview').classList.contains('hidden') : document.getElementById('attachmentPreview').classList.contains('hidden'))
+                    );
+                    let element = document.createElement('div');
+                    PrivateBin.AttachmentViewer.moveAttachmentTo(element, attachment[0], prefix + '%s' + postfix);
+                    // messageIDs with links get a relaxed treatment
+                    if (prefix.indexOf('<a') === -1 && postfix.indexOf('<a') === -1) {
+                        const tempTA = document.createElement('textarea');
+                        tempTA.textContent = (prefix + filename + postfix);
+                        result = tempTA.textContent;
+                    } else {
+                        result = DOMPurify.sanitize(
+                            prefix + PrivateBin.Helper.htmlEntities(filename) + postfix, {
+                                ALLOWED_TAGS: ['a', 'i', 'span'],
+                                ALLOWED_ATTR: ['href', 'id']
+                            }
+                        );
+                    }
+                    if (filename.length) {
+                        results.push(
+                            element.querySelector('a').href === data &&
+                            element.querySelector('a').getAttribute('download') === filename &&
+                            element.querySelector('a').textContent === result
+                        );
+                    } else {
+                        results.push(element.querySelector('a').href === data);
+                    }
+                    PrivateBin.AttachmentViewer.removeAttachment();
+                    results.push(
+                        document.getElementById('attachment').classList.contains('hidden') &&
+                        document.getElementById('attachment').children.length === 0 &&
+                        document.getElementById('attachmentPreview').classList.contains('hidden')
+                    );
+                    return results.every(element => element);
                 }
-                PrivateBin.AttachmentViewer.removeAttachment();
-                results.push(
-                    document.getElementById('attachment').classList.contains('hidden') &&
-                    document.getElementById('attachment').children.length === 0 &&
-                    document.getElementById('attachmentPreview').classList.contains('hidden')
-                );
-                return results.every(element => element);
-            }
-        );
+            ));
+        });
 
         it(
             'sanitizes file names in attachments',

+ 67 - 62
js/test/Check.js

@@ -1,16 +1,17 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 /* global Legacy, WebCrypto */
 
 describe('Check', function () {
     describe('init', function () {
         this.timeout(30000);
 
-        it('returns false and shows error, if a bot UA is detected', function () {
-            jsc.assert(jsc.forall(
-                'string',
-                jsc.elements(['Bot', 'bot']),
-                'string',
+        it('returns false and shows error, if a bot UA is detected', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.constantFrom('Bot', 'bot'),
+                fc.string(),
                 function (prefix, botBit, suffix) {
                     const clean = globalThis.cleanup(
                         '<html><body><div id="errormessage" class="hidden"></div>' +
@@ -24,66 +25,70 @@ describe('Check', function () {
                     return result;
                 }
             ),
-            {tests: 10});
+            {numRuns: 10});
         });
 
-        jsc.property(
-            'shows error, if no webcrypto is detected',
-            'bool',
-            jsc.elements(['localhost', '127.0.0.1', '[::1]', '']),
-            jsc.nearray(common.jscA2zString()),
-            jsc.elements(['.onion', '.i2p', '']),
-            function (secureProtocol, localhost, domain, tld) {
-                const isDomain = localhost === '',
-                      isSecureContext = secureProtocol || !isDomain || tld.length > 0,
-                      clean = globalThis.cleanup(
-                          '<html><body><div id="errormessage" class="hidden"></div>' +
-                          '<div id="oldnotice" class="hidden"></div>' +
-                          '<div id="insecurecontextnotice" class="hidden"></div></body></html>',
-                          {
-                              'url': (secureProtocol ? 'https' : 'http' ) + '://' +
-                                     (isDomain ? domain.join('') + tld : localhost) + '/'
-                          }
-                      );
-                Legacy.Check.init();
-                const result1 = Legacy.Check.getInit() && !Legacy.Check.getStatus(),
-                      result2 = isSecureContext === (document.getElementById('insecurecontextnotice').className === 'hidden'),
-                      result3 = (document.getElementById('oldnotice').className !== 'hidden');
-                clean();
-                if (result1 && result2 && result3) {
-                    return true;
+        it('shows error, if no webcrypto is detected', () => {
+            fc.assert(fc.property(
+                fc.boolean(),
+                fc.constantFrom('localhost', '127.0.0.1', '[::1]', ''),
+                fc.array(common.fcA2zString(), {minLength: 1}),
+                fc.constantFrom('.onion', '.i2p', ''),
+                function (secureProtocol, localhost, domain, tld) {
+                    const isDomain = localhost === '',
+                          isSecureContext = secureProtocol || !isDomain || tld.length > 0,
+                          clean = globalThis.cleanup(
+                              '<html><body><div id="errormessage" class="hidden"></div>' +
+                              '<div id="oldnotice" class="hidden"></div>' +
+                              '<div id="insecurecontextnotice" class="hidden"></div></body></html>',
+                              {
+                                  'url': (secureProtocol ? 'https' : 'http' ) + '://' +
+                                         (isDomain ? domain.join('') + tld : localhost) + '/'
+                              }
+                          );
+                    Legacy.Check.init();
+                    const result1 = Legacy.Check.getInit() && !Legacy.Check.getStatus(),
+                          result2 = isSecureContext === (document.getElementById('insecurecontextnotice').className === 'hidden'),
+                          result3 = (document.getElementById('oldnotice').className !== 'hidden');
+                    clean();
+                    if (result1 && result2 && result3) {
+                        return true;
+                    }
+                    console.log(result1, result2, result3);
+                    return false;
                 }
-                console.log(result1, result2, result3);
-                return false;
-            }
-        );
+            ));
+        });
 
-        jsc.property(
-            'shows error, if HTTP only site is detected',
-            'bool',
-            jsc.nearray(common.jscA2zString()),
-            function (secureProtocol, domain) {
-                const clean = globalThis.cleanup(
-                          '<html><body><div id="httpnotice" class="hidden"></div>' +
-                          '</body></html>',
-                          {
-                              'url': (secureProtocol ? 'https' : 'http' ) + '://' + domain.join('') + '/'
-                          }
-                      );
-                Object.defineProperty(window, 'crypto', {
-                    value: new WebCrypto(),
-                    writeable: false
-                });
-                Legacy.Check.init();
-                const result1 = Legacy.Check.getInit() && Legacy.Check.getStatus(),
-                      result2 = secureProtocol === (document.getElementById('httpnotice').className === 'hidden');
-                clean();
-                if (result1 && result2) {
-                    return true;
+        it('shows error, if HTTP only site is detected', () => {
+            fc.assert(fc.property(
+                fc.boolean(),
+                fc.array(common.fcA2zString(), {minLength: 1}),
+                function (secureProtocol, domain) {
+                    const clean = globalThis.cleanup(
+                              '<html><body><div id="httpnotice" class="hidden"></div>' +
+                              '</body></html>',
+                              {
+                                  'url': (secureProtocol ? 'https' : 'http' ) + '://' + domain.join('') + '/'
+                              }
+                          );
+                    Object.defineProperty(window, 'crypto', {
+                        value: new WebCrypto(),
+                        configurable: true,
+                        enumerable: true,
+                        writable: false
+                    });
+                    Legacy.Check.init();
+                    const result1 = Legacy.Check.getInit() && Legacy.Check.getStatus(),
+                          result2 = secureProtocol === (document.getElementById('httpnotice').className === 'hidden');
+                    clean();
+                    if (result1 && result2) {
+                        return true;
+                    }
+                    console.log(result1, result2);
+                    return false;
                 }
-                console.log(result1, result2);
-                return false;
-            }
-        );
+            ));
+        });
     });
 });

+ 61 - 54
js/test/CopyToClipboard.js

@@ -8,69 +8,74 @@ describe('CopyToClipboard', function () {
     });
 
     describe('Copy document to clipboard', function () {
-        jsc.property('Copy with button click',
-            common.jscFormats(),
-            'nestring',
-            async function (format, text) {
-                common.enableClipboard();
-
-                document.body.innerHTML = (
-                    '<div id="placeholder" class="hidden">+++ no document text ' +
-                    '+++</div><div id="prettymessage" class="hidden">' +
-                    '<button type="button" id="prettyMessageCopyBtn"><svg id="copyIcon"></svg>' +
-                    '<svg id="copySuccessIcon"></svg></button><pre ' +
-                    'id="prettyprint" class="prettyprint linenums:1"></pre>' +
-                    '</div><div id="plaintext" class="hidden"></div>'
-                );
-
-                PrivateBin.PasteViewer.init();
-                PrivateBin.PasteViewer.setFormat(format);
-                PrivateBin.PasteViewer.setText(text);
-                PrivateBin.PasteViewer.run();
+        it('Copy with button click', async function () {
+            await fc.assert(fc.asyncProperty(
+                common.fcFormats(),
+                fc.string({minLength: 1}),
+                async function (format, text) {
+                    common.enableClipboard();
+
+                    document.body.innerHTML = (
+                        '<div id="placeholder" class="hidden">+++ no document text ' +
+                        '+++</div><div id="prettymessage" class="hidden">' +
+                        '<button type="button" id="prettyMessageCopyBtn"><svg id="copyIcon"></svg>' +
+                        '<svg id="copySuccessIcon"></svg></button><pre ' +
+                        'id="prettyprint" class="prettyprint linenums:1"></pre>' +
+                        '</div><div id="plaintext" class="hidden"></div>'
+                    );
+
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText(text);
+                    PrivateBin.PasteViewer.run();
 
-                PrivateBin.CopyToClipboard.init();
+                    PrivateBin.CopyToClipboard.init();
 
-                document.getElementById('prettyMessageCopyBtn').click();
+                    document.getElementById('prettyMessageCopyBtn').click();
 
-                const savedToClipboardText = await navigator.clipboard.readText();
+                    const savedToClipboardText = await navigator.clipboard.readText();
 
-                return text === savedToClipboardText;
-            }
-        );
+                    return text === savedToClipboardText;
+                }
+            ));
+        });
 
         /**
-         * Unfortunately in JSVerify impossible to check if copy with shortcut when user selected some text on the page
+         * Unfortunately, in JSVerify impossible to check if copy with shortcut when user selected some text on the page
          * (the copy document to clipboard should not work in this case) due to lacking window.getSelection() in jsdom.
          */
-        jsc.property('Copy with keyboard shortcut',
-            common.jscFormats(),
-            'nestring',
-            async function (format, text) {
-                common.enableClipboard();
+        it('Copy with keyboard shortcut', async function () {
+            await fc.assert(fc.asyncProperty(
+                common.fcFormats(),
+                fc.string({minLength: 1}),
+                async function (format, text) {
+                    common.enableClipboard();
+
+                    document.body.innerHTML = (
+                        '<div id="placeholder">+++ no document text ' +
+                        '+++</div><div id="prettymessage" class="hidden">' +
+                        '<button type="button" id="prettyMessageCopyBtn"><svg id="copyIcon"></svg>' +
+                        '<svg id="copySuccessIcon"></svg></button><pre ' +
+                        'id="prettyprint" class="prettyprint linenums:1"></pre>' +
+                        '</div><div id="plaintext" class="hidden"></div>'
+                    );
+
+                    PrivateBin.Alert.init();
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText(text);
+                    PrivateBin.PasteViewer.run();
 
-                document.body.innerHTML = (
-                    '<div id="placeholder">+++ no document text ' +
-                    '+++</div><div id="prettymessage" class="hidden">' +
-                    '<button type="button" id="prettyMessageCopyBtn"><svg id="copyIcon"></svg>' +
-                    '<svg id="copySuccessIcon"></svg></button><pre ' +
-                    'id="prettyprint" class="prettyprint linenums:1"></pre>' +
-                    '</div><div id="plaintext" class="hidden"></div>'
-                );
-
-                PrivateBin.PasteViewer.init();
-                PrivateBin.PasteViewer.setFormat(format);
-                PrivateBin.PasteViewer.setText(text);
-                PrivateBin.PasteViewer.run();
-
-                PrivateBin.CopyToClipboard.init();
+                    PrivateBin.CopyToClipboard.init();
 
-                document.body.dispatchEvent(getClipboardEvent());
+                    document.body.dispatchEvent(getClipboardEvent());
 
-                const copiedTextWithoutSelectedText = await navigator.clipboard.readText();
+                    const copiedTextWithoutSelectedText = await navigator.clipboard.readText();
 
-                return copiedTextWithoutSelectedText === text;
-            }
-        );
+                    return copiedTextWithoutSelectedText === text;
+                }
+            ));
+        });
 
         /**
          * ClipboardEvent is not supported in jsdom yet, so this creates a mock event to trigger the copy event listener.
@@ -133,8 +138,9 @@ describe('CopyToClipboard', function () {
         });
     });
 
-        jsc.property('Hide hint',
-            'nestring',
+    it('Hide hint', () => {
+        fc.assert(fc.property(
+            fc.string({minLength: 1}),
             function (text) {
                 document.body.innerHTML = '<small id="copyShortcutHintText">' + text + '</small>';
 
@@ -145,5 +151,6 @@ describe('CopyToClipboard', function () {
 
                 return keyboardShortcutHint.length === 0;
             }
-        );
+        ));
+    });
 });

+ 83 - 81
js/test/DiscussionViewer.js

@@ -1,35 +1,36 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('DiscussionViewer', function () {
     describe('handleNotification, prepareNewDiscussion, addComment, finishDiscussion, getReplyMessage, getReplyNickname, getReplyCommentId & highlightComment', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'displays & hides comments as requested',
-            jsc.array(
-                jsc.record({
-                    idArray: jsc.nearray(common.jscAlnumString()),
-                    parentidArray: jsc.nearray(common.jscAlnumString()),
-                    data: jsc.string,
-                    meta: jsc.record({
-                        nickname: jsc.string,
-                        postdate: jsc.nat,
-                        vizhash: jsc.string
+        it('displays & hides comments as requested', () => {
+            fc.assert(fc.property(
+                fc.array(
+                    fc.record({
+                        idArray: fc.array(common.fcAlnumString(), {minLength: 1}),
+                        parentidArray: fc.array(common.fcAlnumString(), {minLength: 1}),
+                        data: fc.string(),
+                        meta: fc.record({
+                            nickname: fc.string(),
+                            postdate: fc.nat(),
+                            vizhash: fc.string()
+                        })
                     })
-                })
-            ),
-            'nat',
-            'bool',
-            'string',
-            'string',
-            jsc.elements(['loading', 'danger', 'other']),
-            'nestring',
-            function (comments, commentKey, fadeOut, nickname, message, alertType, alert) {
-                var clean = globalThis.cleanup(),
-                    results = [];
-                document.body.innerHTML = (
-                    `<div id="discussion">
+                ),
+                fc.nat(),
+                fc.boolean(),
+                fc.string(),
+                fc.string(),
+                fc.constantFrom('loading', 'danger', 'other'),
+                fc.string({minLength: 1}),
+                function (comments, commentKey, fadeOut, nickname, message, alertType, alert) {
+                    var clean = globalThis.cleanup(),
+                        results = [];
+                    document.body.innerHTML = (
+                        `<div id="discussion">
 	<h4>Discussion</h4>
 	<div id="commentcontainer"></div>
 </div>
@@ -57,67 +58,68 @@ describe('DiscussionViewer', function () {
 	</div>
 </div>
 `
-                );
-                PrivateBin.Model.init();
-                PrivateBin.DiscussionViewer.init();
-                results.push(
-                    !document.getElementById('discussion').classList.contains('hidden')
-                );
-                PrivateBin.DiscussionViewer.prepareNewDiscussion();
-                results.push(
-                    document.getElementById('discussion').classList.contains('hidden')
-                );
-                comments.forEach(function (comment) {
-                    comment.id = comment.idArray.join('');
-                    comment.parentid = comment.parentidArray.join('');
-                    PrivateBin.DiscussionViewer.addComment(PrivateBin.Helper.CommentFactory(comment), comment.data, comment.meta.nickname);
-                });
-                results.push(
-                    document.getElementById('discussion').classList.contains('hidden')
-                );
-                PrivateBin.DiscussionViewer.finishDiscussion();
-                results.push(
-                    !document.getElementById('discussion').classList.contains('hidden') &&
-                    comments.length + 1 >= document.getElementById('commentcontainer').children.length
-                );
-                if (comments.length > 0) {
-                    if (commentKey >= comments.length) {
-                        commentKey = commentKey % comments.length;
+                    );
+                    PrivateBin.Model.init();
+                    PrivateBin.DiscussionViewer.init();
+                    results.push(
+                        !document.getElementById('discussion').classList.contains('hidden')
+                    );
+                    PrivateBin.DiscussionViewer.prepareNewDiscussion();
+                    results.push(
+                        document.getElementById('discussion').classList.contains('hidden')
+                    );
+                    comments.forEach(function (comment) {
+                        comment.id = comment.idArray.join('');
+                        comment.parentid = comment.parentidArray.join('');
+                        PrivateBin.DiscussionViewer.addComment(PrivateBin.Helper.CommentFactory(comment), comment.data, comment.meta.nickname);
+                    });
+                    results.push(
+                        document.getElementById('discussion').classList.contains('hidden')
+                    );
+                    PrivateBin.DiscussionViewer.finishDiscussion();
+                    results.push(
+                        !document.getElementById('discussion').classList.contains('hidden') &&
+                        comments.length + 1 >= document.getElementById('commentcontainer').children.length
+                    );
+                    if (comments.length > 0) {
+                        if (commentKey >= comments.length) {
+                            commentKey = commentKey % comments.length;
+                        }
+                        PrivateBin.DiscussionViewer.highlightComment(comments[commentKey].id, fadeOut);
+                        results.push(
+                            document.getElementById('comment_' + comments[commentKey].id).classList.contains('highlight')
+                        );
                     }
-                    PrivateBin.DiscussionViewer.highlightComment(comments[commentKey].id, fadeOut);
+                    // clicking "Add comment" button should open the reply form
+                    document.getElementById('commentcontainer').querySelector('button').click();
                     results.push(
-                        document.getElementById('comment_' + comments[commentKey].id).classList.contains('highlight')
+                        !document.getElementById('reply').classList.contains('hidden')
                     );
-                }
-                // clicking "Add comment" button should open the reply form
-                document.getElementById('commentcontainer').querySelector('button').click();
-                results.push(
-                    !document.getElementById('reply').classList.contains('hidden')
-                );
-                document.querySelector('#reply #nickname').value = nickname;
-                document.querySelector('#reply #replymessage').value = message;
-                PrivateBin.DiscussionViewer.getReplyCommentId();
-                results.push(
-                    PrivateBin.DiscussionViewer.getReplyNickname() === document.querySelector('#reply #nickname').value &&
-                    PrivateBin.DiscussionViewer.getReplyMessage() === document.querySelector('#reply #replymessage').value
-                );
-                var notificationResult = PrivateBin.DiscussionViewer.handleNotification(alertType === 'other' ? alert : alertType);
-                if (alertType === 'loading') {
-                    results.push(notificationResult === false);
-                } else {
+                    document.querySelector('#reply #nickname').value = nickname;
+                    document.querySelector('#reply #replymessage').value = message;
+                    PrivateBin.DiscussionViewer.getReplyCommentId();
                     results.push(
-                        alertType === 'danger' ? (
-                            notificationResult.classList.contains('alert-danger') &&
-                            !notificationResult.classList.contains('alert-info')
-                        ) : (
-                            !notificationResult.classList.contains('alert-danger') &&
-                            notificationResult.classList.contains('alert-info')
-                        )
+                        PrivateBin.DiscussionViewer.getReplyNickname() === document.querySelector('#reply #nickname').value &&
+                        PrivateBin.DiscussionViewer.getReplyMessage() === document.querySelector('#reply #replymessage').value
                     );
+                    var notificationResult = PrivateBin.DiscussionViewer.handleNotification(alertType === 'other' ? alert : alertType);
+                    if (alertType === 'loading') {
+                        results.push(notificationResult === false);
+                    } else {
+                        results.push(
+                            alertType === 'danger' ? (
+                                notificationResult.classList.contains('alert-danger') &&
+                                !notificationResult.classList.contains('alert-info')
+                            ) : (
+                                !notificationResult.classList.contains('alert-danger') &&
+                                notificationResult.classList.contains('alert-info')
+                            )
+                        );
+                    }
+                    clean();
+                    return results.every(element => element);
                 }
-                clean();
-                return results.every(element => element);
-            }
-        );
+            ));
+        });
     });
 });

+ 70 - 68
js/test/Editor.js

@@ -1,77 +1,79 @@
 'use strict';
 require('../common');
+const fc = require('fast-check');
 
 describe('Editor', function () {
     describe('show, hide, getText, setText & isPreview', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'returns text fed into the textarea, handles editor tabs',
-            'string',
-            function (text) {
-                var clean = globalThis.cleanup(),
-                    results = [];
-                document.body.innerHTML = (
-                    '<ul id="editorTabs" class="nav nav-tabs hidden">' +
-                        '<li role="presentation" class="active">' +
-                            '<a id="messageedit" href="#">Editor</a>' +
-                        '</li>' +
-                        '<li role="presentation">' +
-                            '<a id="messagepreview" href="#">Preview</a>' +
-                        '</li>' +
-                    '</ul>' +
-                    '<div id="placeholder" class="hidden">+++ no document text +++</div>' +
-                    '<div id="prettymessage" class="hidden">' +
-                        '<pre id="prettyprint" class="prettyprint linenums:1"></pre>' +
-                    '</div>' +
-                    '<div id="plaintext" class="hidden"></div>' +
-                    '<p>' +
-                        '<textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea>' +
-                    '</p>' +
-                    '<input id="messagetab" type="checkbox" checked="checked" />'
-                );
-                PrivateBin.Editor.init();
-                results.push(
-                    document.getElementById('editorTabs').classList.contains('hidden') &&
-                    document.getElementById('message').classList.contains('hidden')
-                );
-                PrivateBin.Editor.show();
-                results.push(
-                    !document.getElementById('editorTabs').classList.contains('hidden') &&
-                    !document.getElementById('message').classList.contains('hidden')
-                );
-                PrivateBin.Editor.hide();
-                results.push(
-                    document.getElementById('editorTabs').classList.contains('hidden') &&
-                    document.getElementById('message').classList.contains('hidden')
-                );
-                PrivateBin.Editor.show();
-                PrivateBin.Editor.focusInput();
-                results.push(
-                    PrivateBin.Editor.getText().length === 0
-                );
-                PrivateBin.Editor.setText(text);
-                results.push(
-                    PrivateBin.Editor.getText() === document.getElementById('message').value
-                );
-                PrivateBin.Editor.setText();
-                results.push(
-                    !PrivateBin.Editor.isPreview() &&
-                    !document.getElementById('message').classList.contains('hidden')
-                );
-                document.getElementById('messagepreview').click();
-                results.push(
-                    PrivateBin.Editor.isPreview() &&
-                    document.getElementById('message').classList.contains('hidden')
-                );
-                document.getElementById('messageedit').click();
-                results.push(
-                    !PrivateBin.Editor.isPreview() &&
-                    !document.getElementById('message').classList.contains('hidden')
-                );
-                clean();
-                return results.every(element => element);
-            }
-        );
+        it('returns text fed into the textarea, handles editor tabs', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (text) {
+                    var clean = globalThis.cleanup(),
+                        results = [];
+                    document.body.innerHTML = (
+                        '<ul id="editorTabs" class="nav nav-tabs hidden">' +
+                            '<li role="presentation" class="active">' +
+                                '<a id="messageedit" href="#">Editor</a>' +
+                            '</li>' +
+                            '<li role="presentation">' +
+                                '<a id="messagepreview" href="#">Preview</a>' +
+                            '</li>' +
+                        '</ul>' +
+                        '<div id="placeholder" class="hidden">+++ no document text +++</div>' +
+                        '<div id="prettymessage" class="hidden">' +
+                            '<pre id="prettyprint" class="prettyprint linenums:1"></pre>' +
+                        '</div>' +
+                        '<div id="plaintext" class="hidden"></div>' +
+                        '<p>' +
+                            '<textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea>' +
+                        '</p>' +
+                        '<input id="messagetab" type="checkbox" checked="checked" />'
+                    );
+                    PrivateBin.Editor.init();
+                    results.push(
+                        document.getElementById('editorTabs').classList.contains('hidden') &&
+                        document.getElementById('message').classList.contains('hidden')
+                    );
+                    PrivateBin.Editor.show();
+                    results.push(
+                        !document.getElementById('editorTabs').classList.contains('hidden') &&
+                        !document.getElementById('message').classList.contains('hidden')
+                    );
+                    PrivateBin.Editor.hide();
+                    results.push(
+                        document.getElementById('editorTabs').classList.contains('hidden') &&
+                        document.getElementById('message').classList.contains('hidden')
+                    );
+                    PrivateBin.Editor.show();
+                    PrivateBin.Editor.focusInput();
+                    results.push(
+                        PrivateBin.Editor.getText().length === 0
+                    );
+                    PrivateBin.Editor.setText(text);
+                    results.push(
+                        PrivateBin.Editor.getText() === document.getElementById('message').value
+                    );
+                    PrivateBin.Editor.setText();
+                    results.push(
+                        !PrivateBin.Editor.isPreview() &&
+                        !document.getElementById('message').classList.contains('hidden')
+                    );
+                    document.getElementById('messagepreview').click();
+                    results.push(
+                        PrivateBin.Editor.isPreview() &&
+                        document.getElementById('message').classList.contains('hidden')
+                    );
+                    document.getElementById('messageedit').click();
+                    results.push(
+                        !PrivateBin.Editor.isPreview() &&
+                        !document.getElementById('message').classList.contains('hidden')
+                    );
+                    clean();
+                    return results.every(element => element);
+                }
+            ));
+        });
     });
 });

+ 292 - 257
js/test/Helper.js

@@ -1,45 +1,68 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('Helper', function () {
     describe('secondsToHuman', function () {
-        jsc.property('returns an array with a number and a word', 'integer', function (number) {
-            var result = PrivateBin.Helper.secondsToHuman(number);
-            return Array.isArray(result) &&
-                result.length === 2 &&
-                result[0] === parseInt(result[0], 10) &&
-                typeof result[1] === 'string';
+        it('returns an array with a number and a word', () => {
+            fc.assert(fc.property(fc.integer(), function (number) {
+                var result = PrivateBin.Helper.secondsToHuman(number);
+                return Array.isArray(result) &&
+                    result.length === 2 &&
+                    result[0] === parseInt(result[0], 10) &&
+                    typeof result[1] === 'string';
+            }));
         });
-        jsc.property('returns seconds on the first array position', 'integer 59', function (number) {
-            return PrivateBin.Helper.secondsToHuman(number)[0] === number;
+        it('returns seconds on the first array position', () => {
+            fc.assert(fc.property(fc.integer({max: 59}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[0] === number;
+            }));
         });
-        jsc.property('returns seconds on the second array position', 'integer 59', function (number) {
-            return PrivateBin.Helper.secondsToHuman(number)[1] === 'second';
+        it('returns seconds on the second array position', () => {
+            fc.assert(fc.property(fc.integer({max: 59}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[1] === 'second';
+            }));
         });
-        jsc.property('returns minutes on the first array position', 'integer 60 3599', function (number) {
-            return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / 60);
+        it('returns minutes on the first array position', () => {
+            fc.assert(fc.property(fc.integer({min: 60, max: 3599}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / 60);
+            }));
         });
-        jsc.property('returns minutes on the second array position', 'integer 60 3599', function (number) {
-            return PrivateBin.Helper.secondsToHuman(number)[1] === 'minute';
+        it('returns minutes on the second array position', () => {
+            fc.assert(fc.property(fc.integer({min: 60, max: 3599}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[1] === 'minute';
+            }));
         });
-        jsc.property('returns hours on the first array position', 'integer 3600 86399', function (number) {
-            return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60));
+        it('returns hours on the first array position', () => {
+            fc.assert(fc.property(fc.integer({min: 3600, max: 86399}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60));
+            }));
         });
-        jsc.property('returns hours on the second array position', 'integer 3600 86399', function (number) {
-            return PrivateBin.Helper.secondsToHuman(number)[1] === 'hour';
+        it('returns hours on the second array position', () => {
+            fc.assert(fc.property(fc.integer({min: 3600, max: 86399}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[1] === 'hour';
+            }));
         });
-        jsc.property('returns days on the first array position', 'integer 86400 5184000', function (number) {
-            return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24));
+        it('returns days on the first array position', () => {
+            fc.assert(fc.property(fc.integer({min: 86400, max: 5184000}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24));
+            }));
         });
-        jsc.property('returns days on the second array position', 'integer 86400 5184000', function (number) {
-            return PrivateBin.Helper.secondsToHuman(number)[1] === 'day';
+        it('returns days on the second array position', () => {
+            fc.assert(fc.property(fc.integer({min: 86400, max: 5184000}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[1] === 'day';
+            }));
         });
         // max safe integer as per http://ecma262-5.com/ELS5_HTML.htm#Section_8.5
-        jsc.property('returns months on the first array position', 'integer 5184000 9007199254740991', function (number) {
-            return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24 * 30));
+        it('returns months on the first array position', () => {
+            fc.assert(fc.property(fc.integer({min: 5184000}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[0] === Math.floor(number / (60 * 60 * 24 * 30));
+            }));
         });
-        jsc.property('returns months on the second array position', 'integer 5184000 9007199254740991', function (number) {
-            return PrivateBin.Helper.secondsToHuman(number)[1] === 'month';
+        it('returns months on the second array position', () => {
+            fc.assert(fc.property(fc.integer({min: 5184000}), function (number) {
+                return PrivateBin.Helper.secondsToHuman(number)[1] === 'month';
+            }));
         });
     });
 
@@ -47,29 +70,30 @@ describe('Helper', function () {
     // TODO: This needs to be tested using a browser.
     describe('selectText', function () {
         this.timeout(30000);
-        jsc.property(
-            'selection contains content of given ID',
-            jsc.nearray(jsc.nearray(common.jscAlnumString())),
-            'nearray string',
-            function (ids, contents) {
-                var html = '',
-                    result = true,
-                    clean = globalThis.cleanup(html);
-                ids.forEach(function(item, i) {
-                    html += '<div id="' + item.join('') + '">' + PrivateBin.Helper.htmlEntities(contents[i] || contents[0]) + '</div>';
-                });
-                // TODO: As per https://github.com/tmpvar/jsdom/issues/321 there is no getSelection in jsdom, yet.
-                // Once there is one, uncomment the block below to actually check the result.
-                /*
-                ids.forEach(function(item, i) {
-                    PrivateBin.Helper.selectText(item.join(''));
-                    result *= (contents[i] || contents[0]) === window.getSelection().toString();
-                });
-                */
-                clean();
-                return Boolean(result);
-            }
-        );
+        it('selection contains content of given ID', () => {
+            fc.assert(fc.property(
+                fc.array(fc.array(common.fcAlnumString(), {minLength: 1}), {minLength: 1}),
+                fc.array(fc.string(), {minLength: 1}),
+                function (ids, contents) {
+                    var html = '',
+                        result = true,
+                        clean = globalThis.cleanup(html);
+                    ids.forEach(function(item, i) {
+                        html += '<div id="' + item.join('') + '">' + PrivateBin.Helper.htmlEntities(contents[i] || contents[0]) + '</div>';
+                    });
+                    // TODO: As per https://github.com/tmpvar/jsdom/issues/321 there is no getSelection in jsdom, yet.
+                    // Once there is one, uncomment the block below to actually check the result.
+                    /*
+                    ids.forEach(function(item, i) {
+                        PrivateBin.Helper.selectText(item.join(''));
+                        result *= (contents[i] || contents[0]) === window.getSelection().toString();
+                    });
+                    */
+                    clean();
+                    return Boolean(result);
+                }
+            ));
+        });
     });
 
     describe('urls2links', function () {
@@ -81,279 +105,290 @@ describe('Helper', function () {
 
         this.timeout(30000);
         before(function () {
-            cleanup = globalThis.cleanup();
+            globalThis.cleanup();
         });
 
-        jsc.property(
-            'ignores non-URL content',
-            'string',
-            function (content) {
-                // eslint-disable-next-line no-control-regex
-                content = content.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
-                let clean = globalThis.cleanup();
-                document.body.innerHTML = '<div id="foo"></div>';
-                let e = document.getElementById('foo');
-                e.textContent = content;
-                PrivateBin.Helper.urls2links(e);
-                let result = e.textContent;
-                clean();
-                return content === result;
-            }
-        );
-        jsc.property(
-            'replaces URLs with anchors',
-            'string',
-            common.jscUrl(),
-            jsc.array(common.jscHashString()),
-            'string',
-            function (prefix, url, fragment, postfix) {
-                // eslint-disable-next-line no-control-regex
-                prefix = prefix.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
-                // eslint-disable-next-line no-control-regex
-                postfix  = ' ' + postfix.replace(/\r/g, '\n').replace(/\u0000/g, '');
-                url.fragment = fragment.join('');
-                let urlString = common.urlToString(url),
-                    clean = globalThis.cleanup();
-                document.body.innerHTML = '<div id="foo"></div>';
-                let e = document.getElementById('foo');
-
-                // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. &#0 or &#x
-                if (
-                    url.query[-1] === '&' &&
-                    (parseInt(url.fragment.charAt(0), 10) >= 0 || url.fragment.charAt(0) === 'x')
-                ) {
-                    url.query.pop();
-                    urlString = common.urlToString(url);
-                    postfix = '';
+        it('ignores non-URL content', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (content) {
+                    // eslint-disable-next-line no-control-regex
+                    content = content.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
+                    let clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="foo"></div>';
+                    let e = document.getElementById('foo');
+                    e.textContent = content;
+                    PrivateBin.Helper.urls2links(e);
+                    let result = e.textContent;
+                    clean();
+                    return content === result;
                 }
-                e.textContent = prefix + urlString + postfix;
-                PrivateBin.Helper.urls2links(e);
+            ));
+        });
+        it('replaces URLs with anchors', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                common.fcUrl(),
+                fc.array(common.fcHashString()),
+                fc.string(),
+                function (prefix, url, fragment, postfix) {
+                    // eslint-disable-next-line no-control-regex
+                    prefix = prefix.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
+                    // eslint-disable-next-line no-control-regex
+                    postfix  = ' ' + postfix.replace(/\r/g, '\n').replace(/\u0000/g, '');
+                    url.fragment = fragment.join('');
+                    let urlString = common.urlToString(url),
+                        clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="foo"></div>';
+                    let e = document.getElementById('foo');
 
-                let result = e.innerHTML;
-                clean();
+                    // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. &#0 or &#x
+                    if (
+                        url.query[-1] === '&' &&
+                        (parseInt(url.fragment.charAt(0), 10) >= 0 || url.fragment.charAt(0) === 'x')
+                    ) {
+                        url.query.pop();
+                        urlString = common.urlToString(url);
+                        postfix = '';
+                    }
+                    e.textContent = prefix + urlString + postfix;
+                    PrivateBin.Helper.urls2links(e);
+
+                    let result = e.innerHTML;
+                    clean();
 
-                urlString = getTextAsRenderedHtml(urlString);
-                const expected = getTextAsRenderedHtml(prefix) + '<a href="' + urlString + '" target="_blank" rel="nofollow noopener noreferrer">' + urlString + '</a>' + getTextAsRenderedHtml(postfix);
-                return expected === result;
-            }
-        );
-        jsc.property(
-            'replaces magnet links with anchors',
-            'string',
-            jsc.array(common.jscQueryString()),
-            'string',
-            function (prefix, query, postfix) {
-                // eslint-disable-next-line no-control-regex
-                prefix = prefix.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
-                // eslint-disable-next-line no-control-regex
-                postfix = ' ' + postfix.replace(/\r/g, '\n').replace(/\u0000/g, '');
-                let url  = 'magnet:?' + query.join('').replace(/^&+|&+$/gm, ''),
-                    clean = globalThis.cleanup();
-                document.body.innerHTML = '<div id="foo"></div>';
-                let e = document.getElementById('foo');
-                e.textContent = prefix + url + postfix;
-                PrivateBin.Helper.urls2links(e);
-                let result = e.innerHTML;
-                clean();
-                url = getTextAsRenderedHtml(url);
-                return getTextAsRenderedHtml(prefix) + '<a href="' + url + '" target="_blank" rel="nofollow noopener noreferrer">' + url + '</a>' + getTextAsRenderedHtml(postfix) === result;
-            }
-        );
+                    urlString = getTextAsRenderedHtml(urlString);
+                    const expected = getTextAsRenderedHtml(prefix) + '<a href="' + urlString + '" target="_blank" rel="nofollow noopener noreferrer">' + urlString + '</a>' + getTextAsRenderedHtml(postfix);
+                    return expected === result;
+                }
+            ));
+        });
+        it('replaces magnet links with anchors', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(common.fcQueryString()),
+                fc.string(),
+                function (prefix, query, postfix) {
+                    // eslint-disable-next-line no-control-regex
+                    prefix = prefix.replace(/\r|\f/g, '\n').replace(/\u0000|\u000b/g, '');
+                    // eslint-disable-next-line no-control-regex
+                    postfix = ' ' + postfix.replace(/\r/g, '\n').replace(/\u0000/g, '');
+                    let url  = 'magnet:?' + query.join('').replace(/^&+|&+$/gm, ''),
+                        clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="foo"></div>';
+                    let e = document.getElementById('foo');
+                    e.textContent = prefix + url + postfix;
+                    PrivateBin.Helper.urls2links(e);
+                    let result = e.innerHTML;
+                    clean();
+                    url = getTextAsRenderedHtml(url);
+                    return getTextAsRenderedHtml(prefix) + '<a href="' + url + '" target="_blank" rel="nofollow noopener noreferrer">' + url + '</a>' + getTextAsRenderedHtml(postfix) === result;
+                }
+            ));
+        });
     });
 
     describe('sprintf', function () {
-        jsc.property(
-            'replaces %s in strings with first given parameter',
-            'string',
-            '(small nearray) string',
-            'string',
-            function (prefix, params, postfix) {
-                prefix    =    prefix.replace(/%(s|d)/g, '%%');
-                params[0] = params[0].replace(/%(s|d)/g, '%%');
-                postfix   =   postfix.replace(/%(s|d)/g, '%%');
-                var result = prefix + params[0] + postfix;
-                params.unshift(prefix + '%s' + postfix);
-                return result === PrivateBin.Helper.sprintf.apply(this, params);
-            }
-        );
-        jsc.property(
-            'replaces %d in strings with first given parameter',
-            'string',
-            '(small nearray) nat',
-            'string',
-            function (prefix, params, postfix) {
-                prefix  =  prefix.replace(/%(s|d)/g, '%%');
-                postfix = postfix.replace(/%(s|d)/g, '%%');
-                var result = prefix + params[0] + postfix;
-                params.unshift(prefix + '%d' + postfix);
-                return result === PrivateBin.Helper.sprintf.apply(this, params);
-            }
-        );
-        jsc.property(
-            'replaces %d in strings with 0 if first parameter is not a number',
-            'string',
-            '(small nearray) falsy',
-            'string',
-            function (prefix, params, postfix) {
-                prefix  =  prefix.replace(/%(s|d)/g, '%%');
-                postfix = postfix.replace(/%(s|d)/g, '%%');
-                var result = prefix + '0' + postfix;
-                params.unshift(prefix + '%d' + postfix);
-                return result === PrivateBin.Helper.sprintf.apply(this, params);
-            }
-        );
-        jsc.property(
-            'replaces %d and %s in strings in order',
-            'string',
-            'nat',
-            'string',
-            'string',
-            'string',
-            function (prefix, uint, middle, string, postfix) {
-                prefix  =  prefix.replace(/%(s|d)/g, '');
-                middle  =  middle.replace(/%(s|d)/g, '');
-                postfix = postfix.replace(/%(s|d)/g, '');
-                var params = [prefix + '%d' + middle + '%s' + postfix, uint, string],
-                    result = prefix + uint + middle + string + postfix;
-                return result === PrivateBin.Helper.sprintf.apply(this, params);
-            }
-        );
-        jsc.property(
-            'replaces %d and %s in strings in reverse order',
-            'string',
-            'nat',
-            'string',
-            'string',
-            'string',
-            function (prefix, uint, middle, string, postfix) {
-                prefix  =  prefix.replace(/%(s|d)/g, '');
-                middle  =  middle.replace(/%(s|d)/g, '');
-                postfix = postfix.replace(/%(s|d)/g, '');
-                var params = [prefix + '%s' + middle + '%d' + postfix, string, uint],
-                    result = prefix + string + middle + uint + postfix;
-                return result === PrivateBin.Helper.sprintf.apply(this, params);
-            }
-        );
+        it('replaces %s in strings with first given parameter', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.string(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix    =    prefix.replace(/%(s|d)/g, '%%');
+                    params[0] = params[0].replace(/%(s|d)/g, '%%');
+                    postfix   =   postfix.replace(/%(s|d)/g, '%%');
+                    var result = prefix + params[0] + postfix;
+                    params.unshift(prefix + '%s' + postfix);
+                    return result === PrivateBin.Helper.sprintf.apply(this, params);
+                }
+            ));
+        });
+        it('replaces %d in strings with first given parameter', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.nat(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix  =  prefix.replace(/%(s|d)/g, '%%');
+                    postfix = postfix.replace(/%(s|d)/g, '%%');
+                    var result = prefix + params[0] + postfix;
+                    params.unshift(prefix + '%d' + postfix);
+                    return result === PrivateBin.Helper.sprintf.apply(this, params);
+                }
+            ));
+        });
+        it('replaces %d in strings with 0 if first parameter is not a number', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.falsy(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix  =  prefix.replace(/%(s|d)/g, '%%');
+                    postfix = postfix.replace(/%(s|d)/g, '%%');
+                    var result = prefix + '0' + postfix;
+                    params.unshift(prefix + '%d' + postfix);
+                    return result === PrivateBin.Helper.sprintf.apply(this, params);
+                }
+            ));
+        });
+        it('replaces %d and %s in strings in order', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.nat(),
+                fc.string(),
+                fc.string(),
+                fc.string(),
+                function (prefix, uint, middle, string, postfix) {
+                    prefix  =  prefix.replace(/%(s|d)/g, '');
+                    middle  =  middle.replace(/%(s|d)/g, '');
+                    postfix = postfix.replace(/%(s|d)/g, '');
+                    var params = [prefix + '%d' + middle + '%s' + postfix, uint, string],
+                        result = prefix + uint + middle + string + postfix;
+                    return result === PrivateBin.Helper.sprintf.apply(this, params);
+                }
+            ));
+        });
+        it('replaces %d and %s in strings in reverse order', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.nat(),
+                fc.string(),
+                fc.string(),
+                fc.string(),
+                function (prefix, uint, middle, string, postfix) {
+                    prefix  =  prefix.replace(/%(s|d)/g, '');
+                    middle  =  middle.replace(/%(s|d)/g, '');
+                    postfix = postfix.replace(/%(s|d)/g, '');
+                    var params = [prefix + '%s' + middle + '%d' + postfix, string, uint],
+                        result = prefix + string + middle + uint + postfix;
+                    return result === PrivateBin.Helper.sprintf.apply(this, params);
+                }
+            ));
+        });
     });
 
     describe('getCookie', function () {
         this.timeout(30000);
         before(function () {
-            cleanup();
+            globalThis.cleanup();
         });
 
 /* TODO test fails since jsDOM version 17 - document.cookie remains empty
-        jsc.property(
-            'returns the requested cookie',
-            jsc.nearray(jsc.nearray(common.jscAlnumString())),
-            jsc.nearray(jsc.nearray(common.jscAlnumString())),
-            function (labels, values) {
-                let selectedKey = '', selectedValue = '';
-                const clean = globalThis.cleanup();
-                labels.forEach(function(item, i) {
-                    const key = item.join(''),
-                        value = (values[i] || values[0]).join('');
-                    document.cookie = key + '=' + value;
-                    if (Math.random() < 1 / i || selectedKey === key)
-                    {
-                        selectedKey = key;
-                        selectedValue = value;
-                    }
-                });
-                const result = PrivateBin.Helper.getCookie(selectedKey);
-                PrivateBin.Helper.reset();
-                clean();
-                return result === selectedValue;
-            }
-        ); */
+        it('returns the requested cookie', () => {
+            fc.assert(fc.property(
+                fc.array(fc.array(common.fcAlnumString(), {minLength: 1}), {minLength: 1}),
+                fc.array(fc.array(common.fcAlnumString(), {minLength: 1}), {minLength: 1}),
+                function (labels, values) {
+                    let selectedKey = '', selectedValue = '';
+                    const clean = globalThis.cleanup();
+                    labels.forEach(function(item, i) {
+                        const key = item.join(''),
+                            value = (values[i] || values[0]).join('');
+                        document.cookie = key + '=' + value;
+                        if (Math.random() < 1 / i || selectedKey === key)
+                        {
+                            selectedKey = key;
+                            selectedValue = value;
+                        }
+                    });
+                    const result = PrivateBin.Helper.getCookie(selectedKey);
+                    PrivateBin.Helper.reset();
+                    clean();
+                    return result === selectedValue;
+                }
+            ));
+        }); */
     });
 
     describe('baseUri', function () {
         this.timeout(30000);
-        jsc.property(
-            'returns the URL without query & fragment',
-            common.jscSchemas(false),
-            common.jscUrl(),
-            function (schema, url) {
-                url.schema = schema;
-                const fullUrl = common.urlToString(url);
-                delete(url.query);
-                delete(url.fragment);
-                PrivateBin.Helper.reset();
-                const expected = common.urlToString(url),
-                    clean = globalThis.cleanup('', {url: fullUrl}),
-                    result = PrivateBin.Helper.baseUri();
-                clean();
-                return expected === result;
-            }
-        );
+        it('returns the URL without query & fragment', () => {
+            fc.assert(fc.property(
+                common.fcSchemas(false),
+                common.fcUrl(),
+                function (schema, url) {
+                    url.schema = schema;
+                    const fullUrl = common.urlToString(url);
+                    delete(url.query);
+                    delete(url.fragment);
+                    PrivateBin.Helper.reset();
+                    const expected = common.urlToString(url),
+                        clean = globalThis.cleanup('', {url: fullUrl}),
+                        result = PrivateBin.Helper.baseUri();
+                    clean();
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('htmlEntities', function () {
         before(function () {
-            cleanup = globalThis.cleanup();
+            globalThis.cleanup();
         });
 
-        jsc.property(
-            'removes all HTML entities from any given string',
-            'string',
-            function (string) {
-                var result = PrivateBin.Helper.htmlEntities(string);
-                return !(/[<>]/.test(result)) && !(string.indexOf('&') > -1 && !(/&amp;/.test(result)));
-            }
-        );
+        it('removes all HTML entities from any given string', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (string) {
+                    var result = PrivateBin.Helper.htmlEntities(string);
+                    return !(/[<>]/.test(result)) && !(string.indexOf('&') > -1 && !(/&amp;/.test(result)));
+                }
+            ));
+        });
     });
 
     describe('formatBytes', function () {
-        jsc.property('returns 0 B for 0 bytes', function () {
+        it('returns 0 B for 0 bytes', function () {
             return PrivateBin.Helper.formatBytes(0) === '0 B';
         });
 
-        jsc.property('formats bytes < 1000 as B', function () {
+        it('formats bytes < 1000 as B', function () {
             return PrivateBin.Helper.formatBytes(500) === '500 B';
         });
 
-        jsc.property('formats kilobytes correctly', function () {
+        it('formats kilobytes correctly', function () {
             return PrivateBin.Helper.formatBytes(1500) === '1.5 kB';
         });
 
-        jsc.property('formats megabytes correctly', function () {
+        it('formats megabytes correctly', function () {
             return PrivateBin.Helper.formatBytes(2 * 1000 * 1000) === '2 MB';
         });
 
-        jsc.property('formats gigabytes correctly', function () {
+        it('formats gigabytes correctly', function () {
             return PrivateBin.Helper.formatBytes(3.45 * 1000 * 1000 * 1000) === '3.45 GB';
         });
 
-        jsc.property('formats terabytes correctly', function () {
+        it('formats terabytes correctly', function () {
             return PrivateBin.Helper.formatBytes(1.75 * 1000 ** 4) === '1.75 TB';
         });
 
-        jsc.property('formats petabytes correctly', function () {
+        it('formats petabytes correctly', function () {
             return PrivateBin.Helper.formatBytes(1.5 * 1000 ** 5) === '1.5 PB';
         });
 
-        jsc.property('formats exabytes correctly', function () {
+        it('formats exabytes correctly', function () {
             return PrivateBin.Helper.formatBytes(1.2345 * 1000 ** 6).startsWith('1.23 EB');
         });
 
-        jsc.property('formats yottabytes correctly', function () {
+        it('formats yottabytes correctly', function () {
             return PrivateBin.Helper.formatBytes(1.23 * 1000 ** 8).startsWith('1.23 YB');
         });
 
-        jsc.property('rounds to two decimal places', function () {
+        it('rounds to two decimal places', function () {
             return PrivateBin.Helper.formatBytes(1234567) === '1.23 MB';
         });
     });
 
 
     describe('isBootstrap5', function () {
-        jsc.property('Bootstrap 5 has been detected', function () {
+        it('Bootstrap 5 has been detected', function () {
             global.bootstrap = {};
             return PrivateBin.Helper.isBootstrap5() === true;
         });
 
-        jsc.property('Bootstrap 5 has not been detected', function () {
+        it('Bootstrap 5 has not been detected', function () {
             delete global.bootstrap;
             return PrivateBin.Helper.isBootstrap5() === false;
         });

+ 207 - 200
js/test/I18n.js

@@ -1,5 +1,6 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('I18n', function () {
     describe('translate', function () {
@@ -8,156 +9,161 @@ describe('I18n', function () {
             PrivateBin.I18n.reset();
         });
 
-        jsc.property(
-            'returns message ID unchanged if no translation found',
-            'string',
-            function (messageId) {
-                messageId   = messageId.replace(/%(s|d)/g, '%%');
-                var plurals = [messageId, messageId + 's'],
-                    fake    = [messageId],
-                    result  = PrivateBin.I18n.translate(messageId);
-                PrivateBin.I18n.reset();
-
-                var alias = PrivateBin.I18n._(messageId);
-                PrivateBin.I18n.reset();
-
-                var pluralResult = PrivateBin.I18n.translate(plurals);
-                PrivateBin.I18n.reset();
-
-                var pluralAlias = PrivateBin.I18n._(plurals);
-                PrivateBin.I18n.reset();
-
-                var fakeResult = PrivateBin.I18n.translate(fake);
-                PrivateBin.I18n.reset();
-
-                var fakeAlias = PrivateBin.I18n._(fake);
-                PrivateBin.I18n.reset();
-
-                if (messageId.indexOf('<a') === -1) {
-                    messageId = PrivateBin.Helper.htmlEntities(messageId);
-                } else {
-                    messageId = DOMPurify.sanitize(
-                        messageId, {
-                            ALLOWED_TAGS: ['a', 'i', 'span'],
+        it('returns message ID unchanged if no translation found', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (messageId) {
+                    messageId   = messageId.replace(/%(s|d)/g, '%%');
+                    var plurals = [messageId, messageId + 's'],
+                        fake    = [messageId],
+                        result  = PrivateBin.I18n.translate(messageId);
+                    PrivateBin.I18n.reset();
+
+                    var alias = PrivateBin.I18n._(messageId);
+                    PrivateBin.I18n.reset();
+
+                    var pluralResult = PrivateBin.I18n.translate(plurals);
+                    PrivateBin.I18n.reset();
+
+                    var pluralAlias = PrivateBin.I18n._(plurals);
+                    PrivateBin.I18n.reset();
+
+                    var fakeResult = PrivateBin.I18n.translate(fake);
+                    PrivateBin.I18n.reset();
+
+                    var fakeAlias = PrivateBin.I18n._(fake);
+                    PrivateBin.I18n.reset();
+
+                    if (messageId.indexOf('<a') === -1) {
+                        messageId = PrivateBin.Helper.htmlEntities(messageId);
+                    } else {
+                        messageId = DOMPurify.sanitize(
+                            messageId, {
+                                ALLOWED_TAGS: ['a', 'i', 'span'],
+                                ALLOWED_ATTR: ['href', 'id']
+                            }
+                        );
+                    }
+                    return messageId === result && messageId === alias &&
+                        messageId === pluralResult && messageId === pluralAlias &&
+                        messageId === fakeResult && messageId === fakeAlias;
+                }
+            ));
+        });
+        it('replaces %s in strings with first given parameter, encoding all, when no link is in the messageID', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.string(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix    =    prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
+                    params[0] = params[0].replace(/%(s|d)/g, '%%');
+                    postfix   =   postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
+                    const translation = PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix);
+                    params.unshift(prefix + '%s' + postfix);
+                    const result = PrivateBin.I18n.translate.apply(this, params);
+                    PrivateBin.I18n.reset();
+                    const alias = PrivateBin.I18n._.apply(this, params);
+                    PrivateBin.I18n.reset();
+                    return translation === result && translation === alias;
+                }
+            ));
+        });
+        it('replaces %s in strings with first given parameter, encoding params only, when a link is part of the messageID', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.string(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix    =    prefix.replace(/%(s|d)/g, '%%');
+                    params[0] = params[0].replace(/%(s|d)/g, '%%');
+                    postfix   =   postfix.replace(/%(s|d)/g, '%%');
+                    const translation = DOMPurify.sanitize(
+                        prefix + '<a href="' + params[0] + '"></a>' + postfix, {
+                            ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
+                            ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
                             ALLOWED_ATTR: ['href', 'id']
                         }
                     );
+                    params.unshift(prefix + '<a href="%s"></a>' + postfix);
+                    const result = PrivateBin.I18n.translate.apply(this, params);
+                    PrivateBin.I18n.reset();
+                    const alias = PrivateBin.I18n._.apply(this, params);
+                    PrivateBin.I18n.reset();
+                    return translation === result && translation === alias;
                 }
-                return messageId === result && messageId === alias &&
-                    messageId === pluralResult && messageId === pluralAlias &&
-                    messageId === fakeResult && messageId === fakeAlias;
-            }
-        );
-        jsc.property(
-            'replaces %s in strings with first given parameter, encoding all, when no link is in the messageID',
-            'string',
-            '(small nearray) string',
-            'string',
-            function (prefix, params, postfix) {
-                prefix    =    prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
-                params[0] = params[0].replace(/%(s|d)/g, '%%');
-                postfix   =   postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
-                const translation = PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix);
-                params.unshift(prefix + '%s' + postfix);
-                const result = PrivateBin.I18n.translate.apply(this, params);
-                PrivateBin.I18n.reset();
-                const alias = PrivateBin.I18n._.apply(this, params);
-                PrivateBin.I18n.reset();
-                return translation === result && translation === alias;
-            }
-        );
-        jsc.property(
-            'replaces %s in strings with first given parameter, encoding params only, when a link is part of the messageID',
-            'string',
-            '(small nearray) string',
-            'string',
-            function (prefix, params, postfix) {
-                prefix    =    prefix.replace(/%(s|d)/g, '%%');
-                params[0] = params[0].replace(/%(s|d)/g, '%%');
-                postfix   =   postfix.replace(/%(s|d)/g, '%%');
-                const translation = DOMPurify.sanitize(
-                    prefix + '<a href="' + params[0] + '"></a>' + postfix, {
-                        ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
-                        ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
-                        ALLOWED_ATTR: ['href', 'id']
-                    }
-                );
-                params.unshift(prefix + '<a href="%s"></a>' + postfix);
-                const result = PrivateBin.I18n.translate.apply(this, params);
-                PrivateBin.I18n.reset();
-                const alias = PrivateBin.I18n._.apply(this, params);
-                PrivateBin.I18n.reset();
-                return translation === result && translation === alias;
-            }
-        );
-        jsc.property(
-            'replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID',
-            'string',
-            '(small nearray) string',
-            'string',
-            function (prefix, params, postfix) {
-                prefix    =    prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
-                params[0] = params[0].replace(/%(s|d)/g, '%%');
-                postfix   =   postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
-                const tempDiv = document.createElement('textarea');
-                tempDiv.textContent = (prefix + params[0] + postfix);
-                const translation = tempDiv.textContent;
-                let args = Array.prototype.slice.call(params);
-                args.unshift(prefix + '%s' + postfix);
-                let clean = globalThis.cleanup();
-                document.body.innerHTML = '<div id="i18n"></div>';
-                const i18nElement = document.getElementById('i18n');
-                args.unshift(i18nElement);
-                PrivateBin.I18n.translate.apply(this, args);
-                const result = i18nElement.textContent;
-                PrivateBin.I18n.reset();
-                clean();
-                clean = globalThis.cleanup();
-                document.body.innerHTML = '<div id="i18n"></div>';
-                args[0] = document.getElementById('i18n');
-                PrivateBin.I18n._.apply(this, args);
-                const alias = document.getElementById('i18n').textContent;
-                PrivateBin.I18n.reset();
-                clean();
-                return translation === result && translation === alias;
-            }
-        );
-        jsc.property(
-            'replaces %s in strings with first given parameter into an element, encoding params only, when a link is part of the messageID inserted',
-            'string',
-            '(small nearray) string',
-            'string',
-            function (prefix, params, postfix) {
-                prefix    =    prefix.replace(/%(s|d)/g, '%%').trim();
-                params[0] = params[0].replace(/%(s|d)/g, '%%').trim();
-                postfix   =   postfix.replace(/%(s|d)/g, '%%').trim();
-                const translation = DOMPurify.sanitize(
-                    prefix + '<a href="' + params[0] + '"></a>' + postfix, {
-                        ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
-                        ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
-                        ALLOWED_ATTR: ['href', 'id']
-                    }
-                );
-                let args = Array.prototype.slice.call(params);
-                args.unshift(prefix + '<a href="%s"></a>' + postfix);
-                let clean = globalThis.cleanup();
-                document.body.innerHTML = '<div id="i18n"></div>';
-                const i18nElement2 = document.getElementById('i18n');
-                args.unshift(i18nElement2);
-                PrivateBin.I18n.translate.apply(this, args);
-                const result = i18nElement2.innerHTML;
-                PrivateBin.I18n.reset();
-                clean();
-                clean = globalThis.cleanup();
-                document.body.innerHTML = '<div id="i18n"></div>';
-                args[0] = document.getElementById('i18n');
-                PrivateBin.I18n._.apply(this, args);
-                const alias = document.getElementById('i18n').innerHTML;
-                PrivateBin.I18n.reset();
-                clean();
-                return translation === result && translation === alias;
-            }
-        );
+            ));
+        });
+        it('replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.string(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix    =    prefix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
+                    params[0] = params[0].replace(/%(s|d)/g, '%%');
+                    postfix   =   postfix.replace(/%(s|d)/g, '%%').replace(/<a/g, '');
+                    const tempDiv = document.createElement('textarea');
+                    tempDiv.textContent = (prefix + params[0] + postfix);
+                    const translation = tempDiv.textContent;
+                    let args = Array.prototype.slice.call(params);
+                    args.unshift(prefix + '%s' + postfix);
+                    let clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="i18n"></div>';
+                    const i18nElement = document.getElementById('i18n');
+                    args.unshift(i18nElement);
+                    PrivateBin.I18n.translate.apply(this, args);
+                    const result = i18nElement.textContent;
+                    PrivateBin.I18n.reset();
+                    clean();
+                    clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="i18n"></div>';
+                    args[0] = document.getElementById('i18n');
+                    PrivateBin.I18n._.apply(this, args);
+                    const alias = document.getElementById('i18n').textContent;
+                    PrivateBin.I18n.reset();
+                    clean();
+                    return translation === result && translation === alias;
+                }
+            ));
+        });
+        it('replaces %s in strings with first given parameter into an element, encoding params only, when a link is part of the messageID inserted', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                fc.array(fc.string(), {minLength: 1}),
+                fc.string(),
+                function (prefix, params, postfix) {
+                    prefix    =    prefix.replace(/%(s|d)/g, '%%').trim();
+                    params[0] = params[0].replace(/%(s|d)/g, '%%').trim();
+                    postfix   =   postfix.replace(/%(s|d)/g, '%%').trim();
+                    const translation = DOMPurify.sanitize(
+                        prefix + '<a href="' + params[0] + '"></a>' + postfix, {
+                            ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i,
+                            ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'],
+                            ALLOWED_ATTR: ['href', 'id']
+                        }
+                    );
+                    let args = Array.prototype.slice.call(params);
+                    args.unshift(prefix + '<a href="%s"></a>' + postfix);
+                    let clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="i18n"></div>';
+                    const i18nElement2 = document.getElementById('i18n');
+                    args.unshift(i18nElement2);
+                    PrivateBin.I18n.translate.apply(this, args);
+                    const result = i18nElement2.innerHTML;
+                    PrivateBin.I18n.reset();
+                    clean();
+                    clean = globalThis.cleanup();
+                    document.body.innerHTML = '<div id="i18n"></div>';
+                    args[0] = document.getElementById('i18n');
+                    PrivateBin.I18n._.apply(this, args);
+                    const alias = document.getElementById('i18n').innerHTML;
+                    PrivateBin.I18n.reset();
+                    clean();
+                    return translation === result && translation === alias;
+                }
+            ));
+        });
     });
 
     describe('getPluralForm', function () {
@@ -165,17 +171,18 @@ describe('I18n', function () {
             PrivateBin.I18n.reset();
         });
 
-        jsc.property(
-            'returns valid key for plural form',
-            common.jscSupportedLanguages(),
-            'integer',
-            function(language, n) {
-                PrivateBin.I18n.reset(language);
-                var result = PrivateBin.I18n.getPluralForm(n);
-                // arabic seems to have the highest plural count with 6 forms
-                return result >= 0 && result <= 5;
-            }
-        );
+        it('returns valid key for plural form', () => {
+            fc.assert(fc.property(
+                common.fcSupportedLanguages(),
+                fc.integer(),
+                function(language, n) {
+                    PrivateBin.I18n.reset(language);
+                    var result = PrivateBin.I18n.getPluralForm(n);
+                    // arabic seems to have the highest plural count with 6 forms
+                    return result >= 0 && result <= 5;
+                }
+            ));
+        });
     });
 
     // loading of JSON via AJAX needs to be tested in the browser, this just mocks it
@@ -186,50 +193,50 @@ describe('I18n', function () {
             PrivateBin.I18n.reset();
         });
 
-        jsc.property(
-            'downloads and handles any supported language',
-            common.jscSupportedLanguages(),
-            function(language) {
-                // cleanup
-                var clean = globalThis.cleanup('', {cookie: ['lang=en']});
-                PrivateBin.I18n.reset('en');
-                PrivateBin.I18n.loadTranslations();
-                clean();
-
-                // mock
-                clean = globalThis.cleanup('', {cookie: ['lang=' + language]});
-                // eslint-disable-next-line global-require
-                PrivateBin.I18n.reset(language, require('../../i18n/' + language + '.json'));
-                var loadedLang = PrivateBin.I18n.getLanguage(),
-                    result = PrivateBin.I18n.translate('Never'),
-                    alias  = PrivateBin.I18n._('Never');
-                clean();
-                return language === loadedLang && result === alias;
-            }
-        );
-
-        jsc.property(
-            'should default to en',
-            function() {
-                var clean = globalThis.cleanup('', {url: 'https://privatebin.net/'});
-
-                // when navigator.userLanguage is undefined and no default language
-                // is specified, it would throw an error
-                [ 'language', 'userLanguage' ].forEach(function (key) {
-                    Object.defineProperty(navigator, key, {
-                        value: undefined,
-                        writeable: false
-                    });
+        it('downloads and handles any supported language', () => {
+            fc.assert(fc.property(
+                common.fcSupportedLanguages(),
+                function(language) {
+                    // cleanup
+                    var clean = globalThis.cleanup('', {cookie: ['lang=en']});
+                    PrivateBin.I18n.reset('en');
+                    PrivateBin.I18n.loadTranslations();
+                    clean();
+
+                    // mock
+                    clean = globalThis.cleanup('', {cookie: ['lang=' + language]});
+                    // eslint-disable-next-line global-require
+                    PrivateBin.I18n.reset(language, require('../../i18n/' + language + '.json'));
+                    var loadedLang = PrivateBin.I18n.getLanguage(),
+                        result = PrivateBin.I18n.translate('Never'),
+                        alias  = PrivateBin.I18n._('Never');
+                    clean();
+                    return language === loadedLang && result === alias;
+                }
+            ));
+        });
+
+        it('should default to en', () => {
+            var clean = globalThis.cleanup('', {url: 'https://privatebin.net/'});
+
+            // when navigator.userLanguage is undefined and no default language
+            // is specified, it would throw an error
+            [ 'language', 'userLanguage' ].forEach(function (key) {
+                Object.defineProperty(navigator, key, {
+                    value: undefined,
+                    configurable: true,
+                    enumerable: true,
+                    writable: false
                 });
+            });
 
-                PrivateBin.I18n.reset('en');
-                PrivateBin.I18n.loadTranslations();
-                var result = PrivateBin.I18n.translate('Never'),
-                    alias  = PrivateBin.I18n._('Never');
+            PrivateBin.I18n.reset('en');
+            PrivateBin.I18n.loadTranslations();
+            var result = PrivateBin.I18n.translate('Never'),
+                alias  = PrivateBin.I18n._('Never');
 
-                clean();
-                return 'Never' === result && 'Never' === alias;
-            }
-        );
+            clean();
+            return 'Never' === result && 'Never' === alias;
+        });
     });
 });

+ 206 - 195
js/test/Model.js

@@ -1,39 +1,41 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('Model', function () {
     describe('getExpirationDefault', function () {
         before(function () {
             PrivateBin.Model.reset();
-            cleanup = globalThis.cleanup();
+            globalThis.cleanup();
         });
 
-        jsc.property(
-            'returns the contents of the element with id "pasteExpiration"',
-            'nearray asciinestring',
-            'string',
-            'small nat',
-            function (keys, value, key) {
-                keys = keys.map(PrivateBin.Helper.htmlEntities);
-                value = PrivateBin.Helper.htmlEntities(value);
-                var content = keys.length > key ? keys[key] : keys[0],
-                    contents = '<select id="pasteExpiration" name="pasteExpiration">';
-                keys.forEach(function(item) {
-                    contents += '<option value="' + item + '"';
-                    if (item === content) {
-                        contents += ' selected="selected"';
-                    }
-                    contents += '>' + value + '</option>';
-                });
-                contents += '</select>';
-                document.body.innerHTML = contents;
-                var result = PrivateBin.Helper.htmlEntities(
-                    PrivateBin.Model.getExpirationDefault()
-                );
-                PrivateBin.Model.reset();
-                return content === result;
-            }
-        );
+        it('returns the contents of the element with id "pasteExpiration"', () => {
+            fc.assert(fc.property(
+                fc.array(fc.asciiString({minLength: 1}), {minLength: 1}),
+                fc.string(),
+                fc.nat(),
+                function (keys, value, key) {
+                    keys = keys.map(PrivateBin.Helper.htmlEntities);
+                    value = PrivateBin.Helper.htmlEntities(value);
+                    var content = keys.length > key ? keys[key] : keys[0],
+                        contents = '<select id="pasteExpiration" name="pasteExpiration">';
+                    keys.forEach(function(item) {
+                        contents += '<option value="' + item + '"';
+                        if (item === content) {
+                            contents += ' selected="selected"';
+                        }
+                        contents += '>' + value + '</option>';
+                    });
+                    contents += '</select>';
+                    document.body.innerHTML = contents;
+                    var result = PrivateBin.Helper.htmlEntities(
+                        PrivateBin.Model.getExpirationDefault()
+                    );
+                    PrivateBin.Model.reset();
+                    return content === result;
+                }
+            ));
+        });
     });
 
     describe('getFormatDefault', function () {
@@ -41,35 +43,36 @@ describe('Model', function () {
             PrivateBin.Model.reset();
         });
         after(function () {
-            cleanup();
+            globalThis.cleanup();
         });
 
-        jsc.property(
-            'returns the contents of the element with id "pasteFormatter"',
-            'nearray asciinestring',
-            'string',
-            'small nat',
-            function (keys, value, key) {
-                keys = keys.map(PrivateBin.Helper.htmlEntities);
-                value = PrivateBin.Helper.htmlEntities(value);
-                var content = keys.length > key ? keys[key] : keys[0],
-                    contents = '<select id="pasteFormatter" name="pasteFormatter">';
-                keys.forEach(function(item) {
-                    contents += '<option value="' + item + '"';
-                    if (item === content) {
-                        contents += ' selected="selected"';
-                    }
-                    contents += '>' + value + '</option>';
-                });
-                contents += '</select>';
-                document.body.innerHTML = contents;
-                var result = PrivateBin.Helper.htmlEntities(
-                    PrivateBin.Model.getFormatDefault()
-                );
-                PrivateBin.Model.reset();
-                return content === result;
-            }
-        );
+        it('returns the contents of the element with id "pasteFormatter"', () => {
+            fc.assert(fc.property(
+                fc.array(fc.asciiString({minLength: 1}), {minLength: 1}),
+                fc.string(),
+                fc.nat(),
+                function (keys, value, key) {
+                    keys = keys.map(PrivateBin.Helper.htmlEntities);
+                    value = PrivateBin.Helper.htmlEntities(value);
+                    var content = keys.length > key ? keys[key] : keys[0],
+                        contents = '<select id="pasteFormatter" name="pasteFormatter">';
+                    keys.forEach(function(item) {
+                        contents += '<option value="' + item + '"';
+                        if (item === content) {
+                            contents += ' selected="selected"';
+                        }
+                        contents += '>' + value + '</option>';
+                    });
+                    contents += '</select>';
+                    document.body.innerHTML = contents;
+                    var result = PrivateBin.Helper.htmlEntities(
+                        PrivateBin.Model.getFormatDefault()
+                    );
+                    PrivateBin.Model.reset();
+                    return content === result;
+                }
+            ));
+        });
     });
 
     describe('getPasteId', function () {
@@ -78,45 +81,47 @@ describe('Model', function () {
             PrivateBin.Model.reset();
         });
 
-        jsc.property(
-            'returns the query string without separator, if any',
-            common.jscUrl(true, false),
-            jsc.tuple(new Array(16).fill(common.jscHexString)),
-            jsc.array(common.jscQueryString()),
-            jsc.array(common.jscQueryString()),
-            function (url, pasteId, queryStart, queryEnd) {
-                if (queryStart.length > 0) {
-                    queryStart.push('&');
-                }
-                if (queryEnd.length > 0) {
-                    queryEnd.unshift('&');
-                }
-                url.query = queryStart.concat(pasteId, queryEnd);
-                const pasteIdString = pasteId.join(''),
-                    clean           = globalThis.cleanup('', {url: common.urlToString(url)});
-                const result = PrivateBin.Model.getPasteId();
-                PrivateBin.Model.reset();
-                clean();
-                return pasteIdString === result;
-            }
-        );
-        jsc.property(
-            'throws exception on empty query string',
-            common.jscUrl(true, false),
-            function (url) {
-                const clean = globalThis.cleanup('', {url: common.urlToString(url)});
-                let result = false;
-                try {
-                    PrivateBin.Model.getPasteId();
+        it('returns the query string without separator, if any', () => {
+            fc.assert(fc.property(
+                common.fcUrl(true, false),
+                fc.array(common.fcHexString(), {minLength: 16, maxLength: 16}),
+                fc.array(common.fcQueryString()),
+                fc.array(common.fcQueryString()),
+                function (url, pasteId, queryStart, queryEnd) {
+                    if (queryStart.length > 0) {
+                        queryStart.push('&');
+                    }
+                    if (queryEnd.length > 0) {
+                        queryEnd.unshift('&');
+                    }
+                    url.query = queryStart.concat(pasteId, queryEnd);
+                    const pasteIdString = pasteId.join(''),
+                        clean           = globalThis.cleanup('', {url: common.urlToString(url)});
+                    const result = PrivateBin.Model.getPasteId();
+                    PrivateBin.Model.reset();
+                    clean();
+                    return pasteIdString === result;
                 }
-                catch(err) {
-                    result = true;
+            ));
+        });
+        it('throws exception on empty query string', () => {
+            fc.assert(fc.property(
+                common.fcUrl(true, false),
+                function (url) {
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)});
+                    let result = false;
+                    try {
+                        PrivateBin.Model.getPasteId();
+                    }
+                    catch(err) {
+                        result = true;
+                    }
+                    PrivateBin.Model.reset();
+                    clean();
+                    return result;
                 }
-                PrivateBin.Model.reset();
-                clean();
-                return result;
-            }
-        );
+            ));
+        });
     });
 
     describe('getPasteKey', function () {
@@ -125,84 +130,89 @@ describe('Model', function () {
             PrivateBin.Model.reset();
         });
 
-        jsc.property(
-            'throws exception on v1 URLs',
-            common.jscUrl(),
-            function (url) {
-                url.fragment = '0OIl'; // any non-base58 string
-                const clean = globalThis.cleanup('', {url: common.urlToString(url)});
-                let result = false;
-                try {
-                    PrivateBin.Model.getPasteId();
+        it('throws exception on v1 URLs', () => {
+            fc.assert(fc.property(
+                common.fcUrl(),
+                function (url) {
+                    url.fragment = '0OIl'; // any non-base58 string
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)});
+                    let result = false;
+                    try {
+                        PrivateBin.Model.getPasteId();
+                    }
+                    catch(err) {
+                        result = true;
+                    }
+                    PrivateBin.Model.reset();
+                    clean();
+                    return result;
                 }
-                catch(err) {
-                    result = true;
+            ));
+        });
+        it('returns the fragment stripped of trailing query parts', () => {
+            fc.assert(fc.property(
+                common.fcUrl(),
+                fc.array(common.fcHashString()),
+                function (url, trail) {
+                    const fragment = url.fragment.padStart(32, '\u0000');
+                    url.fragment = PrivateBin.CryptTool.base58encode(fragment) + '&' + trail.join('');
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)}),
+                        result = PrivateBin.Model.getPasteKey();
+                    PrivateBin.Model.reset();
+                    clean();
+                    return fragment === result;
                 }
-                PrivateBin.Model.reset();
-                clean();
-                return result;
-            }
-        );
-        jsc.property(
-            'returns the fragment stripped of trailing query parts',
-            common.jscUrl(),
-            jsc.array(common.jscHashString()),
-            function (url, trail) {
-                const fragment = url.fragment.padStart(32, '\u0000');
-                url.fragment = PrivateBin.CryptTool.base58encode(fragment) + '&' + trail.join('');
-                const clean = globalThis.cleanup('', {url: common.urlToString(url)}),
-                    result = PrivateBin.Model.getPasteKey();
-                PrivateBin.Model.reset();
-                clean();
-                return fragment === result;
-            }
-        );
-        jsc.property(
-            'returns the fragment of a v2 URL',
-            common.jscUrl(),
-            function (url) {
-                // base58 strips leading NULL bytes, so the string is padded with these if not found
-                const fragment = url.fragment.padStart(32, '\u0000');
-                url.fragment = PrivateBin.CryptTool.base58encode(fragment);
-                const clean = globalThis.cleanup('', {url: common.urlToString(url)}),
-                    result = PrivateBin.Model.getPasteKey();
-                PrivateBin.Model.reset();
-                clean();
-                return fragment === result;
-            }
-        );
-        jsc.property(
-            'returns the v2 fragment stripped of trailing query parts',
-            common.jscUrl(),
-            jsc.array(common.jscHashString()),
-            function (url, trail) {
-                // base58 strips leading NULL bytes, so the string is padded with these if not found
-                const fragment = url.fragment.padStart(32, '\u0000');
-                url.fragment = PrivateBin.CryptTool.base58encode(fragment) + '&' + trail.join('');
-                const clean = globalThis.cleanup('', {url: common.urlToString(url)}),
-                    result = PrivateBin.Model.getPasteKey();
-                PrivateBin.Model.reset();
-                clean();
-                return fragment === result;
-            }
-        );
-        jsc.property(
-            'throws exception on empty fragment of the URL',
-            common.jscUrl(false),
-            function (url) {
-                let clean = globalThis.cleanup('', {url: common.urlToString(url)}),
-                    result = false;
-                try {
-                    PrivateBin.Model.getPasteKey();
+            ));
+        });
+        it('returns the fragment of a v2 URL', () => {
+            fc.assert(fc.property(
+                common.fcUrl(),
+                function (url) {
+                    // base58 strips leading NULL bytes, so the string is padded with these if not found
+                    const fragment = url.fragment.padStart(32, '\u0000');
+                    url.fragment = PrivateBin.CryptTool.base58encode(fragment);
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)}),
+                        result = PrivateBin.Model.getPasteKey();
+                    PrivateBin.Model.reset();
+                    clean();
+                    return fragment === result;
                 }
-                catch(err) {
-                    result = true;
+            ));
+        });
+        it('returns the v2 fragment stripped of trailing query parts', () => {
+            fc.assert(fc.property(
+                common.fcUrl(),
+                fc.array(common.fcHashString()),
+                function (url, trail) {
+                    // base58 strips leading NULL bytes, so the string is padded with these if not found
+                    const fragment = url.fragment.padStart(32, '\u0000');
+                    url.fragment = PrivateBin.CryptTool.base58encode(fragment) + '&' + trail.join('');
+                    const clean = globalThis.cleanup('', {url: common.urlToString(url)}),
+                        result = PrivateBin.Model.getPasteKey();
+                    PrivateBin.Model.reset();
+                    clean();
+                    return fragment === result;
+                }
+            ));
+        });
+        it('throws exception on empty fragment of the URL', () => {
+            fc.assert(fc.property(
+                common.fcUrl(false),
+                function (url) {
+                    let clean = globalThis.cleanup('', {url: common.urlToString(url)}),
+                        result = false;
+                    try {
+                        PrivateBin.Model.getPasteKey();
+                    }
+                    catch(err) {
+                        result = true;
+                    }
+                    PrivateBin.Model.reset();
+                    clean();
+                    return result;
                 }
-                PrivateBin.Model.reset();
-                clean();
-                return result;
-            }
-        );
+            ));
+        });
     });
 
     describe('getTemplate', function () {
@@ -210,37 +220,38 @@ describe('Model', function () {
             PrivateBin.Model.reset();
         });
 
-        jsc.property(
-            'returns the contents of the element with id "[name]template"',
-            jsc.nearray(common.jscAlnumString()),
-            jsc.nearray(common.jscA2zString()),
-            jsc.nearray(common.jscAlnumString()),
-            function (id, element, value) {
-                id = id.join('');
-                element = element.join('');
-                value = value.join('').trim();
+        it('returns the contents of the element with id "[name]template"', () => {
+            fc.assert(fc.property(
+                fc.array(common.fcAlnumString(), {minLength: 1}),
+                fc.array(common.fcA2zString(), {minLength: 1}),
+                fc.array(common.fcAlnumString(), {minLength: 1}),
+                function (id, element, value) {
+                    id = id.join('');
+                    element = element.join('');
+                    value = value.join('').trim();
 
-                // <br>, <hr>, <img> and <wbr> tags can't contain strings,
-                // table tags can't be alone, so test with a <p> instead
-                if (['br', 'col', 'hr', 'img', 'tr', 'td', 'th', 'wbr'].indexOf(element) >= 0) {
-                    element = 'p';
-                }
+                    // <br>, <hr>, <img> and <wbr> tags can't contain strings,
+                    // table tags can't be alone, so test with a <p> instead
+                    if (['br', 'col', 'hr', 'img', 'tr', 'td', 'th', 'wbr'].indexOf(element) >= 0) {
+                        element = 'p';
+                    }
 
-                document.body.innerHTML = (
-                    '<div id="templates"><' + element + ' id="' + id +
-                    'template">' + value + '</' + element + '></div>'
-                );
-                PrivateBin.Model.init();
-                var template = '<' + element + ' id="' + id + '">' + value +
-                    '</' + element + '>',
-                    templateEl = PrivateBin.Model.getTemplate(id),
-                    wrapper = document.createElement('p');
-                wrapper.appendChild(templateEl.cloneNode(true));
-                var result = wrapper.innerHTML;
-                PrivateBin.Model.reset();
-                return template === result;
-            }
-        );
+                    document.body.innerHTML = (
+                        '<div id="templates"><' + element + ' id="' + id +
+                        'template">' + value + '</' + element + '></div>'
+                    );
+                    PrivateBin.Model.init();
+                    var template = '<' + element + ' id="' + id + '">' + value +
+                        '</' + element + '>',
+                        templateEl = PrivateBin.Model.getTemplate(id),
+                        wrapper = document.createElement('p');
+                    wrapper.appendChild(templateEl.cloneNode(true));
+                    var result = wrapper.innerHTML;
+                    PrivateBin.Model.reset();
+                    return template === result;
+                }
+            ));
+        });
     });
 });
 

+ 155 - 148
js/test/PasteStatus.js

@@ -1,5 +1,6 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 function urlStrings(schema, longUrl, shortUrl) {
     longUrl.schema = schema;
@@ -35,18 +36,18 @@ describe('PasteStatus', function () {
             assert.ok(!document.getElementById('pastesuccess').classList.contains('hidden'));
         });
 
-        jsc.property(
-            'creates a notification after a successful document upload (jsc)',
-            common.jscUrl(),
-            common.jscUrl(false),
-            function (url1, url2) {
+        it('creates a notification after a successful document upload (jsc)', () => {
+            fc.assert(fc.asyncProperty(
+                common.fcUrl(),
+                common.fcUrl(false),
+                async function (url1, url2) {
                     // sometimes the generator returns incomplete objects, bail out
                     if (!url1 || !url1.address || !url2 || !url2.address) {
                         return true;
                     }
                     const expected1 = common.urlToString(url1).replace(/&(gt|lt)$/, '&$1a'),
                         expected2 = common.urlToString(url2).replace(/&(gt|lt)$/, '&$1a');
-                    cleanup();
+                    globalThis.cleanup();
                     document.body.innerHTML = '<a href="#" id="deletelink"><span></span></a><div id="pastelink"></div><div id="pastesuccess"></div>';
                     PrivateBin.PasteStatus.init();
                     PrivateBin.PasteStatus.createPasteNotification(expected1, expected2);
@@ -55,175 +56,181 @@ describe('PasteStatus', function () {
 
                     const result2 = document.getElementById('deletelink').href;
                     return document.getElementById('pasteurl').href === expected1 && result2 === expected2;
-            }
-        );
+                }
+            ));
+        });
     });
 
     describe('extractUrl', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'extracts and updates IDN URLs found in given response',
-            common.jscSchemas(false),
-            'nestring',
-            common.jscUrl(),
-            function (schema, domain, url) {
-                domain = domain.replace(/\P{Letter}|[\u{AA}-\u{BA}]/gu, '').toLowerCase();
-                if (domain.length === 0) {
-                    domain = 'a';
-                }
-                url.schema = schema;
-                url.address.unshift('.');
-                url.address = domain.split('').concat(url.address);
-                const urlString = common.urlToString(url),
-                    expected = urlString.substring((schema + '://' + domain).length),
-                    clean = globalThis.cleanup();
-
-                document.body.innerHTML = '<div><div id="pastelink"></div></div>';
-                PrivateBin.PasteStatus.init();
-                PrivateBin.PasteStatus.createPasteNotification('', '');
-                PrivateBin.PasteStatus.extractUrl(urlString);
+        it('extracts and updates IDN URLs found in given response', () => {
+            fc.assert(fc.property(
+                common.fcSchemas(false),
+                fc.string({minLength: 1}),
+                common.fcUrl(),
+                function (schema, domain, url) {
+                    domain = domain.replace(/\P{Letter}|[\u{AA}-\u{BA}]/gu, '').toLowerCase();
+                    if (domain.length === 0) {
+                        domain = 'a';
+                    }
+                    url.schema = schema;
+                    url.address.unshift('.');
+                    url.address = domain.split('').concat(url.address);
+                    const urlString = common.urlToString(url),
+                        expected = urlString.substring((schema + '://' + domain).length),
+                        clean = globalThis.cleanup();
+
+                    document.body.innerHTML = '<div><div id="pastelink"></div></div>';
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.createPasteNotification('', '');
+                    PrivateBin.PasteStatus.extractUrl(urlString);
 
-                const result = document.getElementById('pasteurl').href;
-                clean();
+                    const result = document.getElementById('pasteurl').href;
+                    clean();
 
-                return result.endsWith(expected) && (
-                    result.startsWith(schema + '://xn--') ||
-                    result.startsWith(schema + '://' + domain)
-                );
-            }
-        );
+                    return result.endsWith(expected) && (
+                        result.startsWith(schema + '://xn--') ||
+                        result.startsWith(schema + '://' + domain)
+                    );
+                }
+            ));
+        });
 
         // YOURLS API samples from: https://yourls.org/readme.html#API;apireturn
-        jsc.property(
-            'extracts and updates URLs found in YOURLS API JSON response',
-            common.jscSchemas(false),
-            common.jscUrl(),
-            common.jscUrl(false),
-            function (schema, longUrl, shortUrl) {
-                const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
-                    yourlsResponse = {
-                        url: {
-                            keyword: longUrl.address.join(''),
-                            url: longUrlString,
+        it('extracts and updates URLs found in YOURLS API JSON response', () => {
+            fc.assert(fc.property(
+                common.fcSchemas(false),
+                common.fcUrl(),
+                common.fcUrl(false),
+                function (schema, longUrl, shortUrl) {
+                    const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
+                        yourlsResponse = {
+                            url: {
+                                keyword: longUrl.address.join(''),
+                                url: longUrlString,
+                                title: 'example title',
+                                date: '2014-10-24 16:01:39',
+                                ip: '127.0.0.1'
+                            },
+                            status: 'success',
+                            message: longUrlString + ' added to database',
                             title: 'example title',
-                            date: '2014-10-24 16:01:39',
-                            ip: '127.0.0.1'
+                            shorturl: shortUrlString,
+                            statusCode: 200
                         },
-                        status: 'success',
-                        message: longUrlString + ' added to database',
-                        title: 'example title',
-                        shorturl: shortUrlString,
-                        statusCode: 200
-                    },
-                    clean = globalThis.cleanup();
-
-                document.body.innerHTML = '<div><div id="pastelink"></div></div>';
-                PrivateBin.PasteStatus.init();
-                PrivateBin.PasteStatus.createPasteNotification('', '');
-                PrivateBin.PasteStatus.extractUrl(JSON.stringify(yourlsResponse, undefined, 4));
+                        clean = globalThis.cleanup();
 
-                const result = document.getElementById('pasteurl').href;
-                clean();
+                    document.body.innerHTML = '<div><div id="pastelink"></div></div>';
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.createPasteNotification('', '');
+                    PrivateBin.PasteStatus.extractUrl(JSON.stringify(yourlsResponse, undefined, 4));
 
-                return result === shortUrlString;
-            }
-        );
-        jsc.property(
-            'extracts and updates URLs found in YOURLS API XML response',
-            common.jscSchemas(false),
-            common.jscUrl(),
-            common.jscUrl(false),
-            function (schema, longUrl, shortUrl) {
-                const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
-                    yourlsResponse = '<result>\n' +
-                        '    <keyword>' + longUrl.address.join('') + '</keyword>\n' +
-                        '    <shorturl>' + shortUrlString + '</shorturl>\n' +
-                        '    <longurl>' + longUrlString + '</longurl>\n' +
-                        '    <message>success</message>\n' +
-                        '    <statusCode>200</statusCode>\n' +
+                    const result = document.getElementById('pasteurl').href;
+                    clean();
+
+                    return result === shortUrlString;
+                }
+            ));
+        });
+        it('extracts and updates URLs found in YOURLS API XML response', () => {
+            fc.assert(fc.property(
+                common.fcSchemas(false),
+                common.fcUrl(),
+                common.fcUrl(false),
+                function (schema, longUrl, shortUrl) {
+                    const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
+                        yourlsResponse = '<result>\n' +
+                            '    <keyword>' + longUrl.address.join('') + '</keyword>\n' +
+                            '    <shorturl>' + shortUrlString + '</shorturl>\n' +
+                            '    <longurl>' + longUrlString + '</longurl>\n' +
+                            '    <message>success</message>\n' +
+                            '    <statusCode>200</statusCode>\n' +
                         '</result>',
-                    clean = globalThis.cleanup();
+                        clean = globalThis.cleanup();
 
-                document.body.innerHTML = '<div><div id="pastelink"></div></div>';
-                PrivateBin.PasteStatus.init();
-                PrivateBin.PasteStatus.createPasteNotification('', '');
-                PrivateBin.PasteStatus.extractUrl(yourlsResponse);
+                    document.body.innerHTML = '<div><div id="pastelink"></div></div>';
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.createPasteNotification('', '');
+                    PrivateBin.PasteStatus.extractUrl(yourlsResponse);
 
-                const result = document.getElementById('pasteurl').href;
-                clean();
+                    const result = document.getElementById('pasteurl').href;
+                    clean();
 
-                return result === shortUrlString;
-            }
-        );
-        jsc.property(
-            'extracts and updates URLs found in YOURLS proxy HTML response',
-            common.jscSchemas(false),
-            common.jscUrl(),
-            common.jscUrl(false),
-            function (schema, longUrl, shortUrl) {
-                const [_, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
-                    yourlsResponse = '<!DOCTYPE html>\n' +
-                        '<html lang="en">\n' +
-                        '\t<head>\n' +
-                        '\t\t<meta charset="utf-8" />\n' +
-                        '\t\t<meta name="robots" content="noindex" />\n' +
-                        '\t\t<meta name="google" content="notranslate">\n' +
-                        '\t\t<title>PrivateBin</title>\n' +
-                        '\t</head>\n' +
-                        '\t<body>\n' +
-                        '\t\t<p>Your document is <a id="pasteurl" href="' + shortUrlString + '">' + shortUrlString + '</a> <span id="copyhint">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span></p>\n' +
-                        '\t</body>\n' +
+                    return result === shortUrlString;
+                }
+            ));
+        });
+        it('extracts and updates URLs found in YOURLS proxy HTML response', () => {
+            fc.assert(fc.property(
+                common.fcSchemas(false),
+                common.fcUrl(),
+                common.fcUrl(false),
+                function (schema, longUrl, shortUrl) {
+                    const [_, shortUrlString] = urlStrings(schema, longUrl, shortUrl),
+                        yourlsResponse = '<!DOCTYPE html>\n' +
+                            '<html lang="en">\n' +
+                            '\t<head>\n' +
+                            '\t\t<meta charset="utf-8" />\n' +
+                            '\t\t<meta name="robots" content="noindex" />\n' +
+                            '\t\t<meta name="google" content="notranslate">\n' +
+                            '\t\t<title>PrivateBin</title>\n' +
+                            '\t</head>\n' +
+                            '\t<body>\n' +
+                            '\t\t<p>Your document is <a id="pasteurl" href="' + shortUrlString + '">' + shortUrlString + '</a> <span id="copyhint">(Hit <kbd>Ctrl</kbd>+<kbd>c</kbd> to copy)</span></p>\n' +
+                            '\t</body>\n' +
                         '</html>',
-                    clean = globalThis.cleanup();
+                        clean = globalThis.cleanup();
 
-                document.body.innerHTML = '<div><div id="pastelink"></div></div>';
-                PrivateBin.PasteStatus.init();
-                PrivateBin.PasteStatus.createPasteNotification('', '');
-                PrivateBin.PasteStatus.extractUrl(yourlsResponse);
+                    document.body.innerHTML = '<div><div id="pastelink"></div></div>';
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.createPasteNotification('', '');
+                    PrivateBin.PasteStatus.extractUrl(yourlsResponse);
 
-                const result = document.getElementById('pasteurl').href;
-                clean();
+                    const result = document.getElementById('pasteurl').href;
+                    clean();
 
-                return result === shortUrlString;
-            }
-        );
+                    return result === shortUrlString;
+                }
+            ));
+        });
     });
 
     describe('showRemainingTime', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'shows burn after reading message or remaining time',
-            'bool',
-            'nat',
-            common.jscUrl(),
-            function (burnafterreading, remainingTime, url) {
-                let clean = globalThis.cleanup('', {url: common.urlToString(url)}),
-                    result;
-                document.body.innerHTML = '<div id="remainingtime" class="hidden"></div>';
-                PrivateBin.PasteStatus.init();
-                PrivateBin.PasteStatus.showRemainingTime(PrivateBin.Helper.PasteFactory({
-                    'adata': [null, null, null, burnafterreading],
-                    'v': 2,
-                    'meta': {
-                        'time_to_live': remainingTime
+        it('shows burn after reading message or remaining time', () => {
+            fc.assert(fc.property(
+                fc.boolean(),
+                fc.nat(),
+                common.fcUrl(),
+                function (burnafterreading, remainingTime, url) {
+                    let clean = globalThis.cleanup('', {url: common.urlToString(url)}),
+                        result;
+                    document.body.innerHTML = '<div id="remainingtime" class="hidden"></div>';
+                    PrivateBin.PasteStatus.init();
+                    PrivateBin.PasteStatus.showRemainingTime(PrivateBin.Helper.PasteFactory({
+                        'adata': [null, null, null, burnafterreading],
+                        'v': 2,
+                        'meta': {
+                            'time_to_live': remainingTime
+                        }
+                    }));
+                    if (burnafterreading) {
+                        result = document.getElementById('remainingtime').classList.contains('foryoureyesonly') &&
+                                !document.getElementById('remainingtime').classList.contains('hidden');
+                    } else if (remainingTime) {
+                        result =!document.getElementById('remainingtime').classList.contains('foryoureyesonly') &&
+                                !document.getElementById('remainingtime').classList.contains('hidden');
+                    } else {
+                        result = document.getElementById('remainingtime').classList.contains('hidden') &&
+                                !document.getElementById('remainingtime').classList.contains('foryoureyesonly');
                     }
-                }));
-                if (burnafterreading) {
-                    result = document.getElementById('remainingtime').classList.contains('foryoureyesonly') &&
-                            !document.getElementById('remainingtime').classList.contains('hidden');
-                } else if (remainingTime) {
-                    result =!document.getElementById('remainingtime').classList.contains('foryoureyesonly') &&
-                            !document.getElementById('remainingtime').classList.contains('hidden');
-                } else {
-                    result = document.getElementById('remainingtime').classList.contains('hidden') &&
-                            !document.getElementById('remainingtime').classList.contains('foryoureyesonly');
+                    clean();
+                    return result;
                 }
-                clean();
-                return result;
-            }
-        );
+            ));
+        });
     });
 
     describe('hideMessages', function () {

+ 114 - 112
js/test/PasteViewer.js

@@ -1,5 +1,6 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('PasteViewer', function () {
     describe('run, hide, getText, setText, getFormat, setFormat & isPrettyPrinted', function () {
@@ -35,125 +36,126 @@ describe('PasteViewer', function () {
             assert.ok(!document.getElementById('plaintext').classList.contains('hidden'));
         });
 
-        jsc.property(
-            'initializes with empty text and shows nothing',
-            common.jscFormats(),
-            function (format) {
-                var results = [];
-                PrivateBin.PasteViewer.init();
-                PrivateBin.PasteViewer.setFormat(format);
-                PrivateBin.PasteViewer.setText('');
-                results.push(
-                    document.getElementById('placeholder').classList.contains('hidden') &&
-                    document.getElementById('prettymessage').classList.contains('hidden') &&
-                    document.getElementById('plaintext').classList.contains('hidden') &&
-                    PrivateBin.PasteViewer.getFormat() === format &&
-                    PrivateBin.PasteViewer.getText() === ''
-                );
-                return results.every(element => element);
-            }
-        );
-
-        jsc.property(
-            'when no text is given and view is rendered, it shows placeholder',
-            common.jscFormats(),
-            function (format) {
-                var results = [];
-                PrivateBin.PasteViewer.init();
-                PrivateBin.PasteViewer.setFormat(format);
-                PrivateBin.PasteViewer.setText('');
-                PrivateBin.PasteViewer.run();
-                results.push(
-                    !document.getElementById('placeholder').classList.contains('hidden') &&
-                    document.getElementById('prettymessage').classList.contains('hidden') &&
-                    document.getElementById('plaintext').classList.contains('hidden')
-                );
-                return results.every(element => element);
-            }
-        );
-
-        jsc.property(
-            'displays text according to format',
-            common.jscFormats(),
-            'nestring',
-            function (format, text) {
-                var results = [];
-                PrivateBin.PasteViewer.init();
-                PrivateBin.PasteViewer.setFormat(format);
-                PrivateBin.PasteViewer.setText(text);
-                PrivateBin.PasteViewer.run();
-                results.push(
-                    document.getElementById('placeholder').classList.contains('hidden') &&
-                    !PrivateBin.PasteViewer.isPrettyPrinted() &&
-                    PrivateBin.PasteViewer.getText() === text
-                );
-                if (format === 'markdown') {
+        it('initializes with empty text and shows nothing', () => {
+            fc.assert(fc.property(
+                common.fcFormats(),
+                function (format) {
+                    var results = [];
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText('');
                     results.push(
+                        document.getElementById('placeholder').classList.contains('hidden') &&
                         document.getElementById('prettymessage').classList.contains('hidden') &&
-                        !document.getElementById('plaintext').classList.contains('hidden')
+                        document.getElementById('plaintext').classList.contains('hidden') &&
+                        PrivateBin.PasteViewer.getFormat() === format &&
+                        PrivateBin.PasteViewer.getText() === ''
                     );
-                    console.log(document.getElementById('prettymessage').classList.contains('hidden'),
-                    !document.getElementById('plaintext').classList.contains('hidden'));
-                } else {
+                    return results.every(element => element);
+                }
+            ));
+        });
+
+        it('when no text is given and view is rendered, it shows placeholder', () => {
+            fc.assert(fc.property(
+                common.fcFormats(),
+                function (format) {
+                    var results = [];
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText('');
+                    PrivateBin.PasteViewer.run();
                     results.push(
-                        !document.getElementById('prettymessage').classList.contains('hidden') &&
+                        !document.getElementById('placeholder').classList.contains('hidden') &&
+                        document.getElementById('prettymessage').classList.contains('hidden') &&
                         document.getElementById('plaintext').classList.contains('hidden')
                     );
+                    return results.every(element => element);
                 }
+            ));
+        });
 
-                return results.every(element => element);
-            }
-        );
+        it('displays text according to format', () => {
+            fc.assert(fc.property(
+                common.fcFormats(),
+                fc.string({minLength: 1}),
+                function (format, text) {
+                    var results = [];
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText(text);
+                    PrivateBin.PasteViewer.run();
+                    results.push(
+                        document.getElementById('placeholder').classList.contains('hidden') &&
+                        !PrivateBin.PasteViewer.isPrettyPrinted() &&
+                        PrivateBin.PasteViewer.getText() === text
+                    );
+                    if (format === 'markdown') {
+                        results.push(
+                            document.getElementById('prettymessage').classList.contains('hidden') &&
+                            !document.getElementById('plaintext').classList.contains('hidden')
+                        );
+                    } else {
+                        results.push(
+                            !document.getElementById('prettymessage').classList.contains('hidden') &&
+                            document.getElementById('plaintext').classList.contains('hidden')
+                        );
+                    }
+
+                    return results.every(element => element);
+                }
+            ));
+        });
 
-        jsc.property(
-            'sanitizes XSS',
-            common.jscFormats(),
-            'string',
-            // @see    {@link https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet}
-            jsc.elements([
-                '<PLAINTEXT>',
-                '></SCRIPT>">\'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>',
-                '\'\';!--"<XSS>=&{()}',
-                '<SCRIPT SRC=http://example.com/xss.js></SCRIPT>',
-                '\'">><marquee><img src=x onerror=confirm(1)></marquee>">' +
-                '</plaintext\\></|\\><plaintext/onmouseover=prompt(1)>' +
-                '<script>prompt(1)</script>@gmail.com<isindex formaction=java' +
-                // obfuscate script URL from eslint rule: no-script-url
-                'script:alert(/XSS/) type=submit>\'-->"></script>' +
-                '<script>alert(document.cookie)</script>"><img/id="confirm' +
-                '&lpar;1)"/alt="/"src="/"onerror=eval(id)>\'">',
-                '<IMG SRC="javascript:alert(\'XSS\');">',
-                '<IMG SRC=javascript:alert(\'XSS\')>',
-                '<IMG SRC=JaVaScRiPt:alert(\'XSS\')>',
-                '<IMG SRC=javascript:alert(&quot;XSS&quot;)>',
-                '<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>',
-                '<a onmouseover="alert(document.cookie)">xxs link</a>',
-                '<a onmouseover=alert(document.cookie)>xxs link</a>',
-                '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
-                '<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>',
-                '<IMG STYLE="xss:expr/*XSS*/ession(alert(\'XSS\'))">',
-                '<FRAMESET><FRAME SRC="javascript:alert(\'XSS\');"></FRAMESET>',
-                '<TABLE BACKGROUND="javascript:alert(\'XSS\')">',
-                '<TABLE><TD BACKGROUND="javascript:alert(\'XSS\')">',
-                '<SCRIPT>document.write("<SCRI");</SCRIPT>PT SRC="httx://xss.rocks/xss.js"></SCRIPT>'
-            ]),
-            'string',
-            function (format, prefix, xss, suffix) {
-                var text = prefix + xss + suffix;
-                document.body.innerHTML = (
-                    '<div id="placeholder" class="hidden">+++ no document text ' +
-                    '+++</div><div id="prettymessage" class="hidden"><pre ' +
-                    'id="prettyprint" class="prettyprint linenums:1"></pre>' +
-                    '</div><div id="plaintext" class="hidden"></div>'
-                );
-                PrivateBin.PasteViewer.init();
-                PrivateBin.PasteViewer.setFormat(format);
-                PrivateBin.PasteViewer.setText(text);
-                PrivateBin.PasteViewer.run();
-                var result = document.body.innerHTML.indexOf(xss) === -1;
-                cleanup();
-                return result;
-            }
-        );
+        it('sanitizes XSS', () => {
+            fc.assert(fc.property(
+                common.fcFormats(),
+                fc.string(),
+                fc.constantFrom(
+                    '<PLAINTEXT>',
+                    '></SCRIPT>">\'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>',
+                    '\'\';!--"<XSS>=&{()}',
+                    '<SCRIPT SRC=http://example.com/xss.js></SCRIPT>',
+                    '\'">><marquee><img src=x onerror=confirm(1)></marquee>">' +
+                    '</plaintext\\></|\\><plaintext/onmouseover=prompt(1)>' +
+                    '<script>prompt(1)</script>@gmail.com<isindex formaction=java' +
+                    // obfuscate script URL from eslint rule: no-script-url
+                    'script:alert(/XSS/) type=submit>\'-->"></script>' +
+                    '<script>alert(document.cookie)</script>"><img/id="confirm' +
+                    '&lpar;1)"/alt="/"src="/"onerror=eval(id)>\'">',
+                    '<IMG SRC="javascript:alert(\'XSS\');">',
+                    '<IMG SRC=javascript:alert(\'XSS\')>',
+                    '<IMG SRC=JaVaScRiPt:alert(\'XSS\')>',
+                    '<IMG SRC=javascript:alert(&quot;XSS&quot;)>',
+                    '<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>',
+                    '<a onmouseover="alert(document.cookie)">xxs link</a>',
+                    '<a onmouseover=alert(document.cookie)>xxs link</a>',
+                    '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
+                    '<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>',
+                    '<IMG STYLE="xss:expr/*XSS*/ession(alert(\'XSS\'))">',
+                    '<FRAMESET><FRAME SRC="javascript:alert(\'XSS\');"></FRAMESET>',
+                    '<TABLE BACKGROUND="javascript:alert(\'XSS\')">',
+                    '<TABLE><TD BACKGROUND="javascript:alert(\'XSS\')">',
+                    '<SCRIPT>document.write("<SCRI");</SCRIPT>PT SRC="httx://xss.rocks/xss.js"></SCRIPT>'
+                ),
+                fc.string(),
+                function (format, prefix, xss, suffix) {
+                    var text = prefix + xss + suffix;
+                    document.body.innerHTML = (
+                        '<div id="placeholder" class="hidden">+++ no document text ' +
+                        '+++</div><div id="prettymessage" class="hidden"><pre ' +
+                        'id="prettyprint" class="prettyprint linenums:1"></pre>' +
+                        '</div><div id="plaintext" class="hidden"></div>'
+                    );
+                    PrivateBin.PasteViewer.init();
+                    PrivateBin.PasteViewer.setFormat(format);
+                    PrivateBin.PasteViewer.setText(text);
+                    PrivateBin.PasteViewer.run();
+                    var result = document.body.innerHTML.indexOf(xss) === -1;
+                    globalThis.cleanup();
+                    return result;
+                }
+            ));
+        });
     });
 });

+ 24 - 22
js/test/Prompt.js

@@ -1,31 +1,33 @@
 'use strict';
 require('../common');
+const fc = require('fast-check');
 
 describe('Prompt', function () {
     describe('requestPassword & getPassword', function () {
         this.timeout(30000);
 
-        jsc.property(
-            'returns the password fed into the dialog',
-            'string',
-            function (password) {
-                password = password.replace(/\r+|\n+/g, '');
-                const clean = globalThis.cleanup('', {url: 'ftp://example.com/?0000000000000000'});
-                document.body.innerHTML = (
-                    '<div id="passwordmodal" class="modal fade" role="dialog">' +
-                    '<div class="modal-dialog"><div class="modal-content">' +
-                    '<div class="modal-body"><form id="passwordform" role="form">' +
-                    '<div class="form-group"><input id="passworddecrypt" ' +
-                    'type="password" class="form-control" placeholder="Enter ' +
-                    'password"></div><button type="submit">Decrypt</button>' +
-                    '</form></div></div></div></div>'
-                );
-                const passwordInput = document.getElementById('passworddecrypt');
-                passwordInput.value = password;
-                const result = passwordInput.value;
-                clean();
-                return result === password;
-            }
-        );
+        it('returns the password fed into the dialog', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (password) {
+                    password = password.replace(/\r+|\n+/g, '');
+                    const clean = globalThis.cleanup('', {url: 'ftp://example.com/?0000000000000000'});
+                    document.body.innerHTML = (
+                        '<div id="passwordmodal" class="modal fade" role="dialog">' +
+                        '<div class="modal-dialog"><div class="modal-content">' +
+                        '<div class="modal-body"><form id="passwordform" role="form">' +
+                        '<div class="form-group"><input id="passworddecrypt" ' +
+                        'type="password" class="form-control" placeholder="Enter ' +
+                        'password"></div><button type="submit">Decrypt</button>' +
+                        '</form></div></div></div></div>'
+                    );
+                    const passwordInput = document.getElementById('passworddecrypt');
+                    passwordInput.value = password;
+                    const result = passwordInput.value;
+                    clean();
+                    return result === password;
+                }
+            ));
+        });
     });
 });

+ 33 - 31
js/test/TopNav.js

@@ -1,5 +1,6 @@
 'use strict';
 require('../common');
+const fc = require('fast-check');
 
 function query(selector) {
     if (selector.startsWith('#')) {
@@ -659,40 +660,41 @@ describe('TopNav', function () {
 
     describe('getPassword', function () {
         before(function () {
-            cleanup();
+            globalThis.cleanup();
         });
 
-        jsc.property(
-            'returns the contents of the password input',
-            'string',
-            function (password) {
-                password = password.replace(/\r+|\n+/g, '');
-                let results = [];
-                document.documentElement.innerHTML =
-                    '<nav><div id="navbar"><ul><li><div id="password" ' +
-                    'class="navbar-form hidden"><input type="password" ' +
-                    'id="passwordinput" placeholder="Password (recommended)" ' +
-                    'class="form-control" size="23" /></div></li></ul></div></nav>';
-                PrivateBin.TopNav.init();
-                results.push(
-                    PrivateBin.TopNav.getPassword() === ''
-                );
-                query('#passwordinput').value = password;
-                results.push(
-                    PrivateBin.TopNav.getPassword() === password
-                );
-                query('#passwordinput').value = '';
-                results.push(
-                    PrivateBin.TopNav.getPassword() === ''
-                );
-                cleanup();
-                const result = results.every(element => element);
-                if (!result) {
-                    console.log(results);
+        it('returns the contents of the password input', () => {
+            fc.assert(fc.property(
+                fc.string(),
+                function (password) {
+                    password = password.replace(/\r+|\n+/g, '');
+                    let results = [];
+                    document.documentElement.innerHTML =
+                        '<nav><div id="navbar"><ul><li><div id="password" ' +
+                        'class="navbar-form hidden"><input type="password" ' +
+                        'id="passwordinput" placeholder="Password (recommended)" ' +
+                        'class="form-control" size="23" /></div></li></ul></div></nav>';
+                    PrivateBin.TopNav.init();
+                    results.push(
+                        PrivateBin.TopNav.getPassword() === ''
+                    );
+                    query('#passwordinput').value = password;
+                    results.push(
+                        PrivateBin.TopNav.getPassword() === password
+                    );
+                    query('#passwordinput').value = '';
+                    results.push(
+                        PrivateBin.TopNav.getPassword() === ''
+                    );
+                    globalThis.cleanup();
+                    const result = results.every(element => element);
+                    if (!result) {
+                        console.log(results);
+                    }
+                    return result;
                 }
-                return result;
-            }
-        );
+            ));
+        });
     });
 
     describe('getCustomAttachment', function () {

+ 34 - 31
js/test/UiHelper.js

@@ -1,5 +1,6 @@
 'use strict';
-var common = require('../common');
+const common = require('../common');
+const fc = require('fast-check');
 
 describe('UiHelper', function () {
     // TODO: As per https://github.com/tmpvar/jsdom/issues/1565 there is no navigation support in jsdom, yet.
@@ -11,39 +12,41 @@ describe('UiHelper', function () {
             cleanup();
         });
 
-        jsc.property(
-            'redirects to home, when the state is null',
-            common.jscUrl(false, false),
-            function (url) {
-                const expected = common.urlToString(url),
-                    clean = globalThis.cleanup('', {url: expected});
+        it('redirects to home, when the state is null', () => {
+            fc.assert(fc.property(
+                common.fcUrl(false, false),
+                function (url) {
+                    const expected = common.urlToString(url),
+                        clean = globalThis.cleanup('', {url: expected});
 
-                PrivateBin.UiHelper.mockHistoryChange();
-                PrivateBin.Helper.reset();
-                var result = window.location.href;
-                clean();
-                return expected === result;
-            }
-        );
+                    PrivateBin.UiHelper.mockHistoryChange();
+                    PrivateBin.Helper.reset();
+                    var result = window.location.href;
+                    clean();
+                    return expected === result;
+                }
+            ));
+        });
 
-        jsc.property(
-            'does not redirect to home, when a new document is created',
-            common.jscUrl(false),
-            jsc.nearray(common.jscBase64String()),
-            function (url, fragment) {
-                url.fragment = fragment.join('');
-                const expected = common.urlToString(url),
-                    clean = globalThis.cleanup('', {url: expected});
+        it('does not redirect to home, when a new document is created', () => {
+            fc.assert(fc.property(
+                common.fcUrl(false),
+                fc.array(common.fcBase64String(), {minLength: 1}),
+                function (url, fragment) {
+                    url.fragment = fragment.join('');
+                    const expected = common.urlToString(url),
+                        clean = globalThis.cleanup('', {url: expected});
 
-                PrivateBin.UiHelper.mockHistoryChange([
-                    {type: 'newpaste'}, '', expected
-                ]);
-                PrivateBin.Helper.reset();
-                var result = window.location.href;
-                clean();
-                return expected === result;
-            }
-        );
+                    PrivateBin.UiHelper.mockHistoryChange([
+                        {type: 'newpaste'}, '', expected
+                    ]);
+                    PrivateBin.Helper.reset();
+                    var result = window.location.href;
+                    clean();
+                    return expected === result;
+                }
+            ));
+        });
     });
 
     describe('reloadHome', function () {