feat(controller): WorkbenchNavController 异步化(NavRequest续延+并发计数, abort-and-replace+身份比对, 删busy_/drain/BusyGuard, busyChanged=在飞存在性) + 单测
- 控制器依赖切换到 IAsyncProjectRepository(异步句柄) - 删除 busy_/BusyGuard/drainPendingCheckedTms/checkedTmsPending_/pendingCheckedTms_/friend struct BusyGuard - start/switchWorkspace 用 NavRequest 续延依赖链(startStepReq_ 跟踪当前在飞级) - switchProject/loadMore*/selectDataset 单请求 + abort-and-replace + 身份比对 - selectObject 三并发(data/file/detail), 各自身份比对独立 emit - setCheckedTms 并发拉取未命中缓存项, 计数汇聚; 新勾选 abort 旧批(以最后一次为准); tmExceptionCache_ 命中不发请求 - busyChanged 由 anyInflight() 驱动(emitBusyIfChanged 去抖, 值变才发) - 析构 abortAll() 退出契约 - 对外信号面零改动, main.cpp 接线据引用绑定自动切换(无需改) - 新增 9 个控制器单测(依赖链/并发/abort-and-replace/busyChanged/缓存语义/回灌防护/失败路径) - 测试 96 -> 105 全绿
This commit is contained in:
parent
05f0bf3d4f
commit
b097fa6e56
|
|
@ -1,192 +1,387 @@
|
|||
#include "WorkbenchNavController.hpp"
|
||||
|
||||
#include <QMetaObject>
|
||||
|
||||
#include "api/NavLoads.hpp"
|
||||
#include "api/NavRequest.hpp"
|
||||
#include "dto/NavDto.hpp"
|
||||
#include "repo/IAsyncProjectRepository.hpp"
|
||||
|
||||
namespace geopro::controller {
|
||||
|
||||
using data::DsPage;
|
||||
using data::DynamicForm;
|
||||
using data::ExceptionRow;
|
||||
using data::NavRequest;
|
||||
using data::ProjectListPage;
|
||||
using data::ProjectSummary;
|
||||
using data::StructNode;
|
||||
using data::Workspace;
|
||||
|
||||
WorkbenchNavController::WorkbenchNavController(data::IProjectRepository& repo, QObject* parent)
|
||||
WorkbenchNavController::WorkbenchNavController(data::IAsyncProjectRepository& repo, QObject* parent)
|
||||
: QObject(parent), repo_(repo) {}
|
||||
|
||||
// RAII:进入公共导航操作时置忙(驱动等待光标),任何返回路径都复位——保证 busyChanged 配平。
|
||||
// 命名(非匿名)以匹配 controller 的 friend 声明,从而在析构时排空挂起的勾选请求。
|
||||
struct BusyGuard {
|
||||
WorkbenchNavController* self;
|
||||
bool* busy;
|
||||
BusyGuard(WorkbenchNavController* s, bool* b) : self(s), busy(b) {
|
||||
*busy = true;
|
||||
emit self->busyChanged(true);
|
||||
}
|
||||
~BusyGuard() {
|
||||
WorkbenchNavController* ctrl = self; // 取本地副本:lambda 不能捕获成员
|
||||
*busy = false;
|
||||
emit ctrl->busyChanged(false);
|
||||
// 触发源是延迟合并发射,可能落在嵌套事件循环里:用队列调用在调用栈/嵌套循环展开后再排空,
|
||||
// 那时 busy_ 已可靠为 false,重放才会真正执行(lambda 捕获的 ctrl 生命周期与应用一致,安全)。
|
||||
if (ctrl->checkedTmsPending_)
|
||||
QMetaObject::invokeMethod(
|
||||
ctrl, [ctrl] { ctrl->drainPendingCheckedTms(); }, Qt::QueuedConnection);
|
||||
}
|
||||
};
|
||||
WorkbenchNavController::~WorkbenchNavController() { abortAll(); }
|
||||
|
||||
void WorkbenchNavController::start() {
|
||||
if (busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
const auto ws = repo_.listWorkspaces();
|
||||
if (!ws.ok) {
|
||||
emit loadFailed(QStringLiteral("workspaces"), QString::fromStdString(ws.error));
|
||||
return;
|
||||
}
|
||||
QString cur;
|
||||
for (const auto& w : ws.value)
|
||||
if (w.isCurrent) cur = QString::fromStdString(w.id);
|
||||
if (cur.isEmpty() && !ws.value.empty()) cur = QString::fromStdString(ws.value.front().id);
|
||||
currentWorkspaceId_ = cur.toStdString();
|
||||
emit workspacesLoaded(ws.value, cur);
|
||||
loadProjectsAndStructure();
|
||||
bool WorkbenchNavController::anyInflight() const {
|
||||
if (startStepReq_ || structReq_ || selDataReq_ || selFileReq_ || selDetailReq_ ||
|
||||
moreDataReq_ || moreFilesReq_ || datasetReq_)
|
||||
return true;
|
||||
for (const auto& h : checkedInflight_)
|
||||
if (h) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void WorkbenchNavController::loadProjectsAndStructure() {
|
||||
const auto ps = repo_.pageProjects(std::string(), std::string(), 1, 10); // 下拉首页 10
|
||||
if (!ps.ok) {
|
||||
emit loadFailed(QStringLiteral("projects"), QString::fromStdString(ps.error));
|
||||
return;
|
||||
void WorkbenchNavController::emitBusyIfChanged() {
|
||||
const bool now = anyInflight();
|
||||
if (now != lastBusy_) {
|
||||
lastBusy_ = now;
|
||||
emit busyChanged(now);
|
||||
}
|
||||
lastProjects_ = ps.value.rows;
|
||||
tmExceptionCache_.clear();
|
||||
currentParentId_.clear(); // 切项目/工作空间重置选中态(spec §6)
|
||||
currentParentConfType_ = 0;
|
||||
checkedTmsPending_ = false; // 丢弃跨项目的陈旧挂起重放
|
||||
pendingCheckedTms_.clear();
|
||||
QString curP;
|
||||
if (!ps.value.rows.empty()) {
|
||||
const auto& first = ps.value.rows.front();
|
||||
curP = QString::fromStdString(first.id);
|
||||
currentProjectId_ = first.id;
|
||||
currentProjectName_ = first.name;
|
||||
currentCrsCode_ = first.crsCode;
|
||||
} else {
|
||||
currentProjectId_.clear();
|
||||
currentProjectName_.clear();
|
||||
currentCrsCode_.clear();
|
||||
}
|
||||
emit projectsLoaded(ps.value.rows, curP, ps.value.total);
|
||||
}
|
||||
|
||||
if (curP.isEmpty()) {
|
||||
lastStructNodes_.clear();
|
||||
emit structureLoaded(QString(), {}); // 暂无项目 → 空树
|
||||
return;
|
||||
}
|
||||
const auto st = repo_.loadStructure(currentProjectId_);
|
||||
if (!st.ok) {
|
||||
emit loadFailed(QStringLiteral("structure"), QString::fromStdString(st.error));
|
||||
return;
|
||||
}
|
||||
lastStructNodes_ = st.value;
|
||||
emit structureLoaded(QString::fromStdString(currentProjectName_), st.value);
|
||||
void WorkbenchNavController::abortAll() {
|
||||
if (startStepReq_) startStepReq_->abort();
|
||||
if (structReq_) structReq_->abort();
|
||||
if (selDataReq_) selDataReq_->abort();
|
||||
if (selFileReq_) selFileReq_->abort();
|
||||
if (selDetailReq_) selDetailReq_->abort();
|
||||
if (moreDataReq_) moreDataReq_->abort();
|
||||
if (moreFilesReq_) moreFilesReq_->abort();
|
||||
if (datasetReq_) datasetReq_->abort();
|
||||
for (const auto& h : checkedInflight_)
|
||||
if (h) h->abort();
|
||||
checkedInflight_.clear();
|
||||
}
|
||||
|
||||
void WorkbenchNavController::resetSelectionState() {
|
||||
tmExceptionCache_.clear();
|
||||
currentParentId_.clear();
|
||||
currentParentConfType_ = 0;
|
||||
}
|
||||
|
||||
// ── start / switchWorkspace 依赖链:listWorkspaces → pageProjects → loadStructure ──
|
||||
// 用 NavRequest 续延(每级 done 内用业务值构造下一级),startStepReq_ 跟踪当前在飞级。
|
||||
// abort-and-replace:start/switchWorkspace 入口 abort 旧 startStepReq_ → 自然丢弃旧链迟到信号。
|
||||
|
||||
void WorkbenchNavController::start() {
|
||||
if (startStepReq_) startStepReq_->abort();
|
||||
NavRequest* req = repo_.listWorkspacesAsync();
|
||||
startStepReq_ = req;
|
||||
emitBusyIfChanged();
|
||||
QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) {
|
||||
if (req != startStepReq_) return; // §5.0 身份比对
|
||||
startStepReq_.clear();
|
||||
const auto ws = qvariant_cast<std::vector<Workspace>>(v);
|
||||
QString cur;
|
||||
for (const auto& w : ws)
|
||||
if (w.isCurrent) cur = QString::fromStdString(w.id);
|
||||
if (cur.isEmpty() && !ws.empty()) cur = QString::fromStdString(ws.front().id);
|
||||
currentWorkspaceId_ = cur.toStdString();
|
||||
emit workspacesLoaded(ws, cur);
|
||||
runProjectsAndStructure();
|
||||
});
|
||||
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
|
||||
if (req != startStepReq_) return;
|
||||
startStepReq_.clear();
|
||||
emit loadFailed(QStringLiteral("workspaces"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void WorkbenchNavController::switchWorkspace(const QString& tenantId) {
|
||||
if (tenantId.isEmpty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
const auto r = repo_.switchWorkspace(tenantId.toStdString());
|
||||
if (!r.ok) {
|
||||
emit loadFailed(QStringLiteral("switchWorkspace"), QString::fromStdString(r.error));
|
||||
return;
|
||||
}
|
||||
currentWorkspaceId_ = tenantId.toStdString();
|
||||
loadProjectsAndStructure();
|
||||
if (tenantId.isEmpty()) return;
|
||||
if (startStepReq_) startStepReq_->abort();
|
||||
NavRequest* req = repo_.switchWorkspaceAsync(tenantId.toStdString());
|
||||
startStepReq_ = req;
|
||||
emitBusyIfChanged();
|
||||
const std::string tid = tenantId.toStdString();
|
||||
QObject::connect(req, &NavRequest::done, this, [this, req, tid](const QVariant&) {
|
||||
if (req != startStepReq_) return;
|
||||
startStepReq_.clear();
|
||||
currentWorkspaceId_ = tid;
|
||||
runProjectsAndStructure();
|
||||
});
|
||||
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
|
||||
if (req != startStepReq_) return;
|
||||
startStepReq_.clear();
|
||||
emit loadFailed(QStringLiteral("switchWorkspace"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void WorkbenchNavController::runProjectsAndStructure() {
|
||||
NavRequest* req = repo_.pageProjectsAsync(std::string(), std::string(), 1, 10); // 下拉首页 10
|
||||
startStepReq_ = req;
|
||||
emitBusyIfChanged();
|
||||
QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) {
|
||||
if (req != startStepReq_) return;
|
||||
startStepReq_.clear();
|
||||
const auto page = qvariant_cast<ProjectListPage>(v);
|
||||
lastProjects_ = page.rows;
|
||||
resetSelectionState();
|
||||
QString curP;
|
||||
if (!page.rows.empty()) {
|
||||
const auto& first = page.rows.front();
|
||||
curP = QString::fromStdString(first.id);
|
||||
currentProjectId_ = first.id;
|
||||
currentProjectName_ = first.name;
|
||||
currentCrsCode_ = first.crsCode;
|
||||
} else {
|
||||
currentProjectId_.clear();
|
||||
currentProjectName_.clear();
|
||||
currentCrsCode_.clear();
|
||||
}
|
||||
emit projectsLoaded(page.rows, curP, page.total);
|
||||
if (curP.isEmpty()) {
|
||||
lastStructNodes_.clear();
|
||||
emit structureLoaded(QString(), {}); // 暂无项目 → 空树
|
||||
emitBusyIfChanged();
|
||||
return;
|
||||
}
|
||||
NavRequest* st = repo_.loadStructureAsync(currentProjectId_);
|
||||
startStepReq_ = st;
|
||||
emitBusyIfChanged();
|
||||
QObject::connect(st, &NavRequest::done, this, [this, st](const QVariant& sv) {
|
||||
if (st != startStepReq_) return;
|
||||
startStepReq_.clear();
|
||||
const auto nodes = qvariant_cast<std::vector<StructNode>>(sv);
|
||||
lastStructNodes_ = nodes;
|
||||
emit structureLoaded(QString::fromStdString(currentProjectName_), nodes);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
QObject::connect(st, &NavRequest::failed, this, [this, st](const QString& msg) {
|
||||
if (st != startStepReq_) return;
|
||||
startStepReq_.clear();
|
||||
emit loadFailed(QStringLiteral("structure"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
});
|
||||
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
|
||||
if (req != startStepReq_) return;
|
||||
startStepReq_.clear();
|
||||
emit loadFailed(QStringLiteral("projects"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
}
|
||||
|
||||
// ── switchProject:单请求 loadStructure ──
|
||||
void WorkbenchNavController::switchProject(const QString& projectId) {
|
||||
if (projectId.isEmpty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
if (projectId.isEmpty()) return;
|
||||
currentProjectId_ = projectId.toStdString();
|
||||
for (const auto& p : lastProjects_)
|
||||
if (p.id == currentProjectId_) {
|
||||
currentProjectName_ = p.name;
|
||||
currentCrsCode_ = p.crsCode;
|
||||
}
|
||||
const auto st = repo_.loadStructure(currentProjectId_);
|
||||
if (!st.ok) {
|
||||
emit loadFailed(QStringLiteral("structure"), QString::fromStdString(st.error));
|
||||
return;
|
||||
}
|
||||
lastStructNodes_ = st.value;
|
||||
tmExceptionCache_.clear();
|
||||
currentParentId_.clear(); // 切项目/工作空间重置选中态(spec §6)
|
||||
currentParentConfType_ = 0;
|
||||
checkedTmsPending_ = false; // 丢弃跨项目的陈旧挂起重放
|
||||
pendingCheckedTms_.clear();
|
||||
emit structureLoaded(QString::fromStdString(currentProjectName_), st.value);
|
||||
if (structReq_) structReq_->abort(); // abort-and-replace
|
||||
NavRequest* req = repo_.loadStructureAsync(currentProjectId_);
|
||||
structReq_ = req;
|
||||
emitBusyIfChanged();
|
||||
QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) {
|
||||
if (req != structReq_) return; // §5.0 身份比对
|
||||
structReq_.clear();
|
||||
const auto nodes = qvariant_cast<std::vector<StructNode>>(v);
|
||||
lastStructNodes_ = nodes;
|
||||
resetSelectionState();
|
||||
emit structureLoaded(QString::fromStdString(currentProjectName_), nodes);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
|
||||
if (req != structReq_) return;
|
||||
structReq_.clear();
|
||||
emit loadFailed(QStringLiteral("structure"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
}
|
||||
|
||||
// ── selectObject:三并发(data 行 / file 行 / 对象详情),各自身份比对、独立 emit ──
|
||||
void WorkbenchNavController::selectObject(const QString& objectId, int confType) {
|
||||
if (objectId.isEmpty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
if (objectId.isEmpty()) return;
|
||||
if (selDataReq_) selDataReq_->abort(); // abort-and-replace 三路
|
||||
if (selFileReq_) selFileReq_->abort();
|
||||
if (selDetailReq_) selDetailReq_->abort();
|
||||
currentParentId_ = objectId.toStdString();
|
||||
currentParentConfType_ = confType;
|
||||
const std::string pid = currentProjectId_;
|
||||
dataPageNo_ = 1;
|
||||
filePageNo_ = 1;
|
||||
const auto d = repo_.loadRows(pid, currentParentId_, confType, 3, dataPageNo_);
|
||||
if (!d.ok) {
|
||||
emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(d.error));
|
||||
return;
|
||||
}
|
||||
dataTotal_ = d.value.total;
|
||||
emit datasetsLoaded(objectId, d.value.rows, d.value.total, false);
|
||||
const auto f = repo_.loadRows(pid, currentParentId_, confType, 1, filePageNo_);
|
||||
if (!f.ok) {
|
||||
emit loadFailed(QStringLiteral("files"), QString::fromStdString(f.error));
|
||||
return;
|
||||
}
|
||||
fileTotal_ = f.value.total;
|
||||
emit filesLoaded(objectId, f.value.rows, f.value.total, false);
|
||||
|
||||
const auto detail = repo_.loadObjectDetail(currentParentId_, confType);
|
||||
if (!detail.ok) {
|
||||
emit loadFailed(QStringLiteral("objectDetail"), QString::fromStdString(detail.error));
|
||||
return;
|
||||
}
|
||||
emit objectDetailLoaded(objectId, detail.value);
|
||||
NavRequest* dReq = repo_.loadRowsAsync(pid, currentParentId_, confType, 3, dataPageNo_);
|
||||
selDataReq_ = dReq;
|
||||
NavRequest* fReq = repo_.loadRowsAsync(pid, currentParentId_, confType, 1, filePageNo_);
|
||||
selFileReq_ = fReq;
|
||||
NavRequest* detReq = repo_.loadObjectDetailAsync(currentParentId_, confType);
|
||||
selDetailReq_ = detReq;
|
||||
emitBusyIfChanged();
|
||||
|
||||
QObject::connect(dReq, &NavRequest::done, this, [this, dReq, objectId](const QVariant& v) {
|
||||
if (dReq != selDataReq_) return;
|
||||
selDataReq_.clear();
|
||||
const auto page = qvariant_cast<DsPage>(v);
|
||||
dataTotal_ = page.total;
|
||||
emit datasetsLoaded(objectId, page.rows, page.total, false);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
QObject::connect(dReq, &NavRequest::failed, this, [this, dReq](const QString& msg) {
|
||||
if (dReq != selDataReq_) return;
|
||||
selDataReq_.clear();
|
||||
emit loadFailed(QStringLiteral("datasets"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
QObject::connect(fReq, &NavRequest::done, this, [this, fReq, objectId](const QVariant& v) {
|
||||
if (fReq != selFileReq_) return;
|
||||
selFileReq_.clear();
|
||||
const auto page = qvariant_cast<DsPage>(v);
|
||||
fileTotal_ = page.total;
|
||||
emit filesLoaded(objectId, page.rows, page.total, false);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
QObject::connect(fReq, &NavRequest::failed, this, [this, fReq](const QString& msg) {
|
||||
if (fReq != selFileReq_) return;
|
||||
selFileReq_.clear();
|
||||
emit loadFailed(QStringLiteral("files"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
QObject::connect(detReq, &NavRequest::done, this, [this, detReq, objectId](const QVariant& v) {
|
||||
if (detReq != selDetailReq_) return;
|
||||
selDetailReq_.clear();
|
||||
emit objectDetailLoaded(objectId, qvariant_cast<DynamicForm>(v));
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
QObject::connect(detReq, &NavRequest::failed, this, [this, detReq](const QString& msg) {
|
||||
if (detReq != selDetailReq_) return;
|
||||
selDetailReq_.clear();
|
||||
emit loadFailed(QStringLiteral("objectDetail"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
}
|
||||
|
||||
// ── loadMoreData / loadMoreFiles:单请求,append=true ──
|
||||
void WorkbenchNavController::loadMoreData() {
|
||||
if (currentParentId_.empty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
const auto d = repo_.loadRows(currentProjectId_, currentParentId_, currentParentConfType_, 3, ++dataPageNo_);
|
||||
if (!d.ok) {
|
||||
emit loadFailed(QStringLiteral("datasets"), QString::fromStdString(d.error));
|
||||
return;
|
||||
}
|
||||
dataTotal_ = d.value.total;
|
||||
emit datasetsLoaded(QString::fromStdString(currentParentId_), d.value.rows, d.value.total, true);
|
||||
if (currentParentId_.empty()) return;
|
||||
if (moreDataReq_) moreDataReq_->abort();
|
||||
NavRequest* req =
|
||||
repo_.loadRowsAsync(currentProjectId_, currentParentId_, currentParentConfType_, 3, ++dataPageNo_);
|
||||
moreDataReq_ = req;
|
||||
const QString parent = QString::fromStdString(currentParentId_);
|
||||
emitBusyIfChanged();
|
||||
QObject::connect(req, &NavRequest::done, this, [this, req, parent](const QVariant& v) {
|
||||
if (req != moreDataReq_) return;
|
||||
moreDataReq_.clear();
|
||||
const auto page = qvariant_cast<DsPage>(v);
|
||||
dataTotal_ = page.total;
|
||||
emit datasetsLoaded(parent, page.rows, page.total, true);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
|
||||
if (req != moreDataReq_) return;
|
||||
moreDataReq_.clear();
|
||||
emit loadFailed(QStringLiteral("datasets"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void WorkbenchNavController::loadMoreFiles() {
|
||||
if (currentParentId_.empty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
const auto f = repo_.loadRows(currentProjectId_, currentParentId_, currentParentConfType_, 1, ++filePageNo_);
|
||||
if (!f.ok) {
|
||||
emit loadFailed(QStringLiteral("files"), QString::fromStdString(f.error));
|
||||
return;
|
||||
}
|
||||
fileTotal_ = f.value.total;
|
||||
emit filesLoaded(QString::fromStdString(currentParentId_), f.value.rows, f.value.total, true);
|
||||
if (currentParentId_.empty()) return;
|
||||
if (moreFilesReq_) moreFilesReq_->abort();
|
||||
NavRequest* req =
|
||||
repo_.loadRowsAsync(currentProjectId_, currentParentId_, currentParentConfType_, 1, ++filePageNo_);
|
||||
moreFilesReq_ = req;
|
||||
const QString parent = QString::fromStdString(currentParentId_);
|
||||
emitBusyIfChanged();
|
||||
QObject::connect(req, &NavRequest::done, this, [this, req, parent](const QVariant& v) {
|
||||
if (req != moreFilesReq_) return;
|
||||
moreFilesReq_.clear();
|
||||
const auto page = qvariant_cast<DsPage>(v);
|
||||
fileTotal_ = page.total;
|
||||
emit filesLoaded(parent, page.rows, page.total, true);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
|
||||
if (req != moreFilesReq_) return;
|
||||
moreFilesReq_.clear();
|
||||
emit loadFailed(QStringLiteral("files"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
}
|
||||
|
||||
// ── selectDataset:单请求 ──
|
||||
void WorkbenchNavController::selectDataset(const QString& dsObjectId) {
|
||||
if (dsObjectId.isEmpty()) return;
|
||||
if (datasetReq_) datasetReq_->abort();
|
||||
NavRequest* req = repo_.loadDatasetFormAsync(dsObjectId.toStdString());
|
||||
datasetReq_ = req;
|
||||
emitBusyIfChanged();
|
||||
QObject::connect(req, &NavRequest::done, this, [this, req](const QVariant& v) {
|
||||
if (req != datasetReq_) return;
|
||||
datasetReq_.clear();
|
||||
emit datasetDetailLoaded(qvariant_cast<DynamicForm>(v));
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
QObject::connect(req, &NavRequest::failed, this, [this, req](const QString& msg) {
|
||||
if (req != datasetReq_) return;
|
||||
datasetReq_.clear();
|
||||
emit loadFailed(QStringLiteral("datasetDetail"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
}
|
||||
|
||||
// ── setCheckedTms:未命中缓存项并发拉取,全到齐后组装;新勾选 abort 旧批(以最后一次为准)──
|
||||
void WorkbenchNavController::setCheckedTms(const QStringList& tmObjectIds) {
|
||||
if (busy_) { // 触发源是延迟合并发射,可能落在别的同步操作的嵌套事件循环里:
|
||||
pendingCheckedTms_ = tmObjectIds; // 不丢弃,记下最新一次请求,待空闲重放
|
||||
checkedTmsPending_ = true;
|
||||
for (const auto& h : checkedInflight_) // abort-and-replace 旧批
|
||||
if (h) h->abort();
|
||||
checkedInflight_.clear();
|
||||
|
||||
QStringList missing;
|
||||
for (const QString& tmQ : tmObjectIds) {
|
||||
const std::string tm = tmQ.toStdString();
|
||||
if (tmExceptionCache_.find(tm) == tmExceptionCache_.end()) missing.push_back(tmQ);
|
||||
}
|
||||
if (missing.isEmpty()) { // 全命中缓存:同步组装,busyChanged 不抖动
|
||||
assembleAndEmitExceptionTree(tmObjectIds);
|
||||
emitBusyIfChanged();
|
||||
return;
|
||||
}
|
||||
BusyGuard guard(this, &busy_);
|
||||
|
||||
// 并发拉取未命中项;每个 done 写缓存、清自身槽,全到齐后组装。计数用 shared 计数器。
|
||||
auto remaining = std::make_shared<int>(missing.size());
|
||||
auto failedFlag = std::make_shared<bool>(false);
|
||||
for (const QString& tmQ : missing) {
|
||||
const std::string tm = tmQ.toStdString();
|
||||
NavRequest* req = repo_.loadExceptionsByTmAsync(tm);
|
||||
checkedInflight_.push_back(req);
|
||||
QObject::connect(req, &NavRequest::done, this,
|
||||
[this, req, tm, remaining, failedFlag, tmObjectIds](const QVariant& v) {
|
||||
// 身份比对:req 仍在当前批中才接受(旧批已 abort + 从 vector 移除)。
|
||||
bool inCurrent = false;
|
||||
for (const auto& h : checkedInflight_)
|
||||
if (h == req) inCurrent = true;
|
||||
if (!inCurrent) return;
|
||||
tmExceptionCache_[tm] = qvariant_cast<std::vector<ExceptionRow>>(v);
|
||||
if (--(*remaining) == 0 && !*failedFlag) {
|
||||
checkedInflight_.clear();
|
||||
assembleAndEmitExceptionTree(tmObjectIds);
|
||||
emitBusyIfChanged();
|
||||
}
|
||||
});
|
||||
QObject::connect(req, &NavRequest::failed, this,
|
||||
[this, req, remaining, failedFlag](const QString& msg) {
|
||||
bool inCurrent = false;
|
||||
for (const auto& h : checkedInflight_)
|
||||
if (h == req) inCurrent = true;
|
||||
if (!inCurrent) return;
|
||||
if (*failedFlag) return; // 仅首个失败发一次
|
||||
*failedFlag = true;
|
||||
for (const auto& h : checkedInflight_) // abort 其余在飞
|
||||
if (h && h != req) h->abort();
|
||||
checkedInflight_.clear();
|
||||
emit loadFailed(QStringLiteral("exceptions"), msg);
|
||||
emitBusyIfChanged();
|
||||
});
|
||||
}
|
||||
emitBusyIfChanged();
|
||||
}
|
||||
|
||||
void WorkbenchNavController::assembleAndEmitExceptionTree(const QStringList& tmObjectIds) {
|
||||
auto nameOf = [this](const std::string& id) -> std::string {
|
||||
for (const auto& n : lastStructNodes_)
|
||||
if (n.id == id) return n.name;
|
||||
|
|
@ -197,14 +392,7 @@ void WorkbenchNavController::setCheckedTms(const QStringList& tmObjectIds) {
|
|||
for (const QString& tmQ : tmObjectIds) {
|
||||
const std::string tm = tmQ.toStdString();
|
||||
auto it = tmExceptionCache_.find(tm);
|
||||
if (it == tmExceptionCache_.end()) {
|
||||
const auto ex = repo_.loadExceptionsByTm(tm);
|
||||
if (!ex.ok) {
|
||||
emit loadFailed(QStringLiteral("exceptions"), QString::fromStdString(ex.error));
|
||||
return;
|
||||
}
|
||||
it = tmExceptionCache_.emplace(tm, ex.value).first;
|
||||
}
|
||||
if (it == tmExceptionCache_.end()) continue; // 防御:理论上此时全命中
|
||||
auto grouped = data::dto::groupExceptionsByConsortium(it->second);
|
||||
data::ObjectExceptionGroup g;
|
||||
g.objectId = tm;
|
||||
|
|
@ -217,21 +405,4 @@ void WorkbenchNavController::setCheckedTms(const QStringList& tmObjectIds) {
|
|||
emit exceptionTreeLoaded(groups, total);
|
||||
}
|
||||
|
||||
void WorkbenchNavController::drainPendingCheckedTms() {
|
||||
if (busy_ || !checkedTmsPending_) return;
|
||||
checkedTmsPending_ = false; // 先清标志再重放,避免重入自旋
|
||||
setCheckedTms(pendingCheckedTms_); // 此时 busy_=false,会正常执行
|
||||
}
|
||||
|
||||
void WorkbenchNavController::selectDataset(const QString& dsObjectId) {
|
||||
if (dsObjectId.isEmpty() || busy_) return;
|
||||
BusyGuard guard(this, &busy_);
|
||||
const auto form = repo_.loadDatasetForm(dsObjectId.toStdString());
|
||||
if (!form.ok) {
|
||||
emit loadFailed(QStringLiteral("datasetDetail"), QString::fromStdString(form.error));
|
||||
return;
|
||||
}
|
||||
emit datasetDetailLoaded(form.value);
|
||||
}
|
||||
|
||||
} // namespace geopro::controller
|
||||
|
|
|
|||
|
|
@ -1,30 +1,39 @@
|
|||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "repo/IProjectRepository.hpp"
|
||||
#include "repo/RepoTypes.hpp"
|
||||
|
||||
namespace geopro::data {
|
||||
class IAsyncProjectRepository;
|
||||
class NavRequest;
|
||||
} // namespace geopro::data
|
||||
|
||||
namespace geopro::controller {
|
||||
|
||||
// 导航状态机:编排 IProjectRepository,持有当前 空间/项目 状态,经信号驱动 UI。不持有 widget。
|
||||
// 导航状态机:编排 IAsyncProjectRepository(异步句柄),持有当前 空间/项目 状态,经信号驱动 UI。
|
||||
// 不持有 widget。abort-and-replace + 句柄身份比对保证迟到信号被丢弃(spec §5.0)。
|
||||
// busyChanged 语义:「是否存在任一在飞句柄」(去抖:值变才发)。
|
||||
class WorkbenchNavController : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WorkbenchNavController(data::IProjectRepository& repo, QObject* parent = nullptr);
|
||||
explicit WorkbenchNavController(data::IAsyncProjectRepository& repo, QObject* parent = nullptr);
|
||||
~WorkbenchNavController() override; // 退出契约:abort 所有在飞句柄
|
||||
|
||||
void start(); // 启动:拉空间 → 项目 → 结构
|
||||
void start(); // 启动:拉空间 → 项目 → 结构(依赖链)
|
||||
|
||||
QString currentCrsCode() const { return QString::fromStdString(currentCrsCode_); }
|
||||
|
||||
public slots:
|
||||
void switchWorkspace(const QString& tenantId);
|
||||
void switchProject(const QString& projectId);
|
||||
void selectObject(const QString& objectId, int confType); // 单击对象→DS列表+对象详情
|
||||
void setCheckedTms(const QStringList& tmObjectIds); // 勾选叶子集→异常树
|
||||
void selectObject(const QString& objectId, int confType); // 单击对象→DS列表+对象详情(并发)
|
||||
void setCheckedTms(const QStringList& tmObjectIds); // 勾选叶子集→异常树(并发,带缓存)
|
||||
void selectDataset(const QString& dsObjectId); // 单击DS→数据集动态表单
|
||||
void loadMoreData();
|
||||
void loadMoreFiles();
|
||||
|
|
@ -45,19 +54,33 @@ signals:
|
|||
void loadFailed(const QString& stage, const QString& message);
|
||||
|
||||
private:
|
||||
friend struct BusyGuard; // 允许在 guard 析构时排空挂起的勾选请求
|
||||
void loadProjectsAndStructure(); // start + switchWorkspace 共用
|
||||
void drainPendingCheckedTms(); // 空闲后重放最近一次被挂起的勾选集
|
||||
// start / switchWorkspace 依赖链:拉项目 → 拉结构(续延,复用)。
|
||||
void runProjectsAndStructure();
|
||||
void abortAll(); // 退出/重置时 abort 所有在飞句柄
|
||||
void resetSelectionState(); // 切项目/工作空间重置选中态(spec §6)
|
||||
void emitBusyIfChanged(); // 据「是否存在任一在飞句柄」去抖发 busyChanged
|
||||
bool anyInflight() const; // OR 所有在飞 QPointer / 集合
|
||||
void assembleAndEmitExceptionTree(const QStringList& tmObjectIds); // 缓存命中后组装异常树
|
||||
|
||||
data::IAsyncProjectRepository& repo_;
|
||||
bool lastBusy_ = false;
|
||||
|
||||
// 在飞句柄(QPointer 防悬垂;身份比对用):
|
||||
QPointer<data::NavRequest> startStepReq_; // start / switchWorkspace 依赖链当前在飞级
|
||||
QPointer<data::NavRequest> structReq_; // switchProject
|
||||
QPointer<data::NavRequest> selDataReq_; // selectObject:data 行
|
||||
QPointer<data::NavRequest> selFileReq_; // selectObject:file 行
|
||||
QPointer<data::NavRequest> selDetailReq_; // selectObject:对象详情
|
||||
QPointer<data::NavRequest> moreDataReq_;
|
||||
QPointer<data::NavRequest> moreFilesReq_;
|
||||
QPointer<data::NavRequest> datasetReq_;
|
||||
std::vector<QPointer<data::NavRequest>> checkedInflight_; // setCheckedTms:未命中缓存的并发批
|
||||
|
||||
data::IProjectRepository& repo_;
|
||||
bool busy_ = false;
|
||||
bool checkedTmsPending_ = false;
|
||||
QStringList pendingCheckedTms_;
|
||||
std::vector<data::ProjectSummary> lastProjects_;
|
||||
std::string currentWorkspaceId_, currentProjectId_, currentProjectName_, currentCrsCode_;
|
||||
std::string currentParentId_;
|
||||
int currentParentConfType_ = 0;
|
||||
std::vector<data::StructNode> lastStructNodes_; // tmId→name 解析
|
||||
std::vector<data::StructNode> lastStructNodes_; // tmId→name 解析
|
||||
std::map<std::string, std::vector<data::ExceptionRow>> tmExceptionCache_;
|
||||
int dataPageNo_ = 0;
|
||||
int filePageNo_ = 0;
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ target_sources(geopro_tests PRIVATE
|
|||
# controller 层:DatasetDetailController 编排集成测试(QSignalSpy 验证 chartReady/loadFailed)。
|
||||
find_package(Qt6 COMPONENTS Test REQUIRED)
|
||||
target_sources(geopro_tests PRIVATE controller/test_dataset_detail_controller.cpp)
|
||||
target_sources(geopro_tests PRIVATE controller/test_workbench_nav_controller.cpp)
|
||||
target_link_libraries(geopro_tests PRIVATE geopro_controller Qt6::Test)
|
||||
|
||||
add_subdirectory(spike) # spike S3: banded contour 渲染验证
|
||||
|
|
|
|||
|
|
@ -0,0 +1,201 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <QSignalSpy>
|
||||
#include <QVariant>
|
||||
#include <vector>
|
||||
|
||||
#include "WorkbenchNavController.hpp"
|
||||
#include "api/NavLoads.hpp"
|
||||
#include "api/NavRequest.hpp"
|
||||
#include "repo/IAsyncProjectRepository.hpp"
|
||||
|
||||
using namespace geopro;
|
||||
|
||||
namespace {
|
||||
// 桩句柄:不声明 Q_OBJECT —— 发射继承自 data::NavRequest 的 done/failed、override abort 记录。
|
||||
struct StubNavRequest : data::NavRequest {
|
||||
bool aborted = false;
|
||||
void abort() override { aborted = true; }
|
||||
void fireDone(const QVariant& v) { emit done(v); }
|
||||
void fireFailed() { emit failed(QStringLiteral("x")); }
|
||||
};
|
||||
|
||||
struct StubAsyncRepo : data::IAsyncProjectRepository {
|
||||
StubNavRequest* lastWorkspaces = nullptr;
|
||||
StubNavRequest* lastSwitchWs = nullptr;
|
||||
StubNavRequest* lastProjects = nullptr;
|
||||
StubNavRequest* lastStructure = nullptr;
|
||||
StubNavRequest* lastData = nullptr;
|
||||
StubNavRequest* lastFile = nullptr;
|
||||
StubNavRequest* lastDetail = nullptr;
|
||||
StubNavRequest* lastDataset = nullptr;
|
||||
std::vector<StubNavRequest*> exceptions; // setCheckedTms 并发批
|
||||
|
||||
data::NavRequest* listWorkspacesAsync() override { return lastWorkspaces = new StubNavRequest; }
|
||||
data::NavRequest* switchWorkspaceAsync(const std::string&) override {
|
||||
return lastSwitchWs = new StubNavRequest;
|
||||
}
|
||||
data::NavRequest* pageProjectsAsync(const std::string&, const std::string&, int, int) override {
|
||||
return lastProjects = new StubNavRequest;
|
||||
}
|
||||
data::NavRequest* listProjectTypesAsync() override { return new StubNavRequest; }
|
||||
data::NavRequest* loadStructureAsync(const std::string&) override {
|
||||
return lastStructure = new StubNavRequest;
|
||||
}
|
||||
data::NavRequest* loadRowsAsync(const std::string&, const std::string&, int, int classifyType,
|
||||
int) override {
|
||||
auto* r = new StubNavRequest;
|
||||
if (classifyType == 1)
|
||||
lastFile = r;
|
||||
else
|
||||
lastData = r;
|
||||
return r;
|
||||
}
|
||||
data::NavRequest* loadObjectDetailAsync(const std::string&, int) override {
|
||||
return lastDetail = new StubNavRequest;
|
||||
}
|
||||
data::NavRequest* loadDatasetFormAsync(const std::string&) override {
|
||||
return lastDataset = new StubNavRequest;
|
||||
}
|
||||
data::NavRequest* loadExceptionsByTmAsync(const std::string&) override {
|
||||
auto* r = new StubNavRequest;
|
||||
exceptions.push_back(r);
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
QVariant wsVar() {
|
||||
return QVariant::fromValue(std::vector<data::Workspace>{{"w1", "WS", 2, true}});
|
||||
}
|
||||
QVariant pageVar() {
|
||||
data::ProjectSummary p;
|
||||
p.id = "p1";
|
||||
p.name = "P1";
|
||||
return QVariant::fromValue(data::ProjectListPage{{p}, 1});
|
||||
}
|
||||
QVariant emptyPageVar() { return QVariant::fromValue(data::ProjectListPage{{}, 0}); }
|
||||
QVariant nodesVar() { return QVariant::fromValue(std::vector<data::StructNode>{}); }
|
||||
QVariant dsPageVar() { return QVariant::fromValue(data::DsPage{{}, 0}); }
|
||||
QVariant formVar() { return QVariant::fromValue(data::DynamicForm{}); }
|
||||
QVariant exVar() { return QVariant::fromValue(std::vector<data::ExceptionRow>{}); }
|
||||
} // namespace
|
||||
|
||||
// start() 依赖链:workspaces → projects → structure,逐级 emit 既有信号。
|
||||
TEST(WorkbenchNavController, StartChainEmitsWorkspacesThenProjectsThenStructure) {
|
||||
StubAsyncRepo repo;
|
||||
controller::WorkbenchNavController c(repo);
|
||||
QSignalSpy wsSpy(&c, &controller::WorkbenchNavController::workspacesLoaded);
|
||||
QSignalSpy psSpy(&c, &controller::WorkbenchNavController::projectsLoaded);
|
||||
QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded);
|
||||
c.start();
|
||||
repo.lastWorkspaces->fireDone(wsVar());
|
||||
EXPECT_EQ(wsSpy.count(), 1);
|
||||
repo.lastProjects->fireDone(pageVar());
|
||||
EXPECT_EQ(psSpy.count(), 1);
|
||||
repo.lastStructure->fireDone(nodesVar());
|
||||
EXPECT_EQ(stSpy.count(), 1);
|
||||
}
|
||||
|
||||
// busyChanged 反映在飞:发起→true,最后完成→false。
|
||||
TEST(WorkbenchNavController, BusyChangedReflectsInflight) {
|
||||
StubAsyncRepo repo;
|
||||
controller::WorkbenchNavController c(repo);
|
||||
QSignalSpy busySpy(&c, &controller::WorkbenchNavController::busyChanged);
|
||||
c.start();
|
||||
ASSERT_GE(busySpy.count(), 1);
|
||||
EXPECT_TRUE(busySpy.takeFirst().at(0).toBool()); // 首次 true
|
||||
repo.lastWorkspaces->fireDone(wsVar());
|
||||
repo.lastProjects->fireDone(pageVar());
|
||||
repo.lastStructure->fireDone(nodesVar());
|
||||
EXPECT_FALSE(busySpy.last().at(0).toBool()); // 末尾 false
|
||||
}
|
||||
|
||||
// 空项目链:projects 空 → structure 发空树 → busy 复位(不发结构请求)。
|
||||
TEST(WorkbenchNavController, StartWithNoProjectsEmitsEmptyStructureAndClearsBusy) {
|
||||
StubAsyncRepo repo;
|
||||
controller::WorkbenchNavController c(repo);
|
||||
QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded);
|
||||
QSignalSpy busySpy(&c, &controller::WorkbenchNavController::busyChanged);
|
||||
c.start();
|
||||
repo.lastWorkspaces->fireDone(wsVar());
|
||||
repo.lastProjects->fireDone(emptyPageVar());
|
||||
EXPECT_EQ(stSpy.count(), 1);
|
||||
EXPECT_FALSE(busySpy.last().at(0).toBool());
|
||||
}
|
||||
|
||||
// setCheckedTms:新勾选 abort 旧异常批(以最后一次为准)。
|
||||
TEST(WorkbenchNavController, SetCheckedTmsAbortsPreviousBatch) {
|
||||
StubAsyncRepo repo;
|
||||
controller::WorkbenchNavController c(repo);
|
||||
c.setCheckedTms({"tmA"});
|
||||
StubNavRequest* a = repo.exceptions.back();
|
||||
c.setCheckedTms({"tmB"}); // 覆盖
|
||||
EXPECT_TRUE(a->aborted);
|
||||
}
|
||||
|
||||
// setCheckedTms:全命中缓存 → 不发新请求、直接组装 emit。
|
||||
TEST(WorkbenchNavController, SetCheckedTmsUsesCacheWithoutRequest) {
|
||||
StubAsyncRepo repo;
|
||||
controller::WorkbenchNavController c(repo);
|
||||
QSignalSpy exSpy(&c, &controller::WorkbenchNavController::exceptionTreeLoaded);
|
||||
c.setCheckedTms({"tmA"}); // 首次未命中 → 发请求
|
||||
ASSERT_EQ(repo.exceptions.size(), 1u);
|
||||
repo.exceptions.back()->fireDone(exVar()); // 写缓存 + emit
|
||||
EXPECT_EQ(exSpy.count(), 1);
|
||||
c.setCheckedTms({"tmA"}); // 第二次命中缓存 → 不发新请求
|
||||
EXPECT_EQ(repo.exceptions.size(), 1u);
|
||||
EXPECT_EQ(exSpy.count(), 2); // 仍 emit
|
||||
}
|
||||
|
||||
// 回灌防护:abort 后旧句柄迟到 done 被身份比对丢弃。
|
||||
TEST(WorkbenchNavController, DropsLateStructureAfterProjectSwitch) {
|
||||
StubAsyncRepo repo;
|
||||
controller::WorkbenchNavController c(repo);
|
||||
QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded);
|
||||
c.switchProject("pA");
|
||||
StubNavRequest* a = repo.lastStructure;
|
||||
c.switchProject("pB");
|
||||
StubNavRequest* b = repo.lastStructure;
|
||||
EXPECT_TRUE(a->aborted); // 旧句柄被 abort
|
||||
a->fireDone(nodesVar()); // 旧 → 丢弃
|
||||
EXPECT_EQ(stSpy.count(), 0);
|
||||
b->fireDone(nodesVar()); // 新 → 正常
|
||||
EXPECT_EQ(stSpy.count(), 1);
|
||||
}
|
||||
|
||||
// selectObject 三并发:data/file/detail 各自完成 → 各发对应信号。
|
||||
TEST(WorkbenchNavController, SelectObjectConcurrentEmitsAllThree) {
|
||||
StubAsyncRepo repo;
|
||||
controller::WorkbenchNavController c(repo);
|
||||
QSignalSpy dsSpy(&c, &controller::WorkbenchNavController::datasetsLoaded);
|
||||
QSignalSpy flSpy(&c, &controller::WorkbenchNavController::filesLoaded);
|
||||
QSignalSpy dtSpy(&c, &controller::WorkbenchNavController::objectDetailLoaded);
|
||||
c.selectObject("obj1", 1);
|
||||
repo.lastData->fireDone(dsPageVar());
|
||||
repo.lastFile->fireDone(dsPageVar());
|
||||
repo.lastDetail->fireDone(formVar());
|
||||
EXPECT_EQ(dsSpy.count(), 1);
|
||||
EXPECT_EQ(flSpy.count(), 1);
|
||||
EXPECT_EQ(dtSpy.count(), 1);
|
||||
}
|
||||
|
||||
// selectDataset 单请求 → datasetDetailLoaded。
|
||||
TEST(WorkbenchNavController, SelectDatasetEmitsDetail) {
|
||||
StubAsyncRepo repo;
|
||||
controller::WorkbenchNavController c(repo);
|
||||
QSignalSpy spy(&c, &controller::WorkbenchNavController::datasetDetailLoaded);
|
||||
c.selectDataset("ds1");
|
||||
repo.lastDataset->fireDone(formVar());
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
}
|
||||
|
||||
// 失败路径:start 首级失败 → loadFailed + busy 复位。
|
||||
TEST(WorkbenchNavController, StartWorkspacesFailureEmitsLoadFailed) {
|
||||
StubAsyncRepo repo;
|
||||
controller::WorkbenchNavController c(repo);
|
||||
QSignalSpy failSpy(&c, &controller::WorkbenchNavController::loadFailed);
|
||||
QSignalSpy busySpy(&c, &controller::WorkbenchNavController::busyChanged);
|
||||
c.start();
|
||||
repo.lastWorkspaces->fireFailed();
|
||||
EXPECT_EQ(failSpy.count(), 1);
|
||||
EXPECT_FALSE(busySpy.last().at(0).toBool());
|
||||
}
|
||||
Loading…
Reference in New Issue