""" Sprint 21 Tests: Send button polish — hidden until content, pop-in animation, icon-only circle design. """ import re import urllib.request BASE = "http://127.0.0.1:8788" def get_text(path): with urllib.request.urlopen(BASE + path, timeout=10) as r: return r.read().decode(), r.status # ── index.html ──────────────────────────────────────────────────────────── def test_send_button_present(): """btnSend must still exist in the DOM.""" html, status = get_text("/") assert status == 200 assert 'id="btnSend"' in html def test_send_button_hidden_by_default(): """btnSend must start hidden (display:none) — only shown when there is content.""" html, _ = get_text("/") btn_match = re.search(r'id="btnSend"[^>]*>', html) assert btn_match, "btnSend element not found" assert 'display:none' in btn_match.group(0) def test_send_button_no_text_label(): """Send button must be icon-only — no visible 'Send' text label.""" html, _ = get_text("/") # Find the full button element (from opening tag to closing tag) btn_open_end = html.find('>', html.find('id="btnSend"')) + 1 btn_end = html.find('', btn_open_end) + len('') btn_inner = html[btn_open_end:btn_end] # Strip SVG content and any remaining tags; check visible text no_svg = re.sub(r']*>.*?', '', btn_inner, flags=re.DOTALL) visible_text = re.sub(r'<[^>]+>', '', no_svg).strip() assert visible_text == '', f"Send button has visible text: {visible_text!r}" def test_send_button_has_svg_icon(): """Send button must have an SVG icon.""" html, _ = get_text("/") btn_start = html.find('id="btnSend"') btn_end = html.find('', btn_start) + len('') btn_html = html[btn_start:btn_end] assert ']*>', html) assert btn_match assert 'title=' in btn_match.group(0) def test_send_button_svg_arrow_up(): """Send button SVG should use an upward arrow (line + polyline or path).""" html, _ = get_text("/") btn_start = html.find('id="btnSend"') btn_end = html.find('', btn_start) + len('') btn_html = html[btn_start:btn_end] # Must have some directional shape element has_shape = ('' directly (forward search) to avoid hitting # an earlier keyframe when multiple are defined on the same line. kf_start = css.find('@keyframes ' + name) assert kf_start != -1, f"@keyframes {name} not found in CSS" depth = 0 kf_end = kf_start for i, ch in enumerate(css[kf_start:], kf_start): if ch == '{': depth += 1 elif ch == '}': depth -= 1 if depth == 0: kf_end = i break return css[kf_start:kf_end] def test_send_pop_in_uses_scale(): """send-pop-in keyframe must animate from a scaled-down state.""" css, _ = get_text("/static/style.css") kf_rule = _extract_keyframe(css, 'send-pop-in') assert 'scale' in kf_rule def test_send_pop_in_uses_opacity(): """send-pop-in keyframe must fade in (opacity transition).""" css, _ = get_text("/static/style.css") kf_rule = _extract_keyframe(css, 'send-pop-in') assert 'opacity' in kf_rule def test_send_btn_mobile_override_no_padding(): """Mobile override for send-btn must not add text padding (keeps circle shape).""" css, _ = get_text("/static/style.css") # Find the @media block media_idx = css.find('@media') send_mobile_idx = css.find('.send-btn', media_idx) if send_mobile_idx == -1: return # No mobile override, fine brace_open = css.find('{', send_mobile_idx) brace_close = css.find('}', brace_open) rule = css[brace_open:brace_close] assert 'padding:' not in rule and 'font-size' not in rule # ── ui.js ───────────────────────────────────────────────────────────────── def test_ui_js_update_send_btn_function(): """ui.js must define updateSendBtn() function.""" js, status = get_text("/static/ui.js") assert status == 200 assert 'function updateSendBtn' in js def test_update_send_btn_checks_content(): """updateSendBtn must check textarea value length.""" js, _ = get_text("/static/ui.js") fn_idx = js.find('function updateSendBtn') fn_end = js.find('\n}', fn_idx) + 2 fn_body = js[fn_idx:fn_end] assert 'msg' in fn_body assert '.value' in fn_body assert '.length' in fn_body or '.trim()' in fn_body def test_update_send_btn_checks_pending_files(): """updateSendBtn must also show send button when files are attached.""" js, _ = get_text("/static/ui.js") fn_idx = js.find('function updateSendBtn') fn_end = js.find('\n}', fn_idx) + 2 fn_body = js[fn_idx:fn_end] assert 'pendingFiles' in fn_body def test_update_send_btn_uses_visible_class(): """updateSendBtn must add .visible class to trigger the pop-in animation.""" js, _ = get_text("/static/ui.js") fn_idx = js.find('function updateSendBtn') fn_end = js.find('\n}', fn_idx) + 2 fn_body = js[fn_idx:fn_end] assert 'visible' in fn_body def test_update_send_btn_uses_display_none(): """updateSendBtn must hide the button with display:none when no content.""" js, _ = get_text("/static/ui.js") fn_idx = js.find('function updateSendBtn') fn_end = js.find('\n}', fn_idx) + 2 fn_body = js[fn_idx:fn_end] assert 'display' in fn_body assert 'none' in fn_body def test_set_busy_calls_update_send_btn(): """setBusy must call updateSendBtn() so button hides while agent is responding.""" js, _ = get_text("/static/ui.js") busy_idx = js.find('function setBusy') busy_end = js.find('\n}', busy_idx) + 2 busy_body = js[busy_idx:busy_end] assert 'updateSendBtn' in busy_body def test_render_tray_calls_update_send_btn(): """renderTray must call updateSendBtn() so button appears when files are attached.""" js, _ = get_text("/static/ui.js") tray_idx = js.find('function renderTray') tray_end = js.find('\n}', tray_idx) + 2 tray_body = js[tray_idx:tray_end] assert 'updateSendBtn' in tray_body # ── boot.js ────────────────────────────────────────────────────────────── def test_boot_js_input_calls_update_send_btn(): """boot.js input event listener must call updateSendBtn().""" js, status = get_text("/static/boot.js") assert status == 200 assert 'updateSendBtn' in js # ── messages.js ─────────────────────────────────────────────────────────── def test_auto_resize_calls_update_send_btn(): """autoResize() must call updateSendBtn() so button hides after send clears textarea.""" js, status = get_text("/static/messages.js") assert status == 200 assert 'updateSendBtn' in js # ── Regression: existing behaviour unchanged ────────────────────────────── def test_send_button_still_has_send_btn_class(): """btnSend must still carry class='send-btn' for CSS targeting.""" html, _ = get_text("/") assert 'class="send-btn"' in html def test_ui_js_set_busy_still_disables_btn(): """setBusy must still set btnSend.disabled (not just hide it).""" js, _ = get_text("/static/ui.js") busy_idx = js.find('function setBusy') busy_end = js.find('\n}', busy_idx) + 2 busy_body = js[busy_idx:busy_end] assert "btnSend" in busy_body assert 'disabled' in busy_body def test_index_html_attach_button_unchanged(): """btnAttach must still be present (no regression).""" html, _ = get_text("/") assert 'id="btnAttach"' in html def test_send_function_still_exists(): """send() function must still be defined in messages.js.""" js, _ = get_text("/static/messages.js") assert 'async function send()' in js