From 27fa6a1843ec0d68b4a67012a2090dc4554581ba Mon Sep 17 00:00:00 2001 From: mato533 <35281743+mato533@users.noreply.github.com> Date: Fri, 1 Nov 2024 17:42:09 +0900 Subject: [PATCH] test: add missing abnormal condition tests (#64) * test: add missing tests * ci: add configuration for code climate --- .github/workflows/test.yml | 19 ++++++++- src/__tests__/logger.spec.ts | 24 +++++++++++ src/__tests__/main.spec.ts | 11 +++++ src/__tests__/preload.spec.ts | 76 +++++++++++++++++++++++++++-------- src/main.ts | 15 ++++--- src/preload.ts | 38 +++++++++++++++--- 6 files changed, 156 insertions(+), 27 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41b99f4..1567a2b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - node-version: [18.x, 20.x] + node-version: [20.x] steps: - uses: mato533/cicd-actions/setup-pnpm@main with: @@ -37,3 +37,20 @@ jobs: - name: Build run: pnpm run build + + coverage: + name: Upload coverage to code climate + runs-on: ubuntu-latest + needs: + - test + if: github.event_name == 'push' + steps: + - uses: mato533/cicd-actions/setup-pnpm@main + with: + cache: 'pnpm' + + - uses: paambaati/codeclimate-action@v9 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CODE_CLIMATE }} + with: + coverageCommand: pnpm run coverage diff --git a/src/__tests__/logger.spec.ts b/src/__tests__/logger.spec.ts index 2f16b7f..eb0bc0c 100644 --- a/src/__tests__/logger.spec.ts +++ b/src/__tests__/logger.spec.ts @@ -6,6 +6,9 @@ import { preloadLogger, initializeMain, initializePreload, + // todo: remove this method on next major version + oldInitializeMain, + oldInitializePreload, } from '../utils/logger' import type { LogLevel } from '../utils/logger' @@ -73,3 +76,24 @@ describe.each([ }) }) }) + +// todo: remove this method on next major version +describe.each([ + ['main', oldInitializeMain], + ['preload', oldInitializePreload], +])('Logger (%s)', (procName: string, initialize) => { + const logSpy = vi.spyOn(console, 'log') + + beforeEach(() => { + logSpy.mockReset() + }) + + it('log test', () => { + initialize({ logger: { [procName]: new DefaultLogger() } }) + + expect(logSpy).toHaveBeenCalledOnce() + + const message = logSpy.mock.calls[0][0] + expect(message.includes('[DEPRECATION NOTICE]')).toBeTruthy() + }) +}) diff --git a/src/__tests__/main.spec.ts b/src/__tests__/main.spec.ts index 89daa22..e57a99f 100644 --- a/src/__tests__/main.spec.ts +++ b/src/__tests__/main.spec.ts @@ -73,6 +73,17 @@ describe('main', () => { mocks.ipcMain.handle.mockClear() }) + it('invalid api structure', () => { + const apiHandlers = { + dummy: { + fn1: (_e: IpcMainInvokeEvent, arg1: string) => arg1, + }, + } + + // @ts-expect-error + expect(() => registerIpcHandler(apiHandlers)).toThrowError('Implementation error') + }) + it('sender test', () => { const _apiHandlers = { invoke: { diff --git a/src/__tests__/preload.spec.ts b/src/__tests__/preload.spec.ts index 10178ff..c11de21 100644 --- a/src/__tests__/preload.spec.ts +++ b/src/__tests__/preload.spec.ts @@ -5,6 +5,20 @@ import { registerIpcHandler } from '../main' import type { IpcBridgeApiGenerator } from '../preload' import type { IpcMainInvokeEvent, IpcRendererEvent } from 'electron' +const _apiHandlers = { + invoke: { + fn1: (e: IpcMainInvokeEvent) => console.log(e), + fn2: (_e: IpcMainInvokeEvent, arg0: string) => `hello ${arg0}`, + }, + on: { + fn1: (arg1: string) => arg1, + fn2: (arg1: number, arg2: number) => arg1 + arg2, + name1: { + fn2: (arg1: string) => arg1, + fn1: (arg1: number, arg2: number) => arg1 + arg2, + }, + }, +} describe('preload', () => { const mocks = vi.hoisted(() => { return { @@ -28,23 +42,13 @@ describe('preload', () => { // disable console outputs vi.spyOn(console, 'debug').mockImplementation(() => {}) - + beforeEach(() => { + mocks.ipcMain.handle.mockReset() + mocks.ipcRenderer.invoke.mockReset() + mocks.ipcRenderer.on.mockReset() + mocks.randomUUID.mockReset() + }) it('sender test', async () => { - const _apiHandlers = { - invoke: { - fn1: (e: IpcMainInvokeEvent) => console.log(e), - fn2: (_e: IpcMainInvokeEvent, arg0: string) => `hello ${arg0}`, - }, - on: { - fn1: (arg1: string) => arg1, - fn2: (arg1: number, arg2: number) => arg1 + arg2, - name1: { - fn2: (arg1: string) => arg1, - fn1: (arg1: number, arg2: number) => arg1 + arg2, - }, - }, - } - registerIpcHandler(_apiHandlers) const lastArgs = mocks.ipcMain.handle.mock.calls[0] @@ -78,4 +82,44 @@ describe('preload', () => { expect(callback).toHaveBeenCalledOnce() expect(result).toBe(expectedValue) }) + + it('error test', async () => { + mocks.ipcRenderer.invoke.mockImplementation((key: string) => { + if (key === API_CHANNEL_MAP) { + return undefined + } + }) + + expect(() => + generateIpcBridgeApi>() + ).rejects.toThrowError('electron-typed-ipc-bridge') + }) + it('error test', async () => { + mocks.ipcRenderer.invoke.mockImplementation((key: string) => { + if (key === API_CHANNEL_MAP) { + return { + invalid: { api: 'uuid' }, + } + } + }) + + expect(() => + generateIpcBridgeApi>() + ).rejects.toThrowError('Implementation error') + }) + + it('error test', async () => { + mocks.ipcRenderer.invoke.mockImplementation((key: string) => { + if (key === API_CHANNEL_MAP) { + return { + on: { api: 'uuid1a' }, + invalid: { api: 'uuid2' }, + } + } + }) + + expect(() => + generateIpcBridgeApi>() + ).rejects.toThrowError('Implementation error') + }) }) diff --git a/src/main.ts b/src/main.ts index 97f6ab7..f7fa134 100644 --- a/src/main.ts +++ b/src/main.ts @@ -95,11 +95,14 @@ const getCallback = (mode: string) => { return createEmitter case MODE.invoke: return addHandler - default: { - const massage = `Implementation error: Top level is must be 'on' or 'invoke'. (${mode})` - log.error(massage) - throw new Error(massage) - } + } +} + +const checkTruthy = (ipcBridgeApi: IpcBridgeApiHandler, channelMap: ChannelMap) => { + if (!ipcBridgeApi || !channelMap) { + const massage = `Implementation error: Top level must be 'on' or 'invoke'. Both properties are not set.` + log.error(massage) + throw new Error(massage) } } @@ -108,6 +111,8 @@ const serializeApi = ( channelMap: ChannelMap, path: string[] ) => { + checkTruthy(ipcBridgeApi, channelMap) + const callback = getCallback(path[0]) return Object.keys(channelMap).reduce((api, key) => { diff --git a/src/preload.ts b/src/preload.ts index f258a03..18c9deb 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -59,12 +59,45 @@ type IpcBridgeApiTypeGenerator = type IpcBridgeApi = IpcBridgeApiTypeGenerator +const requiredKeys = ['on', 'invoke'] as const + +const hasRequiredKeys = (keys: string[]) => { + if (!requiredKeys.some((key) => keys.includes(key))) { + throw new Error() + } +} + +const checkInvalidKeys = (keys: string[]) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + if (!keys.every((key) => requiredKeys.includes(key))) { + throw new Error() + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const checkTopLevelKeys = (channelMap: any) => { + try { + const keys = Object.keys(channelMap) + + hasRequiredKeys(keys) + + checkInvalidKeys(keys) + } catch (_) { + const message = `Implementation error: Top level must be 'on' or 'invoke'. Both properties are not set.` + log.error(message) + throw new Error(message) + } +} + const generateIpcBridgeApi = async (): Promise => { const result = await ipcRenderer.invoke(API_CHANNEL_MAP) if (!result) { log.error(` --> Failed to get mapping for api and channel `) throw new Error(`'electron-typed-ipc-bridge' is not working correctly`) } + checkTopLevelKeys(result) + log.info('Generation IpcBridgeAPI is stated.') const endpoint = getEndpointApi(result) as T @@ -96,11 +129,6 @@ const createIpcBridgeApi = (channel: string, path: string[]) => { return createInvoker(channel, path) case MODE.on: return createReceiver(channel, path) - default: { - const message = `implementation error: ${path.join('.')} (channel: ${channel}` - log.error(message) - throw new Error(message) - } } }