From defc88a3cb5a1b11e357103b3b21a3cc78158e8b Mon Sep 17 00:00:00 2001 From: zhanghongyuan Date: Fri, 9 Oct 2026 09:53:29 +0800 Subject: [PATCH 1/2] test(ut): add regression cases for 12 blocked PMS bugs without fix commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anchor 13 GTest cases to current master semantics (d634cbad) using PMS reproduction steps, covering tab move-out/restore, window close scheduling, column edit, color mark-all, window-state option and titlebar focus chain; 8 remaining bugs recorded as SKIP with evidence in work order (no testable code path). 新增13个GTest回归用例,为无修复提交的12个PMS bug按复现步骤+当前代码 语义锚定(标签页移出回填、窗口退出调度、列编辑、颜色标记全部、窗口 状态选项、标题栏焦点链);其余8个bug经核实无代码断言路径,在工作单 记录SKIP理由。 Log: 补齐PMS批次4无修复提交bug的回归单测 Influence: 仅测试文件变更,不影响应用功能。 --- tests/common/test_settings.cpp | 27 ++++ tests/editor_core/test_dtextedit_edit.cpp | 71 +++++++++ tests/editor_core/test_dtextedit_mark.cpp | 41 +++++ tests/startmanager/test_startmanager.cpp | 140 +++++++++++++++++ tests/widgets_ut/test_window.cpp | 176 ++++++++++++++++++++++ 5 files changed, 455 insertions(+) diff --git a/tests/common/test_settings.cpp b/tests/common/test_settings.cpp index 2db65b90..c338f091 100644 --- a/tests/common/test_settings.cpp +++ b/tests/common/test_settings.cpp @@ -1221,3 +1221,30 @@ TEST_F(SettingsTest, BUG102351_SettingsDestroyed_ConfigValuePreserved) } EXPECT_TRUE(onDisk); } + +// PMS: https://pms.uniontech.com/bug-view-44975.html commit: N/A(批次4:git 历史无修复提交,按 PMS 复现步骤 + master d634cbad 代码语义锚定) +// 场景:设置中修改"启动时窗口状态"选项后,向已关闭窗口发送 sigChangeWindowSize 导致异常。 +// 锚定语义:Settings 构造中 windowstate→sigChangeWindowSize 的 connect 被 #if 0 禁用 +// (settings.cpp 146-149)——选项值变更正常生效,但不得再向任何窗口 poke 信号。 +TEST_F(SettingsTest, BUG44975_WindowStateOptionChange_DoesNotPokeWindows) +{ + // Arrange: 监听 sigChangeWindowSize(修复前该信号会向已关闭/销毁窗口发送) + QString lastMode; + int pokes = 0; + QObject::connect(Settings::instance(), &Settings::sigChangeWindowSize, + [&lastMode, &pokes](QString mode) { + lastMode = mode; + ++pokes; + }); + auto option = s_instance->settings->option("advance.window.windowstate"); + ASSERT_NE(option, nullptr); + + // Act: 在设置中切换窗口启动状态(PMS 步骤:修改该选项) + option->setValue(QString("fullscreen")); + QApplication::processEvents(); + + // Assert: 选项值正常更新;无窗口 poke 信号发出 + EXPECT_EQ(option->value().toString(), QString("fullscreen")); + EXPECT_EQ(pokes, 0); + EXPECT_TRUE(lastMode.isEmpty()); +} diff --git a/tests/editor_core/test_dtextedit_edit.cpp b/tests/editor_core/test_dtextedit_edit.cpp index 2421e327..307aa17a 100644 --- a/tests/editor_core/test_dtextedit_edit.cpp +++ b/tests/editor_core/test_dtextedit_edit.cpp @@ -1344,3 +1344,74 @@ TEST_F(TextEditTest, BUG60989_UpdateFont_SelectAllInView_NoHang) EXPECT_TRUE(cur.hasSelection()); EXPECT_LT(cur.selectionEnd() - cur.selectionStart(), edit->document()->characterCount()); } + +// PMS: https://pms.uniontech.com/bug-view-46081.html commit: N/A(批次4:git 历史无修复提交,按 PMS 复现步骤 + master d634cbad 代码语义锚定) +// 场景:alt+鼠标列选择模式下,复制/剪切/粘贴异常(列选区内容未按行拼接进入剪贴板)。 +// 锚定语义:TextEdit::copy(true) 的 m_bIsAltMod 分支将各列选区文本按 "\n" 拼接写入剪贴板; +// 文本保持不变、列选区保留(copy 不破坏选择状态)。 +TEST_F(TextEditTest, BUG46081_ColumnSelectionCopy_JoinsLines) +{ + // Arrange: 三行各造一个列选区(每行前 2 字符),进入列编辑模式 + setDocText(QString("aa\nbb\ncc\n")); + QList sels; + for (int line = 0; line < 3; ++line) { + QTextCursor cur(edit->document()); + const int blockPos = edit->document()->findBlockByNumber(line).position(); + cur.setPosition(blockPos); + cur.setPosition(blockPos + 2, QTextCursor::KeepAnchor); + QTextEdit::ExtraSelection sel; + sel.cursor = cur; + sels << sel; + } + edit->restoreColumnEditSelection(sels); + edit->m_bIsAltMod = true; + + // Act: 列复制(ignoreCheck=true 绕开权限校验,聚焦列分支) + edit->copy(true); + + // Assert: 剪贴板为各选区按行拼接;文本未被改动;列选区仍在 + EXPECT_EQ(QApplication::clipboard()->text(), QString("aa\nbb\ncc")); + EXPECT_EQ(edit->toPlainText(), QString("aa\nbb\ncc\n")); + EXPECT_EQ(edit->m_altModSelections.size(), 3); + EXPECT_TRUE(edit->m_bIsAltMod); +} + +// PMS: https://pms.uniontech.com/bug-view-46081.html commit: N/A(批次4:无修复提交,按 PMS 步骤 + master 代码语义锚定) +// 场景:alt 列选择后粘贴,粘贴内容应作用于各列选区且可整体撤销。 +// 锚定语义:TextEdit::paste() 的 m_bIsAltMod 分支走 insertColumnEditTextEx → +// InsertTextUndoCommand(列路径):每个列选区替换为剪贴板文本(多选区共享同一插入文本, +// InsertTextUndoCommand::redo 列分支语义);undo_ 后完整还原文本与列选区。 +TEST_F(TextEditTest, BUG46081_ColumnPaste_ReplacesEachSelectionAndUndoRestores) +{ + // Arrange: 三行各造列选区(每行首字符),剪贴板单行文本 + setDocText(QString("xx\nyy\nzz\n")); + QList sels; + for (int line = 0; line < 3; ++line) { + QTextCursor cur(edit->document()); + const int blockPos = edit->document()->findBlockByNumber(line).position(); + cur.setPosition(blockPos); + cur.setPosition(blockPos + 1, QTextCursor::KeepAnchor); + QTextEdit::ExtraSelection sel; + sel.cursor = cur; + sels << sel; + } + edit->restoreColumnEditSelection(sels); + edit->m_bIsAltMod = true; + QApplication::clipboard()->setText(QStringLiteral("1")); + + // Act: 列粘贴 + edit->paste(); + + // Assert: 每个选区被剪贴板文本替换 + EXPECT_EQ(edit->toPlainText(), QString("1x\n1y\n1z\n")); + EXPECT_EQ(edit->m_altModSelections.size(), 3); + + // Act: 撤销整次列粘贴 + edit->undo_(); + + // Assert: 文本与列选区完整还原(可逆) + EXPECT_EQ(edit->toPlainText(), QString("xx\nyy\nzz\n")); + ASSERT_EQ(edit->m_altModSelections.size(), 3); + EXPECT_EQ(edit->m_altModSelections.first().cursor.selectedText(), QString("x")); + EXPECT_EQ(edit->m_altModSelections.last().cursor.selectedText(), QString("z")); +} diff --git a/tests/editor_core/test_dtextedit_mark.cpp b/tests/editor_core/test_dtextedit_mark.cpp index 8697f84f..80e8b3b7 100644 --- a/tests/editor_core/test_dtextedit_mark.cpp +++ b/tests/editor_core/test_dtextedit_mark.cpp @@ -1484,3 +1484,44 @@ TEST_F(TextEditTest, BUG331945_UpdateMark_ColumnSelection_ContainedMarksRemoved) // Assert: 两个标记均被 [0,20] 完全包含 → 同批次移除,索引不错位 EXPECT_TRUE(edit->m_wordMarkSelections.isEmpty()); } + +// PMS: https://pms.uniontech.com/bug-view-44591.html commit: N/A(批次4:git 历史无修复提交,按 PMS 复现步骤 + master d634cbad 代码语义锚定) +// 场景:右键菜单 → 颜色标记 → 高亮所有,标记无任何颜色效果。 +// 锚定语义:slotSigColorAllSelected(true, color) → isMarkAllLine 以 color.name() 建立 +// MarkAllMatch 记录 + m_mapKeywordMarkSelections[选中文本] 全文匹配选区(格式背景色为所选 +// 颜色)→ renderAllSelections 渲染进 extraSelections。 +TEST_F(TextEditTest, BUG44591_ColorMarkAll_AppliesChosenColor) +{ + // Arrange: 文本含 3 处目标词,选中第一处;直连颜色面板选择信号 + setDocText(QString("cat dog cat bird cat")); + QTextCursor cur(edit->document()); + cur.setPosition(0); + cur.setPosition(3, QTextCursor::KeepAnchor); // 选中 "cat" + edit->setTextCursor(cur); + const QColor chosen(255, 127, 80); + + // Act: 颜色面板确认(高亮所有选中内容) + edit->slotSigColorAllSelected(true, chosen); + + // Assert: 操作记录携带所选颜色,类型为 MarkAllMatch + ASSERT_FALSE(edit->m_markOperations.isEmpty()); + const auto &op = edit->m_markOperations.last().first; + EXPECT_EQ(op.type, TextEdit::MarkAllMatch); + EXPECT_EQ(op.color, chosen.name()); + EXPECT_EQ(op.matchText, QString("cat")); + + // Assert: 关键词标记映射建立,全部匹配选区背景色为所选颜色 + ASSERT_TRUE(edit->m_mapKeywordMarkSelections.contains(QString("cat"))); + const auto marked = edit->m_mapKeywordMarkSelections.value(QString("cat")); + EXPECT_EQ(marked.size(), 3); + for (const auto &pair : marked) + EXPECT_EQ(pair.first.format.background().color(), chosen); + + // Assert: 渲染结果(extraSelections)实际携带该背景色(修复前无颜色效果) + bool rendered = false; + for (const auto &es : edit->extraSelections()) { + if (es.format.background().color() == chosen) + rendered = true; + } + EXPECT_TRUE(rendered); +} diff --git a/tests/startmanager/test_startmanager.cpp b/tests/startmanager/test_startmanager.cpp index dd0c043a..28d41ff9 100644 --- a/tests/startmanager/test_startmanager.cpp +++ b/tests/startmanager/test_startmanager.cpp @@ -168,6 +168,7 @@ #include #include "stubext.h" +#include #include #include #include @@ -2757,3 +2758,142 @@ TEST_F(StartManagerTest, BUG324727_MultiRecords_AllTabsRecovered) EXPECT_EQ(pendingCalls, 2); EXPECT_EQ(pendingPaths, QStringList({ lazyB, lazyC })); } + +// PMS: https://pms.uniontech.com/bug-view-55533.html commit: N/A(批次4:git 历史无修复提交,按 PMS 复现步骤 + master d634cbad 代码语义锚定) +// 场景:关闭其中一个窗口后,应用直接退出。 +// 锚定语义:slotCloseWindow 仅当窗口列表清空才走清退(unregister + 延迟 quit); +// 多窗口关闭其一 → 仅移除该窗口,剩余窗口完好,绝不调度退出/注销总线。 +TEST_F(StartManagerTest, BUG55533_CloseOneOfTwoWindows_RemainingIntact_NoQuit) +{ + // Arrange:两个窗口,关闭其一 + Window *winA = qobjFake(); + Window *winB = qobjFake(); + obj->m_windows << winA << winB; + stub.set_lamda(static_cast(&QObject::sender), + [winB](const QObject *) -> QObject * { return winB; }); + curPathOverride = tmp->path(); + + // Act + obj->slotCloseWindow(); + + // Assert:剩余窗口完好;未注销 DBus 总线、未调度退出(含延迟任务也未触发) + ASSERT_EQ(obj->m_windows.count(), 1); + EXPECT_EQ(obj->m_windows.at(0), winA); + EXPECT_EQ(unregisterCalls, 0); + EXPECT_EQ(lastUnregisterService, QString()); + EXPECT_EQ(quitCalls, 0); + processEventsFor(1200); // 即便误调度 1000ms 延迟退出也会在此暴露 + EXPECT_EQ(quitCalls, 0); +} + +// PMS: https://pms.uniontech.com/bug-view-177757.html commit: N/A(批次4:无修复提交,按 PMS 步骤 + master 代码语义锚定) +// 场景:多个窗口被快速连续关闭(用户连按 alt+F4),偶发应用异常。 +// 锚定语义:逐个移除仅移除对应窗口;只有最后一次(列表清空)进入清退分支—— +// 恰好一次 unregister、恰好一次延迟 quit 调度,不重复调度、不闪退。 +TEST_F(StartManagerTest, BUG177757_RapidSequentialClose_SingleQuitScheduling) +{ + // Arrange:三个窗口 + 发送者队列(模拟快速连关) + std::deque senders; + Window *w1 = qobjFake(); + Window *w2 = qobjFake(); + Window *w3 = qobjFake(); + senders.push_back(w1); + senders.push_back(w2); + senders.push_back(w3); + obj->m_windows << w1 << w2 << w3; + stub.set_lamda(static_cast(&QObject::sender), + [&senders](const QObject *) -> QObject * { + Window *w = senders.front(); + senders.pop_front(); + return w; + }); + curPathOverride = tmp->path(); + + // Act:快速连续关闭三个窗口(中间不跑事件循环) + obj->slotCloseWindow(); + obj->slotCloseWindow(); + obj->slotCloseWindow(); + + // Assert:前两次仅移除;末次清退恰好一次注销 + 一次延迟退出 + EXPECT_TRUE(obj->m_windows.isEmpty()); + EXPECT_EQ(unregisterCalls, 1); + EXPECT_EQ(lastUnregisterService, QString("com.deepin.Editor")); + EXPECT_EQ(quitCalls, 0); // 延迟 1000ms 尚未触发 + processEventsFor(1200); + EXPECT_EQ(quitCalls, 1); // 仅一次退出调度(重复调度会 >1) +} + +// PMS: https://pms.uniontech.com/bug-view-49292.html commit: N/A(批次4:无修复提交,按 PMS 步骤 + master 代码语义锚定) +// 场景:重复开关后无法打开(总线名 com.deepin.Editor 被残留进程占用)。 +// 锚定语义:末窗关闭即先注销总线名再延迟退出(startmanager.cpp 末窗清退分支)—— +// unregisterService("com.deepin.Editor") 先于 quit 发生,让下一次启动能成功注册。 +TEST_F(StartManagerTest, BUG49292_LastClose_FreesBusBeforeQuit) +{ + // Arrange:单窗口关闭进入清退;桩内记录事件顺序 + Window *win = qobjFake(); + obj->m_windows << win; + stub.set_lamda(static_cast(&QObject::sender), + [win](const QObject *) -> QObject * { return win; }); + curPathOverride = tmp->path(); + QStringList order; + int unregCalls = 0; + stub.set_lamda( + static_cast(&QDBusConnection::unregisterService), + [&order, &unregCalls](QDBusConnection *, const QString &svc) -> bool { + order << QStringLiteral("unregister:") + svc; + ++unregCalls; + return true; + }); + stub.set_lamda(&QCoreApplication::quit, [&order]() -> void { + order << QStringLiteral("quit"); + }); + + // Act + obj->slotCloseWindow(); + + // Assert:注销先于退出;总线名正确释放一次(新进程可注册) + EXPECT_TRUE(obj->m_windows.isEmpty()); + EXPECT_EQ(unregCalls, 1); + processEventsFor(1200); + EXPECT_EQ(order, QStringList({ QStringLiteral("unregister:com.deepin.Editor"), + QStringLiteral("quit") })); +} + +// PMS: https://pms.uniontech.com/bug-view-177725.html commit: N/A(批次4:无修复提交,按 PMS 步骤 + master 代码语义锚定) +// 场景:设置中取消勾选"保留页签"后打开文件,打开的是新建文本而非文件内容。 +// 锚定语义:openFilesInTab 带文件参数且无窗口 → createWindow(true) + showCenterWindow + +// 延迟 50ms recoverFile(无临时记录时恢复 0 个) + window->addTab(resolvedFile)—— +// 文件参数路径必须落为文件 tab,不能退化为空白新建。 +TEST_F(StartManagerTest, BUG177725_KeepTabsDisabled_OpenFile_OpensFileTab) +{ + // Arrange:无窗口 + 无临时记录(保留页签已取消)+ 带文件参数 + obj->m_qlistTemFile = QStringList(); // 无临时记录 + const QString file = tmp->filePath("keep_off.txt"); + Window *newWin = qobjFake(); + int recoverCalls = 0; + int blankCalls = 0; + QString addedPath; + stub.set_lamda(static_cast(&StartManager::createWindow), + [newWin](StartManager *, bool) -> Window * { return newWin; }); + stubWindowInteraction(newWin); + stub.set_lamda(static_cast(&StartManager::recoverFile), + [&recoverCalls](StartManager *, Window *) -> int { + ++recoverCalls; + return 0; + }); + stub.set_lamda(static_cast(&Window::addBlankTab), + [&blankCalls](Window *) { ++blankCalls; }); + stub.set_lamda(static_cast(&Window::addTab), + [&addedPath](Window *, const QString &p, bool) { addedPath = p; }); + + // Act + obj->openFilesInTab(QStringList { file }); + processEventsFor(100); // 触发 50ms 延迟打开 + + // Assert:文件 tab 被添加(非空白新建);恢复空转一次 + EXPECT_EQ(showCenterCalls, 1); + EXPECT_TRUE(lastCenterFlag); + EXPECT_EQ(recoverCalls, 1); + EXPECT_EQ(blankCalls, 0); + EXPECT_EQ(addedPath, file); +} diff --git a/tests/widgets_ut/test_window.cpp b/tests/widgets_ut/test_window.cpp index 27e0eac5..8d6af7ef 100644 --- a/tests/widgets_ut/test_window.cpp +++ b/tests/widgets_ut/test_window.cpp @@ -3501,3 +3501,179 @@ TEST_F(WindowTest, BUG282985_CloseTab_TriggersMemoryTrim) // Assert: 关闭 tab 触发延迟内存回收 EXPECT_GE(trimCalls, 1); } + +// PMS: https://pms.uniontech.com/bug-view-37292.html commit: N/A(批次4:git 历史无修复提交,按 PMS 复现步骤 + master d634cbad 代码语义锚定) +// 场景:标签页拖出当前窗口再拖回后,右键查找/替换/跳行闪退。 +// 锚定语义:移出 = Tabbar::closeTab + Window::removeWrapper(false)(handleTabReleased 移除侧); +// 移回 = addTabWithWrapper 重建 textEditor 信号连接(insertFromMimeDataOnDragEnter / +// createWindowFromWrapper 动画完成回填侧)。回填后 popupFindBar/popupReplaceBar/popupJumpLineBar +// 均依赖 currentWrapper 空判 + 非空文档守卫,必须正常弹出且不闪退。 +TEST_F(WindowTest, BUG37292_TabMoveOutAndBack_BarsPopup_NoCrash) +{ + // Arrange: 打开带内容的文件 tab,模拟"拖出窗口"(closeTab + 摘除 wrapper,不删除) + const QString path = addFileTab(QStringLiteral("move.txt"), "find me\nsecond line\n"); + EditWrapper *w = m_win->wrapper(path); + ASSERT_NE(w, nullptr); + const QString truePath = w->textEditor()->getTruePath(); + int idx = -1; + for (int i = 0; i < m_tabbar->count(); ++i) { + if (m_tabbar->fileAt(i) == path) { idx = i; break; } + } + ASSERT_GE(idx, 0); + m_tabbar->closeTab(idx); + m_win->removeWrapper(path, false); + QApplication::processEvents(); + ASSERT_EQ(m_win->wrapper(path), nullptr); // 已移出 + ASSERT_EQ(m_win->currentWrapper(), nullptr); + + // Act: 拖回原窗口(addTabWithWrapper 重建连接),依次右键查找/替换/跳行 + // 注:子栏 isVisible 需窗口可见;popup* 内有 10ms focus 定时器需事件循环 + m_win->show(); + m_win->addTabWithWrapper(w, path, truePath, QFileInfo(path).fileName(), -1); + QApplication::processEvents(); + ASSERT_EQ(m_win->wrapper(path), w); + ASSERT_EQ(m_win->currentWrapper(), w); + EXPECT_NO_FATAL_FAILURE(m_win->popupFindBar()); + processEventsFor(60); + EXPECT_TRUE(m_win->findBarIsVisiable()); + + EXPECT_NO_FATAL_FAILURE(m_win->popupReplaceBar()); + processEventsFor(60); + EXPECT_TRUE(m_win->replaceBarIsVisiable()); + + EXPECT_NO_FATAL_FAILURE(m_win->popupJumpLineBar()); + processEventsFor(60); + EXPECT_TRUE(m_win->m_jumpLineBar->isVisible()); +} + +// PMS: https://pms.uniontech.com/bug-view-46084.html commit: N/A(批次4:无修复提交,按 PMS 步骤 + master 代码语义锚定) +// 场景:标签页拖拽至其他窗口,高概率闪退。 +// 锚定语义:跨窗口迁移的往返闭环——移出(closeTab+removeWrapper(false),wrapper 保留)→ +// 回填(addTabWithWrapper)。反复三轮后 wrapper 存活、标签数守恒、映射无悬挂键。 +TEST_F(WindowTest, BUG46084_TabMoveBackForth_RepeatedTransfer_WrapperIntact) +{ + // Arrange + const QString path = addFileTab(QStringLiteral("drag.txt"), "drag content\n"); + EditWrapper *w = m_win->wrapper(path); + ASSERT_NE(w, nullptr); + const QString truePath = w->textEditor()->getTruePath(); + + // Act+Assert: 三轮"移出→移回" + for (int round = 0; round < 3; ++round) { + int idx = -1; + for (int i = 0; i < m_tabbar->count(); ++i) { + if (m_tabbar->fileAt(i) == path) { idx = i; break; } + } + ASSERT_GE(idx, 0); + m_tabbar->closeTab(idx); + m_win->removeWrapper(path, false); + QApplication::processEvents(); + EXPECT_EQ(m_win->m_wrappers.contains(path), false); + EXPECT_FALSE(w == nullptr); // wrapper 未被销毁(UAF 防护语义) + + m_win->addTabWithWrapper(w, path, truePath, QFileInfo(path).fileName(), -1); + QApplication::processEvents(); + EXPECT_EQ(m_win->wrapper(path), w); + EXPECT_EQ(m_tabbar->count(), 1); // 无重复/幽灵 tab + } + EXPECT_EQ(m_win->currentWrapper(), w); +} + +// PMS: https://pms.uniontech.com/bug-view-48750.html commit: N/A(批次4:无修复提交,按 PMS 步骤 + master 代码语义锚定) +// 场景:标签页被拖拽至新窗口后,原窗口仍显示该标签页;关闭后应用直接退出。 +// 锚定语义:移出后原窗口不得残留"幽灵标签"——tabbar 无该路径、wrapper 映射已摘除(removeWrapper +// 第二参 false 仅摘除不删除);且摘除只发生一次(不重复入映射)。 +TEST_F(WindowTest, BUG48750_TabMovedOut_SourceWindowNoGhostTab) +{ + // Arrange: 两个 tab,模拟第一个被拖出新窗口 + const QString moved = addFileTab(QStringLiteral("ghost.txt"), "ghost\n"); + const QString kept = addFileTab(QStringLiteral("kept.txt"), "kept\n"); + EditWrapper *wMoved = m_win->wrapper(moved); + ASSERT_NE(wMoved, nullptr); + ASSERT_EQ(m_tabbar->count(), 2); + + // Act: 移出(handleTabReleased 移除侧语义:closeTab + removeWrapper(false)) + int idx = -1; + for (int i = 0; i < m_tabbar->count(); ++i) { + if (m_tabbar->fileAt(i) == moved) { idx = i; break; } + } + ASSERT_GE(idx, 0); + m_tabbar->closeTab(idx); + m_win->removeWrapper(moved, false); + QApplication::processEvents(); + + // Assert: 原窗口无幽灵标签;wrapper 存活(供新窗口回填),仅剩 kept + EXPECT_EQ(m_win->m_wrappers.contains(moved), false); + EXPECT_NE(wMoved, nullptr); + EXPECT_EQ(m_tabbar->count(), 1); + EXPECT_EQ(m_tabbar->fileAt(0), kept); + EXPECT_EQ(m_win->wrapper(kept), m_win->m_wrappers.value(kept)); +} + +// PMS: https://pms.uniontech.com/bug-view-157589.html commit: N/A(批次4:无修复提交,按 PMS 步骤 + master 代码语义锚定) +// 场景:wayland 下拖入 txt 文本文件闪退(外部拖放源 source()==nullptr)。 +// 锚定语义:Window::dropEvent 仅依赖 mimeData->hasUrls() 分流 addTab,不依赖 +// event->source();外部(无来源)拖入受支持文件必须正常开 tab 且不闪退。 +TEST_F(WindowTest, BUG157589_DropEvent_FileUrlWithoutSource_OpensFile) +{ + // Arrange: 单个 txt 文件 URL 的放下事件(QDropEvent 构造不携带 source → 外部拖放语义) + const QString file = createFile(QStringLiteral("wayland.txt"), "external drop\n"); + ASSERT_FALSE(file.isEmpty()); + QMimeData mime; + mime.setUrls({ QUrl::fromLocalFile(file) }); + QDropEvent ev(QPointF(10, 10), Qt::DropActions(Qt::CopyAction), &mime, + Qt::LeftButton, Qt::NoModifier); + ASSERT_EQ(ev.source(), nullptr); + + // Act + EXPECT_NO_FATAL_FAILURE(m_win->dropEvent(&ev)); + QApplication::processEvents(); + + // Assert: 文件正常打开为 tab,无闪退、无异常提示 + EXPECT_NE(m_win->wrapper(file), nullptr); + EXPECT_EQ(m_iconMsgCalls, 0); +} + +// PMS: https://pms.uniontech.com/bug-view-41754.html commit: N/A(批次4:无修复提交,按 PMS 步骤 + master 代码语义锚定) +// 场景:ESC 切至标题栏后用 Tab 切换标题栏/底栏控件,焦点消失。 +// 锚定语义:slot_setTitleFocus 将标题栏按钮全部转入 Tab 焦点链(initTitlebar 默认 NoFocus), +// 建立 addButton→option→min→quitFull→max→close 的 tabOrder,并经 +// bottomBar()->setChildrenFocus(true, closeBtn) 把焦点链续接到底栏菜单按钮。 +TEST_F(WindowTest, BUG41754_SlotSetTitleFocus_ChainsFocusToBottomBar) +{ + // Arrange: 存在当前 wrapper(底栏可访问) + addBlankAndGetPath(); + ASSERT_NE(m_win->currentWrapper(), nullptr); + + // Act + m_win->slot_setTitleFocus(); + + // Assert: 标题栏与全部窗口按钮进入 Tab 焦点链 + EXPECT_EQ(m_win->titlebar()->focusPolicy(), Qt::TabFocus); + const QStringList buttons = { "AddButton", "DTitlebarDWindowOptionButton", + "DTitlebarDWindowMinButton", "DTitlebarDWindowQuitFullscreenButton", + "DTitlebarDWindowMaxButton", "DTitlebarDWindowCloseButton" }; + QWidget *prev = nullptr; + for (const QString &name : buttons) { + QWidget *btn = (name == "AddButton") + ? m_win->getTabbar()->findChild(name) + : m_win->titlebar()->findChild(name); + ASSERT_NE(btn, nullptr) << name.toStdString(); + EXPECT_EQ(btn->focusPolicy(), Qt::TabFocus) << name.toStdString(); + if (prev) { + // tabOrder 链:prev 的 focus 代理下一个即为 btn + EXPECT_EQ(prev->nextInFocusChain(), btn) << name.toStdString(); + } + prev = btn; + } + + // Assert: 焦点链续接到底栏三个菜单按钮(encode/format/highlight) + BottomBar *bar = m_win->currentWrapper()->bottomBar(); + ASSERT_NE(bar, nullptr); + EXPECT_NE(bar->m_pEncodeMenu->getButton(), nullptr); + EXPECT_NE(bar->m_formatMenu->getButton(), nullptr); + EXPECT_NE(bar->m_pHighlightMenu->getButton(), nullptr); + EXPECT_EQ(prev->nextInFocusChain(), bar->m_pEncodeMenu->getButton()); + EXPECT_EQ(bar->m_pEncodeMenu->getButton()->nextInFocusChain(), bar->m_formatMenu->getButton()); + EXPECT_EQ(bar->m_formatMenu->getButton()->nextInFocusChain(), bar->m_pHighlightMenu->getButton()); +} From 1c4f8645c34b2f9f2415b8334cf59360f449e64c Mon Sep 17 00:00:00 2001 From: zhanghongyuan Date: Fri, 9 Oct 2026 09:53:30 +0800 Subject: [PATCH 2/2] test(ut): drop stale manual deleteLater in BUG184107 case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error path already registers quit()+deleteLater() since upstream a66591ed, so the manual deleteLater in the test ran on an already destroyed object and SIGBUSed the suite after relink; assert guard cleared by runSync's DeferredDelete dispatch instead. 异常路径自 a66591ed 起已自带 quit()+deleteLater(),用例中手动补删在 对象析构后执行导致重链后 SIGBUS;改为断言 runSync 派发后 guard 已清空。 Log: 修复批次2用例在新基线上的释放语义错配 Influence: 仅测试文件变更,不影响应用功能。 --- tests/common2/test_fileloadthread.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/common2/test_fileloadthread.cpp b/tests/common2/test_fileloadthread.cpp index af255903..33649f4c 100644 --- a/tests/common2/test_fileloadthread.cpp +++ b/tests/common2/test_fileloadthread.cpp @@ -453,9 +453,7 @@ TEST_F(FileLoadThreadTest, BUG184107_BadAllocDuringRead_EmitsErrorNoCrash) EXPECT_TRUE(spy.at(0).at(2).toBool()); EXPECT_FALSE(spy.at(0).at(3).toBool()); } - // 当前代码:catch 分支提前 return(:88),deleteLater 仅在正常路径末尾(:132)注册, - // 故此处 guard 未清空属预期;手动补 deleteLater 防泄漏后断言回收 - t->deleteLater(); - QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + // 当前代码(master a66591ed):catch 分支已自带 quit()+deleteLater(), + // runSync 的 DeferredDelete 派发即完成回收,无需(也不得)再手动补删 EXPECT_TRUE(guard.isNull()); }