Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tests/browser/captions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ test("only changed editor text dismisses Jim", async (t) => {
nodes,
state,
dismissCaptions: captions.dismissCaptions,
// History is covered separately; this fixture isolates input side effects.
replayingEditorHistory: true,
drawingTurn: null,
flushPendingLanguagePublish: () => events.push("language"),
scheduleEditorPaint: () => events.push("paint"),
Expand Down
6 changes: 2 additions & 4 deletions tests/browser/comment-shortcut.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,6 @@ test("the comment marker follows the language tab", async (t) => {
});

// One toggle over several lines is one edit to take back, not one per line.
// Asked of the browser's own undo stack, which is where a toggle written by
// assigning the textarea's value would have left nothing at all.
test("one undo takes a toggled block back", async (t) => {
const ctx = await interviewPage(t);
if (!ctx) return;
Expand All @@ -192,7 +190,7 @@ test("one undo takes a toggled block back", async (t) => {
await setBuffer(page, code, 0, code.length);
await page.keyboard.press("Control+/");
assert.equal(await buffer(page), "# a = 1\n# b = 2\n# c = 3");
await page.evaluate(() => document.execCommand("undo"));
await page.keyboard.press("ControlOrMeta+Z");
assert.equal(await buffer(page), code);
assert.deepEqual(errors, []);
} finally {
Expand All @@ -217,7 +215,7 @@ test("a held chord toggles once", async (t) => {
await page.keyboard.up("/");
await page.keyboard.up("Control");
assert.equal(await buffer(page), "# a = 1");
await page.evaluate(() => document.execCommand("undo"));
await page.keyboard.press("ControlOrMeta+Z");
assert.equal(await buffer(page), "a = 1");
assert.deepEqual(errors, []);
} finally {
Expand Down
4 changes: 1 addition & 3 deletions tests/browser/dom-contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -433,9 +433,7 @@ test("rapid language selections publish only the final tab", () => {
// is the regression this test exists for.
const flushAt = setLanguage.indexOf("flushPendingEditorPublish()");
const languageAt = setLanguage.indexOf("state.language = language;");
const bufferAt = setLanguage.indexOf(
"nodes.editor.value = state.codeByLanguage[language];",
);
const bufferAt = setLanguage.indexOf("nodes.editor.value = next.value;");
assert.ok(flushAt >= 0, "setLanguage flushes a pending editor publish");
assert.ok(
languageAt > flushAt,
Expand Down
168 changes: 168 additions & 0 deletions tests/browser/editor-history.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { loadInterview } from "./source.js";
import {
createEditorHistory,
editorSnapshot,
} from "../../web/editor-history.js";

const snapshot = (
value,
start = value.length,
end = start,
direction = "none",
) => ({ value, start, end, direction });

test("interleaved deletions undo and redo within their own language", () => {
const history = createEditorHistory();
const python = snapshot("AAA\nBBB");
const javascript = snapshot("111\n222");
const pythonDeleted = snapshot("AAA\n");
const javascriptDeleted = snapshot("111\n");
history.select("python", python);
history.select("javascript", javascript, python);
history.record(javascript, javascriptDeleted);
history.select("python", python, javascriptDeleted);
history.record(python, pythonDeleted);
history.select("javascript", javascript, pythonDeleted);
assert.deepEqual(history.undo(), javascript);
history.select("python", python, javascript);
assert.deepEqual(history.undo(), python);
history.select("javascript", javascript, python);
assert.deepEqual(history.redo(), javascriptDeleted);
history.select("python", python, javascriptDeleted);
assert.deepEqual(history.redo(), pythonDeleted);
});

test("switching without editing neither creates an entry nor loses redo", () => {
const history = createEditorHistory();
const before = snapshot("before");
const after = snapshot("after");
history.select("python", before);
history.record(before, after);
assert.deepEqual(history.undo(), before);
history.select("javascript", snapshot("js"), before);
assert.equal(history.undo(), null);
history.select("python", before, snapshot("js"));
assert.deepEqual(history.redo(), after);
});

test("a new edit discards only that language's redo branch", () => {
const history = createEditorHistory();
history.select("python", snapshot("a"));
history.record(snapshot("a"), snapshot("b"));
history.undo();
history.select("javascript", snapshot("1"), snapshot("a"));
history.record(snapshot("1"), snapshot("2"));
history.undo();
history.select("python", snapshot("a"), snapshot("1"));
history.record(snapshot("a"), snapshot("c"));
assert.equal(history.redo(), null);
history.select("javascript", snapshot("1"), snapshot("c"));
assert.deepEqual(history.redo(), snapshot("2"));
});

test("undo and redo restore distinct selection bounds and direction", () => {
const history = createEditorHistory();
const before = snapshot("one\ntwo", 0, 7, "backward");
const after = snapshot("# one\n# two", 2, 11, "backward");
history.select("python", before);
history.record(before, after);
assert.deepEqual(history.undo(), before);
assert.deepEqual(history.redo(), after);
});

test("each language has its own bounded history", () => {
const history = createEditorHistory({ limit: 2 });
history.select("python", snapshot("0"));
for (let i = 1; i <= 3; i++)
history.record(snapshot(String(i - 1)), snapshot(String(i)));
history.select("javascript", snapshot("js"), snapshot("3"));
history.record(snapshot("js"), snapshot("changed"));
history.select("python", snapshot("3"), snapshot("changed"));
assert.deepEqual(history.undo(), snapshot("2"));
assert.deepEqual(history.undo(), snapshot("1"));
assert.equal(history.undo(), null);
history.select("javascript", snapshot("changed"), snapshot("1"));
assert.deepEqual(history.undo(), snapshot("js"));
});

test("adjacent typing groups but cursor movement and pauses split entries", () => {
const history = createEditorHistory({ groupDelay: 100 });
history.select("python", snapshot(""));
history.record(snapshot(""), snapshot("a"), "insertText", 0);
history.record(snapshot("a"), snapshot("ab"), "insertText", 50);
history.record(snapshot("ab"), snapshot("abc"), "insertText", 200);
assert.deepEqual(history.undo(), snapshot("ab"));
assert.deepEqual(history.undo(), snapshot(""));
history.redo();
history.breakGroup();
history.record(snapshot("ab"), snapshot("abd"), "insertText", 220);
assert.deepEqual(history.undo(), snapshot("ab"));
});

test("selection-only edits preserve redo and create no text history", () => {
const history = createEditorHistory();
history.select("python", snapshot("a"));
history.record(snapshot("a"), snapshot("b"));
history.undo();
history.record(snapshot("a"), snapshot("a", 0));
assert.deepEqual(history.redo(), snapshot("b"));
});

test("snapshot objects cannot mutate stored history", () => {
const history = createEditorHistory();
const before = snapshot("before");
history.select("python", before);
history.record(before, snapshot("after"));
before.value = "mutated";
const restored = history.undo();
assert.equal(restored.value, "before");
restored.value = "mutated again";
history.redo();
assert.equal(history.undo().value, "before");
});

test("starter buffer replacement through applyEditorEdit is one undoable edit", () => {
const before = snapshot("candidate solution\n", 0, 9, "backward");
const starter = "starter\n";
const inputs = [];
const editor = {
value: before.value,
selectionStart: before.start,
selectionEnd: before.end,
selectionDirection: before.direction,
setSelectionRange(start, end, direction = "none") {
this.selectionStart = start;
this.selectionEnd = end;
this.selectionDirection = direction;
},
dispatchEvent() {
inputs.push(this.value);
},
};
const editorHistory = createEditorHistory();
editorHistory.select("python", before);
const scope = {
nodes: { editor },
editorHistory,
editorSnapshot,
editorComposition: null,
editorDrag: null,
compositionTimer: null,
pendingEditorEdit: null,
observedEditor: before,
applyingProgrammaticEdit: false,
clearTimeout,
Event,
};
const { applyEditorEdit } = loadInterview(
["applyEditorEdit", "finishEditorComposition", "finishEditorDrag"],
scope,
);
applyEditorEdit({ value: starter, start: 0, end: 0 });
assert.deepEqual(inputs, [starter]);
assert.deepEqual(editorHistory.undo(), before);
assert.equal(editorHistory.undo(), null);
assert.deepEqual(editorHistory.redo(), snapshot(starter, 0));
});
Loading