From 84b536eaf6ddaa9f2ca797788631ed60b2d9e599 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sun, 11 Oct 2026 03:01:10 +0200 Subject: [PATCH 01/11] test: cover removing a local file from Local Files Removing a file the user played with "Play with PodNotes" from the Local Files playlist must keep that file in the vault. Both tests fail on master: the removal trashes the user's own file. --- src/downloadEpisode.test.ts | 26 ++++ tests/e2e/harness.ts | 44 ++++++ tests/e2e/local-files-runtime.test.ts | 193 ++++++++++++++++++++++++++ tests/e2e/podnotes-runtime.test.ts | 41 +----- 4 files changed, 265 insertions(+), 39 deletions(-) create mode 100644 tests/e2e/local-files-runtime.test.ts diff --git a/src/downloadEpisode.test.ts b/src/downloadEpisode.test.ts index 1b1adfa8..51db6383 100644 --- a/src/downloadEpisode.test.ts +++ b/src/downloadEpisode.test.ts @@ -660,6 +660,32 @@ describe("removeDownloadedEpisode", () => { expect(vault.removeFile).not.toHaveBeenCalled(); expect(get(downloadedEpisodes)[episode.podcastName]).toEqual([]); }); + + it("drops a local file from the offline set but leaves the user's file on disk", async () => { + const vault = setupVault(); + const filePath = "Recordings/Interview.wav"; + const localEpisode: LocalEpisode = { + title: "Interview", + description: "", + content: "", + podcastName: "local file", + url: "[[Recordings/Interview.wav]]", + streamUrl: "app://local/Recordings/Interview.wav", + filePath, + mediaType: "audio", + }; + vault.present.add(filePath); + vault.disk.add(filePath); + downloadedEpisodes.set({ "local file": [{ ...localEpisode, filePath, size: 4 }] }); + + await removeDownloadedEpisode(localEpisode); + + expect(get(downloadedEpisodes)["local file"]).toEqual([]); + expect(vault.trashFile).not.toHaveBeenCalled(); + expect(vault.removeFile).not.toHaveBeenCalled(); + expect(vault.present.has(filePath)).toBe(true); + expect(vault.disk.has(filePath)).toBe(true); + }); }); describe("safeDownloadFilePath (#22)", () => { diff --git a/tests/e2e/harness.ts b/tests/e2e/harness.ts index 8a9ca094..c912f33d 100644 --- a/tests/e2e/harness.ts +++ b/tests/e2e/harness.ts @@ -72,6 +72,50 @@ export function evalJsonAsync(obsidian: ObsidianClient, code: string): Promis return obsidian.dev.evalJsonAsync(code); } +export async function writeSilentWav( + obsidian: ObsidianClient, + audioPath: string, + seconds: number, +): Promise { + await evalJsonAsync( + obsidian, + `(async () => { + const sampleRate = 8_000; + const sampleCount = sampleRate * ${seconds}; + const bytes = new Uint8Array(44 + sampleCount); + const view = new DataView(bytes.buffer); + const ascii = (offset, value) => { + for (let index = 0; index < value.length; index++) { + bytes[offset + index] = value.charCodeAt(index); + } + }; + + ascii(0, "RIFF"); + view.setUint32(4, 36 + sampleCount, true); + ascii(8, "WAVE"); + ascii(12, "fmt "); + view.setUint32(16, 16, true); + view.setUint16(20, 1, true); + view.setUint16(22, 1, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, sampleRate, true); + view.setUint16(32, 1, true); + view.setUint16(34, 8, true); + ascii(36, "data"); + view.setUint32(40, sampleCount, true); + bytes.fill(128, 44); + + const existing = app.vault.getAbstractFileByPath(${JSON.stringify(audioPath)}); + if (existing) { + await app.vault.modifyBinary(existing, bytes.buffer); + } else { + await app.vault.createBinary(${JSON.stringify(audioPath)}, bytes.buffer); + } + return true; + })()`, + ); +} + export async function waitForPodNotesReady(obsidian: ObsidianClient): Promise { await obsidian.waitFor(() => podNotesReady(obsidian), { ...WAIT_OPTS, diff --git a/tests/e2e/local-files-runtime.test.ts b/tests/e2e/local-files-runtime.test.ts new file mode 100644 index 00000000..a57b43c9 --- /dev/null +++ b/tests/e2e/local-files-runtime.test.ts @@ -0,0 +1,193 @@ +import type { ObsidianClient } from "obsidian-e2e"; +import { describe, expect, test } from "vitest"; +import type { IPodNotesSettings } from "../../src/types/IPodNotesSettings"; +import { + createPodNotesE2EHarness, + evalJsonAsync, + RELOAD_OPTIONS, + VIEW_TYPE, + WAIT_OPTS, + waitForPodNotesReady, + writeSilentWav, +} from "./harness"; + +type PodNotesData = Partial; + +const getContext = createPodNotesE2EHarness("local-files-runtime"); + +describe("local files runtime", () => { + test("removing a local file from Local Files keeps the user's file in the vault", async () => { + const { obsidian, plugin, sandbox } = getContext(); + const title = `E2E Own Recording ${Date.now()}`; + const fileName = `${title}.wav`; + const audioPath = sandbox.path(fileName); + const inDownloads = (data: PodNotesData) => + data.downloadedEpisodes?.["local file"]?.some( + (entry) => entry.filePath === audioPath, + ) ?? false; + const inLocalFiles = (data: PodNotesData) => + data.localFiles?.episodes.some((episode) => episode.title === title) ?? false; + const trashedFixture = async () => + (await listTrash(obsidian)).filter((path) => path.includes(title)); + + await plugin.updateDataAndReload((data) => { + data.downloadedEpisodes = {}; + data.playedEpisodes = {}; + }, RELOAD_OPTIONS); + await waitForPodNotesReady(obsidian); + await writeSilentWav(obsidian, audioPath, 60); + await sandbox.waitForExists(fileName, WAIT_OPTS); + const fileExplorerWasEnabled = await setFileExplorerEnabled(obsidian, true); + + try { + await openFileMenu(obsidian, audioPath); + await clickMenuItem(obsidian, "Play with PodNotes"); + await plugin.waitForData( + (data) => inDownloads(data) && inLocalFiles(data), + WAIT_OPTS, + ); + // Until #355 keeps the player's