diff --git a/jest.config.js b/jest.config.js index 46a9c7e..d470c05 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,6 +4,9 @@ const { compilerOptions } = require('./tsconfig'); module.exports = { preset: 'ts-jest', testEnvironment: 'node', + globals: { + window: {}, + }, moduleFileExtensions: ['ts', 'js', 'jsx', 'tsx', 'json', 'node'], roots: ['/src/', 'node_modules'], modulePaths: ['', 'node_modules'], diff --git a/src/Handlers/__tests__/vaultHandler.test.ts b/src/Handlers/__tests__/vaultHandler.test.ts index 90320bc..1648935 100644 --- a/src/Handlers/__tests__/vaultHandler.test.ts +++ b/src/Handlers/__tests__/vaultHandler.test.ts @@ -1,12 +1,4 @@ -jest.mock('electron', () => { - return { - ipcRenderer: { - sendSync: jest.fn(), - }, - }; -}); - -import { ipcRenderer } from 'electron'; +import { IpcRenderer } from 'electron'; import { Mode, SuggestionType, MatchType, SearchQuery } from 'src/types'; import { InputInfo } from 'src/switcherPlus'; import { Handler, VaultHandler, VaultData } from 'src/Handlers'; @@ -18,14 +10,14 @@ import { vaultTrigger, makeVaultSuggestion, } from '@fixtures'; -import { mock, MockProxy } from 'jest-mock-extended'; +import { mock, mockFn, MockProxy } from 'jest-mock-extended'; import { Searcher } from 'src/search'; describe('vaultHandler', () => { let settings: SwitcherPlusSettings; let mockApp: MockProxy; let sut: VaultHandler; - const mockedIpcRenderer = jest.mocked(ipcRenderer); + const mockIpcRenderer = mock(); const mockPlatform = jest.mocked(Platform); const vaultData: VaultData = { @@ -37,11 +29,20 @@ describe('vaultHandler', () => { settings = new SwitcherPlusSettings(null); jest.spyOn(settings, 'vaultListCommand', 'get').mockReturnValue(vaultTrigger); + // Used when the electron module is dynamically loaded on desktop platforms + window.require = mockFn<(typeof window)['require']>().mockReturnValue({ + ipcRenderer: mockIpcRenderer, + }); + mockApp = mock(); sut = new VaultHandler(mockApp, settings); }); + afterAll(() => { + delete window['require']; + }); + describe('getCommandString', () => { it('should return vaultListCommand trigger', () => { expect(sut.getCommandString()).toBe(vaultTrigger); @@ -71,8 +72,8 @@ describe('vaultHandler', () => { }); describe('getSuggestions', () => { - afterEach(() => { - mockedIpcRenderer.sendSync.mockClear(); + beforeEach(() => { + mockIpcRenderer.sendSync.mockClear(); }); test('with falsy input, it should return an empty array', () => { @@ -83,22 +84,39 @@ describe('vaultHandler', () => { expect(results).toHaveLength(0); }); - test('.getItems() should log errors to the console', () => { + test('.getVaultListDataOnDesktop() should log errors to the console', () => { + mockPlatform.isDesktop = true; const consoleLogSpy = jest.spyOn(console, 'log').mockReturnValueOnce(); - const error = new Error('getItems unit test error'); - mockedIpcRenderer.sendSync.mockImplementationOnce(() => { + const error = new Error('vaultHandler.getVaultListDataOnDesktop unit test error'); + mockIpcRenderer.sendSync.mockImplementationOnce(() => { throw error; }); + sut.getVaultListDataOnDesktop(); + + expect(consoleLogSpy).toHaveBeenCalledWith(expect.any(String), error); + + consoleLogSpy.mockRestore(); + }); + + test('.getItems() should log errors to the console', () => { + const consoleLogSpy = jest.spyOn(console, 'log').mockReturnValueOnce(); + + const error = new Error('vaultHandler.getItems unit test error'); + const getVaultListDataSpy = jest + .spyOn(sut, 'getVaultListDataOnDesktop') + .mockImplementationOnce(() => { + throw error; + }); + sut.getItems(); - expect(consoleLogSpy).toHaveBeenCalledWith( - 'Switcher++: error retrieving list of available vaults. ', - error, - ); + expect(getVaultListDataSpy).toHaveBeenCalled(); + expect(consoleLogSpy).toHaveBeenCalledWith(expect.any(String), error); consoleLogSpy.mockRestore(); + getVaultListDataSpy.mockRestore(); }); test('on mobile platforms, it should return the open vault chooser marker', () => { @@ -113,8 +131,9 @@ describe('vaultHandler', () => { }); test('with default settings, it should return suggestions for vault list mode', () => { + mockPlatform.isDesktop = true; const inputInfo = new InputInfo(vaultTrigger); - mockedIpcRenderer.sendSync.mockReturnValueOnce(vaultData); + mockIpcRenderer.sendSync.mockReturnValueOnce(vaultData); const results = sut.getSuggestions(inputInfo); @@ -127,10 +146,11 @@ describe('vaultHandler', () => { expect(results).toHaveLength(2); expect(areAllFound).toBe(true); expect(results.every((sugg) => sugg.type === SuggestionType.VaultList)).toBe(true); - expect(mockedIpcRenderer.sendSync).toHaveBeenCalledWith('vault-list'); + expect(mockIpcRenderer.sendSync).toHaveBeenCalledWith('vault-list'); }); test('with filter search term, it should return only matching suggestions for vault list mode', () => { + mockPlatform.isDesktop = true; const inputInfo = new InputInfo(null, Mode.VaultList); const parsedInputQuerySpy = jest .spyOn(inputInfo, 'parsedInputQuery', 'get') @@ -147,12 +167,12 @@ describe('vaultHandler', () => { return text.endsWith(filterText) ? makeFuzzyMatch() : null; }); - mockedIpcRenderer.sendSync.mockReturnValueOnce(vaultData); + mockIpcRenderer.sendSync.mockReturnValueOnce(vaultData); const results = sut.getSuggestions(inputInfo); expect(results).toHaveLength(1); expect(results[0].pathSegments.path).toBe(expectedItem.path); - expect(mockedIpcRenderer.sendSync).toHaveBeenCalledWith('vault-list'); + expect(mockIpcRenderer.sendSync).toHaveBeenCalledWith('vault-list'); searchSpy.mockRestore(); parsedInputQuerySpy.mockRestore(); @@ -227,20 +247,47 @@ describe('vaultHandler', () => { expect(() => sut.onChooseSuggestion(null, null)).not.toThrow(); }); + test('.openVaultOnDesktop() should do nothing on non-desktop platforms', () => { + mockPlatform.isDesktop = false; + mockIpcRenderer.sendSync.mockClear(); + + sut.openVaultOnDesktop(null); + + expect(mockIpcRenderer.sendSync).not.toHaveBeenCalled(); + + mockPlatform.isDesktop = true; + }); + + test('.openVaultOnDesktop() should log errors to the console', () => { + mockPlatform.isDesktop = true; + const consoleLogSpy = jest.spyOn(console, 'log').mockReturnValueOnce(); + + const error = new Error('vaultHandler.openVaultOnDesktop unit test error'); + mockIpcRenderer.sendSync.mockImplementationOnce(() => { + throw error; + }); + + sut.openVaultOnDesktop(null); + + expect(consoleLogSpy).toHaveBeenCalledWith(expect.any(String), error); + + consoleLogSpy.mockRestore(); + }); + it('should open the vault on desktop platforms', () => { mockPlatform.isDesktop = true; const sugg = makeVaultSuggestion(); sut.onChooseSuggestion(sugg, null); - expect(mockedIpcRenderer.sendSync).toHaveBeenCalledWith( + expect(mockIpcRenderer.sendSync).toHaveBeenCalledWith( 'vault-open', sugg.pathSegments.path, false, ); mockPlatform.isDesktop = false; - mockedIpcRenderer.sendSync.mockClear(); + mockIpcRenderer.sendSync.mockClear(); }); it('should launch the vault chooser on mobile platforms', () => { diff --git a/src/Handlers/vaultHandler.ts b/src/Handlers/vaultHandler.ts index 0922f54..8832bea 100644 --- a/src/Handlers/vaultHandler.ts +++ b/src/Handlers/vaultHandler.ts @@ -1,4 +1,3 @@ -import { ipcRenderer } from 'electron'; import { filenameFromPath } from 'src/utils'; import { AnySuggestion, @@ -57,9 +56,7 @@ export class VaultHandler extends Handler { if (inputInfo) { const { query, hasSearchTerm } = inputInfo.parsedInputQuery; const searcher = Searcher.create(query); - const items = Platform.isDesktop - ? this.getItems() - : [this.mobileVaultChooserMarker]; + const items = this.getItems(); items.forEach((item) => { let shouldPush = true; @@ -126,12 +123,8 @@ export class VaultHandler extends Handler { let handled = false; if (sugg) { if (Platform.isDesktop) { - // 12/8/23: "vault-open" is the Obsidian defined channel for open a vault - handled = ipcRenderer.sendSync( - 'vault-open', - sugg.pathSegments?.path, - false, // true to create if it doesn't exist - ) as boolean; + this.openVaultOnDesktop(sugg.pathSegments?.path); + handled = true; } else if (sugg === this.mobileVaultChooserMarker) { // It's the mobile app context, show the vault chooser this.app.openVaultChooser(); @@ -145,12 +138,10 @@ export class VaultHandler extends Handler { getItems(): VaultSuggestion[] { const items: VaultSuggestion[] = []; - try { - // 12/8/23: "vault-list" is the Obsidian defined channel for retrieving - // the vault list - const vaultData = ipcRenderer.sendSync('vault-list') as VaultData; + if (Platform.isDesktop) { + try { + const vaultData = this.getVaultListDataOnDesktop(); - if (vaultData) { for (const [id, { path, open }] of Object.entries(vaultData)) { const basename = filenameFromPath(path); const sugg: VaultSuggestion = { @@ -163,13 +154,61 @@ export class VaultHandler extends Handler { items.push(sugg); } + } catch (err) { + console.log('Switcher++: error parsing vault data. ', err); } - } catch (err) { - console.log('Switcher++: error retrieving list of available vaults. ', err); + } else { + items.push(this.mobileVaultChooserMarker); } return items.sort((a, b) => a.pathSegments.basename.localeCompare(b.pathSegments.basename), ); } + + /** + * Instructs Obsidian to open the vault at vaultPath. This should only be called + * Desktop Platforms. + * + * @param {string} vaultPath + */ + openVaultOnDesktop(vaultPath: string): void { + if (!Platform.isDesktop) { + return; + } + + try { + const ipcRenderer = window.require('electron').ipcRenderer; + + // 12/8/23: "vault-open" is the Obsidian defined channel for opening a vault + ipcRenderer.sendSync( + 'vault-open', + vaultPath, + false, // true to create if it doesn't exist + ); + } catch (error) { + console.log(`Switcher++: error opening vault with path: ${vaultPath} `, error); + } + } + + /** + * Retrieves the list of available vaults that can be opened. This should only be + * called on Desktop Platforms. + * + * @returns {VaultData} + */ + getVaultListDataOnDesktop(): VaultData { + let data: VaultData = null; + + if (Platform.isDesktop) { + try { + const ipcRenderer = window.require('electron').ipcRenderer; + data = ipcRenderer.sendSync('vault-list') as VaultData; + } catch (error) { + console.log('Switcher++: error retrieving list of available vaults. ', error); + } + } + + return data; + } }