diff --git a/plugins/toolbox-search/src/block_searcher.ts b/plugins/toolbox-search/src/block_searcher.ts index d04f9b53f..005974b73 100644 --- a/plugins/toolbox-search/src/block_searcher.ts +++ b/plugins/toolbox-search/src/block_searcher.ts @@ -113,7 +113,7 @@ export class BlockSearcher { if (normalizedInput.length <= 3) return [normalizedInput]; const trigrams: string[] = []; - for (let start = 0; start < normalizedInput.length - 3; start++) { + for (let start = 0; start <= normalizedInput.length - 3; start++) { trigrams.push(normalizedInput.substring(start, start + 3)); } diff --git a/plugins/toolbox-search/test/tests.mocha.js b/plugins/toolbox-search/test/tests.mocha.js index 3699ee177..1c4ae96f0 100644 --- a/plugins/toolbox-search/test/tests.mocha.js +++ b/plugins/toolbox-search/test/tests.mocha.js @@ -15,6 +15,15 @@ suite('Toolbox search', () => { }); suite('BlockSearcher', () => { + test('generateTrigrams handles empty and short input', () => { + const searcher = new BlockSearcher(); + const generateTrigrams = searcher.generateTrigrams.bind(searcher); + + assert.deepEqual(generateTrigrams(''), []); + assert.deepEqual(generateTrigrams('a'), ['a']); + assert.deepEqual(generateTrigrams('abc'), ['abc']); + }); + test('indexes the default value of dropdown fields', () => { const searcher = new BlockSearcher(); const blocks = [ @@ -57,6 +66,119 @@ suite('BlockSearcher', () => { assert.sameMembers(ransomNoteMatches, [listCreateWithBlock]); }); + test('requires the final trigram when matching longer queries', () => { + const searcher = new BlockSearcher(); + const mathConstrainBlock = { + kind: 'block', + type: 'math_constrain', + }; + searcher.indexBlocks([mathConstrainBlock]); + + const matches = searcher.blockTypesMatching('conso'); + + assert.notInclude( + matches, + mathConstrainBlock, + 'query missing trailing trigram should not match', + ); + }); + + test('normalizes underscores in block types to spaces', () => { + if (!Blockly.Blocks['searcher_underscore_block']) { + Blockly.defineBlocksWithJsonArray([ + { + type: 'searcher_underscore_block', + message0: 'custom block with underscore', + }, + ]); + } + + const searcher = new BlockSearcher(); + const blockInfo = { + kind: 'block', + type: 'searcher_underscore_block', + }; + searcher.indexBlocks([blockInfo]); + + assert.sameMembers( + searcher.blockTypesMatching('custom block with underscore'), + [blockInfo], + ); + assert.isEmpty(searcher.blockTypesMatching('custom_block_with_underscore')); + }); + + test('longer queries disambiguate similar blocks', () => { + if (!Blockly.Blocks['searcher_charlie']) { + Blockly.defineBlocksWithJsonArray([ + { + type: 'searcher_charlie', + message0: 'alpha bravo charlie', + }, + { + type: 'searcher_delta', + message0: 'alpha bravo delta', + }, + ]); + } + + const searcher = new BlockSearcher(); + const blockA = {kind: 'block', type: 'searcher_charlie'}; + const blockB = {kind: 'block', type: 'searcher_delta'}; + + searcher.indexBlocks([blockA, blockB]); + + const broadQueryMatches = searcher.blockTypesMatching('alpha bravo'); + assert.sameMembers(broadQueryMatches, [blockA, blockB]); + + const specificQueryMatches = + searcher.blockTypesMatching('alpha bravo charlie'); + assert.sameMembers(specificQueryMatches, [blockA]); + }); + + test('indexes dropdown alt text options', () => { + if (!Blockly.Blocks['searcher_dropdown_alt']) { + Blockly.defineBlocksWithJsonArray([ + { + type: 'searcher_dropdown_alt', + message0: 'weather %1', + args0: [ + { + type: 'field_dropdown', + name: 'WEATHER', + options: [ + [ + { + src: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEA', + width: 1, + height: 1, + alt: 'Sunny', + }, + 'SUN', + ], + [ + { + src: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEA', + width: 1, + height: 1, + alt: 'Cloudy', + }, + 'CLOUD', + ], + ], + }, + ], + }, + ]); + } + + const searcher = new BlockSearcher(); + const blockInfo = {kind: 'block', type: 'searcher_dropdown_alt'}; + searcher.indexBlocks([blockInfo]); + + assert.sameMembers(searcher.blockTypesMatching('sunny'), [blockInfo]); + assert.sameMembers(searcher.blockTypesMatching('cloudy'), [blockInfo]); + }); + test('returns an empty list when no matches are found', () => { const searcher = new BlockSearcher(); assert.isEmpty(searcher.blockTypesMatching('abc123'));