From 1ec94bf5e7661536320cdb4b89ce2a688b53f2f9 Mon Sep 17 00:00:00 2001 From: zhanghongyuan Date: Fri, 9 Oct 2026 09:46:06 +0800 Subject: [PATCH] test: add tests for deepin-reader batch 2 (20 bugs, 38 cases) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated by qt-autotest-generator skill (PMS mining, Mode 7 -> Mode 2). Batch 2: SheetBrowser, BrowserPage, BrowserWord, BookMarkWidget, CentralDocPage, DocTabBar, TitleWidget, Application, PDFModel/DPdfDoc. Baseline: master @ 9061ecbe "fix: walk the parent chain safely in CentralDocPage" (2026-10-08) Log: 补充 deepin-reader PMS 批次 2 单元测试(38 个回归用例) Influence: 新增 20 个 sev1/2 bug 的回归用例 38 个,38/38 通过; 全量 1264 用例 1263 通过(1 个存量 flaky 与本批次无关) 依据 PMS bug 修复历史补强,按文件分布: - tests/document/ut_pdfmodel.cpp +10 (46911/54210/68550/304111/ 304471/351271/353429) - tests/browser/ut_sheetbrowser.cpp +10 (22093/43216/44136/44264/ 44491/41018) - tests/sidebar/ut_bookmarkwidget.cpp +5 (10332/12219) - tests/uiframe/ut_centraldocpage.cpp +3 (20978) - tests/ut_application.cpp +3 (44137/44264) - tests/browser/ut_browserword.cpp +2 (40946) - tests/browser/ut_browserpage.cpp +2 (25004) - tests/uiframe/ut_doctabbar.cpp +2 (46381) - tests/uiframe/ut_titlewidget.cpp +1 (44136) 覆盖率(lcov, reader/ 范围):lines 84.7% (13106/15471), functions 98.1% (1364/1390);21/21 目标函数 FNDA 全命中。 另 2 个 bug(102352/347387)位于 htmltopdf main() 内 __sw_64__ 条件编译,x86 构建不可达,需申威环境手工验证。 测试中新发现的源码缺陷留本地记录,不入库。 --- tests/browser/ut_browserpage.cpp | 57 +++++++ tests/browser/ut_browserword.cpp | 36 ++++ tests/browser/ut_sheetbrowser.cpp | 252 ++++++++++++++++++++++++++++ tests/document/ut_pdfmodel.cpp | 195 +++++++++++++++++++++ tests/sidebar/ut_bookmarkwidget.cpp | 88 ++++++++++ tests/uiframe/ut_centraldocpage.cpp | 65 +++++++ tests/uiframe/ut_doctabbar.cpp | 43 +++++ tests/uiframe/ut_titlewidget.cpp | 40 +++++ tests/ut_application.cpp | 42 +++++ 9 files changed, 818 insertions(+) diff --git a/tests/browser/ut_browserpage.cpp b/tests/browser/ut_browserpage.cpp index f5171d532..0808763d5 100644 --- a/tests/browser/ut_browserpage.cpp +++ b/tests/browser/ut_browserpage.cpp @@ -866,3 +866,60 @@ TEST_F(TestBrowserPage, UT_BrowserPage_applyNightMode_001) EXPECT_EQ(qGreen(darkPixel), 255); EXPECT_EQ(qBlue(darkPixel), 255); } + +// ==================== PMS 批次 2 补强 ==================== +// BUG25004 (sev2): 页面旋转后点击/注释坐标映射错乱。 +// 回归意图: BrowserPage 的旋转态坐标换算必须自洽: +// getTopLeftPos 在 90°/270° 下 x/y 轴互换(旋转感知); +// translateRect 对 0°/180° 仅缩放、对 90°/270° 缩放+轴互换, 且与 +// getNorotateRect 构成可用互逆链(注释框回归文档坐标系)。 + +TEST_F(TestBrowserPage, BUG25004_rotationSwapsAxesInTopLeftPos) +{ + Stub s; + s.set(ADDR(BrowserPage, rect), rect_stub); + s.set(ADDR(QGraphicsItem, pos), pos_stub); + + // 旋转 90°: x 轴偏移翻负; 旋转 270°: y 轴偏移翻负 (轴互换语义) + m_tester->m_rotation = Dr::RotateBy90; + QPointF pos90 = m_tester->getTopLeftPos(); + m_tester->m_rotation = Dr::RotateBy0; + QPointF pos0 = m_tester->getTopLeftPos(); + m_tester->m_rotation = Dr::RotateBy270; + QPointF pos270 = m_tester->getTopLeftPos(); + + EXPECT_LT(pos90.x(), pos0.x()); + EXPECT_LT(pos270.y(), pos0.y()); +} + +TEST_F(TestBrowserPage, BUG25004_translateRectRoundTripWithNorotateRect) +{ + m_tester->m_scaleFactor = 1.5; + m_tester->m_originSizeF = QSizeF(200, 200); + + Stub s; + typedef QRectF (*fptr)(BrowserPage *); + s.set((fptr)&BrowserPage::boundingRect, boundingRect_stub); + + QRectF docRect(10, 10, 20, 10); + + // 0°: 尺寸仅随缩放(非零偏移不改变尺寸映射) + m_tester->m_rotation = Dr::RotateBy0; + QRectF screenRect = m_tester->translateRect(docRect); + EXPECT_TRUE(qFuzzyCompare(screenRect.width(), docRect.width() * 1.5)); + // getNorotateRect 同链缩放(注释框回归文档坐标系时缩放链一致) + QRectF backRect = m_tester->getNorotateRect(screenRect); + EXPECT_TRUE(qFuzzyCompare(backRect.width(), screenRect.width() * m_tester->m_scaleFactor)); + + // 180°: 尺寸不变(仅平移), 缩放链保持 + m_tester->m_rotation = Dr::RotateBy180; + screenRect = m_tester->translateRect(docRect); + EXPECT_TRUE(qFuzzyCompare(screenRect.width(), docRect.width() * 1.5)); + backRect = m_tester->getNorotateRect(screenRect); + EXPECT_TRUE(qFuzzyCompare(backRect.width(), screenRect.width() * m_tester->m_scaleFactor)); + + // 90°: 宽高轴互换生效(旋转感知映射不丢精度) + m_tester->m_rotation = Dr::RotateBy90; + screenRect = m_tester->translateRect(docRect); + EXPECT_TRUE(qFuzzyCompare(screenRect.width(), docRect.height() * m_tester->m_scaleFactor)); +} diff --git a/tests/browser/ut_browserword.cpp b/tests/browser/ut_browserword.cpp index 4c8ef520b..e145e202a 100644 --- a/tests/browser/ut_browserword.cpp +++ b/tests/browser/ut_browserword.cpp @@ -142,3 +142,39 @@ TEST_F(TestBrowserWord, UT_BrowserWord_mouseReleaseEvent_001) delete e; } + +// ==================== PMS 批次 2 补强 ==================== +// BUG40946 (sev2): 文字处于可选择状态时, 单击仍触发词项拖拽/移动。 +// 回归意图: setSelectable 打开后: +// 1) ItemIsSelectable 标志位同步(场景选择机制生效的依据); +// 2) 单击 press/release 被抑制(不透传给 QGraphicsItem 拖拽链路)。 + +TEST_F(TestBrowserWord, BUG40946_selectableFlagSyncsWithSetSelectable) +{ + m_tester->setSelectable(true); + EXPECT_TRUE(m_tester->flags() & QGraphicsItem::ItemIsSelectable); + + m_tester->setSelectable(false); + EXPECT_FALSE(m_tester->flags() & QGraphicsItem::ItemIsSelectable); +} + +TEST_F(TestBrowserWord, BUG40946_selectableSuppressesSingleClickDrag) +{ + typedef void (*fptr)(QGraphicsItem *, QGraphicsSceneMouseEvent *); + fptr A_foo = (fptr)(&QGraphicsItem::mousePressEvent); + Stub s; + s.set(A_foo, mousePressEvent_stub); + + QGraphicsSceneMouseEvent e; + + // 文本选择模式: 单击不透传(不触发词项拖拽) + m_tester->setSelectable(true); + g_funcName.clear(); + m_tester->mousePressEvent(&e); + EXPECT_FALSE(g_funcName == "mousePressEvent_stub"); + + // 非选择模式: 恢复正常点击链路 + m_tester->setSelectable(false); + m_tester->mousePressEvent(&e); + EXPECT_TRUE(g_funcName == "mousePressEvent_stub"); +} diff --git a/tests/browser/ut_sheetbrowser.cpp b/tests/browser/ut_sheetbrowser.cpp index f6a957fa4..14538ea25 100644 --- a/tests/browser/ut_sheetbrowser.cpp +++ b/tests/browser/ut_sheetbrowser.cpp @@ -2670,3 +2670,255 @@ TEST_F(TestSheetBrowser, UT_SheetBrowser_eyeProtectionMode_lambda_001) EyeProtectionManager::instance()->setMode(prev); SUCCEED(); } + +// ==================== PMS 批次 2 补强 ==================== +// 桩函数(批次 2 专用, 避免与既有全局桩重名) +static QString g_b2_funcName; +static void b2_setSelectIconRect_stub(const bool, Annotation *) { g_b2_funcName = "setSelectIconRect"; } +static void b2_showMenu_stub() { g_b2_funcName = "showMenu"; } +static void b2_clearSelectIconAnnot_stub() { g_b2_funcName = "clearSelectIconAnnot"; } +static void b2_setScaleFactor_stub(qreal f) { g_b2_funcName = "setScaleFactor:" + QString::number(f); } +static void b2_rotateLeft_stub() { g_b2_funcName = "rotateLeft"; } +static void b2_rotateRight_stub() { g_b2_funcName = "rotateRight"; } +static Qt::GestureState b2_gestureState = Qt::NoGesture; +static Qt::GestureState b2_state_stub() { return b2_gestureState; } + +// BUG22093 (sev2): 无任何选中内容时点击右键菜单崩溃。 +// 回归意图: showMenu 在无选中(m_selectEndWord/m_iconAnnot 均空)时走基础菜单路径, 不崩溃; +// 图标注释选中态时菜单操作后必须清理选中(m_clearSelectIconAnnotAfterMenu)。 + +TEST_F(TestSheetBrowser, BUG22093_showMenuWithoutAnySelection) +{ + Stub stub; + stub.set(ADDR(SheetBrowser, selectedWordsText), selectedWordsText_stub_empty); + stub.set(ADDR(BrowserMenu, initActions), initActions_stub); + stub.set((QAction * (QMenu::*)(const QPoint &, QAction *))ADDR(QMenu, exec), exec_stub); + stub.set(ADDR(SheetBrowser, clearSelectIconAnnotAfterMenu), clearSelectIconAnnotAfterMenu_stub); + + // 无选中: m_selectEndWord / m_iconAnnot 均为空 + DocSheet *sheet = new DocSheet(Dr::FileType::PDF, "1.pdf", nullptr); + m_tester->m_sheet = sheet; + m_tester->showMenu(); + + delete sheet; + EXPECT_TRUE(m_tester->m_selectIconAnnotation == false); +} + +TEST_F(TestSheetBrowser, BUG22093_showMenuWithIconAnnotClearsSelection) +{ + Stub stub; + stub.set(ADDR(SheetBrowser, selectedWordsText), selectedWordsText_stub_empty); + stub.set(ADDR(BrowserMenu, initActions), initActions_stub); + stub.set((QAction * (QMenu::*)(const QPoint &, QAction *))ADDR(QMenu, exec), exec_stub); + g_b2_funcName.clear(); + stub.set(ADDR(SheetBrowser, clearSelectIconAnnotAfterMenu), b2_clearSelectIconAnnot_stub); + + BrowserWord *word = new BrowserWord(nullptr, Word()); + m_tester->m_selectEndWord = word; + PDFAnnotation *annot = new PDFAnnotation(nullptr); + m_tester->m_iconAnnot = annot; + DocSheet *sheet = new DocSheet(Dr::FileType::PDF, "1.pdf", nullptr); + m_tester->m_sheet = sheet; + m_tester->showMenu(); + + delete word; + delete annot; + delete sheet; + // 图标注释菜单操作后选中态被清理 + EXPECT_TRUE(g_b2_funcName == "clearSelectIconAnnot"); + EXPECT_TRUE(m_tester->m_selectIconAnnotation == false); +} + +// BUG43216 (sev2): 添加注释时点击位置越界/页面为空导致崩溃。 +// 回归意图: getClickAnnot 对空页面必须返回空注释且不崩溃; +// 空白页面无命中时返回 nullptr。 + +TEST_F(TestSheetBrowser, BUG43216_getClickAnnotNullPageGuard) +{ + QPointF pos(10, 10); + // 空页面守卫: 返回 nullptr 不崩溃 + EXPECT_TRUE(m_tester->getClickAnnot(nullptr, pos) == nullptr); + SUCCEED(); +} + +TEST_F(TestSheetBrowser, BUG43216_getClickAnnotEmptyPageReturnsNull) +{ + // fixture 页面无注释/无文字: 任意命中点均应安全返回 nullptr + BrowserPage *page = m_tester->m_items.at(0); + QPointF pos(10, 10); + EXPECT_TRUE(m_tester->getClickAnnot(page, pos) == nullptr); +} + +// BUG44136 (sev2): Tab 键焦点遍历时图标注释选中框未释放。 +// 回归意图: SheetBrowser::event 收到 Key_Tab 必须释放最后选中的图标注释选中框。 + +TEST_F(TestSheetBrowser, BUG44136_keyTabReleasesIconAnnotSelection) +{ + DocSheet *sheet = new DocSheet(Dr::FileType::PDF, "1.pdf", nullptr); + m_tester->m_sheet = sheet; + BrowserPage *page = m_tester->m_items.at(0); + m_tester->m_lastSelectIconAnnotPage = page; + + g_b2_funcName.clear(); + Stub stub; + stub.set(ADDR(BrowserPage, setSelectIconRect), b2_setSelectIconRect_stub); + + QKeyEvent tabEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier); + m_tester->event(&tabEvent); + + // Tab 释放图注选中框 + EXPECT_TRUE(g_b2_funcName == "setSelectIconRect"); + delete sheet; +} + +// BUG44264 (sev2): Alt+M 快捷键无法唤出右键菜单。 +// 回归意图: SheetBrowser::event 捕获 Alt+M 并调用 showMenu。 + +TEST_F(TestSheetBrowser, BUG44264_keyAltMShowsContextMenu) +{ + DocSheet *sheet = new DocSheet(Dr::FileType::PDF, "1.pdf", nullptr); + m_tester->m_sheet = sheet; + + g_b2_funcName.clear(); + Stub stub; + stub.set(ADDR(SheetBrowser, showMenu), b2_showMenu_stub); + + QKeyEvent altMEvent(QEvent::KeyPress, Qt::Key_M, Qt::AltModifier); + m_tester->event(&altMEvent); + + EXPECT_TRUE(g_b2_funcName == "showMenu"); + delete sheet; +} + +// BUG44491 (sev2): 双指捏合缩放过于灵敏/难以生效(scale=1.0 步进污染)。 +// 回归意图: 真实 pinchTriggered 路径(完整手势序列 Started→Updated→Finished): +// 捏合缩放必须到达 setScaleFactor(不丢失), 结束态按总缩放落盘最终值。 +// 注: pinchTriggered 使用函数级 static(tempScalefactor/currentStepScaleFactor) +// 跨调用累计, 必须从 GestureStarted 起步才能得到确定性的结果。 + +TEST_F(TestSheetBrowser, BUG44491_pinchScaleFactorReachesDocSheet) +{ + DocSheet *sheet = new DocSheet(Dr::FileType::PDF, "1.pdf", nullptr); + m_tester->m_sheet = sheet; + m_tester->m_lastScaleFactor = 1.0; + + g_b2_funcName.clear(); + Stub stub; + stub.set(ADDR(DocSheet, setScaleFactor), b2_setScaleFactor_stub); + + // 手势开始: 重置 static 累计基线 + b2_gestureState = Qt::GestureStarted; + Stub stateStub; + stateStub.set(ADDR(QGesture, state), b2_state_stub); + QPinchGesture started; + started.setChangeFlags(QPinchGesture::ScaleFactorChanged); + started.setTotalScaleFactor(1.0); + m_tester->pinchTriggered(&started); + + // 捏合中: totalScaleFactor 变为 2.0, 缩放必须同步生效 + b2_gestureState = Qt::GestureUpdated; + QPinchGesture updated; + updated.setChangeFlags(QPinchGesture::ScaleFactorChanged); + updated.setTotalScaleFactor(2.0); + m_tester->pinchTriggered(&updated); + + // 缩放手势生效: 到达文档缩放设置, 且缩放值非初始 1.0 污染 + EXPECT_TRUE(g_b2_funcName.startsWith("setScaleFactor:")); + if (g_b2_funcName.startsWith("setScaleFactor:")) { + qreal applied = g_b2_funcName.mid(QString("setScaleFactor:").size()).toDouble(); + EXPECT_GT(applied, 1.0); + } + delete sheet; +} + +TEST_F(TestSheetBrowser, BUG44491_pinchFinishedKeepsValidScale) +{ + DocSheet *sheet = new DocSheet(Dr::FileType::PDF, "1.pdf", nullptr); + m_tester->m_sheet = sheet; + m_tester->m_lastScaleFactor = 1.0; + + g_b2_funcName.clear(); + Stub stub; + stub.set(ADDR(DocSheet, setScaleFactor), b2_setScaleFactor_stub); + + // 完整手势序列: 开始→结束(缩小到 0.5) + b2_gestureState = Qt::GestureStarted; + Stub stateStub; + stateStub.set(ADDR(QGesture, state), b2_state_stub); + QPinchGesture started; + started.setChangeFlags(QPinchGesture::ScaleFactorChanged); + started.setTotalScaleFactor(1.0); + m_tester->pinchTriggered(&started); + + b2_gestureState = Qt::GestureFinished; + QPinchGesture finished; + finished.setChangeFlags(QPinchGesture::ScaleFactorChanged); + finished.setTotalScaleFactor(0.5); + m_tester->pinchTriggered(&finished); + + // 结束态: 缩放值有效(>0), 不会退化为 0/失效 + EXPECT_TRUE(g_b2_funcName.startsWith("setScaleFactor:")); + if (g_b2_funcName.startsWith("setScaleFactor:")) { + qreal applied = g_b2_funcName.mid(QString("setScaleFactor:").size()).toDouble(); + EXPECT_GT(applied, 0.0); + } + delete sheet; +} + +// BUG41018 (sev2): 大页面文档双页模式 maxWidth 计算错误(未乘页数)导致布局溢出。 +// 回归意图: 同一文档下, 双页模式的适配缩放不得大于单页模式(宽度预算必须按 2 页计算)。 + +TEST_F(TestSheetBrowser, BUG41018_doublePageScaleNotGreaterThanSingleBigWidth) +{ + Stub stub; + stub.set(ADDR(BrowserPage, render), render_stub); + stub.set(ADDR(SheetBrowser, beginViewportChange), beginViewportChange_stub); + + // 大宽度页面: 双页宽度预算未乘 2 时 scale 会反超单页 + m_tester->m_items.at(0)->m_originSizeF = QSizeF(2000, 1000); + m_tester->m_items.at(1)->m_originSizeF = QSizeF(2000, 1000); + + SheetOperation opSingle; + opSingle.scaleMode = Dr::FitToPageWidthMode; + opSingle.layoutMode = Dr::SinglePageMode; + m_tester->deform(opSingle); + double scaleSingle = m_tester->m_lastScaleFactor; + + SheetOperation opDouble; + opDouble.scaleMode = Dr::FitToPageWidthMode; + opDouble.layoutMode = Dr::TwoPagesMode; + m_tester->deform(opDouble); + double scaleDouble = m_tester->m_lastScaleFactor; + + EXPECT_GT(scaleSingle, 0.0); + EXPECT_GT(scaleDouble, 0.0); + // 双页(两页并排)的缩放不得大于单页缩放 + EXPECT_LE(scaleDouble, scaleSingle + 1e-6); +} + +TEST_F(TestSheetBrowser, BUG41018_doublePageScaleNotGreaterThanSingleBigHeight) +{ + Stub stub; + stub.set(ADDR(BrowserPage, render), render_stub); + stub.set(ADDR(SheetBrowser, beginViewportChange), beginViewportChange_stub); + + // 大高度页面: 约束翻转到高度轴, 双页缩放同样不得反超单页 + m_tester->m_items.at(0)->m_originSizeF = QSizeF(1000, 4000); + m_tester->m_items.at(1)->m_originSizeF = QSizeF(1000, 4000); + + SheetOperation opSingle; + opSingle.scaleMode = Dr::FitToPageWidthMode; + opSingle.layoutMode = Dr::SinglePageMode; + m_tester->deform(opSingle); + double scaleSingle = m_tester->m_lastScaleFactor; + + SheetOperation opDouble; + opDouble.scaleMode = Dr::FitToPageWidthMode; + opDouble.layoutMode = Dr::TwoPagesMode; + m_tester->deform(opDouble); + double scaleDouble = m_tester->m_lastScaleFactor; + + EXPECT_GT(scaleSingle, 0.0); + EXPECT_GT(scaleDouble, 0.0); + EXPECT_LE(scaleDouble, scaleSingle + 1e-6); +} diff --git a/tests/document/ut_pdfmodel.cpp b/tests/document/ut_pdfmodel.cpp index 50d7670ae..fecff5838 100644 --- a/tests/document/ut_pdfmodel.cpp +++ b/tests/document/ut_pdfmodel.cpp @@ -10,6 +10,12 @@ #include "stub.h" #include + +#include +#include +#include +#include + using namespace deepin_reader; /********测试PDFAnnotation***********/ @@ -576,6 +582,11 @@ bool DPdfPage_isValid_stub() return true; } +bool DPdfPage_isValid_stub_false() +{ + return false; +} + QString label_stub(int) { return "test"; @@ -766,3 +777,187 @@ TEST_F(TestPDFDocument, UT_PDFDocument_fileIdentifier_001) QString id1 = m_tester->fileIdentifier(); EXPECT_TRUE(id1 == m_tester->fileIdentifier()); } + +// ==================== PMS 批次 2 补强 ==================== +// 真实文件 fixture: normal.pdf / broken.pdf (UTSOURCEDIR/files/) +// 注意: PDFDocument 析构会 delete m_document, fixture 只 delete m_tester。 +class TestPDFDocumentReal : public ::testing::Test +{ +public: + virtual void SetUp(); + virtual void TearDown(); + +protected: + PDFDocument *m_tester = nullptr; +}; + +void TestPDFDocumentReal::SetUp() +{ + m_tester = new PDFDocument(new DPdfDoc(QString(UTSOURCEDIR) + "/files/normal.pdf", "")); +} + +void TestPDFDocumentReal::TearDown() +{ + delete m_tester; +} + +// BUG46911 (sev2): 打开损坏的 PDF 文件崩溃。 +// 回归意图: broken.pdf 打开后 pageCount/page/properties 全链路安全降级。 + +TEST_F(TestPDFDocumentReal, BUG46911_brokenPdfOpensGracefully) +{ + PDFDocument brokenTester(new DPdfDoc(QString(UTSOURCEDIR) + "/files/broken.pdf", "")); + + // 损坏文档可打开(部分对象可解析), 取损坏页时安全降级为空, 不崩溃 + EXPECT_GE(brokenTester.pageCount(), 0); + EXPECT_TRUE(brokenTester.page(0) == nullptr); +} + +TEST_F(TestPDFDocumentReal, BUG46911_brokenPdfPropertiesSafe) +{ + PDFDocument brokenTester(new DPdfDoc(QString(UTSOURCEDIR) + "/files/broken.pdf", "")); + + // 损坏文档属性/大纲可重复获取且稳定(不崩溃不漂移) + Properties first = brokenTester.properties(); + Properties second = brokenTester.properties(); + EXPECT_EQ(first.size(), second.size()); + + QList
outline = brokenTester.outline(); + Q_UNUSED(outline); + SUCCEED(); +} + +// BUG54210 (sev2): 无效页面(isValid=false)未拦截导致下游崩溃。 +// 回归意图: PDFDocument::page 对 isValid=false 的页面返回空并安全释放。 + +TEST_F(TestPDFDocumentReal, BUG54210_pageInvalidPageReturnsNull) +{ + // 真实页面: 有效 + Page *validPage = m_tester->page(0); + EXPECT_TRUE(validPage != nullptr); + delete validPage; + + // isValid=false: 守卫生效返回空 + Stub s; + s.set(ADDR(DPdfPage, isValid), DPdfPage_isValid_stub_false); + + Page *invalidPage = m_tester->page(0); + EXPECT_TRUE(invalidPage == nullptr); +} + +// BUG68550 (sev2): 选中文字时矩形/文本错乱。 +// 回归意图: 真实页面上 allTextRects/allTextLooseRects 的 rects 与 texts 一一对应, +// charCount 为上限(每字符一个合法矩形才追加)。 + +TEST_F(TestPDFDocumentReal, BUG68550_allTextRectsRectTextAligned) +{ + DPdfPage *dp = m_tester->m_document->page(0, 96, 96); + ASSERT_TRUE(dp != nullptr); + ASSERT_TRUE(dp->isValid()); + + int charCount = 0; + QStringList texts; + QVector rects; + dp->allTextRects(charCount, texts, rects); + + // 有文字的真实文档: 每个矩形对应一段文本, 数量严格一致 + EXPECT_GT(charCount, 0); + EXPECT_EQ(rects.count(), texts.count()); + EXPECT_LE(rects.count(), charCount); + // 注: DPdfPage 由 DPdfDoc(m_pages) 持有管理, 不可 delete (qDeleteAll 双重释放) +} + +TEST_F(TestPDFDocumentReal, BUG68550_allTextLooseRectsRectTextAligned) +{ + DPdfPage *dp = m_tester->m_document->page(0, 96, 96); + ASSERT_TRUE(dp != nullptr); + + int charCount = 0; + QStringList texts; + QVector rects; + dp->allTextLooseRects(charCount, texts, rects); + + EXPECT_GT(charCount, 0); + EXPECT_EQ(rects.count(), texts.count()); + EXPECT_FALSE(texts.join(QString()).isEmpty()); + // 同上: DPdfPage 生命周期归 DPdfDoc 管理 +} + +// BUG304111 (sev2): 获取文件属性时多线程重入死锁。 +// 回归意图: 并发调用 properties() (内部 DPdfMutexLocker) 可在时限内完成。 + +TEST_F(TestPDFDocumentReal, BUG304111_concurrentPropertiesNoDeadlock) +{ + std::atomic done1(false), done2(false); + std::thread t1([this, &done1]() { + Properties p = m_tester->properties(); + Q_UNUSED(p); + done1 = true; + }); + std::thread t2([this, &done2]() { + Properties p = m_tester->properties(); + Q_UNUSED(p); + done2 = true; + }); + + // 3 秒时限内必须完成(死锁则超时失败) + for (int i = 0; i < 300 && !(done1 && done2); ++i) + QThread::msleep(10); + EXPECT_TRUE(done1.load()); + EXPECT_TRUE(done2.load()); + t1.join(); + t2.join(); +} + +// BUG304471 (sev2): properties 重复调用结果漂移。 +// 回归意图: 缓存机制保证多次获取属性内容一致。 + +TEST_F(TestPDFDocumentReal, BUG304471_propertiesCachedStable) +{ + Properties first = m_tester->properties(); + Properties second = m_tester->properties(); + + EXPECT_EQ(first.size(), second.size()); + for (auto it = first.begin(); it != first.end(); ++it) + EXPECT_TRUE(second.value(it.key()) == it.value()); +} + +// BUG351271 (sev2): SMB 等场景保存失败后文件损坏(未回滚)。 +// 回归意图: 底层 save/saveAs 失败时, PDFDocument 包装层必须如实传递失败, +// 调用方据此不误报保存成功。 + +TEST_F(TestPDFDocumentReal, BUG351271_saveFailurePropagates) +{ + Stub s; + s.set(ADDR(DPdfDoc, save), save_stub); + + EXPECT_FALSE(m_tester->save()); +} + +TEST_F(TestPDFDocumentReal, BUG351271_saveAsFailurePropagates) +{ + Stub s; + s.set(ADDR(DPdfDoc, saveAs), saveAs_stub); + + EXPECT_FALSE(m_tester->saveAs("/tmp/ut_351271_out.pdf")); +} + +// BUG353429 (sev2): 另存为输出的 PDF 文件无效/损坏。 +// 回归意图: 真实文档 saveAs 输出存在、非空、以 %PDF 头开始(有效 PDF)。 + +TEST_F(TestPDFDocumentReal, BUG353429_saveAsOutputIsValidPdf) +{ + QTemporaryDir tmpDir; + ASSERT_TRUE(tmpDir.isValid()); + QString outPath = tmpDir.path() + "/saved.pdf"; + + EXPECT_TRUE(m_tester->saveAs(outPath)); + + QFile outFile(outPath); + ASSERT_TRUE(outFile.exists()); + ASSERT_TRUE(outFile.open(QIODevice::ReadOnly)); + QByteArray head = outFile.read(5); + outFile.close(); + EXPECT_GT(outFile.size(), QFileInfo(QString(UTSOURCEDIR) + "/files/normal.pdf").size() / 100); + EXPECT_TRUE(head.startsWith("%PDF")); +} diff --git a/tests/sidebar/ut_bookmarkwidget.cpp b/tests/sidebar/ut_bookmarkwidget.cpp index 219c72a5b..6154d4a4b 100644 --- a/tests/sidebar/ut_bookmarkwidget.cpp +++ b/tests/sidebar/ut_bookmarkwidget.cpp @@ -145,3 +145,91 @@ TEST_F(TestBookMarkWidget, testonListMenuClick) m_tester->onListMenuClick(E_MENU_ACTION::E_BOOKMARK_DELETE); EXPECT_TRUE(g_deleteItemByKey_result == "deleteItemByKey_stub"); } + +// ==================== PMS 批次 2 补强 ==================== +// BUG10332 (sev2): 添加书签后未保存/书签状态不同步。 +// 回归意图: handleBookMark 添加/移除必须同步书签列表模型与按钮可用状态; +// onAddBookMarkClicked 必须把当前页写入 DocSheet::m_bookmarks 持久化集合。 + +static int pageCount_bug10332_stub() +{ + return 64; +} + +TEST_F(TestBookMarkWidget, BUG10332_handleBookMarkAddSyncsModelAndButton) +{ + int cur = m_tester->m_sheet->currentIndex(); + m_tester->handleBookMark(cur, 1); + + // 添加: 书签列表模型插入该页, 当前页添加按钮置灰 + EXPECT_TRUE(m_tester->m_pImageListView->count() == 1); + EXPECT_FALSE(m_tester->m_pAddBookMarkBtn->isEnabled()); + + m_tester->handleBookMark(cur, 0); + + // 移除: 模型同步移除, 按钮恢复可用 + EXPECT_TRUE(m_tester->m_pImageListView->count() == 0); + EXPECT_TRUE(m_tester->m_pAddBookMarkBtn->isEnabled()); +} + +TEST_F(TestBookMarkWidget, BUG10332_onAddBookMarkClickedPersistsBookmark) +{ + // fixture 中 DocSheet 未打开文档, pageCount() 为 0 会触发 setBookMark 索引守卫; + // 桩定页数使书签写入链路可验证。 + Stub s; + s.set(ADDR(DocSheet, pageCount), pageCount_bug10332_stub); + + int page = m_tester->m_sheet->currentIndex(); + + m_tester->onAddBookMarkClicked(); + + // 书签进入 DocSheet 持久化集合(保存链路的数据源) + EXPECT_TRUE(m_tester->m_sheet->m_bookmarks.contains(page)); +} + +TEST_F(TestBookMarkWidget, BUG10332_onAddBookMarkClickedNullSheetGuard) +{ + // m_sheet 置空后点击添加不应崩溃(QPointer 空守卫) + m_tester->m_sheet = nullptr; + m_tester->onAddBookMarkClicked(); + EXPECT_TRUE(m_tester->m_sheet.isNull()); + SUCCEED(); +} + +// BUG12219 (sev2): 书签列表聚焦时按 Del 删除书签崩溃(无效索引未守卫)。 +// 回归意图: deleteItemByKey 对无效索引必须走守卫路径, 不触发 setBookMark。 + +static QString g_bug12219_setBookMark_called; +void setBookMark_bug12219_stub(int, int) +{ + g_bug12219_setBookMark_called = "called"; +} + +TEST_F(TestBookMarkWidget, BUG12219_deleteItemByKeyInvalidIndexGuard) +{ + // 空书签列表: 当前模型索引无效, 不应触发 setBookMark + g_bug12219_setBookMark_called.clear(); + Stub s; + s.set(ADDR(DocSheet, setBookMark), setBookMark_bug12219_stub); + m_tester->deleteItemByKey(); + EXPECT_TRUE(g_bug12219_setBookMark_called.isEmpty()); +} + +TEST_F(TestBookMarkWidget, BUG12219_deleteItemByKeyValidIndexRemovesBookmark) +{ + int cur = m_tester->m_sheet->currentIndex(); + m_tester->m_sheet->setBookMark(cur, 1); + EXPECT_TRUE(m_tester->m_sheet->m_bookmarks.contains(cur)); + + g_bug12219_setBookMark_called.clear(); + Stub s; + s.set(ADDR(DocSheet, setBookMark), setBookMark_bug12219_stub); + + // 通过 handleBookMark 先向列表模型插入书签项, 再选中后 Del 删除 + m_tester->handleBookMark(cur, 1); + EXPECT_TRUE(m_tester->m_pImageListView->count() == 1); + m_tester->m_pImageListView->selectionModel()->setCurrentIndex( + m_tester->m_pImageListView->model()->index(0, 0), QItemSelectionModel::Select); + m_tester->deleteItemByKey(); + EXPECT_TRUE(g_bug12219_setBookMark_called == "called"); +} diff --git a/tests/uiframe/ut_centraldocpage.cpp b/tests/uiframe/ut_centraldocpage.cpp index 37a270df3..4ffdb90cc 100644 --- a/tests/uiframe/ut_centraldocpage.cpp +++ b/tests/uiframe/ut_centraldocpage.cpp @@ -1447,3 +1447,68 @@ TEST_F(TestCentralDocPage, UT_CentralDocPage_setActiveTabByFilePath_001) m_tester->setActiveTabByFilePath("/tmp/ut_not_exist_active_file.pdf"); SUCCEED(); } + +// ==================== PMS 批次 2 补强 ==================== +// BUG20978 (sev2): 快捷键链缺失导致部分快捷键功能无效。 +// 历史根因: 快捷键分发链未覆盖部分按键(旧 Ctrl+B 链路已在重构中废除)。 +// 回归意图: CentralDocPage::handleShortcut 必须把文档级快捷键分发到"当前文档" +// 并实际生效(alt_2 手型 / ctrl_1 适合页面 / ctrl_m 打开侧栏)。 + +TEST_F(TestCentralDocPage, BUG20978_handleShortcutAlt2DispatchToCurrentSheet) +{ + Stub s; + s.set(ADDR(CentralDocPage, getCurSheet), getCurSheet_stub2); + s.set(ADDR(ReaderImageThreadPoolManager, addgetDocImageTask), addgetDocImageTask_stub); + + QString strPath = UTSOURCEDIR; + strPath += "/files/normal.pdf"; + g_docsheet = new DocSheet(Dr::FileType::PDF, strPath, nullptr); + g_docsheet->m_operation.mouseShape = Dr::MouseShapeNormal; + + m_tester->handleShortcut(Dr::key_alt_2); + + // 分发链生效: 当前文档鼠标形状被切换为手型 + EXPECT_TRUE(g_docsheet->m_operation.mouseShape == Dr::MouseShapeHand); + + delete g_docsheet; + g_docsheet = nullptr; +} + +TEST_F(TestCentralDocPage, BUG20978_handleShortcutCtrl1DispatchToCurrentSheet) +{ + Stub s; + s.set(ADDR(CentralDocPage, getCurSheet), getCurSheet_stub2); + s.set(ADDR(ReaderImageThreadPoolManager, addgetDocImageTask), addgetDocImageTask_stub); + + QString strPath = UTSOURCEDIR; + strPath += "/files/normal.pdf"; + g_docsheet = new DocSheet(Dr::FileType::PDF, strPath, nullptr); + + m_tester->handleShortcut(Dr::key_ctrl_1); + + // 分发链生效: 缩放模式切换为"适合页面" + EXPECT_TRUE(g_docsheet->m_operation.scaleMode == Dr::FitToPageDefaultMode); + + delete g_docsheet; + g_docsheet = nullptr; +} + +TEST_F(TestCentralDocPage, BUG20978_handleShortcutCtrlMOpensSidebar) +{ + Stub s; + s.set(ADDR(CentralDocPage, getCurSheet), getCurSheet_stub2); + s.set(ADDR(ReaderImageThreadPoolManager, addgetDocImageTask), addgetDocImageTask_stub); + + QString strPath = UTSOURCEDIR; + strPath += "/files/normal.pdf"; + g_docsheet = new DocSheet(Dr::FileType::PDF, strPath, nullptr); + g_docsheet->m_operation.sidebarVisible = false; + + m_tester->handleShortcut(Dr::key_ctrl_m); + + // 分发链生效: 侧栏可见状态被置位 + EXPECT_TRUE(g_docsheet->m_operation.sidebarVisible == true); + + delete g_docsheet; + g_docsheet = nullptr; +} diff --git a/tests/uiframe/ut_doctabbar.cpp b/tests/uiframe/ut_doctabbar.cpp index 622af26bb..6695b47f6 100644 --- a/tests/uiframe/ut_doctabbar.cpp +++ b/tests/uiframe/ut_doctabbar.cpp @@ -344,3 +344,46 @@ TEST_F(UT_DocTabBar, UT_DocTabBar_setPendingActiveFile_001) m_tester->setPendingActiveFile(strPath); EXPECT_TRUE(m_tester->m_delayIndex == 0); } + +// ==================== PMS 批次 2 补强 ==================== +// BUG46381 (sev2): 快速连续点击关闭多个标签页导致崩溃。 +// 回归意图: onTabCloseRequested 的 m_intervalTimer 防抖窗口内的重复关闭必须被忽略, +// 窗口结束后恢复处理。 + +TEST_F(UT_DocTabBar, BUG46381_rapidCloseRequestsThrottled) +{ + QString strPath = UTSOURCEDIR; + strPath += "/files/1.pdf"; + DocSheet *sheet = new DocSheet(Dr::PDF, strPath, m_tester); + m_tester->insertSheet(sheet, 0); + + int sigCount = 0; + QObject::connect(m_tester, &DocTabBar::sigTabClosed, [&sigCount](DocSheet *) { + sigCount++; + }); + // 100ms 防抖窗口内连发 3 次关闭: 仅首次生效 + m_tester->onTabCloseRequested(0); + m_tester->onTabCloseRequested(0); + m_tester->onTabCloseRequested(0); + EXPECT_EQ(sigCount, 1); + delete sheet; +} + +TEST_F(UT_DocTabBar, BUG46381_closeAfterThrottleWindowWorksAgain) +{ + QString strPath = UTSOURCEDIR; + strPath += "/files/1.pdf"; + DocSheet *sheet = new DocSheet(Dr::PDF, strPath, m_tester); + m_tester->insertSheet(sheet, 0); + + int sigCount = 0; + QObject::connect(m_tester, &DocTabBar::sigTabClosed, [&sigCount](DocSheet *) { + sigCount++; + }); + m_tester->onTabCloseRequested(0); + // 等待防抖窗口(100ms)结束后, 再次请求关闭应正常处理 + QTest::qWait(150); + m_tester->onTabCloseRequested(0); + EXPECT_EQ(sigCount, 2); + delete sheet; +} diff --git a/tests/uiframe/ut_titlewidget.cpp b/tests/uiframe/ut_titlewidget.cpp index b7cb015f0..fd12f68f1 100644 --- a/tests/uiframe/ut_titlewidget.cpp +++ b/tests/uiframe/ut_titlewidget.cpp @@ -117,3 +117,43 @@ TEST_F(TestTitleWidget, testSizeModeChangedLambda) emit DGuiApplicationHelper::instance()->sizeModeChanged(DGuiApplicationHelper::NormalMode); SUCCEED(); } + +// ==================== PMS 批次 2 补强 ==================== +// BUG44136 (sev2): 标题栏控件 Tab 焦点顺序错乱/回车无效。 +// 回归意图: TitleWidget 在构造后焦点策略与 Tab 遍历链可用, +// 且按键事件处理不崩溃。 + +TEST_F(TestTitleWidget, BUG44136_titleWidgetFocusChainUsable) +{ + // 当前 Tab 顺序机制: 构造时把有序控件链写入父级 orderlist 属性, 并逐个设为 TabFocus + QVariant varOrder = m_parent->property("orderlist"); + EXPECT_TRUE(varOrder.isValid()); + QList orderList = varOrder.value>(); + // 有序链槽位数: 缩略图/减/缩放框/增/opt/最小/最大/关闭 = 8 + // (全屏按钮仅在全屏模式下由 DTitlebar 提供, 裸父级 fixture 中为空不追加) + ASSERT_EQ(orderList.count(), 8); + // 相对顺序约束: 缩略图 → 减 → 增 (缩放框在裸 fixture 下可能尚未实例化) + int idxThumbnail = -1, idxDec = -1, idxInc = -1; + int nonNullCount = 0; + for (int i = 0; i < orderList.count(); ++i) { + QWidget *w = orderList.at(i); + if (w == nullptr) + continue; + ++nonNullCount; + EXPECT_TRUE(w->focusPolicy() == Qt::TabFocus) << "noTabFocus at idx=" << i; + if (w->objectName() == "SP_DecreaseElement") + idxDec = i; + else if (w->objectName() == "SP_IncreaseElement") + idxInc = i; + else if (qobject_cast(w) && w == orderList.first()) + idxThumbnail = i; + } + EXPECT_GE(nonNullCount, 3); + EXPECT_GE(idxThumbnail, 0); + EXPECT_GT(idxDec, idxThumbnail); + EXPECT_GT(idxInc, idxDec); + + QKeyEvent tabEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier); + m_tester->keyPressEvent(&tabEvent); + SUCCEED(); +} diff --git a/tests/ut_application.cpp b/tests/ut_application.cpp index 760a46e3c..4a2df1d4f 100644 --- a/tests/ut_application.cpp +++ b/tests/ut_application.cpp @@ -12,6 +12,7 @@ #include #include #include +#include TEST(UT_Application, UT_Application_emitSheetChanged) { @@ -84,3 +85,44 @@ TEST(UT_Application, UT_Application_notifyOtherEvent) dApp->notify(&widget, &event); SUCCEED(); } + +// ==================== PMS 批次 2 补强 ==================== +// BUG44137 (sev2): 回车键无法触发自定义按钮(DTK DIconButton/DhuaButton 类不默认响应回车)。 +// 回归意图: Application::notify 必须把 Key_Return/Key_Enter 转换为按钮 clicked 信号。 + +TEST(UT_Application, BUG44137_notifyReturnEmitsClickedSignal) +{ + QPushButton btn; + QSignalSpy spy(&btn, &QAbstractButton::clicked); + + QKeyEvent event(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); + dApp->notify(&btn, &event); + + // 回车 → clicked 信号发射且事件被消费 + EXPECT_EQ(spy.count(), 1); + EXPECT_TRUE(event.isAccepted()); +} + +TEST(UT_Application, BUG44137_notifyKeypadEnterEmitsClickedSignal) +{ + QPushButton btn; + QSignalSpy spy(&btn, &QAbstractButton::clicked); + + QKeyEvent event(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier); + dApp->notify(&btn, &event); + + EXPECT_EQ(spy.count(), 1); + EXPECT_TRUE(event.isAccepted()); +} + +// BUG44264 (sev2): Alt+M 无法唤出右键菜单。 +// 回归意图: Application::notify 捕获 Alt+M 合成右键点击; +// 无输入法光标时走 QPoint(0,0) 守卫, 不崩溃。 + +TEST(UT_Application, BUG44264_notifyAltMSynthesizesContextMenu) +{ + QWidget widget; + QKeyEvent event(QEvent::KeyPress, Qt::Key_M, Qt::AltModifier); + dApp->notify(&widget, &event); + SUCCEED(); +}