Ctrl K
ComunidadGuías…

tests/monitor-search-results.test.cjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const ts = require('typescript');

const read = (...p) => fs.readFileSync(path.join(__dirname, '..', ...p), 'utf8');
const assert = (cond, msg) => { if (!cond) throw new Error(msg); };

const panel = read('frontend', 'src', 'components', 'MonitorPanel.tsx');
const client = read('frontend', 'src', 'api', 'client.ts');

// Hide and Reject rows only appear with "show all"; the counter counts what is shown.
const visible = panel.match(/const visibleResults = ([^\n]+);/);
assert(visible, 'visibleResults not found');
const rows = ['AutoDownload', 'Review', 'Hide', 'Reject'].map(decision => ({ decision }));
const shown = showAll => vm.runInNewContext(visible[1], { results: rows, showAll }).map(r => r.decision).join(',');
assert(shown(false) === 'AutoDownload,Review', 'Hide and Reject rows must be hidden by default');
assert(shown(true) === 'AutoDownload,Review,Hide,Reject', 'show all must list Hide and Reject rows');
assert(panel.includes("t('monitorResultsCount').replace('{count}', String(visibleResults.length))"),
  'the result counter must count the visible rows');

// A rejected row says why next to its badge.
assert(panel.includes("{r.decision === 'Reject' && r.reason && <span className=\"monitor-reject-reason\">{r.reason}</span>}"),
  'Reject rows must show their reason');

// Search diagnostics: queries, rejected count and provider errors.
assert(/export interface MonitorSearchResultDto \{[\s\S]*rejectedCount: number;[\s\S]*queries: string\[\];[\s\S]*providerErrors: string\[\];/.test(client),
  'MonitorSearchResultDto must carry rejectedCount, queries and providerErrors');
for (const needle of ["{searchInfo && searchInfo.queries.length > 0 && (", "t('searchQueriesSent')", "t('monitorRejectedCount')", 'searchInfo.providerErrors.map(']) {
  assert(panel.includes(needle), `monitor panel must render ${needle}`);
}

// An auto-download held back by a pending grab says so instead of just not happening.
assert(/export interface MonitorSearchResultDto \{[\s\S]*autoDispatchSkipped\?: string \| null;/.test(client),
  'MonitorSearchResultDto must carry autoDispatchSkipped');
assert(panel.includes("t('monitorAutoDispatchSkipped').replace('{reason}', searchInfo.autoDispatchSkipped)"),
  'the diagnostics line must show why an auto-download was skipped');
const translations = read('frontend', 'src', 'i18n', 'translations.ts');
assert((translations.match(/\n        monitorAutoDispatchSkipped: '[^'\n]*\{reason\}[^'\n]*',/g) || []).length ===
  (translations.match(/\n        monitorRejectedCount: /g) || []).length,
  'monitorAutoDispatchSkipped must exist with {reason} in every language block');

// Pull one `const name = ... };` block out of the panel and run it with the given stand-ins.
const load = (name, sandbox) => {
  const block = panel.match(new RegExp(`const ${name} = [\\s\\S]*?\\n  \\};`));
  assert(block, `${name} not found`);
  const js = ts.transpileModule(block[0], { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText;
  return vm.runInNewContext(`${js}\n${name}`, sandbox);
};

// The search response feeds the diagnostics line.
const response = { gameId: 7, scored: [], queries: ['Halo 3'], providerErrors: ['Jackett search failed: refused'], rejectedCount: 2, error: null };
let searchInfo = 'untouched';
const searchNow = load('searchNow', {
  monitorApi: { searchNow: async () => ({ data: response }) },
  gameId: 7, autoDispatch: false,
  setSearching: () => {}, setError: () => {}, setNotice: () => {}, setAutoQueued: () => {}, setResults: () => {},
  setSearchInfo: v => { searchInfo = v; }, getErrorMessage: () => '', t: k => k,
});

// GameDetails keeps the sent queries and categories and shows them above the results.
const details = read('frontend', 'src', 'pages', 'GameDetails.tsx');
for (const needle of [
  'queries: response.data.queries || []',
  'categories: response.data.categories || []',
  '{!searching && !error && searchDiagnostics?.queries && searchDiagnostics.queries.length > 0 && (',
  "t('searchQueriesSent')",
  "t('searchCategoriesSent')",
]) {
  assert(details.includes(needle), `game details search diagnostics must contain ${needle}`);
}

// A manual grab sends the game and the release title; the backend resolves the platform and the subfolder from them.
let posted;
const queueRelease = load('queueRelease', {
  apiClient: { post: async (_url, body) => { posted = body; return { data: {} }; } },
  gameId: 7, queueingUrl: null, setQueueingUrl: () => {}, setError: () => {}, setNotice: () => {},
  getErrorMessage: () => '', t: k => k,
});

(async () => {
  await searchNow();
  assert(searchInfo === response, 'searchNow must hand the response to the diagnostics state');

  await queueRelease({ magnetUrl: 'magnet:?xt=x', protocol: 'torrent', platformFolder: 'wii', title: 'Zelda TotK [NSP] + Update 1.2.1' });
  assert(posted && posted.gameId === 7, 'the grab must send the game id');
  assert(!('platformFolder' in posted), 'the grab must not send the release platform folder');
  assert(posted.releaseTitle === 'Zelda TotK [NSP] + Update 1.2.1', 'the grab must send the release title');
  assert(!('importSubfolder' in posted), 'the grab must leave the subfolder to the backend');
  console.log('monitor-search-results: all contract checks passed');
})().catch(e => { console.error(e.message); process.exit(1); });