-
Notifications
You must be signed in to change notification settings - Fork 61
AXON-1694: rovo dev error boundary #1459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 10 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
5ebe3e2
rovo dev error boundary
jwang19-atlassian cd767d0
expand href for custom buton in dialog message
jwang19-atlassian 89d69c3
add report render error msg type
jwang19-atlassian b052cdd
report redner error msg type
jwang19-atlassian 0d12a97
wrap error boundary in rovo dev view
jwang19-atlassian ad8ec1d
start new session msg
jwang19-atlassian 1d2ca41
fix: remove delay for reset error state
jwang19-atlassian 793ddd8
Merge branch 'main' into axon-1694-error-boundary
jwang19-atlassian 8c8e8d0
ut
jwang19-atlassian 5f8bb94
keep same func signature
jwang19-atlassian 2d49e13
fix: ut
jwang19-atlassian 5c87a8b
fix: structure error log
jwang19-atlassian 76ab487
Merge branch 'main' into axon-1694-error-boundary
jwang19-atlassian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,285 @@ | ||
| import { render, screen } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import React from 'react'; | ||
|
|
||
| import { RovoDevErrorBoundary } from './RovoDevErrorBoundary'; | ||
| import { RovoDevViewResponseType } from './rovoDevViewMessages'; | ||
|
|
||
| jest.mock('./common/DialogMessage', () => ({ | ||
| DialogMessageItem: ({ msg, customButton, onLinkClick }: any) => ( | ||
| <div data-testid="dialog-message-item"> | ||
| <div data-testid="dialog-title">{msg.title}</div> | ||
| <div data-testid="dialog-text">{msg.text}</div> | ||
| {msg.stackTrace && <div data-testid="dialog-stack-trace">{msg.stackTrace}</div>} | ||
| {msg.stderr && <div data-testid="dialog-stderr">{msg.stderr}</div>} | ||
| {customButton && ( | ||
| <button data-testid="custom-button" onClick={customButton.onClick}> | ||
| {customButton.text} | ||
| </button> | ||
| )} | ||
| <button data-testid="link-click" onClick={() => onLinkClick('test-link')}> | ||
| Link | ||
| </button> | ||
| </div> | ||
| ), | ||
| })); | ||
|
|
||
| const ThrowError: React.FC<{ shouldThrow?: boolean; errorMessage?: string }> = ({ | ||
| shouldThrow = true, | ||
| errorMessage = 'Test error', | ||
| }) => { | ||
| if (shouldThrow) { | ||
| throw new Error(errorMessage); | ||
| } | ||
| return <div>No error</div>; | ||
| }; | ||
|
|
||
| describe('RovoDevErrorBoundary', () => { | ||
| let mockPostMessage: jest.Mock; | ||
| let mockOnStartNewSession: jest.Mock; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| mockPostMessage = jest.fn(); | ||
| mockOnStartNewSession = jest.fn(); | ||
|
|
||
| jest.spyOn(console, 'error').mockImplementation(() => {}); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.restoreAllMocks(); | ||
| }); | ||
|
|
||
| describe('Normal rendering', () => { | ||
| it('renders children when there is no error', () => { | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <div>Test content</div> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| expect(screen.getByText('Test content')).toBeTruthy(); | ||
| expect(screen.queryByTestId('dialog-message-item')).not.toBeTruthy(); | ||
| expect(mockPostMessage).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Error handling', () => { | ||
| it('catches errors and displays error dialog', () => { | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| expect(screen.getByTestId('dialog-message-item')).toBeTruthy(); | ||
| expect(screen.getByTestId('dialog-title')).toBeTruthy(); | ||
| expect(screen.getByText('Rovo Dev encountered a rendering error')).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('displays error message in dialog', () => { | ||
| const errorMessage = 'Custom error message'; | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError errorMessage={errorMessage} /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| expect(screen.getByTestId('dialog-text')).toBeTruthy(); | ||
| expect(screen.getByText(errorMessage)).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('displays default error message when error message is missing', () => { | ||
| const ErrorWithoutMessage = () => { | ||
| const error = new Error(); | ||
| error.message = ''; | ||
| throw error; | ||
| }; | ||
|
|
||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ErrorWithoutMessage /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| expect(screen.getByText('An unexpected error occurred')).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('reports error to backend via postMessage', () => { | ||
| const errorMessage = 'Test error message'; | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError errorMessage={errorMessage} /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| expect(mockPostMessage).toHaveBeenCalledTimes(1); | ||
| expect(mockPostMessage).toHaveBeenCalledWith({ | ||
| type: RovoDevViewResponseType.ReportRenderError, | ||
| errorType: 'Error', | ||
| errorMessage: errorMessage, | ||
| errorStack: expect.any(String), | ||
| componentStack: expect.any(String), | ||
| }); | ||
| }); | ||
|
|
||
| it('includes error stack trace in dialog when available', () => { | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| const stackTraceElement = screen.queryByTestId('dialog-stack-trace'); | ||
| expect(stackTraceElement).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('includes component stack in stderr field when available', () => { | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| const stderrElement = screen.queryByTestId('dialog-stderr'); | ||
| expect(stderrElement).toBeTruthy(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Start new session', () => { | ||
| it('renders start new session button', () => { | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| const button = screen.getByTestId('custom-button'); | ||
| expect(button).toBeTruthy(); | ||
| expect(screen.getByText('Start New Chat Session')).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('calls postMessage with StartNewSession when button is clicked', async () => { | ||
| const user = userEvent.setup(); | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| const button = screen.getByTestId('custom-button'); | ||
| await user.click(button); | ||
|
|
||
| expect(mockPostMessage).toHaveBeenCalledWith({ | ||
| type: RovoDevViewResponseType.StartNewSession, | ||
| }); | ||
| }); | ||
|
|
||
| it('calls onStartNewSession callback when button is clicked', async () => { | ||
| const user = userEvent.setup(); | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage} onStartNewSession={mockOnStartNewSession}> | ||
| <ThrowError /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| const button = screen.getByTestId('custom-button'); | ||
| await user.click(button); | ||
|
|
||
| expect(mockOnStartNewSession).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('resets error state after starting new session', async () => { | ||
| const user = userEvent.setup(); | ||
| const NoErrorComponent = () => <div>No error</div>; | ||
|
|
||
| const { rerender } = render( | ||
| <RovoDevErrorBoundary | ||
| key="error-boundary-1" | ||
| postMessage={mockPostMessage} | ||
| onStartNewSession={mockOnStartNewSession} | ||
| > | ||
| <ThrowError /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| expect(screen.getByTestId('dialog-message-item')).toBeTruthy(); | ||
|
|
||
| const button = screen.getByTestId('custom-button'); | ||
| await user.click(button); | ||
|
|
||
| // Verify that postMessage and callback were called | ||
| expect(mockPostMessage).toHaveBeenCalledWith({ | ||
| type: RovoDevViewResponseType.StartNewSession, | ||
| }); | ||
| expect(mockOnStartNewSession).toHaveBeenCalledTimes(1); | ||
|
|
||
| // After clicking, the error state should be reset internally | ||
| rerender( | ||
| <RovoDevErrorBoundary | ||
| key="error-boundary-2" | ||
| postMessage={mockPostMessage} | ||
| onStartNewSession={mockOnStartNewSession} | ||
| > | ||
| <NoErrorComponent /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| // The error boundary should now render children normally | ||
| expect(screen.getByText('No error')).toBeTruthy(); | ||
| expect(screen.queryByTestId('dialog-message-item')).not.toBeTruthy(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Error boundary lifecycle', () => { | ||
| it('updates state correctly when error is caught', () => { | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| // Error should be caught and displayed | ||
| expect(screen.getByTestId('dialog-message-item')).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('handles multiple errors correctly', () => { | ||
| const { rerender } = render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError errorMessage="First error" /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| expect(screen.getByText('First error')).toBeTruthy(); | ||
| expect(mockPostMessage).toHaveBeenCalledTimes(1); | ||
|
|
||
| // Simulate a new error by re-rendering with a different error | ||
| rerender( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError errorMessage="Second error" /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| // Should still show error dialog | ||
| expect(screen.getByTestId('dialog-message-item')).toBeTruthy(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Link click handler', () => { | ||
| it('provides empty link click handler', () => { | ||
| render( | ||
| <RovoDevErrorBoundary postMessage={mockPostMessage}> | ||
| <ThrowError /> | ||
| </RovoDevErrorBoundary>, | ||
| ); | ||
|
|
||
| const linkButton = screen.getByTestId('link-click'); | ||
| expect(linkButton).toBeTruthy(); | ||
|
|
||
| // Should not throw when clicked | ||
| expect(() => { | ||
| linkButton.click(); | ||
| }).not.toThrow(); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.