Files
learnharness/tests/render-composer.test.tsx
T
Amos 0baa00e418 UI iteration 2: toggleable sidebar, floating composer blob, pending-question morph
Sidebar (replaces the modal drawer):
- Inline, toggleable, no backdrop/focus theft — can stay open while working
- Open state persists across pages (provider in root layout) and reloads
  (localStorage; starts closed server-side to avoid hydration mismatch,
  stored value applied post-mount; default open on first run)
- Hamburger in page headers only when closed; close button in sidebar header

Floating composer blob (per reference screenshot):
- Floats over the transcript (absolute overlay), persists at bottom while
  scrolling, content fades behind it (gradient mask); pb-44 scroll clearance
- Concentric corner radii: blob rounded-2xl(16px) − p-1.5(6px) = send
  rounded-[10px] = 25% of the 40px button; CornerDownLeft icon per reference
- Stop moved from header into the blob (busy → square icon)
- prompt-input.tsx + 7 orphan ui files pruned (attachments/command palette
  unused); hand-rolled Composer is ~100 lines and render-tested

Pending-question morph (one text field on screen, never two):
- Quiz/ask-select pending → blob becomes the note field (send hidden, hint
  line above); card's internal note input removed; Submit consumes it
- Ask free-text pending → blob becomes the answer field; its send submits
  via the card's registered submit api (ref bridge); Enter works
- Chat owns the single text state; card clears it on submit
- canSend for answer mode mirrors the card predicate directly — reading the
  ref during render lagged one commit (found in browser testing)

Widths: one shared column (max-w-3xl + px-4) for transcript, cards, and
composer — all three measured pixel-identical (left 416, width 736).
Assistant Message max-w-full/MessageContent w-full (was max-w-[95%]/w-fit —
the 'cards are a bit shorter' bug).

User messages render plain text again — no math pipeline on learner-authored
text (currency hazard accepted for the model only, guarded by the prompt).

Tests: 58 pass (5 new composer pins; card tests updated to controlled
props). Verified live: note flow, answer flow, sidebar persistence, float,
fade, zero console errors — plus the live model spontaneously honoring the
$ currency escape contract in a continuation.
2026-08-22 04:28:13 +03:00

61 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Composer blob render tests — the morphing text field (chat / note / answer
* modes), concentric corner radii, and the stop-while-busy button.
*
* Same tsx classic-JSX shim as render-cards.test.tsx.
*/
import React from "react";
(globalThis as Record<string, unknown>).React = React;
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import { Composer } from "../components/composer.tsx";
function render(mode: "chat" | "note" | "answer", opts: { busy?: boolean; canSend?: boolean } = {}) {
return renderToStaticMarkup(
<Composer
mode={mode}
busy={opts.busy ?? false}
value=""
onChange={() => {}}
onSend={() => {}}
onStop={() => {}}
canSend={opts.canSend ?? true}
/>,
);
}
describe("Composer blob", () => {
it("chat mode: chat placeholder + send button", () => {
const html = render("chat");
assert.match(html, /Ask, answer, or steer the lesson/);
assert.ok(html.includes('aria-label="Send"'), "send button present");
});
it("note mode: note placeholder and NO send button (card submits)", () => {
const html = render("note");
assert.match(html, /Add a note/);
assert.ok(!html.includes('aria-label="Send"'), "no send in note mode");
assert.ok(!html.includes('aria-label="Stop"'), "no stop either");
});
it("answer mode: answer placeholder + send button (submits the answer)", () => {
const html = render("answer");
assert.match(html, /Type your answer/);
assert.ok(html.includes('aria-label="Send"'), "send present in answer mode");
});
it("busy: send button becomes stop", () => {
const html = render("chat", { busy: true });
assert.ok(html.includes('aria-label="Stop"'), "stop replaces send while streaming");
});
it("concentric corner radii: blob 16px, send 10px (25% of the 40px button)", () => {
const html = render("chat");
assert.ok(html.includes("rounded-2xl"), "blob radius");
assert.ok(html.includes("rounded-[10px]"), "send radius = blob radius − padding");
assert.ok(html.includes("size-10"), "40px send button (10px = 25%)");
});
});