Extend Self-Serve Studio with "action" nodes, supporting creation, focus, and editing workflows. Add unit tests, normalize forms, and enhance virtual binding/gateway logic. Update E2E tests and node visualization settings.
This commit is contained in:
+954
-17
File diff suppressed because it is too large
Load Diff
+1
-8
@@ -5,8 +5,7 @@ import { BLoading } from "buefy";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { finished_loading, is_latest_fetch, is_loading, next_fetch_id } from "@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue";
|
||||
import { selected_date, selected_date_to } from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue";
|
||||
import { buildOutsideHoursWarningText, createEmptyOutsideHours, normalizeOutsideHours } from "@/views/dashboards/departmentDashboard/modules/daily-report/outsideHours.js";
|
||||
import { departments as loadedDepartments } from "@/components/pagination/departmentTabs.vue";
|
||||
import { createEmptyOutsideHours, normalizeOutsideHours } from "@/views/dashboards/departmentDashboard/modules/daily-report/outsideHours.js";
|
||||
|
||||
const props = defineProps({
|
||||
department_id: {
|
||||
@@ -75,12 +74,6 @@ watch([selected_date, selected_date_to], () => {
|
||||
<div class="column is-2 has-text-centered" :data-testid="`overview-department-${props.department_id}-night-washes`">
|
||||
<h3 class="title is-5" :data-testid="`overview-department-${props.department_id}-night-washes-total`">{{ outsideHours.total }}</h3>
|
||||
<h6 class="subtitle is-6">{{ $t("admin.daily_report.reports.night_washes.title") }}</h6>
|
||||
<p
|
||||
v-if="buildOutsideHoursWarningText(outsideHours, loadedDepartments, $t)"
|
||||
class="is-size-7 has-text-warning-dark"
|
||||
>
|
||||
{{ buildOutsideHoursWarningText(outsideHours, loadedDepartments, $t) }}
|
||||
</p>
|
||||
<b-loading :is-full-page="false" :modelValue="is_loading(identifier)" />
|
||||
</div>
|
||||
<div class="column is-2 has-text-centered">
|
||||
|
||||
@@ -367,6 +367,9 @@ test.describe("Admin overview night washes", () => {
|
||||
await expect(page.getByTestId("overview-department-1-night-washes-total")).toContainText("2");
|
||||
await expect(page.getByTestId("overview-department-2-night-washes-total")).toContainText("3");
|
||||
await expect(page.getByTestId("overview-department-3-night-washes-total")).toContainText("0");
|
||||
await expect(page.getByTestId("overview-department-3-night-washes")).not.toContainText(
|
||||
/mangler|Opening hours missing|Missing Hours/
|
||||
);
|
||||
|
||||
await tabs.nth(2).click();
|
||||
await expect(page.getByTestId("outside-hours-chart")).toBeVisible();
|
||||
|
||||
@@ -26,8 +26,28 @@ function buildStudioGraph() {
|
||||
},
|
||||
],
|
||||
products: [
|
||||
{ id: 2, label: "Forvogn", is_wash: 1, subscription_allowed: 1 },
|
||||
{ id: 8, label: "Bus", is_wash: 1, subscription_allowed: 1 },
|
||||
{
|
||||
id: 2,
|
||||
name: "Forvogn",
|
||||
label: "Forvogn",
|
||||
description: "Forvogn wash",
|
||||
price: 649,
|
||||
category: "6",
|
||||
piktogram: "05",
|
||||
is_wash: 1,
|
||||
subscription_allowed: 1,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Bus",
|
||||
label: "Bus",
|
||||
description: "Bus wash",
|
||||
price: 799,
|
||||
category: "8",
|
||||
piktogram: "08",
|
||||
is_wash: 1,
|
||||
subscription_allowed: 1,
|
||||
},
|
||||
],
|
||||
machine_types: [{ id: 1001, label: "Portal" }],
|
||||
dynamic_images: [
|
||||
@@ -52,6 +72,7 @@ function buildStudioGraph() {
|
||||
{ id: 41, label: "Fold mirrors" },
|
||||
{ id: 48, label: "Start bus wash" },
|
||||
],
|
||||
actions: [{ id: 81, label: "Open entry on start" }],
|
||||
gateways: [{ id: 701, label: "Roskilde Edge 01", status: "ONLINE" }],
|
||||
relays: [{ id: "M-7", label: "Machine relay" }],
|
||||
bindings: [{ id: "701:M-7:0", label: "Machine relay", role: "MACHINE", services: ["MACHINE"] }],
|
||||
@@ -66,6 +87,7 @@ function buildStudioGraph() {
|
||||
conditions: { 21: "Trailer present", 28: "Bus program allowed" },
|
||||
rules: { 31: "Mirror answer" },
|
||||
tasks: { 41: "Fold mirrors", 48: "Start bus wash" },
|
||||
actions: { 81: "Open entry on start" },
|
||||
gateways: { 701: "Roskilde Edge 01" },
|
||||
relays: { "M-7": "Machine relay" },
|
||||
bindings: { "701:M-7:0": "Machine relay" },
|
||||
@@ -83,7 +105,20 @@ function buildStudioGraph() {
|
||||
object_id: 2,
|
||||
label: "Forvogn",
|
||||
subtitle: "Vehicle scope",
|
||||
raw: { id: 2, product: 2, product_id: 2, name: "Forvogn", label: "Forvogn", source: "products" },
|
||||
raw: {
|
||||
id: 2,
|
||||
product: 2,
|
||||
product_id: 2,
|
||||
name: "Forvogn",
|
||||
label: "Forvogn",
|
||||
description: "Forvogn wash",
|
||||
price: 649,
|
||||
category: "6",
|
||||
piktogram: "05",
|
||||
is_wash: 1,
|
||||
subscription_allowed: 1,
|
||||
source: "products",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -95,7 +130,20 @@ function buildStudioGraph() {
|
||||
object_id: 8,
|
||||
label: "Bus",
|
||||
subtitle: "Vehicle scope",
|
||||
raw: { id: 8, product: 8, product_id: 8, name: "Bus", label: "Bus", source: "products" },
|
||||
raw: {
|
||||
id: 8,
|
||||
product: 8,
|
||||
product_id: 8,
|
||||
name: "Bus",
|
||||
label: "Bus",
|
||||
description: "Bus wash",
|
||||
price: 799,
|
||||
category: "8",
|
||||
piktogram: "08",
|
||||
is_wash: 1,
|
||||
subscription_allowed: 1,
|
||||
source: "products",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -202,6 +250,43 @@ function buildStudioGraph() {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "action:81",
|
||||
type: "default",
|
||||
position: { x: 1280, y: 140 },
|
||||
data: {
|
||||
kind: "action",
|
||||
object_id: 81,
|
||||
label: "Open entry on start",
|
||||
subtitle: "On self-serve wash start command / Both / Open lane entrance port",
|
||||
event_label: "On self-serve wash start command",
|
||||
action_label: "Open lane entrance port",
|
||||
relay_role: "ENTRY",
|
||||
raw: {
|
||||
id: 81,
|
||||
department: 6,
|
||||
lane: 7,
|
||||
product: 2,
|
||||
machine_type_id: 1001,
|
||||
condition_id: 21,
|
||||
name: "Open entry on start",
|
||||
description: "",
|
||||
event: "wash_start_command",
|
||||
wash_mode: "both",
|
||||
operation: "open_lane_entrance_port",
|
||||
relay_state: null,
|
||||
enabled: true,
|
||||
order_priority: 1,
|
||||
options: {
|
||||
delay_ms: 0,
|
||||
toggle_after_seconds: 2,
|
||||
retry_count: 0,
|
||||
failure_policy: "continue",
|
||||
record_event: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "condition:28",
|
||||
type: "default",
|
||||
@@ -375,6 +460,30 @@ function buildStudioGraph() {
|
||||
label: "scope",
|
||||
data: { kind: "scope" },
|
||||
},
|
||||
{
|
||||
id: "scope:lane:7:action:81",
|
||||
source: "lane:7",
|
||||
target: "action:81",
|
||||
type: "smoothstep",
|
||||
label: "scope",
|
||||
data: { kind: "scope" },
|
||||
},
|
||||
{
|
||||
id: "scope:vehicle_type:2:action:81",
|
||||
source: "vehicle_type:2",
|
||||
target: "action:81",
|
||||
type: "smoothstep",
|
||||
label: "scope",
|
||||
data: { kind: "scope" },
|
||||
},
|
||||
{
|
||||
id: "action-gate:21:81",
|
||||
source: "condition:21",
|
||||
target: "action:81",
|
||||
type: "smoothstep",
|
||||
label: "allows",
|
||||
data: { kind: "action_gate" },
|
||||
},
|
||||
{
|
||||
id: "question-gate:21:11",
|
||||
source: "condition:21",
|
||||
@@ -795,6 +904,28 @@ function buildSimulationResponse() {
|
||||
},
|
||||
{
|
||||
sequence: 2,
|
||||
runtime_stage: "start",
|
||||
signal_type: "studio_action_relay_pulse",
|
||||
relay_role: "ENTRY",
|
||||
relay_id: "IN-7",
|
||||
target_gateway_label: "Virtual Studio Gateway",
|
||||
target_binding: "binding:virtual-main:IN-7:3",
|
||||
source: "virtual",
|
||||
virtual: true,
|
||||
predicted_status: "virtual_only",
|
||||
payload: {
|
||||
action_id: 81,
|
||||
action: "Open entry on start",
|
||||
event: "wash_start_command",
|
||||
operation: "open_lane_entrance_port",
|
||||
id: "IN-7",
|
||||
on: true,
|
||||
toggle_after: 2,
|
||||
},
|
||||
skip_block_reason: "Virtual studio hardware only; live dispatch would require a real gateway binding.",
|
||||
},
|
||||
{
|
||||
sequence: 3,
|
||||
runtime_stage: "eligibility_sync",
|
||||
signal_type: "relay_switch",
|
||||
relay_role: "MACHINE",
|
||||
@@ -808,7 +939,7 @@ function buildSimulationResponse() {
|
||||
skip_block_reason: "Virtual studio hardware only; live dispatch would require a real gateway binding.",
|
||||
},
|
||||
{
|
||||
sequence: 3,
|
||||
sequence: 4,
|
||||
runtime_stage: "machine_start_signal",
|
||||
signal_type: "shelly_event",
|
||||
relay_role: "MACHINE",
|
||||
@@ -822,7 +953,7 @@ function buildSimulationResponse() {
|
||||
skip_block_reason: "Virtual studio hardware only; live dispatch would require a real gateway binding.",
|
||||
},
|
||||
{
|
||||
sequence: 4,
|
||||
sequence: 5,
|
||||
runtime_stage: "machine_start",
|
||||
signal_type: "relay_switch",
|
||||
relay_role: "CLEANER",
|
||||
@@ -834,7 +965,7 @@ function buildSimulationResponse() {
|
||||
payload: { id: "CLEAN-7", channel: 0, on: true },
|
||||
},
|
||||
{
|
||||
sequence: 5,
|
||||
sequence: 6,
|
||||
runtime_stage: "stop",
|
||||
signal_type: "relay_pulse",
|
||||
relay_role: "EXIT",
|
||||
@@ -850,6 +981,7 @@ function buildSimulationResponse() {
|
||||
nodes: {
|
||||
"question:11": { state: "warning", label: "Missing answer" },
|
||||
"task:41": { state: "blocked", label: "Gate blocked" },
|
||||
"action:81": { state: "active", label: "Action would run for this simulator event." },
|
||||
"lane:7": { state: "error", label: "Lane relay missing" },
|
||||
},
|
||||
edges: {
|
||||
@@ -1087,6 +1219,23 @@ async function installStudioRoutes(page, graph, captured) {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (operation.action === "create" && operation.entity === "action") {
|
||||
graph.nodes.push({
|
||||
id: "action:99",
|
||||
type: "default",
|
||||
position: { x: 1280, y: 320 },
|
||||
data: {
|
||||
kind: "action",
|
||||
object_id: 99,
|
||||
label: operation.data.name,
|
||||
subtitle: "On self-serve wash start command / Both / Open lane entrance port",
|
||||
event_label: "On self-serve wash start command",
|
||||
action_label: "Open lane entrance port",
|
||||
relay_role: "ENTRY",
|
||||
raw: { id: 99, ...operation.data },
|
||||
},
|
||||
});
|
||||
}
|
||||
if (operation.action === "update" && operation.entity === "question" && Number(operation.id) === 11) {
|
||||
const node = graph.nodes.find((entry) => entry.id === "question:11");
|
||||
node.data.raw = { ...node.data.raw, ...operation.data };
|
||||
@@ -1103,6 +1252,14 @@ async function installStudioRoutes(page, graph, captured) {
|
||||
node.data.raw = { ...node.data.raw, ...operation.data };
|
||||
node.data.label = operation.data.task || node.data.label;
|
||||
}
|
||||
if (operation.action === "update" && operation.entity === "action" && Number(operation.id) === 81) {
|
||||
const node = graph.nodes.find((entry) => entry.id === "action:81");
|
||||
node.data.raw = { ...node.data.raw, ...operation.data };
|
||||
node.data.label = operation.data.name || node.data.label;
|
||||
node.data.action_label =
|
||||
operation.data.operation === "set_cleaner_relay" ? "Turn OFF CLEANER" : "Open lane entrance port";
|
||||
node.data.relay_role = operation.data.operation === "set_cleaner_relay" ? "CLEANER" : "ENTRY";
|
||||
}
|
||||
if (operation.action === "update" && operation.entity === "lane") {
|
||||
const lane = graph.lookups.lanes.find((entry) => Number(entry.id) === Number(operation.id));
|
||||
if (lane) {
|
||||
@@ -1128,6 +1285,47 @@ async function installStudioRoutes(page, graph, captured) {
|
||||
await route.fulfill(json({ data: graph }));
|
||||
});
|
||||
|
||||
await page.route(/\/products(?:\?.*)?$/i, async (route, request) => {
|
||||
if (request.method() !== "PUT") {
|
||||
await route.fulfill(json({ data: graph.lookups.products }));
|
||||
return;
|
||||
}
|
||||
const body = request.postDataJSON?.() || {};
|
||||
captured.productUpdates ||= [];
|
||||
captured.productUpdates.push(body);
|
||||
const product = graph.lookups.products.find((entry) => Number(entry.id) === Number(body.id));
|
||||
if (product) {
|
||||
Object.assign(product, body, {
|
||||
label: body.name || product.label,
|
||||
name: body.name || product.name,
|
||||
});
|
||||
graph.lookups.labels.products[String(body.id)] = product.label;
|
||||
graph.lookups.labels.vehicle_types[String(body.id)] = product.label;
|
||||
}
|
||||
const vehicleType = graph.lookups.vehicle_types.find(
|
||||
(entry) => Number(entry.product || entry.id) === Number(body.id)
|
||||
);
|
||||
if (vehicleType) {
|
||||
Object.assign(vehicleType, body, {
|
||||
product: body.id,
|
||||
product_id: body.id,
|
||||
label: body.name || vehicleType.label,
|
||||
});
|
||||
}
|
||||
const node = graph.nodes.find((entry) => entry.id === `vehicle_type:${body.id}`);
|
||||
if (node) {
|
||||
node.data.label = body.name || node.data.label;
|
||||
node.data.raw = {
|
||||
...node.data.raw,
|
||||
...body,
|
||||
product: body.id,
|
||||
product_id: body.id,
|
||||
label: body.name || node.data.raw?.label,
|
||||
};
|
||||
}
|
||||
await route.fulfill(json({ data: product || body }));
|
||||
});
|
||||
|
||||
await page.route(/\/department\/selfserve\/studio\/layout(?:\?.*)?$/i, async (route, request) => {
|
||||
captured.layouts.push(request.postDataJSON?.() || {});
|
||||
await route.fulfill(json({ data: { nodes: [], viewport: [], runtime_affecting: false } }));
|
||||
@@ -1212,6 +1410,7 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
dynamicImages: [],
|
||||
gatewayActions: [],
|
||||
virtualHardware: [],
|
||||
productUpdates: [],
|
||||
rollbacks: [],
|
||||
published: 0,
|
||||
};
|
||||
@@ -1230,6 +1429,11 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
await expect(page.getByTestId("studio-custom-node-rule")).toHaveCount(0);
|
||||
await expect(page.getByTestId("studio-custom-node-task").first()).toContainText("services");
|
||||
await expect(page.getByTestId("studio-custom-node-task").first()).toContainText("MACHINE");
|
||||
await expect(page.getByTestId("studio-palette-action")).toBeVisible();
|
||||
await expect(page.getByTestId("studio-custom-node-action").first()).toContainText(
|
||||
"On self-serve wash start command"
|
||||
);
|
||||
await expect(page.getByTestId("studio-custom-node-action").first()).toContainText("Open lane entrance port");
|
||||
await expect(page.getByTestId("studio-custom-node-gateway").first()).toContainText("ONLINE");
|
||||
await expect(page.getByTestId("studio-custom-node-binding").first()).toContainText("MACHINE");
|
||||
await expect(page.getByTestId("studio-custom-node-scope").first()).toContainText("Forvogn");
|
||||
@@ -1325,8 +1529,8 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
await expect(page.getByText("Start bus wash").first()).toBeVisible();
|
||||
await expect(page.getByText("Are mirrors folded?").first()).not.toBeVisible();
|
||||
await page.getByTestId("studio-panel-tasks").click();
|
||||
await expect(page.locator(".studio-task-list")).toContainText("Start bus wash");
|
||||
await expect(page.locator(".studio-task-list")).not.toContainText("Fold mirrors");
|
||||
await expect(page.getByTestId("studio-task-list")).toContainText("Start bus wash");
|
||||
await expect(page.getByTestId("studio-task-list")).not.toContainText("Fold mirrors");
|
||||
await page.getByTestId("studio-panel-flow").click();
|
||||
await expect
|
||||
.poll(async () =>
|
||||
@@ -1364,6 +1568,16 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
await page.getByTestId("studio-filter-lane").selectOption("");
|
||||
await expect(page.getByText("Are mirrors folded?").first()).toBeVisible();
|
||||
|
||||
await page.locator(".studio-node-list button").filter({ hasText: "Forvogn" }).click();
|
||||
await expect(page.getByTestId("studio-managed-inspector")).toBeVisible();
|
||||
await expect(page.getByTestId("studio-vehicle-type-form")).toBeVisible();
|
||||
await expect(page.getByTestId("studio-vehicle-type-price")).toHaveValue("649");
|
||||
await page.getByTestId("studio-vehicle-type-name").fill("Forvogn XL");
|
||||
await page.getByRole("button", { name: "Save vehicle type" }).click();
|
||||
await expect.poll(() => captured.productUpdates.at(-1)?.name).toBe("Forvogn XL");
|
||||
await expect(page.locator('.vue-flow__node[data-id="vehicle_type:2"]')).toContainText("Forvogn XL");
|
||||
await page.getByTestId("studio-panel-flow").click();
|
||||
|
||||
await page.locator(".studio-node-list button").filter({ hasText: "Fold mirrors" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Inspector" })).toBeVisible();
|
||||
await expect(page.getByTestId("studio-service-picker")).toContainText("MACHINE");
|
||||
@@ -1387,11 +1601,32 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
.toBe(true);
|
||||
await expect(page.locator('.vue-flow__node[data-id="task:41"]')).toContainText("No service");
|
||||
|
||||
await page.getByTestId("studio-panel-tasks").click();
|
||||
await expect(page.locator(".studio-task-row").filter({ hasText: "Open entry on start" })).toContainText(
|
||||
"On self-serve wash start command"
|
||||
);
|
||||
await page.locator(".studio-task-row").filter({ hasText: "Open entry on start" }).getByRole("button").click();
|
||||
await expect(page.getByTestId("studio-inspector-action-event")).toHaveValue("wash_start_command");
|
||||
await page.getByTestId("studio-inspector-action-operation").selectOption("set_cleaner_relay");
|
||||
await page.getByRole("button", { name: "OFF" }).click();
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect
|
||||
.poll(() =>
|
||||
captured.graphSaves.some(
|
||||
(save) =>
|
||||
save.operations?.[0]?.entity === "action" &&
|
||||
save.operations?.[0]?.data?.operation === "set_cleaner_relay" &&
|
||||
save.operations?.[0]?.data?.relay_state === false &&
|
||||
save.operations?.[0]?.data?.options?.failure_policy === "continue"
|
||||
)
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
await page.getByTestId("studio-panel-conditions").click();
|
||||
await expect(page.locator(".studio-condition-card").filter({ hasText: "Trailer present" })).toContainText(
|
||||
"Are mirrors folded?"
|
||||
);
|
||||
await expect(page.locator(".studio-condition-card").filter({ hasText: "Trailer present" })).toContainText("1 uses");
|
||||
await expect(page.locator(".studio-condition-card").filter({ hasText: "Trailer present" })).toContainText("2 uses");
|
||||
await page.locator(".studio-condition-card").filter({ hasText: "Trailer present" }).click();
|
||||
await expect(page.getByTestId("condition-expression-builder")).toBeVisible();
|
||||
await expect(page.getByTestId("condition-expression-row").first()).toContainText("Are mirrors folded?");
|
||||
@@ -1459,6 +1694,44 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
await expect.poll(() => captured.graphSaves.some((save) => save.operations?.[0]?.action === "create")).toBe(true);
|
||||
await expect(page.getByText("New question").first()).toBeVisible();
|
||||
|
||||
await page.getByTestId("studio-filter-vehicle-type").selectOption({ label: "Bus" });
|
||||
await page.getByTestId("studio-palette-action").click();
|
||||
await expect
|
||||
.poll(() =>
|
||||
captured.graphSaves.some(
|
||||
(save) =>
|
||||
save.operations?.[0]?.action === "create" &&
|
||||
save.operations?.[0]?.entity === "action" &&
|
||||
save.operations?.[0]?.data?.product === 8
|
||||
)
|
||||
)
|
||||
.toBe(true);
|
||||
await expect(page.getByTestId("studio-inspector-action-name")).toHaveValue("Open lane entrance port");
|
||||
await expect(page.getByTestId("studio-inspector-action-event")).toBeVisible();
|
||||
await expect(page.locator('.vue-flow__node[data-id="action:99"]')).toHaveClass(/selected/);
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate(() => {
|
||||
const node = document.querySelector('.vue-flow__node[data-id="action:99"]');
|
||||
const surface = document.querySelector('[data-testid="self-serve-studio-flow"]');
|
||||
if (!node || !surface) {
|
||||
return false;
|
||||
}
|
||||
const nodeRect = node.getBoundingClientRect();
|
||||
const surfaceRect = surface.getBoundingClientRect();
|
||||
const nodeCenterX = nodeRect.left + nodeRect.width / 2;
|
||||
const nodeCenterY = nodeRect.top + nodeRect.height / 2;
|
||||
const surfaceCenterX = surfaceRect.left + surfaceRect.width / 2;
|
||||
const surfaceCenterY = surfaceRect.top + surfaceRect.height / 2;
|
||||
return (
|
||||
Math.abs(nodeCenterX - surfaceCenterX) < surfaceRect.width * 0.3 &&
|
||||
Math.abs(nodeCenterY - surfaceCenterY) < surfaceRect.height * 0.3
|
||||
);
|
||||
})
|
||||
)
|
||||
.toBe(true);
|
||||
await page.getByTestId("studio-filter-vehicle-type").selectOption("");
|
||||
|
||||
await page.getByTestId("studio-panel-hardware").click();
|
||||
await expect(page.getByTestId("studio-service-coverage")).toContainText("MACHINE");
|
||||
await expect(page.getByTestId("studio-service-coverage")).toContainText("Fold mirrors");
|
||||
@@ -1468,7 +1741,20 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
await expect(page.getByTestId("studio-virtual-hardware")).toContainText("3 virtual bindings active");
|
||||
await expect(page.getByText("Virtual Studio Gateway").first()).toBeVisible();
|
||||
await expect(page.getByTestId("studio-service-coverage")).toContainText("Virtual coverage");
|
||||
await page.getByTitle("Discover gateway").click();
|
||||
await page.getByTestId("studio-panel-flow").click();
|
||||
await page.getByTitle("Fit view").click();
|
||||
await page.locator(".studio-node-list button").filter({ hasText: "Lane 7 MACHINE" }).click();
|
||||
await expect(page.getByTestId("studio-binding-inspector")).toBeVisible();
|
||||
await expect(page.getByTestId("studio-binding-services")).toHaveValue("MACHINE");
|
||||
await page.getByTestId("studio-binding-label").fill("Lane 7 MACHINE updated");
|
||||
await page.getByRole("button", { name: "Save binding" }).click();
|
||||
await expect.poll(() => captured.virtualHardware.at(-1)?.operation).toBe("upsert_binding");
|
||||
await expect.poll(() => captured.virtualHardware.at(-1)?.data?.label).toBe("Lane 7 MACHINE updated");
|
||||
await page.getByTestId("studio-panel-flow").click();
|
||||
await page.locator(".studio-node-list button").filter({ hasText: "Roskilde Edge 01" }).click();
|
||||
await expect(page.getByTestId("studio-managed-inspector")).toContainText("Roskilde Edge 01");
|
||||
await expect(page.getByTestId("studio-gateway-inspector")).toContainText("Bindings");
|
||||
await page.getByRole("button", { name: "Discover" }).click();
|
||||
await expect.poll(() => captured.gatewayActions[0]?.action).toBe("discovery");
|
||||
|
||||
await page.getByTestId("studio-filter-lane").selectOption({ label: "Lane 7" });
|
||||
@@ -1520,6 +1806,9 @@ test.describe("All-in-one self-serve studio", () => {
|
||||
await expect(page.getByText("Tasks and Services")).toBeVisible();
|
||||
await expect(page.getByText("Gateway Readiness")).toBeVisible();
|
||||
await expect(page.getByTestId("studio-signal-timeline")).toContainText("Signals");
|
||||
await expect(page.getByTestId("studio-signal-timeline")).toContainText("start / ENTRY");
|
||||
await expect(page.getByTestId("studio-signal-timeline")).toContainText("studio_action_relay_pulse");
|
||||
await expect(page.getByTestId("studio-signal-timeline")).toContainText('"action_id":81');
|
||||
await expect(page.getByTestId("studio-signal-timeline")).toContainText("eligibility_sync / MACHINE");
|
||||
await expect(page.getByTestId("studio-signal-timeline")).toContainText("machine_start_signal / MACHINE");
|
||||
await expect(page.getByTestId("studio-signal-timeline")).toContainText('"bill_machine_wash":true');
|
||||
|
||||
@@ -208,4 +208,54 @@ describe("Department overview period sync behavior", () => {
|
||||
);
|
||||
expect(rangeCalls).toEqual(expect.arrayContaining(["10:2026-03-23:2026-03-24", "20:2026-03-23:2026-03-24"]));
|
||||
});
|
||||
|
||||
it("does not show missing-opening-hours labels in compact department rows", async () => {
|
||||
getTransactionCountMock.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: {
|
||||
earnings: 10,
|
||||
products: 2,
|
||||
quantity: 3,
|
||||
washes: 4,
|
||||
outside_hours: {
|
||||
total: 1,
|
||||
by_source: {
|
||||
orders: 0,
|
||||
xlvask: 1,
|
||||
selfserve: 0,
|
||||
},
|
||||
has_missing_opening_hours: true,
|
||||
missing_department_ids: [20],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(DepartmentDailyReportSmall, {
|
||||
props: {
|
||||
department_id: 20,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key, values = {}) => {
|
||||
if (key === "admin.daily_report.reports.night_washes.warning_missing_opening_hours") {
|
||||
return `Opening hours missing for: ${values.departments}`;
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
BLoading: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await flushAll();
|
||||
await flushAll();
|
||||
|
||||
expect(wrapper.text()).toContain("Dept 20");
|
||||
expect(wrapper.text()).toContain("1");
|
||||
expect(wrapper.text()).not.toContain("Opening hours missing for:");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
|
||||
|
||||
describe("self-serve studio create focus", () => {
|
||||
const studioSource = () =>
|
||||
readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
it("selects and focuses a newly created graph node after save hydration", () => {
|
||||
const source = studioSource();
|
||||
|
||||
expect(source).toContain("const setSelectedNode = (nodeId, openInspector = true) => {");
|
||||
expect(source).toContain("const findCreatedNode = (graph, kind, previousNodeIds) => {");
|
||||
expect(source).toContain("const focusCreatedNode = async (nodeId) => {");
|
||||
expect(source).toContain("const previousNodeIds = new Set(flowNodes.value.map((node) => node.id));");
|
||||
expect(source).toContain("const createdNode = findCreatedNode(graph, kind, previousNodeIds);");
|
||||
expect(source).toContain("await focusCreatedNode(createdNode.id);");
|
||||
expect(source).toContain('activePanel.value = "inspector";');
|
||||
expect(source).toContain("flowNodes.value = flowNodes.value.map((node) => {");
|
||||
expect(source).toContain("nodes: [nodeId]");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
|
||||
|
||||
describe("self-serve studio managed inspector", () => {
|
||||
const studioSource = () =>
|
||||
readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
it("routes non-graph self-serve nodes to managed inspector panels", () => {
|
||||
const source = studioSource();
|
||||
|
||||
expect(source).toContain(
|
||||
'const managedInspectorNodeKinds = ["runtime_checkpoint", "vehicle_type", "machine_type", "edge_gateway", "relay_binding", "relay"];'
|
||||
);
|
||||
expect(source).toContain('v-else-if="isSelectedManaged"');
|
||||
expect(source).toContain('data-testid="studio-managed-inspector"');
|
||||
expect(source).toContain('data-testid="studio-runtime-inspector"');
|
||||
expect(source).toContain('data-testid="studio-gateway-inspector"');
|
||||
expect(source).toContain('data-testid="studio-binding-inspector"');
|
||||
expect(source).toContain('data-testid="studio-relay-inspector"');
|
||||
});
|
||||
|
||||
it("persists vehicle and machine type configuration through their owning endpoints", () => {
|
||||
const source = studioSource();
|
||||
|
||||
expect(source).toContain("const normalizeVehicleTypeForm = (raw = {}) => {");
|
||||
expect(source).toContain("const saveVehicleTypeProduct = async () => {");
|
||||
expect(source).toContain('await requestPut("/products", {');
|
||||
expect(source).toContain('data-testid="studio-vehicle-type-form"');
|
||||
expect(source).toContain('data-testid="studio-vehicle-type-name"');
|
||||
expect(source).toContain("const normalizeMachineTypeForm = (raw = {}) => {");
|
||||
expect(source).toContain("const saveMachineType = async () => {");
|
||||
expect(source).toContain('await requestPut("/department/selfserve/machine-types", {');
|
||||
});
|
||||
|
||||
it("manages virtual hardware from selected gateway and relay binding nodes", () => {
|
||||
const source = studioSource();
|
||||
|
||||
expect(source).toContain("const saveSelectedVirtualGateway = () => {");
|
||||
expect(source).toContain('return applyVirtualHardwareOperation("upsert_gateway", {');
|
||||
expect(source).toContain("const saveSelectedVirtualBinding = () => {");
|
||||
expect(source).toContain('return applyVirtualHardwareOperation("upsert_binding", payload);');
|
||||
expect(source).toContain("const deleteSelectedVirtualBinding = () => deleteVirtualBinding({");
|
||||
expect(source).toContain("const selectedGatewayBindingEntries = computed(() => {");
|
||||
expect(source).toContain("const selectedRelayBindingEntries = computed(() => {");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user