From 5e9af2c15ca84b64510f21b163026b793e8ead79 Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Wed, 6 Apr 2022 11:14:43 +0100 Subject: [PATCH] Migrate worker tests to Cypress (#7359) GitOrigin-RevId: f373f4215e5f25d14256008cf5f6582eb3124431 --- package-lock.json | 726 ++++-------------- services/web/.eslintrc | 2 +- services/web/.gitignore | 2 + services/web/cypress.json | 7 +- .../fixtures/build/output-2.pdf} | Bin .../fixtures/build/output-corrupt.pdf} | Bin .../fixtures/build/output-human-readable.log | 21 + .../build/output-undefined-references.log | 10 + .../web/cypress/fixtures/build/output.blg | 1 + .../web/cypress/fixtures/build/output.log | 19 + .../fixtures/build/output.pdf} | Bin services/web/cypress/plugins/index.js | 25 +- services/web/cypress/support/ct/index.js | 3 - services/web/cypress/support/ct/index.ts | 5 + .../cypress/support/ct/{i18n.js => window.ts} | 2 - .../web/cypress/support/shared/commands.ts | 3 + .../web/cypress/support/shared/compile.ts | 68 ++ services/web/cypress/support/shared/events.ts | 5 + .../shared/{exceptions.js => exceptions.ts} | 0 .../web/cypress/support/shared/types.d.ts | 8 + .../js/shared/components/beta-badge.js | 2 +- services/web/frontend/js/utils/worker.js | 3 + services/web/package.json | 3 + .../detach-compile-button.spec.tsx | 75 ++ .../pdf-preview/pdf-js-viewer.spec.tsx | 74 ++ .../pdf-preview/pdf-logs-entries.spec.tsx | 143 ++++ .../pdf-preview-detached-root.spec.tsx | 75 ++ .../pdf-preview-hybrid-toolbar.spec.tsx | 102 +++ .../pdf-preview/pdf-preview.spec.tsx | 599 +++++++++++++++ .../pdf-preview/pdf-synctex-controls.spec.tsx | 380 +++++++++ .../frontend/components/pdf-preview/scope.tsx | 13 + ...beta-badge.spec.js => beta-badge.spec.tsx} | 0 .../file-tree-item-inner.test.js | 5 - .../components/file-tree-root.test.js | 2 - .../file-tree/flows/create-folder.test.js | 2 - .../file-tree/flows/rename-entity.test.js | 2 - .../components/detach-compile-button.test.js | 51 -- .../components/pdf-js-viewer.test.js | 37 - .../components/pdf-logs-entries.test.js | 120 --- .../pdf-preview-detached-root.test.js | 70 -- .../pdf-preview-hybrid-toolbar.test.js | 74 -- .../components/pdf-preview.test.js | 523 ------------- .../components/pdf-synctex-controls.test.js | 437 ----------- .../pdf-preview/utils/mock-compile.js | 148 ---- .../test/frontend/helpers/editor-providers.js | 2 +- services/web/tsconfig.json | 4 +- services/web/types/window.ts | 5 + services/web/webpack.config.js | 6 +- 48 files changed, 1812 insertions(+), 2052 deletions(-) rename services/web/{test/frontend/features/pdf-preview/fixtures/test-example-2.pdf => cypress/fixtures/build/output-2.pdf} (100%) rename services/web/{test/frontend/features/pdf-preview/fixtures/test-example-corrupt.pdf => cypress/fixtures/build/output-corrupt.pdf} (100%) create mode 100644 services/web/cypress/fixtures/build/output-human-readable.log create mode 100644 services/web/cypress/fixtures/build/output-undefined-references.log create mode 100644 services/web/cypress/fixtures/build/output.blg create mode 100644 services/web/cypress/fixtures/build/output.log rename services/web/{test/frontend/features/pdf-preview/fixtures/test-example.pdf => cypress/fixtures/build/output.pdf} (100%) delete mode 100644 services/web/cypress/support/ct/index.js create mode 100644 services/web/cypress/support/ct/index.ts rename services/web/cypress/support/ct/{i18n.js => window.ts} (70%) create mode 100644 services/web/cypress/support/shared/commands.ts create mode 100644 services/web/cypress/support/shared/compile.ts create mode 100644 services/web/cypress/support/shared/events.ts rename services/web/cypress/support/shared/{exceptions.js => exceptions.ts} (100%) create mode 100644 services/web/cypress/support/shared/types.d.ts create mode 100644 services/web/test/frontend/components/pdf-preview/detach-compile-button.spec.tsx create mode 100644 services/web/test/frontend/components/pdf-preview/pdf-js-viewer.spec.tsx create mode 100644 services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx create mode 100644 services/web/test/frontend/components/pdf-preview/pdf-preview-detached-root.spec.tsx create mode 100644 services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx create mode 100644 services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx create mode 100644 services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx create mode 100644 services/web/test/frontend/components/pdf-preview/scope.tsx rename services/web/test/frontend/components/shared/{beta-badge.spec.js => beta-badge.spec.tsx} (100%) delete mode 100644 services/web/test/frontend/features/pdf-preview/components/detach-compile-button.test.js delete mode 100644 services/web/test/frontend/features/pdf-preview/components/pdf-js-viewer.test.js delete mode 100644 services/web/test/frontend/features/pdf-preview/components/pdf-logs-entries.test.js delete mode 100644 services/web/test/frontend/features/pdf-preview/components/pdf-preview-detached-root.test.js delete mode 100644 services/web/test/frontend/features/pdf-preview/components/pdf-preview-hybrid-toolbar.test.js delete mode 100644 services/web/test/frontend/features/pdf-preview/components/pdf-preview.test.js delete mode 100644 services/web/test/frontend/features/pdf-preview/components/pdf-synctex-controls.test.js delete mode 100644 services/web/test/frontend/features/pdf-preview/utils/mock-compile.js diff --git a/package-lock.json b/package-lock.json index 592ca32f8c..0c91b38963 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5682,6 +5682,12 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" }, + "node_modules/@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, "node_modules/@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -5741,11 +5747,10 @@ } }, "node_modules/@types/html-minifier-terser": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", - "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", - "dev": true, - "peer": true + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true }, "node_modules/@types/http-proxy": { "version": "1.17.8", @@ -6047,13 +6052,6 @@ "@types/node": "*" } }, - "node_modules/@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", - "dev": true, - "peer": true - }, "node_modules/@types/superagent": { "version": "3.8.7", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", @@ -6064,90 +6062,16 @@ "@types/node": "*" } }, - "node_modules/@types/tapable": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", - "dev": true, - "peer": true - }, "node_modules/@types/tough-cookie": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" }, - "node_modules/@types/uglify-js": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", - "integrity": "sha512-O3MmRAk6ZuAKa9CHgg0Pr0+lUOqoMLpc9AS4R8ano2auvsg7IE8syF3Xh/NPr26TWklxYcqoEEFdzLLs1fV9PQ==", - "dev": true, - "peer": true, - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/@types/uglify-js/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@types/underscore": { "version": "1.11.4", "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.4.tgz", "integrity": "sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg==" }, - "node_modules/@types/webpack": { - "version": "4.41.32", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", - "integrity": "sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "anymatch": "^3.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/@types/webpack-sources": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", - "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.7.3" - } - }, - "node_modules/@types/webpack-sources/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@types/webpack/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@types/ws": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz", @@ -9049,7 +8973,6 @@ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", "dev": true, - "peer": true, "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -9468,16 +9391,15 @@ "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "node_modules/clean-css": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", - "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", + "integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==", "dev": true, - "peer": true, "dependencies": { "source-map": "~0.6.0" }, "engines": { - "node": ">= 4.0" + "node": ">= 10.0" } }, "node_modules/clean-css/node_modules/source-map": { @@ -9485,7 +9407,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11825,7 +11746,6 @@ "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "dev": true, - "peer": true, "dependencies": { "utila": "~0.4" } @@ -11931,7 +11851,6 @@ "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dev": true, - "peer": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -12331,14 +12250,6 @@ "node": ">=10.13.0" } }, - "node_modules/enhanced-resolve/node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -16384,25 +16295,33 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, "node_modules/html-minifier-terser": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", - "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", "dev": true, - "peer": true, "dependencies": { - "camel-case": "^4.1.1", - "clean-css": "^4.2.3", - "commander": "^4.1.1", + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", "he": "^1.2.0", - "param-case": "^3.0.3", + "param-case": "^3.0.4", "relateurl": "^0.2.7", - "terser": "^4.6.3" + "terser": "^5.10.0" }, "bin": { "html-minifier-terser": "cli.js" }, "engines": { - "node": ">=6" + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" } }, "node_modules/html-parse-stringify": { @@ -16422,83 +16341,26 @@ } }, "node_modules/html-webpack-plugin": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz", - "integrity": "sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", "dev": true, - "peer": true, "dependencies": { - "@types/html-minifier-terser": "^5.0.0", - "@types/tapable": "^1.0.5", - "@types/webpack": "^4.41.8", - "html-minifier-terser": "^5.0.1", - "loader-utils": "^1.2.3", - "lodash": "^4.17.20", - "pretty-error": "^2.1.1", - "tapable": "^1.1.3", - "util.promisify": "1.0.0" + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" }, "engines": { - "node": ">=6.9" + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/html-webpack-plugin/node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/html-webpack-plugin/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "peer": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/html-webpack-plugin/node_modules/loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "peer": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/html-webpack-plugin/node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true, - "peer": true - }, - "node_modules/html-webpack-plugin/node_modules/util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "peer": true, - "dependencies": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" + "webpack": "^5.20.0" } }, "node_modules/htmlparser2": { @@ -20375,7 +20237,6 @@ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "dev": true, - "peer": true, "dependencies": { "tslib": "^2.0.3" } @@ -21942,7 +21803,6 @@ "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dev": true, - "peer": true, "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -23335,7 +23195,6 @@ "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", "dev": true, - "peer": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -23449,7 +23308,6 @@ "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dev": true, - "peer": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -24233,14 +24091,13 @@ } }, "node_modules/pretty-error": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", - "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", "dev": true, - "peer": true, "dependencies": { "lodash": "^4.17.20", - "renderkid": "^2.0.4" + "renderkid": "^3.0.0" } }, "node_modules/pretty-format": { @@ -25698,7 +25555,6 @@ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", "dev": true, - "peer": true, "engines": { "node": ">= 0.10" } @@ -25819,40 +25675,16 @@ "dev": true }, "node_modules/renderkid": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", - "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", "dev": true, - "peer": true, "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", "htmlparser2": "^6.1.0", "lodash": "^4.17.21", - "strip-ansi": "^3.0.1" - } - }, - "node_modules/renderkid/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/renderkid/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "strip-ansi": "^6.0.1" } }, "node_modules/repeat-element": { @@ -28924,11 +28756,9 @@ "dev": true }, "node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true, - "peer": true, + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "engines": { "node": ">=6" } @@ -29121,21 +28951,20 @@ } }, "node_modules/terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "dev": true, - "peer": true, + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", + "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", "dependencies": { + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" }, "engines": { - "node": ">=6.0.0" + "node": ">=10" } }, "node_modules/terser-webpack-plugin": { @@ -29171,7 +29000,15 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/acorn": { + "node_modules/terser-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/acorn": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", @@ -29182,37 +29019,12 @@ "node": ">=0.4.0" } }, - "node_modules/terser-webpack-plugin/node_modules/commander": { + "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "node_modules/terser-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/terser": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", - "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", - "dependencies": { - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin/node_modules/terser/node_modules/source-map": { + "node_modules/terser/node_modules/source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", @@ -29220,23 +29032,6 @@ "node": ">= 8" } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true - }, - "node_modules/terser/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -30511,8 +30306,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", - "dev": true, - "peer": true + "dev": true }, "node_modules/utils-flatten": { "version": "1.0.0", @@ -31274,14 +31068,6 @@ "node": ">=0.8.x" } }, - "node_modules/webpack/node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -35102,6 +34888,7 @@ "daterangepicker": "https://github.com/overleaf/daterangepicker/archive/e496d2d44ca53e208c930e4cb4bcf29bcefa4550.tar.gz", "downshift": "^6.1.0", "east": "^2.0.2", + "events": "^3.3.0", "express": "4.17.1", "express-bearer-token": "^2.4.0", "express-http-proxy": "^1.6.0", @@ -35204,6 +34991,7 @@ "@testing-library/react": "^11.2.7", "@testing-library/react-hooks": "^7.0.0", "@types/chai": "^4.3.0", + "@types/events": "^3.0.0", "@types/mocha": "^9.1.0", "@types/react": "^17.0.40", "@types/react-bootstrap": "^0.32.29", @@ -35250,6 +35038,7 @@ "fetch-mock": "^9.10.2", "glob": "^7.1.6", "handlebars-loader": "^1.7.1", + "html-webpack-plugin": "^5.5.0", "i18next-scanner": "^3.0.0", "jsdom": "^19.0.0", "jsdom-global": "^3.0.2", @@ -36189,6 +35978,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "services/web/node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "services/web/node_modules/expose-loader": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", @@ -37526,15 +37323,6 @@ "node": ">=10.13.0" } }, - "services/web/node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "services/web/node_modules/toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -43173,6 +42961,7 @@ "@testing-library/react": "^11.2.7", "@testing-library/react-hooks": "^7.0.0", "@types/chai": "^4.3.0", + "@types/events": "^3.0.0", "@types/mocha": "^9.1.0", "@types/react": "^17.0.40", "@types/react-bootstrap": "^0.32.29", @@ -43252,6 +43041,7 @@ "eslint-plugin-react": "^7.27.0", "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-standard": "^5.0.0", + "events": "^3.3.0", "expose-loader": "^3.1.0", "express": "4.17.1", "express-bearer-token": "^2.4.0", @@ -43266,6 +43056,7 @@ "handlebars": "^4.7.7", "handlebars-loader": "^1.7.1", "helmet": "^3.22.0", + "html-webpack-plugin": "^5.5.0", "http-proxy": "^1.18.1", "i18next": "^19.6.3", "i18next-fs-backend": "^1.0.7", @@ -44026,6 +43817,11 @@ } } }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, "expose-loader": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", @@ -44962,12 +44758,6 @@ "stable": "^0.1.8" } }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true - }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -45789,6 +45579,12 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, "@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -45847,11 +45643,10 @@ } }, "@types/html-minifier-terser": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", - "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==", - "dev": true, - "peer": true + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true }, "@types/http-proxy": { "version": "1.17.8", @@ -46154,13 +45949,6 @@ "@types/node": "*" } }, - "@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", - "dev": true, - "peer": true - }, "@types/superagent": { "version": "3.8.7", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", @@ -46171,87 +45959,16 @@ "@types/node": "*" } }, - "@types/tapable": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", - "dev": true, - "peer": true - }, "@types/tough-cookie": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" }, - "@types/uglify-js": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", - "integrity": "sha512-O3MmRAk6ZuAKa9CHgg0Pr0+lUOqoMLpc9AS4R8ano2auvsg7IE8syF3Xh/NPr26TWklxYcqoEEFdzLLs1fV9PQ==", - "dev": true, - "peer": true, - "requires": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true - } - } - }, "@types/underscore": { "version": "1.11.4", "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.4.tgz", "integrity": "sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg==" }, - "@types/webpack": { - "version": "4.41.32", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", - "integrity": "sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "anymatch": "^3.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true - } - } - }, - "@types/webpack-sources": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", - "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", - "dev": true, - "peer": true, - "requires": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "peer": true - } - } - }, "@types/ws": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz", @@ -48492,7 +48209,6 @@ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", "dev": true, - "peer": true, "requires": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -48822,11 +48538,10 @@ "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "clean-css": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", - "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", + "integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==", "dev": true, - "peer": true, "requires": { "source-map": "~0.6.0" }, @@ -48835,8 +48550,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true + "dev": true } } }, @@ -50687,7 +50401,6 @@ "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "dev": true, - "peer": true, "requires": { "utila": "~0.4" } @@ -50769,7 +50482,6 @@ "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dev": true, - "peer": true, "requires": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -51105,13 +50817,6 @@ "requires": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" - }, - "dependencies": { - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" - } } }, "enquirer": { @@ -54335,19 +54040,26 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, "html-minifier-terser": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", - "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", "dev": true, - "peer": true, "requires": { - "camel-case": "^4.1.1", - "clean-css": "^4.2.3", - "commander": "^4.1.1", + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", "he": "^1.2.0", - "param-case": "^3.0.3", + "param-case": "^3.0.4", "relateurl": "^0.2.7", - "terser": "^4.6.3" + "terser": "^5.10.0" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + } } }, "html-parse-stringify": { @@ -54366,70 +54078,16 @@ } }, "html-webpack-plugin": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz", - "integrity": "sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", "dev": true, - "peer": true, "requires": { - "@types/html-minifier-terser": "^5.0.0", - "@types/tapable": "^1.0.5", - "@types/webpack": "^4.41.8", - "html-minifier-terser": "^5.0.1", - "loader-utils": "^1.2.3", - "lodash": "^4.17.20", - "pretty-error": "^2.1.1", - "tapable": "^1.1.3", - "util.promisify": "1.0.0" - }, - "dependencies": { - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "peer": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "peer": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, - "peer": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true, - "peer": true - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "peer": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - } + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" } }, "htmlparser2": { @@ -57460,7 +57118,6 @@ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "dev": true, - "peer": true, "requires": { "tslib": "^2.0.3" } @@ -58728,7 +58385,6 @@ "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dev": true, - "peer": true, "requires": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -60370,7 +60026,6 @@ "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", "dev": true, - "peer": true, "requires": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -60460,7 +60115,6 @@ "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dev": true, - "peer": true, "requires": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -61031,14 +60685,13 @@ "dev": true }, "pretty-error": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", - "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", "dev": true, - "peer": true, "requires": { "lodash": "^4.17.20", - "renderkid": "^2.0.4" + "renderkid": "^3.0.0" } }, "pretty-format": { @@ -62199,8 +61852,7 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", - "dev": true, - "peer": true + "dev": true }, "release-zalgo": { "version": "1.0.0", @@ -62307,36 +61959,16 @@ "dev": true }, "renderkid": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", - "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", "dev": true, - "peer": true, "requires": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", "htmlparser2": "^6.1.0", "lodash": "^4.17.21", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "peer": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "peer": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } + "strip-ansi": "^6.0.1" } }, "repeat-element": { @@ -64834,11 +64466,9 @@ "dev": true }, "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true, - "peer": true + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" }, "tar": { "version": "6.1.11", @@ -64986,30 +64616,30 @@ "dev": true }, "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "dev": true, - "peer": true, + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", + "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", "requires": { + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" }, "dependencies": { + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==" + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "peer": true + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" } } }, @@ -65025,38 +64655,10 @@ "terser": "^5.7.2" }, "dependencies": { - "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==" - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "terser": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", - "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", - "requires": { - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.20" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" - } - } } } }, @@ -66107,8 +65709,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", - "dev": true, - "peer": true + "dev": true }, "utils-flatten": { "version": "1.0.0", @@ -66497,11 +66098,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" } } }, diff --git a/services/web/.eslintrc b/services/web/.eslintrc index 5749fffc9e..74eb48f119 100644 --- a/services/web/.eslintrc +++ b/services/web/.eslintrc @@ -104,7 +104,7 @@ }, { // Cypress specific rules - "files": ["cypress/**/*.js", "**/test/frontend/**/*.spec.js"], + "files": ["cypress/**/*.{js,ts,tsx}", "**/test/frontend/**/*.spec.{js,ts,tsx}"], "extends": [ "plugin:cypress/recommended" ] diff --git a/services/web/.gitignore b/services/web/.gitignore index 31c533d1ea..061e7d6bc2 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -91,3 +91,5 @@ cypress/downloads/ # Ace themes for conversion modules/source-editor/frontend/js/themes/ace/ + +!**/fixtures/**/*.log diff --git a/services/web/cypress.json b/services/web/cypress.json index 9fc6f1108a..ac49706665 100644 --- a/services/web/cypress.json +++ b/services/web/cypress.json @@ -1,11 +1,10 @@ { "component": { "componentFolder": ".", - "testFiles": "./{test,modules/**/test}/frontend/components/**/*.spec.js", - "supportFile": "cypress/support/ct/index.js" + "testFiles": "./{test,modules/**/test}/frontend/components/**/*.spec.{js,ts,tsx}", + "supportFile": "cypress/support/ct/index.ts" }, - "experimentalFetchPolyfill": true, - "fixturesFolder": false, + "fixturesFolder": "cypress/fixtures", "video": false, "viewportHeight": 800, "viewportWidth": 800 diff --git a/services/web/test/frontend/features/pdf-preview/fixtures/test-example-2.pdf b/services/web/cypress/fixtures/build/output-2.pdf similarity index 100% rename from services/web/test/frontend/features/pdf-preview/fixtures/test-example-2.pdf rename to services/web/cypress/fixtures/build/output-2.pdf diff --git a/services/web/test/frontend/features/pdf-preview/fixtures/test-example-corrupt.pdf b/services/web/cypress/fixtures/build/output-corrupt.pdf similarity index 100% rename from services/web/test/frontend/features/pdf-preview/fixtures/test-example-corrupt.pdf rename to services/web/cypress/fixtures/build/output-corrupt.pdf diff --git a/services/web/cypress/fixtures/build/output-human-readable.log b/services/web/cypress/fixtures/build/output-human-readable.log new file mode 100644 index 0000000000..af776aa2d3 --- /dev/null +++ b/services/web/cypress/fixtures/build/output-human-readable.log @@ -0,0 +1,21 @@ +log This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdflatex 2020.9.10) 8 FEB 2022 16:27 +entering extended mode + \write18 enabled. + %&-line parsing enabled. +**main.tex +(./main.tex +LaTeX2e <2020-02-02> patch level 5 + +LaTeX Warning: Reference `intorduction' on page 1 undefined on input line 11. + + +LaTeX Warning: Reference `section1' on page 1 undefined on input line 13. + +[1 + +{/usr/local/texlive/2020/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (/compi +le/output.aux) + +LaTeX Warning: There were undefined references. + + ) diff --git a/services/web/cypress/fixtures/build/output-undefined-references.log b/services/web/cypress/fixtures/build/output-undefined-references.log new file mode 100644 index 0000000000..c2e452138c --- /dev/null +++ b/services/web/cypress/fixtures/build/output-undefined-references.log @@ -0,0 +1,10 @@ +Package rerunfilecheck Info: File `output.out' has not changed. +(rerunfilecheck) Checksum: 339DB29951BB30436898BC39909EA4FA;11265. + +Package rerunfilecheck Warning: File `output.brf' has changed. +(rerunfilecheck) Rerun to get bibliographical references right. + +Package rerunfilecheck Info: Checksums for `output.brf': +(rerunfilecheck) Before: D41D8CD98F00B204E9800998ECF8427E;0 +(rerunfilecheck) After: DF3260FAD3828D54C5E4E9337E97F7AF;4841. +) diff --git a/services/web/cypress/fixtures/build/output.blg b/services/web/cypress/fixtures/build/output.blg new file mode 100644 index 0000000000..dd54e2034c --- /dev/null +++ b/services/web/cypress/fixtures/build/output.blg @@ -0,0 +1 @@ +This is BibTeX, Version 4.0 diff --git a/services/web/cypress/fixtures/build/output.log b/services/web/cypress/fixtures/build/output.log new file mode 100644 index 0000000000..768a90556f --- /dev/null +++ b/services/web/cypress/fixtures/build/output.log @@ -0,0 +1,19 @@ +The LaTeX compiler output + * With a lot of details + +Wrapped in an HTML
 element with
+      preformatted text which is to be presented exactly
+            as written in the HTML file
+
+                                              (whitespace included™)
+
+The text is typically rendered using a non-proportional ("monospace") font.
+
+LaTeX Font Info:    External font `cmex10' loaded for size
+(Font)              <7> on input line 18.
+LaTeX Font Info:    External font `cmex10' loaded for size
+(Font)              <5> on input line 18.
+! Undefined control sequence.
+ \Zlpha
+
+ main.tex, line 23
diff --git a/services/web/test/frontend/features/pdf-preview/fixtures/test-example.pdf b/services/web/cypress/fixtures/build/output.pdf
similarity index 100%
rename from services/web/test/frontend/features/pdf-preview/fixtures/test-example.pdf
rename to services/web/cypress/fixtures/build/output.pdf
diff --git a/services/web/cypress/plugins/index.js b/services/web/cypress/plugins/index.js
index 06a2eec53c..e31a2a6aaa 100644
--- a/services/web/cypress/plugins/index.js
+++ b/services/web/cypress/plugins/index.js
@@ -3,17 +3,40 @@ module.exports = (on, config) => {
     const { startDevServer } = require('@cypress/webpack-dev-server')
     const { merge } = require('webpack-merge')
     const path = require('path')
+    const webpack = require('webpack')
     const devConfig = require('../../webpack.config.dev')
 
     const webpackConfig = merge(devConfig, {
       devServer: {
-        static: path.join(__dirname, '../../../../public'),
+        static: path.join(__dirname, '../../public'),
       },
       stats: 'none',
+      plugins: [
+        new webpack.EnvironmentPlugin({
+          CYPRESS: true,
+        }),
+      ],
     })
 
     delete webpackConfig.devServer.client
 
+    webpackConfig.entry = {}
+    const addWorker = (name, importPath) => {
+      webpackConfig.entry[name] = require.resolve(importPath)
+    }
+
+    // add entrypoint under '/' for latex-linter worker
+    addWorker(
+      'latex-linter-worker',
+      '../../modules/source-editor/frontend/js/languages/latex/linter/latex-linter.worker.js'
+    )
+
+    // add entrypoints under '/' for pdfjs workers
+    const pdfjsVersions = ['pdfjs-dist210', 'pdfjs-dist213']
+    for (const name of pdfjsVersions) {
+      addWorker(name, `${name}/legacy/build/pdf.worker.js`)
+    }
+
     on('dev-server:start', options => {
       return startDevServer({ options, webpackConfig })
     })
diff --git a/services/web/cypress/support/ct/index.js b/services/web/cypress/support/ct/index.js
deleted file mode 100644
index ea50299e96..0000000000
--- a/services/web/cypress/support/ct/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-require('../../../frontend/stylesheets/style.less')
-require('../shared/exceptions')
-require('./i18n')
diff --git a/services/web/cypress/support/ct/index.ts b/services/web/cypress/support/ct/index.ts
new file mode 100644
index 0000000000..c660b651b0
--- /dev/null
+++ b/services/web/cypress/support/ct/index.ts
@@ -0,0 +1,5 @@
+import '../../../frontend/stylesheets/style.less'
+import './window' // needs to be before i18n
+import '../../../frontend/js/i18n'
+import '../shared/commands'
+import '../shared/exceptions'
diff --git a/services/web/cypress/support/ct/i18n.js b/services/web/cypress/support/ct/window.ts
similarity index 70%
rename from services/web/cypress/support/ct/i18n.js
rename to services/web/cypress/support/ct/window.ts
index f61d91e56a..82b972bf27 100644
--- a/services/web/cypress/support/ct/i18n.js
+++ b/services/web/cypress/support/ct/window.ts
@@ -1,4 +1,2 @@
 window.i18n = { currentLangCode: 'en' }
 window.ExposedSettings = { appName: 'Overleaf' }
-
-require('../../../frontend/js/i18n')
diff --git a/services/web/cypress/support/shared/commands.ts b/services/web/cypress/support/shared/commands.ts
new file mode 100644
index 0000000000..546e07f648
--- /dev/null
+++ b/services/web/cypress/support/shared/commands.ts
@@ -0,0 +1,3 @@
+import '@testing-library/cypress/add-commands'
+import './compile'
+import './events'
diff --git a/services/web/cypress/support/shared/compile.ts b/services/web/cypress/support/shared/compile.ts
new file mode 100644
index 0000000000..c4be3ca455
--- /dev/null
+++ b/services/web/cypress/support/shared/compile.ts
@@ -0,0 +1,68 @@
+import { v4 as uuid } from 'uuid'
+
+const outputFiles = () => {
+  const build = uuid()
+
+  return [
+    {
+      path: 'output.pdf',
+      build,
+      url: `/build/${build}/output.pdf`,
+      type: 'pdf',
+    },
+    {
+      path: 'output.bbl',
+      build,
+      url: `/build/${build}/output.bbl`,
+      type: 'bbl',
+    },
+    {
+      path: 'output.bib',
+      build,
+      url: `/build/${build}/output.bib`,
+      type: 'bib',
+    },
+    {
+      path: 'example.txt',
+      build,
+      url: `/build/${build}/example.txt`,
+      type: 'txt',
+    },
+    {
+      path: 'output.log',
+      build,
+      url: `/build/${build}/output.log`,
+      type: 'log',
+    },
+    {
+      path: 'output.blg',
+      build,
+      url: `/build/${build}/output.blg`,
+      type: 'blg',
+    },
+  ]
+}
+
+Cypress.Commands.add('interceptCompile', (prefix = 'compile') => {
+  cy.intercept('POST', '/project/*/compile*', {
+    body: {
+      status: 'success',
+      clsiServerId: 'foo',
+      compileGroup: 'priority',
+      pdfDownloadDomain: 'https://clsi.test-overleaf.com',
+      outputFiles: outputFiles(),
+    },
+  }).as(`${prefix}`)
+
+  cy.intercept('/build/*/output.pdf*', {
+    fixture: 'build/output.pdf,null',
+  }).as(`${prefix}-pdf`)
+
+  cy.intercept('/build/*/output.log*', {
+    fixture: 'build/output.log',
+  }).as(`${prefix}-log`)
+
+  cy.intercept('/build/*/output.blg*', {
+    fixture: 'build/output.blg',
+  }).as(`${prefix}-blg`)
+})
diff --git a/services/web/cypress/support/shared/events.ts b/services/web/cypress/support/shared/events.ts
new file mode 100644
index 0000000000..46987e1612
--- /dev/null
+++ b/services/web/cypress/support/shared/events.ts
@@ -0,0 +1,5 @@
+Cypress.Commands.add('interceptEvents', () => {
+  cy.intercept('POST', '/event/*', {
+    statusCode: 204,
+  })
+})
diff --git a/services/web/cypress/support/shared/exceptions.js b/services/web/cypress/support/shared/exceptions.ts
similarity index 100%
rename from services/web/cypress/support/shared/exceptions.js
rename to services/web/cypress/support/shared/exceptions.ts
diff --git a/services/web/cypress/support/shared/types.d.ts b/services/web/cypress/support/shared/types.d.ts
new file mode 100644
index 0000000000..89a7b6a9aa
--- /dev/null
+++ b/services/web/cypress/support/shared/types.d.ts
@@ -0,0 +1,8 @@
+// eslint-disable-next-line no-unused-vars
+declare namespace Cypress {
+  // eslint-disable-next-line no-unused-vars
+  interface Chainable {
+    interceptCompile(prefix?: string): void
+    interceptEvents(): void
+  }
+}
diff --git a/services/web/frontend/js/shared/components/beta-badge.js b/services/web/frontend/js/shared/components/beta-badge.js
index 2c4d8fa580..b05cb6f5bf 100644
--- a/services/web/frontend/js/shared/components/beta-badge.js
+++ b/services/web/frontend/js/shared/components/beta-badge.js
@@ -25,7 +25,7 @@ export default function BetaBadge({ tooltip, url = '/beta/participate' }) {
 }
 
 BetaBadge.propTypes = {
-  tooltip: PropTypes.exact({
+  tooltip: PropTypes.shape({
     id: PropTypes.string.isRequired,
     text: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
     placement: PropTypes.string,
diff --git a/services/web/frontend/js/utils/worker.js b/services/web/frontend/js/utils/worker.js
index 96b9fc73bd..28f161c37e 100644
--- a/services/web/frontend/js/utils/worker.js
+++ b/services/web/frontend/js/utils/worker.js
@@ -1,4 +1,7 @@
 export const createWorker = callback => {
+  if (process.env.CYPRESS) {
+    return callback()
+  }
   const webpackPublicPath = __webpack_public_path__
   __webpack_public_path__ = '/'
   callback()
diff --git a/services/web/package.json b/services/web/package.json
index 9b59e8eade..ada229d172 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -116,6 +116,7 @@
     "daterangepicker": "https://github.com/overleaf/daterangepicker/archive/e496d2d44ca53e208c930e4cb4bcf29bcefa4550.tar.gz",
     "downshift": "^6.1.0",
     "east": "^2.0.2",
+    "events": "^3.3.0",
     "express": "4.17.1",
     "express-bearer-token": "^2.4.0",
     "express-http-proxy": "^1.6.0",
@@ -218,6 +219,7 @@
     "@testing-library/react": "^11.2.7",
     "@testing-library/react-hooks": "^7.0.0",
     "@types/chai": "^4.3.0",
+    "@types/events": "^3.0.0",
     "@types/mocha": "^9.1.0",
     "@types/react": "^17.0.40",
     "@types/react-bootstrap": "^0.32.29",
@@ -264,6 +266,7 @@
     "fetch-mock": "^9.10.2",
     "glob": "^7.1.6",
     "handlebars-loader": "^1.7.1",
+    "html-webpack-plugin": "^5.5.0",
     "i18next-scanner": "^3.0.0",
     "jsdom": "^19.0.0",
     "jsdom-global": "^3.0.2",
diff --git a/services/web/test/frontend/components/pdf-preview/detach-compile-button.spec.tsx b/services/web/test/frontend/components/pdf-preview/detach-compile-button.spec.tsx
new file mode 100644
index 0000000000..8fd0fb26e0
--- /dev/null
+++ b/services/web/test/frontend/components/pdf-preview/detach-compile-button.spec.tsx
@@ -0,0 +1,75 @@
+import { mount } from '@cypress/react'
+import sysendTestHelper from '../../helpers/sysend'
+import { EditorProviders } from '../../helpers/editor-providers'
+import DetachCompileButton from '../../../../frontend/js/features/pdf-preview/components/detach-compile-button'
+import { mockScope } from './scope'
+
+describe('', function () {
+  beforeEach(function () {
+    cy.interceptCompile()
+    cy.interceptEvents()
+  })
+
+  afterEach(function () {
+    window.metaAttributesCache = new Map()
+    sysendTestHelper.resetHistory()
+  })
+
+  it('detacher mode and not linked: does not show button ', function () {
+    cy.window().then(win => {
+      win.metaAttributesCache = new Map([['ol-detachRole', 'detacher']])
+    })
+
+    const scope = mockScope()
+
+    mount(
+      
+        
+      
+    )
+
+    cy.findByRole('button', { name: 'Recompile' }).should('not.exist')
+  })
+
+  it('detacher mode and linked: show button ', function () {
+    cy.window().then(win => {
+      win.metaAttributesCache = new Map([['ol-detachRole', 'detacher']])
+    })
+
+    const scope = mockScope()
+
+    mount(
+      
+        
+      
+    ).then(() => {
+      sysendTestHelper.receiveMessage({
+        role: 'detached',
+        event: 'connected',
+      })
+    })
+
+    cy.findByRole('button', { name: 'Recompile' })
+  })
+
+  it('not detacher mode and linked: does not show button ', function () {
+    cy.window().then(win => {
+      win.metaAttributesCache = new Map([['ol-detachRole', 'detached']])
+    })
+
+    const scope = mockScope()
+
+    mount(
+      
+        
+      
+    ).then(() => {
+      sysendTestHelper.receiveMessage({
+        role: 'detacher',
+        event: 'connected',
+      })
+    })
+
+    cy.findByRole('button', { name: 'Recompile' }).should('not.exist')
+  })
+})
diff --git a/services/web/test/frontend/components/pdf-preview/pdf-js-viewer.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-js-viewer.spec.tsx
new file mode 100644
index 0000000000..c0f8a60b37
--- /dev/null
+++ b/services/web/test/frontend/components/pdf-preview/pdf-js-viewer.spec.tsx
@@ -0,0 +1,74 @@
+import { mount, unmount } from '@cypress/react'
+import { EditorProviders } from '../../helpers/editor-providers'
+import PdfJsViewer from '../../../../frontend/js/features/pdf-preview/components/pdf-js-viewer'
+import { mockScope } from './scope'
+
+describe('', function () {
+  beforeEach(function () {
+    cy.interceptCompile()
+    cy.interceptEvents()
+  })
+
+  it('loads all PDF pages', function () {
+    const scope = mockScope()
+
+    mount(
+      
+        
+ +
+
+ ) + + cy.findByLabelText('Page 1') + cy.findByLabelText('Page 2') + cy.findByLabelText('Page 3') + cy.findByLabelText('Page 4').should('not.exist') + + cy.contains('Your Paper') + }) + + it('renders pages in a "loading" state', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + cy.findByLabelText('Loading…') + }) + + it('can be unmounted while loading a document', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + unmount() + }) + + it('can be unmounted after loading a document', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + cy.findByLabelText('Page 1') + + unmount() + }) +}) diff --git a/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx new file mode 100644 index 0000000000..c07e2881c1 --- /dev/null +++ b/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx @@ -0,0 +1,143 @@ +import { mount } from '@cypress/react' +import sysendTestHelper from '../../helpers/sysend' +import { EditorProviders } from '../../helpers/editor-providers' +import PdfLogsEntries from '../../../../frontend/js/features/pdf-preview/components/pdf-logs-entries' +window.metaAttributesCache = new Map([['ol-debugPdfDetach', true]]) + +describe('', function () { + const fakeEntity = { type: 'doc' } + + const logEntries = [ + { + file: 'main.tex', + line: 9, + column: 8, + level: 'error', + message: 'LaTeX Error', + content: 'See the LaTeX manual', + raw: '', + ruleId: 'hint_misplaced_alignment_tab_character', + key: '', + }, + ] + + let props + + beforeEach(function () { + props = { + fileTreeManager: { + findEntityByPath: cy.stub().as('findEntityByPath').returns(fakeEntity), + }, + editorManager: { + openDoc: cy.stub().as('openDoc'), + }, + } + + cy.interceptCompile() + cy.interceptEvents() + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + sysendTestHelper.resetHistory() + }) + + it('displays human readable hint', function () { + mount( + + + + ) + + cy.contains('You have placed an alignment tab character') + }) + + it('opens doc on click', function () { + mount( + + + + ) + + cy.findByRole('button', { + name: 'Navigate to log position in source code: main.tex, 9', + }) + .click() + .then(() => { + expect(props.fileTreeManager.findEntityByPath).to.be.calledOnce + expect(props.editorManager.openDoc).to.be.calledOnce + expect(props.editorManager.openDoc).to.be.calledWith(fakeEntity, { + gotoLine: 9, + gotoColumn: 8, + }) + }) + }) + + it('opens doc via detached action', function () { + cy.window().then(win => { + win.metaAttributesCache = new Map([['ol-detachRole', 'detacher']]) + }) + + mount( + + + + ).then(() => { + sysendTestHelper.receiveMessage({ + role: 'detached', + event: 'action-sync-to-entry', + data: { + args: [ + { + file: 'main.tex', + line: 7, + column: 6, + }, + ], + }, + }) + + expect(props.fileTreeManager.findEntityByPath).to.be.calledOnce + expect(props.editorManager.openDoc).to.be.calledOnce + expect(props.editorManager.openDoc).to.be.calledWith(fakeEntity, { + gotoLine: 7, + gotoColumn: 6, + }) + }) + }) + + it('sends open doc clicks via detached action', function () { + cy.window().then(win => { + win.metaAttributesCache = new Map([['ol-detachRole', 'detached']]) + }) + + mount( + + + + ) + + cy.findByRole('button', { + name: 'Navigate to log position in source code: main.tex, 9', + }) + .click() + .then(() => { + expect(props.fileTreeManager.findEntityByPath).not.to.be.called + expect(props.editorManager.openDoc).not.to.be.called + + expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({ + role: 'detached', + event: 'action-sync-to-entry', + data: { + args: [ + { + file: 'main.tex', + line: 9, + column: 8, + }, + ], + }, + }) + }) + }) +}) diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview-detached-root.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview-detached-root.spec.tsx new file mode 100644 index 0000000000..b20d6a1e43 --- /dev/null +++ b/services/web/test/frontend/components/pdf-preview/pdf-preview-detached-root.spec.tsx @@ -0,0 +1,75 @@ +import { mount } from '@cypress/react' +import sysendTestHelper from '../../helpers/sysend' +import PdfPreviewDetachedRoot from '../../../../frontend/js/features/pdf-preview/components/pdf-preview-detached-root' + +describe('', function () { + beforeEach(function () { + window.user = { id: 'user1' } + + window.metaAttributesCache = new Map([ + ['ol-user', window.user], + ['ol-project_id', 'project1'], + ['ol-detachRole', 'detached'], + ['ol-projectName', 'Project Name'], + ]) + + cy.interceptCompile() + cy.interceptEvents() + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + sysendTestHelper.resetHistory() + }) + + it('syncs compiling state', function () { + mount().then(() => { + sysendTestHelper.receiveMessage({ + role: 'detacher', + event: 'connected', + }) + + sysendTestHelper.receiveMessage({ + role: 'detacher', + event: 'state-compiling', + data: { value: true }, + }) + }) + + cy.findByRole('button', { name: 'Compiling…' }) + cy.findByRole('button', { name: 'Recompile' }) + .should('not.exist') + .then(() => { + sysendTestHelper.receiveMessage({ + role: 'detacher', + event: 'state-compiling', + data: { value: false }, + }) + }) + cy.findByRole('button', { name: 'Recompile' }) + cy.findByRole('button', { name: 'Compiling…' }).should('not.exist') + }) + + it('sends a clear cache request when the button is pressed', function () { + mount().then(() => { + sysendTestHelper.receiveMessage({ + role: 'detacher', + event: 'state-showLogs', + data: { value: true }, + }) + }) + + cy.findByRole('button', { name: 'Clear cached files' }) + .should('not.be.disabled') + .click() + .then(() => { + expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({ + role: 'detached', + event: 'action-clearCache', + data: { + args: [], + }, + }) + }) + }) +}) diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx new file mode 100644 index 0000000000..d8f6dfa35c --- /dev/null +++ b/services/web/test/frontend/components/pdf-preview/pdf-preview-hybrid-toolbar.spec.tsx @@ -0,0 +1,102 @@ +import { mount } from '@cypress/react' +import sysendTestHelper from '../../helpers/sysend' +import { EditorProviders } from '../../helpers/editor-providers' +import PdfPreviewHybridToolbar from '../../../../frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar' + +describe('', function () { + beforeEach(function () { + cy.interceptCompile() + cy.interceptEvents() + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + sysendTestHelper.resetHistory() + }) + + it('shows normal mode', function () { + mount( + + + + ) + + cy.findByRole('button', { name: 'Recompile' }) + }) + + describe('orphan mode', function () { + it('shows connecting message on load', function () { + cy.window().then(win => { + win.metaAttributesCache = new Map([['ol-detachRole', 'detached']]) + }) + + mount( + + + + ) + + cy.contains('Connecting with the editor') + }) + + it('shows compile UI when connected', function () { + cy.window().then(win => { + win.metaAttributesCache = new Map([['ol-detachRole', 'detached']]) + }) + + mount( + + + + ).then(() => { + sysendTestHelper.receiveMessage({ + role: 'detacher', + event: 'connected', + }) + }) + + cy.findByRole('button', { name: 'Recompile' }) + }) + + it('shows connecting message when disconnected', function () { + cy.window().then(win => { + win.metaAttributesCache = new Map([['ol-detachRole', 'detached']]) + }) + + mount( + + + + ).then(() => { + sysendTestHelper.receiveMessage({ + role: 'detacher', + event: 'connected', + }) + sysendTestHelper.receiveMessage({ + role: 'detacher', + event: 'closed', + }) + }) + + cy.contains('Connecting with the editor') + }) + + it('shows redirect button after timeout', function () { + cy.window().then(win => { + win.metaAttributesCache = new Map([['ol-detachRole', 'detached']]) + }) + + cy.clock() + + mount( + + + + ) + + cy.tick(6000) + + cy.findByRole('button', { name: 'Redirect to editor' }) + }) + }) +}) diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx new file mode 100644 index 0000000000..8f20d0838d --- /dev/null +++ b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx @@ -0,0 +1,599 @@ +import { mount } from '@cypress/react' +import localStorage from '../../../../frontend/js/infrastructure/local-storage' +import PdfPreview from '../../../../frontend/js/features/pdf-preview/components/pdf-preview' +import { EditorProviders } from '../../helpers/editor-providers' +import { mockScope } from './scope' + +const storeAndFireEvent = (win, key, value) => { + localStorage.setItem(key, value) + win.dispatchEvent(new StorageEvent('storage', { key })) +} + +describe('', function () { + beforeEach(function () { + cy.interceptCompile() + cy.interceptEvents() + }) + + it('renders the PDF preview', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.wait('@compile') + cy.findByRole('button', { name: 'Recompile' }) + cy.wait('@compile-pdf') + }) + + it('runs a compile when the Recompile button is pressed', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.wait('@compile') + cy.wait('@compile-pdf') + + cy.interceptCompile('recompile') + + // press the Recompile button => compile + cy.findByRole('button', { name: 'Recompile' }).click() + + // wait for "recompile" to finish + // cy.findByRole('button', { name: 'Compiling…' }) + cy.wait('@recompile-pdf') + cy.wait('@recompile-log') + cy.wait('@recompile-blg') + + cy.findByRole('button', { name: 'Recompile' }) + + cy.contains('Your Paper') + }) + + it('runs a compile on `pdf:recompile` event', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.wait('@compile') + cy.wait('@compile-pdf') + + cy.interceptCompile('recompile') + + cy.window().then(win => { + win.dispatchEvent(new CustomEvent('pdf:recompile')) + }) + + // wait for "recompile" to finish + // cy.findByRole('button', { name: 'Compiling…' }) + cy.wait('@recompile') + + cy.findByRole('button', { name: 'Recompile' }) + + cy.wait('@recompile-pdf') + cy.contains('Your Paper') + }) + + it('does not compile while compiling', function () { + let compileResolve + let counter = 0 + + const promise = new Promise(resolve => { + compileResolve = resolve + }) + + cy.intercept( + 'POST', + '/project/project123/compile?auto_compile=true', + req => { + counter++ + + promise.then(() => { + req.reply({ + body: { + status: 'success', + clsiServerId: 'foo', + compileGroup: 'priority', + pdfDownloadDomain: 'https://clsi.test-overleaf.com', + outputFiles: [ + { + path: 'output.pdf', + build: '123', + url: '/build/123/output.pdf', + type: 'pdf', + }, + { + path: 'output.log', + build: '123', + url: '/build/123/output.log', + type: 'log', + }, + ], + }, + }) + }) + + return promise + } + ).as('compile') + + const scope = mockScope() + + mount( + +
+ +
+
+ ).then(() => { + cy.findByRole('button', { name: 'Compiling…' }) + + cy.window().then(win => { + win.dispatchEvent(new CustomEvent('pdf:recompile')) + }) + + compileResolve() + + cy.findByRole('button', { name: 'Recompile' }) + + cy.contains('Your Paper').should(() => { + expect(counter).to.equal(1) + }) + }) + }) + + it('disables compile button while compile is running', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + cy.findByRole('button', { name: 'Compiling…' }).should('be.disabled') + cy.findByRole('button', { name: 'Recompile' }).should('not.be.disabled') + }) + + it('runs a compile on doc change if autocompile is enabled', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.wait('@compile') + cy.findByRole('button', { name: 'Recompile' }) + + cy.window().then(win => { + cy.clock() + + // switch on auto compile + storeAndFireEvent(win, 'autocompile_enabled:project123', true) + + // fire a doc:changed event => compile + win.dispatchEvent(new CustomEvent('doc:changed')) + + cy.tick(5000) // AUTO_COMPILE_DEBOUNCE + + cy.clock().invoke('restore') + }) + + cy.findByRole('button', { name: 'Compiling…' }) + cy.findByRole('button', { name: 'Recompile' }) + }) + + it('does not run a compile on doc change if autocompile is disabled', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.findByRole('button', { name: 'Recompile' }) + + cy.window().then(win => { + cy.clock() + + // make sure auto compile is switched off + storeAndFireEvent(win, 'autocompile_enabled:project123', false) + + // fire a doc:changed event => no compile + win.dispatchEvent(new CustomEvent('doc:changed')) + + cy.tick(5000) // AUTO_COMPILE_DEBOUNCE + + cy.clock().invoke('restore') + }) + + cy.findByRole('button', { name: 'Recompile' }) + }) + + it('does not run a compile on doc change if autocompile is blocked by syntax check', function () { + const scope = mockScope() + // enable linting in the editor + scope.settings.syntaxValidation = true + // mock a linting error + scope.hasLintingError = true + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.findByRole('button', { name: 'Recompile' }) + + cy.window().then(win => { + cy.clock() + + // switch on auto compile + storeAndFireEvent(win, 'autocompile_enabled:project123', true) + + // switch on syntax checking + storeAndFireEvent(win, 'stop_on_validation_error', true) + + // fire a doc:changed event => no compile + win.dispatchEvent(new CustomEvent('doc:changed')) + + cy.tick(5000) // AUTO_COMPILE_DEBOUNCE + + cy.clock().invoke('restore') + }) + + cy.findByRole('button', { name: 'Recompile' }) + cy.findByText('Code check failed') + }) + + describe('displays error messages', function () { + const compileErrorStatuses = { + 'clear-cache': + 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.', + 'clsi-maintenance': + 'The compile servers are down for maintenance, and will be back shortly.', + 'compile-in-progress': + 'A previous compile is still running. Please wait a minute and try compiling again.', + exited: 'Server Error', + failure: 'No PDF', + generic: 'Server Error', + 'project-too-large': 'Project too large', + 'rate-limited': 'Compile rate limit hit', + terminated: 'Compilation cancelled', + timedout: 'Timed out', + 'too-recently-compiled': + 'This project was compiled very recently, so this compile has been skipped.', + unavailable: + 'Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.', + foo: 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.', + } + + for (const [status, message] of Object.entries(compileErrorStatuses)) { + it(`displays error message for '${status}' status`, function () { + cy.intercept('POST', '/project/*/compile?*', { + body: { + status, + clsiServerId: 'foo', + compileGroup: 'priority', + }, + }).as('compile') + + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.findByRole('button', { name: 'Recompile' }) + + cy.findByText(message) + }) + } + + it('displays expandable raw logs', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.findByRole('button', { name: 'Recompile' }) + + cy.findByRole('button', { name: 'View logs' }).click() + cy.findByRole('button', { name: 'View PDF' }) + + cy.findByRole('button', { name: 'Expand' }).click() + cy.findByRole('button', { name: 'Collapse' }).click() + }) + + it('displays error messages if there were validation problems', function () { + const validationProblems = { + sizeCheck: { + resources: [ + { path: 'foo/bar', kbSize: 76221 }, + { path: 'bar/baz', kbSize: 2342 }, + ], + }, + mainFile: true, + conflictedPaths: [ + { + path: 'foo/bar', + }, + { + path: 'foo/baz', + }, + ], + } + + cy.intercept('POST', '/project/*/compile?*', { + body: { + status: 'validation-problems', + validationProblems, + clsiServerId: 'foo', + compileGroup: 'priority', + }, + }).as('compile') + + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.findByRole('button', { name: 'Recompile' }) + + cy.wait('@compile') + + cy.findByText('Project too large') + cy.findByText('Unknown main document') + cy.findByText('Conflicting Paths Found') + }) + + it('sends a clear cache request when the button is pressed', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.findByRole('button', { name: 'Recompile' }) + + cy.findByRole('button', { name: 'View logs' }).click() + cy.findByRole('button', { name: 'Clear cached files' }).should( + 'not.be.disabled' + ) + + cy.intercept('DELETE', 'project/*/output?*', { + statusCode: 204, + delay: 100, + }).as('clear-cache') + + // click the button + cy.findByRole('button', { name: 'Clear cached files' }).click() + cy.findByRole('button', { name: 'Clear cached files' }).should( + 'be.disabled' + ) + cy.wait('@clear-cache') + cy.findByRole('button', { name: 'Clear cached files' }).should( + 'not.be.disabled' + ) + }) + + it('handle "recompile from scratch"', function () { + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + // wait for "compile on load" to finish + cy.findByRole('button', { name: 'Compiling…' }) + cy.findByRole('button', { name: 'Recompile' }) + + // show the logs UI + cy.findByRole('button', { name: 'View logs' }).click() + + cy.findByRole('button', { name: 'Clear cached files' }).should( + 'not.be.disabled' + ) + + cy.interceptCompile() + + cy.intercept('DELETE', 'project/*/output?*', { + statusCode: 204, + delay: 100, + }).as('clear-cache') + + // TODO: open the menu? + cy.findByRole('menuitem', { + name: 'Recompile from scratch', + hidden: true, + }).trigger('click', { force: true }) + + cy.findByRole('button', { name: 'Clear cached files' }).should( + 'be.disabled' + ) + + cy.findByRole('button', { name: 'Compiling…' }) + cy.wait('@clear-cache') + cy.findByRole('button', { name: 'Recompile' }) + + cy.wait('@compile') + cy.wait('@compile-pdf') + }) + + it('shows an error for an invalid URL', function () { + cy.intercept('/build/*/output.pdf?*', { + statusCode: 500, + body: { + message: 'something awful happened', + code: 'AWFUL_ERROR', + }, + }).as('compile-pdf-error') + + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + cy.wait('@compile-pdf-error') + + cy.findByText('Something went wrong while rendering this PDF.') + cy.findByLabelText('Page 1').should('not.exist') + }) + + it('shows an error for a corrupt PDF', function () { + cy.intercept('/build/*/output.pdf?*', { + fixture: 'build/output-corrupt.pdf,null', + }).as('compile-pdf-corrupt') + + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + cy.wait('@compile-pdf-corrupt') + + cy.findByText('Something went wrong while rendering this PDF.') + cy.findByLabelText('Page 1').should('not.exist') + }) + }) + + describe('human readable logs', function () { + it('shows human readable hint for undefined reference errors', function () { + cy.intercept('/build/*/output.log?*', { + fixture: 'build/output-human-readable.log', + }).as('log') + + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + cy.wait('@log') + cy.findByRole('button', { name: 'View logs' }).click() + + cy.findByText( + "Reference `intorduction' on page 1 undefined on input line 11." + ) + cy.findByText( + "Reference `section1' on page 1 undefined on input line 13." + ) + cy.findByText('There were undefined references.') + + cy.findAllByText( + /You have referenced something which has not yet been labelled/ + ).should('have.length', 3) + }) + + it('does not show human readable hint when no undefined reference errors', function () { + cy.intercept('/build/*/output.log?*', { + fixture: 'build/output-undefined-references.log', + }).as('log') + + const scope = mockScope() + + mount( + +
+ +
+
+ ) + + cy.wait('@log') + cy.findByRole('button', { name: 'View logs' }).click() + + cy.findByText( + "Package rerunfilecheck Warning: File `output.brf' has changed. Rerun to get bibliographical references right." + ) + + cy.findByText( + /You have referenced something which has not yet been labelled/ + ).should('not.exist') + }) + }) +}) diff --git a/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx new file mode 100644 index 0000000000..62d642d27a --- /dev/null +++ b/services/web/test/frontend/components/pdf-preview/pdf-synctex-controls.spec.tsx @@ -0,0 +1,380 @@ +import PdfSynctexControls from '../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls' +import sysendTestHelper from '../../helpers/sysend' +import { cloneDeep } from 'lodash' +import { useDetachCompileContext as useCompileContext } from '../../../../frontend/js/shared/context/detach-compile-context' +import { useFileTreeData } from '../../../../frontend/js/shared/context/file-tree-data-context' +import { useEffect } from 'react' +import { mount } from '@cypress/react' +import { EditorProviders } from '../../helpers/editor-providers' +import { mockScope } from './scope' + +const mockHighlights = [ + { + page: 1, + h: 85.03936, + v: 509.999878, + width: 441.921265, + height: 8.855677, + }, + { + page: 1, + h: 85.03936, + v: 486.089539, + width: 441.921265, + height: 8.855677, + }, +] + +const mockPosition = { + page: 1, + offset: { top: 10, left: 10 }, + pageSize: { height: 500, width: 500 }, +} + +const mockSelectedEntities = [{ type: 'doc' }] + +const WithPosition = ({ mockPosition }) => { + const { setPosition } = useCompileContext() + + // mock PDF scroll position update + useEffect(() => { + setPosition(mockPosition) + }, [mockPosition, setPosition]) + + return null +} + +const WithSelectedEntities = ({ mockSelectedEntities = [] }) => { + const { setSelectedEntities } = useFileTreeData() + + useEffect(() => { + setSelectedEntities(mockSelectedEntities) + }, [mockSelectedEntities, setSelectedEntities]) + + return null +} +describe('', function () { + beforeEach(function () { + window.metaAttributesCache = new Map() + + cy.interceptCompile() + cy.interceptEvents() + + cy.intercept('/project/*/sync/code?*', { + body: { pdf: cloneDeep(mockHighlights) }, + delay: 100, + }).as('sync-code') + + cy.intercept('/project/*/sync/pdf?*', { + body: { code: [{ file: 'main.tex', line: 100 }] }, + delay: 100, + }).as('sync-pdf') + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + }) + + it('handles clicks on sync buttons', function () { + const scope = mockScope() + + mount( + + + + + + ) + + cy.get('.synctex-control-icon').should('have.length', 2) + + // mock editor cursor position update + cy.window().then(win => { + win.dispatchEvent( + new CustomEvent('cursor:editor:update', { + detail: { row: 100, column: 10 }, + }) + ) + }) + + cy.get('body') + .findByRole('button', { name: 'Go to code location in PDF' }) + .click() + cy.get('body') + .findByRole('button', { name: 'Go to code location in PDF' }) + .should('be.disabled') + + cy.wait('@sync-code') + + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .click() + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .should('be.disabled') + + cy.wait('@sync-pdf') + }) + + it('disables button when multiple entities are selected', function () { + const scope = mockScope() + + mount( + + + + + + ) + + cy.get('body') + .findByRole('button', { name: 'Go to code location in PDF' }) + .should('be.disabled') + + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .should('be.disabled') + }) + + it('disables button when a file is selected', function () { + const scope = mockScope() + + mount( + + + + + + ) + + cy.get('body') + .findByRole('button', { name: 'Go to code location in PDF' }) + .should('be.disabled') + + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .should('be.disabled') + }) + + describe('with detacher role', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-detachRole', 'detacher') + }) + + it('does not have go to PDF location button nor arrow icon', function () { + const scope = mockScope() + + mount( + + + + + + ) + + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .should('not.exist') + + cy.get('.synctex-control-icon').should('not.exist') + }) + + it('send set highlights action', function () { + const scope = mockScope() + + mount( + + + + + + ).then(() => { + sysendTestHelper.resetHistory() + }) + + cy.wait('@compile') + + // mock editor cursor position update + cy.window().then(win => { + win.dispatchEvent( + new CustomEvent('cursor:editor:update', { + detail: { row: 100, column: 10 }, + }) + ) + }) + + cy.findByRole('button', { + name: 'Go to code location in PDF', + }) + .should('not.be.disabled') + .click() + + cy.findByRole('button', { + name: 'Go to code location in PDF', + }).should('be.disabled') + + cy.wait('@sync-code').then(() => { + // synctex is called locally and the result are broadcast for the detached tab + expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({ + role: 'detacher', + event: 'action-setHighlights', + data: { args: [mockHighlights] }, + }) + }) + }) + + it('reacts to sync to code action', function () { + const scope = mockScope() + + mount( + + + + + + ) + + cy.wait('@compile').then(() => { + sysendTestHelper.receiveMessage({ + role: 'detached', + event: 'action-sync-to-code', + data: { + args: [mockPosition], + }, + }) + }) + + cy.wait('@sync-pdf') + }) + }) + + describe('with detached role', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-detachRole', 'detached') + }) + + it('does not have go to code location button nor arrow icon', function () { + const scope = mockScope() + + mount( + + + + + ) + + cy.findByRole('button', { + name: 'Go to code location in PDF', + }).should('not.exist') + + cy.get('.synctex-control-icon').should('not.exist') + }) + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('send go to code line action', function () { + const scope = mockScope() + + mount( + + + + + ) + + cy.wait('@compile') + + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .should('be.disabled') + .then(() => { + sysendTestHelper.receiveMessage({ + role: 'detached', + event: 'state-has-single-selected-doc', + data: { value: true }, + }) + }) + + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .should('not.be.disabled') + .then(() => { + sysendTestHelper.resetHistory() + }) + + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .click() + + // the button is only disabled when the state is updated via sysend + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .should('not.be.disabled') + + cy.get('.synctex-spin-icon') + .should('not.exist') + .then(() => { + expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({ + role: 'detached', + event: 'action-sync-to-code', + data: { + args: [mockPosition, 72], + }, + }) + }) + }) + + // eslint-disable-next-line mocha/no-skipped-tests + it.skip('update inflight state', function () { + const scope = mockScope() + + mount( + + + + + ).then(() => { + sysendTestHelper.receiveMessage({ + role: 'detached', + event: 'state-has-single-selected-doc', + data: { value: true }, + }) + }) + + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .should('be.disabled') + + cy.get('.synctex-spin-icon') + .should('not.exist') + .then(() => { + sysendTestHelper.receiveMessage({ + role: 'detacher', + event: 'state-sync-to-code-inflight', + data: { value: true }, + }) + }) + + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .should('be.disabled') + + cy.get('.synctex-spin-icon') + .should('have.length', 1) + .then(() => { + sysendTestHelper.receiveMessage({ + role: 'detacher', + event: 'state-sync-to-code-inflight', + data: { value: false }, + }) + }) + + cy.get('body') + .findByRole('button', { name: /^Go to PDF location in code/ }) + .should('not.be.disabled') + + cy.get('.synctex-spin-icon').should('not.exist') + }) + }) +}) diff --git a/services/web/test/frontend/components/pdf-preview/scope.tsx b/services/web/test/frontend/components/pdf-preview/scope.tsx new file mode 100644 index 0000000000..ca60f9970e --- /dev/null +++ b/services/web/test/frontend/components/pdf-preview/scope.tsx @@ -0,0 +1,13 @@ +export const mockScope = () => ({ + settings: { + syntaxValidation: false, + pdfViewer: 'pdfjs', + }, + editor: { + sharejs_doc: { + doc_id: 'test-doc', + getSnapshot: () => 'some doc content', + }, + }, + hasLintingError: false, +}) diff --git a/services/web/test/frontend/components/shared/beta-badge.spec.js b/services/web/test/frontend/components/shared/beta-badge.spec.tsx similarity index 100% rename from services/web/test/frontend/components/shared/beta-badge.spec.js rename to services/web/test/frontend/components/shared/beta-badge.spec.tsx diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.js index f3828e42a1..334d755154 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.js @@ -9,13 +9,8 @@ import FileTreeContextMenu from '../../../../../../frontend/js/features/file-tre describe('', function () { const setContextMenuCoords = sinon.stub() - beforeEach(function () { - global.requestAnimationFrame = sinon.stub() - }) - afterEach(function () { setContextMenuCoords.reset() - delete global.requestAnimationFrame }) describe('menu', function () { diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js index 11c498191a..e9e94f01b0 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js @@ -14,13 +14,11 @@ describe('', function () { const onInit = sinon.stub() beforeEach(function () { - global.requestAnimationFrame = sinon.stub() window.metaAttributesCache = new Map() window.metaAttributesCache.set('ol-user', { id: 'user1' }) }) afterEach(function () { - delete global.requestAnimationFrame fetchMock.restore() onSelect.reset() onInit.reset() diff --git a/services/web/test/frontend/features/file-tree/flows/create-folder.test.js b/services/web/test/frontend/features/file-tree/flows/create-folder.test.js index e46a86b3ca..1cc5f0eb19 100644 --- a/services/web/test/frontend/features/file-tree/flows/create-folder.test.js +++ b/services/web/test/frontend/features/file-tree/flows/create-folder.test.js @@ -15,13 +15,11 @@ describe('FileTree Create Folder Flow', function () { const onInit = sinon.stub() beforeEach(function () { - global.requestAnimationFrame = sinon.stub() window.metaAttributesCache = new Map() window.metaAttributesCache.set('ol-user', { id: 'user1' }) }) afterEach(function () { - delete global.requestAnimationFrame fetchMock.restore() onSelect.reset() onInit.reset() diff --git a/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js b/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js index 9c2fd64c0d..431b1efeef 100644 --- a/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js +++ b/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js @@ -15,13 +15,11 @@ describe('FileTree Rename Entity Flow', function () { const onInit = sinon.stub() beforeEach(function () { - global.requestAnimationFrame = sinon.stub() window.metaAttributesCache = new Map() window.metaAttributesCache.set('ol-user', { id: 'user1' }) }) afterEach(function () { - delete global.requestAnimationFrame fetchMock.restore() onSelect.reset() onInit.reset() diff --git a/services/web/test/frontend/features/pdf-preview/components/detach-compile-button.test.js b/services/web/test/frontend/features/pdf-preview/components/detach-compile-button.test.js deleted file mode 100644 index f2c5ac4b50..0000000000 --- a/services/web/test/frontend/features/pdf-preview/components/detach-compile-button.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import DetachCompileButton from '../../../../../frontend/js/features/pdf-preview/components/detach-compile-button' -import { renderWithEditorContext } from '../../../helpers/render-with-context' -import { screen } from '@testing-library/react' -import sysendTestHelper from '../../../helpers/sysend' -import { expect } from 'chai' - -describe('', function () { - afterEach(function () { - window.metaAttributesCache = new Map() - sysendTestHelper.resetHistory() - }) - - it('detacher mode and linked: show button ', async function () { - window.metaAttributesCache.set('ol-detachRole', 'detacher') - renderWithEditorContext() - sysendTestHelper.receiveMessage({ - role: 'detached', - event: 'connected', - }) - - await screen.getByRole('button', { - name: 'Recompile', - }) - }) - - it('detacher mode and not linked: does not show button ', async function () { - window.metaAttributesCache.set('ol-detachRole', 'detacher') - renderWithEditorContext() - - expect( - await screen.queryByRole('button', { - name: 'Recompile', - }) - ).to.not.exist - }) - - it('not detacher mode and linked: does not show button ', async function () { - window.metaAttributesCache.set('ol-detachRole', 'detached') - renderWithEditorContext() - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'connected', - }) - - expect( - await screen.queryByRole('button', { - name: 'Recompile', - }) - ).to.not.exist - }) -}) diff --git a/services/web/test/frontend/features/pdf-preview/components/pdf-js-viewer.test.js b/services/web/test/frontend/features/pdf-preview/components/pdf-js-viewer.test.js deleted file mode 100644 index 03d146a7bb..0000000000 --- a/services/web/test/frontend/features/pdf-preview/components/pdf-js-viewer.test.js +++ /dev/null @@ -1,37 +0,0 @@ -import { expect } from 'chai' -import { screen } from '@testing-library/react' -import path from 'path' -import { renderWithEditorContext } from '../../../helpers/render-with-context' -import { pathToFileURL } from 'url' -import PdfJsViewer from '../../../../../frontend/js/features/pdf-preview/components/pdf-js-viewer' - -const example = pathToFileURL( - path.join(__dirname, '../fixtures/test-example.pdf') -).toString() - -describe('', function () { - it('loads all PDF pages', async function () { - renderWithEditorContext() - - await screen.findByLabelText('Page 1') - await screen.findByLabelText('Page 2') - await screen.findByLabelText('Page 3') - expect(screen.queryByLabelText('Page 4')).to.not.exist - }) - - it('renders pages in a "loading" state', async function () { - renderWithEditorContext() - await screen.findByLabelText('Loading…') - }) - - it('can be unmounted while loading a document', async function () { - const { unmount } = renderWithEditorContext() - unmount() - }) - - it('can be unmounted after loading a document', async function () { - const { unmount } = renderWithEditorContext() - await screen.findByLabelText('Page 1') - unmount() - }) -}) diff --git a/services/web/test/frontend/features/pdf-preview/components/pdf-logs-entries.test.js b/services/web/test/frontend/features/pdf-preview/components/pdf-logs-entries.test.js deleted file mode 100644 index 51796f6275..0000000000 --- a/services/web/test/frontend/features/pdf-preview/components/pdf-logs-entries.test.js +++ /dev/null @@ -1,120 +0,0 @@ -import PdfLogsEntries from '../../../../../frontend/js/features/pdf-preview/components/pdf-logs-entries' -import { renderWithEditorContext } from '../../../helpers/render-with-context' -import { screen, fireEvent } from '@testing-library/react' -import sysendTestHelper from '../../../helpers/sysend' -import { expect } from 'chai' -import sinon from 'sinon' - -describe('', function () { - const fileTreeManager = {} - const editorManager = {} - const logEntries = [ - { - file: 'main.tex', - line: 9, - column: 8, - level: 'error', - message: 'LaTeX Error', - content: 'See the LaTeX manual', - raw: '', - ruleId: 'hint_misplaced_alignment_tab_character', - key: '', - }, - ] - const fakeEntity = { type: 'doc' } - - beforeEach(function () { - fileTreeManager.findEntityByPath = sinon.stub().returns(fakeEntity) - editorManager.openDoc = sinon.stub() - }) - - afterEach(function () { - window.metaAttributesCache = new Map() - sysendTestHelper.resetHistory() - fileTreeManager.findEntityByPath.resetHistory() - }) - - it('displays human readable hint', async function () { - renderWithEditorContext(, { - fileTreeManager, - editorManager, - }) - screen.getByText(/You have placed an alignment tab character/) - }) - - it('opens doc on click', async function () { - renderWithEditorContext(, { - fileTreeManager, - editorManager, - }) - - const button = await screen.getByRole('button', { - name: 'Navigate to log position in source code: main.tex, 9', - }) - fireEvent.click(button) - sinon.assert.calledOnce(fileTreeManager.findEntityByPath) - sinon.assert.calledOnce(editorManager.openDoc) - sinon.assert.calledWith(editorManager.openDoc, fakeEntity, { - gotoLine: 9, - gotoColumn: 8, - }) - }) - - it('opens doc via detached action', async function () { - window.metaAttributesCache.set('ol-detachRole', 'detacher') - - renderWithEditorContext(, { - fileTreeManager, - editorManager, - }) - - sysendTestHelper.receiveMessage({ - role: 'detached', - event: 'action-sync-to-entry', - data: { - args: [ - { - file: 'main.tex', - line: 7, - column: 6, - }, - ], - }, - }) - - sinon.assert.calledOnce(fileTreeManager.findEntityByPath) - sinon.assert.calledOnce(editorManager.openDoc) - sinon.assert.calledWith(editorManager.openDoc, fakeEntity, { - gotoLine: 7, - gotoColumn: 6, - }) - }) - - it('sends open doc clicks via detached action', async function () { - window.metaAttributesCache.set('ol-detachRole', 'detached') - renderWithEditorContext(, { - fileTreeManager, - editorManager, - }) - - const button = await screen.getByRole('button', { - name: 'Navigate to log position in source code: main.tex, 9', - }) - fireEvent.click(button) - sinon.assert.notCalled(fileTreeManager.findEntityByPath) - sinon.assert.notCalled(editorManager.openDoc) - expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({ - role: 'detached', - event: 'action-sync-to-entry', - data: { - args: [ - { - file: 'main.tex', - line: 9, - column: 8, - }, - ], - }, - }) - }) -}) diff --git a/services/web/test/frontend/features/pdf-preview/components/pdf-preview-detached-root.test.js b/services/web/test/frontend/features/pdf-preview/components/pdf-preview-detached-root.test.js deleted file mode 100644 index 6af6d9ca32..0000000000 --- a/services/web/test/frontend/features/pdf-preview/components/pdf-preview-detached-root.test.js +++ /dev/null @@ -1,70 +0,0 @@ -import { expect } from 'chai' -import { render, screen, fireEvent } from '@testing-library/react' -import sysendTestHelper from '../../../helpers/sysend' -import PdfPreviewDetachedRoot from '../../../../../frontend/js/features/pdf-preview/components/pdf-preview-detached-root' - -describe('', function () { - beforeEach(function () { - const user = { id: 'user1' } - window.user = user - - window.metaAttributesCache = new Map() - window.metaAttributesCache.set('ol-user', user) - window.metaAttributesCache.set('ol-project_id', 'project1') - window.metaAttributesCache.set('ol-detachRole', 'detached') - window.metaAttributesCache.set('ol-projectName', 'Project Name') - }) - - afterEach(function () { - window.metaAttributesCache = new Map() - }) - - it('syncs compiling state', async function () { - render() - - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'connected', - }) - - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'state-compiling', - data: { value: true }, - }) - await screen.findByRole('button', { name: 'Compiling…' }) - expect(screen.queryByRole('button', { name: 'Recompile' })).to.not.exist - - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'state-compiling', - data: { value: false }, - }) - await screen.findByRole('button', { name: 'Recompile' }) - expect(screen.queryByRole('button', { name: 'Compiling…' })).to.not.exist - }) - - it('sends a clear cache request when the button is pressed', async function () { - render() - - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'state-showLogs', - data: { value: true }, - }) - - const clearCacheButton = await screen.findByRole('button', { - name: 'Clear cached files', - }) - expect(clearCacheButton.hasAttribute('disabled')).to.be.false - - fireEvent.click(clearCacheButton) - expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({ - role: 'detached', - event: 'action-clearCache', - data: { - args: [], - }, - }) - }) -}) diff --git a/services/web/test/frontend/features/pdf-preview/components/pdf-preview-hybrid-toolbar.test.js b/services/web/test/frontend/features/pdf-preview/components/pdf-preview-hybrid-toolbar.test.js deleted file mode 100644 index 6167bd1a10..0000000000 --- a/services/web/test/frontend/features/pdf-preview/components/pdf-preview-hybrid-toolbar.test.js +++ /dev/null @@ -1,74 +0,0 @@ -import sinon from 'sinon' -import PdfPreviewHybridToolbar from '../../../../../frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar' -import { renderWithEditorContext } from '../../../helpers/render-with-context' -import { screen } from '@testing-library/react' -import sysendTestHelper from '../../../helpers/sysend' - -describe('', function () { - let clock - - beforeEach(function () { - clock = sinon.useFakeTimers() - }) - - afterEach(function () { - window.metaAttributesCache = new Map() - sysendTestHelper.resetHistory() - clock.runAll() - clock.restore() - }) - - it('shows normal mode', async function () { - renderWithEditorContext() - - await screen.getByRole('button', { - name: 'Recompile', - }) - }) - - describe('orphan mode', async function () { - it('shows connecting message on load', async function () { - window.metaAttributesCache.set('ol-detachRole', 'detached') - renderWithEditorContext() - - await screen.getByText(/Connecting with the editor/) - }) - - it('shows compile UI when connected', async function () { - window.metaAttributesCache.set('ol-detachRole', 'detached') - renderWithEditorContext() - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'connected', - }) - await screen.getByRole('button', { - name: 'Recompile', - }) - }) - - it('shows connecting message when disconnected', async function () { - window.metaAttributesCache.set('ol-detachRole', 'detached') - renderWithEditorContext() - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'connected', - }) - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'closed', - }) - - await screen.getByText(/Connecting with the editor/) - }) - - it('shows redirect button after timeout', async function () { - window.metaAttributesCache.set('ol-detachRole', 'detached') - renderWithEditorContext() - clock.tick(6000) - - await screen.getByRole('button', { - name: 'Redirect to editor', - }) - }) - }) -}) diff --git a/services/web/test/frontend/features/pdf-preview/components/pdf-preview.test.js b/services/web/test/frontend/features/pdf-preview/components/pdf-preview.test.js deleted file mode 100644 index 501c6d4b6d..0000000000 --- a/services/web/test/frontend/features/pdf-preview/components/pdf-preview.test.js +++ /dev/null @@ -1,523 +0,0 @@ -import { expect } from 'chai' -import sinon from 'sinon' -import fetchMock from 'fetch-mock' -import { screen, fireEvent, waitFor, cleanup } from '@testing-library/react' -import PdfPreview from '../../../../../frontend/js/features/pdf-preview/components/pdf-preview' -import { renderWithEditorContext } from '../../../helpers/render-with-context' -import nock from 'nock' -import { - corruptPDF, - defaultFileResponses, - mockBuildFile, - mockClearCache, - mockCompile, - mockCompileError, - mockValidationProblems, - mockValidPdf, -} from '../utils/mock-compile' - -const mockDelayed = fn => { - let _resolve = null - const delayPromise = new Promise((resolve, reject) => { - _resolve = resolve - }) - fn(delayPromise) - return _resolve -} - -const storeAndFireEvent = (key, value) => { - localStorage.setItem(key, value) - fireEvent(window, new StorageEvent('storage', { key })) -} - -const scope = { - settings: { - syntaxValidation: false, - }, - editor: { - sharejs_doc: { - doc_id: 'test-doc', - getSnapshot: () => 'some doc content', - }, - }, -} - -describe('', function () { - let clock - - beforeEach(function () { - clock = sinon.useFakeTimers({ - shouldAdvanceTime: true, - now: Date.now(), - }) - nock.cleanAll() - }) - - afterEach(function () { - clock.runAll() - clock.restore() - fetchMock.reset() - localStorage.clear() - sinon.restore() - }) - - it('renders the PDF preview', async function () { - mockCompile() - mockBuildFile() - mockValidPdf() - - renderWithEditorContext(, { scope }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - }) - - it('runs a compile when the Recompile button is pressed', async function () { - mockCompile() - mockBuildFile() - mockValidPdf() - - renderWithEditorContext(, { scope }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - mockValidPdf() - - // press the Recompile button => compile - const button = screen.getByRole('button', { name: 'Recompile' }) - button.click() - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - expect(fetchMock.calls()).to.have.length(6) - }) - - it('runs a compile on `pdf:recompile` event', async function () { - mockCompile() - mockBuildFile() - mockValidPdf() - - renderWithEditorContext(, { scope }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - mockValidPdf() - - fireEvent(window, new CustomEvent('pdf:recompile')) - - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - expect(fetchMock.calls()).to.have.length(6) - }) - - it('does not compile while compiling', async function () { - mockDelayed(mockCompile) - - renderWithEditorContext(, { scope }) - - // trigger compiles while "compile on load" is running - await screen.findByRole('button', { name: 'Compiling…' }) - fireEvent(window, new CustomEvent('pdf:recompile')) - - expect(fetchMock.calls()).to.have.length(1) - }) - - it('disables compile button while compile is running', async function () { - mockCompile() - mockBuildFile() - mockValidPdf() - - renderWithEditorContext(, { scope }) - - let button = screen.getByRole('button', { name: 'Compiling…' }) - expect(button.hasAttribute('disabled')).to.be.true - - button = await screen.findByRole('button', { name: 'Recompile' }) - expect(button.hasAttribute('disabled')).to.be.false - }) - - it('runs a compile on doc change if autocompile is enabled', async function () { - mockCompile() - mockBuildFile() - mockValidPdf() - - renderWithEditorContext(, { scope }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - // switch on auto compile - storeAndFireEvent('autocompile_enabled:project123', true) - - mockValidPdf() - - // fire a doc:changed event => compile - fireEvent(window, new CustomEvent('doc:changed')) - clock.tick(2000) // AUTO_COMPILE_DEBOUNCE - - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - expect(fetchMock.calls()).to.have.length(6) - }) - - it('does not run a compile on doc change if autocompile is disabled', async function () { - mockCompile() - mockBuildFile() - mockValidPdf() - - renderWithEditorContext(, { scope }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - // make sure auto compile is switched off - storeAndFireEvent('autocompile_enabled:project123', false) - - // fire a doc:changed event => no compile - fireEvent(window, new CustomEvent('doc:changed')) - clock.tick(2000) // AUTO_COMPILE_DEBOUNCE - screen.getByRole('button', { name: 'Recompile' }) - - expect(fetchMock.calls()).to.have.length(3) - }) - - it('does not run a compile on doc change if autocompile is blocked by syntax check', async function () { - mockCompile() - mockBuildFile() - mockValidPdf() - - renderWithEditorContext(, { - scope: { - ...scope, - 'settings.syntaxValidation': true, // enable linting in the editor - hasLintingError: true, // mock a linting error - }, - }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - // switch on auto compile and syntax checking - storeAndFireEvent('autocompile_enabled:project123', true) - storeAndFireEvent('stop_on_validation_error:project123', true) - - // fire a doc:changed event => no compile - fireEvent(window, new CustomEvent('doc:changed')) - clock.tick(2000) // AUTO_COMPILE_DEBOUNCE - screen.getByRole('button', { name: 'Recompile' }) - await screen.findByText('Code check failed') - - expect(fetchMock.calls()).to.have.length(3) - }) - - describe('displays error messages', function () { - const compileErrorStatuses = { - 'clear-cache': - 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.', - 'clsi-maintenance': - 'The compile servers are down for maintenance, and will be back shortly.', - 'compile-in-progress': - 'A previous compile is still running. Please wait a minute and try compiling again.', - exited: 'Server Error', - failure: 'No PDF', - generic: 'Server Error', - 'project-too-large': 'Project too large', - 'rate-limited': 'Compile rate limit hit', - terminated: 'Compilation cancelled', - timedout: 'Timed out', - 'too-recently-compiled': - 'This project was compiled very recently, so this compile has been skipped.', - unavailable: - 'Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.', - foo: 'Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.', - } - - for (const [status, message] of Object.entries(compileErrorStatuses)) { - it(`displays error message for '${status}' status`, async function () { - cleanup() - fetchMock.restore() - mockCompileError(status) - - renderWithEditorContext(, { scope }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - screen.getByText(message) - }) - } - }) - - it('displays expandable raw logs', async function () { - mockCompile() - mockBuildFile() - mockValidPdf() - - // pretend that the content is large enough to trigger a "collapse" - // (in jsdom these values are always zero) - sinon.stub(HTMLElement.prototype, 'scrollHeight').value(500) - sinon.stub(HTMLElement.prototype, 'scrollWidth').value(500) - - renderWithEditorContext(, { scope }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - const logsButton = screen.getByRole('button', { name: 'View logs' }) - logsButton.click() - - await screen.findByRole('button', { name: 'View PDF' }) - - // expand the log - const [expandButton] = screen.getAllByRole('button', { name: 'Expand' }) - expandButton.click() - - // collapse the log - const [collapseButton] = screen.getAllByRole('button', { name: 'Collapse' }) - collapseButton.click() - }) - - it('displays error messages if there were validation problems', async function () { - const validationProblems = { - sizeCheck: { - resources: [ - { path: 'foo/bar', kbSize: 76221 }, - { path: 'bar/baz', kbSize: 2342 }, - ], - }, - mainFile: true, - conflictedPaths: [ - { - path: 'foo/bar', - }, - { - path: 'foo/baz', - }, - ], - } - - mockValidationProblems(validationProblems) - - renderWithEditorContext(, { scope }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - screen.getByText('Project too large') - screen.getByText('Unknown main document') - screen.getByText('Conflicting Paths Found') - - expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param - expect(fetchMock.called('begin:https://clsi.test-overleaf.com/')).to.be - .false // TODO: actual path - }) - - it('sends a clear cache request when the button is pressed', async function () { - mockCompile() - mockBuildFile() - mockValidPdf() - - renderWithEditorContext(, { scope }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - const logsButton = screen.getByRole('button', { - name: 'View logs', - }) - logsButton.click() - - const clearCacheButton = await screen.findByRole('button', { - name: 'Clear cached files', - }) - expect(clearCacheButton.hasAttribute('disabled')).to.be.false - - mockClearCache() - - // click the button - clearCacheButton.click() - await waitFor(() => { - expect(clearCacheButton.hasAttribute('disabled')).to.be.true - }) - - await waitFor(() => { - expect(clearCacheButton.hasAttribute('disabled')).to.be.false - }) - - expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param - expect(fetchMock.called('begin:https://clsi.test-overleaf.com/')).to.be.true // TODO: actual path - }) - - it('handle "recompile from scratch"', async function () { - mockCompile() - mockBuildFile() - mockValidPdf() - - renderWithEditorContext(, { scope }) - - // wait for "compile on load" to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - // show the logs UI - const logsButton = screen.getByRole('button', { - name: 'View logs', - }) - logsButton.click() - - const clearCacheButton = await screen.findByRole('button', { - name: 'Clear cached files', - }) - expect(clearCacheButton.hasAttribute('disabled')).to.be.false - - mockValidPdf() - const finishClearCache = mockDelayed(mockClearCache) - - const recompileFromScratch = screen.getByRole('menuitem', { - name: 'Recompile from scratch', - hidden: true, - }) - recompileFromScratch.click() - - await waitFor(() => { - expect(clearCacheButton.hasAttribute('disabled')).to.be.true - }) - - finishClearCache() - - // wait for compile to finish - await screen.findByRole('button', { name: 'Compiling…' }) - await screen.findByRole('button', { name: 'Recompile' }) - - expect(fetchMock.called('express:/project/:projectId/compile')).to.be.true // TODO: auto_compile query param - expect(fetchMock.called('express:/project/:projectId/output')).to.be.true - expect(fetchMock.called('begin:https://clsi.test-overleaf.com/')).to.be.true // TODO: actual path - }) - - it('shows an error for an invalid URL', async function () { - mockCompile() - mockBuildFile() - - nock('https://clsi.test-overleaf.com') - .get(/^\/build\/output.pdf/) - .replyWithError({ - message: 'something awful happened', - code: 'AWFUL_ERROR', - }) - - renderWithEditorContext(, { scope }) - - await screen.findByText('Something went wrong while rendering this PDF.') - expect(screen.queryByLabelText('Page 1')).to.not.exist - - expect(nock.isDone()).to.be.true - }) - - it('shows an error for a corrupt PDF', async function () { - mockCompile() - mockBuildFile() - - nock('https://clsi.test-overleaf.com') - .get(/^\/build\/output.pdf/) - .replyWithFile(200, corruptPDF) - - renderWithEditorContext(, { scope }) - - await screen.findByText('Something went wrong while rendering this PDF.') - expect(screen.queryByLabelText('Page 1')).to.not.exist - - expect(nock.isDone()).to.be.true - }) - - describe('human readable logs', function () { - it('shows human readable hint for undefined reference errors', async function () { - mockCompile() - mockBuildFile({ - ...defaultFileResponses, - '/build/output.log': ` -log This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdflatex 2020.9.10) 8 FEB 2022 16:27 -entering extended mode - \\write18 enabled. - %&-line parsing enabled. -**main.tex -(./main.tex -LaTeX2e <2020-02-02> patch level 5 - -LaTeX Warning: Reference \`intorduction' on page 1 undefined on input line 11. - - -LaTeX Warning: Reference \`section1' on page 1 undefined on input line 13. - -[1 - -{/usr/local/texlive/2020/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (/compi -le/output.aux) - -LaTeX Warning: There were undefined references. - - ) -`, - }) - mockValidPdf() - - renderWithEditorContext(, { scope }) - - await screen.findByText( - "Reference `intorduction' on page 1 undefined on input line 11." - ) - await screen.findByText( - "Reference `section1' on page 1 undefined on input line 13." - ) - await screen.findByText('There were undefined references.') - const hints = await screen.findAllByText( - /You have referenced something which has not yet been labelled/ - ) - expect(hints.length).to.equal(3) - }) - - it('idoes not show human readable hint for undefined reference errors', async function () { - mockCompile() - mockBuildFile({ - ...defaultFileResponses, - '/build/output.log': ` -Package rerunfilecheck Info: File \`output.out' has not changed. -(rerunfilecheck) Checksum: 339DB29951BB30436898BC39909EA4FA;11265. - -Package rerunfilecheck Warning: File \`output.brf' has changed. -(rerunfilecheck) Rerun to get bibliographical references right. - -Package rerunfilecheck Info: Checksums for \`output.brf': -(rerunfilecheck) Before: D41D8CD98F00B204E9800998ECF8427E;0 -(rerunfilecheck) After: DF3260FAD3828D54C5E4E9337E97F7AF;4841. - ) -`, - }) - mockValidPdf() - - renderWithEditorContext(, { scope }) - - await screen.findByText( - /Package rerunfilecheck Warning: File `output.brf' has changed. Rerun to get bibliographical references right./ - ) - expect( - screen.queryByText( - /You have referenced something which has not yet been labelled/ - ) - ).to.not.exist - }) - }) -}) diff --git a/services/web/test/frontend/features/pdf-preview/components/pdf-synctex-controls.test.js b/services/web/test/frontend/features/pdf-preview/components/pdf-synctex-controls.test.js deleted file mode 100644 index aa91463237..0000000000 --- a/services/web/test/frontend/features/pdf-preview/components/pdf-synctex-controls.test.js +++ /dev/null @@ -1,437 +0,0 @@ -import PdfSynctexControls from '../../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls' -import { renderWithEditorContext } from '../../../helpers/render-with-context' -import sysendTestHelper from '../../../helpers/sysend' -import { cloneDeep } from 'lodash' -import fetchMock from 'fetch-mock' -import { fireEvent, screen, waitFor } from '@testing-library/react' -import fs from 'fs' -import path from 'path' -import { expect } from 'chai' -import { useDetachCompileContext as useCompileContext } from '../../../../../frontend/js/shared/context/detach-compile-context' -import { useFileTreeData } from '../../../../../frontend/js/shared/context/file-tree-data-context' -import { useEffect } from 'react' - -const examplePDF = path.join(__dirname, '../fixtures/test-example.pdf') - -const scope = { - settings: { - syntaxValidation: false, - pdfViewer: 'pdfjs', - }, - editor: { - sharejs_doc: { - doc_id: 'test-doc', - getSnapshot: () => 'some doc content', - }, - }, -} - -const outputFiles = [ - { - path: 'output.pdf', - build: '123', - url: '/build/output.pdf', - type: 'pdf', - }, - { - path: 'output.log', - build: '123', - url: '/build/output.log', - type: 'log', - }, -] - -const mockCompile = () => - fetchMock.post('express:/project/:projectId/compile', { - body: { - status: 'success', - clsiServerId: 'foo', - compileGroup: 'standard', - pdfDownloadDomain: 'https://clsi.test-overleaf.com', - outputFiles: cloneDeep(outputFiles), - }, - }) - -const fileResponses = { - '/build/output.pdf': () => fs.createReadStream(examplePDF), - '/build/output.log': '', -} - -const mockBuildFile = () => - fetchMock.get('begin:https://clsi.test-overleaf.com/', _url => { - const url = new URL(_url, 'https://clsi.test-overleaf.com') - - if (url.pathname in fileResponses) { - return fileResponses[url.pathname] - } - - return 404 - }) - -const mockHighlights = [ - { - page: 1, - h: 85.03936, - v: 509.999878, - width: 441.921265, - height: 8.855677, - }, - { - page: 1, - h: 85.03936, - v: 486.089539, - width: 441.921265, - height: 8.855677, - }, -] - -const mockPosition = { - page: 1, - offset: { top: 10, left: 10 }, - pageSize: { height: 500, width: 500 }, -} - -const mockSelectedEntities = [{ type: 'doc' }] - -const mockSynctex = () => - fetchMock - .get('express:/project/:projectId/sync/code', () => { - return { pdf: cloneDeep(mockHighlights) } - }) - .get('express:/project/:projectId/sync/pdf', () => { - return { code: [{ file: 'main.tex', line: 100 }] } - }) - -const WithPosition = ({ mockPosition }) => { - const { setPosition } = useCompileContext() - - // mock PDF scroll position update - useEffect(() => { - setPosition(mockPosition) - }, [mockPosition, setPosition]) - - return null -} - -const WithSelectedEntities = ({ mockSelectedEntities = [] }) => { - const { setSelectedEntities } = useFileTreeData() - - useEffect(() => { - setSelectedEntities(mockSelectedEntities) - }, [mockSelectedEntities, setSelectedEntities]) - - return null -} -describe('', function () { - beforeEach(function () { - window.metaAttributesCache = new Map() - fetchMock.restore() - mockCompile() - mockSynctex() - mockBuildFile() - }) - - afterEach(function () { - window.metaAttributesCache = new Map() - fetchMock.restore() - }) - - it('handles clicks on sync buttons', async function () { - const { container } = renderWithEditorContext( - <> - - - - , - { scope } - ) - - const syncToPdfButton = await screen.findByRole('button', { - name: 'Go to code location in PDF', - }) - - const syncToCodeButton = await screen.findByRole('button', { - name: /Go to PDF location in code/, - }) - - expect(container.querySelectorAll('.synctex-control-icon').length).to.equal( - 2 - ) - - // mock editor cursor position update - fireEvent( - window, - new CustomEvent('cursor:editor:update', { - detail: { row: 100, column: 10 }, - }) - ) - - fireEvent.click(syncToPdfButton) - - expect(syncToPdfButton.disabled).to.be.true - - await waitFor(() => { - expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be - .true - }) - - fireEvent.click(syncToCodeButton) - - expect(syncToCodeButton.disabled).to.be.true - - await waitFor(() => { - expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be - .true - }) - }) - it('disables button when multiple entities are selected', async function () { - renderWithEditorContext( - <> - - - - , - { scope } - ) - - const syncToPdfButton = await screen.findByRole('button', { - name: 'Go to code location in PDF', - }) - expect(syncToPdfButton.disabled).to.be.true - - const syncToCodeButton = await screen.findByRole('button', { - name: /Go to PDF location in code/, - }) - expect(syncToCodeButton.disabled).to.be.true - }) - - it('disables button when a file is selected', async function () { - renderWithEditorContext( - <> - - - - , - { scope } - ) - - const syncToPdfButton = await screen.findByRole('button', { - name: 'Go to code location in PDF', - }) - expect(syncToPdfButton.disabled).to.be.true - - const syncToCodeButton = await screen.findByRole('button', { - name: /Go to PDF location in code/, - }) - expect(syncToCodeButton.disabled).to.be.true - }) - - describe('with detacher role', async function () { - beforeEach(function () { - window.metaAttributesCache.set('ol-detachRole', 'detacher') - }) - - it('does not have go to PDF location button nor arrow icon', async function () { - const { container } = renderWithEditorContext( - <> - - - - , - { scope } - ) - - expect( - await screen.queryByRole('button', { - name: 'Go to PDF location in code', - }) - ).to.not.exist - - expect(container.querySelector('.synctex-control-icon')).to.not.exist - }) - - it('send set highlights action', async function () { - renderWithEditorContext( - <> - - - - , - { scope } - ) - sysendTestHelper.resetHistory() - - const syncToPdfButton = await screen.findByRole('button', { - name: 'Go to code location in PDF', - }) - - // mock editor cursor position update - fireEvent( - window, - new CustomEvent('cursor:editor:update', { - detail: { row: 100, column: 10 }, - }) - ) - - expect(syncToPdfButton.disabled).to.be.false - - fireEvent.click(syncToPdfButton) - - expect(syncToPdfButton.disabled).to.be.true - - await waitFor(() => { - expect(fetchMock.called('express:/project/:projectId/sync/code')).to.be - .true - }) - - // synctex is called locally and the result are broadcast for the detached - // tab - expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({ - role: 'detacher', - event: 'action-setHighlights', - data: { args: [mockHighlights] }, - }) - }) - - it('reacts to sync to code action', async function () { - renderWithEditorContext( - <> - - - - , - { scope } - ) - - await waitFor(() => { - expect(fetchMock.called('express:/project/:projectId/compile')).to.be - .true - }) - - sysendTestHelper.receiveMessage({ - role: 'detached', - event: 'action-sync-to-code', - data: { - args: [mockPosition], - }, - }) - - await waitFor(() => { - expect(fetchMock.called('express:/project/:projectId/sync/pdf')).to.be - .true - }) - }) - }) - - describe('with detached role', async function () { - beforeEach(function () { - window.metaAttributesCache.set('ol-detachRole', 'detached') - }) - - it('does not have go to code location button nor arrow icon', async function () { - const { container } = renderWithEditorContext( - <> - - - , - { scope } - ) - - expect( - await screen.queryByRole('button', { - name: 'Go to code location in PDF', - }) - ).to.not.exist - - expect(container.querySelector('.synctex-control-icon')).to.not.exist - }) - - it('send go to code line action', async function () { - const { container } = renderWithEditorContext( - <> - - - , - { scope } - ) - - const syncToCodeButton = await screen.findByRole('button', { - name: /Go to PDF location in code/, - }) - expect(syncToCodeButton.disabled).to.be.true - - sysendTestHelper.receiveMessage({ - role: 'detached', - event: 'state-has-single-selected-doc', - data: { value: true }, - }) - expect(syncToCodeButton.disabled).to.be.false - - sysendTestHelper.resetHistory() - - fireEvent.click(syncToCodeButton) - - // the button is only disabled when the state is updated via sysend - expect(syncToCodeButton.disabled).to.be.false - expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal( - 0 - ) - - expect(sysendTestHelper.getLastBroacastMessage()).to.deep.equal({ - role: 'detached', - event: 'action-sync-to-code', - data: { - args: [mockPosition, 72], - }, - }) - }) - - it('update inflight state', async function () { - const { container } = renderWithEditorContext( - <> - - - , - { scope } - ) - sysendTestHelper.receiveMessage({ - role: 'detached', - event: 'state-has-single-selected-doc', - data: { value: true }, - }) - - const syncToCodeButton = await screen.findByRole('button', { - name: /Go to PDF location in code/, - }) - - expect(syncToCodeButton.disabled).to.be.false - expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal( - 0 - ) - - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'state-sync-to-code-inflight', - data: { value: true }, - }) - - expect(syncToCodeButton.disabled).to.be.true - expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal( - 1 - ) - - sysendTestHelper.receiveMessage({ - role: 'detacher', - event: 'state-sync-to-code-inflight', - data: { value: false }, - }) - - expect(syncToCodeButton.disabled).to.be.false - expect(container.querySelectorAll('.synctex-spin-icon').length).to.equal( - 0 - ) - }) - }) -}) diff --git a/services/web/test/frontend/features/pdf-preview/utils/mock-compile.js b/services/web/test/frontend/features/pdf-preview/utils/mock-compile.js deleted file mode 100644 index 55d78820d4..0000000000 --- a/services/web/test/frontend/features/pdf-preview/utils/mock-compile.js +++ /dev/null @@ -1,148 +0,0 @@ -import fetchMock from 'fetch-mock' -import { cloneDeep } from 'lodash' -import nock from 'nock' -import fs from 'fs' -import path from 'path' - -export const examplePDF = path.join(__dirname, '../fixtures/test-example.pdf') -export const corruptPDF = path.join( - __dirname, - '../fixtures/test-example-corrupt.pdf' -) - -const outputFiles = [ - { - path: 'output.pdf', - build: '123', - url: '/build/output.pdf', - type: 'pdf', - }, - { - path: 'output.bbl', - build: '123', - url: '/build/output.bbl', - type: 'bbl', - }, - { - path: 'output.bib', - build: '123', - url: '/build/output.bib', - type: 'bib', - }, - { - path: 'example.txt', - build: '123', - url: '/build/example.txt', - type: 'txt', - }, - { - path: 'output.log', - build: '123', - url: '/build/output.log', - type: 'log', - }, - { - path: 'output.blg', - build: '123', - url: '/build/output.blg', - type: 'blg', - }, -] - -export const mockCompile = (delayPromise = Promise.resolve()) => - fetchMock.post( - 'express:/project/:projectId/compile', - delayPromise.then(() => ({ - body: { - status: 'success', - clsiServerId: 'foo', - compileGroup: 'priority', - pdfDownloadDomain: 'https://clsi.test-overleaf.com', - outputFiles: cloneDeep(outputFiles), - }, - })) - ) - -export const mockCompileError = status => - fetchMock.post('express:/project/:projectId/compile', { - body: { - status, - clsiServerId: 'foo', - compileGroup: 'priority', - }, - }) - -export const mockValidationProblems = validationProblems => - fetchMock.post('express:/project/:projectId/compile', { - body: { - status: 'validation-problems', - validationProblems, - clsiServerId: 'foo', - compileGroup: 'priority', - }, - }) - -export const mockClearCache = (delayPromise = Promise.resolve()) => - fetchMock.delete( - 'express:/project/:projectId/output', - delayPromise.then(() => ({ - body: { - status: 204, - }, - })) - ) - -export const mockValidPdf = () => { - nock('https://clsi.test-overleaf.com') - .get(/^\/build\/output\.pdf/) - .replyWithFile(200, examplePDF) -} - -export const defaultFileResponses = { - '/build/output.pdf': () => fs.createReadStream(examplePDF), - '/build/output.blg': 'This is BibTeX, Version 4.0', // FIXME - '/build/output.log': ` -The LaTeX compiler output - * With a lot of details - -Wrapped in an HTML
 element with
-      preformatted text which is to be presented exactly
-            as written in the HTML file
-
-                                              (whitespace included™)
-
-The text is typically rendered using a non-proportional ("monospace") font.
-
-LaTeX Font Info:    External font \`cmex10' loaded for size
-(Font)              <7> on input line 18.
-LaTeX Font Info:    External font \`cmex10' loaded for size
-(Font)              <5> on input line 18.
-! Undefined control sequence.
- \\Zlpha
-
- main.tex, line 23
-
-`,
-}
-
-export const mockBuildFile = (responses = defaultFileResponses) => {
-  fetchMock.get('begin:https://clsi.test-overleaf.com/', _url => {
-    const url = new URL(_url, 'https://clsi.test-overleaf.com')
-
-    if (url.pathname in responses) {
-      return responses[url.pathname]
-    }
-
-    return 404
-  })
-
-  fetchMock.get('express:/build/:file', (_url, options, request) => {
-    const url = new URL(_url, 'https://example.com')
-
-    if (url.pathname in responses) {
-      return responses[url.pathname]
-    }
-
-    return 404
-  })
-}
diff --git a/services/web/test/frontend/helpers/editor-providers.js b/services/web/test/frontend/helpers/editor-providers.js
index 40878b87ea..f039bebc24 100644
--- a/services/web/test/frontend/helpers/editor-providers.js
+++ b/services/web/test/frontend/helpers/editor-providers.js
@@ -28,7 +28,7 @@ export function EditorProviders({
   },
   isRestrictedTokenMember = false,
   clsiServerId = '1234',
-  scope,
+  scope = {},
   features = {
     referencesSearch: true,
   },
diff --git a/services/web/tsconfig.json b/services/web/tsconfig.json
index 098dd4cde8..5b033da62b 100644
--- a/services/web/tsconfig.json
+++ b/services/web/tsconfig.json
@@ -10,13 +10,15 @@
     "moduleResolution": "node" /* Specify module resolution strategy */,
     "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
     "skipLibCheck": true /* Skip type checking of declaration files. */,
-    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
+    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
+    "types": ["cypress", "@testing-library/cypress"]
   },
   "include": [
     "frontend/js/**/*.*",
     "modules/**/frontend/js/**/*.*",
     "test/frontend/**/*.*",
     "modules/**/test/frontend/**/*.*",
+    "cypress",
     "types"
   ]
 }
diff --git a/services/web/types/window.ts b/services/web/types/window.ts
index 36972f87da..5667e84da2 100644
--- a/services/web/types/window.ts
+++ b/services/web/types/window.ts
@@ -5,6 +5,11 @@ declare global {
     user: {
       id: string
     }
+    metaAttributesCache: Map
+    i18n: {
+      currentLangCode: string
+    }
+    ExposedSettings: Record
   }
 }
 export {} // pretend this is a module
diff --git a/services/web/webpack.config.js b/services/web/webpack.config.js
index 0b12c6058a..3a6462b6fa 100644
--- a/services/web/webpack.config.js
+++ b/services/web/webpack.config.js
@@ -234,10 +234,12 @@ module.exports = {
       output: 'manifest.json',
     }),
 
-    // Ensure that process.env.RESET_APP_DATA_TIMER is defined, to avoid an error.
-    // https://github.com/algolia/algoliasearch-client-javascript/issues/756
     new webpack.EnvironmentPlugin({
+      // Ensure that process.env.RESET_APP_DATA_TIMER is defined, to avoid an error.
+      // https://github.com/algolia/algoliasearch-client-javascript/issues/756
       RESET_APP_DATA_TIMER: '120000',
+      // Ensure that process.env.CYPRESS is defined (see utils/worker.js)
+      CYPRESS: false,
     }),
 
     // Prevent moment from loading (very large) locale files that aren't used