Skip to content

Commit 4e82235

Browse files
committed
[WIP] Rework elements location strategy:
- Finding the closest element to matching point instead of the topmost element/ all elements on point - Support finding multiple elements (multiple image matches) - Support relative search (e.g. from element) - Add javadocs
1 parent 3dcf029 commit 4e82235

File tree

1 file changed

+71
-22
lines changed
  • src/main/java/aquality/selenium/elements/interfaces

1 file changed

+71
-22
lines changed

src/main/java/aquality/selenium/elements/interfaces/ByImage.java

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
package aquality.selenium.elements.interfaces;
22

33
import aquality.selenium.browser.AqualityServices;
4-
import org.opencv.core.Core;
5-
import org.opencv.core.Mat;
6-
import org.opencv.core.MatOfByte;
74
import org.opencv.core.Point;
5+
import org.opencv.core.*;
86
import org.opencv.imgcodecs.Imgcodecs;
97
import org.opencv.imgproc.Imgproc;
108
import org.openqa.selenium.*;
9+
import org.openqa.selenium.interactions.Locatable;
1110

1211
import java.io.File;
1312
import java.util.ArrayList;
13+
import java.util.Comparator;
1414
import java.util.List;
15+
import java.util.stream.Collectors;
1516

17+
/**
18+
* Locator to search elements by image.
19+
* Takes screenshot and finds match using openCV.
20+
* Then finds elements by coordinates using javascript.
21+
*/
1622
public class ByImage extends By {
1723
private static boolean wasLibraryLoaded = false;
1824
private final Mat template;
@@ -25,16 +31,31 @@ private static void loadLibrary() {
2531
}
2632
}
2733

34+
/**
35+
* Constructor accepting image file.
36+
*
37+
* @param file image file to locate element by.
38+
*/
2839
public ByImage(File file) {
2940
loadLibrary();
3041
this.template = Imgcodecs.imread(file.getAbsolutePath(), Imgcodecs.IMREAD_UNCHANGED);
3142
}
3243

44+
/**
45+
* Constructor accepting image file.
46+
*
47+
* @param bytes image bytes to locate element by.
48+
*/
3349
public ByImage(byte[] bytes) {
3450
loadLibrary();
3551
this.template = Imgcodecs.imdecode(new MatOfByte(bytes), Imgcodecs.IMREAD_UNCHANGED);
3652
}
3753

54+
@Override
55+
public String toString() {
56+
return "ByImage: " + new Dimension(template.width(), template.height());
57+
}
58+
3859
@Override
3960
public List<WebElement> findElements(SearchContext context) {
4061
byte[] screenshotBytes = getScreenshot(context);
@@ -45,33 +66,61 @@ public List<WebElement> findElements(SearchContext context) {
4566
float threshold = 1 - AqualityServices.getConfiguration().getVisualizationConfiguration().getDefaultThreshold();
4667
Core.MinMaxLocResult minMaxLoc = Core.minMaxLoc(result);
4768

48-
if (minMaxLoc.maxVal < threshold) {
49-
AqualityServices.getLogger().warn(String.format("No elements found by image [%s]", template));
50-
return new ArrayList<>(0);
69+
int matchCounter = (result.width() - template.width() + 1) * (result.height() - template.height() + 1);
70+
List<Point> matchLocations = new ArrayList<>();
71+
while (matchCounter > 0 && minMaxLoc.maxVal >= threshold) {
72+
matchCounter--;
73+
Point matchLocation = minMaxLoc.maxLoc;
74+
matchLocations.add(matchLocation);
75+
Imgproc.rectangle(result, new Point(matchLocation.x, matchLocation.y), new Point(matchLocation.x + template.cols(),
76+
matchLocation.y + template.rows()), new Scalar(0, 0, 0), -1);
77+
minMaxLoc = Core.minMaxLoc(result);
5178
}
5279

53-
return getElementsOnPoint(minMaxLoc.maxLoc, context);
80+
return matchLocations.stream().map(matchLocation -> getElementOnPoint(matchLocation, context)).collect(Collectors.toList());
5481
}
5582

56-
private List<WebElement> getElementsOnPoint(Point matchLocation, SearchContext context) {
57-
int centerX = (int)(matchLocation.x + (template.width() / 2));
58-
int centerY = (int)(matchLocation.y + (template.height() / 2));
59-
60-
JavascriptExecutor js;
61-
if (!(context instanceof JavascriptExecutor)) {
62-
AqualityServices.getLogger().debug("Current search context doesn't support executing scripts. " +
63-
"Will take browser js executor instead");
64-
js = AqualityServices.getBrowser().getDriver();
83+
/**
84+
* Gets a single element on point (find by center coordinates, then select closest to matchLocation).
85+
*
86+
* @param matchLocation location of the upper-left point of the element.
87+
* @param context search context.
88+
* If the searchContext is Locatable (like WebElement), adjust coordinates to be absolute coordinates.
89+
* @return the closest found element.
90+
*/
91+
protected WebElement getElementOnPoint(Point matchLocation, SearchContext context) {
92+
if (context instanceof Locatable) {
93+
final org.openqa.selenium.Point point = ((Locatable) context).getCoordinates().onPage();
94+
matchLocation.x += point.getX();
95+
matchLocation.y += point.getY();
6596
}
66-
else {
67-
js = (JavascriptExecutor) context;
68-
}
69-
97+
int centerX = (int) (matchLocation.x + (template.width() / 2));
98+
int centerY = (int) (matchLocation.y + (template.height() / 2));
7099
//noinspection unchecked
71-
return (List<WebElement>) js.executeScript("return document.elementsFromPoint(arguments[0], arguments[1]);", centerX, centerY);
100+
List<WebElement> elements = (List<WebElement>) AqualityServices.getBrowser().executeScript("return document.elementsFromPoint(arguments[0], arguments[1]);", centerX, centerY);
101+
elements.sort(Comparator.comparingDouble(e -> distanceToPoint(matchLocation, e)));
102+
return elements.get(0);
103+
}
104+
105+
/**
106+
* Calculates distance from element to matching point.
107+
*
108+
* @param matchLocation matching point.
109+
* @param element target element.
110+
* @return distance in pixels.
111+
*/
112+
protected static double distanceToPoint(Point matchLocation, WebElement element) {
113+
org.openqa.selenium.Point elementLocation = element.getLocation();
114+
return Math.sqrt(Math.pow(matchLocation.x - elementLocation.x, 2) + Math.pow(matchLocation.y - elementLocation.y, 2));
72115
}
73116

74-
private byte[] getScreenshot(SearchContext context) {
117+
/**
118+
* Takes screenshot from searchContext if supported, or from browser.
119+
*
120+
* @param context search context for element location.
121+
* @return captured screenshot as byte array.
122+
*/
123+
protected byte[] getScreenshot(SearchContext context) {
75124
byte[] screenshotBytes;
76125

77126
if (!(context instanceof TakesScreenshot)) {

0 commit comments

Comments
 (0)