Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions tests/browser/ut_browserpage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -866,3 +866,60 @@
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);

Check warning on line 902 in tests/browser/ut_browserpage.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

C-style pointer casting detected. C++ offers four different kinds of casts as replacements: static_cast, const_cast, dynamic_cast and reinterpret_cast. A C-style cast could evaluate to any of those automatically, thus it is considered safer if the programmer explicitly states which kind of cast is expected.

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));
Comment on lines +911 to +912

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Coordinate round trip scales twice

When the inverse coordinate mapping is incorrect, the test describes getNorotateRect() as restoring docRect, but expects backRect.width() to be screenRect.width() * m_scaleFactor; an extra scale therefore passes instead of detecting the broken round trip.

Compare the returned rectangle with the original docRect rather than expecting another scale multiplication.

Prompt for AI agents
In `tests/browser/ut_browserpage.cpp` at lines 911-912:

**Coordinate round trip scales twice**

When the inverse coordinate mapping is incorrect, the test describes `getNorotateRect()` as restoring `docRect`, but expects `backRect.width()` to be `screenRect.width() * m_scaleFactor`; an extra scale therefore passes instead of detecting the broken round trip.

Compare the returned rectangle with the original `docRect` rather than expecting another scale multiplication.


// 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));
}
36 changes: 36 additions & 0 deletions tests/browser/ut_browserword.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,39 @@

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);

Check warning on line 164 in tests/browser/ut_browserword.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

C-style pointer casting detected. C++ offers four different kinds of casts as replacements: static_cast, const_cast, dynamic_cast and reinterpret_cast. A C-style cast could evaluate to any of those automatically, thus it is considered safer if the programmer explicitly states which kind of cast is expected.
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");
}
252 changes: 252 additions & 0 deletions tests/browser/ut_sheetbrowser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +2878 to +2890

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Two-page scale regression passes

When the tests run with the current fixture setup, deform() uses cached m_maxWidth and m_maxHeight, which these tests do not update after changing m_originSizeF (and are zero in the current fixture); the safety fallbacks do not exercise the large-page width budget. Also, EXPECT_LE accepts equal scales if the two-page divisor is removed.

Set up valid cached maxima after changing page sizes and assert a scale that distinguishes the two-page width budget from the single-page budget.

Also at tests/browser/ut_sheetbrowser.cpp:2896, tests/browser/ut_sheetbrowser.cpp:2906-2924.

Prompt for AI agents
In `tests/browser/ut_sheetbrowser.cpp` at lines 2878-2890:

**Two-page scale regression passes**

When the tests run with the current fixture setup, `deform()` uses cached `m_maxWidth` and `m_maxHeight`, which these tests do not update after changing `m_originSizeF` (and are zero in the current fixture); the safety fallbacks do not exercise the large-page width budget. Also, `EXPECT_LE` accepts equal scales if the two-page divisor is removed.

Set up valid cached maxima after changing page sizes and assert a scale that distinguishes the two-page width budget from the single-page budget.

Also at `tests/browser/ut_sheetbrowser.cpp:2896`, `tests/browser/ut_sheetbrowser.cpp:2906-2924`.

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);
}
Loading
Loading