mirror of
https://github.com/fancive/obsidian-parallel-reader.git
synced 2026-07-22 06:53:43 +00:00
UX change: - The "open Parallel Reader" command now toggles the right sidebar's collapsed state instead of detaching the leaf. The tab and its content are preserved across hide/show, matching standard Obsidian behaviour for File Explorer and friends. Detaching the tab on every second press destroyed cached cards and forced re-generation. Behaviour: - No leaf yet → create + reveal - Leaf exists, right sidebar collapsed → reveal (expands sidebar) - Leaf exists, right sidebar expanded → collapse(); tab is preserved - No right sidebar (mobile / unusual layout) → reveal as fallback Tests: - New tests/view-render.test.js (18 cases) covering the component layer that was previously a gap. The 1.0.12 → 1.0.13 sidebar regression could not have been caught by any existing pure-function test; this file fills that gap by mocking app.workspace + view spies and running real runForFile / refreshViewAfterCache* / toggleParallelView. - shouldRender matrix (4 cases): silentView × sourceFile state — the exact regression introduced in PR2 where guards were over-applied. - toggleParallelView (4 cases): no-leaf opens, expanded → collapse, collapsed → reveal, no-rightSplit fallback. - refreshViewAfterCacheDelete + Clear (4 cases): matching file vs different file vs no-view, plus clear-all always renderEmpty. - runForFile outcome enum mapping (6 cases): already-running early return, already-running from catch, cancelled, generic error, regenerate-confirm cancellation, skipEditConfirm bypass — these drive batch statistics and were untested before. - view.deleteCard / updateCard cardPersistFailed path (3 cases): exposes ParallelReaderView via test-exports for instantiation; mocks cacheReplaceCards returning false to verify the failure notice path lands and the public method returns false. src/test-exports.ts: export ParallelReaderView for component tests. tests/catalog.json: register view-render.test.js under component. Change-Id: I439b5fca07b0928e6fe44d9025c2421c71c6cb28
542 lines
20 KiB
JavaScript
542 lines
20 KiB
JavaScript
/**
|
||
* Component-level tests for plugin/view interaction paths.
|
||
* These cover regressions that pure unit tests cannot catch — the bugs only
|
||
* manifest when state on `view` (sourceFile) and runForFile options
|
||
* (silentView) interact in specific combinations.
|
||
*/
|
||
const { assert, t } = require('./test-setup');
|
||
|
||
const { CACHE_SCHEMA_VERSION, generationFingerprint } = t;
|
||
const crypto = require('crypto');
|
||
const hashContent = (text) => crypto.createHash('sha1').update(text, 'utf8').digest('hex');
|
||
|
||
// Settings used by all tests in this file. Anthropic backend by default so we
|
||
// never accidentally hit any HTTP code path — the cache-hit path returns before
|
||
// summarizeDocument is called.
|
||
function makeSettings() {
|
||
return {
|
||
uiLanguage: 'en',
|
||
backend: 'api',
|
||
cliPath: '',
|
||
apiProvider: 'anthropic',
|
||
apiFormat: 'anthropic-messages',
|
||
apiBaseUrl: 'https://api.anthropic.com/v1',
|
||
apiKey: 'test',
|
||
apiKeyEnvVar: '',
|
||
apiAuthType: 'x-api-key',
|
||
apiHeaders: '',
|
||
apiMaxTokens: 4096,
|
||
maxDocChars: 100000,
|
||
maxCacheEntries: 100,
|
||
promptLanguage: 'en',
|
||
minCards: 5,
|
||
maxCards: 15,
|
||
customSystemPrompt: '',
|
||
model: 'claude-sonnet-4-6',
|
||
exportFolder: 'Reading/Articles',
|
||
cliTimeoutMs: 120000,
|
||
streaming: false,
|
||
streamingTimeoutMs: 120000,
|
||
};
|
||
}
|
||
|
||
function makeFakeView() {
|
||
const calls = { renderLoading: [], renderError: [], renderEmpty: 0, loadFor: [], renderStreamingPreview: [] };
|
||
const view = {
|
||
sourceFile: null,
|
||
sections: [],
|
||
renderLoading(file, msg) {
|
||
this.sourceFile = file;
|
||
calls.renderLoading.push([file?.path, msg]);
|
||
},
|
||
renderError(file, msg) {
|
||
this.sourceFile = file;
|
||
calls.renderError.push([file?.path, msg]);
|
||
},
|
||
renderEmpty() {
|
||
this.sourceFile = null;
|
||
calls.renderEmpty++;
|
||
},
|
||
loadFor(file, sections) {
|
||
this.sourceFile = file;
|
||
this.sections = sections;
|
||
calls.loadFor.push([file.path, sections.length]);
|
||
},
|
||
renderStreamingPreview(file, text) {
|
||
this.sourceFile = file;
|
||
calls.renderStreamingPreview.push([file.path, text.length]);
|
||
},
|
||
};
|
||
return { view, calls };
|
||
}
|
||
|
||
function makeFakeFile(path) {
|
||
return { path, basename: path.replace(/\.md$/, '') };
|
||
}
|
||
|
||
function makeBasePlugin(settings, { view, leaves, vaultRead } = {}) {
|
||
const plugin = new t.ParallelReaderPlugin();
|
||
plugin.settings = settings;
|
||
plugin.t = (key) => key;
|
||
plugin.jobs = new t.GenerationJobManager();
|
||
plugin.cacheManager = {
|
||
cache: {},
|
||
get: () => null,
|
||
put: async () => {},
|
||
touch: () => null,
|
||
delete: async () => {},
|
||
clear: async () => {},
|
||
};
|
||
plugin.app = {
|
||
workspace: {
|
||
getLeavesOfType: () => leaves || [],
|
||
getRightLeaf: () => null,
|
||
revealLeaf: async () => {},
|
||
getActiveViewOfType: () => null,
|
||
on: () => ({}),
|
||
},
|
||
vault: {
|
||
read: vaultRead || (async () => 'hello world content'),
|
||
adapter: {},
|
||
configDir: '.obsidian',
|
||
getMarkdownFiles: () => [],
|
||
getAbstractFileByPath: () => null,
|
||
on: () => ({}),
|
||
},
|
||
};
|
||
// Default getParallelView returns the supplied view if any.
|
||
plugin.getParallelView = () => view;
|
||
// Default getActiveView (used by activeFileStillMatches) returns null → match.
|
||
plugin.getActiveView = () => null;
|
||
return plugin;
|
||
}
|
||
|
||
/* ============================================================
|
||
* shouldRender matrix: silentView × view.sourceFile state
|
||
* Regression coverage for 1.0.13 (bug 1).
|
||
* Uses the cache-hit code path (no LLM call) to focus on the render guard.
|
||
* ============================================================ */
|
||
async function testShouldRender_NonSilent_FreshView() {
|
||
const settings = makeSettings();
|
||
const { view, calls } = makeFakeView();
|
||
const fileA = makeFakeFile('A.md');
|
||
const content = 'hello world content';
|
||
const cardEntry = {
|
||
schemaVersion: CACHE_SCHEMA_VERSION,
|
||
contentHash: hashContent(content),
|
||
settingsHash: generationFingerprint(settings),
|
||
cards: [{ title: 'T', anchor: 'hello', gist: '', bullets: [] }],
|
||
generatedAt: '2026-01-01T00:00:00.000Z',
|
||
};
|
||
const plugin = makeBasePlugin(settings, { view, leaves: [{ view, setViewState: async () => {} }] });
|
||
plugin.cacheManager.get = () => cardEntry;
|
||
|
||
// sourceFile=null (panel never showed anything yet)
|
||
view.sourceFile = null;
|
||
const result = await plugin.runForFile(fileA, false);
|
||
|
||
assert.strictEqual(result, 'cached', 'cache-hit returns cached');
|
||
assert.strictEqual(calls.loadFor.length, 1, 'non-silent + fresh view → loadFor must fire (1.0.12 regression)');
|
||
assert.strictEqual(calls.loadFor[0][0], 'A.md', 'loadFor passed correct file');
|
||
}
|
||
|
||
async function testShouldRender_NonSilent_DifferentFile() {
|
||
const settings = makeSettings();
|
||
const { view, calls } = makeFakeView();
|
||
const fileB = makeFakeFile('B.md');
|
||
const content = 'hello world content';
|
||
const cardEntry = {
|
||
schemaVersion: CACHE_SCHEMA_VERSION,
|
||
contentHash: hashContent(content),
|
||
settingsHash: generationFingerprint(settings),
|
||
cards: [{ title: 'T', anchor: 'hello', gist: '', bullets: [] }],
|
||
generatedAt: '2026-01-01T00:00:00.000Z',
|
||
};
|
||
const plugin = makeBasePlugin(settings, { view, leaves: [{ view, setViewState: async () => {} }] });
|
||
plugin.cacheManager.get = () => cardEntry;
|
||
|
||
// panel was showing fileA, user generates fileB via hotkey
|
||
view.sourceFile = makeFakeFile('A.md');
|
||
await plugin.runForFile(fileB, false);
|
||
|
||
assert.strictEqual(
|
||
calls.loadFor.length,
|
||
1,
|
||
'non-silent + different sourceFile → loadFor must fire (1.0.12 regression)',
|
||
);
|
||
assert.strictEqual(calls.loadFor[0][0], 'B.md', 'loadFor switched to file B');
|
||
}
|
||
|
||
async function testShouldRender_Silent_DifferentFile_Skipped() {
|
||
const settings = makeSettings();
|
||
const { view, calls } = makeFakeView();
|
||
const fileB = makeFakeFile('B.md');
|
||
const content = 'hello world content';
|
||
const cardEntry = {
|
||
schemaVersion: CACHE_SCHEMA_VERSION,
|
||
contentHash: hashContent(content),
|
||
settingsHash: generationFingerprint(settings),
|
||
cards: [{ title: 'T', anchor: 'hello', gist: '', bullets: [] }],
|
||
generatedAt: '2026-01-01T00:00:00.000Z',
|
||
};
|
||
const plugin = makeBasePlugin(settings, { view, leaves: [{ view, setViewState: async () => {} }] });
|
||
plugin.cacheManager.get = () => cardEntry;
|
||
|
||
// batch path: panel shows A, batch processes B → must NOT disturb panel
|
||
view.sourceFile = makeFakeFile('A.md');
|
||
const result = await plugin.runForFile(fileB, false, { silentView: true });
|
||
|
||
assert.strictEqual(result, 'cached');
|
||
assert.strictEqual(calls.loadFor.length, 0, 'silentView + different file → must NOT render (preserve user focus)');
|
||
}
|
||
|
||
async function testShouldRender_Silent_SameFile_Updates() {
|
||
const settings = makeSettings();
|
||
const { view, calls } = makeFakeView();
|
||
const fileA = makeFakeFile('A.md');
|
||
const content = 'hello world content';
|
||
const cardEntry = {
|
||
schemaVersion: CACHE_SCHEMA_VERSION,
|
||
contentHash: hashContent(content),
|
||
settingsHash: generationFingerprint(settings),
|
||
cards: [{ title: 'T', anchor: 'hello', gist: '', bullets: [] }],
|
||
generatedAt: '2026-01-01T00:00:00.000Z',
|
||
};
|
||
const plugin = makeBasePlugin(settings, { view, leaves: [{ view, setViewState: async () => {} }] });
|
||
plugin.cacheManager.get = () => cardEntry;
|
||
|
||
// batch path but the panel happens to be showing the file currently being processed
|
||
view.sourceFile = fileA;
|
||
await plugin.runForFile(fileA, false, { silentView: true });
|
||
|
||
assert.strictEqual(calls.loadFor.length, 1, 'silentView + same file → render IS allowed');
|
||
assert.strictEqual(calls.loadFor[0][0], 'A.md');
|
||
}
|
||
|
||
/* ============================================================
|
||
* toggleParallelView: open → close → open
|
||
* Regression coverage for 1.0.13 (bug 2).
|
||
* ============================================================ */
|
||
async function testToggleParallelView_NoLeaf_Opens() {
|
||
const settings = makeSettings();
|
||
let setViewStateCalled = 0;
|
||
let revealLeafCalled = 0;
|
||
const fakeLeaf = {
|
||
view: { containerEl: { children: [{}, {}] } },
|
||
setViewState: async () => {
|
||
setViewStateCalled++;
|
||
},
|
||
};
|
||
|
||
const plugin = makeBasePlugin(settings, { leaves: [] });
|
||
plugin.app.workspace.getRightLeaf = () => fakeLeaf;
|
||
plugin.app.workspace.revealLeaf = async () => {
|
||
revealLeafCalled++;
|
||
};
|
||
|
||
await plugin.toggleParallelView();
|
||
|
||
assert.strictEqual(setViewStateCalled, 1, 'first toggle creates leaf via setViewState');
|
||
assert.strictEqual(revealLeafCalled, 1, 'first toggle reveals leaf');
|
||
}
|
||
|
||
async function testToggleParallelView_LeafExists_SidebarOpen_Collapses() {
|
||
const settings = makeSettings();
|
||
let detachCalled = 0;
|
||
let collapseCalled = 0;
|
||
let revealCalled = 0;
|
||
const fakeLeaf = { detach: () => detachCalled++, view: {} };
|
||
const plugin = makeBasePlugin(settings, { leaves: [fakeLeaf] });
|
||
plugin.app.workspace.rightSplit = {
|
||
collapsed: false,
|
||
collapse: () => collapseCalled++,
|
||
};
|
||
plugin.app.workspace.revealLeaf = async () => {
|
||
revealCalled++;
|
||
};
|
||
|
||
await plugin.toggleParallelView();
|
||
|
||
assert.strictEqual(detachCalled, 0, 'leaf must NOT be detached (preserve tab content)');
|
||
assert.strictEqual(collapseCalled, 1, 'expanded sidebar → collapse');
|
||
assert.strictEqual(revealCalled, 0, 'no reveal when collapsing');
|
||
}
|
||
|
||
async function testToggleParallelView_LeafExists_SidebarCollapsed_Reveals() {
|
||
const settings = makeSettings();
|
||
let detachCalled = 0;
|
||
let collapseCalled = 0;
|
||
let revealCalled = 0;
|
||
const fakeLeaf = { detach: () => detachCalled++, view: {} };
|
||
const plugin = makeBasePlugin(settings, { leaves: [fakeLeaf] });
|
||
plugin.app.workspace.rightSplit = {
|
||
collapsed: true,
|
||
collapse: () => collapseCalled++,
|
||
};
|
||
plugin.app.workspace.revealLeaf = async () => {
|
||
revealCalled++;
|
||
};
|
||
|
||
await plugin.toggleParallelView();
|
||
|
||
assert.strictEqual(detachCalled, 0, 'leaf must NOT be detached');
|
||
assert.strictEqual(collapseCalled, 0, 'no collapse when sidebar already collapsed');
|
||
assert.strictEqual(revealCalled, 1, 'collapsed sidebar → reveal expands it and focuses our tab');
|
||
}
|
||
|
||
async function testToggleParallelView_NoRightSplit_FallsBackToReveal() {
|
||
const settings = makeSettings();
|
||
let revealCalled = 0;
|
||
const fakeLeaf = { detach: () => {}, view: {} };
|
||
const plugin = makeBasePlugin(settings, { leaves: [fakeLeaf] });
|
||
plugin.app.workspace.rightSplit = undefined;
|
||
plugin.app.workspace.revealLeaf = async () => {
|
||
revealCalled++;
|
||
};
|
||
|
||
await plugin.toggleParallelView();
|
||
|
||
assert.strictEqual(revealCalled, 1, 'no right sidebar (mobile?) → fallback to revealLeaf');
|
||
}
|
||
|
||
/* ============================================================
|
||
* refreshViewAfterCacheDelete / refreshViewAfterCacheClear
|
||
* Regression coverage for PR2 #14 (clear cache UI not refreshed).
|
||
* ============================================================ */
|
||
async function testRefreshViewAfterCacheDelete_MatchingFile() {
|
||
const settings = makeSettings();
|
||
const { view, calls } = makeFakeView();
|
||
view.sourceFile = makeFakeFile('A.md');
|
||
const plugin = makeBasePlugin(settings, { view });
|
||
|
||
plugin.refreshViewAfterCacheDelete('A.md');
|
||
|
||
assert.strictEqual(calls.renderEmpty, 1, 'view showing the cleared file → renderEmpty');
|
||
}
|
||
|
||
async function testRefreshViewAfterCacheDelete_DifferentFile() {
|
||
const settings = makeSettings();
|
||
const { view, calls } = makeFakeView();
|
||
view.sourceFile = makeFakeFile('A.md');
|
||
const plugin = makeBasePlugin(settings, { view });
|
||
|
||
plugin.refreshViewAfterCacheDelete('B.md'); // different file
|
||
|
||
assert.strictEqual(calls.renderEmpty, 0, 'view showing different file → must NOT renderEmpty');
|
||
}
|
||
|
||
async function testRefreshViewAfterCacheClear_AlwaysClears() {
|
||
const settings = makeSettings();
|
||
const { view, calls } = makeFakeView();
|
||
view.sourceFile = makeFakeFile('A.md');
|
||
const plugin = makeBasePlugin(settings, { view });
|
||
|
||
plugin.refreshViewAfterCacheClear();
|
||
|
||
assert.strictEqual(calls.renderEmpty, 1, 'clear-all → renderEmpty regardless of file');
|
||
}
|
||
|
||
/* ============================================================
|
||
* runForFile outcome enum mapping — drives batch statistics.
|
||
* Covers the catch-block branches that were untested before.
|
||
* ============================================================ */
|
||
async function testRunForFile_AlreadyRunning_EarlyReturn() {
|
||
const plugin = makeBasePlugin(makeSettings());
|
||
plugin.jobs = { isPending: () => true, isRunning: () => true };
|
||
const result = await plugin.runForFile(makeFakeFile('A.md'), false);
|
||
assert.strictEqual(result, 'already-running', 'isPending=true returns early with already-running');
|
||
}
|
||
|
||
async function testRunForFile_AlreadyRunning_FromCatch() {
|
||
const plugin = makeBasePlugin(makeSettings());
|
||
plugin.jobs = {
|
||
isPending: () => false,
|
||
isRunning: () => false,
|
||
start: async () => {
|
||
throw new t.GenerationJobAlreadyRunningError('A.md');
|
||
},
|
||
};
|
||
const result = await plugin.runForFile(makeFakeFile('A.md'), false);
|
||
assert.strictEqual(result, 'already-running', 'GenerationJobAlreadyRunningError from start → already-running');
|
||
}
|
||
|
||
async function testRunForFile_Cancelled_FromCatch() {
|
||
const plugin = makeBasePlugin(makeSettings());
|
||
plugin.jobs = {
|
||
isPending: () => false,
|
||
isRunning: () => false,
|
||
start: async () => {
|
||
throw new t.GenerationJobCancelledError('A.md');
|
||
},
|
||
};
|
||
const result = await plugin.runForFile(makeFakeFile('A.md'), false);
|
||
assert.strictEqual(result, 'cancelled', 'GenerationJobCancelledError → cancelled');
|
||
}
|
||
|
||
async function testRunForFile_GenericError_FromCatch() {
|
||
const plugin = makeBasePlugin(makeSettings());
|
||
plugin.jobs = {
|
||
isPending: () => false,
|
||
isRunning: () => false,
|
||
start: async () => {
|
||
throw new Error('backend down');
|
||
},
|
||
};
|
||
const origError = console.error;
|
||
console.error = () => {};
|
||
try {
|
||
const result = await plugin.runForFile(makeFakeFile('A.md'), false);
|
||
assert.strictEqual(result, 'error', 'plain Error → error');
|
||
} finally {
|
||
console.error = origError;
|
||
}
|
||
}
|
||
|
||
async function testRunForFile_RegenerateConfirm_Cancels() {
|
||
const plugin = makeBasePlugin(makeSettings());
|
||
// shouldConfirmRegenerate triggers when entry has user edits — simulate via cache entry shape.
|
||
plugin.cacheManager.get = () => ({
|
||
schemaVersion: CACHE_SCHEMA_VERSION,
|
||
contentHash: 'x',
|
||
settingsHash: 'y',
|
||
cards: [],
|
||
generatedAt: '2026-01-01T00:00:00.000Z',
|
||
updatedAt: '2026-01-02T00:00:00.000Z', // user edited after generation → confirm prompt
|
||
});
|
||
plugin.confirmRegenerateEditedCards = async () => false; // user clicks cancel
|
||
const result = await plugin.runForFile(makeFakeFile('A.md'), true);
|
||
assert.strictEqual(result, 'cancelled', 'user-cancelled regenerate confirm → cancelled');
|
||
}
|
||
|
||
async function testRunForFile_SkipEditConfirm_BypassesPrompt() {
|
||
const plugin = makeBasePlugin(makeSettings());
|
||
plugin.cacheManager.get = () => ({
|
||
schemaVersion: CACHE_SCHEMA_VERSION,
|
||
contentHash: 'x',
|
||
settingsHash: 'y',
|
||
cards: [],
|
||
generatedAt: '2026-01-01T00:00:00.000Z',
|
||
updatedAt: '2026-01-02T00:00:00.000Z',
|
||
});
|
||
let confirmCalled = 0;
|
||
plugin.confirmRegenerateEditedCards = async () => {
|
||
confirmCalled++;
|
||
return false;
|
||
};
|
||
// Make jobs.start succeed so we can verify confirm was skipped.
|
||
plugin.jobs = {
|
||
isPending: () => false,
|
||
isRunning: () => false,
|
||
start: async () => {
|
||
throw new Error('downstream'); // any quick exit
|
||
},
|
||
};
|
||
const origError = console.error;
|
||
console.error = () => {};
|
||
try {
|
||
await plugin.runForFile(makeFakeFile('A.md'), true, { skipEditConfirm: true });
|
||
assert.strictEqual(confirmCalled, 0, 'skipEditConfirm=true must bypass confirm prompt (batch flow)');
|
||
} finally {
|
||
console.error = origError;
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* view.deleteCard / view.updateCard — cardPersistFailed path
|
||
* Covers the case where cacheReplaceCards returns false (cache missing)
|
||
* and verifies that user is notified of the failure.
|
||
* ============================================================ */
|
||
async function testViewDeleteCard_PersistFails_ReturnsFalse() {
|
||
const plugin = makeBasePlugin(makeSettings());
|
||
let cacheReplaceCalled = null;
|
||
plugin.cacheReplaceCards = async (path, sections) => {
|
||
cacheReplaceCalled = { path, count: sections.length };
|
||
return false; // simulate missing cache entry
|
||
};
|
||
|
||
const fakeLeaf = { view: {} };
|
||
const view = new t.ParallelReaderView(fakeLeaf, plugin);
|
||
view.render = () => {}; // skip DOM
|
||
view.sourceFile = makeFakeFile('A.md');
|
||
view.sections = [
|
||
{ title: 'a', anchor: '', gist: '', bullets: [], startLine: 0, level: 0 },
|
||
{ title: 'b', anchor: '', gist: '', bullets: [], startLine: 1, level: 0 },
|
||
];
|
||
view.activeIdx = 0;
|
||
|
||
const ok = await view.deleteCard(0);
|
||
|
||
assert.strictEqual(ok, false, 'deleteCard returns false when cache persist fails');
|
||
assert.deepStrictEqual(cacheReplaceCalled, { path: 'A.md', count: 1 }, 'cacheReplaceCards called with new sections');
|
||
assert.strictEqual(view.sections.length, 1, 'view.sections is updated locally even on persist failure');
|
||
}
|
||
|
||
async function testViewDeleteCard_PersistOk_ReturnsTrue() {
|
||
const plugin = makeBasePlugin(makeSettings());
|
||
plugin.cacheReplaceCards = async () => true;
|
||
|
||
const fakeLeaf = { view: {} };
|
||
const view = new t.ParallelReaderView(fakeLeaf, plugin);
|
||
view.render = () => {};
|
||
view.sourceFile = makeFakeFile('A.md');
|
||
view.sections = [
|
||
{ title: 'a', anchor: '', gist: '', bullets: [], startLine: 0, level: 0 },
|
||
{ title: 'b', anchor: '', gist: '', bullets: [], startLine: 1, level: 0 },
|
||
];
|
||
view.activeIdx = 0;
|
||
|
||
const ok = await view.deleteCard(0);
|
||
assert.strictEqual(ok, true, 'deleteCard returns true on successful persist');
|
||
}
|
||
|
||
async function testViewUpdateCard_PersistFails_ReturnsFalse() {
|
||
const plugin = makeBasePlugin(makeSettings());
|
||
plugin.cacheReplaceCards = async () => false;
|
||
|
||
const fakeLeaf = { view: {} };
|
||
const view = new t.ParallelReaderView(fakeLeaf, plugin);
|
||
view.render = () => {};
|
||
view.sourceFile = makeFakeFile('A.md');
|
||
view.sections = [{ title: 'a', anchor: '', gist: '', bullets: [], startLine: 0, level: 0 }];
|
||
view.activeIdx = 0;
|
||
|
||
const ok = await view.updateCard(0, { title: 'a-edited' });
|
||
assert.strictEqual(ok, false, 'updateCard returns false on persist failure');
|
||
}
|
||
|
||
async function testRefreshViewAfterCacheClear_NoView() {
|
||
const settings = makeSettings();
|
||
const plugin = makeBasePlugin(settings, { view: undefined });
|
||
plugin.getParallelView = () => undefined;
|
||
|
||
// Must not throw when no view exists.
|
||
plugin.refreshViewAfterCacheClear();
|
||
}
|
||
|
||
(async () => {
|
||
await testShouldRender_NonSilent_FreshView();
|
||
await testShouldRender_NonSilent_DifferentFile();
|
||
await testShouldRender_Silent_DifferentFile_Skipped();
|
||
await testShouldRender_Silent_SameFile_Updates();
|
||
await testToggleParallelView_NoLeaf_Opens();
|
||
await testToggleParallelView_LeafExists_SidebarOpen_Collapses();
|
||
await testToggleParallelView_LeafExists_SidebarCollapsed_Reveals();
|
||
await testToggleParallelView_NoRightSplit_FallsBackToReveal();
|
||
await testRefreshViewAfterCacheDelete_MatchingFile();
|
||
await testRefreshViewAfterCacheDelete_DifferentFile();
|
||
await testRefreshViewAfterCacheClear_AlwaysClears();
|
||
await testRefreshViewAfterCacheClear_NoView();
|
||
await testRunForFile_AlreadyRunning_EarlyReturn();
|
||
await testRunForFile_AlreadyRunning_FromCatch();
|
||
await testRunForFile_Cancelled_FromCatch();
|
||
await testRunForFile_GenericError_FromCatch();
|
||
await testRunForFile_RegenerateConfirm_Cancels();
|
||
await testRunForFile_SkipEditConfirm_BypassesPrompt();
|
||
await testViewDeleteCard_PersistFails_ReturnsFalse();
|
||
await testViewDeleteCard_PersistOk_ReturnsTrue();
|
||
await testViewUpdateCard_PersistFails_ReturnsFalse();
|
||
console.log('view-render tests passed');
|
||
})().catch((e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
});
|