Skip to content

fix: improve iOS bridge stability#410

Merged
dcalhoun merged 5 commits intotrunkfrom
fix/improve-ios-bridge-stability
Apr 1, 2026
Merged

fix: improve iOS bridge stability#410
dcalhoun merged 5 commits intotrunkfrom
fix/improve-ios-bridge-stability

Conversation

@dcalhoun
Copy link
Copy Markdown
Member

@dcalhoun dcalhoun commented Mar 30, 2026

What?

Ensure the iOS native-to-web bridge is fully initialized before signaling editor readiness, and guard all JS bridge calls against early invocation.

Why?

Fix CMM-2008. Fix CMM-2009.

The onEditorLoaded signal was firing (via IntersectionObserver) before window.editor.* methods were assigned in the useHostBridge React effect. When native iOS code called bridge methods immediately after receiving the signal — e.g., getTitleAndContent() during an autosave triggered by opening the block inserter — the methods were undefined, causing TypeErrors (CMM-2008) or ReferenceErrors (CMM-2009).

How?

JS side

  • Replace the standalone IntersectionObserver in use-editor-visible with a new useEditorReady hook that coordinates two conditions before dispatching onEditorLoaded:
    1. All window.editor.* bridge methods have been assigned (signaled by useHostBridge via a markBridgeReady callback)
    2. The editor content element is visible in the viewport (via IntersectionObserver on the VisualEditor/TextEditor root element, received through forwardRef)
  • Use a callback ref in useEditorReady so the IntersectionObserver attaches when React mounts the editor content — not on initial render when the element doesn't exist yet.
  • Defer editorLoaded() by one frame via requestAnimationFrame so the browser has painted the editor content before the native host starts the fade-in animation.
  • Fix missing window.editor.focus cleanup on unmount.

iOS side

  • Add isReady guards to undo(), redo(), dismissTopModal(), isCodeEditorEnabled, appendTextAtCursor(), getContent(), and getTitleAndContent(). Previously only setContent() was guarded.
  • Reset isReady to false in controllerWebContentProcessDidTerminate so bridge calls are blocked until the editor re-emits onEditorLoaded after a WebView process crash.
  • Fire-and-forget methods silently return when not ready. Async throwing methods throw EditorNotReadyError.

Tests

  • Add use-host-bridge tests verifying window.editor.* methods are assigned and markBridgeReady is called, and that all methods are cleaned up on unmount.

Test infrastructure (first two commits)

  • Consolidate @wordpress mock stubs into __mocks__/ directory.
  • Remove unnecessary vi.mock() calls from existing tests.

Testing Instructions

  1. Open the editor on iOS and immediately tap the "+" block inserter button.
  2. Verify the inserter opens without a crash or error message.
  3. Verify the editor loading indicator dismisses at the right time and the WebView content is visible when it fades in (no "pop" or flash).
  4. Open a new empty post and verify the post title input is auto-focused with the keyboard visible.
  5. Open the editor normally and verify all editing functionality works (undo/redo, block insertion, code editor toggle).

Accessibility Testing Instructions

N/A — no UI changes.

Screenshots or screencast

N/A

@github-actions github-actions bot added the [Type] Bug An existing feature does not function as intended label Mar 30, 2026
@dcalhoun dcalhoun force-pushed the fix/improve-ios-bridge-stability branch 2 times, most recently from c74ef1b to e494542 Compare March 30, 2026 18:33
dcalhoun and others added 5 commits March 31, 2026 11:59
Add shared mock stubs for @WordPress packages that crash when imported
in the test environment. Flatten existing directory-based mocks (i18n,
components) to top-level files for consistency. Remove exports that
have no effect on test outcomes — the files themselves are still needed
to prevent Vitest from importing the real modules.

Tests declare vi.mock('module') without a factory to use the shared
stub, and override locally only when test-specific behavior is needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove @wordpress/preferences (editor.test.jsx) and @wordpress/i18n
(editor-load-notice, offline-indicator) mocks that have no effect on
test outcomes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…aded

Replace the IntersectionObserver in use-editor-visible with a new
useEditorReady hook that coordinates two conditions before signaling
the native host via onEditorLoaded:

1. All window.editor.* bridge methods are assigned (from useHostBridge)
2. The editor content is visible in the viewport (via
   IntersectionObserver on the VisualEditor/TextEditor root element)

useEditorReady returns a callback ref (for DOM attachment observation),
a standard ref (for imperative access in useHostBridge), and a
markBridgeReady callback. The callback ref is forwarded to
VisualEditor/TextEditor via forwardRef so the observer fires when the
actual editor content is in the viewport — not just the wrapper div.

The editorLoaded() call is deferred by one frame via
requestAnimationFrame so the browser has painted the editor content
before the native host starts the fade-in animation.

Remove the now-unused useEditorVisible hook and add the missing
window.editor.focus cleanup on unmount.

The error path in editor-environment.js retains its own editorLoaded()
call since useEditorReady won't run when initialization fails.

Addresses CMM-2008 and CMM-2009.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add isReady guards to undo(), redo(), dismissTopModal(),
isCodeEditorEnabled, appendTextAtCursor(), getContent(), and
getTitleAndContent(). Previously only setContent() was guarded.

Reset isReady to false in controllerWebContentProcessDidTerminate so
JS bridge calls are blocked until the editor re-emits onEditorLoaded
after a WebView process crash and reload.

Fire-and-forget methods silently return when not ready. Async throwing
methods (getContent, getTitleAndContent) throw EditorNotReadyError so
callers can handle the case explicitly.

Addresses CMM-2008 and CMM-2009.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verify window.editor.* methods are assigned and markBridgeReady is
called, and that all methods are cleaned up on unmount.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dcalhoun dcalhoun force-pushed the fix/improve-ios-bridge-stability branch from e494542 to 5cb4d59 Compare March 31, 2026 16:03
@dcalhoun dcalhoun marked this pull request as ready for review March 31, 2026 16:16
@dcalhoun dcalhoun requested a review from kean March 31, 2026 16:17
Copy link
Copy Markdown
Contributor

@kean kean left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great.

///
/// - parameter text: The text to append at the cursor position.
public func appendTextAtCursor(_ text: String) {
guard isReady else { return }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it actually happen? If yes, it might be worth saving it as "pending update" and applying automatically when the editor is ready. Not sure if it's worth it though. If there is a spinner, and you hit Cmd+V via the keyboard, you probably wouldn't expect it to work.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I did not observe any reports of this occurring nor did I produce it. The guard was applied universally to avoid unexpected errors.

I agree. Stashing and later applying the pending update would be nice, but I'm not sure how realistic that being a real problem is currently. I'll likely defer that type of improvement until a problem surfaces.

@dcalhoun dcalhoun merged commit 590a121 into trunk Apr 1, 2026
17 checks passed
@dcalhoun dcalhoun deleted the fix/improve-ios-bridge-stability branch April 1, 2026 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants