UI: tests for app.dom, stage, cover, look, tiles; feature rule fixtures
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155S2Ye8BRUakV7GHAsmYWu
This commit is contained in:
+1
-1
@@ -31,7 +31,7 @@ const link = linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if
|
||||
const frame = build(document.getElementById("app"));
|
||||
const selection = createSelection({ project });
|
||||
selection.on(() => say("select"));
|
||||
const app = { editorSettings: [], timelineLanes: [], timelineView: {}, mediaMenu: [], mediaPeek: {}, sceneSections: [], clipTags: [], has: new Set(), actions: [], inputHooks: [], beforeRun: [], promptPrefix: [], lastRun: { typed: undefined, n: 0 }, runner: {}, project, pipeline, pipelineOwned: link.owns, pipelineWhy: link.why, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, dock: createDock(), maintenance: createMaintenance({ api, flush: async () => { await project.flush(); if (project.unsaved) throw new Error("Your latest edits could not be saved, so nothing was changed. Try again in a moment."); } }), keys: createKeys(), dom: createDom(), peek: createPeek(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
const app = { editorSettings: [], timelineLanes: [], timelineView: {}, monitor: {}, mediaMenu: [], mediaPeek: {}, sceneSections: [], clipTags: [], has: new Set(), actions: [], inputHooks: [], beforeRun: [], promptPrefix: [], lastRun: { typed: undefined, n: 0 }, runner: {}, project, pipeline, pipelineOwned: link.owns, pipelineWhy: link.why, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, dock: createDock(), maintenance: createMaintenance({ api, flush: async () => { await project.flush(); if (project.unsaved) throw new Error("Your latest edits could not be saved, so nothing was changed. Try again in a moment."); } }), keys: createKeys(), dom: createDom(), peek: createPeek(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
// Which backend modules the server has (loaded, even if this model cannot use them). Unknown (server down) = assume all, so nothing is hidden by a failed fetch.
|
||||
const installed = await Promise.race([api.modules(), new Promise((_, no) => setTimeout(() => no(new Error("slow")), 4000))]) // a stalled server must not leave the page blank
|
||||
.then((m) => (Array.isArray(m.modules) ? new Set([...m.modules, ...(m.incompatible || [])].map((x) => x.id)) : null)).catch(() => null);
|
||||
|
||||
@@ -163,4 +163,4 @@
|
||||
.cx-viewer-cover[hidden] { display: none; }
|
||||
.cx-viewer-cover-media { max-inline-size: 100%; max-block-size: 100%; object-fit: contain; }
|
||||
.cx-viewer-cover-audio { inline-size: 80%; }
|
||||
.cx-viewer-cover > .cx-btn { position: absolute; inset-block-start: var(--sp-3); inset-inline-end: var(--sp-3); }
|
||||
.cx-viewer-cover > button { position: absolute; inset-block-start: var(--sp-3); inset-inline-end: var(--sp-3); }
|
||||
|
||||
@@ -99,9 +99,9 @@ body { -webkit-font-smoothing: antialiased; letter-spacing: -.003em; }
|
||||
.cx-viewer-media { border-radius: var(--r-3); }
|
||||
|
||||
/* inspector: related fields share one rounded card */
|
||||
.cx-card { background: var(--elev-2); border-radius: var(--r-5); padding: var(--sp-6); gap: var(--sp-5); }
|
||||
.cx-card .cx-input, .cx-card .cx-textarea, .cx-card .cx-select { background: var(--elev-3); }
|
||||
.cx-card .cx-segmented { background: var(--elev-3); }
|
||||
.cx-fieldcard { background: var(--elev-2); border-radius: var(--r-5); padding: var(--sp-6); gap: var(--sp-5); }
|
||||
.cx-fieldcard .cx-input, .cx-fieldcard .cx-textarea, .cx-fieldcard .cx-select { background: var(--elev-3); }
|
||||
.cx-fieldcard .cx-segmented { background: var(--elev-3); }
|
||||
.cx-panel-body > .cx-eyebrow, .cx-stack > .cx-eyebrow { margin-block-start: var(--sp-4); }
|
||||
|
||||
/* list views read as a grouped card, like a settings list */
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
.cx-grid-pair { grid-template-columns: 1fr 1fr; gap: var(--sp-4); margin-block-start: var(--sp-5); }
|
||||
.cx-grid-cards { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
|
||||
|
||||
.cx-tile { position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-5) var(--sp-6); border: 0; border-radius: var(--r-5); background: var(--elev-2); color: var(--text); text-align: left; cursor: pointer; }
|
||||
.cx-tile { position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-1); padding: var(--sp-5) var(--sp-6); border: 0; border-radius: var(--r-5); background: var(--elev-2); color: var(--text); text-align: left; cursor: pointer; }
|
||||
.cx-tile:hover { background: var(--hover); }
|
||||
.cx-tile-title { font-size: var(--fs-4); font-weight: var(--fw-medium); }
|
||||
.cx-tile-sub { font-size: var(--fs-6); color: var(--muted); }
|
||||
|
||||
.cx-tile-node { min-block-size: 84px; padding: var(--sp-6) var(--sp-6) var(--sp-5); }
|
||||
.cx-tile-node { gap: var(--sp-2); min-block-size: 84px; padding: var(--sp-6) var(--sp-6) var(--sp-5); }
|
||||
.cx-tile-node .cx-tile-title { font-weight: var(--fw-semibold); padding-inline-end: var(--sp-6); }
|
||||
.cx-tile-node .cx-tile-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-inline-size: 100%; }
|
||||
.cx-tile-state { position: absolute; inset-block-start: var(--sp-6); inset-inline-end: var(--sp-6); inline-size: 8px; block-size: 8px; border-radius: var(--r-pill); background: var(--good); }
|
||||
|
||||
@@ -6,7 +6,7 @@ import { setText } from "../internals/text.js";
|
||||
/** Related fields on one rounded card. Takes handles, like a stack. */
|
||||
define("card", "default", ({ children = [], gap = "md" } = {}) => {
|
||||
const stack = lookup("region", "stack")({ children, gap });
|
||||
stack.node.classList.add("cx-card");
|
||||
stack.node.classList.add("cx-fieldcard");
|
||||
return stack;
|
||||
});
|
||||
|
||||
|
||||
@@ -76,3 +76,23 @@ test("a url containing a hash is not a colour", () => {
|
||||
assert.deepEqual(scanSource('export const a = "#abc";', MODULE_RULES).map((v) => v.rule),
|
||||
["hardcoded-colour"], "a real colour is caught");
|
||||
});
|
||||
|
||||
test("the feature rules catch the page, the frame's classes and styling, and let prose and imports through", () => {
|
||||
const bad = `
|
||||
const a = document.createElement("div");
|
||||
window.addEventListener("resize", f);
|
||||
node.classList.add("x"); node.style.left = "1px";
|
||||
q.closest(".cx-panel-head"); el.className = "fp-card";
|
||||
box.innerHTML = "x"; const c = "#ff0066";
|
||||
`;
|
||||
const caught = new Set(scanSource(bad, FEATURE_RULES).map((v) => v.rule));
|
||||
for (const rule of ["document", "reaching-for-globals", "class-manipulation", "inline-style", "class-name", "markup", "hardcoded-colour", "hardcoded-size"]) {
|
||||
assert.ok(caught.has(rule), `not caught: ${rule}`);
|
||||
}
|
||||
const fine = `
|
||||
import { open } from "./window.js";
|
||||
const text = "Check the Log window. Wrong look? Open the document, then close it.";
|
||||
localStorage.setItem("k", "1"); app.dom.onPage("focusin", f);
|
||||
`;
|
||||
assert.deepEqual(scanSource(fine, FEATURE_RULES), []);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// The stage drawn over a picture, the cover over a monitor, the picture's look, and the other pieces added for features.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { setupDom, teardownDom } from "./_dom.js";
|
||||
|
||||
let c;
|
||||
test.before(async () => { setupDom(); ({ composer: c } = await import("../composer.js")); });
|
||||
test.after(() => teardownDom());
|
||||
|
||||
const pointer = (node, type, { x = 0, y = 0 } = {}) =>
|
||||
node.dispatchEvent(new window.PointerEvent(type, { clientX: x, clientY: y, pointerId: 1, button: 0, bubbles: true, cancelable: true }));
|
||||
const rect = (node, r) => { node.getBoundingClientRect = () => ({ left: 0, top: 0, right: r.width, bottom: r.height, ...r }); };
|
||||
|
||||
// A parent holding the picture area (400 x 200 on screen, a 4:2 picture) and the stage.
|
||||
function monitor(handlers = {}) {
|
||||
const parent = document.createElement("div"), area = document.createElement("div");
|
||||
document.body.append(parent);
|
||||
parent.append(area);
|
||||
rect(parent, { width: 400, height: 200 }); rect(area, { width: 400, height: 200 });
|
||||
const stage = c.stage.overlay(handlers);
|
||||
parent.append(stage.node);
|
||||
return { stage, parent, area };
|
||||
}
|
||||
const text = (over = {}) => ({ id: "t1", kind: "text", text: " Hi ", fontSize: 20, x: 0.5, y: 0.5, ...over });
|
||||
|
||||
test("the stage fits the picture's aspect into the room it has and draws sizes at that scale", () => {
|
||||
const { stage } = monitor();
|
||||
assert.equal(stage.fit(200, 100), true);
|
||||
assert.deepEqual(stage.size, { w: 400, h: 200 });
|
||||
stage.setItems([text()]);
|
||||
const item = stage.node.querySelector(".cx-stage-item");
|
||||
assert.equal(item.textContent, "Hi", "text is trimmed, as the render trims it");
|
||||
assert.equal(item.style.fontSize, "40px", "20 picture pixels at a scale of 2");
|
||||
stage.destroy();
|
||||
});
|
||||
|
||||
test("with no visible area there is nothing to fit, so the stage says so", () => {
|
||||
const parent = document.createElement("div");
|
||||
document.body.append(parent);
|
||||
const stage = c.stage.overlay();
|
||||
parent.append(stage.node);
|
||||
assert.equal(stage.fit(200, 100), false);
|
||||
});
|
||||
|
||||
test("dragging an item reports its new place as fractions of the picture, clamped, and busy is true meanwhile", () => {
|
||||
const moved = [];
|
||||
const { stage } = monitor({ onMove: (id, to) => moved.push([id, to]) });
|
||||
stage.fit(200, 100);
|
||||
stage.setItems([text()]);
|
||||
const item = stage.node.querySelector(".cx-stage-item");
|
||||
pointer(item, "pointerdown", { x: 100, y: 100 });
|
||||
assert.equal(stage.busy, true);
|
||||
pointer(item, "pointermove", { x: 200, y: 100 }); // 100 px over a 400 px wide picture: a quarter of it
|
||||
assert.equal(item.style.left, "75%", "shown live while dragging");
|
||||
pointer(item, "pointermove", { x: 5000, y: 100 });
|
||||
pointer(item, "pointerup", { x: 5000, y: 100 });
|
||||
assert.equal(stage.busy, false);
|
||||
assert.deepEqual(moved, [["t1", { x: 1, y: 0.5 }]], "far past the edge stops at the edge");
|
||||
});
|
||||
|
||||
test("the corner handle reports a growth factor, not a style; a cancelled gesture reports nothing but onCancel", () => {
|
||||
const grown = [], cancelled = [];
|
||||
const { stage } = monitor({ onResize: (id, f) => grown.push([id, f]), onCancel: () => cancelled.push(1) });
|
||||
stage.fit(200, 100);
|
||||
stage.setItems([text()]);
|
||||
const item = stage.node.querySelector(".cx-stage-item"), grip = item.querySelector(".cx-stage-grip");
|
||||
rect(item, { width: 100, height: 40 });
|
||||
pointer(grip, "pointerdown", { x: 0, y: 0 });
|
||||
pointer(grip, "pointermove", { x: 50, y: 0 }); // both sides grow: 100 -> 200 wide
|
||||
assert.equal(item.style.fontSize, "80px", "shown live while dragging");
|
||||
pointer(grip, "pointerup", { x: 50, y: 0 });
|
||||
assert.deepEqual(grown, [["t1", 2]]);
|
||||
pointer(grip, "pointerdown", { x: 0, y: 0 });
|
||||
pointer(grip, "pointermove", { x: 10, y: 0 });
|
||||
pointer(grip, "pointercancel", { x: 10, y: 0 });
|
||||
assert.equal(grown.length, 1);
|
||||
assert.equal(cancelled.length, 1);
|
||||
});
|
||||
|
||||
test("a picture item has its width in picture pixels and keeps its aspect unless a height is given", () => {
|
||||
const { stage } = monitor();
|
||||
stage.fit(200, 100);
|
||||
stage.setItems([{ id: "i", kind: "image", src: "/x.png", widthPx: 50, heightPx: null }, { id: "j", kind: "image", src: "/y.png", widthPx: 50, heightPx: 20 }]);
|
||||
const [a, b] = [...stage.node.querySelectorAll("img")];
|
||||
assert.equal(a.style.width, "100px"); assert.equal(a.style.height, "");
|
||||
assert.equal(b.style.height, "40px");
|
||||
});
|
||||
|
||||
test("the cover shows a file over its parent, closes on ✕ or close(), and says so once", () => {
|
||||
let closed = 0;
|
||||
const cover = c.viewer.cover({ onClose: () => { closed += 1; } });
|
||||
document.body.append(cover.node);
|
||||
assert.equal(cover.isOpen, false);
|
||||
cover.show({ src: "/a.mp4", kind: "video" });
|
||||
assert.equal(cover.isOpen, true);
|
||||
assert.equal(cover.node.querySelector("video").hasAttribute("controls"), true);
|
||||
cover.node.querySelector("button").click();
|
||||
assert.equal(cover.isOpen, false); assert.equal(closed, 1);
|
||||
cover.close();
|
||||
assert.equal(closed, 1, "closing what is shut is not a close");
|
||||
cover.show({ src: "/s.wav", kind: "audio" });
|
||||
assert.ok(cover.node.querySelector("audio"));
|
||||
cover.show({});
|
||||
assert.equal(cover.isOpen, false, "nothing to show, nothing shown");
|
||||
});
|
||||
|
||||
test("the viewer keeps its look across a change of source", () => {
|
||||
const v = c.viewer.media({ kind: "video" });
|
||||
v.setSource("/a.mp4", "video");
|
||||
v.setLook({ transform: "scale(1.5)", filter: "blur(2px)", fit: "cover", opacity: 0.5 });
|
||||
const style = () => v.element.style;
|
||||
assert.deepEqual([style().transform, style().filter, style().objectFit, style().opacity], ["scale(1.5)", "blur(2px)", "cover", "0.5"]);
|
||||
v.setSource("/b.mp4", "video");
|
||||
assert.equal(style().transform, "scale(1.5)", "the new picture is drawn the same way");
|
||||
v.setLook(null);
|
||||
assert.equal(style().transform, "");
|
||||
});
|
||||
|
||||
test("a slider whose top changes draws its fill against the new top", () => {
|
||||
const s = c.slider.sm({ label: "Position", min: 0, max: 1, step: 0.01 });
|
||||
s.setMax(10); s.setValue(5);
|
||||
assert.equal(s.input.style.getPropertyValue("--fill"), "50%");
|
||||
});
|
||||
|
||||
test("a corner button follows its box, and goes (once) when the box does", () => {
|
||||
const box = document.createElement("textarea");
|
||||
document.body.append(box);
|
||||
rect(box, { left: 100, right: 400, top: 200, bottom: 300, width: 300, height: 100 });
|
||||
let gone = 0;
|
||||
const corner = c.floating.corner({ anchor: box, label: "💡", onGone: () => { gone += 1; } });
|
||||
rect(corner.node, { width: 24, height: 20 });
|
||||
return new Promise((resolve) => setTimeout(() => {
|
||||
assert.deepEqual([corner.node.style.left, corner.node.style.top], ["360px", "276px"]);
|
||||
box.remove();
|
||||
setTimeout(() => {
|
||||
assert.equal(gone, 1);
|
||||
assert.equal(corner.node.isConnected, false);
|
||||
resolve();
|
||||
}, 60);
|
||||
}, 60));
|
||||
});
|
||||
|
||||
test("tiles carry a state, refuse an unknown one, and say what the dot means", () => {
|
||||
const ready = c.tile.node({ title: "Load", sub: "m.safetensors" });
|
||||
assert.ok(ready.node.querySelector(".cx-tile-state"));
|
||||
assert.equal(ready.node.querySelector(".cx-tile-state").title, "Ready");
|
||||
const needs = c.tile.node({ title: "Load", sub: "", state: "needs" });
|
||||
assert.equal(needs.node.querySelector(".cx-tile-state").title, "Needs attention");
|
||||
assert.throws(() => c.tile.node({ title: "x", state: "bogus" }), /Unknown tile state/);
|
||||
let hit = 0;
|
||||
c.tile.action({ title: "Go", sub: "now", onClick: () => { hit += 1; } }).node.click();
|
||||
assert.equal(hit, 1);
|
||||
});
|
||||
|
||||
test("a hero, an inline progress and a settings row's hint can be changed after the fact", () => {
|
||||
const row = c.settingsRow.default({ label: "Update", hint: "Checking…", control: c.button.sm({ label: "Update" }) });
|
||||
row.setHint("Up to date");
|
||||
assert.match(row.node.textContent, /Up to date/);
|
||||
const bar = c.progress.inline({ label: "Generating" });
|
||||
bar.setText("Step 1/2"); bar.setValue(50); bar.setHidden(true);
|
||||
assert.equal(bar.node.hidden, true); assert.match(bar.node.textContent, /Step 1\/2/);
|
||||
assert.match(c.hero.default({ title: "FunPack 5", hint: "Cutting Room" }).node.textContent, /FunPack 5.*Cutting Room/);
|
||||
});
|
||||
@@ -37,6 +37,7 @@ export default {
|
||||
const p = app.project, head = app.playhead;
|
||||
function draw() {
|
||||
const open = p.project;
|
||||
if (!app.monitor.look) return; // no monitor on screen: nothing to draw on
|
||||
const seg = open && segments(open).find((s) => s.kind === "scene" && head.at >= s.start - 1e-6 && head.at < s.start + s.dur - 1e-6);
|
||||
const st = fxStyle(seg ? seg.scene.effects : {}, seg ? head.at - seg.start : 0, seg ? seg.dur : 0, seg ? num(seg.scene.fps_mode === "project" ? open.frame_rate : seg.scene.fps, 25) || 25 : 25);
|
||||
app.monitor.look({ transform: st.transform, filter: st.filter, fit: st.objectFit, opacity: st.opacity === "" ? null : st.opacity });
|
||||
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
if (s && !(head.at >= s.start && head.at < s.start + s.dur)) head.set(s.start); else sync();
|
||||
};
|
||||
sync();
|
||||
app.monitor = { look: (look) => viewer.setLook(look) }; // the effects feature draws on the picture through this
|
||||
app.monitor.look = (look) => viewer.setLook(look); // the effects feature draws on the picture through this
|
||||
const off = [head.on(sync), app.on((what) => {
|
||||
if (what === "select") onSelect();
|
||||
else if (what === "play.toggle") (playing ? pause() : start());
|
||||
@@ -138,6 +138,6 @@ export default {
|
||||
else if (what === "open") { pause(); head.set(0); } // another project: nothing carries over
|
||||
else sync();
|
||||
})];
|
||||
return () => { clearInterval(walk); off.forEach((f) => f()); delete app.monitor; viewer.destroy(); };
|
||||
return () => { clearInterval(walk); off.forEach((f) => f()); delete app.monitor.look; viewer.destroy(); };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,3 +18,14 @@ test("a bin tile dropped on a matching target reaches the handler; files and oth
|
||||
off();
|
||||
assert.deepEqual(Object.keys(doc.l), []);
|
||||
});
|
||||
|
||||
test("a target can be named, and the handler is told which clip is under the drop and which row the target is", () => {
|
||||
const lanes = [{}, {}, {}], clip = { dataset: { id: "s3" } };
|
||||
const doc = { ...fake(), querySelectorAll: () => lanes };
|
||||
const got = [];
|
||||
onMediaDrop("lane:video", (item, el, e, where) => got.push([el === lanes[1], where.clipId, where.row]), doc);
|
||||
const e = ev({ id: "m", kind: "image" }, lanes[1]);
|
||||
e.target.closest = (sel) => (sel.includes("cx-nle-clip") ? clip : lanes[1]);
|
||||
doc.l.drop(e);
|
||||
assert.deepEqual(got, [[true, "s3", 1]]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { setupDom, teardownDom } from "../../composer/tests/_dom.js";
|
||||
|
||||
let createDom;
|
||||
test.before(async () => { setupDom(); ({ createDom } = await import("../dom.js")); });
|
||||
test.after(() => teardownDom());
|
||||
|
||||
test("typingIn is true only for a text field inside the node that has the caret", () => {
|
||||
document.body.innerHTML = `<div id="a"><input id="i"><button id="b"></button></div><textarea id="t"></textarea>`;
|
||||
const dom = createDom(), a = document.getElementById("a");
|
||||
assert.equal(dom.typingIn(a), false);
|
||||
document.getElementById("i").focus();
|
||||
assert.equal(dom.typingIn(a), true);
|
||||
assert.equal(dom.typingIn(a, "TEXTAREA"), false, "narrowed to one tag");
|
||||
document.getElementById("t").focus();
|
||||
assert.equal(dom.typingIn(a), false, "a field outside is not inside");
|
||||
assert.equal(dom.focusOn(document.getElementById("t")), true);
|
||||
document.getElementById("b").focus();
|
||||
assert.equal(dom.holdsFocus(a), true);
|
||||
dom.blur();
|
||||
assert.equal(dom.holdsFocus(a), false);
|
||||
});
|
||||
|
||||
test("listeners come back with their own off switch", () => {
|
||||
const dom = createDom(); let n = 0;
|
||||
const off = dom.onPage("pointerdown", () => { n += 1; });
|
||||
document.body.dispatchEvent(new window.Event("pointerdown", { bubbles: true }));
|
||||
off();
|
||||
document.body.dispatchEvent(new window.Event("pointerdown", { bubbles: true }));
|
||||
assert.equal(n, 1);
|
||||
});
|
||||
|
||||
test("a dialog on screen is reported, and a landmark is found by name", () => {
|
||||
document.body.innerHTML = `<header class="cx-frame-head"></header><main class="cx-frame-main"><section class="cx-zone"></section><section class="cx-zone"></section><button> ▶ Generate </button></main>`;
|
||||
const dom = createDom();
|
||||
assert.equal(dom.dialogOpen(), false);
|
||||
document.body.insertAdjacentHTML("beforeend", `<div role="dialog"></div>`);
|
||||
assert.equal(dom.dialogOpen(), true);
|
||||
assert.equal(dom.landmark("menubar").className, "cx-frame-head");
|
||||
assert.equal(dom.landmark("zone:1"), document.querySelectorAll("section.cx-zone")[1]);
|
||||
assert.equal(dom.landmark("button:▶ Generate").tagName, "BUTTON");
|
||||
assert.equal(dom.landmark("zone:9"), null);
|
||||
assert.equal(dom.landmark("nonsense"), null);
|
||||
});
|
||||
|
||||
test("a download is a click on a link with the file's name", () => {
|
||||
const dom = createDom(), seen = [];
|
||||
const click = window.HTMLAnchorElement.prototype.click;
|
||||
window.HTMLAnchorElement.prototype.click = function () { seen.push([this.getAttribute("href"), this.download]); };
|
||||
try { dom.download("/funpack/api/x", "x.json"); } finally { window.HTMLAnchorElement.prototype.click = click; }
|
||||
assert.deepEqual(seen, [["/funpack/api/x", "x.json"]]);
|
||||
});
|
||||
|
||||
test("a file chooser that is cancelled gives null, and one that is answered gives the file", async () => {
|
||||
const dom = createDom();
|
||||
const click = window.HTMLInputElement.prototype.click;
|
||||
let input;
|
||||
window.HTMLInputElement.prototype.click = function () { input = this; };
|
||||
try {
|
||||
const cancelled = dom.pickFile(".json"); input.oncancel();
|
||||
assert.equal(await cancelled, null);
|
||||
assert.equal(input.accept, ".json");
|
||||
const picked = dom.pickFile(".json");
|
||||
Object.defineProperty(input, "files", { value: [{ name: "a.json" }] });
|
||||
input.onchange();
|
||||
assert.deepEqual(await picked, { name: "a.json" });
|
||||
} finally { window.HTMLInputElement.prototype.click = click; }
|
||||
});
|
||||
|
||||
test("selectingIn is true only while text inside the node is selected", () => {
|
||||
document.body.innerHTML = `<pre id="p">some log text</pre><p id="q">elsewhere</p>`;
|
||||
const dom = createDom(), p = document.getElementById("p");
|
||||
assert.equal(dom.selectingIn(p), false);
|
||||
const range = document.createRange(); range.selectNodeContents(p.firstChild);
|
||||
const sel = document.getSelection(); sel.removeAllRanges(); sel.addRange(range);
|
||||
assert.equal(dom.selectingIn(p), true);
|
||||
assert.equal(dom.selectingIn(document.getElementById("q")), false);
|
||||
});
|
||||
Reference in New Issue
Block a user