Skip to content

Commit 2f847ce

Browse files
fix: Improve screenshot highlighting and capture completeness
- Use outline + box-shadow for element highlighting (not clipped by Shadow DOM) - Add z-index and position adjustments for visibility above siblings - Handle ha-integration-list-item, role=listitem, role=option elements - Add capture_name support to add_another_entity() function - Skip fill_textbox when field already contains target value - Capture all solar forecast selections (4 total) - Capture all grid price entity selections (2 import, 2 export) Addresses feedback: - Screenshots now captured for all entity selections - 'Add entity' button clicks are now captured - Highlighting surrounds elements properly via outline - Pre-filled name fields are skipped
1 parent 423073d commit 2f847ce

File tree

1 file changed

+104
-23
lines changed

1 file changed

+104
-23
lines changed

tests/guides/sigenergy/run_guide.py

Lines changed: 104 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)