Filament Actions 测试完整指南:从 callAction 到 TestAction 的实战与源码解析

发布时间:2026/9/11 15:35:33
Filament Actions 测试完整指南:从 callAction 到 TestAction 的实战与源码解析 Filament Actions 测试完整指南从 callAction 到 TestAction 的实战与源码解析【免费下载链接】filamentA powerful open-source UI framework for Laravel • Build and ship apps admin panels fast with Livewire项目地址: https://gitcode.com/GitHub_Trending/fi/filamentFilament 的 Action 系统贯穿表格、表单、模态框与 infolist 等几乎所有交互场景而 docs/10-testing/05-testing-actions.md 正是针对这一系统编写的官方测试指南。本文以该文档为骨架结合packages/actions包内的测试宏TestsActions、TestAction定位对象以及tests/src/Actions下的真实测试用例系统讲解如何用 Pest Livewire 测试助手对 Filament Action 进行端到端断言帮助你在读完本文后能独立编写覆盖触发、表单、校验、可见性、禁用、状态、外观、参数等维度的完整 Action 测试套件。测试基础为什么 Filament 的 Action 可以用 Livewire 测试Filament 的所有组件最终都挂载在一个 Livewire 组件上因此测试 Filament 与测试 Livewire 组件是同一件事——全程使用 Livewire 的测试助手。在 Pest 中借助其 Livewire 插件提供的livewire()函数在 PHPUnit 中则替换为Livewire::test()方法即可参见 docs/10-testing/01-overview.md。需要特别区分的是资源类、Schema 组件、Action 本身都不是 Livewire 组件但页面含资源Pages目录下的类、RelationManager、Widget 是。因此测试 Action 时传入livewire()的应是承载该 Action 的页面或组件类例如EditInvoice::class、ListInvoices::class、ManageInvoices::class。Action 的所有测试宏都实现在 packages/actions/src/Testing/TestsActions.php它以mixin Testable方式混入 Livewire 的测试对象所以-callAction()、-assertActionExists()等方法都可以直接链式调用。调用 ActioncallAction / mountAction / callMountedAction用名称或类调用最简单的方式是把 Action 的名称字符串或类名传给callAction()use function Pest\Livewire\livewire; it(can send invoices, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [ invoice $invoice, ]) -callAction(send); expect($invoice-refresh()) -isSent()-toBeTrue(); });callAction的底层执行序列见 TestsActions.php是先assertActionVisible()断言可见 →parseNestedActions()解析嵌套 Action →mountAction()挂载 → 若存在模态框表单且有$data则fillForm()填充 → 最后callMountedAction()真正提交。这也解释了为何callAction()在内部会自动完成挂载 填充 提交三步。只挂载不调用如果只想打开 Action 的模态框而不提交使用mountAction()对已经挂载的 Action 提交使用callMountedAction()。这在先断言模态框内容、再提交的场景中非常常用livewire(EditInvoice::class, [invoice $invoice]) -mountAction(send) -assertMountedActionModalSee($recipientEmail) -callMountedAction();从源码看mountAction会逐个调用 Livewire 的mountAction($name, $arguments, $context)方法mountedActions是组件上记录挂载栈的状态而callMountedAction则调用callMountedAction($arguments)并直接取得当前挂载的 Action 实例。向 Action 传入数据模态框表单Action 模态框中的表单数据通过data:命名参数传入it(can send invoices, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [ invoice $invoice, ]) -callAction(send, data: [ email $email fake()-email(), ]) -assertHasNoFormErrors(); expect($invoice-refresh()) -isSent()-toBeTrue() -recipient_email-toBe($email); });若只想预填数据而不立即触发可先用mountAction()挂载再用fillForm()填充在callAction的实现里filled($data)时执行的也正是同一个fillForm()两者行为一致。断言表单校验错误assertHasNoFormErrors()断言提交 Action 表单时没有产生校验错误assertHasFormErrors()断言产生了指定校验错误用法与 Livewire 的assertHasErrors()类似第二个参数为字段名、第三个为规则名it(can validate invoice recipient email, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -callAction(send, data: [ email Str::random(), ]) -assertHasFormErrors([email [email]]); });此外还有一对别名方法assertHasActionErrors()/assertHasNoActionErrors()它们内部直接委托给assertHasFormErrors()/assertHasNoFormErrors()见 TestsActions.php。断言表单被预填assertSchemaStateSet()用于断言 Action 的 Schema 状态已被预填为期望值非常适合验证默认值逻辑it(can send invoices to the primary contact by default, function () { $invoice Invoice::factory()-create(); $recipientEmail $invoice-company-primaryContact-email; livewire(EditInvoice::class, [invoice $invoice]) -mountAction(send) -assertSchemaStateSet([ email $recipientEmail, ]) -callMountedAction() -assertHasNoFormErrors(); expect($invoice-refresh()) -isSent()-toBeTrue() -recipient_email-toBe($recipientEmail); });定位 ActionTestAction 对象当 Action 不在页面最顶层例如位于表格行内、表格头部、批量操作区、infolist 的 schema 组件内时字符串名称无法唯一定位。此时使用Filament\Actions\Testing\TestAction对象。其核心方法见 packages/actions/src/Testing/TestAction.php包括方法作用源码位置TestAction::make($name)创建定位对象指定 Action 名称TestAction.php-table($record true)定位表格内的 Action不传参时定位表格头部 ActionTestAction.php-bulk(bool $condition true)定位表格批量bulkActionTestAction.php-schemaComponent($component, ?string $schema null)定位 Schema表单/infolist组件内的 ActionTestAction.php-arguments(array \| Closure \| null $arguments)指定/断言 Action 参数TestAction.php在序列化toArray()时table()会把记录键写入context.recordKey模型取getKey()数组取主键bulk()写入context.bulkschemaComponent()则生成context.schemaComponent最终由parseNestedActions()解析并传给 Livewire 的mountAction()。测试表格行内 Actionuse Filament\Actions\Testing\TestAction; use function Pest\Livewire\livewire; $invoice Invoice::factory()-create(); livewire(ListInvoices::class) -callAction(TestAction::make(send)-table($invoice)); livewire(ListInvoices::class) -assertActionVisible(TestAction::make(send)-table($invoice)); livewire(ListInvoices::class) -assertActionExists(TestAction::make(send)-table($invoice));table($invoice)传模型时测试宏会通过getTableRecordKey()将其转换为表格记录键见 TestsActions.php。测试表格头部 Action头部 Action 不针对特定记录table()不带参数即可livewire(ListInvoices::class) -callAction(TestAction::make(create)-table()); livewire(ListInvoices::class) -assertActionVisible(TestAction::make(create)-table()); livewire(ListInvoices::class) -assertActionExists(TestAction::make(create)-table());测试表格批量 Action批量 Action 需要先用selectTableRecords()勾选记录再用table()-bulk()组合定位$invoices Invoice::factory()-count(3)-create(); livewire(ListInvoices::class) -selectTableRecords($invoices-pluck(id)-toArray()) -callAction(TestAction::make(send)-table()-bulk()); livewire(ListInvoices::class) -assertActionVisible(TestAction::make(send)-table()-bulk()); livewire(ListInvoices::class) -assertActionExists(TestAction::make(send)-table()-bulk());测试 Schemainfolist / 表单内的 Action若 Action 属于某个 infolist entry 的belowContent()之类的 Schema 组件用schemaComponent()指定组件名$invoice Invoice::factory()-create(); livewire(EditInvoice::class) -callAction(TestAction::make(send)-schemaComponent(customer_id)); livewire(EditInvoice::class) -assertActionVisible(TestAction::make(send)-schemaComponent(customer_id)); livewire(EditInvoice::class) -assertActionExists(TestAction::make(send)-schemaComponent(customer_id));schemaComponent()的第二个参数可指定所在 schema 的名称TestAction::make(...)-schemaComponent(form-actions, schema: content)这在资源页的getFormActions()场景下是必需的详见 docs/10-testing/02-testing-resources.md 中Testing create / edit pagegetFormActions()一节自定义的Action::make(createAndVerifyEmail)位于CreateUser页contentschema 的form-actions键中需写成-callAction(TestAction::make(createAndVerifyEmail)-schemaComponent(form-actions, schema: content))。测试另一个 Action 的模态框内嵌 Action如果 Action 位于另一个 Action 模态框的schema()/form()内例如内嵌在模态框某个字段的belowContent()则按嵌套顺序传入一个TestAction数组由parseNestedActions()逐层解析$invoice Invoice::factory()-create(); livewire(ManageInvoices::class) -callAction([ TestAction::make(view)-table($invoice), TestAction::make(send)-schemaComponent(customer.name), ]); livewire(ManageInvoices::class) -assertActionVisible([ TestAction::make(view)-table($invoice), TestAction::make(send)-schemaComponent(customer.name), ]); livewire(ManageInvoices::class) -assertActionExists([ TestAction::make(view)-table($invoice), TestAction::make(send)-schemaComponent(customer.name), ]);源码层面parseNestedActions()对TestAction调用toArray(defaultSchema: ...)其中嵌套 Action 的默认 schema 名为mountedActionSchema{n}见 TestsActions.php从而将内嵌 Action 正确绑定到外层 Action 的模态框 schema 上。测试 Action 参数Action 定义时若声明了arguments如Action::make(send)-arguments([...])测试中可用arguments()指定期望的参数值传Closure时还能通过checkArguments()做自定义参数断言TestAction.phpuse Filament\Actions\Testing\TestAction; $invoice Invoice::factory()-create(); livewire(ManageInvoices::class) -callAction(TestAction::make(send)-arguments([invoice $invoice-getKey()])); livewire(ManageInvoices::class) -assertActionVisible(TestAction::make(send)-arguments([invoice $invoice-getKey()])); livewire(ManageInvoices::class) -assertActionExists(TestAction::make(send)-arguments([invoice $invoice-getKey()]));断言模态框内容要检查模态框渲染出的内容应先mountAction()callAction()会关闭模态框然后使用以下四个断言断言方法说明匹配方式assertMountedActionModalSee($values)断言模态框 HTML 包含给定内容默认对内容做e()转义后匹配assertMountedActionModalDontSee($values)断言模态框 HTML 不包含给定内容默认转义后匹配assertMountedActionModalSeeHtml($values)断言模态框 HTML 包含给定 HTML不转义直接匹配assertMountedActionModalDontSeeHtml($values)断言模态框 HTML 不包含给定 HTML不转义it(confirms the target address before sending, function () { $invoice Invoice::factory()-create(); $recipientEmail $invoice-company-primaryContact-email; livewire(EditInvoice::class, [invoice $invoice]) -mountAction(send) -assertMountedActionModalSee($recipientEmail); });底层实现中这四者都依赖getMountedActionModalHtml()从 Livewire 最近一次响应的partials中提取action-modals或带嵌套索引的action-modals.{index}部分未找到时直接Assert::fail()见 TestsActions.php。断言存在性与可见性存在 / 不存在assertActionExists()与assertActionDoesNotExist()用于断言 Action 是否注册it(can send but not unsend invoices, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionExists(send) -assertActionDoesNotExist(unsend); });assertActionExists()可追加一个闭包作为真值测试用于断言 Action 的具体配置。闭包接收解析出的Filament\Actions\Action实例可调用其 getteruse Filament\Actions\Action; it(has the correct description, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionExists(send, function (Action $action): bool { return $action-getModalDescription() This will send an email to the customer\s primary address, with the invoice attached as a PDF; }); });从 TestsActions.php 的实现看该断言先通过组件上的getAction()HasActions契约方法见 packages/actions/src/Contracts/HasActions.php解析出 Action 实例并断言其类型再对checkActionUsing闭包做assertTrue。assertActionDoesNotExist()则捕获ActionNotResolvableException解析不到即视为不存在。可见 / 隐藏assertActionVisible()/assertActionHidden()分别断言$action-isVisible()/$action-isHidden()it(can only print invoices, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionHidden(send) -assertActionVisible(print); });源码中两者都是assertActionExists()加一个checkActionUsing闭包的语法糖TestsActions.php失败消息会明确提示Failed asserting that an action with name [...] is visible/hidden on the [...] component.。断言启用 / 禁用状态与顺序assertActionEnabled()/assertActionDisabled()断言isEnabled()/isDisabled()it(can only print a sent invoice, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionDisabled(send) -assertActionEnabled(print); });assertActionListInOrder()断言一组 Action 以正确的顺序存在支持 Action 组自动展开见 TestsActions.phpit(can have actions in order, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionListInOrder([send, export]); });断言 Action 外观标签、图标、颜色、URL断言方法底层 getterassertActionHasLabel($actions, $label)/assertActionDoesNotHaveLabel(...)getLabel()assertActionHasIcon($actions, $icon)/assertActionDoesNotHaveIcon(...)getIcon()assertActionHasColor($actions, $color)/assertActionDoesNotHaveColor(...)getColor()assertActionHasUrl($actions, $url)/assertActionDoesNotHaveUrl(...)getUrl()assertActionShouldOpenUrlInNewTab(...)/assertActionShouldNotOpenUrlInNewTab(...)shouldOpenUrlInNewTab()标签断言it(send action has correct label, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionHasLabel(send, Email Invoice) -assertActionDoesNotHaveLabel(send, Send); });图标断言图标既支持字符串也支持BackedEnum枚举时取-value比较见 TestsActions.phpit(when enabled the send button has correct icon, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionEnabled(send) -assertActionHasIcon(send, envelope-open) -assertActionDoesNotHaveIcon(send, envelope); });颜色断言颜色名取字符串本身自定义色数组会归一化为customit(actions display proper colors, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionHasColor(delete, danger) -assertActionDoesNotHaveColor(print, danger); });URL 与新标签页打开断言it(links to the correct Filament sites, function () { $invoice Invoice::factory()-create(); livewire(EditInvoice::class, [invoice $invoice]) -assertActionHasUrl(filament, https://filamentphp.com/) -assertActionDoesNotHaveUrl(filament, https://github.com/filamentphp/filament) -assertActionShouldOpenUrlInNewTab(filament) -assertActionShouldNotOpenUrlInNewTab(github); });断言 Action 被 halt中断在 Action 的action()闭包中调用halt()会抛出Halt异常中断执行见 packages/actions/src/Action.phpcancel()则抛出Cancel常用于条件不满足就中止的业务逻辑。测试中用assertActionHalted()断言该 Action 仍处于挂载中断状态it(stops sending if invoice has no email address, function () { $invoice Invoice::factory([email null])-create(); livewire(EditInvoice::class, [invoice $invoice]) -callAction(send) -assertActionHalted(send); });从源码看assertActionHalted()就是assertActionMounted()的别名TestsActions.php旧名assertActionHeld()已标记废弃。仓库测试中也有对应用例见 tests/src/Actions/ActionTest.php 的 can call an action and halt先断言事件halt-called被派发再assertActionHalted(halt)。在测试中使用 Action 类名Filament 内置了大量预置 ActionCreateAction、EditAction、DeleteAction等位于 packages/actions/src 下它们可以直接以类名传入测试方法use Filament\Actions\CreateAction; livewire(ManageInvoices::class) -callAction(CreateAction::class);通过#[ActionName]属性暴露自定义 Action 名对于自带make()方法的普通 Action 类Filament 无法高效地通过运行make()来探测名称因此提供了#[ActionName]属性见 packages/actions/src/ActionName.php属性值必须与测试中使用的 Action 名一致use Filament\Actions\Action; use Filament\Actions\ActionName; #[ActionName(send)] class SendInvoiceAction { public static function make(): Action { return Action::make(send) -requiresConfirmation() -action(function () { // ... }); } }之后即可在测试中使用类名use App\Filament\Resources\Invoices\Actions\SendInvoiceAction; use Filament\Actions\Testing\TestAction; $invoice Invoice::factory()-create(); livewire(ManageInvoices::class) -callAction(TestAction::make(SendInvoiceAction::class)-table($invoice));parseNestedActions()在解析时会读取类上的ActionName属性并替换为真实名称见 TestsActions.phpassertActionListInOrder()也做了同样的名称解析。通过getDefaultName()让 Action 类自报名称若自定义 Action 类直接继承Filament\Actions\Action可重写静态方法getDefaultName()基类默认返回null见 packages/actions/src/Action.php。这样既能让 Filament 发现名称也允许实例化时省略make()的名称参数use Filament\Actions\Action; class SendInvoiceAction extends Action { public static function getDefaultName(): string { return send; } protected function setUp(): void { parent::setUp(); $this -requiresConfirmation() -action(function () { // ... }); } }Action::make($name ?? static::getDefaultName())Action.php与测试解析中的$actionName::getDefaultName()TestsActions.php共同构成了这条名称发现链路。测试要点小结先确认承载 Action 的 Livewire 组件类再传给livewire()Action 本身不是 Livewire 组件顶层 Action 用字符串名即可表格 / schema / 嵌套场景务必使用TestAction的table()、bulk()、schemaComponent()、arguments()组合定位需要断言模态框内容时用mountAction()而非callAction()校验、预填、外观、可见性、禁用、halt 等维度均有对应的断言宏且大多是对assertActionExists() 闭包的包装可读性与失败信息都经过优化自定义 Action 类接入测试体系有两种方式#[ActionName]属性普通类或getDefaultName()继承Action的类。如需进一步了解 Action 的定义、模态框与嵌套行为可继续阅读 docs/10-testing/02-testing-resources.md资源页getFormActions()的测试、docs/10-testing/03-testing-tables.md表格与表格 Action 测试以及 docs/10-testing/04-testing-schemas.mdSchema 组件测试。【免费下载链接】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),仅供参考