import { describe, expect, it } from 'vitest'; import { applySavedViewState, initialAppState } from '../../src/app/app-state'; import { makeSheet } from '../../src/importers/sheet-factory'; import type { StorageAdapter } from '../../src/persistence/storage-adapter'; import { VIEW_BOOKMARK_LIMIT, VIEW_BOOKMARK_STORAGE_KEY, captureViewState, loadProjectViewBookmarks, pathSnapshotFindingIds, parseViewBookmarks, saveProjectViewBookmarks, type ViewBookmark } from '../../src/features/view-bookmarks/view-bookmarks'; class MemoryStorage implements StorageAdapter { values = new Map(); get(key: string) { return this.values.get(key) ?? null; } set(key: string, value: string) { this.values.set(key, value); } remove(key: string) { this.values.delete(key); } } function bookmark(projectId: string, index: number): ViewBookmark { return { id: `${projectId}-${index}`, projectId, name: `View ${index}`, createdAt: '2026-08-17T00:00:00.000Z', view: { activeSheetId: 'sheet-a', view: 'columns', search: '', sortColumn: 'all', sortDirection: 'desc', titleColumn: 'Title', statusFilter: 'deviation', facetFilters: {}, actionOnly: false, selectedRecordId: '' } }; } function pathBookmark(projectId: string, index: number): ViewBookmark { return { ...bookmark(projectId, index), kind: 'path', path: { activeNodeIds: ['finding:a', 'tactic:b'], nodeIds: ['finding:a', 'tactic:b', 'technique:c'] } }; } describe('view bookmarks', () => { it('captures an independent copy of the complete working view', () => { const source = { ...initialAppState, activeSheetId: 'sheet-a', view: 'table' as const, search: 'synthetic', statusFilter: 'deviation', facetFilters: { severity: { values: ['critical', 'high'] } }, selectedRecordId: 'record-a' }; const captured = captureViewState(source); source.facetFilters.severity.values?.push('medium'); expect(captured).toMatchObject({ activeSheetId: 'sheet-a', view: 'table', search: 'synthetic', statusFilter: 'deviation', selectedRecordId: 'record-a' }); expect(captured.facetFilters.severity.values).toEqual(['critical', 'high']); }); it('keeps at most seven bookmarks per project and preserves other projects', () => { const storage = new MemoryStorage(); storage.set(VIEW_BOOKMARK_STORAGE_KEY, JSON.stringify([bookmark('project-b', 1)])); saveProjectViewBookmarks( storage, 'project-a', Array.from({ length: VIEW_BOOKMARK_LIMIT + 2 }, (_, index) => bookmark('project-a', index)) ); expect(loadProjectViewBookmarks(storage, 'project-a')).toHaveLength(7); expect(loadProjectViewBookmarks(storage, 'project-b').map(item => item.name)).toEqual(['View 1']); }); it('shares the seven slots between normal and path snapshots', () => { const storage = new MemoryStorage(); const mixed = [bookmark('project-a', 0), pathBookmark('project-a', 1), ...Array.from( { length: VIEW_BOOKMARK_LIMIT }, (_, index) => bookmark('project-a', index + 2) )]; saveProjectViewBookmarks(storage, 'project-a', mixed); const saved = loadProjectViewBookmarks(storage, 'project-a'); expect(saved).toHaveLength(VIEW_BOOKMARK_LIMIT); expect(saved[1]).toMatchObject({ kind: 'path', path: { activeNodeIds: ['finding:a', 'tactic:b'], nodeIds: ['finding:a', 'tactic:b', 'technique:c'] } }); }); it('rejects path snapshots without active nodes or a complete direct-neighbour set', () => { const validPath = pathBookmark('project-a', 1); const emptyPath = { ...validPath, id: 'empty', path: { activeNodeIds: [], nodeIds: [] } }; const incompletePath = { ...validPath, id: 'incomplete', path: { activeNodeIds: ['missing'], nodeIds: ['other'] } }; expect(parseViewBookmarks(JSON.stringify([validPath, emptyPath, incompletePath]))).toEqual([validPath]); }); it('derives the Work finding scope from all finding nodes in a path snapshot', () => { expect(pathSnapshotFindingIds({ activeNodeIds: ['category:Identity'], nodeIds: ['category:Identity', 'finding:a', 'finding:b', 'finding:a', 'tactic:Execution'] })).toEqual(['a', 'b']); }); it('ignores malformed local entries instead of applying partial state', () => { const valid = bookmark('project-a', 1); expect(parseViewBookmarks(JSON.stringify([{ bad: true }, valid]))).toEqual([valid]); expect(parseViewBookmarks('{broken')).toEqual([]); }); it('restores a saved view atomically only when its sheet still exists', () => { const sheet = { ...makeSheet('Synthetic', [['Title'], ['One']], '', 'synthetic.csv'), id: 'sheet-a' }; const current = { ...initialAppState, pathFindingIds: ['stale-path-record'], dataset: { fileName: 'synthetic.csv', fileSize: 1, lastModified: 1, format: 'csv', rawText: '', sheets: [sheet], profileId: 'generic', scanTool: 'Synthetic' } }; const saved = { ...bookmark('project-a', 1).view, view: 'records' as const, search: 'one', statusFilter: 'all', facetFilters: { severity: { values: ['high', 'critical'] } }, selectedRecordId: sheet.analysis.records[0].id }; const restored = applySavedViewState(current, saved); expect(restored).toMatchObject(saved); expect(restored.pathFindingIds).toEqual([]); expect(restored.facetFilters).not.toBe(saved.facetFilters); expect(applySavedViewState(current, { ...saved, activeSheetId: 'missing' })).toBe(current); }); });