generated from coulomb/repo-seed
166 lines
4.3 KiB
JavaScript
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);
|
|
});
|