mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-03 06:27:47 +00:00
Fix tests by removing sequence matcher dependency on lit-html
This commit is contained in:
parent
12ce2e6ed9
commit
7198129578
@ -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;
|
||||||
|
@ -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>")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user