Skip to content

Commit 0d7e5c1

Browse files
fix(dashboard): restore Enter-navigation guard and workflow timeout assertion
a17d374 reverted the review fix that routes Enter in the URL field through the Go button plus the browserNavigating in-flight flag, the per-handler scoping of the button-state tests, and the MD022 blank line in palette.md, while changing the unit timeout cap to 450s but restoring the test that asserts a literal 300s. Reinstate the fixes and assert the cap by value. Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
1 parent 65e7de5 commit 0d7e5c1

4 files changed

Lines changed: 51 additions & 18 deletions

File tree

.Jules/palette.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,6 @@
3535
**Action:** Leg altijd het actieve element vast dat de overlay opent en herstel de focus bij sluiten, annuleren of succesvolle voltooiing.
3636

3737
## 2026-07-24 - [Context-Safe Async Button States]
38+
3839
**Learning:** For asynchronous action buttons in control views (e.g., Browser navigate, reload, or close actions), passing the calling element to JavaScript using `this` (e.g., `onclick="browserNavigate(this)"`) allows contextual loading feedback and disabled states to be applied directly. This prevents double-submits or redundant in-flight network requests during longer operations without expensive DOM element lookups.
3940
**Action:** Ensure asynchronous button handlers support passing and handling `this` to maintain context-safe button states and clear screen-reader feedback via `aria-busy`.

src/kater/web/dashboard.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,8 +1387,9 @@
13871387
<div class="browser-toolbar">
13881388
<input class="browser-url" id="browser-url" type="url"
13891389
placeholder="https://…" autocomplete="off" aria-label="Browser URL"
1390-
onkeydown="if(event.key==='Enter'){event.preventDefault();browserNavigate();}">
1391-
<button class="mini-btn interactive" type="button" onclick="browserNavigate(this)"
1390+
onkeydown="if(event.key==='Enter'){event.preventDefault();browserNavigate(document.getElementById('browser-go'));}">
1391+
<button class="mini-btn interactive" type="button" id="browser-go"
1392+
onclick="browserNavigate(this)"
13921393
aria-label="Navigate">Go</button>
13931394
<button class="mini-btn interactive" type="button" onclick="browserReload(this)"
13941395
aria-label="Reload page">Reload</button>
@@ -3892,6 +3893,7 @@ class ApiError extends Error {
38923893
let browserSelectedId = null;
38933894
let browserPollTimer = null;
38943895
let browserShotSeq = 0;
3896+
let browserNavigating = false;
38953897
const browserActionLog = new Map(); // session_id -> [{kind, ok, detail, ts}]
38963898
38973899
function stopBrowserPoll() {
@@ -4106,10 +4108,12 @@ class ApiError extends Error {
41064108
}
41074109
41084110
async function browserNavigate(btn) {
4111+
if (browserNavigating) return;
41094112
if (!browserSelectedId) { toast('no session selected', 'error'); return; }
41104113
const urlEl = document.getElementById('browser-url');
41114114
const url = urlEl ? urlEl.value.trim() : '';
41124115
if (!url) { toast('enter a URL', 'error'); return; }
4116+
browserNavigating = true;
41134117
if (btn) {
41144118
btn.disabled = true;
41154119
btn.setAttribute('aria-busy', 'true');
@@ -4130,6 +4134,7 @@ class ApiError extends Error {
41304134
pushBrowserLog(browserSelectedId, { kind: 'navigate', ok: false, detail: e.message || 'failed' });
41314135
toast('navigate: ' + (e.message || 'failed'), 'error');
41324136
} finally {
4137+
browserNavigating = false;
41334138
if (btn) {
41344139
btn.disabled = false;
41354140
btn.removeAttribute('aria-busy');

tests/test_ci_workflow_changes.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
only run inside GitHub Actions.
77
"""
88

9+
import re
910
from pathlib import Path
1011

1112
ROOT = Path(__file__).resolve().parent.parent
@@ -39,8 +40,11 @@ def test_ci_jobs_install_the_browser_extra() -> None:
3940
def test_unit_matrix_job_uses_kater_checkout_sha_and_longer_timeout() -> None:
4041
block = _job_block(CI.read_text(encoding="utf-8"), "unit", "integration")
4142
assert KATER_CHECKOUT_SHA in block
42-
assert "timeout 300s uv run pytest" in block
43-
assert "timeout 180s" not in block
43+
# The exact cap is tuned as the suite grows; assert only that the inner
44+
# guard exists and stays clear of the ~180s cap that used to trip on 3.14.
45+
match = re.search(r"timeout (\d+)s uv run pytest", block)
46+
assert match is not None
47+
assert int(match.group(1)) > 180
4448

4549

4650
def test_computer_acceptance_checks_out_kater_and_the_private_runtime() -> None:

tests/test_dashboard.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -791,18 +791,41 @@ def test_credentials_modal_focus_restoration_behavior_node(tmp_path):
791791
assert res["body"]["focusRestored"] is False
792792

793793

794-
def test_browser_view_buttons_use_context_loading_states():
794+
def _js_handler_block(html: str, signature: str) -> str:
795+
start = html.index(signature)
796+
end = html.index("\nasync function", start + len(signature))
797+
return html[start:end]
798+
799+
800+
@pytest.mark.parametrize(
801+
("handler", "loading_label", "idle_label"),
802+
[
803+
("closeBrowserSession", "Closing...", "Close"),
804+
("browserNavigate", "Go...", "Go"),
805+
("browserReload", "Reloading...", "Reload"),
806+
],
807+
)
808+
def test_browser_view_buttons_use_context_loading_states(handler, loading_label, idle_label):
795809
html = render_dashboard()
796-
# Check that Go, Reload, Close buttons pass 'this'
797-
assert 'onclick="browserNavigate(this)"' in html
798-
assert 'onclick="browserReload(this)"' in html
799-
assert 'onclick="closeBrowserSession(this)"' in html
800-
# Check that the JavaScript functions contain btn context handling
801-
assert "async function browserNavigate(btn)" in html
802-
assert "async function browserReload(btn)" in html
803-
assert "async function closeBrowserSession(btn)" in html
804-
# Check busy attribute setting and disabling
805-
assert "btn.setAttribute('aria-busy', 'true')" in html
806-
assert "btn.removeAttribute('aria-busy')" in html
807-
assert "btn.disabled = true" in html
808-
assert "btn.disabled = false" in html
810+
assert f'onclick="{handler}(this)"' in html
811+
812+
block = _js_handler_block(html, f"async function {handler}(btn)")
813+
assert "btn.disabled = true" in block
814+
assert "btn.setAttribute('aria-busy', 'true')" in block
815+
assert f"btn.textContent = '{loading_label}'" in block
816+
817+
restore = block[block.index("} finally {") :]
818+
assert "btn.disabled = false" in restore
819+
assert "btn.removeAttribute('aria-busy')" in restore
820+
assert f"btn.textContent = '{idle_label}'" in restore
821+
822+
823+
def test_browser_url_enter_serializes_navigation_through_go_button():
824+
html = render_dashboard()
825+
assert 'id="browser-go"' in html
826+
assert "browserNavigate(document.getElementById('browser-go'))" in html
827+
828+
block = _js_handler_block(html, "async function browserNavigate(btn)")
829+
assert "if (browserNavigating) return;" in block
830+
assert "browserNavigating = true;" in block
831+
assert "browserNavigating = false;" in block[block.index("} finally {") :]

0 commit comments

Comments
 (0)