Update 3d chart mode, add US heatmap, move kjol-web -> kjol-website
This commit is contained in:
179
go/cmd/kjol-website/app/ssr_test.go
Normal file
179
go/cmd/kjol-website/app/ssr_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kjol/vdom"
|
||||
"kjol/webui"
|
||||
)
|
||||
|
||||
func TestSSRPages(t *testing.T) {
|
||||
for _, path := range []string{"/", "/about", "/wasm/chart", "/wasm/data", "/wasm/components"} {
|
||||
deps := Deps{Path: func() string { return path }}
|
||||
html := vdom.RenderHTML(Shell(deps, Routes(deps)))
|
||||
t.Logf("%-8s %6d bytes portals=%d", path, len(html), strings.Count(html, "data-portal"))
|
||||
if len(html) < 200 {
|
||||
t.Errorf("%s rendered only %d bytes", path, len(html))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSRTablePage(t *testing.T) {
|
||||
deps := Deps{Path: func() string { return "/wasm/components" }}
|
||||
html := vdom.RenderHTML(Shell(deps, Routes(deps)))
|
||||
|
||||
// The table persists a personal layout in localStorage, which the SERVER CANNOT
|
||||
// READ. So the server renders a SKELETON, not the default table: if it rendered
|
||||
// the default one, a user who had reordered their columns would watch them
|
||||
// rearrange themselves once the wasm booted.
|
||||
//
|
||||
// This is a real cost — the page ships no table content — and it is the price of
|
||||
// never showing the wrong table. See webui.RestoreLayout.
|
||||
if !strings.Contains(html, `aria-busy="true"`) {
|
||||
t.Error("SSR should render the AutoTable's loading skeleton, not a table")
|
||||
}
|
||||
if !strings.Contains(html, "animate-pulse") {
|
||||
t.Error("the skeleton bars are missing")
|
||||
}
|
||||
|
||||
// A salary, which ONLY the AutoTable renders.
|
||||
//
|
||||
// This used to look for "Ada Lovelace", which was a fine proxy back when the table
|
||||
// had a page to itself. It is not one any more: the components page also demos
|
||||
// PrettyTable, and PrettyTable's rows are Ada, Alan and Grace — so the old assertion
|
||||
// failed on a page that was behaving perfectly. A test that names a value only the
|
||||
// component under test can produce cannot be fooled by its neighbours.
|
||||
if strings.Contains(html, "$1,610.25") {
|
||||
t.Error("SSR rendered AutoTable CONTENT — a user with a saved layout would watch it rearrange")
|
||||
}
|
||||
}
|
||||
|
||||
// renderedTable drives the very table the page renders, past its skeleton. Natively
|
||||
// there is nothing to restore, so RestoreLayout just marks the layout settled.
|
||||
func renderedTable(t *testing.T) string {
|
||||
t.Helper()
|
||||
highlight := vdom.NewSignal("")
|
||||
table := newEmployeeTable(highlight)
|
||||
table.SetRows(employees())
|
||||
table.RestoreLayout()
|
||||
return vdom.RenderHTML(table.Render())
|
||||
}
|
||||
|
||||
// Once the layout has settled, the table renders in full.
|
||||
func TestTableRendersOnceSettled(t *testing.T) {
|
||||
html := renderedTable(t)
|
||||
|
||||
for _, want := range []string{"Ada Lovelace", "Salary"} {
|
||||
if !strings.Contains(html, want) {
|
||||
t.Errorf("settled table missing %q", want)
|
||||
}
|
||||
}
|
||||
// PerPage is 5, so page one holds 5 of the 12 rows.
|
||||
if got := strings.Count(html, "@example.com"); got != 5 {
|
||||
t.Errorf("rendered %d rows, want 5 (one page)", got)
|
||||
}
|
||||
// The Rank column is HiddenByDefault.
|
||||
if strings.Contains(html, ">Rank<") {
|
||||
t.Error("a HiddenByDefault column was rendered")
|
||||
}
|
||||
if !strings.Contains(html, "Page 1 of 3") {
|
||||
t.Error("pagination did not compute 3 pages for 12 rows at 5/page")
|
||||
}
|
||||
}
|
||||
|
||||
// Calculated columns, end to end through the page, in all three shapes.
|
||||
//
|
||||
// Page 1 (declared order):
|
||||
//
|
||||
// salary 1200.50 1500.00 980.00 1340.00 1610.25
|
||||
// bonus 150.00 300.00 0.00 220.00 400.00
|
||||
func TestSSRCalculatedColumns(t *testing.T) {
|
||||
html := renderedTable(t)
|
||||
|
||||
// BASIC: sum over the operand columns [Salary, Bonus], combined ACROSS each row.
|
||||
// If this ever aggregated DOWN the column instead, every row would read the same
|
||||
// number — which is exactly the bug these values are here to catch.
|
||||
for _, want := range []string{"$1,350.50", "$1,800.00", "$980.00", "$1,560.00", "$2,010.25"} {
|
||||
if !strings.Contains(html, want) {
|
||||
t.Errorf("Total comp missing %s (a per-row Salary + Bonus)", want)
|
||||
}
|
||||
}
|
||||
|
||||
// ADVANCED: ([Salary] + [Bonus]) * 12.
|
||||
for _, want := range []string{"$16,206.00", "$21,600.00", "$11,760.00"} {
|
||||
if !strings.Contains(html, want) {
|
||||
t.Errorf("Annual column missing %s", want)
|
||||
}
|
||||
}
|
||||
|
||||
// ADVANCED, position-dependent: SUM({Salary:1:ROW()}) accumulates down the rows.
|
||||
for _, want := range []string{"$2,700.50", "$3,680.50", "$5,020.50", "$6,630.75"} {
|
||||
if !strings.Contains(html, want) {
|
||||
t.Errorf("running total missing %s", want)
|
||||
}
|
||||
}
|
||||
|
||||
// SUMMARY: aggregated DOWN the column, over ALL 12 filtered rows — not the 5 on
|
||||
// this page. 1200.50+1500+980+1340+1610.25+1120+1275.75+1050+1400+860+1180+990.
|
||||
if !strings.Contains(html, "$14,506.50") {
|
||||
t.Error("footer did not total the whole filtered set ($14,506.50)")
|
||||
}
|
||||
if !strings.Contains(html, "Average salary") {
|
||||
t.Error("summary row label missing")
|
||||
}
|
||||
}
|
||||
|
||||
// The export path, driven through the very table the /table page renders.
|
||||
//
|
||||
// Export must write what the FILTER selected — every matching row across every page
|
||||
// — not the five rows on screen; the columns the user can SEE, in their order; and
|
||||
// the calculated columns, with each row's own value.
|
||||
func TestTableExport(t *testing.T) {
|
||||
highlight := vdom.NewSignal("")
|
||||
table := newEmployeeTable(highlight)
|
||||
table.RestoreLayout() // nothing to restore natively; reveals the table over its skeleton
|
||||
table.SetRows(employees())
|
||||
|
||||
// Filter to one team, then render (which resolves FilteredRows).
|
||||
table.SetSearchValue("Team", "Research", true)
|
||||
table.Render()
|
||||
|
||||
csv := string(webui.ExportCSV(table.ExportColumns(), table.FilteredRows(), nil))
|
||||
|
||||
// PerPage is 5 and Research has 4 members, but the point is that export ignores
|
||||
// paging entirely: every filtered row, no one else's.
|
||||
for _, want := range []string{"Alan Turing", "Katherine Johnson", "Barbara Liskov", "Evelyn Boyd Granville"} {
|
||||
if !strings.Contains(csv, want) {
|
||||
t.Errorf("CSV missing filtered row %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(csv, "Ada Lovelace") {
|
||||
t.Error("CSV contains a row the filter excluded")
|
||||
}
|
||||
// Rank is HiddenByDefault, so it must not be exported.
|
||||
if strings.Contains(csv, "Item 10") {
|
||||
t.Error("CSV exported a hidden column")
|
||||
}
|
||||
// The calculated columns come along, and the running total ACCUMULATES —
|
||||
// $1,500.00 then $2,840.00 (Turing + Johnson), not the same number twice.
|
||||
if !strings.Contains(csv, "Running total") || !strings.Contains(csv, "$2,840.00") {
|
||||
t.Errorf("running total did not accumulate in the export:\n%s", csv)
|
||||
}
|
||||
|
||||
// And the PDF: a real file, with the same filtered content.
|
||||
pdf := table.ExportPDFBytes(webui.AutoTablePDFHeader{
|
||||
Title: "Employees", ShowDate: true, Orientation: webui.PDF_ORIENTATION_LANDSCAPE,
|
||||
})
|
||||
if !bytes.HasPrefix(pdf, []byte("%PDF-")) || !bytes.Contains(pdf, []byte("%%EOF")) {
|
||||
t.Fatalf("PDF is not a PDF (%d bytes)", len(pdf))
|
||||
}
|
||||
if out := os.Getenv("PDF_OUT"); out != "" {
|
||||
if err := os.WriteFile(out, pdf, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("wrote %s (%d bytes)", out, len(pdf))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user