Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions backend/tests/test_frontend_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,26 @@ def test_index_html_references_chat_assets() -> None:
assert 'meta name="tutor-backend"' in index


def test_chat_panel_hidden_attribute_works() -> None:
"""Regression: the panel's `display: flex` overrides the UA stylesheet's
`[hidden] { display: none }` rule because they share specificity, so
clicking the close X never actually hid the panel. The CSS must re-assert
`display: none` for the hidden state."""
css = (FRONTEND_DIR / "tutor-chat.css").read_text(encoding="utf-8")
assert re.search(
r"\.tutor-chat__panel\[hidden\]\s*\{[^}]*display\s*:\s*none",
css,
), "tutor-chat.css must re-assert display:none for .tutor-chat__panel[hidden]"


def test_chat_close_button_is_wired() -> None:
"""The close X must have a click handler that calls toggle(false, ...)."""
js = (FRONTEND_DIR / "tutor-chat.js").read_text(encoding="utf-8")
assert "tutorChatClose" in js
assert "closeBtn.addEventListener('click'" in js or 'closeBtn.addEventListener("click"' in js
assert "toggle(false" in js


def test_chat_js_posts_to_api_chat() -> None:
js = (FRONTEND_DIR / "tutor-chat.js").read_text(encoding="utf-8")
# The module must POST to /api/chat with a JSON body containing `messages`.
Expand Down
4 changes: 2 additions & 2 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@

<link rel="stylesheet" href="base.css?v=20260416c" />
<link rel="stylesheet" href="style.css?v=20260416b" />
<link rel="stylesheet" href="tutor-chat.css?v=20260516a" />
<link rel="stylesheet" href="tutor-chat.css?v=20260519a" />
<link rel="stylesheet" href="tutor-codelab.css?v=20260516a" />

<!--
Expand Down Expand Up @@ -269,7 +269,7 @@ <h1 class="section__title" id="secTitle">Title</h1>

<script src="tutor-codelab.js?v=20260516a" defer></script>
<script src="app.js?v=20260416" defer></script>
<script src="tutor-chat.js?v=20260516a" defer></script>
<script src="tutor-chat.js?v=20260519a" defer></script>

<!-- Service Worker Registration + Cache Bust -->
<script>
Expand Down
2 changes: 1 addition & 1 deletion frontend/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Cache-first for shell, network-first for content
============================================================ */

const CACHE_VERSION = 'pytutor-v2026-05-16d';
const CACHE_VERSION = 'pytutor-v2026-05-19a';
const SHELL_ASSETS = [
'./',
'./index.html',
Expand Down
8 changes: 7 additions & 1 deletion frontend/tutor-chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,15 @@
border-radius: var(--r-lg);
box-shadow: var(--shadow-pop);
overflow: hidden;
z-index: 60;
z-index: 61;
}

/* The class above sets display:flex, which has the same specificity as the UA
rule [hidden] { display: none }. Author rules win the cascade, so the
hidden attribute would otherwise be a no-op and the panel would never
close. Re-assert display:none for the hidden state. */
.tutor-chat__panel[hidden] { display: none; }

@media (max-width: 559px) {
.tutor-chat__panel {
left: var(--sp-3);
Expand Down
17 changes: 12 additions & 5 deletions frontend/tutor-chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
root.id = 'tutorChatRoot';
root.className = 'tutor-chat';
root.innerHTML = `
<button class="tutor-chat__fab" id="tutorChatFab" aria-label="Open tutor chat" aria-expanded="false" aria-controls="tutorChatPanel">
<button type="button" class="tutor-chat__fab" id="tutorChatFab" aria-label="Open tutor chat" aria-expanded="false" aria-controls="tutorChatPanel">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
<path d="M5 5h14a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H9l-4 4V7a2 2 0 0 1 2-2Z"
fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
Expand All @@ -134,8 +134,8 @@
<h2 class="tutor-chat__title">Ask about Python</h2>
<p class="tutor-chat__sub" id="tutorChatSub">Connecting…</p>
</div>
<button class="tutor-chat__close" id="tutorChatClose" aria-label="Close tutor chat">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<button type="button" class="tutor-chat__close" id="tutorChatClose" aria-label="Close tutor chat" title="Close tutor chat">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false" pointer-events="none">
<path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
Expand Down Expand Up @@ -174,8 +174,15 @@
const banner = root.querySelector('#tutorChatBanner');
const sendBtn = root.querySelector('#tutorChatSend');

fab.addEventListener('click', () => toggle(true, { panel, fab, input }));
closeBtn.addEventListener('click', () => toggle(false, { panel, fab, input }));
fab.addEventListener('click', (e) => {
e.preventDefault();
toggle(true, { panel, fab, input });
});
closeBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
toggle(false, { panel, fab, input });
});
resetBtn.addEventListener('click', () => {
state.history = [];
renderLog(log);
Expand Down
Loading