diff --git a/packages/backend/src/studio.spec.ts b/packages/backend/src/studio.spec.ts index 28cba3267..237d4bf10 100644 --- a/packages/backend/src/studio.spec.ts +++ b/packages/backend/src/studio.spec.ts @@ -26,6 +26,10 @@ import * as fs from 'node:fs'; vi.mock('./managers/modelsManager'); +vi.mock('@shared/src/messages/NoTimeoutChannels', () => ({ + noTimeoutChannels: [], +})); + const mockedExtensionContext = { subscriptions: [], storagePath: 'dummy-storage-path', diff --git a/packages/frontend/src/stores/rpcReadable.spec.ts b/packages/frontend/src/stores/rpcReadable.spec.ts index edccf88ed..54408c4eb 100644 --- a/packages/frontend/src/stores/rpcReadable.spec.ts +++ b/packages/frontend/src/stores/rpcReadable.spec.ts @@ -52,6 +52,10 @@ vi.mock('../utils/client', async () => { }; }); +vi.mock('@shared/src/messages/NoTimeoutChannels', () => ({ + noTimeoutChannels: [], +})); + beforeEach(() => { vi.clearAllMocks(); }); diff --git a/packages/shared/src/messages/MessageProxy.spec.ts b/packages/shared/src/messages/MessageProxy.spec.ts index edd0b78e5..68c404d4c 100644 --- a/packages/shared/src/messages/MessageProxy.spec.ts +++ b/packages/shared/src/messages/MessageProxy.spec.ts @@ -16,15 +16,20 @@ * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ -import { test, expect, beforeAll, vi } from 'vitest'; -import { RpcBrowser, RpcExtension } from './MessageProxy'; +import { test, expect, vi, describe, beforeEach, afterEach } from 'vitest'; +import { getChannel, RpcBrowser, RpcExtension } from './MessageProxy'; import type { Webview } from '@podman-desktop/api'; +import * as defaultNoTimeoutChannels from './NoTimeoutChannels'; let webview: Webview; let window: Window; let api: PodmanDesktopApi; -beforeAll(() => { +vi.mock('./NoTimeoutChannels', async () => ({ + noTimeoutChannels: [], +})); + +beforeEach(() => { let windowListener: (message: unknown) => void; let webviewListener: (message: unknown) => void; @@ -42,7 +47,6 @@ beforeAll(() => { expect(channel).toBe('message'); windowListener = listener; }, - setTimeout: vi.fn(), } as unknown as Window; api = { @@ -170,16 +174,91 @@ test('Test raising exception', async () => { await expect(rpcBrowser.invoke('raiseError')).rejects.toThrow('big error'); }); -test('A noTimeoutChannel should not call the setTimeout', async () => { - const rpcExtension = new RpcExtension(webview); - rpcExtension.init(); - const rpcBrowser = new RpcBrowser(window, api); +test('getChannel should use CHANNEL property of classType provided', () => { + class Dummy { + static readonly CHANNEL: string = 'dummy'; + async ping(): Promise<'pong'> { + return new Promise(vi.fn()); + } + } + + const channel = getChannel(Dummy, 'ping'); + expect(channel).toBe('dummy-ping'); +}); + +describe('no timeout channel', () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.useFakeTimers(); - rpcExtension.register('openDialog', () => { - return Promise.resolve(); + (defaultNoTimeoutChannels.noTimeoutChannels as string[]) = []; }); - const setTimeoutMock = vi.spyOn(window, 'setTimeout'); - await rpcBrowser.invoke('openDialog'); - expect(setTimeoutMock).not.toHaveBeenCalled(); + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('default function should have a timeout', async () => { + class Dummy { + static readonly CHANNEL: string = 'dummy'; + async ping(): Promise<'pong'> { + return new Promise(vi.fn()); + } + } + + const rpcExtension = new RpcExtension(webview); + rpcExtension.init(); + const rpcBrowser = new RpcBrowser(window, api); + + rpcExtension.registerInstance(Dummy, new Dummy()); + + const proxy = rpcBrowser.getProxy(Dummy); + + let error: Error | undefined; + proxy.ping().catch((err: unknown) => { + error = err as Error; + }); + + await vi.advanceTimersByTimeAsync(5_000); + expect(error?.message).toBe('Timeout'); + }); + + test('noTimeoutChannels should not have a timeout', async () => { + class Dummy { + static readonly CHANNEL: string = 'dummy'; + async ping(): Promise<'pong'> { + return new Promise(resolve => { + setTimeout(resolve.bind(undefined, 'pong'), 8_000); + }); + } + } + + // fake the noTimeoutChannels + (defaultNoTimeoutChannels.noTimeoutChannels as string[]) = [`${Dummy.CHANNEL}-ping`]; + + const rpcExtension = new RpcExtension(webview); + rpcExtension.init(); + const rpcBrowser = new RpcBrowser(window, api); + + rpcExtension.registerInstance(Dummy, new Dummy()); + + const proxy = rpcBrowser.getProxy(Dummy); + + let error: Error | undefined; + let result: 'pong' | undefined; + proxy + .ping() + .then(mResult => { + result = mResult; + }) + .catch((err: unknown) => { + error = err as Error; + }); + + await vi.advanceTimersByTimeAsync(5_000); + expect(error).toBeUndefined(); + await vi.advanceTimersByTimeAsync(5_000); + expect(error).toBeUndefined(); + expect(result).toBe('pong'); + }); }); diff --git a/packages/shared/src/messages/MessageProxy.ts b/packages/shared/src/messages/MessageProxy.ts index 6cc247614..26d850ce0 100644 --- a/packages/shared/src/messages/MessageProxy.ts +++ b/packages/shared/src/messages/MessageProxy.ts @@ -40,6 +40,10 @@ export interface ISubscribedMessage { body: any; } +export function getChannel(classType: { CHANNEL: string; prototype: T }, method: keyof T): string { + return `${classType.CHANNEL}-${String(method)}`; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any type UnaryRPC = (...args: any[]) => Promise; @@ -115,7 +119,7 @@ export class RpcExtension implements Disposable { methodNames.forEach(name => { const method = (instance[name as keyof T] as any).bind(instance); - this.register(`${classType.CHANNEL}-${name}`, method); + this.register(getChannel(classType, name as keyof T), method); }); } @@ -180,7 +184,7 @@ export class RpcBrowser { if (typeof prop === 'string') { return (...args: unknown[]) => { const channel = prop.toString(); - return thisRef.invoke(`${classType.CHANNEL}-${channel}`, ...args); + return thisRef.invoke(getChannel(classType, channel as keyof T), ...args); }; } return Reflect.get(target, prop, receiver); diff --git a/packages/shared/src/messages/NoTimeoutChannels.ts b/packages/shared/src/messages/NoTimeoutChannels.ts index 1e9d8bdee..28f29c0da 100644 --- a/packages/shared/src/messages/NoTimeoutChannels.ts +++ b/packages/shared/src/messages/NoTimeoutChannels.ts @@ -15,5 +15,7 @@ * * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ +import { StudioAPI } from '../StudioAPI'; +import { getChannel } from './MessageProxy'; -export const noTimeoutChannels: string[] = ['openDialog']; +export const noTimeoutChannels: string[] = [getChannel(StudioAPI, 'openDialog')];