@@ -57,10 +57,7 @@ def _find_cookie(page: Page, name: str) -> Optional[Dict[str, Any]]:
5757
5858# Iframe tests require X_FRAME_OPTIONS=ALLOW-ALL to function properly
5959# Skip marker for when this requirement is not met
60- _requires_allow_all = pytest .mark .skipif (
61- settings .x_frame_options != "ALLOW-ALL" ,
62- reason = "Iframe embedding tests require X_FRAME_OPTIONS=ALLOW-ALL environment variable"
63- )
60+ _requires_allow_all = pytest .mark .skipif (settings .x_frame_options != "ALLOW-ALL" , reason = "Iframe embedding tests require X_FRAME_OPTIONS=ALLOW-ALL environment variable" )
6461
6562
6663@pytest .fixture
@@ -203,13 +200,11 @@ def test_x_frame_options_deny_blocks_iframe(self, page: Page, base_url: str):
203200
204201 _ensure_admin_logged_in (page , base_url )
205202 admin_url = f"{ base_url } /admin/"
206- page .set_content (
207- f"""<!DOCTYPE html>
203+ page .set_content (f"""<!DOCTYPE html>
208204<html><head><title>deny test</title></head>
209205<body>
210206<iframe id="admin-frame" src="{ admin_url } " style="width:100%;height:100vh;border:none"></iframe>
211- </body></html>"""
212- )
207+ </body></html>""" )
213208
214209 frame = page .frame_locator ("#admin-frame" )
215210 try :
@@ -376,15 +371,13 @@ def _strip_headers(route: Route) -> None:
376371 page .route (admin_pattern , _strip_headers )
377372
378373 admin_url = f"{ base_url } /admin/?ui_hide=metrics"
379- page .set_content (
380- f"""<!DOCTYPE html>
374+ page .set_content (f"""<!DOCTYPE html>
381375<html><head><title>ui_hide iframe test</title></head>
382376<body>
383377<iframe id="admin-frame" src="{ admin_url } " style="width:100%;height:100vh;border:none"
384378 sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals">
385379</iframe>
386- </body></html>"""
387- )
380+ </body></html>""" )
388381
389382 frame = page .frame_locator ("#admin-frame" )
390383 try :
@@ -440,16 +433,14 @@ class TestCORSInIframeContext:
440433 def test_same_origin_fetch_from_iframe (self , page : Page , iframe_host ):
441434 """fetch('/health') from inside the iframe succeeds (same-origin)."""
442435 frame = iframe_host
443- result = frame .locator ("body" ).evaluate (
444- """async () => {
436+ result = frame .locator ("body" ).evaluate ("""async () => {
445437 try {
446438 const resp = await fetch('/health');
447439 return { status: resp.status, ok: resp.ok };
448440 } catch (e) {
449441 return { error: e.message };
450442 }
451- }"""
452- )
443+ }""" )
453444 assert "error" not in result , f"Same-origin fetch from iframe failed: { result .get ('error' )} "
454445 assert result ["status" ] == 200 , f"Expected 200 from /health, got { result ['status' ]} "
455446
@@ -595,10 +586,7 @@ def _reload_iframe(self, page: Page, iframe_host: FrameLocator) -> None:
595586 iframe_host .locator ('[data-testid="servers-tab"]' ).wait_for (state = "visible" , timeout = 30000 )
596587
597588 # Wait for JS initialization (Admin object + HTMX) - also critical
598- iframe_frame .wait_for_function (
599- "typeof window.Admin !== 'undefined' && typeof window.Admin.showTab === 'function' && typeof window.htmx !== 'undefined'" ,
600- timeout = 15000
601- )
589+ iframe_frame .wait_for_function ("typeof window.Admin !== 'undefined' && typeof window.Admin.showTab === 'function' && typeof window.htmx !== 'undefined'" , timeout = 15000 )
602590
603591 # Give HTMX a moment to settle after page load
604592 page .wait_for_timeout (500 )
@@ -619,8 +607,7 @@ def _search_gateway_in_iframe(self, page: Page, frame: FrameLocator, gw_name: st
619607 search_input .fill (gw_name )
620608 # Wait for the HTMX indicator to disappear (search response settled)
621609 page .wait_for_function (
622- "() => !document.querySelector('#admin-frame')?.contentDocument"
623- "?.querySelector('#gateways-loading.htmx-request')" ,
610+ "() => !document.querySelector('#admin-frame')?.contentDocument" "?.querySelector('#gateways-loading.htmx-request')" ,
624611 timeout = 10000 ,
625612 )
626613 except (PlaywrightTimeoutError , Exception ):
@@ -649,9 +636,7 @@ def _navigate_to_gateways_tab(self, page: Page, frame: FrameLocator) -> None:
649636 panel .wait_for (state = "visible" , timeout = 5000 )
650637 except PlaywrightTimeoutError :
651638 try :
652- iframe_frame .evaluate (
653- "() => { if (typeof window.Admin !== 'undefined' && typeof window.Admin.showTab === 'function') window.Admin.showTab('gateways'); }"
654- )
639+ iframe_frame .evaluate ("() => { if (typeof window.Admin !== 'undefined' && typeof window.Admin.showTab === 'function') window.Admin.showTab('gateways'); }" )
655640 except Exception :
656641 pass
657642 panel .wait_for (state = "visible" , timeout = 15000 )
@@ -665,7 +650,7 @@ def _navigate_to_gateways_tab(self, page: Page, frame: FrameLocator) -> None:
665650 // Either has data rows or shows empty state
666651 return tbody.children.length > 0 || tbody.textContent.includes('No');
667652 }""" ,
668- timeout = 15000
653+ timeout = 15000 ,
669654 )
670655 except PlaywrightTimeoutError :
671656 pass # Table may still be loading; proceed with what is available
@@ -716,8 +701,7 @@ def test_add_gateway_via_iframe_form(self, page: Page, iframe_host: FrameLocator
716701 # Override transport to HTTP so _initialize_gateway skips the external
717702 # connection check (only SSE/StreamableHTTP try to connect).
718703 iframe_frame = page .frames [- 1 ]
719- iframe_frame .evaluate (
720- """() => {
704+ iframe_frame .evaluate ("""() => {
721705 const form = document.getElementById('add-gateway-form');
722706 if (!form) return;
723707 const select = form.querySelector('select[name="transport"]');
@@ -729,8 +713,7 @@ def test_add_gateway_via_iframe_form(self, page: Page, iframe_host: FrameLocator
729713 select.appendChild(opt);
730714 }
731715 select.value = 'HTTP';
732- }"""
733- )
716+ }""" )
734717
735718 # Submit and wait for the POST response from the admin form handler.
736719 # The admin form uses JS fetch() so the page does NOT navigate.
@@ -846,7 +829,7 @@ def test_edit_gateway_via_iframe_modal(self, page: Page, iframe_host: FrameLocat
846829
847830 # Click the edit button for this specific gateway
848831 # tojson_attr encodes the ID with double quotes → onclick='Admin.editGateway("id")'
849- edit_btn = frame .locator (f' button[onclick*=\ ' editGateway("{ gw_id } ") \' ]' ).first
832+ edit_btn = frame .locator (f" button[onclick*='editGateway(\ "{ gw_id } \" )']" ).first
850833 try :
851834 edit_btn .wait_for (state = "visible" , timeout = 15000 )
852835 except PlaywrightTimeoutError :
@@ -896,9 +879,13 @@ def test_delete_gateway_via_iframe(self, page: Page, iframe_host: FrameLocator,
896879 # Auto-accept confirmation dialogs on the HOST page
897880 page .on ("dialog" , lambda d : d .accept ())
898881
899- # Click delete button (use .first for responsive layouts)
882+ # Open the actions dropdown (PR #3802 moved buttons into Alpine.js menu)
883+ gw_row = frame .locator (f'tr[id="gateway-row-{ gw_id } "]' ).first
884+ gw_row .wait_for (state = "attached" , timeout = 10000 )
885+ gw_row .scroll_into_view_if_needed ()
886+ gw_row .locator ("button[aria-expanded]" ).click ()
887+ gw_row .locator ('[role="menu"]' ).wait_for (state = "visible" , timeout = 5000 )
900888 delete_btn = frame .locator (f'form[action*="/gateways/{ gw_id } /delete"] button[type="submit"]' ).first
901- delete_btn .wait_for (state = "visible" , timeout = 5000 )
902889
903890 with page .expect_response (lambda r : f"/gateways/{ gw_id } /delete" in r .url , timeout = 15000 ):
904891 delete_btn .click ()
@@ -926,9 +913,13 @@ def test_toggle_gateway_state_via_iframe(self, page: Page, iframe_host: FrameLoc
926913 # Search by name so the gateway is visible regardless of pagination
927914 self ._search_gateway_in_iframe (page , frame , gw_name )
928915
929- # Click the deactivate/toggle button for this gateway
916+ # Open the actions dropdown (PR #3802 moved buttons into Alpine.js menu)
917+ gw_row = frame .locator (f'tr[id="gateway-row-{ gw_id } "]' ).first
918+ gw_row .wait_for (state = "attached" , timeout = 10000 )
919+ gw_row .scroll_into_view_if_needed ()
920+ gw_row .locator ("button[aria-expanded]" ).click ()
921+ gw_row .locator ('[role="menu"]' ).wait_for (state = "visible" , timeout = 5000 )
930922 toggle_btn = frame .locator (f'form[action*="/gateways/{ gw_id } /state"] button[type="submit"]' )
931- toggle_btn .first .wait_for (state = "visible" , timeout = 5000 )
932923
933924 with page .expect_response (lambda r : f"/gateways/{ gw_id } /state" in r .url , timeout = 15000 ):
934925 toggle_btn .first .click ()
0 commit comments