Compare commits

...
11 changed files with 145 additions and 47 deletions
+6 -2
View File
@@ -102,7 +102,7 @@ jobs:
needs: build-and-unit
name: E2E-pr-${{ matrix.suite }}-${{ matrix.project }}
runs-on: [self-hosted, Linux, X64, pleno, frontend, docker]
timeout-minutes: 30
timeout-minutes: 45
strategy:
fail-fast: false
max-parallel: 4
@@ -142,20 +142,24 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
HEAD_SHA: ${{ github.sha }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.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"
head_ref="$PR_HEAD_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"
head_ref="$HEAD_SHA"
else
base_ref="$PUSH_BEFORE_SHA"
head_ref="$HEAD_SHA"
fi
echo "base=$base_ref" >> "$GITHUB_OUTPUT"
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "head=$head_ref" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v5
@@ -22,7 +22,7 @@ const props = defineProps({
import {computed, ref, watch} from 'vue';
import { departments, getDepartments, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {orderBy, orderDirection, setOrder, loadList} from "@/components/pagination/paginatedList.vue";
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import Swal from "sweetalert2";
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
@@ -38,6 +38,8 @@ import {
setCachedXlvaskUsageAmount
} from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
const { orderBy, orderDirection, setOrder, loadList } = usePaginatedListInstance();
const redirectDepartmentOrderPage = (orderId, departmentId) => {
// Send the user to the order page (In a new tab)
// `/admin/${departmentId}/modules/pos/orders/${orderId}`
@@ -0,0 +1 @@
export const SELFWASH_PERIOD_ALL_LIMIT = 10000;
@@ -21,9 +21,32 @@ const props = defineProps({
type: Number, default: 0
}
})
import {computed, ref, watch } from "vue";
import {computed, provide, ref, watch } from "vue";
import { useRouter } from "vue-router";
import {
PaginatedListKey,
usePaginatedList,
} from "@/components/pagination/paginatedList.vue";
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
import PaginationDisplayTemplateDates
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
import ShowErrorField from "@/components/global/ShowErrorField.vue";
import { useI18n } from 'vue-i18n'
import { BSwitch } from "buefy";
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n()
const router = useRouter();
const paginatedList = usePaginatedList();
provide(PaginatedListKey, paginatedList);
const {
isLoading,
list,
loadList,
@@ -38,23 +61,8 @@ import {
setOrder,
hideSearchField,
setHideSearchField,
} from "@/components/pagination/paginatedList.vue";
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
} = paginatedList;
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
import PaginationDisplayTemplateDates
from "@/components/displays/pagination/templates/PaginationDisplayTemplateDates.vue";
import ShowErrorField from "@/components/global/ShowErrorField.vue";
import { useI18n } from 'vue-i18n'
import { BSwitch } from "buefy";
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
import { formatLocalDateOnly, parseLocalDateOnly } from "@/services/dateOnly.js";
const { t } = useI18n()
const router = useRouter();
const SELFWASH_PERIOD_ALL_LIMIT = 10000;
setEndpoint("/modules/xlvask/services/usage/orders", false);
if (props.loadAllAtOnce) {
setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false);
@@ -23,6 +23,7 @@ import {
} from "../../imports/InvoicingBillingPeriodImportPaging.js";
import { buildPossibleDuplicateGroups } from "../../utils/possibleDuplicateGroups.js";
import { isUsageOrderAttachedToOrder } from "@/components/displays/department/pos/sync/xlvaskUsageFilters.js";
import { SELFWASH_PERIOD_ALL_LIMIT } from "@/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js";
const period_result = ref<any>(null);
const route = useRoute();
@@ -416,7 +417,7 @@ const loadSelfWashCounts = async ({ forceRefresh = false } = {}) => {
'GET',
{
page: 1,
limit: 500,
limit: SELFWASH_PERIOD_ALL_LIMIT,
filters,
order: 'StartTime:desc',
},
@@ -4,7 +4,7 @@ export const PERIOD_CACHE_TTL_MS = 10 * 60 * 1000;
export const PERIOD_LIMIT_OPTIONS = [10, 25, 50, 100, 200, 500, "all"];
const CACHE_PREFIX = "invoicing-period-page:v2:";
const SELF_WASH_COUNTS_CACHE_PREFIX = "invoicing-period-self-wash-counts:";
const SELF_WASH_COUNTS_CACHE_PREFIX = "invoicing-period-self-wash-counts:v2:";
const DEFAULT_LIMIT = 100;
const ALL_LIMIT = "all";
@@ -16,6 +16,7 @@ const json = (body: unknown, status = 200) => ({
contentType: "application/json",
body: JSON.stringify(body),
});
const pageReadyTimeout = process.env.CI ? 30_000 : 15_000;
const setupNotificationSmsApi = async (page: Page) => {
let listRequestCount = 0;
@@ -115,9 +116,10 @@ test("@smoke department notification SMS active toggle and delete refresh the ta
const notificationSmsApi = await setupNotificationSmsApi(page);
await page.goto("/admin/1/modules/notifications", { waitUntil: "domcontentloaded" });
await expect.poll(() => notificationSmsApi.listRequestCount, { timeout: pageReadyTimeout }).toBeGreaterThan(0);
const row = page.locator("tr", { hasText: "Dispatch line" });
await expect(row).toBeVisible();
await expect(row).toBeVisible({ timeout: pageReadyTimeout });
const enabledToggle = page.getByTestId("department-notification-sms-enabled-toggle-501");
await expect(enabledToggle).toBeChecked();
+45 -20
View File
@@ -17,6 +17,8 @@ const POS_PERMISSIONS = [
const SUPERUSER_POS_PERMISSIONS = [...POS_PERMISSIONS, "superuser"];
const POS_BOOT_URL = "/admin/12/modules/pos?step=1";
const POS_BOOT_READY_TIMEOUT = 20_000;
const POS_ORDER_READY_TIMEOUT = 20_000;
const DRAFT_TRANSACTION_CUSTOMER_ID = 44556677;
const DRAFT_TRANSACTION_CUSTOMER_NAME = "(TEST) Draft Transaction Customer";
@@ -49,6 +51,22 @@ function getPosBootStep(page: Page) {
return page.locator('[data-testid="pos-step-1"]:visible, [data-testid="pos-mobile-step-1"]:visible').first();
}
function getPosOrderReadyTarget(page: Page) {
return page
.locator(
[
'[data-testid="pos-order-card"]:visible',
'[data-testid="pos-order-main"]:visible',
'[data-testid="pos-order-detail"]:visible',
].join(", ")
)
.first();
}
async function expectPosOrderReady(page: Page, timeout = POS_ORDER_READY_TIMEOUT) {
await expect(getPosOrderReadyTarget(page)).toBeVisible({ timeout });
}
async function navigateToPosBootPage(page: Page, { waitForSession = false }: { waitForSession?: boolean } = {}) {
let lastError: unknown;
@@ -64,9 +82,8 @@ async function navigateToPosBootPage(page: Page, { waitForSession = false }: { w
try {
await page.goto(POS_BOOT_URL, { waitUntil: "domcontentloaded", timeout: 30_000 });
await Promise.race([sessionRequest, getPosBootStep(page).waitFor({ state: "visible", timeout: 10_000 })]);
await expect(getPosBootStep(page)).toBeVisible({ timeout: 10_000 });
await sessionRequest;
await expect(getPosBootStep(page)).toBeVisible({ timeout: POS_BOOT_READY_TIMEOUT });
return;
} catch (error) {
lastError = error;
@@ -162,19 +179,24 @@ async function waitForSwalToClose(page: Page) {
async function openOrderSettings(page: Page, orderId: number) {
await page.goto(`/admin/12/modules/pos/orders/${orderId}`);
await expect(page.getByTestId("pos-order-detail")).toBeVisible({ timeout: 20_000 });
await expectPosOrderReady(page);
await clickVisibleTestId(page, "pos-order-tab-settings");
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
}
async function revisitCurrentPage(page: Page) {
await page.goto(page.url(), { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("pos-order-detail")).toBeVisible({ timeout: 20_000 });
if (new URL(page.url()).pathname.includes("/modules/pos/orders/")) {
await expectPosOrderReady(page);
return;
}
await expect(getPosBootStep(page)).toBeVisible({ timeout: POS_BOOT_READY_TIMEOUT });
}
async function reloadOrderSettings(page: Page, orderId: number) {
await revisitCurrentPage(page);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await page.goto(`/admin/12/modules/pos/orders/${orderId}`, { waitUntil: "domcontentloaded", timeout: 30_000 });
await expectPosOrderReady(page);
await clickVisibleTestId(page, "pos-order-tab-settings");
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
}
@@ -313,17 +335,18 @@ async function changeOrderInvoiceCollection(page: Page, closedAt: string) {
async function openOrderDetail(page: Page, orderId = 54518) {
const orderUrl = `/admin/12/modules/pos/orders/${orderId}`;
for (let attempt = 0; attempt < 2; attempt += 1) {
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
await page.goto(orderUrl, { waitUntil: "domcontentloaded", timeout: 30_000 });
await expect(page.getByTestId("pos-order-detail")).toBeVisible({ timeout: 10_000 });
await expectPosOrderReady(page);
return;
} catch (error) {
if (attempt === 1) {
if (attempt === 2) {
throw error;
}
await navigateToPosBootPage(page);
await page.goto("about:blank").catch(() => {});
await delay(250);
}
}
}
@@ -989,7 +1012,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
expect(Math.abs((beforeRailBox?.width ?? 0) - (restoredRailBox?.width ?? 0))).toBeLessThanOrEqual(20);
await page.goto("/admin/12/modules/pos/orders/54518", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expectPosOrderReady(page);
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
await expectOrderTotal(page, 1397);
});
@@ -1107,7 +1130,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
await expect(noteInput).toBeFocused();
await revisitCurrentPage(page);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expectPosOrderReady(page);
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(referenceValue);
await expect(page.getByTestId("pos-order-metadata-customer-wishes")).toContainText(poValue);
await expect(page.getByTestId("pos-order-metadata-note")).toContainText(noteValue);
@@ -1160,7 +1183,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
});
await revisitCurrentPage(page);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expectPosOrderReady(page);
await expect(page.getByTestId("pos-order-customer-wishes-reference")).toContainText("+ Tilføj");
await expect(page.getByTestId("pos-order-customer-wishes-po")).toContainText("+ Tilføj");
});
@@ -1221,7 +1244,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
await expect(reg3Input).toHaveValue("NO9012", { timeout: 10000 });
await revisitCurrentPage(page);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expectPosOrderReady(page);
await expect(page.getByTestId("pos-order-registration-1")).toContainText("XY1234");
await expect(page.getByTestId("pos-order-registration-2")).toContainText("TR5678");
await expect(page.getByTestId("pos-order-registration-3")).toContainText("NO9012");
@@ -1249,7 +1272,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
await expectOrderTotal(page, 2123);
await revisitCurrentPage(page);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expectPosOrderReady(page);
await expect(page.getByTestId("pos-order-item-note-trigger-9101")).toBeVisible();
await expect(page.getByTestId("pos-order-item-reference-trigger-9101")).toBeVisible();
await expect(page.getByTestId("pos-order-item-price-9101")).toContainText("1400");
@@ -1274,7 +1297,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
await expectOrderTotal(page, 1423);
await revisitCurrentPage(page);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expectPosOrderReady(page);
await expect(page.getByTestId("pos-order-item-note-trigger-9102")).toBeVisible();
await expect(page.getByTestId("pos-order-item-price-9102")).toContainText("450");
await expectOrderTotal(page, 1423);
@@ -1324,7 +1347,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
await expectOrderTotal(page, 0);
await revisitCurrentPage(page);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expectPosOrderReady(page);
await expect(page.getByTestId("pos-order-empty-state")).toBeVisible();
await expectOrderTotal(page, 0);
});
@@ -1347,7 +1370,7 @@ test.describe("Admin POS Orders - desktop settings", () => {
await expectOrderTotal(page, 2021);
await revisitCurrentPage(page);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expectPosOrderReady(page);
await expect(page.getByTestId("pos-order-item-edit-9200").first()).toBeVisible();
await expectOrderTotal(page, 2021);
});
@@ -1432,6 +1455,8 @@ test.describe("Admin POS Orders - desktop settings", () => {
});
test("persists scalar metadata edits across settings and details tabs", async ({ page }) => {
test.setTimeout(90_000);
const { orderId } = await createDisposableOrder(page);
const submittedValues = {
reference: "UPDATED-REF-2026",
@@ -2964,7 +2989,7 @@ test.describe("Admin POS Orders - mobile smoke", () => {
test("opens the settings tab and renders the new controls", async ({ page }) => {
await page.goto("/admin/12/modules/pos/orders/54518");
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expectPosOrderReady(page);
await clickVisibleTestId(page, "pos-order-tab-settings");
await expect(page.getByTestId("pos-order-panel-settings")).toBeVisible();
@@ -3093,7 +3118,7 @@ test.describe("Admin POS Orders - mobile smoke", () => {
expect(Math.abs((restoredMainBox?.width ?? 0) - (beforeMainBox?.width ?? 0))).toBeLessThanOrEqual(20);
await revisitCurrentPage(page);
await expect(page.getByTestId("pos-order-detail")).toBeVisible();
await expectPosOrderReady(page);
await expect(getVisibleTestId(page, "pos-order-item-edit-9200")).toBeVisible();
await expectOrderTotal(page, 1397);
});
+8 -3
View File
@@ -1132,7 +1132,6 @@ test.describe("Invoicing period tab", () => {
};
await openPeriodView(page);
await setEntireMarchPeriod(page);
await page.route("**/modules/xlvask/services/usage/orders**", async (route) => {
const path = new URL(route.request().url()).pathname;
@@ -1242,8 +1241,10 @@ test.describe("Invoicing period tab", () => {
);
});
await setEntireMarchPeriod(page);
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toBeVisible();
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (0/1)");
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (0/2)");
await expect(page.getByTestId("invoicing-period-view-selector-progress-self_wash")).toBeVisible();
await page.getByTestId("invoicing-period-view-selector-self_wash").click();
@@ -1273,6 +1274,7 @@ test.describe("Invoicing period tab", () => {
await page.getByTestId("xlvask-automation-accept-8101").click();
await expect(page.getByTestId("xlvask-automation-suggestion-8101")).toContainText("Accepteret #7001");
expect(automationAcceptRequests).toEqual([{ suggestion_id: 7101 }]);
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (1/2)");
await expect.poll(() => usageOrderRequests.length).toBeGreaterThan(0);
expect(usageOrderRequests.some((request) => request.limit === "10000")).toBe(true);
@@ -2667,7 +2669,10 @@ test.describe("Invoicing period tab", () => {
await page.goto("/superuser/invoices?activeTab=period");
await expect(page).toHaveURL(/activeTab=period/);
await expect.poll(() => requestCount).toBeGreaterThan(0);
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible({
timeout: periodRouteReadyTimeout,
});
await expect.poll(() => requestCount, { timeout: periodRouteReadyTimeout }).toBeGreaterThan(0);
await page.getByTestId("invoicing-period-view-selector-all").click();
await expect(page.getByTestId("invoicing-period-customer-queue-6001")).toBeVisible();
@@ -755,6 +755,42 @@ describe("Invoicing period queue-driven refresh", () => {
expect(selfWashCalls()).toHaveLength(1);
});
it("loads Selvvask selector counts with the full selected-period limit", async () => {
requestMock.mockImplementation((url) => {
if (url === "/superuser/invoicing/period") {
return Promise.resolve(emptyPeriodResponse());
}
return Promise.resolve(
selfWashResponse(
[
{ id: 8101, linked_order_id: 7001 },
{
id: 8102,
linked_order_id: null,
automation: {
status: "accepted",
action: "attach_order",
},
},
{ id: 8103, linked_order_id: null },
],
3
)
);
});
const wrapper = mountRight();
await flushAll();
expect(wrapper.get("[data-testid='invoicing-period-view-selector-self_wash']").text()).toContain("Selvvask (2/3)");
expect(selfWashCalls()[0][2]).toMatchObject({
limit: 10000,
filters: "StartTime-date_from:2026-04-01,StartTime-date_to:2026-04-30",
order: "StartTime:desc",
});
});
it("ignores stale period responses that resolve after a newer search request", async () => {
const deferred = [];
requestMock.mockImplementation((url) => {
+15 -1
View File
@@ -100,6 +100,10 @@ const xlvaskUsagePaginationSource = readFileSync(
join(root, "src/components/displays/pagination/models/DepartmentPos/XLVaskUsagePagination.vue"),
"utf8"
);
const xlvaskUsagePeriodConstantsSource = readFileSync(
join(root, "src/components/displays/department/pos/sync/xlvaskUsagePeriodConstants.js"),
"utf8"
);
const xlvaskUsageOrdersTableSource = readFileSync(
join(root, "src/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue"),
"utf8"
@@ -479,7 +483,7 @@ describe("Periode tab contract", () => {
expect(periodViewSelfWashSource).toContain(':load-all-at-once="true"');
expect(xlvaskUsagePaginationSource).toContain("inheritPeriodFilters");
expect(xlvaskUsagePaginationSource).toContain("loadAllAtOnce");
expect(xlvaskUsagePaginationSource).toContain("const SELFWASH_PERIOD_ALL_LIMIT = 10000");
expect(xlvaskUsagePeriodConstantsSource).toContain("export const SELFWASH_PERIOD_ALL_LIMIT = 10000");
expect(xlvaskUsagePaginationSource).toContain("setMetaItemsPerPage(SELFWASH_PERIOD_ALL_LIMIT, false)");
expect(xlvaskUsagePaginationSource).toContain(
"const shouldShowLocalFilters = computed(() => !props.inheritPeriodFilters)"
@@ -496,6 +500,7 @@ describe("Periode tab contract", () => {
expect(periodRightSource).toContain("getCachedSelfWashCounts");
expect(periodRightSource).toContain("setCachedSelfWashCounts");
expect(periodRightSource).toContain("'/modules/xlvask/services/usage/orders'");
expect(periodRightSource).toContain("limit: SELFWASH_PERIOD_ALL_LIMIT");
expect(periodRightSource).toContain("StartTime-date_from:${dates.computed.formattedStartDate.value}");
expect(periodRightSource).toContain("StartTime-date_to:${dates.computed.formattedEndDate.value}");
expect(periodRightSource).toContain("if (type.name === 'self_wash')");
@@ -504,6 +509,15 @@ describe("Periode tab contract", () => {
expect(periodRightSource).not.toContain("showProgress: false");
});
it("keeps Selvvask usage pagination scoped to the period view", () => {
expect(xlvaskUsagePaginationSource).toContain("usePaginatedList");
expect(xlvaskUsagePaginationSource).toContain("provide(PaginatedListKey, paginatedList)");
expect(xlvaskUsageOrdersTableSource).toContain("usePaginatedListInstance");
expect(xlvaskUsageOrdersTableSource).toContain(
"const { orderBy, orderDirection, setOrder, loadList } = usePaginatedListInstance();"
);
});
it("keeps Selvvask usage entries wrapped inside the period content area", () => {
expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-card-primary");
expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-card-status");