226 lines
8.1 KiB
JavaScript
226 lines
8.1 KiB
JavaScript
// L1 tests for the pure widget-descriptor builders (spec D2/D4, §4.1).
|
|
// Each builder turns plain params into a ui_request descriptor object -- no Pi
|
|
// context, no I/O. Golden files pin the exact shape; fuzzy tests check that bad
|
|
// params throw clearly rather than silently produce a broken widget.
|
|
// Run: npm test (node --test)
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const {
|
|
buildSelectRequest,
|
|
buildMultiselectRequest,
|
|
buildArtifactGate,
|
|
buildInfoRequest,
|
|
buildFreetextRequest,
|
|
buildSchemaLinkingRequest,
|
|
buildJoinReviewRequest,
|
|
withChildLinkage,
|
|
} = require("../core/builders.js");
|
|
|
|
const GOLDEN = path.join(__dirname, "golden");
|
|
const golden = (name) => JSON.parse(fs.readFileSync(path.join(GOLDEN, name)));
|
|
|
|
// --- golden shape tests -------------------------------------------------------
|
|
|
|
test("select F1 matches golden shape", () => {
|
|
const result = buildSelectRequest({
|
|
id: "u1", phase: "F1", title: "Disambigua 'ablazione'",
|
|
options: [{ id: "o1", label: "procedura" }, { id: "o2", label: "patologia" }],
|
|
});
|
|
const g = golden("select_F1.json");
|
|
assert.equal(result.widget, "select");
|
|
assert.equal(result.type, "ui_request");
|
|
assert.equal(result.schema_version, 1);
|
|
assert.deepEqual(result.reserved, g.reserved); // back/exit/other framework hatches
|
|
assert.deepEqual(result.options, g.options);
|
|
});
|
|
|
|
test("select no longer injects an Altro option (free-text is the reserved 'Other' control)", () => {
|
|
const result = buildSelectRequest({
|
|
id: "u", phase: "F1", title: "x",
|
|
options: [{ id: "a", label: "A" }],
|
|
});
|
|
assert.equal(result.options.find((o) => o.id === "other"), undefined, "no injected Altro option");
|
|
assert.ok(!("allow_other" in result), "the unused allow_other flag is gone");
|
|
assert.deepEqual(result.reserved, ["back", "exit", "other"], "Altro still offered via the reserved control");
|
|
});
|
|
|
|
test("select surfaces a recommended marker when given", () => {
|
|
const result = buildSelectRequest({
|
|
id: "u", phase: "F1", title: "x",
|
|
options: [{ id: "a", label: "A" }, { id: "b", label: "B" }],
|
|
recommended: "b",
|
|
});
|
|
assert.equal(result.recommended, "b");
|
|
});
|
|
|
|
test("multiselect F4 matches golden + carries content + allowEmpty", () => {
|
|
// a multiselect with allow_empty:false MUST have options (zero options is a
|
|
// broken widget); the golden uses allow_empty:true for the empty-options case
|
|
// and allow_empty:false when options are present.
|
|
const result = buildMultiselectRequest({
|
|
id: "u3", phase: "F4", title: "Tabelle",
|
|
options: [{ id: "dim_pazienti", label: "dim_pazienti" }],
|
|
content: { candidates: [] }, allowEmpty: false,
|
|
});
|
|
const g = golden("multiselect_F4.json");
|
|
assert.equal(result.widget, "multiselect");
|
|
assert.equal(result.allow_empty, false);
|
|
assert.ok(!("allow_other" in result), "the unused allow_other flag is gone");
|
|
assert.ok("content" in result);
|
|
assert.deepEqual(result.reserved, g.reserved);
|
|
});
|
|
|
|
test("multiselect with allow_empty:true accepts zero options", () => {
|
|
const result = buildMultiselectRequest({
|
|
id: "u", phase: "F4", title: "x", options: [], allowEmpty: true,
|
|
});
|
|
assert.equal(result.allow_empty, true);
|
|
assert.deepEqual(result.options, []);
|
|
});
|
|
|
|
test("multiselect with allowEmpty:false and zero options throws", () => {
|
|
assert.throws(
|
|
() => buildMultiselectRequest({ id: "u", phase: "F4", title: "x", options: [], allowEmpty: false }),
|
|
/allow_empty|options/i,
|
|
);
|
|
});
|
|
|
|
test("artifact-gate F5 schema matches golden", () => {
|
|
const result = buildArtifactGate({
|
|
id: "u2", phase: "F5", title: "Schema-linking",
|
|
artifact: { kind: "schema_linking", data: { candidates: [] }, version: 1 },
|
|
action: { kind: "confirm", prompt: "Confermi?" },
|
|
});
|
|
const g = golden("artifact_gate_F5.json");
|
|
assert.equal(result.widget, "artifact-gate");
|
|
assert.equal(result.artifact.kind, "schema_linking");
|
|
assert.equal(result.action.kind, "confirm");
|
|
assert.deepEqual(result.reserved, g.reserved);
|
|
});
|
|
|
|
// --- the other widgets --------------------------------------------------------
|
|
|
|
test("info request is fire-and-forget (no reserved, no options)", () => {
|
|
const result = buildInfoRequest({ phase: "F2", level: "info", text: "sto per proporti lo schema-linking" });
|
|
assert.equal(result.widget, "info");
|
|
assert.equal(result.level, "info");
|
|
assert.equal(result.type, "ui_request");
|
|
assert.equal(result.id, undefined, "info is not correlated like a blocking widget");
|
|
assert.ok(!("reserved" in result));
|
|
});
|
|
|
|
test("freetext request carries a title and no options", () => {
|
|
const result = buildFreetextRequest({ id: "u9", phase: "F1", title: "Specifica…" });
|
|
assert.equal(result.widget, "freetext");
|
|
assert.equal(result.title, "Specifica…");
|
|
assert.deepEqual(result.reserved, ["back", "exit", "other"]);
|
|
});
|
|
|
|
test("buildSchemaLinkingRequest carries tables + reserved", () => {
|
|
const out = buildSchemaLinkingRequest({
|
|
id: "u7", phase: "F4", title: "F4 — Schema linking",
|
|
tables: [{ id: "t1", name: "dim_patient", kind: "promote", columns: [] }],
|
|
});
|
|
assert.equal(out.widget, "schema-linking");
|
|
assert.equal(out.id, "u7");
|
|
assert.equal(out.tables.length, 1);
|
|
assert.deepEqual(out.reserved, ["back", "exit", "other"]);
|
|
});
|
|
|
|
test("buildJoinReviewRequest exposes joins as read-only review items", () => {
|
|
const out = buildJoinReviewRequest({
|
|
id: "u8",
|
|
phase: "F4",
|
|
title: "F4 — Proposed joins",
|
|
options: [
|
|
{
|
|
id: "patient-events",
|
|
label: "Patient to events",
|
|
detail: "JOIN dim_patient p ON p.cod_paz = e.cod_paz",
|
|
rationale: "Standard patient key",
|
|
},
|
|
],
|
|
});
|
|
|
|
assert.equal(out.widget, "join-review");
|
|
assert.equal(out.confirm_label, "Continue");
|
|
assert.deepEqual(out.options, [
|
|
{
|
|
id: "patient-events",
|
|
label: "Patient to events",
|
|
detail: "JOIN dim_patient p ON p.cod_paz = e.cod_paz",
|
|
rationale: "Standard patient key",
|
|
},
|
|
]);
|
|
assert.deepEqual(out.reserved, ["back", "exit", "other"]);
|
|
});
|
|
|
|
test("withChildLinkage sets option.opens and returns the option", () => {
|
|
const child = { widget: "freetext", title: "Motivazione del rifiuto" };
|
|
const option = withChildLinkage({ id: "reject", label: "Rifiuta" }, child);
|
|
assert.deepEqual(option.opens, child);
|
|
});
|
|
|
|
// --- fuzzy tests (bad params must throw clearly, never silently break) --------
|
|
|
|
test("select with missing title throws clearly", () => {
|
|
assert.throws(
|
|
() => buildSelectRequest({ id: "u", phase: "F1", options: [] }),
|
|
/title/i,
|
|
);
|
|
});
|
|
|
|
test("select with non-array options throws clearly", () => {
|
|
assert.throws(
|
|
() => buildSelectRequest({ id: "u", phase: "F1", title: "x", options: "notalist" }),
|
|
/options/i,
|
|
);
|
|
});
|
|
|
|
test("artifact-gate with missing artifact throws clearly", () => {
|
|
assert.throws(
|
|
() => buildArtifactGate({ id: "u", phase: "F5", title: "x", action: { kind: "confirm" } }),
|
|
/artifact/i,
|
|
);
|
|
});
|
|
|
|
test("artifact-gate with unknown action kind throws clearly", () => {
|
|
assert.throws(
|
|
() => buildArtifactGate({
|
|
id: "u", phase: "F5", title: "x",
|
|
artifact: { kind: "cte", data: {}, version: 1 },
|
|
action: { kind: "bogus" },
|
|
}),
|
|
/action/i,
|
|
);
|
|
});
|
|
|
|
test("buildArtifactGate derives approve/reject options from action.kind", () => {
|
|
const w = buildArtifactGate({
|
|
id: "u1",
|
|
phase: "F1",
|
|
title: "Chiudi fase",
|
|
artifact: { kind: "phase", data: {} },
|
|
action: { kind: "approve_reject" },
|
|
});
|
|
assert.deepEqual(w.options, [
|
|
{ id: "approve", label: "Salva e procedi", recommended: true },
|
|
{ id: "reject", label: "Rifiuta" },
|
|
]);
|
|
});
|
|
|
|
test("buildArtifactGate: confirm -> single approve; view_only -> no options", () => {
|
|
const confirm = buildArtifactGate({
|
|
id: "u1", phase: "F1", title: "t", artifact: { kind: "phase", data: {} }, action: { kind: "confirm" },
|
|
});
|
|
assert.deepEqual(confirm.options, [{ id: "approve", label: "Salva e procedi", recommended: true }]);
|
|
const viewOnly = buildArtifactGate({
|
|
id: "u1", phase: "F1", title: "t", artifact: { kind: "phase", data: {} }, action: { kind: "view_only" },
|
|
});
|
|
assert.deepEqual(viewOnly.options, []);
|
|
});
|