import { SwitcherPlusSettings } from 'src/settings'; import { Mode, EditorSuggestion } from 'src/types'; import { InputInfo } from 'src/switcherPlus'; import { WorkspaceLeaf, PreparedQuery, prepareQuery, fuzzySearch, App, Workspace, renderResults, View, WorkspaceItem, WorkspaceSplit, TFile, Keymap, } from 'obsidian'; import { rootSplitEditorFixtures, leftSplitEditorFixtures, rightSplitEditorFixtures, editorTrigger, makeFuzzyMatch, makeLeaf, defaultOpenViewState, } from '@fixtures'; import { EditorHandler, Handler } from 'src/Handlers'; import { mock, MockProxy } from 'jest-mock-extended'; function makeLeafWithRoot(text: string, root: WorkspaceItem): MockProxy<WorkspaceLeaf> { const mockLeaf = makeLeaf(); mockLeaf.getDisplayText.mockImplementation(() => text); mockLeaf.getRoot.mockImplementation(() => root); return mockLeaf; } describe('editorHandler', () => { let settings: SwitcherPlusSettings; let mockApp: MockProxy<App>; let mockWorkspace: MockProxy<Workspace>; let sut: EditorHandler; beforeAll(() => { mockWorkspace = mock<Workspace>({ rootSplit: mock<WorkspaceSplit>(), leftSplit: mock<WorkspaceSplit>(), rightSplit: mock<WorkspaceSplit>(), }); mockApp = mock<App>({ workspace: mockWorkspace }); settings = new SwitcherPlusSettings(null); jest.spyOn(settings, 'editorListCommand', 'get').mockReturnValue(editorTrigger); sut = new EditorHandler(mockApp, settings); }); describe('commandString', () => { it('should return editorListCommand trigger', () => { expect(sut.commandString).toBe(editorTrigger); }); }); describe('validateCommand', () => { let inputText: string; let startIndex: number; const filterText = 'foo'; beforeAll(() => { inputText = `${editorTrigger}${filterText}`; startIndex = editorTrigger.length; }); it('should validate parsed input', () => { const inputInfo = new InputInfo(inputText); sut.validateCommand(inputInfo, startIndex, filterText, null, null); expect(inputInfo.mode).toBe(Mode.EditorList); const editorCmd = inputInfo.parsedCommand(); expect(editorCmd.parsedInput).toBe(filterText); expect(editorCmd.isValidated).toBe(true); }); }); describe('getSuggestions', () => { const mockPrepareQuery = jest.mocked(prepareQuery); const mockFuzzySearch = jest.mocked(fuzzySearch); const rootFixture = rootSplitEditorFixtures[0]; const leftFixture = leftSplitEditorFixtures[0]; const rightFixture = rightSplitEditorFixtures[0]; let mockRootSplitLeaf: MockProxy<WorkspaceLeaf>; let mockLeftSplitLeaf: MockProxy<WorkspaceLeaf>; let mockRightSplitLeaf: MockProxy<WorkspaceLeaf>; beforeAll(() => { mockWorkspace.iterateAllLeaves.mockImplementation( (callback: (leaf: WorkspaceLeaf) => void) => { const leaves = [mockRootSplitLeaf, mockLeftSplitLeaf, mockRightSplitLeaf]; leaves.forEach((leaf) => callback(leaf)); }, ); }); beforeEach(() => { mockRootSplitLeaf = makeLeafWithRoot( rootFixture.displayText, mockWorkspace.rootSplit, ); mockLeftSplitLeaf = makeLeafWithRoot( leftFixture.displayText, mockWorkspace.leftSplit, ); mockRightSplitLeaf = makeLeafWithRoot( rightFixture.displayText, mockWorkspace.rightSplit, ); }); test('with falsy input, it should return an empty array', () => { const results = sut.getSuggestions(null); expect(results).not.toBeNull(); expect(results).toBeInstanceOf(Array); expect(results).toHaveLength(0); }); test('that EditorSuggestion have a file property to enable interop with other plugins (like HoverEditor)', () => { const inputInfo = new InputInfo(editorTrigger); const results = sut.getSuggestions(inputInfo); expect(results.every((v) => v.file !== null)).toBe(true); }); test('with default settings, it should return suggestions for editor mode', () => { const inputInfo = new InputInfo(editorTrigger, Mode.EditorList); const results = sut.getSuggestions(inputInfo); expect(results).not.toBeNull(); expect(results).toBeInstanceOf(Array); expect(results).toHaveLength(3); const resultLeaves = new Set(results.map((sugg: EditorSuggestion) => sugg.item)); expect(resultLeaves.has(mockRootSplitLeaf)).toBe(true); expect(resultLeaves.has(mockLeftSplitLeaf)).toBe(true); expect(resultLeaves.has(mockRightSplitLeaf)).toBe(true); expect(results.every((sugg) => sugg.type === 'editor')).toBe(true); expect(mockPrepareQuery).toHaveBeenCalled(); expect(mockWorkspace.iterateAllLeaves).toHaveBeenCalled(); expect(mockRootSplitLeaf.getRoot).toHaveBeenCalled(); expect(mockLeftSplitLeaf.getRoot).toHaveBeenCalled(); expect(mockRightSplitLeaf.getRoot).toHaveBeenCalled(); }); test('with filter search term, it should return only matching suggestions for editor mode', () => { mockPrepareQuery.mockReturnValueOnce(rootFixture.prepQuery); mockFuzzySearch.mockImplementation((_q: PreparedQuery, text: string) => { return text === rootFixture.displayText ? rootFixture.fuzzyMatch : null; }); const inputInfo = new InputInfo(rootFixture.inputText, Mode.EditorList); const results = sut.getSuggestions(inputInfo); expect(results).not.toBeNull(); expect(results).toBeInstanceOf(Array); expect(results).toHaveLength(1); const resultLeaves = new Set(results.map((sugg: EditorSuggestion) => sugg.item)); expect(resultLeaves.has(mockRootSplitLeaf)).toBe(true); expect(resultLeaves.has(mockLeftSplitLeaf)).toBe(false); expect(resultLeaves.has(mockRightSplitLeaf)).toBe(false); expect(results[0]).toHaveProperty('type', 'editor'); expect(mockPrepareQuery).toHaveBeenCalled(); expect(mockFuzzySearch).toHaveBeenCalled(); expect(mockWorkspace.iterateAllLeaves).toHaveBeenCalled(); expect(mockRootSplitLeaf.getRoot).toHaveBeenCalled(); expect(mockLeftSplitLeaf.getRoot).toHaveBeenCalled(); expect(mockRightSplitLeaf.getRoot).toHaveBeenCalled(); expect(mockRootSplitLeaf.getDisplayText).toHaveBeenCalled(); expect(mockLeftSplitLeaf.getDisplayText).toHaveBeenCalled(); expect(mockRightSplitLeaf.getDisplayText).toHaveBeenCalled(); mockFuzzySearch.mockReset(); }); test('with INCLUDED side view type, it should return included side panel editor suggestions for editor mode', () => { const includeViewType = 'foo'; const includeViewTypesSpy = jest .spyOn(settings, 'includeSidePanelViewTypes', 'get') .mockReturnValue([includeViewType]); const mockView = mockLeftSplitLeaf.view as MockProxy<View>; mockView.getViewType.mockReturnValue(includeViewType); const inputInfo = new InputInfo(editorTrigger, Mode.EditorList); const results = sut.getSuggestions(inputInfo); expect(results).not.toBeNull(); expect(results).toBeInstanceOf(Array); expect(results).toHaveLength(2); const resultLeaves = new Set(results.map((sugg: EditorSuggestion) => sugg.item)); expect(resultLeaves.has(mockRootSplitLeaf)).toBe(true); expect(resultLeaves.has(mockLeftSplitLeaf)).toBe(true); expect(resultLeaves.has(mockRightSplitLeaf)).toBe(false); expect(results.every((sugg) => sugg.type === 'editor')).toBe(true); expect(includeViewTypesSpy).toHaveBeenCalled(); expect(mockView.getViewType).toHaveBeenCalled(); expect(mockPrepareQuery).toHaveBeenCalled(); expect(mockWorkspace.iterateAllLeaves).toHaveBeenCalled(); expect(mockRootSplitLeaf.getRoot).toHaveBeenCalled(); expect(mockLeftSplitLeaf.getRoot).toHaveBeenCalled(); expect(mockRightSplitLeaf.getRoot).toHaveBeenCalled(); includeViewTypesSpy.mockRestore(); }); test('with EXCLUDED main view type, it should not return excluded main panel editor suggestions for editor mode', () => { const excludeViewType = 'foo'; const excludeViewTypesSpy = jest .spyOn(settings, 'excludeViewTypes', 'get') .mockReturnValue([excludeViewType]); const mockView = mockRootSplitLeaf.view as MockProxy<View>; mockView.getViewType.mockReturnValue(excludeViewType); const inputInfo = new InputInfo(editorTrigger, Mode.EditorList); const results = sut.getSuggestions(inputInfo); expect(results).not.toBeNull(); expect(results).toBeInstanceOf(Array); expect(results).toHaveLength(2); const resultLeaves = new Set(results.map((sugg: EditorSuggestion) => sugg.item)); expect(resultLeaves.has(mockRootSplitLeaf)).toBe(false); expect(resultLeaves.has(mockLeftSplitLeaf)).toBe(true); expect(resultLeaves.has(mockRightSplitLeaf)).toBe(true); expect(results.every((sugg) => sugg.type === 'editor')).toBe(true); expect(excludeViewTypesSpy).toHaveBeenCalled(); expect(mockView.getViewType).toHaveBeenCalled(); expect(mockPrepareQuery).toHaveBeenCalled(); expect(mockWorkspace.iterateAllLeaves).toHaveBeenCalled(); expect(mockRootSplitLeaf.getRoot).toHaveBeenCalled(); expect(mockLeftSplitLeaf.getRoot).toHaveBeenCalled(); expect(mockRightSplitLeaf.getRoot).toHaveBeenCalled(); excludeViewTypesSpy.mockRestore(); }); }); describe('renderSuggestion', () => { it('should not throw an error with a null suggestion', () => { expect(() => sut.renderSuggestion(null, null)).not.toThrow(); }); it('should render a suggestion with match offsets', () => { const mockParentEl = mock<HTMLElement>(); const displayText = 'foo'; const mockLeaf = makeLeafWithRoot(displayText, null); const mockRenderResults = jest.mocked(renderResults); const sugg: EditorSuggestion = { type: 'editor', file: null, item: mockLeaf, match: makeFuzzyMatch(), }; sut.renderSuggestion(sugg, mockParentEl); expect(mockRenderResults).toHaveBeenCalledWith( mockParentEl, displayText, sugg.match, ); expect(mockLeaf.getDisplayText).toHaveBeenCalled(); }); }); describe('onChooseSuggestion', () => { beforeAll(() => { const fileContainerLeaf = makeLeaf(); fileContainerLeaf.openFile.mockResolvedValueOnce(); mockWorkspace.getLeaf.mockReturnValueOnce(fileContainerLeaf); }); it('should not throw an error with a null suggestion', () => { expect(() => sut.onChooseSuggestion(null, null)).not.toThrow(); }); it('should activate the selected leaf', () => { const activateLeafSpy = jest.spyOn(Handler.prototype, 'activateLeaf'); const mockLeaf = makeLeafWithRoot(null, null); const sugg: EditorSuggestion = { type: 'editor', file: new TFile(), item: mockLeaf, match: makeFuzzyMatch(), }; sut.onChooseSuggestion(sugg, null); expect(activateLeafSpy).toHaveBeenCalledWith( sugg.item, true, defaultOpenViewState.eState, ); activateLeafSpy.mockRestore(); }); it('should open file in new leaf when Mod is down', () => { const isModDown = true; const mockLeaf = makeLeafWithRoot(null, null); const mockKeymap = jest.mocked<typeof Keymap>(Keymap); const navigateToLeafOrOpenFileSpy = jest.spyOn( Handler.prototype, 'navigateToLeafOrOpenFile', ); mockKeymap.isModEvent.mockReturnValueOnce(isModDown); const sugg: EditorSuggestion = { type: 'editor', file: new TFile(), item: mockLeaf, match: null, }; sut.onChooseSuggestion(sugg, null); expect(mockKeymap.isModEvent).toHaveBeenCalled(); expect(navigateToLeafOrOpenFileSpy).toHaveBeenCalledWith( isModDown, sugg.file, expect.any(String), null, mockLeaf, ); navigateToLeafOrOpenFileSpy.mockRestore(); }); }); });