feat: init
This commit is contained in:
+203
@@ -0,0 +1,203 @@
|
||||
import { mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import esbuild from "esbuild";
|
||||
import sveltePlugin from "esbuild-svelte";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
const root = new URL("..", import.meta.url).pathname;
|
||||
const fixtures = join(root, "tests/fixtures");
|
||||
const FOLDER = "Boards";
|
||||
|
||||
const out = join(mkdtempSync(join(tmpdir(), "bcal-")), "bundle.mjs");
|
||||
writeFileSync(
|
||||
join(root, "tests/.entry.ts"),
|
||||
`export { default as App } from "../src/ui/app.svelte";
|
||||
export { BoardStore } from "../src/vault/store.svelte";
|
||||
export { mount, unmount, flushSync } from "svelte";
|
||||
export { TFile, Component } from "obsidian";
|
||||
`,
|
||||
);
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: [join(root, "tests/.entry.ts")],
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
target: "es2022",
|
||||
outfile: out,
|
||||
logLevel: "error",
|
||||
conditions: ["svelte", "browser"],
|
||||
mainFields: ["svelte", "browser", "module", "main"],
|
||||
alias: { obsidian: join(root, "tests/obsidian-stub.ts") },
|
||||
plugins: [sveltePlugin({ compilerOptions: { css: "injected", runes: true } })],
|
||||
});
|
||||
|
||||
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
|
||||
pretendToBeVisual: true,
|
||||
url: "http://localhost/",
|
||||
});
|
||||
for (const key of Object.getOwnPropertyNames(dom.window)) {
|
||||
if (key.startsWith("_")) continue;
|
||||
if (globalThis[key] !== undefined) continue;
|
||||
try {
|
||||
globalThis[key] = dom.window[key];
|
||||
} catch {
|
||||
/* getter-only window properties are fine to skip */
|
||||
}
|
||||
}
|
||||
globalThis.window = dom.window;
|
||||
globalThis.document = dom.window.document;
|
||||
dom.window.Element.prototype.getBoundingClientRect = () => ({
|
||||
bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0,
|
||||
toJSON: () => ({}),
|
||||
});
|
||||
dom.window.Element.prototype.setPointerCapture = () => {};
|
||||
dom.window.Element.prototype.releasePointerCapture = () => {};
|
||||
dom.window.Element.prototype.hasPointerCapture = () => false;
|
||||
dom.window.Element.prototype.scrollIntoView = () => {};
|
||||
globalThis.ResizeObserver = class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
const { App, BoardStore, mount, unmount, flushSync, TFile, Component } =
|
||||
await import(pathToFileURL(out).href);
|
||||
|
||||
const names = readdirSync(fixtures).filter((name) => name.endsWith(".md"));
|
||||
const files = new Map(
|
||||
names.map((name) => [
|
||||
`${FOLDER}/${name}`,
|
||||
readFileSync(join(fixtures, name), "utf8"),
|
||||
]),
|
||||
);
|
||||
const handles = new Map([...files.keys()].map((path) => [path, new TFile(path)]));
|
||||
|
||||
const writes = [];
|
||||
const app = {
|
||||
vault: {
|
||||
getMarkdownFiles: () => [...handles.values()],
|
||||
getAbstractFileByPath: (path) => handles.get(path) ?? null,
|
||||
cachedRead: async (file) => files.get(file.path),
|
||||
read: async (file) => files.get(file.path),
|
||||
process: async (file, fn) => {
|
||||
const next = fn(files.get(file.path));
|
||||
writes.push({ path: file.path, content: next });
|
||||
files.set(file.path, next);
|
||||
return next;
|
||||
},
|
||||
on: () => ({}),
|
||||
},
|
||||
workspace: { getLeaf: () => ({}), activeEditor: null },
|
||||
plugins: { plugins: {} },
|
||||
};
|
||||
|
||||
const settings = {
|
||||
boardsFolder: FOLDER,
|
||||
archiveHeading: "Archive",
|
||||
view: "list",
|
||||
groupBy: "sphere",
|
||||
listSort: "due",
|
||||
calendarSort: "priority",
|
||||
showDone: false,
|
||||
includeArchive: false,
|
||||
railCollapsed: false,
|
||||
undatedCollapsed: false,
|
||||
};
|
||||
|
||||
const failures = [];
|
||||
function check(name, condition, detail = "") {
|
||||
if (condition) console.log(` ok ${name}`);
|
||||
else {
|
||||
failures.push(name);
|
||||
console.log(` FAIL ${name}${detail ? ` -- ${detail}` : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 60));
|
||||
|
||||
const store = new BoardStore(app, settings, () => {});
|
||||
await store.reloadAll();
|
||||
|
||||
// Every task outside the archive, done ones included; the done filter
|
||||
// applies to `visible`, not to the pool.
|
||||
const outsideArchive = 15;
|
||||
check("parsed every board", store.spheres.length === names.length, `${store.spheres.length}`);
|
||||
check(`${outsideArchive} tasks outside the archive`, store.all.length === outsideArchive, `got ${store.all.length}`);
|
||||
|
||||
const target = dom.window.document.body;
|
||||
const instance = mount(App, {
|
||||
target,
|
||||
props: { app, store, settings, component: new Component() },
|
||||
});
|
||||
flushSync();
|
||||
await settle();
|
||||
flushSync();
|
||||
|
||||
const text = () => target.textContent ?? "";
|
||||
check("list rendered a task", text().includes("renew the gym membership"));
|
||||
check("sphere rail rendered", text().includes("All spheres"));
|
||||
check("group header rendered", text().includes("personal"));
|
||||
check("project chip rendered", text().includes("errands"));
|
||||
check("no crash markers", !text().includes("undefined"));
|
||||
|
||||
const links = target.querySelectorAll("a.internal-link");
|
||||
check("wikilink rendered as a link", links.length > 0, `${links.length} links`);
|
||||
check("alias is displayed, not the target", text().includes("Alex"));
|
||||
check("no raw wikilink syntax leaked", !text().includes("[["));
|
||||
|
||||
store.includeArchive = true;
|
||||
store.showDone = true;
|
||||
flushSync();
|
||||
await settle();
|
||||
check("archive appears when both toggles are on", text().includes("pick up the parcel"));
|
||||
check("archived task carries the Archive badge", text().includes("Archive"));
|
||||
|
||||
store.includeArchive = false;
|
||||
flushSync();
|
||||
check("archive hidden again", !text().includes("pick up the parcel"));
|
||||
store.showDone = false;
|
||||
flushSync();
|
||||
|
||||
store.view = "calendar";
|
||||
flushSync();
|
||||
await settle();
|
||||
check("calendar rendered a month label", /\d{4}/.test(store.monthLabel));
|
||||
check("calendar rendered the undated tray", text().includes("No date"));
|
||||
|
||||
store.view = "list";
|
||||
store.groupBy = "due";
|
||||
flushSync();
|
||||
const BUCKETS = ["Overdue", "Today", "Tomorrow", "This week", "Later", "No date"];
|
||||
check(
|
||||
"due grouping produced known buckets",
|
||||
store.groups.length > 0 && store.groups.every((g) => BUCKETS.includes(g.label)),
|
||||
store.groups.map((g) => g.label).join(", "),
|
||||
);
|
||||
|
||||
const button = target.querySelector('button[aria-label^="Status"]');
|
||||
check("status button present", button !== null);
|
||||
if (button) {
|
||||
button.dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true }));
|
||||
await settle();
|
||||
check("clicking status wrote to the vault", writes.length > 0, `${writes.length}`);
|
||||
if (writes.length > 0) {
|
||||
const after = writes[writes.length - 1];
|
||||
const before = readFileSync(
|
||||
join(fixtures, after.path.slice(FOLDER.length + 1)),
|
||||
"utf8",
|
||||
).split("\n");
|
||||
const now = after.content.split("\n");
|
||||
const diff = now.filter((line, index) => line !== before[index]);
|
||||
check("exactly one line changed", diff.length === 1, JSON.stringify(diff));
|
||||
check("line is now done", diff[0]?.startsWith("- [x] "), diff[0]);
|
||||
check("done date stamped", /✅ \d{4}-\d{2}-\d{2}$/u.test(diff[0] ?? ""), diff[0]);
|
||||
check("settings block intact", after.content.includes("%% kanban:settings"));
|
||||
}
|
||||
}
|
||||
|
||||
await unmount(instance);
|
||||
|
||||
console.log(failures.length === 0 ? "\nsmoke: all good" : `\nsmoke: ${failures.length} FAILED`);
|
||||
process.exit(failures.length === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user