diff --git a/public/js/campanhas.js b/public/js/campanhas.js index e3279b9..467e6dd 100644 --- a/public/js/campanhas.js +++ b/public/js/campanhas.js @@ -219,11 +219,11 @@ window.campanhasInit = window.campanhasInit || (() => { } async function carregarReplies() { + const container = el('pending-replies'); try { const campaigns = await API.get('/api/campaigns').then((r) => r.campaigns).catch(() => []); const campaignNames = new Map(campaigns.map((c) => [c.id, c.name])); const { pending } = await API.get('/api/campaigns/replies/pending'); - const container = el('pending-replies'); if (!pending || !pending.length) { container.replaceChildren(h('p', { class: 'muted' }, 'Nenhuma resposta pendente.')); return; @@ -240,7 +240,13 @@ window.campanhasInit = window.campanhasInit || (() => { }, 'Classificar') ) )); - } catch { /* silêncio */ } + } catch (error) { + // Nunca deixar o placeholder de "nenhuma pendente" no lugar de um erro + // real de carregamento — os dois ficariam visualmente indistinguiveis e + // uma resposta de cliente poderia passar despercebida. + container.replaceChildren(h('p', { class: 'muted' }, 'Erro ao carregar respostas pendentes.')); + toast(`Erro ao carregar respostas pendentes: ${error.message}`); + } } function abrirStageDialog(leadId) { diff --git a/tests/campaign-ui.test.mjs b/tests/campaign-ui.test.mjs index e1bc2f1..5774feb 100644 --- a/tests/campaign-ui.test.mjs +++ b/tests/campaign-ui.test.mjs @@ -67,6 +67,7 @@ async function executeApp({ get, post, patch, del, put } = {}) { const timers = []; const gets = []; const posts = []; + const toasts = []; const element = (id) => { if (!elements.has(id)) elements.set(id, fakeElement(id)); return elements.get(id); @@ -149,14 +150,14 @@ async function executeApp({ get, post, patch, del, put } = {}) { timers.push({ callback, delay }); return timers.length; }, - toast() {}, + toast(message) { toasts.push(message); }, window: { Campanhas: { bindEvents() {}, ativar() {} }, confirm: () => true, location: {} }, URLSearchParams }; vm.createContext(context); vm.runInContext(await read('public/js/app.js'), context); await new Promise((resolve) => setImmediate(resolve)); - return { context, element, elements, gets, posts, timers }; + return { context, element, elements, gets, posts, timers, toasts }; } async function renderCampaign(app, campaigns, options = {}) { @@ -508,6 +509,31 @@ test('pending replies section shows reply items', async () => { assert.match(t, /não tenho interesse/i); }); +// Regressao: uma falha ao buscar /replies/pending nao pode deixar o +// placeholder "nenhuma pendente" no lugar — os dois ficam indistinguiveis e +// uma resposta de cliente passaria despercebida. +test('pending replies fetch failure shows an error state, not the empty-state placeholder', async () => { + const app = await executeApp(); + const campaigns = [{ id: 1, code: 'gdelivery-food', name: 'G-Delivery', produto: 'G-Delivery', status: 'running', pause_reason: null, week_number: 1, approved_daily_limit: 10, segments: ['restaurantes'], manual_usage: 0, auto_usage: 0 }]; + + const origGet = app.context.API.get; + app.context.API.get = async (url) => { + app.gets.push(url); + if (url === '/api/campaigns/replies/pending') throw new Error('falha de rede'); + if (url === '/api/campaigns') return { campaigns }; + if (url.includes('/metrics')) return { metrics: {} }; + return origGet(url); + }; + + vm.runInContext(await read('public/js/campanhas.js'), app.context); + await app.context.window.Campanhas.ativar(); + + const t = nodeText(app.element('pending-replies')); + assert.doesNotMatch(t, /nenhuma resposta pendente/i); + assert.match(t, /erro/i); + assert.ok(app.toasts.some((message) => /erro/i.test(message)), app.toasts.join(' | ')); +}); + test('stage classification card shows stage area', async () => { const app = await executeApp(); const campaigns = [{ id: 1, code: 'gdelivery-food', name: 'G-Delivery', produto: 'G-Delivery', status: 'running', pause_reason: null, week_number: 1, approved_daily_limit: 10, segments: ['restaurantes'], manual_usage: 0, auto_usage: 0 }];