Filament Schema 测试指南从表单填充到 Repeater、Builder 与 Wizard 的完整断言体系【免费下载链接】filamentA powerful open-source UI framework for Laravel • Build and ship apps admin panels fast with Livewire项目地址: https://gitcode.com/GitHub_Trending/fi/filamentFilament 的 Schema 是表单Forms与 InfoList 的统称二者共享同一套组件模型与状态机制。本文基于仓库docs/10-testing/04-testing-schemas.md的完整内容结合packages/schemas/src/Testing/TestsSchemas.php、packages/forms/src/Testing/TestsForms.php等源码实现系统讲解如何在 Pest或 PHPUnit中为 Schema 编写测试如何向表单填充数据、断言字段状态与验证错误、检查字段/组件的存在性与可见性以及如何应对 Repeater、Builder 的 UUID 机制和 Wizard 的分步导航。读完后你将能为一套基于 Filament 的创建、编辑页面写出覆盖数据流、联动逻辑与验证规则的完整测试用例。测试 Schema 的前置认知在编写 Schema 测试之前需要先明确一个概念Filament 中真正的 Livewire 组件是页面Page、资源内的 Relation Manager 和 Widget而 Schema、Schema 组件和 Action 并不是 Livewire 组件。它们全部挂载在某个 Livewire 组件上因此测试时传给livewire()Pest或Livewire::test()PHPUnit的类必须是页面类本身而不是 Schema 或资源类。详见 Testing overview。本指南中的示例统一使用 Pest 的livewire()辅助函数。如果使用 PHPUnit只需将其替换为Livewire::test()即可其余断言方法完全一致。示例中出现的CreatePost、EditPost等是面板内的页面类通常在资源Resource的Pages目录下定义。Schema 测试所依赖的断言方法由两个测试特质提供TestsSchemas提供assertSchemaStateSet()、assertSchemaExists()、assertSchemaComponentExists()及 Wizard 导航等通用方法TestsForms提供fillForm()、assertHasFormErrors()、assertFormFieldExists()等表单专属方法。二者均以mixin Testable的方式混入 Livewire 的Testable对象所以可以直接在livewire()返回的链式调用上使用。在测试中向表单填充数据要向表单写入数据将数据数组传给fillForm()use function Pest\Livewire\livewire; livewire(CreatePost::class) -fillForm([ title fake()-sentence(), // ... ]);fillForm()内部会先断言目标 Schema 存在然后通过fillFormDataForTesting等内部方法把数据写入 Livewire 状态并重新刷新组件refresh()保证后续断言拿到的是最新的组件状态。从源码看fillForm()还支持传入Closure闭包会收到当前的原始状态$schemaInstance-getRawState()你可以基于已有状态动态计算要填充的值。多 Schema 场景如果同一个 Livewire 组件上存在多个 Schema可以通过第二个参数指定要填充的表单livewire(CreatePost::class) -fillForm([...], createPostForm);当组件实现了HasActions接口例如弹出了某个带表单的 Action 模态框时fillForm()会优先填充当前挂载的 Action Schema这正是许多 Action 测试可以直接复用fillForm()的原因。断言表单字段与 InfoList 条目的状态填充数据后通常需要验证字段的状态是否符合预期。使用assertSchemaStateSet()检查 Schema 中某个组件是否持有指定的状态值。最典型的场景是验证根据标题自动生成 slug这类联动逻辑use Illuminate\Support\Str; use function Pest\Livewire\livewire; it(can automatically generate a slug from the title, function () { $title fake()-sentence(); livewire(CreatePost::class) -fillForm([ title $title, ]) -assertSchemaStateSet([ slug Str::slug($title), ]); });如果组件上存在多个 Schema可以通过第二个参数指定要检查的 Schema如assertSchemaStateSet([...], createPostForm)。assertSchemaStateSet()的第二个参数也可以是闭包闭包会收到 Schema 的完整原始状态$state你可以先对状态做自定义断言再返回一个数组让 Filament 继续执行标准的状态断言use Illuminate\Support\Str; use function Pest\Livewire\livewire; it(can automatically generate a slug from the title without any spaces, function () { $title fake()-sentence(); livewire(CreatePost::class) -fillForm([ title $title, ]) -assertSchemaStateSet(function (array $state): array { expect($state[slug]) -not-toContain( ); return [ slug Str::slug($title), ]; }); });从 TestsSchemas.php 的实现看闭包形式会先执行$state($schemaInstance-getRawState())若返回数组则继续断言断言时对于getFlatComponents(withHidden: true)能匹配到的组件使用assertEquals逐一比对getState()其余键则回退到 Livewire 的assertSet()按状态路径校验。这意味着隐藏字段的状态同样会被校验。该系列方法在仓库测试中的实际用法可参考 InteractsWithSchemasTest.php例如对roles多选字段的状态断言。测试表单验证使用assertHasFormErrors()确保数据经过正确的验证use function Pest\Livewire\livewire; it(can validate input, function () { livewire(CreatePost::class) -fillForm([ title null, ]) -call(create) -assertHasFormErrors([title required]); });使用assertHasNoFormErrors()确保没有任何验证错误use function Pest\Livewire\livewire; livewire(CreatePost::class) -fillForm([ title fake()-sentence(), // ... ]) -call(create) -assertHasNoFormErrors();多 Schema 场景下可以把表单名作为第二个参数传入如assertHasFormErrors([title required], createPostForm)或assertHasNoFormErrors([], createPostForm)。在底层这两个方法会结合 Schema 的状态路径getStatePath()把传入的字段键前缀化后再转发给 Livewire 的assertHasErrors()/assertHasNoErrors()见 TestsForms.php因此错误键的书写方式与 Laravel 验证器返回的错误键一致。[title required]表示title 字段必须存在 required 规则产生的错误如果只写[title]整型键则只关心该字段是否存在任何验证错误不限定具体规则。测试表单的存在性检查 Livewire 组件上是否存在某个表单使用assertSchemaExists(form)use function Pest\Livewire\livewire; it(has a form, function () { livewire(CreatePost::class) -assertSchemaExists(form); });多 Schema 场景下可以传入具体表单名如assertSchemaExists(createPostForm)。从实现看该方法断言组件实例上的同名属性是Schema实例Assert::assertInstanceOf(Schema::class, ...)失败信息中会附带组件类名便于定位问题。测试表单字段的存在性确保表单包含某个字段把字段名传给assertFormFieldExists()use function Pest\Livewire\livewire; it(has a title field, function () { livewire(CreatePost::class) -assertFormFieldExists(title); });可以额外传入一个闭包作为真值测试truth test用于断言字段的具体配置例如该字段是否被禁用use function Pest\Livewire\livewire; it(has a title field, function () { livewire(CreatePost::class) -assertFormFieldExists(title, function (TextInput $field): bool { return $field-isDisabled(); }); });断言字段不存在使用assertFormFieldDoesNotExist()use function Pest\Livewire\livewire; it(does not have a conditional field, function () { livewire(CreatePost::class) -assertFormFieldDoesNotExist(no-such-field); });多 Schema 场景下可以把表单名作为参数指定如assertFormFieldExists(title, createPostForm)。底层实现TestsForms.php使用getFlatFields(withHidden: true)拉取全部字段含隐藏并断言目标是Field实例assertFormFieldDoesNotExist()则改用withHidden: false这意味着不可见即视为不存在可用于断言条件字段在某种状态下确实被移除了。测试表单字段的可见性确保字段可见把字段名传给assertFormFieldVisible()use function Pest\Livewire\livewire; test(title is visible, function () { livewire(CreatePost::class) -assertFormFieldVisible(title); });确保字段隐藏则使用assertFormFieldHidden()use function Pest\Livewire\livewire; test(title is hidden, function () { livewire(CreatePost::class) -assertFormFieldHidden(title); });assertFormFieldHidden()与assertFormFieldVisible()都可以把字段所属的表单名作为第二个参数传入如assertFormFieldHidden(title, createPostForm)。可见性断言的关键在于getFlatFields(withHidden: false)只返回当前可见的字段集合assertFormFieldVisible()断言目标键存在于该集合中assertFormFieldHidden()断言其不存在。因此这两组方法天然支持根据visible()/hidden()或live()联动切换显示状态的字段测试。测试表单字段的禁用状态确保字段可用使用assertFormFieldEnabled()use function Pest\Livewire\livewire; test(title is enabled, function () { livewire(CreatePost::class) -assertFormFieldEnabled(title); });确保字段被禁用则使用assertFormFieldDisabled()use function Pest\Livewire\livewire; test(title is disabled, function () { livewire(CreatePost::class) -assertFormFieldDisabled(title); });这两个方法同样支持把表单名作为第二个参数传入如assertFormFieldEnabled(title, createPostForm)。另外源码中还提供了assertFormFieldReadOnly()用于断言字段处于只读状态isReadOnly()可配合readOnly()配置使用。旧版assertFormFieldIsDisabled()/assertFormFieldIsEnabled()/assertFormFieldIsVisible()/assertFormFieldIsHidden()已标记为deprecated新代码应使用本文的命名方式。测试其他 Schema 组件字段之外的布局与容器组件如Section、Fieldset、Tabs等没有名称它们通过key()来标识。因此测试这类组件时使用assertSchemaComponentExists()并传入开发者为组件设置的 keyuse Filament\Schemas\Components\Section; Section::make(Comments) -key(comments-section) -schema([ // ])use function Pest\Livewire\livewire; test(comments section exists, function () { livewire(EditPost::class) -assertSchemaComponentExists(comments-section); });断言某个组件不存在使用assertSchemaComponentDoesNotExist()use function Pest\Livewire\livewire; it(does not have a conditional component, function () { livewire(CreatePost::class) -assertSchemaComponentDoesNotExist(no-such-section); });要同时检查组件是否存在且满足某个条件可通过checkComponentUsing参数传入闭包闭包收到组件实例并返回布尔值use Filament\Schemas\Components\Section; use function Pest\Livewire\livewire; test(comments section has heading, function () { livewire(EditPost::class) -assertSchemaComponentExists( comments-section, checkComponentUsing: function (Section $component): bool { return $component-getHeading() Comments; }, ); });如果希望失败信息更具体可以在真值闭包中内嵌Illuminate\Testing\Assert断言并始终返回trueuse Filament\Schemas\Components\Section; use Illuminate\Testing\Assert; use function Pest\Livewire\livewire; test(comments section is enabled, function () { livewire(EditPost::class) -assertSchemaComponentExists( comments-section, checkComponentUsing: function (Section $component): bool { Assert::assertTrue( $component-isEnabled(), Failed asserting that comments-section is enabled., ); return true; }, ); });从 TestsSchemas.php 的实现看组件存在性断言通过$schemaInstance-getFlatComponents(withHidden: true)[$key]查找组件并断言其是Component实例若传入了checkComponentUsing则额外执行真值测试。仓库中的 SectionTest.php 展示了大量Section::make(...)的配置测试示例可作参考。测试 Schema 组件的可见性确保组件可见把 key 传给assertSchemaComponentVisible()use function Pest\Livewire\livewire; test(comments section is visible, function () { livewire(EditPost::class) -assertSchemaComponentVisible(comments-section); });确保组件隐藏使用assertSchemaComponentHidden()use function Pest\Livewire\livewire; test(comments section is hidden, function () { livewire(EditPost::class) -assertSchemaComponentHidden(comments-section); });这两个方法都可以把组件所属的 Schema 名作为第二个参数传入如assertSchemaComponentHidden(comments-section, createPostForm)。与字段可见性类似这里基于getFlatComponents(withHidden: false)的可见组件集合做键存在性判断见 TestsSchemas.php。测试 Repeater用Repeater::fake()驯服 UUIDRepeater 内部会为每一项生成 UUID以便在 Livewire HTML 中稳定追踪条目。这意味着测试中表单与测试数据之间的 UUID 必须保持一致——如果你直接按数字下标组织期望数据测试会因为拿到的不是数字键而是 UUID 而失败。好消息是在测试环境里 Livewire 并不真正需要追踪这些 UUID因此可以在测试开头调用Repeater::fake()禁用 UUID 生成改用数字键use Filament\Forms\Components\Repeater; use function Pest\Livewire\livewire; $undoRepeaterFake Repeater::fake(); livewire(EditPost::class, [record $post]) -assertSchemaStateSet([ quotes [ [ content First quote, ], [ content Second quote, ], ], // ... ]); $undoRepeaterFake();Repeater::fake()返回一个撤销函数测试结束时调用它即可恢复 UUID 生成避免影响同文件中的其他测试。其底层实现位于 CanGenerateUuids.php通过configureUsing()对后续创建的组件统一执行generateUuidUsing(false)而generateUuid()在配置为false时返回null从而回退为数字键。也可以借助assertSchemaStateSet()的闭包形式统计 Repeater 的条目数量use Filament\Forms\Components\Repeater; use function Pest\Livewire\livewire; $undoRepeaterFake Repeater::fake(); livewire(EditPost::class, [record $post]) -assertSchemaStateSet(function (array $state) { expect($state[quotes]) -toHaveCount(2); }); $undoRepeaterFake();测试 Repeater 内的 Action要验证 Repeater 内的 Action如删除该项发送该项是否按预期工作可以使用callFormComponentAction()调用组件 Action然后执行后续断言例如通过assertNotified()验证通知更多 Action 断言参见 Testing actions。与 Repeater 中特定条目交互时需要传入item参数并指定该条目的 key如果 Repeater 读取自关联关系relationship需要把关联记录的主键加上record-前缀来构成条目 keyuse App\Models\Quote; use Filament\Forms\Components\Repeater; use function Pest\Livewire\livewire; $quote Quote::first(); livewire(EditPost::class, [record $post]) -callAction(TestAction::make(sendQuote)-schemaComponent(quotes)-arguments([ item record-{$quote-getKey()}, ])) -assertNotified(Quote sent!);callFormComponentAction()等系列方法定义在 TestsFormComponentActions.php它会把{$form}.{$component}上下文注入 Action 后再调用callAction()同文件还提供mountFormComponentAction()、assertFormComponentActionExists()、assertFormComponentActionVisible()、assertFormComponentActionEnabled()等配套断言覆盖组件 Action 的挂载、可见、可用状态与图标、标签、颜色、URL 等属性。测试 Builder同样需要Builder::fake()Builder 与 Repeater 面临相同的 UUID 问题——它内部同样为每个块block生成 UUID 用于在 Livewire HTML 中追踪条目。因此测试 Builder 时同样需要在测试开头调用Builder::fake()禁用 UUID、改用数字键use Filament\Forms\Components\Builder; use function Pest\Livewire\livewire; $undoBuilderFake Builder::fake(); livewire(EditPost::class, [record $post]) -assertSchemaStateSet([ content [ [ type heading, data [ content Hello, world!, level h1, ], ], [ type paragraph, data [ content This is a test post., ], ], ], // ... ]); $undoBuilderFake();与 Repeater 相同Builder 的每个条目由type块类型与data该块的具体数据两部分组成期望数据需要按此结构组织。同样可以通过闭包统计 Builder 的块数量use Filament\Forms\Components\Builder; use function Pest\Livewire\livewire; $undoBuilderFake Builder::fake(); livewire(EditPost::class, [record $post]) -assertSchemaStateSet(function (array $state) { expect($state[content]) -toHaveCount(2); }); $undoBuilderFake();Builder与Repeater都引入了Concerns\CanGenerateUuids特质见 Builder.php 与 Repeater.php所以两者的fake()行为完全一致。测试 Wizard分步导航Wizard向导是分步表单测试时可以在步骤之间切换。进入下一步使用goToNextWizardStep()use function Pest\Livewire\livewire; it(moves to next wizard step, function () { livewire(CreatePost::class) -goToNextWizardStep() -assertHasFormErrors([title]); });返回上一步使用goToPreviousWizardStep()use function Pest\Livewire\livewire; it(moves to next wizard step, function () { livewire(CreatePost::class) -goToPreviousWizardStep() -assertHasFormErrors([title]); });如果需要跳到指定步骤使用goToWizardStep()随后用assertWizardCurrentStep()确认当前所处的步骤——这样断言的是确实到达了目标步骤而不会带上跳转过程中前一步的验证错误use function Pest\Livewire\livewire; it(moves to the wizards second step, function () { livewire(CreatePost::class) -goToWizardStep(2) -assertWizardCurrentStep(2); });如果单个 Livewire 组件上有多个 Schema上述 Wizard 测试辅助方法都可以接收schema参数来限定操作目标use function Pest\Livewire\livewire; it(moves to next wizard step only for fooForm, function () { livewire(CreatePost::class) -goToNextWizardStep(schema: fooForm) -assertHasFormErrors([title], schema: fooForm); });从 TestsSchemas.php 的实现看Wizard 测试的核心是先通过getComponent(...)找到 Schema 内的Wizard组件实例再调用 Livewire 的callSchemaComponentMethod触发Wizard的nextStep(int $currentStepIndex)/previousStep(int $currentStepIndex)方法定义见 Wizard.php。其中goToWizardStep($step)会把目标步骤换算成起始索引($step 1) ? 0 : $step - 2即从当前步骤走到底 N 步的索引差而assertWizardCurrentStep()则通过getCurrentStepIndex() 1换算为 1 起始的步骤号进行比对并附带当前实际步骤的失败提示。另外assertWizardStepExists()可以单独断言某个步骤号是否存在基于getDefaultChildComponents()的下标。总结Schema 测试方法速查目标方法多 Schema 参数填充表单数据fillForm([...], formName)第二个参数断言状态已写入assertSchemaStateSet([...], schemaName)第二个参数断言存在验证错误assertHasFormErrors([...], formName)第二个参数断言无验证错误assertHasNoFormErrors([], formName)第二个参数断言 Schema 存在assertSchemaExists(form)直接传名称断言字段存在/不存在assertFormFieldExists()/assertFormFieldDoesNotExist()表单名参数断言字段可见/隐藏assertFormFieldVisible()/assertFormFieldHidden()表单名参数断言字段可用/禁用/只读assertFormFieldEnabled()/assertFormFieldDisabled()/assertFormFieldReadOnly()表单名参数断言组件存在/不存在assertSchemaComponentExists()/assertSchemaComponentDoesNotExist()Schema 名参数断言组件可见/隐藏assertSchemaComponentVisible()/assertSchemaComponentHidden()Schema 名参数Repeater / Builder 条目Repeater::fake()/Builder::fake()assertSchemaStateSet()—调用组件内 ActioncallFormComponentAction()formName参数Wizard 分步导航goToNextWizardStep()/goToPreviousWizardStep()/goToWizardStep()/assertWizardCurrentStep()schema参数建议的测试套路是每个测试用fillForm()建立前置状态 → 用call(create)或组件 Action 触发业务逻辑 → 用assertSchemaStateSet()、assertHasFormErrors()/assertHasNoFormErrors()验证数据与验证结果 → 对联动字段用assertFormFieldVisible()/assertFormFieldHidden()与assertFormFieldEnabled()/assertFormFieldDisabled()验证 UI 状态变化。涉及 Repeater、Builder 的用例不要忘记在开头调用对应的fake()并在结尾撤销涉及 Wizard 的用例则善用goToWizardStep()assertWizardCurrentStep()组合。完整的资源级测试示例还可参考 Testing resources 与 Testing tables。【免费下载链接】filamentA powerful open-source UI framework for Laravel • Build and ship apps admin panels fast with Livewire项目地址: https://gitcode.com/GitHub_Trending/fi/filament创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?