Files
Jyotisha/frontend/tests/character-remaining-contract.test.ts
T
Jesse_Chen 54269fcfcc
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
refactor(chat): extract home helpers, chart library, and starter surfaces
page.tsx still owns the chat main chain, but the first product surfaces now
live in their own modules so later splits can land without editing the 4k-line
Home. Source-lock tests follow the moved tokens; the orphan user-data contract
is aligned and added to the quick gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 22:58:02 +08:00

55 lines
3.0 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
characterRemainingAnnouncement,
characterRemainingLabel,
characterRemainingVisible,
remainingThreshold,
} from "../src/lib/character-remaining.ts";
import { homeSurface as page } from "./home-surface.ts";
const composer = readFileSync(new URL("../src/components/chat-composer.tsx", import.meta.url), "utf8");
const remainingSource = readFileSync(new URL("../src/components/character-remaining.tsx", import.meta.url), "utf8");
const guide = readFileSync(new URL("../src/components/birth-time-guide-turn.tsx", import.meta.url), "utf8");
const choice = readFileSync(new URL("../src/components/birth-time-choice-question.tsx", import.meta.url), "utf8");
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
test("thresholds stay quiet until the last 10 percent, floored at 8 and capped at 50", () => {
assert.equal(remainingThreshold(500), 50);
assert.equal(remainingThreshold(240), 24);
assert.equal(remainingThreshold(80), 8);
assert.equal(characterRemainingVisible(449, 500), false);
assert.equal(characterRemainingVisible(450, 500), true);
assert.equal(characterRemainingVisible(71, 80), false);
assert.equal(characterRemainingVisible(72, 80), true);
});
test("the visible label updates while the live announcement stays at two phrases", () => {
assert.equal(characterRemainingLabel(12, 500), "还剩 12 字");
assert.equal(characterRemainingLabel(0, 500), "已达 500 字上限");
assert.equal(characterRemainingAnnouncement(12, 500), "接近字数上限");
assert.equal(characterRemainingAnnouncement(0, 80), "已达 80 字上限");
});
test("the count region is polite, atomic, and only mounted inside the threshold", () => {
assert.match(remainingSource, /if \(!characterRemainingVisible\(length, maxLength\)\) return null;/);
assert.match(remainingSource, /aria-live="polite"/);
assert.match(remainingSource, /aria-atomic="true"/);
assert.match(remainingSource, /aria-hidden="true"/);
assert.match(remainingSource, /className="sr-only"/);
assert.match(css, /\.character-remaining\[data-limit\] \{ color: var\(--color-danger\); \}/);
});
test("the four capped fields wire describedby to a count that lives in existing chrome", () => {
assert.match(composer, /aria-describedby=\{showRemaining \? composerRemainingId : undefined\}/);
assert.match(page, /<ComposerCharacterRemaining maxLength=\{!profileComplete && onboardingStep === "name" \? 80 : 500\} \/>/);
assert.match(page, /className="composer-footer"/);
assert.match(page, /aria-describedby=\{characterRemainingVisible\(value\.name\.length, 80\) \? nameRemainingId : undefined\}/);
assert.match(guide, /aria-describedby=\{describedBy\}/);
assert.match(choice, /aria-describedby=\{characterRemainingVisible\(note\.length, 240\) \? unmatchedRemainingId : undefined\}/);
assert.match(css, /\.composer-footer > p \{ display: none; \}/);
assert.doesNotMatch(page, /<p className="character-remaining"/);
});