diff --git a/public/js/campanhas.js b/public/js/campanhas.js index 467e6dd..4adf57d 100644 --- a/public/js/campanhas.js +++ b/public/js/campanhas.js @@ -267,10 +267,10 @@ window.campanhasInit = window.campanhasInit || (() => { } async function carregarMetrics() { + const filters = el('metrics-filters'); + const display = el('campaign-metrics'); try { const campaigns = await API.get('/api/campaigns').then((r) => r.campaigns).catch(() => []); - const filters = el('metrics-filters'); - const display = el('campaign-metrics'); if (!campaigns.length) { filters.replaceChildren(h('p', { class: 'muted' }, 'Sem campanhas para métricas.')); @@ -297,7 +297,11 @@ window.campanhasInit = window.campanhasInit || (() => { ); display.replaceChildren(h('p', { class: 'muted' }, 'Selecione uma campanha e clique em "Ver fila" para ver métricas detalhadas.')); - } catch { /* silêncio */ } + } catch (error) { + filters.replaceChildren(h('p', { class: 'muted' }, 'Erro ao carregar métricas.')); + display.replaceChildren(); + toast(`Erro ao carregar métricas: ${error.message}`); + } } async function carregarMetricsData() { @@ -336,7 +340,10 @@ window.campanhasInit = window.campanhasInit || (() => { : null ].filter(Boolean) ); - } catch { /* silêncio */ } + } catch (error) { + el('campaign-metrics').replaceChildren(h('p', { class: 'muted' }, 'Erro ao carregar métricas.')); + toast(`Erro ao carregar métricas: ${error.message}`); + } } // Todos os listeners abaixo sao anexados UMA VEZ, aqui, sobre botoes fixos e diff --git a/tests/campaign-ui.test.mjs b/tests/campaign-ui.test.mjs index 5774feb..f8206a8 100644 --- a/tests/campaign-ui.test.mjs +++ b/tests/campaign-ui.test.mjs @@ -650,6 +650,32 @@ test('metrics section contains segmento and variant filters', async () => { assert.match(t, /Variante/); }); +// Regressao: uma falha ao buscar /metrics nao pode deixar o card de metricas +// mudo sem nenhum indicio de que algo deu errado. +test('metrics fetch failure shows an error state and toasts, not a silent no-op', 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 }]; + + await renderCampaign(app, campaigns); + app.context.API.get = async (url) => { + if (url.includes('/metrics')) throw new Error('falha de rede'); + if (url === '/api/campaigns') return { campaigns }; + if (url.includes('/queue')) return { queue: [] }; + throw new Error(`GET inesperado: ${url}`); + }; + + const verFilaBtns = Array.from(app.element('campaign-cards').children[0].children) + .flatMap((row) => row.children || []) + .filter((node) => /Ver fila/.test(nodeText(node))); + await verFilaBtns[0].click(); + const segmentoSelect = app.element('filtro-metrics-segmento'); + await segmentoSelect.listeners.change(); + + const t = nodeText(app.element('campaign-metrics')); + assert.match(t, /erro/i); + assert.ok(app.toasts.some((message) => /erro ao carregar m.tricas/i.test(message)), app.toasts.join(' | ')); +}); + test('metrics stats render using the real /metrics response shape ({ metrics: { previewed, approved, sent, replies, firstResponseTimeAvg } })', 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 }];