Files
Find-Word/tests/content.test.ts
2026-10-14 06:19:24 +00:00

189 lines
6.3 KiB
TypeScript
Raw Permalink 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.
import { describe, expect, it } from "vitest";
import {
ENVIRONMENTS,
LEVELS,
PATTERNS,
RULE_DIFFICULTY,
WORDS,
} from "../src/content";
// 개수만 맞추는 더미 데이터를 방지하기 위해 식별자·템플릿·변형 규칙까지
// 검증한다. 사전 등재 여부의 출처와 의미는 별도의 단어 목록 문서로 검토한다.
describe("저장된 문제·환경 데이터", () => {
it("열 패턴은 서로 다른 청취 연산 mechanic을 하나씩 가진다", () => {
expect(PATTERNS).toHaveLength(10);
expect(new Set(PATTERNS.map((p) => p.mechanic))).toEqual(
new Set([
"direct-target",
"distractor-password",
"ignore-then-remember",
"first",
"last",
"middle",
"repetition",
"correction",
"conditional-cue",
"spatial-right-cue",
]),
);
});
// 문구가 서로 다르다는 사실만으로는 청취 과제가 달라지지 않는다. 각 문장에
// 실제 경쟁 단어가 있고, 순서·반복 횟수·취소·조건·방향으로 답이 정해져야 한다.
it.each([
{
id: "luck",
mechanic: "direct-target",
order: [0],
cues: [/행운/],
question: /행운/,
},
{
id: "password",
mechanic: "distractor-password",
order: [1, 0],
cues: [/연습 암호/, /진짜 암호/],
question: /진짜 암호/,
},
{
id: "remember",
mechanic: "ignore-then-remember",
order: [1, 0],
cues: [/잊으세요.*이제.*기억하세요/],
question: /기억/,
},
{
id: "first",
mechanic: "first",
order: [0, 1, 2],
cues: [/세 단어.*첫/],
question: /첫/,
},
{
id: "last",
mechanic: "last",
order: [1, 2, 0],
cues: [/세 단어.*마지막/],
question: /마지막/,
},
{
id: "middle",
mechanic: "middle",
order: [1, 0, 2],
cues: [/세 단어.*가운데/],
question: /가운데/,
},
{
id: "repeat",
mechanic: "repetition",
order: [0, 1, 0, 2],
cues: [/두 번/],
question: /두 번/,
},
{
id: "correction",
mechanic: "correction",
order: [1, 0],
cues: [/아니.*잘못.*정정/],
question: /정정/,
},
{
id: "message",
mechanic: "conditional-cue",
order: [1, 0],
cues: [/초록 불이면 앞 단어/, /빨간 불이면 뒤 단어/, /지금은 빨간 불/],
question: /신호/,
},
{
id: "mission",
mechanic: "spatial-right-cue",
order: [0, 1],
cues: [/오른쪽 표지.*왼쪽 표지/],
question: /오른쪽/,
},
])(
"$mechanic의 열 문장은 해당 청취 의도로만 답을 고를 수 있다",
({ id, mechanic, order, cues, question }) => {
const pattern = PATTERNS.find((p) => p.id === id)!;
expect.soft(pattern.mechanic).toBe(mechanic);
expect.soft(pattern.question).toMatch(question);
cues.forEach((cue) => expect.soft(pattern.sentence).toMatch(cue));
for (const entry of WORDS) {
const choices = [entry.answer, ...entry.distractors.map((d) => d.word)];
const sentence = pattern.sentence
.replaceAll("{word}", choices[0]!)
.replaceAll("{distractor1}", choices[1]!)
.replaceAll("{distractor2}", choices[2]!);
const heard = [...sentence.matchAll(/‘([^’]+)’/g)].map(
(match) => match[1],
);
expect.soft(heard).toEqual(order.map((index) => choices[index]));
expect(sentence).not.toMatch(/[{}]/);
// 인용 단어 목록 안에서 반복 목표는 정확히 두 번, 나머지는 한 번이다.
expect
.soft(heard.filter((word) => word === entry.answer))
.toHaveLength(mechanic === "repetition" ? 2 : 1);
}
},
);
it("오답마다 서로 다른 변형 혼동 난이도를 추적할 수 있다", () => {
WORDS.forEach((entry) => {
const ranks = entry.distractors.map((d) => RULE_DIFFICULTY[d.rule]);
ranks.forEach((rank) => expect(rank).toBeGreaterThan(0));
expect(new Set(ranks).size).toBe(3);
});
});
it("서로 다른 질문 의도를 가진 열 가지 이상 문장 패턴을 제공한다", () => {
expect(PATTERNS.length).toBeGreaterThanOrEqual(10);
expect(new Set(PATTERNS.map((p) => p.id)).size).toBe(PATTERNS.length);
expect(new Set(PATTERNS.map((p) => p.question)).size).toBe(PATTERNS.length);
PATTERNS.forEach((p) => expect(p.sentence).toContain("{word}"));
});
it("네 개의 고유 사전 단어와 서로 다른 오답 규칙을 제공한다", () => {
expect(WORDS.length).toBeGreaterThanOrEqual(10);
WORDS.forEach((entry) => {
expect(entry.distractors).toHaveLength(3);
expect(
new Set([entry.answer, ...entry.distractors.map((d) => d.word)]).size,
).toBe(4);
expect(new Set(entry.distractors.map((d) => d.rule)).size).toBe(3);
// 한글 완성형을 초성·중성·종성으로 분해하여 설명과 실제 변형이 일치하는지 본다.
for (const { word, rule } of entry.distractors) {
const split = (s: string) =>
[...s].map((c) => {
const n = c.charCodeAt(0) - 0xac00;
return [Math.floor(n / 588), Math.floor(n / 28) % 21, n % 28];
});
const before = split(entry.answer).flat();
const after = split(word).flat();
const changed = before.flatMap((v, i) =>
v === after[i] ? [] : [i % 3],
);
if (rule !== "음절 교체") {
expect(changed).toHaveLength(1);
expect(changed[0]).toBe(
["초성 교체", "모음 교체", "받침 교체"].indexOf(rule),
);
}
}
});
});
it("여덟 환경과 단조롭게 낮아지는 다섯 상대 dB를 제공한다", () => {
expect(ENVIRONMENTS.map((e) => e.name)).toEqual([
"길거리",
"쇼핑몰",
"극장",
"백화점",
"시장",
"지하철",
"카페",
"비 오는 거리",
]);
expect(new Set(ENVIRONMENTS.map((e) => e.seed)).size).toBe(8);
expect(LEVELS).toHaveLength(5);
LEVELS.forEach((l, i) => {
if (i) expect(l.db).toBeLessThan(LEVELS[i - 1]!.db);
});
expect(PATTERNS.length * WORDS.length).toBeGreaterThan(10);
});
});