Assessor_2.0/tests/features/view-bookmarks.test.ts

160 lines
5.6 KiB
TypeScript

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<string, string>();
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);
});
});