/** * 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 { return fixture('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(el, 'yj-combobox')[1]!; } async function type(box: YjCombobox, text: string): Promise { const input = shadow(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(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); }); });