Fixed: basePath self→static binding, namespace detection, event wiring,
SQLite cache, file cache driver. All Mod Boot classes converted to
$listens pattern for lifecycle event discovery.
Working endpoints:
- /v1/explorer/info — live chain height, difficulty, aliases
- /v1/explorer/stats — formatted chain statistics
- /v1/names/directory — alias directory grouped by type
- /v1/names/available/{name} — name availability check
- /v1/names/lookup/{name} — name details
Co-Authored-By: Charon <charon@lethean.io>
942 lines
32 KiB
PHP
942 lines
32 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Core PHP Framework
|
|
*
|
|
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
|
* See LICENSE file for details.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Core Component Library Tests
|
|
*
|
|
* Verifies all <core:*> 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('/<flux:[a-z\.\-]+/', $content)) {
|
|
// Should have attribute forwarding (various patterns)
|
|
$hasForwarding = str_contains($content, '{{ $attributes }}')
|
|
|| str_contains($content, '$attributes->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:[a-z\.\-]+[^\/]*>.*<\/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('<core:text>Hello World</core:text>');
|
|
$flux = Blade::render('<flux:text>Hello World</flux:text>');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('button matches flux:button', function () {
|
|
$core = Blade::render('<core:button variant="primary">Click Me</core:button>');
|
|
$flux = Blade::render('<flux:button variant="primary">Click Me</flux:button>');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('heading matches flux:heading', function () {
|
|
$core = Blade::render('<core:heading level="2">Title</core:heading>');
|
|
$flux = Blade::render('<flux:heading level="2">Title</flux:heading>');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('input matches flux:input', function () {
|
|
$core = Blade::render('<core:input type="email" placeholder="you@example.com" />');
|
|
$flux = Blade::render('<flux:input type="email" placeholder="you@example.com" />');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('select matches flux:select', function () {
|
|
$core = Blade::render('
|
|
<core:select>
|
|
<core:select.option value="active">Active</core:select.option>
|
|
<core:select.option value="inactive">Inactive</core:select.option>
|
|
</core:select>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:select>
|
|
<flux:select.option value="active">Active</flux:select.option>
|
|
<flux:select.option value="inactive">Inactive</flux:select.option>
|
|
</flux:select>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('checkbox.group matches flux:checkbox.group', function () {
|
|
$core = Blade::render('
|
|
<core:checkbox.group>
|
|
<core:checkbox value="opt1" />
|
|
<core:checkbox value="opt2" />
|
|
</core:checkbox.group>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:checkbox.group>
|
|
<flux:checkbox value="opt1" />
|
|
<flux:checkbox value="opt2" />
|
|
</flux:checkbox.group>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('table matches flux:table', function () {
|
|
$core = Blade::render('
|
|
<core:table>
|
|
<core:table.columns>
|
|
<core:table.column>Name</core:table.column>
|
|
<core:table.column>Email</core:table.column>
|
|
</core:table.columns>
|
|
<core:table.rows>
|
|
<core:table.row>
|
|
<core:table.cell>John</core:table.cell>
|
|
<core:table.cell>john@example.com</core:table.cell>
|
|
</core:table.row>
|
|
</core:table.rows>
|
|
</core:table>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:table>
|
|
<flux:table.columns>
|
|
<flux:table.column>Name</flux:table.column>
|
|
<flux:table.column>Email</flux:table.column>
|
|
</flux:table.columns>
|
|
<flux:table.rows>
|
|
<flux:table.row>
|
|
<flux:table.cell>John</flux:table.cell>
|
|
<flux:table.cell>john@example.com</flux:table.cell>
|
|
</flux:table.row>
|
|
</flux:table.rows>
|
|
</flux:table>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('menu matches flux:menu', function () {
|
|
$core = Blade::render('
|
|
<core:menu>
|
|
<core:menu.item>Dashboard</core:menu.item>
|
|
<core:menu.separator />
|
|
<core:menu.item>Settings</core:menu.item>
|
|
</core:menu>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:menu>
|
|
<flux:menu.item>Dashboard</flux:menu.item>
|
|
<flux:menu.separator />
|
|
<flux:menu.item>Settings</flux:menu.item>
|
|
</flux:menu>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('modal matches flux:modal', function () {
|
|
$core = Blade::render('
|
|
<core:modal name="confirm-delete">
|
|
<p>Are you sure?</p>
|
|
</core:modal>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:modal name="confirm-delete">
|
|
<p>Are you sure?</p>
|
|
</flux:modal>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('callout matches flux:callout', function () {
|
|
$core = Blade::render('
|
|
<core:callout variant="warning">
|
|
<core:callout.heading>Warning</core:callout.heading>
|
|
<core:callout.text>This action cannot be undone.</core:callout.text>
|
|
</core:callout>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:callout variant="warning">
|
|
<flux:callout.heading>Warning</flux:callout.heading>
|
|
<flux:callout.text>This action cannot be undone.</flux:callout.text>
|
|
</flux:callout>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('accordion matches flux:accordion', function () {
|
|
$core = Blade::render('
|
|
<core:accordion>
|
|
<core:accordion.item>
|
|
<core:accordion.heading>FAQ 1</core:accordion.heading>
|
|
<core:accordion.content>Answer 1</core:accordion.content>
|
|
</core:accordion.item>
|
|
</core:accordion>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:accordion>
|
|
<flux:accordion.item>
|
|
<flux:accordion.heading>FAQ 1</flux:accordion.heading>
|
|
<flux:accordion.content>Answer 1</flux:accordion.content>
|
|
</flux:accordion.item>
|
|
</flux:accordion>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
});
|
|
|
|
describe('Core Pro Component Rendering', function () {
|
|
|
|
it('autocomplete matches flux:autocomplete', function () {
|
|
$core = Blade::render('
|
|
<core:autocomplete placeholder="Search...">
|
|
<core:autocomplete.item>Option 1</core:autocomplete.item>
|
|
</core:autocomplete>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:autocomplete placeholder="Search...">
|
|
<flux:autocomplete.item>Option 1</flux:autocomplete.item>
|
|
</flux:autocomplete>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('slider matches flux:slider', function () {
|
|
$core = Blade::render('<core:slider min="0" max="100" />');
|
|
$flux = Blade::render('<flux:slider min="0" max="100" />');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('calendar matches flux:calendar', function () {
|
|
$core = Blade::render('<core:calendar />');
|
|
$flux = Blade::render('<flux:calendar />');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('date-picker matches flux:date-picker', function () {
|
|
$core = Blade::render('<core:date-picker placeholder="Select date" />');
|
|
$flux = Blade::render('<flux:date-picker placeholder="Select date" />');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('time-picker matches flux:time-picker', function () {
|
|
$core = Blade::render('<core:time-picker placeholder="Select time" />');
|
|
$flux = Blade::render('<flux:time-picker placeholder="Select time" />');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('editor matches flux:editor', function () {
|
|
$core = Blade::render('<core:editor placeholder="Write here..." />');
|
|
$flux = Blade::render('<flux:editor placeholder="Write here..." />');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('composer matches flux:composer', function () {
|
|
$core = Blade::render('<core:composer placeholder="Type a message..." />');
|
|
$flux = Blade::render('<flux:composer placeholder="Type a message..." />');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('file-upload matches flux:file-upload', function () {
|
|
$core = Blade::render('
|
|
<core:file-upload>
|
|
<core:file-upload.dropzone />
|
|
</core:file-upload>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:file-upload>
|
|
<flux:file-upload.dropzone />
|
|
</flux:file-upload>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('chart matches flux:chart', function () {
|
|
$core = Blade::render('
|
|
<core:chart class="h-64">
|
|
<core:chart.svg>
|
|
<core:chart.line field="value" />
|
|
</core:chart.svg>
|
|
</core:chart>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:chart class="h-64">
|
|
<flux:chart.svg>
|
|
<flux:chart.line field="value" />
|
|
</flux:chart.svg>
|
|
</flux:chart>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('kanban matches flux:kanban', function () {
|
|
$core = Blade::render('
|
|
<core:kanban>
|
|
<core:kanban.column>
|
|
<core:kanban.column.cards>
|
|
<core:kanban.card>Task content</core:kanban.card>
|
|
</core:kanban.column.cards>
|
|
</core:kanban.column>
|
|
</core:kanban>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:kanban>
|
|
<flux:kanban.column>
|
|
<flux:kanban.column.cards>
|
|
<flux:kanban.card>Task content</flux:kanban.card>
|
|
</flux:kanban.column.cards>
|
|
</flux:kanban.column>
|
|
</flux:kanban>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('command matches flux:command', function () {
|
|
$core = Blade::render('
|
|
<core:command>
|
|
<core:command.input placeholder="Search..." />
|
|
<core:command.items>
|
|
<core:command.item>Go Home</core:command.item>
|
|
</core:command.items>
|
|
</core:command>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:command>
|
|
<flux:command.input placeholder="Search..." />
|
|
<flux:command.items>
|
|
<flux:command.item>Go Home</flux:command.item>
|
|
</flux:command.items>
|
|
</flux:command>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('context matches flux:context', function () {
|
|
$core = Blade::render('
|
|
<core:context>
|
|
<div>Right-click me</div>
|
|
<core:menu>
|
|
<core:menu.item>Action</core:menu.item>
|
|
</core:menu>
|
|
</core:context>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:context>
|
|
<div>Right-click me</div>
|
|
<flux:menu>
|
|
<flux:menu.item>Action</flux:menu.item>
|
|
</flux:menu>
|
|
</flux:context>
|
|
');
|
|
|
|
expect($core)->toBe($flux);
|
|
});
|
|
|
|
it('pillbox matches flux:pillbox', function () {
|
|
$core = Blade::render('
|
|
<core:pillbox multiple>
|
|
<core:pillbox.option value="php">PHP</core:pillbox.option>
|
|
<core:pillbox.option value="js">JavaScript</core:pillbox.option>
|
|
</core:pillbox>
|
|
');
|
|
$flux = Blade::render('
|
|
<flux:pillbox multiple>
|
|
<flux:pillbox.option value="php">PHP</flux:pillbox.option>
|
|
<flux:pillbox.option value="js">JavaScript</flux:pillbox.option>
|
|
</flux:pillbox>
|
|
');
|
|
|
|
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('<h1');
|
|
});
|
|
|
|
it('Text builder renders with variant', function () {
|
|
$text = Text::make()
|
|
->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('<core:icon name="home" />');
|
|
|
|
expect($html)
|
|
->toContain('<i')
|
|
->toContain('fa-home')
|
|
->toContain('aria-hidden="true"');
|
|
});
|
|
|
|
it('core:icon detects brand icons automatically', function () {
|
|
$html = Blade::render('<core:icon name="github" />');
|
|
|
|
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('<core:icon name="globe" />');
|
|
|
|
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('<core:icon name="globe" />');
|
|
|
|
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('<core:icon name="globe" style="solid" />');
|
|
|
|
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('<core:icon name="star" style="light" />');
|
|
expect($html)->toContain('fa-regular');
|
|
|
|
// Thin → Regular
|
|
$html = Blade::render('<core:icon name="star" style="thin" />');
|
|
expect($html)->toContain('fa-regular');
|
|
|
|
// Duotone → Solid
|
|
$html = Blade::render('<core:icon name="star" style="duotone" />');
|
|
expect($html)->toContain('fa-solid');
|
|
});
|
|
});
|
|
|
|
describe('Comprehensive Core=Flux Parity', function () {
|
|
|
|
it('simple self-closing components match flux', function () {
|
|
$components = [
|
|
'<core:badge>Active</core:badge>' => '<flux:badge>Active</flux:badge>',
|
|
'<core:separator />' => '<flux:separator />',
|
|
'<core:avatar src="/avatar.jpg" />' => '<flux:avatar src="/avatar.jpg" />',
|
|
// Note: <core:icon> is custom FontAwesome implementation, not a Flux wrapper
|
|
'<core:subheading>Sub</core:subheading>' => '<flux:subheading>Sub</flux:subheading>',
|
|
'<core:text size="sm">Small</core:text>' => '<flux:text size="sm">Small</flux:text>',
|
|
'<core:textarea placeholder="Write..." />' => '<flux:textarea placeholder="Write..." />',
|
|
'<core:switch />' => '<flux:switch />',
|
|
'<core:label>Name</core:label>' => '<flux:label>Name</flux:label>',
|
|
'<core:description>Help text</core:description>' => '<flux:description>Help text</flux:description>',
|
|
// Note: <core:error> 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 = [
|
|
'<core:input type="password" viewable />' => '<flux:input type="password" viewable />',
|
|
'<core:input clearable placeholder="Search" />' => '<flux:input clearable placeholder="Search" />',
|
|
'<core:select searchable><core:select.option value="a">A</core:select.option></core:select>' => '<flux:select searchable><flux:select.option value="a">A</flux:select.option></flux:select>',
|
|
'<core:checkbox value="yes" />' => '<flux:checkbox value="yes" />',
|
|
'<core:radio value="opt1" />' => '<flux:radio value="opt1" />',
|
|
];
|
|
|
|
$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 = [
|
|
'<core:dropdown><core:button>Open</core:button><core:menu><core:menu.item>Item</core:menu.item></core:menu></core:dropdown>' => '<flux:dropdown><flux:button>Open</flux:button><flux:menu><flux:menu.item>Item</flux:menu.item></flux:menu></flux:dropdown>',
|
|
'<core:tabs><core:tab name="one">One</core:tab><core:tab name="two">Two</core:tab></core:tabs>' => '<flux:tabs><flux:tab name="one">One</flux:tab><flux:tab name="two">Two</flux:tab></flux:tabs>',
|
|
'<core:navbar><core:navbar.item>Home</core:navbar.item></core:navbar>' => '<flux:navbar><flux:navbar.item>Home</flux:navbar.item></flux:navbar>',
|
|
];
|
|
|
|
$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)
|
|
);
|
|
});
|
|
});
|