Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions src/Console/Command/Dev/InspectorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace OpenForgeProject\MageForge\Console\Command\Dev;

use Laravel\Prompts\SelectPrompt;
use Magento\Framework\App\Cache\Manager as CacheManager;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Config\Storage\WriterInterface;
Expand Down Expand Up @@ -51,12 +52,15 @@ protected function configure(): void
->setDescription('Manage MageForge Frontend Inspector (Actions: enable|disable|status)')
->addArgument(
self::ARGUMENT_ACTION,
InputArgument::REQUIRED,
'Action to perform: enable, disable, or status',
InputArgument::OPTIONAL,
'Action to perform: enable, disable, or status (interactive menu if omitted)',
)
->setHelp(<<<HELP
The <info>%command.name%</info> command manages the MageForge Frontend Inspector:

<info>php %command.full_name%</info>
Show an interactive menu to enable or disable the inspector

<info>php %command.full_name%</info> <comment>enable</comment>
Enable the inspector (requires developer mode)

Expand All @@ -83,7 +87,20 @@ protected function configure(): void
protected function executeCommand(InputInterface $input, OutputInterface $output): int
{
$arg = $input->getArgument(self::ARGUMENT_ACTION);
$action = strtolower(is_string($arg) ? $arg : '');
$action = strtolower(is_string($arg) ? trim($arg) : '');

// No action given: show interactive menu, fall back to status in non-interactive mode
if ($action === '') {
if (!$this->isInteractiveTerminal($output)) {
return $this->showStatus();
}
Comment thread
dermatz marked this conversation as resolved.

$selectedAction = $this->promptAction();
if ($selectedAction === null) {
return Cli::RETURN_FAILURE;
}
$action = $selectedAction;
}

// Validate action
if (!in_array($action, ['enable', 'disable', 'status'], true)) {
Expand All @@ -110,6 +127,41 @@ protected function executeCommand(InputInterface $input, OutputInterface $output
};
}

/**
* Prompt user to select an action via interactive menu
*
* @return string|null The selected action (enable, disable, status), or null if cancelled/failed
*/
protected function promptAction(): ?string
{
$currentStatus = $this->isInspectorEnabled() ? 'enabled' : 'disabled';

$prompt = new SelectPrompt(
label: sprintf('MageForge Inspector is currently %s – select an action', $currentStatus),
options: [
'enable' => 'Enable inspector',
'disable' => 'Disable inspector',
'status' => 'Show status',
],
default: $this->isInspectorEnabled() ? 'disable' : 'enable',
hint: 'Arrow keys to navigate, Enter to confirm',
);

// Set environment variables for Laravel Prompts (Docker/DDEV compatibility)
$this->setPromptEnvironment();

try {
$selection = $prompt->prompt();
return is_string($selection) ? $selection : null;
} catch (\Exception $e) {
$this->io->error('Selection failed: ' . $e->getMessage());
return null;
} finally {
\Laravel\Prompts\Prompt::terminal()->restoreTty();
$this->resetPromptEnvironment();
}
}

/**
* Enable inspector
*
Expand Down
80 changes: 80 additions & 0 deletions tests/Unit/Console/Command/Dev/InspectorCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use OpenForgeProject\MageForge\Model\Config\Inspector as InspectorConfig;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;

class InspectorCommandTest extends TestCase
Expand Down Expand Up @@ -229,4 +230,83 @@ public function testStatusUppercasesActionArgument(): void
$this->assertSame(Cli::RETURN_SUCCESS, $exitCode);
$this->assertStringContainsString('MageForge Inspector Status', $tester->getDisplay());
}

// -------------------------------------------------------------------------
// No-argument execution path (interactive menu / non-interactive fallback)
// -------------------------------------------------------------------------

public function testNoActionFallsBackToStatusInNonInteractiveMode(): void
{
$this->state->method('getMode')->willReturn(State::MODE_DEVELOPER);
$this->scopeConfig->method('isSetFlag')->willReturn(true);
$this->configWriter->expects($this->never())->method('save');

$tester = new CommandTester($this->command);
$exitCode = $tester->execute([]);

$this->assertSame(Cli::RETURN_SUCCESS, $exitCode);
$this->assertStringContainsString('MageForge Inspector Status', $tester->getDisplay());
}

public function testNoActionUsesSelectedActionFromInteractiveMenu(): void
{
$this->state->method('getMode')->willReturn(State::MODE_DEVELOPER);
$this->configWriter->expects($this->once())
->method('save')
->with(InspectorConfig::XML_PATH_ENABLED, '0');

$tester = new CommandTester($this->createInteractiveCommand('disable'));
$exitCode = $tester->execute([]);

$this->assertSame(Cli::RETURN_SUCCESS, $exitCode);
$this->assertStringContainsString('has been disabled', $tester->getDisplay());
}

public function testNoActionFailsWhenInteractiveMenuIsCancelled(): void
{
$this->configWriter->expects($this->never())->method('save');

$tester = new CommandTester($this->createInteractiveCommand(null));
$exitCode = $tester->execute([]);

$this->assertSame(Cli::RETURN_FAILURE, $exitCode);
}

/**
* Create a command double that always takes the interactive path and returns
* the given selection from the menu instead of rendering a real prompt
*
* @param string|null $selection
* @return InspectorCommand
*/
private function createInteractiveCommand(?string $selection): InspectorCommand
{
return new class(
$this->configWriter,
$this->state,
$this->cacheManager,
$this->scopeConfig,
$selection,
) extends InspectorCommand {
public function __construct(
WriterInterface $configWriter,
State $state,
CacheManager $cacheManager,
ScopeConfigInterface $scopeConfig,
private readonly ?string $selection,
) {
parent::__construct($configWriter, $state, $cacheManager, $scopeConfig);
}

protected function isInteractiveTerminal(OutputInterface $output): bool
{
return true;
}

protected function promptAction(): ?string
{
return $this->selection;
}
};
}
}
Loading