From 4450432d81e4efdaa53578ebc88624d811f7634b Mon Sep 17 00:00:00 2001 From: Willem Leuverink Date: Mon, 20 May 2024 23:48:09 +0200 Subject: [PATCH] added inbox component tests --- app/Livewire/Inbox.php | 1 + app/Models/Message.php | 3 + composer.json | 1 + composer.lock | 276 ++++++++++++++++-- database/factories/MessageFactory.php | 43 +++ .../factories/stubs/raw-message.blade.php | 39 +++ phpunit.xml | 4 +- tests/Feature/InboxTest.php | 146 ++++++++- tests/Pest.php | 4 + 9 files changed, 475 insertions(+), 42 deletions(-) create mode 100644 database/factories/MessageFactory.php create mode 100644 database/factories/stubs/raw-message.blade.php diff --git a/app/Livewire/Inbox.php b/app/Livewire/Inbox.php index 8894615..12258af 100644 --- a/app/Livewire/Inbox.php +++ b/app/Livewire/Inbox.php @@ -108,6 +108,7 @@ public function restartServer(?int $port = null) // NativePHP's supervisor seems to be delayed slightly. // We'll invoke the serve command immediately and // use the scheduler as a restart mechanism. + // NOTE: Explore pcntl fork approach. Artisan::queue('smtp:serve'); } diff --git a/app/Models/Message.php b/app/Models/Message.php index 4608182..c1e36c4 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use ZBateson\MailMimeParser\Message as ParsedMessage; +use Illuminate\Database\Eloquent\Factories\HasFactory; use ZBateson\MailMimeParser\IMessage as ParsedMessageContract; /** @@ -13,6 +14,8 @@ */ class Message extends Model { + use HasFactory; + protected $fillable = [ 'bookmarked', 'content', diff --git a/composer.json b/composer.json index 81173a6..8385a88 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "nunomaduro/collision": "^8.0", "pestphp/pest": "^2.34", "pestphp/pest-plugin-laravel": "^2.3", + "pestphp/pest-plugin-livewire": "^2.1", "spatie/laravel-ignition": "^2.4", "squizlabs/php_codesniffer": "^3.9", "tightenco/duster": "^2.7", diff --git a/composer.lock b/composer.lock index 2e6fb99..78578f3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e46f92d3cdafa0ceaec4d79092171dfb", + "content-hash": "ea82bbf394e95ce37c15fa0ab455b8bc", "packages": [ { "name": "blade-ui-kit/blade-heroicons", @@ -3347,20 +3347,20 @@ }, { "name": "psr/http-factory", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, "type": "library", @@ -3384,7 +3384,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -3396,9 +3396,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-04-10T20:10:41+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", @@ -7614,6 +7614,70 @@ ], "time": "2024-02-20T07:24:02+00:00" }, + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, { "name": "composer/class-map-generator", "version": "1.1.1", @@ -8102,25 +8166,32 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.56.0", + "version": "v3.57.2", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "4429303e62a4ce583ddfe64ff5c34c76bcf74931" + "reference": "22f7f3145606df92b02fb1bd22c30abfce956d3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4429303e62a4ce583ddfe64ff5c34c76bcf74931", - "reference": "4429303e62a4ce583ddfe64ff5c34c76bcf74931", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/22f7f3145606df92b02fb1bd22c30abfce956d3c", + "reference": "22f7f3145606df92b02fb1bd22c30abfce956d3c", "shasum": "" }, "require": { + "clue/ndjson-react": "^1.0", "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.3", "ext-filter": "*", "ext-json": "*", "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.0", "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", "sebastian/diff": "^4.0 || ^5.0 || ^6.0", "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", @@ -8183,7 +8254,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.56.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.57.2" }, "funding": [ { @@ -8191,7 +8262,7 @@ "type": "github" } ], - "time": "2024-05-07T15:50:05+00:00" + "time": "2024-05-20T20:41:57+00:00" }, { "name": "gedachtegoed/workspace", @@ -8719,16 +8790,16 @@ }, { "name": "mockery/mockery", - "version": "1.6.11", + "version": "1.6.12", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "81a161d0b135df89951abd52296adf97deb0723d" + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/81a161d0b135df89951abd52296adf97deb0723d", - "reference": "81a161d0b135df89951abd52296adf97deb0723d", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", "shasum": "" }, "require": { @@ -8798,7 +8869,7 @@ "security": "https://github.com/mockery/mockery/security/advisories", "source": "https://github.com/mockery/mockery" }, - "time": "2024-03-21T18:34:15+00:00" + "time": "2024-05-16T03:13:13+00:00" }, { "name": "myclabs/deep-copy", @@ -9279,6 +9350,72 @@ ], "time": "2024-04-27T10:41:54+00:00" }, + { + "name": "pestphp/pest-plugin-livewire", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-livewire.git", + "reference": "e72a2f850f727dfdb6bfa6e2ee6ff478ccc93f97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-livewire/zipball/e72a2f850f727dfdb6bfa6e2ee6ff478ccc93f97", + "reference": "e72a2f850f727dfdb6bfa6e2ee6ff478ccc93f97", + "shasum": "" + }, + "require": { + "livewire/livewire": "^2.12.3|^3.0", + "pestphp/pest": "^2.9.1", + "php": "^8.1" + }, + "require-dev": { + "orchestra/testbench": "^8.5.10", + "pestphp/pest-dev-tools": "^2.12.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Livewire\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Livewire Plugin", + "keywords": [ + "framework", + "livewire", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-livewire/tree/v2.1.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2023-07-20T16:28:21+00:00" + }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -9551,16 +9688,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.67", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493" + "reference": "e524358f930e41a2b4cca1320e3b04fc26b39e0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493", - "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e524358f930e41a2b4cca1320e3b04fc26b39e0b", + "reference": "e524358f930e41a2b4cca1320e3b04fc26b39e0b", "shasum": "" }, "require": { @@ -9605,7 +9742,7 @@ "type": "github" } ], - "time": "2024-04-16T07:22:02+00:00" + "time": "2024-05-15T08:00:59+00:00" }, { "name": "phpunit/php-code-coverage", @@ -10029,6 +10166,85 @@ ], "time": "2024-04-05T04:39:01+00:00" }, + { + "name": "react/child-process", + "version": "v0.6.5", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/socket": "^1.8", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.5" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-09-16T13:41:56+00:00" + }, { "name": "sebastian/cli-parser", "version": "2.0.1", @@ -11254,16 +11470,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.2", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480" + "reference": "57e09801c2fbae2d257b8b75bebb3deeb7e9deb2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/aac1f6f347a5c5ac6bc98ad395007df00990f480", - "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/57e09801c2fbae2d257b8b75bebb3deeb7e9deb2", + "reference": "57e09801c2fbae2d257b8b75bebb3deeb7e9deb2", "shasum": "" }, "require": { @@ -11330,7 +11546,7 @@ "type": "open_collective" } ], - "time": "2024-04-23T20:25:34+00:00" + "time": "2024-05-20T08:11:32+00:00" }, { "name": "symfony/filesystem", diff --git a/database/factories/MessageFactory.php b/database/factories/MessageFactory.php new file mode 100644 index 0000000..e29a5b0 --- /dev/null +++ b/database/factories/MessageFactory.php @@ -0,0 +1,43 @@ + $this->rawMessage([ + 'title' => 'Hello World!', + ]), + 'bookmarked' => false, + 'read_at' => null, + ]; + } + + public function bookmarked(): Factory + { + return $this->state(fn () => [ + 'bookmarked' => true, + ]); + } + + public function content(array $variables): Factory + { + return $this->state(fn () => [ + 'content' => $this->rawMessage($variables), + ]); + } + + private function rawMessage(array $variables) + { + return Blade::render( + file_get_contents(database_path('factories/stubs/raw-message.blade.php')), + $variables, + deleteCachedView: true + ); + } +} diff --git a/database/factories/stubs/raw-message.blade.php b/database/factories/stubs/raw-message.blade.php new file mode 100644 index 0000000..a9500db --- /dev/null +++ b/database/factories/stubs/raw-message.blade.php @@ -0,0 +1,39 @@ +From: hello@leuver.ink +To: recipient@leuver.ink +Subject: {{ $title }} +Message-ID: <29ad0e2e374359979e2a1afdf6f06dc3@wijck.com> +MIME-Version: 1.0 +Date: Thu, 16 May 2024 09:20:21 +020 +0 +Content-Type: multipart/alternative; boundary=M_re20x6 + +--M_re20x6 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printabl +e + + + + + + +

Text Message body

= + + + + +--M_re20x6 +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + + + + + + +

HTML Message body

= + + + +--M_re20x6-- + diff --git a/phpunit.xml b/phpunit.xml index 506b9a3..61c031c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,8 +22,8 @@ - - + + diff --git a/tests/Feature/InboxTest.php b/tests/Feature/InboxTest.php index df4a4c2..cbea672 100644 --- a/tests/Feature/InboxTest.php +++ b/tests/Feature/InboxTest.php @@ -1,17 +1,143 @@ get(route('inbox')); $response->assertStatus(200); }); -it('displays inbox-zero message when inbox is empty'); -it('can display a message'); -it('marks message as read when selected'); -it('marks message as read when directly routed to'); -it('can delete a message'); -it('can select previous message'); -it('can select next message'); -it('can bookmark a message'); -it('can remove bookmark from message'); -it('listens to server restart event'); +it('listens to server restart event') + ->livewire(Inbox::class) + ->set('online', true) + ->dispatch('restart-server') + ->assertSet('online', false); + +it('displays inbox-zero message when inbox is empty') + ->livewire(Inbox::class) + ->assertSee('Inbox zero 🎉'); + +it('displays settings button when inbox is empty') + ->livewire(Inbox::class) + ->assertSee('settings') + ->assertSeeHtml("x-on:click=\"\$dispatch('open-settings-dialog')\""); + +it('can displays message in the sidebar', function () { + $message = Message::factory()->content([ + 'title' => 'Shown without explicitly selecting', + ])->create(); + + $this->livewire(Inbox::class) + ->assertSee('Shown without explicitly selecting') + ->assertSeeHtml("selectMessage({$message->id}"); + + expect($message->fresh()) + ->read_at->toBeNull(); +}); + +it('can display a message', function () { + $message = Message::factory()->content([ + 'title' => 'Shown when visited', + ])->create(); + + $this->livewire(Inbox::class, [$message->id]) + ->assertSee('Shown when visited'); +}); + +it('can select a message', function () { + $message = Message::factory()->create(); + + $this->livewire(Inbox::class) + ->call('selectMessage', $message->id) + ->assertSet('selectedMessageId', $message->id); +}); + +it('marks message as read when selected', function () { + $message = Message::factory()->create(); + + expect($message)->read_at->toBeNull(); + + $this->livewire(Inbox::class)->call('selectMessage', $message->id); + + expect($message)->fresh()->read_at->not->toBeNull(); +}); + +it('marks message as read when directly routed to', function () { + $message = Message::factory()->create(); + + expect($message)->read_at->toBeNull(); + + $this->livewire(Inbox::class, [$message->id]); + + expect($message)->fresh()->read_at->not->toBeNull(); +}); + +it('can delete a message', function () { + $message = Message::factory()->create(); + + $this->assertModelExists($message); + + $this->livewire(Inbox::class)->call('deleteMessage', $message->id); + + $this->assertModelMissing($message); +}); + +it('deselects message when deleted', function () { + $message = Message::factory()->create(); + + $this->livewire(Inbox::class, [$message->id]) + ->assertSet('selectedMessageId', $message->id) + ->call('deleteMessage', $message->id) + ->assertSet('selectedMessageId', null); +}); + +it('can select next message', function () { + $messageOne = Message::factory()->create(); + $messageTwo = Message::factory()->create(); + + dump($messageOne->id, $messageTwo->id); + + $this->livewire(Inbox::class, [$messageOne->id]) + ->assertSet('selectedMessageId', $messageOne->id) + ->call('selectNext') + ->assertSet('selectedMessageId', $messageTwo->id); +}); + +it('can select previous message', function () { + $messageOne = Message::factory()->create(); + $messageTwo = Message::factory()->create(); + + $this->livewire(Inbox::class, [$messageTwo->id]) + ->assertSet('selectedMessageId', $messageTwo->id) + ->call('selectPrevious') + ->assertSet('selectedMessageId', $messageOne->id); +}); + +it('can bookmark a message', function () { + $message = Message::factory()->create(); + + expect($message)->bookmarked->toBeFalse(); + + // Toggle bookmark on + $this->livewire(Inbox::class, [$message->id]) + ->call('toggleBookmark', $message->id); + + expect($message)->fresh()->bookmarked->toBeTrue(); + +}); + +it('can remove bookmark from message', function () { + $message = Message::factory()->bookmarked()->create(); + + expect($message)->bookmarked->toBeTrue(); + + // Toggle bookmark off + $this->livewire(Inbox::class, [$message->id]) + ->call('toggleBookmark', $message->id); + + expect($message)->fresh()->bookmarked->toBeFalse(); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 83fba1b..56156e6 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -35,6 +35,10 @@ return $this->toBe(1); }); +expect()->extend('toBeNull', function () { + return $this->toBe(null); +}); + /* |-------------------------------------------------------------------------- | Functions