Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
98b423c28This file is pinned to the selected release. Download the original to open it in your design tool.
/**
* "Edit my published pattern" smoke test — `npm run check:labedit`.
*
* The round trip that makes revising a post possible instead of deleting and
* reposting it: the community page hands the lab an `edit` reference, the lab
* carries it as `editOf`, and Share turns into Update. Three ways that breaks
* quietly, all of them costing somebody their post:
*
* - the reference is dropped in transit (handoff) or on reload (persistence),
* so Update silently reverts to publishing a NEW pattern
* - fork and edit end up set at once, and the same Share both forks and
* updates
* - parking the opened version clears the canvas, taking the edit with it
*
* Both modules are browser-only by design (sessionStorage, localStorage), so
* the storage they reach for is stubbed here rather than mocked away.
*/
// Only dynamic imports below (the stub has to be in place first), so this
// needs a marker to be a MODULE — without it every top-level name here lands
// in the global scope and collides with the DOM lib and the other scripts.
export {};
class MemStorage {
private map = new Map<string, string>();
failWrites = false;
maxWriteChars = Infinity;
getItem(key: string): string | null {
return this.map.has(key) ? (this.map.get(key) as string) : null;
}
setItem(key: string, value: string): void {
if (this.failWrites || value.length > this.maxWriteChars) throw new Error("QuotaExceededError");
this.map.set(key, String(value));
}
removeItem(key: string): void {
this.map.delete(key);
}
clear(): void {
this.map.clear();
}
}
const sessionStore = new MemStorage();
const localStore = new MemStorage();
Object.assign(globalThis, {
window: {
sessionStorage: sessionStore,
localStorage: localStore,
addEventListener: () => undefined,
removeEventListener: () => undefined,
},
localStorage: localStore,
sessionStorage: sessionStore,
});
let failures = 0;
function check(label: string, actual: unknown, expected: unknown) {
const ok = JSON.stringify(actual) === JSON.stringify(expected);
if (!ok) failures += 1;
console.log(
`${ok ? " ok " : "FAIL "}${label}${ok ? "" : `\n got ${JSON.stringify(actual)}\n want ${JSON.stringify(expected)}`}`,
);
}
const EDIT = {
id: "p-mine",
title: "My Pattern",
description: "as published",
visibility: "public",
hasCpp: true,
portCount: 2,
};
async function main() {
const { readLabHandoff, writeLabHandoff, clearLabHandoff } = await import(
"../src/lib/community/handoff"
);
const { deserializeProject, serializeProject, saveProject } = await import("../src/lib/lab/serialize");
const { LAB_STORAGE } = await import("../src/lib/lab/persist");
const { emptyShow } = await import("../src/lib/lab/director/types");
const { codeLayerFromSource, useLabStore, saveLabProjectNow } = await import("../src/lib/lab/store");
const { listSessions, stashSession } = await import("../src/lib/lab/sessions");
console.log("\n── the community → lab handoff ──");
writeLabHandoff({
code: "// js",
parentId: null,
parentTitle: null,
parentLicense: null,
edit: EDIT,
});
check("an owner's open carries the whole edit reference", readLabHandoff()?.edit, EDIT);
check("and no fork lineage with it", readLabHandoff()?.parentId, null);
writeLabHandoff({
code: "// js",
parentId: "p-theirs",
parentTitle: "Theirs",
parentLicense: "CC-BY-4.0",
edit: null,
});
check("a stranger's open still forks", readLabHandoff()?.parentId, "p-theirs");
check("and carries no edit reference", readLabHandoff()?.edit, null);
// Handoffs written by the build before in-place editing existed have no
// `edit` key at all — they must read as forks, not crash the lab on boot.
sessionStore.setItem(
"patternflow.community.labHandoff",
JSON.stringify({ code: "// js", parentId: "p-old", parentTitle: "Old", parentLicense: null }),
);
check("an older handoff reads as a plain fork", readLabHandoff()?.edit, null);
// Anything can be in the slot; a broken reference loses edit mode, nothing more.
sessionStore.setItem(
"patternflow.community.labHandoff",
JSON.stringify({ code: "// js", edit: { title: "no id" } }),
);
check("a malformed edit reference is refused", readLabHandoff()?.edit, null);
clearLabHandoff();
check("clearing leaves nothing behind", readLabHandoff(), null);
console.log("\n── it survives a reload ──");
const layer = codeLayerFromSource("// pattern", "Code 1").layer;
const project = {
matrix: { width: 128, height: 64 },
layers: [layer],
activeLayerId: layer.id,
knobs: [0.5, 0.5, 0.5, 0.5],
ranges: [[0, 1], [0, 1], [0, 1], [0, 1]] as [number, number][],
knobLabels: ["A", "B", "C", "D"],
forkOf: null,
editOf: EDIT,
gen: { count: 5, thinking: "LOW" as const, refs: 6, colorMode: "vfield" as const },
director: emptyShow(),
name: "Edit Smoke",
};
const json = serializeProject(project);
check("the project serializes", typeof json, "string");
check("and comes back still editing that post", deserializeProject(json!)?.editOf, EDIT);
const legacy = JSON.parse(json!) as Record<string, unknown>;
delete legacy.editOf;
check(
"a project saved before edit mode restores as a draft",
deserializeProject(JSON.stringify(legacy))?.editOf,
null,
);
const broken = JSON.parse(json!) as Record<string, unknown>;
broken.editOf = { id: 5, title: null };
check(
"a corrupted reference restores as a draft too",
deserializeProject(JSON.stringify(broken))?.editOf,
null,
);
console.log("\n── fork and edit are mutually exclusive ──");
const store = useLabStore.getState();
store.setEditOf(EDIT);
check("editing is set", useLabStore.getState().editOf?.id, "p-mine");
store.setForkOf({ id: "p-theirs", title: "Theirs", license: null });
check("forking replaces it", useLabStore.getState().editOf, null);
check("and the fork stands", useLabStore.getState().forkOf?.id, "p-theirs");
store.setEditOf(EDIT);
check("editing replaces the fork in turn", useLabStore.getState().forkOf, null);
// Detaching one must not silently arm the other.
store.setEditOf(null);
check("detaching leaves a plain draft", useLabStore.getState().forkOf, null);
console.log("\n── parking the opened version keeps the canvas ──");
store.setEditOf(EDIT);
const layerCountBefore = useLabStore.getState().layers.length;
check("something is on the canvas to park", layerCountBefore > 0, true);
check("it parks", useLabStore.getState().parkSnapshot("My Pattern · published"), true);
check("the canvas is untouched", useLabStore.getState().layers.length, layerCountBefore);
check("and so is edit mode", useLabStore.getState().editOf?.id, "p-mine");
check("the ring has it, under the given name", listSessions()[0]?.title, "My Pattern · published");
// The parked copy is what you get back if the update was a mistake, so it
// has to come back still pointed at the post it can be re-published to.
check(
"restoring it returns to editing that post",
(() => {
const id = listSessions()[0]?.id;
useLabStore.getState().restoreSession(id!);
return useLabStore.getState().editOf?.id;
})(),
"p-mine",
);
console.log("\n── failed backups cannot replace current work ──");
const parkedId = listSessions()[0]!.id;
useLabStore.getState().setName("Unsaved new work");
saveProject(useLabStore.getState());
const before = useLabStore.getState();
const ringBefore = localStore.getItem(LAB_STORAGE.sessions);
const projectBefore = localStore.getItem(LAB_STORAGE.project);
localStore.failWrites = true;
check("failed park is reported", before.stashCurrent(), false);
check("current layers remain identical", useLabStore.getState().layers === before.layers, true);
check("old Recent bytes survive all failed attempts", localStore.getItem(LAB_STORAGE.sessions), ringBefore);
check("the autosaved project survives", localStore.getItem(LAB_STORAGE.project), projectBefore);
check("snapshot failure is reported too", before.parkSnapshot(), false);
check("restore aborts when the current work cannot be parked", before.restoreSession(parkedId), false);
check("restore does not replace the name", useLabStore.getState().name, "Unsaved new work");
check("restore does not replace the layers", useLabStore.getState().layers === before.layers, true);
useLabStore.setState({ hydrated: true });
check("autosave retry reports the failure", saveLabProjectNow(), false);
check("the UI can display the failure", useLabStore.getState().saveStatus, "error");
localStore.failWrites = false;
check("autosave can recover", saveLabProjectNow(), true);
check("successful retry clears the failure", useLabStore.getState().saveStatus, "saved");
useLabStore.setState({ layers: [], activeLayerId: "" });
localStore.failWrites = true;
check("an empty canvas can restore without a backup", before.restoreSession(parkedId), true);
localStore.failWrites = false;
saveLabProjectNow();
console.log("\n── quota retries only evict after a successful write ──");
localStore.clear();
check("seed a large earlier session", stashSession("x".repeat(2000), "Earlier", 1), true);
localStore.maxWriteChars = 500;
check("a smaller new session can still be saved", stashSession("new", "Latest", 1), true);
check("only the saved latest work remains", listSessions().map((entry) => entry.title), ["Latest"]);
localStore.maxWriteChars = Infinity;
localStore.clear();
stashSession("x".repeat(2000), "Work just parked", 1);
const protectedRing = localStore.getItem(LAB_STORAGE.sessions);
localStore.maxWriteChars = 3500;
check("a published snapshot cannot displace the just-parked work", stashSession("y".repeat(3000), "Published", 1, { keepPrevious: true }), false);
check("the protected ring is unchanged", localStore.getItem(LAB_STORAGE.sessions), protectedRing);
localStore.maxWriteChars = Infinity;
}
main()
.then(() => {
console.log(failures === 0 ? "\nAll lab-edit checks passed.\n" : `\n${failures} check(s) FAILED.\n`);
process.exit(failures === 0 ? 0 : 1);
})
.catch((error: unknown) => {
console.error(error);
process.exit(1);
});