From bb6324adae434382fdcc38e36555ed756b719da8 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 9 Sep 2026 15:12:12 -0700 Subject: [PATCH] Add nested location group for 7.0 --- DOCUMENTATION.md | 41 ++- resources/fieldsets/event.yaml | 38 ++ src/Types/Event.php | 49 +-- tests/Http/Contollers/IcsControllerTest.php | 375 +++++++++++--------- 4 files changed, 288 insertions(+), 215 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index c856a6d..1cc503a 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -44,22 +44,33 @@ ICS downloads read the following entry fields when present: | ICS property | Source | |---|---| -| `LOCATION` | `address`, falling back to `location` — only when the value is a string | -| `URL` | `online_url`, then deprecated `link`, then deprecated `location` when that string is a URL | -| `GEO` | `coordinates` (`latitude` / `longitude`) | +| `LOCATION` | `location.name`, falling back to `online_url` when the name is empty | +| `URL` | `online_url` | +| `GEO` | `location.coordinates` (`latitude` / `longitude`) | | `DESCRIPTION` | `description` | -A URL-valued `location` (with no separate `address` / `online_url` / `link`) currently emits **both** `LOCATION:` and `URL:`. Prefer `online_url` for join links; `link` and the URL-valued `location` fallback are deprecated and will be removed in 7.0. +`location` and `online_url` are independent and combinable (hybrid events). -The `coordinates` field must be a keyed array: +| Event | `LOCATION:` | `URL:` | `GEO:` | +|---|---|---|---| +| Physical only | `location.name` | — | `location.coordinates` | +| Online only | `online_url` | `online_url` | — | +| Hybrid | `location.name` | `online_url` | `location.coordinates` | + +`location` must be a group. A string or other non-group value is skipped (no `LOCATION:` from it). Nested coordinates shape: ```php -'coordinates' => [ - 'latitude' => 40, - 'longitude' => 50, +'location' => [ + 'name' => '123 Main St', + 'coordinates' => [ + 'latitude' => 40, + 'longitude' => 50, + ], ], ``` +Partial or non-numeric coordinates are skipped (no `GEO:`) rather than failing the download. + If your field names differ from the defaults above, use a [Computed Value](https://statamic.dev/content-modeling/computed-values#defining-computed-values) to map them. --- @@ -83,9 +94,19 @@ Using the sample fieldset is the fastest way to get started. | Field | Description | |-------|-------------| -| `online_url` | Join link for online or hybrid events (Zoom, livestream, etc.). Optional; can be combined with a physical place once `location` is declared. | +| `location` | Group: `name` (localizable text — venue, description, or address) and optional nested `coordinates` (`latitude` / `longitude`). | +| `online_url` | Join link for online or hybrid events (Zoom, livestream, etc.). Optional; independent of `location`. | + +### Upgrading to 7.0 + +Breaking changes for location fields: + +- `location` is now a **group** (`name` + nested `coordinates`), not a string +- `address`, `link`, and top-level `coordinates` are no longer read +- URL sniffing on a string `location` is gone — use `online_url` for join links +- Protected `eventUrl()` / `icsAddress()` were replaced by `icsUrl()` / `icsLocation()` -`link` is deprecated in favour of `online_url` and will be removed in 7.0. +An update script (#196) migrates common legacy handles. Back up content before upgrading. Computed-value mappings and foreign (e.g. Prime) `location` shapes need a separate cutover. ### Single-Day Events diff --git a/resources/fieldsets/event.yaml b/resources/fieldsets/event.yaml index 9ba674c..a503206 100644 --- a/resources/fieldsets/event.yaml +++ b/resources/fieldsets/event.yaml @@ -208,6 +208,44 @@ fields: field: type: section display: Location + - + handle: location + field: + type: group + display: Location + fullscreen: false + border: false + localizable: true + instructions: 'Physical place for the event — a plain description or an address. Optional; combine with Online URL for hybrid events.' + fields: + - + handle: name + field: + type: text + display: Name + localizable: true + instructions: 'Venue name, plain description, or street address (ICS LOCATION).' + - + handle: coordinates + field: + type: group + display: Coordinates + fullscreen: false + border: false + instructions: 'Optional map coordinates for ICS GEO.' + fields: + - + handle: latitude + field: + type: float + display: Latitude + width: 50 + - + handle: longitude + field: + type: float + display: Longitude + width: 50 - handle: online_url field: diff --git a/src/Types/Event.php b/src/Types/Event.php index fcb496c..534845a 100644 --- a/src/Types/Event.php +++ b/src/Types/Event.php @@ -7,7 +7,6 @@ use DateTimeInterface; use Illuminate\Support\Arr; use Illuminate\Support\Collection; -use Illuminate\Support\Str; use RRule\RRuleInterface; use Spatie\IcalendarGenerator\Components\Event as ICalendarEvent; use Statamic\Entries\Entry; @@ -133,11 +132,11 @@ protected function buildICalendarEvent(string|CarbonInterface $date): ICalendarE protected function decorate(ICalendarEvent $iCalEvent): ICalendarEvent { - if ($address = $this->icsAddress()) { - $iCalEvent->address($address); + if ($location = $this->icsLocation()) { + $iCalEvent->address($location); } - if ($this->hasValidCoordinates($coords = $this->event->get('coordinates'))) { + if ($this->hasValidCoordinates($coords = Arr::get($this->event->get('location'), 'coordinates'))) { $iCalEvent->coordinates((float) $coords['latitude'], (float) $coords['longitude']); } @@ -145,47 +144,37 @@ protected function decorate(ICalendarEvent $iCalEvent): ICalendarEvent $iCalEvent->description($description); } - if (! is_null($url = $this->eventUrl())) { + if (! is_null($url = $this->icsUrl())) { $iCalEvent->url($url); } return $iCalEvent; } - protected function eventUrl(): ?string + // Entry::get() is untyped; keep mixed so a bad value can't TypeError the public ICS route. + protected function hasValidCoordinates(mixed $coords): bool { - if (is_string($url = $this->event->get('online_url')) && $url !== '') { - return $url; - } - - // @deprecated Will be removed in 7.0. Use online_url. - if (is_string($link = $this->event->get('link')) && $link !== '') { - return $link; - } + return is_array($coords) + && is_numeric($coords['latitude'] ?? null) + && is_numeric($coords['longitude'] ?? null); + } - // @deprecated Will be removed in 7.0. Use online_url. - $location = $this->event->get('location'); + protected function icsLocation(): ?string + { + $name = Arr::get($this->event->get('location'), 'name'); - if (! is_string($location) || $location === '') { - return null; + if (is_string($name) && $name !== '') { + return $name; } - return Str::isUrl($location) ? $location : null; + return $this->icsUrl(); } - protected function icsAddress(): ?string + protected function icsUrl(): ?string { - $address = $this->event->address ?? $this->event->get('location'); - - return is_string($address) && $address !== '' ? $address : null; - } + $url = $this->event->get('online_url'); - // Entry::get() is untyped; keep mixed so a bad value can't TypeError the public ICS route. - protected function hasValidCoordinates(mixed $coords): bool - { - return is_array($coords) - && is_numeric($coords['latitude'] ?? null) - && is_numeric($coords['longitude'] ?? null); + return is_string($url) && $url !== '' ? $url : null; } protected function supplement(CarbonInterface $date): ?Entry diff --git a/tests/Http/Contollers/IcsControllerTest.php b/tests/Http/Contollers/IcsControllerTest.php index 121c5ae..6c60b42 100755 --- a/tests/Http/Contollers/IcsControllerTest.php +++ b/tests/Http/Contollers/IcsControllerTest.php @@ -15,11 +15,12 @@ 'start_date' => Carbon::now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', - 'address' => '123 Main St', - 'location' => 'The Location', - 'coordinates' => [ - 'latitude' => 40, - 'longitude' => 50, + 'location' => [ + 'name' => '123 Main St', + 'coordinates' => [ + 'latitude' => 40, + 'longitude' => 50, + ], ], 'description' => 'The description', ])->save(); @@ -54,7 +55,9 @@ 'start_time' => '11:00', 'end_time' => '12:00', 'recurrence' => 'weekly', - 'location' => 'The Location', + 'location' => [ + 'name' => 'The Location', + ], 'description' => 'The description', ])->save(); @@ -86,7 +89,9 @@ 'start_time' => '11:00', 'end_time' => '12:00', 'recurrence' => 'weekly', - 'location' => 'The Location', + 'location' => [ + 'name' => 'The Location', + ], 'description' => 'The description', ])->save(); @@ -113,7 +118,9 @@ 'start_time' => '11:00', 'end_time' => '12:00', 'recurrence' => 'weekly', - 'location' => 'The Location', + 'location' => [ + 'name' => 'The Location', + ], 'description' => 'The description', ])->save(); @@ -136,7 +143,9 @@ ->data([ 'title' => 'Multi-day Event', 'multi_day' => true, - 'location' => 'The Location', + 'location' => [ + 'name' => 'The Location', + ], 'description' => 'The description', 'days' => [ [ @@ -197,7 +206,7 @@ ]))->assertStatus(404); }); -test('can create ics when location is a group without address', function () { +test('location group without name does not emit LOCATION', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); Entry::make() @@ -219,26 +228,25 @@ 'description' => 'The description', ])->save(); - $response = $this->get(route('statamic.events.ics.show', [ + $content = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), 'event' => 'the-grouped-location-id', - ]))->assertDownload('grouped-location-event.ics'); - - $content = $response->streamedContent(); + ]))->assertDownload('grouped-location-event.ics')->streamedContent(); $this->assertStringNotContainsString('LOCATION:', $content); + $this->assertStringContainsString('GEO:40;50', $content); $this->assertStringContainsString('DESCRIPTION:The description', $content); }); -test('falls back to string location when address is missing', function () { +test('string location is ignored', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); Entry::make() ->collection('events') - ->slug('location-fallback-event') - ->id('the-location-fallback-id') + ->slug('string-location-event') + ->id('string-location-id') ->data([ - 'title' => 'Location Fallback Event', + 'title' => 'String Location Event', 'start_date' => Carbon::now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', @@ -246,176 +254,110 @@ 'description' => 'The description', ])->save(); - $response = $this->get(route('statamic.events.ics.show', [ + $content = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), - 'event' => 'the-location-fallback-id', - ]))->assertDownload('location-fallback-event.ics'); + 'event' => 'string-location-id', + ]))->assertDownload('string-location-event.ics')->streamedContent(); - $this->assertStringContainsString('LOCATION:The Location', $response->streamedContent()); + $this->assertStringNotContainsString('LOCATION:', $content); }); -test('can create recurring ics when location is a group without address', function () { - Carbon::setTestNow(now()->addDay()->setTimeFromTimeString('10:00')); +test('address and link and top-level coordinates are no longer read', function () { + Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); Entry::make() ->collection('events') - ->slug('grouped-recurring-event') - ->id('the-grouped-recurring-id') + ->slug('legacy-handles-event') + ->id('legacy-handles-id') ->data([ - 'title' => 'Grouped Recurring Event', + 'title' => 'Legacy Handles Event', 'start_date' => Carbon::now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', - 'recurrence' => 'weekly', - 'location' => [ - 'details' => 'Virtual', - ], - 'description' => 'The description', - ])->save(); - - $response = $this->get(route('statamic.events.ics.show', [ - 'event' => 'the-grouped-recurring-id', - ]))->assertDownload('grouped-recurring-event.ics'); - - $this->assertStringNotContainsString('LOCATION:', $response->streamedContent()); -}); - -test('can create multi-day ics when location is a group without address', function () { - Carbon::setTestNow(now()); - - Entry::make() - ->slug('grouped-multi-day-event') - ->collection('events') - ->id('the-grouped-multi-day-id') - ->data([ - 'title' => 'Grouped Multi-day Event', - 'multi_day' => true, - 'location' => [ - 'details' => 'Virtual', - ], - 'description' => 'The description', - 'days' => [ - [ - 'date' => now()->toDateString(), - 'start_time' => '19:00', - 'end_time' => '21:00', - ], - ], - ])->save(); - - $response = $this->get(route('statamic.events.ics.show', [ - 'date' => now()->toDateString(), - 'event' => 'the-grouped-multi-day-id', - ]))->assertDownload('grouped-multi-day-event.ics'); - - $this->assertStringNotContainsString('LOCATION:', $response->streamedContent()); -}); - -test('multi-day whole-event download includes location url description and geo on every day', function () { - Carbon::setTestNow(now()); - - Entry::make() - ->slug('multi-day-whole-event') - ->collection('events') - ->id('the-multi-day-whole-event') - ->data([ - 'title' => 'Multi-day Whole Event', - 'multi_day' => true, 'address' => '123 Main St', 'link' => 'https://example.com/join', 'coordinates' => [ 'latitude' => 40, 'longitude' => 50, ], - 'description' => 'The description', - 'days' => [ - [ - 'date' => now()->toDateString(), - 'start_time' => '19:00', - 'end_time' => '21:00', - ], - [ - 'date' => now()->addDay()->toDateString(), - 'start_time' => '11:00', - 'end_time' => '15:00', - ], - ], ])->save(); $content = $this->get(route('statamic.events.ics.show', [ - 'event' => 'the-multi-day-whole-event', - ]))->assertDownload('multi-day-whole-event.ics')->streamedContent(); + 'date' => now()->toDateString(), + 'event' => 'legacy-handles-id', + ]))->assertDownload('legacy-handles-event.ics')->streamedContent(); - expect(substr_count($content, 'LOCATION:123 Main St'))->toBe(2) - ->and(substr_count($content, 'URL:https://example.com/join'))->toBe(2) - ->and(substr_count($content, 'DESCRIPTION:The description'))->toBe(2) - ->and(substr_count($content, 'GEO:40;50'))->toBe(2); + $this->assertStringNotContainsString('LOCATION:', $content); + $this->assertStringNotContainsString('URL:', $content); + $this->assertStringNotContainsString('GEO:', $content); }); -test('partial coordinates do not fatal and omit geo', function () { +test('url in location name is treated as a location not a join url', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); Entry::make() ->collection('events') - ->slug('partial-coords-event') - ->id('partial-coords-id') + ->slug('url-name-event') + ->id('url-name-id') ->data([ - 'title' => 'Partial Coords Event', + 'title' => 'URL Name Event', 'start_date' => Carbon::now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', - 'address' => '123 Main St', - 'coordinates' => [ - 'latitude' => 40, + 'location' => [ + 'name' => 'https://zoom.us/j/456', ], ])->save(); $content = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), - 'event' => 'partial-coords-id', - ]))->assertDownload('partial-coords-event.ics')->streamedContent(); + 'event' => 'url-name-id', + ]))->assertDownload('url-name-event.ics')->streamedContent(); - $this->assertStringContainsString('LOCATION:123 Main St', $content); - $this->assertStringNotContainsString('GEO:', $content); + $this->assertStringContainsString('LOCATION:https://zoom.us/j/456', $content); + $this->assertStringNotContainsString('URL:', $content); }); -test('non-numeric coordinates do not fatal and omit geo', function () { +test('physical only emits LOCATION and GEO', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); Entry::make() ->collection('events') - ->slug('bad-coords-event') - ->id('bad-coords-id') + ->slug('physical-only-event') + ->id('physical-only-id') ->data([ - 'title' => 'Bad Coords Event', + 'title' => 'Physical Only Event', 'start_date' => Carbon::now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', - 'address' => '123 Main St', - 'coordinates' => [ - 'latitude' => 'north', - 'longitude' => 'west', + 'location' => [ + 'name' => '123 Main St', + 'coordinates' => [ + 'latitude' => 40, + 'longitude' => 50, + ], ], ])->save(); $content = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), - 'event' => 'bad-coords-id', - ]))->assertDownload('bad-coords-event.ics')->streamedContent(); + 'event' => 'physical-only-id', + ]))->assertDownload('physical-only-event.ics')->streamedContent(); $this->assertStringContainsString('LOCATION:123 Main St', $content); - $this->assertStringNotContainsString('GEO:', $content); + $this->assertStringContainsString('GEO:40;50', $content); + $this->assertStringNotContainsString('URL:', $content); }); -test('online_url emits URL', function () { +test('online only emits LOCATION and URL from online_url', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); Entry::make() ->collection('events') - ->slug('online-url-event') - ->id('online-url-id') + ->slug('online-only-event') + ->id('online-only-id') ->data([ - 'title' => 'Online URL Event', + 'title' => 'Online Only Event', 'start_date' => Carbon::now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', @@ -424,119 +366,196 @@ $content = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), - 'event' => 'online-url-id', - ]))->assertDownload('online-url-event.ics')->streamedContent(); + 'event' => 'online-only-id', + ]))->assertDownload('online-only-event.ics')->streamedContent(); + $this->assertStringContainsString('LOCATION:https://zoom.us/j/123', $content); $this->assertStringContainsString('URL:https://zoom.us/j/123', $content); + $this->assertStringNotContainsString('GEO:', $content); }); -test('online_url wins over link when both are set', function () { +test('hybrid emits location name URL and GEO', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); Entry::make() ->collection('events') - ->slug('online-over-link-event') - ->id('online-over-link-id') + ->slug('hybrid-event') + ->id('hybrid-id') ->data([ - 'title' => 'Online Over Link Event', + 'title' => 'Hybrid Event', 'start_date' => Carbon::now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', + 'location' => [ + 'name' => '123 Main St', + 'coordinates' => [ + 'latitude' => 40, + 'longitude' => 50, + ], + ], 'online_url' => 'https://zoom.us/j/123', - 'link' => 'https://example.com/old-link', ])->save(); $content = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), - 'event' => 'online-over-link-id', - ]))->assertDownload('online-over-link-event.ics')->streamedContent(); + 'event' => 'hybrid-id', + ]))->assertDownload('hybrid-event.ics')->streamedContent(); + $this->assertStringContainsString('LOCATION:123 Main St', $content); $this->assertStringContainsString('URL:https://zoom.us/j/123', $content); - $this->assertStringNotContainsString('URL:https://example.com/old-link', $content); + $this->assertStringContainsString('GEO:40;50', $content); +}); + +test('multi-day whole-event download includes location url description and geo on every day', function () { + Carbon::setTestNow(now()); + + Entry::make() + ->slug('multi-day-whole-event') + ->collection('events') + ->id('the-multi-day-whole-event') + ->data([ + 'title' => 'Multi-day Whole Event', + 'multi_day' => true, + 'location' => [ + 'name' => '123 Main St', + 'coordinates' => [ + 'latitude' => 40, + 'longitude' => 50, + ], + ], + 'online_url' => 'https://example.com/join', + 'description' => 'The description', + 'days' => [ + [ + 'date' => now()->toDateString(), + 'start_time' => '19:00', + 'end_time' => '21:00', + ], + [ + 'date' => now()->addDay()->toDateString(), + 'start_time' => '11:00', + 'end_time' => '15:00', + ], + ], + ])->save(); + + $content = $this->get(route('statamic.events.ics.show', [ + 'event' => 'the-multi-day-whole-event', + ]))->assertDownload('multi-day-whole-event.ics')->streamedContent(); + + expect(substr_count($content, 'LOCATION:123 Main St'))->toBe(2) + ->and(substr_count($content, 'URL:https://example.com/join'))->toBe(2) + ->and(substr_count($content, 'DESCRIPTION:The description'))->toBe(2) + ->and(substr_count($content, 'GEO:40;50'))->toBe(2); }); -test('deprecated link alone still emits URL', function () { +test('partial coordinates do not fatal and omit geo', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); Entry::make() ->collection('events') - ->slug('link-only-event') - ->id('link-only-id') + ->slug('partial-coords-event') + ->id('partial-coords-id') ->data([ - 'title' => 'Link Only Event', + 'title' => 'Partial Coords Event', 'start_date' => Carbon::now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', - 'link' => 'https://example.com/join', + 'location' => [ + 'name' => '123 Main St', + 'coordinates' => [ + 'latitude' => 40, + ], + ], ])->save(); $content = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), - 'event' => 'link-only-id', - ]))->assertDownload('link-only-event.ics')->streamedContent(); + 'event' => 'partial-coords-id', + ]))->assertDownload('partial-coords-event.ics')->streamedContent(); - $this->assertStringContainsString('URL:https://example.com/join', $content); + $this->assertStringContainsString('LOCATION:123 Main St', $content); + $this->assertStringNotContainsString('GEO:', $content); }); -test('deprecated URL-valued location alone still emits LOCATION and URL', function () { +test('non-numeric coordinates do not fatal and omit geo', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); Entry::make() ->collection('events') - ->slug('url-location-event') - ->id('url-location-id') + ->slug('bad-coords-event') + ->id('bad-coords-id') ->data([ - 'title' => 'URL Location Event', + 'title' => 'Bad Coords Event', 'start_date' => Carbon::now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', - 'location' => 'https://zoom.us/j/456', + 'location' => [ + 'name' => '123 Main St', + 'coordinates' => [ + 'latitude' => 'north', + 'longitude' => 'west', + ], + ], ])->save(); $content = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), - 'event' => 'url-location-id', - ]))->assertDownload('url-location-event.ics')->streamedContent(); + 'event' => 'bad-coords-id', + ]))->assertDownload('bad-coords-event.ics')->streamedContent(); - $this->assertStringContainsString('LOCATION:https://zoom.us/j/456', $content); - $this->assertStringContainsString('URL:https://zoom.us/j/456', $content); + $this->assertStringContainsString('LOCATION:123 Main St', $content); + $this->assertStringNotContainsString('GEO:', $content); }); -test('online_url is included on all four download routes', function () { +test('location and online_url are included on all four download routes', function () { Carbon::setTestNow(now()); Entry::make() ->collection('events') - ->slug('online-single') - ->id('online-single-id') + ->slug('place-single') + ->id('place-single-id') ->data([ - 'title' => 'Online Single', + 'title' => 'Place Single', 'start_date' => now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', + 'location' => [ + 'name' => 'Hall A', + 'coordinates' => [ + 'latitude' => 10, + 'longitude' => 20, + ], + ], 'online_url' => 'https://zoom.us/j/single', ])->save(); Entry::make() ->collection('events') - ->slug('online-recurring') - ->id('online-recurring-id') + ->slug('place-recurring') + ->id('place-recurring-id') ->data([ - 'title' => 'Online Recurring', + 'title' => 'Place Recurring', 'start_date' => now()->toDateString(), 'start_time' => '11:00', 'end_time' => '12:00', 'recurrence' => 'weekly', + 'location' => [ + 'name' => 'Hall B', + ], 'online_url' => 'https://zoom.us/j/recurring', ])->save(); Entry::make() ->collection('events') - ->slug('online-multi-day') - ->id('online-multi-day-id') + ->slug('place-multi-day') + ->id('place-multi-day-id') ->data([ - 'title' => 'Online Multi Day', + 'title' => 'Place Multi Day', 'multi_day' => true, + 'location' => [ + 'name' => 'Hall C', + ], 'online_url' => 'https://zoom.us/j/multiday', 'days' => [ [ @@ -554,30 +573,36 @@ $single = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), - 'event' => 'online-single-id', - ]))->assertDownload('online-single.ics')->streamedContent(); + 'event' => 'place-single-id', + ]))->assertDownload('place-single.ics')->streamedContent(); $recurringDate = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), - 'event' => 'online-recurring-id', - ]))->assertDownload('online-recurring.ics')->streamedContent(); + 'event' => 'place-recurring-id', + ]))->assertDownload('place-recurring.ics')->streamedContent(); $recurringWhole = $this->get(route('statamic.events.ics.show', [ - 'event' => 'online-recurring-id', - ]))->assertDownload('online-recurring.ics')->streamedContent(); + 'event' => 'place-recurring-id', + ]))->assertDownload('place-recurring.ics')->streamedContent(); $multiDate = $this->get(route('statamic.events.ics.show', [ 'date' => now()->toDateString(), - 'event' => 'online-multi-day-id', - ]))->assertDownload('online-multi-day.ics')->streamedContent(); + 'event' => 'place-multi-day-id', + ]))->assertDownload('place-multi-day.ics')->streamedContent(); $multiWhole = $this->get(route('statamic.events.ics.show', [ - 'event' => 'online-multi-day-id', - ]))->assertDownload('online-multi-day.ics')->streamedContent(); + 'event' => 'place-multi-day-id', + ]))->assertDownload('place-multi-day.ics')->streamedContent(); + $this->assertStringContainsString('LOCATION:Hall A', $single); $this->assertStringContainsString('URL:https://zoom.us/j/single', $single); + $this->assertStringContainsString('GEO:10;20', $single); + $this->assertStringContainsString('LOCATION:Hall B', $recurringDate); $this->assertStringContainsString('URL:https://zoom.us/j/recurring', $recurringDate); + $this->assertStringContainsString('LOCATION:Hall B', $recurringWhole); $this->assertStringContainsString('URL:https://zoom.us/j/recurring', $recurringWhole); + $this->assertStringContainsString('LOCATION:Hall C', $multiDate); $this->assertStringContainsString('URL:https://zoom.us/j/multiday', $multiDate); - expect(substr_count($multiWhole, 'URL:https://zoom.us/j/multiday'))->toBe(2); + expect(substr_count($multiWhole, 'LOCATION:Hall C'))->toBe(2) + ->and(substr_count($multiWhole, 'URL:https://zoom.us/j/multiday'))->toBe(2); });