Skip to content

Fix CKEditor 4 typing after frame rewrites, a watchdog restart, and a lost arrow-key choice - #477

Merged
bartekplus merged 5 commits into
masterfrom
claude/master-test-failure-9e25bd
Oct 6, 2026
Merged

bartekplus merged 5 commits into
masterfrom
claude/master-test-failure-9e25bd

Conversation

@bartekplus

@bartekplus bartekplus commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Problem

The e2e test ckeditor4 typing accepts a prediction mid-line and keeps the caret after the word failed on master in Firefox (run 37345187610). The test is new in #476 and it is flaky.

Under CPU load I reproduced it locally: 7 of 120 CKEditor 4 typing runs in Firefox failed. In each failure, the prediction menu was visible, but Tab did not accept the prediction. An event log in the editing frame showed that nobody handled the Tab key, so Tab moved the focus out of the editor.

Root cause

CKEditor 4 writes its editing frame with document.open(). It does this at start and on each setData(). document.open() erases all event listeners of the document and its window, and it replaces the root element. The iframe, its window and the Document object stay.

  • The MAIN-world bridge adds its own listeners again, but it sets itself to "disabled" and waits for the content script to send "enabled" again. The content script did not know about the rewrite, so it did not send it. The bridge then ignored the write request for the accepted word.
  • In Chrome the same rewrite also kills the content script: all its listeners and its root observer are gone. After editor.setData(), no prediction showed at all.

In Firefox it is flaky because the order of "content script starts" and "CKEditor writes the frame" changes with CPU load.

Fix

The content script observes the Document node. A MutationObserver on the document stays after document.open() (I measured this in Chrome; the event listeners do not stay). When the root element changes, the content script destroys its instance and starts a new one on the new document. The new instance adds all its listeners again and sends "enabled" to the bridge again. A late config answer does not enable the destroyed instance.

Tests

  • New e2e test: CKEditor 4 setData rewrites its frame with document.open, and typing still accepts a prediction. In Chrome it fails without the fix (the same "Tab not accepted" failure as the master flake) and passes 10 of 10 runs with it.
  • New unit test in tests/content_script.behavior.test.ts. It fails without the fix.
  • Coverage matrix: new behavior rewritten_document_restart.
  • Local: bun run check, bun run test, bun run check:e2e:coverage, bun run test:e2e and bun run test:e2e:full --platform=chrome pass.
  • Firefox: my local shell cannot start Firefox, so a temporary workflow ran the CKEditor 4 typing tests 20 times per job under CPU load (run 37352747215): 600 of 600 Firefox runs and 100 of 100 Chrome runs passed. Before the fix, about 6% of the local Firefox runs under CPU load failed. The last commit removes that workflow.

Two more fixes (found while I checked the CI of this PR)

2. Chrome: a watchdog restart closed the menu (ckeditor4 typing expands a snippet)

One CI run failed in Chrome: the menu showed "Best regards", but Tab did not expand ftsig.

  • Cause: in Chrome, the content script in the CKEditor 4 frame can start while CKEditor still writes the frame (readyState: "loading", no <body>). Then the runtime observes <html>. Later HostChangeWatcher.watchDog() saw "observed node ≠ document.body" and restarted the runtime. The restart closed the menu. A restart between "menu shown" and Tab sent Tab to the browser.
  • Fix: if the observed node is still in the document and contains the body, the watchdog observes the body and does not restart. A replaced root still causes a restart.
  • Evidence: 4 parallel Chrome instances under full CPU load: before, 9 of 120 snippet runs failed; after, 120 of 120 passed, and all CKEditor 4 tests passed 288 of 288. New unit test.

3. A late answer moved the menu selection back to the first row

  • Bug: the user pressed ArrowDown to a suggestion, then a late prediction answer came and highlighted the first row again. Tab then inserted a word that the user did not choose. Reproduction: type th, ArrowDown to that, a new answer comes, Tab inserts the .
  • Fix: the keyboard handler records the chosen suggestion. A new answer keeps it highlighted when the answer contains it. An edit clears the choice.
  • Tests: new e2e test A late prediction answer keeps the suggestion that the user chose with an arrow key (failed before the fix, passes 5 of 5 after) and two unit tests.

Not in this PR

  • The MAIN-world start script (early Tab bridge) also loses its listener after document.open(). In a rewritten frame, the content script's own key handler accepts with Tab, so I did not change it.
  • The CI log from master also showed a lost space ("We saww teh") once. I did not reproduce that. If it comes back with this fix, it needs its own look.

🤖 Generated with Claude Code

bartekplus and others added 3 commits October 5, 2026 20:00
…ewrites

The e2e test "ckeditor4 typing accepts a prediction mid-line" failed on
master in Firefox. The prediction menu was visible, but Tab did not
accept the prediction. Tab moved the focus out of the editor.

Root cause: CKEditor 4 writes its editing frame with document.open().
It does this at start and on each setData(). document.open() erases
all event listeners of the document and its window, and it replaces
the root element. The iframe, its window and the Document object stay.

- The MAIN-world bridge adds its listeners again, but it sets itself
  to "disabled". It waits for the content script to send "enabled"
  again. The content script did not send it again, because it did not
  know about the rewrite. Thus the bridge ignored the write request,
  and Tab did not accept the prediction.
- In Chrome, the content script also lost all its listeners and its
  root observer. After setData(), no prediction showed.

The failure was flaky in Firefox because the order of the content
script start and the CKEditor frame write changes with CPU load.
Under CPU load, 7 of 120 local CKEditor 4 runs failed.

Fix: the content script observes the Document node. A MutationObserver
on the Document stays after document.open() (measured in Chrome). When
the root element changes, the content script destroys its instance and
starts a new one on the new document. The new instance adds its
listeners again and sends "enabled" to the bridge again. A late config
answer does not enable the destroyed instance.

Tests:
- e2e: "CKEditor 4 setData rewrites its frame with document.open, and
  typing still accepts a prediction". It fails in Chrome without the fix
  (Tab is not accepted) and passes 10 of 10 runs with the fix.
- unit: a rewritten document starts a new instance that sends "enabled"
  to the host bridge again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run the CKEditor 4 typing tests 20 times in each job under CPU load in Firefox (6 jobs) and Chrome (1 job). Remove this workflow before merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The probe passed: 600 of 600 Firefox runs and 100 of 100 Chrome runs under CPU load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bartekplus
bartekplus marked this pull request as ready for review October 5, 2026 18:20
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

bartekplus and others added 2 commits October 5, 2026 21:29
…bserved root

The e2e test "ckeditor4 typing expands a snippet in one native undo
step" failed at random in Chrome. The menu showed "Best regards", but
Tab did not expand the shortcut. Tab moved the focus out of the editor.

Root cause: in Chrome, the content script in the CKEditor 4 editing
frame can start while CKEditor still writes the frame. Then the
document is "loading" and has no body. The runtime observes the root
element in place of the body. Later, HostChangeWatcher.watchDog()
compared the observed node with document.body. The nodes were not the
same, so it restarted the runtime. The restart hid the open menu. When
the restart came between the menu and the Tab key, the Tab key went to
the browser.

Fix: when the observed node is still in the document and contains the
current body, the watchdog observes the body and does not restart. A
replaced root (a node that is not in the document) still causes a
restart.

Evidence: 4 parallel Chrome instances under full CPU load ran the
snippet test 120 times. Before the fix, 9 of 120 runs failed. With the
fix, 120 of 120 runs passed. All CKEditor 4 tests passed 288 of 288
runs under the same load.

Tests: a new unit test in tests/content_script.behavior.test.ts fails
without the fix. The coverage matrix has a new behavior,
late_body_without_restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ow key

A prediction answer that came after an arrow key moved the menu
selection back to the first row. Then Tab accepted a word that the user
did not choose. This occurs when the answer for the typed text is late:
the user presses ArrowDown on the old menu, and the new answer resets
the selection.

Reproduction: type "th" in an input. The menu shows "the", "that",
"this". Press ArrowDown to "that". A new answer for the same text comes.
Before this fix, Tab inserted "the ".

Fix: the keyboard handler records the suggestion that the user moved to
(entry.chosenSuggestion). A new answer keeps that suggestion highlighted
when the answer contains it. If the answer does not contain it, the
first row is highlighted. An edit or a cleared menu clears the choice.

Tests:
- e2e: "A late prediction answer keeps the suggestion that the user
  chose with an arrow key". It sends a new answer after ArrowDown. It
  failed without the fix ("the " in place of "that ") and passes 5 of 5
  runs with the fix.
- unit: the keyboard handler records the choice; the session keeps it
  for a late answer, drops it when the answer does not contain it, and
  clears it on input.
- The coverage matrix has a new behavior, late_answer_keeps_arrow_choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bartekplus bartekplus changed the title Fix CKEditor 4 typing after the editor rewrites its frame (Firefox flake on master, Chrome setData) Fix CKEditor 4 typing after frame rewrites, a watchdog restart, and a lost arrow-key choice Oct 5, 2026
@bartekplus
bartekplus merged commit c82270e into master Oct 6, 2026
15 of 24 checks passed
@bartekplus
bartekplus deleted the claude/master-test-failure-9e25bd branch October 6, 2026 05:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant