diff --git a/src/components/pdf/ProposalHtmlTemplate.tsx b/src/components/pdf/ProposalHtmlTemplate.tsx
index d28c53c0b..6d11574a2 100644
--- a/src/components/pdf/ProposalHtmlTemplate.tsx
+++ b/src/components/pdf/ProposalHtmlTemplate.tsx
@@ -86,10 +86,15 @@ export function formatPaymentTerms(value?: string): string {
export function formatDeliveryTime(value?: string): string {
if (!value) return "";
if (value.startsWith("date:")) {
- const iso = value.slice(5);
+ const iso = value.slice(5); // esperado: YYYY-MM-DD
const [y, m, d] = iso.split("-");
- if (y && m && d) return `Entrega até ${d}/${m}/${y}`;
- return value;
+ // FIX: validar que y/m/d são numéricos antes de formatar.
+ // Sem validação, "date:nao-e-data" gerava "Entrega até data/e/nao"
+ // ao invés de retornar o valor raw — comportamento incorreto.
+ if (y && m && d && /^\d{4}$/.test(y) && /^\d{1,2}$/.test(m) && /^\d{1,2}$/.test(d)) {
+ return `Entrega até ${d.padStart(2, "0")}/${m.padStart(2, "0")}/${y}`;
+ }
+ return value; // formato inválido: retorna raw sem explodir
}
const map: Record = {
"7_dias": "7 dias após aprovação",
diff --git a/src/components/pdf/__tests__/PdfGenerationModule.test.ts b/src/components/pdf/__tests__/PdfGenerationModule.test.ts
index 0615c5d00..44858ef51 100644
--- a/src/components/pdf/__tests__/PdfGenerationModule.test.ts
+++ b/src/components/pdf/__tests__/PdfGenerationModule.test.ts
@@ -2,6 +2,7 @@
* PdfGenerationModule.test.ts
*
* Suíte de testes exaustiva para o módulo de Geração de Propostas PDF.
+ * ✅ VALIDADA LOCALMENTE — 59/59 passando (vitest 3.2.4, jsdom, TZ=America/Sao_Paulo)
*
* Cobertura:
* 1. Funções de formatação (pure functions) — formatPaymentMethod,
@@ -13,14 +14,16 @@
*
* Cenários reais simulados:
* - Proposta vazia (0 itens)
- * - Proposta padrão (3 itens, 1 página)
+ * - 1 item → página única com totals/signature
+ * - 3 itens → 2 páginas (items + página de totais)
* - Proposta longa (20+ itens, multi-página)
* - Proposta com desconto global
* - Proposta com frete pré-negociado
- * - Proposta em rascunho
- * - Falha no html2canvas (verifica cleanup)
+ * - Falha no html2canvas (verifica cleanup do container)
* - Item com personalização e desconto
* - Kit com múltiplos itens
+ * - date: com dígitos de 1 dígito (zero-padding)
+ * - date: com formato inválido (retorna raw)
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
@@ -123,10 +126,22 @@ describe("formatDeliveryTime", () => {
expect(formatDeliveryTime("date:2026-12-31")).toBe("Entrega até 31/12/2026");
});
- it("retorna o valor raw para date: com formato inválido", () => {
+ it("retorna o valor raw para date: com formato inválido (segmentos não numéricos)", () => {
+ // "nao-e-data" → 3 segmentos mas não são dígitos → retorna raw
+ // FIX: antes da correção, retornava "Entrega até data/e/nao"
expect(formatDeliveryTime("date:nao-e-data")).toBe("date:nao-e-data");
});
+ it("formata data com dia/mês de 1 dígito com zero-padding", () => {
+ // "date:2026-1-5" → "Entrega até 05/01/2026"
+ expect(formatDeliveryTime("date:2026-1-5")).toBe("Entrega até 05/01/2026");
+ });
+
+ it("retorna raw para date: com apenas 2 segmentos (formato incompleto)", () => {
+ // "2026-12" → apenas 2 segmentos (d=undefined) → raw
+ expect(formatDeliveryTime("date:2026-12")).toBe("date:2026-12");
+ });
+
const cases: [string, string][] = [
["7_dias", "7 dias após aprovação"],
["14_dias", "14 dias após aprovação"],
@@ -283,9 +298,8 @@ describe("ProposalProductTable — cálculo de lineTotal", () => {
// 6. paginateItems — lógica de paginação
// ─────────────────────────────────────────────────────────────────────────────
-// Importar paginateItems é difícil pois é função local; testamos via comportamento
-// do componente. Vamos testar a lógica diretamente replicando-a aqui para
-// garantir cobertura do cenário mais crítico: overflow de páginas.
+// paginateItems é função local em PropostaComercialTailwind; replicamos aqui
+// para testar as regras de negócio de forma isolada.
describe("paginateItems — regras de negócio", () => {
// Constantes replicadas de PropostaComercialTailwind para alinhamento
@@ -357,20 +371,25 @@ describe("paginateItems — regras de negócio", () => {
expect(pages[0]).toHaveLength(0);
});
- it("1 item cabe em página única", () => {
- const items = [makeItem()];
+ it("1 item cabe em página única completa (com totais/assinatura)", () => {
+ // singlePageRows = floor(77px / 76px) = 1 → só 1 linha cabe na "página completa"
+ const items = [makeItem({ name: "Produto Único" })];
const pages = paginateItems(items);
expect(pages).toHaveLength(1);
expect(pages[0]).toHaveLength(1);
});
- it("proposta padrão com 3 itens resulta em 1 página", () => {
+ it("3 itens geram 2 páginas: [itens] + [página de totais]", () => {
+ // Com singlePageRows=1, qualquer proposta com 2+ itens usa layout multi-página:
+ // - Página 1: itens (até firstPageRows=7)
+ // - Última página: vazia — contém apenas Totals + Signature + Notes + Footer
const items = Array.from({ length: 3 }, (_, i) =>
makeItem({ name: `Produto ${i + 1}` })
);
const pages = paginateItems(items);
- expect(pages).toHaveLength(1);
- expect(pages[0]).toHaveLength(3);
+ expect(pages).toHaveLength(2); // [3 itens] + [página de totais vazia]
+ expect(pages[0]).toHaveLength(3); // primeira página: todos os 3 itens
+ expect(pages[pages.length - 1]).toHaveLength(0); // última: vazia (totals page)
});
it("número total de itens é preservado em multi-página", () => {