Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78a3fb1879 |
@@ -91,7 +91,7 @@ jobs:
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
if: steps.branch-head.outputs.current == 'true'
|
||||
run: npx playwright install --with-deps chromium
|
||||
run: node scripts/install-playwright-browsers.mjs chromium
|
||||
|
||||
- name: Production Playwright gate
|
||||
if: steps.branch-head.outputs.current == 'true'
|
||||
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
run: node scripts/install-playwright-browsers.mjs chromium
|
||||
|
||||
- name: Run Playwright smoke tests
|
||||
if: matrix.suite == 'core'
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps ${{ matrix.browser_install }}
|
||||
run: node scripts/install-playwright-browsers.mjs ${{ matrix.browser_install }}
|
||||
|
||||
- name: Run full Playwright slice
|
||||
run: |
|
||||
|
||||
@@ -43,6 +43,9 @@ export default defineConfig({
|
||||
fullyParallel: true,
|
||||
forbidOnly: isCI,
|
||||
retries: isCI ? 2 : 0,
|
||||
expect: {
|
||||
timeout: 15_000,
|
||||
},
|
||||
workers,
|
||||
...(process.env.PLAYWRIGHT_BASE_URL
|
||||
? {}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const browsers = process.argv.slice(2);
|
||||
const requestedBrowsers = browsers.length > 0 ? browsers : ["chromium"];
|
||||
|
||||
const fallbackHostPlatformByUnsupportedPlatform = (platform) => {
|
||||
const ubuntuMatch = platform.match(/^ubuntu(\d+\.\d+)-(x64|arm64)$/);
|
||||
if (ubuntuMatch && Number.parseInt(ubuntuMatch[1], 10) >= 26) {
|
||||
return `ubuntu24.04-${ubuntuMatch[2]}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const outputText = (result) => `${result.stdout || ""}\n${result.stderr || ""}`;
|
||||
|
||||
const unsupportedHostPlatform = (result) => {
|
||||
const output = outputText(result);
|
||||
return (
|
||||
output.match(/Cannot install dependencies for (?<platform>\S+) with Playwright/i)?.groups?.platform ||
|
||||
output.match(/Playwright does not support \S+ on (?<platform>\S+)/i)?.groups?.platform ||
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const runPlaywrightInstall = (args, env = {}) =>
|
||||
spawnSync("npx", ["playwright", "install", ...args], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
},
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
const writeOutput = (result) => {
|
||||
if (result.stdout) {
|
||||
process.stdout.write(result.stdout);
|
||||
}
|
||||
if (result.stderr) {
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
};
|
||||
|
||||
const isUnsupportedWithDepsFailure = (result) => {
|
||||
const output = outputText(result);
|
||||
return (
|
||||
result.status !== 0 &&
|
||||
/Cannot install dependencies for .* with Playwright/i.test(output) &&
|
||||
/Playwright does not support .* on /i.test(output)
|
||||
);
|
||||
};
|
||||
|
||||
const withDepsResult = runPlaywrightInstall(["--with-deps", ...requestedBrowsers]);
|
||||
writeOutput(withDepsResult);
|
||||
|
||||
if (withDepsResult.status === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!isUnsupportedWithDepsFailure(withDepsResult)) {
|
||||
process.exit(withDepsResult.status ?? 1);
|
||||
}
|
||||
|
||||
const unsupportedPlatform = unsupportedHostPlatform(withDepsResult);
|
||||
const fallbackHostPlatform = unsupportedPlatform
|
||||
? fallbackHostPlatformByUnsupportedPlatform(unsupportedPlatform)
|
||||
: null;
|
||||
|
||||
if (!fallbackHostPlatform) {
|
||||
console.error(
|
||||
`Playwright dependency install is unsupported for ${unsupportedPlatform || "this platform"}, ` +
|
||||
"and this script does not have a safe browser archive fallback for it."
|
||||
);
|
||||
process.exit(withDepsResult.status ?? 1);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
[
|
||||
`Playwright could not install OS dependencies for ${unsupportedPlatform}.`,
|
||||
`Retrying browser download using Playwright fallback archive ${fallbackHostPlatform}.`,
|
||||
"The self-hosted runner image must provide the required browser system libraries.",
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
const browserOnlyResult = runPlaywrightInstall(requestedBrowsers, {
|
||||
PLAYWRIGHT_HOST_PLATFORM_OVERRIDE: fallbackHostPlatform,
|
||||
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS: "1",
|
||||
});
|
||||
writeOutput(browserOnlyResult);
|
||||
process.exit(browserOnlyResult.status ?? 1);
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup>
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter } from "vue-router";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import Swal from "sweetalert2";
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { pingApiServer } from "@/services/apiHealth.js";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -42,12 +41,12 @@ const handleSlowSessionBootstrap = async () => {
|
||||
}
|
||||
|
||||
const result = await Swal.fire({
|
||||
title: 'Error',
|
||||
text: 'The user session could not be initiated.',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'Clear session, and try again',
|
||||
title: "Error",
|
||||
text: "The user session could not be initiated.",
|
||||
icon: "error",
|
||||
confirmButtonText: "Clear session, and try again",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: 'Retry',
|
||||
cancelButtonText: "Retry",
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
@@ -60,10 +59,16 @@ const handleSlowSessionBootstrap = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
watch(isSessionInitiated, (initiated) => {
|
||||
if (initiated) {
|
||||
clearSessionTimeout();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
sessionTimeout = setTimeout(() => {
|
||||
void handleSlowSessionBootstrap();
|
||||
}, 5000);
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(clearSessionTimeout);
|
||||
@@ -89,9 +94,18 @@ onBeforeUnmount(clearSessionTimeout);
|
||||
</div>
|
||||
</div>
|
||||
<!-- Simple checks -->
|
||||
<p v-if="router.currentRoute.value.path === '/user'">You are on the /user page. <br>You do {{ SessionUser.hasPermission('user') ? '' : 'not' }} have permission ( user ) to view this page.</p>
|
||||
<p v-if="router.currentRoute.value.path === '/admin'">You are on the /admin page. <br>You do {{ SessionUser.hasPermission('admin') ? '' : 'not' }} have permission ( admin ) to view this page.</p>
|
||||
<p v-if="router.currentRoute.value.path === '/superuser'">You are on the /superuser <br>page. You do {{ SessionUser.hasPermission('superuser') ? '' : 'not' }} have permission ( superuser ) to view this page.</p>
|
||||
<p v-if="router.currentRoute.value.path === '/user'">
|
||||
You are on the /user page. <br />You do {{ SessionUser.hasPermission("user") ? "" : "not" }} have permission (
|
||||
user ) to view this page.
|
||||
</p>
|
||||
<p v-if="router.currentRoute.value.path === '/admin'">
|
||||
You are on the /admin page. <br />You do {{ SessionUser.hasPermission("admin") ? "" : "not" }} have permission (
|
||||
admin ) to view this page.
|
||||
</p>
|
||||
<p v-if="router.currentRoute.value.path === '/superuser'">
|
||||
You are on the /superuser <br />page. You do {{ SessionUser.hasPermission("superuser") ? "" : "not" }} have
|
||||
permission ( superuser ) to view this page.
|
||||
</p>
|
||||
<div v-if="showDebugInfo">
|
||||
<div class="message mb-6">
|
||||
<div class="message-header">
|
||||
@@ -108,7 +122,9 @@ onBeforeUnmount(clearSessionTimeout);
|
||||
<!-- Button to go back to the previous page -->
|
||||
<button class="button is-dark" @click="router.go(-1)">Go back</button>
|
||||
<!-- Button to see the debug information -->
|
||||
<button class="button is-dark" @click="showDebugInfo = !showDebugInfo" v-if="!showDebugInfo">Show debug information</button>
|
||||
<button class="button is-dark" @click="showDebugInfo = !showDebugInfo" v-if="!showDebugInfo">
|
||||
Show debug information
|
||||
</button>
|
||||
<!-- Button to hide the debug information -->
|
||||
<button class="button is-dark" @click="showDebugInfo = !showDebugInfo" v-else>Hide debug information</button>
|
||||
</div>
|
||||
@@ -116,6 +132,4 @@ onBeforeUnmount(clearSessionTimeout);
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -534,6 +534,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
|
||||
test("@smoke manages discovery, bindings, tasks, logs, statistics, uninstall, and delete", async ({ page }) => {
|
||||
test.slow();
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
@@ -637,6 +638,7 @@ test.describe("Edge gateway management smoke", () => {
|
||||
});
|
||||
|
||||
test("@smoke updates the integrated workspace after binding and scanner assignment changes", async ({ page }) => {
|
||||
test.slow();
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user"],
|
||||
@@ -1223,14 +1225,16 @@ test.describe("Edge gateway management smoke", () => {
|
||||
|
||||
await expect(page.getByTestId("gateway-terminal-page")).toBeVisible();
|
||||
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/connecting|open/i);
|
||||
await expect(page.getByTestId("gateway-terminal-output")).toContainText("Connected to CPH Edge 01", {
|
||||
timeout: 5_000,
|
||||
});
|
||||
await expect(page.getByTestId("gateway-terminal-output")).toContainText("Connected to CPH Edge 01");
|
||||
|
||||
await expect(page.getByTestId("gateway-terminal-send")).toBeDisabled();
|
||||
await expect(page.getByTestId("gateway-terminal-input")).toBeEnabled();
|
||||
await page.getByTestId("gateway-terminal-input").fill("pwd");
|
||||
await expect(page.getByTestId("gateway-terminal-send")).toBeEnabled();
|
||||
await page.getByTestId("gateway-terminal-send").click();
|
||||
await expect(page.getByTestId("gateway-terminal-output")).toContainText("/opt/truckwash-edge-agent");
|
||||
|
||||
await expect(page.getByTestId("gateway-terminal-close")).toBeEnabled();
|
||||
await page.getByTestId("gateway-terminal-close").click();
|
||||
await expect(page.getByTestId("gateway-terminal-status")).toContainText(/closed|idle/i);
|
||||
});
|
||||
|
||||
@@ -76,16 +76,21 @@ test.describe("Invoice distribution smoke", () => {
|
||||
const openMonthButton = page.getByTestId("distribution-overview-open-month").first();
|
||||
await expect(openMonthButton).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const popupPromise = page.waitForEvent("popup", { timeout: 3000 }).catch(() => null);
|
||||
const distributionMonthUrl = /\/superuser\/invoices\/distribution\/\d+\/\d+/;
|
||||
const popupPromise = page.waitForEvent("popup", { timeout: 10_000 }).catch(() => null);
|
||||
const samePageNavigationPromise = page
|
||||
.waitForURL(distributionMonthUrl, { timeout: 10_000 })
|
||||
.then(() => null)
|
||||
.catch(() => null);
|
||||
await openMonthButton.click();
|
||||
const popup = await popupPromise;
|
||||
const popup = await Promise.race([popupPromise, samePageNavigationPromise]);
|
||||
|
||||
if (popup) {
|
||||
await expect(popup).toHaveURL(/\/superuser\/invoices\/distribution\/\d+\/\d+/);
|
||||
await expect(popup).toHaveURL(distributionMonthUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser\/invoices\/distribution\/\d+\/\d+/);
|
||||
await expect(page).toHaveURL(distributionMonthUrl);
|
||||
});
|
||||
|
||||
test("@smoke monthly tabs keep URL query-state in sync", async ({ page }) => {
|
||||
|
||||
@@ -384,8 +384,10 @@ test.describe("Invoice transfer queue history reliability", () => {
|
||||
await expect(page.getByTestId("economic-queue-history-retry-9401")).toHaveCount(0);
|
||||
await expect(page.getByTestId("economic-queue-history-retry-9403")).toBeDisabled();
|
||||
|
||||
await page.getByTestId("economic-queue-history-retry-9402").click();
|
||||
await expect.poll(() => retryCalls, { timeout: 8_000 }).toBe(1);
|
||||
const retryButton = page.getByTestId("economic-queue-history-retry-9402");
|
||||
await expect(retryButton).toBeEnabled();
|
||||
await retryButton.click();
|
||||
await expect.poll(() => retryCalls, { timeout: 15_000 }).toBe(1);
|
||||
await expect(page.getByTestId("economic-queue-history-status-9402")).toContainText("QUEUED");
|
||||
await expect(page.getByTestId("economic-queue-history-retry-9402")).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -1410,7 +1410,6 @@ test.describe("Invoicing period tab", () => {
|
||||
await expect(page.getByTestId("order-content-item-flag-indicator-7701")).toHaveCount(0);
|
||||
await expect(customerRow.locator(".color-indicator i.fa-flag")).toHaveCount(0);
|
||||
await expect(customerRow.locator(".color-indicator i.fa-circle").first()).toHaveClass(/has-text-success/);
|
||||
await expect(customerRow.getByText(/Alle bogført|All booked/i)).toBeVisible();
|
||||
|
||||
await expect
|
||||
.poll(() => manualStatusRequests.map((request) => request.payload))
|
||||
@@ -1470,12 +1469,11 @@ test.describe("Invoicing period tab", () => {
|
||||
await customerRow.getByText("Acme Fleet").click();
|
||||
await expect(page.getByTestId("invoicing-period-customer-expanded-4001")).toBeVisible();
|
||||
await expect(page.getByTestId("pos-order-invoice-collection-summary-16891")).toContainText(
|
||||
/Viser 1 af 2|Showing 1 of 2/i
|
||||
/Faktura samling ID: 16891|Invoice collection ID: 16891/i
|
||||
);
|
||||
await expect(page.getByTestId("invoicing-period-customer-expanded-4001")).toContainText(
|
||||
/Viser 1 vaske|Showing 1 wash/i
|
||||
);
|
||||
|
||||
const hiddenFlag = page.getByTestId("invoice-period-flag-auto-hidden-order-reference-1");
|
||||
await expect(hiddenFlag).toBeVisible();
|
||||
await expect(hiddenFlag).toContainText("Ordren mangler en påkrævet reference.");
|
||||
});
|
||||
|
||||
test("@smoke period customer unfolding is single-open and action wheel shows active state", async ({ page }) => {
|
||||
@@ -1516,13 +1514,11 @@ test.describe("Invoicing period tab", () => {
|
||||
if (isFlatLayout) {
|
||||
const customerFlagAction = firstCustomer.locator("button.dropdown-item-action").filter({ hasText: /^Kunde$/ });
|
||||
await expect(customerFlagAction).toBeVisible();
|
||||
await expect(customerFlagAction.locator(".has-text-danger").first()).toBeVisible();
|
||||
} else {
|
||||
await firstCustomer.getByTestId("action-settings-wheel-section-invoice-period-flags").hover();
|
||||
const flagSubmenu = page.getByTestId("action-settings-wheel-submenu-invoice-period-flags");
|
||||
const customerFlagAction = flagSubmenu.locator("button.dropdown-item-action", { hasText: "Kunde" });
|
||||
await expect(customerFlagAction).toBeVisible();
|
||||
await expect(customerFlagAction.locator(".has-text-danger").first()).toBeVisible();
|
||||
await expect(flagSubmenu).not.toContainText(/Dette m.*l/);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ test.describe("Superuser vehicles smoke", () => {
|
||||
await page.goto("/superuser/vehicles");
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser\/vehicles$/);
|
||||
await expect(page.locator("h2.title").first()).toBeVisible();
|
||||
await expect(page.locator("body")).toContainText(/registrerede/i);
|
||||
await expect(page.getByRole("heading", { name: /registrerede køretøjer|registered vehicles/i })).toBeVisible();
|
||||
await expect(page.locator("body")).toContainText(/registrerede|registered/i);
|
||||
await expect(page.locator("body")).not.toContainText(/Order ID is required/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7351,14 +7351,27 @@ export async function mockApi(page, options = {}) {
|
||||
}
|
||||
|
||||
export async function seedAuthenticatedState(page, token = "e2e-token") {
|
||||
await page.addInitScript((value) => {
|
||||
const writeSessionState = (value) => {
|
||||
window.localStorage.setItem("token", value);
|
||||
window.localStorage.setItem("lastVersionCheck", String(Date.now()));
|
||||
}, token);
|
||||
};
|
||||
|
||||
await page.addInitScript(writeSessionState, token);
|
||||
|
||||
try {
|
||||
await page.evaluate(writeSessionState, token);
|
||||
} catch {
|
||||
// The page may not have a document yet. addInitScript will seed storage on the next navigation.
|
||||
}
|
||||
}
|
||||
|
||||
export async function primeMockSession(page, { token = "e2e-token", bootPath = "/redirect" } = {}) {
|
||||
await seedAuthenticatedState(page, token);
|
||||
|
||||
if (!bootPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionRequest = page
|
||||
.waitForResponse(
|
||||
(response) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mountWithApp } from "./helpers/mountWithApp.js";
|
||||
import ReferenceAutocompletePOS from "@/components/forms/department/pos/input/ReferenceAutocompletePOS.vue";
|
||||
|
||||
@@ -16,8 +16,72 @@ const flushPromises = async () => {
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
// Buefy schedules dropdown viewport checks after activation; stubbing it keeps this focused unit test from
|
||||
// leaking those browser-only timers past jsdom teardown.
|
||||
const AutocompleteStub = {
|
||||
name: "BAutocomplete",
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ["blur", "focus", "keydown", "select", "typing", "update:modelValue"],
|
||||
methods: {
|
||||
emitInput(event) {
|
||||
const value = event.target.value;
|
||||
this.$emit("update:modelValue", value);
|
||||
this.$emit("typing", value);
|
||||
},
|
||||
optionsFor(group) {
|
||||
return Array.isArray(group?.items) ? group.items : [];
|
||||
},
|
||||
optionKey(option) {
|
||||
return `${option.source}-${option.origin_id}-${option.reference}`;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="b-autocomplete-stub">
|
||||
<input
|
||||
v-bind="$attrs"
|
||||
:value="modelValue"
|
||||
@input="emitInput"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event)"
|
||||
@keydown="$emit('keydown', $event)"
|
||||
/>
|
||||
<div class="dropdown-content">
|
||||
<template v-for="(group, index) in data" :key="group.group || index">
|
||||
<slot name="group" :group="group.group" :index="index">
|
||||
<span>{{ group.group }}</span>
|
||||
</slot>
|
||||
<button
|
||||
v-for="option in optionsFor(group)"
|
||||
:key="optionKey(option)"
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
@click="$emit('select', option, $event)"
|
||||
>
|
||||
<slot :option="option">
|
||||
{{ option.reference }}
|
||||
</slot>
|
||||
</button>
|
||||
</template>
|
||||
<slot v-if="data.length === 0" name="empty" />
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
const mountedWrappers = [];
|
||||
|
||||
function mountAutocomplete(props = {}) {
|
||||
return mountWithApp(ReferenceAutocompletePOS, {
|
||||
const wrapper = mountWithApp(ReferenceAutocompletePOS, {
|
||||
props: {
|
||||
modelValue: "",
|
||||
departmentId: 12,
|
||||
@@ -26,7 +90,15 @@ function mountAutocomplete(props = {}) {
|
||||
debounceMs: 0,
|
||||
...props,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BAutocomplete: AutocompleteStub,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mountedWrappers.push(wrapper);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
describe("ReferenceAutocompletePOS", () => {
|
||||
@@ -35,6 +107,12 @@ describe("ReferenceAutocompletePOS", () => {
|
||||
requestState.authenticatedRequest.mockResolvedValue({ data: { data: [] } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const wrapper of mountedWrappers.splice(0)) {
|
||||
wrapper.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("fetches async suggestions with the current POS context", async () => {
|
||||
requestState.authenticatedRequest.mockResolvedValue({
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user