9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
399 lines
17 KiB
JavaScript
399 lines
17 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { CliError } from '@jackwener/opencli/errors';
|
|
const { mockFetchPrivateApi } = vi.hoisted(() => ({
|
|
mockFetchPrivateApi: vi.fn(),
|
|
}));
|
|
vi.mock('./utils.js', async () => {
|
|
const actual = await vi.importActual('./utils.js');
|
|
return {
|
|
...actual,
|
|
fetchPrivateApi: mockFetchPrivateApi,
|
|
};
|
|
});
|
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
import './book.js';
|
|
import './highlights.js';
|
|
import './notes.js';
|
|
import { extractReaderFallbackMetadata, strictTitleFromWereadDocumentTitle } from './book.js';
|
|
describe('weread book-id positional args', () => {
|
|
const book = getRegistry().get('weread/book');
|
|
const highlights = getRegistry().get('weread/highlights');
|
|
const notes = getRegistry().get('weread/notes');
|
|
const repeatValue = (value, count) => Array.from({ length: count }, () => value);
|
|
const createPageStub = (...evaluateResults) => ({
|
|
getCookies: vi.fn().mockResolvedValue([
|
|
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
]),
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
wait: vi.fn().mockResolvedValue(undefined),
|
|
evaluate: vi.fn().mockImplementation(async () => evaluateResults.shift()),
|
|
});
|
|
beforeEach(() => {
|
|
mockFetchPrivateApi.mockReset();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
it('passes the positional book-id to book details', async () => {
|
|
mockFetchPrivateApi.mockResolvedValue({ title: 'Three Body', newRating: 880 });
|
|
await book.func({}, { 'book-id': '12345' });
|
|
expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/book/info', { bookId: '12345' });
|
|
});
|
|
it('falls back to the shelf reader page when private API auth has expired', async () => {
|
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
const page = createPageStub({
|
|
cacheFound: true,
|
|
rawBooks: [
|
|
{ bookId: 'MP_WXS_3634777637', title: '文明、现代化、价值投资与中国', author: '李录' },
|
|
],
|
|
shelfIndexes: [
|
|
{ bookId: 'MP_WXS_3634777637', idx: 0, role: 'book' },
|
|
],
|
|
}, ['https://weread.qq.com/web/reader/6f5323f071bd7f7b6f521e8'], {
|
|
title: '文明、现代化、价值投资与中国',
|
|
author: '李录',
|
|
publisher: '中信出版集团',
|
|
intro: '对中国未来几十年的预测。',
|
|
category: '',
|
|
rating: '84.1%',
|
|
metadataReady: true,
|
|
});
|
|
const result = await book.func(page, { 'book-id': 'MP_WXS_3634777637' });
|
|
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/6f5323f071bd7f7b6f521e8');
|
|
expect(page.evaluate).toHaveBeenCalledTimes(3);
|
|
expect(result).toEqual([
|
|
{
|
|
title: '文明、现代化、价值投资与中国',
|
|
author: '李录',
|
|
publisher: '中信出版集团',
|
|
intro: '对中国未来几十年的预测。',
|
|
category: '',
|
|
rating: '84.1%',
|
|
},
|
|
]);
|
|
});
|
|
it('keeps mixed shelf entries aligned when resolving MP_WXS reader urls', async () => {
|
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
const page = createPageStub({
|
|
cacheFound: true,
|
|
rawBooks: [
|
|
{ bookId: 'MP_WXS_1', title: '公众号文章一', author: '作者甲' },
|
|
{ bookId: 'BOOK_2', title: '普通书二', author: '作者乙' },
|
|
{ bookId: 'MP_WXS_3', title: '公众号文章三', author: '作者丙' },
|
|
],
|
|
shelfIndexes: [
|
|
{ bookId: 'MP_WXS_1', idx: 0, role: 'mp' },
|
|
{ bookId: 'BOOK_2', idx: 1, role: 'book' },
|
|
{ bookId: 'MP_WXS_3', idx: 2, role: 'mp' },
|
|
],
|
|
}, [
|
|
'https://weread.qq.com/web/reader/mp1',
|
|
'https://weread.qq.com/web/reader/book2',
|
|
'https://weread.qq.com/web/reader/mp3',
|
|
], {
|
|
title: '公众号文章一',
|
|
author: '作者甲',
|
|
publisher: '微信读书',
|
|
intro: '第一篇文章。',
|
|
category: '',
|
|
rating: '',
|
|
metadataReady: true,
|
|
});
|
|
const result = await book.func(page, { 'book-id': 'MP_WXS_1' });
|
|
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/mp1');
|
|
expect(result).toEqual([
|
|
{
|
|
title: '公众号文章一',
|
|
author: '作者甲',
|
|
publisher: '微信读书',
|
|
intro: '第一篇文章。',
|
|
category: '',
|
|
rating: '',
|
|
},
|
|
]);
|
|
});
|
|
it('rethrows AUTH_REQUIRED when shelf ordering is incomplete and reader urls cannot be trusted', async () => {
|
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network disabled')));
|
|
const page = createPageStub({
|
|
cacheFound: true,
|
|
rawBooks: [
|
|
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
{ bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
|
|
],
|
|
shelfIndexes: [
|
|
{ bookId: 'BOOK_2', idx: 0, role: 'book' },
|
|
],
|
|
}, [
|
|
'https://weread.qq.com/web/reader/book2',
|
|
'https://weread.qq.com/web/reader/book1',
|
|
]);
|
|
await expect(book.func(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
|
|
code: 'AUTH_REQUIRED',
|
|
message: 'Not logged in to WeRead',
|
|
});
|
|
expect(page.goto).toHaveBeenCalledTimes(1);
|
|
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
});
|
|
it('falls back to the public search page when a cached ordinary book has no trusted shelf reader url', async () => {
|
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
const fetchMock = vi.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
books: [
|
|
{
|
|
bookInfo: {
|
|
title: '数据化运营:系统方法与实践案例',
|
|
author: '赵宏田 江丽萍 李宁',
|
|
bookId: '22920382',
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
text: () => Promise.resolve(`
|
|
<ul class="search_bookDetail_list">
|
|
<li class="wr_bookList_item">
|
|
<a class="wr_bookList_item_link" href="/web/reader/book229"></a>
|
|
<p class="wr_bookList_item_title">数据化运营:系统方法与实践案例</p>
|
|
<p class="wr_bookList_item_author">赵宏田 江丽萍 李宁</p>
|
|
</li>
|
|
</ul>
|
|
`),
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
const staleSnapshot = {
|
|
cacheFound: true,
|
|
rawBooks: [
|
|
{ bookId: '22920382', title: '数据化运营:系统方法与实践案例', author: '赵宏田 江丽萍 李宁' },
|
|
],
|
|
shelfIndexes: [
|
|
{ bookId: 'stale-entry', idx: 0, role: 'book' },
|
|
],
|
|
};
|
|
const page = createPageStub(...repeatValue(staleSnapshot, 2), {
|
|
title: '数据化运营:系统方法与实践案例',
|
|
author: '赵宏田 江丽萍 李宁',
|
|
publisher: '电子工业出版社',
|
|
intro: '一本关于数据化运营的方法论书籍。',
|
|
category: '',
|
|
rating: '',
|
|
metadataReady: true,
|
|
});
|
|
const result = await book.func(page, { 'book-id': '22920382' });
|
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
expect(String(fetchMock.mock.calls[0][0])).toContain('/web/search/global?keyword=');
|
|
expect(String(fetchMock.mock.calls[1][0])).toContain('/web/search/books?keyword=');
|
|
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/book229');
|
|
expect(result).toEqual([
|
|
{
|
|
title: '数据化运营:系统方法与实践案例',
|
|
author: '赵宏田 江丽萍 李宁',
|
|
publisher: '电子工业出版社',
|
|
intro: '一本关于数据化运营的方法论书籍。',
|
|
category: '',
|
|
rating: '',
|
|
},
|
|
]);
|
|
});
|
|
it('rethrows AUTH_REQUIRED when search fallback finds the same title with a different visible author', async () => {
|
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
const fetchMock = vi.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
books: [
|
|
{
|
|
bookInfo: {
|
|
title: '文明',
|
|
author: '作者乙',
|
|
bookId: 'wrong-book',
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
text: () => Promise.resolve(`
|
|
<ul class="search_bookDetail_list">
|
|
<li class="wr_bookList_item">
|
|
<a class="wr_bookList_item_link" href="/web/reader/wrong-reader"></a>
|
|
<p class="wr_bookList_item_title">文明</p>
|
|
<p class="wr_bookList_item_author">作者乙</p>
|
|
</li>
|
|
</ul>
|
|
`),
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
const staleSnapshot = {
|
|
cacheFound: true,
|
|
rawBooks: [
|
|
{ bookId: 'BOOK_1', title: '文明', author: '作者甲' },
|
|
],
|
|
shelfIndexes: [
|
|
{ bookId: 'stale-entry', idx: 0, role: 'book' },
|
|
],
|
|
};
|
|
const page = createPageStub(...repeatValue(staleSnapshot, 2));
|
|
await expect(book.func(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
|
|
code: 'AUTH_REQUIRED',
|
|
message: 'Not logged in to WeRead',
|
|
});
|
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
});
|
|
it('falls back to raw cache order when shelf indexes never hydrate but rendered reader urls cover every cached entry', async () => {
|
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network disabled')));
|
|
const emptyIndexSnapshot = {
|
|
cacheFound: true,
|
|
rawBooks: [
|
|
{ bookId: '22920382', title: '数据化运营:系统方法与实践案例', author: '赵宏田 江丽萍 李宁' },
|
|
{ bookId: 'MP_WXS_3634777637', title: '方伟看10年', author: '公众号' },
|
|
],
|
|
shelfIndexes: [],
|
|
};
|
|
const page = createPageStub(...repeatValue(emptyIndexSnapshot, 2), [
|
|
'https://weread.qq.com/web/reader/book229',
|
|
'https://weread.qq.com/web/reader/mp3634',
|
|
], {
|
|
title: '方伟看10年',
|
|
author: '公众号',
|
|
publisher: '',
|
|
intro: '公众号文章详情。',
|
|
category: '',
|
|
rating: '',
|
|
metadataReady: true,
|
|
});
|
|
const result = await book.func(page, { 'book-id': 'MP_WXS_3634777637' });
|
|
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/mp3634');
|
|
expect(result).toEqual([
|
|
{
|
|
title: '方伟看10年',
|
|
author: '公众号',
|
|
publisher: '',
|
|
intro: '公众号文章详情。',
|
|
category: '',
|
|
rating: '',
|
|
},
|
|
]);
|
|
});
|
|
it('waits for shelf indexes to hydrate before resolving a trusted reader url', async () => {
|
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
const page = createPageStub({
|
|
cacheFound: true,
|
|
rawBooks: [
|
|
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
{ bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
|
|
],
|
|
shelfIndexes: [
|
|
{ bookId: 'BOOK_2', idx: 0, role: 'book' },
|
|
],
|
|
}, {
|
|
cacheFound: true,
|
|
rawBooks: [
|
|
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
{ bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
|
|
],
|
|
shelfIndexes: [
|
|
{ bookId: 'BOOK_2', idx: 0, role: 'book' },
|
|
{ bookId: 'BOOK_1', idx: 1, role: 'book' },
|
|
],
|
|
}, [
|
|
'https://weread.qq.com/web/reader/book2',
|
|
'https://weread.qq.com/web/reader/book1',
|
|
], {
|
|
title: '第一本',
|
|
author: '作者甲',
|
|
publisher: '出版社甲',
|
|
intro: '简介甲',
|
|
category: '',
|
|
rating: '',
|
|
metadataReady: true,
|
|
});
|
|
const result = await book.func(page, { 'book-id': 'BOOK_1' });
|
|
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/book1');
|
|
expect(result).toEqual([
|
|
{
|
|
title: '第一本',
|
|
author: '作者甲',
|
|
publisher: '出版社甲',
|
|
intro: '简介甲',
|
|
category: '',
|
|
rating: '',
|
|
},
|
|
]);
|
|
});
|
|
it('rethrows AUTH_REQUIRED when the reader page lacks stable cover metadata', async () => {
|
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
const page = createPageStub({
|
|
cacheFound: true,
|
|
rawBooks: [
|
|
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
],
|
|
shelfIndexes: [
|
|
{ bookId: 'BOOK_1', idx: 0, role: 'book' },
|
|
],
|
|
}, [
|
|
'https://weread.qq.com/web/reader/book1',
|
|
], {
|
|
title: '',
|
|
author: '',
|
|
publisher: '',
|
|
intro: '这是正文第一段,不应该被当成简介。',
|
|
category: '',
|
|
rating: '',
|
|
metadataReady: false,
|
|
});
|
|
await expect(book.func(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
|
|
code: 'AUTH_REQUIRED',
|
|
message: 'Not logged in to WeRead',
|
|
});
|
|
});
|
|
it('does not guess author from document.title when the reader page skips cover metadata', async () => {
|
|
const nodes = new Map([
|
|
['.readerTopBar_title_link', { textContent: 'Part 1 - Part 2' }],
|
|
['.introDialog_content_pub_line', { textContent: '出版社 测试出版社' }],
|
|
['.introDialog_content_intro_para', { textContent: '测试简介。' }],
|
|
]);
|
|
const mockDocument = {
|
|
title: 'Part 1 - Part 2 - 作者甲 - 微信读书',
|
|
body: { innerText: '微信读书推荐值 88.8%' },
|
|
scripts: [],
|
|
querySelector: (selector) => nodes.get(selector) || null,
|
|
};
|
|
expect(strictTitleFromWereadDocumentTitle(mockDocument.title)).toBe('');
|
|
expect(extractReaderFallbackMetadata(mockDocument)).toEqual({
|
|
title: 'Part 1 - Part 2',
|
|
author: '',
|
|
publisher: '测试出版社',
|
|
intro: '测试简介。',
|
|
category: '',
|
|
rating: '88.8%',
|
|
metadataReady: true,
|
|
});
|
|
});
|
|
it('passes the positional book-id to highlights', async () => {
|
|
mockFetchPrivateApi.mockResolvedValue({ updated: [] });
|
|
await highlights.func({}, { 'book-id': 'abc', limit: 5 });
|
|
expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/book/bookmarklist', { bookId: 'abc' });
|
|
});
|
|
it('passes the positional book-id to notes', async () => {
|
|
mockFetchPrivateApi.mockResolvedValue({ reviews: [] });
|
|
await notes.func({}, { 'book-id': 'xyz', limit: 5 });
|
|
expect(mockFetchPrivateApi).toHaveBeenCalledWith({}, '/review/list', {
|
|
bookId: 'xyz',
|
|
listType: '11',
|
|
mine: '1',
|
|
synckey: '0',
|
|
});
|
|
});
|
|
});
|