Fix tests by removing sequence matcher dependency on lit-html

This commit is contained in:
Donnie 2021-04-14 16:43:27 -07:00
parent 12ce2e6ed9
commit 7198129578
3 changed files with 122 additions and 74 deletions

View File

@ -1,6 +1,4 @@
import { html, TemplateResult } from "lit-html";
import { createMatches, FuzzyScore, fuzzyScore } from "./filter"; import { createMatches, FuzzyScore, fuzzyScore } from "./filter";
import { unsafeHTML } from "lit-html/directives/unsafe-html";
/** /**
* Determine whether a sequence of letters exists in another string, * Determine whether a sequence of letters exists in another string,
@ -14,12 +12,17 @@ import { unsafeHTML } from "lit-html/directives/unsafe-html";
type FuzzySequentialMatcher = ( type FuzzySequentialMatcher = (
filter: string, filter: string,
item: ScorableTextItem item: ScorableTextItem,
decorate?: MatchDecorator
) => ScorableTextItem | undefined; ) => ScorableTextItem | undefined;
export const fuzzySequentialMatch: FuzzySequentialMatcher = (filter, item) => { export const fuzzySequentialMatch: FuzzySequentialMatcher = (
filter,
item,
decorate = createMatchDecorator("[", "]")
) => {
let topScore = Number.NEGATIVE_INFINITY; let topScore = Number.NEGATIVE_INFINITY;
const decoratedStrings: TemplateResult[][] = []; const decoratedStrings: string[][] = [];
for (const word of item.strings) { for (const word of item.strings) {
const scores = fuzzyScore( const scores = fuzzyScore(
@ -32,7 +35,9 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = (filter, item) => {
true true
); );
decoratedStrings.push(decorateMatch(word, scores)); if (decorate) {
decoratedStrings.push(decorate(word, scores));
}
if (!scores) { if (!scores) {
continue; continue;
@ -75,18 +80,23 @@ export const fuzzySequentialMatch: FuzzySequentialMatcher = (filter, item) => {
export interface ScorableTextItem { export interface ScorableTextItem {
score?: number; score?: number;
strings: string[]; strings: string[];
decoratedStrings?: TemplateResult[][]; decoratedStrings?: string[][];
} }
type FuzzyFilterSort = <T extends ScorableTextItem>( type FuzzyFilterSort = <T extends ScorableTextItem>(
filter: string, filter: string,
items: T[] items: T[],
decorate?: MatchDecorator
) => T[]; ) => T[];
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => { export const fuzzyFilterSort: FuzzyFilterSort = (
filter,
items,
decorate = createMatchDecorator("[", "]")
) => {
return items return items
.map((item) => { .map((item) => {
const match = fuzzySequentialMatch(filter, item); const match = fuzzySequentialMatch(filter, item, decorate);
item.score = match?.score; item.score = match?.score;
item.decoratedStrings = match?.decoratedStrings; item.decoratedStrings = match?.decoratedStrings;
@ -99,32 +109,39 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
); );
}; };
type MatchDecorator = (word: string, scores?: FuzzyScore) => TemplateResult[]; type MatchDecorator = (word: string, scores?: FuzzyScore) => string[];
export const decorateMatch: MatchDecorator = (word, scores) => { 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) { if (!scores) {
return [html`${word}`]; return [word];
} }
const decoratedText: TemplateResult[] = []; const decoratedText: string[] = [];
const matches = createMatches(scores); const matches = createMatches(scores);
const [left, right] = surroundWith;
let pos = 0; let pos = 0;
let actualWord = ""; let actualWord = "";
for (const match of matches) { for (const match of matches) {
actualWord += word.substring(pos, match.start); actualWord += word.substring(pos, match.start);
actualWord += `<span class="highlight-letter">${word.substring( actualWord += `${left}${word.substring(match.start, match.end)}${right}`;
match.start,
match.end
)}</span>`;
pos = match.end; pos = match.end;
} }
actualWord += word.substring(pos); actualWord += word.substring(pos);
const fragments = actualWord.split("::"); const fragments = actualWord.split("::");
for (let i = 0; i < fragments.length; i++) { for (const fragment of fragments) {
const fragment = fragments[i]; decoratedText.push(fragment);
decoratedText.push(html`${unsafeHTML(fragment)}`);
} }
return decoratedText; return decoratedText;

View File

@ -34,6 +34,7 @@ import { navigate } from "../../common/navigate";
import "../../common/search/search-input"; import "../../common/search/search-input";
import { compare } from "../../common/string/compare"; import { compare } from "../../common/string/compare";
import { import {
createMatchDecorator,
fuzzyFilterSort, fuzzyFilterSort,
ScorableTextItem, ScorableTextItem,
} from "../../common/string/filter/sequence-matching"; } from "../../common/string/filter/sequence-matching";
@ -54,6 +55,7 @@ import {
import { QuickBarParams } from "./show-dialog-quick-bar"; import { QuickBarParams } from "./show-dialog-quick-bar";
import "../../components/ha-chip"; import "../../components/ha-chip";
import { toTitleCase } from "../../common/string/casing"; import { toTitleCase } from "../../common/string/casing";
import { unsafeHTML } from "lit-html/directives/unsafe-html";
interface QuickBarItem extends ScorableTextItem { interface QuickBarItem extends ScorableTextItem {
primaryText: string; primaryText: string;
@ -75,10 +77,6 @@ const isCommandItem = (item: QuickBarItem): item is CommandItem => {
return (item as CommandItem).categoryKey !== undefined; return (item as CommandItem).categoryKey !== undefined;
}; };
const isEntityItem = (item: QuickBarItem): item is EntityItem => {
return !isCommandItem(item);
};
interface QuickBarNavigationItem extends CommandItem { interface QuickBarNavigationItem extends CommandItem {
path: string; path: string;
} }
@ -259,16 +257,16 @@ export class QuickBar extends LitElement {
slot="graphic" slot="graphic"
></ha-icon>`} ></ha-icon>`}
<span class="item-text primary" <span class="item-text primary"
>${item.decoratedWords >${item.decoratedStrings
? item.decoratedWords[0] ? unsafeHTML(item.decoratedStrings[0])
: item.primaryText}</span : item.primaryText}</span
> >
${item.altText ${item.altText
? html` ? html`
<span slot="secondary" class="item-text secondary"> <span slot="secondary" class="item-text secondary">
<span <span
>${item.decoratedWords >${item.decoratedStrings
? item.decoratedWords[1] ? unsafeHTML(item.decoratedStrings[1])
: item.altText}</span : item.altText}</span
></span ></span
> >
@ -279,7 +277,7 @@ export class QuickBar extends LitElement {
} }
private _renderCommandItem(item: CommandItem, index?: number) { private _renderCommandItem(item: CommandItem, index?: number) {
const decoratedItem = item.decoratedWords && item.decoratedWords[0]; const decoratedItem = item.decoratedStrings && item.decoratedStrings[0];
return html` return html`
<mwc-list-item <mwc-list-item
@ -300,12 +298,16 @@ export class QuickBar extends LitElement {
slot="icon" slot="icon"
></ha-svg-icon>` ></ha-svg-icon>`
: ""} : ""}
${decoratedItem ? decoratedItem[0] : item.categoryText}</ha-chip ${decoratedItem
? unsafeHTML(decoratedItem[0])
: item.categoryText}</ha-chip
> >
</span> </span>
<span class="command-text" <span class="command-text"
>${decoratedItem ? decoratedItem[1] : item.primaryText}</span >${decoratedItem
? unsafeHTML(decoratedItem[1])
: item.primaryText}</span
> >
</mwc-list-item> </mwc-list-item>
`; `;
@ -384,11 +386,11 @@ export class QuickBar extends LitElement {
private _resetDecorations() { private _resetDecorations() {
this._entityItems = this._entityItems?.map((item) => ({ this._entityItems = this._entityItems?.map((item) => ({
...item, ...item,
decoratedWords: undefined, decoratedStrings: undefined,
})); }));
this._commandItems = this._commandItems?.map((item) => ({ this._commandItems = this._commandItems?.map((item) => ({
...item, ...item,
decoratedWords: undefined, decoratedStrings: undefined,
})); }));
} }
@ -474,7 +476,7 @@ export class QuickBar extends LitElement {
return { return {
...commandItem, ...commandItem,
categoryKey: "reload", categoryKey: "reload",
strings: [`${commandItem.categoryText} ${commandItem.primaryText}`], strings: [`${commandItem.categoryText}::${commandItem.primaryText}`],
}; };
}); });
} }
@ -508,7 +510,7 @@ export class QuickBar extends LitElement {
return this._generateConfirmationCommand( return this._generateConfirmationCommand(
{ {
...item, ...item,
strings: [`${item.categoryText} ${item.primaryText}`], strings: [`${item.categoryText}::${item.primaryText}`],
}, },
this.hass.localize("ui.dialogs.generic.ok") this.hass.localize("ui.dialogs.generic.ok")
); );
@ -609,7 +611,7 @@ export class QuickBar extends LitElement {
return { return {
...navItem, ...navItem,
strings: [`${navItem.categoryText} ${navItem.primaryText}`], strings: [`${navItem.categoryText}::${navItem.primaryText}`],
categoryKey, categoryKey,
}; };
}); });
@ -621,7 +623,11 @@ export class QuickBar extends LitElement {
private _filterItems = memoizeOne( private _filterItems = memoizeOne(
(items: QuickBarItem[], filter: string): QuickBarItem[] => { (items: QuickBarItem[], filter: string): QuickBarItem[] => {
return fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items); return fuzzyFilterSort<QuickBarItem>(
filter.trimLeft(),
items,
createMatchDecorator("<span class='highlight-letter'>", "</span>")
);
} }
); );

View File

@ -8,25 +8,24 @@ import {
type CreateExpectation = ( type CreateExpectation = (
pattern: string, pattern: string,
score: ScorableTextItem["score"], expScore: number,
strings?: ScorableTextItem["strings"], expDecorated: string
decoratedStrings?: ScorableTextItem["decoratedStrings"]
) => { ) => {
pattern: string; pattern: string;
expected: ScorableTextItem; expected: {
score: number;
decoratedString: string;
};
}; };
const createExpectation: CreateExpectation = ( const createExpectation: CreateExpectation = (
pattern, pattern,
score, expScore,
strings = [], expDecorated
decoratedStrings = []
) => ({ ) => ({
pattern, pattern,
expected: { expected: {
score, score: expScore,
strings, decoratedString: expDecorated,
decoratedStrings,
}, },
}); });
@ -36,25 +35,25 @@ describe("fuzzySequentialMatch", () => {
}; };
const shouldMatchEntity = [ const shouldMatchEntity = [
createExpectation("automation.ticker", 131), createExpectation("automation.ticker", 131, "[automation.ticker]"),
createExpectation("automation.ticke", 121), createExpectation("automation.ticke", 121, "[automation.ticke]r"),
createExpectation("automation.", 82), createExpectation("automation.", 82, "[automation.]ticker"),
createExpectation("au", 10), createExpectation("au", 10, "[au]tomation.ticker"),
createExpectation("automationticker", 85), createExpectation("automationticker", 85, "[automation].[ticker]"),
createExpectation("tion.tick", 8), createExpectation("tion.tick", 8, "automa[tion.tick]er"),
createExpectation("ticker", -4), createExpectation("ticker", -4, "automation.[ticker]"),
createExpectation("automation.r", 73), createExpectation("automation.r", 73, "[automation.]ticke[r]"),
createExpectation("tick", -8), createExpectation("tick", -8, "automation.[tick]er"),
createExpectation("aumatick", 9), createExpectation("aumatick", 9, "[au]to[ma]tion.[tick]er"),
createExpectation("aion.tck", 4), createExpectation("aion.tck", 4, "[a]utomat[ion.t]i[ck]er"),
createExpectation("ioticker", -4), createExpectation("ioticker", -4, "automat[io]n.[ticker]"),
createExpectation("atmto.ikr", -34), createExpectation("atmto.ikr", -34, "[a]u[t]o[m]a[t]i[o]n[.]t[i]c[k]e[r]"),
createExpectation("uoaintce", -39), createExpectation("uoaintce", -39, "a[u]t[o]m[a]t[i]o[n].[t]i[c]k[e]r"),
createExpectation("au.tce", -3), createExpectation("au.tce", -3, "[au]tomation[.t]i[c]k[e]r"),
createExpectation("tomaontkr", -19), createExpectation("tomaontkr", -19, "au[toma]ti[on].[t]ic[k]e[r]"),
createExpectation("s", 1), createExpectation("s", 1, "[S]tocks"),
createExpectation("stocks", 42), createExpectation("stocks", 42, "[Stocks]"),
createExpectation("sks", -5), createExpectation("sks", -5, "[S]toc[ks]"),
]; ];
const shouldNotMatchEntity = [ const shouldNotMatchEntity = [
@ -71,9 +70,16 @@ describe("fuzzySequentialMatch", () => {
describe(`Entity '${item.strings[0]}'`, () => { describe(`Entity '${item.strings[0]}'`, () => {
for (const expectation of shouldMatchEntity) { 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); 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`, () => { it(`filters and sorts correctly`, () => {
const expectedItemsAfterFilter = [ const expectedItemsAfterFilter = [
{ ...ticker, score: 44 }, {
{ ...sensorTicker, score: 1 }, ...ticker,
{ ...automationTicker, score: -4 }, score: 44,
{ ...timerCheckRouter, score: -8 }, 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); const res = fuzzyFilterSort(filter, itemsBeforeFilter);