blob: bbe5e9d22178700ab744633b6f923cccc449ab67 [file]
// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {recentGraphsStorage, type RecentGraphEntry} from './recent_graphs';
describe('RecentGraphsStorage', () => {
beforeEach(() => {
// Clear localStorage before each test
window.localStorage.clear();
// Reset the storage instance
recentGraphsStorage.data = [];
});
// Helper to create a mock graph entry
function createEntry(
nodeCount: number,
options?: Partial<RecentGraphEntry>,
): RecentGraphEntry {
return {
name: options?.name ?? `Graph ${nodeCount}`,
json: options?.json ?? `{"nodes": ${nodeCount}}`,
timestamp: options?.timestamp ?? Date.now(),
nodeCount,
labelCount: options?.labelCount ?? 0,
starred: options?.starred ?? false,
};
}
describe('data getter/setter', () => {
test('should start with empty data after clear', () => {
expect(recentGraphsStorage.data.length).toBe(0);
});
test('should allow setting data directly', () => {
recentGraphsStorage.data = [createEntry(3)];
expect(recentGraphsStorage.data.length).toBe(1);
expect(recentGraphsStorage.data[0].nodeCount).toBe(3);
});
});
describe('finalizeCurrentGraph', () => {
test('should do nothing when there is no working graph', () => {
recentGraphsStorage.finalizeCurrentGraph();
expect(recentGraphsStorage.data.length).toBe(0);
});
test('should do nothing when working graph has zero nodes', () => {
recentGraphsStorage.data = [createEntry(0)];
recentGraphsStorage.finalizeCurrentGraph();
// Should still be just one entry (unchanged)
expect(recentGraphsStorage.data.length).toBe(1);
});
test('should finalize current graph and create new working slot', () => {
recentGraphsStorage.data = [createEntry(3)];
expect(recentGraphsStorage.data.length).toBe(1);
recentGraphsStorage.finalizeCurrentGraph();
// Should now have 2 entries: new working slot at 0, finalized graph at 1
expect(recentGraphsStorage.data.length).toBe(2);
expect(recentGraphsStorage.data[0].nodeCount).toBe(0); // New empty working slot
expect(recentGraphsStorage.data[1].nodeCount).toBe(3); // Finalized graph
});
test('should enforce maxItems limit for unstarred graphs', () => {
const originalMaxItems = recentGraphsStorage.maxItems;
recentGraphsStorage.maxItems = 3;
// Set up 3 existing finalized graphs (index 0 is working slot)
recentGraphsStorage.data = [
createEntry(4), // Working slot with content
createEntry(3),
createEntry(2),
createEntry(1),
];
// Finalize current - should remove oldest unstarred
recentGraphsStorage.finalizeCurrentGraph();
// Count unstarred items with content
const unstarredWithContent = recentGraphsStorage.data.filter(
(e) => !e.starred && (e.nodeCount ?? 0) > 0,
);
// Should have at most maxItems unstarred graphs with content
expect(unstarredWithContent.length).toBeLessThanOrEqual(3);
recentGraphsStorage.maxItems = originalMaxItems;
});
test('should not count starred graphs toward maxItems limit', () => {
const originalMaxItems = recentGraphsStorage.maxItems;
recentGraphsStorage.maxItems = 2;
// Set up data with one starred graph and working slot
recentGraphsStorage.data = [
createEntry(4), // Working slot with content
createEntry(3, {starred: true}), // Starred - should be preserved
createEntry(2),
createEntry(1),
];
recentGraphsStorage.finalizeCurrentGraph();
// Count starred and unstarred with content
const starred = recentGraphsStorage.data.filter(
(e) => e.starred && (e.nodeCount ?? 0) > 0,
);
const unstarred = recentGraphsStorage.data.filter(
(e) => !e.starred && (e.nodeCount ?? 0) > 0,
);
// Should have 1 starred graph preserved
expect(starred.length).toBe(1);
// Should have at most maxItems unstarred graphs
expect(unstarred.length).toBeLessThanOrEqual(2);
recentGraphsStorage.maxItems = originalMaxItems;
});
});
describe('getCurrentJson', () => {
test('should return undefined when no graphs exist', () => {
expect(recentGraphsStorage.getCurrentJson()).toBeUndefined();
});
test('should return undefined when only empty working slot exists', () => {
recentGraphsStorage.data = [createEntry(0)];
expect(recentGraphsStorage.getCurrentJson()).toBeUndefined();
});
test('should return first graph with content', () => {
recentGraphsStorage.data = [createEntry(2, {json: '{"test": true}'})];
const json = recentGraphsStorage.getCurrentJson();
expect(json).toBe('{"test": true}');
});
test('should skip empty working slot and return finalized graph', () => {
recentGraphsStorage.data = [
createEntry(0), // Empty working slot
createEntry(3, {json: '{"finalized": true}'}),
];
const json = recentGraphsStorage.getCurrentJson();
expect(json).toBe('{"finalized": true}');
});
});
describe('setStarred', () => {
test('should star a graph', () => {
recentGraphsStorage.data = [createEntry(2)];
recentGraphsStorage.setStarred(0, true);
expect(recentGraphsStorage.data[0].starred).toBe(true);
});
test('should unstar a graph', () => {
recentGraphsStorage.data = [createEntry(2, {starred: true})];
recentGraphsStorage.setStarred(0, false);
expect(recentGraphsStorage.data[0].starred).toBe(false);
});
});
describe('rename', () => {
test('should rename a graph', () => {
recentGraphsStorage.data = [createEntry(2)];
recentGraphsStorage.rename(0, 'My Custom Name');
expect(recentGraphsStorage.data[0].name).toBe('My Custom Name');
});
test('should trim whitespace from name', () => {
recentGraphsStorage.data = [createEntry(2)];
recentGraphsStorage.rename(0, ' Trimmed Name ');
expect(recentGraphsStorage.data[0].name).toBe('Trimmed Name');
});
test('should keep original name if new name is empty', () => {
recentGraphsStorage.data = [createEntry(2, {name: 'Original'})];
recentGraphsStorage.rename(0, ' ');
expect(recentGraphsStorage.data[0].name).toBe('Original');
});
});
describe('getJson', () => {
test('should return undefined for invalid index', () => {
expect(recentGraphsStorage.getJson(-1)).toBeUndefined();
expect(recentGraphsStorage.getJson(0)).toBeUndefined();
expect(recentGraphsStorage.getJson(100)).toBeUndefined();
});
test('should return json for valid index', () => {
recentGraphsStorage.data = [createEntry(2, {json: '{"test": 123}'})];
const json = recentGraphsStorage.getJson(0);
expect(json).toBe('{"test": 123}');
});
});
describe('remove', () => {
test('should remove a graph from history', () => {
recentGraphsStorage.data = [
createEntry(0), // Working slot
createEntry(3),
createEntry(2),
createEntry(1),
];
const initialLength = recentGraphsStorage.data.length;
recentGraphsStorage.remove(2); // Remove middle entry
expect(recentGraphsStorage.data.length).toBe(initialLength - 1);
expect(recentGraphsStorage.data[2].nodeCount).toBe(1); // Last entry moved up
});
test('should remove first entry', () => {
recentGraphsStorage.data = [createEntry(1), createEntry(2)];
recentGraphsStorage.remove(0);
expect(recentGraphsStorage.data.length).toBe(1);
expect(recentGraphsStorage.data[0].nodeCount).toBe(2);
});
test('should remove last entry', () => {
recentGraphsStorage.data = [createEntry(1), createEntry(2)];
recentGraphsStorage.remove(1);
expect(recentGraphsStorage.data.length).toBe(1);
expect(recentGraphsStorage.data[0].nodeCount).toBe(1);
});
});
describe('clear', () => {
test('should clear all data', () => {
recentGraphsStorage.data = [
createEntry(1),
createEntry(2),
createEntry(3),
];
recentGraphsStorage.clear();
expect(recentGraphsStorage.data.length).toBe(0);
});
});
describe('generateName', () => {
test('should generate a name with date and time', () => {
const name = recentGraphsStorage.generateName();
// Should contain "Graph" and some date/time info
expect(name).toContain('Graph');
// Should match pattern like "Graph Jan 15 14:30"
expect(name).toMatch(/Graph \w+ \d+ \d{2}:\d{2}/);
});
});
describe('persistence', () => {
test('should persist graphs to localStorage', () => {
recentGraphsStorage.data = [createEntry(2)];
// Trigger save by calling a mutating method
recentGraphsStorage.setStarred(0, true);
const stored = window.localStorage.getItem('recentExploreGraphs');
expect(stored).not.toBeNull();
const parsed = JSON.parse(stored!);
expect(parsed.length).toBe(1);
expect(parsed[0].nodeCount).toBe(2);
expect(parsed[0].starred).toBe(true);
});
test('should persist renamed graph', () => {
recentGraphsStorage.data = [createEntry(2)];
recentGraphsStorage.rename(0, 'Custom Name');
const stored = window.localStorage.getItem('recentExploreGraphs');
const parsed = JSON.parse(stored!);
expect(parsed[0].name).toBe('Custom Name');
});
test('should persist removal', () => {
recentGraphsStorage.data = [createEntry(1), createEntry(2)];
recentGraphsStorage.remove(0);
const stored = window.localStorage.getItem('recentExploreGraphs');
const parsed = JSON.parse(stored!);
expect(parsed.length).toBe(1);
expect(parsed[0].nodeCount).toBe(2);
});
});
describe('historical graphs ordering', () => {
test('starred graphs should be identifiable in data', () => {
recentGraphsStorage.data = [
createEntry(0), // Working slot
createEntry(1, {starred: false}),
createEntry(2, {starred: true}),
createEntry(3, {starred: false}),
];
const starred = recentGraphsStorage.data.filter(
(e, i) => i > 0 && e.starred,
);
const unstarred = recentGraphsStorage.data.filter(
(e, i) => i > 0 && !e.starred,
);
expect(starred.length).toBe(1);
expect(starred[0].nodeCount).toBe(2);
expect(unstarred.length).toBe(2);
});
});
});