tests/test_exec_links_browser.py
Rama master · réplica de lectura del repositorio original.
"""A link Exec names is a link Wai can tap (WebKit / playwright).
The regression, 2026-09-19: the biweekly EI nudge said "open the login link"
and the panel rendered exactly that — no anchor — while the URL sat in the
card's notes the whole time. The prompt half of the fix lives in
`chat._CHAT_STATIC_PREFIX` / `nudge_llm._TONE`; this pins the render half.
Three properties:
* a markdown link in a NUDGE renders as an anchor that opens in a new tab.
Same-tab is not a style choice here — the panel lives ON /rd and /hq, so
following a link in place tears down the board, the panel, and the nudge's
own unanswered answer row;
* an unusable scheme keeps the label and drops the anchor;
* a nudge's links are cyan like a reply's. `.msg.probe` is a panel-only role,
so chat-msg.css's `.msg.assistant a` never reached it and it fell through
to the UA's default blue.
Boundaries mocked, no LLM and no board writes: /api/cc/exec-history serves a canned
history, marked is stubbed with a link-only parser that honours the renderer
option (the real one is a CDN fetch).
Marked `browser` so the fast smoke step skips it.
.venv/bin/pytest tests/test_exec_links_browser.py -q
"""
import json
import pytest
pytest.importorskip("playwright.sync_api")
pytestmark = pytest.mark.browser
# Enough marked to exercise the path under test: [text](href) through the
# renderer the caller passes, everything else verbatim.
_MARKED = """window.marked = {
use(){},
Renderer: function(){},
parse: (s, o) => '<p>' + s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,
(m, text, href) => (o && o.renderer && o.renderer.link)
? o.renderer.link({ href: href, text: text })
: m) + '</p>'
};"""
_LOGIN_URL = "https://srv265.hrdc-drhc.gc.ca/interdec/ouverturedesession-login/x.aspx?lang=eng"
_HISTORY = {
"messages": [
{
"role": "monitor",
"ts": "2026-09-19T09:00:00+00:00",
"card_id": "card-ei",
"text": f"Fifteen minutes, biweekly. [The EI login]({_LOGIN_URL}) is open in one tap.",
},
{
"role": "monitor",
"ts": "2026-09-19T09:01:00+00:00",
"card_id": "card-ei",
"text": "[Nothing doing](javascript:alert(1)) is not a link.",
},
],
"monitorTotal": 0,
}
@pytest.fixture
def panel(browser, base_url, admin_headers):
"""/rd with the chat history canned and marked stubbed; panel open."""
ctx = browser.new_context(extra_http_headers=admin_headers)
pg = ctx.new_page()
pg.route("**/vendor/marked-*.min.js",
lambda r: r.fulfill(status=200,
content_type="application/javascript",
body=_MARKED))
pg.route("**/hosaka-audio.js*",
lambda r: r.fulfill(status=200,
content_type="application/javascript",
body="window.HosakaAudio={createPlayer:()=>({"
"unlock(){},speak(){return Promise.resolve()},"
"flush(){},setVolume(){},elapsed:()=>0,"
"audioDuration:()=>0,isUnlocked:()=>true,"
"gestureUnlocked:()=>true})};"))
pg.route("**/api/cc/exec-history",
lambda r: r.fulfill(status=200, content_type="application/json",
body=json.dumps(_HISTORY)))
pg.goto(f"{base_url}/rd", wait_until="domcontentloaded")
pg.wait_for_selector("#exec-bubble", timeout=5000)
pg.click("#exec-bubble")
pg.wait_for_selector("#exec-panel.open", timeout=4000)
pg.wait_for_selector("#exec-term .msg.probe", timeout=4000)
yield pg
try:
ctx.close()
except Exception:
pass
def test_a_nudge_link_is_an_anchor_that_opens_a_new_tab(panel):
a = panel.locator("#exec-term .msg.probe a").first
assert a.text_content() == "The EI login"
assert a.get_attribute("href") == _LOGIN_URL
# New tab, and no window.opener handed to the page it opens.
assert a.get_attribute("target") == "_blank"
assert a.get_attribute("rel") == "noopener"
def test_an_unusable_scheme_keeps_the_label_and_drops_the_link(panel):
"""`javascript:` never becomes an anchor — the label survives as text."""
second = panel.locator("#exec-term .msg.probe").nth(1)
assert "Nothing doing" in second.text_content()
assert second.locator("a").count() == 0
def test_a_nudge_link_is_cyan_like_a_reply_link(panel):
"""`.msg.probe a` in exec-bubble.css — the shared vocabulary only covers
`.msg.assistant a`, so without it a nudge's link renders UA-default blue."""
got, want = panel.evaluate("""() => {
const a = document.querySelector('#exec-term .msg.probe a');
const probe = document.createElement('span');
probe.style.color = 'hsl(var(--cyan-hsl) / 0.8)';
document.body.appendChild(probe);
const want = getComputedStyle(probe).color;
probe.remove();
return [getComputedStyle(a).color, want];
}""")
assert got == want