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
: interfaceT
described 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.
impl
pass an instance which implementedT
to be called from renderer process throu IPC named by Descriptor.IpcChannel.ipcMain
: passipcMain
of 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
window
object as named bydescriptor.window
. exposeInMainWorld
: passcontextBridge.exposeInMainWorld
of Electron.ipcRenderer
: passipcRenderer
of 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 globalwindow
object 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 などを差し込む方法が見あたらない。