dev went binary on H3 and said why: the steering reads the SIGN of a rating and never its size, so a ten-point scale was the UI claiming a precision nothing downstream used -- and at the sample sizes this actually runs at, a mediocre middle rating only diluted the direction. v5 had invented a four-way scale of its own; it now stores what dev stores. Pressing the one that is on takes it back. Without a way out a mis-click is a permanent opinion, and "no opinion" is a real answer nothing should have to guess at.
216 lines
8.1 KiB
JavaScript
216 lines
8.1 KiB
JavaScript
// The parts added to make v5 an app rather than a generate button: the action
|
|
// registry and the wheel that shows it, the inspector, and the way in.
|
|
//
|
|
// One file, because each is small and they are all the same shape: something
|
|
// announces, something else draws whatever announced.
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { setupDom, teardownDom, fire } from "../../composer/tests/_dom.js";
|
|
|
|
let actions, createWheel, createInspector, openWizard;
|
|
test.before(async () => {
|
|
setupDom();
|
|
await import("../../composer/composer.js");
|
|
actions = await import("../actions.js");
|
|
({ createWheel } = await import("../wheel.js"));
|
|
({ createInspector } = await import("../inspector.js"));
|
|
({ open: openWizard } = await import("../wizard.js"));
|
|
});
|
|
test.after(() => teardownDom());
|
|
|
|
// --- actions ----------------------------------------------------------------
|
|
|
|
test("an action is offered by whoever owns it, and taken away with it", () => {
|
|
actions._reset();
|
|
const ran = [];
|
|
const drop = actions.offerAction({ id: "go", label: "Go", run: () => ran.push("go") });
|
|
|
|
assert.deepEqual(actions.offered().map((a) => a.id), ["go"]);
|
|
assert.equal(actions.run("go"), true);
|
|
assert.deepEqual(ran, ["go"]);
|
|
|
|
drop();
|
|
assert.deepEqual(actions.offered(), []);
|
|
// Not an error: whatever offered it may simply be gone.
|
|
assert.equal(actions.run("go"), false);
|
|
});
|
|
|
|
test("one broken action does not take down the list that showed it", () => {
|
|
actions._reset();
|
|
actions.offerAction({ id: "bad", run: () => { throw new Error("nope"); } });
|
|
actions.offerAction({ id: "good", run: () => {} });
|
|
|
|
assert.equal(actions.run("bad"), false);
|
|
assert.equal(actions.run("good"), true, "a neighbour's failure stopped this one");
|
|
});
|
|
|
|
test("offering the same id twice replaces it", () => {
|
|
// Two entries that look identical and do different things is worse than one.
|
|
actions._reset();
|
|
const ran = [];
|
|
actions.offerAction({ id: "go", label: "First", run: () => ran.push(1) });
|
|
actions.offerAction({ id: "go", label: "Second", run: () => ran.push(2) });
|
|
|
|
assert.equal(actions.offered().length, 1);
|
|
assert.equal(actions.offered()[0].label, "Second");
|
|
actions.run("go");
|
|
assert.deepEqual(ran, [2]);
|
|
});
|
|
|
|
// --- the wheel --------------------------------------------------------------
|
|
|
|
const middle = (node, init = {}) => node.dispatchEvent(
|
|
new window.MouseEvent("mousedown", { button: 1, bubbles: true, cancelable: true, ...init }));
|
|
|
|
test("the middle button opens the wheel on whatever is offered", () => {
|
|
actions._reset();
|
|
actions.offerAction({ id: "generate", label: "Generate", run: () => {} });
|
|
actions.offerAction({ id: "cancel", label: "Cancel", run: () => {} });
|
|
const wheel = createWheel();
|
|
|
|
middle(document.body);
|
|
assert.equal(wheel.isOpen, true);
|
|
assert.match(document.body.textContent, /Generate/);
|
|
wheel.close();
|
|
assert.equal(wheel.isOpen, false);
|
|
wheel.destroy();
|
|
});
|
|
|
|
test("a wheel of one is not a wheel", () => {
|
|
// Every action comes from a part of the app that loaded, so "only one" is a
|
|
// real state -- and there is nothing to aim between. The kit refuses it; no
|
|
// wheel is a better answer than an error thrown under the pointer.
|
|
actions._reset();
|
|
const wheel = createWheel();
|
|
middle(document.body);
|
|
assert.equal(wheel.isOpen, false, "a wheel opened with nothing in it");
|
|
|
|
actions.offerAction({ id: "a", run: () => {} });
|
|
middle(document.body);
|
|
assert.equal(wheel.isOpen, false, "a wheel opened with one item in it");
|
|
wheel.destroy();
|
|
});
|
|
|
|
test("the middle button's own behaviour is prevented", () => {
|
|
// Otherwise Chrome starts autoscroll under the wheel.
|
|
actions._reset();
|
|
actions.offerAction({ id: "a", run: () => {} });
|
|
actions.offerAction({ id: "b", run: () => {} });
|
|
const wheel = createWheel();
|
|
const event = new window.MouseEvent("mousedown", { button: 1, bubbles: true, cancelable: true });
|
|
document.body.dispatchEvent(event);
|
|
assert.equal(event.defaultPrevented, true);
|
|
wheel.destroy();
|
|
});
|
|
|
|
test("a wheel that is already open is not opened again", () => {
|
|
actions._reset();
|
|
actions.offerAction({ id: "a", run: () => {} });
|
|
actions.offerAction({ id: "b", run: () => {} });
|
|
const wheel = createWheel();
|
|
middle(document.body);
|
|
const first = document.querySelectorAll(".cx-wheel").length;
|
|
middle(document.body);
|
|
assert.equal(document.querySelectorAll(".cx-wheel").length, first);
|
|
wheel.destroy();
|
|
});
|
|
|
|
// --- the inspector ----------------------------------------------------------
|
|
|
|
function fakeProject(over = {}) {
|
|
const scene = { id: "aaaaaaaaaaaa", text: "", result: null, length: null, rating: null };
|
|
const written = [];
|
|
return {
|
|
written,
|
|
project: { id: "p", name: "Untitled" },
|
|
scenes: [scene],
|
|
selectedId: scene.id,
|
|
selected: scene,
|
|
video: { length: 97 },
|
|
setScene: (id, key, value) => { written.push([id, key, value]); scene[key] = value; },
|
|
rename: (name) => written.push(["project", "name", name]),
|
|
...over,
|
|
};
|
|
}
|
|
|
|
test("the scene tab edits the scene, and says the crop is not what regenerates", () => {
|
|
const project = fakeProject();
|
|
const inspector = createInspector({ project });
|
|
document.body.replaceChildren(inspector.node);
|
|
|
|
assert.match(inspector.node.textContent, /Regenerating uses the project's length/);
|
|
const length = inspector.node.querySelector('input[type="number"]');
|
|
assert.equal(length.value, "97", "a scene with no crop of its own shows the project's length");
|
|
|
|
length.value = "48";
|
|
fire(length, "blur");
|
|
assert.deepEqual(project.written.at(-1), [project.selected.id, "length", 48]);
|
|
});
|
|
|
|
test("a rating is kept with the scene, and can be taken back", () => {
|
|
// Binary, because the sign is all anything downstream reads. And reversible:
|
|
// without a way back a mis-click is a permanent opinion, and "no opinion" is
|
|
// a real answer nothing should have to guess at.
|
|
const project = fakeProject();
|
|
const inspector = createInspector({ project });
|
|
document.body.replaceChildren(inspector.node);
|
|
|
|
const press = (label) => fire([...inspector.node.querySelectorAll("button")]
|
|
.find((b) => b.textContent === label), "click");
|
|
|
|
press("Liked");
|
|
assert.deepEqual(project.written.at(-1), [project.selected.id, "rating", "liked"]);
|
|
press("Disliked");
|
|
assert.deepEqual(project.written.at(-1), [project.selected.id, "rating", "disliked"]);
|
|
press("Disliked");
|
|
assert.deepEqual(project.written.at(-1), [project.selected.id, "rating", null]);
|
|
});
|
|
|
|
test("the project tab renames the project", () => {
|
|
const project = fakeProject();
|
|
const renamed = [];
|
|
const inspector = createInspector({ project, onRename: (n) => renamed.push(n) });
|
|
document.body.replaceChildren(inspector.node);
|
|
|
|
inspector.show("project");
|
|
const name = inspector.node.querySelector('input[type="text"]');
|
|
assert.equal(name.value, "Untitled");
|
|
name.value = "Rooftops";
|
|
fire(name, "blur");
|
|
assert.deepEqual(renamed, ["Rooftops"]);
|
|
});
|
|
|
|
test("with no scene selected the inspector says so instead of drawing nothing", () => {
|
|
const inspector = createInspector({ project: fakeProject({ selected: null, scenes: [] }) });
|
|
document.body.replaceChildren(inspector.node);
|
|
assert.match(inspector.node.textContent, /No scene/);
|
|
});
|
|
|
|
// --- the way in -------------------------------------------------------------
|
|
|
|
test("the wizard can be left, and leaving is an answer", () => {
|
|
// v4's could not be dismissed: a page refresh was the only way out, which
|
|
// makes it a trap rather than a wizard.
|
|
const picked = [];
|
|
const window_ = openWizard({ onPick: (id) => picked.push(id) });
|
|
assert.ok(document.querySelector(".cx-modal"));
|
|
|
|
window_.close("dismissed");
|
|
assert.deepEqual(picked, [null], "dismissing said nothing at all");
|
|
assert.equal(document.querySelector(".cx-modal"), null);
|
|
});
|
|
|
|
test("a choice is answered once, however it was closed", () => {
|
|
const picked = [];
|
|
const window_ = openWizard({ onPick: (id) => picked.push(id) });
|
|
const card = [...document.querySelectorAll(".cx-card")]
|
|
.find((c) => /A few scenes/.test(c.textContent));
|
|
fire(card, "click");
|
|
|
|
assert.deepEqual(picked, ["scenes"]);
|
|
window_.close("again");
|
|
assert.deepEqual(picked, ["scenes"], "closing after a choice answered a second time");
|
|
});
|