Files
readest/apps/readest-app/src/__tests__/components/ReviewModeController.test.ts
T
2026-07-09 14:15:36 +08:00

148 lines
4.6 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import { __testing } from '@/app/reader/components/ReviewModeController';
import type { BookDoc } from '@/libs/document';
describe('ReviewModeController marks', () => {
test('marks bilingual source and target paragraphs by file and index', () => {
const doc = document.implementation.createHTMLDocument('chapter');
doc.body.innerHTML = [
'<p class="sourceText">原文一</p>',
'<p>译文一</p>',
'<p class="sourceText">原文二</p>',
'<p>译文二</p>',
].join('');
__testing.applyReviewMarks(
doc,
'OEBPS/Text/chapter.xhtml',
[
{
id: 'R00002',
file: 'Text/chapter.xhtml',
ja_p_index: 2,
cn_p_index: 3,
jp_html: '原文二',
jp_text: '原文二',
cn_html: '译文二',
current_html: '<span>新译文二</span><script>alert(1)</script>',
},
],
'R00002',
);
const paragraphs = Array.from(doc.querySelectorAll('p'));
expect(paragraphs[2]?.getAttribute('data-readest-review-row-id')).toBe('R00002');
expect(paragraphs[2]?.classList.contains('readest-review-source')).toBe(true);
expect(paragraphs[3]?.getAttribute('data-readest-review-row-id')).toBe('R00002');
expect(paragraphs[3]?.classList.contains('readest-review-target')).toBe(true);
expect(paragraphs[3]?.innerHTML).toBe('<span>新译文二</span>');
});
test('uses section id when foliate sections do not expose href', () => {
const bookDoc = {
sections: [{ id: 'OEBPS/Text/chapter.xhtml' }],
} as unknown as BookDoc;
expect(__testing.sectionHrefAt(bookDoc, 0)).toBe('OEBPS/Text/chapter.xhtml');
});
test('clearReviewMarks restores original target HTML', () => {
const doc = document.implementation.createHTMLDocument('chapter');
doc.body.innerHTML = '<p class="sourceText">原文</p><p><ruby>旧<rt>old</rt></ruby>译文</p>';
__testing.applyReviewMarks(
doc,
'chapter.xhtml',
[
{
id: 'R00001',
file: 'chapter.xhtml',
ja_p_index: 0,
cn_p_index: 1,
jp_html: '原文',
jp_text: '原文',
cn_html: '<ruby>旧<rt>old</rt></ruby>译文',
current_html: '<span>新译文</span>',
},
],
'',
);
__testing.clearReviewMarks(doc);
const target = doc.querySelectorAll('p')[1];
expect(target?.innerHTML).toBe('<ruby>旧<rt>old</rt></ruby>译文');
expect(target?.hasAttribute('data-readest-review-row-id')).toBe(false);
});
test('strips event handlers and dangerous links from target HTML', () => {
const doc = document.implementation.createHTMLDocument('chapter');
doc.body.innerHTML = '<p>原文</p><p>译文</p>';
__testing.applyReviewMarks(
doc,
'chapter.xhtml',
[
{
id: 'R00001',
file: 'chapter.xhtml',
ja_p_index: 0,
cn_p_index: 1,
jp_html: '原文',
jp_text: '原文',
cn_html: '译文',
current_html: '<span onclick="alert(1)"><a href="javascript:alert(1)">新译文</a></span>',
},
],
'R00001',
);
const target = doc.querySelectorAll('p')[1];
expect(target?.innerHTML).toBe('<span>新译文</span>');
});
test('updates active classes without rewriting translated paragraph HTML', () => {
const doc = document.implementation.createHTMLDocument('chapter');
doc.body.innerHTML = '<p>原文一</p><p>译文一</p><p>原文二</p><p>译文二</p>';
__testing.applyReviewMarks(
doc,
'chapter.xhtml',
[
{
id: 'R00001',
file: 'chapter.xhtml',
ja_p_index: 0,
cn_p_index: 1,
jp_html: '原文一',
jp_text: '原文一',
cn_html: '译文一',
current_html: '<span>新译文一</span>',
},
{
id: 'R00002',
file: 'chapter.xhtml',
ja_p_index: 2,
cn_p_index: 3,
jp_html: '原文二',
jp_text: '原文二',
cn_html: '译文二',
current_html: '<span>新译文二</span>',
},
],
'R00001',
);
const target = doc.querySelectorAll('p')[1]!;
target.dataset['localState'] = 'kept';
__testing.applyActiveReviewMark(doc, 'R00002');
expect(target.innerHTML).toBe('<span>新译文一</span>');
expect(target.dataset['localState']).toBe('kept');
expect(target.classList.contains('readest-review-active')).toBe(false);
expect(doc.querySelectorAll('p')[3]?.classList.contains('readest-review-active')).toBe(true);
});
});