Fix diagnostics checks across Obsidian renderers

This commit is contained in:
vorotamoroz committed 2026-10-23 13:53:56 +00:00
1 parent ce0524d49f
commit e56098eb57
1 file changed
+74 -45
@@ -31,6 +31,18 @@ type DiagnosticPageGlobal = typeof globalThis & {
diagnosticDownload?: { name: string; content: string }; diagnosticDownload?: { name: string; content: string };
}; };
async function findVisibleElement(page: Page, selector: string): Promise<Locator> {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
for (const candidate of page.context().pages()) {
const element = candidate.locator(selector);
if (await element.isVisible()) return element;
}
await page.waitForTimeout(50);
}
throw new Error(`No Obsidian renderer contains the visible element: ${selector}`);
}
async function openCommand(page: Page, kind: "usage" | "integrity") { async function openCommand(page: Page, kind: "usage" | "integrity") {
const id = kind === "usage" ? "analyse-database" : "inspect-database-integrity"; const id = kind === "usage" ? "analyse-database" : "inspect-database-integrity";
const opened = await page.evaluate( const opened = await page.evaluate(
@@ -243,11 +255,14 @@ async function assertDiagnosticFileLookup(
session: ObsidianLiveSyncSession, session: ObsidianLiveSyncSession,
mobile = false mobile = false
): Promise<void> { ): Promise<void> {
const dialoguePage = hatch.page();
const viewport = page.viewportSize();
if (viewport) await dialoguePage.setViewportSize(viewport);
const setting = hatch.locator(".setting-item").filter({ const setting = hatch.locator(".setting-item").filter({
has: page.getByText("Find a file from a report pseudonym", { exact: true }), has: hatch.page().getByText("Find a file from a report pseudonym", { exact: true }),
}); });
await setting.getByRole("button", { name: "Find file", exact: true }).click(); await setting.getByRole("button", { name: "Find file", exact: true }).click();
const dialogue = page.locator(".diagnostic-file-lookup"); const dialogue = dialoguePage.locator(".diagnostic-file-lookup");
await dialogue.waitFor({ state: "visible", timeout }); await dialogue.waitFor({ state: "visible", timeout });
const input = dialogue.getByLabel("Report path pseudonym", { exact: true }); const input = dialogue.getByLabel("Report path pseudonym", { exact: true });
await input.fill("file-001"); await input.fill("file-001");
@@ -259,8 +274,8 @@ async function assertDiagnosticFileLookup(
await candidates.first().waitFor({ timeout }); await candidates.first().waitFor({ timeout });
assert.equal(await candidates.count(), 2, "Distinct Metadata IDs claiming the same path must remain selectable"); assert.equal(await candidates.count(), 2, "Distinct Metadata IDs claiming the same path must remain selectable");
if (mobile) { if (mobile) {
await assertNoHorizontalOverflow(page, dialogue, { label: "Pseudonym file lookup matches" }); await assertNoHorizontalOverflow(dialoguePage, dialogue, { label: "Pseudonym file lookup matches" });
await assertNoHorizontalOverflow(page, dialogue.locator(".lookup-candidates"), { await assertNoHorizontalOverflow(dialoguePage, dialogue.locator(".lookup-candidates"), {
label: "Pseudonym file lookup candidate list", label: "Pseudonym file lookup candidate list",
}); });
assert.ok( assert.ok(
@@ -270,7 +285,7 @@ async function assertDiagnosticFileLookup(
await captureObsidianElement( await captureObsidianElement(
session.remoteDebuggingPort, session.remoteDebuggingPort,
"database-diagnostics-file-matches-mobile.png", "database-diagnostics-file-matches-mobile.png",
(page) => page.locator(".modal-container").filter({ has: page.locator(".diagnostic-file-lookup") }) (page) => findVisibleElement(page, ".modal-container:has(.diagnostic-file-lookup)")
); );
} }
const shadow = candidates.filter({ hasText: shadowId }); const shadow = candidates.filter({ hasText: shadowId });
@@ -294,28 +309,30 @@ async function assertDiagnosticFileLookup(
for (const privateValue of [fixturePath, shadowId, fixtureContent.trim()]) { for (const privateValue of [fixturePath, shadowId, fixtureContent.trim()]) {
assert.ok(!json.includes(privateValue), "A follow-up report must preserve the original report's pseudonyms"); assert.ok(!json.includes(privateValue), "A follow-up report must preserve the original report's pseudonyms");
} }
await installDiagnosticExportInterception(page); await installDiagnosticExportInterception(dialoguePage);
await dialogue.getByRole("button", { name: "Copy report", exact: true }).click(); await dialogue.getByRole("button", { name: "Copy report", exact: true }).click();
assert.equal(await page.evaluate(() => (globalThis as DiagnosticPageGlobal).diagnosticClipboard), json); assert.equal(await dialoguePage.evaluate(() => (globalThis as DiagnosticPageGlobal).diagnosticClipboard), json);
await dialogue.getByRole("button", { name: "Save report", exact: true }).click(); await dialogue.getByRole("button", { name: "Save report", exact: true }).click();
await page.waitForFunction(() => !!(globalThis as DiagnosticPageGlobal).diagnosticDownload, undefined, { timeout }); await dialoguePage.waitForFunction(() => !!(globalThis as DiagnosticPageGlobal).diagnosticDownload, undefined, {
assert.deepEqual(await page.evaluate(() => (globalThis as DiagnosticPageGlobal).diagnosticDownload), { timeout,
});
assert.deepEqual(await dialoguePage.evaluate(() => (globalThis as DiagnosticPageGlobal).diagnosticDownload), {
name: "diagnostic-file-report.json", name: "diagnostic-file-report.json",
content: json, content: json,
}); });
await assertDesktopDiagnosticResultLayout(page, dialogue, "saving a follow-up report"); await assertDesktopDiagnosticResultLayout(dialoguePage, dialogue, "saving a follow-up report");
if (mobile) { if (mobile) {
const modal = page.locator(".modal-container").filter({ has: dialogue }); const modal = dialoguePage.locator(".modal-container").filter({ has: dialogue });
await assertMobileDialogueLayout(page, modal, "Pseudonym file follow-up report"); await assertMobileDialogueLayout(dialoguePage, modal, "Pseudonym file follow-up report");
await assertNoHorizontalOverflow(page, dialogue, { label: "Pseudonym file follow-up content" }); await assertNoHorizontalOverflow(dialoguePage, dialogue, { label: "Pseudonym file follow-up content" });
await assertNoHorizontalOverflow(page, dialogue.locator(".report-token"), { await assertNoHorizontalOverflow(dialoguePage, dialogue.locator(".report-token"), {
label: "Pseudonym file follow-up token", label: "Pseudonym file follow-up token",
}); });
} }
await captureObsidianElement( await captureObsidianElement(
session.remoteDebuggingPort, session.remoteDebuggingPort,
mobile ? "database-diagnostics-file-follow-up-mobile.png" : "database-diagnostics-file-follow-up.png", mobile ? "database-diagnostics-file-follow-up-mobile.png" : "database-diagnostics-file-follow-up.png",
(page) => page.locator(".modal-container").filter({ has: page.locator(".diagnostic-file-lookup") }) (page) => findVisibleElement(page, ".modal-container:has(.diagnostic-file-lookup)")
); );
await dialogue.getByRole("radio", { name: "MARKDOWN", exact: true }).check(); await dialogue.getByRole("radio", { name: "MARKDOWN", exact: true }).check();
const markdown = await dialogue.locator("textarea").inputValue(); const markdown = await dialogue.locator("textarea").inputValue();
@@ -379,10 +396,10 @@ async function assertFollowUpStopsAfterClose(page: Page, hatch: Locator, token:
})()`); })()`);
try { try {
const setting = hatch.locator(".setting-item").filter({ const setting = hatch.locator(".setting-item").filter({
has: page.getByText("Find a file from a report pseudonym", { exact: true }), has: hatch.page().getByText("Find a file from a report pseudonym", { exact: true }),
}); });
await setting.getByRole("button", { name: "Find file", exact: true }).click(); await setting.getByRole("button", { name: "Find file", exact: true }).click();
const dialogue = page.locator(".diagnostic-file-lookup"); const dialogue = hatch.page().locator(".diagnostic-file-lookup");
await dialogue.getByLabel("Report path pseudonym", { exact: true }).fill(token); await dialogue.getByLabel("Report path pseudonym", { exact: true }).fill(token);
await dialogue.getByRole("button", { name: "Find file", exact: true }).click(); await dialogue.getByRole("button", { name: "Find file", exact: true }).click();
const candidate = dialogue.locator("[data-diagnostic-candidate]").filter({ hasText: shadowId }); const candidate = dialogue.locator("[data-diagnostic-candidate]").filter({ hasText: shadowId });
@@ -466,7 +483,7 @@ async function main(): Promise<void> {
const activeSession = session; const activeSession = session;
let usageJSON = ""; let usageJSON = "";
let usageFixtureRow: { path: string; id: string; revision: string } | undefined; let usageFixtureRow: { path: string; id: string; revision: string } | undefined;
await withObsidianPage(session.remoteDebuggingPort, async (page) => { const desktopScreenshot = await withObsidianPage(session.remoteDebuggingPort, async (page) => {
await page.setViewportSize({ width: 800, height: 600 }); await page.setViewportSize({ width: 800, height: 600 });
const choice = await openCommand(page, "usage"); const choice = await openCommand(page, "usage");
await choice.getByRole("radio", { name: "Pseudonyms (recommended)", exact: true }).focus(); await choice.getByRole("radio", { name: "Pseudonyms (recommended)", exact: true }).focus();
@@ -560,13 +577,15 @@ async function main(): Promise<void> {
hatch = await (await openLiveSyncSettings(page, timeout)).openPage("Hatch"); hatch = await (await openLiveSyncSettings(page, timeout)).openPage("Hatch");
await assertFollowUpStopsAfterClose(page, hatch, usageFixtureRow.path); await assertFollowUpStopsAfterClose(page, hatch, usageFixtureRow.path);
await captureObsidianElement(activeSession.remoteDebuggingPort, "database-diagnostics-hatch.png", (page) => await captureObsidianElement(activeSession.remoteDebuggingPort, "database-diagnostics-hatch.png", (page) =>
page.locator(".modal.mod-settings") findVisibleElement(page, ".modal.mod-settings")
); );
const setting = hatch const setting = hatch
.locator(".setting-item") .locator(".setting-item")
.filter({ has: page.getByText("Inspect database integrity", { exact: true }) }); .filter({ has: hatch.page().getByText("Inspect database integrity", { exact: true }) });
await setting.getByRole("button", { name: "Inspect", exact: true }).click(); await setting.getByRole("button", { name: "Inspect", exact: true }).click();
const progressDialogue = page.locator(".database-diagnostic"); const progressDialogue = await findVisibleElement(page, ".database-diagnostic");
const progressPage = progressDialogue.page();
await progressPage.setViewportSize({ width: 800, height: 600 });
assert.equal( assert.equal(
await progressDialogue await progressDialogue
.getByRole("radio", { name: "Pseudonyms (recommended)", exact: true }) .getByRole("radio", { name: "Pseudonyms (recommended)", exact: true })
@@ -579,7 +598,7 @@ async function main(): Promise<void> {
.isChecked(), .isChecked(),
true true
); );
await assertDesktopDiagnosticChoiceLayout(page, progressDialogue); await assertDesktopDiagnosticChoiceLayout(progressPage, progressDialogue);
await installDiagnosticReadGate(cli, activeSession, shadowId, "metadata"); await installDiagnosticReadGate(cli, activeSession, shadowId, "metadata");
try { try {
await progressDialogue.getByRole("button", { name: "Begin inspection", exact: true }).click(); await progressDialogue.getByRole("button", { name: "Begin inspection", exact: true }).click();
@@ -593,7 +612,7 @@ async function main(): Promise<void> {
await captureObsidianElement( await captureObsidianElement(
activeSession.remoteDebuggingPort, activeSession.remoteDebuggingPort,
"database-diagnostics-progress.png", "database-diagnostics-progress.png",
(page) => page.locator(".modal-container").filter({ has: page.locator(".database-diagnostic") }) (page) => findVisibleElement(page, ".modal-container:has(.database-diagnostic)")
); );
await progressDialogue.getByRole("button", { name: "Cancel inspection", exact: true }).click(); await progressDialogue.getByRole("button", { name: "Cancel inspection", exact: true }).click();
await evalObsidianJson( await evalObsidianJson(
@@ -601,7 +620,7 @@ async function main(): Promise<void> {
`(async()=>{globalThis.databaseDiagnosticReadGate?.release();return JSON.stringify(true);})()`, `(async()=>{globalThis.databaseDiagnosticReadGate?.release();return JSON.stringify(true);})()`,
activeSession.cliEnv activeSession.cliEnv
); );
await page.locator('[data-diagnostic-phase="result"]').waitFor({ timeout }); await progressPage.locator('[data-diagnostic-phase="result"]').waitFor({ timeout });
await assertFormatChoices(progressDialogue, ["MARKDOWN", "JSON"]); await assertFormatChoices(progressDialogue, ["MARKDOWN", "JSON"]);
await chooseFormat(progressDialogue, "json"); await chooseFormat(progressDialogue, "json");
assert.equal(JSON.parse(await progressDialogue.locator("textarea").inputValue()).status, "cancelled"); assert.equal(JSON.parse(await progressDialogue.locator("textarea").inputValue()).status, "cancelled");
@@ -612,33 +631,41 @@ async function main(): Promise<void> {
} }
await setting.getByRole("button", { name: "Inspect", exact: true }).click(); await setting.getByRole("button", { name: "Inspect", exact: true }).click();
const integrity = page.locator(".database-diagnostic"); const integrity = await findVisibleElement(page, ".database-diagnostic");
const integrityPage = integrity.page();
assert.equal( assert.equal(
await integrity.getByRole("radio", { name: "Pseudonyms (recommended)", exact: true }).isChecked(), await integrity.getByRole("radio", { name: "Pseudonyms (recommended)", exact: true }).isChecked(),
true true
); );
await integrity.getByRole("button", { name: "Begin inspection", exact: true }).click(); await integrity.getByRole("button", { name: "Begin inspection", exact: true }).click();
await page.locator('[data-diagnostic-phase="result"]').waitFor({ timeout }); await integrityPage.locator('[data-diagnostic-phase="result"]').waitFor({ timeout });
await assertFormatChoices(integrity, ["MARKDOWN", "JSON"]); await assertFormatChoices(integrity, ["MARKDOWN", "JSON"]);
assert.equal(await integrity.getByRole("radio", { name: "MARKDOWN", exact: true }).isChecked(), true); assert.equal(await integrity.getByRole("radio", { name: "MARKDOWN", exact: true }).isChecked(), true);
const markdownReport = await integrity.locator("textarea").inputValue(); const markdownReport = await integrity.locator("textarea").inputValue();
const markdownDetailsWithCodeBlock = const markdownDetailsWithCodeBlock =
/<details>\s*<summary>Database integrity report \(partial\)<\/summary>\s*(`{3,})text\r?\n[\s\S]*?\1\s*<\/details>/; /<details>\s*<summary>Database integrity report \(partial\)<\/summary>\s*(`{3,})text\r?\n[\s\S]*?\1\s*<\/details>/;
assert.match(markdownReport, markdownDetailsWithCodeBlock); assert.match(markdownReport, markdownDetailsWithCodeBlock);
await installDiagnosticExportInterception(page); await installDiagnosticExportInterception(integrityPage);
await integrity.getByRole("button", { name: "Copy report", exact: true }).click(); await integrity.getByRole("button", { name: "Copy report", exact: true }).click();
assert.equal( assert.equal(
await page.evaluate(() => (globalThis as DiagnosticPageGlobal).diagnosticClipboard), await integrityPage.evaluate(() => (globalThis as DiagnosticPageGlobal).diagnosticClipboard),
markdownReport markdownReport
); );
await integrity.getByRole("button", { name: "Save report", exact: true }).click(); await integrity.getByRole("button", { name: "Save report", exact: true }).click();
await page.waitForFunction(() => !!(globalThis as DiagnosticPageGlobal).diagnosticDownload, undefined, { await integrityPage.waitForFunction(
timeout, () => !!(globalThis as DiagnosticPageGlobal).diagnosticDownload,
}); undefined,
assert.deepEqual(await page.evaluate(() => (globalThis as DiagnosticPageGlobal).diagnosticDownload), { {
name: "database-integrity-partial.md", timeout,
content: markdownReport, }
}); );
assert.deepEqual(
await integrityPage.evaluate(() => (globalThis as DiagnosticPageGlobal).diagnosticDownload),
{
name: "database-integrity-partial.md",
content: markdownReport,
}
);
await chooseFormat(integrity, "json"); await chooseFormat(integrity, "json");
const reportText = await integrity.locator("textarea").inputValue(); const reportText = await integrity.locator("textarea").inputValue();
const integrityReport = JSON.parse(reportText); const integrityReport = JSON.parse(reportText);
@@ -670,15 +697,17 @@ async function main(): Promise<void> {
); );
for (const privateValue of [fixturePath, shadowId, fixtureContent.trim()]) for (const privateValue of [fixturePath, shadowId, fixtureContent.trim()])
assert.ok(!reportText.includes(privateValue)); assert.ok(!reportText.includes(privateValue));
const screenshot = await captureObsidianElement(
activeSession.remoteDebuggingPort,
"database-diagnostics-desktop.png",
(page) => findVisibleElement(page, ".modal-container:has(.database-diagnostic)")
);
await integrity.getByRole("button", { name: "Close", exact: true }).click();
await integrity.waitFor({ state: "hidden", timeout });
await settingsNavigator.close();
return screenshot;
}); });
const desktopScreenshot = await captureObsidianElement(
session.remoteDebuggingPort,
"database-diagnostics-desktop.png",
(page) => page.locator(".modal-container").filter({ has: page.locator(".database-diagnostic") })
);
await withObsidianPage(session.remoteDebuggingPort, async (page) => { await withObsidianPage(session.remoteDebuggingPort, async (page) => {
await page.locator(".database-diagnostic").getByRole("button", { name: "Close", exact: true }).click();
await page.keyboard.press("Escape");
const localOnly = await openCommand(page, "integrity"); const localOnly = await openCommand(page, "integrity");
await localOnly.getByRole("checkbox", { name: "Include remote database", exact: true }).uncheck(); await localOnly.getByRole("checkbox", { name: "Include remote database", exact: true }).uncheck();
await localOnly.getByRole("button", { name: "Begin inspection", exact: true }).click(); await localOnly.getByRole("button", { name: "Begin inspection", exact: true }).click();
@@ -780,7 +809,7 @@ async function main(): Promise<void> {
const mobileScreenshot = await captureObsidianElement( const mobileScreenshot = await captureObsidianElement(
session.remoteDebuggingPort, session.remoteDebuggingPort,
"database-diagnostics-mobile.png", "database-diagnostics-mobile.png",
(page) => page.locator(".modal-container").filter({ has: page.locator(".database-diagnostic") }) (page) => findVisibleElement(page, ".modal-container:has(.database-diagnostic)")
); );
await withObsidianPage(session.remoteDebuggingPort, async (page) => { await withObsidianPage(session.remoteDebuggingPort, async (page) => {
await page.setViewportSize({ width: 375, height: 667 }); await page.setViewportSize({ width: 375, height: 667 });
@@ -828,7 +857,7 @@ async function main(): Promise<void> {
const mobileResultScreenshot = await captureObsidianElement( const mobileResultScreenshot = await captureObsidianElement(
session.remoteDebuggingPort, session.remoteDebuggingPort,
"database-diagnostics-mobile-result.png", "database-diagnostics-mobile-result.png",
(page) => page.locator(".modal-container").filter({ has: page.locator(".database-diagnostic") }) (page) => findVisibleElement(page, ".modal-container:has(.database-diagnostic)")
); );
await withObsidianPage(session.remoteDebuggingPort, async (page) => { await withObsidianPage(session.remoteDebuggingPort, async (page) => {
await page.setViewportSize({ width: 375, height: 667 }); await page.setViewportSize({ width: 375, height: 667 });
@@ -857,7 +886,7 @@ async function main(): Promise<void> {
await captureObsidianElement( await captureObsidianElement(
activeSession.remoteDebuggingPort, activeSession.remoteDebuggingPort,
"database-diagnostics-mobile-saved.png", "database-diagnostics-mobile-saved.png",
(page) => page.locator(".modal-container").filter({ has: page.locator(".database-diagnostic") }) (page) => findVisibleElement(page, ".modal-container:has(.database-diagnostic)")
); );
await dialogue.getByRole("button", { name: "Close", exact: true }).click(); await dialogue.getByRole("button", { name: "Close", exact: true }).click();
await dialogue.waitFor({ state: "hidden", timeout }); await dialogue.waitFor({ state: "hidden", timeout });