Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
266 lines
8.2 KiB
TypeScript
266 lines
8.2 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import React from "react";
|
|
import { act, create, type ReactTestRenderer } from "react-test-renderer";
|
|
|
|
import {
|
|
NoteSourceEditor,
|
|
type NoteSourceEditorHandle,
|
|
} from "./NoteSourceEditor.tsx";
|
|
|
|
test("NoteSourceEditor toolbar undo restores coalesced ordinary typing", async () => {
|
|
const actEnvironment = globalThis as typeof globalThis & {
|
|
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
|
requestAnimationFrame?: (callback: FrameRequestCallback) => number;
|
|
};
|
|
const previousActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT;
|
|
const previousAnimationFrame = actEnvironment.requestAnimationFrame;
|
|
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
actEnvironment.requestAnimationFrame = (callback) => {
|
|
callback(0);
|
|
return 0;
|
|
};
|
|
|
|
const editorRef = React.createRef<NoteSourceEditorHandle>();
|
|
const changes: string[] = [];
|
|
const textareaNode = {
|
|
selectionStart: 1,
|
|
selectionEnd: 1,
|
|
focus: () => undefined,
|
|
setSelectionRange(start: number, end: number) {
|
|
this.selectionStart = start;
|
|
this.selectionEnd = end;
|
|
},
|
|
scrollTop: 0,
|
|
scrollTo: () => undefined,
|
|
};
|
|
let renderer: ReactTestRenderer | null = null;
|
|
|
|
try {
|
|
await act(async () => {
|
|
renderer = create(
|
|
<NoteSourceEditor
|
|
ref={editorRef}
|
|
noteId="note-1"
|
|
value="a"
|
|
onChange={(nextValue) => changes.push(nextValue)}
|
|
/>,
|
|
{
|
|
createNodeMock: (element) => element.type === "textarea"
|
|
? textareaNode
|
|
: { scrollTop: 0 },
|
|
},
|
|
);
|
|
});
|
|
|
|
const textarea = renderer!.root.findByType("textarea");
|
|
await act(async () => {
|
|
textarea.props.onChange({
|
|
target: { value: "ab" },
|
|
nativeEvent: { inputType: "insertText" },
|
|
});
|
|
});
|
|
await act(async () => {
|
|
textarea.props.onChange({
|
|
target: { value: "abc" },
|
|
nativeEvent: { inputType: "insertText" },
|
|
});
|
|
});
|
|
await act(async () => {
|
|
editorRef.current?.insertAction("undo");
|
|
});
|
|
|
|
let prevented = false;
|
|
await act(async () => {
|
|
renderer!.root.findByType("textarea").props.onKeyDown({
|
|
key: "z",
|
|
metaKey: true,
|
|
ctrlKey: false,
|
|
altKey: false,
|
|
shiftKey: true,
|
|
preventDefault: () => { prevented = true; },
|
|
});
|
|
});
|
|
const composedTextarea = renderer!.root.findByType("textarea");
|
|
await act(async () => {
|
|
composedTextarea.props.onCompositionStart();
|
|
composedTextarea.props.onChange({
|
|
target: { value: "abc你", selectionStart: 4 },
|
|
nativeEvent: { inputType: "insertCompositionText" },
|
|
});
|
|
});
|
|
await act(async () => {
|
|
renderer!.root.findByType("textarea").props.onCompositionEnd();
|
|
});
|
|
await act(async () => {
|
|
renderer!.root.findByType("textarea").props.onCompositionStart();
|
|
renderer!.root.findByType("textarea").props.onChange({
|
|
target: { value: "abc你好", selectionStart: 5 },
|
|
nativeEvent: { inputType: "insertCompositionText" },
|
|
});
|
|
});
|
|
await act(async () => {
|
|
renderer!.root.findByType("textarea").props.onCompositionEnd();
|
|
});
|
|
await act(async () => editorRef.current?.insertAction("undo"));
|
|
await act(async () => editorRef.current?.insertAction("undo"));
|
|
|
|
assert.equal(prevented, true);
|
|
assert.deepEqual(changes, ["ab", "abc", "a", "abc", "abc你", "abc你好", "abc你", "abc"]);
|
|
assert.equal(renderer!.root.findByType("textarea").props.value, "abc");
|
|
} finally {
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
actEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
|
|
actEnvironment.requestAnimationFrame = previousAnimationFrame;
|
|
}
|
|
});
|
|
|
|
test("NoteSourceEditor keeps formatting and later typing as separate undo steps", async () => {
|
|
const actEnvironment = globalThis as typeof globalThis & {
|
|
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
|
requestAnimationFrame?: (callback: FrameRequestCallback) => number;
|
|
};
|
|
const previousActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT;
|
|
const previousAnimationFrame = actEnvironment.requestAnimationFrame;
|
|
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
actEnvironment.requestAnimationFrame = (callback) => {
|
|
callback(0);
|
|
return 0;
|
|
};
|
|
|
|
const editorRef = React.createRef<NoteSourceEditorHandle>();
|
|
const changes: string[] = [];
|
|
const textareaNode = {
|
|
selectionStart: 0,
|
|
selectionEnd: 0,
|
|
focus: () => undefined,
|
|
setSelectionRange(start: number, end: number) {
|
|
this.selectionStart = start;
|
|
this.selectionEnd = end;
|
|
},
|
|
scrollTop: 0,
|
|
scrollTo: () => undefined,
|
|
};
|
|
let renderer: ReactTestRenderer | null = null;
|
|
|
|
try {
|
|
await act(async () => {
|
|
renderer = create(
|
|
<NoteSourceEditor
|
|
ref={editorRef}
|
|
noteId="note-1"
|
|
value=""
|
|
onChange={(nextValue) => changes.push(nextValue)}
|
|
/>,
|
|
{
|
|
createNodeMock: (element) => element.type === "textarea"
|
|
? textareaNode
|
|
: { scrollTop: 0 },
|
|
},
|
|
);
|
|
});
|
|
|
|
await act(async () => {
|
|
editorRef.current?.insertAction("bold");
|
|
});
|
|
const formatted = "**bold text**";
|
|
const edited = "**bold text!**";
|
|
const textarea = renderer!.root.findByType("textarea");
|
|
await act(async () => {
|
|
textarea.props.onChange({
|
|
target: { value: edited },
|
|
nativeEvent: { inputType: "insertText" },
|
|
});
|
|
});
|
|
await act(async () => {
|
|
editorRef.current?.insertAction("undo");
|
|
});
|
|
await act(async () => {
|
|
editorRef.current?.insertAction("undo");
|
|
});
|
|
|
|
assert.deepEqual(changes, [formatted, edited, formatted, ""]);
|
|
assert.equal(renderer!.root.findByType("textarea").props.value, "");
|
|
} finally {
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
actEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
|
|
actEnvironment.requestAnimationFrame = previousAnimationFrame;
|
|
}
|
|
});
|
|
|
|
test("NoteSourceEditor undo and redo restore toolbar formatting selections", async () => {
|
|
const actEnvironment = globalThis as typeof globalThis & {
|
|
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
|
requestAnimationFrame?: (callback: FrameRequestCallback) => number;
|
|
};
|
|
const previousActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT;
|
|
const previousAnimationFrame = actEnvironment.requestAnimationFrame;
|
|
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
actEnvironment.requestAnimationFrame = (callback) => {
|
|
callback(0);
|
|
return 0;
|
|
};
|
|
|
|
const editorRef = React.createRef<NoteSourceEditorHandle>();
|
|
const textareaNode = {
|
|
selectionStart: 0,
|
|
selectionEnd: 5,
|
|
focus: () => undefined,
|
|
setSelectionRange(start: number, end: number) {
|
|
this.selectionStart = start;
|
|
this.selectionEnd = end;
|
|
},
|
|
scrollTop: 0,
|
|
scrollTo: () => undefined,
|
|
};
|
|
let renderer: ReactTestRenderer | null = null;
|
|
|
|
try {
|
|
await act(async () => {
|
|
renderer = create(
|
|
<NoteSourceEditor
|
|
ref={editorRef}
|
|
noteId="note-1"
|
|
value="hello"
|
|
onChange={() => undefined}
|
|
/>,
|
|
{
|
|
createNodeMock: (element) => element.type === "textarea"
|
|
? textareaNode
|
|
: { scrollTop: 0 },
|
|
},
|
|
);
|
|
});
|
|
|
|
await act(async () => editorRef.current?.insertAction("bold"));
|
|
assert.deepEqual(
|
|
{ start: textareaNode.selectionStart, end: textareaNode.selectionEnd },
|
|
{ start: 2, end: 7 },
|
|
);
|
|
|
|
await act(async () => editorRef.current?.insertAction("undo"));
|
|
assert.equal(renderer!.root.findByType("textarea").props.value, "hello");
|
|
assert.deepEqual(
|
|
{ start: textareaNode.selectionStart, end: textareaNode.selectionEnd },
|
|
{ start: 0, end: 5 },
|
|
);
|
|
|
|
await act(async () => editorRef.current?.insertAction("redo"));
|
|
assert.equal(renderer!.root.findByType("textarea").props.value, "**hello**");
|
|
assert.deepEqual(
|
|
{ start: textareaNode.selectionStart, end: textareaNode.selectionEnd },
|
|
{ start: 2, end: 7 },
|
|
);
|
|
} finally {
|
|
await act(async () => {
|
|
renderer?.unmount();
|
|
});
|
|
actEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
|
|
actEnvironment.requestAnimationFrame = previousAnimationFrame;
|
|
}
|
|
});
|