From 57905418b4162497f27c839684347dcf918834aa Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Tue, 16 Jun 2026 10:19:09 +0200 Subject: [PATCH 1/2] fix(view): reliably reveal PodNotes view via command + ribbon (#55) The Show PodNotes command was gated to only appear when no view leaf existed, and it never revealed the leaf it set. Once the view was open but hidden (collapsed sidebar, sidebar-icon overflow, or dragged out of sight) there was no way to bring it back: the command disappeared from the palette and did nothing when bound to a hotkey. Add an activateView() helper that reuses the existing leaf when present and always calls revealLeaf, wire the Show PodNotes command to it as a plain always-available callback, and add a left-ribbon podcast icon as a persistent, overflow-proof entry point. Update the command docs. Closes #55 --- docs/docs/commands.md | 8 ++-- src/main.activateView.test.ts | 73 +++++++++++++++++++++++++++++++++++ src/main.ts | 43 +++++++++++++++++---- 3 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 src/main.activateView.test.ts diff --git a/docs/docs/commands.md b/docs/docs/commands.md index 251b58ab..dc2b871b 100644 --- a/docs/docs/commands.md +++ b/docs/docs/commands.md @@ -1,9 +1,11 @@ ## Show PodNotes -This command is only accessible if there doesn't exist a pane with PodNotes already. +Opens the PodNotes pane and brings it into focus. -Activating it will spawn a new pane with PodNotes in the right sidebar. +If the pane already exists but is hidden — for example in a collapsed sidebar or out of view because the right sidebar has too many icons — this command reveals it. If it does not exist yet, the command creates it in the right sidebar. You can run it from the command palette or bind it to a hotkey. -If you are having issues with PodNotes not being shown, feel free to create an [issue](https://github.com/chhoumann/PodNotes/issues/new). However, do make sure to check that the icon isn't just out of view by scrolling on the right sidebar. +You can also open PodNotes from the **podcast icon in the left ribbon**, which is always visible and is the most reliable way to reopen the pane. + +If you are having issues with PodNotes not being shown, feel free to create an [issue](https://github.com/chhoumann/PodNotes/issues/new). ## Play Podcast This will start playback if the current episode is paused. diff --git a/src/main.activateView.test.ts b/src/main.activateView.test.ts new file mode 100644 index 00000000..4152efb5 --- /dev/null +++ b/src/main.activateView.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import PodNotes from "./main"; +import { VIEW_TYPE } from "./constants"; + +// Regression coverage for #55: "Show PodNotes" / the ribbon icon must reliably +// surface the view. The bug was that the command was gated on the leaf NOT +// existing and never revealed it, so an already-open-but-hidden view (collapsed +// or overflowing sidebar) could not be brought back. activateView reuses the +// existing leaf when present and always reveals it. + +function makeLeaf() { + return { + setViewState: vi.fn().mockResolvedValue(undefined), + }; +} + +function setupPlugin({ + existingLeaves = [] as ReturnType[], + rightLeaf = makeLeaf() as ReturnType | null, +} = {}) { + const workspace = { + getLeavesOfType: vi.fn().mockReturnValue(existingLeaves), + getRightLeaf: vi.fn().mockReturnValue(rightLeaf), + revealLeaf: vi.fn().mockResolvedValue(undefined), + }; + + // Build a bare instance so we exercise activateView without running the full + // onload() side effects (store wiring, command registration, etc.). + const plugin = Object.create(PodNotes.prototype) as PodNotes; + (plugin as unknown as { app: { workspace: typeof workspace } }).app = { + workspace, + }; + + return { plugin, workspace, rightLeaf }; +} + +describe("PodNotes.activateView", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("reuses an existing leaf and reveals it without creating a new one", async () => { + const existing = makeLeaf(); + const { plugin, workspace } = setupPlugin({ existingLeaves: [existing] }); + + await plugin.activateView(); + + expect(workspace.getRightLeaf).not.toHaveBeenCalled(); + expect(existing.setViewState).not.toHaveBeenCalled(); + expect(workspace.revealLeaf).toHaveBeenCalledTimes(1); + expect(workspace.revealLeaf).toHaveBeenCalledWith(existing); + }); + + it("creates a right-sidebar leaf when none exists, then reveals it", async () => { + const { plugin, workspace, rightLeaf } = setupPlugin(); + + await plugin.activateView(); + + expect(workspace.getRightLeaf).toHaveBeenCalledWith(false); + expect(rightLeaf?.setViewState).toHaveBeenCalledWith({ + type: VIEW_TYPE, + active: true, + }); + expect(workspace.revealLeaf).toHaveBeenCalledWith(rightLeaf); + }); + + it("does not throw or reveal when no right leaf is available", async () => { + const { plugin, workspace } = setupPlugin({ rightLeaf: null }); + + await expect(plugin.activateView()).resolves.toBeUndefined(); + expect(workspace.revealLeaf).not.toHaveBeenCalled(); + }); +}); diff --git a/src/main.ts b/src/main.ts index faa2d127..856d7aa7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -155,14 +155,13 @@ export default class PodNotes extends Plugin implements IPodNotes { id: "podnotes-show-leaf", name: "Show PodNotes", icon: "podcast" as IconType, - checkCallback: (checking: boolean) => { - if (checking) { - return !this.app.workspace.getLeavesOfType(VIEW_TYPE).length; - } - - this.app.workspace.getRightLeaf(false)?.setViewState({ - type: VIEW_TYPE, - }); + // Always available, and always reveals the view. The previous + // checkCallback hid this command whenever a leaf already existed, so + // once the view was open-but-hidden (collapsed sidebar, sidebar + // overflow, dragged out of sight) there was no way to bring it back + // (#55). activateView reuses the existing leaf and reveals it. + callback: () => { + void this.activateView(); }, }); @@ -377,6 +376,14 @@ export default class PodNotes extends Plugin implements IPodNotes { return this.view; }); + // Persistent, discoverable entry point in the left ribbon. The right + // sidebar header can overflow and hide the view's tab icon (the original + // report in #55), but the ribbon is always reachable, so users can always + // reopen PodNotes. + this.addRibbonIcon("podcast" as IconType, "Show PodNotes", () => { + void this.activateView(); + }); + this.app.workspace.onLayoutReady(this.onLayoutReady.bind(this)); this.registerObsidianProtocolHandler("podnotes", (action) => @@ -415,6 +422,26 @@ export default class PodNotes extends Plugin implements IPodNotes { } } + // Reveal the PodNotes view, creating its leaf when needed. Reusing an + // existing leaf (instead of gating on its absence) plus revealLeaf is what + // makes "Show PodNotes" and the ribbon icon reliably surface the view even + // when it is already open but hidden in a collapsed/overflowing sidebar (#55). + async activateView(): Promise { + const { workspace } = this.app; + + const existing = workspace.getLeavesOfType(VIEW_TYPE); + let leaf: WorkspaceLeaf | null = existing[0] ?? null; + + if (!leaf) { + leaf = workspace.getRightLeaf(false); + await leaf?.setViewState({ type: VIEW_TYPE, active: true }); + } + + if (leaf) { + await workspace.revealLeaf(leaf); + } + } + private getTranscriptionService(): TranscriptionService { if (!this.transcriptionService) { this.transcriptionService = new TranscriptionService(this); From 046f0ec2c5551bde7ee77829c904b651daae404b Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Tue, 16 Jun 2026 10:31:59 +0200 Subject: [PATCH 2/2] test(view): lock Show PodNotes command+ribbon wiring; soften ribbon docs (#55) Add an onload wiring test that asserts the podnotes-show-leaf command is registered as a plain always-available callback (never the old leaf-gated checkCallback) and that both the command and the ribbon icon route to activateView. activateView's own unit tests stay green under a refactor that reintroduced the gate, so this locks the actual #55 regression. Soften the ribbon docs: the icon is hideable via Manage ribbon actions and appears in the ribbon menu on mobile, so drop the 'always visible' claim. Addresses review nits; no behavior change. --- docs/docs/commands.md | 2 +- src/main.activateView.test.ts | 95 ++++++++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/docs/docs/commands.md b/docs/docs/commands.md index dc2b871b..e41041fa 100644 --- a/docs/docs/commands.md +++ b/docs/docs/commands.md @@ -3,7 +3,7 @@ Opens the PodNotes pane and brings it into focus. If the pane already exists but is hidden — for example in a collapsed sidebar or out of view because the right sidebar has too many icons — this command reveals it. If it does not exist yet, the command creates it in the right sidebar. You can run it from the command palette or bind it to a hotkey. -You can also open PodNotes from the **podcast icon in the left ribbon**, which is always visible and is the most reliable way to reopen the pane. +PodNotes also adds a **podcast icon to the left ribbon** as a reliable way to reopen the pane. On mobile it appears in the ribbon menu, and you can hide it via Obsidian's *Manage ribbon actions* if you prefer to use the command instead. If you are having issues with PodNotes not being shown, feel free to create an [issue](https://github.com/chhoumann/PodNotes/issues/new). diff --git a/src/main.activateView.test.ts b/src/main.activateView.test.ts index 4152efb5..36c122d3 100644 --- a/src/main.activateView.test.ts +++ b/src/main.activateView.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import PodNotes from "./main"; import { VIEW_TYPE } from "./constants"; @@ -71,3 +71,96 @@ describe("PodNotes.activateView", () => { expect(workspace.revealLeaf).not.toHaveBeenCalled(); }); }); + +// Locks the actual #55 wiring (not just activateView's internals): the +// "Show PodNotes" command must stay always-available (a plain callback, never +// a leaf-gated checkCallback) and the ribbon icon must route to activateView. +// A refactor that reintroduced the old checkCallback gate or unwired the ribbon +// would reproduce the bug while activateView's own unit tests stayed green. +describe("PodNotes onload wiring (#55)", () => { + // onload() wires real module-level stores to controllers; unload them after + // each test so leaked subscriptions don't fire into a disposed plugin. + const loaded: PodNotes[] = []; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + for (const p of loaded.splice(0)) { + p.onunload(); + } + vi.restoreAllMocks(); + }); + + async function loadPlugin() { + const activateSpy = vi + .spyOn(PodNotes.prototype, "activateView") + .mockResolvedValue(undefined); + + const commands: Array> = []; + const ribbonCalls: Array<{ + icon: string; + title: string; + handler: (evt: unknown) => unknown; + }> = []; + + const plugin = Object.create(PodNotes.prototype) as PodNotes; + Object.assign(plugin, { + loadData: vi.fn().mockResolvedValue({}), + saveData: vi.fn().mockResolvedValue(undefined), + addCommand: vi.fn((cmd: Record) => { + commands.push(cmd); + return cmd; + }), + addRibbonIcon: vi.fn( + (icon: string, title: string, handler: (evt: unknown) => unknown) => { + ribbonCalls.push({ icon, title, handler }); + return document.createElement("div"); + }, + ), + addSettingTab: vi.fn(), + registerView: vi.fn(), + registerObsidianProtocolHandler: vi.fn(), + registerEvent: vi.fn(), + app: { + workspace: { + onLayoutReady: vi.fn(), + on: vi.fn(() => ({})), + getLeavesOfType: vi.fn(() => []), + getRightLeaf: vi.fn(() => null), + revealLeaf: vi.fn(), + detachLeavesOfType: vi.fn(), + }, + }, + }); + + await plugin.onload(); + loaded.push(plugin); + + return { commands, ribbonCalls, activateSpy }; + } + + it("registers Show PodNotes as an always-available callback, not a leaf-gated checkCallback", async () => { + const { commands } = await loadPlugin(); + + const showCmd = commands.find((c) => c.id === "podnotes-show-leaf"); + expect(showCmd).toBeDefined(); + expect(typeof showCmd?.callback).toBe("function"); + expect(showCmd?.checkCallback).toBeUndefined(); + }); + + it("Show PodNotes command and ribbon icon both route to activateView", async () => { + const { commands, ribbonCalls, activateSpy } = await loadPlugin(); + + const showCmd = commands.find((c) => c.id === "podnotes-show-leaf"); + (showCmd?.callback as () => void)(); + expect(activateSpy).toHaveBeenCalledTimes(1); + + const ribbon = ribbonCalls.find((r) => r.title === "Show PodNotes"); + expect(ribbon).toBeDefined(); + expect(ribbon?.icon).toBe("podcast"); + ribbon?.handler(new MouseEvent("click")); + expect(activateSpy).toHaveBeenCalledTimes(2); + }); +});