Skip to content
9 changes: 9 additions & 0 deletions web/packages/build/jest/jest-environment-patched-jsdom.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,17 @@ export default class PatchedJSDOMEnvironment extends JSDOMEnvironment {
this.unobserve = () => {};
this.disconnect = () => {};
}

global.ResizeObserver = NullResizeObserver;
}

if (!global.navigator.permissions) {
global.navigator.permissions = {
query: async () => ({
onchange: () => {},
}),
};
}
}
}
export const TestEnvironment = PatchedJSDOMEnvironment;
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
TdpClient,
TdpClientEvent,
} from 'shared/libs/tdp';
import { TdpError as RemoteTdpError } from 'shared/libs/tdp/client';

import { DesktopSession, DesktopSessionProps } from './DesktopSession';

Expand Down Expand Up @@ -86,7 +87,15 @@ export const FetchError = () => (
export const TdpError = () => {
const client = fakeClient();
client.connect = async () => {
client.emit(TdpClientEvent.ERROR, new Error('some tdp error'));
client.emit(
TdpClientEvent.ERROR,
new RemoteTdpError(
'RDP client exited with an error: Connection Timed Out.\n\n' +
'Teleport could not connect to the host within the timeout period. This may be due to a firewall blocking the connection, an overloaded system, or network congestion.\n\n' +
'To resolve this issue, ensure that the Teleport agent can reach the Windows host.\n\n' +
'You can use the command "nc -vz HOST 3389" to help diagnose connectivity problems.'
)
);
};

return <DesktopSession {...props} client={client} />;
Expand All @@ -96,10 +105,10 @@ export const Connected = () => {
return <DesktopSession {...props} />;
};

export const Disconnected = () => {
export const DisconnectedWithNoMessage = () => {
const client = fakeClient();
client.connect = async () => {
client.emit(TdpClientEvent.TRANSPORT_CLOSE, 'session disconnected');
client.emit(TdpClientEvent.TRANSPORT_CLOSE);
};

return <DesktopSession {...props} client={client} />;
Expand Down
113 changes: 113 additions & 0 deletions web/packages/shared/components/DesktopSession/DesktopSession.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { EventEmitter } from 'events';

import { screen } from '@testing-library/react';

import { render } from 'design/utils/testing';
import { makeSuccessAttempt } from 'shared/hooks/useAsync';
import { TdpClient } from 'shared/libs/tdp';
import { wait } from 'shared/utils/wait';

import { DesktopSession } from './DesktopSession';

import 'jest-canvas-mock';

import userEvent from '@testing-library/user-event';

import { TdpTransport } from 'shared/libs/tdp/client';

// Disable WASM in tests.
jest.mock('shared/libs/ironrdp/pkg/ironrdp');

const hasNoOtherSession = jest.fn().mockResolvedValue(false);
const aclAttempt = makeSuccessAttempt({
clipboardSharingEnabled: true,
directorySharingEnabled: true,
});
const getMockTransport = () => {
const emitter = new EventEmitter();
return {
emitTransportError: () =>
emitter.emit('error', new Error('Could not send bytes')),
getTransport: async (abortSignal: AbortSignal): Promise<TdpTransport> => {
abortSignal.onabort = async () => {
await wait(50);
emitter.emit('complete');
};
return {
send: () => {},
onMessage: callback => {
emitter.on('message', callback);
return () => emitter.off('message', callback);
},
onComplete: callback => {
emitter.on('complete', callback);
return () => emitter.off('complete', callback);
},
onError: callback => {
emitter.on('error', callback);
return () => emitter.off('error', callback);
},
};
},
};
};

test('reconnect button reinitializes the connection', async () => {
const transport = getMockTransport();
const tpdClient = new TdpClient(transport.getTransport);
jest.spyOn(tpdClient, 'connect');
jest.spyOn(tpdClient, 'shutdown');
const { unmount } = render(
<DesktopSession
client={tpdClient}
username="admin"
desktop="win-lab"
aclAttempt={aclAttempt}
hasAnotherSession={hasNoOtherSession}
/>
);

// The session is initializing.
expect(await screen.findByTestId('indicator')).toBeInTheDocument();

// An error occurred, the connection has been closed.
transport.emitTransportError();

expect(
await screen.findByText('The desktop session is offline.')
).toBeInTheDocument();
expect(await screen.findByText('Could not send bytes')).toBeInTheDocument();
const reconnect = await screen.findByRole('button', { name: 'Reconnect' });

await userEvent.click(reconnect);

// The session is initializing again.
expect(
screen.queryByText('The desktop session is offline.')
).not.toBeInTheDocument();
expect(await screen.findByTestId('indicator')).toBeInTheDocument();

expect(hasNoOtherSession).toHaveBeenCalledTimes(2);
expect(tpdClient.connect).toHaveBeenCalledTimes(2);
unmount();
// Called 2 times: the first one during reconnecting, the second one after unmounting.
expect(tpdClient.shutdown).toHaveBeenCalledTimes(2);
});
Loading
Loading