Repository navigation
Close editor-support gaps: IME safety, writer tests, Tiptap, Outlook and Notion - #476
Merged
Merged
Conversation
- review-reference.md: Draft.js Review pushes "insert-fragment" (ReviewModelEditors.ts:279), not "spellcheck-change". Draft.js merges only insert/backspace/delete-character pushes (draft-js EditorState.js:533-536), so each push is its own undo step. - editor-capabilities.md: Quill typing is native input (ContentEditableAdapter.ts:780-791, :259-305); the bridge has no Quill typing branch (HostEditorMainWorldBridge.ts:520-544). - Lexical, Draft.js, CKEditor 5 and Trix each have a typing path: Draft.js/Trix through MODEL_TYPING_SELECTOR (HostEditorBridgeProtocol.ts:15, ReviewModelEditors.ts:509-550), CKEditor 5 through model.change in the MAIN world (HostEditorMainWorldBridge.ts:229-312), Lexical through a synthetic beforeinput (ContentEditableAdapter.ts:258-266). - Fingerprint-only typing: EditorCapabilities.ts:72-78 (since 6ad9fb9) permits only an attempt. The doc now splits "fingerprint without a typing path" (unverified-writer) from "typing-path fingerprint without its editor" (shown, each write refused). A new test in ContentEditableAdapter.test.ts proves that each of the ten typing-path fingerprints refuses the write without its editor. - Add CodeMirror 5 typing (HostEditorControllerUtils.ts:44-60, :150-187). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Trix Review adapter returned "composing: () => false". Thus the writer did not see an IME composition. During a composition, Review read the text that the IME had not committed. Review Apply and typing (replaceModelBlock through MODEL_TYPING_SELECTOR) could write into the composition. Trix has no public composition state, and its internal Level2InputController sets "composing" only at the first insertCompositionText. The MAIN-world bridge now records the target of each compositionstart and compositionend (recordComposition in HostEditorControllerUtils.ts). It clears the record when the bridge is turned on or off. isComposingIn(root) is true while a composition runs in the root, in a descendant or in an ancestor. The Trix adapter returns isComposingIn(root) (ReviewModelEditors.ts:405). Its snapshot then refuses Review reads, Review writes and typing writes. Test: HostEditorCKEditor5Bridge.test.ts, "Trix refuses Review reads, Review writes and typing writes while composing". Before the fix, readReviewModel gave the text during the composition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Slate bridge (readSlate, applySlate, slateBlockContext and replaceSlateBlock) had no composition guard. During a composition the DOM holds text that the Slate model does not have yet. Thus Review read the uncommitted text, and a Review fix or a typing completion could write into the composition. slate-react keeps its composition state in a WeakMap of the page's own bundle (ReactEditor.isComposing), which the bridge cannot reach. owningSlate() now refuses the editor while isComposingIn(root) is true (SlateEditor.ts:99). All Slate reads and writes go through it. Test: SlateEditor.test.ts, "an IME composition blocks Review reads, Review writes and typing writes" (real slate-react). Before the fix, readSlate gave the text during the composition. The test file also defines the HTMLInputElement and HTMLTextAreaElement globals, because the slate-react composition handlers read them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ring a composition The content-script Review targets keep a "composing" flag. The ReviewController sets it on compositionstart and compositionend, but it adds these listeners only when Review opens. The flag started as false. Thus a Review that opened during a composition read the uncommitted text, and Apply wrote into the composition. This applies to text controls, plain contenteditable fields, Quill beforeinput writes and the DOM-model editors. The content runtime now records the composition target from its start (ContentRuntimeController.ts, recordComposition on document capture). It uses the same helper as the MAIN-world bridge; each world has its own module instance. When Review opens, the target flag starts as isComposingIn(element) (ReviewController.ts:315). Test: ReviewAdapters.test.ts, "a review opened during an IME composition writes nothing until the composition ends". Before the fix, Review showed "Issues: 1" during the composition and Apply wrote "We saw the cat.". Now Review shows "Paused while you compose text." and writes only after compositionend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ites during composition Three bridge writers had no composition guard: the CKEditor 5 typing path (applyCKEditor5BlockReplacement), the CodeMirror 5 typing path (applyBlockReplacement) and the native edit of the DOM-model editors (applyDomEditor). The upstream typing guard (entry.isComposing) blocks them in practice. These guards are extra safety: no bridge writer can change the text during an IME composition. - CKEditor 5 typing: readCKEditor5Block refuses while view.document.isComposing is true. It also refuses the block context. - CodeMirror 5: applyBlockReplacement refuses while isComposingIn(elem). - TinyMCE, CKEditor 4, Froala, Summernote: applyDomEditor refuses while isComposingIn(elem). The check runs before "begin" and again immediately before insertText. Tests (HostEditorCKEditor5Bridge.test.ts, "IME composition"): "CKEditor 5 typing refuses to write while its view composes", "a CodeMirror 5 controller refuses to write while the field composes", "a DOM-model editor (TinyMCE) refuses its native edit while the field composes". Before the fix, each write gave "applied": true during the composition. Composition guard of each writer (Phase 2 record): - ProseMirror Review and typing: ProseMirrorEditor.ts:131 (view.composing in readProseMirror; applyProseMirror and replaceProseMirrorBlock call it) - Quill Delta Review path: QuillEditor.ts:77, :122, :175 (selection.composing) - Quill and Lexical beforeinput, plain contenteditable, text controls and DOM-model Review writes (content script): ReviewTargets.ts:338, :376, :526, :551, :621; flag set at ReviewController.ts:315-324 - Slate Review and typing: SlateEditor.ts:99 (isComposingIn in owningSlate) - Gutenberg Review and typing: GutenbergEditor.ts:379 (fieldFor) - Lexical Review: ReviewModelEditors.ts:101 (editor.isComposing()) - Draft.js Review and typing: ReviewModelEditors.ts:205 (isInCompositionMode) - CKEditor 5 Review: ReviewModelEditors.ts:357 (view.document.isComposing) - Trix Review and typing: ReviewModelEditors.ts:405 (isComposingIn); model adapters checked at ReviewModelEditors.ts:458 - CKEditor 5 typing: HostEditorMainWorldBridge.ts:231 - CodeMirror 5 typing: HostEditorMainWorldBridge.ts:345 - TinyMCE, CKEditor 4, Froala, Summernote typing: HostEditorMainWorldBridge.ts:366 - All typing writes, upstream: SuggestionTextEditService.ts:119, :505, :812 (canEdit -> refreshInteraction, SuggestionEntrySession.ts:150) - Composition record: HostEditorControllerUtils.ts (recordComposition, isComposingIn); MAIN world at HostEditorMainWorldBridge.ts:408, :453; content script at ContentRuntimeController.ts:128 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MAIN-world bridge recorded IME composition events only while it was turned on. A composition that started before the bridge turned on was not seen. The Trix, Slate, CodeMirror 5 and DOM-editor writers could then write during that composition. The bridge now listens for compositionstart and compositionend at all times, through the same listener list that survives document.open() (HostEditorMainWorldBridge.ts). Remove the unused setCompositionTarget. Test: "a composition that starts while the bridge is off still blocks writes after it turns on" (tests/HostEditorCKEditor5Bridge.test.ts). It failed before the fix with applied: true. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each writer now has a direct unit test with the real vendored library in JSDOM. No stub of an editor library is used. - tests/ReviewModelEditors.test.ts: Lexical, Draft.js and Trix. - tests/ReviewDomEditors.test.ts: TinyMCE, CKEditor 4, Froala and Summernote, through the real InjectedHostEditorPageBridge. For each editor the tests check: detection (and refusal of a fingerprint without its editor), one replacement with formatting and caret kept, one host Undo step that keeps earlier typing, refusal when the content changed after the read, and refusal during an IME composition with a write after it ends. Draft.js and Trix also test the typing path (replaceModelBlock). The test files polyfill only missing JSDOM features (customElements, ElementInternals.setValidity/setFormValue, InputEvent.getTargetRanges, Range rects, matchMedia, canvas). tests/modules.d.ts gets the Draft.js types the tests use and a "trix" module declaration. Known limits of the tests: the Lexical composition case is also caught by the model/DOM text check; Draft.js runs on React 19 in the unit test (the e2e fixture uses React 18). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ved away from
A page can focus an editor and then move the caret by script, for
example to the end of the text. FluentTyper asked for predictions at the
old caret place on focus. The response then showed at the new caret,
and Space or Tab wrote the stale word there ("We saw" became "We We").
Cause:
- handlePredictionResponse checked only requestId. A caret move without
input does not change requestId.
- reconcileSelection kept a visible menu when the caret moved forward
over unchanged text ("anything".startsWith("") is true).
Fix (SuggestionEntrySession.ts):
- Drop a response when the text before the caret is no longer a prefix
of the request text and the next character changed too. A caret behind
the request is still accepted, because a host can report a stale
selection.
- A longer before-caret text keeps the menu only when the text changed.
Tests (tests/SuggestionEntrySession.test.ts), both fail without the fix:
- "a response for a caret place that the page moved away from is not shown"
- "a caret move without a text change dismisses a contenteditable menu"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For Draft.js, Trix, CKEditor 4, Froala, Summernote and the bundled Quill 1 and Quill 2 fixtures (no window.Quill global), add e2e cases: - acceptance mid-line, in the second paragraph and inside a list item, with the caret after the accepted word; - snippet expansion in one native undo step; - the seven fixtures join the "caret after the inserted word" list. The fixtures add a second paragraph and a ul/li item with ?reviewSeed=blocks. Without the parameter the seed is unchanged. A shared helper (openTypingEditor, TYPING_EDITORS) replaces the copied setup in the typing and Review tests. Editor limits that the tests wait for (no skips): - Trix 2.1.19 loses its block element when Backspace deletes all text. - Draft.js with React 18 renders after a model write; a key typed before that render goes into the old DOM. coverage-matrix.json: rich_editor_typing_positions, rich_editor_snippet_expansion, stale_predictions_after_caret_move. Chrome: 57 targeted cases and the full suite (229) pass. Firefox: not run in this session (see the follow-up commit or report). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… up with the DOM When Review opened on a Draft.js field whose DOM was ahead of the model, the session stayed Review-only for its full life. The ReviewTargets.ts constructor set "model-editor" one time only. Now resolveModelWriter() changes the writer to "host-model" only when the full readReviewModel check passes. The poll of 1 s calls it. The Review-only note updates on paint. No write check changed. A partial read stays refused: a model write renders the block again and can remove text that only the DOM has. The test "a DOM ahead of the model when Review opens gets Apply after the model takes the text" failed before the fix with apply:false/bulk:false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quill history merged the accept into the typing of the last second. Thus
one Undo also removed the typed text.
Fix: a bridge action quillHistoryBoundary (quill.update("user"), then
history.cutoff()) runs before and after the native write, for .ql-editor
only. It adds no write and skips no check.
window.Quill and a bundled Quill 1 get a separate undo step. A bundled
Quill 2 keeps the limit, because it keeps its instances in a
module-private map.
The tests failed before the fix. For example, in the quill1 e2e test the
model after Undo did not have " w".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A block with an inline image had no model mapping. Thus the accept went back to the CKEditor typing batch, and one Undo also removed the typed text. A collapsed edit at a softBreak made an inverted model range: writer.remove threw, and the write was refused (a latent fault). The attributes of an image next to the text could go onto the text. Fix: - Map only textless inline objects (schema object and inline, empty DOM text). - Refuse an edit that contains an object, or that has an object between the edit and the caret. - Use one model offset for a collapsed edit. - Take attributes only from $text or $textProxy. These tests failed before the fix: - "block context next to an inline image has the text around it and the caret" - "a replacement after an inline image uses model offsets and keeps the image" - "an insertion right after an inline image does not take the image's attributes" - "a collapsed insertion at a softBreak goes to the caret's side of the break" These tests check the refusals: - "a replacement across an inline image is refused and the model stays" - "a word before an inline image is refused when the caret is after the image" - "an inline object that shows text is refused for context and writes" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the real @tiptap/core, @tiptap/starter-kit and @tiptap/pm 3.31.4 as dev dependencies. The lockfile keeps one prosemirror copy. The Tiptap fixture is in REVIEW_EDITORS and TYPING_EDITORS. These tests pass on Chrome (6 pass): - Review Apply and Fix all, with one Undo. - Typing and Tab accept, with one Undo. - Accept mid-line, in the 2nd paragraph and in a list item. - Snippet expansion. Firefox is pending: it cannot start in this session. The docs and the coverage matrix tell that Tiptap uses the ProseMirror path. This commit also removes a space from the Trix section comment in tests/ReviewModelEditors.test.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Google Docs applies one fix at a time (capabilities bulk: false). The open note said so, but each paint set the Docs notes to only "review_cap_docs" or to nothing. Thus the user did not see why Fix all was unavailable. The Docs paint now adds writeCapabilityKeys(target), as the other targets do (ReviewController.ts). Test: the Docs controller test in tests/ReviewAdapters.test.ts now checks the note after a paint. It failed before the fix: the notes held only the Docs highlight note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Typer popup Escape closed the popup, but the page also got the Escape. On Notion, Escape selects the block, and the next keys typed nothing (live probe: four keys were lost). Now FluentTyper consumes Escape only when a popup or an inline suggestion is visible and FluentTyper owns the keys. In all other conditions, the page gets Escape. The unit test failed before this fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On live Outlook, the generic path lost an edit: one Undo removed two steps, and Redo did not restore the edit. RoosterJS records only its own input in its undo snapshots. The new Transaction in ReviewDomEditors.ts uses these steps: takeSnapshot(), the validated native edit, takeSnapshot() and a contentChanged event. The MAIN-world bridge finds the editor in window.__ROOSTERJS_DEVTOOLS_EDITORS__ (roosterjs 9.59 and later). Without this list, an identified Rooster editor is Review-only. The writer refuses an edit during an IME composition, in shadow edit, without focus and without a range selection. When "end" does not give an undo step, the result is unverified. The TinyMCE and CKEditor 4 "begin" and "end" functions now return void. The e2e fixture uses the real roosterjs package (9.60.0, dev dependency). 8 Chrome e2e tests pass (4 failed before). The Firefox run is pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Notion page root is a canvas, as the Gutenberg canvas is. Each block leaf is the field for typing, block context and Review. A write occurs only when these conditions are true: the caret is already in that leaf, no composition runs, and the range is in the leaf. After the write, FluentTyper reads the leaf text again and watches the leaf for a revert for 1 s. Review applies one fix at a time in each leaf (bulk: false). The e2e fixture is a synthetic page and contains no Notion code. 5 Chrome e2e tests pass (4 failed before). The live check on Notion is pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the Phase 6 report. For each surface, it gives the editor model, the safety of the generic path, the verified writer, the risks and the live checks that are open. Gmail keeps the generic path (maintainer decision). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symptom: in Outlook on the web, Undo of a word that Tab accepted
restored the text, but the typed prefix stayed selected. The next key
replaced it ("It was bi" + Tab, Undo, "g" gave "It was g"). Undo of a
typing autocorrect put the caret at the start of the word.
Cause: applyDomEditor selects the replaced range, then begin() records
the host undo step. RoosterJS takeSnapshot() records the live DOM
selection, and Undo restores the selection of that snapshot. Froala,
Summernote and CKEditor 4 record the selection in their undo step in the
same way. TinyMCE restores the bookmark of its last beforeChange(),
which is the start of the user's typing.
Fix: the typing request sends the user's caret as a text offset.
applyDomEditor puts this caret back before begin(), then selects the
range again for the native edit. The TinyMCE begin() also calls
beforeChange(), so it records the same caret. All refusals stay.
Test: tests/ReviewDomEditors.test.ts (real CKEditor 4, Froala,
Summernote and RoosterJS in JSDOM): after a typing edit with the caret
after the word or after the next space, Undo gives a collapsed caret at
that offset. Before the fix, the selection was not collapsed. A Review
fix keeps the caret too. tests/e2e/full.e2e.test.ts: for TinyMCE,
CKEditor 4, Froala, Summernote and RoosterJS, after accept and Undo the
selection is collapsed and the next key extends the typed word. Before
the fix, all five failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symptom: on Notion, the console showed many warnings "Reverting mutation of attribute data-ft-suggestion-id" and others. No block leaf kept a data-ft-* attribute. The MAIN-world early Tab bridge did not find FluentTyper's state, so Tab used only the isolated-world handler. Cause: resolveSuggestionStateHost returned the leaf. Notion's DOM lock removes each foreign attribute of a leaf at once. FluentTyper has no write loop: its DOM observer does not watch data-ft-* attributes, and it writes them only on attach and on each menu render. Thus each FluentTyper write gave one Notion revert. The page root is not locked. Fix: resolveSuggestionStateHost returns the Notion page root for a block leaf. The root is shared by all leaves, so the menu render also writes the entry id of the leaf that shows the menu. The early Tab bridge finds the root in the event path, as before. Test: tests/NotionEnvironment.test.ts: after attach and render, no leaf has a data-ft-* attribute, and the root holds the state with the shown entry id. Before the fix, each leaf had six attributes. The Notion-like e2e fixture now reverts each foreign leaf attribute, as Notion does. A new e2e test: one key press gives no locked leaf attribute (before the fix: 2), and Tab goes through the early bridge (before the fix: 0 requests). The other Notion-like tests also pass with the lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Record the live check in Chrome: Outlook checks 1 to 6 and Notion checks 7 to 11 pass. Name the two bugs that the check found and their fixes. Record the limits that stay: no Review button on a one-line Notion block (the launcher needs a field of 36 px height), one Notion Undo can remove typing and an autocorrect together, and the Outlook "To" field gets first-word capitalization. Only a real IME composition and Firefox stay open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symptom: In the full Chrome suite, the test "Contenteditable keeps surrounding rich formatting when accepting a suggestion" failed in shard 1/6. No suggestion showed after the test typed "h". The test passed alone. Cause: The Slate inline test sets inline suggestions on. On Chrome, notifyConfigChange opens the options page. With inline suggestions on, that page stores prefixOnlyMode=true. The Slate test reset only inline suggestions, so prefix-only mode stayed on. The rich test puts the caret directly before "next". In prefix-only mode, Presage gives no prediction there. The two tests are in the same shard since commit 5e1019c added one test before them. The pair also fails on master. Fix: The rich test sets inline suggestions and prefix-only mode to off. The Slate test resets prefix-only mode in its finally block. Test: The Slate test and the rich test pass in one process two times. Before the fix, the pair failed on this branch and on master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symptom: on Firefox, "trix typing accepts a prediction with formatting kept and one native undo step" failed in each run. The typed " w" went to the start of the text, and the accept changed "wWe" to "wee ". Cause: Trix 2.1.19, not FluentTyper. The test clicked the editable and pressed End. In a focused Firefox window, the click puts the DOM caret at the end of the text. Then Trix gets the focus event before it reads that selection. EditorController.compositionControllerDidFocus sees no location range (isFocusedInvisibly) and calls setLocationRange at index 0, offset 0. Firefox does not move the caret from that place on End. A probe without the extension shows the same result: the text becomes " wWe saw teh cat and teh dog.". The test also failed on master (2 of 2 runs), before commit a069dd7. Fix: put the caret after "dog." with the placeCaretAfter helper, as the other typing tests do. The test does not use a click and End. Test: Firefox, the typing acceptance cases pass 3 times in a row. The full Firefox suite (257) and the full Chrome suite (262) pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The full e2e suite passes on Firefox (257 pass) and Chrome (262 pass). Only a live Firefox check of Outlook and Notion stays open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Symptom: In inline mode, an exact-match prediction (an empty suffix) or a suffix of only white space shows no ghost. Escape was still consumed. Thus a page Escape (for example, to close a dialog) did nothing, and the user had to push Escape two times. Cause: The keyboard handler decided that a suggestion showed when entry.inlineSuggestion was set. The inline presenter keeps that value armed for Tab also when it renders no ghost. Fix: The handler now asks if a ghost for this entry is in the page (InlineSuggestionView.hasForEntry) or if the menu is visible. The presenter renders no ghost for a suffix of only white space. Tab acceptance does not change. Test: SuggestionKeyboardHandler.test.ts "Escape with an armed inline suggestion that shows no ghost goes to the page" and InlineSuggestionPresenter.test.ts "keeps a suggestion armed without a ghost when only white space is left to preview". Both failed before the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symptom: When Review opened during an IME composition in a Notion block, Review stayed "Paused while you compose text." after the composition ended. It did not read the block again for the full session. Cause: The controller set target.composing from isComposingIn(leaf), and listened for compositionstart and compositionend on the leaf. Notion keeps focus on its page root, so these events fire on the root. They do not go down to the leaf. Thus nothing cleared the flag. Fix: For a Notion leaf, the controller listens for the composition events on its page root (notionRootOf(element) ?? element). Review opened during a composition still reads and writes nothing until the composition ends. Test: ReviewAdapters.test.ts "a review opened during an IME composition in a Notion block reads again when it ends". Before the fix, the status did not change to "Issues: 1" after compositionend on the root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Review reference said that a live check on Notion was pending. The Chrome live check passed (see the live check results in editor-surfaces.md). The text now says that and links to the results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symptom: In TinyMCE, CKEditor 4, Froala, Summernote and RoosterJS, push Enter, then accept a next-word prediction (an empty token) at the start of the new block. After one host Undo, the caret went to the end of the block before it, and the next typing went there. Cause: The bridge records the user's caret in the host undo step before the write. It got the caret as a text offset and mapped it with textPosition, which takes the first text node that ends at that offset. A block start and the end of the block before it have the same text offset. Fix: When the user's caret is at the end of the replaced range (the usual case), the bridge collapses to the end of the live replaced range. It uses the offset mapping only for other carets. The write checks do not change. Test: ReviewDomEditors.test.ts "after a typing edit at the start of a second block, Undo puts the caret back in that block" (CKEditor 4, Froala, Summernote, RoosterJS with the real libraries). Before the fix, the caret was in "We saw teh cat." after Undo, not in "Hi". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symptom: In Notion, when one block leaf detached (for example, when Review suspended it for a write), the early Tab bridge stopped to claim Tab in the other attached leaves of the page. Cause: All Notion leaves use the page root as state host. detachHelper removed all FluentTyper attributes from the state host, also when other attached entries used the same host. Fix: When another attached entry uses the same state host, detachHelper keeps the attributes. If the host named the detached entry, it names the other entry and sets the visible flag to false. The last entry that detaches removes all attributes, as before. Test: NotionEnvironment.test.ts "a Notion leaf that detaches leaves the shared root state to the other leaves". Before the fix, data-suggestion on the root was null after one leaf detached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symptom: When Review opened during an IME composition in Slate or ProseMirror, the target stayed "model-editor" (Review only, no Apply) for the full session, also after the composition ended. Cause: The bridge does not read Slate or ProseMirror during a composition, so the constructor found no writer. resolveModelWriter tried again only readReviewModel (Lexical, Draft.js, CKEditor 5, Trix), not the other kinds. Fix: The constructor and resolveModelWriter use the same kind detection (detect). resolveModelWriter accepts only a verified writer kind (prosemirror, slate, host-model or host-dom). A fingerprint without its editor stays Review only. No write check changes. Test: SlateEditor.test.ts "a Review opened during an IME composition gets the Slate writer after it" (real slate-react) and ProseMirrorReview.test.ts "a Review opened during an IME composition gets the ProseMirror writer after it". Before the fix, resolveModelWriter() gave false after compositionend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… wait Symptom: In Notion, a Review fix gives focus back to the page root and waits 100 ms before it writes. When the user closed Review in this wait, the fix was still written. Cause: After the wait, apply() did not examine if the review was still open. Fix: ContentEditableReviewTarget.dispose() (which ReviewController.close() calls) now sets a disposed flag. After the Notion wait, apply() refuses with "host-refused" when the flag is set. The other write checks do not change. The Review reference now tells about this check. Test: NotionEnvironment.test.ts "a Review fix in Notion writes nothing when the review closes during the focus wait". Before the fix, the result was "applied" and the leaf held "We saw the". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- recordComposition: set the target in one line. Only compositionstart and compositionend call this function. - ReviewController: one capabilityKeys helper gives the keys, also the Google Docs key. createUi and paint use it. - SuggestionMenuPresenter: compare only the entry id on the state host. A host that is not shared already holds the id of its entry. - HostEditorPageBridge: quillHistoryBoundary returns nothing. No caller reads the result. ContentEditableAdapter makes one bridge for each write. - Use the ReviewTransactionPhase type in the bridge protocol and bridge. - NotionEnvironment: NOTION_REVERT_WINDOW_MS is private. Remove the test that compared the constant with its own value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Use the shared helpers: setCaret, setCaretAtTextOffset, enableHostEditorBridge (now with an "enabled" parameter) and a shared "until" wait in tests/reviewTestUtils.ts. - ReviewDomEditors.test.ts records each global change (browser features, window globals, library globals) and removes it in afterAll. - NotionEnvironment.test.ts: the preload gives execCommand. Fake timers replace the real 1 s wait and stop the revert check timer of the typing write. - Remove the Trix mock from the bridge test. The real Trix test now also checks the typing write during a composition. The "bridge is off" composition test uses a CodeMirror 5 controller. - full.e2e.test.ts: one ModelEditor type, one "w" prediction wait, seeds from the fixture constants. Remove a Notion sleep that the revert counter makes unnecessary. Give the reason for the other sleep. - Notion fixture: runs() reads the model. Remove the Redo stack, the undoDepth counter and the unused export. - Coverage matrix: add the IME composition behavior and the Google Docs note behavior. Map the CKEditor 5 inline object row to tests that fail without the support. Mark the two guard tests in their descriptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- review-reference.md: the Notion checks are a numbered list. Each sentence has one claim. - editor-capabilities.md: the CKEditor 5 inline object item has one claim in each sentence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symptom: Escape closed the suggestion menu, but the menu can open again some time later. CI job "E2E Full (chrome, 1/2)" failed in the test "Notion-like page: Escape closes the suggestion popup only". The menu stayed visible for 5 s after Escape. Cause: Escape called only clearSuggestions. It did not stop the debounced prediction request, and it did not change the request id. Thus the answer to a request that was on its way when the user pushed Escape showed the menu again. On a slow machine, the answer to the " " of " w" can show a word that starts with "w" first. Then the answer to "w" comes after Escape. Fix: Escape uses dismissEntry, as a click does. It stops the pending request and the idle timers and increments the request id. The entry stays active, and Escape goes to the page as before. Test: SuggestionManagerRuntime.test.ts "Escape drops the prediction answers that are still on their way". It failed before the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While inline mode is on, the options page also stores prefix-only mode, Tab acceptance and 10 suggestions. The tests turned off only inline mode. Thus later tests ran with prefix-only mode and 10 suggestions. - INLINE_MODE_OFF holds inline mode off and the defaults of its locks. restoreInlineMode() writes it. The inline tests and openTypingEditor use it. - beforeEach writes INLINE_MODE_OFF and the space after an accepted word when an earlier test wrote settings. - Cause of the order-dependent failure of "Inline suggestion prediction is inserted on TAB in #test-input": the CKEditor inline image test turns off the space after an accepted word and does not turn it on again. The inline test expects that space. - The measurement unit test sets the enabled languages. An auto-detect test leaves only English and Arabic, and then the Polish step failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # tests/e2e/full.e2e.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR closes the editor-support gaps that an audit found in tests, IME safety, docs and coverage of high-traffic sites. The work is in six phases. Each real bug has its own
fix:commit with a regression test that failed before the fix.insert-fragment, Quill native input, typing paths for Lexical, Draft.js, CKEditor 5 and Trix, CodeMirror 5). A fingerprint without its editor permits only an attempt. A new test proves that each of the ten typing-path fingerprints refuses the write.@tiptap/*packages (dev dependency only).roosterjspackage (dev dependency only).Other bugs found and fixed
Constraints kept
Test plan
bun run checkbun run test: 97,576 + 42 passbun run check:e2e:coverage: 268 behaviorsbun run test:e2e:full(Chrome): 262 pass, 0 failbun run test:e2e:full --platform=firefox: 257 pass, 0 fail🤖 Generated with Claude Code