1. Fix RU_TO_EN keyboard map in keyboardLayout.ts: - ч was mapped to 'c' (wrong), now correctly 'x' - с was mapped to 'v' (wrong), now correctly 'c' - ю was mapped to '.' (wrong), now correctly ',' - Full rewrite with standard QWERTY/ЙЦУКЕН positional mapping - Examples: dthcnfr→верстак, руддщ→hello, цщкдв→world all correct now 2. Root cause of search breaking after 3-4 chars: Old map had ч:'c', с:'v' swapped. So dthcnfr → 'верчиак' (wrong). Each character was mapped to wrong Cyrillic equivalent. 3. Add unit tests: keyboardLayout.test.js (39 tests, node-runner): - EN→RU: dthcnfr, ghbdtn, ntcn - RU→EN: руддщ, цщкдв, ышеш - Unicode safety: Latin c (U+0063) ≠ Cyrillic с (U+0441) - expandKeyboardVariants for mixed inputs - Edge cases: empty, single char, mixed case, numbers 4. InternalLinkPicker type tabs → filters (not search modes): - Store rawResults (all) + filtered results by activeType - Switching type tab no longer clears query or triggers new search - Just filters existing rawResults by selected type - Shows 'Нет результатов для этого типа' when filtered empty 5. Both GlobalSearch and InternalLinkPicker use same expandKeyboardVariants() All tests PASS, full build OK.
127 lines
5.8 KiB
JavaScript
127 lines
5.8 KiB
JavaScript
/**
|
||
* Unit tests for keyboardLayout.ts
|
||
* Run: node frontend/src/lib/util/keyboardLayout.test.js
|
||
*
|
||
* Tests exact Unicode character mappings for RU/EN QWERTY keyboard layout swap.
|
||
* Critical: Latin 'c' ≠ Cyrillic 'с' (different Unicode codepoints).
|
||
*/
|
||
|
||
// Inline the module logic to avoid Svelte/TS build dependencies
|
||
const RU_TO_EN = {
|
||
й:'q', ц:'w', у:'e', к:'r', е:'t', н:'y', г:'u', ш:'i', щ:'o', з:'p', х:'[', ъ:']',
|
||
ф:'a', ы:'s', в:'d', а:'f', п:'g', р:'h', о:'j', л:'k', д:'l', ж:';', э:"'",
|
||
я:'z', ч:'x', с:'c', м:'v', и:'b', т:'n', ь:'m', ю:',', ё:'`',
|
||
Й:'Q', Ц:'W', У:'E', К:'R', Е:'T', Н:'Y', Г:'U', Ш:'I', Щ:'O', З:'P', Х:'{', Ъ:'}',
|
||
Ф:'A', Ы:'S', В:'D', А:'F', П:'G', Р:'H', О:'J', Л:'K', Д:'L', Ж:':', Э:'"',
|
||
Я:'Z', Ч:'X', С:'C', М:'V', И:'B', Т:'N', Ь:'M', Ю:'<', Ё:'~',
|
||
};
|
||
|
||
const EN_TO_RU = {};
|
||
for (const [ru, en] of Object.entries(RU_TO_EN)) EN_TO_RU[en] = ru;
|
||
|
||
function swap(s, map) { let o=''; for(const c of s) o+=map[c]??c; return o; }
|
||
function ruToEn(s) { return swap(s, RU_TO_EN); }
|
||
function enToRu(s) { return swap(s, EN_TO_RU); }
|
||
function expandKeyboardVariants(query) {
|
||
const v = new Set([query]);
|
||
const en = ruToEn(query); if(en!==query) v.add(en);
|
||
const ru = enToRu(query); if(ru!==query) v.add(ru);
|
||
for(const x of [...v]) v.add(x.toLowerCase());
|
||
return [...v];
|
||
}
|
||
|
||
// ─── Test runner ─────────────────────────────────────────────
|
||
let passed = 0, failed = 0;
|
||
function assert(label, actual, expected) {
|
||
if (actual === expected) {
|
||
passed++;
|
||
} else {
|
||
failed++;
|
||
console.error(`FAIL: ${label}`);
|
||
console.error(` actual: ${JSON.stringify(actual)}`);
|
||
console.error(` expected: ${JSON.stringify(expected)}`);
|
||
}
|
||
}
|
||
function assertArray(label, actual, expected) {
|
||
const a = JSON.stringify(actual.sort());
|
||
const e = JSON.stringify(expected.sort());
|
||
if (a === e) {
|
||
passed++;
|
||
} else {
|
||
failed++;
|
||
console.error(`FAIL: ${label}`);
|
||
console.error(` actual: ${a}`);
|
||
console.error(` expected: ${e}`);
|
||
}
|
||
}
|
||
|
||
// ─── EN → RU: user typed Latin chars on Russian layout ──────
|
||
console.log('=== EN → RU (latin on RU layout) ===');
|
||
assert('dthcnfr → верстак', enToRu('dthcnfr'), 'верстак');
|
||
assert('ghbdtn → привет', enToRu('ghbdtn'), 'привет');
|
||
assert('ntcn → тест', enToRu('ntcn'), 'тест');
|
||
|
||
// Verify each character mapping individually
|
||
assert('d → в', enToRu('d'), 'в');
|
||
assert('t → е', enToRu('t'), 'е');
|
||
assert('h → р', enToRu('h'), 'р');
|
||
assert('c → с', enToRu('c'), 'с'); // Latin c → Cyrillic с (NOT ч!)
|
||
assert('n → т', enToRu('n'), 'т');
|
||
assert('f → а', enToRu('f'), 'а');
|
||
assert('r → к', enToRu('r'), 'к');
|
||
|
||
// ─── RU → EN: user typed Cyrillic chars on English layout ───
|
||
console.log('=== RU → EN (cyrillic on EN layout) ===');
|
||
assert('руддщ → hello', ruToEn('руддщ'), 'hello');
|
||
assert('цщкдв → world', ruToEn('цщкдв'), 'world');
|
||
assert('ышеш → siti', ruToEn('ышеш'), 'siti');
|
||
|
||
// Verify each character mapping individually
|
||
assert('р → h', ruToEn('р'), 'h');
|
||
assert('у → e', ruToEn('у'), 'e');
|
||
assert('д → l', ruToEn('д'), 'l');
|
||
assert('щ → o', ruToEn('щ'), 'o');
|
||
assert('ц → w', ruToEn('ц'), 'w');
|
||
assert('к → r', ruToEn('к'), 'r');
|
||
assert('в → d', ruToEn('в'), 'd');
|
||
|
||
// ─── Critical: Latin c vs Cyrillic с ────────────────────────
|
||
console.log('=== Unicode safety: Latin c ≠ Cyrillic с ===');
|
||
const latinC = 'c';
|
||
const cyrillicC = 'с'; // U+0441
|
||
assert('Latin c !== Cyrillic с', latinC === cyrillicC, false);
|
||
assert('Latin c charCode', latinC.charCodeAt(0), 99);
|
||
assert('Cyrillic с charCode', cyrillicC.charCodeAt(0), 1089); // 0x0441
|
||
assert('enToRu(c) → с (Cyrillic)', enToRu('c'), 'с');
|
||
assert('enToRu(c) is Cyrillic с', enToRu('c').charCodeAt(0), 1089);
|
||
assert('ruToEn(с) → c (Latin)', ruToEn('с'), 'c');
|
||
assert('ruToEn(с) is Latin c', ruToEn('с').charCodeAt(0), 99);
|
||
|
||
// ─── expandKeyboardVariants ─────────────────────────────────
|
||
console.log('=== expandKeyboardVariants ===');
|
||
assertArray('dthcnfr variants', expandKeyboardVariants('dthcnfr'), ['dthcnfr', 'верстак']);
|
||
assertArray('руддщ variants', expandKeyboardVariants('руддщ'), ['руддщ', 'hello']);
|
||
assertArray('верстак variants', expandKeyboardVariants('верстак'), ['верстак', 'dthcnfr']);
|
||
assertArray('hello variants', expandKeyboardVariants('hello'), ['hello', 'руддщ']);
|
||
// Screen contains chars that map to RU: S→Ы, c→с, r→к, e→у, n→т
|
||
assertArray('Screen variants (EN+RU swap)', expandKeyboardVariants('Screen'), ['Screen', 'screen', 'Ыскуут', 'ыскуут']);
|
||
|
||
// ─── Edge cases ─────────────────────────────────────────────
|
||
console.log('=== Edge cases ===');
|
||
assert('empty string', enToRu(''), '');
|
||
assert('single char d', enToRu('d'), 'в');
|
||
assert('already correct RU', ruToEn('верстак'), 'dthcnfr');
|
||
assert('already correct EN', enToRu('hello'), 'руддщ');
|
||
assert('mixed case EN: DthCnFr → ВерСтАк', enToRu('DthCnFr'), 'ВерСтАк');
|
||
assert('numbers pass through', enToRu('123'), '123');
|
||
assert('spaces pass through', enToRu('dth cnfr'), 'вер стак');
|
||
|
||
// ─── Summary ─────────────────────────────────────────────────
|
||
console.log();
|
||
console.log(`Results: ${passed} passed, ${failed} failed`);
|
||
if (failed > 0) {
|
||
process.exit(1);
|
||
} else {
|
||
console.log('ALL TESTS PASSED');
|
||
}
|