Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78f41b8380 | ||
|
|
90d4bc7c04 | ||
|
|
b59c17f120 | ||
|
|
c279e7fd38 | ||
|
|
8a4fcb3e62 | ||
|
|
680ef7088b | ||
|
|
83935af973 | ||
|
|
37973f8fcb | ||
|
|
acf19c281a | ||
|
|
683fce32da | ||
|
|
4c9ba7c30d | ||
|
|
ca1f3ab477 | ||
|
|
2ab09076f5 | ||
|
|
97e17fadd4 | ||
|
|
ef780e5542 | ||
|
|
931c31c7be | ||
|
|
25f2be875c | ||
|
|
ff70326665 | ||
|
|
52ed49211a | ||
|
|
0565fc2a7c |
@@ -18,15 +18,14 @@ jobs:
|
||||
build-upload-and-verify:
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
env:
|
||||
RELEASE_BASE_URL: https://dev.truckwash.io
|
||||
PLAYWRIGHT_BASE_URL: https://dev.truckwash.io
|
||||
RELEASE_BASE_URL: https://api-v2.truckwash.io/master/frontend
|
||||
PLAYWRIGHT_BASE_URL: https://api-v2.truckwash.io/master/frontend
|
||||
PLAYWRIGHT_RELEASE_API_BASE_URL: https://api-v2.truckwash.io
|
||||
PLAYWRIGHT_RELEASE_API_PING_PATHS: /ping,/master/api/ping,/canary/api/ping,/stable/api/ping
|
||||
RELEASE_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
RELEASE_EXPECTED_BUILD_ID: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||
RELEASE_EXPECTED_COMMIT: ${{ github.sha }}
|
||||
RELEASE_WAIT_INITIAL_SECONDS: 30
|
||||
RELEASE_WAIT_TIMEOUT_SECONDS: 300
|
||||
RELEASE_WAIT_INITIAL_SECONDS: 45
|
||||
RELEASE_WAIT_TIMEOUT_SECONDS: 600
|
||||
RELEASE_POLL_INTERVAL_SECONDS: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -72,26 +71,7 @@ jobs:
|
||||
path: dist
|
||||
retention-days: 14
|
||||
|
||||
- name: Install lftp
|
||||
run: |
|
||||
if ! command -v lftp >/dev/null 2>&1; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y lftp
|
||||
fi
|
||||
|
||||
- name: Upload hashed assets before release metadata
|
||||
run: |
|
||||
test -n "$RELEASE_DEPLOY_HOST" || (echo "RELEASE_DEPLOY_HOST is required" >&2; exit 1)
|
||||
test -n "$RELEASE_DEPLOY_USER" || (echo "RELEASE_DEPLOY_USER is required" >&2; exit 1)
|
||||
test -n "$RELEASE_DEPLOY_PASSWORD" || (echo "RELEASE_DEPLOY_PASSWORD is required" >&2; exit 1)
|
||||
npm run release:upload:lftp
|
||||
env:
|
||||
RELEASE_DEPLOY_HOST: ${{ secrets.RELEASE_DEPLOY_HOST }}
|
||||
RELEASE_DEPLOY_USER: ${{ secrets.RELEASE_DEPLOY_USER }}
|
||||
RELEASE_DEPLOY_PASSWORD: ${{ secrets.RELEASE_DEPLOY_PASSWORD }}
|
||||
RELEASE_DEPLOY_REMOTE_ROOT: ${{ secrets.RELEASE_DEPLOY_REMOTE_ROOT }}
|
||||
|
||||
- name: Wait for exact uploaded build
|
||||
- name: Wait for Coolify release artifact
|
||||
run: npm run release:verify-upload
|
||||
|
||||
- name: Public live Playwright gate
|
||||
@@ -118,7 +98,7 @@ jobs:
|
||||
-X POST "$RELEASE_MANAGER_GATE_URL" \
|
||||
-H "Authorization: Bearer $RELEASE_MANAGER_GATE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$RELEASE_EXPECTED_BUILD_ID\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
|
||||
--data "{\"environment_url\":\"$RELEASE_BASE_URL\",\"channel_slug\":\"stable\",\"app\":\"frontend\",\"repository\":\"$RELEASE_REPOSITORY\",\"branch\":\"$RELEASE_BRANCH\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"workflow_url\":\"$RELEASE_WORKFLOW_URL\",\"auto_sync\":true,\"wait_timeout_seconds\":300,\"poll_interval_seconds\":10,\"required_checks\":[\"static_artifact\",\"api_gateway\"]}"
|
||||
env:
|
||||
RELEASE_MANAGER_GATE_URL: ${{ secrets.RELEASE_MANAGER_GATE_URL || 'https://api.truckwash.io/release/gate/test-runs' }}
|
||||
RELEASE_MANAGER_GATE_TOKEN: ${{ secrets.RELEASE_MANAGER_GATE_TOKEN }}
|
||||
|
||||
@@ -65,15 +65,35 @@ jobs:
|
||||
if: github.event_name != 'schedule'
|
||||
needs: build-and-unit
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
env:
|
||||
PLAYWRIGHT_PR_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
PLAYWRIGHT_PR_HEAD: ${{ github.sha }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve Playwright diff refs
|
||||
id: playwright-diff
|
||||
shell: bash
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
PUSH_BEFORE_SHA: ${{ github.event.before }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
zero_sha="0000000000000000000000000000000000000000"
|
||||
if [[ "$EVENT_NAME" == "pull_request" && -n "$PR_BASE_SHA" ]]; then
|
||||
base_ref="$PR_BASE_SHA"
|
||||
elif [[ -z "$PUSH_BEFORE_SHA" || "$PUSH_BEFORE_SHA" == "$zero_sha" ]]; then
|
||||
git fetch --no-tags --prune origin "$DEFAULT_BRANCH"
|
||||
base_ref="origin/$DEFAULT_BRANCH"
|
||||
else
|
||||
base_ref="$PUSH_BEFORE_SHA"
|
||||
fi
|
||||
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
|
||||
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
@@ -87,7 +107,7 @@ jobs:
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run Playwright PR tests
|
||||
run: npm run test:e2e:pr -- --base="$PLAYWRIGHT_PR_BASE" --head="$PLAYWRIGHT_PR_HEAD"
|
||||
run: npm run test:e2e:pr -- --base="${{ steps.playwright-diff.outputs.base }}" --head="${{ steps.playwright-diff.outputs.head }}"
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: failure()
|
||||
|
||||
@@ -8,6 +8,14 @@ COPY package*.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
COPY . .
|
||||
ARG SOURCE_COMMIT
|
||||
ARG RELEASE_COMMIT_SHA
|
||||
ARG COMMIT_SHA
|
||||
ARG GITHUB_SHA
|
||||
ENV SOURCE_COMMIT=$SOURCE_COMMIT
|
||||
ENV RELEASE_COMMIT_SHA=$RELEASE_COMMIT_SHA
|
||||
ENV COMMIT_SHA=$COMMIT_SHA
|
||||
ENV GITHUB_SHA=$GITHUB_SHA
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
@@ -3056,6 +3056,8 @@ paths:
|
||||
get:
|
||||
tags:
|
||||
- Orders
|
||||
x-api-coverage:
|
||||
happy: true
|
||||
summary: List orders
|
||||
description: Retrieve a paginated list of orders
|
||||
operationId: listOrders
|
||||
|
||||
Generated
+15
-34
@@ -233,7 +233,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -1931,6 +1930,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@bubblewrap/core/node_modules/inquirer/node_modules/@types/node": {
|
||||
"version": "25.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
||||
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@bubblewrap/core/node_modules/mute-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
|
||||
@@ -1954,7 +1963,8 @@
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@bubblewrap/validator": {
|
||||
"version": "1.22.0",
|
||||
@@ -2077,7 +2087,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -2126,7 +2135,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -2564,7 +2572,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"
|
||||
}
|
||||
@@ -2808,7 +2815,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"
|
||||
}
|
||||
@@ -2845,7 +2851,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"
|
||||
},
|
||||
@@ -2858,7 +2863,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"
|
||||
},
|
||||
@@ -2883,7 +2887,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"
|
||||
@@ -2927,7 +2930,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"
|
||||
},
|
||||
@@ -3051,7 +3053,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"
|
||||
},
|
||||
@@ -3064,7 +3065,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"
|
||||
},
|
||||
@@ -3080,7 +3080,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"
|
||||
},
|
||||
@@ -5343,7 +5342,6 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5378,7 +5376,6 @@
|
||||
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -5472,8 +5469,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",
|
||||
@@ -5597,7 +5593,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",
|
||||
@@ -5821,7 +5816,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -5906,8 +5900,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",
|
||||
@@ -6165,7 +6158,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"
|
||||
},
|
||||
@@ -6588,7 +6580,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"
|
||||
}
|
||||
@@ -6777,7 +6768,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"
|
||||
@@ -8335,8 +8325,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",
|
||||
@@ -10403,7 +10392,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",
|
||||
@@ -10758,7 +10746,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
|
||||
"integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -10942,7 +10929,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",
|
||||
@@ -10964,7 +10950,6 @@
|
||||
"integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.5.0",
|
||||
"colorjs.io": "^0.5.0",
|
||||
@@ -12730,7 +12715,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
|
||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -14123,7 +14107,6 @@
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -14183,7 +14166,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",
|
||||
@@ -14871,7 +14853,6 @@
|
||||
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
||||
@@ -120,7 +120,7 @@ async function verifyShell(baseUrl, shellPath) {
|
||||
|
||||
async function verifyRelease(baseUrl) {
|
||||
const expectedCommit = process.env.RELEASE_EXPECTED_COMMIT || process.env.GITHUB_SHA || "";
|
||||
const expectedBuildId = process.env.RELEASE_EXPECTED_BUILD_ID || process.env.RELEASE_BUILD_ID || "";
|
||||
const expectedBuildId = process.env.RELEASE_EXPECTED_BUILD_ID || "";
|
||||
const manifest = await fetchJson(baseUrl, "release-manifest.json");
|
||||
const releaseEntry = await fetchJson(baseUrl, "release-entry.json");
|
||||
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const workingDirectory = process.cwd();
|
||||
const playwrightCliPath = path.join(workingDirectory, "node_modules", "@playwright", "test", "cli.js");
|
||||
const execFileAsync = promisify(execFile);
|
||||
const roles = ["customer", "subuser", "admin", "superuser"];
|
||||
export const roles = ["customer", "subuser", "admin", "superuser"];
|
||||
const listEntryPattern = /^\s+\[[^\]]+\]\s+›\s+(.+?):(\d+):(\d+)\s+›\s+(.+)\s*$/u;
|
||||
|
||||
const ownedFilesByRole = {
|
||||
export const ownedFilesByRole = {
|
||||
customer: [
|
||||
"auth.smoke.spec.js",
|
||||
"booking-selfserve.smoke.spec.js",
|
||||
"connectivityIssue.spec.ts",
|
||||
"example.spec.ts",
|
||||
"guest-book-wash-mobile.spec.ts",
|
||||
"i18n-catalog-switch.spec.ts",
|
||||
"i18n-v2-integrity.spec.ts",
|
||||
"i18n.smoke.spec.ts",
|
||||
"i18n.views.spec.ts",
|
||||
"navigation.smoke.spec.js",
|
||||
"qr-new-customer-layout.spec.ts",
|
||||
"release-bootstrap.spec.js",
|
||||
"release-channel-switched.spec.js",
|
||||
"release-channel-unavailable.spec.js",
|
||||
"release-update-widget.spec.js",
|
||||
"self-serve-wash.spec.js",
|
||||
"session-release-runtime.spec.ts",
|
||||
"user-orders.spec.ts",
|
||||
"userBookings.spec.ts",
|
||||
"userBookWash.spec.ts",
|
||||
@@ -53,6 +62,7 @@ const ownedFilesByRole = {
|
||||
"assign-draft-order-modal-layout.spec.ts",
|
||||
"change-invoice-collection.spec.ts",
|
||||
"change-customer.spec.ts",
|
||||
"default-mobile-redirect.spec.ts",
|
||||
"economic-queue-workflow.spec.js",
|
||||
"pos-customer-rules.spec.js",
|
||||
"pos-desktop-card-payments.spec.js",
|
||||
@@ -62,18 +72,23 @@ const ownedFilesByRole = {
|
||||
"pos.visual.spec.js",
|
||||
],
|
||||
superuser: [
|
||||
"coolify-infrastructure.spec.js",
|
||||
"edge-gateways.fleet-outline.spec.js",
|
||||
"edge-gateways.routes.spec.js",
|
||||
"edge-gateways.smoke.spec.js",
|
||||
"edge-gateways.visual.spec.js",
|
||||
"errorReports.spec.ts",
|
||||
"failover-config.source.spec.ts",
|
||||
"invoice-distribution.smoke.spec.js",
|
||||
"invoice-transfer-monitor.spec.ts",
|
||||
"invoice-transfer-queue-history.spec.js",
|
||||
"invoicing-period.smoke.spec.js",
|
||||
"issue-repro-duplicates-date.spec.js",
|
||||
"release-manager.spec.js",
|
||||
"self-serve-sessions.spec.js",
|
||||
"self-serve-studio-audit-navigation.spec.js",
|
||||
"self-serve-studio-flow.spec.js",
|
||||
"session-bootstrap.spec.ts",
|
||||
"superuser-bookings.spec.ts",
|
||||
"superuser-customer-complaints.spec.ts",
|
||||
"superuser-customers-mass-import.spec.ts",
|
||||
@@ -84,12 +99,13 @@ const ownedFilesByRole = {
|
||||
"superuser-drafts.spec.ts",
|
||||
"superuser-products-layout.spec.ts",
|
||||
"superuser-system-status.smoke.spec.js",
|
||||
"superuser-users.spec.ts",
|
||||
"superuser-vehicles.smoke.spec.js",
|
||||
"workfeed-config.smoke.spec.js",
|
||||
],
|
||||
};
|
||||
|
||||
const titleRules = [
|
||||
export const titleRules = [
|
||||
{ role: "customer", file: "userAuth.spec.ts", patterns: [/\[AUTH\]\[User\]/u] },
|
||||
{ role: "subuser", file: "userAuth.spec.ts", patterns: [/\[AUTH\]\[Subuser\]/u] },
|
||||
{ role: "admin", file: "userAuth.spec.ts", patterns: [/\[AUTH\]\[Operator\]/u] },
|
||||
@@ -116,6 +132,11 @@ const titleRules = [
|
||||
file: "subuser-management.spec.ts",
|
||||
patterns: [/^authorized subuser managers/i, /^subuser self-service/i, /^subusers without /i],
|
||||
},
|
||||
{
|
||||
role: "superuser",
|
||||
file: "subuser-management.spec.ts",
|
||||
patterns: [/^superusers can list and invite chauffeurs/i],
|
||||
},
|
||||
{ role: "subuser", file: "userProfileVisibility.spec.ts", patterns: [/\[PROFILE\]\[Subuser\]\[Visibility\]/u] },
|
||||
];
|
||||
|
||||
@@ -200,7 +221,7 @@ function toBaseName(filePath) {
|
||||
return filePath.split(/[\\/]/u).pop() || filePath;
|
||||
}
|
||||
|
||||
function parseListedTests(listOutput) {
|
||||
export function parseListedTests(listOutput) {
|
||||
return listOutput
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => {
|
||||
@@ -222,7 +243,7 @@ function parseListedTests(listOutput) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function classifyTest(testEntry) {
|
||||
export function classifyTest(testEntry) {
|
||||
const matches = new Set();
|
||||
const directOwner = ownedFileToRole.get(testEntry.fileName);
|
||||
|
||||
@@ -309,8 +330,8 @@ async function runPlaywright(project, testListPath, forwardedArgs) {
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { options, forwardedArgs } = parseCliArgs(process.argv.slice(2));
|
||||
export async function main(argv = process.argv.slice(2)) {
|
||||
const { options, forwardedArgs } = parseCliArgs(argv);
|
||||
validateOptions(options, forwardedArgs);
|
||||
|
||||
const listOutput = await listProjectTests(options.project, forwardedArgs);
|
||||
@@ -342,4 +363,17 @@ async function main() {
|
||||
await runPlaywright(options.project, testListPath, forwardedArgs);
|
||||
}
|
||||
|
||||
await main();
|
||||
async function isDirectRun() {
|
||||
if (!process.argv[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentPath = await fs.realpath(fileURLToPath(import.meta.url));
|
||||
const invokedPath = await fs.realpath(process.argv[1]).catch(() => path.resolve(process.argv[1]));
|
||||
|
||||
return currentPath === invokedPath;
|
||||
}
|
||||
|
||||
if (await isDirectRun()) {
|
||||
await main();
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
reg_3: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
order_booking_id: {
|
||||
type: Number,
|
||||
default: null,
|
||||
@@ -917,6 +921,15 @@ const getAttachmentLabel = (attachment) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isWashCertificateAttachment = (attachment) => {
|
||||
const marker = String(getAttachmentOtherPayload(attachment) || "").trim().toUpperCase();
|
||||
if (marker === "WASH_CERTIFICATE") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return /(?:^|[/\\])wash[_-]?certificate.*\.pdf$/i.test(getAttachmentLabel(attachment));
|
||||
};
|
||||
|
||||
const getAttachmentExtension = (attachment) => {
|
||||
const match = String(getAttachmentLabel(attachment))
|
||||
.toLowerCase()
|
||||
@@ -1045,6 +1058,10 @@ const activeAttachmentPreviewSource = computed(() => {
|
||||
return previewSourcesById.value[activeAttachment.value.id] ?? null;
|
||||
});
|
||||
|
||||
const hasWashCertificateAttachment = computed(() =>
|
||||
attachmentsFromOrder.value.some((attachment) => isWashCertificateAttachment(attachment))
|
||||
);
|
||||
|
||||
const hasCachedPreviewSource = (attachmentId) => {
|
||||
return Object.prototype.hasOwnProperty.call(previewSourcesById.value, attachmentId);
|
||||
};
|
||||
@@ -1609,6 +1626,48 @@ const showCompleteOrderBookingConfirmation = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const showEmailNotificationActionResult = async (requestAction, successKey, errorKey) => {
|
||||
try {
|
||||
await requestAction();
|
||||
await Swal.fire({
|
||||
title: t(successKey),
|
||||
icon: "success",
|
||||
showConfirmButton: false,
|
||||
timer: 2000,
|
||||
heightAuto: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await Swal.fire({
|
||||
title: t("common.error"),
|
||||
text: [t(errorKey), SessionUser.functions.parseErrorMessage?.(error)].filter(Boolean).join(": "),
|
||||
icon: "error",
|
||||
heightAuto: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resendBookingConfirmation = () =>
|
||||
showEmailNotificationActionResult(
|
||||
() => SessionUser.objects.order_bookings.functions.resendBookingConfirmation(props.order_booking_id),
|
||||
"admin.pos.settings_wheel.resend_booking_confirmation_success",
|
||||
"admin.pos.settings_wheel.resend_booking_confirmation_error"
|
||||
);
|
||||
|
||||
const resendBookingCompletionConfirmation = () =>
|
||||
showEmailNotificationActionResult(
|
||||
() => SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation(props.order_booking_id),
|
||||
"admin.pos.settings_wheel.resend_booking_completion_confirmation_success",
|
||||
"admin.pos.settings_wheel.resend_booking_completion_confirmation_error"
|
||||
);
|
||||
|
||||
const resendWashCertificate = () =>
|
||||
showEmailNotificationActionResult(
|
||||
() => SessionUser.objects.orders.functions.resendWashCertificate(props.order_id),
|
||||
"admin.pos.settings_wheel.resend_wash_certificate_success",
|
||||
"admin.pos.settings_wheel.resend_wash_certificate_error"
|
||||
);
|
||||
|
||||
const getAvailableInvoiceCollectionsForCustomer = async (customerNumber) => {
|
||||
const normalizedCustomerNumber = normalizePositiveInteger(customerNumber);
|
||||
if (!normalizedCustomerNumber) {
|
||||
@@ -1835,6 +1894,40 @@ const flatBuiltInMenuSections = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
if (props.order_booking_id || hasWashCertificateAttachment.value) {
|
||||
const emailNotificationsSection = buildMenuSection(
|
||||
"email-notifications",
|
||||
t("admin.pos.settings_wheel.email_notifications_section"),
|
||||
[
|
||||
props.order_booking_id
|
||||
? buildMenuAction("email-notifications-resend-booking-confirmation", {
|
||||
icon: "fas fa-envelope",
|
||||
label: t("admin.pos.settings_wheel.resend_booking_confirmation"),
|
||||
clickAction: resendBookingConfirmation,
|
||||
})
|
||||
: null,
|
||||
props.order_booking_id && hasWashCertificateAttachment.value
|
||||
? buildMenuAction("email-notifications-resend-booking-completion-confirmation", {
|
||||
icon: "fas fa-envelope-open-text",
|
||||
label: t("admin.pos.settings_wheel.resend_booking_completion_confirmation"),
|
||||
clickAction: resendBookingCompletionConfirmation,
|
||||
})
|
||||
: null,
|
||||
!props.order_booking_id && hasWashCertificateAttachment.value
|
||||
? buildMenuAction("email-notifications-resend-wash-certificate", {
|
||||
icon: "fas fa-file-pdf",
|
||||
label: t("admin.pos.settings_wheel.resend_wash_certificate"),
|
||||
clickAction: resendWashCertificate,
|
||||
})
|
||||
: null,
|
||||
]
|
||||
);
|
||||
|
||||
if (emailNotificationsSection) {
|
||||
sections.push(emailNotificationsSection);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.invoice_collection_id && SessionUser.canAccessSuperUser()) {
|
||||
const invoiceCollectionLinkSection = buildMenuSection(
|
||||
"invoice-collection-link",
|
||||
@@ -2136,10 +2229,10 @@ const flatBuiltInMenuSections = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
if (props.reg_1 || props.reg_2) {
|
||||
if (props.reg_1 || props.reg_2 || props.reg_3) {
|
||||
const vehicleSection = buildMenuSection(
|
||||
"vehicle",
|
||||
props.reg_1 && props.reg_2
|
||||
[props.reg_1, props.reg_2, props.reg_3].filter(Boolean).length > 1
|
||||
? SessionUser.objects.vehicles.meta.labels.multiple
|
||||
: SessionUser.objects.vehicles.meta.labels.single,
|
||||
[
|
||||
@@ -2157,6 +2250,13 @@ const flatBuiltInMenuSections = computed(() => {
|
||||
clickAction: () => SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_2, true),
|
||||
})
|
||||
: null,
|
||||
props.reg_3 && SessionUser.canAccessSuperUser()
|
||||
? buildMenuAction("vehicle-reg-3", {
|
||||
icon: "fas fa-car",
|
||||
label: t("admin.pos.settings_wheel.view_vehicle_new_tab", { reg: props.reg_3 }),
|
||||
clickAction: () => SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_3, true),
|
||||
})
|
||||
: null,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -1263,6 +1263,9 @@ const formatCashierName = (order) => {
|
||||
v-bind:customer_number="order.customer_id"
|
||||
v-bind:department_id="order.department_id"
|
||||
v-bind:reg_1="order.reg_1"
|
||||
v-bind:reg_2="order.reg_2"
|
||||
v-bind:reg_3="order.reg_3"
|
||||
v-bind:order_booking_id="order.booking_id"
|
||||
:refreshFunction="loadList"
|
||||
@deleted="loadList()"
|
||||
@flag-created="emitFlagCreated"
|
||||
@@ -1506,6 +1509,9 @@ const formatCashierName = (order) => {
|
||||
v-bind:customer_number="order.customer_id"
|
||||
v-bind:department_id="order.department_id"
|
||||
v-bind:reg_1="order.reg_1"
|
||||
v-bind:reg_2="order.reg_2"
|
||||
v-bind:reg_3="order.reg_3"
|
||||
v-bind:order_booking_id="order.booking_id"
|
||||
:refreshFunction="loadList"
|
||||
@deleted="loadList()"
|
||||
@flag-created="emitFlagCreated"
|
||||
|
||||
@@ -43,9 +43,10 @@ const emitAnswerQuestion = (questionId: number, value: boolean) => {
|
||||
|
||||
<div class="self-serve-questions-status-slot" aria-live="polite">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="notification is-info is-light py-2 px-3 mb-0"
|
||||
:class="{ 'is-invisible': !isLoading }"
|
||||
data-testid="self-serve-questions-inline-loading"
|
||||
:aria-hidden="!isLoading"
|
||||
>
|
||||
<b-icon pack="fas" icon="spinner" custom-class="fa-pulse" size="is-small" />
|
||||
<span class="ml-2">{{ $t("self_wash.loading_data") }}</span>
|
||||
@@ -72,4 +73,8 @@ const emitAnswerQuestion = (questionId: number, value: boolean) => {
|
||||
.self-serve-questions-status-slot .notification {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.self-serve-questions-status-slot .notification.is-invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -217,6 +217,44 @@ const getVehicleReferenceValue = () => {
|
||||
return String(vehicleObject.value?.reference ?? "").trim();
|
||||
};
|
||||
|
||||
const getVehicleStateKey = (vehicle) => {
|
||||
if (!vehicle) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
vehicle?.id ?? "",
|
||||
normalizePlateValue(vehicle?.reg),
|
||||
normalizeCustomerNumber(vehicle?.customer_id) ?? "",
|
||||
vehicle?.type ?? "",
|
||||
vehicle?.booking_id ?? "",
|
||||
String(vehicle?.reference ?? "").trim(),
|
||||
].join("|");
|
||||
};
|
||||
|
||||
const getBookingStateKey = (booking) => {
|
||||
if (!booking) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
booking?.id ?? "",
|
||||
getBookingReg1Value(booking),
|
||||
getBookingReg2Value(booking),
|
||||
getBookingCustomerNumber(booking) ?? "",
|
||||
String(booking?.reference_number ?? booking?.reference ?? "").trim(),
|
||||
String(booking?.po ?? "").trim(),
|
||||
].join("|");
|
||||
};
|
||||
|
||||
const getBookingMatchesStateKey = (matches) => {
|
||||
if (!Array.isArray(matches) || matches.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return matches.map((booking) => getBookingStateKey(booking)).join("||");
|
||||
};
|
||||
|
||||
const setVehicleObject = (emittedVehicleObject, options = {}) => {
|
||||
const normalizedOptions = {
|
||||
preserveManualReference: true,
|
||||
@@ -224,17 +262,26 @@ const setVehicleObject = (emittedVehicleObject, options = {}) => {
|
||||
...options,
|
||||
};
|
||||
|
||||
vehicleObject.value = emittedVehicleObject;
|
||||
|
||||
const emittedVehiclePlate = normalizePlateValue(vehicleObject.value?.reg || reg_1.value);
|
||||
const emittedVehiclePlate = normalizePlateValue(emittedVehicleObject?.reg || reg_1.value);
|
||||
if (
|
||||
emittedVehiclePlate &&
|
||||
skippedDesktopBookingVehiclePlate.value === emittedVehiclePlate &&
|
||||
!isBookingMarkedVehicle(vehicleObject.value)
|
||||
!isBookingMarkedVehicle(emittedVehicleObject)
|
||||
) {
|
||||
skippedDesktopBookingVehiclePlate.value = "";
|
||||
}
|
||||
|
||||
const currentVehicleKey = getVehicleStateKey(vehicleObject.value);
|
||||
const nextVehicleKey = getVehicleStateKey(emittedVehicleObject);
|
||||
if (currentVehicleKey === nextVehicleKey) {
|
||||
if (normalizedOptions.nextSelectionSource) {
|
||||
setSelectionSource(normalizedOptions.nextSelectionSource);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
vehicleObject.value = emittedVehicleObject;
|
||||
|
||||
const matchedVehicleCustomerNumber = normalizeCustomerNumber(vehicleObject.value?.customer_id);
|
||||
const selectedCustomerNumber = normalizeCustomerNumber(customer_id.value);
|
||||
|
||||
@@ -274,6 +321,10 @@ const setBookingObject = (emittedBookingObject, options = {}) => {
|
||||
...options,
|
||||
};
|
||||
|
||||
if (getBookingStateKey(bookingObject.value) === getBookingStateKey(emittedBookingObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookingObject.value = emittedBookingObject;
|
||||
|
||||
if (emittedBookingObject && emittedBookingObject.reference_number) {
|
||||
@@ -284,7 +335,12 @@ const setBookingObject = (emittedBookingObject, options = {}) => {
|
||||
};
|
||||
|
||||
const setBookingMatches = (emittedBookingMatches) => {
|
||||
bookingMatches.value = Array.isArray(emittedBookingMatches) ? emittedBookingMatches : [];
|
||||
const nextBookingMatches = Array.isArray(emittedBookingMatches) ? emittedBookingMatches : [];
|
||||
if (getBookingMatchesStateKey(bookingMatches.value) === getBookingMatchesStateKey(nextBookingMatches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bookingMatches.value = nextBookingMatches;
|
||||
};
|
||||
|
||||
const mergeBookingMatchDetails = (booking) => {
|
||||
|
||||
@@ -416,6 +416,21 @@ const shouldPreserveCustomerSelection = (plateValue) => {
|
||||
};
|
||||
|
||||
let pendingVehicleCustomerLookup = null;
|
||||
const lastAutoSyncedVehicleKey = ref("");
|
||||
|
||||
const getVehicleAutoSyncKey = (vehicle, plateOverride = null) => {
|
||||
if (!vehicle) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
resolveBookingPlate(vehicle, plateOverride),
|
||||
vehicle?.id ?? "",
|
||||
normalizeCustomerNumber(vehicle?.customer_id) ?? "",
|
||||
normalizeCustomerNumber(customer_id.value) ?? "",
|
||||
].join("|");
|
||||
};
|
||||
|
||||
const syncMatchedVehicleSelection = (vehicle, options = {}) => {
|
||||
const normalizedOptions = {
|
||||
plateOverride: null,
|
||||
@@ -590,6 +605,7 @@ watch(reg_1, (newValue) => {
|
||||
// Define the search ID for this input change
|
||||
const search_id = register_new_search();
|
||||
console.log("reg_1 changed:", newValue);
|
||||
lastAutoSyncedVehicleKey.value = "";
|
||||
const shouldKeepCustomerSelection = shouldPreserveCustomerSelection(newValue);
|
||||
selectedDropdownItem.value = -1;
|
||||
// Check if the new value is empty, if so, clear the vehicles_matching array
|
||||
@@ -847,11 +863,17 @@ watch(vehicles_matching, (newValue) => {
|
||||
const currentValue = reg_1.value;
|
||||
const vehicle = newValue.find((vehicle) => vehicle.reg === currentValue);
|
||||
if (vehicle) {
|
||||
syncMatchedVehicleSelection(vehicle);
|
||||
const nextAutoSyncKey = getVehicleAutoSyncKey(vehicle, currentValue);
|
||||
if (lastAutoSyncedVehicleKey.value !== nextAutoSyncKey) {
|
||||
lastAutoSyncedVehicleKey.value = nextAutoSyncKey;
|
||||
syncMatchedVehicleSelection(vehicle);
|
||||
}
|
||||
} else if (getBookingMatchesForSelection(null, currentValue).length > 0) {
|
||||
lastAutoSyncedVehicleKey.value = "";
|
||||
clearCustomerConflict();
|
||||
emitVehicleObject(null, currentValue);
|
||||
} else {
|
||||
lastAutoSyncedVehicleKey.value = "";
|
||||
clearCustomerConflict();
|
||||
}
|
||||
// Check if the selectedDropdownItem index is valid
|
||||
|
||||
@@ -356,6 +356,20 @@ export const OrderBookings = {
|
||||
{ safety_seal: normalizeCompletionSafetySeal(safetySeal) },
|
||||
onAfterComplete
|
||||
);
|
||||
},
|
||||
resendBookingConfirmation: async (id) => {
|
||||
return SessionUser.request(
|
||||
OrderBookings.meta.endpoint + "/booking-confirmation/resend",
|
||||
"POST",
|
||||
{ id: parseInt(id, 10) }
|
||||
).then((response) => response.data.data);
|
||||
},
|
||||
resendBookingCompletionConfirmation: async (id) => {
|
||||
return SessionUser.request(
|
||||
OrderBookings.meta.endpoint + "/completion-confirmation/resend",
|
||||
"POST",
|
||||
{ id: parseInt(id, 10) }
|
||||
).then((response) => response.data.data);
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -924,6 +924,13 @@ const assignDraftOrderCustomer = async ({
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
resendWashCertificate: async (id) => {
|
||||
return SessionUser.request(
|
||||
Orders.meta.endpoint + "/wash-certificate/resend",
|
||||
"POST",
|
||||
{ id: parseInt(id, 10) }
|
||||
).then((response) => response.data.data);
|
||||
},
|
||||
showAttachWashCertificateForm(order_id, onAfterSubmit = null) {
|
||||
const normalizedOrderId = Number.parseInt(order_id, 10);
|
||||
if (!Number.isInteger(normalizedOrderId) || normalizedOrderId <= 0) {
|
||||
|
||||
@@ -1188,6 +1188,7 @@ export const selectScan = (scan) => {
|
||||
};
|
||||
|
||||
const cached_customer_names = [];
|
||||
const activeCustomerSelectionRequests = new Map();
|
||||
|
||||
/** Get the customer's name */
|
||||
export const getCustomerName = async (customerNumber) => {
|
||||
@@ -1216,23 +1217,62 @@ export const getCustomerName = async (customerNumber) => {
|
||||
|
||||
/** Search, then select customer by customer number */
|
||||
export const searchAndSelectCustomer = async (customerNumber, options = {}) => {
|
||||
const normalizedOptions = {
|
||||
forceRefresh: false,
|
||||
...options,
|
||||
};
|
||||
const normalizedCustomerNumber = resolveCustomerNumber(customerNumber);
|
||||
if (!normalizedCustomerNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedCustomerNumber = getSelectedCustomerNumber();
|
||||
const hasSelectedCustomerData =
|
||||
customer_data.value &&
|
||||
typeof customer_data.value === "object" &&
|
||||
!Array.isArray(customer_data.value) &&
|
||||
Object.keys(customer_data.value).length > 0;
|
||||
|
||||
if (
|
||||
!normalizedOptions.forceRefresh &&
|
||||
selectedCustomerNumber === normalizedCustomerNumber &&
|
||||
hasSelectedCustomerData
|
||||
) {
|
||||
return customer_data.value;
|
||||
}
|
||||
|
||||
if (!normalizedOptions.forceRefresh && activeCustomerSelectionRequests.has(normalizedCustomerNumber)) {
|
||||
return await activeCustomerSelectionRequests.get(normalizedCustomerNumber);
|
||||
}
|
||||
|
||||
// Get the customer data
|
||||
return await authenticatedRequest(`/users/customer?customer_number=${customerNumber}`, "GET")
|
||||
const request = authenticatedRequest(`/users/customer?customer_number=${normalizedCustomerNumber}`, "GET")
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
const responseData = response?.data?.data ?? {};
|
||||
const rawEconomicCustomer = responseData.economic_customer ?? null;
|
||||
const normalizedCustomer = normalizeCustomerRecord(rawEconomicCustomer, {
|
||||
...responseData,
|
||||
customerNumber: resolveCustomerNumber(rawEconomicCustomer) ?? resolveCustomerNumber(customerNumber),
|
||||
customerNumber: resolveCustomerNumber(rawEconomicCustomer) ?? normalizedCustomerNumber,
|
||||
});
|
||||
selectCustomer(normalizedCustomer, options);
|
||||
selectCustomer(normalizedCustomer, normalizedOptions);
|
||||
return normalizedCustomer;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return null;
|
||||
})
|
||||
.finally(() => {
|
||||
if (activeCustomerSelectionRequests.get(normalizedCustomerNumber) === request) {
|
||||
activeCustomerSelectionRequests.delete(normalizedCustomerNumber);
|
||||
}
|
||||
});
|
||||
|
||||
if (!normalizedOptions.forceRefresh) {
|
||||
activeCustomerSelectionRequests.set(normalizedCustomerNumber, request);
|
||||
}
|
||||
|
||||
return await request;
|
||||
};
|
||||
|
||||
/** Get order items */
|
||||
|
||||
@@ -600,10 +600,20 @@
|
||||
"delete_user": "Slet bruger",
|
||||
"download_invoice": "Download faktura",
|
||||
"edit_permissions": "Rediger tilladelser",
|
||||
"email_notifications_section": "E-mail notifikationer",
|
||||
"enter_password": "Indtast adgangskode",
|
||||
"error": "Fejl",
|
||||
"error_changing_password": "Fejl ved ændring af adgangskode",
|
||||
"error_downloading_attachment": "Fejl ved download af vedhæftet fil",
|
||||
"resend_booking_confirmation": "(Gen)send bookingbekræftelse",
|
||||
"resend_booking_confirmation_error": "Bookingbekræftelsen kunne ikke sendes",
|
||||
"resend_booking_confirmation_success": "Bookingbekræftelsen er sendt",
|
||||
"resend_booking_completion_confirmation": "(Gen)send bekræftelse for færdig booking",
|
||||
"resend_booking_completion_confirmation_error": "Bekræftelsen for færdig booking kunne ikke sendes",
|
||||
"resend_booking_completion_confirmation_success": "Bekræftelsen for færdig booking er sendt",
|
||||
"resend_wash_certificate": "(Gen)send vaskecertifikat",
|
||||
"resend_wash_certificate_error": "Vaskecertifikatet kunne ikke sendes",
|
||||
"resend_wash_certificate_success": "Vaskecertifikatet er sendt",
|
||||
"link_copied": "Link kopieret",
|
||||
"login_as_customer": "Log ind som kunde",
|
||||
"login_as_user": "Log ind som bruger",
|
||||
|
||||
@@ -600,10 +600,20 @@
|
||||
"delete_user": "Benutzer l?schen",
|
||||
"download_invoice": "Rechnung herunterladen",
|
||||
"edit_permissions": "Berechtigungen bearbeiten",
|
||||
"email_notifications_section": "E-Mail-Benachrichtigungen",
|
||||
"enter_password": "Passwort eingeben",
|
||||
"error": "Fehler",
|
||||
"error_changing_password": "Beim ?ndern des Passworts ist ein Fehler aufgetreten.",
|
||||
"error_downloading_attachment": "Beim Herunterladen der angeh?ngten Datei ist ein Fehler aufgetreten.",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"link_copied": "Link in die Zwischenablage kopiert",
|
||||
"login_as_customer": "Als Kunde anmelden",
|
||||
"login_as_user": "Als Benutzer anmelden",
|
||||
|
||||
@@ -600,10 +600,20 @@
|
||||
"delete_user": "Delete user",
|
||||
"download_invoice": "Download invoice",
|
||||
"edit_permissions": "Edit permissions",
|
||||
"email_notifications_section": "Email notifications",
|
||||
"enter_password": "Enter password",
|
||||
"error": "Error",
|
||||
"error_changing_password": "An error occurred while changing the password.",
|
||||
"error_downloading_attachment": "An error occurred while downloading the attached file.",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"link_copied": "Link copied to clipboard",
|
||||
"login_as_customer": "Log in as customer",
|
||||
"login_as_user": "Log in as user",
|
||||
|
||||
@@ -600,10 +600,20 @@
|
||||
"delete_user": "Delete user",
|
||||
"download_invoice": "Last ned faktura",
|
||||
"edit_permissions": "Rediger tillatelser",
|
||||
"email_notifications_section": "E-postvarsler",
|
||||
"enter_password": "Skriv inn passord",
|
||||
"error": "Feil",
|
||||
"error_changing_password": "Det oppstod en feil under endring av passord.",
|
||||
"error_downloading_attachment": "Det oppsto en feil under nedlasting av den vedlagte filen.",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"link_copied": "Linken er kopiert til utklippstavlen",
|
||||
"login_as_customer": "Logg inn som kunde",
|
||||
"login_as_user": "Logg inn som bruker",
|
||||
|
||||
@@ -600,10 +600,20 @@
|
||||
"delete_user": "Delete user",
|
||||
"download_invoice": "Ladda ner faktura",
|
||||
"edit_permissions": "Edit permissions",
|
||||
"email_notifications_section": "E-postaviseringar",
|
||||
"enter_password": "Enter password",
|
||||
"error": "Fel",
|
||||
"error_changing_password": "Ett fel uppstod när lösenordet ändrades.",
|
||||
"error_downloading_attachment": "Ett fel uppstod när den bifogade filen laddades ner.",
|
||||
"resend_booking_confirmation": "(Re)send booking confirmation",
|
||||
"resend_booking_confirmation_error": "The booking confirmation could not be sent",
|
||||
"resend_booking_confirmation_success": "The booking confirmation has been sent",
|
||||
"resend_booking_completion_confirmation": "(Re)send booking completion confirmation",
|
||||
"resend_booking_completion_confirmation_error": "The booking completion confirmation could not be sent",
|
||||
"resend_booking_completion_confirmation_success": "The booking completion confirmation has been sent",
|
||||
"resend_wash_certificate": "(Re)send wash certificate",
|
||||
"resend_wash_certificate_error": "The wash certificate could not be sent",
|
||||
"resend_wash_certificate_success": "The wash certificate has been sent",
|
||||
"link_copied": "Link copied to clipboard",
|
||||
"login_as_customer": "Log in as customer",
|
||||
"login_as_user": "Log in as user",
|
||||
|
||||
+14
-6
@@ -863,6 +863,14 @@ const selectedSimulatorVehicleTypeRow = computed(() => {
|
||||
const selectedSimulatorVehicleTypeId = computed(() => (
|
||||
selectedSimulatorVehicleTypeRow.value ? parseNullableInt(selectedSimulatorVehicleTypeRow.value.id) : null
|
||||
));
|
||||
const selectedSimulatorVehicleTypeScopeId = computed(() => {
|
||||
const selectedRow = selectedSimulatorVehicleTypeRow.value;
|
||||
if (!selectedRow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return firstPositiveInt(selectedRow.product, selectedRow.product_id, selectedRow.id);
|
||||
});
|
||||
const simulatorScopeSummary = computed(() => ({
|
||||
lane: selectedSimulatorLaneId.value ? labelFor("lanes", selectedSimulatorLaneId.value) : "Select lane",
|
||||
vehicleType:
|
||||
@@ -877,7 +885,7 @@ const pathOutcomesRequestPayload = computed(() => {
|
||||
return {
|
||||
department: departmentId.value,
|
||||
lane_id: selectedSimulatorLaneId.value,
|
||||
vehicle_type_id: selectedSimulatorVehicleTypeId.value,
|
||||
vehicle_type_id: selectedSimulatorVehicleTypeScopeId.value,
|
||||
config_source: simulatorForm.value.config_source || "draft",
|
||||
hardware_mode: hardwareMode,
|
||||
include_hardware: hardwareMode !== "none",
|
||||
@@ -1570,7 +1578,7 @@ const pathVerificationDynamicImageUrl = computed(() => {
|
||||
dynamicImageId,
|
||||
buttons: pathVerificationDynamicImageButtons.value,
|
||||
currentStep: 0,
|
||||
vehicleTypeId: selectedSimulatorVehicleTypeId.value,
|
||||
vehicleTypeId: selectedSimulatorVehicleTypeScopeId.value,
|
||||
thumbPosition: pathVerificationDynamicImageThumbPosition.value,
|
||||
});
|
||||
});
|
||||
@@ -1961,7 +1969,7 @@ const savePathEditorPath = async () => {
|
||||
previous_path_key: pathEditorForm.value.previous_path_key || undefined,
|
||||
scope: {
|
||||
lane_id: parseNullableInt(selectedSimulatorLaneId.value),
|
||||
vehicle_type_id: parseNullableInt(selectedSimulatorVehicleTypeId.value),
|
||||
vehicle_type_id: parseNullableInt(selectedSimulatorVehicleTypeScopeId.value),
|
||||
machine_type_id:
|
||||
parseNullableInt(filters.value.machine_type_id) ||
|
||||
parseNullableInt(selectedSimulatorLaneRow.value?.machine_type_id),
|
||||
@@ -3087,7 +3095,7 @@ const inspectorTaskPreviewVehicleTypeId = computed(() =>
|
||||
selectedRaw.value?.vehicle_type_id,
|
||||
inspectorForm.value?.product,
|
||||
selectedRaw.value?.product,
|
||||
selectedSimulatorVehicleTypeId.value
|
||||
selectedSimulatorVehicleTypeScopeId.value
|
||||
)
|
||||
);
|
||||
|
||||
@@ -3629,7 +3637,7 @@ const simulatorResolvedLaneId = computed(() =>
|
||||
|
||||
const simulatorResolvedVehicleTypeId = computed(() =>
|
||||
parseNullableInt(
|
||||
selectedSimulatorVehicleTypeId.value ??
|
||||
selectedSimulatorVehicleTypeScopeId.value ??
|
||||
resolvedSimulatorVehicleTypeId.value ??
|
||||
simulatorResult.value?.vehicle?.type ??
|
||||
simulatorDebug.value?.parameters?.vehicle_type_id
|
||||
@@ -4887,7 +4895,7 @@ const buildSimulatorPayload = () => {
|
||||
.map(([questionId, value]) => ({ question_id: parseIntOrZero(questionId), value }));
|
||||
const hardwareMode = simulatorForm.value.hardware_mode || (hasVirtualHardware.value ? "studio" : "real");
|
||||
const laneId = parseIntOrZero(selectedSimulatorLaneId.value);
|
||||
const vehicleTypeId = selectedSimulatorVehicleTypeId.value;
|
||||
const vehicleTypeId = selectedSimulatorVehicleTypeScopeId.value;
|
||||
if (laneId <= 0 || !vehicleTypeId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -54,9 +54,12 @@ const isVehicleStepNextLoading = ref(false);
|
||||
const hasStartFormUserInput = ref(false);
|
||||
const shouldRestoreServerActiveWash = ref(false);
|
||||
const isRestoringServerActiveWash = ref(false);
|
||||
const isSyncingActiveWash = ref(false);
|
||||
const MIN_FINISHING_SCREEN_MS = 250;
|
||||
const RECENT_COMPLETED_WASH_KEY = "mywash_recent_completed_v1";
|
||||
const RECENT_COMPLETED_WASH_SUPPRESSION_MS = 10 * 60 * 1000;
|
||||
const ACTIVE_WASH_REFRESH_MS = 5 * 1000;
|
||||
const WASH_START_SERVER_SYNC_GRACE_MS = 10 * 1000;
|
||||
|
||||
const normalizePositiveInteger = (value: any) => {
|
||||
const parsed = parseInt(String(value ?? ""), 10);
|
||||
@@ -374,6 +377,7 @@ const completeGuidedWash = async () => {
|
||||
};
|
||||
|
||||
const unregisterBeforeUnload = ref<null | (() => void)>(null);
|
||||
const activeWashRefreshInterval = ref<ReturnType<typeof window.setInterval> | null>(null);
|
||||
|
||||
const registrationOptions = computed(() =>
|
||||
customerVehicles.value
|
||||
@@ -468,11 +472,11 @@ const vehicleStepGuidanceKey = computed(() => {
|
||||
}
|
||||
|
||||
if (!normalizeLicensePlate(licensePlateInput.value)) {
|
||||
return "self_wash.vehicle_step_missing_registration";
|
||||
return ""; //"self_wash.vehicle_step_missing_registration";
|
||||
}
|
||||
|
||||
if ((availableProductIds.value || []).length > 0 && !hasAllowedVehicleTypeSelection.value) {
|
||||
return "self_wash.vehicle_step_missing_vehicle_type";
|
||||
return ""; //"self_wash.vehicle_step_missing_vehicle_type";
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -505,12 +509,7 @@ const questionReviewStateKey = computed(() => {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
normalizeLicensePlate(licensePlateInput.value),
|
||||
normalizeLaneId(radioLaneOption.value) ?? "",
|
||||
vehicleTypeSelect.value ?? "",
|
||||
visibleQuestionIds,
|
||||
].join("|");
|
||||
return [normalizeLicensePlate(licensePlateInput.value), vehicleTypeSelect.value ?? "", visibleQuestionIds].join("|");
|
||||
});
|
||||
|
||||
const hasConfirmedCurrentQuestionReview = computed(
|
||||
@@ -519,9 +518,11 @@ const hasConfirmedCurrentQuestionReview = computed(
|
||||
|
||||
const shouldReturnToQuestionsForReview = computed(
|
||||
() =>
|
||||
!isCompletingWash.value &&
|
||||
!washInProgress.value &&
|
||||
visibleQuestions.value.length > 0 &&
|
||||
currentStep.value > steps.QUESTIONS &&
|
||||
currentStep.value < steps.COMPLETED &&
|
||||
!hasConfirmedCurrentQuestionReview.value
|
||||
);
|
||||
|
||||
@@ -891,6 +892,111 @@ const scheduleServerActiveWashRestore = (delayMs = 0) => {
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
const clearLocalActiveWashFromServer = () => {
|
||||
const completedLaneId = washLaneId.value;
|
||||
markRecentlyCompletedWash(completedLaneId, licensePlateInput.value, getNumericCustomerNumber());
|
||||
completedDurationMs.value = washStartTime.value ? now.value - washStartTime.value : 0;
|
||||
currentStep.value = steps.COMPLETED;
|
||||
washInProgress.value = false;
|
||||
washLaneId.value = null;
|
||||
washStartTime.value = null;
|
||||
shouldRestoreServerActiveWash.value = false;
|
||||
stopElapsedTimer();
|
||||
clearProgress();
|
||||
resetGuidedWashStep();
|
||||
};
|
||||
|
||||
const syncActiveWashWithServer = async () => {
|
||||
if (isSyncingActiveWash.value || !washInProgress.value || !washLaneId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const laneId = normalizeLaneId(washLaneId.value);
|
||||
if (!laneId) {
|
||||
clearLocalActiveWashFromServer();
|
||||
return;
|
||||
}
|
||||
|
||||
isSyncingActiveWash.value = true;
|
||||
try {
|
||||
const response = await SessionUser.request("/modules/self-serve/lane/wash/in-progress", "GET", {
|
||||
lane_id: laneId,
|
||||
});
|
||||
const details = unwrapApiData(response);
|
||||
if (!Object.prototype.hasOwnProperty.call(details || {}, "in_progress")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customerNumber = getNumericCustomerNumber();
|
||||
const serverCustomerNumber = normalizePositiveInteger(
|
||||
details?.session?.customer_number ?? details?.customer?.customer_number
|
||||
);
|
||||
const serverStillMatchesCurrentWash =
|
||||
!!details?.in_progress && (!customerNumber || !serverCustomerNumber || serverCustomerNumber === customerNumber);
|
||||
|
||||
if (!serverStillMatchesCurrentWash || isRecentlyCompletedActiveWash({ details, laneId })) {
|
||||
const isWithinStartGracePeriod =
|
||||
washStartTime.value && Date.now() - washStartTime.value < WASH_START_SERVER_SYNC_GRACE_MS;
|
||||
if (isWithinStartGracePeriod && !isRecentlyCompletedActiveWash({ details, laneId })) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearLocalActiveWashFromServer();
|
||||
return;
|
||||
}
|
||||
|
||||
const session = details?.session || {};
|
||||
const vehicle = details?.vehicle || {};
|
||||
const serverReg = normalizeLicensePlate(session?.reg ?? vehicle?.reg);
|
||||
if (serverReg) {
|
||||
licensePlateInput.value = serverReg;
|
||||
}
|
||||
|
||||
applyEffectiveCustomerNumberInput(session?.customer_number ?? details?.customer?.customer_number ?? customerNumber);
|
||||
vehicleTypeSelect.value =
|
||||
normalizePositiveInteger(session?.vehicle_type_id ?? vehicle?.type) ?? vehicleTypeSelect.value;
|
||||
radioWashType.value = session?.machine_relay_enabled ? "Machine" : radioWashType.value;
|
||||
washStartTime.value =
|
||||
parseServerDateTimeMs(session?.wash_started_at) ??
|
||||
parseServerDateTimeMs(session?.machine_start_triggered_at) ??
|
||||
parseServerDateTimeMs(session?.machine_relay_enabled_at) ??
|
||||
washStartTime.value;
|
||||
|
||||
const summaryParams: Record<string, any> = session?.id
|
||||
? { session_id: session.id }
|
||||
: { lane_id: laneId, reg: normalizeLicensePlate(licensePlateInput.value) };
|
||||
const selectedVehicleTypeId = normalizePositiveInteger(vehicleTypeSelect.value);
|
||||
if (selectedVehicleTypeId) {
|
||||
summaryParams.vehicle_type = selectedVehicleTypeId;
|
||||
}
|
||||
await fetchWashSummary(summaryParams, false);
|
||||
saveProgress("serverActiveWashRefresh");
|
||||
} catch (error) {
|
||||
console.warn("Failed to refresh active self-serve wash:", error);
|
||||
} finally {
|
||||
isSyncingActiveWash.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const stopActiveWashRefresh = () => {
|
||||
if (activeWashRefreshInterval.value) {
|
||||
window.clearInterval(activeWashRefreshInterval.value);
|
||||
activeWashRefreshInterval.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startActiveWashRefresh = () => {
|
||||
stopActiveWashRefresh();
|
||||
if (!washInProgress.value || !washLaneId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncActiveWashWithServer();
|
||||
activeWashRefreshInterval.value = window.setInterval(() => {
|
||||
syncActiveWashWithServer();
|
||||
}, ACTIVE_WASH_REFRESH_MS);
|
||||
};
|
||||
|
||||
const retrySelfServeData = async () => {
|
||||
if (isSelfServeRetrying.value) {
|
||||
return;
|
||||
@@ -977,6 +1083,29 @@ const onVehicleStepNext = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onPreviousStep = (fallbackPrevious: { action?: () => void } | null = null) => {
|
||||
if (washInProgress.value || currentStep.value === steps.COMPLETED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep.value === steps.QUESTIONS) {
|
||||
currentStep.value = steps.VEHICLE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep.value === steps.SELECT_LANE) {
|
||||
currentStep.value = visibleQuestions.value.length > 0 ? steps.QUESTIONS : steps.VEHICLE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep.value === steps.TASKS) {
|
||||
currentStep.value = steps.SELECT_LANE;
|
||||
return;
|
||||
}
|
||||
|
||||
fallbackPrevious?.action?.();
|
||||
};
|
||||
|
||||
const onToggleTask = (taskId: number, value: boolean) => {
|
||||
completedTasks.value = {
|
||||
...completedTasks.value,
|
||||
@@ -1069,6 +1198,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
stopAutoRefresh();
|
||||
stopActiveWashRefresh();
|
||||
markDestroying();
|
||||
setShowFooterInContent(true);
|
||||
});
|
||||
@@ -1196,6 +1326,15 @@ watch(
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch([() => washInProgress.value, () => washLaneId.value], ([inProgress]) => {
|
||||
if (inProgress) {
|
||||
startActiveWashRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
stopActiveWashRefresh();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => nearestDepartment.value,
|
||||
(newValue) => {
|
||||
@@ -1217,11 +1356,7 @@ watch(
|
||||
(newLaneId) => {
|
||||
if (newLaneId && newLaneId !== "Any" && !isRestoring.value) {
|
||||
fetchSelfServeData().then(() => {
|
||||
if (
|
||||
(!allVisibleQuestionsAnswered.value || shouldReturnToQuestionsForReview.value) &&
|
||||
!washInProgress.value &&
|
||||
currentStep.value > steps.QUESTIONS
|
||||
) {
|
||||
if (!allVisibleQuestionsAnswered.value && !washInProgress.value && currentStep.value > steps.QUESTIONS) {
|
||||
currentStep.value = steps.QUESTIONS;
|
||||
}
|
||||
});
|
||||
@@ -1511,7 +1646,7 @@ watch(
|
||||
icon-left="arrow-left"
|
||||
data-testid="self-serve-nav-previous"
|
||||
:disabled="previous.disabled || currentStep === steps.COMPLETED || washInProgress"
|
||||
@click.prevent="previous.action"
|
||||
@click.prevent="onPreviousStep(previous)"
|
||||
>
|
||||
{{ $t("common.previous") }}
|
||||
</b-button>
|
||||
|
||||
@@ -51,8 +51,9 @@ async function navigateToPosBootPage(page: Page, { waitForSession = false }: { w
|
||||
|
||||
try {
|
||||
await page.goto(POS_BOOT_URL, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
||||
await sessionRequest;
|
||||
await Promise.race([sessionRequest, getPosBootStep(page).waitFor({ state: "visible", timeout: 10_000 })]);
|
||||
await expect(getPosBootStep(page)).toBeVisible({ timeout: 10_000 });
|
||||
await sessionRequest;
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
@@ -2747,6 +2748,72 @@ test.describe("Admin POS Orders - desktop action menu layout", () => {
|
||||
await expect(attachmentsSection).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows email notification actions and resends booking completion confirmation", async ({ page }) => {
|
||||
const baseFixture = createPosFixture();
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: SUPERUSER_POS_PERMISSIONS,
|
||||
edgeGateways: false,
|
||||
pos: createPosFixture({
|
||||
ordersById: {
|
||||
54518: {
|
||||
...baseFixture.ordersById[54518],
|
||||
user_id: 77,
|
||||
booking_id: 9101,
|
||||
customer_name: "(TEST) Pleno Vognmandsforretning",
|
||||
invoice_collection_id: 101,
|
||||
},
|
||||
},
|
||||
attachmentsByOrderId: {
|
||||
54518: [
|
||||
{
|
||||
id: 7401,
|
||||
object_type: "orders",
|
||||
object_id: 54518,
|
||||
content: {
|
||||
image: null,
|
||||
document: "wash_certificate_54518.pdf",
|
||||
relation: null,
|
||||
other: "WASH_CERTIFICATE",
|
||||
src: null,
|
||||
},
|
||||
created_at: "2026-04-21 08:44:07",
|
||||
updated_at: "2026-04-21 08:44:07",
|
||||
deleted_at: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
await primeOperatorSession(page, "pos-orders-email-notifications-token", SUPERUSER_POS_PERMISSIONS);
|
||||
|
||||
await page.setViewportSize({ width: 1900, height: 900 });
|
||||
await page.goto("/admin/12/modules/pos/orders");
|
||||
await expect(page.locator("table")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
const dropdownRoot = await openLowestVisibleOrderActionDropdown(page);
|
||||
const dropdownContent = dropdownRoot.locator(".dropdown-content").first();
|
||||
const emailSection = dropdownContent.getByTestId("action-settings-wheel-section-email-notifications");
|
||||
|
||||
await expect(emailSection).toBeVisible();
|
||||
await emailSection.hover();
|
||||
|
||||
const emailSubmenu = dropdownContent.getByTestId("action-settings-wheel-submenu-email-notifications");
|
||||
await expect(emailSubmenu).toBeVisible();
|
||||
await expect(emailSubmenu).toContainText("(Gen)send bookingbekræftelse");
|
||||
await expect(emailSubmenu).toContainText("(Gen)send bekræftelse for færdig booking");
|
||||
await expect(emailSubmenu).not.toContainText("(Gen)send vaskecertifikat");
|
||||
|
||||
const resendRequestPromise = page.waitForRequest((request) => {
|
||||
return request.method() === "POST" && request.url().includes("/order-bookings/completion-confirmation/resend");
|
||||
});
|
||||
|
||||
await emailSubmenu.getByRole("button", { name: /\(Gen\)send bekræftelse for færdig booking/i }).click();
|
||||
const resendRequest = await resendRequestPromise;
|
||||
|
||||
expect(resendRequest.postDataJSON()).toEqual({ id: 9101 });
|
||||
});
|
||||
|
||||
test("keeps the first action reachable while async menu sections load", async ({ page }) => {
|
||||
const posFixture = createAsyncActionMenuPosFixture();
|
||||
|
||||
|
||||
@@ -761,86 +761,14 @@ async function installCoolifyMocks(page, state) {
|
||||
}
|
||||
|
||||
test.describe("Coolify infrastructure management", () => {
|
||||
test("@smoke configures and tests a Coolify connection with mocked API data", async ({ page }) => {
|
||||
const state = await boot(page);
|
||||
test("@smoke opens the current Coolify integration surface from the legacy route", async ({ page }) => {
|
||||
await boot(page);
|
||||
|
||||
await page.goto("/superuser/configuration/coolify");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Coolify configuration" })).toBeVisible({ timeout: 30_000 });
|
||||
await page.locator(".card-header").filter({ hasText: "API connection" }).click();
|
||||
await page.locator(".card-header").filter({ hasText: "Gateway load balancer" }).click();
|
||||
await page.locator(".card-header").filter({ hasText: "Instances" }).click();
|
||||
await page.locator(".card-header").filter({ hasText: "Managed targets" }).click();
|
||||
await expect(page.getByTestId("coolify-load-balancer-card")).toContainText("api-v2.truckwash.io");
|
||||
await expect(page.getByTestId("coolify-gateways-table")).toContainText("65.21.214.30");
|
||||
await expect(page.getByTestId("coolify-load-balancer-drift")).toContainText("Add target 65.21.214.30");
|
||||
await page.getByRole("button", { name: "Dry-run reconcile" }).click();
|
||||
expect(state.lbReconciles.at(-1)).toEqual({ dry_run: true });
|
||||
await page.getByRole("button", { name: "Enforce reconcile" }).click();
|
||||
expect(state.lbReconciles.at(-1)).toEqual({ dry_run: false });
|
||||
await expect(page.getByTestId("coolify-load-balancer-card")).toContainText("ok");
|
||||
await expect(page.getByTestId("coolify-load-balancer-drift")).toContainText("No planned changes.");
|
||||
await page.getByRole("button", { name: "Dry-run route deploy" }).click();
|
||||
expect(state.routeDeploys.at(-1)).toEqual({ dry_run: true });
|
||||
expect(state.routePlans.at(-1)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
public_url: "https://api-v2.truckwash.io/internal/api",
|
||||
frontend_public_url: "https://api-v2.truckwash.io/internal/frontend",
|
||||
}),
|
||||
])
|
||||
);
|
||||
await expect(page.getByTestId("coolify-gateway-route-result")).toContainText("Route plan");
|
||||
await expect(page.getByTestId("coolify-gateway-route-result")).toContainText("Warnings 1");
|
||||
await page.getByRole("button", { name: "Deploy api-v2 route" }).click();
|
||||
expect(state.routeDeploys.at(-1)).toEqual({ dry_run: false });
|
||||
expect(state.routePlans.at(-1)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
public_url: "https://api-v2.truckwash.io/internal/api",
|
||||
frontend_public_url: "https://api-v2.truckwash.io/internal/frontend",
|
||||
}),
|
||||
])
|
||||
);
|
||||
await expect(page.getByTestId("coolify-gateway-route-result")).toContainText("Route deploy");
|
||||
await expect(page.getByTestId("coolify-gateway-route-result")).toContainText("Applied 1");
|
||||
await page.getByRole("button", { name: "Deploy latest API code" }).click();
|
||||
expect(state.apiCodeDeploys.at(-1)).toEqual({ dry_run: false, deploy_routes: true });
|
||||
expect(state.apiCodePlans.at(-1)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
public_url: "https://api-v2.truckwash.io/internal/api",
|
||||
}),
|
||||
])
|
||||
);
|
||||
await expect(page.getByTestId("coolify-gateway-code-deploy-result")).toContainText("API code deploy");
|
||||
await expect(page.getByTestId("coolify-gateway-code-deploy-result")).toContainText("Applied 1");
|
||||
await expect(page.getByTestId("coolify-gateway-code-deploy-result")).toContainText("Route applied 1");
|
||||
await expect(page.getByTestId("coolify-instances-table")).toContainText("Production Coolify");
|
||||
await expect(page.getByTestId("coolify-targets-table")).toContainText("failover_ready");
|
||||
|
||||
const form = page.getByTestId("coolify-instance-form");
|
||||
await form.locator('input[type="text"]').nth(0).fill("Staging Coolify");
|
||||
await form.locator('input[type="url"]').fill("https://staging-coolify.example.test");
|
||||
await form.locator('input[type="password"]').fill("token-secret");
|
||||
await expect(form.getByText("Project UUID")).toHaveCount(0);
|
||||
await expect(form.getByText("Server UUID")).toHaveCount(0);
|
||||
await form.getByRole("button", { name: "Save" }).click();
|
||||
|
||||
await expect(page.getByTestId("coolify-instances-table")).toContainText("Staging Coolify");
|
||||
await page.getByTestId("coolify-instances-table").getByRole("button", { name: "Test" }).first().click();
|
||||
await expect(page.getByTestId("coolify-instances-table")).toContainText("ok");
|
||||
|
||||
await page.getByTestId("coolify-targets-table").getByRole("button", { name: "Reconcile" }).first().click();
|
||||
await expect.poll(() => state.reconciledTargets).toContain(7);
|
||||
await expect(page.getByTestId("coolify-targets-table")).toContainText("failover_ready");
|
||||
|
||||
page.once("dialog", async (dialog) => {
|
||||
expect(dialog.message()).toContain("delete-coolify-target-7");
|
||||
await dialog.accept("delete-coolify-target-7");
|
||||
});
|
||||
await page.getByTestId("coolify-targets-table").getByRole("button", { name: "Delete" }).first().click();
|
||||
await expect(page.getByTestId("coolify-targets-table")).toContainText("No Coolify targets configured.");
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/releases\/integrations\?panel=coolify/);
|
||||
await expect(page.getByRole("heading", { name: "Release Manager" })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId("release-coolify-panel")).toContainText("Coolify");
|
||||
});
|
||||
|
||||
test("@smoke creates Coolify-backed database, Redis, and MinIO replicas from replication management", async ({
|
||||
|
||||
@@ -5,6 +5,57 @@ async function primeSuperuserSession(page) {
|
||||
await primeMockSession(page, { token: "superuser-edge-gateway-token" });
|
||||
}
|
||||
|
||||
const transientDynamicImportPattern = /(?:Failed to fetch dynamically imported module|ERR_NETWORK_CHANGED)/iu;
|
||||
|
||||
async function gotoEdgeAgentTerminal(page, gatewayId = 701) {
|
||||
const terminalPath = `/superuser/selfserve/edge-agents/${encodeURIComponent(String(gatewayId))}/terminal`;
|
||||
const loadErrors = [];
|
||||
const recordLoadError = (error) => {
|
||||
const message = typeof error === "string" ? error : error?.message || String(error || "");
|
||||
if (transientDynamicImportPattern.test(message)) {
|
||||
loadErrors.push(message);
|
||||
}
|
||||
};
|
||||
const onConsole = (message) => {
|
||||
if (message.type() === "error") {
|
||||
recordLoadError(message.text());
|
||||
}
|
||||
};
|
||||
const onPageError = (error) => recordLoadError(error);
|
||||
|
||||
page.on("console", onConsole);
|
||||
page.on("pageerror", onPageError);
|
||||
|
||||
try {
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
loadErrors.length = 0;
|
||||
let navigationError = null;
|
||||
try {
|
||||
await page.goto(terminalPath, { waitUntil: "domcontentloaded" });
|
||||
} catch (error) {
|
||||
navigationError = error;
|
||||
recordLoadError(error);
|
||||
}
|
||||
|
||||
if (navigationError && loadErrors.length === 0) {
|
||||
throw navigationError;
|
||||
}
|
||||
|
||||
const terminalVisible = await page
|
||||
.getByTestId("gateway-terminal-page")
|
||||
.isVisible({ timeout: attempt === 0 ? 5_000 : 10_000 })
|
||||
.catch(() => false);
|
||||
|
||||
if (terminalVisible || loadErrors.length === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
page.off("console", onConsole);
|
||||
page.off("pageerror", onPageError);
|
||||
}
|
||||
}
|
||||
|
||||
async function selectLastGatewayBindingRelay(page, relayId) {
|
||||
const relaySelect = page.locator('[data-testid^="gateway-binding-relay-"]').last();
|
||||
await expect
|
||||
@@ -919,14 +970,11 @@ test.describe("Edge gateway management smoke", () => {
|
||||
};
|
||||
};
|
||||
|
||||
await page.route(
|
||||
/https?:\/\/(?:api\.truckwash\.io(?::\d+)?)\/modules\/edge-gateways\/workspace\/departments\/1(?:\?.*)?$/i,
|
||||
async (route) => {
|
||||
await route.fulfill(json({ data: buildGateWorkspace() }));
|
||||
}
|
||||
);
|
||||
await page.route(/\/modules\/edge-gateways\/workspace\/departments\/1(?:\?.*)?$/i, async (route) => {
|
||||
await route.fulfill(json({ data: buildGateWorkspace() }));
|
||||
});
|
||||
|
||||
await page.route(/https?:\/\/(?:api\.truckwash\.io(?::\d+)?)\/department\/gates(?:\?.*)?$/i, async (route) => {
|
||||
await page.route(/\/department\/gates(?:\?.*)?$/i, async (route) => {
|
||||
const request = route.request();
|
||||
const method = request.method();
|
||||
|
||||
@@ -1104,7 +1152,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/terminal", { waitUntil: "domcontentloaded" });
|
||||
await gotoEdgeAgentTerminal(page);
|
||||
|
||||
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/not connected to the broker/i);
|
||||
@@ -1133,7 +1181,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/terminal", { waitUntil: "domcontentloaded" });
|
||||
await gotoEdgeAgentTerminal(page);
|
||||
|
||||
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/Broker did not complete/i, {
|
||||
@@ -1154,7 +1202,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
await primeSuperuserSession(page);
|
||||
|
||||
await page.goto("/superuser/selfserve/edge-agents/701/terminal", { waitUntil: "domcontentloaded" });
|
||||
await gotoEdgeAgentTerminal(page);
|
||||
|
||||
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/connecting|open/i);
|
||||
|
||||
@@ -2304,7 +2304,8 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
};
|
||||
});
|
||||
expect(laneScopeOptionMetrics.bottomGap).toBeLessThanOrEqual(2);
|
||||
expect(laneScopeOptionMetrics.optionHeight).toBeGreaterThan(80);
|
||||
const minimumScopeOptionHeight = (page.viewportSize()?.width ?? 1024) < 640 ? 44 : 80;
|
||||
expect(laneScopeOptionMetrics.optionHeight).toBeGreaterThanOrEqual(minimumScopeOptionHeight);
|
||||
await page.getByTestId("studio-scope-option-lane-7").click();
|
||||
await page.getByTestId("studio-scope-option-vehicle-8").click();
|
||||
await page.getByTestId("studio-scope-apply").click();
|
||||
@@ -2847,20 +2848,29 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
const sidebar = document.querySelector(".studio-sidebar")?.getBoundingClientRect();
|
||||
const workspace = document.querySelector('[data-testid="studio-simulator-workspace"]')?.getBoundingClientRect();
|
||||
return {
|
||||
shellLeft: shell?.left || 0,
|
||||
shellRight: shell?.right || 0,
|
||||
shellWidth: shell?.width || 0,
|
||||
sidebarBottom: sidebar?.bottom || 0,
|
||||
sidebarRight: sidebar?.right || 0,
|
||||
sidebarWidth: sidebar?.width || 0,
|
||||
workspaceLeft: workspace?.left || 0,
|
||||
workspaceRight: workspace?.right || 0,
|
||||
workspaceTop: workspace?.top || 0,
|
||||
workspaceWidth: workspace?.width || 0,
|
||||
};
|
||||
});
|
||||
expect(simulatorLayoutMetrics.workspaceLeft).toBeGreaterThanOrEqual(simulatorLayoutMetrics.sidebarRight - 4);
|
||||
expect(simulatorLayoutMetrics.workspaceRight).toBeGreaterThanOrEqual(simulatorLayoutMetrics.shellRight - 8);
|
||||
expect(simulatorLayoutMetrics.workspaceWidth).toBeGreaterThan(
|
||||
simulatorLayoutMetrics.shellWidth - simulatorLayoutMetrics.sidebarWidth - 40
|
||||
);
|
||||
if ((page.viewportSize()?.width ?? 1024) < 820) {
|
||||
expect(simulatorLayoutMetrics.workspaceTop).toBeGreaterThanOrEqual(simulatorLayoutMetrics.sidebarBottom - 4);
|
||||
expect(simulatorLayoutMetrics.workspaceLeft).toBeGreaterThanOrEqual(simulatorLayoutMetrics.shellLeft - 8);
|
||||
expect(simulatorLayoutMetrics.workspaceWidth).toBeGreaterThan(simulatorLayoutMetrics.shellWidth - 40);
|
||||
} else {
|
||||
expect(simulatorLayoutMetrics.workspaceLeft).toBeGreaterThanOrEqual(simulatorLayoutMetrics.sidebarRight - 4);
|
||||
expect(simulatorLayoutMetrics.workspaceRight).toBeGreaterThanOrEqual(simulatorLayoutMetrics.shellRight - 8);
|
||||
expect(simulatorLayoutMetrics.workspaceWidth).toBeGreaterThan(
|
||||
simulatorLayoutMetrics.shellWidth - simulatorLayoutMetrics.sidebarWidth - 40
|
||||
);
|
||||
}
|
||||
await expect(page.getByTestId("studio-hardware-mode")).toContainText("Studio");
|
||||
await expect(page.getByTestId("studio-simulator-scope")).toContainText("Lane 7");
|
||||
await expect(page.getByTestId("studio-simulator-scope")).toContainText("Bus");
|
||||
@@ -3051,6 +3061,53 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
await expect(page.getByTestId("studio-path-editor-answer-12-yes")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("uses product scoped vehicle ids when Roskilde vehicle rows differ from products", async ({ page }) => {
|
||||
const graph = buildStudioGraph();
|
||||
graph.lookups.vehicle_types = graph.lookups.vehicle_types.map((vehicleType) => ({
|
||||
...vehicleType,
|
||||
id: Number(vehicleType.id) === 2 ? 202 : 808,
|
||||
}));
|
||||
graph.lookups.labels.vehicle_types = { 202: "Forvogn", 808: "Bus" };
|
||||
graph.simulator_defaults.vehicle_type_id = 202;
|
||||
const captured = {
|
||||
graphSaves: [],
|
||||
layouts: [],
|
||||
simulations: [],
|
||||
simulationDelays: [],
|
||||
pathOutcomes: [],
|
||||
dynamicImages: [],
|
||||
gatewayActions: [],
|
||||
virtualHardware: [],
|
||||
rollbacks: [],
|
||||
published: 0,
|
||||
};
|
||||
await installStudioRoutes(page, graph, captured);
|
||||
|
||||
await page.goto("/admin/6/modules/self-serve/studio?lane_id=7&vehicle_type_id=2", {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 90_000,
|
||||
});
|
||||
await expect(page.getByTestId("self-serve-studio")).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId("studio-panel-pathEditor").click();
|
||||
await expect(page.getByTestId("studio-path-cases")).toContainText("Allowed", { timeout: 15_000 });
|
||||
await expect(page.getByTestId("studio-path-verification-board")).toContainText("Forvogn");
|
||||
|
||||
await expect.poll(() => captured.pathOutcomes.at(-1)?.vehicle_type_id).toBe(2);
|
||||
await page.getByTestId("studio-path-editor-save").click();
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
captured.graphSaves.findLast(
|
||||
(save) => save.operations?.[0]?.action === "upsert_path" && save.operations?.[0]?.entity === "path"
|
||||
)?.operations?.[0]?.data?.scope?.vehicle_type_id
|
||||
)
|
||||
.toBe(2);
|
||||
|
||||
await page.getByTestId("studio-path-case-path-1").getByRole("button", { name: "Open in simulator" }).click();
|
||||
await expect(page.getByTestId("studio-simulator-workspace")).toBeVisible();
|
||||
await expect.poll(() => captured.simulations.at(-1)?.vehicle_type_id).toBe(2);
|
||||
});
|
||||
|
||||
test("creates a scoped question from the path editor without leaving the panel", async ({ page }) => {
|
||||
const graph = buildStudioGraph();
|
||||
const captured = {
|
||||
|
||||
@@ -50,18 +50,20 @@ async function advanceGuidedWashToLastStep(page) {
|
||||
}
|
||||
}
|
||||
|
||||
function waitForLaneCommandRequest(page, command) {
|
||||
return page.waitForRequest((request) => {
|
||||
if (!request.url().includes("/modules/self-serve/lane/command")) {
|
||||
async function waitForLaneCommandRequest(page, command) {
|
||||
const request = await page.waitForRequest((candidateRequest) => {
|
||||
if (!candidateRequest.url().includes("/modules/self-serve/lane/command")) {
|
||||
return false;
|
||||
}
|
||||
if (request.method() !== "POST") {
|
||||
if (candidateRequest.method() !== "POST") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const body = request.postDataJSON?.() || {};
|
||||
const body = candidateRequest.postDataJSON?.() || {};
|
||||
return body.command === command;
|
||||
});
|
||||
await request.response();
|
||||
return request;
|
||||
}
|
||||
|
||||
function buildSavedProgress(overrides = {}) {
|
||||
@@ -234,7 +236,7 @@ test.describe("Self-serve wash", () => {
|
||||
lane_id: 7,
|
||||
command: "OPEN_PROPERTY_EXIT_GATE",
|
||||
});
|
||||
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible();
|
||||
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible({ timeout: 15_000 });
|
||||
await page.getByTestId("self-serve-nav-close").click();
|
||||
await expect(page.getByTestId("self-serve-completed-step")).toBeHidden();
|
||||
await expect(page.getByTestId("self-serve-vehicle-step")).toBeVisible();
|
||||
@@ -857,7 +859,7 @@ test.describe("Self-serve wash", () => {
|
||||
command: "OPEN_PROPERTY_EXIT_GATE",
|
||||
});
|
||||
await expect(page.getByTestId("self-serve-action-error")).toBeHidden();
|
||||
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible();
|
||||
await expect(page.getByTestId("self-serve-completed-step")).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByTestId("self-serve-live-elapsed")).toBeHidden();
|
||||
});
|
||||
|
||||
@@ -981,6 +983,7 @@ test.describe("Self-serve wash", () => {
|
||||
permissions: ["user"],
|
||||
selfServe: true,
|
||||
});
|
||||
api.selfServe.answerResponseDelayMs = 1000;
|
||||
const preview = api.selfServe.previewByKey["7:AB12345"];
|
||||
const primaryQuestion = preview.questions[0];
|
||||
const followUpQuestion = {
|
||||
@@ -1014,7 +1017,29 @@ test.describe("Self-serve wash", () => {
|
||||
.poll(async () => questionCards.evaluateAll((cards) => cards.map((card) => card.getAttribute("data-testid"))))
|
||||
.toEqual(["self-serve-question-11", "self-serve-question-21"]);
|
||||
|
||||
const firstQuestionBefore = await page.getByTestId("self-serve-question-11").boundingBox();
|
||||
const firstQuestion = page.getByTestId("self-serve-question-11");
|
||||
let stableQuestionBoxSamples = 0;
|
||||
let previousQuestionBoxKey = "";
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const [box, viewport] = await Promise.all([firstQuestion.boundingBox(), page.viewportSize()]);
|
||||
if (!box || !viewport || box.x < 0 || box.x >= viewport.width / 2) {
|
||||
stableQuestionBoxSamples = 0;
|
||||
previousQuestionBoxKey = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
const boxKey = [box.x, box.y, box.width, box.height].map((value) => Math.round(value)).join(":");
|
||||
stableQuestionBoxSamples = boxKey === previousQuestionBoxKey ? stableQuestionBoxSamples + 1 : 0;
|
||||
previousQuestionBoxKey = boxKey;
|
||||
return stableQuestionBoxSamples >= 2;
|
||||
},
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
const firstQuestionBefore = await firstQuestion.boundingBox();
|
||||
expect(firstQuestionBefore).not.toBeNull();
|
||||
|
||||
const reorderedAnsweredQuestions = [{ ...followUpQuestion }, { ...primaryQuestion, answer: false }];
|
||||
@@ -1034,7 +1059,7 @@ test.describe("Self-serve wash", () => {
|
||||
await expect(noButton).toHaveClass(/is-danger/);
|
||||
await expect(noButton).not.toHaveClass(/is-light/);
|
||||
await expect(page.getByTestId("self-serve-questions-inline-loading")).toBeVisible();
|
||||
const firstQuestionWhileLoading = await page.getByTestId("self-serve-question-11").boundingBox();
|
||||
const firstQuestionWhileLoading = await firstQuestion.boundingBox();
|
||||
expect(firstQuestionWhileLoading).not.toBeNull();
|
||||
expect(Math.abs((firstQuestionWhileLoading?.y || 0) - (firstQuestionBefore?.y || 0))).toBeLessThanOrEqual(4);
|
||||
|
||||
|
||||
@@ -851,13 +851,15 @@ test.describe("Superuser system status smoke", () => {
|
||||
await expect(page.getByTestId("replication-host-database-2").locator(".buttons.are-small")).toHaveCount(0);
|
||||
await expect(primaryActions.locator(".action-settings-wheel-trigger")).toBeVisible();
|
||||
await primaryActions.locator(".action-settings-wheel-trigger").click();
|
||||
await expect(primaryActions.locator(".dropdown-item-action:visible")).toHaveCount(1);
|
||||
await expect(primaryActions.locator(".dropdown-item-action:visible")).toHaveCount(2);
|
||||
await expect(primaryActions.getByRole("button", { name: "Test" })).toBeVisible();
|
||||
await expect(primaryActions.getByRole("button", { name: "Omdøb" })).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(replicaActions.locator(".action-settings-wheel-trigger")).toBeVisible();
|
||||
const replicaActionTrigger = replicaActions.locator(".action-settings-wheel-trigger");
|
||||
await replicaActionTrigger.click();
|
||||
await expect(replicaActions.locator(".dropdown-item-action:visible")).toHaveCount(3);
|
||||
await expect(replicaActions.locator(".dropdown-item-action:visible")).toHaveCount(4);
|
||||
await expect(replicaActions.getByRole("button", { name: "Omdøb" })).toBeVisible();
|
||||
const [triggerBox, menuBox] = await Promise.all([
|
||||
replicaActionTrigger.boundingBox(),
|
||||
replicaActions.locator(".dropdown-content").boundingBox(),
|
||||
|
||||
@@ -3203,6 +3203,16 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
(pathname.endsWith("/order-bookings/booking-confirmation/resend") ||
|
||||
pathname.endsWith("/order-bookings/completion-confirmation/resend")) &&
|
||||
method === "POST"
|
||||
) {
|
||||
const body = request.postDataJSON?.() || {};
|
||||
await route.fulfill(json({ success: true, data: { id: Number(body.id || 0), sent: true } }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/department/numberplatescanners") && method === "GET") {
|
||||
await route.fulfill(json({ success: true, data: posFixture.numberPlateScanners || [] }));
|
||||
return true;
|
||||
@@ -3802,6 +3812,12 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/orders/wash-certificate/resend") && method === "POST") {
|
||||
const body = request.postDataJSON?.() || {};
|
||||
await route.fulfill(json({ success: true, data: { id: Number(body.id || 0), sent: true } }));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/departments/order/recommended") && method === "GET") {
|
||||
await route.fulfill(
|
||||
json({
|
||||
|
||||
@@ -89,6 +89,8 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
},
|
||||
showEditObjectFieldForm: vi.fn(() => Promise.resolve()),
|
||||
functions: {
|
||||
resendBookingCompletionConfirmation: vi.fn(() => Promise.resolve()),
|
||||
resendBookingConfirmation: vi.fn(() => Promise.resolve()),
|
||||
showDeleteConfirmationModal: vi.fn(() => Promise.resolve()),
|
||||
},
|
||||
},
|
||||
@@ -120,6 +122,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
assignDraftCustomer: vi.fn(() => Promise.resolve()),
|
||||
get_department_id: vi.fn(() => Promise.resolve(1)),
|
||||
removeAttachment: vi.fn(() => Promise.resolve({ success: true })),
|
||||
resendWashCertificate: vi.fn(() => Promise.resolve()),
|
||||
showAttachWashCertificateForm: vi.fn(() => Promise.resolve()),
|
||||
showChangeCustomerForm: vi.fn(() => Promise.resolve()),
|
||||
showChangeInvoiceCollectionForm: vi.fn(() => Promise.resolve()),
|
||||
@@ -184,6 +187,7 @@ const SETTINGS_WHEEL_TRANSLATION_KEYS = [
|
||||
"admin.pos.settings_wheel.delete_user",
|
||||
"admin.pos.settings_wheel.download_invoice",
|
||||
"admin.pos.settings_wheel.edit_permissions",
|
||||
"admin.pos.settings_wheel.email_notifications_section",
|
||||
"admin.pos.settings_wheel.enter_password",
|
||||
"admin.pos.settings_wheel.error",
|
||||
"admin.pos.settings_wheel.error_changing_password",
|
||||
@@ -212,6 +216,15 @@ const SETTINGS_WHEEL_TRANSLATION_KEYS = [
|
||||
"admin.pos.settings_wheel.password_changed_text",
|
||||
"admin.pos.settings_wheel.please_enter_password",
|
||||
"admin.pos.settings_wheel.relays_section",
|
||||
"admin.pos.settings_wheel.resend_booking_completion_confirmation",
|
||||
"admin.pos.settings_wheel.resend_booking_completion_confirmation_error",
|
||||
"admin.pos.settings_wheel.resend_booking_completion_confirmation_success",
|
||||
"admin.pos.settings_wheel.resend_booking_confirmation",
|
||||
"admin.pos.settings_wheel.resend_booking_confirmation_error",
|
||||
"admin.pos.settings_wheel.resend_booking_confirmation_success",
|
||||
"admin.pos.settings_wheel.resend_wash_certificate",
|
||||
"admin.pos.settings_wheel.resend_wash_certificate_error",
|
||||
"admin.pos.settings_wheel.resend_wash_certificate_success",
|
||||
"admin.pos.settings_wheel.scan_qr_to_login",
|
||||
"admin.pos.settings_wheel.self_serve_customer",
|
||||
"admin.pos.settings_wheel.self_serve_driver",
|
||||
@@ -461,6 +474,12 @@ describe("ActionSettingsWheelButton", () => {
|
||||
SessionUser.objects.orders.functions.downloadAttachment.mockResolvedValue("https://cdn.example.test/attachment");
|
||||
SessionUser.objects.orders.functions.removeAttachment.mockReset();
|
||||
SessionUser.objects.orders.functions.removeAttachment.mockResolvedValue({ success: true });
|
||||
SessionUser.objects.orders.functions.resendWashCertificate.mockReset();
|
||||
SessionUser.objects.orders.functions.resendWashCertificate.mockResolvedValue({});
|
||||
SessionUser.objects.order_bookings.functions.resendBookingConfirmation.mockReset();
|
||||
SessionUser.objects.order_bookings.functions.resendBookingConfirmation.mockResolvedValue({});
|
||||
SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation.mockReset();
|
||||
SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation.mockResolvedValue({});
|
||||
SessionUser.objects.department_gates.showCreateObjectForm.mockClear();
|
||||
SessionUser.objects.department_relays.showCreateObjectForm.mockClear();
|
||||
SessionUser.objects.collectedOrderInvoices.add.mockReset();
|
||||
@@ -749,6 +768,141 @@ describe("ActionSettingsWheelButton", () => {
|
||||
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.mark_as_completed");
|
||||
});
|
||||
|
||||
it("hides email notifications when the order has no booking or wash certificate", async () => {
|
||||
const wrapper = mountFlatDropdownButton({
|
||||
order_id: 42,
|
||||
order_booking_id: null,
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
await wrapper.find(".dropdown-trigger button").trigger("click");
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.email_notifications_section");
|
||||
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_confirmation");
|
||||
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_wash_certificate");
|
||||
});
|
||||
|
||||
it("shows booking email notification actions for booking-only orders", async () => {
|
||||
const wrapper = mountFlatDropdownButton({
|
||||
order_id: 42,
|
||||
order_booking_id: 123,
|
||||
department_id: 1,
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
await wrapper.find(".dropdown-trigger button").trigger("click");
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(wrapper.text()).toContain("admin.pos.settings_wheel.email_notifications_section");
|
||||
expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_booking_confirmation");
|
||||
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_completion_confirmation");
|
||||
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_wash_certificate");
|
||||
|
||||
const button = wrapper
|
||||
.findAll("button.dropdown-item-action")
|
||||
.find((buttonWrapper) => buttonWrapper.text().includes("admin.pos.settings_wheel.resend_booking_confirmation"));
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
await button.trigger("click");
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(SessionUser.objects.order_bookings.functions.resendBookingConfirmation).toHaveBeenCalledWith(123);
|
||||
expect(swalFireMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "admin.pos.settings_wheel.resend_booking_confirmation_success",
|
||||
icon: "success",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("shows booking completion email action when a booking has a wash certificate", async () => {
|
||||
SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([
|
||||
{
|
||||
id: 301,
|
||||
content: {
|
||||
document: "wash_certificate_42.pdf",
|
||||
other: "WASH_CERTIFICATE",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountFlatDropdownButton({
|
||||
order_id: 42,
|
||||
order_booking_id: 123,
|
||||
department_id: 1,
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
await wrapper.find(".dropdown-trigger button").trigger("click");
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(wrapper.text()).toContain("admin.pos.settings_wheel.email_notifications_section");
|
||||
expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_booking_confirmation");
|
||||
expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_booking_completion_confirmation");
|
||||
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_wash_certificate");
|
||||
|
||||
const button = wrapper
|
||||
.findAll("button.dropdown-item-action")
|
||||
.find((buttonWrapper) =>
|
||||
buttonWrapper.text().includes("admin.pos.settings_wheel.resend_booking_completion_confirmation")
|
||||
);
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
await button.trigger("click");
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(SessionUser.objects.order_bookings.functions.resendBookingCompletionConfirmation).toHaveBeenCalledWith(123);
|
||||
expect(swalFireMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "admin.pos.settings_wheel.resend_booking_completion_confirmation_success",
|
||||
icon: "success",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("shows wash certificate email action when an order has a certificate without booking", async () => {
|
||||
SessionUser.objects.orders.functions.fetchAttachments.mockResolvedValue([
|
||||
{
|
||||
id: 302,
|
||||
content: {
|
||||
document: "wash_certificate_42_302.pdf",
|
||||
other: "wash_certificate_42_302.pdf",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const wrapper = mountFlatDropdownButton({
|
||||
order_id: 42,
|
||||
order_booking_id: null,
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
await wrapper.find(".dropdown-trigger button").trigger("click");
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(wrapper.text()).toContain("admin.pos.settings_wheel.email_notifications_section");
|
||||
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_confirmation");
|
||||
expect(wrapper.text()).not.toContain("admin.pos.settings_wheel.resend_booking_completion_confirmation");
|
||||
expect(wrapper.text()).toContain("admin.pos.settings_wheel.resend_wash_certificate");
|
||||
|
||||
const button = wrapper
|
||||
.findAll("button.dropdown-item-action")
|
||||
.find((buttonWrapper) => buttonWrapper.text().includes("admin.pos.settings_wheel.resend_wash_certificate"));
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
await button.trigger("click");
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(SessionUser.objects.orders.functions.resendWashCertificate).toHaveBeenCalledWith(42);
|
||||
expect(swalFireMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "admin.pos.settings_wheel.resend_wash_certificate_success",
|
||||
icon: "success",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("routes 'change invoice collection' action without opening a new tab", async () => {
|
||||
const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
||||
|
||||
|
||||
@@ -1280,4 +1280,61 @@ describe("MyWashStart", () => {
|
||||
"display: none"
|
||||
);
|
||||
});
|
||||
|
||||
it("clears restored in-progress state when active wash refresh says the lane is no longer in progress", async () => {
|
||||
mocks.restoredProgressPayload = {
|
||||
washInProgress: true,
|
||||
washLaneId: 7,
|
||||
washStartTime: Date.now() - 30_000,
|
||||
licensePlateInput: "AB12345",
|
||||
vehicleTypeSelect: 2,
|
||||
radioWashType: "Manual",
|
||||
radioLaneOption: 7,
|
||||
customerNumberInput: 12345679,
|
||||
isForcingNearestDepartment: false,
|
||||
forceNearestDepartmentEvaluationId: 0,
|
||||
answers: {},
|
||||
completedTasks: {},
|
||||
currentStep: 4,
|
||||
};
|
||||
mocks.sessionRequest.mockImplementation(async (path, method, payload) => {
|
||||
if (path === "/modules/self-serve/lane/wash/in-progress" && method === "GET" && payload?.lane_id === 7) {
|
||||
return {
|
||||
data: {
|
||||
data: {
|
||||
lane_id: 7,
|
||||
in_progress: false,
|
||||
session: null,
|
||||
customer: null,
|
||||
vehicle: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const wrapper = mountWithApp(MyWashStart, {
|
||||
global: {
|
||||
stubs: stubComponents,
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await nextTick();
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.sessionRequest).toHaveBeenCalledWith("/modules/self-serve/lane/wash/in-progress", "GET", {
|
||||
lane_id: 7,
|
||||
});
|
||||
expect(mocks.stopElapsedTimer).toHaveBeenCalled();
|
||||
expect(mocks.clearProgress).toHaveBeenCalled();
|
||||
expect(mocks.fetchWashSummary).not.toHaveBeenCalled();
|
||||
expect(wrapper.get('[data-testid="self-serve-bottom-actions"]').attributes("style") || "").toContain(
|
||||
"display: none"
|
||||
);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
classifyTest,
|
||||
ownedFilesByRole,
|
||||
parseListedTests,
|
||||
titleRules,
|
||||
} from "../../scripts/run-playwright-full-slice.mjs";
|
||||
|
||||
const root = process.cwd();
|
||||
const playwrightCliPath = join(root, "node_modules", "@playwright", "test", "cli.js");
|
||||
const titleRuleFiles = new Set(titleRules.map((rule) => rule.file));
|
||||
const e2eSpecFiles = readdirSync(join(root, "tests/e2e"))
|
||||
.filter((file) => /\.spec\.(?:js|ts)$/u.test(file))
|
||||
.sort();
|
||||
|
||||
describe("Playwright full-slice ownership", () => {
|
||||
it("assigns every top-level e2e spec to one role or a title rule", () => {
|
||||
const directOwners = new Map();
|
||||
|
||||
for (const [role, files] of Object.entries(ownedFilesByRole)) {
|
||||
for (const file of files) {
|
||||
if (directOwners.has(file)) {
|
||||
throw new Error(`${file} is owned by both ${directOwners.get(file)} and ${role}.`);
|
||||
}
|
||||
|
||||
directOwners.set(file, role);
|
||||
}
|
||||
}
|
||||
|
||||
const coveredFiles = new Set([...directOwners.keys(), ...titleRuleFiles]);
|
||||
const uncoveredFiles = e2eSpecFiles.filter((file) => !coveredFiles.has(file));
|
||||
|
||||
expect(uncoveredFiles).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps shared and infrastructure specs in their intended shards", () => {
|
||||
expect(ownedFilesByRole.admin).toEqual(expect.arrayContaining(["default-mobile-redirect.spec.ts"]));
|
||||
expect(ownedFilesByRole.superuser).toEqual(
|
||||
expect.arrayContaining([
|
||||
"coolify-infrastructure.spec.js",
|
||||
"errorReports.spec.ts",
|
||||
"failover-config.source.spec.ts",
|
||||
"release-manager.spec.js",
|
||||
"session-bootstrap.spec.ts",
|
||||
"superuser-users.spec.ts",
|
||||
])
|
||||
);
|
||||
expect(ownedFilesByRole.customer).toEqual(
|
||||
expect.arrayContaining([
|
||||
"guest-book-wash-mobile.spec.ts",
|
||||
"i18n-catalog-switch.spec.ts",
|
||||
"i18n-v2-integrity.spec.ts",
|
||||
"release-bootstrap.spec.js",
|
||||
"release-channel-switched.spec.js",
|
||||
"release-channel-unavailable.spec.js",
|
||||
"release-update-widget.spec.js",
|
||||
"session-release-runtime.spec.ts",
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("classifies every listed test before role filtering", () => {
|
||||
const listOutput = execFileSync(
|
||||
process.execPath,
|
||||
[playwrightCliPath, "test", "--list", "--project=chromium-desktop"],
|
||||
{
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
}
|
||||
);
|
||||
const classificationErrors = [];
|
||||
|
||||
for (const testEntry of parseListedTests(listOutput)) {
|
||||
try {
|
||||
classifyTest(testEntry);
|
||||
} catch (error) {
|
||||
classificationErrors.push(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
expect(classificationErrors).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -116,12 +116,17 @@ vi.mock("sweetalert2", () => ({
|
||||
}));
|
||||
|
||||
import axios from "axios";
|
||||
import { getAttributes } from "@/components/shop/CustomerAttributes.vue";
|
||||
import { getNotes } from "@/components/shop/CustomerNotes.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import {
|
||||
clearActivePosOrderContext,
|
||||
clearCustomerSelection,
|
||||
clearStoredPosOrderId,
|
||||
createOrder,
|
||||
completed_at,
|
||||
customer_data,
|
||||
customer_id,
|
||||
customer_name,
|
||||
department_id,
|
||||
@@ -137,6 +142,7 @@ import {
|
||||
reg_2,
|
||||
reg_3,
|
||||
restoreStoredPosOrderId,
|
||||
searchAndSelectCustomer,
|
||||
scan_data,
|
||||
scans,
|
||||
step,
|
||||
@@ -192,6 +198,77 @@ describe("POSDepartmentProcess.loadOrderItems", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("POSDepartmentProcess.searchAndSelectCustomer", () => {
|
||||
beforeEach(() => {
|
||||
clearCustomerSelection();
|
||||
authenticatedRequest.mockReset();
|
||||
getNotes.mockReset();
|
||||
getNotes.mockResolvedValue({ data: { data: [] } });
|
||||
getAttributes.mockReset();
|
||||
getAttributes.mockResolvedValue({ data: { success: true, data: [] } });
|
||||
});
|
||||
|
||||
const mockCustomerResponse = (customerNumber = 12345679, name = "Pleno Logistics") => ({
|
||||
data: {
|
||||
data: {
|
||||
customer_number: customerNumber,
|
||||
customer_name: name,
|
||||
economic_customer: {
|
||||
customerNumber,
|
||||
name,
|
||||
barred: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("returns the already selected customer without a duplicate lookup", async () => {
|
||||
authenticatedRequest.mockResolvedValueOnce(mockCustomerResponse());
|
||||
|
||||
const selectedCustomer = await searchAndSelectCustomer(12345679);
|
||||
const cachedCustomer = await searchAndSelectCustomer(12345679);
|
||||
|
||||
expect(authenticatedRequest).toHaveBeenCalledTimes(1);
|
||||
expect(authenticatedRequest).toHaveBeenCalledWith("/users/customer?customer_number=12345679", "GET");
|
||||
expect(cachedCustomer).toBe(customer_data.value);
|
||||
expect(cachedCustomer).toEqual(selectedCustomer);
|
||||
expect(customer_id.value).toBe(12345679);
|
||||
expect(customer_name.value).toBe("Pleno Logistics");
|
||||
});
|
||||
|
||||
it("shares concurrent lookups for the same customer number", async () => {
|
||||
let resolveRequest;
|
||||
authenticatedRequest.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveRequest = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
const firstLookup = searchAndSelectCustomer(12345679);
|
||||
const secondLookup = searchAndSelectCustomer(12345679);
|
||||
resolveRequest(mockCustomerResponse());
|
||||
|
||||
const [firstCustomer, secondCustomer] = await Promise.all([firstLookup, secondLookup]);
|
||||
|
||||
expect(authenticatedRequest).toHaveBeenCalledTimes(1);
|
||||
expect(firstCustomer).toEqual(secondCustomer);
|
||||
expect(customer_id.value).toBe(12345679);
|
||||
});
|
||||
|
||||
it("performs a new lookup when forceRefresh is requested", async () => {
|
||||
authenticatedRequest
|
||||
.mockResolvedValueOnce(mockCustomerResponse(12345679, "Pleno Logistics"))
|
||||
.mockResolvedValueOnce(mockCustomerResponse(12345679, "Pleno Logistics Updated"));
|
||||
|
||||
await searchAndSelectCustomer(12345679);
|
||||
const refreshedCustomer = await searchAndSelectCustomer(12345679, { forceRefresh: true });
|
||||
|
||||
expect(authenticatedRequest).toHaveBeenCalledTimes(2);
|
||||
expect(refreshedCustomer.name).toBe("Pleno Logistics Updated");
|
||||
expect(customer_name.value).toBe("Pleno Logistics Updated");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POSDepartmentProcess.clearActivePosOrderContext", () => {
|
||||
it("clears active order, customer, metadata and stored mobile order id", () => {
|
||||
localStorage.setItem("pos_order_id", "51211");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @vitest-environment jsdom
|
||||
import { nextTick, ref } from "vue";
|
||||
import { flushPromises } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mountWithApp } from "./helpers/mountWithApp.js";
|
||||
|
||||
@@ -223,9 +224,17 @@ const LicensePlateReg1InputStub = {
|
||||
emitLinkedVehicle() {
|
||||
this.$emit("update:vehicleObject", { ...linkedVehicle });
|
||||
},
|
||||
emitLinkedVehicleTwice() {
|
||||
this.$emit("update:vehicleObject", { ...linkedVehicle });
|
||||
this.$emit("update:vehicleObject", { ...linkedVehicle });
|
||||
},
|
||||
emitBookedVehicle() {
|
||||
this.$emit("update:vehicleObject", { ...bookedVehicle });
|
||||
},
|
||||
emitLinkedBookingMatchesTwice() {
|
||||
this.$emit("update:bookingMatches", [{ ...bookingWithItems }]);
|
||||
this.$emit("update:bookingMatches", [{ ...bookingWithItems }]);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
@@ -243,6 +252,13 @@ const LicensePlateReg1InputStub = {
|
||||
>
|
||||
Emit linked vehicle
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="emit-linked-vehicle-twice"
|
||||
@click="emitLinkedVehicleTwice"
|
||||
>
|
||||
Emit linked vehicle twice
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="emit-booked-vehicle"
|
||||
@@ -250,6 +266,13 @@ const LicensePlateReg1InputStub = {
|
||||
>
|
||||
Emit booked vehicle
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="emit-linked-booking-matches-twice"
|
||||
@click="emitLinkedBookingMatchesTwice"
|
||||
>
|
||||
Emit linked booking matches twice
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -422,6 +445,27 @@ describe("SelectVehicleFormPOS", () => {
|
||||
expect(wrapper.findAll('[data-testid="customer-search-card-payment-stub"]')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("ignores repeated identical linked vehicle emissions", async () => {
|
||||
const wrapper = mountForm();
|
||||
|
||||
await wrapper.get('[data-testid="emit-linked-vehicle-twice"]').trigger("click");
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.findAll('[data-testid="customer-search-field-pos-stub"]')).toHaveLength(0);
|
||||
expect(posState.searchAndSelectCustomer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not re-apply identical booking matches", async () => {
|
||||
const wrapper = mountForm();
|
||||
|
||||
await wrapper.get('[data-testid="emit-linked-booking-matches-twice"]').trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(posState.setSelectedOrderBookingSelection).toHaveBeenCalledTimes(1);
|
||||
expect(posState.searchAndSelectCustomer).toHaveBeenCalledTimes(1);
|
||||
expect(posState.searchAndSelectCustomer).toHaveBeenCalledWith(12345679);
|
||||
});
|
||||
|
||||
it("hides a booked desktop vehicle summary after continuing without booking", async () => {
|
||||
const wrapper = mountForm();
|
||||
|
||||
|
||||
@@ -45,7 +45,9 @@ describe("self-serve studio vehicle type scope", () => {
|
||||
expect(source).toContain("const selectedSimulatorVehicleTypeId = computed(() =>");
|
||||
expect(source).toContain("vehicleTypeMatchesFilter(vehicleType, selectedValue)");
|
||||
expect(source).toContain("lane_id: laneId");
|
||||
expect(source).toContain("vehicle_type_id: selectedSimulatorVehicleTypeId.value");
|
||||
expect(source).toContain("vehicle_type_id: selectedSimulatorVehicleTypeScopeId.value");
|
||||
expect(source).toContain("const selectedSimulatorVehicleTypeScopeId = computed(() => {");
|
||||
expect(source).toContain("return firstPositiveInt(selectedRow.product, selectedRow.product_id, selectedRow.id);");
|
||||
expect(source).toContain('data-testid="studio-simulator-scope"');
|
||||
expect(source).not.toContain('v-model="simulatorForm.lane_id"');
|
||||
expect(source).not.toContain('v-model="simulatorForm.vehicle_type_id"');
|
||||
|
||||
@@ -37,6 +37,11 @@ const PUBLIC_ASSET_ALIASES = [
|
||||
]
|
||||
|
||||
function getGitCommit() {
|
||||
const envCommit = firstReleaseCommitEnv()
|
||||
if (envCommit) {
|
||||
return envCommit.slice(0, 12)
|
||||
}
|
||||
|
||||
try {
|
||||
return execSync('git rev-parse --short HEAD').toString().trim()
|
||||
} catch {
|
||||
@@ -45,6 +50,11 @@ function getGitCommit() {
|
||||
}
|
||||
|
||||
function getGitCommitSha() {
|
||||
const envCommit = firstReleaseCommitEnv()
|
||||
if (envCommit) {
|
||||
return envCommit
|
||||
}
|
||||
|
||||
try {
|
||||
return execSync('git rev-parse HEAD').toString().trim()
|
||||
} catch {
|
||||
@@ -52,6 +62,16 @@ function getGitCommitSha() {
|
||||
}
|
||||
}
|
||||
|
||||
function firstReleaseCommitEnv() {
|
||||
for (const key of ['RELEASE_COMMIT_SHA', 'SOURCE_COMMIT', 'COMMIT_SHA', 'GITHUB_SHA', 'VITE_COMMIT_HASH']) {
|
||||
const value = String(process.env[key] || '').trim()
|
||||
if (/^[0-9a-f]{7,40}$/i.test(value)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function releaseBuildId(commitSha) {
|
||||
const explicitBuildId = process.env.RELEASE_BUILD_ID || process.env.BUILD_ID
|
||||
if (explicitBuildId) {
|
||||
|
||||
Reference in New Issue
Block a user