712 lines
25 KiB
JavaScript
712 lines
25 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, primeMockSession } from "./support/network.js";
|
|
|
|
const json = (body, status = 200) => ({
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
function buildStudioGraph() {
|
|
const lookups = {
|
|
departments: [{ id: 6, label: "Roskilde" }],
|
|
lanes: [{ id: 7, label: "Lane 7" }],
|
|
products: [
|
|
{ id: 2, label: "Forvogn", is_wash: 1, subscription_allowed: 1 },
|
|
{ id: 8, label: "Bus", is_wash: 1, subscription_allowed: 1 },
|
|
],
|
|
machine_types: [{ id: 1001, label: "Portal" }],
|
|
vehicle_types: [
|
|
{ id: 2, product: 2, product_id: 2, label: "Forvogn", source: "products" },
|
|
{ id: 8, product: 8, product_id: 8, label: "Bus", source: "products" },
|
|
],
|
|
questions: [
|
|
{ id: 11, label: "Are mirrors folded?" },
|
|
{ id: 12, label: "Is the lift lowered?" },
|
|
{ id: 18, label: "Bus roof fixtures checked?" },
|
|
],
|
|
conditions: [
|
|
{ id: 21, label: "Trailer present" },
|
|
{ id: 28, label: "Bus program allowed" },
|
|
],
|
|
rules: [{ id: 31, label: "Mirror answer" }],
|
|
tasks: [
|
|
{ id: 41, label: "Fold mirrors" },
|
|
{ id: 48, label: "Start bus wash" },
|
|
],
|
|
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"] }],
|
|
labels: {
|
|
departments: { 6: "Roskilde" },
|
|
lanes: { 7: "Lane 7" },
|
|
products: { 2: "Forvogn", 8: "Bus" },
|
|
machine_types: { 1001: "Portal" },
|
|
vehicle_types: { 2: "Forvogn", 8: "Bus" },
|
|
questions: { 11: "Are mirrors folded?", 12: "Is the lift lowered?", 18: "Bus roof fixtures checked?" },
|
|
conditions: { 21: "Trailer present", 28: "Bus program allowed" },
|
|
rules: { 31: "Mirror answer" },
|
|
tasks: { 41: "Fold mirrors", 48: "Start bus wash" },
|
|
gateways: { 701: "Roskilde Edge 01" },
|
|
relays: { "M-7": "Machine relay" },
|
|
bindings: { "701:M-7:0": "Machine relay" },
|
|
},
|
|
};
|
|
|
|
return {
|
|
nodes: [
|
|
{
|
|
id: "vehicle_type:2",
|
|
type: "default",
|
|
position: { x: 0, y: 140 },
|
|
data: {
|
|
kind: "vehicle_type",
|
|
object_id: 2,
|
|
label: "Forvogn",
|
|
subtitle: "Vehicle scope",
|
|
raw: { id: 2, product: 2, product_id: 2, name: "Forvogn", label: "Forvogn", source: "products" },
|
|
},
|
|
},
|
|
{
|
|
id: "vehicle_type:8",
|
|
type: "default",
|
|
position: { x: 0, y: 330 },
|
|
data: {
|
|
kind: "vehicle_type",
|
|
object_id: 8,
|
|
label: "Bus",
|
|
subtitle: "Vehicle scope",
|
|
raw: { id: 8, product: 8, product_id: 8, name: "Bus", label: "Bus", source: "products" },
|
|
},
|
|
},
|
|
{
|
|
id: "condition:21",
|
|
type: "default",
|
|
position: { x: 320, y: 140 },
|
|
data: {
|
|
kind: "condition",
|
|
object_id: 21,
|
|
label: "Trailer present",
|
|
subtitle: "Lane 7 / Forvogn",
|
|
raw: { id: 21, department: 6, lane: 7, product: 2, name: "Trailer present", description: "" },
|
|
},
|
|
},
|
|
{
|
|
id: "question:11",
|
|
type: "default",
|
|
position: { x: 640, y: 140 },
|
|
data: {
|
|
kind: "question",
|
|
object_id: 11,
|
|
label: "Are mirrors folded?",
|
|
subtitle: "Lane 7 / Forvogn",
|
|
raw: {
|
|
id: 11,
|
|
department: 6,
|
|
lane: 7,
|
|
product: 2,
|
|
condition_id: 21,
|
|
question: "Are mirrors folded?",
|
|
description: "",
|
|
order_priority: 1,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "question:12",
|
|
type: "default",
|
|
position: { x: 642, y: 150 },
|
|
data: {
|
|
kind: "question",
|
|
object_id: 12,
|
|
label: "Is the lift lowered?",
|
|
subtitle: "Lane 7 / Forvogn",
|
|
raw: {
|
|
id: 12,
|
|
department: 6,
|
|
lane: 7,
|
|
product: 2,
|
|
condition_id: null,
|
|
question: "Is the lift lowered?",
|
|
description: "",
|
|
order_priority: 2,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "task:41",
|
|
type: "default",
|
|
position: { x: 960, y: 140 },
|
|
data: {
|
|
kind: "task",
|
|
object_id: 41,
|
|
label: "Fold mirrors",
|
|
subtitle: "Lane 7 / Forvogn",
|
|
raw: {
|
|
id: 41,
|
|
department: 6,
|
|
lane: 7,
|
|
product: 2,
|
|
task: "Fold mirrors",
|
|
description: "",
|
|
gate_type: "QUESTION",
|
|
gate_ref_id: 11,
|
|
order_priority: 1,
|
|
services: ["MACHINE"],
|
|
buttons: [1],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "condition:28",
|
|
type: "default",
|
|
position: { x: 1880, y: 900 },
|
|
data: {
|
|
kind: "condition",
|
|
object_id: 28,
|
|
label: "Bus program allowed",
|
|
subtitle: "Lane 7 / Bus",
|
|
raw: { id: 28, department: 6, lane: 7, product: 8, name: "Bus program allowed", description: "" },
|
|
},
|
|
},
|
|
{
|
|
id: "question:18",
|
|
type: "default",
|
|
position: { x: 2440, y: 1380 },
|
|
data: {
|
|
kind: "question",
|
|
object_id: 18,
|
|
label: "Bus roof fixtures checked?",
|
|
subtitle: "Lane 7 / Bus",
|
|
raw: {
|
|
id: 18,
|
|
department: 6,
|
|
lane: 7,
|
|
product: 8,
|
|
condition_id: 28,
|
|
question: "Bus roof fixtures checked?",
|
|
description: "",
|
|
order_priority: 3,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "task:48",
|
|
type: "default",
|
|
position: { x: 3600, y: 1840 },
|
|
data: {
|
|
kind: "task",
|
|
object_id: 48,
|
|
label: "Start bus wash",
|
|
subtitle: "Lane 7 / Bus",
|
|
raw: {
|
|
id: 48,
|
|
department: 6,
|
|
lane: 7,
|
|
product: 8,
|
|
task: "Start bus wash",
|
|
description: "",
|
|
gate_type: "QUESTION",
|
|
gate_ref_id: 18,
|
|
order_priority: 2,
|
|
services: ["MACHINE"],
|
|
buttons: [2],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "rule:31",
|
|
type: "default",
|
|
position: { x: 520, y: 320 },
|
|
data: {
|
|
kind: "rule",
|
|
object_id: 31,
|
|
label: "Mirror answer",
|
|
subtitle: "IS_TRUE Are mirrors folded?",
|
|
raw: {
|
|
id: 31,
|
|
condition_id: 21,
|
|
type: "IS_TRUE",
|
|
object_type: "question",
|
|
object_id: 11,
|
|
name: "Mirror answer",
|
|
description: "",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: "gateway:701",
|
|
type: "default",
|
|
position: { x: 1280, y: 140 },
|
|
data: {
|
|
kind: "edge_gateway",
|
|
object_id: 701,
|
|
label: "Roskilde Edge 01",
|
|
subtitle: "ONLINE",
|
|
raw: { id: 701, label: "Roskilde Edge 01", status: "ONLINE" },
|
|
},
|
|
},
|
|
{
|
|
id: "binding:701:M-7:0",
|
|
type: "default",
|
|
position: { x: 1600, y: 140 },
|
|
data: {
|
|
kind: "relay_binding",
|
|
object_id: "M-7",
|
|
label: "Machine relay",
|
|
subtitle: "MACHINE",
|
|
raw: { relay_id: "M-7", label: "Machine relay", role: "MACHINE", services: ["MACHINE"], channel: 0 },
|
|
},
|
|
},
|
|
{
|
|
id: "relay:M-7",
|
|
type: "default",
|
|
position: { x: 1920, y: 140 },
|
|
data: {
|
|
kind: "relay",
|
|
object_id: "M-7",
|
|
label: "Machine relay",
|
|
subtitle: "Hardware relay",
|
|
raw: { relay_id: "M-7", name: "Machine relay", status: "ONLINE" },
|
|
},
|
|
},
|
|
],
|
|
edges: [
|
|
{
|
|
id: "scope:vehicle_type:2:condition:21",
|
|
source: "vehicle_type:2",
|
|
target: "condition:21",
|
|
type: "smoothstep",
|
|
label: "scope",
|
|
data: { kind: "scope" },
|
|
},
|
|
{
|
|
id: "scope:vehicle_type:2:question:11",
|
|
source: "vehicle_type:2",
|
|
target: "question:11",
|
|
type: "smoothstep",
|
|
label: "scope",
|
|
data: { kind: "scope" },
|
|
},
|
|
{
|
|
id: "scope:vehicle_type:2:question:12",
|
|
source: "vehicle_type:2",
|
|
target: "question:12",
|
|
type: "smoothstep",
|
|
label: "scope",
|
|
data: { kind: "scope" },
|
|
},
|
|
{
|
|
id: "scope:vehicle_type:2:task:41",
|
|
source: "vehicle_type:2",
|
|
target: "task:41",
|
|
type: "smoothstep",
|
|
label: "scope",
|
|
data: { kind: "scope" },
|
|
},
|
|
{
|
|
id: "question-gate:21:11",
|
|
source: "condition:21",
|
|
target: "question:11",
|
|
type: "smoothstep",
|
|
label: "show if",
|
|
data: { kind: "visibility_gate" },
|
|
},
|
|
{
|
|
id: "rule-input:question:11:31",
|
|
source: "question:11",
|
|
target: "rule:31",
|
|
type: "smoothstep",
|
|
label: "IS_TRUE",
|
|
data: { kind: "rule_input" },
|
|
},
|
|
{
|
|
id: "rule-owner:21:31",
|
|
source: "rule:31",
|
|
target: "condition:21",
|
|
type: "smoothstep",
|
|
label: "rule of",
|
|
data: { kind: "condition_rule" },
|
|
},
|
|
{
|
|
id: "task-gate:question:11:41",
|
|
source: "question:11",
|
|
target: "task:41",
|
|
type: "smoothstep",
|
|
label: "unlocks",
|
|
data: { kind: "task_gate" },
|
|
},
|
|
{
|
|
id: "scope:vehicle_type:8:condition:28",
|
|
source: "vehicle_type:8",
|
|
target: "condition:28",
|
|
type: "smoothstep",
|
|
label: "scope",
|
|
data: { kind: "scope" },
|
|
},
|
|
{
|
|
id: "scope:vehicle_type:8:question:18",
|
|
source: "vehicle_type:8",
|
|
target: "question:18",
|
|
type: "smoothstep",
|
|
label: "scope",
|
|
data: { kind: "scope" },
|
|
},
|
|
{
|
|
id: "scope:vehicle_type:8:task:48",
|
|
source: "vehicle_type:8",
|
|
target: "task:48",
|
|
type: "smoothstep",
|
|
label: "scope",
|
|
data: { kind: "scope" },
|
|
},
|
|
{
|
|
id: "question-gate:28:18",
|
|
source: "condition:28",
|
|
target: "question:18",
|
|
type: "smoothstep",
|
|
label: "show if",
|
|
data: { kind: "visibility_gate" },
|
|
},
|
|
{
|
|
id: "task-gate:question:18:48",
|
|
source: "question:18",
|
|
target: "task:48",
|
|
type: "smoothstep",
|
|
label: "unlocks",
|
|
data: { kind: "task_gate" },
|
|
},
|
|
{
|
|
id: "gateway-binding:701:M-7:0",
|
|
source: "gateway:701",
|
|
target: "binding:701:M-7:0",
|
|
type: "smoothstep",
|
|
label: "binds",
|
|
data: { kind: "gateway_binding" },
|
|
},
|
|
{
|
|
id: "binding-relay:701:M-7:0",
|
|
source: "binding:701:M-7:0",
|
|
target: "relay:M-7",
|
|
type: "smoothstep",
|
|
label: "controls",
|
|
data: { kind: "relay_binding" },
|
|
},
|
|
{
|
|
id: "task-service:41:MACHINE:701:M-7:0",
|
|
source: "task:41",
|
|
target: "binding:701:M-7:0",
|
|
type: "smoothstep",
|
|
label: "MACHINE",
|
|
data: { kind: "task_service" },
|
|
},
|
|
{
|
|
id: "task-service:48:MACHINE:701:M-7:0",
|
|
source: "task:48",
|
|
target: "binding:701:M-7:0",
|
|
type: "smoothstep",
|
|
label: "MACHINE",
|
|
data: { kind: "task_service" },
|
|
},
|
|
],
|
|
lookups,
|
|
validation: { valid: true, errors: [], warnings: [], items: [] },
|
|
layout: { nodes: [], viewport: [], runtime_affecting: false },
|
|
versions: [
|
|
{ id: 90, version_number: 3, status: "DRAFT", created_at: "2026-04-28 09:00:00" },
|
|
{ id: 80, version_number: 2, status: "PUBLISHED", published_at: "2026-04-27 09:00:00" },
|
|
],
|
|
active_config: null,
|
|
draft: { id: 90, status: "DRAFT", version_number: 3 },
|
|
simulator_defaults: { department: 6, lane_id: 7, vehicle_type_id: 2, reg: "TEST123", customer_number: null },
|
|
gateway_workspace: {
|
|
gateways: [
|
|
{
|
|
id: 701,
|
|
label: "Roskilde Edge 01",
|
|
status: "ONLINE",
|
|
bindings: [{ relay_id: "M-7", label: "Machine relay", role: "MACHINE", services: ["MACHINE"], channel: 0 }],
|
|
},
|
|
],
|
|
relays: [{ relay_id: "M-7", name: "Machine relay" }],
|
|
lanes: [],
|
|
issues: [],
|
|
actions: [],
|
|
},
|
|
permissions: {
|
|
can_view: true,
|
|
can_edit: true,
|
|
can_publish: true,
|
|
can_rollback: true,
|
|
can_simulate: true,
|
|
modules_shelly_config: true,
|
|
can_manage_gateways: true,
|
|
can_run_gateway_destructive_actions: true,
|
|
can_run_live_lane_actions: true,
|
|
},
|
|
meta: { department_id: 6, layout_affects_runtime: false, generated_at: "2026-04-28T09:00:00+02:00" },
|
|
};
|
|
}
|
|
|
|
async function installStudioRoutes(page, graph, captured) {
|
|
await page.route(/\/department\/selfserve\/studio\/graph(?:\?.*)?$/i, async (route, request) => {
|
|
if (request.method() === "GET") {
|
|
await route.fulfill(json({ data: graph }));
|
|
return;
|
|
}
|
|
|
|
const body = request.postDataJSON?.() || {};
|
|
captured.graphSaves.push(body);
|
|
for (const operation of body.operations || []) {
|
|
if (operation.action === "create" && operation.entity === "question") {
|
|
graph.nodes.push({
|
|
id: "question:99",
|
|
type: "default",
|
|
position: { x: 700, y: 320 },
|
|
data: {
|
|
kind: "question",
|
|
object_id: 99,
|
|
label: operation.data.question,
|
|
subtitle: "Lane 7 / Forvogn",
|
|
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 };
|
|
node.data.label = operation.data.question;
|
|
}
|
|
if (operation.action === "update" && operation.entity === "task" && Number(operation.id) === 41) {
|
|
const node = graph.nodes.find((entry) => entry.id === "task:41");
|
|
node.data.raw = { ...node.data.raw, ...operation.data };
|
|
node.data.label = operation.data.task || node.data.label;
|
|
}
|
|
}
|
|
await route.fulfill(json({ data: graph }));
|
|
});
|
|
|
|
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 } }));
|
|
});
|
|
|
|
await page.route(/\/department\/selfserve\/studio\/validate(?:\?.*)?$/i, async (route) => {
|
|
await route.fulfill(json({ data: graph.validation }));
|
|
});
|
|
|
|
await page.route(/\/department\/selfserve\/studio\/simulate(?:\?.*)?$/i, async (route, request) => {
|
|
captured.simulations.push(request.postDataJSON?.() || {});
|
|
await route.fulfill(json({ data: { allowed: true, questions: [], tasks: [{ task: "Fold mirrors" }] } }));
|
|
});
|
|
|
|
await page.route(/\/department\/selfserve\/studio\/publish(?:\?.*)?$/i, async (route) => {
|
|
captured.published += 1;
|
|
await route.fulfill(json({ data: { id: 91, status: "PUBLISHED", version_number: 4 } }));
|
|
});
|
|
|
|
await page.route(/\/department\/selfserve\/studio\/rollback(?:\?.*)?$/i, async (route, request) => {
|
|
captured.rollbacks.push(request.postDataJSON?.() || {});
|
|
await route.fulfill(json({ data: { id: 92, status: "PUBLISHED", version_number: 5 } }));
|
|
});
|
|
|
|
await page.route(/\/department\/selfserve\/studio\/gateway-action(?:\?.*)?$/i, async (route, request) => {
|
|
captured.gatewayActions.push(request.postDataJSON?.() || {});
|
|
await route.fulfill(json({ data: { action: "discovery", operation: { id: 1, status: "PENDING" } } }));
|
|
});
|
|
}
|
|
|
|
test.describe("All-in-one self-serve studio", () => {
|
|
test.describe.configure({ mode: "serial", timeout: 120_000 });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
selfServe: true,
|
|
});
|
|
await primeMockSession(page, { token: "self-serve-studio-token" });
|
|
});
|
|
|
|
test("loads the replacement graph workspace and edits a question from the inspector", async ({ page }) => {
|
|
const graph = buildStudioGraph();
|
|
const captured = { graphSaves: [], layouts: [], simulations: [], gatewayActions: [], rollbacks: [], published: 0 };
|
|
await installStudioRoutes(page, graph, captured);
|
|
|
|
await page.goto("/admin/6/modules/self-serve/studio", { waitUntil: "domcontentloaded", timeout: 90_000 });
|
|
|
|
await expect(page.getByTestId("self-serve-studio")).toBeVisible();
|
|
await expect(page.getByTestId("self-serve-studio-flow")).toBeVisible();
|
|
await expect(page.getByTestId("studio-flow-actions")).toBeVisible();
|
|
await expect(page.getByText("Are mirrors folded?").first()).toBeVisible();
|
|
await expect(page.getByText("Roskilde Edge 01").first()).toBeVisible();
|
|
await expect(page.getByTestId("studio-custom-node-question").first()).toContainText("Visibility");
|
|
await expect(page.getByTestId("studio-custom-node-condition").first()).toContainText("Root condition");
|
|
await expect(page.getByTestId("studio-custom-node-rule").first()).toContainText("Input");
|
|
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-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");
|
|
|
|
await page.getByTitle("Auto layout").click();
|
|
await page.waitForTimeout(350);
|
|
const customNodesDoNotOverlap = await page.evaluate(() => {
|
|
const rects = [...document.querySelectorAll(".studio-custom-node")]
|
|
.map((node) => node.getBoundingClientRect())
|
|
.filter((rect) => rect.width > 0 && rect.height > 0);
|
|
|
|
for (let i = 0; i < rects.length; i += 1) {
|
|
for (let j = i + 1; j < rects.length; j += 1) {
|
|
const left = rects[i];
|
|
const right = rects[j];
|
|
const separated =
|
|
left.right <= right.left + 1 ||
|
|
right.right <= left.left + 1 ||
|
|
left.bottom <= right.top + 1 ||
|
|
right.bottom <= left.top + 1;
|
|
if (!separated) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
expect(customNodesDoNotOverlap).toBe(true);
|
|
|
|
const controlsDoNotCoverDetails = await page.evaluate(() => {
|
|
const controls = document.querySelector('[data-testid="studio-flow-actions"]')?.getBoundingClientRect();
|
|
const details = document.querySelector(".studio-details")?.getBoundingClientRect();
|
|
if (!controls || !details) {
|
|
return false;
|
|
}
|
|
return (
|
|
controls.right <= details.left ||
|
|
controls.bottom <= details.top ||
|
|
controls.left >= details.right ||
|
|
controls.top >= details.bottom
|
|
);
|
|
});
|
|
expect(controlsDoNotCoverDetails).toBe(true);
|
|
|
|
await page.getByTestId("studio-fullscreen-toggle").click();
|
|
await expect(page.getByTestId("self-serve-studio")).toHaveClass(/is-studio-fullscreen/);
|
|
await expect(page.getByTestId("studio-fullscreen-toggle")).toContainText("Exit fullscreen");
|
|
await page.getByTestId("studio-fullscreen-toggle").click();
|
|
await expect(page.getByTestId("self-serve-studio")).not.toHaveClass(/is-studio-fullscreen/);
|
|
|
|
await page.getByTestId("studio-filter-vehicle-type").selectOption({ label: "Bus" });
|
|
await expect
|
|
.poll(async () => page.locator('[data-testid="studio-custom-node-scope"]:visible h4').allTextContents())
|
|
.toEqual(["Bus"]);
|
|
await expect(page.getByText("Bus roof fixtures checked?").first()).toBeVisible();
|
|
await expect(page.getByText("Start bus wash").first()).toBeVisible();
|
|
await expect(page.getByText("Are mirrors folded?").first()).not.toBeVisible();
|
|
await expect
|
|
.poll(async () =>
|
|
page.evaluate(() => {
|
|
const widths = [...document.querySelectorAll(".studio-custom-node")]
|
|
.filter((node) => node.offsetParent !== null)
|
|
.map((node) => node.getBoundingClientRect().width);
|
|
return Math.min(...widths);
|
|
})
|
|
)
|
|
.toBeGreaterThan(110);
|
|
const focusedNodeMetrics = await page.evaluate(() => {
|
|
const rects = [...document.querySelectorAll(".studio-custom-node")]
|
|
.filter((node) => node.offsetParent !== null)
|
|
.map((node) => node.getBoundingClientRect());
|
|
const left = Math.min(...rects.map((rect) => rect.left));
|
|
const right = Math.max(...rects.map((rect) => rect.right));
|
|
const top = Math.min(...rects.map((rect) => rect.top));
|
|
const bottom = Math.max(...rects.map((rect) => rect.bottom));
|
|
return {
|
|
count: rects.length,
|
|
minWidth: Math.min(...rects.map((rect) => rect.width)),
|
|
spreadX: right - left,
|
|
spreadY: bottom - top,
|
|
};
|
|
});
|
|
expect(focusedNodeMetrics.count).toBeGreaterThanOrEqual(4);
|
|
expect(focusedNodeMetrics.spreadX).toBeLessThan(950);
|
|
expect(focusedNodeMetrics.spreadY).toBeLessThan(460);
|
|
await page.getByTestId("studio-filter-vehicle-type").selectOption("");
|
|
await expect(page.getByText("Are mirrors folded?").first()).toBeVisible();
|
|
|
|
await page.getByTestId("studio-custom-node-task").filter({ hasText: "Fold mirrors" }).click();
|
|
await expect(page.getByRole("heading", { name: "Inspector" })).toBeVisible();
|
|
await expect(page.getByTestId("studio-service-picker")).toContainText("MACHINE");
|
|
await expect(page.getByTestId("studio-service-picker")).toContainText("Machine relay on Roskilde Edge 01");
|
|
await expect(page.getByTestId("studio-service-picker").locator('input[value="MACHINE"]')).toBeChecked();
|
|
await page.getByTestId("studio-service-picker").locator('input[value="MACHINE"]').uncheck();
|
|
await page.getByRole("button", { name: "Save" }).click();
|
|
await expect
|
|
.poll(() =>
|
|
captured.graphSaves.some(
|
|
(save) =>
|
|
save.operations?.[0]?.entity === "task" &&
|
|
Array.isArray(save.operations?.[0]?.data?.services) &&
|
|
save.operations[0].data.services.length === 0
|
|
)
|
|
)
|
|
.toBe(true);
|
|
await expect(page.locator('.vue-flow__node[data-id="task:41"]')).toContainText("No service");
|
|
|
|
await page.getByTestId("studio-panel-flow").click();
|
|
await page.locator(".studio-node-list button").filter({ hasText: "Are mirrors folded?" }).click();
|
|
await expect(page.getByRole("heading", { name: "Inspector" })).toBeVisible();
|
|
await page.getByRole("textbox", { name: "Question", exact: true }).fill("Are loose straps secured?");
|
|
await page.getByRole("button", { name: "Save" }).click();
|
|
|
|
await expect
|
|
.poll(() =>
|
|
captured.graphSaves.some(
|
|
(save) =>
|
|
save.operations?.[0]?.entity === "question" &&
|
|
save.operations?.[0]?.data?.question === "Are loose straps secured?"
|
|
)
|
|
)
|
|
.toBe(true);
|
|
await expect(page.locator('.vue-flow__node[data-id="question:11"]')).toContainText("Are loose straps secured?");
|
|
});
|
|
|
|
test("supports drag-drop quick add, gateway action, simulator, publish, and rollback panels", async ({ page }) => {
|
|
const graph = buildStudioGraph();
|
|
const captured = { graphSaves: [], layouts: [], simulations: [], gatewayActions: [], rollbacks: [], published: 0 };
|
|
await installStudioRoutes(page, graph, captured);
|
|
|
|
await page.goto("/admin/6/modules/self-serve/studio", { waitUntil: "domcontentloaded", timeout: 90_000 });
|
|
await expect(page.getByTestId("self-serve-studio-flow")).toBeVisible();
|
|
|
|
await page.evaluate(() => {
|
|
const source = document.querySelector('[data-testid="studio-palette-question"]');
|
|
const target = document.querySelector('[data-testid="self-serve-studio-flow"]');
|
|
const dataTransfer = new DataTransfer();
|
|
source.dispatchEvent(new DragEvent("dragstart", { bubbles: true, cancelable: true, dataTransfer }));
|
|
target.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true, dataTransfer }));
|
|
target.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer }));
|
|
});
|
|
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-panel-hardware").click();
|
|
await expect(page.getByTestId("studio-service-coverage")).toContainText("MACHINE");
|
|
await expect(page.getByTestId("studio-service-coverage")).toContainText("Fold mirrors");
|
|
await expect(page.getByTestId("studio-service-coverage")).toContainText("Start bus wash");
|
|
await page.getByTitle("Discover gateway").click();
|
|
await expect.poll(() => captured.gatewayActions[0]?.action).toBe("discovery");
|
|
|
|
await page.getByTestId("studio-panel-simulator").click();
|
|
await page.getByLabel("Registration").fill("AB12345");
|
|
await page.getByRole("button", { name: "Run" }).click();
|
|
await expect.poll(() => captured.simulations[0]?.reg).toBe("AB12345");
|
|
await expect(page.locator(".studio-json")).toContainText("Fold mirrors");
|
|
|
|
await page.getByTestId("studio-panel-versions").click();
|
|
await page.getByRole("button", { name: "Publish" }).first().click();
|
|
await expect.poll(() => captured.published).toBe(1);
|
|
await page.getByRole("button", { name: "Rollback" }).last().click();
|
|
await expect.poll(() => captured.rollbacks[0]?.target_version_id).toBe(80);
|
|
});
|
|
});
|