综合平台编程器项目的远程存储
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

561 linhas
28 KiB

  1. #include "domain/project_storage.h"
  2. #include "domain/register_repository.h"
  3. #include "domain/virtual_register_repository.h"
  4. #include "services/hmi_editor_service.h"
  5. #include "services/editor_history.h"
  6. #include "services/hmi_runtime_service.h"
  7. #include "services/hmi_navigation_service.h"
  8. #include "services/project_service.h"
  9. #include "support/test_support.h"
  10. #include <exception>
  11. #include <iostream>
  12. #include <limits>
  13. #include <stdexcept>
  14. #include <string>
  15. #include <variant>
  16. namespace {
  17. using TestProjectStorage = TestSupport::InMemoryProjectStorage;
  18. using TestSupport::require;
  19. void testControlEditing()
  20. {
  21. // 覆盖控件创建、移动、绑定校验、重命名冲突和删除流程
  22. TestProjectStorage storage;
  23. ProjectService project_service(storage, defaultProjectLimitSettings());
  24. HmiEditorService service(project_service, HmiDefaultSettings{});
  25. const HmiEditorResult page_result = service.ensureDefaultPage();
  26. require(page_result.succeeded, "default HMI page creation must succeed");
  27. const std::string page_id = page_result.id;
  28. const HmiEditorResult button = service.addControl(page_id, HmiControlType::Button);
  29. const HmiEditorResult indicator = service.addControl(page_id, HmiControlType::Indicator);
  30. const HmiEditorResult display = service.addControl(
  31. page_id, HmiControlType::NumericDisplay);
  32. const HmiEditorResult input = service.addControl(page_id, HmiControlType::NumericInput);
  33. require(button.succeeded && indicator.succeeded && display.succeeded
  34. && input.succeeded,
  35. "basic HMI controls must be added");
  36. const HmiPage *page = service.findPage(page_id);
  37. require(page != nullptr && page->controls.size() == 4,
  38. "all added controls must be kept in the page model");
  39. require(service.moveControl(page_id, button.id, {120, 80, 120, 40}).succeeded,
  40. "a valid control move must succeed");
  41. require(!service.moveControl(page_id, button.id, {790, 460, 120, 40}).succeeded,
  42. "a move outside the page must be rejected");
  43. HmiControl updated = *service.findControl(page_id, button.id);
  44. updated.binding = RegisterAddress{RegisterArea::D, 0};
  45. require(!service.updateControl(page_id, button.id, updated).succeeded,
  46. "buttons must reject D address bindings during editing");
  47. updated.binding = RegisterAddress{RegisterArea::M, 7};
  48. updated.text = "启动";
  49. require(service.updateControl(page_id, button.id, updated).succeeded,
  50. "a button M binding and edited text must be accepted");
  51. HmiControl duplicate = *service.findControl(page_id, indicator.id);
  52. duplicate.id = button.id;
  53. require(service.updateControl(page_id, indicator.id, duplicate).error
  54. == HmiEditorError::DuplicateId,
  55. "duplicate control ids must be rejected");
  56. require(service.removeControl(page_id, indicator.id).succeeded,
  57. "deleting a selected control must succeed");
  58. require(service.findControl(page_id, indicator.id) == nullptr,
  59. "deleted controls must not remain in the model");
  60. }
  61. void testRuntimeUsesRegisterRepository()
  62. {
  63. // 运行服务只能经由仓库接口读写 M/D,不依赖具体离线实现
  64. VirtualRegisterRepository repository;
  65. HmiRuntimeService runtime_service(repository);
  66. HmiControl button;
  67. button.id = "start";
  68. button.type = HmiControlType::Button;
  69. button.binding = RegisterAddress{RegisterArea::M, 12};
  70. require(button.buttonOperation == HmiButtonOperation::MomentaryOn,
  71. "new buttons must default to momentary ON");
  72. require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded,
  73. "momentary button press must be accepted by the repository");
  74. require(repository.readBit(*button.binding).value,
  75. "momentary button press must write ON");
  76. require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded,
  77. "momentary button release must be accepted by the repository");
  78. require(!repository.readBit(*button.binding).value,
  79. "momentary button release must write OFF");
  80. button.buttonOperation = HmiButtonOperation::SetOn;
  81. require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded,
  82. "set ON button press must succeed");
  83. require(repository.readBit(*button.binding).value,
  84. "set ON button press must write ON");
  85. require(runtime_service.operateButton(button, HmiButtonEvent::Released).succeeded,
  86. "set ON button release must be ignored successfully");
  87. require(repository.readBit(*button.binding).value,
  88. "set ON button release must keep the bit ON");
  89. button.buttonOperation = HmiButtonOperation::SetOff;
  90. require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded,
  91. "set OFF button press must succeed");
  92. require(!repository.readBit(*button.binding).value,
  93. "set OFF button press must write OFF");
  94. button.buttonOperation = HmiButtonOperation::Toggle;
  95. require(runtime_service.operateButton(button, HmiButtonEvent::Pressed).succeeded,
  96. "toggle button press must succeed");
  97. require(repository.readBit(*button.binding).value,
  98. "toggle button press must invert the current value");
  99. HmiControl indicator;
  100. indicator.id = "running";
  101. indicator.type = HmiControlType::Indicator;
  102. indicator.binding = RegisterAddress{RegisterArea::M, 12};
  103. const HmiRuntimeReadResult indicator_value = runtime_service.readControl(indicator);
  104. require(indicator_value.succeeded && indicator_value.bit_value,
  105. "indicators must read M values through the repository");
  106. HmiControl numeric_input;
  107. numeric_input.id = "target";
  108. numeric_input.type = HmiControlType::NumericInput;
  109. numeric_input.binding = RegisterAddress{RegisterArea::D, 9};
  110. require(runtime_service.writeNumericInput(numeric_input, -18).succeeded,
  111. "numeric input runtime writes must be accepted by the repository");
  112. HmiControl numeric_display;
  113. numeric_display.id = "actual";
  114. numeric_display.type = HmiControlType::NumericDisplay;
  115. numeric_display.binding = RegisterAddress{RegisterArea::D, 9};
  116. const HmiRuntimeReadResult numeric_value = runtime_service.readControl(numeric_display);
  117. require(numeric_value.succeeded
  118. && std::get<std::int16_t>(numeric_value.numeric_value) == -18,
  119. "numeric display must read D values through the repository");
  120. HmiControl float_input = numeric_input;
  121. float_input.dataType = RegisterDataType::Float32;
  122. float_input.binding = RegisterAddress{RegisterArea::D, 20};
  123. require(runtime_service.writeNumericInput(float_input, -2.5).succeeded,
  124. "Float32 numeric input must write two consecutive D words");
  125. const HmiRuntimeReadResult float_value = runtime_service.readControl(float_input);
  126. require(float_value.succeeded
  127. && std::get<float>(float_value.numeric_value) == -2.5f,
  128. "Float32 numeric control must decode two consecutive D words");
  129. require(!runtime_service.writeNumericInput(
  130. float_input, std::numeric_limits<double>::infinity()).succeeded,
  131. "Float32 numeric input must reject infinity");
  132. HmiControl int32_input = numeric_input;
  133. int32_input.dataType = RegisterDataType::Int32;
  134. int32_input.binding = RegisterAddress{RegisterArea::D, 30};
  135. require(runtime_service.writeNumericInput(int32_input, 305419896).succeeded,
  136. "Int32 numeric input must write two consecutive D words");
  137. const WordsReadResult int32_words = repository.readWords(*int32_input.binding, 2);
  138. const HmiRuntimeReadResult int32_value = runtime_service.readControl(int32_input);
  139. require(int32_words.succeeded
  140. && static_cast<std::uint16_t>(int32_words.values[0]) == 0x5678U
  141. && static_cast<std::uint16_t>(int32_words.values[1]) == 0x1234U
  142. && int32_value.succeeded
  143. && std::get<std::int32_t>(int32_value.numeric_value) == 0x12345678,
  144. "Int32 HMI read/write must use low-word-first Xinje ordering");
  145. HmiControl double_input = numeric_input;
  146. double_input.dataType = RegisterDataType::Float64;
  147. double_input.binding = RegisterAddress{RegisterArea::D, 40};
  148. require(runtime_service.writeNumericInput(double_input, 1.0).succeeded,
  149. "Double numeric input must write four consecutive D words");
  150. const WordsReadResult double_words = repository.readWords(*double_input.binding, 4);
  151. const HmiRuntimeReadResult double_value = runtime_service.readControl(double_input);
  152. require(double_words.succeeded
  153. && static_cast<std::uint16_t>(double_words.values[0]) == 0x0000U
  154. && static_cast<std::uint16_t>(double_words.values[1]) == 0x0000U
  155. && static_cast<std::uint16_t>(double_words.values[2]) == 0x0000U
  156. && static_cast<std::uint16_t>(double_words.values[3]) == 0x3ff0U
  157. && double_value.succeeded
  158. && std::get<double>(double_value.numeric_value) == 1.0,
  159. "Double HMI read/write must use four low-address-first words");
  160. require(!runtime_service.writeNumericInput(
  161. double_input, std::numeric_limits<double>::quiet_NaN()).succeeded,
  162. "Double numeric input must reject NaN");
  163. double_input.binding = RegisterAddress{RegisterArea::D, 3997};
  164. require(runtime_service.writeNumericInput(double_input, 1.0).error
  165. == HmiRuntimeError::InvalidBinding,
  166. "Double numeric input must reject odd or overflowing start addresses");
  167. }
  168. void testHistoryAndAtomicBatchDelete()
  169. {
  170. TestProjectStorage storage;
  171. ProjectService project_service(storage, defaultProjectLimitSettings());
  172. HmiEditorService service(project_service, HmiDefaultSettings{});
  173. const std::string page_id = service.ensureDefaultPage().id;
  174. const HmiEditorResult first = service.addControl(page_id, HmiControlType::Label);
  175. const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label);
  176. const HmiEditorResult third = service.addControl(page_id, HmiControlType::Label);
  177. require(first.succeeded && second.succeeded && third.succeeded,
  178. "controls for history testing must be created");
  179. service.clearHistory();
  180. require(!service.removeControls(page_id, {first.id, "missing-control"}).succeeded,
  181. "batch deletion must reject an unknown control before changing the page");
  182. require(service.findPage(page_id)->controls.size() == 3U
  183. && !service.canUndo(),
  184. "failed batch deletion must be atomic and leave history unchanged");
  185. require(service.removeControls(page_id, {first.id, second.id}).succeeded,
  186. "batch deletion of valid controls must succeed");
  187. require(service.findPage(page_id)->controls.size() == 1U
  188. && service.canUndo(),
  189. "valid batch deletion must remove all selected controls in one history step");
  190. require(service.undo().succeeded && service.findPage(page_id)->controls.size() == 3U,
  191. "HMI undo must restore a deleted batch");
  192. require(service.redo().succeeded && service.findPage(page_id)->controls.size() == 1U,
  193. "HMI redo must reapply a deleted batch");
  194. service.clearHistory();
  195. const HmiControl *remaining = service.findControl(page_id, third.id);
  196. require(remaining != nullptr,
  197. "the unselected control must survive a batch deletion");
  198. require(service.moveControl(page_id, third.id, remaining->bounds).succeeded
  199. && !service.canUndo(),
  200. "a no-op control move must not consume an undo step");
  201. service.clearHistory();
  202. HmiControl candidate = *service.findControl(page_id, third.id);
  203. for (int index = 1; index <= 101; ++index)
  204. {
  205. candidate.bounds.x = index;
  206. require(service.updateControl(page_id, third.id, candidate).succeeded,
  207. "repeated valid HMI edits must succeed");
  208. }
  209. int undo_count = 0;
  210. while (service.undo().succeeded)
  211. {
  212. ++undo_count;
  213. }
  214. require(undo_count == static_cast<int>(EditorHistory<int>::kMaximumEntries),
  215. "HMI history must retain exactly the configured most recent steps");
  216. }
  217. void testBatchPasteControls()
  218. {
  219. TestProjectStorage storage;
  220. ProjectService project_service(storage, defaultProjectLimitSettings());
  221. HmiEditorService service(project_service, HmiDefaultSettings{});
  222. const std::string page_id = service.ensureDefaultPage().id;
  223. const HmiEditorResult first = service.addControl(
  224. page_id, HmiControlType::NumericDisplay);
  225. const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label);
  226. require(first.succeeded && second.succeeded,
  227. "controls for paste testing must be created");
  228. HmiControl first_copy = *service.findControl(page_id, first.id);
  229. HmiControl second_copy = *service.findControl(page_id, second.id);
  230. first_copy.bounds = {100, 80, 80, 32};
  231. first_copy.type = HmiControlType::NumericDisplay;
  232. first_copy.text = "复制 Double";
  233. first_copy.binding = RegisterAddress{RegisterArea::D, 100};
  234. first_copy.dataType = RegisterDataType::Float64;
  235. first_copy.properties[HmiAppearanceProperty::kTextColor] = "#E53935";
  236. second_copy.bounds = {220, 80, 80, 32};
  237. require(service.updateControl(page_id, first.id, first_copy).succeeded
  238. && service.updateControl(page_id, second.id, second_copy).succeeded,
  239. "source controls must be movable before paste");
  240. service.clearHistory();
  241. const HmiEditorResult pasted = service.pasteControls(
  242. page_id, {first_copy, second_copy});
  243. require(pasted.succeeded
  244. && service.findPage(page_id)->controls.size() == 4U
  245. && pasted.id != first.id && pasted.id != second.id,
  246. "batch paste must create controls with fresh ids");
  247. const HmiControl *pasted_control = service.findControl(page_id, pasted.id);
  248. require(pasted_control != nullptr
  249. && pasted_control->bounds.x == first_copy.bounds.x + 20
  250. && pasted_control->bounds.y == first_copy.bounds.y + 20
  251. && pasted_control->text == first_copy.text
  252. && pasted_control->binding == first_copy.binding
  253. && pasted_control->dataType == RegisterDataType::Float64
  254. && pasted_control->properties == first_copy.properties,
  255. "pasted controls must preserve data type, binding and appearance with an offset");
  256. require(service.undo().succeeded
  257. && service.findPage(page_id)->controls.size() == 2U,
  258. "batch control paste must be one undoable operation");
  259. }
  260. void testRuntimeValidationAndPasteBoundaries()
  261. {
  262. TestProjectStorage storage;
  263. ProjectService project_service(storage, defaultProjectLimitSettings());
  264. HmiEditorService editor(project_service, HmiDefaultSettings{});
  265. const std::string page_id = editor.ensureDefaultPage().id;
  266. HmiControl label;
  267. label.id = "label-source";
  268. label.type = HmiControlType::Label;
  269. label.bounds = {760, 360, 32, 32};
  270. label.text = "边界";
  271. require(editor.pasteControls(page_id, {label}, 20, 20).succeeded,
  272. "a paste near the page edge must be clamped into the page");
  273. const HmiPage *page = editor.findPage(page_id);
  274. require(page != nullptr && page->controls.size() == 1U,
  275. "edge paste must add exactly one control");
  276. const HmiControl &edge_copy = page->controls.front();
  277. require(edge_copy.bounds.x + edge_copy.bounds.width <= page->width
  278. && edge_copy.bounds.y + edge_copy.bounds.height <= page->height
  279. && edge_copy.bounds.x >= 0 && edge_copy.bounds.y >= 0,
  280. "edge paste must keep the copied control inside page bounds");
  281. require(!editor.pasteControls(page_id, {}).succeeded,
  282. "an empty HMI clipboard must be rejected");
  283. HmiControl oversized = label;
  284. oversized.bounds = {0, 0, page->width + 1, 32};
  285. require(!editor.pasteControls(page_id, {oversized}).succeeded
  286. && editor.findPage(page_id)->controls.size() == 1U,
  287. "a copied control wider than the target page must fail atomically");
  288. HmiControl unsupported = label;
  289. unsupported.type = HmiControlType::Count;
  290. require(!editor.pasteControls(page_id, {unsupported}).succeeded
  291. && editor.findPage(page_id)->controls.size() == 1U,
  292. "an unsupported copied HMI type must fail atomically");
  293. std::vector<HmiControl> too_many(513U, label);
  294. require(!editor.pasteControls(page_id, too_many).succeeded
  295. && editor.findPage(page_id)->controls.size() == 1U,
  296. "a paste batch above the page limit must be rejected before mutation");
  297. VirtualRegisterRepository repository;
  298. HmiRuntimeService runtime(repository);
  299. HmiControl unbound_indicator;
  300. unbound_indicator.type = HmiControlType::Indicator;
  301. require(runtime.readControl(unbound_indicator).error
  302. == HmiRuntimeError::MissingBinding,
  303. "an unbound runtime indicator must report MissingBinding");
  304. HmiControl wrong_area_indicator = unbound_indicator;
  305. wrong_area_indicator.binding = RegisterAddress{RegisterArea::D, 0};
  306. require(runtime.readControl(wrong_area_indicator).error
  307. == HmiRuntimeError::InvalidBinding,
  308. "an indicator bound to D must report InvalidBinding");
  309. HmiControl invalid_indicator = unbound_indicator;
  310. invalid_indicator.binding = RegisterAddress{RegisterArea::M, 4001};
  311. require(runtime.readControl(invalid_indicator).error
  312. == HmiRuntimeError::InvalidBinding,
  313. "an indicator with an out-of-range address must report InvalidBinding");
  314. require(runtime.readControl(label).error
  315. == HmiRuntimeError::UnsupportedControl,
  316. "a static label must not be treated as a register runtime control");
  317. HmiControl wrong_button = unbound_indicator;
  318. wrong_button.type = HmiControlType::Button;
  319. wrong_button.binding = RegisterAddress{RegisterArea::D, 0};
  320. require(runtime.operateButton(wrong_button, HmiButtonEvent::Pressed).error
  321. == HmiRuntimeError::InvalidBinding,
  322. "a button bound to D must reject runtime writes");
  323. HmiControl wrong_numeric = unbound_indicator;
  324. wrong_numeric.type = HmiControlType::NumericInput;
  325. wrong_numeric.binding = RegisterAddress{RegisterArea::M, 0};
  326. require(runtime.writeNumericInput(wrong_numeric, 1).error
  327. == HmiRuntimeError::InvalidBinding,
  328. "a numeric input bound to M must reject runtime writes");
  329. }
  330. void testAppearanceEditing()
  331. {
  332. TestProjectStorage storage;
  333. ProjectService project_service(storage, defaultProjectLimitSettings());
  334. HmiEditorService service(project_service, HmiDefaultSettings{});
  335. const std::string page_id = service.ensureDefaultPage().id;
  336. const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label);
  337. require(label.succeeded, "a label must be available for appearance editing");
  338. HmiControl appearance = *service.findControl(page_id, label.id);
  339. appearance.properties[HmiAppearanceProperty::kTextColor] = "#E53935";
  340. appearance.properties[HmiAppearanceProperty::kFontSize] = "18";
  341. appearance.properties[HmiAppearanceProperty::kFontBold] = "true";
  342. appearance.properties[HmiAppearanceProperty::kFontItalic] = "false";
  343. require(service.updateControl(page_id, label.id, appearance).succeeded,
  344. "valid appearance properties must be applied atomically");
  345. const HmiControl *updated = service.findControl(page_id, label.id);
  346. require(updated != nullptr
  347. && updated->properties.at(HmiAppearanceProperty::kTextColor) == "#E53935"
  348. && updated->properties.at(HmiAppearanceProperty::kFontSize) == "18"
  349. && updated->properties.at(HmiAppearanceProperty::kFontBold) == "true",
  350. "appearance properties must be stored on the HMI control");
  351. HmiControl invalid = *updated;
  352. invalid.properties[HmiAppearanceProperty::kTextColor] = "invalid";
  353. require(!service.updateControl(page_id, label.id, invalid).succeeded,
  354. "invalid appearance properties must be rejected without a partial update");
  355. require(service.findControl(page_id, label.id)->properties.at(
  356. HmiAppearanceProperty::kTextColor) == "#E53935",
  357. "failed appearance updates must leave the old color intact");
  358. require(service.undo().succeeded,
  359. "appearance updates must participate in HMI undo history");
  360. require(service.findControl(page_id, label.id)->properties.empty(),
  361. "undo must remove the applied appearance properties");
  362. require(service.redo().succeeded
  363. && service.findControl(page_id, label.id)->properties.at(
  364. HmiAppearanceProperty::kFontSize) == "18",
  365. "redo must restore the applied appearance properties");
  366. }
  367. void testPageLifecycleAndNavigation()
  368. {
  369. TestProjectStorage storage;
  370. ProjectService project_service(storage, defaultProjectLimitSettings());
  371. HmiEditorService service(project_service, HmiDefaultSettings{});
  372. const std::string main_id = service.ensureDefaultPage().id;
  373. require(project_service.project().initialHmiPageId == main_id,
  374. "the first page must become the initial HMI page");
  375. const HmiEditorResult settings = service.addPage("Settings");
  376. const HmiEditorResult maintenance = service.addPage("Maintenance");
  377. require(settings.succeeded && maintenance.succeeded,
  378. "multiple HMI pages must be creatable");
  379. require(service.renamePage(settings.id, "Parameters").succeeded,
  380. "an HMI page must be renamable by stable id");
  381. require(service.renamePage(maintenance.id, "Parameters").error
  382. == HmiEditorError::DuplicateName,
  383. "HMI page names must remain unique");
  384. require(service.movePage(maintenance.id, -1).succeeded
  385. && project_service.project().hmiPages.at(1).id == maintenance.id,
  386. "HMI page order must be editable independently from page ids");
  387. const HmiEditorResult label = service.addControl(main_id, HmiControlType::Label);
  388. require(label.succeeded
  389. && service.findControl(main_id, label.id)->isConfigured(),
  390. "the editor service must expose a configured static Label control");
  391. const HmiEditorResult alarm_list = service.addControl(
  392. main_id, HmiControlType::AlarmList);
  393. require(alarm_list.succeeded
  394. && service.findControl(main_id, alarm_list.id)->isConfigured()
  395. && !service.findControl(main_id, alarm_list.id)->binding.has_value(),
  396. "AlarmList must be configured without a register binding");
  397. HmiControl bound_alarm_list = *service.findControl(main_id, alarm_list.id);
  398. bound_alarm_list.binding = RegisterAddress{RegisterArea::M, 20};
  399. require(!service.updateControl(
  400. main_id, alarm_list.id, bound_alarm_list).succeeded,
  401. "AlarmList must reject direct register bindings");
  402. const HmiEditorResult jump = service.addControl(main_id, HmiControlType::PageJump);
  403. require(jump.succeeded, "the editor service must expose PageJump creation");
  404. HmiControl jump_control = *service.findControl(main_id, jump.id);
  405. jump_control.pageJump = HmiPageJumpConfig{maintenance.id};
  406. require(service.updateControl(main_id, jump.id, jump_control).succeeded,
  407. "PageJump target updates must resolve stable page ids");
  408. require(service.removePage(maintenance.id).error == HmiEditorError::PageReferenced,
  409. "a page referenced by PageJump must not be deletable");
  410. require(service.setInitialPage(settings.id).succeeded,
  411. "the user must be able to select a different initial page");
  412. require(service.removePage(settings.id).error
  413. == HmiEditorError::InitialPageCannotBeRemoved,
  414. "the initial page must not be deleted implicitly");
  415. HmiNavigationService navigation(project_service);
  416. const HmiNavigationResult start = navigation.start();
  417. require(start.succeeded && start.pageId == settings.id,
  418. "runtime navigation must start from the persisted initial page");
  419. require(navigation.navigateTo(maintenance.id).succeeded
  420. && navigation.currentPageId() == maintenance.id,
  421. "runtime navigation must switch to an existing page");
  422. require(navigation.navigateTo("missing").error
  423. == HmiNavigationError::PageNotFound,
  424. "runtime navigation must reject an unknown page id");
  425. navigation.stop();
  426. require(navigation.currentPageId().empty(),
  427. "stopping runtime navigation must clear session state");
  428. }
  429. void testPageResizeIsAtomicAndUndoable()
  430. {
  431. TestProjectStorage storage;
  432. ProjectService project_service(storage, defaultProjectLimitSettings());
  433. HmiEditorService service(project_service, HmiDefaultSettings{});
  434. const std::string page_id = service.ensureDefaultPage().id;
  435. const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label);
  436. require(label.succeeded, "page resize fixture must add a control");
  437. HmiControl control = *service.findControl(page_id, label.id);
  438. control.bounds = {700, 340, 80, 32};
  439. require(service.updateControl(page_id, label.id, control).succeeded,
  440. "page resize fixture must place the control near the page edge");
  441. require(service.resizePage(page_id, 1024, 600).succeeded,
  442. "an HMI page must be resizable to a larger valid rectangle");
  443. require(service.findPage(page_id)->width == 1024
  444. && service.findPage(page_id)->height == 600,
  445. "page resize must update both dimensions");
  446. require(service.resizePage(page_id, 700, 300).error
  447. == HmiEditorError::InvalidPage,
  448. "page resize must reject dimensions that clip an existing control");
  449. require(service.findPage(page_id)->width == 1024
  450. && service.findPage(page_id)->height == 600,
  451. "a rejected page resize must leave the original dimensions intact");
  452. require(service.undo().succeeded
  453. && service.findPage(page_id)->width == 800
  454. && service.findPage(page_id)->height == 400,
  455. "page resize must participate in HMI undo history");
  456. require(service.redo().succeeded
  457. && service.findPage(page_id)->width == 1024
  458. && service.findPage(page_id)->height == 600,
  459. "page resize redo must restore the new dimensions");
  460. require(service.resizePage(page_id, 319, 600).error
  461. == HmiEditorError::InvalidPage,
  462. "page width below the business minimum must be rejected");
  463. }
  464. void testConfiguredPageDefaultsAndLimits()
  465. {
  466. TestProjectStorage storage;
  467. ProjectLimitSettings limits;
  468. limits.maximumHmiPages = 1U;
  469. limits.maximumHmiControlsPerPage = 1U;
  470. HmiDefaultSettings defaults;
  471. defaults.pageWidth = 1024;
  472. defaults.pageHeight = 600;
  473. ProjectService project_service(storage, limits);
  474. HmiEditorService service(project_service, defaults);
  475. const HmiEditorResult page = service.ensureDefaultPage();
  476. require(page.succeeded
  477. && service.findPage(page.id)->width == 1024
  478. && service.findPage(page.id)->height == 600,
  479. "configured HMI dimensions must initialize the default page");
  480. require(!service.addPage("Second page").succeeded,
  481. "the HMI editor must use the configured page limit");
  482. require(service.addControl(page.id, HmiControlType::Label).succeeded
  483. && !service.addControl(page.id, HmiControlType::Label).succeeded,
  484. "the HMI editor must use the configured per-page control limit");
  485. }
  486. } // namespace
  487. int main()
  488. {
  489. try
  490. {
  491. // 编辑和运行场景分别验证服务层两条独立职责
  492. testControlEditing();
  493. testHistoryAndAtomicBatchDelete();
  494. testBatchPasteControls();
  495. testRuntimeValidationAndPasteBoundaries();
  496. testAppearanceEditing();
  497. testRuntimeUsesRegisterRepository();
  498. testPageLifecycleAndNavigation();
  499. testPageResizeIsAtomicAndUndoable();
  500. testConfiguredPageDefaultsAndLimits();
  501. }
  502. catch (const std::exception &error)
  503. {
  504. std::cerr << "HMI editor service tests failed: " << error.what() << '\n';
  505. return 1;
  506. }
  507. std::cout << "HMI editor service tests passed\n";
  508. return 0;
  509. }