feat: add click-first birth time questions
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import {
|
||||
choiceQuestionGroups,
|
||||
choiceSelectionIntent,
|
||||
normalizeUnmatchedNote,
|
||||
} from "../src/lib/birth-time-choice-question-model.ts";
|
||||
import type { PublicDynamicChoiceQuestion } from "../src/lib/birth-time-dynamic-choice.ts";
|
||||
import {
|
||||
advanceDynamicBirthTimePreview,
|
||||
dynamicBirthTimePreview,
|
||||
} from "../src/lib/birth-time-dynamic-preview.ts";
|
||||
|
||||
const question: PublicDynamicChoiceQuestion = {
|
||||
questionId: "education-shift",
|
||||
prompt: "哪段时间更接近一次明显的学习方向变化?",
|
||||
options: [
|
||||
{ optionId: "early", label: "更接近 2007—2009 年", kind: "primary" },
|
||||
{ optionId: "late", label: "更接近 2010—2012 年", kind: "primary" },
|
||||
{ optionId: "unknown", label: "不记得", kind: "unknown" },
|
||||
{ optionId: "unmatched", label: "都不符合", kind: "unmatched" },
|
||||
],
|
||||
};
|
||||
|
||||
test("click intent submits primary and unknown options immediately", () => {
|
||||
const groups = choiceQuestionGroups(question);
|
||||
|
||||
assert.deepEqual(groups.primary.map((option) => option.optionId), ["early", "late"]);
|
||||
assert.deepEqual(choiceSelectionIntent(groups.primary[0]), {
|
||||
kind: "submit", optionId: "early", effective: true,
|
||||
});
|
||||
assert.deepEqual(choiceSelectionIntent(groups.unknown), {
|
||||
kind: "submit", optionId: "unknown", effective: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("unmatched requests clarification without creating scoring evidence", () => {
|
||||
const groups = choiceQuestionGroups(question);
|
||||
|
||||
assert.deepEqual(choiceSelectionIntent(groups.unmatched), {
|
||||
kind: "submit", optionId: "unmatched", effective: false,
|
||||
});
|
||||
assert.equal(normalizeUnmatchedNote(" 大约是在搬家之后 "), "大约是在搬家之后");
|
||||
assert.equal(normalizeUnmatchedNote("甲".repeat(260)).length, 240);
|
||||
});
|
||||
|
||||
test("the rendered component keeps one-click and optional-note semantics", () => {
|
||||
const source = readFileSync(
|
||||
new URL("../src/components/birth-time-choice-question.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.match(source, /<fieldset/);
|
||||
assert.match(source, /<legend/);
|
||||
assert.match(source, /onSelect\(intent\.optionId\)/);
|
||||
assert.match(source, /maxLength=\{240\}/);
|
||||
assert.doesNotMatch(source, /整理为经历草稿|记得的精度|发生时间|第.*\/.*轮/);
|
||||
assert.doesNotMatch(source, /required/);
|
||||
});
|
||||
|
||||
test("preview choices expose real local interaction states without authentication", () => {
|
||||
const start = dynamicBirthTimePreview();
|
||||
const scoring = advanceDynamicBirthTimePreview(start, { kind: "select", optionId: "earlier" });
|
||||
const clarification = advanceDynamicBirthTimePreview(start, { kind: "select", optionId: "unmatched" });
|
||||
const reframed = advanceDynamicBirthTimePreview(clarification, { kind: "reframe" });
|
||||
const terminal = advanceDynamicBirthTimePreview(reframed, { kind: "finish" });
|
||||
|
||||
assert.equal(scoring.nextAction.kind, "score_pending");
|
||||
assert.equal(clarification.nextAction.kind, "clarify_unmatched_answer");
|
||||
assert.equal(reframed.nextAction.kind, "ask_dynamic_choice");
|
||||
assert.equal(terminal.nextAction.kind, "present_low_result");
|
||||
});
|
||||
|
||||
test("dynamic previews cover every visible result and recovery state", () => {
|
||||
const expected = new Map([
|
||||
["generating", "generate_dynamic_question"],
|
||||
["question-retry", "retry_question_generation"],
|
||||
["question", "ask_dynamic_choice"],
|
||||
["clarification", "clarify_unmatched_answer"],
|
||||
["scoring", "score_pending"],
|
||||
["scoring-retry", "retry_scoring"],
|
||||
["low", "present_low_result"],
|
||||
["medium", "present_medium_result"],
|
||||
["confirmation", "request_candidate_confirmation"],
|
||||
["ready", "ready"],
|
||||
["paused", "paused"],
|
||||
] as const);
|
||||
|
||||
for (const [state, action] of expected) {
|
||||
assert.equal(dynamicBirthTimePreview(state).nextAction.kind, action);
|
||||
}
|
||||
});
|
||||
@@ -5,6 +5,8 @@ import { guidedTurnIdentity } from "../src/lib/birth-time-guided-turn-identity.t
|
||||
|
||||
const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
|
||||
const rectificationSource = read("../src/components/birth-time-rectification.tsx");
|
||||
const legacyRectificationSource = read("../src/components/birth-time-legacy-rectification.tsx");
|
||||
const choiceSource = read("../src/components/birth-time-choice-question.tsx");
|
||||
const turnSource = read("../src/components/birth-time-guide-turn.tsx");
|
||||
const draftSource = read("../src/components/birth-time-evidence-draft-card.tsx");
|
||||
const candidateSource = read("../src/components/birth-time-candidate-result.tsx");
|
||||
@@ -24,7 +26,8 @@ test("each persisted question has a stable remount identity so skipped input can
|
||||
guidedTurnIdentity(3, "education_entry"),
|
||||
guidedTurnIdentity(4, "relationship_entry"),
|
||||
);
|
||||
assert.match(rectificationSource, /key=\{guidedTurnIdentity\(props\.journey\.turnVersion, action\.question\.questionId\)\}/);
|
||||
assert.match(legacyRectificationSource, /key=\{guidedTurnIdentity\(props\.journey\.turnVersion, action\.question\.questionId\)\}/);
|
||||
assert.match(rectificationSource, /key=\{`\$\{props\.journey\.turnVersion\}:\$\{action\.question\.questionId\}`\}/);
|
||||
});
|
||||
|
||||
test("draft review is explicit, domain locked, and incomplete confirmation stays disabled", () => {
|
||||
@@ -64,4 +67,12 @@ test("guided controls expose live status and minimum target classes", () => {
|
||||
assert.match(draftSource, /role=["']alert/);
|
||||
assert.match(turnSource, /birth-time-guided-action/);
|
||||
assert.match(candidateSource, /birth-time-guided-action/);
|
||||
assert.match(choiceSource, /aria-busy=\{props\.pending\}/);
|
||||
assert.match(choiceSource, /birth-time-guided-action/);
|
||||
});
|
||||
|
||||
test("the active v2 surface cannot render the legacy prose and date draft flow", () => {
|
||||
assert.doesNotMatch(rectificationSource, /BirthTimeGuideTurn|BirthTimeEvidenceDraftCard|<textarea/);
|
||||
assert.match(rectificationSource, /<BirthTimeChoiceQuestion/);
|
||||
assert.match(rectificationSource, /<BirthTimeUnmatchedClarification/);
|
||||
});
|
||||
|
||||
@@ -10,6 +10,10 @@ const source = readFileSync(
|
||||
new URL("../src/components/birth-time-rectification.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const legacySource = readFileSync(
|
||||
new URL("../src/components/birth-time-legacy-rectification.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const pageSource = readFileSync(
|
||||
new URL("../src/app/page.tsx", import.meta.url),
|
||||
"utf8",
|
||||
@@ -35,7 +39,7 @@ test("rectification UI is driven only by the persisted guided action", () => {
|
||||
|
||||
test("development previews cover every guided state with legal persisted actions", () => {
|
||||
const expectedActions = new Map([
|
||||
["birth-time-rectification", "ask_baseline_evidence"],
|
||||
["birth-time-rectification", "ask_dynamic_choice"],
|
||||
["birth-time-rectification-draft", "review_evidence_draft"],
|
||||
["birth-time-rectification-score-pending", "score_pending"],
|
||||
["birth-time-rectification-retry", "retry_scoring"],
|
||||
@@ -50,6 +54,7 @@ test("development previews cover every guided state with legal persisted actions
|
||||
assert.equal(isGuidedBirthTimePreview(mode), true);
|
||||
assert.equal(guidedBirthTimePreview(mode).nextAction.kind, action);
|
||||
}
|
||||
assert.equal(guidedBirthTimePreview("birth-time-rectification").journeyProtocol, "dynamic-choice-v2");
|
||||
assert.match(pageSource, /guidedBirthTimePreview\(previewMode\)/);
|
||||
});
|
||||
|
||||
@@ -90,8 +95,10 @@ test("declared-time edits preserve the current journey until the revised profile
|
||||
});
|
||||
|
||||
test("rectification renders draft review and candidates only from nextAction", () => {
|
||||
assert.match(source, /action\.kind === "review_evidence_draft"/);
|
||||
assert.match(source, /<BirthTimeEvidenceDraftCard/);
|
||||
assert.match(legacySource, /action\.kind === "review_evidence_draft"/);
|
||||
assert.match(legacySource, /<BirthTimeEvidenceDraftCard/);
|
||||
assert.doesNotMatch(source, /BirthTimeEvidenceDraftCard|BirthTimeGuideTurn/);
|
||||
assert.match(source, /<BirthTimeChoiceQuestion/);
|
||||
assert.match(source, /<BirthTimeCandidateResult/);
|
||||
assert.match(pageSource, /controller=\{birthTimeGuided\}/);
|
||||
assert.doesNotMatch(pageSource, /submitBirthTimeLifeEvents/);
|
||||
|
||||
Reference in New Issue
Block a user