Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.8.11
2.8.12
3 changes: 3 additions & 0 deletions core/src/org/testar/monkey/alayer/Tags.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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<String> 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<List<Verdict>> OracleVerdicts = from("OracleVerdicts", (Class<List<Verdict>>)(Class<?>)List.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<no description>" + "'"));
ret.set(Tags.Desc, "Type '" + Util.abbreviate(text, DISPLAY_TEXT_MAX_LENGTH, "...") + "' into '" + widget.get(Tags.Desc, "<no description>" + "'")); // by urueda
ret.mapOriginWidget(widget);
ret.set(Tags.InputText, text);
return ret;
}

Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down
21 changes: 21 additions & 0 deletions statemodel/resources/graphs/graph.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -849,13 +863,20 @@
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')));
descDiv.classList.add('action');

// add the divs in order
popupContent.appendChild(sourceDiv);
popupContent.appendChild(actionDiv);
popupContent.appendChild(descDiv);
popupContent.appendChild(targetDiv);
contentPanelHeader.appendChild(popupContent);
Expand Down
24 changes: 22 additions & 2 deletions statemodel/src/org/testar/statemodel/ConcreteAction.java
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -30,6 +30,7 @@

package org.testar.statemodel;

import java.util.Arrays;
import java.util.Objects;

public class ConcreteAction extends ModelWidget {
Expand All @@ -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
Expand All @@ -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;
}
}
43 changes: 41 additions & 2 deletions statemodel/src/org/testar/statemodel/ConcreteActionFactory.java
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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);

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ private List<Element> 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;
Expand Down Expand Up @@ -655,6 +655,33 @@ private ArrayList<Element> fetchEdges(OResultSet resultSet, String className) {
return elements;
}

private ArrayList<Element> fetchConcreteActionEdges(OResultSet resultSet, String modelIdentifier) {
ArrayList<Element> elements = new ArrayList<>();
while (resultSet.hasNext()) {
OResult result = resultSet.next();
if (result.isEdge()) {
Optional<OEdge> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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()) {
Expand Down
33 changes: 33 additions & 0 deletions statemodel/test/org/testar/statemodel/ConcreteActionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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]);
}

}
Loading
Loading