Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ZeusFiscal_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ jobs:
run: |
dotnet build -p:Version=${{ steps.date.outputs.date }} ".\NFe.Danfe.QuestPdf\NFe.Danfe.QuestPdf.csproj" -c Release

- name: Build Impressão DANFE HTML
run: |
dotnet build -p:Version=${{ steps.date.outputs.date }} ".\NFe.Danfe.Html\NFe.Danfe.Html.csproj" -c Release

#- name: Test
# run: dotnet test --no-build --verbosity normal

Expand All @@ -91,6 +95,7 @@ jobs:
dotnet pack -o ${{ github.workspace }} -v minimal -c Release -p:NuspecProperties=version=${{ steps.date.outputs.date }} -p:PackageVersion=${{ steps.date.outputs.date }} NuGet\Hercules.NET.MDFe\Hercules.NET.MDFe.csproj
dotnet pack -o ${{ github.workspace }} -v minimal -c Release -p:NuspecProperties=version=${{ steps.date.outputs.date }} -p:PackageVersion=${{ steps.date.outputs.date }} NuGet\Hercules.NET.CTe\Hercules.NET.CTe.csproj
dotnet pack -o ${{ github.workspace }} -v minimal -c Release -p:NuspecProperties=version=${{ steps.date.outputs.date }} -p:PackageVersion=${{ steps.date.outputs.date }} NuGet\Hercules.NET.Impressao.NFCe.QuestPdf\Hercules.NET.Impressao.NFCe.QuestPdf.csproj
dotnet pack -o ${{ github.workspace }} -v minimal -c Release -p:NuspecProperties=version=${{ steps.date.outputs.date }} -p:PackageVersion=${{ steps.date.outputs.date }} NuGet\Hercules.NET.NFe.Danfe.Html\Hercules.NET.NFe.Danfe.Html.csproj

- name: Upload Artifacts
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -154,6 +159,7 @@ jobs:
- Hercules.NET.MDFe
- Hercules.NET.CTe
- Hercules.NET.Impressao.NFCe.QuestPdf
- Hercules.NET.NFe.Danfe.Html

files: |
${{ github.workspace }}/*.nupkg
Expand Down
83 changes: 83 additions & 0 deletions NFe.Danfe.Html.Testes/DanfeNfeHtmlLeiauteAtualTestes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using NFe.Danfe.Html.CrossCutting;
using NFe.Danfe.Html.Dominio;
using NFe.Danfe.Html.Testes.Fixtures;
using Xunit;

namespace NFe.Danfe.Html.Testes
{
/// <summary>
/// Garante que o leiaute atual continua idêntico ao gerado antes da NT 2026.010.
/// Os arquivos em Fixtures/Golden foram gerados a partir do master anterior a esta alteração, com a cultura pt-BR.
/// </summary>
public class DanfeNfeHtmlLeiauteAtualTestes
{
private const string Protocolo = "135261000001234 - 21/10/2026 10:31:00";
private const string Creditos = "Hercules.NET";

public DanfeNfeHtmlLeiauteAtualTestes()
{
//a formatação do leiaute atual depende da cultura corrente
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("pt-BR");
}

private static string LerGolden(int quantidadeProdutos)
{
return File.ReadAllText(Path.Combine("Fixtures", "Golden", $"danfe-leiaute-atual-{quantidadeProdutos}-itens.html"));
}

private static async Task<string> GerarHtmlAsync(DanfeNFe danfe)
{
return (await new DanfeNfeHtml2(danfe).ObterDocHtmlAsync()).Html;
}

[Theory]
[InlineData(3)]
[InlineData(40)]
public async Task ConstrutorAnterior_GeraHtmlIdenticoAoLeiauteAtual(int quantidadeProdutos)
{
// Arrange
var danfe = new DanfeNFe(NFeFixture.CriarNfe(quantidadeProdutos), Status.Autorizada, Protocolo, Creditos);

// Act
var html = await GerarHtmlAsync(danfe);

// Assert
Assert.Equal(LeiauteDanfe.Atual, danfe.Leiaute);
Assert.Equal(LerGolden(quantidadeProdutos), html);
}

[Theory]
[InlineData(3)]
[InlineData(40)]
public async Task LeiauteAtualExplicito_GeraHtmlIdenticoAoLeiauteAtual(int quantidadeProdutos)
{
// Arrange
var danfe = new DanfeNFe(NFeFixture.CriarNfe(quantidadeProdutos), Status.Autorizada, Protocolo, Creditos, LeiauteDanfe.Atual);

// Act
var html = await GerarHtmlAsync(danfe);

// Assert
Assert.Equal(LerGolden(quantidadeProdutos), html);
}

[Theory]
[InlineData(3)]
[InlineData(40)]
public async Task LeiauteAtual_ComXmlDaReformaTributaria_IgnoraOsGruposNovos(int quantidadeProdutos)
{
// Arrange
var nfe = NFeFixture.CriarNfeReformaTributaria(quantidadeProdutos, comQuadrosFacultativos: true);
var danfe = new DanfeNFe(nfe, Status.Autorizada, Protocolo, Creditos);

// Act
var html = await GerarHtmlAsync(danfe);

// Assert
Assert.Equal(LerGolden(quantidadeProdutos), html);
}
}
}
182 changes: 182 additions & 0 deletions NFe.Danfe.Html.Testes/DanfeNfeHtmlReformaTributariaTestes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NFe.Danfe.Html.CrossCutting;
using NFe.Danfe.Html.Dominio;
using NFe.Danfe.Html.Testes.Fixtures;
using Xunit;

namespace NFe.Danfe.Html.Testes
{
/// <summary>
/// Leiaute do DANFE NF-e da NT 2026.010 (Reforma Tributária)
/// </summary>
public class DanfeNfeHtmlReformaTributariaTestes
{
private static async Task<string> GerarHtmlAsync(int quantidadeProdutos, bool comQuadrosFacultativos = true, Issqn issqn = null)
{
var nfe = NFeFixture.CriarNfeReformaTributaria(quantidadeProdutos, comQuadrosFacultativos);
var danfe = new DanfeNFe(nfe, Status.Autorizada, "135261000001234 - 21/10/2026 10:31:00", "Hercules.NET",
LeiauteDanfe.ReformaTributaria, issqn);
return (await new DanfeNfeHtml2(danfe).ObterDocHtmlAsync()).Html;
}

private static int ContarPaginas(string html)
{
return Regex.Matches(html, "class=\"page nfeArea rtc\"").Count;
}

[Fact]
public async Task GerarHtml_ImprimeOsQuadrosDaNt2026010()
{
// Act
var html = await GerarHtmlAsync(3);

// Assert
Assert.Contains("Total do IBS / CBS / IS", html);
Assert.Contains("CÓDIGO DO REGIME TRIBUTÁRIO", html);
Assert.Contains("3 - REGIME NORMAL", html);
Assert.Contains("TIPO DE REGIME DE APURAÇÃO DO IBS E DA CBS", html);
Assert.Contains("BASES DE CÁLCULO", html);
Assert.Contains("ALÍQUOTAS", html);
Assert.Contains("VALOR DOS TRIBUTOS", html);
Assert.Contains("[NCM 27101921] [cClassTrib 000001]", html);
Assert.Contains("CST 000", html);
}

[Fact]
public async Task GerarHtml_PreencheOsTotaisDeIbsCbsIsEMonofasia()
{
// Act
var html = await GerarHtmlAsync(3);

// Assert
Assert.Contains("VALOR DA CBS</span>\n <span class=\"info\">2,16</span>", html);
Assert.Contains("VALOR DO IBS UF</span>\n <span class=\"info\">0,24</span>", html);
Assert.Contains("VALOR DO IMPOSTO SELETIVO</span>\n <span class=\"info\">0,25</span>", html);
Assert.Contains("VALOR DO IBS MONOFÁSICO</span>\n <span class=\"info\">12,30</span>", html);
Assert.Contains("VALOR DA CBS MONOFÁSICA</span>\n <span class=\"info\">110,70</span>", html);
Assert.Contains("BC DO ICMS MONOFÁSICO</span>\n <span class=\"info\">1.000,00</span>", html);
}

[Fact]
public async Task GerarHtml_ItemComGRed_ImprimeAAliquotaEfetiva()
{
// Act
var html = await GerarHtmlAsync(3);
var item2 = Regex.Match(html, "<tr><td><div class=\"descricao\"[^>]*><strong>00002</strong>.*?</tr></table></td></tr>").Value;

// Assert
Assert.Contains("[cClassTrib 200003]", item2);
Assert.Contains("<td class=\"rot\">IBS UF</td><td class=\"val\">0,04%</td>", item2);
Assert.Contains("<td class=\"rot\">CBS</td><td class=\"val\">0,36%</td>", item2);
Assert.DoesNotContain("0,10%", item2);
Assert.DoesNotContain("0,90%", item2);
}

[Fact]
public async Task GerarHtml_ItemSemIsOuIpi_DeixaOsCamposEmBranco()
{
// Act
var html = await GerarHtmlAsync(3);
var item1 = Regex.Match(html, "<tr><td><div class=\"descricao\"[^>]*><strong>00001</strong>.*?</tr></table></td></tr>").Value;
var item3 = Regex.Match(html, "<tr><td><div class=\"descricao\"[^>]*><strong>00003</strong>.*?</tr></table></td></tr>").Value;

// Assert
Assert.Contains("<td class=\"rot\">IS</td><td class=\"val\"></td>", item1);
Assert.Contains("<td class=\"rot\">IPI</td><td class=\"val\"></td>", item1);
Assert.Contains("<td class=\"rot\">IS</td><td class=\"val\">0,25%</td>", item3);
Assert.Contains("<td class=\"rot\">IPI</td><td class=\"val\">5,00</td>", item3);
}

[Fact]
public async Task GerarHtml_SemInformacaoNoXml_OmiteOsQuadrosFacultativos()
{
// Act
var html = await GerarHtmlAsync(3, comQuadrosFacultativos: false);

// Assert
Assert.DoesNotContain("Fatura / Duplicatas", html);
Assert.DoesNotContain("VALOR DO FCP RETIDO POR ST", html);
Assert.DoesNotContain("BC DO ICMS MONOFÁSICO", html);
Assert.DoesNotContain("VALOR DO IBS MONOFÁSICO", html);
Assert.DoesNotContain("Cálculo do ISSQN", html);
Assert.DoesNotContain("Transportador / volumes transportados", html);
Assert.Contains("Total do IBS / CBS / IS", html);
}

[Fact]
public async Task GerarHtml_ComInformacaoNoXml_ImprimeOsQuadrosFacultativos()
{
// Act
var html = await GerarHtmlAsync(3, issqn: new Issqn { Im = "123456", ValorTotServ = 50m, BaseCalculo = 50m, ValorIssqn = 2.5m });

// Assert
Assert.Contains("Fatura / Duplicatas", html);
Assert.Contains("VALOR DO FCP RETIDO POR ST", html);
Assert.Contains("BC DO ICMS MONOFÁSICO", html);
Assert.Contains("VALOR DO IBS MONOFÁSICO", html);
Assert.Contains("Cálculo do ISSQN", html);
Assert.Contains("Transportador / volumes transportados", html);
Assert.Contains("0 - Remetente", html);
}

[Theory]
[InlineData(1)]
[InlineData(40)]
public async Task GerarHtml_NaoDeixaMarcadoresDoTemplate(int quantidadeProdutos)
{
// Act
var html = await GerarHtmlAsync(quantidadeProdutos);

// Assert
Assert.DoesNotContain("<!--[secao", html);
Assert.DoesNotContain("<!--[/secao", html);
var marcadores = Regex.Matches(html, @"\[[a-z_]+\]").Cast<Match>().Select(m => m.Value).Distinct().ToList();
Assert.Empty(marcadores);
}

[Theory]
//todos os quadros facultativos: 3 itens na primeira folha e 17 nas demais
[InlineData(1, true, 1)]
[InlineData(3, true, 1)]
[InlineData(4, true, 2)]
[InlineData(20, true, 2)]
[InlineData(21, true, 3)]
[InlineData(40, true, 4)]
//sem quadros facultativos: 9 itens na primeira folha e 17 nas demais
[InlineData(9, false, 1)]
[InlineData(10, false, 2)]
[InlineData(26, false, 2)]
[InlineData(27, false, 3)]
public async Task GerarHtml_PaginaPelaCapacidadeDeCadaFolha(int quantidadeProdutos, bool comQuadrosFacultativos, int paginasEsperadas)
{
// Arrange
var issqn = comQuadrosFacultativos ? new Issqn { Im = "123456", ValorTotServ = 50m, BaseCalculo = 50m, ValorIssqn = 2.5m } : null;

// Act
var html = await GerarHtmlAsync(quantidadeProdutos, comQuadrosFacultativos, issqn);

// Assert
Assert.Equal(paginasEsperadas, ContarPaginas(html));
Assert.Contains($"<span>{paginasEsperadas}</span>", html);
for (var i = 1; i <= quantidadeProdutos; i++)
Assert.Contains($"<strong>{i:00000}</strong>", html);
}

[Fact]
public async Task GerarHtml_EscapaTextoDoItem()
{
// Arrange
var nfe = NFeFixture.CriarNfeReformaTributaria(1);
nfe.infNFe.det[0].prod.xProd = "Oleo <15W40> & aditivo";
var danfe = new DanfeNFe(nfe, Status.Autorizada, "1", "", LeiauteDanfe.ReformaTributaria);

// Act
var html = (await new DanfeNfeHtml2(danfe).ObterDocHtmlAsync()).Html;

// Assert
Assert.Contains("Oleo &lt;15W40&gt; &amp; aditivo", html);
}
}
}
80 changes: 80 additions & 0 deletions NFe.Danfe.Html.Testes/DanfeNfeHtmlRenderizacaoTestes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Playwright;
using NFe.Danfe.Html.CrossCutting;
using NFe.Danfe.Html.Dominio;
using NFe.Danfe.Html.Testes.Fixtures;
using Xunit;

namespace NFe.Danfe.Html.Testes
{
/// <summary>
/// Renderiza o DANFE no Chromium (impressão A4, mesmas opções usadas para gerar o PDF via Playwright) e confere que
/// cada página lógica cabe em uma folha. Salva os PDFs em artifacts/danfe-html para conferência visual.
/// <para>
/// Só executa com RUN_PLAYWRIGHT_DANFE_TESTS=1. O Chromium vem do "playwright install chromium" ou do caminho em
/// PLAYWRIGHT_CHROMIUM_EXECUTABLE.
/// </para>
/// </summary>
public class DanfeNfeHtmlRenderizacaoTestes
{
//A4 tem 297 mm; a folga cobre a variação de renderização entre plataformas
private const double AlturaMaximaPaginaMm = 290;

private static bool Habilitado =>
string.Equals(Environment.GetEnvironmentVariable("RUN_PLAYWRIGHT_DANFE_TESTS"), "1", StringComparison.Ordinal);

[Theory]
[InlineData(LeiauteDanfe.Atual, 3, true)]
[InlineData(LeiauteDanfe.Atual, 40, true)]
[InlineData(LeiauteDanfe.ReformaTributaria, 3, true)]
[InlineData(LeiauteDanfe.ReformaTributaria, 40, true)]
[InlineData(LeiauteDanfe.ReformaTributaria, 9, false)]
[InlineData(LeiauteDanfe.ReformaTributaria, 27, false)]
public async Task Renderizar_CadaPaginaCabeEmUmaFolhaA4(LeiauteDanfe leiaute, int quantidadeProdutos, bool comQuadrosFacultativos)
{
if (!Habilitado) return;

// Arrange
var nfe = NFeFixture.CriarNfeReformaTributaria(quantidadeProdutos, comQuadrosFacultativos);
var issqn = comQuadrosFacultativos ? new Issqn { Im = "123456", ValorTotServ = 50m, BaseCalculo = 50m, ValorIssqn = 2.5m } : null;
var danfe = new DanfeNFe(nfe, Status.Autorizada, "135261000001234 - 21/10/2026 10:31:00", "Hercules.NET", leiaute, issqn);
var html = (await new DanfeNfeHtml2(danfe).ObterDocHtmlAsync()).Html;

using var playwright = await Playwright.CreateAsync();
var executavel = Environment.GetEnvironmentVariable("PLAYWRIGHT_CHROMIUM_EXECUTABLE");
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = true,
ExecutablePath = string.IsNullOrWhiteSpace(executavel) ? null : executavel
});
var page = await browser.NewPageAsync();
await page.EmulateMediaAsync(new PageEmulateMediaOptions { Media = Media.Print });

// Act
await page.SetContentAsync(html, new PageSetContentOptions { WaitUntil = WaitUntilState.Load });
var alturas = JsonSerializer.Deserialize<double[]>(await page.EvaluateAsync<string>(
"() => JSON.stringify([...document.querySelectorAll('.boxFields')].map(b => b.getBoundingClientRect().height * 25.4 / 96))"));

var pasta = Path.Combine(AppContext.BaseDirectory, "artifacts", "danfe-html");
Directory.CreateDirectory(pasta);
var nome = $"danfe-{leiaute}-{quantidadeProdutos}-itens{(comQuadrosFacultativos ? "" : "-sem-facultativos")}";
await File.WriteAllTextAsync(Path.Combine(pasta, nome + ".html"), html);
await page.PdfAsync(new PagePdfOptions
{
Path = Path.Combine(pasta, nome + ".pdf"),
Format = "A4",
PrintBackground = true,
PreferCSSPageSize = true
});

// Assert
Assert.NotEmpty(alturas);
Assert.All(alturas, altura => Assert.True(altura <= AlturaMaximaPaginaMm,
$"Página com {altura:0.0} mm não cabe na folha A4 ({string.Join("; ", alturas.Select(a => a.ToString("0.0")))})"));
}
}
}
Loading
Loading