diff --git a/CHANGELOG b/CHANGELOG index 59122d3cc..699b669ac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +#TESTAR v2.8.12 (1-Jun-2026) +- Add executeTriggeredAction in GenericUtilsProtocol +- Update parabank protocol to save login sequence in the state model +- Set Tag.InputText to action and concrete action +- Add screenshot to ConcreteAction +- Update web isFullVisibleAtCanvasBrowser logic + + #TESTAR v2.8.11 (25-May-2026) - Bump org.seleniumhq.selenium:selenium-java from 4.43.0 to 4.44.0 - Update devtools dependencies to v148 diff --git a/VERSION b/VERSION index 05822b7e6..d4a14aba0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.8.11 \ No newline at end of file +2.8.12 \ No newline at end of file diff --git a/core/src/org/testar/monkey/alayer/Tags.java b/core/src/org/testar/monkey/alayer/Tags.java index aafdfb849..67e36d3ec 100644 --- a/core/src/org/testar/monkey/alayer/Tags.java +++ b/core/src/org/testar/monkey/alayer/Tags.java @@ -144,6 +144,9 @@ private Tags() {} /** Usually attached to an object of {@link State}. The value is a screenshot of the state. */ public static final Tag ScreenshotPath = from("ScreenshotPath", String.class); + /** Usually attached to an object of {@link Action}. The value is a screenshot of the action target area. */ + public static final Tag ActionScreenshotPath = from("ActionScreenshotPath", String.class); + /** Usually attached to a {@link State} object. The value is a list of outcomes of test oracles for that state. */ @SuppressWarnings("unchecked") public static final Tag> OracleVerdicts = from("OracleVerdicts", (Class>)(Class)List.class); diff --git a/core/src/org/testar/monkey/alayer/actions/AnnotatingActionCompiler.java b/core/src/org/testar/monkey/alayer/actions/AnnotatingActionCompiler.java index 337eebbcb..d45678dbd 100644 --- a/core/src/org/testar/monkey/alayer/actions/AnnotatingActionCompiler.java +++ b/core/src/org/testar/monkey/alayer/actions/AnnotatingActionCompiler.java @@ -166,6 +166,7 @@ public Action clickTypeInto(final Widget widget, double relX, double relY, final //ret.set(Tags.Desc, "Type '" + Util.abbreviate(text, 5, "...") + "' into '" + widget.get(Tags.Desc, "" + "'")); ret.set(Tags.Desc, "Type '" + Util.abbreviate(text, DISPLAY_TEXT_MAX_LENGTH, "...") + "' into '" + widget.get(Tags.Desc, "" + "'")); // by urueda ret.mapOriginWidget(widget); + ret.set(Tags.InputText, text); return ret; } @@ -176,6 +177,7 @@ public Action clickAndReplaceText(final Position position, final String text){ ret.set(Tags.Visualizer, new TextVisualizer(position, Util.abbreviate(text, DISPLAY_TEXT_MAX_LENGTH, "..."), TypePen)); //ret.set(Tags.Desc, "Type '" + Util.abbreviate(text, 5, "...") + "' into '" + position.toString() + "'"); ret.set(Tags.Desc, "Replace '" + Util.abbreviate(text, DISPLAY_TEXT_MAX_LENGTH, "...") + "' into '" + position.toString() + "'"); + ret.set(Tags.InputText, text); ret.set(Tags.Role, ActionRoles.ClickTypeInto); return ret; } @@ -187,6 +189,7 @@ public Action clickAndAppendText(final Position position, final String text){ ret.set(Tags.Visualizer, new TextVisualizer(position, Util.abbreviate(text, DISPLAY_TEXT_MAX_LENGTH, "..."), TypePen)); //ret.set(Tags.Desc, "Type '" + Util.abbreviate(text, 5, "...") + "' into '" + position.toString() + "'"); ret.set(Tags.Desc, "Append '" + Util.abbreviate(text, DISPLAY_TEXT_MAX_LENGTH, "...") + "' into '" + position.toString() + "'"); + ret.set(Tags.InputText, text); ret.set(Tags.Role, ActionRoles.ClickTypeInto); return ret; } @@ -197,6 +200,7 @@ public Action pasteAndReplaceText(final Position position, final String text){ ret.set(Tags.Visualizer, new TextVisualizer(position, Util.abbreviate(text, DISPLAY_TEXT_MAX_LENGTH, "..."), TypePen)); ret.set(Tags.Role, ActionRoles.PasteTextInto); ret.set(Tags.Desc, "Paste Text: " + StringEscapeUtils.escapeHtml4(text)); + ret.set(Tags.InputText, text); return ret; } @@ -206,6 +210,7 @@ public Action pasteAndAppendText(final Position position, final String text){ ret.set(Tags.Visualizer, new TextVisualizer(position, Util.abbreviate(text, DISPLAY_TEXT_MAX_LENGTH, "..."), TypePen)); ret.set(Tags.Role, ActionRoles.PasteTextInto); ret.set(Tags.Desc, "Append Paste Text: " + StringEscapeUtils.escapeHtml4(text)); + ret.set(Tags.InputText, text); return ret; } diff --git a/statemodel/resources/graphs/graph.jsp b/statemodel/resources/graphs/graph.jsp index 4b5d5fb01..dca1ead38 100644 --- a/statemodel/resources/graphs/graph.jsp +++ b/statemodel/resources/graphs/graph.jsp @@ -831,6 +831,20 @@ contentPanelHeader.appendChild(closeButton); if (targetEdge.hasClass('ConcreteAction')) { + // add the concrete action screenshot in the side panel + let actionPopupAnchor = document.createElement("a"); + actionPopupAnchor.href = "${contentFolder}/" + targetEdge.id() + ".png"; + $(actionPopupAnchor).magnificPopup( + {type: "image"} + ); + + let actionImage = document.createElement("img"); + actionImage.alt = "Image for edge " + targetEdge.id(); + actionImage.src = "${contentFolder}/" + targetEdge.id() + ".png"; + actionImage.classList.add("node-img-full"); + actionPopupAnchor.appendChild(actionImage); + contentPanel.appendChild(actionPopupAnchor); + // if it is a concrete action edge, we add a popup // first the content let popupContent = document.createElement("div"); @@ -849,6 +863,12 @@ targetDiv.classList.add('screenshot'); targetDiv.appendChild(targetImg); + let actionDiv = document.createElement("div"); + let actionImg = document.createElement("img"); + actionImg.src = "${contentFolder}/" + targetEdge.id() + ".png"; + actionDiv.classList.add('screenshot'); + actionDiv.appendChild(actionImg); + // add the edge text let descDiv = document.createElement("div"); descDiv.appendChild(document.createTextNode(targetEdge.data('Desc'))); @@ -856,6 +876,7 @@ // add the divs in order popupContent.appendChild(sourceDiv); + popupContent.appendChild(actionDiv); popupContent.appendChild(descDiv); popupContent.appendChild(targetDiv); contentPanelHeader.appendChild(popupContent); diff --git a/statemodel/src/org/testar/statemodel/ConcreteAction.java b/statemodel/src/org/testar/statemodel/ConcreteAction.java index d32ecba98..fd8dc9344 100644 --- a/statemodel/src/org/testar/statemodel/ConcreteAction.java +++ b/statemodel/src/org/testar/statemodel/ConcreteAction.java @@ -1,7 +1,7 @@ /*************************************************************************************************** * - * Copyright (c) 2018 - 2025 Open Universiteit - www.ou.nl - * Copyright (c) 2018 - 2025 Universitat Politecnica de Valencia - www.upv.es + * Copyright (c) 2018 - 2026 Open Universiteit - www.ou.nl + * Copyright (c) 2018 - 2026 Universitat Politecnica de Valencia - www.upv.es * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -30,6 +30,7 @@ package org.testar.statemodel; +import java.util.Arrays; import java.util.Objects; public class ConcreteAction extends ModelWidget { @@ -44,6 +45,9 @@ public class ConcreteAction extends ModelWidget { */ private final AbstractAction abstractAction; + // a byte array holding the screenshot for this action + private byte[] screenshot; + /** * Constructor. * @param actionId @@ -64,4 +68,20 @@ public String getActionId() { public AbstractAction getAbstractAction() { return abstractAction; } + + /** + * Retrieves the screenshot data for this action. + * @return + */ + public byte[] getScreenshot() { + return screenshot != null ? Arrays.copyOf(screenshot, screenshot.length) : new byte[0]; + } + + /** + * Sets the screenshot data for this action. + * @param screenshot + */ + public void setScreenshot(byte[] screenshot) { + this.screenshot = screenshot != null ? Arrays.copyOf(screenshot, screenshot.length) : null; + } } diff --git a/statemodel/src/org/testar/statemodel/ConcreteActionFactory.java b/statemodel/src/org/testar/statemodel/ConcreteActionFactory.java index 1f0fec34b..57b8196ed 100644 --- a/statemodel/src/org/testar/statemodel/ConcreteActionFactory.java +++ b/statemodel/src/org/testar/statemodel/ConcreteActionFactory.java @@ -1,7 +1,7 @@ /*************************************************************************************************** * - * Copyright (c) 2018 - 2025 Open Universiteit - www.ou.nl - * Copyright (c) 2018 - 2025 Universitat Politecnica de Valencia - www.upv.es + * Copyright (c) 2018 - 2026 Open Universiteit - www.ou.nl + * Copyright (c) 2018 - 2026 Universitat Politecnica de Valencia - www.upv.es * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -30,13 +30,24 @@ package org.testar.statemodel; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.testar.monkey.Util; import org.testar.monkey.alayer.Action; import org.testar.monkey.alayer.Tag; import org.testar.monkey.alayer.Tags; import org.testar.monkey.alayer.Widget; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + public class ConcreteActionFactory { + protected static final Logger logger = LogManager.getLogger(); + public static ConcreteAction createConcreteAction(Action action, AbstractAction abstractAction) { ConcreteAction concreteAction = new ConcreteAction(action.get(Tags.ConcreteID), abstractAction); @@ -51,6 +62,34 @@ public static ConcreteAction createConcreteAction(Action action, AbstractAction if(action.get(Tags.Desc, null) != null) setSpecificAttribute(concreteAction, Tags.Desc, action.get(Tags.Desc)); + // check if the action as attached an InputText + // if so, set this InputText to the current ConcreteAction + if(action.get(Tags.InputText, null) != null) + setSpecificAttribute(concreteAction, Tags.InputText, action.get(Tags.InputText)); + + // get a screenshot for this concrete action + // in a headless environment, there may not be screenshots + String srcPath = action.get(Tags.ActionScreenshotPath, null); + if (srcPath != null && !srcPath.isEmpty()) { + Path normalizePath = Paths.get(srcPath).normalize(); + + // wait for action screenshot to be saved (max ~2s) + for (int i = 0; i < 20 && !Files.isRegularFile(normalizePath); i++) { + Util.pause(0.1); + } + + try { + if (Files.isRegularFile(normalizePath)) { + byte[] bytes = Files.readAllBytes(normalizePath); + concreteAction.setScreenshot(bytes); + } else { + logger.log(Level.WARN,"Action screenshot file not found: {}", normalizePath.toAbsolutePath()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return concreteAction; } diff --git a/statemodel/src/org/testar/statemodel/analysis/AnalysisManager.java b/statemodel/src/org/testar/statemodel/analysis/AnalysisManager.java index 5f21ed619..3f717acfc 100644 --- a/statemodel/src/org/testar/statemodel/analysis/AnalysisManager.java +++ b/statemodel/src/org/testar/statemodel/analysis/AnalysisManager.java @@ -452,7 +452,7 @@ private List fetchConcreteLayer(String modelIdentifier, ODatabaseSessio // concrete actions stmt = "SELECT FROM (TRAVERSE in('isAbstractedBy').outE('ConcreteAction') FROM (SELECT FROM AbstractState WHERE modelIdentifier = :identifier)) WHERE @class = 'ConcreteAction'"; resultSet = db.query(stmt, params); - elements.addAll(fetchEdges(resultSet, "ConcreteAction")); + elements.addAll(fetchConcreteActionEdges(resultSet, modelIdentifier)); resultSet.close(); return elements; @@ -655,6 +655,33 @@ private ArrayList fetchEdges(OResultSet resultSet, String className) { return elements; } + private ArrayList fetchConcreteActionEdges(OResultSet resultSet, String modelIdentifier) { + ArrayList elements = new ArrayList<>(); + while (resultSet.hasNext()) { + OResult result = resultSet.next(); + if (result.isEdge()) { + Optional op = result.getEdge(); + if (!op.isPresent()) continue; + OEdge actionEdge = op.get(); + OVertexDocument source = actionEdge.getProperty("out"); + OVertexDocument target = actionEdge.getProperty("in"); + Edge jsonEdge = new Edge("e" + formatId(actionEdge.getIdentity().toString()), "n" + formatId(source.getIdentity().toString()), "n" + formatId(target.getIdentity().toString())); + for (String propertyName : actionEdge.getPropertyNames()) { + if (propertyName.contains("in") || propertyName.contains("out")) { + continue; + } + if (propertyName.equals("screenshot")) { + processScreenShot(actionEdge.getProperty("screenshot"), "e" + formatId(actionEdge.getIdentity().toString()), modelIdentifier); + continue; + } + jsonEdge.addProperty(getExportPropertyName(propertyName), actionEdge.getProperty(propertyName).toString()); + } + elements.add(new Element(Element.GROUP_EDGES, jsonEdge, "ConcreteAction")); + } + } + return elements; + } + /** * This method saves screenshots to disk. * @param recordBytes diff --git a/statemodel/src/org/testar/statemodel/persistence/orientdb/OrientDBManager.java b/statemodel/src/org/testar/statemodel/persistence/orientdb/OrientDBManager.java index f3211e97b..102b1091c 100644 --- a/statemodel/src/org/testar/statemodel/persistence/orientdb/OrientDBManager.java +++ b/statemodel/src/org/testar/statemodel/persistence/orientdb/OrientDBManager.java @@ -1,7 +1,7 @@ /*************************************************************************************************** * - * Copyright (c) 2018 - 2025 Open Universiteit - www.ou.nl - * Copyright (c) 2018 - 2025 Universitat Politecnica de Valencia - www.upv.es + * Copyright (c) 2018 - 2026 Open Universiteit - www.ou.nl + * Copyright (c) 2018 - 2026 Universitat Politecnica de Valencia - www.upv.es * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -567,6 +567,7 @@ public void persistSequenceNode(SequenceNode sequenceNode) { // get the concrete state and make a vertex out of it EntityClass concreteStateClass = EntityClassFactory.createEntityClass(EntityClassFactory.EntityClassName.ConcreteState); VertexEntity stateEntity = new VertexEntity(concreteStateClass); + stateEntity.enableUpdate(false); // hydrate the entity to a format the orient database can store try { diff --git a/statemodel/src/org/testar/statemodel/persistence/orientdb/entity/EntityClassFactory.java b/statemodel/src/org/testar/statemodel/persistence/orientdb/entity/EntityClassFactory.java index 00f2b8f10..9a27190b8 100644 --- a/statemodel/src/org/testar/statemodel/persistence/orientdb/entity/EntityClassFactory.java +++ b/statemodel/src/org/testar/statemodel/persistence/orientdb/entity/EntityClassFactory.java @@ -1,7 +1,7 @@ /*************************************************************************************************** * - * Copyright (c) 2018 - 2025 Open Universiteit - www.ou.nl - * Copyright (c) 2018 - 2025 Universitat Politecnica de Valencia - www.upv.es + * Copyright (c) 2018 - 2026 Open Universiteit - www.ou.nl + * Copyright (c) 2018 - 2026 Universitat Politecnica de Valencia - www.upv.es * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -281,6 +281,11 @@ private static EntityClass createConcreteActionClass() { actionId.setNullable(false); actionId.setIdentifier(false); concreteActionClass.addProperty(actionId); + Property screenshot = new Property("screenshot", OType.BINARY); + screenshot.setMandatory(false); + screenshot.setNullable(true); + screenshot.setIdentifier(false); + concreteActionClass.addProperty(screenshot); Property counter = new Property("counter", OType.INTEGER); counter.setMandatory(true); counter.setNullable(false); diff --git a/statemodel/src/org/testar/statemodel/persistence/orientdb/hydrator/ConcreteActionHydrator.java b/statemodel/src/org/testar/statemodel/persistence/orientdb/hydrator/ConcreteActionHydrator.java index dbe3ab423..a502d51f2 100644 --- a/statemodel/src/org/testar/statemodel/persistence/orientdb/hydrator/ConcreteActionHydrator.java +++ b/statemodel/src/org/testar/statemodel/persistence/orientdb/hydrator/ConcreteActionHydrator.java @@ -1,7 +1,7 @@ /*************************************************************************************************** * - * Copyright (c) 2018 - 2025 Open Universiteit - www.ou.nl - * Copyright (c) 2018 - 2025 Universitat Politecnica de Valencia - www.upv.es + * Copyright (c) 2018 - 2026 Open Universiteit - www.ou.nl + * Copyright (c) 2018 - 2026 Universitat Politecnica de Valencia - www.upv.es * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -75,6 +75,11 @@ public void hydrate(EdgeEntity edgeEntity, Object source) throws HydrationExcept // add the action id edgeEntity.addPropertyValue("actionId", new PropertyValue(OType.STRING, ((ConcreteAction) source).getActionId())); + // add the screenshot + if (((ConcreteAction) source).getScreenshot() != null) { + edgeEntity.addPropertyValue("screenshot", new PropertyValue(OType.BINARY, ((ConcreteAction) source).getScreenshot())); + } + // loop through the tagged attributes for this state and add them TaggableBase attributes = ((ConcreteAction) source).getAttributes(); for (Tag tag :attributes.tags()) { diff --git a/statemodel/test/org/testar/statemodel/ConcreteActionTest.java b/statemodel/test/org/testar/statemodel/ConcreteActionTest.java index 2197f834b..ca5a9a641 100644 --- a/statemodel/test/org/testar/statemodel/ConcreteActionTest.java +++ b/statemodel/test/org/testar/statemodel/ConcreteActionTest.java @@ -22,6 +22,18 @@ public void testConcreteActionConstructor() { assertEquals(abstractAction, concreteAction.getAbstractAction()); } + @Test + public void testEmptyScreenshot() { + assertNotNull(concreteAction.getScreenshot()); + } + + @Test + public void testSetAndGetScreenshot() { + byte[] screenshotData = {1, 2, 3}; + concreteAction.setScreenshot(screenshotData); + assertArrayEquals(screenshotData, concreteAction.getScreenshot()); + } + @Test(expected = NullPointerException.class) public void testConstructorWithNullActionId() { new ConcreteAction(null, abstractAction); @@ -42,4 +54,25 @@ public void testConstructorWithBlankActionId() { new ConcreteAction(" ", abstractAction); } + @Test + public void testSetNullScreenshot() { + concreteAction.setScreenshot(null); + assertNotNull(concreteAction.getScreenshot()); + assertEquals(0, concreteAction.getScreenshot().length); + } + + @Test + public void testScreenshotDefensiveCopy() { + byte[] data = {1, 2, 3}; + concreteAction.setScreenshot(data); + data[0] = 99; + assertEquals(1, concreteAction.getScreenshot()[0]); + assertNotEquals(99, concreteAction.getScreenshot()[0]); + + byte[] retrieved = concreteAction.getScreenshot(); + retrieved[1] = 88; + assertEquals(1, concreteAction.getScreenshot()[0]); + assertNotEquals(88, concreteAction.getScreenshot()[1]); + } + } diff --git a/statemodel/test/org/testar/statemodel/persistence/orientdb/OrientDBManagerTest.java b/statemodel/test/org/testar/statemodel/persistence/orientdb/OrientDBManagerTest.java new file mode 100644 index 000000000..5d00e6a90 --- /dev/null +++ b/statemodel/test/org/testar/statemodel/persistence/orientdb/OrientDBManagerTest.java @@ -0,0 +1,101 @@ +package org.testar.statemodel.persistence.orientdb; + +import org.junit.Before; +import org.junit.Test; +import org.testar.monkey.alayer.Tags; +import org.testar.monkey.alayer.Verdict; +import org.testar.statemodel.AbstractState; +import org.testar.statemodel.ConcreteState; +import org.testar.statemodel.persistence.orientdb.entity.DocumentEntity; +import org.testar.statemodel.persistence.orientdb.entity.EdgeEntity; +import org.testar.statemodel.persistence.orientdb.entity.EntityManager; +import org.testar.statemodel.persistence.orientdb.entity.PropertyValue; +import org.testar.statemodel.persistence.orientdb.entity.VertexEntity; +import org.testar.statemodel.sequence.SequenceNode; +import org.testar.statemodel.util.EventHelper; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +public class OrientDBManagerTest { + + private EntityManager entityManager; + private OrientDBManager orientDBManager; + private Map> persistedConcreteStates; + + @Before + public void setUp() { + entityManager = mock(EntityManager.class); + persistedConcreteStates = new HashMap<>(); + doAnswer(invocation -> { + DocumentEntity documentEntity = invocation.getArgument(0); + capturePersistedConcreteState(documentEntity); + return null; + }).when(entityManager).saveEntity(any(DocumentEntity.class)); + orientDBManager = new OrientDBManager(new EventHelper(), entityManager); + } + + @Test + public void testPersistSequenceNodeDoesNotEnableConcreteStateUpdates() { + AbstractState abstractState = new AbstractState("abstract-state", Collections.emptySet()); + abstractState.setModelIdentifier("model-identifier"); + + List failVerdicts = Collections.singletonList(new Verdict(Verdict.Severity.WARNING_ACCESSIBILITY_FAULT, "Accessibility issue")); + List okVerdicts = Collections.singletonList(Verdict.OK); + + ConcreteState failingConcreteState = new ConcreteState("concrete-state", abstractState); + failingConcreteState.addAttribute(Tags.OracleVerdicts, failVerdicts); + + ConcreteState okConcreteState = new ConcreteState("concrete-state", abstractState); + okConcreteState.addAttribute(Tags.OracleVerdicts, okVerdicts); + + SequenceNode failingSequenceNode = new SequenceNode("sequence-id", 1, failingConcreteState, null, Collections.emptySet()); + SequenceNode okSequenceNode = new SequenceNode("sequence-id", 2, okConcreteState, null, Collections.emptySet()); + + orientDBManager.persistSequenceNode(failingSequenceNode); + orientDBManager.persistSequenceNode(okSequenceNode); + + Map persistedConcreteState = persistedConcreteStates.get("model-identifier-concrete-state"); + + assertNotNull(persistedConcreteState); + assertFalse(okVerdicts.equals(persistedConcreteState.get("OracleVerdicts"))); + assertEquals(failVerdicts, persistedConcreteState.get("OracleVerdicts")); + } + + private void capturePersistedConcreteState(DocumentEntity documentEntity) { + if (!(documentEntity instanceof EdgeEntity)) { + return; + } + + EdgeEntity edgeEntity = (EdgeEntity) documentEntity; + if (!"Accessed".equals(edgeEntity.getEntityClass().getClassName())) { + return; + } + + VertexEntity targetEntity = edgeEntity.getTargetEntity(); + if (!"ConcreteState".equals(targetEntity.getEntityClass().getClassName())) { + return; + } + + String concreteStateKey = (String) targetEntity.getPropertyValue("widgetId").getValue(); + if (persistedConcreteStates.containsKey(concreteStateKey) && !targetEntity.updateEnabled()) { + return; + } + + Map persistedProperties = new HashMap<>(); + for (String propertyName : targetEntity.getPropertyNames()) { + PropertyValue propertyValue = targetEntity.getPropertyValue(propertyName); + persistedProperties.put(propertyName, propertyValue == null ? null : propertyValue.getValue()); + } + persistedConcreteStates.put(concreteStateKey, persistedProperties); + } +} diff --git a/statemodel/test/org/testar/statemodel/persistence/orientdb/entity/EntityClassFactoryTest.java b/statemodel/test/org/testar/statemodel/persistence/orientdb/entity/EntityClassFactoryTest.java index 04a0a1891..154c05dbf 100644 --- a/statemodel/test/org/testar/statemodel/persistence/orientdb/entity/EntityClassFactoryTest.java +++ b/statemodel/test/org/testar/statemodel/persistence/orientdb/entity/EntityClassFactoryTest.java @@ -39,6 +39,7 @@ public void testCreateConcreteStateEntity() { assertEquals("ConcreteState", entityClass.getClassName()); assertEquals(EntityClass.EntityType.Vertex, entityClass.getEntityType()); assertFalse(entityClass.getProperties().isEmpty()); + assertTrue(entityClass.getProperties().stream().anyMatch(property -> "screenshot".equals(property.getPropertyName()))); } @Test @@ -48,6 +49,7 @@ public void testCreateConcreteActionEntity() { assertEquals("ConcreteAction", entityClass.getClassName()); assertEquals(EntityClass.EntityType.Edge, entityClass.getEntityType()); assertFalse(entityClass.getProperties().isEmpty()); + assertTrue(entityClass.getProperties().stream().anyMatch(property -> "screenshot".equals(property.getPropertyName()))); } @Test diff --git a/testar/resources/settings/02_webdriver_parabank/Protocol_02_webdriver_parabank.java b/testar/resources/settings/02_webdriver_parabank/Protocol_02_webdriver_parabank.java index cf0cd23cc..f5060a6f9 100644 --- a/testar/resources/settings/02_webdriver_parabank/Protocol_02_webdriver_parabank.java +++ b/testar/resources/settings/02_webdriver_parabank/Protocol_02_webdriver_parabank.java @@ -105,24 +105,17 @@ protected SUT startSystem() throws SystemStartException { protected void beginSequence(SUT system, State state) { super.beginSequence(system, state); - // Add your login sequence here + // Add your login sequence here + // The login sequence can be saved in the state model if enabled (due to executeTriggeredAction logic) + // NOTE: We pass a refreshed state for correctly mapping the transitions /* - waitLeftClickAndTypeIntoWidgetWithMatchingTag("name","username", "john", state, system, 5,1.0); + waitLeftClickAndTypeIntoWidgetWithMatchingTag("name","username", "john", getState(system), system, 5,1.0); - waitLeftClickAndTypeIntoWidgetWithMatchingTag("name","password", "demo", state, system, 5,1.0); + waitLeftClickAndPasteIntoWidgetWithMatchingTag("name","password", "demo", getState(system), system, 5,1.0); - waitAndLeftClickWidgetWithMatchingTag("value", "Log In", state, system, 5, 1.0); + waitAndLeftClickWidgetWithMatchingTag("value", "Log In", getState(system), system, 5, 1.0); */ - /* - * If you have issues typing special characters - * - * Try to use Paste Action with method: - * waitLeftClickAndPasteIntoWidgetWithMatchingTag - */ - // waitLeftClickAndPasteIntoWidgetWithMatchingTag("name", "username", "john", state, system, 5,1.0); - - /* * You can also use multiple Tags to find the correct widget. * This is because some widgets have common Tags Values. @@ -133,10 +126,24 @@ protected void beginSequence(SUT system, State state) { mapParabank.put("TextContent", "About Us"); mapParabank.put("Display", "inline"); - waitAndLeftClickWidgetWithMatchingTags(mapParabank, state, system, 5, 1.0); + waitAndLeftClickWidgetWithMatchingTags(mapParabank, getState(system), system, 5, 1.0); */ } + @Override + protected boolean executeTriggeredAction(SUT system, State state, Action triggeredAction) { + // Save the state information in the model + stateModelManager.notifyNewStateReached(state, new HashSet<>(Collections.singletonList(triggeredAction))); + + // Execute the desired triggeredAction (click, type, paste) + boolean executed = super.executeTriggeredAction(system, state, triggeredAction); + + // Save the triggeredAction information in the model + stateModelManager.notifyActionExecution(triggeredAction); + + return executed; + } + /** * This method is called when TESTAR requests the state of the SUT. * Here you can add additional information to the SUT's state or write your diff --git a/testar/resources/settings/02_webdriver_parabank/test.settings b/testar/resources/settings/02_webdriver_parabank/test.settings index cd551e260..edbd02cb8 100644 --- a/testar/resources/settings/02_webdriver_parabank/test.settings +++ b/testar/resources/settings/02_webdriver_parabank/test.settings @@ -164,7 +164,8 @@ ResetDataStore = false # Specify the widget attributes that you wish to use in constructing # the widget and state hash strings. Use a comma separated list. ################################################################# -AbstractStateAttributes = WebWidgetId + +AbstractStateAttributes = WebWidgetValue,WebWidgetId,WebWidgetName ################################################################# # WebDriver features diff --git a/testar/src/org/testar/monkey/DefaultProtocol.java b/testar/src/org/testar/monkey/DefaultProtocol.java index 414299122..3d39c3863 100644 --- a/testar/src/org/testar/monkey/DefaultProtocol.java +++ b/testar/src/org/testar/monkey/DefaultProtocol.java @@ -834,6 +834,19 @@ private void setStateScreenshot(State state) { } } + /** + * Take a Screenshot of the Action and associate the path into action tag + */ + private void setActionScreenshot(State state, Action action) { + // If the environment is not headless, take a screenshot + if (!GraphicsEnvironment.isHeadless()) { + String actionScreenshotPath = ScreenshotProviderFactory.current().getActionshot(state, action); + if (actionScreenshotPath != null && !actionScreenshotPath.isEmpty()) { + action.set(Tags.ActionScreenshotPath, actionScreenshotPath); + } + } + } + @Override protected List getVerdicts(State state) { Assert.notNull(state); @@ -1043,7 +1056,7 @@ protected boolean executeAction(SUT system, State state, Action action){ // adding the action that is going to be executed into report: reportManager.addSelectedAction(state, action); - ScreenshotProviderFactory.current().getActionshot(state, action); + setActionScreenshot(state, action); double waitTime = settings.get(ConfigTags.TimeToWaitAfterAction); @@ -1076,7 +1089,7 @@ protected boolean replayAction(SUT system, State state, Action action, double ac reportManager.addSelectedAction(state, action); // Get an action screenshot based on the NativeLinker platform - ScreenshotProviderFactory.current().getActionshot(state, action); + setActionScreenshot(state, action); try{ double halfWait = actionWaitTime == 0 ? 0.01 : actionWaitTime / 2.0; // seconds diff --git a/testar/src/org/testar/monkey/GenerateMode.java b/testar/src/org/testar/monkey/GenerateMode.java index 35c09df20..ac29c7a53 100644 --- a/testar/src/org/testar/monkey/GenerateMode.java +++ b/testar/src/org/testar/monkey/GenerateMode.java @@ -187,11 +187,12 @@ private List runGenerateInnerLoop(DefaultProtocol protocol, SUT system, protocol.cv.paintBatch(); } - //before action execution, pass it to the state model manager - protocol.stateModelManager.notifyActionExecution(action); - //Executing the selected action: protocol.executeAction(system, state, action); + + // pass the executed action execution to the state model manager + protocol.stateModelManager.notifyActionExecution(action); + DefaultProtocol.lastExecutedAction = action; protocol.actionCount++; diff --git a/testar/src/org/testar/monkey/Main.java b/testar/src/org/testar/monkey/Main.java index 95f33305a..84ffe6723 100644 --- a/testar/src/org/testar/monkey/Main.java +++ b/testar/src/org/testar/monkey/Main.java @@ -65,7 +65,7 @@ public class Main { - public static final String TESTAR_VERSION = "v2.8.11 (25-May-2026)"; + public static final String TESTAR_VERSION = "v2.8.12 (1-Jun-2026)"; //public static final String TESTAR_DIR_PROPERTY = "DIRNAME"; //Use the OS environment to obtain TESTAR directory public static final String SETTINGS_FILE = "test.settings"; diff --git a/testar/src/org/testar/monkey/ReplayMode.java b/testar/src/org/testar/monkey/ReplayMode.java index bb82f41e7..13883e5be 100644 --- a/testar/src/org/testar/monkey/ReplayMode.java +++ b/testar/src/org/testar/monkey/ReplayMode.java @@ -231,11 +231,11 @@ public void runReplayLoop(DefaultProtocol protocol) { protocol.preSelectAction(system, state, actions); - //before action execution, pass it to the state model manager - protocol.stateModelManager.notifyActionExecution(actionToReplay); - protocol.replayAction(system, state, actionToReplay, actionDelay, actionDuration); + // pass the replayed action execution to the state model manager + protocol.stateModelManager.notifyActionExecution(actionToReplay); + success = true; protocol.actionCount++; LogSerialiser.log("Success!\n", LogSerialiser.LogLevel.Info); diff --git a/testar/src/org/testar/protocols/GenericUtilsProtocol.java b/testar/src/org/testar/protocols/GenericUtilsProtocol.java index b901cdf71..86f3ef3a1 100644 --- a/testar/src/org/testar/protocols/GenericUtilsProtocol.java +++ b/testar/src/org/testar/protocols/GenericUtilsProtocol.java @@ -113,8 +113,7 @@ protected boolean waitAndLeftClickWidgetWithMatchingTags(Map tagV // Create the triggered action, build the identifier, and execute it. Action triggeredAction = triggeredClickAction(state, widget); buildStateActionsIdentifiers(state, Collections.singleton(triggeredAction)); - executeAction(system, state, triggeredAction); - return true; + return executeTriggeredAction(system, state, triggeredAction); } else { Util.pause(waitBetween); @@ -148,9 +147,7 @@ protected boolean waitAndLeftClickWidgetWithMatchingTag(Tag tag, String value // Create the triggered action, build the identifier, and execute it. Action triggeredAction = triggeredClickAction(state, widget); buildStateActionsIdentifiers(state, Collections.singleton(triggeredAction)); - executeAction(system, state, triggeredAction); - // is waiting needed after the action has been executed? - return true; + return executeTriggeredAction(system, state, triggeredAction); } else{ Util.pause(waitBetween); @@ -219,8 +216,7 @@ protected boolean waitLeftClickAndTypeIntoWidgetWithMatchingTags(Map tag, Stri // Create the triggered action, build the identifier, and execute it. Action triggeredAction = triggeredTypeAction(state, widget, textToType, true); buildStateActionsIdentifiers(state, Collections.singleton(triggeredAction)); - executeAction(system, state, triggeredAction); - // is waiting needed after the action has been executed? - return true; + return executeTriggeredAction(system, state, triggeredAction); } else{ Util.pause(waitBetween); @@ -327,8 +321,7 @@ protected boolean waitLeftClickAndPasteIntoWidgetWithMatchingTags(Map tag, Str // Create the triggered action, build the identifier, and execute it. Action triggeredAction = triggeredPasteAction(state, widget, textToPaste, true); buildStateActionsIdentifiers(state, Collections.singleton(triggeredAction)); - executeAction(system, state, triggeredAction); - // is waiting needed after the action has been executed? - return true; + return executeTriggeredAction(system, state, triggeredAction); } else{ Util.pause(waitBetween); @@ -708,6 +699,18 @@ protected Set retryDeriveAction(SUT system, int maxRetries, int waitingS return new HashSet<>(); } + /** + * Execute the selected triggered action. + * + * @param system + * @param state + * @param triggeredAction + * @return + */ + protected boolean executeTriggeredAction(SUT system, State state, Action triggeredAction) { + return executeAction(system, state, triggeredAction); + } + /** * By default, trigger click widget using LeftClickAt (Windows level). */ diff --git a/webdriver/src/org/testar/monkey/alayer/webdriver/WdElement.java b/webdriver/src/org/testar/monkey/alayer/webdriver/WdElement.java index f74fad50d..db6483cc5 100644 --- a/webdriver/src/org/testar/monkey/alayer/webdriver/WdElement.java +++ b/webdriver/src/org/testar/monkey/alayer/webdriver/WdElement.java @@ -408,12 +408,11 @@ private boolean isFullVisibleAtCanvasBrowser() { boolean isVisibleAtCanvas = rect.x() >= 0 && rect.x() + rect.width() <= CanvasDimensions.getCanvasWidth() && rect.y() >= 0 && rect.y() + rect.height() <= CanvasDimensions.getInnerHeight(); - // If the web element is a controls only render the selected option when collapsed. + // Custom listbox/menu widgets should rely on their actual geometry instead. + if (tagName != null && tagName.equalsIgnoreCase("option")) { + if (parent != null && parent.tagName != null && parent.tagName.equalsIgnoreCase("select")) { + return (selected || Boolean.TRUE.equals(ariaSelected)) && isVisibleAtCanvas; } }