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 ,
MatrixRTCFocusMissingError ,
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 ([
{
error : new MatrixRTCFocusMissingError ( "example.com" ),
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 () => {
const error = new MatrixRTCFocusMissingError ( "example.com" );
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 (
screen . getByText ( /Error Code: MISSING_MATRIX_RTC_FOCUS/i ),
). 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 () => {
const error = new MatrixRTCFocusMissingError ( "example.com" );
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 ();
});