ceddee3793
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>
100 lines
2.6 KiB
TypeScript
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);
|
|
});
|
|
});
|