@@ -171,18 +171,29 @@ def _show_click_indicator(self, locator: Any) -> None:
171171 // If the element is very small, look for a better parent
172172 if (rect.width < minSize || rect.height < minSize) {
173173 // Look for common clickable parent patterns
174- const clickableParent = el.closest('button, [role="button"], [role="option"], [role="listitem"], a, ha-list-item, ha-combo-box-item, mwc-list-item, md-item, ha-button, ha-icon-button, .mdc-text-field, ha-textfield, input, select, ha-select');
174+ const clickableParent = el.closest('button, [role="button"], [role="option"], [role="listitem"], a, ha-list-item, ha-combo-box-item, mwc-list-item, md-item, ha-button, ha-icon-button, .mdc-text-field, ha-textfield, input, select, ha-select, ha-integration-list-item ');
175175 if (clickableParent) {
176176 target = clickableParent;
177177 }
178178 }
179179
180- // Always prefer md-item if we're inside one (Material Design list items)
180+ // Always prefer ha-integration-list-item, md-item, or similar list items
181+ const integrationItem = el.closest('ha-integration-list-item');
182+ if (integrationItem) {
183+ target = integrationItem;
184+ }
185+
181186 const mdItem = el.closest('md-item');
182187 if (mdItem) {
183188 target = mdItem;
184189 }
185190
191+ // For list items and options, prefer the item element itself
192+ const listItem = el.closest('[role="listitem"], [role="option"]');
193+ if (listItem) {
194+ target = listItem;
195+ }
196+
186197 // Also check if we're inside a form field and should highlight the field container
187198 const textField = el.closest('.mdc-text-field, ha-textfield, ha-select, ha-combo-box');
188199 if (textField) {
@@ -194,10 +205,26 @@ def _show_click_indicator(self, locator: Any) -> None:
194205 }
195206 }
196207
208+ // Apply box-shadow indicator
197209 target.setAttribute('data-click-target', 'true');
198210 target.dataset.originalBoxShadow = target.style.boxShadow || '';
199- target.style.boxShadow = boxShadow;
200- target.style.outline = 'none';
211+ target.dataset.originalOutline = target.style.outline || '';
212+ target.dataset.originalPosition = target.style.position || '';
213+ target.dataset.originalZIndex = target.style.zIndex || '';
214+ target.dataset.originalOverflow = target.style.overflow || '';
215+
216+ // Use outline instead of box-shadow for better visibility on list items
217+ // Outline is not clipped by parent overflow:hidden
218+ target.style.outline = '3px solid rgba(255, 0, 0, 0.9)';
219+ target.style.outlineOffset = '2px';
220+ target.style.boxShadow = '0 0 15px 5px rgba(255, 0, 0, 0.4)';
221+
222+ // Ensure the element is visible above siblings
223+ const currentPosition = getComputedStyle(target).position;
224+ if (currentPosition === 'static') {
225+ target.style.position = 'relative';
226+ }
227+ target.style.zIndex = '9999';
201228 }""" ,
202229 box_shadow ,
203230 )
@@ -259,31 +286,43 @@ def _remove_click_indicator(self) -> None:
259286 """Remove click indicator from any marked elements and restore original styles."""
260287 self .page .evaluate ("""
261288 // Remove the data attribute and restore original styles from any marked elements
262- const marked = document.querySelectorAll('[data-click-target]');
263- for (const el of marked) {
289+ function restoreElement(el) {
264290 el.removeAttribute('data-click-target');
265- // Restore original box-shadow if it was saved
291+ // Restore all saved styles
266292 if (el.dataset.originalBoxShadow !== undefined) {
267293 el.style.boxShadow = el.dataset.originalBoxShadow;
268294 delete el.dataset.originalBoxShadow;
269295 } else {
270296 el.style.boxShadow = '';
271297 }
272- el.style.outline = '';
298+ if (el.dataset.originalOutline !== undefined) {
299+ el.style.outline = el.dataset.originalOutline;
300+ el.style.outlineOffset = '';
301+ delete el.dataset.originalOutline;
302+ } else {
303+ el.style.outline = '';
304+ el.style.outlineOffset = '';
305+ }
306+ if (el.dataset.originalPosition !== undefined) {
307+ el.style.position = el.dataset.originalPosition;
308+ delete el.dataset.originalPosition;
309+ }
310+ if (el.dataset.originalZIndex !== undefined) {
311+ el.style.zIndex = el.dataset.originalZIndex;
312+ delete el.dataset.originalZIndex;
313+ }
314+ }
315+
316+ const marked = document.querySelectorAll('[data-click-target]');
317+ for (const el of marked) {
318+ restoreElement(el);
273319 }
274320
275321 // Also traverse shadow roots to find any marked elements there
276322 function walkShadowRoots(root) {
277323 root.querySelectorAll('*').forEach(el => {
278324 if (el.hasAttribute('data-click-target')) {
279- el.removeAttribute('data-click-target');
280- if (el.dataset.originalBoxShadow !== undefined) {
281- el.style.boxShadow = el.dataset.originalBoxShadow;
282- delete el.dataset.originalBoxShadow;
283- } else {
284- el.style.boxShadow = '';
285- }
286- el.style.outline = '';
325+ restoreElement(el);
287326 }
288327 if (el.shadowRoot) {
289328 walkShadowRoots(el.shadowRoot);
@@ -375,10 +414,17 @@ def click_button(self, name: str, *, timeout: int = DEFAULT_TIMEOUT, capture_nam
375414 def fill_textbox (self , name : str , value : str , * , capture_name : str | None = None ) -> None :
376415 """Fill a textbox by its accessible name.
377416
417+ If the textbox already contains the target value, skips filling.
378418 If capture_name is provided, captures before (with indicator) and after (filled).
379419 """
380420 textbox = self .page .get_by_role ("textbox" , name = name )
381421
422+ # Check if field is already filled with the target value
423+ current_value = textbox .input_value (timeout = DEFAULT_TIMEOUT )
424+ if current_value == value :
425+ # Field is already correctly filled, skip
426+ return
427+
382428 if capture_name :
383429 self ._scroll_into_view (textbox )
384430 self .capture (f"{ capture_name } _before" )
@@ -505,7 +551,9 @@ def select_entity(
505551 if capture_name :
506552 self .capture (f"{ capture_name } _result" )
507553
508- def add_another_entity (self , field_label : str , search_term : str , entity_name : str ) -> None :
554+ def add_another_entity (
555+ self , field_label : str , search_term : str , entity_name : str , * , capture_name : str | None = None
556+ ) -> None :
509557 """Add another entity to a multi-select field.
510558
511559 For fields that accept multiple entities, an "Add entity" button appears after first selection.
@@ -516,29 +564,52 @@ def add_another_entity(self, field_label: str, search_term: str, entity_name: st
516564
517565 # Click the "Add entity" button within the selector
518566 add_btn = selector .get_by_role ("button" , name = "Add entity" )
567+
568+ if capture_name :
569+ self ._scroll_into_view (add_btn )
570+ self .capture (f"{ capture_name } _before" )
571+ self ._capture_with_indicator (f"{ capture_name } _add_btn" , add_btn )
572+
519573 add_btn .click (timeout = DEFAULT_TIMEOUT )
520574 self .page .wait_for_timeout (MEDIUM_WAIT * 1000 )
521575
522576 # Wait for a dialog to appear - HA uses "Select option" as the dialog name
523577 dialog = self .page .get_by_role ("dialog" , name = "Select option" )
524578 dialog .wait_for (timeout = DEFAULT_TIMEOUT )
525579
580+ if capture_name :
581+ self .capture (f"{ capture_name } _dialog" )
582+
526583 # Fill the search textbox within the dialog
527584 search_input = dialog .get_by_role ("textbox" , name = "Search" )
528585 search_input .fill (search_term )
529586 self .page .wait_for_timeout (1000 ) # Wait 1s for search results to populate
530587
588+ if capture_name :
589+ self .capture (f"{ capture_name } _search" )
590+
531591 # Click the matching item in the dialog's results
532592 # HA uses different selectors: listitem in some dialogs, ha-combo-box-item in others
533593 try :
534594 result_item = dialog .get_by_role ("listitem" ).filter (has_text = entity_name ).first
595+ if capture_name :
596+ self ._scroll_into_view (result_item )
597+ self .capture (f"{ capture_name } _select_before" )
598+ self ._capture_with_indicator (f"{ capture_name } _select" , result_item )
535599 result_item .click (timeout = 1000 )
536600 except Exception :
537601 # Fall back to ha-combo-box-item
538602 result_item = dialog .locator ("ha-combo-box-item" ).filter (has_text = entity_name ).first
603+ if capture_name :
604+ self ._scroll_into_view (result_item )
605+ self .capture (f"{ capture_name } _select_before" )
606+ self ._capture_with_indicator (f"{ capture_name } _select" , result_item )
539607 result_item .click (timeout = DEFAULT_TIMEOUT )
540608 self .page .wait_for_timeout (SHORT_WAIT * 1000 )
541609
610+ if capture_name :
611+ self .capture (f"{ capture_name } _result" )
612+
542613 def close_network_dialog (self , * , capture_name : str | None = None ) -> None :
543614 """Close the network creation dialog (has 'Skip and finish' button)."""
544615 button = self .page .get_by_role ("button" , name = "Skip and finish" )
@@ -748,10 +819,16 @@ def add_solar(guide: SigenergyGuide) -> None:
748819 "Forecast" , "east solar today" , "East solar production forecast" , capture_name = "solar_forecast"
749820 )
750821
751- # Add the other three array forecasts (no extra captures - too many screenshots)
752- guide .add_another_entity ("Forecast" , "north solar today" , "North solar production forecast" )
753- guide .add_another_entity ("Forecast" , "south solar today" , "South solar prediction forecast" )
754- guide .add_another_entity ("Forecast" , "west solar today" , "West solar production forecast" )
822+ # Add the other three array forecasts
823+ guide .add_another_entity (
824+ "Forecast" , "north solar today" , "North solar production forecast" , capture_name = "solar_forecast2"
825+ )
826+ guide .add_another_entity (
827+ "Forecast" , "south solar today" , "South solar prediction forecast" , capture_name = "solar_forecast3"
828+ )
829+ guide .add_another_entity (
830+ "Forecast" , "west solar today" , "West solar production forecast" , capture_name = "solar_forecast4"
831+ )
755832
756833 guide .click_button ("Submit" , capture_name = "solar_submit" )
757834 guide .close_element_dialog (capture_name = "solar_close" )
@@ -774,13 +851,17 @@ def add_grid(guide: SigenergyGuide) -> None:
774851 guide .select_entity (
775852 "Import Price" , "general price" , "Home - General Price" , capture_name = "grid_import_price"
776853 )
777- guide .add_another_entity ("Import Price" , "general forecast" , "Home - General Forecast" )
854+ guide .add_another_entity (
855+ "Import Price" , "general forecast" , "Home - General Forecast" , capture_name = "grid_import_price2"
856+ )
778857
779858 # Export price
780859 guide .select_entity (
781860 "Export Price" , "feed in price" , "Home - Feed In Price" , capture_name = "grid_export_price"
782861 )
783- guide .add_another_entity ("Export Price" , "feed in forecast" , "Home - Feed In Forecast" )
862+ guide .add_another_entity (
863+ "Export Price" , "feed in forecast" , "Home - Feed In Forecast" , capture_name = "grid_export_price2"
864+ )
784865
785866 # Submit step 1 → moves to step 2 (values) for limit spinbuttons
786867 guide .click_button ("Submit" , capture_name = "grid_step1_submit" )
0 commit comments