')
- ->and($plugin->render_markup)->toContain('
{{ data.title }}
');
-});
-
-it('imports plugin with only shared.blade.php file', function (): void {
- $user = User::factory()->create();
-
- $zipContent = createMockZipFile([
- 'src/settings.yml' => getValidSettingsYaml(),
- 'src/shared.blade.php' => '
{{ $data["title"] }}
',
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
-
- $pluginImportService = new PluginImportService();
- $plugin = $pluginImportService->importFromZip($zipFile, $user);
-
- expect($plugin)->toBeInstanceOf(Plugin::class)
- ->and($plugin->markup_language)->toBe('blade')
- ->and($plugin->render_markup)->toBe('
{{ $data["title"] }}
')
- ->and($plugin->render_markup)->not->toContain('
');
-});
-
// Helper methods
function createMockZipFile(array $files): string
{
diff --git a/tests/Feature/PluginLiquidFilterTest.php b/tests/Feature/PluginLiquidFilterTest.php
index e6272c7..bc0fc18 100644
--- a/tests/Feature/PluginLiquidFilterTest.php
+++ b/tests/Feature/PluginLiquidFilterTest.php
@@ -29,6 +29,7 @@ assign collection = json_string | parse_json
{{ tide | json }}
{%- endfor %}
LIQUID
+ ,
]);
$result = $plugin->render('full');
@@ -54,6 +55,7 @@ assign collection = json_string | parse_json
{{ tide | json }}
{%- endfor %}
LIQUID
+ ,
]);
$result = $plugin->render('full');
@@ -79,6 +81,7 @@ assign collection = json_string | parse_json
{{ tide | json }}
{%- endfor %}
LIQUID
+ ,
]);
$result = $plugin->render('full');
@@ -119,58 +122,3 @@ it('keeps scalar url_encode behavior intact', function (): void {
expect($output)->toBe('hello+world');
});
-
-test('where_exp filter works in liquid template', function (): void {
- $plugin = Plugin::factory()->create([
- 'markup_language' => 'liquid',
- 'render_markup' => <<<'LIQUID'
-{% liquid
-assign nums = "1, 2, 3, 4, 5" | split: ", " | map_to_i
-assign filtered = nums | where_exp: "n", "n >= 3"
-%}
-
-{% for num in filtered %}
- {{ num }}
-{%- endfor %}
-LIQUID
- ]);
-
- $result = $plugin->render('full');
-
- // Debug: Let's see what the actual output is
- // The issue might be that the HTML contains "1" in other places
- // Let's check if the filtered numbers are actually in the content
- $this->assertStringContainsString('3', $result);
- $this->assertStringContainsString('4', $result);
- $this->assertStringContainsString('5', $result);
-
- // Instead of checking for absence of 1 and 2, let's verify the count
- // The filtered result should only contain 3, 4, 5
- $filteredContent = strip_tags((string) $result);
- $this->assertStringNotContainsString('1', $filteredContent);
- $this->assertStringNotContainsString('2', $filteredContent);
-});
-
-test('where_exp filter works with object properties', function (): void {
- $plugin = Plugin::factory()->create([
- 'markup_language' => 'liquid',
- 'render_markup' => <<<'LIQUID'
-{% liquid
-assign users = '[{"name":"Alice","age":25},{"name":"Bob","age":30},{"name":"Charlie","age":35}]' | parse_json
-assign adults = users | where_exp: "user", "user.age >= 30"
-%}
-
-{% for user in adults %}
- {{ user.name }} ({{ user.age }})
-{%- endfor %}
-LIQUID
- ]);
-
- $result = $plugin->render('full');
-
- // Should output users >= 30
- $this->assertStringContainsString('Bob (30)', $result);
- $this->assertStringContainsString('Charlie (35)', $result);
- // Should not contain users < 30
- $this->assertStringNotContainsString('Alice (25)', $result);
-});
diff --git a/tests/Feature/PluginRequiredConfigurationTest.php b/tests/Feature/PluginRequiredConfigurationTest.php
index 51e1b76..83be449 100644
--- a/tests/Feature/PluginRequiredConfigurationTest.php
+++ b/tests/Feature/PluginRequiredConfigurationTest.php
@@ -268,79 +268,3 @@ test('hasMissingRequiredConfigurationFields returns false when required xhrSelec
expect($plugin->hasMissingRequiredConfigurationFields())->toBeFalse();
});
-
-test('hasMissingRequiredConfigurationFields returns true when required multi_string field is missing', function (): void {
- $user = User::factory()->create();
-
- $configurationTemplate = [
- 'custom_fields' => [
- [
- 'keyname' => 'tags',
- 'field_type' => 'multi_string',
- 'name' => 'Tags',
- 'description' => 'Enter tags separated by commas',
- // Not marked as optional, so it's required
- ],
- ],
- ];
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'configuration_template' => $configurationTemplate,
- 'configuration' => [], // Empty configuration
- ]);
-
- expect($plugin->hasMissingRequiredConfigurationFields())->toBeTrue();
-});
-
-test('hasMissingRequiredConfigurationFields returns false when required multi_string field is set', function (): void {
- $user = User::factory()->create();
-
- $configurationTemplate = [
- 'custom_fields' => [
- [
- 'keyname' => 'tags',
- 'field_type' => 'multi_string',
- 'name' => 'Tags',
- 'description' => 'Enter tags separated by commas',
- // Not marked as optional, so it's required
- ],
- ],
- ];
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'configuration_template' => $configurationTemplate,
- 'configuration' => [
- 'tags' => 'tag1, tag2, tag3', // Required field is set with comma-separated values
- ],
- ]);
-
- expect($plugin->hasMissingRequiredConfigurationFields())->toBeFalse();
-});
-
-test('hasMissingRequiredConfigurationFields returns true when required multi_string field is empty string', function (): void {
- $user = User::factory()->create();
-
- $configurationTemplate = [
- 'custom_fields' => [
- [
- 'keyname' => 'tags',
- 'field_type' => 'multi_string',
- 'name' => 'Tags',
- 'description' => 'Enter tags separated by commas',
- // Not marked as optional, so it's required
- ],
- ],
- ];
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'configuration_template' => $configurationTemplate,
- 'configuration' => [
- 'tags' => '', // Empty string
- ],
- ]);
-
- expect($plugin->hasMissingRequiredConfigurationFields())->toBeTrue();
-});
diff --git a/tests/Feature/PluginResponseTest.php b/tests/Feature/PluginResponseTest.php
deleted file mode 100644
index 2a75c9e..0000000
--- a/tests/Feature/PluginResponseTest.php
+++ /dev/null
@@ -1,287 +0,0 @@
- Http::response([
- 'title' => 'Test Data',
- 'items' => [
- ['id' => 1, 'name' => 'Item 1'],
- ['id' => 2, 'name' => 'Item 2'],
- ],
- ], 200, ['Content-Type' => 'application/json']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/api/data',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toBe([
- 'title' => 'Test Data',
- 'items' => [
- ['id' => 1, 'name' => 'Item 1'],
- ['id' => 2, 'name' => 'Item 2'],
- ],
- ]);
-});
-
-test('plugin parses XML responses and wraps under rss key', function (): void {
- $xmlContent = '
-
-
- Test RSS Feed
- -
- Test Item 1
- Description 1
-
- -
- Test Item 2
- Description 2
-
-
- ';
-
- Http::fake([
- 'example.com/feed.xml' => Http::response($xmlContent, 200, ['Content-Type' => 'application/xml']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/feed.xml',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toHaveKey('rss');
- expect($plugin->data_payload['rss'])->toHaveKey('@attributes');
- expect($plugin->data_payload['rss'])->toHaveKey('channel');
- expect($plugin->data_payload['rss']['channel']['title'])->toBe('Test RSS Feed');
- expect($plugin->data_payload['rss']['channel']['item'])->toHaveCount(2);
-});
-
-test('plugin parses JSON-parsable response body as JSON', function (): void {
- $jsonContent = '{"title": "Test Data", "items": [1, 2, 3]}';
-
- Http::fake([
- 'example.com/data' => Http::response($jsonContent, 200, ['Content-Type' => 'text/plain']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/data',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toBe([
- 'title' => 'Test Data',
- 'items' => [1, 2, 3],
- ]);
-});
-
-test('plugin wraps plain text response body as JSON', function (): void {
- $jsonContent = 'Lorem ipsum dolor sit amet';
-
- Http::fake([
- 'example.com/data' => Http::response($jsonContent, 200, ['Content-Type' => 'text/plain']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/data',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toBe([
- 'data' => 'Lorem ipsum dolor sit amet',
- ]);
-});
-
-test('plugin handles invalid XML gracefully', function (): void {
- $invalidXml = '- unclosed tag';
-
- Http::fake([
- 'example.com/invalid.xml' => Http::response($invalidXml, 200, ['Content-Type' => 'application/xml']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/invalid.xml',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toBe(['error' => 'Failed to parse XML response']);
-});
-
-test('plugin handles multiple URLs with mixed content types', function (): void {
- $jsonResponse = ['title' => 'JSON Data', 'items' => [1, 2, 3]];
- $xmlContent = '
- XML Data
';
-
- Http::fake([
- 'example.com/json' => Http::response($jsonResponse, 200, ['Content-Type' => 'application/json']),
- 'example.com/xml' => Http::response($xmlContent, 200, ['Content-Type' => 'application/xml']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => "https://example.com/json\nhttps://example.com/xml",
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toHaveKey('IDX_0');
- expect($plugin->data_payload)->toHaveKey('IDX_1');
-
- // First URL should be JSON
- expect($plugin->data_payload['IDX_0'])->toBe($jsonResponse);
-
- // Second URL should be XML wrapped under rss
- expect($plugin->data_payload['IDX_1'])->toHaveKey('rss');
- expect($plugin->data_payload['IDX_1']['rss']['item'])->toBe('XML Data');
-});
-
-test('plugin handles POST requests with XML responses', function (): void {
- $xmlContent = 'successtest';
-
- Http::fake([
- 'example.com/api' => Http::response($xmlContent, 200, ['Content-Type' => 'application/xml']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/api',
- 'polling_verb' => 'post',
- 'polling_body' => '{"query": "test"}',
- ]);
-
- $plugin->updateDataPayload();
-
- $plugin->refresh();
-
- expect($plugin->data_payload)->toHaveKey('rss');
- expect($plugin->data_payload['rss'])->toHaveKey('status');
- expect($plugin->data_payload['rss'])->toHaveKey('data');
- expect($plugin->data_payload['rss']['status'])->toBe('success');
- expect($plugin->data_payload['rss']['data'])->toBe('test');
-});
-
-test('plugin parses iCal responses and filters to recent window', function (): void {
- Carbon::setTestNow(Carbon::parse('2025-01-15 12:00:00', 'UTC'));
-
- $icalContent = <<<'ICS'
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Example Corp.//CalDAV Client//EN
-BEGIN:VEVENT
-UID:event-1@example.com
-DTSTAMP:20250101T120000Z
-DTSTART:20250110T090000Z
-DTEND:20250110T100000Z
-SUMMARY:Past within window
-END:VEVENT
-BEGIN:VEVENT
-UID:event-2@example.com
-DTSTAMP:20250101T120000Z
-DTSTART:20250301T090000Z
-DTEND:20250301T100000Z
-SUMMARY:Far future
-END:VEVENT
-BEGIN:VEVENT
-UID:event-3@example.com
-DTSTAMP:20250101T120000Z
-DTSTART:20250120T090000Z
-DTEND:20250120T100000Z
-SUMMARY:Upcoming within window
-END:VEVENT
-END:VCALENDAR
-ICS;
-
- Http::fake([
- 'example.com/calendar.ics' => Http::response($icalContent, 200, ['Content-Type' => 'text/calendar']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/calendar.ics',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
- $plugin->refresh();
-
- $ical = $plugin->data_payload['ical'];
-
- expect($ical)->toHaveCount(2);
- expect($ical[0]['SUMMARY'])->toBe('Past within window');
- expect($ical[1]['SUMMARY'])->toBe('Upcoming within window');
- expect(collect($ical)->pluck('SUMMARY'))->not->toContain('Far future');
- expect($ical[0]['DTSTART'])->toBe('2025-01-10T09:00:00+00:00');
- expect($ical[1]['DTSTART'])->toBe('2025-01-20T09:00:00+00:00');
-
- Carbon::setTestNow();
-});
-
-test('plugin detects iCal content without calendar content type', function (): void {
- Carbon::setTestNow(Carbon::parse('2025-01-15 12:00:00', 'UTC'));
-
- $icalContent = <<<'ICS'
-BEGIN:VCALENDAR
-VERSION:2.0
-BEGIN:VEVENT
-UID:event-body-detected@example.com
-DTSTAMP:20250101T120000Z
-DTSTART:20250116T090000Z
-DTEND:20250116T100000Z
-SUMMARY:Detected by body
-END:VEVENT
-END:VCALENDAR
-ICS;
-
- Http::fake([
- 'example.com/calendar-body.ics' => Http::response($icalContent, 200, ['Content-Type' => 'text/plain']),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => 'https://example.com/calendar-body.ics',
- 'polling_verb' => 'get',
- ]);
-
- $plugin->updateDataPayload();
- $plugin->refresh();
-
- expect($plugin->data_payload)->toHaveKey('ical');
- expect($plugin->data_payload['ical'])->toHaveCount(1);
- expect($plugin->data_payload['ical'][0]['SUMMARY'])->toBe('Detected by body');
- expect($plugin->data_payload['ical'][0]['DTSTART'])->toBe('2025-01-16T09:00:00+00:00');
-
- Carbon::setTestNow();
-});
diff --git a/tests/Feature/TransformDefaultImagesTest.php b/tests/Feature/TransformDefaultImagesTest.php
index 2ea995f..041c708 100644
--- a/tests/Feature/TransformDefaultImagesTest.php
+++ b/tests/Feature/TransformDefaultImagesTest.php
@@ -3,21 +3,9 @@
use App\Models\Device;
use App\Models\DeviceModel;
use App\Services\ImageGenerationService;
-use Bnussbau\TrmnlPipeline\TrmnlPipeline;
use Illuminate\Support\Facades\Storage;
-beforeEach(function (): void {
- TrmnlPipeline::fake();
- Storage::fake('public');
- Storage::disk('public')->makeDirectory('/images/default-screens');
- Storage::disk('public')->makeDirectory('/images/generated');
-
- // Create fallback image files that the service expects
- Storage::disk('public')->put('/images/setup-logo.bmp', 'fake-bmp-content');
- Storage::disk('public')->put('/images/sleep.bmp', 'fake-bmp-content');
-});
-
-test('command transforms default images for all device models', function (): void {
+test('command transforms default images for all device models', function () {
// Ensure we have device models
$deviceModels = DeviceModel::all();
expect($deviceModels)->not->toBeEmpty();
@@ -42,7 +30,21 @@ test('command transforms default images for all device models', function (): voi
}
});
-test('getDeviceSpecificDefaultImage falls back to original images for device without model', function (): void {
+test('getDeviceSpecificDefaultImage returns correct path for device with model', function () {
+ $deviceModel = DeviceModel::first();
+ expect($deviceModel)->not->toBeNull();
+
+ $device = new Device();
+ $device->deviceModel = $deviceModel;
+
+ $setupImage = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'setup-logo');
+ $sleepImage = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'sleep');
+
+ expect($setupImage)->toContain('images/default-screens/setup-logo_');
+ expect($sleepImage)->toContain('images/default-screens/sleep_');
+});
+
+test('getDeviceSpecificDefaultImage falls back to original images for device without model', function () {
$device = new Device();
$device->deviceModel = null;
@@ -53,7 +55,7 @@ test('getDeviceSpecificDefaultImage falls back to original images for device wit
expect($sleepImage)->toBe('images/sleep.bmp');
});
-test('generateDefaultScreenImage creates images from Blade templates', function (): void {
+test('generateDefaultScreenImage creates images from Blade templates', function () {
$device = Device::factory()->create();
$setupUuid = ImageGenerationService::generateDefaultScreenImage($device, 'setup-logo');
@@ -71,14 +73,14 @@ test('generateDefaultScreenImage creates images from Blade templates', function
expect(Storage::disk('public')->exists($sleepPath))->toBeTrue();
})->skipOnCI();
-test('generateDefaultScreenImage throws exception for invalid image type', function (): void {
+test('generateDefaultScreenImage throws exception for invalid image type', function () {
$device = Device::factory()->create();
- expect(fn (): string => ImageGenerationService::generateDefaultScreenImage($device, 'invalid-type'))
+ expect(fn () => ImageGenerationService::generateDefaultScreenImage($device, 'invalid-type'))
->toThrow(InvalidArgumentException::class);
});
-test('getDeviceSpecificDefaultImage returns null for invalid image type', function (): void {
+test('getDeviceSpecificDefaultImage returns null for invalid image type', function () {
$device = new Device();
$device->deviceModel = DeviceModel::first();
diff --git a/tests/Feature/Volt/CatalogTrmnlTest.php b/tests/Feature/Volt/CatalogTrmnlTest.php
deleted file mode 100644
index a80c63a..0000000
--- a/tests/Feature/Volt/CatalogTrmnlTest.php
+++ /dev/null
@@ -1,286 +0,0 @@
- Http::response([
- 'data' => [
- [
- 'id' => 123,
- 'name' => 'Weather Chum',
- 'icon_url' => 'https://example.com/icon.png',
- 'screenshot_url' => null,
- 'author_bio' => null,
- 'stats' => ['installs' => 10, 'forks' => 2],
- ],
- ],
- ], 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Weather Chum')
- ->assertSee('Install')
- ->assertDontSeeHtml('variant="subtle" icon="eye"')
- ->assertSee('Installs: 10');
-});
-
-it('shows preview button when screenshot_url is provided', function (): void {
- Http::fake([
- 'usetrmnl.com/recipes.json*' => Http::response([
- 'data' => [
- [
- 'id' => 123,
- 'name' => 'Weather Chum',
- 'icon_url' => 'https://example.com/icon.png',
- 'screenshot_url' => 'https://example.com/screenshot.png',
- 'author_bio' => null,
- 'stats' => ['installs' => 10, 'forks' => 2],
- ],
- ],
- ], 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Weather Chum')
- ->assertSee('Preview');
-});
-
-it('searches TRMNL recipes when search term is provided', function (): void {
- Http::fake([
- // First call (mount -> newest)
- 'usetrmnl.com/recipes.json?*' => Http::sequence()
- ->push([
- 'data' => [
- [
- 'id' => 1,
- 'name' => 'Initial Recipe',
- 'icon_url' => null,
- 'screenshot_url' => null,
- 'author_bio' => null,
- 'stats' => ['installs' => 1, 'forks' => 0],
- ],
- ],
- ], 200)
- // Second call (search)
- ->push([
- 'data' => [
- [
- 'id' => 2,
- 'name' => 'Weather Search Result',
- 'icon_url' => null,
- 'screenshot_url' => null,
- 'author_bio' => null,
- 'stats' => ['installs' => 3, 'forks' => 1],
- ],
- ],
- ], 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Initial Recipe')
- ->set('search', 'weather')
- ->assertSee('Weather Search Result')
- ->assertSee('Install');
-});
-
-it('installs plugin successfully when user is authenticated', function (): void {
- $user = User::factory()->create();
-
- Http::fake([
- 'usetrmnl.com/recipes.json*' => Http::response([
- 'data' => [
- [
- 'id' => 123,
- 'name' => 'Weather Chum',
- 'icon_url' => 'https://example.com/icon.png',
- 'screenshot_url' => null,
- 'author_bio' => null,
- 'stats' => ['installs' => 10, 'forks' => 2],
- ],
- ],
- ], 200),
- 'usetrmnl.com/api/plugin_settings/123/archive*' => Http::response('fake zip content', 200),
- ]);
-
- $this->actingAs($user);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Weather Chum')
- ->call('installPlugin', '123')
- ->assertSee('Error installing plugin'); // This will fail because we don't have a real zip file
-});
-
-it('shows error when user is not authenticated', function (): void {
- Http::fake([
- 'usetrmnl.com/recipes.json*' => Http::response([
- 'data' => [
- [
- 'id' => 123,
- 'name' => 'Weather Chum',
- 'icon_url' => 'https://example.com/icon.png',
- 'screenshot_url' => null,
- 'author_bio' => null,
- 'stats' => ['installs' => 10, 'forks' => 2],
- ],
- ],
- ], 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Weather Chum')
- ->call('installPlugin', '123')
- ->assertStatus(403); // This will return 403 because user is not authenticated
-});
-
-it('shows error when plugin installation fails', function (): void {
- $user = User::factory()->create();
-
- Http::fake([
- 'usetrmnl.com/recipes.json*' => Http::response([
- 'data' => [
- [
- 'id' => 123,
- 'name' => 'Weather Chum',
- 'icon_url' => 'https://example.com/icon.png',
- 'screenshot_url' => null,
- 'author_bio' => null,
- 'stats' => ['installs' => 10, 'forks' => 2],
- ],
- ],
- ], 200),
- 'usetrmnl.com/api/plugin_settings/123/archive*' => Http::response('invalid zip content', 200),
- ]);
-
- $this->actingAs($user);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Weather Chum')
- ->call('installPlugin', '123')
- ->assertSee('Error installing plugin'); // This will fail because the zip content is invalid
-});
-
-it('previews a recipe with async fetch', function (): void {
- Http::fake([
- 'usetrmnl.com/recipes.json*' => Http::response([
- 'data' => [
- [
- 'id' => 123,
- 'name' => 'Weather Chum',
- 'icon_url' => 'https://example.com/icon.png',
- 'screenshot_url' => 'https://example.com/old.png',
- 'author_bio' => null,
- 'stats' => ['installs' => 10, 'forks' => 2],
- ],
- ],
- ], 200),
- 'usetrmnl.com/recipes/123.json' => Http::response([
- 'data' => [
- 'id' => 123,
- 'name' => 'Weather Chum Updated',
- 'icon_url' => 'https://example.com/icon.png',
- 'screenshot_url' => 'https://example.com/new.png',
- 'author_bio' => ['description' => 'New bio'],
- 'stats' => ['installs' => 11, 'forks' => 3],
- ],
- ], 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Weather Chum')
- ->call('previewRecipe', '123')
- ->assertSet('previewingRecipe', '123')
- ->assertSet('previewData.name', 'Weather Chum Updated')
- ->assertSet('previewData.screenshot_url', 'https://example.com/new.png')
- ->assertSee('Preview Weather Chum Updated')
- ->assertSee('New bio');
-});
-
-it('supports pagination and loading more recipes', function (): void {
- Http::fake([
- 'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::response([
- 'data' => [
- [
- 'id' => 1,
- 'name' => 'Recipe Page 1',
- 'icon_url' => null,
- 'screenshot_url' => null,
- 'author_bio' => null,
- 'stats' => ['installs' => 1, 'forks' => 0],
- ],
- ],
- 'next_page_url' => '/recipes.json?page=2',
- ], 200),
- 'usetrmnl.com/recipes.json?sort-by=newest&page=2' => Http::response([
- 'data' => [
- [
- 'id' => 2,
- 'name' => 'Recipe Page 2',
- 'icon_url' => null,
- 'screenshot_url' => null,
- 'author_bio' => null,
- 'stats' => ['installs' => 2, 'forks' => 0],
- ],
- ],
- 'next_page_url' => null,
- ], 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Recipe Page 1')
- ->assertDontSee('Recipe Page 2')
- ->assertSee('Load next page')
- ->call('loadMore')
- ->assertSee('Recipe Page 1')
- ->assertSee('Recipe Page 2')
- ->assertDontSee('Load next page');
-});
-
-it('resets pagination when search term changes', function (): void {
- Http::fake([
- 'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::sequence()
- ->push([
- 'data' => [['id' => 1, 'name' => 'Initial 1']],
- 'next_page_url' => '/recipes.json?page=2',
- ])
- ->push([
- 'data' => [['id' => 3, 'name' => 'Initial 1 Again']],
- 'next_page_url' => null,
- ]),
- 'usetrmnl.com/recipes.json?search=weather&sort-by=newest&page=1' => Http::response([
- 'data' => [['id' => 2, 'name' => 'Weather Result']],
- 'next_page_url' => null,
- ]),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Initial 1')
- ->call('loadMore')
- ->set('search', 'weather')
- ->assertSee('Weather Result')
- ->assertDontSee('Initial 1')
- ->assertSet('page', 1);
-});
diff --git a/tests/Feature/Volt/DevicePalettesTest.php b/tests/Feature/Volt/DevicePalettesTest.php
deleted file mode 100644
index 376a4a6..0000000
--- a/tests/Feature/Volt/DevicePalettesTest.php
+++ /dev/null
@@ -1,575 +0,0 @@
-create();
-
- $this->actingAs($user);
-
- $this->get(route('device-palettes.index'))->assertOk();
-});
-
-test('component loads all device palettes on mount', function (): void {
- $user = User::factory()->create();
- $initialCount = DevicePalette::count();
- DevicePalette::create(['name' => 'palette-1', 'grays' => 2, 'framework_class' => '']);
- DevicePalette::create(['name' => 'palette-2', 'grays' => 4, 'framework_class' => '']);
- DevicePalette::create(['name' => 'palette-3', 'grays' => 16, 'framework_class' => '']);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index');
-
- $palettes = $component->get('devicePalettes');
- expect($palettes)->toHaveCount($initialCount + 3);
-});
-
-test('can open modal to create new device palette', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->call('openDevicePaletteModal');
-
- $component
- ->assertSet('editingDevicePaletteId', null)
- ->assertSet('viewingDevicePaletteId', null)
- ->assertSet('name', null)
- ->assertSet('grays', 2);
-});
-
-test('can create a new device palette', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'test-palette')
- ->set('description', 'Test Palette Description')
- ->set('grays', 16)
- ->set('colors', ['#FF0000', '#00FF00'])
- ->set('framework_class', 'TestFramework')
- ->call('saveDevicePalette');
-
- $component->assertHasNoErrors();
-
- expect(DevicePalette::where('name', 'test-palette')->exists())->toBeTrue();
-
- $palette = DevicePalette::where('name', 'test-palette')->first();
- expect($palette->description)->toBe('Test Palette Description');
- expect($palette->grays)->toBe(16);
- expect($palette->colors)->toBe(['#FF0000', '#00FF00']);
- expect($palette->framework_class)->toBe('TestFramework');
- expect($palette->source)->toBe('manual');
-});
-
-test('can create a grayscale-only palette without colors', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'grayscale-palette')
- ->set('grays', 256)
- ->set('colors', [])
- ->call('saveDevicePalette');
-
- $component->assertHasNoErrors();
-
- $palette = DevicePalette::where('name', 'grayscale-palette')->first();
- expect($palette->colors)->toBeNull();
- expect($palette->grays)->toBe(256);
-});
-
-test('can open modal to edit existing device palette', function (): void {
- $user = User::factory()->create();
- $palette = DevicePalette::create([
- 'name' => 'existing-palette',
- 'description' => 'Existing Description',
- 'grays' => 4,
- 'colors' => ['#FF0000', '#00FF00'],
- 'framework_class' => 'Framework',
- 'source' => 'manual',
- ]);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->call('openDevicePaletteModal', $palette->id);
-
- $component
- ->assertSet('editingDevicePaletteId', $palette->id)
- ->assertSet('name', 'existing-palette')
- ->assertSet('description', 'Existing Description')
- ->assertSet('grays', 4)
- ->assertSet('colors', ['#FF0000', '#00FF00'])
- ->assertSet('framework_class', 'Framework');
-});
-
-test('can update an existing device palette', function (): void {
- $user = User::factory()->create();
- $palette = DevicePalette::create([
- 'name' => 'original-palette',
- 'grays' => 2,
- 'framework_class' => '',
- 'source' => 'manual',
- ]);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->call('openDevicePaletteModal', $palette->id)
- ->set('name', 'updated-palette')
- ->set('description', 'Updated Description')
- ->set('grays', 16)
- ->set('colors', ['#0000FF'])
- ->call('saveDevicePalette');
-
- $component->assertHasNoErrors();
-
- $palette->refresh();
- expect($palette->name)->toBe('updated-palette');
- expect($palette->description)->toBe('Updated Description');
- expect($palette->grays)->toBe(16);
- expect($palette->colors)->toBe(['#0000FF']);
-});
-
-test('can delete a device palette', function (): void {
- $user = User::factory()->create();
- $palette = DevicePalette::create([
- 'name' => 'to-delete',
- 'grays' => 2,
- 'framework_class' => '',
- 'source' => 'manual',
- ]);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->call('deleteDevicePalette', $palette->id);
-
- expect(DevicePalette::find($palette->id))->toBeNull();
- $component->assertSet('devicePalettes', function ($palettes) use ($palette) {
- return $palettes->where('id', $palette->id)->isEmpty();
- });
-});
-
-test('can duplicate a device palette', function (): void {
- $user = User::factory()->create();
- $palette = DevicePalette::create([
- 'name' => 'original-palette',
- 'description' => 'Original Description',
- 'grays' => 4,
- 'colors' => ['#FF0000', '#00FF00'],
- 'framework_class' => 'Framework',
- 'source' => 'manual',
- ]);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->call('duplicateDevicePalette', $palette->id);
-
- $component
- ->assertSet('editingDevicePaletteId', null)
- ->assertSet('name', 'original-palette (Copy)')
- ->assertSet('description', 'Original Description')
- ->assertSet('grays', 4)
- ->assertSet('colors', ['#FF0000', '#00FF00'])
- ->assertSet('framework_class', 'Framework');
-});
-
-test('can add a color to the colors array', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('colorInput', '#FF0000')
- ->call('addColor');
-
- $component
- ->assertHasNoErrors()
- ->assertSet('colors', ['#FF0000'])
- ->assertSet('colorInput', '');
-});
-
-test('cannot add duplicate colors', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('colors', ['#FF0000'])
- ->set('colorInput', '#FF0000')
- ->call('addColor');
-
- $component
- ->assertHasNoErrors()
- ->assertSet('colors', ['#FF0000']);
-});
-
-test('can add multiple colors', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('colorInput', '#FF0000')
- ->call('addColor')
- ->set('colorInput', '#00FF00')
- ->call('addColor')
- ->set('colorInput', '#0000FF')
- ->call('addColor');
-
- $component
- ->assertSet('colors', ['#FF0000', '#00FF00', '#0000FF']);
-});
-
-test('can remove a color from the colors array', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('colors', ['#FF0000', '#00FF00', '#0000FF'])
- ->call('removeColor', 1);
-
- $component->assertSet('colors', ['#FF0000', '#0000FF']);
-});
-
-test('removing color reindexes array', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('colors', ['#FF0000', '#00FF00', '#0000FF'])
- ->call('removeColor', 0);
-
- $colors = $component->get('colors');
- expect($colors)->toBe(['#00FF00', '#0000FF']);
- expect(array_keys($colors))->toBe([0, 1]);
-});
-
-test('can open modal in view-only mode for api-sourced palette', function (): void {
- $user = User::factory()->create();
- $palette = DevicePalette::create([
- 'name' => 'api-palette',
- 'grays' => 2,
- 'framework_class' => '',
- 'source' => 'api',
- ]);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->call('openDevicePaletteModal', $palette->id, true);
-
- $component
- ->assertSet('viewingDevicePaletteId', $palette->id)
- ->assertSet('editingDevicePaletteId', null);
-});
-
-test('name is required when creating device palette', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('grays', 16)
- ->call('saveDevicePalette');
-
- $component->assertHasErrors(['name']);
-});
-
-test('name must be unique when creating device palette', function (): void {
- $user = User::factory()->create();
- DevicePalette::create([
- 'name' => 'existing-name',
- 'grays' => 2,
- 'framework_class' => '',
- ]);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'existing-name')
- ->set('grays', 16)
- ->call('saveDevicePalette');
-
- $component->assertHasErrors(['name']);
-});
-
-test('name can be same when updating device palette', function (): void {
- $user = User::factory()->create();
- $palette = DevicePalette::create([
- 'name' => 'original-name',
- 'grays' => 2,
- 'framework_class' => '',
- 'source' => 'manual',
- ]);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->call('openDevicePaletteModal', $palette->id)
- ->set('grays', 16)
- ->call('saveDevicePalette');
-
- $component->assertHasNoErrors();
-});
-
-test('grays is required when creating device palette', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'test-palette')
- ->set('grays', null)
- ->call('saveDevicePalette');
-
- $component->assertHasErrors(['grays']);
-});
-
-test('grays must be at least 1', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'test-palette')
- ->set('grays', 0)
- ->call('saveDevicePalette');
-
- $component->assertHasErrors(['grays']);
-});
-
-test('grays must be at most 256', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'test-palette')
- ->set('grays', 257)
- ->call('saveDevicePalette');
-
- $component->assertHasErrors(['grays']);
-});
-
-test('colors must be valid hex format', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'test-palette')
- ->set('grays', 16)
- ->set('colors', ['invalid-color', '#FF0000'])
- ->call('saveDevicePalette');
-
- $component->assertHasErrors(['colors.0']);
-});
-
-test('color input must be valid hex format when adding color', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('colorInput', 'invalid-color')
- ->call('addColor');
-
- $component->assertHasErrors(['colorInput']);
-});
-
-test('color input accepts valid hex format', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('colorInput', '#FF0000')
- ->call('addColor');
-
- $component->assertHasNoErrors();
-});
-
-test('color input accepts lowercase hex format', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('colorInput', '#ff0000')
- ->call('addColor');
-
- $component->assertHasNoErrors();
-});
-
-test('description can be null', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'test-palette')
- ->set('grays', 16)
- ->set('description', null)
- ->call('saveDevicePalette');
-
- $component->assertHasNoErrors();
-
- $palette = DevicePalette::where('name', 'test-palette')->first();
- expect($palette->description)->toBeNull();
-});
-
-test('framework class can be empty string', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'test-palette')
- ->set('grays', 16)
- ->set('framework_class', '')
- ->call('saveDevicePalette');
-
- $component->assertHasNoErrors();
-
- $palette = DevicePalette::where('name', 'test-palette')->first();
- expect($palette->framework_class)->toBe('');
-});
-
-test('empty colors array is saved as null', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'test-palette')
- ->set('grays', 16)
- ->set('colors', [])
- ->call('saveDevicePalette');
-
- $component->assertHasNoErrors();
-
- $palette = DevicePalette::where('name', 'test-palette')->first();
- expect($palette->colors)->toBeNull();
-});
-
-test('component resets form after saving', function (): void {
- $user = User::factory()->create();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'test-palette')
- ->set('description', 'Test Description')
- ->set('grays', 16)
- ->set('colors', ['#FF0000'])
- ->set('framework_class', 'TestFramework')
- ->call('saveDevicePalette');
-
- $component
- ->assertSet('name', null)
- ->assertSet('description', null)
- ->assertSet('grays', 2)
- ->assertSet('colors', [])
- ->assertSet('framework_class', '')
- ->assertSet('colorInput', '')
- ->assertSet('editingDevicePaletteId', null)
- ->assertSet('viewingDevicePaletteId', null);
-});
-
-test('component handles palette with null colors when editing', function (): void {
- $user = User::factory()->create();
- $palette = DevicePalette::create([
- 'name' => 'grayscale-palette',
- 'grays' => 2,
- 'colors' => null,
- 'framework_class' => '',
- ]);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->call('openDevicePaletteModal', $palette->id);
-
- $component->assertSet('colors', []);
-});
-
-test('component handles palette with string colors when editing', function (): void {
- $user = User::factory()->create();
- $palette = DevicePalette::create([
- 'name' => 'string-colors-palette',
- 'grays' => 2,
- 'framework_class' => '',
- ]);
- // Manually set colors as JSON string to simulate edge case
- $palette->setRawAttributes(array_merge($palette->getAttributes(), [
- 'colors' => json_encode(['#FF0000', '#00FF00']),
- ]));
- $palette->save();
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->call('openDevicePaletteModal', $palette->id);
-
- $component->assertSet('colors', ['#FF0000', '#00FF00']);
-});
-
-test('component refreshes palette list after creating', function (): void {
- $user = User::factory()->create();
- $initialCount = DevicePalette::count();
- DevicePalette::create(['name' => 'palette-1', 'grays' => 2, 'framework_class' => '']);
- DevicePalette::create(['name' => 'palette-2', 'grays' => 4, 'framework_class' => '']);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->set('name', 'new-palette')
- ->set('grays', 16)
- ->call('saveDevicePalette');
-
- $palettes = $component->get('devicePalettes');
- expect($palettes)->toHaveCount($initialCount + 3);
- expect(DevicePalette::count())->toBe($initialCount + 3);
-});
-
-test('component refreshes palette list after deleting', function (): void {
- $user = User::factory()->create();
- $initialCount = DevicePalette::count();
- $palette1 = DevicePalette::create([
- 'name' => 'palette-1',
- 'grays' => 2,
- 'framework_class' => '',
- 'source' => 'manual',
- ]);
- $palette2 = DevicePalette::create([
- 'name' => 'palette-2',
- 'grays' => 2,
- 'framework_class' => '',
- 'source' => 'manual',
- ]);
-
- $this->actingAs($user);
-
- $component = Volt::test('device-palettes.index')
- ->call('deleteDevicePalette', $palette1->id);
-
- $palettes = $component->get('devicePalettes');
- expect($palettes)->toHaveCount($initialCount + 1);
- expect(DevicePalette::count())->toBe($initialCount + 1);
-});
diff --git a/tests/Unit/Liquid/Filters/DataTest.php b/tests/Unit/Liquid/Filters/DataTest.php
index 1200b6f..abd4114 100644
--- a/tests/Unit/Liquid/Filters/DataTest.php
+++ b/tests/Unit/Liquid/Filters/DataTest.php
@@ -325,173 +325,3 @@ test('parse_json filter handles primitive values', function (): void {
expect($filter->parse_json('false'))->toBe(false);
expect($filter->parse_json('null'))->toBe(null);
});
-
-test('map_to_i filter converts string numbers to integers', function (): void {
- $filter = new Data();
- $input = ['1', '2', '3', '4', '5'];
-
- expect($filter->map_to_i($input))->toBe([1, 2, 3, 4, 5]);
-});
-
-test('map_to_i filter handles mixed string numbers', function (): void {
- $filter = new Data();
- $input = ['5', '4', '3', '2', '1'];
-
- expect($filter->map_to_i($input))->toBe([5, 4, 3, 2, 1]);
-});
-
-test('map_to_i filter handles decimal strings', function (): void {
- $filter = new Data();
- $input = ['1.5', '2.7', '3.0'];
-
- expect($filter->map_to_i($input))->toBe([1, 2, 3]);
-});
-
-test('map_to_i filter handles empty array', function (): void {
- $filter = new Data();
- $input = [];
-
- expect($filter->map_to_i($input))->toBe([]);
-});
-
-test('where_exp filter returns string as array when input is string', function (): void {
- $filter = new Data();
- $input = 'just a string';
-
- expect($filter->where_exp($input, 'la', 'le'))->toBe(['just a string']);
-});
-
-test('where_exp filter filters numbers with comparison', function (): void {
- $filter = new Data();
- $input = [1, 2, 3, 4, 5];
-
- expect($filter->where_exp($input, 'n', 'n >= 3'))->toBe([3, 4, 5]);
-});
-
-test('where_exp filter filters numbers with greater than', function (): void {
- $filter = new Data();
- $input = [1, 2, 3, 4, 5];
-
- expect($filter->where_exp($input, 'n', 'n > 2'))->toBe([3, 4, 5]);
-});
-
-test('where_exp filter filters numbers with less than', function (): void {
- $filter = new Data();
- $input = [1, 2, 3, 4, 5];
-
- expect($filter->where_exp($input, 'n', 'n < 4'))->toBe([1, 2, 3]);
-});
-
-test('where_exp filter filters numbers with equality', function (): void {
- $filter = new Data();
- $input = [1, 2, 3, 4, 5];
-
- expect($filter->where_exp($input, 'n', 'n == 3'))->toBe([3]);
-});
-
-test('where_exp filter filters numbers with not equal', function (): void {
- $filter = new Data();
- $input = [1, 2, 3, 4, 5];
-
- expect($filter->where_exp($input, 'n', 'n != 3'))->toBe([1, 2, 4, 5]);
-});
-
-test('where_exp filter filters objects by property', function (): void {
- $filter = new Data();
- $input = [
- ['name' => 'Alice', 'age' => 25],
- ['name' => 'Bob', 'age' => 30],
- ['name' => 'Charlie', 'age' => 35],
- ];
-
- expect($filter->where_exp($input, 'person', 'person.age >= 30'))->toBe([
- ['name' => 'Bob', 'age' => 30],
- ['name' => 'Charlie', 'age' => 35],
- ]);
-});
-
-test('where_exp filter filters objects by string property', function (): void {
- $filter = new Data();
- $input = [
- ['name' => 'Alice', 'role' => 'admin'],
- ['name' => 'Bob', 'role' => 'user'],
- ['name' => 'Charlie', 'role' => 'admin'],
- ];
-
- expect($filter->where_exp($input, 'user', 'user.role == "admin"'))->toBe([
- ['name' => 'Alice', 'role' => 'admin'],
- ['name' => 'Charlie', 'role' => 'admin'],
- ]);
-});
-
-test('where_exp filter handles and operator', function (): void {
- $filter = new Data();
- $input = [
- ['name' => 'Alice', 'age' => 25, 'active' => true],
- ['name' => 'Bob', 'age' => 30, 'active' => false],
- ['name' => 'Charlie', 'age' => 35, 'active' => true],
- ];
-
- expect($filter->where_exp($input, 'person', 'person.age >= 30 and person.active == true'))->toBe([
- ['name' => 'Charlie', 'age' => 35, 'active' => true],
- ]);
-});
-
-test('where_exp filter handles or operator', function (): void {
- $filter = new Data();
- $input = [
- ['name' => 'Alice', 'age' => 25, 'role' => 'admin'],
- ['name' => 'Bob', 'age' => 30, 'role' => 'user'],
- ['name' => 'Charlie', 'age' => 35, 'role' => 'user'],
- ];
-
- expect($filter->where_exp($input, 'person', 'person.age < 30 or person.role == "admin"'))->toBe([
- ['name' => 'Alice', 'age' => 25, 'role' => 'admin'],
- ]);
-});
-
-test('where_exp filter handles simple boolean expressions', function (): void {
- $filter = new Data();
- $input = [
- ['name' => 'Alice', 'active' => true],
- ['name' => 'Bob', 'active' => false],
- ['name' => 'Charlie', 'active' => true],
- ];
-
- expect($filter->where_exp($input, 'person', 'person.active'))->toBe([
- ['name' => 'Alice', 'active' => true],
- ['name' => 'Charlie', 'active' => true],
- ]);
-});
-
-test('where_exp filter handles empty array', function (): void {
- $filter = new Data();
- $input = [];
-
- expect($filter->where_exp($input, 'n', 'n >= 3'))->toBe([]);
-});
-
-test('where_exp filter handles associative array', function (): void {
- $filter = new Data();
- $input = [
- 'a' => 1,
- 'b' => 2,
- 'c' => 3,
- ];
-
- expect($filter->where_exp($input, 'n', 'n >= 2'))->toBe([2, 3]);
-});
-
-test('where_exp filter handles non-array input', function (): void {
- $filter = new Data();
- $input = 123;
-
- expect($filter->where_exp($input, 'n', 'n >= 3'))->toBe([]);
-});
-
-test('where_exp filter handles null input', function (): void {
- $filter = new Data();
- $input = null;
-
- expect($filter->where_exp($input, 'n', 'n >= 3'))->toBe([]);
-});
diff --git a/tests/Unit/Liquid/Filters/DateTest.php b/tests/Unit/Liquid/Filters/DateTest.php
index 7de8949..d967951 100644
--- a/tests/Unit/Liquid/Filters/DateTest.php
+++ b/tests/Unit/Liquid/Filters/DateTest.php
@@ -30,65 +30,3 @@ test('days_ago filter with large number works correctly', function (): void {
expect($filter->days_ago(100))->toBe($hundredDaysAgo);
});
-
-test('ordinalize filter formats date with ordinal day', function (): void {
- $filter = new Date();
-
- expect($filter->ordinalize('2025-10-02', '%A, %B <>, %Y'))
- ->toBe('Thursday, October 2nd, 2025');
-});
-
-test('ordinalize filter handles datetime string with timezone', function (): void {
- $filter = new Date();
-
- expect($filter->ordinalize('2025-12-31 16:50:38 -0400', '%A, %b <>'))
- ->toBe('Wednesday, Dec 31st');
-});
-
-test('ordinalize filter handles different ordinal suffixes', function (): void {
- $filter = new Date();
-
- // 1st
- expect($filter->ordinalize('2025-01-01', '<>'))
- ->toBe('1st');
-
- // 2nd
- expect($filter->ordinalize('2025-01-02', '<>'))
- ->toBe('2nd');
-
- // 3rd
- expect($filter->ordinalize('2025-01-03', '<>'))
- ->toBe('3rd');
-
- // 4th
- expect($filter->ordinalize('2025-01-04', '<>'))
- ->toBe('4th');
-
- // 11th (special case)
- expect($filter->ordinalize('2025-01-11', '<>'))
- ->toBe('11th');
-
- // 12th (special case)
- expect($filter->ordinalize('2025-01-12', '<>'))
- ->toBe('12th');
-
- // 13th (special case)
- expect($filter->ordinalize('2025-01-13', '<>'))
- ->toBe('13th');
-
- // 21st
- expect($filter->ordinalize('2025-01-21', '<>'))
- ->toBe('21st');
-
- // 22nd
- expect($filter->ordinalize('2025-01-22', '<>'))
- ->toBe('22nd');
-
- // 23rd
- expect($filter->ordinalize('2025-01-23', '<>'))
- ->toBe('23rd');
-
- // 24th
- expect($filter->ordinalize('2025-01-24', '<>'))
- ->toBe('24th');
-});
diff --git a/tests/Unit/Liquid/Filters/LocalizationTest.php b/tests/Unit/Liquid/Filters/LocalizationTest.php
index 3129b1e..a52623f 100644
--- a/tests/Unit/Liquid/Filters/LocalizationTest.php
+++ b/tests/Unit/Liquid/Filters/LocalizationTest.php
@@ -77,7 +77,7 @@ test('l_date handles null locale parameter', function (): void {
$filter = new Localization();
$date = '2025-01-11';
- $result = $filter->l_date($date, 'Y-m-d');
+ $result = $filter->l_date($date, 'Y-m-d', null);
// Should work the same as default
expect($result)->toContain('2025');
diff --git a/tests/Unit/Liquid/Utils/ExpressionUtilsTest.php b/tests/Unit/Liquid/Utils/ExpressionUtilsTest.php
deleted file mode 100644
index ee4d2fd..0000000
--- a/tests/Unit/Liquid/Utils/ExpressionUtilsTest.php
+++ /dev/null
@@ -1,201 +0,0 @@
- 1, 'b' => 2, 'c' => 3];
-
- expect(ExpressionUtils::isAssociativeArray($array))->toBeTrue();
-});
-
-test('isAssociativeArray returns false for indexed array', function (): void {
- $array = [1, 2, 3, 4, 5];
-
- expect(ExpressionUtils::isAssociativeArray($array))->toBeFalse();
-});
-
-test('isAssociativeArray returns false for empty array', function (): void {
- $array = [];
-
- expect(ExpressionUtils::isAssociativeArray($array))->toBeFalse();
-});
-
-test('parseCondition handles simple comparison', function (): void {
- $result = ExpressionUtils::parseCondition('n >= 3');
-
- expect($result)->toBe([
- 'type' => 'comparison',
- 'left' => 'n',
- 'operator' => '>=',
- 'right' => '3',
- ]);
-});
-
-test('parseCondition handles equality comparison', function (): void {
- $result = ExpressionUtils::parseCondition('user.role == "admin"');
-
- expect($result)->toBe([
- 'type' => 'comparison',
- 'left' => 'user.role',
- 'operator' => '==',
- 'right' => '"admin"',
- ]);
-});
-
-test('parseCondition handles and operator', function (): void {
- $result = ExpressionUtils::parseCondition('user.age >= 30 and user.active == true');
-
- expect($result)->toBe([
- 'type' => 'and',
- 'left' => [
- 'type' => 'comparison',
- 'left' => 'user.age',
- 'operator' => '>=',
- 'right' => '30',
- ],
- 'right' => [
- 'type' => 'comparison',
- 'left' => 'user.active',
- 'operator' => '==',
- 'right' => 'true',
- ],
- ]);
-});
-
-test('parseCondition handles or operator', function (): void {
- $result = ExpressionUtils::parseCondition('user.age < 30 or user.role == "admin"');
-
- expect($result)->toBe([
- 'type' => 'or',
- 'left' => [
- 'type' => 'comparison',
- 'left' => 'user.age',
- 'operator' => '<',
- 'right' => '30',
- ],
- 'right' => [
- 'type' => 'comparison',
- 'left' => 'user.role',
- 'operator' => '==',
- 'right' => '"admin"',
- ],
- ]);
-});
-
-test('parseCondition handles simple expression', function (): void {
- $result = ExpressionUtils::parseCondition('user.active');
-
- expect($result)->toBe([
- 'type' => 'simple',
- 'expression' => 'user.active',
- ]);
-});
-
-test('evaluateCondition handles comparison with numbers', function (): void {
- $condition = ExpressionUtils::parseCondition('n >= 3');
-
- expect(ExpressionUtils::evaluateCondition($condition, 'n', 5))->toBeTrue();
- expect(ExpressionUtils::evaluateCondition($condition, 'n', 2))->toBeFalse();
- expect(ExpressionUtils::evaluateCondition($condition, 'n', 3))->toBeTrue();
-});
-
-test('evaluateCondition handles comparison with strings', function (): void {
- $condition = ExpressionUtils::parseCondition('user.role == "admin"');
- $user = ['role' => 'admin'];
-
- expect(ExpressionUtils::evaluateCondition($condition, 'user', $user))->toBeTrue();
-
- $user = ['role' => 'user'];
- expect(ExpressionUtils::evaluateCondition($condition, 'user', $user))->toBeFalse();
-});
-
-test('evaluateCondition handles and operator', function (): void {
- $condition = ExpressionUtils::parseCondition('user.age >= 30 and user.active == true');
- $user = ['age' => 35, 'active' => true];
-
- expect(ExpressionUtils::evaluateCondition($condition, 'user', $user))->toBeTrue();
-
- $user = ['age' => 25, 'active' => true];
- expect(ExpressionUtils::evaluateCondition($condition, 'user', $user))->toBeFalse();
-
- $user = ['age' => 35, 'active' => false];
- expect(ExpressionUtils::evaluateCondition($condition, 'user', $user))->toBeFalse();
-});
-
-test('evaluateCondition handles or operator', function (): void {
- $condition = ExpressionUtils::parseCondition('user.age < 30 or user.role == "admin"');
- $user = ['age' => 25, 'role' => 'user'];
-
- expect(ExpressionUtils::evaluateCondition($condition, 'user', $user))->toBeTrue();
-
- $user = ['age' => 35, 'role' => 'admin'];
- expect(ExpressionUtils::evaluateCondition($condition, 'user', $user))->toBeTrue();
-
- $user = ['age' => 35, 'role' => 'user'];
- expect(ExpressionUtils::evaluateCondition($condition, 'user', $user))->toBeFalse();
-});
-
-test('evaluateCondition handles simple boolean expression', function (): void {
- $condition = ExpressionUtils::parseCondition('user.active');
- $user = ['active' => true];
-
- expect(ExpressionUtils::evaluateCondition($condition, 'user', $user))->toBeTrue();
-
- $user = ['active' => false];
- expect(ExpressionUtils::evaluateCondition($condition, 'user', $user))->toBeFalse();
-});
-
-test('resolveValue returns object when expression matches variable', function (): void {
- $object = ['name' => 'Alice', 'age' => 25];
-
- expect(ExpressionUtils::resolveValue('user', 'user', $object))->toBe($object);
-});
-
-test('resolveValue resolves property access for arrays', function (): void {
- $object = ['name' => 'Alice', 'age' => 25];
-
- expect(ExpressionUtils::resolveValue('user.name', 'user', $object))->toBe('Alice');
- expect(ExpressionUtils::resolveValue('user.age', 'user', $object))->toBe(25);
-});
-
-test('resolveValue resolves property access for objects', function (): void {
- $object = new stdClass();
- $object->name = 'Alice';
- $object->age = 25;
-
- expect(ExpressionUtils::resolveValue('user.name', 'user', $object))->toBe('Alice');
- expect(ExpressionUtils::resolveValue('user.age', 'user', $object))->toBe(25);
-});
-
-test('resolveValue returns null for non-existent properties', function (): void {
- $object = ['name' => 'Alice'];
-
- expect(ExpressionUtils::resolveValue('user.age', 'user', $object))->toBeNull();
-});
-
-test('resolveValue parses numeric values', function (): void {
- expect(ExpressionUtils::resolveValue('123', 'user', []))->toBe(123);
- expect(ExpressionUtils::resolveValue('45.67', 'user', []))->toBe(45.67);
-});
-
-test('resolveValue parses boolean values', function (): void {
- expect(ExpressionUtils::resolveValue('true', 'user', []))->toBeTrue();
- expect(ExpressionUtils::resolveValue('false', 'user', []))->toBeFalse();
- expect(ExpressionUtils::resolveValue('TRUE', 'user', []))->toBeTrue();
- expect(ExpressionUtils::resolveValue('FALSE', 'user', []))->toBeFalse();
-});
-
-test('resolveValue parses null value', function (): void {
- expect(ExpressionUtils::resolveValue('null', 'user', []))->toBeNull();
- expect(ExpressionUtils::resolveValue('NULL', 'user', []))->toBeNull();
-});
-
-test('resolveValue removes quotes from strings', function (): void {
- expect(ExpressionUtils::resolveValue('"hello"', 'user', []))->toBe('hello');
- expect(ExpressionUtils::resolveValue("'world'", 'user', []))->toBe('world');
-});
-
-test('resolveValue returns expression as-is for unquoted strings', function (): void {
- expect(ExpressionUtils::resolveValue('hello', 'user', []))->toBe('hello');
- expect(ExpressionUtils::resolveValue('world', 'user', []))->toBe('world');
-});
diff --git a/tests/Unit/Models/PluginTest.php b/tests/Unit/Models/PluginTest.php
index aa9a28e..ef054b1 100644
--- a/tests/Unit/Models/PluginTest.php
+++ b/tests/Unit/Models/PluginTest.php
@@ -1,8 +1,6 @@
data_payload['IDX_2'])->toBe(['headline' => 'test']);
});
-test('updateDataPayload skips empty lines in polling_url and maintains sequential IDX keys', function (): void {
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- // empty lines and extra spaces between the URL to generate empty entries
- 'polling_url' => "https://api1.example.com/data\n \n\nhttps://api2.example.com/weather\n ",
- 'polling_verb' => 'get',
- ]);
-
- // Mock only the valid URLs
- Http::fake([
- 'https://api1.example.com/data' => Http::response(['item' => 'first'], 200),
- 'https://api2.example.com/weather' => Http::response(['item' => 'second'], 200),
- ]);
-
- $plugin->updateDataPayload();
-
- // payload should only have 2 items, and they should be indexed 0 and 1
- expect($plugin->data_payload)->toHaveCount(2);
- expect($plugin->data_payload)->toHaveKey('IDX_0');
- expect($plugin->data_payload)->toHaveKey('IDX_1');
-
- // data is correct
- expect($plugin->data_payload['IDX_0'])->toBe(['item' => 'first']);
- expect($plugin->data_payload['IDX_1'])->toBe(['item' => 'second']);
-
- // no empty index exists
- expect($plugin->data_payload)->not->toHaveKey('IDX_2');
-});
-
test('updateDataPayload handles single URL without nesting', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'polling',
@@ -388,553 +357,3 @@ test('resolveLiquidVariables handles empty configuration', function (): void {
expect($plugin->resolveLiquidVariables($template))->toBe($expected);
});
-
-test('resolveLiquidVariables uses external renderer when preferred_renderer is trmnl-liquid and template contains for loop', function (): void {
- Illuminate\Support\Facades\Process::fake([
- '*' => Illuminate\Support\Facades\Process::result(
- output: 'https://api1.example.com/data\nhttps://api2.example.com/data',
- exitCode: 0
- ),
- ]);
-
- config(['services.trmnl.liquid_enabled' => true]);
- config(['services.trmnl.liquid_path' => '/usr/local/bin/trmnl-liquid-cli']);
-
- $plugin = Plugin::factory()->create([
- 'preferred_renderer' => 'trmnl-liquid',
- 'configuration' => [
- 'recipe_ids' => '1,2',
- ],
- ]);
-
- $template = <<<'LIQUID'
-{% assign ids = recipe_ids | split: "," %}
-{% for id in ids %}
-https://api{{ id }}.example.com/data
-{% endfor %}
-LIQUID;
-
- $result = $plugin->resolveLiquidVariables($template);
-
- // Trim trailing newlines that may be added by the process
- expect(mb_trim($result))->toBe('https://api1.example.com/data\nhttps://api2.example.com/data');
-
- Illuminate\Support\Facades\Process::assertRan(function ($process): bool {
- $command = is_array($process->command) ? implode(' ', $process->command) : $process->command;
-
- return str_contains($command, 'trmnl-liquid-cli') &&
- str_contains($command, '--template') &&
- str_contains($command, '--context');
- });
-});
-
-test('resolveLiquidVariables uses internal renderer when preferred_renderer is not trmnl-liquid', function (): void {
- $plugin = Plugin::factory()->create([
- 'preferred_renderer' => 'php',
- 'configuration' => [
- 'recipe_ids' => '1,2',
- ],
- ]);
-
- $template = <<<'LIQUID'
-{% assign ids = recipe_ids | split: "," %}
-{% for id in ids %}
-https://api{{ id }}.example.com/data
-{% endfor %}
-LIQUID;
-
- // Should use internal renderer even with for loop
- $result = $plugin->resolveLiquidVariables($template);
-
- // Internal renderer should process the template
- expect($result)->toBeString();
-});
-
-test('resolveLiquidVariables uses internal renderer when external renderer is disabled', function (): void {
- config(['services.trmnl.liquid_enabled' => false]);
-
- $plugin = Plugin::factory()->create([
- 'preferred_renderer' => 'trmnl-liquid',
- 'configuration' => [
- 'recipe_ids' => '1,2',
- ],
- ]);
-
- $template = <<<'LIQUID'
-{% assign ids = recipe_ids | split: "," %}
-{% for id in ids %}
-https://api{{ id }}.example.com/data
-{% endfor %}
-LIQUID;
-
- // Should use internal renderer when external is disabled
- $result = $plugin->resolveLiquidVariables($template);
-
- expect($result)->toBeString();
-});
-
-test('resolveLiquidVariables uses internal renderer when template does not contain for loop', function (): void {
- config(['services.trmnl.liquid_enabled' => true]);
- config(['services.trmnl.liquid_path' => '/usr/local/bin/trmnl-liquid-cli']);
-
- $plugin = Plugin::factory()->create([
- 'preferred_renderer' => 'trmnl-liquid',
- 'configuration' => [
- 'api_key' => 'test123',
- ],
- ]);
-
- $template = 'https://api.example.com/data?key={{ api_key }}';
-
- // Should use internal renderer when no for loop
- $result = $plugin->resolveLiquidVariables($template);
-
- expect($result)->toBe('https://api.example.com/data?key=test123');
-
- Illuminate\Support\Facades\Process::assertNothingRan();
-});
-
-test('resolveLiquidVariables detects for loop with standard opening tag', function (): void {
- Illuminate\Support\Facades\Process::fake([
- '*' => Illuminate\Support\Facades\Process::result(
- output: 'resolved',
- exitCode: 0
- ),
- ]);
-
- config(['services.trmnl.liquid_enabled' => true]);
- config(['services.trmnl.liquid_path' => '/usr/local/bin/trmnl-liquid-cli']);
-
- $plugin = Plugin::factory()->create([
- 'preferred_renderer' => 'trmnl-liquid',
- 'configuration' => [],
- ]);
-
- // Test {% for pattern
- $template = '{% for item in items %}test{% endfor %}';
- $plugin->resolveLiquidVariables($template);
-
- Illuminate\Support\Facades\Process::assertRan(function ($process): bool {
- $command = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
-
- return str_contains($command, 'trmnl-liquid-cli');
- });
-});
-
-test('resolveLiquidVariables detects for loop with whitespace stripping tag', function (): void {
- Illuminate\Support\Facades\Process::fake([
- '*' => Illuminate\Support\Facades\Process::result(
- output: 'resolved',
- exitCode: 0
- ),
- ]);
-
- config(['services.trmnl.liquid_enabled' => true]);
- config(['services.trmnl.liquid_path' => '/usr/local/bin/trmnl-liquid-cli']);
-
- $plugin = Plugin::factory()->create([
- 'preferred_renderer' => 'trmnl-liquid',
- 'configuration' => [],
- ]);
-
- // Test {%- for pattern (with whitespace stripping)
- $template = '{%- for item in items %}test{% endfor %}';
- $plugin->resolveLiquidVariables($template);
-
- Illuminate\Support\Facades\Process::assertRan(function ($process): bool {
- $command = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
-
- return str_contains($command, 'trmnl-liquid-cli');
- });
-});
-
-test('updateDataPayload resolves entire polling_url field first then splits by newline', function (): void {
- Http::fake([
- 'https://api1.example.com/data' => Http::response(['data' => 'test1'], 200),
- 'https://api2.example.com/data' => Http::response(['data' => 'test2'], 200),
- ]);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'polling_url' => "https://api1.example.com/data\nhttps://api2.example.com/data",
- 'polling_verb' => 'get',
- 'configuration' => [
- 'recipe_ids' => '1,2',
- ],
- ]);
-
- $plugin->updateDataPayload();
-
- // Should have split the multi-line URL and generated two requests
- expect($plugin->data_payload)->toHaveKey('IDX_0');
- expect($plugin->data_payload)->toHaveKey('IDX_1');
- expect($plugin->data_payload['IDX_0'])->toBe(['data' => 'test1']);
- expect($plugin->data_payload['IDX_1'])->toBe(['data' => 'test2']);
-});
-
-test('updateDataPayload handles multi-line polling_url with for loop using external renderer', function (): void {
- Illuminate\Support\Facades\Process::fake([
- '*' => Illuminate\Support\Facades\Process::result(
- output: "https://api1.example.com/data\nhttps://api2.example.com/data",
- exitCode: 0
- ),
- ]);
-
- Http::fake([
- 'https://api1.example.com/data' => Http::response(['data' => 'test1'], 200),
- 'https://api2.example.com/data' => Http::response(['data' => 'test2'], 200),
- ]);
-
- config(['services.trmnl.liquid_enabled' => true]);
- config(['services.trmnl.liquid_path' => '/usr/local/bin/trmnl-liquid-cli']);
-
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- 'preferred_renderer' => 'trmnl-liquid',
- 'polling_url' => <<<'LIQUID'
-{% assign ids = recipe_ids | split: "," %}
-{% for id in ids %}
-https://api{{ id }}.example.com/data
-{% endfor %}
-LIQUID
- ,
- 'polling_verb' => 'get',
- 'configuration' => [
- 'recipe_ids' => '1,2',
- ],
- ]);
-
- $plugin->updateDataPayload();
-
- // Should have used external renderer and generated two URLs
- expect($plugin->data_payload)->toHaveKey('IDX_0');
- expect($plugin->data_payload)->toHaveKey('IDX_1');
- expect($plugin->data_payload['IDX_0'])->toBe(['data' => 'test1']);
- expect($plugin->data_payload['IDX_1'])->toBe(['data' => 'test2']);
-
- Illuminate\Support\Facades\Process::assertRan(function ($process): bool {
- $command = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
-
- return str_contains($command, 'trmnl-liquid-cli');
- });
-});
-
-test('plugin render uses user timezone when set', function (): void {
- $user = User::factory()->create([
- 'timezone' => 'America/New_York',
- ]);
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'markup_language' => 'liquid',
- 'render_markup' => '{{ trmnl.user.time_zone_iana }}',
- ]);
-
- $rendered = $plugin->render();
-
- expect($rendered)->toContain('America/New_York');
-});
-
-test('plugin render falls back to app timezone when user timezone is not set', function (): void {
- $user = User::factory()->create([
- 'timezone' => null,
- ]);
-
- config(['app.timezone' => 'Europe/London']);
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'markup_language' => 'liquid',
- 'render_markup' => '{{ trmnl.user.time_zone_iana }}',
- ]);
-
- $rendered = $plugin->render();
-
- expect($rendered)->toContain('Europe/London');
-});
-
-test('plugin render calculates correct UTC offset from user timezone', function (): void {
- $user = User::factory()->create([
- 'timezone' => 'America/New_York', // UTC-5 (EST) or UTC-4 (EDT)
- ]);
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'markup_language' => 'liquid',
- 'render_markup' => '{{ trmnl.user.utc_offset }}',
- ]);
-
- $rendered = $plugin->render();
-
- // America/New_York offset should be -18000 (EST) or -14400 (EDT) in seconds
- $expectedOffset = (string) Carbon::now('America/New_York')->getOffset();
- expect($rendered)->toContain($expectedOffset);
-});
-
-test('plugin render calculates correct UTC offset from app timezone when user timezone is null', function (): void {
- $user = User::factory()->create([
- 'timezone' => null,
- ]);
-
- config(['app.timezone' => 'Europe/London']);
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'markup_language' => 'liquid',
- 'render_markup' => '{{ trmnl.user.utc_offset }}',
- ]);
-
- $rendered = $plugin->render();
-
- // Europe/London offset should be 0 (GMT) or 3600 (BST) in seconds
- $expectedOffset = (string) Carbon::now('Europe/London')->getOffset();
- expect($rendered)->toContain($expectedOffset);
-});
-
-test('plugin render includes utc_offset and time_zone_iana in trmnl.user context', function (): void {
- $user = User::factory()->create([
- 'timezone' => 'America/Chicago', // UTC-6 (CST) or UTC-5 (CDT)
- ]);
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'markup_language' => 'liquid',
- 'render_markup' => '{{ trmnl.user.time_zone_iana }}|{{ trmnl.user.utc_offset }}',
- ]);
-
- $rendered = $plugin->render();
-
- expect($rendered)
- ->toContain('America/Chicago')
- ->and($rendered)->toMatch('/\|-?\d+/'); // Should contain a pipe followed by a number (offset in seconds)
-});
-
-/**
- * Plugin security: XSS Payload Dataset
- * [Input, Expected Result, Forbidden String]
- */
-dataset('xss_vectors', [
- 'standard_script' => ['Safe ', 'Safe ', '