From 0b36cb6b4bdca810c7aa9819d5310722131ef693 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Thu, 14 May 2026 09:53:38 +0000 Subject: [PATCH] test: add final scorecard composed e2e --- tests/e2e/scorecard-composed-flows.test.ts | 273 +++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 tests/e2e/scorecard-composed-flows.test.ts diff --git a/tests/e2e/scorecard-composed-flows.test.ts b/tests/e2e/scorecard-composed-flows.test.ts new file mode 100644 index 00000000..17112056 --- /dev/null +++ b/tests/e2e/scorecard-composed-flows.test.ts @@ -0,0 +1,273 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { + acquireVaultRunLock, + captureFailureArtifacts, + clearVaultRunLockMarker, + createObsidianClient, + createSandboxApi, +} from "obsidian-e2e"; +import type { + ObsidianClient, + PluginHandle, + SandboxApi, + VaultRunLock, +} from "obsidian-e2e"; + +const VAULT = "dev"; +const PLUGIN_ID = "quickadd"; +const TEST_PREFIX = "__qa-scorecard-"; +const WAIT_OPTS = { timeoutMs: 10_000, intervalMs: 200 }; + +let obsidian: ObsidianClient; +let sandbox: SandboxApi; +let qa: PluginHandle; +let lock: VaultRunLock | undefined; + +type QuickAddData = { + choices: Record[]; + migrations: Record; +}; + +function templateChoice(id: string) { + return { + id, + name: id, + type: "Template", + command: false, + templatePath: sandbox.path("scorecard-template.md"), + fileNameFormat: { + enabled: true, + format: sandbox.path("scorecard-template-output"), + }, + folder: { + enabled: false, + folders: [], + chooseWhenCreatingNote: false, + createInSameFolderAsActiveFile: false, + chooseFromSubfolders: false, + }, + appendLink: false, + openFile: false, + fileOpening: { + location: "tab", + direction: "vertical", + mode: "source", + focus: false, + }, + }; +} + +function captureChoice(id: string, content: string) { + return { + id, + name: id, + type: "Capture", + command: false, + captureTo: sandbox.path("scorecard-capture-target.md"), + captureToActiveFile: false, + activeFileWritePosition: "cursor", + createFileIfItDoesntExist: { + enabled: true, + createWithTemplate: false, + template: "", + }, + format: { enabled: true, format: content }, + prepend: false, + appendLink: false, + task: false, + insertAfter: { + enabled: false, + after: "", + insertAtEnd: false, + considerSubsections: false, + createIfNotFound: false, + createIfNotFoundLocation: "", + }, + newLineCapture: { enabled: false, direction: "below" }, + openFile: false, + fileOpening: { + location: "tab", + direction: "vertical", + mode: "default", + focus: false, + }, + }; +} + +function macroChoice(id: string, childChoiceIds: string[]) { + return { + id, + name: id, + type: "Macro", + command: false, + runOnStartup: false, + macro: { + id, + name: id, + commands: childChoiceIds.map((choiceId, index) => ({ + id: `${id}-choice-${index + 1}`, + name: `Run ${choiceId}`, + type: "Choice", + choiceId, + })), + }, + }; +} + +function multiChoice(id: string, choices: Record[]) { + return { + id, + name: id, + type: "Multi", + command: false, + choices, + }; +} + +function clearTestChoices(data: QuickAddData) { + data.choices = data.choices.filter( + (choice) => !String(choice.id ?? "").startsWith(TEST_PREFIX), + ); +} + +async function runChoice(name: string) { + await obsidian.exec("quickadd:run", { choice: name }); +} + +async function runTeardownStep( + label: string, + step: () => Promise | unknown, + errors: unknown[], +) { + try { + await step(); + } catch (error) { + errors.push(error); + console.warn(`scorecard-composed teardown failed during ${label}`, error); + } +} + +beforeAll(async () => { + obsidian = createObsidianClient({ vault: VAULT }); + await obsidian.verify(); + + lock = await acquireVaultRunLock({ + vaultName: VAULT, + vaultPath: await obsidian.vaultPath(), + }); + await lock.publishMarker(obsidian); + + qa = obsidian.plugin(PLUGIN_ID); + sandbox = await createSandboxApi({ + obsidian, + sandboxRoot: "__obsidian_e2e__", + testName: "scorecard-composed-flows", + }); +}, 30_000); + +afterAll(async () => { + const errors: unknown[] = []; + + await runTeardownStep("restoreData", () => qa?.restoreData?.(), errors); + await runTeardownStep("reload", () => qa?.reload?.(), errors); + await runTeardownStep("sandbox cleanup", () => sandbox?.cleanup?.(), errors); + await runTeardownStep( + "clear vault run lock marker", + () => (obsidian ? clearVaultRunLockMarker(obsidian) : undefined), + errors, + ); + await runTeardownStep("release vault lock", () => lock?.release(), errors); + + if (errors.length > 0) { + throw errors[0]; + } +}, 15_000); + +beforeEach((ctx) => { + ctx.onTestFailed(async () => { + await captureFailureArtifacts( + { id: ctx.task.id, name: ctx.task.name }, + obsidian, + { plugin: qa, captureOnFailure: true }, + ); + }); +}); + +describe("scorecard final acceptance composed flows", () => { + beforeAll(async () => { + const templateId = `${TEST_PREFIX}template`; + const captureId = `${TEST_PREFIX}capture`; + const macroId = `${TEST_PREFIX}macro`; + const multiId = `${TEST_PREFIX}multi`; + const multiCaptureId = `${TEST_PREFIX}multi-capture`; + + await sandbox.write("scorecard-template.md", "scorecard template body", { + waitForContent: true, + waitOptions: WAIT_OPTS, + }); + await sandbox.delete("scorecard-template-output.md"); + await sandbox.delete("scorecard-capture-target.md"); + + await qa.data().patch((data) => { + clearTestChoices(data); + const template = templateChoice(templateId); + const capture = captureChoice(captureId, "scorecard capture body"); + const multiChild = captureChoice( + multiCaptureId, + "scorecard multi child body", + ); + + data.choices.push( + template, + capture, + macroChoice(macroId, [templateId, captureId]), + multiChoice(multiId, [multiChild]), + ); + }); + + await qa.reload(); + }); + + it("runs a macro that composes template and capture choices", async () => { + await runChoice(`${TEST_PREFIX}macro`); + + await sandbox.waitForContent( + "scorecard-template-output.md", + (content) => content.includes("scorecard template body"), + WAIT_OPTS, + ); + await sandbox.waitForContent( + "scorecard-capture-target.md", + (content) => content.includes("scorecard capture body"), + WAIT_OPTS, + ); + }); + + it("exposes multi child routing and runs the routed child choice", async () => { + const listed = await obsidian.execJson<{ + choices: Array<{ name: string; path: string; runnable: boolean }>; + }>("quickadd:list"); + const multi = listed.choices.find( + (choice) => choice.name === `${TEST_PREFIX}multi`, + ); + const child = listed.choices.find( + (choice) => choice.name === `${TEST_PREFIX}multi-capture`, + ); + + expect(multi).toMatchObject({ + path: `${TEST_PREFIX}multi`, + runnable: false, + }); + expect(child).toMatchObject({ + path: `${TEST_PREFIX}multi / ${TEST_PREFIX}multi-capture`, + runnable: true, + }); + + await runChoice(`${TEST_PREFIX}multi-capture`); + await sandbox.waitForContent( + "scorecard-capture-target.md", + (content) => content.includes("scorecard multi child body"), + WAIT_OPTS, + ); + }); +});