From 1df3c34b7e24a0f7e57caaf38a46d68bcc9769b6 Mon Sep 17 00:00:00 2001 From: simonhamp Date: Sun, 17 Nov 2024 15:08:11 +0000 Subject: [PATCH 01/21] Update CHANGELOG --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af3daf40..9a866e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to `nativephp-laravel` will be documented in this file. +## 0.6.4 - 2024-11-17 + +### What's Changed + +* Fix some DB stuff by @simonhamp in https://github.com/NativePHP/laravel/pull/413 +* Add dedicated PHP ChildProcess endpoint by @gwleuverink in https://github.com/NativePHP/laravel/pull/414 + +**Full Changelog**: https://github.com/NativePHP/laravel/compare/0.6.3...0.6.4 + ## 0.6.3 - 2024-11-14 ### What's Changed From c29bd0d63b64ff87ac3f4859f0dea3d720558304 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Tue, 19 Nov 2024 16:26:24 +0100 Subject: [PATCH 02/21] Fix Settings facade DocBloc (#419) --- src/Facades/Settings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Facades/Settings.php b/src/Facades/Settings.php index d1126c5c..2fc2fdda 100644 --- a/src/Facades/Settings.php +++ b/src/Facades/Settings.php @@ -6,7 +6,7 @@ /** * @method static void set($key, $value) - * @method static void mixed($key, $default = null) + * @method static mixed get($key, $default = null) */ class Settings extends Facade { From 3e0c562e871a038a4ed4f6699251b6eda060fa67 Mon Sep 17 00:00:00 2001 From: A G Date: Wed, 20 Nov 2024 07:29:21 -0500 Subject: [PATCH 03/21] Fake test double for WindowManager::Class: (#422) - Usage of (relatively lenient) WindowManagerContract::class in Laravel container - Implement WindowManagerFake::class with several testing assertions and helper methods - Laravel-style facade Window::fake() method - Tests --- src/Contracts/WindowManager.php | 23 +++++ src/Facades/Window.php | 11 +- src/Fakes/WindowManagerFake.php | 94 +++++++++++++++++ src/NativeServiceProvider.php | 7 ++ src/Windows/WindowManager.php | 3 +- tests/Fakes/FakeWindowManagerTest.php | 142 ++++++++++++++++++++++++++ 6 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 src/Contracts/WindowManager.php create mode 100644 src/Fakes/WindowManagerFake.php create mode 100644 tests/Fakes/FakeWindowManagerTest.php diff --git a/src/Contracts/WindowManager.php b/src/Contracts/WindowManager.php new file mode 100644 index 00000000..e4bbe63e --- /dev/null +++ b/src/Contracts/WindowManager.php @@ -0,0 +1,23 @@ + + */ + public function all(): array; + + public function get(string $id): Window; +} diff --git a/src/Facades/Window.php b/src/Facades/Window.php index 5ac1e901..e7747552 100644 --- a/src/Facades/Window.php +++ b/src/Facades/Window.php @@ -3,6 +3,8 @@ namespace Native\Laravel\Facades; use Illuminate\Support\Facades\Facade; +use Native\Laravel\Contracts\WindowManager as WindowManagerContract; +use Native\Laravel\Fakes\WindowManagerFake; /** * @method static \Native\Laravel\Windows\PendingOpenWindow open(string $id = 'main') @@ -18,8 +20,15 @@ */ class Window extends Facade { + public static function fake() + { + return tap(new WindowManagerFake, function ($fake) { + static::swap($fake); + }); + } + protected static function getFacadeAccessor() { - return \Native\Laravel\Windows\WindowManager::class; + return WindowManagerContract::class; } } diff --git a/src/Fakes/WindowManagerFake.php b/src/Fakes/WindowManagerFake.php new file mode 100644 index 00000000..2f2a1cec --- /dev/null +++ b/src/Fakes/WindowManagerFake.php @@ -0,0 +1,94 @@ + $windows + */ + public function alwaysReturnWindows(array $windows): self + { + $this->forcedWindowReturnValues = $windows; + + return $this; + } + + public function open(string $id = 'main') + { + $this->opened[] = $id; + } + + public function close($id = null) + { + $this->closed[] = $id; + } + + public function hide($id = null) + { + $this->hidden[] = $id; + } + + public function current(): Window + { + $this->ensureForceReturnWindowsProvided(); + + return $this->forcedWindowReturnValues[array_rand($this->forcedWindowReturnValues)]; + } + + /** + * @return array + */ + public function all(): array + { + $this->ensureForceReturnWindowsProvided(); + + return $this->forcedWindowReturnValues; + } + + public function get(string $id): Window + { + $this->ensureForceReturnWindowsProvided(); + + $matchingWindows = array_filter($this->forcedWindowReturnValues, fn (Window $window) => $window->getId() === $id); + + PHPUnit::assertNotEmpty($matchingWindows); + PHPUnit::assertCount(1, $matchingWindows); + + return Arr::first($matchingWindows); + } + + public function assertOpened(string $id): void + { + PHPUnit::assertContains($id, $this->opened); + } + + public function assertClosed(?string $id): void + { + PHPUnit::assertContains($id, $this->closed); + } + + public function assertHidden(?string $id): void + { + PHPUnit::assertContains($id, $this->hidden); + } + + private function ensureForceReturnWindowsProvided(): void + { + PHPUnit::assertNotEmpty($this->forcedWindowReturnValues); + } +} diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index d9c28a1f..dc2e2582 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -3,6 +3,7 @@ namespace Native\Laravel; use Illuminate\Console\Application; +use Illuminate\Foundation\Application as Foundation; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; @@ -12,9 +13,11 @@ use Native\Laravel\Commands\MigrateCommand; use Native\Laravel\Commands\MinifyApplicationCommand; use Native\Laravel\Commands\SeedDatabaseCommand; +use Native\Laravel\Contracts\WindowManager as WindowManagerContract; use Native\Laravel\Events\EventWatcher; use Native\Laravel\Exceptions\Handler; use Native\Laravel\Logging\LogWatcher; +use Native\Laravel\Windows\WindowManager as WindowManagerImplementation; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -47,6 +50,10 @@ public function packageRegistered() return new MigrateCommand($app['migrator'], $app['events']); }); + $this->app->bind(WindowManagerContract::class, function (Foundation $app) { + return $app->make(WindowManagerImplementation::class); + }); + if (config('nativephp-internal.running')) { $this->app->singleton( \Illuminate\Contracts\Debug\ExceptionHandler::class, diff --git a/src/Windows/WindowManager.php b/src/Windows/WindowManager.php index 49574e61..64046869 100644 --- a/src/Windows/WindowManager.php +++ b/src/Windows/WindowManager.php @@ -4,8 +4,9 @@ use Native\Laravel\Client\Client; use Native\Laravel\Concerns\DetectsWindowId; +use Native\Laravel\Contracts\WindowManager as WindowManagerContract; -class WindowManager +class WindowManager implements WindowManagerContract { use DetectsWindowId; diff --git a/tests/Fakes/FakeWindowManagerTest.php b/tests/Fakes/FakeWindowManagerTest.php new file mode 100644 index 00000000..5f0fbb1d --- /dev/null +++ b/tests/Fakes/FakeWindowManagerTest.php @@ -0,0 +1,142 @@ +toBeInstanceOf(WindowManagerFake::class); +}); + +it('asserts that a window was opened', function () { + swap(WindowManagerContract::class, $fake = new WindowManagerFake); + + app(WindowManagerContract::class)->open('main'); + app(WindowManagerContract::class)->open('secondary'); + + $fake->assertOpened('main'); + $fake->assertOpened('secondary'); + + try { + $fake->assertOpened('tertiary'); + } catch (AssertionFailedError) { + expect(true)->toBeTrue(); + + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was closed', function () { + swap(WindowManagerContract::class, $fake = new WindowManagerFake); + + app(WindowManagerContract::class)->close('main'); + app(WindowManagerContract::class)->close('secondary'); + + $fake->assertClosed('main'); + $fake->assertClosed('secondary'); + + try { + $fake->assertClosed('tertiary'); + } catch (AssertionFailedError) { + expect(true)->toBeTrue(); + + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was hidden', function () { + swap(WindowManagerContract::class, $fake = new WindowManagerFake); + + app(WindowManagerContract::class)->hide('main'); + app(WindowManagerContract::class)->hide('secondary'); + + $fake->assertHidden('main'); + $fake->assertHidden('secondary'); + + try { + $fake->assertHidden('tertiary'); + } catch (AssertionFailedError) { + expect(true)->toBeTrue(); + + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('forces the return value of current window', function () { + swap(WindowManagerContract::class, $fake = new WindowManagerFake); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + new WindowClass('testB'), + ]); + + expect($windows)->toContain(app(WindowManagerContract::class)->current()); +}); + +it('forces the return value of all windows', function () { + swap(WindowManagerContract::class, $fake = new WindowManagerFake); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + new WindowClass('testB'), + ]); + + expect(app(WindowManagerContract::class)->all())->toBe($windows); +}); + +it('forces the return value of a specific window', function () { + swap(WindowManagerContract::class, $fake = new WindowManagerFake); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + new WindowClass('testB'), + ]); + + expect(app(WindowManagerContract::class)->get('testA'))->toBe($windows[0]); + expect(app(WindowManagerContract::class)->get('testB'))->toBe($windows[1]); +}); + +test('that the get method throws an exception if multiple matching window ids exist', function () { + swap(WindowManagerContract::class, $fake = new WindowManagerFake); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + new WindowClass('testA'), + ]); + + app(WindowManagerContract::class)->get('testA'); +})->throws(AssertionFailedError::class); + +test('that the get method throws an exception if no matching window id exists', function () { + swap(WindowManagerContract::class, $fake = new WindowManagerFake); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + ]); + + app(WindowManagerContract::class)->get('testB'); +})->throws(AssertionFailedError::class); + +test('that the current method throws an exception if no forced window return values are provided', function () { + swap(WindowManagerContract::class, $fake = new WindowManagerFake); + + app(WindowManagerContract::class)->current(); +})->throws(AssertionFailedError::class); + +test('that the all method throws an exception if no forced window return values are provided', function () { + swap(WindowManagerContract::class, $fake = new WindowManagerFake); + + app(WindowManagerContract::class)->all(); +})->throws(AssertionFailedError::class); From 3b3d8dc8e12a5150b428cc3b2879467d839e7648 Mon Sep 17 00:00:00 2001 From: simonhamp Date: Wed, 20 Nov 2024 12:29:41 +0000 Subject: [PATCH 04/21] Fix styling --- src/Fakes/WindowManagerFake.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Fakes/WindowManagerFake.php b/src/Fakes/WindowManagerFake.php index 2f2a1cec..c3cf7910 100644 --- a/src/Fakes/WindowManagerFake.php +++ b/src/Fakes/WindowManagerFake.php @@ -6,7 +6,6 @@ use Native\Laravel\Contracts\WindowManager as WindowManagerContract; use Native\Laravel\Windows\Window; use PHPUnit\Framework\Assert as PHPUnit; -use RuntimeException; class WindowManagerFake implements WindowManagerContract { From ff9283e990cd3a47c5b7433b9f758b3263b5922f Mon Sep 17 00:00:00 2001 From: A G Date: Fri, 22 Nov 2024 06:24:04 -0500 Subject: [PATCH 05/21] Child process test double: (#430) - ChildProcess::class now implements Contracts\ChildProcess::class interface - Facades\ChildProcess::fake() swaps implementations with a test double concrete - Implement new binding in NativeServiceProvider::class - Implement Contracts/ChildProcess::class methods, mimicing that of the implementation - Implement ChildProcessFake::class with assertion helpers - Test that ChildProcessFake::class assertions work --- src/ChildProcess.php | 3 +- src/Contracts/ChildProcess.php | 28 +++ src/Facades/ChildProcess.php | 14 +- src/Fakes/ChildProcessFake.php | 252 +++++++++++++++++++++++++++ src/NativeServiceProvider.php | 6 + tests/Fakes/FakeChildProcessTest.php | 219 +++++++++++++++++++++++ 6 files changed, 518 insertions(+), 4 deletions(-) create mode 100644 src/Contracts/ChildProcess.php create mode 100644 src/Fakes/ChildProcessFake.php create mode 100644 tests/Fakes/FakeChildProcessTest.php diff --git a/src/ChildProcess.php b/src/ChildProcess.php index 171210fb..37cff65c 100644 --- a/src/ChildProcess.php +++ b/src/ChildProcess.php @@ -3,8 +3,9 @@ namespace Native\Laravel; use Native\Laravel\Client\Client; +use Native\Laravel\Contracts\ChildProcess as ChildProcessContract; -class ChildProcess +class ChildProcess implements ChildProcessContract { public readonly int $pid; diff --git a/src/Contracts/ChildProcess.php b/src/Contracts/ChildProcess.php new file mode 100644 index 00000000..9859e3e6 --- /dev/null +++ b/src/Contracts/ChildProcess.php @@ -0,0 +1,28 @@ +make(ChildProcessFake::class), function ($fake) { + static::swap($fake); + }); + } + protected static function getFacadeAccessor() { - self::clearResolvedInstance(Implement::class); + self::clearResolvedInstance(ChildProcessContract::class); - return Implement::class; + return ChildProcessContract::class; } } diff --git a/src/Fakes/ChildProcessFake.php b/src/Fakes/ChildProcessFake.php new file mode 100644 index 00000000..4e6add8b --- /dev/null +++ b/src/Fakes/ChildProcessFake.php @@ -0,0 +1,252 @@ + + */ + public array $gets = []; + + /** + * @var array + */ + public array $starts = []; + + /** + * @var array + */ + public array $phps = []; + + /** + * @var array + */ + public array $artisans = []; + + /** + * @var array + */ + public array $stops = []; + + /** + * @var array + */ + public array $restarts = []; + + /** + * @var array + */ + public array $messages = []; + + public function get(?string $alias = null): self + { + $this->gets[] = $alias; + + return $this; + } + + public function all(): array + { + return [$this]; + } + + public function start( + array|string $cmd, + string $alias, + ?string $cwd = null, + ?array $env = null, + bool $persistent = false + ): self { + $this->starts[] = [ + 'cmd' => $cmd, + 'alias' => $alias, + 'cwd' => $cwd, + 'env' => $env, + 'persistent' => $persistent, + ]; + + return $this; + } + + public function php( + array|string $cmd, + string $alias, + ?array $env = null, + ?bool $persistent = false + ): self { + $this->phps[] = [ + 'cmd' => $cmd, + 'alias' => $alias, + 'env' => $env, + 'persistent' => $persistent, + ]; + + return $this; + } + + public function artisan( + array|string $cmd, + string $alias, + ?array $env = null, + ?bool $persistent = false + ): self { + $this->artisans[] = [ + 'cmd' => $cmd, + 'alias' => $alias, + 'env' => $env, + 'persistent' => $persistent, + ]; + + return $this; + } + + public function stop(?string $alias = null): void + { + $this->stops[] = $alias; + } + + public function restart(?string $alias = null): self + { + $this->restarts[] = $alias; + + return $this; + } + + public function message(string $message, ?string $alias = null): self + { + $this->messages[] = [ + 'message' => $message, + 'alias' => $alias, + ]; + + return $this; + } + + /** + * @param string|Closure(string): bool $alias + */ + public function assertGet(string|Closure $alias): void + { + if (is_callable($alias) === false) { + PHPUnit::assertContains($alias, $this->gets); + + return; + } + + $hit = empty( + array_filter( + $this->gets, + fn (mixed $get) => $alias($get) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param Closure(array|string $cmd, string $alias, ?string $cwd, ?array $env, bool $persistent): bool $callback + */ + public function assertStarted(Closure $callback): void + { + $hit = empty( + array_filter( + $this->starts, + fn (array $started) => $callback(...$started) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param Closure(array|string $cmd, string $alias, ?array $env, ?bool $persistent): bool $callback + */ + public function assertPhp(Closure $callback): void + { + $hit = empty( + array_filter( + $this->phps, + fn (array $php) => $callback(...$php) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param Closure(array|string $cmd, string $alias, ?array $env, ?bool $persistent): bool $callback + */ + public function assertArtisan(Closure $callback): void + { + $hit = empty( + array_filter( + $this->artisans, + fn (array $artisan) => $callback(...$artisan) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $alias + */ + public function assertStop(string|Closure $alias): void + { + if (is_callable($alias) === false) { + PHPUnit::assertContains($alias, $this->stops); + + return; + } + + $hit = empty( + array_filter( + $this->stops, + fn (mixed $stop) => $alias($stop) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $alias + */ + public function assertRestart(string|Closure $alias): void + { + if (is_callable($alias) === false) { + PHPUnit::assertContains($alias, $this->restarts); + + return; + } + + $hit = empty( + array_filter( + $this->restarts, + fn (mixed $restart) => $alias($restart) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param Closure(string $message, string|null $alias): bool $callback + */ + public function assertMessage(Closure $callback): void + { + $hit = empty( + array_filter( + $this->messages, + fn (array $message) => $callback(...$message) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } +} diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index dc2e2582..a17d9534 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -7,12 +7,14 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; +use Native\Laravel\ChildProcess as ChildProcessImplementation; use Native\Laravel\Commands\FreshCommand; use Native\Laravel\Commands\LoadPHPConfigurationCommand; use Native\Laravel\Commands\LoadStartupConfigurationCommand; use Native\Laravel\Commands\MigrateCommand; use Native\Laravel\Commands\MinifyApplicationCommand; use Native\Laravel\Commands\SeedDatabaseCommand; +use Native\Laravel\Contracts\ChildProcess as ChildProcessContract; use Native\Laravel\Contracts\WindowManager as WindowManagerContract; use Native\Laravel\Events\EventWatcher; use Native\Laravel\Exceptions\Handler; @@ -54,6 +56,10 @@ public function packageRegistered() return $app->make(WindowManagerImplementation::class); }); + $this->app->bind(ChildProcessContract::class, function (Foundation $app) { + return $app->make(ChildProcessImplementation::class); + }); + if (config('nativephp-internal.running')) { $this->app->singleton( \Illuminate\Contracts\Debug\ExceptionHandler::class, diff --git a/tests/Fakes/FakeChildProcessTest.php b/tests/Fakes/FakeChildProcessTest.php new file mode 100644 index 00000000..8fece8a9 --- /dev/null +++ b/tests/Fakes/FakeChildProcessTest.php @@ -0,0 +1,219 @@ +toBeInstanceOf(ChildProcessFake::class); +}); + +it('asserts get using string', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->get('testA'); + $fake->get('testB'); + + $fake->assertGet('testA'); + $fake->assertGet('testB'); + + try { + $fake->assertGet('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts get using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->get('testA'); + $fake->get('testB'); + + $fake->assertGet(fn (string $alias) => $alias === 'testA'); + $fake->assertGet(fn (string $alias) => $alias === 'testB'); + + try { + $fake->assertGet(fn (string $alias) => $alias === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts started using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->start('cmdA', 'aliasA', 'cwdA', ['envA'], true); + $fake->start('cmdB', 'aliasB', 'cwdB', ['envB'], false); + + $fake->assertStarted(fn ($cmd, $alias, $cwd, $env, $persistent) => $alias === 'aliasA' && + $cmd === 'cmdA' && + $cwd === 'cwdA' && + $env === ['envA'] && + $persistent === true); + + $fake->assertStarted(fn ($cmd, $alias, $cwd, $env, $persistent) => $alias === 'aliasB' && + $cmd === 'cmdB' && + $cwd === 'cwdB' && + $env === ['envB'] && + $persistent === false); + + try { + $fake->assertStarted(fn ($cmd, $alias, $cwd, $env, $persistent) => $alias === 'aliasC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts php using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->php('cmdA', 'aliasA', ['envA'], true); + $fake->php('cmdB', 'aliasB', ['envB'], false); + + $fake->assertPhp(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasA' && + $cmd === 'cmdA' && + $env === ['envA'] && + $persistent === true); + + $fake->assertPhp(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasB' && + $cmd === 'cmdB' && + $env === ['envB'] && + $persistent === false); + + try { + $fake->assertPhp(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts artisan using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->artisan('cmdA', 'aliasA', ['envA'], true); + $fake->artisan('cmdB', 'aliasB', ['envB'], false); + + $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasA' && + $cmd === 'cmdA' && + $env === ['envA'] && + $persistent === true); + + $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasB' && + $cmd === 'cmdB' && + $env === ['envB'] && + $persistent === false); + + try { + $fake->assertArtisan(fn ($cmd, $alias, $env, $persistent) => $alias === 'aliasC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts stop using string', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->stop('testA'); + $fake->stop('testB'); + + $fake->assertStop('testA'); + $fake->assertStop('testB'); + + try { + $fake->assertStop('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts stop using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->stop('testA'); + $fake->stop('testB'); + + $fake->assertStop(fn (string $alias) => $alias === 'testA'); + $fake->assertStop(fn (string $alias) => $alias === 'testB'); + + try { + $fake->assertStop(fn (string $alias) => $alias === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts restart using string', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->restart('testA'); + $fake->restart('testB'); + + $fake->assertRestart('testA'); + $fake->assertRestart('testB'); + + try { + $fake->assertRestart('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts restart using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->restart('testA'); + $fake->restart('testB'); + + $fake->assertRestart(fn (string $alias) => $alias === 'testA'); + $fake->assertRestart(fn (string $alias) => $alias === 'testB'); + + try { + $fake->assertRestart(fn (string $alias) => $alias === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts message using callable', function () { + swap(ChildProcessContract::class, $fake = app(ChildProcessFake::class)); + + $fake->message('messageA', 'aliasA'); + $fake->message('messageB', 'aliasB'); + + $fake->assertMessage(fn (string $message, string $alias) => $message === 'messageA' && $alias === 'aliasA'); + $fake->assertMessage(fn (string $message, string $alias) => $message === 'messageB' && $alias === 'aliasB'); + + try { + $fake->assertMessage(fn (string $message, string $alias) => $message === 'messageC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + + From 3133d14af3f1f2695dcc3143d07b4824deaf2d4a Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Fri, 22 Nov 2024 12:24:24 +0100 Subject: [PATCH 06/21] fix: Notification facade docbloc (#428) Method returns `static` instead of `object` --- src/Facades/Notification.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Facades/Notification.php b/src/Facades/Notification.php index 4f9f56d3..425fea13 100644 --- a/src/Facades/Notification.php +++ b/src/Facades/Notification.php @@ -5,9 +5,9 @@ use Illuminate\Support\Facades\Facade; /** - * @method static object title(string $title) - * @method static object event(string $event) - * @method static object message(string $body) + * @method static static title(string $title) + * @method static static event(string $event) + * @method static static message(string $body) * @method static void show() */ class Notification extends Facade From a6dea1f2bb014ddc74befc44479d24a59a2aca70 Mon Sep 17 00:00:00 2001 From: A G Date: Fri, 22 Nov 2024 06:25:25 -0500 Subject: [PATCH 07/21] Improvements to window test doubles (#426) * Improvements to fake assertions: - Add support for optionally passing closures to methods that perform value assertions - Add count assertions - Enhancements to FakeWindowManagerTest::class * Improvements to window faking: - Resolve test environment error caused FakeWindowManager::open() not returning a Window/PendingOpenWindow object. This now mimics the real WindowManager::open() behavior - Remove usage of PHPUnit assertions in FakeWindowManager::class for internal assertions. Now only used for final assertions. This change fixes issue where PHPUnit was reporting 2 assertions per ::assertX() call on the test double --- src/Facades/Window.php | 2 +- src/Fakes/WindowManagerFake.php | 103 +++++++++++-- tests/Fakes/FakeWindowManagerTest.php | 209 +++++++++++++++++++++++--- 3 files changed, 287 insertions(+), 27 deletions(-) diff --git a/src/Facades/Window.php b/src/Facades/Window.php index e7747552..f73e2164 100644 --- a/src/Facades/Window.php +++ b/src/Facades/Window.php @@ -22,7 +22,7 @@ class Window extends Facade { public static function fake() { - return tap(new WindowManagerFake, function ($fake) { + return tap(static::getFacadeApplication()->make(WindowManagerFake::class), function ($fake) { static::swap($fake); }); } diff --git a/src/Fakes/WindowManagerFake.php b/src/Fakes/WindowManagerFake.php index c3cf7910..8604224f 100644 --- a/src/Fakes/WindowManagerFake.php +++ b/src/Fakes/WindowManagerFake.php @@ -2,10 +2,13 @@ namespace Native\Laravel\Fakes; +use Closure; use Illuminate\Support\Arr; +use Native\Laravel\Client\Client; use Native\Laravel\Contracts\WindowManager as WindowManagerContract; use Native\Laravel\Windows\Window; use PHPUnit\Framework\Assert as PHPUnit; +use Webmozart\Assert\Assert; class WindowManagerFake implements WindowManagerContract { @@ -17,6 +20,10 @@ class WindowManagerFake implements WindowManagerContract public array $forcedWindowReturnValues = []; + public function __construct( + protected Client $client + ) {} + /** * @param array $windows */ @@ -30,6 +37,21 @@ public function alwaysReturnWindows(array $windows): self public function open(string $id = 'main') { $this->opened[] = $id; + + $this->ensureForceReturnWindowsProvided(); + + $matchingWindows = array_filter( + $this->forcedWindowReturnValues, + fn (Window $window) => $window->getId() === $id + ); + + if (empty($matchingWindows)) { + return $this->forcedWindowReturnValues[array_rand($this->forcedWindowReturnValues)]->setClient($this->client); + } + + Assert::count($matchingWindows, 1); + + return Arr::first($matchingWindows)->setClient($this->client); } public function close($id = null) @@ -65,29 +87,92 @@ public function get(string $id): Window $matchingWindows = array_filter($this->forcedWindowReturnValues, fn (Window $window) => $window->getId() === $id); - PHPUnit::assertNotEmpty($matchingWindows); - PHPUnit::assertCount(1, $matchingWindows); + Assert::notEmpty($matchingWindows); + Assert::count($matchingWindows, 1); return Arr::first($matchingWindows); } - public function assertOpened(string $id): void + /** + * @param string|Closure(string): bool $id + */ + public function assertOpened(string|Closure $id): void + { + if (is_callable($id) === false) { + PHPUnit::assertContains($id, $this->opened); + + return; + } + + $hit = empty( + array_filter( + $this->opened, + fn (string $openedId) => $id($openedId) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $id + */ + public function assertClosed(string|Closure $id): void + { + if (is_callable($id) === false) { + PHPUnit::assertContains($id, $this->closed); + + return; + } + + $hit = empty( + array_filter( + $this->closed, + fn (mixed $closedId) => $id($closedId) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $id + */ + public function assertHidden(string|Closure $id): void + { + if (is_callable($id) === false) { + PHPUnit::assertContains($id, $this->hidden); + + return; + } + + $hit = empty( + array_filter( + $this->hidden, + fn (mixed $hiddenId) => $id($hiddenId) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + public function assertOpenedCount(int $expected): void { - PHPUnit::assertContains($id, $this->opened); + PHPUnit::assertCount($expected, $this->opened); } - public function assertClosed(?string $id): void + public function assertClosedCount(int $expected): void { - PHPUnit::assertContains($id, $this->closed); + PHPUnit::assertCount($expected, $this->closed); } - public function assertHidden(?string $id): void + public function assertHiddenCount(int $expected): void { - PHPUnit::assertContains($id, $this->hidden); + PHPUnit::assertCount($expected, $this->hidden); } private function ensureForceReturnWindowsProvided(): void { - PHPUnit::assertNotEmpty($this->forcedWindowReturnValues); + Assert::notEmpty($this->forcedWindowReturnValues, 'No windows were provided to return'); } } diff --git a/tests/Fakes/FakeWindowManagerTest.php b/tests/Fakes/FakeWindowManagerTest.php index 5f0fbb1d..b2f4af25 100644 --- a/tests/Fakes/FakeWindowManagerTest.php +++ b/tests/Fakes/FakeWindowManagerTest.php @@ -1,10 +1,13 @@ Http::response(status: 200)]); + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows([ + new PendingOpenWindow('doesnt-matter'), + ]); app(WindowManagerContract::class)->open('main'); app(WindowManagerContract::class)->open('secondary'); @@ -26,8 +34,30 @@ try { $fake->assertOpened('tertiary'); } catch (AssertionFailedError) { - expect(true)->toBeTrue(); + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was opened using callable', function () { + Http::fake(['*' => Http::response(status: 200)]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows([ + new PendingOpenWindow('doesnt-matter'), + ]); + + app(WindowManagerContract::class)->open('main'); + app(WindowManagerContract::class)->open('secondary'); + $fake->assertOpened(fn (string $id) => $id === 'main'); + $fake->assertOpened(fn (string $id) => $id === 'secondary'); + + try { + $fake->assertOpened(fn (string $id) => $id === 'tertiary'); + } catch (AssertionFailedError) { return; } @@ -35,7 +65,7 @@ }); it('asserts that a window was closed', function () { - swap(WindowManagerContract::class, $fake = new WindowManagerFake); + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); app(WindowManagerContract::class)->close('main'); app(WindowManagerContract::class)->close('secondary'); @@ -46,8 +76,24 @@ try { $fake->assertClosed('tertiary'); } catch (AssertionFailedError) { - expect(true)->toBeTrue(); + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was closed using callable', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + app(WindowManagerContract::class)->close('main'); + app(WindowManagerContract::class)->close('secondary'); + + $fake->assertClosed(fn (string $id) => $id === 'main'); + $fake->assertClosed(fn (string $id) => $id === 'secondary'); + + try { + $fake->assertClosed(fn (string $id) => $id === 'tertiary'); + } catch (AssertionFailedError) { return; } @@ -55,7 +101,7 @@ }); it('asserts that a window was hidden', function () { - swap(WindowManagerContract::class, $fake = new WindowManagerFake); + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); app(WindowManagerContract::class)->hide('main'); app(WindowManagerContract::class)->hide('secondary'); @@ -66,8 +112,84 @@ try { $fake->assertHidden('tertiary'); } catch (AssertionFailedError) { - expect(true)->toBeTrue(); + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts that a window was hidden using callable', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + app(WindowManagerContract::class)->hide('main'); + app(WindowManagerContract::class)->hide('secondary'); + + $fake->assertHidden(fn (string $id) => $id === 'main'); + $fake->assertHidden(fn (string $id) => $id === 'secondary'); + + try { + $fake->assertHidden(fn (string $id) => $id === 'tertiary'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts opened count', function () { + Http::fake(['*' => Http::response(status: 200)]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows([ + new PendingOpenWindow('doesnt-matter'), + ]); + + app(WindowManagerContract::class)->open('main'); + app(WindowManagerContract::class)->open(); + app(WindowManagerContract::class)->open(); + + $fake->assertOpenedCount(3); + + try { + $fake->assertOpenedCount(4); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts closed count', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->close('main'); + app(WindowManagerContract::class)->close(); + app(WindowManagerContract::class)->close(); + + $fake->assertClosedCount(3); + + try { + $fake->assertClosedCount(4); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts hidden count', function () { + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->hide('main'); + app(WindowManagerContract::class)->hide(); + app(WindowManagerContract::class)->hide(); + + $fake->assertHiddenCount(3); + + try { + $fake->assertHiddenCount(4); + } catch (AssertionFailedError) { return; } @@ -75,7 +197,7 @@ }); it('forces the return value of current window', function () { - swap(WindowManagerContract::class, $fake = new WindowManagerFake); + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); $fake->alwaysReturnWindows($windows = [ new WindowClass('testA'), @@ -86,7 +208,7 @@ }); it('forces the return value of all windows', function () { - swap(WindowManagerContract::class, $fake = new WindowManagerFake); + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); $fake->alwaysReturnWindows($windows = [ new WindowClass('testA'), @@ -97,7 +219,7 @@ }); it('forces the return value of a specific window', function () { - swap(WindowManagerContract::class, $fake = new WindowManagerFake); + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); $fake->alwaysReturnWindows($windows = [ new WindowClass('testA'), @@ -109,7 +231,7 @@ }); test('that the get method throws an exception if multiple matching window ids exist', function () { - swap(WindowManagerContract::class, $fake = new WindowManagerFake); + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); $fake->alwaysReturnWindows($windows = [ new WindowClass('testA'), @@ -117,26 +239,79 @@ ]); app(WindowManagerContract::class)->get('testA'); -})->throws(AssertionFailedError::class); +})->throws(InvalidArgumentException::class); test('that the get method throws an exception if no matching window id exists', function () { - swap(WindowManagerContract::class, $fake = new WindowManagerFake); + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); $fake->alwaysReturnWindows($windows = [ new WindowClass('testA'), ]); app(WindowManagerContract::class)->get('testB'); -})->throws(AssertionFailedError::class); +})->throws(InvalidArgumentException::class); test('that the current method throws an exception if no forced window return values are provided', function () { - swap(WindowManagerContract::class, $fake = new WindowManagerFake); + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); app(WindowManagerContract::class)->current(); -})->throws(AssertionFailedError::class); +})->throws(InvalidArgumentException::class); test('that the all method throws an exception if no forced window return values are provided', function () { - swap(WindowManagerContract::class, $fake = new WindowManagerFake); + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); app(WindowManagerContract::class)->all(); -})->throws(AssertionFailedError::class); +})->throws(InvalidArgumentException::class); + +test('that the open method throws an exception if no forced window return values are provided', function () { + Http::fake([ + '*' => Http::response(status: 200), + ]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + app(WindowManagerContract::class)->open('test'); +})->throws(InvalidArgumentException::class); + +test('that the open method throws an exception if multiple matching window ids exist', function () { + Http::fake([ + '*' => Http::response(status: 200), + ]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new WindowClass('testA'), + new WindowClass('testA'), + ]); + + app(WindowManagerContract::class)->open('testA'); +})->throws(InvalidArgumentException::class); + +test('that the open method returns a random window if none match the id provided', function () { + Http::fake([ + '*' => Http::response(status: 200), + ]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new PendingOpenWindow('testA'), + ]); + + expect($windows)->toContain(app(WindowManagerContract::class)->open('testC')); +}); + +test('that the open method returns a window if a matching window id exists', function () { + Http::fake([ + '*' => Http::response(status: 200), + ]); + + swap(WindowManagerContract::class, $fake = app(WindowManagerFake::class)); + + $fake->alwaysReturnWindows($windows = [ + new PendingOpenWindow('testA'), + ]); + + expect(app(WindowManagerContract::class)->open('testA'))->toBe($windows[0]); +}); From 7b9096955bd033bd57e1665d01372a4c65ac812f Mon Sep 17 00:00:00 2001 From: simonhamp Date: Fri, 22 Nov 2024 11:26:17 +0000 Subject: [PATCH 08/21] Fix styling --- tests/Fakes/FakeChildProcessTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Fakes/FakeChildProcessTest.php b/tests/Fakes/FakeChildProcessTest.php index 8fece8a9..57b0c354 100644 --- a/tests/Fakes/FakeChildProcessTest.php +++ b/tests/Fakes/FakeChildProcessTest.php @@ -215,5 +215,3 @@ $this->fail('Expected assertion to fail'); }); - - From 98405aee00259d2ee80251f5cd6c204ef7eb9042 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Sat, 23 Nov 2024 11:27:07 +0100 Subject: [PATCH 09/21] fix: child process cmd: option except iterable array (#429) * fix: child process cmd: option except iterable array TypeError: settings.cmd is not iterable at startPhpProcess * fix: throw an exception if $cmd is not an indexed array * fix: array_values but with a hint for static analyzer --- src/ChildProcess.php | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/ChildProcess.php b/src/ChildProcess.php index 37cff65c..634e3f4d 100644 --- a/src/ChildProcess.php +++ b/src/ChildProcess.php @@ -52,6 +52,10 @@ public function all(): array return $hydrated; } + /** + * @param string|string[] $cmd + * @return $this + */ public function start( string|array $cmd, string $alias, @@ -59,10 +63,11 @@ public function start( ?array $env = null, bool $persistent = false ): static { + $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; $process = $this->client->post('child-process/start', [ 'alias' => $alias, - 'cmd' => (array) $cmd, + 'cmd' => $cmd, 'cwd' => $cwd ?? base_path(), 'env' => $env, 'persistent' => $persistent, @@ -71,11 +76,17 @@ public function start( return $this->fromRuntimeProcess($process); } + /** + * @param string|string[] $cmd + * @return $this + */ public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self { + $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; + $process = $this->client->post('child-process/start-php', [ 'alias' => $alias, - 'cmd' => (array) $cmd, + 'cmd' => $cmd, 'cwd' => $cwd ?? base_path(), 'env' => $env, 'persistent' => $persistent, @@ -84,9 +95,15 @@ public function php(string|array $cmd, string $alias, ?array $env = null, ?bool return $this->fromRuntimeProcess($process); } + /** + * @param string|string[] $cmd + * @return $this + */ public function artisan(string|array $cmd, string $alias, ?array $env = null, ?bool $persistent = false): self { - $cmd = ['artisan', ...(array) $cmd]; + $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; + + $cmd = ['artisan', ...$cmd]; return $this->php($cmd, $alias, env: $env, persistent: $persistent); } From 41459c2a9df696741febf960cfd46f669f89f636 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Mon, 25 Nov 2024 14:56:20 +0100 Subject: [PATCH 10/21] feature: improve Settings (#432) --- src/Facades/Settings.php | 6 ++++-- src/Settings.php | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Facades/Settings.php b/src/Facades/Settings.php index 2fc2fdda..527c0f91 100644 --- a/src/Facades/Settings.php +++ b/src/Facades/Settings.php @@ -5,8 +5,10 @@ use Illuminate\Support\Facades\Facade; /** - * @method static void set($key, $value) - * @method static mixed get($key, $default = null) + * @method static void set(string $key, $value) + * @method static mixed get(string $key, $default = null) + * @method static void forget(string $key) + * @method static void clear() */ class Settings extends Facade { diff --git a/src/Settings.php b/src/Settings.php index 68e65b57..e849c729 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -8,15 +8,25 @@ class Settings { public function __construct(protected Client $client) {} - public function set($key, $value): void + public function set(string $key, $value): void { $this->client->post('settings/'.$key, [ 'value' => $value, ]); } - public function get($key, $default = null): mixed + public function get(string $key, $default = null): mixed { return $this->client->get('settings/'.$key)->json('value') ?? $default; } + + public function forget(string $key): void + { + $this->client->delete('settings/'.$key); + } + + public function clear(): void + { + $this->client->delete('settings/'); + } } From 79f8966d6d711844e1917877cd61989710c72526 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 26 Nov 2024 14:15:03 +0000 Subject: [PATCH 11/21] Dock goodies (#421) * Dock goodies * Fix styling * fix --------- Co-authored-by: simonhamp --- src/Dock.php | 34 ++++++++++++++++++++++++++++++++++ src/Facades/Dock.php | 6 ++++++ 2 files changed, 40 insertions(+) diff --git a/src/Dock.php b/src/Dock.php index dbd091dc..bdbb2988 100644 --- a/src/Dock.php +++ b/src/Dock.php @@ -17,4 +17,38 @@ public function menu(Menu $menu) 'items' => $items, ]); } + + public function show() + { + $this->client->post('dock/show'); + } + + public function hide() + { + $this->client->post('dock/hide'); + } + + public function icon(string $path) + { + $this->client->post('dock/icon', ['path' => $path]); + } + + public function bounce(string $type = 'informational') + { + $this->client->post('dock/bounce', ['type' => $type]); + } + + public function cancelBounce() + { + $this->client->post('dock/cancel-bounce'); + } + + public function badge(?string $label = null): void|string + { + if (is_null($label)) { + return $this->client->get('dock/badge'); + } + + $this->client->post('dock/badge', ['label' => $label]); + } } diff --git a/src/Facades/Dock.php b/src/Facades/Dock.php index f9386433..b088219d 100644 --- a/src/Facades/Dock.php +++ b/src/Facades/Dock.php @@ -6,7 +6,13 @@ use Native\Laravel\Menu\Menu; /** + * @method static void bounce() + * @method static void|string badge(string $type = null) + * @method static void cancelBounce() + * @method static void hide() + * @method static void icon(string $Path) * @method static void menu(Menu $menu) + * @method static void show() */ class Dock extends Facade { From 49dee31a662aa7208c4b51745471978fad21fd41 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 26 Nov 2024 14:16:16 +0000 Subject: [PATCH 12/21] MenuBars continued (#420) * Remove `event` prop * Standardise events * Fix styling * Fix class name --------- Co-authored-by: simonhamp --- src/Events/MenuBar/MenuBarClicked.php | 23 +++++++++++++++++++ src/Events/MenuBar/MenuBarDoubleClicked.php | 23 +++++++++++++++++++ ...MenuOpened.php => MenuBarRightClicked.php} | 4 +++- src/MenuBar/MenuBar.php | 10 -------- 4 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 src/Events/MenuBar/MenuBarClicked.php create mode 100644 src/Events/MenuBar/MenuBarDoubleClicked.php rename src/Events/MenuBar/{MenuBarContextMenuOpened.php => MenuBarRightClicked.php} (77%) diff --git a/src/Events/MenuBar/MenuBarClicked.php b/src/Events/MenuBar/MenuBarClicked.php new file mode 100644 index 00000000..4ac8f9e9 --- /dev/null +++ b/src/Events/MenuBar/MenuBarClicked.php @@ -0,0 +1,23 @@ +event = $event; - - return $this; - } - public function withContextMenu(Menu $menu): self { $this->contextMenu = $menu; @@ -131,7 +122,6 @@ public function toArray(): array 'onlyShowContextMenu' => $this->onlyShowContextMenu, 'contextMenu' => ! is_null($this->contextMenu) ? $this->contextMenu->toArray()['submenu'] : null, 'alwaysOnTop' => $this->alwaysOnTop, - 'event' => $this->event, ]; } } From 7f4994d0c08208ea2df297ad355232927e4b3516 Mon Sep 17 00:00:00 2001 From: A G Date: Sun, 1 Dec 2024 18:15:00 -0500 Subject: [PATCH 13/21] Global shortcut test double: (#436) - GlobalShortcut::class now implements Contracts\GlobalShortcut::class interface - Facades\GlobalShortcut::fake() swaps implementations with a test double concrete - Implement new binding in NativeServiceProvider::clas - Implement GlobalShortcutFake::class with assertion helpers - Test that GlobalShortcutFake::class assertions work --- src/Contracts/GlobalShortcut.php | 14 +++ src/Facades/GlobalShortcut.php | 11 ++- src/Fakes/GlobalShortcutFake.php | 100 ++++++++++++++++++++ src/GlobalShortcut.php | 3 +- src/NativeServiceProvider.php | 6 ++ tests/Fakes/FakeGlobalShortcutTest.php | 124 +++++++++++++++++++++++++ 6 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 src/Contracts/GlobalShortcut.php create mode 100644 src/Fakes/GlobalShortcutFake.php create mode 100644 tests/Fakes/FakeGlobalShortcutTest.php diff --git a/src/Contracts/GlobalShortcut.php b/src/Contracts/GlobalShortcut.php new file mode 100644 index 00000000..2fff35e8 --- /dev/null +++ b/src/Contracts/GlobalShortcut.php @@ -0,0 +1,14 @@ +make(GlobalShortcutFake::class), function ($fake) { + static::swap($fake); + }); + } + protected static function getFacadeAccessor() { - return \Native\Laravel\GlobalShortcut::class; + return GlobalShortcutContract::class; } } diff --git a/src/Fakes/GlobalShortcutFake.php b/src/Fakes/GlobalShortcutFake.php new file mode 100644 index 00000000..13efa1ee --- /dev/null +++ b/src/Fakes/GlobalShortcutFake.php @@ -0,0 +1,100 @@ + + */ + public array $keys = []; + + /** + * @var array + */ + public array $events = []; + + public int $registeredCount = 0; + + public int $unregisteredCount = 0; + + public function key(string $key): self + { + $this->keys[] = $key; + + return $this; + } + + public function event(string $event): self + { + $this->events[] = $event; + + return $this; + } + + public function register(): void + { + $this->registeredCount++; + } + + public function unregister(): void + { + $this->unregisteredCount++; + } + + /** + * @param string|Closure(string): bool $key + */ + public function assertKey(string|Closure $key): void + { + if (is_callable($key) === false) { + PHPUnit::assertContains($key, $this->keys); + + return; + } + + $hit = empty( + array_filter( + $this->keys, + fn (string $keyIteration) => $key($keyIteration) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + /** + * @param string|Closure(string): bool $event + */ + public function assertEvent(string|Closure $event): void + { + if (is_callable($event) === false) { + PHPUnit::assertContains($event, $this->events); + + return; + } + + $hit = empty( + array_filter( + $this->events, + fn (string $eventIteration) => $event($eventIteration) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + public function assertRegisteredCount(int $count): void + { + PHPUnit::assertSame($count, $this->registeredCount); + } + + public function assertUnregisteredCount(int $count): void + { + PHPUnit::assertSame($count, $this->unregisteredCount); + } +} diff --git a/src/GlobalShortcut.php b/src/GlobalShortcut.php index 991c3418..3859e6af 100644 --- a/src/GlobalShortcut.php +++ b/src/GlobalShortcut.php @@ -3,8 +3,9 @@ namespace Native\Laravel; use Native\Laravel\Client\Client; +use Native\Laravel\Contracts\GlobalShortcut as GlobalShortcutContract; -class GlobalShortcut +class GlobalShortcut implements GlobalShortcutContract { protected string $key; diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index a17d9534..97017662 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -15,9 +15,11 @@ use Native\Laravel\Commands\MinifyApplicationCommand; use Native\Laravel\Commands\SeedDatabaseCommand; use Native\Laravel\Contracts\ChildProcess as ChildProcessContract; +use Native\Laravel\Contracts\GlobalShortcut as GlobalShortcutContract; use Native\Laravel\Contracts\WindowManager as WindowManagerContract; use Native\Laravel\Events\EventWatcher; use Native\Laravel\Exceptions\Handler; +use Native\Laravel\GlobalShortcut as GlobalShortcutImplementation; use Native\Laravel\Logging\LogWatcher; use Native\Laravel\Windows\WindowManager as WindowManagerImplementation; use Spatie\LaravelPackageTools\Package; @@ -60,6 +62,10 @@ public function packageRegistered() return $app->make(ChildProcessImplementation::class); }); + $this->app->bind(GlobalShortcutContract::class, function (Foundation $app) { + return $app->make(GlobalShortcutImplementation::class); + }); + if (config('nativephp-internal.running')) { $this->app->singleton( \Illuminate\Contracts\Debug\ExceptionHandler::class, diff --git a/tests/Fakes/FakeGlobalShortcutTest.php b/tests/Fakes/FakeGlobalShortcutTest.php new file mode 100644 index 00000000..07bba84a --- /dev/null +++ b/tests/Fakes/FakeGlobalShortcutTest.php @@ -0,0 +1,124 @@ +toBeInstanceOf(GlobalShortcutFake::class); +}); + +it('asserts key using string', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->key('testA'); + $fake->key('testB'); + + $fake->assertKey('testA'); + $fake->assertKey('testB'); + + try { + $fake->assertKey('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts key using callable', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->key('testA'); + $fake->key('testB'); + + $fake->assertKey(fn (string $key) => $key === 'testA'); + $fake->assertKey(fn (string $key) => $key === 'testB'); + + try { + $fake->assertKey(fn (string $key) => $key === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts event using string', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->event('testA'); + $fake->event('testB'); + + $fake->assertEvent('testA'); + $fake->assertEvent('testB'); + + try { + $fake->assertEvent('testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts event using callable', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->event('testA'); + $fake->event('testB'); + + $fake->assertEvent(fn (string $event) => $event === 'testA'); + $fake->assertEvent(fn (string $event) => $event === 'testB'); + + try { + $fake->assertEvent(fn (string $event) => $event === 'testC'); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts registered count', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->register(); + $fake->register(); + $fake->register(); + + $fake->assertRegisteredCount(3); + + try { + $fake->assertRegisteredCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts unregistered count', function () { + swap(GlobalShortcutContract::class, $fake = app(GlobalShortcutFake::class)); + + $fake->unregister(); + $fake->unregister(); + $fake->unregister(); + + $fake->assertUnregisteredCount(3); + + try { + $fake->assertUnregisteredCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + From a30505f842e0a906d1d8e059071698791c999ca7 Mon Sep 17 00:00:00 2001 From: simonhamp Date: Sun, 1 Dec 2024 23:15:21 +0000 Subject: [PATCH 14/21] Fix styling --- tests/Fakes/FakeGlobalShortcutTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Fakes/FakeGlobalShortcutTest.php b/tests/Fakes/FakeGlobalShortcutTest.php index 07bba84a..0847cb97 100644 --- a/tests/Fakes/FakeGlobalShortcutTest.php +++ b/tests/Fakes/FakeGlobalShortcutTest.php @@ -1,9 +1,8 @@ fail('Expected assertion to fail'); }); - From 53c7fa2f882aaee55bb678ba656e5394bc28ff7e Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Sun, 1 Dec 2024 23:38:14 +0000 Subject: [PATCH 15/21] Menu improvements (#423) * Extract MenuBuilder * Add more Electron MenuItem roles * Allow MenuItems to have submenus * enabled/disabled * Add GoToUrl convenience item * Fix styling * Add help and hide roles * Receive combo key data * Add id's to menu items * Support radio items * Style * Remove custom event menu item type * Support custom event firing on all menu items * Fix label * Type hints and consistency * Fix styling * Get rid of the yucky GoTo* stuff Fold it all into Link instead * Fix test * Add hotkey alias method * Update docblock * Make Menu JsonSerializable * Fix styling --------- Co-authored-by: simonhamp --- src/Enums/RolesEnum.php | 14 ++- src/Events/Menu/MenuItemClicked.php | 2 +- src/Facades/Menu.php | 51 +++++++++ src/Menu/Items/Checkbox.php | 7 +- src/Menu/Items/Event.php | 17 --- src/Menu/Items/Link.php | 10 ++ src/Menu/Items/MenuItem.php | 50 ++++++++- src/Menu/Items/Radio.php | 7 +- src/Menu/Menu.php | 103 +++-------------- src/Menu/MenuBuilder.php | 165 ++++++++++++++++++++++++++++ tests/MenuBar/MenuBarTest.php | 7 +- 11 files changed, 316 insertions(+), 117 deletions(-) create mode 100644 src/Facades/Menu.php delete mode 100644 src/Menu/Items/Event.php create mode 100644 src/Menu/MenuBuilder.php diff --git a/src/Enums/RolesEnum.php b/src/Enums/RolesEnum.php index 337c4798..0039d72a 100644 --- a/src/Enums/RolesEnum.php +++ b/src/Enums/RolesEnum.php @@ -4,12 +4,24 @@ enum RolesEnum: string { - case APP_MENU = 'appMenu'; + case APP_MENU = 'appMenu'; // macOS case FILE_MENU = 'fileMenu'; case EDIT_MENU = 'editMenu'; case VIEW_MENU = 'viewMenu'; case WINDOW_MENU = 'windowMenu'; + case HELP = 'help'; // macOS + case UNDO = 'undo'; + case REDO = 'redo'; + case CUT = 'cut'; + case COPY = 'copy'; + case PASTE = 'paste'; + case PASTE_STYLE = 'pasteAndMatchStyle'; + case RELOAD = 'reload'; + case HIDE = 'hide'; // macOS + case MINIMIZE = 'minimize'; + case CLOSE = 'close'; case QUIT = 'quit'; case TOGGLE_FULL_SCREEN = 'togglefullscreen'; case TOGGLE_DEV_TOOLS = 'toggleDevTools'; + case ABOUT = 'about'; } diff --git a/src/Events/Menu/MenuItemClicked.php b/src/Events/Menu/MenuItemClicked.php index d9daa74f..961ed1c7 100644 --- a/src/Events/Menu/MenuItemClicked.php +++ b/src/Events/Menu/MenuItemClicked.php @@ -12,7 +12,7 @@ class MenuItemClicked implements ShouldBroadcastNow { use Dispatchable, InteractsWithSockets, SerializesModels; - public function __construct(public array $item) {} + public function __construct(public array $item, public array $combo = []) {} public function broadcastOn() { diff --git a/src/Facades/Menu.php b/src/Facades/Menu.php new file mode 100644 index 00000000..e12c2cb7 --- /dev/null +++ b/src/Facades/Menu.php @@ -0,0 +1,51 @@ +label = $label; } } diff --git a/src/Menu/Items/Event.php b/src/Menu/Items/Event.php deleted file mode 100644 index 02af55ee..00000000 --- a/src/Menu/Items/Event.php +++ /dev/null @@ -1,17 +0,0 @@ - 'event', - 'event' => $this->event, - 'label' => $this->label, - ]); - } -} diff --git a/src/Menu/Items/Link.php b/src/Menu/Items/Link.php index ec5a1cd7..36555747 100644 --- a/src/Menu/Items/Link.php +++ b/src/Menu/Items/Link.php @@ -6,12 +6,22 @@ class Link extends MenuItem { protected string $type = 'link'; + protected bool $openInBrowser = false; + public function __construct(protected string $url, protected ?string $label, protected ?string $accelerator = null) {} + public function openInBrowser(bool $openInBrowser = true): self + { + $this->openInBrowser = $openInBrowser; + + return $this; + } + public function toArray(): array { return array_merge(parent::toArray(), [ 'url' => $this->url, + 'openInBrowser' => $this->openInBrowser, ]); } } diff --git a/src/Menu/Items/MenuItem.php b/src/Menu/Items/MenuItem.php index 34d41ba0..7fe90517 100644 --- a/src/Menu/Items/MenuItem.php +++ b/src/Menu/Items/MenuItem.php @@ -3,11 +3,15 @@ namespace Native\Laravel\Menu\Items; use Native\Laravel\Contracts\MenuItem as MenuItemContract; +use Native\Laravel\Facades\Menu as MenuFacade; +use Native\Laravel\Menu\Menu; abstract class MenuItem implements MenuItemContract { protected string $type = 'normal'; + protected ?string $id = null; + protected ?string $label = null; protected ?string $sublabel = null; @@ -18,15 +22,33 @@ abstract class MenuItem implements MenuItemContract protected ?string $toolTip = null; + protected ?Menu $submenu = null; + protected bool $isEnabled = true; protected bool $isVisible = true; protected bool $isChecked = false; - public function enabled($enabled = true): self + protected ?string $event = null; + + public function enabled(): self + { + $this->isEnabled = true; + + return $this; + } + + public function disabled(): self + { + $this->isEnabled = false; + + return $this; + } + + public function id(string $id): self { - $this->isEnabled = $enabled; + $this->id = $id; return $this; } @@ -66,6 +88,11 @@ public function accelerator(string $accelerator): self return $this; } + public function hotkey(string $hotkey): self + { + return $this->accelerator($hotkey); + } + public function checked($checked = true): self { $this->isChecked = $checked; @@ -73,18 +100,34 @@ public function checked($checked = true): self return $this; } - public function toolTip(string $toolTip): self + public function tooltip(string $toolTip): self { $this->toolTip = $toolTip; return $this; } + public function submenu(MenuItemContract ...$items): self + { + $this->submenu = MenuFacade::make(...$items); + + return $this; + } + + public function event(string $event): self + { + $this->event = $event; + + return $this; + } + public function toArray(): array { return array_filter([ 'type' => $this->type, + 'id' => $this->id, 'label' => $this->label, + 'event' => $this->event, 'sublabel' => $this->sublabel, 'toolTip' => $this->toolTip, 'enabled' => $this->isEnabled, @@ -92,6 +135,7 @@ public function toArray(): array 'checked' => $this->isChecked, 'accelerator' => $this->accelerator, 'icon' => $this->icon, + 'submenu' => $this->submenu?->toArray(), ], fn ($value) => $value !== null); } } diff --git a/src/Menu/Items/Radio.php b/src/Menu/Items/Radio.php index 63587a2e..0751522a 100644 --- a/src/Menu/Items/Radio.php +++ b/src/Menu/Items/Radio.php @@ -6,8 +6,11 @@ class Radio extends MenuItem { protected string $type = 'radio'; - public function __construct(string $label) - { + public function __construct( + string $label, + protected bool $isChecked = false, + protected ?string $accelerator = null + ) { $this->label = $label; } } diff --git a/src/Menu/Menu.php b/src/Menu/Menu.php index 47a81b07..c97a30d7 100644 --- a/src/Menu/Menu.php +++ b/src/Menu/Menu.php @@ -3,31 +3,20 @@ namespace Native\Laravel\Menu; use Illuminate\Support\Traits\Conditionable; +use JsonSerializable; use Native\Laravel\Client\Client; use Native\Laravel\Contracts\MenuItem; -use Native\Laravel\Enums\RolesEnum; -use Native\Laravel\Menu\Items\Checkbox; -use Native\Laravel\Menu\Items\Event; -use Native\Laravel\Menu\Items\Label; -use Native\Laravel\Menu\Items\Link; -use Native\Laravel\Menu\Items\Role; -use Native\Laravel\Menu\Items\Separator; -class Menu implements MenuItem +class Menu implements JsonSerializable, MenuItem { use Conditionable; protected array $items = []; - protected string $prepend = ''; + protected string $label = ''; public function __construct(protected Client $client) {} - public static function new(): static - { - return new static(new Client); - } - public function register(): void { $items = $this->toArray()['submenu']; @@ -37,81 +26,11 @@ public function register(): void ]); } - public function prepend(string $prepend): self - { - $this->prepend = $prepend; - - return $this; - } - - public function submenu(string $header, Menu $submenu): static - { - return $this->add($submenu->prepend($header)); - } - - public function separator(): static - { - return $this->add(new Separator); - } - - public function quit(): static - { - return $this->add(new Role(RolesEnum::QUIT)); - } - public function label(string $label): self { - return $this->add(new Label($label)); - } - - public function checkbox(string $label, bool $checked = false, ?string $hotkey = null): self - { - return $this->add(new Checkbox($label, $checked, $hotkey)); - } - - public function event(string $event, string $text, ?string $hotkey = null): self - { - return $this->add(new Event($event, $text, $hotkey)); - } - - public function link(string $url, string $text, ?string $hotkey = null): self - { - return $this->add(new Link($url, $text, $hotkey)); - } + $this->label = $label; - public function appMenu(): static - { - return $this->add(new Role(RolesEnum::APP_MENU)); - } - - public function fileMenu($label = 'File'): static - { - return $this->add(new Role(RolesEnum::FILE_MENU, $label)); - } - - public function editMenu($label = 'Edit'): static - { - return $this->add(new Role(RolesEnum::EDIT_MENU, $label)); - } - - public function viewMenu($label = 'View'): static - { - return $this->add(new Role(RolesEnum::VIEW_MENU, $label)); - } - - public function windowMenu($label = 'Window'): static - { - return $this->add(new Role(RolesEnum::WINDOW_MENU, $label)); - } - - public function toggleFullscreen(): static - { - return $this->add(new Role(RolesEnum::TOGGLE_FULL_SCREEN)); - } - - public function toggleDevTools(): static - { - return $this->add(new Role(RolesEnum::TOGGLE_DEV_TOOLS)); + return $this; } public function add(MenuItem $item): self @@ -123,12 +42,18 @@ public function add(MenuItem $item): self public function toArray(): array { - $items = collect($this->items)->map(fn (MenuItem $item) => $item->toArray())->toArray(); - $label = $this->prepend; + $items = collect($this->items) + ->map(fn (MenuItem $item) => $item->toArray()) + ->toArray(); return [ - 'label' => $label, + 'label' => $this->label, 'submenu' => $items, ]; } + + public function jsonSerialize(): array + { + return $this->toArray(); + } } diff --git a/src/Menu/MenuBuilder.php b/src/Menu/MenuBuilder.php new file mode 100644 index 00000000..52ca3c95 --- /dev/null +++ b/src/Menu/MenuBuilder.php @@ -0,0 +1,165 @@ +client); + + foreach ($items as $item) { + $menu->add($item); + } + + return $menu; + } + + public function create(MenuItem ...$items): void + { + $this->make(...$items) + ->register(); + } + + public function default(): void + { + $this->create( + $this->app(), + $this->file(), + $this->edit(), + $this->view(), + $this->window(), + ); + } + + public function label(string $label): Items\Label + { + return new Items\Label($label); + } + + public function checkbox(string $label, bool $checked = false, ?string $hotkey = null): Items\Checkbox + { + return new Items\Checkbox($label, $checked, $hotkey); + } + + public function radio(string $label, bool $checked = false, ?string $hotkey = null): Items\Radio + { + return new Items\Radio($label, $checked, $hotkey); + } + + public function link(string $url, ?string $label = null, ?string $hotkey = null): Items\Link + { + return new Items\Link($url, $label, $hotkey); + } + + public function route(string $route, ?string $label = null, ?string $hotkey = null): Items\Link + { + return new Items\Link(route($route), $label, $hotkey); + } + + public function app(): Items\Role + { + return new Items\Role(RolesEnum::APP_MENU); + } + + public function file(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::FILE_MENU, $label); + } + + public function edit(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::EDIT_MENU, $label); + } + + public function view(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::VIEW_MENU, $label); + } + + public function window(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::WINDOW_MENU, $label); + } + + public function separator(): Items\Separator + { + return new Items\Separator; + } + + public function fullscreen(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::TOGGLE_FULL_SCREEN, $label); + } + + public function devTools(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::TOGGLE_DEV_TOOLS, $label); + } + + public function undo(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::UNDO, $label); + } + + public function redo(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::REDO, $label); + } + + public function cut(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::CUT, $label); + } + + public function copy(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::COPY, $label); + } + + public function paste(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::PASTE, $label); + } + + public function pasteAndMatchStyle(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::PASTE_STYLE, $label); + } + + public function reload(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::RELOAD, $label); + } + + public function minimize(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::MINIMIZE, $label); + } + + public function close(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::CLOSE, $label); + } + + public function quit(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::QUIT, $label); + } + + public function help(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::HELP, $label); + } + + public function hide(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::HIDE, $label); + } +} diff --git a/tests/MenuBar/MenuBarTest.php b/tests/MenuBar/MenuBarTest.php index ec1c022d..4b86aa95 100644 --- a/tests/MenuBar/MenuBarTest.php +++ b/tests/MenuBar/MenuBarTest.php @@ -1,7 +1,7 @@ set('nativephp-internal.api_url', 'https://jsonplaceholder.typicode.com/todos/1'); @@ -13,7 +13,10 @@ ->icon('nativephp.png') ->url('https://github.com/milwad-dev') ->withContextMenu( - Menu::new()->label('My Application')->quit(), + Menu::make( + Menu::label('My Application'), + Menu::quit(), + ), ); $menuBarArray = $menuBar->toArray(); From 3dae3bc891b6d59fb9493211ae489e7dc3476d65 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 3 Dec 2024 12:45:46 +0000 Subject: [PATCH 16/21] Update bug.yml --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index e84c04c8..262bbd9b 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -31,7 +31,7 @@ body: placeholder: When I do X I see Y. validations: required: true - - type: input + - type: markdown id: package-version attributes: label: Package Versions From 8bbef24d5889b789da220e6070acb4d750d68316 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Tue, 3 Dec 2024 12:49:34 +0000 Subject: [PATCH 17/21] Update bug.yml --- .github/ISSUE_TEMPLATE/bug.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 262bbd9b..f4547857 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,12 +1,20 @@ name: Bug Report description: | - Found a bug in NativePHP? Before submitting your report, please make sure you've been through the section "Debugging" in the docs: https://nativephp.com/docs/getting-started/debugging. + Found a bug in NativePHP? You're in the right place! labels: ["bug"] body: - type: markdown attributes: value: | - We're sorry to hear you have a problem. Please help us solve it by providing the following details. + We're sorry to hear you have a problem. + + Before submitting your report, please make sure you've been through the section "[Debugging](https://nativephp.com/docs/getting-started/debugging)" in the docs. + + If nothing here has helped you, please provide as much useful context as you can here to help us solve help you. + + Note that reams and reams of logs isn't helpful - please share only relevant errors. + + If possible, please prepare a reproduction repo and link to it in the Notes field. - type: textarea id: what-doing attributes: @@ -31,7 +39,7 @@ body: placeholder: When I do X I see Y. validations: required: true - - type: markdown + - type: textarea id: package-version attributes: label: Package Versions @@ -84,6 +92,6 @@ body: id: notes attributes: label: Notes - description: Use this field to provide any other notes that you feel might be relevant to the issue. + description: Use this field to provide any other notes that you feel might be relevant to the issue. Include links to any reproduction repos you've created here. validations: required: false From 5bb2e17507a6ab93832028f50c7302e9ee63892a Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Wed, 18 Dec 2024 19:21:54 +0100 Subject: [PATCH 18/21] fix: database migration on first launch (#439) --- src/NativeServiceProvider.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 97017662..89c07408 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -84,6 +84,13 @@ public function packageRegistered() } } + public function bootingPackage() + { + if (config('nativephp-internal.running')) { + $this->rewriteDatabase(); + } + } + protected function configureApp() { if (config('app.debug')) { @@ -94,8 +101,6 @@ protected function configureApp() $this->rewriteStoragePath(); - $this->rewriteDatabase(); - $this->configureDisks(); config(['session.driver' => 'file']); From c96a7c5121ba830c4d24b4f77f031dc5461f6223 Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Wed, 18 Dec 2024 19:25:08 +0100 Subject: [PATCH 19/21] Fixes and improvements to powerMonitor (#445) * fix: powerMonitor couldn't pass arguments with get methods * feat: additional events * fix: PowerMonitor Facade docblock * feat: added fake class for tests --- src/Client/Client.php | 4 +- src/Contracts/PowerMonitor.php | 17 +++ src/Events/PowerMonitor/ScreenLocked.php | 23 ++++ src/Events/PowerMonitor/ScreenUnlocked.php | 23 ++++ src/Events/PowerMonitor/Shutdown.php | 23 ++++ .../PowerMonitor/UserDidBecomeActive.php | 23 ++++ .../PowerMonitor/UserDidResignActive.php | 23 ++++ src/Facades/PowerMonitor.php | 15 ++- src/Fakes/PowerMonitorFake.php | 93 +++++++++++++ src/NativeServiceProvider.php | 6 + src/PowerMonitor.php | 3 +- tests/Fakes/FakePowerMonitorTest.php | 123 ++++++++++++++++++ 12 files changed, 370 insertions(+), 6 deletions(-) create mode 100644 src/Contracts/PowerMonitor.php create mode 100644 src/Events/PowerMonitor/ScreenLocked.php create mode 100644 src/Events/PowerMonitor/ScreenUnlocked.php create mode 100644 src/Events/PowerMonitor/Shutdown.php create mode 100644 src/Events/PowerMonitor/UserDidBecomeActive.php create mode 100644 src/Events/PowerMonitor/UserDidResignActive.php create mode 100644 src/Fakes/PowerMonitorFake.php create mode 100644 tests/Fakes/FakePowerMonitorTest.php diff --git a/src/Client/Client.php b/src/Client/Client.php index e444ea4a..9a8ec815 100644 --- a/src/Client/Client.php +++ b/src/Client/Client.php @@ -21,9 +21,9 @@ public function __construct() ->asJson(); } - public function get(string $endpoint): Response + public function get(string $endpoint, array|string|null $query = null): Response { - return $this->client->get($endpoint); + return $this->client->get($endpoint, $query); } public function post(string $endpoint, array $data = []): Response diff --git a/src/Contracts/PowerMonitor.php b/src/Contracts/PowerMonitor.php new file mode 100644 index 00000000..e8ec3d52 --- /dev/null +++ b/src/Contracts/PowerMonitor.php @@ -0,0 +1,17 @@ +make(PowerMonitorFake::class), function ($fake) { + static::swap($fake); + }); + } + + protected static function getFacadeAccessor(): string + { + return PowerMonitorContract::class; } } diff --git a/src/Fakes/PowerMonitorFake.php b/src/Fakes/PowerMonitorFake.php new file mode 100644 index 00000000..2af99167 --- /dev/null +++ b/src/Fakes/PowerMonitorFake.php @@ -0,0 +1,93 @@ +getSystemIdleStateCount++; + + $this->getSystemIdleStateCalls[] = $threshold; + + return SystemIdleStatesEnum::UNKNOWN; + } + + public function getSystemIdleTime(): int + { + $this->getSystemIdleTimeCount++; + + return 0; + } + + public function getCurrentThermalState(): ThermalStatesEnum + { + $this->getCurrentThermalStateCount++; + + return ThermalStatesEnum::UNKNOWN; + } + + public function isOnBatteryPower(): bool + { + $this->isOnBatteryPowerCount++; + + return false; + } + + /** + * @param int|Closure(int): bool $key + */ + public function assertGetSystemIdleState(int|Closure $key): void + { + if (is_callable($key) === false) { + PHPUnit::assertContains($key, $this->getSystemIdleStateCalls); + + return; + } + + $hit = empty( + array_filter( + $this->getSystemIdleStateCalls, + fn (string $keyIteration) => $key($keyIteration) === true + ) + ) === false; + + PHPUnit::assertTrue($hit); + } + + public function assertGetSystemIdleStateCount(int $count): void + { + PHPUnit::assertSame($count, $this->getSystemIdleStateCount); + } + + public function assertGetSystemIdleTimeCount(int $count): void + { + PHPUnit::assertSame($count, $this->getSystemIdleTimeCount); + } + + public function assertGetCurrentThermalStateCount(int $count): void + { + PHPUnit::assertSame($count, $this->getCurrentThermalStateCount); + } + + public function assertIsOnBatteryPowerCount(int $count): void + { + PHPUnit::assertSame($count, $this->isOnBatteryPowerCount); + } +} diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 89c07408..079bb0a6 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -16,11 +16,13 @@ use Native\Laravel\Commands\SeedDatabaseCommand; use Native\Laravel\Contracts\ChildProcess as ChildProcessContract; use Native\Laravel\Contracts\GlobalShortcut as GlobalShortcutContract; +use Native\Laravel\Contracts\PowerMonitor as PowerMonitorContract; use Native\Laravel\Contracts\WindowManager as WindowManagerContract; use Native\Laravel\Events\EventWatcher; use Native\Laravel\Exceptions\Handler; use Native\Laravel\GlobalShortcut as GlobalShortcutImplementation; use Native\Laravel\Logging\LogWatcher; +use Native\Laravel\PowerMonitor as PowerMonitorImplementation; use Native\Laravel\Windows\WindowManager as WindowManagerImplementation; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -66,6 +68,10 @@ public function packageRegistered() return $app->make(GlobalShortcutImplementation::class); }); + $this->app->bind(PowerMonitorContract::class, function (Foundation $app) { + return $app->make(PowerMonitorImplementation::class); + }); + if (config('nativephp-internal.running')) { $this->app->singleton( \Illuminate\Contracts\Debug\ExceptionHandler::class, diff --git a/src/PowerMonitor.php b/src/PowerMonitor.php index ea0d587c..c0307b11 100644 --- a/src/PowerMonitor.php +++ b/src/PowerMonitor.php @@ -3,10 +3,11 @@ namespace Native\Laravel; use Native\Laravel\Client\Client; +use Native\Laravel\Contracts\PowerMonitor as PowerMonitorContract; use Native\Laravel\Enums\SystemIdleStatesEnum; use Native\Laravel\Enums\ThermalStatesEnum; -class PowerMonitor +class PowerMonitor implements PowerMonitorContract { public function __construct(protected Client $client) {} diff --git a/tests/Fakes/FakePowerMonitorTest.php b/tests/Fakes/FakePowerMonitorTest.php new file mode 100644 index 00000000..4761877c --- /dev/null +++ b/tests/Fakes/FakePowerMonitorTest.php @@ -0,0 +1,123 @@ +toBeInstanceOf(PowerMonitorFake::class); +}); + +it('asserts getSystemIdleState using int', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->getSystemIdleState(10); + $fake->getSystemIdleState(60); + + $fake->assertGetSystemIdleState(10); + $fake->assertGetSystemIdleState(60); + + try { + $fake->assertGetSystemIdleState(20); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts getSystemIdleState using callable', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->getSystemIdleState(10); + $fake->getSystemIdleState(60); + + $fake->assertGetSystemIdleState(fn (int $key) => $key === 10); + $fake->assertGetSystemIdleState(fn (int $key) => $key === 60); + + try { + $fake->assertGetSystemIdleState(fn (int $key) => $key === 20); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts getSystemIdleState count', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->getSystemIdleState(10); + $fake->getSystemIdleState(20); + $fake->getSystemIdleState(60); + + $fake->assertGetSystemIdleStateCount(3); + + try { + $fake->assertGetSystemIdleStateCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts getSystemIdleTime count', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->getSystemIdleTime(); + $fake->getSystemIdleTime(); + $fake->getSystemIdleTime(); + + $fake->assertGetSystemIdleTimeCount(3); + + try { + $fake->assertGetSystemIdleTimeCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts getCurrentThermalState count', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->getCurrentThermalState(); + $fake->getCurrentThermalState(); + $fake->getCurrentThermalState(); + + $fake->assertGetCurrentThermalStateCount(3); + + try { + $fake->assertGetCurrentThermalStateCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); + +it('asserts isOnBatteryPower count', function () { + swap(PowerMonitorContract::class, $fake = app(PowerMonitorFake::class)); + + $fake->isOnBatteryPower(); + $fake->isOnBatteryPower(); + $fake->isOnBatteryPower(); + + $fake->assertIsOnBatteryPowerCount(3); + + try { + $fake->assertIsOnBatteryPowerCount(2); + } catch (AssertionFailedError) { + return; + } + + $this->fail('Expected assertion to fail'); +}); From 1a8151f9efddf339bcf70acf6c11ae53ae7b84df Mon Sep 17 00:00:00 2001 From: Eser DENIZ Date: Thu, 19 Dec 2024 15:44:09 +0100 Subject: [PATCH 20/21] feat: phpstan level 5 (#446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: phpstan level 5 * fix: $database undefined * fix: Unsafe usage of new static() * fix: ignore NativeAppServiceProvider not existing * fix: FreshCommand constructor invoked with 1 parameter, 0 required * fix: MenuBuilder facade function duplicates and arguments * fix: Type void cannot be part of a union type declaration. * fix: Php compactor missing imports and return statement * fix: missing SeedDatabaseCommand@handle return statement * Fix: cannot assign a value to a public readonly property outside of the constructor * Fix: PowerMonitor invoked Client::get() with 2 parameters, 1 required * fix: alternative for FreshCommand migrator argument * Revert "fix: alternative for FreshCommand migrator argument" This reverts commit cac9ea1442e5a8a4019e97aa58fdc39b9b3aa4c9. * Revert "fix: FreshCommand constructor invoked with 1 parameter, 0 required" This reverts commit cc1cb879145df52c11751f2370471a298f25b0a2. * fix: trying something * fix: phpstan.yml * Revert "fix: trying something" This reverts commit 6b88d133254bcb8881df7b4fc88a4aa5f4edc72a. * fix: trying to lower the minimum laravel 10 dependency * fix: final fix 🎉 * Revert "Fix: cannot assign a value to a public readonly property outside of the constructor" This reverts commit 585fb4727ced16a729f18a32a188a99a7b1cd1ea. * fix: put back previous fixes and ignore phpstan errors --- .github/workflows/phpstan.yml | 40 +++++++++++++++++++--------- .gitignore | 1 - composer.json | 10 ++++--- phpstan-baseline.neon | 0 phpstan.neon | 18 +++++++++++++ phpstan.neon.dist | 13 --------- src/ChildProcess.php | 15 ++++++----- src/Commands/SeedDatabaseCommand.php | 2 +- src/Compactor/Php.php | 5 ++-- src/Dialog.php | 2 +- src/Dock.php | 4 ++- src/Facades/Menu.php | 5 ++-- src/NativeServiceProvider.php | 33 +++++++++++++---------- src/Notification.php | 2 +- src/ProgressBar.php | 2 +- 15 files changed, 92 insertions(+), 60 deletions(-) delete mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon delete mode 100644 phpstan.neon.dist diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 3855a084..84219d82 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -1,26 +1,42 @@ name: PHPStan on: + workflow_dispatch: push: - paths: - - '**.php' - - 'phpstan.neon.dist' + branches-ignore: + - 'dependabot/npm_and_yarn/*' jobs: phpstan: - name: phpstan - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + php: [8.3] + steps: - - uses: actions/checkout@v4 + + - name: Checkout code + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' - coverage: none + php-version: ${{ matrix.php }} + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- - - name: Install composer dependencies - uses: ramsey/composer-install@v3 + - name: Install Dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - - name: Run PHPStan - run: ./vendor/bin/phpstan --error-format=github + - name: Run analysis + run: ./vendor/bin/phpstan analyse --error-format=github diff --git a/.gitignore b/.gitignore index e26945a7..3dd7896c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ composer.lock coverage docs phpunit.xml -phpstan.neon testbench.yaml vendor node_modules diff --git a/composer.json b/composer.json index c3950b1e..9f7b760f 100644 --- a/composer.json +++ b/composer.json @@ -38,9 +38,9 @@ }, "require-dev": { "guzzlehttp/guzzle": "^7.0", + "larastan/larastan": "^2.0|^3.0", "laravel/pint": "^1.0", "nunomaduro/collision": "^7.9", - "nunomaduro/larastan": "^2.0.1", "orchestra/testbench": "^8.0", "pestphp/pest": "^2.0", "pestphp/pest-plugin-arch": "^2.0", @@ -52,8 +52,7 @@ }, "autoload": { "psr-4": { - "Native\\Laravel\\": "src/", - "Native\\Laravel\\Database\\Factories\\": "database/factories/" + "Native\\Laravel\\": "src/" } }, "autoload-dev": { @@ -62,6 +61,11 @@ } }, "scripts": { + "qa" : [ + "@composer format", + "@composer analyse", + "@composer test" + ], "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", "analyse": "vendor/bin/phpstan analyse", "test": "vendor/bin/pest", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index e69de29b..00000000 diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..33be1e3a --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,18 @@ +parameters: + + paths: + - src/ + - config/ +# - tests/ + + + # Level 9 is the highest level + level: 5 + + ignoreErrors: + - '#Class App\\Providers\\NativeAppServiceProvider not found#' + - '#Class Native\\Laravel\\ChildProcess has an uninitialized readonly property#' + +# +# excludePaths: +# - ./*/*/FileToBeExcluded.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index 260b5e18..00000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,13 +0,0 @@ -includes: - - phpstan-baseline.neon - -parameters: - level: 4 - paths: - - src - - config - - database - tmpDir: build/phpstan - checkOctaneCompatibility: true - checkModelProperties: true - diff --git a/src/ChildProcess.php b/src/ChildProcess.php index 634e3f4d..f524370c 100644 --- a/src/ChildProcess.php +++ b/src/ChildProcess.php @@ -19,9 +19,9 @@ class ChildProcess implements ChildProcessContract public readonly bool $persistent; - public function __construct(protected Client $client) {} + final public function __construct(protected Client $client) {} - public function get(?string $alias = null): ?static + public function get(?string $alias = null): ?self { $alias = $alias ?? $this->alias; @@ -62,7 +62,7 @@ public function start( ?string $cwd = null, ?array $env = null, bool $persistent = false - ): static { + ): self { $cmd = is_array($cmd) ? array_values($cmd) : [$cmd]; $process = $this->client->post('child-process/start', [ @@ -87,7 +87,7 @@ public function php(string|array $cmd, string $alias, ?array $env = null, ?bool $process = $this->client->post('child-process/start-php', [ 'alias' => $alias, 'cmd' => $cmd, - 'cwd' => $cwd ?? base_path(), + 'cwd' => base_path(), 'env' => $env, 'persistent' => $persistent, ])->json(); @@ -115,7 +115,7 @@ public function stop(?string $alias = null): void ])->json(); } - public function restart(?string $alias = null): ?static + public function restart(?string $alias = null): ?self { $process = $this->client->post('child-process/restart', [ 'alias' => $alias ?? $this->alias, @@ -128,7 +128,7 @@ public function restart(?string $alias = null): ?static return $this->fromRuntimeProcess($process); } - public function message(string $message, ?string $alias = null): static + public function message(string $message, ?string $alias = null): self { $this->client->post('child-process/message', [ 'alias' => $alias ?? $this->alias, @@ -138,9 +138,10 @@ public function message(string $message, ?string $alias = null): static return $this; } - protected function fromRuntimeProcess($process): static + protected function fromRuntimeProcess($process) { if (isset($process['pid'])) { + // @phpstan-ignore-next-line $this->pid = $process['pid']; } diff --git a/src/Commands/SeedDatabaseCommand.php b/src/Commands/SeedDatabaseCommand.php index c83879e7..cdc032e0 100644 --- a/src/Commands/SeedDatabaseCommand.php +++ b/src/Commands/SeedDatabaseCommand.php @@ -15,6 +15,6 @@ public function handle() { (new NativeServiceProvider($this->laravel))->rewriteDatabase(); - parent::handle(); + return parent::handle(); } } diff --git a/src/Compactor/Php.php b/src/Compactor/Php.php index f4838bfd..7b5d3ad1 100644 --- a/src/Compactor/Php.php +++ b/src/Compactor/Php.php @@ -3,6 +3,8 @@ namespace Native\Laravel\Compactor; use PhpToken; +use RuntimeException; +use Webmozart\Assert\Assert; class Php { @@ -17,7 +19,7 @@ public function compact(string $file, string $contents): string return $this->compactContent($contents); } - $this->compactContent($contents); + return $this->compactContent($contents); } protected function compactContent(string $contents): string @@ -145,7 +147,6 @@ private function retokenizeAttribute(array &$tokens, int $opener): ?array { Assert::keyExists($tokens, $opener); - /** @var PhpToken $token */ $token = $tokens[$opener]; $attributeBody = mb_substr($token->text, 2); $subTokens = PhpToken::tokenize('client->post('dock/cancel-bounce'); } - public function badge(?string $label = null): void|string + public function badge(?string $label = null): ?string { if (is_null($label)) { return $this->client->get('dock/badge'); } $this->client->post('dock/badge', ['label' => $label]); + + return null; } } diff --git a/src/Facades/Menu.php b/src/Facades/Menu.php index e12c2cb7..332de247 100644 --- a/src/Facades/Menu.php +++ b/src/Facades/Menu.php @@ -3,6 +3,7 @@ namespace Native\Laravel\Facades; use Illuminate\Support\Facades\Facade; +use Native\Laravel\Contracts\MenuItem; use Native\Laravel\Menu\Items\Checkbox; use Native\Laravel\Menu\Items\Label; use Native\Laravel\Menu\Items\Link; @@ -11,7 +12,7 @@ use Native\Laravel\Menu\Items\Separator; /** - * @method static \Native\Laravel\Menu\Menu make(\Native\Laravel\Menu\Items\MenuItem ...$items) + * @method static \Native\Laravel\Menu\Menu make(MenuItem ...$items) * @method static Checkbox checkbox(string $label, bool $checked = false, ?string $hotkey = null) * @method static Label label(string $label) * @method static Link link(string $url, string $label = null, ?string $hotkey = null) @@ -23,7 +24,6 @@ * @method static Role view() * @method static Role window() * @method static Role help() - * @method static Role window() * @method static Role fullscreen() * @method static Role separator() * @method static Role devTools() @@ -37,7 +37,6 @@ * @method static Role minimize() * @method static Role close() * @method static Role quit() - * @method static Role help() * @method static Role hide() * @method static void create(MenuItem ...$items) * @method static void default() diff --git a/src/NativeServiceProvider.php b/src/NativeServiceProvider.php index 079bb0a6..22e39913 100644 --- a/src/NativeServiceProvider.php +++ b/src/NativeServiceProvider.php @@ -49,6 +49,7 @@ public function packageRegistered() $this->mergeConfigFrom($this->package->basePath('/../config/nativephp-internal.php'), 'nativephp-internal'); $this->app->singleton(FreshCommand::class, function ($app) { + /* @phpstan-ignore-next-line (beacause we support Laravel 10 & 11) */ return new FreshCommand($app['migrator']); }); @@ -148,13 +149,15 @@ public function rewriteDatabase() } } - config(['database.connections.nativephp' => [ - 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => $databasePath, - 'prefix' => '', - 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), - ]]); + config([ + 'database.connections.nativephp' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => $databasePath, + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + ]); config(['database.default' => 'nativephp']); @@ -174,7 +177,7 @@ public function removeDatabase() @unlink($databasePath); @unlink($databasePath.'-shm'); - @unlink($database.'-wal'); + @unlink($databasePath.'-wal'); } protected function configureDisks(): void @@ -197,12 +200,14 @@ protected function configureDisks(): void continue; } - config(['filesystems.disks.'.$disk => [ - 'driver' => 'local', - 'root' => env($env, ''), - 'throw' => false, - 'links' => 'skip', - ]]); + config([ + 'filesystems.disks.'.$disk => [ + 'driver' => 'local', + 'root' => env($env, ''), + 'throw' => false, + 'links' => 'skip', + ], + ]); } } } diff --git a/src/Notification.php b/src/Notification.php index 85c7a73a..82d13d82 100644 --- a/src/Notification.php +++ b/src/Notification.php @@ -12,7 +12,7 @@ class Notification protected string $event = ''; - public function __construct(protected Client $client) {} + final public function __construct(protected Client $client) {} public static function new() { diff --git a/src/ProgressBar.php b/src/ProgressBar.php index 74ca9d70..c9e318f8 100644 --- a/src/ProgressBar.php +++ b/src/ProgressBar.php @@ -16,7 +16,7 @@ class ProgressBar protected float $maxSecondsBetweenRedraws = 1; - public function __construct(protected int $maxSteps, protected Client $client) {} + final public function __construct(protected int $maxSteps, protected Client $client) {} public static function create(int $maxSteps): static { From 1cf8438a85bacf565b175d048fff5a3e8424ce4d Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Thu, 19 Dec 2024 14:49:06 +0000 Subject: [PATCH 21/21] Consistency --- src/Menu/Items/Checkbox.php | 6 ++---- src/Menu/Items/Label.php | 8 ++++---- src/Menu/Items/Link.php | 6 +++++- src/Menu/Items/Radio.php | 6 ++---- src/Menu/Items/Role.php | 5 ++++- src/Menu/MenuBuilder.php | 9 +++++++-- 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Menu/Items/Checkbox.php b/src/Menu/Items/Checkbox.php index afc550ee..baa44fc9 100644 --- a/src/Menu/Items/Checkbox.php +++ b/src/Menu/Items/Checkbox.php @@ -7,10 +7,8 @@ class Checkbox extends MenuItem protected string $type = 'checkbox'; public function __construct( - string $label, + protected ?string $label, protected bool $isChecked = false, protected ?string $accelerator = null - ) { - $this->label = $label; - } + ) {} } diff --git a/src/Menu/Items/Label.php b/src/Menu/Items/Label.php index 571c5ae5..cd03d091 100644 --- a/src/Menu/Items/Label.php +++ b/src/Menu/Items/Label.php @@ -4,8 +4,8 @@ class Label extends MenuItem { - public function __construct(string $label) - { - $this->label = $label; - } + public function __construct( + protected ?string $label, + protected ?string $accelerator = null + ) {} } diff --git a/src/Menu/Items/Link.php b/src/Menu/Items/Link.php index 36555747..25bc3c17 100644 --- a/src/Menu/Items/Link.php +++ b/src/Menu/Items/Link.php @@ -8,7 +8,11 @@ class Link extends MenuItem protected bool $openInBrowser = false; - public function __construct(protected string $url, protected ?string $label, protected ?string $accelerator = null) {} + public function __construct( + protected string $url, + protected ?string $label, + protected ?string $accelerator = null + ) {} public function openInBrowser(bool $openInBrowser = true): self { diff --git a/src/Menu/Items/Radio.php b/src/Menu/Items/Radio.php index 0751522a..759af459 100644 --- a/src/Menu/Items/Radio.php +++ b/src/Menu/Items/Radio.php @@ -7,10 +7,8 @@ class Radio extends MenuItem protected string $type = 'radio'; public function __construct( - string $label, + protected ?string $label, protected bool $isChecked = false, protected ?string $accelerator = null - ) { - $this->label = $label; - } + ) {} } diff --git a/src/Menu/Items/Role.php b/src/Menu/Items/Role.php index 3fa3b244..bde40a4a 100644 --- a/src/Menu/Items/Role.php +++ b/src/Menu/Items/Role.php @@ -8,7 +8,10 @@ class Role extends MenuItem { protected string $type = 'role'; - public function __construct(protected RolesEnum $role, protected ?string $label = '') {} + public function __construct( + protected RolesEnum $role, + protected ?string $label = '' + ) {} public function toArray(): array { diff --git a/src/Menu/MenuBuilder.php b/src/Menu/MenuBuilder.php index 52ca3c95..f0627f78 100644 --- a/src/Menu/MenuBuilder.php +++ b/src/Menu/MenuBuilder.php @@ -38,9 +38,9 @@ public function default(): void ); } - public function label(string $label): Items\Label + public function label(string $label, ?string $hotkey = null): Items\Label { - return new Items\Label($label); + return new Items\Label($label, $hotkey); } public function checkbox(string $label, bool $checked = false, ?string $hotkey = null): Items\Checkbox @@ -162,4 +162,9 @@ public function hide(?string $label = null): Items\Role { return new Items\Role(RolesEnum::HIDE, $label); } + + public function about(?string $label = null): Items\Role + { + return new Items\Role(RolesEnum::ABOUT, $label); + } }