Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions resources/views/pdf.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
width: 40px;
}

.day:last-child {
.day:last-child,
.region:last-child {
page-break-after: auto;
}

Expand Down Expand Up @@ -163,7 +164,9 @@
@elseif ($group_by === 'region-day')
@foreach ($regions as $region => $days)
<div class="region">
<span class="heading">{{ $region }}</span>
@if ($region)
<span class="heading">{{ $region }}</span>
@endif
@foreach ($days as $day => $meetings)
<div class="day">
@if ($day)
Expand Down
122 changes: 122 additions & 0 deletions tests/Feature/PdfTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<array<string, mixed>>
*/
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(
'<span class="heading"></span>',
preg_replace('~<span class="heading">\s*</span>~', '<span class="heading"></span>', $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
{
Expand Down
158 changes: 158 additions & 0 deletions tests/Fixtures/meetings-multi-region.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]