components compile, render, and forward props correctly. * These thin wrappers delegate to Flux components, isolating Livewire dependencies. * * Run with: php artisan test app/Core/Tests/Feature/CoreComponentsTest.php */ use Core\Front\Components\Button; use Core\Front\Components\Card; use Core\Front\Components\Heading; use Core\Front\Components\Layout; use Core\Front\Components\NavList; use Core\Front\Components\Text; use Core\Pro; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\File; use Illuminate\View\Compilers\BladeCompiler; uses()->group('core', 'components'); describe('Core Detection Helpers', function () { it('detects Flux Pro installation', function () { // Flux Pro is installed in this project expect(Pro::hasFluxPro())->toBeTrue(); }); it('identifies Flux Pro components', function () { expect(Pro::requiresFluxPro('calendar'))->toBeTrue(); expect(Pro::requiresFluxPro('editor'))->toBeTrue(); expect(Pro::requiresFluxPro('chart'))->toBeTrue(); expect(Pro::requiresFluxPro('chart.line'))->toBeTrue(); // Nested component expect(Pro::requiresFluxPro('core:kanban'))->toBeTrue(); // With prefix // Free components expect(Pro::requiresFluxPro('button'))->toBeFalse(); expect(Pro::requiresFluxPro('input'))->toBeFalse(); expect(Pro::requiresFluxPro('modal'))->toBeFalse(); }); it('respects FontAwesome Pro config', function () { Pro::clearCache(); config(['core.fontawesome.pro' => false]); expect(Pro::hasFontAwesomePro())->toBeFalse(); Pro::clearCache(); config(['core.fontawesome.pro' => true]); expect(Pro::hasFontAwesomePro())->toBeTrue(); // Clean up Pro::clearCache(); }); it('provides correct FA styles based on Pro/Free', function () { Pro::clearCache(); config(['core.fontawesome.pro' => false]); $freeStyles = Pro::fontAwesomeStyles(); expect($freeStyles)->toContain('solid'); expect($freeStyles)->toContain('regular'); expect($freeStyles)->toContain('brands'); expect($freeStyles)->not->toContain('jelly'); expect($freeStyles)->not->toContain('light'); Pro::clearCache(); config(['core.fontawesome.pro' => true]); $proStyles = Pro::fontAwesomeStyles(); expect($proStyles)->toContain('jelly'); expect($proStyles)->toContain('light'); expect($proStyles)->toContain('thin'); // Clean up Pro::clearCache(); }); }); describe('Core Component Library', function () { it('has all expected component files', function () { $basePath = app_path('Core/Front/Components/View/Blade'); $expectedComponents = [ // Foundation 'button.blade.php', 'text.blade.php', 'heading.blade.php', 'subheading.blade.php', 'icon.blade.php', 'card.blade.php', 'badge.blade.php', 'input.blade.php', 'textarea.blade.php', 'switch.blade.php', // Forms 'select.blade.php', 'select/option.blade.php', 'checkbox.blade.php', 'checkbox/group.blade.php', 'radio.blade.php', 'radio/group.blade.php', 'label.blade.php', 'field.blade.php', 'error.blade.php', 'description.blade.php', // Navigation 'dropdown.blade.php', 'menu.blade.php', 'menu/item.blade.php', 'menu/separator.blade.php', 'navbar.blade.php', 'navbar/item.blade.php', 'navlist.blade.php', 'navlist/item.blade.php', 'navlist/group.blade.php', 'tabs.blade.php', 'tab.blade.php', // Data Display 'table.blade.php', 'table/columns.blade.php', 'table/column.blade.php', 'table/rows.blade.php', 'table/row.blade.php', 'table/cell.blade.php', 'avatar.blade.php', 'separator.blade.php', // Overlays 'modal.blade.php', 'callout.blade.php', 'callout/heading.blade.php', 'callout/text.blade.php', 'popover.blade.php', 'tooltip.blade.php', 'accordion.blade.php', 'accordion/item.blade.php', 'accordion/heading.blade.php', 'accordion/content.blade.php', // Pro - Inputs 'autocomplete.blade.php', 'autocomplete/item.blade.php', 'slider.blade.php', 'slider/tick.blade.php', 'pillbox.blade.php', 'pillbox/option.blade.php', // Pro - Date/Time 'calendar.blade.php', 'date-picker.blade.php', 'date-picker/input.blade.php', 'date-picker/button.blade.php', 'time-picker.blade.php', // Pro - Rich Content 'editor.blade.php', 'editor/toolbar.blade.php', 'editor/button.blade.php', 'editor/content.blade.php', 'composer.blade.php', 'file-upload.blade.php', 'file-upload/dropzone.blade.php', 'file-item.blade.php', 'file-item/remove.blade.php', // Pro - Visualisation 'chart.blade.php', 'chart/svg.blade.php', 'chart/line.blade.php', 'chart/area.blade.php', 'chart/point.blade.php', 'chart/axis.blade.php', 'chart/cursor.blade.php', 'chart/tooltip.blade.php', 'chart/legend.blade.php', 'chart/summary.blade.php', 'chart/viewport.blade.php', 'kanban.blade.php', 'kanban/column.blade.php', 'kanban/card.blade.php', // Pro - Command 'command.blade.php', 'command/input.blade.php', 'command/items.blade.php', 'command/item.blade.php', 'context.blade.php', ]; $missing = []; foreach ($expectedComponents as $component) { $path = $basePath.'/'.$component; if (! File::exists($path)) { $missing[] = $component; } } expect($missing)->toBeEmpty( 'Missing components: '.implode(', ', $missing) ); }); it('all core blade components compile without errors', function () { $basePath = app_path('Core/Front/Components/View/Blade'); $errors = []; $bladeFiles = File::allFiles($basePath); foreach ($bladeFiles as $file) { if (! str_contains($file->getFilename(), '.blade.php')) { continue; } try { $compiler = app(BladeCompiler::class); $compiler->compile($file->getPathname()); } catch (Throwable $e) { $relativePath = str_replace($basePath.'/', '', $file->getPathname()); $errors[] = "{$relativePath}: {$e->getMessage()}"; } } expect($errors)->toBeEmpty( "Components failed to compile:\n".implode("\n", $errors) ); }); it('components delegate to flux with attribute forwarding', function () { $basePath = app_path('Core/Front/Components/View/Blade'); $missingAttributeForwarding = []; $bladeFiles = File::allFiles($basePath); foreach ($bladeFiles as $file) { if (! str_contains($file->getFilename(), '.blade.php')) { continue; } $content = File::get($file->getPathname()); $relativePath = str_replace($basePath.'/', '', $file->getPathname()); // Skip directories with different patterns $skipPrefixes = ['layout', 'forms/', 'examples/', 'errors/', 'components/', 'web/']; $shouldSkip = false; foreach ($skipPrefixes as $prefix) { if (str_starts_with($relativePath, $prefix)) { $shouldSkip = true; break; } } if ($shouldSkip) { continue; } // Check if it delegates to flux if (preg_match('/except') || str_contains($content, '$attributes->merge') || str_contains($content, ':$attributes'); if (! $hasForwarding) { $missingAttributeForwarding[] = $relativePath; } } } expect($missingAttributeForwarding)->toBeEmpty( "Components missing attribute forwarding:\n".implode("\n", $missingAttributeForwarding) ); }); it('components with slots forward {{ $slot }}', function () { $basePath = app_path('Core/Front/Components/View/Blade'); $missingSlotForwarding = []; $bladeFiles = File::allFiles($basePath); foreach ($bladeFiles as $file) { if (! str_contains($file->getFilename(), '.blade.php')) { continue; } $content = File::get($file->getPathname()); $relativePath = str_replace($basePath.'/', '', $file->getPathname()); // Skip directories with different patterns $skipPrefixes = ['layout', 'forms/', 'examples/', 'errors/', 'components/', 'web/']; $shouldSkip = false; foreach ($skipPrefixes as $prefix) { if (str_starts_with($relativePath, $prefix)) { $shouldSkip = true; break; } } if ($shouldSkip) { continue; } // If it has opening and closing flux tags, should have {{ $slot }} if (preg_match('/.*<\/flux:/s', $content)) { if (! str_contains($content, '{{ $slot }}')) { $missingSlotForwarding[] = $relativePath; } } } expect($missingSlotForwarding)->toBeEmpty( "Components missing {{ \$slot }} forwarding:\n".implode("\n", $missingSlotForwarding) ); }); }); describe('Core Component Rendering', function () { it('text matches flux:text', function () { $core = Blade::render('Hello World'); $flux = Blade::render('Hello World'); expect($core)->toBe($flux); }); it('button matches flux:button', function () { $core = Blade::render('Click Me'); $flux = Blade::render('Click Me'); expect($core)->toBe($flux); }); it('heading matches flux:heading', function () { $core = Blade::render('Title'); $flux = Blade::render('Title'); expect($core)->toBe($flux); }); it('input matches flux:input', function () { $core = Blade::render(''); $flux = Blade::render(''); expect($core)->toBe($flux); }); it('select matches flux:select', function () { $core = Blade::render(' Active Inactive '); $flux = Blade::render(' Active Inactive '); expect($core)->toBe($flux); }); it('checkbox.group matches flux:checkbox.group', function () { $core = Blade::render(' '); $flux = Blade::render(' '); expect($core)->toBe($flux); }); it('table matches flux:table', function () { $core = Blade::render(' Name Email John john@example.com '); $flux = Blade::render(' Name Email John john@example.com '); expect($core)->toBe($flux); }); it('menu matches flux:menu', function () { $core = Blade::render(' Dashboard Settings '); $flux = Blade::render(' Dashboard Settings '); expect($core)->toBe($flux); }); it('modal matches flux:modal', function () { $core = Blade::render('

Are you sure?

'); $flux = Blade::render('

Are you sure?

'); expect($core)->toBe($flux); }); it('callout matches flux:callout', function () { $core = Blade::render(' Warning This action cannot be undone. '); $flux = Blade::render(' Warning This action cannot be undone. '); expect($core)->toBe($flux); }); it('accordion matches flux:accordion', function () { $core = Blade::render(' FAQ 1 Answer 1 '); $flux = Blade::render(' FAQ 1 Answer 1 '); expect($core)->toBe($flux); }); }); describe('Core Pro Component Rendering', function () { it('autocomplete matches flux:autocomplete', function () { $core = Blade::render(' Option 1 '); $flux = Blade::render(' Option 1 '); expect($core)->toBe($flux); }); it('slider matches flux:slider', function () { $core = Blade::render(''); $flux = Blade::render(''); expect($core)->toBe($flux); }); it('calendar matches flux:calendar', function () { $core = Blade::render(''); $flux = Blade::render(''); expect($core)->toBe($flux); }); it('date-picker matches flux:date-picker', function () { $core = Blade::render(''); $flux = Blade::render(''); expect($core)->toBe($flux); }); it('time-picker matches flux:time-picker', function () { $core = Blade::render(''); $flux = Blade::render(''); expect($core)->toBe($flux); }); it('editor matches flux:editor', function () { $core = Blade::render(''); $flux = Blade::render(''); expect($core)->toBe($flux); }); it('composer matches flux:composer', function () { $core = Blade::render(''); $flux = Blade::render(''); expect($core)->toBe($flux); }); it('file-upload matches flux:file-upload', function () { $core = Blade::render(' '); $flux = Blade::render(' '); expect($core)->toBe($flux); }); it('chart matches flux:chart', function () { $core = Blade::render(' '); $flux = Blade::render(' '); expect($core)->toBe($flux); }); it('kanban matches flux:kanban', function () { $core = Blade::render(' Task content '); $flux = Blade::render(' Task content '); expect($core)->toBe($flux); }); it('command matches flux:command', function () { $core = Blade::render(' Go Home '); $flux = Blade::render(' Go Home '); expect($core)->toBe($flux); }); it('context matches flux:context', function () { $core = Blade::render('
Right-click me
Action
'); $flux = Blade::render('
Right-click me
Action
'); expect($core)->toBe($flux); }); it('pillbox matches flux:pillbox', function () { $core = Blade::render(' PHP JavaScript '); $flux = Blade::render(' PHP JavaScript '); expect($core)->toBe($flux); }); }); describe('Core PHP Builders', function () { it('Button builder renders HTML', function () { $button = Button::make() ->label('Save') ->primary(); // Use the actual API method $html = $button->toHtml(); expect($html) ->toContain('Save') ->toContain('button'); }); it('Card builder renders with title and body', function () { $card = Card::make() ->title('Card Title') ->body('Card content goes here'); $html = $card->toHtml(); expect($html) ->toContain('Card Title') ->toContain('Card content goes here'); }); it('Heading builder renders with level', function () { $heading = Heading::make() ->text('Page Title') ->level(1); $html = $heading->toHtml(); expect($html) ->toContain('Page Title') ->toContain('content('Some text') ->muted(); $html = $text->toHtml(); expect($html)->toContain('Some text'); }); it('NavList builder renders items', function () { // NavList::item() signature is: label, href, active, icon $navlist = NavList::make() ->item('Dashboard', '/dashboard', false, 'home') ->item('Settings', '/settings', false, 'cog'); $html = $navlist->toHtml(); expect($html) ->toContain('Dashboard') ->toContain('Settings') ->toContain('/dashboard') ->toContain('/settings'); }); it('Layout builder renders HLCRF structure', function () { // Layout uses short method names: h(), c(), f() $layout = Layout::make('HCF') ->h('Header Content') ->c('Main Content') ->f('Footer Content'); $html = $layout->toHtml(); expect($html) ->toContain('Header Content') ->toContain('Main Content') ->toContain('Footer Content') ->toContain('data-layout='); // Layout uses data-layout attribute }); }); describe('Component Count Verification', function () { it('has at least 100 core components', function () { $basePath = app_path('Core/Front/Components/View/Blade'); $bladeFiles = File::allFiles($basePath); $componentCount = collect($bladeFiles) ->filter(fn ($file) => str_contains($file->getFilename(), '.blade.php')) ->count(); expect($componentCount)->toBeGreaterThanOrEqual(100); }); it('covers all major Flux component categories', function () { $basePath = app_path('Core/Front/Components/View/Blade'); $categories = [ 'button.blade.php' => 'Foundation', 'input.blade.php' => 'Forms', 'select.blade.php' => 'Forms', 'table.blade.php' => 'Data Display', 'menu.blade.php' => 'Navigation', 'modal.blade.php' => 'Overlays', 'chart.blade.php' => 'Pro Visualisation', 'editor.blade.php' => 'Pro Rich Content', 'calendar.blade.php' => 'Pro Date/Time', 'command.blade.php' => 'Pro Command', 'kanban.blade.php' => 'Pro Kanban', ]; $missing = []; foreach ($categories as $file => $category) { if (! File::exists($basePath.'/'.$file)) { $missing[] = "{$category} ({$file})"; } } expect($missing)->toBeEmpty( 'Missing category coverage: '.implode(', ', $missing) ); }); }); describe('Custom Components (Non-Flux)', function () { it('core:icon renders FontAwesome icons', function () { // icon is intentionally custom - uses FontAwesome, not Flux $html = Blade::render(''); expect($html) ->toContain('toContain('fa-home') ->toContain('aria-hidden="true"'); }); it('core:icon detects brand icons automatically', function () { $html = Blade::render(''); expect($html)->toContain('fa-brands'); }); it('core:icon falls back to solid for jelly when FA Free', function () { // Without FA Pro config, jelly icons should fall back to solid Pro::clearCache(); config(['core.fontawesome.pro' => false]); $html = Blade::render(''); expect($html)->toContain('fa-solid'); // Jelly → Solid fallback }); it('core:icon uses jelly style when FA Pro enabled', function () { // With FA Pro config, jelly icons should use fa-jelly Pro::clearCache(); config(['core.fontawesome.pro' => true]); $html = Blade::render(''); expect($html)->toContain('fa-jelly'); // Clean up Pro::clearCache(); config(['core.fontawesome.pro' => false]); }); it('core:icon respects explicit style override', function () { $html = Blade::render(''); expect($html) ->toContain('fa-solid') ->not->toContain('fa-jelly'); }); it('core:icon falls back pro-only styles to free equivalents', function () { Pro::clearCache(); config(['core.fontawesome.pro' => false]); // Light → Regular $html = Blade::render(''); expect($html)->toContain('fa-regular'); // Thin → Regular $html = Blade::render(''); expect($html)->toContain('fa-regular'); // Duotone → Solid $html = Blade::render(''); expect($html)->toContain('fa-solid'); }); }); describe('Comprehensive Core=Flux Parity', function () { it('simple self-closing components match flux', function () { $components = [ 'Active' => 'Active', '' => '', '' => '', // Note: is custom FontAwesome implementation, not a Flux wrapper 'Sub' => 'Sub', 'Small' => 'Small', '' => '', '' => '', 'Name' => 'Name', 'Help text' => 'Help text', // Note: requires $errors variable from Livewire context, tested separately ]; $failures = []; foreach ($components as $core => $flux) { $coreHtml = Blade::render($core); $fluxHtml = Blade::render($flux); if ($coreHtml !== $fluxHtml) { $failures[] = $core; } } expect($failures)->toBeEmpty( 'Components not matching flux: '.implode(', ', $failures) ); }); it('form components with props match flux', function () { $components = [ '' => '', '' => '', 'A' => 'A', '' => '', '' => '', ]; $failures = []; foreach ($components as $core => $flux) { $coreHtml = Blade::render($core); $fluxHtml = Blade::render($flux); if ($coreHtml !== $fluxHtml) { $failures[] = $core; } } expect($failures)->toBeEmpty( 'Form components not matching flux: '.implode(', ', $failures) ); }); it('navigation components match flux', function () { $components = [ 'OpenItem' => 'OpenItem', 'OneTwo' => 'OneTwo', 'Home' => 'Home', ]; $failures = []; foreach ($components as $core => $flux) { $coreHtml = Blade::render($core); $fluxHtml = Blade::render($flux); if ($coreHtml !== $fluxHtml) { $failures[] = $core; } } expect($failures)->toBeEmpty( 'Navigation components not matching flux: '.implode(', ', $failures) ); }); });