[MM-68738][MM-68739][MM-68741][MM-68742][MM-68747][MM-68748][MM-68750][MM-68814][MM-68880][MM-68908][MM-69030][MM-69783] Guard against destroyed Electron objects and premature window creation - #3921
Conversation
…][MM-68814][MM-68880][MM-68908][MM-69030][MM-69783] Guard against destroyed Electron objects and premature window creation
📝 WalkthroughWalkthroughThe changes add destruction checks for Electron windows, trays, and parent-window IPC, update related mocks, and introduce readiness-aware handling for the app activation event. ChangesElectron teardown safety
App activation readiness
Estimated code review effort: 2 (Simple) | ~15 minutes Sequence Diagram(s)sequenceDiagram
participant ElectronApp
participant handleAppActivate
participant MainWindow
ElectronApp->>handleAppActivate: activate event
handleAppActivate->>ElectronApp: check isReady()
alt app is ready
handleAppActivate->>MainWindow: show()
else app is not ready
handleAppActivate->>ElectronApp: once('ready')
ElectronApp-->>handleAppActivate: ready event
handleAppActivate->>MainWindow: show()
end
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
|
❌ E2E Test Setup Failed Failed to create E2E test instances: failed to create installation: failed with status code 409 |
|
Test check c57a656 — action needed Tests are present but only update mocks for compatibility; the core isDestroyed() guard branches (the actual crash fixes) are never exercised in any test. More details (truncated)Test Files Detected
AnalysisThe PR fixes multiple crash-on-destruction bugs by adding 1. Mock compatibility updates (not new coverage):
2. Genuine new test coverage:
Coverage gaps for the core crash-prevention logic:
The most safety-critical fixes — the Files AnalyzedProduction files (5/5):
Test files (3/3):
...truncated. View full analysis details Suggestions
To override, comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/app/views/MattermostWebContentsView.test.js (1)
127-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for destroyed parent windows.
All updated mocks return
falsefromisDestroyed(), so the new early-return behavior is untested. Add cases withisDestroyed()returningtrueand assert that parentwebContents.send()is not called.Also applies to: 193-193, 257-257, 275-275, 305-305, 334-334, 466-466, 498-498, 559-559, 635-635, 642-642, 649-649, 658-658, 668-668
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/views/MattermostWebContentsView.test.js` at line 127, Add test cases in MattermostWebContentsView.test.js for each affected parent-window mock, configuring isDestroyed() to return true and asserting parent webContents.send() is not called. Cover the early-return behavior while preserving the existing assertions for non-destroyed windows.src/main/app/app.test.js (1)
69-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
jest.mocked()for mock-function access.The new tests call
.mockReturnValue(),.mockImplementation(), and assertions directly on mocked functions. Wrapapp.isReady,app.once, andMainWindow.showwithjest.mocked()to follow the repository test convention.Proposed adjustment
- app.isReady.mockReturnValue(true); + jest.mocked(app.isReady).mockReturnValue(true); ... - expect(MainWindow.show).toHaveBeenCalled(); + expect(jest.mocked(MainWindow.show)).toHaveBeenCalled(); ... - app.once.mockImplementation((event, cb) => { + jest.mocked(app.once).mockImplementation((event, cb) => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/app/app.test.js` around lines 69 - 85, Update the tests for handleAppActivate to wrap app.isReady, app.once, and MainWindow.show with jest.mocked() before calling mockReturnValue, mockImplementation, or assertion methods, preserving the existing test behavior and expectations.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/mainWindow/mainWindow.ts`:
- Around line 133-137: Update show() to obtain the window through the guarded
get() accessor instead of directly using this.win.browserWindow. Reuse that
guarded result for the branch and all subsequent show/focus operations,
preserving the existing behavior while preventing operations on a destroyed
window.
- Around line 222-226: Update the onBlur() flow to check whether the target
window is destroyed before calling getContentBounds() or emitting bounds. Return
immediately for a destroyed window, then pass the validated window to
saveWindowState() so its guard remains effective.
---
Nitpick comments:
In `@src/app/views/MattermostWebContentsView.test.js`:
- Line 127: Add test cases in MattermostWebContentsView.test.js for each
affected parent-window mock, configuring isDestroyed() to return true and
asserting parent webContents.send() is not called. Cover the early-return
behavior while preserving the existing assertions for non-destroyed windows.
In `@src/main/app/app.test.js`:
- Around line 69-85: Update the tests for handleAppActivate to wrap app.isReady,
app.once, and MainWindow.show with jest.mocked() before calling mockReturnValue,
mockImplementation, or assertion methods, preserving the existing test behavior
and expectations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 526855ce-195e-490c-89f7-9290266117b5
📒 Files selected for processing (7)
src/app/mainWindow/mainWindow.tssrc/app/system/tray/tray.tssrc/app/views/MattermostWebContentsView.test.jssrc/app/views/MattermostWebContentsView.tssrc/main/app/app.test.jssrc/main/app/app.tssrc/main/app/initialize.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/app/mainWindow/mainWindow.test.js (1)
589-590: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCover the destroyed-window paths with a stable fixture.
jest.resetAllMocks()resets theisDestroyedimplementation after each test, so later calls returnundefinedinstead of modeling Electron’s boolean API. Reinitialize it inbeforeEach()(or deliberately usejest.clearAllMocks()), addtrue-state tests for the new guards, and providegetContentBoundsonbaseWindowbefore testinggetBounds().Based on learnings, reset mock state deliberately when assertions depend on repeated test invocations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/mainWindow/mainWindow.test.js` around lines 589 - 590, Update the mainWindow test setup so beforeEach reinitializes baseWindow.isDestroyed with a boolean-returning implementation, or replace resetAllMocks with clearAllMocks where appropriate. Add coverage for both destroyed-window guard paths, and define baseWindow.getContentBounds before exercising getBounds().Source: Learnings
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/app/mainWindow/mainWindow.test.js`:
- Around line 589-590: Update the mainWindow test setup so beforeEach
reinitializes baseWindow.isDestroyed with a boolean-returning implementation, or
replace resetAllMocks with clearAllMocks where appropriate. Add coverage for
both destroyed-window guard paths, and define baseWindow.getContentBounds before
exercising getBounds().
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e6a50353-e092-4478-82c8-165f6f1436d9
📒 Files selected for processing (2)
src/app/mainWindow/mainWindow.test.jssrc/app/mainWindow/mainWindow.ts
Summary
Sentry surfaced a cluster of crashes where the main window, a server's
WebContents, or the tray were accessed after being destroyed, or where aBrowserWindowwas constructed before Electron'sreadyevent fired. In most cases the offending code either had noisDestroyed()check at all, or checked one object (e.g. a view's ownwebContents) while dereferencing another (e.g. its parent window) unchecked.This PR adds the missing guards at each crash site, and in the "Object has been destroyed" category, fixes the shared root cause:
MainWindow.get()/getBounds()returned a destroyedBrowserWindowinstead ofundefined, so the many existingif (!mainWindow)checks throughout the codebase didn't catch it.TypeError: Object has been destroyed
MattermostWebContentsView's load-retry, cert-error, and load-success/incompatible-server paths sent IPC messages toparentWindow.webContentsafter the parent window was destroyed. Added asendToParentWindowhelper that checksisDestroyed()before sending.AppState.emitStatusForServer()/updateUnreadsAndMentionsPerServer()fan out to listeners (e.g. the badge) that calledMainWindow.get()and used the result without checking for destruction. Fixed by hardeningMainWindow.get().ServerManager.setLoggedIn()drivesTabManager.setActiveTab(), which sent toMainWindow.get()with the same gap. Fixed by hardeningMainWindow.get().ViewManager.updateViewTitle()(fromhandlePageTitleUpdated) drivesTabManager.handleViewUpdated(), sameMainWindow.get()gap. Fixed by hardeningMainWindow.get().PermissionsManager.doPermissionRequest()readmainWindow.webContents.idoff a possibly-destroyedMainWindow.get()result. Fixed by hardeningMainWindow.get().downloadsDropdownView.updateDownloadsDropdown()calledMainWindow.getBounds()duringdownloadsManager.saveAll(), which calledgetContentBounds()on a destroyed window. Fixed by routinggetBounds()through the hardenedget().MainWindow.saveWindowState()read bounds/maximized/fullscreen state off theBrowserWindowonblur/closewithout anisDestroyed()check. Added the guard directly.AppState.emitStatus()had the sameMainWindow.get()gap as MM-68739. Fixed by hardeningMainWindow.get().Error: Tray is destroyed
MenuManager's IPC-driven menu refresh calledTray.setMenu()during shutdown, hitting a destroyed nativeTray.setMenu()now checksisDestroyed()before callingsetContextMenu(), matching the existing guard inupdate().setMenu()call, triggered byrefreshMenu()instead. Covered by the same guard.Error: Cannot create BrowserWindow before app is ready
activatecould fire before Electron'sreadyevent, soMainWindow.show()calledinit()and constructed aBrowserWindowtoo early.handleAppActivate()now checksapp.isReady()and defers toapp.once('ready', ...)if it isn't.Ticket Link
Release Note
Change Impact: 🟡 Medium
Regression Risk: Lifecycle/window-management hardening across startup/activation, IPC, tray menu updates, and multiple Electron view paths; while changes are defensive (destroyed-object guards) and tests cover
handleAppActivate/some window behaviors, key destroyed-object guard cases are not fully exercised, leaving moderate risk around startup/activate and window/tray state transitions.QA Recommendation: Perform brief manual smoke testing: app startup, macOS dock “activate” before/at ready, main window open/close, tray menu interactions, and a scenario that triggers Mattermost view load/retry (including teardown) to ensure no crashes/regressions.
Generated by CodeRabbitAI