mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
0f535d0b80
* fix(notes): protect title IME, surface persist failures, harden paste Note titles rewrote controlled value during CJK composition (Sogou Wubi). Notes localStorage writes ignored QuotaExceededError so restarts lost content. Markdown paste intercept could preventDefault with a no-op insert (fixes #2783). Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(notes): commit title draft on blur for IME composition IME-only title entry never called onCommit until compositionend; blur could flush an empty draftTitle and then adopt the stale external value. Commit the local draft on blur before parent flushNoteDraft. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(notes): stash IME title drafts in refs without controlled rewrite Composition updates now live-stash into draftTitleRef for teardown and note-switch flushes, while onCommit still waits for idle IME. Blur syncs superseded external titles before flush so stale drafts cannot overwrite. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(notes): clear stashed title when IME composition is superseded compositionEnd now live-stashes the adopted external title so pagehide and note-switch flushes cannot persist rejected composed text. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(notes): cancel draft flush during IME title stash Prior idle commits could still debounce-flush mid-composition and rewrite the controlled title. Stash now clears the timer, and external supersede in the adopt effect live-stashes the authoritative title. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(notes): dedupe quota failure toasts during autosave Debounced draft flushes can hit QuotaExceededError repeatedly; rate-limit the error toast so users are not spammed every 300ms. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
71 lines
2.7 KiB
TypeScript
71 lines
2.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
const source = readFileSync(new URL("./NoteTitleInput.tsx", import.meta.url), "utf8");
|
|
const managerSource = readFileSync(new URL("./NotesManager.tsx", import.meta.url), "utf8");
|
|
|
|
test("NoteTitleInput keeps a local draft and only commits when IME composition is idle", () => {
|
|
assert.match(source, /shouldCommitImeControlledChange/);
|
|
assert.match(source, /shouldAdoptExternalImeControlledValue/);
|
|
assert.match(source, /onCompositionStart=\{/);
|
|
assert.match(source, /onCompositionEnd=\{/);
|
|
assert.match(source, /value=\{draft\}/);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/onChange=\{\(event\) => onCommit\(event\.target\.value\)\}/,
|
|
);
|
|
});
|
|
|
|
test("NoteTitleInput stashes live drafts during composition without idle-only commits", () => {
|
|
assert.match(source, /onLiveDraft\?\.\(next\)/);
|
|
assert.match(source, /onLiveDraft\?: \(title: string\) => void/);
|
|
});
|
|
|
|
test("NoteTitleInput commits the local draft on blur before parent flush", () => {
|
|
assert.match(source, /onBlur=\{\(event\) => \{/);
|
|
assert.match(source, /onCommit\(next\)/);
|
|
assert.match(source, /onCommit\(value\)/);
|
|
assert.match(source, /onBlur\?\.\(\)/);
|
|
});
|
|
|
|
test("NoteTitleInput clears live-stashed title when composition is externally superseded", () => {
|
|
assert.match(
|
|
source,
|
|
/supersededRef\.current = true;[\s\S]*?setDraft\(value\);[\s\S]*?onLiveDraft\?\.\(value\)/,
|
|
);
|
|
assert.match(source, /adoptedExternal/);
|
|
assert.match(source, /onLiveDraftRef\.current\?\.\(adoptedExternal\)/);
|
|
});
|
|
|
|
test("NotesManager cancels debounced flush while stashing IME title drafts", () => {
|
|
assert.match(
|
|
managerSource,
|
|
/clearDraftTimer\(\);[\s\S]*?draftNoteIdRef\.current = note\.id;[\s\S]*?draftTitleRef\.current = title/,
|
|
);
|
|
});
|
|
|
|
test("NoteTitleInput resets IME guards when the active note changes", () => {
|
|
assert.match(source, /noteId/);
|
|
assert.match(
|
|
source,
|
|
/composingRef\.current = false;[\s\S]*?supersededRef\.current = false;[\s\S]*?setDraft\(value\)/,
|
|
);
|
|
});
|
|
|
|
test("NotesManager title rows use NoteTitleInput instead of raw controlled saves", () => {
|
|
assert.match(managerSource, /NoteTitleInput/);
|
|
assert.match(managerSource, /data-note-title-row/);
|
|
assert.match(managerSource, /onCommit=\{\(title\) => saveNoteTitleDraft/);
|
|
assert.doesNotMatch(
|
|
managerSource,
|
|
/data-note-title-row[\s\S]{0,500}<input[\s\S]{0,250}onChange=\{\(event\) => saveNoteTitleDraft/,
|
|
);
|
|
});
|
|
|
|
test("NotesManager title rows stash live IME drafts into refs", () => {
|
|
assert.match(managerSource, /stashNoteTitleDraft/);
|
|
assert.match(managerSource, /onLiveDraft=\{\(title\) => stashNoteTitleDraft/);
|
|
assert.match(managerSource, /draftTitleRef\.current = title/);
|
|
});
|