diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2e7dc862e12..7ad10418a8b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -36,6 +36,7 @@ This serves two purposes: - Added a Vite HMR support for the realtime compiler in https://github.com/hydephp/develop/pull/2016 - Added Vite facade in https://github.com/hydephp/develop/pull/2016 - Added a custom Blade-based heading renderer for Markdown conversions in https://github.com/hydephp/develop/pull/2047 +- The `publish:views` command is now interactive for Unix-like systems in https://github.com/hydephp/develop/pull/2062 ### Changed diff --git a/docs/digging-deeper/customization.md b/docs/digging-deeper/customization.md index 95965376d78..6bdaafe30ac 100644 --- a/docs/digging-deeper/customization.md +++ b/docs/digging-deeper/customization.md @@ -370,6 +370,8 @@ php hyde publish:views The files will then be available in the `resources/views/vendor/hyde` directory. +>info **Tip:** If you use Linux/macOS or Windows with WSL you will be able to interactively select individual files to publish. + ## Frontend Styles Hyde is designed to not only serve as a framework but a whole starter kit and comes with a Tailwind starter template diff --git a/packages/framework/src/Console/Commands/PublishViewsCommand.php b/packages/framework/src/Console/Commands/PublishViewsCommand.php index 46ef8d6c1be..c66aadecc84 100644 --- a/packages/framework/src/Console/Commands/PublishViewsCommand.php +++ b/packages/framework/src/Console/Commands/PublishViewsCommand.php @@ -4,9 +4,17 @@ namespace Hyde\Console\Commands; +use Closure; use Hyde\Console\Concerns\Command; -use Illuminate\Support\Facades\Artisan; - +use Hyde\Console\Helpers\ConsoleHelper; +use Hyde\Console\Helpers\InteractivePublishCommandHelper; +use Hyde\Console\Helpers\ViewPublishGroup; +use Illuminate\Support\Str; +use Laravel\Prompts\Key; +use Laravel\Prompts\MultiSelectPrompt; +use Laravel\Prompts\SelectPrompt; + +use function Laravel\Prompts\select; use function str_replace; use function sprintf; use function strstr; @@ -20,78 +28,131 @@ class PublishViewsCommand extends Command protected $signature = 'publish:views {category? : The category to publish}'; /** @var string */ - protected $description = 'Publish the hyde components for customization. Note that existing files will be overwritten'; - - /** @var array> */ - protected array $options = [ - 'layouts' => [ - 'name' => 'Blade Layouts', - 'description' => 'Shared layout views, such as the app layout, navigation menu, and Markdown page templates', - 'group' => 'hyde-layouts', - ], - 'components' => [ - 'name' => 'Blade Components', - 'description' => 'More or less self contained components, extracted for customizability and DRY code', - 'group' => 'hyde-components', - ], - 'page-404' => [ - 'name' => '404 Page', - 'description' => 'A beautiful 404 error page by the Laravel Collective', - 'group' => 'hyde-page-404', - ], - ]; + protected $description = 'Publish the Hyde components for customization. Note that existing files will be overwritten'; + + /** @var array */ + protected array $options; public function handle(): int { - $selected = (string) ($this->argument('category') ?? $this->promptForCategory()); + $this->options = static::mapToKeys([ + ViewPublishGroup::fromGroup('hyde-layouts', 'Blade Layouts', 'Shared layout views, such as the app layout, navigation menu, and Markdown page templates'), + ViewPublishGroup::fromGroup('hyde-components', 'Blade Components', 'More or less self contained components, extracted for customizability and DRY code'), + ]); - if ($selected === 'all' || $selected === '') { - foreach ($this->options as $key => $_ignored) { - $this->publishOption($key); - } - } else { - $this->publishOption($selected); + $selected = ($this->argument('category') ?? $this->promptForCategory()) ?: 'all'; + + if ($selected !== 'all' && (bool) $this->argument('category') === false && ConsoleHelper::canUseLaravelPrompts($this->input)) { + $this->infoComment(sprintf('Selected category [%s]', $selected)); } - return Command::SUCCESS; - } + if (! in_array($selected, $allowed = array_merge(['all'], array_keys($this->options)), true)) { + $this->error("Invalid selection: '$selected'"); + $this->infoComment('Allowed values are: ['.implode(', ', $allowed).']'); - protected function publishOption(string $selected): void - { - Artisan::call('vendor:publish', [ - '--tag' => $this->options[$selected]['group'] ?? $selected, - '--force' => true, - ], $this->output); + return Command::FAILURE; + } + + $files = $selected === 'all' + ? collect($this->options)->flatMap(fn (ViewPublishGroup $option): array => $option->publishableFilesMap())->all() + : $this->options[$selected]->publishableFilesMap(); + + $publisher = $this->publishSelectedFiles($files, $selected === 'all'); + + $this->infoComment($publisher->formatOutput($selected)); + + return Command::SUCCESS; } protected function promptForCategory(): string { - /** @var string $choice */ - $choice = $this->choice( - 'Which category do you want to publish?', - $this->formatPublishableChoices(), - 0 + SelectPrompt::fallbackUsing(function (SelectPrompt $prompt): string { + return $this->choice($prompt->label, $prompt->options, $prompt->default); + }); + + return $this->parseChoiceIntoKey( + select('Which category do you want to publish?', $this->formatPublishableChoices(), 0) ?: 'all' ); + } - $selection = $this->parseChoiceIntoKey($choice); + protected function formatPublishableChoices(): array + { + return collect($this->options) + ->map(fn (ViewPublishGroup $option, string $key): string => sprintf('%s: %s', $key, $option->description)) + ->prepend('Publish all categories listed below') + ->values() + ->all(); + } - $this->infoComment(sprintf("Selected category [%s]\n", $selection ?: 'all')); + protected function parseChoiceIntoKey(string $choice): string + { + return strstr(str_replace(['', ''], '', $choice), ':', true) ?: ''; + } - return $selection; + /** + * @param array $groups + * @return array + */ + protected static function mapToKeys(array $groups): array + { + return collect($groups)->mapWithKeys(function (ViewPublishGroup $group): array { + return [Str::after($group->group, 'hyde-') => $group]; + })->all(); } - protected function formatPublishableChoices(): array + /** @param array $files */ + protected function publishSelectedFiles(array $files, bool $isPublishingAll): InteractivePublishCommandHelper { - $keys = ['Publish all categories listed below']; - foreach ($this->options as $key => $option) { - $keys[] = "$key: {$option['description']}"; + $publisher = new InteractivePublishCommandHelper($files); + + if (! $isPublishingAll && ConsoleHelper::canUseLaravelPrompts($this->input)) { + $publisher->only($this->promptUserForWhichFilesToPublish($publisher->getFileChoices())); } - return $keys; + $publisher->publishFiles(); + + return $publisher; } - protected function parseChoiceIntoKey(string $choice): string + /** + * @param array $files + * @return array + */ + protected function promptUserForWhichFilesToPublish(array $files): array { - return strstr(str_replace(['', ''], '', $choice), ':', true) ?: ''; + $choices = array_merge(['all' => 'All files'], $files); + + $prompt = new MultiSelectPrompt('Select the files you want to publish', $choices, [], 10, 'required', hint: 'Navigate with arrow keys, space to select, enter to confirm.'); + + $prompt->on('key', static::supportTogglingAll($prompt)); + + return (array) $prompt->prompt(); + } + + protected static function supportTogglingAll(MultiSelectPrompt $prompt): Closure + { + return function (string $key) use ($prompt): void { + static $isToggled = false; + + if ($prompt->isHighlighted('all')) { + if ($key === Key::SPACE) { + $prompt->emit('key', Key::CTRL_A); + + if ($isToggled) { + // We need to emit CTRL+A twice to deselect all for some reason + $prompt->emit('key', Key::CTRL_A); + $isToggled = false; + } else { + $isToggled = true; + } + } elseif ($key === Key::ENTER) { + if (! $isToggled) { + $prompt->emit('key', Key::CTRL_A); + } + + $prompt->state = 'submit'; + } + } + }; } } diff --git a/packages/framework/src/Console/Helpers/ConsoleHelper.php b/packages/framework/src/Console/Helpers/ConsoleHelper.php new file mode 100644 index 00000000000..f59a8a09ab3 --- /dev/null +++ b/packages/framework/src/Console/Helpers/ConsoleHelper.php @@ -0,0 +1,43 @@ +isInteractive() && windows_os() === false && Prompt::shouldFallback() === false; + } +} diff --git a/packages/framework/src/Console/Helpers/InteractivePublishCommandHelper.php b/packages/framework/src/Console/Helpers/InteractivePublishCommandHelper.php new file mode 100644 index 00000000000..faf33bb20a7 --- /dev/null +++ b/packages/framework/src/Console/Helpers/InteractivePublishCommandHelper.php @@ -0,0 +1,87 @@ + Map of source files to target files */ + protected array $publishableFilesMap; + + protected readonly int $originalFileCount; + + /** @param array $publishableFilesMap */ + public function __construct(array $publishableFilesMap) + { + $this->publishableFilesMap = $publishableFilesMap; + $this->originalFileCount = count($publishableFilesMap); + } + + /** @return array */ + public function getFileChoices(): array + { + return Arr::mapWithKeys($this->publishableFilesMap, /** @return array */ function (string $target, string $source): array { + return [$source => $this->pathRelativeToDirectory($source, $this->getBaseDirectory())]; + }); + } + + /** + * Only publish the selected files. + * + * @param array $selectedFiles Array of selected file paths, matching the keys of the publishableFilesMap. + */ + public function only(array $selectedFiles): void + { + $this->publishableFilesMap = Arr::only($this->publishableFilesMap, $selectedFiles); + } + + /** Find the most specific common parent directory path for the files, trimming as much as possible whilst keeping specificity and uniqueness. */ + public function getBaseDirectory(): string + { + $partsMap = collect($this->publishableFilesMap)->map(function (string $file): array { + return explode('/', $file); + }); + + $commonParts = $partsMap->reduce(function (array $carry, array $parts): array { + return array_intersect($carry, $parts); + }, $partsMap->first()); + + return implode('/', $commonParts); + } + + public function publishFiles(): void + { + foreach ($this->publishableFilesMap as $source => $target) { + Filesystem::ensureDirectoryExists(dirname($target)); + Filesystem::copy($source, $target); + } + } + + public function formatOutput(string $group): string + { + $fileCount = count($this->publishableFilesMap); + $publishedOneFile = $fileCount === 1; + $publishedAllGroups = $group === 'all'; + $publishedAllFiles = $fileCount === $this->originalFileCount; + $selectedFilesModifier = $publishedAllFiles ? 'all' : 'selected'; + + return match (true) { + $publishedAllGroups => sprintf('Published all %d files to [%s]', $fileCount, $this->getBaseDirectory()), + $publishedOneFile => sprintf('Published selected file to [%s]', reset($this->publishableFilesMap)), + default => sprintf('Published %s [%s] files to [%s]', $selectedFilesModifier, Str::singular($group), $this->getBaseDirectory()) + }; + } + + protected function pathRelativeToDirectory(string $source, string $directory): string + { + return Str::after($source, basename($directory).'/'); + } +} diff --git a/packages/framework/src/Console/Helpers/ViewPublishGroup.php b/packages/framework/src/Console/Helpers/ViewPublishGroup.php new file mode 100644 index 00000000000..43d381d7916 --- /dev/null +++ b/packages/framework/src/Console/Helpers/ViewPublishGroup.php @@ -0,0 +1,88 @@ + The filenames relative to the source directory. */ + public readonly array $files; + + /** @var class-string<\Hyde\Foundation\Providers\ViewServiceProvider> */ + protected static string $provider = ViewServiceProvider::class; + + protected function __construct(string $group, string $source, string $target, array $files, ?string $name = null, ?string $description = null) + { + $this->group = $group; + $this->source = $source; + $this->target = $target; + $this->files = $files; + + $this->name = $name ?? Hyde::makeTitle($group); + $this->description = $description ?? "Publish the '$group' files for customization."; + } + + public static function fromGroup(string $group, ?string $name = null, ?string $description = null): static + { + [$source, $target] = static::keyedArrayToTuple(static::$provider::pathsToPublish(static::$provider, $group)); + [$source, $target] = [static::normalizePath($source), static::normalizePath($target)]; + + $files = static::findFiles($source); + + return new static($group, $source, $target, $files, $name, $description); + } + + /** @return array The source file paths mapped to their target file paths. */ + public function publishableFilesMap(): array + { + return collect($this->files)->mapWithKeys(fn (string $file): array => [ + path_join($this->source, $file) => path_join($this->target, $file), + ])->all(); + } + + /** + * @param array $array + * @return list + */ + protected static function keyedArrayToTuple(array $array): array + { + return [key($array), current($array)]; + } + + /** @return array */ + protected static function findFiles(string $source): array + { + return Filesystem::findFiles($source, recursive: true) + ->map(fn (string $file) => static::normalizePath($file)) + ->map(fn (string $file) => unslash(Str::after($file, $source))) + ->sort(fn (string $a, string $b): int => substr_count($a, '/') <=> substr_count($b, '/') ?: strcmp($a, $b)) + ->all(); + } + + protected static function normalizePath(string $path): string + { + return Hyde::pathToRelative( + Filesystem::exists($path) ? realpath($path) : $path + ); + } +} diff --git a/packages/framework/tests/Feature/Commands/PublishViewsCommandTest.php b/packages/framework/tests/Feature/Commands/PublishViewsCommandTest.php index 190823910b0..a57133db3bb 100644 --- a/packages/framework/tests/Feature/Commands/PublishViewsCommandTest.php +++ b/packages/framework/tests/Feature/Commands/PublishViewsCommandTest.php @@ -4,48 +4,258 @@ namespace Hyde\Framework\Testing\Feature\Commands; +use Hyde\Console\Commands\PublishViewsCommand; +use Hyde\Console\Helpers\ConsoleHelper; +use Hyde\Facades\Filesystem; use Hyde\Hyde; use Hyde\Testing\TestCase; +use Illuminate\Console\OutputStyle; use Illuminate\Support\Facades\File; +use Laravel\Prompts\Key; +use Laravel\Prompts\Prompt; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\BufferedOutput; /** * @covers \Hyde\Console\Commands\PublishViewsCommand + * @covers \Hyde\Console\Helpers\InteractivePublishCommandHelper + * + * @see \Hyde\Framework\Testing\Unit\InteractivePublishCommandHelperTest */ class PublishViewsCommandTest extends TestCase { public function testCommandPublishesViews() { - $path = str_replace('\\', '/', Hyde::pathToRelative(realpath(Hyde::vendorPath('resources/views/pages/404.blade.php')))); + $count = Filesystem::findFiles('vendor/hyde/framework/resources/views/components', '.blade.php', true)->count() + + Filesystem::findFiles('vendor/hyde/framework/resources/views/layouts', '.blade.php', true)->count(); + $this->artisan('publish:views') ->expectsQuestion('Which category do you want to publish?', 'all') - ->expectsOutputToContain("Copying file [$path] to [_pages/404.blade.php]") + ->doesntExpectOutputToContain('Selected category') + ->expectsOutput("Published all $count files to [resources/views/vendor/hyde]") ->assertExitCode(0); + // Assert all groups were published + $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde')); + $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde/layouts')); + $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde/components')); + + // Assert files were published $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); - if (is_dir(Hyde::path('resources/views/vendor/hyde'))) { - File::deleteDirectory(Hyde::path('resources/views/vendor/hyde')); - } + // Assert subdirectories were published with files + $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde/components/docs')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/components/docs/documentation-article.blade.php')); } - public function testCanSelectView() + public function testCanSelectGroupWithArgument() { - $path = str_replace('\\', '/', Hyde::pathToRelative(realpath(Hyde::vendorPath('resources/views/pages/404.blade.php')))); - $this->artisan('publish:views page-404') - ->expectsOutputToContain("Copying file [$path] to [_pages/404.blade.php]") + ConsoleHelper::disableLaravelPrompts(); + + $this->artisan('publish:views layouts') + ->expectsOutput('Published all [layout] files to [resources/views/vendor/hyde/layouts]') ->assertExitCode(0); - $this->assertFileExists(Hyde::path('_pages/404.blade.php')); + // Assert selected group was published + $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde')); + $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde/layouts')); - if (is_dir(Hyde::path('resources/views/vendor/hyde'))) { - File::deleteDirectory(Hyde::path('resources/views/vendor/hyde')); - } + // Assert files were published + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php')); + + // Assert not selected group was not published + $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); + } + + public function testCanSelectGroupWithQuestion() + { + ConsoleHelper::disableLaravelPrompts(); + + $this->artisan('publish:views') + ->expectsQuestion('Which category do you want to publish?', 'layouts: Shared layout views, such as the app layout, navigation menu, and Markdown page templates') + ->expectsOutput('Published all [layout] files to [resources/views/vendor/hyde/layouts]') + ->assertExitCode(0); + + // Assert selected group was published + $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde')); + $this->assertDirectoryExists(Hyde::path('resources/views/vendor/hyde/layouts')); + + // Assert files were published + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php')); + + // Assert not selected group was not published + $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); } public function testWithInvalidSuppliedTag() { $this->artisan('publish:views invalid') - ->expectsOutputToContain('No publishable resources for tag [invalid].') + ->expectsOutput("Invalid selection: 'invalid'") + ->expectsOutput('Allowed values are: [all, layouts, components]') + ->assertExitCode(1); + } + + public function testInteractiveSelectionOnWindowsSystemsSkipsInteractiveness() + { + ConsoleHelper::mockWindowsOs(true); + + $this->artisan('publish:views components') + ->expectsOutput('Published all [component] files to [resources/views/vendor/hyde/components]') ->assertExitCode(0); } + + public function testInteractiveSelectionOnUnixSystems() + { + if (windows_os()) { + $this->markTestSkipped('Test is not applicable on Windows systems.'); + } + + Prompt::fake([ + Key::DOWN, Key::SPACE, + Key::DOWN, Key::SPACE, + Key::DOWN, Key::SPACE, + Key::ENTER, + ]); + + $output = $this->executePublishViewsCommand(); + + Prompt::assertOutputContains('Select the files you want to publish'); + Prompt::assertOutputContains('All files'); + Prompt::assertOutputContains('app.blade.php'); + Prompt::assertOutputContains('docs.blade.php'); + Prompt::assertOutputContains('footer.blade.php'); + + $this->assertSame("Published selected [layout] files to [resources/views/vendor/hyde/layouts]\n", $output->fetch()); + + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/docs.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/footer.blade.php')); + + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); + $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); + } + + public function testInteractiveSelectionWithHittingEnterRightAway() + { + if (windows_os()) { + $this->markTestSkipped('Test is not applicable on Windows systems.'); + } + + Prompt::fake([ + Key::ENTER, + ]); + + $output = $this->executePublishViewsCommand(); + + Prompt::assertOutputContains('Select the files you want to publish'); + Prompt::assertOutputContains('All files'); + Prompt::assertOutputContains('app.blade.php'); + Prompt::assertOutputContains('docs.blade.php'); + + $this->assertSame("Published all [layout] files to [resources/views/vendor/hyde/layouts]\n", $output->fetch()); + + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/docs.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/footer.blade.php')); + + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); + $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); + } + + public function testInteractiveSelectionWithComplexToggles() + { + if (windows_os()) { + $this->markTestSkipped('Test is not applicable on Windows systems.'); + } + + Prompt::fake([ + // Select "all files" + Key::SPACE, + // Unselect next file + Key::DOWN, Key::SPACE, + // Go back up and deselect the all files option + Key::UP, Key::SPACE, + // Select the next three files + Key::DOWN, Key::SPACE, Key::DOWN, Key::SPACE, Key::DOWN, Key::SPACE, + // De-select the last file + Key::SPACE, + // Confirm selection + Key::ENTER, + ]); + + $output = $this->executePublishViewsCommand(); + + Prompt::assertOutputContains('Select the files you want to publish'); + Prompt::assertOutputContains('All files'); + Prompt::assertOutputContains('app.blade.php'); + Prompt::assertOutputContains('docs.blade.php'); + + $this->assertSame("Published selected [layout] files to [resources/views/vendor/hyde/layouts]\n", $output->fetch()); + + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/docs.blade.php')); + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/layouts/footer.blade.php')); + + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); + $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); + } + + public function testCanSelectGroupWithQuestionAndPrompts() + { + if (windows_os()) { + $this->markTestSkipped('Test is not applicable on Windows systems.'); + } + + $this->artisan('publish:views') + ->expectsQuestion('Which category do you want to publish?', 'layouts: Shared layout views, such as the app layout, navigation menu, and Markdown page templates') + ->expectsOutput('Selected category [layouts]') + ->expectsQuestion('Select the files you want to publish', (is_dir(Hyde::path('packages')) ? 'packages' : 'vendor/hyde').'/framework/resources/views/layouts/app.blade.php') + ->expectsOutput('Published selected file to [resources/views/vendor/hyde/layouts/app.blade.php]') + ->assertExitCode(0); + + $this->assertFileExists(Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php')); + + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php')); + $this->assertDirectoryDoesNotExist(Hyde::path('resources/views/vendor/hyde/components')); + $this->assertFileDoesNotExist(Hyde::path('resources/views/vendor/hyde/components/article-excerpt.blade.php')); + } + + protected function executePublishViewsCommand(): BufferedOutput + { + $command = (new PublishViewsCommand()); + $input = new ArrayInput(['category' => 'layouts'], $command->getDefinition()); + $output = new BufferedOutput(); + $command->setInput($input); + $command->setOutput(new OutputStyle($input, $output)); + $command->handle(); + + return $output; + } + + protected function tearDown(): void + { + ConsoleHelper::clearMocks(); + PromptsReset::resetFallbacks(); + + if (File::isDirectory(Hyde::path('resources/views/vendor'))) { + File::deleteDirectory(Hyde::path('resources/views/vendor')); + } + + parent::tearDown(); + } +} + +abstract class PromptsReset extends Prompt +{ + // Workaround for https://github.com/laravel/prompts/issues/158 + public static function resetFallbacks(): void + { + static::$shouldFallback = false; + } } diff --git a/packages/framework/tests/Unit/Console/Helpers/ViewPublishGroupTest.php b/packages/framework/tests/Unit/Console/Helpers/ViewPublishGroupTest.php new file mode 100644 index 00000000000..fb7942d0fbd --- /dev/null +++ b/packages/framework/tests/Unit/Console/Helpers/ViewPublishGroupTest.php @@ -0,0 +1,129 @@ +bind(FileFinder::class, TestFileFinder::class); + } + + protected function tearDown(): void + { + TestViewPublishGroup::setProvider(ViewServiceProvider::class); + + app()->bind(FileFinder::class, FileFinder::class); + } + + public function testCanCreateGroup() + { + $group = ViewPublishGroup::fromGroup('layouts'); + + $this->assertInstanceOf(ViewPublishGroup::class, $group); + + $this->assertSame($group->group, 'layouts'); + $this->assertSame($group->name, 'Layouts'); + $this->assertSame($group->description, "Publish the 'layouts' files for customization."); + $this->assertSame($group->source, ViewPublishGroupTest::$packageDirectory.'/framework/resources/views/layouts'); + $this->assertSame($group->target, 'resources/views/vendor/hyde/layouts'); + $this->assertSame($group->files, ['app.blade.php', 'page.blade.php', 'post.blade.php']); + } + + public function testCanCreateGroupWithCustomName() + { + $group = ViewPublishGroup::fromGroup('layouts', 'Custom Layouts'); + + $this->assertSame($group->name, 'Custom Layouts'); + $this->assertSame($group->description, "Publish the 'layouts' files for customization."); + } + + public function testCanCreateGroupWithCustomDescription() + { + $group = ViewPublishGroup::fromGroup('layouts', null, 'Custom description'); + + $this->assertSame($group->name, 'Layouts'); + $this->assertSame($group->description, 'Custom description'); + } + + public function testCanCreateGroupWithCustomNameAndDescription() + { + $group = ViewPublishGroup::fromGroup('layouts', 'Custom Layouts', 'Custom description'); + + $this->assertSame($group->name, 'Custom Layouts'); + $this->assertSame($group->description, 'Custom description'); + } + + public function testCanGetPublishableFilesMap() + { + $group = ViewPublishGroup::fromGroup('layouts'); + + $this->assertSame($group->publishableFilesMap(), [ + ViewPublishGroupTest::$packageDirectory.'/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + ViewPublishGroupTest::$packageDirectory.'/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', + ViewPublishGroupTest::$packageDirectory.'/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', + ]); + } +} + +class TestViewPublishGroup extends ViewPublishGroup +{ + public static function setProvider(string $provider): void + { + parent::$provider = $provider; + } +} + +class TestViewServiceProvider extends ViewServiceProvider +{ + public static function pathsToPublish($provider = null, $group = null): array + { + ViewPublishGroupTest::assertSame($provider, TestViewServiceProvider::class); + ViewPublishGroupTest::assertSame($group, 'layouts'); + + return [ + Hyde::path(ViewPublishGroupTest::$packageDirectory.'/framework/src/Foundation/Providers/../../../resources/views/layouts') => Hyde::path('resources/views/vendor/hyde/layouts'), + ]; + } +} + +class TestFileFinder extends FileFinder +{ + public static function handle(string $directory, array|string|false $matchExtensions = false, bool $recursive = false): Collection + { + ViewPublishGroupTest::assertSame($directory, ViewPublishGroupTest::$packageDirectory.'/framework/resources/views/layouts'); + ViewPublishGroupTest::assertSame($matchExtensions, false); + ViewPublishGroupTest::assertSame($recursive, true); + + return collect([ + ViewPublishGroupTest::$packageDirectory.'/framework/resources/views/layouts/app.blade.php', + ViewPublishGroupTest::$packageDirectory.'/framework/resources/views/layouts/page.blade.php', + ViewPublishGroupTest::$packageDirectory.'/framework/resources/views/layouts/post.blade.php', + ]); + } +} diff --git a/packages/framework/tests/Unit/ConsoleHelperTest.php b/packages/framework/tests/Unit/ConsoleHelperTest.php new file mode 100644 index 00000000000..ff3307e1088 --- /dev/null +++ b/packages/framework/tests/Unit/ConsoleHelperTest.php @@ -0,0 +1,34 @@ +createMock(InputInterface::class); + $input->method('isInteractive')->willReturn(true); + + ConsoleHelper::mockWindowsOs(false); + + $this->assertTrue(ConsoleHelper::canUseLaravelPrompts($input)); + + ConsoleHelper::mockWindowsOs(true); + + $this->assertFalse(ConsoleHelper::canUseLaravelPrompts($input)); + } +} diff --git a/packages/framework/tests/Unit/InteractivePublishCommandHelperTest.php b/packages/framework/tests/Unit/InteractivePublishCommandHelperTest.php new file mode 100644 index 00000000000..8b669e08e94 --- /dev/null +++ b/packages/framework/tests/Unit/InteractivePublishCommandHelperTest.php @@ -0,0 +1,202 @@ +filesystem = $this->mockFilesystemStrict(); + + app()->instance(Filesystem::class, $this->filesystem); + } + + protected function tearDown(): void + { + $this->verifyMockeryExpectations(); + + app()->forgetInstance(Filesystem::class); + } + + public function testGetFileChoices(): void + { + $helper = new InteractivePublishCommandHelper([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', + 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', + ]); + + $this->assertSame([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'app.blade.php', + 'packages/framework/resources/views/layouts/page.blade.php' => 'page.blade.php', + 'packages/framework/resources/views/layouts/post.blade.php' => 'post.blade.php', + ], $helper->getFileChoices()); + } + + public function testOnlyFiltersPublishableFiles(): void + { + $helper = new InteractivePublishCommandHelper([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', + 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', + ]); + + $helper->only([ + 'packages/framework/resources/views/layouts/app.blade.php', + 'packages/framework/resources/views/layouts/post.blade.php', + ]); + + $this->assertSame([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'app.blade.php', + 'packages/framework/resources/views/layouts/post.blade.php' => 'post.blade.php', + ], $helper->getFileChoices()); + } + + public function testPublishFiles(): void + { + $this->filesystem->shouldReceive('ensureDirectoryExists')->times(3); + $this->filesystem->shouldReceive('copy')->times(3); + + $helper = new InteractivePublishCommandHelper([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', + 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', + ]); + + $helper->publishFiles(); + + $this->filesystem->shouldHaveReceived('ensureDirectoryExists')->with(Hyde::path('resources/views/vendor/hyde/layouts'))->times(3); + + $this->filesystem->shouldHaveReceived('copy')->with( + Hyde::path('packages/framework/resources/views/layouts/app.blade.php'), + Hyde::path('resources/views/vendor/hyde/layouts/app.blade.php') + )->once(); + + $this->filesystem->shouldHaveReceived('copy')->with( + Hyde::path('packages/framework/resources/views/layouts/page.blade.php'), + Hyde::path('resources/views/vendor/hyde/layouts/page.blade.php') + )->once(); + + $this->filesystem->shouldHaveReceived('copy')->with( + Hyde::path('packages/framework/resources/views/layouts/post.blade.php'), + Hyde::path('resources/views/vendor/hyde/layouts/post.blade.php') + )->once(); + } + + public function testFormatOutputForSingleFile(): void + { + $helper = new InteractivePublishCommandHelper([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + ]); + + $this->assertSame( + 'Published selected file to [resources/views/vendor/hyde/layouts/app.blade.php]', + $helper->formatOutput('layouts') + ); + } + + public function testFormatOutputForMultipleFiles(): void + { + $helper = new InteractivePublishCommandHelper([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', + ]); + + $this->assertSame( + 'Published all 2 files to [resources/views/vendor/hyde/layouts]', + $helper->formatOutput('all') + ); + } + + public function testFormatOutputForSingleChosenFile(): void + { + $helper = new InteractivePublishCommandHelper([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', + 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', + ]); + + $helper->only([ + 'packages/framework/resources/views/layouts/app.blade.php', + ]); + + $this->assertSame( + 'Published selected file to [resources/views/vendor/hyde/layouts/app.blade.php]', + $helper->formatOutput('layouts') + ); + } + + public function testFormatOutputForMultipleChosenFiles(): void + { + $helper = new InteractivePublishCommandHelper([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', + 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', + ]); + + $helper->only([ + 'packages/framework/resources/views/layouts/app.blade.php', + 'packages/framework/resources/views/layouts/page.blade.php', + ]); + + $this->assertSame( + 'Published selected [layout] files to [resources/views/vendor/hyde/layouts]', + $helper->formatOutput('layouts') + ); + } + + public function testGetBaseDirectoryWithOneSet(): void + { + $helper = new InteractivePublishCommandHelper([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + 'packages/framework/resources/views/layouts/page.blade.php' => 'resources/views/vendor/hyde/layouts/page.blade.php', + 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', + ]); + + $this->assertSame( + 'resources/views/vendor/hyde/layouts', + $helper->getBaseDirectory() + ); + } + + public function testGetBaseDirectoryWithMultipleSets(): void + { + $helper = new InteractivePublishCommandHelper([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + 'packages/framework/resources/views/layouts/post.blade.php' => 'resources/views/vendor/hyde/layouts/post.blade.php', + 'packages/framework/resources/views/components/page.blade.php' => 'resources/views/vendor/hyde/components/page.blade.php', + ]); + + $this->assertSame( + 'resources/views/vendor/hyde', + $helper->getBaseDirectory() + ); + } + + public function testGetBaseDirectoryWithSinglePath(): void + { + $helper = new InteractivePublishCommandHelper([ + 'packages/framework/resources/views/layouts/app.blade.php' => 'resources/views/vendor/hyde/layouts/app.blade.php', + ]); + + $this->assertSame( + 'resources/views/vendor/hyde/layouts/app.blade.php', + $helper->getBaseDirectory() + ); + } +} diff --git a/packages/hyde/tests/HydeCLITest.php b/packages/hyde/tests/HydeCLITest.php index b72eea310a2..7ba4502c7b4 100644 --- a/packages/hyde/tests/HydeCLITest.php +++ b/packages/hyde/tests/HydeCLITest.php @@ -9,7 +9,7 @@ class HydeCLITest extends TestCase public function testCanShowHydeConsole() { $this->artisan('list') - ->expectsOutputToContain('hyde') + ->expectsOutputToContain('Hyde') ->assertExitCode(0); } }