From 7198129578277fe091d3d05c8c505fd5f00ca2f0 Mon Sep 17 00:00:00 2001 From: Donnie Date: Wed, 14 Apr 2021 16:43:27 -0700 Subject: [PATCH] Fix tests by removing sequence matcher dependency on lit-html --- src/common/string/filter/sequence-matching.ts | 59 +++++++---- src/dialogs/quick-bar/ha-quick-bar.ts | 40 ++++---- .../common/string/sequence_matching.test.ts | 97 ++++++++++++------- 3 files changed, 122 insertions(+), 74 deletions(-) diff --git a/src/common/string/filter/sequence-matching.ts b/src/common/string/filter/sequence-matching.ts index cc18217375..dafc799099 100644 --- a/src/common/string/filter/sequence-matching.ts +++ b/src/common/string/filter/sequence-matching.ts @@ -1,6 +1,4 @@ -import { html, TemplateResult } from "lit-html"; import { createMatches, FuzzyScore, fuzzyScore } from "./filter"; -import { unsafeHTML } from "lit-html/directives/unsafe-html"; /** * Determine whether a sequence of letters exists in another string, @@ -14,12 +12,17 @@ import { unsafeHTML } from "lit-html/directives/unsafe-html"; type FuzzySequentialMatcher = ( filter: string, - item: ScorableTextItem + item: ScorableTextItem, + decorate?: MatchDecorator ) => ScorableTextItem | undefined; -export const fuzzySequentialMatch: FuzzySequentialMatcher = (filter, item) => { +export const fuzzySequentialMatch: FuzzySequentialMatcher = ( + filter, + item, + decorate = createMatchDecorator("[", "]") +) => { let topScore = Number.NEGATIVE_INFINITY; - const decoratedStrings: TemplateResult[][] = []; + const decoratedStrings: string[][] = []; for (const word of item.strings) { const scores = fuzzyScore( @@ -32,7 +35,9 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = (filter, item) => { true ); - decoratedStrings.push(decorateMatch(word, scores)); + if (decorate) { + decoratedStrings.push(decorate(word, scores)); + } if (!scores) { continue; @@ -75,18 +80,23 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = (filter, item) => { export interface ScorableTextItem { score?: number; strings: string[]; - decoratedStrings?: TemplateResult[][]; + decoratedStrings?: string[][]; } type FuzzyFilterSort = ( filter: string, - items: T[] + items: T[], + decorate?: MatchDecorator ) => T[]; -export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => { +export const fuzzyFilterSort: FuzzyFilterSort = ( + filter, + items, + decorate = createMatchDecorator("[", "]") +) => { return items .map((item) => { - const match = fuzzySequentialMatch(filter, item); + const match = fuzzySequentialMatch(filter, item, decorate); item.score = match?.score; item.decoratedStrings = match?.decoratedStrings; @@ -99,32 +109,39 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => { ); }; -type MatchDecorator = (word: string, scores?: FuzzyScore) => TemplateResult[]; -export const decorateMatch: MatchDecorator = (word, scores) => { +type MatchDecorator = (word: string, scores?: FuzzyScore) => string[]; +export const createMatchDecorator: ( + left: string, + right: string +) => MatchDecorator = (left, right) => (word, scores) => + _decorateMatch(word, [left, right], scores); + +const _decorateMatch: ( + word: string, + surroundWith: [string, string], + scores?: FuzzyScore +) => string[] = (word, surroundWith, scores) => { if (!scores) { - return [html`${word}`]; + return [word]; } - const decoratedText: TemplateResult[] = []; + const decoratedText: string[] = []; const matches = createMatches(scores); + const [left, right] = surroundWith; let pos = 0; let actualWord = ""; for (const match of matches) { actualWord += word.substring(pos, match.start); - actualWord += `${word.substring( - match.start, - match.end - )}`; + actualWord += `${left}${word.substring(match.start, match.end)}${right}`; pos = match.end; } actualWord += word.substring(pos); const fragments = actualWord.split("::"); - for (let i = 0; i < fragments.length; i++) { - const fragment = fragments[i]; - decoratedText.push(html`${unsafeHTML(fragment)}`); + for (const fragment of fragments) { + decoratedText.push(fragment); } return decoratedText; diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 780e79ff62..fe49937964 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -34,6 +34,7 @@ import { navigate } from "../../common/navigate"; import "../../common/search/search-input"; import { compare } from "../../common/string/compare"; import { + createMatchDecorator, fuzzyFilterSort, ScorableTextItem, } from "../../common/string/filter/sequence-matching"; @@ -54,6 +55,7 @@ import { import { QuickBarParams } from "./show-dialog-quick-bar"; import "../../components/ha-chip"; import { toTitleCase } from "../../common/string/casing"; +import { unsafeHTML } from "lit-html/directives/unsafe-html"; interface QuickBarItem extends ScorableTextItem { primaryText: string; @@ -75,10 +77,6 @@ const isCommandItem = (item: QuickBarItem): item is CommandItem => { return (item as CommandItem).categoryKey !== undefined; }; -const isEntityItem = (item: QuickBarItem): item is EntityItem => { - return !isCommandItem(item); -}; - interface QuickBarNavigationItem extends CommandItem { path: string; } @@ -259,16 +257,16 @@ export class QuickBar extends LitElement { slot="graphic" >`} ${item.decoratedWords - ? item.decoratedWords[0] + >${item.decoratedStrings + ? unsafeHTML(item.decoratedStrings[0]) : item.primaryText} ${item.altText ? html` ${item.decoratedWords - ? item.decoratedWords[1] + >${item.decoratedStrings + ? unsafeHTML(item.decoratedStrings[1]) : item.altText} @@ -279,7 +277,7 @@ export class QuickBar extends LitElement { } private _renderCommandItem(item: CommandItem, index?: number) { - const decoratedItem = item.decoratedWords && item.decoratedWords[0]; + const decoratedItem = item.decoratedStrings && item.decoratedStrings[0]; return html` ` : ""} - ${decoratedItem ? decoratedItem[0] : item.categoryText} ${decoratedItem ? decoratedItem[1] : item.primaryText}${decoratedItem + ? unsafeHTML(decoratedItem[1]) + : item.primaryText} `; @@ -384,11 +386,11 @@ export class QuickBar extends LitElement { private _resetDecorations() { this._entityItems = this._entityItems?.map((item) => ({ ...item, - decoratedWords: undefined, + decoratedStrings: undefined, })); this._commandItems = this._commandItems?.map((item) => ({ ...item, - decoratedWords: undefined, + decoratedStrings: undefined, })); } @@ -474,7 +476,7 @@ export class QuickBar extends LitElement { return { ...commandItem, categoryKey: "reload", - strings: [`${commandItem.categoryText} ${commandItem.primaryText}`], + strings: [`${commandItem.categoryText}::${commandItem.primaryText}`], }; }); } @@ -508,7 +510,7 @@ export class QuickBar extends LitElement { return this._generateConfirmationCommand( { ...item, - strings: [`${item.categoryText} ${item.primaryText}`], + strings: [`${item.categoryText}::${item.primaryText}`], }, this.hass.localize("ui.dialogs.generic.ok") ); @@ -609,7 +611,7 @@ export class QuickBar extends LitElement { return { ...navItem, - strings: [`${navItem.categoryText} ${navItem.primaryText}`], + strings: [`${navItem.categoryText}::${navItem.primaryText}`], categoryKey, }; }); @@ -621,7 +623,11 @@ export class QuickBar extends LitElement { private _filterItems = memoizeOne( (items: QuickBarItem[], filter: string): QuickBarItem[] => { - return fuzzyFilterSort(filter.trimLeft(), items); + return fuzzyFilterSort( + filter.trimLeft(), + items, + createMatchDecorator("", "") + ); } ); diff --git a/test-mocha/common/string/sequence_matching.test.ts b/test-mocha/common/string/sequence_matching.test.ts index fe2482132d..f8eb947470 100644 --- a/test-mocha/common/string/sequence_matching.test.ts +++ b/test-mocha/common/string/sequence_matching.test.ts @@ -8,25 +8,24 @@ import { type CreateExpectation = ( pattern: string, - score: ScorableTextItem["score"], - strings?: ScorableTextItem["strings"], - decoratedStrings?: ScorableTextItem["decoratedStrings"] + expScore: number, + expDecorated: string ) => { pattern: string; - expected: ScorableTextItem; + expected: { + score: number; + decoratedString: string; + }; }; - const createExpectation: CreateExpectation = ( pattern, - score, - strings = [], - decoratedStrings = [] + expScore, + expDecorated ) => ({ pattern, expected: { - score, - strings, - decoratedStrings, + score: expScore, + decoratedString: expDecorated, }, }); @@ -36,25 +35,25 @@ describe("fuzzySequentialMatch", () => { }; const shouldMatchEntity = [ - createExpectation("automation.ticker", 131), - createExpectation("automation.ticke", 121), - createExpectation("automation.", 82), - createExpectation("au", 10), - createExpectation("automationticker", 85), - createExpectation("tion.tick", 8), - createExpectation("ticker", -4), - createExpectation("automation.r", 73), - createExpectation("tick", -8), - createExpectation("aumatick", 9), - createExpectation("aion.tck", 4), - createExpectation("ioticker", -4), - createExpectation("atmto.ikr", -34), - createExpectation("uoaintce", -39), - createExpectation("au.tce", -3), - createExpectation("tomaontkr", -19), - createExpectation("s", 1), - createExpectation("stocks", 42), - createExpectation("sks", -5), + createExpectation("automation.ticker", 131, "[automation.ticker]"), + createExpectation("automation.ticke", 121, "[automation.ticke]r"), + createExpectation("automation.", 82, "[automation.]ticker"), + createExpectation("au", 10, "[au]tomation.ticker"), + createExpectation("automationticker", 85, "[automation].[ticker]"), + createExpectation("tion.tick", 8, "automa[tion.tick]er"), + createExpectation("ticker", -4, "automation.[ticker]"), + createExpectation("automation.r", 73, "[automation.]ticke[r]"), + createExpectation("tick", -8, "automation.[tick]er"), + createExpectation("aumatick", 9, "[au]to[ma]tion.[tick]er"), + createExpectation("aion.tck", 4, "[a]utomat[ion.t]i[ck]er"), + createExpectation("ioticker", -4, "automat[io]n.[ticker]"), + createExpectation("atmto.ikr", -34, "[a]u[t]o[m]a[t]i[o]n[.]t[i]c[k]e[r]"), + createExpectation("uoaintce", -39, "a[u]t[o]m[a]t[i]o[n].[t]i[c]k[e]r"), + createExpectation("au.tce", -3, "[au]tomation[.t]i[c]k[e]r"), + createExpectation("tomaontkr", -19, "au[toma]ti[on].[t]ic[k]e[r]"), + createExpectation("s", 1, "[S]tocks"), + createExpectation("stocks", 42, "[Stocks]"), + createExpectation("sks", -5, "[S]toc[ks]"), ]; const shouldNotMatchEntity = [ @@ -71,9 +70,16 @@ describe("fuzzySequentialMatch", () => { describe(`Entity '${item.strings[0]}'`, () => { for (const expectation of shouldMatchEntity) { - it(`matches '${expectation.pattern}' with return of '${expectation.expected}'`, () => { + it(`matches '${expectation.pattern}' with score of '${expectation.expected?.score}'`, () => { const res = fuzzySequentialMatch(expectation.pattern, item); - assert.equal(res, expectation.expected); + assert.equal(res?.score, expectation.expected?.score); + }); + + it(`decorates '${expectation.pattern}' as '${expectation.expected?.decoratedString}'`, () => { + const res = fuzzySequentialMatch(expectation.pattern, item); + assert.includeDeepMembers(res!.decoratedStrings!, [ + [expectation.expected!.decoratedString!], + ]); }); } @@ -118,10 +124,29 @@ describe("fuzzyFilterSort", () => { it(`filters and sorts correctly`, () => { const expectedItemsAfterFilter = [ - { ...ticker, score: 44 }, - { ...sensorTicker, score: 1 }, - { ...automationTicker, score: -4 }, - { ...timerCheckRouter, score: -8 }, + { + ...ticker, + score: 44, + decoratedStrings: [["[ticker]"], ["Just [ticker]"]], + }, + { + ...sensorTicker, + score: 1, + decoratedStrings: [["sensor.[ticker]"], ["Stocks up"]], + }, + { + ...automationTicker, + score: -4, + decoratedStrings: [["automation.[ticker]"], ["Stocks"]], + }, + { + ...timerCheckRouter, + score: -8, + decoratedStrings: [ + ["automa[ti]on.[c]hec[k]_rout[er]"], + ["[Ti]mer [C]hec[k] Rout[er]"], + ], + }, ]; const res = fuzzyFilterSort(filter, itemsBeforeFilter);