From 625757fafc9cd8b760eeeb59e943be3602f5943f Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Wed, 27 May 2026 17:34:51 +0200 Subject: [PATCH] Fix Danish translations and enhance mobile department auto-selection logic --- .github/workflows/release.yml | 5 +- playwright.global-setup.mjs | 62 +- scripts/run-playwright-pr.mjs | 53 +- .../displays/buttons/DatePeriodSelector.vue | 2 +- .../pos/steps/PosDepartmentStep1.vue | 2 +- .../department/tables/SelfServeTryModal.vue | 38 +- .../displays/selfServe/SelfServeLaneStep.vue | 110 +- .../selfServe/SelfServeQuestionsStep.vue | 2 +- .../displays/selfServe/SelfServeTaskList.vue | 11 +- .../SelfServeVehicleTypeSelector.vue | 4 +- .../displays/user/vehicles/vehiclesTable.vue | 2 +- .../release/ReleaseOperationTimeline.vue | 53 +- .../DepartmentDailyReportComplaints.vue | 12 +- src/composables/useSelfServeLogic.js | 14 +- src/composables/useWashProgress.js | 5 + src/composables/useWashSessionActions.js | 8 +- src/i18n/generated/da-v2.json | 16 +- src/i18n/locales/da.json | 6 + src/i18n/locales/de.json | 6 + src/i18n/locales/en.json | 6 + src/i18n/locales/no.json | 6 + src/i18n/locales/sv.json | 6 + .../phrases/compat/configuration/index.json | 2 +- .../da/phrases/compat/replication/index.json | 2 +- .../da/phrases/compat/self_wash/index.json | 12 +- src/services/selfServeDynamicImage.js | 27 +- src/views/DefaultPage.vue | 212 +- ...partmentDashboardDailyReportComplaints.vue | 16 +- .../self-serve/DepartmentSelfServeStudio.vue | 1835 ++++++++++++++++- .../CollectedOrderInvoicesQueueHistory.vue | 2 +- .../ConfigurationReleaseManager.vue | 20 + .../InvoiceDistributionOverview.vue | 2 +- .../userDashboard/wash/MyWashStart.vue | 283 ++- tests/e2e/admin-daily-report.spec.ts | 2 +- tests/e2e/default-mobile-redirect.spec.ts | 51 + tests/e2e/i18n.smoke.spec.ts | 14 + tests/e2e/release-channel-unavailable.spec.js | 2 +- tests/e2e/release-manager.spec.js | 45 + tests/e2e/self-serve-studio-flow.spec.js | 469 ++++- tests/e2e/self-serve-wash.spec.js | 70 +- tests/e2e/support/network.js | 6 +- .../unit/default-page-mobile-redirect.spec.js | 210 ++ ...department-daily-report-complaints.spec.js | 2 +- tests/unit/my-wash-start.spec.js | 255 ++- tests/unit/navigation-menu-department.spec.js | 4 +- tests/unit/self-serve-danish-labels.spec.js | 13 + tests/unit/self-serve-dynamic-image.spec.js | 18 +- tests/unit/self-serve-lane-step.spec.js | 21 +- tests/unit/self-serve-task-list.spec.js | 4 +- tests/unit/self-serve-try-modal.spec.js | 19 +- .../unit/self-serve-vehicle-selector.spec.js | 10 + tests/unit/use-wash-progress.spec.js | 16 + tests/unit/use-wash-session-actions.spec.js | 8 +- tests/unit/weatherapi-wiring.spec.js | 2 +- tests/unit/workfeed-wiring.spec.js | 2 +- vite.config.js | 20 +- 56 files changed, 3757 insertions(+), 348 deletions(-) create mode 100644 tests/e2e/default-mobile-redirect.spec.ts create mode 100644 tests/unit/default-page-mobile-redirect.spec.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5205e9c7..6f94bb4c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,10 +118,13 @@ 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\",\"expected_commit\":\"$RELEASE_EXPECTED_COMMIT\",\"build_id\":\"$RELEASE_EXPECTED_BUILD_ID\",\"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\",\"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\"]}" 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 }} + RELEASE_REPOSITORY: ${{ github.repository }} + RELEASE_BRANCH: ${{ github.ref_name }} + RELEASE_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - name: Update server version after verification run: npm run release:update-server-version diff --git a/playwright.global-setup.mjs b/playwright.global-setup.mjs index 0981b63f..5715042e 100644 --- a/playwright.global-setup.mjs +++ b/playwright.global-setup.mjs @@ -1,4 +1,5 @@ import { execFile, spawn } from "node:child_process"; +import { createWriteStream } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import readline from "node:readline"; @@ -11,15 +12,16 @@ const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://${devHost}:${devPort} const runtimeNamespace = String(process.env.PLAYWRIGHT_ARTIFACT_NAMESPACE || `port-${devPort}`) .trim() .replace(/[^a-zA-Z0-9._-]+/g, "-"); -const pidFile = path.resolve(process.cwd(), "output/playwright", `dev-server-${runtimeNamespace}.json`); +const serverLogDir = path.resolve(process.cwd(), "output/playwright"); +const pidFile = path.resolve(serverLogDir, `dev-server-${runtimeNamespace}.json`); +const stdoutLogFile = path.resolve(serverLogDir, `dev-server-${runtimeNamespace}.stdout.log`); +const stderrLogFile = path.resolve(serverLogDir, `dev-server-${runtimeNamespace}.stderr.log`); +const viteCliPath = path.resolve(process.cwd(), "node_modules/vite/bin/vite.js"); const serverOutputLimit = 80; const activeOutputReaders = []; // Hardlinked Windows worktrees can break Vite's bundled config temp paths during Playwright boot. const viteDevArgs = [ - "run", - "dev", - "--", - "--force", + ...(process.env.PLAYWRIGHT_VITE_FORCE === "1" ? ["--force"] : []), ...(process.platform === "win32" ? ["--configLoader", "runner"] : []), "--host", devHost, @@ -176,14 +178,18 @@ async function killProcessTree(pid) { } } -function captureProcessOutput(stream, lines) { +function captureProcessOutput(stream, lines, logStream) { const reader = readline.createInterface({ input: stream }); reader.on("line", (line) => { lines.push(line); + logStream.write(`${line}\n`); if (lines.length > serverOutputLimit) { lines.splice(0, lines.length - serverOutputLimit); } }); + reader.on("close", () => { + logStream.end(); + }); return reader; } @@ -368,6 +374,10 @@ async function warmModuleGraph(entryUrl, { depth = 2, timeoutMs = 120_000 } = {} } for (const specifier of extractModuleImports(source)) { + if (specifier.startsWith("/node_modules/.vite/deps/")) { + continue; + } + const expectedContentType = resolveExpectedContentType(specifier); const importUrl = new URL(specifier, current.url).toString(); @@ -436,31 +446,25 @@ export default async function globalSetup() { await new Promise((resolve) => setTimeout(resolve, 1000)); } - const serverProcess = - process.platform === "win32" - ? spawn("cmd.exe", ["/d", "/s", "/c", `npm.cmd ${viteDevArgs.join(" ")}`], { - cwd: process.cwd(), - detached: true, - env: { - ...process.env, - PLAYWRIGHT: "1", - }, - stdio: ["ignore", "pipe", "pipe"], - windowsHide: true, - }) - : spawn("npm", viteDevArgs, { - cwd: process.cwd(), - detached: true, - env: { - ...process.env, - PLAYWRIGHT: "1", - }, - stdio: ["ignore", "pipe", "pipe"], - }); + const serverProcess = spawn(process.execPath, [viteCliPath, ...viteDevArgs], { + cwd: process.cwd(), + detached: true, + env: { + ...process.env, + PLAYWRIGHT: "1", + }, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); const stdoutLines = []; const stderrLines = []; - const stdoutReader = captureProcessOutput(serverProcess.stdout, stdoutLines); - const stderrReader = captureProcessOutput(serverProcess.stderr, stderrLines); + const stdoutLogStream = createWriteStream(stdoutLogFile, { flags: "w" }); + const stderrLogStream = createWriteStream(stderrLogFile, { flags: "w" }); + serverProcess.on("exit", (code, signal) => { + stderrLogStream.write(`[playwright-global-setup] vite exited with code ${code ?? "null"} signal ${signal ?? "null"}\n`); + }); + const stdoutReader = captureProcessOutput(serverProcess.stdout, stdoutLines, stdoutLogStream); + const stderrReader = captureProcessOutput(serverProcess.stderr, stderrLines, stderrLogStream); activeOutputReaders.push(stdoutReader, stderrReader); serverProcess.unref(); diff --git a/scripts/run-playwright-pr.mjs b/scripts/run-playwright-pr.mjs index 9fbb952d..d1da717c 100644 --- a/scripts/run-playwright-pr.mjs +++ b/scripts/run-playwright-pr.mjs @@ -159,6 +159,7 @@ async function runPlaywright({ label, commandArgs, artifactSuffix }) { PLAYWRIGHT: "1", PLAYWRIGHT_ARTIFACT_NAMESPACE: getArtifactNamespace(artifactSuffix), PLAYWRIGHT_REPORTER_MODE: "line-html", + PLAYWRIGHT_WORKERS: process.env.PLAYWRIGHT_WORKERS || "1", }, stdio: "inherit", windowsHide: true, @@ -318,11 +319,19 @@ function groupSpecsByProjects(specProjects) { async function runCorePrGate() { const projects = getSelectedProjects(); - return runPlaywright({ - label: `core ${prGrep} gate`, - artifactSuffix: "core", - commandArgs: ["--grep", prGrep, ...buildProjectArgs(projects)], - }); + for (const project of projects) { + const code = await runPlaywright({ + label: `core ${prGrep} gate (${project})`, + artifactSuffix: `core-${project}`, + commandArgs: ["--grep", prGrep, "--project", project], + }); + + if (code !== 0) { + return code; + } + } + + return 0; } async function runChangedSelection(selection) { @@ -332,11 +341,19 @@ async function runChangedSelection(selection) { console.log( `[playwright-pr] Falling back to broader ${smokeGrep} coverage because these changed files were unmapped: ${selection.unmappedFiles.join(", ")}` ); - return runPlaywright({ - label: `fallback ${smokeGrep} gate`, - artifactSuffix: "smoke-fallback", - commandArgs: ["--grep", smokeGrep, "--grep-invert", prGrep, ...buildProjectArgs(projects)], - }); + for (const project of projects) { + const code = await runPlaywright({ + label: `fallback ${smokeGrep} gate (${project})`, + artifactSuffix: `smoke-fallback-${project}`, + commandArgs: ["--grep", smokeGrep, "--grep-invert", prGrep, "--project", project], + }); + + if (code !== 0) { + return code; + } + } + + return 0; } const groups = groupSpecsByProjects(selection.specProjects); @@ -346,14 +363,16 @@ async function runChangedSelection(selection) { } for (const [index, group] of groups.entries()) { - const code = await runPlaywright({ - label: `changed-area specs ${index + 1}/${groups.length}`, - artifactSuffix: `changed-${index + 1}`, - commandArgs: [...group.specs, ...buildProjectArgs(group.projects)], - }); + for (const project of group.projects) { + const code = await runPlaywright({ + label: `changed-area specs ${index + 1}/${groups.length} (${project})`, + artifactSuffix: `changed-${index + 1}-${project}`, + commandArgs: [...group.specs, "--project", project], + }); - if (code !== 0) { - return code; + if (code !== 0) { + return code; + } } } diff --git a/src/components/displays/buttons/DatePeriodSelector.vue b/src/components/displays/buttons/DatePeriodSelector.vue index bc9bb126..8ae23ce5 100644 --- a/src/components/displays/buttons/DatePeriodSelector.vue +++ b/src/components/displays/buttons/DatePeriodSelector.vue @@ -415,7 +415,7 @@ const handleShortcutSelection = (event) => { :disabled="props.isDisabled || props.isReadonly" @change="handleShortcutSelection" > - + diff --git a/src/components/displays/department/pos/steps/PosDepartmentStep1.vue b/src/components/displays/department/pos/steps/PosDepartmentStep1.vue index 14bd4de6..ef54d725 100644 --- a/src/components/displays/department/pos/steps/PosDepartmentStep1.vue +++ b/src/components/displays/department/pos/steps/PosDepartmentStep1.vue @@ -297,7 +297,7 @@ const bookingSelectionObjects = computed(() => { const duplicateDetailsObjects = computed(() => { return duplicateOrders.value.map((order) => ({ id: Number(order.id), - label: `${t("common.order")} #${order.id} - ${formatDuplicateOrderDate(order.created_at)}`, + label: `${t("admin.pos.order")} #${order.id} - ${formatDuplicateOrderDate(order.created_at)}`, content: getDuplicateOrderContent(order), buttons: [ { diff --git a/src/components/displays/department/tables/SelfServeTryModal.vue b/src/components/displays/department/tables/SelfServeTryModal.vue index 9f12f52c..0bd7a5f3 100644 --- a/src/components/displays/department/tables/SelfServeTryModal.vue +++ b/src/components/displays/department/tables/SelfServeTryModal.vue @@ -229,7 +229,7 @@ const clearAnswers = async () => { const confirmation = await Swal.fire({ title: "Ryd besvarelser?", - text: `Registreringsnummer ${normalizedReg.value} pa bane ${selectedLaneId.value} bliver ryddet.`, + text: `Registreringsnummer ${normalizedReg.value} på bane ${selectedLaneId.value} bliver ryddet.`, icon: "warning", showCancelButton: true, confirmButtonText: "Ja, ryd besvarelser", @@ -284,7 +284,7 @@ watch(dynamicImageUrl, () => {