clone

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

chore/app-model-decomp
puzzletea / cmd / export_pdf_test.go
export_pdf_test.go 7.7K
1package cmd23import (4 "bytes"5 "encoding/json"6 "os"7 "path/filepath"8 "strings"9 "testing"10 "time"1112 "github.com/FelineStateMachine/puzzletea/export/pdf"1314 "github.com/spf13/cobra"15)1617func TestExportPDFVolumeFlagDefault(t *testing.T) {18 flag := exportPDFCmd.Flags().Lookup("volume")19 if flag == nil {20 t.Fatal("expected --volume flag")21 }22 if flag.DefValue != "1" {23 t.Fatalf("default volume = %q, want %q", flag.DefValue, "1")24 }25}2627func TestValidatePDFVolume(t *testing.T) {28 tests := []struct {29 name string30 volume int31 wantOK bool32 }{33 {name: "default volume", volume: 1, wantOK: true},34 {name: "positive volume", volume: 7, wantOK: true},35 {name: "zero volume rejected", volume: 0, wantOK: false},36 {name: "negative volume rejected", volume: -3, wantOK: false},37 }3839 for _, tt := range tests {40 t.Run(tt.name, func(t *testing.T) {41 err := validatePDFVolume(tt.volume)42 if tt.wantOK && err != nil {43 t.Fatalf("validatePDFVolume(%d) error = %v, want nil", tt.volume, err)44 }45 if !tt.wantOK {46 if err == nil {47 t.Fatalf("validatePDFVolume(%d) expected error", tt.volume)48 }49 if !strings.Contains(err.Error(), "--volume") {50 t.Fatalf("validatePDFVolume(%d) error = %q, want mention of --volume", tt.volume, err.Error())51 }52 }53 })54 }55}5657func TestBuildRenderConfigForPDFUsesTitleAsCoverSubtitle(t *testing.T) {58 reset := snapshotExportPDFFlags()59 defer reset()6061 flagPDFTitle = "Catacombs & Pines"62 flagPDFHeader = "Custom heading paragraph"63 flagPDFVolume = 764 flagPDFAdvert = "Custom advert"65 flagPDFSheetLayout = "half-letter"6667 now := time.Date(2026, 2, 22, 11, 0, 0, 0, time.UTC)68 docs := []pdfexport.PackDocument{{Metadata: pdfexport.PackMetadata{Category: "Nonogram"}}}69 cfg, err := buildRenderConfigForPDF(docs, "seed-1", now)70 if err != nil {71 t.Fatalf("buildRenderConfigForPDF error = %v", err)72 }73 if cfg.CoverSubtitle != "Catacombs & Pines" {74 t.Fatalf("CoverSubtitle = %q, want %q", cfg.CoverSubtitle, "Catacombs & Pines")75 }76 if cfg.VolumeNumber != 7 {77 t.Fatalf("VolumeNumber = %d, want %d", cfg.VolumeNumber, 7)78 }79 if cfg.HeaderText != "Custom heading paragraph" {80 t.Fatalf("HeaderText = %q, want %q", cfg.HeaderText, "Custom heading paragraph")81 }82 if cfg.AdvertText != "Custom advert" {83 t.Fatalf("AdvertText = %q, want %q", cfg.AdvertText, "Custom advert")84 }85 if !cfg.GeneratedAt.Equal(now) {86 t.Fatalf("GeneratedAt = %v, want %v", cfg.GeneratedAt, now)87 }88}8990func TestBuildRenderConfigForPDFDefaultsSubtitleFromDocs(t *testing.T) {91 reset := snapshotExportPDFFlags()92 defer reset()9394 flagPDFTitle = ""95 flagPDFVolume = 196 flagPDFAdvert = "Find more puzzles"97 flagPDFSheetLayout = "half-letter"9899 docs := []pdfexport.PackDocument{{Metadata: pdfexport.PackMetadata{Category: "Sudoku"}}}100 cfg, err := buildRenderConfigForPDF(docs, "seed-2", time.Now())101 if err != nil {102 t.Fatalf("buildRenderConfigForPDF error = %v", err)103 }104 if cfg.CoverSubtitle != "Sudoku Puzzle Pack" {105 t.Fatalf("CoverSubtitle = %q, want %q", cfg.CoverSubtitle, "Sudoku Puzzle Pack")106 }107}108109func TestSheetLayoutControlsCoverPages(t *testing.T) {110 reset := snapshotExportPDFFlags()111 defer reset()112113 flagPDFTitle = "Issue 01"114 flagPDFVolume = 1115 flagPDFAdvert = "Find more puzzles"116 if pdfexport.SheetLayoutHalfLetter == pdfexport.SheetLayoutDuplexBooklet {117 t.Fatal("expected distinct sheet layout values")118 }119}120121func TestBuildRenderConfigForPDFParsesSheetLayout(t *testing.T) {122 reset := snapshotExportPDFFlags()123 defer reset()124125 flagPDFVolume = 1126 flagPDFSheetLayout = "duplex-booklet"127128 cfg, err := buildRenderConfigForPDF(nil, "seed-5", time.Now())129 if err != nil {130 t.Fatalf("buildRenderConfigForPDF error = %v", err)131 }132 if cfg.SheetLayout != pdfexport.SheetLayoutDuplexBooklet {133 t.Fatalf("SheetLayout = %d, want duplex booklet", cfg.SheetLayout)134 }135}136137func TestBuildRenderConfigForPDFRejectsInvalidSheetLayout(t *testing.T) {138 reset := snapshotExportPDFFlags()139 defer reset()140141 flagPDFVolume = 1142 flagPDFSheetLayout = "brochure"143144 _, err := buildRenderConfigForPDF(nil, "seed-6", time.Now())145 if err == nil {146 t.Fatal("expected invalid --sheet-layout error")147 }148 if !strings.Contains(err.Error(), "--sheet-layout") {149 t.Fatalf("error = %q, want mention of --sheet-layout", err.Error())150 }151}152153func TestRunExportPDFRejectsInputsWhenAllPuzzlesUnsupported(t *testing.T) {154 reset := snapshotExportPDFFlags()155 defer reset()156157 dir := t.TempDir()158 input := filepath.Join(dir, "lights.jsonl")159 output := filepath.Join(dir, "lights.pdf")160161 record := pdfexport.JSONLRecord{162 Schema: pdfexport.ExportSchemaV1,163 Pack: pdfexport.JSONLPackMeta{164 Generated: "2026-02-22T10:00:00Z",165 Version: "v-test",166 Category: "Lights Out",167 ModeSelection: "Standard",168 Count: 1,169 },170 Puzzle: pdfexport.JSONLPuzzle{171 Index: 1,172 Name: "glow-shore",173 Game: "Lights Out",174 Mode: "Standard",175 Save: json.RawMessage(`{"size":5}`),176 },177 }178 data, err := json.Marshal(record)179 if err != nil {180 t.Fatal(err)181 }182 if err := os.WriteFile(input, append(data, '\n'), 0o644); err != nil {183 t.Fatal(err)184 }185186 flagPDFOutput = output187 flagPDFVolume = 1188189 var out bytes.Buffer190 cmd := &cobra.Command{}191 cmd.SetOut(&out)192193 err = runExportPDF(cmd, []string{input})194 if err == nil {195 t.Fatal("expected unsupported export error")196 }197 if !strings.Contains(err.Error(), "no printable puzzles found") {198 t.Fatalf("unexpected error: %v", err)199 }200 if out.String() != "" {201 t.Fatalf("expected no stdout output, got %q", out.String())202 }203 if _, err := os.Stat(output); !os.IsNotExist(err) {204 t.Fatalf("expected no output file, stat err = %v", err)205 }206}207208func TestRunExportPDFSkipsUnsupportedRecordsWhenMixed(t *testing.T) {209 reset := snapshotExportPDFFlags()210 defer reset()211212 dir := t.TempDir()213 input := filepath.Join(dir, "mixed.jsonl")214 output := filepath.Join(dir, "mixed.pdf")215216 records := []pdfexport.JSONLRecord{217 {218 Schema: pdfexport.ExportSchemaV1,219 Pack: pdfexport.JSONLPackMeta{220 Generated: "2026-02-22T10:00:00Z",221 Version: "v-test",222 Category: "Lights Out",223 ModeSelection: "Standard",224 Count: 2,225 },226 Puzzle: pdfexport.JSONLPuzzle{227 Index: 1,228 Name: "glow-shore",229 Game: "Lights Out",230 Mode: "Standard",231 Save: json.RawMessage(`{"size":5}`),232 },233 },234 {235 Schema: pdfexport.ExportSchemaV1,236 Pack: pdfexport.JSONLPackMeta{237 Generated: "2026-02-22T10:00:00Z",238 Version: "v-test",239 Category: "Sudoku",240 ModeSelection: "Easy",241 Count: 2,242 },243 Puzzle: pdfexport.JSONLPuzzle{244 Index: 2,245 Name: "moss-pine",246 Game: "Sudoku",247 Mode: "Easy",248 Save: json.RawMessage(`{"provided":[{"x":0,"y":0,"v":5}]}`),249 },250 },251 }252 var lines []byte253 for _, record := range records {254 line, err := json.Marshal(record)255 if err != nil {256 t.Fatal(err)257 }258 lines = append(lines, line...)259 lines = append(lines, '\n')260 }261 if err := os.WriteFile(input, lines, 0o644); err != nil {262 t.Fatal(err)263 }264265 flagPDFOutput = output266 flagPDFVolume = 1267268 cmd := &cobra.Command{}269 cmd.SetOut(&bytes.Buffer{})270 if err := runExportPDF(cmd, []string{input}); err != nil {271 t.Fatalf("expected mixed export success, got %v", err)272 }273274 info, err := os.Stat(output)275 if err != nil {276 t.Fatalf("expected output file, got stat error: %v", err)277 }278 if info.Size() == 0 {279 t.Fatal("expected non-empty output PDF")280 }281}282283func snapshotExportPDFFlags() func() {284 oldTitle := flagPDFTitle285 oldHeader := flagPDFHeader286 oldVolume := flagPDFVolume287 oldAdvert := flagPDFAdvert288 oldOutput := flagPDFOutput289 oldShuffle := flagPDFShuffleSeed290 oldSheetLayout := flagPDFSheetLayout291292 return func() {293 flagPDFTitle = oldTitle294 flagPDFHeader = oldHeader295 flagPDFVolume = oldVolume296 flagPDFAdvert = oldAdvert297 flagPDFOutput = oldOutput298 flagPDFShuffleSeed = oldShuffle299 flagPDFSheetLayout = oldSheetLayout300 }301}