Files
readest/apps/readest-app/src/__tests__/utils/cache.test.ts
T
Huang Xin bed31e8181 feat(library): add Manage Cache to advanced settings (#4359)
Add a "Manage Cache" item to the library Advanced Settings menu (native
mobile apps only) that opens a modern dialog showing the combined size and
file count of the app's reclaimable storage, with a confirm-gated clear that
reports per-file progress.

- iOS clears Cache + Temp + Documents/Inbox; Android clears Cache + Temp.
- Multi-source helper (getCacheEntries/getCacheStats/clearCacheEntries) with
  unit tests; per-file failures are counted, never abort the run.
- Dialog uses the centered-hero + btn-contrast design language, theme-neutral
  progress, and is e-ink correct.
- i18n: new strings translated across all locales (+ en plural forms).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 19:57:07 +02:00

115 lines
4.0 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { AppService, FileItem } from '@/types/system';
import {
clearCacheEntries,
getCacheEntries,
getCacheStats,
CacheClearProgress,
CacheEntry,
} from '@/utils/cache';
const makeFiles = (...names: string[]): FileItem[] =>
names.map((path, i) => ({ path, size: (i + 1) * 10 }));
describe('getCacheStats', () => {
it('sums file count and byte size', () => {
const entries: CacheEntry[] = [
{ base: 'Cache', path: 'a', size: 10 },
{ base: 'Cache', path: 'b', size: 20 },
{ base: 'None', path: '/Inbox/c', size: 30 },
];
expect(getCacheStats(entries)).toEqual({ count: 3, size: 60 });
});
it('returns zeros for an empty cache', () => {
expect(getCacheStats([])).toEqual({ count: 0, size: 0 });
});
});
describe('getCacheEntries', () => {
it('reads the Cache base root with base-relative paths', async () => {
const readDirectory = vi.fn().mockResolvedValue(makeFiles('x.json', 'y.epub'));
const appService = { readDirectory } as unknown as AppService;
const entries = await getCacheEntries(appService, [{ base: 'Cache', dir: '' }]);
expect(readDirectory).toHaveBeenCalledWith('', 'Cache');
expect(entries).toEqual([
{ base: 'Cache', path: 'x.json', size: 10 },
{ base: 'Cache', path: 'y.epub', size: 20 },
]);
});
it('prefixes a non-root source dir so paths are directly deletable (iOS Inbox)', async () => {
const readDirectory = vi.fn().mockResolvedValue(makeFiles('book.epub'));
const appService = { readDirectory } as unknown as AppService;
const entries = await getCacheEntries(appService, [
{ base: 'None', dir: '/var/mobile/.../Documents/Inbox' },
]);
expect(readDirectory).toHaveBeenCalledWith('/var/mobile/.../Documents/Inbox', 'None');
expect(entries).toEqual([
{ base: 'None', path: '/var/mobile/.../Documents/Inbox/book.epub', size: 10 },
]);
});
it('merges multiple sources and skips unreadable ones', async () => {
const readDirectory = vi
.fn()
.mockResolvedValueOnce(makeFiles('cache.json'))
.mockRejectedValueOnce(new Error('no inbox'));
const appService = { readDirectory } as unknown as AppService;
const entries = await getCacheEntries(appService, [
{ base: 'Cache', dir: '' },
{ base: 'None', dir: '/Inbox' },
]);
expect(entries).toEqual([{ base: 'Cache', path: 'cache.json', size: 10 }]);
});
});
describe('clearCacheEntries', () => {
it('deletes every entry with its own base and reports progress', async () => {
const entries: CacheEntry[] = [
{ base: 'Cache', path: 'a.json', size: 10 },
{ base: 'Cache', path: 'b.epub', size: 20 },
{ base: 'None', path: '/Inbox/c.epub', size: 30 },
];
const deleteFile = vi.fn().mockResolvedValue(undefined);
const appService = { deleteFile } as unknown as AppService;
const progress: CacheClearProgress[] = [];
const result = await clearCacheEntries(appService, entries, (p) => progress.push(p));
expect(result).toEqual({ deleted: 3, failed: 0 });
expect(deleteFile).toHaveBeenCalledWith('a.json', 'Cache');
expect(deleteFile).toHaveBeenCalledWith('/Inbox/c.epub', 'None');
expect(progress).toEqual([
{ current: 1, total: 3, currentFile: 'a.json' },
{ current: 2, total: 3, currentFile: 'b.epub' },
{ current: 3, total: 3, currentFile: '/Inbox/c.epub' },
]);
});
it('counts failures without aborting the loop', async () => {
const entries: CacheEntry[] = [
{ base: 'Cache', path: 'a', size: 1 },
{ base: 'Cache', path: 'b', size: 1 },
{ base: 'Cache', path: 'c', size: 1 },
];
const deleteFile = vi
.fn()
.mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(new Error('locked'))
.mockResolvedValueOnce(undefined);
const appService = { deleteFile } as unknown as AppService;
const result = await clearCacheEntries(appService, entries);
expect(result).toEqual({ deleted: 2, failed: 1 });
expect(deleteFile).toHaveBeenCalledTimes(3);
});
});