diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 9f87bb53..6b8c540e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -9,7 +9,26 @@ on:
- cron: "0 2 * * *"
jobs:
+ format-tests:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci --legacy-peer-deps
+
+ - name: Check frontend test formatting
+ run: npm run format:tests:check
+
build-and-unit:
+ needs: format-tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 00000000..4f310576
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "endOfLine": "lf",
+ "tabWidth": 2,
+ "printWidth": 120,
+ "semi": true
+}
diff --git a/package-lock.json b/package-lock.json
index 47c8b384..b78e8ccb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -70,6 +70,7 @@
"@vue/test-utils": "^2.4.6",
"jsdom": "^29.0.0",
"otpauth": "^9.5.0",
+ "prettier": "2.8.8",
"sass-embedded": "^1.81.0",
"vite": "7.1.11",
"vite-plugin-pwa": "^1.0.2",
@@ -228,8 +229,8 @@
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -259,6 +260,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -554,6 +556,7 @@
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
@@ -1962,7 +1965,7 @@
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
- "devOptional": true,
+ "dev": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@creativebulma/bulma-badge": {
@@ -2066,7 +2069,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -2115,7 +2117,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -2127,6 +2128,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2143,6 +2145,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2159,6 +2162,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2175,6 +2179,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2191,6 +2196,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2207,6 +2213,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2223,6 +2230,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2239,6 +2247,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2255,6 +2264,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2271,6 +2281,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2287,6 +2298,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2303,6 +2315,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2319,6 +2332,7 @@
"cpu": [
"mips64el"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2335,6 +2349,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2351,6 +2366,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2367,6 +2383,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2383,6 +2400,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2399,6 +2417,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2415,6 +2434,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2431,6 +2451,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2447,6 +2468,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2463,6 +2485,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2479,6 +2502,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2495,6 +2519,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2511,6 +2536,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2553,7 +2579,6 @@
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz",
"integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"preact": "~10.12.1"
}
@@ -2797,7 +2822,6 @@
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz",
"integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jimp/core": "^0.22.12"
}
@@ -2834,7 +2858,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz",
"integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -2847,7 +2870,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz",
"integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -2872,7 +2894,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz",
"integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12",
"tinycolor2": "^1.6.0"
@@ -2916,7 +2937,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz",
"integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3040,7 +3060,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz",
"integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3053,7 +3072,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz",
"integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3069,7 +3087,6 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz",
"integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3204,6 +3221,7 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -3223,7 +3241,7 @@
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -3932,6 +3950,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3945,6 +3964,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3958,6 +3978,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3971,6 +3992,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3984,6 +4006,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3997,6 +4020,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4010,6 +4034,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4023,6 +4048,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4036,6 +4062,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4049,6 +4076,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4062,6 +4090,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4075,6 +4104,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4088,6 +4118,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4101,6 +4132,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4114,6 +4146,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4127,6 +4160,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4140,6 +4174,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4153,6 +4188,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4166,6 +4202,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4179,6 +4216,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4192,6 +4230,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4205,6 +4244,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4218,6 +4258,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4231,6 +4272,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4359,6 +4401,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/event-calendar__core": {
@@ -5330,9 +5373,8 @@
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5367,7 +5409,6 @@
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -5461,8 +5502,7 @@
"version": "5.10.4",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.10.4.tgz",
"integrity": "sha512-gt0VUqZ2+mr25ScbUcKZgJr96jKYm4vjOcxEWCEh/E5F4dWqhyo3dBhPRvNNnkKiWxkMd2cBwj3ZYH3rK39fkA==",
- "license": "SEE LICENSE IN LICENSE",
- "peer": true
+ "license": "SEE LICENSE IN LICENSE"
},
"node_modules/aria-query": {
"version": "5.3.1",
@@ -5586,7 +5626,6 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
@@ -5801,7 +5840,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5886,8 +5924,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
"integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/bulma-block-list": {
"version": "1.1.0",
@@ -6145,7 +6182,6 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -6334,7 +6370,7 @@
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/colors": {
@@ -6408,6 +6444,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
"license": "MIT"
},
"node_modules/copy-anything": {
@@ -6559,7 +6596,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
- "peer": true,
"engines": {
"node": ">=12"
}
@@ -6748,7 +6784,6 @@
"integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
"deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.",
"license": "MIT",
- "peer": true,
"dependencies": {
"@vue/compiler-sfc": "2.7.16",
"csstype": "^3.1.0"
@@ -7186,6 +7221,7 @@
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -7230,6 +7266,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7507,6 +7544,7 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -7761,6 +7799,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -7854,6 +7893,7 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -8277,8 +8317,7 @@
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
"integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==",
- "license": "Apache-2.0",
- "peer": true
+ "license": "Apache-2.0"
},
"node_modules/ieee754": {
"version": "1.2.1",
@@ -9183,6 +9222,7 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@@ -9543,16 +9583,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/moment": {
- "version": "2.30.1",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
- "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": "*"
- }
- },
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -10117,6 +10147,7 @@
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -10250,8 +10281,8 @@
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "devOptional": true,
"license": "MIT",
- "optional": true,
"bin": {
"prettier": "bin-prettier.js"
},
@@ -10345,7 +10376,6 @@
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
@@ -10699,8 +10729,8 @@
"version": "4.60.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
"integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -10747,6 +10777,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -10884,7 +10915,6 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz",
"integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.1.5",
@@ -10904,9 +10934,8 @@
"version": "1.98.0",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.98.0.tgz",
"integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@bufbuild/protobuf": "^2.5.0",
"colorjs.io": "^0.5.0",
@@ -11253,7 +11282,7 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -11546,7 +11575,7 @@
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
@@ -11557,7 +11586,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -11967,7 +11996,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"sync-message-port": "^1.0.0"
@@ -11980,7 +12009,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz",
"integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=16.0.0"
@@ -12077,7 +12106,7 @@
"version": "5.46.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
"integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
- "devOptional": true,
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@@ -12096,7 +12125,7 @@
"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==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/tesseract.js": {
@@ -12188,6 +12217,7 @@
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -12646,15 +12676,15 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "7.1.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -13476,6 +13506,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -14047,7 +14078,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -14107,7 +14137,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz",
"integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.31",
"@vue/compiler-sfc": "3.5.31",
@@ -14795,7 +14824,6 @@
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
diff --git a/package.json b/package.json
index 6a25f97e..b21525b4 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,8 @@
"dev": "vite",
"build": "vite build",
"build:dev": "vite build --mode development",
+ "format:tests": "prettier --write \"tests/**/*.{js,ts}\"",
+ "format:tests:check": "prettier --check \"tests/**/*.{js,ts}\"",
"preview": "vite preview",
"preview:prod": "npm run build && npm run preview -- --host 127.0.0.1 --port 4173",
"test:unit": "vitest run",
@@ -84,6 +86,7 @@
"@vue/test-utils": "^2.4.6",
"jsdom": "^29.0.0",
"otpauth": "^9.5.0",
+ "prettier": "2.8.8",
"sass-embedded": "^1.81.0",
"vite": "7.1.11",
"vite-plugin-pwa": "^1.0.2",
diff --git a/src/components/displays/department/pos/PosOrderItemsCurrent.vue b/src/components/displays/department/pos/PosOrderItemsCurrent.vue
index a39750c4..b223c0e6 100644
--- a/src/components/displays/department/pos/PosOrderItemsCurrent.vue
+++ b/src/components/displays/department/pos/PosOrderItemsCurrent.vue
@@ -12,6 +12,7 @@ const props = defineProps({
loadOrderItems: Function,
editPossible: Boolean,
isLarge: Boolean,
+ stackMetadata: Boolean,
reg_1: String,
reg_2: String,
reg_3: String,
@@ -58,6 +59,7 @@ const rootClasses = computed(() => ({
'pos-order-items--large': isLarge,
'pos-order-items--order-detail': isOrderDetailVariant.value,
'pos-order-items--sidebar': isSidebarVariant.value,
+ 'pos-order-items--stacked-metadata': props.stackMetadata,
}));
const titleClasses = computed(() => ({
title: true,
@@ -367,7 +369,7 @@ const deleteOrderItem = async (orderItemId) => {
height="1.5rem"
skeleton-class="is-size-4"
>
- {{ displayCustomerName }}
+ {{ displayCustomerName }}
{
margin: 0;
}
+.pos-order-items--order-detail.pos-order-items--stacked-metadata .pos-order-metadata-grid {
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-areas:
+ "license-plates"
+ "reference"
+ "note";
+}
+
.pos-order-items__footer {
padding: 0 1.25rem 0.4rem;
}
diff --git a/src/components/displays/department/pos/order/PosDesktopOrderWorkspace.vue b/src/components/displays/department/pos/order/PosDesktopOrderWorkspace.vue
index 704d2da3..4be22cb0 100644
--- a/src/components/displays/department/pos/order/PosDesktopOrderWorkspace.vue
+++ b/src/components/displays/department/pos/order/PosDesktopOrderWorkspace.vue
@@ -158,6 +158,7 @@ watch(normalizedOrderId, async (nextOrderId, previousOrderId) => {
:orderId="orderId"
:loadOrderItems="loadOrderItems"
:editPossible="editPossible"
+ :stackMetadata="showCatalogPane"
variant="order-detail"
>
@@ -213,6 +214,14 @@ watch(normalizedOrderId, async (nextOrderId, previousOrderId) => {
+
+
@@ -258,17 +267,29 @@ watch(normalizedOrderId, async (nextOrderId, previousOrderId) => {
width: 100%;
}
+.pos-order-detail__order-footer {
+ min-width: 0;
+}
+
.pos-order-detail--editing {
grid-template-columns: minmax(0, 1fr);
}
.pos-order-detail__workspace--editing {
- grid-template-columns: minmax(24rem, 0.95fr) minmax(32rem, 1.15fr);
+ grid-template-columns: minmax(0, 1fr) minmax(20rem, 23.75rem);
+}
+
+.pos-order-detail__order-footer--editing {
+ grid-column: 2;
}
@media screen and (max-width: 1023px) {
.pos-order-detail__workspace--editing {
grid-template-columns: 1fr;
}
+
+ .pos-order-detail__order-footer--editing {
+ grid-column: auto;
+ }
}
diff --git a/src/components/displays/department/pos/steps/PosDepartmentStep2.vue b/src/components/displays/department/pos/steps/PosDepartmentStep2.vue
index eddba2d5..32e34f87 100644
--- a/src/components/displays/department/pos/steps/PosDepartmentStep2.vue
+++ b/src/components/displays/department/pos/steps/PosDepartmentStep2.vue
@@ -29,12 +29,14 @@ setProductsCategory(null);
:showCatalogPane="true"
:showCustomerRail="false"
>
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -46,5 +48,9 @@ setProductsCategory(null);
diff --git a/src/components/displays/edgeGateway/EdgeGatewayTerminal.vue b/src/components/displays/edgeGateway/EdgeGatewayTerminal.vue
index 2ba249d1..3d0e8f01 100644
--- a/src/components/displays/edgeGateway/EdgeGatewayTerminal.vue
+++ b/src/components/displays/edgeGateway/EdgeGatewayTerminal.vue
@@ -3,7 +3,13 @@ import { computed, nextTick, ref, watch } from "vue";
import { Terminal } from "xterm";
import { FitAddon } from "@xterm/addon-fit";
-import { createEdgeGatewayShellSession } from "@/services/edgeGateways.js";
+import {
+ closeEdgeGatewayShellSession,
+ createEdgeGatewayShellSession,
+ pollEdgeGatewayShellSession,
+ sendEdgeGatewayShellInput,
+ sendEdgeGatewayShellResize,
+} from "@/services/edgeGateways.js";
import {
EDGE_GATEWAY_TERMINAL_STATES,
useEdgeGatewayTerminal,
@@ -22,9 +28,21 @@ const props = defineProps({
type: Function,
default: createEdgeGatewayShellSession,
},
- createSocket: {
+ pollShellSessionEvents: {
type: Function,
- default: (url) => new WebSocket(url),
+ default: pollEdgeGatewayShellSession,
+ },
+ sendShellInput: {
+ type: Function,
+ default: sendEdgeGatewayShellInput,
+ },
+ sendShellResize: {
+ type: Function,
+ default: sendEdgeGatewayShellResize,
+ },
+ closeShellSession: {
+ type: Function,
+ default: closeEdgeGatewayShellSession,
},
createTerminal: {
type: Function,
@@ -47,7 +65,10 @@ const shellReason = ref("");
const terminalHost = ref(null);
const terminalState = useEdgeGatewayTerminal({
- createSocket: props.createSocket,
+ pollSessionEvents: props.pollShellSessionEvents,
+ sendSessionInput: props.sendShellInput,
+ sendSessionResize: props.sendShellResize,
+ closeSession: props.closeShellSession,
createTerminal: props.createTerminal,
createFitAddon: props.createFitAddon,
copyText: props.copyText,
diff --git a/src/components/displays/edgeGateway/EdgeGatewayWorkspace.vue b/src/components/displays/edgeGateway/EdgeGatewayWorkspace.vue
index 06f44967..796655d5 100644
--- a/src/components/displays/edgeGateway/EdgeGatewayWorkspace.vue
+++ b/src/components/displays/edgeGateway/EdgeGatewayWorkspace.vue
@@ -179,27 +179,27 @@ const getDepartmentName = (departmentId) => {
const getStatusMeta = (status) => STATUS_META[status] ?? STATUS_META.UNKNOWN;
-const getBrokerMeta = (brokerConnected) => {
- if (brokerConnected === true) {
+const getControlChannelMeta = (gatewayStatus) => {
+ if (gatewayStatus === "OFFLINE") {
return {
- label: "Broker forbundet",
- tone: "success",
- helper: "Websocket-kanalen er aktiv til live shell og hurtig status.",
+ label: "Polling stoppet",
+ tone: "warning",
+ helper: "Gatewayen sender ikke heartbeat nok til at hente kommandoer via API'et.",
};
}
- if (brokerConnected === false) {
+ if (gatewayStatus === "DEGRADED") {
return {
- label: "Broker frakoblet",
+ label: "API polling",
tone: "warning",
- helper: "Gatewayen kan stadig hente k\u00f8ede kommandoer via API'et.",
+ helper: "Gatewayen henter stadig kommandoer via API'et, men heartbeat er ved at blive gammel.",
};
}
return {
- label: "Broker ukendt",
- tone: "neutral",
- helper: "Gatewayen har endnu ikke rapporteret brokerstatus.",
+ label: "API polling",
+ tone: "success",
+ helper: "Gatewayen henter kommandoer og shell-events via API'et.",
};
};
@@ -320,7 +320,7 @@ const buildGatewayView = (gateway) => {
const recentShellSessions = Array.isArray(gateway.recent_shell_sessions) ? gateway.recent_shell_sessions : [];
const recentCommands = Array.isArray(gateway.recent_commands) ? gateway.recent_commands : [];
const auditLogs = Array.isArray(gateway.audit_logs) ? gateway.audit_logs : [];
- const brokerMeta = getBrokerMeta(metadata.broker_connected);
+ const brokerMeta = getControlChannelMeta(gateway.status);
const metrics = buildGatewayMetricView(gateway);
return {
@@ -345,7 +345,6 @@ const buildGatewayView = (gateway) => {
(gateway.transport_mode ?? null) !== null &&
(gateway.department_transport_mode ?? null) !== null &&
gateway.transport_mode !== gateway.department_transport_mode,
- brokerConnected: metadata.broker_connected ?? null,
brokerStatusLabel: brokerMeta.label,
brokerStatusTone: brokerMeta.tone,
brokerStatusHelper: brokerMeta.helper,
@@ -666,24 +665,12 @@ const resolveEdgeGatewayErrorMessage = (error, fallbackMessage) => {
error?.message ??
"";
- if (message.includes("Gateway agent is offline")) {
- return "Gateway-agenten er offline og kan ikke hente nye kommandoer lige nu.";
- }
-
- if (message.includes("Could not resolve host: edge-broker")) {
- return "API-miljøet kan ikke nÃ¥ broker-servicen. Gatewayen kan stadig hente køede kommandoer via API'et.";
- }
-
if (typeof message !== "string" || message.trim() === "") {
return fallbackMessage;
}
if (message.includes("Gateway agent is offline")) {
- return "Gateway-agenten er offline. Discovery kræver en aktiv brokerforbindelse på port 4300.";
- }
-
- if (message.includes("Could not resolve host: edge-broker")) {
- return "API-miljøet kan ikke nå edge-broker-servicen.";
+ return "Gateway-agenten er offline og kan ikke hente nye kommandoer lige nu.";
}
return message;
@@ -1256,7 +1243,7 @@ onBeforeUnmount(() => {
{{ selectedGatewayView.bindings.length }} bindinger
- Brokerforbindelse
+ Styringskanal
{{ selectedGatewayView.brokerStatusLabel }}
{{ selectedGatewayView.brokerStatusHelper }}
@@ -1328,7 +1315,7 @@ onBeforeUnmount(() => {
{{ selectedGatewayView.discoveryStatusLabel }}
-
Brokerforbindelse
+ Styringskanal
{{ selectedGatewayView.brokerStatusLabel }}
diff --git a/src/components/shop/POSDepartmentProcess.vue b/src/components/shop/POSDepartmentProcess.vue
index 80a5e706..0925f0ec 100644
--- a/src/components/shop/POSDepartmentProcess.vue
+++ b/src/components/shop/POSDepartmentProcess.vue
@@ -175,6 +175,63 @@ const getSelectedCustomerNumber = () => {
return resolveCustomerNumber(customer_id.value);
};
+const normalizeCustomerRecord = (customer, fallback = {}) => {
+ if (!customer || typeof customer !== 'object') {
+ const fallbackCustomerNumber = resolveCustomerNumber(
+ fallback.customerNumber
+ ?? fallback.customer_number
+ ?? customer
+ );
+
+ return {
+ id: fallback.id ?? fallbackCustomerNumber ?? null,
+ customerNumber: fallbackCustomerNumber,
+ name: fallback.name ?? fallback.customer_name ?? fallback.customerName ?? '',
+ address: fallback.address ?? '',
+ zip: fallback.zip ?? fallback.zip_code ?? fallback.postalCode ?? '',
+ city: fallback.city ?? '',
+ mobilePhone: fallback.mobilePhone ?? fallback.mobile_phone ?? fallback.phone ?? '',
+ email: fallback.email ?? '',
+ corporateIdentificationNumber: fallback.corporateIdentificationNumber
+ ?? fallback.corporate_identification_number
+ ?? fallback.cvr
+ ?? '',
+ economic_customer: fallback.economic_customer ?? fallbackCustomerNumber,
+ barred: Boolean(fallback.barred),
+ };
+ }
+
+ const resolvedCustomerNumber = resolveCustomerNumber(
+ customer.customerNumber
+ ?? customer.customer_number
+ ?? customer.economic_customer
+ ?? customer.economicCustomer
+ ?? fallback.customerNumber
+ ?? fallback.customer_number
+ );
+
+ return {
+ ...customer,
+ id: customer.id ?? resolvedCustomerNumber ?? null,
+ customerNumber: resolvedCustomerNumber,
+ name: customer.name ?? customer.customer_name ?? customer.customerName ?? fallback.name ?? fallback.customer_name ?? '',
+ address: customer.address ?? fallback.address ?? '',
+ zip: customer.zip ?? customer.zip_code ?? customer.postalCode ?? fallback.zip ?? fallback.zip_code ?? '',
+ city: customer.city ?? fallback.city ?? '',
+ mobilePhone: customer.mobilePhone ?? customer.mobile_phone ?? customer.phone ?? fallback.mobilePhone ?? fallback.mobile_phone ?? fallback.phone ?? '',
+ email: customer.email ?? fallback.email ?? '',
+ corporateIdentificationNumber: customer.corporateIdentificationNumber
+ ?? customer.corporate_identification_number
+ ?? customer.cvr
+ ?? fallback.corporateIdentificationNumber
+ ?? fallback.corporate_identification_number
+ ?? fallback.cvr
+ ?? '',
+ economic_customer: customer.economic_customer ?? customer.economicCustomer ?? resolvedCustomerNumber,
+ barred: Boolean(customer.barred ?? fallback.barred),
+ };
+};
+
const clearSelectedCustomerState = (options = {}) => {
const normalizedOptions = {
clearOrderNotes: false,
@@ -522,7 +579,9 @@ export const selectCustomer = (customer, options = {}) => {
return;
}
- const nextCustomerNumber = resolveCustomerNumber(customer);
+ const normalizedCustomer = normalizeCustomerRecord(customer);
+
+ const nextCustomerNumber = resolveCustomerNumber(normalizedCustomer);
if (!nextCustomerNumber) {
clearSelectedCustomerState();
return;
@@ -536,9 +595,9 @@ export const selectCustomer = (customer, options = {}) => {
}
customer_id.value = nextCustomerNumber;
- customer_name.value = customer.name || '';
- customer_data.value = customer;
- console.log(customer);
+ customer_name.value = normalizedCustomer.name || '';
+ customer_data.value = normalizedCustomer;
+ console.log(normalizedCustomer);
if (isSameCustomer && !normalizedOptions.forceRefresh) {
return;
@@ -684,7 +743,14 @@ export const searchAndSelectCustomer = async (customerNumber, options = {}) => {
`/users/customer?customer_number=${customerNumber}`,
'GET').then((response) => {
console.log(response);
- selectCustomer(response.data.data.economic_customer, options);
+ const responseData = response?.data?.data ?? {};
+ const rawEconomicCustomer = responseData.economic_customer ?? null;
+ const normalizedCustomer = normalizeCustomerRecord(rawEconomicCustomer, {
+ ...responseData,
+ customerNumber: resolveCustomerNumber(rawEconomicCustomer) ?? resolveCustomerNumber(customerNumber),
+ });
+ selectCustomer(normalizedCustomer, options);
+ return normalizedCustomer;
}).catch((error) => {
console.error(error);
});
diff --git a/src/composables/useEdgeGatewayTerminal.js b/src/composables/useEdgeGatewayTerminal.js
index d2c1cb09..b7837b51 100644
--- a/src/composables/useEdgeGatewayTerminal.js
+++ b/src/composables/useEdgeGatewayTerminal.js
@@ -9,10 +9,9 @@ export const EDGE_GATEWAY_TERMINAL_STATES = Object.freeze({
error: "error",
});
-const SOCKET_CONNECTING = 0;
-const SOCKET_OPEN = 1;
-const SOCKET_CLOSING = 2;
const CONNECTION_TIMEOUT_MS = 8000;
+const SHELL_EVENT_WAIT_SECONDS = 5;
+const SHELL_POLL_RETRY_DELAY_MS = 1000;
function defaultCopyText(value) {
if (navigator?.clipboard?.writeText) {
@@ -31,22 +30,17 @@ function defaultCopyText(value) {
return Promise.resolve();
}
-function bindEvent(target, eventName, handler) {
- if (typeof target?.addEventListener === "function") {
- target.addEventListener(eventName, handler);
- return () => target.removeEventListener(eventName, handler);
- }
-
- if (typeof target?.on === "function") {
- target.on(eventName, handler);
- return () => target.off?.(eventName, handler);
- }
-
- return () => {};
+function wait(delayMs) {
+ return new Promise((resolve) => {
+ window.setTimeout(resolve, delayMs);
+ });
}
export function useEdgeGatewayTerminal({
- createSocket = (url) => new WebSocket(url),
+ pollSessionEvents,
+ sendSessionInput,
+ sendSessionResize,
+ closeSession,
createTerminal,
createFitAddon,
copyText = defaultCopyText,
@@ -65,11 +59,13 @@ export function useEdgeGatewayTerminal({
let terminal = null;
let fitAddon = null;
let resizeObserver = null;
- let socket = null;
- let socketOff = [];
let connectionTimer = null;
let shellOpened = false;
let terminationRequested = false;
+ let activeGatewayId = null;
+ let latestEventId = 0;
+ let pollSequence = 0;
+ let requestChain = Promise.resolve();
const hasSession = computed(() => Boolean(session.value));
const hasOutput = computed(() => outputBuffer.value.length > 0);
@@ -108,56 +104,44 @@ export function useEdgeGatewayTerminal({
}
};
- const clearSocketBindings = () => {
- socketOff.forEach((dispose) => dispose());
- socketOff = [];
- };
-
- const closeSocket = ({ force = false } = {}) => {
+ const stopPolling = () => {
+ pollSequence += 1;
clearConnectionTimer();
-
- if (!socket) {
- return;
- }
-
- clearSocketBindings();
-
- if (force && typeof socket.close === "function" && socket.readyState < SOCKET_CLOSING) {
- try {
- socket.close();
- } catch {
- // Ignore socket shutdown errors during cleanup.
- }
- }
-
- socket = null;
};
- const sendResize = () => {
- if (!socket || socket.readyState !== SOCKET_OPEN || !shellOpened || !terminal) {
- return;
- }
-
- socket.send(
- JSON.stringify({
- type: "resize",
- cols: Math.max(1, Number(terminal.cols || 0)),
- rows: Math.max(1, Number(terminal.rows || 0)),
- })
- );
+ const markClosed = (detail) => {
+ connectionState.value = EDGE_GATEWAY_TERMINAL_STATES.closed;
+ statusDetail.value = detail;
+ connectionError.value = "";
+ inputEnabled.value = false;
};
- const sendInput = (data) => {
- if (!inputEnabled.value || !socket || socket.readyState !== SOCKET_OPEN) {
- return;
- }
+ const markConnectionError = (detail) => {
+ stopPolling();
+ connectionState.value = EDGE_GATEWAY_TERMINAL_STATES.error;
+ statusDetail.value = detail;
+ connectionError.value = detail;
+ inputEnabled.value = false;
+ appendNotice(`[${detail}]`);
+ };
- socket.send(
- JSON.stringify({
- type: "input",
- data: String(data || ""),
+ const queueRequest = (factory, onError) => {
+ requestChain = requestChain
+ .catch(() => {})
+ .then(() => {
+ if (!session.value?.id || !activeGatewayId) {
+ return null;
+ }
+
+ return factory();
})
- );
+ .catch(() => {
+ if (typeof onError === "string" && onError !== "") {
+ appendNotice(`[${onError}]`);
+ }
+ });
+
+ return requestChain;
};
const fitTerminal = () => {
@@ -170,13 +154,49 @@ export function useEdgeGatewayTerminal({
} catch {
return;
}
+ };
- sendResize();
+ const sendResize = () => {
+ if (
+ !shellOpened ||
+ !session.value?.id ||
+ !activeGatewayId ||
+ typeof sendSessionResize !== "function" ||
+ !terminal
+ ) {
+ return;
+ }
+
+ const cols = Math.max(1, Number(terminal.cols || 0));
+ const rows = Math.max(1, Number(terminal.rows || 0));
+ queueRequest(
+ () => sendSessionResize(activeGatewayId, session.value.id, { cols, rows }),
+ "Kunne ikke opdatere shell-storrelsen."
+ );
+ };
+
+ const sendInput = (data) => {
+ if (
+ !inputEnabled.value ||
+ !session.value?.id ||
+ !activeGatewayId ||
+ typeof sendSessionInput !== "function"
+ ) {
+ return;
+ }
+
+ queueRequest(
+ () => sendSessionInput(activeGatewayId, session.value.id, String(data || "")),
+ "Kunne ikke sende input til gateway-shell."
+ );
};
const setupResizeObserver = () => {
resizeObserver?.disconnect?.();
- resizeObserver = resizeObserverFactory(() => fitTerminal());
+ resizeObserver = resizeObserverFactory(() => {
+ fitTerminal();
+ sendResize();
+ });
resizeObserver?.observe?.(terminalHost.value);
};
@@ -205,23 +225,11 @@ export function useEdgeGatewayTerminal({
}
terminal.open(terminalHost.value);
-
terminal.onData?.((data) => {
sendInput(data);
});
-
- terminal.onResize?.(({ cols, rows }) => {
- if (!socket || socket.readyState !== SOCKET_OPEN || !shellOpened) {
- return;
- }
-
- socket.send(
- JSON.stringify({
- type: "resize",
- cols,
- rows,
- })
- );
+ terminal.onResize?.(() => {
+ sendResize();
});
if (outputBuffer.value) {
@@ -257,175 +265,170 @@ export function useEdgeGatewayTerminal({
window.setTimeout(() => fitTerminal(), 0);
};
- const markClosed = (detail) => {
- connectionState.value = EDGE_GATEWAY_TERMINAL_STATES.closed;
- statusDetail.value = detail;
- connectionError.value = "";
- inputEnabled.value = false;
- };
+ const handleShellEvent = (event) => {
+ const type = String(event?.event_type || event?.type || "").toUpperCase();
+ const payload = event?.payload && typeof event.payload === "object" ? event.payload : {};
- const markConnectionError = (detail, { closeSocketAfter = true } = {}) => {
- connectionState.value = EDGE_GATEWAY_TERMINAL_STATES.error;
- statusDetail.value = detail;
- connectionError.value = detail;
- inputEnabled.value = false;
- appendNotice(`[${detail}]`);
+ if (type === "OPENED") {
+ clearConnectionTimer();
+ shellOpened = true;
+ inputEnabled.value = true;
+ connectionState.value = EDGE_GATEWAY_TERMINAL_STATES.connected;
+ statusDetail.value = "Live shell er forbundet og klar til input.";
+ connectionError.value = "";
+ appendNotice("[Live shell forbundet]");
+ terminal?.focus?.();
+ fitTerminal();
+ return;
+ }
- if (closeSocketAfter && socket?.readyState < SOCKET_CLOSING) {
- try {
- socket.close();
- } catch {
- // Ignore socket shutdown errors after a failed connect attempt.
+ if (type === "OUTPUT") {
+ appendToBuffer(String(payload.data || ""));
+ return;
+ }
+
+ if (type === "CLOSED") {
+ clearConnectionTimer();
+ inputEnabled.value = false;
+ shellOpened = false;
+
+ if (connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.closed) {
+ return;
}
+
+ if (!terminationRequested && connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.connecting) {
+ markConnectionError("Gateway-shell blev lukket, for sessionen blev klar.");
+ return;
+ }
+
+ markClosed(terminationRequested ? "Sessionen blev lukket." : "Sessionen blev lukket af gateway-agenten.");
+ appendNotice(
+ `[Session lukket${payload.code !== undefined ? ` (exit ${payload.code})` : ""}]`
+ );
}
};
- const openSocket = (websocketUrl) => {
- closeSocket({ force: true });
+ const pollUntilClosed = async (gatewayId, sessionId) => {
+ if (typeof pollSessionEvents !== "function") {
+ throw new Error("Missing shell session polling transport.");
+ }
- shellOpened = false;
- terminationRequested = false;
- socket = createSocket(websocketUrl);
-
- clearConnectionTimer();
- connectionTimer = window.setTimeout(() => {
- if (connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.connecting && !shellOpened) {
- markConnectionError("Forbindelsen til gateway-shell blev ikke etableret.");
- }
- }, CONNECTION_TIMEOUT_MS);
-
- const handleMessage = (event) => {
- let message = null;
+ const currentSequence = ++pollSequence;
+ latestEventId = 0;
+ while (pollSequence === currentSequence) {
try {
- message = JSON.parse(String(event?.data ?? ""));
+ const response = await pollSessionEvents(gatewayId, sessionId, {
+ after_id: latestEventId,
+ wait_seconds: SHELL_EVENT_WAIT_SECONDS,
+ });
+
+ if (pollSequence !== currentSequence) {
+ return;
+ }
+
+ const payload = response?.data?.data ?? {};
+ const nextSession = payload?.session ?? null;
+ const events = Array.isArray(payload?.events) ? payload.events : [];
+
+ if (nextSession) {
+ session.value = nextSession;
+ }
+
+ for (const event of events) {
+ latestEventId = Math.max(latestEventId, Number(event?.id || 0));
+ handleShellEvent(event);
+ }
+
+ if (connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.error) {
+ return;
+ }
+
+ if (
+ session.value?.closed_at &&
+ connectionState.value !== EDGE_GATEWAY_TERMINAL_STATES.closed &&
+ connectionState.value !== EDGE_GATEWAY_TERMINAL_STATES.error
+ ) {
+ handleShellEvent({ event_type: "CLOSED", payload: session.value?.metadata ?? {} });
+ return;
+ }
+
+ if (connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.closed) {
+ return;
+ }
} catch {
- return;
+ if (pollSequence !== currentSequence) {
+ return;
+ }
+
+ if (!shellOpened) {
+ markConnectionError("Forbindelsen til gateway-shell blev ikke etableret via API'et.");
+ return;
+ }
+
+ appendNotice("[Midlertidig fejl ved polling af gateway-shell]");
+ await wait(SHELL_POLL_RETRY_DELAY_MS);
}
-
- if (message.type === "opened") {
- clearConnectionTimer();
- shellOpened = true;
- inputEnabled.value = true;
- connectionState.value = EDGE_GATEWAY_TERMINAL_STATES.connected;
- statusDetail.value = "Live shell er forbundet og klar til input.";
- connectionError.value = "";
- appendNotice("[Live shell forbundet]");
- terminal?.focus?.();
- fitTerminal();
- return;
- }
-
- if (message.type === "output") {
- appendToBuffer(String(message.data || ""));
- return;
- }
-
- if (message.type === "closed") {
- clearConnectionTimer();
- shellOpened = false;
- inputEnabled.value = false;
- const detail = terminationRequested
- ? "Sessionen blev lukket."
- : "Sessionen blev lukket af gateway-agenten.";
- markClosed(detail);
- appendNotice(`[Session lukket${message.code !== undefined ? ` (exit ${message.code})` : ""}]`);
- return;
- }
- };
-
- const handleOpen = () => {
- statusDetail.value = "Browserforbindelsen er åben. Afventer shell fra gatewayen.";
- };
-
- const handleError = () => {
- if (!shellOpened) {
- markConnectionError("Forbindelsen til gateway-shell fejlede før sessionen blev åbnet.", {
- closeSocketAfter: false,
- });
- }
- };
-
- const handleClose = () => {
- clearConnectionTimer();
- clearSocketBindings();
-
- if (
- connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.closed ||
- connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.error
- ) {
- shellOpened = false;
- socket = null;
- return;
- }
-
- if (!shellOpened && connectionState.value !== EDGE_GATEWAY_TERMINAL_STATES.error) {
- markConnectionError("Gateway-shell blev afbrudt før terminalen blev klar.", {
- closeSocketAfter: false,
- });
- } else if (
- shellOpened &&
- connectionState.value !== EDGE_GATEWAY_TERMINAL_STATES.closed &&
- connectionState.value !== EDGE_GATEWAY_TERMINAL_STATES.error
- ) {
- markClosed(terminationRequested ? "Sessionen blev lukket." : "Forbindelsen til gateway-shell blev lukket.");
- appendNotice("[Forbindelsen til gateway-shell blev lukket]");
- }
-
- shellOpened = false;
- socket = null;
- };
-
- socketOff = [
- bindEvent(socket, "open", handleOpen),
- bindEvent(socket, "message", handleMessage),
- bindEvent(socket, "error", handleError),
- bindEvent(socket, "close", handleClose),
- ];
+ }
};
const approveSession = async ({ gatewayId, reason, createSession }) => {
if (!gatewayId || !String(reason || "").trim() || typeof createSession !== "function") {
return null;
}
+ if (typeof pollSessionEvents !== "function") {
+ throw new Error("Missing shell session polling transport.");
+ }
+ ensureTerminal();
+ stopPolling();
connectionState.value = EDGE_GATEWAY_TERMINAL_STATES.approving;
statusDetail.value = "Godkender root shell og opretter session.";
connectionError.value = "";
inputEnabled.value = false;
+ shellOpened = false;
+ terminationRequested = false;
try {
- const response = await createSession(gatewayId, { reason: String(reason).trim() });
+ const response = await createSession(gatewayId, {
+ reason: String(reason).trim(),
+ cols: terminal ? Math.max(1, Number(terminal.cols || 0)) : undefined,
+ rows: terminal ? Math.max(1, Number(terminal.rows || 0)) : undefined,
+ });
const payload = response?.data?.data ?? {};
const nextSession = payload?.session ?? null;
- const websocketUrl = String(payload?.websocket_url || "");
- if (!nextSession || websocketUrl === "") {
+ if (!nextSession) {
throw new Error("Shell session payload was incomplete.");
}
+ activeGatewayId = gatewayId;
session.value = nextSession;
clearOutput();
appendNotice("[Shell-session godkendt]");
- appendNotice(`Årsag: ${String(reason).trim()}`);
- appendNotice("[Forbinder til gateway-shell...]");
- statusDetail.value = "Forbinder til den godkendte shell-session.";
+ appendNotice(`Arsag: ${String(reason).trim()}`);
+ appendNotice("[Forbinder til gateway-shell via API-polling...]");
+ statusDetail.value = "Afventer shell fra gatewayen.";
connectionState.value = EDGE_GATEWAY_TERMINAL_STATES.connecting;
- ensureTerminal();
- openSocket(websocketUrl);
+ connectionTimer = window.setTimeout(() => {
+ if (connectionState.value === EDGE_GATEWAY_TERMINAL_STATES.connecting && !shellOpened) {
+ markConnectionError("Forbindelsen til gateway-shell blev ikke etableret.");
+ }
+ }, CONNECTION_TIMEOUT_MS);
+ void pollUntilClosed(gatewayId, nextSession.id);
return payload;
} catch (error) {
connectionState.value = EDGE_GATEWAY_TERMINAL_STATES.error;
- statusDetail.value = "Kunne ikke åbne root shell-session.";
- connectionError.value = "Kunne ikke åbne root shell-session.";
- appendNotice("[Kunne ikke åbne root shell-session]");
+ statusDetail.value = "Kunne ikke abne root shell-session.";
+ connectionError.value = "Kunne ikke abne root shell-session.";
+ appendNotice("[Kunne ikke abne root shell-session]");
throw error;
}
};
const terminateSession = () => {
- if (!socket) {
+ if (!session.value?.id || !activeGatewayId || typeof closeSession !== "function") {
return;
}
@@ -433,14 +436,10 @@ export function useEdgeGatewayTerminal({
inputEnabled.value = false;
statusDetail.value = "Lukker den aktive shell-session.";
- if (socket.readyState === SOCKET_OPEN) {
- socket.send(JSON.stringify({ type: "close" }));
- return;
- }
-
- if (socket.readyState === SOCKET_CONNECTING) {
- socket.close();
- }
+ queueRequest(
+ () => closeSession(activeGatewayId, session.value.id),
+ "Kunne ikke lukke gateway-shell."
+ );
};
const copyOutput = async () => {
@@ -456,9 +455,9 @@ export function useEdgeGatewayTerminal({
};
const dispose = () => {
+ stopPolling();
resizeObserver?.disconnect?.();
resizeObserver = null;
- closeSocket({ force: true });
terminal?.dispose?.();
terminal = null;
fitAddon = null;
diff --git a/src/services/edgeGateways.js b/src/services/edgeGateways.js
index 1c33abbc..ab2c2be7 100644
--- a/src/services/edgeGateways.js
+++ b/src/services/edgeGateways.js
@@ -24,6 +24,34 @@ export const createEdgeGatewayUpdateJob = async (gatewayId, payload) =>
export const createEdgeGatewayShellSession = async (gatewayId, payload) =>
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}/shell-sessions`, "POST", payload);
+export const pollEdgeGatewayShellSession = async (gatewayId, sessionId, { after_id = 0, wait_seconds = 5 } = {}) =>
+ authenticatedRequest(
+ `/edge-gateways/${encodeURIComponent(gatewayId)}/shell-sessions/${encodeURIComponent(sessionId)}/events`,
+ "GET",
+ { after_id, wait_seconds }
+ );
+
+export const sendEdgeGatewayShellInput = async (gatewayId, sessionId, data) =>
+ authenticatedRequest(
+ `/edge-gateways/${encodeURIComponent(gatewayId)}/shell-sessions/${encodeURIComponent(sessionId)}/input`,
+ "POST",
+ { data }
+ );
+
+export const sendEdgeGatewayShellResize = async (gatewayId, sessionId, { cols, rows }) =>
+ authenticatedRequest(
+ `/edge-gateways/${encodeURIComponent(gatewayId)}/shell-sessions/${encodeURIComponent(sessionId)}/resize`,
+ "POST",
+ { cols, rows }
+ );
+
+export const closeEdgeGatewayShellSession = async (gatewayId, sessionId) =>
+ authenticatedRequest(
+ `/edge-gateways/${encodeURIComponent(gatewayId)}/shell-sessions/${encodeURIComponent(sessionId)}/close`,
+ "POST",
+ {}
+ );
+
export const rotateEdgeGatewayCredentials = async (gatewayId) =>
authenticatedRequest(`/edge-gateways/${encodeURIComponent(gatewayId)}/rotate-credentials`, "POST", {});
diff --git a/src/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js b/src/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js
index d2f704a8..3f37e53a 100644
--- a/src/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js
+++ b/src/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js
@@ -54,7 +54,7 @@ export const applyPosRouteSearch = (
}
if (parsedState.customerId !== null) {
- searchAndSelectCustomer(parsedState.customerId);
+ searchAndSelectCustomer(parsedState.customerId, { forceRefresh: true });
}
return parsedState;
diff --git a/tests/e2e/admin-bookings-mobile.spec.ts b/tests/e2e/admin-bookings-mobile.spec.ts
index 1639fd37..3bba70b3 100644
--- a/tests/e2e/admin-bookings-mobile.spec.ts
+++ b/tests/e2e/admin-bookings-mobile.spec.ts
@@ -1,11 +1,7 @@
import { expect, test } from "@playwright/test";
import { mockApi, seedAuthenticatedState } from "./support/network.js";
-const adminPermissions = [
- "admin",
- "department_access_1",
- "list_bookings",
-];
+const adminPermissions = ["admin", "department_access_1", "list_bookings"];
const bookingsFixture = [
{
@@ -59,9 +55,12 @@ test.describe("Admin bookings mobile", () => {
await expect(page.locator(".title")).toContainText("Oversigt over bookinger");
- const bookingCard = page.locator(".box.has-custom-border").filter({
- hasText: bookingsFixture[0].customer_name,
- }).first();
+ const bookingCard = page
+ .locator(".box.has-custom-border")
+ .filter({
+ hasText: bookingsFixture[0].customer_name,
+ })
+ .first();
await expect(bookingCard).toBeVisible();
diff --git a/tests/e2e/admin-daily-report.spec.ts b/tests/e2e/admin-daily-report.spec.ts
index 44d512d1..abe9ca77 100644
--- a/tests/e2e/admin-daily-report.spec.ts
+++ b/tests/e2e/admin-daily-report.spec.ts
@@ -27,10 +27,31 @@ const buildOverviewPayload = ({
products = [
{ product_id: 24, slug: "spot-free-lastbil", title: "Spot Free (Lastbil)", state: "ready", value: 3, out_of: 8 },
{ product_id: 25, slug: "faelg-flex", title: "Fælg flex pr. enhed", state: "ready", value: 2, out_of: 8 },
- { product_id: 27, slug: "extraordinary-10-min", title: "Ekstraordinær pr. 10 min inkl. kemi", state: "ready", value: 1, out_of: 8 },
- { product_id: 26, slug: "hoejglans", title: "Højglans - Voksforsegling pr. enhed", state: "ready", value: 4, out_of: 8 },
+ {
+ product_id: 27,
+ slug: "extraordinary-10-min",
+ title: "Ekstraordinær pr. 10 min inkl. kemi",
+ state: "ready",
+ value: 1,
+ out_of: 8,
+ },
+ {
+ product_id: 26,
+ slug: "hoejglans",
+ title: "Højglans - Voksforsegling pr. enhed",
+ state: "ready",
+ value: 4,
+ out_of: 8,
+ },
{ product_id: 21, slug: "undervognsskyl", title: "Undervognsskyl pr. enhed", state: "ready", value: 2, out_of: 8 },
- { product_id: 22, slug: "double-duty-kemi", title: "Tillæg for Specialsæbe - DD", state: "ready", value: 5, out_of: 8 },
+ {
+ product_id: 22,
+ slug: "double-duty-kemi",
+ title: "Tillæg for Specialsæbe - DD",
+ state: "ready",
+ value: 5,
+ out_of: 8,
+ },
],
...overrides
}: Record = {}) => ({
@@ -62,26 +83,33 @@ const buildOverviewPayload = ({
async function mockDailyReportDependencies(
page,
- input: {
- overviewHandler: (url: URL) => Promise> | ReturnType;
- complaintHandler?: (body: Record) => Promise> | ReturnType;
- complaintCustomerLookupHandler?: (url: URL) => Promise> | ReturnType;
- permissions?: string[];
- } | ((url: URL) => Promise> | ReturnType)
+ input:
+ | {
+ overviewHandler: (url: URL) => Promise> | ReturnType;
+ complaintHandler?: (
+ body: Record
+ ) => Promise> | ReturnType;
+ complaintCustomerLookupHandler?: (url: URL) => Promise> | ReturnType;
+ permissions?: string[];
+ }
+ | ((url: URL) => Promise> | ReturnType)
) {
const options = typeof input === "function" ? { overviewHandler: input } : input;
- const complaintHandler = options.complaintHandler || (async (body) => json({
- data: {
- id: 99,
- department_id: body.department_id,
- customer_number: body.customer_number ?? null,
- wash_date: body.wash_date ?? null,
- category: body.category ?? null,
- description: body.description,
- created_by: 1,
- created_at: "2026-04-08 10:00:00",
- },
- }));
+ const complaintHandler =
+ options.complaintHandler ||
+ (async (body) =>
+ json({
+ data: {
+ id: 99,
+ department_id: body.department_id,
+ customer_number: body.customer_number ?? null,
+ wash_date: body.wash_date ?? null,
+ category: body.category ?? null,
+ description: body.description,
+ created_by: 1,
+ created_at: "2026-04-08 10:00:00",
+ },
+ }));
const complaintCustomerLookupHandler = options.complaintCustomerLookupHandler || (async () => json({ data: [] }));
await seedAuthenticatedState(page);
@@ -91,12 +119,14 @@ async function mockDailyReportDependencies(
});
await page.route(/\/departments(\?.*)?$/i, async (route) => {
- await route.fulfill(json({
- data: [
- { id: 1, name: "North" },
- { id: 2, name: "South" },
- ],
- }));
+ await route.fulfill(
+ json({
+ data: [
+ { id: 1, name: "North" },
+ { id: 2, name: "South" },
+ ],
+ })
+ );
});
await page.route(/\/departments\/weather(\?.*)?$/i, async (route) => {
@@ -106,16 +136,18 @@ async function mockDailyReportDependencies(
await page.route(/\/departments\/daily-reports\/get(\?.*)?$/i, async (route) => {
const url = new URL(route.request().url());
const date = url.searchParams.get("date") || "2026-03-23";
- await route.fulfill(json({
- data: {
- id: 10,
- department_id: 1,
- water_usage: 32,
- notes: "",
- filled_by: 1,
- created_at: `${date} 00:00:00`,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 10,
+ department_id: 1,
+ water_usage: 32,
+ notes: "",
+ filled_by: 1,
+ created_at: `${date} 00:00:00`,
+ },
+ })
+ );
});
await page.route(/\/departments\/daily-reports\/complaints$/i, async (route) => {
@@ -171,13 +203,17 @@ test.describe("Admin daily report", () => {
await expect(page.getByTestId("daily-report-tile-spot-free-lastbil")).toContainText("Spot Free (Lastbil)");
await expect(page.getByTestId("daily-report-tile-spot-free-lastbil")).toContainText("3");
await expect(page.getByTestId("daily-report-tile-faelg-flex")).toContainText("Fælg flex pr. enhed");
- await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText("Ekstraordinær pr. 10 min inkl. kemi");
+ await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText(
+ "Ekstraordinær pr. 10 min inkl. kemi"
+ );
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Højglans - Voksforsegling pr. enhed");
await expect(page.getByTestId("daily-report-tile-undervognsskyl")).toContainText("Undervognsskyl pr. enhed");
await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD");
});
- test("opens and closes the complaints modal, requires wash date/category on multi-day ranges, clears a selected customer, submits without customer number, and refreshes the count", async ({ page }) => {
+ test("opens and closes the complaints modal, requires wash date/category on multi-day ranges, clears a selected customer, submits without customer number, and refreshes the count", async ({
+ page,
+ }) => {
const complaintRequests: Array> = [];
let complaintCount = 2;
@@ -199,11 +235,10 @@ test.describe("Admin daily report", () => {
},
});
},
- complaintCustomerLookupHandler: async () => json({
- data: [
- { customer_number: 12345, customer_name: "Acme Transport" },
- ],
- }),
+ complaintCustomerLookupHandler: async () =>
+ json({
+ data: [{ customer_number: 12345, customer_name: "Acme Transport" }],
+ }),
});
await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-24");
@@ -274,9 +309,7 @@ test.describe("Admin daily report", () => {
complaintCustomerLookupHandler: async (url) => {
customerLookupQueries.push(url.searchParams.get("search") || "");
return json({
- data: [
- { customer_number: 12345, customer_name: "Acme Transport" },
- ],
+ data: [{ customer_number: 12345, customer_name: "Acme Transport" }],
});
},
});
@@ -363,35 +396,45 @@ test.describe("Admin daily report", () => {
});
test("falls back to local optional product names when the overview payload omits titles", async ({ page }) => {
- await mockDailyReportDependencies(page, async () => json(buildOverviewPayload({
- products: [
- { product_id: 24, slug: "spot-free-lastbil", title: "", state: "ready", value: 3, out_of: 8 },
- { product_id: 25, slug: "faelg-flex", title: null, state: "ready", value: 2, out_of: 8 },
- { product_id: 27, slug: "extraordinary-10-min", state: "ready", value: 1, out_of: 8 },
- { product_id: 26, slug: "hoejglans", title: " ", state: "ready", value: 4, out_of: 8 },
- ],
- })));
+ await mockDailyReportDependencies(page, async () =>
+ json(
+ buildOverviewPayload({
+ products: [
+ { product_id: 24, slug: "spot-free-lastbil", title: "", state: "ready", value: 3, out_of: 8 },
+ { product_id: 25, slug: "faelg-flex", title: null, state: "ready", value: 2, out_of: 8 },
+ { product_id: 27, slug: "extraordinary-10-min", state: "ready", value: 1, out_of: 8 },
+ { product_id: 26, slug: "hoejglans", title: " ", state: "ready", value: 4, out_of: 8 },
+ ],
+ })
+ )
+ );
await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23");
await expect(page.getByTestId("daily-report-page")).toBeVisible();
await expect(page.getByTestId("daily-report-tile-spot-free-lastbil")).toContainText("Spot Free (Lastbil)");
await expect(page.getByTestId("daily-report-tile-faelg-flex")).toContainText("Fælg flex pr. enhed");
- await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText("Ekstraordinær pr. 10 min inkl. kemi");
+ await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText(
+ "Ekstraordinær pr. 10 min inkl. kemi"
+ );
await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Højglans - Voksforsegling pr. enhed");
await expect(page.getByTestId("daily-report-tile-undervognsskyl")).toContainText("Undervognsskyl pr. enhed");
await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD");
});
- test("updates departments and date query params with one overview request per UI action", async ({ page }, testInfo) => {
+ test("updates departments and date query params with one overview request per UI action", async ({
+ page,
+ }, testInfo) => {
const overviewRequests: URL[] = [];
await mockDailyReportDependencies(page, async (url) => {
overviewRequests.push(url);
- return json(buildOverviewPayload({
- department_ids: (url.searchParams.get("department_ids") || "1").split(",").map((value) => Number(value)),
- date_to: url.searchParams.get("date_to") || "2026-03-23",
- }));
+ return json(
+ buildOverviewPayload({
+ department_ids: (url.searchParams.get("department_ids") || "1").split(",").map((value) => Number(value)),
+ date_to: url.searchParams.get("date_to") || "2026-03-23",
+ })
+ );
});
await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23");
@@ -423,35 +466,54 @@ test.describe("Admin daily report", () => {
if (dateTo === "2026-03-23") {
await new Promise((resolve) => setTimeout(resolve, 400));
- return json(buildOverviewPayload({
- metrics: {
- bookings: { state: "ready", value: 1, out_of: 2, message: null },
- complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu." },
- night_washes: { state: "ready", value: 0, out_of: null, message: null },
- revenue: { state: "ready", value: 100, out_of: null, message: null },
- washes: { state: "ready", value: 1, out_of: null, message: null },
- products_sold: { state: "ready", value: 2, out_of: null, message: null },
- transactions: { state: "ready", value: 1, out_of: null, message: null },
- water_usage: { state: "ready", value: 3, out_of: null, message: null },
- overtime: { state: "ready", value: 0.25, out_of: null, message: null },
- },
- }));
+ return json(
+ buildOverviewPayload({
+ metrics: {
+ bookings: { state: "ready", value: 1, out_of: 2, message: null },
+ complaints: {
+ state: "unavailable",
+ value: null,
+ out_of: null,
+ message: "Kundeklager er ikke tilgængelig endnu.",
+ },
+ night_washes: { state: "ready", value: 0, out_of: null, message: null },
+ revenue: { state: "ready", value: 100, out_of: null, message: null },
+ washes: { state: "ready", value: 1, out_of: null, message: null },
+ products_sold: { state: "ready", value: 2, out_of: null, message: null },
+ transactions: { state: "ready", value: 1, out_of: null, message: null },
+ water_usage: { state: "ready", value: 3, out_of: null, message: null },
+ overtime: { state: "ready", value: 0.25, out_of: null, message: null },
+ },
+ })
+ );
}
- return json(buildOverviewPayload({
- date_to: "2026-03-24",
- metrics: {
- bookings: { state: "ready", value: 6, out_of: 9, message: null },
- complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu." },
- night_washes: { state: "ready", value: 3, out_of: null, message: null },
- revenue: { state: "ready", value: 3200, out_of: null, message: null },
- washes: { state: "ready", value: 10, out_of: null, message: null },
- products_sold: { state: "ready", value: 20, out_of: null, message: null },
- transactions: { state: "ready", value: 9, out_of: null, message: null },
- water_usage: { state: "ready", value: 40, out_of: null, message: null },
- overtime: { state: "unavailable", value: null, out_of: null, message: "Overarbejde kræver Workfeed-kobling." },
- },
- }));
+ return json(
+ buildOverviewPayload({
+ date_to: "2026-03-24",
+ metrics: {
+ bookings: { state: "ready", value: 6, out_of: 9, message: null },
+ complaints: {
+ state: "unavailable",
+ value: null,
+ out_of: null,
+ message: "Kundeklager er ikke tilgængelig endnu.",
+ },
+ night_washes: { state: "ready", value: 3, out_of: null, message: null },
+ revenue: { state: "ready", value: 3200, out_of: null, message: null },
+ washes: { state: "ready", value: 10, out_of: null, message: null },
+ products_sold: { state: "ready", value: 20, out_of: null, message: null },
+ transactions: { state: "ready", value: 9, out_of: null, message: null },
+ water_usage: { state: "ready", value: 40, out_of: null, message: null },
+ overtime: {
+ state: "unavailable",
+ value: null,
+ out_of: null,
+ message: "Overarbejde kræver Workfeed-kobling.",
+ },
+ },
+ })
+ );
});
await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23");
diff --git a/tests/e2e/admin-department-visibility.spec.ts b/tests/e2e/admin-department-visibility.spec.ts
index 42701a20..5b13f6dd 100644
--- a/tests/e2e/admin-department-visibility.spec.ts
+++ b/tests/e2e/admin-department-visibility.spec.ts
@@ -23,7 +23,12 @@ const buildOverviewPayload = () => ({
date_to: "2026-04-08",
metrics: {
bookings: { state: "ready", value: 4, out_of: 6, message: null },
- complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu." },
+ complaints: {
+ state: "unavailable",
+ value: null,
+ out_of: null,
+ message: "Kundeklager er ikke tilgængelig endnu.",
+ },
night_washes: { state: "ready", value: 1, out_of: null, message: null },
revenue: { state: "ready", value: 2400, out_of: null, message: null },
washes: { state: "ready", value: 8, out_of: null, message: null },
@@ -48,14 +53,16 @@ async function mockAdminDepartmentDependencies(page, options: { departmentDelayM
await new Promise((resolve) => setTimeout(resolve, options.departmentDelayMs));
}
- await route.fulfill(json({
- data: [
- { id: 1, name: "Visible North", visible: true },
- { id: 2, name: "Hidden South", visible: false },
- { id: 3, name: "Legacy East" },
- { id: 4, name: "Numeric Hidden", visible: 0 },
- ],
- }));
+ await route.fulfill(
+ json({
+ data: [
+ { id: 1, name: "Visible North", visible: true },
+ { id: 2, name: "Hidden South", visible: false },
+ { id: 3, name: "Legacy East" },
+ { id: 4, name: "Numeric Hidden", visible: 0 },
+ ],
+ })
+ );
});
await page.route(/\/departments\/weather(\?.*)?$/i, async (route) => {
@@ -63,16 +70,18 @@ async function mockAdminDepartmentDependencies(page, options: { departmentDelayM
});
await page.route(/\/departments\/daily-reports\/get(\?.*)?$/i, async (route) => {
- await route.fulfill(json({
- data: {
- id: 10,
- department_id: 1,
- water_usage: 32,
- notes: "",
- filled_by: 1,
- created_at: "2026-04-08 00:00:00",
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 10,
+ department_id: 1,
+ water_usage: 32,
+ notes: "",
+ filled_by: 1,
+ created_at: "2026-04-08 00:00:00",
+ },
+ })
+ );
});
await page.route(/\/departments\/daily-reports(?:\/overview)?(\?.*)?$/i, async (route) => {
@@ -100,7 +109,9 @@ async function expandDepartmentFiltersOnMobile(page, testInfo) {
}
test.describe("Admin department visibility", () => {
- test("hides invisible departments from both the header selector and the daily report department controls", async ({ page }, testInfo) => {
+ test("hides invisible departments from both the header selector and the daily report department controls", async ({
+ page,
+ }, testInfo) => {
await mockAdminDepartmentDependencies(page);
await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-04-08&dateTo=2026-04-08");
diff --git a/tests/e2e/admin-overview-mobile.spec.ts b/tests/e2e/admin-overview-mobile.spec.ts
index e19e9e56..68256712 100644
--- a/tests/e2e/admin-overview-mobile.spec.ts
+++ b/tests/e2e/admin-overview-mobile.spec.ts
@@ -43,31 +43,35 @@ async function mockOverviewPageDependencies(page) {
});
await page.route(/\/departments(\?.*)?$/i, async (route) => {
- await route.fulfill(json({
- data: [
- { id: 1, name: "North" },
- ],
- }));
+ await route.fulfill(
+ json({
+ data: [{ id: 1, name: "North" }],
+ })
+ );
});
await page.route(/\/admin\/bookings\/department\/count(\?.*)?$/i, async (route) => {
- await route.fulfill(json({
- data: {
+ await route.fulfill(
+ json({
data: {
- message: 5,
+ data: {
+ message: 5,
+ },
},
- },
- }));
+ })
+ );
});
await page.route(/\/departments\/self-serve\/enabled(\?.*)?$/i, async (route) => {
- await route.fulfill(json({
- data: {
+ await route.fulfill(
+ json({
data: {
- enabled: false,
+ data: {
+ enabled: false,
+ },
},
- },
- }));
+ })
+ );
});
await page.route(/\/departments\/weather(\?.*)?$/i, async (route) => {
@@ -75,16 +79,18 @@ async function mockOverviewPageDependencies(page) {
});
await page.route(/\/departments\/daily-reports\/get(\?.*)?$/i, async (route) => {
- await route.fulfill(json({
- data: {
- id: 10,
- department_id: 1,
- water_usage: 32,
- notes: "",
- filled_by: 1,
- created_at: "2026-04-09 00:00:00",
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 10,
+ department_id: 1,
+ water_usage: 32,
+ notes: "",
+ filled_by: 1,
+ created_at: "2026-04-09 00:00:00",
+ },
+ })
+ );
});
await page.route(/\/departments\/daily-reports\/complaints$/i, async (route) => {
@@ -103,7 +109,9 @@ async function mockOverviewPageDependencies(page) {
}
test.describe("Admin overview mobile", () => {
- test("keeps modules first, collapses reports by default, and preserves mobile report actions", async ({ page }, testInfo) => {
+ test("keeps modules first, collapses reports by default, and preserves mobile report actions", async ({
+ page,
+ }, testInfo) => {
test.skip(!testInfo.project.use.isMobile, "Mobile-only overview regression");
await mockOverviewPageDependencies(page);
@@ -119,7 +127,9 @@ test.describe("Admin overview mobile", () => {
const modulesBox = await modules.boundingBox();
const reportsBox = await reportsToggle.boundingBox();
expect(modulesBox?.y ?? 0).toBeLessThan(reportsBox?.y ?? Number.MAX_SAFE_INTEGER);
- expect(await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth)).toBe(false);
+ expect(await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth)).toBe(
+ false
+ );
await reportsToggle.click();
await expect(page.getByTestId("daily-report-page")).toBeVisible();
@@ -129,7 +139,9 @@ test.describe("Admin overview mobile", () => {
await page.getByTestId("daily-report-mobile-departments-toggle").click();
await expect(page.getByTestId("daily-report-department-controls")).toBeVisible();
await expect(page.getByTestId("daily-report-department-all")).toBeVisible();
- expect(await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth)).toBe(false);
+ expect(await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth)).toBe(
+ false
+ );
await page.getByTestId("daily-report-complaints-add-button").click();
await expect(page.getByTestId("daily-report-complaints-modal")).toBeVisible();
diff --git a/tests/e2e/admin-overview-night-washes.spec.ts b/tests/e2e/admin-overview-night-washes.spec.ts
index 2f8dbf8c..b0de61ae 100644
--- a/tests/e2e/admin-overview-night-washes.spec.ts
+++ b/tests/e2e/admin-overview-night-washes.spec.ts
@@ -29,7 +29,12 @@ const overviewByRange: Record> = {
date_to: "2026-03-24",
metrics: {
bookings: { state: "ready", value: 9, out_of: 12, message: null },
- complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu." },
+ complaints: {
+ state: "unavailable",
+ value: null,
+ out_of: null,
+ message: "Kundeklager er ikke tilgængelig endnu.",
+ },
night_washes: {
state: "ready",
value: 5,
@@ -58,7 +63,12 @@ const overviewByRange: Record> = {
date_to: "2026-03-25",
metrics: {
bookings: { state: "ready", value: 12, out_of: 16, message: null },
- complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu." },
+ complaints: {
+ state: "unavailable",
+ value: null,
+ out_of: null,
+ message: "Kundeklager er ikke tilgængelig endnu.",
+ },
night_washes: {
state: "ready",
value: 7,
@@ -233,9 +243,11 @@ async function mockOverviewDependencies(page) {
const url = new URL(route.request().url());
const departmentId = Number(url.searchParams.get("id"));
if (departmentId > 0) {
- await route.fulfill(json({
- data: departments.find((department) => department.id === departmentId) || null,
- }));
+ await route.fulfill(
+ json({
+ data: departments.find((department) => department.id === departmentId) || null,
+ })
+ );
return;
}
@@ -249,47 +261,59 @@ async function mockOverviewDependencies(page) {
await page.route(/\/departments\/daily-reports\/get(\?.*)?$/i, async (route) => {
const url = new URL(route.request().url());
const date = url.searchParams.get("date") || "2026-03-23";
- await route.fulfill(json({
- data: {
- id: 10,
- department_id: 1,
- water_usage: 32,
- notes: "",
- filled_by: 1,
- created_at: `${date} 00:00:00`,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 10,
+ department_id: 1,
+ water_usage: 32,
+ notes: "",
+ filled_by: 1,
+ created_at: `${date} 00:00:00`,
+ },
+ })
+ );
});
await page.route(/\/departments\/daily-reports\/overview(\?.*)?$/i, async (route) => {
const url = new URL(route.request().url());
const key = `${url.searchParams.get("date")}|${url.searchParams.get("date_to") || url.searchParams.get("date")}`;
- await route.fulfill(json({
- data: overviewByRange[key] || overviewByRange["2026-03-23|2026-03-24"],
- }));
+ await route.fulfill(
+ json({
+ data: overviewByRange[key] || overviewByRange["2026-03-23|2026-03-24"],
+ })
+ );
});
await page.route(/\/departments\/daily-reports\/transaction-count(\?.*)?$/i, async (route) => {
const url = new URL(route.request().url());
const key = `${url.searchParams.get("date")}|${url.searchParams.get("date_to") || url.searchParams.get("date")}`;
const departmentId = Number(url.searchParams.get("department_id"));
- const payload = transactionCountsByRange[key]?.[departmentId] || transactionCountsByRange["2026-03-23|2026-03-24"][departmentId];
- await route.fulfill(json({
- data: payload,
- }));
+ const payload =
+ transactionCountsByRange[key]?.[departmentId] || transactionCountsByRange["2026-03-23|2026-03-24"][departmentId];
+ await route.fulfill(
+ json({
+ data: payload,
+ })
+ );
});
await page.route(/\/departments\/daily-reports\/outside-hours-trend(\?.*)?$/i, async (route) => {
const url = new URL(route.request().url());
const key = `${url.searchParams.get("date")}|${url.searchParams.get("date_to")}`;
- await route.fulfill(json({
- data: trendByRange[key] || trendByRange["2026-03-23|2026-03-24"],
- }));
+ await route.fulfill(
+ json({
+ data: trendByRange[key] || trendByRange["2026-03-23|2026-03-24"],
+ })
+ );
});
- await page.route(/\/departments\/daily-reports(?!\/overview|\/transaction-count|\/outside-hours-trend|\/get)(\?.*)?$/i, async (route) => {
- await route.fulfill(json({ data: [] }));
- });
+ await page.route(
+ /\/departments\/daily-reports(?!\/overview|\/transaction-count|\/outside-hours-trend|\/get)(\?.*)?$/i,
+ async (route) => {
+ await route.fulfill(json({ data: [] }));
+ }
+ );
}
async function expandOverviewReportsOnMobile(page, testInfo) {
@@ -306,7 +330,9 @@ async function expandOverviewReportsOnMobile(page, testInfo) {
}
test.describe("Admin overview night washes", () => {
- test("renders deduped Døgnvask totals across cards, list, and chart with stable range updates", async ({ page }, testInfo) => {
+ test("renders deduped Døgnvask totals across cards, list, and chart with stable range updates", async ({
+ page,
+ }, testInfo) => {
await mockOverviewDependencies(page);
await page.goto("/admin?departmentIds=1,2,3&dateFrom=2026-03-23&dateTo=2026-03-24");
diff --git a/tests/e2e/admin-pos-orders.spec.ts b/tests/e2e/admin-pos-orders.spec.ts
index 4347bf36..abe2aa13 100644
--- a/tests/e2e/admin-pos-orders.spec.ts
+++ b/tests/e2e/admin-pos-orders.spec.ts
@@ -1,22 +1,23 @@
-import { test, expect, Page } from '@playwright/test';
-import { createPosFixture, mockApi, seedAuthenticatedState } from './support/network.js';
+import { test, expect, Page } from "@playwright/test";
+import { createPosFixture, mockApi, seedAuthenticatedState } from "./support/network.js";
const POS_PERMISSIONS = [
- 'admin',
- 'department_access_12',
- 'delete_order',
- 'edit_order_items',
- 'get_user',
- 'list_customer_attributes',
- 'get_custom_prices_other',
+ "admin",
+ "department_access_12",
+ "delete_order",
+ "edit_order_items",
+ "get_user",
+ "list_customer_notes",
+ "list_customer_attributes",
+ "get_custom_prices_other",
];
-async function primeOperatorSession(page: Page, token = 'pos-orders-token', _permissions = POS_PERMISSIONS) {
+async function primeOperatorSession(page: Page, token = "pos-orders-token", _permissions = POS_PERMISSIONS) {
await seedAuthenticatedState(page, token);
const sessionRequest = page.waitForResponse((response) => {
- return response.request().method() === 'GET' && response.url().includes('/auth/session');
+ return response.request().method() === "GET" && response.url().includes("/auth/session");
});
- await page.goto('/login');
+ await page.goto("/login");
await sessionRequest;
}
@@ -25,21 +26,23 @@ async function createDisposableOrder(page: Page) {
const reg1 = `PW${uniqueSuffix}`;
const reference = `E2E-${uniqueSuffix}`;
- await page.goto('/admin/12/modules/pos?step=1');
- await expect(page.getByTestId('pos-step-1')).toBeVisible();
+ await page.goto("/admin/12/modules/pos?step=1");
+ await expect(page.getByTestId("pos-step-1")).toBeVisible();
- await page.locator('#reg_1').fill(reg1);
- await page.locator('#reference').fill(reference);
- await page.locator('#pos_select_customer_input').fill('12345679');
- await expect(page.locator('.customer-drop-down-select').first()).toBeVisible();
- await page.locator('.customer-drop-down-select').first().click();
- await expect(page.locator('.field.has-addons input[disabled]').last()).toHaveValue(/\(TEST\) Pleno Vognmandsforretning/);
+ await page.locator("#reg_1").fill(reg1);
+ await page.locator("#reference").fill(reference);
+ await page.locator("#pos_select_customer_input").fill("12345679");
+ await expect(page.locator(".customer-drop-down-select").first()).toBeVisible();
+ await page.locator(".customer-drop-down-select").first().click();
+ await expect(page.locator(".field.has-addons input[disabled]").last()).toHaveValue(
+ /\(TEST\) Pleno Vognmandsforretning/
+ );
- await page.getByTestId('pos-step-1').getByTestId('pos-next-step').click();
+ await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=12345679&step=2/);
const url = new URL(page.url());
- const orderId = Number(url.searchParams.get('id'));
+ const orderId = Number(url.searchParams.get("id"));
expect(orderId).toBeGreaterThan(0);
return {
@@ -66,40 +69,40 @@ async function clickVisibleTestId(page: Page, testId: string) {
async function openOrderSettings(page: Page, orderId: number) {
await page.goto(`/admin/12/modules/pos/orders/${orderId}`);
- await expect(page.getByTestId('pos-order-detail')).toBeVisible();
- await clickVisibleTestId(page, 'pos-order-tab-settings');
- await expect(page.getByTestId('pos-order-panel-settings')).toBeVisible();
+ await expect(page.getByTestId("pos-order-detail")).toBeVisible();
+ await clickVisibleTestId(page, "pos-order-tab-settings");
+ await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
}
async function reloadOrderSettings(page: Page, orderId: number) {
await page.reload();
- await expect(page.getByTestId('pos-order-detail')).toBeVisible();
- await clickVisibleTestId(page, 'pos-order-tab-settings');
- await expect(page.getByTestId('pos-order-panel-settings')).toBeVisible();
+ await expect(page.getByTestId("pos-order-detail")).toBeVisible();
+ await clickVisibleTestId(page, "pos-order-tab-settings");
+ await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
}
function getCreatedAtInput(page: Page) {
- return page.getByTestId('pos-order-settings-created-at').locator('input').first();
+ return page.getByTestId("pos-order-settings-created-at").locator("input").first();
}
async function openOrderDetail(page: Page, orderId = 54518) {
await page.goto(`/admin/12/modules/pos/orders/${orderId}`);
- await expect(page.getByTestId('pos-order-detail')).toBeVisible();
+ await expect(page.getByTestId("pos-order-detail")).toBeVisible();
}
async function openOrderAttachments(page: Page, orderId = 54518) {
await openOrderDetail(page, orderId);
- await clickVisibleTestId(page, 'pos-order-tab-attachments');
- await expect(page.getByTestId('pos-order-panel-attachments')).toBeVisible();
+ await clickVisibleTestId(page, "pos-order-tab-attachments");
+ await expect(page.getByTestId("pos-order-panel-attachments")).toBeVisible();
}
async function openAddAttachmentCard(page: Page) {
- const addAttachmentCard = getVisibleTestId(page, 'pos-order-attachments-add-card');
- const attachWashCertificateButton = page.getByTestId('pos-order-attachments-attach-wash-certificate');
+ const addAttachmentCard = getVisibleTestId(page, "pos-order-attachments-add-card");
+ const attachWashCertificateButton = page.getByTestId("pos-order-attachments-attach-wash-certificate");
await expect(addAttachmentCard).toBeVisible();
- if (await attachWashCertificateButton.count() === 0) {
- await addAttachmentCard.locator('.card-header').click();
+ if ((await attachWashCertificateButton.count()) === 0) {
+ await addAttachmentCard.locator(".card-header").click();
}
await expect(attachWashCertificateButton).toBeVisible();
@@ -108,7 +111,7 @@ async function openAddAttachmentCard(page: Page) {
async function openAddItemsPanel(page: Page) {
const addItemsPanel = page.locator('[data-testid="pos-order-add-items-panel"]:visible').first();
const addItemsBackButton = page.locator('[data-testid="pos-order-add-items-back"]:visible').first();
- await page.getByTestId('pos-order-add-item').click();
+ await page.getByTestId("pos-order-add-item").click();
await expect(addItemsPanel).toBeVisible();
await expect(addItemsBackButton).toBeVisible();
}
@@ -118,29 +121,27 @@ function getVisibleTestId(page: Page, testId: string) {
}
async function expectOrderTotal(page: Page, total: number) {
- await expect(page.getByTestId('pos-order-total').first()).toHaveText(`${total} DKK`);
+ await expect(page.getByTestId("pos-order-total").first()).toHaveText(`${total} DKK`);
}
async function openOrderItemEditModal(page: Page, orderItemId: number) {
await page.getByTestId(`pos-order-item-edit-${orderItemId}`).click();
- await expect(page.getByTestId('pos-order-item-edit-modal')).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-edit-modal")).toBeVisible();
}
-async function waitForOrderItemMutation(page: Page, method: 'POST' | 'PUT' | 'DELETE', match: RegExp | string) {
+async function waitForOrderItemMutation(page: Page, method: "POST" | "PUT" | "DELETE", match: RegExp | string) {
return page.waitForRequest((request) => {
- if (request.method() !== method || !request.url().includes('/order/items')) {
+ if (request.method() !== method || !request.url().includes("/order/items")) {
return false;
}
- return typeof match === 'string'
- ? request.url().includes(match)
- : match.test(request.url());
+ return typeof match === "string" ? request.url().includes(match) : match.test(request.url());
});
}
-test.describe('Admin POS Orders - desktop settings', () => {
+test.describe("Admin POS Orders - desktop settings", () => {
test.beforeEach(async ({ page }, testInfo) => {
- test.skip(!testInfo.project.name.includes('desktop'), 'Desktop-only order settings coverage');
+ test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order settings coverage");
await mockApi(page, {
authenticated: true,
@@ -151,52 +152,52 @@ test.describe('Admin POS Orders - desktop settings', () => {
await primeOperatorSession(page);
});
- test('loads order detail metadata without order_id prop/setup errors on first render', async ({ page }) => {
+ test("loads order detail metadata without order_id prop/setup errors on first render", async ({ page }) => {
const consoleMessages: string[] = [];
const pageErrors: string[] = [];
- page.on('console', (message) => {
- if (message.type() === 'warning' || message.type() === 'error') {
+ page.on("console", (message) => {
+ if (message.type() === "warning" || message.type() === "error") {
consoleMessages.push(message.text());
}
});
- page.on('pageerror', (error) => {
+ page.on("pageerror", (error) => {
pageErrors.push(String(error));
});
await openOrderDetail(page);
- await expect(page.getByTestId('pos-order-registration-1')).toBeVisible();
- await expect(page.getByTestId('pos-order-metadata-reference')).toBeVisible();
- await expect(page.getByTestId('pos-order-metadata-note')).toBeVisible();
+ await expect(page.getByTestId("pos-order-registration-1")).toBeVisible();
+ await expect(page.getByTestId("pos-order-metadata-reference")).toBeVisible();
+ await expect(page.getByTestId("pos-order-metadata-note")).toBeVisible();
- const combinedMessages = [...consoleMessages, ...pageErrors].join('\n');
- expect(combinedMessages).not.toContain('Missing required prop: order_id');
+ const combinedMessages = [...consoleMessages, ...pageErrors].join("\n");
+ expect(combinedMessages).not.toContain("Missing required prop: order_id");
expect(combinedMessages).not.toContain('Invalid prop: type check failed for prop "order_id"');
- expect(combinedMessages).not.toContain('Unhandled error during execution of setup function');
+ expect(combinedMessages).not.toContain("Unhandled error during execution of setup function");
});
- test('renders order detail metadata and item actions without the Excel export affordance', async ({ page }) => {
+ test("renders order detail metadata and item actions without the Excel export affordance", async ({ page }) => {
await openOrderDetail(page);
- await expect(page.getByTestId('pos-order-header-export-actions')).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-header-export-actions")).toHaveCount(0);
await expect(page.locator('[data-auto-excel-export-button="1"]')).toHaveCount(0);
- const inlineDepartment = page.getByTestId('pos-order-inline-department');
- const inlineCreated = page.getByTestId('pos-order-inline-created');
+ const inlineDepartment = page.getByTestId("pos-order-inline-department");
+ const inlineCreated = page.getByTestId("pos-order-inline-created");
await expect(inlineDepartment).toBeVisible();
await expect(inlineCreated).toBeVisible();
- await expect(inlineDepartment).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
- await expect(inlineCreated).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
- await expect(inlineDepartment).toHaveCSS('padding-left', '0px');
- await expect(inlineCreated).toHaveCSS('padding-left', '0px');
- const transactionLabel = page.getByTestId('pos-order-transaction-label');
+ await expect(inlineDepartment).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
+ await expect(inlineCreated).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
+ await expect(inlineDepartment).toHaveCSS("padding-left", "0px");
+ await expect(inlineCreated).toHaveCSS("padding-left", "0px");
+ const transactionLabel = page.getByTestId("pos-order-transaction-label");
await expect(transactionLabel).toBeVisible();
- const priceHeader = page.getByTestId('pos-order-price-header');
- const actionsHeader = page.getByTestId('pos-order-actions-header');
- await expect(priceHeader).toContainText('Pris (DKK)');
- await expect(actionsHeader).toContainText('Handlinger');
- await expect(actionsHeader).not.toContainText('table.actions');
+ const priceHeader = page.getByTestId("pos-order-price-header");
+ const actionsHeader = page.getByTestId("pos-order-actions-header");
+ await expect(priceHeader).toContainText("Pris (DKK)");
+ await expect(actionsHeader).toContainText("Handlinger");
+ await expect(actionsHeader).not.toContainText("table.actions");
const [transactionFontSize, inlineDepartmentFontSize, inlineCreatedFontSize] = await Promise.all([
transactionLabel.evaluate((element) => window.getComputedStyle(element).fontSize),
@@ -208,7 +209,7 @@ test.describe('Admin POS Orders - desktop settings', () => {
const [priceHeaderStyles, actionsHeaderStyles] = await Promise.all([
priceHeader.evaluate((element) => {
- const target = element.querySelector('.th-wrap') || element;
+ const target = element.querySelector(".th-wrap") || element;
const styles = window.getComputedStyle(target);
return {
justifyContent: styles.justifyContent,
@@ -216,7 +217,7 @@ test.describe('Admin POS Orders - desktop settings', () => {
};
}),
actionsHeader.evaluate((element) => {
- const target = element.querySelector('.th-wrap') || element;
+ const target = element.querySelector(".th-wrap") || element;
const styles = window.getComputedStyle(target);
return {
justifyContent: styles.justifyContent,
@@ -224,15 +225,15 @@ test.describe('Admin POS Orders - desktop settings', () => {
};
}),
]);
- expect(priceHeaderStyles.justifyContent).toBe('flex-end');
- expect(priceHeaderStyles.textAlign).toBe('right');
- expect(actionsHeaderStyles.justifyContent).toBe('flex-end');
- expect(actionsHeaderStyles.textAlign).toBe('right');
- await expect(page.getByTestId('pos-order-item-price-9101')).toHaveCSS('text-align', 'right');
+ expect(priceHeaderStyles.justifyContent).toBe("flex-end");
+ expect(priceHeaderStyles.textAlign).toBe("right");
+ expect(actionsHeaderStyles.justifyContent).toBe("flex-end");
+ expect(actionsHeaderStyles.textAlign).toBe("right");
+ await expect(page.getByTestId("pos-order-item-price-9101")).toHaveCSS("text-align", "right");
- const licensePlatesSection = page.getByTestId('pos-order-metadata-license-plates');
- const referenceSection = page.getByTestId('pos-order-metadata-reference');
- const noteSection = page.getByTestId('pos-order-metadata-note');
+ const licensePlatesSection = page.getByTestId("pos-order-metadata-license-plates");
+ const referenceSection = page.getByTestId("pos-order-metadata-reference");
+ const noteSection = page.getByTestId("pos-order-metadata-note");
const metadataSections = [licensePlatesSection, referenceSection, noteSection];
for (const section of metadataSections) {
@@ -243,18 +244,18 @@ test.describe('Admin POS Orders - desktop settings', () => {
expect(box?.height ?? 0).toBeGreaterThan(70);
}
- await expect(licensePlatesSection.locator('.skeleton-lines')).toHaveCount(0);
- const reg1 = page.getByTestId('pos-order-registration-1');
- const reg2Add = page.getByTestId('pos-order-registration-add-2');
- const reg3Add = page.getByTestId('pos-order-registration-add-3');
+ await expect(licensePlatesSection.locator(".skeleton-lines")).toHaveCount(0);
+ const reg1 = page.getByTestId("pos-order-registration-1");
+ const reg2Add = page.getByTestId("pos-order-registration-add-2");
+ const reg3Add = page.getByTestId("pos-order-registration-add-3");
await expect(reg1).toBeVisible();
await expect(reg2Add).toBeVisible();
await expect(reg3Add).toBeVisible();
- const noteEmptyState = page.getByTestId('pos-order-note-empty-state');
+ const noteEmptyState = page.getByTestId("pos-order-note-empty-state");
await expect(noteEmptyState).toBeVisible();
- await expect(noteEmptyState.locator('.pos-order-field__empty-pill')).toHaveText(/^\+\s+\S+/);
+ await expect(noteEmptyState.locator(".pos-order-field__empty-pill")).toHaveText(/^\+\s+\S+/);
await expect(noteEmptyState).not.toContainText(/Ingen data/i);
- await expect(noteEmptyState.locator('.pos-order-note-editor__empty-copy')).toHaveCount(0);
+ await expect(noteEmptyState.locator(".pos-order-note-editor__empty-copy")).toHaveCount(0);
const reg1Box = await reg1.boundingBox();
const reg2AddBox = await reg2Add.boundingBox();
@@ -273,36 +274,38 @@ test.describe('Admin POS Orders - desktop settings', () => {
expect(Math.abs((licensePlatesBox?.x ?? 0) - (noteSectionBox?.x ?? 0))).toBeLessThanOrEqual(2);
expect(noteSectionBox?.width ?? 0).toBeGreaterThan(referenceSectionBox?.width ?? 0);
- const referenceControlBox = await referenceSection.locator('.pos-order-field__control').boundingBox();
- const noteControlBox = await noteSection.locator('.pos-order-field__control').boundingBox();
+ const referenceControlBox = await referenceSection.locator(".pos-order-field__control").boundingBox();
+ const noteControlBox = await noteSection.locator(".pos-order-field__control").boundingBox();
expect(referenceControlBox).not.toBeNull();
expect(noteControlBox).not.toBeNull();
expect(referenceControlBox?.height ?? 0).toBeLessThan(65);
expect(noteControlBox?.height ?? 0).toBeLessThan(100);
- await expect(noteEmptyState).not.toContainText('pos.add_note_placeholder');
+ await expect(noteEmptyState).not.toContainText("pos.add_note_placeholder");
- await expect(page.getByTestId('pos-order-item-edit-9101')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-delete-9101')).toBeVisible();
- await expect(page.getByTestId('pos-order-add-item')).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-edit-9101")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-delete-9101")).toBeVisible();
+ await expect(page.getByTestId("pos-order-add-item")).toBeVisible();
await expectOrderTotal(page, 1372);
- await page.getByTestId('pos-order-add-item').click();
- await expect(page.getByTestId('pos-order-add-items-panel').first()).toBeVisible();
- await expect(page.getByTestId('pos-product-card-53').first()).toBeVisible();
+ await page.getByTestId("pos-order-add-item").click();
+ await expect(page.getByTestId("pos-order-add-items-panel").first()).toBeVisible();
+ await expect(page.getByTestId("pos-product-card-53").first()).toBeVisible();
});
- test('keeps a readable split layout in add-items mode and restores the normal shell after backing out', async ({ page }) => {
+ test("keeps a readable split layout in add-items mode and restores the normal shell after backing out", async ({
+ page,
+ }) => {
await openOrderDetail(page);
- const main = getVisibleTestId(page, 'pos-order-main');
+ const main = getVisibleTestId(page, "pos-order-main");
const beforeMainBox = await main.boundingBox();
- const beforeRailBox = await getVisibleTestId(page, 'pos-order-rail').boundingBox();
+ const beforeRailBox = await getVisibleTestId(page, "pos-order-rail").boundingBox();
expect(beforeMainBox).not.toBeNull();
expect(beforeRailBox).not.toBeNull();
await openAddItemsPanel(page);
await expect(page.locator('[data-testid="pos-order-rail"]:visible')).toHaveCount(0);
- const workspace = getVisibleTestId(page, 'pos-order-workspace');
+ const workspace = getVisibleTestId(page, "pos-order-workspace");
const productPanel = page.locator('[data-testid="pos-order-add-items-panel"]:visible').first();
const cartPanel = page.locator('[data-testid="pos-order-panel-cart"]:visible').first();
const afterMainBox = await main.boundingBox();
@@ -315,104 +318,154 @@ test.describe('Admin POS Orders - desktop settings', () => {
expect(cartPanelBox).not.toBeNull();
expect((afterMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0)).toBeGreaterThan(200);
expect(workspaceBox?.width ?? 0).toBeGreaterThan(800);
- expect(productPanelBox?.width ?? 0).toBeGreaterThan(330);
- expect(cartPanelBox?.width ?? 0).toBeGreaterThan(420);
+ const productPanelWidth = productPanelBox?.width ?? 0;
+ const cartPanelWidth = cartPanelBox?.width ?? 0;
+ const workspaceWidth = workspaceBox?.width ?? 0;
+ expect(productPanelWidth).toBeGreaterThan(470);
+ expect(cartPanelWidth).toBeGreaterThan(300);
+ expect(cartPanelWidth).toBeLessThan(420);
+ expect(productPanelWidth).toBeGreaterThan(cartPanelWidth);
+ expect(productPanelWidth / workspaceWidth).toBeGreaterThan(0.55);
+ expect(cartPanelWidth / workspaceWidth).toBeLessThan(0.42);
expect(productPanelBox?.x ?? 0).toBeLessThan(cartPanelBox?.x ?? 0);
- await expect(getVisibleTestId(page, 'pos-order-total')).toBeVisible();
- await expect(getVisibleTestId(page, 'pos-order-item-edit-9101')).toBeVisible();
- await expect(getVisibleTestId(page, 'pos-order-registration-1')).toBeVisible();
- await expect(getVisibleTestId(page, 'pos-order-metadata-reference')).toBeVisible();
+ await expect(getVisibleTestId(page, "pos-order-total")).toBeVisible();
+ await expect(getVisibleTestId(page, "pos-order-item-edit-9101")).toBeVisible();
+ await expect(getVisibleTestId(page, "pos-order-registration-1")).toBeVisible();
+ await expect(getVisibleTestId(page, "pos-order-metadata-reference")).toBeVisible();
- const categoryTabs = productPanel.locator('.tabs li');
+ const categoryTabs = productPanel.locator(".tabs li");
await expect(categoryTabs).toHaveCount(3);
await categoryTabs.nth(2).click();
- await expect(productPanel.getByTestId('pos-product-card-64')).toBeVisible();
- await expect(productPanel.getByTestId('pos-product-card-53')).toHaveCount(0);
+ await expect(productPanel.getByTestId("pos-product-card-64")).toBeVisible();
+ await expect(productPanel.getByTestId("pos-product-card-53")).toHaveCount(0);
- await productPanel.getByTestId('pos-product-card-64').click();
- const addRequest = waitForOrderItemMutation(page, 'POST', '/order/items');
- await productPanel.getByTestId('pos-add-to-cart-64').click();
+ await productPanel.getByTestId("pos-product-card-64").click();
+ const addRequest = waitForOrderItemMutation(page, "POST", "/order/items");
+ await productPanel.getByTestId("pos-add-to-cart-64").click();
await addRequest;
- await expect(getVisibleTestId(page, 'pos-order-item-edit-9200')).toBeVisible();
- await expect(getVisibleTestId(page, 'pos-order-item-name-9200')).toContainText('Vaskecertifikat');
+ await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
+ await expect(getVisibleTestId(page, "pos-order-item-name-9200")).toContainText("Vaskecertifikat");
await expectOrderTotal(page, 1397);
await page.locator('[data-testid="pos-order-add-items-back"]:visible').first().click();
await expect(page.locator('[data-testid="pos-order-add-items-panel"]:visible')).toHaveCount(0);
- await expect(getVisibleTestId(page, 'pos-order-rail')).toBeVisible();
+ await expect(getVisibleTestId(page, "pos-order-rail")).toBeVisible();
const restoredMainBox = await main.boundingBox();
- const restoredRailBox = await getVisibleTestId(page, 'pos-order-rail').boundingBox();
+ const restoredRailBox = await getVisibleTestId(page, "pos-order-rail").boundingBox();
expect(restoredMainBox).not.toBeNull();
expect(restoredRailBox).not.toBeNull();
expect(Math.abs((restoredMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0))).toBeLessThanOrEqual(20);
expect(Math.abs((beforeRailBox?.width ?? 0) - (restoredRailBox?.width ?? 0))).toBeLessThanOrEqual(20);
await page.reload();
- await expect(page.getByTestId('pos-order-detail')).toBeVisible();
- await expect(getVisibleTestId(page, 'pos-order-item-edit-9200')).toBeVisible();
+ await expect(page.getByTestId("pos-order-detail")).toBeVisible();
+ await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
await expectOrderTotal(page, 1397);
});
- test('edits a primary order item in the Buefy modal and persists after reload', async ({ page }) => {
+ test("loads step 2 customer context after reload and keeps actions below the basket column", async ({ page }) => {
+ await page.goto("/admin/12/modules/pos");
+ await expect(page.getByTestId("pos-step-1")).toBeVisible();
+ await page.locator("#reg_1").fill("EC21235");
+ await expect(page.getByTestId("pos-step-1").locator(".pos-card-tabs > .tabs li.is-active")).toContainText("Kunde");
+ await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
+ await expect(page).toHaveURL(/\/admin\/12\/modules\/pos\?id=\d+&customer_id=12345679&step=2/);
+
+ const stepTwo = page.getByTestId("pos-step-2");
+ const cartPanel = stepTwo.getByTestId("pos-order-panel-cart");
+ const actions = stepTwo.getByTestId("pos-step-2-actions");
+ const customerName = page.locator('[data-testid="pos-order-customer-name"]:visible').first();
+ const licensePlatesCard = stepTwo.getByTestId("pos-order-metadata-license-plates");
+ const referenceCard = stepTwo.getByTestId("pos-order-metadata-reference");
+
+ await expect(stepTwo).toBeVisible();
+ await expect(customerName).toHaveText(/\(TEST\) Pleno Vognmandsforretning/);
+ await expect(actions.getByTestId("pos-next-step")).toBeVisible();
+ await expect(licensePlatesCard).toBeVisible();
+ await expect(referenceCard).toBeVisible();
+
+ const cartPanelBox = await cartPanel.boundingBox();
+ const actionPanelBox = await actions.boundingBox();
+ const licensePlatesCardBox = await licensePlatesCard.boundingBox();
+ const referenceCardBox = await referenceCard.boundingBox();
+ expect(cartPanelBox).not.toBeNull();
+ expect(actionPanelBox).not.toBeNull();
+ expect(licensePlatesCardBox).not.toBeNull();
+ expect(referenceCardBox).not.toBeNull();
+ expect(actionPanelBox?.y ?? 0).toBeGreaterThan((cartPanelBox?.y ?? 0) + (cartPanelBox?.height ?? 0) - 2);
+ expect(Math.abs((actionPanelBox?.x ?? 0) - (cartPanelBox?.x ?? 0))).toBeLessThanOrEqual(2);
+ expect(Math.abs((actionPanelBox?.width ?? 0) - (cartPanelBox?.width ?? 0))).toBeLessThanOrEqual(2);
+ expect(referenceCardBox?.y ?? 0).toBeGreaterThan(
+ (licensePlatesCardBox?.y ?? 0) + (licensePlatesCardBox?.height ?? 0) - 2
+ );
+ expect(Math.abs((referenceCardBox?.x ?? 0) - (licensePlatesCardBox?.x ?? 0))).toBeLessThanOrEqual(2);
+
+ await page.reload();
+ await expect(stepTwo).toBeVisible();
+ await expect(customerName).toHaveText(/\(TEST\) Pleno Vognmandsforretning/);
+ await expect(stepTwo.getByTestId("pos-next-step")).toBeVisible();
+ });
+
+ test("edits a primary order item in the Buefy modal and persists after reload", async ({ page }) => {
await openOrderDetail(page);
await expectOrderTotal(page, 1372);
await openOrderItemEditModal(page, 9101);
- await page.getByTestId('pos-order-item-edit-price').fill('700');
- await page.getByTestId('pos-order-item-edit-quantity').fill('2');
- await page.getByTestId('pos-order-item-edit-notes').fill('Primary item note');
- await page.getByTestId('pos-order-item-edit-reference').fill('PRIMARY-REF');
+ await page.getByTestId("pos-order-item-edit-price").fill("700");
+ await page.getByTestId("pos-order-item-edit-quantity").fill("2");
+ await page.getByTestId("pos-order-item-edit-notes").fill("Primary item note");
+ await page.getByTestId("pos-order-item-edit-reference").fill("PRIMARY-REF");
- const editRequest = waitForOrderItemMutation(page, 'PUT', '/order/items');
- await page.getByTestId('pos-order-item-edit-save').click();
+ const editRequest = waitForOrderItemMutation(page, "PUT", "/order/items");
+ await page.getByTestId("pos-order-item-edit-save").click();
await editRequest;
- await expect(page.getByTestId('pos-order-item-edit-modal')).toBeHidden();
- await expect(page.getByTestId('pos-order-item-note-trigger-9101')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-reference-trigger-9101')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-quantity-9101')).toContainText('2');
- await expect(page.getByTestId('pos-order-item-price-9101')).toContainText('1400');
+ await expect(page.getByTestId("pos-order-item-edit-modal")).toBeHidden();
+ await expect(page.getByTestId("pos-order-item-note-trigger-9101")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-reference-trigger-9101")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-quantity-9101")).toContainText("2");
+ await expect(page.getByTestId("pos-order-item-price-9101")).toContainText("1400");
await expectOrderTotal(page, 2123);
await page.reload();
- await expect(page.getByTestId('pos-order-detail')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-note-trigger-9101')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-reference-trigger-9101')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-price-9101')).toContainText('1400');
+ await expect(page.getByTestId("pos-order-detail")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-note-trigger-9101")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-reference-trigger-9101")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-price-9101")).toContainText("1400");
await expectOrderTotal(page, 2123);
});
- test('edits a related addon item and preserves grouped rendering after reload', async ({ page }) => {
+ test("edits a related addon item and preserves grouped rendering after reload", async ({ page }) => {
await openOrderDetail(page);
await openOrderItemEditModal(page, 9102);
- await page.getByTestId('pos-order-item-edit-price').fill('450');
- await page.getByTestId('pos-order-item-edit-notes').fill('Addon note');
+ await page.getByTestId("pos-order-item-edit-price").fill("450");
+ await page.getByTestId("pos-order-item-edit-notes").fill("Addon note");
- const editRequest = waitForOrderItemMutation(page, 'PUT', '/order/items');
- await page.getByTestId('pos-order-item-edit-save').click();
+ const editRequest = waitForOrderItemMutation(page, "PUT", "/order/items");
+ await page.getByTestId("pos-order-item-edit-save").click();
await editRequest;
- await expect(page.getByTestId('pos-order-item-edit-modal')).toBeHidden();
- await expect(page.getByTestId('pos-order-item-note-trigger-9102')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-price-9102')).toContainText('450');
- await expect(page.getByTestId('pos-order-item-name-9102')).toContainText('Indvendig vask Forvogn');
+ await expect(page.getByTestId("pos-order-item-edit-modal")).toBeHidden();
+ await expect(page.getByTestId("pos-order-item-note-trigger-9102")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-price-9102")).toContainText("450");
+ await expect(page.getByTestId("pos-order-item-name-9102")).toContainText("Indvendig vask Forvogn");
await expectOrderTotal(page, 1423);
await page.reload();
- await expect(page.getByTestId('pos-order-detail')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-note-trigger-9102')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-price-9102')).toContainText('450');
+ await expect(page.getByTestId("pos-order-detail")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-note-trigger-9102")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-price-9102")).toContainText("450");
await expectOrderTotal(page, 1423);
});
- test('cancels modal edits without mutating the order item', async ({ page }) => {
+ test("cancels modal edits without mutating the order item", async ({ page }) => {
const putRequests: string[] = [];
- page.on('request', (request) => {
- if (request.method() === 'PUT' && request.url().includes('/order/items')) {
+ page.on("request", (request) => {
+ if (request.method() === "PUT" && request.url().includes("/order/items")) {
putRequests.push(request.url());
}
});
@@ -420,81 +473,83 @@ test.describe('Admin POS Orders - desktop settings', () => {
await openOrderDetail(page);
await openOrderItemEditModal(page, 9104);
- await page.getByTestId('pos-order-item-edit-price').fill('999');
- await page.getByTestId('pos-order-item-edit-notes').fill('Do not save');
- await page.getByTestId('pos-order-item-edit-cancel').click();
- await expect(page.getByTestId('pos-order-item-edit-modal')).toBeHidden();
+ await page.getByTestId("pos-order-item-edit-price").fill("999");
+ await page.getByTestId("pos-order-item-edit-notes").fill("Do not save");
+ await page.getByTestId("pos-order-item-edit-cancel").click();
+ await expect(page.getByTestId("pos-order-item-edit-modal")).toBeHidden();
await page.waitForTimeout(200);
expect(putRequests).toHaveLength(0);
- await expect(page.getByTestId('pos-order-item-price-9104')).toContainText('299');
- await expect(page.getByTestId('pos-order-item-note-trigger-9104')).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-item-price-9104")).toContainText("299");
+ await expect(page.getByTestId("pos-order-item-note-trigger-9104")).toHaveCount(0);
await expectOrderTotal(page, 1372);
});
- test('deletes related and primary items and hides orphaned related rows after reload', async ({ page }) => {
+ test("deletes related and primary items and hides orphaned related rows after reload", async ({ page }) => {
await openOrderDetail(page);
await expectOrderTotal(page, 1372);
- const deleteRelatedRequest = waitForOrderItemMutation(page, 'DELETE', 'id=9104');
- await page.getByTestId('pos-order-item-delete-9104').click();
+ const deleteRelatedRequest = waitForOrderItemMutation(page, "DELETE", "id=9104");
+ await page.getByTestId("pos-order-item-delete-9104").click();
await deleteRelatedRequest;
- await expect(page.getByTestId('pos-order-item-delete-9104')).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-item-delete-9104")).toHaveCount(0);
await expectOrderTotal(page, 1073);
- const deletePrimaryRequest = waitForOrderItemMutation(page, 'DELETE', 'id=9101');
- await page.getByTestId('pos-order-item-delete-9101').click();
+ const deletePrimaryRequest = waitForOrderItemMutation(page, "DELETE", "id=9101");
+ await page.getByTestId("pos-order-item-delete-9101").click();
await deletePrimaryRequest;
- await expect(page.getByTestId('pos-order-empty-state')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-delete-9102')).toHaveCount(0);
- await expect(page.getByTestId('pos-order-item-delete-9103')).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-empty-state")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-delete-9102")).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-item-delete-9103")).toHaveCount(0);
await expectOrderTotal(page, 0);
await page.reload();
- await expect(page.getByTestId('pos-order-detail')).toBeVisible();
- await expect(page.getByTestId('pos-order-empty-state')).toBeVisible();
+ await expect(page.getByTestId("pos-order-detail")).toBeVisible();
+ await expect(page.getByTestId("pos-order-empty-state")).toBeVisible();
await expectOrderTotal(page, 0);
});
- test('adds a new product from the side panel and persists the created order item', async ({ page }) => {
+ test("adds a new product from the side panel and persists the created order item", async ({ page }) => {
await openOrderDetail(page);
await expectOrderTotal(page, 1372);
- await page.getByTestId('pos-order-add-item').click();
- await expect(page.getByTestId('pos-order-add-items-panel').first()).toBeVisible();
- await page.getByTestId('pos-product-card-53').first().click();
- await expect(page.getByTestId('pos-add-to-cart-53')).toBeVisible();
+ await page.getByTestId("pos-order-add-item").click();
+ await expect(page.getByTestId("pos-order-add-items-panel").first()).toBeVisible();
+ await page.getByTestId("pos-product-card-53").first().click();
+ await expect(page.getByTestId("pos-add-to-cart-53")).toBeVisible();
- const addRequest = waitForOrderItemMutation(page, 'POST', '/order/items');
- await page.getByTestId('pos-add-to-cart-53').click();
+ const addRequest = waitForOrderItemMutation(page, "POST", "/order/items");
+ await page.getByTestId("pos-add-to-cart-53").click();
await addRequest;
- await expect(page.getByTestId('pos-order-item-edit-9200').first()).toBeVisible();
- await expect(page.getByTestId('pos-order-item-name-9200').first()).toContainText('Forvogn');
+ await expect(page.getByTestId("pos-order-item-edit-9200").first()).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-name-9200").first()).toContainText("Forvogn");
await expectOrderTotal(page, 2021);
await page.reload();
- await expect(page.getByTestId('pos-order-detail')).toBeVisible();
- await expect(page.getByTestId('pos-order-item-edit-9200').first()).toBeVisible();
+ await expect(page.getByTestId("pos-order-detail")).toBeVisible();
+ await expect(page.getByTestId("pos-order-item-edit-9200").first()).toBeVisible();
await expectOrderTotal(page, 2021);
});
- test('attaches a wash certificate from the attachments tab and keeps duplicate attempts idempotent', async ({ page }) => {
+ test("attaches a wash certificate from the attachments tab and keeps duplicate attempts idempotent", async ({
+ page,
+ }) => {
await openOrderAttachments(page);
await openAddAttachmentCard(page);
- const attachmentCards = page.locator('.card-header-title').filter({ hasText: /Attachment #|Vedhæftning #/i });
- const attachWashCertificateButton = page.getByTestId('pos-order-attachments-attach-wash-certificate');
- const uploadAction = page.getByTestId('pos-order-attachments-upload-action');
+ const attachmentCards = page.locator(".card-header-title").filter({ hasText: /Attachment #|Vedhæftning #/i });
+ const attachWashCertificateButton = page.getByTestId("pos-order-attachments-attach-wash-certificate");
+ const uploadAction = page.getByTestId("pos-order-attachments-upload-action");
await expect(attachmentCards).toHaveCount(1);
await expect(attachWashCertificateButton).toBeVisible();
- await expect(uploadAction).toHaveText('Upload');
+ await expect(uploadAction).toHaveText("Upload");
const firstRequestPromise = page.waitForRequest((request) => {
- if (request.method() !== 'POST' || !request.url().includes('/order/wash-certificate')) {
+ if (request.method() !== "POST" || !request.url().includes("/order/wash-certificate")) {
return false;
}
@@ -503,25 +558,25 @@ test.describe('Admin POS Orders - desktop settings', () => {
});
await attachWashCertificateButton.click();
- await expect(page.locator('.swal2-popup')).toBeVisible();
- await page.locator('.swal2-input').fill('12345');
- await page.locator('.swal2-confirm').click();
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+ await page.locator(".swal2-input").fill("12345");
+ await page.locator(".swal2-confirm").click();
const firstRequest = await firstRequestPromise;
expect(firstRequest.postDataJSON()).toMatchObject({
id: 54518,
- safety_seal: '12345',
+ safety_seal: "12345",
});
await expect(page.getByText(/(Attachment|Vedhæftning) #400/i)).toBeVisible();
await expect(attachmentCards).toHaveCount(2);
- await expect(page.locator('.swal2-popup')).toContainText(/generated and attached|genereret og vedhæftet/i);
- await expect(page.locator('.swal2-popup')).toBeHidden({ timeout: 3000 });
- await getVisibleTestId(page, 'pos-order-attachment-card-400').locator('.card-header').click();
- await expect(page.getByTestId('pos-order-attachment-download-400')).toHaveText('Download');
+ await expect(page.locator(".swal2-popup")).toContainText(/generated and attached|genereret og vedhæftet/i);
+ await expect(page.locator(".swal2-popup")).toBeHidden({ timeout: 3000 });
+ await getVisibleTestId(page, "pos-order-attachment-card-400").locator(".card-header").click();
+ await expect(page.getByTestId("pos-order-attachment-download-400")).toHaveText("Download");
const secondRequestPromise = page.waitForRequest((request) => {
- if (request.method() !== 'POST' || !request.url().includes('/order/wash-certificate')) {
+ if (request.method() !== "POST" || !request.url().includes("/order/wash-certificate")) {
return false;
}
@@ -530,119 +585,119 @@ test.describe('Admin POS Orders - desktop settings', () => {
});
await attachWashCertificateButton.click();
- await expect(page.locator('.swal2-popup')).toBeVisible();
- await page.locator('.swal2-input').fill('99999');
- await page.locator('.swal2-confirm').click();
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+ await page.locator(".swal2-input").fill("99999");
+ await page.locator(".swal2-confirm").click();
const secondRequest = await secondRequestPromise;
expect(secondRequest.postDataJSON()).toMatchObject({
id: 54518,
- safety_seal: '99999',
+ safety_seal: "99999",
});
- await expect(page.locator('.swal2-popup')).toContainText(/already attached|allerede vedhæftet/i);
+ await expect(page.locator(".swal2-popup")).toContainText(/already attached|allerede vedhæftet/i);
await expect(attachmentCards).toHaveCount(2);
- await expect(page.locator('.swal2-popup')).toBeHidden({ timeout: 3000 });
+ await expect(page.locator(".swal2-popup")).toBeHidden({ timeout: 3000 });
});
- test('renders the settings surface and keeps destructive actions available', async ({ page }) => {
+ test("renders the settings surface and keeps destructive actions available", async ({ page }) => {
const { orderId } = await createDisposableOrder(page);
await openOrderSettings(page, orderId);
- await expect(page.getByTestId('pos-order-settings-department')).toBeVisible();
- await expect(page.getByTestId('pos-order-settings-created-at')).toBeVisible();
- await expect(page.getByTestId('pos-order-settings-include-in-invoice')).toBeVisible();
- await expect(page.getByTestId('pos-order-settings-include-helper')).not.toHaveText(/^\s*$/);
- await expect(page.getByTestId('pos-order-settings-reset')).toBeVisible();
- await expect(page.getByTestId('pos-order-settings-save')).toBeVisible();
- await expect(page.getByTestId('pos-order-settings-delete')).toBeVisible();
+ await expect(page.getByTestId("pos-order-settings-department")).toBeVisible();
+ await expect(page.getByTestId("pos-order-settings-created-at")).toBeVisible();
+ await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toBeVisible();
+ await expect(page.getByTestId("pos-order-settings-include-helper")).not.toHaveText(/^\s*$/);
+ await expect(page.getByTestId("pos-order-settings-reset")).toBeVisible();
+ await expect(page.getByTestId("pos-order-settings-save")).toBeVisible();
+ await expect(page.getByTestId("pos-order-settings-delete")).toBeVisible();
});
- test('persists department changes and shows department names in read-only details', async ({ page }) => {
+ test("persists department changes and shows department names in read-only details", async ({ page }) => {
const { orderId } = await createDisposableOrder(page);
await openOrderSettings(page, orderId);
- await page.getByTestId('pos-order-settings-department').selectOption('1');
- await expect(page.getByTestId('pos-order-settings-save')).toBeEnabled();
- await page.getByTestId('pos-order-settings-save').click();
- await expect(page.getByTestId('pos-order-settings-save')).toBeDisabled();
+ await page.getByTestId("pos-order-settings-department").selectOption("1");
+ await expect(page.getByTestId("pos-order-settings-save")).toBeEnabled();
+ await page.getByTestId("pos-order-settings-save").click();
+ await expect(page.getByTestId("pos-order-settings-save")).toBeDisabled();
await reloadOrderSettings(page, orderId);
- await expect(page.getByTestId('pos-order-settings-department')).toHaveValue('1');
+ await expect(page.getByTestId("pos-order-settings-department")).toHaveValue("1");
- await clickVisibleTestId(page, 'pos-order-tab-details');
- const detailInputs = page.getByTestId('pos-order-panel-details').locator('input');
- await expect(detailInputs.first()).toHaveValue('Taastrup');
+ await clickVisibleTestId(page, "pos-order-tab-details");
+ const detailInputs = page.getByTestId("pos-order-panel-details").locator("input");
+ await expect(detailInputs.first()).toHaveValue("Taastrup");
});
- test('persists created_at updates and shows the Buefy datetime display after reload', async ({ page }) => {
+ test("persists created_at updates and shows the Buefy datetime display after reload", async ({ page }) => {
const { orderId } = await createDisposableOrder(page);
- const updatedCreatedAt = '09/04/2026 13.37';
+ const updatedCreatedAt = "09/04/2026 13.37";
await openOrderSettings(page, orderId);
await getCreatedAtInput(page).fill(updatedCreatedAt);
- await getCreatedAtInput(page).press('Tab');
- await page.getByTestId('pos-order-settings-save').click();
- await expect(page.getByTestId('pos-order-settings-save')).toBeDisabled();
+ await getCreatedAtInput(page).press("Tab");
+ await page.getByTestId("pos-order-settings-save").click();
+ await expect(page.getByTestId("pos-order-settings-save")).toBeDisabled();
await reloadOrderSettings(page, orderId);
await expect(getCreatedAtInput(page)).toHaveValue(updatedCreatedAt);
- await clickVisibleTestId(page, 'pos-order-tab-details');
- const detailInputs = page.getByTestId('pos-order-panel-details').locator('input');
- await expect(detailInputs.nth(1)).toHaveValue('2026-04-09 13:37:00');
+ await clickVisibleTestId(page, "pos-order-tab-details");
+ const detailInputs = page.getByTestId("pos-order-panel-details").locator("input");
+ await expect(detailInputs.nth(1)).toHaveValue("2026-04-09 13:37:00");
});
- test('supports invoice inclusion override across inherited, excluded, and included states', async ({ page }) => {
+ test("supports invoice inclusion override across inherited, excluded, and included states", async ({ page }) => {
const { orderId } = await createDisposableOrder(page);
await openOrderSettings(page, orderId);
- const helper = page.getByTestId('pos-order-settings-include-helper');
- const initialHelper = ((await helper.textContent()) || '').trim();
- expect(initialHelper).not.toBe('');
- await expect(page.getByTestId('pos-order-settings-include-in-invoice')).toHaveValue('use_department');
+ const helper = page.getByTestId("pos-order-settings-include-helper");
+ const initialHelper = ((await helper.textContent()) || "").trim();
+ expect(initialHelper).not.toBe("");
+ await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toHaveValue("use_department");
- await page.getByTestId('pos-order-settings-include-in-invoice').selectOption('exclude');
- await page.getByTestId('pos-order-settings-save').click();
- await expect(page.getByTestId('pos-order-settings-save')).toBeDisabled();
+ await page.getByTestId("pos-order-settings-include-in-invoice").selectOption("exclude");
+ await page.getByTestId("pos-order-settings-save").click();
+ await expect(page.getByTestId("pos-order-settings-save")).toBeDisabled();
await reloadOrderSettings(page, orderId);
- await expect(page.getByTestId('pos-order-settings-include-in-invoice')).toHaveValue('exclude');
- const excludedHelper = ((await helper.textContent()) || '').trim();
- expect(excludedHelper).not.toBe('');
+ await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toHaveValue("exclude");
+ const excludedHelper = ((await helper.textContent()) || "").trim();
+ expect(excludedHelper).not.toBe("");
expect(excludedHelper).not.toBe(initialHelper);
- await page.getByTestId('pos-order-settings-include-in-invoice').selectOption('include');
- await page.getByTestId('pos-order-settings-save').click();
- await expect(page.getByTestId('pos-order-settings-save')).toBeDisabled();
+ await page.getByTestId("pos-order-settings-include-in-invoice").selectOption("include");
+ await page.getByTestId("pos-order-settings-save").click();
+ await expect(page.getByTestId("pos-order-settings-save")).toBeDisabled();
await reloadOrderSettings(page, orderId);
- await expect(page.getByTestId('pos-order-settings-include-in-invoice')).toHaveValue('include');
- const includedHelper = ((await helper.textContent()) || '').trim();
- expect(includedHelper).not.toBe('');
+ await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toHaveValue("include");
+ const includedHelper = ((await helper.textContent()) || "").trim();
+ expect(includedHelper).not.toBe("");
expect(includedHelper).not.toBe(excludedHelper);
- await page.getByTestId('pos-order-settings-department').selectOption('1');
- await page.getByTestId('pos-order-settings-include-in-invoice').selectOption('use_department');
- await page.getByTestId('pos-order-settings-save').click();
- await expect(page.getByTestId('pos-order-settings-save')).toBeDisabled();
+ await page.getByTestId("pos-order-settings-department").selectOption("1");
+ await page.getByTestId("pos-order-settings-include-in-invoice").selectOption("use_department");
+ await page.getByTestId("pos-order-settings-save").click();
+ await expect(page.getByTestId("pos-order-settings-save")).toBeDisabled();
await reloadOrderSettings(page, orderId);
- await expect(page.getByTestId('pos-order-settings-department')).toHaveValue('1');
- await expect(page.getByTestId('pos-order-settings-include-in-invoice')).toHaveValue('use_department');
- const inheritedExcludedHelper = ((await helper.textContent()) || '').trim();
- expect(inheritedExcludedHelper).not.toBe('');
+ await expect(page.getByTestId("pos-order-settings-department")).toHaveValue("1");
+ await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toHaveValue("use_department");
+ const inheritedExcludedHelper = ((await helper.textContent()) || "").trim();
+ expect(inheritedExcludedHelper).not.toBe("");
expect(inheritedExcludedHelper).not.toBe(includedHelper);
});
});
-test.describe('Admin POS Orders - desktop locked states', () => {
+test.describe("Admin POS Orders - desktop locked states", () => {
test.beforeEach(async ({ page }, testInfo) => {
- test.skip(!testInfo.project.name.includes('desktop'), 'Desktop-only order lock coverage');
+ test.skip(!testInfo.project.name.includes("desktop"), "Desktop-only order lock coverage");
});
- test('hides item mutation controls when edit permission is missing', async ({ page }) => {
- const limitedPermissions = POS_PERMISSIONS.filter((permission) => permission !== 'edit_order_items');
+ test("hides item mutation controls when edit permission is missing", async ({ page }) => {
+ const limitedPermissions = POS_PERMISSIONS.filter((permission) => permission !== "edit_order_items");
await mockApi(page, {
authenticated: true,
@@ -650,16 +705,16 @@ test.describe('Admin POS Orders - desktop locked states', () => {
edgeGateways: false,
pos: createPosFixture(),
});
- await primeOperatorSession(page, 'pos-orders-no-edit-token', limitedPermissions);
+ await primeOperatorSession(page, "pos-orders-no-edit-token", limitedPermissions);
await openOrderDetail(page);
- await expect(page.getByTestId('pos-order-item-edit-9101')).toHaveCount(0);
- await expect(page.getByTestId('pos-order-item-delete-9101')).toHaveCount(0);
- await expect(page.getByTestId('pos-order-add-item')).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-item-edit-9101")).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-item-delete-9101")).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-add-item")).toHaveCount(0);
await expectOrderTotal(page, 1372);
});
- test('hides item mutation controls when the order is already invoiced', async ({ page }) => {
+ test("hides item mutation controls when the order is already invoiced", async ({ page }) => {
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
@@ -673,19 +728,19 @@ test.describe('Admin POS Orders - desktop locked states', () => {
},
}),
});
- await primeOperatorSession(page, 'pos-orders-invoiced-token');
+ await primeOperatorSession(page, "pos-orders-invoiced-token");
await openOrderDetail(page);
- await expect(page.getByTestId('pos-order-item-edit-9101')).toHaveCount(0);
- await expect(page.getByTestId('pos-order-item-delete-9101')).toHaveCount(0);
- await expect(page.getByTestId('pos-order-add-item')).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-item-edit-9101")).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-item-delete-9101")).toHaveCount(0);
+ await expect(page.getByTestId("pos-order-add-item")).toHaveCount(0);
await expectOrderTotal(page, 1372);
});
});
-test.describe('Admin POS Orders - mobile smoke', () => {
+test.describe("Admin POS Orders - mobile smoke", () => {
test.beforeEach(async ({ page }, testInfo) => {
- test.skip(!testInfo.project.name.toLowerCase().includes('mobile'), 'Mobile-only order settings smoke');
+ test.skip(!testInfo.project.name.toLowerCase().includes("mobile"), "Mobile-only order settings smoke");
await mockApi(page, {
authenticated: true,
@@ -693,69 +748,69 @@ test.describe('Admin POS Orders - mobile smoke', () => {
edgeGateways: false,
pos: createPosFixture(),
});
- await primeOperatorSession(page, 'pos-orders-mobile-token');
+ await primeOperatorSession(page, "pos-orders-mobile-token");
});
- test('opens the settings tab and renders the new controls', async ({ page }) => {
- await page.goto('/admin/12/modules/pos/orders/54518');
- await expect(page.getByTestId('pos-order-detail')).toBeVisible();
+ test("opens the settings tab and renders the new controls", async ({ page }) => {
+ await page.goto("/admin/12/modules/pos/orders/54518");
+ await expect(page.getByTestId("pos-order-detail")).toBeVisible();
- await clickVisibleTestId(page, 'pos-order-tab-settings');
- await expect(page.getByTestId('pos-order-panel-settings')).toBeVisible();
- await expect(page.getByTestId('pos-order-settings-department')).toBeVisible();
- await expect(page.getByTestId('pos-order-settings-created-at')).toBeVisible();
- await expect(page.getByTestId('pos-order-settings-include-in-invoice')).toBeVisible();
+ await clickVisibleTestId(page, "pos-order-tab-settings");
+ await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
+ await expect(page.getByTestId("pos-order-settings-department")).toBeVisible();
+ await expect(page.getByTestId("pos-order-settings-created-at")).toBeVisible();
+ await expect(page.getByTestId("pos-order-settings-include-in-invoice")).toBeVisible();
});
- test('shows the wash certificate action in the attachments tab', async ({ page }) => {
+ test("shows the wash certificate action in the attachments tab", async ({ page }) => {
await openOrderAttachments(page);
await openAddAttachmentCard(page);
});
- test('keeps horizontal gutters around the mobile order tabs', async ({ page }) => {
+ test("keeps horizontal gutters around the mobile order tabs", async ({ page }) => {
await openOrderDetail(page);
- const noteEmptyState = page.getByTestId('pos-order-note-empty-state');
+ const noteEmptyState = page.getByTestId("pos-order-note-empty-state");
await expect(noteEmptyState).toBeVisible();
- await expect(noteEmptyState.locator('.pos-order-field__empty-pill')).toHaveText(/^\+\s+\S+/);
+ await expect(noteEmptyState.locator(".pos-order-field__empty-pill")).toHaveText(/^\+\s+\S+/);
- const tabsContainer = getVisibleTestId(page, 'pos-order-mobile-tabs');
- const cartTab = getVisibleTestId(page, 'pos-order-tab-cart');
+ const tabsContainer = getVisibleTestId(page, "pos-order-mobile-tabs");
+ const cartTab = getVisibleTestId(page, "pos-order-tab-cart");
const containerBox = await tabsContainer.boundingBox();
const cartTabBox = await cartTab.boundingBox();
expect(containerBox).not.toBeNull();
expect(cartTabBox).not.toBeNull();
expect(cartTabBox!.x - containerBox!.x).toBeGreaterThanOrEqual(10);
- expect((containerBox!.x + containerBox!.width) - (cartTabBox!.x + cartTabBox!.width)).toBeGreaterThanOrEqual(10);
+ expect(containerBox!.x + containerBox!.width - (cartTabBox!.x + cartTabBox!.width)).toBeGreaterThanOrEqual(10);
});
- test('supports mobile item edit, add-panel access, and delete control visibility', async ({ page }) => {
+ test("supports mobile item edit, add-panel access, and delete control visibility", async ({ page }) => {
await openOrderDetail(page);
await expectOrderTotal(page, 1372);
await openOrderItemEditModal(page, 9101);
- await page.getByTestId('pos-order-item-edit-quantity').fill('2');
+ await page.getByTestId("pos-order-item-edit-quantity").fill("2");
- const editRequest = waitForOrderItemMutation(page, 'PUT', '/order/items');
- await page.getByTestId('pos-order-item-edit-save').click();
+ const editRequest = waitForOrderItemMutation(page, "PUT", "/order/items");
+ await page.getByTestId("pos-order-item-edit-save").click();
await editRequest;
- await expect(page.getByTestId('pos-order-item-edit-modal')).toBeHidden();
+ await expect(page.getByTestId("pos-order-item-edit-modal")).toBeHidden();
await expectOrderTotal(page, 2021);
- await page.getByTestId('pos-order-add-item').click();
- await expect(page.getByTestId('pos-order-add-items-panel').first()).toBeVisible();
- await page.getByTestId('pos-order-item-delete-9101').first().scrollIntoViewIfNeeded();
- await expect(page.getByTestId('pos-order-item-delete-9101').first()).toBeVisible();
+ await page.getByTestId("pos-order-add-item").click();
+ await expect(page.getByTestId("pos-order-add-items-panel").first()).toBeVisible();
+ await page.getByTestId("pos-order-item-delete-9101").first().scrollIntoViewIfNeeded();
+ await expect(page.getByTestId("pos-order-item-delete-9101").first()).toBeVisible();
});
- test('keeps the mobile search and reload controls inline and shows a clear card boundary', async ({ page }) => {
- await page.goto('/admin/12/modules/pos/orders');
+ test("keeps the mobile search and reload controls inline and shows a clear card boundary", async ({ page }) => {
+ await page.goto("/admin/12/modules/pos/orders");
- const searchInput = page.getByTestId('pagination-search-input');
- const reloadActions = page.getByTestId('pagination-reload-actions');
- const orderCard = page.getByTestId('pos-order-list-card-54518');
+ const searchInput = page.getByTestId("pagination-search-input");
+ const reloadActions = page.getByTestId("pagination-reload-actions");
+ const orderCard = page.getByTestId("pos-order-list-card-54518");
await expect(searchInput).toBeVisible();
await expect(reloadActions).toBeVisible();
@@ -766,25 +821,25 @@ test.describe('Admin POS Orders - mobile smoke', () => {
expect(searchBox).not.toBeNull();
expect(reloadBox).not.toBeNull();
- const searchCenterY = (searchBox?.y ?? 0) + ((searchBox?.height ?? 0) / 2);
- const reloadCenterY = (reloadBox?.y ?? 0) + ((reloadBox?.height ?? 0) / 2);
+ const searchCenterY = (searchBox?.y ?? 0) + (searchBox?.height ?? 0) / 2;
+ const reloadCenterY = (reloadBox?.y ?? 0) + (reloadBox?.height ?? 0) / 2;
expect(Math.abs(searchCenterY - reloadCenterY)).toBeLessThanOrEqual(4);
- expect((searchBox?.x ?? 0)).toBeLessThan((reloadBox?.x ?? 0));
- expect(((searchBox?.x ?? 0) + (searchBox?.width ?? 0)) - (reloadBox?.x ?? 0)).toBeLessThanOrEqual(4);
- expect((searchBox?.width ?? 0)).toBeGreaterThan(reloadBox?.width ?? 0);
+ expect(searchBox?.x ?? 0).toBeLessThan(reloadBox?.x ?? 0);
+ expect((searchBox?.x ?? 0) + (searchBox?.width ?? 0) - (reloadBox?.x ?? 0)).toBeLessThanOrEqual(4);
+ expect(searchBox?.width ?? 0).toBeGreaterThan(reloadBox?.width ?? 0);
- await expect(orderCard).toHaveCSS('border-top-width', '1px');
- await expect(orderCard).toHaveCSS('border-right-width', '1px');
- await expect(orderCard).toHaveCSS('border-bottom-width', '1px');
- await expect(orderCard).toHaveCSS('border-left-width', '1px');
+ await expect(orderCard).toHaveCSS("border-top-width", "1px");
+ await expect(orderCard).toHaveCSS("border-right-width", "1px");
+ await expect(orderCard).toHaveCSS("border-bottom-width", "1px");
+ await expect(orderCard).toHaveCSS("border-left-width", "1px");
});
- test('stacks the add-items workspace on mobile and still lets the user add products', async ({ page }) => {
+ test("stacks the add-items workspace on mobile and still lets the user add products", async ({ page }) => {
await openOrderDetail(page);
- const main = getVisibleTestId(page, 'pos-order-main');
+ const main = getVisibleTestId(page, "pos-order-main");
const beforeMainBox = await main.boundingBox();
- const beforeRailBox = await getVisibleTestId(page, 'pos-order-rail').boundingBox();
+ const beforeRailBox = await getVisibleTestId(page, "pos-order-rail").boundingBox();
expect(beforeMainBox).not.toBeNull();
expect(beforeRailBox).not.toBeNull();
@@ -793,7 +848,7 @@ test.describe('Admin POS Orders - mobile smoke', () => {
const productPanel = page.locator('[data-testid="pos-order-add-items-panel"]:visible').first();
const cartPanel = page.locator('[data-testid="pos-order-panel-cart"]:visible').first();
- const workspace = getVisibleTestId(page, 'pos-order-workspace');
+ const workspace = getVisibleTestId(page, "pos-order-workspace");
const productPanelBox = await productPanel.boundingBox();
const cartPanelBox = await cartPanel.boundingBox();
const workspaceBox = await workspace.boundingBox();
@@ -805,30 +860,30 @@ test.describe('Admin POS Orders - mobile smoke', () => {
expect(Math.abs((productPanelBox?.width ?? 0) - (cartPanelBox?.width ?? 0))).toBeLessThanOrEqual(24);
expect(cartPanelBox?.y ?? 0).toBeGreaterThan((productPanelBox?.y ?? 0) + 100);
- await productPanel.locator('select').first().selectOption('8');
- await expect(productPanel.getByTestId('pos-product-card-64')).toBeVisible();
- await expect(productPanel.getByTestId('pos-product-card-53')).toHaveCount(0);
+ await productPanel.locator("select").first().selectOption("8");
+ await expect(productPanel.getByTestId("pos-product-card-64")).toBeVisible();
+ await expect(productPanel.getByTestId("pos-product-card-53")).toHaveCount(0);
- await productPanel.getByTestId('pos-product-card-64').click();
- const addRequest = waitForOrderItemMutation(page, 'POST', '/order/items');
- await productPanel.getByTestId('pos-add-to-cart-64').click();
+ await productPanel.getByTestId("pos-product-card-64").click();
+ const addRequest = waitForOrderItemMutation(page, "POST", "/order/items");
+ await productPanel.getByTestId("pos-add-to-cart-64").click();
await addRequest;
- await expect(getVisibleTestId(page, 'pos-order-item-edit-9200')).toBeVisible();
- await expect(getVisibleTestId(page, 'pos-order-item-name-9200')).toContainText('Vaskecertifikat');
+ await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
+ await expect(getVisibleTestId(page, "pos-order-item-name-9200")).toContainText("Vaskecertifikat");
await expectOrderTotal(page, 1397);
await page.locator('[data-testid="pos-order-add-items-back"]:visible').first().click();
await expect(page.locator('[data-testid="pos-order-add-items-panel"]:visible')).toHaveCount(0);
- await expect(getVisibleTestId(page, 'pos-order-rail')).toBeVisible();
+ await expect(getVisibleTestId(page, "pos-order-rail")).toBeVisible();
const restoredMainBox = await main.boundingBox();
expect(restoredMainBox).not.toBeNull();
expect(Math.abs((restoredMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0))).toBeLessThanOrEqual(20);
await page.reload();
- await expect(page.getByTestId('pos-order-detail')).toBeVisible();
- await expect(getVisibleTestId(page, 'pos-order-item-edit-9200')).toBeVisible();
+ await expect(page.getByTestId("pos-order-detail")).toBeVisible();
+ await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
await expectOrderTotal(page, 1397);
});
});
diff --git a/tests/e2e/adminModuleGoals.spec.ts b/tests/e2e/adminModuleGoals.spec.ts
index 1950d386..9fbfff03 100644
--- a/tests/e2e/adminModuleGoals.spec.ts
+++ b/tests/e2e/adminModuleGoals.spec.ts
@@ -1,5 +1,5 @@
-import { test, expect, Page } from '@playwright/test';
-import { bookingTestData, loginAsOperator } from './fixtures';
+import { test, expect, Page } from "@playwright/test";
+import { bookingTestData, loginAsOperator } from "./fixtures";
type GoalProgressBucket = {
count: number;
@@ -16,22 +16,22 @@ type MockGoal = {
departments: number[];
criteria: {
label: string;
- type: 'REVENUE' | 'PRODUCT' | 'VISITS';
+ type: "REVENUE" | "PRODUCT" | "VISITS";
target: number;
start: string;
end: string;
products: number[];
users: number[];
departments: number[];
- progress_alert_frequency: 'NONE' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'CHANGED';
- progress_alert_destination: 'NONE' | 'SLACK' | 'EMAIL' | 'SMS';
+ progress_alert_frequency: "NONE" | "DAILY" | "WEEKLY" | "MONTHLY" | "CHANGED";
+ progress_alert_destination: "NONE" | "SLACK" | "EMAIL" | "SMS";
progress_alert_progress_type: string;
progress_alert_style: string;
progress_alert_format: string | null;
progress_alert_weekdays: string[];
progress_alert_time_of_day: string | null;
department_daily_targets: Record;
- target_duration?: 'ENTIRE_DURATION' | 'WEEKS' | 'MONTHS' | 'YEARS' | null;
+ target_duration?: "ENTIRE_DURATION" | "WEEKS" | "MONTHS" | "YEARS" | null;
target_duration_every?: number | null;
};
progress: GoalProgressByKey & {
@@ -64,17 +64,17 @@ const makeMockGoal = (params: {
departments: [params.departmentId],
criteria: {
label: params.label,
- type: 'REVENUE',
+ type: "REVENUE",
target: params.target,
start: params.start,
end: params.end,
products: [],
users: [],
departments: [params.departmentId],
- progress_alert_frequency: 'NONE',
- progress_alert_destination: 'NONE',
- progress_alert_progress_type: 'ALL',
- progress_alert_style: 'NONE',
+ progress_alert_frequency: "NONE",
+ progress_alert_destination: "NONE",
+ progress_alert_progress_type: "ALL",
+ progress_alert_style: "NONE",
progress_alert_format: null,
progress_alert_weekdays: [],
progress_alert_time_of_day: null,
@@ -108,51 +108,51 @@ const setupGoalsApiMock = async (page: Page, departmentId: number) => {
makeMockGoal({
id: 1,
departmentId,
- label: 'Mock Active Goal',
+ label: "Mock Active Goal",
target: 100,
allCount: 45,
todayCount: 10,
weekCount: 28,
monthCount: 45,
- start: '2026-01-01',
- end: '2099-01-01',
+ start: "2026-01-01",
+ end: "2099-01-01",
}),
makeMockGoal({
id: 2,
departmentId,
- label: 'Mock Expired Goal',
+ label: "Mock Expired Goal",
target: 200,
allCount: 180,
todayCount: 0,
weekCount: 0,
monthCount: 0,
- start: '2024-01-01',
- end: '2024-02-01',
+ start: "2024-01-01",
+ end: "2024-02-01",
}),
];
- await page.route('**/goals/department/progress-alert/test**', async (route) => {
+ await page.route("**/goals/department/progress-alert/test**", async (route) => {
await route.fulfill({
status: 200,
- contentType: 'application/json',
- body: JSON.stringify({ data: { destination: 'EMAIL' } }),
+ contentType: "application/json",
+ body: JSON.stringify({ data: { destination: "EMAIL" } }),
});
});
- await page.route('**/goals/department**', async (route) => {
+ await page.route("**/goals/department**", async (route) => {
const request = route.request();
const method = request.method();
- if (method === 'GET') {
+ if (method === "GET") {
await route.fulfill({
status: 200,
- contentType: 'application/json',
+ contentType: "application/json",
body: JSON.stringify({ data: goals }),
});
return;
}
- if (method === 'PUT') {
+ if (method === "PUT") {
const body = request.postDataJSON() as { id: number; departments: number[]; criteria: Record };
const id = Number(body.id);
const departments = (body.departments || [departmentId]).map((x) => Number(x));
@@ -173,19 +173,19 @@ const setupGoalsApiMock = async (page: Page, departmentId: number) => {
await route.fulfill({
status: 200,
- contentType: 'application/json',
+ contentType: "application/json",
body: JSON.stringify({ data: goals.find((g) => g.id === id) || null }),
});
return;
}
- if (method === 'DELETE') {
- const id = Number(new URL(request.url()).searchParams.get('id'));
+ if (method === "DELETE") {
+ const id = Number(new URL(request.url()).searchParams.get("id"));
goals = goals.filter((goal) => goal.id !== id);
await route.fulfill({
status: 200,
- contentType: 'application/json',
+ contentType: "application/json",
body: JSON.stringify({ data: { success: true } }),
});
return;
@@ -193,36 +193,46 @@ const setupGoalsApiMock = async (page: Page, departmentId: number) => {
await route.fulfill({
status: 200,
- contentType: 'application/json',
+ contentType: "application/json",
body: JSON.stringify({ data: {} }),
});
});
};
-test.describe('Admin Module - Goals', () => {
+test.describe("Admin Module - Goals", () => {
const getGoalsUrl = (departmentId: string) => `/admin/${departmentId}/modules/goals`;
const openGoalAction = async (page: Page, label: string, actionLabel: string) => {
- const card = page.locator('.goal-card', { hasText: label }).first();
+ const card = page.locator(".goal-card", { hasText: label }).first();
await expect(card).toBeVisible({ timeout: 15000 });
- const dropdown = card.locator('.dropdown.is-hoverable').first();
+ const dropdown = card.locator(".dropdown.is-hoverable").first();
await dropdown.hover();
- await card.locator('.dropdown-item', { hasText: actionLabel }).click({ force: true });
+ await card.locator(".dropdown-item", { hasText: actionLabel }).click({ force: true });
};
const openCreateGoalModalProgrammatically = async (page: Page, departmentId: number) => {
- await page.evaluate(async ({ targetDepartmentId }) => {
- const modalModule = await import('/src/views/dashboards/departmentDashboard/modules/goals/functions/showGoalFormModal.js');
- void modalModule.showGoalCreateModal(null, { departments: [targetDepartmentId] });
- }, { targetDepartmentId: departmentId });
+ await page.evaluate(
+ async ({ targetDepartmentId }) => {
+ const modalModule = await import(
+ "/src/views/dashboards/departmentDashboard/modules/goals/functions/showGoalFormModal.js"
+ );
+ void modalModule.showGoalCreateModal(null, { departments: [targetDepartmentId] });
+ },
+ { targetDepartmentId: departmentId }
+ );
};
const openEditGoalModalProgrammatically = async (page: Page, goal: MockGoal) => {
- await page.evaluate(async ({ editableGoal }) => {
- const modalModule = await import('/src/views/dashboards/departmentDashboard/modules/goals/functions/showGoalFormModal.js');
- void modalModule.showGoalEditModal(null, editableGoal);
- }, { editableGoal: goal });
+ await page.evaluate(
+ async ({ editableGoal }) => {
+ const modalModule = await import(
+ "/src/views/dashboards/departmentDashboard/modules/goals/functions/showGoalFormModal.js"
+ );
+ void modalModule.showGoalEditModal(null, editableGoal);
+ },
+ { editableGoal: goal }
+ );
};
const overrideSessionPermissionsForNextReload = async (
@@ -235,32 +245,33 @@ test.describe('Admin Module - Goals', () => {
const removeSet = new Set((options.remove || []).map((permission) => String(permission)));
const addList = (options.add || []).map((permission) => String(permission));
- await page.route('**/auth/session', async (route) => {
+ await page.route("**/auth/session", async (route) => {
const response = await route.fetch();
const status = response.status();
const headers = response.headers();
const body = await response.json();
- const responseDataRoot = body && typeof body === 'object' ? body : {};
+ const responseDataRoot = body && typeof body === "object" ? body : {};
const nestedData = responseDataRoot?.data?.data;
const flatData = responseDataRoot?.data;
- const sessionPayload = (nestedData && typeof nestedData === 'object')
- ? nestedData
- : (flatData && typeof flatData === 'object' ? flatData : responseDataRoot);
+ const sessionPayload =
+ nestedData && typeof nestedData === "object"
+ ? nestedData
+ : flatData && typeof flatData === "object"
+ ? flatData
+ : responseDataRoot;
const currentPermissions = Array.isArray(sessionPayload?.permissions)
? sessionPayload.permissions.map((permission) => String(permission))
: [];
- const nextPermissions = Array.from(new Set(
- currentPermissions
- .filter((permission) => !removeSet.has(permission))
- .concat(addList)
- ));
+ const nextPermissions = Array.from(
+ new Set(currentPermissions.filter((permission) => !removeSet.has(permission)).concat(addList))
+ );
- if (nestedData && typeof nestedData === 'object') {
+ if (nestedData && typeof nestedData === "object") {
responseDataRoot.data.data.permissions = nextPermissions;
- } else if (flatData && typeof flatData === 'object') {
+ } else if (flatData && typeof flatData === "object") {
responseDataRoot.data.permissions = nextPermissions;
} else {
responseDataRoot.permissions = nextPermissions;
@@ -275,8 +286,11 @@ test.describe('Admin Module - Goals', () => {
};
test.beforeEach(async ({ page }, testInfo) => {
- test.skip(testInfo.project.name.toLowerCase().includes('mobile'), 'Goals admin suite is desktop-focused');
- test.skip(!testInfo.project.name.toLowerCase().includes('chromium'), 'Goals admin suite is stabilized for Chromium in this environment.');
+ test.skip(testInfo.project.name.toLowerCase().includes("mobile"), "Goals admin suite is desktop-focused");
+ test.skip(
+ !testInfo.project.name.toLowerCase().includes("chromium"),
+ "Goals admin suite is stabilized for Chromium in this environment."
+ );
await loginAsOperator(page);
@@ -285,43 +299,43 @@ test.describe('Admin Module - Goals', () => {
await page.goto(getGoalsUrl(String(departmentId)));
await expect(page).toHaveURL(new RegExp(`/admin/${departmentId}/modules/goals`));
- await expect(page.locator('nav.level.box')).toBeVisible({ timeout: 15000 });
+ await expect(page.locator("nav.level.box")).toBeVisible({ timeout: 15000 });
});
- test('loads goals page and supports status/timeframe tab switching', async ({ page }) => {
- const statusTabs = page.locator('.tabs.is-toggle').first();
- const timeframeTabs = page.locator('.tabs.is-toggle').nth(1);
+ test("loads goals page and supports status/timeframe tab switching", async ({ page }) => {
+ const statusTabs = page.locator(".tabs.is-toggle").first();
+ const timeframeTabs = page.locator(".tabs.is-toggle").nth(1);
- await expect(page.locator('.goal-card')).toHaveCount(1);
+ await expect(page.locator(".goal-card")).toHaveCount(1);
- await statusTabs.locator('li').nth(1).click();
- await expect(statusTabs.locator('li').nth(1)).toHaveClass(/is-active/);
- await expect(page.locator('.goal-card')).toHaveCount(1);
+ await statusTabs.locator("li").nth(1).click();
+ await expect(statusTabs.locator("li").nth(1)).toHaveClass(/is-active/);
+ await expect(page.locator(".goal-card")).toHaveCount(1);
- await statusTabs.locator('li').nth(2).click();
- await expect(statusTabs.locator('li').nth(2)).toHaveClass(/is-active/);
- await expect(page.locator('.goal-card')).toHaveCount(2);
+ await statusTabs.locator("li").nth(2).click();
+ await expect(statusTabs.locator("li").nth(2)).toHaveClass(/is-active/);
+ await expect(page.locator(".goal-card")).toHaveCount(2);
- await statusTabs.locator('li').nth(0).click();
- await expect(statusTabs.locator('li').nth(0)).toHaveClass(/is-active/);
+ await statusTabs.locator("li").nth(0).click();
+ await expect(statusTabs.locator("li").nth(0)).toHaveClass(/is-active/);
- await timeframeTabs.locator('li', { hasText: 'Indtil nu' }).click();
- await expect(timeframeTabs.locator('li', { hasText: 'Indtil nu' })).toHaveClass(/is-active/);
+ await timeframeTabs.locator("li", { hasText: "Indtil nu" }).click();
+ await expect(timeframeTabs.locator("li", { hasText: "Indtil nu" })).toHaveClass(/is-active/);
- await timeframeTabs.locator('li').nth(2).click();
- await expect(timeframeTabs.locator('li').nth(2)).toHaveClass(/is-active/);
+ await timeframeTabs.locator("li").nth(2).click();
+ await expect(timeframeTabs.locator("li").nth(2)).toHaveClass(/is-active/);
- await timeframeTabs.locator('li').nth(3).click();
- await expect(timeframeTabs.locator('li').nth(3)).toHaveClass(/is-active/);
+ await timeframeTabs.locator("li").nth(3).click();
+ await expect(timeframeTabs.locator("li").nth(3)).toHaveClass(/is-active/);
- await timeframeTabs.locator('li').nth(4).click();
- await expect(timeframeTabs.locator('li').nth(4)).toHaveClass(/is-active/);
+ await timeframeTabs.locator("li").nth(4).click();
+ await expect(timeframeTabs.locator("li").nth(4)).toHaveClass(/is-active/);
- await timeframeTabs.locator('li').nth(0).click();
- await expect(timeframeTabs.locator('li').nth(0)).toHaveClass(/is-active/);
+ await timeframeTabs.locator("li").nth(0).click();
+ await expect(timeframeTabs.locator("li").nth(0)).toHaveClass(/is-active/);
});
- test('updates progress target when timeframe changes for cadence goals', async ({ page }) => {
+ test("updates progress target when timeframe changes for cadence goals", async ({ page }) => {
const departmentId = bookingTestData.departmentId;
const nowIso = new Date().toISOString();
@@ -330,33 +344,38 @@ test.describe('Admin Module - Goals', () => {
created_by: 11,
departments: [departmentId],
criteria: {
- label: 'Cadence Biweekly Goal',
- type: 'PRODUCT',
+ label: "Cadence Biweekly Goal",
+ type: "PRODUCT",
target: 7,
- start: '2026-03-24T00:00:00+01:00',
- end: '2026-07-24T23:59:59+01:00',
+ start: "2026-03-24T00:00:00+01:00",
+ end: "2026-07-24T23:59:59+01:00",
products: [1],
users: [],
departments: [departmentId],
- progress_alert_frequency: 'NONE',
- progress_alert_destination: 'NONE',
- progress_alert_progress_type: 'ALL',
- progress_alert_style: 'NONE',
+ progress_alert_frequency: "NONE",
+ progress_alert_destination: "NONE",
+ progress_alert_progress_type: "ALL",
+ progress_alert_style: "NONE",
progress_alert_format: null,
- progress_alert_weekdays: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'],
+ progress_alert_weekdays: ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"],
progress_alert_time_of_day: null,
department_daily_targets: {},
- target_duration: 'WEEKS',
+ target_duration: "WEEKS",
target_duration_every: 2,
},
progress: {
- all: { count: 11, target: 63, date_from: '2026-03-24T00:00:00+01:00', date_end: '2026-07-24T23:59:59+01:00' },
- to_date: { count: 6, target: 7, date_from: '2026-03-24T00:00:00+01:00', date_end: '2026-03-24T23:59:59+01:00' },
- today: { count: 1, target: 2, date_from: '2026-04-04T00:00:00+01:00', date_end: '2026-04-04T23:59:59+01:00' },
- week: { count: 4, target: 7, date_from: '2026-03-23T00:00:00+01:00', date_end: '2026-03-29T23:59:59+01:00' },
- month: { count: 9, target: 7, date_from: '2026-04-01T00:00:00+01:00', date_end: '2026-04-30T23:59:59+01:00' },
- year: { count: 20, target: 7, date_from: '2026-01-01T00:00:00+01:00', date_end: '2026-12-31T23:59:59+01:00' },
- every_2_weeks: { count: 5, target: 14, date_from: '2026-03-12T00:00:00+01:00', date_end: '2026-03-25T23:59:59+01:00' },
+ all: { count: 11, target: 63, date_from: "2026-03-24T00:00:00+01:00", date_end: "2026-07-24T23:59:59+01:00" },
+ to_date: { count: 6, target: 7, date_from: "2026-03-24T00:00:00+01:00", date_end: "2026-03-24T23:59:59+01:00" },
+ today: { count: 1, target: 2, date_from: "2026-04-04T00:00:00+01:00", date_end: "2026-04-04T23:59:59+01:00" },
+ week: { count: 4, target: 7, date_from: "2026-03-23T00:00:00+01:00", date_end: "2026-03-29T23:59:59+01:00" },
+ month: { count: 9, target: 7, date_from: "2026-04-01T00:00:00+01:00", date_end: "2026-04-30T23:59:59+01:00" },
+ year: { count: 20, target: 7, date_from: "2026-01-01T00:00:00+01:00", date_end: "2026-12-31T23:59:59+01:00" },
+ every_2_weeks: {
+ count: 5,
+ target: 14,
+ date_from: "2026-03-12T00:00:00+01:00",
+ date_end: "2026-03-25T23:59:59+01:00",
+ },
departmental_distribution: {
[String(departmentId)]: {
all: { count: 11, target: 63 },
@@ -373,13 +392,13 @@ test.describe('Admin Module - Goals', () => {
updated_at: nowIso,
};
- await page.unroute('**/goals/department**');
- await page.route('**/goals/department**', async (route) => {
+ await page.unroute("**/goals/department**");
+ await page.route("**/goals/department**", async (route) => {
const method = route.request().method();
- if (method === 'GET') {
+ if (method === "GET") {
await route.fulfill({
status: 200,
- contentType: 'application/json',
+ contentType: "application/json",
body: JSON.stringify({ data: [cadenceGoal] }),
});
return;
@@ -387,90 +406,94 @@ test.describe('Admin Module - Goals', () => {
await route.fulfill({
status: 200,
- contentType: 'application/json',
+ contentType: "application/json",
body: JSON.stringify({ data: {} }),
});
});
await page.reload();
- const timeframeTabs = page.locator('.tabs.is-toggle').nth(1);
- const card = page.locator('.goal-card', { hasText: 'Cadence Biweekly Goal' }).first();
- const progressBar = card.locator('progress.progress').first();
+ const timeframeTabs = page.locator(".tabs.is-toggle").nth(1);
+ const card = page.locator(".goal-card", { hasText: "Cadence Biweekly Goal" }).first();
+ const progressBar = card.locator("progress.progress").first();
const expectProgress = async (value: number, max: number) => {
- await expect(progressBar).toHaveAttribute('value', new RegExp(`^${value}(?:\\.0+)?$`));
- await expect(progressBar).toHaveAttribute('max', new RegExp(`^${max}(?:\\.0+)?$`));
+ await expect(progressBar).toHaveAttribute("value", new RegExp(`^${value}(?:\\.0+)?$`));
+ await expect(progressBar).toHaveAttribute("max", new RegExp(`^${max}(?:\\.0+)?$`));
};
await expectProgress(11, 63);
- await timeframeTabs.locator('li').nth(1).click(); // Indtil nu
- await expect(timeframeTabs.locator('li').nth(1)).toHaveClass(/is-active/);
+ await timeframeTabs.locator("li").nth(1).click(); // Indtil nu
+ await expect(timeframeTabs.locator("li").nth(1)).toHaveClass(/is-active/);
await expectProgress(6, 7);
- await timeframeTabs.locator('li').nth(2).click(); // I dag
- await expect(timeframeTabs.locator('li').nth(2)).toHaveClass(/is-active/);
+ await timeframeTabs.locator("li").nth(2).click(); // I dag
+ await expect(timeframeTabs.locator("li").nth(2)).toHaveClass(/is-active/);
await expectProgress(1, 2);
- await timeframeTabs.locator('li').nth(3).click(); // Denne uge
- await expect(timeframeTabs.locator('li').nth(3)).toHaveClass(/is-active/);
+ await timeframeTabs.locator("li").nth(3).click(); // Denne uge
+ await expect(timeframeTabs.locator("li").nth(3)).toHaveClass(/is-active/);
await expectProgress(5, 14);
- await timeframeTabs.locator('li').nth(4).click(); // Denne måned
- await expect(timeframeTabs.locator('li').nth(4)).toHaveClass(/is-active/);
+ await timeframeTabs.locator("li").nth(4).click(); // Denne måned
+ await expect(timeframeTabs.locator("li").nth(4)).toHaveClass(/is-active/);
await expectProgress(9, 7);
- await timeframeTabs.locator('li').nth(5).click(); // År
- await expect(timeframeTabs.locator('li').nth(5)).toHaveClass(/is-active/);
+ await timeframeTabs.locator("li").nth(5).click(); // År
+ await expect(timeframeTabs.locator("li").nth(5)).toHaveClass(/is-active/);
await expectProgress(20, 63);
});
- test('opens create goal modal and supports cancel', async ({ page }) => {
+ test("opens create goal modal and supports cancel", async ({ page }) => {
await openCreateGoalModalProgrammatically(page, bookingTestData.departmentId);
- const modal = page.locator('.swal2-popup');
+ const modal = page.locator(".swal2-popup");
await expect(modal).toBeVisible({ timeout: 15000 });
await expect(modal.locator('input[placeholder*="f.eks."]').first()).toBeVisible();
await expect(modal.locator('input[type="number"]').first()).toBeVisible();
await expect(modal.locator('input[type="date"]').first()).toBeVisible();
- await modal.locator('.tabs li', { hasText: 'Alert Configuration' }).click();
- await expect(modal.locator('textarea').first()).toBeVisible();
+ await modal.locator(".tabs li", { hasText: "Alert Configuration" }).click();
+ await expect(modal.locator("textarea").first()).toBeVisible();
await modal.locator('button.button:has-text("Annuller")').click();
- await expect(page.locator('.swal2-popup')).toBeHidden({ timeout: 10000 });
+ await expect(page.locator(".swal2-popup")).toBeHidden({ timeout: 10000 });
});
- test('submits edit modal and sends expected update payload', async ({ page }) => {
+ test("submits edit modal and sends expected update payload", async ({ page }) => {
const editableGoal = makeMockGoal({
id: 1,
departmentId: bookingTestData.departmentId,
- label: 'Mock Active Goal',
+ label: "Mock Active Goal",
target: 100,
allCount: 45,
todayCount: 10,
weekCount: 28,
monthCount: 45,
- start: '2026-01-01',
- end: '2099-01-01',
+ start: "2026-01-01",
+ end: "2099-01-01",
});
await openEditGoalModalProgrammatically(page, editableGoal);
- const modal = page.locator('.swal2-popup');
+ const modal = page.locator(".swal2-popup");
await expect(modal).toBeVisible({ timeout: 15000 });
- await modal.locator('input[placeholder*="f.eks."]').first().fill('Mock Active Goal Updated');
- await modal.locator('input[type="number"]').first().fill('321');
- await modal.locator('input[type="date"]').first().fill('2026-02-01');
- await modal.locator('input[type="date"]').nth(1).fill('2026-03-15');
- await modal.locator('select').filter({ has: page.locator('option[value="LEGACY"]') }).first().selectOption('WEEKS');
- await modal.locator('input[type="number"]').nth(1).fill('2');
+ await modal.locator('input[placeholder*="f.eks."]').first().fill("Mock Active Goal Updated");
+ await modal.locator('input[type="number"]').first().fill("321");
+ await modal.locator('input[type="date"]').first().fill("2026-02-01");
+ await modal.locator('input[type="date"]').nth(1).fill("2026-03-15");
+ await modal
+ .locator("select")
+ .filter({ has: page.locator('option[value="LEGACY"]') })
+ .first()
+ .selectOption("WEEKS");
+ await modal.locator('input[type="number"]').nth(1).fill("2");
- const putRequestPromise = page.waitForRequest((request) =>
- request.method() === 'PUT' && request.url().includes('/goals/department')
+ const putRequestPromise = page.waitForRequest(
+ (request) => request.method() === "PUT" && request.url().includes("/goals/department")
);
- await modal.locator('.buttons.is-right .button.is-dark').click({ force: true });
+ await modal.locator(".buttons.is-right .button.is-dark").click({ force: true });
const putRequest = await putRequestPromise;
const putPayload = putRequest.postDataJSON() as {
@@ -484,9 +507,9 @@ test.describe('Admin Module - Goals', () => {
};
};
- expect(putPayload.criteria?.label).toBe('Mock Active Goal Updated');
+ expect(putPayload.criteria?.label).toBe("Mock Active Goal Updated");
expect(Number(putPayload.criteria?.target)).toBe(321);
- expect(putPayload.criteria?.target_duration).toBe('WEEKS');
+ expect(putPayload.criteria?.target_duration).toBe("WEEKS");
expect(Number(putPayload.criteria?.target_duration_every)).toBe(2);
expect(putPayload.criteria?.start).toMatch(/^2026-02-01T00:00:00(?:Z|[+-]\d{2}:\d{2})$/);
expect(putPayload.criteria?.end).toMatch(/^2026-03-15T23:59:59(?:Z|[+-]\d{2}:\d{2})$/);
@@ -496,24 +519,24 @@ test.describe('Admin Module - Goals', () => {
await expect(successDialog).toBeHidden({ timeout: 15000 });
});
- test('supports test-alert action and delete request flow', async ({ page }) => {
+ test("supports test-alert action and delete request flow", async ({ page }) => {
await overrideSessionPermissionsForNextReload(page, {
- add: ['goals_department_progress_alert_test', 'admin', `department_access_${bookingTestData.departmentId}`],
+ add: ["goals_department_progress_alert_test", "admin", `department_access_${bookingTestData.departmentId}`],
});
await page.reload();
- await expect(page.locator('nav.level.box')).toBeVisible({ timeout: 15000 });
+ await expect(page.locator("nav.level.box")).toBeVisible({ timeout: 15000 });
- const testAlertRequestPromise = page.waitForRequest((request) =>
- request.method() === 'POST' && request.url().includes('/goals/department/progress-alert/test')
+ const testAlertRequestPromise = page.waitForRequest(
+ (request) => request.method() === "POST" && request.url().includes("/goals/department/progress-alert/test")
);
- await openGoalAction(page, 'Mock Active Goal', 'Test alert');
- const modal = page.locator('.swal2-popup');
+ await openGoalAction(page, "Mock Active Goal", "Test alert");
+ const modal = page.locator(".swal2-popup");
await expect(modal).toBeVisible({ timeout: 10000 });
- await modal.locator('#goal-test-alert-destination').selectOption('EMAIL');
- await modal.locator('#goal-test-alert-email-to').fill('tester@example.com');
- await modal.locator('#goal-test-alert-subject').fill('Dept Goal Progress Test');
- await modal.locator('.swal2-confirm').click();
+ await modal.locator("#goal-test-alert-destination").selectOption("EMAIL");
+ await modal.locator("#goal-test-alert-email-to").fill("tester@example.com");
+ await modal.locator("#goal-test-alert-subject").fill("Dept Goal Progress Test");
+ await modal.locator(".swal2-confirm").click();
const testAlertRequest = await testAlertRequestPromise;
const testAlertPayload = testAlertRequest.postDataJSON() as {
@@ -524,49 +547,50 @@ test.describe('Admin Module - Goals', () => {
};
expect(Number(testAlertPayload.id)).toBe(1);
- expect(testAlertPayload.overrideDestination).toBe('EMAIL');
- expect(testAlertPayload.email_to).toBe('tester@example.com');
- expect(testAlertPayload.subject).toBe('Dept Goal Progress Test');
+ expect(testAlertPayload.overrideDestination).toBe("EMAIL");
+ expect(testAlertPayload.email_to).toBe("tester@example.com");
+ expect(testAlertPayload.subject).toBe("Dept Goal Progress Test");
- await openGoalAction(page, 'Mock Active Goal', 'Slet');
- await expect(page.locator('.swal2-popup')).toBeVisible({ timeout: 10000 });
+ await openGoalAction(page, "Mock Active Goal", "Slet");
+ await expect(page.locator(".swal2-popup")).toBeVisible({ timeout: 10000 });
- const deleteRequestPromise = page.waitForRequest((request) =>
- request.method() === 'DELETE' && request.url().includes('/goals/department')
+ const deleteRequestPromise = page.waitForRequest(
+ (request) => request.method() === "DELETE" && request.url().includes("/goals/department")
);
- await page.locator('.swal2-popup .swal2-confirm').click();
+ await page.locator(".swal2-popup .swal2-confirm").click();
const deleteRequest = await deleteRequestPromise;
- expect(deleteRequest.url()).toContain('id=');
- await expect(page.locator('.swal2-popup')).toBeHidden({ timeout: 10000 });
+ expect(deleteRequest.url()).toContain("id=");
+ await expect(page.locator(".swal2-popup")).toBeHidden({ timeout: 10000 });
});
- test('hides create and test-alert controls when permission is missing and reports COMPONENT entries', async ({ page }) => {
+ test("hides create and test-alert controls when permission is missing and reports COMPONENT entries", async ({
+ page,
+ }) => {
await overrideSessionPermissionsForNextReload(page, {
- remove: ['goals_department_create', 'goals_department_progress_alert_test', 'superuser'],
- add: ['admin', `department_access_${bookingTestData.departmentId}`],
+ remove: ["goals_department_create", "goals_department_progress_alert_test", "superuser"],
+ add: ["admin", `department_access_${bookingTestData.departmentId}`],
});
await page.reload();
- await expect(page.locator('nav.level.box')).toBeVisible({ timeout: 15000 });
+ await expect(page.locator("nav.level.box")).toBeVisible({ timeout: 15000 });
- await expect(page.locator('button.button.is-dark:has(.fa-plus)')).toHaveCount(0);
- await expect(page.locator('button.button.is-dark.mt-2')).toHaveCount(0);
+ await expect(page.locator("button.button.is-dark:has(.fa-plus)")).toHaveCount(0);
+ await expect(page.locator("button.button.is-dark.mt-2")).toHaveCount(0);
- const card = page.locator('.goal-card', { hasText: 'Mock Active Goal' }).first();
+ const card = page.locator(".goal-card", { hasText: "Mock Active Goal" }).first();
await expect(card).toBeVisible({ timeout: 15000 });
- await card.locator('.dropdown.is-hoverable').first().hover();
- await expect(card.locator('.dropdown-item', { hasText: 'Test alert' })).toHaveCount(0);
+ await card.locator(".dropdown.is-hoverable").first().hover();
+ await expect(card.locator(".dropdown-item", { hasText: "Test alert" })).toHaveCount(0);
- await page.keyboard.press('Shift');
- await page.keyboard.press('Shift');
- await page.keyboard.press('Shift');
+ await page.keyboard.press("Shift");
+ await page.keyboard.press("Shift");
+ await page.keyboard.press("Shift");
const missingPermissionsBox = page.locator("[data-testid='request-queue-missing-permissions-box']");
await expect(missingPermissionsBox).toBeVisible({ timeout: 10000 });
- await expect(missingPermissionsBox).toContainText('goals_department_create');
- await expect(missingPermissionsBox).toContainText('goals_department_progress_alert_test');
- await expect(missingPermissionsBox).toContainText('COMPONENT');
+ await expect(missingPermissionsBox).toContainText("goals_department_create");
+ await expect(missingPermissionsBox).toContainText("goals_department_progress_alert_test");
+ await expect(missingPermissionsBox).toContainText("COMPONENT");
});
});
-
diff --git a/tests/e2e/change-invoice-collection.spec.ts b/tests/e2e/change-invoice-collection.spec.ts
index d3683c34..3adcacd5 100644
--- a/tests/e2e/change-invoice-collection.spec.ts
+++ b/tests/e2e/change-invoice-collection.spec.ts
@@ -88,8 +88,6 @@ test.describe("POS order actions", () => {
await selectButtons.first().click();
await expect(page).toHaveURL(beforeUrl);
- await expect(page.locator(".swal2-container, .swal2-popup")).toContainText(
- /Faktura samling|invoice collection/i
- );
+ await expect(page.locator(".swal2-container, .swal2-popup")).toContainText(/Faktura samling|invoice collection/i);
});
});
diff --git a/tests/e2e/connectivityIssue.spec.ts b/tests/e2e/connectivityIssue.spec.ts
index 4d858ce8..1fae9f02 100644
--- a/tests/e2e/connectivityIssue.spec.ts
+++ b/tests/e2e/connectivityIssue.spec.ts
@@ -1,34 +1,38 @@
-import { test, expect } from '@playwright/test';
+import { test, expect } from "@playwright/test";
/** Connectivity Issue Tests */
-test('[PAGES][ConnectivityIssue][/connectivity-issue] should display the title', async ({ page }) => {
- await page.route('http://localhost/api/ping', route => route.fulfill({ status: 500 }));
- await page.goto('/connectivity-issue');
- await expect(page.locator('.title')).toContainText('Forbindelsesproblem');
+test("[PAGES][ConnectivityIssue][/connectivity-issue] should display the title", async ({ page }) => {
+ await page.route("http://localhost/api/ping", (route) => route.fulfill({ status: 500 }));
+ await page.goto("/connectivity-issue");
+ await expect(page.locator(".title")).toContainText("Forbindelsesproblem");
});
-test('[PAGES][ConnectivityIssue] should display server down state', async ({ page }) => {
- await page.route('http://localhost/api/ping', route => route.fulfill({ status: 500 }));
- await page.goto('/connectivity-issue');
- await expect(page.locator('i.fas.fa-server.fa-3x.has-text-danger')).toBeVisible();
- await expect(page.locator('.subtitle')).toContainText('Ingen forbindelse til serveren.');
- await expect(page.locator('progress.progress.is-large')).toBeVisible();
- await expect(page.locator('.content.mt-6')).toContainText('Dette løser sig som regel selv. Vent venligst, mens vi tjekker serveren.');
- await expect(page.locator('.is-size-7.has-text-grey')).toContainText('Hvis problemet fortsætter, kontakt venligst support.');
+test("[PAGES][ConnectivityIssue] should display server down state", async ({ page }) => {
+ await page.route("http://localhost/api/ping", (route) => route.fulfill({ status: 500 }));
+ await page.goto("/connectivity-issue");
+ await expect(page.locator("i.fas.fa-server.fa-3x.has-text-danger")).toBeVisible();
+ await expect(page.locator(".subtitle")).toContainText("Ingen forbindelse til serveren.");
+ await expect(page.locator("progress.progress.is-large")).toBeVisible();
+ await expect(page.locator(".content.mt-6")).toContainText(
+ "Dette løser sig som regel selv. Vent venligst, mens vi tjekker serveren."
+ );
+ await expect(page.locator(".is-size-7.has-text-grey")).toContainText(
+ "Hvis problemet fortsætter, kontakt venligst support."
+ );
});
-test('[ConnectivityIssue] should switch to no internet state on offline event', async ({ page }) => {
- await page.route('http://localhost/api/ping', route => route.fulfill({ status: 500 }));
- await page.goto('/connectivity-issue');
+test("[ConnectivityIssue] should switch to no internet state on offline event", async ({ page }) => {
+ await page.route("http://localhost/api/ping", (route) => route.fulfill({ status: 500 }));
+ await page.goto("/connectivity-issue");
// initial server_down
- await expect(page.locator('i.fas.fa-server')).toBeVisible();
- await page.evaluate(() => window.dispatchEvent(new Event('offline')));
+ await expect(page.locator("i.fas.fa-server")).toBeVisible();
+ await page.evaluate(() => window.dispatchEvent(new Event("offline")));
await page.waitForTimeout(1000);
- await expect(page.locator('i.fas.fa-wifi-slash')).toBeVisible();
- await expect(page.locator('.subtitle')).toContainText('Ingen internetforbindelse.');
+ await expect(page.locator("i.fas.fa-wifi-slash")).toBeVisible();
+ await expect(page.locator(".subtitle")).toContainText("Ingen internetforbindelse.");
});
-test('[ConnectivityIssue] should not redirect when status ok', async ({ page }) => {
+test("[ConnectivityIssue] should not redirect when status ok", async ({ page }) => {
// no mock, assume ping works, but wait no redirect? Wait, it does redirect.
// Alternative: mock ping success after load? But for now skip.
});
diff --git a/tests/e2e/economic-queue-workflow.spec.js b/tests/e2e/economic-queue-workflow.spec.js
index 29f041a6..2abc8df3 100644
--- a/tests/e2e/economic-queue-workflow.spec.js
+++ b/tests/e2e/economic-queue-workflow.spec.js
@@ -28,7 +28,8 @@ async function suppressVueDevtoolsOverlay(page) {
if (!document.getElementById(STYLE_ID)) {
const style = document.createElement("style");
style.id = STYLE_ID;
- style.textContent = "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
+ style.textContent =
+ "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
(document.head || document.documentElement).appendChild(style);
}
@@ -65,16 +66,21 @@ test.describe("Economic queue async export workflow", () => {
return;
}
- await route.fulfill(json({
- data: {
- job: {
- id: 1101,
- status: "QUEUED",
- progress_percent: 0,
- progress_message: "Queued export",
+ await route.fulfill(
+ json(
+ {
+ data: {
+ job: {
+ id: 1101,
+ status: "QUEUED",
+ progress_percent: 0,
+ progress_message: "Queued export",
+ },
+ },
},
- },
- }, 202));
+ 202
+ )
+ );
});
await page.route("**/economic/invoice/export/status**", async (route) => {
@@ -85,28 +91,32 @@ test.describe("Economic queue async export workflow", () => {
statusCalls += 1;
if (statusCalls === 1) {
- await route.fulfill(json({
- data: {
- id: 1101,
- status: "PROCESSING",
- progress_percent: 40,
- progress_message: "Preparing invoice payload",
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 1101,
+ status: "PROCESSING",
+ progress_percent: 40,
+ progress_message: "Preparing invoice payload",
+ },
+ })
+ );
return;
}
- await route.fulfill(json({
- data: {
- id: 1101,
- status: "COMPLETED",
- progress_percent: 100,
- progress_message: "Completed",
- result: {
- message: "Invoice export completed by queue worker.",
+ await route.fulfill(
+ json({
+ data: {
+ id: 1101,
+ status: "COMPLETED",
+ progress_percent: 100,
+ progress_message: "Completed",
+ result: {
+ message: "Invoice export completed by queue worker.",
+ },
},
- },
- }));
+ })
+ );
});
await openHarness(page);
@@ -116,7 +126,9 @@ test.describe("Economic queue async export workflow", () => {
await expect(submitButton).toBeDisabled();
await expect(page.getByTestId("economic-invoice-export-progress")).toContainText("Preparing invoice payload");
- await expect(page.getByTestId("economic-invoice-export-completed")).toContainText("Invoice export completed by queue worker.");
+ await expect(page.getByTestId("economic-invoice-export-completed")).toContainText(
+ "Invoice export completed by queue worker."
+ );
await expect(page.getByTestId("economic-invoice-export-completed")).toBeVisible();
const terminalCallCount = statusCalls;
@@ -134,16 +146,21 @@ test.describe("Economic queue async export workflow", () => {
return;
}
- await route.fulfill(json({
- data: {
- job: {
- id: 1201,
- status: "QUEUED",
- progress_percent: 0,
- progress_message: "Queued export",
+ await route.fulfill(
+ json(
+ {
+ data: {
+ job: {
+ id: 1201,
+ status: "QUEUED",
+ progress_percent: 0,
+ progress_message: "Queued export",
+ },
+ },
},
- },
- }, 202));
+ 202
+ )
+ );
});
await page.route("**/economic/invoice/export/retry", async (route) => {
@@ -153,16 +170,21 @@ test.describe("Economic queue async export workflow", () => {
}
phase = "retry";
- await route.fulfill(json({
- data: {
- job: {
- id: 1201,
- status: "QUEUED",
- progress_percent: 0,
- progress_message: "Retry queued",
+ await route.fulfill(
+ json(
+ {
+ data: {
+ job: {
+ id: 1201,
+ status: "QUEUED",
+ progress_percent: 0,
+ progress_message: "Retry queued",
+ },
+ },
},
- },
- }, 200));
+ 200
+ )
+ );
});
await page.route("**/economic/invoice/export/status**", async (route) => {
@@ -172,52 +194,62 @@ test.describe("Economic queue async export workflow", () => {
}
if (phase === "initial") {
- await route.fulfill(json({
- data: {
- id: 1201,
- status: "FAILED",
- progress_percent: 10,
- progress_message: "Failed",
- error_message: "no billable order items after zero-price/zero-quantity skips",
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 1201,
+ status: "FAILED",
+ progress_percent: 10,
+ progress_message: "Failed",
+ error_message: "no billable order items after zero-price/zero-quantity skips",
+ },
+ })
+ );
return;
}
retryStatusCalls += 1;
if (retryStatusCalls === 1) {
- await route.fulfill(json({
- data: {
- id: 1201,
- status: "PROCESSING",
- progress_percent: 70,
- progress_message: "Retry processing",
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 1201,
+ status: "PROCESSING",
+ progress_percent: 70,
+ progress_message: "Retry processing",
+ },
+ })
+ );
return;
}
- await route.fulfill(json({
- data: {
- id: 1201,
- status: "COMPLETED",
- progress_percent: 100,
- progress_message: "Retry completed",
- result: {
- message: "Invoice export completed after retry.",
+ await route.fulfill(
+ json({
+ data: {
+ id: 1201,
+ status: "COMPLETED",
+ progress_percent: 100,
+ progress_message: "Retry completed",
+ result: {
+ message: "Invoice export completed after retry.",
+ },
},
- },
- }));
+ })
+ );
});
await openHarness(page);
await page.getByTestId("economic-invoice-export-submit").click();
- await expect(page.getByTestId("economic-invoice-export-failed")).toContainText("no billable order items after zero-price/zero-quantity skips");
+ await expect(page.getByTestId("economic-invoice-export-failed")).toContainText(
+ "no billable order items after zero-price/zero-quantity skips"
+ );
await page.getByTestId("economic-invoice-export-retry").click();
await expect(page.getByTestId("economic-invoice-export-progress")).toContainText("Retry processing");
- await expect(page.getByTestId("economic-invoice-export-completed")).toContainText("Invoice export completed after retry.");
+ await expect(page.getByTestId("economic-invoice-export-completed")).toContainText(
+ "Invoice export completed after retry."
+ );
});
test("draft export surfaces API transport errors separately from queue failure state", async ({ page }) => {
@@ -252,16 +284,21 @@ test.describe("Economic queue async export workflow", () => {
send_as_is: false,
});
- await route.fulfill(json({
- data: {
- job: {
- id: 1301,
- status: "QUEUED",
- progress_percent: 0,
- progress_message: "Collected invoice queued",
+ await route.fulfill(
+ json(
+ {
+ data: {
+ job: {
+ id: 1301,
+ status: "QUEUED",
+ progress_percent: 0,
+ progress_message: "Collected invoice queued",
+ },
+ },
},
- },
- }, 202));
+ 202
+ )
+ );
});
await page.route("**/collected-invoices/economic/queue/status**", async (route) => {
@@ -272,28 +309,32 @@ test.describe("Economic queue async export workflow", () => {
statusCalls += 1;
if (statusCalls === 1) {
- await route.fulfill(json({
- data: {
- id: 1301,
- status: "PROCESSING",
- progress_percent: 55,
- progress_message: "Building e-conomic payload",
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 1301,
+ status: "PROCESSING",
+ progress_percent: 55,
+ progress_message: "Building e-conomic payload",
+ },
+ })
+ );
return;
}
- await route.fulfill(json({
- data: {
- id: 1301,
- status: "COMPLETED",
- progress_percent: 100,
- progress_message: "Completed",
- result: {
- message: "Collected invoice exported.",
+ await route.fulfill(
+ json({
+ data: {
+ id: 1301,
+ status: "COMPLETED",
+ progress_percent: 100,
+ progress_message: "Completed",
+ result: {
+ message: "Collected invoice exported.",
+ },
},
- },
- }));
+ })
+ );
});
await openHarness(page);
@@ -312,17 +353,22 @@ test.describe("Economic queue async export workflow", () => {
return;
}
- await route.fulfill(json({
- data: {
- message: "Queued collected invoice export",
- job_id: 1302,
- job: {
- status: "QUEUED",
- progress_percent: 0,
- progress_message: "Collected invoice queued",
+ await route.fulfill(
+ json(
+ {
+ data: {
+ message: "Queued collected invoice export",
+ job_id: 1302,
+ job: {
+ status: "QUEUED",
+ progress_percent: 0,
+ progress_message: "Collected invoice queued",
+ },
+ },
},
- },
- }, 202));
+ 202
+ )
+ );
});
await page.route("**/collected-invoices/economic/queue/status**", async (route) => {
@@ -333,35 +379,41 @@ test.describe("Economic queue async export workflow", () => {
statusCalls += 1;
if (statusCalls === 1) {
- await route.fulfill(json({
- data: {
- id: 1302,
- status: "PROCESSING",
- progress_percent: 65,
- progress_message: "Creating draft invoice",
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 1302,
+ status: "PROCESSING",
+ progress_percent: 65,
+ progress_message: "Creating draft invoice",
+ },
+ })
+ );
return;
}
- await route.fulfill(json({
- data: {
- id: 1302,
- status: "COMPLETED",
- progress_percent: 100,
- progress_message: "Completed",
- result: {
- message: "Collected invoice exported from alias response.",
+ await route.fulfill(
+ json({
+ data: {
+ id: 1302,
+ status: "COMPLETED",
+ progress_percent: 100,
+ progress_message: "Completed",
+ result: {
+ message: "Collected invoice exported from alias response.",
+ },
},
- },
- }));
+ })
+ );
});
await openHarness(page);
await page.getByTestId("collected-economic-create-invoice").click();
await expect(page.getByTestId("collected-economic-progress")).toContainText("Creating draft invoice");
- await expect(page.getByTestId("collected-economic-completed")).toContainText("Collected invoice exported from alias response.");
+ await expect(page.getByTestId("collected-economic-completed")).toContainText(
+ "Collected invoice exported from alias response."
+ );
});
test("invalid boolean send_as_is is blocked and shown as validation error", async ({ page }) => {
@@ -380,7 +432,9 @@ test.describe("Economic queue async export workflow", () => {
await openHarness(page);
await page.getByTestId("economic-queue-harness-invalid-boolean-trigger").click();
- await expect(page.getByTestId("economic-queue-harness-invalid-boolean-error")).toContainText("send_as_is must be a boolean.");
+ await expect(page.getByTestId("economic-queue-harness-invalid-boolean-error")).toContainText(
+ "send_as_is must be a boolean."
+ );
expect(enqueueCalls).toBe(0);
});
@@ -393,16 +447,21 @@ test.describe("Economic queue async export workflow", () => {
return;
}
- await route.fulfill(json({
- data: {
- job: {
- id: 1401,
- status: "QUEUED",
- progress_percent: 0,
- progress_message: "Queued export",
+ await route.fulfill(
+ json(
+ {
+ data: {
+ job: {
+ id: 1401,
+ status: "QUEUED",
+ progress_percent: 0,
+ progress_message: "Queued export",
+ },
+ },
},
- },
- }, 202));
+ 202
+ )
+ );
});
await page.route("**/economic/invoice/export/status**", async (route) => {
@@ -412,14 +471,16 @@ test.describe("Economic queue async export workflow", () => {
}
statusCalls += 1;
- await route.fulfill(json({
- data: {
- id: 1401,
- status: "PROCESSING",
- progress_percent: 25,
- progress_message: "Still processing",
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 1401,
+ status: "PROCESSING",
+ progress_percent: 25,
+ progress_message: "Still processing",
+ },
+ })
+ );
});
await openHarness(page);
diff --git a/tests/e2e/edge-gateways.smoke.spec.js b/tests/e2e/edge-gateways.smoke.spec.js
index fc0cd12f..242b4f34 100644
--- a/tests/e2e/edge-gateways.smoke.spec.js
+++ b/tests/e2e/edge-gateways.smoke.spec.js
@@ -118,7 +118,7 @@ test.describe("Edge gateway management smoke", () => {
await expect(terminalBuffer).toContainText("Session lukket");
});
- test("@smoke shows a shell connection error when the browser socket fails before open", async ({ page }) => {
+ test("@smoke shows a shell connection error when the API-polled shell closes before open", async ({ page }) => {
await mockApi(page, {
authenticated: true,
permissions: ["superuser", "user"],
@@ -137,9 +137,7 @@ test.describe("Edge gateway management smoke", () => {
await expect(page.locator("body")).toContainText("Fejl");
await expect(page.getByRole("button", { name: "Terminate session" })).toBeDisabled();
- await expect(page.locator('[aria-label="Root shell buffer"]')).toContainText(
- "gateway-shell fejlede"
- );
+ await expect(page.locator('[aria-label="Root shell buffer"]')).toContainText("Gateway-shell blev lukket");
});
test("@smoke applies department cutover mode from department page", async ({ page }) => {
@@ -159,19 +157,16 @@ test.describe("Edge gateway management smoke", () => {
await expect(page.locator("body")).toContainText("Cloud fallback");
});
- test("@smoke keeps discovery working when the broker is reported disconnected", async ({ page }) => {
+ test("@smoke shows API polling as the control channel while discovery still works", async ({ page }) => {
await mockApi(page, {
authenticated: true,
permissions: ["superuser", "user"],
- edgeGateways: {
- brokerConnected: false,
- },
});
await primeSuperuserSession(page);
await page.goto("/superuser/gateways");
- await expect(page.locator("body")).toContainText("Broker frakoblet");
+ await expect(page.locator("body")).toContainText("API polling");
await page.getByRole("button", { name: /discovery/i }).click();
await expect(page.locator("body")).toContainText("Discovery er");
await expect(page.locator("body")).toContainText("shelly-plus-new");
diff --git a/tests/e2e/example.spec.ts b/tests/e2e/example.spec.ts
index 54a906a4..839cef5c 100644
--- a/tests/e2e/example.spec.ts
+++ b/tests/e2e/example.spec.ts
@@ -1,18 +1,18 @@
-import { test, expect } from '@playwright/test';
+import { test, expect } from "@playwright/test";
-test('has title', async ({ page }) => {
- await page.goto('https://playwright.dev/');
+test("has title", async ({ page }) => {
+ await page.goto("https://playwright.dev/");
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
-test('get started link', async ({ page }) => {
- await page.goto('https://playwright.dev/');
+test("get started link", async ({ page }) => {
+ await page.goto("https://playwright.dev/");
// Click the get started link.
- await page.getByRole('link', { name: 'Get started' }).click();
+ await page.getByRole("link", { name: "Get started" }).click();
// Expects page to have a heading with the name of Installation.
- await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible();
});
diff --git a/tests/e2e/fixtures/authHelpers.ts b/tests/e2e/fixtures/authHelpers.ts
index 2931d08c..5264dfb3 100644
--- a/tests/e2e/fixtures/authHelpers.ts
+++ b/tests/e2e/fixtures/authHelpers.ts
@@ -1,4 +1,4 @@
-import { Page, expect } from '@playwright/test';
+import { Page, expect } from "@playwright/test";
import {
userCredentials,
subuserPhoneCredentials,
@@ -7,8 +7,8 @@ import {
user2FACredentials,
subuser2FACredentials,
qrAuthToken,
-} from './testData';
-import { generateOTP } from './otp';
+} from "./testData";
+import { generateOTP } from "./otp";
// Auth timeout in milliseconds (15 seconds)
const AUTH_TIMEOUT = 15000;
@@ -30,7 +30,7 @@ export async function loginAsUser(
credentials?: { customerNumber: string; password: string; twoFactorAuthentication?: boolean; otpSecret?: string }
) {
const creds = credentials || userCredentials;
- await page.goto('/login');
+ await page.goto("/login");
await page.fill('input[name="customer_number"]', creds.customerNumber);
await page.fill('input[name="password"]', creds.password);
await page.click('button[id="login-button"]');
@@ -51,10 +51,16 @@ export async function loginAsUser(
*/
export async function loginAsSubuserByPhone(
page: Page,
- credentials?: { phoneCountryCode: string; phone: string; password: string; twoFactorAuthentication?: boolean; otpSecret?: string }
+ credentials?: {
+ phoneCountryCode: string;
+ phone: string;
+ password: string;
+ twoFactorAuthentication?: boolean;
+ otpSecret?: string;
+ }
) {
const creds = credentials || subuserPhoneCredentials;
- await page.goto('/login/driver');
+ await page.goto("/login/driver");
await page.fill('input[name="phone_country_code"]', creds.phoneCountryCode);
await page.fill('input[name="phone"]', creds.phone);
await page.fill('input[name="password"]', creds.password);
@@ -79,7 +85,7 @@ export async function loginAsSubuserByUsername(
credentials?: { username: string; password: string; twoFactorAuthentication?: boolean; otpSecret?: string }
) {
const creds = credentials || subuserUsernameCredentials;
- await page.goto('/login/driver');
+ await page.goto("/login/driver");
await page.click('button[id="subuser_login_method_username_button"]');
await page.waitForSelector('input[name="username"]');
await page.waitForSelector('input[name="password"]');
@@ -101,12 +107,9 @@ export async function loginAsSubuserByUsername(
* @param page - Playwright page object
* @param credentials - Optional custom credentials (defaults to operatorCredentials)
*/
-export async function loginAsOperator(
- page: Page,
- credentials?: { userId: string; password: string }
-) {
+export async function loginAsOperator(page: Page, credentials?: { userId: string; password: string }) {
const creds = credentials || operatorCredentials;
- await page.goto('/admin/login');
+ await page.goto("/admin/login");
await page.fill('input[name="user_id"]', creds.userId);
await page.fill('input[name="password"]', creds.password);
await page.click('button[id="operator_login_button"]');
@@ -119,7 +122,7 @@ export async function loginAsOperator(
* @param page - Playwright page object
*/
export async function goToUserLogin(page: Page) {
- await page.goto('/login');
+ await page.goto("/login");
}
/**
@@ -127,7 +130,7 @@ export async function goToUserLogin(page: Page) {
* @param page - Playwright page object
*/
export async function goToSubuserLogin(page: Page) {
- await page.goto('/login/driver');
+ await page.goto("/login/driver");
}
/**
@@ -135,7 +138,7 @@ export async function goToSubuserLogin(page: Page) {
* @param page - Playwright page object
*/
export async function goToOperatorLogin(page: Page) {
- await page.goto('/admin/login');
+ await page.goto("/admin/login");
}
/**
@@ -143,12 +146,9 @@ export async function goToOperatorLogin(page: Page) {
* @param page - Playwright page object
* @param credentials - Optional custom credentials (defaults to user2FACredentials)
*/
-export async function loginAsUserWith2FA(
- page: Page,
- credentials?: { customerNumber: string; password: string }
-) {
+export async function loginAsUserWith2FA(page: Page, credentials?: { customerNumber: string; password: string }) {
const creds = credentials || user2FACredentials;
- await page.goto('/login');
+ await page.goto("/login");
await page.fill('input[name="customer_number"]', creds.customerNumber);
await page.fill('input[name="password"]', creds.password);
await page.click('button[id="login-button"]');
@@ -184,7 +184,7 @@ export async function loginAsSubuserWith2FA(
credentials?: { phoneCountryCode: string; phone: string; password: string }
) {
const creds = credentials || subuser2FACredentials;
- await page.goto('/login/driver');
+ await page.goto("/login/driver");
await page.fill('input[name="phone_country_code"]', creds.phoneCountryCode);
await page.fill('input[name="phone"]', creds.phone);
await page.fill('input[name="password"]', creds.password);
@@ -207,13 +207,10 @@ export async function isPasskeyButtonVisible(page: Page): Promise {
* @param page - Playwright page object
* @param token - Optional QR auth token (defaults to qrAuthToken from testData)
*/
-export async function loginWithQRCode(
- page: Page,
- token?: string
-) {
+export async function loginWithQRCode(page: Page, token?: string) {
const authToken = token || qrAuthToken;
if (!authToken) {
- throw new Error('QR auth token is not set. Set PLENO_QR_TOKEN environment variable or pass token directly.');
+ throw new Error("QR auth token is not set. Set PLENO_QR_TOKEN environment variable or pass token directly.");
}
await page.goto(`/login/qr?token=${authToken}`);
await expect(page).toHaveURL(USER_HOME_URL, { timeout: AUTH_TIMEOUT });
@@ -225,7 +222,7 @@ export async function loginWithQRCode(
* @param page - Playwright page object
*/
export async function goToQRCodeLogin(page: Page) {
- await page.goto('/login/qr');
+ await page.goto("/login/qr");
}
/**
@@ -234,9 +231,6 @@ export async function goToQRCodeLogin(page: Page) {
* @param page - Playwright page object
* @param invalidToken - Invalid token to test with
*/
-export async function attemptInvalidQRCodeLogin(
- page: Page,
- invalidToken: string = 'invalid-token'
-) {
+export async function attemptInvalidQRCodeLogin(page: Page, invalidToken: string = "invalid-token") {
await page.goto(`/login/qr?token=${invalidToken}`);
}
diff --git a/tests/e2e/fixtures/index.ts b/tests/e2e/fixtures/index.ts
index 6c894711..4397af87 100644
--- a/tests/e2e/fixtures/index.ts
+++ b/tests/e2e/fixtures/index.ts
@@ -7,7 +7,7 @@
*/
// Re-export all test data
-export * from './testData';
+export * from "./testData";
// Re-export all authentication helpers
-export * from './authHelpers';
+export * from "./authHelpers";
diff --git a/tests/e2e/fixtures/otp.ts b/tests/e2e/fixtures/otp.ts
index 9e7b5e7a..43085d93 100644
--- a/tests/e2e/fixtures/otp.ts
+++ b/tests/e2e/fixtures/otp.ts
@@ -1,4 +1,4 @@
-import * as OTPAuth from 'otpauth';
+import * as OTPAuth from "otpauth";
/**
* Generate a TOTP code from a secret
@@ -7,9 +7,9 @@ import * as OTPAuth from 'otpauth';
*/
export function generateOTP(secret: string): string {
const totp = new OTPAuth.TOTP({
- issuer: 'Truck Wash',
- label: 'User',
- algorithm: 'SHA1',
+ issuer: "Truck Wash",
+ label: "User",
+ algorithm: "SHA1",
digits: 6,
period: 30,
secret: secret,
diff --git a/tests/e2e/fixtures/testData.ts b/tests/e2e/fixtures/testData.ts
index 79c27567..d1c0c687 100644
--- a/tests/e2e/fixtures/testData.ts
+++ b/tests/e2e/fixtures/testData.ts
@@ -6,148 +6,137 @@
const hasAnyEnv = (...names: string[]) => {
return names.some((name) => {
const value = process.env[name];
- return typeof value === 'string' && value.length > 0;
+ return typeof value === "string" && value.length > 0;
});
};
const readEnv = (name: string, fallback: string) => {
const value = process.env[name];
- return typeof value === 'string' && value.length > 0 ? value : fallback;
+ return typeof value === "string" && value.length > 0 ? value : fallback;
};
const customerEnvEnabled = hasAnyEnv(
- 'PLAYWRIGHT_USER_CUSTOMER_NUMBER',
- 'PLAYWRIGHT_USER_PASSWORD',
- 'PLAYWRIGHT_USER_OTP_SECRET',
+ "PLAYWRIGHT_USER_CUSTOMER_NUMBER",
+ "PLAYWRIGHT_USER_PASSWORD",
+ "PLAYWRIGHT_USER_OTP_SECRET"
);
const subuserPhoneEnvEnabled = hasAnyEnv(
- 'PLAYWRIGHT_SUBUSER_PHONE_COUNTRY_CODE',
- 'PLAYWRIGHT_SUBUSER_PHONE',
- 'PLAYWRIGHT_SUBUSER_PASSWORD',
- 'PLAYWRIGHT_SUBUSER_OTP_SECRET',
+ "PLAYWRIGHT_SUBUSER_PHONE_COUNTRY_CODE",
+ "PLAYWRIGHT_SUBUSER_PHONE",
+ "PLAYWRIGHT_SUBUSER_PASSWORD",
+ "PLAYWRIGHT_SUBUSER_OTP_SECRET"
);
const subuserUsernameEnvEnabled = hasAnyEnv(
- 'PLAYWRIGHT_SUBUSER_USERNAME',
- 'PLAYWRIGHT_SUBUSER_PASSWORD',
- 'PLAYWRIGHT_SUBUSER_OTP_SECRET',
+ "PLAYWRIGHT_SUBUSER_USERNAME",
+ "PLAYWRIGHT_SUBUSER_PASSWORD",
+ "PLAYWRIGHT_SUBUSER_OTP_SECRET"
);
-const operatorEnvEnabled = hasAnyEnv(
- 'PLAYWRIGHT_OPERATOR_USER_ID',
- 'PLAYWRIGHT_OPERATOR_PASSWORD',
-);
+const operatorEnvEnabled = hasAnyEnv("PLAYWRIGHT_OPERATOR_USER_ID", "PLAYWRIGHT_OPERATOR_PASSWORD");
// User (Customer) credentials
export const userCredentials = {
- customerNumber: readEnv('PLAYWRIGHT_USER_CUSTOMER_NUMBER', '12345679'),
- password: readEnv('PLAYWRIGHT_USER_PASSWORD', '5679'),
- twoFactorAuthentication: customerEnvEnabled
- ? hasAnyEnv('PLAYWRIGHT_USER_OTP_SECRET')
- : true,
- otpSecret: customerEnvEnabled
- ? readEnv('PLAYWRIGHT_USER_OTP_SECRET', '')
- : 'TBIS7MUDZ2O2VNM5',
+ customerNumber: readEnv("PLAYWRIGHT_USER_CUSTOMER_NUMBER", "12345679"),
+ password: readEnv("PLAYWRIGHT_USER_PASSWORD", "5679"),
+ twoFactorAuthentication: customerEnvEnabled ? hasAnyEnv("PLAYWRIGHT_USER_OTP_SECRET") : true,
+ otpSecret: customerEnvEnabled ? readEnv("PLAYWRIGHT_USER_OTP_SECRET", "") : "TBIS7MUDZ2O2VNM5",
};
// Subuser (Driver) credentials - Phone login
export const subuserPhoneCredentials = {
- phoneCountryCode: readEnv('PLAYWRIGHT_SUBUSER_PHONE_COUNTRY_CODE', '45'),
- phone: readEnv('PLAYWRIGHT_SUBUSER_PHONE', '42331128'),
- password: readEnv('PLAYWRIGHT_SUBUSER_PASSWORD', 'Test1234'),
+ phoneCountryCode: readEnv("PLAYWRIGHT_SUBUSER_PHONE_COUNTRY_CODE", "45"),
+ phone: readEnv("PLAYWRIGHT_SUBUSER_PHONE", "42331128"),
+ password: readEnv("PLAYWRIGHT_SUBUSER_PASSWORD", "Test1234"),
twoFactorAuthentication: false,
- otpSecret: subuserPhoneEnvEnabled
- ? readEnv('PLAYWRIGHT_SUBUSER_OTP_SECRET', '')
- : 'SUBUSER2FASECRET',
+ otpSecret: subuserPhoneEnvEnabled ? readEnv("PLAYWRIGHT_SUBUSER_OTP_SECRET", "") : "SUBUSER2FASECRET",
};
// Subuser (Driver) credentials - Username login
export const subuserUsernameCredentials = {
- username: readEnv('PLAYWRIGHT_SUBUSER_USERNAME', 'testsubuser'),
- password: readEnv('PLAYWRIGHT_SUBUSER_PASSWORD', 'Test1234'),
+ username: readEnv("PLAYWRIGHT_SUBUSER_USERNAME", "testsubuser"),
+ password: readEnv("PLAYWRIGHT_SUBUSER_PASSWORD", "Test1234"),
twoFactorAuthentication: false,
- otpSecret: subuserUsernameEnvEnabled
- ? readEnv('PLAYWRIGHT_SUBUSER_OTP_SECRET', '')
- : '',
+ otpSecret: subuserUsernameEnvEnabled ? readEnv("PLAYWRIGHT_SUBUSER_OTP_SECRET", "") : "",
};
// Operator credentials
export const operatorCredentials = {
- userId: readEnv('PLAYWRIGHT_OPERATOR_USER_ID', '11'),
- password: readEnv('PLAYWRIGHT_OPERATOR_PASSWORD', 'aef18KHAPGiu90'),
+ userId: readEnv("PLAYWRIGHT_OPERATOR_USER_ID", "11"),
+ password: readEnv("PLAYWRIGHT_OPERATOR_PASSWORD", "aef18KHAPGiu90"),
};
// Test data for bookings
export const bookingTestData = {
- departmentId: Number.parseInt(readEnv('PLAYWRIGHT_DEPARTMENT_ID', '12'), 10),
- registrationNumber: 'EC21233',
- reference: 'test_ref123',
- poNumber: 'test_po123',
- notes: 'test_note123',
+ departmentId: Number.parseInt(readEnv("PLAYWRIGHT_DEPARTMENT_ID", "12"), 10),
+ registrationNumber: "EC21233",
+ reference: "test_ref123",
+ poNumber: "test_po123",
+ notes: "test_note123",
};
// Test data for customer registration
export const customerRegistrationData = {
- existingCvr: '41004355',
- email: '2jepp9350@gmail.com',
- existingPhone: '42331128',
- newCvr: '43423010',
- newPhone: '42331129',
+ existingCvr: "41004355",
+ email: "2jepp9350@gmail.com",
+ existingPhone: "42331128",
+ newCvr: "43423010",
+ newPhone: "42331129",
};
// Test data for subuser/driver registration
export const driverRegistrationData = {
- existingCvr: '41004355',
- existingPhone: '42331128',
- invalidCvr7Char: '1234567',
- invalidCvr9Char: '1234567890',
+ existingCvr: "41004355",
+ existingPhone: "42331128",
+ invalidCvr7Char: "1234567",
+ invalidCvr9Char: "1234567890",
};
// Invalid credentials for negative tests
export const invalidCredentials = {
- invalidPassword: 'invalidpassword',
- invalidCustomerNumber: '99999999999',
- invalidPhoneNumber: '123456789',
- invalidUsername: 'invalidusername',
- invalidUserId: '99999999999',
- invalidOperatorPassword: '0000',
- invalidToken: 'invalidtoken',
- invalid2FACode: '000000',
+ invalidPassword: "invalidpassword",
+ invalidCustomerNumber: "99999999999",
+ invalidPhoneNumber: "123456789",
+ invalidUsername: "invalidusername",
+ invalidUserId: "99999999999",
+ invalidOperatorPassword: "0000",
+ invalidToken: "invalidtoken",
+ invalid2FACode: "000000",
};
// 2FA test credentials - User with 2FA enabled
export const user2FACredentials = {
- customerNumber: readEnv('PLAYWRIGHT_USER_2FA_CUSTOMER_NUMBER', '12345680'),
- password: readEnv('PLAYWRIGHT_USER_2FA_PASSWORD', '5680'),
- validCode: '123456', // For testing with mock/TOTP
+ customerNumber: readEnv("PLAYWRIGHT_USER_2FA_CUSTOMER_NUMBER", "12345680"),
+ password: readEnv("PLAYWRIGHT_USER_2FA_PASSWORD", "5680"),
+ validCode: "123456", // For testing with mock/TOTP
};
// 2FA test credentials - Subuser with 2FA enabled
export const subuser2FACredentials = {
- phoneCountryCode: readEnv('PLAYWRIGHT_SUBUSER_2FA_PHONE_COUNTRY_CODE', '45'),
- phone: readEnv('PLAYWRIGHT_SUBUSER_2FA_PHONE', '42331129'),
- password: readEnv('PLAYWRIGHT_SUBUSER_2FA_PASSWORD', 'Test1234'),
+ phoneCountryCode: readEnv("PLAYWRIGHT_SUBUSER_2FA_PHONE_COUNTRY_CODE", "45"),
+ phone: readEnv("PLAYWRIGHT_SUBUSER_2FA_PHONE", "42331129"),
+ password: readEnv("PLAYWRIGHT_SUBUSER_2FA_PASSWORD", "Test1234"),
twoFactorAuthentication: false,
- otpSecret: readEnv('PLAYWRIGHT_SUBUSER_2FA_OTP_SECRET', 'SUBUSER2FASECRET'),
+ otpSecret: readEnv("PLAYWRIGHT_SUBUSER_2FA_OTP_SECRET", "SUBUSER2FASECRET"),
};
// Passkey test data
export const passkeyTestData = {
- unsupportedBrowserMessage: 'Passkeys are not supported on this device',
- cancelledMessage: 'Authentication was cancelled or not allowed',
+ unsupportedBrowserMessage: "Passkeys are not supported on this device",
+ cancelledMessage: "Authentication was cancelled or not allowed",
};
// QR Code authentication token
// Set this via environment variable PLENO_QR_TOKEN or directly for testing
-export const qrAuthToken = process.env.PLENO_QR_TOKEN || '';
+export const qrAuthToken = process.env.PLENO_QR_TOKEN || "";
export const vehicleTestData = {
- validRegNr: 'EC21234',
- invalidRegNrShort: 'EC21',
- invalidRegNrLong: 'EC2123456',
- invalidRegNrLetters: 'ABCDEFGH',
- brand: 'Volvo',
- model: 'V70',
- nickname: 'Test Car',
+ validRegNr: "EC21234",
+ invalidRegNrShort: "EC21",
+ invalidRegNrLong: "EC2123456",
+ invalidRegNrLetters: "ABCDEFGH",
+ brand: "Volvo",
+ model: "V70",
+ nickname: "Test Car",
};
diff --git a/tests/e2e/invoice-distribution.smoke.spec.js b/tests/e2e/invoice-distribution.smoke.spec.js
index e0bdb236..1fe77d86 100644
--- a/tests/e2e/invoice-distribution.smoke.spec.js
+++ b/tests/e2e/invoice-distribution.smoke.spec.js
@@ -9,7 +9,8 @@ async function suppressVueDevtoolsOverlay(page) {
if (!document.getElementById(STYLE_ID)) {
const style = document.createElement("style");
style.id = STYLE_ID;
- style.textContent = "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
+ style.textContent =
+ "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
(document.head || document.documentElement).appendChild(style);
}
@@ -47,7 +48,7 @@ test.describe("Invoice distribution smoke", () => {
await mockApi(page, {
authenticated: true,
permissions: ["superuser", "user"],
- invoiceDistribution: true
+ invoiceDistribution: true,
});
await primeSuperuserSession(page);
});
@@ -73,7 +74,9 @@ test.describe("Invoice distribution smoke", () => {
});
test("@smoke monthly tabs keep URL query-state in sync", async ({ page }) => {
- await page.goto("/superuser/invoices/distribution/2026/3/customers?customerSearch=acme&customerSource=fixed_pricing&customerDepartment=Copenhagen&compareMode=line_by_line");
+ await page.goto(
+ "/superuser/invoices/distribution/2026/3/customers?customerSearch=acme&customerSource=fixed_pricing&customerDepartment=Copenhagen&compareMode=line_by_line"
+ );
await expect(page.getByTestId("distribution-month-tabs")).toBeVisible();
await expect(page).toHaveURL(/customerSearch=acme/);
@@ -101,7 +104,7 @@ test.describe("Invoice distribution smoke", () => {
await Promise.race([
progress.waitFor({ state: "visible", timeout: 6_000 }).catch(() => null),
- compareTable.waitFor({ state: "visible", timeout: 10_000 })
+ compareTable.waitFor({ state: "visible", timeout: 10_000 }),
]);
await expect(compareTable).toBeVisible();
@@ -129,24 +132,20 @@ test.describe("Invoice distribution smoke", () => {
permissions: ["superuser", "user"],
invoiceDistribution: true,
invoiceDistributionForceLegacyFallback: true,
- invoiceDistributionForceCompareFallback: true
+ invoiceDistributionForceCompareFallback: true,
});
await primeSuperuserSession(page);
await page.goto("/superuser/invoices?activeTab=distribution");
- await expect(
- page.locator(".message.is-warning").filter({ hasText: /v2/i }).first()
- ).toBeVisible({ timeout: 15_000 });
+ await expect(page.locator(".message.is-warning").filter({ hasText: /v2/i }).first()).toBeVisible({
+ timeout: 15_000,
+ });
await page.goto("/superuser/invoices/distribution/2026/3/compare?compareMode=line_by_line");
- await expect(
- page.locator(".message.is-warning").filter({ hasText: /v2/i }).first()
- ).toBeVisible();
+ await expect(page.locator(".message.is-warning").filter({ hasText: /v2/i }).first()).toBeVisible();
await page.getByTestId("distribution-compare-submit").click();
await expect(page.getByTestId("distribution-compare-table")).toBeVisible();
- await expect(
- page.locator(".message.is-warning").filter({ hasText: /v2/i }).first()
- ).toBeVisible();
+ await expect(page.locator(".message.is-warning").filter({ hasText: /v2/i }).first()).toBeVisible();
});
});
diff --git a/tests/e2e/invoice-transfer-queue-history.spec.js b/tests/e2e/invoice-transfer-queue-history.spec.js
index 236cdca0..3d001156 100644
--- a/tests/e2e/invoice-transfer-queue-history.spec.js
+++ b/tests/e2e/invoice-transfer-queue-history.spec.js
@@ -27,7 +27,8 @@ async function suppressVueDevtoolsOverlay(page) {
if (!document.getElementById(STYLE_ID)) {
const style = document.createElement("style");
style.id = STYLE_ID;
- style.textContent = "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
+ style.textContent =
+ "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
(document.head || document.documentElement).appendChild(style);
}
@@ -79,36 +80,40 @@ test.describe("Invoice transfer queue history reliability", () => {
queueListCalls += 1;
const isTerminal = queueListCalls > 1;
- await route.fulfill(json({
- data: {
- items: [
- {
- id: 9301,
- status: isTerminal ? "COMPLETED" : "PROCESSING",
- progress_percent: isTerminal ? 100 : 45,
- progress_message: isTerminal ? "Completed" : "Exporting line items",
- collected_invoice_id: 7001,
- attempts: 1,
- max_attempts: 3,
- created_at: "2026-04-08T10:00:00Z",
- updated_at: "2026-04-08T10:00:00Z",
- },
- ],
- count: 1,
- total: 1,
- limit: 50,
- offset: 0,
- has_more: false,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ items: [
+ {
+ id: 9301,
+ status: isTerminal ? "COMPLETED" : "PROCESSING",
+ progress_percent: isTerminal ? 100 : 45,
+ progress_message: isTerminal ? "Completed" : "Exporting line items",
+ collected_invoice_id: 7001,
+ attempts: 1,
+ max_attempts: 3,
+ created_at: "2026-04-08T10:00:00Z",
+ updated_at: "2026-04-08T10:00:00Z",
+ },
+ ],
+ count: 1,
+ total: 1,
+ limit: 50,
+ offset: 0,
+ has_more: false,
+ },
+ })
+ );
});
await openQueueHistory(page, token);
await expect(page.getByTestId("economic-queue-history-status-9301")).toContainText("PROCESSING");
- await expect.poll(async () => page.getByTestId("economic-queue-history-status-9301").innerText(), {
- timeout: 12_000,
- }).toContain("COMPLETED");
+ await expect
+ .poll(async () => page.getByTestId("economic-queue-history-status-9301").innerText(), {
+ timeout: 12_000,
+ })
+ .toContain("COMPLETED");
const callsAtTerminal = queueListCalls;
await page.waitForTimeout(5_000);
@@ -133,29 +138,31 @@ test.describe("Invoice transfer queue history reliability", () => {
queueRequests.push({ status, limit, offset });
const hasMore = offset < 10;
- await route.fulfill(json({
- data: {
- items: [
- {
- id: hasMore ? 9501 : 9502,
- status: "FAILED",
- progress_percent: hasMore ? 20 : 100,
- progress_message: hasMore ? "Failed" : "Failed and exhausted",
- error_message: hasMore ? "Transient API timeout" : "Permanent validation failure",
- collected_invoice_id: hasMore ? 7301 : 7302,
- attempts: hasMore ? 1 : 3,
- max_attempts: 3,
- created_at: "2026-04-08T11:00:00Z",
- updated_at: "2026-04-08T11:00:30Z",
- },
- ],
- count: 1,
- total: 11,
- limit,
- offset,
- has_more: hasMore,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ items: [
+ {
+ id: hasMore ? 9501 : 9502,
+ status: "FAILED",
+ progress_percent: hasMore ? 20 : 100,
+ progress_message: hasMore ? "Failed" : "Failed and exhausted",
+ error_message: hasMore ? "Transient API timeout" : "Permanent validation failure",
+ collected_invoice_id: hasMore ? 7301 : 7302,
+ attempts: hasMore ? 1 : 3,
+ max_attempts: 3,
+ created_at: "2026-04-08T11:00:00Z",
+ updated_at: "2026-04-08T11:00:30Z",
+ },
+ ],
+ count: 1,
+ total: 11,
+ limit,
+ offset,
+ has_more: hasMore,
+ },
+ })
+ );
});
await openQueueHistory(page, token);
@@ -163,23 +170,31 @@ test.describe("Invoice transfer queue history reliability", () => {
await page.getByTestId("economic-queue-history-status-filter").selectOption("FAILED");
await page.getByTestId("economic-queue-history-limit-filter").selectOption("10");
- await expect.poll(
- () => queueRequests.some((request) => request.status === "FAILED" && request.limit === 10 && request.offset === 0),
- { timeout: 8_000 },
- ).toBeTruthy();
+ await expect
+ .poll(
+ () =>
+ queueRequests.some((request) => request.status === "FAILED" && request.limit === 10 && request.offset === 0),
+ { timeout: 8_000 }
+ )
+ .toBeTruthy();
await expect(page.getByTestId("economic-queue-history-summary")).toContainText("More results available.");
await page.getByTestId("economic-queue-history-next-page").click();
- await expect.poll(
- () => queueRequests.some((request) => request.status === "FAILED" && request.limit === 10 && request.offset === 10),
- { timeout: 8_000 },
- ).toBeTruthy();
+ await expect
+ .poll(
+ () =>
+ queueRequests.some((request) => request.status === "FAILED" && request.limit === 10 && request.offset === 10),
+ { timeout: 8_000 }
+ )
+ .toBeTruthy();
await expect(page.getByTestId("economic-queue-history-summary")).not.toContainText("More results available.");
await expect(page.getByTestId("economic-queue-history-next-page")).toBeDisabled();
});
- test("@smoke run queue now falls back when the backend route is ahead of the queue class deployment", async ({ page }) => {
+ test("@smoke run queue now falls back when the backend route is ahead of the queue class deployment", async ({
+ page,
+ }) => {
let statusCalls = 0;
let queueListCalls = 0;
const token = await bootstrapAuthenticatedSuperuser(page);
@@ -190,9 +205,15 @@ test.describe("Invoice transfer queue history reliability", () => {
return;
}
- await route.fulfill(json({
- message: "Internal server error: Call to undefined method classes\\economic_transfer_queue::processPendingByTransferType()",
- }, 500));
+ await route.fulfill(
+ json(
+ {
+ message:
+ "Internal server error: Call to undefined method classes\\economic_transfer_queue::processPendingByTransferType()",
+ },
+ 500
+ )
+ );
});
await page.route("**/collected-invoices/economic/queue/status**", async (route) => {
@@ -205,14 +226,16 @@ test.describe("Invoice transfer queue history reliability", () => {
const url = new URL(route.request().url());
expect(url.searchParams.get("job_id")).toBe("9351");
- await route.fulfill(json({
- data: {
- id: 9351,
- status: "PROCESSING",
- progress_percent: 45,
- progress_message: "Legacy worker tick",
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id: 9351,
+ status: "PROCESSING",
+ progress_percent: 45,
+ progress_message: "Legacy worker tick",
+ },
+ })
+ );
});
await page.route("**/collected-invoices/economic/queue**", async (route) => {
@@ -222,28 +245,30 @@ test.describe("Invoice transfer queue history reliability", () => {
}
queueListCalls += 1;
- await route.fulfill(json({
- data: {
- items: [
- {
- id: 9351,
- status: queueListCalls === 1 ? "QUEUED" : "PROCESSING",
- progress_percent: queueListCalls === 1 ? 0 : 45,
- progress_message: queueListCalls === 1 ? "Queued" : "Legacy worker tick",
- collected_invoice_id: 7351,
- attempts: 0,
- max_attempts: 3,
- created_at: "2026-04-08T11:30:00Z",
- updated_at: "2026-04-08T11:30:30Z",
- },
- ],
- count: 1,
- total: 1,
- limit: 50,
- offset: 0,
- has_more: false,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ items: [
+ {
+ id: 9351,
+ status: queueListCalls === 1 ? "QUEUED" : "PROCESSING",
+ progress_percent: queueListCalls === 1 ? 0 : 45,
+ progress_message: queueListCalls === 1 ? "Queued" : "Legacy worker tick",
+ collected_invoice_id: 7351,
+ attempts: 0,
+ max_attempts: 3,
+ created_at: "2026-04-08T11:30:00Z",
+ updated_at: "2026-04-08T11:30:30Z",
+ },
+ ],
+ count: 1,
+ total: 1,
+ limit: 50,
+ offset: 0,
+ has_more: false,
+ },
+ })
+ );
});
await openQueueHistory(page, token);
@@ -252,7 +277,9 @@ test.describe("Invoice transfer queue history reliability", () => {
await expect.poll(() => statusCalls, { timeout: 8_000 }).toBe(1);
await expect(page.getByTestId("economic-queue-history-run-summary")).toContainText("Legacy fallback active");
- await expect(page.getByTestId("economic-queue-history-summary")).toContainText("Legacy fallback active while the backend queue runner is unavailable.");
+ await expect(page.getByTestId("economic-queue-history-summary")).toContainText(
+ "Legacy fallback active while the backend queue runner is unavailable."
+ );
await expect(page.getByTestId("economic-queue-history-error")).toHaveCount(0);
await expect(page.getByTestId("economic-queue-history-status-9351")).toContainText("PROCESSING");
});
@@ -272,16 +299,18 @@ test.describe("Invoice transfer queue history reliability", () => {
expect(route.request().postDataJSON()).toEqual({ job_id: 9402 });
retryTriggered = true;
- await route.fulfill(json({
- data: {
- job: {
- id: 9402,
- status: "QUEUED",
- progress_percent: 0,
- progress_message: "Retry queued",
+ await route.fulfill(
+ json({
+ data: {
+ job: {
+ id: 9402,
+ status: "QUEUED",
+ progress_percent: 0,
+ progress_message: "Retry queued",
+ },
},
- },
- }));
+ })
+ );
});
await page.route("**/collected-invoices/economic/queue**", async (route) => {
@@ -290,52 +319,54 @@ test.describe("Invoice transfer queue history reliability", () => {
return;
}
- await route.fulfill(json({
- data: {
- items: [
- {
- id: 9401,
- status: "COMPLETED",
- progress_percent: 100,
- progress_message: "Completed",
- collected_invoice_id: 7401,
- attempts: 1,
- max_attempts: 3,
- created_at: "2026-04-08T12:00:00Z",
- updated_at: "2026-04-08T12:01:00Z",
- },
- {
- id: 9402,
- status: retryTriggered ? "QUEUED" : "FAILED",
- progress_percent: retryTriggered ? 0 : 40,
- progress_message: retryTriggered ? "Retry queued" : "Failed",
- error_message: retryTriggered ? "" : "Temporary upstream issue",
- collected_invoice_id: 7402,
- attempts: retryTriggered ? 1 : 1,
- max_attempts: 3,
- created_at: "2026-04-08T12:00:00Z",
- updated_at: "2026-04-08T12:01:00Z",
- },
- {
- id: 9403,
- status: "FAILED",
- progress_percent: 100,
- progress_message: "Failed",
- error_message: "Max attempts reached",
- collected_invoice_id: 7403,
- attempts: 3,
- max_attempts: 3,
- created_at: "2026-04-08T12:00:00Z",
- updated_at: "2026-04-08T12:01:00Z",
- },
- ],
- count: 3,
- total: 3,
- limit: 50,
- offset: 0,
- has_more: false,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ items: [
+ {
+ id: 9401,
+ status: "COMPLETED",
+ progress_percent: 100,
+ progress_message: "Completed",
+ collected_invoice_id: 7401,
+ attempts: 1,
+ max_attempts: 3,
+ created_at: "2026-04-08T12:00:00Z",
+ updated_at: "2026-04-08T12:01:00Z",
+ },
+ {
+ id: 9402,
+ status: retryTriggered ? "QUEUED" : "FAILED",
+ progress_percent: retryTriggered ? 0 : 40,
+ progress_message: retryTriggered ? "Retry queued" : "Failed",
+ error_message: retryTriggered ? "" : "Temporary upstream issue",
+ collected_invoice_id: 7402,
+ attempts: retryTriggered ? 1 : 1,
+ max_attempts: 3,
+ created_at: "2026-04-08T12:00:00Z",
+ updated_at: "2026-04-08T12:01:00Z",
+ },
+ {
+ id: 9403,
+ status: "FAILED",
+ progress_percent: 100,
+ progress_message: "Failed",
+ error_message: "Max attempts reached",
+ collected_invoice_id: 7403,
+ attempts: 3,
+ max_attempts: 3,
+ created_at: "2026-04-08T12:00:00Z",
+ updated_at: "2026-04-08T12:01:00Z",
+ },
+ ],
+ count: 3,
+ total: 3,
+ limit: 50,
+ offset: 0,
+ has_more: false,
+ },
+ })
+ );
});
await openQueueHistory(page, token);
@@ -361,37 +392,44 @@ test.describe("Invoice transfer queue history reliability", () => {
queueListCalls += 1;
if (queueListCalls >= 2 && queueListCalls <= 4) {
- await route.fulfill(json({
- data: {
- message: "gateway timeout",
- },
- }, 503));
+ await route.fulfill(
+ json(
+ {
+ data: {
+ message: "gateway timeout",
+ },
+ },
+ 503
+ )
+ );
return;
}
const isTerminal = queueListCalls >= 6;
- await route.fulfill(json({
- data: {
- items: [
- {
- id: 9601,
- status: isTerminal ? "COMPLETED" : "PROCESSING",
- progress_percent: isTerminal ? 100 : 60,
- progress_message: isTerminal ? "Completed" : "Retrying worker connection",
- collected_invoice_id: 7601,
- attempts: 1,
- max_attempts: 3,
- created_at: "2026-04-08T13:00:00Z",
- updated_at: "2026-04-08T13:00:30Z",
- },
- ],
- count: 1,
- total: 1,
- limit: 50,
- offset: 0,
- has_more: false,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ items: [
+ {
+ id: 9601,
+ status: isTerminal ? "COMPLETED" : "PROCESSING",
+ progress_percent: isTerminal ? 100 : 60,
+ progress_message: isTerminal ? "Completed" : "Retrying worker connection",
+ collected_invoice_id: 7601,
+ attempts: 1,
+ max_attempts: 3,
+ created_at: "2026-04-08T13:00:00Z",
+ updated_at: "2026-04-08T13:00:30Z",
+ },
+ ],
+ count: 1,
+ total: 1,
+ limit: 50,
+ offset: 0,
+ has_more: false,
+ },
+ })
+ );
});
await openQueueHistory(page, token);
@@ -404,9 +442,11 @@ test.describe("Invoice transfer queue history reliability", () => {
await page.getByTestId("economic-queue-history-refresh").click();
await expect(page.getByTestId("economic-queue-history-poll-error")).toHaveCount(0);
- await expect.poll(async () => page.getByTestId("economic-queue-history-status-9601").innerText(), {
- timeout: 12_000,
- }).toContain("COMPLETED");
+ await expect
+ .poll(async () => page.getByTestId("economic-queue-history-status-9601").innerText(), {
+ timeout: 12_000,
+ })
+ .toContain("COMPLETED");
await expect(page.getByTestId("economic-queue-history-error")).toHaveCount(0);
});
@@ -430,28 +470,30 @@ test.describe("Invoice transfer queue history reliability", () => {
try {
await new Promise((resolve) => setTimeout(resolve, 700));
const isTerminal = callNumber >= 3;
- await route.fulfill(json({
- data: {
- items: [
- {
- id: 9701,
- status: isTerminal ? "COMPLETED" : "PROCESSING",
- progress_percent: isTerminal ? 100 : 55,
- progress_message: isTerminal ? "Completed" : "Working",
- collected_invoice_id: 7701,
- attempts: 1,
- max_attempts: 3,
- created_at: "2026-04-08T14:00:00Z",
- updated_at: "2026-04-08T14:00:30Z",
- },
- ],
- count: 1,
- total: 1,
- limit: 50,
- offset: 0,
- has_more: false,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ items: [
+ {
+ id: 9701,
+ status: isTerminal ? "COMPLETED" : "PROCESSING",
+ progress_percent: isTerminal ? 100 : 55,
+ progress_message: isTerminal ? "Completed" : "Working",
+ collected_invoice_id: 7701,
+ attempts: 1,
+ max_attempts: 3,
+ created_at: "2026-04-08T14:00:00Z",
+ updated_at: "2026-04-08T14:00:30Z",
+ },
+ ],
+ count: 1,
+ total: 1,
+ limit: 50,
+ offset: 0,
+ has_more: false,
+ },
+ })
+ );
} finally {
inFlight -= 1;
}
@@ -475,78 +517,80 @@ test.describe("Invoice transfer queue history reliability", () => {
return;
}
- await route.fulfill(json({
- data: {
- items: [
- {
- id: 9801,
- status: "FAILED",
- progress_percent: 100,
- progress_message: "Failed",
- error_message: "Retry available",
- collected_invoice_id: 7801,
- attempts: 1,
- max_attempts: 3,
- created_at: "2026-04-08T15:00:00Z",
- updated_at: "2026-04-08T15:00:30Z",
- details_summary: {
- message: "Queue failure details",
- target: {
- collected_invoice_id: 7801,
- send_as_is: false,
- requested_by: 7,
- },
- customer: {
- customer_number: 43425425,
- name: "Carrier A/S",
- },
- outcome: {
- status: "FAILED",
- error_message: "Retry available",
- economic_invoice_draft_id: null,
- economic_invoice_booked_id: null,
- external_id: null,
- total_net_amount: null,
- order_count: 0,
- },
- transfer: {
- transfer_type: "COLLECTED_INVOICE_EXPORT",
- status: "FAILED",
- progress_percent: 100,
- progress_message: "Failed",
- attempts: 1,
- max_attempts: 3,
- },
- technical: {
- job_id: 9801,
- created_by: 7,
- created_at: "2026-04-08T15:00:00Z",
- updated_at: "2026-04-08T15:00:30Z",
+ await route.fulfill(
+ json({
+ data: {
+ items: [
+ {
+ id: 9801,
+ status: "FAILED",
+ progress_percent: 100,
+ progress_message: "Failed",
+ error_message: "Retry available",
+ collected_invoice_id: 7801,
+ attempts: 1,
+ max_attempts: 3,
+ created_at: "2026-04-08T15:00:00Z",
+ updated_at: "2026-04-08T15:00:30Z",
+ details_summary: {
+ message: "Queue failure details",
+ target: {
+ collected_invoice_id: 7801,
+ send_as_is: false,
+ requested_by: 7,
+ },
+ customer: {
+ customer_number: 43425425,
+ name: "Carrier A/S",
+ },
+ outcome: {
+ status: "FAILED",
+ error_message: "Retry available",
+ economic_invoice_draft_id: null,
+ economic_invoice_booked_id: null,
+ external_id: null,
+ total_net_amount: null,
+ order_count: 0,
+ },
+ transfer: {
+ transfer_type: "COLLECTED_INVOICE_EXPORT",
+ status: "FAILED",
+ progress_percent: 100,
+ progress_message: "Failed",
+ attempts: 1,
+ max_attempts: 3,
+ },
+ technical: {
+ job_id: 9801,
+ created_by: 7,
+ created_at: "2026-04-08T15:00:00Z",
+ updated_at: "2026-04-08T15:00:30Z",
+ raw_available: {
+ payload: true,
+ result: true,
+ },
+ raw: {
+ payload: {
+ collected_invoice_id: 7801,
+ },
+ result: null,
+ },
+ },
raw_available: {
payload: true,
result: true,
},
- raw: {
- payload: {
- collected_invoice_id: 7801,
- },
- result: null,
- },
- },
- raw_available: {
- payload: true,
- result: true,
},
},
- },
- ],
- count: 1,
- total: 1,
- limit: 50,
- offset: 0,
- has_more: false,
- },
- }));
+ ],
+ count: 1,
+ total: 1,
+ limit: 50,
+ offset: 0,
+ has_more: false,
+ },
+ })
+ );
});
await openQueueHistory(page, token);
diff --git a/tests/e2e/invoicing-period.smoke.spec.js b/tests/e2e/invoicing-period.smoke.spec.js
index cf94231c..94d0a03c 100644
--- a/tests/e2e/invoicing-period.smoke.spec.js
+++ b/tests/e2e/invoicing-period.smoke.spec.js
@@ -27,7 +27,8 @@ async function suppressVueDevtoolsOverlay(page) {
if (!document.getElementById(STYLE_ID)) {
const style = document.createElement("style");
style.id = STYLE_ID;
- style.textContent = "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
+ style.textContent =
+ "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
target.appendChild(style);
}
@@ -53,9 +54,7 @@ function createPeriodPayload() {
customer_number: 4001,
customer_name: "Acme Fleet",
requires_action: true,
- transactions: [
- { id: 9001, date: "2026-03-10T10:00:00.000Z", amount: 120, booked: false, excluded: false },
- ],
+ transactions: [{ id: 9001, date: "2026-03-10T10:00:00.000Z", amount: 120, booked: false, excluded: false }],
queue: {
has_active_job: false,
statuses: [],
@@ -73,9 +72,7 @@ function createPeriodPayload() {
customer_number: 4002,
customer_name: "Nordic Transport",
requires_action: false,
- transactions: [
- { id: 9002, date: "2026-03-11T10:00:00.000Z", amount: 80, booked: true, excluded: false },
- ],
+ transactions: [{ id: 9002, date: "2026-03-11T10:00:00.000Z", amount: 80, booked: true, excluded: false }],
queue: {
has_active_job: false,
statuses: [],
@@ -91,9 +88,7 @@ function createPeriodPayload() {
customer_number: 7001,
customer_name: "Invoice Per Order Co",
requires_action: true,
- transactions: [
- { id: 9003, date: "2026-03-12T10:00:00.000Z", amount: 75, booked: false, excluded: false },
- ],
+ transactions: [{ id: 9003, date: "2026-03-12T10:00:00.000Z", amount: 75, booked: false, excluded: false }],
queue: {
has_active_job: false,
statuses: [],
@@ -131,9 +126,7 @@ function createPeriodPayload() {
customer_number: 8001,
customer_name: "Subscription Movers",
requires_action: true,
- transactions: [
- { id: 9004, date: "2026-03-13T10:00:00.000Z", amount: 60, booked: false, excluded: false },
- ],
+ transactions: [{ id: 9004, date: "2026-03-13T10:00:00.000Z", amount: 60, booked: false, excluded: false }],
queue: {
has_active_job: false,
statuses: [],
@@ -157,9 +150,7 @@ function createPeriodPayloadForChangedRange() {
customer_number: 5001,
customer_name: "April Logistics",
requires_action: true,
- transactions: [
- { id: 9101, date: "2026-04-10T10:00:00.000Z", amount: 250, booked: false, excluded: false },
- ],
+ transactions: [{ id: 9101, date: "2026-04-10T10:00:00.000Z", amount: 250, booked: false, excluded: false }],
queue: {
has_active_job: false,
statuses: [],
@@ -175,9 +166,7 @@ function createPeriodPayloadForChangedRange() {
customer_number: 5002,
customer_name: "April Per Order",
requires_action: true,
- transactions: [
- { id: 9102, date: "2026-04-11T11:00:00.000Z", amount: 150, booked: false, excluded: false },
- ],
+ transactions: [{ id: 9102, date: "2026-04-11T11:00:00.000Z", amount: 150, booked: false, excluded: false }],
queue: {
has_active_job: false,
statuses: [],
@@ -261,13 +250,13 @@ async function setupPeriodEndpoints(page, requests) {
initialRange = { dateFrom, dateTo };
}
- const didChangeRange = !initialRange
- || dateFrom !== initialRange.dateFrom
- || dateTo !== initialRange.dateTo;
+ const didChangeRange = !initialRange || dateFrom !== initialRange.dateFrom || dateTo !== initialRange.dateTo;
- await route.fulfill(json({
- data: didChangeRange ? createPeriodPayloadForChangedRange() : createPeriodPayload(),
- }));
+ await route.fulfill(
+ json({
+ data: didChangeRange ? createPeriodPayloadForChangedRange() : createPeriodPayload(),
+ })
+ );
});
await page.route("**/superuser/invoicing/period/distribution/fixed-pricing**", async (route) => {
@@ -276,26 +265,28 @@ async function setupPeriodEndpoints(page, requests) {
return;
}
- await route.fulfill(json({
- includes: {
- collective_fixed_pricing_results: {
- total_fixed_price: 500,
- total_original_price: 400,
- total_department_totals: {
- 1: 220,
- 2: 180,
- },
- total_department_totals_parsed: {
- Copenhagen: 220,
- Odense: 180,
- },
- total_department_totals_relative_parsed: {
- Copenhagen: 260,
- Odense: 240,
+ await route.fulfill(
+ json({
+ includes: {
+ collective_fixed_pricing_results: {
+ total_fixed_price: 500,
+ total_original_price: 400,
+ total_department_totals: {
+ 1: 220,
+ 2: 180,
+ },
+ total_department_totals_parsed: {
+ Copenhagen: 220,
+ Odense: 180,
+ },
+ total_department_totals_relative_parsed: {
+ Copenhagen: 260,
+ Odense: 240,
+ },
},
},
- },
- }));
+ })
+ );
});
await page.route("**/superuser/invoicing/period/distribution/wash-subscriptions**", async (route) => {
@@ -304,23 +295,25 @@ async function setupPeriodEndpoints(page, requests) {
return;
}
- await route.fulfill(json({
- includes: {
- collective_subscription_results: {
- total_subscription_price: 300,
- total_fixed_price: 300,
- total_original_price: 300,
- total_department_totals: {
- 1: 200,
- 2: 100,
- },
- subscription_price_department_distribution_parsed: {
- Copenhagen: 200,
- Odense: 100,
+ await route.fulfill(
+ json({
+ includes: {
+ collective_subscription_results: {
+ total_subscription_price: 300,
+ total_fixed_price: 300,
+ total_original_price: 300,
+ total_department_totals: {
+ 1: 200,
+ 2: 100,
+ },
+ subscription_price_department_distribution_parsed: {
+ Copenhagen: 200,
+ Odense: 100,
+ },
},
},
- },
- }));
+ })
+ );
});
await page.route("**/departments**", async (route) => {
@@ -335,12 +328,14 @@ async function setupPeriodEndpoints(page, requests) {
return;
}
- await route.fulfill(json({
- data: [
- { id: 1, name: "Copenhagen" },
- { id: 2, name: "Odense" },
- ],
- }));
+ await route.fulfill(
+ json({
+ data: [
+ { id: 1, name: "Copenhagen" },
+ { id: 2, name: "Odense" },
+ ],
+ })
+ );
});
}
@@ -384,9 +379,7 @@ test.describe("Invoicing period tab", () => {
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible();
await page.waitForTimeout(250);
- expect(pageErrors).not.toEqual(expect.arrayContaining([
- expect.stringContaining("reading 'value'"),
- ]));
+ expect(pageErrors).not.toEqual(expect.arrayContaining([expect.stringContaining("reading 'value'")]));
});
test("@smoke period view loads selectors and displays all-customer list", async ({ page }) => {
@@ -406,7 +399,10 @@ test.describe("Invoicing period tab", () => {
await openPeriodView(page);
await page.getByTestId("invoicing-period-view-selector-invoice_per_order").click();
- await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute("data-current-view", "invoice_per_order");
+ await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute(
+ "data-current-view",
+ "invoice_per_order"
+ );
await expect(page.getByTestId("invoicing-period-customer-7001")).toBeVisible();
await expect(page.getByTestId("invoicing-period-customer-4001")).toHaveCount(0);
await expect(page).toHaveURL(/activeTab=period/);
@@ -438,9 +434,13 @@ test.describe("Invoicing period tab", () => {
await dateInputs.nth(1).dispatchEvent("change");
const firstRange = periodRequests[0];
- await expect.poll(() => periodRequests.some((request) => (
- request.dateFrom !== firstRange?.dateFrom || request.dateTo !== firstRange?.dateTo
- ))).toBeTruthy();
+ await expect
+ .poll(() =>
+ periodRequests.some(
+ (request) => request.dateFrom !== firstRange?.dateFrom || request.dateTo !== firstRange?.dateTo
+ )
+ )
+ .toBeTruthy();
await expect(page.getByTestId("invoicing-period-customer-5001")).toBeVisible();
await expect(page).toHaveURL(/activeTab=period/);
});
@@ -462,9 +462,7 @@ test.describe("Invoicing period tab", () => {
await page.waitForTimeout(250);
await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute("data-current-view", "all");
await expect(page).toHaveURL(/activeTab=period/);
- expect(pageErrors).not.toEqual(expect.arrayContaining([
- expect.stringContaining("reading 'types'"),
- ]));
+ expect(pageErrors).not.toEqual(expect.arrayContaining([expect.stringContaining("reading 'types'")]));
});
test("@smoke period view shows a queued CTA when backend queue metadata blocks invoicing", async ({ page }) => {
@@ -484,9 +482,11 @@ test.describe("Invoicing period tab", () => {
dateTo: url.searchParams.get("dateTo"),
});
- await route.fulfill(json({
- data: createQueuedPeriodPayload(),
- }));
+ await route.fulfill(
+ json({
+ data: createQueuedPeriodPayload(),
+ })
+ );
});
await page.goto("/superuser/invoices?activeTab=period");
@@ -522,9 +522,11 @@ test.describe("Invoicing period tab", () => {
dateTo: url.searchParams.get("dateTo"),
});
- await route.fulfill(json({
- data: returnQueuedPayload ? createQueuedPeriodPayload() : createPeriodPayload(),
- }));
+ await route.fulfill(
+ json({
+ data: returnQueuedPayload ? createQueuedPeriodPayload() : createPeriodPayload(),
+ })
+ );
});
await page.goto("/superuser/invoices?activeTab=overview");
diff --git a/tests/e2e/passkeyAuth.spec.ts b/tests/e2e/passkeyAuth.spec.ts
index 1db7bfa8..02df5d8b 100644
--- a/tests/e2e/passkeyAuth.spec.ts
+++ b/tests/e2e/passkeyAuth.spec.ts
@@ -1,86 +1,83 @@
-import { test, expect } from '@playwright/test';
-import {
- passkeyTestData,
- goToUserLogin,
- goToSubuserLogin,
- isPasskeyButtonVisible,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { passkeyTestData, goToUserLogin, goToSubuserLogin, isPasskeyButtonVisible } from "./fixtures";
/** Passkey Authentication Tests */
// User Passkey Tests
-test.describe('[AUTH][Passkey][User]', () => {
- test('should display passkey login button on user login page when supported', async ({ page }) => {
+test.describe("[AUTH][Passkey][User]", () => {
+ test("should display passkey login button on user login page when supported", async ({ page }) => {
await goToUserLogin(page);
-
+
// Check if passkey button is visible (depends on browser WebAuthn support)
const passkeyButton = page.locator('button[id="passkey-login-button"]');
-
+
// The button visibility depends on WebAuthn support
// In most modern browsers (Chrome, Edge, Firefox), it should be visible
const isVisible = await passkeyButton.isVisible();
-
+
if (isVisible) {
await expect(passkeyButton).toBeVisible();
- await expect(passkeyButton.locator('.fa-fingerprint')).toBeVisible();
+ await expect(passkeyButton.locator(".fa-fingerprint")).toBeVisible();
}
});
- test('should have passkey button with fingerprint icon', async ({ page }) => {
+ test("should have passkey button with fingerprint icon", async ({ page }) => {
await goToUserLogin(page);
-
+
const passkeyButton = page.locator('button[id="passkey-login-button"]');
-
+
// Skip test if passkeys not supported in this browser
if (!(await passkeyButton.isVisible())) {
test.skip();
return;
}
-
+
// Button should contain fingerprint icon
- await expect(passkeyButton.locator('.fa-fingerprint')).toBeVisible();
+ await expect(passkeyButton.locator(".fa-fingerprint")).toBeVisible();
});
- test('should display loading spinner when passkey login is clicked', async ({ page }) => {
+ test("should display loading spinner when passkey login is clicked", async ({ page }) => {
await goToUserLogin(page);
-
+
const passkeyButton = page.locator('button[id="passkey-login-button"]');
-
+
// Skip test if passkeys not supported
if (!(await passkeyButton.isVisible())) {
test.skip();
return;
}
-
+
// Click passkey button - this will trigger WebAuthn prompt
// Note: The actual WebAuthn flow cannot be completed in automated tests
// without additional browser configuration or mocking
await passkeyButton.click();
-
+
// Button should show loading spinner while waiting for authenticator
// (This may be brief or the test may need adjustment based on actual behavior)
- await expect(passkeyButton.locator('.fa-spinner')).toBeVisible({ timeout: 1000 }).catch(() => {
- // Loading state may be too fast to catch
- });
+ await expect(passkeyButton.locator(".fa-spinner"))
+ .toBeVisible({ timeout: 1000 })
+ .catch(() => {
+ // Loading state may be too fast to catch
+ });
});
- test('should not disable other login options when passkey is available', async ({ page }) => {
+ test("should not disable other login options when passkey is available", async ({ page }) => {
await goToUserLogin(page);
-
+
// Regular login form should still be functional
const customerNumberInput = page.locator('input[name="customer_number"]');
const passwordInput = page.locator('input[name="password"]');
const loginButton = page.locator('button[id="login-button"]');
-
+
await expect(customerNumberInput).toBeVisible();
await expect(passwordInput).toBeVisible();
await expect(loginButton).toBeVisible();
await expect(loginButton).not.toBeDisabled();
});
- test('should show QR code login option alongside passkey', async ({ page }) => {
+ test("should show QR code login option alongside passkey", async ({ page }) => {
await goToUserLogin(page);
-
+
// QR code login should still be available
const qrLoginLink = page.locator('a[href="/auth/loginqr"], a:has-text("QR")');
await expect(qrLoginLink).toBeVisible();
@@ -88,28 +85,28 @@ test.describe('[AUTH][Passkey][User]', () => {
});
// Subuser Passkey Tests
-test.describe('[AUTH][Passkey][Subuser]', () => {
- test('should display passkey login button on subuser login page when supported', async ({ page }) => {
+test.describe("[AUTH][Passkey][Subuser]", () => {
+ test("should display passkey login button on subuser login page when supported", async ({ page }) => {
await goToSubuserLogin(page);
-
+
const passkeyButton = page.locator('button[id="passkey-login-button"]');
-
+
// The button visibility depends on WebAuthn support
const isVisible = await passkeyButton.isVisible();
-
+
if (isVisible) {
await expect(passkeyButton).toBeVisible();
}
});
- test('should not disable phone login when passkey is available', async ({ page }) => {
+ test("should not disable phone login when passkey is available", async ({ page }) => {
await goToSubuserLogin(page);
-
+
// Regular login form should still be functional
const phoneInput = page.locator('input[name="phone"]');
const passwordInput = page.locator('input[name="password"]');
const loginButton = page.locator('button[id="subuser-login-button"]');
-
+
await expect(phoneInput).toBeVisible();
await expect(passwordInput).toBeVisible();
await expect(loginButton).toBeVisible();
@@ -118,26 +115,26 @@ test.describe('[AUTH][Passkey][Subuser]', () => {
});
// Passkey Browser Support Tests
-test.describe('[AUTH][Passkey][Browser Support]', () => {
- test('should gracefully handle browsers without passkey support', async ({ page, browserName }) => {
+test.describe("[AUTH][Passkey][Browser Support]", () => {
+ test("should gracefully handle browsers without passkey support", async ({ page, browserName }) => {
await goToUserLogin(page);
-
+
// In browsers without WebAuthn, the passkey button should not be visible
const passkeyButton = page.locator('button[id="passkey-login-button"]');
-
+
// Get visibility state - this test documents the behavior rather than asserting specific state
const isVisible = await passkeyButton.isVisible();
-
+
// Log the state for test reporting
console.log(`Passkey button visible in ${browserName}: ${isVisible}`);
-
+
// The important thing is that the page loads correctly regardless
- await expect(page.locator('form')).toBeVisible();
+ await expect(page.locator("form")).toBeVisible();
});
- test('should always show password-based login as fallback', async ({ page }) => {
+ test("should always show password-based login as fallback", async ({ page }) => {
await goToUserLogin(page);
-
+
// Password login should always be available, regardless of passkey support
await expect(page.locator('input[name="customer_number"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
@@ -146,24 +143,24 @@ test.describe('[AUTH][Passkey][Browser Support]', () => {
});
// Passkey Button State Tests
-test.describe('[AUTH][Passkey][Button State]', () => {
- test('should disable passkey button while loading', async ({ page }) => {
+test.describe("[AUTH][Passkey][Button State]", () => {
+ test("should disable passkey button while loading", async ({ page }) => {
await goToUserLogin(page);
-
+
const passkeyButton = page.locator('button[id="passkey-login-button"]');
-
+
// Skip if passkeys not supported
if (!(await passkeyButton.isVisible())) {
test.skip();
return;
}
-
+
// Initial state - button should be enabled
await expect(passkeyButton).not.toBeDisabled();
-
+
// After clicking, button should become disabled while loading
await passkeyButton.click();
-
+
// The button should be disabled during the authentication attempt
await expect(passkeyButton).toBeDisabled();
});
diff --git a/tests/e2e/pos-customer-rules.spec.js b/tests/e2e/pos-customer-rules.spec.js
index 680b71bb..e708a4e2 100644
--- a/tests/e2e/pos-customer-rules.spec.js
+++ b/tests/e2e/pos-customer-rules.spec.js
@@ -56,8 +56,10 @@ test("rules tab reloads customer attributes when the panel becomes visible", asy
await expect(rulesTab).toBeVisible();
const customerAttributesResponse = page.waitForResponse((response) => {
- return response.request().method() === "GET"
- && response.url().includes(`/customer/attributes?customer_number=${customer.customerNumber}`);
+ return (
+ response.request().method() === "GET" &&
+ response.url().includes(`/customer/attributes?customer_number=${customer.customerNumber}`)
+ );
});
await rulesTab.click();
diff --git a/tests/e2e/pos-flow.spec.js b/tests/e2e/pos-flow.spec.js
index 22b6f7a0..c9152a98 100644
--- a/tests/e2e/pos-flow.spec.js
+++ b/tests/e2e/pos-flow.spec.js
@@ -1,7 +1,8 @@
import { expect, test } from "@playwright/test";
import { mockApi, seedAuthenticatedState } from "./support/network.js";
-const API_HOST = /https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
+const API_HOST =
+ /https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
function json(body, status = 200) {
return {
@@ -14,7 +15,8 @@ function json(body, status = 200) {
function suppressVueDevtoolsOverlay(page) {
return page.addInitScript(() => {
const style = document.createElement("style");
- style.textContent = "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
+ style.textContent =
+ "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
document.documentElement.appendChild(style);
});
}
@@ -22,20 +24,23 @@ function suppressVueDevtoolsOverlay(page) {
async function primeSession(page, { token, permissions, sessionData = {} }) {
await seedAuthenticatedState(page, token);
await page.goto("/login");
- await page.evaluate(async ({ sessionToken, sessionPermissions, data }) => {
- const sessionModule = await import("/src/components/session/token/SessionUser.vue");
- window.localStorage.setItem("token", sessionToken);
- sessionModule.SessionUser.token.value = sessionToken;
- sessionModule.SessionUser.authenticated.value = true;
- sessionModule.SessionUser.permissions.value = sessionPermissions;
- sessionModule.SessionUser.initiated.value = true;
- sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345;
- sessionModule.SessionUser.user.display_name.value = data.display_name ?? "POS E2E";
- }, {
- sessionToken: token,
- sessionPermissions: permissions,
- data: sessionData,
- });
+ await page.evaluate(
+ async ({ sessionToken, sessionPermissions, data }) => {
+ const sessionModule = await import("/src/components/session/token/SessionUser.vue");
+ window.localStorage.setItem("token", sessionToken);
+ sessionModule.SessionUser.token.value = sessionToken;
+ sessionModule.SessionUser.authenticated.value = true;
+ sessionModule.SessionUser.permissions.value = sessionPermissions;
+ sessionModule.SessionUser.initiated.value = true;
+ sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345;
+ sessionModule.SessionUser.user.display_name.value = data.display_name ?? "POS E2E";
+ },
+ {
+ sessionToken: token,
+ sessionPermissions: permissions,
+ data: sessionData,
+ }
+ );
}
async function expectDesktopOrderItemsTable(scope) {
@@ -182,99 +187,119 @@ async function mockPosApi(page, fixture) {
const method = request.method();
if (pathname.endsWith("/departments") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: [
- {
- id: 1,
- name: "Taastrup",
- },
- ],
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: [
+ {
+ id: 1,
+ name: "Taastrup",
+ },
+ ],
+ })
+ );
return;
}
if (pathname.endsWith("/departments/categories") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: fixture.departmentCategories,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: fixture.departmentCategories,
+ })
+ );
return;
}
if (pathname.endsWith("/bookings") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: [],
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: [],
+ })
+ );
return;
}
if (pathname.endsWith("/vehicles") && method === "GET") {
const search = (parsedUrl.searchParams.get("search") || "").toUpperCase();
const matches = fixture.vehicles.filter((vehicle) => vehicle.reg.includes(search));
- await route.fulfill(json({
- success: true,
- data: matches,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: matches,
+ })
+ );
return;
}
if (pathname.endsWith("/department/vehicles/unknown-customer") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: fixture.unknownVehicles,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: fixture.unknownVehicles,
+ })
+ );
return;
}
if (pathname.endsWith("/users/customer") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: {
- customer_name: fixture.customer.name,
- economic_customer: fixture.customer,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ customer_name: fixture.customer.name,
+ economic_customer: fixture.customer,
+ },
+ })
+ );
return;
}
if (pathname.endsWith("/customers") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: [fixture.customer],
- meta: {
- pagination: {
- page: 1,
- limit: 10,
- total: 1,
+ await route.fulfill(
+ json({
+ success: true,
+ data: [fixture.customer],
+ meta: {
+ pagination: {
+ page: 1,
+ limit: 10,
+ total: 1,
+ },
},
- },
- }));
+ })
+ );
return;
}
if (pathname.endsWith("/customer/notes") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: [],
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: [],
+ })
+ );
return;
}
if (pathname.endsWith("/customer/attributes") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: [],
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: [],
+ })
+ );
return;
}
if (pathname.endsWith("/superuser/user/discounts") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: [],
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: [],
+ })
+ );
return;
}
@@ -283,10 +308,12 @@ async function mockPosApi(page, fixture) {
const category = parsedUrl.searchParams.get("category");
if (id) {
const product = fixture.products.find((entry) => Number(entry.id) === Number(id));
- await route.fulfill(json({
- success: true,
- data: product || null,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: product || null,
+ })
+ );
return;
}
@@ -294,18 +321,22 @@ async function mockPosApi(page, fixture) {
? fixture.products.filter((entry) => Number(entry.category) === Number(category))
: fixture.products;
- await route.fulfill(json({
- success: true,
- data: list,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: list,
+ })
+ );
return;
}
if (pathname.endsWith("/orders") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: [],
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: [],
+ })
+ );
return;
}
@@ -327,21 +358,25 @@ async function mockPosApi(page, fixture) {
created_at: new Date().toISOString(),
};
fixture.orderItemsByOrderId[orderId] = [];
- await route.fulfill(json({
- success: true,
- data: {
- id: orderId,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ id: orderId,
+ },
+ })
+ );
return;
}
if (pathname.endsWith("/order") && method === "GET") {
const id = Number(parsedUrl.searchParams.get("id"));
- await route.fulfill(json({
- success: true,
- data: fixture.ordersById[id] || null,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: fixture.ordersById[id] || null,
+ })
+ );
return;
}
@@ -358,19 +393,23 @@ async function mockPosApi(page, fixture) {
};
}
}
- await route.fulfill(json({
- success: true,
- data: fixture.ordersById[orderId] || null,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: fixture.ordersById[orderId] || null,
+ })
+ );
return;
}
if (pathname.endsWith("/order/items") && method === "GET") {
const orderId = Number(parsedUrl.searchParams.get("order_id"));
- await route.fulfill(json({
- success: true,
- data: fixture.orderItemsByOrderId[orderId] || [],
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: fixture.orderItemsByOrderId[orderId] || [],
+ })
+ );
return;
}
@@ -380,12 +419,17 @@ async function mockPosApi(page, fixture) {
const productId = Number(body.product_id);
const product = fixture.products.find((entry) => Number(entry.id) === productId);
if (!product || !fixture.ordersById[orderId]) {
- await route.fulfill(json({
- success: false,
- data: {
- message: "Order or product not found",
- },
- }, 422));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: {
+ message: "Order or product not found",
+ },
+ },
+ 422
+ )
+ );
return;
}
@@ -394,10 +438,12 @@ async function mockPosApi(page, fixture) {
fixture.orderItemsByOrderId[orderId] = [];
}
fixture.orderItemsByOrderId[orderId].push(item);
- await route.fulfill(json({
- success: true,
- data: item,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: item,
+ })
+ );
return;
}
@@ -418,42 +464,50 @@ async function mockPosApi(page, fixture) {
};
});
});
- await route.fulfill(json({
- success: true,
- data: {
- id: orderItemId,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ id: orderItemId,
+ },
+ })
+ );
return;
}
if (pathname.endsWith("/order/items") && method === "DELETE") {
const orderItemId = Number(parsedUrl.searchParams.get("id"));
Object.keys(fixture.orderItemsByOrderId).forEach((orderIdKey) => {
- fixture.orderItemsByOrderId[orderIdKey] = (fixture.orderItemsByOrderId[orderIdKey] || []).filter((item) => item.id !== orderItemId);
+ fixture.orderItemsByOrderId[orderIdKey] = (fixture.orderItemsByOrderId[orderIdKey] || []).filter(
+ (item) => item.id !== orderItemId
+ );
});
- await route.fulfill(json({
- success: true,
- data: true,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: true,
+ })
+ );
return;
}
if (pathname.endsWith("/departments/order/recommended") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: {
- reg_1: {
- motorapi: [53],
- order_history: {
- 2: [],
- 3: [],
- 4: [],
- 5: [],
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ reg_1: {
+ motorapi: [53],
+ order_history: {
+ 2: [],
+ 3: [],
+ 4: [],
+ 5: [],
+ },
},
},
- },
- }));
+ })
+ );
return;
}
@@ -464,12 +518,14 @@ async function mockPosApi(page, fixture) {
if (fixture.ordersById[orderId]) {
fixture.ordersById[orderId].completed_at = new Date().toISOString();
}
- await route.fulfill(json({
- success: true,
- data: {
- id: orderId,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ id: orderId,
+ },
+ })
+ );
return;
}
@@ -562,14 +618,12 @@ test.describe("POS flow", () => {
const requestUrl = request.url();
if (
request.method() === "GET" &&
- (
- requestUrl.includes("/order/items?order_id=null") ||
+ (requestUrl.includes("/order/items?order_id=null") ||
requestUrl.includes("/order/items?order_id=undefined") ||
requestUrl.includes("/order/items?order_id=NaN") ||
requestUrl.includes("/order?id=null") ||
requestUrl.includes("/order?id=undefined") ||
- requestUrl.includes("/order?id=NaN")
- )
+ requestUrl.includes("/order?id=NaN"))
) {
invalidOrderRequests.push(requestUrl);
}
@@ -626,7 +680,9 @@ test.describe("POS flow", () => {
await page.locator("#reg_1").fill("AB12345");
await expect(page.getByTestId("pos-step-1").getByText("Pleno Logistics").first()).toBeVisible({ timeout: 10_000 });
- await expect(page.getByTestId("pos-step-1").locator(".pos-card-tabs > .tabs li.is-active")).toContainText("Kunde", { timeout: 10_000 });
+ await expect(page.getByTestId("pos-step-1").locator(".pos-card-tabs > .tabs li.is-active")).toContainText("Kunde", {
+ timeout: 10_000,
+ });
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: 10_000 });
@@ -655,19 +711,23 @@ test.describe("POS flow", () => {
await page.locator('[data-testid="pos-next-step"]:visible').click();
await markCompletedRequest;
- await expect.poll(async () => {
- if (await page.getByTestId("pos-step-4").isVisible()) {
- return "step4";
- }
- if (/\/admin\/1\/modules\/pos(?:\?|$)/.test(page.url()) && !page.url().includes("step=3")) {
- return "reset";
- }
- return "pending";
- }, { timeout: 10_000 }).not.toBe("pending");
+ await expect
+ .poll(
+ async () => {
+ if (await page.getByTestId("pos-step-4").isVisible()) {
+ return "step4";
+ }
+ if (/\/admin\/1\/modules\/pos(?:\?|$)/.test(page.url()) && !page.url().includes("step=3")) {
+ return "reset";
+ }
+ return "pending";
+ },
+ { timeout: 10_000 }
+ )
+ .not.toBe("pending");
if (await page.getByTestId("pos-step-4").isVisible()) {
await expectDesktopOrderItemsTable(page.getByTestId("pos-step-4"));
}
});
-
});
diff --git a/tests/e2e/pos-mobile-card-payments.spec.js b/tests/e2e/pos-mobile-card-payments.spec.js
index eef9f981..86b02f23 100644
--- a/tests/e2e/pos-mobile-card-payments.spec.js
+++ b/tests/e2e/pos-mobile-card-payments.spec.js
@@ -39,10 +39,15 @@ test.describe("POS mobile card payments", () => {
await page.getByTestId("pos-mobile-direct-card-payment").click();
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeHidden({ timeout: 10_000 });
- await expect.poll(async () => {
- const snapshot = await getStoredPosSnapshot(page);
- return snapshot?.metadata?.customerId ?? null;
- }, { timeout: 10_000 }).toBe(CARD_CUSTOMER_ID);
+ await expect
+ .poll(
+ async () => {
+ const snapshot = await getStoredPosSnapshot(page);
+ return snapshot?.metadata?.customerId ?? null;
+ },
+ { timeout: 10_000 }
+ )
+ .toBe(CARD_CUSTOMER_ID);
const snapshot = await getStoredPosSnapshot(page);
expect(snapshot?.vehicles?.vehicle_1?.reg).toBe("ZZ00000");
@@ -83,10 +88,15 @@ test.describe("POS mobile card payments", () => {
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
- await expect.poll(() => {
- const currentUrl = new URL(page.url());
- return currentUrl.searchParams.get("step");
- }, { timeout: 12_000 }).toBe("1");
+ await expect
+ .poll(
+ () => {
+ const currentUrl = new URL(page.url());
+ return currentUrl.searchParams.get("step");
+ },
+ { timeout: 12_000 }
+ )
+ .toBe("1");
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
});
@@ -234,10 +244,15 @@ test.describe("POS mobile card payments", () => {
await page.getByTestId("pos-stripe-capture-intent").click();
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
- await expect.poll(() => {
- const currentUrl = new URL(page.url());
- return currentUrl.searchParams.get("step");
- }, { timeout: 12_000 }).toBe("1");
+ await expect
+ .poll(
+ () => {
+ const currentUrl = new URL(page.url());
+ return currentUrl.searchParams.get("step");
+ },
+ { timeout: 12_000 }
+ )
+ .toBe("1");
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
});
@@ -315,10 +330,15 @@ test.describe("POS mobile card payments", () => {
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
expect(fixture.requestCounters.paymentIntentCreate).toBe(0);
expect(fixture.requestCounters.paymentIntentCapture).toBe(0);
- await expect.poll(() => {
- const currentUrl = new URL(page.url());
- return currentUrl.searchParams.get("step");
- }, { timeout: 12_000 }).toBe("1");
+ await expect
+ .poll(
+ () => {
+ const currentUrl = new URL(page.url());
+ return currentUrl.searchParams.get("step");
+ },
+ { timeout: 12_000 }
+ )
+ .toBe("1");
});
test("returning to step 2 and re-entering step 3 does not duplicate order items", async ({ page }) => {
@@ -435,10 +455,15 @@ test.describe("POS mobile card payments", () => {
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-mobile-next-step").click();
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
- await expect.poll(() => {
- const currentUrl = new URL(page.url());
- return currentUrl.searchParams.get("step");
- }, { timeout: 12_000 }).toBe("1");
+ await expect
+ .poll(
+ () => {
+ const currentUrl = new URL(page.url());
+ return currentUrl.searchParams.get("step");
+ },
+ { timeout: 12_000 }
+ )
+ .toBe("1");
expect(fixture.requestCounters.markAsCompleted).toBe(0);
await expect(page.getByTestId("pos-mobile-step-3")).toHaveCount(0);
diff --git a/tests/e2e/pos-mobile-order-flow.spec.js b/tests/e2e/pos-mobile-order-flow.spec.js
index 26cfa3db..7d80d3a8 100644
--- a/tests/e2e/pos-mobile-order-flow.spec.js
+++ b/tests/e2e/pos-mobile-order-flow.spec.js
@@ -39,39 +39,38 @@ async function selectCustomerFromPopup(page, customerNumber = REGULAR_CUSTOMER_I
}
async function waitForStepReset(page) {
- await expect.poll(() => {
- const currentUrl = new URL(page.url());
- return currentUrl.searchParams.get("step") || "1";
- }, { timeout: 12_000 }).toBe("1");
+ await expect
+ .poll(
+ () => {
+ const currentUrl = new URL(page.url());
+ return currentUrl.searchParams.get("step") || "1";
+ },
+ { timeout: 12_000 }
+ )
+ .toBe("1");
await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 });
}
async function expectHorizontallyCentered(container, target, tolerance = 8) {
- const [containerBox, targetBox] = await Promise.all([
- container.boundingBox(),
- target.boundingBox(),
- ]);
+ const [containerBox, targetBox] = await Promise.all([container.boundingBox(), target.boundingBox()]);
expect(containerBox).not.toBeNull();
expect(targetBox).not.toBeNull();
- const containerCenter = containerBox.x + (containerBox.width / 2);
- const targetCenter = targetBox.x + (targetBox.width / 2);
+ const containerCenter = containerBox.x + containerBox.width / 2;
+ const targetCenter = targetBox.x + targetBox.width / 2;
expect(Math.abs(containerCenter - targetCenter)).toBeLessThanOrEqual(tolerance);
}
async function expectSameRow(left, right, tolerance = 8) {
- const [leftBox, rightBox] = await Promise.all([
- left.boundingBox(),
- right.boundingBox(),
- ]);
+ const [leftBox, rightBox] = await Promise.all([left.boundingBox(), right.boundingBox()]);
expect(leftBox).not.toBeNull();
expect(rightBox).not.toBeNull();
- const leftCenterY = leftBox.y + (leftBox.height / 2);
- const rightCenterY = rightBox.y + (rightBox.height / 2);
+ const leftCenterY = leftBox.y + leftBox.height / 2;
+ const rightCenterY = rightBox.y + rightBox.height / 2;
expect(Math.abs(leftCenterY - rightCenterY)).toBeLessThanOrEqual(tolerance);
}
@@ -98,32 +97,38 @@ async function selectPrimaryProduct(page, productId = 53) {
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
}
-async function waitForBookingHydration(page, {
- primaryId,
- addonProductIds = [],
-} = {}) {
- await expect.poll(async () => {
- const snapshot = await getStoredPosSnapshot(page);
- const addonIds = (snapshot?.transactionItems?.primaryItem?.addons || [])
- .map((addon) => Number(addon?.product?.id ?? addon?.id))
- .sort((left, right) => left - right);
- return {
- primaryId: snapshot?.transactionItems?.primaryItem?.id ?? null,
- addonIds,
- };
- }, { timeout: 10_000 }).toEqual({
- primaryId,
- addonIds: [...addonProductIds].sort((left, right) => left - right),
- });
+async function waitForBookingHydration(page, { primaryId, addonProductIds = [] } = {}) {
+ await expect
+ .poll(
+ async () => {
+ const snapshot = await getStoredPosSnapshot(page);
+ const addonIds = (snapshot?.transactionItems?.primaryItem?.addons || [])
+ .map((addon) => Number(addon?.product?.id ?? addon?.id))
+ .sort((left, right) => left - right);
+ return {
+ primaryId: snapshot?.transactionItems?.primaryItem?.id ?? null,
+ addonIds,
+ };
+ },
+ { timeout: 10_000 }
+ )
+ .toEqual({
+ primaryId,
+ addonIds: [...addonProductIds].sort((left, right) => left - right),
+ });
}
-async function createOrderFromStep1(page, fixture, {
- reg = "FREE123",
- reference = "STEP1-REF",
- manualInput = false,
- customerNumber = REGULAR_CUSTOMER_ID,
- extraSeedState = {},
-} = {}) {
+async function createOrderFromStep1(
+ page,
+ fixture,
+ {
+ reg = "FREE123",
+ reference = "STEP1-REF",
+ manualInput = false,
+ customerNumber = REGULAR_CUSTOMER_ID,
+ extraSeedState = {},
+ } = {}
+) {
await setupMobilePosPage(page, fixture, {
token: `step1-${reg.toLowerCase()}-token`,
seedState: {
@@ -180,9 +185,18 @@ test.describe("POS mobile order flow", () => {
await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-mobile-step-1-title")).toContainText("Scan nummerplader");
await expect(page.getByTestId("pos-mobile-step-1-subtitle")).toContainText("Hold kameraet op mod nummerpladerne.");
- await expectHorizontallyCentered(page.getByTestId("pos-mobile-step-1-shell"), page.getByTestId("pos-mobile-step-1-title"));
- await expectHorizontallyCentered(page.getByTestId("pos-mobile-step-1-shell"), page.getByTestId("pos-mobile-step-1-subtitle"));
- await expectHorizontallyCentered(page.getByTestId("pos-mobile-step-1-shell"), page.getByTestId("pos-mobile-manual-input-toggle"));
+ await expectHorizontallyCentered(
+ page.getByTestId("pos-mobile-step-1-shell"),
+ page.getByTestId("pos-mobile-step-1-title")
+ );
+ await expectHorizontallyCentered(
+ page.getByTestId("pos-mobile-step-1-shell"),
+ page.getByTestId("pos-mobile-step-1-subtitle")
+ );
+ await expectHorizontallyCentered(
+ page.getByTestId("pos-mobile-step-1-shell"),
+ page.getByTestId("pos-mobile-manual-input-toggle")
+ );
const [registrationRowBox, referenceTriggerBox] = await Promise.all([
page.getByTestId("pos-mobile-step-1-registration-row").boundingBox(),
@@ -368,7 +382,9 @@ test.describe("POS mobile order flow", () => {
},
});
- await page.goto(`/admin/${fixture.departmentId ?? DEFAULT_DEPARTMENT_ID}/modules/pos?step=999&id=abc&customer_id=def`);
+ await page.goto(
+ `/admin/${fixture.departmentId ?? DEFAULT_DEPARTMENT_ID}/modules/pos?step=999&id=abc&customer_id=def`
+ );
await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-mobile-step-2")).toHaveCount(0);
@@ -391,7 +407,9 @@ test.describe("POS mobile order flow", () => {
},
});
- await page.goto(`/admin/${fixture.departmentId ?? DEFAULT_DEPARTMENT_ID}/modules/pos?step=2&customer_id=${REGULAR_CUSTOMER_ID}`);
+ await page.goto(
+ `/admin/${fixture.departmentId ?? DEFAULT_DEPARTMENT_ID}/modules/pos?step=2&customer_id=${REGULAR_CUSTOMER_ID}`
+ );
await expect(page.getByTestId("pos-mobile-step-1-shell")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-mobile-next-step")).toContainText("Pleno Logistics", { timeout: 10_000 });
@@ -486,10 +504,15 @@ test.describe("POS mobile order flow", () => {
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-mobile-last-order")).toBeVisible({ timeout: 10_000 });
- await expect.poll(async () => {
- const snapshot = await getStoredPosSnapshot(page);
- return snapshot?.transactionItems?.primaryItem?.id ?? null;
- }, { timeout: 10_000 }).toBe(53);
+ await expect
+ .poll(
+ async () => {
+ const snapshot = await getStoredPosSnapshot(page);
+ return snapshot?.transactionItems?.primaryItem?.id ?? null;
+ },
+ { timeout: 10_000 }
+ )
+ .toBe(53);
await page.getByTestId("pos-mobile-notes-input").scrollIntoViewIfNeeded();
await expectAboveFixedActions(page, page.getByTestId("pos-mobile-notes-input"));
@@ -540,7 +563,7 @@ test.describe("POS mobile order flow", () => {
await expect(page.getByTestId("pos-mobile-additional-items-header-title")).toBeVisible({ timeout: 10_000 });
await expectSameRow(
page.getByTestId("pos-mobile-additional-items-header-title"),
- page.getByTestId("pos-mobile-additional-items-header-icon"),
+ page.getByTestId("pos-mobile-additional-items-header-icon")
);
await expect(page.getByTestId("pos-mobile-addon-71")).toBeVisible({ timeout: 10_000 });
@@ -554,26 +577,38 @@ test.describe("POS mobile order flow", () => {
await page.getByTestId("pos-mobile-category-8").click();
await page.waitForTimeout(2_000);
await page.getByTestId("pos-mobile-product-91").click();
- await expect.poll(async () => {
- const snapshot = await getStoredPosSnapshot(page);
- return (snapshot?.transactionItems?.additionalItems || []).map((item) => Number(item?.id)).sort((left, right) => left - right);
- }, { timeout: 10_000 }).toEqual([91]);
+ await expect
+ .poll(
+ async () => {
+ const snapshot = await getStoredPosSnapshot(page);
+ return (snapshot?.transactionItems?.additionalItems || [])
+ .map((item) => Number(item?.id))
+ .sort((left, right) => left - right);
+ },
+ { timeout: 10_000 }
+ )
+ .toEqual([91]);
await page.getByTestId("pos-mobile-next-step").click();
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-mobile-addon-71-value")).toHaveText("2", { timeout: 10_000 });
- await expect.poll(async () => {
- const snapshot = await getStoredPosSnapshot(page);
- return (snapshot?.transactionItems?.additionalItems || []).map((item) => ({
- id: Number(item?.id),
- quantity: Number(item?.quantity ?? 0),
- }));
- }, { timeout: 10_000 }).toEqual([
- {
- id: 91,
- quantity: 1,
- },
- ]);
+ await expect
+ .poll(
+ async () => {
+ const snapshot = await getStoredPosSnapshot(page);
+ return (snapshot?.transactionItems?.additionalItems || []).map((item) => ({
+ id: Number(item?.id),
+ quantity: Number(item?.quantity ?? 0),
+ }));
+ },
+ { timeout: 10_000 }
+ )
+ .toEqual([
+ {
+ id: 91,
+ quantity: 1,
+ },
+ ]);
await page.waitForTimeout(500);
await page.getByTestId("pos-mobile-next-step").click();
@@ -741,10 +776,15 @@ test.describe("POS mobile order flow", () => {
},
});
- await expect.poll(async () => {
- const snapshot = await getStoredPosSnapshot(page);
- return snapshot?.transactionItems?.primaryItem?.id ?? null;
- }, { timeout: 10_000 }).toBe(53);
+ await expect
+ .poll(
+ async () => {
+ const snapshot = await getStoredPosSnapshot(page);
+ return snapshot?.transactionItems?.primaryItem?.id ?? null;
+ },
+ { timeout: 10_000 }
+ )
+ .toBe(53);
});
test("booking completion without safety seal completes and resets", async ({ page }) => {
@@ -837,10 +877,17 @@ test.describe("POS mobile order flow", () => {
await page.getByTestId("pos-mobile-category-8").click();
await page.waitForTimeout(2_000);
await page.getByTestId("pos-mobile-product-41").click();
- await expect.poll(async () => {
- const snapshot = await getStoredPosSnapshot(page);
- return (snapshot?.transactionItems?.additionalItems || []).map((item) => Number(item?.id)).sort((left, right) => left - right);
- }, { timeout: 10_000 }).toEqual([41]);
+ await expect
+ .poll(
+ async () => {
+ const snapshot = await getStoredPosSnapshot(page);
+ return (snapshot?.transactionItems?.additionalItems || [])
+ .map((item) => Number(item?.id))
+ .sort((left, right) => left - right);
+ },
+ { timeout: 10_000 }
+ )
+ .toEqual([41]);
await page.getByTestId("pos-mobile-next-step").click();
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
diff --git a/tests/e2e/pos.visual.spec.js b/tests/e2e/pos.visual.spec.js
index 3cf85b69..be20db47 100644
--- a/tests/e2e/pos.visual.spec.js
+++ b/tests/e2e/pos.visual.spec.js
@@ -22,89 +22,95 @@ async function primeSession(page, token = "pos-visual-token") {
}
async function seedMobileStepTwoState(page, { customerId = 12345679, orderId = 54518 } = {}) {
- await page.addInitScript(({ activeCustomerId, activeOrderId }) => {
- window.localStorage.removeItem("pos_order_id");
- window.localStorage.setItem("pos", JSON.stringify({
- vehicles: {
- vehicle_1: {
- reg: "EC21235",
- status: "verified",
- reference: "EC21233 - Test Ref. / Intern nummer",
- type: 53,
- customer_id: activeCustomerId,
- },
- vehicle_2: null,
- vehicle_3: null,
- activeVehicleIndex: 1,
- },
- views: {
- manualInput: false,
- vehicleSelection: false,
- additionalItemSelection: false,
- transactionHistoryView: false,
- },
- transactionItems: {
- primaryItem: {
- id: 53,
- name: "Forvogn",
- price: 649,
- addons: [
- {
- id: 63,
- quantity: 1,
- product: {
- id: 63,
- name: "Indvendig vask Forvogn",
- price: 399,
- },
+ await page.addInitScript(
+ ({ activeCustomerId, activeOrderId }) => {
+ window.localStorage.removeItem("pos_order_id");
+ window.localStorage.setItem(
+ "pos",
+ JSON.stringify({
+ vehicles: {
+ vehicle_1: {
+ reg: "EC21235",
+ status: "verified",
+ reference: "EC21233 - Test Ref. / Intern nummer",
+ type: 53,
+ customer_id: activeCustomerId,
},
- ],
- },
- additionalItems: [
- {
- id: 65,
- name: "Ekstraordinær pr. 10 min inkl. kemi",
- price: 299,
- quantity: 1,
+ vehicle_2: null,
+ vehicle_3: null,
+ activeVehicleIndex: 1,
},
- ],
- },
- categories: {
- list: [],
- selected: null,
- },
- productList: {
- list: [],
- },
- metadata: {
- customerId: activeCustomerId,
- notes: "Visual regression note",
- reference: "EC21233 - Test Ref. / Intern nummer",
- washId: null,
- bookingId: null,
- laneId: null,
- },
- attachments: {
- files: [],
- base64: [],
- wash_certificate: false,
- },
- transactionHistory: [],
- lastVehicleOrders: {
- vehicle_1: {
- id: activeOrderId,
- created_at: "2026-04-08 08:44:07",
- items: [],
- },
- vehicle_2: null,
- vehicle_3: null,
- },
- timestamp: Date.now(),
- }));
- }, {
- activeCustomerId: customerId,
- activeOrderId: orderId,
- });
+ views: {
+ manualInput: false,
+ vehicleSelection: false,
+ additionalItemSelection: false,
+ transactionHistoryView: false,
+ },
+ transactionItems: {
+ primaryItem: {
+ id: 53,
+ name: "Forvogn",
+ price: 649,
+ addons: [
+ {
+ id: 63,
+ quantity: 1,
+ product: {
+ id: 63,
+ name: "Indvendig vask Forvogn",
+ price: 399,
+ },
+ },
+ ],
+ },
+ additionalItems: [
+ {
+ id: 65,
+ name: "Ekstraordinær pr. 10 min inkl. kemi",
+ price: 299,
+ quantity: 1,
+ },
+ ],
+ },
+ categories: {
+ list: [],
+ selected: null,
+ },
+ productList: {
+ list: [],
+ },
+ metadata: {
+ customerId: activeCustomerId,
+ notes: "Visual regression note",
+ reference: "EC21233 - Test Ref. / Intern nummer",
+ washId: null,
+ bookingId: null,
+ laneId: null,
+ },
+ attachments: {
+ files: [],
+ base64: [],
+ wash_certificate: false,
+ },
+ transactionHistory: [],
+ lastVehicleOrders: {
+ vehicle_1: {
+ id: activeOrderId,
+ created_at: "2026-04-08 08:44:07",
+ items: [],
+ },
+ vehicle_2: null,
+ vehicle_3: null,
+ },
+ timestamp: Date.now(),
+ })
+ );
+ },
+ {
+ activeCustomerId: customerId,
+ activeOrderId: orderId,
+ }
+ );
}
async function expectCustomerTabsDividerSpacing(scope) {
@@ -292,10 +298,7 @@ test.describe("POS visuals", () => {
const customerNoteRequests = [];
page.on("request", (request) => {
- if (
- request.method() === "GET" &&
- request.url().includes("/customer/notes?customer_number=12345679")
- ) {
+ if (request.method() === "GET" && request.url().includes("/customer/notes?customer_number=12345679")) {
customerNoteRequests.push(request.url());
}
});
@@ -386,6 +389,9 @@ test.describe("POS visuals", () => {
await expect(stepTwo.getByTestId("pos-order-add-items-panel")).toBeVisible();
await expect(stepTwo.getByTestId("pos-order-panel-cart")).toBeVisible();
await expect(stepTwo.getByTestId("pos-order-metadata-grid")).toBeVisible();
+ await expect(page.locator('[data-testid="pos-order-customer-name"]:visible').first()).toHaveText(
+ /\(TEST\) Pleno Vognmandsforretning/
+ );
await expect(stepTwo).toHaveScreenshot("pos-step-2-desktop.png", {
maxDiffPixels: 300,
});
@@ -447,9 +453,12 @@ test.describe("POS visuals", () => {
await primeSession(page, "pos-visual-desktop-add-items-token");
await openOrderDetailAddItems(page);
- await expect(getVisibleTestId(page, "pos-order-workspace")).toHaveScreenshot("pos-order-detail-add-items-desktop.png", {
- maxDiffPixels: 300,
- });
+ await expect(getVisibleTestId(page, "pos-order-workspace")).toHaveScreenshot(
+ "pos-order-detail-add-items-desktop.png",
+ {
+ maxDiffPixels: 300,
+ }
+ );
});
test("desktop order detail edit modal snapshot", async ({ page }, testInfo) => {
@@ -467,9 +476,12 @@ test.describe("POS visuals", () => {
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await page.getByTestId("pos-order-item-edit-9101").click();
await expect(page.getByTestId("pos-order-item-edit-modal")).toBeVisible();
- await expect(page.getByTestId("pos-order-item-edit-modal")).toHaveScreenshot("pos-order-detail-edit-modal-desktop.png", {
- maxDiffPixels: 300,
- });
+ await expect(page.getByTestId("pos-order-item-edit-modal")).toHaveScreenshot(
+ "pos-order-detail-edit-modal-desktop.png",
+ {
+ maxDiffPixels: 300,
+ }
+ );
});
test("mobile step 2 snapshot", async ({ page }, testInfo) => {
@@ -582,11 +594,19 @@ test.describe("POS visuals", () => {
expect(reloadIconBounds.left).toBeGreaterThan(reloadLabelBounds.right + 8);
expect(reloadActionBounds.right - reloadIconBounds.right).toBeLessThan(24);
- expect(Math.abs(((reloadIconBounds.top + reloadIconBounds.bottom) / 2) - ((reloadActionBounds.top + reloadActionBounds.bottom) / 2))).toBeLessThan(6);
+ expect(
+ Math.abs(
+ (reloadIconBounds.top + reloadIconBounds.bottom) / 2 - (reloadActionBounds.top + reloadActionBounds.bottom) / 2
+ )
+ ).toBeLessThan(6);
expect(closeIconBounds.left).toBeGreaterThan(closeLabelBounds.right + 8);
expect(closeActionBounds.right - closeIconBounds.right).toBeLessThan(24);
- expect(Math.abs(((closeIconBounds.top + closeIconBounds.bottom) / 2) - ((closeActionBounds.top + closeActionBounds.bottom) / 2))).toBeLessThan(6);
+ expect(
+ Math.abs(
+ (closeIconBounds.top + closeIconBounds.bottom) / 2 - (closeActionBounds.top + closeActionBounds.bottom) / 2
+ )
+ ).toBeLessThan(6);
});
test("mobile attachment view actions keep the same side inset", async ({ page }, testInfo) => {
@@ -636,11 +656,20 @@ test.describe("POS visuals", () => {
expect(takePictureIconBounds.left).toBeGreaterThan(takePictureLabelBounds.right + 8);
expect(takePictureActionBounds.right - takePictureIconBounds.right).toBeLessThan(24);
- expect(Math.abs(((takePictureIconBounds.top + takePictureIconBounds.bottom) / 2) - ((takePictureActionBounds.top + takePictureActionBounds.bottom) / 2))).toBeLessThan(6);
+ expect(
+ Math.abs(
+ (takePictureIconBounds.top + takePictureIconBounds.bottom) / 2 -
+ (takePictureActionBounds.top + takePictureActionBounds.bottom) / 2
+ )
+ ).toBeLessThan(6);
expect(closeIconBounds.left).toBeGreaterThan(closeLabelBounds.right + 8);
expect(closeActionBounds.right - closeIconBounds.right).toBeLessThan(24);
- expect(Math.abs(((closeIconBounds.top + closeIconBounds.bottom) / 2) - ((closeActionBounds.top + closeActionBounds.bottom) / 2))).toBeLessThan(6);
+ expect(
+ Math.abs(
+ (closeIconBounds.top + closeIconBounds.bottom) / 2 - (closeActionBounds.top + closeActionBounds.bottom) / 2
+ )
+ ).toBeLessThan(6);
});
test("mobile order detail add-items workspace snapshot", async ({ page }, testInfo) => {
@@ -655,9 +684,12 @@ test.describe("POS visuals", () => {
await primeSession(page, "pos-visual-mobile-add-items-token");
await openOrderDetailAddItems(page);
- await expect(getVisibleTestId(page, "pos-order-workspace")).toHaveScreenshot("pos-order-detail-add-items-mobile.png", {
- maxDiffPixels: 300,
- });
+ await expect(getVisibleTestId(page, "pos-order-workspace")).toHaveScreenshot(
+ "pos-order-detail-add-items-mobile.png",
+ {
+ maxDiffPixels: 300,
+ }
+ );
});
test("mobile step 3 snapshot", async ({ page }, testInfo) => {
diff --git a/tests/e2e/pos.visual.spec.js-snapshots/pos-order-detail-add-items-desktop-chromium-desktop-win32.png b/tests/e2e/pos.visual.spec.js-snapshots/pos-order-detail-add-items-desktop-chromium-desktop-win32.png
index 586dda0d..64c89fac 100644
Binary files a/tests/e2e/pos.visual.spec.js-snapshots/pos-order-detail-add-items-desktop-chromium-desktop-win32.png and b/tests/e2e/pos.visual.spec.js-snapshots/pos-order-detail-add-items-desktop-chromium-desktop-win32.png differ
diff --git a/tests/e2e/pos.visual.spec.js-snapshots/pos-step-2-desktop-chromium-desktop-win32.png b/tests/e2e/pos.visual.spec.js-snapshots/pos-step-2-desktop-chromium-desktop-win32.png
index 03713675..45f0ff73 100644
Binary files a/tests/e2e/pos.visual.spec.js-snapshots/pos-step-2-desktop-chromium-desktop-win32.png and b/tests/e2e/pos.visual.spec.js-snapshots/pos-step-2-desktop-chromium-desktop-win32.png differ
diff --git a/tests/e2e/release/helpers.ts b/tests/e2e/release/helpers.ts
index 037525a1..236388ed 100644
--- a/tests/e2e/release/helpers.ts
+++ b/tests/e2e/release/helpers.ts
@@ -93,10 +93,16 @@ export function attachPageHealthGuards(page: Page, baseURL: string) {
export async function expectBodyHasContent(page: Page, minimumLength = 20) {
await expect
- .poll(async () => {
- const text = await page.locator("body").innerText().catch(() => "");
- return text.replace(/\s+/g, " ").trim().length;
- }, { timeout: 15_000 })
+ .poll(
+ async () => {
+ const text = await page
+ .locator("body")
+ .innerText()
+ .catch(() => "");
+ return text.replace(/\s+/g, " ").trim().length;
+ },
+ { timeout: 15_000 }
+ )
.toBeGreaterThan(minimumLength);
}
@@ -107,15 +113,18 @@ export async function settlePage(page: Page) {
export async function expectOneVisible(locators: Locator[], timeout = 15_000) {
await expect
- .poll(async () => {
- for (const locator of locators) {
- if (await locator.isVisible().catch(() => false)) {
- return true;
+ .poll(
+ async () => {
+ for (const locator of locators) {
+ if (await locator.isVisible().catch(() => false)) {
+ return true;
+ }
}
- }
- return false;
- }, { timeout })
+ return false;
+ },
+ { timeout }
+ )
.toBe(true);
}
diff --git a/tests/e2e/release/release.live-smoke.spec.ts b/tests/e2e/release/release.live-smoke.spec.ts
index 0e862d96..05827a77 100644
--- a/tests/e2e/release/release.live-smoke.spec.ts
+++ b/tests/e2e/release/release.live-smoke.spec.ts
@@ -1,19 +1,13 @@
import { expect, test } from "@playwright/test";
import { loginAsOperator, loginAsUser } from "../fixtures/authHelpers";
-import {
- attachPageHealthGuards,
- expectBodyHasContent,
- expectOneVisible,
- requiredEnv,
- settlePage,
-} from "./helpers";
+import { attachPageHealthGuards, expectBodyHasContent, expectOneVisible, requiredEnv, settlePage } from "./helpers";
const liveSmokeEnabled = Boolean(
- process.env.PLAYWRIGHT_BASE_URL
- && process.env.PLAYWRIGHT_USER_CUSTOMER_NUMBER
- && process.env.PLAYWRIGHT_USER_PASSWORD
- && process.env.PLAYWRIGHT_OPERATOR_USER_ID
- && process.env.PLAYWRIGHT_OPERATOR_PASSWORD,
+ process.env.PLAYWRIGHT_BASE_URL &&
+ process.env.PLAYWRIGHT_USER_CUSTOMER_NUMBER &&
+ process.env.PLAYWRIGHT_USER_PASSWORD &&
+ process.env.PLAYWRIGHT_OPERATOR_USER_ID &&
+ process.env.PLAYWRIGHT_OPERATOR_PASSWORD
);
function getLiveSettings() {
@@ -84,10 +78,7 @@ test.describe("Live smoke release gate", () => {
await page.goto(`/admin/${departmentId}/modules/pos`);
await settlePage(page);
await expect(page).toHaveURL(new RegExp(`/admin/${departmentId}/modules/pos(?:\\?.*)?$`));
- await expectOneVisible([
- page.getByTestId("pos-step-1"),
- page.getByTestId("pos-mobile-step-1-shell"),
- ]);
+ await expectOneVisible([page.getByTestId("pos-step-1"), page.getByTestId("pos-mobile-step-1-shell")]);
await guards.expectHealthy();
guards.dispose();
diff --git a/tests/e2e/release/release.local-prod.spec.ts b/tests/e2e/release/release.local-prod.spec.ts
index 80926b3d..763db83c 100644
--- a/tests/e2e/release/release.local-prod.spec.ts
+++ b/tests/e2e/release/release.local-prod.spec.ts
@@ -1,12 +1,7 @@
import { expect, test } from "@playwright/test";
import { loginAsUser } from "../fixtures/authHelpers";
import { createPosFixture, mockApi, seedAuthenticatedState } from "../support/network.js";
-import {
- attachPageHealthGuards,
- expectBodyHasContent,
- expectOneVisible,
- settlePage,
-} from "./helpers";
+import { attachPageHealthGuards, expectBodyHasContent, expectOneVisible, settlePage } from "./helpers";
const LOCAL_PROD_BASE_URL = "http://127.0.0.1:4173";
const LOGIN_URL = /\/login(?:\?.*)?$/;
@@ -27,17 +22,20 @@ async function gotoHealthyRoute(page, path: string) {
await expect(page.locator("body")).not.toContainText(/404/i);
}
-async function seedAuthenticatedRole(page, {
- token,
- permissions,
- sessionData = {},
- pos = undefined,
-}: {
- token: string;
- permissions: string[];
- sessionData?: Record;
- pos?: unknown;
-}) {
+async function seedAuthenticatedRole(
+ page,
+ {
+ token,
+ permissions,
+ sessionData = {},
+ pos = undefined,
+ }: {
+ token: string;
+ permissions: string[];
+ sessionData?: Record;
+ pos?: unknown;
+ }
+) {
await mockApi(page, {
authenticated: true,
permissions,
@@ -54,7 +52,7 @@ test.describe("Local production release gate", () => {
test("PWA bootstrap registers on the production build", async ({ page }, testInfo) => {
test.skip(
!testInfo.project.name.startsWith("chromium"),
- "Service worker registration is release-gated on Chromium where Playwright exposes stable registrations.",
+ "Service worker registration is release-gated on Chromium where Playwright exposes stable registrations."
);
const guards = attachPageHealthGuards(page, LOCAL_PROD_BASE_URL);
@@ -64,12 +62,16 @@ test.describe("Local production release gate", () => {
await gotoHealthyRoute(page, "/");
await expect(page.getByTestId("landing-title")).toContainText("Truck Wash");
- await page.waitForFunction(async () => {
- const registrations = await navigator.serviceWorker.getRegistrations();
- return registrations.some((registration) => Boolean(
- registration.active || registration.installing || registration.waiting,
- ));
- }, undefined, { timeout: 20_000 });
+ await page.waitForFunction(
+ async () => {
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ return registrations.some((registration) =>
+ Boolean(registration.active || registration.installing || registration.waiting)
+ );
+ },
+ undefined,
+ { timeout: 20_000 }
+ );
await guards.expectHealthy();
guards.dispose();
@@ -178,10 +180,7 @@ test.describe("Local production release gate", () => {
});
await gotoHealthyRoute(page, "/admin/12/modules/pos");
- await expectOneVisible([
- page.getByTestId("pos-step-1"),
- page.getByTestId("pos-mobile-step-1-shell"),
- ]);
+ await expectOneVisible([page.getByTestId("pos-step-1"), page.getByTestId("pos-mobile-step-1-shell")]);
await guards.expectHealthy();
guards.dispose();
diff --git a/tests/e2e/self-serve-wash.spec.js b/tests/e2e/self-serve-wash.spec.js
index 2e88302a..b319baae 100644
--- a/tests/e2e/self-serve-wash.spec.js
+++ b/tests/e2e/self-serve-wash.spec.js
@@ -4,7 +4,8 @@ import { mockApi, seedAuthenticatedState } from "./support/network.js";
async function suppressVueDevtoolsOverlay(page) {
await page.addInitScript(() => {
const style = document.createElement("style");
- style.textContent = "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
+ style.textContent =
+ "#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
document.documentElement.appendChild(style);
});
}
@@ -12,20 +13,23 @@ async function suppressVueDevtoolsOverlay(page) {
async function primeSession(page, { token, permissions, sessionData = {} }) {
await seedAuthenticatedState(page, token);
await page.goto("/login");
- await page.evaluate(async ({ sessionToken, sessionPermissions, data }) => {
- const sessionModule = await import("/src/components/session/token/SessionUser.vue");
- window.localStorage.setItem("token", sessionToken);
- sessionModule.SessionUser.token.value = sessionToken;
- sessionModule.SessionUser.authenticated.value = true;
- sessionModule.SessionUser.permissions.value = sessionPermissions;
- sessionModule.SessionUser.initiated.value = true;
- sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345;
- sessionModule.SessionUser.user.display_name.value = data.display_name ?? "E2E User";
- }, {
- sessionToken: token,
- sessionPermissions: permissions,
- data: sessionData,
- });
+ await page.evaluate(
+ async ({ sessionToken, sessionPermissions, data }) => {
+ const sessionModule = await import("/src/components/session/token/SessionUser.vue");
+ window.localStorage.setItem("token", sessionToken);
+ sessionModule.SessionUser.token.value = sessionToken;
+ sessionModule.SessionUser.authenticated.value = true;
+ sessionModule.SessionUser.permissions.value = sessionPermissions;
+ sessionModule.SessionUser.initiated.value = true;
+ sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345;
+ sessionModule.SessionUser.user.display_name.value = data.display_name ?? "E2E User";
+ },
+ {
+ sessionToken: token,
+ sessionPermissions: permissions,
+ data: sessionData,
+ }
+ );
}
async function seedGeolocation(context) {
@@ -91,14 +95,19 @@ async function seedSavedProgress(page, overrides = {}) {
}
test.describe("Self-serve wash", () => {
- test.skip(({ browserName }) => browserName !== "chromium", "Self-serve coverage is maintained on Chromium desktop/mobile.");
+ test.skip(
+ ({ browserName }) => browserName !== "chromium",
+ "Self-serve coverage is maintained on Chromium desktop/mobile."
+ );
test.beforeEach(async ({ page, context }) => {
await suppressVueDevtoolsOverlay(page);
await seedGeolocation(context);
});
- test("@smoke landing route sorts departments, loads vehicle selection, and restores direct wash progress", async ({ page }) => {
+ test("@smoke landing route sorts departments, loads vehicle selection, and restores direct wash progress", async ({
+ page,
+ }) => {
await mockApi(page, {
authenticated: true,
permissions: ["user"],
@@ -114,13 +123,10 @@ test.describe("Self-serve wash", () => {
await expect(page.getByTestId("self-serve-wash-home")).toBeVisible();
await expect(page.getByTestId("self-serve-home-nearest-name")).toContainText("Copenhagen");
- const orderedDepartmentIds = await page.locator('[data-testid^="self-serve-home-department-"]').evaluateAll(
- (elements) => elements.map((element) => element.getAttribute("data-testid"))
- );
- expect(orderedDepartmentIds).toEqual([
- "self-serve-home-department-2",
- "self-serve-home-department-3",
- ]);
+ const orderedDepartmentIds = await page
+ .locator('[data-testid^="self-serve-home-department-"]')
+ .evaluateAll((elements) => elements.map((element) => element.getAttribute("data-testid")));
+ expect(orderedDepartmentIds).toEqual(["self-serve-home-department-2", "self-serve-home-department-3"]);
await page.getByTestId("self-serve-home-start").click();
await expect(page).toHaveURL(/\/user\/wash\/start$/);
@@ -177,7 +183,9 @@ test.describe("Self-serve wash", () => {
await expect(page.getByTestId("self-serve-department-name")).toContainText("Copenhagen");
});
- test("lane flow gates invalid lanes, direct wash path auto-starts, and expired progress is discarded", async ({ page }) => {
+ test("lane flow gates invalid lanes, direct wash path auto-starts, and expired progress is discarded", async ({
+ page,
+ }) => {
await seedSavedProgress(page, {
washInProgress: true,
washLaneId: 7,
@@ -188,7 +196,7 @@ test.describe("Self-serve wash", () => {
radioWashType: "Manual",
radioLaneOption: 7,
customerNumberInput: "12345",
- savedAt: Date.now() - (3 * 60 * 60 * 1000),
+ savedAt: Date.now() - 3 * 60 * 60 * 1000,
});
await mockApi(page, {
@@ -251,12 +259,25 @@ test.describe("Self-serve wash", () => {
lane: { id: 7, name: "7" },
session: { id: 501, status: "IN_PROGRESS", allowed: true },
questions: [
- { id: 11, question: "Is the tarp removed?", description: "Required before machine wash.", answer: true, order_priority: 1 },
+ {
+ id: 11,
+ question: "Is the tarp removed?",
+ description: "Required before machine wash.",
+ answer: true,
+ order_priority: 1,
+ },
],
conditions: [],
rules: [],
tasks: [
- { id: 9001, task: "Prepare the truck", description: "Complete the pre-wash checks.", order_priority: 1, services: ["MACHINE"], buttons: [1] },
+ {
+ id: 9001,
+ task: "Prepare the truck",
+ description: "Complete the pre-wash checks.",
+ order_priority: 1,
+ services: ["MACHINE"],
+ buttons: [1],
+ },
],
allowed_services: ["MACHINE"],
},
@@ -266,16 +287,27 @@ test.describe("Self-serve wash", () => {
session: { id: 501, status: "IN_PROGRESS", allowed: true },
lane: { id: 7, name: "7" },
questions: [
- { id: 11, question: "Is the tarp removed?", description: "Required before machine wash.", answer: true, order_priority: 1 },
+ {
+ id: 11,
+ question: "Is the tarp removed?",
+ description: "Required before machine wash.",
+ answer: true,
+ order_priority: 1,
+ },
],
conditions: [],
rules: [],
tasks: [
- { id: 9001, task: "Prepare the truck", description: "Complete the pre-wash checks.", order_priority: 1, services: ["MACHINE"], buttons: [1] },
- ],
- events: [
- { id: 3, type: "QUESTION_ANSWERED", created_at: "2026-01-01T10:01:00.000Z" },
+ {
+ id: 9001,
+ task: "Prepare the truck",
+ description: "Complete the pre-wash checks.",
+ order_priority: 1,
+ services: ["MACHINE"],
+ buttons: [1],
+ },
],
+ events: [{ id: 3, type: "QUESTION_ANSWERED", created_at: "2026-01-01T10:01:00.000Z" }],
},
},
},
diff --git a/tests/e2e/subuserProfileContact.spec.ts b/tests/e2e/subuserProfileContact.spec.ts
index bde2add5..a8a90c4b 100644
--- a/tests/e2e/subuserProfileContact.spec.ts
+++ b/tests/e2e/subuserProfileContact.spec.ts
@@ -1,135 +1,132 @@
-import { test, expect } from '@playwright/test';
-import {
- userCredentials,
- loginAsSubuserByPhone,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { userCredentials, loginAsSubuserByPhone } from "./fixtures";
// Navigate to profile page helper
async function goToUserProfile(page) {
await page.click('a[href="/user/profile"]');
- await expect(page).toHaveURL('/user/profile');
+ await expect(page).toHaveURL("/user/profile");
}
// Subuser contact information display tests
-test('[PROFILE][Subuser][Contact] should display contact information section', async ({ page }) => {
+test("[PROFILE][Subuser][Contact] should display contact information section", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Should display the contact information section header (look for the address-card icon section)
- await expect(page.locator('.card-header .fa-address-card')).toBeVisible();
+ await expect(page.locator(".card-header .fa-address-card")).toBeVisible();
});
-test('[PROFILE][Subuser][Contact] should display email field', async ({ page }) => {
+test("[PROFILE][Subuser][Contact] should display email field", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand contact section (second Chauffør section with address-card icon)
- const contactHeader = page.locator('.card-header').filter({ has: page.locator('.fa-address-card') });
+ const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
await contactHeader.click();
await page.waitForTimeout(200);
-
+
// Email field should be visible
- await expect(page.getByText('E-mail', { exact: true })).toBeVisible();
+ await expect(page.getByText("E-mail", { exact: true })).toBeVisible();
});
-test('[PROFILE][Subuser][Contact] should display edit email button', async ({ page }) => {
+test("[PROFILE][Subuser][Contact] should display edit email button", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand contact section
- const contactHeader = page.locator('.card-header').filter({ has: page.locator('.fa-address-card') });
+ const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
await contactHeader.click();
await page.waitForTimeout(200);
-
+
// Edit email button should be visible for subuser
const editEmailButton = page.locator('button:has-text("Rediger e-mail")');
await expect(editEmailButton).toBeVisible();
});
-test('[PROFILE][Subuser][Contact] should open edit email dialog when clicking edit email button', async ({ page }) => {
+test("[PROFILE][Subuser][Contact] should open edit email dialog when clicking edit email button", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand contact section
- const contactHeader = page.locator('.card-header').filter({ has: page.locator('.fa-address-card') });
+ const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
await contactHeader.click();
await page.waitForTimeout(200);
-
+
// Click edit email button
const editEmailButton = page.locator('button:has-text("Rediger e-mail")');
await editEmailButton.click();
-
+
// Should show email dialog (SweetAlert)
- await expect(page.locator('.swal2-popup')).toBeVisible();
- await expect(page.locator('.swal2-title')).toContainText('e-mailadresse');
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+ await expect(page.locator(".swal2-title")).toContainText("e-mailadresse");
});
-test('[PROFILE][Subuser][Contact] should display phone number field (read-only)', async ({ page }) => {
+test("[PROFILE][Subuser][Contact] should display phone number field (read-only)", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand contact section
- const contactHeader = page.locator('.card-header').filter({ has: page.locator('.fa-address-card') });
+ const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
await contactHeader.click();
await page.waitForTimeout(200);
-
+
// Phone number field should be visible
- await expect(page.getByText('Telefonnummer', { exact: true }).first()).toBeVisible();
+ await expect(page.getByText("Telefonnummer", { exact: true }).first()).toBeVisible();
});
-test('[PROFILE][Subuser][Contact] should display country code field (read-only)', async ({ page }) => {
+test("[PROFILE][Subuser][Contact] should display country code field (read-only)", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand contact section
- const contactHeader = page.locator('.card-header').filter({ has: page.locator('.fa-address-card') });
+ const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
await contactHeader.click();
await page.waitForTimeout(200);
-
+
// Country code field should be visible
- await expect(page.getByText('Landekode', { exact: true })).toBeVisible();
+ await expect(page.getByText("Landekode", { exact: true })).toBeVisible();
});
/** Subuser Email Validation Tests */
-test('[PROFILE][Subuser][Contact] should show validation error when email is empty', async ({ page }) => {
+test("[PROFILE][Subuser][Contact] should show validation error when email is empty", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand contact section
- const contactHeader = page.locator('.card-header').filter({ has: page.locator('.fa-address-card') });
+ const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
await contactHeader.click();
await page.waitForTimeout(200);
-
+
// Click edit email button
const editEmailButton = page.locator('button:has-text("Rediger e-mail")');
await editEmailButton.click();
-
+
// Clear the input and click confirm
- await page.fill('.swal2-input', '');
- await page.click('.swal2-confirm');
-
+ await page.fill(".swal2-input", "");
+ await page.click(".swal2-confirm");
+
// Should show validation error
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
-test('[PROFILE][Subuser][Contact] should show validation error for invalid email format', async ({ page }) => {
+test("[PROFILE][Subuser][Contact] should show validation error for invalid email format", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand contact section
- const contactHeader = page.locator('.card-header').filter({ has: page.locator('.fa-address-card') });
+ const contactHeader = page.locator(".card-header").filter({ has: page.locator(".fa-address-card") });
await contactHeader.click();
await page.waitForTimeout(200);
-
+
// Click edit email button
const editEmailButton = page.locator('button:has-text("Rediger e-mail")');
await editEmailButton.click();
-
+
// Fill in invalid email
- await page.fill('.swal2-input', 'invalid-email-format');
- await page.click('.swal2-confirm');
-
+ await page.fill(".swal2-input", "invalid-email-format");
+ await page.click(".swal2-confirm");
+
// Should show validation error
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
diff --git a/tests/e2e/subuserProfileGrant.spec.ts b/tests/e2e/subuserProfileGrant.spec.ts
index ffb0f133..452e1215 100644
--- a/tests/e2e/subuserProfileGrant.spec.ts
+++ b/tests/e2e/subuserProfileGrant.spec.ts
@@ -1,35 +1,32 @@
-import { test, expect } from '@playwright/test';
-import {
- userCredentials,
- loginAsSubuserByUsername,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { userCredentials, loginAsSubuserByUsername } from "./fixtures";
// Navigate to profile page helper
async function goToUserProfile(page) {
await page.click('a[href="/user/profile"]');
- await expect(page).toHaveURL('/user/profile');
+ await expect(page).toHaveURL("/user/profile");
}
// Subuser grant selection display tests
-test('[PROFILE][Subuser][Grant] should display customer selection section', async ({ page }) => {
+test("[PROFILE][Subuser][Grant] should display customer selection section", async ({ page }) => {
await loginAsSubuserByUsername(page);
await goToUserProfile(page);
-
+
// Should display the customer selection section header (look for users icon)
- await expect(page.locator('.card-header .fa-users')).toBeVisible();
+ await expect(page.locator(".card-header .fa-users")).toBeVisible();
});
-test('[PROFILE][Subuser][Grant] should display customer switch help text', async ({ page }) => {
+test("[PROFILE][Subuser][Grant] should display customer switch help text", async ({ page }) => {
await loginAsSubuserByUsername(page);
await goToUserProfile(page);
-
+
// Expand grant section (with users icon)
- const grantHeader = page.locator('.card-header').filter({ has: page.locator('.fa-users') });
+ const grantHeader = page.locator(".card-header").filter({ has: page.locator(".fa-users") });
await grantHeader.click();
await page.waitForTimeout(200);
-
+
// Should display help text for customer switching (inside the expanded card with users icon)
- const grantCard = page.locator('.card').filter({ has: page.locator('.fa-users') });
- const helpText = grantCard.locator('.card-content .help');
+ const grantCard = page.locator(".card").filter({ has: page.locator(".fa-users") });
+ const helpText = grantCard.locator(".card-content .help");
await expect(helpText).toBeVisible();
});
diff --git a/tests/e2e/subuserProfileInformation.spec.ts b/tests/e2e/subuserProfileInformation.spec.ts
index 531921a7..e028277a 100644
--- a/tests/e2e/subuserProfileInformation.spec.ts
+++ b/tests/e2e/subuserProfileInformation.spec.ts
@@ -1,141 +1,138 @@
-import { test, expect } from '@playwright/test';
-import {
- userCredentials,
- loginAsSubuserByPhone,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { userCredentials, loginAsSubuserByPhone } from "./fixtures";
// Navigate to profile page helper
async function goToUserProfile(page) {
await page.click('a[href="/user/profile"]');
- await expect(page).toHaveURL('/user/profile');
+ await expect(page).toHaveURL("/user/profile");
}
// Helper to expand a category section by clicking its header
async function expandCategory(page, categoryText: string) {
- const categoryHeader = page.locator('.card-header').filter({ hasText: categoryText }).first();
+ const categoryHeader = page.locator(".card-header").filter({ hasText: categoryText }).first();
await categoryHeader.click();
// Wait for expansion animation
await page.waitForTimeout(200);
}
// Subuser profile information display tests
-test('[PROFILE][Subuser][Profile] should display profile information section', async ({ page }) => {
+test("[PROFILE][Subuser][Profile] should display profile information section", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Should display the driver/subuser profile section header
- await expect(page.locator('.card-header').filter({ hasText: 'Chauffør' }).first()).toBeVisible();
+ await expect(page.locator(".card-header").filter({ hasText: "Chauffør" }).first()).toBeVisible();
});
-test('[PROFILE][Subuser][Profile] should display username field (read-only)', async ({ page }) => {
+test("[PROFILE][Subuser][Profile] should display username field (read-only)", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand the profile section (first Chauffør section)
- await expandCategory(page, 'Chauffør');
-
+ await expandCategory(page, "Chauffør");
+
// Username field should be visible
- await expect(page.getByText('Brugernavn', { exact: true })).toBeVisible();
+ await expect(page.getByText("Brugernavn", { exact: true })).toBeVisible();
});
-test('[PROFILE][Subuser][Profile] should display name field', async ({ page }) => {
+test("[PROFILE][Subuser][Profile] should display name field", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand the profile section
- await expandCategory(page, 'Chauffør');
-
+ await expandCategory(page, "Chauffør");
+
// Name field should be visible
- await expect(page.getByText('Navn', { exact: true })).toBeVisible();
+ await expect(page.getByText("Navn", { exact: true })).toBeVisible();
});
-test('[PROFILE][Subuser][Profile] should display edit name button', async ({ page }) => {
+test("[PROFILE][Subuser][Profile] should display edit name button", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand the profile section
- await expandCategory(page, 'Chauffør');
-
+ await expandCategory(page, "Chauffør");
+
// Edit name button should be visible
const editNameButton = page.locator('button:has-text("Rediger navn")');
await expect(editNameButton).toBeVisible();
});
-test('[PROFILE][Subuser][Profile] should open edit name dialog when clicking edit name button', async ({ page }) => {
+test("[PROFILE][Subuser][Profile] should open edit name dialog when clicking edit name button", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand the profile section
- await expandCategory(page, 'Chauffør');
-
+ await expandCategory(page, "Chauffør");
+
// Click edit name button
const editNameButton = page.locator('button:has-text("Rediger navn")');
await editNameButton.click();
-
+
// Should show name dialog (SweetAlert)
- await expect(page.locator('.swal2-popup')).toBeVisible();
- await expect(page.locator('.swal2-title')).toContainText('navn');
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+ await expect(page.locator(".swal2-title")).toContainText("navn");
});
/** Subuser Name Validation Tests */
-test('[PROFILE][Subuser][Profile] should show validation error when name is empty', async ({ page }) => {
+test("[PROFILE][Subuser][Profile] should show validation error when name is empty", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand the profile section
- await expandCategory(page, 'Chauffør');
-
+ await expandCategory(page, "Chauffør");
+
// Click edit name button
const editNameButton = page.locator('button:has-text("Rediger navn")');
await editNameButton.click();
-
+
// Clear the input and click confirm
- await page.fill('.swal2-input', '');
- await page.click('.swal2-confirm');
-
+ await page.fill(".swal2-input", "");
+ await page.click(".swal2-confirm");
+
// Should show validation error
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
-test('[PROFILE][Subuser][Profile] should show validation error when name is too short', async ({ page }) => {
+test("[PROFILE][Subuser][Profile] should show validation error when name is too short", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand the profile section
- await expandCategory(page, 'Chauffør');
-
+ await expandCategory(page, "Chauffør");
+
// Click edit name button
const editNameButton = page.locator('button:has-text("Rediger navn")');
await editNameButton.click();
-
+
// Fill in name that is too short
- await page.fill('.swal2-input', 'A');
- await page.click('.swal2-confirm');
-
+ await page.fill(".swal2-input", "A");
+ await page.click(".swal2-confirm");
+
// Should show validation error
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
/** Dialog Cancel Tests */
-test('[PROFILE][Subuser][Profile] should close dialog when cancel is clicked on name change', async ({ page }) => {
+test("[PROFILE][Subuser][Profile] should close dialog when cancel is clicked on name change", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand the profile section
- await expandCategory(page, 'Chauffør');
-
+ await expandCategory(page, "Chauffør");
+
// Click edit name button
const editNameButton = page.locator('button:has-text("Rediger navn")');
await editNameButton.click();
-
+
// Verify dialog is open
- await expect(page.locator('.swal2-popup')).toBeVisible();
-
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+
// Click cancel
- await page.click('.swal2-cancel');
-
+ await page.click(".swal2-cancel");
+
// Dialog should be closed
- await expect(page.locator('.swal2-popup')).not.toBeVisible();
+ await expect(page.locator(".swal2-popup")).not.toBeVisible();
});
diff --git a/tests/e2e/subuserProfileSecurity.spec.ts b/tests/e2e/subuserProfileSecurity.spec.ts
index f116e098..3c6d8af1 100644
--- a/tests/e2e/subuserProfileSecurity.spec.ts
+++ b/tests/e2e/subuserProfileSecurity.spec.ts
@@ -1,163 +1,191 @@
-import { test, expect } from '@playwright/test';
-import {
- userCredentials,
- loginAsSubuserByPhone,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { userCredentials, loginAsSubuserByPhone } from "./fixtures";
// Navigate to profile page helper
async function goToUserProfile(page) {
await page.click('a[href="/user/profile"]');
- await expect(page).toHaveURL('/user/profile');
+ await expect(page).toHaveURL("/user/profile");
}
// Subuser security section display tests
-test('[PROFILE][Subuser][Security] should display security section', async ({ page }) => {
+test("[PROFILE][Subuser][Security] should display security section", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Should display the security section header (look for shield-alt icon)
- await expect(page.locator('.card-header .fa-shield-alt')).toBeVisible();
+ await expect(page.locator(".card-header .fa-shield-alt")).toBeVisible();
});
-test('[PROFILE][Subuser][Security] should display logout button', async ({ page }) => {
+test("[PROFILE][Subuser][Security] should display logout button", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand security section (with shield-alt icon)
- const securityHeader = page.locator('.card-header').filter({ has: page.locator('.fa-shield-alt') });
+ const securityHeader = page.locator(".card-header").filter({ has: page.locator(".fa-shield-alt") });
await securityHeader.click();
await page.waitForTimeout(200);
-
+
// Logout button should be visible
const logoutButton = page.locator('button:has-text("Log ud")');
await expect(logoutButton).toBeVisible();
});
-test('[PROFILE][Subuser][Security] should open logout confirmation dialog when clicking logout button', async ({ page }) => {
+test("[PROFILE][Subuser][Security] should open logout confirmation dialog when clicking logout button", async ({
+ page,
+}) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand security section
- const securityHeader = page.locator('.card-header').filter({ has: page.locator('.fa-shield-alt') });
+ const securityHeader = page.locator(".card-header").filter({ has: page.locator(".fa-shield-alt") });
await securityHeader.click();
await page.waitForTimeout(200);
-
+
// Click logout button
const logoutButton = page.locator('button:has-text("Log ud")');
await logoutButton.click();
-
+
// Should show confirmation dialog (SweetAlert)
- await expect(page.locator('.swal2-popup')).toBeVisible();
+ await expect(page.locator(".swal2-popup")).toBeVisible();
});
// Passkey management tests
-test('[PROFILE][Subuser][Security][Passkey] should display passkey management section', async ({ page }) => {
+test("[PROFILE][Subuser][Security][Passkey] should display passkey management section", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand security section
- const securityHeader = page.locator('.card-header').filter({ has: page.locator('.fa-shield-alt') });
+ const securityHeader = page.locator(".card-header").filter({ has: page.locator(".fa-shield-alt") });
await securityHeader.click();
await page.waitForTimeout(200);
-
+
// Should display passkey section (look for key icon or passkey text)
- const passkeySection = page.locator('text=Passkey').or(page.locator('.fa-key'));
+ const passkeySection = page.locator("text=Passkey").or(page.locator(".fa-key"));
await expect(passkeySection.first()).toBeVisible();
});
-test('[PROFILE][Subuser][Security][Passkey] should display register passkey button', async ({ page }) => {
+test("[PROFILE][Subuser][Security][Passkey] should display register passkey button", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand security section
- const securityHeader = page.locator('.card-header').filter({ has: page.locator('.fa-shield-alt') });
+ const securityHeader = page.locator(".card-header").filter({ has: page.locator(".fa-shield-alt") });
await securityHeader.click();
await page.waitForTimeout(200);
-
+
// Register passkey button should be visible
- const registerButton = page.locator('button:has-text("Registrer passkey")').or(page.locator('button:has-text("Tilføj passkey")'));
+ const registerButton = page
+ .locator('button:has-text("Registrer passkey")')
+ .or(page.locator('button:has-text("Tilføj passkey")'));
await expect(registerButton.first()).toBeVisible();
});
-test('[PROFILE][Subuser][Security][Passkey] should show passkey registration dialog when clicking register button', async ({ page }) => {
+test("[PROFILE][Subuser][Security][Passkey] should show passkey registration dialog when clicking register button", async ({
+ page,
+}) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand security section
- const securityHeader = page.locator('.card-header').filter({ has: page.locator('.fa-shield-alt') });
+ const securityHeader = page.locator(".card-header").filter({ has: page.locator(".fa-shield-alt") });
await securityHeader.click();
await page.waitForTimeout(200);
-
+
// Click register passkey button
- const registerButton = page.locator('button:has-text("Registrer passkey")').or(page.locator('button:has-text("Tilføj passkey")'));
+ const registerButton = page
+ .locator('button:has-text("Registrer passkey")')
+ .or(page.locator('button:has-text("Tilføj passkey")'));
await registerButton.first().click();
-
+
// Should show registration dialog or WebAuthn prompt
// Wait for either a modal or the browser's WebAuthn prompt handling
await page.waitForTimeout(500);
-
+
// Check if a modal/dialog appeared or if there's an error message (WebAuthn not supported)
const modalVisible = await page.locator('.modal, .swal2-popup, [role="dialog"]').isVisible();
- const errorVisible = await page.locator('text=ikke understøttet').or(page.locator('text=not supported')).isVisible();
-
+ const errorVisible = await page.locator("text=ikke understøttet").or(page.locator("text=not supported")).isVisible();
+
expect(modalVisible || errorVisible).toBeTruthy();
});
// Two-Factor Authentication (2FA) tests
-test('[PROFILE][Subuser][Security][2FA] should display 2FA management section', async ({ page }) => {
+test("[PROFILE][Subuser][Security][2FA] should display 2FA management section", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand security section
- const securityHeader = page.locator('.card-header').filter({ has: page.locator('.fa-shield-alt') });
+ const securityHeader = page.locator(".card-header").filter({ has: page.locator(".fa-shield-alt") });
await securityHeader.click();
await page.waitForTimeout(200);
-
+
// Should display 2FA section (look for shield icon or 2FA/two-factor text)
- const twoFactorSection = page.locator('text=To-faktor').or(page.locator('text=2FA')).or(page.locator('.fa-shield'));
+ const twoFactorSection = page.locator("text=To-faktor").or(page.locator("text=2FA")).or(page.locator(".fa-shield"));
await expect(twoFactorSection.first()).toBeVisible();
});
-test('[PROFILE][Subuser][Security][2FA] should display enable 2FA button when 2FA is disabled', async ({ page }) => {
+test("[PROFILE][Subuser][Security][2FA] should display enable 2FA button when 2FA is disabled", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand security section
- const securityHeader = page.locator('.card-header').filter({ has: page.locator('.fa-shield-alt') });
+ const securityHeader = page.locator(".card-header").filter({ has: page.locator(".fa-shield-alt") });
await securityHeader.click();
await page.waitForTimeout(200);
-
+
// Enable 2FA button should be visible (or disable if already enabled)
- const enableButton = page.locator('button:has-text("Aktivér 2FA")').or(page.locator('button:has-text("Aktiver to-faktor")'));
- const disableButton = page.locator('button:has-text("Deaktivér 2FA")').or(page.locator('button:has-text("Deaktiver to-faktor")'));
-
+ const enableButton = page
+ .locator('button:has-text("Aktivér 2FA")')
+ .or(page.locator('button:has-text("Aktiver to-faktor")'));
+ const disableButton = page
+ .locator('button:has-text("Deaktivér 2FA")')
+ .or(page.locator('button:has-text("Deaktiver to-faktor")'));
+
// Either enable or disable button should be visible depending on current state
- const enableVisible = await enableButton.first().isVisible().catch(() => false);
- const disableVisible = await disableButton.first().isVisible().catch(() => false);
-
+ const enableVisible = await enableButton
+ .first()
+ .isVisible()
+ .catch(() => false);
+ const disableVisible = await disableButton
+ .first()
+ .isVisible()
+ .catch(() => false);
+
expect(enableVisible || disableVisible).toBeTruthy();
});
-test('[PROFILE][Subuser][Security][2FA] should show 2FA setup dialog when clicking enable button', async ({ page }) => {
+test("[PROFILE][Subuser][Security][2FA] should show 2FA setup dialog when clicking enable button", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand security section
- const securityHeader = page.locator('.card-header').filter({ has: page.locator('.fa-shield-alt') });
+ const securityHeader = page.locator(".card-header").filter({ has: page.locator(".fa-shield-alt") });
await securityHeader.click();
await page.waitForTimeout(200);
-
+
// Try to click enable 2FA button if visible
- const enableButton = page.locator('button:has-text("Aktivér 2FA")').or(page.locator('button:has-text("Aktiver to-faktor")'));
-
- if (await enableButton.first().isVisible().catch(() => false)) {
+ const enableButton = page
+ .locator('button:has-text("Aktivér 2FA")')
+ .or(page.locator('button:has-text("Aktiver to-faktor")'));
+
+ if (
+ await enableButton
+ .first()
+ .isVisible()
+ .catch(() => false)
+ ) {
await enableButton.first().click();
await page.waitForTimeout(500);
-
+
// Should show QR code dialog or setup modal
- const qrCodeVisible = await page.locator('img[alt*="QR"], canvas, .qr-code').isVisible().catch(() => false);
- const modalVisible = await page.locator('.modal, .swal2-popup, [role="dialog"]').isVisible().catch(() => false);
-
+ const qrCodeVisible = await page
+ .locator('img[alt*="QR"], canvas, .qr-code')
+ .isVisible()
+ .catch(() => false);
+ const modalVisible = await page
+ .locator('.modal, .swal2-popup, [role="dialog"]')
+ .isVisible()
+ .catch(() => false);
+
expect(qrCodeVisible || modalVisible).toBeTruthy();
} else {
// 2FA is already enabled, skip this test
@@ -165,34 +193,56 @@ test('[PROFILE][Subuser][Security][2FA] should show 2FA setup dialog when clicki
}
});
-test('[PROFILE][Subuser][Security][2FA] should validate 6-digit code input', async ({ page }) => {
+test("[PROFILE][Subuser][Security][2FA] should validate 6-digit code input", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Expand security section
- const securityHeader = page.locator('.card-header').filter({ has: page.locator('.fa-shield-alt') });
+ const securityHeader = page.locator(".card-header").filter({ has: page.locator(".fa-shield-alt") });
await securityHeader.click();
await page.waitForTimeout(200);
-
+
// Try to click enable 2FA button if visible to open the dialog
- const enableButton = page.locator('button:has-text("Aktivér 2FA")').or(page.locator('button:has-text("Aktiver to-faktor")'));
-
- if (await enableButton.first().isVisible().catch(() => false)) {
+ const enableButton = page
+ .locator('button:has-text("Aktivér 2FA")')
+ .or(page.locator('button:has-text("Aktiver to-faktor")'));
+
+ if (
+ await enableButton
+ .first()
+ .isVisible()
+ .catch(() => false)
+ ) {
await enableButton.first().click();
await page.waitForTimeout(500);
-
+
// Look for code input field
- const codeInput = page.locator('input[type="text"][maxlength="6"], input[placeholder*="kode"], input[name*="code"]');
-
- if (await codeInput.first().isVisible().catch(() => false)) {
+ const codeInput = page.locator(
+ 'input[type="text"][maxlength="6"], input[placeholder*="kode"], input[name*="code"]'
+ );
+
+ if (
+ await codeInput
+ .first()
+ .isVisible()
+ .catch(() => false)
+ ) {
// Try entering invalid code (less than 6 digits)
- await codeInput.first().fill('123');
-
+ await codeInput.first().fill("123");
+
// Submit button should be disabled or validation error should show
- const submitButton = page.locator('button[type="submit"], button:has-text("Bekræft"), button:has-text("Verificer")');
- const isDisabled = await submitButton.first().isDisabled().catch(() => false);
- const hasError = await page.locator('.error, .is-danger, [class*="error"]').isVisible().catch(() => false);
-
+ const submitButton = page.locator(
+ 'button[type="submit"], button:has-text("Bekræft"), button:has-text("Verificer")'
+ );
+ const isDisabled = await submitButton
+ .first()
+ .isDisabled()
+ .catch(() => false);
+ const hasError = await page
+ .locator('.error, .is-danger, [class*="error"]')
+ .isVisible()
+ .catch(() => false);
+
// Either button disabled or error shown for invalid input
expect(isDisabled || hasError || true).toBeTruthy(); // Allow pass if validation is client-side
}
diff --git a/tests/e2e/subuserProfileUsername.spec.ts b/tests/e2e/subuserProfileUsername.spec.ts
index 20827148..44777a91 100644
--- a/tests/e2e/subuserProfileUsername.spec.ts
+++ b/tests/e2e/subuserProfileUsername.spec.ts
@@ -1,37 +1,34 @@
-import { test, expect } from '@playwright/test';
-import {
- userCredentials,
- loginAsSubuserByUsername,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { userCredentials, loginAsSubuserByUsername } from "./fixtures";
// Navigate to profile page helper
async function goToUserProfile(page) {
await page.click('a[href="/user/profile"]');
- await expect(page).toHaveURL('/user/profile');
+ await expect(page).toHaveURL("/user/profile");
}
/** User Profile Tests - Subuser via Username Login */
-test('[PROFILE][Subuser][Username] should display profile section when logged in via username', async ({ page }) => {
+test("[PROFILE][Subuser][Username] should display profile section when logged in via username", async ({ page }) => {
await loginAsSubuserByUsername(page);
await goToUserProfile(page);
-
+
// Should display the driver/subuser profile section header
- await expect(page.locator('.card-header').filter({ hasText: 'Chauffør' }).first()).toBeVisible();
+ await expect(page.locator(".card-header").filter({ hasText: "Chauffør" }).first()).toBeVisible();
});
-test('[PROFILE][Subuser][Username] should display contact section when logged in via username', async ({ page }) => {
+test("[PROFILE][Subuser][Username] should display contact section when logged in via username", async ({ page }) => {
await loginAsSubuserByUsername(page);
await goToUserProfile(page);
-
+
// Should display the contact information section header
- await expect(page.locator('.card-header .fa-address-card')).toBeVisible();
+ await expect(page.locator(".card-header .fa-address-card")).toBeVisible();
});
-test('[PROFILE][Subuser][Username] should display security section when logged in via username', async ({ page }) => {
+test("[PROFILE][Subuser][Username] should display security section when logged in via username", async ({ page }) => {
await loginAsSubuserByUsername(page);
await goToUserProfile(page);
-
+
// Should display the security section header
- await expect(page.locator('.card-header .fa-shield-alt')).toBeVisible();
+ await expect(page.locator(".card-header .fa-shield-alt")).toBeVisible();
});
diff --git a/tests/e2e/superuser-customer-complaints.spec.ts b/tests/e2e/superuser-customer-complaints.spec.ts
index 23a65bd5..9d0ee7b8 100644
--- a/tests/e2e/superuser-customer-complaints.spec.ts
+++ b/tests/e2e/superuser-customer-complaints.spec.ts
@@ -246,11 +246,13 @@ test.describe("Superuser customer complaints", () => {
await expect(page.locator('[data-testid="superuser-complaint-row-102"]')).toHaveCount(0);
await expect
- .poll(() => requestLog.getUrls.some((url) => url.includes("filters=department_id%3A2") || url.includes("filters=department_id:2")))
- .toBe(true);
- await expect
- .poll(() => requestLog.getUrls.some((url) => url.toLowerCase().includes("search=mirror")))
+ .poll(() =>
+ requestLog.getUrls.some(
+ (url) => url.includes("filters=department_id%3A2") || url.includes("filters=department_id:2")
+ )
+ )
.toBe(true);
+ await expect.poll(() => requestLog.getUrls.some((url) => url.toLowerCase().includes("search=mirror"))).toBe(true);
});
test("edits complaints with department, searchable customer selection, and description updates", async ({ page }) => {
@@ -294,7 +296,9 @@ test.describe("Superuser customer complaints", () => {
await expect(page.getByTestId("superuser-complaint-wash-date-101")).toContainText(/8\.4\.2026|08\.04\.2026/);
});
- test("requires wash date/category for legacy rows, supports clearing the optional customer number, and deletes complaints", async ({ page }) => {
+ test("requires wash date/category for legacy rows, supports clearing the optional customer number, and deletes complaints", async ({
+ page,
+ }) => {
const { requestLog } = await installComplaintMocks(page);
await page.goto("/superuser/complaints");
diff --git a/tests/e2e/superuser-system-status.smoke.spec.js b/tests/e2e/superuser-system-status.smoke.spec.js
index 5ae461da..aa58ea1d 100644
--- a/tests/e2e/superuser-system-status.smoke.spec.js
+++ b/tests/e2e/superuser-system-status.smoke.spec.js
@@ -186,7 +186,8 @@ const longReasonSnapshot = {
configured: true,
probe_supported: true,
status: "configured",
- status_reason: "Shelly cloud credentials are configured, but no known device id is available for a safe read-only probe.",
+ status_reason:
+ "Shelly cloud credentials are configured, but no known device id is available for a safe read-only probe.",
status_reason_key: "shelly_no_device_id",
status_reason_params: {},
checked_at: "2026-04-08T08:45:00.000Z",
@@ -231,7 +232,9 @@ test.describe("Superuser system status smoke", () => {
await primeSuperuserSession(page);
});
- test("@smoke superuser status dashboard renders Danish translations for degraded infrastructure and recent sessions", async ({ page }) => {
+ test("@smoke superuser status dashboard renders Danish translations for degraded infrastructure and recent sessions", async ({
+ page,
+ }) => {
await installSystemStatusMock(page, degradedSnapshot);
await page.goto("/superuser");
@@ -286,10 +289,7 @@ test.describe("Superuser system status smoke", () => {
expect(pillBox).not.toBeNull();
expect(reasonBox).not.toBeNull();
- const headerBottom = Math.max(
- titleBox.y + titleBox.height,
- pillBox.y + pillBox.height
- );
+ const headerBottom = Math.max(titleBox.y + titleBox.height, pillBox.y + pillBox.height);
expect(reasonBox.y).toBeGreaterThanOrEqual(headerBottom - 1);
});
diff --git a/tests/e2e/support/mobilePos.js b/tests/e2e/support/mobilePos.js
index ae8a2bca..0eee44c8 100644
--- a/tests/e2e/support/mobilePos.js
+++ b/tests/e2e/support/mobilePos.js
@@ -1,6 +1,7 @@
import { mockApi, seedAuthenticatedState } from "./network.js";
-export const API_HOST = /https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
+export const API_HOST =
+ /https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
export const DEFAULT_DEPARTMENT_ID = 1;
export const DEFAULT_ORDER_ID = 9201;
export const DEFAULT_LAST_ORDER_ID = 9100;
@@ -216,13 +217,8 @@ function buildDefaultProducts() {
];
const byId = Object.fromEntries(products.map((product) => [product.id, product]));
- byId[53].addons = [
- createAddon(byId[71], { min: 0, max: 3 }),
- createAddon(byId[41], { min: 0, max: 1 }),
- ];
- byId[63].addons = [
- createAddon(byId[71], { min: 0, max: 2 }),
- ];
+ byId[53].addons = [createAddon(byId[71], { min: 0, max: 3 }), createAddon(byId[41], { min: 0, max: 1 })];
+ byId[63].addons = [createAddon(byId[71], { min: 0, max: 2 })];
return clone(products);
}
@@ -333,15 +329,23 @@ function buildDefaultFixture() {
reg_1: "AB12345",
created_at: "2026-01-01T10:00:00.000Z",
});
- const lastOrderPrimary = buildOrderItem(productById[53], {
- order_id: DEFAULT_LAST_ORDER_ID,
- quantity: 1,
- }, 9001);
- const lastOrderAddon = buildOrderItem(productById[71], {
- order_id: DEFAULT_LAST_ORDER_ID,
- quantity: 1,
- related_item_id: 9001,
- }, 9002);
+ const lastOrderPrimary = buildOrderItem(
+ productById[53],
+ {
+ order_id: DEFAULT_LAST_ORDER_ID,
+ quantity: 1,
+ },
+ 9001
+ );
+ const lastOrderAddon = buildOrderItem(
+ productById[71],
+ {
+ order_id: DEFAULT_LAST_ORDER_ID,
+ quantity: 1,
+ related_item_id: 9001,
+ },
+ 9002
+ );
const bookingPrimary = createBookingRecord(DEFAULT_BOOKING_ID, {
reg_1: "BOOK123",
reference: "BOOKING-REF-8101",
@@ -606,7 +610,9 @@ export function createMobilePosFixture(overrides = {}) {
...clone(overrides),
departments: overrides.departments ? clone(overrides.departments) : clone(base.departments),
products: overrides.products ? clone(overrides.products) : clone(base.products),
- departmentCategories: overrides.departmentCategories ? clone(overrides.departmentCategories) : clone(base.departmentCategories),
+ departmentCategories: overrides.departmentCategories
+ ? clone(overrides.departmentCategories)
+ : clone(base.departmentCategories),
employees: overrides.employees ? clone(overrides.employees) : clone(base.employees),
vehicles: overrides.vehicles ? clone(overrides.vehicles) : clone(base.vehicles),
unknownVehicles: overrides.unknownVehicles ? clone(overrides.unknownVehicles) : clone(base.unknownVehicles),
@@ -649,17 +655,21 @@ export function buildMobilePosState(options = {}) {
const fixture = options.fixture || createMobilePosFixture();
const customerId = Object.prototype.hasOwnProperty.call(options, "customerId")
? options.customerId
- : (fixture.regularCustomerId ?? REGULAR_CUSTOMER_ID);
+ : fixture.regularCustomerId ?? REGULAR_CUSTOMER_ID;
const primaryItemId = options.primaryItemId ?? 53;
const includePrimaryItem = options.includePrimaryItem ?? true;
const vehicleType = Object.prototype.hasOwnProperty.call(options, "vehicleType")
? options.vehicleType
- : (includePrimaryItem ? primaryItemId : 53);
+ : includePrimaryItem
+ ? primaryItemId
+ : 53;
const bookingId = options.bookingId ?? null;
const reference = options.reference ?? (bookingId ? `BOOKING-${bookingId}` : "REF-9201");
const primaryItem = Object.prototype.hasOwnProperty.call(options, "primaryItem")
? clone(options.primaryItem)
- : (includePrimaryItem ? createStateProductSnapshot(fixture, primaryItemId) : null);
+ : includePrimaryItem
+ ? createStateProductSnapshot(fixture, primaryItemId)
+ : null;
return {
vehicles: {
@@ -674,7 +684,9 @@ export function buildMobilePosState(options = {}) {
booking_id: bookingId,
addons: [],
reference,
- last_order_id: Object.prototype.hasOwnProperty.call(options, "lastOrderId") ? options.lastOrderId : DEFAULT_LAST_ORDER_ID,
+ last_order_id: Object.prototype.hasOwnProperty.call(options, "lastOrderId")
+ ? options.lastOrderId
+ : DEFAULT_LAST_ORDER_ID,
},
vehicle_2: options.vehicle_2 ?? null,
vehicle_3: options.vehicle_3 ?? null,
@@ -688,7 +700,9 @@ export function buildMobilePosState(options = {}) {
},
transactionItems: {
primaryItem,
- additionalItems: Object.prototype.hasOwnProperty.call(options, "additionalItems") ? clone(options.additionalItems) : [],
+ additionalItems: Object.prototype.hasOwnProperty.call(options, "additionalItems")
+ ? clone(options.additionalItems)
+ : [],
},
categories: {
list: clone(options.categoriesList ?? []),
@@ -707,23 +721,25 @@ export function buildMobilePosState(options = {}) {
},
attachments: {
files: Object.prototype.hasOwnProperty.call(options, "attachmentFiles") ? clone(options.attachmentFiles) : [],
- base64: Object.prototype.hasOwnProperty.call(options, "attachmentsBase64") ? clone(options.attachmentsBase64) : [],
+ base64: Object.prototype.hasOwnProperty.call(options, "attachmentsBase64")
+ ? clone(options.attachmentsBase64)
+ : [],
wash_certificate: options.washCertificate ?? false,
},
transactionHistory: clone(options.transactionHistory ?? []),
- lastVehicleOrders: clone(options.lastVehicleOrders ?? {
- vehicle_1: null,
- vehicle_2: null,
- vehicle_3: null,
- }),
+ lastVehicleOrders: clone(
+ options.lastVehicleOrders ?? {
+ vehicle_1: null,
+ vehicle_2: null,
+ vehicle_3: null,
+ }
+ ),
timestamp: options.timestamp ?? Date.now(),
};
}
export function seedMobilePosState(page, optionsOrSnapshot = {}) {
- const snapshot = optionsOrSnapshot?.vehicles
- ? clone(optionsOrSnapshot)
- : buildMobilePosState(optionsOrSnapshot);
+ const snapshot = optionsOrSnapshot?.vehicles ? clone(optionsOrSnapshot) : buildMobilePosState(optionsOrSnapshot);
return page.addInitScript((payload) => {
window.localStorage.setItem("pos", JSON.stringify(payload));
}, snapshot);
@@ -759,7 +775,9 @@ export function suppressVueDevtoolsOverlay(page) {
if (root && !root.querySelector("style[data-test-hide-vue-devtools='1']")) {
const style = document.createElement("style");
style.setAttribute("data-test-hide-vue-devtools", "1");
- style.textContent = `${selectors.join(", ")} { display: none !important; visibility: hidden !important; pointer-events: none !important; opacity: 0 !important; }`;
+ style.textContent = `${selectors.join(
+ ", "
+ )} { display: none !important; visibility: hidden !important; pointer-events: none !important; opacity: 0 !important; }`;
root.appendChild(style);
}
selectors.forEach((selector) => {
@@ -782,35 +800,32 @@ export function waitForMobileNextStepCooldown(page, timeout = MOBILE_NEXT_STEP_C
return page.waitForTimeout(timeout);
}
-export async function primeOperatorSession(page, {
- token,
- permissions = MOBILE_PERMISSIONS,
- sessionData = {},
-} = {}) {
+export async function primeOperatorSession(page, { token, permissions = MOBILE_PERMISSIONS, sessionData = {} } = {}) {
await seedAuthenticatedState(page, token);
await page.goto("/login");
- await page.evaluate(async ({ sessionToken, sessionPermissions, data }) => {
- const sessionModule = await import("/src/components/session/token/SessionUser.vue");
- window.localStorage.setItem("token", sessionToken);
- sessionModule.SessionUser.token.value = sessionToken;
- sessionModule.SessionUser.authenticated.value = true;
- sessionModule.SessionUser.permissions.value = sessionPermissions;
- sessionModule.SessionUser.initiated.value = true;
- sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345;
- sessionModule.SessionUser.user.display_name.value = data.display_name ?? "POS Mobile E2E";
- }, {
- sessionToken: token,
- sessionPermissions: permissions,
- data: sessionData,
- });
+ await page.evaluate(
+ async ({ sessionToken, sessionPermissions, data }) => {
+ const sessionModule = await import("/src/components/session/token/SessionUser.vue");
+ window.localStorage.setItem("token", sessionToken);
+ sessionModule.SessionUser.token.value = sessionToken;
+ sessionModule.SessionUser.authenticated.value = true;
+ sessionModule.SessionUser.permissions.value = sessionPermissions;
+ sessionModule.SessionUser.initiated.value = true;
+ sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345;
+ sessionModule.SessionUser.user.display_name.value = data.display_name ?? "POS Mobile E2E";
+ },
+ {
+ sessionToken: token,
+ sessionPermissions: permissions,
+ data: sessionData,
+ }
+ );
}
-export function createPaymentIntent(orderId, {
- status = "requires_capture",
- amount = 123400,
- readerId = "reader_online_1",
- taxPercentage = 25,
-} = {}) {
+export function createPaymentIntent(
+ orderId,
+ { status = "requires_capture", amount = 123400, readerId = "reader_online_1", taxPercentage = 25 } = {}
+) {
const isSucceeded = status === "succeeded";
return {
id: `pi_${orderId}`,
@@ -929,12 +944,14 @@ export async function mockMobilePosApi(page, fixture) {
if (pathname.endsWith("/modules/stripe/department/terminal/readers") && method === "GET") {
recordCounter(fixture, "readersGet");
- await route.fulfill(json({
- success: true,
- data: {
- data: clone(fixture.readers),
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ data: clone(fixture.readers),
+ },
+ })
+ );
return;
}
@@ -942,19 +959,27 @@ export async function mockMobilePosApi(page, fixture) {
recordCounter(fixture, "paymentIntentGet");
if (fixture.failureBudget.paymentIntentGet > 0) {
fixture.failureBudget.paymentIntentGet -= 1;
- await route.fulfill(json({
- success: false,
- data: {
- message: "Unable to load payment intent state",
- },
- }, 500));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: {
+ message: "Unable to load payment intent state",
+ },
+ },
+ 500
+ )
+ );
return;
}
const orderId = extractOrderId(request, parsedUrl);
- await route.fulfill(json(paymentIntentResponse(
- orderId ? fixture.paymentIntentsByOrderId[orderId] || null : null,
- { message: "No active payment intent for this order." },
- )));
+ await route.fulfill(
+ json(
+ paymentIntentResponse(orderId ? fixture.paymentIntentsByOrderId[orderId] || null : null, {
+ message: "No active payment intent for this order.",
+ })
+ )
+ );
return;
}
@@ -962,22 +987,29 @@ export async function mockMobilePosApi(page, fixture) {
recordCounter(fixture, "paymentIntentCreate");
if (fixture.failureBudget.paymentIntentCreate > 0) {
fixture.failureBudget.paymentIntentCreate -= 1;
- await route.fulfill(json({
- success: false,
- data: {
- message: "Unable to create payment intent",
- },
- }, 500));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: {
+ message: "Unable to create payment intent",
+ },
+ },
+ 500
+ )
+ );
return;
}
const orderId = toPositiveInteger(body.id);
const readerId = body.reader || "reader_online_1";
const existingIntent = fixture.paymentIntentsByOrderId[orderId] || null;
- const nextIntent = existingIntent || createPaymentIntent(orderId, {
- status: "requires_capture",
- readerId,
- taxPercentage: Number(body.tax_percentage ?? 25),
- });
+ const nextIntent =
+ existingIntent ||
+ createPaymentIntent(orderId, {
+ status: "requires_capture",
+ readerId,
+ taxPercentage: Number(body.tax_percentage ?? 25),
+ });
nextIntent.metadata = {
...(nextIntent.metadata || {}),
order_id: String(orderId),
@@ -986,9 +1018,13 @@ export async function mockMobilePosApi(page, fixture) {
tax_percentage: String(body.tax_percentage ?? nextIntent.metadata?.tax_percentage ?? 25),
};
fixture.paymentIntentsByOrderId[orderId] = nextIntent;
- await route.fulfill(json(paymentIntentResponse(nextIntent, {
- reused: !!existingIntent,
- })));
+ await route.fulfill(
+ json(
+ paymentIntentResponse(nextIntent, {
+ reused: !!existingIntent,
+ })
+ )
+ );
return;
}
@@ -1004,12 +1040,17 @@ export async function mockMobilePosApi(page, fixture) {
recordCounter(fixture, "paymentIntentCapture");
if (fixture.failureBudget.paymentIntentCapture > 0) {
fixture.failureBudget.paymentIntentCapture -= 1;
- await route.fulfill(json({
- success: false,
- data: {
- message: "Unable to capture payment intent",
- },
- }, 500));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: {
+ message: "Unable to capture payment intent",
+ },
+ },
+ 500
+ )
+ );
return;
}
const orderId = toPositiveInteger(body.id);
@@ -1043,15 +1084,17 @@ export async function mockMobilePosApi(page, fixture) {
if (pathname.endsWith("/departments/order/recommended") && method === "GET") {
recordCounter(fixture, "recommendedGet");
- await route.fulfill(json({
- success: true,
- data: {
- reg_1: {
- motorapi: [53],
- order_history: { 2: [], 3: [], 4: [], 5: [] },
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ reg_1: {
+ motorapi: [53],
+ order_history: { 2: [], 3: [], 4: [], 5: [] },
+ },
},
- },
- }));
+ })
+ );
return;
}
@@ -1059,10 +1102,12 @@ export async function mockMobilePosApi(page, fixture) {
recordCounter(fixture, "bookingsGet");
const bookingId = toPositiveInteger(parsedUrl.searchParams.get("id"));
if (bookingId) {
- await route.fulfill(json({
- success: true,
- data: clone(fixture.bookingsById[bookingId] || null),
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(fixture.bookingsById[bookingId] || null),
+ })
+ );
return;
}
const bookings = filterBookings(Object.values(fixture.bookingsById), parsedUrl.searchParams.get("filters"));
@@ -1075,12 +1120,17 @@ export async function mockMobilePosApi(page, fixture) {
recordLog(fixture, "bookingCompletions", body);
if (fixture.failureBudget.bookingComplete > 0) {
fixture.failureBudget.bookingComplete -= 1;
- await route.fulfill(json({
- success: false,
- data: {
- message: "Unable to complete booking",
- },
- }, 500));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: {
+ message: "Unable to complete booking",
+ },
+ },
+ 500
+ )
+ );
return;
}
const bookingId = toPositiveInteger(body.id);
@@ -1091,10 +1141,12 @@ export async function mockMobilePosApi(page, fixture) {
fixture.bookingStatusById[bookingId] = "completed";
removeBookingFromPending(fixture, bookingId);
}
- await route.fulfill(json({
- success: true,
- data: clone(booking || { id: bookingId, status: "completed" }),
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(booking || { id: bookingId, status: "completed" }),
+ })
+ );
return;
}
@@ -1102,15 +1154,17 @@ export async function mockMobilePosApi(page, fixture) {
recordCounter(fixture, "orderBookingsGet");
const bookingId = toPositiveInteger(parsedUrl.searchParams.get("id"));
if (bookingId) {
- await route.fulfill(json({
- success: true,
- data: clone(fixture.bookingsById[bookingId] || null),
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(fixture.bookingsById[bookingId] || null),
+ })
+ );
return;
}
const bookings = filterBookings(
fixture.pendingBookings ?? Object.values(fixture.bookingsById),
- parsedUrl.searchParams.get("filters"),
+ parsedUrl.searchParams.get("filters")
);
await route.fulfill(json({ success: true, data: clone(bookings) }));
return;
@@ -1132,7 +1186,13 @@ export async function mockMobilePosApi(page, fixture) {
if (pathname.endsWith("/vehicles/search") && method === "GET") {
recordCounter(fixture, "vehiclesSearchGet");
const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
- const vehicles = fixture.vehicles.filter((vehicle) => !search || String(vehicle.reg || "").toUpperCase().includes(search));
+ const vehicles = fixture.vehicles.filter(
+ (vehicle) =>
+ !search ||
+ String(vehicle.reg || "")
+ .toUpperCase()
+ .includes(search)
+ );
await route.fulfill(json({ success: true, data: clone(vehicles) }));
return;
}
@@ -1140,7 +1200,13 @@ export async function mockMobilePosApi(page, fixture) {
if (pathname.endsWith("/vehicles") && method === "GET") {
recordCounter(fixture, "vehiclesGet");
const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
- const vehicles = fixture.vehicles.filter((vehicle) => !search || String(vehicle.reg || "").toUpperCase().includes(search));
+ const vehicles = fixture.vehicles.filter(
+ (vehicle) =>
+ !search ||
+ String(vehicle.reg || "")
+ .toUpperCase()
+ .includes(search)
+ );
await route.fulfill(json({ success: true, data: clone(vehicles) }));
return;
}
@@ -1148,31 +1214,45 @@ export async function mockMobilePosApi(page, fixture) {
if (pathname.endsWith("/department/vehicles/unknown-customer") && method === "GET") {
recordCounter(fixture, "unknownVehiclesGet");
const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
- const vehicles = fixture.unknownVehicles.filter((vehicle) => !search || String(vehicle.reg_1 || "").toUpperCase().includes(search));
+ const vehicles = fixture.unknownVehicles.filter(
+ (vehicle) =>
+ !search ||
+ String(vehicle.reg_1 || "")
+ .toUpperCase()
+ .includes(search)
+ );
await route.fulfill(json({ success: true, data: clone(vehicles) }));
return;
}
if (pathname.endsWith("/users/customer") && method === "GET") {
recordCounter(fixture, "usersCustomerGet");
- const customerNumber = toPositiveInteger(parsedUrl.searchParams.get("customer_number")) ?? fixture.regularCustomerId;
+ const customerNumber =
+ toPositiveInteger(parsedUrl.searchParams.get("customer_number")) ?? fixture.regularCustomerId;
const customer = fixture.customersByNumber[customerNumber] || null;
if (!customer) {
- await route.fulfill(json({
- success: false,
- data: {
- message: "Customer not found",
- },
- }, 404));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: {
+ message: "Customer not found",
+ },
+ },
+ 404
+ )
+ );
return;
}
- await route.fulfill(json({
- success: true,
- data: {
- customer_name: customer.name,
- economic_customer: clone(customer),
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ customer_name: customer.name,
+ economic_customer: clone(customer),
+ },
+ })
+ );
return;
}
@@ -1183,51 +1263,66 @@ export async function mockMobilePosApi(page, fixture) {
if (!search) {
return true;
}
- return String(customer.customerNumber).includes(search) || String(customer.name || "").toLowerCase().includes(search);
+ return (
+ String(customer.customerNumber).includes(search) ||
+ String(customer.name || "")
+ .toLowerCase()
+ .includes(search)
+ );
});
- await route.fulfill(json({
- success: true,
- data: clone(customers),
- meta: {
- pagination: {
- page: 1,
- limit: 10,
- total: customers.length,
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(customers),
+ meta: {
+ pagination: {
+ page: 1,
+ limit: 10,
+ total: customers.length,
+ },
},
- },
- }));
+ })
+ );
return;
}
if (pathname.endsWith("/customer/notes") && method === "GET") {
recordCounter(fixture, "customerNotesGet");
- const customerNumber = toPositiveInteger(parsedUrl.searchParams.get("customer_number"))
- ?? toPositiveInteger(parsedUrl.searchParams.get("id"))
- ?? fixture.regularCustomerId;
- await route.fulfill(json({
- success: true,
- data: clone(fixture.customerNotesByNumber[customerNumber] || []),
- }));
+ const customerNumber =
+ toPositiveInteger(parsedUrl.searchParams.get("customer_number")) ??
+ toPositiveInteger(parsedUrl.searchParams.get("id")) ??
+ fixture.regularCustomerId;
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(fixture.customerNotesByNumber[customerNumber] || []),
+ })
+ );
return;
}
if (pathname.endsWith("/public/employees") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: clone(fixture.employees || []),
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(fixture.employees || []),
+ })
+ );
return;
}
if (pathname.endsWith("/customer/attributes") && method === "GET") {
recordCounter(fixture, "customerAttributesGet");
- const customerNumber = toPositiveInteger(parsedUrl.searchParams.get("customer_number"))
- ?? toPositiveInteger(parsedUrl.searchParams.get("id"))
- ?? fixture.regularCustomerId;
- await route.fulfill(json({
- success: true,
- data: clone(fixture.customerAttributesByNumber[customerNumber] || []),
- }));
+ const customerNumber =
+ toPositiveInteger(parsedUrl.searchParams.get("customer_number")) ??
+ toPositiveInteger(parsedUrl.searchParams.get("id")) ??
+ fixture.regularCustomerId;
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(fixture.customerAttributesByNumber[customerNumber] || []),
+ })
+ );
return;
}
@@ -1245,10 +1340,12 @@ export async function mockMobilePosApi(page, fixture) {
const limit = Number(parsedUrl.searchParams.get("limit") || 0);
if (productId) {
- await route.fulfill(json({
- success: true,
- data: clone(getProductById(fixture, productId)),
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(getProductById(fixture, productId)),
+ })
+ );
return;
}
@@ -1269,10 +1366,12 @@ export async function mockMobilePosApi(page, fixture) {
if (pathname.endsWith("/orders") && method === "GET") {
recordCounter(fixture, "ordersGet");
- await route.fulfill(json({
- success: true,
- data: clone(Object.values(fixture.ordersById)),
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(Object.values(fixture.ordersById)),
+ })
+ );
return;
}
@@ -1281,12 +1380,17 @@ export async function mockMobilePosApi(page, fixture) {
recordLog(fixture, "orderCreates", body);
if (fixture.failureBudget.orderCreate > 0) {
fixture.failureBudget.orderCreate -= 1;
- await route.fulfill(json({
- success: false,
- data: {
- message: "Unable to create order",
- },
- }, 500));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: {
+ message: "Unable to create order",
+ },
+ },
+ 500
+ )
+ );
return;
}
const orderId = fixture.nextOrderId++;
@@ -1302,12 +1406,14 @@ export async function mockMobilePosApi(page, fixture) {
});
fixture.orderItemsByOrderId[orderId] = [];
fixture.attachmentsByOrderId[orderId] = [];
- await route.fulfill(json({
- success: true,
- data: {
- id: orderId,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ id: orderId,
+ },
+ })
+ );
return;
}
@@ -1333,12 +1439,17 @@ export async function mockMobilePosApi(page, fixture) {
recordLog(fixture, "orderDeletes", { id: orderId });
if (fixture.failureBudget.orderDelete > 0) {
fixture.failureBudget.orderDelete -= 1;
- await route.fulfill(json({
- success: false,
- data: {
- message: "Unable to delete order",
- },
- }, 500));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: {
+ message: "Unable to delete order",
+ },
+ },
+ 500
+ )
+ );
return;
}
if (orderId) {
@@ -1355,20 +1466,24 @@ export async function mockMobilePosApi(page, fixture) {
if (pathname.endsWith("/order") && method === "GET") {
recordCounter(fixture, "orderGet");
const orderId = toPositiveInteger(parsedUrl.searchParams.get("id"));
- await route.fulfill(json({
- success: true,
- data: clone(fixture.ordersById[orderId] || null),
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(fixture.ordersById[orderId] || null),
+ })
+ );
return;
}
if (pathname.endsWith("/order/items") && method === "GET") {
recordCounter(fixture, "orderItemsGet");
const orderId = toPositiveInteger(parsedUrl.searchParams.get("order_id"));
- await route.fulfill(json({
- success: true,
- data: clone(fixture.orderItemsByOrderId[orderId] || []),
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(fixture.orderItemsByOrderId[orderId] || []),
+ })
+ );
return;
}
@@ -1380,19 +1495,28 @@ export async function mockMobilePosApi(page, fixture) {
const order = fixture.ordersById[orderId];
const product = getProductById(fixture, productId);
if (!order || !product) {
- await route.fulfill(json({
- success: false,
- data: {
- message: "Order or product not found",
- },
- }, 422));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: {
+ message: "Order or product not found",
+ },
+ },
+ 422
+ )
+ );
return;
}
const orderItemId = fixture.nextOrderItemId++;
- const item = buildOrderItem(product, {
- ...body,
- order_id: orderId,
- }, orderItemId);
+ const item = buildOrderItem(
+ product,
+ {
+ ...body,
+ order_id: orderId,
+ },
+ orderItemId
+ );
if (!Array.isArray(fixture.orderItemsByOrderId[orderId])) {
fixture.orderItemsByOrderId[orderId] = [];
}
@@ -1419,12 +1543,14 @@ export async function mockMobilePosApi(page, fixture) {
};
});
});
- await route.fulfill(json({
- success: true,
- data: {
- id: orderItemId,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ id: orderItemId,
+ },
+ })
+ );
return;
}
@@ -1433,7 +1559,9 @@ export async function mockMobilePosApi(page, fixture) {
const orderItemId = toPositiveInteger(parsedUrl.searchParams.get("id")) ?? toPositiveInteger(body.id);
recordLog(fixture, "orderItemDeletes", { id: orderItemId });
Object.keys(fixture.orderItemsByOrderId).forEach((orderIdKey) => {
- fixture.orderItemsByOrderId[orderIdKey] = (fixture.orderItemsByOrderId[orderIdKey] || []).filter((item) => Number(item.id) !== orderItemId);
+ fixture.orderItemsByOrderId[orderIdKey] = (fixture.orderItemsByOrderId[orderIdKey] || []).filter(
+ (item) => Number(item.id) !== orderItemId
+ );
});
await route.fulfill(json({ success: true, data: true }));
return;
@@ -1448,31 +1576,40 @@ export async function mockMobilePosApi(page, fixture) {
fixture.ordersById[orderId].completed_at = new Date().toISOString();
}
}
- await route.fulfill(json({
- success: true,
- data: {
- id: orderId,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ id: orderId,
+ },
+ })
+ );
return;
}
if (pathname.endsWith("/orders/attachments") && method === "GET") {
recordCounter(fixture, "attachmentsGet");
- const orderId = toPositiveInteger(parsedUrl.searchParams.get("id")) ?? toPositiveInteger(parsedUrl.searchParams.get("order_id"));
- await route.fulfill(json({
- success: true,
- data: clone(fixture.attachmentsByOrderId[orderId] || []),
- }));
+ const orderId =
+ toPositiveInteger(parsedUrl.searchParams.get("id")) ??
+ toPositiveInteger(parsedUrl.searchParams.get("order_id"));
+ await route.fulfill(
+ json({
+ success: true,
+ data: clone(fixture.attachmentsByOrderId[orderId] || []),
+ })
+ );
return;
}
if (pathname.endsWith("/orders/attachments") && method === "DELETE") {
recordCounter(fixture, "attachmentsDelete");
const orderId = toPositiveInteger(body.order_id) ?? toPositiveInteger(parsedUrl.searchParams.get("order_id"));
- const attachmentId = toPositiveInteger(body.attachment_id) ?? toPositiveInteger(parsedUrl.searchParams.get("attachment_id"));
+ const attachmentId =
+ toPositiveInteger(body.attachment_id) ?? toPositiveInteger(parsedUrl.searchParams.get("attachment_id"));
recordLog(fixture, "attachmentDeletes", { order_id: orderId, attachment_id: attachmentId });
- fixture.attachmentsByOrderId[orderId] = (fixture.attachmentsByOrderId[orderId] || []).filter((attachment) => Number(attachment.id) !== attachmentId);
+ fixture.attachmentsByOrderId[orderId] = (fixture.attachmentsByOrderId[orderId] || []).filter(
+ (attachment) => Number(attachment.id) !== attachmentId
+ );
await route.fulfill(json({ success: true, data: true }));
return;
}
@@ -1482,12 +1619,17 @@ export async function mockMobilePosApi(page, fixture) {
recordLog(fixture, "attachmentUploads", body);
if (fixture.failureBudget.attachmentUpload > 0) {
fixture.failureBudget.attachmentUpload -= 1;
- await route.fulfill(json({
- success: false,
- data: {
- message: "Unable to upload attachment",
- },
- }, 500));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: {
+ message: "Unable to upload attachment",
+ },
+ },
+ 500
+ )
+ );
return;
}
const orderId = toPositiveInteger(body.order_id);
@@ -1509,12 +1651,14 @@ export async function mockMobilePosApi(page, fixture) {
recordCounter(fixture, "attachmentDownload");
const orderId = toPositiveInteger(parsedUrl.searchParams.get("order_id"));
const attachmentId = toPositiveInteger(parsedUrl.searchParams.get("attachment_id"));
- await route.fulfill(json({
- success: true,
- data: {
- download_link: `${ATTACHMENT_DOWNLOAD_URL}/${orderId}/${attachmentId}`,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ download_link: `${ATTACHMENT_DOWNLOAD_URL}/${orderId}/${attachmentId}`,
+ },
+ })
+ );
return;
}
@@ -1525,12 +1669,14 @@ export async function mockMobilePosApi(page, fixture) {
if (pathname.endsWith("/modules/scanner/lpr") && method === "POST") {
recordCounter(fixture, "lprPost");
- await route.fulfill(json({
- success: false,
- data: {
- message: "License plate extraction failed",
- },
- }));
+ await route.fulfill(
+ json({
+ success: false,
+ data: {
+ message: "License plate extraction failed",
+ },
+ })
+ );
return;
}
@@ -1538,12 +1684,10 @@ export async function mockMobilePosApi(page, fixture) {
});
}
-export async function gotoMobilePos(page, {
- step = 1,
- orderId = null,
- customerId = null,
- departmentId = DEFAULT_DEPARTMENT_ID,
-} = {}) {
+export async function gotoMobilePos(
+ page,
+ { step = 1, orderId = null, customerId = null, departmentId = DEFAULT_DEPARTMENT_ID } = {}
+) {
const params = new URLSearchParams();
if (orderId !== null && orderId !== undefined) {
params.set("id", String(orderId));
@@ -1558,14 +1702,18 @@ export async function gotoMobilePos(page, {
await page.goto(`/admin/${departmentId}/modules/pos${query ? `?${query}` : ""}`);
}
-export async function setupMobilePosPage(page, fixture, {
- token = "pos-mobile-token",
- permissions = MOBILE_PERMISSIONS,
- seedState = {},
- route = {},
- sessionData = {},
- storedOrderId = null,
-} = {}) {
+export async function setupMobilePosPage(
+ page,
+ fixture,
+ {
+ token = "pos-mobile-token",
+ permissions = MOBILE_PERMISSIONS,
+ seedState = {},
+ route = {},
+ sessionData = {},
+ storedOrderId = null,
+ } = {}
+) {
await mockApi(page, {
authenticated: true,
permissions,
@@ -1595,21 +1743,23 @@ export async function setupMobilePosPage(page, fixture, {
departmentId: fixture.departmentId ?? DEFAULT_DEPARTMENT_ID,
...route,
});
- await page.evaluate(() => {
- const selectors = [
- "#__vue-devtools-container__",
- ".vue-devtools__anchor-btn",
- ".vue-devtools__panel",
- ".vue-devtools__panel-content",
- ];
- selectors.forEach((selector) => {
- document.querySelectorAll(selector).forEach((element) => {
- element.setAttribute("aria-hidden", "true");
- element.style.setProperty("display", "none", "important");
- element.style.setProperty("visibility", "hidden", "important");
- element.style.setProperty("pointer-events", "none", "important");
- element.style.setProperty("opacity", "0", "important");
+ await page
+ .evaluate(() => {
+ const selectors = [
+ "#__vue-devtools-container__",
+ ".vue-devtools__anchor-btn",
+ ".vue-devtools__panel",
+ ".vue-devtools__panel-content",
+ ];
+ selectors.forEach((selector) => {
+ document.querySelectorAll(selector).forEach((element) => {
+ element.setAttribute("aria-hidden", "true");
+ element.style.setProperty("display", "none", "important");
+ element.style.setProperty("visibility", "hidden", "important");
+ element.style.setProperty("pointer-events", "none", "important");
+ element.style.setProperty("opacity", "0", "important");
+ });
});
- });
- }).catch(() => {});
+ })
+ .catch(() => {});
}
diff --git a/tests/e2e/support/network.js b/tests/e2e/support/network.js
index a02abb47..842339ae 100644
--- a/tests/e2e/support/network.js
+++ b/tests/e2e/support/network.js
@@ -1,4 +1,5 @@
-const API_HOST = /https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
+const API_HOST =
+ /https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
const TINY_PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAukB9pY9ZxQAAAAASUVORK5CYII=",
"base64"
@@ -8,7 +9,7 @@ function json(body, status = 200) {
return {
status,
contentType: "application/json",
- body: JSON.stringify(body)
+ body: JSON.stringify(body),
};
}
@@ -98,9 +99,7 @@ function withEffectiveOrderState(posFixture, order) {
...order,
include_in_invoice: includeInInvoice,
include_in_invoice_effective:
- includeInInvoice === null
- ? resolveDepartmentIncludeInInvoice(posFixture, order.department_id)
- : includeInInvoice,
+ includeInInvoice === null ? resolveDepartmentIncludeInInvoice(posFixture, order.department_id) : includeInInvoice,
};
}
@@ -158,7 +157,9 @@ function filterPosOrders(posFixture, filters) {
function filterNumberPlateScans(posFixture, { filters = "", search = "", order = "created_at:desc" } = {}) {
const filterMap = parseFilterExpressions(filters);
- const searchValue = String(search || "").trim().toUpperCase();
+ const searchValue = String(search || "")
+ .trim()
+ .toUpperCase();
const [orderBy = "created_at", orderDirection = "desc"] = String(order || "created_at:desc").split(":");
const filteredRows = (posFixture.numberPlateScans || []).filter((scan) => {
@@ -166,7 +167,12 @@ function filterNumberPlateScans(posFixture, { filters = "", search = "", order =
return false;
}
- if (searchValue && !String(scan.plate || "").toUpperCase().includes(searchValue)) {
+ if (
+ searchValue &&
+ !String(scan.plate || "")
+ .toUpperCase()
+ .includes(searchValue)
+ ) {
return false;
}
@@ -304,9 +310,36 @@ function createSelfServeFixture(overrides = {}) {
];
const products = [
- { id: 2, name: "Truck", price: 100, description: "Large truck", piktogram: "truck", is_wash: true, subscription_allowed: true, category: 4 },
- { id: 3, name: "Van", price: 80, description: "Medium van", piktogram: "van", is_wash: true, subscription_allowed: true, category: 4 },
- { id: 4, name: "Car", price: 60, description: "Small car", piktogram: "car", is_wash: true, subscription_allowed: true, category: 4 },
+ {
+ id: 2,
+ name: "Truck",
+ price: 100,
+ description: "Large truck",
+ piktogram: "truck",
+ is_wash: true,
+ subscription_allowed: true,
+ category: 4,
+ },
+ {
+ id: 3,
+ name: "Van",
+ price: 80,
+ description: "Medium van",
+ piktogram: "van",
+ is_wash: true,
+ subscription_allowed: true,
+ category: 4,
+ },
+ {
+ id: 4,
+ name: "Car",
+ price: 60,
+ description: "Small car",
+ piktogram: "car",
+ is_wash: true,
+ subscription_allowed: true,
+ category: 4,
+ },
];
const baseFixture = {
@@ -324,12 +357,25 @@ function createSelfServeFixture(overrides = {}) {
lane: { id: 7, name: "7" },
session: { id: 501, status: "IN_PROGRESS", allowed: true },
questions: [
- { id: 11, question: "Is the tarp removed?", description: "Required before machine wash.", answer: null, order_priority: 1 },
+ {
+ id: 11,
+ question: "Is the tarp removed?",
+ description: "Required before machine wash.",
+ answer: null,
+ order_priority: 1,
+ },
],
conditions: [],
rules: [],
tasks: [
- { id: 9001, task: "Prepare the truck", description: "Complete the pre-wash checks.", order_priority: 1, services: ["MACHINE"], buttons: [1] },
+ {
+ id: 9001,
+ task: "Prepare the truck",
+ description: "Complete the pre-wash checks.",
+ order_priority: 1,
+ services: ["MACHINE"],
+ buttons: [1],
+ },
],
allowed_services: ["MACHINE"],
},
@@ -338,9 +384,7 @@ function createSelfServeFixture(overrides = {}) {
machine_available: true,
lane: { id: 7, name: "7" },
session: { id: 601, status: "IN_PROGRESS", allowed: true },
- questions: [
- { id: 21, question: "Ready for direct wash?", answer: null, order_priority: 1 },
- ],
+ questions: [{ id: 21, question: "Ready for direct wash?", answer: null, order_priority: 1 }],
conditions: [],
rules: [],
tasks: [],
@@ -352,29 +396,36 @@ function createSelfServeFixture(overrides = {}) {
session: { id: 501, status: "IN_PROGRESS", allowed: true },
lane: { id: 7, name: "7" },
questions: [
- { id: 11, question: "Is the tarp removed?", description: "Required before machine wash.", answer: null, order_priority: 1 },
+ {
+ id: 11,
+ question: "Is the tarp removed?",
+ description: "Required before machine wash.",
+ answer: null,
+ order_priority: 1,
+ },
],
conditions: [],
rules: [],
tasks: [
- { id: 9001, task: "Prepare the truck", description: "Complete the pre-wash checks.", order_priority: 1, services: ["MACHINE"], buttons: [1] },
- ],
- events: [
- { id: 1, type: "STARTED", created_at: "2026-01-01T10:00:00.000Z" },
+ {
+ id: 9001,
+ task: "Prepare the truck",
+ description: "Complete the pre-wash checks.",
+ order_priority: 1,
+ services: ["MACHINE"],
+ buttons: [1],
+ },
],
+ events: [{ id: 1, type: "STARTED", created_at: "2026-01-01T10:00:00.000Z" }],
},
601: {
session: { id: 601, status: "IN_PROGRESS", allowed: true },
lane: { id: 7, name: "7" },
- questions: [
- { id: 21, question: "Ready for direct wash?", answer: null, order_priority: 1 },
- ],
+ questions: [{ id: 21, question: "Ready for direct wash?", answer: null, order_priority: 1 }],
conditions: [],
rules: [],
tasks: [],
- events: [
- { id: 2, type: "STARTED", created_at: "2026-01-01T11:00:00.000Z" },
- ],
+ events: [{ id: 2, type: "STARTED", created_at: "2026-01-01T11:00:00.000Z" }],
},
},
summaryByKey: {},
@@ -383,29 +434,36 @@ function createSelfServeFixture(overrides = {}) {
session: { id: 501, status: "IN_PROGRESS", allowed: true },
lane: { id: 7, name: "7" },
questions: [
- { id: 11, question: "Is the tarp removed?", description: "Required before machine wash.", answer: true, order_priority: 1 },
+ {
+ id: 11,
+ question: "Is the tarp removed?",
+ description: "Required before machine wash.",
+ answer: true,
+ order_priority: 1,
+ },
],
conditions: [],
rules: [],
tasks: [
- { id: 9001, task: "Prepare the truck", description: "Complete the pre-wash checks.", order_priority: 1, services: ["MACHINE"], buttons: [1] },
- ],
- events: [
- { id: 3, type: "QUESTION_ANSWERED", created_at: "2026-01-01T10:01:00.000Z" },
+ {
+ id: 9001,
+ task: "Prepare the truck",
+ description: "Complete the pre-wash checks.",
+ order_priority: 1,
+ services: ["MACHINE"],
+ buttons: [1],
+ },
],
+ events: [{ id: 3, type: "QUESTION_ANSWERED", created_at: "2026-01-01T10:01:00.000Z" }],
},
"7:ZZ00000:21:true": {
session: { id: 601, status: "IN_PROGRESS", allowed: true },
lane: { id: 7, name: "7" },
- questions: [
- { id: 21, question: "Ready for direct wash?", answer: true, order_priority: 1 },
- ],
+ questions: [{ id: 21, question: "Ready for direct wash?", answer: true, order_priority: 1 }],
conditions: [],
rules: [],
tasks: [],
- events: [
- { id: 4, type: "QUESTION_ANSWERED", created_at: "2026-01-01T11:01:00.000Z" },
- ],
+ events: [{ id: 4, type: "QUESTION_ANSWERED", created_at: "2026-01-01T11:01:00.000Z" }],
},
},
attachmentsByTaskId: {
@@ -439,6 +497,7 @@ function createEdgeGatewayFixture(options = {}) {
return {
pendingDiscoveryByGatewayId: {},
pendingUpdateByGatewayId: {},
+ shellSessionsById: {},
gateways: [
{
id: 701,
@@ -476,7 +535,7 @@ function createEdgeGatewayFixture(options = {}) {
model: "Shelly Plus 2PM",
channel_count: 2,
online: true,
- capabilities: { generation: 2 }
+ capabilities: { generation: 2 },
},
{
id: 2,
@@ -485,28 +544,31 @@ function createEdgeGatewayFixture(options = {}) {
model: "Shelly Mini 1",
channel_count: 1,
online: false,
- capabilities: { generation: 3 }
- }
+ capabilities: { generation: 3 },
+ },
],
bindings: [
- { id: 1, relay_id: "M-7", device_id: "shelly-plus-01", local_ip: "10.1.0.31", channel: 0, binding_source: "MANUAL" },
+ {
+ id: 1,
+ relay_id: "M-7",
+ device_id: "shelly-plus-01",
+ local_ip: "10.1.0.31",
+ channel: 0,
+ binding_source: "MANUAL",
+ },
{
id: 2,
relay_id: "M-7-LEGACY",
device_id: "shelly-missing-legacy",
local_ip: "10.1.0.99",
channel: 1,
- binding_source: "MANUAL"
- }
- ],
- recent_updates: [
- { id: 11, target_version: "1.2.1", status: "COMPLETED" }
+ binding_source: "MANUAL",
+ },
],
+ recent_updates: [{ id: 11, target_version: "1.2.1", status: "COMPLETED" }],
recent_shell_sessions: [],
recent_commands: [],
- audit_logs: [
- { id: 501, created_at: "2026-04-08 08:16:00", action: "GATEWAY_CLAIMED", actor_type: "USER" }
- ]
+ audit_logs: [{ id: 501, created_at: "2026-04-08 08:16:00", action: "GATEWAY_CLAIMED", actor_type: "USER" }],
},
{
id: 702,
@@ -541,18 +603,15 @@ function createEdgeGatewayFixture(options = {}) {
recent_updates: [],
recent_shell_sessions: [],
recent_commands: [],
- audit_logs: [
- { id: 502, created_at: "2026-04-07 21:05:00", action: "HEARTBEAT_TIMEOUT", actor_type: "SYSTEM" }
- ]
- }
- ]
+ audit_logs: [{ id: 502, created_at: "2026-04-07 21:05:00", action: "HEARTBEAT_TIMEOUT", actor_type: "SYSTEM" }],
+ },
+ ],
};
}
function buildEdgeGatewayShellSocketConfig(overrides = {}) {
return {
mode: "success",
- websocketUrl: "ws://127.0.0.1:65500/ws/browser-shell?token=shell-session-token",
prompt: "root@pi:~# ",
directoryListing: "agent.mjs",
...overrides,
@@ -574,9 +633,7 @@ function settleEdgeGatewayWork(edgeGatewayFixture, gatewayId) {
gateway.inventory = [...gateway.inventory, pendingDiscovery.device];
}
gateway.recent_commands = (gateway.recent_commands || []).map((job) =>
- Number(job.id) === Number(pendingDiscovery.jobId)
- ? { ...job, status: "COMPLETED" }
- : job
+ Number(job.id) === Number(pendingDiscovery.jobId) ? { ...job, status: "COMPLETED" } : job
);
delete edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId];
}
@@ -587,9 +644,7 @@ function settleEdgeGatewayWork(edgeGatewayFixture, gatewayId) {
pendingUpdate.fetchCount = (pendingUpdate.fetchCount || 0) + 1;
if (pendingUpdate.fetchCount >= 2) {
gateway.recent_updates = (gateway.recent_updates || []).map((job) =>
- Number(job.id) === Number(pendingUpdate.jobId)
- ? { ...job, status: "COMPLETED" }
- : job
+ Number(job.id) === Number(pendingUpdate.jobId) ? { ...job, status: "COMPLETED" } : job
);
delete edgeGatewayFixture.pendingUpdateByGatewayId[gatewayId];
}
@@ -598,132 +653,43 @@ function settleEdgeGatewayWork(edgeGatewayFixture, gatewayId) {
return gateway;
}
-async function installEdgeGatewayShellSocketMock(page, shellConfig) {
- if (!shellConfig) {
- return;
+function createShellSessionFixture(edgeGatewayFixture, gatewayId, session, shellConfig) {
+ const state = {
+ gatewayId,
+ session,
+ prompt: shellConfig.prompt,
+ directoryListing: shellConfig.directoryListing,
+ inputBuffer: "",
+ events: [],
+ nextEventId: 1,
+ };
+
+ const pushEvent = (event_type, payload = {}) => {
+ state.events.push({
+ id: state.nextEventId,
+ session_id: session.id,
+ event_type,
+ payload,
+ created_at: `2026-04-09 12:${String(40 + state.nextEventId).padStart(2, "0")}:00`,
+ });
+ state.nextEventId += 1;
+ };
+
+ if (shellConfig.mode === "fail-before-open") {
+ state.session = {
+ ...state.session,
+ closed_at: "2026-04-09 12:45:30",
+ metadata: {
+ closed_reason: "open_failed",
+ },
+ };
+ } else {
+ pushEvent("OPENED", {});
+ pushEvent("OUTPUT", { data: shellConfig.prompt });
}
- await page.addInitScript((config) => {
- const originalWebSocket = window.WebSocket;
- const CONNECTING = 0;
- const OPEN = 1;
- const CLOSING = 2;
- const CLOSED = 3;
-
- window.__EDGE_GATEWAY_SHELL_MESSAGES__ = [];
-
- class FakeShellSocket {
- constructor(url) {
- this.url = url;
- this.readyState = CONNECTING;
- this.inputBuffer = "";
- this.listeners = new Map();
- this.onopen = null;
- this.onmessage = null;
- this.onerror = null;
- this.onclose = null;
-
- window.setTimeout(() => {
- if (config.mode === "fail-before-open") {
- this.dispatch("error");
- this.readyState = CLOSED;
- this.dispatch("close");
- return;
- }
-
- this.readyState = OPEN;
- this.dispatch("open");
- this.dispatch("message", {
- data: JSON.stringify({ type: "opened" }),
- });
- this.dispatch("message", {
- data: JSON.stringify({ type: "output", data: config.prompt }),
- });
- }, 0);
- }
-
- addEventListener(eventName, callback) {
- const listeners = this.listeners.get(eventName) || [];
- listeners.push(callback);
- this.listeners.set(eventName, listeners);
- }
-
- removeEventListener(eventName, callback) {
- const listeners = this.listeners.get(eventName) || [];
- this.listeners.set(
- eventName,
- listeners.filter((listener) => listener !== callback)
- );
- }
-
- dispatch(eventName, payload = {}) {
- const event = { type: eventName, target: this, ...payload };
- const handler = this[`on${eventName}`];
-
- if (typeof handler === "function") {
- handler.call(this, event);
- }
-
- for (const listener of this.listeners.get(eventName) || []) {
- listener.call(this, event);
- }
- }
-
- send(message) {
- const parsed = JSON.parse(message);
- window.__EDGE_GATEWAY_SHELL_MESSAGES__.push(parsed);
-
- if (parsed.type === "input") {
- this.inputBuffer += String(parsed.data || "");
- if (/[\r\n]/.test(String(parsed.data || ""))) {
- const command = this.inputBuffer.replace(/[\r\n]+/g, "").trim();
- this.inputBuffer = "";
-
- if (command !== "") {
- this.dispatch("message", {
- data: JSON.stringify({
- type: "output",
- data: `${command}\r\n${config.directoryListing}\r\n${config.prompt}`,
- }),
- });
- }
- }
- }
-
- if (parsed.type === "close") {
- this.dispatch("message", {
- data: JSON.stringify({ type: "closed", code: 0 }),
- });
- this.close();
- }
- }
-
- close() {
- if (this.readyState === CLOSED) {
- return;
- }
-
- this.readyState = CLOSED;
- this.dispatch("close");
- }
- }
-
- function MockableWebSocket(url, protocols) {
- if (String(url) === config.websocketUrl) {
- return new FakeShellSocket(url, protocols);
- }
-
- return new originalWebSocket(url, protocols);
- }
-
- MockableWebSocket.CONNECTING = CONNECTING;
- MockableWebSocket.OPEN = OPEN;
- MockableWebSocket.CLOSING = CLOSING;
- MockableWebSocket.CLOSED = CLOSED;
- MockableWebSocket.prototype = originalWebSocket.prototype;
-
- window.WebSocket = MockableWebSocket;
- }, shellConfig);
+ edgeGatewayFixture.shellSessionsById[session.id] = state;
+ return state;
}
export function createPosFixture(overrides = {}) {
@@ -922,10 +888,50 @@ export function createPosFixture(overrides = {}) {
},
orderItemsByOrderId: {
54518: [
- { id: 9101, order_id: 54518, product_id: 53, product: products[0], quantity: 1, notes: "", reference: "", related_item_id: null, price: 649 },
- { id: 9102, order_id: 54518, product_id: 63, product: products[1], quantity: 1, notes: "", reference: "", related_item_id: 9101, price: 399 },
- { id: 9103, order_id: 54518, product_id: 64, product: products[2], quantity: 1, notes: "", reference: "", related_item_id: 9101, price: 25 },
- { id: 9104, order_id: 54518, product_id: 65, product: products[3], quantity: 1, notes: "", reference: "", related_item_id: 9101, price: 299 },
+ {
+ id: 9101,
+ order_id: 54518,
+ product_id: 53,
+ product: products[0],
+ quantity: 1,
+ notes: "",
+ reference: "",
+ related_item_id: null,
+ price: 649,
+ },
+ {
+ id: 9102,
+ order_id: 54518,
+ product_id: 63,
+ product: products[1],
+ quantity: 1,
+ notes: "",
+ reference: "",
+ related_item_id: 9101,
+ price: 399,
+ },
+ {
+ id: 9103,
+ order_id: 54518,
+ product_id: 64,
+ product: products[2],
+ quantity: 1,
+ notes: "",
+ reference: "",
+ related_item_id: 9101,
+ price: 25,
+ },
+ {
+ id: 9104,
+ order_id: 54518,
+ product_id: 65,
+ product: products[3],
+ quantity: 1,
+ notes: "",
+ reference: "",
+ related_item_id: 9101,
+ price: 299,
+ },
],
},
attachmentsByOrderId: {
@@ -957,9 +963,7 @@ export function createPosFixture(overrides = {}) {
54518: {},
},
paymentIntentsByOrderId: {},
- readers: [
- { id: "reader_online_1", label: "Mobile Reader", status: "online", action: null },
- ],
+ readers: [{ id: "reader_online_1", label: "Mobile Reader", status: "online", action: null }],
nextOrderId: 54519,
nextOrderItemId: 9200,
nextAttachmentId: 400,
@@ -1111,17 +1115,25 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
parsedUrl.searchParams.get("limit") || 10
);
- await route.fulfill(json({
- success: true,
- data: paginated.rows,
- meta: paginated.meta,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: paginated.rows,
+ meta: paginated.meta,
+ })
+ );
return true;
}
if (pathname.endsWith("/vehicles") && method === "GET") {
const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
- const vehicles = (posFixture.vehicles || []).filter((vehicle) => !search || String(vehicle.reg || "").toUpperCase().includes(search));
+ const vehicles = (posFixture.vehicles || []).filter(
+ (vehicle) =>
+ !search ||
+ String(vehicle.reg || "")
+ .toUpperCase()
+ .includes(search)
+ );
await route.fulfill(json({ success: true, data: vehicles }));
return true;
}
@@ -1134,13 +1146,15 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
if (pathname.endsWith("/users/customer") && method === "GET") {
const customerNumber = Number(parsedUrl.searchParams.get("customer_number") || 0);
const customer = posFixture.customersByNumber[customerNumber] || Object.values(posFixture.customersByNumber)[0];
- await route.fulfill(json({
- success: true,
- data: {
- customer_name: customer?.name || "",
- economic_customer: customer || null,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ customer_name: customer?.name || "",
+ economic_customer: customer || null,
+ },
+ })
+ );
return true;
}
@@ -1149,17 +1163,24 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
const vehicleData = posFixture.motorApiLookupByPlate?.[licensePlate];
if (!vehicleData) {
- await route.fulfill(json({
- success: false,
- data: null,
- }, 404));
+ await route.fulfill(
+ json(
+ {
+ success: false,
+ data: null,
+ },
+ 404
+ )
+ );
return true;
}
- await route.fulfill(json({
- success: true,
- data: vehicleData,
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: vehicleData,
+ })
+ );
return true;
}
@@ -1169,19 +1190,26 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
if (!search) {
return true;
}
- return String(customer.customerNumber).includes(search) || String(customer.name || "").toLowerCase().includes(search);
+ return (
+ String(customer.customerNumber).includes(search) ||
+ String(customer.name || "")
+ .toLowerCase()
+ .includes(search)
+ );
});
- await route.fulfill(json({
- success: true,
- data: customers,
- meta: {
- pagination: {
- page: 1,
- limit: 10,
- total: customers.length,
+ await route.fulfill(
+ json({
+ success: true,
+ data: customers,
+ meta: {
+ pagination: {
+ page: 1,
+ limit: 10,
+ total: customers.length,
+ },
},
- },
- }));
+ })
+ );
return true;
}
@@ -1206,12 +1234,15 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
const productId = Number(parsedUrl.searchParams.get("id") || 0);
const category = Number(parsedUrl.searchParams.get("category") || 0);
if (productId > 0) {
- await route.fulfill(json({ success: true, data: (posFixture.products || []).find((product) => product.id === productId) || null }));
+ await route.fulfill(
+ json({ success: true, data: (posFixture.products || []).find((product) => product.id === productId) || null })
+ );
return true;
}
- const products = category > 0
- ? (posFixture.products || []).filter((product) => Number(product.category) === category)
- : (posFixture.products || []);
+ const products =
+ category > 0
+ ? (posFixture.products || []).filter((product) => Number(product.category) === category)
+ : posFixture.products || [];
await route.fulfill(json({ success: true, data: products }));
return true;
}
@@ -1221,17 +1252,19 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
const orders = filterPosOrders(posFixture, filters)
.map((order) => withEffectiveOrderState(posFixture, order))
.sort((a, b) => Number(b.id) - Number(a.id));
- await route.fulfill(json({
- success: true,
- data: orders,
- meta: {
- pagination: {
- page: 1,
- limit: orders.length || 20,
- total: orders.length,
+ await route.fulfill(
+ json({
+ success: true,
+ data: orders,
+ meta: {
+ pagination: {
+ page: 1,
+ limit: orders.length || 20,
+ total: orders.length,
+ },
},
- },
- }));
+ })
+ );
return true;
}
@@ -1279,7 +1312,9 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
: {}),
};
}
- await route.fulfill(json({ success: true, data: withEffectiveOrderState(posFixture, posFixture.ordersById[orderId] || null) }));
+ await route.fulfill(
+ json({ success: true, data: withEffectiveOrderState(posFixture, posFixture.ordersById[orderId] || null) })
+ );
return true;
}
@@ -1289,9 +1324,7 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
if (posFixture.ordersById[orderId]) {
if (body.field) {
posFixture.ordersById[orderId][body.field] =
- body.field === "include_in_invoice"
- ? normalizeIncludeInInvoiceValue(body.value)
- : body.value;
+ body.field === "include_in_invoice" ? normalizeIncludeInInvoiceValue(body.value) : body.value;
} else {
posFixture.ordersById[orderId] = {
...posFixture.ordersById[orderId],
@@ -1302,7 +1335,9 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
};
}
}
- await route.fulfill(json({ success: true, data: withEffectiveOrderState(posFixture, posFixture.ordersById[orderId] || null) }));
+ await route.fulfill(
+ json({ success: true, data: withEffectiveOrderState(posFixture, posFixture.ordersById[orderId] || null) })
+ );
return true;
}
@@ -1354,7 +1389,9 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
if (pathname.endsWith("/order/items") && method === "DELETE") {
const orderItemId = Number(parsedUrl.searchParams.get("id") || 0);
Object.keys(posFixture.orderItemsByOrderId).forEach((orderIdKey) => {
- posFixture.orderItemsByOrderId[orderIdKey] = (posFixture.orderItemsByOrderId[orderIdKey] || []).filter((item) => item.id !== orderItemId);
+ posFixture.orderItemsByOrderId[orderIdKey] = (posFixture.orderItemsByOrderId[orderIdKey] || []).filter(
+ (item) => item.id !== orderItemId
+ );
});
await route.fulfill(json({ success: true, data: true }));
return true;
@@ -1396,7 +1433,9 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
if (pathname.endsWith("/orders/attachments") && method === "DELETE") {
const orderId = Number(parsedUrl.searchParams.get("order_id") || 0);
const attachmentId = Number(parsedUrl.searchParams.get("attachment_id") || 0);
- posFixture.attachmentsByOrderId[orderId] = (posFixture.attachmentsByOrderId[orderId] || []).filter((attachment) => attachment.id !== attachmentId);
+ posFixture.attachmentsByOrderId[orderId] = (posFixture.attachmentsByOrderId[orderId] || []).filter(
+ (attachment) => attachment.id !== attachmentId
+ );
await route.fulfill(json({ success: true, data: true }));
return true;
}
@@ -1404,12 +1443,14 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
if (pathname.endsWith("/orders/attachments/download") && method === "GET") {
const orderId = Number(parsedUrl.searchParams.get("order_id") || 0);
const attachmentId = Number(parsedUrl.searchParams.get("attachment_id") || 0);
- await route.fulfill(json({
- success: true,
- data: {
- download_link: `https://cdn.example.test/orders/${orderId}/attachments/${attachmentId}`,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ download_link: `https://cdn.example.test/orders/${orderId}/attachments/${attachmentId}`,
+ },
+ })
+ );
return true;
}
@@ -1421,20 +1462,23 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
posFixture.attachmentsByOrderId[orderId] = [];
}
- const existingAttachment = (posFixture.attachmentsByOrderId[orderId] || []).find((attachment) => {
- return String(attachment?.content?.other || "").toUpperCase() === "WASH_CERTIFICATE";
- }) || null;
+ const existingAttachment =
+ (posFixture.attachmentsByOrderId[orderId] || []).find((attachment) => {
+ return String(attachment?.content?.other || "").toUpperCase() === "WASH_CERTIFICATE";
+ }) || null;
if (existingAttachment) {
- await route.fulfill(json({
- success: true,
- data: {
- order_id: orderId,
- created: false,
- already_existed: true,
- attachment_id: existingAttachment.id,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ order_id: orderId,
+ created: false,
+ already_existed: true,
+ attachment_id: existingAttachment.id,
+ },
+ })
+ );
return true;
}
@@ -1456,28 +1500,32 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
};
posFixture.attachmentsByOrderId[orderId].push(attachment);
- await route.fulfill(json({
- success: true,
- data: {
- order_id: orderId,
- created: true,
- already_existed: false,
- attachment_id: attachmentId,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ order_id: orderId,
+ created: true,
+ already_existed: false,
+ attachment_id: attachmentId,
+ },
+ })
+ );
return true;
}
if (pathname.endsWith("/departments/order/recommended") && method === "GET") {
- await route.fulfill(json({
- success: true,
- data: {
- reg_1: {
- motorapi: [53],
- order_history: { 2: [], 3: [], 4: [], 5: [] },
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ reg_1: {
+ motorapi: [53],
+ order_history: { 2: [], 3: [], 4: [], 5: [] },
+ },
},
- },
- }));
+ })
+ );
return true;
}
@@ -1499,13 +1547,15 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "GET") {
const orderId = Number(parsedUrl.searchParams.get("id") || 0);
const paymentIntent = posFixture.paymentIntentsByOrderId[orderId] || null;
- await route.fulfill(json({
- success: true,
- data: {
- payment_intent: paymentIntent,
- has_payment_intent: !!paymentIntent,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ payment_intent: paymentIntent,
+ has_payment_intent: !!paymentIntent,
+ },
+ })
+ );
return true;
}
@@ -1526,13 +1576,15 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
},
};
posFixture.paymentIntentsByOrderId[orderId] = paymentIntent;
- await route.fulfill(json({
- success: true,
- data: {
- payment_intent: paymentIntent,
- has_payment_intent: true,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ payment_intent: paymentIntent,
+ has_payment_intent: true,
+ },
+ })
+ );
return true;
}
@@ -1562,29 +1614,40 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
},
};
posFixture.paymentIntentsByOrderId[orderId] = paymentIntent;
- await route.fulfill(json({
- success: true,
- data: {
- payment_intent: paymentIntent,
- has_payment_intent: true,
- },
- }));
+ await route.fulfill(
+ json({
+ success: true,
+ data: {
+ payment_intent: paymentIntent,
+ has_payment_intent: true,
+ },
+ })
+ );
return true;
}
return false;
}
-async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, method, edgeGatewayFixture, shellSocketConfig }) {
+async function handleEdgeGatewayRoute({
+ route,
+ request,
+ parsedUrl,
+ pathname,
+ method,
+ edgeGatewayFixture,
+ shellSocketConfig,
+}) {
if (!edgeGatewayFixture) {
return false;
}
if (pathname.endsWith("/edge-gateways") && method === "GET") {
const departmentId = Number(parsedUrl.searchParams.get("department_id") || 0);
- const gateways = departmentId > 0
- ? edgeGatewayFixture.gateways.filter((gateway) => gateway.department_id === departmentId)
- : edgeGatewayFixture.gateways;
+ const gateways =
+ departmentId > 0
+ ? edgeGatewayFixture.gateways.filter((gateway) => gateway.department_id === departmentId)
+ : edgeGatewayFixture.gateways;
await route.fulfill(json({ data: gateways }));
return true;
}
@@ -1597,16 +1660,22 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
if (pathname.endsWith("/edge-gateways/install-token") && method === "POST") {
const body = request.postDataJSON?.() || {};
- await route.fulfill(json({
- data: {
- claim_token_id: 9001,
- token: "edge-install-token",
- expires_at: "2026-04-08 08:45:00",
- install_command: "curl -fsSL https://api.truckwash.test/edge-agent/install.sh?token=edge-install-token | sudo bash",
- install_url: "https://api.truckwash.test/edge-agent/install.sh?token=edge-install-token",
- department_id: body.department_id ?? 1
- }
- }, 201));
+ await route.fulfill(
+ json(
+ {
+ data: {
+ claim_token_id: 9001,
+ token: "edge-install-token",
+ expires_at: "2026-04-08 08:45:00",
+ install_command:
+ "curl -fsSL https://api.truckwash.test/edge-agent/install.sh?token=edge-install-token | sudo bash",
+ install_url: "https://api.truckwash.test/edge-agent/install.sh?token=edge-install-token",
+ department_id: body.department_id ?? 1,
+ },
+ },
+ 201
+ )
+ );
return true;
}
@@ -1634,7 +1703,7 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
model: "Shelly Plus 1PM",
channel_count: 1,
online: true,
- capabilities: { generation: 2 }
+ capabilities: { generation: 2 },
},
};
}
@@ -1649,7 +1718,7 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
if (gateway) {
gateway.bindings = (body.bindings || []).map((binding, index) => ({
id: index + 1,
- ...binding
+ ...binding,
}));
}
await route.fulfill(json({ data: gateway?.bindings || [] }));
@@ -1663,7 +1732,7 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
const job = {
id: Date.now(),
target_version: body.target_version || "1.2.1",
- status: "PENDING"
+ status: "PENDING",
};
if (gateway) {
gateway.recent_updates.unshift(job);
@@ -1685,30 +1754,131 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
id: 12345,
gateway_id: gatewayId,
reason: body.reason || "Maintenance",
- expires_at: "2026-04-09 12:45:00"
+ expires_at: "2026-04-09 12:45:00",
+ metadata: {
+ transport: "API_POLLING",
+ },
};
if (gateway) {
gateway.recent_shell_sessions.unshift(session);
}
- await route.fulfill(json({
- data: {
- session,
- session_token: "shell-session-token",
- websocket_url: shellSocketConfig?.websocketUrl || "ws://127.0.0.1:65500/ws/browser-shell?token=shell-session-token"
+ createShellSessionFixture(
+ edgeGatewayFixture,
+ gatewayId,
+ session,
+ shellSocketConfig || buildEdgeGatewayShellSocketConfig()
+ );
+ await route.fulfill(
+ json(
+ {
+ data: {
+ session,
+ session_token: "shell-session-token",
+ transport: "API_POLLING",
+ },
+ },
+ 201
+ )
+ );
+ return true;
+ }
+
+ if (/\/edge-gateways\/\d+\/shell-sessions\/\d+\/events$/.test(pathname) && method === "GET") {
+ const segments = pathname.split("/");
+ const sessionId = Number(segments[4]);
+ const shellSession = edgeGatewayFixture.shellSessionsById[sessionId];
+ const afterId = Number(parsedUrl.searchParams.get("after_id") || 0);
+
+ await route.fulfill(
+ json({
+ data: {
+ session: shellSession?.session || null,
+ events: (shellSession?.events || []).filter((event) => Number(event.id) > afterId),
+ },
+ })
+ );
+ return true;
+ }
+
+ if (/\/edge-gateways\/\d+\/shell-sessions\/\d+\/input$/.test(pathname) && method === "POST") {
+ const segments = pathname.split("/");
+ const sessionId = Number(segments[4]);
+ const shellSession = edgeGatewayFixture.shellSessionsById[sessionId];
+ const body = request.postDataJSON?.() || {};
+
+ if (shellSession) {
+ shellSession.inputBuffer += String(body.data || "");
+ if (/[\r\n]/.test(String(body.data || ""))) {
+ const command = shellSession.inputBuffer.replace(/[\r\n]+/g, "").trim();
+ shellSession.inputBuffer = "";
+ if (command !== "") {
+ shellSession.events.push({
+ id: shellSession.nextEventId,
+ session_id: sessionId,
+ event_type: "OUTPUT",
+ payload: {
+ data: `${command}\r\n${shellSession.directoryListing}\r\n${shellSession.prompt}`,
+ },
+ created_at: `2026-04-09 12:${String(40 + shellSession.nextEventId).padStart(2, "0")}:00`,
+ });
+ shellSession.nextEventId += 1;
+ }
}
- }, 201));
+ }
+
+ await route.fulfill(json({ data: shellSession?.session || null }));
+ return true;
+ }
+
+ if (/\/edge-gateways\/\d+\/shell-sessions\/\d+\/resize$/.test(pathname) && method === "POST") {
+ const segments = pathname.split("/");
+ const sessionId = Number(segments[4]);
+ await route.fulfill(json({ data: edgeGatewayFixture.shellSessionsById[sessionId]?.session || null }));
+ return true;
+ }
+
+ if (/\/edge-gateways\/\d+\/shell-sessions\/\d+\/close$/.test(pathname) && method === "POST") {
+ const segments = pathname.split("/");
+ const sessionId = Number(segments[4]);
+ const shellSession = edgeGatewayFixture.shellSessionsById[sessionId];
+
+ if (shellSession) {
+ shellSession.session = {
+ ...shellSession.session,
+ closed_at: "2026-04-09 12:46:00",
+ metadata: {
+ ...(shellSession.session.metadata || {}),
+ closed_reason: "browser_requested",
+ },
+ };
+ shellSession.events.push({
+ id: shellSession.nextEventId,
+ session_id: sessionId,
+ event_type: "CLOSED",
+ payload: {
+ reason: "browser_requested",
+ code: 0,
+ },
+ created_at: `2026-04-09 12:${String(40 + shellSession.nextEventId).padStart(2, "0")}:00`,
+ });
+ shellSession.nextEventId += 1;
+ }
+
+ await route.fulfill(json({ data: shellSession?.session || null }));
return true;
}
if (/\/edge-gateways\/\d+\/rotate-credentials$/.test(pathname) && method === "POST") {
const gatewayId = Number(pathname.split("/")[2]);
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
- await route.fulfill(json({
- data: {
- gateway,
- agent_token: `rotated-token-${gatewayId}`
- }
- }));
+ await route.fulfill(
+ json({
+ data: {
+ gateway,
+ agent_token: `rotated-token-${gatewayId}`,
+ },
+ })
+ );
return true;
}
@@ -1720,12 +1890,14 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
.forEach((gateway) => {
gateway.department_transport_mode = body.transport_mode || "gateway";
});
- await route.fulfill(json({
- data: {
- department_id: departmentId,
- transport_mode: body.transport_mode || "gateway"
- }
- }));
+ await route.fulfill(
+ json({
+ data: {
+ department_id: departmentId,
+ transport_mode: body.transport_mode || "gateway",
+ },
+ })
+ );
return true;
}
@@ -1734,7 +1906,9 @@ async function handleEdgeGatewayRoute({ route, request, parsedUrl, pathname, met
export async function mockApi(page, options = {}) {
const posFixture = options.pos ? createPosFixture(options.pos === true ? {} : options.pos) : null;
- const selfServe = options.selfServe ? createSelfServeFixture(options.selfServe === true ? {} : options.selfServe) : null;
+ const selfServe = options.selfServe
+ ? createSelfServeFixture(options.selfServe === true ? {} : options.selfServe)
+ : null;
const edgeGatewayOptions =
options.edgeGateways && typeof options.edgeGateways === "object" ? options.edgeGateways : {};
const edgeGatewayFixture = options.edgeGateways === false ? null : createEdgeGatewayFixture(edgeGatewayOptions);
@@ -1742,8 +1916,6 @@ export async function mockApi(page, options = {}) {
? buildEdgeGatewayShellSocketConfig(edgeGatewayOptions.shellSocket || {})
: null;
- await installEdgeGatewayShellSocketMock(page, shellSocketConfig);
-
await page.route(API_HOST, async (route) => {
const request = route.request();
const url = request.url();
@@ -1757,16 +1929,16 @@ export async function mockApi(page, options = {}) {
data: {
recaptcha: {
enabled: false,
- site_key: ""
+ site_key: "",
},
rate_limit: {
enabled: false,
limit: 0,
remaining: 0,
reset: 0,
- warning: null
- }
- }
+ warning: null,
+ },
+ },
})
);
return;
@@ -1819,8 +1991,8 @@ export async function mockApi(page, options = {}) {
await route.fulfill(
json({
data: {
- token: options.loginToken || "e2e-token"
- }
+ token: options.loginToken || "e2e-token",
+ },
})
);
return;
@@ -1839,23 +2011,23 @@ export async function mockApi(page, options = {}) {
email: "e2e@example.com",
phone: {
number: "12345678",
- country_code: 45
+ country_code: 45,
},
notifications: {
wash_certificate_email: null,
email_notifications_enabled: true,
- sms_notifications_enabled: false
+ sms_notifications_enabled: false,
},
created_at: "2026-01-01T00:00:00.000Z",
updated_at: "2026-01-01T00:00:00.000Z",
display_name: "E2E User",
permissions: options.permissions || ["user"],
- economic_customer: []
+ economic_customer: [],
};
const sessionData = {
...defaultSession,
- ...(options.sessionData || {})
+ ...(options.sessionData || {}),
};
if (options.permissions) {
@@ -1864,7 +2036,7 @@ export async function mockApi(page, options = {}) {
await route.fulfill(
json({
- data: sessionData
+ data: sessionData,
})
);
return;
@@ -1911,12 +2083,14 @@ export async function mockApi(page, options = {}) {
},
];
- await route.fulfill(json({
- data: {
- items: departments,
- pagination: { cursor: null, nextCursor: null, limit: 20, total: departments.length },
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ items: departments,
+ pagination: { cursor: null, nextCursor: null, limit: 20, total: departments.length },
+ },
+ })
+ );
return;
}
@@ -1942,28 +2116,32 @@ export async function mockApi(page, options = {}) {
},
];
- await route.fulfill(json({
- data: {
- items: employees,
- pagination: { cursor: null, nextCursor: null, limit: 20, total: employees.length },
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ items: employees,
+ pagination: { cursor: null, nextCursor: null, limit: 20, total: employees.length },
+ },
+ })
+ );
return;
}
if (/\/modules\/workfeed\/employees\/[^/]+$/i.test(pathname) && method === "GET") {
const id = pathname.split("/").pop();
- await route.fulfill(json({
- data: {
- id,
- firstName: "Anne",
- lastName: "Nielsen",
- fullName: "Anne Nielsen",
- email: "anne.nielsen@example.com",
- departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3",
- active: true,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id,
+ firstName: "Anne",
+ lastName: "Nielsen",
+ fullName: "Anne Nielsen",
+ email: "anne.nielsen@example.com",
+ departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3",
+ active: true,
+ },
+ })
+ );
return;
}
@@ -2014,28 +2192,32 @@ export async function mockApi(page, options = {}) {
return true;
});
- await route.fulfill(json({
- data: {
- items: filtered,
- pagination: { cursor: null, nextCursor: null, limit: 20, total: filtered.length },
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ items: filtered,
+ pagination: { cursor: null, nextCursor: null, limit: 20, total: filtered.length },
+ },
+ })
+ );
return;
}
if (/\/modules\/workfeed\/shifts\/[^/]+$/i.test(pathname) && method === "GET") {
const id = pathname.split("/").pop();
- await route.fulfill(json({
- data: {
- id,
- title: "Morning Shift",
- status: "published",
- employeeID: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P",
- released: true,
- startAt: "2026-03-24T06:00:00Z",
- endAt: "2026-03-24T14:00:00Z",
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ id,
+ title: "Morning Shift",
+ status: "published",
+ employeeID: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P",
+ released: true,
+ startAt: "2026-03-24T06:00:00Z",
+ endAt: "2026-03-24T14:00:00Z",
+ },
+ })
+ );
return;
}
@@ -2094,7 +2276,9 @@ export async function mockApi(page, options = {}) {
const laneId = parsedUrl.searchParams.get("lane_id");
const reg = (parsedUrl.searchParams.get("reg") || "").toUpperCase();
const previewData = selfServe.previewByKey[`${laneId}:${reg}`] || null;
- await route.fulfill(json({ data: previewData || { allowed: false, questions: [], tasks: [], conditions: [], rules: [] } }));
+ await route.fulfill(
+ json({ data: previewData || { allowed: false, questions: [], tasks: [], conditions: [], rules: [] } })
+ );
return;
}
@@ -2103,13 +2287,14 @@ export async function mockApi(page, options = {}) {
const laneId = parsedUrl.searchParams.get("lane_id");
const reg = (parsedUrl.searchParams.get("reg") || "").toUpperCase();
- const summaryData = (
- (sessionId ? selfServe.summaryBySessionId[sessionId] : null)
- || selfServe.summaryByKey[`${laneId}:${reg}`]
- || null
- );
+ const summaryData =
+ (sessionId ? selfServe.summaryBySessionId[sessionId] : null) ||
+ selfServe.summaryByKey[`${laneId}:${reg}`] ||
+ null;
- await route.fulfill(json({ data: summaryData || { events: [], questions: [], tasks: [], conditions: [], rules: [] } }));
+ await route.fulfill(
+ json({ data: summaryData || { events: [], questions: [], tasks: [], conditions: [], rules: [] } })
+ );
return;
}
@@ -2118,35 +2303,43 @@ export async function mockApi(page, options = {}) {
const key = `${body.lane}:${String(body.reg || "").toUpperCase()}:${body.question}:${body.value}`;
const responseSummary = selfServe.answerResponseByKey[key] || null;
- await route.fulfill(json({
- data: {
- selfserve: responseSummary,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ selfserve: responseSummary,
+ },
+ })
+ );
return;
}
if (pathname.endsWith("/department/selfserve/tasks/attachments") && method === "GET") {
const taskId = parsedUrl.searchParams.get("id");
- await route.fulfill(json({
- data: selfServe.attachmentsByTaskId[taskId] || [],
- }));
+ await route.fulfill(
+ json({
+ data: selfServe.attachmentsByTaskId[taskId] || [],
+ })
+ );
return;
}
if (pathname.endsWith("/department/selfserve/tasks/attachments/download") && method === "GET") {
const taskId = parsedUrl.searchParams.get("task_id");
const attachmentId = parsedUrl.searchParams.get("attachment_id");
- await route.fulfill(json(selfServe.attachmentDownloadByKey[`${taskId}:${attachmentId}`] || { download_link: null }));
+ await route.fulfill(
+ json(selfServe.attachmentDownloadByKey[`${taskId}:${attachmentId}`] || { download_link: null })
+ );
return;
}
if (pathname.endsWith("/modules/self-serve/lane/services/allowed") && method === "POST") {
- await route.fulfill(json({
- data: {
- allowed_services: selfServe.laneAllowedServices,
- },
- }));
+ await route.fulfill(
+ json({
+ data: {
+ allowed_services: selfServe.laneAllowedServices,
+ },
+ })
+ );
return;
}
@@ -2171,8 +2364,8 @@ export async function mockApi(page, options = {}) {
json({
data: [
{ id: 1, name: "Copenhagen" },
- { id: 2, name: "Odense" }
- ]
+ { id: 2, name: "Odense" },
+ ],
})
);
return;
@@ -2191,9 +2384,9 @@ export async function mockApi(page, options = {}) {
data: [
{
id: 1,
- created_at: "2026-01-01T00:00:00.000Z"
- }
- ]
+ created_at: "2026-01-01T00:00:00.000Z",
+ },
+ ],
})
);
return;
@@ -2209,12 +2402,12 @@ export async function mockApi(page, options = {}) {
transactions: [
{ amount: 100 + monthBase, booked: true, excluded: false },
{ amount: 50 + monthBase, booked: true, excluded: false },
- { amount: 25, booked: false, excluded: false }
- ]
- }
- ]
- }
- }
+ { amount: 25, booked: false, excluded: false },
+ ],
+ },
+ ],
+ },
+ },
})
);
return;
@@ -2235,25 +2428,34 @@ export async function mockApi(page, options = {}) {
customer_number: 1001,
customer_name: "Acme Transport",
requires_action: false,
- transactions: [{ id: 1, date: "2026-01-10T00:00:00.000Z", amount: 80 + monthBase, booked: true, department_id: 1, excluded: false }],
+ transactions: [
+ {
+ id: 1,
+ date: "2026-01-10T00:00:00.000Z",
+ amount: 80 + monthBase,
+ booked: true,
+ department_id: 1,
+ excluded: false,
+ },
+ ],
meta: {
fixed_pricing: {
created_at: "2026-01-10T00:00:00.000Z",
price: 80 + monthBase,
original_price: 120 + monthBase,
- department_totals_relative: { 1: 40 + monthBase, 2: 40 }
- }
- }
- }
+ department_totals_relative: { 1: 40 + monthBase, 2: 40 },
+ },
+ },
+ },
],
collective_results: {
total_fixed_price: 80 + monthBase,
total_department_totals_relative_parsed: {
Copenhagen: 40 + monthBase,
- Odense: 40
- }
+ Odense: 40,
+ },
},
- warnings: []
+ warnings: [],
},
wash_subscriptions: {
customers: [
@@ -2262,25 +2464,34 @@ export async function mockApi(page, options = {}) {
customer_number: 1002,
customer_name: "Nordic Haul",
requires_action: false,
- transactions: [{ id: 2, date: "2026-01-05T00:00:00.000Z", amount: 40 + monthBase, booked: true, department_id: 1, excluded: false }],
+ transactions: [
+ {
+ id: 2,
+ date: "2026-01-05T00:00:00.000Z",
+ amount: 40 + monthBase,
+ booked: true,
+ department_id: 1,
+ excluded: false,
+ },
+ ],
meta: {
wash_subscription: {
created_at: "2026-01-05T00:00:00.000Z",
price: 40 + monthBase,
original_price: 55 + monthBase,
- department_totals_relative: { 1: 20 + monthBase, 2: 20 }
- }
- }
- }
+ department_totals_relative: { 1: 20 + monthBase, 2: 20 },
+ },
+ },
+ },
],
collective_results: {
total_subscription_price: 40 + monthBase,
subscription_price_department_distribution_parsed: {
Copenhagen: 20 + monthBase,
- Odense: 20
- }
+ Odense: 20,
+ },
},
- warnings: []
+ warnings: [],
},
customer_prices: {
customers: [
@@ -2289,24 +2500,33 @@ export async function mockApi(page, options = {}) {
customer_number: 1003,
customer_name: "Discount Fleet",
requires_action: false,
- transactions: [{ id: 3, date: "2026-01-08T00:00:00.000Z", amount: 15 + monthBase, booked: true, department_id: 2, excluded: false }],
+ transactions: [
+ {
+ id: 3,
+ date: "2026-01-08T00:00:00.000Z",
+ amount: 15 + monthBase,
+ booked: true,
+ department_id: 2,
+ excluded: false,
+ },
+ ],
meta: {
customer_price: {
created_at: "2026-01-08T00:00:00.000Z",
price: 15 + monthBase,
- department_totals_relative: { 2: 15 + monthBase }
- }
- }
- }
+ department_totals_relative: { 2: 15 + monthBase },
+ },
+ },
+ },
],
collective_results: {
total_customer_price: 15 + monthBase,
customer_price_department_distribution_parsed: {
- Odense: 15 + monthBase
- }
+ Odense: 15 + monthBase,
+ },
},
- warnings: []
- }
+ warnings: [],
+ },
})
);
return;
@@ -2326,25 +2546,34 @@ export async function mockApi(page, options = {}) {
customer_number: 1001,
customer_name: "Acme Transport",
requires_action: false,
- transactions: [{ id: 1, date: "2026-01-10T00:00:00.000Z", amount: 80 + monthBase, booked: true, department_id: 1, excluded: false }],
+ transactions: [
+ {
+ id: 1,
+ date: "2026-01-10T00:00:00.000Z",
+ amount: 80 + monthBase,
+ booked: true,
+ department_id: 1,
+ excluded: false,
+ },
+ ],
meta: {
fixed_pricing: {
created_at: "2026-01-10T00:00:00.000Z",
price: 80 + monthBase,
original_price: 120 + monthBase,
- department_totals_relative: { 1: 40 + monthBase, 2: 40 }
- }
- }
- }
+ department_totals_relative: { 1: 40 + monthBase, 2: 40 },
+ },
+ },
+ },
],
collective_results: {
total_fixed_price: 80 + monthBase,
total_department_totals_relative_parsed: {
Copenhagen: 40 + monthBase,
- Odense: 40
- }
+ Odense: 40,
+ },
},
- warnings: []
+ warnings: [],
})
);
return;
@@ -2364,25 +2593,34 @@ export async function mockApi(page, options = {}) {
customer_number: 1002,
customer_name: "Nordic Haul",
requires_action: false,
- transactions: [{ id: 2, date: "2026-01-05T00:00:00.000Z", amount: 40 + monthBase, booked: true, department_id: 1, excluded: false }],
+ transactions: [
+ {
+ id: 2,
+ date: "2026-01-05T00:00:00.000Z",
+ amount: 40 + monthBase,
+ booked: true,
+ department_id: 1,
+ excluded: false,
+ },
+ ],
meta: {
wash_subscription: {
created_at: "2026-01-05T00:00:00.000Z",
price: 40 + monthBase,
original_price: 55 + monthBase,
- department_totals_relative: { 1: 20 + monthBase, 2: 20 }
- }
- }
- }
+ department_totals_relative: { 1: 20 + monthBase, 2: 20 },
+ },
+ },
+ },
],
collective_results: {
total_subscription_price: 40 + monthBase,
subscription_price_department_distribution_parsed: {
Copenhagen: 20 + monthBase,
- Odense: 20
- }
+ Odense: 20,
+ },
},
- warnings: []
+ warnings: [],
})
);
return;
@@ -2402,23 +2640,32 @@ export async function mockApi(page, options = {}) {
customer_number: 1003,
customer_name: "Discount Fleet",
requires_action: false,
- transactions: [{ id: 3, date: "2026-01-08T00:00:00.000Z", amount: 15 + monthBase, booked: true, department_id: 2, excluded: false }],
+ transactions: [
+ {
+ id: 3,
+ date: "2026-01-08T00:00:00.000Z",
+ amount: 15 + monthBase,
+ booked: true,
+ department_id: 2,
+ excluded: false,
+ },
+ ],
meta: {
customer_price: {
created_at: "2026-01-08T00:00:00.000Z",
price: 15 + monthBase,
- department_totals_relative: { 2: 15 + monthBase }
- }
- }
- }
+ department_totals_relative: { 2: 15 + monthBase },
+ },
+ },
+ },
],
collective_results: {
total_customer_price: 15 + monthBase,
customer_price_department_distribution_parsed: {
- Odense: 15 + monthBase
- }
+ Odense: 15 + monthBase,
+ },
},
- warnings: []
+ warnings: [],
})
);
return;
@@ -2437,20 +2684,20 @@ export async function mockApi(page, options = {}) {
created_at: "2026-01-10T00:00:00.000Z",
price: 80 + monthBase,
original_price: 120 + monthBase,
- department_totals_relative: { 1: 40 + monthBase, 2: 40 }
- }
- }
- }
+ department_totals_relative: { 1: 40 + monthBase, 2: 40 },
+ },
+ },
+ },
],
includes: {
collective_fixed_pricing_results: {
total_fixed_price: 80 + monthBase,
total_department_totals_relative_parsed: {
Copenhagen: 40 + monthBase,
- Odense: 40
- }
- }
- }
+ Odense: 40,
+ },
+ },
+ },
})
);
return;
@@ -2469,20 +2716,20 @@ export async function mockApi(page, options = {}) {
created_at: "2026-01-05T00:00:00.000Z",
price: 40 + monthBase,
original_price: 55 + monthBase,
- department_totals_relative: { 1: 20 + monthBase, 2: 20 }
- }
- }
- }
+ department_totals_relative: { 1: 20 + monthBase, 2: 20 },
+ },
+ },
+ },
],
includes: {
collective_subscription_results: {
total_subscription_price: 40 + monthBase,
subscription_price_department_distribution_parsed: {
Copenhagen: 20 + monthBase,
- Odense: 20
- }
- }
- }
+ Odense: 20,
+ },
+ },
+ },
})
);
return;
@@ -2493,12 +2740,12 @@ export async function mockApi(page, options = {}) {
id: invoiceId,
customer_number: 1001 + index,
customer_name: `Customer ${invoiceId}`,
- total_net_amount: 100 + (index * 10)
+ total_net_amount: 100 + index * 10,
}));
await route.fulfill(
json({
- data: invoices
+ data: invoices,
})
);
return;
@@ -2517,8 +2764,8 @@ export async function mockApi(page, options = {}) {
booked_total: mismatch ? 145 : 120,
draft_total: null,
difference: mismatch ? 5 : 0,
- order_ids: mismatch ? [1, 2] : [3]
- }
+ order_ids: mismatch ? [1, 2] : [3],
+ },
})
);
return;
@@ -2544,20 +2791,20 @@ export async function mockApi(page, options = {}) {
order_ids: mismatch ? [1, 2] : [3],
customer: {
internal_customer_number: 4000 + Number(invoiceId),
- name: `Customer ${invoiceId}`
+ name: `Customer ${invoiceId}`,
},
internal: {
normalized: {
totals: {
net_total: internalTotal,
- billable_line_count: mismatch ? 2 : 1
- }
- }
- }
+ billable_line_count: mismatch ? 2 : 1,
+ },
+ },
+ },
},
comparison: {
totals: {
- internal_net_total: internalTotal
+ internal_net_total: internalTotal,
},
targets: {
booked: {
@@ -2565,7 +2812,7 @@ export async function mockApi(page, options = {}) {
status: mismatch ? "partial_mismatch" : "exact_match",
overall_match: !mismatch,
totals: {
- target_net_total: targetTotal
+ target_net_total: targetTotal,
},
mismatch_reasons: mismatch ? ["department_total_mismatch"] : [],
warnings: mismatch ? ["Line mismatch found"] : [],
@@ -2573,13 +2820,13 @@ export async function mockApi(page, options = {}) {
summary: {
internal_billable_count: mismatch ? 2 : 1,
target_billable_count: mismatch ? 2 : 1,
- mismatch_count: mismatch ? 1 : 0
- }
- }
- }
+ mismatch_count: mismatch ? 1 : 0,
+ },
+ },
+ },
},
- warnings: mismatch ? ["comparison warning"] : []
- }
+ warnings: mismatch ? ["comparison warning"] : [],
+ },
};
});
@@ -2589,7 +2836,7 @@ export async function mockApi(page, options = {}) {
compared: ids.length,
failed: 0,
results,
- errors: []
+ errors: [],
})
);
return;
diff --git a/tests/e2e/twoFactorAuth.spec.ts b/tests/e2e/twoFactorAuth.spec.ts
index 2b013da0..5fddfb61 100644
--- a/tests/e2e/twoFactorAuth.spec.ts
+++ b/tests/e2e/twoFactorAuth.spec.ts
@@ -1,4 +1,4 @@
-import { test, expect } from '@playwright/test';
+import { test, expect } from "@playwright/test";
import {
user2FACredentials,
subuser2FACredentials,
@@ -9,96 +9,96 @@ import {
cancel2FA,
goToUserLogin,
goToSubuserLogin,
-} from './fixtures';
+} from "./fixtures";
/** Two-Factor Authentication (2FA) Tests */
// User 2FA Tests
-test.describe('[AUTH][2FA][User]', () => {
- test('should display 2FA verification screen after login for 2FA-enabled user', async ({ page }) => {
+test.describe("[AUTH][2FA][User]", () => {
+ test("should display 2FA verification screen after login for 2FA-enabled user", async ({ page }) => {
await loginAsUserWith2FA(page);
-
+
// Verify 2FA screen elements
- await expect(page.locator('.fa-shield-alt')).toBeVisible();
+ await expect(page.locator(".fa-shield-alt")).toBeVisible();
await expect(page.locator('input[name="2fa_code"]')).toBeVisible();
await expect(page.locator('button[id="2fa-verify-button"]')).toBeVisible();
});
- test('should have disabled verify button when code is not 6 digits', async ({ page }) => {
+ test("should have disabled verify button when code is not 6 digits", async ({ page }) => {
await loginAsUserWith2FA(page);
-
+
// Empty code - button should be disabled
await expect(page.locator('button[id="2fa-verify-button"]')).toBeDisabled();
-
+
// Partial code - button should still be disabled
- await page.fill('input[name="2fa_code"]', '123');
+ await page.fill('input[name="2fa_code"]', "123");
await expect(page.locator('button[id="2fa-verify-button"]')).toBeDisabled();
});
- test('should enable verify button when code is 6 digits', async ({ page }) => {
+ test("should enable verify button when code is 6 digits", async ({ page }) => {
await loginAsUserWith2FA(page);
-
- await page.fill('input[name="2fa_code"]', '123456');
+
+ await page.fill('input[name="2fa_code"]', "123456");
await expect(page.locator('button[id="2fa-verify-button"]')).not.toBeDisabled();
});
- test('should show error for invalid 2FA code', async ({ page }) => {
+ test("should show error for invalid 2FA code", async ({ page }) => {
await loginAsUserWith2FA(page);
-
+
await verify2FACode(page, invalidCredentials.invalid2FACode);
-
+
// Should display error message
- await expect(page.locator('#2fa_auth_alert_error')).toBeVisible();
+ await expect(page.locator("#2fa_auth_alert_error")).toBeVisible();
});
- test('should return to login form when cancelling 2FA', async ({ page }) => {
+ test("should return to login form when cancelling 2FA", async ({ page }) => {
await loginAsUserWith2FA(page);
-
+
await cancel2FA(page);
-
+
// Should be back at login form
await expect(page.locator('input[name="customer_number"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
});
- test('should clear password when cancelling 2FA', async ({ page }) => {
+ test("should clear password when cancelling 2FA", async ({ page }) => {
await loginAsUserWith2FA(page);
-
+
await cancel2FA(page);
-
+
// Password field should be empty
const passwordValue = await page.locator('input[name="password"]').inputValue();
- expect(passwordValue).toBe('');
+ expect(passwordValue).toBe("");
});
- test('should login successfully with valid 2FA code', async ({ page }) => {
+ test("should login successfully with valid 2FA code", async ({ page }) => {
await loginAsUserWith2FA(page);
-
+
await verify2FACode(page, user2FACredentials.validCode);
-
+
// Should show success message and redirect to user dashboard
- await expect(page.locator('#2fa_auth_alert_success')).toBeVisible();
- await expect(page).toHaveURL('/user', { timeout: 5000 });
+ await expect(page.locator("#2fa_auth_alert_success")).toBeVisible();
+ await expect(page).toHaveURL("/user", { timeout: 5000 });
});
- test('should only accept numeric input in 2FA code field', async ({ page }) => {
+ test("should only accept numeric input in 2FA code field", async ({ page }) => {
await loginAsUserWith2FA(page);
-
+
const codeInput = page.locator('input[name="2fa_code"]');
- await codeInput.type('abc123');
-
+ await codeInput.type("abc123");
+
// Should only contain numbers
const inputValue = await codeInput.inputValue();
// The input has pattern="[0-9]*" and inputmode="numeric"
expect(inputValue.length).toBeLessThanOrEqual(6);
});
- test('should limit 2FA code to 6 characters', async ({ page }) => {
+ test("should limit 2FA code to 6 characters", async ({ page }) => {
await loginAsUserWith2FA(page);
-
+
const codeInput = page.locator('input[name="2fa_code"]');
- await codeInput.type('1234567890');
-
+ await codeInput.type("1234567890");
+
// Should be limited to 6 characters
const inputValue = await codeInput.inputValue();
expect(inputValue.length).toBe(6);
@@ -106,42 +106,42 @@ test.describe('[AUTH][2FA][User]', () => {
});
// Subuser 2FA Tests
-test.describe('[AUTH][2FA][Subuser]', () => {
- test('should display 2FA verification screen after login for 2FA-enabled subuser', async ({ page }) => {
+test.describe("[AUTH][2FA][Subuser]", () => {
+ test("should display 2FA verification screen after login for 2FA-enabled subuser", async ({ page }) => {
await loginAsSubuserWith2FA(page);
-
+
// Verify 2FA screen elements
- await expect(page.locator('.fa-shield-alt')).toBeVisible();
+ await expect(page.locator(".fa-shield-alt")).toBeVisible();
await expect(page.locator('input[name="2fa_code"]')).toBeVisible();
await expect(page.locator('button[id="2fa-verify-button"]')).toBeVisible();
});
- test('should show error for invalid 2FA code', async ({ page }) => {
+ test("should show error for invalid 2FA code", async ({ page }) => {
await loginAsSubuserWith2FA(page);
-
+
await verify2FACode(page, invalidCredentials.invalid2FACode);
-
+
// Should display error message
- await expect(page.locator('#2fa_auth_alert_error')).toBeVisible();
+ await expect(page.locator("#2fa_auth_alert_error")).toBeVisible();
});
- test('should return to login form when cancelling 2FA', async ({ page }) => {
+ test("should return to login form when cancelling 2FA", async ({ page }) => {
await loginAsSubuserWith2FA(page);
-
+
await cancel2FA(page);
-
+
// Should be back at subuser login form
await expect(page.locator('input[name="phone"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
});
- test('should login successfully with valid 2FA code', async ({ page }) => {
+ test("should login successfully with valid 2FA code", async ({ page }) => {
await loginAsSubuserWith2FA(page);
-
+
await verify2FACode(page, subuser2FACredentials.validCode);
-
+
// Should show success message and redirect to user dashboard
- await expect(page.locator('#2fa_auth_alert_success')).toBeVisible();
- await expect(page).toHaveURL('/user', { timeout: 5000 });
+ await expect(page.locator("#2fa_auth_alert_success")).toBeVisible();
+ await expect(page).toHaveURL("/user", { timeout: 5000 });
});
});
diff --git a/tests/e2e/userAuth.spec.ts b/tests/e2e/userAuth.spec.ts
index 792ffeff..84207a45 100644
--- a/tests/e2e/userAuth.spec.ts
+++ b/tests/e2e/userAuth.spec.ts
@@ -1,4 +1,4 @@
-import { test, expect } from '@playwright/test';
+import { test, expect } from "@playwright/test";
import {
userCredentials,
subuserPhoneCredentials,
@@ -14,98 +14,105 @@ import {
goToUserLogin,
goToSubuserLogin,
goToOperatorLogin,
-} from './fixtures';
+} from "./fixtures";
/** User Authentication Tests */
// Customer number login tests
-test('[AUTH][User][Customer number] should login successfully', async ({ page }) => {
+test("[AUTH][User][Customer number] should login successfully", async ({ page }) => {
await loginAsUser(page);
});
-test('[AUTH][User][Customer number] should fail to login using invalid password', async ({ page }) => {
+test("[AUTH][User][Customer number] should fail to login using invalid password", async ({ page }) => {
await goToUserLogin(page);
await page.fill('input[name="customer_number"]', userCredentials.customerNumber);
await page.fill('input[name="password"]', invalidCredentials.invalidPassword);
await page.click('button[id="login-button"]');
- await expect(page.locator('#user_auth_alert_error')).toHaveText('Invalid credentials');
+ await expect(page.locator("#user_auth_alert_error")).toHaveText("Invalid credentials");
});
-test('[AUTH][User][Customer number] should fail to login using invalid customer number', async ({ page }) => {
+test("[AUTH][User][Customer number] should fail to login using invalid customer number", async ({ page }) => {
await goToUserLogin(page);
await page.fill('input[name="customer_number"]', invalidCredentials.invalidCustomerNumber);
await page.fill('input[name="password"]', userCredentials.password);
await page.click('button[id="login-button"]');
- await expect(page.locator('#user_auth_alert_error')).toHaveText('Invalid credentials');
-})
+ await expect(page.locator("#user_auth_alert_error")).toHaveText("Invalid credentials");
+});
// Password reset request tests
-test('[AUTH][User][Customer number] should request password reset successfully', async ({ page }) => {
+test("[AUTH][User][Customer number] should request password reset successfully", async ({ page }) => {
await goToUserLogin(page);
await page.click('a[id="forgot-password-button"]');
- await expect(page).toHaveURL('/auth/password-reset');
+ await expect(page).toHaveURL("/auth/password-reset");
await page.fill('input[name="customer_number"]', userCredentials.customerNumber);
await page.click('button[id="password-reset-button"]');
- await expect(page.locator('#password_reset_alert_success')).toHaveText('If the customer exists, a password reset email has been sent.');
-})
-test('[AUTH][User][Customer number] should not show failure to request password reset using invalid customer number', async ({ page }) => {
+ await expect(page.locator("#password_reset_alert_success")).toHaveText(
+ "If the customer exists, a password reset email has been sent."
+ );
+});
+test("[AUTH][User][Customer number] should not show failure to request password reset using invalid customer number", async ({
+ page,
+}) => {
await goToUserLogin(page);
await page.click('a[id="forgot-password-button"]');
- await expect(page).toHaveURL('/auth/password-reset');
+ await expect(page).toHaveURL("/auth/password-reset");
await page.fill('input[name="customer_number"]', invalidCredentials.invalidCustomerNumber);
await page.click('button[id="password-reset-button"]');
- await expect(page.locator('#password_reset_alert_success')).toHaveText('If the customer exists, a password reset email has been sent.');
-})
+ await expect(page.locator("#password_reset_alert_success")).toHaveText(
+ "If the customer exists, a password reset email has been sent."
+ );
+});
// Password reset tests
-test('[AUTH][User][Customer number] should fail to reset password using invalid token', async ({ page }) => {
+test("[AUTH][User][Customer number] should fail to reset password using invalid token", async ({ page }) => {
await page.goto(`/auth/password-reset/${invalidCredentials.invalidToken}`);
- await expect(page.locator('#password_reset_alert_error')).toHaveText('Invalid or expired token');
-})
+ await expect(page.locator("#password_reset_alert_error")).toHaveText("Invalid or expired token");
+});
// Registration tests
-test('[AUTH][User][Customer number] should fail to register already existing cvr', async ({ page }) => {
- await page.goto('/customer-creation');
+test("[AUTH][User][Customer number] should fail to register already existing cvr", async ({ page }) => {
+ await page.goto("/customer-creation");
await page.fill('input[id="customer_cvr"]', customerRegistrationData.existingCvr);
await page.fill('input[id="email"]', customerRegistrationData.email);
await page.fill('input[id="customer_phone"]', customerRegistrationData.newPhone);
await page.click('button[id="customer-register-button"]');
- await expect(page.locator('#customer_creation_alert_error')).toHaveText('CVR already registered');
-})
-test('[AUTH][User][Customer number] should fail to register already existing company phone number', async ({ page }) => {
- await page.goto('/customer-creation');
+ await expect(page.locator("#customer_creation_alert_error")).toHaveText("CVR already registered");
+});
+test("[AUTH][User][Customer number] should fail to register already existing company phone number", async ({
+ page,
+}) => {
+ await page.goto("/customer-creation");
await page.fill('input[id="customer_cvr"]', customerRegistrationData.newCvr);
await page.fill('input[id="email"]', customerRegistrationData.email);
await page.fill('input[id="customer_phone"]', customerRegistrationData.existingPhone);
await page.click('button[id="customer-register-button"]');
- await expect(page.locator('#customer_creation_alert_error')).toHaveText('Company phone number already registered');
-})
-
+ await expect(page.locator("#customer_creation_alert_error")).toHaveText("Company phone number already registered");
+});
/** Subuser Authentication Tests */
// Phone number login tests
-test('[AUTH][Subuser][Phone number] should login successfully', async ({ page }) => {
+test("[AUTH][Subuser][Phone number] should login successfully", async ({ page }) => {
await loginAsSubuserByPhone(page);
-})
-test('[AUTH][Subuser][Phone number] should fail to login using invalid phone number', async ({ page }) => {
+});
+test("[AUTH][Subuser][Phone number] should fail to login using invalid phone number", async ({ page }) => {
await goToSubuserLogin(page);
await page.fill('input[name="phone_country_code"]', subuserPhoneCredentials.phoneCountryCode);
await page.fill('input[name="phone"]', invalidCredentials.invalidPhoneNumber);
await page.fill('input[name="password"]', subuserPhoneCredentials.password);
await page.click('button[id="subuser-login-button"]');
- await expect(page.locator('#subuser_auth_alert_error')).toHaveText('Subuser not found');
-})
-test('[AUTH][Subuser][Phone number] should fail to login using invalid password', async ({ page }) => {
+ await expect(page.locator("#subuser_auth_alert_error")).toHaveText("Subuser not found");
+});
+test("[AUTH][Subuser][Phone number] should fail to login using invalid password", async ({ page }) => {
await goToSubuserLogin(page);
await page.fill('input[name="phone_country_code"]', subuserPhoneCredentials.phoneCountryCode);
await page.fill('input[name="phone"]', subuserPhoneCredentials.phone);
await page.fill('input[name="password"]', invalidCredentials.invalidPassword);
await page.click('button[id="subuser-login-button"]');
- await expect(page.locator('#subuser_auth_alert_error')).toHaveText('Invalid password');
-})
+ await expect(page.locator("#subuser_auth_alert_error")).toHaveText("Invalid password");
+});
// Username login tests
-test('[AUTH][Subuser][Username] should login successfully', async ({ page }) => {
+test("[AUTH][Subuser][Username] should login successfully", async ({ page }) => {
await loginAsSubuserByUsername(page);
-})
-test('[AUTH][Subuser][Username] should fail to login using invalid username', async ({ page }) => {
+});
+test("[AUTH][Subuser][Username] should fail to login using invalid username", async ({ page }) => {
await goToSubuserLogin(page);
await page.click('button[id="subuser_login_method_username_button"]');
@@ -114,9 +121,9 @@ test('[AUTH][Subuser][Username] should fail to login using invalid username', as
await page.fill('input[name="username"]', invalidCredentials.invalidUsername);
await page.fill('input[name="password"]', subuserUsernameCredentials.password);
await page.click('button[id="subuser-login-button"]');
- await expect(page.locator('#subuser_auth_alert_error')).toHaveText('Subuser not found');
-})
-test('[AUTH][Subuser][Username] should fail to login using invalid password', async ({ page }) => {
+ await expect(page.locator("#subuser_auth_alert_error")).toHaveText("Subuser not found");
+});
+test("[AUTH][Subuser][Username] should fail to login using invalid password", async ({ page }) => {
await goToSubuserLogin(page);
await page.click('button[id="subuser_login_method_username_button"]');
@@ -125,48 +132,54 @@ test('[AUTH][Subuser][Username] should fail to login using invalid password', as
await page.fill('input[name="username"]', subuserUsernameCredentials.username);
await page.fill('input[name="password"]', invalidCredentials.invalidPassword);
await page.click('button[id="subuser-login-button"]');
- await expect(page.locator('#subuser_auth_alert_error')).toHaveText('Invalid password');
-})
+ await expect(page.locator("#subuser_auth_alert_error")).toHaveText("Invalid password");
+});
// Registration tests
-test('[AUTH][Subuser][Username] should fail to register with already existing phone number', async ({ page }) => {
- await page.goto('/customer-creation');
+test("[AUTH][Subuser][Username] should fail to register with already existing phone number", async ({ page }) => {
+ await page.goto("/customer-creation");
await page.fill('input[id="driver_cvr"]', driverRegistrationData.existingCvr);
await page.fill('input[id="driver_phone"]', driverRegistrationData.existingPhone);
await page.click('button[id="driver-register-button"]');
- await expect(page.locator('#driver_creation_alert_error')).toHaveText('Account already exists with this phone number');
-})
-test('[AUTH][Subuser][Username] should fail to register with invalid (7-char) CVR number', async ({ page }) => {
- await page.goto('/customer-creation');
+ await expect(page.locator("#driver_creation_alert_error")).toHaveText(
+ "Account already exists with this phone number"
+ );
+});
+test("[AUTH][Subuser][Username] should fail to register with invalid (7-char) CVR number", async ({ page }) => {
+ await page.goto("/customer-creation");
await page.fill('input[id="driver_cvr"]', driverRegistrationData.invalidCvr7Char);
await page.fill('input[id="driver_phone"]', driverRegistrationData.existingPhone);
await page.click('button[id="driver-register-button"]');
- await expect(page.locator('#driver_creation_alert_error')).toHaveText('Parameter cvr must be at least 8 characters long');
-})
-test('[AUTH][Subuser][Username] should fail to register with invalid (9-char) CVR number', async ({ page }) => {
- await page.goto('/customer-creation');
+ await expect(page.locator("#driver_creation_alert_error")).toHaveText(
+ "Parameter cvr must be at least 8 characters long"
+ );
+});
+test("[AUTH][Subuser][Username] should fail to register with invalid (9-char) CVR number", async ({ page }) => {
+ await page.goto("/customer-creation");
await page.fill('input[id="driver_cvr"]', driverRegistrationData.invalidCvr9Char);
await page.fill('input[id="driver_phone"]', driverRegistrationData.existingPhone);
await page.click('button[id="driver-register-button"]');
- await expect(page.locator('#driver_creation_alert_error')).toHaveText('Parameter cvr must be at most 8 characters long');
-})
+ await expect(page.locator("#driver_creation_alert_error")).toHaveText(
+ "Parameter cvr must be at most 8 characters long"
+ );
+});
/** Operator Authentication Tests */
// User ID login tests
-test('[AUTH][Operator][User ID] should login successfully', async ({ page }) => {
+test("[AUTH][Operator][User ID] should login successfully", async ({ page }) => {
await loginAsOperator(page);
-})
-test('[AUTH][Operator][User ID] should fail to login using invalid user ID', async ({ page }) => {
+});
+test("[AUTH][Operator][User ID] should fail to login using invalid user ID", async ({ page }) => {
await goToOperatorLogin(page);
await page.fill('input[name="user_id"]', invalidCredentials.invalidUserId);
await page.fill('input[name="password"]', operatorCredentials.password);
await page.click('button[id="operator_login_button"]');
- await expect(page.locator('#user_auth_alert_error')).toHaveText('Invalid credentials');
-})
-test('[AUTH][Operator][User ID] should fail to login using invalid password', async ({ page }) => {
+ await expect(page.locator("#user_auth_alert_error")).toHaveText("Invalid credentials");
+});
+test("[AUTH][Operator][User ID] should fail to login using invalid password", async ({ page }) => {
await goToOperatorLogin(page);
await page.fill('input[name="user_id"]', operatorCredentials.userId);
await page.fill('input[name="password"]', invalidCredentials.invalidOperatorPassword);
await page.click('button[id="operator_login_button"]');
- await expect(page.locator('#user_auth_alert_error')).toHaveText('Invalid credentials');
-})
+ await expect(page.locator("#user_auth_alert_error")).toHaveText("Invalid credentials");
+});
diff --git a/tests/e2e/userBookWash.spec.ts b/tests/e2e/userBookWash.spec.ts
index e9422188..4f604513 100644
--- a/tests/e2e/userBookWash.spec.ts
+++ b/tests/e2e/userBookWash.spec.ts
@@ -1,20 +1,20 @@
-import { test, expect } from '@playwright/test';
-import { loginAsUser, bookingTestData } from './fixtures';
+import { test, expect } from "@playwright/test";
+import { loginAsUser, bookingTestData } from "./fixtures";
/** User "Book Wash" Tests */
// Entire flow: Login -> Book Wash -> Check if booking is created
-test('[BOOKINGS][User][Creation] should create a new booking', async ({ page }) => {
+test("[BOOKINGS][User][Creation] should create a new booking", async ({ page }) => {
const { departmentId, registrationNumber, reference, poNumber, notes } = bookingTestData;
// Login as user
await loginAsUser(page);
// Does the book wash button appear?
- await expect(page.locator('#book-wash-button')).toBeVisible();
+ await expect(page.locator("#book-wash-button")).toBeVisible();
// Click the book wash button and check if it navigates to the booking page
await page.click('a[id="book-wash-button"]');
- await expect(page).toHaveURL('/user/bookings/book');
+ await expect(page).toHaveURL("/user/bookings/book");
// Select the department (Demo: 12)
const department_select_id = `#department-option-${departmentId}`;
@@ -22,11 +22,11 @@ test('[BOOKINGS][User][Creation] should create a new booking', async ({ page })
// Enter the registration number (Demo: EC21233)
// Click the button to reveal the input field
- await page.click('#reg1-button');
+ await page.click("#reg1-button");
// Fill in the registration number
- await page.fill('#reg1-input', registrationNumber);
+ await page.fill("#reg1-input", registrationNumber);
// Press Enter to confirm the input
- await page.press('#reg1-input', 'Enter');
+ await page.press("#reg1-input", "Enter");
// Note: Vehicle type selection only shows if vehicle is registered in the system.
// EC21233 is not registered, so no suggestions appear. Proceed to next step.
@@ -36,8 +36,8 @@ test('[BOOKINGS][User][Creation] should create a new booking', async ({ page })
// Select the product (Demo: first available product)
// Wait for products to load, then click the first product box
- await page.waitForSelector('.pos-product-box');
- await page.locator('.pos-product-box').first().click();
+ await page.waitForSelector(".pos-product-box");
+ await page.locator(".pos-product-box").first().click();
// Add-ons selection is optional
// Add-ons are displayed as clickable product rows (not buttons) after selecting a product
@@ -49,31 +49,31 @@ test('[BOOKINGS][User][Creation] should create a new booking', async ({ page })
// Set reference number (Demo: test_ref123)
await page.click('button:has-text("Tilføj reference")');
- await page.fill('#reference', reference);
- await page.press('#reference', 'Enter');
+ await page.fill("#reference", reference);
+ await page.press("#reference", "Enter");
// Set PO number (Demo: test_po123)
await page.click('button:has-text("Tilføj PO nummer")');
- await page.fill('#poNumber', poNumber);
- await page.press('#poNumber', 'Enter');
+ await page.fill("#poNumber", poNumber);
+ await page.press("#poNumber", "Enter");
// Set Note (Demo: test_note123)
await page.click('button:has-text("Tilføj note")');
- await page.fill('#notes', notes);
- await page.press('#notes', 'Enter');
+ await page.fill("#notes", notes);
+ await page.press("#notes", "Enter");
// Set Pickup (Demo: checked)
- await page.check('#pickup');
+ await page.check("#pickup");
// Go to confirmation page
- await page.click('#go-to-confirmation-button');
+ await page.click("#go-to-confirmation-button");
// Verify the booking summary is displayed
- await expect(page.locator('#booking-summary-title')).toBeVisible();
+ await expect(page.locator("#booking-summary-title")).toBeVisible();
// Confirm the booking
// Use force: true because an icon overlay can intercept pointer events on some browsers
- await page.click('#confirm-booking-button', { force: true });
+ await page.click("#confirm-booking-button", { force: true });
// Verify booking was created successfully (redirect to user dashboard or success message)
// The exact verification depends on the application behavior after booking creation
diff --git a/tests/e2e/userBookings.spec.ts b/tests/e2e/userBookings.spec.ts
index 4ca459a4..c89eef23 100644
--- a/tests/e2e/userBookings.spec.ts
+++ b/tests/e2e/userBookings.spec.ts
@@ -1,80 +1,76 @@
-import { test, expect } from '@playwright/test';
-import {
- loginAsUser,
- bookingTestData,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { loginAsUser, bookingTestData } from "./fixtures";
/** User bookings Tests */
-test('[PAGES][User][/user/bookings] should load the page', async ({ page }) => {
+test("[PAGES][User][/user/bookings] should load the page", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/bookings');
+ await page.goto("/user/bookings");
await expect(page).toHaveURL(/user\/bookings/);
});
// User bookings page content tests
-test('[PAGES][User][/user/bookings] should display the title', async ({ page }) => {
+test("[PAGES][User][/user/bookings] should display the title", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/bookings');
+ await page.goto("/user/bookings");
// Check if title contains 'Oversigt over bookinger'
- await expect(page.locator('.title')).toContainText('Oversigt over bookinger');
+ await expect(page.locator(".title")).toContainText("Oversigt over bookinger");
});
-
// Verify booking appears in list after creation (integrates userBookWash flow)
-test('[BOOKINGS][User][List] should show created booking after book wash flow', async ({ page }) => {
+test("[BOOKINGS][User][List] should show created booking after book wash flow", async ({ page }) => {
const { departmentId, registrationNumber, reference, poNumber, notes } = bookingTestData;
await loginAsUser(page);
// Book wash button on home/dashboard
- await page.click('#book-wash-button');
- await expect(page).toHaveURL('/user/bookings/book');
+ await page.click("#book-wash-button");
+ await expect(page).toHaveURL("/user/bookings/book");
// Select department
const department_select_id = `#department-option-${departmentId}`;
await page.click(department_select_id);
// Registration number
- await page.click('#reg1-button');
- await page.fill('#reg1-input', registrationNumber);
- await page.press('#reg1-input', 'Enter');
+ await page.click("#reg1-button");
+ await page.fill("#reg1-input", registrationNumber);
+ await page.press("#reg1-input", "Enter");
// Next
await page.click('button:has-text("Næste trin")');
// Select product
- await page.waitForSelector('.pos-product-box');
- await page.locator('.pos-product-box').first().click();
+ await page.waitForSelector(".pos-product-box");
+ await page.locator(".pos-product-box").first().click();
// Reference
await page.click('button:has-text("Tilføj reference")');
- await page.fill('#reference', reference);
- await page.press('#reference', 'Enter');
+ await page.fill("#reference", reference);
+ await page.press("#reference", "Enter");
// PO
await page.click('button:has-text("Tilføj PO nummer")');
- await page.fill('#poNumber', poNumber);
- await page.press('#poNumber', 'Enter');
+ await page.fill("#poNumber", poNumber);
+ await page.press("#poNumber", "Enter");
// Notes
await page.click('button:has-text("Tilføj note")');
- await page.fill('#notes', notes);
- await page.press('#notes', 'Enter');
+ await page.fill("#notes", notes);
+ await page.press("#notes", "Enter");
// Pickup
- await page.check('#pickup');
+ await page.check("#pickup");
// Confirmation
- await page.click('#go-to-confirmation-button');
- await expect(page.locator('#booking-summary-title')).toBeVisible();
+ await page.click("#go-to-confirmation-button");
+ await expect(page.locator("#booking-summary-title")).toBeVisible();
// Confirm
- await page.click('#confirm-booking-button', { force: true });
+ await page.click("#confirm-booking-button", { force: true });
await expect(page).toHaveURL(/\/user/);
// Verify in list
- await page.waitForLoadState('networkidle');
- await page.goto('/user/bookings');
- await expect(page.locator('text=' + registrationNumber).nth(1)).toBeVisible();
- await expect(page.locator('text=' + reference).nth(1)).toBeVisible();
-});
\ No newline at end of file
+ await page.waitForLoadState("networkidle");
+ await page.goto("/user/bookings");
+ await expect(page.locator("text=" + registrationNumber).nth(1)).toBeVisible();
+ await expect(page.locator("text=" + reference).nth(1)).toBeVisible();
+});
diff --git a/tests/e2e/userHome.spec.ts b/tests/e2e/userHome.spec.ts
index ae1e248f..c8a22434 100644
--- a/tests/e2e/userHome.spec.ts
+++ b/tests/e2e/userHome.spec.ts
@@ -1,4 +1,4 @@
-import { test, expect } from '@playwright/test';
+import { test, expect } from "@playwright/test";
import {
userCredentials,
subuserPhoneCredentials,
@@ -14,37 +14,37 @@ import {
goToUserLogin,
goToSubuserLogin,
goToOperatorLogin,
-} from './fixtures';
+} from "./fixtures";
/** User home Tests */
// User home page content tests
-test('[PAGES][User][/user] should display a welcome message', async ({ page }) => {
+test("[PAGES][User][/user] should display a welcome message", async ({ page }) => {
await loginAsUser(page);
// Check if the "Velkommen Demo" message is displayed
- await expect(page.locator('.title')).toContainText('Velkommen Demo');
+ await expect(page.locator(".title")).toContainText("Velkommen Demo");
});
// Book wash button visibility test
-test('[PAGES][User][/user] should display the book wash button', async ({ page }) => {
+test("[PAGES][User][/user] should display the book wash button", async ({ page }) => {
await loginAsUser(page);
// Check if the "book wash" element is visible
- await expect(page.locator('a#book-wash-button')).toBeVisible();
+ await expect(page.locator("a#book-wash-button")).toBeVisible();
});
// Add vehicle button visibility test
-test('[PAGES][User][/user] should display the add vehicle button', async ({ page }) => {
+test("[PAGES][User][/user] should display the add vehicle button", async ({ page }) => {
await loginAsUser(page);
// Check if the "create vehicle" element is visible
- await expect(page.locator('a#add-vehicle-button')).toBeVisible();
+ await expect(page.locator("a#add-vehicle-button")).toBeVisible();
});
// Download wash certificate button visibility test
-test('[PAGES][User][/user] should display the download certificate button', async ({ page }) => {
+test("[PAGES][User][/user] should display the download certificate button", async ({ page }) => {
await loginAsUser(page);
// Check if the "download certificate" element is visible
- await expect(page.locator('a#download-certificates-button')).toBeVisible();
+ await expect(page.locator("a#download-certificates-button")).toBeVisible();
});
// Download invoices button visibility test
-test('[PAGES][User][/user] should display the download invoices button', async ({ page }) => {
+test("[PAGES][User][/user] should display the download invoices button", async ({ page }) => {
await loginAsUser(page);
// Check if the "download invoices" element is visible
- await expect(page.locator('a#download-invoices-button')).toBeVisible();
-});
\ No newline at end of file
+ await expect(page.locator("a#download-invoices-button")).toBeVisible();
+});
diff --git a/tests/e2e/userInvoices.spec.ts b/tests/e2e/userInvoices.spec.ts
index a8adfa82..277e405a 100644
--- a/tests/e2e/userInvoices.spec.ts
+++ b/tests/e2e/userInvoices.spec.ts
@@ -1,42 +1,39 @@
-import { test, expect } from '@playwright/test';
-import {
- userCredentials,
- loginAsUser,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { userCredentials, loginAsUser } from "./fixtures";
/** User invoices Tests */
// User invoices page content tests
-test('[PAGES][User][/user/invoices] should load the page', async ({ page }) => {
+test("[PAGES][User][/user/invoices] should load the page", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/invoices');
+ await page.goto("/user/invoices");
await expect(page).toHaveURL(/invoices/);
});
-test('[PAGES][User][/user/invoices] should display the title', async ({ page }) => {
+test("[PAGES][User][/user/invoices] should display the title", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/invoices');
- await expect(page.locator('.title')).toContainText('Oversigt over fakturaer');
+ await page.goto("/user/invoices");
+ await expect(page.locator(".title")).toContainText("Oversigt over fakturaer");
});
// Invoices table tests
-test('[PAGES][User][/user/invoices] should display the invoices table', async ({ page }) => {
+test("[PAGES][User][/user/invoices] should display the invoices table", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/invoices');
- await expect(page.locator('table.is-fullwidth')).toBeVisible();
+ await page.goto("/user/invoices");
+ await expect(page.locator("table.is-fullwidth")).toBeVisible();
});
-test('[PAGES][User][/user/invoices] should show empty invoices list', async ({ page }) => {
+test("[PAGES][User][/user/invoices] should show empty invoices list", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/invoices');
- await expect(page.locator('tbody tr')).toHaveCount(0);
+ await page.goto("/user/invoices");
+ await expect(page.locator("tbody tr")).toHaveCount(0);
});
-test('[PAGES][User][/user/invoices] should display table headers', async ({ page }) => {
+test("[PAGES][User][/user/invoices] should display table headers", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/invoices');
- await expect(page.locator('thead th')).toHaveCount(6);
+ await page.goto("/user/invoices");
+ await expect(page.locator("thead th")).toHaveCount(6);
});
// TODO: tests for pagination, search, download, details, mobile, etc.
-// ...
\ No newline at end of file
+// ...
diff --git a/tests/e2e/userMyWashStart.spec.ts b/tests/e2e/userMyWashStart.spec.ts
index 79a049ff..7c2fc983 100644
--- a/tests/e2e/userMyWashStart.spec.ts
+++ b/tests/e2e/userMyWashStart.spec.ts
@@ -1,4 +1,4 @@
-import { test, expect } from '@playwright/test';
+import { test, expect } from "@playwright/test";
import {
userCredentials,
subuserPhoneCredentials,
@@ -15,37 +15,37 @@ import {
goToSubuserLogin,
goToOperatorLogin,
vehicleTestData,
-} from './fixtures';
+} from "./fixtures";
/** My wash start tests */
-const LIVE_WASH_START_ENABLED = process.env.PLAYWRIGHT_LIVE === '1';
+const LIVE_WASH_START_ENABLED = process.env.PLAYWRIGHT_LIVE === "1";
/**
* Test that the dynamic machine status image loads successfully after lane selection.
* Fails if ERR_BLOCKED_BY_ORB or other load error hides the image.
*/
-test('dynamic machine status image renders after lane selection @live', async ({ browser }) => {
- test.skip(!LIVE_WASH_START_ENABLED, 'Set PLAYWRIGHT_LIVE=1 to run the live wash-start machine image check.');
+test("dynamic machine status image renders after lane selection @live", async ({ browser }) => {
+ test.skip(!LIVE_WASH_START_ENABLED, "Set PLAYWRIGHT_LIVE=1 to run the live wash-start machine image check.");
// Mock geolocation to Copenhagen area for nearest department with lanes
const context = await browser.newContext({
geolocation: {
latitude: 55.6761,
- longitude: 12.5683
+ longitude: 12.5683,
},
- permissions: ['geolocation']
+ permissions: ["geolocation"],
});
const page = await context.newPage();
await loginAsUser(page);
- await page.goto('/user/wash/start');
- await page.waitForLoadState('networkidle');
+ await page.goto("/user/wash/start");
+ await page.waitForLoadState("networkidle");
// Wait for lane options to load
- await expect(page.locator('b-radio-button')).toBeVisible({ timeout: 45000 });
+ await expect(page.locator("b-radio-button")).toBeVisible({ timeout: 45000 });
// Select the second radio (first lane, skip 'Any')
- await page.locator('b-radio-button').nth(1).click();
+ await page.locator("b-radio-button").nth(1).click();
// Wait a bit for watch/effect
await page.waitForTimeout(1000);
diff --git a/tests/e2e/userProfileInvoicing.spec.ts b/tests/e2e/userProfileInvoicing.spec.ts
index bdb77742..66cd45aa 100644
--- a/tests/e2e/userProfileInvoicing.spec.ts
+++ b/tests/e2e/userProfileInvoicing.spec.ts
@@ -1,52 +1,49 @@
-import { test, expect } from '@playwright/test';
-import {
- userCredentials,
- loginAsUser,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { userCredentials, loginAsUser } from "./fixtures";
// Navigate to profile page helper
async function goToUserProfile(page) {
await page.click('a[href="/user/profile"]');
- await expect(page).toHaveURL('/user/profile');
+ await expect(page).toHaveURL("/user/profile");
}
// Helper to expand a category section by clicking its header
async function expandCategory(page, categoryText: string) {
- const categoryHeader = page.locator('.card-header').filter({ hasText: categoryText }).first();
+ const categoryHeader = page.locator(".card-header").filter({ hasText: categoryText }).first();
await categoryHeader.click();
// Wait for expansion animation
await page.waitForTimeout(200);
}
// Customer invoice information display tests
-test('[PROFILE][User][Invoicing] should display customer invoice information section', async ({ page }) => {
+test("[PROFILE][User][Invoicing] should display customer invoice information section", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Should display the invoicing section header
- await expect(page.locator('.card-header').filter({ hasText: 'Fakturaoplysninger' })).toBeVisible();
+ await expect(page.locator(".card-header").filter({ hasText: "Fakturaoplysninger" })).toBeVisible();
});
-test('[PROFILE][User][Invoicing] should display customer name', async ({ page }) => {
+test("[PROFILE][User][Invoicing] should display customer name", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the invoicing section
- await expandCategory(page, 'Fakturaoplysninger');
-
+ await expandCategory(page, "Fakturaoplysninger");
+
// Customer name field should be visible and disabled
- const customerNameInput = page.locator('.card-content input[disabled]').first();
+ const customerNameInput = page.locator(".card-content input[disabled]").first();
await expect(customerNameInput).toBeVisible();
});
-test('[PROFILE][User][Invoicing] should display customer number', async ({ page }) => {
+test("[PROFILE][User][Invoicing] should display customer number", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the invoicing section
- await expandCategory(page, 'Fakturaoplysninger');
-
+ await expandCategory(page, "Fakturaoplysninger");
+
// Customer number field should be visible (look for the input containing the customer number)
- const customerNumberInput = page.locator('.card-content input[disabled]').nth(1);
+ const customerNumberInput = page.locator(".card-content input[disabled]").nth(1);
await expect(customerNumberInput).toBeVisible();
});
diff --git a/tests/e2e/userProfileNotifications.spec.ts b/tests/e2e/userProfileNotifications.spec.ts
index 837a1c86..f80402a5 100644
--- a/tests/e2e/userProfileNotifications.spec.ts
+++ b/tests/e2e/userProfileNotifications.spec.ts
@@ -1,139 +1,138 @@
-import { test, expect } from '@playwright/test';
-import {
- userCredentials,
- loginAsUser,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { userCredentials, loginAsUser } from "./fixtures";
// Navigate to profile page helper
async function goToUserProfile(page) {
await page.click('a[href="/user/profile"]');
- await expect(page).toHaveURL('/user/profile');
+ await expect(page).toHaveURL("/user/profile");
}
// Helper to expand a category section by clicking its header
async function expandCategory(page, categoryText: string) {
- const categoryHeader = page.locator('.card-header').filter({ hasText: categoryText }).first();
+ const categoryHeader = page.locator(".card-header").filter({ hasText: categoryText }).first();
await categoryHeader.click();
// Wait for expansion animation
await page.waitForTimeout(200);
}
// Notifications section display tests
-test('[PROFILE][User][Notifications] should display email notifications section', async ({ page }) => {
+test("[PROFILE][User][Notifications] should display email notifications section", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Should display the notifications section header (E-mail subtitle)
- await expect(page.locator('.card-header').filter({ hasText: 'E-mail' }).first()).toBeVisible();
+ await expect(page.locator(".card-header").filter({ hasText: "E-mail" }).first()).toBeVisible();
});
-test('[PROFILE][User][Notifications] should display booking confirmation email field', async ({ page }) => {
+test("[PROFILE][User][Notifications] should display booking confirmation email field", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the email notifications section
- await expandCategory(page, 'E-mail');
-
+ await expandCategory(page, "E-mail");
+
// Booking confirmation email section should be visible
- await expect(page.locator('text=Booking bekræftelser')).toBeVisible();
+ await expect(page.locator("text=Booking bekræftelser")).toBeVisible();
});
-test('[PROFILE][User][Notifications] should display edit booking email button', async ({ page }) => {
+test("[PROFILE][User][Notifications] should display edit booking email button", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the email notifications section
- await expandCategory(page, 'E-mail');
-
+ await expandCategory(page, "E-mail");
+
// Edit booking email button should be visible
const editBookingEmailButton = page.locator('button:has-text("Rediger e-mail til booking")');
await expect(editBookingEmailButton).toBeVisible();
});
-test('[PROFILE][User][Notifications] should display email notifications toggle', async ({ page }) => {
+test("[PROFILE][User][Notifications] should display email notifications toggle", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the email notifications section
- await expandCategory(page, 'E-mail');
-
+ await expandCategory(page, "E-mail");
+
// Email notifications toggle should be visible
- await expect(page.locator('text=E-mail notifikationer')).toBeVisible();
+ await expect(page.locator("text=E-mail notifikationer")).toBeVisible();
});
-test('[PROFILE][User][Notifications] should display SMS notifications section', async ({ page }) => {
+test("[PROFILE][User][Notifications] should display SMS notifications section", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Should display the SMS notifications section header
- await expect(page.locator('.card-header').filter({ hasText: 'SMS' }).first()).toBeVisible();
+ await expect(page.locator(".card-header").filter({ hasText: "SMS" }).first()).toBeVisible();
});
-test('[PROFILE][User][Notifications] should display SMS notifications toggle', async ({ page }) => {
+test("[PROFILE][User][Notifications] should display SMS notifications toggle", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the SMS notifications section
- await expandCategory(page, 'SMS');
-
+ await expandCategory(page, "SMS");
+
// SMS notifications toggle should be visible
- await expect(page.getByText('SMS notifikationer', { exact: true })).toBeVisible();
+ await expect(page.getByText("SMS notifikationer", { exact: true })).toBeVisible();
});
// Notifications button functionality tests
-test('[PROFILE][User][Notifications] should open edit booking email dialog when clicking edit button', async ({ page }) => {
+test("[PROFILE][User][Notifications] should open edit booking email dialog when clicking edit button", async ({
+ page,
+}) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the email notifications section
- await expandCategory(page, 'E-mail');
-
+ await expandCategory(page, "E-mail");
+
// Click edit booking email button
const editBookingEmailButton = page.locator('button:has-text("Rediger e-mail til booking")');
await editBookingEmailButton.click();
-
+
// Should show email dialog (SweetAlert)
- await expect(page.locator('.swal2-popup')).toBeVisible();
- await expect(page.locator('.swal2-title')).toContainText('e-mailadresse');
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+ await expect(page.locator(".swal2-title")).toContainText("e-mailadresse");
});
/** Email Validation Tests */
-test('[PROFILE][User][Notifications] should show validation error for invalid email format', async ({ page }) => {
+test("[PROFILE][User][Notifications] should show validation error for invalid email format", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the email notifications section
- await expandCategory(page, 'E-mail');
-
+ await expandCategory(page, "E-mail");
+
// Click edit booking email button
const editBookingEmailButton = page.locator('button:has-text("Rediger e-mail til booking")');
await editBookingEmailButton.click();
-
+
// Fill in invalid email
- await page.fill('.swal2-input', 'invalid-email');
-
+ await page.fill(".swal2-input", "invalid-email");
+
// Click confirm
- await page.click('.swal2-confirm');
-
+ await page.click(".swal2-confirm");
+
// Should show validation error
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
-test('[PROFILE][User][Notifications] should show validation error when email is empty', async ({ page }) => {
+test("[PROFILE][User][Notifications] should show validation error when email is empty", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the email notifications section
- await expandCategory(page, 'E-mail');
-
+ await expandCategory(page, "E-mail");
+
// Click edit booking email button
const editBookingEmailButton = page.locator('button:has-text("Rediger e-mail til booking")');
await editBookingEmailButton.click();
-
+
// Click confirm without filling email
- await page.click('.swal2-confirm');
-
+ await page.click(".swal2-confirm");
+
// Should show validation error
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
diff --git a/tests/e2e/userProfileSecurity.spec.ts b/tests/e2e/userProfileSecurity.spec.ts
index 3f46e9cf..1d2f4f49 100644
--- a/tests/e2e/userProfileSecurity.spec.ts
+++ b/tests/e2e/userProfileSecurity.spec.ts
@@ -1,313 +1,333 @@
-import { test, expect } from '@playwright/test';
-import {
- userCredentials,
- loginAsUser,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { userCredentials, loginAsUser } from "./fixtures";
// Navigate to profile page helper
async function goToUserProfile(page) {
await page.click('a[href="/user/profile"]');
- await expect(page).toHaveURL('/user/profile');
+ await expect(page).toHaveURL("/user/profile");
}
// Helper to expand a category section by clicking its header
async function expandCategory(page, categoryText: string) {
- const categoryHeader = page.locator('.card-header').filter({ hasText: categoryText }).first();
+ const categoryHeader = page.locator(".card-header").filter({ hasText: categoryText }).first();
await categoryHeader.click();
// Wait for expansion animation
await page.waitForTimeout(200);
}
// Security section display tests
-test('[PROFILE][User][Security] should display security section', async ({ page }) => {
+test("[PROFILE][User][Security] should display security section", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Should display the security section header
- await expect(page.locator('.card-header').filter({ hasText: 'Sikkerhed' }).first()).toBeVisible();
+ await expect(page.locator(".card-header").filter({ hasText: "Sikkerhed" }).first()).toBeVisible();
});
-test('[PROFILE][User][Security] should display email field', async ({ page }) => {
+test("[PROFILE][User][Security] should display email field", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Email field should be visible (look for the edit icon inside expanded content)
- await expect(page.locator('.card-content i.fas.fa-edit').first()).toBeVisible();
+ await expect(page.locator(".card-content i.fas.fa-edit").first()).toBeVisible();
});
-test('[PROFILE][User][Security] should display edit email button', async ({ page }) => {
+test("[PROFILE][User][Security] should display edit email button", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Edit email button should be visible (Danish: "Rediger e-mail")
- const editEmailButton = page.locator('button').filter({ hasText: 'Rediger e-mail' }).first();
+ const editEmailButton = page.locator("button").filter({ hasText: "Rediger e-mail" }).first();
await expect(editEmailButton).toBeVisible();
});
-test('[PROFILE][User][Security] should display phone number field', async ({ page }) => {
+test("[PROFILE][User][Security] should display phone number field", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Phone number section should be visible
- await expect(page.getByText('Telefonnummer', { exact: true }).first()).toBeVisible();
+ await expect(page.getByText("Telefonnummer", { exact: true }).first()).toBeVisible();
});
-test('[PROFILE][User][Security] should display edit phone number button', async ({ page }) => {
+test("[PROFILE][User][Security] should display edit phone number button", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Edit phone button should be visible (first one, in Security section)
- const editPhoneButton = page.locator('button').filter({ hasText: /^Rediger telefonnummer$/ }).first();
+ const editPhoneButton = page
+ .locator("button")
+ .filter({ hasText: /^Rediger telefonnummer$/ })
+ .first();
await expect(editPhoneButton).toBeVisible();
});
-test('[PROFILE][User][Security] should display change password button', async ({ page }) => {
+test("[PROFILE][User][Security] should display change password button", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Change password button should be visible
const changePasswordButton = page.locator('button:has-text("Skift adgangskode")');
await expect(changePasswordButton).toBeVisible();
});
-test('[PROFILE][User][Security] should display logout button', async ({ page }) => {
+test("[PROFILE][User][Security] should display logout button", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Logout button should be visible
const logoutButton = page.locator('button:has-text("Log ud")');
await expect(logoutButton).toBeVisible();
});
// Security button functionality tests
-test('[PROFILE][User][Security] should open edit email dialog when clicking edit email button', async ({ page }) => {
+test("[PROFILE][User][Security] should open edit email dialog when clicking edit email button", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click edit email button (Danish: "Rediger e-mail")
- const editEmailButton = page.locator('button').filter({ hasText: 'Rediger e-mail' }).first();
+ const editEmailButton = page.locator("button").filter({ hasText: "Rediger e-mail" }).first();
await editEmailButton.click();
-
+
// Should show password prompt dialog (SweetAlert)
- await expect(page.locator('.swal2-popup')).toBeVisible();
- await expect(page.locator('.swal2-title')).toBeVisible();
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+ await expect(page.locator(".swal2-title")).toBeVisible();
});
-test('[PROFILE][User][Security] should open change phone number dialog when clicking edit phone button', async ({ page }) => {
+test("[PROFILE][User][Security] should open change phone number dialog when clicking edit phone button", async ({
+ page,
+}) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click edit phone button (first one, in Security section)
- const editPhoneButton = page.locator('button').filter({ hasText: /^Rediger telefonnummer$/ }).first();
+ const editPhoneButton = page
+ .locator("button")
+ .filter({ hasText: /^Rediger telefonnummer$/ })
+ .first();
await editPhoneButton.click();
-
+
// Should show phone number dialog (SweetAlert)
- await expect(page.locator('.swal2-popup')).toBeVisible();
- await expect(page.locator('.swal2-title')).toContainText('telefonnummer');
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+ await expect(page.locator(".swal2-title")).toContainText("telefonnummer");
});
-test('[PROFILE][User][Security] should open change password dialog when clicking change password button', async ({ page }) => {
+test("[PROFILE][User][Security] should open change password dialog when clicking change password button", async ({
+ page,
+}) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click change password button
const changePasswordButton = page.locator('button:has-text("Skift adgangskode")');
await changePasswordButton.click();
-
+
// Should show change password dialog (SweetAlert)
- await expect(page.locator('.swal2-popup')).toBeVisible();
- await expect(page.locator('.swal2-title')).toContainText('adgangskode');
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+ await expect(page.locator(".swal2-title")).toContainText("adgangskode");
});
-test('[PROFILE][User][Security] should open logout confirmation dialog when clicking logout button', async ({ page }) => {
+test("[PROFILE][User][Security] should open logout confirmation dialog when clicking logout button", async ({
+ page,
+}) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click logout button
const logoutButton = page.locator('button:has-text("Log ud")');
await logoutButton.click();
-
+
// Should show confirmation dialog (SweetAlert)
- await expect(page.locator('.swal2-popup')).toBeVisible();
+ await expect(page.locator(".swal2-popup")).toBeVisible();
});
/** Password Change Validation Tests */
-test('[PROFILE][User][Security] should show validation error when passwords do not match', async ({ page }) => {
+test("[PROFILE][User][Security] should show validation error when passwords do not match", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click change password button
const changePasswordButton = page.locator('button:has-text("Skift adgangskode")');
await changePasswordButton.click();
-
+
// Fill in mismatched passwords
- await page.fill('#swal-input1', 'currentpass');
- await page.fill('#swal-input2', 'newpassword1');
- await page.fill('#swal-input3', 'newpassword2');
-
+ await page.fill("#swal-input1", "currentpass");
+ await page.fill("#swal-input2", "newpassword1");
+ await page.fill("#swal-input3", "newpassword2");
+
// Click confirm
- await page.click('.swal2-confirm');
-
+ await page.click(".swal2-confirm");
+
// Should show validation error
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
-test('[PROFILE][User][Security] should show validation error when password is too short', async ({ page }) => {
+test("[PROFILE][User][Security] should show validation error when password is too short", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click change password button
const changePasswordButton = page.locator('button:has-text("Skift adgangskode")');
await changePasswordButton.click();
-
+
// Fill in short password
- await page.fill('#swal-input1', 'currentpass');
- await page.fill('#swal-input2', '123');
- await page.fill('#swal-input3', '123');
-
+ await page.fill("#swal-input1", "currentpass");
+ await page.fill("#swal-input2", "123");
+ await page.fill("#swal-input3", "123");
+
// Click confirm
- await page.click('.swal2-confirm');
-
+ await page.click(".swal2-confirm");
+
// Should show validation error about password length
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
-test('[PROFILE][User][Security] should show validation error when fields are empty', async ({ page }) => {
+test("[PROFILE][User][Security] should show validation error when fields are empty", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click change password button
const changePasswordButton = page.locator('button:has-text("Skift adgangskode")');
await changePasswordButton.click();
-
+
// Click confirm without filling fields
- await page.click('.swal2-confirm');
-
+ await page.click(".swal2-confirm");
+
// Should show validation error
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
/** Phone Number Validation Tests */
-test('[PROFILE][User][Security] should show validation error when phone number is empty', async ({ page }) => {
+test("[PROFILE][User][Security] should show validation error when phone number is empty", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click edit phone button (first one, in Security section)
- const editPhoneButton = page.locator('button').filter({ hasText: /^Rediger telefonnummer$/ }).first();
+ const editPhoneButton = page
+ .locator("button")
+ .filter({ hasText: /^Rediger telefonnummer$/ })
+ .first();
await editPhoneButton.click();
-
+
// Click confirm without filling phone number
- await page.click('.swal2-confirm');
-
+ await page.click(".swal2-confirm");
+
// Should show validation error
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
-test('[PROFILE][User][Security] should show validation error when country code is empty', async ({ page }) => {
+test("[PROFILE][User][Security] should show validation error when country code is empty", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click edit phone button (first one, in Security section)
- const editPhoneButton = page.locator('button').filter({ hasText: /^Rediger telefonnummer$/ }).first();
+ const editPhoneButton = page
+ .locator("button")
+ .filter({ hasText: /^Rediger telefonnummer$/ })
+ .first();
await editPhoneButton.click();
-
+
// Fill in phone number but not country code
- await page.fill('#swal-input1', '12345678');
-
+ await page.fill("#swal-input1", "12345678");
+
// Click confirm
- await page.click('.swal2-confirm');
-
+ await page.click(".swal2-confirm");
+
// Should show validation error
- await expect(page.locator('.swal2-validation-message')).toBeVisible();
+ await expect(page.locator(".swal2-validation-message")).toBeVisible();
});
/** Dialog Cancel Tests */
-test('[PROFILE][User][Security] should close dialog when cancel is clicked on password change', async ({ page }) => {
+test("[PROFILE][User][Security] should close dialog when cancel is clicked on password change", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click change password button
const changePasswordButton = page.locator('button:has-text("Skift adgangskode")');
await changePasswordButton.click();
-
+
// Verify dialog is open
- await expect(page.locator('.swal2-popup')).toBeVisible();
-
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+
// Click cancel
- await page.click('.swal2-cancel');
-
+ await page.click(".swal2-cancel");
+
// Dialog should be closed
- await expect(page.locator('.swal2-popup')).not.toBeVisible();
+ await expect(page.locator(".swal2-popup")).not.toBeVisible();
});
-test('[PROFILE][User][Security] should close dialog when cancel is clicked on phone number change', async ({ page }) => {
+test("[PROFILE][User][Security] should close dialog when cancel is clicked on phone number change", async ({
+ page,
+}) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Expand the security section
- await expandCategory(page, 'Sikkerhed');
-
+ await expandCategory(page, "Sikkerhed");
+
// Click edit phone button (first one, in Security section)
- const editPhoneButton = page.locator('button').filter({ hasText: /^Rediger telefonnummer$/ }).first();
+ const editPhoneButton = page
+ .locator("button")
+ .filter({ hasText: /^Rediger telefonnummer$/ })
+ .first();
await editPhoneButton.click();
-
+
// Verify dialog is open
- await expect(page.locator('.swal2-popup')).toBeVisible();
-
+ await expect(page.locator(".swal2-popup")).toBeVisible();
+
// Click cancel
- await page.click('.swal2-cancel');
-
+ await page.click(".swal2-cancel");
+
// Dialog should be closed
- await expect(page.locator('.swal2-popup')).not.toBeVisible();
+ await expect(page.locator(".swal2-popup")).not.toBeVisible();
});
diff --git a/tests/e2e/userProfileVisibility.spec.ts b/tests/e2e/userProfileVisibility.spec.ts
index 703be6e6..169980cd 100644
--- a/tests/e2e/userProfileVisibility.spec.ts
+++ b/tests/e2e/userProfileVisibility.spec.ts
@@ -1,34 +1,30 @@
-import { test, expect } from '@playwright/test';
-import {
- userCredentials,
- loginAsUser,
- loginAsSubuserByPhone,
-} from './fixtures';
+import { test, expect } from "@playwright/test";
+import { userCredentials, loginAsUser, loginAsSubuserByPhone } from "./fixtures";
// Navigate to profile page helper
async function goToUserProfile(page) {
await page.click('a[href="/user/profile"]');
- await expect(page).toHaveURL('/user/profile');
+ await expect(page).toHaveURL("/user/profile");
}
/** User Profile Tests - Section Visibility */
-test('[PROFILE][User][Visibility] should NOT display subuser sections for customer user', async ({ page }) => {
+test("[PROFILE][User][Visibility] should NOT display subuser sections for customer user", async ({ page }) => {
await loginAsUser(page);
await goToUserProfile(page);
-
+
// Customer users should see Invoicing section
- await expect(page.locator('.card-header').filter({ hasText: 'Fakturaoplysninger' })).toBeVisible();
+ await expect(page.locator(".card-header").filter({ hasText: "Fakturaoplysninger" })).toBeVisible();
// Grant selector (users icon) should not be visible for customers
- await expect(page.locator('.card-header .fa-users')).not.toBeVisible();
+ await expect(page.locator(".card-header .fa-users")).not.toBeVisible();
});
-test('[PROFILE][Subuser][Visibility] should NOT display customer sections for subuser', async ({ page }) => {
+test("[PROFILE][Subuser][Visibility] should NOT display customer sections for subuser", async ({ page }) => {
await loginAsSubuserByPhone(page);
await goToUserProfile(page);
-
+
// Invoicing section should not be visible for subusers
- await expect(page.locator('.card-header').filter({ hasText: 'Fakturaoplysninger' })).not.toBeVisible();
+ await expect(page.locator(".card-header").filter({ hasText: "Fakturaoplysninger" })).not.toBeVisible();
// Subuser should see driver profile sections
- await expect(page.locator('.card-header').filter({ hasText: 'Chauffør' }).first()).toBeVisible();
+ await expect(page.locator(".card-header").filter({ hasText: "Chauffør" }).first()).toBeVisible();
});
diff --git a/tests/e2e/userVehicles.spec.ts b/tests/e2e/userVehicles.spec.ts
index 75dc842b..6a161f5c 100644
--- a/tests/e2e/userVehicles.spec.ts
+++ b/tests/e2e/userVehicles.spec.ts
@@ -1,4 +1,4 @@
-import { test, expect } from '@playwright/test';
+import { test, expect } from "@playwright/test";
import {
userCredentials,
subuserPhoneCredentials,
@@ -15,21 +15,21 @@ import {
goToSubuserLogin,
goToOperatorLogin,
vehicleTestData,
-} from './fixtures';
+} from "./fixtures";
/** User vehicles Tests */
// User vehicles page content tests
-test('[PAGES][User][/user/vehicles] should display the title', async ({ page }) => {
+test("[PAGES][User][/user/vehicles] should display the title", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/vehicles');
+ await page.goto("/user/vehicles");
// Check if the "Oversigt over registrerede køretøjer" message is displayed
- await expect(page.locator('.title')).toContainText('Oversigt over registrerede køretøjer');
+ await expect(page.locator(".title")).toContainText("Oversigt over registrerede køretøjer");
});
// Add vehicle button visibility test
-test('[PAGES][User][/user/vehicles] should display the add vehicle button', async ({ page }) => {
+test("[PAGES][User][/user/vehicles] should display the add vehicle button", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/vehicles');
+ await page.goto("/user/vehicles");
// Check if the "Tilføj køretøj" add button is visible
await expect(page.locator('text="Tilføj køretøj"')).toBeVisible();
});
@@ -39,69 +39,69 @@ test('[PAGES][User][/user/vehicles] should display the add vehicle button', asyn
// Pagination tests
// Vehicle details page tests
//...
-test('DEBUG: inspect vehicles page buttons', async ({ page }) => {
+test("DEBUG: inspect vehicles page buttons", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/vehicles');
- await expect(page.locator('.title')).toContainText('Oversigt over registrerede køretøjer');
+ await page.goto("/user/vehicles");
+ await expect(page.locator(".title")).toContainText("Oversigt over registrerede køretøjer");
const buttons = page.locator('button, [role="button"]');
const count = await buttons.count();
- console.log('Total buttons: ' + count);
+ console.log("Total buttons: " + count);
for (let i = 0; i < Math.min(count, 10); i++) {
const btn = buttons.nth(i);
- const text = await btn.textContent() || '';
+ const text = (await btn.textContent()) || "";
const visible = await btn.isVisible();
- console.log('Button ' + i + ': visible=' + visible + ', text="' + text.trim() + '"');
+ console.log("Button " + i + ": visible=" + visible + ', text="' + text.trim() + '"');
}
const addBtn = page.locator('text="Tilføj køretøj"');
const addCount = await addBtn.count();
- console.log('Add button count: ' + addCount);
- const table = page.locator('table');
+ console.log("Add button count: " + addCount);
+ const table = page.locator("table");
const tableCount = await table.count();
- console.log('Table count: ' + tableCount);
+ console.log("Table count: " + tableCount);
});
-test('[DEBUG] click add vehicle and inspect', async ({ page }) => {
+test("[DEBUG] click add vehicle and inspect", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/vehicles');
+ await page.goto("/user/vehicles");
const addBtn = page.locator('text="Tilføj køretøj"');
await expect(addBtn).toBeVisible();
await addBtn.click();
await page.waitForTimeout(3000);
- console.log('Current URL: ' + page.url());
- const inputs = page.locator('input');
+ console.log("Current URL: " + page.url());
+ const inputs = page.locator("input");
const inputCount = await inputs.count();
- console.log('Input count after click: ' + inputCount);
+ console.log("Input count after click: " + inputCount);
for (let i = 0; i < inputCount; i++) {
const inp = inputs.nth(i);
- const ph = await inp.getAttribute('placeholder') || '';
- console.log('Input ' + i + ': placeholder="' + ph + '"');
+ const ph = (await inp.getAttribute("placeholder")) || "";
+ console.log("Input " + i + ': placeholder="' + ph + '"');
}
const modals = page.locator('.modal, .is-active.modal, [class*="modal"]');
const modalCount = await modals.count();
- console.log('Modal count: ' + modalCount);
+ console.log("Modal count: " + modalCount);
if (modalCount > 0) {
- const modalContent = await modals.first().textContent() || '';
- console.log('Modal preview: ' + modalContent.substring(0, 500));
- const modalClass = await modals.first().getAttribute('class') || '';
- console.log('Modal class: ' + modalClass);
+ const modalContent = (await modals.first().textContent()) || "";
+ console.log("Modal preview: " + modalContent.substring(0, 500));
+ const modalClass = (await modals.first().getAttribute("class")) || "";
+ console.log("Modal class: " + modalClass);
}
});
-test('[User Vehicles] should open add vehicle modal', async ({ page }) => {
+test("[User Vehicles] should open add vehicle modal", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/vehicles');
- await page.locator('text=\"Tilføj køretøj\"').click();
- await expect(page.locator('.modal.is-active')).toBeVisible();
- await expect(page.locator('text=\"Opret køretøj\"')).toBeVisible();
+ await page.goto("/user/vehicles");
+ await page.locator('text="Tilføj køretøj"').click();
+ await expect(page.locator(".modal.is-active")).toBeVisible();
+ await expect(page.locator('text="Opret køretøj"')).toBeVisible();
});
-test('[User Vehicles] add new vehicle - valid', async ({ page }) => {
+test("[User Vehicles] add new vehicle - valid", async ({ page }) => {
await loginAsUser(page);
- await page.goto('/user/vehicles');
+ await page.goto("/user/vehicles");
const regNr = vehicleTestData.validRegNr;
- await page.locator('text=\"Tilføj køretøj\"').click();
- await expect(page.locator('.modal.is-active')).toBeVisible();
- await page.getByLabel('Type').selectOption('Trækker');
- await page.getByLabel('Reg. 1.').fill(regNr);
- await page.getByLabel('Vaskeabonnement').check();
- await page.locator('.modal.is-active >> text=\"Opret køretøj\"').click();
- await expect(page.locator('.modal.is-active')).not.toBeVisible({ timeout: 5000 });
- await expect(page.locator('text=' + regNr)).toBeVisible();
-});
\ No newline at end of file
+ await page.locator('text="Tilføj køretøj"').click();
+ await expect(page.locator(".modal.is-active")).toBeVisible();
+ await page.getByLabel("Type").selectOption("Trækker");
+ await page.getByLabel("Reg. 1.").fill(regNr);
+ await page.getByLabel("Vaskeabonnement").check();
+ await page.locator('.modal.is-active >> text="Opret køretøj"').click();
+ await expect(page.locator(".modal.is-active")).not.toBeVisible({ timeout: 5000 });
+ await expect(page.locator("text=" + regNr)).toBeVisible();
+});
diff --git a/tests/e2e/workfeed-config.smoke.spec.js b/tests/e2e/workfeed-config.smoke.spec.js
index 9767566e..26570c8e 100644
--- a/tests/e2e/workfeed-config.smoke.spec.js
+++ b/tests/e2e/workfeed-config.smoke.spec.js
@@ -26,12 +26,7 @@ async function runDiagnostic(page, testId, inputValue = "") {
await expect(page.locator(".swal2-popup pre")).toBeVisible();
}
-async function runShiftDiagnostic(page, {
- startFrom,
- startTo,
- employeeID = "",
- released = "",
-}) {
+async function runShiftDiagnostic(page, { startFrom, startTo, employeeID = "", released = "" }) {
await page.locator('[data-testid="workfeed-diagnostics-list-shifts"]:visible').first().click();
await expect(page.locator(".swal2-popup")).toBeVisible();
await page.locator("#workfeed-start-from").fill(startFrom);
diff --git a/tests/unit/action-settings-wheel-button.spec.js b/tests/unit/action-settings-wheel-button.spec.js
index eeb2fc2c..97bee9d7 100644
--- a/tests/unit/action-settings-wheel-button.spec.js
+++ b/tests/unit/action-settings-wheel-button.spec.js
@@ -5,9 +5,7 @@ import { createI18n } from "vue-i18n";
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
-const getUserIdMock = vi.hoisted(() =>
- vi.fn(() => Promise.resolve({ data: { data: { user_id: 777 } } }))
-);
+const getUserIdMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ data: { data: { user_id: 777 } } })));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
@@ -216,7 +214,10 @@ describe("ActionSettingsWheelButton", () => {
await flushMicrotasks();
expect(SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm).toHaveBeenCalledTimes(1);
- expect(SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm).toHaveBeenCalledWith(42, expect.any(Function));
+ expect(SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm).toHaveBeenCalledWith(
+ 42,
+ expect.any(Function)
+ );
expect(windowOpenSpy).not.toHaveBeenCalled();
windowOpenSpy.mockRestore();
diff --git a/tests/unit/authenticated-request.spec.js b/tests/unit/authenticated-request.spec.js
index 6b291656..3b5f380b 100644
--- a/tests/unit/authenticated-request.spec.js
+++ b/tests/unit/authenticated-request.spec.js
@@ -1,19 +1,19 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from "vitest";
const { axiosMock } = vi.hoisted(() => ({
- axiosMock: vi.fn()
+ axiosMock: vi.fn(),
}));
-vi.mock('axios', () => ({
- default: axiosMock
+vi.mock("axios", () => ({
+ default: axiosMock,
}));
-import { authenticatedRequest } from '@/components/session/authenticatedRequest.vue';
+import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import {
__configureRequestQueueForTests,
__resetRequestQueueForTests,
- requestQueueState
-} from '@/services/requestQueue.js';
+ requestQueueState,
+} from "@/services/requestQueue.js";
const flushMicrotasks = async () => {
await Promise.resolve();
@@ -37,7 +37,7 @@ const createDeferred = () => {
return { promise, resolve, reject };
};
-describe('authenticatedRequest', () => {
+describe("authenticatedRequest", () => {
beforeEach(() => {
axiosMock.mockReset();
__resetRequestQueueForTests();
@@ -45,78 +45,76 @@ describe('authenticatedRequest', () => {
maxConcurrentGet: 1,
maxConcurrentOther: 1,
spacingMs: 0,
- retryByStatusCode: {}
+ retryByStatusCode: {},
});
localStorage.clear();
- localStorage.setItem('token', 'token');
+ localStorage.setItem("token", "token");
});
- it('suppresses known scanner lpr extraction failures and resolves with response', async () => {
+ it("suppresses known scanner lpr extraction failures and resolves with response", async () => {
const response = {
status: 500,
data: {
success: false,
data: {
- message: 'Internal server error: License plate extraction failed.'
+ message: "Internal server error: License plate extraction failed.",
},
meta: [],
- includes: []
- }
+ includes: [],
+ },
};
axiosMock.mockRejectedValueOnce({ response });
const catchCallable = vi.fn();
- const result = await authenticatedRequest('/modules/scanner/lpr', 'POST', { base64_image: 'x' }, catchCallable);
+ const result = await authenticatedRequest("/modules/scanner/lpr", "POST", { base64_image: "x" }, catchCallable);
expect(result).toBe(response);
expect(catchCallable).not.toHaveBeenCalled();
});
- it('continues to reject non-suppressed scanner errors', async () => {
+ it("continues to reject non-suppressed scanner errors", async () => {
const error = {
response: {
status: 500,
data: {
success: false,
data: {
- message: 'Internal server error: Different scanner problem.'
- }
- }
- }
+ message: "Internal server error: Different scanner problem.",
+ },
+ },
+ },
};
axiosMock.mockRejectedValueOnce(error);
const catchCallable = vi.fn();
await expect(
- authenticatedRequest('/modules/scanner/lpr', 'POST', { base64_image: 'x' }, catchCallable)
+ authenticatedRequest("/modules/scanner/lpr", "POST", { base64_image: "x" }, catchCallable)
).rejects.toBe(error);
expect(catchCallable).toHaveBeenCalledWith(error);
});
- it('continues to reject non-lpr errors', async () => {
+ it("continues to reject non-lpr errors", async () => {
const error = {
response: {
status: 500,
data: {
success: false,
data: {
- message: 'Internal server error: License plate extraction failed.'
- }
- }
- }
+ message: "Internal server error: License plate extraction failed.",
+ },
+ },
+ },
};
axiosMock.mockRejectedValueOnce(error);
- await expect(
- authenticatedRequest('/auth/session', 'GET')
- ).rejects.toBe(error);
+ await expect(authenticatedRequest("/auth/session", "GET")).rejects.toBe(error);
});
- it('queues requests and executes them sequentially', async () => {
+ it("queues requests and executes them sequentially", async () => {
const first = createDeferred();
const second = createDeferred();
const third = createDeferred();
@@ -124,9 +122,9 @@ describe('authenticatedRequest', () => {
axiosMock.mockImplementation(() => deferredCalls.shift()?.promise);
- const request1 = authenticatedRequest('/one', 'GET');
- const request2 = authenticatedRequest('/two', 'GET');
- const request3 = authenticatedRequest('/three', 'GET');
+ const request1 = authenticatedRequest("/one", "GET");
+ const request2 = authenticatedRequest("/two", "GET");
+ const request3 = authenticatedRequest("/three", "GET");
await flushMicrotasks();
@@ -153,7 +151,7 @@ describe('authenticatedRequest', () => {
await expect(Promise.all([request1, request2, request3])).resolves.toEqual([
{ status: 200, data: { id: 1 } },
{ status: 200, data: { id: 2 } },
- { status: 200, data: { id: 3 } }
+ { status: 200, data: { id: 3 } },
]);
await flushMicrotasks();
diff --git a/tests/unit/axios-request-queue.spec.js b/tests/unit/axios-request-queue.spec.js
index c64d878c..2824568d 100644
--- a/tests/unit/axios-request-queue.spec.js
+++ b/tests/unit/axios-request-queue.spec.js
@@ -3,11 +3,11 @@ import { beforeEach, afterEach, describe, expect, it } from "vitest";
import {
__configureRequestQueueForTests,
__resetRequestQueueForTests,
- requestQueueState
+ requestQueueState,
} from "@/services/requestQueue.js";
import {
__resetAxiosRequestQueueInstallerForTests,
- installAxiosRequestQueue
+ installAxiosRequestQueue,
} from "@/services/installAxiosRequestQueue.js";
const flushMicrotasks = async () => {
@@ -37,7 +37,7 @@ const createResponse = (data) => ({
statusText: "OK",
headers: {},
config: {},
- data
+ data,
});
describe("axios request queue interceptor", () => {
@@ -61,13 +61,13 @@ describe("axios request queue interceptor", () => {
const requestOne = axios({
url: "/queue-test-1",
method: "GET",
- adapter: () => adapters.shift()?.promise
+ adapter: () => adapters.shift()?.promise,
});
const requestTwo = axios({
url: "/queue-test-2",
method: "GET",
- adapter: () => adapters.shift()?.promise
+ adapter: () => adapters.shift()?.promise,
});
await flushMicrotasks();
@@ -107,7 +107,7 @@ describe("axios request queue interceptor", () => {
axios({
url: `/queue-get-${index + 1}`,
method: "GET",
- adapter
+ adapter,
})
);
@@ -139,7 +139,7 @@ describe("axios request queue interceptor", () => {
retryByStatusCode: { 429: 2 },
retryDelayBaseMs: 0,
retryDelayMaxMs: 0,
- retryDelayJitterMs: 0
+ retryDelayJitterMs: 0,
});
let attempts = 0;
@@ -152,7 +152,7 @@ describe("axios request queue interceptor", () => {
throw { response: { status: 429 } };
}
return createResponse({ ok: true, attempts });
- }
+ },
});
const response = await request;
diff --git a/tests/unit/collected-order-invoice-overview.spec.js b/tests/unit/collected-order-invoice-overview.spec.js
index 5e6a2132..197c0ed8 100644
--- a/tests/unit/collected-order-invoice-overview.spec.js
+++ b/tests/unit/collected-order-invoice-overview.spec.js
@@ -1,13 +1,13 @@
// @vitest-environment jsdom
-import { mount } from '@vue/test-utils';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { nextTick } from 'vue';
+import { mount } from "@vue/test-utils";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { nextTick } from "vue";
const mocks = vi.hoisted(() => ({
showEditObjectFieldForm: vi.fn(),
}));
-vi.mock('@/components/session/token/SessionUser.vue', () => ({
+vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
objects: {
collectedOrderInvoices: {
@@ -15,7 +15,7 @@ vi.mock('@/components/session/token/SessionUser.vue', () => ({
},
global: {
language: {
- no_data: 'Ingen data',
+ no_data: "Ingen data",
},
},
},
@@ -27,9 +27,9 @@ vi.mock('@/components/session/token/SessionUser.vue', () => ({
},
}));
-import CollectedOrderInvoiceOverview from '@/views/dashboards/superUserDashboard/collectedOrderInvoice/displays/collectedOrderInvoiceOverview.vue';
+import CollectedOrderInvoiceOverview from "@/views/dashboards/superUserDashboard/collectedOrderInvoice/displays/collectedOrderInvoiceOverview.vue";
-describe('CollectedOrderInvoiceOverview', () => {
+describe("CollectedOrderInvoiceOverview", () => {
beforeEach(() => {
mocks.showEditObjectFieldForm.mockReset();
});
@@ -38,7 +38,7 @@ describe('CollectedOrderInvoiceOverview', () => {
vi.restoreAllMocks();
});
- it('triggers closed_at edit flow and emits updated', async () => {
+ it("triggers closed_at edit flow and emits updated", async () => {
mocks.showEditObjectFieldForm.mockImplementation(async (id, column, value, onAfterSubmit) => {
onAfterSubmit?.();
return {};
@@ -51,25 +51,25 @@ describe('CollectedOrderInvoiceOverview', () => {
collectedOrderInvoice: {
id: 14560,
total_net_amount: 9894,
- closed_at: '2026-04-08 11:43:01',
- external_id: 'X-100',
+ closed_at: "2026-04-08 11:43:01",
+ external_id: "X-100",
po_number: null,
},
},
});
- await wrapper.get('[data-test="edit-closed-at-button"]').trigger('click');
+ await wrapper.get('[data-test="edit-closed-at-button"]').trigger("click");
await nextTick();
expect(mocks.showEditObjectFieldForm).toHaveBeenCalledTimes(1);
expect(mocks.showEditObjectFieldForm).toHaveBeenCalledWith(
14560,
- 'closed_at',
- '2026-04-08 11:43:01',
+ "closed_at",
+ "2026-04-08 11:43:01",
expect.any(Function)
);
- expect(wrapper.emitted('updated')).toBeTruthy();
- expect(wrapper.emitted('updated').length).toBe(1);
+ expect(wrapper.emitted("updated")).toBeTruthy();
+ expect(wrapper.emitted("updated").length).toBe(1);
});
});
diff --git a/tests/unit/collected-order-invoice-queue-history.spec.js b/tests/unit/collected-order-invoice-queue-history.spec.js
index f2771799..ddfee852 100644
--- a/tests/unit/collected-order-invoice-queue-history.spec.js
+++ b/tests/unit/collected-order-invoice-queue-history.spec.js
@@ -4,11 +4,8 @@ import { describe, expect, it } from "vitest";
const root = process.cwd();
const source = readFileSync(
- join(
- root,
- "src/views/dashboards/superUserDashboard/collectedOrderInvoices/CollectedOrderInvoicesQueueHistory.vue",
- ),
- "utf8",
+ join(root, "src/views/dashboards/superUserDashboard/collectedOrderInvoices/CollectedOrderInvoicesQueueHistory.vue"),
+ "utf8"
);
describe("collected invoice queue history reliability contract", () => {
@@ -61,13 +58,15 @@ describe("collected invoice queue history reliability contract", () => {
expect(source).toContain("manualRunUnavailable.value = true;");
expect(source).toContain("const onRunQueueNow = async () => {");
expect(source).toContain("await SessionUser.objects.collectedOrderInvoices.functions.economic.queue.run();");
- expect(source).toContain("await SessionUser.objects.collectedOrderInvoices.functions.economic.queue.status(queuedJob.id);");
+ expect(source).toContain(
+ "await SessionUser.objects.collectedOrderInvoices.functions.economic.queue.status(queuedJob.id);"
+ );
expect(source).toContain("const run = await runQueueBatchWithFallback();");
expect(source).toContain("runSummary.value = run;");
expect(source).toContain("Legacy fallback active while the backend queue runner is unavailable.");
- expect(source).toContain("data-testid=\"economic-queue-history-run-now\"");
- expect(source).toContain("data-testid=\"economic-queue-history-run-summary\"");
- expect(source).toContain(":disabled=\"loading || refreshing || runningQueue\"");
+ expect(source).toContain('data-testid="economic-queue-history-run-now"');
+ expect(source).toContain('data-testid="economic-queue-history-run-summary"');
+ expect(source).toContain(':disabled="loading || refreshing || runningQueue"');
});
it("retains server details_summary, builds local fallback summaries, and renders details modal test ids", () => {
@@ -75,13 +74,13 @@ describe("collected invoice queue history reliability contract", () => {
expect(source).toContain("const buildLocalFallbackSummary = (rawJob) => {");
expect(source).toContain("const normalizeDetailsSummary = (rawSummary, rawJob) => {");
expect(source).toContain("Se detaljer");
- expect(source).toContain("data-testid=\"economic-queue-history-details-modal\"");
- expect(source).toContain("data-testid=\"economic-queue-history-details-close\"");
- expect(source).toContain("data-testid=\"economic-queue-history-details-section-udfald\"");
- expect(source).toContain("data-testid=\"economic-queue-history-details-section-faktura\"");
- expect(source).toContain("data-testid=\"economic-queue-history-details-section-overfoersel\"");
- expect(source).toContain("data-testid=\"economic-queue-history-details-section-koersel\"");
- expect(source).toContain("data-testid=\"economic-queue-history-details-section-teknisk\"");
- expect(source).toContain("data-testid=\"economic-queue-history-details-technical-raw\"");
+ expect(source).toContain('data-testid="economic-queue-history-details-modal"');
+ expect(source).toContain('data-testid="economic-queue-history-details-close"');
+ expect(source).toContain('data-testid="economic-queue-history-details-section-udfald"');
+ expect(source).toContain('data-testid="economic-queue-history-details-section-faktura"');
+ expect(source).toContain('data-testid="economic-queue-history-details-section-overfoersel"');
+ expect(source).toContain('data-testid="economic-queue-history-details-section-koersel"');
+ expect(source).toContain('data-testid="economic-queue-history-details-section-teknisk"');
+ expect(source).toContain('data-testid="economic-queue-history-details-technical-raw"');
});
});
diff --git a/tests/unit/collected-order-invoice-stripe-queue.spec.js b/tests/unit/collected-order-invoice-stripe-queue.spec.js
index c40a7a2d..3a1f27dc 100644
--- a/tests/unit/collected-order-invoice-stripe-queue.spec.js
+++ b/tests/unit/collected-order-invoice-stripe-queue.spec.js
@@ -14,9 +14,9 @@ const stripeManageSource = readFileSync(
describe("collected invoice stripe queue wiring", () => {
it("uses shared queue composable with collected-invoice queue endpoints", () => {
expect(stripeManageSource).toContain("useEconomicQueueJob");
- expect(stripeManageSource).toContain("enqueueEndpoint: \"/collected-invoices/stripe/book\"");
- expect(stripeManageSource).toContain("statusEndpoint: \"/collected-invoices/economic/queue/status\"");
- expect(stripeManageSource).toContain("retryEndpoint: \"/collected-invoices/economic/queue/retry\"");
+ expect(stripeManageSource).toContain('enqueueEndpoint: "/collected-invoices/stripe/book"');
+ expect(stripeManageSource).toContain('statusEndpoint: "/collected-invoices/economic/queue/status"');
+ expect(stripeManageSource).toContain('retryEndpoint: "/collected-invoices/economic/queue/retry"');
expect(stripeManageSource).toContain("await stripeTransferQueue.enqueue({});");
expect(stripeManageSource).toContain("await stripeTransferQueue.retry();");
});
@@ -24,8 +24,8 @@ describe("collected invoice stripe queue wiring", () => {
it("renders progress and retry states for queued transfers", () => {
expect(stripeManageSource).toContain("isStripeTransferQueuedOrProcessing");
expect(stripeManageSource).toContain("isStripeTransferFailed");
- expect(stripeManageSource).toContain("data-testid=\"collected-stripe-progress\"");
- expect(stripeManageSource).toContain("data-testid=\"collected-stripe-retry\"");
- expect(stripeManageSource).toContain("data-testid=\"collected-stripe-book-invoice\"");
+ expect(stripeManageSource).toContain('data-testid="collected-stripe-progress"');
+ expect(stripeManageSource).toContain('data-testid="collected-stripe-retry"');
+ expect(stripeManageSource).toContain('data-testid="collected-stripe-book-invoice"');
});
});
diff --git a/tests/unit/collected-order-invoices-closed-at-modal.spec.js b/tests/unit/collected-order-invoices-closed-at-modal.spec.js
index 0c0f4be9..8916d537 100644
--- a/tests/unit/collected-order-invoices-closed-at-modal.spec.js
+++ b/tests/unit/collected-order-invoices-closed-at-modal.spec.js
@@ -1,5 +1,5 @@
// @vitest-environment jsdom
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
swalFire: vi.fn(),
@@ -10,7 +10,7 @@ const mocks = vi.hoisted(() => ({
authenticatedRequest: vi.fn(),
}));
-vi.mock('sweetalert2', () => ({
+vi.mock("sweetalert2", () => ({
default: {
fire: mocks.swalFire,
showValidationMessage: mocks.swalShowValidationMessage,
@@ -18,11 +18,11 @@ vi.mock('sweetalert2', () => ({
},
}));
-vi.mock('@/components/session/authenticatedRequest.vue', () => ({
+vi.mock("@/components/session/authenticatedRequest.vue", () => ({
authenticatedRequest: mocks.authenticatedRequest,
}));
-vi.mock('@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue', () => ({
+vi.mock("@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue", () => ({
ObjectsGlobal: {
set: {
column: mocks.setColumn,
@@ -34,32 +34,32 @@ vi.mock('@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue', () =
get: { objects: vi.fn(), object: vi.fn() },
delete: { object: vi.fn() },
language: {
- field: () => 'Rediger arkiveret',
- save: 'Gem',
- cancel: 'Annuller',
- clear: 'Ryd',
+ field: () => "Rediger arkiveret",
+ save: "Gem",
+ cancel: "Annuller",
+ clear: "Ryd",
},
},
}));
-vi.mock('@/components/session/token/SessionUser.vue', () => ({
+vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
functions: {
- parseErrorMessage: vi.fn(() => 'error'),
+ parseErrorMessage: vi.fn(() => "error"),
redirectTo: { superUser: vi.fn() },
currency: { toLocal: (value) => String(value) },
},
objects: {
global: {
language: {
- no_data: 'Ingen data',
+ no_data: "Ingen data",
},
},
},
},
}));
-vi.mock('@/i18n', () => ({
+vi.mock("@/i18n", () => ({
default: {
global: {
t: (key) => key,
@@ -67,25 +67,25 @@ vi.mock('@/i18n', () => ({
},
}));
-vi.mock('@/services/economicTransferQueue.js', () => ({
+vi.mock("@/services/economicTransferQueue.js", () => ({
buildCollectedInvoiceEconomicPayload: vi.fn(() => ({})),
enqueueEconomicTransferJob: vi.fn(),
fetchEconomicTransferJobStatus: vi.fn(),
retryEconomicTransferJob: vi.fn(),
}));
-vi.mock('@/components/displays/modals/PickCustomerInvoiceCollectionModal.vue', () => ({
+vi.mock("@/components/displays/modals/PickCustomerInvoiceCollectionModal.vue", () => ({
default: {
- name: 'PickCustomerInvoiceCollectionModalStub',
- template: '',
+ name: "PickCustomerInvoiceCollectionModalStub",
+ template: "",
},
}));
-import { CollectedOrderInvoices } from '@/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue';
+import { CollectedOrderInvoices } from "@/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue";
-describe('CollectedOrderInvoices closed_at edit modal', () => {
+describe("CollectedOrderInvoices closed_at edit modal", () => {
beforeEach(() => {
- document.body.innerHTML = '';
+ document.body.innerHTML = "";
mocks.swalFire.mockReset();
mocks.swalShowValidationMessage.mockReset();
mocks.swalClose.mockReset();
@@ -94,20 +94,20 @@ describe('CollectedOrderInvoices closed_at edit modal', () => {
mocks.authenticatedRequest.mockReset();
});
- it('opens dedicated closed_at modal, normalizes date value, and saves API date', async () => {
+ it("opens dedicated closed_at modal, normalizes date value, and saves API date", async () => {
const onAfterSubmit = vi.fn();
mocks.setColumn.mockResolvedValue({});
mocks.swalFire.mockImplementation(async (options) => {
document.body.innerHTML = options.html;
options.didOpen?.();
- const input = document.getElementById('collected-order-invoice-closed-at-input');
- input.value = '2026-04-08';
+ const input = document.getElementById("collected-order-invoice-closed-at-input");
+ input.value = "2026-04-08";
await options.preConfirm();
return { isConfirmed: true };
});
- await CollectedOrderInvoices.showEditObjectFieldForm(14560, 'closed_at', '2026-04-08 11:43:01', onAfterSubmit);
+ await CollectedOrderInvoices.showEditObjectFieldForm(14560, "closed_at", "2026-04-08 11:43:01", onAfterSubmit);
expect(mocks.swalFire).toHaveBeenCalledTimes(1);
const modalOptions = mocks.swalFire.mock.calls[0][0];
@@ -116,27 +116,27 @@ describe('CollectedOrderInvoices closed_at edit modal', () => {
expect(mocks.setColumn).toHaveBeenCalledTimes(1);
const setColumnArgs = mocks.setColumn.mock.calls[0];
- expect(setColumnArgs[0]).toBe('/collected-invoices');
+ expect(setColumnArgs[0]).toBe("/collected-invoices");
expect(setColumnArgs[1]).toBe(14560);
- expect(setColumnArgs[2]).toBe('closed_at');
- expect(setColumnArgs[3]).toBe('2026-04-08');
+ expect(setColumnArgs[2]).toBe("closed_at");
+ expect(setColumnArgs[3]).toBe("2026-04-08");
expect(onAfterSubmit).toHaveBeenCalledTimes(1);
});
- it('supports clearing closed_at and saves null', async () => {
+ it("supports clearing closed_at and saves null", async () => {
const onAfterSubmit = vi.fn();
mocks.setColumn.mockResolvedValue({});
mocks.swalFire.mockImplementation(async (options) => {
document.body.innerHTML = options.html;
options.didOpen?.();
- const clearButton = document.getElementById('collected-order-invoice-closed-at-clear-button');
+ const clearButton = document.getElementById("collected-order-invoice-closed-at-clear-button");
clearButton.click();
await options.preConfirm();
return { isConfirmed: true };
});
- await CollectedOrderInvoices.showEditObjectFieldForm(14561, 'closed_at', '2026-04-08 11:43:01', onAfterSubmit);
+ await CollectedOrderInvoices.showEditObjectFieldForm(14561, "closed_at", "2026-04-08 11:43:01", onAfterSubmit);
expect(mocks.setColumn).toHaveBeenCalledTimes(1);
expect(mocks.setColumn.mock.calls[0][3]).toBeNull();
@@ -144,18 +144,18 @@ describe('CollectedOrderInvoices closed_at edit modal', () => {
expect(mocks.swalShowValidationMessage).not.toHaveBeenCalled();
});
- it('falls back to generic object editor for non-closed_at columns', async () => {
+ it("falls back to generic object editor for non-closed_at columns", async () => {
const onAfterSubmit = vi.fn();
mocks.showEditObjectFieldForm.mockResolvedValue({});
- await CollectedOrderInvoices.showEditObjectFieldForm(11, 'name', 'Invoice name', onAfterSubmit);
+ await CollectedOrderInvoices.showEditObjectFieldForm(11, "name", "Invoice name", onAfterSubmit);
expect(mocks.showEditObjectFieldForm).toHaveBeenCalledTimes(1);
expect(mocks.showEditObjectFieldForm).toHaveBeenCalledWith(
CollectedOrderInvoices,
11,
- 'name',
- 'Invoice name',
+ "name",
+ "Invoice name",
onAfterSubmit
);
expect(mocks.swalFire).not.toHaveBeenCalled();
diff --git a/tests/unit/department-daily-report-complaints.spec.js b/tests/unit/department-daily-report-complaints.spec.js
index 02f5d811..4ef2a157 100644
--- a/tests/unit/department-daily-report-complaints.spec.js
+++ b/tests/unit/department-daily-report-complaints.spec.js
@@ -16,7 +16,8 @@ const sharedState = vi.hoisted(() => ({
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
- hasPermission: (permission) => permission === "create_department_daily_report_complaints" && permissionState.canCreate,
+ hasPermission: (permission) =>
+ permission === "create_department_daily_report_complaints" && permissionState.canCreate,
objects: {
department_daily_reports: {
functions: {
@@ -55,11 +56,10 @@ vi.mock("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentD
vi.mock("@/services/departmentDailyReportComplaintCustomers.js", () => ({
COMPLAINT_CUSTOMER_SEARCH_DEBOUNCE_MS: 250,
COMPLAINT_CUSTOMER_SEARCH_MIN_LENGTH: 2,
- formatComplaintCustomerLabel: (customer) => (
+ formatComplaintCustomerLabel: (customer) =>
customer?.customer_name
? `${customer.customer_name} - ${customer.customer_number}`
- : String(customer?.customer_number ?? "")
- ),
+ : String(customer?.customer_number ?? ""),
normalizeComplaintCustomer: (customer) => customer,
searchComplaintCustomers: searchComplaintCustomersMock,
}));
@@ -73,16 +73,17 @@ vi.mock("@/services/departmentDailyReportComplaintCategories.js", () => ({
import DepartmentDashboardDailyReportComplaints from "@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportComplaints.vue";
-const mountTile = (props = {}) => mountWithApp(DepartmentDashboardDailyReportComplaints, {
- props: {
- count: 3,
- isLoading: false,
- state: "ready",
- unavailableMessage: null,
- dataTestid: "daily-report-tile-complaints",
- ...props,
- },
-});
+const mountTile = (props = {}) =>
+ mountWithApp(DepartmentDashboardDailyReportComplaints, {
+ props: {
+ count: 3,
+ isLoading: false,
+ state: "ready",
+ unavailableMessage: null,
+ dataTestid: "daily-report-tile-complaints",
+ ...props,
+ },
+ });
const formatDisplayedDate = (isoDate) => {
const [year, month, day] = String(isoDate || "").split("-");
@@ -202,7 +203,9 @@ describe("DepartmentDashboardDailyReportComplaints", () => {
await wrapper.get('[data-testid="daily-report-complaints-add-button"]').trigger("click");
await flushPromises();
- expect(wrapper.get('[data-testid="daily-report-complaints-wash-date-input"]').element.value).toBe(formatDisplayedDate(today));
+ expect(wrapper.get('[data-testid="daily-report-complaints-wash-date-input"]').element.value).toBe(
+ formatDisplayedDate(today)
+ );
await wrapper.get('[data-testid="daily-report-complaints-category-select"]').setValue("service");
await wrapper.get('[data-testid="daily-report-complaints-description-textarea"]').setValue("Machine damaged cargo");
await wrapper.get('[data-testid="daily-report-complaints-submit"]').trigger("click");
@@ -221,9 +224,7 @@ describe("DepartmentDashboardDailyReportComplaints", () => {
it("searches and selects an optional customer before submitting", async () => {
createComplaintMock.mockResolvedValue({ data: { data: { id: 2 } } });
- searchComplaintCustomersMock.mockResolvedValue([
- { customer_number: 12345, customer_name: "Acme Transport" },
- ]);
+ searchComplaintCustomersMock.mockResolvedValue([{ customer_number: 12345, customer_name: "Acme Transport" }]);
const wrapper = mountTile();
@@ -239,11 +240,15 @@ describe("DepartmentDashboardDailyReportComplaints", () => {
await wrapper.get('[data-testid="daily-report-complaints-customer-option-12345"]').trigger("mousedown");
await flushPromises();
- expect(wrapper.get('[data-testid="daily-report-complaints-customer-selected"]').element.value).toBe("Acme Transport - 12345");
+ expect(wrapper.get('[data-testid="daily-report-complaints-customer-selected"]').element.value).toBe(
+ "Acme Transport - 12345"
+ );
expect(wrapper.get('[data-testid="daily-report-complaints-wash-date-input"]').element.value).toBe("23/03/2026");
await wrapper.get('[data-testid="daily-report-complaints-category-select"]').setValue("wash_quality");
- await wrapper.get('[data-testid="daily-report-complaints-description-textarea"]').setValue("Complaint with customer number");
+ await wrapper
+ .get('[data-testid="daily-report-complaints-description-textarea"]')
+ .setValue("Complaint with customer number");
await wrapper.get('[data-testid="daily-report-complaints-submit"]').trigger("click");
await flushPromises();
@@ -267,7 +272,9 @@ describe("DepartmentDashboardDailyReportComplaints", () => {
await vi.advanceTimersByTimeAsync(250);
await flushPromises();
await wrapper.get('[data-testid="daily-report-complaints-category-select"]').setValue("service");
- await wrapper.get('[data-testid="daily-report-complaints-description-textarea"]').setValue("Complaint with unresolved customer");
+ await wrapper
+ .get('[data-testid="daily-report-complaints-description-textarea"]')
+ .setValue("Complaint with unresolved customer");
await wrapper.get('[data-testid="daily-report-complaints-submit"]').trigger("click");
await flushPromises();
diff --git a/tests/unit/department-daily-report-object.behavior.spec.js b/tests/unit/department-daily-report-object.behavior.spec.js
index e509b2d5..71c558f8 100644
--- a/tests/unit/department-daily-report-object.behavior.spec.js
+++ b/tests/unit/department-daily-report-object.behavior.spec.js
@@ -70,7 +70,12 @@ const makeOverview = ({
has_missing_opening_hours: nightWashesMissingDepartmentIds.length > 0,
missing_department_ids: nightWashesMissingDepartmentIds,
},
- overtime: { state: overtimeState, value: overtime, out_of: null, message: overtimeState === "ready" ? null : "Overarbejde kræver Workfeed-kobling." },
+ overtime: {
+ state: overtimeState,
+ value: overtime,
+ out_of: null,
+ message: overtimeState === "ready" ? null : "Overarbejde kræver Workfeed-kobling.",
+ },
},
products: [
{
@@ -95,11 +100,11 @@ describe("DepartmentDailyReportObject behavior", () => {
it("fetches once per filter change and lets the latest overview response win", async () => {
const firstRequest = deferred();
const secondRequest = deferred();
- getOverviewMock
- .mockReturnValueOnce(firstRequest.promise)
- .mockReturnValueOnce(secondRequest.promise);
+ getOverviewMock.mockReturnValueOnce(firstRequest.promise).mockReturnValueOnce(secondRequest.promise);
- const module = await import("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue");
+ const module = await import(
+ "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue"
+ );
module.initializeDailyReportFilters({
department_ids: [1],
@@ -117,34 +122,38 @@ describe("DepartmentDailyReportObject behavior", () => {
expect(getOverviewMock).toHaveBeenCalledTimes(2);
expect(getOverviewMock).toHaveBeenLastCalledWith([1], "2026-03-23", "2026-03-24");
- secondRequest.resolve(makeOverview({
- transactions: 9,
- productsSold: 18,
- revenue: 4200,
- washes: 11,
- waterUsage: 34,
- bookings: 5,
- bookingsOutOf: 7,
- complaints: 2,
- nightWashes: 2,
- nightWashesBySource: { orders: 1, xlvask: 0, selfserve: 1 },
- nightWashesMissingDepartmentIds: [7],
- overtime: 1.25,
- }));
+ secondRequest.resolve(
+ makeOverview({
+ transactions: 9,
+ productsSold: 18,
+ revenue: 4200,
+ washes: 11,
+ waterUsage: 34,
+ bookings: 5,
+ bookingsOutOf: 7,
+ complaints: 2,
+ nightWashes: 2,
+ nightWashesBySource: { orders: 1, xlvask: 0, selfserve: 1 },
+ nightWashesMissingDepartmentIds: [7],
+ overtime: 1.25,
+ })
+ );
await flushAll();
- firstRequest.resolve(makeOverview({
- transactions: 1,
- productsSold: 2,
- revenue: 300,
- washes: 1,
- waterUsage: 3,
- bookings: 1,
- bookingsOutOf: 1,
- nightWashes: 0,
- nightWashesBySource: { orders: 0, xlvask: 0, selfserve: 0 },
- overtime: 0.25,
- }));
+ firstRequest.resolve(
+ makeOverview({
+ transactions: 1,
+ productsSold: 2,
+ revenue: 300,
+ washes: 1,
+ waterUsage: 3,
+ bookings: 1,
+ bookingsOutOf: 1,
+ nightWashes: 0,
+ nightWashesBySource: { orders: 0, xlvask: 0, selfserve: 0 },
+ overtime: 0.25,
+ })
+ );
await flushAll();
expect(module.count_transactions.value).toBe(9);
@@ -163,24 +172,28 @@ describe("DepartmentDailyReportObject behavior", () => {
});
it("keeps unavailable complaint and overtime tiles out of fake zero state", async () => {
- getOverviewMock.mockResolvedValue(makeOverview({
- transactions: 4,
- productsSold: 12,
- revenue: 1800,
- washes: 6,
- waterUsage: 22,
- bookings: 3,
- bookingsOutOf: 4,
- complaintsState: "unavailable",
- complaintsMessage: "Kundeklager er ikke tilgængelig endnu.",
- nightWashesState: "ready",
- nightWashes: 1,
- nightWashesBySource: { orders: 0, xlvask: 1, selfserve: 0 },
- overtimeState: "unavailable",
- overtime: 0,
- }));
+ getOverviewMock.mockResolvedValue(
+ makeOverview({
+ transactions: 4,
+ productsSold: 12,
+ revenue: 1800,
+ washes: 6,
+ waterUsage: 22,
+ bookings: 3,
+ bookingsOutOf: 4,
+ complaintsState: "unavailable",
+ complaintsMessage: "Kundeklager er ikke tilgængelig endnu.",
+ nightWashesState: "ready",
+ nightWashes: 1,
+ nightWashesBySource: { orders: 0, xlvask: 1, selfserve: 0 },
+ overtimeState: "unavailable",
+ overtime: 0,
+ })
+ );
- const module = await import("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue");
+ const module = await import(
+ "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue"
+ );
module.initializeDailyReportFilters({
department_ids: [2],
@@ -206,12 +219,16 @@ describe("DepartmentDailyReportObject behavior", () => {
});
it("exposes an explicit overview refresh helper", async () => {
- getOverviewMock.mockResolvedValue(makeOverview({
- transactions: 7,
- complaints: 3,
- }));
+ getOverviewMock.mockResolvedValue(
+ makeOverview({
+ transactions: 7,
+ complaints: 3,
+ })
+ );
- const module = await import("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue");
+ const module = await import(
+ "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue"
+ );
module.initializeDailyReportFilters({
department_ids: [3],
diff --git a/tests/unit/department-gates-config-modal.spec.js b/tests/unit/department-gates-config-modal.spec.js
index 2fab04ab..7562fb00 100644
--- a/tests/unit/department-gates-config-modal.spec.js
+++ b/tests/unit/department-gates-config-modal.spec.js
@@ -1,5 +1,5 @@
// @vitest-environment jsdom
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
setColumn: vi.fn(),
@@ -8,14 +8,14 @@ const mocks = vi.hoisted(() => ({
swalShowValidationMessage: vi.fn(),
}));
-vi.mock('sweetalert2', () => ({
+vi.mock("sweetalert2", () => ({
default: {
fire: mocks.swalFire,
showValidationMessage: mocks.swalShowValidationMessage,
},
}));
-vi.mock('@/i18n', () => ({
+vi.mock("@/i18n", () => ({
default: {
global: {
t: (key) => key,
@@ -23,12 +23,12 @@ vi.mock('@/i18n', () => ({
},
}));
-vi.mock('@/components/session/token/SessionUser.vue', () => ({
+vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
objects: {
global: {
parse: {
- boolean: (value) => value === true || value === 'true',
+ boolean: (value) => value === true || value === "true",
},
},
departments: {
@@ -40,7 +40,7 @@ vi.mock('@/components/session/token/SessionUser.vue', () => ({
},
}));
-vi.mock('@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue', () => ({
+vi.mock("@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue", () => ({
ObjectsGlobal: {
set: {
column: mocks.setColumn,
@@ -52,19 +52,19 @@ vi.mock('@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue', () =
get: { objects: vi.fn(), object: vi.fn() },
delete: { object: vi.fn() },
language: {
- save: 'Save',
- cancel: 'Cancel',
- field: () => 'Edit config',
+ save: "Save",
+ cancel: "Cancel",
+ field: () => "Edit config",
},
},
}));
-import { DepartmentGates } from '@/components/session/token/SessionUser/Objects/DepartmentGates.vue';
+import { DepartmentGates } from "@/components/session/token/SessionUser/Objects/DepartmentGates.vue";
-describe('DepartmentGates config edit modal', () => {
+describe("DepartmentGates config edit modal", () => {
beforeEach(() => {
- document.body.innerHTML = '';
- Object.defineProperty(window, 'innerWidth', {
+ document.body.innerHTML = "";
+ Object.defineProperty(window, "innerWidth", {
value: 1440,
writable: true,
configurable: true,
@@ -75,16 +75,16 @@ describe('DepartmentGates config edit modal', () => {
mocks.swalShowValidationMessage.mockReset();
});
- it('uses dedicated config modal and saves typed config values', async () => {
+ it("uses dedicated config modal and saves typed config values", async () => {
const onAfterSubmit = vi.fn();
mocks.setColumn.mockResolvedValue({});
mocks.swalFire.mockResolvedValue({});
await DepartmentGates.showEditObjectFieldForm(
7,
- 'config',
+ "config",
{
- phone_number: '+4530878410',
+ phone_number: "+4530878410",
call_duration_threshold: 30,
},
onAfterSubmit
@@ -94,61 +94,57 @@ describe('DepartmentGates config edit modal', () => {
expect(mocks.swalFire).toHaveBeenCalledTimes(1);
const modalOptions = mocks.swalFire.mock.calls[0][0];
- expect(modalOptions.width).toBe('1100px');
- expect(modalOptions.html).toContain('data-config-header-key');
- expect(modalOptions.html).toContain('data-config-header-type');
- expect(modalOptions.html).toContain('data-config-header-value');
+ expect(modalOptions.width).toBe("1100px");
+ expect(modalOptions.html).toContain("data-config-header-key");
+ expect(modalOptions.html).toContain("data-config-header-type");
+ expect(modalOptions.html).toContain("data-config-header-value");
document.body.innerHTML = modalOptions.html;
modalOptions.didOpen();
- const rows = Array.from(document.querySelectorAll('[data-config-row]'));
- const thresholdRow = rows.find((row) => row.querySelector('[data-config-key]')?.value === 'call_duration_threshold');
+ const rows = Array.from(document.querySelectorAll("[data-config-row]"));
+ const thresholdRow = rows.find(
+ (row) => row.querySelector("[data-config-key]")?.value === "call_duration_threshold"
+ );
expect(thresholdRow).toBeTruthy();
- thresholdRow.querySelector('[data-config-value]').value = '45';
+ thresholdRow.querySelector("[data-config-value]").value = "45";
- const addRowButton = document.querySelector('[data-config-add-row]');
+ const addRowButton = document.querySelector("[data-config-add-row]");
addRowButton.click();
- const updatedRows = Array.from(document.querySelectorAll('[data-config-row]'));
+ const updatedRows = Array.from(document.querySelectorAll("[data-config-row]"));
const newRow = updatedRows[updatedRows.length - 1];
- newRow.querySelector('[data-config-key]').value = 'enabled';
- newRow.querySelector('[data-config-type]').value = 'boolean';
- newRow.querySelector('[data-config-type]').dispatchEvent(new Event('change', { bubbles: true }));
- newRow.querySelector('[data-config-value]').value = 'true';
+ newRow.querySelector("[data-config-key]").value = "enabled";
+ newRow.querySelector("[data-config-type]").value = "boolean";
+ newRow.querySelector("[data-config-type]").dispatchEvent(new Event("change", { bubbles: true }));
+ newRow.querySelector("[data-config-value]").value = "true";
await modalOptions.preConfirm();
- expect(mocks.setColumn).toHaveBeenCalledWith('/department/gates', 7, 'config', {
- phone_number: '+4530878410',
+ expect(mocks.setColumn).toHaveBeenCalledWith("/department/gates", 7, "config", {
+ phone_number: "+4530878410",
call_duration_threshold: 45,
enabled: true,
});
expect(onAfterSubmit).toHaveBeenCalledTimes(1);
});
- it('falls back to generic editor for non-config columns', async () => {
+ it("falls back to generic editor for non-config columns", async () => {
const onAfterSubmit = vi.fn();
- await DepartmentGates.showEditObjectFieldForm(3, 'name', 'Gate 1', onAfterSubmit);
+ await DepartmentGates.showEditObjectFieldForm(3, "name", "Gate 1", onAfterSubmit);
expect(mocks.showEditObjectFieldForm).toHaveBeenCalledTimes(1);
- expect(mocks.showEditObjectFieldForm).toHaveBeenCalledWith(
- DepartmentGates,
- 3,
- 'name',
- 'Gate 1',
- onAfterSubmit
- );
+ expect(mocks.showEditObjectFieldForm).toHaveBeenCalledWith(DepartmentGates, 3, "name", "Gate 1", onAfterSubmit);
expect(mocks.swalFire).not.toHaveBeenCalled();
});
- it('uses a compact width on small screens', async () => {
+ it("uses a compact width on small screens", async () => {
window.innerWidth = 640;
mocks.swalFire.mockResolvedValue({});
- await DepartmentGates.showEditObjectFieldForm(10, 'config', { value: 'x' }, vi.fn());
+ await DepartmentGates.showEditObjectFieldForm(10, "config", { value: "x" }, vi.fn());
const modalOptions = mocks.swalFire.mock.calls[0][0];
- expect(modalOptions.width).toBe('94vw');
+ expect(modalOptions.width).toBe("94vw");
});
});
diff --git a/tests/unit/department-modules-display-self-serve.spec.js b/tests/unit/department-modules-display-self-serve.spec.js
index c0eba64a..2bb59dae 100644
--- a/tests/unit/department-modules-display-self-serve.spec.js
+++ b/tests/unit/department-modules-display-self-serve.spec.js
@@ -60,12 +60,13 @@ vi.mock("buefy", () => ({
},
emits: ["update:model-value"],
setup(props, { emit }) {
- return () => h("button", {
- "data-testid": "self-serve-switch",
- "data-state": props.modelValue ? "on" : "off",
- "disabled": props.disabled,
- onClick: () => emit("update:model-value", !props.modelValue),
- });
+ return () =>
+ h("button", {
+ "data-testid": "self-serve-switch",
+ "data-state": props.modelValue ? "on" : "off",
+ disabled: props.disabled,
+ onClick: () => emit("update:model-value", !props.modelValue),
+ });
},
}),
}));
@@ -154,12 +155,9 @@ describe("DepartmentModulesDisplay self-serve management", () => {
await flushMicrotasks();
const firstColumn = wrapper.find(".column");
- expect(firstColumn.classes()).toEqual(expect.arrayContaining([
- "is-12-mobile",
- "is-6-tablet",
- "is-4-desktop",
- "is-3-widescreen",
- ]));
+ expect(firstColumn.classes()).toEqual(
+ expect.arrayContaining(["is-12-mobile", "is-6-tablet", "is-4-desktop", "is-3-widescreen"])
+ );
});
it("updates self-serve status and keeps UI in sync on success", async () => {
diff --git a/tests/unit/department-modules-setup-self-serve.spec.js b/tests/unit/department-modules-setup-self-serve.spec.js
index 269eda63..24ed297b 100644
--- a/tests/unit/department-modules-setup-self-serve.spec.js
+++ b/tests/unit/department-modules-setup-self-serve.spec.js
@@ -3,10 +3,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
const source = readFileSync(
- join(
- process.cwd(),
- "src/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue"
- ),
+ join(process.cwd(), "src/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue"),
"utf8"
);
@@ -19,8 +16,8 @@ describe("DepartmentModulesSetup self-serve management regression", () => {
});
it("renders a dedicated self-serve module switch in setup UI", () => {
- expect(source).toContain("subtitle=\"Selvvask\"");
- expect(source).toContain("title=\"Selvvask\"");
+ expect(source).toContain('subtitle="Selvvask"');
+ expect(source).toContain('title="Selvvask"');
expect(source).toContain("Aktiver selvvask i denne afdeling.");
});
});
diff --git a/tests/unit/department-outside-gate-capabilities.spec.js b/tests/unit/department-outside-gate-capabilities.spec.js
index 14955315..2117d327 100644
--- a/tests/unit/department-outside-gate-capabilities.spec.js
+++ b/tests/unit/department-outside-gate-capabilities.spec.js
@@ -51,16 +51,12 @@ describe("department outside gate capabilities", () => {
hasOutsideEntranceGate: true,
hasOutsideExitGate: true,
});
- expect(mocks.request).toHaveBeenCalledWith(
- "/department/gates",
- "GET",
- {
- page: 1,
- limit: 100,
- order: "id:asc",
- filters: "department:12",
- }
- );
+ expect(mocks.request).toHaveBeenCalledWith("/department/gates", "GET", {
+ page: 1,
+ limit: 100,
+ order: "id:asc",
+ filters: "department:12",
+ });
});
it("normalizes truthy/falsy gate flags before computing capabilities", async () => {
diff --git a/tests/unit/department-outside-hours.spec.js b/tests/unit/department-outside-hours.spec.js
index 60bc2ae6..b4704945 100644
--- a/tests/unit/department-outside-hours.spec.js
+++ b/tests/unit/department-outside-hours.spec.js
@@ -17,16 +17,18 @@ const t = (key, values = {}) => {
describe("department outside-hours helpers", () => {
it("normalizes totals, breakdowns, and diagnostics from overview metrics", () => {
- expect(normalizeOutsideHours({
- value: 5,
- by_source: {
- orders: "2",
- xlvask: 1,
- selfserve: "2",
- },
- has_missing_opening_hours: true,
- missing_department_ids: ["4", 7, 0],
- })).toEqual({
+ expect(
+ normalizeOutsideHours({
+ value: 5,
+ by_source: {
+ orders: "2",
+ xlvask: 1,
+ selfserve: "2",
+ },
+ has_missing_opening_hours: true,
+ missing_department_ids: ["4", 7, 0],
+ })
+ ).toEqual({
total: 5,
by_source: {
orders: 2,
@@ -54,34 +56,45 @@ describe("department outside-hours helpers", () => {
"admin.daily_report.reports.night_washes.sources.orders: 1 | admin.daily_report.reports.night_washes.sources.xlvask: 1 | admin.daily_report.reports.night_washes.sources.selfserve: 1"
);
- expect(buildOutsideHoursWarningText(outsideHours, [
- { id: 1, name: "North" },
- { id: 2, name: "South" },
- { id: 4, name: "West" },
- ], t)).toBe("Missing opening hours: South, West");
+ expect(
+ buildOutsideHoursWarningText(
+ outsideHours,
+ [
+ { id: 1, name: "North" },
+ { id: 2, name: "South" },
+ { id: 4, name: "West" },
+ ],
+ t
+ )
+ ).toBe("Missing opening hours: South, West");
});
it("transforms trend payloads into stacked chart series", () => {
- expect(buildOutsideHoursTrendChartState({
- points: [
+ expect(
+ buildOutsideHoursTrendChartState(
{
- date: "2026-03-23",
- by_source: {
- orders: 1,
- xlvask: 0,
- selfserve: 1,
- },
+ points: [
+ {
+ date: "2026-03-23",
+ by_source: {
+ orders: 1,
+ xlvask: 0,
+ selfserve: 1,
+ },
+ },
+ {
+ date: "2026-03-24",
+ by_source: {
+ orders: 0,
+ xlvask: 2,
+ selfserve: 1,
+ },
+ },
+ ],
},
- {
- date: "2026-03-24",
- by_source: {
- orders: 0,
- xlvask: 2,
- selfserve: 1,
- },
- },
- ],
- }, t)).toEqual({
+ t
+ )
+ ).toEqual({
categories: ["2026-03-23", "2026-03-24"],
series: [
{
diff --git a/tests/unit/department-overview-navigation.behavior.spec.js b/tests/unit/department-overview-navigation.behavior.spec.js
index 26e93712..03a22e5e 100644
--- a/tests/unit/department-overview-navigation.behavior.spec.js
+++ b/tests/unit/department-overview-navigation.behavior.spec.js
@@ -9,21 +9,18 @@ const sharedDateState = vi.hoisted(() => ({
selected_date_to: null,
}));
-vi.mock(
- "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue",
- async () => {
- const { ref } = await import("vue");
- const selected_date = ref("2026-03-23");
- const selected_date_to = ref("2026-03-23");
- sharedDateState.selected_date = selected_date;
- sharedDateState.selected_date_to = selected_date_to;
- return {
- selected_date,
- selected_date_to,
- selectDate: selectDateMock,
- };
- }
-);
+vi.mock("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue", async () => {
+ const { ref } = await import("vue");
+ const selected_date = ref("2026-03-23");
+ const selected_date_to = ref("2026-03-23");
+ sharedDateState.selected_date = selected_date;
+ sharedDateState.selected_date_to = selected_date_to;
+ return {
+ selected_date,
+ selected_date_to,
+ selectDate: selectDateMock,
+ };
+});
import DepartmentDashboardOverviewNavigation from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDashboardOverviewNavigation.vue";
diff --git a/tests/unit/department-overview-period-sync.behavior.spec.js b/tests/unit/department-overview-period-sync.behavior.spec.js
index 03f15cfe..c0e8cbfb 100644
--- a/tests/unit/department-overview-period-sync.behavior.spec.js
+++ b/tests/unit/department-overview-period-sync.behavior.spec.js
@@ -45,50 +45,41 @@ vi.mock("@/components/pagination/departmentTabs.vue", async () => {
};
});
-vi.mock(
- "@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue",
- () => {
- let fetchId = 0;
- return {
- next_fetch_id: () => {
- fetchId += 1;
- return fetchId;
- },
- is_latest_fetch: () => true,
- is_loading: () => false,
- finished_loading: vi.fn(),
- };
- }
-);
-
-vi.mock(
- "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue",
- async () => {
- const { ref } = await import("vue");
- const selected_date = ref("2026-03-23");
- const selected_date_to = ref("2026-03-23");
- sharedDateState.selected_date = selected_date;
- sharedDateState.selected_date_to = selected_date_to;
- return {
- selected_date,
- selected_date_to,
- selectDate: (startIso, endIso) => {
- selected_date.value = startIso;
- selected_date_to.value = endIso;
- },
- };
- }
-);
-
-vi.mock(
- "@/views/dashboards/departmentDashboard/other/displays/DepartmentDashboardOverviewNavigation.vue",
- () => ({
- default: {
- name: "DepartmentDashboardOverviewNavigation",
- template: "",
+vi.mock("@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue", () => {
+ let fetchId = 0;
+ return {
+ next_fetch_id: () => {
+ fetchId += 1;
+ return fetchId;
},
- })
-);
+ is_latest_fetch: () => true,
+ is_loading: () => false,
+ finished_loading: vi.fn(),
+ };
+});
+
+vi.mock("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue", async () => {
+ const { ref } = await import("vue");
+ const selected_date = ref("2026-03-23");
+ const selected_date_to = ref("2026-03-23");
+ sharedDateState.selected_date = selected_date;
+ sharedDateState.selected_date_to = selected_date_to;
+ return {
+ selected_date,
+ selected_date_to,
+ selectDate: (startIso, endIso) => {
+ selected_date.value = startIso;
+ selected_date_to.value = endIso;
+ },
+ };
+});
+
+vi.mock("@/views/dashboards/departmentDashboard/other/displays/DepartmentDashboardOverviewNavigation.vue", () => ({
+ default: {
+ name: "DepartmentDashboardOverviewNavigation",
+ template: "",
+ },
+}));
import DepartmentDailyReportThisWeek from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportThisWeek.vue";
import DepartmentDailyReportSmall from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportSmall.vue";
@@ -168,13 +159,10 @@ describe("Department overview period sync behavior", () => {
await flushAll();
await flushAll();
- const initialCalls = getTransactionCountMock.mock.calls.map(([departmentId, date, dateTo]) => `${departmentId}:${date}:${dateTo}`);
- expect(initialCalls).toEqual(
- expect.arrayContaining([
- "10:2026-03-23:2026-03-23",
- "20:2026-03-23:2026-03-23",
- ])
+ const initialCalls = getTransactionCountMock.mock.calls.map(
+ ([departmentId, date, dateTo]) => `${departmentId}:${date}:${dateTo}`
);
+ expect(initialCalls).toEqual(expect.arrayContaining(["10:2026-03-23:2026-03-23", "20:2026-03-23:2026-03-23"]));
});
it("changing shared range refetches both list and cards with the new period", async () => {
@@ -215,12 +203,9 @@ describe("Department overview period sync behavior", () => {
await flushAll();
await flushAll();
- const rangeCalls = getTransactionCountMock.mock.calls.map(([departmentId, date, dateTo]) => `${departmentId}:${date}:${dateTo}`);
- expect(rangeCalls).toEqual(
- expect.arrayContaining([
- "10:2026-03-23:2026-03-24",
- "20:2026-03-23:2026-03-24",
- ])
+ const rangeCalls = getTransactionCountMock.mock.calls.map(
+ ([departmentId, date, dateTo]) => `${departmentId}:${date}:${dateTo}`
);
+ expect(rangeCalls).toEqual(expect.arrayContaining(["10:2026-03-23:2026-03-24", "20:2026-03-23:2026-03-24"]));
});
});
diff --git a/tests/unit/department-self-serve-enabled.spec.js b/tests/unit/department-self-serve-enabled.spec.js
index f4acfba3..36502296 100644
--- a/tests/unit/department-self-serve-enabled.spec.js
+++ b/tests/unit/department-self-serve-enabled.spec.js
@@ -30,10 +30,7 @@ describe("department self-serve enabled management", () => {
const enabled = await getDepartmentSelfServeEnabled("12");
expect(enabled).toBe(true);
- expect(mocks.authenticatedRequest).toHaveBeenCalledWith(
- "/departments/self-serve/enabled?id=12",
- "GET"
- );
+ expect(mocks.authenticatedRequest).toHaveBeenCalledWith("/departments/self-serve/enabled?id=12", "GET");
});
it("normalizes false-like enabled responses", async () => {
diff --git a/tests/unit/department-weather.spec.js b/tests/unit/department-weather.spec.js
index 285c772b..20c1d533 100644
--- a/tests/unit/department-weather.spec.js
+++ b/tests/unit/department-weather.spec.js
@@ -66,7 +66,10 @@ const getRowValues = (wrapper, rowIndex) => {
if (!row) {
return [];
}
- return row.findAll("td").slice(1).map((td) => td.text());
+ return row
+ .findAll("td")
+ .slice(1)
+ .map((td) => td.text());
};
const getProductivityCellClasses = (wrapper) => {
@@ -75,7 +78,10 @@ const getProductivityCellClasses = (wrapper) => {
if (!productivityRow) {
return [];
}
- return productivityRow.findAll("td").slice(1).map((td) => td.classes());
+ return productivityRow
+ .findAll("td")
+ .slice(1)
+ .map((td) => td.classes());
};
describe("DepartmentWeather", () => {
@@ -105,9 +111,7 @@ describe("DepartmentWeather", () => {
});
it("refetches when ids or dates change", async () => {
- getObjectsMock
- .mockResolvedValueOnce([makeEntry({ washes: 3 })])
- .mockResolvedValueOnce([makeEntry({ washes: 7 })]);
+ getObjectsMock.mockResolvedValueOnce([makeEntry({ washes: 3 })]).mockResolvedValueOnce([makeEntry({ washes: 7 })]);
const wrapper = mount(DepartmentWeather, {
props: {
@@ -161,9 +165,7 @@ describe("DepartmentWeather", () => {
const first = createDeferred();
const second = createDeferred();
- getObjectsMock
- .mockImplementationOnce(() => first.promise)
- .mockImplementationOnce(() => second.promise);
+ getObjectsMock.mockImplementationOnce(() => first.promise).mockImplementationOnce(() => second.promise);
const wrapper = mount(DepartmentWeather, {
props: {
diff --git a/tests/unit/economic-transfer-queue.spec.js b/tests/unit/economic-transfer-queue.spec.js
index b45192c0..724c267c 100644
--- a/tests/unit/economic-transfer-queue.spec.js
+++ b/tests/unit/economic-transfer-queue.spec.js
@@ -204,15 +204,19 @@ describe("economic transfer queue workflow", () => {
});
it("enforces strict boolean send_as_is in collected invoice payload", () => {
- expect(() => buildCollectedInvoiceEconomicPayload({
- id: 123,
- send_as_is: "true",
- })).toThrow("send_as_is must be a boolean.");
+ expect(() =>
+ buildCollectedInvoiceEconomicPayload({
+ id: 123,
+ send_as_is: "true",
+ })
+ ).toThrow("send_as_is must be a boolean.");
- expect(buildCollectedInvoiceEconomicPayload({
- id: 123,
- send_as_is: true,
- })).toEqual({
+ expect(
+ buildCollectedInvoiceEconomicPayload({
+ id: 123,
+ send_as_is: true,
+ })
+ ).toEqual({
id: 123,
send_as_is: true,
});
@@ -386,45 +390,50 @@ describe("economic transfer queue workflow", () => {
});
it("still throws when enqueue response has no queue job id", () => {
- expect(() => parseQueueJobFromEnqueueResponse({
- data: {
+ expect(() =>
+ parseQueueJobFromEnqueueResponse({
data: {
- message: "Queued",
- job: {
- status: "QUEUED",
- progress_percent: 0,
+ data: {
+ message: "Queued",
+ job: {
+ status: "QUEUED",
+ progress_percent: 0,
+ },
},
},
- },
- })).toThrow("Missing queue job id in enqueue response.");
+ })
+ ).toThrow("Missing queue job id in enqueue response.");
});
it("parses additive queue list metadata fields from list responses", () => {
- const parsed = parseEconomicTransferQueueListResponse({
- data: {
+ const parsed = parseEconomicTransferQueueListResponse(
+ {
data: {
- items: [
- {
- id: 7001,
- status: "PROCESSING",
- progress_percent: 30,
- progress_message: "Exporting",
- attempts: 1,
- max_attempts: 3,
- created_at: "2026-03-10T10:00:00Z",
- },
- ],
- count: 1,
- total: 8,
- limit: 1,
- offset: 2,
- has_more: true,
+ data: {
+ items: [
+ {
+ id: 7001,
+ status: "PROCESSING",
+ progress_percent: 30,
+ progress_message: "Exporting",
+ attempts: 1,
+ max_attempts: 3,
+ created_at: "2026-03-10T10:00:00Z",
+ },
+ ],
+ count: 1,
+ total: 8,
+ limit: 1,
+ offset: 2,
+ has_more: true,
+ },
},
},
- }, {
- defaultLimit: 50,
- defaultOffset: 0,
- });
+ {
+ defaultLimit: 50,
+ defaultOffset: 0,
+ }
+ );
expect(parsed.items).toHaveLength(1);
expect(parsed.items[0]).toMatchObject({
@@ -525,9 +534,7 @@ describe("economic transfer queue workflow", () => {
const tailPage = parseEconomicTransferQueueListResponse({
data: {
data: {
- items: [
- { id: 7103, status: "COMPLETED" },
- ],
+ items: [{ id: 7103, status: "COMPLETED" }],
count: 1,
total: 5,
limit: 2,
@@ -675,10 +682,12 @@ describe("economic transfer queue workflow", () => {
buildEnqueuePayload: (payload) => buildCollectedInvoiceEconomicPayload(payload),
});
- await expect(queue.enqueue({
- id: 91,
- send_as_is: false,
- })).rejects.toBeDefined();
+ await expect(
+ queue.enqueue({
+ id: 91,
+ send_as_is: false,
+ })
+ ).rejects.toBeDefined();
expect(queue.transportErrorMessage.value).toContain("gateway timeout");
expect(queue.isPolling.value).toBe(false);
diff --git a/tests/unit/edge-gateway-service.spec.js b/tests/unit/edge-gateway-service.spec.js
index 97fc345e..53ed7fff 100644
--- a/tests/unit/edge-gateway-service.spec.js
+++ b/tests/unit/edge-gateway-service.spec.js
@@ -9,11 +9,11 @@ describe("edge gateway service contract", () => {
expect(source).toContain('"/departments"');
expect(source).toContain('"/edge-gateways"');
expect(source).toContain('"/edge-gateways/install-token"');
- expect(source).toContain('/edge-gateways/${encodeURIComponent(gatewayId)}/discovery');
- expect(source).toContain('/edge-gateways/${encodeURIComponent(gatewayId)}/bindings');
- expect(source).toContain('/edge-gateways/${encodeURIComponent(gatewayId)}/update-jobs');
- expect(source).toContain('/edge-gateways/${encodeURIComponent(gatewayId)}/shell-sessions');
- expect(source).toContain('/edge-gateways/${encodeURIComponent(gatewayId)}/rotate-credentials');
- expect(source).toContain('/departments/${encodeURIComponent(departmentId)}/gateway-cutover');
+ expect(source).toContain("/edge-gateways/${encodeURIComponent(gatewayId)}/discovery");
+ expect(source).toContain("/edge-gateways/${encodeURIComponent(gatewayId)}/bindings");
+ expect(source).toContain("/edge-gateways/${encodeURIComponent(gatewayId)}/update-jobs");
+ expect(source).toContain("/edge-gateways/${encodeURIComponent(gatewayId)}/shell-sessions");
+ expect(source).toContain("/edge-gateways/${encodeURIComponent(gatewayId)}/rotate-credentials");
+ expect(source).toContain("/departments/${encodeURIComponent(departmentId)}/gateway-cutover");
});
});
diff --git a/tests/unit/edge-gateway-workspace.spec.js b/tests/unit/edge-gateway-workspace.spec.js
index e6192828..75cec9ef 100644
--- a/tests/unit/edge-gateway-workspace.spec.js
+++ b/tests/unit/edge-gateway-workspace.spec.js
@@ -26,19 +26,18 @@ describe("edge gateway workspace contract", () => {
expect(workspaceSource).toContain('@session-approved="handleShellSessionApproved"');
});
- it("renders queued discovery/update flows and broker connectivity as separate UI signals", () => {
+ it("renders queued discovery/update flows and the API polling control channel", () => {
expect(workspaceSource).toContain("Generér installer");
expect(workspaceSource).toContain('aria-label="Installer afdeling"');
expect(workspaceSource).toContain("Kør discovery");
- expect(workspaceSource).toContain("Brokerforbindelse");
+ expect(workspaceSource).toContain("Styringskanal");
expect(workspaceSource).toContain("Latens");
expect(workspaceSource).toContain("CPU");
expect(workspaceSource).toContain("RAM");
expect(workspaceSource).toContain("Disk");
- expect(workspaceSource).toContain("getBrokerMeta");
+ expect(workspaceSource).toContain("getControlChannelMeta");
expect(workspaceSource).toContain("buildGatewayMetricView");
expect(workspaceSource).toContain("system_metrics");
- expect(workspaceSource).toContain("metadata.broker_connected");
expect(workspaceSource).toContain("startGatewayRefreshLoop");
expect(workspaceSource).toContain("stopGatewayRefreshLoop");
expect(workspaceSource).toContain("Discovery er køet");
@@ -46,9 +45,12 @@ describe("edge gateway workspace contract", () => {
expect(workspaceSource).toContain("resolveEdgeGatewayErrorMessage");
expect(workspaceSource).toContain("normalizeDisplayText");
expect(workspaceSource).toContain("decodeMojibakeText");
- expect(workspaceSource).toContain("const displayErrorMessage = computed(() => decodeMojibakeText(normalizeDisplayText(errorMessage.value)));");
+ expect(workspaceSource).toContain(
+ "const displayErrorMessage = computed(() => decodeMojibakeText(normalizeDisplayText(errorMessage.value)));"
+ );
expect(workspaceSource).toContain("{{ displayErrorMessage }}");
expect(workspaceSource).toContain("Gateway-agenten er offline og kan ikke hente nye kommandoer lige nu.");
+ expect(workspaceSource).toContain("Gatewayen henter kommandoer og shell-events via API'et.");
expect(workspaceSource).toContain("Gem bindinger");
expect(workspaceSource).toContain("Planlæg opdatering");
expect(workspaceSource).toContain("Transporttilstand");
diff --git a/tests/unit/goal-criteria-payload.spec.js b/tests/unit/goal-criteria-payload.spec.js
index 22ab88b2..9699e81c 100644
--- a/tests/unit/goal-criteria-payload.spec.js
+++ b/tests/unit/goal-criteria-payload.spec.js
@@ -1,92 +1,104 @@
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it } from "vitest";
import {
buildGoalCriteriaApiPayload,
normalizeGoalCriteriaInput,
TARGET_DURATION_MODE_LEGACY,
- TARGET_DURATION_MODE_WEEKS
-} from '@/views/dashboards/departmentDashboard/modules/goals/functions/goalCriteriaPayload';
+ TARGET_DURATION_MODE_WEEKS,
+} from "@/views/dashboards/departmentDashboard/modules/goals/functions/goalCriteriaPayload";
-describe('goal criteria payload helpers', () => {
- it('omits target duration fields in legacy mode and serializes day boundaries as ISO date-time', () => {
- const payload = buildGoalCriteriaApiPayload({
- label: 'Quarter Goal',
- type: 'REVENUE',
- target: 1000,
- start: '2026-03-01',
- end: '2026-03-31',
- departments: [5]
- }, {
- targetDurationMode: TARGET_DURATION_MODE_LEGACY
- });
+describe("goal criteria payload helpers", () => {
+ it("omits target duration fields in legacy mode and serializes day boundaries as ISO date-time", () => {
+ const payload = buildGoalCriteriaApiPayload(
+ {
+ label: "Quarter Goal",
+ type: "REVENUE",
+ target: 1000,
+ start: "2026-03-01",
+ end: "2026-03-31",
+ departments: [5],
+ },
+ {
+ targetDurationMode: TARGET_DURATION_MODE_LEGACY,
+ }
+ );
- expect(payload).not.toHaveProperty('target_duration');
- expect(payload).not.toHaveProperty('target_duration_every');
+ expect(payload).not.toHaveProperty("target_duration");
+ expect(payload).not.toHaveProperty("target_duration_every");
expect(payload.start).toMatch(/^2026-03-01T00:00:00(?:Z|[+-]\d{2}:\d{2})$/);
expect(payload.end).toMatch(/^2026-03-31T23:59:59(?:Z|[+-]\d{2}:\d{2})$/);
});
- it('includes cadence fields when duration mode requires them', () => {
- const payload = buildGoalCriteriaApiPayload({
- label: 'Bi-weekly Goal',
- type: 'PRODUCT',
- target: 42,
- start: '2026-04-01',
- end: '2026-04-30',
- departments: [2],
- target_duration_every: 2
- }, {
- targetDurationMode: TARGET_DURATION_MODE_WEEKS
- });
+ it("includes cadence fields when duration mode requires them", () => {
+ const payload = buildGoalCriteriaApiPayload(
+ {
+ label: "Bi-weekly Goal",
+ type: "PRODUCT",
+ target: 42,
+ start: "2026-04-01",
+ end: "2026-04-30",
+ departments: [2],
+ target_duration_every: 2,
+ },
+ {
+ targetDurationMode: TARGET_DURATION_MODE_WEEKS,
+ }
+ );
- expect(payload.target_duration).toBe('WEEKS');
+ expect(payload.target_duration).toBe("WEEKS");
expect(payload.target_duration_every).toBe(2);
});
- it('normalizes legacy camelCase fields to canonical snake_case', () => {
- const normalized = normalizeGoalCriteriaInput({
- targetDuration: 'months',
- targetDurationEvery: '3',
- progressAlertFrequency: 'weekly',
- progressAlertDestination: 'email',
- progressAlertProgressType: 'count_only',
- progressAlertStyle: 'collective',
- progressAlertFormat: 'Report: {percent}%',
- progressAlertWeekdays: ['monday', 'friday', 'invalid'],
- progressAlertTimeOfDay: '09:30+01:00',
- departmentDailyTargets: { '2': '14', 'invalid': 'x' },
- start: '2026-01-15T12:00:00+01:00',
- end: '2026-02-20T18:30:00+01:00'
- }, [8]);
+ it("normalizes legacy camelCase fields to canonical snake_case", () => {
+ const normalized = normalizeGoalCriteriaInput(
+ {
+ targetDuration: "months",
+ targetDurationEvery: "3",
+ progressAlertFrequency: "weekly",
+ progressAlertDestination: "email",
+ progressAlertProgressType: "count_only",
+ progressAlertStyle: "collective",
+ progressAlertFormat: "Report: {percent}%",
+ progressAlertWeekdays: ["monday", "friday", "invalid"],
+ progressAlertTimeOfDay: "09:30+01:00",
+ departmentDailyTargets: { 2: "14", invalid: "x" },
+ start: "2026-01-15T12:00:00+01:00",
+ end: "2026-02-20T18:30:00+01:00",
+ },
+ [8]
+ );
- expect(normalized.target_duration).toBe('MONTHS');
+ expect(normalized.target_duration).toBe("MONTHS");
expect(normalized.target_duration_every).toBe(3);
- expect(normalized.progress_alert_frequency).toBe('WEEKLY');
- expect(normalized.progress_alert_destination).toBe('EMAIL');
- expect(normalized.progress_alert_progress_type).toBe('COUNT_ONLY');
- expect(normalized.progress_alert_style).toBe('COLLECTIVE');
- expect(normalized.progress_alert_format).toBe('Report: {percent}%');
- expect(normalized.progress_alert_weekdays).toEqual(['MONDAY', 'FRIDAY']);
- expect(normalized.progress_alert_time_of_day).toBe('09:30+01:00');
- expect(normalized.department_daily_targets).toEqual({ '2': 14 });
- expect(normalized.start).toBe('2026-01-15');
- expect(normalized.end).toBe('2026-02-20');
+ expect(normalized.progress_alert_frequency).toBe("WEEKLY");
+ expect(normalized.progress_alert_destination).toBe("EMAIL");
+ expect(normalized.progress_alert_progress_type).toBe("COUNT_ONLY");
+ expect(normalized.progress_alert_style).toBe("COLLECTIVE");
+ expect(normalized.progress_alert_format).toBe("Report: {percent}%");
+ expect(normalized.progress_alert_weekdays).toEqual(["MONDAY", "FRIDAY"]);
+ expect(normalized.progress_alert_time_of_day).toBe("09:30+01:00");
+ expect(normalized.department_daily_targets).toEqual({ 2: 14 });
+ expect(normalized.start).toBe("2026-01-15");
+ expect(normalized.end).toBe("2026-02-20");
expect(normalized.departments).toEqual([8]);
});
- it('converts weekly department targets to daily values in API payload', () => {
- const payload = buildGoalCriteriaApiPayload({
- label: 'Dept Weekly Goal',
- type: 'VISITS',
- target: 10,
- start: '2026-05-01',
- end: '2026-05-31',
- departments: [9],
- department_daily_targets: { '9': 21 }
- }, {
- departmentTargetUnit: 'weeks',
- targetDurationMode: TARGET_DURATION_MODE_LEGACY
- });
+ it("converts weekly department targets to daily values in API payload", () => {
+ const payload = buildGoalCriteriaApiPayload(
+ {
+ label: "Dept Weekly Goal",
+ type: "VISITS",
+ target: 10,
+ start: "2026-05-01",
+ end: "2026-05-31",
+ departments: [9],
+ department_daily_targets: { 9: 21 },
+ },
+ {
+ departmentTargetUnit: "weeks",
+ targetDurationMode: TARGET_DURATION_MODE_LEGACY,
+ }
+ );
- expect(payload.department_daily_targets['9']).toBe(3);
+ expect(payload.department_daily_targets["9"]).toBe(3);
});
});
diff --git a/tests/unit/goal-progress-resolver.spec.js b/tests/unit/goal-progress-resolver.spec.js
index eb3414e0..e1ecc131 100644
--- a/tests/unit/goal-progress-resolver.spec.js
+++ b/tests/unit/goal-progress-resolver.spec.js
@@ -1,4 +1,4 @@
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it } from "vitest";
import {
computeCadenceTargetForTimeframe,
isGoalProgressKeyForTimeframe,
@@ -6,128 +6,132 @@ import {
resolveGoalProgressKey,
resolveGoalProgressPeriod,
resolveGoalProgressSlice,
- toProgressNumber
-} from '@/views/dashboards/departmentDashboard/modules/goals/functions/goalProgressResolver.js';
+ toProgressNumber,
+} from "@/views/dashboards/departmentDashboard/modules/goals/functions/goalProgressResolver.js";
-describe('goal progress resolver', () => {
- it('normalizes until_now to to_date', () => {
- expect(normalizeTimeframeKey('until_now')).toBe('to_date');
- expect(normalizeTimeframeKey('month')).toBe('month');
+describe("goal progress resolver", () => {
+ it("normalizes until_now to to_date", () => {
+ expect(normalizeTimeframeKey("until_now")).toBe("to_date");
+ expect(normalizeTimeframeKey("month")).toBe("month");
});
- it('resolves legacy this_week alias for week timeframe', () => {
+ it("resolves legacy this_week alias for week timeframe", () => {
const scope = {
- this_week: { count: 3, target: 7 }
+ this_week: { count: 3, target: 7 },
};
- expect(resolveGoalProgressKey(scope, 'week', {})).toBe('this_week');
- expect(resolveGoalProgressSlice(scope, 'week', {})).toEqual({ count: 3, target: 7 });
+ expect(resolveGoalProgressKey(scope, "week", {})).toBe("this_week");
+ expect(resolveGoalProgressSlice(scope, "week", {})).toEqual({ count: 3, target: 7 });
});
- it('resolves cadence-aware keys for bi-weekly targets', () => {
+ it("resolves cadence-aware keys for bi-weekly targets", () => {
const scope = {
every_2_weeks: { count: 5, target: 14 },
- week: { count: 1, target: 7 }
+ week: { count: 1, target: 7 },
};
const criteria = {
- target_duration: 'WEEKS',
- target_duration_every: 2
+ target_duration: "WEEKS",
+ target_duration_every: 2,
};
- expect(resolveGoalProgressKey(scope, 'week', criteria)).toBe('every_2_weeks');
- expect(resolveGoalProgressSlice(scope, 'week', criteria)).toEqual({ count: 5, target: 14 });
+ expect(resolveGoalProgressKey(scope, "week", criteria)).toBe("every_2_weeks");
+ expect(resolveGoalProgressSlice(scope, "week", criteria)).toEqual({ count: 5, target: 14 });
});
- it('does not force cadence bucket for unrelated timeframe tabs', () => {
+ it("does not force cadence bucket for unrelated timeframe tabs", () => {
const scope = {
to_date: { count: 1, target: 7 },
- every_2_weeks: { count: 5, target: 14 }
+ every_2_weeks: { count: 5, target: 14 },
};
const criteria = {
- target_duration: 'WEEKS',
- target_duration_every: 2
+ target_duration: "WEEKS",
+ target_duration_every: 2,
};
- expect(resolveGoalProgressKey(scope, 'month', criteria)).toBe('to_date');
- expect(resolveGoalProgressSlice(scope, 'month', criteria)).toEqual({ count: 1, target: 7 });
+ expect(resolveGoalProgressKey(scope, "month", criteria)).toBe("to_date");
+ expect(resolveGoalProgressSlice(scope, "month", criteria)).toEqual({ count: 1, target: 7 });
});
- it('keeps timeframe-specific slices when available', () => {
+ it("keeps timeframe-specific slices when available", () => {
const scope = {
today: { count: 2, target: 2 },
week: { count: 7, target: 14 },
- every_2_weeks: { count: 7, target: 14 }
+ every_2_weeks: { count: 7, target: 14 },
};
const criteria = {
- target_duration: 'WEEKS',
- target_duration_every: 2
+ target_duration: "WEEKS",
+ target_duration_every: 2,
};
- expect(resolveGoalProgressKey(scope, 'today', criteria)).toBe('today');
- expect(resolveGoalProgressSlice(scope, 'today', criteria)).toEqual({ count: 2, target: 2 });
+ expect(resolveGoalProgressKey(scope, "today", criteria)).toBe("today");
+ expect(resolveGoalProgressSlice(scope, "today", criteria)).toEqual({ count: 2, target: 2 });
});
- it('marks only matching keys as timeframe-compatible', () => {
+ it("marks only matching keys as timeframe-compatible", () => {
const criteria = {
- target_duration: 'WEEKS',
- target_duration_every: 2
+ target_duration: "WEEKS",
+ target_duration_every: 2,
};
- expect(isGoalProgressKeyForTimeframe('every_2_weeks', 'week', criteria)).toBe(true);
- expect(isGoalProgressKeyForTimeframe('to_date', 'week', criteria)).toBe(false);
- expect(isGoalProgressKeyForTimeframe('month', 'month', criteria)).toBe(true);
- expect(isGoalProgressKeyForTimeframe('year', 'month', criteria)).toBe(false);
+ expect(isGoalProgressKeyForTimeframe("every_2_weeks", "week", criteria)).toBe(true);
+ expect(isGoalProgressKeyForTimeframe("to_date", "week", criteria)).toBe(false);
+ expect(isGoalProgressKeyForTimeframe("month", "month", criteria)).toBe(true);
+ expect(isGoalProgressKeyForTimeframe("year", "month", criteria)).toBe(false);
});
- it('falls back to preferred OpenAPI keys when selected key is missing', () => {
+ it("falls back to preferred OpenAPI keys when selected key is missing", () => {
const scope = {
to_date: { count: 21, target: 30 },
- all: { count: 42, target: 100 }
+ all: { count: 42, target: 100 },
};
- expect(resolveGoalProgressKey(scope, 'month', {})).toBe('to_date');
+ expect(resolveGoalProgressKey(scope, "month", {})).toBe("to_date");
});
- it('extracts period from snake_case or camelCase date fields', () => {
- expect(resolveGoalProgressPeriod(
- { date_from: '2026-03-24T00:00:00+01:00', date_end: '2026-04-06T23:59:59+01:00' },
- { start: '2026-03-01T00:00:00+01:00', end: '2026-05-01T23:59:59+01:00' }
- )).toEqual({
- from: '2026-03-24T00:00:00+01:00',
- end: '2026-04-06T23:59:59+01:00'
+ it("extracts period from snake_case or camelCase date fields", () => {
+ expect(
+ resolveGoalProgressPeriod(
+ { date_from: "2026-03-24T00:00:00+01:00", date_end: "2026-04-06T23:59:59+01:00" },
+ { start: "2026-03-01T00:00:00+01:00", end: "2026-05-01T23:59:59+01:00" }
+ )
+ ).toEqual({
+ from: "2026-03-24T00:00:00+01:00",
+ end: "2026-04-06T23:59:59+01:00",
});
- expect(resolveGoalProgressPeriod(
- { dateFrom: '2026-03-24T00:00:00+01:00', dateEnd: '2026-04-06T23:59:59+01:00' },
- { start: '2026-03-01T00:00:00+01:00', end: '2026-05-01T23:59:59+01:00' }
- )).toEqual({
- from: '2026-03-24T00:00:00+01:00',
- end: '2026-04-06T23:59:59+01:00'
+ expect(
+ resolveGoalProgressPeriod(
+ { dateFrom: "2026-03-24T00:00:00+01:00", dateEnd: "2026-04-06T23:59:59+01:00" },
+ { start: "2026-03-01T00:00:00+01:00", end: "2026-05-01T23:59:59+01:00" }
+ )
+ ).toEqual({
+ from: "2026-03-24T00:00:00+01:00",
+ end: "2026-04-06T23:59:59+01:00",
});
});
- it('parses numeric strings safely for progress values', () => {
- expect(toProgressNumber('12.5')).toBe(12.5);
+ it("parses numeric strings safely for progress values", () => {
+ expect(toProgressNumber("12.5")).toBe(12.5);
expect(toProgressNumber(8)).toBe(8);
- expect(toProgressNumber('')).toBe(null);
- expect(toProgressNumber('abc')).toBe(null);
+ expect(toProgressNumber("")).toBe(null);
+ expect(toProgressNumber("abc")).toBe(null);
});
- it('computes cadence target per timeframe for bi-weekly goals', () => {
+ it("computes cadence target per timeframe for bi-weekly goals", () => {
const criteria = {
target: 7,
- target_duration: 'WEEKS',
+ target_duration: "WEEKS",
target_duration_every: 2,
- start: '2026-03-24T00:00:00+01:00',
- end: '2026-07-24T23:59:59+01:00',
+ start: "2026-03-24T00:00:00+01:00",
+ end: "2026-07-24T23:59:59+01:00",
};
- const now = new Date('2026-04-10T12:00:00+01:00');
- expect(computeCadenceTargetForTimeframe(criteria, 'today', now)).toBe(7);
- expect(computeCadenceTargetForTimeframe(criteria, 'week', now)).toBe(14);
- expect(computeCadenceTargetForTimeframe(criteria, 'month', now)).toBe(21);
- expect(computeCadenceTargetForTimeframe(criteria, 'year', now)).toBe(63);
- expect(computeCadenceTargetForTimeframe(criteria, 'all', now)).toBe(63);
- expect(computeCadenceTargetForTimeframe(criteria, 'to_date', new Date('2026-03-24T12:00:00+01:00'))).toBe(7);
+ const now = new Date("2026-04-10T12:00:00+01:00");
+ expect(computeCadenceTargetForTimeframe(criteria, "today", now)).toBe(7);
+ expect(computeCadenceTargetForTimeframe(criteria, "week", now)).toBe(14);
+ expect(computeCadenceTargetForTimeframe(criteria, "month", now)).toBe(21);
+ expect(computeCadenceTargetForTimeframe(criteria, "year", now)).toBe(63);
+ expect(computeCadenceTargetForTimeframe(criteria, "all", now)).toBe(63);
+ expect(computeCadenceTargetForTimeframe(criteria, "to_date", new Date("2026-03-24T12:00:00+01:00"))).toBe(7);
});
});
diff --git a/tests/unit/helpers/mountWithApp.js b/tests/unit/helpers/mountWithApp.js
index 9ea64771..e40119ed 100644
--- a/tests/unit/helpers/mountWithApp.js
+++ b/tests/unit/helpers/mountWithApp.js
@@ -30,17 +30,18 @@ const UserDashboardPageWrapperStub = {
const RouterLinkStub = {
props: ["to"],
- template: "",
+ template: '',
};
-export const createTestI18n = (messages = { en: {} }) => createI18n({
- legacy: false,
- globalInjection: true,
- locale: "en",
- missingWarn: false,
- fallbackWarn: false,
- messages,
-});
+export const createTestI18n = (messages = { en: {} }) =>
+ createI18n({
+ legacy: false,
+ globalInjection: true,
+ locale: "en",
+ missingWarn: false,
+ fallbackWarn: false,
+ messages,
+ });
export const defaultStubs = {
WhiteBoxCard: WhiteBoxCardStub,
diff --git a/tests/unit/i18n-bookings.spec.js b/tests/unit/i18n-bookings.spec.js
index 46ac896e..48d14579 100644
--- a/tests/unit/i18n-bookings.spec.js
+++ b/tests/unit/i18n-bookings.spec.js
@@ -47,7 +47,7 @@ const bookingKeys = [
"missing_products",
"error_copied",
"guest_info_html",
- "confirm_booking"
+ "confirm_booking",
];
function findMissingKeys(messages, keys) {
diff --git a/tests/unit/invoice-distribution-api.spec.js b/tests/unit/invoice-distribution-api.spec.js
index 7d701475..6a358511 100644
--- a/tests/unit/invoice-distribution-api.spec.js
+++ b/tests/unit/invoice-distribution-api.spec.js
@@ -1,8 +1,8 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-vi.mock('@/components/session/token/SessionUser.vue', () => {
+vi.mock("@/components/session/token/SessionUser.vue", () => {
const request = vi.fn();
- const parseErrorMessage = vi.fn((error) => error?.message || String(error || 'Unknown error'));
+ const parseErrorMessage = vi.fn((error) => error?.message || String(error || "Unknown error"));
return {
SessionUser: {
@@ -14,11 +14,11 @@ vi.mock('@/components/session/token/SessionUser.vue', () => {
};
});
-import { SessionUser } from '@/components/session/token/SessionUser.vue';
+import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {
compareCollectedInvoicesForMonth,
fetchMonthData,
-} from '@/views/dashboards/superUserDashboard/invoiceDistribution/imports/invoiceDistributionApi.js';
+} from "@/views/dashboards/superUserDashboard/invoiceDistribution/imports/invoiceDistributionApi.js";
const buildInvoice = (id) => ({
id,
@@ -32,9 +32,9 @@ const buildV2Category = (amount) => ({
{
id: 1,
customer_number: 5001,
- customer_name: 'ACME',
+ customer_name: "ACME",
requires_action: false,
- transactions: [{ id: 1, amount, booked: true, excluded: false, department_id: 1, date: '2026-03-01T00:00:00Z' }],
+ transactions: [{ id: 1, amount, booked: true, excluded: false, department_id: 1, date: "2026-03-01T00:00:00Z" }],
meta: {
fixed_pricing: {
price: amount,
@@ -52,7 +52,7 @@ const buildV2Category = (amount) => ({
const buildV2CompareResult = (invoiceId, { internalTotal = 100, bookedTotal = 100, mismatch = false } = {}) => ({
collected_invoice_id: invoiceId,
- warnings: mismatch ? ['Top level warning'] : [],
+ warnings: mismatch ? ["Top level warning"] : [],
details: {
order_ids: mismatch ? [1, 2] : [1],
customer: {
@@ -74,15 +74,15 @@ const buildV2CompareResult = (invoiceId, { internalTotal = 100, bookedTotal = 10
},
targets: {
booked: {
- target: 'booked',
- status: mismatch ? 'partial_mismatch' : 'exact_match',
+ target: "booked",
+ status: mismatch ? "partial_mismatch" : "exact_match",
overall_match: !mismatch,
totals: {
target_net_total: bookedTotal,
difference: internalTotal - bookedTotal,
},
- mismatch_reasons: mismatch ? ['department_total_mismatch'] : [],
- warnings: mismatch ? ['Line mismatch'] : [],
+ mismatch_reasons: mismatch ? ["department_total_mismatch"] : [],
+ warnings: mismatch ? ["Line mismatch"] : [],
lines: {
summary: {
internal_billable_count: mismatch ? 2 : 1,
@@ -101,32 +101,32 @@ const buildV2CompareResult = (invoiceId, { internalTotal = 100, bookedTotal = 10
},
});
-describe('invoice distribution api adapter', () => {
+describe("invoice distribution api adapter", () => {
let consoleWarnSpy;
beforeEach(() => {
- consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
SessionUser.request.mockReset();
SessionUser.functions.parseErrorMessage.mockReset();
- SessionUser.functions.parseErrorMessage.mockImplementation((error) => error?.message || String(error || 'Unknown error'));
+ SessionUser.functions.parseErrorMessage.mockImplementation(
+ (error) => error?.message || String(error || "Unknown error")
+ );
});
afterEach(() => {
consoleWarnSpy?.mockRestore();
});
- it('uses v2 all endpoint for month distribution when available', async () => {
+ it("uses v2 all endpoint for month distribution when available", async () => {
SessionUser.request.mockImplementation(async (endpoint) => {
- if (endpoint === '/superuser/invoicing/period') {
+ if (endpoint === "/superuser/invoicing/period") {
return {
data: {
data: {
types: {
all: [
{
- transactions: [
- { amount: 200, booked: true, excluded: false },
- ],
+ transactions: [{ amount: 200, booked: true, excluded: false }],
},
],
},
@@ -135,7 +135,7 @@ describe('invoice distribution api adapter', () => {
};
}
- if (endpoint === '/superuser/invoicing/period/distribution/v2/all') {
+ if (endpoint === "/superuser/invoicing/period/distribution/v2/all") {
return {
data: {
fixed_pricing: buildV2Category(80),
@@ -154,12 +154,16 @@ describe('invoice distribution api adapter', () => {
expect(result.summary.distributionAmount).toBe(150);
expect(result.summary.customerPriceAmount).toBe(50);
expect(result.summary.totalAmount).toBe(350);
- expect(SessionUser.request).toHaveBeenCalledWith('/superuser/invoicing/period/distribution/v2/all', 'GET', expect.any(Object));
+ expect(SessionUser.request).toHaveBeenCalledWith(
+ "/superuser/invoicing/period/distribution/v2/all",
+ "GET",
+ expect.any(Object)
+ );
});
- it('falls back to v2 split endpoints when v2 all fails', async () => {
+ it("falls back to v2 split endpoints when v2 all fails", async () => {
SessionUser.request.mockImplementation(async (endpoint) => {
- if (endpoint === '/superuser/invoicing/period') {
+ if (endpoint === "/superuser/invoicing/period") {
return {
data: {
data: {
@@ -171,19 +175,19 @@ describe('invoice distribution api adapter', () => {
};
}
- if (endpoint === '/superuser/invoicing/period/distribution/v2/all') {
- throw new Error('v2 all unavailable');
+ if (endpoint === "/superuser/invoicing/period/distribution/v2/all") {
+ throw new Error("v2 all unavailable");
}
- if (endpoint === '/superuser/invoicing/period/distribution/v2/fixed-pricing') {
+ if (endpoint === "/superuser/invoicing/period/distribution/v2/fixed-pricing") {
return { data: buildV2Category(40) };
}
- if (endpoint === '/superuser/invoicing/period/distribution/v2/wash-subscriptions') {
+ if (endpoint === "/superuser/invoicing/period/distribution/v2/wash-subscriptions") {
return { data: buildV2Category(30) };
}
- if (endpoint === '/superuser/invoicing/period/distribution/v2/customer-prices') {
+ if (endpoint === "/superuser/invoicing/period/distribution/v2/customer-prices") {
return { data: buildV2Category(20) };
}
@@ -193,15 +197,15 @@ describe('invoice distribution api adapter', () => {
const result = await fetchMonthData(2026, 3);
expect(result.fallback.usedLegacyDistribution).toBe(false);
expect(result.fallback.usedSplitV2Distribution).toBe(true);
- expect(result.fallback.distributionFallbackReason).toContain('v2 all unavailable');
+ expect(result.fallback.distributionFallbackReason).toContain("v2 all unavailable");
expect(result.summary.distributionAmount).toBe(70);
expect(result.summary.customerPriceAmount).toBe(20);
expect(result.summary.totalAmount).toBe(190);
});
- it('falls back to legacy distribution endpoints when all v2 paths fail', async () => {
+ it("falls back to legacy distribution endpoints when all v2 paths fail", async () => {
SessionUser.request.mockImplementation(async (endpoint) => {
- if (endpoint === '/superuser/invoicing/period') {
+ if (endpoint === "/superuser/invoicing/period") {
return {
data: {
data: {
@@ -214,22 +218,22 @@ describe('invoice distribution api adapter', () => {
}
if (
- endpoint === '/superuser/invoicing/period/distribution/v2/all'
- || endpoint === '/superuser/invoicing/period/distribution/v2/fixed-pricing'
- || endpoint === '/superuser/invoicing/period/distribution/v2/wash-subscriptions'
- || endpoint === '/superuser/invoicing/period/distribution/v2/customer-prices'
+ endpoint === "/superuser/invoicing/period/distribution/v2/all" ||
+ endpoint === "/superuser/invoicing/period/distribution/v2/fixed-pricing" ||
+ endpoint === "/superuser/invoicing/period/distribution/v2/wash-subscriptions" ||
+ endpoint === "/superuser/invoicing/period/distribution/v2/customer-prices"
) {
throw new Error(`${endpoint} unavailable`);
}
- if (endpoint === '/superuser/invoicing/period/distribution/fixed-pricing') {
+ if (endpoint === "/superuser/invoicing/period/distribution/fixed-pricing") {
return {
data: {
customers: [
{
id: 1,
customer_number: 1001,
- customer_name: 'Legacy Fixed',
+ customer_name: "Legacy Fixed",
meta: { fixed_pricing: { price: 50 } },
},
],
@@ -242,14 +246,14 @@ describe('invoice distribution api adapter', () => {
};
}
- if (endpoint === '/superuser/invoicing/period/distribution/wash-subscriptions') {
+ if (endpoint === "/superuser/invoicing/period/distribution/wash-subscriptions") {
return {
data: {
customers: [
{
id: 2,
customer_number: 1002,
- customer_name: 'Legacy Subscription',
+ customer_name: "Legacy Subscription",
meta: { wash_subscription: { price: 25 } },
},
],
@@ -267,18 +271,20 @@ describe('invoice distribution api adapter', () => {
const result = await fetchMonthData(2026, 3);
expect(result.fallback.usedLegacyDistribution).toBe(true);
- expect(result.fallback.distributionFallbackReason).toContain('/superuser/invoicing/period/distribution/v2/all unavailable');
+ expect(result.fallback.distributionFallbackReason).toContain(
+ "/superuser/invoicing/period/distribution/v2/all unavailable"
+ );
expect(result.summary.fixedPricingAmount).toBe(50);
expect(result.summary.subscriptionAmount).toBe(25);
expect(result.summary.distributionAmount).toBe(75);
expect(result.summary.totalAmount).toBe(195);
});
- it('chunks v2 bulk compare requests to max 200 ids', async () => {
+ it("chunks v2 bulk compare requests to max 200 ids", async () => {
const invoices = Array.from({ length: 250 }, (_, index) => buildInvoice(index + 1));
SessionUser.request.mockImplementation(async (endpoint, method, params) => {
- if (endpoint === '/collected-invoices' && method === 'GET') {
+ if (endpoint === "/collected-invoices" && method === "GET") {
const page = Number(params?.page || 1);
const perPage = 100;
const start = (page - 1) * perPage;
@@ -296,7 +302,7 @@ describe('invoice distribution api adapter', () => {
};
}
- if (endpoint === '/collected-invoices/economic/v2/compare/bulk' && method === 'POST') {
+ if (endpoint === "/collected-invoices/economic/v2/compare/bulk" && method === "POST") {
const ids = params?.collected_invoice_ids || [];
return {
data: {
@@ -317,11 +323,13 @@ describe('invoice distribution api adapter', () => {
const result = await compareCollectedInvoicesForMonth({
year: 2026,
month: 3,
- mode: 'invoice_total',
+ mode: "invoice_total",
batchSize: 500,
});
- const bulkCalls = SessionUser.request.mock.calls.filter(([endpoint]) => endpoint === '/collected-invoices/economic/v2/compare/bulk');
+ const bulkCalls = SessionUser.request.mock.calls.filter(
+ ([endpoint]) => endpoint === "/collected-invoices/economic/v2/compare/bulk"
+ );
expect(bulkCalls).toHaveLength(2);
expect(bulkCalls[0][2].collected_invoice_ids).toHaveLength(200);
expect(bulkCalls[1][2].collected_invoice_ids).toHaveLength(50);
@@ -329,13 +337,13 @@ describe('invoice distribution api adapter', () => {
expect(result.fallback.usedLegacyCompare).toBe(false);
});
- it('emits incremental compare progress and row chunks while v2 compare runs', async () => {
+ it("emits incremental compare progress and row chunks while v2 compare runs", async () => {
const invoices = [buildInvoice(1), buildInvoice(2), buildInvoice(3), buildInvoice(4), buildInvoice(5)];
const onRows = vi.fn();
const onProgress = vi.fn();
SessionUser.request.mockImplementation(async (endpoint, method, params) => {
- if (endpoint === '/collected-invoices' && method === 'GET') {
+ if (endpoint === "/collected-invoices" && method === "GET") {
return {
data: {
data: invoices,
@@ -343,7 +351,7 @@ describe('invoice distribution api adapter', () => {
};
}
- if (endpoint === '/collected-invoices/economic/v2/compare/bulk' && method === 'POST') {
+ if (endpoint === "/collected-invoices/economic/v2/compare/bulk" && method === "POST") {
const ids = params?.collected_invoice_ids || [];
return {
data: {
@@ -364,7 +372,7 @@ describe('invoice distribution api adapter', () => {
const result = await compareCollectedInvoicesForMonth({
year: 2026,
month: 3,
- mode: 'invoice_total',
+ mode: "invoice_total",
batchSize: 2,
onRows,
onProgress,
@@ -374,20 +382,20 @@ describe('invoice distribution api adapter', () => {
expect(onRows).toHaveBeenCalledTimes(5);
expect(onRows.mock.calls.map(([rows]) => rows.length)).toEqual([1, 1, 1, 1, 1]);
expect(onProgress.mock.calls.map(([progress]) => `${progress.processed}/${progress.total}`)).toEqual([
- '0/5',
- '1/5',
- '2/5',
- '3/5',
- '4/5',
- '5/5',
+ "0/5",
+ "1/5",
+ "2/5",
+ "3/5",
+ "4/5",
+ "5/5",
]);
});
- it('recovers missing bulk compare rows via v2 single endpoint', async () => {
+ it("recovers missing bulk compare rows via v2 single endpoint", async () => {
const invoices = [buildInvoice(101), buildInvoice(102)];
SessionUser.request.mockImplementation(async (endpoint, method, params) => {
- if (endpoint === '/collected-invoices' && method === 'GET') {
+ if (endpoint === "/collected-invoices" && method === "GET") {
return {
data: {
data: invoices,
@@ -395,7 +403,7 @@ describe('invoice distribution api adapter', () => {
};
}
- if (endpoint === '/collected-invoices/economic/v2/compare/bulk' && method === 'POST') {
+ if (endpoint === "/collected-invoices/economic/v2/compare/bulk" && method === "POST") {
return {
data: {
data: {
@@ -403,13 +411,13 @@ describe('invoice distribution api adapter', () => {
compared: 1,
failed: 1,
results: [buildV2CompareResult(101)],
- errors: [{ collected_invoice_id: 102, error: 'temporary mismatch in bulk compare' }],
+ errors: [{ collected_invoice_id: 102, error: "temporary mismatch in bulk compare" }],
},
},
};
}
- if (endpoint === '/collected-invoices/economic/v2/compare' && method === 'GET') {
+ if (endpoint === "/collected-invoices/economic/v2/compare" && method === "GET") {
return {
data: {
data: buildV2CompareResult(102, { mismatch: true, internalTotal: 110, bookedTotal: 90 }),
@@ -423,21 +431,21 @@ describe('invoice distribution api adapter', () => {
const result = await compareCollectedInvoicesForMonth({
year: 2026,
month: 3,
- mode: 'line_by_line',
+ mode: "line_by_line",
});
expect(result.fallback.usedLegacyCompare).toBe(false);
expect(result.fallback.usedSingleCompareFallback).toBe(true);
- expect(result.fallback.compareFallbackReason).toContain('temporary mismatch in bulk compare');
+ expect(result.fallback.compareFallbackReason).toContain("temporary mismatch in bulk compare");
expect(result.rows).toHaveLength(2);
expect(result.rows.some((row) => row.invoiceId === 102)).toBe(true);
});
- it('falls back to legacy compare when v2 bulk compare fails', async () => {
+ it("falls back to legacy compare when v2 bulk compare fails", async () => {
const invoices = [buildInvoice(101), buildInvoice(102)];
SessionUser.request.mockImplementation(async (endpoint, method, params) => {
- if (endpoint === '/collected-invoices' && method === 'GET') {
+ if (endpoint === "/collected-invoices" && method === "GET") {
return {
data: {
data: invoices,
@@ -445,11 +453,11 @@ describe('invoice distribution api adapter', () => {
};
}
- if (endpoint === '/collected-invoices/economic/v2/compare/bulk' && method === 'POST') {
- throw new Error('v2 compare failed');
+ if (endpoint === "/collected-invoices/economic/v2/compare/bulk" && method === "POST") {
+ throw new Error("v2 compare failed");
}
- if (endpoint === '/collected-invoices/economic/compare' && method === 'GET') {
+ if (endpoint === "/collected-invoices/economic/compare" && method === "GET") {
const invoiceId = Number(params?.collected_invoice_id);
return {
data: {
@@ -471,26 +479,28 @@ describe('invoice distribution api adapter', () => {
const result = await compareCollectedInvoicesForMonth({
year: 2026,
month: 3,
- mode: 'invoice_total',
+ mode: "invoice_total",
legacyBatchSize: 10,
});
- const legacyCalls = SessionUser.request.mock.calls.filter(([endpoint]) => endpoint === '/collected-invoices/economic/compare');
+ const legacyCalls = SessionUser.request.mock.calls.filter(
+ ([endpoint]) => endpoint === "/collected-invoices/economic/compare"
+ );
expect(legacyCalls).toHaveLength(2);
expect(result.fallback.usedLegacyCompare).toBe(true);
- expect(result.fallback.compareFallbackReason).toContain('v2 compare failed');
+ expect(result.fallback.compareFallbackReason).toContain("v2 compare failed");
expect(result.rows).toHaveLength(2);
});
- it('creates mismatch fallback row when both bulk and single v2 data are missing', async () => {
+ it("creates mismatch fallback row when both bulk and single v2 data are missing", async () => {
const invoices = [buildInvoice(301)];
SessionUser.request.mockImplementation(async (endpoint, method) => {
- if (endpoint === '/collected-invoices' && method === 'GET') {
+ if (endpoint === "/collected-invoices" && method === "GET") {
return { data: { data: invoices } };
}
- if (endpoint === '/collected-invoices/economic/v2/compare/bulk' && method === 'POST') {
+ if (endpoint === "/collected-invoices/economic/v2/compare/bulk" && method === "POST") {
return {
data: {
data: {
@@ -498,14 +508,14 @@ describe('invoice distribution api adapter', () => {
compared: 0,
failed: 1,
results: [],
- errors: [{ collected_invoice_id: 301, error: 'no bulk result' }],
+ errors: [{ collected_invoice_id: 301, error: "no bulk result" }],
},
},
};
}
- if (endpoint === '/collected-invoices/economic/v2/compare' && method === 'GET') {
- throw new Error('single compare down');
+ if (endpoint === "/collected-invoices/economic/v2/compare" && method === "GET") {
+ throw new Error("single compare down");
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
@@ -514,13 +524,13 @@ describe('invoice distribution api adapter', () => {
const result = await compareCollectedInvoicesForMonth({
year: 2026,
month: 3,
- mode: 'invoice_total',
+ mode: "invoice_total",
});
expect(result.fallback.usedLegacyCompare).toBe(false);
expect(result.fallback.usedSingleCompareFallback).toBe(true);
expect(result.rows).toHaveLength(1);
- expect(result.rows[0].status).toBe('mismatch');
- expect(result.rows[0].warnings.join(' ')).toContain('single compare down');
+ expect(result.rows[0].status).toBe("mismatch");
+ expect(result.rows[0].warnings.join(" ")).toContain("single compare down");
});
});
diff --git a/tests/unit/invoice-distribution-calculations.spec.js b/tests/unit/invoice-distribution-calculations.spec.js
index 396c711b..0f50e70e 100644
--- a/tests/unit/invoice-distribution-calculations.spec.js
+++ b/tests/unit/invoice-distribution-calculations.spec.js
@@ -1,4 +1,4 @@
-import { describe, expect, it } from 'vitest';
+import { describe, expect, it } from "vitest";
import {
buildDistributionQueryState,
getMonthRange,
@@ -13,15 +13,15 @@ import {
aggregateCompareRows,
sortCompareRows,
sortMonthSummaries,
-} from '@/views/dashboards/superUserDashboard/invoiceDistribution/imports/invoiceDistributionCalculations.js';
+} from "@/views/dashboards/superUserDashboard/invoiceDistribution/imports/invoiceDistributionCalculations.js";
-describe('invoice distribution calculations', () => {
- it('builds month ranges from first month to current month', () => {
- const result = getMonthRange(new Date('2026-01-10T00:00:00Z'), new Date('2026-03-20T00:00:00Z'), true);
- expect(result.map((item) => item.key)).toEqual(['2026-03', '2026-02', '2026-01']);
+describe("invoice distribution calculations", () => {
+ it("builds month ranges from first month to current month", () => {
+ const result = getMonthRange(new Date("2026-01-10T00:00:00Z"), new Date("2026-03-20T00:00:00Z"), true);
+ expect(result.map((item) => item.key)).toEqual(["2026-03", "2026-02", "2026-01"]);
});
- it('builds month summary with booked and source split', () => {
+ it("builds month summary with booked and source split", () => {
const summary = buildMonthSummary({
year: 2026,
month: 2,
@@ -68,7 +68,7 @@ describe('invoice distribution calculations', () => {
expect(summary.totalAmount).toBe(160);
});
- it('prefers actually distributed fixed and subscription amounts for summary totals', () => {
+ it("prefers actually distributed fixed and subscription amounts for summary totals", () => {
const summary = buildMonthSummary({
year: 2026,
month: 2,
@@ -76,9 +76,7 @@ describe('invoice distribution calculations', () => {
types: {
all: [
{
- transactions: [
- { amount: 200, booked: true, excluded: false },
- ],
+ transactions: [{ amount: 200, booked: true, excluded: false }],
},
],
},
@@ -115,11 +113,11 @@ describe('invoice distribution calculations', () => {
expect(summary.totalAmount).toBe(330);
});
- it('builds department allocations with percentages and booked estimate', () => {
+ it("builds department allocations with percentages and booked estimate", () => {
const rows = buildDepartmentAllocations({
departments: [
- { id: 1, name: 'Alpha' },
- { id: 2, name: 'Beta' },
+ { id: 1, name: "Alpha" },
+ { id: 2, name: "Beta" },
],
bookedAmount: 300,
fixedDistribution: {
@@ -151,16 +149,16 @@ describe('invoice distribution calculations', () => {
});
expect(rows).toHaveLength(2);
- expect(rows[0].departmentName).toBe('Alpha');
+ expect(rows[0].departmentName).toBe("Alpha");
expect(rows[0].totalAmount).toBe(90);
expect(rows[0].sharePercent).toBe(69.23);
expect(rows[0].estimatedBookedAllocation).toBe(207.69);
- expect(rows[1].departmentName).toBe('Beta');
+ expect(rows[1].departmentName).toBe("Beta");
expect(rows[1].customerPriceAmount).toBe(10);
expect(rows[1].totalAmount).toBe(40);
});
- it('builds department footer totals without adding customer prices to the total column', () => {
+ it("builds department footer totals without adding customer prices to the total column", () => {
const totals = buildDepartmentAllocationFooterTotals([
{
fixedAmount: 120000,
@@ -182,20 +180,20 @@ describe('invoice distribution calculations', () => {
expect(totals.totalAmount).toBe(297000);
});
- it('builds customer rows and respects creation date filter', () => {
+ it("builds customer rows and respects creation date filter", () => {
const rows = buildCustomerAllocationRows({
year: 2026,
month: 2,
- departments: [{ id: 1, name: 'Alpha' }],
+ departments: [{ id: 1, name: "Alpha" }],
fixedDistribution: {
data: [
{
id: 1,
customer_number: 10,
- customer_name: 'Customer A',
+ customer_name: "Customer A",
meta: {
fixed_pricing: {
- created_at: '2026-02-01T00:00:00Z',
+ created_at: "2026-02-01T00:00:00Z",
price: 100,
original_price: 140,
department_totals_relative: { 1: 100 },
@@ -205,10 +203,10 @@ describe('invoice distribution calculations', () => {
{
id: 2,
customer_number: 20,
- customer_name: 'Future Customer',
+ customer_name: "Future Customer",
meta: {
fixed_pricing: {
- created_at: '2026-03-01T00:00:00Z',
+ created_at: "2026-03-01T00:00:00Z",
price: 100,
original_price: 140,
department_totals_relative: { 1: 100 },
@@ -225,11 +223,11 @@ describe('invoice distribution calculations', () => {
{
id: 7,
customer_number: 99,
- customer_name: 'Discount Customer',
- transactions: [{ id: 1, amount: 22, booked: true, excluded: false, date: '2026-02-05T00:00:00Z' }],
+ customer_name: "Discount Customer",
+ transactions: [{ id: 1, amount: 22, booked: true, excluded: false, date: "2026-02-05T00:00:00Z" }],
meta: {
customer_price: {
- created_at: '2026-02-05T00:00:00Z',
+ created_at: "2026-02-05T00:00:00Z",
price: 22,
department_totals_relative: { 1: 22 },
},
@@ -240,12 +238,12 @@ describe('invoice distribution calculations', () => {
});
expect(rows).toHaveLength(2);
- expect(rows[0].customerName).toBe('Customer A');
+ expect(rows[0].customerName).toBe("Customer A");
expect(rows[0].departmentAllocations).toEqual({ Alpha: 100 });
- expect(rows[1].source).toBe('customer_prices');
+ expect(rows[1].source).toBe("customer_prices");
});
- it('builds month-over-month comparison deltas', () => {
+ it("builds month-over-month comparison deltas", () => {
const comparison = buildMonthComparison(
{ totalAmount: 200, bookedAmount: 140, distributionAmount: 60 },
{ totalAmount: 100, bookedAmount: 120, distributionAmount: 30 }
@@ -256,61 +254,61 @@ describe('invoice distribution calculations', () => {
expect(comparison.distributionAmount.delta).toBe(30);
});
- it('normalizes and serializes distribution query state', () => {
+ it("normalizes and serializes distribution query state", () => {
const normalized = normalizeDistributionQueryState({
- compareMonth: '2026-02',
- compareMode: 'line_by_line',
- customerSearch: ' acme ',
- customerSource: 'customer_prices',
- customerDepartment: 'Ops',
- departmentSearch: ' nord ',
+ compareMonth: "2026-02",
+ compareMode: "line_by_line",
+ customerSearch: " acme ",
+ customerSource: "customer_prices",
+ customerDepartment: "Ops",
+ departmentSearch: " nord ",
});
expect(normalized).toEqual({
- compareMonth: '2026-02',
- compareMode: 'line_by_line',
- compareVisibility: 'all',
- customerSearch: 'acme',
- customerSource: 'customer_prices',
- customerDepartment: 'Ops',
- departmentSearch: 'nord',
+ compareMonth: "2026-02",
+ compareMode: "line_by_line",
+ compareVisibility: "all",
+ customerSearch: "acme",
+ customerSource: "customer_prices",
+ customerDepartment: "Ops",
+ departmentSearch: "nord",
});
const serialized = buildDistributionQueryState(normalized);
expect(serialized).toEqual({
- compareMonth: '2026-02',
- compareMode: 'line_by_line',
- customerSearch: 'acme',
- customerSource: 'customer_prices',
- customerDepartment: 'Ops',
- departmentSearch: 'nord',
+ compareMonth: "2026-02",
+ compareMode: "line_by_line",
+ customerSearch: "acme",
+ customerSource: "customer_prices",
+ customerDepartment: "Ops",
+ departmentSearch: "nord",
});
});
- it('sorts month summaries and compare rows for decision-first tables', () => {
+ it("sorts month summaries and compare rows for decision-first tables", () => {
const sortedMonths = sortMonthSummaries(
[
- { key: '2026-01', year: 2026, month: 1, totalAmount: 10 },
- { key: '2026-03', year: 2026, month: 3, totalAmount: 40 },
- { key: '2026-02', year: 2026, month: 2, totalAmount: 20 },
+ { key: "2026-01", year: 2026, month: 1, totalAmount: 10 },
+ { key: "2026-03", year: 2026, month: 3, totalAmount: 40 },
+ { key: "2026-02", year: 2026, month: 2, totalAmount: 20 },
],
- 'month',
- 'desc'
+ "month",
+ "desc"
);
- expect(sortedMonths.map((row) => row.key)).toEqual(['2026-03', '2026-02', '2026-01']);
+ expect(sortedMonths.map((row) => row.key)).toEqual(["2026-03", "2026-02", "2026-01"]);
- const sortedTotals = sortMonthSummaries(sortedMonths, 'total', 'asc');
+ const sortedTotals = sortMonthSummaries(sortedMonths, "total", "asc");
expect(sortedTotals.map((row) => row.totalAmount)).toEqual([10, 20, 40]);
const sortedCompare = sortCompareRows([
- { invoiceId: 3, status: 'ok', warningCount: 0, difference: 0 },
- { invoiceId: 2, status: 'ok', warningCount: 2, difference: 4 },
- { invoiceId: 1, status: 'mismatch', warningCount: 1, difference: 10 },
+ { invoiceId: 3, status: "ok", warningCount: 0, difference: 0 },
+ { invoiceId: 2, status: "ok", warningCount: 2, difference: 4 },
+ { invoiceId: 1, status: "mismatch", warningCount: 1, difference: 10 },
]);
expect(sortedCompare.map((row) => row.invoiceId)).toEqual([1, 2, 3]);
});
- it('builds source composition percentages for KPI cues', () => {
+ it("builds source composition percentages for KPI cues", () => {
const composition = buildSourceComposition({
fixedPricingAmount: 60,
subscriptionAmount: 40,
@@ -322,9 +320,9 @@ describe('invoice distribution calculations', () => {
expect(composition.customerPricePercent).toBe(16.67);
});
- it('normalizes compare payloads for both modes and aggregates', () => {
+ it("normalizes compare payloads for both modes and aggregates", () => {
const okRow = normalizeCompareResult(
- { id: 1, customer_name: 'A', total_net_amount: 100 },
+ { id: 1, customer_name: "A", total_net_amount: 100 },
{
internal_total: 100,
booked_total: 100,
@@ -332,26 +330,26 @@ describe('invoice distribution calculations', () => {
warnings: [],
order_ids: [1, 2],
},
- 'invoice_total'
+ "invoice_total"
);
const mismatchRow = normalizeCompareResult(
- { id: 2, customer_name: 'B', total_net_amount: 100 },
+ { id: 2, customer_name: "B", total_net_amount: 100 },
{
internal_total: 100,
booked_total: 90,
difference: 10,
- warnings: ['Line mismatch'],
+ warnings: ["Line mismatch"],
order_ids: [3],
},
- 'line_by_line'
+ "line_by_line"
);
const v2Row = normalizeCompareResult(
- { id: 3, customer_name: 'C', total_net_amount: 200 },
+ { id: 3, customer_name: "C", total_net_amount: 200 },
{
collected_invoice_id: 3,
- warnings: ['Top-level warning'],
+ warnings: ["Top-level warning"],
details: {
order_ids: [10, 20],
internal: {
@@ -363,7 +361,7 @@ describe('invoice distribution calculations', () => {
},
},
customer: {
- name: 'Customer V2',
+ name: "Customer V2",
},
},
comparison: {
@@ -372,12 +370,12 @@ describe('invoice distribution calculations', () => {
},
targets: {
booked: {
- status: 'partial_mismatch',
+ status: "partial_mismatch",
overall_match: false,
totals: {
target_net_total: 180,
},
- mismatch_reasons: ['department_total_mismatch'],
+ mismatch_reasons: ["department_total_mismatch"],
lines: {
summary: {
mismatch_count: 1,
@@ -386,19 +384,19 @@ describe('invoice distribution calculations', () => {
},
},
},
- warnings: ['comparison warning'],
+ warnings: ["comparison warning"],
},
},
- 'line_by_line'
+ "line_by_line"
);
- expect(okRow.status).toBe('ok');
- expect(mismatchRow.status).toBe('mismatch');
+ expect(okRow.status).toBe("ok");
+ expect(mismatchRow.status).toBe("mismatch");
expect(mismatchRow.lineMismatchCount).toBe(1);
- expect(v2Row.status).toBe('mismatch');
+ expect(v2Row.status).toBe("mismatch");
expect(v2Row.bookedTotal).toBe(180);
expect(v2Row.difference).toBe(20);
- expect(v2Row.mismatchReasons).toContain('department_total_mismatch');
+ expect(v2Row.mismatchReasons).toContain("department_total_mismatch");
expect(v2Row.lineMismatchCount).toBe(1);
const aggregate = aggregateCompareRows([okRow, mismatchRow, v2Row]);
diff --git a/tests/unit/invoice-distribution-i18n.spec.js b/tests/unit/invoice-distribution-i18n.spec.js
index b6d80f12..f8d1501c 100644
--- a/tests/unit/invoice-distribution-i18n.spec.js
+++ b/tests/unit/invoice-distribution-i18n.spec.js
@@ -1,19 +1,19 @@
-import { readFileSync } from 'node:fs';
-import { join } from 'node:path';
-import { describe, expect, it } from 'vitest';
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
const root = process.cwd();
const locales = {
- da: JSON.parse(readFileSync(join(root, 'src/i18n/locales/da.json'), 'utf8')),
- en: JSON.parse(readFileSync(join(root, 'src/i18n/locales/en.json'), 'utf8')),
- sv: JSON.parse(readFileSync(join(root, 'src/i18n/locales/sv.json'), 'utf8')),
- de: JSON.parse(readFileSync(join(root, 'src/i18n/locales/de.json'), 'utf8')),
- no: JSON.parse(readFileSync(join(root, 'src/i18n/locales/no.json'), 'utf8')),
+ da: JSON.parse(readFileSync(join(root, "src/i18n/locales/da.json"), "utf8")),
+ en: JSON.parse(readFileSync(join(root, "src/i18n/locales/en.json"), "utf8")),
+ sv: JSON.parse(readFileSync(join(root, "src/i18n/locales/sv.json"), "utf8")),
+ de: JSON.parse(readFileSync(join(root, "src/i18n/locales/de.json"), "utf8")),
+ no: JSON.parse(readFileSync(join(root, "src/i18n/locales/no.json"), "utf8")),
};
-const flattenKeys = (value, prefix = '') => {
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
+const flattenKeys = (value, prefix = "") => {
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
return [prefix];
}
@@ -25,16 +25,15 @@ const flattenKeys = (value, prefix = '') => {
return keys;
};
-describe('invoice distribution i18n coverage', () => {
- it('defines distribution namespace in every locale', () => {
+describe("invoice distribution i18n coverage", () => {
+ it("defines distribution namespace in every locale", () => {
for (const [locale, data] of Object.entries(locales)) {
expect(data.superuser_invoice_distribution, `missing namespace in ${locale}`).toBeTruthy();
}
});
- it('keeps the same translation key set across locales', () => {
- const baselineKeys = flattenKeys(locales.en.superuser_invoice_distribution)
- .sort();
+ it("keeps the same translation key set across locales", () => {
+ const baselineKeys = flattenKeys(locales.en.superuser_invoice_distribution).sort();
for (const [locale, data] of Object.entries(locales)) {
const localeKeys = flattenKeys(data.superuser_invoice_distribution).sort();
@@ -42,43 +41,43 @@ describe('invoice distribution i18n coverage', () => {
}
});
- it('includes required distribution keys used by views', () => {
+ it("includes required distribution keys used by views", () => {
const requiredKeys = [
- 'tab_label',
- 'overview.title',
- 'overview.subtitle',
- 'monthly_page.title',
- 'monthly_page.toolbar_month',
- 'actions.reload',
- 'actions.open_month',
- 'actions.try_again',
- 'actions.reset_filters',
- 'tabs.overview',
- 'tabs.departments',
- 'tabs.customers',
- 'tabs.compare',
- 'compare.mode',
- 'compare.modes.invoice_total',
- 'compare.modes.line_by_line',
- 'compare.run',
- 'compare.run_short',
- 'compare.progress',
- 'compare.warning_details',
- 'compare.status_label',
- 'compare.status.ok',
- 'compare.status.mismatch',
- 'errors.overview_load_failed',
- 'errors.month_load_failed',
- 'errors.compare_failed',
- 'warnings.legacy_distribution_fallback',
- 'warnings.legacy_distribution_month',
- 'warnings.legacy_compare_fallback',
- 'sections.trend',
- 'metrics.customer_prices',
- 'table.customer_prices',
- 'table.source_mix',
- 'aria.month_table_caption',
- 'aria.compare_table_caption'
+ "tab_label",
+ "overview.title",
+ "overview.subtitle",
+ "monthly_page.title",
+ "monthly_page.toolbar_month",
+ "actions.reload",
+ "actions.open_month",
+ "actions.try_again",
+ "actions.reset_filters",
+ "tabs.overview",
+ "tabs.departments",
+ "tabs.customers",
+ "tabs.compare",
+ "compare.mode",
+ "compare.modes.invoice_total",
+ "compare.modes.line_by_line",
+ "compare.run",
+ "compare.run_short",
+ "compare.progress",
+ "compare.warning_details",
+ "compare.status_label",
+ "compare.status.ok",
+ "compare.status.mismatch",
+ "errors.overview_load_failed",
+ "errors.month_load_failed",
+ "errors.compare_failed",
+ "warnings.legacy_distribution_fallback",
+ "warnings.legacy_distribution_month",
+ "warnings.legacy_compare_fallback",
+ "sections.trend",
+ "metrics.customer_prices",
+ "table.customer_prices",
+ "table.source_mix",
+ "aria.month_table_caption",
+ "aria.compare_table_caption",
];
const availableKeys = new Set(flattenKeys(locales.en.superuser_invoice_distribution));
diff --git a/tests/unit/invoicing-billing-period-calculations.spec.js b/tests/unit/invoicing-billing-period-calculations.spec.js
index f39df7c0..2a3d5d0b 100644
--- a/tests/unit/invoicing-billing-period-calculations.spec.js
+++ b/tests/unit/invoicing-billing-period-calculations.spec.js
@@ -38,7 +38,12 @@ describe("invoicing billing period calculations", () => {
it("falls back to transaction total when fixed pricing is not included", () => {
const value = getCustomerNetAmount(
- { transactions: [{ amount: 100, excluded: false }, { amount: 35.2, excluded: false }] },
+ {
+ transactions: [
+ { amount: 100, excluded: false },
+ { amount: 35.2, excluded: false },
+ ],
+ },
{ includeFixedPricing: false, includeTransactions: true, includeVehicleSubscriptions: false },
{ meta: { fixed_pricing: { price: 999 } } }
);
@@ -73,4 +78,3 @@ describe("invoicing billing period calculations", () => {
expect(totals.notBooked).toBe(170);
});
});
-
diff --git a/tests/unit/invoicing-period-queue-refresh.behavior.spec.js b/tests/unit/invoicing-period-queue-refresh.behavior.spec.js
index 30ac1a86..c46496e9 100644
--- a/tests/unit/invoicing-period-queue-refresh.behavior.spec.js
+++ b/tests/unit/invoicing-period-queue-refresh.behavior.spec.js
@@ -60,33 +60,42 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
},
}));
-vi.mock("@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportDates.vue", () => ({
- dates: {
- variables: {
- start: startDateRef,
- end: endDateRef,
+vi.mock(
+ "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportDates.vue",
+ () => ({
+ dates: {
+ variables: {
+ start: startDateRef,
+ end: endDateRef,
+ },
+ computed: {
+ isEntireMonth: computed(() => true),
+ },
},
- computed: {
- isEntireMonth: computed(() => true),
- },
- },
-}));
+ })
+);
-vi.mock("@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportView.vue", () => ({
- view: {
- variables: {
- sharedVariables: sharedVariablesRef,
- currentView: currentViewRef,
- availableViewNames: availableViewNamesRef,
+vi.mock(
+ "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportView.vue",
+ () => ({
+ view: {
+ variables: {
+ sharedVariables: sharedVariablesRef,
+ currentView: currentViewRef,
+ availableViewNames: availableViewNamesRef,
+ },
},
- },
-}));
+ })
+);
-vi.mock("@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue", () => ({
- invoiceQueue: {
- periodRefreshSignal: periodRefreshSignalRef,
- },
-}));
+vi.mock(
+ "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue",
+ () => ({
+ invoiceQueue: {
+ periodRefreshSignal: periodRefreshSignalRef,
+ },
+ })
+);
import Right from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Right.vue";
diff --git a/tests/unit/invoicing-period-queue-state.behavior.spec.js b/tests/unit/invoicing-period-queue-state.behavior.spec.js
index c532dfa7..d05af21c 100644
--- a/tests/unit/invoicing-period-queue-state.behavior.spec.js
+++ b/tests/unit/invoicing-period-queue-state.behavior.spec.js
@@ -3,14 +3,7 @@ import { computed, nextTick } from "vue";
import { mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
-const {
- sharedVariablesRef,
- currentViewRef,
- startDateRef,
- endDateRef,
- queueRef,
- inProgressRef,
-} = vi.hoisted(() => {
+const { sharedVariablesRef, currentViewRef, startDateRef, endDateRef, queueRef, inProgressRef } = vi.hoisted(() => {
const { ref } = require("vue");
return {
@@ -78,44 +71,53 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
},
}));
-vi.mock("@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportView.vue", () => ({
- view: {
- variables: {
- currentView: currentViewRef,
- sharedVariables: sharedVariablesRef,
- },
- computed: {
- componentName: computed(() => currentViewRef.value),
- },
- functions: {
- filterExcluded: (transactions = []) => transactions.filter((transaction) => !transaction?.excluded),
- getCustomerViewTotalNetAmount: (customer) => {
- const transactions = Array.isArray(customer?.transactions) ? customer.transactions : [];
- return transactions.reduce((sum, transaction) => sum + Number(transaction?.amount ?? 0), 0);
+vi.mock(
+ "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportView.vue",
+ () => ({
+ view: {
+ variables: {
+ currentView: currentViewRef,
+ sharedVariables: sharedVariablesRef,
+ },
+ computed: {
+ componentName: computed(() => currentViewRef.value),
+ },
+ functions: {
+ filterExcluded: (transactions = []) => transactions.filter((transaction) => !transaction?.excluded),
+ getCustomerViewTotalNetAmount: (customer) => {
+ const transactions = Array.isArray(customer?.transactions) ? customer.transactions : [];
+ return transactions.reduce((sum, transaction) => sum + Number(transaction?.amount ?? 0), 0);
+ },
},
},
- },
-}));
+ })
+);
-vi.mock("@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportDates.vue", () => ({
- dates: {
- variables: {
- start: startDateRef,
- end: endDateRef,
+vi.mock(
+ "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportDates.vue",
+ () => ({
+ dates: {
+ variables: {
+ start: startDateRef,
+ end: endDateRef,
+ },
},
- },
-}));
+ })
+);
-vi.mock("@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue", () => ({
- invoiceQueue: {
- addInvoiceCollectionsToQueue: vi.fn(),
- processInvoiceCollectionQueue: vi.fn(),
- invoiceCollectionQueue: queueRef,
- invoiceCollectionQueueInProgress: inProgressRef,
- invoiceCollectionQueueFailed: require("vue").ref([]),
- invoiceCollectionQueueSuccess: require("vue").ref([]),
- },
-}));
+vi.mock(
+ "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue",
+ () => ({
+ invoiceQueue: {
+ addInvoiceCollectionsToQueue: vi.fn(),
+ processInvoiceCollectionQueue: vi.fn(),
+ invoiceCollectionQueue: queueRef,
+ invoiceCollectionQueueInProgress: inProgressRef,
+ invoiceCollectionQueueFailed: require("vue").ref([]),
+ invoiceCollectionQueueSuccess: require("vue").ref([]),
+ },
+ })
+);
import InvoicingBillingPeriodViewAll from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue";
diff --git a/tests/unit/my-wash-start.spec.js b/tests/unit/my-wash-start.spec.js
index e822e661..f1b9f9da 100644
--- a/tests/unit/my-wash-start.spec.js
+++ b/tests/unit/my-wash-start.spec.js
@@ -12,9 +12,7 @@ const mocks = vi.hoisted(() => {
name: "Copenhagen",
address: "Main street 1",
self_serve_enabled: true,
- lanes: [
- { id: 7, name: "7", status: "AVAILABLE", products: [2], machine_available: true },
- ],
+ lanes: [{ id: 7, name: "7", status: "AVAILABLE", products: [2], machine_available: true }],
},
};
const guestDepartments = { value: [nearestDepartment.value] };
@@ -107,9 +105,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
type: {
options: vi.fn(async () => {
mocks.fetchVehicleTypeOptions();
- return [
- { id: 2, name: "Truck", product: { id: 2, description: "Truck" } },
- ];
+ return [{ id: 2, name: "Truck", product: { id: 2, description: "Truck" } }];
}),
},
},
@@ -320,9 +316,7 @@ describe("MyWashStart", () => {
name: "Copenhagen",
address: "Main street 1",
self_serve_enabled: true,
- lanes: [
- { id: 7, name: "7", status: "AVAILABLE", products: [2], machine_available: true },
- ],
+ lanes: [{ id: 7, name: "7", status: "AVAILABLE", products: [2], machine_available: true }],
};
mocks.guestDepartments.value = [mocks.nearestDepartment.value];
mocks.doesCurrentDepartmentSelectionHaveSelfServeEnabled.value = true;
diff --git a/tests/unit/navigation-menu-department.spec.js b/tests/unit/navigation-menu-department.spec.js
index 969e94a2..52821d7b 100644
--- a/tests/unit/navigation-menu-department.spec.js
+++ b/tests/unit/navigation-menu-department.spec.js
@@ -158,4 +158,3 @@ describe("NavigationMenuDepartment", () => {
expect(wrapper.get("[data-testid='mock-button-label']").text()).toBe("Taastrup");
});
});
-
diff --git a/tests/unit/navigation-menu-items-admin-contract.spec.js b/tests/unit/navigation-menu-items-admin-contract.spec.js
index 5b69e42a..80a7c328 100644
--- a/tests/unit/navigation-menu-items-admin-contract.spec.js
+++ b/tests/unit/navigation-menu-items-admin-contract.spec.js
@@ -1,20 +1,20 @@
-import { readFileSync } from 'node:fs';
-import { join } from 'node:path';
-import { describe, expect, it } from 'vitest';
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
const root = process.cwd();
const source = readFileSync(
- join(root, 'src/components/models/navigation/items/NavigationMenuItemsAdmin.vue'),
- 'utf8'
-).replace(/\r\n/g, '\n');
+ join(root, "src/components/models/navigation/items/NavigationMenuItemsAdmin.vue"),
+ "utf8"
+).replace(/\r\n/g, "\n");
-describe('NavigationMenuItemsAdmin contract', () => {
- it('derives department id from synced route context without router import', () => {
+describe("NavigationMenuItemsAdmin contract", () => {
+ it("derives department id from synced route context without router import", () => {
expect(source).not.toContain('import { router } from "@/router.js";');
- expect(source).toContain('const routeContext = ref(');
- expect(source).toContain('const syncAdminNavigationRoute = (route: any) => {');
- expect(source).toContain('const currentRoute = routeContext.value;');
- expect(source).toContain('currentRoute?.params?.departmentId');
- expect(source).toContain('currentRoute?.path?.match(/^\\/admin\\/(\\d+)(?:\\/|$)/)');
+ expect(source).toContain("const routeContext = ref(");
+ expect(source).toContain("const syncAdminNavigationRoute = (route: any) => {");
+ expect(source).toContain("const currentRoute = routeContext.value;");
+ expect(source).toContain("currentRoute?.params?.departmentId");
+ expect(source).toContain("currentRoute?.path?.match(/^\\/admin\\/(\\d+)(?:\\/|$)/)");
});
});
diff --git a/tests/unit/orders-items.spec.js b/tests/unit/orders-items.spec.js
index 928f550f..d6c66266 100644
--- a/tests/unit/orders-items.spec.js
+++ b/tests/unit/orders-items.spec.js
@@ -1,6 +1,6 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from "vitest";
-vi.mock('axios', () => {
+vi.mock("axios", () => {
return {
default: {
get: vi.fn(),
@@ -11,54 +11,54 @@ vi.mock('axios', () => {
};
});
-import axios from 'axios';
-import { createOrderItem } from '@/components/shop/OrdersItems.vue';
+import axios from "axios";
+import { createOrderItem } from "@/components/shop/OrdersItems.vue";
-describe('createOrderItem', () => {
+describe("createOrderItem", () => {
beforeEach(() => {
localStorage.clear();
axios.post.mockReset();
});
- it('returns null when token is missing', () => {
+ it("returns null when token is missing", () => {
const result = createOrderItem(123, 10, 1);
expect(result).toBeNull();
expect(axios.post).not.toHaveBeenCalled();
});
- it('rejects and does not call API when order id is missing or invalid', async () => {
- localStorage.setItem('token', 'test-token');
+ it("rejects and does not call API when order id is missing or invalid", async () => {
+ localStorage.setItem("token", "test-token");
- const invalidOrderIds = [null, undefined, 0, '0', -1, 'abc'];
+ const invalidOrderIds = [null, undefined, 0, "0", -1, "abc"];
for (const invalidOrderId of invalidOrderIds) {
- await expect(createOrderItem(invalidOrderId, 10, 1)).rejects.toThrow('Order ID is required');
+ await expect(createOrderItem(invalidOrderId, 10, 1)).rejects.toThrow("Order ID is required");
}
expect(axios.post).not.toHaveBeenCalled();
});
- it('calls API with normalized numeric order id when valid', async () => {
- localStorage.setItem('token', 'test-token');
+ it("calls API with normalized numeric order id when valid", async () => {
+ localStorage.setItem("token", "test-token");
axios.post.mockResolvedValue({ data: { success: true } });
- await createOrderItem('51207', 77, 2, 99, 'note', 1234);
+ await createOrderItem("51207", 77, 2, 99, "note", 1234);
expect(axios.post).toHaveBeenCalledTimes(1);
expect(axios.post).toHaveBeenCalledWith(
- expect.stringContaining('/order/items'),
+ expect.stringContaining("/order/items"),
expect.objectContaining({
order_id: 51207,
product_id: 77,
quantity: 2,
related_item_id: 99,
- notes: 'note',
+ notes: "note",
price: 1234,
}),
expect.objectContaining({
headers: expect.objectContaining({
- Authorization: 'Bearer test-token',
+ Authorization: "Bearer test-token",
}),
})
);
diff --git a/tests/unit/origin-migration.spec.js b/tests/unit/origin-migration.spec.js
index d319e7ee..dbe16056 100644
--- a/tests/unit/origin-migration.spec.js
+++ b/tests/unit/origin-migration.spec.js
@@ -62,10 +62,7 @@ describe("origin migration route guard", () => {
describe("origin migration redirect url", () => {
it("preserves path, query, and hash on migration origin", () => {
- const redirectUrl = buildMigrationUrl(
- "https://truckwash.io",
- "/user/bookings?tab=open#latest",
- );
+ const redirectUrl = buildMigrationUrl("https://truckwash.io", "/user/bookings?tab=open#latest");
expect(redirectUrl).toBe("https://truckwash.io/user/bookings?tab=open#latest");
});
diff --git a/tests/unit/paginated-list-export.spec.js b/tests/unit/paginated-list-export.spec.js
index cc38a94b..3512c1dd 100644
--- a/tests/unit/paginated-list-export.spec.js
+++ b/tests/unit/paginated-list-export.spec.js
@@ -1,22 +1,22 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-import axios from 'axios';
-import { usePaginatedList } from '@/components/pagination/paginatedList.vue';
-import * as tableExportService from '@/services/TableExcelExportService.js';
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import axios from "axios";
+import { usePaginatedList } from "@/components/pagination/paginatedList.vue";
+import * as tableExportService from "@/services/TableExcelExportService.js";
-vi.mock('axios', () => ({
+vi.mock("axios", () => ({
default: {
get: vi.fn(),
},
}));
-describe('paginated list excel export flow', () => {
+describe("paginated list excel export flow", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
- localStorage.setItem('token', 'test-token');
+ localStorage.setItem("token", "test-token");
});
- it('fetches all pages for export and preserves filters/search/order/query params', async () => {
+ it("fetches all pages for export and preserves filters/search/order/query params", async () => {
axios.get
.mockResolvedValueOnce({
data: {
@@ -38,11 +38,11 @@ describe('paginated list excel export flow', () => {
});
const list = usePaginatedList();
- list.setEndpoint('/orders', false);
- list.search('AB12345', false);
- list.setFilter('department_id', 7, false);
- list.setOrder('created_at', 'desc');
- list.setAdditionalQueryParameters({ include_archived: '1' });
+ list.setEndpoint("/orders", false);
+ list.search("AB12345", false);
+ list.setFilter("department_id", 7, false);
+ list.setOrder("created_at", "desc");
+ list.setAdditionalQueryParameters({ include_archived: "1" });
list.setMetaItemsPerPage(2, false);
const rows = await list.fetchAllPagesForExport();
@@ -51,71 +51,71 @@ describe('paginated list excel export flow', () => {
expect(axios.get).toHaveBeenCalledTimes(3);
expect(axios.get).toHaveBeenNthCalledWith(
1,
- expect.stringContaining('/orders'),
+ expect.stringContaining("/orders"),
expect.objectContaining({
params: expect.objectContaining({
page: 1,
limit: 2,
- search: 'AB12345',
- filters: 'department_id:7',
- order: 'created_at:desc',
- include_archived: '1',
+ search: "AB12345",
+ filters: "department_id:7",
+ order: "created_at:desc",
+ include_archived: "1",
}),
})
);
expect(axios.get).toHaveBeenNthCalledWith(
3,
- expect.stringContaining('/orders'),
+ expect.stringContaining("/orders"),
expect.objectContaining({
params: expect.objectContaining({
page: 3,
limit: 2,
- search: 'AB12345',
- filters: 'department_id:7',
- order: 'created_at:desc',
- include_archived: '1',
+ search: "AB12345",
+ filters: "department_id:7",
+ order: "created_at:desc",
+ include_archived: "1",
}),
})
);
});
- it('supports export transforms for client-side list shaping', async () => {
+ it("supports export transforms for client-side list shaping", async () => {
axios.get.mockResolvedValue({
data: {
data: [
- { id: 1, status: 'active' },
- { id: 2, status: 'archived' },
+ { id: 1, status: "active" },
+ { id: 2, status: "archived" },
],
meta: { pagination: { page: 1, per_page: 100, total: 2 } },
},
});
const list = usePaginatedList();
- list.setEndpoint('/users', false);
- list.setExportTransform((rows) => rows.filter((row) => row.status === 'active'));
+ list.setEndpoint("/users", false);
+ list.setExportTransform((rows) => rows.filter((row) => row.status === "active"));
const rows = await list.fetchAllPagesForExport();
- expect(rows).toEqual([{ id: 1, status: 'active' }]);
+ expect(rows).toEqual([{ id: 1, status: "active" }]);
});
- it('exports to excel and resets export loading state', async () => {
- const exportSpy = vi.spyOn(tableExportService, 'exportRowsToExcel').mockReturnValue('users.xlsx');
+ it("exports to excel and resets export loading state", async () => {
+ const exportSpy = vi.spyOn(tableExportService, "exportRowsToExcel").mockReturnValue("users.xlsx");
const list = usePaginatedList();
- list.setEndpoint('/users', false);
+ list.setEndpoint("/users", false);
const result = await list.exportToExcel({
- rows: [{ id: 1, name: 'Alice' }],
- filename: 'users',
- sheetName: 'Users',
+ rows: [{ id: 1, name: "Alice" }],
+ filename: "users",
+ sheetName: "Users",
});
expect(result).toBe(true);
expect(exportSpy).toHaveBeenCalledWith(
- [{ id: 1, name: 'Alice' }],
+ [{ id: 1, name: "Alice" }],
expect.objectContaining({
- filename: 'users',
- sheetName: 'Users',
+ filename: "users",
+ sheetName: "Users",
})
);
expect(list.isExporting.value).toBe(false);
diff --git a/tests/unit/pos-department-process.spec.js b/tests/unit/pos-department-process.spec.js
index 539598bc..ca0af711 100644
--- a/tests/unit/pos-department-process.spec.js
+++ b/tests/unit/pos-department-process.spec.js
@@ -1,10 +1,10 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
getOrderItems: vi.fn(),
}));
-vi.mock('axios', () => ({
+vi.mock("axios", () => ({
default: {
get: vi.fn(),
post: vi.fn(),
@@ -13,42 +13,42 @@ vi.mock('axios', () => ({
},
}));
-vi.mock('@/components/numberplatescanners/Scans.vue', () => ({
+vi.mock("@/components/numberplatescanners/Scans.vue", () => ({
getScansDepartmentPagination: vi.fn(),
}));
-vi.mock('vue-router', () => ({
+vi.mock("vue-router", () => ({
useRouter: () => ({
currentRoute: {
value: {
params: {
- departmentId: '1',
+ departmentId: "1",
},
},
},
}),
}));
-vi.mock('@/components/request/HandleGlobalError.vue', () => ({
+vi.mock("@/components/request/HandleGlobalError.vue", () => ({
parseError: vi.fn(),
clearErrors: vi.fn(),
}));
-vi.mock('@/components/shop/CustomerNotes.vue', () => ({
+vi.mock("@/components/shop/CustomerNotes.vue", () => ({
getNotes: vi.fn(),
createNote: vi.fn(),
deleteNote: vi.fn(),
}));
-vi.mock('@/components/shop/OrdersItems.vue', () => ({
+vi.mock("@/components/shop/OrdersItems.vue", () => ({
getOrderItems: mocks.getOrderItems,
}));
-vi.mock('@/components/shop/CustomerAttributes.vue', () => ({
+vi.mock("@/components/shop/CustomerAttributes.vue", () => ({
getAttributes: vi.fn(),
}));
-vi.mock('@/components/session/token/SessionUser.vue', () => ({
+vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
request: vi.fn(),
hasPermission: vi.fn(() => true),
@@ -60,12 +60,12 @@ vi.mock('@/components/session/token/SessionUser.vue', () => ({
objects: {
global: {
language: {
- possible_duplicates: 'Possible duplicates',
+ possible_duplicates: "Possible duplicates",
},
},
orders: {
meta: {
- endpoint: '/orders',
+ endpoint: "/orders",
},
functions: {
mark_as_completed: vi.fn(),
@@ -80,23 +80,23 @@ vi.mock('@/components/session/token/SessionUser.vue', () => ({
},
order_bookings: {
meta: {
- endpoint: '/order_bookings',
+ endpoint: "/order_bookings",
},
},
vehicles: {
meta: {
- endpoint: '/vehicles',
+ endpoint: "/vehicles",
},
},
},
},
}));
-vi.mock('@/components/session/authenticatedRequest.vue', () => ({
+vi.mock("@/components/session/authenticatedRequest.vue", () => ({
authenticatedRequest: vi.fn(),
}));
-vi.mock('sweetalert2', () => ({
+vi.mock("sweetalert2", () => ({
default: {
fire: vi.fn(() => Promise.resolve({ isConfirmed: false })),
showValidationMessage: vi.fn(),
@@ -104,38 +104,38 @@ vi.mock('sweetalert2', () => ({
},
}));
-import { SessionUser } from '@/components/session/token/SessionUser.vue';
+import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {
clearStoredPosOrderId,
loadOrderItems,
order_id,
order_items,
restoreStoredPosOrderId,
-} from '@/components/shop/POSDepartmentProcess.vue';
+} from "@/components/shop/POSDepartmentProcess.vue";
-describe('POSDepartmentProcess.loadOrderItems', () => {
+describe("POSDepartmentProcess.loadOrderItems", () => {
beforeEach(() => {
order_id.value = null;
order_items.value = [];
mocks.getOrderItems.mockReset();
});
- it('skips loading when order id is missing or invalid', async () => {
+ it("skips loading when order id is missing or invalid", async () => {
order_id.value = null;
await expect(loadOrderItems()).resolves.toEqual([]);
order_id.value = 0;
await expect(loadOrderItems()).resolves.toEqual([]);
- order_id.value = 'abc';
+ order_id.value = "abc";
await expect(loadOrderItems()).resolves.toEqual([]);
expect(mocks.getOrderItems).not.toHaveBeenCalled();
expect(order_items.value).toEqual([]);
});
- it('loads order items for a valid order id', async () => {
- order_id.value = '9201';
+ it("loads order items for a valid order id", async () => {
+ order_id.value = "9201";
mocks.getOrderItems.mockResolvedValue({
data: {
data: [{ id: 1, price: 10, quantity: 1 }],
@@ -148,7 +148,7 @@ describe('POSDepartmentProcess.loadOrderItems', () => {
expect(order_items.value).toEqual([{ id: 1, price: 10, quantity: 1 }]);
});
- it('falls back to empty list when response payload is malformed', async () => {
+ it("falls back to empty list when response payload is malformed", async () => {
order_id.value = 9201;
mocks.getOrderItems.mockResolvedValue({
data: {
@@ -163,24 +163,24 @@ describe('POSDepartmentProcess.loadOrderItems', () => {
});
});
-describe('POSDepartmentProcess.restoreStoredPosOrderId', () => {
+describe("POSDepartmentProcess.restoreStoredPosOrderId", () => {
beforeEach(() => {
clearStoredPosOrderId();
order_id.value = null;
SessionUser.objects.orders.get.single.mockReset();
});
- it('clears invalid stored order ids before validation', async () => {
- localStorage.setItem('pos_order_id', 'invalid');
+ it("clears invalid stored order ids before validation", async () => {
+ localStorage.setItem("pos_order_id", "invalid");
await expect(restoreStoredPosOrderId()).resolves.toBeNull();
- expect(localStorage.getItem('pos_order_id')).toBeNull();
+ expect(localStorage.getItem("pos_order_id")).toBeNull();
expect(SessionUser.objects.orders.get.single).not.toHaveBeenCalled();
});
- it('restores a valid stored order id when order context matches', async () => {
- localStorage.setItem('pos_order_id', '51211');
+ it("restores a valid stored order id when order context matches", async () => {
+ localStorage.setItem("pos_order_id", "51211");
SessionUser.objects.orders.get.single.mockResolvedValue({
id: 51211,
customer_id: 12345679,
@@ -197,12 +197,12 @@ describe('POSDepartmentProcess.restoreStoredPosOrderId', () => {
).resolves.toBe(51211);
expect(order_id.value).toBe(51211);
- expect(localStorage.getItem('pos_order_id')).toBe('51211');
+ expect(localStorage.getItem("pos_order_id")).toBe("51211");
expect(SessionUser.objects.orders.get.single).toHaveBeenCalledWith(51211);
});
- it('clears stored order id when customer or department does not match', async () => {
- localStorage.setItem('pos_order_id', '51211');
+ it("clears stored order id when customer or department does not match", async () => {
+ localStorage.setItem("pos_order_id", "51211");
order_id.value = 51211;
SessionUser.objects.orders.get.single.mockResolvedValue({
id: 51211,
@@ -220,13 +220,13 @@ describe('POSDepartmentProcess.restoreStoredPosOrderId', () => {
).resolves.toBeNull();
expect(order_id.value).toBeNull();
- expect(localStorage.getItem('pos_order_id')).toBeNull();
+ expect(localStorage.getItem("pos_order_id")).toBeNull();
});
- it('clears stored order id when order lookup fails', async () => {
- localStorage.setItem('pos_order_id', '51211');
+ it("clears stored order id when order lookup fails", async () => {
+ localStorage.setItem("pos_order_id", "51211");
order_id.value = 51211;
- SessionUser.objects.orders.get.single.mockRejectedValue(new Error('Order not found'));
+ SessionUser.objects.orders.get.single.mockRejectedValue(new Error("Order not found"));
await expect(
restoreStoredPosOrderId({
@@ -235,6 +235,6 @@ describe('POSDepartmentProcess.restoreStoredPosOrderId', () => {
).resolves.toBeNull();
expect(order_id.value).toBeNull();
- expect(localStorage.getItem('pos_order_id')).toBeNull();
+ expect(localStorage.getItem("pos_order_id")).toBeNull();
});
});
diff --git a/tests/unit/pos-last-scanned-license-plates.spec.js b/tests/unit/pos-last-scanned-license-plates.spec.js
index f4cdb99a..dd3f2009 100644
--- a/tests/unit/pos-last-scanned-license-plates.spec.js
+++ b/tests/unit/pos-last-scanned-license-plates.spec.js
@@ -236,9 +236,7 @@ describe("PosLastScannedLicensePlatesV2", () => {
return Promise.resolve({
data: {
- data: [
- { id: 501, reg_1: "AB12345", reg_2: "" },
- ],
+ data: [{ id: 501, reg_1: "AB12345", reg_2: "" }],
},
});
});
@@ -249,13 +247,15 @@ describe("PosLastScannedLicensePlatesV2", () => {
return Promise.resolve(buildLookupResponse());
}
- return Promise.resolve(buildLookupResponse({
- make: "SCANIA",
- model: "R500",
- variant: "Highline",
- type: "Lastbil",
- use: "Godstransport",
- }));
+ return Promise.resolve(
+ buildLookupResponse({
+ make: "SCANIA",
+ model: "R500",
+ variant: "Highline",
+ type: "Lastbil",
+ use: "Godstransport",
+ })
+ );
});
});
diff --git a/tests/unit/pos-route-state.spec.js b/tests/unit/pos-route-state.spec.js
index eddacac5..90e522e2 100644
--- a/tests/unit/pos-route-state.spec.js
+++ b/tests/unit/pos-route-state.spec.js
@@ -1,31 +1,34 @@
-import { describe, expect, it, vi } from 'vitest';
+import { describe, expect, it, vi } from "vitest";
-import { applyPosRouteSearch, parsePosRouteSearch } from '@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js';
+import {
+ applyPosRouteSearch,
+ parsePosRouteSearch,
+} from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js";
-describe('parsePosRouteSearch', () => {
- it('extracts valid positive integers from search params', () => {
- expect(parsePosRouteSearch('?id=9201&customer_id=12345&step=2')).toEqual({
+describe("parsePosRouteSearch", () => {
+ it("extracts valid positive integers from search params", () => {
+ expect(parsePosRouteSearch("?id=9201&customer_id=12345&step=2")).toEqual({
orderId: 9201,
customerId: 12345,
step: 2,
});
});
- it('ignores invalid or missing params', () => {
- expect(parsePosRouteSearch('?id=abc&customer_id=0&step=-1')).toEqual({
+ it("ignores invalid or missing params", () => {
+ expect(parsePosRouteSearch("?id=abc&customer_id=0&step=-1")).toEqual({
orderId: null,
customerId: null,
step: null,
});
- expect(parsePosRouteSearch('')).toEqual({
+ expect(parsePosRouteSearch("")).toEqual({
orderId: null,
customerId: null,
step: null,
});
});
- it('does not treat customer_id as id', () => {
- expect(parsePosRouteSearch('?customer_id=12345&step=1')).toEqual({
+ it("does not treat customer_id as id", () => {
+ expect(parsePosRouteSearch("?customer_id=12345&step=1")).toEqual({
orderId: null,
customerId: 12345,
step: 1,
@@ -33,8 +36,8 @@ describe('parsePosRouteSearch', () => {
});
});
-describe('applyPosRouteSearch', () => {
- it('loads order data only when a valid order id is present', () => {
+describe("applyPosRouteSearch", () => {
+ it("loads order data only when a valid order id is present", () => {
const handlers = {
setOrderId: vi.fn(),
loadOrderItems: vi.fn(),
@@ -42,7 +45,7 @@ describe('applyPosRouteSearch', () => {
searchAndSelectCustomer: vi.fn(),
};
- const result = applyPosRouteSearch('?id=9201&customer_id=12345&step=2', handlers);
+ const result = applyPosRouteSearch("?id=9201&customer_id=12345&step=2", handlers);
expect(result).toEqual({
orderId: 9201,
@@ -52,10 +55,10 @@ describe('applyPosRouteSearch', () => {
expect(handlers.setOrderId).toHaveBeenCalledWith(9201);
expect(handlers.loadOrderItems).toHaveBeenCalledTimes(1);
expect(handlers.setStep).toHaveBeenCalledWith(2);
- expect(handlers.searchAndSelectCustomer).toHaveBeenCalledWith(12345);
+ expect(handlers.searchAndSelectCustomer).toHaveBeenCalledWith(12345, { forceRefresh: true });
});
- it('does not call order handlers when only customer_id exists', () => {
+ it("does not call order handlers when only customer_id exists", () => {
const handlers = {
setOrderId: vi.fn(),
loadOrderItems: vi.fn(),
@@ -63,7 +66,7 @@ describe('applyPosRouteSearch', () => {
searchAndSelectCustomer: vi.fn(),
};
- const result = applyPosRouteSearch('?customer_id=12345&step=1', handlers);
+ const result = applyPosRouteSearch("?customer_id=12345&step=1", handlers);
expect(result).toEqual({
orderId: null,
@@ -73,6 +76,6 @@ describe('applyPosRouteSearch', () => {
expect(handlers.setOrderId).not.toHaveBeenCalled();
expect(handlers.loadOrderItems).not.toHaveBeenCalled();
expect(handlers.setStep).toHaveBeenCalledWith(1);
- expect(handlers.searchAndSelectCustomer).toHaveBeenCalledWith(12345);
+ expect(handlers.searchAndSelectCustomer).toHaveBeenCalledWith(12345, { forceRefresh: true });
});
});
diff --git a/tests/unit/request-queue-progress.spec.js b/tests/unit/request-queue-progress.spec.js
index c31d74ce..dbe7d9aa 100644
--- a/tests/unit/request-queue-progress.spec.js
+++ b/tests/unit/request-queue-progress.spec.js
@@ -10,7 +10,7 @@ import {
__resetRequestQueueForTests,
enqueueRequest,
reportComponentMissingPermission,
- requestQueueState
+ requestQueueState,
} from "@/services/requestQueue.js";
const flushMicrotasks = async () => {
@@ -64,7 +64,10 @@ describe("RequestQueueProgress", () => {
localStorage.clear();
__resetRequestQueueForTests();
__configureRequestQueueForTests({ maxConcurrentGet: 1, maxConcurrentOther: 1, spacingMs: 0 });
- vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, status: 200 })));
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async () => ({ ok: true, status: 200 }))
+ );
resetSessionUserState();
});
@@ -159,9 +162,7 @@ describe("RequestQueueProgress", () => {
await triggerShiftTriplePress();
expect(closeEventListener).toHaveBeenCalled();
- expect(
- closeEventListener.mock.calls.some(([event]) => event?.type === "system-search:close")
- ).toBe(true);
+ expect(closeEventListener.mock.calls.some(([event]) => event?.type === "system-search:close")).toBe(true);
window.removeEventListener("system-search:close", closeEventListener);
});
@@ -216,27 +217,21 @@ describe("RequestQueueProgress", () => {
const first = createDeferred();
const second = createDeferred();
- const requestOne = enqueueRequest(
- () => first.promise,
- {
- method: "POST",
- url: "/runtime-metrics-a",
- requestData: {
- headers: { "X-Trace": "a" },
- data: { payload: "first" },
- },
- }
- );
- const requestTwo = enqueueRequest(
- () => second.promise,
- {
- method: "GET",
- url: "/runtime-metrics-b",
- requestData: {
- params: { page: 1, limit: 10 },
- },
- }
- );
+ const requestOne = enqueueRequest(() => first.promise, {
+ method: "POST",
+ url: "/runtime-metrics-a",
+ requestData: {
+ headers: { "X-Trace": "a" },
+ data: { payload: "first" },
+ },
+ });
+ const requestTwo = enqueueRequest(() => second.promise, {
+ method: "GET",
+ url: "/runtime-metrics-b",
+ requestData: {
+ params: { page: 1, limit: 10 },
+ },
+ });
await flushManyMicrotasks();
await triggerShiftTriplePress();
await flushManyMicrotasks();
@@ -327,20 +322,18 @@ describe("RequestQueueProgress", () => {
await flushManyMicrotasks();
const requests = Array.from({ length: 30 }, (_, index) =>
- enqueueRequest(
- async () => ({ status: 200, data: { index } }),
- {
- method: "GET",
- url: `/bulk-recursive-check-${index}`,
- }
- )
+ enqueueRequest(async () => ({ status: 200, data: { index } }), {
+ method: "GET",
+ url: `/bulk-recursive-check-${index}`,
+ })
);
await Promise.all(requests);
await flushManyMicrotasks(30);
- const hasRecursiveError = [...errorSpy.mock.calls, ...warnSpy.mock.calls]
- .some((call) => call.join(" ").includes("Maximum recursive updates exceeded"));
+ const hasRecursiveError = [...errorSpy.mock.calls, ...warnSpy.mock.calls].some((call) =>
+ call.join(" ").includes("Maximum recursive updates exceeded")
+ );
expect(hasRecursiveError).toBe(false);
});
@@ -349,7 +342,7 @@ describe("RequestQueueProgress", () => {
errorHistoryLimit: 1,
missingPermissionsLimit: 1,
retryByStatusCode: {},
- payloadMaxChars: 8000
+ payloadMaxChars: 8000,
});
const wrapper = mount(RequestQueueProgress);
@@ -357,13 +350,13 @@ describe("RequestQueueProgress", () => {
async () => {
throw {
message: "First failure",
- response: { status: 500, data: { reason: "first" } }
+ response: { status: 500, data: { reason: "first" } },
};
},
{
method: "POST",
url: "/first-error",
- requestData: { data: { requestBody: "first" } }
+ requestData: { data: { requestBody: "first" } },
}
).catch((error) => error);
@@ -377,18 +370,18 @@ describe("RequestQueueProgress", () => {
success: false,
data: {
message: "Missing permission(s)",
- permissions: ["department_notification_sms_get"]
+ permissions: ["department_notification_sms_get"],
},
meta: [],
- includes: []
- }
- }
+ includes: [],
+ },
+ },
};
},
{
method: "PATCH",
url: "/second-error",
- requestData: { data: { requestBody: "second" } }
+ requestData: { data: { requestBody: "second" } },
}
).catch((error) => error);
@@ -426,7 +419,9 @@ describe("RequestQueueProgress", () => {
await flushManyMicrotasks();
expect(requestQueueState.missingPermissions.length).toBe(0);
- expect(wrapper.get("[data-testid='request-queue-missing-permissions-box']").text()).toContain("No missing permissions detected");
+ expect(wrapper.get("[data-testid='request-queue-missing-permissions-box']").text()).toContain(
+ "No missing permissions detected"
+ );
});
it("stays visible after errors occur", async () => {
@@ -437,13 +432,13 @@ describe("RequestQueueProgress", () => {
async () => {
throw {
message: "Failure should keep panel visible",
- response: { status: 500, data: { detail: "boom" } }
+ response: { status: 500, data: { detail: "boom" } },
};
},
{
method: "DELETE",
url: "/error-visible",
- requestData: { data: { id: 123 } }
+ requestData: { data: { id: 123 } },
}
).catch((error) => error);
@@ -662,7 +657,9 @@ describe("RequestQueueProgress", () => {
await triggerShiftTriplePress();
await flushManyMicrotasks();
- const actionButton = wrapper.get("[data-testid='request-queue-permission-action-department_notification_sms_get-GET']");
+ const actionButton = wrapper.get(
+ "[data-testid='request-queue-permission-action-department_notification_sms_get-GET']"
+ );
await actionButton.trigger("click");
await flushManyMicrotasks();
diff --git a/tests/unit/self-serve-alignment.spec.js b/tests/unit/self-serve-alignment.spec.js
index cd4fb880..0810e6c0 100644
--- a/tests/unit/self-serve-alignment.spec.js
+++ b/tests/unit/self-serve-alignment.spec.js
@@ -8,8 +8,12 @@ describe("self-serve alignment", () => {
it("registers reusable machine types and lane relay/machine_type wiring", () => {
const sessionUserSource = readSource("src/components/session/token/SessionUser.vue");
const laneSource = readSource("src/components/session/token/SessionUser/Objects/DepartmentLanes.vue");
- const laneAdminViewSource = readSource("src/views/dashboards/departmentDashboard/modules/wash-lanes/DepartmentWashLane.vue");
- const laneSuperUserViewSource = readSource("src/views/dashboards/superUserDashboard/department/lanes/DepartmentLane.vue");
+ const laneAdminViewSource = readSource(
+ "src/views/dashboards/departmentDashboard/modules/wash-lanes/DepartmentWashLane.vue"
+ );
+ const laneSuperUserViewSource = readSource(
+ "src/views/dashboards/superUserDashboard/department/lanes/DepartmentLane.vue"
+ );
expect(sessionUserSource).toMatch(/self_serve_machine_types\s*:\s*SelfServeMachineTypes\s*,/);
expect(sessionUserSource).not.toMatch(/get\s+self_serve_machine_types\s*\(/);
@@ -17,7 +21,7 @@ describe("self-serve alignment", () => {
expect(laneSource).toContain("relay_machine_cleaner_id:");
expect(laneSource).toContain("machine_type_id:");
expect(laneSource).toContain("SessionUser.objects.self_serve_machine_types.get.all()");
- expect(laneAdminViewSource).toContain(":set-machine-type-filter=\"lane.machine_type_id || null\"");
+ expect(laneAdminViewSource).toContain(':set-machine-type-filter="lane.machine_type_id || null"');
expect(laneAdminViewSource).toContain("lane.relay_machine_program_picker_id");
expect(laneAdminViewSource).toContain("lane.relay_machine_cleaner_id");
expect(laneSuperUserViewSource).toContain("lane.relay_machine_program_picker_id");
@@ -26,7 +30,9 @@ describe("self-serve alignment", () => {
it("normalizes self-serve config reads and submits merged config payloads", () => {
const configClientSource = readSource("src/components/session/token/superUser/modules/selfserve/Config.vue");
- const configViewSource = readSource("src/views/dashboards/superUserDashboard/configuration/ConfigurationSelfServe.vue");
+ const configViewSource = readSource(
+ "src/views/dashboards/superUserDashboard/configuration/ConfigurationSelfServe.vue"
+ );
expect(configClientSource).toContain("const normalizeEntries =");
expect(configClientSource).toContain("const toConfigPayload =");
@@ -39,9 +45,9 @@ describe("self-serve alignment", () => {
it("limits condition rules to documented object types and rule types", () => {
const rulesSource = readSource("src/components/session/token/SessionUser/Objects/SelfServeConditionRules.vue");
- expect(rulesSource).toContain("{ id: \"question\", name: \"Spørgsmål\" }");
- expect(rulesSource).toContain("{ id: \"condition\", name: \"Betingelse\" }");
- expect(rulesSource).not.toContain("{ id: \"task\"");
+ expect(rulesSource).toContain('{ id: "question", name: "Spørgsmål" }');
+ expect(rulesSource).toContain('{ id: "condition", name: "Betingelse" }');
+ expect(rulesSource).not.toContain('{ id: "task"');
expect(rulesSource).toContain("IS_TRUE_OR_ANY_TRUE");
expect(rulesSource).not.toContain("IS_NOT_SET");
expect(rulesSource).not.toContain("IS_FALSE_OR_ANY_FALSE");
@@ -64,7 +70,9 @@ describe("self-serve alignment", () => {
});
it("keeps studio condition/rule action buttons on a single line", () => {
- const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
+ const studioSource = readSource(
+ "src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
+ );
const alignedActionCells = studioSource.match(/class="has-text-right studio-action-cell"/g) || [];
const actionGroups = studioSource.match(/class="studio-action-buttons"/g) || [];
diff --git a/tests/unit/self-serve-connectivity-relay.spec.js b/tests/unit/self-serve-connectivity-relay.spec.js
index aed4dd2e..4dc62bdf 100644
--- a/tests/unit/self-serve-connectivity-relay.spec.js
+++ b/tests/unit/self-serve-connectivity-relay.spec.js
@@ -39,24 +39,9 @@ describe("self-serve connectivity relay api wrappers", () => {
await relays.get.status.PROGRAM_PICKER(5);
await relays.get.status.CLEANER(5);
- expect(mocks.request).toHaveBeenNthCalledWith(
- 1,
- statusEndpoints.MACHINE,
- "GET",
- { lane_id: 5 }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 2,
- statusEndpoints.PROGRAM_PICKER,
- "GET",
- { lane_id: 5 }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 3,
- statusEndpoints.CLEANER,
- "GET",
- { lane_id: 5 }
- );
+ expect(mocks.request).toHaveBeenNthCalledWith(1, statusEndpoints.MACHINE, "GET", { lane_id: 5 });
+ expect(mocks.request).toHaveBeenNthCalledWith(2, statusEndpoints.PROGRAM_PICKER, "GET", { lane_id: 5 });
+ expect(mocks.request).toHaveBeenNthCalledWith(3, statusEndpoints.CLEANER, "GET", { lane_id: 5 });
});
it("fetches all relay statuses and returns keyed responses", async () => {
@@ -75,42 +60,17 @@ describe("self-serve connectivity relay api wrappers", () => {
await relays.set.byRelay(relayKinds.PROGRAM_PICKER, 8, false);
await relays.set.byRelay(relayKinds.CLEANER, 8, true);
- expect(mocks.request).toHaveBeenNthCalledWith(
- 1,
- setEndpoints.MACHINE,
- "POST",
- { lane_id: 8, on: true }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 2,
- setEndpoints.PROGRAM_PICKER,
- "POST",
- { lane_id: 8, on: false }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 3,
- setEndpoints.CLEANER,
- "POST",
- { lane_id: 8, on: true }
- );
+ expect(mocks.request).toHaveBeenNthCalledWith(1, setEndpoints.MACHINE, "POST", { lane_id: 8, on: true });
+ expect(mocks.request).toHaveBeenNthCalledWith(2, setEndpoints.PROGRAM_PICKER, "POST", { lane_id: 8, on: false });
+ expect(mocks.request).toHaveBeenNthCalledWith(3, setEndpoints.CLEANER, "POST", { lane_id: 8, on: true });
});
it("gets lane status with and without a lane id filter", async () => {
await lane.get.status();
await lane.get.status("9");
- expect(mocks.request).toHaveBeenNthCalledWith(
- 1,
- "/modules/self-serve/lane/status",
- "GET",
- {}
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 2,
- "/modules/self-serve/lane/status",
- "GET",
- { lane_id: 9 }
- );
+ expect(mocks.request).toHaveBeenNthCalledWith(1, "/modules/self-serve/lane/status", "GET", {});
+ expect(mocks.request).toHaveBeenNthCalledWith(2, "/modules/self-serve/lane/status", "GET", { lane_id: 9 });
});
it("executes lane command helpers with normalized lane ids", async () => {
@@ -118,36 +78,21 @@ describe("self-serve connectivity relay api wrappers", () => {
await lane.command.STOP(11);
await lane.command.execute(11, "RESET", { license_plate: "BB54321" });
- expect(mocks.request).toHaveBeenNthCalledWith(
- 1,
- "/modules/self-serve/lane/command",
- "POST",
- {
- lane_id: 11,
- command: "START",
- customer_number: 123,
- license_plate: "AA12345",
- }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 2,
- "/modules/self-serve/lane/command",
- "POST",
- {
- lane_id: 11,
- command: "STOP",
- }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 3,
- "/modules/self-serve/lane/command",
- "POST",
- {
- lane_id: 11,
- command: "RESET",
- license_plate: "BB54321",
- }
- );
+ expect(mocks.request).toHaveBeenNthCalledWith(1, "/modules/self-serve/lane/command", "POST", {
+ lane_id: 11,
+ command: "START",
+ customer_number: 123,
+ license_plate: "AA12345",
+ });
+ expect(mocks.request).toHaveBeenNthCalledWith(2, "/modules/self-serve/lane/command", "POST", {
+ lane_id: 11,
+ command: "STOP",
+ });
+ expect(mocks.request).toHaveBeenNthCalledWith(3, "/modules/self-serve/lane/command", "POST", {
+ lane_id: 11,
+ command: "RESET",
+ license_plate: "BB54321",
+ });
});
it("normalizes machine force action payloads", async () => {
@@ -156,78 +101,44 @@ describe("self-serve connectivity relay api wrappers", () => {
await lane.force.machine.disable(4, " cd67890 ");
await lane.force.machine.disable(4, " ");
- expect(mocks.request).toHaveBeenNthCalledWith(
- 1,
- "/modules/self-serve/lane/force/machine/enable",
- "POST",
- {
- lane_id: 4,
- duration: 13,
- license_plate: "AB12345",
- }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 2,
- "/modules/self-serve/lane/force/machine/enable",
- "POST",
- {
- lane_id: 4,
- duration: 0,
- }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 3,
- "/modules/self-serve/lane/force/machine/disable",
- "POST",
- {
- lane_id: 4,
- license_plate: "CD67890",
- }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 4,
- "/modules/self-serve/lane/force/machine/disable",
- "POST",
- {
- lane_id: 4,
- }
- );
+ expect(mocks.request).toHaveBeenNthCalledWith(1, "/modules/self-serve/lane/force/machine/enable", "POST", {
+ lane_id: 4,
+ duration: 13,
+ license_plate: "AB12345",
+ });
+ expect(mocks.request).toHaveBeenNthCalledWith(2, "/modules/self-serve/lane/force/machine/enable", "POST", {
+ lane_id: 4,
+ duration: 0,
+ });
+ expect(mocks.request).toHaveBeenNthCalledWith(3, "/modules/self-serve/lane/force/machine/disable", "POST", {
+ lane_id: 4,
+ license_plate: "CD67890",
+ });
+ expect(mocks.request).toHaveBeenNthCalledWith(4, "/modules/self-serve/lane/force/machine/disable", "POST", {
+ lane_id: 4,
+ });
});
it("opens entrance and exit gates with the expected payload", async () => {
await lane.gate.open("12", "ENTRANCE");
await lane.gate.open(12, "EXIT");
- expect(mocks.request).toHaveBeenNthCalledWith(
- 1,
- "/modules/self-serve/lane/gate/open",
- "POST",
- {
- lane_id: 12,
- gate: "ENTRANCE",
- }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 2,
- "/modules/self-serve/lane/gate/open",
- "POST",
- {
- lane_id: 12,
- gate: "EXIT",
- }
- );
+ expect(mocks.request).toHaveBeenNthCalledWith(1, "/modules/self-serve/lane/gate/open", "POST", {
+ lane_id: 12,
+ gate: "ENTRANCE",
+ });
+ expect(mocks.request).toHaveBeenNthCalledWith(2, "/modules/self-serve/lane/gate/open", "POST", {
+ lane_id: 12,
+ gate: "EXIT",
+ });
});
it("fetches in-progress wash details for a lane", async () => {
await lane.wash.inProgress("13");
- expect(mocks.request).toHaveBeenCalledWith(
- "/modules/self-serve/lane/wash/in-progress",
- "GET",
- {
- lane_id: 13,
- }
- );
+ expect(mocks.request).toHaveBeenCalledWith("/modules/self-serve/lane/wash/in-progress", "GET", {
+ lane_id: 13,
+ });
});
it("rejects invalid lane ids for relay and lane commands", async () => {
diff --git a/tests/unit/self-serve-machine-connectivity.spec.js b/tests/unit/self-serve-machine-connectivity.spec.js
index 10c3347e..cacca056 100644
--- a/tests/unit/self-serve-machine-connectivity.spec.js
+++ b/tests/unit/self-serve-machine-connectivity.spec.js
@@ -38,11 +38,7 @@ const inProgressEndpoint = "/modules/self-serve/lane/wash/in-progress";
const buildRelayStatus = (
laneId,
relay,
- {
- relayId = `${relay.toLowerCase()}-${laneId}`,
- online = true,
- on = false,
- } = {}
+ { relayId = `${relay.toLowerCase()}-${laneId}`, online = true, on = false } = {}
) => ({
lane_id: laneId,
relay,
@@ -53,12 +49,7 @@ const buildRelayStatus = (
const buildInProgressDetails = (
laneId,
- {
- inProgress = false,
- session = null,
- customer = null,
- vehicle = null,
- } = {}
+ { inProgress = false, session = null, customer = null, vehicle = null } = {}
) => ({
lane_id: laneId,
in_progress: inProgress,
@@ -156,34 +147,19 @@ describe("self-serve machine connectivity store", () => {
await connectivity.forceDisableMachine("cd67890");
await connectivity.stopWash();
- expect(mocks.request).toHaveBeenNthCalledWith(
- 1,
- "/modules/self-serve/lane/force/machine/enable",
- "POST",
- {
- lane_id: laneId,
- duration: 120,
- license_plate: "AB12345",
- }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 2,
- "/modules/self-serve/lane/force/machine/disable",
- "POST",
- {
- lane_id: laneId,
- license_plate: "CD67890",
- }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 3,
- "/modules/self-serve/lane/command",
- "POST",
- {
- lane_id: laneId,
- command: "STOP",
- }
- );
+ expect(mocks.request).toHaveBeenNthCalledWith(1, "/modules/self-serve/lane/force/machine/enable", "POST", {
+ lane_id: laneId,
+ duration: 120,
+ license_plate: "AB12345",
+ });
+ expect(mocks.request).toHaveBeenNthCalledWith(2, "/modules/self-serve/lane/force/machine/disable", "POST", {
+ lane_id: laneId,
+ license_plate: "CD67890",
+ });
+ expect(mocks.request).toHaveBeenNthCalledWith(3, "/modules/self-serve/lane/command", "POST", {
+ lane_id: laneId,
+ command: "STOP",
+ });
});
it("runs entrance/exit gate actions through shared connectivity helpers", async () => {
@@ -195,24 +171,14 @@ describe("self-serve machine connectivity store", () => {
await connectivity.openEntranceGate();
await connectivity.openExitGate();
- expect(mocks.request).toHaveBeenNthCalledWith(
- 1,
- "/modules/self-serve/lane/gate/open",
- "POST",
- {
- lane_id: laneId,
- gate: "ENTRANCE",
- }
- );
- expect(mocks.request).toHaveBeenNthCalledWith(
- 2,
- "/modules/self-serve/lane/gate/open",
- "POST",
- {
- lane_id: laneId,
- gate: "EXIT",
- }
- );
+ expect(mocks.request).toHaveBeenNthCalledWith(1, "/modules/self-serve/lane/gate/open", "POST", {
+ lane_id: laneId,
+ gate: "ENTRANCE",
+ });
+ expect(mocks.request).toHaveBeenNthCalledWith(2, "/modules/self-serve/lane/gate/open", "POST", {
+ lane_id: laneId,
+ gate: "EXIT",
+ });
});
it("fetches in-progress wash details through shared connectivity helpers", async () => {
@@ -232,11 +198,7 @@ describe("self-serve machine connectivity store", () => {
const connectivity = useMachineConnectivity(laneId);
const details = await connectivity.fetchInProgressDetails();
- expect(mocks.request).toHaveBeenCalledWith(
- inProgressEndpoint,
- "GET",
- { lane_id: laneId }
- );
+ expect(mocks.request).toHaveBeenCalledWith(inProgressEndpoint, "GET", { lane_id: laneId });
expect(details).toMatchObject({
lane_id: laneId,
in_progress: true,
diff --git a/tests/unit/self-serve-machine-department-self-serve.spec.js b/tests/unit/self-serve-machine-department-self-serve.spec.js
index 6a058ff8..4b3ee85b 100644
--- a/tests/unit/self-serve-machine-department-self-serve.spec.js
+++ b/tests/unit/self-serve-machine-department-self-serve.spec.js
@@ -39,44 +39,46 @@ const MockSwitch = defineComponent({
},
emits: ["update:model-value"],
setup(props, { emit }) {
- return () => h("button", {
- "data-testid": "department-self-serve-switch",
- "data-state": props.modelValue ? "on" : "off",
- "disabled": props.disabled,
- onClick: () => emit("update:model-value", !props.modelValue),
- });
+ return () =>
+ h("button", {
+ "data-testid": "department-self-serve-switch",
+ "data-state": props.modelValue ? "on" : "off",
+ disabled: props.disabled,
+ onClick: () => emit("update:model-value", !props.modelValue),
+ });
},
});
-const mountMachine = (machineOverrides = {}) => mount(SelfServeMachine, {
- props: {
- machine: {
- id: 1,
- department: 12,
- name: "Lane 1",
- status: "AVAILABLE",
- ...machineOverrides,
+const mountMachine = (machineOverrides = {}) =>
+ mount(SelfServeMachine, {
+ props: {
+ machine: {
+ id: 1,
+ department: 12,
+ name: "Lane 1",
+ status: "AVAILABLE",
+ ...machineOverrides,
+ },
},
- },
- global: {
- stubs: {
- WhiteBoxCard: {
- template: `
+ global: {
+ stubs: {
+ WhiteBoxCard: {
+ template: `
`,
+ },
+ SelfServeMachineStatus: { template: "" },
+ SelfServeMachineRelayControls: { template: "" },
+ SelfServeMachineControls: { template: "" },
+ BSwitch: MockSwitch,
+ "b-switch": MockSwitch,
},
- SelfServeMachineStatus: { template: "" },
- SelfServeMachineRelayControls: { template: "" },
- SelfServeMachineControls: { template: "" },
- BSwitch: MockSwitch,
- "b-switch": MockSwitch,
},
- },
-});
+ });
describe("SelfServeMachine department self-serve management", () => {
let consoleErrorSpy;
diff --git a/tests/unit/self-serve-machine-relay-controls.spec.js b/tests/unit/self-serve-machine-relay-controls.spec.js
index 560e3d36..1e5d77a0 100644
--- a/tests/unit/self-serve-machine-relay-controls.spec.js
+++ b/tests/unit/self-serve-machine-relay-controls.spec.js
@@ -47,25 +47,21 @@ const buildConnectivity = () => ({
stopPolling: mocks.stopPolling,
});
-vi.mock(
- "@/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue",
- () => ({
- parseRelayStatus: () => "MAINTENANCE",
- relayKinds: {
- MACHINE: "MACHINE",
- PROGRAM_PICKER: "PROGRAM_PICKER",
- CLEANER: "CLEANER",
- },
- useMachineConnectivity: mocks.useMachineConnectivity,
- })
-);
+vi.mock("@/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue", () => ({
+ parseRelayStatus: () => "MAINTENANCE",
+ relayKinds: {
+ MACHINE: "MACHINE",
+ PROGRAM_PICKER: "PROGRAM_PICKER",
+ CLEANER: "CLEANER",
+ },
+ useMachineConnectivity: mocks.useMachineConnectivity,
+}));
vi.mock("@/composables/departmentOutsideGateCapabilities.js", () => ({
getDepartmentOutsideGateCapabilities: mocks.getDepartmentOutsideGateCapabilities,
}));
-import SelfServeMachineRelayControls
- from "@/views/dashboards/superUserDashboard/selfserve/displays/machine/SelfServeMachineRelayControls.vue";
+import SelfServeMachineRelayControls from "@/views/dashboards/superUserDashboard/selfserve/displays/machine/SelfServeMachineRelayControls.vue";
const flushMicrotasks = async () => {
await Promise.resolve();
diff --git a/tests/unit/self-serve-pagination-machine-scope.spec.js b/tests/unit/self-serve-pagination-machine-scope.spec.js
index c6135998..0bce2b00 100644
--- a/tests/unit/self-serve-pagination-machine-scope.spec.js
+++ b/tests/unit/self-serve-pagination-machine-scope.spec.js
@@ -6,22 +6,30 @@ const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePa
describe("self-serve pagination machine scope filtering", () => {
it("enforces machine-scoped vehicle type matching in tasks pagination", () => {
- const source = readSource("src/components/displays/pagination/models/DepartmentDashboard/SelfServeTasksPagination.vue");
+ const source = readSource(
+ "src/components/displays/pagination/models/DepartmentDashboard/SelfServeTasksPagination.vue"
+ );
expect(source).toContain("if (vehicleTypeFilter) {");
expect(source).toContain("if (itemMachineType !== 0) {");
expect(source).toContain("if (itemMachineType !== vehicleTypeFilter) {");
expect(source).toContain("} else if (itemProduct !== 0 && itemProduct !== vehicleTypeFilter) {");
- expect(source).not.toContain("if (vehicleTypeFilter && itemMachineType === 0 && itemProduct !== 0 && itemProduct !== vehicleTypeFilter)");
+ expect(source).not.toContain(
+ "if (vehicleTypeFilter && itemMachineType === 0 && itemProduct !== 0 && itemProduct !== vehicleTypeFilter)"
+ );
});
it("enforces machine-scoped vehicle type matching in conditions pagination", () => {
- const source = readSource("src/components/displays/pagination/models/DepartmentDashboard/SelfServeConditionsPagination.vue");
+ const source = readSource(
+ "src/components/displays/pagination/models/DepartmentDashboard/SelfServeConditionsPagination.vue"
+ );
expect(source).toContain("if (vehicleTypeFilter) {");
expect(source).toContain("if (itemMachineType !== 0) {");
expect(source).toContain("if (itemMachineType !== vehicleTypeFilter) {");
expect(source).toContain("} else if (itemProduct !== 0 && itemProduct !== vehicleTypeFilter) {");
- expect(source).not.toContain("if (vehicleTypeFilter && itemMachineType === 0 && itemProduct !== 0 && itemProduct !== vehicleTypeFilter)");
+ expect(source).not.toContain(
+ "if (vehicleTypeFilter && itemMachineType === 0 && itemProduct !== 0 && itemProduct !== vehicleTypeFilter)"
+ );
});
});
diff --git a/tests/unit/self-serve-questions-step.spec.js b/tests/unit/self-serve-questions-step.spec.js
index 2a7f3e8d..0ac8476d 100644
--- a/tests/unit/self-serve-questions-step.spec.js
+++ b/tests/unit/self-serve-questions-step.spec.js
@@ -8,18 +8,12 @@ describe("SelfServeQuestionsStep", () => {
isLoading: false,
loadingQuestionId: null,
loadingAnswerValue: null,
- visibleQuestions: [
- { id: 11, question: "Is the trailer closed?" },
- ],
+ visibleQuestions: [{ id: 11, question: "Is the trailer closed?" }],
answers: { 11: true },
editAnswers: true,
showDebug: true,
- conditions: [
- { id: 21, name: "Closed trailer" },
- ],
- rules: [
- { id: 31, condition_id: 21, type: "IS_TRUE", name: "Question 11 is true" },
- ],
+ conditions: [{ id: 21, name: "Closed trailer" }],
+ rules: [{ id: 31, condition_id: 21, type: "IS_TRUE", name: "Question 11 is true" }],
evaluateCondition: () => true,
evaluateRule: () => true,
isQuestionVisible: () => true,
diff --git a/tests/unit/self-serve-services.spec.js b/tests/unit/self-serve-services.spec.js
index d069cbc9..a8ebebe4 100644
--- a/tests/unit/self-serve-services.spec.js
+++ b/tests/unit/self-serve-services.spec.js
@@ -2,24 +2,21 @@ import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
-const filePath = join(
- process.cwd(),
- "src/components/session/token/SessionUser/Objects/SelfServeTasks.vue"
-);
+const filePath = join(process.cwd(), "src/components/session/token/SessionUser/Objects/SelfServeTasks.vue");
const source = readFileSync(filePath, "utf8");
describe("self-serve services regression", () => {
it("keeps services column as multi-select", () => {
expect(source).toContain("services:");
- expect(source).toContain("type: \"multi-select\"");
+ expect(source).toContain('type: "multi-select"');
});
it("keeps MACHINE as a selectable service option", () => {
- expect(source).toContain("{ id: \"MACHINE\", name: \"MACHINE\" }");
+ expect(source).toContain('{ id: "MACHINE", name: "MACHINE" }');
});
it("keeps fallback display for empty services", () => {
- expect(source).toContain("return \"Ingen\";");
- expect(source).toContain("return services.join(\", \");");
+ expect(source).toContain('return "Ingen";');
+ expect(source).toContain('return services.join(", ");');
});
});
diff --git a/tests/unit/self-serve-studio-condition-scope.spec.js b/tests/unit/self-serve-studio-condition-scope.spec.js
index 227c565e..4ac64276 100644
--- a/tests/unit/self-serve-studio-condition-scope.spec.js
+++ b/tests/unit/self-serve-studio-condition-scope.spec.js
@@ -6,18 +6,22 @@ const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePa
describe("self-serve studio condition scope", () => {
it("renders legacy scope selectors for condition lane and vehicle", () => {
- const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
+ const studioSource = readSource(
+ "src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
+ );
expect(studioSource).toContain("v-if=\"conditionForm.scope_mode === 'legacy'\"");
- expect(studioSource).toContain("v-model=\"conditionForm.lane\"");
- expect(studioSource).toContain("v-model=\"conditionForm.product\"");
+ expect(studioSource).toContain('v-model="conditionForm.lane"');
+ expect(studioSource).toContain('v-model="conditionForm.product"');
expect(studioSource).toContain("Select lane");
expect(studioSource).toContain("Select vehicle type");
- expect(studioSource).toContain("v-for=\"entry in lanes\"");
+ expect(studioSource).toContain('v-for="entry in lanes"');
});
it("guards condition submission for both machine type and legacy scopes", () => {
- const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
+ const studioSource = readSource(
+ "src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
+ );
expect(studioSource).toContain("Machine type is required for this scope.");
expect(studioSource).toContain("Lane is required for Department/Lane/Vehicle scope.");
diff --git a/tests/unit/self-serve-studio-question-scope.spec.js b/tests/unit/self-serve-studio-question-scope.spec.js
index f8cd4e5e..2dd7bae2 100644
--- a/tests/unit/self-serve-studio-question-scope.spec.js
+++ b/tests/unit/self-serve-studio-question-scope.spec.js
@@ -6,18 +6,22 @@ const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePa
describe("self-serve studio question scope", () => {
it("renders legacy scope selectors for question lane and vehicle", () => {
- const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
+ const studioSource = readSource(
+ "src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
+ );
expect(studioSource).toContain("v-if=\"questionForm.scope_mode === 'legacy'\"");
- expect(studioSource).toContain("v-model=\"questionForm.lane\"");
- expect(studioSource).toContain("v-model=\"questionForm.product\"");
+ expect(studioSource).toContain('v-model="questionForm.lane"');
+ expect(studioSource).toContain('v-model="questionForm.product"');
expect(studioSource).toContain("Select lane");
expect(studioSource).toContain("Select vehicle type");
- expect(studioSource).toContain("v-for=\"entry in lanes\"");
+ expect(studioSource).toContain('v-for="entry in lanes"');
});
it("guards question submission for legacy Department/Lane/Vehicle scope", () => {
- const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
+ const studioSource = readSource(
+ "src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
+ );
expect(studioSource).toContain("Question text is required.");
expect(studioSource).toContain("Question description is required.");
@@ -29,10 +33,12 @@ describe("self-serve studio question scope", () => {
});
it("renders and submits question order priority", () => {
- const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
+ const studioSource = readSource(
+ "src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
+ );
- expect(studioSource).toContain("");
- expect(studioSource).toContain("v-model=\"questionForm.order_priority\"");
+ expect(studioSource).toContain('');
+ expect(studioSource).toContain('v-model="questionForm.order_priority"');
expect(studioSource).toContain("order_priority: parseIntOrZero(questionForm.value.order_priority)");
});
});
diff --git a/tests/unit/self-serve-studio-refresh.spec.js b/tests/unit/self-serve-studio-refresh.spec.js
index 4884c178..6b525155 100644
--- a/tests/unit/self-serve-studio-refresh.spec.js
+++ b/tests/unit/self-serve-studio-refresh.spec.js
@@ -6,9 +6,13 @@ const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePa
describe("self-serve studio refresh behavior", () => {
it("clears ObjectsGlobal cache before loading studio lists", () => {
- const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
+ const studioSource = readSource(
+ "src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
+ );
- expect(studioSource).toContain("import { ObjectsGlobal } from \"@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue\";");
+ expect(studioSource).toContain(
+ 'import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";'
+ );
expect(studioSource).toContain("const loadStudioData = async () => {");
expect(studioSource).toContain("ObjectsGlobal.clearCache();");
expect(studioSource).toContain("SessionUser.objects.self_serve_questions.get.all()");
diff --git a/tests/unit/self-serve-studio-task-scope.spec.js b/tests/unit/self-serve-studio-task-scope.spec.js
index 8aaa2b63..d1f37cb2 100644
--- a/tests/unit/self-serve-studio-task-scope.spec.js
+++ b/tests/unit/self-serve-studio-task-scope.spec.js
@@ -6,44 +6,54 @@ const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePa
describe("self-serve studio task scope", () => {
it("renders task selectors for lane and vehicle", () => {
- const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
+ const studioSource = readSource(
+ "src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
+ );
- expect(studioSource).toContain("v-model=\"taskForm.lane\"");
- expect(studioSource).toContain("v-model=\"taskForm.product\"");
+ expect(studioSource).toContain('v-model="taskForm.lane"');
+ expect(studioSource).toContain('v-model="taskForm.product"');
expect(studioSource).toContain("Select lane");
expect(studioSource).toContain("Select vehicle type");
- expect(studioSource).toContain("v-for=\"entry in lanes\"");
+ expect(studioSource).toContain('v-for="entry in lanes"');
});
it("keeps department and lane visible for machine-type task scope", () => {
- const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
+ const studioSource = readSource(
+ "src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue"
+ );
- expect(studioSource).toContain("v-if=\"taskForm.scope_mode === 'machine_type'\" class=\"field\"");
- expect(studioSource).toContain("");
- expect(studioSource).toContain("