Generate check / check-changes (pull_request) Successful in 26s
Generate check / verify-generate (pull_request) Skipped
Quality / check-changes (pull_request) Successful in 2s
Quality / static-checks (pull_request) Failing after 16s
Quality / run-tests (pull_request) Failing after 56s
181 lines
5.4 KiB
Go
181 lines
5.4 KiB
Go
package html_test
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nmoniz/any2anexoj/internal"
|
|
"github.com/nmoniz/any2anexoj/internal/writing"
|
|
htmlwriter "github.com/nmoniz/any2anexoj/internal/writing/html"
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
// buildTestAggregator returns an AggregatorWriter seeded with two
|
|
// deterministic ReportItems so every test can assert on a stable document
|
|
// shape. Mirrors the fixture pattern used in
|
|
// internal/writing/table/printer_test.go.
|
|
func buildTestAggregator(t *testing.T) *internal.AggregatorWriter {
|
|
t.Helper()
|
|
aw := internal.NewAggregatorWriter()
|
|
ctx := t.Context()
|
|
|
|
err := aw.Write(ctx, internal.ReportItem{
|
|
Symbol: "AAPL",
|
|
Nature: internal.NatureG01,
|
|
BrokerCountry: 826,
|
|
AssetCountry: 840,
|
|
BuyValue: decimal.NewFromFloat(100.50),
|
|
BuyTimestamp: time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC),
|
|
SellValue: decimal.NewFromFloat(150.75),
|
|
SellTimestamp: time.Date(2023, 6, 20, 0, 0, 0, 0, time.UTC),
|
|
Fees: decimal.NewFromFloat(2.50),
|
|
Taxes: decimal.NewFromFloat(5.00),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to write first report item: %v", err)
|
|
}
|
|
|
|
err = aw.Write(ctx, internal.ReportItem{
|
|
Symbol: "GOOGL",
|
|
Nature: internal.NatureG20,
|
|
BrokerCountry: 826,
|
|
AssetCountry: 840,
|
|
BuyValue: decimal.NewFromFloat(200.00),
|
|
BuyTimestamp: time.Date(2023, 3, 10, 0, 0, 0, 0, time.UTC),
|
|
SellValue: decimal.NewFromFloat(225.50),
|
|
SellTimestamp: time.Date(2023, 9, 5, 0, 0, 0, 0, time.UTC),
|
|
Fees: decimal.NewFromFloat(3.00),
|
|
Taxes: decimal.NewFromFloat(7.50),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to write second report item: %v", err)
|
|
}
|
|
|
|
return aw
|
|
}
|
|
|
|
func TestWriter_Render(t *testing.T) {
|
|
loc, err := writing.NewLocalizer("en")
|
|
if err != nil {
|
|
t.Fatalf("create localizer: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
hw := htmlwriter.NewWriter(&buf, loc, "en")
|
|
|
|
if err := hw.Render(buildTestAggregator(t)); err != nil {
|
|
t.Fatalf("Render returned an error: %v", err)
|
|
}
|
|
|
|
got := buf.String()
|
|
|
|
wantSubstrings := []string{
|
|
"<!DOCTYPE html>",
|
|
`<html lang="en">`,
|
|
`<meta charset="utf-8">`,
|
|
`<meta name="viewport" content="width=device-width, initial-scale=1">`,
|
|
`<title>Tax report</title>`,
|
|
`https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css`,
|
|
`integrity="`,
|
|
`crossorigin="anonymous"`,
|
|
`<main class="container-fluid py-4">`,
|
|
`<h1>Tax report</h1>`,
|
|
`<div class="table-responsive">`,
|
|
`<table class="table table-striped table-hover table-bordered" style="table-layout: fixed">`,
|
|
`<colgroup>`,
|
|
`</table>`,
|
|
`<tfoot>`,
|
|
`</tfoot>`,
|
|
`</html>`,
|
|
`<p class="text-muted small">Generated at `,
|
|
}
|
|
for _, s := range wantSubstrings {
|
|
if !strings.Contains(got, s) {
|
|
t.Errorf("rendered HTML missing expected substring %q\nGot:\n%s", s, got)
|
|
}
|
|
}
|
|
|
|
// The table fragment must be embedded as raw HTML, not escaped.
|
|
if strings.Contains(got, "<table") {
|
|
t.Errorf("table fragment appears to be HTML-escaped; expected raw <table> markup\nGot:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "<table") {
|
|
t.Errorf("expected raw <table> tag in output\nGot:\n%s", got)
|
|
}
|
|
|
|
// Generated timestamp must be valid RFC3339 UTC.
|
|
startIdx := strings.Index(got, "Generated at ")
|
|
if startIdx == -1 {
|
|
t.Fatalf("missing 'Generated at' prefix in output")
|
|
}
|
|
timestampLine := got[startIdx:]
|
|
endIdx := strings.Index(timestampLine, "</p>")
|
|
if endIdx == -1 {
|
|
t.Fatalf("missing </p> closing tag after 'Generated at' line")
|
|
}
|
|
timestampStr := strings.TrimPrefix(strings.SplitN(timestampLine, "</p>", 2)[0], "Generated at ")
|
|
if _, err := time.Parse(time.RFC3339, timestampStr); err != nil {
|
|
t.Errorf("generation timestamp is not valid RFC3339: %q (err: %v)", timestampStr, err)
|
|
}
|
|
}
|
|
|
|
// TestWriter_NoRowDuplication verifies that calling Render twice on the
|
|
// same writer produces documents with the same number of <tr> rows, just
|
|
// like table.Printer's no-duplication guarantee.
|
|
func TestWriter_NoRowDuplication(t *testing.T) {
|
|
loc, err := writing.NewLocalizer("en")
|
|
if err != nil {
|
|
t.Fatalf("create localizer: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
hw := htmlwriter.NewWriter(&buf, loc, "en")
|
|
|
|
aw := buildTestAggregator(t)
|
|
|
|
if err := hw.Render(aw); err != nil {
|
|
t.Fatalf("first Render returned an error: %v", err)
|
|
}
|
|
first := buf.String()
|
|
buf.Reset()
|
|
|
|
if err := hw.Render(aw); err != nil {
|
|
t.Fatalf("second Render returned an error: %v", err)
|
|
}
|
|
second := buf.String()
|
|
|
|
firstRows := strings.Count(first, "<tr")
|
|
secondRows := strings.Count(second, "<tr")
|
|
if firstRows == 0 {
|
|
t.Fatalf("expected at least one <tr> row in the first render; got:\n%s", first)
|
|
}
|
|
if firstRows != secondRows {
|
|
t.Errorf("expected second Render to produce the same number of <tr> rows as the first (no duplication)\nfirst rows=%d, second rows=%d\nFirst:\n%s\nSecond:\n%s",
|
|
firstRows, secondRows, first, second)
|
|
}
|
|
}
|
|
|
|
func TestWriter_LangAttribute(t *testing.T) {
|
|
loc, err := writing.NewLocalizer("pt")
|
|
if err != nil {
|
|
t.Fatalf("create localizer: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
hw := htmlwriter.NewWriter(&buf, loc, "pt")
|
|
|
|
if err := hw.Render(buildTestAggregator(t)); err != nil {
|
|
t.Fatalf("Render returned an error: %v", err)
|
|
}
|
|
|
|
got := buf.String()
|
|
if !strings.Contains(got, `<html lang="pt">`) {
|
|
t.Errorf("expected <html lang=\"pt\"> attribute; got:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, `<h1>Relatório fiscal</h1>`) {
|
|
t.Errorf("expected Portuguese title in <h1>; got:\n%s", got)
|
|
}
|
|
}
|