diff --git a/docs/assets/widget-select-groups-dark-static-ascii-no-ansi.svg b/docs/assets/widget-select-groups-dark-static-ascii-no-ansi.svg index 9a9028d0..b85d8489 100644 --- a/docs/assets/widget-select-groups-dark-static-ascii-no-ansi.svg +++ b/docs/assets/widget-select-groups-dark-static-ascii-no-ansi.svg @@ -1 +1 @@ -+------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Rhubarb(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file ++------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Cherry(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file diff --git a/docs/assets/widget-select-groups-dark-static-ascii.svg b/docs/assets/widget-select-groups-dark-static-ascii.svg index 5c4bbbe3..4a83e980 100644 --- a/docs/assets/widget-select-groups-dark-static-ascii.svg +++ b/docs/assets/widget-select-groups-dark-static-ascii.svg @@ -1 +1 @@ -+------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Rhubarb(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file ++------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Cherry(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file diff --git a/docs/assets/widget-select-groups-dark-static-no-ansi.svg b/docs/assets/widget-select-groups-dark-static-no-ansi.svg index 3cdaf868..9d7aeae7 100644 --- a/docs/assets/widget-select-groups-dark-static-no-ansi.svg +++ b/docs/assets/widget-select-groups-dark-static-no-ansi.svg @@ -1 +1 @@ -├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Rhubarb(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Cherry(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-select-groups-dark-static.svg b/docs/assets/widget-select-groups-dark-static.svg index 8722fef3..70311547 100644 --- a/docs/assets/widget-select-groups-dark-static.svg +++ b/docs/assets/widget-select-groups-dark-static.svg @@ -1 +1 @@ -├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Rhubarb(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Cherry(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-select-groups-light-static-ascii-no-ansi.svg b/docs/assets/widget-select-groups-light-static-ascii-no-ansi.svg index 12cb0c7e..d34b59a6 100644 --- a/docs/assets/widget-select-groups-light-static-ascii-no-ansi.svg +++ b/docs/assets/widget-select-groups-light-static-ascii-no-ansi.svg @@ -1 +1 @@ -+------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Rhubarb(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file ++------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Cherry(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file diff --git a/docs/assets/widget-select-groups-light-static-ascii.svg b/docs/assets/widget-select-groups-light-static-ascii.svg index 00b5159f..3c70b31b 100644 --- a/docs/assets/widget-select-groups-light-static-ascii.svg +++ b/docs/assets/widget-select-groups-light-static-ascii.svg @@ -1 +1 @@ -+------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Rhubarb(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file ++------------------------------------------+|||Selectwithgroups>Select||>FruitFruit||(*)Apple||()Banana||-------------------------------||()Cherry(outofseason)||^/vmove*<accept*esccancel| \ No newline at end of file diff --git a/docs/assets/widget-select-groups-light-static-no-ansi.svg b/docs/assets/widget-select-groups-light-static-no-ansi.svg index 6e440c37..e1c83eab 100644 --- a/docs/assets/widget-select-groups-light-static-no-ansi.svg +++ b/docs/assets/widget-select-groups-light-static-no-ansi.svg @@ -1 +1 @@ -├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Rhubarb(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Cherry(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-select-groups-light-static.svg b/docs/assets/widget-select-groups-light-static.svg index d77f45bc..62554965 100644 --- a/docs/assets/widget-select-groups-light-static.svg +++ b/docs/assets/widget-select-groups-light-static.svg @@ -1 +1 @@ -├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Rhubarb(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────┤╭──────────────────────────────────────────╮SelectwithgroupsSelectFruitFruitAppleBanana───────────────────────────────Cherry(outofseason)↑/↓move·accept·esccancel╰──────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-table-dark-animated-ascii-no-ansi.svg b/docs/assets/widget-table-dark-animated-ascii-no-ansi.svg index 04bdc807..b7ca3947 100644 --- a/docs/assets/widget-table-dark-animated-ascii-no-ansi.svg +++ b/docs/assets/widget-table-dark-animated-ascii-no-ansi.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+------------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file ++--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+-------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file diff --git a/docs/assets/widget-table-dark-animated-ascii.svg b/docs/assets/widget-table-dark-animated-ascii.svg index b7bf3e94..b8973bd4 100644 --- a/docs/assets/widget-table-dark-animated-ascii.svg +++ b/docs/assets/widget-table-dark-animated-ascii.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+------------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file ++--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+-------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file diff --git a/docs/assets/widget-table-dark-animated-no-ansi.svg b/docs/assets/widget-table-dark-animated-no-ansi.svg index 6d45c1f6..72114cff 100644 --- a/docs/assets/widget-table-dark-animated-no-ansi.svg +++ b/docs/assets/widget-table-dark-animated-no-ansi.svg @@ -1 +1 @@ -╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file +╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file diff --git a/docs/assets/widget-table-dark-animated.svg b/docs/assets/widget-table-dark-animated.svg index 38fcc5f5..0134c70b 100644 --- a/docs/assets/widget-table-dark-animated.svg +++ b/docs/assets/widget-table-dark-animated.svg @@ -1 +1 @@ -╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file +╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file diff --git a/docs/assets/widget-table-dark-static-ascii-no-ansi.svg b/docs/assets/widget-table-dark-static-ascii-no-ansi.svg index b7b329c0..fdda2cf3 100644 --- a/docs/assets/widget-table-dark-static-ascii-no-ansi.svg +++ b/docs/assets/widget-table-dark-static-ascii-no-ansi.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||+------------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file ++--------------------------------------------------------------------------+|||+-------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file diff --git a/docs/assets/widget-table-dark-static-ascii.svg b/docs/assets/widget-table-dark-static-ascii.svg index fcf3fa41..9735b91c 100644 --- a/docs/assets/widget-table-dark-static-ascii.svg +++ b/docs/assets/widget-table-dark-static-ascii.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||+------------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file ++--------------------------------------------------------------------------+|||+-------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file diff --git a/docs/assets/widget-table-dark-static-no-ansi.svg b/docs/assets/widget-table-dark-static-no-ansi.svg index 772ee6d4..e4454614 100644 --- a/docs/assets/widget-table-dark-static-no-ansi.svg +++ b/docs/assets/widget-table-dark-static-no-ansi.svg @@ -1 +1 @@ -├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-table-dark-static.svg b/docs/assets/widget-table-dark-static.svg index 1a7d1fc1..d78480df 100644 --- a/docs/assets/widget-table-dark-static.svg +++ b/docs/assets/widget-table-dark-static.svg @@ -1 +1 @@ -├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-table-light-animated-ascii-no-ansi.svg b/docs/assets/widget-table-light-animated-ascii-no-ansi.svg index c1c190b1..005e8b97 100644 --- a/docs/assets/widget-table-light-animated-ascii-no-ansi.svg +++ b/docs/assets/widget-table-light-animated-ascii-no-ansi.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+------------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file ++--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+-------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file diff --git a/docs/assets/widget-table-light-animated-ascii.svg b/docs/assets/widget-table-light-animated-ascii.svg index aebc6a6a..96056dde 100644 --- a/docs/assets/widget-table-light-animated-ascii.svg +++ b/docs/assets/widget-table-light-animated-ascii.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+------------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file ++--------------------------------------------------------------------------+|||^/vmove*<select*escback*qquit*?help||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:||+-------+--------+----------+|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||Tablewidget||>Stock>||[Submit][Cancel]| \ No newline at end of file diff --git a/docs/assets/widget-table-light-animated-no-ansi.svg b/docs/assets/widget-table-light-animated-no-ansi.svg index c67932c1..42d3776f 100644 --- a/docs/assets/widget-table-light-animated-no-ansi.svg +++ b/docs/assets/widget-table-light-animated-no-ansi.svg @@ -1 +1 @@ -╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file +╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file diff --git a/docs/assets/widget-table-light-animated.svg b/docs/assets/widget-table-light-animated.svg index 4da66f5f..e23a844d 100644 --- a/docs/assets/widget-table-light-animated.svg +++ b/docs/assets/widget-table-light-animated.svg @@ -1 +1 @@ -╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file +╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯TablewidgetStock[Submit][Cancel] \ No newline at end of file diff --git a/docs/assets/widget-table-light-static-ascii-no-ansi.svg b/docs/assets/widget-table-light-static-ascii-no-ansi.svg index b74f5be8..ccf37f76 100644 --- a/docs/assets/widget-table-light-static-ascii-no-ansi.svg +++ b/docs/assets/widget-table-light-static-ascii-no-ansi.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||+------------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file ++--------------------------------------------------------------------------+|||+-------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file diff --git a/docs/assets/widget-table-light-static-ascii.svg b/docs/assets/widget-table-light-static-ascii.svg index d3af670b..a6776726 100644 --- a/docs/assets/widget-table-light-static-ascii.svg +++ b/docs/assets/widget-table-light-static-ascii.svg @@ -1 +1 @@ -+--------------------------------------------------------------------------+|||+------------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Blackberry|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file ++--------------------------------------------------------------------------+|||+-------+--------+----------+||Tablewidget>Stock||Basketcontents||Everythingpickedsofar:|||Fruit|Colour|Instock||||Apple|Red|12||||Pear|Green|5||||Plum|Purple|120|||^/vmove*<select*escback*qquit*?help| \ No newline at end of file diff --git a/docs/assets/widget-table-light-static-no-ansi.svg b/docs/assets/widget-table-light-static-no-ansi.svg index 751d762b..7585ec9b 100644 --- a/docs/assets/widget-table-light-static-no-ansi.svg +++ b/docs/assets/widget-table-light-static-no-ansi.svg @@ -1 +1 @@ -├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/assets/widget-table-light-static.svg b/docs/assets/widget-table-light-static.svg index d74ed079..7dd07c9c 100644 --- a/docs/assets/widget-table-light-static.svg +++ b/docs/assets/widget-table-light-static.svg @@ -1 +1 @@ -├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭────────────┬────────┬──────────╮FruitColourInstock├────────────┼────────┼──────────┤AppleRed12PearGreen5BlackberryPurple120╰────────────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file +├──────────────────────────────────────────────────────────────────────────┤╭──────────────────────────────────────────────────────────────────────────╮TablewidgetStockBasketcontentsEverythingpickedsofar:╭───────┬────────┬──────────╮FruitColourInstock├───────┼────────┼──────────┤AppleRed12PearGreen5PlumPurple120╰───────┴────────┴──────────╯↑/↓move·select·escback·qquit·?help╰──────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/docs/content/widgets/option-groups.mdx b/docs/content/widgets/option-groups.mdx index 73c6d881..feec605e 100644 --- a/docs/content/widgets/option-groups.mdx +++ b/docs/content/widgets/option-groups.mdx @@ -19,7 +19,7 @@ $p->select('item', 'Item') ->separator() // A non-selectable divider row. ->heading('Vegetable') ->option('carrot', 'Carrot') - ->option('rhubarb', 'Rhubarb', disabled: TRUE, disabled_reason: 'out of season'); + ->option('cherry', 'Cherry', disabled: TRUE, disabled_reason: 'out of season'); ``` Runnable scripts: [`playground/02-widgets-select-groups.php`](https://github.com/drevops/tui/blob/main/playground/02-widgets-select-groups.php) and [`select-multiple-groups.php`](https://github.com/drevops/tui/blob/main/playground/02-widgets-select-multiple-groups.php). diff --git a/docs/content/widgets/table.mdx b/docs/content/widgets/table.mdx index 8921498f..9d9c2634 100644 --- a/docs/content/widgets/table.mdx +++ b/docs/content/widgets/table.mdx @@ -21,7 +21,7 @@ $p->note('stock', 'Basket contents') ->table(['Fruit', 'Colour', 'In stock'], [ ['Apple', 'Red', '12'], ['Pear', 'Green', '5'], - ['Blackberry', 'Purple', '120'], + ['Plum', 'Purple', '120'], ]); ``` diff --git a/docs/util/render-widget-svgs.php b/docs/util/render-widget-svgs.php index 53263ffe..cff53633 100644 --- a/docs/util/render-widget-svgs.php +++ b/docs/util/render-widget-svgs.php @@ -238,7 +238,7 @@ function widgetSpecs(string $tree): array { $p->note('stock', 'Basket contents')->description('Everything picked so far:')->table(['Fruit', 'Colour', 'In stock'], [ ['Apple', 'Red', '12'], ['Pear', 'Green', '5'], - ['Blackberry', 'Purple', '120'], + ['Plum', 'Purple', '120'], ]); }), 'keys' => [$enter], diff --git a/docs/util/update-assets.php b/docs/util/update-assets.php index ed95553e..7707ebb1 100644 --- a/docs/util/update-assets.php +++ b/docs/util/update-assets.php @@ -1282,7 +1282,7 @@ function getJobs(string $project_dir): array { // static frame is anchored to an option only visible once the list is open // (the hub shows the form title too, so gating on it would capture the hub). $group_demos = [ - 'select-groups' => ['gate' => 'Select with groups', 'needle' => 'Rhubarb', 'rows' => 18], + 'select-groups' => ['gate' => 'Select with groups', 'needle' => 'Cherry', 'rows' => 18], 'select-multiple-groups' => ['gate' => 'MultiSelect with groups', 'needle' => 'Leek', 'rows' => 21], ]; diff --git a/playground/02-widgets-select-groups.php b/playground/02-widgets-select-groups.php index 1f33a5d6..0160002d 100644 --- a/playground/02-widgets-select-groups.php +++ b/playground/02-widgets-select-groups.php @@ -30,7 +30,7 @@ ->option('apple', 'Apple') ->option('banana', 'Banana') ->separator() - ->option('rhubarb', 'Rhubarb', disabled: TRUE, disabled_reason: 'out of season'); + ->option('cherry', 'Cherry', disabled: TRUE, disabled_reason: 'out of season'); }); try { diff --git a/playground/02-widgets-table.php b/playground/02-widgets-table.php index 2107ab15..2d0328d1 100644 --- a/playground/02-widgets-table.php +++ b/playground/02-widgets-table.php @@ -28,7 +28,7 @@ $rows = [ ['Apple', 'Red', '12'], ['Pear', 'Green', '5'], - ['Blackberry', 'Purple', '120'], + ['Plum', 'Purple', '120'], ]; $form = Form::create('Table') diff --git a/playground/11-display-modes-glyph-gallery.php b/playground/11-display-modes-glyph-gallery.php index b3a9cdf5..b43f8487 100644 --- a/playground/11-display-modes-glyph-gallery.php +++ b/playground/11-display-modes-glyph-gallery.php @@ -110,11 +110,11 @@ return implode("\n", $rows); }; -echo "\n"; -echo $columns('UNICODE', 'TEXTUAL (ASCII)') . "\n"; -echo str_repeat('-', 60) . "\n\n"; +echo PHP_EOL; +echo $columns('UNICODE', 'TEXTUAL (ASCII)') . PHP_EOL; +echo str_repeat('-', 60) . PHP_EOL . PHP_EOL; foreach ($widgets as $name => $make) { - echo $name . "\n"; - echo $columns($make()->view($unicode), $make()->view($ascii)) . "\n\n"; + echo $name . PHP_EOL; + echo $columns($make()->view($unicode), $make()->view($ascii)) . PHP_EOL . PHP_EOL; } diff --git a/playground/16-loading-data.php b/playground/16-loading-data.php index 27c7de28..2a1ce8d8 100644 --- a/playground/16-loading-data.php +++ b/playground/16-loading-data.php @@ -44,5 +44,6 @@ echo (new Tui($form))->run()->toJson() . PHP_EOL; } catch (InterruptException) { + // Leave quietly on Ctrl-C. exit(130); } diff --git a/playground/17-query-options.php b/playground/17-query-options.php index 450ff931..e695d209 100644 --- a/playground/17-query-options.php +++ b/playground/17-query-options.php @@ -57,5 +57,6 @@ echo (new Tui($form))->run()->toJson() . PHP_EOL; } catch (InterruptException) { + // Leave quietly on Ctrl-C. exit(130); } diff --git a/playground/18-output-text.php b/playground/18-output-text.php index 1079aeab..561aa88a 100644 --- a/playground/18-output-text.php +++ b/playground/18-output-text.php @@ -45,6 +45,6 @@ $out->text(''); // The markdown subset: bold, emphasis, inline code and bullet lists. -$out->text("**Before you order:**\n\n- Pick a *delivery day* between Monday and Saturday\n- Leave a note if the gate code is not `1234`\n- Cancel any time before seven the evening before"); +$out->text('**Before you order:**' . chr(10) . chr(10) . '- Pick a *delivery day* between Monday and Saturday' . chr(10) . '- Leave a note if the gate code is not `1234`' . chr(10) . '- Cancel any time before seven the evening before'); $out->rule(); diff --git a/playground/19-dynamic-options.php b/playground/19-dynamic-options.php index 7664ca84..8be50769 100644 --- a/playground/19-dynamic-options.php +++ b/playground/19-dynamic-options.php @@ -58,5 +58,6 @@ echo (new Tui($form))->run()->toJson() . PHP_EOL; } catch (InterruptException) { + // Leave quietly on Ctrl-C. exit(130); } diff --git a/playground/themes/OceanTheme.php b/playground/themes/OceanTheme.php index e673267c..cee1f173 100644 --- a/playground/themes/OceanTheme.php +++ b/playground/themes/OceanTheme.php @@ -5,10 +5,10 @@ namespace Playground\Themes; use DrevOps\Tui\Answers\Answers; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\ScopedKeyMap; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\Navigator; use DrevOps\Tui\Theme\DefaultTheme; diff --git a/rector.php b/rector.php index a40e6c4d..4afa86d9 100644 --- a/rector.php +++ b/rector.php @@ -25,6 +25,7 @@ use Rector\Naming\Rector\Foreach_\RenameForeachValueVariableToMatchExprVariableRector; use Rector\Naming\Rector\Foreach_\RenameForeachValueVariableToMatchMethodCallReturnTypeRector; use Rector\Naming\Rector\ClassMethod\RenameParamToMatchTypeRector; +use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; use Rector\Php80\Rector\Switch_\ChangeSwitchToMatchRector; use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; use Rector\PHPUnit\Set\PHPUnitSetList; @@ -56,6 +57,12 @@ // Rules added by Rector's rule sets. CatchExceptionNameMatchingTypeRector::class, ChangeSwitchToMatchRector::class, + // Promotion ties the constructor parameter name to the property name, but + // the two are named for different things here: the parameter for the seed + // value a caller passes, the property for the live answer it becomes. + ClassPropertyAssignToConstructorPromotionRector::class => [ + __DIR__ . '/src/Widget/ConfirmWidget.php', + ], CompleteDynamicPropertiesRector::class, CountArrayToEmptyArrayComparisonRector::class, DisallowedEmptyRuleFixerRector::class, diff --git a/src/Builder/FieldBuilder.php b/src/Builder/FieldBuilder.php index 2094dc6e..d69b722c 100644 --- a/src/Builder/FieldBuilder.php +++ b/src/Builder/FieldBuilder.php @@ -5,12 +5,14 @@ namespace DrevOps\Tui\Builder; use DrevOps\Tui\Condition\ConditionInterface; -use DrevOps\Tui\Model\FormException; +use DrevOps\Tui\Derive\Derive; +use DrevOps\Tui\Discovery\DiscoverInterface; use DrevOps\Tui\Model\DateBounds; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\FilePickerConstraints; use DrevOps\Tui\Model\FilePickerMode; +use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Model\Option; use DrevOps\Tui\Model\OptionKind; @@ -19,8 +21,6 @@ use DrevOps\Tui\Model\TableSpec; use DrevOps\Tui\Model\Template; use DrevOps\Tui\Model\Weekday; -use DrevOps\Tui\Derive\Derive; -use DrevOps\Tui\Discovery\DiscoverInterface; /** * A fluent builder for a single Field. diff --git a/src/Builder/Form.php b/src/Builder/Form.php index 6b01d4a2..fdd69841 100644 --- a/src/Builder/Form.php +++ b/src/Builder/Form.php @@ -6,9 +6,9 @@ use DrevOps\Tui\Model\Buttons; use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Fixup; use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Model\FormException; -use DrevOps\Tui\Model\Fixup; use DrevOps\Tui\Model\Option; use DrevOps\Tui\Model\Panel; diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index c85b3f93..3bcdb754 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -7,13 +7,13 @@ use DrevOps\Tui\Answers\Answers; use DrevOps\Tui\Answers\Provenance; use DrevOps\Tui\Condition\ConditionInterface; -use DrevOps\Tui\Model\FormDefinition; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\Option; use DrevOps\Tui\Derive\Deriver; use DrevOps\Tui\Discovery\DiscoverInterface; use DrevOps\Tui\Handler\Context; use DrevOps\Tui\Handler\HandlerRegistry; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FormDefinition; +use DrevOps\Tui\Model\Option; use DrevOps\Tui\Translation\Translator; /** diff --git a/src/Model/Modal.php b/src/Model/Modal.php index b0dcfed8..6d1e6f30 100644 --- a/src/Model/Modal.php +++ b/src/Model/Modal.php @@ -22,6 +22,9 @@ * * @param \DrevOps\Tui\Model\Buttons $buttons * The dialog's submit/cancel buttons. + * + * @throws \DrevOps\Tui\Model\FormException + * When the buttons are hidden. */ public function __construct( public Buttons $buttons = new Buttons(), @@ -29,7 +32,7 @@ public function __construct( // The buttons are a modal's only on-screen way out, so hiding them would // strand the dialog; the constructor rejects that rather than ignoring it. if (!$this->buttons->show) { - throw new \InvalidArgumentException('A modal dialog must show its buttons.'); + throw new FormException('A modal dialog must show its buttons.'); } } diff --git a/src/Render/PanelController.php b/src/Render/PanelController.php index 4d922063..ee9caabe 100644 --- a/src/Render/PanelController.php +++ b/src/Render/PanelController.php @@ -4,19 +4,12 @@ namespace DrevOps\Tui\Render; -use DrevOps\Tui\Derive\Derive; use DrevOps\Tui\Answers\Answers; use DrevOps\Tui\Answers\Provenance; +use DrevOps\Tui\Derive\Derive; use DrevOps\Tui\Engine\Engine; use DrevOps\Tui\Handler\Context; use DrevOps\Tui\Handler\HandlerRegistry; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\FormDefinition; -use DrevOps\Tui\Model\Option; -use DrevOps\Tui\Model\Panel; -use DrevOps\Tui\Model\RenderMode; -use DrevOps\Tui\Primitive\ProgressReporter; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; @@ -25,6 +18,13 @@ use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Input\KeyParser; use DrevOps\Tui\Input\ScopedKeyMap; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\FormDefinition; +use DrevOps\Tui\Model\Option; +use DrevOps\Tui\Model\Panel; +use DrevOps\Tui\Model\RenderMode; +use DrevOps\Tui\Primitive\ProgressReporter; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Widget\Capability\ExternalEditCapableInterface; diff --git a/src/Schema/SchemaGenerator.php b/src/Schema/SchemaGenerator.php index 3d77afff..1c9f07db 100644 --- a/src/Schema/SchemaGenerator.php +++ b/src/Schema/SchemaGenerator.php @@ -4,10 +4,10 @@ namespace DrevOps\Tui\Schema; +use DrevOps\Tui\Discovery\DiscoverInterface; use DrevOps\Tui\Handler\Context; -use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Discovery\DiscoverInterface; +use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Resolver\EnvNameResolver; /** diff --git a/src/Schema/SchemaValidator.php b/src/Schema/SchemaValidator.php index e4e9b240..d68bccf2 100644 --- a/src/Schema/SchemaValidator.php +++ b/src/Schema/SchemaValidator.php @@ -5,8 +5,8 @@ namespace DrevOps\Tui\Schema; use DrevOps\Tui\Handler\Context; -use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Translation\Translator; /** diff --git a/src/Testing/TuiTester.php b/src/Testing/TuiTester.php index 4c2f54d9..e0ba7ad7 100644 --- a/src/Testing/TuiTester.php +++ b/src/Testing/TuiTester.php @@ -6,8 +6,8 @@ use DrevOps\Tui\Answers\Answers; use DrevOps\Tui\Builder\Form; -use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Theme\Mode; use DrevOps\Tui\Tui; diff --git a/src/Theme/DefaultTheme.php b/src/Theme/DefaultTheme.php index 1ae472ae..14bcefda 100644 --- a/src/Theme/DefaultTheme.php +++ b/src/Theme/DefaultTheme.php @@ -7,17 +7,17 @@ use DrevOps\Tui\Answers\Answers; use DrevOps\Tui\Answers\Provenance; use DrevOps\Tui\Answers\ValueFormatter; +use DrevOps\Tui\Input\Action; +use DrevOps\Tui\Input\Hint; +use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Input\ScopedKeyMap; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Model\Modal; use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Model\TableSpec; -use DrevOps\Tui\Input\Action; -use DrevOps\Tui\Input\Hint; -use DrevOps\Tui\Input\Key; -use DrevOps\Tui\Input\KeyName; -use DrevOps\Tui\Input\ScopedKeyMap; use DrevOps\Tui\Primitive\Status; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\Box; @@ -1580,10 +1580,7 @@ protected function renderColumnBlock(Panel $panel, Answers $answers, bool $selec continue; } - // A grid cell is one physical row, so a multi-line value previews as - // its first line - an embedded newline would desync the column zip. - $value_lines = explode("\n", $this->normalizeLines($this->renderFieldValue($field, $answers->value($field->id)))); - $value = $value_lines[0] . (count($value_lines) > 1 ? '…' : ''); + $value = $this->columnValuePreview($field, $answers); $lines[] = $indent . ' ' . $this->description(Translator::t($field->label), $selected) . ' ' . $this->value($value, $selected); } @@ -1594,6 +1591,30 @@ protected function renderColumnBlock(Panel $panel, Answers $answers, bool $selec return $lines; } + /** + * A field's value as one grid cell: first line, marked when there is more. + * + * A grid cell is one physical row, so a multi-line value previews as its + * first line - an embedded newline would desync the column zip - followed by + * a marker so the cell does not read as the whole value. Rendering and + * measuring both route through here, so a column can never be sized without + * the room its marker needs. + * + * @param \DrevOps\Tui\Model\Field $field + * The field to preview. + * @param \DrevOps\Tui\Answers\Answers $answers + * The collected answers. + * + * @return string + * The previewed value. + */ + protected function columnValuePreview(Field $field, Answers $answers): string { + $value_lines = explode("\n", $this->normalizeLines($this->renderFieldValue($field, $answers->value($field->id)))); + $more = $this->unicode ? '…' : '...'; + + return $value_lines[0] . (count($value_lines) > 1 ? $more : ''); + } + /** * Render a field row, one entry per physical line. * @@ -1988,7 +2009,15 @@ public function summarizePanel(Panel $panel, Answers $answers): string { */ public function renderSummaryLine(string $summary, bool $selected): string { $max = max(1, $this->width - 4); - $clipped = Strings::length($summary) > $max ? Strings::substr($summary, 0, $max - 1) . '…' : $summary; + + if (Strings::length($summary) > $max) { + // Only the Unicode marker fits the budget in one column; ASCII clips to + // the full width instead, as a table cell does. + $clipped = $this->unicode ? Strings::substr($summary, 0, $max - 1) . '…' : Strings::substr($summary, 0, $max); + } + else { + $clipped = $summary; + } return ' ' . $this->value($clipped, $selected); } @@ -2169,7 +2198,7 @@ protected function measureColumnBlock(Panel $panel, Answers $answers): int { continue; } - $width = max($width, $indent + 4 + Markup::width(Translator::t($field->label), FALSE, $this->color) + $this->measureValueWidth($field, $answers)); + $width = max($width, $indent + 4 + Markup::width(Translator::t($field->label), FALSE, $this->color) + Ansi::width($this->columnValuePreview($field, $answers))); } foreach ($panel->panels as $subpanel) { @@ -2198,7 +2227,7 @@ protected function measureValueWidth(Field $field, Answers $answers): int { $width = 0; foreach (explode("\n", $this->normalizeLines($this->renderFieldValue($field, $answers->value($field->id)))) as $line) { - $width = max($width, Strings::length($line)); + $width = max($width, Ansi::width($line)); } return $width; diff --git a/src/Tui.php b/src/Tui.php index 692a78e5..f443d9a1 100644 --- a/src/Tui.php +++ b/src/Tui.php @@ -14,12 +14,12 @@ use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Primitive\Output; use DrevOps\Tui\Primitive\Progress; +use DrevOps\Tui\Render\PanelController; +use DrevOps\Tui\Render\Terminal; use DrevOps\Tui\Resolver\InputResolver; use DrevOps\Tui\Schema\AgentHelp; use DrevOps\Tui\Schema\SchemaGenerator; use DrevOps\Tui\Schema\SchemaValidator; -use DrevOps\Tui\Render\PanelController; -use DrevOps\Tui\Render\Terminal; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\Mode; use DrevOps\Tui\Theme\ThemeManager; diff --git a/src/Widget/CalendarWidget.php b/src/Widget/CalendarWidget.php index 0a1bd862..45cdddcc 100644 --- a/src/Widget/CalendarWidget.php +++ b/src/Widget/CalendarWidget.php @@ -4,15 +4,15 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\DateBounds; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Weekday; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Input\Scope; use DrevOps\Tui\Input\ScopedKeyMap; +use DrevOps\Tui\Model\DateBounds; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Weekday; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Utils\Strings; @@ -55,15 +55,15 @@ class CalendarWidget extends AbstractWidget implements StepCapableInterface { /** * Construct a calendar widget. * - * @param string $value + * @param string $default * The initial date as an ISO `Y-m-d` string; empty opens on today. * @param \DrevOps\Tui\Model\DateBounds|null $bounds * Optional min/max range and week-start day; NULL for an open range that * starts the week on Monday. */ - public function __construct(string $value = '', ?DateBounds $bounds = NULL) { + public function __construct(string $default = '', ?DateBounds $bounds = NULL) { $this->bounds = $bounds ?? new DateBounds(); - $seed = DateBounds::parse($value) ?? new \DateTimeImmutable('today'); + $seed = DateBounds::parse($default) ?? new \DateTimeImmutable('today'); $this->cursor = $this->bounds->clamp($seed); } diff --git a/src/Widget/Capability/SelectionCapableTrait.php b/src/Widget/Capability/SelectionCapableTrait.php index b407d143..b49bbd4b 100644 --- a/src/Widget/Capability/SelectionCapableTrait.php +++ b/src/Widget/Capability/SelectionCapableTrait.php @@ -4,12 +4,12 @@ namespace DrevOps\Tui\Widget\Capability; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Option; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Option; use DrevOps\Tui\Theme\ThemeInterface; /** diff --git a/src/Widget/ConfirmWidget.php b/src/Widget/ConfirmWidget.php index 35a588bb..16af2a9b 100644 --- a/src/Widget/ConfirmWidget.php +++ b/src/Widget/ConfirmWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Widget\Capability\StepCapableInterface; @@ -20,13 +20,19 @@ */ class ConfirmWidget extends AbstractWidget implements StepCapableInterface { + /** + * The chosen answer. + */ + protected bool $current; + /** * Construct a confirm widget. * - * @param bool $current + * @param bool $default * The initial choice. */ - public function __construct(protected bool $current = FALSE) { + public function __construct(bool $default = FALSE) { + $this->current = $default; } /** diff --git a/src/Widget/FilePickerWidget.php b/src/Widget/FilePickerWidget.php index 1dd3200b..e8188d81 100644 --- a/src/Widget/FilePickerWidget.php +++ b/src/Widget/FilePickerWidget.php @@ -4,15 +4,15 @@ namespace DrevOps\Tui\Widget; +use DrevOps\Tui\Input\Action; +use DrevOps\Tui\Input\Hint; +use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Input\Scope; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\FilePickerConstraints; use DrevOps\Tui\Model\FilePickerMode; use DrevOps\Tui\Model\SelectionBounds; -use DrevOps\Tui\Input\Action; -use DrevOps\Tui\Input\Hint; -use DrevOps\Tui\Input\Key; -use DrevOps\Tui\Input\Scope; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; @@ -569,11 +569,11 @@ protected function entries(): array { */ protected function sortFilter(array $names): array { if ($this->filter !== '') { - $needle = strtolower($this->filter); - $names = array_filter($names, static fn(string $name): bool => str_contains(strtolower($name), $needle)); + $needle = Strings::lower($this->filter); + $names = array_filter($names, static fn(string $name): bool => str_contains(Strings::lower($name), $needle)); } - usort($names, strcasecmp(...)); + usort($names, static fn(string $a, string $b): int => strcmp(Strings::lower($a), Strings::lower($b))); return $names; } diff --git a/src/Widget/NumberWidget.php b/src/Widget/NumberWidget.php index 22636204..d9b4ceca 100644 --- a/src/Widget/NumberWidget.php +++ b/src/Widget/NumberWidget.php @@ -4,12 +4,12 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Utils\Strings; @@ -38,13 +38,13 @@ class NumberWidget extends AbstractWidget implements TextEditCapableInterface, S /** * Construct a number widget. * - * @param string $buffer + * @param string $default * The initial value (and live input buffer). * @param \DrevOps\Tui\Model\NumberBounds|null $bounds * Optional bounds and step; NULL for a plain integer entry. */ - public function __construct(string $buffer = '', protected ?NumberBounds $bounds = NULL) { - $this->initTextBuffer($buffer); + public function __construct(string $default = '', protected ?NumberBounds $bounds = NULL) { + $this->initTextBuffer($default); } /** diff --git a/src/Widget/PasswordWidget.php b/src/Widget/PasswordWidget.php index 293f7d79..f78be192 100644 --- a/src/Widget/PasswordWidget.php +++ b/src/Widget/PasswordWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Utils\Strings; @@ -46,15 +46,15 @@ class PasswordWidget extends AbstractWidget implements TextEditCapableInterface, /** * Construct a password widget. * - * @param string $buffer + * @param string $default * The initial value (and live input buffer). * @param bool $revealable * Whether the reveal toggle is enabled. * @param bool $confirm * Whether confirmation mode is enabled. */ - public function __construct(string $buffer = '', protected bool $revealable = FALSE, protected bool $confirm = FALSE) { - $this->initTextBuffer($buffer); + public function __construct(string $default = '', protected bool $revealable = FALSE, protected bool $confirm = FALSE) { + $this->initTextBuffer($default); } /** diff --git a/src/Widget/PauseWidget.php b/src/Widget/PauseWidget.php index 9263c482..6c64ab35 100644 --- a/src/Widget/PauseWidget.php +++ b/src/Widget/PauseWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Translation\Translator; diff --git a/src/Widget/RatingWidget.php b/src/Widget/RatingWidget.php index bf25e01f..ac58d6ea 100644 --- a/src/Widget/RatingWidget.php +++ b/src/Widget/RatingWidget.php @@ -35,7 +35,7 @@ class RatingWidget extends AbstractWidget implements StepCapableInterface { /** * Construct a rating widget. * - * @param int $point + * @param int $default * The initially chosen point; clamped onto the scale. * @param int $min * The lowest point of the scale. @@ -44,8 +44,8 @@ class RatingWidget extends AbstractWidget implements StepCapableInterface { * @param array $captions * The caption of a point, keyed by the point; points may be uncaptioned. */ - public function __construct(int $point, protected int $min = 1, protected int $max = 5, protected array $captions = []) { - $this->point = $this->clamp($point); + public function __construct(int $default, protected int $min = 1, protected int $max = 5, protected array $captions = []) { + $this->point = $this->clamp($default); } /** diff --git a/src/Widget/ReorderWidget.php b/src/Widget/ReorderWidget.php index cbc06256..cdb7c823 100644 --- a/src/Widget/ReorderWidget.php +++ b/src/Widget/ReorderWidget.php @@ -4,13 +4,13 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Option; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Option; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Widget\Capability\OptionsCapableInterface; use DrevOps\Tui\Widget\Capability\PagingCapableInterface; diff --git a/src/Widget/SearchWidget.php b/src/Widget/SearchWidget.php index 069bb27b..17c3b56f 100644 --- a/src/Widget/SearchWidget.php +++ b/src/Widget/SearchWidget.php @@ -4,10 +4,10 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\SelectionBounds; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\SelectionBounds; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Widget\Capability\FilterCapableInterface; use DrevOps\Tui\Widget\Capability\FilterCapableTrait; diff --git a/src/Widget/SuggestWidget.php b/src/Widget/SuggestWidget.php index 32087d8d..211666be 100644 --- a/src/Widget/SuggestWidget.php +++ b/src/Widget/SuggestWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\Option; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Utils\Strings; @@ -40,6 +40,11 @@ class SuggestWidget extends AbstractWidget implements SearchCapableInterface, Te */ protected int $cursor = -1; + /** + * The live input buffer. + */ + protected string $buffer = ''; + /** * The buffer the memoized ranking was computed for, or NULL for none yet. */ @@ -57,7 +62,7 @@ class SuggestWidget extends AbstractWidget implements SearchCapableInterface, Te * * @param list $values * The suggestion values. - * @param string $buffer + * @param string $default * The initial input. * @param int|null $page_size * The number of suggestions shown at once before the list pages; NULL uses @@ -69,7 +74,8 @@ class SuggestWidget extends AbstractWidget implements SearchCapableInterface, Te * Whether the leading prefix match is previewed as inline ghost-text after * the caret; FALSE leaves the ranked list as the only completion. */ - public function __construct(protected array $values, protected string $buffer = '', ?int $page_size = NULL, protected array $descriptions = [], protected bool $ghost = FALSE) { + public function __construct(protected array $values, string $default = '', ?int $page_size = NULL, protected array $descriptions = [], protected bool $ghost = FALSE) { + $this->buffer = $default; $this->pageSize = $this->resolvePageSize($page_size); } diff --git a/src/Widget/TemplateWidget.php b/src/Widget/TemplateWidget.php index a78dae4d..c0f29c76 100644 --- a/src/Widget/TemplateWidget.php +++ b/src/Widget/TemplateWidget.php @@ -54,13 +54,13 @@ class TemplateWidget extends AbstractWidget implements TextEditCapableInterface * * @param \DrevOps\Tui\Model\Template $template * The shape to fill in. - * @param string $value + * @param string $default * The initial assembled value; a value that does not have the shape leaves * every slot empty. */ - public function __construct(protected Template $template, string $value = '') { + public function __construct(protected Template $template, string $default = '') { $this->names = $this->template->placeholders(); - $extracted = $this->template->extract($value); + $extracted = $this->template->extract($default); foreach ($this->names as $name) { $this->parts[$name] = $extracted[$name] ?? ''; diff --git a/src/Widget/TextWidget.php b/src/Widget/TextWidget.php index 47b3433c..f9a033a4 100644 --- a/src/Widget/TextWidget.php +++ b/src/Widget/TextWidget.php @@ -4,10 +4,10 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Widget\Capability\CompletionCapableInterface; use DrevOps\Tui\Widget\Capability\CompletionCapableTrait; @@ -30,14 +30,14 @@ class TextWidget extends AbstractWidget implements TextEditCapableInterface, Com /** * Construct a text widget. * - * @param string $buffer + * @param string $default * The initial value (and live input buffer). * @param list $completions * Inline ghost-text candidates: the buffer is completed to the first * candidate it is a prefix of. Empty leaves a plain text field. */ - public function __construct(string $buffer = '', protected array $completions = []) { - $this->initTextBuffer($buffer); + public function __construct(string $default = '', protected array $completions = []) { + $this->initTextBuffer($default); } /** diff --git a/src/Widget/TextareaWidget.php b/src/Widget/TextareaWidget.php index 64c9a8f6..de9fae5c 100644 --- a/src/Widget/TextareaWidget.php +++ b/src/Widget/TextareaWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Utils\Strings; use DrevOps\Tui\Widget\Capability\ExternalEditCapableInterface; @@ -35,13 +35,13 @@ class TextareaWidget extends AbstractWidget implements TextEditCapableInterface, /** * Construct a textarea widget. * - * @param string $buffer + * @param string $default * The initial value (and live input buffer). * @param bool $externalEdit * Whether the external-editor handoff is offered (an available $EDITOR). */ - public function __construct(string $buffer = '', protected bool $externalEdit = FALSE) { - $this->initTextBuffer($buffer); + public function __construct(string $default = '', protected bool $externalEdit = FALSE) { + $this->initTextBuffer($default); } /** diff --git a/src/Widget/ToggleWidget.php b/src/Widget/ToggleWidget.php index 92449ef5..334612f1 100644 --- a/src/Widget/ToggleWidget.php +++ b/src/Widget/ToggleWidget.php @@ -4,11 +4,11 @@ namespace DrevOps\Tui\Widget; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\Scope; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Theme\ThemeInterface; use DrevOps\Tui\Utils\Strings; use DrevOps\Tui\Widget\Capability\StepCapableInterface; diff --git a/src/Widget/WidgetFactory.php b/src/Widget/WidgetFactory.php index 8cc72bad..28e4fd40 100644 --- a/src/Widget/WidgetFactory.php +++ b/src/Widget/WidgetFactory.php @@ -5,13 +5,13 @@ namespace DrevOps\Tui\Widget; use DrevOps\Tui\Handler\HandlerRegistry; +use DrevOps\Tui\Input\KeyMap; +use DrevOps\Tui\Input\KeyMapManager; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Model\Option; use DrevOps\Tui\Model\Template; -use DrevOps\Tui\Input\KeyMap; -use DrevOps\Tui\Input\KeyMapManager; use DrevOps\Tui\Translation\Translator; use DrevOps\Tui\Widget\Capability\PlaceholderCapableInterface; use DrevOps\Tui\Widget\Capability\QueryOptionsCapableInterface; diff --git a/tests/phpunit/Traits/BuildsThemesTrait.php b/tests/phpunit/Traits/BuildsThemesTrait.php new file mode 100644 index 00000000..867ae833 --- /dev/null +++ b/tests/phpunit/Traits/BuildsThemesTrait.php @@ -0,0 +1,46 @@ + $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); + } + + /** + * A narrow, unstyled, borderless theme for frame and layout assertions. + * + * Independent of the theme's bordered-and-padded defaults, so an assertion + * reads against the content alone. + * + * @return \DrevOps\Tui\Theme\DefaultTheme + * The theme. + */ + protected function plainTheme(): DefaultTheme { + return new DefaultTheme(40, ['color' => FALSE, 'border' => Border::None, 'spacing' => Spacing::Normal]); + } + +} diff --git a/tests/phpunit/Unit/Builder/FormTest.php b/tests/phpunit/Unit/Builder/FormTest.php index 41d41193..eb424155 100644 --- a/tests/phpunit/Unit/Builder/FormTest.php +++ b/tests/phpunit/Unit/Builder/FormTest.php @@ -9,12 +9,14 @@ use DrevOps\Tui\Builder\LayoutGuard; use DrevOps\Tui\Builder\PanelBuilder; use DrevOps\Tui\Condition\Condition; -use DrevOps\Tui\Model\FormException; +use DrevOps\Tui\Derive\Derive; +use DrevOps\Tui\Discovery\Dotenv; use DrevOps\Tui\Model\DateBounds; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\FilePickerMode; use DrevOps\Tui\Model\Fixup; +use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Model\Modal; use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Model\OptionKind; @@ -23,8 +25,6 @@ use DrevOps\Tui\Model\TableSpec; use DrevOps\Tui\Model\Template; use DrevOps\Tui\Model\Weekday; -use DrevOps\Tui\Derive\Derive; -use DrevOps\Tui\Discovery\Dotenv; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; @@ -37,7 +37,7 @@ #[CoversClass(LayoutGuard::class)] #[CoversClass(PanelBuilder::class)] #[CoversClass(FieldBuilder::class)] -#[Group('model')] +#[Group('builder')] final class FormTest extends TestCase { public function testBuildsExpectedForm(): void { diff --git a/tests/phpunit/Unit/Derive/DeriveTest.php b/tests/phpunit/Unit/Derive/DeriveTest.php index ccdfdf9a..e2dc7475 100644 --- a/tests/phpunit/Unit/Derive/DeriveTest.php +++ b/tests/phpunit/Unit/Derive/DeriveTest.php @@ -4,8 +4,8 @@ namespace DrevOps\Tui\Tests\Unit\Derive; -use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Derive\Derive; +use DrevOps\Tui\Model\FormException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Engine/EngineConditionalTest.php b/tests/phpunit/Unit/Engine/EngineConditionalTest.php index a3267d3c..ec949703 100644 --- a/tests/phpunit/Unit/Engine/EngineConditionalTest.php +++ b/tests/phpunit/Unit/Engine/EngineConditionalTest.php @@ -8,11 +8,11 @@ use DrevOps\Tui\Builder\Form; use DrevOps\Tui\Builder\PanelBuilder; use DrevOps\Tui\Condition\Condition; -use DrevOps\Tui\Model\FormDefinition; -use DrevOps\Tui\Model\Fixup; use DrevOps\Tui\Engine\Engine; use DrevOps\Tui\Handler\Context; use DrevOps\Tui\Handler\HandlerRegistry; +use DrevOps\Tui\Model\Fixup; +use DrevOps\Tui\Model\FormDefinition; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; diff --git a/tests/phpunit/Unit/Input/HintTest.php b/tests/phpunit/Unit/Input/HintTest.php index e030dc21..73c87432 100644 --- a/tests/phpunit/Unit/Input/HintTest.php +++ b/tests/phpunit/Unit/Input/HintTest.php @@ -16,7 +16,7 @@ * Tests the key-hint fragment. */ #[CoversClass(Hint::class)] -#[Group('tui')] +#[Group('input')] final class HintTest extends TestCase { use ResetsTranslatorTrait; diff --git a/tests/phpunit/Unit/Input/KeyMapTest.php b/tests/phpunit/Unit/Input/KeyMapTest.php index ea0eb888..72fad473 100644 --- a/tests/phpunit/Unit/Input/KeyMapTest.php +++ b/tests/phpunit/Unit/Input/KeyMapTest.php @@ -4,7 +4,6 @@ namespace DrevOps\Tui\Tests\Unit\Input; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Binding; use DrevOps\Tui\Input\DefaultKeyMap; @@ -15,6 +14,7 @@ use DrevOps\Tui\Input\Scope; use DrevOps\Tui\Input\ScopedKeyMap; use DrevOps\Tui\Input\VimKeyMap; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Tests\Traits\ResetsRegistriesTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/phpunit/Unit/Model/ButtonsTest.php b/tests/phpunit/Unit/Model/ButtonsTest.php index 001562e9..ca914fb8 100644 --- a/tests/phpunit/Unit/Model/ButtonsTest.php +++ b/tests/phpunit/Unit/Model/ButtonsTest.php @@ -13,7 +13,7 @@ * Tests the submit/cancel button pair value object. */ #[CoversClass(Buttons::class)] -#[Group('tui')] +#[Group('model')] final class ButtonsTest extends TestCase { public function testDefaults(): void { diff --git a/tests/phpunit/Unit/Model/DateBoundsTest.php b/tests/phpunit/Unit/Model/DateBoundsTest.php index 2605f09e..70f146ba 100644 --- a/tests/phpunit/Unit/Model/DateBoundsTest.php +++ b/tests/phpunit/Unit/Model/DateBoundsTest.php @@ -4,8 +4,8 @@ namespace DrevOps\Tui\Tests\Unit\Model; -use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Model\DateBounds; +use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Model\Weekday; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/phpunit/Unit/Model/ModalTest.php b/tests/phpunit/Unit/Model/ModalTest.php index c3ebe7c1..6b7a02df 100644 --- a/tests/phpunit/Unit/Model/ModalTest.php +++ b/tests/phpunit/Unit/Model/ModalTest.php @@ -5,6 +5,7 @@ namespace DrevOps\Tui\Tests\Unit\Model; use DrevOps\Tui\Model\Buttons; +use DrevOps\Tui\Model\FormException; use DrevOps\Tui\Model\Modal; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; @@ -14,7 +15,7 @@ * Tests the modal presentation config value object. */ #[CoversClass(Modal::class)] -#[Group('tui')] +#[Group('model')] final class ModalTest extends TestCase { public function testDefaultButtons(): void { @@ -33,7 +34,7 @@ public function testCustomButtons(): void { } public function testHiddenButtonsRejected(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(FormException::class); $this->expectExceptionMessage('A modal dialog must show its buttons.'); new Modal(new Buttons(FALSE)); diff --git a/tests/phpunit/Unit/Model/PanelTest.php b/tests/phpunit/Unit/Model/PanelTest.php index 60c8a893..eaeb1fb7 100644 --- a/tests/phpunit/Unit/Model/PanelTest.php +++ b/tests/phpunit/Unit/Model/PanelTest.php @@ -16,7 +16,7 @@ * Tests the panel model. */ #[CoversClass(Panel::class)] -#[Group('tui')] +#[Group('model')] final class PanelTest extends TestCase { public function testItemCountSumsFieldsAndPanels(): void { diff --git a/tests/phpunit/Unit/Model/TableSpecTest.php b/tests/phpunit/Unit/Model/TableSpecTest.php index a0333e62..98b1a70e 100644 --- a/tests/phpunit/Unit/Model/TableSpecTest.php +++ b/tests/phpunit/Unit/Model/TableSpecTest.php @@ -13,7 +13,7 @@ * Tests the presentational table value object. */ #[CoversClass(TableSpec::class)] -#[Group('tui')] +#[Group('model')] final class TableSpecTest extends TestCase { public function testKeepsStringCells(): void { diff --git a/tests/phpunit/Unit/Primitive/OutputTest.php b/tests/phpunit/Unit/Primitive/OutputTest.php index b1f828d3..c9166248 100644 --- a/tests/phpunit/Unit/Primitive/OutputTest.php +++ b/tests/phpunit/Unit/Primitive/OutputTest.php @@ -7,9 +7,7 @@ use DrevOps\Tui\Primitive\Output; use DrevOps\Tui\Primitive\Status; use DrevOps\Tui\Testing\BufferedTerminal; -use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Theme\Mode; -use DrevOps\Tui\Theme\ThemeManager; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; @@ -19,13 +17,15 @@ * Tests the standalone output primitive. */ #[CoversClass(Output::class)] -#[Group('tui')] +#[Group('primitive')] final class OutputTest extends TestCase { + use BuildsThemesTrait; + public function testBoxWritesTheFramedLines(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->box('Pick your fruit.', 'Welcome'); + (new Output($terminal, $this->theme(color: FALSE)))->box('Pick your fruit.', 'Welcome'); $output = $terminal->output(); @@ -38,7 +38,7 @@ public function testBoxWritesTheFramedLines(): void { public function testBoxAcceptsLineList(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->box(['Apples', 'Pears']); + (new Output($terminal, $this->theme(color: FALSE)))->box(['Apples', 'Pears']); $lines = explode("\n", trim($terminal->output(), "\n")); @@ -50,7 +50,7 @@ public function testBoxAcceptsLineList(): void { public function testAnEmptyBoxWritesNothing(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->box(''); + (new Output($terminal, $this->theme(color: FALSE)))->box(''); $this->assertSame('', $terminal->output()); } @@ -59,7 +59,7 @@ public function testAnEmptyBoxWritesNothing(): void { public function testEachStatusMethodWritesItsOwnLine(string $method, string $glyph): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->{$method}('Preserves are ready'); + (new Output($terminal, $this->theme(color: FALSE)))->{$method}('Preserves are ready'); $this->assertSame($glyph . ' Preserves are ready' . "\n", $terminal->output()); } @@ -81,7 +81,7 @@ public static function dataProviderEachStatusMethodWritesItsOwnLine(): \Iterator public function testCardWritesItsGridInsideTheBox(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->card('Summary', 'Your order is packed.', ['Item', 'Count'], [['Apricot', '12']]); + (new Output($terminal, $this->theme(color: FALSE)))->card('Summary', 'Your order is packed.', ['Item', 'Count'], [['Apricot', '12']]); $output = $terminal->output(); @@ -94,7 +94,7 @@ public function testCardWritesItsGridInsideTheBox(): void { public function testUnborderedCardWritesIndentedLines(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->card('Summary', 'Packed.', bordered: FALSE); + (new Output($terminal, $this->theme(color: FALSE)))->card('Summary', 'Packed.', bordered: FALSE); $this->assertSame(" Summary\n Packed.\n", $terminal->output()); } @@ -102,7 +102,7 @@ public function testUnborderedCardWritesIndentedLines(): void { public function testTableWritesTheGridAlone(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->table(['Item'], [['Apricot']]); + (new Output($terminal, $this->theme(color: FALSE)))->table(['Item'], [['Apricot']]); $lines = explode("\n", trim($terminal->output(), "\n")); @@ -114,7 +114,7 @@ public function testTableWritesTheGridAlone(): void { public function testAnEmptyTableWritesNothing(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->table([], []); + (new Output($terminal, $this->theme(color: FALSE)))->table([], []); $this->assertSame('', $terminal->output()); } @@ -122,14 +122,14 @@ public function testAnEmptyTableWritesNothing(): void { public function testTextWritesWrappedParagraph(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->text('Everything is picked the morning it ships.'); + (new Output($terminal, $this->theme(color: FALSE)))->text('Everything is picked the morning it ships.'); $this->assertSame("Everything is picked the morning it ships.\n", $terminal->output()); } public function testRuleWritesSpanningLine(): void { $terminal = new BufferedTerminal(); - $theme = $this->theme(); + $theme = $this->theme(color: FALSE); (new Output($terminal, $theme))->rule(); @@ -139,7 +139,7 @@ public function testRuleWritesSpanningLine(): void { public function testBannerWritesTheLogoAndVersion(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->banner('Produce Box', '1.2.3'); + (new Output($terminal, $this->theme(color: FALSE)))->banner('Produce Box', '1.2.3'); $output = $terminal->output(); @@ -151,7 +151,7 @@ public function testBannerWritesTheLogoAndVersion(): void { public function testStatusTakesTheKindDirectly(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->status(Status::Warning, 'Short on jars'); + (new Output($terminal, $this->theme(color: FALSE)))->status(Status::Warning, 'Short on jars'); $this->assertSame("! Short on jars\n", $terminal->output()); } @@ -159,7 +159,7 @@ public function testStatusTakesTheKindDirectly(): void { public function testDefinitionsWriteAnAlignedList(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->definitions(['Jars' => '12', 'Fruit' => 'Apricot']); + (new Output($terminal, $this->theme(color: FALSE)))->definitions(['Jars' => '12', 'Fruit' => 'Apricot']); $this->assertSame(" Jars 12\n Fruit Apricot\n", $terminal->output()); } @@ -167,14 +167,14 @@ public function testDefinitionsWriteAnAlignedList(): void { public function testEmptyDefinitionsWriteNothing(): void { $terminal = new BufferedTerminal(); - (new Output($terminal, $this->theme()))->definitions([]); + (new Output($terminal, $this->theme(color: FALSE)))->definitions([]); $this->assertSame('', $terminal->output()); } public function testCallsChainInOrder(): void { $terminal = new BufferedTerminal(); - $output = new Output($terminal, $this->theme()); + $output = new Output($terminal, $this->theme(color: FALSE)); $this->assertSame($output, $output->success('Packed')->note('Sealed')); $this->assertSame("✓ Packed\n• Sealed\n", $terminal->output()); @@ -207,19 +207,4 @@ public function testColourWrapsEveryPiece(): void { $this->assertStringContainsString("\033[32m", $output); } - /** - * A default theme in the given display modes, fixed to dark. - * - * @param bool $color - * Whether colour is on. - * @param bool $unicode - * Whether Unicode glyphs are on. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(bool $color = FALSE, bool $unicode = TRUE): DefaultTheme { - return ThemeManager::create('default', DefaultTheme::DEFAULT_WIDTH, ['color' => $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); - } - } diff --git a/tests/phpunit/Unit/Primitive/ProgressTest.php b/tests/phpunit/Unit/Primitive/ProgressTest.php index b093bdf3..6437b640 100644 --- a/tests/phpunit/Unit/Primitive/ProgressTest.php +++ b/tests/phpunit/Unit/Primitive/ProgressTest.php @@ -6,9 +6,7 @@ use DrevOps\Tui\Primitive\Progress; use DrevOps\Tui\Testing\BufferedTerminal; -use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Theme\Mode; -use DrevOps\Tui\Theme\ThemeManager; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; @@ -17,9 +15,11 @@ * Tests the progress primitive (spinner and determinate bar). */ #[CoversClass(Progress::class)] -#[Group('tui')] +#[Group('primitive')] final class ProgressTest extends TestCase { + use BuildsThemesTrait; + public function testReturnsTheCallbackResult(): void { $progress = new Progress(new BufferedTerminal(), $this->theme(), TRUE, NULL, 'Scanning'); @@ -150,19 +150,4 @@ public function testCleansUpWhenTheWorkThrows(): void { $this->assertStringContainsString("\033[?25h", $terminal->output()); } - /** - * A default theme in the given display modes, fixed to dark. - * - * @param bool $color - * Whether colour is on. - * @param bool $unicode - * Whether Unicode glyphs are on. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(bool $color = TRUE, bool $unicode = TRUE): DefaultTheme { - return ThemeManager::create('default', DefaultTheme::DEFAULT_WIDTH, ['color' => $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); - } - } diff --git a/tests/phpunit/Unit/ProgressableTest.php b/tests/phpunit/Unit/ProgressableTest.php index 75000acc..f76d8665 100644 --- a/tests/phpunit/Unit/ProgressableTest.php +++ b/tests/phpunit/Unit/ProgressableTest.php @@ -7,12 +7,12 @@ use DrevOps\Tui\Builder\FieldBuilder; use DrevOps\Tui\Builder\Form; use DrevOps\Tui\Builder\PanelBuilder; -use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Engine\Engine; use DrevOps\Tui\Engine\EngineException; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Render\PanelController; use DrevOps\Tui\Testing\TuiTester; use DrevOps\Tui\Theme\DefaultTheme; diff --git a/tests/phpunit/Unit/Render/AnsiTest.php b/tests/phpunit/Unit/Render/AnsiTest.php index 63d37862..63b941e9 100644 --- a/tests/phpunit/Unit/Render/AnsiTest.php +++ b/tests/phpunit/Unit/Render/AnsiTest.php @@ -13,7 +13,7 @@ * Tests the ANSI helpers. */ #[CoversClass(Ansi::class)] -#[Group('tui')] +#[Group('render')] final class AnsiTest extends TestCase { public function testStyle(): void { diff --git a/tests/phpunit/Unit/Render/BoxTest.php b/tests/phpunit/Unit/Render/BoxTest.php index 02ccfda0..58729613 100644 --- a/tests/phpunit/Unit/Render/BoxTest.php +++ b/tests/phpunit/Unit/Render/BoxTest.php @@ -16,7 +16,7 @@ * Tests the pure box-drawing geometry helper. */ #[CoversClass(Box::class)] -#[Group('tui')] +#[Group('render')] final class BoxTest extends TestCase { #[DataProvider('dataProviderChars')] diff --git a/tests/phpunit/Unit/Render/ExternalEditorTest.php b/tests/phpunit/Unit/Render/ExternalEditorTest.php index 7409c06a..33e56024 100644 --- a/tests/phpunit/Unit/Render/ExternalEditorTest.php +++ b/tests/phpunit/Unit/Render/ExternalEditorTest.php @@ -17,7 +17,7 @@ * Tests the external editor service. */ #[CoversClass(ExternalEditor::class)] -#[Group('tui')] +#[Group('render')] final class ExternalEditorTest extends TestCase { use IsolatesEnvTrait; diff --git a/tests/phpunit/Unit/Render/MarkupTest.php b/tests/phpunit/Unit/Render/MarkupTest.php index f043e6dd..9ca0d059 100644 --- a/tests/phpunit/Unit/Render/MarkupTest.php +++ b/tests/phpunit/Unit/Render/MarkupTest.php @@ -70,10 +70,10 @@ public function testParseMarkdownMarkersAreLiteralWhenOff(string $source): void } public static function dataProviderParseMarkdownMarkersAreLiteralWhenOff(): \Iterator { - yield ['**bold**']; - yield ['*emphasis*']; - yield ['`code`']; - yield ['- bullet']; + yield 'bold' => ['**bold**']; + yield 'emphasis' => ['*emphasis*']; + yield 'code' => ['`code`']; + yield 'bullet' => ['- bullet']; } public function testParseBold(): void { diff --git a/tests/phpunit/Unit/Render/NavigatorTest.php b/tests/phpunit/Unit/Render/NavigatorTest.php index ea8aee40..44421223 100644 --- a/tests/phpunit/Unit/Render/NavigatorTest.php +++ b/tests/phpunit/Unit/Render/NavigatorTest.php @@ -14,7 +14,7 @@ * Tests the panel navigator. */ #[CoversClass(Navigator::class)] -#[Group('tui')] +#[Group('render')] final class NavigatorTest extends TestCase { public function testNavigation(): void { diff --git a/tests/phpunit/Unit/Render/OverlayTest.php b/tests/phpunit/Unit/Render/OverlayTest.php index ad3ab5c9..2a89b693 100644 --- a/tests/phpunit/Unit/Render/OverlayTest.php +++ b/tests/phpunit/Unit/Render/OverlayTest.php @@ -16,7 +16,7 @@ * Tests the line-compositor that overlays a box on a backdrop. */ #[CoversClass(Overlay::class)] -#[Group('tui')] +#[Group('render')] final class OverlayTest extends TestCase { public function testCenterPositionsWithEqualPadding(): void { diff --git a/tests/phpunit/Unit/Render/PanelControllerTest.php b/tests/phpunit/Unit/Render/PanelControllerTest.php index bf2b52e0..46a9b405 100644 --- a/tests/phpunit/Unit/Render/PanelControllerTest.php +++ b/tests/phpunit/Unit/Render/PanelControllerTest.php @@ -11,9 +11,9 @@ use DrevOps\Tui\Condition\Condition; use DrevOps\Tui\Derive\Derive; use DrevOps\Tui\Handler\HandlerRegistry; -use DrevOps\Tui\Model\Fixup; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\Fixup; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\ExternalEditor; use DrevOps\Tui\Render\PanelController; @@ -21,6 +21,7 @@ use DrevOps\Tui\Render\TerminalControl; use DrevOps\Tui\Testing\BufferedTerminal; use DrevOps\Tui\Testing\KeyEncoder; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use DrevOps\Tui\Theme\Border; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\DosTheme; @@ -35,9 +36,11 @@ * Tests the interactive panel controller. */ #[CoversClass(PanelController::class)] -#[Group('tui')] +#[Group('render')] final class PanelControllerTest extends TestCase { + use BuildsThemesTrait; + public function testDrillIntoPanelAndBack(): void { $controller = $this->controller(); @@ -1425,16 +1428,6 @@ protected function requiredController(array $values): PanelController { return new PanelController($form, $this->plainTheme(), values: $values); } - /** - * An unstyled, borderless theme for frame assertions. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function plainTheme(): DefaultTheme { - return new DefaultTheme(40, ['color' => FALSE, 'border' => Border::None, 'spacing' => Spacing::Normal]); - } - public function testEditEnforcesHandlerBehaviour(): void { $form = Form::create('Demo') ->panel('stall', 'Stall', function (PanelBuilder $p): void { diff --git a/tests/phpunit/Unit/Render/ScrollerTest.php b/tests/phpunit/Unit/Render/ScrollerTest.php index 3fe9cf81..a0cb46dd 100644 --- a/tests/phpunit/Unit/Render/ScrollerTest.php +++ b/tests/phpunit/Unit/Render/ScrollerTest.php @@ -15,7 +15,7 @@ */ #[CoversClass(Scroller::class)] #[CoversClass(Viewport::class)] -#[Group('tui')] +#[Group('render')] final class ScrollerTest extends TestCase { public function testCursorAtTop(): void { diff --git a/tests/phpunit/Unit/Render/TableTest.php b/tests/phpunit/Unit/Render/TableTest.php index d07c24d6..5369d7d5 100644 --- a/tests/phpunit/Unit/Render/TableTest.php +++ b/tests/phpunit/Unit/Render/TableTest.php @@ -14,7 +14,7 @@ * Tests the pure table geometry helper. */ #[CoversClass(Table::class)] -#[Group('tui')] +#[Group('render')] final class TableTest extends TestCase { public function testRendersAlignedGrid(): void { diff --git a/tests/phpunit/Unit/Render/TerminalControlTest.php b/tests/phpunit/Unit/Render/TerminalControlTest.php index 256d12a1..98546a04 100644 --- a/tests/phpunit/Unit/Render/TerminalControlTest.php +++ b/tests/phpunit/Unit/Render/TerminalControlTest.php @@ -13,7 +13,7 @@ * Tests the terminal control sequences. */ #[CoversClass(TerminalControl::class)] -#[Group('tui')] +#[Group('render')] final class TerminalControlTest extends TestCase { public function testSequences(): void { diff --git a/tests/phpunit/Unit/Render/TerminalTest.php b/tests/phpunit/Unit/Render/TerminalTest.php index 4b0317c4..5d36ae5d 100644 --- a/tests/phpunit/Unit/Render/TerminalTest.php +++ b/tests/phpunit/Unit/Render/TerminalTest.php @@ -18,7 +18,7 @@ * Tests the terminal output and capability detection. */ #[CoversClass(Terminal::class)] -#[Group('tui')] +#[Group('render')] final class TerminalTest extends TestCase { use IsolatesEnvTrait; diff --git a/tests/phpunit/Unit/Schema/SchemaGeneratorTest.php b/tests/phpunit/Unit/Schema/SchemaGeneratorTest.php index d8aaf881..e9f03c5a 100644 --- a/tests/phpunit/Unit/Schema/SchemaGeneratorTest.php +++ b/tests/phpunit/Unit/Schema/SchemaGeneratorTest.php @@ -7,9 +7,9 @@ use DrevOps\Tui\Builder\Form; use DrevOps\Tui\Builder\PanelBuilder; use DrevOps\Tui\Condition\Condition; +use DrevOps\Tui\Derive\Derive; use DrevOps\Tui\Handler\Context; use DrevOps\Tui\Model\Weekday; -use DrevOps\Tui\Derive\Derive; use DrevOps\Tui\Schema\SchemaGenerator; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Testing/AllWidgetsFormTest.php b/tests/phpunit/Unit/Testing/AllWidgetsFormTest.php index 57aa9584..c026b630 100644 --- a/tests/phpunit/Unit/Testing/AllWidgetsFormTest.php +++ b/tests/phpunit/Unit/Testing/AllWidgetsFormTest.php @@ -4,9 +4,9 @@ namespace DrevOps\Tui\Tests\Unit\Testing; -use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Testing\TuiTester; use DrevOps\Tui\Tests\Fixtures\Form\AllWidgetsForm; use DrevOps\Tui\Tests\Fixtures\Theme\OceanTheme; diff --git a/tests/phpunit/Unit/Testing/KeyEncoderTest.php b/tests/phpunit/Unit/Testing/KeyEncoderTest.php index b76c413d..82cc7495 100644 --- a/tests/phpunit/Unit/Testing/KeyEncoderTest.php +++ b/tests/phpunit/Unit/Testing/KeyEncoderTest.php @@ -17,7 +17,7 @@ * Tests encoding a key back into the bytes a parser decodes into it. */ #[CoversClass(KeyEncoder::class)] -#[Group('input')] +#[Group('testing')] final class KeyEncoderTest extends TestCase { #[DataProvider('dataProviderRoundTrip')] diff --git a/tests/phpunit/Unit/Testing/KeyStreamTest.php b/tests/phpunit/Unit/Testing/KeyStreamTest.php index e6e8381f..e8352e63 100644 --- a/tests/phpunit/Unit/Testing/KeyStreamTest.php +++ b/tests/phpunit/Unit/Testing/KeyStreamTest.php @@ -16,7 +16,7 @@ */ #[CoversClass(Key::class)] #[CoversClass(ArrayKeyStream::class)] -#[Group('input')] +#[Group('testing')] final class KeyStreamTest extends TestCase { public function testCharAndNamed(): void { diff --git a/tests/phpunit/Unit/Theme/BuiltinThemesTest.php b/tests/phpunit/Unit/Theme/BuiltinThemesTest.php index ce535e69..4846ea7a 100644 --- a/tests/phpunit/Unit/Theme/BuiltinThemesTest.php +++ b/tests/phpunit/Unit/Theme/BuiltinThemesTest.php @@ -28,7 +28,7 @@ #[CoversClass(MonoTheme::class)] #[CoversClass(DosTheme::class)] #[CoversClass(Sgr::class)] -#[Group('tui')] +#[Group('theme')] final class BuiltinThemesTest extends TestCase { /** diff --git a/tests/phpunit/Unit/Theme/OutputRenderTest.php b/tests/phpunit/Unit/Theme/OutputRenderTest.php index bbbc82f8..a4df21ba 100644 --- a/tests/phpunit/Unit/Theme/OutputRenderTest.php +++ b/tests/phpunit/Unit/Theme/OutputRenderTest.php @@ -6,6 +6,7 @@ use DrevOps\Tui\Primitive\Status; use DrevOps\Tui\Render\Ansi; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\EmberTheme; use DrevOps\Tui\Theme\Mode; @@ -21,9 +22,11 @@ */ #[CoversClass(DefaultTheme::class)] #[CoversClass(EmberTheme::class)] -#[Group('tui')] +#[Group('theme')] final class OutputRenderTest extends TestCase { + use BuildsThemesTrait; + public function testBoxDrawsTitledFrameSizedToItsContent(): void { $lines = $this->theme(color: FALSE)->renderCard('Welcome', ['Pick your fruit.']); @@ -232,7 +235,7 @@ public function testStatusLeadsWithItsOwnGlyph(Status $status, string $unicode, } /** - * Data provider. + * Data provider for testStatusLeadsWithItsOwnGlyph(). * * @return \Iterator * The status, its Unicode glyph and its ASCII glyph. @@ -261,7 +264,7 @@ public function testStatusCarriesItsOwnColour(Status $status, string $sgr): void } /** - * Data provider. + * Data provider for testStatusCarriesItsOwnColour(). * * @return \Iterator * The status and the SGR the default dark theme paints it with. @@ -354,19 +357,4 @@ public function testDefinitionsStyleLabelsAndValuesApart(): void { $this->assertStringContainsString('Jars', $lines[0]); } - /** - * A default theme in the given display modes, fixed to dark. - * - * @param bool $color - * Whether colour is on. - * @param bool $unicode - * Whether Unicode glyphs are on. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(bool $color = TRUE, bool $unicode = TRUE): DefaultTheme { - return ThemeManager::create('default', DefaultTheme::DEFAULT_WIDTH, ['color' => $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); - } - } diff --git a/tests/phpunit/Unit/Theme/ProgressRenderTest.php b/tests/phpunit/Unit/Theme/ProgressRenderTest.php index 6a398e97..4feb0639 100644 --- a/tests/phpunit/Unit/Theme/ProgressRenderTest.php +++ b/tests/phpunit/Unit/Theme/ProgressRenderTest.php @@ -4,6 +4,7 @@ namespace DrevOps\Tui\Tests\Unit\Theme; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\EmberTheme; use DrevOps\Tui\Theme\Mode; @@ -17,9 +18,11 @@ */ #[CoversClass(DefaultTheme::class)] #[CoversClass(EmberTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ProgressRenderTest extends TestCase { + use BuildsThemesTrait; + public function testSpinnerCyclesFramesInTheAccent(): void { $theme = $this->theme(); @@ -110,19 +113,4 @@ public function testLoadingAsciiFallbackAndEmptyCaption(): void { $this->assertStringContainsString('Loading ...', $theme->renderLoading('Loading')); } - /** - * A default theme in the given display modes, fixed to dark. - * - * @param bool $color - * Whether colour is on. - * @param bool $unicode - * Whether Unicode glyphs are on. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(bool $color = TRUE, bool $unicode = TRUE): DefaultTheme { - return ThemeManager::create('default', DefaultTheme::DEFAULT_WIDTH, ['color' => $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); - } - } diff --git a/tests/phpunit/Unit/Theme/ScaleRenderTest.php b/tests/phpunit/Unit/Theme/ScaleRenderTest.php index 5cb267c6..a73ebb0c 100644 --- a/tests/phpunit/Unit/Theme/ScaleRenderTest.php +++ b/tests/phpunit/Unit/Theme/ScaleRenderTest.php @@ -4,10 +4,11 @@ namespace DrevOps\Tui\Tests\Unit\Theme; -use DrevOps\Tui\Render\Ansi; -use DrevOps\Tui\Testing\TuiTester; use DrevOps\Tui\Builder\Form; use DrevOps\Tui\Builder\PanelBuilder; +use DrevOps\Tui\Render\Ansi; +use DrevOps\Tui\Testing\TuiTester; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\Mode; use DrevOps\Tui\Theme\ThemeManager; @@ -20,9 +21,11 @@ * Tests the theme's rating-scale rendering. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ScaleRenderTest extends TestCase { + use BuildsThemesTrait; + public function testScaleFillsUpToTheChosenPoint(): void { $line = $this->theme(color: FALSE)->renderScale(3, 1, 5, 'Fair'); @@ -124,19 +127,4 @@ protected function ratingForm(): Form { }); } - /** - * A default theme in the given display modes, fixed to dark. - * - * @param bool $color - * Whether colour is on. - * @param bool $unicode - * Whether Unicode glyphs are on. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(bool $color = TRUE, bool $unicode = TRUE): DefaultTheme { - return ThemeManager::create('default', DefaultTheme::DEFAULT_WIDTH, ['color' => $color, 'unicode' => $unicode, 'mode' => Mode::Dark]); - } - } diff --git a/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php b/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php index 424be2a1..a017aeae 100644 --- a/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php +++ b/tests/phpunit/Unit/Theme/ThemeConditionalIndentTest.php @@ -25,7 +25,7 @@ * Tests the opt-in indentation of conditional fields. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeConditionalIndentTest extends TestCase { /** @@ -36,7 +36,7 @@ final class ThemeConditionalIndentTest extends TestCase { public function testOptionIsOffByDefault(): void { $panel = $this->chainPanel(); - [$lines] = $this->theme(FALSE)->renderBody($panel, new Answers(), 0); + [$lines] = $this->indentTheme(FALSE)->renderBody($panel, new Answers(), 0); // The cursor row leads with the marker glyph; every other row leads with // the two blank columns that stand in for it, and nothing more. @@ -47,7 +47,7 @@ public function testOptionIsOffByDefault(): void { public function testEachConditionStepsTheRowInFurther(): void { $panel = $this->chainPanel(); - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0); + [$lines] = $this->indentTheme()->renderBody($panel, new Answers(), 0); // The cursor sits on the unconditional root, so its marker shows; the // conditional rows carry the blank marker plus one step per condition. @@ -63,7 +63,7 @@ public function testIndentedRowKeepsBadgeAtFrameEdge(): void { $field = $this->fieldsOf($this->chainPanel())['first']; $answers = new Answers(['first' => 'Acme'], ['first' => Provenance::Edited]); - $lines = $this->theme()->renderFieldLine($field, $answers, FALSE); + $lines = $this->indentTheme()->renderFieldLine($field, $answers, FALSE); $this->assertStringContainsString('First Acme', Ansi::strip($lines[0])); $this->assertStringContainsString('edited', Ansi::strip($lines[0])); @@ -80,7 +80,7 @@ public function testValueContinuationLinesFollowTheIndentedColumn(): void { $form = new FormDefinition('T', 'S', [$panel]); $answers = new Answers(['notes' => "Crisp and sweet\nHint of citrus"], []); - $lines = $this->theme()->renderFieldLine($this->fieldsOf($form->panels[0])['notes'], $answers, FALSE); + $lines = $this->indentTheme()->renderFieldLine($this->fieldsOf($form->panels[0])['notes'], $answers, FALSE); $this->assertSame(' Notes Crisp and sweet', Ansi::strip($lines[0])); // The second line aligns under the value column, which the gutter moved. @@ -94,7 +94,7 @@ public function testDescriptionStepsInWithItsField(): void { ]); new FormDefinition('T', 'S', [$panel]); - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0); + [$lines] = $this->indentTheme()->renderBody($panel, new Answers(), 0); $rows = array_map(Ansi::strip(...), $lines); // A description already sits four columns in; the conditional one carries @@ -106,7 +106,7 @@ public function testDescriptionStepsInWithItsField(): void { public function testInlineEditorOpensAtTheIndentedColumn(): void { $field = $this->fieldsOf($this->chainPanel())['second']; - $lines = $this->theme()->renderInlineEditor($field, "line one\nline two", TRUE); + $lines = $this->indentTheme()->renderInlineEditor($field, "line one\nline two", TRUE); $this->assertSame(' ❯ Second line one', Ansi::strip($lines[0])); $this->assertSame($this->columnOf($lines[0], 'line one'), $this->columnOf($lines[1], 'line two')); @@ -119,7 +119,7 @@ public function testPlainNoteCardStepsInWithItsCondition(): void { ]); new FormDefinition('T', 'S', [$panel]); - $lines = array_map(Ansi::strip(...), $this->theme()->renderNoteLines($this->fieldsOf($panel)['hint'], new Answers())); + $lines = array_map(Ansi::strip(...), $this->indentTheme()->renderNoteLines($this->fieldsOf($panel)['hint'], new Answers())); // A card already sits in a two-column gutter of its own. $this->assertSame(' Storage', $lines[0]); @@ -134,8 +134,8 @@ public function testBorderedNoteNarrowsToStayInsideTheFrame(): void { new FormDefinition('T', 'S', [$panel]); $note = $this->fieldsOf($panel)['hint']; - $flush = $this->theme(FALSE)->renderNoteLines($note, new Answers()); - $stepped = $this->theme()->renderNoteLines($note, new Answers()); + $flush = $this->indentTheme(FALSE)->renderNoteLines($note, new Answers()); + $stepped = $this->indentTheme()->renderNoteLines($note, new Answers()); // The body wraps to the room the card's own chrome leaves, so an indented // card narrows by the columns its gutter takes and its right edge still @@ -160,8 +160,8 @@ public function testMeasuredWidthCoversTheIndent(): void { ]); $form = new FormDefinition('T', 'S', [$panel], buttons: new Buttons(show: FALSE)); - $flush = $this->theme(FALSE)->measureContentWidth($form, new Answers()); - $stepped = $this->theme()->measureContentWidth($form, new Answers()); + $flush = $this->indentTheme(FALSE)->measureContentWidth($form, new Answers()); + $stepped = $this->indentTheme()->measureContentWidth($form, new Answers()); $this->assertSame($flush + self::STEP, $stepped); } @@ -174,7 +174,7 @@ public function testGridColumnPreviewStepsInToo(): void { $panel = new Panel('p', 'P', '', [], [$sub], layout: [1]); new FormDefinition('T', 'S', [$panel]); - [$lines] = $this->theme()->renderBody($panel, new Answers(), -1); + [$lines] = $this->indentTheme()->renderBody($panel, new Answers(), -1); $rows = array_map(Ansi::strip(...), $lines); $this->assertSame(2, $this->leadingSpaces($rows[1]), 'An unconditional preview row keeps the block gutter.'); @@ -290,7 +290,7 @@ protected function columnOf(string $line, string $needle): int { * @return \DrevOps\Tui\Theme\DefaultTheme * The theme. */ - protected function theme(bool $indent = TRUE): DefaultTheme { + protected function indentTheme(bool $indent = TRUE): DefaultTheme { return new DefaultTheme(40, [ 'color' => FALSE, 'border' => Border::None, diff --git a/tests/phpunit/Unit/Theme/ThemeFullscreenTest.php b/tests/phpunit/Unit/Theme/ThemeFullscreenTest.php index 3589ee85..27b27309 100644 --- a/tests/phpunit/Unit/Theme/ThemeFullscreenTest.php +++ b/tests/phpunit/Unit/Theme/ThemeFullscreenTest.php @@ -22,7 +22,7 @@ * Tests the fullscreen frame stretch, block alignment and content measuring. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeFullscreenTest extends TestCase { #[DataProvider('dataProviderChromeHeight')] diff --git a/tests/phpunit/Unit/Theme/ThemeLayoutTest.php b/tests/phpunit/Unit/Theme/ThemeLayoutTest.php index ad783ecb..10f6f160 100644 --- a/tests/phpunit/Unit/Theme/ThemeLayoutTest.php +++ b/tests/phpunit/Unit/Theme/ThemeLayoutTest.php @@ -23,7 +23,7 @@ * Tests the panel-grid layout rendering and measurement. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeLayoutTest extends TestCase { public function testLayoutZipsPanelsSideBySide(): void { @@ -148,15 +148,30 @@ public function testLayoutPreviewsMultiLineValuesAsTheirFirstLine(): void { [$lines] = $theme->renderBody($panel, new Answers(['notes' => "Crisp and sweet\nHint of citrus", 'two' => 'x'], []), 0); // A grid cell is one physical row: the multi-line value previews as its - // first line with an ellipsis, and no entry carries an embedded newline - // that would desync the column zip. + // first line with a there-is-more marker, and no entry carries an embedded + // newline that would desync the column zip. $body = implode('|', $lines); - $this->assertStringContainsString('Crisp and sweet…', $body); + $this->assertStringContainsString('Crisp and sweet...', $body); $this->assertStringNotContainsString('Hint of citrus', $body); $this->assertStringNotContainsString("\n", $body); $this->assertCount(2, $lines); } + public function testLayoutPreviewMarkerFollowsTheDisplayMode(): void { + $panel = new Panel('p', 'P', '', [], [ + new Panel('a', 'A', '', [new Field('notes', 'Notes', '', FieldType::Textarea, '')]), + new Panel('b', 'B', '', [new Field('two', 'Two', '', FieldType::Text, '')]), + ], NULL, [2]); + $answers = new Answers(['notes' => "Crisp and sweet\nHint of citrus", 'two' => 'x'], []); + + [$unicode_lines] = (new DefaultTheme(60, ['color' => FALSE, 'unicode' => TRUE]))->renderBody($panel, $answers, 0); + [$ascii_lines] = (new DefaultTheme(60, ['color' => FALSE, 'unicode' => FALSE]))->renderBody($panel, $answers, 0); + + // A terminal without Unicode must never be handed the glyph. + $this->assertStringContainsString('Crisp and sweet…', implode('|', $unicode_lines)); + $this->assertStringNotContainsString('…', implode('|', $ascii_lines)); + } + public function testMeasureUsesTheWidestValueLine(): void { $form = Form::create('T') ->buttons(FALSE) @@ -184,6 +199,51 @@ public function testMeasureContentWidthCoversTheGrid(): void { $this->assertSame(56, (new DefaultTheme(40, ['color' => FALSE, 'border' => Border::None, 'spacing' => Spacing::Compact]))->measureContentWidth($form, $answers)); } + public function testMeasureReservesRoomForTheMultiLineMarker(): void { + $form = Form::create('T') + ->buttons(FALSE) + ->layout(2) + ->panel('a', 'A', fn(PanelBuilder $p): FieldBuilder => $p->textarea('notes', 'A')->default("Crisp and sweet\nshort")) + ->panel('b', 'B', fn(PanelBuilder $p): FieldBuilder => $p->text('two', 'B')) + ->build(); + $answers = new Answers(['notes' => "Crisp and sweet\nshort"], []); + $options = ['color' => FALSE, 'border' => Border::None, 'spacing' => Spacing::Compact]; + + $unicode = (new DefaultTheme(40, $options + ['unicode' => TRUE]))->measureContentWidth($form, $answers); + $ascii = (new DefaultTheme(40, $options + ['unicode' => FALSE]))->measureContentWidth($form, $answers); + + // The first line is the widest, so a grid cell is that line plus its + // marker. Measuring the raw lines alone would size both modes the same and + // clip the marker off the end of the cell. + $this->assertSame(44, $unicode); + $this->assertSame(48, $ascii); + } + + public function testMeasureCountsColumnsNotEscapeSequences(): void { + $grid = Form::create('T') + ->buttons(FALSE) + ->layout(2) + ->panel('a', 'A', fn(PanelBuilder $p): FieldBuilder => $p->rating('grade', 'A')) + ->panel('b', 'B', fn(PanelBuilder $p): FieldBuilder => $p->text('two', 'B')) + ->build(); + $linear = Form::create('T') + ->buttons(FALSE) + ->panel('a', 'A', fn(PanelBuilder $p): FieldBuilder => $p->rating('grade', 'A')) + ->build(); + $answers = new Answers(['grade' => 3], []); + $options = ['border' => Border::None, 'spacing' => Spacing::Compact]; + + // A rating renders painted, so measuring its raw string would count the + // escape sequences as columns and oversize the frame. Colour is invisible, + // so it must not move the measurement either in a grid cell or in a row. + foreach ([$grid, $linear] as $form) { + $plain = (new DefaultTheme(40, $options + ['color' => FALSE]))->measureContentWidth($form, $answers); + $painted = (new DefaultTheme(40, $options + ['color' => TRUE]))->measureContentWidth($form, $answers); + + $this->assertSame($plain, $painted); + } + } + public function testLayoutPreviewsNoteAsItsTitle(): void { $theme = new DefaultTheme(60, ['color' => FALSE, 'unicode' => FALSE]); $panel = new Panel('p', 'P', '', [], [ diff --git a/tests/phpunit/Unit/Theme/ThemeManagerTest.php b/tests/phpunit/Unit/Theme/ThemeManagerTest.php index c469a3f1..c35774ea 100644 --- a/tests/phpunit/Unit/Theme/ThemeManagerTest.php +++ b/tests/phpunit/Unit/Theme/ThemeManagerTest.php @@ -18,7 +18,7 @@ * Tests the theme registry and factory. */ #[CoversClass(ThemeManager::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeManagerTest extends TestCase { use ResetsRegistriesTrait; diff --git a/tests/phpunit/Unit/Theme/ThemeOptionsTest.php b/tests/phpunit/Unit/Theme/ThemeOptionsTest.php index ef43ecc7..662ee3dd 100644 --- a/tests/phpunit/Unit/Theme/ThemeOptionsTest.php +++ b/tests/phpunit/Unit/Theme/ThemeOptionsTest.php @@ -5,12 +5,12 @@ namespace DrevOps\Tui\Tests\Unit\Theme; use DrevOps\Tui\Answers\Answers; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\KeyMapManager; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\Viewport; use DrevOps\Tui\Tests\Fixtures\Theme\AccentOptionTheme; @@ -30,7 +30,7 @@ * Tests the theme spacing, border and custom display options. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeOptionsTest extends TestCase { public function testCompactSpacingDropsDescriptionsAndSummary(): void { diff --git a/tests/phpunit/Unit/Theme/ThemeRenderTest.php b/tests/phpunit/Unit/Theme/ThemeRenderTest.php index eec7a5a2..a49c4ccd 100644 --- a/tests/phpunit/Unit/Theme/ThemeRenderTest.php +++ b/tests/phpunit/Unit/Theme/ThemeRenderTest.php @@ -6,21 +6,22 @@ use DrevOps\Tui\Answers\Answers; use DrevOps\Tui\Answers\Provenance; +use DrevOps\Tui\Input\Action; +use DrevOps\Tui\Input\Hint; +use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Input\KeyMapManager; +use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Model\Buttons; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\Modal; use DrevOps\Tui\Model\Panel; use DrevOps\Tui\Model\TableSpec; -use DrevOps\Tui\Input\Action; -use DrevOps\Tui\Input\Hint; -use DrevOps\Tui\Input\Key; -use DrevOps\Tui\Input\KeyMapManager; -use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\HelpSection; use DrevOps\Tui\Render\Navigator; use DrevOps\Tui\Render\Viewport; +use DrevOps\Tui\Tests\Traits\BuildsThemesTrait; use DrevOps\Tui\Theme\Border; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Theme\Spacing; @@ -34,11 +35,13 @@ */ #[CoversClass(DefaultTheme::class)] #[CoversClass(HelpSection::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeRenderTest extends TestCase { + use BuildsThemesTrait; + public function testFieldLineSelectedRightAlignsBadge(): void { - $lines = $this->theme()->renderFieldLine(new Field('name', 'Name', '', FieldType::Text, ''), new Answers(['name' => 'Acme'], ['name' => Provenance::Edited]), TRUE); + $lines = $this->plainTheme()->renderFieldLine(new Field('name', 'Name', '', FieldType::Text, ''), new Answers(['name' => 'Acme'], ['name' => Provenance::Edited]), TRUE); // A single-line value is one row. $this->assertCount(1, $lines); @@ -48,14 +51,14 @@ public function testFieldLineSelectedRightAlignsBadge(): void { } public function testFieldLineDefaultHasNoBadge(): void { - $lines = $this->theme()->renderFieldLine(new Field('name', 'Name', '', FieldType::Text, ''), new Answers(['name' => 'Acme'], ['name' => Provenance::Default]), FALSE); + $lines = $this->plainTheme()->renderFieldLine(new Field('name', 'Name', '', FieldType::Text, ''), new Answers(['name' => 'Acme'], ['name' => Provenance::Default]), FALSE); $this->assertStringNotContainsString('default', $lines[0]); $this->assertStringContainsString('Name Acme', Ansi::strip($lines[0])); } public function testFieldLineRendersValues(): void { - $theme = $this->theme(); + $theme = $this->plainTheme(); $bool = Ansi::strip($theme->renderFieldLine(new Field('b', 'B', '', FieldType::Confirm, FALSE), new Answers(['b' => TRUE], ['b' => Provenance::Default]), FALSE)[0]); $this->assertStringContainsString('B yes', $bool); @@ -67,20 +70,20 @@ public function testFieldLineRendersValues(): void { public function testFieldLineMasksPasswordValue(): void { $field = new Field('token', 'Token', '', FieldType::Password, ''); - $line = Ansi::strip($this->theme()->renderFieldLine($field, new Answers(['token' => 's3cret-long'], ['token' => Provenance::Edited]), FALSE)[0]); + $line = Ansi::strip($this->plainTheme()->renderFieldLine($field, new Answers(['token' => 's3cret-long'], ['token' => Provenance::Edited]), FALSE)[0]); $this->assertStringNotContainsString('s3cret-long', $line); // The mask has a fixed length so it does not leak the value's length. $this->assertStringContainsString('Token ••••••••', $line); - $empty = Ansi::strip($this->theme()->renderFieldLine($field, new Answers(['token' => ''], ['token' => Provenance::Default]), FALSE)[0]); + $empty = Ansi::strip($this->plainTheme()->renderFieldLine($field, new Answers(['token' => ''], ['token' => Provenance::Default]), FALSE)[0]); $this->assertStringNotContainsString('•', $empty); } public function testRenderInlineEditorPutsViewInPlaceOfValue(): void { $field = new Field('cdn', 'CDN', '', FieldType::Confirm, FALSE); - $lines = $this->theme()->renderInlineEditor($field, "line one\nline two", TRUE); + $lines = $this->plainTheme()->renderInlineEditor($field, "line one\nline two", TRUE); // The view's first line sits on the label row where the value would be; a // further line aligns under that value column. @@ -93,7 +96,7 @@ public function testBodyExpandsMultiLineValueAcrossRows(): void { $panel = new Panel('p', 'P', '', [new Field('notes', 'Notes', '', FieldType::Textarea, '')]); $answers = new Answers(['notes' => "Crisp and sweet\nHint of citrus"], ['notes' => Provenance::Edited]); - [$lines] = $this->theme()->renderBody($panel, $answers, 0); + [$lines] = $this->plainTheme()->renderBody($panel, $answers, 0); // Each body entry is one physical row: an embedded newline would desync the // box border, the badge alignment and the scroll maths. @@ -118,7 +121,7 @@ public function testBodyExpandsInlineEditorMultiLineView(): void { $panel = new Panel('p', 'P', '', [$field]); // The editor hands back a multi-line caret view for the field being edited. - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0, $field, "Crisp and sweet\nHint of citrus"); + [$lines] = $this->plainTheme()->renderBody($panel, new Answers(), 0, $field, "Crisp and sweet\nHint of citrus"); foreach ($lines as $line) { $this->assertStringNotContainsString("\n", $line); @@ -145,7 +148,7 @@ public function testBodyRendersHintUnderDescription(): void { $field = new Field('name', 'Name', 'The grower of record', FieldType::Text, '', hint: 'Type a few letters to filter.'); $panel = new Panel('p', 'P', '', [$field]); - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0); + [$lines] = $this->plainTheme()->renderBody($panel, new Answers(), 0); $stripped = array_map(Ansi::strip(...), $lines); // Guidance stacks under the row in declaration order: what is being asked, @@ -158,7 +161,7 @@ public function testBodyRendersHintWithoutDescription(): void { $field = new Field('name', 'Name', '', FieldType::Text, '', hint: 'Type a few letters to filter.'); $panel = new Panel('p', 'P', '', [$field]); - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0); + [$lines] = $this->plainTheme()->renderBody($panel, new Answers(), 0); $stripped = array_map(Ansi::strip(...), $lines); $this->assertSame(' Type a few letters to filter.', $stripped[1]); @@ -169,7 +172,7 @@ public function testBodyRendersHintUnderInlineEditor(): void { $field = new Field('name', 'Name', '', FieldType::Text, '', hint: 'Type a few letters to filter.'); $panel = new Panel('p', 'P', '', [$field]); - [$lines] = $this->theme()->renderBody($panel, new Answers(), 0, $field, 'Acme'); + [$lines] = $this->plainTheme()->renderBody($panel, new Answers(), 0, $field, 'Acme'); $stripped = array_map(Ansi::strip(...), $lines); $this->assertStringContainsString('Name Acme', $stripped[0]); @@ -224,7 +227,7 @@ public function testPanelSummaryCollapsesMultiLineValue(): void { $panel = new Panel('sub', 'Sub', '', [new Field('notes', 'Notes', '', FieldType::Textarea, '')]); $answers = new Answers(['notes' => "Crisp and sweet\nHint of citrus"], []); - $summary = $this->theme()->summarizePanel($panel, $answers); + $summary = $this->plainTheme()->summarizePanel($panel, $answers); // A summary is a single line: newlines collapse so a multi-line value does // not break the row it sits on. @@ -238,7 +241,7 @@ public function testBodyNormalizesLineEndingsInMultiLineValue(string $value): vo $panel = new Panel('p', 'P', '', [new Field('notes', 'Notes', '', FieldType::Textarea, '')]); $answers = new Answers(['notes' => $value], []); - [$lines] = $this->theme()->renderBody($panel, $answers, 0); + [$lines] = $this->plainTheme()->renderBody($panel, $answers, 0); // A carriage return would send the terminal cursor back to the row start // and overprint the row, so every line ending an external editor's save @@ -264,7 +267,7 @@ public function testPanelSummaryNormalizesLineEndings(string $value): void { $panel = new Panel('sub', 'Sub', '', [new Field('notes', 'Notes', '', FieldType::Textarea, '')]); $answers = new Answers(['notes' => $value], []); - $summary = $this->theme()->summarizePanel($panel, $answers); + $summary = $this->plainTheme()->summarizePanel($panel, $answers); $this->assertStringNotContainsString("\r", $summary); $this->assertStringNotContainsString("\n", $summary); @@ -278,7 +281,7 @@ public static function dataProviderPanelSummaryNormalizesLineEndings(): \Iterato } public function testPanelLineShowsDrillIndicator(): void { - $line = Ansi::strip($this->theme()->renderPanelLine(new Panel('adv', 'Advanced', ''), TRUE)); + $line = Ansi::strip($this->plainTheme()->renderPanelLine(new Panel('adv', 'Advanced', ''), TRUE)); $this->assertStringContainsString('❯ Advanced', $line); $this->assertStringContainsString('›', $line); @@ -290,7 +293,7 @@ public function testBodyReportsCursorLine(): void { new Field('b', 'B', '', FieldType::Text, ''), ]); - [$lines, $cursor_line] = $this->theme()->renderBody($panel, new Answers(), 1); + [$lines, $cursor_line] = $this->plainTheme()->renderBody($panel, new Answers(), 1); $this->assertSame(2, $cursor_line); $this->assertStringContainsString('❯ B', Ansi::strip($lines[2])); @@ -304,7 +307,7 @@ public function testBodyRendersNoteCardAndSkipsItInTheCursorCount(): void { ]); // Cursor index 1 is the second navigable field; the note is not counted. - [$lines, $cursor_line] = $this->theme()->renderBody($panel, new Answers(['name' => 'Acme', 'agree' => FALSE], []), 1); + [$lines, $cursor_line] = $this->plainTheme()->renderBody($panel, new Answers(['name' => 'Acme', 'agree' => FALSE], []), 1); $body = Ansi::strip(implode("\n", $lines)); $this->assertStringContainsString('Getting started', $body); @@ -321,17 +324,17 @@ public function testBodySkipsEmptyNote(): void { new Field('blank', '', '', FieldType::Note, ''), ]); - [$lines] = $this->theme()->renderBody($panel, new Answers(['name' => 'Acme'], []), 0); + [$lines] = $this->plainTheme()->renderBody($panel, new Answers(['name' => 'Acme'], []), 0); // A note with neither title nor body contributes no lines. $this->assertStringContainsString('Name', Ansi::strip(implode("\n", $lines))); - $this->assertSame([], $this->theme()->renderNoteLines(new Field('blank', '', '', FieldType::Note, ''), new Answers())); + $this->assertSame([], $this->plainTheme()->renderNoteLines(new Field('blank', '', '', FieldType::Note, ''), new Answers())); } public function testNoteInterpolatesAnswersInTitleAndBody(): void { $note = new Field('echo', 'Hello {{name}}', 'You picked {{fruit}}.', FieldType::Note, ''); - $lines = Ansi::strip(implode("\n", $this->theme()->renderNoteLines($note, new Answers(['name' => 'Ada', 'fruit' => 'pear'], [])))); + $lines = Ansi::strip(implode("\n", $this->plainTheme()->renderNoteLines($note, new Answers(['name' => 'Ada', 'fruit' => 'pear'], [])))); $this->assertStringContainsString('Hello Ada', $lines); $this->assertStringContainsString('You picked pear.', $lines); @@ -384,7 +387,7 @@ public function testPaddedSpacingSeparatesNoteFromTheFieldAbove(): void { public function testRenderNoteLinesBoxesBorderedNote(): void { // The theme frame is borderless, so an opt-in note border falls back to the // single-line box; its glyphs come only from the note. - $lines = $this->theme()->renderNoteLines(new Field('boxed', 'Boxed', 'In a box.', FieldType::Note, '', bordered: TRUE), new Answers()); + $lines = $this->plainTheme()->renderNoteLines(new Field('boxed', 'Boxed', 'In a box.', FieldType::Note, '', bordered: TRUE), new Answers()); $joined = Ansi::strip(implode("\n", $lines)); $this->assertStringContainsString('Boxed', $joined); @@ -406,7 +409,7 @@ public function testRenderTableDrawsAlignedGrid(): void { '└───────┴────────┘', ]; - $this->assertSame($expected, $this->theme()->renderTable(['Fruit', 'Colour'], [['Apple', 'Red']])); + $this->assertSame($expected, $this->plainTheme()->renderTable(['Fruit', 'Colour'], [['Apple', 'Red']])); } public function testRenderTableColorsCellsAndBorders(): void { @@ -441,7 +444,7 @@ public function testRenderTableCapsAtFrameWidth(): void { public function testNoteRendersTableBeneathTitleAndBody(): void { $field = new Field('stock', 'Stock', 'Current basket:', FieldType::Note, '', table: new TableSpec(['Fruit', 'Qty'], [['Apple', '3']])); - $joined = Ansi::strip(implode("\n", $this->theme()->renderNoteLines($field, new Answers()))); + $joined = Ansi::strip(implode("\n", $this->plainTheme()->renderNoteLines($field, new Answers()))); $this->assertStringContainsString('Stock', $joined); $this->assertStringContainsString('Current basket:', $joined); @@ -451,7 +454,7 @@ public function testNoteRendersTableBeneathTitleAndBody(): void { public function testNoteInterpolatesTableCells(): void { $field = new Field('echo', '', '', FieldType::Note, '', table: new TableSpec(['Item'], [['{{fruit}}']])); - $joined = Ansi::strip(implode("\n", $this->theme()->renderNoteLines($field, new Answers(['fruit' => 'pear'], [])))); + $joined = Ansi::strip(implode("\n", $this->plainTheme()->renderNoteLines($field, new Answers(['fruit' => 'pear'], [])))); $this->assertStringContainsString('pear', $joined); $this->assertStringNotContainsString('{{fruit}}', $joined); @@ -461,7 +464,7 @@ public function testNoteTableFoldsInterpolatedNewlines(): void { // An answer carrying newlines - a textarea value - interpolated into a cell // folds to one row so it never splits the grid. $field = new Field('memo', '', '', FieldType::Note, '', table: new TableSpec(['Memo'], [['{{memo}}']])); - $lines = $this->theme()->renderNoteLines($field, new Answers(['memo' => "first\nsecond"], [])); + $lines = $this->plainTheme()->renderNoteLines($field, new Answers(['memo' => "first\nsecond"], [])); foreach ($lines as $line) { $this->assertStringNotContainsString("\n", $line); @@ -472,7 +475,7 @@ public function testNoteTableFoldsInterpolatedNewlines(): void { public function testBorderedNoteBoxesItsTable(): void { $field = new Field('stock', 'Stock', '', FieldType::Note, '', bordered: TRUE, table: new TableSpec(['Fruit'], [['Apple']])); - $joined = Ansi::strip(implode("\n", $this->theme()->renderNoteLines($field, new Answers()))); + $joined = Ansi::strip(implode("\n", $this->plainTheme()->renderNoteLines($field, new Answers()))); $this->assertStringContainsString('Stock', $joined); $this->assertStringContainsString('Apple', $joined); @@ -485,7 +488,7 @@ public function testBodyIncludesSubPanels(): void { new Panel('sub', 'Sub', 'sub desc'), ]); - [$lines, $cursor_line] = $this->theme()->renderBody($panel, new Answers(), 1); + [$lines, $cursor_line] = $this->plainTheme()->renderBody($panel, new Answers(), 1); // The cursor is on the sub-panel (index 1, after the single field). $this->assertSame(1, $cursor_line); @@ -498,7 +501,7 @@ public function testBodyIncludesPanelSummary(): void { new Panel('general', 'General', 'the general panel', [new Field('name', 'Name', '', FieldType::Text, '')]), ]); - [$lines] = $this->theme()->renderBody($hub, new Answers(['name' => 'Acme'], []), 0); + [$lines] = $this->plainTheme()->renderBody($hub, new Answers(['name' => 'Acme'], []), 0); // The hub shows the sub-panel's title, description and value summary. $body = Ansi::strip(implode("\n", $lines)); @@ -520,16 +523,26 @@ public function testPanelSummaryJoinsActiveValues(): void { // "gated" is skipped (no answer), the multiselect condenses to a pluralized // count, and only the first four active values appear ("Delta" is dropped). - $this->assertSame('Acme · Beta · 4 items selected · Gamma', $this->theme()->summarizePanel($panel, $answers)); + $this->assertSame('Acme · Beta · 4 items selected · Gamma', $this->plainTheme()->summarizePanel($panel, $answers)); } public function testSummaryLineClipsToWidth(): void { - $line = Ansi::strip($this->theme()->renderSummaryLine(str_repeat('x', 100), FALSE)); + $line = Ansi::strip($this->plainTheme()->renderSummaryLine(str_repeat('x', 100), FALSE)); $this->assertLessThanOrEqual(40, mb_strlen($line)); $this->assertStringContainsString('…', $line); } + public function testSummaryLineClipsWithoutTheGlyphInAsciiMode(): void { + $theme = new DefaultTheme(40, ['color' => FALSE, 'unicode' => FALSE]); + + $line = Ansi::strip($theme->renderSummaryLine(str_repeat('x', 100), FALSE)); + + // ASCII spends no column on a marker, so the clip still fits the width. + $this->assertLessThanOrEqual(40, mb_strlen($line)); + $this->assertStringNotContainsString('…', $line); + } + public function testSelectedItemIsBold(): void { $theme = new DefaultTheme(40); $field = new Field('name', 'Name', '', FieldType::Text, ''); @@ -548,7 +561,7 @@ public function testSelectedItemIsBold(): void { public function testFrameShowsIndicatorsAndWindow(): void { $body = array_map(static fn(int $i): string => 'line' . $i, range(0, 9)); - $frame = $this->theme()->renderFrame(['HEAD'], $body, ['FOOT'], new Viewport(3, TRUE, TRUE), 4); + $frame = $this->plainTheme()->renderFrame(['HEAD'], $body, ['FOOT'], new Viewport(3, TRUE, TRUE), 4); $this->assertStringContainsString('▲', $frame); $this->assertStringContainsString('▼', $frame); @@ -561,16 +574,16 @@ public function testFrameShowsIndicatorsAndWindow(): void { public function testBreadcrumbLine(): void { $navigator = new Navigator(new Panel('hub', 'Hub', '', [], [new Panel('d', 'Drupal', '')])); - $this->assertSame('Hub', Ansi::strip($this->theme()->renderBreadcrumbLine($navigator))); + $this->assertSame('Hub', Ansi::strip($this->plainTheme()->renderBreadcrumbLine($navigator))); } public function testBanner(): void { - $banner = Ansi::strip($this->theme()->renderBanner("LOGO\nline", '1.2.3')); + $banner = Ansi::strip($this->plainTheme()->renderBanner("LOGO\nline", '1.2.3')); $this->assertStringContainsString('LOGO', $banner); $this->assertStringContainsString('Version: 1.2.3', $banner); - $this->assertStringNotContainsString('Version', Ansi::strip($this->theme()->renderBanner('LOGO', ''))); + $this->assertStringNotContainsString('Version', Ansi::strip($this->plainTheme()->renderBanner('LOGO', ''))); } public function testHintsLineIsThemed(): void { @@ -730,11 +743,11 @@ public function testPanelError(): void { public function testDimRecedesText(): void { // With colour, dim wraps the text; with colour off, it is left untouched. $this->assertSame("\033[2mx\033[0m", (new DefaultTheme(40))->dim('x')); - $this->assertSame('x', $this->theme()->dim('x')); + $this->assertSame('x', $this->plainTheme()->dim('x')); } public function testRenderModalCentersDialogOverBackdrop(): void { - $theme = $this->theme(); + $theme = $this->plainTheme(); $modal = new Panel('c', 'Confirm', 'Proceed with care.', [ new Field('opt', 'Option', '', FieldType::Text, 'val'), ], [], new Modal(new Buttons(TRUE, 'Yes', 'No'))); @@ -753,7 +766,7 @@ public function testRenderModalCentersDialogOverBackdrop(): void { } public function testRenderModalWithoutFields(): void { - $theme = $this->theme(); + $theme = $this->plainTheme(); $modal = new Panel('n', 'Notice', 'Saved successfully.', [], [], new Modal()); $backdrop = "aaaaaa\nbbbbbb\ncccccc\ndddddd\neeeeee\nffffff\ngggggg\nhhhhhh"; @@ -780,7 +793,7 @@ public function testRenderModalForcesBorderOnBorderlessTheme(): void { } public function testRenderModalScrollsBodyAndPinsButtonsWhenTall(): void { - $theme = $this->theme(); + $theme = $this->plainTheme(); $fields = []; $values = []; for ($i = 1; $i <= 10; $i++) { @@ -801,17 +814,4 @@ public function testRenderModalScrollsBodyAndPinsButtonsWhenTall(): void { $this->assertStringNotContainsString('Field 10', $out); } - /** - * A colourless borderless theme of fixed width. - * - * The explicit border and spacing keep these layout assertions on the - * plain look, independent of the theme's bordered-and-padded defaults. - * - * @return \DrevOps\Tui\Theme\DefaultTheme - * The theme. - */ - protected function theme(): DefaultTheme { - return new DefaultTheme(40, ['color' => FALSE, 'border' => Border::None, 'spacing' => Spacing::Normal]); - } - } diff --git a/tests/phpunit/Unit/Theme/ThemeTest.php b/tests/phpunit/Unit/Theme/ThemeTest.php index 71cb05c4..f1b7f9b5 100644 --- a/tests/phpunit/Unit/Theme/ThemeTest.php +++ b/tests/phpunit/Unit/Theme/ThemeTest.php @@ -17,7 +17,7 @@ * Tests the theme's semantic styler and symbol methods. */ #[CoversClass(DefaultTheme::class)] -#[Group('tui')] +#[Group('theme')] final class ThemeTest extends TestCase { #[DataProvider('dataProviderStyler')] diff --git a/tests/phpunit/Unit/Translation/ChromeCatalogTest.php b/tests/phpunit/Unit/Translation/ChromeCatalogTest.php index 5bfe01f8..b784b59c 100644 --- a/tests/phpunit/Unit/Translation/ChromeCatalogTest.php +++ b/tests/phpunit/Unit/Translation/ChromeCatalogTest.php @@ -18,7 +18,7 @@ * chrome stays complete and honest. */ #[CoversNothing] -#[Group('tui')] +#[Group('translation')] final class ChromeCatalogTest extends TestCase { public function testTemplateMatchesSourceLiterals(): void { diff --git a/tests/phpunit/Unit/Translation/TranslationRenderTest.php b/tests/phpunit/Unit/Translation/TranslationRenderTest.php index dcb42903..632afbab 100644 --- a/tests/phpunit/Unit/Translation/TranslationRenderTest.php +++ b/tests/phpunit/Unit/Translation/TranslationRenderTest.php @@ -9,10 +9,10 @@ use DrevOps\Tui\Answers\SummaryFormatter; use DrevOps\Tui\Builder\Form; use DrevOps\Tui\Builder\PanelBuilder; -use DrevOps\Tui\Model\Field; -use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\Field; +use DrevOps\Tui\Model\FormDefinition; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\PanelController; use DrevOps\Tui\Schema\AgentHelp; @@ -34,7 +34,7 @@ #[CoversClass(SummaryFormatter::class)] #[CoversClass(SchemaValidator::class)] #[CoversClass(AgentHelp::class)] -#[Group('tui')] +#[Group('translation')] final class TranslationRenderTest extends TestCase { use ResetsTranslatorTrait; diff --git a/tests/phpunit/Unit/Translation/TranslatorTest.php b/tests/phpunit/Unit/Translation/TranslatorTest.php index 988b2e60..597a1fd1 100644 --- a/tests/phpunit/Unit/Translation/TranslatorTest.php +++ b/tests/phpunit/Unit/Translation/TranslatorTest.php @@ -16,7 +16,7 @@ * Tests the translator and its static t() bridge. */ #[CoversClass(Translator::class)] -#[Group('tui')] +#[Group('translation')] final class TranslatorTest extends TestCase { use IsolatesEnvTrait; diff --git a/tests/phpunit/Unit/TuiTest.php b/tests/phpunit/Unit/TuiTest.php index 93581d56..77903955 100644 --- a/tests/phpunit/Unit/TuiTest.php +++ b/tests/phpunit/Unit/TuiTest.php @@ -13,7 +13,6 @@ use DrevOps\Tui\Discovery\Dotenv; use DrevOps\Tui\Discovery\JsonValue; use DrevOps\Tui\Engine\Engine; -use DrevOps\Tui\Primitive\Progress; use DrevOps\Tui\Handler\Context; use DrevOps\Tui\Handler\HandlerRegistry; use DrevOps\Tui\Input\Action; @@ -23,6 +22,7 @@ use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Input\Scope; use DrevOps\Tui\InterruptException; +use DrevOps\Tui\Primitive\Progress; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Render\PanelController; use DrevOps\Tui\Render\Terminal; diff --git a/tests/phpunit/Unit/Widget/CalendarWidgetTest.php b/tests/phpunit/Unit/Widget/CalendarWidgetTest.php index 5604c32b..1f75ae1f 100644 --- a/tests/phpunit/Unit/Widget/CalendarWidgetTest.php +++ b/tests/phpunit/Unit/Widget/CalendarWidgetTest.php @@ -4,13 +4,13 @@ namespace DrevOps\Tui\Tests\Unit\Widget; -use DrevOps\Tui\Model\DateBounds; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Weekday; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyMapManager; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\DateBounds; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Weekday; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; diff --git a/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php b/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php index 4d893f2c..9d7b801c 100644 --- a/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php +++ b/tests/phpunit/Unit/Widget/FilePickerWidgetTest.php @@ -4,21 +4,21 @@ namespace DrevOps\Tui\Tests\Unit\Widget; +use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Input\KeyMapManager; +use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Model\FieldType; use DrevOps\Tui\Model\FilePickerConstraints; use DrevOps\Tui\Model\FilePickerMode; use DrevOps\Tui\Model\SelectionBounds; -use DrevOps\Tui\Input\Key; -use DrevOps\Tui\Input\KeyMapManager; -use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; use DrevOps\Tui\Theme\DefaultTheme; use DrevOps\Tui\Widget\AbstractWidget; -use DrevOps\Tui\Widget\FilePickerWidget; use DrevOps\Tui\Widget\Capability\PagingCapableTrait; use DrevOps\Tui\Widget\Capability\SelectionBoundedTrait; +use DrevOps\Tui\Widget\FilePickerWidget; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversTrait; @@ -206,6 +206,18 @@ public function testTypeToFilterNarrowsEntries(): void { $this->assertSame($this->root . '/docs', $widget->value()); } + public function testTypeToFilterFoldsCaseBeyondAscii(): void { + vfsStream::setup('accents', NULL, ['Äpfel.md' => '', 'pears.md' => '']); + $widget = new FilePickerWidget(vfsStream::url('accents')); + + $widget->handle(Key::char('ä')); + + // A lowercase non-ASCII query matches its uppercase entry, which a + // byte-level fold would miss. + $this->assertSame(vfsStream::url('accents') . '/Äpfel.md', $widget->value()); + $this->assertStringNotContainsString('pears.md', $this->render($widget)); + } + public function testBackspaceAscendsWhenFilterEmpty(): void { $widget = new FilePickerWidget($this->root); diff --git a/tests/phpunit/Unit/Widget/NumberWidgetTest.php b/tests/phpunit/Unit/Widget/NumberWidgetTest.php index 7cb2eec9..8f370ad6 100644 --- a/tests/phpunit/Unit/Widget/NumberWidgetTest.php +++ b/tests/phpunit/Unit/Widget/NumberWidgetTest.php @@ -4,17 +4,17 @@ namespace DrevOps\Tui\Tests\Unit\Widget; -use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\NumberBounds; use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Widget\NumberWidget; use DrevOps\Tui\Widget\Capability\PlaceholderCapableTrait; use DrevOps\Tui\Widget\Capability\TextEditCapableTrait; +use DrevOps\Tui\Widget\NumberWidget; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Widget/PasswordDisplayTest.php b/tests/phpunit/Unit/Widget/PasswordDisplayTest.php index 33507913..4be1ebe3 100644 --- a/tests/phpunit/Unit/Widget/PasswordDisplayTest.php +++ b/tests/phpunit/Unit/Widget/PasswordDisplayTest.php @@ -25,13 +25,13 @@ public function testNext(PasswordDisplay $from, PasswordDisplay $to): void { /** * Data provider for testNext(). * - * @return \Iterator<(int | string), array{\DrevOps\Tui\Widget\PasswordDisplay, \DrevOps\Tui\Widget\PasswordDisplay}> + * @return \Iterator * The current display and the one that follows it. */ public static function dataProviderNext(): \Iterator { - yield [PasswordDisplay::Hidden, PasswordDisplay::Masked]; - yield [PasswordDisplay::Masked, PasswordDisplay::Plaintext]; - yield [PasswordDisplay::Plaintext, PasswordDisplay::Hidden]; + yield 'hidden to masked' => [PasswordDisplay::Hidden, PasswordDisplay::Masked]; + yield 'masked to plaintext' => [PasswordDisplay::Masked, PasswordDisplay::Plaintext]; + yield 'plaintext to hidden' => [PasswordDisplay::Plaintext, PasswordDisplay::Hidden]; } } diff --git a/tests/phpunit/Unit/Widget/PasswordWidgetTest.php b/tests/phpunit/Unit/Widget/PasswordWidgetTest.php index c945878b..ec312066 100644 --- a/tests/phpunit/Unit/Widget/PasswordWidgetTest.php +++ b/tests/phpunit/Unit/Widget/PasswordWidgetTest.php @@ -10,9 +10,9 @@ use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Widget\PasswordWidget; use DrevOps\Tui\Widget\Capability\PlaceholderCapableTrait; use DrevOps\Tui\Widget\Capability\TextEditCapableTrait; +use DrevOps\Tui\Widget\PasswordWidget; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Widget/SelectWidgetTest.php b/tests/phpunit/Unit/Widget/SelectWidgetTest.php index 8daffe04..b1062aed 100644 --- a/tests/phpunit/Unit/Widget/SelectWidgetTest.php +++ b/tests/phpunit/Unit/Widget/SelectWidgetTest.php @@ -4,15 +4,15 @@ namespace DrevOps\Tui\Tests\Unit\Widget; -use DrevOps\Tui\Model\FieldType; -use DrevOps\Tui\Model\Option; -use DrevOps\Tui\Model\OptionKind; -use DrevOps\Tui\Model\SelectionBounds; use DrevOps\Tui\Input\Action; use DrevOps\Tui\Input\Hint; use DrevOps\Tui\Input\Key; use DrevOps\Tui\Input\KeyMapManager; use DrevOps\Tui\Input\KeyName; +use DrevOps\Tui\Model\FieldType; +use DrevOps\Tui\Model\Option; +use DrevOps\Tui\Model\OptionKind; +use DrevOps\Tui\Model\SelectionBounds; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; diff --git a/tests/phpunit/Unit/Widget/TextareaWidgetTest.php b/tests/phpunit/Unit/Widget/TextareaWidgetTest.php index 2826b23c..fd026d20 100644 --- a/tests/phpunit/Unit/Widget/TextareaWidgetTest.php +++ b/tests/phpunit/Unit/Widget/TextareaWidgetTest.php @@ -10,9 +10,9 @@ use DrevOps\Tui\Testing\ArrayKeyStream; use DrevOps\Tui\Testing\WidgetRunner; use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Widget\TextareaWidget; use DrevOps\Tui\Widget\Capability\PlaceholderCapableTrait; use DrevOps\Tui\Widget\Capability\TextEditCapableTrait; +use DrevOps\Tui\Widget\TextareaWidget; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/phpunit/Unit/Widget/WidgetFactoryTest.php b/tests/phpunit/Unit/Widget/WidgetFactoryTest.php index f2330951..b52f3a16 100644 --- a/tests/phpunit/Unit/Widget/WidgetFactoryTest.php +++ b/tests/phpunit/Unit/Widget/WidgetFactoryTest.php @@ -5,6 +5,10 @@ namespace DrevOps\Tui\Tests\Unit\Widget; use DrevOps\Tui\Handler\HandlerRegistry; +use DrevOps\Tui\Input\Hint; +use DrevOps\Tui\Input\Key; +use DrevOps\Tui\Input\KeyMapManager; +use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Model\DateBounds; use DrevOps\Tui\Model\Field; use DrevOps\Tui\Model\FieldType; @@ -12,14 +16,10 @@ use DrevOps\Tui\Model\Option; use DrevOps\Tui\Model\OptionKind; use DrevOps\Tui\Model\Template; -use DrevOps\Tui\Input\Hint; -use DrevOps\Tui\Input\Key; -use DrevOps\Tui\Input\KeyMapManager; -use DrevOps\Tui\Input\KeyName; use DrevOps\Tui\Render\Ansi; use DrevOps\Tui\Theme\DefaultTheme; -use DrevOps\Tui\Widget\ConfirmWidget; use DrevOps\Tui\Widget\CalendarWidget; +use DrevOps\Tui\Widget\ConfirmWidget; use DrevOps\Tui\Widget\FilePickerWidget; use DrevOps\Tui\Widget\NumberWidget; use DrevOps\Tui\Widget\PasswordWidget;