|
- #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 <exception>
- #include <iostream>
- #include <limits>
- #include <stdexcept>
- #include <string>
- #include <variant>
-
- 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.findControl(page_id, button.id)->binding
- == RegisterAddress{RegisterArea::M, 0}
- && service.findControl(page_id, indicator.id)->binding
- == RegisterAddress{RegisterArea::M, 1}
- && service.findControl(page_id, display.id)->binding
- == RegisterAddress{RegisterArea::D, 0}
- && service.findControl(page_id, input.id)->binding
- == RegisterAddress{RegisterArea::D, 1},
- "new basic HMI controls must receive sequential M/D addresses");
- 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<std::string> ids{first.id, second.id, third.id};
- const std::vector<HmiRect> 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<HmiRect> &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<HmiRect> 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");
-
- button.buttonOperation = HmiButtonOperation::MomentaryOn;
- button.buttonEnableCondition = HmiButtonBitEnableCondition{
- RegisterAddress{RegisterArea::M, 13}, true};
- repository.writeBit({RegisterArea::M, 13}, false);
- HmiButtonEnabledResult condition = runtime_service.evaluateButtonEnabled(button);
- require(condition.succeeded && !condition.enabled
- && condition.error == HmiRuntimeError::ConditionNotMet,
- "an M button condition must disable the button when it is false");
- require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).error
- == HmiRuntimeError::ConditionNotMet,
- "a false M button condition must reject the write at the service boundary");
- repository.writeBit({RegisterArea::M, 13}, true);
- condition = runtime_service.evaluateButtonEnabled(button);
- require(condition.succeeded && condition.enabled,
- "an M button condition must enable the button when it is true");
- require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded
- && repository.readBit(*button.binding).value,
- "a satisfied M button condition must allow the configured operation");
- repository.writeBit({RegisterArea::M, 13}, false);
- require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded
- && !repository.readBit(*button.binding).value,
- "a momentary button release must reset safely after its condition changes");
- repository.writeBit({RegisterArea::M, 12}, true);
-
- HmiControl d_button = button;
- d_button.buttonEnableCondition = HmiButtonWordEnableCondition{
- RegisterAddress{RegisterArea::D, 100},
- RegisterDataType::Int16,
- HmiButtonConditionOperator::GreaterThanOrEqual,
- 50.0};
- repository.writeWord({RegisterArea::D, 100}, 50);
- condition = runtime_service.evaluateButtonEnabled(d_button);
- require(condition.succeeded && condition.enabled,
- "a satisfied D button condition must enable the button");
- repository.writeWord({RegisterArea::D, 100}, 49);
- condition = runtime_service.evaluateButtonEnabled(d_button);
- require(condition.succeeded && !condition.enabled,
- "a D button condition must compare the current repository 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<std::int16_t>(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>(float_value.numeric_value) == -2.5f,
- "Float32 numeric control must decode two consecutive D words");
- require(!runtime_service.writeNumericInput(
- float_input, std::numeric_limits<double>::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<std::uint16_t>(int32_words.values[0]) == 0x5678U
- && static_cast<std::uint16_t>(int32_words.values[1]) == 0x1234U
- && int32_value.succeeded
- && std::get<std::int32_t>(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<std::uint16_t>(double_words.values[0]) == 0x0000U
- && static_cast<std::uint16_t>(double_words.values[1]) == 0x0000U
- && static_cast<std::uint16_t>(double_words.values[2]) == 0x0000U
- && static_cast<std::uint16_t>(double_words.values[3]) == 0x3ff0U
- && double_value.succeeded
- && std::get<double>(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<double>::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<std::int16_t>(0), static_cast<std::int16_t>(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<int>(EditorHistory<int>::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<HmiControl> 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;
- }
|