fix: one notepad on the quiz page, not two

Per-question notes were already built and saved on blur. The global
notes tab was still floating over the same screen beside them, so it was
never clear which notepad a note was going into. The global note stays
where it belongs, on the dashboard.

The quiz test mock had no `put`, which is why nothing had ever covered
the per-question note path; it does now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
Daniel 2026-09-11 13:15:03 +02:00
parent db2bbf0638
commit a5f5e3a536
3 changed files with 27 additions and 6 deletions

View file

@ -217,8 +217,10 @@ Captured so nothing is lost while the article writing runs.
## Quiz runner
- [ ] **Per-question notes in study mode**, replacing the global notes tab that is
currently on the quiz page.
- [x] **Per-question notes** — done. The notes themselves were already built
(`question_notes`, saved on blur); what was missing was removing the
global notes tab that floated over the same screen, so it was never clear
which notepad you were writing in. The global note stays on the dashboard.
- [ ] **Per-question feedback** to the educator.
- [ ] **Tutorial mode** — first-run coach marks ("Step 2 of 6", Skip / Next).

View file

@ -9,7 +9,6 @@ import { useAuth } from '../context/AuthContext'
import api from '../api/client'
import useMediaQuery from '../hooks/useMediaQuery'
import FigureStrip from '../components/FigureStrip'
import MyNote from '../components/MyNote'
import QuizTools, { QuizDialog } from '../components/QuizTools'
import './QuizPlayer.css'
@ -1112,7 +1111,11 @@ const timerStarted = timeLeft !== null
return (
<div className="quiz-bottom quiz-player">
<MyNote variant="tab" />
{/* The floating global-notes tab is gone. A note taken while sitting a
question is about that question, and there is a per-question note in
the toolbar below; a second, unrelated notepad floating over the same
screen only made it ambiguous which one you were writing in. The
global note still lives on the dashboard. */}
{tool && <QuizTools tool={tool} onClose={() => setTool(null)} />}
{showReview && <QuizDialog title="Review & Complete" onClose={() => setShowReview(false)}>
<p>{answeredCount} of {totalCount} questions answered. Unanswered questions count as incorrect.</p>

View file

@ -5,9 +5,8 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom'
import QuizPage from './QuizPage'
import api from '../api/client'
vi.mock('../api/client', () => ({ default: { get: vi.fn(), post: vi.fn(), delete: vi.fn() } }))
vi.mock('../api/client', () => ({ default: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() } }))
vi.mock('../context/AuthContext', () => ({ useAuth: () => ({ user: { id: 1, name: 'Learner', role: 'user' } }) }))
vi.mock('../components/MyNote', () => ({ default: () => null }))
vi.mock('../components/TeachChat', () => ({ default: ({ attemptId }) => <div data-testid="tutor-context">Study tutor {attemptId}</div> }))
const questions = [
@ -249,6 +248,23 @@ describe('quiz player', () => {
expect(screen.queryByText('Full first clinical question.')).not.toBeInTheDocument()
})
it('keeps notes with the question, not in a second notepad floating over it', async () => {
await begin()
const bar = await screen.findByRole('toolbar', { name: 'Question actions' })
// The global note tab used to float over the player alongside this one, so
// it was never clear which notepad you were typing into.
expect(document.querySelector('.mynote-tab')).toBeNull()
await userEvent.click(within(bar).getByRole('button', { name: /Add notes/ }))
const box = screen.getByRole('textbox', { name: /note/i })
await userEvent.type(box, 'Ask about vaccination status')
expect(screen.getByText('Unsaved')).toBeInTheDocument()
// Saved on blur, so a note is not written on every keystroke.
await userEvent.click(screen.getByRole('button', { name: 'Save note' }))
await waitFor(() => expect(api.put).toHaveBeenCalledWith(
'/questions/detail/1/note', { content: 'Ask about vaccination status' }))
})
it('shows per-option explanations in study feedback behind a toggle', async () => {
const originalGet = api.get.getMockImplementation()
api.get.mockImplementation(async (url, ...args) => {