#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 #include #include namespace { using TestProjectStorage = TestSupport::InMemoryProjectStorage; using TestSupport::require; void testControlEditing() { // 覆盖控件创建、移动、绑定校验、重命名冲突和删除流程 TestProjectStorage storage; ProjectService project_service(storage, defaultProjectLimitSettings()); HmiEditorService service(project_service, HmiDefaultSettings{}); 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 testBatchControlAlignment() { TestProjectStorage storage; ProjectService project_service(storage, defaultProjectLimitSettings()); HmiEditorService service(project_service, HmiDefaultSettings{}); 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 alignment testing must be created"); const std::vector ids{first.id, second.id, third.id}; const std::vector original_bounds{ {100, 80, 80, 30}, {240, 120, 100, 40}, {380, 160, 60, 20}}; for (std::size_t index = 0U; index < ids.size(); ++index) { HmiControl control = *service.findControl(page_id, ids[index]); control.bounds = original_bounds[index]; require(service.updateControl(page_id, ids[index], control).succeeded, "alignment fixtures must be positioned successfully"); } service.clearHistory(); require(service.alignControls(page_id, ids, HmiAlignment::Left).succeeded, "left alignment must succeed"); require(service.findControl(page_id, first.id)->bounds.x == 100 && service.findControl(page_id, second.id)->bounds.x == 100 && service.findControl(page_id, third.id)->bounds.x == 100, "left alignment must use the selected group left edge"); require(service.findControl(page_id, second.id)->bounds.y == 120, "alignment must preserve the non-aligned coordinate"); require(service.undo().succeeded && service.findControl(page_id, second.id)->bounds .x == original_bounds[1].x, "alignment must be restored by one undo step"); require(service.redo().succeeded && service.findControl(page_id, third.id)->bounds.x == 100, "alignment redo must restore the whole batch"); const auto resetBounds = [&service, &page_id, &ids, &original_bounds] { for (std::size_t index = 0U; index < ids.size(); ++index) { HmiControl control = *service.findControl(page_id, ids[index]); control.bounds = original_bounds[index]; require(service.updateControl(page_id, ids[index], control).succeeded, "alignment fixtures must be reset successfully"); } }; const auto assertBounds = [&service, &page_id, &ids, &resetBounds]( HmiAlignment alignment, const std::vector &expected, const char *message) { resetBounds(); service.clearHistory(); require(service.alignControls(page_id, ids, alignment).succeeded, message); for (std::size_t index = 0U; index < ids.size(); ++index) { const HmiControl *control = service.findControl(page_id, ids[index]); require(control != nullptr && control->bounds.x == expected[index].x && control->bounds.y == expected[index].y, "alignment must calculate deterministic target coordinates"); } }; assertBounds( HmiAlignment::HorizontalCenter, {{230, 80, 0, 0}, {220, 120, 0, 0}, {240, 160, 0, 0}}, "horizontal center alignment must succeed"); assertBounds( HmiAlignment::Right, {{360, 80, 0, 0}, {340, 120, 0, 0}, {380, 160, 0, 0}}, "right alignment must succeed"); assertBounds( HmiAlignment::Top, {{100, 80, 0, 0}, {240, 80, 0, 0}, {380, 80, 0, 0}}, "top alignment must succeed"); assertBounds( HmiAlignment::VerticalCenter, {{100, 115, 0, 0}, {240, 110, 0, 0}, {380, 120, 0, 0}}, "vertical center alignment must succeed"); assertBounds( HmiAlignment::Bottom, {{100, 150, 0, 0}, {240, 140, 0, 0}, {380, 160, 0, 0}}, "bottom alignment must succeed"); service.clearHistory(); const std::vector before_invalid{ service.findControl(page_id, first.id)->bounds, service.findControl(page_id, second.id)->bounds, service.findControl(page_id, third.id)->bounds}; require(!service.alignControls( page_id, {first.id, "missing-control"}, HmiAlignment::Left) .succeeded, "alignment must reject an unknown control before mutation"); require(!service.canUndo() && service.findControl(page_id, first.id)->bounds.x == before_invalid[0].x && service.findControl(page_id, second.id)->bounds.y == before_invalid[1].y, "failed alignment must be atomic and leave history unchanged"); require(!service.alignControls(page_id, {first.id}, HmiAlignment::Left).succeeded, "alignment must require at least two controls"); HmiControl no_op = *service.findControl(page_id, first.id); no_op.bounds.x = 100; HmiControl no_op_second = *service.findControl(page_id, second.id); no_op_second.bounds.x = 100; HmiControl no_op_third = *service.findControl(page_id, third.id); no_op_third.bounds.x = 100; require(service.updateControl(page_id, first.id, no_op).succeeded && service.updateControl(page_id, second.id, no_op_second).succeeded && service.updateControl(page_id, third.id, no_op_third).succeeded, "alignment no-op fixtures must be positioned successfully"); service.clearHistory(); require(service.alignControls(page_id, ids, HmiAlignment::Left).succeeded && !service.canUndo(), "a no-op alignment must not create an undo step"); } 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 && std::get(numeric_value.numeric_value) == -18, "numeric display must read D values through the repository"); HmiControl float_input = numeric_input; float_input.dataType = RegisterDataType::Float32; float_input.binding = RegisterAddress{RegisterArea::D, 20}; require(runtime_service.writeNumericInput(float_input, -2.5).succeeded, "Float32 numeric input must write two consecutive D words"); const HmiRuntimeReadResult float_value = runtime_service.readControl(float_input); require(float_value.succeeded && std::get(float_value.numeric_value) == -2.5f, "Float32 numeric control must decode two consecutive D words"); require(!runtime_service.writeNumericInput( float_input, std::numeric_limits::infinity()).succeeded, "Float32 numeric input must reject infinity"); HmiControl int32_input = numeric_input; int32_input.dataType = RegisterDataType::Int32; int32_input.binding = RegisterAddress{RegisterArea::D, 30}; require(runtime_service.writeNumericInput(int32_input, 305419896).succeeded, "Int32 numeric input must write two consecutive D words"); const WordsReadResult int32_words = repository.readWords(*int32_input.binding, 2); const HmiRuntimeReadResult int32_value = runtime_service.readControl(int32_input); require(int32_words.succeeded && static_cast(int32_words.values[0]) == 0x5678U && static_cast(int32_words.values[1]) == 0x1234U && int32_value.succeeded && std::get(int32_value.numeric_value) == 0x12345678, "Int32 HMI read/write must use low-word-first Xinje ordering"); HmiControl double_input = numeric_input; double_input.dataType = RegisterDataType::Float64; double_input.binding = RegisterAddress{RegisterArea::D, 40}; require(runtime_service.writeNumericInput(double_input, 1.0).succeeded, "Double numeric input must write four consecutive D words"); const WordsReadResult double_words = repository.readWords(*double_input.binding, 4); const HmiRuntimeReadResult double_value = runtime_service.readControl(double_input); require(double_words.succeeded && static_cast(double_words.values[0]) == 0x0000U && static_cast(double_words.values[1]) == 0x0000U && static_cast(double_words.values[2]) == 0x0000U && static_cast(double_words.values[3]) == 0x3ff0U && double_value.succeeded && std::get(double_value.numeric_value) == 1.0, "Double HMI read/write must use four low-address-first words"); require(!runtime_service.writeNumericInput( double_input, std::numeric_limits::quiet_NaN()).succeeded, "Double numeric input must reject NaN"); double_input.binding = RegisterAddress{RegisterArea::D, 3997}; require(runtime_service.writeNumericInput(double_input, 1.0).error == HmiRuntimeError::InvalidBinding, "Double numeric input must reject odd or overflowing start addresses"); } void testStatusTextRuntimeMapping() { VirtualRegisterRepository repository; HmiRuntimeService runtime(repository); HmiControl status; status.id = "machine-status"; status.type = HmiControlType::StatusText; status.text = "Status"; status.binding = RegisterAddress{RegisterArea::M, 5}; status.statusText = HmiStatusBitTextConfig{"Stopped", "Running"}; require(runtime.readStatusText(status).text == "Stopped", "an OFF M bit must resolve to the configured OFF text"); repository.writeBit(*status.binding, true); require(runtime.readStatusText(status).text == "Running", "an ON M bit must resolve to the configured ON text"); require(runtime.writeNumericInput(status, 1).error == HmiRuntimeError::UnsupportedControl, "status text must remain read-only"); status.binding = RegisterAddress{RegisterArea::D, 20}; status.dataType = RegisterDataType::Float32; status.statusText = HmiStatusWordTextConfig{{ {std::nullopt, 30.0, "Low"}, {30.0, 80.0, "Normal"}, {80.0, std::nullopt, "High"}}}; const auto write_float = [&repository, &status](float value) { const auto words = Float32Codec::encode(value); repository.writeWords(*status.binding, {words[0], words[1]}); }; write_float(29.5f); require(runtime.readStatusText(status).text == "Low", "values below the first upper bound must resolve to the first text"); write_float(30.0f); require(runtime.readStatusText(status).text == "Normal", "a shared boundary must belong to the range starting at that boundary"); write_float(80.0f); require(runtime.readStatusText(status).text == "High", "the final boundary must belong to the unlimited upper range"); repository.writeWords( *status.binding, {static_cast(0), static_cast(0x7fc0)}); require(!runtime.readStatusText(status).succeeded, "NaN register bits must make status text unavailable"); } void testHistoryAndAtomicBatchDelete() { TestProjectStorage storage; ProjectService project_service(storage, defaultProjectLimitSettings()); HmiEditorService service(project_service, HmiDefaultSettings{}); 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, defaultProjectLimitSettings()); HmiEditorService service(project_service, HmiDefaultSettings{}); const std::string page_id = service.ensureDefaultPage().id; const HmiEditorResult first = service.addControl( page_id, HmiControlType::NumericDisplay); 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.type = HmiControlType::NumericDisplay; first_copy.text = "复制 Double"; first_copy.binding = RegisterAddress{RegisterArea::D, 100}; first_copy.dataType = RegisterDataType::Float64; 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->binding == first_copy.binding && pasted_control->dataType == RegisterDataType::Float64 && pasted_control->properties == first_copy.properties, "pasted controls must preserve data type, binding 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, defaultProjectLimitSettings()); HmiEditorService editor(project_service, HmiDefaultSettings{}); 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, defaultProjectLimitSettings()); HmiEditorService service(project_service, HmiDefaultSettings{}); 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, defaultProjectLimitSettings()); HmiEditorService service(project_service, HmiDefaultSettings{}); 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, defaultProjectLimitSettings()); HmiEditorService service(project_service, HmiDefaultSettings{}); 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"); } void testConfiguredPageDefaultsAndLimits() { TestProjectStorage storage; ProjectLimitSettings limits; limits.maximumHmiPages = 1U; limits.maximumHmiControlsPerPage = 1U; HmiDefaultSettings defaults; defaults.pageWidth = 1024; defaults.pageHeight = 600; ProjectService project_service(storage, limits); HmiEditorService service(project_service, defaults); const HmiEditorResult page = service.ensureDefaultPage(); require(page.succeeded && service.findPage(page.id)->width == 1024 && service.findPage(page.id)->height == 600, "configured HMI dimensions must initialize the default page"); require(!service.addPage("Second page").succeeded, "the HMI editor must use the configured page limit"); require(service.addControl(page.id, HmiControlType::Label).succeeded && !service.addControl(page.id, HmiControlType::Label).succeeded, "the HMI editor must use the configured per-page control limit"); } } // namespace int main() { try { // 编辑和运行场景分别验证服务层两条独立职责 testControlEditing(); testBatchControlAlignment(); testHistoryAndAtomicBatchDelete(); testBatchPasteControls(); testRuntimeValidationAndPasteBoundaries(); testAppearanceEditing(); testRuntimeUsesRegisterRepository(); testStatusTextRuntimeMapping(); testPageLifecycleAndNavigation(); testPageResizeIsAtomicAndUndoable(); testConfiguredPageDefaultsAndLimits(); } 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; }