2031 lines
84 KiB
JavaScript
2031 lines
84 KiB
JavaScript
const { app, BrowserWindow, safeStorage, dialog } = require('electron');
|
||
const fs = require('fs');
|
||
const os = require('os');
|
||
const path = require('path');
|
||
|
||
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
||
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'peoplelib-library-notes-ui-'));
|
||
const LIBRARY_DIR = path.join(TMP, 'library');
|
||
const FIXTURE_FILE = path.join(TMP, 'retained-book.txt');
|
||
const FIXTURE_PDF = path.join(TMP, 'retained-book.pdf');
|
||
|
||
function makePdf(file) {
|
||
const objects = [
|
||
'',
|
||
'<< /Type /Catalog /Pages 2 0 R >>',
|
||
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
|
||
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 400] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>',
|
||
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>'
|
||
];
|
||
const stream = 'BT /F1 16 Tf 40 320 Td (Retained Research Book) Tj ET';
|
||
objects.push(`<< /Length ${Buffer.byteLength(stream)} >>\nstream\n${stream}\nendstream`);
|
||
let body = '%PDF-1.4\n';
|
||
const offsets = [0];
|
||
for (let i = 1; i < objects.length; i++) {
|
||
offsets[i] = Buffer.byteLength(body);
|
||
body += `${i} 0 obj\n${objects[i]}\nendobj\n`;
|
||
}
|
||
const xref = Buffer.byteLength(body);
|
||
body += `xref\n0 ${objects.length}\n0000000000 65535 f \n`;
|
||
for (let i = 1; i < objects.length; i++) {
|
||
body += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`;
|
||
}
|
||
body += `trailer\n<< /Size ${objects.length} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
|
||
fs.writeFileSync(file, body);
|
||
}
|
||
|
||
// main.js derives its development profile from appData rather than userData.
|
||
// Redirect both before requiring it so even its initial side effects stay isolated.
|
||
app.setPath('appData', TMP);
|
||
app.setPath('userData', TMP);
|
||
fs.writeFileSync(FIXTURE_FILE, 'PeopleLib library and notes integration fixture.\n');
|
||
makePdf(FIXTURE_PDF);
|
||
|
||
const results = [];
|
||
const rendererErrors = [];
|
||
let win = null;
|
||
let coverGenerator = null;
|
||
|
||
function check(name, condition, detail = '') {
|
||
results.push([condition ? 'OK' : 'FAIL', name, detail]);
|
||
}
|
||
|
||
function wait(ms) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
async function js(source) {
|
||
if (!win || win.isDestroyed()) throw new Error('主窗口不可用');
|
||
return win.webContents.executeJavaScript(source);
|
||
}
|
||
|
||
// soft=true 时超时不抛也不记失败,只返回 false,交给调用方自己断言,
|
||
// 这样失败信息里能带上真实量到的状态而不是一句"等待超时"
|
||
async function poll(name, predicate, timeout = 8000, soft = false) {
|
||
const deadline = Date.now() + timeout;
|
||
let lastError = null;
|
||
while (Date.now() < deadline) {
|
||
try {
|
||
if (await predicate()) return true;
|
||
} catch (error) {
|
||
lastError = error;
|
||
}
|
||
await wait(50);
|
||
}
|
||
if (soft) return false;
|
||
const detail = lastError ? lastError.message : '等待超时';
|
||
check(name, false, detail);
|
||
throw new Error(`${name}: ${detail}`);
|
||
}
|
||
|
||
async function pollJs(name, source, timeout) {
|
||
return poll(name, () => js(source), timeout);
|
||
}
|
||
|
||
async function waitForModal(title) {
|
||
await pollJs(
|
||
`弹窗显示:${title}`,
|
||
`(() => {
|
||
const modal = document.getElementById('modal');
|
||
return modal && !modal.classList.contains('hidden')
|
||
&& document.getElementById('modalTitle').textContent === ${JSON.stringify(title)};
|
||
})()`
|
||
);
|
||
}
|
||
|
||
async function submitModal() {
|
||
await js("document.getElementById('modalOk').click()");
|
||
await pollJs(
|
||
'弹窗提交完成',
|
||
"document.getElementById('modal').classList.contains('hidden')"
|
||
);
|
||
}
|
||
|
||
async function libraryTitles() {
|
||
return js(`Array.from(document.querySelectorAll('#libGrid .card .card-title'))
|
||
.map((element) => element.textContent.trim())`);
|
||
}
|
||
|
||
async function noteCards() {
|
||
return js(`Array.from(document.querySelectorAll('#notesList .note-card')).map((card) => ({
|
||
book: (card.querySelector('.note-book-title') || {}).textContent || '',
|
||
title: (card.querySelector('.note-title') || {}).textContent || '',
|
||
text: (card.querySelector('.note-text') || {}).textContent || '',
|
||
quote: (card.querySelector('.note-quote') || {}).textContent || '',
|
||
source: (card.querySelector('.note-badge.source') || {}).textContent || '',
|
||
type: (card.querySelector('.note-badge.type') || {}).textContent || '',
|
||
pinned: card.classList.contains('pinned'),
|
||
tags: Array.from(card.querySelectorAll('.note-tags .note-tag')).map((tag) => tag.textContent),
|
||
meta: (card.querySelector('.note-meta') || {}).textContent || ''
|
||
}))`);
|
||
}
|
||
|
||
function finish() {
|
||
console.log('\n========== 书库整理与我的笔记集成验证 ==========');
|
||
for (const [status, name, detail] of results) {
|
||
console.log(`${status.padEnd(5)} ${name}${detail ? ` [${detail}]` : ''}`);
|
||
}
|
||
const failed = results.filter((result) => result[0] === 'FAIL').length;
|
||
console.log(`\n通过 ${results.length - failed}/${results.length}`);
|
||
|
||
try {
|
||
if (coverGenerator) coverGenerator.close();
|
||
} catch (error) {
|
||
console.error('关闭封面任务失败:', error.message);
|
||
}
|
||
for (const window of BrowserWindow.getAllWindows()) {
|
||
try {
|
||
if (!window.isDestroyed()) window.destroy();
|
||
} catch (error) {
|
||
console.error('关闭窗口失败:', error.message);
|
||
}
|
||
}
|
||
app.exit(failed ? 1 : 0);
|
||
}
|
||
|
||
app.on('browser-window-created', (_event, window) => {
|
||
window.webContents.on('console-message', (consoleEvent) => {
|
||
const { level, message, lineNumber, sourceId } = consoleEvent;
|
||
if (level < 2 || /Autofill|Indexing all PDF objects/i.test(message)) return;
|
||
const detail = `${message}${sourceId ? ` (${sourceId}:${lineNumber || 0})` : ''}`;
|
||
rendererErrors.push(detail);
|
||
console.error('RENDERER:', detail);
|
||
});
|
||
});
|
||
|
||
app.whenReady().then(async () => {
|
||
require(path.join(ROOT, 'main.js'));
|
||
|
||
// main.js initializes singleton stores as a require-time side effect. Point
|
||
// every profile-backed singleton back at this test's isolated directory.
|
||
const settings = require(path.join(ROOT, 'src', 'settings'));
|
||
const readerStore = require(path.join(ROOT, 'src', 'reader', 'store'));
|
||
const annotations = require(path.join(ROOT, 'src', 'reader', 'annotations'));
|
||
const noteAssets = require(path.join(ROOT, 'src', 'reader', 'note-assets'));
|
||
const noteWindow = require(path.join(ROOT, 'src', 'reader', 'note-window'));
|
||
const aiConfig = require(path.join(ROOT, 'src', 'reader', 'ai-config'));
|
||
const zlibAuth = require(path.join(ROOT, 'src', 'sources', 'zlib-auth'));
|
||
const semanticKey = require(path.join(ROOT, 'src', 'sources', 'semantic-key'));
|
||
const library = require(path.join(ROOT, 'src', 'library', 'store'));
|
||
coverGenerator = require(path.join(ROOT, 'src', 'library', 'cover-generator'));
|
||
|
||
settings.init(TMP);
|
||
readerStore.init(TMP);
|
||
annotations.init(TMP);
|
||
noteAssets.init(TMP);
|
||
aiConfig.init(TMP, safeStorage);
|
||
zlibAuth.init(TMP, safeStorage);
|
||
semanticKey.init(TMP, safeStorage);
|
||
library.init(LIBRARY_DIR);
|
||
require(path.join(ROOT, 'src', 'sources', 'http')).setProxy('');
|
||
|
||
check(
|
||
'所有配置和书库数据使用隔离目录',
|
||
app.getPath('appData') === TMP
|
||
&& app.getPath('userData').startsWith(TMP)
|
||
&& library.getRoot() === LIBRARY_DIR,
|
||
TMP
|
||
);
|
||
|
||
const researchShelf = library.addShelf({ name: '研究书架' });
|
||
const reviewShelf = library.addShelf({ name: '待复核书架' });
|
||
const retainedBook = library.add({
|
||
title: 'Retained Research Book',
|
||
authors: ['Alice Author'],
|
||
shelfId: researchShelf.id,
|
||
tags: ['Methods', 'Shared'],
|
||
files: [
|
||
{ path: FIXTURE_FILE, name: 'retained-book.txt', format: 'TXT' },
|
||
{ path: FIXTURE_PDF, name: 'retained-book.pdf', format: 'PDF' }
|
||
]
|
||
});
|
||
const selectionBook = library.add({
|
||
title: 'Selection Source Book',
|
||
authors: ['Bob Author'],
|
||
shelfId: reviewShelf.id,
|
||
tags: ['Shared', 'Review'],
|
||
files: []
|
||
});
|
||
const purgeBook = library.add({
|
||
title: 'Purge Reading Data Book',
|
||
authors: ['Carol Author'],
|
||
shelfId: null,
|
||
tags: ['Archive'],
|
||
files: []
|
||
});
|
||
|
||
const notebook = readerStore.addCollection({ name: '研究笔记本' });
|
||
readerStore.setBookSnapshot(retainedBook.id, {
|
||
title: retainedBook.title,
|
||
authors: retainedBook.authors
|
||
});
|
||
const manualNote = readerStore.addNote(retainedBook.id, {
|
||
title: 'Pinned Research Note',
|
||
text: 'Alpha insight searchable body',
|
||
quote: '',
|
||
context: 'Manual note context',
|
||
source: 'manual',
|
||
locator: null,
|
||
documentKey: null,
|
||
fileIndex: null,
|
||
collectionId: notebook.id,
|
||
tags: ['focus', 'shared-note'],
|
||
pinned: true
|
||
});
|
||
readerStore.setBookSnapshot(selectionBook.id, {
|
||
title: selectionBook.title,
|
||
authors: selectionBook.authors
|
||
});
|
||
const selectionNote = readerStore.addNote(selectionBook.id, {
|
||
title: 'Selected Passage',
|
||
text: 'Selection commentary body',
|
||
quote: 'Quoted selection excerpt',
|
||
context: 'Around selection context',
|
||
source: 'selection',
|
||
locator: { type: 'pdf', page: 3 },
|
||
documentKey: 'fixture-document-key',
|
||
fileIndex: 0,
|
||
collectionId: null,
|
||
tags: ['excerpt'],
|
||
pinned: false
|
||
});
|
||
readerStore.setBookSnapshot(purgeBook.id, {
|
||
title: purgeBook.title,
|
||
authors: purgeBook.authors
|
||
});
|
||
const purgeNote = readerStore.addNote(purgeBook.id, {
|
||
title: 'Purge With Reading Data',
|
||
text: 'This note must disappear with explicit reading-data deletion',
|
||
source: 'manual',
|
||
locator: null,
|
||
collectionId: null,
|
||
tags: ['purge-note'],
|
||
pinned: false
|
||
});
|
||
const realNow = Date.now;
|
||
try {
|
||
Date.now = () => 1000;
|
||
readerStore.setProgress(retainedBook.id, { kind: 'pdf', page: 1 }, 0.1);
|
||
Date.now = () => 2000;
|
||
readerStore.setProgress(selectionBook.id, { kind: 'pdf', page: 2 }, 0.2);
|
||
} finally {
|
||
Date.now = realNow;
|
||
}
|
||
|
||
check(
|
||
'真实存储预置三本书、两个书架和三个结构化笔记',
|
||
library.list().length === 3
|
||
&& library.listShelves().length === 2
|
||
&& readerStore.listNotes({}).length === 3
|
||
&& readerStore.listCollections().length === 1
|
||
);
|
||
check(
|
||
'预置笔记包含人工与摘录结构字段',
|
||
manualNote.source === 'manual'
|
||
&& manualNote.collectionId === notebook.id
|
||
&& manualNote.pinned === true
|
||
&& selectionNote.source === 'selection'
|
||
&& selectionNote.quote === 'Quoted selection excerpt'
|
||
&& selectionNote.locator.page === 3
|
||
&& purgeNote.source === 'manual'
|
||
);
|
||
|
||
await poll('主窗口创建', async () => {
|
||
win = BrowserWindow.getAllWindows()
|
||
.find((candidate) => candidate.getTitle() === 'PeopleLib') || null;
|
||
return !!win;
|
||
});
|
||
win.hide();
|
||
win.webContents.setBackgroundThrottling(false);
|
||
await pollJs(
|
||
'主窗口书库渲染完成',
|
||
"document.readyState === 'complete' && document.querySelectorAll('#libGrid .card').length === 3"
|
||
);
|
||
|
||
check(
|
||
'顶部提供“我的笔记”页签',
|
||
await js(`(() => {
|
||
const tab = document.querySelector('.tab[data-tab="notes"]');
|
||
return !!tab && tab.textContent.trim() === '我的笔记';
|
||
})()`)
|
||
);
|
||
check(
|
||
'设置旁边提供明暗主题图标按钮',
|
||
await js(`(() => {
|
||
const settingsButton = document.querySelector('.tab[data-tab="settings"]');
|
||
const themeButton = document.getElementById('uiThemeBtn');
|
||
return !!themeButton && settingsButton.previousElementSibling === themeButton
|
||
&& !!themeButton.querySelector('.ui-theme-sun')
|
||
&& themeButton.title === '切换到明亮主题'
|
||
&& document.querySelector('.titlebar-left').textContent.includes('人民阅读器')
|
||
&& document.querySelector('.brand-logo-dark').naturalWidth > 0;
|
||
})()`)
|
||
);
|
||
await js("document.getElementById('uiThemeBtn').click()");
|
||
await pollJs(
|
||
'主窗口切换为明亮主题',
|
||
`document.documentElement.dataset.uiTheme === 'light'
|
||
&& document.getElementById('uiThemeBtn').title === '切换到暗色主题'
|
||
&& getComputedStyle(document.body).color === 'rgb(31, 41, 55)'
|
||
&& getComputedStyle(document.querySelector('.brand-logo-light')).display !== 'none'
|
||
&& document.querySelector('.brand-logo-light').naturalWidth > 0`
|
||
);
|
||
check(
|
||
'主窗口主题与阅读器主题偏好同步持久化',
|
||
settings.get('ui.theme', 'dark') === 'light'
|
||
&& settings.get('reader.uiTheme', 'dark') === 'light'
|
||
);
|
||
await js("document.querySelector('.tab[data-tab=\"settings\"]').click()");
|
||
await js("document.getElementById('zlibLoginBtn').click()");
|
||
await pollJs('Z-Library 登录弹窗打开', "!document.getElementById('modal').classList.contains('hidden')");
|
||
check(
|
||
'Z-Library 登录取消与确定按钮尺寸一致',
|
||
await js(`(() => {
|
||
const cancel = document.getElementById('modalCancel').getBoundingClientRect();
|
||
const ok = document.getElementById('modalOk').getBoundingClientRect();
|
||
return cancel.width === ok.width && cancel.height === ok.height;
|
||
})()`)
|
||
);
|
||
await js("document.getElementById('modalCancel').click()");
|
||
await js("document.querySelector('.tab[data-tab=\"library\"]').click()");
|
||
await pollJs('返回书库页', "document.querySelectorAll('#libGrid .card').length === 3");
|
||
const allTitles = await libraryTitles();
|
||
check(
|
||
'全部书籍显示三张卡和总数',
|
||
allTitles.length === 3
|
||
&& ['Purge Reading Data Book', 'Selection Source Book', 'Retained Research Book']
|
||
.every((title) => allTitles.includes(title))
|
||
&& (await js("document.getElementById('libStatus').textContent")) === '共 3 条'
|
||
);
|
||
|
||
const rescanMissingFile = path.join(TMP, 'rescan-missing.txt');
|
||
fs.writeFileSync(rescanMissingFile, 'rescan missing fixture');
|
||
const rescanMissingShelf = library.addShelf({ name: 'Rescan Missing Shelf' });
|
||
const rescanMissingBook = library.add({
|
||
title: 'Rescan Missing Book',
|
||
shelfId: rescanMissingShelf.id,
|
||
tags: ['Rescan Missing Tag'],
|
||
files: [{ path: rescanMissingFile, name: 'rescan-missing.txt', format: 'TXT' }]
|
||
});
|
||
await pollJs(
|
||
'缺失扫描夹具显示在书库',
|
||
`!!document.querySelector('#libGrid .card[data-id="${rescanMissingBook.id}"]')`
|
||
);
|
||
fs.unlinkSync(rescanMissingFile);
|
||
await js("document.getElementById('rescanBtn').click()");
|
||
await waitForModal('发现文件缺失');
|
||
check(
|
||
'重新扫描报告缺失数量并说明保留阅读资料',
|
||
await js(`(() => {
|
||
const text = document.getElementById('modalBody').textContent;
|
||
return text.includes('1 条书库内容的文件均已缺失')
|
||
&& text.includes('对应分类和标签也会一并清理')
|
||
&& text.includes('笔记、书签、进度和标注会保留');
|
||
})()`)
|
||
);
|
||
await js("document.getElementById('modalCancel').click()");
|
||
await pollJs(
|
||
'取消缺失清理后恢复扫描按钮',
|
||
"!document.getElementById('rescanBtn').disabled",
|
||
5000
|
||
);
|
||
check(
|
||
'取消清理保留缺失书库条目',
|
||
!!library.get(rescanMissingBook.id)
|
||
&& library.get(rescanMissingBook.id).missing
|
||
&& library.listShelves().some((shelf) => shelf.id === rescanMissingShelf.id)
|
||
&& library.listTags().some((tag) => tag.name === 'Rescan Missing Tag')
|
||
);
|
||
|
||
await js("document.getElementById('rescanBtn').click()");
|
||
await waitForModal('发现文件缺失');
|
||
await submitModal();
|
||
await poll(
|
||
'确认后清理缺失书库条目',
|
||
() => Promise.resolve(!library.get(rescanMissingBook.id))
|
||
);
|
||
await pollJs(
|
||
'缺失条目清理后书库恢复',
|
||
`!document.querySelector('#libGrid .card[data-id="${rescanMissingBook.id}"]')
|
||
&& document.querySelectorAll('#libGrid .card').length === 3`
|
||
);
|
||
check(
|
||
'缺失清理移除对应空分类和标签且不影响其余内容',
|
||
library.list().length === 3
|
||
&& library.list().every((item) => item.id !== rescanMissingBook.id)
|
||
&& !library.listShelves().some((shelf) => shelf.id === rescanMissingShelf.id)
|
||
&& !library.listTags().some((tag) => tag.name === 'Rescan Missing Tag')
|
||
);
|
||
|
||
await js(`(() => {
|
||
document.getElementById('librarySearchInput').value = 'Rsrch';
|
||
document.getElementById('librarySearchBtn').click();
|
||
})()`);
|
||
await pollJs(
|
||
'书库标题模糊搜索刷新',
|
||
`document.querySelectorAll('#libGrid .card').length === 1
|
||
&& document.querySelector('#libGrid .card-title').textContent === 'Retained Research Book'`
|
||
);
|
||
check(
|
||
'书库模糊匹配标题',
|
||
(await js("document.getElementById('libStatus').textContent"))
|
||
=== '搜索“Rsrch”显示 1 条,当前分类 3 条'
|
||
);
|
||
check(
|
||
'书库搜索结果提示独占工具栏下一行',
|
||
await js(`(() => {
|
||
const search = document.querySelector('.library-toolbar').getBoundingClientRect();
|
||
const status = document.getElementById('libStatus').getBoundingClientRect();
|
||
return status.top >= search.bottom && status.left === search.left;
|
||
})()`)
|
||
);
|
||
await js(`(() => {
|
||
document.getElementById('librarySearchInput').value = 'bob auth';
|
||
document.getElementById('librarySearchBtn').click();
|
||
})()`);
|
||
await pollJs(
|
||
'书库作者模糊搜索刷新',
|
||
`document.querySelectorAll('#libGrid .card').length === 1
|
||
&& document.querySelector('#libGrid .card-title').textContent === 'Selection Source Book'`
|
||
);
|
||
check('书库模糊匹配作者', JSON.stringify(await libraryTitles())
|
||
=== JSON.stringify(['Selection Source Book']));
|
||
await js("document.getElementById('libraryClearSearchBtn').click()");
|
||
await pollJs('清除书库搜索恢复全部卡片', "document.querySelectorAll('#libGrid .card').length === 3");
|
||
check(
|
||
'设置中提供最近阅读排序',
|
||
await js(`Array.from(document.getElementById('sortSelect').options)
|
||
.some((option) => option.value === 'recent' && option.textContent === '最近阅读')`)
|
||
);
|
||
await js(`(() => {
|
||
const select = document.getElementById('sortSelect');
|
||
select.value = 'recent';
|
||
select.dispatchEvent(new Event('change'));
|
||
})()`);
|
||
await pollJs(
|
||
'最近阅读排序刷新',
|
||
`Array.from(document.querySelectorAll('#libGrid .card-title'))
|
||
.map((node) => node.textContent).join('|')
|
||
=== 'Selection Source Book|Retained Research Book|Purge Reading Data Book'`
|
||
);
|
||
check(
|
||
'最近阅读按阅读进度时间降序且未读条目置后',
|
||
JSON.stringify(await libraryTitles()) === JSON.stringify([
|
||
'Selection Source Book',
|
||
'Retained Research Book',
|
||
'Purge Reading Data Book'
|
||
])
|
||
);
|
||
check(
|
||
'书架和标签过多时左侧栏独立滚动',
|
||
await js(`new Promise((resolve) => {
|
||
const list = document.getElementById('libraryShelfList');
|
||
for (let index = 0; index < 80; index++) {
|
||
const row = document.createElement('button');
|
||
row.className = 'library-filter';
|
||
row.textContent = \`滚动测试书架 \${index + 1}\`;
|
||
list.appendChild(row);
|
||
}
|
||
const sidebar = document.querySelector('.library-sidebar');
|
||
sidebar.scrollTop = sidebar.scrollHeight;
|
||
requestAnimationFrame(() => {
|
||
const last = list.lastElementChild;
|
||
resolve(
|
||
getComputedStyle(sidebar).overflowY === 'auto'
|
||
&& sidebar.scrollHeight > sidebar.clientHeight
|
||
&& last.getBoundingClientRect().bottom <= sidebar.getBoundingClientRect().bottom + 1
|
||
);
|
||
});
|
||
})`)
|
||
);
|
||
await js("Library.refresh(true)");
|
||
await js(`window.__pendingCoverCard = document.querySelector(
|
||
'#libGrid .card[data-id="${selectionBook.id}"]'
|
||
)`);
|
||
const stableTag = library.addTag({ name: '异步刷新占位标签' });
|
||
await pollJs(
|
||
'无关书库通知刷新侧栏',
|
||
`Array.from(document.querySelectorAll('#libraryTagList .library-filter'))
|
||
.some((button) => button.textContent.includes('异步刷新占位标签'))`
|
||
);
|
||
check(
|
||
'其它封面或目录刷新不会重建未获取封面的卡片',
|
||
await js(`window.__pendingCoverCard === document.querySelector(
|
||
'#libGrid .card[data-id="${selectionBook.id}"]'
|
||
) && window.__pendingCoverCard.querySelector('.card-cover')
|
||
.dataset.coverState === 'pending'`)
|
||
);
|
||
library.removeTag(stableTag.id);
|
||
await pollJs(
|
||
'移除异步刷新占位标签',
|
||
`!Array.from(document.querySelectorAll('#libraryTagList .library-filter'))
|
||
.some((button) => button.textContent.includes('异步刷新占位标签'))`
|
||
);
|
||
check(
|
||
'书库卡片操作使用纯图标和悬浮文字',
|
||
await js(`Array.from(document.querySelectorAll('#libGrid .lib-card-actions button')).every(
|
||
(button) => !!button.querySelector('svg')
|
||
&& !button.textContent.trim()
|
||
&& !!button.title
|
||
&& button.getAttribute('aria-label') === button.title
|
||
)`)
|
||
);
|
||
check(
|
||
'书库长标题单行省略并通过悬浮提示显示全文',
|
||
await js(`(() => {
|
||
const title = document.querySelector(
|
||
'#libGrid .card[data-id="${retainedBook.id}"] .card-title'
|
||
);
|
||
const style = getComputedStyle(title);
|
||
return title.title === title.textContent
|
||
&& style.whiteSpace === 'nowrap'
|
||
&& style.overflow === 'hidden'
|
||
&& style.textOverflow === 'ellipsis';
|
||
})()`)
|
||
);
|
||
await js(`document.querySelector(
|
||
'#libGrid .card[data-id="${retainedBook.id}"] .card-cover'
|
||
).click()`);
|
||
let readerWindow = null;
|
||
await poll('点击封面创建内置阅读器窗口', async () => {
|
||
readerWindow = BrowserWindow.getAllWindows().find((candidate) => (
|
||
candidate !== win && candidate.webContents.getURL().includes('/reader.html?')
|
||
)) || null;
|
||
return !!readerWindow;
|
||
});
|
||
// 这本书的文件顺序是 [txt, pdf],封面走的是"第一个可阅读文件",
|
||
// 也就是 txt(txt/md 同样能内置阅读,走 text-adapter 转 epub 渲染)。
|
||
// 这里断言"渲染出正文",不要写死 PDF 画布:那样等于把
|
||
// "txt 不可阅读所以退到 pdf" 这个旧缺陷当成期望行为锁死
|
||
await poll('封面打开的书在内置阅读器渲染出正文', async () => (
|
||
!readerWindow.isDestroyed()
|
||
&& readerWindow.webContents.executeJavaScript(`(() => {
|
||
if (document.querySelector('.doc-overlay.err')) return false;
|
||
const canvas = document.querySelector('.pdfx-page[data-page="1"] .pdfx-canvas');
|
||
if (canvas && canvas.width > 0) return true;
|
||
const frame = document.querySelector('.host-epub iframe');
|
||
const body = frame && frame.contentDocument && frame.contentDocument.body;
|
||
return !!(body && body.textContent.trim().length > 0);
|
||
})()`)
|
||
), 20000);
|
||
check('点击可阅读图书封面直接打开内置阅读器', !!readerWindow);
|
||
check(
|
||
'封面打开的是第一个可阅读文件(txt 也算)',
|
||
(await readerWindow.webContents.executeJavaScript(
|
||
"new URLSearchParams(location.search).get('fileIndex')"
|
||
)) === '0'
|
||
);
|
||
readerWindow.destroy();
|
||
await wait(200);
|
||
check(
|
||
'分类侧栏显示全部、未分类和各书架的真实书籍数量',
|
||
await js(`(() => {
|
||
const all = document.querySelector('#libraryTab .library-filter[data-shelf=""]');
|
||
const uncategorized = document.querySelector(
|
||
'#libraryTab .library-filter[data-shelf="__uncategorized__"]'
|
||
);
|
||
const shelves = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'));
|
||
const research = shelves.find((button) => button.textContent.includes('研究书架'));
|
||
const review = shelves.find((button) => button.textContent.includes('待复核书架'));
|
||
const actionButtons = Array.from(
|
||
document.querySelectorAll('.library-shelf-actions .notes-icon-btn')
|
||
);
|
||
return all.textContent.trim() === '全部书籍(3)'
|
||
&& uncategorized.textContent.trim() === '未分类(1)'
|
||
&& research.textContent.trim() === '研究书架(1)'
|
||
&& review.textContent.trim() === '待复核书架(1)'
|
||
&& actionButtons.every((button) => {
|
||
const style = getComputedStyle(button);
|
||
return style.backgroundColor === 'rgba(0, 0, 0, 0)'
|
||
&& style.borderTopColor === 'rgba(0, 0, 0, 0)';
|
||
});
|
||
})()`)
|
||
);
|
||
const longShelfName = 'TheArtOfComputerProgramming超长分类名称';
|
||
library.updateShelf(researchShelf.id, { name: longShelfName });
|
||
await pollJs(
|
||
'超长分类名称刷新',
|
||
`Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||
.some((button) => button.firstElementChild.textContent === ${JSON.stringify(longShelfName)})`
|
||
);
|
||
check(
|
||
'超长分类只省略名称并完整保留数量与操作区',
|
||
await js(`(() => {
|
||
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||
.find((candidate) => candidate.firstElementChild.textContent === ${JSON.stringify(longShelfName)});
|
||
const name = button.firstElementChild;
|
||
const count = button.querySelector('.library-filter-count');
|
||
const actions = button.closest('.library-shelf-row').querySelector('.library-shelf-actions');
|
||
return getComputedStyle(button).display === 'flex'
|
||
&& getComputedStyle(name).textOverflow === 'ellipsis'
|
||
&& name.scrollWidth > name.clientWidth
|
||
&& count.textContent === '(1)'
|
||
&& count.getBoundingClientRect().width > 0
|
||
&& count.getBoundingClientRect().right <= actions.getBoundingClientRect().left + 1;
|
||
})()`)
|
||
);
|
||
library.updateShelf(researchShelf.id, { name: researchShelf.name });
|
||
await pollJs(
|
||
'恢复研究书架名称',
|
||
`Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||
.some((button) => button.firstElementChild.textContent === ${JSON.stringify(researchShelf.name)})`
|
||
);
|
||
check(
|
||
'标签侧栏显示真实聚合计数',
|
||
await js(`(() => {
|
||
const buttons = Array.from(document.querySelectorAll('#libraryTagList .library-filter'));
|
||
const shared = buttons.find((button) => button.textContent.includes('# Shared'));
|
||
const methods = buttons.find((button) => button.textContent.includes('# Methods'));
|
||
return !!shared && shared.querySelector('.library-filter-count').textContent === '2'
|
||
&& !!methods && methods.querySelector('.library-filter-count').textContent === '1';
|
||
})()`)
|
||
);
|
||
|
||
await js("document.getElementById('addTagBtn').click()");
|
||
await waitForModal('新建标签');
|
||
check(
|
||
'新建标签使用应用内弹窗',
|
||
await js("!!document.getElementById('libraryTagName')")
|
||
);
|
||
await js("document.getElementById('libraryTagName').value = '界面标签'");
|
||
await submitModal();
|
||
await poll(
|
||
'新标签写入真实存储',
|
||
() => Promise.resolve(library.listTags().some((tag) => tag.name === '界面标签'))
|
||
);
|
||
let uiTag = library.listTags().find((tag) => tag.name === '界面标签');
|
||
check(
|
||
'新建零使用标签并自动选中',
|
||
!!uiTag && uiTag.count === 0
|
||
&& await js(`(() => {
|
||
const button = Array.from(document.querySelectorAll('#libraryTagList .library-filter'))
|
||
.find((candidate) => candidate.textContent.includes('# 界面标签'));
|
||
return !!button && button.classList.contains('active')
|
||
&& document.getElementById('libStatus').textContent === '显示 0 条,共 3 条';
|
||
})()`)
|
||
);
|
||
await js(`document.querySelector('[aria-label="重命名界面标签"]').click()`);
|
||
await waitForModal('重命名标签');
|
||
await js("document.getElementById('libraryTagName').value = '界面标签已重命名'");
|
||
await submitModal();
|
||
await poll(
|
||
'标签重命名写入真实存储',
|
||
() => Promise.resolve(library.listTags().some((tag) => tag.name === '界面标签已重命名'))
|
||
);
|
||
uiTag = library.listTags().find((tag) => tag.name === '界面标签已重命名');
|
||
check(
|
||
'通过界面重命名标签',
|
||
!!uiTag && !library.listTags().some((tag) => tag.name === '界面标签')
|
||
&& await js("!!document.querySelector('[aria-label=\"删除界面标签已重命名\"]')")
|
||
);
|
||
|
||
await js(`document.querySelector('#libraryTab .library-filter[data-shelf="__uncategorized__"]').click()`);
|
||
await pollJs(
|
||
'未分类筛选刷新',
|
||
`document.querySelectorAll('#libGrid .card').length === 1
|
||
&& document.querySelector('#libGrid .card-title').textContent === 'Purge Reading Data Book'`
|
||
);
|
||
check(
|
||
'未分类筛选改变卡片和状态',
|
||
JSON.stringify(await libraryTitles()) === JSON.stringify(['Purge Reading Data Book'])
|
||
&& (await js("document.getElementById('libStatus').textContent")) === '显示 1 条,共 3 条'
|
||
);
|
||
|
||
await js(`(() => {
|
||
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||
.find((candidate) => candidate.firstElementChild.textContent.trim() === '研究书架');
|
||
button.click();
|
||
})()`);
|
||
await pollJs(
|
||
'书架筛选刷新',
|
||
`document.querySelectorAll('#libGrid .card').length === 1
|
||
&& document.querySelector('#libGrid .card-title').textContent === 'Retained Research Book'`
|
||
);
|
||
check(
|
||
'书架筛选只显示所属书籍和状态',
|
||
JSON.stringify(await libraryTitles()) === JSON.stringify(['Retained Research Book'])
|
||
&& (await js("document.getElementById('libStatus').textContent")) === '显示 1 条,共 3 条'
|
||
);
|
||
|
||
await js(`(() => {
|
||
const button = Array.from(document.querySelectorAll('#libraryTagList .library-filter'))
|
||
.find((candidate) => candidate.textContent.includes('# Shared'));
|
||
button.click();
|
||
})()`);
|
||
await pollJs(
|
||
'标签筛选刷新',
|
||
"document.querySelectorAll('#libGrid .card').length === 2"
|
||
);
|
||
const sharedTitles = await libraryTitles();
|
||
check(
|
||
'标签筛选显示两本匹配书籍和状态',
|
||
sharedTitles.includes('Retained Research Book')
|
||
&& sharedTitles.includes('Selection Source Book')
|
||
&& (await js("document.getElementById('libStatus').textContent")) === '显示 2 条,共 3 条'
|
||
);
|
||
|
||
await js("document.getElementById('addShelfBtn').click()");
|
||
await waitForModal('新建书架');
|
||
check(
|
||
'新建书架使用应用内弹窗',
|
||
await js("!!document.getElementById('libraryShelfName')")
|
||
);
|
||
await js("document.getElementById('libraryShelfName').value = '界面书架'");
|
||
await submitModal();
|
||
await poll(
|
||
'新书架写入真实存储',
|
||
() => Promise.resolve(library.listShelves().some((shelf) => shelf.name === '界面书架'))
|
||
);
|
||
let uiShelf = library.listShelves().find((shelf) => shelf.name === '界面书架');
|
||
check(
|
||
'通过界面创建书架并自动选中',
|
||
!!uiShelf
|
||
&& await js(`(() => {
|
||
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||
.find((candidate) => candidate.firstElementChild.textContent.trim() === '界面书架');
|
||
return !!button && button.classList.contains('active')
|
||
&& document.getElementById('libStatus').textContent === '显示 0 条,共 3 条';
|
||
})()`)
|
||
);
|
||
|
||
await js(`document.querySelector('[aria-label="重命名界面书架"]').click()`);
|
||
await waitForModal('重命名书架');
|
||
check(
|
||
'重命名弹窗预填原书架名',
|
||
(await js("document.getElementById('libraryShelfName').value")) === '界面书架'
|
||
);
|
||
await js("document.getElementById('libraryShelfName').value = '界面书架已重命名'");
|
||
await submitModal();
|
||
await poll(
|
||
'书架重命名写入真实存储',
|
||
() => Promise.resolve(library.listShelves().some((shelf) => shelf.name === '界面书架已重命名'))
|
||
);
|
||
uiShelf = library.listShelves().find((shelf) => shelf.name === '界面书架已重命名');
|
||
check(
|
||
'通过界面重命名书架',
|
||
!!uiShelf
|
||
&& !library.listShelves().some((shelf) => shelf.name === '界面书架')
|
||
&& await js("!!document.querySelector('[aria-label=\"删除界面书架已重命名\"]')")
|
||
);
|
||
|
||
await js(`document.querySelector('#libraryTab .library-filter[data-shelf=""]').click()`);
|
||
await pollJs(
|
||
'重置全部书籍筛选',
|
||
"document.querySelectorAll('#libGrid .card').length === 3"
|
||
);
|
||
await js(`document.querySelector(
|
||
'#libGrid .card[data-id="${purgeBook.id}"] [data-act="organize"]'
|
||
).click()`);
|
||
await waitForModal('整理书籍');
|
||
check(
|
||
'整理弹窗提供书架和标签多选下拉',
|
||
await js(`!!document.getElementById('libraryBookShelf')
|
||
&& document.getElementById('libraryBookTags').tagName === 'DETAILS'
|
||
&& document.querySelectorAll('#libraryBookTags input[type="checkbox"]').length >= 4`)
|
||
);
|
||
await js(`(() => {
|
||
document.getElementById('libraryBookShelf').value = ${JSON.stringify(uiShelf.id)};
|
||
document.querySelectorAll('#libraryBookTags input[type="checkbox"]').forEach((input) => {
|
||
input.checked = input.value === 'Shared' || input.value === '界面标签已重命名';
|
||
});
|
||
})()`);
|
||
await submitModal();
|
||
await poll(
|
||
'整理操作写入真实存储',
|
||
() => {
|
||
const entry = library.get(purgeBook.id);
|
||
return Promise.resolve(
|
||
!!entry
|
||
&& entry.shelfId === uiShelf.id
|
||
&& JSON.stringify(entry.tags) === JSON.stringify(['Shared', '界面标签已重命名'])
|
||
);
|
||
}
|
||
);
|
||
check(
|
||
'通过整理弹窗分配书架并多选标签',
|
||
library.get(purgeBook.id).shelfId === uiShelf.id
|
||
&& await js(`(() => {
|
||
const card = document.querySelector('#libGrid .card[data-id="${purgeBook.id}"]');
|
||
return Array.from(card.querySelectorAll('.library-card-tag'))
|
||
.map((tag) => tag.textContent).join('|') === 'Shared|界面标签已重命名';
|
||
})()`)
|
||
);
|
||
check(
|
||
'整理后标签侧栏聚合计数同步',
|
||
await js(`(() => {
|
||
const buttons = Array.from(document.querySelectorAll('#libraryTagList .library-filter'));
|
||
const shared = buttons.find((button) => button.textContent.includes('# Shared'));
|
||
const uiTag = buttons.find((button) => button.textContent.includes('# 界面标签已重命名'));
|
||
return !!shared && shared.querySelector('.library-filter-count').textContent === '3'
|
||
&& !!uiTag && uiTag.querySelector('.library-filter-count').textContent === '1';
|
||
})()`)
|
||
);
|
||
|
||
await js(`document.querySelector('[aria-label="删除界面标签已重命名"]').click()`);
|
||
await waitForModal('删除标签');
|
||
check(
|
||
'删除标签弹窗说明会从所有书籍移除',
|
||
(await js("document.getElementById('modalBody').textContent"))
|
||
.includes('该标签会从所有书籍中移除,书籍不会被删除')
|
||
);
|
||
await submitModal();
|
||
await poll(
|
||
'删除标签和书籍引用完成',
|
||
() => Promise.resolve(
|
||
!library.listTags().some((tag) => tag.id === uiTag.id)
|
||
&& JSON.stringify(library.get(purgeBook.id).tags) === JSON.stringify(['Shared'])
|
||
)
|
||
);
|
||
check(
|
||
'删除标签不删除书籍',
|
||
!!library.get(purgeBook.id)
|
||
&& !library.listTags().some((tag) => tag.name === '界面标签已重命名')
|
||
);
|
||
|
||
await js(`(() => {
|
||
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||
.find((candidate) => candidate.firstElementChild.textContent.trim() === '界面书架已重命名');
|
||
button.click();
|
||
})()`);
|
||
await pollJs(
|
||
'新书架筛选刷新',
|
||
`document.querySelectorAll('#libGrid .card').length === 1
|
||
&& document.querySelector('#libGrid .card').dataset.id === ${JSON.stringify(purgeBook.id)}`
|
||
);
|
||
await js(`document.querySelector('[aria-label="删除界面书架已重命名"]').click()`);
|
||
await waitForModal('删除书架');
|
||
check(
|
||
'删除书架弹窗说明书籍移至未分类',
|
||
(await js("document.getElementById('modalBody').textContent"))
|
||
.includes('书籍不会被删除,将移至「未分类」')
|
||
);
|
||
await submitModal();
|
||
await poll(
|
||
'删除书架完成',
|
||
() => Promise.resolve(!library.listShelves().some((shelf) => shelf.id === uiShelf.id))
|
||
);
|
||
await pollJs(
|
||
'删除书架后未分类视图刷新',
|
||
`document.querySelectorAll('#libGrid .card').length === 1
|
||
&& document.querySelector('#libGrid .card').dataset.id === ${JSON.stringify(purgeBook.id)}`
|
||
);
|
||
check(
|
||
'删除书架将书籍移至未分类',
|
||
library.get(purgeBook.id).shelfId === null
|
||
&& await js(`document.querySelector(
|
||
'#libraryTab .library-filter[data-shelf="__uncategorized__"]'
|
||
).classList.contains('active')`)
|
||
&& (await js("document.getElementById('libStatus').textContent")) === '显示 1 条,共 3 条'
|
||
);
|
||
|
||
await js(`document.querySelector('#libraryTab .library-filter[data-shelf=""]').click()`);
|
||
await pollJs('多选前重置为全部书籍', "document.querySelectorAll('#libGrid .card').length === 3");
|
||
|
||
check(
|
||
'默认不进入多选模式',
|
||
await js(`document.getElementById('librarySelectionBar').classList.contains('hidden')
|
||
&& document.querySelectorAll('#libGrid .card-select').length === 0`)
|
||
);
|
||
|
||
await js("document.getElementById('librarySelectModeBtn').click()");
|
||
await pollJs(
|
||
'进入多选模式后卡片出现复选框',
|
||
"document.querySelectorAll('#libGrid .card-select').length === 3"
|
||
);
|
||
check(
|
||
'多选模式隐藏单卡操作并停用封面阅读入口',
|
||
await js(`!document.getElementById('librarySelectionBar').classList.contains('hidden')
|
||
&& document.querySelectorAll('#libGrid .lib-card-actions').length === 0
|
||
&& document.querySelectorAll('#libGrid .card-cover[data-act="read"]').length === 0
|
||
&& document.getElementById('libraryBulkOrganizeBtn').disabled
|
||
&& document.getElementById('libraryBulkRemoveBtn').disabled`)
|
||
);
|
||
|
||
await js("document.querySelectorAll('#libGrid .card-select input')[0].click()");
|
||
await pollJs(
|
||
'勾选单项后启用批量操作',
|
||
`document.getElementById('librarySelectionCount').textContent.includes('已选择 1')
|
||
&& document.getElementById('librarySelectAll').indeterminate === true
|
||
&& !document.getElementById('libraryBulkOrganizeBtn').disabled`
|
||
);
|
||
|
||
await js("document.getElementById('librarySelectAll').click()");
|
||
await pollJs(
|
||
'全选覆盖当前筛选结果',
|
||
`document.getElementById('librarySelectionCount').textContent.includes('已选择 3')
|
||
&& document.getElementById('librarySelectAllLabel').textContent === '取消全选'`
|
||
);
|
||
|
||
// 全选只作用于当前筛选结果:切到只含一本的书架后,看不见的选中项必须失效,
|
||
// 否则批量操作会误伤用户看不到的书
|
||
await js(`(() => {
|
||
const button = Array.from(document.querySelectorAll('#libraryShelfList .library-filter'))
|
||
.find((candidate) => candidate.firstElementChild.textContent.trim() === '研究书架');
|
||
button.click();
|
||
})()`);
|
||
await pollJs(
|
||
'切换筛选后丢弃不可见项的选中状态',
|
||
`document.querySelectorAll('#libGrid .card').length === 1
|
||
&& document.getElementById('librarySelectionCount').textContent.includes('已选择 1')`
|
||
);
|
||
|
||
await js(`document.querySelector('#libraryTab .library-filter[data-shelf=""]').click()`);
|
||
await pollJs('恢复全部书籍视图', "document.querySelectorAll('#libGrid .card').length === 3");
|
||
await js("document.getElementById('librarySelectAll').click()");
|
||
await pollJs(
|
||
'重新全选三本',
|
||
"document.getElementById('librarySelectionCount').textContent.includes('已选择 3')"
|
||
);
|
||
|
||
const beforeBulk = library.list().reduce((acc, item) => {
|
||
acc[item.id] = { shelfId: item.shelfId, tags: (item.tags || []).slice() };
|
||
return acc;
|
||
}, {});
|
||
const total = Object.keys(beforeBulk).length;
|
||
const holdersOf = (name) => Object.values(beforeBulk)
|
||
.filter((entry) => entry.tags.some((tag) => tag.toLocaleLowerCase() === name.toLocaleLowerCase()))
|
||
.length;
|
||
const expectedState = (name) => {
|
||
const held = holdersOf(name);
|
||
return held === 0 ? 'none' : (held === total ? 'all' : 'some');
|
||
};
|
||
const tagStateExpectations = ['Shared', 'Methods', 'Review', 'Archive']
|
||
.map((name) => [name, expectedState(name), holdersOf(name)]);
|
||
|
||
await js("document.getElementById('libraryBulkOrganizeBtn').click()");
|
||
await waitForModal('批量整理 3 本');
|
||
const actualStates = await js(`(() => {
|
||
const map = {};
|
||
document.querySelectorAll('#libraryBulkTags input[type="checkbox"]').forEach((box) => {
|
||
map[box.value] = { state: box.dataset.state, indeterminate: box.indeterminate, checked: box.checked };
|
||
});
|
||
return map;
|
||
})()`);
|
||
check(
|
||
'批量整理按持有比例给出标签三态',
|
||
tagStateExpectations.every(([name, state]) => {
|
||
const actual = actualStates[name];
|
||
if (!actual || actual.state !== state) return false;
|
||
if (state === 'some') return actual.indeterminate === true && actual.checked === false;
|
||
if (state === 'all') return actual.indeterminate === false && actual.checked === true;
|
||
return actual.indeterminate === false && actual.checked === false;
|
||
})
|
||
// 三种状态都要真实出现过,否则这条断言可能什么都没验到
|
||
&& new Set(tagStateExpectations.map(([, state]) => state)).size === 3,
|
||
tagStateExpectations.map(([n, s, h]) => `${n}=${s}(${h}/${total})`).join(' ')
|
||
);
|
||
check(
|
||
'所选书籍书架不一致时默认保持不变',
|
||
await js("document.getElementById('libraryBulkShelf').value === '__keep__'")
|
||
);
|
||
|
||
// 只勾选一个未被任何书持有的标签,其余保持部分选中
|
||
await js(`(() => {
|
||
const box = Array.from(document.querySelectorAll('#libraryBulkTags input[type="checkbox"]'))
|
||
.find((candidate) => candidate.value === 'Archive');
|
||
box.click();
|
||
})()`);
|
||
await submitModal();
|
||
await poll(
|
||
'批量整理写入真实存储',
|
||
() => Promise.resolve(library.list().every((item) => (item.tags || []).includes('Archive')))
|
||
);
|
||
const afterBulk = library.list().reduce((acc, item) => {
|
||
acc[item.id] = { shelfId: item.shelfId, tags: (item.tags || []).slice() };
|
||
return acc;
|
||
}, {});
|
||
check(
|
||
'新增标签应用到全部选中项',
|
||
Object.values(afterBulk).every((entry) => entry.tags.includes('Archive'))
|
||
);
|
||
check(
|
||
'部分选中的标签保持原样,未被覆盖抹掉',
|
||
Object.entries(beforeBulk).every(([id, before]) => (
|
||
before.tags.every((tag) => afterBulk[id].tags.includes(tag))
|
||
)),
|
||
JSON.stringify(Object.values(afterBulk).map((e) => e.tags))
|
||
);
|
||
check(
|
||
'书架保持不变时未被改动',
|
||
Object.entries(beforeBulk).every(([id, before]) => afterBulk[id].shelfId === before.shelfId)
|
||
);
|
||
check(
|
||
'批量整理完成后退出多选模式',
|
||
await js(`document.getElementById('librarySelectionBar').classList.contains('hidden')
|
||
&& document.querySelectorAll('#libGrid .card-select').length === 0`)
|
||
);
|
||
|
||
await js(`document.querySelector('.tab[data-tab="notes"]').click()`);
|
||
await pollJs(
|
||
'我的笔记页渲染完成',
|
||
"document.querySelectorAll('#notesList .note-card').length === 3"
|
||
);
|
||
const initialCards = await noteCards();
|
||
check(
|
||
'我的笔记显示正文、摘录、书名和笔记本',
|
||
initialCards.some((card) =>
|
||
card.book === 'Retained Research Book'
|
||
&& card.text === 'Alpha insight searchable body'
|
||
&& card.meta.includes('研究笔记本'))
|
||
&& initialCards.some((card) =>
|
||
card.book === 'Selection Source Book'
|
||
&& card.quote === 'Quoted selection excerpt')
|
||
&& (await js("document.getElementById('notesStatus').textContent")) === '共 3 条笔记'
|
||
);
|
||
check(
|
||
'笔记卡使用网格视图并显示类型、来源和置顶状态',
|
||
initialCards[0].title === 'Pinned Research Note'
|
||
&& initialCards[0].pinned === true
|
||
&& initialCards.every((card) => card.type === '读书笔记')
|
||
&& initialCards[0].source === '人工'
|
||
&& initialCards.some((card) => card.title === 'Selected Passage' && card.source === '摘录')
|
||
&& await js(`getComputedStyle(document.getElementById('notesList')).display === 'grid'`)
|
||
);
|
||
check(
|
||
'笔记类型 Tab 提供全部、画布笔记和读书笔记',
|
||
await js(`Array.from(document.querySelectorAll('#notesTypeTabs .notes-type-tab'))
|
||
.map((button) => button.textContent.trim()).join(',') === '全部,画布笔记,读书笔记'`)
|
||
);
|
||
await js(`document.querySelector('#notesTypeTabs [data-note-type="canvas"]').click()`);
|
||
check(
|
||
'画布笔记 Tab 在尚无画布笔记时显示空状态',
|
||
(await noteCards()).length === 0
|
||
&& (await js("document.getElementById('notesStatus').textContent")) === '显示 0 条,共 3 条笔记'
|
||
);
|
||
await js(`document.querySelector('#notesTypeTabs [data-note-type="reading"]').click()`);
|
||
check(
|
||
'读书笔记 Tab 仅显示读书笔记',
|
||
(await noteCards()).length === 3
|
||
&& (await noteCards()).every((card) => card.type === '读书笔记')
|
||
);
|
||
await js(`document.querySelector('#notesTypeTabs [data-note-type=""]').click()`);
|
||
|
||
await js(`(() => {
|
||
document.getElementById('notesSearchInput').value = 'Quoted selection excerpt';
|
||
document.getElementById('notesSearchBtn').click();
|
||
})()`);
|
||
await pollJs(
|
||
'笔记搜索筛选刷新',
|
||
"document.querySelectorAll('#notesList .note-card').length === 1"
|
||
);
|
||
check(
|
||
'搜索筛选命中摘录文本',
|
||
(await noteCards())[0].title === 'Selected Passage'
|
||
&& (await js("document.getElementById('notesStatus').textContent")) === '显示 1 条,共 3 条笔记'
|
||
);
|
||
await js("document.getElementById('notesClearSearchBtn').click()");
|
||
await pollJs(
|
||
'清除笔记搜索',
|
||
"document.querySelectorAll('#notesList .note-card').length === 3"
|
||
);
|
||
|
||
await js(`(() => {
|
||
const select = document.getElementById('notesSourceSelect');
|
||
select.value = 'selection';
|
||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||
})()`);
|
||
check(
|
||
'来源筛选只显示摘录笔记',
|
||
(await noteCards()).length === 1
|
||
&& (await noteCards())[0].title === 'Selected Passage'
|
||
&& (await js("document.getElementById('notesStatus').textContent")) === '显示 1 条,共 3 条笔记'
|
||
);
|
||
await js(`(() => {
|
||
const select = document.getElementById('notesSourceSelect');
|
||
select.value = '';
|
||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||
})()`);
|
||
|
||
await js(`(() => {
|
||
const tag = Array.from(document.querySelectorAll('#notesTagFilters .note-tag'))
|
||
.find((button) => button.textContent === 'focus');
|
||
tag.click();
|
||
})()`);
|
||
check(
|
||
'标签筛选只显示匹配笔记',
|
||
(await noteCards()).length === 1
|
||
&& (await noteCards())[0].title === 'Pinned Research Note'
|
||
&& (await js("document.getElementById('notesStatus').textContent")) === '显示 1 条,共 3 条笔记'
|
||
);
|
||
await js(`(() => {
|
||
const tag = Array.from(document.querySelectorAll('#notesTagFilters .note-tag'))
|
||
.find((button) => button.textContent === 'focus');
|
||
tag.click();
|
||
})()`);
|
||
|
||
await js(`(() => {
|
||
const notebook = Array.from(document.querySelectorAll('#notesCollectionList .notes-collection'))
|
||
.find((button) => button.textContent.trim() === '研究笔记本');
|
||
notebook.click();
|
||
})()`);
|
||
check(
|
||
'笔记本筛选只显示所属笔记',
|
||
(await noteCards()).length === 1
|
||
&& (await noteCards())[0].title === 'Pinned Research Note'
|
||
&& (await js("document.getElementById('notesStatus').textContent")) === '显示 1 条,共 3 条笔记'
|
||
);
|
||
await js(`document.querySelector('#notesTab .notes-collection[data-collection=""]').click()`);
|
||
|
||
await js(`(() => {
|
||
const card = Array.from(document.querySelectorAll('#notesList .note-card'))
|
||
.find((candidate) => {
|
||
const title = candidate.querySelector('.note-title');
|
||
return title && title.textContent === 'Pinned Research Note';
|
||
});
|
||
card.querySelector('.note-action.edit').click();
|
||
})()`);
|
||
await waitForModal('编辑读书笔记');
|
||
check(
|
||
'Quill 段落选择与格式按钮保持同一行且位于 B/I 前方',
|
||
await js(`(() => {
|
||
const toolbar = document.querySelector('#noteEditRich .rich-note-toolbar');
|
||
const picker = toolbar.querySelector('.ql-picker.ql-header');
|
||
const bold = toolbar.querySelector('.ql-bold');
|
||
const options = picker.querySelector('.ql-picker-options');
|
||
return !!picker && !!bold
|
||
&& (picker.compareDocumentPosition(bold) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
||
&& getComputedStyle(toolbar).flexWrap === 'nowrap'
|
||
&& getComputedStyle(options).backgroundColor
|
||
=== getComputedStyle(document.querySelector('.note-card')).backgroundColor;
|
||
})()`)
|
||
);
|
||
await js(`(() => {
|
||
document.getElementById('noteEditTitle').value = 'Edited Research Note';
|
||
const quill = Quill.find(document.querySelector('#noteEditRich .rich-note-quill'));
|
||
quill.setText('Edited alpha insight body');
|
||
quill.formatText(0, 'Edited alpha insight body'.length, 'bold', true);
|
||
document.getElementById('noteEditCollection').value = ${JSON.stringify(notebook.id)};
|
||
document.getElementById('noteEditTags').value = 'focus, edited';
|
||
document.getElementById('noteEditPinned').checked = false;
|
||
const bytes = Uint8Array.from(atob(
|
||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9Z9WQAAAAASUVORK5CYII='
|
||
), (char) => char.charCodeAt(0));
|
||
const transfer = new DataTransfer();
|
||
transfer.items.add(new File([bytes], 'note-image.png', { type: 'image/png' }));
|
||
const input = document.querySelector('#noteEditRich input[type="file"]');
|
||
input.files = transfer.files;
|
||
input.dispatchEvent(new Event('change'));
|
||
})()`);
|
||
await pollJs(
|
||
'富文本编辑器插入图片',
|
||
"document.querySelectorAll('#noteEditRich .ql-editor img').length === 1"
|
||
);
|
||
await submitModal();
|
||
await poll(
|
||
'编辑笔记写入真实存储',
|
||
() => {
|
||
const note = readerStore.listNotes({ entryId: retainedBook.id })
|
||
.find((candidate) => candidate.id === manualNote.id);
|
||
return Promise.resolve(
|
||
!!note
|
||
&& note.title === 'Edited Research Note'
|
||
&& note.text === 'Edited alpha insight body'
|
||
&& note.richContent.ops.some((op) => op.insert && op.insert.image)
|
||
&& note.richContent.ops.some((op) => (
|
||
op.attributes && op.attributes.bold === true
|
||
&& op.insert === 'Edited alpha insight body'
|
||
))
|
||
&& note.collectionId === notebook.id
|
||
&& JSON.stringify(note.tags) === JSON.stringify(['focus', 'edited'])
|
||
&& note.pinned === false
|
||
);
|
||
}
|
||
);
|
||
check(
|
||
'通过界面编辑标题、正文、笔记本、标签和置顶状态',
|
||
(await noteCards()).some((card) =>
|
||
card.title === 'Edited Research Note'
|
||
&& card.text === 'Edited alpha insight body'
|
||
&& card.pinned === false
|
||
&& card.tags.join('|') === 'focus|edited')
|
||
);
|
||
check(
|
||
'富文本正文和内嵌图片安全渲染在笔记卡片',
|
||
await js(`(() => {
|
||
const card = Array.from(document.querySelectorAll('#notesList .note-card'))
|
||
.find((candidate) => candidate.querySelector('.note-title')?.textContent === 'Edited Research Note');
|
||
const image = card && card.querySelector('.note-text img');
|
||
return !!image
|
||
&& image.src.startsWith('data:image/png;base64,')
|
||
&& card.querySelector('.note-text strong')?.textContent === 'Edited alpha insight body';
|
||
})()`)
|
||
);
|
||
|
||
await js(`(() => {
|
||
const card = Array.from(document.querySelectorAll('#notesList .note-card'))
|
||
.find((candidate) => {
|
||
const title = candidate.querySelector('.note-title');
|
||
return title && title.textContent === 'Selected Passage';
|
||
});
|
||
card.querySelector('.note-action.delete').click();
|
||
})()`);
|
||
await waitForModal('删除笔记');
|
||
check(
|
||
'删除笔记使用应用内确认弹窗',
|
||
(await js("document.getElementById('modalBody').textContent")).includes('此操作无法撤销')
|
||
);
|
||
await submitModal();
|
||
await poll(
|
||
'删除笔记写入真实存储',
|
||
() => Promise.resolve(
|
||
!readerStore.listNotes({ entryId: selectionBook.id })
|
||
.some((note) => note.id === selectionNote.id)
|
||
)
|
||
);
|
||
await pollJs(
|
||
'删除笔记后页面刷新',
|
||
"document.querySelectorAll('#notesList .note-card').length === 2"
|
||
);
|
||
check(
|
||
'通过界面删除摘录笔记',
|
||
!(await noteCards()).some((card) => card.title === 'Selected Passage')
|
||
&& (await js("document.getElementById('notesStatus').textContent")) === '共 2 条笔记'
|
||
);
|
||
|
||
await js("document.getElementById('addGlobalNoteBtn').click()");
|
||
await waitForModal('选择笔记类型');
|
||
check(
|
||
'新建笔记先选择读书笔记或画布笔记',
|
||
await js(`Array.from(document.querySelectorAll('.note-type-choice-title'))
|
||
.map((item) => item.textContent.trim()).join(',') === '读书笔记,画布笔记'`)
|
||
);
|
||
await js("document.getElementById('modalOk').click()");
|
||
await waitForModal('新建读书笔记');
|
||
check(
|
||
'新建读书笔记可选择关联书籍',
|
||
await js(`(() => {
|
||
const select = document.getElementById('newNoteBook');
|
||
return !!select && Array.from(select.options)
|
||
.some((option) => option.value === ${JSON.stringify(retainedBook.id)}
|
||
&& option.textContent === 'Retained Research Book');
|
||
})()`)
|
||
);
|
||
await js(`(() => {
|
||
document.getElementById('newNoteBook').value = ${JSON.stringify(retainedBook.id)};
|
||
document.getElementById('newNoteTitle').value = 'UI Created Note';
|
||
Quill.find(document.querySelector('#newNoteRich .rich-note-quill'))
|
||
.setText('Created directly from My Notes page');
|
||
document.getElementById('newNoteCollection').value = ${JSON.stringify(notebook.id)};
|
||
document.getElementById('newNoteTags').value = 'ui-created, focus';
|
||
document.getElementById('newNotePinned').checked = true;
|
||
})()`);
|
||
await submitModal();
|
||
let createdNote = null;
|
||
await poll(
|
||
'页面新建笔记写入真实存储',
|
||
() => {
|
||
createdNote = readerStore.listNotes({ entryId: retainedBook.id })
|
||
.find((note) => note.text === 'Created directly from My Notes page') || null;
|
||
return Promise.resolve(!!createdNote);
|
||
}
|
||
);
|
||
await pollJs(
|
||
'页面新建笔记渲染完成',
|
||
`Array.from(document.querySelectorAll('#notesList .note-title'))
|
||
.some((title) => title.textContent === 'UI Created Note')`
|
||
);
|
||
check(
|
||
'页面新建笔记保存关联、标题、正文、笔记本、标签和置顶',
|
||
createdNote.title === 'UI Created Note'
|
||
&& createdNote.entryId === retainedBook.id
|
||
&& createdNote.collectionId === notebook.id
|
||
&& JSON.stringify(createdNote.tags) === JSON.stringify(['ui-created', 'focus'])
|
||
&& createdNote.pinned === true
|
||
&& createdNote.noteType === 'reading'
|
||
&& !createdNote.canvasContent
|
||
&& await js(`Array.from(document.querySelectorAll('.note-badge.type'))
|
||
.some((item) => item.textContent === '读书笔记')`)
|
||
&& (await noteCards())[0].title === 'UI Created Note'
|
||
);
|
||
|
||
await js(`document.querySelector('.tab[data-tab="library"]').click()`);
|
||
await js(`document.querySelector('#libraryTab .library-filter[data-shelf=""]').click()`);
|
||
await pollJs(
|
||
'返回全部书籍视图',
|
||
"document.querySelectorAll('#libGrid .card').length === 3"
|
||
);
|
||
check(
|
||
'匹配书库卡片显示真实笔记数徽标',
|
||
await js(`(() => {
|
||
const badge = document.querySelector(
|
||
'#libGrid .card[data-id="${retainedBook.id}"] .card-badge.note-count'
|
||
);
|
||
return !!badge && badge.textContent.trim() === '笔记 2';
|
||
})()`)
|
||
);
|
||
|
||
await js(`document.querySelector(
|
||
'#libGrid .card[data-id="${retainedBook.id}"] [data-act="remove"]'
|
||
).click()`);
|
||
await waitForModal('移除条目');
|
||
check(
|
||
'移除弹窗两个删除选项默认均未勾选',
|
||
await js(`(() => {
|
||
const files = document.getElementById('delFiles');
|
||
const reading = document.getElementById('delReadingData');
|
||
return !!files && !files.checked && !!reading && !reading.checked;
|
||
})()`)
|
||
);
|
||
check(
|
||
'移除弹窗明确说明默认保留阅读资料',
|
||
(await js("document.getElementById('modalBody').textContent"))
|
||
.includes('默认保留阅读资料,移除后仍可在「我的笔记」中查看')
|
||
);
|
||
await submitModal();
|
||
await poll(
|
||
'保留阅读资料移除书籍',
|
||
() => Promise.resolve(!library.get(retainedBook.id))
|
||
);
|
||
await pollJs(
|
||
'保留阅读资料移除后书库刷新',
|
||
`!document.querySelector('#libGrid .card[data-id="${retainedBook.id}"]')
|
||
&& document.querySelectorAll('#libGrid .card').length === 2`
|
||
);
|
||
check(
|
||
'默认移除保留该书全部笔记',
|
||
readerStore.listNotes({ entryId: retainedBook.id }).length === 2
|
||
&& fs.existsSync(FIXTURE_FILE)
|
||
);
|
||
|
||
await js(`document.querySelector('.tab[data-tab="notes"]').click()`);
|
||
await pollJs(
|
||
'默认移除后聚合笔记仍显示',
|
||
"document.querySelectorAll('#notesList .note-card').length === 3"
|
||
);
|
||
check(
|
||
'已移除书籍的笔记仍保留书名并出现在聚合页',
|
||
(await noteCards()).filter((card) => card.book === 'Retained Research Book').length === 2
|
||
&& (await js("document.getElementById('notesStatus').textContent")) === '共 3 条笔记'
|
||
);
|
||
|
||
await js(`document.querySelector('.tab[data-tab="library"]').click()`);
|
||
await pollJs(
|
||
'返回书库执行显式阅读数据删除',
|
||
`!!document.querySelector('#libGrid .card[data-id="${purgeBook.id}"]')`
|
||
);
|
||
await js(`document.querySelector(
|
||
'#libGrid .card[data-id="${purgeBook.id}"] [data-act="remove"]'
|
||
).click()`);
|
||
await waitForModal('移除条目');
|
||
check(
|
||
'无文件条目仍提供阅读数据删除且默认未勾选',
|
||
await js(`!document.getElementById('delFiles')
|
||
&& !!document.getElementById('delReadingData')
|
||
&& !document.getElementById('delReadingData').checked`)
|
||
);
|
||
await js("document.getElementById('delReadingData').checked = true");
|
||
await submitModal();
|
||
await poll(
|
||
'显式删除阅读数据完成',
|
||
() => Promise.resolve(
|
||
!library.get(purgeBook.id)
|
||
&& readerStore.listNotes({ entryId: purgeBook.id }).length === 0
|
||
)
|
||
);
|
||
check(
|
||
'勾选阅读数据删除会移除第二本书的笔记',
|
||
!library.get(purgeBook.id)
|
||
&& readerStore.listNotes({ entryId: purgeBook.id }).length === 0
|
||
);
|
||
|
||
await js(`document.querySelector('.tab[data-tab="notes"]').click()`);
|
||
await pollJs(
|
||
'显式删除阅读数据后笔记页刷新',
|
||
"document.querySelectorAll('#notesList .note-card').length === 2"
|
||
);
|
||
check(
|
||
'显式删除后聚合页仅保留默认保留的笔记',
|
||
(await noteCards()).every((card) => card.book === 'Retained Research Book')
|
||
&& !(await noteCards()).some((card) => card.title === 'Purge With Reading Data')
|
||
&& (await js("document.getElementById('notesStatus').textContent")) === '共 2 条笔记'
|
||
);
|
||
|
||
await js("document.getElementById('addGlobalNoteBtn').click()");
|
||
await waitForModal('选择笔记类型');
|
||
await js(`(() => {
|
||
document.querySelector('input[name="newNoteType"][value="canvas"]').checked = true;
|
||
document.getElementById('modalOk').click();
|
||
})()`);
|
||
await waitForModal('新建画布笔记');
|
||
await pollJs(
|
||
'独立画布笔记编辑器加载完成',
|
||
"!!document.querySelector('#newNoteRich .canvas-note-root')",
|
||
15000
|
||
);
|
||
check(
|
||
'主窗口画布仅工作区滚动且工具栏使用一致的图标按钮',
|
||
await js(`(() => {
|
||
const viewport = document.querySelector('#newNoteRich .canvas-note-viewport');
|
||
const toolbar = document.querySelector('#newNoteRich .canvas-note-toolbar');
|
||
const buttons = [...toolbar.querySelectorAll('.canvas-note-button')];
|
||
const scrollables = [];
|
||
for (let node = viewport; node && node.id !== 'modal'; node = node.parentElement) {
|
||
const style = getComputedStyle(node);
|
||
if (/auto|scroll/.test(style.overflowX) || /auto|scroll/.test(style.overflowY)) {
|
||
scrollables.push(node);
|
||
}
|
||
}
|
||
return scrollables.length === 1
|
||
&& scrollables[0] === viewport
|
||
&& getComputedStyle(document.getElementById('modalBody')).overflowY === 'hidden'
|
||
&& getComputedStyle(toolbar).flexWrap === 'wrap'
|
||
&& getComputedStyle(toolbar).overflowX === 'visible'
|
||
&& buttons.length >= 10
|
||
&& buttons.every((button) => !!button.querySelector('.canvas-note-icon'))
|
||
&& viewport.clientHeight > 0;
|
||
})()`)
|
||
);
|
||
check(
|
||
'新建画布笔记允许选择不关联书籍',
|
||
await js(`(() => {
|
||
const select = document.getElementById('newNoteBook');
|
||
return !!select && select.value === ''
|
||
&& select.options[0].textContent === '不关联书籍';
|
||
})()`)
|
||
);
|
||
await js(`(() => {
|
||
document.querySelector('#newNoteRich [data-tool="flow-text"]').click();
|
||
const quill = Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill'));
|
||
const text = Array.from({ length: 90 }, (_, index) =>
|
||
\`第 \${index + 1} 段全局正文会在纸张边界自动流入下一页。\`
|
||
).join('\\n');
|
||
quill.setText(text, 'user');
|
||
quill.setSelection(quill.getLength() - 1, 0, 'silent');
|
||
})()`);
|
||
await pollJs(
|
||
'全局文本超过当前纸张后自动分页',
|
||
`Number(document.querySelector('#newNoteRich .canvas-note-page-counter')
|
||
.textContent.split('/')[1]) > 1`,
|
||
15000
|
||
);
|
||
await js("document.querySelector('#newNoteRich .canvas-note-undo').click()");
|
||
await pollJs(
|
||
'全局文本使用画布统一撤销',
|
||
`Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill')).getText().trim() === ''`,
|
||
10000
|
||
);
|
||
await js("document.querySelector('#newNoteRich .canvas-note-redo').click()");
|
||
await pollJs(
|
||
'全局文本使用画布统一重做',
|
||
`Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill'))
|
||
.getText().includes('第 90 段全局正文')`,
|
||
10000
|
||
);
|
||
await js(`(() => {
|
||
const quill = Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill'));
|
||
quill.formatText(0, 8, 'bold', true, 'user');
|
||
quill.formatLine(0, 1, 'header', 1, 'user');
|
||
})()`);
|
||
const flowBeforePageOperations = await js(`(() => {
|
||
const quill = Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill'));
|
||
return {
|
||
text: quill.getText(),
|
||
pages: Number(document.querySelector('#newNoteRich .canvas-note-page-counter')
|
||
.textContent.split('/')[1])
|
||
};
|
||
})()`);
|
||
await js(`(() => {
|
||
const quill = Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill'));
|
||
quill.setSelection(300, 0, 'silent');
|
||
document.querySelector('#newNoteRich .canvas-note-add-page').click();
|
||
})()`);
|
||
await pollJs(
|
||
'全局文本添加页面写入显式分页符',
|
||
`Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill'))
|
||
.getContents().ops.some((op) => op.insert && op.insert.canvasPageBreak)`,
|
||
10000
|
||
);
|
||
await wait(600);
|
||
await pollJs(
|
||
'添加显式页面操作完成',
|
||
`!document.querySelector('#newNoteRich .canvas-note-delete-page').disabled`,
|
||
10000
|
||
);
|
||
check(
|
||
'显式分页符把后续正文移动到下一张纸',
|
||
await js(`(() => {
|
||
const pageBreak = document.querySelector('#newNoteRich .canvas-flow-page-break');
|
||
const nextBlock = pageBreak?.nextElementSibling;
|
||
return !!nextBlock
|
||
&& nextBlock.getBoundingClientRect().left
|
||
- pageBreak.getBoundingClientRect().left > 300;
|
||
})()`)
|
||
);
|
||
await js("document.querySelector('#newNoteRich .canvas-note-delete-page').click()");
|
||
await pollJs(
|
||
'删除空白显式页面仅移除分页符',
|
||
`!Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill'))
|
||
.getContents().ops.some((op) => op.insert && op.insert.canvasPageBreak)`,
|
||
10000
|
||
);
|
||
const flowAfterPageOperations = await js(`(() => {
|
||
const quill = Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill'));
|
||
return quill.getText();
|
||
})()`);
|
||
check(
|
||
'添加和删除页面不会删除全局正文',
|
||
flowAfterPageOperations === flowBeforePageOperations.text,
|
||
`${flowBeforePageOperations.text.length} -> ${flowAfterPageOperations.length}`
|
||
);
|
||
await js(`(() => {
|
||
const quill = Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill'));
|
||
quill.setSelection(600, 0, 'silent');
|
||
document.querySelector('#newNoteRich .canvas-note-add-page').click();
|
||
})()`);
|
||
await pollJs(
|
||
'显式分页符保留到保存内容',
|
||
`Quill.find(document.querySelector('#newNoteRich .canvas-flow-quill'))
|
||
.getContents().ops.some((op) => op.insert && op.insert.canvasPageBreak)`,
|
||
10000
|
||
);
|
||
await wait(600);
|
||
await pollJs(
|
||
'再次添加显式页面操作完成',
|
||
`!document.querySelector('#newNoteRich .canvas-note-import-pdf').disabled`,
|
||
10000
|
||
);
|
||
await js(`(() => {
|
||
document.getElementById('newNoteTitle').value = 'Standalone Note';
|
||
document.getElementById('newNoteTags').value = 'standalone';
|
||
})()`);
|
||
const originalPdfPicker = dialog.showOpenDialog;
|
||
dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [FIXTURE_PDF] });
|
||
await js("document.querySelector('#newNoteRich .canvas-note-import-pdf').click()");
|
||
await pollJs(
|
||
'PDF 导入为可标注底版',
|
||
`document.querySelector('#newNoteRich .canvas-note-template').value === '__pdf'
|
||
&& document.querySelector('#newNoteRich .canvas-note-background').width > 0
|
||
&& !document.querySelector('#newNoteRich .canvas-note-export-pdf').disabled`,
|
||
20000
|
||
);
|
||
dialog.showOpenDialog = originalPdfPicker;
|
||
|
||
const exportedCanvasPdf = path.join(TMP, 'exported-canvas-note.pdf');
|
||
const originalSaveDialog = dialog.showSaveDialog;
|
||
dialog.showSaveDialog = async () => ({ canceled: false, filePath: exportedCanvasPdf });
|
||
await js("document.querySelector('#newNoteRich .canvas-note-export-pdf').click()");
|
||
await poll(
|
||
'自由画布导出有效 PDF',
|
||
async () => {
|
||
if (fs.existsSync(exportedCanvasPdf)
|
||
&& fs.readFileSync(exportedCanvasPdf).subarray(0, 5).toString() === '%PDF-') {
|
||
return true;
|
||
}
|
||
const error = await js("document.getElementById('newNoteError').textContent");
|
||
if (error) throw new Error(error);
|
||
return false;
|
||
},
|
||
20000
|
||
);
|
||
dialog.showSaveDialog = originalSaveDialog;
|
||
await submitModal();
|
||
let standaloneNote = null;
|
||
await poll(
|
||
'无关联笔记写入独立存储',
|
||
() => {
|
||
standaloneNote = readerStore.listNotes({
|
||
entryId: readerStore.STANDALONE_ENTRY_ID
|
||
}).find((note) => note.title === 'Standalone Note') || null;
|
||
return Promise.resolve(!!standaloneNote);
|
||
}
|
||
);
|
||
await pollJs(
|
||
'无关联笔记渲染完成',
|
||
`Array.from(document.querySelectorAll('#notesList .note-title'))
|
||
.some((title) => title.textContent === 'Standalone Note')`
|
||
);
|
||
await js(`(() => {
|
||
const title = Array.from(document.querySelectorAll('#notesList .note-title'))
|
||
.find((item) => item.textContent === 'Standalone Note');
|
||
title.closest('.note-card').querySelector('.note-action.edit').click();
|
||
})()`);
|
||
await waitForModal('编辑画布笔记');
|
||
await pollJs(
|
||
'重开画布笔记恢复全局正文和显式分页符',
|
||
`Quill.find(document.querySelector('#noteEditRich .canvas-flow-quill'))
|
||
.getText().includes('第 90 段全局正文')
|
||
&& !!document.querySelector('#noteEditRich .canvas-flow-page-break')`,
|
||
15000
|
||
);
|
||
await js("document.getElementById('modalCancel').click()");
|
||
await pollJs('关闭重开画布笔记弹窗', "document.getElementById('modal').classList.contains('hidden')");
|
||
check(
|
||
'无关联笔记显示明确状态且不提供原文按钮',
|
||
standaloneNote.associated === false
|
||
&& standaloneNote.noteType === 'canvas'
|
||
&& standaloneNote.canvasContent.version === 2
|
||
&& standaloneNote.canvasContent.flow.ops.some((op) => (
|
||
typeof op.insert === 'string' && op.insert.includes('全局正文')
|
||
))
|
||
&& standaloneNote.canvasContent.flow.ops.some((op) => op.attributes?.bold === true)
|
||
&& standaloneNote.canvasContent.flow.ops.some((op) => op.attributes?.header === 1)
|
||
&& standaloneNote.canvasContent.flow.ops.some((op) => (
|
||
op.insert?.canvasPageBreak
|
||
&& standaloneNote.canvasContent.pages.some(
|
||
(page) => page.id === op.insert.canvasPageBreak
|
||
)
|
||
))
|
||
&& standaloneNote.canvasContent.pages.some((page) => page.background.type === 'pdf')
|
||
&& /^pdf_[a-f0-9]{64}$/.test(
|
||
standaloneNote.canvasContent.pages.find(
|
||
(page) => page.background.type === 'pdf'
|
||
).background.assetId
|
||
)
|
||
&& !Object.prototype.hasOwnProperty.call(
|
||
standaloneNote.canvasContent.pages.find(
|
||
(page) => page.background.type === 'pdf'
|
||
).background,
|
||
'draftToken'
|
||
)
|
||
&& readerStore.noteAssetIds().includes(
|
||
standaloneNote.canvasContent.pages.find(
|
||
(page) => page.background.type === 'pdf'
|
||
).background.assetId
|
||
)
|
||
&& (await noteCards()).some((card) => (
|
||
card.title === 'Standalone Note' && card.book === '未关联书籍'
|
||
))
|
||
&& await js(`(() => {
|
||
const title = Array.from(document.querySelectorAll('#notesList .note-title'))
|
||
.find((item) => item.textContent === 'Standalone Note');
|
||
const card = title && title.closest('.note-card');
|
||
return !!card && !card.querySelector('.note-action.open');
|
||
})()`)
|
||
);
|
||
await js(`document.querySelector('#notesTypeTabs [data-note-type="canvas"]').click()`);
|
||
check(
|
||
'画布笔记 Tab 只显示画布卡片',
|
||
(await noteCards()).length === 1
|
||
&& (await noteCards())[0].title === 'Standalone Note'
|
||
&& (await noteCards())[0].type === '画布笔记'
|
||
&& await js(`!!document.querySelector('#notesList .note-canvas-preview')`)
|
||
);
|
||
await js(`document.querySelector('#notesTypeTabs [data-note-type="reading"]').click()`);
|
||
check(
|
||
'读书笔记 Tab 隐藏画布卡片',
|
||
(await noteCards()).length === 2
|
||
&& (await noteCards()).every((card) => card.type === '读书笔记')
|
||
&& (await noteCards()).some((card) => card.title === 'Edited Research Note')
|
||
);
|
||
await js(`document.querySelector('#notesTypeTabs [data-note-type=""]').click()`);
|
||
|
||
const importRoot = path.join(TMP, 'existing-folder-layout');
|
||
const literatureDir = path.join(importRoot, '旧文学分类');
|
||
const technologyDir = path.join(importRoot, '旧技术分类');
|
||
fs.mkdirSync(literatureDir, { recursive: true });
|
||
fs.mkdirSync(technologyDir, { recursive: true });
|
||
fs.writeFileSync(path.join(literatureDir, 'Local Novel.epub'), 'epub fixture');
|
||
fs.writeFileSync(path.join(technologyDir, 'Local Manual.pdf'), 'pdf fixture');
|
||
const originalShowOpenDialog = dialog.showOpenDialog;
|
||
dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [importRoot] });
|
||
await js("document.getElementById('addLocalBtn').click()");
|
||
await waitForModal('添加本地内容');
|
||
await js(`(() => {
|
||
document.querySelector('input[name="localImportSource"][value="folder"]').checked = true;
|
||
document.getElementById('modalOk').click();
|
||
})()`);
|
||
await waitForModal('导入本地图书');
|
||
check(
|
||
'本地导入支持递归文件夹和上级目录组织选项',
|
||
await js(`document.getElementById('modalBody').textContent.includes('发现 2 个支持的图书文件')
|
||
&& !!document.querySelector('input[name="localImportOrganization"][value="shelf"]')
|
||
&& !!document.querySelector('input[name="localImportOrganization"][value="tag"]')`)
|
||
);
|
||
await js(`document.querySelector(
|
||
'input[name="localImportOrganization"][value="shelf"]'
|
||
).checked = true`);
|
||
await submitModal();
|
||
await poll(
|
||
'文件夹导入写入书库和上级目录书架',
|
||
() => Promise.resolve(
|
||
!!library.list().find((item) => item.title === 'Local Novel')
|
||
&& !!library.list().find((item) => item.title === 'Local Manual')
|
||
&& library.listShelves().some((shelf) => shelf.name === '旧文学分类')
|
||
&& library.listShelves().some((shelf) => shelf.name === '旧技术分类')
|
||
)
|
||
);
|
||
const localNovel = library.list().find((item) => item.title === 'Local Novel');
|
||
const literatureShelf = library.listShelves().find((shelf) => shelf.name === '旧文学分类');
|
||
check(
|
||
'文件的上一级目录被复用为书架分类',
|
||
localNovel.shelfId === literatureShelf.id
|
||
&& library.list().filter((item) => item.importedByLocal).length === 2
|
||
);
|
||
const repeatSelection = await js("window.api.library.pickLocal('folder')");
|
||
const repeatId = JSON.stringify(repeatSelection.data.selectionId);
|
||
const repeatImport = await js(
|
||
`window.api.library.importLocal(${repeatId}, { organization: 'shelf' })`
|
||
);
|
||
const replayImport = await js(
|
||
`window.api.library.importLocal(${repeatId}, { organization: 'shelf' })`
|
||
);
|
||
check(
|
||
'重复文件被跳过且本地选择令牌不能重放',
|
||
repeatImport.ok
|
||
&& repeatImport.data.added === 0
|
||
&& repeatImport.data.skipped === 2
|
||
&& !replayImport.ok
|
||
&& replayImport.error.includes('已失效')
|
||
);
|
||
dialog.showOpenDialog = originalShowOpenDialog;
|
||
|
||
// --- 笔记独立窗口 ---
|
||
await js(`document.querySelector('.tab[data-tab="notes"]').click()`);
|
||
await pollJs('笔记页有可开窗的卡片', "document.querySelectorAll('#notesList .note-card').length > 0");
|
||
const windowNote = readerStore.listNotes({}).find((note) => note.associated !== false);
|
||
check('存在可用于开窗的笔记', !!windowNote);
|
||
|
||
// 只数笔记窗口。总窗口数会被阅读器窗口的开关干扰,
|
||
// 之前用总数当基线,阅读器中途关掉就把「没多开窗」误判成失败
|
||
const noteWindowCount = () => BrowserWindow.getAllWindows()
|
||
.filter((item) => !item.isDestroyed() && String(item.webContents.getURL()).includes('note.html'))
|
||
.length;
|
||
check('开窗前没有笔记窗口', noteWindowCount() === 0, `笔记窗口数=${noteWindowCount()}`);
|
||
// 按钮必须限定在目标笔记那张卡片内:笔记页此时有多张卡片,
|
||
// 全局找「编辑」会命中别的卡片,断言就变成了自欺欺人
|
||
const cardScript = (inner) => `(() => {
|
||
const target = document.querySelector('#notesList .note-card[data-note-id=' +
|
||
JSON.stringify(${JSON.stringify(String(windowNote.id))}) + ']');
|
||
if (!target) throw new Error('找不到目标笔记卡片');
|
||
${inner}
|
||
})()`;
|
||
const cardLabels = () => js(cardScript(
|
||
'return [...target.querySelectorAll(".note-action")].map((b) => b.textContent).join(",");'
|
||
));
|
||
const clickCardAction = (label) => js(cardScript(`
|
||
const button = [...target.querySelectorAll(".note-action")]
|
||
.find((item) => item.textContent === ${JSON.stringify(label)});
|
||
if (!button) throw new Error("找不到按钮:" + ${JSON.stringify(label)});
|
||
button.click();
|
||
return true;
|
||
`));
|
||
|
||
await clickCardAction('独立窗口');
|
||
// 窗口创建与 URL 就位之间有间隔,刚建好时 getURL() 还是空串,必须轮询
|
||
const findNoteWindow = () => BrowserWindow.getAllWindows()
|
||
.find((item) => !item.isDestroyed() && String(item.webContents.getURL()).includes('note.html'));
|
||
await poll(
|
||
'点击独立窗口后真的多出一个笔记窗口',
|
||
async () => noteWindowCount() === 1 && !!findNoteWindow(),
|
||
15000
|
||
);
|
||
const noteWin = findNoteWindow();
|
||
check('新窗口加载的是笔记页面', !!noteWin);
|
||
const noteJs = (source) => noteWin.webContents.executeJavaScript(source);
|
||
// 多标签后表单是每个标签一份,查询必须限定在当前激活的那个视图内,
|
||
// 否则回收/切换过程中会量到别的标签
|
||
const activeScript = (inner) => `(() => {
|
||
const view = [...document.querySelectorAll('.note-tab-view')]
|
||
.find((item) => !item.classList.contains('inactive'));
|
||
if (!view) throw new Error('没有激活的笔记标签');
|
||
${inner}
|
||
})()`;
|
||
await poll(
|
||
'笔记窗口标签就绪',
|
||
() => noteJs(`(() => {
|
||
const view = [...document.querySelectorAll('.note-tab-view')]
|
||
.find((item) => !item.classList.contains('inactive'));
|
||
return !!(view && view.querySelector('.note-window-title'));
|
||
})()`),
|
||
15000
|
||
);
|
||
check(
|
||
'笔记窗口载入的是被点开的那一条',
|
||
(await noteJs(activeScript("return view.querySelector('.note-window-title').value;")))
|
||
=== String(windowNote.title || '')
|
||
&& (await noteJs("document.getElementById('noteWindowError').textContent")) === '',
|
||
await noteJs(activeScript("return view.querySelector('.note-window-title').value;"))
|
||
);
|
||
check(
|
||
'开出的是一个标签',
|
||
(await noteJs("document.querySelectorAll('.doctab').length")) === 1,
|
||
`标签数=${await noteJs("document.querySelectorAll('.doctab').length")}`
|
||
);
|
||
|
||
// 同一条笔记不允许开出第二个标签,否则两个编辑器会整条覆盖对方
|
||
await clickCardAction('切到窗口');
|
||
await wait(1200);
|
||
check(
|
||
'同一条笔记再次开窗只聚焦不新增窗口',
|
||
noteWindowCount() === 1,
|
||
`笔记窗口数=${noteWindowCount()}`
|
||
);
|
||
check(
|
||
'同一条笔记再次开窗也不新增标签',
|
||
(await noteJs("document.querySelectorAll('.doctab').length")) === 1,
|
||
`标签数=${await noteJs("document.querySelectorAll('.doctab').length")}`
|
||
);
|
||
|
||
// 已开窗时该卡片的按钮改为切窗,避免模态与窗口同时编辑同一条
|
||
const openedLabels = await cardLabels();
|
||
check(
|
||
'已开窗后该笔记不再提供开模态的编辑按钮',
|
||
openedLabels.includes('在窗口中编辑') && !openedLabels.split(',').includes('编辑'),
|
||
openedLabels
|
||
);
|
||
await clickCardAction('在窗口中编辑');
|
||
await wait(1000);
|
||
check(
|
||
'点「在窗口中编辑」不会打开模态',
|
||
await js("document.getElementById('modal').classList.contains('hidden')")
|
||
);
|
||
|
||
// 断言真正落盘的内容,而不是界面状态
|
||
const editedTitle = `窗口改名 ${Date.now()}`;
|
||
await noteJs(activeScript(`
|
||
const title = view.querySelector('.note-window-title');
|
||
title.value = ${JSON.stringify(editedTitle)};
|
||
title.dispatchEvent(new Event('input', { bubbles: true }));
|
||
const tags = view.querySelector('.note-window-tags-input');
|
||
tags.value = '窗口标签';
|
||
tags.dispatchEvent(new Event('input', { bubbles: true }));
|
||
return true;
|
||
`));
|
||
// 有未保存修改时标签上要有脏标记,否则关闭前的二次确认无从触发
|
||
await poll(
|
||
'未保存的修改在标签上有脏标记',
|
||
() => noteJs("document.querySelectorAll('.doctab-dirty').length === 1"),
|
||
8000
|
||
);
|
||
check('未保存的修改在标签上有脏标记', true);
|
||
await noteJs(activeScript("view.querySelector('.note-window-save').click(); return true;"));
|
||
await poll(
|
||
'笔记窗口的修改真正落盘',
|
||
async () => {
|
||
const stored = readerStore.listNotes({}).find((note) => note.id === windowNote.id);
|
||
return !!stored && stored.title === editedTitle && stored.tags.includes('窗口标签');
|
||
},
|
||
12000
|
||
);
|
||
check('笔记窗口保存后落盘内容正确', true);
|
||
|
||
await poll(
|
||
'笔记窗口的改动回流到主窗口列表',
|
||
() => js(`Array.from(document.querySelectorAll('#notesList .note-title'))
|
||
.some((node) => node.textContent.trim() === ${JSON.stringify(editedTitle)})`),
|
||
12000
|
||
);
|
||
check('主窗口列表随笔记窗口保存刷新', true);
|
||
await poll(
|
||
'保存后脏标记清除',
|
||
() => noteJs("document.querySelectorAll('.doctab-dirty').length === 0"),
|
||
8000
|
||
);
|
||
check('保存后脏标记清除', true);
|
||
|
||
const noteWinErrors = [];
|
||
noteWin.webContents.on('console-message', (event) => {
|
||
if (event.level >= 2) noteWinErrors.push(event.message.slice(0, 120));
|
||
});
|
||
await wait(200);
|
||
check('笔记窗口没有控制台错误', noteWinErrors.length === 0, noteWinErrors.slice(0, 2).join(' | '));
|
||
|
||
// 第二条笔记要进同一个窗口的新标签,而不是再开一个窗口
|
||
const secondNote = readerStore.listNotes({})
|
||
.find((note) => note.id !== windowNote.id && note.associated !== false);
|
||
if (secondNote) {
|
||
await js(`(() => {
|
||
const target = document.querySelector('#notesList .note-card[data-note-id=' +
|
||
JSON.stringify(${JSON.stringify(String(secondNote.id))}) + ']');
|
||
if (!target) throw new Error('找不到第二条笔记卡片');
|
||
const button = [...target.querySelectorAll('.note-action')]
|
||
.find((item) => item.textContent === '独立窗口');
|
||
if (!button) throw new Error('第二条笔记没有独立窗口按钮');
|
||
button.click();
|
||
return true;
|
||
})()`);
|
||
await poll(
|
||
'第二条笔记进入同一窗口的新标签',
|
||
async () => noteWindowCount() === 1
|
||
&& (await noteJs("document.querySelectorAll('.doctab').length")) === 2,
|
||
15000
|
||
);
|
||
check(
|
||
'第二条笔记进入同一窗口的新标签',
|
||
noteWindowCount() === 1,
|
||
`笔记窗口数=${noteWindowCount()}`
|
||
);
|
||
check(
|
||
'只有一个标签视图可见',
|
||
(await noteJs(
|
||
"[...document.querySelectorAll('.note-tab-view')].filter((v) => !v.classList.contains('inactive')).length"
|
||
)) === 1
|
||
);
|
||
// 刚打开且只在编辑区点选、按方向键,不改内容,不能被判定为已修改。
|
||
// 早前用 pointerdown/keydown 判脏时这里必然误报,一切标签都要求二次确认;
|
||
// 画布还会因为 version 1→2 归一化在挂载时就"变更"一次
|
||
await wait(1200);
|
||
await noteJs(activeScript(`
|
||
const host = view.querySelector('.note-window-editor');
|
||
host.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
|
||
host.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }));
|
||
host.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'ArrowRight' }));
|
||
host.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'ArrowRight' }));
|
||
return true;
|
||
`));
|
||
await wait(900);
|
||
check(
|
||
'只点选不改内容不会被误判为已修改',
|
||
(await noteJs("document.querySelectorAll('.doctab-dirty').length")) === 0,
|
||
`脏标记数=${await noteJs("document.querySelectorAll('.doctab-dirty').length")}`
|
||
);
|
||
}
|
||
|
||
// 关窗前的未保存拦截:取消之后必须还能再次触发确认。
|
||
// 主进程 closePending 不复位时第二次点关闭会被静默忽略,
|
||
// 而看门狗十秒后仍会把带未保存内容的窗口销毁。
|
||
// 此时激活的是第二条笔记的标签,必须先切回已保存过的那条,
|
||
// 否则下面"改回原样"比对的是另一条笔记的基线
|
||
await noteJs(`(() => {
|
||
const tab = document.querySelector('.doctab[data-note-id=' +
|
||
JSON.stringify(${JSON.stringify(String(windowNote.id))}) + ']');
|
||
if (!tab) throw new Error('找不到目标标签');
|
||
tab.click();
|
||
return true;
|
||
})()`);
|
||
await poll(
|
||
'切回已保存的那个标签',
|
||
() => noteJs(activeScript(
|
||
`return view.dataset.noteId === ${JSON.stringify(String(windowNote.id))};`
|
||
)),
|
||
10000
|
||
);
|
||
await noteJs(activeScript(`
|
||
const title = view.querySelector('.note-window-title');
|
||
title.value = '关窗前的未保存修改';
|
||
title.dispatchEvent(new Event('input', { bubbles: true }));
|
||
return true;
|
||
`));
|
||
await poll(
|
||
'关窗前已置脏',
|
||
() => noteJs("document.querySelectorAll('.doctab-dirty').length >= 1"),
|
||
8000
|
||
);
|
||
await noteJs("document.getElementById('closeBtn').click()");
|
||
await poll(
|
||
'关窗被未保存确认拦下',
|
||
() => noteJs("!document.getElementById('noteDirtyModal').classList.contains('hidden')"),
|
||
10000
|
||
);
|
||
check('关窗被未保存确认拦下,窗口还在', !noteWin.isDestroyed());
|
||
await noteJs("document.getElementById('noteDirtyCancelBtn').click()");
|
||
await wait(1000);
|
||
check('取消后窗口保留', !noteWin.isDestroyed());
|
||
await noteJs("document.getElementById('closeBtn').click()");
|
||
await poll(
|
||
'取消之后再次关窗仍会弹确认',
|
||
() => noteJs("!document.getElementById('noteDirtyModal').classList.contains('hidden')"),
|
||
10000
|
||
);
|
||
check('取消之后再次关窗仍会弹确认', !noteWin.isDestroyed());
|
||
await noteJs("document.getElementById('noteDirtyCancelBtn').click()");
|
||
await wait(800);
|
||
// 存盘收尾,避免未保存状态干扰后面的删除断言
|
||
await noteJs(activeScript("view.querySelector('.note-window-save').click(); return true;"));
|
||
await poll(
|
||
'取消关闭后仍能正常保存',
|
||
() => noteJs(`(() => {
|
||
const tab = document.querySelector('.doctab[data-note-id=' +
|
||
JSON.stringify(${JSON.stringify(String(windowNote.id))}) + ']');
|
||
return !!tab && !tab.querySelector('.doctab-dirty');
|
||
})()`),
|
||
10000
|
||
);
|
||
check('取消关闭后仍能正常保存', true);
|
||
|
||
// 笔记被删除后只关掉它那个标签,窗口和别的标签要留着
|
||
await js(`window.api.reader.removeNote(${JSON.stringify(windowNote.entryId)}, ${JSON.stringify(windowNote.id)})`);
|
||
if (secondNote) {
|
||
await poll(
|
||
'删除笔记只关掉对应标签',
|
||
async () => !noteWin.isDestroyed()
|
||
&& (await noteJs("document.querySelectorAll('.doctab').length")) === 1,
|
||
12000
|
||
);
|
||
check('删除笔记只关掉对应标签,窗口留着', !noteWin.isDestroyed());
|
||
// 删掉最后一个标签,窗口才该退场
|
||
const remainingId = await noteJs("document.querySelector('.doctab').dataset.noteId");
|
||
const remaining = readerStore.listNotes({}).find((note) => note.id === remainingId);
|
||
check('剩下的标签是另一条笔记', !!remaining && remaining.id !== windowNote.id, String(remainingId));
|
||
if (remaining) {
|
||
await js(`window.api.reader.removeNote(${JSON.stringify(remaining.entryId)}, ${JSON.stringify(remaining.id)})`);
|
||
}
|
||
}
|
||
await poll('最后一个标签消失后窗口自动关闭', async () => noteWin.isDestroyed(), 12000);
|
||
check('最后一个标签消失后窗口自动关闭', noteWin.isDestroyed());
|
||
check('窗口关闭后主进程标签集清空', noteWindow.openIds().length === 0, JSON.stringify(noteWindow.openIds()));
|
||
|
||
await wait(300);
|
||
check(
|
||
'主渲染进程没有控制台错误',
|
||
rendererErrors.length === 0,
|
||
rendererErrors.slice(0, 3).join(' | ')
|
||
);
|
||
|
||
finish();
|
||
}).catch((error) => {
|
||
console.error('异常:', error && error.stack ? error.stack : error);
|
||
check('集成脚本无未处理异常', false, error && error.message ? error.message : String(error));
|
||
finish();
|
||
});
|