The rule editor's value box built its suggestions from libraryStore's whole-library arrays, which made it one more reason to load every track at startup, and left it empty when they had not loaded. SuggestSmartPlaylistValues answers with up to 50 distinct values of a field that contain the typed text, from the same joined row the rules match against, so a suggestion is always something a rule can match. yj-combobox announces its filter text so the editor can ask, debounced and cached per field and text. Refs #279
87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
/**
|
|
* The rule editor's value box suggests values from the library by
|
|
* asking for the ones that match what has been typed (#279).
|
|
*
|
|
* It used to build the lists from `libraryStore`'s whole-library
|
|
* arrays: one more reason to load every track at startup, and an empty
|
|
* list whenever they had not loaded. These tests stub *no* library
|
|
* collection — what is asserted is that the suggestions arrive anyway,
|
|
* from the one call made for them.
|
|
*/
|
|
import { describe, expect, it, beforeEach } from 'vitest';
|
|
|
|
import '@components/smart-playlist-editor/smart-playlist-editor';
|
|
import type { YjCombobox } from '@components/combobox/combobox';
|
|
import { calls, resetHarness, stub } from '@test/support/harness';
|
|
import { fixture, shadow, shadowAll } from '@test/support/render';
|
|
import type { LitElement } from 'lit';
|
|
|
|
const SUGGEST = 'playlist.Service.SuggestSmartPlaylistValues';
|
|
|
|
async function editor(field: string): Promise<LitElement> {
|
|
return fixture<LitElement>('smart-playlist-editor', {
|
|
rules: JSON.stringify({ rules: [{ field, operator: 'contains', value: '' }] }),
|
|
});
|
|
}
|
|
|
|
/** The row's second combobox: the value box (the first picks the field). */
|
|
function valueBox(el: LitElement): YjCombobox {
|
|
return shadowAll<YjCombobox>(el, 'yj-combobox')[1]!;
|
|
}
|
|
|
|
async function type(box: YjCombobox, text: string): Promise<void> {
|
|
const input = shadow<HTMLInputElement>(box, 'input')!;
|
|
|
|
input.focus();
|
|
input.value = text;
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
await box.updateComplete;
|
|
}
|
|
|
|
describe('smart playlist value suggestions', () => {
|
|
beforeEach(() => {
|
|
resetHarness();
|
|
stub(SUGGEST, (field: string, needle: string) =>
|
|
[`${field}:${needle}:1`, `${field}:${needle}:2`],
|
|
);
|
|
});
|
|
|
|
it('asks for the values matching what was typed, once typing pauses', async () => {
|
|
const el = await editor('artist');
|
|
const box = valueBox(el);
|
|
|
|
await type(box, 'q');
|
|
await type(box, 'qu');
|
|
|
|
await expect.poll(() => box.options).toEqual(['artist:qu:1', 'artist:qu:2']);
|
|
// Debounced: the pause after "qu" asked, the keystroke before did not.
|
|
expect(calls(SUGGEST).map((c) => c.args)).toEqual([['artist', 'qu']]);
|
|
});
|
|
|
|
it('answers a repeat from what it already fetched', async () => {
|
|
const el = await editor('album');
|
|
const box = valueBox(el);
|
|
|
|
await type(box, 'x');
|
|
await expect.poll(() => box.options).toEqual(['album:x:1', 'album:x:2']);
|
|
await type(box, 'xy');
|
|
await expect.poll(() => box.options).toEqual(['album:xy:1', 'album:xy:2']);
|
|
await type(box, 'x');
|
|
|
|
expect(box.options).toEqual(['album:x:1', 'album:x:2']);
|
|
expect(calls(SUGGEST)).toHaveLength(2);
|
|
});
|
|
|
|
it('asks nothing for a field that has no suggestions', async () => {
|
|
const el = await editor('duration');
|
|
const boxes = shadowAll<YjCombobox>(el, 'yj-combobox');
|
|
|
|
// A numeric range field may render no value combobox at all; if it
|
|
// does, typing in it must not ask.
|
|
if (boxes[1]) await type(boxes[1], '3');
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
|
|
expect(calls(SUGGEST)).toHaveLength(0);
|
|
});
|
|
});
|