diff --git a/.github/workflows/frontend-build-and-packaging.yml b/.github/workflows/frontend-build-and-packaging.yml new file mode 100644 index 00000000..f41cb0aa --- /dev/null +++ b/.github/workflows/frontend-build-and-packaging.yml @@ -0,0 +1,29 @@ +name: frontend-build-and-packaging + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build-and-packaging: + runs-on: ubuntu-22.04 + steps: + - name: Checkout frontend sources + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Display Node version + run: node -v && npm -v + + - name: Install frontend dependencies + working-directory: frontend + run: npm ci + + - name: Run build and package + working-directory: frontend + run: npm run package \ No newline at end of file diff --git a/.github/workflows/frontend-e2e-tests.yml b/.github/workflows/frontend-e2e-tests.yml new file mode 100644 index 00000000..be5cf1d6 --- /dev/null +++ b/.github/workflows/frontend-e2e-tests.yml @@ -0,0 +1,86 @@ +name: frontend-e2e-tests + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + e2e: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Display Python version + run: python -c "import sys; print(sys.version)" + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Install Xvfb + run: sudo apt-get update && sudo apt-get install -y xvfb + + - name: Define dataset list and cache key + id: datasets + run: | + urls=($(cat zenodo_datasets.txt)) + echo "files=${urls[*]}" >> $GITHUB_OUTPUT + + # Generate a hash key from the URLs list, this is used for caching + key=$(echo "${urls[*]}" | sha256sum | cut -d ' ' -f1) + echo "key=${key}" >> $GITHUB_OUTPUT + + - name: Cache Zenodo datasets + id: cache-datasets + uses: actions/cache/restore@v4 + with: + path: e2e_datasets + key: ${{ steps.datasets.outputs.key }} + + - if: ${{ steps.cache-datasets.outputs.cache-hit != 'true' }} + name: Download datasets if not cached + run: | + mkdir -p e2e_datasets + for url in ${{ steps.datasets.outputs.files }}; do + echo "Downloading $(basename $url) ..." + wget -P e2e_datasets/ "$url" + done + + - name: Always save Zenodo datasets (even if pytest would fail) + id: cache-datasets-save + if: always() && steps.cache-datasets.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + key: ${{ steps.cache-datasets.outputs.cache-primary-key }} + path: e2e_datasets + + - name: Run E2E tests + working-directory: frontend + run: | + xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" bash -c " + python3 -m venv ../ibex_venv + source ../ibex_venv/bin/activate + pip install --upgrade pip setuptools wheel + cd ../backend + pip install -e . + cd ../frontend + npm run start:e2e & + npx wait-on tcp:9222 + npx wait-on http://127.0.0.1:8000/docs/ + sleep 5 + npm run test:e2e + " \ No newline at end of file diff --git a/.github/workflows/frontend-linting.yml b/.github/workflows/frontend-linting.yml new file mode 100644 index 00000000..1bf4f54f --- /dev/null +++ b/.github/workflows/frontend-linting.yml @@ -0,0 +1,33 @@ +name: frontend-linting-and-formatting + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + runs-on: ubuntu-22.04 + steps: + - name: Checkout frontend sources + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Display Node version + run: node -v && npm -v + + - name: Install frontend dependencies + working-directory: frontend + run: npm ci + + - name: Run code formatting + working-directory: frontend + run: npm run format + + - name: Run linting + working-directory: frontend + run: npm run lint \ No newline at end of file diff --git a/.gitignore b/.gitignore index 79f635bc..66674af6 100644 --- a/.gitignore +++ b/.gitignore @@ -105,4 +105,7 @@ saxon*.jar # ASV folder /.asv -.idea/ \ No newline at end of file +.idea/ + +# Local folder for E2E tests +e2e_datasets \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 247f579c..ae1bda1c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@mantine/notifications": "7.17.7", "@tabler/icons-react": "3.23.0", "@tensorflow/tfjs": "4.22.0", + "axios": "1.13.6", "dotenv": "16.4.7", "electron-default-menu": "1.0.2", "electron-squirrel-startup": "1.0.1", @@ -50,7 +51,6 @@ "@types/selenium-webdriver": "4.1.28", "@vercel/webpack-asset-relocator-loader": "1.7.3", "chai": "4.3.10", - "chromedriver": "134.0.5", "cross-env": "7.0.3", "css-loader": "6.0.0", "electron": "35.7.5", @@ -1967,12 +1967,6 @@ "node": ">=10" } }, - "node_modules/@testim/chrome-version": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz", - "integrity": "sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g==", - "dev": true - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -1982,12 +1976,6 @@ "node": ">= 10" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -3432,18 +3420,6 @@ "node": "*" } }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -3492,13 +3468,12 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "dev": true, + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -3545,15 +3520,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -4096,28 +4062,6 @@ "node": ">=6.0" } }, - "node_modules/chromedriver": { - "version": "134.0.5", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-134.0.5.tgz", - "integrity": "sha512-edXbiuShAvH6Elx8Hobl4NQkgNRMIozcW7ZlEiE8TBynZHRazrepO9hfftQzZgztPvjMQiSWeWjZaDV3SecYaw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@testim/chrome-version": "^1.1.4", - "axios": "^1.7.4", - "compare-versions": "^6.1.0", - "extract-zip": "^2.0.1", - "proxy-agent": "^6.4.0", - "proxy-from-env": "^1.1.0", - "tcp-port-used": "^1.0.2" - }, - "bin": { - "chromedriver": "bin/chromedriver" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/clamp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", @@ -4445,12 +4389,6 @@ "node": ">=0.10.0" } }, - "node_modules/compare-versions": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", - "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", - "dev": true - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -5031,15 +4969,6 @@ "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", "peer": true }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5321,20 +5250,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6647,6 +6562,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "peer": true, "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -6936,6 +6852,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "peer": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -7505,7 +7422,6 @@ "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, "funding": [ { "type": "individual", @@ -7963,20 +7879,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/gl-mat4": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gl-mat4/-/gl-mat4-1.2.0.tgz", @@ -9053,15 +8955,6 @@ "node": ">= 12" } }, - "node_modules/ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9557,12 +9450,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", - "dev": true - }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -9618,20 +9505,6 @@ "node": ">=8" } }, - "node_modules/is2": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz", - "integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "ip-regex": "^4.1.0", - "is-url": "^1.2.4" - }, - "engines": { - "node": ">=v0.10.0" - } - }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -10831,15 +10704,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -11429,87 +11293,6 @@ "node": ">=4" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -12252,79 +12035,10 @@ "node": ">= 0.10" } }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/pump": { "version": "3.0.3", @@ -14638,39 +14352,6 @@ "node": ">=8" } }, - "node_modules/tcp-port-used": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", - "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", - "dev": true, - "dependencies": { - "debug": "4.3.1", - "is2": "^2.0.6" - } - }, - "node_modules/tcp-port-used/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/tcp-port-used/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index f5809eff..afa3fe86 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,6 @@ "@types/selenium-webdriver": "4.1.28", "@vercel/webpack-asset-relocator-loader": "1.7.3", "chai": "4.3.10", - "chromedriver": "134.0.5", "cross-env": "7.0.3", "css-loader": "6.0.0", "electron": "35.7.5", @@ -72,6 +71,7 @@ "@mantine/notifications": "7.17.7", "@tabler/icons-react": "3.23.0", "@tensorflow/tfjs": "4.22.0", + "axios": "1.13.6", "dotenv": "16.4.7", "electron-default-menu": "1.0.2", "electron-squirrel-startup": "1.0.1", diff --git a/frontend/src/config/bridge.ts b/frontend/src/config/bridge.ts index f73011e3..51310935 100644 --- a/frontend/src/config/bridge.ts +++ b/frontend/src/config/bridge.ts @@ -4,5 +4,8 @@ import { API } from '../preload'; declare global { interface Window { api: typeof API; + env: { + E2E_TEST: string; + }; } } diff --git a/frontend/src/config/config.ts b/frontend/src/config/config.ts index 5550c975..df5726cf 100644 --- a/frontend/src/config/config.ts +++ b/frontend/src/config/config.ts @@ -8,35 +8,12 @@ export type TConfig = { LOGGER_PORT: number; }; -const defaultConfig: TConfig = { - API_URL: 'http://localhost:8000', - WEBPACK_PORT: 3001, - LOGGER_PORT: 9013, -}; - -const getConfigPath = (): string => { - const configDir = path.join(os.homedir(), '.config', 'ibex'); - const configPath = path.join(configDir, 'config.json'); - - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - - return configPath; -}; - -export const getConfigSync = (): TConfig => { - // Check if backend URL is provided by Electron main process - const backendUrl = process.env.IBEX_BACKEND_URL; - - if (backendUrl) { - console.info(`Using backend URL from Electron: ${backendUrl}`); - return { - ...defaultConfig, - API_URL: backendUrl, - }; - } - +const createDefaultConfig = (): TConfig => { + const defaultConfig = { + API_URL: 'http://localhost:8000', + WEBPACK_PORT: 3001, + LOGGER_PORT: Math.floor(49152 + Math.random() * (65535 - 49152)), + } as TConfig; const configPath = getConfigPath(); try { @@ -49,7 +26,7 @@ export const getConfigSync = (): TConfig => { } const data = fs.readFileSync(configPath, 'utf-8'); - const parsed = JSON.parse(data); + const parsed = JSON.parse(data) as TConfig; if ( typeof parsed.API_URL === 'string' && @@ -59,10 +36,33 @@ export const getConfigSync = (): TConfig => { return parsed; } - console.warn('Configuration invalide, retour à la config par défaut.'); - return defaultConfig; + console.warn('Read invalid configuration, get the default configuration.'); + return defaultConfig as TConfig; } catch (err) { - console.error('Erreur de lecture de config:', err); + console.error('Error reading the configuration file:', err); return defaultConfig; } }; + +const getConfigPath = (): string => { + const configDir = path.join(os.homedir(), '.config', 'ibex'); + const configPath = path.join(configDir, 'config.json'); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + return configPath; +}; + +export const getConfigSync = (): TConfig => { + // Check if backend URL is provided by Electron main process + const backendUrl = process.env.IBEX_BACKEND_URL; + if (backendUrl) { + console.info(`Using backend URL from Electron: ${backendUrl}`); + return { + ...createDefaultConfig(), + API_URL: backendUrl, + }; + } else { + return { ...createDefaultConfig() }; + } +}; diff --git a/frontend/src/main/ipc.ts b/frontend/src/main/ipc.ts index 647940fb..0d4161f9 100644 --- a/frontend/src/main/ipc.ts +++ b/frontend/src/main/ipc.ts @@ -147,11 +147,11 @@ export default { return app.getPath('home'); }); - ipcMain.handle('getDefaultTemplatesPath', () => { + ipcMain.handle('getPathFromRessources', (event, folderSteps: string[]) => { const templatesPath = process.env.NODE_ENV === 'development' - ? path.join(app.getAppPath(), '..', 'templates') // root in dev - : path.join(process.resourcesPath, 'templates'); // resources/ in prod + ? path.join(app.getAppPath(), '..', ...folderSteps) // root in dev + : path.join(process.resourcesPath, ...folderSteps); // resources/ in prod return templatesPath; }); }, diff --git a/frontend/src/preload.ts b/frontend/src/preload.ts index 0c4dd73b..9710d84c 100644 --- a/frontend/src/preload.ts +++ b/frontend/src/preload.ts @@ -65,7 +65,10 @@ export const API = { getHomePath: async () => await ipcRenderer.invoke('getHomePath'), getDefaultTemplatesPath: async () => - await ipcRenderer.invoke('getDefaultTemplatesPath'), + await ipcRenderer.invoke('getPathFromRessources', ['templates']), + + getZenodoDataPath: async () => + await ipcRenderer.invoke('getPathFromRessources', ['e2e_datasets']), }, preferences: { @@ -107,3 +110,6 @@ export const API = { contextBridge.exposeInMainWorld('api', API); contextBridge.exposeInMainWorld('stubDataStorage', stubDataStorage); +contextBridge.exposeInMainWorld('env', { + E2E_TEST: process.env.E2E_TEST, +}); diff --git a/frontend/src/renderer/components/configuration/ConfigCreateModal.tsx b/frontend/src/renderer/components/configuration/ConfigCreateModal.tsx index 8bd85a40..728a2fac 100644 --- a/frontend/src/renderer/components/configuration/ConfigCreateModal.tsx +++ b/frontend/src/renderer/components/configuration/ConfigCreateModal.tsx @@ -212,6 +212,9 @@ export function ConfigCreateModal({ title="Create config" size="sm" data-testid="config-create-modal" + {...(window.env.E2E_TEST === 'true' && { + transitionProps: { duration: 0 }, + })} >
diff --git a/frontend/src/renderer/components/confirmation/ConfirmModal.tsx b/frontend/src/renderer/components/confirmation/ConfirmModal.tsx index 7e55647d..307622d0 100644 --- a/frontend/src/renderer/components/confirmation/ConfirmModal.tsx +++ b/frontend/src/renderer/components/confirmation/ConfirmModal.tsx @@ -19,8 +19,10 @@ export function ConfirmModal({ isOpen, onClose, onConfirm, children }: Props) { data-testid="confirm-modal" transitionProps={{ transition: 'fade', - duration: 0, }} + {...(window.env.E2E_TEST === 'true' && { + transitionProps: { duration: 0 }, + })} > { diff --git a/frontend/src/renderer/components/grid/HoverButtons.tsx b/frontend/src/renderer/components/grid/HoverButtons.tsx index 20c72643..bd592ece 100644 --- a/frontend/src/renderer/components/grid/HoverButtons.tsx +++ b/frontend/src/renderer/components/grid/HoverButtons.tsx @@ -249,6 +249,7 @@ export const HoverButtons = React.memo( handleCustomization(data.i)} className={classes.actionButton} > diff --git a/frontend/src/renderer/components/plot/SimplePlotly.tsx b/frontend/src/renderer/components/plot/SimplePlotly.tsx index e66e9051..1a862b33 100644 --- a/frontend/src/renderer/components/plot/SimplePlotly.tsx +++ b/frontend/src/renderer/components/plot/SimplePlotly.tsx @@ -53,8 +53,10 @@ export const SimplePlotly = ({ zeroline: false, type: (itemDataGrid?.xAxisData?.type as AxisType) || - typeof itemDataGrid.plot[0].x[0] === 'string' - ? 'category' + (itemDataGrid.plot.length > 0 && itemDataGrid.plot[0].x?.length > 0) + ? typeof itemDataGrid.plot[0]?.x[0] === 'string' + ? 'category' + : 'linear' : 'linear', exponentformat: 'power', showexponent: 'all', diff --git a/frontend/src/renderer/components/plot/hooks/usePlotLayout.ts b/frontend/src/renderer/components/plot/hooks/usePlotLayout.ts index a3e86f5d..d893f884 100644 --- a/frontend/src/renderer/components/plot/hooks/usePlotLayout.ts +++ b/frontend/src/renderer/components/plot/hooks/usePlotLayout.ts @@ -65,7 +65,9 @@ export function usePlotLayout({ }, [itemDataGrid.y2AxisData?.type, setLayoutPlot]); useEffect(() => { - const newTypeOfX = typeof itemDataGrid.plot[0].x[0]; + const newTypeOfX = itemDataGrid.plot[0]?.x + ? typeof itemDataGrid.plot[0]?.x[0] + : undefined; if (newTypeOfX === 'string') { // Update x axis to category type if it become a string setLayoutPlot((prevLayout) => ({ diff --git a/frontend/src/renderer/components/preferences/PreferenceModal.tsx b/frontend/src/renderer/components/preferences/PreferenceModal.tsx index 31e7654f..379afa51 100644 --- a/frontend/src/renderer/components/preferences/PreferenceModal.tsx +++ b/frontend/src/renderer/components/preferences/PreferenceModal.tsx @@ -24,8 +24,10 @@ export function PreferenceModal({ centered transitionProps={{ transition: 'fade', - duration: 0, }} + {...(window.env.E2E_TEST === 'true' && { + transitionProps: { duration: 0 }, + })} > {/* Main content */} diff --git a/frontend/src/renderer/components/tabs/TabsListCustom.tsx b/frontend/src/renderer/components/tabs/TabsListCustom.tsx index 9b1af74e..0e8565de 100644 --- a/frontend/src/renderer/components/tabs/TabsListCustom.tsx +++ b/frontend/src/renderer/components/tabs/TabsListCustom.tsx @@ -52,6 +52,7 @@ export const TabsListCustom = ({ saveAndClose()} > diff --git a/frontend/src/renderer/components/tree/TreeLibrariesAccordion.tsx b/frontend/src/renderer/components/tree/TreeLibrariesAccordion.tsx index e347b0e1..3994790b 100644 --- a/frontend/src/renderer/components/tree/TreeLibrariesAccordion.tsx +++ b/frontend/src/renderer/components/tree/TreeLibrariesAccordion.tsx @@ -93,7 +93,11 @@ export const TreeLibrariesAccordion = ({ return ( - + {items} diff --git a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx index b530030a..fd414a2a 100644 --- a/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx +++ b/frontend/src/renderer/pages/visualization/DataplotCustomization.tsx @@ -166,7 +166,11 @@ const Customization = ({ opened={item?.disabled ? null : false} > - + {item.value} {item.component} @@ -180,7 +184,11 @@ const Customization = ({ Customize plot parameters - + {items} diff --git a/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx b/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx index 62101d3e..31aeaf57 100644 --- a/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx +++ b/frontend/src/renderer/pages/visualization/VisualizationMetaData.tsx @@ -125,7 +125,11 @@ const RenderMetaDataCoordinates = ({ {coordinates.length === 0 ? ( 'N/A' ) : ( - + {coordinates.map((coordinate, index) => ( {coordinate.name} diff --git a/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx b/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx index 22d316e0..f4618cea 100644 --- a/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx +++ b/frontend/src/renderer/pages/visualization/VisualizationURIModal.tsx @@ -547,6 +547,9 @@ export const VisualizationURIModal = ({ size="90%" centered data-testid="config-uri-selection-modal" + {...(window.env.E2E_TEST === 'true' && { + transitionProps: { duration: 0 }, + })} > setTypedMinRange(value)} w={150} hideControls + data-testid={ + coordinate.axeIndex === 0 ? 'data-range-min-input' : undefined + } /> setTypedMaxRange(value)} w={150} hideControls + data-testid={ + coordinate.axeIndex === 0 ? 'data-range-max-input' : undefined + } /> ) : ( @@ -354,6 +360,9 @@ export const CustomizeDataRange = ({ } loading={isLoadingApply} leftSection={} + data-testid={ + coordinate.axeIndex === 0 ? 'data-range-apply-input' : undefined + } > Apply @@ -363,6 +372,9 @@ export const CustomizeDataRange = ({ loading={isLoadingRestore} variant="outline" leftSection={} + data-testid={ + coordinate.axeIndex === 0 ? 'data-range-restore-input' : undefined + } > Restore diff --git a/frontend/src/renderer/utils/plot.ts b/frontend/src/renderer/utils/plot.ts index 98a6dc1b..bade634c 100644 --- a/frontend/src/renderer/utils/plot.ts +++ b/frontend/src/renderer/utils/plot.ts @@ -636,7 +636,15 @@ export const fetchErrorBands = async ( // Return dataPlot list with the plot which includes error bands return dataPlot; } catch (error) { - console.error('Error handling error bands: ', error); + if ( + !( + error.toString().includes('No data for') && + (error.toString().includes('_error_upper') || + error.toString().includes('_error_lower')) + ) + ) { + console.error('Error handling error bands: ', error); + } } }; diff --git a/frontend/src/tests/plot-ui.spec.ts b/frontend/src/tests/plot-ui.spec.ts index 2c1e7d67..f6186fba 100644 --- a/frontend/src/tests/plot-ui.spec.ts +++ b/frontend/src/tests/plot-ui.spec.ts @@ -11,17 +11,19 @@ import { ensureCssElementIsDisplayed, findCssElementAndClickIt, findTextElementAndClickIt, + getCssElementFromDataTestId, waitForElementToDisappear, waitForValue, writeTextInCssElement, } from './utils'; -import { expect } from 'chai'; +// import { expect } from 'chai'; +import '../config/bridge'; /** * UI Test Suite for the Visualization Component */ -describe('UI Tests for Header Component', function () { - this.timeout(60000); +describe('UI Tests for plotted data', function () { + this.timeout(90000); before(async () => { await startApp(); @@ -75,41 +77,45 @@ describe('UI Tests for Header Component', function () { ); /// - /// Add the URI 'imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3' to the configuration and navigate in the accordion node tree + /// Add the URI of iter_scenario_53298_seq1_DD3.nc to the configuration and navigate in the accordion node tree /// + const dataPath: string = await ( + await getDriver() + ).executeScript(async () => { + const path = + (await window.api.fs.getZenodoDataPath()) + + '/iter_scenario_53298_seq1_DD3.nc'; + return path; + }); await ensureCssElementIsDisplayed('config-uri-selection-modal'); await writeTextInCssElement( 'config-uri-selection-modal-uri-text-input', - 'imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3', + dataPath, + ); + const buttonSavingUris = await ensureCssElementIsDisplayed( + 'config-uri-selection-modal-add-uri-button', ); await findCssElementAndClickIt('config-uri-selection-modal-add-uri-button'); + await waitForElementToDisappear(buttonSavingUris); + await findCssElementAndClickIt( 'config-uri-selection-modal-validate-button', 100, 300, ); - await ensureCssElementIsDisplayed( - 'uriAccordion-imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3', - 200, - 100, - ); + await findCssElementAndClickIt(`uriAccordion-${dataPath}`, 200, 100); await findCssElementAndClickIt( - 'uriAccordion-imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3', + `folder-${dataPath}#core_profiles:0/`, 200, 100, ); await findCssElementAndClickIt( - 'folder-imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3#core_profiles:0/', + `folder-${dataPath}#core_profiles:0/profiles_1d[:]/`, 200, 100, ); await findCssElementAndClickIt( - 'folder-imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3#core_profiles:0/profiles_1d[:]/', - 200, - 100, - ); - await findCssElementAndClickIt( - 'folder-imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3#core_profiles:0/profiles_1d[:]/ion[:]/', + `folder-${dataPath}#core_profiles:0/profiles_1d[:]/ion[:]/`, 200, 100, ); @@ -124,7 +130,7 @@ describe('UI Tests for Header Component', function () { ); // Click on temperature checkbox to start a new plot await findCssElementAndClickIt( - 'checkbox-imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3#core_profiles:0/profiles_1d[:]/ion[:]/temperature', + `checkbox-${dataPath}#core_profiles:0/profiles_1d[:]/ion[:]/temperature`, ); // Check that there is one dataplot created await waitForValue( @@ -148,7 +154,7 @@ describe('UI Tests for Header Component', function () { ); // Click on density checkbox to plot a second axis await findCssElementAndClickIt( - 'checkbox-imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3#core_profiles:0/profiles_1d[:]/ion[:]/density', + `checkbox-${dataPath}#core_profiles:0/profiles_1d[:]/ion[:]/density`, ); // Check that the Y plot is defined await waitForValue( @@ -166,6 +172,306 @@ describe('UI Tests for Header Component', function () { ); }); + it('Should crop the data in all plots by applying a data range', async () => { + /// + /// Create a new configuration named 'New Plot Config' + /// + await findCssElementAndClickIt('header-add-configuration'); + const configCreateModal = await ensureCssElementIsDisplayed( + 'config-create-modal', + ); + await writeTextInCssElement('config-create-name-input', 'New Plot Config'); + await findCssElementAndClickIt('config-create-submit-button'); + await waitForElementToDisappear(configCreateModal); + await waitForValue( + 'Plot configuration length', + async () => (await getTestState()).configurations.length, + 1, + ); + await waitForValue( + 'Plot configuration name', + async () => (await getTestState()).configurations[0].name, + 'New Plot Config', + ); + + /// + /// Add the URI of iter_scenario_53298_seq1_DD3.nc to the configuration and navigate in the accordion node tree + /// + const dataPath: string = await ( + await getDriver() + ).executeScript(async () => { + const path = + (await window.api.fs.getZenodoDataPath()) + + '/iter_scenario_53298_seq1_DD3.nc'; + return path; + }); + await ensureCssElementIsDisplayed('config-uri-selection-modal'); + await writeTextInCssElement( + 'config-uri-selection-modal-uri-text-input', + dataPath, + true, + ); + await findCssElementAndClickIt('config-uri-selection-modal-add-uri-button'); + await findCssElementAndClickIt( + 'config-uri-selection-modal-validate-button', + 100, + 300, + ); + await ensureCssElementIsDisplayed(`uriAccordion-${dataPath}`, 200, 100); + await findCssElementAndClickIt(`uriAccordion-${dataPath}`, 200, 100); + await findCssElementAndClickIt( + `folder-${dataPath}#core_profiles:0/`, + 200, + 100, + ); + await findCssElementAndClickIt( + `folder-${dataPath}#core_profiles:0/profiles_1d[:]/`, + 200, + 100, + ); + + /// + /// The accordion node tree is now unfold, check that the plot are correctly added into the active configuration + /// + await waitForValue( + 'DataPlot configuration length', + async () => (await getTestState()).active.dataPlot.length, + 0, + ); + // Click on j_total checkbox to start a new plot + await findCssElementAndClickIt( + `checkbox-${dataPath}#core_profiles:0/profiles_1d[:]/j_total`, + ); + // Check that there is one dataplot created + await waitForValue( + 'DataPlot configuration length', + async () => (await getTestState()).active.dataPlot.length, + 1, + ); + // Click on j_ohmic checkbox to plot a second data + await findCssElementAndClickIt( + `checkbox-${dataPath}#core_profiles:0/profiles_1d[:]/j_ohmic`, + ); + // Check that there is two dataplot created + await waitForValue( + 'Plot configuration length', + async () => (await getTestState()).active.dataPlot[0].plot.length, + 2, + ); + // Now we check integrity of data manipulation + // Step 1 - The minimum and maximum original data for the two plots + const originalDataFromActive = (await getTestState()).active; + await waitForValue( + 'First y value of j_total at origin', + async () => originalDataFromActive.dataPlot[0]?.plot[0]?.y[0], + -1018173.9490004762, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'Last y value of j_total at origin', + async () => + originalDataFromActive.dataPlot[0]?.plot[0]?.y[ + originalDataFromActive.dataPlot[0]?.plot[0]?.y.length - 1 + ], + -379752.40595339175, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'First y value of j_ohmic at origin', + async () => originalDataFromActive.dataPlot[0]?.plot[1]?.y[0], + -942579.3029552045, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'Last y value of j_ohmic at origin', + async () => + originalDataFromActive.dataPlot[0]?.plot[1]?.y[ + originalDataFromActive.dataPlot[0]?.plot[1]?.y.length - 1 + ], + -34126.34158638138, + (actual, expected) => actual === expected, + ); + + // Step 2 - Enter the data range, apply a range, confirm the change, and check the values + await findCssElementAndClickIt('customization-access-button'); + await findCssElementAndClickIt('customization-Axis range-accordion'); + await writeTextInCssElement('data-range-min-input', '0.2', true); + await writeTextInCssElement('data-range-max-input', '0.8', true); + await findCssElementAndClickIt('data-range-apply-input'); + await new Promise((r) => setTimeout(r, 1500)); + await findCssElementAndClickIt('customization-save-button'); + await new Promise((r) => setTimeout(r, 1500)); + const activeAtFirstApplied = (await getTestState()).active; + await waitForValue( + 'First y value of j_total at first applied', + async () => activeAtFirstApplied.dataPlot[0]?.plot[0]?.y[0], + -1409056.125, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'Last y value of j_total at first applied', + async () => + activeAtFirstApplied.dataPlot[0]?.plot[0]?.y[ + activeAtFirstApplied.dataPlot[0]?.plot[0]?.y.length - 1 + ], + -402749.40625, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'First y value of j_ohmic at first applied', + async () => activeAtFirstApplied.dataPlot[0]?.plot[1]?.y[0], + -1058855.625, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'Last y value of j_ohmic at first applied', + async () => + activeAtFirstApplied.dataPlot[0]?.plot[1]?.y[ + activeAtFirstApplied.dataPlot[0]?.plot[1]?.y.length - 1 + ], + -274728.34375, + (actual, expected) => actual === expected, + ); + + // Step 3 - Enter the data range, apply a second range, confirm the change, and check the values + await findCssElementAndClickIt('customization-access-button'); + await findCssElementAndClickIt('customization-Axis range-accordion'); + await writeTextInCssElement('data-range-min-input', '0.4', true); + await writeTextInCssElement('data-range-max-input', '0.6', true); + await findCssElementAndClickIt('data-range-apply-input'); + await new Promise((r) => setTimeout(r, 1500)); + await findCssElementAndClickIt('customization-save-button'); + await new Promise((r) => setTimeout(r, 1500)); + const activeAtSecondApplied = (await getTestState()).active; + await waitForValue( + 'First y value of j_total at second applied', + async () => activeAtSecondApplied.dataPlot[0]?.plot[0]?.y[0], + -1159740.25, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'Last y value of j_total at second applied', + async () => + activeAtSecondApplied.dataPlot[0]?.plot[0]?.y[ + activeAtSecondApplied.dataPlot[0]?.plot[0]?.y.length - 1 + ], + -697106.25, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'First y value of j_ohmic at second applied', + async () => activeAtSecondApplied.dataPlot[0]?.plot[1]?.y[0], + -910444.625, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'Last y value of j_ohmic at second applied', + async () => + activeAtSecondApplied.dataPlot[0]?.plot[1]?.y[ + activeAtSecondApplied.dataPlot[0]?.plot[1]?.y.length - 1 + ], + -506609.75, + (actual, expected) => actual === expected, + ); + + // Step 4 - Enter the data range, apply a third, wider range, confirm the change, and check the values + await findCssElementAndClickIt('customization-access-button'); + await findCssElementAndClickIt('customization-Axis range-accordion'); + await writeTextInCssElement('data-range-min-input', '0.2', true); + await writeTextInCssElement('data-range-max-input', '0.8', true); + await findCssElementAndClickIt('data-range-apply-input'); + await waitForValue( + 'Restore button finished loading', + async () => + ( + await getCssElementFromDataTestId('data-range-apply-input') + ).getAttribute('data-loading'), + null, + (actual, expected) => actual === expected, + ); + await findCssElementAndClickIt('customization-save-button'); + await new Promise((r) => setTimeout(r, 1500)); + const activeAtThirdApplied = (await getTestState()).active; + await waitForValue( + 'First y value of j_total at third applied', + async () => activeAtThirdApplied.dataPlot[0]?.plot[0]?.y[0], + -1409056.125, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'Last y value of j_total at third applied', + async () => + activeAtThirdApplied.dataPlot[0]?.plot[0]?.y[ + activeAtThirdApplied.dataPlot[0]?.plot[0]?.y.length - 1 + ], + -402749.40625, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'First y value of j_ohmic at third applied', + async () => activeAtThirdApplied.dataPlot[0]?.plot[1]?.y[0], + -1058855.625, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'Last y value of j_ohmic at third applied', + async () => + activeAtThirdApplied.dataPlot[0]?.plot[1]?.y[ + activeAtThirdApplied.dataPlot[0]?.plot[1]?.y.length - 1 + ], + -274728.34375, + (actual, expected) => actual === expected, + ); + + // Step 5 - Enter the data range, restore the range, confirm the change, and check if the values have returned to their original state + await findCssElementAndClickIt('customization-access-button'); + await findCssElementAndClickIt('customization-Axis range-accordion'); + await findCssElementAndClickIt('data-range-restore-input'); + await waitForValue( + 'Restore button finished loading', + async () => + ( + await getCssElementFromDataTestId('data-range-restore-input') + ).getAttribute('data-loading'), + null, + (actual, expected) => actual === expected, + 10, + ); + await findCssElementAndClickIt('customization-save-button'); + await new Promise((r) => setTimeout(r, 1500)); + const restoredDataFromActive = (await getTestState()).active; + await waitForValue( + 'First y value of j_total at restoration', + async () => restoredDataFromActive.dataPlot[0]?.plot[0]?.y[0], + -1018173.9375, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'Last y value of j_total at restoration', + async () => + restoredDataFromActive.dataPlot[0]?.plot[0]?.y[ + restoredDataFromActive.dataPlot[0]?.plot[0]?.y.length - 1 + ], + -379752.40625, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'First y value of j_ohmic at restoration', + async () => restoredDataFromActive.dataPlot[0]?.plot[1]?.y[0], + -942579.3125, + (actual, expected) => actual === expected, + ); + await waitForValue( + 'Last y value of j_ohmic at restoration', + async () => + restoredDataFromActive.dataPlot[0]?.plot[1]?.y[ + restoredDataFromActive.dataPlot[0]?.plot[1]?.y.length - 1 + ], + -34126.33984375, + (actual, expected) => actual === expected, + ); + }); + it('Should create a new configuration that follows a predefined template', async () => { /// /// Create a new configuration named 'New Plot Config' based on the 'PlotKineticProfilesIbexState.json' template @@ -195,14 +501,22 @@ describe('UI Tests for Header Component', function () { ); /// - /// Add the URI 'imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3' to the configuration and navigate in the accordion node tree + /// Add the URI of iter_scenario_53298_seq1_DD3.nc to the configuration and navigate in the accordion node tree /// + const dataPath: string = await ( + await getDriver() + ).executeScript(async () => { + const path = + (await window.api.fs.getZenodoDataPath()) + + '/iter_scenario_53298_seq1_DD3.nc'; + return path; + }); const uriModal = await ensureCssElementIsDisplayed( 'config-uri-selection-modal', ); await writeTextInCssElement( 'config-uri-selection-modal-uri-text-input', - 'imas:hdf5?user=public;pulse=100002;run=1;database=iterdb;version=3', + dataPath, true, ); await findCssElementAndClickIt('config-uri-selection-modal-add-uri-button'); @@ -289,6 +603,7 @@ describe('UI Tests for Header Component', function () { ); }); + /* it('Should create a new configuration containing error bands, and plot error band data', async () => { /// /// Create a new configuration named 'New Plot Config' @@ -318,6 +633,7 @@ describe('UI Tests for Header Component', function () { 'config-uri-selection-modal', ); await writeTextInCssElement( + // TODO : use dataPath 'config-uri-selection-modal-uri-text-input', 'imas:hdf5?user=imbeauf;pulse=58089;run=4;database=west;version=3', true, @@ -403,4 +719,5 @@ describe('UI Tests for Header Component', function () { expect(plotWithErrorBand.arrayminus.length > 0); } }); + */ }); diff --git a/frontend/src/tests/setup.ts b/frontend/src/tests/setup.ts index 9f8b76db..3cd687aa 100644 --- a/frontend/src/tests/setup.ts +++ b/frontend/src/tests/setup.ts @@ -26,7 +26,7 @@ export async function startApp() { ...process.env, ELECTRON_ENABLE_LOGGING: 'true', ELECTRON_ENABLE_STACK_DUMPING: 'true', - // E2E_TEST: 'true', + E2E_TEST: process.env.E2E_TEST, }, }, ); diff --git a/frontend/src/tests/utils/testToolBox.ts b/frontend/src/tests/utils/testToolBox.ts index fbdacdc4..b75c86d2 100644 --- a/frontend/src/tests/utils/testToolBox.ts +++ b/frontend/src/tests/utils/testToolBox.ts @@ -9,14 +9,9 @@ export async function getCssElementFromDataTestId( const cssElement = await getDriver().wait( until.elementLocated(By.css(`[data-testid="${cssElementDataTestIdName}"]`)), timeout, + `Element "${cssElementDataTestIdName}" not found`, ); - - if (!cssElement) { - throw new Error( - `No CSS element found under the name ${cssElementDataTestIdName}, abort`, - ); - } - + await getDriver().wait(until.elementIsVisible(cssElement), timeout); return cssElement; } @@ -62,18 +57,12 @@ export async function ensureCssElementIsDisplayed( delayMs = 100, ): Promise { console.info('Ensuring element is displayed : ', cssElementDataTestIdName); - const cssElement = await getCssElementFromDataTestId( + const cssElement: WebElement = await getCssElementFromDataTestId( cssElementDataTestIdName, retries * delayMs, ); - for (let i = 0; i < retries; i++) { - await new Promise((res) => setTimeout(res, delayMs)); - - if (await cssElement.isDisplayed()) { - break; - } - } + await getDriver().wait(until.elementIsVisible(cssElement), retries * delayMs); expect( await cssElement.isDisplayed(), @@ -91,7 +80,7 @@ export async function writeTextInCssElement( if (clearText) { if ( (await input.getAttribute('value')) != undefined && - (await input.getAttribute('value')).length > 0 + (await input.getAttribute('value'))?.length > 0 ) { while ((await input.getAttribute('value')).length > 0) { await input.sendKeys(Key.BACK_SPACE); @@ -106,23 +95,16 @@ export async function findCssElementAndClickIt( retries = 100, delayMs = 100, ) { - const button = await ensureCssElementIsDisplayed( + const button = await getCssElementFromDataTestId( cssElementDataTestIdName, - retries, - delayMs, + retries * delayMs, ); - for (let i = 0; i < retries; i++) { - await new Promise((res) => setTimeout(res, delayMs)); - - if (await button.isEnabled()) { - break; - } - } + await getDriver().wait(until.elementIsEnabled(button), retries * delayMs); expect( - await button.isEnabled(), - `Button "${cssElementDataTestIdName}" was found but isEnabled() returned false.`, + await button.isDisplayed(), + `Button "${cssElementDataTestIdName}" was found but isDisplayed() returned false.`, ).to.be.true; await button.click(); } diff --git a/frontend/src/tests/visualization-ui.spec.ts b/frontend/src/tests/visualization-ui.spec.ts index a78ee28b..458bb794 100644 --- a/frontend/src/tests/visualization-ui.spec.ts +++ b/frontend/src/tests/visualization-ui.spec.ts @@ -26,7 +26,7 @@ describe('UI Tests for Visualization Component', function () { afterEach(async () => { await setTestState({ configurations: [], active: null }); - // Ferme les modales restantes si besoin + // Close any remaining modals if necessary try { const modalOverlay = await driver.findElement( By.css('.mantine-Modal-overlay'), @@ -38,11 +38,11 @@ describe('UI Tests for Visualization Component', function () { ); await closeButton.click(); - // Attends la disparition de l'overlay + // Wait until the overlay disappears await driver.wait(until.stalenessOf(modalOverlay), 3000); } } catch { - // Ignore si la modale n'existe pas + // Ignore if the modal does not exist } }); diff --git a/zenodo_datasets.txt b/zenodo_datasets.txt new file mode 100644 index 00000000..fe977fe8 --- /dev/null +++ b/zenodo_datasets.txt @@ -0,0 +1 @@ +https://zenodo.org/records/17062700/files/iter_scenario_53298_seq1_DD3.nc \ No newline at end of file