11package aquality .selenium .elements .interfaces ;
22
33import aquality .selenium .browser .AqualityServices ;
4- import org .opencv .core .Core ;
5- import org .opencv .core .Mat ;
6- import org .opencv .core .MatOfByte ;
74import org .opencv .core .Point ;
5+ import org .opencv .core .*;
86import org .opencv .imgcodecs .Imgcodecs ;
97import org .opencv .imgproc .Imgproc ;
108import org .openqa .selenium .*;
9+ import org .openqa .selenium .interactions .Locatable ;
1110
1211import java .io .File ;
1312import java .util .ArrayList ;
13+ import java .util .Comparator ;
1414import 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+ */
1622public 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