diff --git a/resources/views/pdf.blade.php b/resources/views/pdf.blade.php index 192e081..54a0acd 100644 --- a/resources/views/pdf.blade.php +++ b/resources/views/pdf.blade.php @@ -81,7 +81,8 @@ width: 40px; } - .day:last-child { + .day:last-child, + .region:last-child { page-break-after: auto; } @@ -163,7 +164,9 @@ @elseif ($group_by === 'region-day') @foreach ($regions as $region => $days)
- {{ $region }} + @if ($region) + {{ $region }} + @endif @foreach ($days as $day => $meetings)
@if ($day) diff --git a/tests/Feature/PdfTest.php b/tests/Feature/PdfTest.php index ffe3872..2a17d76 100644 --- a/tests/Feature/PdfTest.php +++ b/tests/Feature/PdfTest.php @@ -59,6 +59,128 @@ public function pdf_generates_with_cjk_language(): void $this->assertSame('application/pdf', $response->headers->get('Content-Type')); } + #[Test] + public function region_day_page_breaks_do_not_emit_a_trailing_blank_page(): void + { + $this->fakeMultiRegionFeed(); + + $response = $this->get('/pdf?json=https://example.test/regions&group_by=region-day&options[]=pagebreaks'); + + $response->assertOk(); + $this->assertSame( + 3, + $this->countPdfPages($response->getContent()), + 'Expected one page per region (3); an extra page means the final region still emits page-break-after: always.' + ); + } + + #[Test] + public function day_region_page_breaks_do_not_emit_a_trailing_blank_page(): void + { + $this->fakeMultiRegionFeed(); + + $response = $this->get('/pdf?json=https://example.test/regions&group_by=day-region&options[]=pagebreaks'); + + $response->assertOk(); + $this->assertSame(2, $this->countPdfPages($response->getContent()), 'Expected one page per day (2).'); + } + + #[Test] + public function region_day_page_breaks_do_not_emit_blank_pages_on_the_chunked_path(): void + { + Http::fake(['example.test/large' => Http::response($this->largeMultiRegionFeed(60), 200)]); + + $response = $this->get('/pdf?json=https://example.test/large&group_by=region-day&options[]=pagebreaks'); + + $response->assertOk(); + $this->assertSame( + 60, + $this->countPdfPages($response->getContent()), + 'Expected one page per region (60). Feeds over 500 meetings render each region as a separate ' + .'PDF and merge with FPDI; without a .region:last-child suppressor every chunk ends with a ' + .'trailing blank page, which the merge preserves — 120 pages, half of them blank.' + ); + } + + /** + * A feed large enough to exceed the 500-meeting fast path in Controller::pdf() + * and exercise the chunked render + FPDI merge instead. + * + * @return list> + */ + protected function largeMultiRegionFeed(int $regions): array + { + $meetings = []; + + foreach (range(1, $regions) as $r) { + $region = 'Region'.str_pad((string) $r, 2, '0', STR_PAD_LEFT); + + foreach ([1, 3, 5] as $day) { + for ($i = 0; $i < 3; $i++) { + $meetings[] = [ + 'slug' => strtolower($region)."-$day-$i", + 'name' => $region.' AA Group '.$i, + 'day' => $day, + 'time' => '19:00', + 'address' => (100 + $i).' Main St, '.$region.', MS', + 'regions' => ['Mississippi', 'North', $region], + 'types' => ['O'], + ]; + } + } + } + + return $meetings; + } + + #[Test] + public function region_day_omits_the_heading_for_meetings_with_no_region(): void + { + $meeting = (object) [ + 'time_formatted' => '7 am', + 'name' => 'Sun Up Group', + 'location' => null, + 'address' => '127 Front St', + 'regions_formatted' => '', + 'types' => ['O'], + ]; + + $html = view('pdf', [ + 'language' => 'en', + 'font' => 'Noto Sans', + 'font_size' => 10, + 'numbering' => false, + 'group_by' => 'region-day', + 'types_in_use' => [], + 'types' => [], + 'options' => [], + 'meeting_types_heading' => 'Meeting Types', + 'days' => collect(), + 'regions' => collect(['' => collect(['MONDAY' => collect([$meeting])])]), + ])->render(); + + $this->assertStringNotContainsString( + '', + preg_replace('~\s*~', '', $html), + 'A region-less group must not emit an empty .heading — its border-bottom renders as a stray rule.' + ); + } + + protected function fakeMultiRegionFeed(): void + { + Http::fake([ + 'example.test/regions' => Http::response( + json_decode(file_get_contents(base_path('tests/Fixtures/meetings-multi-region.json')), true), + 200 + ), + ]); + } + + protected function countPdfPages(string $pdf): int + { + return preg_match_all('~/Type\s*/Page[^s]~', $pdf); + } + #[Test] public function pdf_generates_from_google_sheets_url(): void { diff --git a/tests/Fixtures/meetings-multi-region.json b/tests/Fixtures/meetings-multi-region.json new file mode 100644 index 0000000..a135a58 --- /dev/null +++ b/tests/Fixtures/meetings-multi-region.json @@ -0,0 +1,158 @@ +[ + { + "slug": "alpha-0-0", + "name": "Alpha Meeting 00", + "day": 0, + "time": "19:00", + "address": "100 Main St, Alpha, MS", + "regions": [ + "Alpha" + ], + "types": [ + "O" + ] + }, + { + "slug": "alpha-0-1", + "name": "Alpha Meeting 01", + "day": 0, + "time": "19:00", + "address": "101 Main St, Alpha, MS", + "regions": [ + "Alpha" + ], + "types": [ + "O" + ] + }, + { + "slug": "alpha-1-0", + "name": "Alpha Meeting 10", + "day": 1, + "time": "19:00", + "address": "100 Main St, Alpha, MS", + "regions": [ + "Alpha" + ], + "types": [ + "O" + ] + }, + { + "slug": "alpha-1-1", + "name": "Alpha Meeting 11", + "day": 1, + "time": "19:00", + "address": "101 Main St, Alpha, MS", + "regions": [ + "Alpha" + ], + "types": [ + "O" + ] + }, + { + "slug": "bravo-0-0", + "name": "Bravo Meeting 00", + "day": 0, + "time": "19:00", + "address": "100 Main St, Bravo, MS", + "regions": [ + "Bravo" + ], + "types": [ + "O" + ] + }, + { + "slug": "bravo-0-1", + "name": "Bravo Meeting 01", + "day": 0, + "time": "19:00", + "address": "101 Main St, Bravo, MS", + "regions": [ + "Bravo" + ], + "types": [ + "O" + ] + }, + { + "slug": "bravo-1-0", + "name": "Bravo Meeting 10", + "day": 1, + "time": "19:00", + "address": "100 Main St, Bravo, MS", + "regions": [ + "Bravo" + ], + "types": [ + "O" + ] + }, + { + "slug": "bravo-1-1", + "name": "Bravo Meeting 11", + "day": 1, + "time": "19:00", + "address": "101 Main St, Bravo, MS", + "regions": [ + "Bravo" + ], + "types": [ + "O" + ] + }, + { + "slug": "charlie-0-0", + "name": "Charlie Meeting 00", + "day": 0, + "time": "19:00", + "address": "100 Main St, Charlie, MS", + "regions": [ + "Charlie" + ], + "types": [ + "O" + ] + }, + { + "slug": "charlie-0-1", + "name": "Charlie Meeting 01", + "day": 0, + "time": "19:00", + "address": "101 Main St, Charlie, MS", + "regions": [ + "Charlie" + ], + "types": [ + "O" + ] + }, + { + "slug": "charlie-1-0", + "name": "Charlie Meeting 10", + "day": 1, + "time": "19:00", + "address": "100 Main St, Charlie, MS", + "regions": [ + "Charlie" + ], + "types": [ + "O" + ] + }, + { + "slug": "charlie-1-1", + "name": "Charlie Meeting 11", + "day": 1, + "time": "19:00", + "address": "101 Main St, Charlie, MS", + "regions": [ + "Charlie" + ], + "types": [ + "O" + ] + } +] \ No newline at end of file