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 @@
-
\ 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 @@
-├──────────────────────────────────────────┤││╭──────────────────────────────────────────╮│Selectwithgroups›Select││❯FruitFruit││●Apple││○Banana││───────────────────────────────││○Rhubarb(outofseason)││↑/↓move·↵accept·esccancel│╰──────────────────────────────────────────╯
\ No newline at end of file
+├──────────────────────────────────────────┤││╭──────────────────────────────────────────╮│Selectwithgroups›Select││❯FruitFruit││●Apple││○Banana││───────────────────────────────││○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 @@
-├──────────────────────────────────────────┤││╭──────────────────────────────────────────╮│Selectwithgroups›Select││❯FruitFruit││●Apple││○Banana││───────────────────────────────││○Rhubarb(outofseason)││↑/↓move·↵accept·esccancel│╰──────────────────────────────────────────╯
\ No newline at end of file
+├──────────────────────────────────────────┤││╭──────────────────────────────────────────╮│Selectwithgroups›Select││❯FruitFruit││●Apple││○Banana││───────────────────────────────││○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 @@
-├──────────────────────────────────────────┤││╭──────────────────────────────────────────╮│Selectwithgroups›Select││❯FruitFruit││●Apple││○Banana││───────────────────────────────││○Rhubarb(outofseason)││↑/↓move·↵accept·esccancel│╰──────────────────────────────────────────╯
\ No newline at end of file
+├──────────────────────────────────────────┤││╭──────────────────────────────────────────╮│Selectwithgroups›Select││❯FruitFruit││●Apple││○Banana││───────────────────────────────││○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 @@
-├──────────────────────────────────────────┤││╭──────────────────────────────────────────╮│Selectwithgroups›Select││❯FruitFruit││●Apple││○Banana││───────────────────────────────││○Rhubarb(outofseason)││↑/↓move·↵accept·esccancel│╰──────────────────────────────────────────╯
\ No newline at end of file
+├──────────────────────────────────────────┤││╭──────────────────────────────────────────╮│Selectwithgroups›Select││❯FruitFruit││●Apple││○Banana││───────────────────────────────││○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│╰──────────────────────────────────────────────────────────────────────────╯│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
+╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤│││↑/↓move·↵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.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│╰──────────────────────────────────────────────────────────────────────────╯│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
+╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤│││↑/↓move·↵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-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 @@
-├──────────────────────────────────────────────────────────────────────────┤││╭──────────────────────────────────────────────────────────────────────────╮│Tablewidget›Stock││Basketcontents││Everythingpickedsofar:││╭────────────┬────────┬──────────╮│││Fruit│Colour│Instock│││├────────────┼────────┼──────────┤│││Apple│Red│12││││Pear│Green│5││││Blackberry│Purple│120│││╰────────────┴────────┴──────────╯││↑/↓move·↵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│││╰───────┴────────┴──────────╯││↑/↓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 @@
-├──────────────────────────────────────────────────────────────────────────┤││╭──────────────────────────────────────────────────────────────────────────╮│Tablewidget›Stock││Basketcontents││Everythingpickedsofar:││╭────────────┬────────┬──────────╮│││Fruit│Colour│Instock│││├────────────┼────────┼──────────┤│││Apple│Red│12││││Pear│Green│5││││Blackberry│Purple│120│││╰────────────┴────────┴──────────╯││↑/↓move·↵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│││╰───────┴────────┴──────────╯││↑/↓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│╰──────────────────────────────────────────────────────────────────────────╯│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
+╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤│││↑/↓move·↵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.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│╰──────────────────────────────────────────────────────────────────────────╯│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
+╭──────────────────────────────────────────────────────────────────────────╮├──────────────────────────────────────────────────────────────────────────┤│││↑/↓move·↵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-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 @@
-├──────────────────────────────────────────────────────────────────────────┤││╭──────────────────────────────────────────────────────────────────────────╮│Tablewidget›Stock││Basketcontents││Everythingpickedsofar:││╭────────────┬────────┬──────────╮│││Fruit│Colour│Instock│││├────────────┼────────┼──────────┤│││Apple│Red│12││││Pear│Green│5││││Blackberry│Purple│120│││╰────────────┴────────┴──────────╯││↑/↓move·↵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│││╰───────┴────────┴──────────╯││↑/↓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 @@
-├──────────────────────────────────────────────────────────────────────────┤││╭──────────────────────────────────────────────────────────────────────────╮│Tablewidget›Stock││Basketcontents││Everythingpickedsofar:││╭────────────┬────────┬──────────╮│││Fruit│Colour│Instock│││├────────────┼────────┼──────────┤│││Apple│Red│12││││Pear│Green│5││││Blackberry│Purple│120│││╰────────────┴────────┴──────────╯││↑/↓move·↵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│││╰───────┴────────┴──────────╯││↑/↓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;