diff --git a/testing/wicket-js-tests/Gruntfile.js b/testing/wicket-js-tests/Gruntfile.js index b92ccc74f93..85eda6ed714 100644 --- a/testing/wicket-js-tests/Gruntfile.js +++ b/testing/wicket-js-tests/Gruntfile.js @@ -43,8 +43,7 @@ module.exports = function(grunt) { "../../wicket-core/src/test/js/dom.js", "../../wicket-core/src/test/js/channels.js", "../../wicket-core/src/test/js/event.js", - "../../wicket-core/src/test/js/timer.js", - "../../wicket-core/src/test/js/amd.js" + "../../wicket-core/src/test/js/timer.js" ], gymTestsJs = [ "../../wicket-examples/src/main/webapp/js-test/tests/ajax/form.js", @@ -107,19 +106,12 @@ module.exports = function(grunt) { all: { options: { urls: [ - 'http://localhost:38887/test/js/all.html?3.7.1' - ] - } - }, - - /** - * Run Asynchronous module definition tests - */ - amd: { - options: { - urls: [ - 'http://localhost:38887/test/js/amd.html?3.7.1' - ] + 'http://localhost:38887/test/js/all.html?4.0.0' + ], + puppeteer: { + headless: true, + args: ['--no-sandbox'] + } } } }, diff --git a/testing/wicket-js-tests/package-lock.json b/testing/wicket-js-tests/package-lock.json index 01634c023f2..c6cb59f4a3b 100644 --- a/testing/wicket-js-tests/package-lock.json +++ b/testing/wicket-js-tests/package-lock.json @@ -14,7 +14,7 @@ "grunt-contrib-connect": "5.0.1", "grunt-contrib-jshint": "3.2.0", "grunt-contrib-nodeunit": "5.0.0", - "grunt-contrib-qunit": "9.1.1" + "grunt-contrib-qunit": "10.2.0" } }, "node_modules/@ampproject/remapping": { @@ -610,19 +610,18 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", - "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", - "proxy-agent": "^6.4.0", - "semver": "^7.6.3", - "tar-fs": "^3.0.6", - "unbzip2-stream": "^1.4.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { @@ -633,9 +632,9 @@ } }, "node_modules/@puppeteer/browsers/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -658,9 +657,9 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -678,14 +677,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.17.tgz", - "integrity": "sha512-nAJuQXoyPj04uLgu+obZcSmsfOenUg6DxPKogeUy6yNCFwWaj5sBF8/G/pNo8EtBJjAfSVgfIlugR/BCOleO+g==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/yauzl": { @@ -719,9 +718,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -852,11 +851,19 @@ } }, "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } }, "node_modules/balanced-match": { "version": "1.0.2", @@ -865,24 +872,32 @@ "dev": true }, "node_modules/bare-events": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", - "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "dev": true, "license": "Apache-2.0", - "optional": true + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } }, "node_modules/bare-fs": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz", - "integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", - "bare-stream": "^2.6.4" + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" }, "engines": { "bare": ">=1.16.0" @@ -897,12 +912,11 @@ } }, "node_modules/bare-os": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", - "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz", + "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "bare": ">=1.14.0" } @@ -913,26 +927,29 @@ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz", + "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { - "streamx": "^2.21.0" + "streamx": "^2.25.0", + "teex": "^1.0.1" }, "peerDependencies": { + "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, "bare-buffer": { "optional": true }, @@ -941,26 +958,15 @@ } } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } }, "node_modules/basic-auth": { "version": "2.0.1", @@ -975,9 +981,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.2.tgz", - "integrity": "sha512-1tDrzKsdCg70WGvbFss/ulVAxupNauGnOlgpyjKzeQxzyllBLS0CGLV7tjIXTK3ZQA9/FBEm9qyFFN1bciA6pw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.0.tgz", + "integrity": "sha512-5K9eNNn7ywHPsYnFwjKgYH8Hf8B5emh7JKcPaVjjrMJFQQwGpwowEnZNEtHs7DfR7hCZsmaK3VA4HUK0YarT+w==", "dev": true, "license": "MIT", "engines": { @@ -1062,31 +1068,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -1123,6 +1104,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1200,15 +1182,14 @@ } }, "node_modules/chromium-bidi": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", - "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "mitt": "3.0.1", - "urlpattern-polyfill": "10.0.0", - "zod": "3.23.8" + "mitt": "^3.0.1", + "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" @@ -1341,10 +1322,11 @@ "dev": true }, "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, + "license": "MIT", "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -1370,7 +1352,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/cosmiconfig/node_modules/js-yaml": { "version": "4.1.1", @@ -1510,9 +1493,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1312386", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", - "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "version": "0.0.1595872", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz", + "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==", "dev": true, "license": "BSD-3-Clause" }, @@ -1624,9 +1607,9 @@ } }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", "dependencies": { @@ -1644,15 +1627,17 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } @@ -1763,6 +1748,16 @@ "integrity": "sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==", "dev": true }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -1812,9 +1807,9 @@ } }, "node_modules/extract-zip/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2137,9 +2132,9 @@ } }, "node_modules/get-uri": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", - "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", "dev": true, "license": "MIT", "dependencies": { @@ -2152,9 +2147,9 @@ } }, "node_modules/get-uri/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2368,13 +2363,14 @@ } }, "node_modules/grunt-contrib-qunit": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-qunit/-/grunt-contrib-qunit-9.1.1.tgz", - "integrity": "sha512-bzDpYgw3le3N6FuTvI28A5kkk1D14GC1wrj8kzykflAfjg+IYm9euoG9AwgixIYmBSdq3pqBNQbQAlOI8hW0wg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-qunit/-/grunt-contrib-qunit-10.2.0.tgz", + "integrity": "sha512-pOvHgMgDfwUFWvnd2FIksQ+jfEI7lZ1voabDiE5GtU+3Bi2Zuq70vRCSeiKSIbI2vZpVHK0zXpUKzFT4LbNIKw==", "dev": true, + "license": "MIT", "dependencies": { "eventemitter2": "^6.4.9", - "puppeteer": "^22.0.0" + "puppeteer": "^24.0.0" }, "engines": { "node": ">=18" @@ -2576,9 +2572,9 @@ } }, "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2628,9 +2624,9 @@ } }, "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2664,32 +2660,12 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2748,26 +2724,15 @@ "dev": true }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "dev": true, "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, "engines": { "node": ">= 12" } }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -2785,7 +2750,8 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -3191,13 +3157,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true, - "license": "MIT" - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -3232,7 +3191,8 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", @@ -3320,7 +3280,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/locate-path": { "version": "5.0.0", @@ -3521,9 +3482,9 @@ } }, "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", "dev": true, "license": "MIT", "engines": { @@ -3883,9 +3844,9 @@ } }, "node_modules/pac-proxy-agent/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -3941,6 +3902,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -3967,6 +3929,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -4156,9 +4119,9 @@ } }, "node_modules/proxy-agent/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -4188,9 +4151,9 @@ "license": "MIT" }, "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { @@ -4208,46 +4171,50 @@ } }, "node_modules/puppeteer": { - "version": "22.15.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.15.0.tgz", - "integrity": "sha512-XjCY1SiSEi1T7iSYuxS82ft85kwDJUS7wj1Z0eGVXKdtr5g4xnVcbjwxhq5xBnpK/E7x1VZZoJDxpjAOasHT4Q==", + "version": "24.41.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.41.0.tgz", + "integrity": "sha512-W6Fk0J3TPjjtwjXOyR/qf+YaL0H/Uq8HIgHcXG4mNM/IgbKMCH/HPyK0Fi2qbTU/QpSl9bCte2yBpGHKejTpIw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.3.0", + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1312386", - "puppeteer-core": "22.15.0" + "devtools-protocol": "0.0.1595872", + "puppeteer-core": "24.41.0", + "typed-query-selector": "^2.12.1" }, "bin": { - "puppeteer": "lib/esm/puppeteer/node/cli.js" + "puppeteer": "lib/cjs/puppeteer/node/cli.js" }, "engines": { "node": ">=18" } }, "node_modules/puppeteer-core": { - "version": "22.15.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", - "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "version": "24.41.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.41.0.tgz", + "integrity": "sha512-rLIUri7E/NQ3APSEYCCozaSJx0u8Tu9wxO6BJwnvXmIgILSK3L0TombaVh3izp1njAGrO6H2ru0hcIrLF+gWLw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.3.0", - "chromium-bidi": "0.6.3", - "debug": "^4.3.6", - "devtools-protocol": "0.0.1312386", - "ws": "^8.18.0" + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1595872", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" }, "engines": { "node": ">=18" } }, "node_modules/puppeteer-core/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -4394,6 +4361,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4600,13 +4568,13 @@ } }, "node_modules/socks": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", - "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -4630,9 +4598,9 @@ } }, "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -4718,17 +4686,15 @@ } }, "node_modules/streamx": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", - "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", "dev": true, "license": "MIT", "dependencies": { + "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -6850,9 +6816,9 @@ } }, "node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", "dev": true, "license": "MIT", "dependencies": { @@ -6865,13 +6831,14 @@ } }, "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", + "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } @@ -6888,6 +6855,16 @@ "node": ">=10" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6903,22 +6880,15 @@ } }, "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, - "license": "MIT" - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -6974,6 +6944,13 @@ "node": ">=8" } }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "dev": true, + "license": "MIT" + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -6983,17 +6960,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -7023,9 +6989,9 @@ "dev": true }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, "license": "MIT", "optional": true @@ -7078,13 +7044,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/urlpattern-polyfill": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", - "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", - "dev": true, - "license": "MIT" - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7118,6 +7077,13 @@ "node": ">= 10.13.0" } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7175,9 +7141,9 @@ } }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "dev": true, "license": "MIT", "engines": { @@ -7277,9 +7243,9 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", "funding": { diff --git a/testing/wicket-js-tests/package.json b/testing/wicket-js-tests/package.json index 462c287471c..5ceb981b47a 100644 --- a/testing/wicket-js-tests/package.json +++ b/testing/wicket-js-tests/package.json @@ -13,7 +13,7 @@ "grunt-contrib-connect": "5.0.1", "grunt-contrib-jshint": "3.2.0", "grunt-contrib-nodeunit": "5.0.0", - "grunt-contrib-qunit": "9.1.1" + "grunt-contrib-qunit": "10.2.0" }, "scripts": { "test": "grunt --verbose" diff --git a/testing/wicket-js-tests/pom.xml b/testing/wicket-js-tests/pom.xml index 2a8df964b69..0cf3e2ecd95 100644 --- a/testing/wicket-js-tests/pom.xml +++ b/testing/wicket-js-tests/pom.xml @@ -1,4 +1,4 @@ - + - + 4.0.0 org.apache.wicket @@ -101,8 +105,8 @@ generate-resources - 9.6.1 - v18.15.0 + 11.12.1 + v24.15.0 diff --git a/testing/wicket-js-tests/src/test/java/org/apache/wicket/testing/jstest/StartJavaScriptTests.java b/testing/wicket-js-tests/src/test/java/org/apache/wicket/testing/jstest/StartJavaScriptTests.java index 3979abeba0d..d16011ab79b 100644 --- a/testing/wicket-js-tests/src/test/java/org/apache/wicket/testing/jstest/StartJavaScriptTests.java +++ b/testing/wicket-js-tests/src/test/java/org/apache/wicket/testing/jstest/StartJavaScriptTests.java @@ -38,7 +38,7 @@ * -Dcom.sun.management.jmxremote to startup JMX (and e.g. connect with jconsole). * * - * @see JavaScript tests + * @see JavaScript tests */ public class StartJavaScriptTests { @@ -126,16 +126,9 @@ public static void main(String[] args) } } - private static void browse() + private static void browse() throws Exception { - try - { - Desktop.getDesktop().browse(new URI("http://localhost:8080/ajax-tests/test/js/all.html?3.6.4")); - } - catch (Exception e) - { - System.out.println("can not open browser " + e); - } + Desktop.getDesktop().browse(new URI("http://localhost:8080/ajax-tests/test/js/all.html?4.0.0")); } /** diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/TestDetachPageAjaxResult.html b/wicket-core-tests/src/test/java/org/apache/wicket/TestDetachPageAjaxResult.html index 7f2804d79b6..52b0b6b6d62 100644 --- a/wicket-core-tests/src/test/java/org/apache/wicket/TestDetachPageAjaxResult.html +++ b/wicket-core-tests/src/test/java/org/apache/wicket/TestDetachPageAjaxResult.html @@ -1,4 +1,4 @@ -body]]> +body]]> + + - + diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxHeaderContributionPage2_expected.html b/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxHeaderContributionPage2_expected.html index 601944013f7..332619caf8c 100644 --- a/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxHeaderContributionPage2_expected.html +++ b/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxHeaderContributionPage2_expected.html @@ -2,7 +2,7 @@ - + diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxHeaderContributionPage_ajax_expected.html b/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxHeaderContributionPage_ajax_expected.html index 8e3a228b3b5..273b6363ed0 100644 --- a/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxHeaderContributionPage_ajax_expected.html +++ b/wicket-core-tests/src/test/java/org/apache/wicket/ajax/AjaxHeaderContributionPage_ajax_expected.html @@ -7,7 +7,7 @@ ]]> - + ]]> - + +Test]]> + + + + + + +1]]> + + + +]]> - + diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/basic/SimplePageExpectedResult_13.html b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/basic/SimplePageExpectedResult_13.html index 08f96dbc254..f824081f935 100644 --- a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/basic/SimplePageExpectedResult_13.html +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/basic/SimplePageExpectedResult_13.html @@ -2,7 +2,7 @@ - + - \ No newline at end of file + \ No newline at end of file diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/basic/SimplePage_13.html b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/basic/SimplePage_13.html index 606a3c5dab3..cc72acdf440 100644 --- a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/basic/SimplePage_13.html +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/basic/SimplePage_13.html @@ -4,4 +4,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/border/HideableBorderPage_ExpectedResult.html b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/border/HideableBorderPage_ExpectedResult.html index 0f0549f54ef..4f3703b3f3b 100644 --- a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/border/HideableBorderPage_ExpectedResult.html +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/border/HideableBorderPage_ExpectedResult.html @@ -1,5 +1,5 @@ - + + + \n" + "\n" + " + + + + diff --git a/wicket-core/src/test/js/amd.html b/wicket-core/src/test/js/amd.html deleted file mode 100644 index dfd314cffca..00000000000 --- a/wicket-core/src/test/js/amd.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - Asynchronous module tests - - - - - - - - - - -
- -
- -
-
- - diff --git a/wicket-core/src/test/js/amd.js b/wicket-core/src/test/js/amd.js deleted file mode 100644 index b036e9e2149..00000000000 --- a/wicket-core/src/test/js/amd.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/*global ok: true, start: true, test: true, equal: true, deepEqual: true, - QUnit: true, module: true, expect: true, define: true, console: true */ - -define( - [ - "jquery", - "Wicket", - "qunit" - ], - function ($, w, qunit) { - "use strict"; - - return { - runTests: function () { - qunit.test("Wicket object is successfully loaded via RequireJs", assert => { - assert.expect(13); - - // test for one member for each Wicket.** module - assert.ok($.isFunction(w.Event.fix), "Wicket Event (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(w.Ajax.ajax), "Wicket Ajax (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(w.DOM.get), "Wicket DOM (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(w.Form.serialize), "Wicket Form (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(new w.Channel("blah|s").done), "Wicket Channel (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(new w.ChannelManager().done), "Wicket ChannelManager (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(w.Class.create), "Wicket Class (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(w.Head.addElement), "Wicket Head (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(w.Focus.focusin), "Wicket Focus (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(w.Log.error), "Wicket Log (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(new w.Throttler().throttle), "Wicket Throttler (from wicket-ajax-jquery.js) is imported"); - assert.ok($.isFunction(new w.ThrottlerEntry().getFunc), "Wicket ThrottlerEntry (from wicket-ajax-jquery.js) is imported"); - - assert.equal('div', w.DOM.get('amdElement').tagName.toLowerCase(), "Wicket.DOM.get() works"); - }); - } - }; - } -); diff --git a/wicket-core/src/test/js/amd/require.js b/wicket-core/src/test/js/amd/require.js deleted file mode 100644 index 651902f2172..00000000000 --- a/wicket-core/src/test/js/amd/require.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - RequireJS 2.1.22 Copyright (c) 2010-2015, The Dojo Foundation All Rights Reserved. - Available via the MIT or new BSD license. - see: http://github.com/jrburke/requirejs for details -*/ -var requirejs,require,define; -(function(ha){function L(b){return"[object Function]"===R.call(b)}function M(b){return"[object Array]"===R.call(b)}function x(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(L(l)){try{f=h.execCb(c,l,b,f)}catch(d){a=d}this.map.isDefine&&void 0===f&&((b=this.module)?f=b.exports: -this.usingExports&&(f=this.exports));if(a){if(this.events.error&&this.map.isDefine||k.onError!==ia)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",B(this.error=a);if("undefined"!==typeof console&&console.error)console.error(a);else k.onError(a)}}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(r[c]=f,k.onResourceLoad)){var e=[];x(this.depMaps,function(a){e.push(a.normalizedMap||a)});k.onResourceLoad(h, -this.map,e)}D(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}},callPlugin:function(){var a=this.map,b=a.id,d=q(a.prefix);this.depMaps.push(d);v(d,"defined",y(this,function(f){var l,d,e=g(ga,this.map.id),N=this.map.name,p=this.map.parentMap?this.map.parentMap.name:null,r=h.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(N=f.normalize(N,function(a){return c(a, -p,!0)})||""),d=q(a.prefix+"!"+N,this.map.parentMap),v(d,"defined",y(this,function(a){this.map.normalizedMap=d;this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),f=g(t,d.id)){this.depMaps.push(d);if(this.events.error)f.on("error",y(this,function(a){this.emit("error",a)}));f.enable()}}else e?(this.map.url=h.nameToUrl(e),this.load()):(l=y(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=y(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b]; -E(t,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&D(a.map.id)});B(a)}),l.fromText=y(this,function(f,c){var d=a.name,e=q(d),N=T;c&&(f=c);N&&(T=!1);u(e);w(m.config,b)&&(m.config[d]=m.config[b]);try{k.exec(f)}catch(g){return B(G("fromtexteval","fromText eval for "+b+" failed: "+g,g,[b]))}N&&(T=!0);this.depMaps.push(e);h.completeLoad(d);r([d],l)}),f.load(a.name,r,l,m))}));h.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){aa[this.map.id]=this;this.enabling=this.enabled=!0;x(this.depMaps, -y(this,function(a,b){var c,f;if("string"===typeof a){a=q(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=g(S,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;v(a,"defined",y(this,function(a){this.undefed||(this.defineDep(b,a),this.check())}));this.errback?v(a,"error",y(this,this.errback)):this.events.error&&v(a,"error",y(this,function(a){this.emit("error",a)}))}c=a.id;f=t[c];w(S,c)||!f||f.enabled||h.enable(a,this)}));E(this.pluginMaps,y(this,function(a){var b= -g(t,a.id);b&&!b.enabled&&h.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){x(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};h={config:m,contextName:b,registry:t,defined:r,urlFetched:X,defQueue:H,defQueueMap:{},Module:ea,makeModuleMap:q,nextTick:k.nextTick,onError:B,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=m.shim,c={paths:!0, -bundles:!0,config:!0,map:!0};E(a,function(a,b){c[b]?(m[b]||(m[b]={}),Z(m[b],a,!0,!0)):m[b]=a});a.bundles&&E(a.bundles,function(a,b){x(a,function(a){a!==b&&(ga[a]=b)})});a.shim&&(E(a.shim,function(a,c){M(a)&&(a={deps:a});!a.exports&&!a.init||a.exportsFn||(a.exportsFn=h.makeShimExports(a));b[c]=a}),m.shim=b);a.packages&&x(a.packages,function(a){var b;a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(m.paths[b]=a.location);m.pkgs[b]=a.name+"/"+(a.main||"main").replace(na,"").replace(V,"")});E(t, -function(a,b){a.inited||a.map.unnormalized||(a.map=q(b,null,!0))});(a.deps||a.callback)&&h.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ha,arguments));return b||a.exports&&ja(a.exports)}},makeRequire:function(a,n){function e(c,d,g){var m,p;n.enableBuildCallback&&d&&L(d)&&(d.__requireJsBuild=!0);if("string"===typeof c){if(L(d))return B(G("requireargs","Invalid require call"),g);if(a&&w(S,c))return S[c](t[a.id]);if(k.get)return k.get(h, -c,a,e);m=q(c,a,!1,!0);m=m.id;return w(r,m)?r[m]:B(G("notloaded",'Module name "'+m+'" has not been loaded yet for context: '+b+(a?"":". Use require([])")))}Q();h.nextTick(function(){Q();p=u(q(null,a));p.skipMap=n.skipMap;p.init(c,d,g,{enabled:!0});I()});return e}n=n||{};Z(e,{isBrowser:F,toUrl:function(b){var d,e=b.lastIndexOf("."),n=b.split("/")[0];-1!==e&&("."!==n&&".."!==n||1e.attachEvent.toString().indexOf("[native code")||da?(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)):(T=!0,e.attachEvent("onreadystatechange",b.onScriptLoad));e.src=d;Q=e;I?D.insertBefore(e,I):D.appendChild(e);Q=null;return e}if(ka)try{importScripts(d),b.completeLoad(c)}catch(q){b.onError(G("importscripts","importScripts failed for "+ -c+" at "+d,q,[c]))}};F&&!v.skipDataMain&&Y(document.getElementsByTagName("script"),function(b){D||(D=b.parentNode);if(P=b.getAttribute("data-main"))return u=P,v.baseUrl||(J=u.split("/"),u=J.pop(),U=J.length?J.join("/")+"/":"./",v.baseUrl=U),u=u.replace(V,""),k.jsExtRegExp.test(u)&&(u=P),v.deps=v.deps?v.deps.concat(u):[u],!0});define=function(b,c,d){var g,e;"string"!==typeof b&&(d=c,c=b,b=null);M(c)||(d=c,c=null);!c&&L(d)&&(c=[],d.length&&(d.toString().replace(qa,"").replace(ra,function(b,d){c.push(d)}), -c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));T&&(g=Q||pa())&&(b||(b=g.getAttribute("data-requiremodule")),e=K[g.getAttribute("data-requirecontext")]);e?(e.defQueue.push([b,c,d]),e.defQueueMap[b]=!0):W.push([b,c,d])};define.amd={jQuery:!0};k.exec=function(b){return eval(b)};k(v)}})(this); diff --git a/wicket-core/src/test/js/dom.js b/wicket-core/src/test/js/dom.js index 405bfcaaac3..56181b12c18 100644 --- a/wicket-core/src/test/js/dom.js +++ b/wicket-core/src/test/js/dom.js @@ -97,7 +97,7 @@ jQuery(document).ready(function() { assert.equal($deserialized[0].tagName.toLowerCase(), 'div', 'The serialized element name should be
'); assert.equal($deserialized.prop('id'), complexElementId, 'The serialized element id should be ' + complexElementId); assert.equal($deserialized.children()[0].tagName.toLowerCase(), 'a', 'The serialized element should have one child '); - assert.equal(jQuery.trim($deserialized.text()), 'Link', 'The serialized element should have text "Link"'); + assert.equal($deserialized.text().trim(), 'Link', 'The serialized element should have text "Link"'); }); test("show() an element", assert => { diff --git a/wicket-core/src/test/js/form.js b/wicket-core/src/test/js/form.js index 2edeaa2b134..59caa5b53ca 100644 --- a/wicket-core/src/test/js/form.js +++ b/wicket-core/src/test/js/form.js @@ -187,7 +187,7 @@ jQuery(document).ready(function() { }); queryString = jQuery.param(queryString, true); - var space = jQuery.fn.jquery.indexOf("3") === 0 ? "%20" : "+"; + var space = jQuery.fn.jquery.startsWith("2") ? "+" : "%20"; assert.equal(queryString, 'textInput=textValue&textUTFInput=%D0%BD%D0%B5%D1%89%D0%BE'+space+'%D0%BD%D0%B0'+space+'%D0%B1%D1%8A%D0%BB%D0%B3%D0%B0%D1%80%D1%81%D0%BA%D0%B8&checkBoxInput1=cbValue1&checkBoxInput3=cbValue3&radioInput=radioValue1&emailInput=m%40g.com&urlInput=http%3A%2F%2Fexample.com&searchInput=wicket&rangeInput=67&numberInput=16&colorInput=%23123456&multipleSelect=0&multipleSelect=2&select=0&textArea=some'+space+'text', 'Wicket.Form.serialize should serialize the whole form when an element is passed and the parent form should be searched'); }); @@ -202,7 +202,7 @@ jQuery(document).ready(function() { }); queryString = jQuery.param(queryString, true); - var space = jQuery.fn.jquery.indexOf("3") === 0 ? "%20" : "+"; + var space = jQuery.fn.jquery.startsWith("2") ? "+" : "%20"; assert.equal(queryString, 'textInput=textValue&textUTFInput=%D0%BD%D0%B5%D1%89%D0%BE'+space+'%D0%BD%D0%B0'+space+'%D0%B1%D1%8A%D0%BB%D0%B3%D0%B0%D1%80%D1%81%D0%BA%D0%B8&checkBoxInput1=cbValue1&checkBoxInput3=cbValue3&radioInput=radioValue1&emailInput=m%40g.com&urlInput=http%3A%2F%2Fexample.com&searchInput=wicket&rangeInput=67&numberInput=16&colorInput=%23123456&multipleSelect=0&multipleSelect=2&select=0&textArea=some'+space+'text', 'Wicket.Form.serialize should serialize the whole form when a the form itself is passed'); }); diff --git a/wicket-core/src/test/js/qunit/jquery.mockjax.js b/wicket-core/src/test/js/qunit/jquery.mockjax.js index 6fc1b6aa7a0..b3755846a75 100644 --- a/wicket-core/src/test/js/qunit/jquery.mockjax.js +++ b/wicket-core/src/test/js/qunit/jquery.mockjax.js @@ -1,13 +1,12 @@ /*! jQuery Mockjax * A Plugin providing simple and flexible mocking of ajax requests and responses * - * Version: 2.5.0 + * Version: 2.7.0 * Home: https://github.com/jakerella/jquery-mockjax - * Copyright (c) 2018 Jordan Kasper, formerly appendTo; + * Copyright (c) 2026 Jordan Kasper, formerly appendTo; * NOTE: This repository was taken over by Jordan Kasper (@jakerella) October, 2014 * - * Dual licensed under the MIT or GPL licenses. - * http://opensource.org/licenses/MIT OR http://www.gnu.org/licenses/gpl-2.0.html + * Licensed under the MIT license: http://opensource.org/licenses/MIT */ (function(root, factory) { 'use strict'; @@ -79,14 +78,14 @@ logger.debug( mock, ['Checking mock data against request data', mock, live] ); var identical = true; - if ( $.isFunction(mock) ) { + if (typeof mock === 'function') { return !!mock(live); } // Test for situations where the data is a querystring (not an object) if (typeof live === 'string') { // Querystring may be a regex - if ($.isFunction( mock.test )) { + if (typeof mock.test === 'function') { return mock.test(live); } else if (typeof mock === 'object') { live = getQueryParams(live); @@ -101,12 +100,12 @@ return identical; } else { if ( typeof live[k] === 'object' && live[k] !== null ) { - if ( identical && $.isArray( live[k] ) ) { - identical = $.isArray( mock[k] ) && live[k].length === mock[k].length; + if ( identical && Array.isArray( live[k] ) ) { + identical = Array.isArray( mock[k] ) && live[k].length === mock[k].length; } identical = identical && isMockDataEqual(mock[k], live[k]); } else { - if ( mock[k] && $.isFunction( mock[k].test ) ) { + if ( mock[k] && typeof mock[k].test === 'function') { identical = identical && mock[k].test(live[k]); } else { identical = identical && ( mock[k] === live[k] ); @@ -161,36 +160,42 @@ function getMockForRequest( handler, requestSettings ) { // If the mock was registered with a function, let the function decide if we // want to mock this request - if ( $.isFunction(handler) ) { + if (typeof handler === 'function') { return handler( requestSettings ); } + // Apply namespace prefix to the mock handler's url. + var namespace = handler.namespace || (typeof(handler.namespace) === 'undefined' && $.mockjaxSettings.namespace); + // Inspect the URL of the request and check if the mock handler's url // matches the url for this ajax request - if ( $.isFunction(handler.url.test) ) { + if (typeof handler.url.test === 'function') { + // namespace exists prepend handler.url with namespace + if (!!namespace) { + namespace = namespace.replace(/(\/+)$/, ''); + var pattern = handler.url.source.replace(/^(\^+)/, '').replace(/^/, '^(' + namespace + ')?\/?'); + handler.url = new RegExp(pattern); + } // The user provided a regex for the url, test it if ( !handler.url.test( requestSettings.url ) ) { return null; } } else { - var effecitveUrl = handler.url; - - // Apply namespace prefix to the mock handler's url. - var namespace = handler.namespace || (typeof(handler.namespace) === 'undefined' && $.mockjaxSettings.namespace); + var effectiveUrl = handler.url; if (!!namespace) { var namespacedUrl = [ namespace.replace(/(\/+)$/, ''), handler.url.replace(/^(\/+)/, '') ].join('/'); - effecitveUrl = namespacedUrl; + effectiveUrl = namespacedUrl; } // Look for a simple wildcard '*' or a direct URL match - var star = effecitveUrl.indexOf('*'); - if (effecitveUrl !== requestSettings.url && star === -1 || - !new RegExp(effecitveUrl.replace(/[-[\]{}()+?.,\\^$|#\s]/g, '\\$&').replace(/\*/g, '.+')).test(requestSettings.url)) { + var star = effectiveUrl.indexOf('*'); + if (effectiveUrl !== requestSettings.url && star === -1 || + !new RegExp(effectiveUrl.replace(/[-[\]{}()+?.,\\^$|#\s]/g, '\\$&').replace(/\*/g, '.+')).test(requestSettings.url)) { return null; } } @@ -238,7 +243,7 @@ } function parseResponseTimeOpt(responseTime) { - if ($.isArray(responseTime) && responseTime.length === 2) { + if (Array.isArray(responseTime) && responseTime.length === 2) { var min = responseTime[0]; var max = responseTime[1]; if(isPosNum(min) && isPosNum(max)) { @@ -287,7 +292,7 @@ this.responseText = mockHandler.responseText; } - if ($.isArray(mockHandler.status)) { + if (Array.isArray(mockHandler.status)) { var idxStatus = Math.floor(Math.random() * mockHandler.status.length); this.status = mockHandler.status[idxStatus]; } else if (typeof mockHandler.status === 'number' || typeof mockHandler.status === 'string') { @@ -301,7 +306,7 @@ onReady = this.onload || this.onreadystatechange; // jQuery < 1.4 doesn't have onreadystate change for xhr - if ( $.isFunction( onReady ) ) { + if (typeof onReady === 'function') { if( mockHandler.isTimeout) { this.status = -1; } @@ -314,7 +319,7 @@ // We have an executable function, call it to give // the mock handler a chance to update it's data - if ( $.isFunction(mockHandler.response) ) { + if (typeof mockHandler.response === 'function') { // Wait for it to finish if ( mockHandler.response.length === 2 ) { mockHandler.response(origSettings, function () { @@ -424,7 +429,7 @@ var headers = ''; // since jQuery 1.9 responseText type has to match contentType if (mockHandler.contentType) { - mockHandler.headers['Content-Type'] = mockHandler.contentType; + mockHandler.headers['content-type'] = mockHandler.contentType; } $.each(mockHandler.headers, function(k, v) { headers += k + ': ' + v + '\n'; @@ -490,7 +495,7 @@ newMock = ($.Deferred) ? (new $.Deferred()) : null; // If the response handler on the moock is a function, call it - if ( mockHandler.response && $.isFunction(mockHandler.response) ) { + if ( mockHandler.response && typeof mockHandler.response === 'function' ) { mockHandler.response(origSettings); @@ -540,7 +545,7 @@ if ( newMock ) { try { - json = $.parseJSON( mockHandler.responseText ); + json = JSON.parse( mockHandler.responseText ); } catch (err) { /* just checking... */ } newMock.resolveWith( callbackContext, [json || mockHandler.responseText] ); @@ -636,7 +641,7 @@ overrideCallback = function(action, mockHandler) { var origHandler = origSettings[action.toLowerCase()]; return function() { - if ( $.isFunction(origHandler) ) { + if (typeof origHandler === 'function') { origHandler.apply(this, [].slice.call(arguments)); } mockHandler['onAfter' + action](); @@ -720,13 +725,13 @@ } // Set up onAfter[X] callback functions - if ( $.isFunction( mockHandler.onAfterSuccess ) ) { + if (typeof mockHandler.onAfterSuccess === 'function') { origSettings.success = overrideCallback('Success', mockHandler); } - if ( $.isFunction( mockHandler.onAfterError ) ) { + if (typeof mockHandler.onAfterError === 'function') { origSettings.error = overrideCallback('Error', mockHandler); } - if ( $.isFunction( mockHandler.onAfterComplete ) ) { + if (typeof mockHandler.onAfterComplete === 'function') { origSettings.complete = overrideCallback('Complete', mockHandler); } @@ -930,7 +935,7 @@ */ $.mockjax = function(settings) { // Multiple mocks. - if ( $.isArray(settings) ) { + if (Array.isArray(settings)) { return $.map(settings, function(s) { return $.mockjax(s); }); diff --git a/wicket-core/src/test/js/qunit/qunit.css b/wicket-core/src/test/js/qunit/qunit.css index d445a567b73..d074bb78e5f 100644 --- a/wicket-core/src/test/js/qunit/qunit.css +++ b/wicket-core/src/test/js/qunit/qunit.css @@ -1,5 +1,5 @@ /*! - * QUnit 2.19.4 + * QUnit 2.25.0 * https://qunitjs.com/ * * Copyright OpenJS Foundation and other contributors @@ -45,10 +45,10 @@ @media (min-height: 500px) { #qunit { position: fixed; - left: 0px; - right: 0px; - top: 0px; - bottom: 0px; + left: 0; + right: 0; + top: 0; + bottom: 0; padding: 8px; display: -webkit-box; display: flex; @@ -329,11 +329,11 @@ margin: 0; } -#qunit-tests li strong { +#qunit-tests li .qunit-test-name { cursor: pointer; } -#qunit-tests li.skipped strong { +#qunit-tests li.skipped .qunit-test-name { cursor: default; } diff --git a/wicket-core/src/test/js/qunit/qunit.js b/wicket-core/src/test/js/qunit/qunit.js index 5e48b79303e..058aae0b0d8 100644 --- a/wicket-core/src/test/js/qunit/qunit.js +++ b/wicket-core/src/test/js/qunit/qunit.js @@ -1,5 +1,5 @@ /*! - * QUnit 2.19.4 + * QUnit 2.25.0 * https://qunitjs.com/ * * Copyright OpenJS Foundation and other contributors @@ -9,145 +9,102 @@ (function () { 'use strict'; - function _typeof(obj) { - "@babel/helpers - typeof"; - - return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { - return typeof obj; - } : function (obj) { - return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; - }, _typeof(obj); - } - function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } + function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; } - function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - Object.defineProperty(Constructor, "prototype", { - writable: false - }); - return Constructor; + function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; } - function _slicedToArray(arr, i) { - return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); + function _arrayWithoutHoles(r) { + if (Array.isArray(r)) return _arrayLikeToArray(r); } - function _toConsumableArray(arr) { - return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); + function _classCallCheck(a, n) { + if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } - function _arrayWithoutHoles(arr) { - if (Array.isArray(arr)) return _arrayLikeToArray(arr); + function _defineProperties(e, r) { + for (var t = 0; t < r.length; t++) { + var o = r[t]; + o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); + } } - function _arrayWithHoles(arr) { - if (Array.isArray(arr)) return arr; + function _createClass(e, r, t) { + return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { + writable: !1 + }), e; } - function _iterableToArray(iter) { - if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); + function _iterableToArray(r) { + if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } - function _iterableToArrayLimit(arr, i) { - var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; - if (_i == null) return; - var _arr = []; - var _n = true; - var _d = false; - var _s, _e; - try { - for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { - _arr.push(_s.value); - if (i && _arr.length === i) break; - } - } catch (err) { - _d = true; - _e = err; - } finally { + function _iterableToArrayLimit(r, l) { + var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; try { - if (!_n && _i["return"] != null) _i["return"](); + if (i = (t = t.call(r)).next, 0 === l) { + if (Object(t) !== t) return; + f = !1; + } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); + } catch (r) { + o = !0, n = r; } finally { - if (_d) throw _e; + try { + if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; + } finally { + if (o) throw n; + } } + return a; } - return _arr; } - function _unsupportedIterableToArray(o, minLen) { - if (!o) return; - if (typeof o === "string") return _arrayLikeToArray(o, minLen); - var n = Object.prototype.toString.call(o).slice(8, -1); - if (n === "Object" && o.constructor) n = o.constructor.name; - if (n === "Map" || n === "Set") return Array.from(o); - if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); - } - function _arrayLikeToArray(arr, len) { - if (len == null || len > arr.length) len = arr.length; - for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; - return arr2; + function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } - function _nonIterableRest() { - throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + function _slicedToArray(r, e) { + return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } - function _createForOfIteratorHelper(o, allowArrayLike) { - var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; - if (!it) { - if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { - if (it) o = it; - var i = 0; - var F = function () {}; - return { - s: F, - n: function () { - if (i >= o.length) return { - done: true - }; - return { - done: false, - value: o[i++] - }; - }, - e: function (e) { - throw e; - }, - f: F - }; - } - throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + function _toConsumableArray(r) { + return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); + } + function _toPrimitive(t, r) { + if ("object" != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || "default"); + if ("object" != typeof i) return i; + throw new TypeError("@@toPrimitive must return a primitive value."); + } + return ("string" === r ? String : Number)(t); + } + function _toPropertyKey(t) { + var i = _toPrimitive(t, "string"); + return "symbol" == typeof i ? i : i + ""; + } + function _typeof(o) { + "@babel/helpers - typeof"; + + return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { + return typeof o; + } : function (o) { + return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; + }, _typeof(o); + } + function _unsupportedIterableToArray(r, a) { + if (r) { + if ("string" == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } - var normalCompletion = true, - didErr = false, - err; - return { - s: function () { - it = it.call(o); - }, - n: function () { - var step = it.next(); - normalCompletion = step.done; - return step; - }, - e: function (e) { - didErr = true; - err = e; - }, - f: function () { - try { - if (!normalCompletion && it.return != null) it.return(); - } finally { - if (didErr) throw err; - } - } - }; } // We don't use global-this-polyfill [1], because it modifies @@ -191,10 +148,15 @@ // This avoids a simple `export const` assignment as that would lead Rollup // to change getGlobalThis and use the same (generated) variable name there. var g = getGlobalThis(); - var window$1 = g.window; + + // These optional globals are undefined in one or more environments: + // modern browser, old browser, Node.js, SpiderMonkey. + // Calling code must check these for truthy-ness before use. var console$1 = g.console; var setTimeout$1 = g.setTimeout; var clearTimeout = g.clearTimeout; + var process$1 = g.process; + var window$1 = g.window; var document = window$1 && window$1.document; var navigator = window$1 && window$1.navigator; var localSessionStorage = function () { @@ -280,53 +242,15 @@ }; }; - // Support: IE 9 - // Detect if the console object exists and no-op otherwise. - // This allows support for IE 9, which doesn't have a console - // object if the developer tools are not open. - - // Support: IE 9 - // Function#bind is supported, but no console.log.bind(). - - // Support: SpiderMonkey (mozjs 68+) - // The console object has a log method, but no warn method. - - var Logger = { - warn: console$1 ? Function.prototype.bind.call(console$1.warn || console$1.log, console$1) : function () {} - }; - var toString = Object.prototype.toString; var hasOwn$1 = Object.prototype.hasOwnProperty; - var nativePerf = getNativePerf(); - - // TODO: Consider using globalThis instead so that perf marks work - // in Node.js as well. As they can have overhead, we should also - // have a way to disable these, and/or make them an opt-in reporter - // in QUnit 3 and then support globalThis. - // For example: `QUnit.addReporter(QUnit.reporters.perf)`. - function getNativePerf() { - if (window$1 && typeof window$1.performance !== 'undefined' && typeof window$1.performance.mark === 'function' && typeof window$1.performance.measure === 'function') { - return window$1.performance; - } else { - return undefined; - } - } var performance = { - now: nativePerf ? nativePerf.now.bind(nativePerf) : Date.now, - measure: nativePerf ? function (comment, startMark, endMark) { - // `performance.measure` may fail if the mark could not be found. - // reasons a specific mark could not be found include: outside code invoking `performance.clearMarks()` - try { - nativePerf.measure(comment, startMark, endMark); - } catch (ex) { - Logger.warn('performance.measure could not be executed because of ', ex.message); - } - } : function () {}, - mark: nativePerf ? nativePerf.mark.bind(nativePerf) : function () {} + // eslint-disable-next-line compat/compat -- Checked + now: window$1 && window$1.performance && window$1.performance.now ? window$1.performance.now.bind(window$1.performance) : Date.now }; // Returns a new Array with the elements that are in a but not in b - function diff(a, b) { + function diff$1(a, b) { return a.filter(function (a) { return b.indexOf(a) === -1; }); @@ -340,9 +264,11 @@ * @param {Array} array * @return {boolean} */ - function inArray(elem, array) { + var inArray = Array.prototype.includes ? function (elem, array) { + return array.includes(elem); + } : function (elem, array) { return array.indexOf(elem) !== -1; - } + }; /** * Recursively clone an object into a plain array or object, taking only the @@ -478,6 +404,27 @@ return resultErrorString; } } + function escapeText(str) { + if (!str) { + return ''; + } + + // Both single quotes and double quotes (for attributes) + return ('' + str).replace(/['"<>&]/g, function (s) { + switch (s) { + case "'": + return '''; + case '"': + return '"'; + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + } + }); + } var BOXABLE_TYPES = new StringSet(['boolean', 'number', 'string']); @@ -748,14 +695,25 @@ var config = { // HTML Reporter: Modify document.title when suite is done altertitle: true, + // TODO: Move here from /src/core.js in QUnit 3. + // autostart: true, + // HTML Reporter: collapse every test except the first failing test // If false, all failing tests will be expanded collapse: true, + countStepsAsOne: false, + // TODO: Make explicit in QUnit 3. + // current: undefined, + // whether or not to fail when there are zero tests // defaults to `true` failOnZeroTests: true, // Select by pattern or case-insensitive substring match against "moduleName: testName" filter: undefined, + testFilter: null, + // TODO: Make explicit in QUnit 3. + // fixture: undefined, + // Depth up-to which object will be dumped maxDepth: 5, // Select case-insensitive match of the module name @@ -765,13 +723,25 @@ // By default, run previously failed tests first // very useful in combination with "Hide passed tests" checked reorder: true, + reporters: {}, // When enabled, all tests must call expect() requireExpects: false, // By default, scroll to top of the page when suite is done scrolltop: true, + // TODO: Make explicit in QUnit 3. + // seed: undefined, + // The storage module to use for reordering tests storage: localSessionStorage, testId: undefined, + // The updateRate controls how often QUnit will yield the main thread + // between tests. This is mainly for the benefit of the HTML Reporter, + // so that the browser can visually paint DOM changes with test results. + // This also helps avoid causing browsers to prompt a warning about + // long-running scripts. + // TODO: Move here from /src/core.js in QUnit 3. + // updateRate: 1000, + // HTML Reporter: List of URL parameters that are given visual controls urlConfig: [], // Internal: The first unnamed module @@ -823,7 +793,17 @@ // Internal: Exposed to make resets easier // Ref https://github.com/qunitjs/qunit/pull/1598 globalHooks: {}, + // Internal: ProcessingQueue singleton, created in /src/core.js + pq: null, + // Internal: Created in /src/core.js + // TODO: Move definitions here in QUnit 3.0. + // started: 0, + // Internal state + _event_listeners: Object.create(null), + _event_memory: {}, + _deprecated_timeout_shown: false, + _deprecated_countEachStep_shown: false, blocking: true, callbacks: {}, modules: [], @@ -834,6 +814,67 @@ testCount: 0 } }; + function readFlatPreconfigBoolean(val, dest) { + if (typeof val === 'boolean' || typeof val === 'string' && val !== '') { + config[dest] = val === true || val === 'true'; + } + } + function readFlatPreconfigNumber(val, dest) { + if (typeof val === 'number' || typeof val === 'string' && /^[0-9]+$/.test(val)) { + config[dest] = +val; + } + } + function readFlatPreconfigString(val, dest) { + if (typeof val === 'string' && val !== '') { + config[dest] = val; + } + } + function readFlatPreconfigStringOrBoolean(val, dest) { + if (typeof val === 'boolean' || typeof val === 'string' && val !== '') { + config[dest] = val; + } + } + function readFlatPreconfigStringArray(val, dest) { + if (typeof val === 'string' && val !== '') { + config[dest] = [val]; + } + } + function readFlatPreconfig(obj) { + readFlatPreconfigBoolean(obj.qunit_config_altertitle, 'altertitle'); + readFlatPreconfigBoolean(obj.qunit_config_autostart, 'autostart'); + readFlatPreconfigBoolean(obj.qunit_config_collapse, 'collapse'); + readFlatPreconfigBoolean(obj.qunit_config_failonzerotests, 'failOnZeroTests'); + readFlatPreconfigString(obj.qunit_config_filter, 'filter'); + readFlatPreconfigString(obj.qunit_config_fixture, 'fixture'); + readFlatPreconfigBoolean(obj.qunit_config_hidepassed, 'hidepassed'); + readFlatPreconfigNumber(obj.qunit_config_maxdepth, 'maxDepth'); + readFlatPreconfigString(obj.qunit_config_module, 'module'); + readFlatPreconfigStringArray(obj.qunit_config_moduleid, 'moduleId'); + readFlatPreconfigBoolean(obj.qunit_config_noglobals, 'noglobals'); + readFlatPreconfigBoolean(obj.qunit_config_notrycatch, 'notrycatch'); + readFlatPreconfigBoolean(obj.qunit_config_reorder, 'reorder'); + readFlatPreconfigBoolean(obj.qunit_config_requireexpects, 'requireExpects'); + readFlatPreconfigBoolean(obj.qunit_config_scrolltop, 'scrolltop'); + readFlatPreconfigStringOrBoolean(obj.qunit_config_seed, 'seed'); + readFlatPreconfigStringArray(obj.qunit_config_testid, 'testId'); + readFlatPreconfigNumber(obj.qunit_config_testtimeout, 'testTimeout'); + var reporterKeys = { + qunit_config_reporters_console: 'console', + qunit_config_reporters_tap: 'tap' + }; + for (var key in reporterKeys) { + var val = obj[key]; + // Based on readFlatPreconfigBoolean + if (typeof val === 'boolean' || typeof val === 'string' && val !== '') { + var dest = reporterKeys[key]; + config.reporters[dest] = val === true || val === 'true' || val === '1'; + } + } + } + if (process$1 && 'env' in process$1) { + readFlatPreconfig(process$1.env); + } + readFlatPreconfig(g); // Apply a predefined QUnit.config object // @@ -846,6 +887,12 @@ // Push a loose unnamed module to the modules collection config.modules.push(config.currentModule); + if (config.seed === 'true' || config.seed === true) { + // Generate a random seed + // Length of `Math.random()` fraction, in base 36, may vary from 6-14. + // Pad and take slice to a consistent 10-digit value. + config.seed = (Math.random().toString(36) + '0000000000').slice(2, 12); + } var dump = (function () { function quote(str) { @@ -1089,6 +1136,21 @@ return dump; })(); + // Support: IE 9 + // Detect if the console object exists and no-op otherwise. + // This allows support for IE 9, which doesn't have a console + // object if the developer tools are not open. + + // Support: IE 9 + // Function#bind is supported, but no console.log.bind(). + + // Support: SpiderMonkey (mozjs 68+) + // The console object has a log method, but no warn method. + + var Logger = { + warn: console$1 ? Function.prototype.bind.call(console$1.warn || console$1.log, console$1) : function () {} + }; + var SuiteReport = /*#__PURE__*/function () { function SuiteReport(name, parentSuite) { _classCallCheck(this, SuiteReport); @@ -1105,13 +1167,11 @@ parentSuite.pushChildSuite(this); } } - _createClass(SuiteReport, [{ + return _createClass(SuiteReport, [{ key: "start", value: function start(recordTime) { if (recordTime) { this._startTime = performance.now(); - var suiteLevel = this.fullName.length; - performance.mark("qunit_suite_".concat(suiteLevel, "_start")); } return { name: this.name, @@ -1132,10 +1192,6 @@ value: function end(recordTime) { if (recordTime) { this._endTime = performance.now(); - var suiteLevel = this.fullName.length; - var suiteName = this.fullName.join(' – '); - performance.mark("qunit_suite_".concat(suiteLevel, "_end")); - performance.measure(suiteLevel === 0 ? 'QUnit Test Run' : "QUnit Test Suite: ".concat(suiteName), "qunit_suite_".concat(suiteLevel, "_start"), "qunit_suite_".concat(suiteLevel, "_end")); } return { name: this.name, @@ -1210,7 +1266,6 @@ } } }]); - return SuiteReport; }(); var moduleStack = []; @@ -1284,10 +1339,10 @@ module.hooks[hookName].push(callback); }; } - function processModule(name, options, executeNow) { + function processModule(name, options, scope) { var modifiers = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; if (typeof options === 'function') { - executeNow = options; + scope = options; options = undefined; } var module = createModule(name, options, modifiers); @@ -1307,10 +1362,10 @@ }; var prevModule = config.currentModule; config.currentModule = module; - if (typeof executeNow === 'function') { + if (typeof scope === 'function') { moduleStack.push(module); try { - var cbReturnValue = executeNow.call(module.testEnvironment, moduleFns); + var cbReturnValue = scope.call(module.testEnvironment, moduleFns); if (cbReturnValue && typeof cbReturnValue.then === 'function') { Logger.warn('Returning a promise from a module callback is not supported. ' + 'Instead, use hooks for async behavior. ' + 'This will become an error in QUnit 3.0.'); } @@ -1324,11 +1379,45 @@ } } } + + /** + * Clear the SuiteReport tree of all tests and leave only current module as child suite + * + * This should be called before defining the first module.only() or test.only() + * because otherwise: + * - `runEnd.testCounts` is too high. + * - UI (HtmlReporter) and TAP (TapReporter) display totals too high. + * - Test runners like QTap might timeout because the TAP plan + * would be printed as "1..9" even if only 2 tests are run, + * which means tap-finished will wait for 3-9. + */ + function clearSuiteReports(currentModule) { + var childSuite = null; + var suiteReport = currentModule.suiteReport; + while (suiteReport) { + suiteReport.tests.length = 0; + var i = suiteReport.childSuites.indexOf(childSuite); + if (i === -1) { + suiteReport.childSuites.length = 0; + } else { + // Reduce in-place to just currentModule.suiteReport or its intermediary + suiteReport.childSuites.splice(0, i); + suiteReport.childSuites.splice(1); + } + if (suiteReport === runSuite) { + suiteReport = null; + } else { + childSuite = suiteReport; + currentModule = currentModule.parentModule; + suiteReport = currentModule && currentModule.suiteReport || runSuite; + } + } + } var focused$1 = false; // indicates that the "only" filter was used - function module$1(name, options, executeNow) { + function module$1(name, options, scope) { var ignored = focused$1 && !isParentModuleInQueue(); - processModule(name, options, executeNow, { + processModule(name, options, scope, { ignored: ignored }); } @@ -1338,6 +1427,7 @@ // delete any and all previously registered modules and tests. config.modules.length = 0; config.queue.length = 0; + clearSuiteReports(config.currentModule); // Ignore any tests declared after this block within the same // module parent. https://github.com/qunitjs/qunit/issues/1645 @@ -1346,33 +1436,147 @@ focused$1 = true; processModule.apply(void 0, arguments); }; - module$1.skip = function (name, options, executeNow) { + module$1.skip = function (name, options, scope) { if (focused$1) { return; } - processModule(name, options, executeNow, { + processModule(name, options, scope, { skip: true }); }; - module$1.todo = function (name, options, executeNow) { + module$1.if = function (name, condition, options, scope) { + if (focused$1) { + return; + } + processModule(name, options, scope, { + skip: !condition + }); + }; + module$1.todo = function (name, options, scope) { if (focused$1) { return; } - processModule(name, options, executeNow, { + processModule(name, options, scope, { todo: true }); }; - // Doesn't support IE9, it will return undefined on these browsers - // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack - var fileName = (sourceFromStacktrace(0) || '').replace(/(:\d+)+\)?/, '') - // Remove anything prior to the last slash (Unix/Windows) - // from the last frame - .replace(/.+[/\\]/, ''); + // Stacktrace cleaner to focus on the path from error source to test suite. + // + // This should reduce a raw stack trace like this: + // + // > foo.broken()@/example/foo.js + // > Bar@/example/bar.js + // > @/test/bar.test.js + // > @/lib/qunit.js:500:12 + // > @/lib/qunit.js:100:28 + // > @/lib/qunit.js:200:56 + // > setTimeout@ + // > @/dist/vendor.js + // + // and shorten it to show up until the end of the user's bar.test.js code. + // + // > foo.broken()@/example/foo.js + // > Bar@/example/bar.js + // > @/test/bar.test.js + // + // QUnit will obtain one example trace (once per process/pageload suffices), + // strip off any : and ::, and use that as match needle, + // to the first QUnit-internal frames, and then stop at that point. + // Any later frames, including those that are outside QUnit again, will be ommitted + // as being uninteresting to the test, since QUnit will have either started or + // resumed the test. This we also clean away browser built-ins, or other + // vendor/bundler that may be higher up the stack. + // + // Stripping :: is not for prettyness, it is essential for the + // match needle to work, since this sample trace will by definitin not be the + // same line as e.g. the QUnit.test() call we're trying to identify. + // + // See also: + // - https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack + + function qunitFileName() { + var error = new Error(); + if (!error.stack) { + // Copy of sourceFromStacktrace() to avoid circular dependency + // Support: IE 9-11 + try { + throw error; + } catch (err) { + error = err; + } + } + return (error.stack || '' + // Copy of extractStacktrace() to avoid circular dependency + // Support: V8/Chrome + ).replace(/^error$\n/im, '').split('\n')[0] + // Global replace, because a frame like localhost:4000/lib/qunit.js:1234:50, + // would otherwise (harmlessly, but uselessly) remove only the port (first match). + // https://github.com/qunitjs/qunit/issues/1769 + .replace(/(:\d+)+\)?/g, '') + // Remove anything prior to the last slash (Unix/Windows) from the last frame, + // leaving only "qunit.js". + .replace(/.+[/\\]/, ''); + } + var fileName = qunitFileName(); + + /** + * Responsibilities: + * - For internal errors from QUnit itself, remove the first qunit.js frames. + * - For errors in Node.js, format any remaining qunit.js and node:internal + * frames as internal (i.e. grey out). + * + * @param {string} stack Error#stack + * @param {Function} formatInternal Format a string in an "internal" color + * @param {string|null} [eToString] Error#toString() to help remove + * noise from Node.js/V8 stack traces. + */ + function annotateStacktrace(stack, formatInternal) { + var eToString = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + var frames = stack.split('\n'); + var annotated = []; + if (eToString && eToString.indexOf(frames[0]) !== -1) { + // In Firefox and Safari e.stack starts with frame 0, but in V8 (Chrome/Node.js), + // e.stack starts first stringified message. Preserve this separately, + // so that, below, we can distinguish between internal frames on top + // (to remove) vs later internal frames (to format differently). + annotated.push(frames.shift()); + } + var initialInternal = true; + for (var i = 0; i < frames.length; i++) { + var frame = frames[i]; + var isInternal = fileName && frame.indexOf(fileName) !== -1 || + // Support Node 16+: ESM-style + // "at wrap (node:internal/modules/cjs/loader:1)" + frame.indexOf('node:internal/') !== -1 || + // Support Node 12-14 (CJS-style) + // "at load (internal/modules/cjs/loader.js:7)" + frame.match(/^\s+at .+\(internal[^)]*\)$/) || + // Support Node 10 + // "at listOnTimeout (timers.js:263)" + // Avoid matching "(C:)" on Windows + // Avoid matching "(http:)" + frame.match(/^\s+at .+\([a-z]+\.js[:\d]*\)$/); + if (!isInternal) { + initialInternal = false; + } + // Remove initial internal frames entirely. + if (!initialInternal) { + annotated.push(isInternal ? formatInternal(frame) : frame); + } + } + return annotated.join('\n'); + } function extractStacktrace(e, offset) { offset = offset === undefined ? 4 : offset; + + // Support: IE9, e.stack is not supported, we will return undefined if (e && e.stack) { var stack = e.stack.split('\n'); + // In Firefox and Safari, e.stack starts immediately with the first frame. + // + // In V8 (Chrome/Node.js), the stack starts first with a stringified error message, + // and the real stack starting on line 2. if (/^error$/i.test(stack[0])) { stack.shift(); } @@ -1394,8 +1598,9 @@ function sourceFromStacktrace(offset) { var error = new Error(); - // Support: Safari <=7 only, IE <=10 - 11 only - // Not all browsers generate the `stack` property for `new Error()`, see also #636 + // Support: IE 9-11, iOS 7 + // Not all browsers generate the `stack` property for `new Error()` + // See also https://github.com/qunitjs/qunit/issues/636 if (!error.stack) { try { throw error; @@ -1411,7 +1616,7 @@ _classCallCheck(this, Assert); this.test = testContext; } - _createClass(Assert, [{ + return _createClass(Assert, [{ key: "timeout", value: function timeout(duration) { if (typeof duration !== 'number') { @@ -1455,6 +1660,7 @@ // Since the steps array is just string values, we can clone with slice var actualStepsClone = this.test.steps.slice(); this.deepEqual(actualStepsClone, steps, message); + this.test.stepsCount += this.test.steps.length; this.test.steps.length = 0; } }, { @@ -1471,16 +1677,32 @@ }, { key: "async", value: function async(count) { - var requiredCalls = count === undefined ? 1 : count; + if (count === undefined) { + count = 1; + } else if (typeof count !== 'number') { + throw new TypeError('async takes number as an input'); + } + var requiredCalls = count; return this.test.internalStop(requiredCalls); } + }, { + key: "closeTo", + value: function closeTo(actual, expected, delta, message) { + if (typeof delta !== 'number') { + throw new TypeError('closeTo() requires a delta argument'); + } + this.pushResult({ + result: Math.abs(actual - expected) <= delta, + actual: actual, + expected: expected, + message: message || "value should be within ".concat(delta, " inclusive") + }); + } - // Exports test.push() to the user API // Alias of pushResult. }, { key: "push", value: function push(result, actual, expected, message, negative) { - Logger.warn('assert.push is deprecated and will be removed in QUnit 3.0.' + ' Please use assert.pushResult instead (https://api.qunitjs.com/assert/pushResult).'); var currentAssert = this instanceof Assert ? this : config.current.assert; return currentAssert.pushResult({ result: result, @@ -1490,6 +1712,8 @@ negative: negative }); } + + // Public API to internal test.pushResult() }, { key: "pushResult", value: function pushResult(resultInfo) { @@ -1759,7 +1983,6 @@ }); } }]); - return Assert; }(); function validateExpectedExceptionArgs(expected, message, assertionMethod) { var expectedType = objectType(expected); @@ -1831,8 +2054,8 @@ // eslint-disable-next-line dot-notation Assert.prototype.raises = Assert.prototype['throws']; - var LISTENERS = Object.create(null); var SUPPORTED_EVENTS = ['error', 'runStart', 'suiteStart', 'testStart', 'assertion', 'testEnd', 'suiteEnd', 'runEnd']; + var MEMORY_EVENTS = ['error', 'runEnd']; /** * Emits an event with the specified data to all currently registered listeners. @@ -1852,11 +2075,14 @@ } // Clone the callbacks in case one of them registers a new callback - var originalCallbacks = LISTENERS[eventName]; + var originalCallbacks = config._event_listeners[eventName]; var callbacks = originalCallbacks ? _toConsumableArray(originalCallbacks) : []; for (var i = 0; i < callbacks.length; i++) { callbacks[i](data); } + if (inArray(eventName, MEMORY_EVENTS)) { + config._event_memory[eventName] = data; + } } /** @@ -1877,13 +2103,14 @@ } else if (typeof callback !== 'function') { throw new TypeError('callback must be a function when registering a listener'); } - if (!LISTENERS[eventName]) { - LISTENERS[eventName] = []; - } + var listeners = config._event_listeners[eventName] || (config._event_listeners[eventName] = []); // Don't register the same callback more than once - if (!inArray(callback, LISTENERS[eventName])) { - LISTENERS[eventName].push(callback); + if (!inArray(callback, listeners)) { + listeners.push(callback); + if (config._event_memory[eventName] !== undefined) { + callback(config._event_memory[eventName]); + } } } @@ -2185,20 +2412,23 @@ }; // Use polyfill for setImmediate for performance gains - Promise._immediateFn = // @ts-ignore - typeof setImmediate === 'function' && function (fn) { + if (typeof setImmediate === 'function') { // @ts-ignore - setImmediate(fn); - } || function (fn) { - setTimeoutFunc(fn, 0); - }; + var setImmediateFunc = setImmediate; + Promise._immediateFn = function (fn) { + setImmediateFunc(fn); + }; + } else { + Promise._immediateFn = function (fn) { + setTimeoutFunc(fn, 0); + }; + } Promise._unhandledRejectionFn = function _unhandledRejectionFn(err) { if (typeof console !== 'undefined' && console) { console.warn('Possible Unhandled Promise Rejection:', err); // eslint-disable-line no-console } }; - promisePolyfill.exports = Promise; })(); var _Promise = promisePolyfill.exports; @@ -2248,248 +2478,44 @@ return promiseChain; } - var priorityCount = 0; - var unitSampler; - - // This is a queue of functions that are tasks within a single test. - // After tests are dequeued from config.queue they are expanded into - // a set of tasks in this queue. - var taskQueue = []; - - /** - * Advances the taskQueue to the next task. If the taskQueue is empty, - * process the testQueue - */ - function advance() { - advanceTaskQueue(); - if (!taskQueue.length && !config.blocking && !config.current) { - advanceTestQueue(); + var TestReport = /*#__PURE__*/function () { + function TestReport(name, suite, options) { + _classCallCheck(this, TestReport); + this.name = name; + this.suiteName = suite.name; + this.fullName = suite.fullName.concat(name); + this.runtime = 0; + this.assertions = []; + this.skipped = !!options.skip; + this.todo = !!options.todo; + this.valid = options.valid; + this._startTime = 0; + this._endTime = 0; + suite.pushTest(this); } - } - - /** - * Advances the taskQueue with an increased depth - */ - function advanceTaskQueue() { - var start = performance.now(); - config.depth = (config.depth || 0) + 1; - processTaskQueue(start); - config.depth--; - } - - /** - * Process the first task on the taskQueue as a promise. - * Each task is a function added by Test#queue() in /src/test.js - */ - function processTaskQueue(start) { - if (taskQueue.length && !config.blocking) { - var elapsedTime = performance.now() - start; - - // The updateRate ensures that a user interface (HTML Reporter) can be updated - // at least once every second. This can also prevent browsers from prompting - // a warning about long running scripts. - if (!setTimeout$1 || config.updateRate <= 0 || elapsedTime < config.updateRate) { - var task = taskQueue.shift(); - _Promise.resolve(task()).then(function () { - if (!taskQueue.length) { - advance(); - } else { - processTaskQueue(start); - } - }); - } else { - setTimeout$1(advance); - } - } - } - - /** - * Advance the testQueue to the next test to process. Call done() if testQueue completes. - */ - function advanceTestQueue() { - if (!config.blocking && !config.queue.length && config.depth === 0) { - done(); - return; - } - var testTasks = config.queue.shift(); - addToTaskQueue(testTasks()); - if (priorityCount > 0) { - priorityCount--; - } - advance(); - } - - /** - * Enqueue the tasks for a test into the task queue. - * @param {Array} tasksArray - */ - function addToTaskQueue(tasksArray) { - taskQueue.push.apply(taskQueue, _toConsumableArray(tasksArray)); - } - - /** - * Return the number of tasks remaining in the task queue to be processed. - * @return {number} - */ - function taskQueueLength() { - return taskQueue.length; - } - - /** - * Adds a test to the TestQueue for execution. - * @param {Function} testTasksFunc - * @param {boolean} prioritize - * @param {string} seed - */ - function addToTestQueue(testTasksFunc, prioritize, seed) { - if (prioritize) { - config.queue.splice(priorityCount++, 0, testTasksFunc); - } else if (seed) { - if (!unitSampler) { - unitSampler = unitSamplerGenerator(seed); - } - - // Insert into a random position after all prioritized items - var index = Math.floor(unitSampler() * (config.queue.length - priorityCount + 1)); - config.queue.splice(priorityCount + index, 0, testTasksFunc); - } else { - config.queue.push(testTasksFunc); - } - } - - /** - * Creates a seeded "sample" generator which is used for randomizing tests. - */ - function unitSamplerGenerator(seed) { - // 32-bit xorshift, requires only a nonzero seed - // https://excamera.com/sphinx/article-xorshift.html - var sample = parseInt(generateHash(seed), 16) || -1; - return function () { - sample ^= sample << 13; - sample ^= sample >>> 17; - sample ^= sample << 5; - - // ECMAScript has no unsigned number type - if (sample < 0) { - sample += 0x100000000; - } - return sample / 0x100000000; - }; - } - - /** - * This function is called when the ProcessingQueue is done processing all - * items. It handles emitting the final run events. - */ - function done() { - // We have reached the end of the processing queue and are about to emit the - // "runEnd" event after which reporters typically stop listening and exit - // the process. First, check if we need to emit one final test. - if (config.stats.testCount === 0 && config.failOnZeroTests === true) { - var error; - if (config.filter && config.filter.length) { - error = new Error("No tests matched the filter \"".concat(config.filter, "\".")); - } else if (config.module && config.module.length) { - error = new Error("No tests matched the module \"".concat(config.module, "\".")); - } else if (config.moduleId && config.moduleId.length) { - error = new Error("No tests matched the moduleId \"".concat(config.moduleId, "\".")); - } else if (config.testId && config.testId.length) { - error = new Error("No tests matched the testId \"".concat(config.testId, "\".")); - } else { - error = new Error('No tests were run.'); - } - test('global failure', extend(function (assert) { - assert.pushResult({ - result: false, - message: error.message, - source: error.stack - }); - }, { - validTest: true - })); - - // We do need to call `advance()` in order to resume the processing queue. - // Once this new test is finished processing, we'll reach `done` again, and - // that time the above condition will evaluate to false. - advance(); - return; - } - var storage = config.storage; - var runtime = Math.round(performance.now() - config.started); - var passed = config.stats.all - config.stats.bad; - ProcessingQueue.finished = true; - emit('runEnd', runSuite.end(true)); - runLoggingCallbacks('done', { - // @deprecated since 2.19.0 Use done() without `details` parameter, - // or use `QUnit.on('runEnd')` instead. Parameter to be replaced in - // QUnit 3.0 with test counts. - passed: passed, - failed: config.stats.bad, - total: config.stats.all, - runtime: runtime - }).then(function () { - // Clear own storage items if all tests passed - if (storage && config.stats.bad === 0) { - for (var i = storage.length - 1; i >= 0; i--) { - var key = storage.key(i); - if (key.indexOf('qunit-test-') === 0) { - storage.removeItem(key); - } - } - } - }); - } - var ProcessingQueue = { - finished: false, - add: addToTestQueue, - advance: advance, - taskCount: taskQueueLength - }; - - var TestReport = /*#__PURE__*/function () { - function TestReport(name, suite, options) { - _classCallCheck(this, TestReport); - this.name = name; - this.suiteName = suite.name; - this.fullName = suite.fullName.concat(name); - this.runtime = 0; - this.assertions = []; - this.skipped = !!options.skip; - this.todo = !!options.todo; - this.valid = options.valid; - this._startTime = 0; - this._endTime = 0; - suite.pushTest(this); - } - _createClass(TestReport, [{ - key: "start", - value: function start(recordTime) { - if (recordTime) { - this._startTime = performance.now(); - performance.mark('qunit_test_start'); - } - return { - name: this.name, - suiteName: this.suiteName, - fullName: this.fullName.slice() - }; - } - }, { - key: "end", - value: function end(recordTime) { - if (recordTime) { - this._endTime = performance.now(); - if (performance) { - performance.mark('qunit_test_end'); - var testName = this.fullName.join(' – '); - performance.measure("QUnit Test: ".concat(testName), 'qunit_test_start', 'qunit_test_end'); - } - } - return extend(this.start(), { - runtime: this.getRuntime(), - status: this.getStatus(), - errors: this.getFailedAssertions(), - assertions: this.getAssertions() + return _createClass(TestReport, [{ + key: "start", + value: function start(recordTime) { + if (recordTime) { + this._startTime = performance.now(); + } + return { + name: this.name, + suiteName: this.suiteName, + fullName: this.fullName.slice() + }; + } + }, { + key: "end", + value: function end(recordTime) { + if (recordTime) { + this._endTime = performance.now(); + } + return extend(this.start(), { + runtime: this.getRuntime(), + status: this.getStatus(), + errors: this.getFailedAssertions(), + assertions: this.getAssertions() }); } }, { @@ -2542,7 +2568,6 @@ }); } }]); - return TestReport; }(); function Test(settings) { @@ -2550,6 +2575,9 @@ this.assertions = []; this.module = config.currentModule; this.steps = []; + // This powers the QUnit.config.countStepsAsOne feature. + // https://github.com/qunitjs/qunit/pull/1775 + this.stepsCount = 0; this.timeout = undefined; this.data = undefined; this.withData = false; @@ -2585,7 +2613,7 @@ // Queuing a late test after the run has ended is not allowed. // This was once supported for internal use by QUnit.onError(). // Ref https://github.com/qunitjs/qunit/issues/1377 - if (ProcessingQueue.finished) { + if (config.pq.finished) { // Using this for anything other than onError(), such as testing in QUnit.done(), // is unstable and will likely result in the added tests being ignored by CI. // (Meaning the CI passes irregardless of the added tests). @@ -2632,7 +2660,7 @@ skip: !!this.skip }); if (this.skip) { - // Skipped tests will fully ignore any sent callback + // Skipped tests will fully ignore (and dereference for garbage collect) any sent callback this.callback = function () {}; this.async = false; this.expected = 0; @@ -2771,7 +2799,7 @@ // The 'after' hook should only execute when there are not tests left and // when the 'after' and 'finish' tasks are the only tasks left to process - if (hookName === 'after' && !lastTestWithinModuleExecuted(hookOwner) && (config.queue.length > 0 || ProcessingQueue.taskCount() > 2)) { + if (hookName === 'after' && !lastTestWithinModuleExecuted(hookOwner) && (config.queue.length > 0 || config.pq.taskCount() > 2)) { return; } config.current = _this3; @@ -2839,11 +2867,24 @@ var stepsList = this.steps.join(', '); this.pushFailure('Expected assert.verifySteps() to be called before end of test ' + "after using assert.step(). Unverified steps: ".concat(stepsList), this.stack); } + if (!config._deprecated_countEachStep_shown && !config.countStepsAsOne && this.expected !== null && this.stepsCount) { + config._deprecated_countEachStep_shown = true; + if (config.requireExpects) { + Logger.warn('Counting each assert.step() for assert.expect() is changing in QUnit 3.0. You can enable QUnit.config.countStepsAsOne to prepare for the upgrade. https://qunitjs.com/api/assert/expect/'); + } else { + Logger.warn('Counting each assert.step() for assert.expect() is changing in QUnit 3.0. Omit assert.expect() from tests that use assert.step(), or enable QUnit.config.countStepsAsOne to prepare for the upgrade. https://qunitjs.com/api/assert/expect/'); + } + } + var actualCountForExpect = config.countStepsAsOne ? this.assertions.length - this.stepsCount : this.assertions.length; if (config.requireExpects && this.expected === null) { this.pushFailure('Expected number of assertions to be defined, but expect() was ' + 'not called.', this.stack); - } else if (this.expected !== null && this.expected !== this.assertions.length) { - this.pushFailure('Expected ' + this.expected + ' assertions, but ' + this.assertions.length + ' were run', this.stack); - } else if (this.expected === null && !this.assertions.length) { + } else if (this.expected !== null && this.expected !== actualCountForExpect && this.stepsCount && this.expected === this.assertions.length - this.stepsCount && !config.countStepsAsOne) { + this.pushFailure('Expected ' + this.expected + ' assertions, but ' + actualCountForExpect + ' were run\nIt looks like you might prefer to enable QUnit.config.countStepsAsOne, which will become the default in QUnit 3.0. https://qunitjs.com/api/assert/expect/', this.stack); + } else if (this.expected !== null && this.expected !== actualCountForExpect && this.stepsCount && this.expected === this.assertions.length && config.countStepsAsOne) { + this.pushFailure('Expected ' + this.expected + ' assertions, but ' + actualCountForExpect + ' were run\nRemember that with QUnit.config.countStepsAsOne and in QUnit 3.0, steps no longer count as separate assertions. https://qunitjs.com/api/assert/expect/', this.stack); + } else if (this.expected !== null && this.expected !== actualCountForExpect) { + this.pushFailure('Expected ' + this.expected + ' assertions, but ' + actualCountForExpect + ' were run', this.stack); + } else if (this.expected === null && !actualCountForExpect) { this.pushFailure('Expected at least one assertion, but none were run - call ' + 'expect(0) to accept zero assertions.', this.stack); } var module = this.module; @@ -2988,7 +3029,7 @@ // Prioritize previously failed tests, detected from storage var prioritize = config.reorder && !!previousFailCount; this.previousFailure = !!previousFailCount; - ProcessingQueue.add(runTest, prioritize, config.seed); + config.pq.add(runTest, prioritize); }, pushResult: function pushResult(resultInfo) { if (this !== config.current) { @@ -3025,14 +3066,13 @@ message: resultInfo.message }); }, - pushFailure: function pushFailure(message, source, actual) { + pushFailure: function pushFailure(message, source) { if (!(this instanceof Test)) { throw new Error('pushFailure() assertion outside test context, was ' + sourceFromStacktrace(2)); } this.pushResult({ result: false, message: message || 'error', - actual: actual || null, source: source }); }, @@ -3170,11 +3210,20 @@ pause.cancelled = true; test.pauses.delete(pauseId); test.pushFailure("Test took longer than ".concat(timeout, "ms; test timed out."), sourceFromStacktrace(2)); - internalStart(test); + internalRecover(test); }; }; clearTimeout(config.timeout); config.timeout = setTimeout$1(config.timeoutHandler(timeoutDuration), timeoutDuration); + } else { + clearTimeout(config.timeout); + config.timeout = setTimeout$1(function () { + config.timeout = null; + if (!config._deprecated_timeout_shown) { + config._deprecated_timeout_shown = true; + Logger.warn("Test \"".concat(test.testName, "\" took longer than 3000ms, but no timeout was set. Set QUnit.config.testTimeout or call assert.timeout() to avoid a timeout in QUnit 3. https://qunitjs.com/api/config/testTimeout/")); + } + }, 3000); } } return release; @@ -3242,12 +3291,32 @@ return false; } var filter = config.filter; - if (!filter) { - return true; + if (filter) { + var regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec(filter); + var fullName = this.module.name + ': ' + this.testName; + if (regexFilter) { + if (!this.regexFilter(!!regexFilter[1], regexFilter[2], regexFilter[3], fullName)) { + return false; + } + } else if (!this.stringFilter(filter, fullName)) { + return false; + } + } + if (typeof config.testFilter === 'function') { + var testInfo = { + testId: this.testId, + testName: this.testName, + module: this.module.name, + skip: !!this.skip + }; + try { + return !!config.testFilter(testInfo); + } catch (error) { + Logger.warn('Error in QUnit.config.testFilter callback: ', error); + return false; + } } - var regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec(filter); - var fullName = this.module.name + ': ' + this.testName; - return regexFilter ? this.regexFilter(!!regexFilter[1], regexFilter[2], regexFilter[3], fullName) : this.stringFilter(filter, fullName); + return true; }, regexFilter: function regexFilter(exclude, pattern, flags, fullName) { var regex = new RegExp(pattern, flags); @@ -3297,11 +3366,11 @@ function checkPollution() { var old = config.pollution; saveGlobal(); - var newGlobals = diff(config.pollution, old); + var newGlobals = diff$1(config.pollution, old); if (newGlobals.length > 0) { pushFailure('Introduced global variable(s): ' + newGlobals.join(', ')); } - var deletedGlobals = diff(old, config.pollution); + var deletedGlobals = diff$1(old, config.pollution); if (deletedGlobals.length > 0) { pushFailure('Deleted global variable(s): ' + deletedGlobals.join(', ')); } @@ -3321,6 +3390,7 @@ } if (!focused) { config.queue.length = 0; + clearSuiteReports(config.currentModule); focused = true; } var newTest = new Test(settings); @@ -3337,10 +3407,54 @@ function makeEachTestName(testName, argument) { return "".concat(testName, " [").concat(argument, "]"); } + + // Characters to avoid in test names especially CLI/AP output: + // * x00-1F: e.g. NULL, backspace (\b), line breaks (\r\n), ESC. + // * x74: DEL. + // * xA0: non-breaking space. + // + // See https://en.wikipedia.org/wiki/ASCII#Character_order + // + // eslint-disable-next-line no-control-regex + var rNonObviousStr = /[\x00-\x1F\x7F\xA0]/; function runEach(data, eachFn) { if (Array.isArray(data)) { for (var i = 0; i < data.length; i++) { - eachFn(data[i], i); + var value = data[i]; + + // Create automatic labels for primitive data in arrays passed to test.each(). + // We want to avoid the default "example [0], example [1]" where possible since + // these are not self-explanatory in results, and are also tedious to locate + // the source of since the numerical key of an array isn't literally in the + // code (you have to count). + // + // Design requirements: + // * Unique. Each label must be unique and correspond 1:1 with a data value. + // This way each test name will hash to a unique testId with Rerun link, + // without having to rely on Test class enforcing uniqueness with invisible + // space hack. + // * Unambigious. While technical uniqueness is a hard requirement above, + // we also want the labels to be obvious and unambiguous to humans. + // For example, abbrebating "foobar" and "foobaz" to "f" and "fo" is + // technically unique, but ambigious to humans which one is which. + // * Short and readable. Where possible we omit the array index numbers + // so that in most cases, the value is simply shown as-is. + // We prefer "example [foo], example [bar]" + // over "example [0: foo], example [2: bar]". + // This also has the benefit of being stable and robust against e.g. + // re-ordering data or adding new items during development, without + // invalidating a previous filter or rerun link immediately. + var valueType = _typeof(value); + var testKey = i; + if (valueType === 'string' && value.length <= 40 && !rNonObviousStr.test(value) && !/\s*\d+: /.test(value)) { + testKey = value; + } else if (valueType === 'string' || valueType === 'number' || valueType === 'boolean' || valueType === 'undefined' || value === null) { + var valueForName = String(value); + if (!rNonObviousStr.test(valueForName)) { + testKey = i + ': ' + (valueForName.length <= 30 ? valueForName : valueForName.slice(0, 29) + '…'); + } + } + eachFn(value, testKey); } } else if (_typeof(data) === 'object' && data !== null) { for (var key in data) { @@ -3364,6 +3478,13 @@ skip: true }); }, + if: function _if(testName, condition, callback) { + addTest({ + testName: testName, + callback: callback, + skip: !condition + }); + }, only: function only(testName, callback) { addOnlyTest({ testName: testName, @@ -3403,6 +3524,18 @@ }); }); }; + test.if.each = function (testName, condition, dataset, callback) { + runEach(dataset, function (data, testKey) { + addTest({ + testName: makeEachTestName(testName, testKey), + callback: callback, + withData: true, + stackOffset: 5, + skip: !condition, + data: condition ? data : undefined + }); + }); + }; test.only.each = function (testName, dataset, callback) { runEach(dataset, function (data, testKey) { addOnlyTest({ @@ -3441,11 +3574,11 @@ clearTimeout(config.timeout); config.timeout = null; config.blocking = false; - ProcessingQueue.advance(); + config.pq.advance(); }); } else { config.blocking = false; - ProcessingQueue.advance(); + config.pq.advance(); } } function collectTests(module) { @@ -3549,7 +3682,7 @@ runner.on('testEnd', this.onTestEnd.bind(this)); runner.on('runEnd', this.onRunEnd.bind(this)); } - _createClass(ConsoleReporter, [{ + return _createClass(ConsoleReporter, [{ key: "onError", value: function onError(error) { this.log('error', error); @@ -3580,7 +3713,88 @@ return new ConsoleReporter(runner, options); } }]); - return ConsoleReporter; + }(); + + // TODO: Consider using globalThis instead of window, so that the reporter + // works for Node.js as well. As this can add overhead, we should make + // this opt-in before we enable it for CLI. + // + // QUnit 3 will switch from `window` to `globalThis` and then make it + // no longer an implicit feature of the HTML Reporter, but rather let + // it be opt-in via `QUnit.config.reporters = ['perf']` or something + // like that. + var nativePerf = window$1 && typeof window$1.performance !== 'undefined' && + // eslint-disable-next-line compat/compat -- Checked + typeof window$1.performance.mark === 'function' && + // eslint-disable-next-line compat/compat -- Checked + typeof window$1.performance.measure === 'function' ? window$1.performance : undefined; + var perf = { + measure: nativePerf ? function (comment, startMark, endMark) { + // `performance.measure` may fail if the mark could not be found. + // reasons a specific mark could not be found include: outside code invoking `performance.clearMarks()` + try { + nativePerf.measure(comment, startMark, endMark); + } catch (ex) { + Logger.warn('performance.measure could not be executed because of ', ex.message); + } + } : function () {}, + mark: nativePerf ? nativePerf.mark.bind(nativePerf) : function () {} + }; + var PerfReporter = /*#__PURE__*/function () { + function PerfReporter(runner) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + _classCallCheck(this, PerfReporter); + this.perf = options.perf || perf; + runner.on('runStart', this.onRunStart.bind(this)); + runner.on('runEnd', this.onRunEnd.bind(this)); + runner.on('suiteStart', this.onSuiteStart.bind(this)); + runner.on('suiteEnd', this.onSuiteEnd.bind(this)); + runner.on('testStart', this.onTestStart.bind(this)); + runner.on('testEnd', this.onTestEnd.bind(this)); + } + return _createClass(PerfReporter, [{ + key: "onRunStart", + value: function onRunStart() { + this.perf.mark('qunit_suite_0_start'); + } + }, { + key: "onSuiteStart", + value: function onSuiteStart(suiteStart) { + var suiteLevel = suiteStart.fullName.length; + this.perf.mark("qunit_suite_".concat(suiteLevel, "_start")); + } + }, { + key: "onSuiteEnd", + value: function onSuiteEnd(suiteEnd) { + var suiteLevel = suiteEnd.fullName.length; + var suiteName = suiteEnd.fullName.join(' – '); + this.perf.mark("qunit_suite_".concat(suiteLevel, "_end")); + this.perf.measure("QUnit Test Suite: ".concat(suiteName), "qunit_suite_".concat(suiteLevel, "_start"), "qunit_suite_".concat(suiteLevel, "_end")); + } + }, { + key: "onTestStart", + value: function onTestStart() { + this.perf.mark('qunit_test_start'); + } + }, { + key: "onTestEnd", + value: function onTestEnd(testEnd) { + this.perf.mark('qunit_test_end'); + var testName = testEnd.fullName.join(' – '); + this.perf.measure("QUnit Test: ".concat(testName), 'qunit_test_start', 'qunit_test_end'); + } + }, { + key: "onRunEnd", + value: function onRunEnd() { + this.perf.mark('qunit_suite_0_end'); + this.perf.measure('QUnit Test Run', 'qunit_suite_0_start', 'qunit_suite_0_end'); + } + }], [{ + key: "init", + value: function init(runner, options) { + return new PerfReporter(runner, options); + } + }]); }(); var FORCE_COLOR, @@ -3691,8 +3905,6 @@ }; } - var hasOwn = Object.prototype.hasOwnProperty; - /** * Format a given value into YAML. * @@ -3726,7 +3938,7 @@ * "[Circular]" as they cannot otherwise be represented. */ function prettyYamlValue(value) { - var indent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 4; + var indent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2; if (value === undefined) { // Not supported in JSON/YAML, turn into string // and let the below output it as bare string. @@ -3750,7 +3962,10 @@ // cause data loss or invalid YAML syntax. // // - Quotes, escapes, line breaks, or JSON-like stuff. - var rSpecialJson = /['"\\/[{}\]\r\n]/; + // - Not allowed in YAML unquoted strings per https://yaml.org/spec/1.2.2/#733-plain-style + // * ": " (colon followed by space) + // * " #" (space followed by hash) + var rSpecialJson = /['"\\/[{}\]\r\n|:#]/; // - Characters that are special at the start of a YAML value var rSpecialYaml = /[-?:,[\]{}#&*!|=>'"%@`]/; @@ -3776,7 +3991,7 @@ // See also // Support IE 9-11: Avoid ES6 String#repeat - var prefix = new Array(indent + 1).join(' '); + var _prefix = new Array(indent * 2 + 1).join(' '); var trailingLinebreakMatch = value.match(/\n+$/); var trailingLinebreaks = trailingLinebreakMatch ? trailingLinebreakMatch[0].length : 0; if (trailingLinebreaks === 1) { @@ -3787,14 +4002,14 @@ // Ignore the last new line, since we'll get that one for free // with the straight-forward Block syntax. .replace(/\n$/, '').split('\n').map(function (line) { - return prefix + line; + return _prefix + line; }); return '|\n' + lines.join('\n'); } else { // This has either no trailing new lines, or more than 1. // Use |+ so that YAML parsers will preserve it exactly. var _lines = value.split('\n').map(function (line) { - return prefix + line; + return _prefix + line; }); return '|+\n' + _lines.join('\n'); } @@ -3803,9 +4018,12 @@ return value; } } + var prefix = new Array(indent + 1).join(' '); // Handle null, boolean, array, and object - return JSON.stringify(decycledShallowClone(value), null, 2); + return JSON.stringify(decycledShallowClone(value), null, 2).split('\n').map(function (line, i) { + return i === 0 ? line : prefix + line; + }).join('\n'); } /** @@ -3850,6 +4068,7 @@ // Support IE 9: Function#bind is supported, but no console.log.bind(). this.log = options.log || Function.prototype.bind.call(console$1.log, console$1); this.testCount = 0; + this.started = false; this.ended = false; this.bailed = false; runner.on('error', this.onError.bind(this)); @@ -3857,10 +4076,13 @@ runner.on('testEnd', this.onTestEnd.bind(this)); runner.on('runEnd', this.onRunEnd.bind(this)); } - _createClass(TapReporter, [{ + return _createClass(TapReporter, [{ key: "onRunStart", value: function onRunStart(_runSuite) { - this.log('TAP version 13'); + if (!this.started) { + this.log('TAP version 13'); + this.started = true; + } } }, { key: "onError", @@ -3873,8 +4095,9 @@ // Imitate onTestEnd // Skip this if we're past "runEnd" as it would look odd if (!this.ended) { + this.onRunStart(); this.testCount = this.testCount + 1; - this.log($.red("not ok ".concat(this.testCount, " global failure"))); + this.log("not ok ".concat(this.testCount, " ").concat($.red('global failure'))); this.logError(error); } this.log('Bail out! ' + errorString(error).split('\n')[0]); @@ -3890,14 +4113,14 @@ if (test.status === 'passed') { this.log("ok ".concat(this.testCount, " ").concat(test.fullName.join(' > '))); } else if (test.status === 'skipped') { - this.log($.yellow("ok ".concat(this.testCount, " # SKIP ").concat(test.fullName.join(' > ')))); + this.log("ok ".concat(this.testCount, " ").concat($.yellow(test.fullName.join(' > ')), " # SKIP")); } else if (test.status === 'todo') { - this.log($.cyan("not ok ".concat(this.testCount, " # TODO ").concat(test.fullName.join(' > ')))); + this.log("not ok ".concat(this.testCount, " ").concat($.cyan(test.fullName.join(' > ')), " # TODO")); test.errors.forEach(function (error) { return _this.logAssertion(error, 'todo'); }); } else { - this.log($.red("not ok ".concat(this.testCount, " ").concat(test.fullName.join(' > ')))); + this.log("not ok ".concat(this.testCount, " ").concat($.red(test.fullName.join(' > ')))); test.errors.forEach(function (error) { return _this.logAssertion(error); }); @@ -3905,13 +4128,13 @@ } }, { key: "onRunEnd", - value: function onRunEnd(runSuite) { + value: function onRunEnd(runEnd) { this.ended = true; - this.log("1..".concat(runSuite.testCounts.total)); - this.log("# pass ".concat(runSuite.testCounts.passed)); - this.log($.yellow("# skip ".concat(runSuite.testCounts.skipped))); - this.log($.cyan("# todo ".concat(runSuite.testCounts.todo))); - this.log($.red("# fail ".concat(runSuite.testCounts.failed))); + this.log("1..".concat(runEnd.testCounts.total)); + this.log("# pass ".concat(runEnd.testCounts.passed)); + this.log("# ".concat($.yellow("skip ".concat(runEnd.testCounts.skipped)))); + this.log("# ".concat($.cyan("todo ".concat(runEnd.testCounts.todo)))); + this.log("# ".concat($.red("fail ".concat(runEnd.testCounts.failed)))); } }, { key: "logAssertion", @@ -3919,16 +4142,22 @@ var out = ' ---'; out += "\n message: ".concat(prettyYamlValue(error.message || 'failed')); out += "\n severity: ".concat(prettyYamlValue(severity || 'failed')); - if (hasOwn.call(error, 'actual')) { + + // When pushFailure() is used, actual/expected are initially unset but + // eventually in Test#logAssertion, for testReport#pushAssertion, these are + // forged into existence as undefined. + var hasAny = error.expected !== undefined || error.actual !== undefined; + if (hasAny) { out += "\n actual : ".concat(prettyYamlValue(error.actual)); - } - if (hasOwn.call(error, 'expected')) { out += "\n expected: ".concat(prettyYamlValue(error.expected)); } if (error.stack) { // Since stacks aren't user generated, take a bit of liberty by // adding a trailing new line to allow a straight-forward YAML Blocks. - out += "\n stack: ".concat(prettyYamlValue(error.stack + '\n')); + var fmtStack = annotateStacktrace(error.stack, $.grey); + if (fmtStack.length) { + out += "\n stack: ".concat(prettyYamlValue(fmtStack + '\n')); + } } out += '\n ...'; this.log(out); @@ -3940,7 +4169,10 @@ out += "\n message: ".concat(prettyYamlValue(errorString(error))); out += "\n severity: ".concat(prettyYamlValue('failed')); if (error && error.stack) { - out += "\n stack: ".concat(prettyYamlValue(error.stack + '\n')); + var fmtStack = annotateStacktrace(error.stack, $.grey, error.toString()); + if (fmtStack.length) { + out += "\n stack: ".concat(prettyYamlValue(fmtStack + '\n')); + } } out += '\n ...'; this.log(out); @@ -3951,11 +4183,11 @@ return new TapReporter(runner, options); } }]); - return TapReporter; }(); var reporters = { console: ConsoleReporter, + perf: PerfReporter, tap: TapReporter }; @@ -3973,3041 +4205,3257 @@ }; /** - * Handle a global error that should result in a failed test run. - * - * Summary: - * - * - If we're strictly inside a test (or one if its module hooks), the exception - * becomes a failed assertion. - * - * This has the important side-effect that uncaught exceptions (such as - * calling an undefined function) during a "todo" test do NOT result in - * a failed test run. - * - * - If we're anywhere outside a test (be it in early event callbacks, or - * internally between tests, or somewhere after "runEnd" if the process is - * still alive for some reason), then send an "error" event to the reporters. - * - * @since 2.17.0 - * @param {Error|any} error + * Creates a seeded "sample" generator which is used for randomizing tests. */ - function onUncaughtException(error) { - if (config.current) { - config.current.assert.pushResult({ - result: false, - message: "global failure: ".concat(errorString(error)), - // We could let callers specify an offset to subtract a number of frames via - // sourceFromStacktrace, in case they are a wrapper further away from the error - // handler, and thus reduce some noise in the stack trace. However, we're not - // doing this right now because it would almost never be used in practice given - // the vast majority of error values will be Error objects, and thus have their - // own stack trace already. - source: error && error.stack || sourceFromStacktrace(2) - }); - } else { - // The "error" event was added in QUnit 2.17. - // Increase "bad assertion" stats despite no longer pushing an assertion in this case. - // This ensures "runEnd" and "QUnit.done()" handlers behave as expected, since the "bad" - // count is typically how reporters decide on the boolean outcome of the test run. - runSuite.globalFailureCount++; - config.stats.bad++; - config.stats.all++; - emit('error', error); - } + function unitSamplerGenerator(seed) { + // 32-bit xorshift, requires only a nonzero seed + // https://excamera.com/sphinx/article-xorshift.html + var sample = parseInt(generateHash(seed), 16) || -1; + return function () { + sample ^= sample << 13; + sample ^= sample >>> 17; + sample ^= sample << 5; + + // ECMAScript has no unsigned number type + if (sample < 0) { + sample += 0x100000000; + } + return sample / 0x100000000; + }; } + var ProcessingQueue = /*#__PURE__*/function () { + /** + * @param {Function} test Reference to the QUnit.test() method + */ + function ProcessingQueue(test) { + _classCallCheck(this, ProcessingQueue); + this.test = test; + this.priorityCount = 0; + this.unitSampler = null; - /** - * Handle a window.onerror error. - * - * If there is a current test that sets the internal `ignoreGlobalErrors` field - * (such as during `assert.throws()`), then the error is ignored and native - * error reporting is suppressed as well. This is because in browsers, an error - * can sometimes end up in `window.onerror` instead of in the local try/catch. - * This ignoring of errors does not apply to our general onUncaughtException - * method, nor to our `unhandledRejection` handlers, as those are not meant - * to receive an "expected" error during `assert.throws()`. - * - * @see - * @deprecated since 2.17.0 Use QUnit.onUncaughtException instead. - * @param {Object} details - * @param {string} details.message - * @param {string} details.fileName - * @param {number} details.lineNumber - * @param {string|undefined} [details.stacktrace] - * @return {bool} True if native error reporting should be suppressed. - */ - function onWindowError(details) { - Logger.warn('QUnit.onError is deprecated and will be removed in QUnit 3.0.' + ' Please use QUnit.onUncaughtException instead.'); - if (config.current && config.current.ignoreGlobalErrors) { - return true; + // This is a queue of functions that are tasks within a single test. + // After tests are dequeued from config.queue they are expanded into + // a set of tasks in this queue. + this.taskQueue = []; + this.finished = false; } - var err = new Error(details.message); - err.stack = details.stacktrace || details.fileName + ':' + details.lineNumber; - onUncaughtException(err); - return false; - } - - var QUnit = {}; - - // The "currentModule" object would ideally be defined using the createModule() - // function. Since it isn't, add the missing suiteReport property to it now that - // we have loaded all source code required to do so. - // - // TODO: Consider defining currentModule in core.js or module.js in its entirely - // rather than partly in config.js and partly here. - config.currentModule.suiteReport = runSuite; - var globalStartCalled = false; - var runStarted = false; - // Figure out if we're running the tests from a server or not - QUnit.isLocal = window$1 && window$1.location && window$1.location.protocol === 'file:'; + /** + * Advances the taskQueue to the next task. If the taskQueue is empty, + * process the testQueue + */ + return _createClass(ProcessingQueue, [{ + key: "advance", + value: function advance() { + this.advanceTaskQueue(); + if (!this.taskQueue.length && !config.blocking && !config.current) { + this.advanceTestQueue(); + } + } - // Expose the current QUnit version - QUnit.version = '2.19.4'; - extend(QUnit, { - config: config, - dump: dump, - equiv: equiv, - reporters: reporters, - hooks: hooks, - is: is, - objectType: objectType, - on: on, - onError: onWindowError, - onUncaughtException: onUncaughtException, - pushFailure: pushFailure, - assert: Assert.prototype, - module: module$1, - test: test, - // alias other test flavors for easy access - todo: test.todo, - skip: test.skip, - only: test.only, - start: function start(count) { - if (config.current) { - throw new Error('QUnit.start cannot be called inside a test context.'); + /** + * Advances the taskQueue with an increased depth + */ + }, { + key: "advanceTaskQueue", + value: function advanceTaskQueue() { + var start = performance.now(); + config.depth = (config.depth || 0) + 1; + this.processTaskQueue(start); + config.depth--; } - var globalStartAlreadyCalled = globalStartCalled; - globalStartCalled = true; - if (runStarted) { - throw new Error('Called start() while test already started running'); + + /** + * Process the first task on the taskQueue as a promise. + * Each task is a function added by Test#queue() in /src/test.js + */ + }, { + key: "processTaskQueue", + value: function processTaskQueue(start) { + var _this = this; + if (this.taskQueue.length && !config.blocking) { + var elapsedTime = performance.now() - start; + if (!setTimeout$1 || config.updateRate <= 0 || elapsedTime < config.updateRate) { + var task = this.taskQueue.shift(); + _Promise.resolve(task()).then(function () { + if (!_this.taskQueue.length) { + _this.advance(); + } else { + _this.processTaskQueue(start); + } + }); + } else { + setTimeout$1(function () { + _this.advance(); + }); + } + } } - if (globalStartAlreadyCalled || count > 1) { - throw new Error('Called start() outside of a test context too many times'); + + /** + * Advance the testQueue to the next test to process. Call done() if testQueue completes. + */ + }, { + key: "advanceTestQueue", + value: function advanceTestQueue() { + if (!config.blocking && !config.queue.length && config.depth === 0) { + this.done(); + return; + } + var testTasks = config.queue.shift(); + this.addToTaskQueue(testTasks()); + if (this.priorityCount > 0) { + this.priorityCount--; + } + this.advance(); } - if (config.autostart) { - throw new Error('Called start() outside of a test context when ' + 'QUnit.config.autostart was true'); + + /** + * Enqueue the tasks for a test into the task queue. + * @param {Array} tasksArray + */ + }, { + key: "addToTaskQueue", + value: function addToTaskQueue(tasksArray) { + var _this$taskQueue; + (_this$taskQueue = this.taskQueue).push.apply(_this$taskQueue, _toConsumableArray(tasksArray)); } - if (!config.pageLoaded) { - // The page isn't completely loaded yet, so we set autostart and then - // load if we're in Node or wait for the browser's load event. - config.autostart = true; - // Starts from Node even if .load was not previously called. We still return - // early otherwise we'll wind up "beginning" twice. - if (!document) { - QUnit.load(); - } - return; + /** + * Return the number of tasks remaining in the task queue to be processed. + * @return {number} + */ + }, { + key: "taskCount", + value: function taskCount() { + return this.taskQueue.length; } - scheduleBegin(); - }, - onUnhandledRejection: function onUnhandledRejection(reason) { - Logger.warn('QUnit.onUnhandledRejection is deprecated and will be removed in QUnit 3.0.' + ' Please use QUnit.onUncaughtException instead.'); - onUncaughtException(reason); - }, - extend: function extend$1() { - Logger.warn('QUnit.extend is deprecated and will be removed in QUnit 3.0.' + ' Please use Object.assign instead.'); - // delegate to utility implementation, which does not warn and can be used elsewhere internally - for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; + /** + * Adds a test to the TestQueue for execution. + * @param {Function} testTasksFunc + * @param {boolean} prioritize + */ + }, { + key: "add", + value: function add(testTasksFunc, prioritize) { + if (prioritize) { + config.queue.splice(this.priorityCount++, 0, testTasksFunc); + } else if (config.seed) { + if (!this.unitSampler) { + this.unitSampler = unitSamplerGenerator(config.seed); + } + + // Insert into a random position after all prioritized items + var index = Math.floor(this.unitSampler() * (config.queue.length - this.priorityCount + 1)); + config.queue.splice(this.priorityCount + index, 0, testTasksFunc); + } else { + config.queue.push(testTasksFunc); + } } - return extend.apply(this, args); - }, - load: function load() { - config.pageLoaded = true; - // Initialize the configuration options - extend(config, { - started: 0, - updateRate: 1000, - autostart: true, - filter: '' - }, true); - if (!runStarted) { - config.blocking = false; - if (config.autostart) { - scheduleBegin(); + /** + * This function is called when the ProcessingQueue is done processing all + * items. It handles emitting the final run events. + */ + }, { + key: "done", + value: function done() { + // We have reached the end of the processing queue and are about to emit the + // "runEnd" event after which reporters typically stop listening and exit + // the process. First, check if we need to emit one final test. + if (config.stats.testCount === 0 && config.failOnZeroTests === true) { + var error; + if (config.filter && config.filter.length) { + error = new Error("No tests matched the filter \"".concat(config.filter, "\".")); + } else if (config.module && config.module.length) { + error = new Error("No tests matched the module \"".concat(config.module, "\".")); + } else if (config.moduleId && config.moduleId.length) { + error = new Error("No tests matched the moduleId \"".concat(config.moduleId, "\".")); + } else if (config.testId && config.testId.length) { + error = new Error("No tests matched the testId \"".concat(config.testId, "\".")); + } else { + error = new Error('No tests were run.'); + } + this.test('global failure', extend(function (assert) { + assert.pushResult({ + result: false, + message: error.message, + source: error.stack + }); + }, { + validTest: true + })); + + // We do need to call `advance()` in order to resume the processing queue. + // Once this new test is finished processing, we'll reach `done` again, and + // that time the above condition will evaluate to false. + this.advance(); + return; } + var storage = config.storage; + var runtime = Math.round(performance.now() - config.started); + var passed = config.stats.all - config.stats.bad; + this.finished = true; + emit('runEnd', runSuite.end(true)); + runLoggingCallbacks('done', { + // @deprecated since 2.19.0 Use done() without `details` parameter, + // or use `QUnit.on('runEnd')` instead. Parameter to be replaced in + // QUnit 3.0 with test counts. + passed: passed, + failed: config.stats.bad, + total: config.stats.all, + runtime: runtime + }).then(function () { + // Clear own storage items if all tests passed + if (storage && config.stats.bad === 0) { + for (var i = storage.length - 1; i >= 0; i--) { + var key = storage.key(i); + if (key.indexOf('qunit-test-') === 0) { + storage.removeItem(key); + } + } + } + }); } - }, - stack: function stack(offset) { - offset = (offset || 0) + 2; - return sourceFromStacktrace(offset); - } - }); - registerLoggingCallbacks(QUnit); - function scheduleBegin() { - runStarted = true; + }]); + }(); - // Add a slight delay to allow definition of more modules and tests. - if (setTimeout$1) { - setTimeout$1(function () { - begin(); + /** + * Handle a global error that should result in a failed test run. + * + * Summary: + * + * - If we're strictly inside a test (or one if its module hooks), the exception + * becomes a failed assertion. + * + * This has the important side-effect that uncaught exceptions (such as + * calling an undefined function) during a "todo" test do NOT result in + * a failed test run. + * + * - If we're anywhere outside a test (be it in early event callbacks, or + * internally between tests, or somewhere after "runEnd" if the process is + * still alive for some reason), then send an "error" event to the reporters. + * + * @since 2.17.0 + * @param {Error|any} error + */ + function onUncaughtException(error) { + if (config.current) { + // This omits 'actual' and 'expected' (undefined) + config.current.assert.pushResult({ + result: false, + message: "global failure: ".concat(errorString(error)), + // We could let callers specify an offset to subtract a number of frames via + // sourceFromStacktrace, in case they are a wrapper further away from the error + // handler, and thus reduce some noise in the stack trace. However, we're not + // doing this right now because it would almost never be used in practice given + // the vast majority of error values will be Error objects, and thus have their + // own stack trace already. + source: error && error.stack || sourceFromStacktrace(2) }); } else { - begin(); + // The "error" event was added in QUnit 2.17. + // Increase "bad assertion" stats despite no longer pushing an assertion in this case. + // This ensures "runEnd" and "QUnit.done()" handlers behave as expected, since the "bad" + // count is typically how reporters decide on the boolean outcome of the test run. + runSuite.globalFailureCount++; + config.stats.bad++; + config.stats.all++; + emit('error', error); } } - function unblockAndAdvanceQueue() { - config.blocking = false; - ProcessingQueue.advance(); - } - function begin() { - if (config.started) { - unblockAndAdvanceQueue(); - return; - } - - // The test run hasn't officially begun yet - // Record the time of the test run's beginning - config.started = performance.now(); - // Delete the loose unnamed module if unused. - if (config.modules[0].name === '' && config.modules[0].tests.length === 0) { - config.modules.shift(); - } - var modulesLog = []; - for (var i = 0; i < config.modules.length; i++) { - // Don't expose the unnamed global test module to plugins. - if (config.modules[i].name !== '') { - modulesLog.push({ - name: config.modules[i].name, - moduleId: config.modules[i].moduleId, - // Added in QUnit 1.16.0 for internal use by html-reporter, - // but no longer used since QUnit 2.7.0. - // @deprecated Kept unofficially to be removed in QUnit 3.0. - tests: config.modules[i].tests - }); - } + /** + * Handle a window.onerror error. + * + * If there is a current test that sets the internal `ignoreGlobalErrors` field + * (such as during `assert.throws()`), then the error is ignored and native + * error reporting is suppressed as well. This is because in browsers, an error + * can sometimes end up in `window.onerror` instead of in the local try/catch. + * This ignoring of errors does not apply to our general onUncaughtException + * method, nor to our `unhandledRejection` handlers, as those are not meant + * to receive an "expected" error during `assert.throws()`. + * + * @see + * @deprecated since 2.17.0 Use QUnit.onUncaughtException instead. + * @param {Object} details + * @param {string} details.message + * @param {string} details.fileName + * @param {number} details.lineNumber + * @param {string|undefined} [details.stacktrace] + * @return {bool} True if native error reporting should be suppressed. + */ + function onWindowError(details) { + Logger.warn('QUnit.onError is deprecated and will be removed in QUnit 3.0.' + ' Please use QUnit.onUncaughtException instead.'); + if (config.current && config.current.ignoreGlobalErrors) { + return true; } - - // The test run is officially beginning now - emit('runStart', runSuite.start(true)); - runLoggingCallbacks('begin', { - totalTests: Test.count, - modules: modulesLog - }).then(unblockAndAdvanceQueue); + var err = new Error(details.message); + err.stack = details.stacktrace || details.fileName + ':' + details.lineNumber; + onUncaughtException(err); + return false; } - exportQUnit(QUnit); - (function () { - if (!window$1 || !document) { - return; - } - var config = QUnit.config; - var hasOwn = Object.prototype.hasOwnProperty; + /* eslint-disable indent */ - // Stores fixture HTML for resetting later - function storeFixture() { - // Avoid overwriting user-defined values - if (hasOwn.call(config, 'fixture')) { - return; - } - var fixture = document.getElementById('qunit-fixture'); - if (fixture) { - config.fixture = fixture.cloneNode(true); - } + /* + * This file is a modified version of google-diff-match-patch's JavaScript implementation + * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), + * modifications are licensed as more fully set forth in LICENSE.txt. + * + * The original source of google-diff-match-patch is attributable and licensed as follows: + * + * Copyright 2006 Google Inc. + * https://code.google.com/p/google-diff-match-patch/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * More Info: + * https://code.google.com/p/google-diff-match-patch/ + * + * Usage: QUnit.diff(expected, actual) + * + */ + function DiffMatchPatch() {} + + // DIFF FUNCTIONS + + /** + * The data structure representing a diff is an array of tuples: + * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] + * which means: delete 'Hello', add 'Goodbye' and keep ' world.' + */ + var DIFF_DELETE = -1; + var DIFF_INSERT = 1; + var DIFF_EQUAL = 0; + var hasOwn = Object.prototype.hasOwnProperty; + + /** + * Find the differences between two texts. Simplifies the problem by stripping + * any common prefix or suffix off the texts before diffing. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean=} optChecklines Optional speedup flag. If present and false, + * then don't run a line-level diff first to identify the changed areas. + * Defaults to true, which does a faster, slightly less optimal diff. + * @return {!Array.} Array of diff tuples. + */ + DiffMatchPatch.prototype.DiffMain = function (text1, text2, optChecklines) { + // The diff must be complete in up to 1 second. + var deadline = Date.now() + 1000; + + // Check for null inputs. + if (text1 === null || text2 === null) { + throw new Error('Cannot diff null input.'); } - QUnit.begin(storeFixture); - // Resets the fixture DOM element if available. - function resetFixture() { - if (config.fixture == null) { - return; - } - var fixture = document.getElementById('qunit-fixture'); - var resetFixtureType = _typeof(config.fixture); - if (resetFixtureType === 'string') { - // support user defined values for `config.fixture` - var newFixture = document.createElement('div'); - newFixture.setAttribute('id', 'qunit-fixture'); - newFixture.innerHTML = config.fixture; - fixture.parentNode.replaceChild(newFixture, fixture); - } else { - var clonedFixture = config.fixture.cloneNode(true); - fixture.parentNode.replaceChild(clonedFixture, fixture); + // Check for equality (speedup). + if (text1 === text2) { + if (text1) { + return [[DIFF_EQUAL, text1]]; } + return []; } - QUnit.testStart(resetFixture); - })(); - - (function () { - // Only interact with URLs via window.location - var location = typeof window$1 !== 'undefined' && window$1.location; - if (!location) { - return; + if (typeof optChecklines === 'undefined') { + optChecklines = true; } - var urlParams = getUrlParams(); - QUnit.urlParams = urlParams; - QUnit.config.filter = urlParams.filter; - QUnit.config.module = urlParams.module; - QUnit.config.moduleId = [].concat(urlParams.moduleId || []); - QUnit.config.testId = [].concat(urlParams.testId || []); - // Test order randomization - if (urlParams.seed === true) { - // Generate a random seed if the option is specified without a value - QUnit.config.seed = Math.random().toString(36).slice(2); - } else if (urlParams.seed) { - QUnit.config.seed = urlParams.seed; + // Trim off common prefix (speedup). + var commonlength = this.diffCommonPrefix(text1, text2); + var commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = this.diffCommonSuffix(text1, text2); + var commonsuffix = text1.substring(text1.length - commonlength); + text1 = text1.substring(0, text1.length - commonlength); + text2 = text2.substring(0, text2.length - commonlength); + + // Compute the diff on the middle block. + var diffs = this.diffCompute(text1, text2, optChecklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix) { + diffs.unshift([DIFF_EQUAL, commonprefix]); } + if (commonsuffix) { + diffs.push([DIFF_EQUAL, commonsuffix]); + } + this.diffCleanupMerge(diffs); + return diffs; + }; - // Add URL-parameter-mapped config values with UI form rendering data - QUnit.config.urlConfig.push({ - id: 'hidepassed', - label: 'Hide passed tests', - tooltip: 'Only show tests and assertions that fail. Stored as query-strings.' - }, { - id: 'noglobals', - label: 'Check for Globals', - tooltip: 'Enabling this will test if any test introduces new properties on the ' + 'global object (`window` in Browsers). Stored as query-strings.' - }, { - id: 'notrycatch', - label: 'No try-catch', - tooltip: 'Enabling this will run tests outside of a try-catch block. Makes debugging ' + 'exceptions in IE reasonable. Stored as query-strings.' - }); - QUnit.begin(function () { - var urlConfig = QUnit.config.urlConfig; - for (var i = 0; i < urlConfig.length; i++) { - // Options can be either strings or objects with nonempty "id" properties - var option = QUnit.config.urlConfig[i]; - if (typeof option !== 'string') { - option = option.id; - } - if (QUnit.config[option] === undefined) { - QUnit.config[option] = urlParams[option]; + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupEfficiency = function (diffs) { + var changes, equalities, equalitiesLength, lastequality, pointer, preIns, preDel, postIns, postDel; + changes = false; + equalities = []; // Stack of indices where equalities are found. + equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + lastequality = null; + + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + pointer = 0; // Index of current position. + + // Is there an insertion operation before the last equality. + preIns = false; + + // Is there a deletion operation before the last equality. + preDel = false; + + // Is there an insertion operation after the last equality. + postIns = false; + + // Is there a deletion operation after the last equality. + postDel = false; + while (pointer < diffs.length) { + // Equality found. + if (diffs[pointer][0] === DIFF_EQUAL) { + if (diffs[pointer][1].length < 4 && (postIns || postDel)) { + // Candidate found. + equalities[equalitiesLength++] = pointer; + preIns = postIns; + preDel = postDel; + lastequality = diffs[pointer][1]; + } else { + // Not a candidate, and can never become one. + equalitiesLength = 0; + lastequality = null; } - } - }); - function getUrlParams() { - var urlParams = Object.create(null); - var params = location.search.slice(1).split('&'); - var length = params.length; - for (var i = 0; i < length; i++) { - if (params[i]) { - var param = params[i].split('='); - var name = decodeQueryParam(param[0]); + postIns = postDel = false; - // Allow just a key to turn on a flag, e.g., test.html?noglobals - var value = param.length === 1 || decodeQueryParam(param.slice(1).join('=')); - if (name in urlParams) { - urlParams[name] = [].concat(urlParams[name], value); + // An insertion or deletion. + } else { + if (diffs[pointer][0] === DIFF_DELETE) { + postDel = true; + } else { + postIns = true; + } + + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (lastequality && (preIns && preDel && postIns && postDel || lastequality.length < 2 && preIns + preDel + postIns + postDel === 3)) { + // Duplicate record. + diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]); + + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; + equalitiesLength--; // Throw away the equality we just deleted; + lastequality = null; + if (preIns && preDel) { + // No changes made which could affect previous entry, keep going. + postIns = postDel = true; + equalitiesLength = 0; } else { - urlParams[name] = value; + equalitiesLength--; // Throw away the previous equality. + pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; + postIns = postDel = false; } + changes = true; } } - return urlParams; + pointer++; } - function decodeQueryParam(param) { - return decodeURIComponent(param.replace(/\+/g, '%20')); + if (changes) { + this.diffCleanupMerge(diffs); } - })(); + }; - var fuzzysort$1 = {exports: {}}; + /** + * Convert a diff array into a pretty HTML report. + * @param {!Array.} diffs Array of diff tuples. + * @param {integer} string to be beautified. + * @return {string} HTML representation. + */ + DiffMatchPatch.prototype.diffPrettyHtml = function (diffs) { + var html = []; + for (var x = 0; x < diffs.length; x++) { + var op = diffs[x][0]; // Operation (insert, delete, equal) + var data = diffs[x][1]; // Text of change. + switch (op) { + case DIFF_INSERT: + html[x] = '' + escapeText(data) + ''; + break; + case DIFF_DELETE: + html[x] = '' + escapeText(data) + ''; + break; + case DIFF_EQUAL: + html[x] = '' + escapeText(data) + ''; + break; + } + } + return html.join(''); + }; - (function (module) { - (function (root, UMD) { - if (module.exports) module.exports = UMD();else root.fuzzysort = UMD(); - })(commonjsGlobal, function UMD() { - function fuzzysortNew(instanceOptions) { - var fuzzysort = { - single: function single(search, target, options) { - if (search == 'farzher') return { - target: "farzher was here (^-^*)/", - score: 0, - indexes: [0, 1, 2, 3, 4, 5, 6] - }; - if (!search) return null; - if (!isObj(search)) search = fuzzysort.getPreparedSearch(search); - if (!target) return null; - if (!isObj(target)) target = fuzzysort.getPrepared(target); - var allowTypo = options && options.allowTypo !== undefined ? options.allowTypo : instanceOptions && instanceOptions.allowTypo !== undefined ? instanceOptions.allowTypo : true; - var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo; - return algorithm(search, target, search[0]); - }, - go: function go(search, targets, options) { - if (search == 'farzher') return [{ - target: "farzher was here (^-^*)/", - score: 0, - indexes: [0, 1, 2, 3, 4, 5, 6], - obj: targets ? targets[0] : null - }]; - if (!search) return noResults; - search = fuzzysort.prepareSearch(search); - var searchLowerCode = search[0]; - var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991; - var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991; - var allowTypo = options && options.allowTypo !== undefined ? options.allowTypo : instanceOptions && instanceOptions.allowTypo !== undefined ? instanceOptions.allowTypo : true; - var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo; - var resultsLen = 0; - var limitedCount = 0; - var targetsLen = targets.length; + /** + * Determine the common prefix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the start of each + * string. + */ + DiffMatchPatch.prototype.diffCommonPrefix = function (text1, text2) { + var pointermid, pointermax, pointermin, pointerstart; + + // Quick check for common null cases. + if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) { + return 0; + } + + // Binary search. + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + pointermin = 0; + pointermax = Math.min(text1.length, text2.length); + pointermid = pointermax; + pointerstart = 0; + while (pointermin < pointermid) { + if (text1.substring(pointerstart, pointermid) === text2.substring(pointerstart, pointermid)) { + pointermin = pointermid; + pointerstart = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + return pointermid; + }; - // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys] + /** + * Determine the common suffix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of each string. + */ + DiffMatchPatch.prototype.diffCommonSuffix = function (text1, text2) { + var pointermid, pointermax, pointermin, pointerend; + + // Quick check for common null cases. + if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { + return 0; + } + + // Binary search. + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + pointermin = 0; + pointermax = Math.min(text1.length, text2.length); + pointermid = pointermax; + pointerend = 0; + while (pointermin < pointermid) { + if (text1.substring(text1.length - pointermid, text1.length - pointerend) === text2.substring(text2.length - pointermid, text2.length - pointerend)) { + pointermin = pointermid; + pointerend = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + return pointermid; + }; - // options.keys - if (options && options.keys) { - var scoreFn = options.scoreFn || defaultScoreFn; - var keys = options.keys; - var keysLen = keys.length; - for (var i = targetsLen - 1; i >= 0; --i) { - var obj = targets[i]; - var objResults = new Array(keysLen); - for (var keyI = keysLen - 1; keyI >= 0; --keyI) { - var key = keys[keyI]; - var target = getValue(obj, key); - if (!target) { - objResults[keyI] = null; - continue; - } - if (!isObj(target)) target = fuzzysort.getPrepared(target); - objResults[keyI] = algorithm(search, target, searchLowerCode); - } - objResults.obj = obj; // before scoreFn so scoreFn can use it - var score = scoreFn(objResults); - if (score === null) continue; - if (score < threshold) continue; - objResults.score = score; - if (resultsLen < limit) { - q.add(objResults); - ++resultsLen; - } else { - ++limitedCount; - if (score > q.peek().score) q.replaceTop(objResults); - } - } + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean} checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster, slightly less optimal diff. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffCompute = function (text1, text2, checklines, deadline) { + var diffs, longtext, shorttext, i, hm, text1A, text2A, text1B, text2B, midCommon, diffsA, diffsB; + if (!text1) { + // Just add some text (speedup). + return [[DIFF_INSERT, text2]]; + } + if (!text2) { + // Just delete some text (speedup). + return [[DIFF_DELETE, text1]]; + } + longtext = text1.length > text2.length ? text1 : text2; + shorttext = text1.length > text2.length ? text2 : text1; + i = longtext.indexOf(shorttext); + if (i !== -1) { + // Shorter text is inside the longer text (speedup). + diffs = [[DIFF_INSERT, longtext.substring(0, i)], [DIFF_EQUAL, shorttext], [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; + + // Swap insertions for deletions if diff is reversed. + if (text1.length > text2.length) { + diffs[0][0] = diffs[2][0] = DIFF_DELETE; + } + return diffs; + } + if (shorttext.length === 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; + } - // options.key - } else if (options && options.key) { - var key = options.key; - for (var i = targetsLen - 1; i >= 0; --i) { - var obj = targets[i]; - var target = getValue(obj, key); - if (!target) continue; - if (!isObj(target)) target = fuzzysort.getPrepared(target); - var result = algorithm(search, target, searchLowerCode); - if (result === null) continue; - if (result.score < threshold) continue; + // Check to see if the problem can be split in two. + hm = this.diffHalfMatch(text1, text2); + if (hm) { + // A half-match was found, sort out the return data. + text1A = hm[0]; + text1B = hm[1]; + text2A = hm[2]; + text2B = hm[3]; + midCommon = hm[4]; - // have to clone result so duplicate targets from different obj can each reference the correct obj - result = { - target: result.target, - _targetLowerCodes: null, - _nextBeginningIndexes: null, - score: result.score, - indexes: result.indexes, - obj: obj - }; // hidden + // Send both pairs off for separate processing. + diffsA = this.DiffMain(text1A, text2A, checklines, deadline); + diffsB = this.DiffMain(text1B, text2B, checklines, deadline); - if (resultsLen < limit) { - q.add(result); - ++resultsLen; - } else { - ++limitedCount; - if (result.score > q.peek().score) q.replaceTop(result); - } - } + // Merge the results. + return diffsA.concat([[DIFF_EQUAL, midCommon]], diffsB); + } + if (checklines && text1.length > 100 && text2.length > 100) { + return this.diffLineMode(text1, text2, deadline); + } + return this.diffBisect(text1, text2, deadline); + }; - // no keys - } else { - for (var i = targetsLen - 1; i >= 0; --i) { - var target = targets[i]; - if (!target) continue; - if (!isObj(target)) target = fuzzysort.getPrepared(target); - var result = algorithm(search, target, searchLowerCode); - if (result === null) continue; - if (result.score < threshold) continue; - if (resultsLen < limit) { - q.add(result); - ++resultsLen; - } else { - ++limitedCount; - if (result.score > q.peek().score) q.replaceTop(result); - } - } + /** + * Do the two texts share a substring which is at least half the length of the + * longer text? + * This speedup can produce non-minimal diffs. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.} Five element Array, containing the prefix of + * text1, the suffix of text1, the prefix of text2, the suffix of + * text2 and the common middle. Or null if there was no match. + * @private + */ + DiffMatchPatch.prototype.diffHalfMatch = function (text1, text2) { + var longtext, shorttext, dmp, text1A, text2B, text2A, text1B, midCommon, hm1, hm2, hm; + longtext = text1.length > text2.length ? text1 : text2; + shorttext = text1.length > text2.length ? text2 : text1; + if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { + return null; // Pointless. + } + dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Does a substring of shorttext exist within longtext such that the substring + * is at least half the length of longtext? + * Closure, but does not reference any external variables. + * @param {string} longtext Longer string. + * @param {string} shorttext Shorter string. + * @param {number} i Start index of quarter length substring within longtext. + * @return {Array.} Five element Array, containing the prefix of + * longtext, the suffix of longtext, the prefix of shorttext, the suffix + * of shorttext and the common middle. Or null if there was no match. + * @private + */ + function diffHalfMatchI(longtext, shorttext, i) { + var seed, j, bestCommon, prefixLength, suffixLength, bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; + + // Start with a 1/4 length substring at position i as a seed. + seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); + j = -1; + bestCommon = ''; + while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { + prefixLength = dmp.diffCommonPrefix(longtext.substring(i), shorttext.substring(j)); + suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), shorttext.substring(0, j)); + if (bestCommon.length < suffixLength + prefixLength) { + bestCommon = shorttext.substring(j - suffixLength, j) + shorttext.substring(j, j + prefixLength); + bestLongtextA = longtext.substring(0, i - suffixLength); + bestLongtextB = longtext.substring(i + prefixLength); + bestShorttextA = shorttext.substring(0, j - suffixLength); + bestShorttextB = shorttext.substring(j + prefixLength); + } + } + if (bestCommon.length * 2 >= longtext.length) { + return [bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB, bestCommon]; + } else { + return null; + } + } + + // First check if the second quarter is the seed for a half-match. + hm1 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 4)); + + // Check again based on the third quarter. + hm2 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 2)); + if (!hm1 && !hm2) { + return null; + } else if (!hm2) { + hm = hm1; + } else if (!hm1) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length > hm2[4].length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + if (text1.length > text2.length) { + text1A = hm[0]; + text1B = hm[1]; + text2A = hm[2]; + text2B = hm[3]; + } else { + text2A = hm[0]; + text2B = hm[1]; + text1A = hm[2]; + text1B = hm[3]; + } + midCommon = hm[4]; + return [text1A, text1B, text2A, text2B, midCommon]; + }; + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffLineMode = function (text1, text2, deadline) { + var a, diffs, linearray, pointer, countInsert, countDelete, textInsert, textDelete, j; + + // Scan the text on a line-by-line basis first. + a = this.diffLinesToChars(text1, text2); + text1 = a.chars1; + text2 = a.chars2; + linearray = a.lineArray; + diffs = this.DiffMain(text1, text2, false, deadline); + + // Convert the diff back to original text. + this.diffCharsToLines(diffs, linearray); + + // Eliminate freak matches (e.g. blank lines) + this.diffCleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.push([DIFF_EQUAL, '']); + pointer = 0; + countDelete = 0; + countInsert = 0; + textDelete = ''; + textInsert = ''; + while (pointer < diffs.length) { + switch (diffs[pointer][0]) { + case DIFF_INSERT: + countInsert++; + textInsert += diffs[pointer][1]; + break; + case DIFF_DELETE: + countDelete++; + textDelete += diffs[pointer][1]; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (countDelete >= 1 && countInsert >= 1) { + // Delete the offending records and add the merged ones. + diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert); + pointer = pointer - countDelete - countInsert; + a = this.DiffMain(textDelete, textInsert, false, deadline); + for (j = a.length - 1; j >= 0; j--) { + diffs.splice(pointer, 0, a[j]); } - if (resultsLen === 0) return noResults; - var results = new Array(resultsLen); - for (var i = resultsLen - 1; i >= 0; --i) { - results[i] = q.poll(); + pointer = pointer + a.length; + } + countInsert = 0; + countDelete = 0; + textDelete = ''; + textInsert = ''; + break; + } + pointer++; + } + diffs.pop(); // Remove the dummy entry at the end. + + return diffs; + }; + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffBisect = function (text1, text2, deadline) { + var text1Length, text2Length, maxD, vOffset, vLength, v1, v2, x, delta, front, k1start, k1end, k2start, k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; + + // Cache the text lengths to prevent multiple calls. + text1Length = text1.length; + text2Length = text2.length; + maxD = Math.ceil((text1Length + text2Length) / 2); + vOffset = maxD; + vLength = 2 * maxD; + v1 = new Array(vLength); + v2 = new Array(vLength); + + // Setting all elements to -1 is faster in Chrome & Firefox than mixing + // integers and undefined. + for (x = 0; x < vLength; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[vOffset + 1] = 0; + v2[vOffset + 1] = 0; + delta = text1Length - text2Length; + + // If the total number of characters is odd, then the front path will collide + // with the reverse path. + front = delta % 2 !== 0; + + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + k1start = 0; + k1end = 0; + k2start = 0; + k2end = 0; + for (d = 0; d < maxD; d++) { + // Bail out if deadline is reached. + if (Date.now() > deadline) { + break; + } + + // Walk the front path one step. + for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + k1Offset = vOffset + k1; + if (k1 === -d || k1 !== d && v1[k1Offset - 1] < v1[k1Offset + 1]) { + x1 = v1[k1Offset + 1]; + } else { + x1 = v1[k1Offset - 1] + 1; + } + y1 = x1 - k1; + while (x1 < text1Length && y1 < text2Length && text1.charAt(x1) === text2.charAt(y1)) { + x1++; + y1++; + } + v1[k1Offset] = x1; + if (x1 > text1Length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2Length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + k2Offset = vOffset + delta - k1; + if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) { + // Mirror x2 onto top-left coordinate system. + x2 = text1Length - v2[k2Offset]; + if (x1 >= x2) { + // Overlap detected. + return this.diffBisectSplit(text1, text2, x1, y1, deadline); } - results.total = resultsLen + limitedCount; - return results; - }, - goAsync: function goAsync(search, targets, options) { - var canceled = false; - var p = new Promise(function (resolve, reject) { - if (search == 'farzher') return resolve([{ - target: "farzher was here (^-^*)/", - score: 0, - indexes: [0, 1, 2, 3, 4, 5, 6], - obj: targets ? targets[0] : null - }]); - if (!search) return resolve(noResults); - search = fuzzysort.prepareSearch(search); - var searchLowerCode = search[0]; - var q = fastpriorityqueue(); - var iCurrent = targets.length - 1; - var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991; - var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991; - var allowTypo = options && options.allowTypo !== undefined ? options.allowTypo : instanceOptions && instanceOptions.allowTypo !== undefined ? instanceOptions.allowTypo : true; - var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo; - var resultsLen = 0; - var limitedCount = 0; - function step() { - if (canceled) return reject('canceled'); - var startMs = Date.now(); + } + } + } - // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys] + // Walk the reverse path one step. + for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + k2Offset = vOffset + k2; + if (k2 === -d || k2 !== d && v2[k2Offset - 1] < v2[k2Offset + 1]) { + x2 = v2[k2Offset + 1]; + } else { + x2 = v2[k2Offset - 1] + 1; + } + y2 = x2 - k2; + while (x2 < text1Length && y2 < text2Length && text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)) { + x2++; + y2++; + } + v2[k2Offset] = x2; + if (x2 > text1Length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2Length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + k1Offset = vOffset + delta - k2; + if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) { + x1 = v1[k1Offset]; + y1 = vOffset + x1 - k1Offset; + + // Mirror x2 onto top-left coordinate system. + x2 = text1Length - x2; + if (x1 >= x2) { + // Overlap detected. + return this.diffBisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + } - // options.keys - if (options && options.keys) { - var scoreFn = options.scoreFn || defaultScoreFn; - var keys = options.keys; - var keysLen = keys.length; - for (; iCurrent >= 0; --iCurrent) { - if (iCurrent % 1000 /*itemsPerCheck*/ === 0) { - if (Date.now() - startMs >= 10 /*asyncInterval*/) { - isNode ? setImmediate(step) : setTimeout(step); - return; - } - } - var obj = targets[iCurrent]; - var objResults = new Array(keysLen); - for (var keyI = keysLen - 1; keyI >= 0; --keyI) { - var key = keys[keyI]; - var target = getValue(obj, key); - if (!target) { - objResults[keyI] = null; - continue; - } - if (!isObj(target)) target = fuzzysort.getPrepared(target); - objResults[keyI] = algorithm(search, target, searchLowerCode); - } - objResults.obj = obj; // before scoreFn so scoreFn can use it - var score = scoreFn(objResults); - if (score === null) continue; - if (score < threshold) continue; - objResults.score = score; - if (resultsLen < limit) { - q.add(objResults); - ++resultsLen; - } else { - ++limitedCount; - if (score > q.peek().score) q.replaceTop(objResults); - } - } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; + }; - // options.key - } else if (options && options.key) { - var key = options.key; - for (; iCurrent >= 0; --iCurrent) { - if (iCurrent % 1000 /*itemsPerCheck*/ === 0) { - if (Date.now() - startMs >= 10 /*asyncInterval*/) { - isNode ? setImmediate(step) : setTimeout(step); - return; - } - } - var obj = targets[iCurrent]; - var target = getValue(obj, key); - if (!target) continue; - if (!isObj(target)) target = fuzzysort.getPrepared(target); - var result = algorithm(search, target, searchLowerCode); - if (result === null) continue; - if (result.score < threshold) continue; + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffBisectSplit = function (text1, text2, x, y, deadline) { + var text1a, text1b, text2a, text2b, diffs, diffsb; + text1a = text1.substring(0, x); + text2a = text2.substring(0, y); + text1b = text1.substring(x); + text2b = text2.substring(y); + + // Compute both diffs serially. + diffs = this.DiffMain(text1a, text2a, false, deadline); + diffsb = this.DiffMain(text1b, text2b, false, deadline); + return diffs.concat(diffsb); + }; - // have to clone result so duplicate targets from different obj can each reference the correct obj - result = { - target: result.target, - _targetLowerCodes: null, - _nextBeginningIndexes: null, - score: result.score, - indexes: result.indexes, - obj: obj - }; // hidden + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupSemantic = function (diffs) { + var changes = false; + var equalities = []; // Stack of indices where equalities are found. + var equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + var lastequality = null; + + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + var pointer = 0; // Index of current position. + + // Number of characters that changed prior to the equality. + var lengthInsertions1 = 0; + var lengthDeletions1 = 0; + + // Number of characters that changed after the equality. + var lengthInsertions2 = 0; + var lengthDeletions2 = 0; + while (pointer < diffs.length) { + if (diffs[pointer][0] === DIFF_EQUAL) { + // Equality found. + equalities[equalitiesLength++] = pointer; + lengthInsertions1 = lengthInsertions2; + lengthDeletions1 = lengthDeletions2; + lengthInsertions2 = 0; + lengthDeletions2 = 0; + lastequality = diffs[pointer][1]; + } else { + // An insertion or deletion. + if (diffs[pointer][0] === DIFF_INSERT) { + lengthInsertions2 += diffs[pointer][1].length; + } else { + lengthDeletions2 += diffs[pointer][1].length; + } - if (resultsLen < limit) { - q.add(result); - ++resultsLen; - } else { - ++limitedCount; - if (result.score > q.peek().score) q.replaceTop(result); - } - } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastequality && lastequality.length <= Math.max(lengthInsertions1, lengthDeletions1) && lastequality.length <= Math.max(lengthInsertions2, lengthDeletions2)) { + // Duplicate record. + diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]); - // no keys - } else { - for (; iCurrent >= 0; --iCurrent) { - if (iCurrent % 1000 /*itemsPerCheck*/ === 0) { - if (Date.now() - startMs >= 10 /*asyncInterval*/) { - isNode ? setImmediate(step) : setTimeout(step); - return; - } - } - var target = targets[iCurrent]; - if (!target) continue; - if (!isObj(target)) target = fuzzysort.getPrepared(target); - var result = algorithm(search, target, searchLowerCode); - if (result === null) continue; - if (result.score < threshold) continue; - if (resultsLen < limit) { - q.add(result); - ++resultsLen; - } else { - ++limitedCount; - if (result.score > q.peek().score) q.replaceTop(result); - } - } - } - if (resultsLen === 0) return resolve(noResults); - var results = new Array(resultsLen); - for (var i = resultsLen - 1; i >= 0; --i) { - results[i] = q.poll(); - } - results.total = resultsLen + limitedCount; - resolve(results); - } - isNode ? setImmediate(step) : step(); //setTimeout here is too slow - }); + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - p.cancel = function () { - canceled = true; - }; - return p; - }, - highlight: function highlight(result, hOpen, hClose) { - if (typeof hOpen == 'function') return fuzzysort.highlightCallback(result, hOpen); - if (result === null) return null; - if (hOpen === undefined) hOpen = ''; - if (hClose === undefined) hClose = ''; - var highlighted = ''; - var matchesIndex = 0; - var opened = false; - var target = result.target; - var targetLen = target.length; - var matchesBest = result.indexes; - for (var i = 0; i < targetLen; ++i) { - var char = target[i]; - if (matchesBest[matchesIndex] === i) { - ++matchesIndex; - if (!opened) { - opened = true; - highlighted += hOpen; - } - if (matchesIndex === matchesBest.length) { - highlighted += char + hClose + target.substr(i + 1); - break; - } - } else { - if (opened) { - opened = false; - highlighted += hClose; - } - } - highlighted += char; - } - return highlighted; - }, - highlightCallback: function highlightCallback(result, cb) { - if (result === null) return null; - var target = result.target; - var targetLen = target.length; - var indexes = result.indexes; - var highlighted = ''; - var matchI = 0; - var indexesI = 0; - var opened = false; - var result = []; - for (var i = 0; i < targetLen; ++i) { - var char = target[i]; - if (indexes[indexesI] === i) { - ++indexesI; - if (!opened) { - opened = true; - result.push(highlighted); - highlighted = ''; - } - if (indexesI === indexes.length) { - highlighted += char; - result.push(cb(highlighted, matchI++)); - highlighted = ''; - result.push(target.substr(i + 1)); - break; - } - } else { - if (opened) { - opened = false; - result.push(cb(highlighted, matchI++)); - highlighted = ''; - } - } - highlighted += char; - } - return result; - }, - prepare: function prepare(target) { - if (!target) return { - target: '', - _targetLowerCodes: [0 /*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], - _nextBeginningIndexes: null, - score: null, - indexes: null, - obj: null - }; // hidden - return { - target: target, - _targetLowerCodes: fuzzysort.prepareLowerCodes(target), - _nextBeginningIndexes: null, - score: null, - indexes: null, - obj: null - }; // hidden - }, + // Throw away the equality we just deleted. + equalitiesLength--; - prepareSlow: function prepareSlow(target) { - if (!target) return { - target: '', - _targetLowerCodes: [0 /*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], - _nextBeginningIndexes: null, - score: null, - indexes: null, - obj: null - }; // hidden - return { - target: target, - _targetLowerCodes: fuzzysort.prepareLowerCodes(target), - _nextBeginningIndexes: fuzzysort.prepareNextBeginningIndexes(target), - score: null, - indexes: null, - obj: null - }; // hidden - }, + // Throw away the previous equality (it needs to be reevaluated). + equalitiesLength--; + pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - prepareSearch: function prepareSearch(search) { - if (!search) search = ''; - return fuzzysort.prepareLowerCodes(search); - }, - // Below this point is only internal code - // Below this point is only internal code - // Below this point is only internal code - // Below this point is only internal code + // Reset the counters. + lengthInsertions1 = 0; + lengthDeletions1 = 0; + lengthInsertions2 = 0; + lengthDeletions2 = 0; + lastequality = null; + changes = true; + } + } + pointer++; + } - getPrepared: function getPrepared(target) { - if (target.length > 999) return fuzzysort.prepare(target); // don't cache huge targets - var targetPrepared = preparedCache.get(target); - if (targetPrepared !== undefined) return targetPrepared; - targetPrepared = fuzzysort.prepare(target); - preparedCache.set(target, targetPrepared); - return targetPrepared; - }, - getPreparedSearch: function getPreparedSearch(search) { - if (search.length > 999) return fuzzysort.prepareSearch(search); // don't cache huge searches - var searchPrepared = preparedSearchCache.get(search); - if (searchPrepared !== undefined) return searchPrepared; - searchPrepared = fuzzysort.prepareSearch(search); - preparedSearchCache.set(search, searchPrepared); - return searchPrepared; - }, - algorithm: function algorithm(searchLowerCodes, prepared, searchLowerCode) { - var targetLowerCodes = prepared._targetLowerCodes; - var searchLen = searchLowerCodes.length; - var targetLen = targetLowerCodes.length; - var searchI = 0; // where we at - var targetI = 0; // where you at - var typoSimpleI = 0; - var matchesSimpleLen = 0; + // Normalize the diff. + if (changes) { + this.diffCleanupMerge(diffs); + } + var deletion, insertion, overlapLength1, overlapLength2; + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while (pointer < diffs.length) { + if (diffs[pointer - 1][0] === DIFF_DELETE && diffs[pointer][0] === DIFF_INSERT) { + deletion = diffs[pointer - 1][1]; + insertion = diffs[pointer][1]; + overlapLength1 = this.diffCommonOverlap(deletion, insertion); + overlapLength2 = this.diffCommonOverlap(insertion, deletion); + if (overlapLength1 >= overlapLength2) { + if (overlapLength1 >= deletion.length / 2 || overlapLength1 >= insertion.length / 2) { + // Overlap found. Insert an equality and trim the surrounding edits. + diffs.splice(pointer, 0, [DIFF_EQUAL, insertion.substring(0, overlapLength1)]); + diffs[pointer - 1][1] = deletion.substring(0, deletion.length - overlapLength1); + diffs[pointer + 1][1] = insertion.substring(overlapLength1); + pointer++; + } + } else { + if (overlapLength2 >= deletion.length / 2 || overlapLength2 >= insertion.length / 2) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.splice(pointer, 0, [DIFF_EQUAL, deletion.substring(0, overlapLength2)]); + diffs[pointer - 1][0] = DIFF_INSERT; + diffs[pointer - 1][1] = insertion.substring(0, insertion.length - overlapLength2); + diffs[pointer + 1][0] = DIFF_DELETE; + diffs[pointer + 1][1] = deletion.substring(overlapLength2); + pointer++; + } + } + pointer++; + } + pointer++; + } + }; - // very basic fuzzy match; to remove non-matching targets ASAP! - // walk through target. find sequential matches. - // if all chars aren't found then exit - for (;;) { - var isMatch = searchLowerCode === targetLowerCodes[targetI]; - if (isMatch) { - matchesSimple[matchesSimpleLen++] = targetI; - ++searchI; - if (searchI === searchLen) break; - searchLowerCode = searchLowerCodes[typoSimpleI === 0 ? searchI : typoSimpleI === searchI ? searchI + 1 : typoSimpleI === searchI - 1 ? searchI - 1 : searchI]; - } - ++targetI; - if (targetI >= targetLen) { - // Failed to find searchI - // Check for typo or exit - // we go as far as possible before trying to transpose - // then we transpose backwards until we reach the beginning - for (;;) { - if (searchI <= 1) return null; // not allowed to transpose first char - if (typoSimpleI === 0) { - // we haven't tried to transpose yet - --searchI; - var searchLowerCodeNew = searchLowerCodes[searchI]; - if (searchLowerCode === searchLowerCodeNew) continue; // doesn't make sense to transpose a repeat char - typoSimpleI = searchI; - } else { - if (typoSimpleI === 1) return null; // reached the end of the line for transposing - --typoSimpleI; - searchI = typoSimpleI; - searchLowerCode = searchLowerCodes[searchI + 1]; - var searchLowerCodeNew = searchLowerCodes[searchI]; - if (searchLowerCode === searchLowerCodeNew) continue; // doesn't make sense to transpose a repeat char - } + /** + * Determine if the suffix of one string is the prefix of another. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of the first + * string and the start of the second string. + * @private + */ + DiffMatchPatch.prototype.diffCommonOverlap = function (text1, text2) { + // Cache the text lengths to prevent multiple calls. + var text1Length = text1.length; + var text2Length = text2.length; - matchesSimpleLen = searchI; - targetI = matchesSimple[matchesSimpleLen - 1] + 1; - break; - } - } - } - var searchI = 0; - var typoStrictI = 0; - var successStrict = false; - var matchesStrictLen = 0; - var nextBeginningIndexes = prepared._nextBeginningIndexes; - if (nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target); - var firstPossibleI = targetI = matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[matchesSimple[0] - 1]; + // Eliminate the null case. + if (text1Length === 0 || text2Length === 0) { + return 0; + } - // Our target string successfully matched all characters in sequence! - // Let's try a more advanced and strict test to improve the score - // only count it as a match if it's consecutive or a beginning character! - if (targetI !== targetLen) for (;;) { - if (targetI >= targetLen) { - // We failed to find a good spot for this search char, go back to the previous search char and force it forward - if (searchI <= 0) { - // We failed to push chars forward for a better match - // transpose, starting from the beginning - ++typoStrictI; - if (typoStrictI > searchLen - 2) break; - if (searchLowerCodes[typoStrictI] === searchLowerCodes[typoStrictI + 1]) continue; // doesn't make sense to transpose a repeat char - targetI = firstPossibleI; - continue; - } - --searchI; - var lastMatch = matchesStrict[--matchesStrictLen]; - targetI = nextBeginningIndexes[lastMatch]; - } else { - var isMatch = searchLowerCodes[typoStrictI === 0 ? searchI : typoStrictI === searchI ? searchI + 1 : typoStrictI === searchI - 1 ? searchI - 1 : searchI] === targetLowerCodes[targetI]; - if (isMatch) { - matchesStrict[matchesStrictLen++] = targetI; - ++searchI; - if (searchI === searchLen) { - successStrict = true; - break; - } - ++targetI; + // Truncate the longer string. + if (text1Length > text2Length) { + text1 = text1.substring(text1Length - text2Length); + } else if (text1Length < text2Length) { + text2 = text2.substring(0, text1Length); + } + var textLength = Math.min(text1Length, text2Length); + + // Quick check for the worst case. + if (text1 === text2) { + return textLength; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: https://neil.fraser.name/news/2010/11/04/ + var best = 0; + var length = 1; + while (true) { + var pattern = text1.substring(textLength - length); + var found = text2.indexOf(pattern); + if (found === -1) { + return best; + } + length += found; + if (found === 0 || text1.substring(textLength - length) === text2.substring(0, length)) { + best = length; + length++; + } + } + }; + + /** + * Split two texts into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {{chars1: string, chars2: string, lineArray: !Array.}} + * An object containing the encoded text1, the encoded text2 and + * the array of unique strings. + * The zeroth element of the array of unique strings is intentionally blank. + * @private + */ + DiffMatchPatch.prototype.diffLinesToChars = function (text1, text2) { + var lineArray = []; // E.g. lineArray[4] === 'Hello\n' + var lineHash = {}; // E.g. lineHash['Hello\n'] === 4 + + // '\x00' is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray[0] = ''; + + /** + * Split a text into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * Modifies linearray and linehash through being a closure. + * @param {string} text String to encode. + * @return {string} Encoded string. + * @private + */ + function diffLinesToCharsMunge(text) { + var chars = ''; + + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + var lineStart = 0; + var lineEnd = -1; + + // Keeping our own length variable is faster than looking it up. + var lineArrayLength = lineArray.length; + while (lineEnd < text.length - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd === -1) { + lineEnd = text.length - 1; + } + var line = text.substring(lineStart, lineEnd + 1); + lineStart = lineEnd + 1; + if (hasOwn.call(lineHash, line)) { + chars += String.fromCharCode(lineHash[line]); + } else { + chars += String.fromCharCode(lineArrayLength); + lineHash[line] = lineArrayLength; + lineArray[lineArrayLength++] = line; + } + } + return chars; + } + var chars1 = diffLinesToCharsMunge(text1); + var chars2 = diffLinesToCharsMunge(text2); + return { + chars1: chars1, + chars2: chars2, + lineArray: lineArray + }; + }; + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param {!Array.} diffs Array of diff tuples. + * @param {!Array.} lineArray Array of unique strings. + * @private + */ + DiffMatchPatch.prototype.diffCharsToLines = function (diffs, lineArray) { + for (var x = 0; x < diffs.length; x++) { + var chars = diffs[x][1]; + var text = []; + for (var y = 0; y < chars.length; y++) { + text[y] = lineArray[chars.charCodeAt(y)]; + } + diffs[x][1] = text.join(''); + } + }; + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupMerge = function (diffs) { + diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end. + var pointer = 0; + var countDelete = 0; + var countInsert = 0; + var textDelete = ''; + var textInsert = ''; + while (pointer < diffs.length) { + switch (diffs[pointer][0]) { + case DIFF_INSERT: + countInsert++; + textInsert += diffs[pointer][1]; + pointer++; + break; + case DIFF_DELETE: + countDelete++; + textDelete += diffs[pointer][1]; + pointer++; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (countDelete + countInsert > 1) { + if (countDelete !== 0 && countInsert !== 0) { + // Factor out any common prefixes. + var commonlength = this.diffCommonPrefix(textInsert, textDelete); + if (commonlength !== 0) { + if (pointer - countDelete - countInsert > 0 && diffs[pointer - countDelete - countInsert - 1][0] === DIFF_EQUAL) { + diffs[pointer - countDelete - countInsert - 1][1] += textInsert.substring(0, commonlength); } else { - targetI = nextBeginningIndexes[targetI]; + diffs.splice(0, 0, [DIFF_EQUAL, textInsert.substring(0, commonlength)]); + pointer++; } + textInsert = textInsert.substring(commonlength); + textDelete = textDelete.substring(commonlength); } - } - { - // tally up the score & keep track of matches for highlighting later - if (successStrict) { - var matchesBest = matchesStrict; - var matchesBestLen = matchesStrictLen; - } else { - var matchesBest = matchesSimple; - var matchesBestLen = matchesSimpleLen; + + // Factor out any common suffixies. + commonlength = this.diffCommonSuffix(textInsert, textDelete); + if (commonlength !== 0) { + diffs[pointer][1] = textInsert.substring(textInsert.length - commonlength) + diffs[pointer][1]; + textInsert = textInsert.substring(0, textInsert.length - commonlength); + textDelete = textDelete.substring(0, textDelete.length - commonlength); } - var score = 0; - var lastTargetI = -1; - for (var i = 0; i < searchLen; ++i) { - var targetI = matchesBest[i]; - // score only goes down if they're not consecutive - if (lastTargetI !== targetI - 1) score -= targetI; - lastTargetI = targetI; - } - if (!successStrict) { - score *= 1000; - if (typoSimpleI !== 0) score += -20; /*typoPenalty*/ - } else { - if (typoStrictI !== 0) score += -20; /*typoPenalty*/ - } - - score -= targetLen - searchLen; - prepared.score = score; - prepared.indexes = new Array(matchesBestLen); - for (var i = matchesBestLen - 1; i >= 0; --i) { - prepared.indexes[i] = matchesBest[i]; - } - return prepared; } - }, - algorithmNoTypo: function algorithmNoTypo(searchLowerCodes, prepared, searchLowerCode) { - var targetLowerCodes = prepared._targetLowerCodes; - var searchLen = searchLowerCodes.length; - var targetLen = targetLowerCodes.length; - var searchI = 0; // where we at - var targetI = 0; // where you at - var matchesSimpleLen = 0; - // very basic fuzzy match; to remove non-matching targets ASAP! - // walk through target. find sequential matches. - // if all chars aren't found then exit - for (;;) { - var isMatch = searchLowerCode === targetLowerCodes[targetI]; - if (isMatch) { - matchesSimple[matchesSimpleLen++] = targetI; - ++searchI; - if (searchI === searchLen) break; - searchLowerCode = searchLowerCodes[searchI]; - } - ++targetI; - if (targetI >= targetLen) return null; // Failed to find searchI + // Delete the offending records and add the merged ones. + if (countDelete === 0) { + diffs.splice(pointer - countInsert, countDelete + countInsert, [DIFF_INSERT, textInsert]); + } else if (countInsert === 0) { + diffs.splice(pointer - countDelete, countDelete + countInsert, [DIFF_DELETE, textDelete]); + } else { + diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert, [DIFF_DELETE, textDelete], [DIFF_INSERT, textInsert]); } + pointer = pointer - countDelete - countInsert + (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1; + } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1][1] += diffs[pointer][1]; + diffs.splice(pointer, 1); + } else { + pointer++; + } + countInsert = 0; + countDelete = 0; + textDelete = ''; + textInsert = ''; + break; + } + } + if (diffs[diffs.length - 1][1] === '') { + diffs.pop(); // Remove the dummy entry at the end. + } - var searchI = 0; - var successStrict = false; - var matchesStrictLen = 0; - var nextBeginningIndexes = prepared._nextBeginningIndexes; - if (nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target); - targetI = matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[matchesSimple[0] - 1]; - - // Our target string successfully matched all characters in sequence! - // Let's try a more advanced and strict test to improve the score - // only count it as a match if it's consecutive or a beginning character! - if (targetI !== targetLen) for (;;) { - if (targetI >= targetLen) { - // We failed to find a good spot for this search char, go back to the previous search char and force it forward - if (searchI <= 0) break; // We failed to push chars forward for a better match + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + var changes = false; + pointer = 1; - --searchI; - var lastMatch = matchesStrict[--matchesStrictLen]; - targetI = nextBeginningIndexes[lastMatch]; - } else { - var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]; - if (isMatch) { - matchesStrict[matchesStrictLen++] = targetI; - ++searchI; - if (searchI === searchLen) { - successStrict = true; - break; - } - ++targetI; - } else { - targetI = nextBeginningIndexes[targetI]; - } - } - } - { - // tally up the score & keep track of matches for highlighting later - if (successStrict) { - var matchesBest = matchesStrict; - var matchesBestLen = matchesStrictLen; - } else { - var matchesBest = matchesSimple; - var matchesBestLen = matchesSimpleLen; - } - var score = 0; - var lastTargetI = -1; - for (var i = 0; i < searchLen; ++i) { - var targetI = matchesBest[i]; - // score only goes down if they're not consecutive - if (lastTargetI !== targetI - 1) score -= targetI; - lastTargetI = targetI; - } - if (!successStrict) score *= 1000; - score -= targetLen - searchLen; - prepared.score = score; - prepared.indexes = new Array(matchesBestLen); - for (var i = matchesBestLen - 1; i >= 0; --i) { - prepared.indexes[i] = matchesBest[i]; - } - return prepared; - } - }, - prepareLowerCodes: function prepareLowerCodes(str) { - var strLen = str.length; - var lowerCodes = []; // new Array(strLen) sparse array is too slow - var lower = str.toLowerCase(); - for (var i = 0; i < strLen; ++i) { - lowerCodes[i] = lower.charCodeAt(i); - } - return lowerCodes; - }, - prepareBeginningIndexes: function prepareBeginningIndexes(target) { - var targetLen = target.length; - var beginningIndexes = []; - var beginningIndexesLen = 0; - var wasUpper = false; - var wasAlphanum = false; - for (var i = 0; i < targetLen; ++i) { - var targetCode = target.charCodeAt(i); - var isUpper = targetCode >= 65 && targetCode <= 90; - var isAlphanum = isUpper || targetCode >= 97 && targetCode <= 122 || targetCode >= 48 && targetCode <= 57; - var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum; - wasUpper = isUpper; - wasAlphanum = isAlphanum; - if (isBeginning) beginningIndexes[beginningIndexesLen++] = i; - } - return beginningIndexes; - }, - prepareNextBeginningIndexes: function prepareNextBeginningIndexes(target) { - var targetLen = target.length; - var beginningIndexes = fuzzysort.prepareBeginningIndexes(target); - var nextBeginningIndexes = []; // new Array(targetLen) sparse array is too slow - var lastIsBeginning = beginningIndexes[0]; - var lastIsBeginningI = 0; - for (var i = 0; i < targetLen; ++i) { - if (lastIsBeginning > i) { - nextBeginningIndexes[i] = lastIsBeginning; - } else { - lastIsBeginning = beginningIndexes[++lastIsBeginningI]; - nextBeginningIndexes[i] = lastIsBeginning === undefined ? targetLen : lastIsBeginning; - } - } - return nextBeginningIndexes; - }, - cleanup: cleanup, - new: fuzzysortNew - }; - return fuzzysort; - } // fuzzysortNew + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) { + var diffPointer = diffs[pointer][1]; + var position = diffPointer.substring(diffPointer.length - diffs[pointer - 1][1].length); - // This stuff is outside fuzzysortNew, because it's shared with instances of fuzzysort.new() - var isNode = typeof commonjsRequire !== 'undefined' && typeof window === 'undefined'; - var MyMap = typeof Map === 'function' ? Map : function () { - var s = Object.create(null); - this.get = function (k) { - return s[k]; - }; - this.set = function (k, val) { - s[k] = val; - return this; - }; - this.clear = function () { - s = Object.create(null); - }; - }; - var preparedCache = new MyMap(); - var preparedSearchCache = new MyMap(); - var noResults = []; - noResults.total = 0; - var matchesSimple = []; - var matchesStrict = []; - function cleanup() { - preparedCache.clear(); - preparedSearchCache.clear(); - matchesSimple = []; - matchesStrict = []; - } - function defaultScoreFn(a) { - var max = -9007199254740991; - for (var i = a.length - 1; i >= 0; --i) { - var result = a[i]; - if (result === null) continue; - var score = result.score; - if (score > max) max = score; + // This is a single edit surrounded by equalities. + if (position === diffs[pointer - 1][1]) { + // Shift the edit over the previous equality. + diffs[pointer][1] = diffs[pointer - 1][1] + diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length); + diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; + diffs.splice(pointer - 1, 1); + changes = true; + } else if (diffPointer.substring(0, diffs[pointer + 1][1].length) === diffs[pointer + 1][1]) { + // Shift the edit over the next equality. + diffs[pointer - 1][1] += diffs[pointer + 1][1]; + diffs[pointer][1] = diffs[pointer][1].substring(diffs[pointer + 1][1].length) + diffs[pointer + 1][1]; + diffs.splice(pointer + 1, 1); + changes = true; } - if (max === -9007199254740991) return null; - return max; } + pointer++; + } - // prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop] - // prop = 'key1.key2' 10ms - // prop = ['key1', 'key2'] 27ms - function getValue(obj, prop) { - var tmp = obj[prop]; - if (tmp !== undefined) return tmp; - var segs = prop; - if (!Array.isArray(prop)) segs = prop.split('.'); - var len = segs.length; - var i = -1; - while (obj && ++i < len) { - obj = obj[segs[i]]; - } - return obj; - } - function isObj(x) { - return _typeof(x) === 'object'; - } // faster as a function + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + this.diffCleanupMerge(diffs); + } + }; + function diff(o, n) { + var diff, output, text; + diff = new DiffMatchPatch(); + output = diff.DiffMain(o, n); + diff.diffCleanupEfficiency(output); + text = diff.diffPrettyHtml(output); + return text; + } - // Hacked version of https://github.com/lemire/FastPriorityQueue.js - var fastpriorityqueue = function fastpriorityqueue() { - var r = [], - o = 0, - e = {}; - function n() { - for (var e = 0, n = r[e], c = 1; c < o;) { - var f = c + 1; - e = c, f < o && r[f].score < r[c].score && (e = f), r[e - 1 >> 1] = r[e], c = 1 + (e << 1); - } - for (var a = e - 1 >> 1; e > 0 && n.score < r[a].score; a = (e = a) - 1 >> 1) { - r[e] = r[a]; - } - r[e] = n; - } - return e.add = function (e) { - var n = o; - r[o++] = e; - for (var c = n - 1 >> 1; n > 0 && e.score < r[c].score; c = (n = c) - 1 >> 1) { - r[n] = r[c]; - } - r[n] = e; - }, e.poll = function () { - if (0 !== o) { - var e = r[0]; - return r[0] = r[--o], n(), e; - } - }, e.peek = function (e) { - if (0 !== o) return r[0]; - }, e.replaceTop = function (o) { - r[0] = o, n(); - }, e; - }; - var q = fastpriorityqueue(); // reuse this, except for async, it needs to make its own + var QUnit = {}; - return fuzzysortNew(); - }); // UMD + // The "currentModule" object would ideally be defined using the createModule() + // function. Since it isn't, add the missing suiteReport property to it now that + // we have loaded all source code required to do so. + // + // TODO: Consider defining currentModule in core.js or module.js in its entirely + // rather than partly in config.js and partly here. + config.currentModule.suiteReport = runSuite; + config.pq = new ProcessingQueue(test); + var globalStartCalled = false; + var runStarted = false; - // TODO: (performance) wasm version!? - // TODO: (performance) threads? - // TODO: (performance) avoid cache misses - // TODO: (performance) preparedCache is a memory leak - // TODO: (like sublime) backslash === forwardslash - // TODO: (like sublime) spaces: "a b" should do 2 searches 1 for a and 1 for b - // TODO: (scoring) garbage in targets that allows most searches to strict match need a penality - // TODO: (performance) idk if allowTypo is optimized - })(fuzzysort$1); - var fuzzysort = fuzzysort$1.exports; + // Figure out if we're running the tests from a server or not + QUnit.isLocal = window$1 && window$1.location && window$1.location.protocol === 'file:'; - var stats = { - failedTests: [], - defined: 0, - completed: 0 - }; + // Expose the current QUnit version + QUnit.version = '2.25.0'; + extend(QUnit, { + config: config, + diff: diff, + dump: dump, + equiv: equiv, + reporters: reporters, + hooks: hooks, + is: is, + objectType: objectType, + on: on, + onError: onWindowError, + onUncaughtException: onUncaughtException, + pushFailure: pushFailure, + assert: Assert.prototype, + module: module$1, + test: test, + // alias other test flavors for easy access + todo: test.todo, + skip: test.skip, + only: test.only, + start: function start(count) { + if (config.current) { + throw new Error('QUnit.start cannot be called inside a test context.'); + } + var globalStartAlreadyCalled = globalStartCalled; + globalStartCalled = true; + if (runStarted) { + throw new Error('Called start() while test already started running'); + } + if (globalStartAlreadyCalled || count > 1) { + throw new Error('Called start() outside of a test context too many times'); + } + if (config.autostart) { + throw new Error('Called start() outside of a test context when ' + 'QUnit.config.autostart was true'); + } - // Escape text for attribute or text content. - function escapeText(str) { - if (!str) { - return ''; + // Until we remove QUnit.load() in QUnit 3, we keep `pageLoaded`. + // It no longer serves any purpose other than to support old test runners + // that still call only QUnit.load(), or that call both it and QUnit.start(). + if (!config.pageLoaded) { + // If the test runner used `autostart = false` and is calling QUnit.start() + // to tell is their resources are ready, but the browser isn't ready yet, + // then enable autostart now, and we'll let the tests really start after + // the browser's "load" event handler calls autostart(). + config.autostart = true; + + // If we're in Node or another non-browser environment, we start now as there + // won't be any "load" event. We return early either way since autostart + // is responsible for calling scheduleBegin (avoid "beginning" twice). + if (!document) { + QUnit.autostart(); + } + return; + } + scheduleBegin(); + }, + onUnhandledRejection: function onUnhandledRejection(reason) { + Logger.warn('QUnit.onUnhandledRejection is deprecated and will be removed in QUnit 3.0.' + ' Please use QUnit.onUncaughtException instead.'); + onUncaughtException(reason); + }, + extend: function extend$1() { + Logger.warn('QUnit.extend is deprecated and will be removed in QUnit 3.0.' + ' Please use Object.assign instead.'); + + // delegate to utility implementation, which does not warn and can be used elsewhere internally + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + return extend.apply(this, args); + }, + load: function load() { + Logger.warn('QUnit.load is deprecated and will be removed in QUnit 3.0.' + ' https://qunitjs.com/api/QUnit/load/'); + QUnit.autostart(); + }, + /** + * @internal + */ + autostart: function autostart() { + config.pageLoaded = true; + + // Initialize the configuration options + // TODO: Move this to config.js in QUnit 3. + extend(config, { + started: 0, + updateRate: 1000, + autostart: true, + filter: '' + }, true); + if (!runStarted) { + config.blocking = false; + if (config.autostart) { + scheduleBegin(); + } + } + }, + stack: function stack(offset) { + offset = (offset || 0) + 2; + // Support Safari: Use temp variable to avoid TCO for consistent cross-browser result + // https://bugs.webkit.org/show_bug.cgi?id=276187 + var source = sourceFromStacktrace(offset); + return source; } + }); + registerLoggingCallbacks(QUnit); + function scheduleBegin() { + runStarted = true; - // Both single quotes and double quotes (for attributes) - return ('' + str).replace(/['"<>&]/g, function (s) { - switch (s) { - case "'": - return '''; - case '"': - return '"'; - case '<': - return '<'; - case '>': - return '>'; - case '&': - return '&'; + // Add a slight delay to allow definition of more modules and tests. + if (setTimeout$1) { + setTimeout$1(function () { + begin(); + }); + } else { + begin(); + } + } + function unblockAndAdvanceQueue() { + config.blocking = false; + config.pq.advance(); + } + function begin() { + if (config.started) { + unblockAndAdvanceQueue(); + return; + } + + // QUnit.config.reporters is considered writable between qunit.js and QUnit.start(). + // Now that QUnit.start() has been called, it is time to decide which built-in reporters + // to load. + if (config.reporters.console) { + reporters.console.init(QUnit); + } + if (config.reporters.tap) { + reporters.tap.init(QUnit); + } + + // The test run hasn't officially begun yet + // Record the time of the test run's beginning + config.started = performance.now(); + + // Delete the loose unnamed module if unused. + if (config.modules[0].name === '' && config.modules[0].tests.length === 0) { + config.modules.shift(); + } + var modulesLog = []; + for (var i = 0; i < config.modules.length; i++) { + // Don't expose the unnamed global test module to plugins. + if (config.modules[i].name !== '') { + modulesLog.push({ + name: config.modules[i].name, + moduleId: config.modules[i].moduleId, + // Added in QUnit 1.16.0 for internal use by html-reporter, + // but no longer used since QUnit 2.7.0. + // @deprecated Kept unofficially to be removed in QUnit 3.0. + tests: config.modules[i].tests + }); } - }); + } + + // The test run is officially beginning now + emit('runStart', runSuite.start(true)); + runLoggingCallbacks('begin', { + totalTests: Test.count, + modules: modulesLog + }).then(unblockAndAdvanceQueue); } + exportQUnit(QUnit); + (function () { - // Don't load the HTML Reporter on non-browser environments if (!window$1 || !document) { return; } var config = QUnit.config; - var hiddenTests = []; - var collapseNext = false; var hasOwn = Object.prototype.hasOwnProperty; - var unfilteredUrl = setUrl({ - filter: undefined, - module: undefined, - moduleId: undefined, - testId: undefined - }); - var dropdownData = null; - function trim(string) { - if (typeof string.trim === 'function') { - return string.trim(); - } else { - return string.replace(/^\s+|\s+$/g, ''); + + // Stores fixture HTML for resetting later + function storeFixture() { + // Avoid overwriting user-defined values + // TODO: Change to negative null/undefined check once declared in /src/config.js + if (hasOwn.call(config, 'fixture')) { + return; } - } - function addEvent(elem, type, fn) { - elem.addEventListener(type, fn, false); - } - function removeEvent(elem, type, fn) { - elem.removeEventListener(type, fn, false); - } - function addEvents(elems, type, fn) { - var i = elems.length; - while (i--) { - addEvent(elems[i], type, fn); + var fixture = document.getElementById('qunit-fixture'); + if (fixture) { + config.fixture = fixture.cloneNode(true); } } - function hasClass(elem, name) { - return (' ' + elem.className + ' ').indexOf(' ' + name + ' ') >= 0; - } - function addClass(elem, name) { - if (!hasClass(elem, name)) { - elem.className += (elem.className ? ' ' : '') + name; + QUnit.begin(storeFixture); + + // Resets the fixture DOM element if available. + function resetFixture() { + if (config.fixture == null) { + return; } - } - function toggleClass(elem, name, force) { - if (force || typeof force === 'undefined' && !hasClass(elem, name)) { - addClass(elem, name); + var fixture = document.getElementById('qunit-fixture'); + var resetFixtureType = _typeof(config.fixture); + if (resetFixtureType === 'string') { + // support user defined values for `config.fixture` + var newFixture = document.createElement('div'); + newFixture.setAttribute('id', 'qunit-fixture'); + newFixture.innerHTML = config.fixture; + fixture.parentNode.replaceChild(newFixture, fixture); } else { - removeClass(elem, name); + var clonedFixture = config.fixture.cloneNode(true); + fixture.parentNode.replaceChild(clonedFixture, fixture); } } - function removeClass(elem, name) { - var set = ' ' + elem.className + ' '; + QUnit.testStart(resetFixture); + })(); - // Class name may appear multiple times - while (set.indexOf(' ' + name + ' ') >= 0) { - set = set.replace(' ' + name + ' ', ' '); - } + (function () { + // Only interact with URLs via window.location + var location = typeof window$1 !== 'undefined' && window$1.location; + if (!location) { + return; + } + var urlParams = getUrlParams(); - // Trim for prettiness - elem.className = trim(set); + // TODO: Move to /src/core/ in QUnit 3 + // TODO: Document this as public API (read-only) + QUnit.urlParams = urlParams; + + // TODO: Move to /src/core/config.js in QUnit 3, + // in accordance with /docs/api/config.index.md#order + QUnit.config.filter = urlParams.filter; + // NOTE: Based on readFlatPreconfigNumber from QUnit 3. + if (/^[0-9]+$/.test(urlParams.maxDepth)) { + QUnit.config.maxDepth = QUnit.dump.maxDepth = +urlParams.maxDepth; } - function id(name) { - return document.getElementById && document.getElementById(name); + QUnit.config.module = urlParams.module; + QUnit.config.moduleId = [].concat(urlParams.moduleId || []); + QUnit.config.testId = [].concat(urlParams.testId || []); + + // Test order randomization + // Generate a random seed if `?seed` is specified without a value (boolean true), + // or when set to the string "true". + if (urlParams.seed === 'true' || urlParams.seed === true) { + // NOTE: This duplicates logic from /src/core/config.js. Consolidated in QUnit 3. + QUnit.config.seed = (Math.random().toString(36) + '0000000000').slice(2, 12); + } else if (urlParams.seed) { + QUnit.config.seed = urlParams.seed; } - function abortTests() { - var abortButton = id('qunit-abort-tests-button'); - if (abortButton) { - abortButton.disabled = true; - abortButton.innerHTML = 'Aborting...'; - } - QUnit.config.queue.length = 0; - return false; - } - function interceptNavigation(ev) { - // Trim potential accidental whitespace so that QUnit doesn't throw an error about no tests matching the filter. - var filterInputElem = id('qunit-filter-input'); - filterInputElem.value = trim(filterInputElem.value); - applyUrlParams(); - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - return false; - } - function getUrlConfigHtml() { - var selection = false; - var urlConfig = config.urlConfig; - var urlConfigHtml = ''; + + // Add URL-parameter-mapped config values with UI form rendering data + QUnit.config.urlConfig.push({ + id: 'hidepassed', + label: 'Hide passed tests', + tooltip: 'Only show tests and assertions that fail. Stored as query-strings.' + }, { + id: 'noglobals', + label: 'Check for Globals', + tooltip: 'Enabling this will test if any test introduces new properties on the ' + 'global object (`window` in Browsers). Stored as query-strings.' + }, { + id: 'notrycatch', + label: 'No try-catch', + tooltip: 'Enabling this will run tests outside of a try-catch block. Makes debugging ' + 'exceptions in IE reasonable. Stored as query-strings.' + }); + QUnit.begin(function () { + var urlConfig = QUnit.config.urlConfig; for (var i = 0; i < urlConfig.length; i++) { // Options can be either strings or objects with nonempty "id" properties - var val = config.urlConfig[i]; - if (typeof val === 'string') { - val = { - id: val, - label: val - }; + var option = QUnit.config.urlConfig[i]; + if (typeof option !== 'string') { + option = option.id; } - var escaped = escapeText(val.id); - var escapedTooltip = escapeText(val.tooltip); - if (!val.value || typeof val.value === 'string') { - urlConfigHtml += "'; - } else { - urlConfigHtml += "'; + if (QUnit.config[option] === undefined) { + QUnit.config[option] = urlParams[option]; } } - return urlConfigHtml; - } - - // Handle "click" events on toolbar checkboxes and "change" for select menus. - // Updates the URL with the new state of `config.urlConfig` values. - function toolbarChanged() { - var field = this; - var params = {}; - - // Detect if field is a select menu or a checkbox - var value; - if ('selectedIndex' in field) { - value = field.options[field.selectedIndex].value || undefined; - } else { - value = field.checked ? field.defaultValue || true : undefined; - } - params[field.name] = value; - var updatedUrl = setUrl(params); + }); + function getUrlParams() { + var urlParams = Object.create(null); + var params = location.search.slice(1).split('&'); + var length = params.length; + for (var i = 0; i < length; i++) { + if (params[i]) { + var param = params[i].split('='); + var name = decodeQueryParam(param[0]); - // Check if we can apply the change without a page refresh - if (field.name === 'hidepassed' && 'replaceState' in window$1.history) { - QUnit.urlParams[field.name] = value; - config[field.name] = value || false; - var tests = id('qunit-tests'); - if (tests) { - var length = tests.children.length; - var children = tests.children; - if (field.checked) { - for (var i = 0; i < length; i++) { - var test = children[i]; - var className = test ? test.className : ''; - var classNameHasPass = className.indexOf('pass') > -1; - var classNameHasSkipped = className.indexOf('skipped') > -1; - if (classNameHasPass || classNameHasSkipped) { - hiddenTests.push(test); - } - } - var _iterator = _createForOfIteratorHelper(hiddenTests), - _step; - try { - for (_iterator.s(); !(_step = _iterator.n()).done;) { - var hiddenTest = _step.value; - tests.removeChild(hiddenTest); - } - } catch (err) { - _iterator.e(err); - } finally { - _iterator.f(); - } + // Allow just a key to turn on a flag, e.g., test.html?noglobals + var value = param.length === 1 || decodeQueryParam(param.slice(1).join('=')); + if (name in urlParams) { + urlParams[name] = [].concat(urlParams[name], value); } else { - var _test; - while ((_test = hiddenTests.pop()) != null) { - tests.appendChild(_test); - } - } - } - window$1.history.replaceState(null, '', updatedUrl); - } else { - window$1.location = updatedUrl; - } - } - function setUrl(params) { - var querystring = '?'; - var location = window$1.location; - params = extend(extend({}, QUnit.urlParams), params); - for (var key in params) { - // Skip inherited or undefined properties - if (hasOwn.call(params, key) && params[key] !== undefined) { - // Output a parameter for each value of this key - // (but usually just one) - var arrValue = [].concat(params[key]); - for (var i = 0; i < arrValue.length; i++) { - querystring += encodeURIComponent(key); - if (arrValue[i] !== true) { - querystring += '=' + encodeURIComponent(arrValue[i]); - } - querystring += '&'; + urlParams[name] = value; } } } - return location.protocol + '//' + location.host + location.pathname + querystring.slice(0, -1); - } - function applyUrlParams() { - var filter = id('qunit-filter-input').value; - window$1.location = setUrl({ - filter: filter === '' ? undefined : filter, - moduleId: _toConsumableArray(dropdownData.selectedMap.keys()), - // Remove module and testId filter - module: undefined, - testId: undefined - }); - } - function toolbarUrlConfigContainer() { - var urlConfigContainer = document.createElement('span'); - urlConfigContainer.innerHTML = getUrlConfigHtml(); - addClass(urlConfigContainer, 'qunit-url-config'); - addEvents(urlConfigContainer.getElementsByTagName('input'), 'change', toolbarChanged); - addEvents(urlConfigContainer.getElementsByTagName('select'), 'change', toolbarChanged); - return urlConfigContainer; - } - function abortTestsButton() { - var button = document.createElement('button'); - button.id = 'qunit-abort-tests-button'; - button.innerHTML = 'Abort'; - addEvent(button, 'click', abortTests); - return button; - } - function toolbarLooseFilter() { - var filter = document.createElement('form'); - var label = document.createElement('label'); - var input = document.createElement('input'); - var button = document.createElement('button'); - addClass(filter, 'qunit-filter'); - label.innerHTML = 'Filter: '; - input.type = 'text'; - input.value = config.filter || ''; - input.name = 'filter'; - input.id = 'qunit-filter-input'; - button.innerHTML = 'Go'; - label.appendChild(input); - filter.appendChild(label); - filter.appendChild(document.createTextNode(' ')); - filter.appendChild(button); - addEvent(filter, 'submit', interceptNavigation); - return filter; + return urlParams; } - function createModuleListItem(moduleId, name, checked) { - return '
  • '; + function decodeQueryParam(param) { + return decodeURIComponent(param.replace(/\+/g, '%20')); } + })(); - /** - * @param {Array} Results from fuzzysort - * @return {string} HTML - */ - function moduleListHtml(results) { - var html = ''; + var fuzzysort$1 = {exports: {}}; - // Hoist the already selected items, and show them always - // even if not matched by the current search. - dropdownData.selectedMap.forEach(function (name, moduleId) { - html += createModuleListItem(moduleId, name, true); - }); - for (var i = 0; i < results.length; i++) { - var mod = results[i].obj; - if (!dropdownData.selectedMap.has(mod.moduleId)) { - html += createModuleListItem(mod.moduleId, mod.name, false); - } - } - return html; - } - function toolbarModuleFilter(beginDetails) { - var initialSelected = null; - dropdownData = { - options: beginDetails.modules.slice(), - selectedMap: new StringMap(), - isDirty: function isDirty() { - return _toConsumableArray(dropdownData.selectedMap.keys()).sort().join(',') !== _toConsumableArray(initialSelected.keys()).sort().join(','); - } - }; - if (config.moduleId.length) { - // The module dropdown is seeded with the runtime configuration of the last run. - // - // We don't reference `config.moduleId` directly after this and keep our own - // copy because: - // 1. This naturally filters out unknown moduleIds. - // 2. Gives us a place to manage and remember unsubmitted checkbox changes. - // 3. Gives us an efficient way to map a selected moduleId to module name - // during rendering. - for (var i = 0; i < beginDetails.modules.length; i++) { - var mod = beginDetails.modules[i]; - if (config.moduleId.indexOf(mod.moduleId) !== -1) { - dropdownData.selectedMap.set(mod.moduleId, mod.name); - } - } - } - initialSelected = new StringMap(dropdownData.selectedMap); - var moduleSearch = document.createElement('input'); - moduleSearch.id = 'qunit-modulefilter-search'; - moduleSearch.autocomplete = 'off'; - addEvent(moduleSearch, 'input', searchInput); - addEvent(moduleSearch, 'input', searchFocus); - addEvent(moduleSearch, 'focus', searchFocus); - addEvent(moduleSearch, 'click', searchFocus); - var label = document.createElement('label'); - label.htmlFor = 'qunit-modulefilter-search'; - label.textContent = 'Module:'; - var searchContainer = document.createElement('span'); - searchContainer.id = 'qunit-modulefilter-search-container'; - searchContainer.appendChild(moduleSearch); - var applyButton = document.createElement('button'); - applyButton.textContent = 'Apply'; - applyButton.title = 'Re-run the selected test modules'; - addEvent(applyButton, 'click', applyUrlParams); - var resetButton = document.createElement('button'); - resetButton.textContent = 'Reset'; - resetButton.type = 'reset'; - resetButton.title = 'Restore the previous module selection'; - var clearButton = document.createElement('button'); - clearButton.textContent = 'Select none'; - clearButton.type = 'button'; - clearButton.title = 'Clear the current module selection'; - addEvent(clearButton, 'click', function () { - dropdownData.selectedMap.clear(); - selectionChange(); - searchInput(); - }); - var actions = document.createElement('span'); - actions.id = 'qunit-modulefilter-actions'; - actions.appendChild(applyButton); - actions.appendChild(resetButton); - if (initialSelected.size) { - // Only show clear button if functionally different from reset - actions.appendChild(clearButton); - } - var dropDownList = document.createElement('ul'); - dropDownList.id = 'qunit-modulefilter-dropdown-list'; - var dropDown = document.createElement('div'); - dropDown.id = 'qunit-modulefilter-dropdown'; - dropDown.style.display = 'none'; - dropDown.appendChild(actions); - dropDown.appendChild(dropDownList); - addEvent(dropDown, 'change', selectionChange); - searchContainer.appendChild(dropDown); - // Set initial moduleSearch.placeholder and clearButton/resetButton. - selectionChange(); - var moduleFilter = document.createElement('form'); - moduleFilter.id = 'qunit-modulefilter'; - moduleFilter.appendChild(label); - moduleFilter.appendChild(document.createTextNode(' ')); - moduleFilter.appendChild(searchContainer); - addEvent(moduleFilter, 'submit', interceptNavigation); - addEvent(moduleFilter, 'reset', function () { - dropdownData.selectedMap = new StringMap(initialSelected); - // Set moduleSearch.placeholder and reflect non-dirty state - selectionChange(); - searchInput(); - }); + (function (module) { + (function (root, UMD) { + if (module.exports) module.exports = UMD();else root.fuzzysort = UMD(); + })(commonjsGlobal, function UMD() { + function fuzzysortNew(instanceOptions) { + var fuzzysort = { + single: function single(search, target, options) { + if (search == 'farzher') return { + target: "farzher was here (^-^*)/", + score: 0, + indexes: [0, 1, 2, 3, 4, 5, 6] + }; + if (!search) return null; + if (!isObj(search)) search = fuzzysort.getPreparedSearch(search); + if (!target) return null; + if (!isObj(target)) target = fuzzysort.getPrepared(target); + var allowTypo = options && options.allowTypo !== undefined ? options.allowTypo : instanceOptions && instanceOptions.allowTypo !== undefined ? instanceOptions.allowTypo : true; + var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo; + return algorithm(search, target, search[0]); + }, + go: function go(search, targets, options) { + if (search == 'farzher') return [{ + target: "farzher was here (^-^*)/", + score: 0, + indexes: [0, 1, 2, 3, 4, 5, 6], + obj: targets ? targets[0] : null + }]; + if (!search) return noResults; + search = fuzzysort.prepareSearch(search); + var searchLowerCode = search[0]; + var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991; + var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991; + var allowTypo = options && options.allowTypo !== undefined ? options.allowTypo : instanceOptions && instanceOptions.allowTypo !== undefined ? instanceOptions.allowTypo : true; + var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo; + var resultsLen = 0; + var limitedCount = 0; + var targetsLen = targets.length; + + // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys] + + // options.keys + if (options && options.keys) { + var scoreFn = options.scoreFn || defaultScoreFn; + var keys = options.keys; + var keysLen = keys.length; + for (var i = targetsLen - 1; i >= 0; --i) { + var obj = targets[i]; + var objResults = new Array(keysLen); + for (var keyI = keysLen - 1; keyI >= 0; --keyI) { + var key = keys[keyI]; + var target = getValue(obj, key); + if (!target) { + objResults[keyI] = null; + continue; + } + if (!isObj(target)) target = fuzzysort.getPrepared(target); + objResults[keyI] = algorithm(search, target, searchLowerCode); + } + objResults.obj = obj; // before scoreFn so scoreFn can use it + var score = scoreFn(objResults); + if (score === null) continue; + if (score < threshold) continue; + objResults.score = score; + if (resultsLen < limit) { + q.add(objResults); + ++resultsLen; + } else { + ++limitedCount; + if (score > q.peek().score) q.replaceTop(objResults); + } + } + + // options.key + } else if (options && options.key) { + var key = options.key; + for (var i = targetsLen - 1; i >= 0; --i) { + var obj = targets[i]; + var target = getValue(obj, key); + if (!target) continue; + if (!isObj(target)) target = fuzzysort.getPrepared(target); + var result = algorithm(search, target, searchLowerCode); + if (result === null) continue; + if (result.score < threshold) continue; + + // have to clone result so duplicate targets from different obj can each reference the correct obj + result = { + target: result.target, + _targetLowerCodes: null, + _nextBeginningIndexes: null, + score: result.score, + indexes: result.indexes, + obj: obj + }; // hidden + + if (resultsLen < limit) { + q.add(result); + ++resultsLen; + } else { + ++limitedCount; + if (result.score > q.peek().score) q.replaceTop(result); + } + } + + // no keys + } else { + for (var i = targetsLen - 1; i >= 0; --i) { + var target = targets[i]; + if (!target) continue; + if (!isObj(target)) target = fuzzysort.getPrepared(target); + var result = algorithm(search, target, searchLowerCode); + if (result === null) continue; + if (result.score < threshold) continue; + if (resultsLen < limit) { + q.add(result); + ++resultsLen; + } else { + ++limitedCount; + if (result.score > q.peek().score) q.replaceTop(result); + } + } + } + if (resultsLen === 0) return noResults; + var results = new Array(resultsLen); + for (var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll(); + results.total = resultsLen + limitedCount; + return results; + }, + goAsync: function goAsync(search, targets, options) { + var canceled = false; + var p = new Promise(function (resolve, reject) { + if (search == 'farzher') return resolve([{ + target: "farzher was here (^-^*)/", + score: 0, + indexes: [0, 1, 2, 3, 4, 5, 6], + obj: targets ? targets[0] : null + }]); + if (!search) return resolve(noResults); + search = fuzzysort.prepareSearch(search); + var searchLowerCode = search[0]; + var q = fastpriorityqueue(); + var iCurrent = targets.length - 1; + var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991; + var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991; + var allowTypo = options && options.allowTypo !== undefined ? options.allowTypo : instanceOptions && instanceOptions.allowTypo !== undefined ? instanceOptions.allowTypo : true; + var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo; + var resultsLen = 0; + var limitedCount = 0; + function step() { + if (canceled) return reject('canceled'); + var startMs = Date.now(); + + // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys] + + // options.keys + if (options && options.keys) { + var scoreFn = options.scoreFn || defaultScoreFn; + var keys = options.keys; + var keysLen = keys.length; + for (; iCurrent >= 0; --iCurrent) { + if (iCurrent % 1000 /*itemsPerCheck*/ === 0) { + if (Date.now() - startMs >= 10 /*asyncInterval*/) { + isNode ? setImmediate(step) : setTimeout(step); + return; + } + } + var obj = targets[iCurrent]; + var objResults = new Array(keysLen); + for (var keyI = keysLen - 1; keyI >= 0; --keyI) { + var key = keys[keyI]; + var target = getValue(obj, key); + if (!target) { + objResults[keyI] = null; + continue; + } + if (!isObj(target)) target = fuzzysort.getPrepared(target); + objResults[keyI] = algorithm(search, target, searchLowerCode); + } + objResults.obj = obj; // before scoreFn so scoreFn can use it + var score = scoreFn(objResults); + if (score === null) continue; + if (score < threshold) continue; + objResults.score = score; + if (resultsLen < limit) { + q.add(objResults); + ++resultsLen; + } else { + ++limitedCount; + if (score > q.peek().score) q.replaceTop(objResults); + } + } + + // options.key + } else if (options && options.key) { + var key = options.key; + for (; iCurrent >= 0; --iCurrent) { + if (iCurrent % 1000 /*itemsPerCheck*/ === 0) { + if (Date.now() - startMs >= 10 /*asyncInterval*/) { + isNode ? setImmediate(step) : setTimeout(step); + return; + } + } + var obj = targets[iCurrent]; + var target = getValue(obj, key); + if (!target) continue; + if (!isObj(target)) target = fuzzysort.getPrepared(target); + var result = algorithm(search, target, searchLowerCode); + if (result === null) continue; + if (result.score < threshold) continue; + + // have to clone result so duplicate targets from different obj can each reference the correct obj + result = { + target: result.target, + _targetLowerCodes: null, + _nextBeginningIndexes: null, + score: result.score, + indexes: result.indexes, + obj: obj + }; // hidden + + if (resultsLen < limit) { + q.add(result); + ++resultsLen; + } else { + ++limitedCount; + if (result.score > q.peek().score) q.replaceTop(result); + } + } + + // no keys + } else { + for (; iCurrent >= 0; --iCurrent) { + if (iCurrent % 1000 /*itemsPerCheck*/ === 0) { + if (Date.now() - startMs >= 10 /*asyncInterval*/) { + isNode ? setImmediate(step) : setTimeout(step); + return; + } + } + var target = targets[iCurrent]; + if (!target) continue; + if (!isObj(target)) target = fuzzysort.getPrepared(target); + var result = algorithm(search, target, searchLowerCode); + if (result === null) continue; + if (result.score < threshold) continue; + if (resultsLen < limit) { + q.add(result); + ++resultsLen; + } else { + ++limitedCount; + if (result.score > q.peek().score) q.replaceTop(result); + } + } + } + if (resultsLen === 0) return resolve(noResults); + var results = new Array(resultsLen); + for (var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll(); + results.total = resultsLen + limitedCount; + resolve(results); + } + isNode ? setImmediate(step) : step(); //setTimeout here is too slow + }); + p.cancel = function () { + canceled = true; + }; + return p; + }, + highlight: function highlight(result, hOpen, hClose) { + if (typeof hOpen == 'function') return fuzzysort.highlightCallback(result, hOpen); + if (result === null) return null; + if (hOpen === undefined) hOpen = ''; + if (hClose === undefined) hClose = ''; + var highlighted = ''; + var matchesIndex = 0; + var opened = false; + var target = result.target; + var targetLen = target.length; + var matchesBest = result.indexes; + for (var i = 0; i < targetLen; ++i) { + var char = target[i]; + if (matchesBest[matchesIndex] === i) { + ++matchesIndex; + if (!opened) { + opened = true; + highlighted += hOpen; + } + if (matchesIndex === matchesBest.length) { + highlighted += char + hClose + target.substr(i + 1); + break; + } + } else { + if (opened) { + opened = false; + highlighted += hClose; + } + } + highlighted += char; + } + return highlighted; + }, + highlightCallback: function highlightCallback(result, cb) { + if (result === null) return null; + var target = result.target; + var targetLen = target.length; + var indexes = result.indexes; + var highlighted = ''; + var matchI = 0; + var indexesI = 0; + var opened = false; + var result = []; + for (var i = 0; i < targetLen; ++i) { + var char = target[i]; + if (indexes[indexesI] === i) { + ++indexesI; + if (!opened) { + opened = true; + result.push(highlighted); + highlighted = ''; + } + if (indexesI === indexes.length) { + highlighted += char; + result.push(cb(highlighted, matchI++)); + highlighted = ''; + result.push(target.substr(i + 1)); + break; + } + } else { + if (opened) { + opened = false; + result.push(cb(highlighted, matchI++)); + highlighted = ''; + } + } + highlighted += char; + } + return result; + }, + prepare: function prepare(target) { + if (!target) return { + target: '', + _targetLowerCodes: [0 /*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], + _nextBeginningIndexes: null, + score: null, + indexes: null, + obj: null + }; // hidden + return { + target: target, + _targetLowerCodes: fuzzysort.prepareLowerCodes(target), + _nextBeginningIndexes: null, + score: null, + indexes: null, + obj: null + }; // hidden + }, + prepareSlow: function prepareSlow(target) { + if (!target) return { + target: '', + _targetLowerCodes: [0 /*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], + _nextBeginningIndexes: null, + score: null, + indexes: null, + obj: null + }; // hidden + return { + target: target, + _targetLowerCodes: fuzzysort.prepareLowerCodes(target), + _nextBeginningIndexes: fuzzysort.prepareNextBeginningIndexes(target), + score: null, + indexes: null, + obj: null + }; // hidden + }, + prepareSearch: function prepareSearch(search) { + if (!search) search = ''; + return fuzzysort.prepareLowerCodes(search); + }, + // Below this point is only internal code + // Below this point is only internal code + // Below this point is only internal code + // Below this point is only internal code + + getPrepared: function getPrepared(target) { + if (target.length > 999) return fuzzysort.prepare(target); // don't cache huge targets + var targetPrepared = preparedCache.get(target); + if (targetPrepared !== undefined) return targetPrepared; + targetPrepared = fuzzysort.prepare(target); + preparedCache.set(target, targetPrepared); + return targetPrepared; + }, + getPreparedSearch: function getPreparedSearch(search) { + if (search.length > 999) return fuzzysort.prepareSearch(search); // don't cache huge searches + var searchPrepared = preparedSearchCache.get(search); + if (searchPrepared !== undefined) return searchPrepared; + searchPrepared = fuzzysort.prepareSearch(search); + preparedSearchCache.set(search, searchPrepared); + return searchPrepared; + }, + algorithm: function algorithm(searchLowerCodes, prepared, searchLowerCode) { + var targetLowerCodes = prepared._targetLowerCodes; + var searchLen = searchLowerCodes.length; + var targetLen = targetLowerCodes.length; + var searchI = 0; // where we at + var targetI = 0; // where you at + var typoSimpleI = 0; + var matchesSimpleLen = 0; - // Enables show/hide for the dropdown - function searchFocus() { - if (dropDown.style.display !== 'none') { - return; - } + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for (;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI]; + if (isMatch) { + matchesSimple[matchesSimpleLen++] = targetI; + ++searchI; + if (searchI === searchLen) break; + searchLowerCode = searchLowerCodes[typoSimpleI === 0 ? searchI : typoSimpleI === searchI ? searchI + 1 : typoSimpleI === searchI - 1 ? searchI - 1 : searchI]; + } + ++targetI; + if (targetI >= targetLen) { + // Failed to find searchI + // Check for typo or exit + // we go as far as possible before trying to transpose + // then we transpose backwards until we reach the beginning + for (;;) { + if (searchI <= 1) return null; // not allowed to transpose first char + if (typoSimpleI === 0) { + // we haven't tried to transpose yet + --searchI; + var searchLowerCodeNew = searchLowerCodes[searchI]; + if (searchLowerCode === searchLowerCodeNew) continue; // doesn't make sense to transpose a repeat char + typoSimpleI = searchI; + } else { + if (typoSimpleI === 1) return null; // reached the end of the line for transposing + --typoSimpleI; + searchI = typoSimpleI; + searchLowerCode = searchLowerCodes[searchI + 1]; + var searchLowerCodeNew = searchLowerCodes[searchI]; + if (searchLowerCode === searchLowerCodeNew) continue; // doesn't make sense to transpose a repeat char + } + matchesSimpleLen = searchI; + targetI = matchesSimple[matchesSimpleLen - 1] + 1; + break; + } + } + } + var searchI = 0; + var typoStrictI = 0; + var successStrict = false; + var matchesStrictLen = 0; + var nextBeginningIndexes = prepared._nextBeginningIndexes; + if (nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target); + var firstPossibleI = targetI = matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[matchesSimple[0] - 1]; - // Optimization: Defer rendering options until focussed. - // https://github.com/qunitjs/qunit/issues/1664 - searchInput(); - dropDown.style.display = 'block'; + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + if (targetI !== targetLen) for (;;) { + if (targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if (searchI <= 0) { + // We failed to push chars forward for a better match + // transpose, starting from the beginning + ++typoStrictI; + if (typoStrictI > searchLen - 2) break; + if (searchLowerCodes[typoStrictI] === searchLowerCodes[typoStrictI + 1]) continue; // doesn't make sense to transpose a repeat char + targetI = firstPossibleI; + continue; + } + --searchI; + var lastMatch = matchesStrict[--matchesStrictLen]; + targetI = nextBeginningIndexes[lastMatch]; + } else { + var isMatch = searchLowerCodes[typoStrictI === 0 ? searchI : typoStrictI === searchI ? searchI + 1 : typoStrictI === searchI - 1 ? searchI - 1 : searchI] === targetLowerCodes[targetI]; + if (isMatch) { + matchesStrict[matchesStrictLen++] = targetI; + ++searchI; + if (searchI === searchLen) { + successStrict = true; + break; + } + ++targetI; + } else { + targetI = nextBeginningIndexes[targetI]; + } + } + } + { + // tally up the score & keep track of matches for highlighting later + if (successStrict) { + var matchesBest = matchesStrict; + var matchesBestLen = matchesStrictLen; + } else { + var matchesBest = matchesSimple; + var matchesBestLen = matchesSimpleLen; + } + var score = 0; + var lastTargetI = -1; + for (var i = 0; i < searchLen; ++i) { + var targetI = matchesBest[i]; + // score only goes down if they're not consecutive + if (lastTargetI !== targetI - 1) score -= targetI; + lastTargetI = targetI; + } + if (!successStrict) { + score *= 1000; + if (typoSimpleI !== 0) score += -20; /*typoPenalty*/ + } else { + if (typoStrictI !== 0) score += -20; /*typoPenalty*/ + } + score -= targetLen - searchLen; + prepared.score = score; + prepared.indexes = new Array(matchesBestLen); + for (var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i]; + return prepared; + } + }, + algorithmNoTypo: function algorithmNoTypo(searchLowerCodes, prepared, searchLowerCode) { + var targetLowerCodes = prepared._targetLowerCodes; + var searchLen = searchLowerCodes.length; + var targetLen = targetLowerCodes.length; + var searchI = 0; // where we at + var targetI = 0; // where you at + var matchesSimpleLen = 0; - // Hide on Escape keydown or on click outside the container - addEvent(document, 'click', hideHandler); - addEvent(document, 'keydown', hideHandler); - function hideHandler(e) { - var inContainer = moduleFilter.contains(e.target); - if (e.keyCode === 27 || !inContainer) { - if (e.keyCode === 27 && inContainer) { - moduleSearch.focus(); + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for (;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI]; + if (isMatch) { + matchesSimple[matchesSimpleLen++] = targetI; + ++searchI; + if (searchI === searchLen) break; + searchLowerCode = searchLowerCodes[searchI]; + } + ++targetI; + if (targetI >= targetLen) return null; // Failed to find searchI + } + var searchI = 0; + var successStrict = false; + var matchesStrictLen = 0; + var nextBeginningIndexes = prepared._nextBeginningIndexes; + if (nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target); + targetI = matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[matchesSimple[0] - 1]; + + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + if (targetI !== targetLen) for (;;) { + if (targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if (searchI <= 0) break; // We failed to push chars forward for a better match + + --searchI; + var lastMatch = matchesStrict[--matchesStrictLen]; + targetI = nextBeginningIndexes[lastMatch]; + } else { + var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]; + if (isMatch) { + matchesStrict[matchesStrictLen++] = targetI; + ++searchI; + if (searchI === searchLen) { + successStrict = true; + break; + } + ++targetI; + } else { + targetI = nextBeginningIndexes[targetI]; + } + } + } + { + // tally up the score & keep track of matches for highlighting later + if (successStrict) { + var matchesBest = matchesStrict; + var matchesBestLen = matchesStrictLen; + } else { + var matchesBest = matchesSimple; + var matchesBestLen = matchesSimpleLen; + } + var score = 0; + var lastTargetI = -1; + for (var i = 0; i < searchLen; ++i) { + var targetI = matchesBest[i]; + // score only goes down if they're not consecutive + if (lastTargetI !== targetI - 1) score -= targetI; + lastTargetI = targetI; + } + if (!successStrict) score *= 1000; + score -= targetLen - searchLen; + prepared.score = score; + prepared.indexes = new Array(matchesBestLen); + for (var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i]; + return prepared; + } + }, + prepareLowerCodes: function prepareLowerCodes(str) { + var strLen = str.length; + var lowerCodes = []; // new Array(strLen) sparse array is too slow + var lower = str.toLowerCase(); + for (var i = 0; i < strLen; ++i) lowerCodes[i] = lower.charCodeAt(i); + return lowerCodes; + }, + prepareBeginningIndexes: function prepareBeginningIndexes(target) { + var targetLen = target.length; + var beginningIndexes = []; + var beginningIndexesLen = 0; + var wasUpper = false; + var wasAlphanum = false; + for (var i = 0; i < targetLen; ++i) { + var targetCode = target.charCodeAt(i); + var isUpper = targetCode >= 65 && targetCode <= 90; + var isAlphanum = isUpper || targetCode >= 97 && targetCode <= 122 || targetCode >= 48 && targetCode <= 57; + var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum; + wasUpper = isUpper; + wasAlphanum = isAlphanum; + if (isBeginning) beginningIndexes[beginningIndexesLen++] = i; + } + return beginningIndexes; + }, + prepareNextBeginningIndexes: function prepareNextBeginningIndexes(target) { + var targetLen = target.length; + var beginningIndexes = fuzzysort.prepareBeginningIndexes(target); + var nextBeginningIndexes = []; // new Array(targetLen) sparse array is too slow + var lastIsBeginning = beginningIndexes[0]; + var lastIsBeginningI = 0; + for (var i = 0; i < targetLen; ++i) { + if (lastIsBeginning > i) { + nextBeginningIndexes[i] = lastIsBeginning; + } else { + lastIsBeginning = beginningIndexes[++lastIsBeginningI]; + nextBeginningIndexes[i] = lastIsBeginning === undefined ? targetLen : lastIsBeginning; + } } - dropDown.style.display = 'none'; - removeEvent(document, 'click', hideHandler); - removeEvent(document, 'keydown', hideHandler); - moduleSearch.value = ''; - searchInput(); - } - } - } + return nextBeginningIndexes; + }, + cleanup: cleanup, + new: fuzzysortNew + }; + return fuzzysort; + } // fuzzysortNew - /** - * @param {string} searchText - * @return {string} HTML - */ - function filterModules(searchText) { - var results; - if (searchText === '') { - // Improve on-boarding experience by having an immediate display of - // module names, indicating how the interface works. This also makes - // for a quicker interaction in the common case of small projects. - // Don't mandate typing just to get the menu. - results = dropdownData.options.slice(0, 20).map(function (obj) { - // Fake empty results. https://github.com/farzher/fuzzysort/issues/41 - return { - obj: obj - }; - }); - } else { - results = fuzzysort.go(searchText, dropdownData.options, { - limit: 20, - key: 'name', - allowTypo: true - }); + // This stuff is outside fuzzysortNew, because it's shared with instances of fuzzysort.new() + var isNode = typeof commonjsRequire !== 'undefined' && typeof window === 'undefined'; + var MyMap = typeof Map === 'function' ? Map : function () { + var s = Object.create(null); + this.get = function (k) { + return s[k]; + }; + this.set = function (k, val) { + s[k] = val; + return this; + }; + this.clear = function () { + s = Object.create(null); + }; + }; + var preparedCache = new MyMap(); + var preparedSearchCache = new MyMap(); + var noResults = []; + noResults.total = 0; + var matchesSimple = []; + var matchesStrict = []; + function cleanup() { + preparedCache.clear(); + preparedSearchCache.clear(); + matchesSimple = []; + matchesStrict = []; + } + function defaultScoreFn(a) { + var max = -9007199254740991; + for (var i = a.length - 1; i >= 0; --i) { + var result = a[i]; + if (result === null) continue; + var score = result.score; + if (score > max) max = score; } - return moduleListHtml(results); + if (max === -9007199254740991) return null; + return max; } - // Processes module search box input - var searchInputTimeout; - function searchInput() { - // Use a debounce with a ~0ms timeout. This is effectively instantaneous, - // but is better than undebounced because it avoids an ever-growing - // backlog of unprocessed now-outdated input events if fuzzysearch or - // drodown DOM is slow (e.g. very large test suite). - window$1.clearTimeout(searchInputTimeout); - searchInputTimeout = window$1.setTimeout(function () { - dropDownList.innerHTML = filterModules(moduleSearch.value); - }); + // prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop] + // prop = 'key1.key2' 10ms + // prop = ['key1', 'key2'] 27ms + function getValue(obj, prop) { + var tmp = obj[prop]; + if (tmp !== undefined) return tmp; + var segs = prop; + if (!Array.isArray(prop)) segs = prop.split('.'); + var len = segs.length; + var i = -1; + while (obj && ++i < len) obj = obj[segs[i]]; + return obj; } + function isObj(x) { + return _typeof(x) === 'object'; + } // faster as a function - // Processes checkbox change, or a generic render (initial render, or after reset event) - // Avoid any dropdown rendering here as this is used by toolbarModuleFilter() - // during the initial render, which should not delay test execution. - function selectionChange(evt) { - var checkbox = evt && evt.target || null; - if (checkbox) { - // Update internal state - if (checkbox.checked) { - dropdownData.selectedMap.set(checkbox.value, checkbox.parentNode.textContent); - } else { - dropdownData.selectedMap.delete(checkbox.value); + // Hacked version of https://github.com/lemire/FastPriorityQueue.js + var fastpriorityqueue = function fastpriorityqueue() { + var r = [], + o = 0, + e = {}; + function n() { + for (var e = 0, n = r[e], c = 1; c < o;) { + var f = c + 1; + e = c, f < o && r[f].score < r[c].score && (e = f), r[e - 1 >> 1] = r[e], c = 1 + (e << 1); } - - // Update UI state - toggleClass(checkbox.parentNode, 'checked', checkbox.checked); + for (var a = e - 1 >> 1; e > 0 && n.score < r[a].score; a = (e = a) - 1 >> 1) r[e] = r[a]; + r[e] = n; } - var textForm = dropdownData.selectedMap.size ? dropdownData.selectedMap.size + ' ' + (dropdownData.selectedMap.size === 1 ? 'module' : 'modules') : 'All modules'; - moduleSearch.placeholder = textForm; - moduleSearch.title = 'Type to search through and reduce the list.'; - resetButton.disabled = !dropdownData.isDirty(); - clearButton.style.display = dropdownData.selectedMap.size ? '' : 'none'; - } - return moduleFilter; - } - function appendToolbar(beginDetails) { - var toolbar = id('qunit-testrunner-toolbar'); - if (toolbar) { - toolbar.appendChild(toolbarUrlConfigContainer()); - var toolbarFilters = document.createElement('span'); - toolbarFilters.id = 'qunit-toolbar-filters'; - toolbarFilters.appendChild(toolbarLooseFilter()); - toolbarFilters.appendChild(toolbarModuleFilter(beginDetails)); - var clearfix = document.createElement('div'); - clearfix.className = 'clearfix'; - toolbar.appendChild(toolbarFilters); - toolbar.appendChild(clearfix); - } - } - function appendHeader() { - var header = id('qunit-header'); - if (header) { - header.innerHTML = "
    " + header.innerHTML + ' '; - } + return e.add = function (e) { + var n = o; + r[o++] = e; + for (var c = n - 1 >> 1; n > 0 && e.score < r[c].score; c = (n = c) - 1 >> 1) r[n] = r[c]; + r[n] = e; + }, e.poll = function () { + if (0 !== o) { + var e = r[0]; + return r[0] = r[--o], n(), e; + } + }, e.peek = function (e) { + if (0 !== o) return r[0]; + }, e.replaceTop = function (o) { + r[0] = o, n(); + }, e; + }; + var q = fastpriorityqueue(); // reuse this, except for async, it needs to make its own + + return fuzzysortNew(); + }); // UMD + + // TODO: (performance) wasm version!? + // TODO: (performance) threads? + // TODO: (performance) avoid cache misses + // TODO: (performance) preparedCache is a memory leak + // TODO: (like sublime) backslash === forwardslash + // TODO: (like sublime) spaces: "a b" should do 2 searches 1 for a and 1 for b + // TODO: (scoring) garbage in targets that allows most searches to strict match need a penality + // TODO: (performance) idk if allowTypo is optimized + })(fuzzysort$1); + var fuzzysort = fuzzysort$1.exports; + + var stats = { + failedTests: [], + defined: 0, + completed: 0 + }; + (function () { + // Don't load the HTML Reporter on non-browser environments + if (!window$1 || !document) { + return; } - function appendBanner() { - var banner = id('qunit-banner'); - if (banner) { - banner.className = ''; + QUnit.reporters.perf.init(QUnit); + var config = QUnit.config; + var hiddenTests = []; + var collapseNext = false; + var hasOwn = Object.prototype.hasOwnProperty; + var unfilteredUrl = setUrl({ + filter: undefined, + module: undefined, + moduleId: undefined, + testId: undefined + }); + var dropdownData = null; + function trim(string) { + if (typeof string.trim === 'function') { + return string.trim(); + } else { + return string.replace(/^\s+|\s+$/g, ''); } } - function appendTestResults() { - var tests = id('qunit-tests'); - var result = id('qunit-testresult'); - var controls; - if (result) { - result.parentNode.removeChild(result); - } - if (tests) { - tests.innerHTML = ''; - result = document.createElement('p'); - result.id = 'qunit-testresult'; - result.className = 'result'; - tests.parentNode.insertBefore(result, tests); - result.innerHTML = '
    Running...
     
    ' + '
    ' + '
    '; - controls = id('qunit-testresult-controls'); - } - if (controls) { - controls.appendChild(abortTestsButton()); - } + function addEvent(elem, type, fn) { + elem.addEventListener(type, fn, false); } - function appendFilteredTest() { - var testId = QUnit.config.testId; - if (!testId || testId.length <= 0) { - return ''; - } - return "
    Rerunning selected tests: " + escapeText(testId.join(', ')) + " Run all tests
    "; + function removeEvent(elem, type, fn) { + elem.removeEventListener(type, fn, false); } - function appendUserAgent() { - var userAgent = id('qunit-userAgent'); - if (userAgent) { - userAgent.innerHTML = ''; - userAgent.appendChild(document.createTextNode('QUnit ' + QUnit.version + '; ' + navigator.userAgent)); + function addEvents(elems, type, fn) { + var i = elems.length; + while (i--) { + addEvent(elems[i], type, fn); } } - function appendInterface(beginDetails) { - var qunit = id('qunit'); - - // For compat with QUnit 1.2, and to support fully custom theme HTML, - // we will use any existing elements if no id="qunit" element exists. - // - // Note that we don't fail or fallback to creating it ourselves, - // because not having id="qunit" (and not having the below elements) - // simply means QUnit acts headless, allowing users to use their own - // reporters, or for a test runner to listen for events directly without - // having the HTML reporter actively render anything. - if (qunit) { - qunit.setAttribute('role', 'main'); - - // Since QUnit 1.3, these are created automatically if the page - // contains id="qunit". - qunit.innerHTML = "

    " + escapeText(document.title) + '

    ' + "

    " + "" + appendFilteredTest() + "

    " + "
      "; - } - appendHeader(); - appendBanner(); - appendTestResults(); - appendUserAgent(); - appendToolbar(beginDetails); + function hasClass(elem, name) { + return (' ' + elem.className + ' ').indexOf(' ' + name + ' ') >= 0; } - function appendTest(name, testId, moduleName) { - var tests = id('qunit-tests'); - if (!tests) { - return; + function addClass(elem, name) { + if (!hasClass(elem, name)) { + elem.className += (elem.className ? ' ' : '') + name; } - var title = document.createElement('strong'); - title.innerHTML = getNameHtml(name, moduleName); - var testBlock = document.createElement('li'); - testBlock.appendChild(title); - - // No ID or rerun link for "global failure" blocks - if (testId !== undefined) { - var rerunTrigger = document.createElement('a'); - rerunTrigger.innerHTML = 'Rerun'; - rerunTrigger.href = setUrl({ - testId: testId - }); - testBlock.id = 'qunit-test-output-' + testId; - testBlock.appendChild(rerunTrigger); + } + function toggleClass(elem, name, force) { + if (force || typeof force === 'undefined' && !hasClass(elem, name)) { + addClass(elem, name); + } else { + removeClass(elem, name); } - var assertList = document.createElement('ol'); - assertList.className = 'qunit-assert-list'; - testBlock.appendChild(assertList); - tests.appendChild(testBlock); - return testBlock; } + function removeClass(elem, name) { + var set = ' ' + elem.className + ' '; - // HTML Reporter initialization and load - QUnit.on('runStart', function (runStart) { - stats.defined = runStart.testCounts.total; - }); - QUnit.begin(function (beginDetails) { - // Initialize QUnit elements - // This is done from begin() instead of runStart, because - // urlparams.js uses begin(), which we need to wait for. - // urlparams.js in turn uses begin() to allow plugins to - // add entries to QUnit.config.urlConfig, which may be done - // asynchronously. - // - appendInterface(beginDetails); - }); - function getRerunFailedHtml(failedTests) { - if (failedTests.length === 0) { - return ''; + // Class name may appear multiple times + while (set.indexOf(' ' + name + ' ') >= 0) { + set = set.replace(' ' + name + ' ', ' '); } - var href = setUrl({ - testId: failedTests - }); - return ["
      ", failedTests.length === 1 ? 'Rerun 1 failed test' : 'Rerun ' + failedTests.length + ' failed tests', ''].join(''); + + // Trim for prettiness + elem.className = trim(set); } - QUnit.on('runEnd', function (runEnd) { - var banner = id('qunit-banner'); - var tests = id('qunit-tests'); + function id(name) { + return document.getElementById && document.getElementById(name); + } + function abortTests() { var abortButton = id('qunit-abort-tests-button'); - var assertPassed = config.stats.all - config.stats.bad; - var html = [runEnd.testCounts.total, ' tests completed in ', runEnd.runtime, ' milliseconds, with ', runEnd.testCounts.failed, ' failed, ', runEnd.testCounts.skipped, ' skipped, and ', runEnd.testCounts.todo, ' todo.
      ', "", assertPassed, " assertions of ", config.stats.all, " passed, ", config.stats.bad, ' failed.', getRerunFailedHtml(stats.failedTests)].join(''); - var test; - var assertLi; - var assertList; - - // Update remaining tests to aborted - if (abortButton && abortButton.disabled) { - html = 'Tests aborted after ' + runEnd.runtime + ' milliseconds.'; - for (var i = 0; i < tests.children.length; i++) { - test = tests.children[i]; - if (test.className === '' || test.className === 'running') { - test.className = 'aborted'; - assertList = test.getElementsByTagName('ol')[0]; - assertLi = document.createElement('li'); - assertLi.className = 'fail'; - assertLi.innerHTML = 'Test aborted.'; - assertList.appendChild(assertLi); - } - } - } - if (banner && (!abortButton || abortButton.disabled === false)) { - banner.className = runEnd.status === 'failed' ? 'qunit-fail' : 'qunit-pass'; - } if (abortButton) { - abortButton.parentNode.removeChild(abortButton); - } - if (tests) { - id('qunit-testresult-display').innerHTML = html; - } - if (config.altertitle && document.title) { - // Show ✖ for good, ✔ for bad suite result in title - // use escape sequences in case file gets loaded with non-utf-8 - // charset - document.title = [runEnd.status === 'failed' ? "\u2716" : "\u2714", document.title.replace(/^[\u2714\u2716] /i, '')].join(' '); - } - - // Scroll back to top to show results - if (config.scrolltop && window$1.scrollTo) { - window$1.scrollTo(0, 0); - } - }); - function getNameHtml(name, module) { - var nameHtml = ''; - if (module) { - nameHtml = "" + escapeText(module) + ': '; + abortButton.disabled = true; + abortButton.innerHTML = 'Aborting...'; } - nameHtml += "" + escapeText(name) + ''; - return nameHtml; + QUnit.config.queue.length = 0; + return false; } - function getProgressHtml(stats) { - return [stats.completed, ' / ', stats.defined, ' tests completed.
      '].join(''); + function interceptNavigation(ev) { + // Trim potential accidental whitespace so that QUnit doesn't throw an error about no tests matching the filter. + var filterInputElem = id('qunit-filter-input'); + filterInputElem.value = trim(filterInputElem.value); + applyUrlParams(); + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + return false; } - QUnit.testStart(function (details) { - var running, bad; - appendTest(details.name, details.testId, details.module); - running = id('qunit-testresult-display'); - if (running) { - addClass(running, 'running'); - bad = QUnit.config.reorder && details.previousFailure; - running.innerHTML = [getProgressHtml(stats), bad ? 'Rerunning previously failed test:
      ' : 'Running: ', getNameHtml(details.name, details.module), getRerunFailedHtml(stats.failedTests)].join(''); + function getUrlConfigHtml() { + var selection = false; + var urlConfig = config.urlConfig; + var urlConfigHtml = ''; + for (var i = 0; i < urlConfig.length; i++) { + // Options can be either strings or objects with nonempty "id" properties + var val = config.urlConfig[i]; + if (typeof val === 'string') { + val = { + id: val, + label: val + }; + } + var escaped = escapeText(val.id); + var escapedTooltip = escapeText(val.tooltip); + if (!val.value || typeof val.value === 'string') { + urlConfigHtml += "'; + } else { + urlConfigHtml += "'; + } } - }); - function stripHtml(string) { - // Strip tags, html entity and whitespaces - return string.replace(/<\/?[^>]+(>|$)/g, '').replace(/"/g, '').replace(/\s+/g, ''); + return urlConfigHtml; } - QUnit.log(function (details) { - var testItem = id('qunit-test-output-' + details.testId); - if (!testItem) { - return; + + // Handle "click" events on toolbar checkboxes and "change" for select menus. + // Updates the URL with the new state of `config.urlConfig` values. + function toolbarChanged() { + var field = this; + var params = {}; + + // Detect if field is a select menu or a checkbox + var value; + if ('selectedIndex' in field) { + value = field.options[field.selectedIndex].value || undefined; + } else { + value = field.checked ? field.defaultValue || true : undefined; } - var message = escapeText(details.message) || (details.result ? 'okay' : 'failed'); - message = "" + message + ''; - message += "@ " + details.runtime + ' ms'; - var expected; - var actual; - var diff; - var showDiff = false; + params[field.name] = value; + var updatedUrl = setUrl(params); - // The pushFailure doesn't provide details.expected - // when it calls, it's implicit to also not show expected and diff stuff - // Also, we need to check details.expected existence, as it can exist and be undefined - if (!details.result && hasOwn.call(details, 'expected')) { - if (details.negative) { - expected = 'NOT ' + QUnit.dump.parse(details.expected); - } else { - expected = QUnit.dump.parse(details.expected); - } - actual = QUnit.dump.parse(details.actual); - message += "'; - if (actual !== expected) { - message += "'; - if (typeof details.actual === 'number' && typeof details.expected === 'number') { - if (!isNaN(details.actual) && !isNaN(details.expected)) { - showDiff = true; - diff = details.actual - details.expected; - diff = (diff > 0 ? '+' : '') + diff; + // Check if we can apply the change without a page refresh + if (field.name === 'hidepassed' && 'replaceState' in window$1.history) { + QUnit.urlParams[field.name] = value; + config[field.name] = value || false; + var tests = id('qunit-tests'); + if (tests) { + if (field.checked) { + var length = tests.children.length; + var children = tests.children; + for (var i = 0; i < length; i++) { + var test = children[i]; + var className = test ? test.className : ''; + var classNameHasPass = className.indexOf('pass') > -1; + var classNameHasSkipped = className.indexOf('skipped') > -1; + if (classNameHasPass || classNameHasSkipped) { + hiddenTests.push(test); + } } - } else if (typeof details.actual !== 'boolean' && typeof details.expected !== 'boolean') { - diff = QUnit.diff(expected, actual); - // don't show diff if there is zero overlap - showDiff = stripHtml(diff).length !== stripHtml(expected).length + stripHtml(actual).length; - } - if (showDiff) { - message += "'; + // Optimization: Avoid `for-of` iterator overhead. + for (var _i = 0; _i < hiddenTests.length; _i++) { + tests.removeChild(hiddenTests[_i]); + } + } else { + // Optimization: Avoid `while (arr.length) arr.shift()` which would mutate the array many times. + // As of Chrome 126, HTMLElement.append(...hiddenTests) is still slower than + // calling appendChild in a loop. + for (var _i2 = 0; _i2 < hiddenTests.length; _i2++) { + tests.appendChild(hiddenTests[_i2]); + } + hiddenTests.length = 0; } - } else if (expected.indexOf('[object Array]') !== -1 || expected.indexOf('[object Object]') !== -1) { - message += "'; - } else { - message += "'; } - if (details.source) { - message += "'; + window$1.history.replaceState(null, '', updatedUrl); + } else { + window$1.location = updatedUrl; + } + } + function setUrl(params) { + var querystring = '?'; + var location = window$1.location; + params = extend(extend({}, QUnit.urlParams), params); + for (var key in params) { + // Skip inherited or undefined properties + if (hasOwn.call(params, key) && params[key] !== undefined) { + // Output a parameter for each value of this key + // (but usually just one) + var arrValue = [].concat(params[key]); + for (var i = 0; i < arrValue.length; i++) { + querystring += encodeURIComponent(key); + if (arrValue[i] !== true) { + querystring += '=' + encodeURIComponent(arrValue[i]); + } + querystring += '&'; + } } - message += '
      Expected:
      " + escapeText(expected) + '
      Result:
      " + escapeText(actual) + '
      Diff:
      " + diff + '
      Message: " + 'Diff suppressed as the depth of object is more than current max depth (' + QUnit.config.maxDepth + ').

      Hint: Use QUnit.dump.maxDepth to ' + " run with a higher max depth or " + 'Rerun without max depth.

      Message: " + 'Diff suppressed as the expected and actual results have an equivalent' + ' serialization
      Source:
      " + escapeText(details.source) + '
      '; + } + return location.protocol + '//' + location.host + location.pathname + querystring.slice(0, -1); + } + function applyUrlParams() { + var filter = id('qunit-filter-input').value; + window$1.location = setUrl({ + filter: filter === '' ? undefined : filter, + moduleId: _toConsumableArray(dropdownData.selectedMap.keys()), + // Remove module and testId filter + module: undefined, + testId: undefined + }); + } + function toolbarUrlConfigContainer() { + var urlConfigContainer = document.createElement('span'); + urlConfigContainer.innerHTML = getUrlConfigHtml(); + addClass(urlConfigContainer, 'qunit-url-config'); + addEvents(urlConfigContainer.getElementsByTagName('input'), 'change', toolbarChanged); + addEvents(urlConfigContainer.getElementsByTagName('select'), 'change', toolbarChanged); + return urlConfigContainer; + } + function abortTestsButton() { + var button = document.createElement('button'); + button.id = 'qunit-abort-tests-button'; + button.innerHTML = 'Abort'; + addEvent(button, 'click', abortTests); + return button; + } + function toolbarLooseFilter() { + var filter = document.createElement('form'); + var label = document.createElement('label'); + var input = document.createElement('input'); + var button = document.createElement('button'); + addClass(filter, 'qunit-filter'); + label.innerHTML = 'Filter: '; + input.type = 'text'; + input.value = config.filter || ''; + input.name = 'filter'; + input.id = 'qunit-filter-input'; + button.innerHTML = 'Go'; + label.appendChild(input); + filter.appendChild(label); + filter.appendChild(document.createTextNode(' ')); + filter.appendChild(button); + addEvent(filter, 'submit', interceptNavigation); + return filter; + } + function createModuleListItem(moduleId, name, checked) { + return '
    1. '; + } - // This occurs when pushFailure is set and we have an extracted stack trace - } else if (!details.result && details.source) { - message += '' + "' + '
      Source:
      " + escapeText(details.source) + '
      '; + /** + * @param {Array} Results from fuzzysort + * @return {string} HTML + */ + function moduleListHtml(results) { + var html = ''; + + // Hoist the already selected items, and show them always + // even if not matched by the current search. + dropdownData.selectedMap.forEach(function (name, moduleId) { + html += createModuleListItem(moduleId, name, true); + }); + for (var i = 0; i < results.length; i++) { + var mod = results[i].obj; + if (!dropdownData.selectedMap.has(mod.moduleId)) { + html += createModuleListItem(mod.moduleId, mod.name, false); + } } - var assertList = testItem.getElementsByTagName('ol')[0]; - var assertLi = document.createElement('li'); - assertLi.className = details.result ? 'pass' : 'fail'; - assertLi.innerHTML = message; - assertList.appendChild(assertLi); - }); - QUnit.testDone(function (details) { - var tests = id('qunit-tests'); - var testItem = id('qunit-test-output-' + details.testId); - if (!tests || !testItem) { - return; + return html; + } + function toolbarModuleFilter(beginDetails) { + var initialSelected = null; + dropdownData = { + options: beginDetails.modules.slice(), + selectedMap: new StringMap(), + isDirty: function isDirty() { + return _toConsumableArray(dropdownData.selectedMap.keys()).sort().join(',') !== _toConsumableArray(initialSelected.keys()).sort().join(','); + } + }; + if (config.moduleId.length) { + // The module dropdown is seeded with the runtime configuration of the last run. + // + // We don't reference `config.moduleId` directly after this and keep our own + // copy because: + // 1. This naturally filters out unknown moduleIds. + // 2. Gives us a place to manage and remember unsubmitted checkbox changes. + // 3. Gives us an efficient way to map a selected moduleId to module name + // during rendering. + for (var i = 0; i < beginDetails.modules.length; i++) { + var mod = beginDetails.modules[i]; + if (config.moduleId.indexOf(mod.moduleId) !== -1) { + dropdownData.selectedMap.set(mod.moduleId, mod.name); + } + } } - removeClass(testItem, 'running'); - var status; - if (details.failed > 0) { - status = 'failed'; - } else if (details.todo) { - status = 'todo'; - } else { - status = details.skipped ? 'skipped' : 'passed'; + initialSelected = new StringMap(dropdownData.selectedMap); + var moduleSearch = document.createElement('input'); + moduleSearch.id = 'qunit-modulefilter-search'; + moduleSearch.autocomplete = 'off'; + addEvent(moduleSearch, 'input', searchInput); + addEvent(moduleSearch, 'input', searchFocus); + addEvent(moduleSearch, 'focus', searchFocus); + addEvent(moduleSearch, 'click', searchFocus); + var label = document.createElement('label'); + label.htmlFor = 'qunit-modulefilter-search'; + label.textContent = 'Module:'; + var searchContainer = document.createElement('span'); + searchContainer.id = 'qunit-modulefilter-search-container'; + searchContainer.appendChild(moduleSearch); + var applyButton = document.createElement('button'); + applyButton.textContent = 'Apply'; + applyButton.title = 'Re-run the selected test modules'; + addEvent(applyButton, 'click', applyUrlParams); + var resetButton = document.createElement('button'); + resetButton.textContent = 'Reset'; + resetButton.type = 'reset'; + resetButton.title = 'Restore the previous module selection'; + var clearButton = document.createElement('button'); + clearButton.textContent = 'Select none'; + clearButton.type = 'button'; + clearButton.title = 'Clear the current module selection'; + addEvent(clearButton, 'click', function () { + dropdownData.selectedMap.clear(); + selectionChange(); + searchInput(); + }); + var actions = document.createElement('span'); + actions.id = 'qunit-modulefilter-actions'; + actions.appendChild(applyButton); + actions.appendChild(resetButton); + if (initialSelected.size) { + // Only show clear button if functionally different from reset + actions.appendChild(clearButton); } - var assertList = testItem.getElementsByTagName('ol')[0]; - var good = details.passed; - var bad = details.failed; + var dropDownList = document.createElement('ul'); + dropDownList.id = 'qunit-modulefilter-dropdown-list'; + var dropDown = document.createElement('div'); + dropDown.id = 'qunit-modulefilter-dropdown'; + dropDown.style.display = 'none'; + dropDown.appendChild(actions); + dropDown.appendChild(dropDownList); + addEvent(dropDown, 'change', selectionChange); + searchContainer.appendChild(dropDown); + // Set initial moduleSearch.placeholder and clearButton/resetButton. + selectionChange(); + var moduleFilter = document.createElement('form'); + moduleFilter.id = 'qunit-modulefilter'; + moduleFilter.appendChild(label); + moduleFilter.appendChild(document.createTextNode(' ')); + moduleFilter.appendChild(searchContainer); + addEvent(moduleFilter, 'submit', interceptNavigation); + addEvent(moduleFilter, 'reset', function () { + dropdownData.selectedMap = new StringMap(initialSelected); + // Set moduleSearch.placeholder and reflect non-dirty state + selectionChange(); + searchInput(); + }); - // This test passed if it has no unexpected failed assertions - var testPassed = details.failed > 0 ? details.todo : !details.todo; - if (testPassed) { - // Collapse the passing tests - addClass(assertList, 'qunit-collapsed'); - } else { - stats.failedTests.push(details.testId); - if (config.collapse) { - if (!collapseNext) { - // Skip collapsing the first failing test - collapseNext = true; - } else { - // Collapse remaining tests - addClass(assertList, 'qunit-collapsed'); + // Enables show/hide for the dropdown + function searchFocus() { + if (dropDown.style.display !== 'none') { + return; + } + + // Optimization: Defer rendering options until focussed. + // https://github.com/qunitjs/qunit/issues/1664 + searchInput(); + dropDown.style.display = 'block'; + + // Hide on Escape keydown or on click outside the container + addEvent(document, 'click', hideHandler); + addEvent(document, 'keydown', hideHandler); + function hideHandler(e) { + var inContainer = moduleFilter.contains(e.target); + if (e.keyCode === 27 || !inContainer) { + if (e.keyCode === 27 && inContainer) { + moduleSearch.focus(); + } + dropDown.style.display = 'none'; + removeEvent(document, 'click', hideHandler); + removeEvent(document, 'keydown', hideHandler); + moduleSearch.value = ''; + searchInput(); } } } - // The testItem.firstChild is the test name - var testTitle = testItem.firstChild; - var testCounts = bad ? "" + bad + ', ' + "" + good + ', ' : ''; - testTitle.innerHTML += " (" + testCounts + details.assertions.length + ')'; - stats.completed++; - if (details.skipped) { - testItem.className = 'skipped'; - var skipped = document.createElement('em'); - skipped.className = 'qunit-skipped-label'; - skipped.innerHTML = 'skipped'; - testItem.insertBefore(skipped, testTitle); - } else { - addEvent(testTitle, 'click', function () { - toggleClass(assertList, 'qunit-collapsed'); - }); - testItem.className = testPassed ? 'pass' : 'fail'; - if (details.todo) { - var todoLabel = document.createElement('em'); - todoLabel.className = 'qunit-todo-label'; - todoLabel.innerHTML = 'todo'; - testItem.className += ' todo'; - testItem.insertBefore(todoLabel, testTitle); + /** + * @param {string} searchText + * @return {string} HTML + */ + function filterModules(searchText) { + var results; + if (searchText === '') { + // Improve on-boarding experience by having an immediate display of + // module names, indicating how the interface works. This also makes + // for a quicker interaction in the common case of small projects. + // Don't mandate typing just to get the menu. + results = dropdownData.options.slice(0, 20).map(function (obj) { + // Fake empty results. https://github.com/farzher/fuzzysort/issues/41 + return { + obj: obj + }; + }); + } else { + results = fuzzysort.go(searchText, dropdownData.options, { + limit: 20, + key: 'name', + allowTypo: true + }); } - var time = document.createElement('span'); - time.className = 'runtime'; - time.innerHTML = details.runtime + ' ms'; - testItem.insertBefore(time, assertList); + return moduleListHtml(results); } - // Show the source of the test when showing assertions - if (details.source) { - var sourceName = document.createElement('p'); - sourceName.innerHTML = 'Source: ' + escapeText(details.source); - addClass(sourceName, 'qunit-source'); - if (testPassed) { - addClass(sourceName, 'qunit-collapsed'); - } - addEvent(testTitle, 'click', function () { - toggleClass(sourceName, 'qunit-collapsed'); + // Processes module search box input + var searchInputTimeout; + function searchInput() { + // Use a debounce with a ~0ms timeout. This is effectively instantaneous, + // but is better than undebounced because it avoids an ever-growing + // backlog of unprocessed now-outdated input events if fuzzysearch or + // drodown DOM is slow (e.g. very large test suite). + window$1.clearTimeout(searchInputTimeout); + searchInputTimeout = window$1.setTimeout(function () { + dropDownList.innerHTML = filterModules(moduleSearch.value); }); - testItem.appendChild(sourceName); } - if (config.hidepassed && (status === 'passed' || details.skipped)) { - // use removeChild instead of remove because of support - hiddenTests.push(testItem); - tests.removeChild(testItem); + + // Processes checkbox change, or a generic render (initial render, or after reset event) + // Avoid any dropdown rendering here as this is used by toolbarModuleFilter() + // during the initial render, which should not delay test execution. + function selectionChange(evt) { + var checkbox = evt && evt.target || null; + if (checkbox) { + // Update internal state + if (checkbox.checked) { + dropdownData.selectedMap.set(checkbox.value, checkbox.parentNode.textContent); + } else { + dropdownData.selectedMap.delete(checkbox.value); + } + + // Update UI state + toggleClass(checkbox.parentNode, 'checked', checkbox.checked); + } + var textForm = dropdownData.selectedMap.size ? dropdownData.selectedMap.size + ' ' + (dropdownData.selectedMap.size === 1 ? 'module' : 'modules') : 'All modules'; + moduleSearch.placeholder = textForm; + moduleSearch.title = 'Type to search through and reduce the list.'; + resetButton.disabled = !dropdownData.isDirty(); + clearButton.style.display = dropdownData.selectedMap.size ? '' : 'none'; } - }); - QUnit.on('error', function (error) { - var testItem = appendTest('global failure'); - if (!testItem) { - // HTML Reporter is probably disabled or not yet initialized. - return; + return moduleFilter; + } + function appendToolbar(beginDetails) { + var toolbar = id('qunit-testrunner-toolbar'); + if (toolbar) { + toolbar.appendChild(toolbarUrlConfigContainer()); + var toolbarFilters = document.createElement('span'); + toolbarFilters.id = 'qunit-toolbar-filters'; + toolbarFilters.appendChild(toolbarLooseFilter()); + toolbarFilters.appendChild(toolbarModuleFilter(beginDetails)); + var clearfix = document.createElement('div'); + clearfix.className = 'clearfix'; + toolbar.appendChild(toolbarFilters); + toolbar.appendChild(clearfix); } - - // Render similar to a failed assertion (see above QUnit.log callback) - var message = escapeText(errorString(error)); - message = "" + message + ''; - if (error && error.stack) { - message += '' + "' + '
      Source:
      " + escapeText(error.stack) + '
      '; + } + function appendHeader() { + var header = id('qunit-header'); + if (header) { + header.innerHTML = "" + header.innerHTML + ' '; } - var assertList = testItem.getElementsByTagName('ol')[0]; - var assertLi = document.createElement('li'); - assertLi.className = 'fail'; - assertLi.innerHTML = message; - assertList.appendChild(assertLi); - - // Make it visible - testItem.className = 'fail'; - }); - - // Avoid readyState issue with phantomjs - // Ref: #818 - var usingPhantom = function (p) { - return p && p.version && p.version.major > 0; - }(window$1.phantom); - if (usingPhantom) { - console$1.warn('Support for PhantomJS is deprecated and will be removed in QUnit 3.0.'); } - if (!usingPhantom && document.readyState === 'complete') { - QUnit.load(); - } else { - addEvent(window$1, 'load', QUnit.load); + function appendBanner() { + var banner = id('qunit-banner'); + if (banner) { + banner.className = ''; + } } - - // Wrap window.onerror. We will call the original window.onerror to see if - // the existing handler fully handles the error; if not, we will call the - // QUnit.onError function. - var originalWindowOnError = window$1.onerror; - - // Cover uncaught exceptions - // Returning true will suppress the default browser handler, - // returning false will let it run. - window$1.onerror = function (message, fileName, lineNumber, columnNumber, errorObj) { - var ret = false; - if (originalWindowOnError) { - for (var _len = arguments.length, args = new Array(_len > 5 ? _len - 5 : 0), _key = 5; _key < _len; _key++) { - args[_key - 5] = arguments[_key]; - } - ret = originalWindowOnError.call.apply(originalWindowOnError, [this, message, fileName, lineNumber, columnNumber, errorObj].concat(args)); + function appendTestResults() { + var tests = id('qunit-tests'); + var result = id('qunit-testresult'); + var controls; + if (result) { + result.parentNode.removeChild(result); } - - // Treat return value as window.onerror itself does, - // Only do our handling if not suppressed. - if (ret !== true) { - // If there is a current test that sets the internal `ignoreGlobalErrors` field - // (such as during `assert.throws()`), then the error is ignored and native - // error reporting is suppressed as well. This is because in browsers, an error - // can sometimes end up in `window.onerror` instead of in the local try/catch. - // This ignoring of errors does not apply to our general onUncaughtException - // method, nor to our `unhandledRejection` handlers, as those are not meant - // to receive an "expected" error during `assert.throws()`. - if (config.current && config.current.ignoreGlobalErrors) { - return true; - } - - // According to - // https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror, - // most modern browsers support an errorObj argument; use that to - // get a full stack trace if it's available. - var error = errorObj || new Error(message); - if (!error.stack && fileName && lineNumber) { - error.stack = "".concat(fileName, ":").concat(lineNumber); - } - QUnit.onUncaughtException(error); + if (tests) { + tests.innerHTML = ''; + result = document.createElement('p'); + result.id = 'qunit-testresult'; + result.className = 'result'; + tests.parentNode.insertBefore(result, tests); + result.innerHTML = '
      Running...
       
      ' + '
      ' + '
      '; + controls = id('qunit-testresult-controls'); } - return ret; - }; - window$1.addEventListener('unhandledrejection', function (event) { - QUnit.onUncaughtException(event.reason); - }); - })(); - - /* - * This file is a modified version of google-diff-match-patch's JavaScript implementation - * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), - * modifications are licensed as more fully set forth in LICENSE.txt. - * - * The original source of google-diff-match-patch is attributable and licensed as follows: - * - * Copyright 2006 Google Inc. - * https://code.google.com/p/google-diff-match-patch/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * More Info: - * https://code.google.com/p/google-diff-match-patch/ - * - * Usage: QUnit.diff(expected, actual) - * - */ - QUnit.diff = function () { - function DiffMatchPatch() {} - - // DIFF FUNCTIONS - - /** - * The data structure representing a diff is an array of tuples: - * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] - * which means: delete 'Hello', add 'Goodbye' and keep ' world.' - */ - var DIFF_DELETE = -1; - var DIFF_INSERT = 1; - var DIFF_EQUAL = 0; - var hasOwn = Object.prototype.hasOwnProperty; - - /** - * Find the differences between two texts. Simplifies the problem by stripping - * any common prefix or suffix off the texts before diffing. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean=} optChecklines Optional speedup flag. If present and false, - * then don't run a line-level diff first to identify the changed areas. - * Defaults to true, which does a faster, slightly less optimal diff. - * @return {!Array.} Array of diff tuples. - */ - DiffMatchPatch.prototype.DiffMain = function (text1, text2, optChecklines) { - // The diff must be complete in up to 1 second. - var deadline = Date.now() + 1000; - - // Check for null inputs. - if (text1 === null || text2 === null) { - throw new Error('Cannot diff null input.'); + if (controls) { + controls.appendChild(abortTestsButton()); } - - // Check for equality (speedup). - if (text1 === text2) { - if (text1) { - return [[DIFF_EQUAL, text1]]; - } - return []; + } + function appendFilteredTest() { + var testId = QUnit.config.testId; + if (!testId || testId.length <= 0) { + return ''; } - if (typeof optChecklines === 'undefined') { - optChecklines = true; + return "
      Rerunning selected tests: " + escapeText(testId.join(', ')) + " Run all tests
      "; + } + function appendUserAgent() { + var userAgent = id('qunit-userAgent'); + if (userAgent) { + userAgent.innerHTML = ''; + userAgent.appendChild(document.createTextNode('QUnit ' + QUnit.version + '; ' + navigator.userAgent)); } + } + function appendInterface(beginDetails) { + var qunit = id('qunit'); - // Trim off common prefix (speedup). - var commonlength = this.diffCommonPrefix(text1, text2); - var commonprefix = text1.substring(0, commonlength); - text1 = text1.substring(commonlength); - text2 = text2.substring(commonlength); - - // Trim off common suffix (speedup). - commonlength = this.diffCommonSuffix(text1, text2); - var commonsuffix = text1.substring(text1.length - commonlength); - text1 = text1.substring(0, text1.length - commonlength); - text2 = text2.substring(0, text2.length - commonlength); - - // Compute the diff on the middle block. - var diffs = this.diffCompute(text1, text2, optChecklines, deadline); + // For compat with QUnit 1.2, and to support fully custom theme HTML, + // we will use any existing elements if no id="qunit" element exists. + // + // Note that we don't fail or fallback to creating it ourselves, + // because not having id="qunit" (and not having the below elements) + // simply means QUnit acts headless, allowing users to use their own + // reporters, or for a test runner to listen for events directly without + // having the HTML reporter actively render anything. + if (qunit) { + qunit.setAttribute('role', 'main'); - // Restore the prefix and suffix. - if (commonprefix) { - diffs.unshift([DIFF_EQUAL, commonprefix]); + // Since QUnit 1.3, these are created automatically if the page + // contains id="qunit". + qunit.innerHTML = "

      " + escapeText(document.title) + '

      ' + "

      " + "" + appendFilteredTest() + "

      " + "
        "; } - if (commonsuffix) { - diffs.push([DIFF_EQUAL, commonsuffix]); + appendHeader(); + appendBanner(); + appendTestResults(); + appendUserAgent(); + appendToolbar(beginDetails); + } + function appendTest(name, testId, moduleName) { + var tests = id('qunit-tests'); + if (!tests) { + return; } - this.diffCleanupMerge(diffs); - return diffs; - }; - - /** - * Reduce the number of edits by eliminating operationally trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupEfficiency = function (diffs) { - var changes, equalities, equalitiesLength, lastequality, pointer, preIns, preDel, postIns, postDel; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - - // Is there an insertion operation before the last equality. - preIns = false; - - // Is there a deletion operation before the last equality. - preDel = false; - - // Is there an insertion operation after the last equality. - postIns = false; + var title = document.createElement('strong'); + title.className = 'qunit-test-name'; + title.innerHTML = getNameHtml(name, moduleName); + var testBlock = document.createElement('li'); + testBlock.appendChild(title); - // Is there a deletion operation after the last equality. - postDel = false; - while (pointer < diffs.length) { - // Equality found. - if (diffs[pointer][0] === DIFF_EQUAL) { - if (diffs[pointer][1].length < 4 && (postIns || postDel)) { - // Candidate found. - equalities[equalitiesLength++] = pointer; - preIns = postIns; - preDel = postDel; - lastequality = diffs[pointer][1]; - } else { - // Not a candidate, and can never become one. - equalitiesLength = 0; - lastequality = null; - } - postIns = postDel = false; + // No ID or rerun link for "global failure" blocks + if (testId !== undefined) { + var rerunTrigger = document.createElement('a'); + rerunTrigger.innerHTML = 'Rerun'; + rerunTrigger.href = setUrl({ + testId: testId + }); + testBlock.id = 'qunit-test-output-' + testId; + testBlock.appendChild(rerunTrigger); + } + var assertList = document.createElement('ol'); + assertList.className = 'qunit-assert-list'; + testBlock.appendChild(assertList); + tests.appendChild(testBlock); + return testBlock; + } - // An insertion or deletion. - } else { - if (diffs[pointer][0] === DIFF_DELETE) { - postDel = true; - } else { - postIns = true; - } + // HTML Reporter initialization and load + QUnit.on('runStart', function (runStart) { + stats.defined = runStart.testCounts.total; + }); + QUnit.begin(function (beginDetails) { + // Initialize QUnit elements + // This is done from begin() instead of runStart, because + // urlparams.js uses begin(), which we need to wait for. + // urlparams.js in turn uses begin() to allow plugins to + // add entries to QUnit.config.urlConfig, which may be done + // asynchronously. + // + appendInterface(beginDetails); + }); + function getRerunFailedHtml(failedTests) { + if (failedTests.length === 0) { + return ''; + } + var href = setUrl({ + testId: failedTests + }); + return ["
        ", failedTests.length === 1 ? 'Rerun 1 failed test' : 'Rerun ' + failedTests.length + ' failed tests', ''].join(''); + } + QUnit.on('runEnd', function (runEnd) { + var banner = id('qunit-banner'); + var tests = id('qunit-tests'); + var abortButton = id('qunit-abort-tests-button'); + var assertPassed = config.stats.all - config.stats.bad; + var html = [runEnd.testCounts.total, ' tests completed in ', runEnd.runtime, ' milliseconds, with ', runEnd.testCounts.failed, ' failed, ', runEnd.testCounts.skipped, ' skipped, and ', runEnd.testCounts.todo, ' todo.
        ', "", assertPassed, " assertions of ", config.stats.all, " passed, ", config.stats.bad, ' failed.', getRerunFailedHtml(stats.failedTests)].join(''); + var test; + var assertLi; + var assertList; - /* - * Five types to be split: - * ABXYCD - * AXCD - * ABXC - * AXCD - * ABXC - */ - if (lastequality && (preIns && preDel && postIns && postDel || lastequality.length < 2 && preIns + preDel + postIns + postDel === 3)) { - // Duplicate record. - diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]); - - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - equalitiesLength--; // Throw away the equality we just deleted; - lastequality = null; - if (preIns && preDel) { - // No changes made which could affect previous entry, keep going. - postIns = postDel = true; - equalitiesLength = 0; - } else { - equalitiesLength--; // Throw away the previous equality. - pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - postIns = postDel = false; - } - changes = true; + // Update remaining tests to aborted + if (abortButton && abortButton.disabled) { + html = 'Tests aborted after ' + runEnd.runtime + ' milliseconds.'; + for (var i = 0; i < tests.children.length; i++) { + test = tests.children[i]; + if (test.className === '' || test.className === 'running') { + test.className = 'aborted'; + assertList = test.getElementsByTagName('ol')[0]; + assertLi = document.createElement('li'); + assertLi.className = 'fail'; + assertLi.innerHTML = 'Test aborted.'; + assertList.appendChild(assertLi); } } - pointer++; } - if (changes) { - this.diffCleanupMerge(diffs); - } - }; - - /** - * Convert a diff array into a pretty HTML report. - * @param {!Array.} diffs Array of diff tuples. - * @param {integer} string to be beautified. - * @return {string} HTML representation. - */ - DiffMatchPatch.prototype.diffPrettyHtml = function (diffs) { - var html = []; - for (var x = 0; x < diffs.length; x++) { - var op = diffs[x][0]; // Operation (insert, delete, equal) - var data = diffs[x][1]; // Text of change. - switch (op) { - case DIFF_INSERT: - html[x] = '' + escapeText(data) + ''; - break; - case DIFF_DELETE: - html[x] = '' + escapeText(data) + ''; - break; - case DIFF_EQUAL: - html[x] = '' + escapeText(data) + ''; - break; - } + if (banner && (!abortButton || abortButton.disabled === false)) { + banner.className = runEnd.status === 'failed' ? 'qunit-fail' : 'qunit-pass'; } - return html.join(''); - }; - - /** - * Determine the common prefix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the start of each - * string. - */ - DiffMatchPatch.prototype.diffCommonPrefix = function (text1, text2) { - var pointermid, pointermax, pointermin, pointerstart; - - // Quick check for common null cases. - if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) { - return 0; - } - - // Binary search. - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min(text1.length, text2.length); - pointermid = pointermax; - pointerstart = 0; - while (pointermin < pointermid) { - if (text1.substring(pointerstart, pointermid) === text2.substring(pointerstart, pointermid)) { - pointermin = pointermid; - pointerstart = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + if (abortButton) { + abortButton.parentNode.removeChild(abortButton); } - return pointermid; - }; - - /** - * Determine the common suffix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of each string. - */ - DiffMatchPatch.prototype.diffCommonSuffix = function (text1, text2) { - var pointermid, pointermax, pointermin, pointerend; - - // Quick check for common null cases. - if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { - return 0; - } - - // Binary search. - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min(text1.length, text2.length); - pointermid = pointermax; - pointerend = 0; - while (pointermin < pointermid) { - if (text1.substring(text1.length - pointermid, text1.length - pointerend) === text2.substring(text2.length - pointermid, text2.length - pointerend)) { - pointermin = pointermid; - pointerend = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + if (tests) { + id('qunit-testresult-display').innerHTML = html; } - return pointermid; - }; - - /** - * Find the differences between two texts. Assumes that the texts do not - * have any common prefix or suffix. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean} checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster, slightly less optimal diff. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffCompute = function (text1, text2, checklines, deadline) { - var diffs, longtext, shorttext, i, hm, text1A, text2A, text1B, text2B, midCommon, diffsA, diffsB; - if (!text1) { - // Just add some text (speedup). - return [[DIFF_INSERT, text2]]; - } - if (!text2) { - // Just delete some text (speedup). - return [[DIFF_DELETE, text1]]; - } - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - i = longtext.indexOf(shorttext); - if (i !== -1) { - // Shorter text is inside the longer text (speedup). - diffs = [[DIFF_INSERT, longtext.substring(0, i)], [DIFF_EQUAL, shorttext], [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; - - // Swap insertions for deletions if diff is reversed. - if (text1.length > text2.length) { - diffs[0][0] = diffs[2][0] = DIFF_DELETE; - } - return diffs; - } - if (shorttext.length === 1) { - // Single character string. - // After the previous speedup, the character can't be an equality. - return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; - } - - // Check to see if the problem can be split in two. - hm = this.diffHalfMatch(text1, text2); - if (hm) { - // A half-match was found, sort out the return data. - text1A = hm[0]; - text1B = hm[1]; - text2A = hm[2]; - text2B = hm[3]; - midCommon = hm[4]; - - // Send both pairs off for separate processing. - diffsA = this.DiffMain(text1A, text2A, checklines, deadline); - diffsB = this.DiffMain(text1B, text2B, checklines, deadline); - - // Merge the results. - return diffsA.concat([[DIFF_EQUAL, midCommon]], diffsB); - } - if (checklines && text1.length > 100 && text2.length > 100) { - return this.diffLineMode(text1, text2, deadline); - } - return this.diffBisect(text1, text2, deadline); - }; - - /** - * Do the two texts share a substring which is at least half the length of the - * longer text? - * This speedup can produce non-minimal diffs. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {Array.} Five element Array, containing the prefix of - * text1, the suffix of text1, the prefix of text2, the suffix of - * text2 and the common middle. Or null if there was no match. - * @private - */ - DiffMatchPatch.prototype.diffHalfMatch = function (text1, text2) { - var longtext, shorttext, dmp, text1A, text2B, text2A, text1B, midCommon, hm1, hm2, hm; - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { - return null; // Pointless. + if (config.altertitle && document.title) { + // Show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8 + // charset + document.title = [runEnd.status === 'failed' ? "\u2716" : "\u2714", document.title.replace(/^[\u2714\u2716] /i, '')].join(' '); } - dmp = this; // 'this' becomes 'window' in a closure. - - /** - * Does a substring of shorttext exist within longtext such that the substring - * is at least half the length of longtext? - * Closure, but does not reference any external variables. - * @param {string} longtext Longer string. - * @param {string} shorttext Shorter string. - * @param {number} i Start index of quarter length substring within longtext. - * @return {Array.} Five element Array, containing the prefix of - * longtext, the suffix of longtext, the prefix of shorttext, the suffix - * of shorttext and the common middle. Or null if there was no match. - * @private - */ - function diffHalfMatchI(longtext, shorttext, i) { - var seed, j, bestCommon, prefixLength, suffixLength, bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; - - // Start with a 1/4 length substring at position i as a seed. - seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); - j = -1; - bestCommon = ''; - while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { - prefixLength = dmp.diffCommonPrefix(longtext.substring(i), shorttext.substring(j)); - suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), shorttext.substring(0, j)); - if (bestCommon.length < suffixLength + prefixLength) { - bestCommon = shorttext.substring(j - suffixLength, j) + shorttext.substring(j, j + prefixLength); - bestLongtextA = longtext.substring(0, i - suffixLength); - bestLongtextB = longtext.substring(i + prefixLength); - bestShorttextA = shorttext.substring(0, j - suffixLength); - bestShorttextB = shorttext.substring(j + prefixLength); - } - } - if (bestCommon.length * 2 >= longtext.length) { - return [bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB, bestCommon]; - } else { - return null; - } + // Scroll back to top to show results + if (config.scrolltop && window$1.scrollTo) { + window$1.scrollTo(0, 0); } - - // First check if the second quarter is the seed for a half-match. - hm1 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 4)); - - // Check again based on the third quarter. - hm2 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 2)); - if (!hm1 && !hm2) { - return null; - } else if (!hm2) { - hm = hm1; - } else if (!hm1) { - hm = hm2; - } else { - // Both matched. Select the longest. - hm = hm1[4].length > hm2[4].length ? hm1 : hm2; + }); + function getNameHtml(name, module) { + var nameHtml = ''; + if (module) { + nameHtml = "" + escapeText(module) + ': '; } - - // A half-match was found, sort out the return data. - if (text1.length > text2.length) { - text1A = hm[0]; - text1B = hm[1]; - text2A = hm[2]; - text2B = hm[3]; - } else { - text2A = hm[0]; - text2B = hm[1]; - text1A = hm[2]; - text1B = hm[3]; + nameHtml += "" + escapeText(name) + ''; + return nameHtml; + } + function getProgressHtml(stats) { + return [stats.completed, ' / ', stats.defined, ' tests completed.
        '].join(''); + } + QUnit.testStart(function (details) { + var running, bad; + appendTest(details.name, details.testId, details.module); + running = id('qunit-testresult-display'); + if (running) { + addClass(running, 'running'); + bad = QUnit.config.reorder && details.previousFailure; + running.innerHTML = [getProgressHtml(stats), bad ? 'Rerunning previously failed test:
        ' : 'Running: ', getNameHtml(details.name, details.module), getRerunFailedHtml(stats.failedTests)].join(''); } - midCommon = hm[4]; - return [text1A, text1B, text2A, text2B, midCommon]; - }; - - /** - * Do a quick line-level diff on both strings, then rediff the parts for - * greater accuracy. - * This speedup can produce non-minimal diffs. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffLineMode = function (text1, text2, deadline) { - var a, diffs, linearray, pointer, countInsert, countDelete, textInsert, textDelete, j; - - // Scan the text on a line-by-line basis first. - a = this.diffLinesToChars(text1, text2); - text1 = a.chars1; - text2 = a.chars2; - linearray = a.lineArray; - diffs = this.DiffMain(text1, text2, false, deadline); - - // Convert the diff back to original text. - this.diffCharsToLines(diffs, linearray); - - // Eliminate freak matches (e.g. blank lines) - this.diffCleanupSemantic(diffs); - - // Rediff any replacement blocks, this time character-by-character. - // Add a dummy entry at the end. - diffs.push([DIFF_EQUAL, '']); - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ''; - textInsert = ''; - while (pointer < diffs.length) { - switch (diffs[pointer][0]) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[pointer][1]; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[pointer][1]; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (countDelete >= 1 && countInsert >= 1) { - // Delete the offending records and add the merged ones. - diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert); - pointer = pointer - countDelete - countInsert; - a = this.DiffMain(textDelete, textInsert, false, deadline); - for (j = a.length - 1; j >= 0; j--) { - diffs.splice(pointer, 0, a[j]); - } - pointer = pointer + a.length; - } - countInsert = 0; - countDelete = 0; - textDelete = ''; - textInsert = ''; - break; - } - pointer++; + }); + function stripHtml(string) { + // Strip tags, html entity and whitespaces + return string.replace(/<\/?[^>]+(>|$)/g, '').replace(/"/g, '').replace(/\s+/g, ''); + } + QUnit.log(function (details) { + var testItem = id('qunit-test-output-' + details.testId); + if (!testItem) { + return; } - diffs.pop(); // Remove the dummy entry at the end. - - return diffs; - }; + var message = escapeText(details.message) || (details.result ? 'okay' : 'failed'); + message = "" + message + ''; + message += "@ " + details.runtime + ' ms'; + var expected; + var actual; + var diff; + var showDiff = false; - /** - * Find the 'middle snake' of a diff, split the problem in two - * and return the recursively constructed diff. - * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisect = function (text1, text2, deadline) { - var text1Length, text2Length, maxD, vOffset, vLength, v1, v2, x, delta, front, k1start, k1end, k2start, k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; - - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - maxD = Math.ceil((text1Length + text2Length) / 2); - vOffset = maxD; - vLength = 2 * maxD; - v1 = new Array(vLength); - v2 = new Array(vLength); - - // Setting all elements to -1 is faster in Chrome & Firefox than mixing - // integers and undefined. - for (x = 0; x < vLength; x++) { - v1[x] = -1; - v2[x] = -1; - } - v1[vOffset + 1] = 0; - v2[vOffset + 1] = 0; - delta = text1Length - text2Length; - - // If the total number of characters is odd, then the front path will collide - // with the reverse path. - front = delta % 2 !== 0; - - // Offsets for start and end of k loop. - // Prevents mapping of space beyond the grid. - k1start = 0; - k1end = 0; - k2start = 0; - k2end = 0; - for (d = 0; d < maxD; d++) { - // Bail out if deadline is reached. - if (Date.now() > deadline) { - break; + // When pushFailure() is called, it is implied that no expected value + // or diff should be shown, because both expected and actual as undefined. + // + // This must check details.expected existence. If it exists as undefined, + // that's a regular assertion for which to render actual/expected and a diff. + var showAnyValues = !details.result && (details.expected !== undefined || details.actual !== undefined); + if (showAnyValues) { + if (details.negative) { + expected = 'NOT ' + QUnit.dump.parse(details.expected); + } else { + expected = QUnit.dump.parse(details.expected); } - - // Walk the front path one step. - for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { - k1Offset = vOffset + k1; - if (k1 === -d || k1 !== d && v1[k1Offset - 1] < v1[k1Offset + 1]) { - x1 = v1[k1Offset + 1]; - } else { - x1 = v1[k1Offset - 1] + 1; - } - y1 = x1 - k1; - while (x1 < text1Length && y1 < text2Length && text1.charAt(x1) === text2.charAt(y1)) { - x1++; - y1++; - } - v1[k1Offset] = x1; - if (x1 > text1Length) { - // Ran off the right of the graph. - k1end += 2; - } else if (y1 > text2Length) { - // Ran off the bottom of the graph. - k1start += 2; - } else if (front) { - k2Offset = vOffset + delta - k1; - if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) { - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - v2[k2Offset]; - if (x1 >= x2) { - // Overlap detected. - return this.diffBisectSplit(text1, text2, x1, y1, deadline); - } + actual = QUnit.dump.parse(details.actual); + message += "'; + if (actual !== expected) { + message += "'; + if (typeof details.actual === 'number' && typeof details.expected === 'number') { + if (!isNaN(details.actual) && !isNaN(details.expected)) { + showDiff = true; + diff = details.actual - details.expected; + diff = (diff > 0 ? '+' : '') + diff; } - } - } + } else if (typeof details.actual !== 'boolean' && typeof details.expected !== 'boolean') { + diff = QUnit.diff(expected, actual); - // Walk the reverse path one step. - for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { - k2Offset = vOffset + k2; - if (k2 === -d || k2 !== d && v2[k2Offset - 1] < v2[k2Offset + 1]) { - x2 = v2[k2Offset + 1]; - } else { - x2 = v2[k2Offset - 1] + 1; - } - y2 = x2 - k2; - while (x2 < text1Length && y2 < text2Length && text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)) { - x2++; - y2++; + // don't show diff if there is zero overlap + showDiff = stripHtml(diff).length !== stripHtml(expected).length + stripHtml(actual).length; } - v2[k2Offset] = x2; - if (x2 > text1Length) { - // Ran off the left of the graph. - k2end += 2; - } else if (y2 > text2Length) { - // Ran off the top of the graph. - k2start += 2; - } else if (!front) { - k1Offset = vOffset + delta - k2; - if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) { - x1 = v1[k1Offset]; - y1 = vOffset + x1 - k1Offset; - - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - x2; - if (x1 >= x2) { - // Overlap detected. - return this.diffBisectSplit(text1, text2, x1, y1, deadline); - } - } + if (showDiff) { + message += "'; } + } else if (expected.indexOf('[object Array]') !== -1 || expected.indexOf('[object Object]') !== -1) { + message += "'; + } else { + message += "'; } - } - - // Diff took too long and hit the deadline or - // number of diffs equals number of characters, no commonality at all. - return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; - }; + if (details.source) { + message += "'; + } + message += '
        Expected:
        " + escapeText(expected) + '
        Result:
        " + escapeText(actual) + '
        Diff:
        " + diff + '
        Message: " + 'Diff suppressed as the depth of object is more than current max depth (' + QUnit.dump.maxDepth + ').

        Hint: Use QUnit.dump.maxDepth to ' + " run with a higher max depth or " + 'Rerun without max depth.

        Message: " + 'Diff suppressed as the expected and actual results have an equivalent' + ' serialization
        Source:
        " + escapeText(details.source) + '
        '; - /** - * Given the location of the 'middle snake', split the diff in two parts - * and recurse. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} x Index of split point in text1. - * @param {number} y Index of split point in text2. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisectSplit = function (text1, text2, x, y, deadline) { - var text1a, text1b, text2a, text2b, diffs, diffsb; - text1a = text1.substring(0, x); - text2a = text2.substring(0, y); - text1b = text1.substring(x); - text2b = text2.substring(y); - - // Compute both diffs serially. - diffs = this.DiffMain(text1a, text2a, false, deadline); - diffsb = this.DiffMain(text1b, text2b, false, deadline); - return diffs.concat(diffsb); - }; + // This occurs when pushFailure is set and we have an extracted stack trace + } else if (!details.result && details.source) { + message += '' + "' + '
        Source:
        " + escapeText(details.source) + '
        '; + } + var assertList = testItem.getElementsByTagName('ol')[0]; + var assertLi = document.createElement('li'); + assertLi.className = details.result ? 'pass' : 'fail'; + assertLi.innerHTML = message; + assertList.appendChild(assertLi); + }); + QUnit.testDone(function (details) { + var tests = id('qunit-tests'); + var testItem = id('qunit-test-output-' + details.testId); + if (!tests || !testItem) { + return; + } + removeClass(testItem, 'running'); + var status; + if (details.failed > 0) { + status = 'failed'; + } else if (details.todo) { + status = 'todo'; + } else { + status = details.skipped ? 'skipped' : 'passed'; + } + var assertList = testItem.getElementsByTagName('ol')[0]; + var good = details.passed; + var bad = details.failed; - /** - * Reduce the number of edits by eliminating semantically trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupSemantic = function (diffs) { - var changes = false; - var equalities = []; // Stack of indices where equalities are found. - var equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - var lastequality = null; - - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - var pointer = 0; // Index of current position. - - // Number of characters that changed prior to the equality. - var lengthInsertions1 = 0; - var lengthDeletions1 = 0; - - // Number of characters that changed after the equality. - var lengthInsertions2 = 0; - var lengthDeletions2 = 0; - while (pointer < diffs.length) { - if (diffs[pointer][0] === DIFF_EQUAL) { - // Equality found. - equalities[equalitiesLength++] = pointer; - lengthInsertions1 = lengthInsertions2; - lengthDeletions1 = lengthDeletions2; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = diffs[pointer][1]; - } else { - // An insertion or deletion. - if (diffs[pointer][0] === DIFF_INSERT) { - lengthInsertions2 += diffs[pointer][1].length; + // This test passed if it has no unexpected failed assertions + var testPassed = details.failed > 0 ? details.todo : !details.todo; + if (testPassed) { + // Collapse the passing tests + addClass(assertList, 'qunit-collapsed'); + } else { + stats.failedTests.push(details.testId); + if (config.collapse) { + if (!collapseNext) { + // Skip collapsing the first failing test + collapseNext = true; } else { - lengthDeletions2 += diffs[pointer][1].length; - } - - // Eliminate an equality that is smaller or equal to the edits on both - // sides of it. - if (lastequality && lastequality.length <= Math.max(lengthInsertions1, lengthDeletions1) && lastequality.length <= Math.max(lengthInsertions2, lengthDeletions2)) { - // Duplicate record. - diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]); - - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - - // Throw away the equality we just deleted. - equalitiesLength--; - - // Throw away the previous equality (it needs to be reevaluated). - equalitiesLength--; - pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - - // Reset the counters. - lengthInsertions1 = 0; - lengthDeletions1 = 0; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = null; - changes = true; + // Collapse remaining tests + addClass(assertList, 'qunit-collapsed'); } } - pointer++; } - // Normalize the diff. - if (changes) { - this.diffCleanupMerge(diffs); - } - var deletion, insertion, overlapLength1, overlapLength2; - - // Find any overlaps between deletions and insertions. - // e.g: abcxxxxxxdef - // -> abcxxxdef - // e.g: xxxabcdefxxx - // -> defxxxabc - // Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = 1; - while (pointer < diffs.length) { - if (diffs[pointer - 1][0] === DIFF_DELETE && diffs[pointer][0] === DIFF_INSERT) { - deletion = diffs[pointer - 1][1]; - insertion = diffs[pointer][1]; - overlapLength1 = this.diffCommonOverlap(deletion, insertion); - overlapLength2 = this.diffCommonOverlap(insertion, deletion); - if (overlapLength1 >= overlapLength2) { - if (overlapLength1 >= deletion.length / 2 || overlapLength1 >= insertion.length / 2) { - // Overlap found. Insert an equality and trim the surrounding edits. - diffs.splice(pointer, 0, [DIFF_EQUAL, insertion.substring(0, overlapLength1)]); - diffs[pointer - 1][1] = deletion.substring(0, deletion.length - overlapLength1); - diffs[pointer + 1][1] = insertion.substring(overlapLength1); - pointer++; - } - } else { - if (overlapLength2 >= deletion.length / 2 || overlapLength2 >= insertion.length / 2) { - // Reverse overlap found. - // Insert an equality and swap and trim the surrounding edits. - diffs.splice(pointer, 0, [DIFF_EQUAL, deletion.substring(0, overlapLength2)]); - diffs[pointer - 1][0] = DIFF_INSERT; - diffs[pointer - 1][1] = insertion.substring(0, insertion.length - overlapLength2); - diffs[pointer + 1][0] = DIFF_DELETE; - diffs[pointer + 1][1] = deletion.substring(overlapLength2); - pointer++; - } - } - pointer++; + // The testItem.firstChild is the test name + var testTitle = testItem.firstChild; + var testCounts = bad ? "" + bad + ', ' + "" + good + ', ' : ''; + testTitle.innerHTML += " (" + testCounts + details.assertions.length + ')'; + stats.completed++; + if (details.skipped) { + testItem.className = 'skipped'; + var skipped = document.createElement('em'); + skipped.className = 'qunit-skipped-label'; + skipped.innerHTML = 'skipped'; + testItem.insertBefore(skipped, testTitle); + } else { + addEvent(testTitle, 'click', function () { + toggleClass(assertList, 'qunit-collapsed'); + }); + testItem.className = testPassed ? 'pass' : 'fail'; + if (details.todo) { + var todoLabel = document.createElement('em'); + todoLabel.className = 'qunit-todo-label'; + todoLabel.innerHTML = 'todo'; + testItem.className += ' todo'; + testItem.insertBefore(todoLabel, testTitle); } - pointer++; + var time = document.createElement('span'); + time.className = 'runtime'; + time.innerHTML = details.runtime + ' ms'; + testItem.insertBefore(time, assertList); } - }; - /** - * Determine if the suffix of one string is the prefix of another. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of the first - * string and the start of the second string. - * @private - */ - DiffMatchPatch.prototype.diffCommonOverlap = function (text1, text2) { - // Cache the text lengths to prevent multiple calls. - var text1Length = text1.length; - var text2Length = text2.length; - - // Eliminate the null case. - if (text1Length === 0 || text2Length === 0) { - return 0; + // Show the source of the test when showing assertions + if (details.source) { + var sourceName = document.createElement('p'); + sourceName.innerHTML = 'Source: ' + escapeText(details.source); + addClass(sourceName, 'qunit-source'); + if (testPassed) { + addClass(sourceName, 'qunit-collapsed'); + } + addEvent(testTitle, 'click', function () { + toggleClass(sourceName, 'qunit-collapsed'); + }); + testItem.appendChild(sourceName); } - - // Truncate the longer string. - if (text1Length > text2Length) { - text1 = text1.substring(text1Length - text2Length); - } else if (text1Length < text2Length) { - text2 = text2.substring(0, text1Length); + if (config.hidepassed && (status === 'passed' || details.skipped)) { + // use removeChild instead of remove because of support + hiddenTests.push(testItem); + tests.removeChild(testItem); } - var textLength = Math.min(text1Length, text2Length); - - // Quick check for the worst case. - if (text1 === text2) { - return textLength; + }); + QUnit.on('error', function (error) { + var testItem = appendTest('global failure'); + if (!testItem) { + // HTML Reporter is probably disabled or not yet initialized. + return; } - // Start by looking for a single character match - // and increase length until no match is found. - // Performance analysis: https://neil.fraser.name/news/2010/11/04/ - var best = 0; - var length = 1; - while (true) { - var pattern = text1.substring(textLength - length); - var found = text2.indexOf(pattern); - if (found === -1) { - return best; - } - length += found; - if (found === 0 || text1.substring(textLength - length) === text2.substring(0, length)) { - best = length; - length++; - } + // Render similar to a failed assertion (see above QUnit.log callback) + var message = escapeText(errorString(error)); + message = "" + message + ''; + if (error && error.stack) { + message += '' + "' + '
        Source:
        " + escapeText(error.stack) + '
        '; } - }; + var assertList = testItem.getElementsByTagName('ol')[0]; + var assertLi = document.createElement('li'); + assertLi.className = 'fail'; + assertLi.innerHTML = message; + assertList.appendChild(assertLi); - /** - * Split two texts into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {{chars1: string, chars2: string, lineArray: !Array.}} - * An object containing the encoded text1, the encoded text2 and - * the array of unique strings. - * The zeroth element of the array of unique strings is intentionally blank. - * @private - */ - DiffMatchPatch.prototype.diffLinesToChars = function (text1, text2) { - var lineArray = []; // E.g. lineArray[4] === 'Hello\n' - var lineHash = {}; // E.g. lineHash['Hello\n'] === 4 + // Make it visible + testItem.className = 'fail'; + }); - // '\x00' is a valid character, but various debuggers don't like it. - // So we'll insert a junk entry to avoid generating a null character. - lineArray[0] = ''; + // Avoid readyState issue with phantomjs + // Ref: #818 + var usingPhantom = function (p) { + return p && p.version && p.version.major > 0; + }(window$1.phantom); + if (usingPhantom) { + console$1.warn('Support for PhantomJS is deprecated and will be removed in QUnit 3.0.'); + } + if (!usingPhantom && document.readyState === 'complete') { + QUnit.autostart(); + } else { + addEvent(window$1, 'load', QUnit.autostart); + } - /** - * Split a text into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * Modifies linearray and linehash through being a closure. - * @param {string} text String to encode. - * @return {string} Encoded string. - * @private - */ - function diffLinesToCharsMunge(text) { - var chars = ''; - - // Walk the text, pulling out a substring for each line. - // text.split('\n') would would temporarily double our memory footprint. - // Modifying text would create many large strings to garbage collect. - var lineStart = 0; - var lineEnd = -1; - - // Keeping our own length variable is faster than looking it up. - var lineArrayLength = lineArray.length; - while (lineEnd < text.length - 1) { - lineEnd = text.indexOf('\n', lineStart); - if (lineEnd === -1) { - lineEnd = text.length - 1; - } - var line = text.substring(lineStart, lineEnd + 1); - lineStart = lineEnd + 1; - if (hasOwn.call(lineHash, line)) { - chars += String.fromCharCode(lineHash[line]); - } else { - chars += String.fromCharCode(lineArrayLength); - lineHash[line] = lineArrayLength; - lineArray[lineArrayLength++] = line; - } - } - return chars; - } - var chars1 = diffLinesToCharsMunge(text1); - var chars2 = diffLinesToCharsMunge(text2); - return { - chars1: chars1, - chars2: chars2, - lineArray: lineArray - }; - }; + // Wrap window.onerror. We will call the original window.onerror to see if + // the existing handler fully handles the error; if not, we will call the + // QUnit.onError function. + var originalWindowOnError = window$1.onerror; - /** - * Rehydrate the text in a diff from a string of line hashes to real lines of - * text. - * @param {!Array.} diffs Array of diff tuples. - * @param {!Array.} lineArray Array of unique strings. - * @private - */ - DiffMatchPatch.prototype.diffCharsToLines = function (diffs, lineArray) { - for (var x = 0; x < diffs.length; x++) { - var chars = diffs[x][1]; - var text = []; - for (var y = 0; y < chars.length; y++) { - text[y] = lineArray[chars.charCodeAt(y)]; + // Cover uncaught exceptions + // Returning true will suppress the default browser handler, + // returning false will let it run. + window$1.onerror = function (message, fileName, lineNumber, columnNumber, errorObj) { + var ret = false; + if (originalWindowOnError) { + for (var _len = arguments.length, args = new Array(_len > 5 ? _len - 5 : 0), _key = 5; _key < _len; _key++) { + args[_key - 5] = arguments[_key]; } - diffs[x][1] = text.join(''); + ret = originalWindowOnError.call.apply(originalWindowOnError, [this, message, fileName, lineNumber, columnNumber, errorObj].concat(args)); } - }; - - /** - * Reorder and merge like edit sections. Merge equalities. - * Any edit section can move as long as it doesn't cross an equality. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupMerge = function (diffs) { - diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end. - var pointer = 0; - var countDelete = 0; - var countInsert = 0; - var textDelete = ''; - var textInsert = ''; - while (pointer < diffs.length) { - switch (diffs[pointer][0]) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[pointer][1]; - pointer++; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[pointer][1]; - pointer++; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (countDelete + countInsert > 1) { - if (countDelete !== 0 && countInsert !== 0) { - // Factor out any common prefixes. - var commonlength = this.diffCommonPrefix(textInsert, textDelete); - if (commonlength !== 0) { - if (pointer - countDelete - countInsert > 0 && diffs[pointer - countDelete - countInsert - 1][0] === DIFF_EQUAL) { - diffs[pointer - countDelete - countInsert - 1][1] += textInsert.substring(0, commonlength); - } else { - diffs.splice(0, 0, [DIFF_EQUAL, textInsert.substring(0, commonlength)]); - pointer++; - } - textInsert = textInsert.substring(commonlength); - textDelete = textDelete.substring(commonlength); - } - - // Factor out any common suffixies. - commonlength = this.diffCommonSuffix(textInsert, textDelete); - if (commonlength !== 0) { - diffs[pointer][1] = textInsert.substring(textInsert.length - commonlength) + diffs[pointer][1]; - textInsert = textInsert.substring(0, textInsert.length - commonlength); - textDelete = textDelete.substring(0, textDelete.length - commonlength); - } - } - // Delete the offending records and add the merged ones. - if (countDelete === 0) { - diffs.splice(pointer - countInsert, countDelete + countInsert, [DIFF_INSERT, textInsert]); - } else if (countInsert === 0) { - diffs.splice(pointer - countDelete, countDelete + countInsert, [DIFF_DELETE, textDelete]); - } else { - diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert, [DIFF_DELETE, textDelete], [DIFF_INSERT, textInsert]); - } - pointer = pointer - countDelete - countInsert + (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1; - } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { - // Merge this equality with the previous one. - diffs[pointer - 1][1] += diffs[pointer][1]; - diffs.splice(pointer, 1); - } else { - pointer++; - } - countInsert = 0; - countDelete = 0; - textDelete = ''; - textInsert = ''; - break; - } - } - if (diffs[diffs.length - 1][1] === '') { - diffs.pop(); // Remove the dummy entry at the end. - } - - // Second pass: look for single edits surrounded on both sides by equalities - // which can be shifted sideways to eliminate an equality. - // e.g: ABAC -> ABAC - var changes = false; - pointer = 1; - - // Intentionally ignore the first and last element (don't need checking). - while (pointer < diffs.length - 1) { - if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) { - var diffPointer = diffs[pointer][1]; - var position = diffPointer.substring(diffPointer.length - diffs[pointer - 1][1].length); - - // This is a single edit surrounded by equalities. - if (position === diffs[pointer - 1][1]) { - // Shift the edit over the previous equality. - diffs[pointer][1] = diffs[pointer - 1][1] + diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length); - diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; - diffs.splice(pointer - 1, 1); - changes = true; - } else if (diffPointer.substring(0, diffs[pointer + 1][1].length) === diffs[pointer + 1][1]) { - // Shift the edit over the next equality. - diffs[pointer - 1][1] += diffs[pointer + 1][1]; - diffs[pointer][1] = diffs[pointer][1].substring(diffs[pointer + 1][1].length) + diffs[pointer + 1][1]; - diffs.splice(pointer + 1, 1); - changes = true; - } + // Treat return value as window.onerror itself does, + // Only do our handling if not suppressed. + if (ret !== true) { + // If there is a current test that sets the internal `ignoreGlobalErrors` field + // (such as during `assert.throws()`), then the error is ignored and native + // error reporting is suppressed as well. This is because in browsers, an error + // can sometimes end up in `window.onerror` instead of in the local try/catch. + // This ignoring of errors does not apply to our general onUncaughtException + // method, nor to our `unhandledRejection` handlers, as those are not meant + // to receive an "expected" error during `assert.throws()`. + if (config.current && config.current.ignoreGlobalErrors) { + return true; } - pointer++; - } - // If shifts were made, the diff needs reordering and another shift sweep. - if (changes) { - this.diffCleanupMerge(diffs); + // According to + // https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror, + // most modern browsers support an errorObj argument; use that to + // get a full stack trace if it's available. + var error = errorObj || new Error(message); + if (!error.stack && fileName && lineNumber) { + error.stack = "".concat(fileName, ":").concat(lineNumber); + } + QUnit.onUncaughtException(error); } + return ret; }; - return function (o, n) { - var diff, output, text; - diff = new DiffMatchPatch(); - output = diff.DiffMain(o, n); - diff.diffCleanupEfficiency(output); - text = diff.diffPrettyHtml(output); - return text; - }; - }(); + window$1.addEventListener('unhandledrejection', function (event) { + QUnit.onUncaughtException(event.reason); + }); + })(); })(); diff --git a/wicket-examples/src/main/webapp/js-test/tests/ajax/form.js b/wicket-examples/src/main/webapp/js-test/tests/ajax/form.js index 6b79e070293..a5ed3af1156 100644 --- a/wicket-examples/src/main/webapp/js-test/tests/ajax/form.js +++ b/wicket-examples/src/main/webapp/js-test/tests/ajax/form.js @@ -34,7 +34,7 @@ $q(document).ready(function() { $emailInput = $('input[name=email]', $form); // enter just the name field - $nameInput.focus(); + $nameInput.trigger('focus'); var name = 'Aj'; $nameInput.val(name); @@ -84,7 +84,7 @@ $q(document).ready(function() { $emailInput = $('input[name=email]', $form); // enter just the name field - $nameInput.focus(); + $nameInput.trigger('focus'); var name = 'abcdef'; $nameInput.val(name); diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/wicket-autocomplete.js b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/wicket-autocomplete.js index eb3afea13dd..e194d294d24 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/wicket-autocomplete.js +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/autocomplete/wicket-autocomplete.js @@ -665,7 +665,7 @@ if (document.activeElement !== input) { ignoreOneFocusGain = true; - input.focus(); + input.trigger('focus'); } return true; }; diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js index 63bf36f0d41..8157833042b 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/modal/trap-focus.js @@ -70,12 +70,12 @@ if (e.shiftKey) { if (e.target === firstFocusable || $element.is(e.target)) { e.preventDefault(); - lastFocusable.focus(); + lastFocusable.trigger('focus'); } } else { if (e.target === lastFocusable || $element.is(e.target)) { e.preventDefault(); - firstFocusable.focus(); + firstFocusable.trigger('focus'); } } } @@ -91,13 +91,13 @@ $(document).off("focusin", focusin); // ... pull in focus - findFocusable($element).first().focus(); + findFocusable($element).first().trigger('focus'); // ... and install new handler focusin = function() { if (!$.contains($element[0], document.activeElement) && $element[0] !== document.activeElement) { // focus is outside of element, so pull in focus - findFocusable($element).first().focus(); + findFocusable($element).first().trigger('focus'); } }; $(document).on("focusin", focusin); @@ -110,7 +110,7 @@ // ... restore old focus if (oldActive) { try { - oldActive.focus(); + oldActive.trigger('focus'); Wicket.Log.debug("trap-focus: restored focus to element ", oldActive); } catch (error) { Wicket.Log.error("trap-focus: error restoring focus. Attempted to set focus to element, but got an exception", oldActive, error); diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/palette/palette.js b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/palette/palette.js index 16ec0148f87..16df1386fd7 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/palette/palette.js +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/palette/palette.js @@ -86,7 +86,7 @@ if(!box.options[i-1].selected) { box.insertBefore(box.options[i],box.options[i-1]); dirty=true; - box.focus(); + box.trigger('focus'); } } } diff --git a/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/data/table/filter/FilterFormTestPage_expected.html b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/data/table/filter/FilterFormTestPage_expected.html index 69db299dae1..8b3309b6a01 100644 --- a/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/data/table/filter/FilterFormTestPage_expected.html +++ b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/repeater/data/table/filter/FilterFormTestPage_expected.html @@ -1,6 +1,6 @@ - +