Add employee fixtures, mobile POS customer note popup, and alignment updates:

- Introduced employee fixtures in `mobilePos.js` for testing workflows involving employee data.
- Enhanced `PosDepartmentStep1MobileCustomerNotes.vue` with translated labels and data-test attributes.
- Added e2e tests for customer notes popup and translated label validation.
- Improved alignment and responsiveness for `PaginationDisplayGeneralSearchReload.vue` and related e2e tests.
- Updated button logic and styles in `GenericButton.vue` and other mobile POS components for consistent UI behavior.
This commit is contained in:
Jeppe Bundgaard
2026-04-09 14:47:43 +02:00
parent 64aa94ebc3
commit e8bd2f5478
13 changed files with 404 additions and 61 deletions
@@ -116,8 +116,12 @@ onBeforeUnmount(() => {
@click="onFieldClicked()"
>
<span v-if="previewValue" class="pos-order-field__preview">{{ previewValue }}</span>
<span v-else class="pos-order-note-editor__empty" data-testid="pos-order-note-empty-state">
<span class="pos-order-field__empty-pill">{{ t('pos.add_note') }}</span>
<span
v-else
class="pos-order-field__empty-state pos-order-note-editor__empty"
data-testid="pos-order-note-empty-state"
>
<span class="pos-order-field__empty-pill">+ {{ t('pos.add_note') }}</span>
</span>
</button>
</div>
@@ -1215,7 +1215,16 @@ const formatCashierName = (order) => {
<!-- Orders as cards -->
<template v-for="order in getVisibleOrders" :key="order.id">
<div class="column is-full">
<WhiteBoxCard :forceStateFooter="true" :forceState="false" :defaultOpen="false" :toggleable="true" class="is-clickable" :has-hover-effect="true" :hasSelectionStyle="false">
<WhiteBoxCard
:forceStateFooter="true"
:forceState="false"
:defaultOpen="false"
:toggleable="true"
class="is-clickable pos-orders-mobile-card"
:has-hover-effect="true"
:hasSelectionStyle="false"
:data-testid="`pos-order-list-card-${order.id}`"
>
<template #header>
<div class="card-header-title is-flex is-justify-content-space-between is-align-items-center" style="width: 100%;">
<div>
@@ -1541,4 +1550,30 @@ const formatCashierName = (order) => {
.status-bar.has-text-success {
border-color: #48c774;
}
@media screen and (max-width: 768px) {
.pos-orders-mobile-card {
border: 1px solid #d7dee8;
border-radius: 0.9rem;
box-shadow: 0 10px 24px rgba(19, 35, 57, 0.08);
overflow: hidden;
}
.pos-orders-mobile-card :deep(.card) {
background: #ffffff;
}
.pos-orders-mobile-card :deep(.card-header) {
border-bottom: 1px solid #e3e9f2;
}
.pos-orders-mobile-card :deep(.card-content) {
border-top: 1px solid #eef2f7;
}
.pos-orders-mobile-card :deep(.card-footer) {
border-top: 1px solid #e3e9f2;
background: #fbfcfe;
}
}
</style>
@@ -244,6 +244,10 @@ const applyPendingBookingFromSelection = async () => {
const preparedAddons = addonProducts
.filter(p => !!p)
.map(p => transactionItems.convertProductToAddon(p as any, { quantity: (p as any)?.quantity || 1 }));
const bookingIncludesWashCertificate = preparedAddons.some((addon: any) =>
addon?.id === 41 || addon?.product?.id === 41
);
attachments.setWashCertificate(bookingIncludesWashCertificate);
let effectivePrimaryProduct = primaryProduct;
if (!primaryProduct.is_wash) {
@@ -568,8 +572,16 @@ watch(() => vehicles?.vehicle_1?.value?.reg, () => {
// Utility: map source addons and carry over quantities from previous addons by product.id
const mapAddonsWithQuantity = (sourceAddons = [], previousAddons = []) =>
sourceAddons.map(addon => {
const carriedQty = previousAddons.find(a => a.product.id === addon.product.id)?.quantity ?? 0;
return { ...addon, quantity: carriedQty };
const previousAddon = previousAddons.find(a => a.product.id === addon.product.id);
const nextQuantity = previousAddon?.quantity ?? addon?.quantity ?? addon?.product?.quantity ?? 0;
return {
...addon,
quantity: nextQuantity,
product: addon?.product ? {
...addon.product,
quantity: nextQuantity,
} : addon?.product,
};
});
watch(
@@ -104,7 +104,7 @@ watch(() => metadata.getCustomerId(), (newCustomerId) => {
</p>
</div>
<!-- Reference -->
<div class="mt-2 mb-2 has-text-centered custom-button-secondary px-2" style="min-width: 174px;" :class="{'opacity-invisible': customerNotes.length === 0, 'has-background-warning': customerNotes.length > 0}" @click="popups.select('customer_notes')" v-if="customerNotes.length > 0">
<div class="mt-2 mb-2 has-text-centered custom-button-secondary px-2" style="min-width: 174px;" :class="{'opacity-invisible': customerNotes.length === 0, 'has-background-warning': customerNotes.length > 0}" @click="popups.select('customer_notes')" v-if="customerNotes.length > 0" data-testid="pos-mobile-customer-notes-trigger">
<p>
<!-- If there are notes, show a warning icon -->
<span v-if="customerNotes.length > 0" class="icon is-small has-text-dark pr-1">
@@ -153,4 +153,4 @@ watch(() => metadata.getCustomerId(), (newCustomerId) => {
flex-grow: 0;
}
</style>
</style>
@@ -7,7 +7,7 @@ import { errors } from "@/components/request/HandleGlobalError.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
import ContinueArrow from "@/components/viewport/elements/icons/ContinueArrow.vue";
import { vehicles, metadata, getCustomerId, popups, resetPos, attachments, transactionHistory } from "../objects/PosDepartmentStepMobileFlow.vue";
import { vehicles, metadata, getCustomerId, popups, resetPos, attachments, transactionHistory, transactionItems } from "../objects/PosDepartmentStepMobileFlow.vue";
import Swal from "sweetalert2";
import LongPressListener from "@/components/viewport/elements/wrappers/LongPressListener.vue";
import {views} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
@@ -247,29 +247,13 @@ const step2 = () => {
// Set the order bookings id to the metadata
if (metadata.getBookingId() && metadata.getBookingId() > 0) {
SessionUser.objects.order_bookings.set.order_id(metadata.getBookingId(), order_id.value).then(() => {
// Check if order requires safety seal popup (product id 41 present as item or addon)
try {
const posStr = localStorage.getItem('pos');
let hasSeal = false;
if (posStr) {
try {
const posData = JSON.parse(posStr);
const primaryItem = posData?.transactionItems?.primaryItem;
const additionalItems = posData?.transactionItems?.additionalItems || [];
const allItems = [primaryItem, ...additionalItems].filter(Boolean);
hasSeal = allItems.some((item: any) =>
item?.product?.id === 41 ||
(item?.addons && item.addons.some((addon: any) => addon?.product?.id === 41))
);
} catch (parseErr) {
console.error('Failed to parse pos data:', parseErr);
}
}
try {
const hasSeal = transactionItems.containsWashCertificate();
if (!hasSeal) {
metadata.setLoadingState(true);
metadata.setLoadingMessage(t('admin.pos.waiting_for_booking'));
SessionUser.objects.order_bookings.functions.complete(metadata.getBookingId(), null, (response: any) => {
console.log('Auto-completed booking without safety seal prompt (mobile POS - localStorage)');
console.log('Auto-completed booking without safety seal prompt (mobile POS transaction state)');
completeOrder();
});
} else {
@@ -279,7 +263,7 @@ const step2 = () => {
});
}
} catch (err) {
console.error('Error checking safety seal via localStorage:', err);
console.error('Error checking safety seal from transaction state:', err);
popups.select('complete_booking', {});
awaitBookingCompletion(() => {
completeOrder();
@@ -549,12 +533,19 @@ const defaultLongPressBehavior = () => {
justify-content: space-between;
gap: 0.75rem;
width: 100%;
min-width: 0;
}
.pos-mobile-cta-label {
min-width: 0;
flex: 1;
display: -webkit-box;
text-align: left;
line-height: 1.2;
overflow: hidden;
overflow-wrap: anywhere;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.pos-mobile-cta-value {
@@ -574,12 +565,19 @@ const defaultLongPressBehavior = () => {
justify-content: space-between;
gap: 0.75rem;
width: 100%;
min-width: 0;
}
:deep(.pos-mobile-cta-label) {
min-width: 0;
flex: 1;
display: -webkit-box;
text-align: left;
line-height: 1.2;
overflow: hidden;
overflow-wrap: anywhere;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
:deep(.pos-mobile-cta-value) {
@@ -588,6 +586,11 @@ const defaultLongPressBehavior = () => {
justify-content: flex-end;
flex-shrink: 0;
}
:deep(.generic-button__content) {
width: 100%;
min-width: 0;
}
/* Loading state */
.is-loading {
pointer-events: none;
@@ -147,7 +147,10 @@ watch(() => props.showInput, (newVal) => {
<br />
<small>{{ SessionUser.functions.ucFirst(SessionUser.functions.date.toWordsWithTime(note?.created_at || new Date())) }}</small>
<br />
<small v-if="note?.cashier_id">{{ t('pos.created_by') }} <strong>{{ cashierNamesMap.find(cashier => cashier.id === parseInt(note.cashier_id))?.display_name || t('pos.unknown') }}</strong></small>
<small
v-if="note?.cashier_id"
:data-testid="`pos-mobile-customer-note-created-by-${note.id}`"
>{{ t('admin.pos.created_by') }} <strong>{{ cashierNamesMap.find(cashier => cashier.id === parseInt(note.cashier_id))?.display_name || t('admin.pos.unknown') }}</strong></small>
</span>
</div>
</div>
@@ -158,7 +161,7 @@ watch(() => props.showInput, (newVal) => {
<input
class="input has-sharp-edges"
type="text"
:placeholder="t('pos.add_note_placeholder')"
:placeholder="t('admin.pos.add_note_placeholder')"
v-model="input"
id="input_field"
@keyup.enter="createNewNote(input); input=''"
@@ -226,4 +229,4 @@ watch(() => props.showInput, (newVal) => {
overflow-y: scroll;
}
</style>
</style>
@@ -64,9 +64,10 @@ onBeforeUnmount(() => {
<template>
<!-- Top row: Reload, Search -->
<div class="columns is-vcentered is-multiline">
<div class="column">
<div class="columns is-vcentered is-multiline pagination-general-search-reload">
<div class="column pagination-general-search-reload__search-column">
<input
data-testid="pagination-search-input"
class="input"
type="text"
:placeholder="$t('global.search_placeholder')"
@@ -74,12 +75,17 @@ onBeforeUnmount(() => {
@input="search($event.target.value)"
/>
</div>
<div class="column is-narrow" v-if="$slots.buttons">
<div class="column is-narrow pagination-general-search-reload__buttons-column" v-if="$slots.buttons">
<!-- Custom buttons slot, passing loadList for convenience -->
<slot name="buttons" :loadList="loadList"></slot>
</div>
<div class="column is-narrow pl-0">
<div ref="actionDropdownRef" class="dropdown is-right" :class="{ 'is-active': isActionDropdownOpen }">
<div class="column is-narrow pl-0 pagination-general-search-reload__action-column">
<div
ref="actionDropdownRef"
class="dropdown is-right pagination-general-search-reload__dropdown"
:class="{ 'is-active': isActionDropdownOpen }"
data-testid="pagination-reload-actions"
>
<div class="dropdown-trigger">
<div class="buttons has-addons action-dropdown-trigger">
<button
@@ -134,13 +140,92 @@ onBeforeUnmount(() => {
.action-dropdown-trigger {
margin-bottom: 0;
}
.pagination-general-search-reload__dropdown {
width: 100%;
}
.action-dropdown-item {
align-items: center;
display: flex;
gap: 0.5rem;
}
.dropdown-item.is-disabled {
opacity: 0.5;
pointer-events: none;
}
@media screen and (max-width: 768px) {
.pagination-general-search-reload {
align-items: stretch;
display: flex;
flex-wrap: wrap;
row-gap: 0;
}
.pagination-general-search-reload__search-column,
.pagination-general-search-reload__buttons-column,
.pagination-general-search-reload__action-column {
padding-top: 0;
padding-bottom: 0;
}
.pagination-general-search-reload__search-column {
flex: 1 1 0;
max-width: none;
min-width: 0;
order: 1;
padding-right: 0;
width: auto;
}
.pagination-general-search-reload__search-column :deep(.input) {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
height: 100%;
}
.pagination-general-search-reload__buttons-column {
flex: 0 0 100%;
max-width: 100%;
order: 3;
width: 100%;
}
.pagination-general-search-reload__action-column {
flex: 0 0 auto;
margin-top: 0;
max-width: none;
order: 2;
padding-left: 0;
width: auto;
}
.pagination-general-search-reload__dropdown {
width: auto;
}
.action-dropdown-trigger {
display: flex;
width: auto;
}
.action-dropdown-main,
.action-dropdown-toggle {
height: 100%;
}
.action-dropdown-main {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
flex: 0 1 auto;
justify-content: center;
width: auto;
}
.action-dropdown-toggle {
flex: 0 0 auto;
}
}
</style>
@@ -3,7 +3,7 @@
<template>
<button class="generic-button">
<span class="is-flex-wrap-nowrap" style="width: 90%;">
<span class="generic-button__content">
<slot />
</span>
</button>
@@ -33,10 +33,14 @@
align-self: stretch;
flex-grow: 0;
}
/* Default text styles */
.generic-button span {
/* Auto layout */
height: 18px;
.generic-button__content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-width: 0;
min-height: 100%;
font-family: 'Arial';
font-style: normal;
font-weight: 700;
@@ -45,9 +49,6 @@
color: #FFFFFF;
/* Inside auto layout */
flex: none;
order: 0;
flex-grow: 0;
}
/* Disabled styles */
.generic-button:disabled {
@@ -71,10 +72,9 @@
align-self: stretch;
flex-grow: 0;
}
/* Disabled text styles */
.generic-button:disabled span {
/* Auto layout */
height: 18px;
.generic-button:disabled .generic-button__content {
min-height: 100%;
font-family: 'Arial';
font-style: normal;
font-weight: 700;
@@ -83,9 +83,6 @@
color: #FFFFFF;
/* Inside auto layout */
flex: none;
order: 0;
flex-grow: 0;
}
/** White Button Styles */
.generic-button.white {
@@ -109,10 +106,9 @@
align-self: stretch;
flex-grow: 0;
}
/* White text styles */
.generic-button.white span {
/* Auto layout */
height: 18px;
.generic-button.white .generic-button__content {
min-height: 100%;
font-family: 'Arial';
font-style: normal;
font-weight: 700;
@@ -121,9 +117,6 @@
color: #000000;
/* Inside auto layout */
flex: none;
order: 0;
flex-grow: 0;
}
.generic-button.white:disabled {
/* Category */
@@ -152,12 +145,12 @@
background: none;
}
/* Disabled white text styles */
.generic-button.white:disabled span {
.generic-button.white:disabled .generic-button__content {
/* Uncategorized */
margin: 0 auto;
width: 100%;
height: 12px;
min-height: 100%;
/* sm-tx */
font-family: 'Arial';
@@ -176,4 +169,4 @@
flex-grow: 0;
}
</style>
</style>
+34
View File
@@ -252,6 +252,7 @@ test.describe('Admin POS Orders - desktop settings', () => {
await expect(reg3Add).toBeVisible();
const noteEmptyState = page.getByTestId('pos-order-note-empty-state');
await expect(noteEmptyState).toBeVisible();
await expect(noteEmptyState.locator('.pos-order-field__empty-pill')).toHaveText(/^\+\s+\S+/);
await expect(noteEmptyState).not.toContainText(/Ingen data/i);
await expect(noteEmptyState.locator('.pos-order-note-editor__empty-copy')).toHaveCount(0);
@@ -714,6 +715,10 @@ test.describe('Admin POS Orders - mobile smoke', () => {
test('keeps horizontal gutters around the mobile order tabs', async ({ page }) => {
await openOrderDetail(page);
const noteEmptyState = page.getByTestId('pos-order-note-empty-state');
await expect(noteEmptyState).toBeVisible();
await expect(noteEmptyState.locator('.pos-order-field__empty-pill')).toHaveText(/^\+\s+\S+/);
const tabsContainer = getVisibleTestId(page, 'pos-order-mobile-tabs');
const cartTab = getVisibleTestId(page, 'pos-order-tab-cart');
const containerBox = await tabsContainer.boundingBox();
@@ -745,6 +750,35 @@ test.describe('Admin POS Orders - mobile smoke', () => {
await expect(page.getByTestId('pos-order-item-delete-9101').first()).toBeVisible();
});
test('keeps the mobile search and reload controls inline and shows a clear card boundary', async ({ page }) => {
await page.goto('/admin/12/modules/pos/orders');
const searchInput = page.getByTestId('pagination-search-input');
const reloadActions = page.getByTestId('pagination-reload-actions');
const orderCard = page.getByTestId('pos-order-list-card-54518');
await expect(searchInput).toBeVisible();
await expect(reloadActions).toBeVisible();
await expect(orderCard).toBeVisible();
const searchBox = await searchInput.boundingBox();
const reloadBox = await reloadActions.boundingBox();
expect(searchBox).not.toBeNull();
expect(reloadBox).not.toBeNull();
const searchCenterY = (searchBox?.y ?? 0) + ((searchBox?.height ?? 0) / 2);
const reloadCenterY = (reloadBox?.y ?? 0) + ((reloadBox?.height ?? 0) / 2);
expect(Math.abs(searchCenterY - reloadCenterY)).toBeLessThanOrEqual(4);
expect((searchBox?.x ?? 0)).toBeLessThan((reloadBox?.x ?? 0));
expect(((searchBox?.x ?? 0) + (searchBox?.width ?? 0)) - (reloadBox?.x ?? 0)).toBeLessThanOrEqual(4);
expect((searchBox?.width ?? 0)).toBeGreaterThan(reloadBox?.width ?? 0);
await expect(orderCard).toHaveCSS('border-top-width', '1px');
await expect(orderCard).toHaveCSS('border-right-width', '1px');
await expect(orderCard).toHaveCSS('border-bottom-width', '1px');
await expect(orderCard).toHaveCSS('border-left-width', '1px');
});
test('stacks the add-items workspace on mobile and still lets the user add products', async ({ page }) => {
await openOrderDetail(page);
+112
View File
@@ -188,6 +188,54 @@ test.describe("POS mobile order flow", () => {
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeVisible({ timeout: 10_000 });
});
test("customer notes popup shows the translated created-by label", async ({ page }) => {
const fixture = createMobilePosFixture({
customerNotesByNumber: {
[REGULAR_CUSTOMER_ID]: [
{
id: 401,
note: "test123",
created_at: "2026-04-09T06:17:00.000Z",
cashier_id: 7,
},
],
},
employees: [
{
id: 7,
display_name: "Jeppe",
},
],
});
await setupMobilePosPage(page, fixture, {
token: "mobile-customer-notes-token",
seedState: {
customerId: REGULAR_CUSTOMER_ID,
reg: "AB12345",
reference: "NOTES-REF",
includePrimaryItem: false,
vehicleType: null,
lastOrderId: null,
},
route: {
step: 1,
},
});
await expect(page.getByTestId("pos-mobile-customer-notes-trigger")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-mobile-customer-notes-trigger").click();
const popup = page.locator('[data-testid="pos-mobile-popup"][data-popup-id="customer_notes"]');
await expect(popup).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-mobile-customer-note-created-by-401")).toContainText("Oprettet af Jeppe");
await expect(page.getByTestId("pos-mobile-customer-note-created-by-401")).not.toContainText("pos.created_by");
await popup.getByText("Tilføj note", { exact: true }).click();
await expect(page.locator("#input_field")).toBeVisible({ timeout: 10_000 });
await expect(page.locator("#input_field")).toHaveAttribute("placeholder", "Tilføj note...");
});
test("@smoke manual input happy path creates, completes, and resets", async ({ page }) => {
const fixture = createMobilePosFixture();
await createOrderFromStep1(page, fixture, {
@@ -677,6 +725,70 @@ test.describe("POS mobile order flow", () => {
await waitForStepReset(page);
});
test("booking completion prompts when a safety seal is added to the basket", async ({ page }) => {
const orderId = 9409;
const fixture = createMobilePosFixture({
ordersById: {
[orderId]: buildRegularOrder(orderId, {
reg_1: "BOOK123",
reference: "",
}),
},
orderItemsByOrderId: {
[orderId]: [],
},
});
await setupMobilePosPage(page, fixture, {
token: "mobile-booking-basket-seal-token",
seedState: {
customerId: REGULAR_CUSTOMER_ID,
reg: "BOOK123",
reference: "",
includePrimaryItem: false,
vehicleType: null,
bookingId: DEFAULT_BOOKING_ID,
vehicleStatus: "booked",
lastOrderId: null,
},
route: {
step: 2,
orderId,
customerId: REGULAR_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
await waitForBookingHydration(page, {
primaryId: 53,
addonProductIds: [71],
});
await page.getByTestId("pos-mobile-additional-items-open").click();
await expect(page.getByTestId("pos-mobile-additional-items-selection")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-mobile-category-8").click();
await page.waitForTimeout(2_000);
await page.getByTestId("pos-mobile-product-41").click();
await expect.poll(async () => {
const snapshot = await getStoredPosSnapshot(page);
return (snapshot?.transactionItems?.additionalItems || []).map((item) => Number(item?.id)).sort((left, right) => left - right);
}, { timeout: 10_000 }).toEqual([41]);
await page.getByTestId("pos-mobile-next-step").click();
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
await page.waitForTimeout(500);
await page.getByTestId("pos-mobile-next-step").click();
await expect(page.getByTestId("pos-mobile-popup")).toBeVisible({ timeout: 10_000 });
await page.getByTestId("pos-mobile-booking-safety-seal-input").fill("9090");
await page.getByTestId("pos-mobile-booking-complete-with-certificate").click();
await expect.poll(() => fixture.requestCounters.bookingComplete, { timeout: 10_000 }).toBe(1);
await expect.poll(() => fixture.bookingsById[DEFAULT_BOOKING_ID]?.status ?? "").toBe("completed");
await expect.poll(() => String(fixture.bookingsById[DEFAULT_BOOKING_ID]?.safety_seal ?? "")).toBe("9090");
await waitForStepReset(page);
});
test("booking completion with safety seal popup submits the explicit seal", async ({ page }) => {
const orderId = 9408;
const fixture = createMobilePosFixture({
+46
View File
@@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
import { createPosFixture, mockApi, seedAuthenticatedState } from "./support/network.js";
import { createMobilePosFixture, REGULAR_CUSTOMER_ID, setupMobilePosPage } from "./support/mobilePos.js";
const POS_PERMISSIONS = [
"admin",
@@ -475,6 +476,51 @@ test.describe("POS visuals", () => {
});
});
test("mobile step 1 selected customer CTA snapshot", async ({ page }, testInfo) => {
test.skip(!testInfo.project.name.includes("mobile"), "Mobile only");
const longCustomerName = "(TEST) Pleno Vognmandsforretning";
const fixture = createMobilePosFixture({
customersByNumber: {
[REGULAR_CUSTOMER_ID]: {
id: REGULAR_CUSTOMER_ID,
customerNumber: REGULAR_CUSTOMER_ID,
name: longCustomerName,
address: "Demo Street 1",
zip: "2630",
city: "Taastrup",
mobilePhone: "12345678",
email: "pos-mobile@example.com",
corporateIdentificationNumber: "00012345",
economic_customer: REGULAR_CUSTOMER_ID,
barred: false,
},
},
});
await setupMobilePosPage(page, fixture, {
token: "pos-visual-mobile-step-1-customer-token",
permissions: ["admin", "department_access_1"],
seedState: {
customerId: REGULAR_CUSTOMER_ID,
reg: "EC21235",
reference: "EC21233 - Test Ref. / Intern nummer",
includePrimaryItem: false,
vehicleType: null,
lastOrderId: null,
},
route: {
step: 1,
},
});
await expect(page.getByTestId("pos-mobile-step-1")).toBeVisible();
await expect(page.getByTestId("pos-mobile-next-step")).toContainText(longCustomerName);
await expect(page.getByTestId("pos-mobile-fixed-actions")).toHaveScreenshot("pos-mobile-step-1-fixed-actions.png", {
maxDiffPixels: 300,
});
});
test("mobile order detail add-items workspace snapshot", async ({ page }, testInfo) => {
test.skip(!testInfo.project.name.includes("mobile"), "Mobile only");
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

+16
View File
@@ -408,6 +408,12 @@ function buildDefaultFixture() {
},
],
},
employees: [
{
id: 7,
display_name: "Jeppe",
},
],
products,
departmentCategories: [
{
@@ -563,6 +569,7 @@ function normalizeFixture(fixture) {
fixture.vehicles = Array.isArray(fixture.vehicles) ? fixture.vehicles : [];
fixture.departments = Array.isArray(fixture.departments) ? fixture.departments : [];
fixture.departmentCategories = Array.isArray(fixture.departmentCategories) ? fixture.departmentCategories : [];
fixture.employees = Array.isArray(fixture.employees) ? fixture.employees : [];
fixture.requestCounters = createRequestCounters(fixture.requestCounters);
fixture.requestLog = createRequestLog(fixture.requestLog);
fixture.failureBudget = createFailureBudget(fixture.failureBudget);
@@ -600,6 +607,7 @@ export function createMobilePosFixture(overrides = {}) {
departments: overrides.departments ? clone(overrides.departments) : clone(base.departments),
products: overrides.products ? clone(overrides.products) : clone(base.products),
departmentCategories: overrides.departmentCategories ? clone(overrides.departmentCategories) : clone(base.departmentCategories),
employees: overrides.employees ? clone(overrides.employees) : clone(base.employees),
vehicles: overrides.vehicles ? clone(overrides.vehicles) : clone(base.vehicles),
unknownVehicles: overrides.unknownVehicles ? clone(overrides.unknownVehicles) : clone(base.unknownVehicles),
failureBudget: createFailureBudget(overrides.failureBudget),
@@ -1203,6 +1211,14 @@ export async function mockMobilePosApi(page, fixture) {
return;
}
if (pathname.endsWith("/public/employees") && method === "GET") {
await route.fulfill(json({
success: true,
data: clone(fixture.employees || []),
}));
return;
}
if (pathname.endsWith("/customer/attributes") && method === "GET") {
recordCounter(fixture, "customerAttributesGet");
const customerNumber = toPositiveInteger(parsedUrl.searchParams.get("customer_number"))