From c6dc4aa8743e79806207c9be7547fa80f0bbacd6 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 30 Sep 2026 15:52:29 -0700 Subject: [PATCH] Test events whose local time falls on another UTC day. Midday times and same-zone fixtures hid the evening ICS date bug. --- tests/EventsTest.php | 111 +++++++++++++++++++- tests/Http/Contollers/IcsControllerTest.php | 23 ++++ tests/Types/SingleDayEventsTest.php | 25 +++++ 3 files changed, 158 insertions(+), 1 deletion(-) diff --git a/tests/EventsTest.php b/tests/EventsTest.php index 13de7ca..40db169 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -294,7 +294,64 @@ $event = EventFactory::createFromEntry($entry); - expect($event->occursOnDate(CarbonImmutable::parse('2026-10-02')))->toBeTrue(); + expect($event->occursOnDate(CarbonImmutable::parse('2026-10-02')))->toBeTrue() + ->and($event->occursOnDate(CarbonImmutable::parse('2026-10-01')))->toBeFalse() + ->and($event->occursOnDate(CarbonImmutable::parse('2026-10-03')))->toBeFalse() + ->and($event->start()->format('Y-m-d H:i'))->toBe('2026-10-02 19:00') + ->and($event->start()->timezone->getName())->toBe('America/Los_Angeles'); +}); + +test('multi day evening event occurs on its local dates when those dates are parsed in the app timezone', function () { + $entry = Entry::make() + ->collection('events') + ->slug('multi-day-evening') + ->data([ + 'title' => 'Multi-day Evening', + 'multi_day' => true, + 'timezone' => 'America/Los_Angeles', + 'days' => [ + [ + 'date' => '2026-10-02', + 'start_time' => '19:00', + 'end_time' => '21:00', + ], + [ + 'date' => '2026-10-03', + 'start_time' => '11:00', + 'end_time' => '15:00', + ], + ], + ]); + + $event = EventFactory::createFromEntry($entry); + + expect($event->start()->format('Y-m-d H:i'))->toBe('2026-10-02 19:00') + ->and($event->start()->timezone->getName())->toBe('America/Los_Angeles') + ->and($event->occursOnDate(CarbonImmutable::parse('2026-10-01')))->toBeFalse() + ->and($event->occursOnDate(CarbonImmutable::parse('2026-10-02')))->toBeTrue() + ->and($event->occursOnDate(CarbonImmutable::parse('2026-10-03')))->toBeTrue() + ->and($event->occursOnDate(CarbonImmutable::parse('2026-10-04')))->toBeFalse(); +}); + +test('recurring evening event includes its last local day', function () { + $entry = Entry::make() + ->collection('events') + ->slug('recurring-evening') + ->data([ + 'title' => 'Recurring Evening', + 'start_date' => '2026-10-01', + 'end_date' => '2026-10-02', + 'start_time' => '19:00', + 'end_time' => '21:00', + 'recurrence' => 'daily', + 'timezone' => 'America/Los_Angeles', + ]); + + $event = EventFactory::createFromEntry($entry); + + expect($event->occursOnDate(CarbonImmutable::parse('2026-10-01')))->toBeTrue() + ->and($event->occursOnDate(CarbonImmutable::parse('2026-10-02')))->toBeTrue() + ->and($event->occursOnDate(CarbonImmutable::parse('2026-10-03')))->toBeFalse(); }); test('can determine occurs at for multiday event', function () { @@ -354,6 +411,32 @@ expect($occurrences)->toHaveCount(3); }); +test('can exclude an evening occurrence on its local date', function () { + Entry::make() + ->collection('events') + ->slug('recurring-evening') + ->data([ + 'title' => 'Recurring Evening', + 'start_date' => '2026-10-01', + 'end_date' => '2026-10-03', + 'start_time' => '19:00', + 'end_time' => '21:00', + 'recurrence' => 'daily', + 'timezone' => 'America/Los_Angeles', + 'exclude_dates' => [['date' => '2026-10-02']], + ])->save(); + + $occurrences = Events::fromCollection(handle: 'events') + ->between( + CarbonImmutable::parse('2026-10-01')->startOfDay(), + CarbonImmutable::parse('2026-10-04')->endOfDay(), + ); + + expect($occurrences)->toHaveCount(2) + ->and($occurrences->get(0)->start->format('Y-m-d H:i'))->toBe('2026-10-02 02:00') + ->and($occurrences->get(1)->start->format('Y-m-d H:i'))->toBe('2026-10-04 02:00'); +}); + test('can handle empty exclude dates', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); @@ -463,6 +546,32 @@ expect($events2)->toHaveCount(1); }); +test('early morning event ahead of UTC stays on its local date', function () { + $entry = Entry::make() + ->collection('events') + ->data([ + 'start_date' => '2026-10-02', + 'timezone' => 'Asia/Tokyo', + 'start_time' => '01:00', + 'end_time' => '02:00', + ]); + + $entry->save(); + + $event = EventFactory::createFromEntry($entry); + + expect($event->occursOnDate(CarbonImmutable::parse('2026-10-02')))->toBeTrue() + ->and($event->occursOnDate(CarbonImmutable::parse('2026-10-01')))->toBeFalse() + ->and($event->start()->format('Y-m-d H:i'))->toBe('2026-10-02 01:00') + ->and($event->start()->timezone->getName())->toBe('Asia/Tokyo'); + + $previousUtcDay = CarbonImmutable::parse('2026-10-01'); + $localDay = CarbonImmutable::parse('2026-10-02'); + + expect(Events::fromCollection('events')->between($previousUtcDay->startOfDay(), $previousUtcDay->endOfDay()))->toHaveCount(1); + expect(Events::fromCollection('events')->between($localDay->startOfDay(), $localDay->endOfDay()))->toHaveCount(0); +}); + it('uses UTC when no app timezone set', function () { expect(Events::defaultTimezone())->toBe('UTC'); }); diff --git a/tests/Http/Contollers/IcsControllerTest.php b/tests/Http/Contollers/IcsControllerTest.php index 121c5ae..edf1518 100755 --- a/tests/Http/Contollers/IcsControllerTest.php +++ b/tests/Http/Contollers/IcsControllerTest.php @@ -581,3 +581,26 @@ $this->assertStringContainsString('URL:https://zoom.us/j/multiday', $multiDate); expect(substr_count($multiWhole, 'URL:https://zoom.us/j/multiday'))->toBe(2); }); + +test('recurring ics until includes the last local evening', function () { + Entry::make() + ->collection('events') + ->slug('recurring-evening') + ->id('recurring-evening-id') + ->data([ + 'title' => 'Recurring Evening', + 'start_date' => '2026-10-01', + 'end_date' => '2026-10-02', + 'start_time' => '19:00', + 'end_time' => '21:00', + 'recurrence' => 'daily', + 'timezone' => 'America/Los_Angeles', + ])->save(); + + $content = $this->get(route('statamic.events.ics.show', [ + 'event' => 'recurring-evening-id', + ]))->assertDownload('recurring-evening.ics')->streamedContent(); + + $this->assertStringContainsString('DTSTART:20261001T190000', $content); + $this->assertStringContainsString('UNTIL=20261002T235959', $content); +}); diff --git a/tests/Types/SingleDayEventsTest.php b/tests/Types/SingleDayEventsTest.php index 7e0f297..26e0996 100755 --- a/tests/Types/SingleDayEventsTest.php +++ b/tests/Types/SingleDayEventsTest.php @@ -195,3 +195,28 @@ [now()->startOfDay(), now()->endOfDay()->addDay(), 1], [now()->startOfDay(), now()->endOfDay(), 1], ]); + +test('evening event is still upcoming after the utc date rolls over', function () { + $entry = Entry::make() + ->collection('events') + ->data([ + 'start_date' => '2026-10-02', + 'start_time' => '19:00', + 'end_time' => '21:00', + 'timezone' => 'America/Los_Angeles', + ]); + + $event = EventFactory::createFromEntry($entry); + + Carbon::setTestNow(CarbonImmutable::parse('2026-10-03 01:00:00')); + + $next = $event->nextOccurrences(); + + expect($next)->toHaveCount(1) + ->and($next->first()->start->format('Y-m-d H:i'))->toBe('2026-10-02 19:00') + ->and($next->first()->start->timezone->getName())->toBe('America/Los_Angeles'); + + Carbon::setTestNow(CarbonImmutable::parse('2026-10-03 05:00:00')); + + expect($event->nextOccurrences())->toBeEmpty(); +});