6190de14cc
The #285 gate dropped every remapped (wrong-layout) candidate shorter than 3 chars, which broke the legitimate short prefix '/сщ' -> 'co' -> Code while '/co' still worked. Replace the blanket length filter with a match-TYPE gate: the original query and remaps >= 3 chars match fully (title/description/searchTerms); a short (1-2 char) remap is restricted to a TITLE fuzzy-match. So '/сщ' -> 'co' matches the 'Code' title again, while '/cy' -> 'сн' and '/b' -> 'и' still do not surface Footnote (they only ever leaked in via the 'сноска'/'примечание' searchTerm substrings, not the title). Adds positive tests for /сщ and /co; keeps the /cy and /b negatives. closes #283 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
87 lines
3.5 KiB
TypeScript
87 lines
3.5 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
buildLayoutCandidates,
|
|
getSuggestionItems,
|
|
} from "./menu-items";
|
|
|
|
/**
|
|
* `buildLayoutCandidates` maps a slash query across physical keyboard layouts
|
|
* (RU ЙЦУКЕН <-> US QWERTY) so the menu matches Latin item titles/terms even
|
|
* when typed with the wrong layout active, while keeping the original query so
|
|
* genuine Cyrillic search terms still match. See bug #283.
|
|
*/
|
|
describe("buildLayoutCandidates", () => {
|
|
it("remaps a RU-layout query to its US-QWERTY equivalent (сщву -> code)", () => {
|
|
expect(buildLayoutCandidates("сщву")).toContain("code");
|
|
});
|
|
|
|
it("remaps a US-layout query to its RU-ЙЦУКЕН equivalent (cyjcrf -> сноска)", () => {
|
|
expect(buildLayoutCandidates("cyjcrf")).toContain("сноска");
|
|
});
|
|
|
|
it("always includes the original query", () => {
|
|
expect(buildLayoutCandidates("сщву")).toContain("сщву");
|
|
expect(buildLayoutCandidates("cyjcrf")).toContain("cyjcrf");
|
|
expect(buildLayoutCandidates("сноска")).toContain("сноска");
|
|
});
|
|
|
|
it("leaves a query with no mappable keys as a single-element set", () => {
|
|
// Digits are on neither layout map, so both remaps are no-ops and de-dup
|
|
// back to one entry.
|
|
expect(buildLayoutCandidates("123")).toEqual(["123"]);
|
|
});
|
|
});
|
|
|
|
/** Helper: flatten grouped suggestion items to a flat list of titles. */
|
|
const titles = (groups: ReturnType<typeof getSuggestionItems>): string[] =>
|
|
Object.values(groups).flatMap((items) => items.map((i) => i.title));
|
|
|
|
describe("getSuggestionItems layout-aware matching", () => {
|
|
it("finds Code when 'code' is typed in RU layout (/сщву)", () => {
|
|
expect(titles(getSuggestionItems({ query: "сщву" }))).toContain("Code");
|
|
});
|
|
|
|
it("still finds Code for the plain /code query", () => {
|
|
expect(titles(getSuggestionItems({ query: "code" }))).toContain("Code");
|
|
});
|
|
|
|
it("finds Code for a short wrong-layout prefix (/сщ -> co)", () => {
|
|
// "сщ" RU->EN remaps to "co", which fuzzy-matches the "Code" title. Short
|
|
// remaps are title-only, but a title match must still get through. See #283.
|
|
expect(titles(getSuggestionItems({ query: "сщ" }))).toContain("Code");
|
|
});
|
|
|
|
it("still finds Code for the plain short query (/co)", () => {
|
|
// Sanity: the original (non-remapped) short query keeps full matching.
|
|
expect(titles(getSuggestionItems({ query: "co" }))).toContain("Code");
|
|
});
|
|
|
|
it("still matches genuine Cyrillic search terms (/сноска -> Footnote)", () => {
|
|
expect(titles(getSuggestionItems({ query: "сноска" }))).toContain(
|
|
"Footnote",
|
|
);
|
|
});
|
|
|
|
it("finds Footnote when 'сноска' is typed in EN layout (/cyjcrf)", () => {
|
|
expect(titles(getSuggestionItems({ query: "cyjcrf" }))).toContain(
|
|
"Footnote",
|
|
);
|
|
});
|
|
|
|
it("does not surface Footnote for a short wrong-layout query (/cy)", () => {
|
|
// "cy" EN->RU remaps to "сн", a substring of the "сноска" searchTerm, but
|
|
// the gate blocks it because the remapped candidate is < 3 chars.
|
|
expect(titles(getSuggestionItems({ query: "cy" }))).not.toContain(
|
|
"Footnote",
|
|
);
|
|
});
|
|
|
|
it("does not surface Footnote for a single-char wrong-layout query (/b)", () => {
|
|
// "b" EN->RU remaps to "и", a substring of the "примечание" searchTerm, but
|
|
// the gate blocks it because the remapped candidate is < 3 chars.
|
|
expect(titles(getSuggestionItems({ query: "b" }))).not.toContain(
|
|
"Footnote",
|
|
);
|
|
});
|
|
});
|