2025-03-05 20:50:19 +01:00
/ *
Copyright 2025 New Vector Ltd .
SPDX - License - Identifier : AGPL - 3.0 - only OR LicenseRef - Element - Commercial
Please see LICENSE in the repository root for full details .
* /
import { describe , expect , test , vi } from "vitest" ;
import { render , screen } from "@testing-library/react" ;
2025-03-07 15:18:32 +01:00
import {
type FC ,
type ReactElement ,
type ReactNode ,
useCallback ,
useState ,
} from "react" ;
2025-03-05 20:50:19 +01:00
import { BrowserRouter } from "react-router-dom" ;
import userEvent from "@testing-library/user-event" ;
2025-03-12 09:44:41 +01:00
import {
type CallErrorRecoveryAction ,
GroupCallErrorBoundary ,
} from "./GroupCallErrorBoundary.tsx" ;
2025-03-05 20:50:19 +01:00
import {
ConnectionLostError ,
E2EENotSupportedError ,
type ElementCallError ,
InsufficientCapacityError ,
2025-10-03 14:43:22 -04:00
MatrixRTCTransportMissingError ,
2025-03-05 20:50:19 +01:00
UnknownCallError ,
} from "../utils/errors.ts" ;
import { mockConfig } from "../utils/test.ts" ;
2025-03-07 10:12:23 +01:00
import { ElementWidgetActions , type WidgetHelpers } from "../widget.ts" ;
2025-03-05 20:50:19 +01:00
test . each ( [
{
2025-10-03 14:43:22 -04:00
error : new MatrixRTCTransportMissingError ( "example.com" ) ,
2025-03-05 20:50:19 +01:00
expectedTitle : "Call is not supported" ,
} ,
{
error : new ConnectionLostError ( ) ,
expectedTitle : "Connection lost" ,
expectedDescription : "You were disconnected from the call." ,
} ,
{
error : new E2EENotSupportedError ( ) ,
expectedTitle : "Incompatible browser" ,
expectedDescription :
"Your web browser does not support encrypted calls. Supported browsers include Chrome, Safari, and Firefox 117+." ,
} ,
{
error : new InsufficientCapacityError ( ) ,
expectedTitle : "Insufficient capacity" ,
expectedDescription :
"The server has reached its maximum capacity and you cannot join the call at this time. Try again later, or contact your server admin if the problem persists." ,
} ,
] ) (
"should report correct error for $expectedTitle" ,
async ( { error , expectedTitle , expectedDescription } ) = > {
const TestComponent = ( ) : ReactNode = > {
throw error ;
} ;
const onErrorMock = vi . fn ( ) ;
const { asFragment } = render (
< BrowserRouter >
2025-03-12 09:44:41 +01:00
< GroupCallErrorBoundary
onError = { onErrorMock }
recoveryActionHandler = { vi . fn ( ) }
2025-03-12 10:27:45 +01:00
widget = { null }
2025-03-12 09:44:41 +01:00
>
2025-03-05 20:50:19 +01:00
< TestComponent / >
< / GroupCallErrorBoundary >
< / BrowserRouter > ,
) ;
await screen . findByText ( expectedTitle ) ;
if ( expectedDescription ) {
expect ( screen . queryByText ( expectedDescription ) ) . toBeInTheDocument ( ) ;
}
expect ( onErrorMock ) . toHaveBeenCalledWith ( error ) ;
expect ( asFragment ( ) ) . toMatchSnapshot ( ) ;
} ,
) ;
test ( "should render the error page with link back to home" , async ( ) = > {
2025-10-03 14:43:22 -04:00
const error = new MatrixRTCTransportMissingError ( "example.com" ) ;
2025-03-05 20:50:19 +01:00
const TestComponent = ( ) : ReactNode = > {
throw error ;
} ;
const onErrorMock = vi . fn ( ) ;
const { asFragment } = render (
< BrowserRouter >
2025-03-12 09:44:41 +01:00
< GroupCallErrorBoundary
onError = { onErrorMock }
recoveryActionHandler = { vi . fn ( ) }
2025-03-12 10:27:45 +01:00
widget = { null }
2025-03-12 09:44:41 +01:00
>
2025-03-05 20:50:19 +01:00
< TestComponent / >
< / GroupCallErrorBoundary >
< / BrowserRouter > ,
) ;
await screen . findByText ( "Call is not supported" ) ;
2025-03-12 09:44:41 +01:00
expect ( screen . getByText ( /Domain: example\.com/i ) ) . toBeInTheDocument ( ) ;
2025-03-05 20:50:19 +01:00
expect (
2025-10-10 11:41:26 +02:00
screen . getByText ( /Error Code: MISSING_MATRIX_RTC_TRANSPORT/i ) ,
2025-03-05 20:50:19 +01:00
) . toBeInTheDocument ( ) ;
await screen . findByRole ( "button" , { name : "Return to home screen" } ) ;
expect ( onErrorMock ) . toHaveBeenCalledOnce ( ) ;
expect ( onErrorMock ) . toHaveBeenCalledWith ( error ) ;
expect ( asFragment ( ) ) . toMatchSnapshot ( ) ;
} ) ;
2025-03-12 09:44:41 +01:00
test ( "ConnectionLostError: Action handling should reset error state" , async ( ) = > {
2025-03-07 15:18:32 +01:00
const user = userEvent . setup ( ) ;
const TestComponent : FC < { fail : boolean } > = ( { fail } ) : ReactNode = > {
if ( fail ) {
throw new ConnectionLostError ( ) ;
}
return < div > HELLO < / div > ;
} ;
2025-03-12 09:44:41 +01:00
const reconnectCallbackSpy = vi . fn ( ) ;
2025-03-07 15:18:32 +01:00
const WrapComponent = ( ) : ReactNode = > {
const [ failState , setFailState ] = useState ( true ) ;
2025-03-12 09:44:41 +01:00
const reconnectCallback = useCallback (
2025-09-08 14:58:46 +02:00
async ( action : CallErrorRecoveryAction ) = > {
2025-03-12 09:44:41 +01:00
reconnectCallbackSpy ( action ) ;
setFailState ( false ) ;
2025-09-08 14:58:46 +02:00
return Promise . resolve ( ) ;
2025-03-12 09:44:41 +01:00
} ,
[ setFailState ] ,
) ;
2025-03-07 15:18:32 +01:00
return (
< BrowserRouter >
2025-03-12 10:27:45 +01:00
< GroupCallErrorBoundary
recoveryActionHandler = { reconnectCallback }
widget = { null }
>
2025-03-07 15:18:32 +01:00
< TestComponent fail = { failState } / >
< / GroupCallErrorBoundary >
< / BrowserRouter >
) ;
} ;
2025-03-12 09:44:41 +01:00
const { asFragment } = render ( < WrapComponent / > ) ;
2025-03-07 15:18:32 +01:00
// Should fail first
await screen . findByText ( "Connection lost" ) ;
2025-03-12 09:44:41 +01:00
await screen . findByRole ( "button" , { name : "Reconnect" } ) ;
await screen . findByRole ( "button" , { name : "Return to home screen" } ) ;
expect ( asFragment ( ) ) . toMatchSnapshot ( ) ;
2025-03-07 15:18:32 +01:00
await user . click ( screen . getByRole ( "button" , { name : "Reconnect" } ) ) ;
// reconnect should have reset the error, thus rendering should be ok
await screen . findByText ( "HELLO" ) ;
2025-03-12 09:44:41 +01:00
expect ( reconnectCallbackSpy ) . toHaveBeenCalledOnce ( ) ;
expect ( reconnectCallbackSpy ) . toHaveBeenCalledWith ( "reconnect" ) ;
2025-03-07 15:18:32 +01:00
} ) ;
2025-03-05 20:50:19 +01:00
describe ( "Rageshake button" , ( ) = > {
function setupTest ( testError : ElementCallError ) : void {
mockConfig ( {
rageshake : {
submit_url : "https://rageshake.example.com.localhost" ,
} ,
} ) ;
const TestComponent = ( ) : ReactElement = > {
throw testError ;
} ;
render (
< BrowserRouter >
2025-03-12 09:44:41 +01:00
< GroupCallErrorBoundary
onError = { vi . fn ( ) }
recoveryActionHandler = { vi . fn ( ) }
2025-03-12 10:27:45 +01:00
widget = { null }
2025-03-12 09:44:41 +01:00
>
2025-03-05 20:50:19 +01:00
< TestComponent / >
< / GroupCallErrorBoundary >
< / BrowserRouter > ,
) ;
}
test ( "should show send rageshake button for unknown errors" , ( ) = > {
setupTest ( new UnknownCallError ( new Error ( "FOO" ) ) ) ;
expect (
screen . queryByRole ( "button" , { name : "Send debug logs" } ) ,
) . toBeInTheDocument ( ) ;
} ) ;
test ( "should not show send rageshake button for call errors" , ( ) = > {
setupTest ( new E2EENotSupportedError ( ) ) ;
expect (
screen . queryByRole ( "button" , { name : "Send debug logs" } ) ,
) . not . toBeInTheDocument ( ) ;
} ) ;
} ) ;
2025-03-07 10:12:23 +01:00
test ( "should have a close button in widget mode" , async ( ) = > {
2025-10-03 14:43:22 -04:00
const error = new MatrixRTCTransportMissingError ( "example.com" ) ;
2025-03-07 10:12:23 +01:00
const TestComponent = ( ) : ReactNode = > {
throw error ;
} ;
const mockWidget = {
api : {
transport : { send : vi.fn ( ) . mockResolvedValue ( undefined ) , stop : vi.fn ( ) } ,
} ,
} as unknown as WidgetHelpers ;
const user = userEvent . setup ( ) ;
const onErrorMock = vi . fn ( ) ;
const { asFragment } = render (
< BrowserRouter >
2025-03-12 10:27:45 +01:00
< GroupCallErrorBoundary
widget = { mockWidget }
onError = { onErrorMock }
recoveryActionHandler = { vi . fn ( ) }
>
2025-03-07 10:12:23 +01:00
< TestComponent / >
< / GroupCallErrorBoundary >
< / BrowserRouter > ,
) ;
await screen . findByText ( "Call is not supported" ) ;
await screen . findByRole ( "button" , { name : "Close" } ) ;
expect ( asFragment ( ) ) . toMatchSnapshot ( ) ;
await user . click ( screen . getByRole ( "button" , { name : "Close" } ) ) ;
expect ( mockWidget . api . transport . send ) . toHaveBeenCalledWith (
ElementWidgetActions . Close ,
expect . anything ( ) ,
) ;
expect ( mockWidget . api . transport . stop ) . toHaveBeenCalled ( ) ;
} ) ;