Files
readest/apps/readest-app/src/__tests__/app/library/book-context-menu.test.ts
T
Huang Xin ceddee3793 feat(library): search a book on Goodreads from the library and reader (#4543) (#4548)
Adds a quick "Search on Goodreads" action so readers can jump straight to
Goodreads to track a book instead of retyping the title there.

- Library: a Goodreads button in the Book Details view (works on web,
  desktop and mobile) searching the book's title + author, plus a
  "Search on Goodreads" item in the desktop right-click context menu.
- Reader: Goodreads is added as a built-in web-search provider so
  highlighted text (e.g. a short-story title inside a magazine) can be
  looked up on Goodreads. Disabled by default like the other built-ins;
  enable it in Settings -> Dictionaries.

Both surfaces are used because the native context menu is desktop-only;
the Book Details button covers web and mobile. Adds a shared
openExternalUrl() helper and translates "Search on Goodreads" across all
locales (the Goodreads brand name is kept verbatim).

Closes #4543

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 08:30:17 +02:00

100 lines
2.6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { getBookContextMenuItemIds } from '@/app/library/utils/libraryUtils';
import { Book } from '@/types/book';
const createBook = (overrides: Partial<Book> = {}): Book => ({
hash: 'hash-1',
format: 'EPUB',
title: 'Test Book',
author: 'Test Author',
createdAt: 0,
updatedAt: 0,
...overrides,
});
describe('getBookContextMenuItemIds', () => {
it('returns a deterministic order for a local downloaded book', () => {
const book = createBook({ downloadedAt: 1 });
expect(getBookContextMenuItemIds(book)).toEqual([
'select',
'group',
'markFinished',
'showDetails',
'showInFinder',
'searchGoodreads',
'upload',
'share',
'delete',
]);
});
it('shows "Mark as Unread" + "Clear Status" for a finished book', () => {
const book = createBook({ downloadedAt: 1, readingStatus: 'finished' });
expect(getBookContextMenuItemIds(book)).toEqual([
'select',
'group',
'markUnread',
'clearStatus',
'showDetails',
'showInFinder',
'searchGoodreads',
'upload',
'share',
'delete',
]);
});
it('shows "Mark as Finished" + "Clear Status" for an unread book', () => {
const book = createBook({ downloadedAt: 1, readingStatus: 'unread' });
expect(getBookContextMenuItemIds(book)).toEqual([
'select',
'group',
'markFinished',
'clearStatus',
'showDetails',
'showInFinder',
'searchGoodreads',
'upload',
'share',
'delete',
]);
});
it('offers Download (not Upload) for a cloud-only book', () => {
const book = createBook({ uploadedAt: 1 });
expect(getBookContextMenuItemIds(book)).toEqual([
'select',
'group',
'markFinished',
'showDetails',
'showInFinder',
'searchGoodreads',
'download',
'share',
'delete',
]);
});
it('omits download/upload/share for a book that is neither downloaded nor uploaded', () => {
const book = createBook({ filePath: '/some/external/file.epub' });
expect(getBookContextMenuItemIds(book)).toEqual([
'select',
'group',
'markFinished',
'showDetails',
'showInFinder',
'searchGoodreads',
'delete',
]);
});
it('produces the same order on repeated calls and never duplicates an item (issue #4389)', () => {
const book = createBook({ downloadedAt: 1, uploadedAt: 1, readingStatus: 'finished' });
const first = getBookContextMenuItemIds(book);
const second = getBookContextMenuItemIds(book);
expect(second).toEqual(first);
expect(new Set(first).size).toBe(first.length);
});
});