#include "domain/project_storage.h" #include "domain/register_repository.h" #include "services/hmi_editor_service.h" #include "services/hmi_runtime_service.h" #include "services/logic_editor_service.h" #include "services/offline_simulation_service.h" #include "services/project_service.h" #include "services/runtime_mode_service.h" #include "ui/hmi_editor_widget.h" #include "ui/logic_editor_widget.h" #include "ui/main_window.h" #include #include #include #include #include #include #include #include namespace { class TestProjectStorage final : public ProjectStorage { public: // 主窗口测试使用无副作用存储实现,隔离文件对 UI 行为的影响 ProjectSaveResult save(const Project &, const std::string &) override { return {true, ProjectStorageError::None, {}}; } ProjectLoadResult load(const std::string &) override { return {false, {}, ProjectStorageError::FileReadFailed, {}}; } }; void require(bool condition, const std::string &message) { if (!condition) { throw std::runtime_error(message); } } template ObjectType *requiredChild(MainWindow &window, const char *name) { // 通过 objectName 取得 Designer 组件,缺失时给出明确测试失败信息 ObjectType *child = window.findChild(QString::fromLatin1(name)); require(child != nullptr, std::string("missing UI object ") + name); return child; } void testModeActionsControlEditingAvailability() { // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择 TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService editor_service(project_service); LogicEditorService logic_editor_service(project_service); VirtualRegisterRepository repository; HmiRuntimeService runtime_service(repository); OfflineSimulationService simulation_service(repository); RuntimeModeService mode_service(project_service, simulation_service); MainWindow window( mode_service, project_service, editor_service, logic_editor_service, runtime_service); window.resize(1000, 640); window.show(); QApplication::processEvents(); QAction *editing_action = requiredChild(window, "editingModeAction"); QAction *offline_action = requiredChild(window, "offlineModeAction"); QAction *online_action = requiredChild(window, "onlineModeAction"); QAction *add_button_action = requiredChild(window, "addButtonAction"); QAction *delete_control_action = requiredChild(window, "deleteControlAction"); QAction *add_normally_open_action = requiredChild( window, "addNormallyOpenAction"); QAction *add_normal_coil_action = requiredChild( window, "addNormalCoilAction"); QAction *parallel_insert_action = requiredChild( window, "parallelInsertAction"); QDockWidget *project_dock = requiredChild(window, "projectDock"); QDockWidget *properties_dock = requiredChild(window, "propertiesDock"); QLabel *selection = requiredChild(window, "selectionValueLabel"); QLineEdit *text_edit = requiredChild(window, "controlTextEdit"); HmiEditorWidget *hmi_editor = requiredChild( window, "hmiEditorWidget"); QLabel *executor_status = requiredChild(window, "executorStatusLabel"); const qreal compact_scale = hmi_editor->transform().m11(); window.resize(1600, 900); QApplication::processEvents(); require(hmi_editor->transform().m11() > compact_scale, "HMI page must refit when the window becomes larger"); require(editing_action->isChecked(), "editing action must be selected initially"); require(project_dock->isEnabled(), "project dock must be enabled while editing"); require(properties_dock->isEnabled(), "properties dock must be enabled while editing"); add_button_action->trigger(); const std::string page_id = editor_service.firstPageId(); require(editor_service.findPage(page_id)->controls.size() == 1, "adding a control must update the HMI page model"); require(selection->text() == QStringLiteral("button-1(未绑定)"), "an unbound added control must be marked in the property panel"); require(text_edit->text() == QStringLiteral("按钮"), "property panel must show the control text"); delete_control_action->trigger(); require(editor_service.findPage(page_id)->controls.empty(), "deleting a selected control must update the HMI page model"); add_normally_open_action->trigger(); parallel_insert_action->trigger(); add_normally_open_action->trigger(); add_normal_coil_action->trigger(); const ControlLogic *logic = logic_editor_service.findLogic( logic_editor_service.firstLogicId()); require(logic != nullptr && logic->rungs.size() == 1, "logic actions must edit the default ladder rung"); require(logic->rungs.front().stages.size() == 1 && logic->rungs.front().stages.front().branches.size() == 2, "parallel insert mode must add a branch to the selected stage"); require(logic->rungs.front().output.has_value(), "coil action must set the fixed ladder output"); offline_action->trigger(); require(mode_service.mode() == ApplicationMode::Editing, "unconfigured ladder nodes must block offline running"); const LadderRung &rung = logic->rungs.front(); for (const LogicNode &node : rung.stages.front().branches) { require(logic_editor_service.updateNodeConfig( logic_editor_service.firstLogicId(), node.id, ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyOpen}) .succeeded, "applying a contact configuration must complete the ladder node"); } require(logic_editor_service.updateNodeConfig( logic_editor_service.firstLogicId(), rung.output->id, CoilNodeConfig{ RegisterAddress{RegisterArea::M, 1}, CoilMode::Normal}) .succeeded, "applying a coil configuration must complete the ladder node"); offline_action->trigger(); require(mode_service.mode() == ApplicationMode::OfflineRunning, "offline action must enter offline running"); require(simulation_service.state() == SimulationState::Running, "offline action must start the actual simulation service"); require(executor_status->text().contains(QStringLiteral("运行")), "executor status label must report the actual running state"); require(!project_dock->isEnabled(), "project dock must be disabled while running"); require(!properties_dock->isEnabled(), "properties dock must be disabled while running"); require(!add_button_action->isEnabled(), "HMI add controls must be disabled while running"); require(!add_normally_open_action->isEnabled(), "logic add nodes must be disabled while running"); online_action->trigger(); require(mode_service.mode() == ApplicationMode::OfflineRunning, "running modes must not switch directly through the UI"); require(offline_action->isChecked(), "failed mode changes must restore the active action"); editing_action->trigger(); require(mode_service.mode() == ApplicationMode::Editing, "editing action must return to editing"); require(simulation_service.state() == SimulationState::Stopped, "editing action must stop the simulation service"); require(executor_status->text() == QStringLiteral("逻辑执行器:停止"), "executor status label must report the actual stopped state"); require(project_dock->isEnabled(), "project dock must be restored after returning to editing"); require(properties_dock->isEnabled(), "properties dock must be restored after returning to editing"); require(add_button_action->isEnabled(), "HMI add controls must be restored after returning to editing"); require(add_normally_open_action->isEnabled(), "logic add nodes must be restored after returning to editing"); online_action->trigger(); require(mode_service.mode() == ApplicationMode::Editing, "online action must require an initial PLC read"); require(editing_action->isChecked(), "rejected online running must restore the editing action"); } } // namespace int main(int argc, char *argv[]) { // 无窗口平台使 Qt Widgets 测试可在自动化环境稳定运行 qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen")); QApplication application(argc, argv); try { testModeActionsControlEditingAvailability(); } catch (const std::exception &error) { std::cerr << "main window tests failed: " << error.what() << '\n'; return 1; } std::cout << "main window tests passed\n"; return 0; }