Add task priority, gate type normalization, and vehicle type logic to self-serve functionalities:

- Introduced task order priority, dynamic image thumb position, and improved task gate type handling in the self-serve studio.
- Enhanced task payload normalization with nullable fields and parameter verification.
- Updated UI with order priority controls, dynamic images configuration, and machine type-specific scoping options.
- Added and refined unit tests for task sorting, form controls, and validation rules.
This commit is contained in:
Jeppe Bundgaard
2026-03-26 11:35:24 +01:00
parent 21be6c0efd
commit 7368c476f1
9 changed files with 297 additions and 57 deletions
@@ -49,19 +49,41 @@ const buildScopedTaskName = async (task) => {
return `${departmentName}-${task.lane}-${productName}: ${task.task}`;
};
const normalizePayload = (payload) => ({
department: parseInt(payload.department || 0),
lane: parseInt(payload.lane || 0),
product: parseInt(payload.product || 0),
machine_type_id: payload.machine_type_id && parseInt(payload.machine_type_id) !== 0 ? parseInt(payload.machine_type_id) : null,
condition_id: payload.condition_id && parseInt(payload.condition_id) !== 0 ? parseInt(payload.condition_id) : null,
task: payload.task,
description: payload.description,
order_priority: parseInt(payload.order_priority || 0),
services: Array.isArray(payload.services) ? payload.services : [],
buttons: Array.isArray(payload.buttons) ? payload.buttons.map((button) => parseInt(button)) : [],
dynamic_images_vehicle_type: payload.dynamic_images_vehicle_type ? parseInt(payload.dynamic_images_vehicle_type) : null,
});
const parseIntOrZero = (value) => {
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) ? 0 : parsed;
};
const parseNullablePositiveInt = (value) => {
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) || parsed <= 0 ? null : parsed;
};
const normalizePayload = (payload = {}) => {
const gateTypeCandidate = String(payload.gate_type || "").toUpperCase();
const normalizedGateType = ["ALWAYS", "CONDITION", "QUESTION"].includes(gateTypeCandidate)
? gateTypeCandidate
: (parseNullablePositiveInt(payload.gate_ref_id ?? payload.condition_id) ? "CONDITION" : "ALWAYS");
const normalizedGateRefId = normalizedGateType === "ALWAYS"
? null
: parseNullablePositiveInt(payload.gate_ref_id ?? payload.condition_id);
return {
department: parseIntOrZero(payload.department),
lane: parseIntOrZero(payload.lane),
product: parseIntOrZero(payload.product),
machine_type_id: parseNullablePositiveInt(payload.machine_type_id),
condition_id: normalizedGateRefId,
gate_type: normalizedGateType,
gate_ref_id: normalizedGateRefId,
task: payload.task,
description: payload.description,
order_priority: parseIntOrZero(payload.order_priority),
services: Array.isArray(payload.services) ? payload.services : [],
buttons: Array.isArray(payload.buttons) ? payload.buttons.map((button) => parseIntOrZero(button)) : [],
dynamic_images_vehicle_type: parseNullablePositiveInt(payload.dynamic_images_vehicle_type),
};
};
/**
* The SelfServeTasks object
@@ -22,6 +22,11 @@ const parseIntOrZero = (value) => {
return Number.isNaN(parsed) ? 0 : parsed;
};
const parseNullablePositiveInt = (value, fallback = null) => {
const parsed = parseNullableInt(value, fallback);
return parsed && parsed > 0 ? parsed : fallback;
};
const buildOptionHtml = (options, selectedValue = null) =>
options.map((option) => {
const optionValue = option?.id ?? "";
@@ -376,11 +381,14 @@ export const showSelfServeTaskForm = async ({ object, record = null, initialData
await updateConditionOptions();
},
preConfirm: async () => {
const selectedConditionId = parseNullablePositiveInt(getFieldValue("condition_id"));
const payload = {
task: getFieldValue("task").trim(),
description: getFieldValue("description").trim(),
order_priority: parseIntOrZero(getFieldValue("order_priority")),
condition_id: parseNullableInt(getFieldValue("condition_id")),
condition_id: selectedConditionId,
gate_type: selectedConditionId ? "CONDITION" : "ALWAYS",
gate_ref_id: selectedConditionId,
services: getCheckedValues("services"),
buttons: getCheckedValues("buttons").map((value) => parseIntOrZero(value)),
dynamic_images_vehicle_type: parseNullableInt(getFieldValue("dynamic_images_vehicle_type")),
+14 -1
View File
@@ -61,6 +61,19 @@ export function useWashSessionActions(options) {
return totalButtonsCompleted;
};
const getThumbPosition = () => {
// Get the position of the thumb based on the session state
let defaultThumbPosition = 0;
// Check if any of the tasks has the "thumb_position" property and use it as the default position
for (const task of activeTasks.value) {
if (task.dynamic_images_vehicle_type !== null) {
defaultThumbPosition = task.dynamic_images_vehicle_type;
break;
}
}
return defaultThumbPosition;
};
const dynamicImageUrl = computed(() => {
const departmentId = nearestDepartment.value?.id;
const laneId = washLaneId.value;
@@ -70,7 +83,7 @@ export function useWashSessionActions(options) {
return null;
}
return `${API_URL}/department/lanes/dynamic-image?department=${departmentId}&lane=${laneId}&current_step=${machineStartCurrentStep.value}&buttons=${JSON.stringify(getButtonsToPress())}&vehicle_type=${vehicleTypeSelect.value}`;
return `${API_URL}/department/lanes/dynamic-image?department=${departmentId}&lane=${laneId}&current_step=${machineStartCurrentStep.value}&buttons=${JSON.stringify(getButtonsToPress())}&vehicle_type=${vehicleTypeSelect.value}&thumb_position=${getThumbPosition()}`;
});
const executeSelfServeCommand = async (laneId, command, args = {
@@ -33,6 +33,52 @@ const parseScopedProduct = (value) => {
return parsed && parsed > 0 ? parsed : null;
};
const compareByOrderPriority = (left, right) => {
const priorityDiff = parseIntOrZero(left?.order_priority) - parseIntOrZero(right?.order_priority);
if (priorityDiff !== 0) {
return priorityDiff;
}
return parseIntOrZero(left?.id) - parseIntOrZero(right?.id);
};
const nextOrderPriority = (items) => (
(Array.isArray(items) ? items : []).reduce((highest, entry) => (
Math.max(highest, parseIntOrZero(entry?.order_priority))
), 0) + 1
);
const TASK_GATE_TYPES = ["ALWAYS", "CONDITION", "QUESTION"];
const TASK_SERVICE_OPTIONS = ["MACHINE"];
const TASK_BUTTON_MIN = 0;
const TASK_BUTTON_MAX = 11;
const normalizeTaskGateType = (value) => {
const gateType = String(value || "ALWAYS").toUpperCase();
return TASK_GATE_TYPES.includes(gateType) ? gateType : "ALWAYS";
};
const normalizeTaskServices = (value) => {
if (!Array.isArray(value)) {
return [];
}
return value
.map((entry) => String(entry || "").toUpperCase())
.filter((entry, index, source) => TASK_SERVICE_OPTIONS.includes(entry) && source.indexOf(entry) === index);
};
const normalizeTaskButtons = (value) => {
if (!Array.isArray(value)) {
return [];
}
const normalized = value
.map((entry) => parseIntOrZero(entry))
.filter((entry) => entry >= TASK_BUTTON_MIN && entry <= TASK_BUTTON_MAX);
return [...new Set(normalized)].sort((a, b) => a - b);
};
const ensureArray = (value) => {
if (Array.isArray(value)) {
return value;
@@ -150,7 +196,7 @@ const ruleForm = ref({
const taskForm = ref({
id: null,
scope_mode: "legacy",
machine_type_id: null,
machine_type_id: 0,
department: 0,
lane: 0,
product: 0,
@@ -202,6 +248,20 @@ const vehicleTypeOptions = computed(() => {
return [...options].sort((a, b) => String(a.name || "").localeCompare(String(b.name || "")));
});
const machineScopePrimaryVehicleTypeOptions = computed(() => {
const options = vehicleTypeOptions.value.map((entry) => ({
id: parseIntOrZero(entry.id),
name: `${entry.name} (#${parseIntOrZero(entry.id)})`,
}));
const selectedId = parseIntOrZero(taskForm.value.machine_type_id);
if (selectedId > 0 && !options.some((entry) => entry.id === selectedId)) {
options.unshift({ id: selectedId, name: `#${selectedId}` });
}
return options;
});
const vehicleTypeName = (productId) => {
const found = vehicleTypeOptions.value.find((entry) => parseInt(entry.id) === parseInt(productId));
return found?.name || `#${productId}`;
@@ -302,7 +362,10 @@ const loadStudioData = async () => {
questions.value = ensureArray(questionList).filter((entry) => [0, departmentId.value].includes(parseIntOrZero(entry.department)));
conditions.value = ensureArray(conditionList).filter((entry) => [0, departmentId.value].includes(parseIntOrZero(entry.department)));
rules.value = ensureArray(ruleList);
tasks.value = ensureArray(taskList).filter((entry) => [0, departmentId.value].includes(parseIntOrZero(entry.department)));
tasks.value = ensureArray(taskList)
.filter((entry) => [0, departmentId.value].includes(parseIntOrZero(entry.department)))
.sort(compareByOrderPriority);
questions.value = [...questions.value].sort(compareByOrderPriority);
machineTypes.value = ensureArray(machineTypeList);
products.value = ensureArray(productList);
lanes.value = ensureArray(laneList).filter((entry) => parseIntOrZero(entry.department) === departmentId.value);
@@ -378,6 +441,7 @@ const openQuestionModal = (record = null) => {
? {
...record,
scope_mode: parseIntOrZero(record.department) === 0 && parseIntOrZero(record.lane) === 0 && parseIntOrZero(record.product) === 0 ? "shared" : "legacy",
order_priority: parseIntOrZero(record.order_priority),
}
: {
id: null,
@@ -388,7 +452,7 @@ const openQuestionModal = (record = null) => {
question: "",
description: "",
condition_id: null,
order_priority: questions.value.length + 1,
order_priority: nextOrderPriority(questions.value),
};
showQuestionModal.value = true;
};
@@ -453,7 +517,7 @@ const openConditionModal = (record = null) => {
: {
id: null,
scope_mode: scopedMachineTypeId.value ? "machine_type" : "legacy",
machine_type_id: scopedMachineTypeId.value,
machine_type_id: scopedProductId.value || scopedMachineTypeId.value || 0,
department: departmentId.value,
lane: scopedLaneId.value || 0,
product: scopedProductId.value || 0,
@@ -563,16 +627,19 @@ const openTaskModal = (record = null) => {
taskForm.value = record
? {
...record,
machine_type_id: parseIntOrZero(record.machine_type_id),
scope_mode: parseNullableInt(record.machine_type_id) ? "machine_type" : "legacy",
gate_type: String(record.gate_type || "ALWAYS").toUpperCase(),
gate_type: normalizeTaskGateType(record.gate_type || (parseNullableInt(record.gate_ref_id ?? record.condition_id) ? "CONDITION" : "ALWAYS")),
gate_ref_id: parseNullableInt(record.gate_ref_id ?? record.condition_id),
services: Array.isArray(record.services) ? [...record.services] : [],
buttons: Array.isArray(record.buttons) ? [...record.buttons] : [],
order_priority: parseIntOrZero(record.order_priority),
services: normalizeTaskServices(record.services),
buttons: normalizeTaskButtons(record.buttons),
dynamic_images_vehicle_type: parseNullableInt(record.dynamic_images_vehicle_type),
}
: {
id: null,
scope_mode: scopedMachineTypeId.value ? "machine_type" : "legacy",
machine_type_id: scopedMachineTypeId.value,
scope_mode: "legacy",
machine_type_id: scopedProductId.value || 0,
department: departmentId.value,
lane: scopedLaneId.value || 0,
product: scopedProductId.value || 0,
@@ -580,7 +647,7 @@ const openTaskModal = (record = null) => {
gate_ref_id: null,
task: "",
description: "",
order_priority: tasks.value.length + 1,
order_priority: nextOrderPriority(tasks.value),
services: [],
buttons: [],
dynamic_images_vehicle_type: null,
@@ -589,20 +656,31 @@ const openTaskModal = (record = null) => {
};
const saveTask = async () => {
const machineScopePrimaryVehicleTypeProductId = taskForm.value.scope_mode === "machine_type"
? parseIntOrZero(taskForm.value.machine_type_id)
: 0;
const gateType = normalizeTaskGateType(taskForm.value.gate_type);
const resolvedDepartmentId = parseIntOrZero(taskForm.value.department || departmentId.value);
const resolvedLaneId = parseIntOrZero(taskForm.value.lane);
const payload = {
task: String(taskForm.value.task || "").trim(),
description: String(taskForm.value.description || "").trim(),
order_priority: parseIntOrZero(taskForm.value.order_priority),
services: Array.isArray(taskForm.value.services) ? taskForm.value.services : [],
buttons: Array.isArray(taskForm.value.buttons) ? taskForm.value.buttons.map((entry) => parseIntOrZero(entry)) : [],
services: normalizeTaskServices(taskForm.value.services),
buttons: normalizeTaskButtons(taskForm.value.buttons),
dynamic_images_vehicle_type: parseNullableInt(taskForm.value.dynamic_images_vehicle_type),
gate_type: String(taskForm.value.gate_type || "ALWAYS").toUpperCase(),
gate_type: gateType,
gate_ref_id: null,
condition_id: null,
machine_type_id: taskForm.value.scope_mode === "machine_type" ? parseNullableInt(taskForm.value.machine_type_id) : null,
department: taskForm.value.scope_mode === "machine_type" ? 0 : parseIntOrZero(taskForm.value.department || departmentId.value),
lane: taskForm.value.scope_mode === "machine_type" ? 0 : parseIntOrZero(taskForm.value.lane),
product: taskForm.value.scope_mode === "machine_type" ? 0 : parseIntOrZero(taskForm.value.product),
machine_type_id: taskForm.value.scope_mode === "machine_type"
? (machineScopePrimaryVehicleTypeProductId > 0 ? machineScopePrimaryVehicleTypeProductId : null)
: null,
department: resolvedDepartmentId,
lane: resolvedLaneId,
product: taskForm.value.scope_mode === "machine_type"
? machineScopePrimaryVehicleTypeProductId
: parseIntOrZero(taskForm.value.product),
};
if (!payload.task) {
@@ -616,8 +694,8 @@ const saveTask = async () => {
}
if (taskForm.value.scope_mode === "machine_type") {
if (!payload.machine_type_id) {
toast.error("Machine type is required for this scope.");
if (!machineScopePrimaryVehicleTypeProductId) {
toast.error("Primary vehicle type product id is required for this scope.");
return;
}
} else {
@@ -641,7 +719,7 @@ const saveTask = async () => {
toast.error("Gate reference is required for selected gate type.");
return;
}
payload.condition_id = payload.gate_ref_id;
payload.condition_id = payload.gate_type === "CONDITION" ? payload.gate_ref_id : null;
}
try {
@@ -1056,6 +1134,10 @@ onMounted(async () => {
</select>
</div>
</div>
<div class="field">
<label class="label">Order Priority</label>
<input v-model="questionForm.order_priority" class="input" type="number" min="0">
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success" @click="saveQuestion">Save</button>
@@ -1211,30 +1293,32 @@ onMounted(async () => {
</div>
</div>
<div v-if="taskForm.scope_mode === 'machine_type'" class="field">
<label class="label">Machine Type</label>
<label class="label">Primary Vehicle Type Product ID</label>
<div class="select is-fullwidth">
<select v-model="taskForm.machine_type_id">
<option :value="null">Select machine type</option>
<option v-for="entry in machineTypes" :key="entry.id" :value="parseInt(entry.id)">{{ entry.name }}</option>
<option :value="0">Select primary vehicle type</option>
<option v-for="entry in machineScopePrimaryVehicleTypeOptions" :key="entry.id" :value="parseInt(entry.id)">
{{ entry.name }}
</option>
</select>
</div>
</div>
<div class="field">
<label class="label">Department</label>
<input :value="departmentId" class="input" disabled>
</div>
<div class="field">
<label class="label">Lane</label>
<div class="select is-fullwidth">
<select v-model="taskForm.lane">
<option :value="0">Select lane</option>
<option v-for="entry in lanes" :key="entry.id" :value="parseInt(entry.id)">
{{ entry.name }} ({{ entry.id }})
</option>
</select>
</div>
</div>
<template v-if="taskForm.scope_mode === 'legacy'">
<div class="field">
<label class="label">Department</label>
<input :value="departmentId" class="input" disabled>
</div>
<div class="field">
<label class="label">Lane</label>
<div class="select is-fullwidth">
<select v-model="taskForm.lane">
<option :value="0">Select lane</option>
<option v-for="entry in lanes" :key="entry.id" :value="parseInt(entry.id)">
{{ entry.name }} ({{ entry.id }})
</option>
</select>
</div>
</div>
<div class="field">
<label class="label">Vehicle Type</label>
<div class="select is-fullwidth">
@@ -1255,6 +1339,10 @@ onMounted(async () => {
<label class="label">Description</label>
<input v-model="taskForm.description" class="input">
</div>
<div class="field">
<label class="label">Order Priority</label>
<input v-model="taskForm.order_priority" class="input" type="number" min="0">
</div>
<div class="field">
<label class="label">Gate Type</label>
<div class="select is-fullwidth">
@@ -1300,6 +1388,16 @@ onMounted(async () => {
</button>
</div>
</div>
<div class="field">
<label class="label">Dynamic Images Thumb Position</label>
<div class="select is-fullwidth">
<select v-model="taskForm.dynamic_images_vehicle_type">
<option :value="null">None</option>
<!-- 1-12 -->
<option v-for="index in 12" :key="index" :value="index - 1">{{ index - 1 }}</option>
</select>
</div>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success" @click="saveTask">Save</button>
@@ -10,7 +10,7 @@ import {
type RelayKind,
} from "@/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue";
import type { Machine } from "@/views/dashboards/superUserDashboard/selfserve/types/MachineType.vue";
import {BTooltip} from "buefy";
import {BSwitch, BTooltip} from "buefy";
import MachineStatusType from "@/views/dashboards/superUserDashboard/selfserve/types/MachineStatusType.vue";
const props = defineProps<{ machine: Machine }>();
@@ -27,4 +27,12 @@ describe("self-serve studio question scope", () => {
expect(studioSource).toContain("if (payload.lane <= 0)");
expect(studioSource).toContain("if (payload.product <= 0)");
});
it("renders and submits question order priority", () => {
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
expect(studioSource).toContain("<label class=\"label\">Order Priority</label>");
expect(studioSource).toContain("v-model=\"questionForm.order_priority\"");
expect(studioSource).toContain("order_priority: parseIntOrZero(questionForm.value.order_priority)");
});
});
@@ -5,10 +5,9 @@ import { describe, expect, it } from "vitest";
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
describe("self-serve studio task scope", () => {
it("renders legacy scope selectors for task lane and vehicle", () => {
it("renders task selectors for lane and vehicle", () => {
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
expect(studioSource).toContain("v-if=\"taskForm.scope_mode === 'legacy'\"");
expect(studioSource).toContain("v-model=\"taskForm.lane\"");
expect(studioSource).toContain("v-model=\"taskForm.product\"");
expect(studioSource).toContain("Select lane");
@@ -16,12 +15,39 @@ describe("self-serve studio task scope", () => {
expect(studioSource).toContain("v-for=\"entry in lanes\"");
});
it("keeps department and lane visible for machine-type task scope", () => {
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
expect(studioSource).toContain("v-if=\"taskForm.scope_mode === 'machine_type'\" class=\"field\"");
expect(studioSource).toContain("<label class=\"label\">Department</label>");
expect(studioSource).toContain("<select v-model=\"taskForm.lane\">");
expect(studioSource).toContain("<template v-if=\"taskForm.scope_mode === 'legacy'\">");
expect(studioSource).toContain("<label class=\"label\">Vehicle Type</label>");
});
it("uses primary vehicle type product selection for machine scope", () => {
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
expect(studioSource).toContain("const machineScopePrimaryVehicleTypeOptions = computed(() => {");
expect(studioSource).toContain("Primary Vehicle Type Product ID");
expect(studioSource).toContain("v-for=\"entry in machineScopePrimaryVehicleTypeOptions\"");
expect(studioSource).toContain("machineScopePrimaryVehicleTypeProductId");
expect(studioSource).toContain("product: taskForm.value.scope_mode === \"machine_type\"");
});
it("defaults new task modal to legacy scope so department and lane are immediately visible", () => {
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
expect(studioSource).toContain("scope_mode: \"legacy\"");
expect(studioSource).toContain("machine_type_id: scopedProductId.value || 0");
});
it("guards task submission for machine-type and legacy scope requirements", () => {
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
expect(studioSource).toContain("Task name is required.");
expect(studioSource).toContain("Task description is required.");
expect(studioSource).toContain("Machine type is required for this scope.");
expect(studioSource).toContain("Primary vehicle type product id is required for this scope.");
expect(studioSource).toContain("Department is required for Department/Lane/Vehicle scope.");
expect(studioSource).toContain("Lane is required for Department/Lane/Vehicle scope.");
expect(studioSource).toContain("Vehicle type is required for Department/Lane/Vehicle scope.");
@@ -35,5 +61,36 @@ describe("self-serve studio task scope", () => {
expect(studioSource).toContain("if (payload.gate_type !== \"ALWAYS\")");
expect(studioSource).toContain("if (!payload.gate_ref_id)");
expect(studioSource).toContain("Gate reference is required for selected gate type.");
expect(studioSource).toContain("payload.condition_id = payload.gate_type === \"CONDITION\" ? payload.gate_ref_id : null;");
});
it("includes all task payload parameters in saveTask submission", () => {
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
expect(studioSource).toContain("const payload = {");
expect(studioSource).toContain("task: String(taskForm.value.task || \"\").trim()");
expect(studioSource).toContain("description: String(taskForm.value.description || \"\").trim()");
expect(studioSource).toContain("order_priority: parseIntOrZero(taskForm.value.order_priority)");
expect(studioSource).toContain("services: normalizeTaskServices(taskForm.value.services)");
expect(studioSource).toContain("buttons: normalizeTaskButtons(taskForm.value.buttons)");
expect(studioSource).toContain("dynamic_images_vehicle_type: parseNullableInt(taskForm.value.dynamic_images_vehicle_type)");
expect(studioSource).toContain("gate_type: gateType");
expect(studioSource).toContain("gate_ref_id: null");
expect(studioSource).toContain("condition_id: null");
expect(studioSource).toContain("machine_type_id: taskForm.value.scope_mode === \"machine_type\"");
expect(studioSource).toContain("const resolvedDepartmentId = parseIntOrZero(taskForm.value.department || departmentId.value);");
expect(studioSource).toContain("const resolvedLaneId = parseIntOrZero(taskForm.value.lane);");
expect(studioSource).toContain("department: resolvedDepartmentId");
expect(studioSource).toContain("lane: resolvedLaneId");
expect(studioSource).toContain("product: taskForm.value.scope_mode === \"machine_type\"");
});
it("renders task controls for order priority and dynamic images vehicle type", () => {
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
expect(studioSource).toContain("<label class=\"label\">Order Priority</label>");
expect(studioSource).toContain("v-model=\"taskForm.order_priority\"");
expect(studioSource).toContain("<label class=\"label\">Dynamic Images Thumb Position</label>");
expect(studioSource).toContain("v-model=\"taskForm.dynamic_images_vehicle_type\"");
});
});
@@ -23,4 +23,12 @@ describe("self-serve studio vehicle type scope", () => {
expect(studioSource).toContain("const scopedTasks = computed(() => tasks.value.filter((entry) => matchesScopedProduct(entry)));");
expect(studioSource).toContain("product: scopedProductId.value || 0,");
});
it("sorts scoped question and task source lists by order priority", () => {
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
expect(studioSource).toContain("const compareByOrderPriority = (left, right) => {");
expect(studioSource).toContain(".sort(compareByOrderPriority);");
expect(studioSource).toContain("questions.value = [...questions.value].sort(compareByOrderPriority);");
});
});
@@ -0,0 +1,26 @@
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 tasks payload contract", () => {
it("maps task form condition gating to gate_type, gate_ref_id and condition_id", () => {
const formSource = readSource("src/components/session/token/SessionUser/Objects/selfServeObjectForms.js");
expect(formSource).toContain("const selectedConditionId = parseNullablePositiveInt(getFieldValue(\"condition_id\"));");
expect(formSource).toContain("condition_id: selectedConditionId");
expect(formSource).toContain("gate_type: selectedConditionId ? \"CONDITION\" : \"ALWAYS\"");
expect(formSource).toContain("gate_ref_id: selectedConditionId");
});
it("normalizes task payloads with explicit gate and nullable fields", () => {
const tasksSource = readSource("src/components/session/token/SessionUser/Objects/SelfServeTasks.vue");
expect(tasksSource).toContain("const normalizePayload = (payload = {}) => {");
expect(tasksSource).toContain("condition_id: normalizedGateRefId");
expect(tasksSource).toContain("gate_type: normalizedGateType");
expect(tasksSource).toContain("gate_ref_id: normalizedGateRefId");
expect(tasksSource).toContain("dynamic_images_vehicle_type: parseNullablePositiveInt(payload.dynamic_images_vehicle_type)");
});
});