From a4676f8f87dac73196cf4b946280eeb5b8cd374d Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Tue, 14 Apr 2026 10:49:20 +0200 Subject: [PATCH] Add e2e and unit tests for i18n coverage and POS workflows: - Introduced i18n key usage validation with `tests/e2e/i18n.views.spec.ts` for deterministic locale coverage. - Added `viewI18nKeyScanner` utility to scan and validate view translation keys. - Created `PosDepartmentStep1` unit tests for duplicate warnings and booking selection flow. - Enhanced POS mobile popup with `SelectOrderBookingPopupProps` and new header close options. - Updated e2e tests with scenarios to verify booking selections, duplicate handling, and locale alignment. - Added new `test:e2e:i18n:views` npm script for targeted i18n test execution. --- package.json | 1 + src/assets/pos.css | 86 ++ .../pos/order/POSOrderCustomerWishes.vue | 16 +- .../pos/steps/PosDepartmentStep1.vue | 226 +++--- .../pos/steps/PosDepartmentStep3.vue | 2 +- .../PosDepartmentStepMobilePopupRenderer.vue | 44 +- ...tmentStepMobilePopupSelectOrderBooking.vue | 757 ++++++++++++++++-- .../objects/PosDepartmentStepMobileFlow.vue | 2 + .../pos/steps/mobile/objects/PosPopup.vue | 12 + .../superuser/tables/SubusersTable.vue | 51 +- .../step1/RegistrationNumberSearchResult.vue | 63 +- .../token/SessionUser/Objects/Subusers.vue | 76 +- src/i18n/locales/da.json | 1 + src/i18n/locales/de.json | 11 + src/i18n/locales/en.json | 2 + src/i18n/locales/no.json | 16 + src/i18n/locales/sv.json | 16 + .../modules/Pos/DepartmentPosOrder.vue | 2 +- .../other/displays/DepartmentsChartReport.vue | 2 +- .../vehicle/displays/XLVaskUsageLog.vue | 4 +- .../MicrosoftAuthenticationBox.vue | 14 +- tests/e2e/admin-pos-orders.spec.ts | 18 + tests/e2e/i18n.views.spec.ts | 110 +++ tests/e2e/pos-desktop-card-payments.spec.js | 90 ++- tests/e2e/pos-flow.spec.js | 49 +- tests/e2e/pos-mobile-order-flow.spec.js | 227 +++++- tests/e2e/pos.visual.spec.js | 50 ++ tests/e2e/subuser-management.spec.ts | 101 ++- tests/e2e/support/viewI18nKeyScanner.ts | 268 +++++++ tests/unit/pos-department-step-1.spec.js | 426 ++++++++++ 30 files changed, 2360 insertions(+), 383 deletions(-) create mode 100644 tests/e2e/i18n.views.spec.ts create mode 100644 tests/e2e/support/viewI18nKeyScanner.ts create mode 100644 tests/unit/pos-department-step-1.spec.js diff --git a/package.json b/package.json index afbcafd0..c1670d9e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "preview:prod": "npm run build && npm run preview -- --host 127.0.0.1 --port 4173", "test:unit": "vitest run", "test:e2e": "playwright test", + "test:e2e:i18n:views": "playwright test tests/e2e/i18n.views.spec.ts --project=chromium-desktop", "test:e2e:ci": "node scripts/run-playwright-ci-parallel.mjs", "test:e2e:ci:serial": "playwright test --reporter=line,html", "test:e2e:smoke": "playwright test --grep @smoke --project=chromium-desktop --project=chromium-mobile", diff --git a/src/assets/pos.css b/src/assets/pos.css index 957a11ed..ef84218f 100644 --- a/src/assets/pos.css +++ b/src/assets/pos.css @@ -39,6 +39,87 @@ gap: 0.75rem; } +.pos-duplicate-warning { + margin: 0; + border: 1px solid #f2d38a; + border-radius: 0.95rem; + box-shadow: 0 10px 24px rgba(142, 108, 24, 0.08); +} + +.pos-duplicate-warning__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.75rem; +} + +.pos-duplicate-warning__header-copy { + display: inline-flex; + align-items: center; + gap: 0.6rem; + min-width: 0; +} + +.pos-duplicate-warning__icon { + color: #8a6300; +} + +.pos-duplicate-warning__title { + margin: 0; + color: #604700; + font-size: 0.95rem; + font-weight: 700; +} + +.pos-duplicate-warning__message { + margin: 0.75rem 0 0; + color: #5e5230; + line-height: 1.45; +} + +.pos-duplicate-warning__details { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-top: 0.85rem; +} + +.pos-duplicate-warning__detail-card { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0.9rem 1rem; + border: 1px solid rgba(142, 108, 24, 0.16); + border-radius: 0.8rem; + background: rgba(255, 255, 255, 0.8); +} + +.pos-duplicate-warning__detail-copy { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.pos-duplicate-warning__detail-title { + margin: 0; + color: #2b3442; + font-weight: 700; +} + +.pos-duplicate-warning__detail-content { + margin: 0; + color: #5c6a7c; + line-height: 1.45; +} + +.pos-duplicate-warning__detail-actions { + margin: 0; +} + +.pos-duplicate-warning__detail-actions .button { + margin: 0; +} + @media screen and (min-width: 1024px) { .pos-rail--sticky { position: sticky; @@ -54,6 +135,11 @@ .pos-shell-actions { grid-template-columns: minmax(0, 1fr); } + + .pos-duplicate-warning__header { + flex-direction: column; + align-items: stretch; + } } .box.has-sharp-edges.pos-card { diff --git a/src/components/displays/department/pos/order/POSOrderCustomerWishes.vue b/src/components/displays/department/pos/order/POSOrderCustomerWishes.vue index bd1bbd18..b9d7916a 100644 --- a/src/components/displays/department/pos/order/POSOrderCustomerWishes.vue +++ b/src/components/displays/department/pos/order/POSOrderCustomerWishes.vue @@ -47,6 +47,7 @@ const createCustomerWishField = ({ isRequired, warningStateWhenEmpty, warningIconClass, + fillRow = false, }) => { const inputId = `${testIdBase}-input`; const isEditing = ref(false); @@ -104,6 +105,7 @@ const createCustomerWishField = ({ closeEditor, testIdBase, warningIconClass, + fillRow, }; }; @@ -138,6 +140,7 @@ const safetySealField = createCustomerWishField({ isRequired: () => false, warningStateWhenEmpty: null, warningIconClass: "fas fa-shield-alt", + fillRow: true, }); const fields = computed(() => { @@ -148,7 +151,12 @@ const fields = computed(() => { - + diff --git a/src/components/displays/department/pos/steps/PosDepartmentStep1.vue b/src/components/displays/department/pos/steps/PosDepartmentStep1.vue index 04ecaccc..c92dfce4 100644 --- a/src/components/displays/department/pos/steps/PosDepartmentStep1.vue +++ b/src/components/displays/department/pos/steps/PosDepartmentStep1.vue @@ -22,7 +22,6 @@ import ElementTabsBox from "@/components/displays/boxes/ElementTabsBox.vue"; import { POS_STEP_1_VERSION } from "@/config.js"; import SelectVehicleFormPOS from "@/components/forms/department/pos/SelectVehicleFormPOS.vue"; import PosLastScannedLicensePlatesV2 from "@/components/displays/department/pos/PosLastScannedLicensePlatesV2.vue"; -import DefaultObjectSelector from "@/components/displays/modals/DefaultObjectSelector.vue"; import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js"; @@ -77,7 +76,7 @@ const desktopModalState = ref(null); const duplicateCheckId = ref(0); const duplicateOrders = ref([]); const duplicateWarningKey = ref(""); -const acknowledgedDuplicateWarningKey = ref(""); +const duplicateDetailsExpanded = ref(false); const pendingNextResolution = ref(false); let desktopStep1CoordinationPromise = Promise.resolve({ canProceed: true }); const isDesktopStep1Active = computed(() => getCurrentStep() === 1); @@ -150,7 +149,7 @@ const setDesktopStep1Context = (context = {}) => { if (!isDesktopStep1Active.value) { duplicateOrders.value = []; duplicateWarningKey.value = ""; - acknowledgedDuplicateWarningKey.value = ""; + duplicateDetailsExpanded.value = false; desktopModalState.value = null; return; } @@ -158,17 +157,15 @@ const setDesktopStep1Context = (context = {}) => { if (!desktopStep1Context.value.reg1) { duplicateOrders.value = []; duplicateWarningKey.value = ""; - acknowledgedDuplicateWarningKey.value = ""; - if (desktopModalState.value !== "duplicate_details") { - desktopModalState.value = null; - } + duplicateDetailsExpanded.value = false; + desktopModalState.value = null; return; } if (desktopStep1Context.value.requiresBookingSelection) { duplicateOrders.value = []; duplicateWarningKey.value = ""; - acknowledgedDuplicateWarningKey.value = ""; + duplicateDetailsExpanded.value = false; desktopModalState.value = "booking_selection"; return; } @@ -284,33 +281,22 @@ const duplicateDetailsObjects = computed(() => { })); }); -const desktopModalTitle = computed(() => { - if (desktopModalState.value === "duplicate_warning") { - return t("admin.pos.warning"); +const isInlineDuplicateWarningVisible = computed(() => { + if (!isDesktopStep1Active.value || desktopModalState.value === "booking_selection") { + return false; } - if (desktopModalState.value === "duplicate_details") { - return `${SessionUser.objects.global.language.possible_duplicates} - ${desktopStep1Context.value.reg1}`; + const normalizedContext = normalizeDuplicateContext(desktopStep1Context.value); + if (!normalizedContext.committed || !normalizedContext.reg1 || duplicateOrders.value.length === 0) { + return false; } - return ""; + return duplicateWarningKey.value === getDuplicateStateKey(normalizedContext); }); -const desktopModalMessage = computed(() => { - if (desktopModalState.value === "duplicate_warning") { - return t("admin.pos.duplicate_order_warning"); - } - - return ""; -}); - -const desktopModalObjects = computed(() => { - if (desktopModalState.value === "duplicate_details") { - return duplicateDetailsObjects.value; - } - - return []; -}); +const duplicateDetailsToggleLabel = computed(() => + duplicateDetailsExpanded.value ? t("admin.pos.hide_order_details") : t("admin.pos.show_order_details") +); const resumePendingNextStep = async () => { if (!pendingNextResolution.value) { @@ -322,80 +308,16 @@ const resumePendingNextStep = async () => { await nextStep({ isMobile: false, orderCreation: true }); }; -const acceptDuplicateWarning = async () => { - acknowledgedDuplicateWarningKey.value = duplicateWarningKey.value || getDuplicateStateKey(desktopStep1Context.value); - desktopModalState.value = null; - await resumePendingNextStep(); +const toggleDuplicateDetails = () => { + duplicateDetailsExpanded.value = !duplicateDetailsExpanded.value; }; -const cancelDuplicateWarning = () => { - desktopModalState.value = null; - pendingNextResolution.value = false; -}; - -const showDuplicateDetails = () => { - desktopModalState.value = "duplicate_details"; -}; - -const showDuplicateWarning = () => { - desktopModalState.value = "duplicate_warning"; -}; - -const desktopModalFooterButtons = computed(() => { - if (desktopModalState.value === "duplicate_warning") { - return [ - { - label: t("admin.pos.continue"), - action: () => acceptDuplicateWarning(), - color: "primary", - testId: "pos-desktop-duplicate-warning-continue", - }, - { - label: t("admin.pos.show_order_details"), - action: () => showDuplicateDetails(), - color: "dark", - testId: "pos-desktop-duplicate-warning-details", - }, - { - label: t("admin.pos.cancel"), - action: () => cancelDuplicateWarning(), - color: "light", - testId: "pos-desktop-duplicate-warning-cancel", - }, - ]; - } - - if (desktopModalState.value === "duplicate_details") { - return [ - { - label: "Tilbage", - action: () => showDuplicateWarning(), - color: "light", - testId: "pos-desktop-duplicate-details-back", - }, - { - label: t("admin.pos.continue"), - action: () => acceptDuplicateWarning(), - color: "primary", - testId: "pos-desktop-duplicate-details-continue", - }, - { - label: t("admin.pos.cancel"), - action: () => cancelDuplicateWarning(), - color: "light", - testId: "pos-desktop-duplicate-details-cancel", - }, - ]; - } - - return []; -}); - const fetchDuplicateOrdersForContext = async (context) => { const normalizedContext = normalizeDuplicateContext(context); if (!normalizedContext.reg1) { duplicateOrders.value = []; duplicateWarningKey.value = ""; + duplicateDetailsExpanded.value = false; return []; } @@ -419,13 +341,18 @@ const fetchDuplicateOrdersForContext = async (context) => { } const orders = Array.isArray(response?.data?.data) ? response.data.data : []; + const nextDuplicateWarningKey = getDuplicateStateKey(normalizedContext); + if (duplicateWarningKey.value !== nextDuplicateWarningKey) { + duplicateDetailsExpanded.value = false; + } duplicateOrders.value = orders; - duplicateWarningKey.value = getDuplicateStateKey(normalizedContext); + duplicateWarningKey.value = nextDuplicateWarningKey; return orders; } catch (error) { console.error("Error checking for duplicate orders:", error); duplicateOrders.value = []; duplicateWarningKey.value = ""; + duplicateDetailsExpanded.value = false; return []; } }; @@ -435,6 +362,7 @@ const ensureDuplicateWarningState = async (context, options = {}) => { if (!normalizedContext.reg1) { duplicateOrders.value = []; duplicateWarningKey.value = ""; + duplicateDetailsExpanded.value = false; if (desktopModalState.value !== "booking_selection") { desktopModalState.value = null; } @@ -446,28 +374,15 @@ const ensureDuplicateWarningState = async (context, options = {}) => { const orders = await fetchDuplicateOrdersForContext(normalizedContext); if (orders.length === 0) { - if (desktopModalState.value === "duplicate_warning" || desktopModalState.value === "duplicate_details") { - desktopModalState.value = null; - } + duplicateDetailsExpanded.value = false; return { canProceed: true, duplicateOrders: [], }; } - if (acknowledgedDuplicateWarningKey.value === duplicateWarningKey.value) { - if (desktopModalState.value === "duplicate_warning" || desktopModalState.value === "duplicate_details") { - desktopModalState.value = null; - } - return { - canProceed: true, - duplicateOrders: orders, - }; - } - - showDuplicateWarning(); return { - canProceed: false, + canProceed: true, duplicateOrders: orders, }; }; @@ -479,10 +394,6 @@ const coordinateDesktopStep1 = async (options = {}) => { ...options, }; - if (normalizedOptions.reason === "next") { - pendingNextResolution.value = true; - } - const context = (await selectVehicleFormRef.value?.finalizeDesktopStep1Context?.({ source: normalizedOptions.reason, @@ -496,6 +407,8 @@ const coordinateDesktopStep1 = async (options = {}) => { desktopModalState.value = null; duplicateOrders.value = []; duplicateWarningKey.value = ""; + duplicateDetailsExpanded.value = false; + pendingNextResolution.value = false; return { canProceed: true, context, @@ -505,7 +418,9 @@ const coordinateDesktopStep1 = async (options = {}) => { if (context.requiresBookingSelection || context.bookingResolution === "selection_required") { duplicateOrders.value = []; duplicateWarningKey.value = ""; + duplicateDetailsExpanded.value = false; desktopModalState.value = "booking_selection"; + pendingNextResolution.value = normalizedOptions.reason === "next"; return { canProceed: false, context, @@ -521,6 +436,7 @@ const coordinateDesktopStep1 = async (options = {}) => { } desktopModalState.value = null; + pendingNextResolution.value = false; return { canProceed: true, context, @@ -614,6 +530,16 @@ onMounted(() => { onBeforeUnmount(() => { clearDesktopStep1PreflightHandler(handleDesktopStep1Preflight); }); + +watch( + isInlineDuplicateWarningVisible, + (isVisible) => { + if (!isVisible) { + duplicateDetailsExpanded.value = false; + } + }, + { immediate: true } +); diff --git a/src/components/displays/department/pos/steps/PosDepartmentStep3.vue b/src/components/displays/department/pos/steps/PosDepartmentStep3.vue index fd292c1a..af1a1b11 100644 --- a/src/components/displays/department/pos/steps/PosDepartmentStep3.vue +++ b/src/components/displays/department/pos/steps/PosDepartmentStep3.vue @@ -17,7 +17,7 @@ import { useI18n } from "vue-i18n"; import PosDesktopOrderWorkspace from "@/components/displays/department/pos/order/PosDesktopOrderWorkspace.vue"; const { t } = useI18n(); -const isGuestStripeCustomer = computed(() => Number(customer_id) === 999); +const isGuestStripeCustomer = computed(() => Number(customer_id.value) === 999);