QuestionHelperTest.php 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Console\Tests\Helper;
  11. use Symfony\Component\Console\Application;
  12. use Symfony\Component\Console\Exception\InvalidArgumentException;
  13. use Symfony\Component\Console\Exception\MissingInputException;
  14. use Symfony\Component\Console\Formatter\OutputFormatter;
  15. use Symfony\Component\Console\Helper\FormatterHelper;
  16. use Symfony\Component\Console\Helper\HelperSet;
  17. use Symfony\Component\Console\Helper\QuestionHelper;
  18. use Symfony\Component\Console\Input\InputInterface;
  19. use Symfony\Component\Console\Output\OutputInterface;
  20. use Symfony\Component\Console\Output\StreamOutput;
  21. use Symfony\Component\Console\Question\ChoiceQuestion;
  22. use Symfony\Component\Console\Question\ConfirmationQuestion;
  23. use Symfony\Component\Console\Question\Question;
  24. use Symfony\Component\Console\Terminal;
  25. use Symfony\Component\Console\Tester\ApplicationTester;
  26. /**
  27. * @group tty
  28. */
  29. class QuestionHelperTest extends AbstractQuestionHelperTestCase
  30. {
  31. public function testAskChoice()
  32. {
  33. $questionHelper = new QuestionHelper();
  34. $helperSet = new HelperSet([new FormatterHelper()]);
  35. $questionHelper->setHelperSet($helperSet);
  36. $heroes = ['Superman', 'Batman', 'Spiderman'];
  37. $inputStream = $this->getInputStream("\n1\n 1 \nFabien\n1\nFabien\n1\n0,2\n 0 , 2 \n\n\n");
  38. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '2');
  39. $question->setMaxAttempts(1);
  40. // first answer is an empty answer, we're supposed to receive the default value
  41. $this->assertEquals('Spiderman', $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  42. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes);
  43. $question->setMaxAttempts(1);
  44. $this->assertEquals('Batman', $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  45. $this->assertEquals('Batman', $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  46. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes);
  47. $question->setErrorMessage('Input "%s" is not a superhero!');
  48. $question->setMaxAttempts(2);
  49. $this->assertEquals('Batman', $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream), $output = $this->createOutputInterface(), $question));
  50. rewind($output->getStream());
  51. $stream = stream_get_contents($output->getStream());
  52. $this->assertStringContainsString('Input "Fabien" is not a superhero!', $stream);
  53. try {
  54. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '1');
  55. $question->setMaxAttempts(1);
  56. $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream), $output = $this->createOutputInterface(), $question);
  57. $this->fail();
  58. } catch (\InvalidArgumentException $e) {
  59. $this->assertEquals('Value "Fabien" is invalid', $e->getMessage());
  60. }
  61. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, null);
  62. $question->setMaxAttempts(1);
  63. $question->setMultiselect(true);
  64. $this->assertEquals(['Batman'], $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  65. $this->assertEquals(['Superman', 'Spiderman'], $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  66. $this->assertEquals(['Superman', 'Spiderman'], $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  67. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '0,1');
  68. $question->setMaxAttempts(1);
  69. $question->setMultiselect(true);
  70. $this->assertEquals(['Superman', 'Batman'], $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  71. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, ' 0 , 1 ');
  72. $question->setMaxAttempts(1);
  73. $question->setMultiselect(true);
  74. $this->assertEquals(['Superman', 'Batman'], $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  75. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, 0);
  76. // We are supposed to get the default value since we are not in interactive mode
  77. $this->assertEquals('Superman', $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, true), $this->createOutputInterface(), $question));
  78. }
  79. public function testAskChoiceNonInteractive()
  80. {
  81. $questionHelper = new QuestionHelper();
  82. $helperSet = new HelperSet([new FormatterHelper()]);
  83. $questionHelper->setHelperSet($helperSet);
  84. $inputStream = $this->getInputStream("\n1\n 1 \nFabien\n1\nFabien\n1\n0,2\n 0 , 2 \n\n\n");
  85. $heroes = ['Superman', 'Batman', 'Spiderman'];
  86. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '0');
  87. $this->assertSame('Superman', $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, false), $this->createOutputInterface(), $question));
  88. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, 'Batman');
  89. $this->assertSame('Batman', $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, false), $this->createOutputInterface(), $question));
  90. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, null);
  91. $this->assertNull($questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, false), $this->createOutputInterface(), $question));
  92. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, '0');
  93. $question->setValidator(null);
  94. $this->assertSame('Superman', $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, false), $this->createOutputInterface(), $question));
  95. try {
  96. $question = new ChoiceQuestion('What is your favorite superhero?', $heroes, null);
  97. $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, false), $this->createOutputInterface(), $question);
  98. } catch (\InvalidArgumentException $e) {
  99. $this->assertSame('Value "" is invalid', $e->getMessage());
  100. }
  101. $question = new ChoiceQuestion('Who are your favorite superheros?', $heroes, '0, 1');
  102. $question->setMultiselect(true);
  103. $this->assertSame(['Superman', 'Batman'], $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, false), $this->createOutputInterface(), $question));
  104. $question = new ChoiceQuestion('Who are your favorite superheros?', $heroes, '0, 1');
  105. $question->setMultiselect(true);
  106. $question->setValidator(null);
  107. $this->assertSame(['Superman', 'Batman'], $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, false), $this->createOutputInterface(), $question));
  108. $question = new ChoiceQuestion('Who are your favorite superheros?', $heroes, '0, Batman');
  109. $question->setMultiselect(true);
  110. $this->assertSame(['Superman', 'Batman'], $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, false), $this->createOutputInterface(), $question));
  111. $question = new ChoiceQuestion('Who are your favorite superheros?', $heroes, null);
  112. $question->setMultiselect(true);
  113. $this->assertNull($questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, false), $this->createOutputInterface(), $question));
  114. $question = new ChoiceQuestion('Who are your favorite superheros?', ['a' => 'Batman', 'b' => 'Superman'], 'a');
  115. $this->assertSame('a', $questionHelper->ask($this->createStreamableInputInterfaceMock('', false), $this->createOutputInterface(), $question), 'ChoiceQuestion validator returns the key if it\'s a string');
  116. try {
  117. $question = new ChoiceQuestion('Who are your favorite superheros?', $heroes, '');
  118. $question->setMultiselect(true);
  119. $questionHelper->ask($this->createStreamableInputInterfaceMock($inputStream, false), $this->createOutputInterface(), $question);
  120. } catch (\InvalidArgumentException $e) {
  121. $this->assertSame('Value "" is invalid', $e->getMessage());
  122. }
  123. }
  124. public function testAsk()
  125. {
  126. $dialog = new QuestionHelper();
  127. $inputStream = $this->getInputStream("\n8AM\n");
  128. $question = new Question('What time is it?', '2PM');
  129. $this->assertEquals('2PM', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  130. $question = new Question('What time is it?', '2PM');
  131. $this->assertEquals('8AM', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $output = $this->createOutputInterface(), $question));
  132. rewind($output->getStream());
  133. $this->assertEquals('What time is it?', stream_get_contents($output->getStream()));
  134. }
  135. public function testAskNonTrimmed()
  136. {
  137. $dialog = new QuestionHelper();
  138. $inputStream = $this->getInputStream(' 8AM ');
  139. $question = new Question('What time is it?', '2PM');
  140. $question->setTrimmable(false);
  141. $this->assertEquals(' 8AM ', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $output = $this->createOutputInterface(), $question));
  142. rewind($output->getStream());
  143. $this->assertEquals('What time is it?', stream_get_contents($output->getStream()));
  144. }
  145. public function testAskWithAutocomplete()
  146. {
  147. if (!Terminal::hasSttyAvailable()) {
  148. $this->markTestSkipped('`stty` is required to test autocomplete functionality');
  149. }
  150. // Acm<NEWLINE>
  151. // Ac<BACKSPACE><BACKSPACE>s<TAB>Test<NEWLINE>
  152. // <NEWLINE>
  153. // <UP ARROW><UP ARROW><UP ARROW><NEWLINE>
  154. // <UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><TAB>Test<NEWLINE>
  155. // <DOWN ARROW><NEWLINE>
  156. // S<BACKSPACE><BACKSPACE><DOWN ARROW><DOWN ARROW><NEWLINE>
  157. // F00<BACKSPACE><BACKSPACE>oo<TAB><NEWLINE>
  158. // F⭐<TAB><BACKSPACE><BACKSPACE>⭐<TAB><NEWLINE>
  159. $inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n\033[A\033[A\033[A\n\033[A\033[A\033[A\033[A\033[A\033[A\033[A\tTest\n\033[B\nS\177\177\033[B\033[B\nF00\177\177oo\t\nF⭐\t\177\177⭐\t\n");
  160. $dialog = new QuestionHelper();
  161. $helperSet = new HelperSet([new FormatterHelper()]);
  162. $dialog->setHelperSet($helperSet);
  163. $question = new Question('Please select a bundle', 'FrameworkBundle');
  164. $question->setAutocompleterValues(['AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle', 'F⭐Y']);
  165. $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  166. $this->assertEquals('AsseticBundleTest', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  167. $this->assertEquals('FrameworkBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  168. $this->assertEquals('SecurityBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  169. $this->assertEquals('FooBundleTest', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  170. $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  171. $this->assertEquals('AsseticBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  172. $this->assertEquals('FooBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  173. $this->assertEquals('F⭐Y', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  174. }
  175. public function testAskWithAutocompleteTrimmable()
  176. {
  177. if (!Terminal::hasSttyAvailable()) {
  178. $this->markTestSkipped('`stty` is required to test autocomplete functionality');
  179. }
  180. // Acm<NEWLINE>
  181. // Ac<BACKSPACE><BACKSPACE>s<TAB>Test<NEWLINE>
  182. // <NEWLINE>
  183. // <UP ARROW><UP ARROW><NEWLINE>
  184. // <UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><TAB>Test<NEWLINE>
  185. // <DOWN ARROW><NEWLINE>
  186. // S<BACKSPACE><BACKSPACE><DOWN ARROW><DOWN ARROW><NEWLINE>
  187. // F00<BACKSPACE><BACKSPACE>oo<TAB><NEWLINE>
  188. $inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n\033[A\033[A\n\033[A\033[A\033[A\033[A\033[A\tTest\n\033[B\nS\177\177\033[B\033[B\nF00\177\177oo\t\n");
  189. $dialog = new QuestionHelper();
  190. $helperSet = new HelperSet([new FormatterHelper()]);
  191. $dialog->setHelperSet($helperSet);
  192. $question = new Question('Please select a bundle', 'FrameworkBundle');
  193. $question->setAutocompleterValues(['AcmeDemoBundle ', 'AsseticBundle', ' SecurityBundle ', 'FooBundle']);
  194. $question->setTrimmable(false);
  195. $this->assertEquals('AcmeDemoBundle ', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  196. $this->assertEquals('AsseticBundleTest', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  197. $this->assertEquals('FrameworkBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  198. $this->assertEquals(' SecurityBundle ', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  199. $this->assertEquals('FooBundleTest', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  200. $this->assertEquals('AcmeDemoBundle ', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  201. $this->assertEquals('AsseticBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  202. $this->assertEquals('FooBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  203. }
  204. public function testAskWithAutocompleteCallback()
  205. {
  206. if (!Terminal::hasSttyAvailable()) {
  207. $this->markTestSkipped('`stty` is required to test autocomplete functionality');
  208. }
  209. // Po<TAB>Cr<TAB>P<DOWN ARROW><DOWN ARROW><NEWLINE>
  210. $inputStream = $this->getInputStream("Pa\177\177o\tCr\tP\033[A\033[A\n");
  211. $dialog = new QuestionHelper();
  212. $helperSet = new HelperSet([new FormatterHelper()]);
  213. $dialog->setHelperSet($helperSet);
  214. $question = new Question('What\'s for dinner?');
  215. // A simple test callback - return an array containing the words the
  216. // user has already completed, suffixed with all known words.
  217. //
  218. // Eg: If the user inputs "Potato C", the return will be:
  219. //
  220. // ["Potato Carrot ", "Potato Creme ", "Potato Curry ", ...]
  221. //
  222. // No effort is made to avoid irrelevant suggestions, as this is handled
  223. // by the autocomplete function.
  224. $callback = function ($input) {
  225. $knownWords = ['Carrot', 'Creme', 'Curry', 'Parsnip', 'Pie', 'Potato', 'Tart'];
  226. $inputWords = explode(' ', $input);
  227. array_pop($inputWords);
  228. $suggestionBase = $inputWords ? implode(' ', $inputWords).' ' : '';
  229. return array_map(
  230. function ($word) use ($suggestionBase) {
  231. return $suggestionBase.$word.' ';
  232. },
  233. $knownWords
  234. );
  235. };
  236. $question->setAutocompleterCallback($callback);
  237. $this->assertSame('Potato Creme Pie', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  238. }
  239. public function testAskWithAutocompleteWithNonSequentialKeys()
  240. {
  241. if (!Terminal::hasSttyAvailable()) {
  242. $this->markTestSkipped('`stty` is required to test autocomplete functionality');
  243. }
  244. // <UP ARROW><UP ARROW><NEWLINE><DOWN ARROW><DOWN ARROW><NEWLINE>
  245. $inputStream = $this->getInputStream("\033[A\033[A\n\033[B\033[B\n");
  246. $dialog = new QuestionHelper();
  247. $dialog->setHelperSet(new HelperSet([new FormatterHelper()]));
  248. $question = new ChoiceQuestion('Please select a bundle', [1 => 'AcmeDemoBundle', 4 => 'AsseticBundle']);
  249. $question->setMaxAttempts(1);
  250. $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  251. $this->assertEquals('AsseticBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  252. }
  253. public function testAskWithAutocompleteWithExactMatch()
  254. {
  255. if (!Terminal::hasSttyAvailable()) {
  256. $this->markTestSkipped('`stty` is required to test autocomplete functionality');
  257. }
  258. $inputStream = $this->getInputStream("b\n");
  259. $possibleChoices = [
  260. 'a' => 'berlin',
  261. 'b' => 'copenhagen',
  262. 'c' => 'amsterdam',
  263. ];
  264. $dialog = new QuestionHelper();
  265. $dialog->setHelperSet(new HelperSet([new FormatterHelper()]));
  266. $question = new ChoiceQuestion('Please select a city', $possibleChoices);
  267. $question->setMaxAttempts(1);
  268. $this->assertSame('b', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  269. }
  270. public static function getInputs()
  271. {
  272. return [
  273. ['$'], // 1 byte character
  274. ['¢'], // 2 bytes character
  275. ['€'], // 3 bytes character
  276. ['𐍈'], // 4 bytes character
  277. ];
  278. }
  279. /**
  280. * @dataProvider getInputs
  281. */
  282. public function testAskWithAutocompleteWithMultiByteCharacter($character)
  283. {
  284. if (!Terminal::hasSttyAvailable()) {
  285. $this->markTestSkipped('`stty` is required to test autocomplete functionality');
  286. }
  287. $inputStream = $this->getInputStream("$character\n");
  288. $possibleChoices = [
  289. '$' => '1 byte character',
  290. '¢' => '2 bytes character',
  291. '€' => '3 bytes character',
  292. '𐍈' => '4 bytes character',
  293. ];
  294. $dialog = new QuestionHelper();
  295. $dialog->setHelperSet(new HelperSet([new FormatterHelper()]));
  296. $question = new ChoiceQuestion('Please select a character', $possibleChoices);
  297. $question->setMaxAttempts(1);
  298. $this->assertSame($character, $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  299. }
  300. public function testAutocompleteWithTrailingBackslash()
  301. {
  302. if (!Terminal::hasSttyAvailable()) {
  303. $this->markTestSkipped('`stty` is required to test autocomplete functionality');
  304. }
  305. $inputStream = $this->getInputStream('E');
  306. $dialog = new QuestionHelper();
  307. $helperSet = new HelperSet([new FormatterHelper()]);
  308. $dialog->setHelperSet($helperSet);
  309. $question = new Question('');
  310. $expectedCompletion = 'ExampleNamespace\\';
  311. $question->setAutocompleterValues([$expectedCompletion]);
  312. $output = $this->createOutputInterface();
  313. $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $output, $question);
  314. $outputStream = $output->getStream();
  315. rewind($outputStream);
  316. $actualOutput = stream_get_contents($outputStream);
  317. // Shell control (esc) sequences are not so important: we only care that
  318. // <hl> tag is interpreted correctly and replaced
  319. $irrelevantEscSequences = [
  320. "\0337" => '', // Save cursor position
  321. "\0338" => '', // Restore cursor position
  322. "\033[K" => '', // Clear line from cursor till the end
  323. ];
  324. $importantActualOutput = strtr($actualOutput, $irrelevantEscSequences);
  325. // Remove colors (e.g. "\033[30m", "\033[31;41m")
  326. $importantActualOutput = preg_replace('/\033\[\d+(;\d+)?m/', '', $importantActualOutput);
  327. $this->assertEquals($expectedCompletion, $importantActualOutput);
  328. }
  329. public function testAskHiddenResponse()
  330. {
  331. if ('\\' === \DIRECTORY_SEPARATOR) {
  332. $this->markTestSkipped('This test is not supported on Windows');
  333. }
  334. $dialog = new QuestionHelper();
  335. $question = new Question('What time is it?');
  336. $question->setHidden(true);
  337. $this->assertEquals('8AM', $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream("8AM\n")), $this->createOutputInterface(), $question));
  338. }
  339. public function testAskHiddenResponseNotTrimmed()
  340. {
  341. if ('\\' === \DIRECTORY_SEPARATOR) {
  342. $this->markTestSkipped('This test is not supported on Windows');
  343. }
  344. $dialog = new QuestionHelper();
  345. $question = new Question('What time is it?');
  346. $question->setHidden(true);
  347. $question->setTrimmable(false);
  348. $this->assertEquals(' 8AM'.\PHP_EOL, $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream(' 8AM'.\PHP_EOL)), $this->createOutputInterface(), $question));
  349. }
  350. public function testAskMultilineResponseWithEOF()
  351. {
  352. $essay = <<<'EOD'
  353. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque pretium lectus quis suscipit porttitor. Sed pretium bibendum vestibulum.
  354. Etiam accumsan, justo vitae imperdiet aliquet, neque est sagittis mauris, sed interdum massa leo id leo.
  355. Aliquam rhoncus, libero ac blandit convallis, est sapien hendrerit nulla, vitae aliquet tellus orci a odio. Aliquam gravida ante sit amet massa lacinia, ut condimentum purus venenatis.
  356. Vivamus et erat dictum, euismod neque in, laoreet odio. Aenean vitae tellus at leo vestibulum auctor id eget urna.
  357. EOD;
  358. $response = $this->getInputStream($essay);
  359. $dialog = new QuestionHelper();
  360. $question = new Question('Write an essay');
  361. $question->setMultiline(true);
  362. $this->assertSame($essay, $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question));
  363. }
  364. public function testAskMultilineResponseWithSingleNewline()
  365. {
  366. $response = $this->getInputStream(\PHP_EOL);
  367. $dialog = new QuestionHelper();
  368. $question = new Question('Write an essay');
  369. $question->setMultiline(true);
  370. $this->assertNull($dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question));
  371. }
  372. public function testAskMultilineResponseWithDataAfterNewline()
  373. {
  374. $response = $this->getInputStream(\PHP_EOL.'this is text');
  375. $dialog = new QuestionHelper();
  376. $question = new Question('Write an essay');
  377. $question->setMultiline(true);
  378. $this->assertNull($dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question));
  379. }
  380. public function testAskMultilineResponseWithMultipleNewlinesAtEnd()
  381. {
  382. $typedText = 'This is a body'.\PHP_EOL.\PHP_EOL;
  383. $response = $this->getInputStream($typedText);
  384. $dialog = new QuestionHelper();
  385. $question = new Question('Write an essay');
  386. $question->setMultiline(true);
  387. $this->assertSame('This is a body', $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question));
  388. }
  389. public function testAskMultilineResponseWithWithCursorInMiddleOfSeekableInputStream()
  390. {
  391. $input = <<<EOD
  392. This
  393. is
  394. some
  395. input
  396. EOD;
  397. $response = $this->getInputStream($input);
  398. fseek($response, 8);
  399. $dialog = new QuestionHelper();
  400. $question = new Question('Write an essay');
  401. $question->setMultiline(true);
  402. $this->assertSame("some\ninput", $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question));
  403. $this->assertSame(8, ftell($response));
  404. }
  405. /**
  406. * @dataProvider getAskConfirmationData
  407. */
  408. public function testAskConfirmation($question, $expected, $default = true)
  409. {
  410. $dialog = new QuestionHelper();
  411. $inputStream = $this->getInputStream($question."\n");
  412. $question = new ConfirmationQuestion('Do you like French fries?', $default);
  413. $this->assertEquals($expected, $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question), 'confirmation question should '.($expected ? 'pass' : 'cancel'));
  414. }
  415. public static function getAskConfirmationData()
  416. {
  417. return [
  418. ['', true],
  419. ['', false, false],
  420. ['y', true],
  421. ['yes', true],
  422. ['n', false],
  423. ['no', false],
  424. ];
  425. }
  426. public function testAskConfirmationWithCustomTrueAnswer()
  427. {
  428. $dialog = new QuestionHelper();
  429. $inputStream = $this->getInputStream("j\ny\n");
  430. $question = new ConfirmationQuestion('Do you like French fries?', false, '/^(j|y)/i');
  431. $this->assertTrue($dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  432. $question = new ConfirmationQuestion('Do you like French fries?', false, '/^(j|y)/i');
  433. $this->assertTrue($dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  434. }
  435. public function testAskAndValidate()
  436. {
  437. $dialog = new QuestionHelper();
  438. $helperSet = new HelperSet([new FormatterHelper()]);
  439. $dialog->setHelperSet($helperSet);
  440. $error = 'This is not a color!';
  441. $validator = function ($color) use ($error) {
  442. if (!\in_array($color, ['white', 'black'])) {
  443. throw new \InvalidArgumentException($error);
  444. }
  445. return $color;
  446. };
  447. $question = new Question('What color was the white horse of Henry IV?', 'white');
  448. $question->setValidator($validator);
  449. $question->setMaxAttempts(2);
  450. $inputStream = $this->getInputStream("\nblack\n");
  451. $this->assertEquals('white', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  452. $this->assertEquals('black', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  453. try {
  454. $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream("green\nyellow\norange\n")), $this->createOutputInterface(), $question);
  455. $this->fail();
  456. } catch (\InvalidArgumentException $e) {
  457. $this->assertEquals($error, $e->getMessage());
  458. }
  459. }
  460. /**
  461. * @dataProvider simpleAnswerProvider
  462. */
  463. public function testSelectChoiceFromSimpleChoices($providedAnswer, $expectedValue)
  464. {
  465. $possibleChoices = [
  466. 'My environment 1',
  467. 'My environment 2',
  468. 'My environment 3',
  469. ];
  470. $dialog = new QuestionHelper();
  471. $helperSet = new HelperSet([new FormatterHelper()]);
  472. $dialog->setHelperSet($helperSet);
  473. $question = new ChoiceQuestion('Please select the environment to load', $possibleChoices);
  474. $question->setMaxAttempts(1);
  475. $answer = $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream($providedAnswer."\n")), $this->createOutputInterface(), $question);
  476. $this->assertSame($expectedValue, $answer);
  477. }
  478. public static function simpleAnswerProvider()
  479. {
  480. return [
  481. [0, 'My environment 1'],
  482. [1, 'My environment 2'],
  483. [2, 'My environment 3'],
  484. ['My environment 1', 'My environment 1'],
  485. ['My environment 2', 'My environment 2'],
  486. ['My environment 3', 'My environment 3'],
  487. ];
  488. }
  489. /**
  490. * @dataProvider specialCharacterInMultipleChoice
  491. */
  492. public function testSpecialCharacterChoiceFromMultipleChoiceList($providedAnswer, $expectedValue)
  493. {
  494. $possibleChoices = [
  495. '.',
  496. 'src',
  497. ];
  498. $dialog = new QuestionHelper();
  499. $inputStream = $this->getInputStream($providedAnswer."\n");
  500. $helperSet = new HelperSet([new FormatterHelper()]);
  501. $dialog->setHelperSet($helperSet);
  502. $question = new ChoiceQuestion('Please select the directory', $possibleChoices);
  503. $question->setMaxAttempts(1);
  504. $question->setMultiselect(true);
  505. $answer = $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question);
  506. $this->assertSame($expectedValue, $answer);
  507. }
  508. public static function specialCharacterInMultipleChoice()
  509. {
  510. return [
  511. ['.', ['.']],
  512. ['., src', ['.', 'src']],
  513. ];
  514. }
  515. /**
  516. * @dataProvider answerProvider
  517. */
  518. public function testSelectChoiceFromChoiceList($providedAnswer, $expectedValue)
  519. {
  520. $possibleChoices = [
  521. 'env_1' => 'My environment 1',
  522. 'env_2' => 'My environment',
  523. 'env_3' => 'My environment',
  524. ];
  525. $dialog = new QuestionHelper();
  526. $helperSet = new HelperSet([new FormatterHelper()]);
  527. $dialog->setHelperSet($helperSet);
  528. $question = new ChoiceQuestion('Please select the environment to load', $possibleChoices);
  529. $question->setMaxAttempts(1);
  530. $answer = $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream($providedAnswer."\n")), $this->createOutputInterface(), $question);
  531. $this->assertSame($expectedValue, $answer);
  532. }
  533. public function testAmbiguousChoiceFromChoicelist()
  534. {
  535. $this->expectException(\InvalidArgumentException::class);
  536. $this->expectExceptionMessage('The provided answer is ambiguous. Value should be one of "env_2" or "env_3".');
  537. $possibleChoices = [
  538. 'env_1' => 'My first environment',
  539. 'env_2' => 'My environment',
  540. 'env_3' => 'My environment',
  541. ];
  542. $dialog = new QuestionHelper();
  543. $helperSet = new HelperSet([new FormatterHelper()]);
  544. $dialog->setHelperSet($helperSet);
  545. $question = new ChoiceQuestion('Please select the environment to load', $possibleChoices);
  546. $question->setMaxAttempts(1);
  547. $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream("My environment\n")), $this->createOutputInterface(), $question);
  548. }
  549. public static function answerProvider()
  550. {
  551. return [
  552. ['env_1', 'env_1'],
  553. ['env_2', 'env_2'],
  554. ['env_3', 'env_3'],
  555. ['My environment 1', 'env_1'],
  556. ];
  557. }
  558. public function testNoInteraction()
  559. {
  560. $dialog = new QuestionHelper();
  561. $question = new Question('Do you have a job?', 'not yet');
  562. $this->assertEquals('not yet', $dialog->ask($this->createStreamableInputInterfaceMock(null, false), $this->createOutputInterface(), $question));
  563. }
  564. /**
  565. * @requires function mb_strwidth
  566. */
  567. public function testChoiceOutputFormattingQuestionForUtf8Keys()
  568. {
  569. $question = 'Lorem ipsum?';
  570. $possibleChoices = [
  571. 'foo' => 'foo',
  572. 'żółw' => 'bar',
  573. 'łabądź' => 'baz',
  574. ];
  575. $outputShown = [
  576. $question,
  577. ' [<info>foo </info>] foo',
  578. ' [<info>żółw </info>] bar',
  579. ' [<info>łabądź</info>] baz',
  580. ];
  581. $output = $this->createMock(OutputInterface::class);
  582. $output->method('getFormatter')->willReturn(new OutputFormatter());
  583. $dialog = new QuestionHelper();
  584. $helperSet = new HelperSet([new FormatterHelper()]);
  585. $dialog->setHelperSet($helperSet);
  586. $output->expects($this->once())->method('writeln')->with($this->equalTo($outputShown));
  587. $question = new ChoiceQuestion($question, $possibleChoices, 'foo');
  588. $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream("\n")), $output, $question);
  589. }
  590. public function testAskThrowsExceptionOnMissingInput()
  591. {
  592. $this->expectException(MissingInputException::class);
  593. $this->expectExceptionMessage('Aborted.');
  594. $dialog = new QuestionHelper();
  595. $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?'));
  596. }
  597. public function testAskThrowsExceptionOnMissingInputForChoiceQuestion()
  598. {
  599. $this->expectException(MissingInputException::class);
  600. $this->expectExceptionMessage('Aborted.');
  601. $dialog = new QuestionHelper();
  602. $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new ChoiceQuestion('Choice', ['a', 'b']));
  603. }
  604. public function testAskThrowsExceptionOnMissingInputWithValidator()
  605. {
  606. $this->expectException(MissingInputException::class);
  607. $this->expectExceptionMessage('Aborted.');
  608. $dialog = new QuestionHelper();
  609. $question = new Question('What\'s your name?');
  610. $question->setValidator(function ($value) {
  611. if (!$value) {
  612. throw new \Exception('A value is required.');
  613. }
  614. });
  615. $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), $question);
  616. }
  617. public function testQuestionValidatorRepeatsThePrompt()
  618. {
  619. $tries = 0;
  620. $application = new Application();
  621. $application->setAutoExit(false);
  622. $application->register('question')
  623. ->setCode(function ($input, $output) use (&$tries) {
  624. $question = new Question('This is a promptable question');
  625. $question->setValidator(function ($value) use (&$tries) {
  626. ++$tries;
  627. if (!$value) {
  628. throw new \Exception();
  629. }
  630. return $value;
  631. });
  632. (new QuestionHelper())->ask($input, $output, $question);
  633. return 0;
  634. })
  635. ;
  636. $tester = new ApplicationTester($application);
  637. $tester->setInputs(['', 'not-empty']);
  638. $statusCode = $tester->run(['command' => 'question'], ['interactive' => true]);
  639. $this->assertSame(2, $tries);
  640. $this->assertSame($statusCode, 0);
  641. }
  642. public function testEmptyChoices()
  643. {
  644. $this->expectException(\LogicException::class);
  645. $this->expectExceptionMessage('Choice question must have at least 1 choice available.');
  646. new ChoiceQuestion('Question', [], 'irrelevant');
  647. }
  648. public function testTraversableAutocomplete()
  649. {
  650. if (!Terminal::hasSttyAvailable()) {
  651. $this->markTestSkipped('`stty` is required to test autocomplete functionality');
  652. }
  653. // Acm<NEWLINE>
  654. // Ac<BACKSPACE><BACKSPACE>s<TAB>Test<NEWLINE>
  655. // <NEWLINE>
  656. // <UP ARROW><UP ARROW><NEWLINE>
  657. // <UP ARROW><UP ARROW><UP ARROW><UP ARROW><UP ARROW><TAB>Test<NEWLINE>
  658. // <DOWN ARROW><NEWLINE>
  659. // S<BACKSPACE><BACKSPACE><DOWN ARROW><DOWN ARROW><NEWLINE>
  660. // F00<BACKSPACE><BACKSPACE>oo<TAB><NEWLINE>
  661. $inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n\033[A\033[A\n\033[A\033[A\033[A\033[A\033[A\tTest\n\033[B\nS\177\177\033[B\033[B\nF00\177\177oo\t\n");
  662. $dialog = new QuestionHelper();
  663. $helperSet = new HelperSet([new FormatterHelper()]);
  664. $dialog->setHelperSet($helperSet);
  665. $question = new Question('Please select a bundle', 'FrameworkBundle');
  666. $question->setAutocompleterValues(new AutocompleteValues(['irrelevant' => 'AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle']));
  667. $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  668. $this->assertEquals('AsseticBundleTest', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  669. $this->assertEquals('FrameworkBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  670. $this->assertEquals('SecurityBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  671. $this->assertEquals('FooBundleTest', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  672. $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  673. $this->assertEquals('AsseticBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  674. $this->assertEquals('FooBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  675. }
  676. public function testDisableStty()
  677. {
  678. if (!Terminal::hasSttyAvailable()) {
  679. $this->markTestSkipped('`stty` is required to test autocomplete functionality');
  680. }
  681. $this->expectException(InvalidArgumentException::class);
  682. $this->expectExceptionMessage('invalid');
  683. QuestionHelper::disableStty();
  684. $dialog = new QuestionHelper();
  685. $dialog->setHelperSet(new HelperSet([new FormatterHelper()]));
  686. $question = new ChoiceQuestion('Please select a bundle', [1 => 'AcmeDemoBundle', 4 => 'AsseticBundle']);
  687. $question->setMaxAttempts(1);
  688. // <UP ARROW><UP ARROW><NEWLINE><DOWN ARROW><DOWN ARROW><NEWLINE>
  689. // Gives `AcmeDemoBundle` with stty
  690. $inputStream = $this->getInputStream("\033[A\033[A\n\033[B\033[B\n");
  691. try {
  692. $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question);
  693. } finally {
  694. $reflection = new \ReflectionProperty(QuestionHelper::class, 'stty');
  695. $reflection->setAccessible(true);
  696. $reflection->setValue(null, true);
  697. }
  698. }
  699. public function testTraversableMultiselectAutocomplete()
  700. {
  701. if (!Terminal::hasSttyAvailable()) {
  702. $this->markTestSkipped('`stty` is required to test autocomplete functionality');
  703. }
  704. // <NEWLINE>
  705. // F<TAB><NEWLINE>
  706. // A<3x UP ARROW><TAB>,F<TAB><NEWLINE>
  707. // F00<BACKSPACE><BACKSPACE>o<TAB>,A<DOWN ARROW>,<SPACE>SecurityBundle<NEWLINE>
  708. // Acme<TAB>,<SPACE>As<TAB><29x BACKSPACE>S<TAB><NEWLINE>
  709. // Ac<TAB>,As<TAB><3x BACKSPACE>d<TAB><NEWLINE>
  710. $inputStream = $this->getInputStream("\nF\t\nA\033[A\033[A\033[A\t,F\t\nF00\177\177o\t,A\033[B\t, SecurityBundle\nAcme\t, As\t\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177S\t\nAc\t,As\t\177\177\177d\t\n");
  711. $dialog = new QuestionHelper();
  712. $helperSet = new HelperSet([new FormatterHelper()]);
  713. $dialog->setHelperSet($helperSet);
  714. $question = new ChoiceQuestion(
  715. 'Please select a bundle (defaults to AcmeDemoBundle and AsseticBundle)',
  716. ['AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle'],
  717. '0,1'
  718. );
  719. // This tests that autocomplete works for all multiselect choices entered by the user
  720. $question->setMultiselect(true);
  721. $this->assertEquals(['AcmeDemoBundle', 'AsseticBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  722. $this->assertEquals(['FooBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  723. $this->assertEquals(['AsseticBundle', 'FooBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  724. $this->assertEquals(['FooBundle', 'AsseticBundle', 'SecurityBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  725. $this->assertEquals(['SecurityBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  726. $this->assertEquals(['AcmeDemoBundle', 'AsseticBundle'], $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
  727. }
  728. public function testAutocompleteMoveCursorBackwards()
  729. {
  730. // F<TAB><BACKSPACE><BACKSPACE><BACKSPACE>
  731. $inputStream = $this->getInputStream("F\t\177\177\177");
  732. $dialog = new QuestionHelper();
  733. $helperSet = new HelperSet([new FormatterHelper()]);
  734. $dialog->setHelperSet($helperSet);
  735. $question = new Question('Question?', 'F⭐Y');
  736. $question->setAutocompleterValues(['F⭐Y']);
  737. $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $output = $this->createOutputInterface(), $question);
  738. $stream = $output->getStream();
  739. rewind($stream);
  740. $this->assertStringEndsWith("\033[1D\033[K\033[2D\033[K\033[1D\033[K", stream_get_contents($stream));
  741. }
  742. protected function getInputStream($input)
  743. {
  744. $stream = fopen('php://memory', 'r+', false);
  745. fwrite($stream, $input);
  746. rewind($stream);
  747. return $stream;
  748. }
  749. protected function createOutputInterface()
  750. {
  751. return new StreamOutput(fopen('php://memory', 'r+', false));
  752. }
  753. protected function createInputInterfaceMock($interactive = true)
  754. {
  755. $mock = $this->createMock(InputInterface::class);
  756. $mock->expects($this->any())
  757. ->method('isInteractive')
  758. ->willReturn($interactive);
  759. return $mock;
  760. }
  761. }
  762. class AutocompleteValues implements \IteratorAggregate
  763. {
  764. private $values;
  765. public function __construct(array $values)
  766. {
  767. $this->values = $values;
  768. }
  769. public function getIterator(): \Traversable
  770. {
  771. return new \ArrayIterator($this->values);
  772. }
  773. }