Files
gitmost/apps/client/src/features/editor/components/slash-menu/menu-items.layout.test.ts
T
agent_coder 6190de14cc fix(editor): let short wrong-layout prefixes match by title (#283)
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>
2026-07-02 14:09:36 +03:00

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",
);
});
});