#include "domain/project_storage.h" #include "domain/register_repository.h" #include "domain/virtual_register_repository.h" #include "services/hmi_editor_service.h" #include "services/editor_history.h" #include "services/hmi_runtime_service.h" #include "services/hmi_navigation_service.h" #include "services/project_service.h" #include "support/test_support.h" #include #include #include #include namespace { using TestProjectStorage = TestSupport::InMemoryProjectStorage; using TestSupport::require; void testControlEditing() { // 覆盖控件创建、移动、绑定校验、重命名冲突和删除流程 TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService service(project_service); const HmiEditorResult page_result = service.ensureDefaultPage(); require(page_result.succeeded, "default HMI page creation must succeed"); const std::string page_id = page_result.id; const HmiEditorResult button = service.addControl(page_id, HmiControlType::Button); const HmiEditorResult indicator = service.addControl(page_id, HmiControlType::Indicator); const HmiEditorResult display = service.addControl( page_id, HmiControlType::NumericDisplay); const HmiEditorResult input = service.addControl(page_id, HmiControlType::NumericInput); require(button.succeeded && indicator.succeeded && display.succeeded && input.succeeded, "basic HMI controls must be added"); const HmiPage *page = service.findPage(page_id); require(page != nullptr && page->controls.size() == 4, "all added controls must be kept in the page model"); require(service.moveControl(page_id, button.id, {120, 80, 120, 40}).succeeded, "a valid control move must succeed"); require(!service.moveControl(page_id, button.id, {790, 460, 120, 40}).succeeded, "a move outside the page must be rejected"); HmiControl updated = *service.findControl(page_id, button.id); updated.binding = RegisterAddress{RegisterArea::D, 0}; require(!service.updateControl(page_id, button.id, updated).succeeded, "buttons must reject D address bindings during editing"); updated.binding = RegisterAddress{RegisterArea::M, 7}; updated.text = "启动"; require(service.updateControl(page_id, button.id, updated).succeeded, "a button M binding and edited text must be accepted"); HmiControl duplicate = *service.findControl(page_id, indicator.id); duplicate.id = button.id; require(service.updateControl(page_id, indicator.id, duplicate).error == HmiEditorError::DuplicateId, "duplicate control ids must be rejected"); require(service.removeControl(page_id, indicator.id).succeeded, "deleting a selected control must succeed"); require(service.findControl(page_id, indicator.id) == nullptr, "deleted controls must not remain in the model"); } void testRuntimeUsesRegisterRepository() { // 运行服务只能经由仓库接口读写 M/D,不依赖具体离线实现 VirtualRegisterRepository repository; HmiRuntimeService runtime_service(repository); HmiControl button; button.id = "start"; button.type = HmiControlType::Button; button.binding = RegisterAddress{RegisterArea::M, 12}; require(button.buttonOperation == HmiButtonOperation::MomentaryOn, "new buttons must default to momentary ON"); require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, "momentary button press must be accepted by the repository"); require(repository.readBit(*button.binding).value, "momentary button press must write ON"); require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded, "momentary button release must be accepted by the repository"); require(!repository.readBit(*button.binding).value, "momentary button release must write OFF"); button.buttonOperation = HmiButtonOperation::SetOn; require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, "set ON button press must succeed"); require(repository.readBit(*button.binding).value, "set ON button press must write ON"); require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded, "set ON button release must be ignored successfully"); require(repository.readBit(*button.binding).value, "set ON button release must keep the bit ON"); button.buttonOperation = HmiButtonOperation::SetOff; require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, "set OFF button press must succeed"); require(!repository.readBit(*button.binding).value, "set OFF button press must write OFF"); button.buttonOperation = HmiButtonOperation::Toggle; require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded, "toggle button press must succeed"); require(repository.readBit(*button.binding).value, "toggle button press must invert the current value"); HmiControl indicator; indicator.id = "running"; indicator.type = HmiControlType::Indicator; indicator.binding = RegisterAddress{RegisterArea::M, 12}; const HmiRuntimeReadResult indicator_value = runtime_service.readControl(indicator); require(indicator_value.succeeded && indicator_value.bit_value, "indicators must read M values through the repository"); HmiControl numeric_input; numeric_input.id = "target"; numeric_input.type = HmiControlType::NumericInput; numeric_input.binding = RegisterAddress{RegisterArea::D, 9}; require(runtime_service.writeNumericInput(numeric_input, -18).succeeded, "numeric input runtime writes must be accepted by the repository"); HmiControl numeric_display; numeric_display.id = "actual"; numeric_display.type = HmiControlType::NumericDisplay; numeric_display.binding = RegisterAddress{RegisterArea::D, 9}; const HmiRuntimeReadResult numeric_value = runtime_service.readControl(numeric_display); require(numeric_value.succeeded && numeric_value.word_value == -18, "numeric display must read D values through the repository"); } void testHistoryAndAtomicBatchDelete() { TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService service(project_service); const std::string page_id = service.ensureDefaultPage().id; const HmiEditorResult first = service.addControl(page_id, HmiControlType::Label); const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label); const HmiEditorResult third = service.addControl(page_id, HmiControlType::Label); require(first.succeeded && second.succeeded && third.succeeded, "controls for history testing must be created"); service.clearHistory(); require(!service.removeControls(page_id, {first.id, "missing-control"}).succeeded, "batch deletion must reject an unknown control before changing the page"); require(service.findPage(page_id)->controls.size() == 3U && !service.canUndo(), "failed batch deletion must be atomic and leave history unchanged"); require(service.removeControls(page_id, {first.id, second.id}).succeeded, "batch deletion of valid controls must succeed"); require(service.findPage(page_id)->controls.size() == 1U && service.canUndo(), "valid batch deletion must remove all selected controls in one history step"); require(service.undo().succeeded && service.findPage(page_id)->controls.size() == 3U, "HMI undo must restore a deleted batch"); require(service.redo().succeeded && service.findPage(page_id)->controls.size() == 1U, "HMI redo must reapply a deleted batch"); service.clearHistory(); const HmiControl *remaining = service.findControl(page_id, third.id); require(remaining != nullptr, "the unselected control must survive a batch deletion"); require(service.moveControl(page_id, third.id, remaining->bounds).succeeded && !service.canUndo(), "a no-op control move must not consume an undo step"); service.clearHistory(); HmiControl candidate = *service.findControl(page_id, third.id); for (int index = 1; index <= 101; ++index) { candidate.bounds.x = index; require(service.updateControl(page_id, third.id, candidate).succeeded, "repeated valid HMI edits must succeed"); } int undo_count = 0; while (service.undo().succeeded) { ++undo_count; } require(undo_count == static_cast(EditorHistory::kMaximumEntries), "HMI history must retain exactly the configured most recent steps"); } void testBatchPasteControls() { TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService service(project_service); const std::string page_id = service.ensureDefaultPage().id; const HmiEditorResult first = service.addControl(page_id, HmiControlType::Label); const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label); require(first.succeeded && second.succeeded, "controls for paste testing must be created"); HmiControl first_copy = *service.findControl(page_id, first.id); HmiControl second_copy = *service.findControl(page_id, second.id); first_copy.bounds = {100, 80, 80, 32}; first_copy.text = "复制标签"; first_copy.properties[HmiAppearanceProperty::kTextColor] = "#E53935"; second_copy.bounds = {220, 80, 80, 32}; require(service.updateControl(page_id, first.id, first_copy).succeeded && service.updateControl(page_id, second.id, second_copy).succeeded, "source controls must be movable before paste"); service.clearHistory(); const HmiEditorResult pasted = service.pasteControls( page_id, {first_copy, second_copy}); require(pasted.succeeded && service.findPage(page_id)->controls.size() == 4U && pasted.id != first.id && pasted.id != second.id, "batch paste must create controls with fresh ids"); const HmiControl *pasted_control = service.findControl(page_id, pasted.id); require(pasted_control != nullptr && pasted_control->bounds.x == first_copy.bounds.x + 20 && pasted_control->bounds.y == first_copy.bounds.y + 20 && pasted_control->text == first_copy.text && pasted_control->properties == first_copy.properties, "pasted controls must preserve content and appearance with an offset"); require(service.undo().succeeded && service.findPage(page_id)->controls.size() == 2U, "batch control paste must be one undoable operation"); } void testRuntimeValidationAndPasteBoundaries() { TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService editor(project_service); const std::string page_id = editor.ensureDefaultPage().id; HmiControl label; label.id = "label-source"; label.type = HmiControlType::Label; label.bounds = {760, 360, 32, 32}; label.text = "边界"; require(editor.pasteControls(page_id, {label}, 20, 20).succeeded, "a paste near the page edge must be clamped into the page"); const HmiPage *page = editor.findPage(page_id); require(page != nullptr && page->controls.size() == 1U, "edge paste must add exactly one control"); const HmiControl &edge_copy = page->controls.front(); require(edge_copy.bounds.x + edge_copy.bounds.width <= page->width && edge_copy.bounds.y + edge_copy.bounds.height <= page->height && edge_copy.bounds.x >= 0 && edge_copy.bounds.y >= 0, "edge paste must keep the copied control inside page bounds"); require(!editor.pasteControls(page_id, {}).succeeded, "an empty HMI clipboard must be rejected"); HmiControl oversized = label; oversized.bounds = {0, 0, page->width + 1, 32}; require(!editor.pasteControls(page_id, {oversized}).succeeded && editor.findPage(page_id)->controls.size() == 1U, "a copied control wider than the target page must fail atomically"); HmiControl unsupported = label; unsupported.type = HmiControlType::Count; require(!editor.pasteControls(page_id, {unsupported}).succeeded && editor.findPage(page_id)->controls.size() == 1U, "an unsupported copied HMI type must fail atomically"); std::vector too_many(513U, label); require(!editor.pasteControls(page_id, too_many).succeeded && editor.findPage(page_id)->controls.size() == 1U, "a paste batch above the page limit must be rejected before mutation"); VirtualRegisterRepository repository; HmiRuntimeService runtime(repository); HmiControl unbound_indicator; unbound_indicator.type = HmiControlType::Indicator; require(runtime.readControl(unbound_indicator).error == HmiRuntimeError::MissingBinding, "an unbound runtime indicator must report MissingBinding"); HmiControl wrong_area_indicator = unbound_indicator; wrong_area_indicator.binding = RegisterAddress{RegisterArea::D, 0}; require(runtime.readControl(wrong_area_indicator).error == HmiRuntimeError::InvalidBinding, "an indicator bound to D must report InvalidBinding"); HmiControl invalid_indicator = unbound_indicator; invalid_indicator.binding = RegisterAddress{RegisterArea::M, 4001}; require(runtime.readControl(invalid_indicator).error == HmiRuntimeError::InvalidBinding, "an indicator with an out-of-range address must report InvalidBinding"); require(runtime.readControl(label).error == HmiRuntimeError::UnsupportedControl, "a static label must not be treated as a register runtime control"); HmiControl wrong_button = unbound_indicator; wrong_button.type = HmiControlType::Button; wrong_button.binding = RegisterAddress{RegisterArea::D, 0}; require(runtime.operateButton(wrong_button, HmiButtonEvent::Pressed).error == HmiRuntimeError::InvalidBinding, "a button bound to D must reject runtime writes"); HmiControl wrong_numeric = unbound_indicator; wrong_numeric.type = HmiControlType::NumericInput; wrong_numeric.binding = RegisterAddress{RegisterArea::M, 0}; require(runtime.writeNumericInput(wrong_numeric, 1).error == HmiRuntimeError::InvalidBinding, "a numeric input bound to M must reject runtime writes"); } void testAppearanceEditing() { TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService service(project_service); const std::string page_id = service.ensureDefaultPage().id; const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label); require(label.succeeded, "a label must be available for appearance editing"); HmiControl appearance = *service.findControl(page_id, label.id); appearance.properties[HmiAppearanceProperty::kTextColor] = "#E53935"; appearance.properties[HmiAppearanceProperty::kFontSize] = "18"; appearance.properties[HmiAppearanceProperty::kFontBold] = "true"; appearance.properties[HmiAppearanceProperty::kFontItalic] = "false"; require(service.updateControl(page_id, label.id, appearance).succeeded, "valid appearance properties must be applied atomically"); const HmiControl *updated = service.findControl(page_id, label.id); require(updated != nullptr && updated->properties.at(HmiAppearanceProperty::kTextColor) == "#E53935" && updated->properties.at(HmiAppearanceProperty::kFontSize) == "18" && updated->properties.at(HmiAppearanceProperty::kFontBold) == "true", "appearance properties must be stored on the HMI control"); HmiControl invalid = *updated; invalid.properties[HmiAppearanceProperty::kTextColor] = "invalid"; require(!service.updateControl(page_id, label.id, invalid).succeeded, "invalid appearance properties must be rejected without a partial update"); require(service.findControl(page_id, label.id)->properties.at( HmiAppearanceProperty::kTextColor) == "#E53935", "failed appearance updates must leave the old color intact"); require(service.undo().succeeded, "appearance updates must participate in HMI undo history"); require(service.findControl(page_id, label.id)->properties.empty(), "undo must remove the applied appearance properties"); require(service.redo().succeeded && service.findControl(page_id, label.id)->properties.at( HmiAppearanceProperty::kFontSize) == "18", "redo must restore the applied appearance properties"); } void testPageLifecycleAndNavigation() { TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService service(project_service); const std::string main_id = service.ensureDefaultPage().id; require(project_service.project().initialHmiPageId == main_id, "the first page must become the initial HMI page"); const HmiEditorResult settings = service.addPage("Settings"); const HmiEditorResult maintenance = service.addPage("Maintenance"); require(settings.succeeded && maintenance.succeeded, "multiple HMI pages must be creatable"); require(service.renamePage(settings.id, "Parameters").succeeded, "an HMI page must be renamable by stable id"); require(service.renamePage(maintenance.id, "Parameters").error == HmiEditorError::DuplicateName, "HMI page names must remain unique"); require(service.movePage(maintenance.id, -1).succeeded && project_service.project().hmiPages.at(1).id == maintenance.id, "HMI page order must be editable independently from page ids"); const HmiEditorResult label = service.addControl(main_id, HmiControlType::Label); require(label.succeeded && service.findControl(main_id, label.id)->isConfigured(), "the editor service must expose a configured static Label control"); const HmiEditorResult alarm_list = service.addControl( main_id, HmiControlType::AlarmList); require(alarm_list.succeeded && service.findControl(main_id, alarm_list.id)->isConfigured() && !service.findControl(main_id, alarm_list.id)->binding.has_value(), "AlarmList must be configured without a register binding"); HmiControl bound_alarm_list = *service.findControl(main_id, alarm_list.id); bound_alarm_list.binding = RegisterAddress{RegisterArea::M, 20}; require(!service.updateControl( main_id, alarm_list.id, bound_alarm_list).succeeded, "AlarmList must reject direct register bindings"); const HmiEditorResult jump = service.addControl(main_id, HmiControlType::PageJump); require(jump.succeeded, "the editor service must expose PageJump creation"); HmiControl jump_control = *service.findControl(main_id, jump.id); jump_control.pageJump = HmiPageJumpConfig{maintenance.id}; require(service.updateControl(main_id, jump.id, jump_control).succeeded, "PageJump target updates must resolve stable page ids"); require(service.removePage(maintenance.id).error == HmiEditorError::PageReferenced, "a page referenced by PageJump must not be deletable"); require(service.setInitialPage(settings.id).succeeded, "the user must be able to select a different initial page"); require(service.removePage(settings.id).error == HmiEditorError::InitialPageCannotBeRemoved, "the initial page must not be deleted implicitly"); HmiNavigationService navigation(project_service); const HmiNavigationResult start = navigation.start(); require(start.succeeded && start.pageId == settings.id, "runtime navigation must start from the persisted initial page"); require(navigation.navigateTo(maintenance.id).succeeded && navigation.currentPageId() == maintenance.id, "runtime navigation must switch to an existing page"); require(navigation.navigateTo("missing").error == HmiNavigationError::PageNotFound, "runtime navigation must reject an unknown page id"); navigation.stop(); require(navigation.currentPageId().empty(), "stopping runtime navigation must clear session state"); } void testPageResizeIsAtomicAndUndoable() { TestProjectStorage storage; ProjectService project_service(storage); HmiEditorService service(project_service); const std::string page_id = service.ensureDefaultPage().id; const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label); require(label.succeeded, "page resize fixture must add a control"); HmiControl control = *service.findControl(page_id, label.id); control.bounds = {700, 340, 80, 32}; require(service.updateControl(page_id, label.id, control).succeeded, "page resize fixture must place the control near the page edge"); require(service.resizePage(page_id, 1024, 600).succeeded, "an HMI page must be resizable to a larger valid rectangle"); require(service.findPage(page_id)->width == 1024 && service.findPage(page_id)->height == 600, "page resize must update both dimensions"); require(service.resizePage(page_id, 700, 300).error == HmiEditorError::InvalidPage, "page resize must reject dimensions that clip an existing control"); require(service.findPage(page_id)->width == 1024 && service.findPage(page_id)->height == 600, "a rejected page resize must leave the original dimensions intact"); require(service.undo().succeeded && service.findPage(page_id)->width == 800 && service.findPage(page_id)->height == 400, "page resize must participate in HMI undo history"); require(service.redo().succeeded && service.findPage(page_id)->width == 1024 && service.findPage(page_id)->height == 600, "page resize redo must restore the new dimensions"); require(service.resizePage(page_id, 319, 600).error == HmiEditorError::InvalidPage, "page width below the business minimum must be rejected"); } } // namespace int main() { try { // 编辑和运行场景分别验证服务层两条独立职责 testControlEditing(); testHistoryAndAtomicBatchDelete(); testBatchPasteControls(); testRuntimeValidationAndPasteBoundaries(); testAppearanceEditing(); testRuntimeUsesRegisterRepository(); testPageLifecycleAndNavigation(); testPageResizeIsAtomicAndUndoable(); } catch (const std::exception &error) { std::cerr << "HMI editor service tests failed: " << error.what() << '\n'; return 1; } std::cout << "HMI editor service tests passed\n"; return 0; }