feat: 发布 v2.1.0 开放书源扩展
构建与发布 / 单测与集成测试 (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
构建与发布 / 发布 GitHub Release (push) Has been cancelled
构建与发布 / 单测与集成测试 (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
构建与发布 / 发布 GitHub Release (push) Has been cancelled
This commit is contained in:
@@ -57,6 +57,19 @@ app.whenReady().then(async () => {
|
||||
const configReadyMs = Date.now() - startedAt;
|
||||
check('慢速书库扫描不会阻塞窗口配置加载', configReadyMs < 1200, `${configReadyMs}ms`);
|
||||
|
||||
const sourceRows = await waitUntil(() => win.webContents.executeJavaScript(`(() => {
|
||||
const rows = Array.from(document.querySelectorAll('#sourceList input[data-id]')).map((input) => ({
|
||||
id: input.dataset.id,
|
||||
checked: input.checked,
|
||||
name: input.parentElement.querySelector('span').textContent
|
||||
}));
|
||||
return rows.length >= 16 ? rows : null;
|
||||
})()`));
|
||||
const expectedSources = ['openstax', 'opentextbook', 'wikisource-zh', 'wikisource-en'];
|
||||
check('新增开放书源出现在设置页且新安装默认启用',
|
||||
expectedSources.every((id) => sourceRows.some((row) => row.id === id && row.checked)),
|
||||
sourceRows.filter((row) => expectedSources.includes(row.id)).map((row) => `${row.id}:${row.name}`).join(', '));
|
||||
|
||||
await waitUntil(() => scanStartedAt > 0, 7000);
|
||||
check('启动维护在首屏完成后延迟执行', scanStartedAt - startedAt >= 1400,
|
||||
`${scanStartedAt - startedAt}ms`);
|
||||
|
||||
@@ -70,6 +70,12 @@ test('下载校验协议,拒绝 file:// 等非 http(s)', () => {
|
||||
assert.ok(/仅允许打开 HTTP 或 HTTPS 链接/.test(mainSrc), 'openExternal 缺协议校验');
|
||||
});
|
||||
|
||||
test('下载请求使用可识别且带项目地址的 User-Agent', () => {
|
||||
assert.match(mainSrc, /UA:\s*DL_UA[\s\S]+require\('\.\/src\/sources\/http'\)/);
|
||||
const httpSrc = fs.readFileSync(path.join(__dirname, '..', 'sources', 'http.js'), 'utf8');
|
||||
assert.match(httpSrc, /PeopleLib\/2\.1\.0 \(\+https:\/\/github\.com\/lofyer\/peoplelib\)/);
|
||||
});
|
||||
|
||||
test('移除书籍默认保留阅读资料,仅显式勾选时清理', () => {
|
||||
const start = mainSrc.indexOf("ipcMain.handle('library:remove'");
|
||||
const end = mainSrc.indexOf('// 下载文件', start);
|
||||
|
||||
+236
-1
@@ -7,7 +7,8 @@ const sources = require('../sources');
|
||||
|
||||
test('注册表:每个源都实现完整接口', () => {
|
||||
const list = sources.listSources();
|
||||
assert.ok(list.length >= 12);
|
||||
assert.ok(list.length >= 16);
|
||||
assert.strictEqual(new Set(list.map((source) => source.id)).size, list.length, '数据源 ID 不能重复');
|
||||
for (const s of list) {
|
||||
const m = sources.getSource(s.id);
|
||||
for (const fn of ['list', 'search', 'detail', 'download']) {
|
||||
@@ -17,6 +18,13 @@ test('注册表:每个源都实现完整接口', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('注册表:开放教材与中英文维基文库已启用', () => {
|
||||
const ids = sources.listSources().map((source) => source.id);
|
||||
for (const id of ['openstax', 'opentextbook', 'wikisource-zh', 'wikisource-en']) {
|
||||
assert.ok(ids.includes(id), `缺少新数据源: ${id}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('注册表:未知 id 抛错', () => {
|
||||
assert.throws(() => sources.getSource('nope'), /未知数据源/);
|
||||
});
|
||||
@@ -275,6 +283,233 @@ test('arxiv: 解析 atom feed 并取 pdf 链接', async () => {
|
||||
assert.strictEqual(d.files[0].link, 'https://arxiv.org/pdf/2201.00978v1');
|
||||
});
|
||||
|
||||
// --- OpenStax ---
|
||||
|
||||
test('openstax: 只展示 live 教材并支持本地关键词分页', async () => {
|
||||
const openstax = h.freshRequire('sources/openstax.js');
|
||||
const books = Array.from({ length: 21 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
slug: `books/book-${i + 1}`,
|
||||
book_state: 'live',
|
||||
title: i === 20 ? 'Advanced Calculus' : `Biology ${i + 1}`,
|
||||
subjects: i === 20 ? ['Math'] : ['Science'],
|
||||
subject_categories: []
|
||||
}));
|
||||
books.push({ id: 99, slug: 'books/draft', book_state: 'draft', title: 'Draft Calculus' });
|
||||
h.setHandler(h.routes([['/apps/cms/api/books', { body: { books } }]]));
|
||||
|
||||
const page2 = await openstax.list(2);
|
||||
assert.strictEqual(page2.items.length, 1);
|
||||
assert.strictEqual(page2.maxPage, 2);
|
||||
const found = await openstax.search('advanced math', 1);
|
||||
assert.deepStrictEqual(found.items.map((item) => item.postId), ['21']);
|
||||
assert.strictEqual(found.items[0].url, 'https://openstax.org/details/books/book-21');
|
||||
});
|
||||
|
||||
test('openstax: 详情解析作者、许可和日期', async () => {
|
||||
const openstax = h.freshRequire('sources/openstax.js');
|
||||
h.setHandler(h.routes([['/apps/cms/api/v2/pages/76/', {
|
||||
body: {
|
||||
id: 76,
|
||||
meta: { slug: 'calculus-volume-3', html_url: 'https://openstax.org/details/books/calculus-volume-3' },
|
||||
title: 'Calculus Volume 3',
|
||||
publish_date: '2016-03-30',
|
||||
authors: [{ value: { name: 'Gilbert Strang' } }, { name: 'Second Author' }],
|
||||
book_subjects: { subject_name: 'Math' },
|
||||
book_categories: [{ subject_name: 'Calculus' }],
|
||||
description: '<p>Open <b>calculus</b> textbook.</p>',
|
||||
license_name: 'Creative Commons Attribution-NonCommercial-ShareAlike License',
|
||||
license_version: '4.0',
|
||||
digital_isbn_13: '978-1-947172-16-6'
|
||||
}
|
||||
}]]));
|
||||
|
||||
const detail = await openstax.detail('76');
|
||||
assert.deepStrictEqual(detail.authors, ['Gilbert Strang', 'Second Author']);
|
||||
assert.strictEqual(detail.date, '2016-03-30');
|
||||
assert.strictEqual(detail.brief, 'Open calculus textbook.');
|
||||
assert.ok(detail.tags.includes('主题:Math'));
|
||||
assert.ok(detail.tags.some((tag) => tag.includes('4.0')));
|
||||
});
|
||||
|
||||
test('openstax: PDF 去重且非法 id 不发请求', async () => {
|
||||
const openstax = h.freshRequire('sources/openstax.js');
|
||||
h.resetCalls();
|
||||
h.setHandler(h.routes([['/apps/cms/api/v2/pages/76/', {
|
||||
body: {
|
||||
id: 76,
|
||||
meta: { slug: 'calculus-volume-3' },
|
||||
title: 'Calculus: Volume 3',
|
||||
pdf_url: 'https://assets.openstax.org/calculus.pdf',
|
||||
high_resolution_pdf_url: 'https://assets.openstax.org/calculus.pdf',
|
||||
license_url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/'
|
||||
}
|
||||
}]]));
|
||||
|
||||
const download = await openstax.download('76');
|
||||
assert.strictEqual(download.files.length, 1);
|
||||
assert.strictEqual(download.files[0].name, 'Calculus_ Volume 3.pdf');
|
||||
await assert.rejects(openstax.detail('../76'), /无效的 OpenStax ID/);
|
||||
assert.strictEqual(h.getCalls().length, 1);
|
||||
});
|
||||
|
||||
// --- Open Textbook Library ---
|
||||
|
||||
test('opentextbook: 搜索结果解析作者和服务端分页', async () => {
|
||||
const opentextbook = h.freshRequire('sources/opentextbook.js');
|
||||
h.setHandler(h.routes([['textbooks.json?q=calculus&page=2', {
|
||||
body: {
|
||||
data: [{
|
||||
id: 10,
|
||||
title: 'Calculus',
|
||||
copyright_year: 2023,
|
||||
contributors: [
|
||||
{ first_name: 'Gilbert', last_name: 'Strang' },
|
||||
{ corporate: true, title: 'Open Education Team' }
|
||||
],
|
||||
url: 'https://open.umn.edu/opentextbooks/textbooks/calculus'
|
||||
}],
|
||||
links: { total_pages: 10, total_count: 98 }
|
||||
}
|
||||
}]]));
|
||||
|
||||
const result = await opentextbook.search('calculus', 2);
|
||||
assert.strictEqual(result.page, 2);
|
||||
assert.strictEqual(result.maxPage, 10);
|
||||
assert.strictEqual(result.items[0].subtitle, 'Gilbert Strang, Open Education Team');
|
||||
assert.strictEqual(result.items[0].date, '2023');
|
||||
});
|
||||
|
||||
test('opentextbook: 详情展开 data 并保留单书许可', async () => {
|
||||
const opentextbook = h.freshRequire('sources/opentextbook.js');
|
||||
h.setHandler(h.routes([['textbooks/10.json', {
|
||||
body: {
|
||||
data: {
|
||||
id: 10,
|
||||
title: 'Calculus',
|
||||
edition_statement: 'Third Edition',
|
||||
copyright_year: 1991,
|
||||
license: 'Attribution-NonCommercial-ShareAlike',
|
||||
language: 'eng',
|
||||
description: '<p>Free <b>calculus</b> textbook.</p>',
|
||||
contributors: [{ first_name: 'Gilbert', last_name: 'Strang' }],
|
||||
subjects: [{ name: 'Mathematics' }],
|
||||
url: 'https://open.umn.edu/opentextbooks/textbooks/calculus'
|
||||
}
|
||||
}
|
||||
}]]));
|
||||
|
||||
const detail = await opentextbook.detail('10');
|
||||
assert.deepStrictEqual(detail.authors, ['Gilbert Strang']);
|
||||
assert.strictEqual(detail.brief, 'Free calculus textbook.');
|
||||
assert.ok(detail.tags.includes('版本:Third Edition'));
|
||||
assert.ok(detail.tags.includes('许可:Attribution-NonCommercial-ShareAlike'));
|
||||
});
|
||||
|
||||
test('opentextbook: 只有真实文件 URL 才进入下载列表', async () => {
|
||||
const opentextbook = h.freshRequire('sources/opentextbook.js');
|
||||
h.resetCalls();
|
||||
h.setHandler(h.routes([['textbooks/10.json', {
|
||||
body: {
|
||||
data: {
|
||||
id: 10,
|
||||
title: 'Calculus: Third Edition',
|
||||
url: 'https://open.umn.edu/opentextbooks/textbooks/calculus',
|
||||
formats: [
|
||||
{ type: 'PDF', url: 'https://ocw.mit.edu/courses/calculus/open-textbook/' },
|
||||
{ type: 'PDF', url: 'https://cdn.example/calculus.pdf?download=1' },
|
||||
{ type: 'EPUB', url: 'https://cdn.example/calculus.epub' },
|
||||
{ type: 'EPUB', url: 'https://cdn.example/calculus.epub' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}]]));
|
||||
|
||||
const download = await opentextbook.download('10');
|
||||
assert.deepStrictEqual(download.files.map((file) => file.format), ['PDF', 'EPUB']);
|
||||
assert.strictEqual(download.links[1].name, 'PDF 获取页');
|
||||
assert.strictEqual(download.files[0].name, 'Calculus_ Third Edition.pdf');
|
||||
await assert.rejects(opentextbook.download('10/../../x'), /无效的开放教材 ID/);
|
||||
assert.strictEqual(h.getCalls().length, 1);
|
||||
});
|
||||
|
||||
// --- Wikisource ---
|
||||
|
||||
test('wikisource: 搜索使用整数偏移并携带可识别 User-Agent', async () => {
|
||||
const wikisource = h.freshRequire('sources/wikisource-zh.js');
|
||||
let request = null;
|
||||
h.setHandler((url, options) => {
|
||||
request = { url, options };
|
||||
return h.makeResponse({
|
||||
body: {
|
||||
query: {
|
||||
searchinfo: { totalhits: 24753 },
|
||||
search: [{ pageid: 6, title: '論語' }]
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const result = await wikisource.search('論語', 2);
|
||||
assert.strictEqual(result.items[0].postId, '6');
|
||||
assert.strictEqual(result.items[0].subtitle, '中文');
|
||||
assert.strictEqual(result.maxPage, 500, 'MediaWiki 搜索最多允许偏移到 10000 条');
|
||||
assert.ok(request.url.includes('sroffset=20'), request.url);
|
||||
assert.match(request.options.headers['User-Agent'], /PeopleLib\/2\.1\.0/);
|
||||
});
|
||||
|
||||
test('wikisource: 浏览按 continuation 令牌翻页', async () => {
|
||||
const wikisource = h.freshRequire('sources/wikisource-en.js');
|
||||
const urls = [];
|
||||
h.setHandler((url) => {
|
||||
urls.push(url);
|
||||
if (url.includes('apcontinue=')) {
|
||||
return h.makeResponse({ body: { query: { allpages: [{ pageid: 2, title: 'Second Book' }] } } });
|
||||
}
|
||||
return h.makeResponse({
|
||||
body: {
|
||||
continue: { apcontinue: 'Second Book', continue: '-||' },
|
||||
query: { allpages: [{ pageid: 1, title: 'First Book' }] }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const first = await wikisource.list(1);
|
||||
const second = await wikisource.list(2);
|
||||
assert.strictEqual(first.maxPage, 2);
|
||||
assert.strictEqual(second.items[0].postId, '2');
|
||||
assert.ok(urls[1].includes('apcontinue=Second+Book'), urls[1]);
|
||||
});
|
||||
|
||||
test('wikisource: 详情和导出链接由服务端标题生成', async () => {
|
||||
const wikisource = h.freshRequire('sources/wikisource-zh.js');
|
||||
h.resetCalls();
|
||||
h.setHandler(h.routes([['pageids=6', {
|
||||
body: {
|
||||
query: {
|
||||
pages: [{
|
||||
pageid: 6,
|
||||
title: '論語/學而第一',
|
||||
extract: '<p>學而時習之。</p>',
|
||||
fullurl: 'https://zh.wikisource.org/wiki/%E8%AB%96%E8%AA%9E',
|
||||
thumbnail: { source: 'https://upload.wikimedia.org/cover.jpg' }
|
||||
}]
|
||||
}
|
||||
}
|
||||
}]]));
|
||||
|
||||
const detail = await wikisource.detail('6');
|
||||
assert.strictEqual(detail.brief, '學而時習之。');
|
||||
assert.strictEqual(detail.cover, 'https://upload.wikimedia.org/cover.jpg');
|
||||
const download = await wikisource.download('6');
|
||||
assert.deepStrictEqual(download.files.map((file) => file.format), ['EPUB', 'PDF']);
|
||||
assert.ok(download.files[0].link.includes('lang=zh'));
|
||||
assert.ok(download.files[0].link.includes('page=%E8%AB%96%E8%AA%9E%2F%E5%AD%B8%E8%80%8C%E7%AC%AC%E4%B8%80'));
|
||||
assert.strictEqual(download.files[0].name, '論語_學而第一.epub');
|
||||
await assert.rejects(wikisource.detail('../6'), /无效的中文维基文库 ID/);
|
||||
assert.strictEqual(h.getCalls().length, 2);
|
||||
});
|
||||
|
||||
// --- Z-Library ---
|
||||
|
||||
test('zlib: postId 缺 hash 时详情仍可用', async () => {
|
||||
|
||||
Reference in New Issue
Block a user