package webui import ( "regexp" "strconv" "strings" "testing" ) // pdfHorizontalRules extracts every horizontal line the PDF draws, as (y, width). // The content stream draws a line as: " w m l S". func pdfHorizontalRules(pdf []byte) map[string]int { re := regexp.MustCompile(`([\d.]+) w\s*\n?[^\n]*?([\d.]+) ([\d.]+) m\s*\n?[^\n]*?([\d.]+) ([\d.]+) l`) out := map[string]int{} for _, m := range re.FindAllStringSubmatch(string(pdf), -1) { y1, y2 := m[3], m[5] if y1 != y2 { continue // not horizontal } out[y1]++ } return out } // The bug: the row loop drew a rule under EVERY row including the last, and then the // summary block drew its divider at exactly the same y. Two rules at one y read as a // double border between the table and its summaries. func TestPDFDrawsOneRuleBetweenTableAndSummaries(t *testing.T) { s := newCalcTable() s.AddSummaryRow(UserSummaryRow{ID: "t", Label: "Total", Fn: CALC_FN_SUM, Operands: []string{"Revenue"}, DataType: CALC_TYPE_NUMBER, Precision: CalcPrecision(0)}) s.Render() rules := pdfHorizontalRules(s.ExportPDFBytes(AutoTablePDFHeader{Title: "R"})) if len(rules) == 0 { t.Fatal("no horizontal rules found — the extractor is wrong, not the PDF") } for y, n := range rules { if n > 1 { t.Errorf("%d rules stacked at y=%s — that is the double border", n, y) } } } // Without summaries the table still gets its closing rule; removing the last row's // rule must not leave the table open at the bottom. func TestPDFClosesTheTableWithoutSummaries(t *testing.T) { s := newCalcTable() s.Render() pdf := s.ExportPDFBytes(AutoTablePDFHeader{Title: "R"}) rules := pdfHorizontalRules(pdf) if len(rules) < 2 { t.Errorf("expected a header rule and a closing rule, got %d distinct rules", len(rules)) } for y, n := range rules { if n > 1 { t.Errorf("%d rules stacked at y=%s", n, y) } } // And the lowest rule sits below the last row of text, i.e. the table is closed. lowest := 1e9 for y := range rules { if f, err := strconv.ParseFloat(y, 64); err == nil && f < lowest { lowest = f } } if lowest > 700 || !strings.Contains(string(pdf), "%%EOF") { t.Errorf("closing rule at y=%v looks wrong", lowest) } } // The extractor must actually see stacked rules, or the two tests above are vacuous. func TestPDFRuleExtractorCatchesADoubledRule(t *testing.T) { pdf := NewPDF(PDFOptions{}) pdf.Line(50, 300, 500, 300, 0.5, PDFColor{}) pdf.Line(50, 300, 500, 300, 1, PDFColor{}) // deliberately stacked pdf.Line(50, 200, 500, 200, 0.5, PDFColor{}) rules := pdfHorizontalRules(pdf.Bytes()) doubled := 0 for _, n := range rules { if n > 1 { doubled++ } } if doubled != 1 { t.Fatalf("extractor found %d stacked positions, want exactly 1 — it cannot see the bug it is meant to catch (%v)", doubled, rules) } }