Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

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',
});
});
});