/* Looking a word up: what its underline says, what adding it adds, and English → 한국어 — against the shipped dictionary. */ import { describe, it, expect, beforeAll, afterAll, vi } from "vitest"; import type { Db } from "@app/db/types.js"; import { editAddCustomWord, editCard } from "@app/db/writes.js"; import { wordInfo, wordToAdd } from "@app/domain/words.js"; import { buildEnIndex, enLookup, isLookupWord, loadEnglishIndex } from "@app/domain/english.js"; import { cardLemmaFor } from "@app/domain/evidence.js"; import { deck } from "@app/domain/cards.js"; import { SECURE_INTERVAL, newCard } from "@lib/srs.js"; import { dictionaryDb } from "../helpers/dict-db.js"; let db: Db; beforeAll(async () => { db = await dictionaryDb(1); }); afterAll(async () => { vi.unstubAllGlobals(); await db.close(); }); describe("the underline", () => { it("marks a deck word he has not studied as new, and a secure one as known", async () => { let info = (await wordInfo(db, ["바다"])).get("바다")!; expect(info).toMatchObject({ state: "new", inDeck: true, head: "바다" }); expect(info.gloss?.gloss).toBeTruthy(); const id = (await cardLemmaFor(db, "바다"))!; await editCard(db, id, { ...newCard(), state: 2, interval: SECURE_INTERVAL, due: 99_999, reps: 6 }); info = (await wordInfo(db, ["바다"])).get("바다")!; expect(info.state).toBe("known"); }); it("gives a form its dictionary word's state", async () => { const id = (await cardLemmaFor(db, "가다"))!; await editCard(db, id, { ...newCard(), state: 1, due: 0, reps: 1 }); const info = (await wordInfo(db, ["갔어"])).get("갔어")!; expect(info.state).toBe("learning"); expect(info.head).toBe("가다"); }); it("calls a word only the dictionary knows explainable, and nothing at all unknown", async () => { const row = await db.get<{ headword: string }>( `SELECT l.headword FROM lemma l WHERE l.source NOT IN ('curated','grammar','sfx','curriculum','custom','sentence') AND l.gloss_en <> '' AND l.headword NOT IN (SELECT headword FROM lemma WHERE source IN ('curated','grammar','sfx','curriculum','custom')) ORDER BY l.freq_rank LIMIT 1`, ); const word = row!.headword; const infos = await wordInfo(db, [word, "뷁뷁"]); expect(infos.get(word)).toMatchObject({ state: "gloss", inDeck: false }); expect(infos.get("뷁뷁")).toMatchObject({ state: "unknown", inDeck: false, gloss: null }); }); }); describe("adding a looked-up word", () => { it("adds the dictionary word, and it then counts as his — in the deck and underlined new", async () => { const row = await db.get<{ headword: string }>( `SELECT l.headword FROM lemma l WHERE l.source NOT IN ('curated','grammar','sfx','curriculum','custom','sentence') AND l.gloss_en <> '' AND l.headword NOT IN (SELECT headword FROM lemma WHERE source IN ('curated','grammar','sfx','curriculum','custom')) ORDER BY l.freq_rank LIMIT 1 OFFSET 3`, ); const word = row!.headword; const before = (await wordInfo(db, [word])).get(word)!; const add = wordToAdd(before); expect(add.headword).toBe(word); await editAddCustomWord(db, add); const after = (await wordInfo(db, [word])).get(word)!; expect(after).toMatchObject({ state: "new", inDeck: true }); expect((await deck(db)).some((e) => e.headword === word)).toBe(true); }); it("adds a form nothing knows as he met it", () => { expect( wordToAdd({ token: "뷁다", state: "unknown", gloss: null, head: null, inDeck: false, lemma: null }), ).toEqual({ headword: "뷁다", pos: "verb", gloss: "—" }); }); }); describe("English → 한국어", () => { const index = buildEnIndex([ { ko: "형", gloss: "older brother (said by a man)", note: "noun", pos: "noun", inDeck: true }, { ko: "크다", gloss: "to be big", note: "adj", pos: "adj", inDeck: true }, { ko: "단어", gloss: "word", note: "noun", pos: "noun", inDeck: true }, { ko: "책상", gloss: "desk; table", note: "noun", pos: "noun", inDeck: true }, ]); it("tries the longest phrase around the word first", () => { const words = "my older brother is tall".split(" "); expect(enLookup(index, words, 2).map((e) => e.ko)).toEqual(["형"]); }); it("finds a word inside a gloss, a clause of one, and a plural made singular", () => { expect(enLookup(index, ["big"], 0).map((e) => e.ko)).toEqual(["크다"]); expect(enLookup(index, ["table"], 0).map((e) => e.ko)).toEqual(["책상"]); expect(enLookup(index, ["the", "words"], 1).map((e) => e.ko)).toEqual(["단어"]); }); it("never offers the small words", () => { expect(isLookupWord("the")).toBe(false); expect(isLookupWord("a")).toBe(false); expect(isLookupWord("table")).toBe(true); }); it("is built from the course's own material, never the frequency bands", async () => { const shipped = await loadEnglishIndex(db); expect(enLookup(shipped, ["water"], 0).map((e) => e.ko)).toContain("물"); const course = new Set( ( await db.all<{ headword: string }>( `SELECT headword FROM lemma WHERE source IN ('curated', 'grammar', 'sentence', 'sfx', 'curriculum', 'custom')`, ) ).map((r) => r.headword), ); const indexed = new Set([...shipped.values()].flat().map((e) => e.ko)); expect(indexed.size).toBeGreaterThan(500); expect([...indexed].filter((ko) => !course.has(ko))).toEqual([]); }); });