From 9e705369f731d0e352e2450385fe86e6ee89145e Mon Sep 17 00:00:00 2001 From: Axel RICHARD Date: Mon, 27 Jul 2026 10:46:01 +0200 Subject: [PATCH] [2358] Fix Expression creation/edition on Succession elements Bug: https://github.com/eclipse-syson/syson/issues/2358 Signed-off-by: Axel RICHARD --- CHANGELOG.adoc | 1 + ...xpressionsControllersIntegrationTests.java | 40 ++++++++ .../services/EditExpressionEventHandler.java | 3 +- .../MetamodelQueryElementService.java | 36 +++++-- .../MetamodelQueryElementServiceTest.java | 99 +++++++++++++++++++ .../pages/release-notes/2026.9.0.adoc | 4 + .../cypress/e2e/project/details/details.cy.ts | 20 ++++ 7 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 backend/services/syson-sysml-metamodel-services/src/test/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementServiceTest.java diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 5f99342eb..ed44136ca 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -21,6 +21,7 @@ Downstream code should now depend on the diagram services module for that bean. === Bug fixes - https://github.com/eclipse-syson/syson/issues/2359[#2359] [diagrams] Fix `Comment` graphical nodes so changing their background color no longer changes their border color. +- https://github.com/eclipse-syson/syson/issues/2358[#2358] [details] Fix expression creation and editing on `SuccessionAsUsage` elements. === Improvements diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/ExpressionsControllersIntegrationTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/ExpressionsControllersIntegrationTests.java index 22d39e931..5a42b088b 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/ExpressionsControllersIntegrationTests.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/ExpressionsControllersIntegrationTests.java @@ -68,6 +68,7 @@ import org.eclipse.syson.sysml.Element; import org.eclipse.syson.sysml.Expression; import org.eclipse.syson.sysml.FeatureValue; +import org.eclipse.syson.sysml.SuccessionAsUsage; import org.eclipse.syson.sysml.TransitionUsage; import org.eclipse.syson.sysml.dto.CreateExpressionInput; import org.eclipse.syson.sysml.dto.CreateExpressionSuccessPayload; @@ -634,6 +635,45 @@ public void canEditGuardExpressionOnTransition() { .verify(Duration.ofSeconds(10)); } + @DisplayName("GIVEN a SysML succession with a feature value expression, WHEN editing it from the succession THEN the expression is replaced") + @GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH }) + @Test + public void canEditFeatureValueExpressionOnSuccession() { + String editingContextId = ExpressionSamplesProjectData.EDITING_CONTEXT_ID; + + List defaultFilters = this.explorerDefaultFiltersSearchService.findTreeDefaultFilterIds(editingContextId, this.sysONExplorerTreeDescriptionId); + var explorerRepresentationId = this.representationIdBuilder.buildExplorerRepresentationId(this.sysONExplorerTreeDescriptionId, List.of(), defaultFilters); + var explorerInput = new ExplorerEventInput(UUID.randomUUID(), editingContextId, explorerRepresentationId); + var flux = this.explorerEventSubscriptionRunner.run(explorerInput).flux(); + + var treeId = new AtomicReference(); + Consumer initialTreeContentConsumer = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + treeId.set(tree.getId()); + }); + + String successionId = ExpressionSamplesProjectData.SemanticIds.THERMAL_CONTROL_TO_HEATING_SUCCESSION_ID; + Runnable createExpression = this.createExpression(editingContextId, successionId, "true"); + Consumer treeRefreshed = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getId()).isEqualTo(treeId.get()); + }); + Runnable checkCreatedExpression = this.checkElementHasExpression(editingContextId, successionId, SuccessionAsUsage.class, new AtomicReference<>(), "true"); + Runnable editExpression = this.editExpression(editingContextId, () -> successionId, "false"); + Runnable checkEditedExpression = this.checkElementHasExpression(editingContextId, successionId, SuccessionAsUsage.class, new AtomicReference<>(), "false"); + + StepVerifier.create(flux) + .consumeNextWith(initialTreeContentConsumer) + .then(createExpression) + .consumeNextWith(treeRefreshed) + .then(checkCreatedExpression) + .then(editExpression) + .consumeNextWith(treeRefreshed) + .then(checkEditedExpression) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } + @DisplayName("GIVEN a SysML attribute that has an existing expression, WHEN editing the expression to an invalid new value THEN the attribute's expression is not modified") @GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH }) @Test diff --git a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/EditExpressionEventHandler.java b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/EditExpressionEventHandler.java index d2b38599d..ff90da328 100644 --- a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/EditExpressionEventHandler.java +++ b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/EditExpressionEventHandler.java @@ -29,6 +29,7 @@ import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; import org.eclipse.syson.sysml.Element; import org.eclipse.syson.sysml.Expression; +import org.eclipse.syson.sysml.Feature; import org.eclipse.syson.sysml.Relationship; import org.eclipse.syson.sysml.dto.EditExpressionInput; import org.eclipse.syson.sysml.dto.EditExpressionSuccessPayload; @@ -89,7 +90,7 @@ public void handle(Sinks.One payloadSink, Sinks.Many getCompatibleExpressionOwnerships(Element element) } /** - * Check is a given {@code element} has a single/non-ambiguous existing {@link Expression} definition associated. + * Finds a single/non-ambiguous existing {@link Expression} definition associated with a given {@code element}, + * either directly or through an owned {@link FeatureValue}. * * @param element * the element to test - * @return true if the element has a single existing associated expression definition. + * @return the single associated expression definition, or an empty optional if none or more than one exists. */ public Optional findSingleExpressionDefinition(Element element) { Optional result = Optional.empty(); @@ -199,9 +202,22 @@ public Optional findSingleExpressionDefinition(Element element) { } else { ownedElements = element.getOwnedElement(); } - var ownedExpressions = ownedElements.stream().filter(child -> this.isExpressionDefinition(child)).toList(); - if (ownedExpressions.size() == 1) { - result = Optional.of((Expression) ownedExpressions.get(0)); + var expressionCandidates = new LinkedHashSet(); + ownedElements.stream() + .filter(this::isExpressionDefinition) + .map(Expression.class::cast) + .forEach(expressionCandidates::add); + if (element instanceof Feature feature) { + feature.getOwnedRelationship().stream() + .filter(FeatureValue.class::isInstance) + .map(FeatureValue.class::cast) + .map(FeatureValue::getValue) + .filter(Objects::nonNull) + .filter(this::isExpressionDefinition) + .forEach(expressionCandidates::add); + } + if (expressionCandidates.size() == 1) { + result = Optional.of(expressionCandidates.getFirst()); } } return result; diff --git a/backend/services/syson-sysml-metamodel-services/src/test/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementServiceTest.java b/backend/services/syson-sysml-metamodel-services/src/test/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementServiceTest.java new file mode 100644 index 000000000..c98f4ebbf --- /dev/null +++ b/backend/services/syson-sysml-metamodel-services/src/test/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementServiceTest.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (c) 2026 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.syson.sysml.metamodel.services; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.syson.sysml.FeatureValue; +import org.eclipse.syson.sysml.LiteralBoolean; +import org.eclipse.syson.sysml.PartUsage; +import org.eclipse.syson.sysml.SuccessionAsUsage; +import org.eclipse.syson.sysml.SysmlFactory; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link MetamodelQueryElementService}. + * + * @author arichard + */ +public class MetamodelQueryElementServiceTest { + + /** + * Verifies that a feature value expression is found from its owning feature. + */ + @Test + public void findSingleExpressionDefinitionInFeatureValueOfSuccessionAsUsage() { + var service = new MetamodelQueryElementService(); + SuccessionAsUsage successionAsUsage = SysmlFactory.eINSTANCE.createSuccessionAsUsage(); + FeatureValue featureValue = SysmlFactory.eINSTANCE.createFeatureValue(); + LiteralBoolean expression = SysmlFactory.eINSTANCE.createLiteralBoolean(); + + successionAsUsage.getOwnedRelationship().add(featureValue); + featureValue.getOwnedRelatedElement().add(expression); + + assertThat(service.findSingleExpressionDefinition(successionAsUsage)).containsSame(expression); + assertThat(service.hasSingleExpressionDefinition(successionAsUsage)).isTrue(); + } + + /** + * Verifies that an expression reached both directly and through a feature value is counted only once. + */ + @Test + public void findSingleExpressionDefinitionInFeatureValueOfPartUsage() { + var service = new MetamodelQueryElementService(); + PartUsage partUsage = SysmlFactory.eINSTANCE.createPartUsage(); + FeatureValue featureValue = SysmlFactory.eINSTANCE.createFeatureValue(); + LiteralBoolean expression = SysmlFactory.eINSTANCE.createLiteralBoolean(); + + partUsage.getOwnedRelationship().add(featureValue); + featureValue.getOwnedRelatedElement().add(expression); + + assertThat(service.findSingleExpressionDefinition(partUsage)).containsSame(expression); + assertThat(service.hasSingleExpressionDefinition(partUsage)).isTrue(); + } + + /** + * Verifies that a directly owned expression is found. + */ + @Test + public void findSingleExpressionDefinitionDirectlyOwnedByFeatureValue() { + var service = new MetamodelQueryElementService(); + FeatureValue featureValue = SysmlFactory.eINSTANCE.createFeatureValue(); + LiteralBoolean expression = SysmlFactory.eINSTANCE.createLiteralBoolean(); + + featureValue.getOwnedRelatedElement().add(expression); + + assertThat(service.findSingleExpressionDefinition(featureValue)).containsSame(expression); + } + + /** + * Verifies that distinct feature value expressions remain ambiguous. + */ + @Test + public void findSingleExpressionDefinitionWithMultipleFeatureValueExpressions() { + var service = new MetamodelQueryElementService(); + SuccessionAsUsage successionAsUsage = SysmlFactory.eINSTANCE.createSuccessionAsUsage(); + FeatureValue firstFeatureValue = SysmlFactory.eINSTANCE.createFeatureValue(); + FeatureValue secondFeatureValue = SysmlFactory.eINSTANCE.createFeatureValue(); + LiteralBoolean firstExpression = SysmlFactory.eINSTANCE.createLiteralBoolean(); + LiteralBoolean secondExpression = SysmlFactory.eINSTANCE.createLiteralBoolean(); + + successionAsUsage.getOwnedRelationship().add(firstFeatureValue); + successionAsUsage.getOwnedRelationship().add(secondFeatureValue); + firstFeatureValue.getOwnedRelatedElement().add(firstExpression); + secondFeatureValue.getOwnedRelatedElement().add(secondExpression); + + assertThat(service.findSingleExpressionDefinition(successionAsUsage)).isEmpty(); + assertThat(service.hasSingleExpressionDefinition(successionAsUsage)).isFalse(); + } +} diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc index d21a54fc3..dc50eaf7e 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc @@ -16,6 +16,10 @@ ** Fix `Comment` graphical nodes so changing their background color no longer changes their border color. +* In all views: + +** Fix expression creation and editing on `SuccessionAsUsage` elements. + * In textual import/export: diff --git a/integration-tests-cypress/cypress/e2e/project/details/details.cy.ts b/integration-tests-cypress/cypress/e2e/project/details/details.cy.ts index fe4520ed2..b5d2c2713 100644 --- a/integration-tests-cypress/cypress/e2e/project/details/details.cy.ts +++ b/integration-tests-cypress/cypress/e2e/project/details/details.cy.ts @@ -153,6 +153,26 @@ describe('Details View Tests', () => { cy.getByTestId('details-expression-value').should('have.text', ''); cy.getByTestId('Create-expression-button').should('exist').should('be.enabled'); }); + + it('Then creating an expression leaves a single Expression value widget', () => { + cy.getByTestId('Create-expression-button').click(); + cy.getByTestId('edit-sysml-expression-modal-textarea').clear().type('true'); + cy.getByTestId('edit-expression-submit').click(); + + cy.getByTestId('edit-sysml-expression-modal').should('not.exist'); + cy.getByTestId('details-expression-value').should('have.length', 1).and('have.text', 'true'); + cy.getByTestId('Edit-expression-button').should('have.length', 1).and('be.enabled'); + + explorer.getSelectedTreeItems().find('button').click(); + cy.getByTestId('treeitem-contextmenu').findByTestId('edit-sysml-expression-menu').should('exist'); + cy.getByTestId('treeitem-contextmenu').findByTestId('new-sysml-expression-menu').should('not.exist'); + cy.getByTestId('treeitem-contextmenu').findByTestId('edit-sysml-expression-menu').click(); + cy.getByTestId('edit-sysml-expression-modal-textarea').clear().type('false'); + cy.getByTestId('edit-expression-submit').click(); + + cy.getByTestId('edit-sysml-expression-modal').should('not.exist'); + cy.getByTestId('details-expression-value').should('have.length', 1).and('have.text', 'false'); + }); }); }); });