|
| 1 | +<?php declare(strict_types=1); |
| 2 | + |
| 3 | +use Nette\CommandLine\{ColorDepth, Console, Prompt}; |
| 4 | +use Tester\Assert; |
| 5 | + |
| 6 | +require __DIR__ . '/bootstrap.php'; |
| 7 | + |
| 8 | + |
| 9 | +/** @return array{Prompt, resource} */ |
| 10 | +function prompt(string $answers, bool $terminal = true): array |
| 11 | +{ |
| 12 | + $input = fopen('php://memory', 'w+'); |
| 13 | + fwrite($input, $answers); |
| 14 | + rewind($input); |
| 15 | + $output = fopen('php://memory', 'w+'); |
| 16 | + return [new Prompt(new Console($output, ColorDepth::None, terminal: $terminal), $input, inputTerminal: $terminal), $output]; |
| 17 | +} |
| 18 | + |
| 19 | + |
| 20 | +/** @param resource $output */ |
| 21 | +function written($output): string |
| 22 | +{ |
| 23 | + rewind($output); |
| 24 | + return (string) stream_get_contents($output); |
| 25 | +} |
| 26 | + |
| 27 | + |
| 28 | +test('the answer is one of the choices, in any case, returned as declared', function () { |
| 29 | + [$prompt, $output] = prompt("Y\n"); |
| 30 | + Assert::same('y', $prompt->choose('Apply this fix?', ['y', 'n', 'a', 'q'])); |
| 31 | + Assert::same('Apply this fix? [y/n/a/q] ', written($output)); |
| 32 | + |
| 33 | + [$prompt, $output] = prompt("all\n"); |
| 34 | + Assert::same('All', $prompt->choose('Apply?', ['Yes', 'All', 'no'], default: 'no')); |
| 35 | + Assert::same('Apply? [yes/all/NO] ', written($output)); // only the default is in capitals |
| 36 | +}); |
| 37 | + |
| 38 | + |
| 39 | +test('an unknown answer asks again, an empty one is the default', function () { |
| 40 | + [$prompt, $output] = prompt("x\n\n"); |
| 41 | + Assert::same('r', $prompt->choose('Choice', ['s', 'r', 'c'], default: 'r')); |
| 42 | + Assert::same('Choice [s/R/c] Choice [s/R/c] ', written($output)); |
| 43 | + |
| 44 | + [$prompt, $output] = prompt("\nn\n"); // without a default an empty answer asks again |
| 45 | + Assert::same('n', $prompt->choose('Go on?', ['y', 'n'])); |
| 46 | + Assert::same('Go on? [y/n] Go on? [y/n] ', written($output)); |
| 47 | +}); |
| 48 | + |
| 49 | + |
| 50 | +test('the end of the input is null, never the default', function () { |
| 51 | + [$prompt, $output] = prompt(''); |
| 52 | + Assert::null($prompt->choose('Go on?', ['y', 'n'], default: 'y')); |
| 53 | + Assert::same("Go on? [Y/n] \n", written($output)); |
| 54 | + |
| 55 | + [$prompt] = prompt("x\n"); // an unknown answer and then the end |
| 56 | + Assert::null($prompt->choose('Go on?', ['y', 'n'])); |
| 57 | +}); |
| 58 | + |
| 59 | + |
| 60 | +test('a map of choices is drawn as a menu above the question, which is not repeated', function () { |
| 61 | + [$prompt, $output] = prompt("x\ns\n"); |
| 62 | + $choice = $prompt->choose('Choice', [ |
| 63 | + 's' => 'Skip this operation', |
| 64 | + 'r' => 'Resume', |
| 65 | + 'c' => 'Cancel deployment', |
| 66 | + ], default: 'r'); |
| 67 | + Assert::same('s', $choice); |
| 68 | + Assert::same( |
| 69 | + " [s] Skip this operation\n" |
| 70 | + . " [r] Resume\n" |
| 71 | + . " [c] Cancel deployment\n" |
| 72 | + . 'Choice [s/R/c] Choice [s/R/c] ', |
| 73 | + written($output), |
| 74 | + ); |
| 75 | +}); |
| 76 | + |
| 77 | + |
| 78 | +test('without a user at a terminal the prompt refuses before it reads or writes', function () { |
| 79 | + [$prompt, $output] = prompt("y\n", terminal: false); |
| 80 | + Assert::false($prompt->isInteractive()); |
| 81 | + Assert::exception(fn() => $prompt->choose('Go on?', ['y', 'n']), LogicException::class); |
| 82 | + Assert::same('', written($output)); |
| 83 | + |
| 84 | + $input = fopen('php://memory', 'w+'); |
| 85 | + $output = fopen('php://memory', 'w+'); |
| 86 | + Assert::false((new Prompt(new Console($output, terminal: false), $input, inputTerminal: true))->isInteractive()); |
| 87 | + Assert::false((new Prompt(new Console($output, terminal: true), $input))->isInteractive()); |
| 88 | + Assert::true((new Prompt(new Console($output, terminal: true), $input, inputTerminal: true))->isInteractive()); |
| 89 | +}); |
| 90 | + |
| 91 | + |
| 92 | +test('an input not open for reading is refused', function () { |
| 93 | + $output = fopen('php://memory', 'w+'); |
| 94 | + Assert::exception( |
| 95 | + fn() => new Prompt(new Console($output), fopen('php://output', 'w')), |
| 96 | + InvalidArgumentException::class, |
| 97 | + 'The stream is not open for reading.', |
| 98 | + ); |
| 99 | +}); |
| 100 | + |
| 101 | + |
| 102 | +test('wrong choices are an error of the call', function () { |
| 103 | + [$prompt] = prompt(''); |
| 104 | + Assert::exception(fn() => $prompt->choose('?', []), InvalidArgumentException::class, 'There are no choices.'); |
| 105 | + Assert::exception(fn() => $prompt->choose('?', ['1' => 'One', 'n' => 'No']), InvalidArgumentException::class, 'Choice 1 is a number, but a choice is a string.'); |
| 106 | + Assert::exception(fn() => $prompt->choose('?', [1, 2]), InvalidArgumentException::class, 'Choice 1 is a number, but a choice is a string.'); |
| 107 | + Assert::exception(fn() => $prompt->choose('?', ['y', '']), InvalidArgumentException::class, 'A choice must be a string with no surrounding whitespace.'); |
| 108 | + Assert::exception(fn() => $prompt->choose('?', ['y', 'Y']), InvalidArgumentException::class, "Choice 'Y' is given twice, the case aside."); |
| 109 | + Assert::exception(fn() => $prompt->choose('?', ['y', 'n'], default: 'x'), InvalidArgumentException::class, "Default 'x' is not one of the choices."); |
| 110 | +}); |
| 111 | + |
| 112 | + |
| 113 | +test('the answer leaves the cursor at the start of a line, so a status needs no new line', function () { |
| 114 | + $input = fopen('php://memory', 'w+'); |
| 115 | + fwrite($input, "y\n"); |
| 116 | + rewind($input); |
| 117 | + $output = fopen('php://memory', 'w+'); |
| 118 | + $console = new Console($output, ColorDepth::None, terminal: true); |
| 119 | + (new Prompt($console, $input, inputTerminal: true))->choose('Go on?', ['y', 'n']); |
| 120 | + $console->setStatus('working'); |
| 121 | + Assert::same("Go on? [y/n] \e[?25lworking\r", written($output)); |
| 122 | +}); |
0 commit comments