Keep the public site readable after import (nbsp, highlighter, wrapping code, compact pager) and finish the admin extras already in this tree: sticky levels, preview, page cache, and live llms.txt. Co-authored-by: Cursor <cursoragent@cursor.com>
198 lines
6.4 KiB
PHP
198 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Filament\Resources\Posts\Pages\EditPost;
|
|
use App\Filament\Resources\Posts\PostResource;
|
|
use App\Filament\Resources\Posts\Schemas\PostForm;
|
|
use App\Models\Post;
|
|
use Filament\Forms\Components\Field;
|
|
use Filament\Forms\Components\ToggleButtons;
|
|
use Filament\Schemas\Components\Component;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
use ReflectionClass;
|
|
use Tests\TestCase;
|
|
|
|
class PostFormInjectionTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_post_form_uses_two_to_one_layout(): void
|
|
{
|
|
$schema = Schema::make();
|
|
PostResource::form($schema);
|
|
|
|
$roots = $schema->getComponents();
|
|
$this->assertCount(2, $roots);
|
|
|
|
[$content, $sidebar] = $roots;
|
|
$this->assertSame('正文', $content->getHeading());
|
|
$this->assertSame(PostForm::SIDEBAR_KEY, $sidebar->getKey(isAbsolute: false));
|
|
$this->assertSame(2, $content->getColumnSpan('default'));
|
|
$this->assertSame(2, $content->getColumnSpan('lg'));
|
|
$this->assertSame(1, $sidebar->getColumnSpan('default'));
|
|
$this->assertSame(1, $sidebar->getColumnSpan('lg'));
|
|
}
|
|
|
|
public function test_post_form_includes_paid_fields_in_access_section(): void
|
|
{
|
|
$schema = Schema::make();
|
|
PostResource::form($schema);
|
|
|
|
$access = $this->findKeyedComponent($schema->getComponents(), PostForm::ACCESS_KEY);
|
|
$this->assertNotNull($access);
|
|
$this->assertSame('访问设置', $access->getHeading());
|
|
|
|
$fieldNames = $this->fieldNames($access);
|
|
$this->assertContains('read_password', $fieldNames);
|
|
$this->assertContains('meta.price', $fieldNames);
|
|
$this->assertContains('meta.members_only', $fieldNames);
|
|
}
|
|
|
|
public function test_content_format_and_status_are_single_choice(): void
|
|
{
|
|
$schema = Schema::make();
|
|
PostResource::form($schema);
|
|
|
|
$fields = $this->collectFields($schema->getComponents());
|
|
|
|
$this->assertInstanceOf(ToggleButtons::class, $fields['content_format'] ?? null);
|
|
$this->assertInstanceOf(ToggleButtons::class, $fields['status'] ?? null);
|
|
$this->assertInstanceOf(ToggleButtons::class, $fields['close_comment'] ?? null);
|
|
$this->assertInstanceOf(ToggleButtons::class, $fields['sticky_level'] ?? null);
|
|
}
|
|
|
|
public function test_post_table_includes_paid_badge_column(): void
|
|
{
|
|
// Table 构造器要求 HasTable Livewire 组件,测试中绕过构造器
|
|
$table = (new ReflectionClass(Table::class))->newInstanceWithoutConstructor();
|
|
PostResource::table($table);
|
|
|
|
$names = array_keys($table->getColumns());
|
|
|
|
// 会员插件注入的付费徽章列(meta.members_only 承载价格/会员判定)
|
|
$this->assertContains('meta.members_only', $names);
|
|
$this->assertContains('title', $names);
|
|
}
|
|
|
|
public function test_meta_price_maps_to_post_meta_json(): void
|
|
{
|
|
$post = Post::create([
|
|
'title' => '付费文章',
|
|
'content' => '内容',
|
|
'content_format' => 'markdown',
|
|
'status' => 'published',
|
|
'published_at' => now(),
|
|
'meta' => ['price' => 9900, 'members_only' => true],
|
|
]);
|
|
|
|
$this->assertSame(9900, $post->meta['price']);
|
|
$this->assertTrue($post->meta['members_only']);
|
|
}
|
|
|
|
public function test_edit_post_save_preserves_other_meta_keys(): void
|
|
{
|
|
$post = Post::create([
|
|
'title' => '已解锁文章',
|
|
'slug' => 'unlocked-post',
|
|
'content' => '内容',
|
|
'content_format' => 'markdown',
|
|
'status' => 'published',
|
|
'published_at' => now(),
|
|
'meta' => ['price' => 9900, 'members_only' => true, 'unlocked_user_ids' => [1, 2]],
|
|
]);
|
|
|
|
Livewire::test(EditPost::class, ['record' => $post->getRouteKey()])
|
|
->call('save')
|
|
->assertHasNoFormErrors();
|
|
|
|
$post->refresh();
|
|
// 表单只含 price/members_only,保存时不得覆盖解锁用户等其他 meta 键
|
|
$this->assertSame([1, 2], $post->meta['unlocked_user_ids']);
|
|
$this->assertSame(9900, $post->meta['price']);
|
|
$this->assertTrue($post->meta['members_only']);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, mixed> $components
|
|
*/
|
|
private function findKeyedComponent(array $components, string $key): ?Component
|
|
{
|
|
foreach ($components as $component) {
|
|
if (! $component instanceof Component) {
|
|
continue;
|
|
}
|
|
|
|
$property = (new ReflectionClass($component))->getProperty('key');
|
|
$componentKey = $property->getValue($component);
|
|
|
|
if ($componentKey === $key) {
|
|
return $component;
|
|
}
|
|
|
|
$found = $this->findKeyedComponent($this->childComponents($component), $key);
|
|
if ($found instanceof Component) {
|
|
return $found;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function fieldNames(Component $section): array
|
|
{
|
|
return collect($this->childComponents($section))
|
|
->filter(fn ($field) => is_object($field) && method_exists($field, 'getName'))
|
|
->map(fn ($field) => $field->getName())
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param array<int, mixed> $components
|
|
* @return array<string, Field>
|
|
*/
|
|
private function collectFields(array $components): array
|
|
{
|
|
$fields = [];
|
|
|
|
foreach ($components as $component) {
|
|
if ($component instanceof Field) {
|
|
$fields[$component->getName()] = $component;
|
|
}
|
|
|
|
if ($component instanceof Component) {
|
|
$fields = [
|
|
...$fields,
|
|
...$this->collectFields($this->childComponents($component)),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, mixed>
|
|
*/
|
|
private function childComponents(?Component $component): array
|
|
{
|
|
if (! $component instanceof Component) {
|
|
return [];
|
|
}
|
|
|
|
$property = (new ReflectionClass($component))->getProperty('childComponents');
|
|
$children = $property->getValue($component)['default'] ?? [];
|
|
|
|
return is_array($children) ? $children : [];
|
|
}
|
|
}
|