|
- #include "domain/virtual_register_repository.h"
- #include "services/alarm_editor_service.h"
- #include "services/alarm_service.h"
- #include "services/hmi_editor_service.h"
- #include "services/hmi_navigation_service.h"
- #include "services/hmi_runtime_service.h"
- #include "services/logic_editor_service.h"
- #include "services/offline_simulation_service.h"
- #include "services/project_service.h"
- #include "services/register_monitor_service.h"
- #include "services/runtime_mode_service.h"
- #include "support/test_support.h"
- #include "ui/alarm_configuration_dialog.h"
- #include "ui/free_monitor_widget.h"
- #include "ui/hmi_editor_widget.h"
- #include "ui/logic_editor_widget.h"
- #include "ui/property_panel_controller.h"
- #include "ui/runtime_monitor_widget.h"
- #include "ui/runtime_panel_controller.h"
- #include "ui_main_window.h"
-
- #include <QApplication>
- #include <QCoreApplication>
- #include <QEventLoop>
- #include <QGraphicsLineItem>
- #include <QGraphicsRectItem>
- #include <QGraphicsScene>
- #include <QGraphicsSimpleTextItem>
- #include <QImage>
- #include <QKeyEvent>
- #include <QLabel>
- #include <QLineEdit>
- #include <QMainWindow>
- #include <QComboBox>
- #include <QMouseEvent>
- #include <QPainter>
- #include <QScrollBar>
- #include <QSpinBox>
- #include <QWidget>
-
- #include <algorithm>
- #include <iostream>
- #include <stdexcept>
- #include <string>
- #include <vector>
-
- namespace {
-
- LogicEditorResult setConditionAtColumn(
- LogicEditorService &editor,
- const std::string &logic_id,
- const std::string &rung_id,
- int column,
- const LogicNodeConfig &config,
- bool configured)
- {
- return editor.applyConditionAndAdvance(
- logic_id, {rung_id, column, false}, config, configured).edit;
- }
-
- using TestProjectStorage = TestSupport::InMemoryProjectStorage;
- using TestSupport::require;
-
- ControlLogic makeAlwaysOnLogic()
- {
- ControlLogic logic;
- logic.id = "queued-trace-logic";
- logic.name = "Queued trace logic";
-
- LadderRung rung;
- rung.id = "always-on-rung";
- rung.name = "Always on";
- for (int column = 0;
- column < ProjectLimits::kMaximumConditionColumns;
- ++column)
- {
- rung.cells.push_back({
- "always-on-cell-" + std::to_string(column),
- LadderCellKind::Wire,
- std::nullopt});
- }
-
- LogicNode output;
- output.id = "always-on-output";
- output.config = CoilNodeConfig{
- RegisterAddress{RegisterArea::M, 1}, CoilMode::Normal};
- rung.output = output;
- logic.rungs.push_back(rung);
- return logic;
- }
-
- void testMonitorOffersAllNumericTypes()
- {
- VirtualRegisterRepository repository;
- RegisterMonitorService service(repository);
- FreeMonitorWidget widget(service);
- QComboBox *type_combo = widget.findChild<QComboBox *>(
- QStringLiteral("dataTypeComboBox"));
- require(type_combo != nullptr
- && type_combo->count() == 4
- && type_combo->itemData(0).toInt()
- == static_cast<int>(RegisterDataType::Int16)
- && type_combo->itemData(1).toInt()
- == static_cast<int>(RegisterDataType::Int32)
- && type_combo->itemData(2).toInt()
- == static_cast<int>(RegisterDataType::Float32)
- && type_combo->itemData(3).toInt()
- == static_cast<int>(RegisterDataType::Float64)
- && type_combo->itemText(3)
- == QStringLiteral("Double (Float64)"),
- "shared data/free monitor UI must expose all four numeric types");
- }
-
- void testAlarmConfigurationOffersMOnAndMOff()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- AlarmEditorService alarm_editor_service(project_service);
- AlarmConfigurationDialog dialog(alarm_editor_service);
-
- QComboBox *area_combo = dialog.findChild<QComboBox *>(
- QStringLiteral("areaComboBox"));
- QComboBox *condition_combo = dialog.findChild<QComboBox *>(
- QStringLiteral("conditionComboBox"));
- require(area_combo != nullptr && condition_combo != nullptr,
- "alarm configuration must expose its area and condition inputs");
- require(area_combo->currentData().toInt()
- == static_cast<int>(RegisterArea::M)
- && condition_combo->count() == 2
- && condition_combo->findData(
- static_cast<int>(AlarmCondition::MOn)) >= 0
- && condition_combo->findData(
- static_cast<int>(AlarmCondition::MOff)) >= 0
- && condition_combo->findText(QStringLiteral("M 为 ON")) >= 0
- && condition_combo->findText(QStringLiteral("M 为 OFF")) >= 0,
- "M alarm configuration must offer both ON and OFF conditions");
- }
-
- void testAlarmListKeepsFixedGeometryWhileRecordsChange()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- VirtualRegisterRepository virtual_repository;
- HmiEditorService hmi_editor_service(project_service, HmiDefaultSettings{});
- HmiRuntimeService hmi_runtime_service(virtual_repository);
- AlarmService alarm_service(project_service, virtual_repository);
-
- const HmiEditorResult page = hmi_editor_service.ensureDefaultPage();
- const HmiEditorResult alarm_list = hmi_editor_service.addControl(
- page.id, HmiControlType::AlarmList);
- require(page.succeeded && alarm_list.succeeded,
- "alarm-list fixture must create a page and control");
- project_service.editProject().alarmDefinitions.push_back({
- "fixed-alarm",
- RegisterAddress{RegisterArea::M, 0},
- AlarmCondition::MOn,
- 0,
- "Fixed alarm"});
-
- HmiEditorWidget widget(
- hmi_editor_service, hmi_runtime_service, alarm_service);
- widget.setPageId(page.id);
- widget.setEditingEnabled(false);
- widget.setRuntimeActive(true);
- widget.refreshRuntimeValues();
-
- QGraphicsItem *alarm_item = nullptr;
- for (QGraphicsItem *item : widget.scene()->items())
- {
- if (dynamic_cast<QGraphicsRectItem *>(item) == nullptr)
- {
- alarm_item = item;
- break;
- }
- }
- const HmiControl *control = hmi_editor_service.findControl(
- page.id, alarm_list.id);
- require(alarm_item != nullptr && control != nullptr,
- "runtime HMI scene must contain the alarm-list item");
- const QRectF fixed_bounds = alarm_item->boundingRect();
- require(alarm_item->isVisible()
- && qFuzzyCompare(
- fixed_bounds.width() + 1.0,
- static_cast<qreal>(control->bounds.width) + 1.0)
- && qFuzzyCompare(
- fixed_bounds.height() + 1.0,
- static_cast<qreal>(control->bounds.height) + 1.0),
- "an empty runtime alarm list must remain visible at its configured size");
-
- require(virtual_repository.writeBit(
- RegisterAddress{RegisterArea::M, 0}, true).succeeded,
- "alarm-list fixture must activate M0");
- alarm_service.refresh();
- widget.refreshRuntimeValues();
- require(alarm_service.records().size() == 1U
- && alarm_item->isVisible()
- && alarm_item->boundingRect() == fixed_bounds,
- "adding an alarm row must not resize or hide the alarm control");
-
- require(virtual_repository.writeBit(
- RegisterAddress{RegisterArea::M, 0}, false).succeeded,
- "alarm-list fixture must restore M0");
- alarm_service.refresh();
- widget.refreshRuntimeValues();
- require(alarm_service.records().empty()
- && alarm_item->isVisible()
- && alarm_item->boundingRect() == fixed_bounds,
- "removing the last alarm row must keep the fixed alarm control visible");
- }
-
- void testHmiPropertyPanelInfersFixedBindingAreas()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- VirtualRegisterRepository repository;
- HmiDefaultSettings hmi_defaults;
- HmiEditorService hmi_editor_service(project_service, hmi_defaults);
- HmiRuntimeService hmi_runtime_service(repository);
- AlarmService alarm_service(project_service, repository);
- LogicEditorService logic_editor_service(project_service);
-
- const HmiEditorResult page = hmi_editor_service.ensureDefaultPage();
- const HmiEditorResult button = hmi_editor_service.addControl(
- page.id, HmiControlType::Button);
- const HmiEditorResult second_button = hmi_editor_service.addControl(
- page.id, HmiControlType::Button);
- const HmiEditorResult display = hmi_editor_service.addControl(
- page.id, HmiControlType::NumericDisplay);
- const HmiEditorResult second_display = hmi_editor_service.addControl(
- page.id, HmiControlType::NumericDisplay);
- require(page.succeeded && button.succeeded && second_button.succeeded
- && display.succeeded && second_display.succeeded,
- "property-panel fixture must create its page and controls");
- const HmiControl *default_button = hmi_editor_service.findControl(
- page.id, button.id);
- const HmiControl *default_second_button = hmi_editor_service.findControl(
- page.id, second_button.id);
- const HmiControl *default_display = hmi_editor_service.findControl(
- page.id, display.id);
- const HmiControl *default_second_display = hmi_editor_service.findControl(
- page.id, second_display.id);
- require(default_button != nullptr && default_second_button != nullptr
- && default_display != nullptr && default_second_display != nullptr
- && default_button->binding
- == RegisterAddress{RegisterArea::M, 0}
- && default_second_button->binding
- == RegisterAddress{RegisterArea::M, 1}
- && default_display->binding
- == RegisterAddress{RegisterArea::D, 0}
- && default_second_display->binding
- == RegisterAddress{RegisterArea::D, 1},
- "new HMI controls must receive sequential default M/D addresses");
-
- QMainWindow parent;
- Ui::MainWindow ui;
- ui.setupUi(&parent);
- HmiEditorWidget hmi_editor_widget(
- hmi_editor_service, hmi_runtime_service, alarm_service, &parent);
- LogicEditorWidget logic_editor_widget(logic_editor_service, &parent);
- hmi_editor_widget.setPageId(page.id);
-
- std::string selected_control_id;
- std::string selected_logic_node_id;
- PropertyPanelController controller(
- parent,
- ui,
- project_service,
- hmi_editor_service,
- logic_editor_service,
- hmi_defaults,
- [&page] { return page.id; },
- [] { return std::string{}; },
- selected_control_id,
- selected_logic_node_id,
- [] {},
- [](const QString &, const QString &message, bool succeeded)
- {
- require(succeeded, message.toStdString());
- },
- [](const QString &, int) {});
- controller.configure();
- controller.bindEditorWidgets(hmi_editor_widget, logic_editor_widget);
-
- controller.showControlProperties(button.id);
- require(ui.bindingAreaLabel->text()
- == QStringLiteral("绑定地址(M)")
- && ui.bindingIndexSpinBox->isEnabled()
- && ui.bindingIndexSpinBox->value() == 0,
- "button properties must show the fixed M address");
- ui.bindingIndexSpinBox->setValue(17);
- controller.applySelectedControlProperties();
- const HmiControl *updated_button = hmi_editor_service.findControl(
- page.id, button.id);
- require(updated_button != nullptr
- && updated_button->binding
- == RegisterAddress{RegisterArea::M, 17},
- "button property submission must infer the M area");
-
- controller.showControlProperties(display.id);
- require(ui.bindingAreaLabel->text()
- == QStringLiteral("绑定地址(D)")
- && ui.bindingIndexSpinBox->isEnabled()
- && ui.bindingIndexSpinBox->value() == 0,
- "numeric properties must show the fixed D address");
- ui.bindingIndexSpinBox->setValue(23);
- controller.applySelectedControlProperties();
- const HmiControl *updated_display = hmi_editor_service.findControl(
- page.id, display.id);
- require(updated_display != nullptr
- && updated_display->binding
- == RegisterAddress{RegisterArea::D, 23},
- "numeric property submission must infer the D area");
-
- }
-
- void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- VirtualRegisterRepository virtual_repository;
- OfflineSimulationService simulation_service(virtual_repository);
- OnlineLogicMonitorService online_monitor_service(virtual_repository);
- LogicEditorService logic_editor_service(project_service);
- RuntimeModeService runtime_mode_service(
- project_service,
- logic_editor_service,
- simulation_service,
- online_monitor_service);
- HmiEditorService hmi_editor_service(project_service, HmiDefaultSettings{});
- HmiRuntimeService hmi_runtime_service(virtual_repository);
- HmiNavigationService hmi_navigation_service(project_service);
- AlarmService alarm_service(project_service, virtual_repository);
- RegisterMonitorService register_monitor_service(virtual_repository);
-
- const ControlLogic logic = makeAlwaysOnLogic();
- project_service.editProject().controlLogics.push_back(logic);
-
- QWidget parent;
- HmiEditorWidget hmi_editor_widget(
- hmi_editor_service, hmi_runtime_service, alarm_service, &parent);
- LogicEditorWidget logic_editor_widget(logic_editor_service, &parent);
- QLabel executor_status_label(&parent);
- std::string current_logic_id = logic.id;
- logic_editor_widget.setLogicId(current_logic_id);
-
- RuntimePanelController controller(
- parent,
- runtime_mode_service,
- project_service,
- hmi_editor_service,
- hmi_runtime_service,
- logic_editor_service,
- hmi_navigation_service,
- alarm_service,
- register_monitor_service,
- hmi_editor_widget,
- logic_editor_widget,
- executor_status_label,
- [¤t_logic_id] { return current_logic_id; },
- [¤t_logic_id](const std::string &logic_id)
- {
- current_logic_id = logic_id;
- },
- [](const std::string &) {},
- [](const QString &, int) {},
- [](const QString &) {},
- [] {});
- controller.configure();
-
- require(runtime_mode_service.enterOfflineRunning().succeeded,
- "offline simulation must start for queued trace regression");
- controller.enterRuntime(
- {}, logic.id, runtime_mode_service.mode(),
- runtime_mode_service.plcConnectionState());
- QLabel *logic_label = controller.runtimeMonitorWidget()
- ->findChild<QLabel *>(QStringLiteral("logicLabel"));
- QLabel *notice_label = controller.runtimeMonitorWidget()
- ->findChild<QLabel *>(QStringLiteral("logicNoticeLabel"));
- require(logic_label != nullptr && notice_label != nullptr,
- "runtime monitor must expose its local trace labels");
- require(logic_label->text() == QStringLiteral("梯形图运行状态")
- && !notice_label->isVisible(),
- "offline runtime must keep the normal ladder trace heading");
- controller.runtimeMonitorWidget()->setMode(
- ApplicationMode::OnlineRunning, PlcConnectionState::Connected);
- require(logic_label->text() == QStringLiteral("本地推算轨迹")
- && notice_label->isVisible()
- && notice_label->text().contains(QStringLiteral("不写入 PLC 程序或 M/D"))
- && notice_label->text().contains(QStringLiteral("HMI 和自由监控仍可写 PLC")),
- "online runtime must explain the read-only local trace boundary");
- controller.runtimeMonitorWidget()->setMode(
- ApplicationMode::OfflineRunning, PlcConnectionState::Disconnected);
- require(simulation_service.executeOnce().succeeded,
- "offline simulation must produce a trace before editing");
- require(simulation_service.traceSnapshot()
- .forLogic(logic.id).rungValues.at("always-on-rung"),
- "the queued trace must contain an energized rung");
- QCoreApplication::processEvents(QEventLoop::AllEvents);
- LogicEditorWidget *runtime_logic_view = controller.runtimeMonitorWidget()
- ->findChild<LogicEditorWidget *>(QStringLiteral("runtimeLogicView"));
- bool found_active_runtime_output = false;
- require(runtime_logic_view != nullptr,
- "runtime monitor must own the ladder trace view");
- for (QGraphicsItem *item : runtime_logic_view->scene()->items())
- {
- if (item->data(0).toString() == QStringLiteral("output")
- && item->data(1).toString()
- == QStringLiteral("always-on-rung"))
- {
- found_active_runtime_output = item->data(3).toBool()
- && item->data(4).toBool();
- }
- }
- require(found_active_runtime_output,
- "the full executor snapshot must reach the runtime ladder exactly once");
-
- require(simulation_service.executeOnce().succeeded,
- "a second scan must queue the stale-trace regression event");
-
- require(runtime_mode_service.enterEditing().succeeded,
- "offline simulation must return to editing before queued delivery");
- controller.leaveRuntime(
- runtime_mode_service.mode(), runtime_mode_service.plcConnectionState());
- logic_editor_widget.clearRuntimeTrace();
- const auto runtimeOutputIsActive = [runtime_logic_view]
- {
- for (QGraphicsItem *item : runtime_logic_view->scene()->items())
- {
- if (item->data(0).toString() == QStringLiteral("output")
- && item->data(1).toString()
- == QStringLiteral("always-on-rung"))
- {
- return item->data(3).toBool() && item->data(4).toBool();
- }
- }
- return false;
- };
- require(!runtimeOutputIsActive(),
- "editing transition must initially clear the runtime trace");
-
- QCoreApplication::processEvents(QEventLoop::AllEvents);
- require(!runtimeOutputIsActive(),
- "a queued offline scan must not restore the trace after returning to editing");
- }
-
- void testLogicEditorGridSelectionAndDeletion()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- LogicEditorService logic_editor_service(project_service);
- const std::string logic_id = logic_editor_service.ensureDefaultLogic().id;
-
- LogicEditorWidget widget(logic_editor_service);
- widget.setLogicId(logic_id);
- require(widget.scene()->items().isEmpty(),
- "an empty logic must not draw standalone power rails");
- require((widget.alignment() & Qt::AlignLeft) != 0
- && (widget.alignment() & Qt::AlignTop) != 0,
- "the ladder canvas must start at its top-left origin");
-
- const std::string rung_id = logic_editor_service.addRung(logic_id).id;
- require(!rung_id.empty(), "the grid regression fixture must create a row");
- require(logic_editor_service.setHorizontalWireRange(
- logic_id, rung_id, 4, 4, true).succeeded,
- "the grid regression fixture must draw one horizontal cell");
- widget.reloadLogic();
- widget.resize(1200, 400);
- widget.show();
- QCoreApplication::processEvents(QEventLoop::AllEvents);
-
- constexpr qreal left_bus = 60.0;
- constexpr qreal cell_width = 96.0;
- constexpr qreal output_width = 224.0;
- constexpr qreal first_grid_top = 46.0;
- constexpr qreal row_height = 78.0;
- const auto clickScene = [&widget](const QPointF &position,
- Qt::KeyboardModifiers modifiers = Qt::NoModifier)
- {
- const QPoint point = widget.mapFromScene(position);
- QMouseEvent press(
- QEvent::MouseButtonPress,
- QPointF(point),
- Qt::LeftButton,
- Qt::LeftButton,
- modifiers);
- QApplication::sendEvent(widget.viewport(), &press);
- QMouseEvent release(
- QEvent::MouseButtonRelease,
- QPointF(point),
- Qt::LeftButton,
- Qt::NoButton,
- modifiers);
- QApplication::sendEvent(widget.viewport(), &release);
- };
- const auto dragScene = [&widget](
- const QPointF &from,
- const QPointF &to,
- Qt::KeyboardModifiers modifiers = Qt::NoModifier)
- {
- const QPoint from_point = widget.mapFromScene(from);
- const QPoint to_point = widget.mapFromScene(to);
- QMouseEvent press(
- QEvent::MouseButtonPress,
- QPointF(from_point),
- Qt::LeftButton,
- Qt::LeftButton,
- modifiers);
- QApplication::sendEvent(widget.viewport(), &press);
- QMouseEvent move(
- QEvent::MouseMove,
- QPointF(to_point),
- Qt::NoButton,
- Qt::LeftButton,
- modifiers);
- QApplication::sendEvent(widget.viewport(), &move);
- QMouseEvent release(
- QEvent::MouseButtonRelease,
- QPointF(to_point),
- Qt::LeftButton,
- Qt::NoButton,
- modifiers);
- QApplication::sendEvent(widget.viewport(), &release);
- };
- clickScene(QPointF(
- left_bus + 4.0 * cell_width + cell_width / 2.0,
- first_grid_top + row_height / 2.0));
-
- require(widget.selectedRungId() == rung_id,
- "clicking a grid cell must select its row");
- QString delete_error;
- QObject::connect(
- &widget,
- &LogicEditorWidget::editorError,
- [&delete_error](const QString &message) { delete_error = message; });
- const LogicEditorResult deleted = widget.deleteSelected();
- require(deleted.succeeded,
- "Delete on a selected horizontal cell must succeed: "
- + delete_error.toStdString());
- const LadderRung *rung = logic_editor_service.findRung(logic_id, rung_id);
- require(rung != nullptr && rung->cells.size() == 10U
- && rung->cells[4].kind == LadderCellKind::Gap,
- "Delete on one horizontal cell must preserve the row and clear only that cell");
-
- widget.clearSelection();
- require(widget.selectedRungId().empty(),
- "clearing the selection must not expose the first row as selected");
- require(!widget.addHorizontalWire().succeeded,
- "the horizontal-wire command must require an explicitly selected cell");
-
- require(logic_editor_service.setHorizontalWireRange(
- logic_id, rung_id, 3, 3, true).succeeded,
- "fixture must restore a wire for precise hit testing");
- widget.reloadLogic();
- clickScene(QPointF(
- left_bus + 3.0 * cell_width,
- first_grid_top + row_height / 2.0));
- require(!widget.deleteSelected().succeeded,
- "Delete on a column boundary must not delete an adjacent cell");
- require(logic_editor_service.findRung(logic_id, rung_id)->cells[3].kind
- == LadderCellKind::Wire,
- "a boundary selection must preserve the adjacent horizontal wire");
-
- clickScene(QPointF(
- left_bus + 10.0 * cell_width + output_width / 2.0,
- first_grid_top + row_height / 2.0));
- require(!widget.deleteSelected().succeeded
- && logic_editor_service.findLogic(logic_id)->rungs.size() == 1U,
- "Delete on an empty output slot must never delete the whole row");
-
- clickScene(QPointF(
- left_bus + 2.0 * cell_width + cell_width / 2.0,
- first_grid_top + row_height / 2.0));
- require(widget.addHorizontalWire().succeeded
- && logic_editor_service.findRung(logic_id, rung_id)->cells[2].kind
- == LadderCellKind::Wire,
- "the horizontal-wire command must act on an explicitly selected cell");
-
- const LogicEditorResult first_node = setConditionAtColumn(logic_editor_service,
- logic_id,
- rung_id,
- 0,
- ContactNodeConfig{
- RegisterAddress{RegisterArea::M, 0},
- ContactMode::NormallyOpen},
- true);
- const LogicEditorResult second_node = setConditionAtColumn(logic_editor_service,
- logic_id,
- rung_id,
- 1,
- ContactNodeConfig{
- RegisterAddress{RegisterArea::M, 1},
- ContactMode::NormallyOpen},
- true);
- require(first_node.succeeded && second_node.succeeded,
- "fixture must create adjacent conditions for multi-selection");
- widget.reloadLogic();
- dragScene(
- QPointF(left_bus + 5.0, first_grid_top + 5.0),
- QPointF(
- left_bus + 2.0 * cell_width - 5.0,
- first_grid_top + row_height - 5.0));
- require(widget.selectedNodeIds().size() == 2U,
- "mouse drag must select multiple conditions in the same row");
- widget.clearSelection();
- clickScene(QPointF(
- left_bus + cell_width / 2.0,
- first_grid_top + row_height / 2.0));
- dragScene(
- QPointF(left_bus + cell_width + 5.0, first_grid_top + 5.0),
- QPointF(
- left_bus + 2.0 * cell_width - 5.0,
- first_grid_top + row_height - 5.0),
- Qt::ControlModifier);
- require(widget.selectedNodeIds().size() == 2U,
- "Ctrl-drag must append objects to the existing selection");
- require(widget.addParallelBranch(ContactNodeConfig{
- RegisterAddress{RegisterArea::M, 2},
- ContactMode::NormallyOpen}).succeeded
- && logic_editor_service.findLogic(logic_id)->rungs.size() == 2U,
- "parallel insertion must use the explicitly selected condition range");
- const LogicEditorResult output = logic_editor_service.setOutput(
- logic_id,
- rung_id,
- CoilNodeConfig{
- RegisterAddress{RegisterArea::M, 3}, CoilMode::Normal},
- true);
- require(output.succeeded,
- "syntax-location fixture must create an output instruction");
- widget.reloadLogic();
- widget.focusSyntaxLocation(
- rung_id, ProjectLimits::kMaximumLadderColumns);
- require(
- widget.selectedRungId() == rung_id
- && widget.selectedNodeId() == output.id,
- "a syntax error at column 11 must focus the output slot");
- widget.focusSyntaxLocation(rung_id, 1);
- require(
- widget.selectedRungId() == rung_id
- && widget.selectedNodeId() == first_node.id,
- "a syntax error in the condition area must focus its one-based column");
- }
-
- void testLadderLayoutAndDragDeletion()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- LogicEditorService editor(project_service);
- const std::string logic_id = editor.ensureDefaultLogic().id;
- const std::string upper = editor.addRung(logic_id).id;
- const std::string lower = editor.addRung(logic_id).id;
- require(
- setConditionAtColumn(editor,
- logic_id,
- upper,
- 0,
- ContactNodeConfig{
- RegisterAddress{RegisterArea::M, 10},
- ContactMode::NormallyClosed},
- true).succeeded
- && editor.setHorizontalWireRange(
- logic_id, upper, 1, 1, true).succeeded
- && editor.setOutput(
- logic_id,
- upper,
- MoveNodeConfig{
- WordOperand{
- WordOperandKind::Register,
- RegisterAddress{RegisterArea::D, 10},
- 0},
- RegisterAddress{RegisterArea::D, 20}},
- true).succeeded
- && editor.setHorizontalWireRange(
- logic_id, lower, 0, 1, true).succeeded
- && editor.setVerticalConnection(
- logic_id, upper, lower, 2, true).succeeded
- && editor.updateNetworkComment(
- logic_id, lower, "主网络注释").succeeded,
- "layout fixture must create a connected two-row network");
- require(
- editor.findNetworkHeadRung(logic_id, lower)->id == upper
- && editor.findRung(logic_id, upper)->comment == "主网络注释"
- && editor.findRung(logic_id, lower)->comment.empty(),
- "editing a branch row must store the comment only on the network head");
- editor.clearHistory();
-
- LogicEditorWidget widget(editor);
- widget.setLogicId(logic_id);
- widget.resize(1320, 360);
- widget.show();
- QCoreApplication::processEvents(QEventLoop::AllEvents);
-
- bool found_head_comment = false;
- for (QGraphicsItem *item : widget.scene()->items())
- {
- auto *text = dynamic_cast<QGraphicsSimpleTextItem *>(item);
- if (text == nullptr)
- {
- continue;
- }
- found_head_comment = found_head_comment
- || text->text() == QStringLiteral("主网络注释");
- }
- require(found_head_comment,
- "the comment edited from a branch must render above the network head");
-
- constexpr qreal left_bus = 60.0;
- constexpr qreal cell_width = 96.0;
- constexpr qreal right_bus = left_bus + 10.0 * cell_width + 224.0;
- constexpr qreal first_grid_top = 46.0;
- constexpr qreal second_grid_bottom = first_grid_top + 2.0 * 78.0;
- QImage grid_image(1320, 360, QImage::Format_ARGB32_Premultiplied);
- grid_image.fill(Qt::transparent);
- {
- QPainter painter(&grid_image);
- widget.scene()->render(
- &painter,
- QRectF(0.0, 0.0, 1320.0, 360.0),
- QRectF(0.0, 0.0, 1320.0, 360.0),
- Qt::IgnoreAspectRatio);
- }
- const auto background_at = [&grid_image, first_grid_top](qreal x)
- {
- return grid_image.pixelColor(
- qRound(x), qRound(first_grid_top + 68.0));
- };
- const QColor node_background = background_at(
- left_bus + cell_width - 10.0);
- const QColor wire_background = background_at(
- left_bus + 2.0 * cell_width - 10.0);
- const QColor gap_background = background_at(
- left_bus + 3.0 * cell_width - 10.0);
- const QColor output_background = background_at(right_bus - 10.0);
- require(
- node_background == QColor(QStringLiteral("#ffffff"))
- && node_background == wire_background
- && wire_background == gap_background
- && gap_background == output_background,
- "node, wire, gap, and output cells must share one grid background");
-
- bool found_left_rail = false;
- bool found_right_rail = false;
- for (QGraphicsItem *item : widget.scene()->items())
- {
- auto *line_item = dynamic_cast<QGraphicsLineItem *>(item);
- if (line_item == nullptr || line_item->data(0).isValid())
- {
- continue;
- }
- const QLineF line = line_item->line();
- const bool exact_span = qFuzzyCompare(
- line.y1() + 1.0, first_grid_top + 1.0)
- && qFuzzyCompare(
- line.y2() + 1.0, second_grid_bottom + 1.0);
- found_left_rail = found_left_rail
- || (exact_span && qFuzzyCompare(
- line.x1() + 1.0, left_bus + 1.0));
- found_right_rail = found_right_rail
- || (exact_span && qFuzzyCompare(
- line.x1() + 1.0, right_bus + 1.0));
- }
- require(found_left_rail && found_right_rail,
- "both rails must share the exact first-to-last row span");
-
- const QPoint from = widget.mapFromScene(
- QPointF(left_bus + 4.0, first_grid_top + 4.0));
- const QPoint to = widget.mapFromScene(
- QPointF(right_bus - 4.0, second_grid_bottom - 4.0));
- QMouseEvent press(
- QEvent::MouseButtonPress,
- QPointF(from),
- Qt::LeftButton,
- Qt::LeftButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &press);
- QMouseEvent move(
- QEvent::MouseMove,
- QPointF(to),
- Qt::NoButton,
- Qt::LeftButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &move);
- QMouseEvent release(
- QEvent::MouseButtonRelease,
- QPointF(to),
- Qt::LeftButton,
- Qt::NoButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &release);
- require(widget.selectedNodeIds().size() == 2U,
- "drag selection must include the condition and output instruction");
- const QList<QGraphicsItem *> selected_scene_items = widget.scene()->items();
- require(
- std::any_of(
- selected_scene_items.cbegin(),
- selected_scene_items.cend(),
- [](QGraphicsItem *item) { return item->zValue() == 100.0; }),
- "selected objects must be painted by the highest selection layer");
-
- require(widget.deleteSelected().succeeded,
- "Delete must submit the complete drag selection once");
- const LadderRung *upper_rung = editor.findRung(logic_id, upper);
- const LadderRung *lower_rung = editor.findRung(logic_id, lower);
- require(
- upper_rung->cells[0].kind == LadderCellKind::Gap
- && upper_rung->cells[1].kind == LadderCellKind::Gap
- && !upper_rung->output.has_value()
- && lower_rung->cells[0].kind == LadderCellKind::Gap
- && lower_rung->cells[1].kind == LadderCellKind::Gap
- && editor.findLogic(logic_id)->verticalConnections.empty(),
- "drag deletion must clear cells, output, and vertical connection together");
- require(editor.undo().succeeded,
- "one undo must restore the complete drag deletion");
- upper_rung = editor.findRung(logic_id, upper);
- require(
- upper_rung->cells[0].kind == LadderCellKind::Node
- && upper_rung->cells[1].kind == LadderCellKind::Wire
- && upper_rung->output.has_value()
- && editor.findLogic(logic_id)->verticalConnections.size() == 1U,
- "one undo must restore every object removed by the drag selection");
- }
-
- void testWireGesturePreviewKeepsAtomicCommit()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- LogicEditorService editor(project_service);
- const std::string logic_id = editor.ensureDefaultLogic().id;
- const std::string first = editor.addRung(logic_id).id;
- const std::string second = editor.addRung(logic_id).id;
- editor.clearHistory();
- project_service.restoreModifiedState(false);
-
- LogicEditorWidget widget(editor);
- widget.setLogicId(logic_id);
- widget.setMouseWireMode(LogicEditorWidget::MouseWireMode::Draw);
- widget.resize(1200, 360);
- widget.show();
- QCoreApplication::processEvents(QEventLoop::AllEvents);
-
- constexpr qreal left_bus = 60.0;
- constexpr qreal cell_width = 96.0;
- constexpr qreal first_row_center = 91.0;
- constexpr qreal second_row_center = 197.0;
- const QPoint first_cell = widget.mapFromScene(QPointF(
- left_bus + cell_width / 2.0, first_row_center));
- const QPoint fourth_cell = widget.mapFromScene(QPointF(
- left_bus + 3.0 * cell_width + cell_width / 2.0,
- first_row_center));
- const QPoint invalid_target = widget.mapFromScene(QPointF(
- left_bus + 3.0 * cell_width + cell_width / 2.0,
- second_row_center));
- const auto press = [&widget](const QPoint &point)
- {
- QMouseEvent event(
- QEvent::MouseButtonPress,
- QPointF(point),
- Qt::LeftButton,
- Qt::LeftButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &event);
- };
- const auto move = [&widget](const QPoint &point)
- {
- QMouseEvent event(
- QEvent::MouseMove,
- QPointF(point),
- Qt::NoButton,
- Qt::LeftButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &event);
- QCoreApplication::processEvents(QEventLoop::AllEvents);
- };
- const auto release = [&widget](const QPoint &point)
- {
- QMouseEvent event(
- QEvent::MouseButtonRelease,
- QPointF(point),
- Qt::LeftButton,
- Qt::NoButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &event);
- QCoreApplication::processEvents(QEventLoop::AllEvents);
- };
- const auto countPreviewPixels = [&widget](bool invalid)
- {
- const QImage image = widget.viewport()->grab().toImage();
- int count = 0;
- for (int y = 0; y < image.height(); ++y)
- {
- for (int x = 0; x < image.width(); ++x)
- {
- const QColor color = image.pixelColor(x, y);
- const bool matches = invalid
- ? color.red() > color.green() + 45
- && color.red() > color.blue() + 25
- : color.blue() > color.red() + 50
- && color.blue() > color.green() + 20;
- count += matches ? 1 : 0;
- }
- }
- return count;
- };
-
- QString error_message;
- QObject::connect(
- &widget,
- &LogicEditorWidget::editorError,
- [&error_message](const QString &message) { error_message = message; });
-
- press(first_cell);
- move(fourth_cell);
- require(countPreviewPixels(false) > 20,
- "a legal drag must paint a visible temporary wire preview");
- const LadderRung *first_rung = editor.findRung(logic_id, first);
- require(
- std::all_of(
- first_rung->cells.cbegin(),
- first_rung->cells.cend(),
- [](const LadderCell &cell)
- {
- return cell.kind == LadderCellKind::Gap;
- })
- && !project_service.isModified()
- && !editor.canUndo(),
- "the temporary preview must not modify the project or history");
-
- move(invalid_target);
- require(countPreviewPixels(true) > 20,
- "an invalid drag direction must switch the preview to an error color");
- release(invalid_target);
- first_rung = editor.findRung(logic_id, first);
- require(
- !error_message.isEmpty()
- && std::all_of(
- first_rung->cells.cbegin(),
- first_rung->cells.cend(),
- [](const LadderCell &cell)
- {
- return cell.kind == LadderCellKind::Gap;
- })
- && editor.findRung(logic_id, second) != nullptr
- && !project_service.isModified()
- && !editor.canUndo(),
- "releasing an invalid gesture must discard its complete preview");
-
- error_message.clear();
- press(first_cell);
- move(fourth_cell);
- require(!project_service.isModified() && !editor.canUndo(),
- "a legal gesture must remain temporary until mouse release");
- release(fourth_cell);
- first_rung = editor.findRung(logic_id, first);
- require(
- error_message.isEmpty()
- && first_rung->cells[0].kind == LadderCellKind::Wire
- && first_rung->cells[1].kind == LadderCellKind::Wire
- && first_rung->cells[2].kind == LadderCellKind::Wire
- && first_rung->cells[3].kind == LadderCellKind::Wire
- && project_service.isModified()
- && editor.canUndo(),
- "releasing a legal gesture must commit its complete wire range once");
- require(editor.undo().succeeded && !editor.canUndo(),
- "one undo must remove the complete committed gesture");
- first_rung = editor.findRung(logic_id, first);
- require(
- std::all_of(
- first_rung->cells.cbegin(),
- first_rung->cells.cend(),
- [](const LadderCell &cell)
- {
- return cell.kind == LadderCellKind::Gap;
- }),
- "undo must restore every cell changed by the gesture");
- }
-
- void testEscapeExitsMouseWireMode()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- LogicEditorService editor(project_service);
- LogicEditorWidget widget(editor);
-
- const auto press_escape = [&widget]
- {
- QKeyEvent event(
- QEvent::KeyPress,
- Qt::Key_Escape,
- Qt::NoModifier);
- QApplication::sendEvent(&widget, &event);
- require(event.isAccepted(), "escape must be consumed by the logic editor");
- };
-
- widget.setMouseWireMode(LogicEditorWidget::MouseWireMode::Draw);
- press_escape();
- require(
- widget.mouseWireMode() == LogicEditorWidget::MouseWireMode::Select,
- "escape must exit mouse draw mode");
-
- widget.setMouseWireMode(LogicEditorWidget::MouseWireMode::Erase);
- press_escape();
- require(
- widget.mouseWireMode() == LogicEditorWidget::MouseWireMode::Select,
- "escape must exit mouse erase mode");
- }
-
- void testCursorAdvanceAndInlineCommandInput()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- LogicEditorService editor(project_service);
- const std::string logic_id = editor.ensureDefaultLogic().id;
- LogicEditorWidget widget(editor);
- widget.setLogicId(logic_id);
- widget.resize(1320, 440);
- widget.show();
- QCoreApplication::processEvents(QEventLoop::AllEvents);
-
- for (int column = 0;
- column < ProjectLimits::kMaximumConditionColumns;
- ++column)
- {
- require(widget.addHorizontalWire().succeeded,
- "repeated toolbar wire input must advance through all ten cells");
- }
- const ControlLogic *logic = editor.findLogic(logic_id);
- require(logic != nullptr && logic->rungs.size() == 1U,
- "the first wire on empty logic must atomically create one row");
- for (const LadderCell &cell : logic->rungs.front().cells)
- {
- require(cell.kind == LadderCellKind::Wire,
- "ten repeated wire actions must fill ten distinct cells");
- }
-
- require(widget.setOutput(
- CoilNodeConfig{
- RegisterAddress{RegisterArea::M, 60}, CoilMode::Normal},
- true).succeeded,
- "the output action must succeed after the tenth cell");
- logic = editor.findLogic(logic_id);
- require(logic->rungs.size() == 2U
- && logic->rungs.front().output.has_value(),
- "the output action must append the next empty row atomically");
- require(widget.addHorizontalWire().succeeded
- && editor.findLogic(logic_id)->rungs[1].cells[0].kind
- == LadderCellKind::Wire,
- "the toolbar cursor must continue at the appended row first cell");
-
- constexpr qreal left_bus = 60.0;
- constexpr qreal cell_width = 96.0;
- constexpr qreal output_width = 224.0;
- constexpr qreal second_row_center = 197.0;
- const auto double_click = [&widget](const QPointF &scene_point)
- {
- const QPoint point = widget.mapFromScene(scene_point);
- QMouseEvent event(
- QEvent::MouseButtonDblClick,
- QPointF(point),
- Qt::LeftButton,
- Qt::LeftButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &event);
- QCoreApplication::processEvents(QEventLoop::AllEvents);
- };
- const auto press_enter = [](QLineEdit *input)
- {
- QKeyEvent event(
- QEvent::KeyPress,
- Qt::Key_Return,
- Qt::NoModifier);
- QApplication::sendEvent(input, &event);
- QCoreApplication::processEvents(QEventLoop::AllEvents);
- };
-
- double_click(QPointF(
- left_bus + 1.5 * cell_width,
- second_row_center));
- QLineEdit *input = widget.findChild<QLineEdit *>(
- QStringLiteral("logicCommandInput"));
- require(input != nullptr && input->isVisible()
- && input->completer() != nullptr,
- "double-clicking a cell must open the inline command editor with completion");
- const int first_input_left = input->geometry().left();
- input->setText(QStringLiteral("LD M4"));
- press_enter(input);
- const LadderRung &second = editor.findLogic(logic_id)->rungs[1];
- require(second.cells[1].node.has_value()
- && input->isVisible()
- && input->geometry().left() > first_input_left,
- "a committed inline condition must move the editor one cell right");
-
- double_click(QPointF(
- left_bus + ProjectLimits::kMaximumConditionColumns * cell_width
- + output_width / 2.0,
- second_row_center));
- input->setText(QStringLiteral("OUT M61"));
- press_enter(input);
- logic = editor.findLogic(logic_id);
- require(logic->rungs.size() == 3U
- && logic->rungs[1].output.has_value()
- && input->isVisible(),
- "an inline output must append a row and keep continuous input active");
- }
-
- void testVerticalWireShortcutAdvancesDownward()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- LogicEditorService editor(project_service);
- const std::string logic_id = editor.ensureDefaultLogic().id;
- std::vector<std::string> rung_ids;
- for (int row = 0; row < 4; ++row)
- {
- rung_ids.push_back(editor.addRung(logic_id).id);
- }
- editor.clearHistory();
-
- LogicEditorWidget widget(editor);
- widget.setLogicId(logic_id);
- widget.resize(1320, 240);
- widget.show();
- QCoreApplication::processEvents(QEventLoop::AllEvents);
-
- constexpr int boundary = 4;
- constexpr qreal left_bus = 60.0;
- constexpr qreal cell_width = 96.0;
- constexpr qreal first_grid_top = 46.0;
- constexpr qreal row_height = 78.0;
- const QPoint point = widget.mapFromScene(QPointF(
- left_bus + (static_cast<qreal>(boundary) + 0.5) * cell_width,
- first_grid_top + row_height / 2.0));
- QMouseEvent press(
- QEvent::MouseButtonPress,
- QPointF(point),
- Qt::LeftButton,
- Qt::LeftButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &press);
- QMouseEvent release(
- QEvent::MouseButtonRelease,
- QPointF(point),
- Qt::LeftButton,
- Qt::NoButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &release);
- require(widget.selectedRungId() == rung_ids.front(),
- "the vertical shortcut fixture must select the first row");
-
- for (std::size_t row = 0U; row + 1U < rung_ids.size(); ++row)
- {
- const LogicEditorResult result = widget.addVerticalWire();
- require(
- result.succeeded
- && widget.selectedRungId() == rung_ids[row + 1U],
- "each vertical shortcut must advance to the next row");
- const ControlLogic *logic = editor.findLogic(logic_id);
- const bool connected = std::any_of(
- logic->verticalConnections.cbegin(),
- logic->verticalConnections.cend(),
- [&rung_ids, row, boundary](const VerticalConnection &connection)
- {
- return connection.upperRungId == rung_ids[row]
- && connection.lowerRungId == rung_ids[row + 1U]
- && connection.columnBoundary == boundary;
- });
- require(connected,
- "repeated vertical shortcuts must stay on one column boundary");
- }
- require(
- editor.findLogic(logic_id)->verticalConnections.size() == 3U
- && widget.verticalScrollBar()->value() > 0,
- "the vertical shortcut must create three segments and keep the target visible");
-
- const LogicEditorResult at_last_row = widget.addVerticalWire();
- require(
- !at_last_row.succeeded
- && at_last_row.message.find("末行") != std::string::npos
- && editor.findLogic(logic_id)->verticalConnections.size() == 3U
- && widget.selectedRungId() == rung_ids.back(),
- "the vertical shortcut must stop cleanly at the last existing row");
-
- for (int remaining = 2; remaining >= 0; --remaining)
- {
- require(
- editor.undo().succeeded
- && editor.findLogic(logic_id)->verticalConnections.size()
- == static_cast<std::size_t>(remaining),
- "each undo must remove exactly one shortcut-created vertical segment");
- }
- require(!editor.canUndo(),
- "three vertical shortcuts must create exactly three history entries");
- }
-
- void testSegmentLevelTraceProjection()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- LogicEditorService editor(project_service);
- const std::string logic_id = editor.ensureDefaultLogic().id;
- const std::string upper = editor.addRung(logic_id).id;
- const std::string lower = editor.addRung(logic_id).id;
- const LogicEditorResult condition = setConditionAtColumn(editor,
- logic_id,
- upper,
- 0,
- ContactNodeConfig{
- RegisterAddress{RegisterArea::M, 0},
- ContactMode::NormallyOpen},
- true);
- const LogicEditorResult output = editor.setOutput(
- logic_id,
- upper,
- CoilNodeConfig{
- RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal},
- true);
- require(condition.succeeded && output.succeeded
- && editor.setVerticalConnection(
- logic_id, upper, lower, 0, true).succeeded,
- "the segment trace fixture must create a contact, output and vertical edge");
- const ControlLogic *logic = editor.findLogic(logic_id);
- const std::string cell_id = logic->rungs.front().cells.front().id;
- const std::string vertical_id = logic->verticalConnections.front().id;
-
- LogicTraceSnapshot trace;
- LogicTraceValues &values = trace.logicValues[logic_id];
- values.cellInputPowerValues[cell_id] = true;
- values.cellPowerValues[cell_id] = false;
- values.rungValues[upper] = false;
- values.nodeValues[output.id] = false;
- LogicEditorWidget widget(editor);
- widget.setLogicId(logic_id);
- widget.resize(1320, 360);
- widget.show();
- widget.setRuntimeTrace(trace);
- QCoreApplication::processEvents(QEventLoop::AllEvents);
-
- bool found_split_contact = false;
- bool found_inactive_output = false;
- bool found_inactive_vertical = false;
- for (QGraphicsItem *item : widget.scene()->items())
- {
- const QString type = item->data(0).toString();
- if (type == QStringLiteral("cell")
- && item->data(1).toString().toStdString() == upper
- && item->data(2).toInt() == 0)
- {
- found_split_contact = item->data(6).toBool()
- && !item->data(7).toBool();
- }
- else if (type == QStringLiteral("output")
- && item->data(1).toString().toStdString() == upper)
- {
- found_inactive_output = !item->data(3).toBool()
- && !item->data(4).toBool();
- }
- else if (type == QStringLiteral("vertical")
- && item->data(1).toString().toStdString() == vertical_id)
- {
- found_inactive_vertical = !item->data(5).toBool();
- }
- }
- require(found_split_contact && found_inactive_output
- && found_inactive_vertical,
- "trace projection must keep left/right contact power and inactive verticals separate");
-
- values.cellPowerValues[cell_id] = true;
- values.rungValues[upper] = true;
- values.nodeValues[output.id] = true;
- values.verticalConnectionValues[vertical_id] = true;
- widget.setRuntimeTrace(trace);
- bool found_active_output = false;
- bool found_active_vertical = false;
- for (QGraphicsItem *item : widget.scene()->items())
- {
- const QString type = item->data(0).toString();
- if (type == QStringLiteral("output")
- && item->data(1).toString().toStdString() == upper)
- {
- found_active_output = item->data(3).toBool()
- && item->data(4).toBool();
- }
- else if (type == QStringLiteral("vertical")
- && item->data(1).toString().toStdString() == vertical_id)
- {
- found_active_vertical = item->data(5).toBool();
- }
- }
- require(found_active_output && found_active_vertical,
- "explicitly energized output and vertical segments must project as active");
- }
-
- void testLogicClipboardUsesExplicitObjectAndRowSelection()
- {
- TestProjectStorage storage;
- ProjectService project_service(storage, defaultProjectLimitSettings());
- LogicEditorService editor(project_service);
- const std::string logic_id = editor.ensureDefaultLogic().id;
- const std::string source = editor.addRung(logic_id).id;
- const std::string target = editor.addRung(logic_id).id;
- require(
- editor.setHorizontalWireRange(
- logic_id, source, 0, 0, true).succeeded,
- "clipboard fixture must create one source wire");
- editor.clearHistory();
-
- LogicEditorWidget widget(editor);
- widget.setLogicId(logic_id);
- widget.resize(1320, 360);
- widget.show();
- QCoreApplication::processEvents(QEventLoop::AllEvents);
- const auto find_item = [&widget](
- const QString &type,
- const std::string &rung_id,
- int column) -> QGraphicsItem *
- {
- const QList<QGraphicsItem *> items = widget.scene()->items();
- const auto found = std::find_if(
- items.cbegin(), items.cend(),
- [&type, &rung_id, column](QGraphicsItem *item)
- {
- return item->data(0).toString() == type
- && item->data(1).toString().toStdString() == rung_id
- && (column < 0 || item->data(2).toInt() == column);
- });
- return found == items.cend() ? nullptr : *found;
- };
- const auto click_item = [&widget](QGraphicsItem *item)
- {
- require(item != nullptr, "clipboard test target item must exist");
- const QPoint point = widget.mapFromScene(
- item->sceneBoundingRect().center());
- QMouseEvent press(
- QEvent::MouseButtonPress,
- QPointF(point),
- Qt::LeftButton,
- Qt::LeftButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &press);
- QMouseEvent release(
- QEvent::MouseButtonRelease,
- QPointF(point),
- Qt::LeftButton,
- Qt::NoButton,
- Qt::NoModifier);
- QApplication::sendEvent(widget.viewport(), &release);
- };
-
- click_item(find_item(QStringLiteral("cell"), source, 0));
- const LogicClipboardCopyResult wire_copy = widget.copySelection();
- require(
- widget.hasCopyableSelection()
- && wire_copy.copy.succeeded
- && wire_copy.fragment.mode == LogicClipboardMode::GridObjects
- && wire_copy.fragment.cells.size() == 1U
- && wire_copy.fragment.cells.front().kind == LadderCellKind::Wire
- && wire_copy.fragment.rows.empty(),
- "clicking one wire must copy one grid object instead of its whole row");
-
- click_item(find_item(QStringLiteral("cell"), target, 0));
- const LogicClipboardPasteResult pasted = widget.pasteClipboard(
- wire_copy.fragment);
- require(
- pasted.edit.succeeded
- && editor.findLogic(logic_id)->rungs.size() == 2U
- && editor.findCell(logic_id, target, 0)->kind
- == LadderCellKind::Wire,
- "widget paste must place one wire without creating or copying a row");
- const LogicClipboardCopyResult pasted_selection = widget.copySelection();
- require(
- pasted_selection.copy.succeeded
- && pasted_selection.fragment.cells.size() == 1U
- && pasted_selection.fragment.cells.front().kind
- == LadderCellKind::Wire,
- "a successful paste must select the newly pasted wire");
-
- click_item(find_item(QStringLiteral("rowHeader"), source, -1));
- const LogicClipboardCopyResult row_copy = widget.copySelection();
- require(
- row_copy.copy.succeeded
- && row_copy.fragment.mode == LogicClipboardMode::WholeRows
- && row_copy.fragment.rows.size() == 1U,
- "only clicking the left row header may create a whole-row clipboard");
- }
-
- } // namespace
-
- int main(int argc, char *argv[])
- {
- qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen"));
- QApplication application(argc, argv);
- return TestSupport::runTestSuite("runtime panel controller tests", {
- {"testAlarmConfigurationOffersMOnAndMOff", testAlarmConfigurationOffersMOnAndMOff},
- {"testMonitorOffersAllNumericTypes", testMonitorOffersAllNumericTypes},
- {"testAlarmListKeepsFixedGeometryWhileRecordsChange", testAlarmListKeepsFixedGeometryWhileRecordsChange},
- {"testHmiPropertyPanelInfersFixedBindingAreas", testHmiPropertyPanelInfersFixedBindingAreas},
- {"testQueuedOfflineTraceIsIgnoredAfterReturningToEditing", testQueuedOfflineTraceIsIgnoredAfterReturningToEditing},
- {"testLogicEditorGridSelectionAndDeletion", testLogicEditorGridSelectionAndDeletion},
- {"testLadderLayoutAndDragDeletion", testLadderLayoutAndDragDeletion},
- {"testWireGesturePreviewKeepsAtomicCommit", testWireGesturePreviewKeepsAtomicCommit},
- {"testEscapeExitsMouseWireMode", testEscapeExitsMouseWireMode},
- {"testCursorAdvanceAndInlineCommandInput", testCursorAdvanceAndInlineCommandInput},
- {"testVerticalWireShortcutAdvancesDownward", testVerticalWireShortcutAdvancesDownward},
- {"testSegmentLevelTraceProjection", testSegmentLevelTraceProjection},
- {"testLogicClipboardUsesExplicitObjectAndRowSelection", testLogicClipboardUsesExplicitObjectAndRowSelection},
- });
- }
|