Skip to main content
Firstlight

Firstlight / Docs

Recipes for the screens a forms app needs, setup for your coding agent, Laravel on native screens, and accessibility.

View source
Test Firstlight screens

Test Firstlight Screens

Firstlight screens are tested with NativePHP's screen test harness, Native\Mobile\Testing\Native::test(). The harness mounts the screen, renders the Element Tree, and sends taps and input through the same callbacks a device uses. Firstlight adds checks for its own controls: field errors, List rows, Confirmation Dialog, and Feedback.

Scaffold a screen

php artisan make:firstlight-screen Posts --form --destroy --test

This creates the following files:

  • app/NativeComponents/Posts.php: a FirstlightUI\NativeComponent with a validated name field and save() (--form), and paginated reload(), loadMore(), and listQuery() (--list). It also has requestDeletion(), cancelDeletion(), and deleteItem() (--destroy, which turns on --list).
  • resources/views/native/posts.blade.php: the matching Text Field, Button, List, and Confirmation Dialog markup.
  • tests/Feature/NativeComponents/PostsTest.php: a Pest test that uses the checks below (--test).

Nested names such as Settings/Notifications create App\NativeComponents\Settings\Notifications and the view native.settings.notifications. The command does not overwrite existing files unless you pass --force. Register the route it prints in routes/mobile.php.

Replace the body of listQuery() with your paginator query, for example Post::query()->latest()->paginate(perPage: 20, page: $page->page).

Register the checks

Register the Firstlight checks once from tests/Pest.php:

FirstlightUI\Testing\ScreenAssertions::register();

Calling it again has no effect. The generated test also calls it in beforeEach().

Test a form

use App\NativeComponents\EditProfile;
use FirstlightUI\Facades\Feedback;
use FirstlightUI\Feedback\FeedbackTone;
use Native\Mobile\Testing\Native;

it('validates and saves the profile', function () {
    Feedback::fake();

    Native::test(EditProfile::class)
        ->input('email', 'not-an-email')
        ->tap('Save')
        ->assertFieldError('email', 'The email must be a valid email address.')
        ->assertNoFeedbackSent()
        ->input('email', '[email protected]')
        ->tap('Save')
        ->assertNoFieldError('email')
        ->assertHasNoErrors()
        ->assertFeedbackSent('Profile saved', FeedbackTone::Success);
});

assertFieldError() reads the error value the field shows in the rendered tree, so it confirms the message reached the control, not only the MessageBag. Use assertHasErrors() to check the bag directly.

Select a Select, Segmented, Pill Group, or Choice Group option by its stable value, never by its label or position:

Native::test(EditProfile::class)
    ->choose('plan', 'team')
    ->assertSet('plan', 'team');

Test a list and its destructive action

it('deletes a post after confirmation', function () {
    Feedback::fake();

    Native::test(PostsScreen::class)
        ->assertListItem('Draft', fn (array $row) => $row['props']['disabled'] === false)
        ->pressListItem('Draft', 'Delete')
        ->assertConfirmationDialog('Delete post?')
        ->confirmDialog()
        ->assertMissingListItem('Draft')
        ->assertFeedbackSent('Post deleted');
});

pressListItem('Draft') presses the row. Pass a swipe action label as the second argument to press that action. Both fail when the row is disabled, as the device would ignore the press. cancelDialog() sends the dismissal that both platforms send for Cancel and system dismissal.

See Screen assertions for every method.

Served from firstlightui/nativephp on GitHub.