Compare commits

...
23 changed files with 487 additions and 35 deletions
+4
View File
@@ -112,6 +112,8 @@ jobs:
env:
PLAYWRIGHT_ARTIFACT_NAMESPACE: e2e-pr-${{ matrix.suite }}-${{ matrix.project }}
PLAYWRIGHT_REPORTER_MODE: line-html
PLAYWRIGHT_WORKERS: 1
PLAYWRIGHT_VIDEO_MODE: on-first-retry
steps:
- name: Repair self-hosted workspace permissions
shell: bash
@@ -223,6 +225,8 @@ jobs:
--env CI="${CI:-}" \
--env PLAYWRIGHT_ARTIFACT_NAMESPACE="$PLAYWRIGHT_ARTIFACT_NAMESPACE" \
--env PLAYWRIGHT_REPORTER_MODE="$PLAYWRIGHT_REPORTER_MODE" \
--env PLAYWRIGHT_WORKERS="$PLAYWRIGHT_WORKERS" \
--env PLAYWRIGHT_VIDEO_MODE="$PLAYWRIGHT_VIDEO_MODE" \
--env PLAYWRIGHT_DEV_PORT="$playwright_dev_port" \
--env MATRIX_SUITE="$MATRIX_SUITE" \
--env MATRIX_PROJECT="$MATRIX_PROJECT" \
+15 -2
View File
@@ -7,14 +7,18 @@ export const fallbackChangePatterns = [
/^vite\.config\.js$/u,
/^playwright(?:\..+)?\.config\.(?:js|ts)$/u,
/^playwright\.global-(?:setup|teardown)\.mjs$/u,
/^scripts\/run-playwright-(?:pr|ci-parallel|batched-chromium)\.mjs$/u,
/^scripts\/run-playwright-(?:ci-parallel|batched-chromium)\.mjs$/u,
/^tests\/e2e\/(?:support|fixtures)\//u,
];
export const sourceMappings = [
{
name: "auth",
patterns: [/^src\/(?:views|components|middleware)\/.*auth/iu, /^src\/views\/auth\//u, /^src\/components\/session\//u],
patterns: [
/^src\/(?:views|components|middleware)\/.*auth/iu,
/^src\/views\/auth\//u,
/^src\/components\/session\/(?!token\/SessionUser\/Objects\/)/u,
],
specs: ["tests/e2e/auth.smoke.spec.js", "tests/e2e/userAuth.spec.ts"],
projects: chromiumProjects,
},
@@ -92,6 +96,15 @@ export const sourceMappings = [
specs: ["tests/e2e/superuser-system-status.smoke.spec.js"],
projects: chromiumProjects,
},
{
name: "superuser-department-pricing",
patterns: [
/^src\/views\/dashboards\/superUserDashboard\/department\/(?:DepartmentPricing|SuperUserSelectedDepartmentObject)\.vue$/u,
/^src\/components\/session\/token\/SessionUser\/Objects\/Departments\.vue$/u,
],
specs: ["tests/e2e/superuser-department-pricing-custom-only.spec.ts"],
projects: ["chromium-desktop"],
},
{
name: "self-serve",
patterns: [/self[-/]?serve/iu, /selfserve/iu, /wash\/MyWash/iu],
+1
View File
@@ -103,6 +103,7 @@ export const ownedFilesByRole = {
"superuser-department-branding.spec.js",
"superuser-department-gates.spec.ts",
"superuser-department-lanes.spec.ts",
"superuser-department-pricing-custom-only.spec.ts",
"superuser-departments-archive.spec.ts",
"superuser-drafts.spec.ts",
"superuser-products-layout.spec.ts",
+24 -4
View File
@@ -261,6 +261,8 @@ function selectChangedTests(changedFiles) {
specProjects: new Map(),
mappedFiles: [],
unmappedFiles: [],
directSpecFiles: [],
skippedDirectSpecFiles: [],
fallback: false,
};
@@ -270,8 +272,7 @@ function selectChangedTests(changedFiles) {
const file = normalizePath(rawFile);
if (isE2eSpec(file)) {
addSpec(selection, file, selectedProjects);
selection.mappedFiles.push(file);
selection.directSpecFiles.push(file);
continue;
}
@@ -295,13 +296,26 @@ function selectChangedTests(changedFiles) {
selection.mappedFiles.push(file);
for (const mapping of matches) {
const projects = mapping.projects.filter((project) => selectedProjects.includes(project));
const mappedProjects = mapping.projects.length > 0 ? mapping.projects : selectedProjects;
const projects = mappedProjects.filter((project) => selectedProjects.includes(project));
if (projects.length === 0) {
continue;
}
for (const spec of mapping.specs) {
addSpec(selection, spec, projects.length > 0 ? projects : selectedProjects);
addSpec(selection, spec, projects);
}
}
}
if (selection.specProjects.size === 0 && !selection.fallback) {
for (const file of selection.directSpecFiles) {
addSpec(selection, file, selectedProjects);
selection.mappedFiles.push(file);
}
} else {
selection.skippedDirectSpecFiles.push(...selection.directSpecFiles);
}
return selection;
}
@@ -370,6 +384,12 @@ async function runChangedSelection(selection) {
return 0;
}
if (selection.skippedDirectSpecFiles.length > 0) {
console.log(
`[playwright-pr] Source mappings selected changed-area specs; direct E2E file edits are covered by mapped/core gates: ${selection.skippedDirectSpecFiles.join(", ")}`
);
}
for (const [index, group] of groups.entries()) {
for (const project of group.projects) {
const code = await runPlaywright({
@@ -108,6 +108,14 @@ export const Departments = {
required: false,
},
},
custom_pricing_only: {
label: t("objects.departments.columns.custom_pricing_only"),
type: "boolean",
sortable: true,
creation: {
required: false,
},
},
longitude: {
label: t("objects.departments.columns.longitude"),
type: "number",
@@ -164,6 +172,14 @@ export const Departments = {
archived: async (id, archived) => {
return ObjectsGlobal.set.column(Departments.meta.endpoint, id, "archived", ObjectsGlobal.parse.boolean(archived));
},
custom_pricing_only: async (id, custom_pricing_only) => {
return ObjectsGlobal.set.column(
Departments.meta.endpoint,
id,
"custom_pricing_only",
ObjectsGlobal.parse.boolean(custom_pricing_only)
);
},
longitude: async (id, longitude) => {
return ObjectsGlobal.set.column(Departments.meta.endpoint, id, "longitude", parseFloat(longitude));
},
+8
View File
@@ -3455,6 +3455,13 @@
"opening_hours": {
"closed": "Stengt"
},
"pricing": {
"custom_pricing_disabled": "Fallback aktiv",
"custom_pricing_enabled": "Kun egne priser",
"custom_pricing_missing_price": "Manglende afdelingspriser bliver 999999.",
"custom_pricing_only": "Ingen fallback-priser",
"effective_department_price": "Effektiv afdelingspris"
},
"search_placeholder": "@.capitalize:{'words.generated.søg'} @:{'words.generated.efter'} @:{'words.generated.afdelingsnavn'}",
"select_department": "@.capitalize:{'words.generated.vælg'} @:{'words.replication.host_definite_suffix'} @:{'words.generated.afdeling'}",
"subtitle": "@.capitalize:{'words.generated.administrer'} @:{'words.generated.afdelinger'}",
@@ -4285,6 +4292,7 @@
"departments": {
"columns": {
"archived": "Arkiveret",
"custom_pricing_only": "Ingen fallback-priser",
"dimension": "Dimension",
"economic_department": "@.upper:{'words.generated.e'}-@:{'words.generated.conomic'} @:{'words.generated.afdeling'}",
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
+8
View File
@@ -3287,6 +3287,13 @@
"opening_hours": {
"closed": "@:{'templates.generated.compat.global.closed'}"
},
"pricing": {
"custom_pricing_disabled": "Fallback enabled",
"custom_pricing_enabled": "Custom only",
"custom_pricing_missing_price": "Missing department prices resolve to 999999.",
"custom_pricing_only": "No fallback pricing",
"effective_department_price": "Effective department price"
},
"search_placeholder": "@.capitalize:{'words.generated.search'} @:{'words.generated.by'} @:{'words.generated.department'} @:{'words.generated.name'}",
"select_department": "@.capitalize:{'words.generated.select'} @:{'words.generated.a'} @:{'words.generated.department'}",
"subtitle": "@.capitalize:{'words.generated.manage'} @:{'words.generated.departments'}",
@@ -4117,6 +4124,7 @@
"departments": {
"columns": {
"archived": "@:{'words.generated.archived'}",
"custom_pricing_only": "No fallback pricing",
"dimension": "Dimension",
"economic_department": "@.upper:{'words.generated.e'}-@:{'words.generated.conomic'} @.capitalize:{'words.generated.department'}",
"latitude": "@:{'templates.generated.compat.departments.form.latitude'}",
+8
View File
@@ -2420,6 +2420,13 @@
"title": "@:{'templates.generated.compat.departments.tab.opening_hours'}"
},
"phone": "@:common.phone",
"pricing": {
"custom_pricing_disabled": "@:{'templates.generated.compat.departments.pricing.custom_pricing_disabled'}",
"custom_pricing_enabled": "@:{'templates.generated.compat.departments.pricing.custom_pricing_enabled'}",
"custom_pricing_missing_price": "@:{'templates.generated.compat.departments.pricing.custom_pricing_missing_price'}",
"custom_pricing_only": "@:{'templates.generated.compat.departments.pricing.custom_pricing_only'}",
"effective_department_price": "@:{'templates.generated.compat.departments.pricing.effective_department_price'}"
},
"products": {
"new_product": "@:products.new_product"
},
@@ -3486,6 +3493,7 @@
"departments": {
"columns": {
"archived": "@:{'templates.generated.compat.objects.departments.columns.archived'}",
"custom_pricing_only": "@:{'templates.generated.compat.objects.departments.columns.custom_pricing_only'}",
"dimension": "@:{'templates.generated.compat.objects.departments.columns.dimension'}",
"economic_department": "@:{'templates.generated.compat.objects.departments.columns.economic_department'}",
"latitude": "@:{'templates.generated.compat.objects.departments.columns.latitude'}",
@@ -21,6 +21,13 @@
"opening_hours": {
"closed": "Stengt"
},
"pricing": {
"custom_pricing_disabled": "Fallback aktiv",
"custom_pricing_enabled": "Kun egne priser",
"custom_pricing_missing_price": "Manglende afdelingspriser bliver 999999.",
"custom_pricing_only": "Ingen fallback-priser",
"effective_department_price": "Effektiv afdelingspris"
},
"search_placeholder": "@.capitalize:{'terms.glossary.søg'} @:{'terms.glossary.efter'} @:{'terms.glossary.afdelingsnavn'}",
"select_department": "@.capitalize:{'terms.glossary.vælg'} @:{'terms.replication.host_definite_suffix'} @:{'terms.glossary.afdeling'}",
"subtitle": "@.capitalize:{'terms.glossary.administrer'} @:{'terms.glossary.afdelinger'}",
@@ -140,6 +140,7 @@
"departments": {
"columns": {
"archived": "Arkiveret",
"custom_pricing_only": "Ingen fallback-priser",
"dimension": "Dimension",
"economic_department": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.conomic'} @:{'terms.glossary.afdeling'}",
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
@@ -21,6 +21,13 @@
"opening_hours": {
"closed": "@:{'phrases.compat.global.closed'}"
},
"pricing": {
"custom_pricing_disabled": "Fallback enabled",
"custom_pricing_enabled": "Custom only",
"custom_pricing_missing_price": "Missing department prices resolve to 999999.",
"custom_pricing_only": "No fallback pricing",
"effective_department_price": "Effective department price"
},
"search_placeholder": "@.capitalize:{'terms.glossary.search'} @:{'terms.glossary.by'} @:{'terms.glossary.department'} @:{'terms.glossary.name'}",
"select_department": "@.capitalize:{'terms.glossary.select'} @:{'terms.glossary.a'} @:{'terms.glossary.department'}",
"subtitle": "@.capitalize:{'terms.glossary.manage'} @:{'terms.glossary.departments'}",
@@ -140,6 +140,7 @@
"departments": {
"columns": {
"archived": "@:{'terms.glossary.archived'}",
"custom_pricing_only": "No fallback pricing",
"dimension": "Dimension",
"economic_department": "@.upper:{'terms.glossary.e'}-@:{'terms.glossary.conomic'} @.capitalize:{'terms.glossary.department'}",
"latitude": "@:{'phrases.compat.departments.form.latitude'}",
@@ -38,6 +38,13 @@
"title": "@:{'phrases.compat.departments.tab.opening_hours'}"
},
"phone": "@:common.phone",
"pricing": {
"custom_pricing_disabled": "@:{'phrases.compat.departments.pricing.custom_pricing_disabled'}",
"custom_pricing_enabled": "@:{'phrases.compat.departments.pricing.custom_pricing_enabled'}",
"custom_pricing_missing_price": "@:{'phrases.compat.departments.pricing.custom_pricing_missing_price'}",
"custom_pricing_only": "@:{'phrases.compat.departments.pricing.custom_pricing_only'}",
"effective_department_price": "@:{'phrases.compat.departments.pricing.effective_department_price'}"
},
"products": {
"new_product": "@:products.new_product"
},
@@ -194,6 +194,7 @@
"departments": {
"columns": {
"archived": "@:{'phrases.compat.objects.departments.columns.archived'}",
"custom_pricing_only": "@:{'phrases.compat.objects.departments.columns.custom_pricing_only'}",
"dimension": "@:{'phrases.compat.objects.departments.columns.dimension'}",
"economic_department": "@:{'phrases.compat.objects.departments.columns.economic_department'}",
"latitude": "@:{'phrases.compat.objects.departments.columns.latitude'}",
@@ -3,7 +3,15 @@ import PageTitle from "@/components/global/PageTitle.vue";
import RestrictedPageWrapper from "@/components/page/wrappers/RestrictedPageWrapper.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import DepartmentSubPageWrapper from "@/views/dashboards/superUserDashboard/department/DepartmentSubPageWrapper.vue";
import { setDepartment, department, departmentId, getDepartmentPrice, editDepartmentPrice } from "@/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue";
import {
CUSTOM_PRICING_MISSING_PRICE,
setDepartment,
getDepartmentPrices,
getExplicitDepartmentPrice,
editDepartmentPrice,
isCustomPricingOnly,
updateCustomPricingOnly,
} from "@/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue";
import { useRouter } from 'vue-router'
import { ref } from 'vue';
import { getProducts } from "@/components/shop/Products.vue";
@@ -11,14 +19,40 @@ import { getProducts } from "@/components/shop/Products.vue";
// Get the department from the route
const router = useRouter()
setDepartment(router.currentRoute.value.params.departmentId);
getDepartmentPrices();
// Set the products
const products = ref([]);
const isUpdatingCustomPricingOnly = ref(false);
// Get the products
getProducts().then((response) => {
products.value = response.data.data;
});
const getDepartmentPriceDisplay = (product) => {
const explicitPrice = getExplicitDepartmentPrice(product);
if (explicitPrice !== null) {
return explicitPrice;
}
return isCustomPricingOnly() ? CUSTOM_PRICING_MISSING_PRICE : '-';
};
const isMissingCustomPrice = (product) => {
return isCustomPricingOnly() && getExplicitDepartmentPrice(product) === null;
};
const toggleCustomPricingOnly = async (event) => {
const enabled = event.target.checked;
isUpdatingCustomPricingOnly.value = true;
try {
await updateCustomPricingOnly(enabled);
} catch {
event.target.checked = isCustomPricingOnly();
} finally {
isUpdatingCustomPricingOnly.value = false;
}
};
</script>
<template>
@@ -28,12 +62,38 @@ getProducts().then((response) => {
<PageTitle title="Department" subtitle="Department pricing" />
</template>
<div>
<section class="department-pricing-settings" data-testid="department-custom-pricing-settings">
<div>
<h2 class="title is-5 mb-1">{{ $t('departments.pricing.custom_pricing_only') }}</h2>
<p class="is-size-7 has-text-grey">
{{ $t('departments.pricing.custom_pricing_missing_price') }}
</p>
</div>
<div class="department-pricing-toggle">
<input
id="department-custom-pricing-only"
class="switch is-rounded is-info"
type="checkbox"
:checked="isCustomPricingOnly()"
:disabled="isUpdatingCustomPricingOnly"
data-testid="department-custom-pricing-only-toggle"
@change="toggleCustomPricingOnly"
/>
<label for="department-custom-pricing-only">
{{
isCustomPricingOnly()
? $t('departments.pricing.custom_pricing_enabled')
: $t('departments.pricing.custom_pricing_disabled')
}}
</label>
</div>
</section>
<table class="is-fullwidth table table-striped is-hoverable is-bordered">
<thead>
<tr>
<th>{{ $t('common.product') }}</th>
<th>{{ $t('tables.common.default_price') }}</th>
<th>{{ $t('tables.common.department_price') }}</th>
<th>{{ $t('departments.pricing.effective_department_price') }}</th>
</tr>
</thead>
<tbody>
@@ -43,24 +103,45 @@ getProducts().then((response) => {
<td
@click="editDepartmentPrice(product)"
class="is-clickable"
>{{ getDepartmentPrice(product) === product.price ? '-' : getDepartmentPrice(product) }}
:class="{ 'has-text-danger has-text-weight-semibold': isMissingCustomPrice(product) }"
:data-testid="`department-price-cell-${product.id}`"
>{{ getDepartmentPriceDisplay(product) }}
<i class="is-pulled-right fas fa-edit"></i>
</td>
</tr>
</tbody>
</table>
Department prices...
<code>{{ departmentId }}</code>
<code>{{ department.id }}</code>
<code>{{ department.name }}</code>
<code>{{ department.description }}</code>
<code>{{ department.created_at }}</code>
<code>{{ department.updated_at }}</code>
</div>
</DepartmentSubPageWrapper>
</RestrictedPageWrapper>
</template>
<style scoped>
.department-pricing-settings {
align-items: center;
border: 1px solid #dbdbdb;
border-radius: 6px;
display: flex;
gap: 1rem;
justify-content: space-between;
margin-bottom: 1rem;
padding: 1rem;
}
</style>
.department-pricing-toggle {
align-items: center;
display: flex;
min-width: 16rem;
}
@media (max-width: 768px) {
.department-pricing-settings {
align-items: flex-start;
flex-direction: column;
}
.department-pricing-toggle {
min-width: 0;
}
}
</style>
@@ -4,12 +4,14 @@ import { authenticatedRequest } from "@/components/session/authenticatedRequest.
import Swal from "sweetalert2";
// Define the department id
export const departmentId = ref(0);
export const CUSTOM_PRICING_MISSING_PRICE = 999999;
const default_department = {
id: null,
name: null,
description: null,
branding: null,
custom_pricing_only: false,
created_at: null,
updated_at: null,
prices: []
@@ -34,6 +36,7 @@ export const department = {
id: ref(''),
name: ref(''),
description: ref(''),
custom_pricing_only: ref(false),
created_at: ref(''),
updated_at: ref(''),
prices: ref([])
@@ -49,6 +52,13 @@ const isDepartmentPricesLoaded = ref(false);
// Set the department id
export const setDepartment = (id) => {
departmentId.value = id;
isDepartmentPricesLoaded.value = false;
department.prices.value = [];
department.custom_pricing_only.value = false;
departmentAdvanced.value = {
...default_department,
...default_department_functions
};
// Load the department data
return getDepartmentData();
}
@@ -60,11 +70,13 @@ export const getDepartmentData = async () => {
department.id.value = response.data.data.id;
department.name.value = response.data.data.name;
department.description.value = response.data.data.description;
department.custom_pricing_only.value = Boolean(response.data.data.custom_pricing_only);
department.created_at.value = response.data.data.created_at;
department.updated_at.value = response.data.data.updated_at;
departmentAdvanced.value = {
...default_department,
...response.data.data,
custom_pricing_only: Boolean(response.data.data.custom_pricing_only),
...default_department_functions
};
})
@@ -88,17 +100,42 @@ export const getDepartmentPrice = (product) => {
getDepartmentPrices();
}
if (isDepartmentPricesLoaded.value) {
const price = department.prices.value.find((price) => price.product_id === product.id);
return price ? price.price : product.price;
const price = getExplicitDepartmentPrice(product);
if (price !== null) {
return price;
}
return isCustomPricingOnly() ? CUSTOM_PRICING_MISSING_PRICE : product.price;
}
return product.price;
}
export const getExplicitDepartmentPrice = (product) => {
const price = department.prices.value.find((price) => price.product_id === product.id);
return price ? price.price : null;
}
export const isCustomPricingOnly = () => Boolean(departmentAdvanced.value.custom_pricing_only || department.custom_pricing_only.value);
export const updateCustomPricingOnly = async (enabled) => {
return authenticatedRequest(`/departments`, "PUT", {
id: departmentId.value,
custom_pricing_only: Boolean(enabled)
}).then(async () => {
department.custom_pricing_only.value = Boolean(enabled);
departmentAdvanced.value = {
...departmentAdvanced.value,
custom_pricing_only: Boolean(enabled)
};
await getDepartmentData();
});
}
export const editDepartmentPrice = (product) => {
const explicitPrice = getExplicitDepartmentPrice(product);
Swal.fire({
title: product.name + ' ( product: ' + product.id + ' )',
input: 'number',
inputValue: getDepartmentPrice(product),
inputValue: explicitPrice === null ? '' : explicitPrice,
inputLabel: 'Price',
inputAttributes: {
autocapitalize: 'off'
+57 -5
View File
@@ -109,6 +109,58 @@ async function acceleratePageTimers(page, timerScale = 0.01) {
}, timerScale);
}
async function dismissUnexpectedSweetAlert(page) {
const overlays = page.locator(".swal2-container.swal2-backdrop-show");
for (let attempt = 0; attempt < 3; attempt += 1) {
const overlay = overlays.first();
if (!(await overlay.isVisible().catch(() => false))) {
return;
}
let dismissed = false;
for (const selector of [".swal2-close", ".swal2-cancel", ".swal2-deny", ".swal2-confirm"]) {
const action = overlay.locator(selector).first();
if (await action.isVisible().catch(() => false)) {
await action.click({ force: true });
dismissed = true;
break;
}
}
if (!dismissed) {
await page.keyboard.press("Escape");
}
await expect(overlays)
.toHaveCount(0, { timeout: 5_000 })
.catch(() => {});
}
}
async function generateGatewayInstaller(page) {
const button = page.getByTestId("gateway-installer-generate");
await expect(button).toBeVisible({ timeout: 15_000 });
await expect(button).toBeEnabled({ timeout: 15_000 });
let lastError;
for (let attempt = 0; attempt < 3; attempt += 1) {
await dismissUnexpectedSweetAlert(page);
try {
await button.click({ timeout: 15_000 });
return;
} catch (error) {
lastError = error;
if (!/swal2-container|intercepts pointer events/i.test(error?.message || "")) {
throw error;
}
}
}
throw lastError;
}
test.describe("Edge gateway management smoke", () => {
test.describe.configure({ mode: "serial" });
@@ -193,7 +245,7 @@ test.describe("Edge gateway management smoke", () => {
await page.getByTestId("gateway-installer-department").selectOption("1");
await page.getByTestId("gateway-installer-label").fill("Copy Test Pi");
await page.getByTestId("gateway-installer-generate").click();
await generateGatewayInstaller(page);
await expect(page.getByTestId("gateway-install-command")).toHaveValue(/install\.sh\?token=edge-install-token/);
await page.getByTestId("gateway-installer-copy").click();
@@ -246,7 +298,7 @@ test.describe("Edge gateway management smoke", () => {
await page.getByTestId("gateway-installer-department").selectOption("1");
await page.getByTestId("gateway-installer-label").fill("Canary Pi");
await page.getByTestId("gateway-installer-generate").click();
await generateGatewayInstaller(page);
await expect(page.getByTestId("gateway-installer-status")).toBeVisible();
await expect.poll(() => page.getByTestId("gateway-installer-status-state").textContent()).toContain("Running");
@@ -274,7 +326,7 @@ test.describe("Edge gateway management smoke", () => {
await page.getByTestId("gateway-installer-department").selectOption("1");
await page.getByTestId("gateway-installer-label").fill("CPH Edge 01");
await page.getByTestId("gateway-installer-generate").click();
await generateGatewayInstaller(page);
await expect(page.getByTestId("gateway-installer-status")).toBeVisible();
await expect.poll(() => page.getByTestId("gateway-installer-status-state").textContent()).toContain("Running");
@@ -318,7 +370,7 @@ test.describe("Edge gateway management smoke", () => {
await page.getByTestId("gateway-installer-department").selectOption("1");
await page.getByTestId("gateway-installer-label").fill("Broken Pi");
await page.getByTestId("gateway-installer-generate").click();
await generateGatewayInstaller(page);
await expect(page.getByTestId("gateway-installer-status")).toBeVisible();
await expect
@@ -382,7 +434,7 @@ test.describe("Edge gateway management smoke", () => {
await page.getByTestId("gateway-installer-department").selectOption("1");
await page.getByTestId("gateway-installer-label").fill("Canary Pi");
await page.getByTestId("gateway-installer-generate").click();
await generateGatewayInstaller(page);
await expect(page.getByTestId("gateway-install-command")).toHaveValue(/install\.sh\?token=edge-install-token/);
await expect(page).toHaveURL(/\/superuser\/selfserve\/edge-agents\/703\/overview$/, { timeout: 20_000 });
+44 -8
View File
@@ -124,6 +124,39 @@ async function seedSavedProgress(page, overrides = {}) {
await page.addInitScript((savedProgress) => {
window.localStorage.setItem("mywash_progress_v6", JSON.stringify(savedProgress));
}, payload);
try {
await page.evaluate((savedProgress) => {
window.localStorage.setItem("mywash_progress_v6", JSON.stringify(savedProgress));
}, payload);
} catch {}
}
async function expectFinishingOrCompletedWash(page) {
const finishing = page.getByTestId("self-serve-finishing-wash");
const completed = page.getByTestId("self-serve-completed-step");
await expect
.poll(
async () => {
if (await finishing.isVisible().catch(() => false)) {
return "finishing";
}
if (await completed.isVisible().catch(() => false)) {
return "completed";
}
return "pending";
},
{
message: "expected the wash to show the finishing state or complete",
timeout: 15_000,
}
)
.toMatch(/^(finishing|completed)$/);
if (await finishing.isVisible().catch(() => false)) {
await expect(finishing).toContainText("Afslutter vask, porten åbnes automatisk");
}
}
function captureSelfServeGatewayRequests(page) {
@@ -276,9 +309,7 @@ test.describe("Self-serve wash", () => {
const stopCommandRequestPromise = waitForLaneCommandRequest(page, "STOP");
const exitGateCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_EXIT_GATE");
await page.getByTestId("self-serve-nav-complete").click();
await expect(page.getByTestId("self-serve-finishing-wash")).toContainText(
"Afslutter vask, porten åbnes automatisk"
);
await expectFinishingOrCompletedWash(page);
const stopCommandRequest = await stopCommandRequestPromise;
const exitGateCommandRequest = await exitGateCommandRequestPromise;
expect(stopCommandRequest.postDataJSON?.()).toMatchObject({
@@ -660,11 +691,18 @@ test.describe("Self-serve wash", () => {
});
await page.reload({ waitUntil: "domcontentloaded" });
await expect(page.getByTestId("self-serve-questions-step")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
await page.getByTestId("self-serve-question-21-yes").click();
await expect(page.getByTestId("self-serve-nav-confirm")).toBeEnabled({ timeout: 10_000 });
await page.getByTestId("self-serve-nav-confirm").click();
await expect(page.getByTestId("self-serve-lane-step")).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole("radio", { name: /Maskine/i })).toBeDisabled();
await expect(page.getByTestId("self-serve-lane-option-7")).toContainText("Tilgængelig");
await expect(page.getByTestId("self-serve-lane-option-8")).toContainText("Vaskebanen er ikke tilgængelig");
await expect(page.getByRole("radio", { name: /Maskine/i })).toBeEnabled();
await expect(page.getByTestId("self-serve-machine-unavailable-guidance")).toHaveCount(0);
await expect(page.locator("body")).not.toContainText(removedMachineUnavailableGuidanceText);
await expect(page.getByTestId("self-serve-nav-confirm")).toBeDisabled();
await page.getByTestId("self-serve-lane-option-7").click();
await page.getByTestId("self-serve-wash-type-manual").click();
@@ -1147,9 +1185,7 @@ test.describe("Self-serve wash", () => {
const stopCommandRequestPromise = waitForLaneCommandRequest(page, "STOP");
const exitGateCommandRequestPromise = waitForLaneCommandRequest(page, "OPEN_PROPERTY_EXIT_GATE");
await page.getByTestId("self-serve-nav-complete").click();
await expect(page.getByTestId("self-serve-finishing-wash")).toContainText(
"Afslutter vask, porten åbnes automatisk"
);
await expectFinishingOrCompletedWash(page);
const stopCommandRequest = await stopCommandRequestPromise;
const exitGateCommandRequest = await exitGateCommandRequestPromise;
@@ -0,0 +1,117 @@
import { expect, test } from "@playwright/test";
import { mockApi, seedAuthenticatedState } from "./support/network.js";
import { isDesktopProject } from "./support/projects";
const json = (body: unknown, status = 200) => ({
status,
contentType: "application/json",
body: JSON.stringify(body),
});
const now = "2026-07-06 10:00:00";
const buildProduct = (product: Record<string, unknown>) => ({
description: "E2E product",
subscription_allowed: true,
category: 1,
piktogram: "truck",
economic_product_id: 0,
apply_category_discount: false,
requires_note: false,
created_at: now,
updated_at: now,
addons: [],
is_wash: true,
display_in_booking_form: true,
order_priority: 1,
...product,
});
test.describe("Superuser department custom-only pricing", () => {
test("toggles no-fallback pricing and shows 999999 for missing department prices", async ({ page }, testInfo) => {
test.skip(!isDesktopProject(testInfo), "Desktop only");
let customPricingOnly = false;
let receivedToggleBody: Record<string, unknown> | null = null;
const products = [
buildProduct({ id: 10, name: "Fallback Wash", price: 12345 }),
buildProduct({ id: 11, name: "Explicit Wash", price: 98765, order_priority: 2 }),
];
await page.setViewportSize({ width: 1280, height: 720 });
await seedAuthenticatedState(page, "superuser-department-pricing-token");
await mockApi(page, {
authenticated: true,
permissions: [
"superuser",
"user",
"list_products",
"list_departments",
"edit_department",
"superuser_fetch_department",
"superuser_fetch_department_prices",
"superuser_set_department_prices",
],
sessionData: {
group_id: 1,
},
});
await page.route("**/api/products**", async (route) => {
await route.fulfill(json({ data: products }));
});
await page.route("**/api/superuser/department/prices**", async (route) => {
await route.fulfill(
json({
data: [{ id: 100, department_id: 42, product_id: 11, price: 2222 }],
})
);
});
await page.route(/\/api\/superuser\/department(?:\?.*)?$/i, async (route) => {
await route.fulfill(
json({
data: {
id: 42,
name: "Custom Pricing Department",
description: "E2E department",
custom_pricing_only: customPricingOnly,
created_at: now,
updated_at: now,
},
})
);
});
await page.route("**/api/departments", async (route) => {
const request = route.request();
if (request.method() !== "PUT") {
await route.fallback();
return;
}
receivedToggleBody = request.postDataJSON() as Record<string, unknown>;
customPricingOnly = Boolean(receivedToggleBody.custom_pricing_only);
await route.fulfill(json({ data: { message: "Department updated successfully" } }));
});
await page.goto("/superuser/departments/42/pricing", { waitUntil: "domcontentloaded" });
await expect(page.getByTestId("department-custom-pricing-settings")).toBeVisible();
await expect(page.getByTestId("department-price-cell-10")).toHaveText(/-/);
await expect(page.getByTestId("department-price-cell-11")).toHaveText(/2222/);
await page.locator('label[for="department-custom-pricing-only"]').click();
await expect(page.getByTestId("department-custom-pricing-only-toggle")).toBeChecked();
await expect(page.getByTestId("department-price-cell-10")).toHaveText(/999999/);
await expect(page.getByTestId("department-price-cell-11")).toHaveText(/2222/);
expect(receivedToggleBody).toMatchObject({
id: "42",
custom_pricing_only: true,
});
});
});
+1
View File
@@ -579,6 +579,7 @@ describe("MyWashStart", () => {
afterEach(() => {
consoleWarnSpy?.mockRestore();
vi.clearAllTimers();
vi.useRealTimers();
localStorage.clear();
});
@@ -40,6 +40,16 @@ describe("Playwright full E2E workflow grouping", () => {
expect(source).toContain("scripts/ci/runner-diagnostics.sh");
});
it("keeps PR E2E runner pressure bounded and diagnosable", () => {
const source = workflowSource();
expect(source).toMatch(/e2e-pr:[\s\S]*?max-parallel: 4/u);
expect(source).toMatch(/e2e-pr:[\s\S]*?PLAYWRIGHT_WORKERS: 1/u);
expect(source).toMatch(/e2e-pr:[\s\S]*?PLAYWRIGHT_VIDEO_MODE: on-first-retry/u);
expect(source).toMatch(/e2e-pr:[\s\S]*?--env PLAYWRIGHT_WORKERS="\$PLAYWRIGHT_WORKERS"/u);
expect(source).toMatch(/e2e-pr:[\s\S]*?--env PLAYWRIGHT_VIDEO_MODE="\$PLAYWRIGHT_VIDEO_MODE"/u);
});
it("allows CI to reduce Playwright video artifact pressure", () => {
const source = readFileSync(join(root, "playwright.config.ts"), "utf8");
+17 -2
View File
@@ -15,6 +15,21 @@ describe("Playwright PR mapping", () => {
);
});
it("maps superuser department pricing changes to custom-only pricing coverage", () => {
expect(specsFor("src/views/dashboards/superUserDashboard/department/DepartmentPricing.vue")).toContain(
"tests/e2e/superuser-department-pricing-custom-only.spec.ts"
);
expect(
specsFor("src/views/dashboards/superUserDashboard/department/SuperUserSelectedDepartmentObject.vue")
).toContain("tests/e2e/superuser-department-pricing-custom-only.spec.ts");
expect(specsFor("src/components/session/token/SessionUser/Objects/Departments.vue")).toContain(
"tests/e2e/superuser-department-pricing-custom-only.spec.ts"
);
expect(specsFor("src/components/session/token/SessionUser/Objects/Departments.vue")).not.toContain(
"tests/e2e/userAuth.spec.ts"
);
});
it("maps department notification table changes to the admin notification E2E coverage", () => {
expect(
specsFor("src/components/displays/department/notifications/departmentNotificationsPhoneTable.vue")
@@ -33,8 +48,8 @@ describe("Playwright PR mapping", () => {
);
});
it("keeps full-slice ownership metadata out of broad PR smoke fallback", () => {
expect(triggersFallback("scripts/run-playwright-pr.mjs")).toBe(true);
it("keeps PR runner and full-slice metadata edits out of broad PR smoke fallback", () => {
expect(triggersFallback("scripts/run-playwright-pr.mjs")).toBe(false);
expect(triggersFallback("scripts/run-playwright-ci-parallel.mjs")).toBe(true);
expect(triggersFallback("scripts/run-playwright-full-slice.mjs")).toBe(false);
});
+1
View File
@@ -95,6 +95,7 @@ afterEach(async () => {
await Promise.resolve();
await Promise.resolve();
vi.clearAllTimers();
vi.useRealTimers();
vi.clearAllMocks();
vi.unstubAllGlobals();