fix(history): recherche et filtres fonctionnels sur les 3 onglets
CI / build-and-test (push) Successful in 14m0s
CI / build-and-test (push) Successful in 14m0s
- distinctUntilChanged() sur Subject<void> avalait toutes les frappes apres la premiere : barre de recherche morte sur les 3 onglets. - Filtre provider recherches : ids courts stockes (yt, dm) vs filtre long (youtube) -> zero resultat. Match court+long cote serveur. - LIKE echappes (%, _, \\) sur q des 3 onglets. - Watch/transcripts : provider normalise (court+long), lang insensible a la casse avec prefixe (fr -> fr, fr-ca). - Ecriture watch canonique (nom long). Test test:history.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// Filtres de la section Historique : q/provider/lang sur les 3 onglets.
|
||||
// Run with: npm run test:history
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'newtube-history-test-'));
|
||||
process.env.NEWTUBE_DB_FILE = path.join(tmpDir, 'test.db');
|
||||
|
||||
const dbMod = await import('../db.mjs');
|
||||
const {
|
||||
insertUser,
|
||||
cryptoRandomUUID,
|
||||
insertSearchHistory,
|
||||
insertSearchHistoryAt,
|
||||
listSearchHistory,
|
||||
upsertWatchHistory,
|
||||
listWatchHistory,
|
||||
upsertTranscriptHistory,
|
||||
listTranscriptHistory,
|
||||
normalizeHistoryProvider,
|
||||
escapeLikePattern,
|
||||
} = dbMod;
|
||||
|
||||
function expect(cond, msg) {
|
||||
if (!cond) throw new Error(`Assertion failed: ${msg}`);
|
||||
}
|
||||
function logOk(msg) { console.log(`✓ ${msg}`); }
|
||||
|
||||
const user = cryptoRandomUUID();
|
||||
insertUser({ id: user, username: 'hist-a', email: null, passwordHash: 'x' });
|
||||
|
||||
// ---------- helpers purs ----------
|
||||
expect(normalizeHistoryProvider('youtube') === 'youtube', 'long id kept');
|
||||
expect(normalizeHistoryProvider('YT') === 'youtube', 'short id mapped, case-insensitive');
|
||||
expect(normalizeHistoryProvider('dm') === 'dailymotion', 'dm mapped');
|
||||
expect(normalizeHistoryProvider('xx') === null, 'unknown -> null');
|
||||
expect(escapeLikePattern('100%_x\\y') === '100\\%\\_x\\\\y', 'LIKE wildcards escaped');
|
||||
logOk('normalizeHistoryProvider + escapeLikePattern');
|
||||
|
||||
// ---------- recherches ----------
|
||||
// Formats réels du front : { provider: 'yt', providers: ['yt','dm'] } (ids courts)
|
||||
insertSearchHistory({ userId: user, query: 'lofi hip hop', filters: { provider: 'yt', providers: ['yt', 'dm'] } });
|
||||
insertSearchHistory({ userId: user, query: 'cuisine italienne', filters: { provider: 'dm', providers: ['dm'] } });
|
||||
insertSearchHistoryAt({ userId: user, query: 'takeout sans filtres', createdAt: '2024-01-01T00:00:00.000Z' });
|
||||
insertSearchHistory({ userId: user, query: '100% bio_pur', filters: { provider: 'yt', providers: ['yt'] } });
|
||||
|
||||
let rows = listSearchHistory({ userId: user, q: 'lofi' });
|
||||
expect(rows.length === 1 && rows[0].query === 'lofi hip hop', 'search q matches query');
|
||||
rows = listSearchHistory({ userId: user, provider: 'youtube' });
|
||||
expect(rows.length === 2 && rows.every((r) => r.query !== 'cuisine italienne'), 'provider long matches short-id filters_json');
|
||||
rows = listSearchHistory({ userId: user, provider: 'yt' });
|
||||
expect(rows.length === 2, 'provider short also matches');
|
||||
rows = listSearchHistory({ userId: user, provider: 'dailymotion' });
|
||||
// 'lofi' cherchait yt+dm : une recherche multi-providers remonte sous chaque provider.
|
||||
expect(rows.length === 2 && rows.some((r) => r.query === 'cuisine italienne') && rows.some((r) => r.query === 'lofi hip hop'), 'provider dailymotion matches dm rows incl. multi-provider');
|
||||
rows = listSearchHistory({ userId: user, q: '100%' });
|
||||
expect(rows.length === 1 && rows[0].query === '100% bio_pur', 'LIKE % escaped (no wildcard leak)');
|
||||
rows = listSearchHistory({ userId: user, q: '100%bio' });
|
||||
expect(rows.length === 0, 'escaped % does not span text');
|
||||
rows = listSearchHistory({ userId: user, q: 'bio_pur' });
|
||||
expect(rows.length === 1, 'LIKE _ escaped, literal match works');
|
||||
logOk('search history q/provider filters');
|
||||
|
||||
// ---------- visionnage ----------
|
||||
upsertWatchHistory({ userId: user, provider: 'youtube', videoId: 'AAA', title: 'Documentaire Océans' });
|
||||
upsertWatchHistory({ userId: user, provider: 'tw', videoId: 'BBB', title: 'Live concert' });
|
||||
upsertWatchHistory({ userId: user, provider: 'dailymotion', videoId: 'CCC', title: '100% рыбалка_2024' });
|
||||
|
||||
rows = listWatchHistory({ userId: user, q: 'océans' });
|
||||
expect(rows.length === 1 && rows[0].video_id === 'AAA', 'watch q matches title');
|
||||
rows = listWatchHistory({ userId: user, provider: 'youtube' });
|
||||
expect(rows.length === 1 && rows[0].video_id === 'AAA', 'watch provider long matches');
|
||||
rows = listWatchHistory({ userId: user, provider: 'tw' });
|
||||
expect(rows.length === 1 && rows[0].video_id === 'BBB', 'watch provider short normalized to twitch');
|
||||
rows = listWatchHistory({ userId: user, q: '100%' });
|
||||
expect(rows.length === 1 && rows[0].video_id === 'CCC', 'watch LIKE % escaped');
|
||||
rows = listWatchHistory({ userId: user, q: '100x' });
|
||||
expect(rows.length === 0, 'watch escaped % has no false positive');
|
||||
logOk('watch history q/provider filters');
|
||||
|
||||
// ---------- transcripts ----------
|
||||
const L = (texts) => texts.map((text, i) => ({ t: i * 2, dur: 2, text }));
|
||||
upsertTranscriptHistory({ userId: user, provider: 'youtube', videoId: 'V1', title: 'Conférence IA', lang: 'fr', languages: ['fr'], lines: L(['bonjour à tous', 'parlons intelligence artificielle']) });
|
||||
upsertTranscriptHistory({ userId: user, provider: 'youtube', videoId: 'V2', title: 'AI talk', lang: 'en-US', languages: ['en-US'], lines: L(['hello world', 'machine learning']) });
|
||||
upsertTranscriptHistory({ userId: user, provider: 'dailymotion', videoId: 'V3', title: 'Recette', lang: 'fr-CA', languages: ['fr-CA'], lines: L(['ajouter du sel', 'mélanger']) });
|
||||
|
||||
rows = listTranscriptHistory({ userId: user, q: 'intelligence' });
|
||||
expect(rows.length === 1 && rows[0].video_id === 'V1', 'transcript q searches lines_json content');
|
||||
rows = listTranscriptHistory({ userId: user, provider: 'yt' });
|
||||
expect(rows.length === 2, 'transcript provider short normalized');
|
||||
rows = listTranscriptHistory({ userId: user, lang: 'FR' });
|
||||
expect(rows.length === 2 && rows.every((r) => r.video_id !== 'V2'), 'transcript lang case-insensitive, fr matches fr + fr-ca');
|
||||
rows = listTranscriptHistory({ userId: user, lang: 'fr' });
|
||||
expect(rows.length === 2 && rows.every((r) => r.video_id !== 'V2'), 'transcript lang fr matches fr + fr-ca, not en-us');
|
||||
rows = listTranscriptHistory({ userId: user, q: 'AI', provider: 'youtube', lang: 'en' });
|
||||
expect(rows.length === 1 && rows[0].video_id === 'V2', 'transcript combined q+provider+lang');
|
||||
logOk('transcript history q/provider/lang filters');
|
||||
|
||||
// ---------- pagination + filtre combinés ----------
|
||||
rows = listSearchHistory({ userId: user, limit: 1, q: 'e' });
|
||||
expect(rows.length === 1, 'limit applies with q filter');
|
||||
logOk('limit with filters');
|
||||
|
||||
console.log('history_filters: ALL OK');
|
||||
Reference in New Issue
Block a user