+ @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