Fix silent failure when loading pending campaign replies
carregarReplies() swallowed any fetch error, leaving whatever was already in #pending-replies — on first load, the static "Nenhuma resposta pendente." placeholder. A failed load was visually indistinguishable from a genuine empty state, so a customer reply could go unnoticed with no error surfaced (every other action in this file already toasts on failure). Now shows an explicit error state and toasts, and the fetch mock harness in campaign-ui.test.mjs gained toast-call tracking to prove it. Found via a full-project audit.
This commit is contained in:
@@ -219,11 +219,11 @@ window.campanhasInit = window.campanhasInit || (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function carregarReplies() {
|
async function carregarReplies() {
|
||||||
|
const container = el('pending-replies');
|
||||||
try {
|
try {
|
||||||
const campaigns = await API.get('/api/campaigns').then((r) => r.campaigns).catch(() => []);
|
const campaigns = await API.get('/api/campaigns').then((r) => r.campaigns).catch(() => []);
|
||||||
const campaignNames = new Map(campaigns.map((c) => [c.id, c.name]));
|
const campaignNames = new Map(campaigns.map((c) => [c.id, c.name]));
|
||||||
const { pending } = await API.get('/api/campaigns/replies/pending');
|
const { pending } = await API.get('/api/campaigns/replies/pending');
|
||||||
const container = el('pending-replies');
|
|
||||||
if (!pending || !pending.length) {
|
if (!pending || !pending.length) {
|
||||||
container.replaceChildren(h('p', { class: 'muted' }, 'Nenhuma resposta pendente.'));
|
container.replaceChildren(h('p', { class: 'muted' }, 'Nenhuma resposta pendente.'));
|
||||||
return;
|
return;
|
||||||
@@ -240,7 +240,13 @@ window.campanhasInit = window.campanhasInit || (() => {
|
|||||||
}, 'Classificar')
|
}, '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) {
|
function abrirStageDialog(leadId) {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ async function executeApp({ get, post, patch, del, put } = {}) {
|
|||||||
const timers = [];
|
const timers = [];
|
||||||
const gets = [];
|
const gets = [];
|
||||||
const posts = [];
|
const posts = [];
|
||||||
|
const toasts = [];
|
||||||
const element = (id) => {
|
const element = (id) => {
|
||||||
if (!elements.has(id)) elements.set(id, fakeElement(id));
|
if (!elements.has(id)) elements.set(id, fakeElement(id));
|
||||||
return elements.get(id);
|
return elements.get(id);
|
||||||
@@ -149,14 +150,14 @@ async function executeApp({ get, post, patch, del, put } = {}) {
|
|||||||
timers.push({ callback, delay });
|
timers.push({ callback, delay });
|
||||||
return timers.length;
|
return timers.length;
|
||||||
},
|
},
|
||||||
toast() {},
|
toast(message) { toasts.push(message); },
|
||||||
window: { Campanhas: { bindEvents() {}, ativar() {} }, confirm: () => true, location: {} },
|
window: { Campanhas: { bindEvents() {}, ativar() {} }, confirm: () => true, location: {} },
|
||||||
URLSearchParams
|
URLSearchParams
|
||||||
};
|
};
|
||||||
vm.createContext(context);
|
vm.createContext(context);
|
||||||
vm.runInContext(await read('public/js/app.js'), context);
|
vm.runInContext(await read('public/js/app.js'), context);
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
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 = {}) {
|
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);
|
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 () => {
|
test('stage classification card shows stage area', async () => {
|
||||||
const app = await executeApp();
|
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 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 }];
|
||||||
|
|||||||
Reference in New Issue
Block a user