electron-testable-ipc-proxy v0.1.4
electron-testable-ipc-proxy
provides a mechanism to call methods defined as interface T implemented in the main process from preload via IPC.
once defined descriptor D as IpcProxyDescriptor<T> and initialized with setupForMain<D>, setupForPreload<D> and setupForTest<D>, you can use the object implements T in the main process, preload in render process and unit tests respectively.
IpcProxyDescriptor
import { IpcProxyDescriptor } from 'electron-testable-ipc-proxy';
type IpcProxyDescriptor<T> = {
window: string;
IpcChannel: string;
template: T;
};describe common parameters for electron-testable-ipc-proxy.
T: interfaceTdescribed above.window: define the name to assign into global objectwindow.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.
setupForMain
import { setupForMain } from 'electron-testable-ipc-proxy';
function setupForMain<T>(Descriptor: IpcProxyDescriptor<T>, ipcMain, impl: T): void- should be called in main process of Electron before loading the page in BrowserWindow.
implpass an instance which implementedTto be called from renderer process throu IPC named by Descriptor.IpcChannel.ipcMain: passipcMainof Electron.
setupForPreload
import { setupForPreload } from 'electron-testable-ipc-proxy';
function setupForPreload<T>(Descriptor: IpcProxyDescriptor<T>, exposeInMainWorld, ipcRenderer): void- should be called in preload module in renderer process of Electron.
- setups proxy object into global
windowobject as named bydescriptor.window. exposeInMainWorld: passcontextBridge.exposeInMainWorldof Electron.ipcRenderer: passipcRendererof Electron.
setupForTest
import { setupForTest } from 'electron-testable-ipc-proxy';
function setupForTest<T, U>(Descriptor: IpcProxyDescriptor<T>, 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(passjest.fn()for jest) to be accessed from your tests, and injects to globalwindowobject to be called from test target.
Example code
full code are in here.
- electron/@types/MyAPI.d.ts
export interface MyAPI {
openDialog: () => Promise<void | string[]>;
}- electron/@types/global.d.ts
import { MyAPI } from "./MyAPI";
declare global {
interface Window {
myAPI: MyAPI;
}
}- src/MyAPIDescriptor.ts
class MyAPITemplate implements MyAPI {
private dontCallMe = new Error("don't call me");
openDialog(): Promise<never> { throw this.dontCallMe; }
}
export const MyAPIDescriptor: IpcProxyDescriptor<MyAPI> = {
window: 'myAPI',
IpcChannel: 'my-api',
template: new MyAPITemplate(),
}- electron/preload.ts
setupForPreload(MyAPIDescriptor, contextBridge.exposeInMainWorld, ipcRenderer);- electron/main.ts
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
const { myAPI } = window;
function App() {
const [files, setFiles] = useState<string[]>([]);
const [buttonBusy, setButtonBusy] = useState(false);
return (
<div className="App">
<header className="App-header">
...
<button disabled={buttonBusy} onClick={async () => {
setButtonBusy(true);
const files = await myAPI.openDialog();
if (Array.isArray(files)) {
setFiles(files);
} else {
setFiles([]);
}
setButtonBusy(false);
}} data-testid="open-dialog">open dialog</button>
<ul>
{files.map((file, index) => (
<li key={file} data-testid={`file${index}`}>{file}</li>
))}
</ul>
</header>
</div>
);
}- src/mock/myAPI.ts
export const myAPI = setupForTest(MyAPIDescriptor, () => jest.fn());- src/App.test.tsx
import { myAPI } from './mock/myAPI';
import App from './App';
test('open files when button clicked', async () => {
myAPI.openDialog.mockResolvedValue(['file1.txt', 'file2.txt']);
render(<App />);
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();
});memo
現在は MyAPITemplate で手で必要なメソッドの名前を持つダミーを並べないといけないが interface から自動生成したい。 しかし、create-react-app だと TypeScriptに transformer などを差し込む方法が見あたらない。