feat(search): suggestions sans doublons + suggestions locales a la frappe
This commit is contained in:
@@ -4,7 +4,7 @@ import { firstValueFrom, of } from 'rxjs';
|
||||
import { Injector, runInInjectionContext } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { SuggestService } from './suggest.service';
|
||||
import { dedupeSort, mergeGroups, highlightParts } from './suggest.util';
|
||||
import { dedupeSort, mergeGroups, highlightParts, dedupeStrings, buildLocalQuerySuggestions } from './suggest.util';
|
||||
|
||||
/**
|
||||
* Step 15 — Unit tests for query typeahead (parsing / dedup / min-length guard).
|
||||
@@ -57,6 +57,23 @@ function logOk(msg: string): void {
|
||||
);
|
||||
logOk('mergeGroups order + dedup');
|
||||
|
||||
// --- Pure helpers: dedupeStrings (whitespace + case variants) ---
|
||||
assertEqual(
|
||||
dedupeStrings([' Tutoriel Angular ', 'tutoriel angular', 'TUTORIEL ANGULAR', 'autre', '', ' ']),
|
||||
['Tutoriel Angular', 'autre'],
|
||||
'dedupeStrings shows each occurrence only once (first wins)',
|
||||
);
|
||||
logOk('dedupeStrings dedup');
|
||||
|
||||
// --- Pure helpers: local fallback suggestions while typing ---
|
||||
assertEqual(buildLocalQuerySuggestions('a'), [], 'no local suggestions below 2 chars');
|
||||
const local = buildLocalQuerySuggestions('angular', 8);
|
||||
assert(local.length > 0 && local.length <= 8, 'local suggestions fill the panel while typing');
|
||||
assertEqual(dedupeStrings(local), local, 'local suggestions contain no duplicates');
|
||||
assert(!local.some((s) => s.trim().toLowerCase() === 'angular'), 'local suggestions never repeat the raw query');
|
||||
assert(local.some((s) => s.toLowerCase().includes('angular')), 'local suggestions derive from the typed text');
|
||||
logOk('buildLocalQuerySuggestions fallback');
|
||||
|
||||
// --- Pure helpers: highlight ---
|
||||
assertEqual(
|
||||
highlightParts('Tutoriel Angular', 'toriel an'),
|
||||
|
||||
@@ -34,6 +34,53 @@ export function dedupeSort(items: string[], q = ''): string[] {
|
||||
return clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicate a flat string list (case/whitespace-insensitive, first
|
||||
* occurrence wins). Order is preserved.
|
||||
*/
|
||||
export function dedupeStrings(items: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const raw of items || []) {
|
||||
const t = String(raw ?? '').trim();
|
||||
if (!t) continue;
|
||||
const k = normSuggest(t);
|
||||
if (seen.has(k)) continue;
|
||||
seen.add(k);
|
||||
out.push(t);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Local query suggestions built while typing, without any network call.
|
||||
* Used as a fallback tier when provider suggestions are missing or sparse
|
||||
* (providers without a suggest API, offline, rate-limited): typing always
|
||||
* yields suggestions. Output is deduplicated; the raw query itself is never
|
||||
* included.
|
||||
*/
|
||||
export function buildLocalQuerySuggestions(q: string, max = 10): string[] {
|
||||
const base = String(q ?? '').trim().replace(/\s+/g, ' ');
|
||||
if (base.length < 2) return [];
|
||||
const year = new Date().getFullYear();
|
||||
const candidates = [
|
||||
`${base} official`,
|
||||
`${base} live`,
|
||||
`${base} lyrics`,
|
||||
`${base} remix`,
|
||||
`${base} cover`,
|
||||
`${base} tutorial`,
|
||||
`${base} review`,
|
||||
`${base} playlist`,
|
||||
`${base} best of`,
|
||||
`${base} full album`,
|
||||
`${base} ${year}`,
|
||||
`"${base}"`,
|
||||
];
|
||||
const n = Math.max(1, Number(max || 10));
|
||||
return dedupeStrings(candidates.filter((c) => normSuggest(c) !== normSuggest(base))).slice(0, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten provider groups into a single ordered list (registry order),
|
||||
* deduped case-insensitively. Unknown provider keys go last (insertion order).
|
||||
|
||||
Reference in New Issue
Block a user