/**
* 検索用の文字列正規化(`lib/format/search-text.ts`)のテスト。指示書AG 段階2。
*
* ------------------------------------------------------------------------
* ★期待値表(実装より先に確定させたもの。CLAUDE.md §6-2)★
* ------------------------------------------------------------------------
* | # | 入力(needle) | 対象(haystack) | includesFoldName | includesFoldPlain |
* |----|---------------------|-------------------------|------------------|-------------------|
* | 1 | ドンキホーテ | ドン・キホーテ戸塚 | ★true★ | false |
* | 2 | ドン・キホーテ | ドン・キホーテ戸塚 | true | true |
* | 3 | 戸塚 | ドン・キホーテ戸塚 | true | true |
* | 4 | ドンキ | ドン・キホーテ戸塚 | ★true★ | false |
* | 5 | ドンキホーテ港南 | MEGAドン・キホーテ 港南 | ★true★ | false |
* | 6 | cc1 | PUDU CC1 | true | ★true★ |
* | 7 | PUDUCC1 | PUDU CC1 | ★true★ | ★false★ |
* | 8 | 8111r | 8111R5628060011 | true | ★true★ |
* | 9 | ★23★ | ★東京都○○区1-2-3★ | ★true(だから住所には使わない)★ | ★false★ |
* | 10 | ★10001★ | ★PUDU-CC1-0001★ | ★true(だからシリアルには使わない)★ | ★false★ |
* | 11 | 空文字 | 何でも | ★true(素通し)★ | ★true★ |
* | 12 | ドン | ドン・キホーテ戸塚 | ★false(D-4)★ | false |
* | 13 | どん | ドン・キホーテ戸塚 | ★false(D-4)★ | false |
* | 14 | ★メガドンキホーテ★ | ★MEGAドン・キホーテ 港南★ | ★false(D-4。残る穴)★ | false |
* | 15 | ★`・`(記号だけ)★ | ドン・キホーテ戸塚 | ★★false(2026-09-14 に反転)★★ | ★true(記号を残すので当たる)★ |
* | 16 | ★`-`(記号だけ)★ | ★PUDU-CC1-0001★ | ★false★ | ★★true(シリアル検索は生きる)★★ |
* | 17 | ★ドン・(記号入り)★| ドン・キホーテ戸塚 | ★true(記号だけではない)★ | true |
* ------------------------------------------------------------------------
*
* ★#9 と #10 の行を消さないこと★ — 「だから住所とシリアルには `foldName` を使わない」
* という D-2 の根拠そのもの。消すと後任が「揃っていないから揃えよう」と
* `foldName` に寄せてしまい、番地とシリアルの桁境界が静かに壊れる。
*
* ★#12〜#14 は「実装していないこと」を固定するテスト★(D-4)。
* ユーザーがカナ・全半角統一と英字カナ読み替えを選ばなかった、という決定の記録である。
* これらを true にしたくなったら、勝手に足さずに仕様確認からやり直すこと。
*
* ★★#15〜#17 は 2026-09-14 に反転させた行★★(ユーザー判断: 記号のみ検索は弾く。案A)。
* ★反転前は #15 / #16 の `includesFoldName` が true(=記号だけ打つと全件)だった★。
* ★#11(未指定)は反転していない★ — 空文字は「絞り込まない」という指示なので素通し。
* ★`includesFoldPlain` の列は1行も変えていない★ — #16 の true が消えると
* ★`-` でシリアルを探せなくなる★。
*/
import { describe, expect, it } from "vitest";
import {
foldName,
foldPlain,
includesFoldName,
includesFoldPlain,
} from "@/lib/format/search-text";
describe("foldPlain", () => {
it("大文字小文字だけを揃える", () => {
expect(foldPlain("PUDU CC1")).toBe("pudu cc1");
expect(foldPlain("8111R5628060011")).toBe("8111r5628060011");
});
it("★記号は残す★(住所の番地・シリアルの桁境界を壊さないため)", () => {
expect(foldPlain("東京都○○区1-2-3")).toBe("東京都○○区1-2-3");
expect(foldPlain("PUDU-CC1-0001")).toBe("pudu-cc1-0001");
expect(foldPlain("ドン・キホーテ戸塚")).toBe("ドン・キホーテ戸塚");
});
});
describe("foldName", () => {
it("大文字小文字を揃え、記号を除く", () => {
expect(foldName("MEGAドン・キホーテ 港南")).toBe("megaドンキホテ港南");
});
it("記号だけの入力は空文字になる(=絞り込み無しと同じ扱いになる)", () => {
expect(foldName("・")).toBe("");
expect(foldName(" ")).toBe("");
});
// ★D-3 の除去対象を1文字ずつ固定する★
// まとめて1件のテストにすると、1文字だけ抜けても他の文字で緑になってしまう。
describe("除く記号(D-3)を1文字ずつ", () => {
const cases: ReadonlyArray<[string, string]> = [
["中点 U+30FB", "あ・い"],
["中点 U+FF65(半角)", "あ・い"],
["中点 U+00B7", "あ·い"],
["中点 U+2022", "あ•い"],
["長音 U+30FC", "あーい"],
["半角スペース U+0020", "あ い"],
["全角スペース U+3000", "あ い"],
["タブ U+0009", "あ\tい"],
["ハイフン U+002D", "あ-い"],
["ハイフン U+2010", "あ‐い"],
["ハイフン U+2011", "あ‑い"],
["ハイフン U+2012", "あ‒い"],
["ハイフン U+2013", "あ–い"],
["ハイフン U+2014", "あ—い"],
["ハイフン U+2015", "あ―い"],
["全角ハイフン U+FF0D", "あ-い"],
["アンダースコア U+005F", "あ_い"],
];
for (const [label, input] of cases) {
it(`${label} を除く`, () => {
expect(foldName(input)).toBe("あい");
});
}
});
// ★除かない文字★ — 集合を広げすぎていないことを固定する
it("スラッシュ・括弧・読点は除かない(D-3 の4種類以外は除かない)", () => {
expect(foldName("あ/い")).toBe("あ/い");
expect(foldName("あ(い)")).toBe("あ(い)");
expect(foldName("あ、い")).toBe("あ、い");
expect(foldName("あ.い")).toBe("あ.い");
});
it("★改行は除かない★(D-3 の空白3種は半角・全角スペースとタブのみ)", () => {
expect(foldName("あ\nい")).toBe("あ\nい");
});
});
describe("includesFoldName / includesFoldPlain の期待値表", () => {
const table: ReadonlyArray<{
no: number;
needle: string;
haystack: string;
name: boolean;
plain: boolean;
}> = [
{ no: 1, needle: "ドンキホーテ", haystack: "ドン・キホーテ戸塚", name: true, plain: false },
{ no: 2, needle: "ドン・キホーテ", haystack: "ドン・キホーテ戸塚", name: true, plain: true },
{ no: 3, needle: "戸塚", haystack: "ドン・キホーテ戸塚", name: true, plain: true },
{ no: 4, needle: "ドンキ", haystack: "ドン・キホーテ戸塚", name: true, plain: false },
{ no: 5, needle: "ドンキホーテ港南", haystack: "MEGAドン・キホーテ 港南", name: true, plain: false },
{ no: 6, needle: "cc1", haystack: "PUDU CC1", name: true, plain: true },
{ no: 7, needle: "PUDUCC1", haystack: "PUDU CC1", name: true, plain: false },
{ no: 8, needle: "8111r", haystack: "8111R5628060011", name: true, plain: true },
// ★9・10 は「だから住所とシリアルに foldName を使わない」の根拠(D-2)★
{ no: 9, needle: "23", haystack: "東京都○○区1-2-3", name: true, plain: false },
{ no: 10, needle: "10001", haystack: "PUDU-CC1-0001", name: true, plain: false },
{ no: 11, needle: "", haystack: "ドン・キホーテ戸塚", name: true, plain: true },
// ★12〜14 は D-4(実装しないと決めたこと)の記録★
{ no: 12, needle: "ドン", haystack: "ドン・キホーテ戸塚", name: false, plain: false },
{ no: 13, needle: "どん", haystack: "ドン・キホーテ戸塚", name: false, plain: false },
{ no: 14, needle: "メガドンキホーテ", haystack: "MEGAドン・キホーテ 港南", name: false, plain: false },
// ★15〜17 は 2026-09-14 に反転させた行(記号だけの入力を弾く。案A)★
// 15・16 の name は反転前 true(=記号だけ打つと全件)だった
{ no: 15, needle: "・", haystack: "ドン・キホーテ戸塚", name: false, plain: true },
{ no: 16, needle: "-", haystack: "PUDU-CC1-0001", name: false, plain: true },
{ no: 17, needle: "ドン・", haystack: "ドン・キホーテ戸塚", name: true, plain: true },
];
for (const { no, needle, haystack, name, plain } of table) {
it(`#${no} "${needle}" in "${haystack}" → foldName:${name} / foldPlain:${plain}`, () => {
expect(includesFoldName(haystack, needle)).toBe(name);
expect(includesFoldPlain(haystack, needle)).toBe(plain);
});
}
});
describe("includesFoldName / includesFoldPlain の性質", () => {
// ★D-5★ 片側だけ正規化する間違いを封じるための関数なので、両側に効くことを固定する
it("データ側に記号があっても、入力側に記号があっても引ける(両側に掛かる)", () => {
// データ側に中点・入力側に無し
expect(includesFoldName("ドン・キホーテ戸塚", "ドンキホーテ")).toBe(true);
// 入力側に中点・データ側に無し
expect(includesFoldName("ドンキホーテ戸塚", "ドン・キホーテ")).toBe(true);
// 両側に記号(位置違い)
expect(includesFoldName("ドン・キホーテ 戸塚", "キホーテ・戸塚")).toBe(true);
});
it("該当しない語は両方 false", () => {
expect(includesFoldName("ドン・キホーテ戸塚", "ZZZ-NOMATCH")).toBe(false);
expect(includesFoldPlain("8111R5628060011", "ZZZ-NOMATCH")).toBe(false);
});
it("空の haystack でも落ちない", () => {
expect(includesFoldName("", "")).toBe(true);
expect(includesFoldName("", "ドン")).toBe(false);
expect(includesFoldPlain("", "ドン")).toBe(false);
});
});
/**
* ★記号だけの入力は「当たらない」★(★ユーザー判断 2026-09-14。案A★)
*
* ★★2026-09-14 に反転させた挙動★★ — 反転前は `foldName("・") === ""` の帰結で
* `includes("")` が常に true になり、★記号だけ打つと全件が出ていた★。
* ★検索したのに全件が出る★のは 0件と違って画面上は成功に見えるため、
* 打ち間違い(キーボード切替ミスで `-` だけ残る等)に気づけない。
*
* ★境界は「未指定(空文字)」と「記号だけ」の1点だけ★。
* ★空文字は反転していない★(素通しのまま)。ここを巻き込むと
* ★施設名を入力していないのに一覧が空になる★。
*/
describe("★記号だけの入力★(2026-09-14 に反転。案A)", () => {
const HAYSTACK = "MEGAドン・キホーテ 港南";
it("★未指定(空文字)は素通し★(=ここは反転していない)", () => {
expect(includesFoldName(HAYSTACK, "")).toBe(true);
expect(includesFoldName("", "")).toBe(true);
});
// ★D-3 の17文字を1文字ずつ★ — まとめて1件にすると、1文字だけ漏れても緑になる
describe("D-3 の除去対象17文字を1文字ずつ → false", () => {
const cases: ReadonlyArray<[string, string]> = [
["中点 U+30FB", "・"],
["中点 U+FF65(半角)", "・"],
["中点 U+00B7", "·"],
["中点 U+2022", "•"],
["長音 U+30FC", "ー"],
["半角スペース U+0020", " "],
["全角スペース U+3000", " "],
["タブ U+0009", "\t"],
["ハイフン U+002D", "-"],
["ハイフン U+2010", "‐"],
["ハイフン U+2011", "‑"],
["ハイフン U+2012", "‒"],
["ハイフン U+2013", "–"],
["ハイフン U+2014", "—"],
["ハイフン U+2015", "―"],
["全角ハイフン U+FF0D", "-"],
["アンダースコア U+005F", "_"],
];
for (const [label, needle] of cases) {
it(`${label} だけの入力は当たらない`, () => {
// ★needle は空文字ではない★(空文字と同じ道に落ちていないことの確認)
expect(needle).not.toBe("");
expect(foldName(needle)).toBe("");
expect(includesFoldName(HAYSTACK, needle)).toBe(false);
});
}
});
it("★記号の組み合わせ★も当たらない", () => {
for (const needle of ["・ー", "--", "・ -", "__", "ー・ー", " \t", "-・-"]) {
expect(includesFoldName(HAYSTACK, needle)).toBe(false);
}
});
it("★記号を含むが記号だけではない入力は従来どおり当たる★", () => {
expect(includesFoldName("ドン・キホーテ戸塚", "ドン・")).toBe(true);
expect(includesFoldName("ドン・キホーテ戸塚", "・キホーテ")).toBe(true);
expect(includesFoldName("ドン・キホーテ戸塚", "ドン キホーテ")).toBe(true);
expect(includesFoldName(HAYSTACK, "ドンキホーテ 港南")).toBe(true);
// 記号を含んだうえで一致しない語は、これまでどおり false
expect(includesFoldName(HAYSTACK, "-ZZZ-")).toBe(false);
});
it("★haystack が記号だけでも落ちない★", () => {
expect(includesFoldName("・", "・")).toBe(false);
expect(includesFoldName("・", "")).toBe(true);
expect(includesFoldName("・", "ドン")).toBe(false);
});
/**
* ★★`includesFoldPlain` は変えていない★★(★揃えないのが正しい★)
* `foldPlain` は記号を残すので `-` はそのまま照合され、
* ★シリアル `PUDU-CC1-0001` に当たる = 意味のある検索★。
* ★ここに同じ早期 return を足すと、ハイフンでシリアルを探せなくなる★。
*/
it("★`includesFoldPlain` は記号だけの入力を弾かない★(シリアル検索が生きる)", () => {
expect(includesFoldPlain("PUDU-CC1-0001", "-")).toBe(true);
expect(includesFoldPlain("DS-001", "-")).toBe(true);
// ★記号を持たないシリアルには当たらない★ = ちゃんと絞り込みとして働いている
expect(includesFoldPlain("8111R5628060011", "-")).toBe(false);
// 中点・空白も「そのまま探す」だけ(弾かない)
expect(includesFoldPlain("ドン・キホーテ戸塚", "・")).toBe(true);
expect(includesFoldPlain("MEGAドン・キホーテ 港南", " ")).toBe(true);
expect(includesFoldPlain("8111R5628060011", "・")).toBe(false);
});
});