[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
265
components/notes/NoteSourceEditor.test.tsx
Normal file
265
components/notes/NoteSourceEditor.test.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user