clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

feat/elo-difficulty
puzzletea / export / pdf / render_layout_test.go
render_layout_test.go 8.9K
1package pdfexport23import (4 "math"5 "testing"6)78func TestThinGridLineFloor(t *testing.T) {9 if thinGridLineMM < 0.22 {10 t.Fatalf("thinGridLineMM = %.2f, want >= 0.22", thinGridLineMM)11 }12}1314func TestCenteredOriginKeepsBoardCentered(t *testing.T) {15 pageArea := puzzleBoardRect(halfLetterWidthMM, halfLetterHeightMM, 2, 1)16 tests := []struct {17 name string18 cols int19 rows int20 family boardFamily21 }{22 {name: "compact", cols: 8, rows: 8, family: boardFamilyCompact},23 {name: "sudoku", cols: 9, rows: 9, family: boardFamilySudoku},24 {name: "hashi-span", cols: 6, rows: 6, family: boardFamilyHashi},25 }2627 for _, tt := range tests {28 t.Run(tt.name, func(t *testing.T) {29 cell := fitBoardCellSize(tt.cols, tt.rows, pageArea, tt.family)30 if cell <= 0 {31 t.Fatal("expected positive cell size")32 }33 x, y := centeredOrigin(pageArea, tt.cols, tt.rows, cell)3435 centerX := x + float64(tt.cols)*cell/236 centerY := y + float64(tt.rows)*cell/237 wantX := pageArea.x + pageArea.w/238 wantY := pageArea.y + pageArea.h/239 if diff := math.Abs(centerX - wantX); diff > 0.01 {40 t.Fatalf("centerX diff = %.4f, want <= 0.01", diff)41 }42 if diff := math.Abs(centerY - wantY); diff > 0.01 {43 t.Fatalf("centerY diff = %.4f, want <= 0.01", diff)44 }45 })46 }47}4849func TestPuzzleBodyRectMirrorsGutterByParity(t *testing.T) {50 even := puzzleBodyRect(halfLetterWidthMM, halfLetterHeightMM, 2)51 odd := puzzleBodyRect(halfLetterWidthMM, halfLetterHeightMM, 3)5253 if even.w <= 0 || odd.w <= 0 {54 t.Fatal("expected positive body widths")55 }56 if math.Abs(even.w-odd.w) > 0.001 {57 t.Fatalf("body widths differ: even=%.3f odd=%.3f", even.w, odd.w)58 }5960 evenCenterX := even.x + even.w/261 oddCenterX := odd.x + odd.w/262 pageCenterX := halfLetterWidthMM / 26364 if evenCenterX >= pageCenterX {65 t.Fatalf("even page center = %.3f, want left of %.3f", evenCenterX, pageCenterX)66 }67 if oddCenterX <= pageCenterX {68 t.Fatalf("odd page center = %.3f, want right of %.3f", oddCenterX, pageCenterX)69 }70}7172func TestSaddleStitchPadCount(t *testing.T) {73 tests := []struct {74 name string75 pages int76 want int77 }{78 {name: "already multiple of four", pages: 36, want: 0},79 {name: "remainder one", pages: 35, want: 1},80 {name: "remainder two", pages: 34, want: 2},81 {name: "remainder three", pages: 33, want: 3},82 {name: "non-positive", pages: 0, want: 0},83 }8485 for _, tt := range tests {86 t.Run(tt.name, func(t *testing.T) {87 got := saddleStitchPadCount(tt.pages)88 if got != tt.want {89 t.Fatalf("saddleStitchPadCount(%d) = %d, want %d", tt.pages, got, tt.want)90 }91 })92 }93}9495func TestSaddleStitchPadCountForStandardPackLayout(t *testing.T) {96 tests := []struct {97 name string98 puzzleRows int99 wantPad int100 }{101 {name: "single puzzle", puzzleRows: 1, wantPad: 2},102 {name: "two puzzles", puzzleRows: 2, wantPad: 1},103 {name: "thirty-two puzzles", puzzleRows: 32, wantPad: 3},104 }105106 for _, tt := range tests {107 t.Run(tt.name, func(t *testing.T) {108 totalWithoutPad := tt.puzzleRows + 5 // outside front + inside front + title + inside back + outside back109 got := saddleStitchPadCount(totalWithoutPad)110 if got != tt.wantPad {111 t.Fatalf("pad pages = %d, want %d", got, tt.wantPad)112 }113 if (totalWithoutPad+got)%4 != 0 {114 t.Fatalf("total pages = %d, want multiple of 4", totalWithoutPad+got)115 }116 })117 }118}119120func TestBookletRenderPlanWithCoverUsesFirstTwoAndLastTwoPages(t *testing.T) {121 plan := newBookletRenderPlan(32, true)122123 if got, want := plan.titlePageNumber(), 3; got != want {124 t.Fatalf("title page = %d, want %d", got, want)125 }126 if got, want := plan.totalPages(), 40; got != want {127 t.Fatalf("total pages = %d, want %d", got, want)128 }129 if got, want := plan.padPages, 3; got != want {130 t.Fatalf("pad pages = %d, want %d", got, want)131 }132133 frontOutside, frontInside, backInside, backOutside, ok := plan.coverPageNumbers()134 if !ok {135 t.Fatal("expected cover page numbers")136 }137 if frontOutside != 1 || frontInside != 2 || backInside != 39 || backOutside != 40 {138 t.Fatalf("cover pages = (%d,%d,%d,%d), want (1,2,39,40)", frontOutside, frontInside, backInside, backOutside)139 }140141 excluded := plan.footerExcludedPages()142 for _, page := range []int{1, 2, 3, 36, 37, 38, 39, 40} {143 if _, ok := excluded[page]; !ok {144 t.Fatalf("expected page %d to be footer-excluded", page)145 }146 }147 for _, page := range []int{4, 20, 35} {148 if _, ok := excluded[page]; ok {149 t.Fatalf("did not expect puzzle page %d to be footer-excluded", page)150 }151 }152}153154func TestBookletRenderPlanWithoutCoverKeepsTitleFirst(t *testing.T) {155 plan := newBookletRenderPlan(2, false)156157 if got, want := plan.titlePageNumber(), 1; got != want {158 t.Fatalf("title page = %d, want %d", got, want)159 }160 if got, want := plan.totalPages(), 4; got != want {161 t.Fatalf("total pages = %d, want %d", got, want)162 }163164 excluded := plan.footerExcludedPages()165 for _, page := range []int{1, 4} {166 if _, ok := excluded[page]; !ok {167 t.Fatalf("expected page %d to be footer-excluded", page)168 }169 }170 if _, ok := excluded[2]; ok {171 t.Fatal("did not expect first puzzle page to be footer-excluded")172 }173}174175func TestBuildLogicalPagesWithCoverUsesExpectedSequence(t *testing.T) {176 pages := buildLogicalPages(32, true)177 if got, want := len(pages), 40; got != want {178 t.Fatalf("logical pages = %d, want %d", got, want)179 }180181 if pages[0].Kind != logicalPageCoverOutside || pages[0].OutsideSlice != coverOutsideFront {182 t.Fatal("page 1 should be front outside cover")183 }184 if pages[1].Kind != logicalPageCoverBlank {185 t.Fatal("page 2 should be inside front blank")186 }187 if pages[2].Kind != logicalPageTitle {188 t.Fatal("page 3 should be title")189 }190 if pages[len(pages)-2].Kind != logicalPageCoverBlank {191 t.Fatal("second-last page should be inside back blank")192 }193 if pages[len(pages)-1].Kind != logicalPageCoverOutside || pages[len(pages)-1].OutsideSlice != coverOutsideBack {194 t.Fatal("last page should be back outside cover")195 }196}197198func TestDuplexBookletSheetsForFourPageBooklet(t *testing.T) {199 sheets := duplexBookletSheets(4)200 if got, want := len(sheets), 1; got != want {201 t.Fatalf("sheet count = %d, want %d", got, want)202 }203 if sheets[0].Front.LeftPage != 4 || sheets[0].Front.RightPage != 1 {204 t.Fatalf("front pair = (%d,%d), want (4,1)", sheets[0].Front.LeftPage, sheets[0].Front.RightPage)205 }206 if sheets[0].Back.LeftPage != 2 || sheets[0].Back.RightPage != 3 {207 t.Fatalf("back pair = (%d,%d), want (2,3)", sheets[0].Back.LeftPage, sheets[0].Back.RightPage)208 }209}210211func TestDuplexBookletSheetsForEightPageBooklet(t *testing.T) {212 sheets := duplexBookletSheets(8)213 if got, want := len(sheets), 2; got != want {214 t.Fatalf("sheet count = %d, want %d", got, want)215 }216 if sheets[0].Front.LeftPage != 8 || sheets[0].Front.RightPage != 1 {217 t.Fatalf("outer front pair = (%d,%d), want (8,1)", sheets[0].Front.LeftPage, sheets[0].Front.RightPage)218 }219 if sheets[1].Back.LeftPage != 4 || sheets[1].Back.RightPage != 5 {220 t.Fatalf("inner back pair = (%d,%d), want (4,5)", sheets[1].Back.LeftPage, sheets[1].Back.RightPage)221 }222}223224func TestDuplexBookletSheetsWithCoverOuterSheetPairs(t *testing.T) {225 pages := buildLogicalPages(32, true)226 sheets := duplexBookletSheets(len(pages))227 if got, want := len(sheets), 10; got != want {228 t.Fatalf("sheet count = %d, want %d", got, want)229 }230 if sheets[0].Front.LeftPage != 40 || sheets[0].Front.RightPage != 1 {231 t.Fatalf("outer front pair = (%d,%d), want (40,1)", sheets[0].Front.LeftPage, sheets[0].Front.RightPage)232 }233 if sheets[0].Back.LeftPage != 2 || sheets[0].Back.RightPage != 39 {234 t.Fatalf("outer back pair = (%d,%d), want (2,39)", sheets[0].Back.LeftPage, sheets[0].Back.RightPage)235 }236}237238func TestDuplexBookletPhysicalPageCountIsHalfLogicalPages(t *testing.T) {239 for _, total := range []int{4, 8, 40} {240 sheets := duplexBookletSheets(total)241 got := len(sheets) * 2242 want := total / 2243 if got != want {244 t.Fatalf("physical pages for %d logical pages = %d, want %d", total, got, want)245 }246 }247}248249func TestNewRenderPDFDuplexBookletUsesLandscapeLetterCanvas(t *testing.T) {250 pdf := newRenderPDF(SheetLayoutDuplexBooklet)251 pageW, pageH := pdf.GetPageSize()252253 if pageW <= pageH {254 t.Fatalf("duplex-booklet canvas = %.1fx%.1f, want landscape", pageW, pageH)255 }256 if math.Abs(pageW-letterWidthMM) > 0.01 || math.Abs(pageH-letterHeightMM) > 0.01 {257 t.Fatalf(258 "duplex-booklet canvas = %.1fx%.1f, want %.1fx%.1f",259 pageW,260 pageH,261 letterWidthMM,262 letterHeightMM,263 )264 }265}266267func TestSaddleStitchPadCountForTitleOnlyPackLayout(t *testing.T) {268 tests := []struct {269 name string270 puzzleRows int271 wantPad int272 }{273 {name: "single puzzle", puzzleRows: 1, wantPad: 2},274 {name: "two puzzles", puzzleRows: 2, wantPad: 1},275 {name: "thirty-two puzzles", puzzleRows: 32, wantPad: 3},276 }277278 for _, tt := range tests {279 t.Run(tt.name, func(t *testing.T) {280 totalWithoutPad := tt.puzzleRows + 1 // title page + puzzle pages281 got := saddleStitchPadCount(totalWithoutPad)282 if got != tt.wantPad {283 t.Fatalf("pad pages = %d, want %d", got, tt.wantPad)284 }285 if (totalWithoutPad+got)%4 != 0 {286 t.Fatalf("total pages = %d, want multiple of 4", totalWithoutPad+got)287 }288 })289 }290}