diff --git a/public/js/tratamento.js b/public/js/tratamento.js index 309a801..d7a51f4 100644 --- a/public/js/tratamento.js +++ b/public/js/tratamento.js @@ -4,6 +4,7 @@ const Tratamento = (() => { let totalPages = 1; let carregado = false; let refreshTimer = null; + let tabelaRequestSeq = 0; const CLASSIFICACAO_LABELS = { excelente: 'Excelente', bom: 'Bom', medio: 'Médio', baixo: 'Baixo', muito_baixo: 'Muito Baixo' @@ -364,7 +365,13 @@ const Tratamento = (() => { } async function carregarTabela() { + const requestId = ++tabelaRequestSeq; const data = await API.get(`/api/leads?${filtroQuery({ page, pageSize: 50 })}`); + // Descarta respostas fora de ordem: pg-prev/pg-next nao usam comCarregando + // (ver bindEvents), entao um clique rapido pode disparar uma nova consulta + // antes da anterior responder. Sem isso, a resposta mais lenta podia + // sobrescrever a tabela/paginacao com uma pagina que ja nao e mais a atual. + if (requestId !== tabelaRequestSeq) return; totalPages = Math.max(1, Math.ceil(data.total / data.pageSize)); el('leads-total').textContent = `— ${fmt.format(data.total)} encontrados`; el('pg-info').textContent = `página ${data.page} de ${fmt.format(totalPages)}`; diff --git a/tests/personal-branding.test.mjs b/tests/personal-branding.test.mjs index 8f57763..de722ee 100644 --- a/tests/personal-branding.test.mjs +++ b/tests/personal-branding.test.mjs @@ -254,6 +254,70 @@ test('launcher carrega APP_PORT do .env do root sem sobrescrever ambiente real', assert.match(source, /StartsWith\("#"\)/); }); +// pg-prev/pg-next nao usam comCarregando de proposito (ver comentario em +// bindEvents), entao nada impede dois cliques rapidos de disparar duas +// consultas concorrentes. Antes, se a resposta da pagina anterior chegasse +// DEPOIS da pagina mais recente, ela sobrescrevia a tabela com dados de uma +// pagina que o operador ja nao esta mais vendo. +test('clique duplo em pg-next nao deixa resposta atrasada sobrescrever a pagina mais recente', async () => { + const app = await executeApp(); + const tick = () => new Promise((resolve) => setImmediate(resolve)); + const page2 = deferred(); + const page3 = deferred(); + const rowFor = (label) => [{ + id: 1, empresa: label, whatsapp: '', segmento: '', segmento_label: '', + prioridade: 1, score: 0, classificacao: 'bom', produto_principal: '', exported_at: null + }]; + + app.context.API.get = async (url) => { + if (url === '/api/leads/facets') return { ufs: [], interesses: [], produtos: [], segmentos: [], cidades: [] }; + if (url.startsWith('/api/dashboard/summary')) return { + total: 1, com_whatsapp: 1, p1: 1, p2: 0, p3: 0, p4: 0, + potencial_pct: 100, cobertura_pct: 0, exportados: 0, disponiveis: 1, + p1_pct: 100, p2_pct: 0, p3_pct: 0, p4_pct: 0, meta_diaria: 10, + meta_pct: 0, hoje_exportados: 0, dias_estimados: 1 + }; + if (url.startsWith('/api/dashboard/evolution')) return { evolution: [] }; + if (url.startsWith('/api/dashboard/oportunidade')) return { oportunidade: null }; + if (url.startsWith('/api/dashboard/rankings')) return { ranking: [] }; + if (url === '/api/exports?limit=12') return { exports: [] }; + if (url === '/api/settings') return { settings: {} }; + if (url === '/api/outreach/templates') return { templates: [] }; + if (url === '/api/outreach/status') return { enviadosHoje: 0, mensagensPorDia: 10, ultimos: [] }; + if (url === '/api/outreach/opt-outs') return { optOuts: [] }; + if (url === '/api/whatsapp/status') return { state: 'READY' }; + if (url.startsWith('/api/leads?')) { + const page = new URLSearchParams(url.split('?')[1]).get('page'); + if (page === '1') return { rows: rowFor('Pagina 1'), total: 150, page: 1, pageSize: 50 }; + if (page === '2') { await page2.promise; return { rows: rowFor('Pagina 2 (atrasada)'), total: 150, page: 2, pageSize: 50 }; } + if (page === '3') { await page3.promise; return { rows: rowFor('Pagina 3'), total: 150, page: 3, pageSize: 50 }; } + throw new Error(`pagina inesperada: ${page}`); + } + throw new Error(`GET inesperado no tratamento: ${url}`); + }; + + vm.runInContext(await read('public/js/tratamento.js'), app.context); + await app.context.window.Tratamento.ativar(); + app.context.window.Tratamento.bindEvents(); + + app.element('pg-next').click(); // pagina 1 -> 2, fica presa em page2.promise + await tick(); + app.element('pg-next').click(); // pagina 2 -> 3, fica presa em page3.promise + await tick(); + + // Resolve fora de ordem: a pagina 3 (mais recente) responde primeiro. + page3.resolve(); + await tick(); + await tick(); + page2.resolve(); + await tick(); + await tick(); + + const t = nodeText(app.element('tabela-leads-tbody')); + assert.match(t, /Pagina 3/); + assert.doesNotMatch(t, /atrasada/); +}); + test('tratamento separa validade do telefone da verificacao de WhatsApp', async () => { const html = await read('public/index.html'); const treatment = await read('public/js/tratamento.js');