feat(stats): Trends dashboard overhaul — title visibility, ranking modes, calendar-accurate windows, tooltips (#140)

This commit is contained in:
2026-07-06 23:52:43 -07:00
committed by GitHub
parent 48a084914a
commit 8b9a70c5a6
16 changed files with 969 additions and 135 deletions
@@ -5,9 +5,40 @@ import type { PerAnimeDataPoint } from './StackedTrendChart';
import {
buildAnimeVisibilityOptions,
filterHiddenAnimeData,
loadHiddenTitles,
loadMaxTitles,
loadMaxTitlesMode,
loadShowEmptyDays,
pruneHiddenAnime,
saveHiddenTitles,
saveMaxTitles,
saveMaxTitlesMode,
saveShowEmptyDays,
} from './anime-visibility';
function installLocalStorage(initial: Record<string, string> = {}) {
const previous = Object.getOwnPropertyDescriptor(globalThis, 'localStorage');
const values = new Map(Object.entries(initial));
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
value: {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => values.set(key, value),
removeItem: (key: string) => values.delete(key),
},
});
return {
values,
restore: () => {
if (previous) {
Object.defineProperty(globalThis, 'localStorage', previous);
} else {
delete (globalThis as { localStorage?: unknown }).localStorage;
}
},
};
}
const SAMPLE_POINTS: PerAnimeDataPoint[] = [
{ epochDay: 1, animeTitle: 'KonoSuba', value: 5 },
{ epochDay: 2, animeTitle: 'KonoSuba', value: 10 },
@@ -45,3 +76,86 @@ test('pruneHiddenAnime drops titles that are no longer available', () => {
assert.deepEqual([...hidden], ['KonoSuba']);
});
test('hidden titles round-trip through localStorage', () => {
const { restore } = installLocalStorage();
try {
saveHiddenTitles(new Set(['KonoSuba', 'One Piece']));
assert.deepEqual([...loadHiddenTitles()], ['KonoSuba', 'One Piece']);
} finally {
restore();
}
});
test('loadHiddenTitles tolerates missing or malformed stored values', () => {
const { restore } = installLocalStorage({
'subminer-stats-trends-hidden-titles': '{"not":"an array"}',
});
try {
assert.deepEqual([...loadHiddenTitles()], []);
} finally {
restore();
}
});
test('max titles preference defaults to 7 and round-trips including explicit All', () => {
const { values, restore } = installLocalStorage();
try {
// First run (nothing stored) defaults to the 7-title cap, not "All".
assert.equal(loadMaxTitles(), 7);
saveMaxTitles(5);
assert.equal(loadMaxTitles(), 5);
// "All" persists explicitly instead of collapsing back to the default.
saveMaxTitles(null);
assert.equal(loadMaxTitles(), null);
assert.equal(values.get('subminer-stats-trends-max-titles'), 'all');
} finally {
restore();
}
});
test('loadMaxTitles falls back to the default for unsupported stored values', () => {
for (const storedValue of ['8', '0', 'banana']) {
const { restore } = installLocalStorage({ 'subminer-stats-trends-max-titles': storedValue });
try {
assert.equal(loadMaxTitles(), 7);
} finally {
restore();
}
}
});
test('max titles mode defaults to recent and round-trips', () => {
const { restore } = installLocalStorage();
try {
assert.equal(loadMaxTitlesMode(), 'recent');
saveMaxTitlesMode('total');
assert.equal(loadMaxTitlesMode(), 'total');
saveMaxTitlesMode('recent');
assert.equal(loadMaxTitlesMode(), 'recent');
} finally {
restore();
}
});
test('loadMaxTitlesMode falls back to recent for unknown stored values', () => {
const { restore } = installLocalStorage({ 'subminer-stats-trends-max-titles-mode': 'sideways' });
try {
assert.equal(loadMaxTitlesMode(), 'recent');
} finally {
restore();
}
});
test('show empty days preference defaults to true and round-trips', () => {
const { restore } = installLocalStorage();
try {
assert.equal(loadShowEmptyDays(), true);
saveShowEmptyDays(false);
assert.equal(loadShowEmptyDays(), false);
saveShowEmptyDays(true);
assert.equal(loadShowEmptyDays(), true);
} finally {
restore();
}
});