Files
state-hub/dashboard/test/status-control.test.mjs

166 lines
4.3 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
class FakeClassList {
constructor() {
this.values = new Set();
}
toggle(name, enabled) {
if (enabled) {
this.values.add(name);
} else {
this.values.delete(name);
}
}
}
class FakeElement {
constructor(tagName) {
this.tagName = tagName;
this.children = [];
this.listeners = {};
this.classList = new FakeClassList();
this.className = "";
this.textContent = "";
this.value = "";
this.disabled = false;
}
setAttribute(name, value) {
this[name] = value;
}
append(...children) {
this.children.push(...children);
}
addEventListener(type, listener) {
this.listeners[type] = listener;
}
}
function installDom() {
const styles = new Map();
globalThis.document = {
head: {
append(element) {
if (element.id) styles.set(element.id, element);
},
},
getElementById(id) {
return styles.get(id) ?? null;
},
createElement(tagName) {
return new FakeElement(tagName);
},
};
globalThis.window = {
confirm: () => true,
prompt: () => "",
};
globalThis.setTimeout = () => 0;
}
installDom();
const {statusControl} = await import("../src/components/status-control.js");
function okResponse(overrides = {}) {
return {
target_type: "task",
target_id: "00000000-0000-0000-0000-000000000001",
actor: "dashboard",
current_status: "todo",
target_status: "progress",
file_backed: true,
archived_file: false,
task_linked: true,
reconciliation_class: "write_through",
reason: "task status can be represented in the workplan task block",
follow_up: "patch task block status and sync the DB from file",
write_through_result: "applied",
workplan_path: "workplans/STATE-WP-9999-demo.md",
reconciliation_record_id: null,
conflict: false,
...overrides,
};
}
test("status control posts dashboard changes through reconciliation", async () => {
const requests = [];
globalThis.fetch = async (url, options) => {
requests.push({url, options, body: JSON.parse(options.body)});
return {
ok: true,
json: async () => okResponse(),
};
};
const entity = {id: "00000000-0000-0000-0000-000000000001", status: "todo"};
let saved = null;
const root = statusControl({
entity,
type: "task",
statuses: ["todo", "progress"],
onSaved: (updated, result) => {
saved = {updated, result};
},
});
const [select, message] = root.children;
select.value = "progress";
await select.listeners.change();
assert.equal(requests.length, 1);
assert.equal(requests[0].url, "http://127.0.0.1:8000/reconciliation/state-change");
assert.equal(requests[0].body.target_type, "task");
assert.equal(requests[0].body.target_status, "progress");
assert.equal(requests[0].body.expected_current_status, "todo");
assert.equal(requests[0].body.apply, true);
assert.equal(entity.status, "progress");
assert.equal(message.textContent, "synced");
assert.equal(saved.result.write_through_result, "applied");
});
test("status control keeps local state on reconciliation conflicts", async () => {
const requests = [];
globalThis.fetch = async (url, options) => {
requests.push({url, options, body: JSON.parse(options.body)});
return {
ok: true,
json: async () => okResponse({
current_status: "done",
target_status: "progress",
reconciliation_class: "deferred",
reason: "cached task status changed from expected 'todo' to 'done'",
follow_up: "refresh the dashboard and retry the state change if it is still intended",
write_through_result: "not_applicable",
reconciliation_record_id: "00000000-0000-0000-0000-000000000002",
conflict: true,
}),
};
};
const entity = {id: "00000000-0000-0000-0000-000000000001", status: "todo"};
let saved = null;
const root = statusControl({
entity,
type: "task",
statuses: ["todo", "progress"],
onSaved: () => {
saved = true;
},
});
const [select, message] = root.children;
select.value = "progress";
await select.listeners.change();
assert.equal(requests.length, 1);
assert.equal(entity.status, "todo");
assert.equal(select.value, "todo");
assert.equal(message.textContent, "out of sync");
assert.equal(saved, null);
});