@@ -71,6 +71,86 @@ protected function locate_path($path) {
7171 return 0 !== strpos ($ path , 'http ' ) ? $ starturl . ltrim ($ path , '/ ' ) : $ path ;
7272 }
7373
74+ /**
75+ * Get the accessible name of a NodeElement.
76+ *
77+ * @param NodeElement $element The element to get the accessible name for.
78+ * @return string|null
79+ */
80+ protected function get_accessible_name (NodeElement $ element ): ?string {
81+ // Check the element's aria-labelledby attribute.
82+ if ($ element ->hasAttribute ('aria-labelledby ' )) {
83+ $ labelledby = $ element ->getAttribute ('aria-labelledby ' );
84+ $ ids = explode (' ' , trim ($ labelledby ));
85+ $ texts = [];
86+ foreach ($ ids as $ id ) {
87+ if ($ ref = $ this ->getSession ()->getPage ()->find ('css ' , "# $ id " )) {
88+ $ texts [] = trim ($ ref ->getText ());
89+ }
90+ }
91+ $ calculatedlabel = trim (implode (' ' , $ texts ));
92+ if ($ calculatedlabel ) {
93+ return $ calculatedlabel ;
94+ } else {
95+ // Warn developers about empty aria-label attributes to fix potential accessibility issues.
96+ debugging ("No accessible name determined from aria-labelledby attribute: " . $ element ->getOuterHtml ());
97+ }
98+ }
99+
100+ // Otherwise, check the element's aria-label attribute.
101+ if ($ element ->hasAttribute ('aria-label ' )) {
102+ $ arialabel = $ element ->getAttribute ('aria-label ' ) ?? '' ;
103+ $ arialabel = trim ($ arialabel );
104+ if ($ arialabel ) {
105+ return $ arialabel ;
106+ } else {
107+ // Warn developers about empty aria-label attributes to fix potential accessibility issues.
108+ debugging ("Element has empty aria-label attribute: " . $ element ->getOuterHtml ());
109+ }
110+ }
111+
112+ // Or check for a <label> tag associated with it.
113+ if ($ id = $ element ->getAttribute ('id ' )) {
114+ if ($ labeltag = $ this ->getSession ()->getPage ()->find ('css ' , "label[for=' $ id'] " )) {
115+ return $ labeltag ->getText ();
116+ }
117+ }
118+
119+ // For images, check the element's alt attribute.
120+ if ($ element ->getTagName () === 'img ' && $ element ->hasAttribute ('alt ' )) {
121+ $ alttext = $ element ->getAttribute ('alt ' ) ?? '' ;
122+ return trim ($ alttext );
123+ }
124+
125+ // Try to fall back to the element text.
126+ $ text = $ element ->getText () ?? '' ;
127+ $ text = trim ($ text );
128+ if ($ text ) {
129+ return $ text ;
130+ }
131+
132+ // Or fall back to the element value (e.g. buttons, links).
133+ $ value = $ element ->getValue () ?? '' ;
134+ $ value = trim ($ value );
135+ if ($ value ) {
136+ return $ value ;
137+ }
138+
139+ // Or check for a title attribute in the element as a last resort.
140+ if ($ element ->getAttribute ('title ' )) {
141+ $ title = $ element ->getAttribute ('title ' ) ?? '' ;
142+ $ title = trim ($ title );
143+ if ($ title ) {
144+ // Using the title attribute for an element's accessible name is discourage. Warn developers know about this.
145+ debugging ("Using the title attribute as accessible name is discouraged: " . $ element ->getOuterHtml ());
146+ }
147+ return $ title ;
148+ }
149+
150+ // An accessible name was not found.
151+ return null ;
152+ }
153+
74154 /**
75155 * Returns the first matching element.
76156 *
@@ -88,9 +168,42 @@ protected function find($selector, $locator, $exception = false, $node = false,
88168 return $ locator ;
89169 }
90170
91- // Returns the first match.
92171 $ items = $ this ->find_all ($ selector , $ locator , $ exception , $ node , $ timeout );
93- return count ($ items ) ? reset ($ items ) : null ;
172+
173+ // Determine the best item to return when multiple items were found.
174+ $ itemsfound = count ($ items );
175+ [
176+ 'selector ' => $ selector ,
177+ 'locator ' => $ locator ,
178+ ] = $ this ->normalise_selector ($ selector , $ locator , $ node ?: $ this ->getSession ()->getPage ());
179+
180+ if ($ itemsfound > 1 && !in_array ($ selector , ['css ' , 'xpath ' ])) {
181+ /** @var NodeElement $item */
182+ foreach ($ items as $ item ) {
183+ // Determine the found item's accessible name.
184+ $ accessiblename = $ this ->get_accessible_name ($ item );
185+ if (empty ($ accessiblename )) {
186+ debugging ("No accessible name found for element: " . $ item ->getOuterHtml ());
187+ continue ;
188+ }
189+
190+ if ($ accessiblename === $ locator ) {
191+ // Return the first item that exactly matches the locator string.
192+ return $ item ;
193+ } else {
194+ if ($ selector === 'named_partial ' && is_array ($ locator )) {
195+ $ locator = $ locator [1 ];
196+ }
197+ if (core_text::strpos ($ locator , $ accessiblename ) === 0 ) {
198+ // If there's no exact match, return the first item that starts with the locator string.
199+ return $ item ;
200+ }
201+ }
202+ }
203+ }
204+
205+ // Fall back to the first item found or return null if there were no items found.
206+ return $ itemsfound ? reset ($ items ) : null ;
94207 }
95208
96209 /**
0 commit comments