import { describe, expect, test } from 'bun:test'; import { rank } from './match'; import type { SpotlightItem } from './useSpotlightItems'; // The behaviours a launcher is judged on, which are all about ORDER rather than about matching. Anyone // can return the right set; returning it with the right thing first is the whole product. const item = (label: string, to: string, hint?: string): SpotlightItem => ({ id: to, label, to, hint }); const ITEMS: SpotlightItem[] = [ item('Home', '/'), item('Files', '/files'), item('Chat', '/chat'), item('Command HQ', '/command-hq'), item('Editor', '/code-editor'), item('Profile', '/settings/profile', 'avatar, name, email'), item('API keys', '/settings/profile/api-keys', 'per-app keys'), item('App store', '/app-store'), item('Dashboards', '/dashboards'), ]; const top = (query: string, n = 1) => rank(ITEMS, query) .slice(0, n) .map((s) => s.item.label); describe('spotlight ranking', () => { test('an exact label wins', () => { expect(top('chat')).toEqual(['Chat']); }); test('a prefix beats a scattered match — "fi" is Files, not Profile', () => { expect(top('fi')).toEqual(['Files']); }); test('initials find a two-word label — "chq" is Command HQ', () => { expect(top('chq')).toEqual(['Command HQ']); }); test('a subsequence works, so you can type the shape of a word', () => { expect(top('apik')).toEqual(['API keys']); }); test('a hint matches, but never outranks a label', () => { // "keys" is API keys' own label AND its hint. It must not be beaten by anything mentioning keys. expect(top('keys')).toEqual(['API keys']); }); test('a path fragment finds the item without outranking labels', () => { expect(rank(ITEMS, 'settings').map((s) => s.item.label)).toContain('Profile'); // "app" is a prefix of "App store" and appears in API keys' hint ("per-app keys"). Label wins. expect(top('app')).toEqual(['App store']); }); test('nothing matches means nothing returned, not everything', () => { expect(rank(ITEMS, 'zzzzz')).toHaveLength(0); }); test('an empty query keeps the given order rather than sorting it', () => { expect(rank(ITEMS, '').map((s) => s.item.label)).toEqual(ITEMS.map((i) => i.label)); }); test('shorter labels break ties — "Chat" over "Chat history"', () => { const withLonger = [...ITEMS, item('Chat history', '/chat-history')]; expect(rank(withLonger, 'chat')[0]!.item.label).toBe('Chat'); }); });