diff --git a/examples/split-item.php b/examples/split-item.php new file mode 100644 index 00000000..47e0d359 --- /dev/null +++ b/examples/split-item.php @@ -0,0 +1,35 @@ +getSelectedItem()->getText(); +}; + +$menu = (new CliMenuBuilder) + ->setWidth(150) + ->addSplitItem() + ->addSubMenu('Sub Menu on a split item') + ->setTitle('Behold the awesomeness') + ->addItem('This is awesome', function() { print 'Yes!'; }) + ->addSplitItem() + ->addItem('Split Item 1', function() { print 'Item 1!'; }) + ->addItem('Split Item 2', function() { print 'Item 2!'; }) + ->addItem('Split Item 3', function() { print 'Item 3!'; }) + ->addSubMenu('Split Item Nested Sub Menu') + ->addItem('One', function() { print 'One!'; }) + ->addItem('Two', function() { print 'Two!'; }) + ->addItem('Three', function() { print 'Three!'; }) + ->end() + ->end() + ->end() + ->addItem('Item 2', $itemCallable) + ->addStaticItem('Item 3 - Static') + ->addItem('Item 4', $itemCallable) + ->end() + ->build(); + +$menu->open(); \ No newline at end of file diff --git a/src/Builder.php b/src/Builder.php new file mode 100644 index 00000000..f19ecad4 --- /dev/null +++ b/src/Builder.php @@ -0,0 +1,17 @@ + + */ +interface Builder +{ + public function getTerminal() : Terminal; + + public function end() : ?Builder; + + public function getMenuStyle() : MenuStyle; +} diff --git a/src/BuilderUtils.php b/src/BuilderUtils.php new file mode 100644 index 00000000..b0428ace --- /dev/null +++ b/src/BuilderUtils.php @@ -0,0 +1,109 @@ + + */ +trait BuilderUtils +{ + /** + * @var null|Builder + */ + private $parent; + + /** + * @var self[] + */ + private $subMenuBuilders = []; + + /** + * @var CliMenu[] + */ + private $subMenus = []; + + /** + * @var array + */ + private $menuItems = []; + + public function addItem( + string $text, + callable $itemCallable, + bool $showItemExtra = false, + bool $disabled = false + ) : self { + $this->menuItems[] = new SelectableItem($text, $itemCallable, $showItemExtra, $disabled); + + return $this; + } + + public function addStaticItem(string $text) : self + { + $this->menuItems[] = new StaticItem($text); + + return $this; + } + + public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self + { + $this->menuItems[] = new LineBreakItem($breakChar, $lines); + + return $this; + } + + /** + * Add a submenu with a name. The name will be displayed as the item text + * in the parent menu. + */ + public function addSubMenu(string $name, CliMenuBuilder $subMenuBuilder = null) : Builder + { + $this->menuItems[] = $id = 'submenu-placeholder-' . $name; + + if (null === $subMenuBuilder) { + $this->subMenuBuilders[$id] = new CliMenuBuilder($this); + return $this->subMenuBuilders[$id]; + } + + $this->subMenuBuilders[$id] = $subMenuBuilder; + return $this; + } + + private function buildSubMenus(array $items) : array + { + return array_map(function ($item) { + if (!is_string($item) || 0 !== strpos($item, 'submenu-placeholder-')) { + return $item; + } + + $menuBuilder = $this->subMenuBuilders[$item]; + $this->subMenus[$item] = $menuBuilder->build(); + + return new MenuMenuItem( + substr($item, \strlen('submenu-placeholder-')), + $this->subMenus[$item], + $menuBuilder->isMenuDisabled() + ); + }, $items); + } + + /** + * Return to parent builder + * + * @throws RuntimeException + */ + public function end() : ?Builder + { + if (null === $this->parent) { + throw new RuntimeException('No parent builder to return to'); + } + + return $this->parent; + } +} diff --git a/src/CliMenu.php b/src/CliMenu.php index 0041af14..e08cfa1c 100644 --- a/src/CliMenu.php +++ b/src/CliMenu.php @@ -12,6 +12,7 @@ use PhpSchool\CliMenu\Input\Text; use PhpSchool\CliMenu\MenuItem\LineBreakItem; use PhpSchool\CliMenu\MenuItem\MenuItemInterface; +use PhpSchool\CliMenu\MenuItem\SplitItem; use PhpSchool\CliMenu\MenuItem\StaticItem; use PhpSchool\CliMenu\Dialogue\Confirm; use PhpSchool\CliMenu\Dialogue\Flash; @@ -256,7 +257,12 @@ private function display() : void switch ($char->getControl()) { case InputCharacter::UP: case InputCharacter::DOWN: - $this->moveSelection($char->getControl()); + $this->moveSelectionVertically($char->getControl()); + $this->draw(); + break; + case InputCharacter::LEFT: + case InputCharacter::RIGHT: + $this->moveSelectionHorizontally($char->getControl()); $this->draw(); break; case InputCharacter::ENTER: @@ -269,11 +275,11 @@ private function display() : void /** * Move the selection in a given direction, up / down */ - protected function moveSelection(string $direction) : void + protected function moveSelectionVertically(string $direction) : void { - do { - $itemKeys = array_keys($this->items); + $itemKeys = array_keys($this->items); + do { $direction === 'UP' ? $this->selectedItem-- : $this->selectedItem++; @@ -282,15 +288,65 @@ protected function moveSelection(string $direction) : void $this->selectedItem = $direction === 'UP' ? end($itemKeys) : reset($itemKeys); - } elseif ($this->getSelectedItem()->canSelect()) { - return; } - } while (!$this->getSelectedItem()->canSelect()); + } while (!$this->canSelect()); } + /** + * Move the selection in a given direction, left / right + */ + protected function moveSelectionHorizontally(string $direction) : void + { + if (!$this->items[$this->selectedItem] instanceof SplitItem) { + return; + } + + /** @var SplitItem $item */ + $item = $this->items[$this->selectedItem]; + $itemKeys = array_keys($item->getItems()); + $selectedItemIndex = $item->getSelectedItemIndex(); + + do { + $direction === 'LEFT' + ? $selectedItemIndex-- + : $selectedItemIndex++; + + if (!array_key_exists($selectedItemIndex, $item->getItems())) { + $selectedItemIndex = $direction === 'LEFT' + ? end($itemKeys) + : reset($itemKeys); + } + } while (!$item->canSelectIndex($selectedItemIndex)); + + $item->setSelectedItemIndex($selectedItemIndex); + } + + /** + * Can the currently selected item actually be selected? + * + * For example: + * selectable item -> yes + * static item -> no + * split item with only static items -> no + * split item with at least one selectable item -> yes + * + * @return bool + */ + private function canSelect() : bool + { + return $this->items[$this->selectedItem]->canSelect(); + } + + /** + * Retrieve the item the user actually selected + * + */ public function getSelectedItem() : MenuItemInterface { - return $this->items[$this->selectedItem]; + $item = $this->items[$this->selectedItem]; + return $item instanceof SplitItem + ? $item->getSelectedItem() + : $item; } /** @@ -385,6 +441,10 @@ protected function draw() : void protected function drawMenuItem(MenuItemInterface $item, bool $selected = false) : array { $rows = $item->getRows($this->style, $selected); + + if ($item instanceof SplitItem) { + $selected = false; + } $invertedColoursSetCode = $selected ? $this->style->getInvertedColoursSetCode() diff --git a/src/CliMenuBuilder.php b/src/CliMenuBuilder.php index 1f10a78f..bd54ce32 100644 --- a/src/CliMenuBuilder.php +++ b/src/CliMenuBuilder.php @@ -9,6 +9,7 @@ use PhpSchool\CliMenu\MenuItem\MenuItemInterface; use PhpSchool\CliMenu\MenuItem\MenuMenuItem; use PhpSchool\CliMenu\MenuItem\SelectableItem; +use PhpSchool\CliMenu\MenuItem\SplitItem; use PhpSchool\CliMenu\MenuItem\StaticItem; use PhpSchool\CliMenu\Terminal\TerminalFactory; use PhpSchool\CliMenu\Util\ColourUtil; @@ -19,27 +20,24 @@ * @author Michael Woodward * @author Aydin Hassan */ -class CliMenuBuilder +class CliMenuBuilder implements Builder { + use BuilderUtils; + /** * @var bool */ private $isBuilt = false; /** - * @var null|self + * @var SplitItemBuilder[] */ - private $parent; + private $splitItemBuilders = []; /** - * @var self[] + * @var SplitItem[] */ - private $subMenuBuilders = []; - - /** - * @var CliMenu[] - */ - private $subMenus = []; + private $splitItems = []; /** * @var string @@ -51,11 +49,6 @@ class CliMenuBuilder */ private $exitButtonText = 'Exit'; - /** - * @var array - */ - private $menuItems = []; - /** * @var array */ @@ -69,7 +62,7 @@ class CliMenuBuilder /** * @var string */ - private $menuTitle = null; + private $menuTitle; /** * @var bool @@ -81,13 +74,13 @@ class CliMenuBuilder */ private $disabled = false; - public function __construct(CliMenuBuilder $parent = null) + public function __construct(Builder $parent = null) { $this->parent = $parent; $this->terminal = $this->parent !== null ? $this->parent->getTerminal() : TerminalFactory::fromSystem(); - $this->style = MenuStyle::getDefaultStyleValues(); + $this->style = MenuStyle::getDefaultStyleValues(); } public function setTitle(string $title) : self @@ -104,17 +97,6 @@ public function addMenuItem(MenuItemInterface $item) : self return $this; } - public function addItem( - string $text, - callable $itemCallable, - bool $showItemExtra = false, - bool $disabled = false - ) : self { - $this->addMenuItem(new SelectableItem($text, $itemCallable, $showItemExtra, $disabled)); - - return $this; - } - public function addItems(array $items) : self { foreach ($items as $item) { @@ -124,20 +106,6 @@ public function addItems(array $items) : self return $this; } - public function addStaticItem(string $text) : self - { - $this->addMenuItem(new StaticItem($text)); - - return $this; - } - - public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self - { - $this->addMenuItem(new LineBreakItem($breakChar, $lines)); - - return $this; - } - public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self { $this->addMenuItem(new AsciiArtItem($art, $position, $alt)); @@ -146,19 +114,14 @@ public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITI } /** - * Add a submenu with a string identifier + * Add a split item */ - public function addSubMenu(string $id, CliMenuBuilder $subMenuBuilder = null) : CliMenuBuilder + public function addSplitItem() : SplitItemBuilder { - $this->menuItems[] = $id; - - if (null === $subMenuBuilder) { - $this->subMenuBuilders[$id] = new static($this); - return $this->subMenuBuilders[$id]; - } - - $this->subMenuBuilders[$id] = $subMenuBuilder; - return $this; + $this->menuItems[] = $id = uniqid('splititem-placeholder-', true); + + $this->splitItemBuilders[$id] = new SplitItemBuilder($this); + return $this->splitItemBuilders[$id]; } /** @@ -405,7 +368,7 @@ private function itemsHaveExtra(array $items) : bool * Recursively drop back to the parents menu style * when the current menu has a parent and has no changes */ - private function getMenuStyle() : MenuStyle + public function getMenuStyle() : MenuStyle { if (null === $this->parent) { return $this->buildStyle(); @@ -442,20 +405,6 @@ private function buildStyle() : MenuStyle return $style; } - /** - * Return to parent builder - * - * @throws RuntimeException - */ - public function end() : CliMenuBuilder - { - if (null === $this->parent) { - throw new RuntimeException('No parent builder to return to'); - } - - return $this->parent; - } - /** * @throws RuntimeException */ @@ -465,20 +414,20 @@ public function getSubMenu(string $id) : CliMenu throw new RuntimeException(sprintf('Menu: "%s" cannot be retrieved until menu has been built', $id)); } - return $this->subMenus[$id]; + return $this->subMenus['submenu-placeholder-' . $id]; } - - private function buildSubMenus(array $items) : array + + private function buildSplitItems(array $items) : array { return array_map(function ($item) { - if (!is_string($item)) { + if (!is_string($item) || 0 !== strpos($item, 'splititem-placeholder-')) { return $item; } - $menuBuilder = $this->subMenuBuilders[$item]; - $this->subMenus[$item] = $menuBuilder->build(); + $splitItemBuilder = $this->splitItemBuilders[$item]; + $this->splitItems[$item] = $splitItemBuilder->build(); - return new MenuMenuItem($item, $this->subMenus[$item], $menuBuilder->isMenuDisabled()); + return $this->splitItems[$item]; }, $items); } @@ -490,7 +439,9 @@ public function build() : CliMenu ? $this->menuItems : array_merge($this->menuItems, $this->getDefaultItems()); - $menuItems = $this->buildSubMenus($mergedItems); + + $menuItems = $this->buildSplitItems($mergedItems); + $menuItems = $this->buildSubMenus($menuItems); $this->style['displaysExtra'] = $this->itemsHaveExtra($menuItems); @@ -504,6 +455,10 @@ public function build() : CliMenu foreach ($this->subMenus as $subMenu) { $subMenu->setParent($menu); } + + foreach ($this->splitItemBuilders as $splitItemBuilder) { + $splitItemBuilder->setSubMenuParents($menu); + } return $menu; } diff --git a/src/MenuItem/SplitItem.php b/src/MenuItem/SplitItem.php new file mode 100644 index 00000000..148e7787 --- /dev/null +++ b/src/MenuItem/SplitItem.php @@ -0,0 +1,300 @@ + + */ +class SplitItem implements MenuItemInterface +{ + /** + * @var array + */ + private $items = []; + + /** + * @var int|null + */ + private $selectedItemIndex; + + /** + * @var bool + */ + private $canBeSelected = true; + + /** + * @var int + */ + private $margin = 2; + + /** + * @var array + */ + private static $blacklistedItems = [ + \PhpSchool\CliMenu\MenuItem\AsciiArtItem::class, + \PhpSchool\CliMenu\MenuItem\LineBreakItem::class, + \PhpSchool\CliMenu\MenuItem\SplitItem::class, + ]; + + public function __construct(array $items = []) + { + $this->addItems($items); + $this->setDefaultSelectedItem(); + } + + public function addItem(MenuItemInterface $item) : self + { + foreach (self::$blacklistedItems as $bl) { + if ($item instanceof $bl) { + throw new \InvalidArgumentException("Cannot add a $bl to a SplitItem"); + } + } + $this->items[] = $item; + $this->setDefaultSelectedItem(); + return $this; + } + + public function addItems(array $items) : self + { + foreach ($items as $item) { + $this->addItem($item); + } + + return $this; + } + + public function setItems(array $items) : self + { + $this->items = []; + $this->addItems($items); + return $this; + } + + /** + * Select default item + */ + private function setDefaultSelectedItem() : void + { + foreach ($this->items as $index => $item) { + if ($item->canSelect()) { + $this->canBeSelected = true; + $this->selectedItemIndex = $index; + return; + } + } + + $this->canBeSelected = false; + $this->selectedItemIndex = null; + } + + /** + * The output text for the item + */ + public function getRows(MenuStyle $style, bool $selected = false) : array + { + $numberOfItems = count($this->items); + + if ($numberOfItems === 0) { + throw new \RuntimeException(sprintf('There should be at least one item added to: %s', __CLASS__)); + } + + if (!$selected) { + $this->setDefaultSelectedItem(); + } + + $length = $style->getDisplaysExtra() + ? floor($style->getContentWidth() / $numberOfItems) - (mb_strlen($style->getItemExtra()) + 2) + : floor($style->getContentWidth() / $numberOfItems); + + $length -= $this->margin; + + $missingLength = $style->getContentWidth() % $numberOfItems; + + return $this->buildRows( + array_map(function ($index, $item) use ($selected, $length, $style) { + $isSelected = $selected && $index === $this->selectedItemIndex; + $marker = $item->canSelect() + ? sprintf('%s ', $style->getMarker($isSelected)) + : ''; + + $itemExtra = ''; + if ($style->getDisplaysExtra()) { + $itemExtra = $item->showsItemExtra() + ? sprintf(' %s', $style->getItemExtra()) + : sprintf(' %s', str_repeat(' ', mb_strlen($style->getItemExtra()))); + } + + return $this->buildCell( + explode("\n", StringUtil::wordwrap(sprintf('%s%s', $marker, $item->getText()), $length)), + $length, + $style, + $isSelected, + $itemExtra + ); + }, array_keys($this->items), $this->items), + $style, + $missingLength, + $length + ); + } + + private function buildRows(array $cells, MenuStyle $style, int $missingLength, int $length) : array + { + $extraPadLength = $style->getDisplaysExtra() ? 2 + mb_strlen($style->getItemExtra()) : 0; + + return array_map( + function ($i) use ($cells, $length, $missingLength, $extraPadLength) { + return $this->buildRow($cells, $i, $length, $missingLength, $extraPadLength); + }, + range(0, max(array_map('count', $cells)) - 1) + ); + } + + private function buildRow(array $cells, int $index, int $length, int $missingLength, int $extraPadLength) : string + { + return sprintf( + '%s%s', + implode( + '', + array_map( + function ($cell) use ($index, $length, $extraPadLength) { + return $cell[$index] ?? str_repeat(' ', $length + $this->margin + $extraPadLength); + }, + $cells + ) + ), + str_repeat(' ', $missingLength) + ); + } + + private function buildCell( + array $content, + int $length, + MenuStyle $style, + bool $isSelected, + string $itemExtra + ) : array { + return array_map(function ($row, $index) use ($length, $style, $isSelected, $itemExtra) { + $invertedColoursSetCode = $isSelected + ? $style->getInvertedColoursSetCode() + : ''; + $invertedColoursUnsetCode = $isSelected + ? $style->getInvertedColoursUnsetCode() + : ''; + + return sprintf( + '%s%s%s%s%s%s', + $invertedColoursSetCode, + $row, + str_repeat(' ', $length - mb_strlen($row)), + $index === 0 ? $itemExtra : str_repeat(' ', mb_strlen($itemExtra)), + $invertedColoursUnsetCode, + str_repeat(' ', $this->margin) + ); + }, $content, array_keys($content)); + } + + /** + * Is there an item with this index and can it be + * selected? + */ + public function canSelectIndex(int $index) : bool + { + return isset($this->items[$index]) && $this->items[$index]->canSelect(); + } + + /** + * Set the item index which should be selected. If the item does + * not exist then throw an exception. + */ + public function setSelectedItemIndex(int $index) : void + { + if (!isset($this->items[$index])) { + throw new \InvalidArgumentException(sprintf('Index: "%s" does not exist', $index)); + } + + $this->selectedItemIndex = $index; + } + + /** + * Get the currently select item index. + * May be null in case of no selectable item. + */ + public function getSelectedItemIndex() : ?int + { + return $this->selectedItemIndex; + } + + /** + * Get the currently selected item - if no items are selectable + * then throw an exception. + */ + public function getSelectedItem() : MenuItemInterface + { + if (null === $this->selectedItemIndex) { + throw new \RuntimeException('No item is selected'); + } + + return $this->items[$this->selectedItemIndex]; + } + + public function getItems() : array + { + return $this->items; + } + + /** + * Can the item be selected + * In this case, it indicates if at least 1 item inside the SplitItem can be selected + */ + public function canSelect() : bool + { + return $this->canBeSelected; + } + + /** + * Execute the items callable if required + */ + public function getSelectAction() : ?callable + { + return null; + } + + /** + * Whether or not the menu item is showing the menustyle extra value + */ + public function showsItemExtra() : bool + { + return false; + } + + /** + * Enable showing item extra + */ + public function showItemExtra() : void + { + //noop + } + + /** + * Disable showing item extra + */ + public function hideItemExtra() : void + { + //noop + } + + /** + * Nothing to return with SplitItem + */ + public function getText() : string + { + throw new \BadMethodCallException(sprintf('Not supported on: %s', __CLASS__)); + } +} diff --git a/src/SplitItemBuilder.php b/src/SplitItemBuilder.php new file mode 100644 index 00000000..47f7b610 --- /dev/null +++ b/src/SplitItemBuilder.php @@ -0,0 +1,43 @@ + + */ +class SplitItemBuilder implements Builder +{ + use BuilderUtils; + + public function __construct(Builder $parent) + { + $this->parent = $parent; + } + + public function build() : SplitItem + { + $items = $this->buildSubMenus($this->menuItems); + + return new SplitItem($items); + } + + public function setSubMenuParents(CliMenu $menu) : void + { + foreach ($this->subMenus as $subMenu) { + $subMenu->setParent($menu); + } + } + + public function getTerminal() : Terminal + { + return $this->parent->getTerminal(); + } + + public function getMenuStyle() : MenuStyle + { + return $this->parent->getMenuStyle(); + } +} diff --git a/src/Util/StringUtil.php b/src/Util/StringUtil.php index a0afbeed..ebd32b7f 100644 --- a/src/Util/StringUtil.php +++ b/src/Util/StringUtil.php @@ -11,19 +11,32 @@ class StringUtil * Minimal multi-byte wordwrap implementation * which also takes break length into consideration */ - public static function wordwrap(string $str, int $width, string $break = "\n") : string + public static function wordwrap(string $string, int $width, string $break = "\n") : string { - $length = 0; - return implode(' ', array_map(function ($word) use (&$length, $width, $break) { - $length += (mb_strlen($word) + 1); - - if ($length > $width) { - $length = mb_strlen($break); - return sprintf('%s%s', $break, $word); - } - - return $word; - }, explode(' ', $str))); + return implode( + $break, + array_map(function (string $line) use ($width, $break) { + $line = rtrim($line); + if (mb_strlen($line) <= $width) { + return $line; + } + + $words = explode(' ', $line); + $line = ''; + $actual = ''; + foreach ($words as $word) { + if (mb_strlen($actual . $word) <= $width) { + $actual .= $word . ' '; + } else { + if ($actual !== '') { + $line .= rtrim($actual) . $break; + } + $actual = $word . ' '; + } + } + return $line . trim($actual); + }, explode($break, $string)) + ); } public static function stripAnsiEscapeSequence(string $str) : string diff --git a/test/CliMenuTest.php b/test/CliMenuTest.php index 0bc271e1..48a14568 100644 --- a/test/CliMenuTest.php +++ b/test/CliMenuTest.php @@ -6,6 +6,8 @@ use PhpSchool\CliMenu\Exception\MenuNotOpenException; use PhpSchool\CliMenu\MenuItem\LineBreakItem; use PhpSchool\CliMenu\MenuItem\SelectableItem; +use PhpSchool\CliMenu\MenuItem\SplitItem; +use PhpSchool\CliMenu\MenuItem\StaticItem; use PhpSchool\CliMenu\MenuStyle; use PhpSchool\Terminal\Terminal; use PhpSchool\Terminal\UnixTerminal; @@ -667,6 +669,171 @@ public function testRemoveCustomControlMapping() : void self::assertSame([], $this->readAttribute($menu, 'customControlMappings')); } + public function testSplitItemWithNoSelectableItemsScrollingVertically() : void + { + $this->terminal->expects($this->exactly(3)) + ->method('read') + ->willReturn("\033[B", "\033[B", "\n"); + + $action = function (CliMenu $menu) { + $menu->close(); + }; + + $menu = new CliMenu('PHP School FTW', [], $this->terminal); + $menu->addItem(new SelectableItem('One', $action)); + $menu->addItem(new SplitItem([new StaticItem('Two'), new StaticItem('Three')])); + $menu->addItem(new SelectableItem('Four', $action)); + + $menu->open(); + + self::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); + } + + public function testSplitItemWithSelectableItemsScrollingVertical() : void + { + $this->terminal->expects($this->exactly(4)) + ->method('read') + ->willReturn("\033[B", "\033[B", "\033[B", "\n"); + + $action = function (CliMenu $menu) { + $menu->close(); + }; + + $splitAction = function (CliMenu $menu) { + }; + + $menu = new CliMenu('PHP School FTW', [], $this->terminal); + $menu->addItem(new SelectableItem('One', $action)); + $menu->addItem( + new SplitItem( + [new SelectableItem('Two', $splitAction), new SelectableItem('Three', $splitAction)] + ) + ); + $menu->addItem(new SelectableItem('Four', $action)); + + $menu->open(); + + self::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); + } + + public function testSplitItemWithSelectableItemsScrollingRight() : void + { + $this->terminal->expects($this->exactly(6)) + ->method('read') + ->willReturn("\033[B", "\033[C", "\033[C", "\033[C", "\033[B", "\n"); + + $action = function (CliMenu $menu) { + $menu->close(); + }; + + $splitAction = function (CliMenu $menu) { + }; + + $menu = new CliMenu('PHP School FTW', [], $this->terminal); + $menu->addItem(new SelectableItem('One', $action)); + $menu->addItem( + new SplitItem( + [new SelectableItem('Two', $splitAction), new SelectableItem('Three', $splitAction)] + ) + ); + $menu->addItem(new SelectableItem('Four', $action)); + + $menu->open(); + + self::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); + } + + public function testSplitItemWithSelectableItemsScrollingLeft() : void + { + $this->terminal->expects($this->exactly(6)) + ->method('read') + ->willReturn("\033[B", "\033[D", "\033[D", "\033[D", "\033[B", "\n"); + + $action = function (CliMenu $menu) { + $menu->close(); + }; + + $splitAction = function (CliMenu $menu) { + }; + + $menu = new CliMenu('PHP School FTW', [], $this->terminal); + $menu->addItem(new SelectableItem('One', $action)); + $menu->addItem( + new SplitItem( + [ + new SelectableItem('Two', $splitAction), + new SelectableItem('Three', $splitAction), + new SelectableItem('Four', $splitAction), + ] + ) + ); + $menu->addItem(new SelectableItem('Five', $action)); + + $menu->open(); + + self::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); + } + + public function testSplitItemWithSelectableAndStaticItemsScrollingHorizontally() : void + { + $this->terminal->expects($this->exactly(6)) + ->method('read') + ->willReturn("\033[B", "\033[D", "\033[D", "\033[D", "\033[B", "\n"); + + $action = function (CliMenu $menu) { + $menu->close(); + }; + + $splitAction = function (CliMenu $menu) { + }; + + $menu = new CliMenu('PHP School FTW', [], $this->terminal); + $menu->addItem(new SelectableItem('One', $action)); + $menu->addItem( + new SplitItem( + [ + new SelectableItem('Two', $splitAction), + new StaticItem('Three'), + new SelectableItem('Four', $splitAction), + ] + ) + ); + $menu->addItem(new SelectableItem('Five', $action)); + + $menu->open(); + + self::assertStringEqualsFile($this->getTestFile(), $this->output->fetch()); + } + + + public function testSelectableCallableReceivesSelectableAndNotSplitItem() : void + { + $this->terminal->expects($this->exactly(1)) + ->method('read') + ->willReturn("\n"); + + $actualSelectedItem = null; + $action = function (CliMenu $menu) use (&$actualSelectedItem) { + $actualSelectedItem = $menu->getSelectedItem(); + $menu->close(); + }; + + $expectedSelectedItem = new SelectableItem('Two', $action); + $menu = new CliMenu('PHP School FTW', [], $this->terminal); + $menu->addItem( + new SplitItem( + [ + $expectedSelectedItem, + new StaticItem('Three'), + new SelectableItem('Four', $action), + ] + ) + ); + $menu->open(); + + self::assertSame($expectedSelectedItem, $actualSelectedItem); + } + private function getTestFile() : string { return sprintf('%s/res/%s.txt', __DIR__, $this->getName()); diff --git a/test/MenuItem/MenuMenuItemTest.php b/test/MenuItem/MenuMenuItemTest.php index 8ffaae9c..e08b17f9 100644 --- a/test/MenuItem/MenuMenuItemTest.php +++ b/test/MenuItem/MenuMenuItemTest.php @@ -124,8 +124,8 @@ public function testGetRowsWithMultipleLines() : void $item = new MenuMenuItem('LONG ITEM LINE', $subMenu); $this->assertEquals( [ - " LONG ", - " ITEM LINE" + " LONG ITEM", + " LINE", ], $item->getRows($menuStyle) ); diff --git a/test/MenuItem/SelectableItemTest.php b/test/MenuItem/SelectableItemTest.php index 322e41b2..9c35a078 100644 --- a/test/MenuItem/SelectableItemTest.php +++ b/test/MenuItem/SelectableItemTest.php @@ -138,8 +138,8 @@ public function testGetRowsWithMultipleLinesWithItemExtra() : void }, true); $this->assertEquals( [ - " LONG [EXTRA]", - " ITEM LINE" + " LONG ITEM [EXTRA]", + " LINE", ], $item->getRows($menuStyle) ); diff --git a/test/MenuItem/SplitItemTest.php b/test/MenuItem/SplitItemTest.php new file mode 100644 index 00000000..99b3aa8e --- /dev/null +++ b/test/MenuItem/SplitItemTest.php @@ -0,0 +1,457 @@ + + */ +class SplitItemTest extends TestCase +{ + + /** + * @dataProvider blacklistedItemProvider + */ + public function testConstructWithBlacklistedItemTypeThrowsException(MenuItemInterface $menuItem) : void + { + self::expectExceptionMessage(\InvalidArgumentException::class); + self::expectExceptionMessage(sprintf('Cannot add a %s to a SplitItem', get_class($menuItem))); + + new SplitItem([$menuItem]); + } + + /** + * @dataProvider blacklistedItemProvider + */ + public function testAddItemsWithBlacklistedItemTypeThrowsException(MenuItemInterface $menuItem) : void + { + self::expectExceptionMessage(\InvalidArgumentException::class); + self::expectExceptionMessage(sprintf('Cannot add a %s to a SplitItem', get_class($menuItem))); + + (new SplitItem([]))->addItems([$menuItem]); + } + + /** + * @dataProvider blacklistedItemProvider + */ + public function testAddItemWithBlacklistedItemTypeThrowsException(MenuItemInterface $menuItem) : void + { + self::expectExceptionMessage(\InvalidArgumentException::class); + self::expectExceptionMessage(sprintf('Cannot add a %s to a SplitItem', get_class($menuItem))); + + (new SplitItem([]))->addItem($menuItem); + } + + /** + * @dataProvider blacklistedItemProvider + */ + public function testSetItemsWithBlacklistedItemTypeThrowsException(MenuItemInterface $menuItem) : void + { + self::expectExceptionMessage(\InvalidArgumentException::class); + self::expectExceptionMessage(sprintf('Cannot add a %s to a SplitItem', get_class($menuItem))); + + (new SplitItem([]))->setItems([$menuItem]); + } + + public function blacklistedItemProvider() : array + { + return [ + [new AsciiArtItem('( ︶︿︶)_╭∩╮')], + [new LineBreakItem('*')], + [new SplitItem([])], + ]; + } + + public function testAddItem() : void + { + $item1 = new StaticItem('One'); + $item2 = new StaticItem('Two'); + $splitItem = new SplitItem(); + $splitItem->addItem($item1); + + self::assertEquals([$item1], $splitItem->getItems()); + + $splitItem->addItem($item2); + + self::assertEquals([$item1, $item2], $splitItem->getItems()); + } + + public function testAddItems() : void + { + $item1 = new StaticItem('One'); + $item2 = new StaticItem('Two'); + $splitItem = new SplitItem(); + $splitItem->addItems([$item1]); + + self::assertEquals([$item1], $splitItem->getItems()); + + $splitItem->addItems([$item2]); + + self::assertEquals([$item1, $item2], $splitItem->getItems()); + } + + public function testSetItems() : void + { + $item1 = new StaticItem('One'); + $item2 = new StaticItem('Two'); + $item3 = new StaticItem('Three'); + $splitItem = new SplitItem([$item1]); + $splitItem->setItems([$item2, $item3]); + + self::assertEquals([$item2, $item3], $splitItem->getItems()); + } + + public function testGetItems() : void + { + $item = new StaticItem('test'); + + self::assertEquals([], (new SplitItem([]))->getItems()); + self::assertEquals([$item], (new SplitItem([$item]))->getItems()); + } + + public function testGetSelectActionReturnsNull() : void + { + $item = new SplitItem([]); + $this->assertNull($item->getSelectAction()); + } + + public function testHideAndShowItemExtraHasNoEffect() : void + { + $item = new SplitItem([]); + + $this->assertFalse($item->showsItemExtra()); + $item->showItemExtra(); + $this->assertFalse($item->showsItemExtra()); + $item->hideItemExtra(); + $this->assertFalse($item->showsItemExtra()); + } + + public function testGetRowsWithStaticItems() : void + { + $menuStyle = $this->createMock(MenuStyle::class); + + $menuStyle + ->expects($this->any()) + ->method('getContentWidth') + ->will($this->returnValue(30)); + + $item = new SplitItem([new StaticItem('One'), new StaticItem('Two')]); + + self::assertEquals(['One Two '], $item->getRows($menuStyle)); + } + + public function testGetRowsWithOneItemSelected() : void + { + $menuStyle = $this->createMock(MenuStyle::class); + + $menuStyle + ->expects($this->any()) + ->method('getContentWidth') + ->will($this->returnValue(30)); + + $menuStyle + ->expects($this->any()) + ->method('getMarker') + ->willReturnMap([[true, '='], [false, '*']]); + + $item = new SplitItem( + [ + new SelectableItem('Item One', function () { + }), + new SelectableItem('Item Two', function () { + }) + ] + ); + + $item->setSelectedItemIndex(0); + + self::assertEquals(['= Item One * Item Two '], $item->getRows($menuStyle, true)); + } + + public function testGetRowsWithMultipleLineStaticItems() : void + { + $menuStyle = $this->createMock(MenuStyle::class); + + $menuStyle + ->expects($this->any()) + ->method('getContentWidth') + ->will($this->returnValue(30)); + + $item = new SplitItem([new StaticItem("Item\nOne"), new StaticItem("Item\nTwo")]); + + self::assertEquals( + [ + 'Item Item ', + 'One Two ', + ], + $item->getRows($menuStyle) + ); + } + + public function testGetRowsWithMultipleLinesWithUnSelectedMarker() : void + { + $menuStyle = $this->createMock(MenuStyle::class); + + $menuStyle + ->expects($this->any()) + ->method('getContentWidth') + ->will($this->returnValue(30)); + + $menuStyle + ->expects($this->any()) + ->method('getMarker') + ->with(false) + ->will($this->returnValue('*')); + + $item = new SplitItem( + [ + new SelectableItem("Item\nOne", function () { + }), + new SelectableItem("Item\nTwo", function () { + }) + ] + ); + + self::assertEquals( + [ + '* Item * Item ', + 'One Two ', + ], + $item->getRows($menuStyle) + ); + } + + public function testGetRowsWithMultipleLinesWithOneItemSelected() : void + { + $menuStyle = $this->createMock(MenuStyle::class); + + $menuStyle + ->expects($this->any()) + ->method('getContentWidth') + ->will($this->returnValue(30)); + + $menuStyle + ->expects($this->any()) + ->method('getMarker') + ->willReturnMap([[true, '='], [false, '*']]); + + $item = new SplitItem( + [ + new SelectableItem("Item\nOne", function () { + }), + new SelectableItem("Item\nTwo", function () { + }) + ] + ); + + $item->setSelectedItemIndex(0); + + self::assertEquals( + [ + '= Item * Item ', + 'One Two ', + ], + $item->getRows($menuStyle, true) + ); + } + + public function testGetRowsWithItemExtra() : void + { + $menuStyle = $this->createMock(MenuStyle::class); + + $menuStyle + ->expects($this->any()) + ->method('getContentWidth') + ->will($this->returnValue(50)); + + $menuStyle + ->expects($this->any()) + ->method('getItemExtra') + ->will($this->returnValue('[EXTRA]')); + + $menuStyle + ->expects($this->any()) + ->method('getDisplaysExtra') + ->willReturn(true); + + $item = new SplitItem( + [ + new SelectableItem('Item 1', function () { + }, true), + new SelectableItem('Item 2', function () { + }, true) + ] + ); + + self::assertEquals([' Item 1 [EXTRA] Item 2 [EXTRA] '], $item->getRows($menuStyle)); + } + + public function testGetRowsWithMultipleLinesWithItemExtra() : void + { + $menuStyle = $this->createMock(MenuStyle::class); + + $menuStyle + ->expects($this->any()) + ->method('getContentWidth') + ->will($this->returnValue(50)); + + $menuStyle + ->expects($this->any()) + ->method('getItemExtra') + ->will($this->returnValue('[EXTRA]')); + + $menuStyle + ->expects($this->any()) + ->method('getDisplaysExtra') + ->willReturn(true); + + $item = new SplitItem( + [ + new SelectableItem("Item 1\nItem 1", function () { + }, true), + new SelectableItem("Item 2\nItem 2", function () { + }, true) + ] + ); + + self::assertEquals( + [ + ' Item 1 [EXTRA] Item 2 [EXTRA] ', + 'Item 1 Item 2 ', + ], + $item->getRows($menuStyle) + ); + } + + public function testGetRowsWithMultipleLinesWithItemExtraOnOne() : void + { + $menuStyle = $this->createMock(MenuStyle::class); + + $menuStyle + ->expects($this->any()) + ->method('getContentWidth') + ->will($this->returnValue(50)); + + $menuStyle + ->expects($this->any()) + ->method('getItemExtra') + ->will($this->returnValue('[EXTRA]')); + + $menuStyle + ->expects($this->any()) + ->method('getDisplaysExtra') + ->willReturn(true); + + $item = new SplitItem( + [ + new SelectableItem("Item 1\nItem 1", function () { + }), + new SelectableItem("Item 2\nItem 2", function () { + }, true) + ] + ); + + self::assertEquals( + [ + ' Item 1 Item 2 [EXTRA] ', + 'Item 1 Item 2 ', + ], + $item->getRows($menuStyle) + ); + } + + public function testGetTextThrowsAnException() : void + { + self::expectException(\BadMethodCallException::class); + self::expectExceptionMessage(sprintf('Not supported on: %s', SplitItem::class)); + + (new SplitItem([]))->getText(); + } + + public function testGetRowsThrowsAnExceptionIfNoItemsWereAdded() : void + { + self::expectException(\RuntimeException::class); + self::expectExceptionMessage(sprintf('There should be at least one item added to: %s', SplitItem::class)); + + (new SplitItem([]))->getRows($this->createMock(MenuStyle::class)); + } + + public function testCanBeSelectedReturnsTrueWhenItContainsSelectableItems() : void + { + self::assertTrue((new SplitItem([new SelectableItem('One', 'strlen')]))->canSelect()); + } + + public function testCanBeSelectedReturnsFalseWhenItContainsNoSelectableItems() : void + { + self::assertFalse((new SplitItem([new StaticItem('One')]))->canSelect()); + } + + public function testGetSelectedItemIndexWhenSelectableItemExists() : void + { + $item1 = new StaticItem('One'); + $item2 = new SelectableItem('Two', function () { + }); + + $splitItem = new SplitItem([$item1, $item2]); + + self::assertEquals(1, $splitItem->getSelectedItemIndex()); + } + + public function testGetSelectedItemIndexWhenNoSelectableItemExists() : void + { + $item1 = new StaticItem('One'); + $splitItem = new SplitItem([$item1]); + + self::assertNull($splitItem->getSelectedItemIndex()); + } + + public function testSetSelectedItemIndexThrowsExceptionIsIndexDoesNotExist() : void + { + self::expectException(\InvalidArgumentException::class); + self::expectExceptionMessage('Index: "2" does not exist'); + + (new SplitItem([]))->setSelectedItemIndex(2); + } + + public function testGetSelectedItemReturnsItem() : void + { + $item1 = new StaticItem('One'); + $item2 = new SelectableItem('Two', function () { + }); + + $splitItem = new SplitItem([$item1, $item2]); + self::assertSame($item2, $splitItem->getSelectedItem()); + } + + public function testGetSelectedItemThrowsExceptionWhenNoSelectableItemExists() : void + { + self::expectException(\RuntimeException::class); + self::expectExceptionMessage('No item is selected'); + + $item1 = new StaticItem('One'); + + $splitItem = new SplitItem([$item1]); + self::assertSame($splitItem, $splitItem->getSelectedItem()); + } + + public function testCanSelectIndex() : void + { + $item1 = new StaticItem('One'); + $item2 = new SelectableItem('Two', function () { + }); + + $splitItem = new SplitItem([$item1, $item2]); + + self::assertFalse($splitItem->canSelectIndex(0)); + self::assertFalse($splitItem->canSelectIndex(5)); + self::assertTrue($splitItem->canSelectIndex(1)); + } +} diff --git a/test/MenuItem/StaticItemTest.php b/test/MenuItem/StaticItemTest.php index aa199424..c1ffca46 100644 --- a/test/MenuItem/StaticItemTest.php +++ b/test/MenuItem/StaticItemTest.php @@ -64,7 +64,7 @@ public function testGetRowsWithContentWhichDoesNotFitOnOneLineIsWrapped() : void $item = new StaticItem('CONTENT 1 LINE'); $this->assertEquals( - ['CONTENT 1 ', 'LINE'], + ['CONTENT 1', 'LINE'], $item->getRows($menuStyle) ); } diff --git a/test/Util/StringUtilTest.php b/test/Util/StringUtilTest.php index 4c0c9459..7760a2aa 100644 --- a/test/Util/StringUtilTest.php +++ b/test/Util/StringUtilTest.php @@ -12,50 +12,62 @@ class StringUtilTest extends TestCase { protected $dummyText; + protected function setup() : void + { + parent::setUp(); + + $this->dummyText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor ' . + 'incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud ' . + 'exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor ' . + 'in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' . + 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt ' . + 'mollit anim id est laborum'; + } + public function testItWrapsAsExpectedTo80Length() : void { $result = StringUtil::wordwrap($this->dummyText, 80); - $expected = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \n" . - "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \n" . - "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor \n" . - "in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \n" . - "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \n" . - "mollit anim id est laborum"; - - $this->assertEquals($result, $expected); + $expected = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor\n" . + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis\n" . + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" . + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu\n" . + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in\n" . + "culpa qui officia deserunt mollit anim id est laborum"; + + self::assertEquals($expected, $result); } public function testItWrapsAsExpectedTo60Length() : void { $result = StringUtil::wordwrap($this->dummyText, 60); - $expected = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \n" . - "sed do eiusmod tempor incididunt ut labore et dolore magna \n" . - "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco \n" . - "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure \n" . - "dolor in reprehenderit in voluptate velit esse cillum dolore eu \n" . - "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non \n" . - "proident, sunt in culpa qui officia deserunt mollit anim id est \n" . - "laborum"; - - $this->assertEquals($result, $expected); + $expected = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed\n" . + "do eiusmod tempor incididunt ut labore et dolore magna\n" . + "aliqua. Ut enim ad minim veniam, quis nostrud exercitation\n" . + "ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" . + "Duis aute irure dolor in reprehenderit in voluptate velit\n" . + "esse cillum dolore eu fugiat nulla pariatur. Excepteur sint\n" . + "occaecat cupidatat non proident, sunt in culpa qui officia\n" . + "deserunt mollit anim id est laborum"; + + self::assertEquals($expected, $result); } public function testItCanUseACustomBreakCharacter() : void { - $result = StringUtil::wordwrap($this->dummyText, 60, 'H'); - $expected = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, H" . - "sed do eiusmod tempor incididunt ut labore et dolore magna H" . - "aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco H" . - "laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure H" . - "dolor in reprehenderit in voluptate velit esse cillum dolore eu H" . - "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non H" . - "proident, sunt in culpa qui officia deserunt mollit anim id est H" . - "laborum"; - - $this->assertEquals($result, $expected); + + $expected = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sedH" . + "do eiusmod tempor incididunt ut labore et dolore magnaH" . + "aliqua. Ut enim ad minim veniam, quis nostrud exercitationH" . + "ullamco laboris nisi ut aliquip ex ea commodo consequat.H" . + "Duis aute irure dolor in reprehenderit in voluptate velitH" . + "esse cillum dolore eu fugiat nulla pariatur. Excepteur sintH" . + "occaecat cupidatat non proident, sunt in culpa qui officiaH" . + "deserunt mollit anim id est laborum"; + + self::assertEquals($expected, $result); } public function testItCanStripAnsiEscapeSequence() : void @@ -70,15 +82,13 @@ public function testItCanStripAnsiEscapeSequence() : void $this->assertEquals('foobarbaz!!!', $result); } - protected function setup() : void + public function testSplitItemBug() : void { - parent::setUp(); - - $this->dummyText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor ' . - 'incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud ' . - 'exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor ' . - 'in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' . - 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt ' . - 'mollit anim id est laborum'; + $test = 'Item three I guess it isn\'t that bad, is it ?'; + + self::assertEquals( + "Item three\nI guess it\nisn't that\nbad, is it\n?", + StringUtil::wordwrap($test, 11) + ); } } diff --git a/test/res/testSplitItemWithNoSelectableItemsScrollingVertically.txt b/test/res/testSplitItemWithNoSelectableItemsScrollingVertically.txt new file mode 100644 index 00000000..a0c4054c --- /dev/null +++ b/test/res/testSplitItemWithNoSelectableItemsScrollingVertically.txt @@ -0,0 +1,33 @@ + + +   +  PHP School FTW  +  ==========================================  +  ● One  +  Two Three  +  ○ Four  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  Two Three  +  ● Four  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ● One  +  Two Three  +  ○ Four  +   + + diff --git a/test/res/testSplitItemWithSelectableAndStaticItemsScrollingHorizontally.txt b/test/res/testSplitItemWithSelectableAndStaticItemsScrollingHorizontally.txt new file mode 100644 index 00000000..a428a263 --- /dev/null +++ b/test/res/testSplitItemWithSelectableAndStaticItemsScrollingHorizontally.txt @@ -0,0 +1,66 @@ + + +   +  PHP School FTW  +  ==========================================  +  ● One  +  ○ Two Three ○ Four  +  ○ Five  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ● Two  Three ○ Four  +  ○ Five  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ○ Two Three ● Four   +  ○ Five  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ● Two  Three ○ Four  +  ○ Five  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ○ Two Three ● Four   +  ○ Five  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ○ Two Three ○ Four  +  ● Five  +   + + diff --git a/test/res/testSplitItemWithSelectableItemsScrollingLeft.txt b/test/res/testSplitItemWithSelectableItemsScrollingLeft.txt new file mode 100644 index 00000000..80db77a7 --- /dev/null +++ b/test/res/testSplitItemWithSelectableItemsScrollingLeft.txt @@ -0,0 +1,66 @@ + + +   +  PHP School FTW  +  ==========================================  +  ● One  +  ○ Two ○ Three ○ Four  +  ○ Five  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ● Two  ○ Three ○ Four  +  ○ Five  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ○ Two ○ Three ● Four   +  ○ Five  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ○ Two ● Three  ○ Four  +  ○ Five  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ● Two  ○ Three ○ Four  +  ○ Five  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ○ Two ○ Three ○ Four  +  ● Five  +   + + diff --git a/test/res/testSplitItemWithSelectableItemsScrollingRight.txt b/test/res/testSplitItemWithSelectableItemsScrollingRight.txt new file mode 100644 index 00000000..e5bc3961 --- /dev/null +++ b/test/res/testSplitItemWithSelectableItemsScrollingRight.txt @@ -0,0 +1,66 @@ + + +   +  PHP School FTW  +  ==========================================  +  ● One  +  ○ Two ○ Three  +  ○ Four  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ● Two  ○ Three  +  ○ Four  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ○ Two ● Three   +  ○ Four  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ● Two  ○ Three  +  ○ Four  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ○ Two ● Three   +  ○ Four  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ○ Two ○ Three  +  ● Four  +   + + diff --git a/test/res/testSplitItemWithSelectableItemsScrollingVertical.txt b/test/res/testSplitItemWithSelectableItemsScrollingVertical.txt new file mode 100644 index 00000000..f68eff09 --- /dev/null +++ b/test/res/testSplitItemWithSelectableItemsScrollingVertical.txt @@ -0,0 +1,44 @@ + + +   +  PHP School FTW  +  ==========================================  +  ● One  +  ○ Two ○ Three  +  ○ Four  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ● Two  ○ Three  +  ○ Four  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ○ One  +  ○ Two ○ Three  +  ● Four  +   + + + + +   +  PHP School FTW  +  ==========================================  +  ● One  +  ○ Two ○ Three  +  ○ Four  +   + +