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
138 changes: 136 additions & 2 deletions tests/e2e/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
import {
type ObsidianClient,
type PluginReloadOptions,
createObsidianClient,
resolveObsidianEnvOptions,
} from "obsidian-e2e";
import { createPluginHarness } from "obsidian-e2e/vitest";
import {
type VaultRunLock,
acquireVaultRunLock,
clearVaultRunLockMarker,
createPluginHarness,
} from "obsidian-e2e/vitest";
import { afterAll, afterEach, beforeAll, beforeEach } from "vitest";

export const PLUGIN_ID = "podnotes";
export const VIEW_TYPE = "podcast_player_view";
Expand Down Expand Up @@ -37,9 +44,112 @@
* Canonical `OBSIDIAN_E2E_*` env is emitted by the shared runner; the legacy
* `PODNOTES_E2E_*` aliases remain a fallback during the migration.
*/
export const createPodNotesE2EHarness = createPluginHarness({
export function createPodNotesE2EHarness(testName: string) {
const suiteClient = createObsidianClient(
resolveObsidianEnvOptions({ legacyPrefix: "PODNOTES" }),
);
let lock: VaultRunLock | undefined;
let wasEnabled = false;
let runtimeFailure: unknown;
// Own the lock before shared setup reloads the plugin and can open a player leaf.
beforeAll(async () => {
lock = await acquireVaultRunLock({
vaultName: suiteClient.vaultName,
vaultPath: await suiteClient.vaultPath(),
timeoutMs: 60_000,
});
await lock.publishMarker(suiteClient);
wasEnabled = await suiteClient.plugin(PLUGIN_ID).isEnabled();
await (await import("./journey/library")).snapshotJourneyState(suiteClient);
await (await import("./journey/library")).installJourneyErrorRecorder(suiteClient);
}, 90_000);
afterAll(async () => {
try {
const library = await import("./journey/library");
if (library.hasJourneySnapshot(suiteClient)) {
await library.restoreJourneyCache(suiteClient);
await suiteClient.plugin(PLUGIN_ID).enable();
await waitForPodNotesReady(suiteClient);
await suiteClient.dev.evalJsonAsync(
"(async () => { await app.plugins.saveConfig(); return true; })()",
);
await library.restoreJourneyWorkspace(suiteClient);
if (!wasEnabled) {
await suiteClient.dev.evalJsonAsync(
`(async () => { await app.plugins.disablePluginAndSave(${JSON.stringify(PLUGIN_ID)}); await app.plugins.saveConfig(); return true; })()`,
);
}
}
await (await import("./journey/actions")).assertNoRuntimeErrors(suiteClient);
} finally {
if (lock) {
try {
await (
await import("./journey/library")
).removeJourneyErrorRecorder(suiteClient);
} finally {
try {
await clearVaultRunLockMarker(suiteClient);
} finally {
await lock.release();
}
}
}
}
}, 60_000);
afterEach(async () => {
try {
if ((await import("./journey/library")).hasJourneySnapshot(getContext().obsidian)) {
await getContext().obsidian.dev.evalJsonAsync(
"(async () => { await app.plugins.saveConfig(); return true; })()",
);
}
await (
await import("./journey/library")
).restoreJourneyWorkspace(getContext().obsidian);
await (await import("./journey/actions")).assertNoRuntimeErrors(getContext().obsidian);
} catch (error) {
if (runtimeFailure)
throw new AggregateError(
[runtimeFailure, error],
"Runtime and workspace restoration failed.",
);
throw error;
}
if (runtimeFailure) throw runtimeFailure;
});
const getContext = createSharedHarness(testName);
beforeAll(async () =>
(await import("./journey/actions")).assertNoRuntimeErrors(getContext().obsidian),
);
afterEach(async () => {
const { obsidian } = getContext();
if ((await import("./journey/library")).hasJourneySnapshot(obsidian)) {
try {
await (await import("./journey/actions")).assertNoRuntimeErrors(obsidian);
} catch (error) {
runtimeFailure = error;
}
}
});
// The shared harness rolls back only files a test wrote through the client, so
// data.json is snapshotted up front for tests that change settings through the UI.
beforeEach(async () => {
await (await import("./journey/actions")).assertNoRuntimeErrors(getContext().obsidian);
runtimeFailure = undefined;
await getContext().obsidian.dev.evalJson("window.__podnotesJourneyEarlyErrors = [], true");
await getContext().obsidian.exec("dev:errors", { clear: true });
await getContext()
.plugin.data()
.patch(() => undefined);
});
return getContext;
}

const createSharedHarness = createPluginHarness({
...resolveObsidianEnvOptions({ legacyPrefix: "PODNOTES" }),
pluginId: PLUGIN_ID,
sharedVaultLock: false,
reload: {
readyCommandId: RELOAD_OPTIONS.readyOptions?.commandId,
timeoutMs: 30_000,
Expand All @@ -54,6 +164,11 @@
beforeDataRestore: async (obsidian) => {
await detachPodNotesViews(obsidian);
await flushPodNotesSaves(obsidian);
const library = await import("./journey/library");
if (library.hasJourneySnapshot(obsidian)) {
// Reload while disabled so old feed requests cannot write over the restored cache.
await library.restoreJourneyCache(obsidian);
}
},
symlinkArtifacts: ["main.js", "manifest.json"],
// Module-relative, not process.cwd(): the factory otherwise compares dev-vault
Expand Down Expand Up @@ -126,6 +241,25 @@
);
}

export function expectKnownFailure<T>(
issue: number,
captured: T,
known: { correct: (value: T) => void; reproduces: (value: T) => boolean },
): void {
let failure: unknown;
try {
known.correct(captured);
} catch (error) {
failure = error;
}
if (failure === undefined) {
throw new Error(
`#${issue} no longer reproduces: remove its known-failure marker. Captured ${JSON.stringify(captured)}`,
);
}
if (!known.reproduces(captured)) throw failure;
}

export async function openPodNotesView(obsidian: ObsidianClient): Promise<void> {
const result = await evalJsonAsync<{
activeViewType: string | null;
Expand Down
Loading
Loading