31namespace morph::ui::testing {
54class ConformanceProbe {
56 ConformanceProbe() =
default;
57 virtual ~ConformanceProbe() =
default;
58 ConformanceProbe(ConformanceProbe
const&) =
delete;
59 ConformanceProbe& operator=(ConformanceProbe
const&) =
delete;
60 ConformanceProbe(ConformanceProbe&&) =
delete;
61 ConformanceProbe& operator=(ConformanceProbe&&) =
delete;
78 [[nodiscard]]
virtual std::string
textOf(
Widget const& widget) = 0;
176 [[nodiscard]]
virtual std::optional<WidgetBounds>
boundsOf(
Widget const& widget) {
177 static_cast<void>(widget);
199 void text(std::string_view what, std::string_view expected, std::string_view actual) {
200 if (expected != actual) {
201 fail(std::string{what} +
": expected '" + std::string{expected} +
"', got '" + std::string{actual} +
"'");
208 void that(
bool holds, std::string_view what) {
210 fail(std::string{what});
216 [[nodiscard]] std::optional<std::string>
result()
const {
return _failure; }
219 void fail(std::string message) {
220 if (!_failure.has_value()) {
221 _failure = std::move(message);
225 std::optional<std::string> _failure;
246 rows, [](
Entry const& entry) {
return Key{entry.
id}; },
248 if (builds !=
nullptr) {
251 return ui::text({.text = [&entry] {
return entry.get().
label; }});
261 {{.label =
"Name"}}, [&rows] {
return rows.get(); }, [](
Entry const& entry) {
return Key{entry.id}; },
263 return std::vector<Node>{
ui::text({.text = [&entry] {
return entry.get().label; }})};
294 return container ==
nullptr ? nullptr : probe.
childAt(*container, index);
303 for (std::size_t i = 0; i < probe.
childCount(container); ++i) {
308 texts += child ==
nullptr ? std::string{
"<null>"} : probe.
textOf(*child);
320 if (!texts.empty()) {
324 texts += cell ==
nullptr ? std::string{
"<null>"} : probe.
textOf(*cell);
332[[nodiscard]]
inline std::string
keyTexts(std::vector<Key>
const& keys) {
334 for (
Key const& key : keys) {
335 if (!texts.empty()) {
338 if (
auto const*
const number = std::get_if<std::int64_t>(&key); number !=
nullptr) {
339 texts += std::to_string(*number);
341 texts +=
'"' + std::get<std::string>(key) +
'"';
354 checks.
text(
"a constant Text",
"hello", probe.
textOf(view.
root()));
372 runtime.
batch([&word] {
377 checks.
text(
"a bound Text after a batch of two writes",
"three", probe.
textOf(view.
root()));
380 checks.
that(evaluations == 2,
"the binding ran " + std::to_string(evaluations) +
381 " times; expected once at mount and once for the batch");
392 .common = {.visible = [&shown] {
return shown.
get(); },
393 .enabled = [&shown] {
return shown.
get(); }}})};
396 checks.
that(probe.
visibleOf(view.
root()),
"a Button bound visible=true is hidden");
397 checks.
that(probe.
enabledOf(view.
root()),
"a Button bound enabled=true is disabled");
400 checks.
that(!probe.
visibleOf(view.
root()),
"a Button bound visible=false is shown");
401 checks.
that(!probe.
enabledOf(view.
root()),
"a Button bound enabled=false is enabled");
411 ui::button({.label = [&clicks] {
return "clicked " + std::to_string(clicks.
get()); },
412 .onClick = [&clicks] { clicks.
set(clicks.
peek() + 1); }})};
417 checks.
that(clicks.
peek() == 1,
"one click ran the action " + std::to_string(clicks.
peek()) +
" times");
418 checks.
text(
"the label bound to the click count",
"clicked 1", probe.
textOf(view.
root()));
431 [&name, &changes](std::string
text) {
439 checks.
that(changes >= 1,
"typing did not reach onChange");
440 checks.
text(
"the value after typing",
"abc", name.
peek());
441 checks.
text(
"the field after typing",
"abc", probe.
textOf(view.
root()));
442 int const typed = changes;
445 checks.
text(
"the field after the application set a value",
"xyz", probe.
textOf(view.
root()));
446 checks.
that(changes == typed,
"a value set by the application was reported through onChange");
457 .cases = {{.key =
Key{std::int64_t{0}}, .node =
ui::text({.text =
"zero"})},
458 {.key =
Key{std::int64_t{1}}, .node =
ui::text({.text =
"one"})}}})};
468 checks.
that(probe.
childCount(
slot) == 0,
"a key with no case and no fallback still shows a child");
482 {.label =
"B", .node =
ui::text({.text =
"second"})}},
483 .selected = [&selected] {
return selected.
get(); },
484 .onSelect = [&selected](std::size_t index) { selected.
set(index); }})};
488 checks.
that(probe.
childCount(bar) == 1,
"Tabs mounted a page that was never selected");
491 checks.
that(probe.
childCount(bar) == 2,
"selecting a tab did not mount its page");
494 checks.
that(first !=
nullptr && !probe.
visibleOf(*first),
"the unselected tab's page is shown");
495 checks.
that(second !=
nullptr && probe.
visibleOf(*second),
"the selected tab's page is hidden");
499 checks.
that(probe.
childCount(bar) == 2,
"re-selecting a tab mounted its page again");
501 checks.
that(reselected !=
nullptr && probe.
visibleOf(*reselected),
"the re-selected tab's page is hidden");
519 rows.
set({{.id = 1, .label =
"a2"}, {.id = 2, .label =
"b"}});
522 checks.
that(builds == 2,
"updating a kept row built its view again: " + std::to_string(builds) +
523 " row views built for two keys");
524 checks.
that(probe.
childAt(list, 0) == before,
"updating a kept row replaced its widget");
525 checks.
text(
"the rows after an update",
"a2,b",
childTexts(probe, list));
534 probe.
runtime(), {{.id = 1, .label =
"a"}, {.id = 2, .label =
"b"}, {.id = 3, .label =
"c"}}};
539 checks.
text(
"the rows as mounted",
"a,b,c",
childTexts(probe, list));
540 rows.
set({{.id = 3, .label =
"c"}, {.id = 1, .label =
"a"}});
542 checks.
text(
"the rows after removing b and moving c first",
"c,a",
childTexts(probe, list));
543 rows.
set({{.id = 3, .label =
"c"}, {.id = 4, .label =
"d"}, {.id = 1, .label =
"a"}});
545 checks.
text(
"the rows after inserting d in the middle",
"c,d,a",
childTexts(probe, list));
554 probe.
runtime(), {{.id = 1, .label =
"a"}, {.id = 2, .label =
"b"}, {.id = 3, .label =
"c"}}};
558 rows, [](
Entry const& entry) {
return Key{entry.
id}; },
565 if (field ==
nullptr) {
566 return "a ForEach of text fields shows no first field";
568 probe.
type(*field,
"edited");
571 checks.
text(
"the fields after typing into the first",
"edited,,",
childTexts(probe, list));
572 rows.
set({{.id = 2, .label =
"b"}, {.id = 3, .label =
"c"}, {.id = 1, .label =
"a"}});
574 checks.
that(probe.
childAt(list, 2) == field,
"moving a row replaced its widget");
575 checks.
text(
"the fields after moving the edited one last; moveChild must keep a widget's native state",
",,edited",
588 {.open = [&open] {
return open.get(); }, .title =
"Confirm", .child =
ui::text({.text =
"inside"})})};
613 dialog->setTitle(
"Confirm");
614 dialog->setOnDismiss([&dismissals] { ++dismissals; });
617 button->setOnClick([&clicks] { ++clicks; });
623 checks.
that(clicks == 0,
"a click reached a button inside a new dialog; a new dialog is closed");
624 checks.
that(dismissals == 0,
"a new dialog was dismissed; a new dialog is closed");
629 checks.
that(clicks == 1,
630 "a click on a button inside an open dialog ran its action " + std::to_string(clicks) +
" times");
633 checks.
that(dismissals == 1,
634 "dismissing an open dialog called onDismiss " + std::to_string(dismissals) +
" times");
640 checks.
that(clicks == 1,
"a click reached a button inside a closed dialog");
641 checks.
that(dismissals == 1,
"a closed dialog was dismissed");
664 ui::button({.label =
"Go", .onClick = [&clicks] { ++clicks; }}),
665 ui::textInput({.onChange = [&edits](std::string
const&) { ++edits; }}),
666 ui::text({.text =
"card", .common = {.dragKey = std::optional<Key>{
Key{std::int64_t{7}}}}}),
668 .onSelectionChange = [&selections](std::vector<Key>
const&) { ++selections; }})}});
673 .collapsed = [&collapsed] {
return collapsed.
get(); }})},
674 .common = {.visible = [&hidden] {
return !hidden.
get(); },
675 .enabled = [&disabled] {
return !disabled.
get(); }}})};
677 ui::panel({.title =
"Target", .common = {.onDrop = [&drops](
Key const&) { ++drops; }}})};
684 if (
button ==
nullptr || field ==
nullptr || card ==
nullptr ||
table ==
nullptr) {
685 return "a Column in a Panel in a Column does not show the inner Column's four children";
689 std::string_view name;
692 std::array<Gate, 3>
const gates{{{.name =
"hidden", .flag = &hidden},
693 {.name =
"disabled", .flag = &disabled},
694 {.name =
"collapsed", .flag = &collapsed}}};
696 std::string
text =
"a";
698 for (Gate
const& gate : gates) {
699 std::string
const where =
" inside a " + std::string{gate.name} +
" container";
700 auto const act = [&] {
707 gate.flag->set(
true);
709 std::array<int, 4>
const before{clicks, edits, drops, selections};
711 checks.
that(clicks == before.at(0),
"a click reached a button" + where);
712 checks.
that(edits == before.at(1),
"typing reached a text field" + where);
713 checks.
that(drops == before.at(2),
"a widget" + where +
" was dragged and dropped");
714 checks.
that(selections == before.at(3),
"a row selection reached a table" + where);
715 gate.flag->set(
false);
718 std::string
const again =
" once its container was no longer " + std::string{gate.name};
719 checks.
that(clicks == before.at(0) + 1,
"a click did not reach a button" + again);
720 checks.
that(edits > before.at(1),
"typing did not reach a text field" + again);
721 checks.
that(drops == before.at(2) + 1,
"a drag did not deliver its key" + again);
722 checks.
that(selections == before.at(3) + 1,
"a row selection did not reach a table" + again);
738 probe.
type(*field,
"abc");
743 "moving the cursor to 1 left it at " + std::to_string(probe.
cursorOf(*field)));
744 field->setText(
"abc");
746 checks.
text(
"the field after setText with the text it shows",
"abc", probe.
textOf(*field));
747 checks.
that(probe.
cursorOf(*field) == 1,
"setText with the text the field shows moved the cursor from 1 to " +
748 std::to_string(probe.
cursorOf(*field)));
761 [&name, &refuse](std::string
text) {
763 std::ranges::transform(
text,
text.begin(), [](
char character) {
764 return character >=
'a' && character <=
'z'
765 ? static_cast<char>(character -
'a' +
'A')
775 checks.
text(
"the field after an edit the document refused",
"kept", probe.
textOf(view.
root()));
780 checks.
text(
"the field after an edit the document upper-cased",
"TY~PED", probe.
textOf(view.
root()));
781 checks.
text(
"the value after an edit the document upper-cased",
"TY~PED", name.
peek());
793 ui::dialog({.open = [&open] {
return open.get(); },
795 .child =
ui::button({.label =
"OK", .onClick = [&clicks] { ++clicks; }}),
796 .onDismiss = [&dismissals] { ++dismissals; }})};
802 checks.
that(dismissals == 1,
"dismissing the dialog called onDismiss " + std::to_string(dismissals) +
" times");
804 checks.
that(
button !=
nullptr,
"a dialog kept open lost its content");
809 checks.
that(clicks == 1,
"a click inside a dialog the document kept open ran its action " +
810 std::to_string(clicks) +
" times; the dialog did not open again");
820 using Options = std::vector<SelectOption>;
821 using std::chrono::milliseconds;
825 std::vector<std::string> fired;
826 auto const note = [&fired](std::string_view handler) {
827 return [&fired, handler](
auto const&...) { fired.emplace_back(handler); };
829 auto const odd = [&phase] {
return phase.
get() % 2 == 1; };
830 auto const instant = [odd, &phase] {
831 using Instant = std::chrono::sys_time<milliseconds>;
839 ui::textInput({.value = [&phase] {
return "v" + std::to_string(phase.
get()); },
840 .onChange = note(
"TextInput onChange"),
841 .onSubmit = note(
"TextInput onSubmit")}),
842 ui::checkbox({.label =
"c", .checked = odd, .onToggle = note(
"Checkbox onToggle")}),
851 options.push_back({.key =
Key{std::int64_t{1}}, .label =
"One"});
853 options.push_back({.key =
Key{std::int64_t{2}}, .label =
"Two"});
857 .selected = [&phase] {
return std::optional<Key>{
Key{std::int64_t{phase.
get() < 2 ? 2 : 1}}}; },
858 .onSelect = note(
"Select onSelect")}),
859 ui::menu({.items = {{.label = [&phase] {
return "m" + std::to_string(phase.
get()); },
860 .onSelect = note(
"Menu item onSelect")}}}),
861 ui::slider({.value = [&phase] {
return std::int64_t{phase.
get()}; },
862 .onChange = note(
"Slider onChange"),
863 .onCommit = note(
"Slider onCommit")}),
864 ui::dateTimeInput({.value = instant, .onChange = note(
"DateTimeInput onChange")}),
866 .onPicked = note(
"FilePicker onPicked")}),
867 ui::panel({.title =
"p", .collapsible =
true, .collapsed = odd, .onToggle = note(
"Panel onToggle")}),
869 {.label =
"B", .node =
ui::text({.text =
"second"})}},
870 .selected = [odd] {
return odd() ? std::size_t{1} : std::size_t{0}; },
871 .onSelect = note(
"Tabs onSelect")}),
872 ui::dialog({.open = odd, .title =
"d", .onDismiss = note(
"Dialog onDismiss")}),
876 return phase.
get() < 2
877 ? std::vector<Key>{
Key{std::int64_t{3}}}
878 : std::vector<Key>{
Key{std::int64_t{1}},
Key{std::int64_t{2}}};
880 .onSelectionChange = note(
"Table onSelectionChange"),
881 .onActivate = note(
"Table onActivate")}),
884 for (
int next = 1; next <= 3; ++next) {
887 rows.
set(next % 2 == 1 ? std::vector<Entry>{{.id = 2, .label =
"b"}, {.id = 3, .label =
"c"}}
888 : std::vector<Entry>{{.id = 1, .label =
"a"}, {.id = 2, .label =
"b"}});
892 for (std::string
const& handler : fired) {
893 list += (list.empty() ?
"" :
", ") + handler;
896 checks.
that(fired.empty(),
"a setter called a handler; only a user's action may: " + list);
905 using Options = std::vector<SelectOption>;
908 SelectOption const three{.key =
Key{std::int64_t{3}}, .label =
"Three"};
913 .selected = std::optional<Key>{two.
key},
914 .onSelect = [&choices](
Key const&) { ++choices; }})};
917 checks.
text(
"a Select whose requested key came before any options",
"", probe.
textOf(view.
root()));
918 options.
set({one, three});
920 checks.
text(
"a Select whose options lack the requested key",
"", probe.
textOf(view.
root()));
921 options.
set({one, two});
923 checks.
text(
"a Select once options with the requested key arrived",
"Two", probe.
textOf(view.
root()));
926 checks.
text(
"a Select whose new options lack the requested key",
"", probe.
textOf(view.
root()));
927 options.
set({two, one});
929 checks.
text(
"a Select whose options bring the requested key back",
"Two", probe.
textOf(view.
root()));
930 checks.
that(choices == 0,
"re-marking the requested key called onSelect " + std::to_string(choices) +
" times");
942 std::vector<Entry>
const three{{.id = 1, .label =
"a"}, {.id = 2, .label =
"b"}, {.id = 3, .label =
"c"}};
943 std::vector<Entry> withNine = three;
944 withNine.push_back({.id = 9, .label =
"i"});
947 std::vector<std::vector<Key>> reports;
951 .selection = [&selection] {
return selection.
get(); },
953 [&reports, &selection](std::vector<Key> keys) {
954 reports.push_back(keys);
955 selection.
set(std::move(keys));
959 checks.
text(
"the rows marked for requested keys 2 and 9, with no row 9",
"b",
selectedTexts(probe, view.
root()));
969 checks.
that(reports.empty(),
"rows arriving or going called onSelectionChange");
974 checks.
text(
"the rows marked after the user selected a and c",
"a,c",
selectedTexts(probe, view.
root()));
977 bool const rowlessReported = std::ranges::any_of(reports, [](std::vector<Key>
const& keys) {
978 return std::ranges::any_of(keys, [](
Key const& key) {
979 return key !=
Key{std::int64_t{1}} && key !=
Key{std::int64_t{2}} && key !=
Key{std::int64_t{3}};
982 std::vector<Key> last = reports.empty() ? std::vector<Key>{} : reports.back();
983 std::ranges::sort(last);
984 checks.
that(!reports.empty(),
"a user's selection of rows 1 and 3 did not call onSelectionChange");
985 checks.
that(!rowlessReported,
986 "a user's selection, with key 9 requested and no row 9, reported a key without a row");
987 checks.
text(
"the last report of a user's selection of rows 1 and 3, with key 9 requested and no row 9",
"1,3",
991 checks.
text(
"the rows marked once row 9 arrived after the user's selection",
"a,c",
1004 .onSelectionChange = [&reports](std::vector<Key>
const&) { ++reports; }})};
1009 checks.
that(reports == 1,
"a user's selection called onSelectionChange " + std::to_string(reports) +
" times");
1010 checks.
text(
"the rows marked after the user selected b, with no selection bound",
"b",
1012 rows.
set({{.id = 1, .label =
"a"}, {.id = 2, .label =
"b2"}});
1014 checks.
text(
"the rows marked after the selected row was updated",
"b2",
selectedTexts(probe, view.
root()));
1028 {{.label =
"Note"}, {.label =
"Name"}}, [&rows] {
return rows.
get(); },
1029 [](
Entry const& entry) {
return Key{entry.id}; },
1031 return std::vector<Node>{
ui::text({.text =
"note", .common = {.visible =
false}}),
1032 ui::text({.text = [&entry] {
return entry.get().label; }})};
1037 if (cells ==
nullptr) {
1038 return "a Table with one row shows no row container";
1041 checks.
text(
"the cells of a row whose first cell is hidden",
"note,a",
childTexts(probe, *cells));
1043 checks.
that(first !=
nullptr && !probe.
visibleOf(*first),
"the hidden cell is shown");
1052 std::optional<Mounted> view;
1055 probe.click(view->root());
1058 checks.that(clicks == 1,
"a click ran the action " + std::to_string(clicks) +
" times");
1059 probe.click(view->root());
1060 int const before = clicks;
1063 checks.that(clicks == before,
"a click's handler ran after the button was destroyed");
1064 return checks.result();
1085 if (_life->running ==
this) {
1086 _life->freedWhileRunning =
true;
1116 std::optional<Mounted> view;
1119 ui::button({.label =
"Close", .onClick = [sentinel = LifeSentinel{life}, &view, &clicks] {
1120 HandlerLife* const report = sentinel.life();
1121 std::optional<Mounted>* const own = &view;
1123 report->running = &sentinel;
1125 report->running = nullptr;
1128 probe.click(view->root());
1131 checks.that(clicks == 1,
1132 "the click that destroys its own button ran the action " + std::to_string(clicks) +
" times");
1133 checks.that(!view.has_value(),
"the click did not destroy its own button");
1134 checks.that(!life.freedWhileRunning,
1135 "destroying a button from its own handler destroyed the running handler; the backend must keep it "
1136 "alive until it returns");
1137 Mounted const after{probe.runtime(), probe.backend(),
ui::text({.text =
"after"})};
1139 checks.text(
"a Text mounted after a handler destroyed its own widget",
"after", probe.textOf(after.
root()));
1140 checks.that(clicks == 1,
"a handler ran again after it destroyed its own widget");
1141 return checks.result();
1149 std::vector<Key> dropped;
1151 ui::text({.text =
"card", .common = {.dragKey = std::optional<Key>{
Key{std::int64_t{7}}}}})};
1154 ui::panel({.title =
"Done", .common = {.accepts = std::move(accepts), .onDrop = [&dropped](
Key key) {
1155 dropped.push_back(std::move(key));
1169 std::vector<Key>{
Key{std::int64_t{7}}},
1170 "a drop the target accepts did not deliver the dragged key exactly once");
1180 "a drop the target refuses reached its onDrop");
1190 "a drop target without accepts refused a key");
1199 std::vector<std::string> pressed;
1205 .common = {.keys = {{.chord =
"Ctrl+S",
1206 .onPress = [&pressed] { pressed.emplace_back(
"inner"); }}}}})}})},
1207 .common = {.keys = {{.chord =
"Ctrl+S", .onPress = [&pressed] { pressed.emplace_back(
"outer"); }},
1209 .onPress = [&pressed] { pressed.emplace_back(
"outer quit"); }}}}})};
1213 return "a Button in a Column in a Column is not shown";
1220 checks.
that(pressed == std::vector<std::string>{
"inner",
"outer quit"},
1221 "Ctrl+S then Ctrl+Q with focus on the button ran " + std::to_string(pressed.size()) +
1222 " handlers, not the inner Ctrl+S and the outer Ctrl+Q");
1237 .common = {.keys = {{.chord =
"Ctrl+S", .onPress = [&presses] { ++presses; }}}}})}})},
1238 .common = {.visible = [&shown] {
return shown.
get(); }}})};
1242 return "a Button in a Column in a Column is not shown";
1251 checks.
that(presses == 1,
"a chord ran " + std::to_string(presses) +
1252 " times; once while shown and not again once hidden is expected");
1267 if (first ==
nullptr || second ==
nullptr) {
1268 return "a Column does not show its three children";
1271 checks.
that(probe.
hasFocus(*first),
"the first autofocus input does not have focus");
1272 checks.
that(!probe.
hasFocus(*second),
"the second autofocus input has focus");
1281 std::vector<std::string> chosen;
1282 auto const choose = [&chosen](std::string name) {
1283 return [&chosen, name = std::move(name)] { chosen.push_back(name); };
1286 ui::menu({.items = {{.label =
"Recent",
1287 .onSelect = choose(
"Recent"),
1288 .items = {{.label =
"a.txt", .onSelect = choose(
"a.txt")}}},
1289 {.label =
"Delete", .onSelect = choose(
"Delete"), .enabled = [&deletable] {
1290 return deletable.
get();
1297 deletable.
set(
true);
1302 checks.
that(chosen == std::vector<std::string>{
"a.txt",
"Delete"},
1303 "choosing a submenu, its entry, a disabled entry and then that entry enabled ran " +
1304 std::to_string(chosen.size()) +
" handlers, not the submenu's entry and the enabled one");
1313 std::vector<std::string> changes;
1316 .onChange = [&changes](std::string
text) { changes.push_back(std::move(
text)); },
1317 .field = {.readonly = [&readonly] {
return readonly.
get(); }}})};
1322 checks.
that(changes.empty(),
"typing into a read-only input reached onChange");
1324 "a read-only input shows '" + probe.
textOf(view.
root()) +
"' after typing, not its value 'kept'");
1325 readonly.
set(
false);
1329 checks.
that(changes == std::vector<std::string>{
"typed"},
1330 "typing into the input once editable again did not reach onChange once with the text");
1340 ui::banner({.text =
"Saved", .dismissible =
true, .onDismiss = [&dismissed] { ++dismissed; }})};
1345 checks.
that(dismissed == 1,
1346 "dismissing the banner ran onDismiss " + std::to_string(dismissed) +
" times, not once");
1347 checks.
that(probe.
visibleOf(view.
root()),
"the banner hid itself; the application hides it");
1357 std::vector<std::string> ran;
1361 {.title =
"Advanced",
1362 .open = [&open] {
return open.get(); },
1363 .onToggle = [&open](
bool wanted) { open.set(wanted); },
1364 .header =
ui::button({.label =
"Reset", .onClick = [&ran] { ran.emplace_back(
"header"); }}),
1365 .child =
ui::button({.label =
"Apply", .onClick = [&ran] { ran.emplace_back(
"content"); }})})};
1369 if (header ==
nullptr || content ==
nullptr) {
1370 return "a Collapsible does not show its header and its content as its two children";
1372 probe.
click(*header);
1373 probe.
click(*content);
1377 probe.
click(*content);
1380 checks.
that(ran == std::vector<std::string>{
"header",
"content"},
1381 "clicking the header and the content while closed, then the content once opened, ran " +
1382 std::to_string(ran.size()) +
" handlers, not the header's and then the content's");
1391 std::vector<std::vector<std::string>> drops;
1393 ui::dropZone({.accept = {
".csv"}, .onDrop = [&drops](std::vector<std::string> paths) {
1394 drops.push_back(std::move(paths));
1402 checks.
that(drops == std::vector<std::vector<std::string>>{{
"/in/RUN.CSV"}},
1403 "of an accepted file, a refused one and two at once, the zone took " + std::to_string(drops.size()) +
1404 " drops, not the accepted file alone");
1420 std::optional<WidgetBounds>
const bounds = _probe->boundsOf(widget);
1421 _checks->that(bounds.has_value(),
"the probe reads no bounds, so a layout case measures nothing");
1429 void near(std::string_view what,
double expected,
double actual) {
1431 actual >= expected - 1.0 && actual <= expected + 1.0,
1432 std::string{what} +
": expected " + std::to_string(expected) +
", got " + std::to_string(actual));
1445 Text made{.text = std::move(
text)};
1477 for (std::size_t index = 0; inner !=
nullptr && index < probe.
childCount(*inner); ++index) {
1494 std::vector<Widget*>
const children =
measured(probe, view).children;
1495 checks.
that(children.size() == 2,
"the row holds two children");
1496 if (children.size() == 2) {
1497 geometry.
near(
"the capped stretch child's width", 20, geometry.
of(*children.at(0)).
width);
1498 geometry.
near(
"its stretch sibling's width", 40, geometry.
of(*children.at(1)).
width);
1513 std::vector<Widget*>
const children =
measured(probe, view).children;
1514 checks.
that(children.size() == 2,
"the row holds two children");
1515 if (children.size() == 2) {
1516 geometry.
near(
"a content text with minWidth 30", 30, geometry.
of(*children.at(0)).
width);
1532 std::vector<Widget*>
const& children = inside.
children;
1533 checks.
that(children.size() == 1,
"the column holds one child");
1534 if (children.size() == 1) {
1536 geometry.
near(
"the column's width", 70, outer.
width);
1538 geometry.
near(
"a stretch child with maxWidth 44 across a 70-unit column", 44, card.
width);
1539 geometry.
near(
"its left edge, at the column's start", outer.
x, card.
x);
1556 std::vector<Widget*>
const children =
measured(probe, view).children;
1557 checks.
that(children.size() == 2,
"the column holds two children");
1558 if (children.size() == 2) {
1559 geometry.
near(
"the capped stretch child's height", 5, geometry.
of(*children.at(0)).
height);
1560 geometry.
near(
"its stretch sibling's height", 15, geometry.
of(*children.at(1)).
height);
1576 checks.
that(inside.
children.size() == 1,
"the row holds one child");
1580 geometry.
near(
"the text's height", 1,
text.height);
1581 geometry.
near(
"the text's offset down the row", 1,
text.y - outer.
y);
1599 checks.
that(inside.
children.size() == 3,
"the column holds three children");
1606 geometry.
near(justify ==
Justify::End ?
"end: the last child's end" :
"between: the last child's end",
1609 geometry.
near(
"between: the first child's start", outer.
y, first.
y);
1611 checks.
that(first.
y > outer.
y + 1,
"end: the first child is not at the column's start");
1624 for (
bool const own : {
true,
false}) {
1634 checks.
that(inside.
children.size() == 1,
"the column holds one child");
1640 double const before = placed.
x - outer.
x;
1641 double const after = (outer.
x + outer.
width) - (placed.
x + placed.
width);
1642 geometry.
near(own ?
"alignSelf center: the capped child's width" :
"align center: the card's width",
1643 own ? 20 : 44, placed.
width);
1644 geometry.
near(own ?
"alignSelf center: the space after it, against the space before"
1645 :
"align center: the space after the card, against the space before",
1657 static std::array<ConformanceCase, 7>
const cases{{
1658 {.name =
"a stretch child's maximum caps its share along a row, and its sibling takes the rest",
1662 {.name =
"a stretch child's maximum height caps its share down a column",
1677 static std::array<ConformanceCase, 34>
const cases{{
1679 {.name =
"a bound Text updates once per batch and not for an equal write",
1683 {.name =
"typing reaches onChange and a value set by the application is not echoed",
1685 {.name =
"a Switch shows the selected case and nothing for a key without one",
1687 {.name =
"Tabs mount a page on first selection and show only the selected one",
1694 {.name =
"a widget inside a hidden, disabled or collapsed container takes no input",
1698 {.name =
"a dismissal the document refuses leaves the dialog open",
1701 {.name =
"a Select marks the requested key whenever its options contain it",
1703 {.name =
"a Table reports a user's selection as exactly the existing rows selected",
1705 {.name =
"a Table shows a user's selection with no selection bound",
1716 {.name =
"a menu reports only an enabled entry without a submenu",
1720 {.name =
"a closed collapsible's content takes no input, its header does",
Justify
Where a stack places its spare length along its axis, while no shown child stretches.
@ Between
In equal gaps between the children, never less than the stack's gap.
@ Stretch
A content child fills the cross extent; one its maximum stops sits at the start.
The owner of a signal graph: every node takes one, and every operation runs on its owner.
decltype(auto) batch(F &&fn)
Runs fn as one batch: every write inside it is one flush.
A source value. Reading it with get() inside a tracked run subscribes that run.
T const & get() const
Reads the value, subscribing the innermost tracked run.
void set(T value)
Replaces the value and notifies observers, unless it equals the current one and the policy is Equalit...
T const & peek() const noexcept
Reads the value without subscribing anything.
Makes widgets. Each factory appends the new widget to parent, or makes a root when it is null,...
virtual std::unique_ptr< ButtonWidget > createButton(ContainerWidget *parent)=0
Makes a push button.
virtual std::unique_ptr< DialogWidget > createDialog(ContainerWidget *parent)=0
Makes a modal overlay.
virtual std::unique_ptr< TextInputWidget > createTextInput(ContainerWidget *parent, TextInputMode mode)=0
Makes a text field.
A view tree mounted on a backend: its widgets, built once, and the bindings that keep them current.
Widget & root() const noexcept
The root node's widget.
Collects a case's checks and keeps the first failure, so a case reads top to bottom.
std::optional< std::string > result() const
The case's verdict.
void text(std::string_view what, std::string_view expected, std::string_view actual)
Checks that a text is what it should be.
void that(bool holds, std::string_view what)
Checks a condition.
Reads where the layout cases' widgets are drawn, and fails a case whose probe reads no geometry.
void near(std::string_view what, double expected, double actual)
Checks that a length is what it should be, within one unit.
Geometry(ConformanceProbe &probe, Checks &checks)
Over probe.
WidgetBounds of(Widget const &widget)
Where widget is drawn; zero, with the case failed, when the probe reads no geometry.
LifeSentinel(LifeSentinel &&)=default
A copy, for a moved handler.
~LifeSentinel()
Reports its own destruction when it belongs to the running handler.
LifeSentinel(HandlerLife &life) noexcept
LifeSentinel & operator=(LifeSentinel &&)=default
Copies the report target.
HandlerLife * life() const noexcept
Where it reports.
LifeSentinel & operator=(LifeSentinel const &)=default
Copies the report target.
LifeSentinel(LifeSentinel const &)=default
A copy, for another copy of the handler.
UTC timestamps for morph actions.
morph::ui::Mounted: builds a view tree's widgets through a backend once, and keeps them current throu...
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
Signal<T>, Computed<T> and Effect: the nodes of a reactive graph.
A UTC instant with millisecond precision.
Children stacked top to bottom.
LayoutHints layout
The size request; set once.
A widget's size request in both dimensions, and the bounds its extent is clamped into.
std::optional< Align > alignSelf
Where the node sits across its row's, column's or grid row's axis; nullopt takes the container's alig...
Children laid out left to right.
Key key
The option's identity.
static constexpr Sizing stretch(int weight=1) noexcept
A share of the leftover space.
static constexpr Sizing fixed(int units) noexcept
A fixed size.
Everything about a table beyond its columns and rows.
Common common
Visibility, enablement, layout, drag-and-drop.
A row of the ForEach and Table cases.
std::string label
The text the row shows.
bool operator==(Entry const &) const =default
Memberwise equality.
std::int64_t id
The row's key.
What LifeSentinel reports about the handler that holds it.
bool freedWhileRunning
Set when the running handler's sentinel was destroyed before the handler returned.
void const * running
The sentinel inside the handler while that handler runs; null otherwise.
The container a case measures, the one child of the mounted root, and its children in order.
std::vector< Widget * > children
Its children.
Widget * container
The measured container; null when the root holds none.
The backend contract: the retained widgets a mount drives, and the factory that makes them.
The toolkit-agnostic view tree: props, node kinds and their builders.
Node panel(Panel spec)
A titled frame, optionally collapsible.
Node collapsible(Collapsible spec)
A titled section the user opens and closes.
@ SingleLine
One line; Enter submits.
Node forEach(std::function< std::vector< RowT >()> rows, std::function< Key(RowT const &)> keyOf, std::function< Node(reactive::Signal< RowT > const &)> rowView, Axis axis=Axis::Vertical, int gap=0)
One widget per row of a keyed collection read through a binding (a Query's value() included).
Node slider(Slider spec)
An integer on a range.
Node banner(Banner spec)
A message across the top of a region.
Node textInput(TextInput spec)
An editable text field.
Node column(Column spec)
Children stacked top to bottom.
Node dateTimeInput(DateTimeInput spec)
A date or date-and-time field.
Node dropZone(DropZone spec)
A region that takes dropped files.
@ Multiple
Any number of rows.
Node menu(Menu spec)
A list of commands.
Node filePicker(FilePicker spec)
A file path field.
Node checkbox(Checkbox spec)
A labelled check box.
Node switchOf(Switch spec)
Shows the case the selector picks. Spelled switchOf because switch is a keyword.
Node text(Text spec)
Static or bound text.
Node select(Select spec)
A choice of one option.
std::shared_ptr< NodeData const > Node
A view-tree node: immutable, shared data.
Node row(Row spec)
Children laid out left to right.
std::variant< std::int64_t, std::string > Key
The identity of a Switch case, a Select option, a ForEach or Table row and a drag payload.
Node table(std::vector< TableColumn > columns, std::function< std::vector< RowT >()> rows, std::function< Key(RowT const &)> keyOf, std::function< std::vector< Node >(reactive::Signal< RowT > const &)> cells, TableOptions options={})
A table of keyed rows: one Row of cells per row, under the column headers.
Node tabs(Tabs spec)
A tab bar over pages.
Node dialog(Dialog spec)
A modal overlay.
Node button(Button spec)
A push button.
SlotBinding< std::decay_t< F > > slot(SlotId slotId, F &&read)
A slot with an id.