Files
reader/web/test/reader-speech-voices.test.ts
hectorqinandClaude Opus 5.5 3b1f8b46dc feat: 时长统计 — reading, watching and listening time by day, month, year and work
Reading time is counted by the reader: only while the page is visible and the
reader has turned, scrolled or tapped within three minutes, or while 朗读 plays.
It is kept per book per 15-minute UTC slot and per device install, saved
locally until sent, and stored as MAX(stored, reported) in the new
reading_time table, so a replayed report never counts twice.

Media time is derived on the server from the progress saves the web player and
Android Media3 already send: the smaller of the wall-clock gap and the position
advance between two saves of one session, gaps over 30s ignored. It is stored
in media.db (media_watch_time), so neither client changes.

我的 → 时长统计 shows a stacked bar per day (one month), month (one year) or
year, with a legend that filters by kind, a table view and the works ranked for
the range or the bar picked. Administrators also get every account's totals.
Chart colours are new mode-level tokens (--color-chart-1..4), validated for
colour-vision separation on both surfaces. The React UI review now covers the
page and names the overflowing elements when a screen overflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:55:52 +00:00

17 lines
755 B
TypeScript

// @vitest-environment jsdom
import { expect, it, vi } from 'vitest';
import { ReaderScreen } from '../src/ui/reader-screen.tsx';
it('publishes asynchronous voice changes without reopening the existing settings panel', () => {
const context = {
voices: [], lastSpeechQueue: [], chrome: { tts: {} }, speechQueueIndex: () => 0,
patch: vi.fn(), setStatus: vi.fn(), view: null, readingTime: { setSpeaking: vi.fn() },
};
const voice = { id: 'zh', name: '中文', lang: 'zh-CN', default: true };
Reflect.apply(ReaderScreen.prototype['renderSpeechState'], context, [{
state: 'idle', voices: [voice], total: 0, index: -1, chunk: '', error: '',
}]);
expect(context.voices).toEqual([voice]);
expect(context.patch).toHaveBeenCalled();
});