mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-31 08:51:23 +00:00
Merge pull request #1009 from SeleiXi/feat/history-rev-nav
feat: Add revision prev/next buttons next to history slider
This commit is contained in:
@@ -58,6 +58,7 @@
|
||||
"test:e2e:obsidian:dialog-mounts": "tsx test/e2e-obsidian/scripts/dialog-mounts.ts",
|
||||
"test:e2e:obsidian:conflict-dialog-policy": "tsx test/e2e-obsidian/scripts/conflict-dialog-policy.ts",
|
||||
"test:e2e:obsidian:revision-repair": "tsx test/e2e-obsidian/scripts/revision-repair.ts",
|
||||
"test:e2e:obsidian:document-history-nav": "tsx test/e2e-obsidian/scripts/document-history-nav.ts",
|
||||
"test:e2e:obsidian:settings-ui": "tsx test/e2e-obsidian/scripts/settings-ui.ts",
|
||||
"test:e2e:obsidian:review-harness": "tsx test/e2e-obsidian/scripts/review-harness.ts",
|
||||
"test:e2e:obsidian:p2p-pane": "tsx test/e2e-obsidian/scripts/p2p-pane.ts",
|
||||
|
||||
@@ -74,6 +74,11 @@ export class DocumentHistoryModal extends Modal {
|
||||
currentDeleted = false;
|
||||
initialRev?: string;
|
||||
|
||||
// Revision navigation state (◀/▶ beside the range slider)
|
||||
revPrevBtn!: HTMLButtonElement;
|
||||
revNextBtn!: HTMLButtonElement;
|
||||
revNavIndicator!: HTMLSpanElement;
|
||||
|
||||
// Diff navigation state
|
||||
currentDiffIndex = -1;
|
||||
diffNavContainer!: HTMLDivElement;
|
||||
@@ -84,6 +89,8 @@ export class DocumentHistoryModal extends Modal {
|
||||
searchKeyword = "";
|
||||
searchResults: { rev: string; index: number; matchType: "Content" | "Diff" }[] = [];
|
||||
currentSearchIndex = -1;
|
||||
searchPrevBtn!: HTMLButtonElement;
|
||||
searchNextBtn!: HTMLButtonElement;
|
||||
searchResultIndicator!: HTMLSpanElement;
|
||||
searchProgressIndicator!: HTMLSpanElement;
|
||||
searchTimeout: number | null = null;
|
||||
@@ -125,12 +132,14 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.range.value = this.range.max;
|
||||
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
|
||||
await this.loadRevs(initialRev);
|
||||
this.updateRevisionNavUI();
|
||||
} catch (ex) {
|
||||
if (isErrorOfMissingDoc(ex)) {
|
||||
this.range.max = "0";
|
||||
this.range.value = "";
|
||||
this.range.disabled = true;
|
||||
this.contentView.setText(`We don't have any history for this note.`);
|
||||
this.updateRevisionNavUI();
|
||||
} else {
|
||||
this.contentView.setText(`Error while loading file.`);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
@@ -148,6 +157,37 @@ export class DocumentHistoryModal extends Modal {
|
||||
const index = this.revs_info.length - 1 - (Number(this.range.value) || 0);
|
||||
const rev = this.revs_info[index];
|
||||
await this.showExactRev(rev.rev);
|
||||
this.updateRevisionNavUI();
|
||||
}
|
||||
|
||||
navigateVersion(direction: "older" | "newer") {
|
||||
const current = Number(this.range.value) || 0;
|
||||
const max = Number(this.range.max) || 0;
|
||||
|
||||
if (direction === "older" && current > 0) {
|
||||
this.range.value = `${current - 1}`;
|
||||
} else if (direction === "newer" && current < max) {
|
||||
this.range.value = `${current + 1}`;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateRevisionNavUI();
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
}
|
||||
|
||||
updateRevisionNavUI() {
|
||||
if (!this.revNavIndicator) return;
|
||||
|
||||
const total = this.revs_info.length;
|
||||
const max = Number(this.range.max) || 0;
|
||||
const current = Number(this.range.value) || 0;
|
||||
|
||||
this.revNavIndicator.setText(total > 0 ? `Rev ${current + 1}/${total}` : "\u2014");
|
||||
|
||||
const disabled = !!this.range.disabled || total <= 1;
|
||||
this.revPrevBtn.disabled = disabled || current <= 0;
|
||||
this.revNextBtn.disabled = disabled || current >= max;
|
||||
}
|
||||
BlobURLs = new Map<string, string>();
|
||||
|
||||
@@ -391,6 +431,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (!keyword) {
|
||||
this.searchResultIndicator.setText("");
|
||||
this.searchProgressIndicator.setText("");
|
||||
this.updateSearchUI();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -464,6 +505,10 @@ export class DocumentHistoryModal extends Modal {
|
||||
const current = this.currentSearchIndex >= 0 ? this.currentSearchIndex + 1 : 0;
|
||||
this.searchResultIndicator.setText(`${current}/${this.searchResults.length} matches`);
|
||||
}
|
||||
|
||||
const hasResults = this.searchResults.length > 0;
|
||||
this.searchPrevBtn.disabled = !hasResults;
|
||||
this.searchNextBtn.disabled = !hasResults;
|
||||
}
|
||||
|
||||
navigateSearch(direction: "prev" | "next") {
|
||||
@@ -515,12 +560,14 @@ export class DocumentHistoryModal extends Modal {
|
||||
}, 500);
|
||||
});
|
||||
|
||||
searchRow.createEl("button", { text: "\u25B2" }, (e) => {
|
||||
this.searchPrevBtn = searchRow.createEl("button", { text: "\u25B2" }, (e) => {
|
||||
e.title = "Previous match";
|
||||
e.disabled = true;
|
||||
e.addEventListener("click", () => this.navigateSearch("prev"));
|
||||
});
|
||||
searchRow.createEl("button", { text: "\u25BC" }, (e) => {
|
||||
this.searchNextBtn = searchRow.createEl("button", { text: "\u25BC" }, (e) => {
|
||||
e.title = "Next match";
|
||||
e.disabled = true;
|
||||
e.addEventListener("click", () => this.navigateSearch("next"));
|
||||
});
|
||||
|
||||
@@ -530,18 +577,37 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.searchProgressIndicator = searchRow.createSpan({ text: "" });
|
||||
this.searchProgressIndicator.addClass("history-search-progress-indicator");
|
||||
|
||||
const divView = contentEl.createDiv("");
|
||||
divView.addClass("op-flex");
|
||||
const revNavRow = contentEl.createDiv({ cls: "history-rev-nav-row" });
|
||||
|
||||
divView.createEl("input", { type: "range" }, (e) => {
|
||||
this.revPrevBtn = revNavRow.createEl("button", { text: "\u25C0" }, (e) => {
|
||||
e.addClass("history-rev-nav-btn");
|
||||
e.title = "Older revision";
|
||||
e.disabled = true;
|
||||
e.addEventListener("click", () => this.navigateVersion("older"));
|
||||
});
|
||||
|
||||
revNavRow.createEl("input", { type: "range" }, (e) => {
|
||||
this.range = e;
|
||||
e.addEventListener("change", (e) => {
|
||||
e.addEventListener("change", () => {
|
||||
this.updateRevisionNavUI();
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
e.addEventListener("input", (e) => {
|
||||
e.addEventListener("input", () => {
|
||||
this.updateRevisionNavUI();
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
});
|
||||
|
||||
this.revNextBtn = revNavRow.createEl("button", { text: "\u25B6" }, (e) => {
|
||||
e.addClass("history-rev-nav-btn");
|
||||
e.title = "Newer revision";
|
||||
e.disabled = true;
|
||||
e.addEventListener("click", () => this.navigateVersion("newer"));
|
||||
});
|
||||
|
||||
this.revNavIndicator = revNavRow.createSpan({ text: "\u2014" }, (e) => {
|
||||
e.addClass("history-rev-indicator");
|
||||
});
|
||||
const diffOptionsRow = contentEl.createDiv("");
|
||||
diffOptionsRow.addClass("op-info");
|
||||
diffOptionsRow.addClass("diff-options-row");
|
||||
|
||||
+45
@@ -748,6 +748,51 @@ body.is-mobile .livesync-compatibility-review-notice {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.history-search-row button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.history-rev-nav-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.history-rev-nav-row input[type="range"] {
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.history-rev-nav-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 10px;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.history-rev-nav-btn:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.history-rev-nav-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.history-rev-indicator {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
min-width: 4.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.history-diff-options-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { evalObsidianJson } from "../runner/cli.ts";
|
||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||
import { createE2eObsidianDeviceLocalState, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||
import { withObsidianPage } from "../runner/ui.ts";
|
||||
import { createTemporaryVault } from "../runner/vault.ts";
|
||||
|
||||
process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "60000";
|
||||
process.env.E2E_OBSIDIAN_CORE_READY_TIMEOUT_MS ??= "60000";
|
||||
process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ??= "30000";
|
||||
|
||||
const notePath = "E2E/document-history-nav.md";
|
||||
const revisions = ["Version one alpha", "Version two beta keyword", "Version three gamma"];
|
||||
|
||||
type RevisionInfo = {
|
||||
revCount: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
type OpenHistoryResult = {
|
||||
opened: boolean;
|
||||
modalTitle: string | null;
|
||||
};
|
||||
|
||||
function assertEqual(actual: unknown, expected: unknown, message: string): void {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${message}\nExpected: ${String(expected)}\nActual: ${String(actual)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertTrue(value: boolean, message: string): void {
|
||||
if (!value) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function dismissWelcomeWizard(port: number): Promise<void> {
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const cancel = page.getByText("No, please take me back");
|
||||
if (await cancel.isVisible({ timeout: 5000 }).catch(() => false)) {
|
||||
await cancel.click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function seedRevisions(cliBinary: string, env: NodeJS.ProcessEnv): Promise<RevisionInfo> {
|
||||
return await evalObsidianJson<RevisionInfo>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(notePath)};`,
|
||||
`const revisions=${JSON.stringify(revisions)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));",
|
||||
"if(!(await app.vault.adapter.exists('E2E'))) await app.vault.createFolder('E2E');",
|
||||
"const existing=app.vault.getAbstractFileByPath(path);",
|
||||
"if(existing) await app.vault.delete(existing);",
|
||||
"const id=await core.services.path.path2id(path);",
|
||||
"let baseRev='';",
|
||||
"for(const content of revisions){",
|
||||
" const blob=new Blob([content],{type:'text/plain'});",
|
||||
" const now=Date.now();",
|
||||
" const result=await core.localDatabase.putDBEntry({",
|
||||
" _id:id,",
|
||||
" path,",
|
||||
" data:blob,",
|
||||
" ctime:now,",
|
||||
" mtime:now,",
|
||||
" size:(await blob.arrayBuffer()).byteLength,",
|
||||
" children:[],",
|
||||
" datatype:'plain',",
|
||||
" type:'plain',",
|
||||
" eden:{},",
|
||||
" },false,baseRev||undefined);",
|
||||
" if(!result?.ok) throw new Error(`Could not store revision for ${path}`);",
|
||||
" baseRev=result.rev;",
|
||||
" await sleep(100);",
|
||||
"}",
|
||||
`await app.vault.create(path,revisions[revisions.length-1]);`,
|
||||
"await core.services.fileProcessing.commitPendingFileEvents();",
|
||||
"const raw=await core.localDatabase.getRaw(id,{revs_info:true});",
|
||||
"const revCount=(raw._revs_info||[]).filter((e)=>e&&e.status==='available').length;",
|
||||
"return JSON.stringify({revCount,id});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function openDocumentHistory(cliBinary: string, env: NodeJS.ProcessEnv): Promise<OpenHistoryResult> {
|
||||
return await evalObsidianJson<OpenHistoryResult>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(notePath)};`,
|
||||
"const file=app.vault.getAbstractFileByPath(path);",
|
||||
"if(!file) throw new Error('Note missing before opening history');",
|
||||
"document.querySelectorAll('.modal-close-button').forEach((btn)=>btn.click());",
|
||||
"await new Promise((resolve)=>setTimeout(resolve,300));",
|
||||
"const leaf=app.workspace.getLeaf(false);",
|
||||
"await leaf.openFile(file);",
|
||||
"await new Promise((resolve)=>setTimeout(resolve,300));",
|
||||
"await app.commands.executeCommandById('obsidian-livesync:livesync-history');",
|
||||
"await new Promise((resolve)=>setTimeout(resolve,500));",
|
||||
"const modal=document.querySelector('.modal-container .modal-title');",
|
||||
"return JSON.stringify({opened:!!modal,modalTitle:modal?modal.textContent:null});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const binary = requireObsidianBinary();
|
||||
const cli = discoverObsidianCli();
|
||||
if (!cli.binary) throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`);
|
||||
|
||||
const vault = await createTemporaryVault();
|
||||
let session: ObsidianLiveSyncSession | undefined;
|
||||
|
||||
const screenshotDir =
|
||||
process.env.E2E_OBSIDIAN_HISTORY_SCREENSHOT_DIR ??
|
||||
join(process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e", "document-history-nav");
|
||||
const reportPath = process.env.E2E_OBSIDIAN_HISTORY_REPORT ?? join(screenshotDir, "report.txt");
|
||||
|
||||
async function captureStep(page: import("playwright").Page, step: string): Promise<string> {
|
||||
await mkdir(screenshotDir, { recursive: true });
|
||||
const path = join(screenshotDir, `${step}.png`);
|
||||
await page.screenshot({ path, fullPage: true });
|
||||
console.log(`Screenshot: ${path}`);
|
||||
return path;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Using Obsidian executable: ${binary}`);
|
||||
console.log(`Temporary vault: ${vault.path}`);
|
||||
|
||||
session = await startObsidianLiveSyncSession({
|
||||
binary,
|
||||
cliBinary: cli.binary,
|
||||
vault,
|
||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||
pluginData: {
|
||||
doctorProcessedVersion: "1.0.0",
|
||||
isConfigured: true,
|
||||
liveSync: false,
|
||||
remoteType: "",
|
||||
couchDB_URI: "",
|
||||
couchDB_DBNAME: "",
|
||||
couchDB_USER: "",
|
||||
couchDB_PASSWORD: "",
|
||||
remoteConfigurations: {},
|
||||
activeConfigurationId: "",
|
||||
notifyThresholdOfRemoteStorageSize: -1,
|
||||
periodicReplication: false,
|
||||
syncAfterMerge: false,
|
||||
syncOnEditorSave: false,
|
||||
syncOnFileOpen: false,
|
||||
syncOnSave: false,
|
||||
syncOnStart: false,
|
||||
},
|
||||
localStorageEntries: createE2eObsidianDeviceLocalState(vault.name),
|
||||
});
|
||||
await waitForLiveSyncCoreReady(cli.binary, session.cliEnv);
|
||||
|
||||
await dismissWelcomeWizard(session.remoteDebuggingPort);
|
||||
|
||||
const revisionInfo = await seedRevisions(cli.binary, session.cliEnv);
|
||||
console.log(`Seeded local history: ${revisionInfo.revCount} revisions for ${revisionInfo.id}`);
|
||||
assertEqual(revisionInfo.revCount, revisions.length, "Unexpected number of seeded revisions.");
|
||||
|
||||
const opened = await openDocumentHistory(cli.binary, session.cliEnv);
|
||||
assertEqual(opened.opened, true, "Document History modal did not open.");
|
||||
assertEqual(opened.modalTitle, "Document History", "Unexpected modal title.");
|
||||
|
||||
const report = await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
const modal = page.locator(".modal-container").filter({ hasText: "Document History" });
|
||||
await modal.waitFor({ state: "visible", timeout: 10000 });
|
||||
|
||||
const revNavRow = modal.locator(".history-rev-nav-row");
|
||||
await revNavRow.waitFor({ state: "visible", timeout: 10000 });
|
||||
|
||||
const indicator = revNavRow.locator(".history-rev-indicator");
|
||||
const initialIndicator = (await indicator.innerText()).trim();
|
||||
|
||||
const prevBtn = revNavRow.locator(".history-rev-nav-btn").first();
|
||||
const nextBtn = revNavRow.locator(".history-rev-nav-btn").last();
|
||||
const range = revNavRow.locator('input[type="range"]');
|
||||
|
||||
assertTrue(/Rev \d+\/\d+/.test(initialIndicator), `Unexpected initial indicator: ${initialIndicator}`);
|
||||
|
||||
const initialRange = await range.inputValue();
|
||||
|
||||
const screenshotPaths: string[] = [];
|
||||
screenshotPaths.push(await captureStep(page, "01-initial-latest-rev"));
|
||||
assertEqual(initialIndicator, "Rev 3/3", "History did not open at the latest revision.");
|
||||
assertEqual(initialRange, "2", "History slider did not open at the latest revision.");
|
||||
assertTrue(
|
||||
!(await prevBtn.isDisabled()),
|
||||
"Older revision button should be enabled at the latest revision."
|
||||
);
|
||||
assertTrue(await nextBtn.isDisabled(), "Newer revision button should be disabled at the latest revision.");
|
||||
|
||||
await prevBtn.click();
|
||||
await page.waitForTimeout(1000);
|
||||
const afterPrevIndicator = (await indicator.innerText()).trim();
|
||||
const afterPrevRange = await range.inputValue();
|
||||
assertTrue(
|
||||
Number(afterPrevRange) < Number(initialRange),
|
||||
`◀ did not move to an older revision. before=${initialRange}, after=${afterPrevRange}`
|
||||
);
|
||||
assertTrue(afterPrevIndicator !== initialIndicator, "◀ did not update Rev indicator.");
|
||||
assertTrue(!(await prevBtn.isDisabled()), "Older revision button was disabled before the oldest revision.");
|
||||
assertTrue(!(await nextBtn.isDisabled()), "Newer revision button was not enabled after moving backwards.");
|
||||
screenshotPaths.push(await captureStep(page, "02-after-click-older-rev"));
|
||||
|
||||
await prevBtn.click();
|
||||
await page.waitForTimeout(1000);
|
||||
assertEqual(await range.inputValue(), "0", "◀ did not reach the oldest revision.");
|
||||
assertTrue(await prevBtn.isDisabled(), "Older revision button should be disabled at the oldest revision.");
|
||||
assertTrue(
|
||||
!(await nextBtn.isDisabled()),
|
||||
"Newer revision button should be enabled at the oldest revision."
|
||||
);
|
||||
screenshotPaths.push(await captureStep(page, "03-at-oldest-rev"));
|
||||
|
||||
await nextBtn.click();
|
||||
await page.waitForTimeout(1000);
|
||||
await nextBtn.click();
|
||||
await page.waitForTimeout(1000);
|
||||
const afterNextIndicator = (await indicator.innerText()).trim();
|
||||
const afterNextRange = await range.inputValue();
|
||||
assertEqual(afterNextRange, initialRange, "▶ did not return to the original revision.");
|
||||
assertEqual(afterNextIndicator, initialIndicator, "▶ did not restore the Rev indicator.");
|
||||
assertTrue(
|
||||
await nextBtn.isDisabled(),
|
||||
"Newer revision button should be disabled after returning to latest."
|
||||
);
|
||||
screenshotPaths.push(await captureStep(page, "04-after-click-newer-rev"));
|
||||
|
||||
const searchInput = modal.locator(".history-search-input");
|
||||
await searchInput.fill("keyword");
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const searchIndicator = modal.locator(".history-search-result-indicator");
|
||||
const searchText = (await searchIndicator.innerText()).trim();
|
||||
assertTrue(/matches/.test(searchText), `Search indicator did not report matches: ${searchText}`);
|
||||
screenshotPaths.push(await captureStep(page, "05-after-search-keyword"));
|
||||
|
||||
const searchPrev = modal.locator(".history-search-row button").nth(0);
|
||||
const searchNext = modal.locator(".history-search-row button").nth(1);
|
||||
assertTrue(!(await searchPrev.isDisabled()), "Search ▲ should be enabled when matches exist.");
|
||||
assertTrue(!(await searchNext.isDisabled()), "Search ▼ should be enabled when matches exist.");
|
||||
|
||||
await searchNext.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const afterSearchIndicator = (await searchIndicator.innerText()).trim();
|
||||
assertTrue(
|
||||
/1\/\d+ matches/.test(afterSearchIndicator),
|
||||
`Search navigation failed: ${afterSearchIndicator}`
|
||||
);
|
||||
screenshotPaths.push(await captureStep(page, "06-after-search-next-match"));
|
||||
|
||||
return [
|
||||
`initialIndicator: ${initialIndicator}`,
|
||||
`afterPrevIndicator: ${afterPrevIndicator}`,
|
||||
`afterNextIndicator: ${afterNextIndicator}`,
|
||||
`searchIndicator: ${searchText}`,
|
||||
`afterSearchIndicator: ${afterSearchIndicator}`,
|
||||
"",
|
||||
"Screenshots:",
|
||||
...screenshotPaths.map((p) => `- ${p}`),
|
||||
].join("\n");
|
||||
});
|
||||
|
||||
await writeFile(reportPath, report, "utf-8");
|
||||
console.log(`Document History UI test passed.`);
|
||||
console.log(`Report: ${reportPath}`);
|
||||
console.log(`Screenshots: ${screenshotDir}`);
|
||||
} finally {
|
||||
if (session) await session.app.stop();
|
||||
await vault.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.stack : error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -9,6 +9,7 @@ const focusedScenarios = new Set([
|
||||
"onboarding-invitation",
|
||||
"dialog-mounts",
|
||||
"revision-repair",
|
||||
"document-history-nav",
|
||||
"settings-ui",
|
||||
"review-harness",
|
||||
"p2p-pane",
|
||||
|
||||
Reference in New Issue
Block a user