testable IPC proxy library for Electron and TypeScript
npm install electron-testable-ipc-proxyprovides a mechanism to call methods defined as interface T implemented in the main process from preload via IPC.
once defined descriptor D as IpcProxyDescriptor and initialized with setupForMain, setupForPreload and setupForTest, you can use the object implements T in the main process, preload in render process and unit tests respectively.
typescript
import { IpcProxyDescriptor } from 'electron-testable-ipc-proxy';type IpcProxyDescriptor = {
window: string;
IpcChannel: string;
template: T;
};
`
describe common parameters for electron-testable-ipc-proxy.
* T: interface T described above.
* window: define the name to assign into global object window.
* IpcChannel: IPC channel name to communicate between main process and renderer process.
* template: class instance object with dummy methods declared in interface T. used only names of methods.
$3
`typescript
import { setupForMain } from 'electron-testable-ipc-proxy';function setupForMain(Descriptor: IpcProxyDescriptor, ipcMain, impl: T): void
`
* should be called in main process of Electron before loading the page in BrowserWindow.
* impl pass an instance which implemented T to be called from renderer process throu IPC named by Descriptor.IpcChannel.
* ipcMain: pass ipcMain of Electron.$3
`typescript
import { setupForPreload } from 'electron-testable-ipc-proxy';function setupForPreload(Descriptor: IpcProxyDescriptor, exposeInMainWorld, ipcRenderer): void
`
* should be called in preload module in renderer process of Electron.
* setups proxy object into global window object as named by descriptor.window.
* exposeInMainWorld: pass contextBridge.exposeInMainWorld of Electron.
* ipcRenderer: pass ipcRenderer of Electron.
$3
`typescript
import { setupForTest } from 'electron-testable-ipc-proxy';function setupForTest(Descriptor: IpcProxyDescriptor, fn: (key: keyof T, fn: (...args: unknown[]) => unknown) => U): {
[k in keyof T]: U;
}
`
* to use with jest, should be called this in a module which imported before the test target module.
* this function creates an object implements each methods of T by given fn (pass jest.fn() for jest) to be accessed from your tests, and injects to global window object to be called from test target.
Example code
full code are in here.* electron/@types/MyAPI.d.ts
`typescript
export interface MyAPI {
openDialog: () => Promise;
}
`* electron/@types/global.d.ts
`typescript
import { MyAPI } from "./MyAPI";declare global {
interface Window {
myAPI: MyAPI;
}
}
`* src/MyAPIDescriptor.ts
`typescript
class MyAPITemplate implements MyAPI {
private dontCallMe = new Error("don't call me"); openDialog(): Promise { throw this.dontCallMe; }
}
export const MyAPIDescriptor: IpcProxyDescriptor = {
window: 'myAPI',
IpcChannel: 'my-api',
template: new MyAPITemplate(),
}
`* electron/preload.ts
`typescript
setupForPreload(MyAPIDescriptor, contextBridge.exposeInMainWorld, ipcRenderer);
`* electron/main.ts
`typescript
class MyApiServer implements MyAPI {
constructor(readonly mainWindow: BrowserWindow) {
} async openDialog() {
const dirPath = await dialog
.showOpenDialog(this.mainWindow, {
properties: ['openDirectory'],
})
.then((result) => {
if (result.canceled) return;
return result.filePaths[0];
})
.catch((err) => console.log(err));
if (!dirPath) return;
return fs.promises
.readdir(dirPath, { withFileTypes: true })
.then((dirents) =>
dirents
.filter((dirent) => dirent.isFile())
.map(({ name }) => path.join(dirPath, name)),
);
}
};
...
const myApi = new MyApiServer(win);
setupForMain(MyAPIDescriptor, ipcMain, myApi);
`* src/App.tsx
`tsx
const { myAPI } = window;function App() {
const [files, setFiles] = useState([]);
const [buttonBusy, setButtonBusy] = useState(false);
return (
...
{files.map((file, index) => (
- file${index}
}>{file}
))}
);
}
`* src/mock/myAPI.ts
`typescript
export const myAPI = setupForTest(MyAPIDescriptor, () => jest.fn());
`* src/App.test.tsx
`typescript
import { myAPI } from './mock/myAPI';
import App from './App';test('open files when button clicked', async () => {
myAPI.openDialog.mockResolvedValue(['file1.txt', 'file2.txt']);
render( );
const button = screen.getByTestId('open-dialog');
expect(button).toBeInTheDocument();
expect(button.innerHTML).toBe('open dialog');
expect(button).toBeEnabled();
fireEvent.click(button);
expect(button).toBeDisabled();
await waitFor(() => screen.getByTestId('file0'));
expect(myAPI.openDialog).toHaveBeenCalled();
expect(screen.getByTestId('file0')).toHaveTextContent('file1.txt');
expect(screen.getByTestId('file1')).toHaveTextContent('file2.txt');
expect(screen.queryByTestId('file2')).toBeNull();
});
``