pest icon indicating copy to clipboard operation
pest copied to clipboard

Laravel Livewire Plugin: date cast with format causes test failures

Open trip-somers opened this issue 3 years ago • 8 comments

  1. Eloquent model contains: a 'dateAttribute' => 'date:Y-m-d' cast.
  2. All tests of this component fail during Livewire's hydration cascade (see below), but it continues to display properly via browser.
  3. When changing the cast from date:Y-m-d to date, all tests pass, but <input type="date" wire:model="dateAttribute" ... > fails to bind the existing value.
  4. [ADDED] Potentially complicated by the fact that the models use jenssegers/laravel-mongodb.

Since Livewire itself seems to handle all of this as expected, I believe the issue is somewhere within Pest's Livewire Plugin.

I have pasted the verbose Pest failure below.

Additional, possibly useful information: I do not know why it says $subject in the exception message. It is neither an attribute nor a relation of the model in question.

  preg_match(): Argument #2 ($subject) must be of type string, array given

  at vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:1335
    1331▕      * @return bool
    1332▕      */
    1333▕     protected function isStandardDateFormat($value)
    1334▕     {
  ➜ 1335▕         return preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value);
    1336▕     }
    1337▕ 
    1338▕     /**
    1339▕      * Convert a DateTime to a storable string.

  1   vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:1335
      preg_match()

  2   vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:1309
      Illuminate\Database\Eloquent\Model::isStandardDateFormat()

  3   vendor/jenssegers/mongodb/src/Eloquent/Model.php:111
      Illuminate\Database\Eloquent\Model::asDateTime()

  4   vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:739
      Jenssegers\Mongodb\Eloquent\Model::asDateTime()

  5   vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:275
      Illuminate\Database\Eloquent\Model::castAttribute()

  6   vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php:194
      Illuminate\Database\Eloquent\Model::addCastAttributesToArray()

  7   vendor/jenssegers/mongodb/src/Eloquent/Model.php:202
      Illuminate\Database\Eloquent\Model::attributesToArray()

  8   vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1485
      Jenssegers\Mongodb\Eloquent\Model::attributesToArray()

  9   vendor/livewire/livewire/src/HydrationMiddleware/HydratePublicProperties.php:236
      Illuminate\Database\Eloquent\Model::toArray()

  10  vendor/livewire/livewire/src/HydrationMiddleware/HydratePublicProperties.php:216
      Livewire\HydrationMiddleware\HydratePublicProperties::filterData()

  11  vendor/livewire/livewire/src/HydrationMiddleware/HydratePublicProperties.php:105
      Livewire\HydrationMiddleware\HydratePublicProperties::dehydrateModel()

  12  [internal]:0
      Livewire\HydrationMiddleware\HydratePublicProperties::Livewire\HydrationMiddleware\{closure}()

  13  vendor/livewire/livewire/src/HydrationMiddleware/HydratePublicProperties.php:137
      array_walk()

  14  vendor/livewire/livewire/src/LifecycleManager.php:154
      Livewire\HydrationMiddleware\HydratePublicProperties::dehydrate()

  15  vendor/livewire/livewire/src/Connection/ConnectionHandler.php:15
      Livewire\LifecycleManager::dehydrate()

  16  vendor/livewire/livewire/src/Controllers/HttpConnectionHandler.php:20
      Livewire\Connection\ConnectionHandler::handle()

  17  vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php:48
      Livewire\Controllers\HttpConnectionHandler::__invoke()

  18  vendor/laravel/framework/src/Illuminate/Routing/Route.php:262
      Illuminate\Routing\ControllerDispatcher::dispatch()

  19  vendor/laravel/framework/src/Illuminate/Routing/Route.php:205
      Illuminate\Routing\Route::runController()

  20  vendor/laravel/framework/src/Illuminate/Routing/Router.php:721
      Illuminate\Routing\Route::run()

  21  vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:128
      Illuminate\Routing\Router::Illuminate\Routing\{closure}()

  22  vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:103
      Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}()

  23  vendor/laravel/framework/src/Illuminate/Routing/Router.php:723
      Illuminate\Pipeline\Pipeline::then()

  24  vendor/laravel/framework/src/Illuminate/Routing/Router.php:698
      Illuminate\Routing\Router::runRouteWithinStack()

  25  vendor/laravel/framework/src/Illuminate/Routing/Router.php:662
      Illuminate\Routing\Router::runRoute()

  26  vendor/laravel/framework/src/Illuminate/Routing/Router.php:651
      Illuminate\Routing\Router::dispatchToRoute()

  27  vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:167
      Illuminate\Routing\Router::dispatch()

  28  vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:128
      Illuminate\Foundation\Http\Kernel::Illuminate\Foundation\Http\{closure}()

  29  vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:103
      Illuminate\Pipeline\Pipeline::Illuminate\Pipeline\{closure}()

  30  vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:142
      Illuminate\Pipeline\Pipeline::then()

  31  vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:111
      Illuminate\Foundation\Http\Kernel::sendRequestThroughRouter()

  32  vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:510
      Illuminate\Foundation\Http\Kernel::handle()

  33  vendor/livewire/livewire/src/Testing/TestableLivewire.php:198
      Livewire\Testing\MakesHttpRequestsWrapper::call()

  34  vendor/livewire/livewire/src/Testing/MakesHttpRequestsWrapper.php:29
      Livewire\Testing\TestableLivewire::Livewire\Testing\{closure}()

  35  vendor/livewire/livewire/src/Testing/TestableLivewire.php:199
      Livewire\Testing\MakesHttpRequestsWrapper::temporarilyDisableExceptionHandlingAndMiddleware()

  36  vendor/livewire/livewire/src/Testing/TestableLivewire.php:183
      Livewire\Testing\TestableLivewire::callEndpoint()

  37  vendor/livewire/livewire/src/Testing/Concerns/MakesCallsToComponent.php:143
      Livewire\Testing\TestableLivewire::pretendWereSendingAComponentUpdateRequest()

  38  vendor/livewire/livewire/src/Testing/Concerns/MakesCallsToComponent.php:38
      Livewire\Testing\TestableLivewire::sendMessage()

  39  vendor/livewire/livewire/src/Testing/Concerns/MakesCallsToComponent.php:32
      Livewire\Testing\TestableLivewire::runAction()

  40  tests/Feature/Tryouts/EvaluateSingleTest.php:96
      Livewire\Testing\TestableLivewire::call()

  41  vendor/pestphp/pest/src/Factories/TestCaseFactory.php:151
      P\Tests\Feature\Tryouts\EvaluateSingleTest::{closure}()

  42  vendor/pestphp/pest/src/Factories/TestCaseFactory.php:151
      call_user_func()

  43  vendor/pestphp/pest/src/Concerns/Testable.php:299
      P\Tests\Feature\Tryouts\EvaluateSingleTest::Pest\Factories\{closure}()

  44  vendor/pestphp/pest/src/Concerns/Testable.php:299
      call_user_func_array()

  45  vendor/pestphp/pest/src/Support/ExceptionTrace.php:29
      P\Tests\Feature\Tryouts\EvaluateSingleTest::Pest\Concerns\{closure}()

  46  vendor/pestphp/pest/src/Concerns/Testable.php:300
      Pest\Support\ExceptionTrace::ensure()

  47  vendor/pestphp/pest/src/Concerns/Testable.php:276
      P\Tests\Feature\{MyComponent}Test::__callClosure()

  48  vendor/phpunit/phpunit/src/Framework/TestCase.php:1545
      P\Tests\Feature\{MyComponent}Test::__test()

  49  vendor/phpunit/phpunit/src/Framework/TestCase.php:1151
      PHPUnit\Framework\TestCase::runTest()

  50  vendor/phpunit/phpunit/src/Framework/TestResult.php:726
      PHPUnit\Framework\TestCase::runBare()

  51  vendor/phpunit/phpunit/src/Framework/TestCase.php:903
      PHPUnit\Framework\TestResult::run()

  52  vendor/phpunit/phpunit/src/Framework/TestSuite.php:670
      PHPUnit\Framework\TestCase::run()

  53  vendor/phpunit/phpunit/src/Framework/TestSuite.php:670
      PHPUnit\Framework\TestSuite::run()

  54  vendor/phpunit/phpunit/src/TextUI/TestRunner.php:673
      PHPUnit\Framework\TestSuite::run()

  55  vendor/phpunit/phpunit/src/TextUI/Command.php:143
      PHPUnit\TextUI\TestRunner::run()

  56  vendor/pestphp/pest/src/Console/Command.php:117
      PHPUnit\TextUI\Command::run()

  57  vendor/pestphp/pest/bin/pest:62
      Pest\Console\Command::run()

  58  vendor/pestphp/pest/bin/pest:63
      {closure}()

  59  vendor/bin/pest:117
      include("/application/vendor/pestphp/pest/bin/pest")

trip-somers avatar Jul 22 '22 05:07 trip-somers

[UPDATED with MongoDB context since it now seems important.]

I was able to recreate this with the following setup:

Model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Jenssegers\Mongodb\Eloquent\Model;

class DateFormattedModel extends Model
{
    use HasFactory;

    protected $connection = 'mongodb';
    protected $collection = 'datemodels';

    protected $casts = [
        'dateAttribute' => 'date:Y-m-d',
    ]
}

Component:

<?php

namespace App\Http\Livewire;

use App\Models\DateFormattedModel;
use Livewire\Component;

class DateFormat extends Component
{
    public DateFormattedModel $model;

    public function doNothing()
    {
        // literally doing nothing
    }

    public function render()
    {
        return view('livewire.date-format');
    }
}

Template:

<div>
    {{ $model->dateAttribute }}
</div>

Test:

<?php

use App\Http\Livewire\DateFormat;
use App\Models\DateFormattedModel;

it('can serialize a date', function () {
    $model = DateFormattedModel::factory()->create();

    $response = $this->livewire(DateFormat::class, ['model' => $model]) // does not fail here
        ->call('doNothing'); // fails here

    $response
        ->assertStatus(200);
});

trip-somers avatar Jul 22 '22 05:07 trip-somers

I have narrowed this further to an interaction with the model factory.

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class DateFormattedModelFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            // 'dateAttribute' => $this->faker->dateTimeBetween('2022-07-03', '2022-07-05'), // fails
            // 'dateAttribute' => \Carbon\Carbon::parse('2022-07-04'), // fails
            'dateAttribute' => '2022-07-04', // succeeds
        ];
    }
}

trip-somers avatar Jul 22 '22 05:07 trip-somers

Hi, please could you provide a small reproducible repository that we can use to test this? 🙂

Regarding the reason it shows $subject is the issue, it's because that's the 2nd parameter to the preg_match() function signature. 👍🏻

owenvoke avatar Jul 22 '22 07:07 owenvoke

Maybe if I get to the end of a sprint with a whole bunch of spare time, sure.

trip-somers avatar Jul 22 '22 14:07 trip-somers

Just copied your code examples verbatim into a clean Laravel app, and was unable to reproduce.

Screenshot 2022-07-22 at 15 21 21

https://github.com/owenvoke/pest-544-repro


No rush about the repro, I'll try and have a look into it if I get time. 👍🏻

owenvoke avatar Jul 22 '22 14:07 owenvoke

I will admit to trying my base case on an existing app instead of a fresh one, but the only other thing I can think of would be that the database is MongoDB and the models use the jenssegers/laravel-mongodb repo. I will mess around with it some more when I can.

For now, my work-around was to change the factory to use a string.

trip-somers avatar Jul 22 '22 14:07 trip-somers

Quick note to say that I have updated previous comments with MongoDB context.

trip-somers avatar Jul 22 '22 21:07 trip-somers

Thanks, I'll try and take a look soon using MongoDB. 👍🏻

Ok, I just pushed some changes to my repro repo, and can confirm that I can reproduce the issue you're getting. 👍🏻

I can exactly replicate this, for some reason (only using the MongoDB model), in render() the $this->model->dateAttribute is set to a \DateTime instance. Whereas in doNothing(), it's set to an empty array. For some reason, it seems to strip that after the page has been rendered. 🤔

owenvoke avatar Jul 23 '22 06:07 owenvoke

This is not a Pest issue.

nunomaduro avatar Nov 09 '22 20:11 nunomaduro