Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/URIHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ describe("podNotesURIHandler", () => {

test("keeps the requested time for the player to apply after loading metadata", async () => {
playedEpisodes.markAsPlayed(testEpisode);
const revealPlayer = vi.fn();

await podNotesURIHandler(
{
Expand All @@ -134,6 +135,7 @@ describe("podNotesURIHandler", () => {
time: "240",
},
api as never,
revealPlayer,
);

expect(mockGetEpisodes).toHaveBeenCalledWith(testFeedUrl);
Expand All @@ -147,6 +149,25 @@ describe("podNotesURIHandler", () => {
expect(
get(playedEpisodes)[`${testEpisode.podcastName}::${testEpisode.title}`]?.finished,
).toBe(true);
expect(revealPlayer).toHaveBeenCalledTimes(1);
});

test("does not reveal the player when URI validation fails", async () => {
const revealPlayer = vi.fn();

await podNotesURIHandler(
{
action: "podnotes",
url: testFeedUrl,
episodeName: testEpisode.title,
time: "not-a-number",
},
api as never,
revealPlayer,
);

expect(revealPlayer).not.toHaveBeenCalled();
expect(get(viewState)).toBe(ViewState.PodcastGrid);
});

test("keeps the requested segment end for the player to apply after loading metadata", async () => {
Expand Down
7 changes: 6 additions & 1 deletion src/URIHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type PodNotesProtocolData = ObsidianProtocolData & {
endTime?: string;
to?: string;
};
type RevealPodNotesPlayer = () => Promise<void> | void;

/**
* Obsidian decodes protocol query values with decodeURIComponent only, which does NOT turn '+'
Expand Down Expand Up @@ -72,7 +73,8 @@ function resolveResumeTime(episode: Episode): number {

export default async function podNotesURIHandler(
{ url, episodeName, time, endTime, end, to }: PodNotesProtocolData,
api: IAPI
api: IAPI,
revealPlayer?: RevealPodNotesPlayer,
) {
if (!url || !episodeName) {
new Notice("URL and episode name are required to play an episode");
Expand Down Expand Up @@ -127,6 +129,7 @@ export default async function podNotesURIHandler(
requestedPlaybackTime.set(null);
}

await revealPlayer?.();
return;
}

Expand All @@ -143,6 +146,7 @@ export default async function podNotesURIHandler(
}
isPaused.set(false);

await revealPlayer?.();
return;
}

Expand Down Expand Up @@ -197,6 +201,7 @@ export default async function podNotesURIHandler(
);
currentEpisode.set(episode);
viewState.set(ViewState.Player);
await revealPlayer?.();
}

function parseSegmentEndTime(
Expand Down
2 changes: 2 additions & 0 deletions src/getContextMenuHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
playedEpisodes,
currentEpisode,
viewState,
plugin,
} from "./store";
import type { LocalEpisode } from "./types/LocalEpisode";
import { ViewState } from "./types/ViewState";
Expand Down Expand Up @@ -75,6 +76,7 @@ export default function getContextMenuHandler(app: App): EventRef {

currentEpisode.set(localEpisode);
viewState.set(ViewState.Player);
get(plugin)?.enablePodcastViewMount();

// Setting the stores above only updates an already-mounted
// PodNotes view. When the view is closed (or hidden in a
Expand Down
1 change: 1 addition & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface Manifest {

// Not everything is implemented.
interface App2 extends App {
isMobile?: boolean;
plugins: {
app: App;
enabledPlugins: Set<string>;
Expand Down
122 changes: 122 additions & 0 deletions src/main.activateView.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import PodNotes from "./main";
import { VIEW_TYPE } from "./constants";
import { Platform } from "obsidian";

// 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
Expand All @@ -14,6 +15,13 @@ function makeLeaf() {
};
}

const originalPlatform = { ...Platform };

afterEach(() => {
Object.assign(Platform, originalPlatform);
vi.useRealTimers();
});

function setupPlugin({
existingLeaves = [] as ReturnType<typeof makeLeaf>[],
rightLeaf = makeLeaf() as ReturnType<typeof makeLeaf> | null,
Expand All @@ -27,6 +35,10 @@ function setupPlugin({
// 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;
Object.assign(plugin as unknown as Record<string, unknown>, {
podcastViewMountEnabled: true,
views: new Set(),
});
(plugin as unknown as { app: { workspace: typeof workspace } }).app = {
workspace,
};
Expand Down Expand Up @@ -70,6 +82,115 @@ describe("PodNotes.activateView", () => {
await expect(plugin.activateView()).resolves.toBeUndefined();
expect(workspace.revealLeaf).not.toHaveBeenCalled();
});

it("enables and mounts a dormant restored view before revealing it", async () => {
const existing = makeLeaf();
const firstRestoredView = { mountPodcastView: vi.fn() };
const secondRestoredView = { mountPodcastView: vi.fn() };
const { plugin } = setupPlugin({ existingLeaves: [existing] });
Object.assign(plugin as unknown as Record<string, unknown>, {
podcastViewMountEnabled: false,
views: new Set([firstRestoredView, secondRestoredView]),
});

expect(plugin.shouldMountPodcastView()).toBe(false);

await plugin.activateView();

expect(plugin.shouldMountPodcastView()).toBe(true);
expect(firstRestoredView.mountPodcastView).toHaveBeenCalledTimes(1);
expect(secondRestoredView.mountPodcastView).toHaveBeenCalledTimes(1);
});
});

describe("PodNotes.onLayoutReady", () => {
function setupLayoutPlugin({
existingLeaves = [] as ReturnType<typeof makeLeaf>[],
isMobile = false,
layoutReady = true,
rightLeaf = makeLeaf() as ReturnType<typeof makeLeaf> | null,
} = {}) {
const workspace = {
layoutReady,
getLeavesOfType: vi.fn().mockReturnValue(existingLeaves),
getRightLeaf: vi.fn().mockReturnValue(rightLeaf),
detachLeavesOfType: vi.fn(),
};

const plugin = Object.create(PodNotes.prototype) as PodNotes;
Object.assign(plugin as unknown as Record<string, unknown>, {
isUnloaded: false,
layoutReadyAttempts: 0,
layoutReadyRetry: null,
maxLayoutReadyAttempts: 10,
views: new Set(),
});
(plugin as unknown as { app: { isMobile: boolean; workspace: typeof workspace } }).app = {
isMobile,
workspace,
};

return { plugin, workspace, rightLeaf };
}

it("creates the startup view on desktop when no leaf exists", () => {
const { plugin, workspace, rightLeaf } = setupLayoutPlugin();

plugin.onLayoutReady();

expect(workspace.getRightLeaf).toHaveBeenCalledWith(false);
expect(rightLeaf?.setViewState).toHaveBeenCalledWith({
type: VIEW_TYPE,
});
});

it("does not auto-create the startup view in the mobile app", () => {
Object.assign(Platform, {
isDesktop: false,
isDesktopApp: false,
isIosApp: true,
isMobile: true,
isMobileApp: true,
isPhone: true,
});
const { plugin, workspace, rightLeaf } = setupLayoutPlugin();

plugin.onLayoutReady();

expect(workspace.getLeavesOfType).not.toHaveBeenCalled();
expect(workspace.getRightLeaf).not.toHaveBeenCalled();
expect(rightLeaf?.setViewState).not.toHaveBeenCalled();
expect(workspace.detachLeavesOfType).not.toHaveBeenCalled();
});

it("does not auto-create the startup view when desktop Obsidian emulates mobile", () => {
const { plugin, workspace, rightLeaf } = setupLayoutPlugin({
isMobile: true,
});

plugin.onLayoutReady();

expect(workspace.getLeavesOfType).not.toHaveBeenCalled();
expect(workspace.getRightLeaf).not.toHaveBeenCalled();
expect(rightLeaf?.setViewState).not.toHaveBeenCalled();
expect(workspace.detachLeavesOfType).not.toHaveBeenCalled();
});

it("cancels a pending startup retry on unload", () => {
vi.useFakeTimers();
const { plugin, workspace } = setupLayoutPlugin({ layoutReady: false });

plugin.onLayoutReady();
expect(vi.getTimerCount()).toBe(1);

plugin.onunload();
expect(vi.getTimerCount()).toBe(0);

vi.advanceTimersByTime(100);

expect(workspace.getRightLeaf).not.toHaveBeenCalled();
expect(workspace.detachLeavesOfType).toHaveBeenCalledWith(VIEW_TYPE);
});
});

// Locks the actual #55 wiring (not just activateView's internals): the
Expand Down Expand Up @@ -124,6 +245,7 @@ describe("PodNotes onload wiring (#55)", () => {
registerObsidianProtocolHandler: vi.fn(),
registerEvent: vi.fn(),
mediaSessionActions: [],
views: new Set(),
app: {
workspace: {
onLayoutReady: vi.fn(),
Expand Down
Loading
Loading