Modules/Widgets/tests/MenuTests.cpp
Шурупов Илья Викторович 81949323d9 Rename the code name to Sketch2D
Stroke2D becomes Sketch2D across folders, files, namespaces, classes,
targets, scripts, the Android package (online.ilushabasic.sketch2d)
and the update URL. A stroke2d-sketches folder is adopted like the
older leo-sketches one.
2026-10-17 19:47:01 +03:00

564 lines
14 KiB
C++

#include "BoxLayout.hpp"
#include "Menus.hpp"
#include "StackLayout.hpp"
#include "Probes.hpp"
#include "RecordingPainter.hpp"
#include "Ui.hpp"
#include "UiDriver.hpp"
#include "UnitTest++/UnitTest++.h"
using namespace tp;
using namespace tp::ui;
using namespace tp::ui::testing;
namespace {
MenuItem* findItem(Menu& menu, const std::string& text) {
for (const auto& item : menu.items()) {
if (item->entry().text == text) return item.get();
}
return nullptr;
}
}
SUITE(PopupButtonWidget) {
struct Scene {
Widget root;
PopupButton button{ "Options" };
EventProbe content{ { 160, 90 }, "popup content" };
Box elsewhere{ { 100, 40 }, "elsewhere" };
Scene() {
root.emplaceLayout<ColumnLayout>(100).setCrossAlign(CrossAlign::Start);
button.setPopupContent(&content);
root.addChild(button);
root.addChild(elsewhere);
}
};
TEST(MenuItemsDrawTheirIconInTheIndicatorSlot) {
Widget root;
root.emplaceLayout<StackLayout>();
Menu menu;
MenuEntry entry = MenuEntry::item("Open", {});
entry.icon = ImageId{ 7 };
MenuItem& withIcon = menu.add(std::move(entry));
menu.addItem("Plain", {});
root.addChild(menu);
UiDriver driver(root, { 300, 200 });
int images = 0;
for (const PaintCommand& command : driver.paint().commands())
if (command.kind == PaintCommand::Kind::Image) {
images++;
CHECK(command.rect.pos.x < withIcon.windowRect().pos.x + withIcon.windowRect().size.x / 3);
}
CHECK_EQUAL(1, images);
}
TEST(ClickTogglesPopupBelowButton) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.button);
CHECK(scene.button.isPopupOpen());
CHECK_CLOSE(scene.button.windowRect().pos.x, scene.content.windowRect().pos.x, 0.01f);
CHECK(scene.content.windowRect().pos.y >= scene.button.windowRect().pos.y + scene.button.size().y);
driver.click(scene.button);
CHECK(!scene.button.isPopupOpen());
}
TEST(OutsideClickCloses) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.button);
driver.click(scene.elsewhere);
CHECK(!scene.button.isPopupOpen());
}
TEST(InteractingWithContentKeepsItOpen) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.button);
driver.click(scene.content);
CHECK(scene.button.isPopupOpen());
CHECK_EQUAL(1, scene.content.count(Event::Type::Click));
}
TEST(OpenOnHover) {
Scene scene;
scene.button.setOpenOnHover(true);
UiDriver driver(scene.root);
driver.moveTo(scene.button);
CHECK(scene.button.isPopupOpen());
}
TEST(PlacementOptionsAreHonored) {
Scene scene;
scene.button.popupOptions().placement = Placement::Right;
UiDriver driver(scene.root);
driver.click(scene.button);
CHECK(scene.content.windowRect().pos.x >= scene.button.windowRect().pos.x + scene.button.size().x);
}
}
SUITE(MenuWidget) {
struct Scene {
Widget root;
PopupButton file{ "File" };
Menu menu;
Menu recent;
int opened = 0;
int savedAs = 0;
bool gridVisible = false;
Scene() {
root.emplaceLayout<ColumnLayout>().setCrossAlign(CrossAlign::Start);
menu.addItem("Open", [this]() { opened++; }, "Ctrl+O");
menu.addSubmenu("Open Recent", recent);
menu.addSeparator();
menu.addToggle("Show Grid", [this]() { return gridVisible; }, [this]() { gridVisible = !gridVisible; });
recent.addItem("project.sketch2d", [this]() { savedAs++; });
file.setPopupContent(&menu);
root.addChild(file);
}
};
TEST(ActivatingItemRunsActionAndClosesMenu) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.file);
driver.click(*findItem(scene.menu, "Open"));
CHECK_EQUAL(1, scene.opened);
CHECK(!scene.menu.isOpen());
CHECK(!scene.file.isPopupOpen());
}
TEST(MenuSizesToWidestItemAndStacksItems) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.file);
MenuItem& open = *findItem(scene.menu, "Open");
MenuItem& grid = *findItem(scene.menu, "Show Grid");
CHECK_CLOSE(open.size().x, grid.size().x, 0.01f);
CHECK(grid.rect().pos.y > open.rect().pos.y);
CHECK_CLOSE(driver.ui().theme().metrics.menuItemHeight, open.size().y, 0.01f);
}
TEST(PaintsLabelsAndShortcuts) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.file);
RecordingPainter& painter = driver.paint();
CHECK(painter.hasText("Open"));
CHECK(painter.hasText("Ctrl+O"));
CHECK(painter.hasText("Show Grid"));
}
TEST(ToggleItemFlipsState) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.file);
driver.click(*findItem(scene.menu, "Show Grid"));
CHECK(scene.gridVisible);
driver.click(scene.file);
driver.click(*findItem(scene.menu, "Show Grid"));
CHECK(!scene.gridVisible);
}
TEST(HoveringSubmenuOpensItToTheRight) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.file);
MenuItem& recentItem = *findItem(scene.menu, "Open Recent");
driver.moveTo(recentItem);
CHECK(scene.recent.isOpen());
CHECK(scene.recent.windowRect().pos.x >= scene.menu.windowRect().pos.x + scene.menu.size().x - 1);
CHECK_EQUAL(2u, driver.ui().openPopupCount());
}
TEST(SubmenuOfAMenuOpenedLeftwardsOpensToTheLeftWithoutCoveringIt) {
Scene scene;
scene.root.layout()->setPadding(Insets{ 700, 0, 0, 0 });
scene.file.popupOptions().placement = Placement::Left;
UiDriver driver(scene.root);
driver.click(scene.file);
REQUIRE CHECK(scene.menu.isOpen());
driver.click(*findItem(scene.menu, "Open Recent"));
REQUIRE CHECK(scene.recent.isOpen());
const RectF parent = scene.menu.windowRect();
const RectF child = scene.recent.windowRect();
CHECK(child.pos.x + child.size.x <= parent.pos.x + 1);
}
TEST(HoveringSiblingClosesSubmenu) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.file);
driver.moveTo(*findItem(scene.menu, "Open Recent"));
driver.moveTo(*findItem(scene.menu, "Open"));
CHECK(!scene.recent.isOpen());
CHECK(scene.menu.isOpen());
}
TEST(ActivatingSubmenuItemClosesWholeChain) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.file);
driver.moveTo(*findItem(scene.menu, "Open Recent"));
driver.click(*findItem(scene.recent, "project.sketch2d"));
CHECK_EQUAL(1, scene.savedAs);
CHECK_EQUAL(0u, driver.ui().openPopupCount());
}
TEST(MovingIntoSubmenuKeepsItOpen) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.file);
driver.moveTo(*findItem(scene.menu, "Open Recent"));
driver.moveTo(*findItem(scene.recent, "project.sketch2d"));
CHECK(scene.recent.isOpen());
CHECK(scene.menu.isOpen());
}
TEST(EscapeClosesSubmenuThenMenu) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.file);
driver.moveTo(*findItem(scene.menu, "Open Recent"));
driver.key(InputID::ESCAPE);
CHECK(!scene.recent.isOpen());
CHECK(scene.menu.isOpen());
driver.key(InputID::ESCAPE);
CHECK(!scene.menu.isOpen());
}
TEST(DisabledItemsDoNothing) {
Widget root;
root.emplaceLayout<ColumnLayout>().setCrossAlign(CrossAlign::Start);
PopupButton button("Edit");
Menu menu;
int pasted = 0;
MenuItem& paste = menu.addItem("Paste", [&pasted]() { pasted++; });
paste.setEnabled(false);
button.setPopupContent(&menu);
root.addChild(button);
UiDriver driver(root);
driver.click(button);
driver.click(paste);
CHECK_EQUAL(0, pasted);
CHECK(menu.isOpen());
}
}
SUITE(TallMenu) {
TEST(MenuTallerThanTheWindowFitsAndScrollsToEveryItem) {
Widget root;
root.emplaceLayout<ColumnLayout>().setCrossAlign(CrossAlign::Start);
PopupButton button{ "Long" };
Menu menu;
int chosen = -1;
for (int index = 0; index < 40; index++)
menu.addItem("Item " + std::to_string(index), [&chosen, index]() { chosen = index; });
button.setPopupContent(&menu);
root.addChild(button);
UiDriver driver(root, { 400, 300 });
driver.click(button);
REQUIRE CHECK(menu.isOpen());
const RectF placed = menu.windowRect();
CHECK(placed.pos.y >= 0);
CHECK(placed.pos.y + placed.size.y <= 300.01f);
MenuItem& last = *findItem(menu, "Item 39");
driver.moveTo(placed.center());
for (int step = 0; step < 40 && last.windowRect().pos.y + last.size().y > placed.pos.y + placed.size.y; step++)
driver.wheel(-1);
driver.settle();
CHECK(last.windowRect().pos.y + last.size().y <= placed.pos.y + placed.size.y + 0.5f);
driver.click(last);
CHECK_EQUAL(39, chosen);
}
}
SUITE(MenuBarWidget) {
struct Scene {
Widget root;
MenuBar bar;
Menu file;
Menu edit;
Box body;
int newClicks = 0;
int undoClicks = 0;
Scene() {
root.emplaceLayout<ColumnLayout>();
file.addItem("New", [this]() { newClicks++; });
edit.addItem("Undo", [this]() { undoClicks++; });
bar.addMenu("File", file);
bar.addMenu("Edit", edit);
body.setSizing(AxisSizing::fill(), AxisSizing::fill());
root.addChild(bar);
root.addChild(body);
}
[[nodiscard]] PopupButton& title(size_t index) const { return *bar.titles()[index]; }
};
TEST(TitlesAreLaidOutInARow) {
Scene scene;
UiDriver driver(scene.root);
CHECK(scene.title(1).rect().pos.x >= scene.title(0).rect().pos.x + scene.title(0).size().x);
CHECK_CLOSE(scene.title(0).rect().pos.y, scene.title(1).rect().pos.y, 0.01f);
CHECK(scene.title(0).size().y <= scene.bar.size().y);
}
TEST(ClickingTitleOpensItsMenu) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.title(0));
CHECK(scene.file.isOpen());
const RectF title = scene.title(0).windowRect();
const halnf gap = scene.file.windowRect().pos.y - (title.pos.y + title.size.y);
CHECK_CLOSE(Theme::standard().metrics.popupGap, gap, 0.01f);
}
TEST(HoveringAnotherTitleWhileOpenSwitchesMenus) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.title(0));
driver.moveTo(scene.title(1));
CHECK(!scene.file.isOpen());
CHECK(scene.edit.isOpen());
}
TEST(ClickingAnotherTitleWhileOpenKeepsItsMenuOpen) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.title(0));
driver.click(scene.title(1));
CHECK(!scene.file.isOpen());
CHECK(scene.edit.isOpen());
}
TEST(ClickingTheOpenTitleAgainClosesItsMenu) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.title(0));
driver.click(scene.title(1));
driver.click(scene.title(1));
CHECK_EQUAL(0u, driver.ui().openPopupCount());
}
TEST(ClickingTitleAfterHoverSwitchAndMovingWithinItClosesMenu) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.title(0));
driver.moveTo(scene.title(1));
driver.click(driver.centerOf(scene.title(1)) + Vec2F(1.0f, 0.0f));
CHECK_EQUAL(0u, driver.ui().openPopupCount());
}
TEST(HoveringTitlesWhileClosedOpensNothing) {
Scene scene;
UiDriver driver(scene.root);
driver.moveTo(scene.title(0));
driver.moveTo(scene.title(1));
CHECK_EQUAL(0u, driver.ui().openPopupCount());
}
TEST(ItemActivationClosesMenuAndResetsTitle) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.title(1));
driver.click(*findItem(scene.edit, "Undo"));
CHECK_EQUAL(1, scene.undoClicks);
CHECK(!scene.title(1).isPopupOpen());
}
TEST(ClickingBodyClosesMenu) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.title(0));
driver.click(Vec2F(700.0f, 500.0f));
CHECK_EQUAL(0u, driver.ui().openPopupCount());
}
}
SUITE(DropdownWidget) {
struct Scene {
Widget root;
Dropdown dropdown{ { "Normal", "Multiply", "Screen" } };
std::vector<long> selections;
Scene() {
root.emplaceLayout<ColumnLayout>().setCrossAlign(CrossAlign::Start);
dropdown.setOnSelected([this](long index) { selections.push_back(index); });
root.addChild(dropdown);
}
};
TEST(ShowsFirstOptionByDefault) {
Scene scene;
UiDriver driver(scene.root);
CHECK_EQUAL(0, scene.dropdown.selectedIndex());
CHECK(driver.paint().hasText("Normal"));
}
TEST(WidthFitsWidestOption) {
Scene scene;
UiDriver driver(scene.root);
CHECK(scene.dropdown.size().x >= MonospaceTextMetrics().textWidth("Multiply", 14));
}
TEST(PickingOptionSelectsItAndCloses) {
Scene scene;
UiDriver driver(scene.root);
driver.click(scene.dropdown);
REQUIRE CHECK(scene.dropdown.isPopupOpen());
driver.click(*findItem(scene.dropdown.menu(), "Screen"));
CHECK_EQUAL(2, scene.dropdown.selectedIndex());
CHECK_EQUAL("Screen", scene.dropdown.selectedText());
CHECK(!scene.dropdown.isPopupOpen());
REQUIRE CHECK_EQUAL(1u, scene.selections.size());
CHECK_EQUAL(2, scene.selections[0]);
}
TEST(MenuIsAtLeastAsWideAsDropdown) {
Scene scene;
scene.dropdown.setSizing(AxisSizing::fixed(300), AxisSizing::fit());
UiDriver driver(scene.root);
driver.click(scene.dropdown);
CHECK(scene.dropdown.menu().size().x >= 300);
}
TEST(ChangingOptionsKeepsValidSelection) {
Scene scene;
UiDriver driver(scene.root);
scene.dropdown.setSelectedIndex(2);
scene.dropdown.setOptions({ "A" });
CHECK_EQUAL(0, scene.dropdown.selectedIndex());
CHECK_EQUAL(1u, scene.dropdown.menu().items().size());
}
TEST(SelectingCurrentOptionDoesNotNotify) {
Scene scene;
UiDriver driver(scene.root);
scene.dropdown.setSelectedIndex(0);
CHECK(scene.selections.empty());
}
}
SUITE(MenuRefresh) {
TEST(DynamicTextAndEnabledStateRefreshWhenMenuOpens) {
Widget root;
root.emplaceLayout<ColumnLayout>();
MenuBar bar;
Menu edit;
root.addChild(bar);
std::string undoName = "Paint";
bool canUndo = false;
MenuEntry entry = MenuEntry::item("Undo", {}, "Ctrl+Z");
entry.dynamicText = [&undoName]() { return "Undo: " + undoName; };
entry.isEnabled = [&canUndo]() { return canUndo; };
MenuItem& undo = edit.add(entry);
bar.addMenu("Edit", edit);
UiDriver driver(root);
CHECK_EQUAL("Undo: Paint", undo.text());
CHECK(!undo.isEnabled());
undoName = "Create layer";
canUndo = true;
driver.click(*bar.titles()[0]);
CHECK(edit.isOpen());
CHECK_EQUAL("Undo: Create layer", undo.text());
CHECK(undo.isEnabled());
CHECK(driver.paint().hasText("Undo: Create layer"));
}
TEST(OnOpenCanRebuildItems) {
Widget root;
root.emplaceLayout<ColumnLayout>();
MenuBar bar;
Menu history;
root.addChild(bar);
int states = 2;
history.setOnOpen([&states](Menu& menu) {
menu.clear();
for (int state = 0; state < states; state++)
menu.addItem("State " + std::to_string(state), {});
});
bar.addMenu("History", history);
UiDriver driver(root);
driver.click(*bar.titles()[0]);
CHECK_EQUAL(2u, history.items().size());
driver.key(InputID::ESCAPE);
states = 5;
driver.click(*bar.titles()[0]);
CHECK_EQUAL(5u, history.items().size());
}
}