228 lines
9.3 KiB
JavaScript
228 lines
9.3 KiB
JavaScript
// publish/utilities.js (word count, tabs, form checks) and utilities.js (the phone menu)
|
|
"use strict";
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { load, STORY_FORM } = require("./helpers");
|
|
|
|
const FORM = STORY_FORM + `
|
|
<input type="hidden" id="remove_img" value="0">
|
|
<img id="remove_img_icon" src="/graphics/trashcan.png">`;
|
|
|
|
function publishWindow(story = "") {
|
|
const window = load(["publish/utilities.js"], FORM, story);
|
|
window.alerts = [];
|
|
window.confirms = [];
|
|
window.confirmAnswer = true;
|
|
window.alert = (message) => window.alerts.push(message);
|
|
window.confirm = (message) => {
|
|
window.confirms.push(message);
|
|
return window.confirmAnswer;
|
|
};
|
|
return window;
|
|
}
|
|
|
|
const window = publishWindow();
|
|
|
|
test("wc counts words", () => {
|
|
assert.equal(window.wc(""), 0);
|
|
assert.equal(window.wc(" "), 0);
|
|
assert.equal(window.wc("one"), 1);
|
|
assert.equal(window.wc("one two three"), 3);
|
|
assert.equal(window.wc(" padded words "), 2);
|
|
});
|
|
|
|
test("wc leaves out the HTML", () => {
|
|
assert.equal(window.wc("<p>one <strong>two</strong></p>\n<p>three</p>"), 3);
|
|
assert.equal(window.wc("one<br />two<br>three"), 3);
|
|
assert.equal(window.wc("<p> </p>"), 0);
|
|
assert.equal(window.wc('<a href="https://example.org/a b c">link</a>'), 1);
|
|
});
|
|
|
|
test("wc counts the words around a dash as two", () => {
|
|
assert.equal(window.wc("left--right"), 2);
|
|
assert.equal(window.wc("left — right"), 2);
|
|
assert.equal(window.wc("left–right"), 2);
|
|
assert.equal(window.wc("well-known"), 1);
|
|
});
|
|
|
|
test("wc does not count punctuation on its own", () => {
|
|
assert.equal(window.wc("one - two ... three"), 3);
|
|
});
|
|
|
|
test("wc counts one hundred words", () => {
|
|
assert.equal(window.wc("<p>" + "word ".repeat(100) + "</p>"), 100);
|
|
});
|
|
|
|
test("the word counter shows the count and whether it is 100", () => {
|
|
const w = publishWindow();
|
|
const story = w.document.getElementById("story");
|
|
const counter = w.document.getElementById("wordcounter");
|
|
story.value = "one";
|
|
assert.equal(w.updateWordCount(), true);
|
|
assert.equal(counter.innerHTML, "1 word");
|
|
assert.equal(counter.className, "counter");
|
|
story.value = "word ".repeat(100);
|
|
w.updateWordCount();
|
|
assert.equal(counter.innerHTML, "100 words");
|
|
assert.equal(counter.className, "counter yes");
|
|
story.value = "word ".repeat(101);
|
|
w.updateWordCount();
|
|
assert.equal(counter.className, "counter no");
|
|
});
|
|
|
|
test("the word counter does nothing without its elements", () => {
|
|
const w = load(["publish/utilities.js"], "");
|
|
assert.equal(w.updateWordCount(), true);
|
|
});
|
|
|
|
test("showHide switches between the text and the image tabs", () => {
|
|
const w = publishWindow();
|
|
const $ = (id) => w.document.getElementById(id);
|
|
let focused = 0;
|
|
w.storyEditor = { focus: () => focused++ };
|
|
assert.equal(w.showHide("upload"), true);
|
|
assert.deepEqual([$("textparent").hidden, $("uploadparent").hidden, $("wordcounter").hidden], [true, false, true]);
|
|
assert.equal(w.showHide("text"), true);
|
|
assert.deepEqual([$("textparent").hidden, $("uploadparent").hidden, $("wordcounter").hidden], [false, true, false]);
|
|
assert.equal(focused, 1);
|
|
assert.equal(w.showHide("other"), false);
|
|
});
|
|
|
|
test("showHide marks the tab that is showing", () => {
|
|
const w = publishWindow();
|
|
const text = w.document.getElementById("texttab");
|
|
const upload = w.document.getElementById("uploadtab");
|
|
w.showHide("upload");
|
|
assert.deepEqual([text.classList.contains("active"), upload.classList.contains("active")], [false, true]);
|
|
w.showHide("text");
|
|
assert.deepEqual([text.classList.contains("active"), upload.classList.contains("active")], [true, false]);
|
|
});
|
|
|
|
test("review asks for a title, then an author", () => {
|
|
const w = publishWindow("<p>words</p>");
|
|
assert.equal(w.review(), false);
|
|
assert.match(w.alerts[0], /add a title/);
|
|
w.document.getElementById("title").value = "T";
|
|
assert.equal(w.review(), false);
|
|
assert.match(w.alerts[1], /haven't added an author/);
|
|
});
|
|
|
|
test("review asks about a missing email address", () => {
|
|
const w = publishWindow("word ".repeat(100));
|
|
w.document.getElementById("title").value = "T";
|
|
w.document.getElementById("author").value = "A";
|
|
w.confirmAnswer = false;
|
|
assert.equal(w.review(), false);
|
|
assert.match(w.confirms[0], /haven't added an email address/);
|
|
w.confirmAnswer = true;
|
|
assert.equal(w.review(), true);
|
|
});
|
|
|
|
test("review stops an empty story and warns about the length", () => {
|
|
const w = publishWindow("");
|
|
for (const [id, value] of [["title", "T"], ["author", "A"], ["email", "a@example.org"]])
|
|
w.document.getElementById(id).value = value;
|
|
assert.equal(w.review(), false);
|
|
assert.match(w.alerts[0], /haven't submitted anything/);
|
|
|
|
w.document.getElementById("story").value = "<p>only a few words</p>";
|
|
w.confirmAnswer = false;
|
|
assert.equal(w.review(), false);
|
|
assert.match(w.confirms[0], /doesn't appear to be 100 words long/);
|
|
w.confirmAnswer = true;
|
|
assert.equal(w.review(), true);
|
|
|
|
w.confirms.length = 0;
|
|
w.document.getElementById("story").value = "word ".repeat(100);
|
|
assert.equal(w.review(), true);
|
|
assert.deepEqual(w.confirms, []);
|
|
});
|
|
|
|
const imageReviews = [
|
|
{ name: "a new upload with no text", upload: true },
|
|
{ name: "a new upload with short text", upload: true, story: "short text" },
|
|
{ name: "a retained image with no text", uploaded: "5" },
|
|
{ name: "a retained image with short text", uploaded: "5", story: "short text" },
|
|
{ name: "a pending image with no text", uploaded: "pending" },
|
|
{ name: "a pending image with short text", uploaded: "pending", story: "short text" },
|
|
{ name: "an image removed without text", uploaded: "5", remove: true, alert: true, accepted: false },
|
|
{ name: "a removed image and cancelled short text", uploaded: "5", remove: true, story: "short text", confirm: true, accepted: false },
|
|
{ name: "a removed image and accepted short text", uploaded: "5", remove: true, story: "short text", confirm: true },
|
|
{ name: "an image replaced with 100 words", uploaded: "5", remove: true, story: "word ".repeat(100) },
|
|
{ name: "an empty image reference and no text", uploaded: "", alert: true, accepted: false },
|
|
{ name: "a new upload replacing a removed image", upload: true, uploaded: "5", remove: true },
|
|
];
|
|
|
|
for (const entry of imageReviews) {
|
|
test(`review handles ${entry.name}`, (t) => {
|
|
const w = publishWindow(entry.story || "");
|
|
t.after(() => w.close());
|
|
for (const [id, value] of [["title", "T"], ["author", "A"], ["email", "a@example.org"]])
|
|
w.document.getElementById(id).value = value;
|
|
if (entry.upload) {
|
|
// jsdom cannot choose a file. review() reads only the input's value.
|
|
Object.defineProperty(w.document.getElementById("upload"), "value", { value: "C:\\fakepath\\a.png" });
|
|
}
|
|
if (entry.uploaded !== undefined) {
|
|
const uploaded = w.document.createElement("input");
|
|
uploaded.type = "hidden";
|
|
uploaded.id = "uploaded";
|
|
uploaded.value = entry.uploaded;
|
|
w.document.getElementById("form").appendChild(uploaded);
|
|
}
|
|
if (entry.remove) w.toggle_remove();
|
|
w.confirmAnswer = entry.accepted !== false;
|
|
assert.equal(w.review(), entry.accepted !== false);
|
|
assert.equal(w.alerts.length, entry.alert ? 1 : 0);
|
|
assert.equal(w.confirms.length, entry.confirm ? 1 : 0);
|
|
if (entry.alert) assert.match(w.alerts[0], /haven't submitted anything/);
|
|
if (entry.confirm) assert.match(w.confirms[0], /doesn't appear to be 100 words long/);
|
|
});
|
|
}
|
|
|
|
test("delete asks first, and then skips the review", () => {
|
|
const w = publishWindow("");
|
|
w.confirmAnswer = false;
|
|
assert.equal(w.do_delete(), false);
|
|
assert.equal(w.eval("is_delete"), 0);
|
|
w.confirmAnswer = true;
|
|
assert.equal(w.do_delete(), true);
|
|
assert.equal(w.review(), true);
|
|
assert.deepEqual(w.alerts, []);
|
|
});
|
|
|
|
test("toggle_remove marks the image for removal and back", () => {
|
|
const w = publishWindow();
|
|
const flag = w.document.getElementById("remove_img");
|
|
const icon = w.document.getElementById("remove_img_icon");
|
|
w.toggle_remove();
|
|
assert.equal(flag.value, "1");
|
|
assert.equal(icon.getAttribute("src"), "/graphics/trashcan-open.png");
|
|
assert.equal(icon.title, "Image is marked for removal.");
|
|
w.toggle_remove();
|
|
assert.equal(flag.value, "0");
|
|
assert.equal(icon.getAttribute("src"), "/graphics/trashcan.png");
|
|
assert.equal(icon.title, "Image has been restored; it will not be removed.");
|
|
});
|
|
|
|
test("the phone menu closes on Escape and on a click outside it", () => {
|
|
const w = load(["utilities.js"], `
|
|
<aside id="sidebar"><input class="mobile menu-toggle" type="checkbox"><a id="inside" href="#">x</a></aside>
|
|
<main id="outside">text</main>`);
|
|
w.document.dispatchEvent(new w.Event("DOMContentLoaded"));
|
|
const toggle = w.document.querySelector("input.menu-toggle");
|
|
|
|
toggle.checked = true;
|
|
w.document.getElementById("inside").dispatchEvent(new w.MouseEvent("click", { bubbles: true }));
|
|
assert.equal(toggle.checked, true, "a click in the menu keeps it open");
|
|
w.document.getElementById("outside").dispatchEvent(new w.MouseEvent("click", { bubbles: true }));
|
|
assert.equal(toggle.checked, false);
|
|
|
|
toggle.checked = true;
|
|
w.document.dispatchEvent(new w.KeyboardEvent("keyup", { key: "a", keyCode: 65, bubbles: true }));
|
|
assert.equal(toggle.checked, true);
|
|
w.document.dispatchEvent(new w.KeyboardEvent("keyup", { key: "Escape", keyCode: 27, bubbles: true }));
|
|
assert.equal(toggle.checked, false);
|
|
});
|