283 lines
10 KiB
C++
283 lines
10 KiB
C++
#include "catch.hpp"
|
|
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
#include "DebrisSelectionChangedEvent.h"
|
|
#include "EntitySelectionChangedEvent.h"
|
|
#include "EventHandler.h"
|
|
#include "EventManager.h"
|
|
#include "SelectionChangedEvent.h"
|
|
#include "SelectionController.h"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Counts the change events the controller publishes, so the tests can assert not
|
|
// just the resulting selection but that the widgets were told about it — and, for
|
|
// the categories a change did not touch, that they were not told needlessly.
|
|
class SelectionEventSpy : public CombinedEventHandler<SelectionChangedEvent,
|
|
EntitySelectionChangedEvent,
|
|
DebrisSelectionChangedEvent>
|
|
{
|
|
public:
|
|
SelectionEventSpy() { registerForEvents(); }
|
|
~SelectionEventSpy() { unregisterForEvents(); }
|
|
|
|
int buildingEvents = 0;
|
|
int actorEvents = 0;
|
|
int debrisEvents = 0;
|
|
|
|
void reset() { buildingEvents = 0; actorEvents = 0; debrisEvents = 0; }
|
|
|
|
private:
|
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> /*event*/) override
|
|
{
|
|
++buildingEvents;
|
|
}
|
|
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> /*event*/) override
|
|
{
|
|
++actorEvents;
|
|
}
|
|
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> /*event*/) override
|
|
{
|
|
++debrisEvents;
|
|
}
|
|
};
|
|
|
|
static entt::entity makeEntity(int index)
|
|
{
|
|
return static_cast<entt::entity>(index);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Category precedence (REQ-UI-SELECTION-CATEGORIES)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("Selecting a building clears actors and debris", "[selection]")
|
|
{
|
|
SelectionController controller;
|
|
controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)},
|
|
SelectionMode::Replace);
|
|
|
|
controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);
|
|
|
|
REQUIRE(controller.getSelectedBuildings() == std::vector<BuildingId>{BuildingId(7)});
|
|
REQUIRE(controller.getSelectedActors().empty());
|
|
REQUIRE(controller.getSelectedDebris().empty());
|
|
}
|
|
|
|
TEST_CASE("Selecting a field object clears buildings", "[selection]")
|
|
{
|
|
SelectionController controller;
|
|
controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);
|
|
|
|
controller.selectFieldObjects({makeEntity(1)}, {}, SelectionMode::Replace);
|
|
|
|
REQUIRE(controller.getSelectedBuildings().empty());
|
|
REQUIRE(controller.getSelectedActors() == std::vector<entt::entity>{makeEntity(1)});
|
|
}
|
|
|
|
TEST_CASE("Actors and debris coexist", "[selection]")
|
|
{
|
|
// The one pair of categories that does not evict each other
|
|
// (REQ-UI-MULTI-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
|
|
SelectionController controller;
|
|
controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)},
|
|
SelectionMode::Replace);
|
|
|
|
REQUIRE(controller.getSelectedActors() == std::vector<entt::entity>{makeEntity(1)});
|
|
REQUIRE(controller.getSelectedDebris() == std::vector<entt::entity>{makeEntity(2)});
|
|
}
|
|
|
|
TEST_CASE("A plain click on an actor drops any selected debris", "[selection]")
|
|
{
|
|
// A point click passes only the category it hit, so Replace with an empty
|
|
// debris list is what "this actor and nothing else" means
|
|
// (REQ-UI-ENTITY-CLICK-SELECT).
|
|
SelectionController controller;
|
|
controller.selectFieldObjects({}, {makeEntity(2)}, SelectionMode::Replace);
|
|
|
|
controller.selectFieldObjects({makeEntity(1)}, {}, SelectionMode::Replace);
|
|
|
|
REQUIRE(controller.getSelectedActors() == std::vector<entt::entity>{makeEntity(1)});
|
|
REQUIRE(controller.getSelectedDebris().empty());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Replace / Toggle / Add
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("Replace makes the hit the whole selection", "[selection]")
|
|
{
|
|
SelectionController controller;
|
|
controller.selectBuildings({BuildingId(1), BuildingId(2)}, SelectionMode::Replace);
|
|
|
|
controller.selectBuildings({BuildingId(3)}, SelectionMode::Replace);
|
|
|
|
REQUIRE(controller.getSelectedBuildings() == std::vector<BuildingId>{BuildingId(3)});
|
|
}
|
|
|
|
TEST_CASE("Toggle adds a building that was not selected", "[selection]")
|
|
{
|
|
SelectionController controller;
|
|
controller.selectBuildings({BuildingId(1)}, SelectionMode::Replace);
|
|
|
|
controller.selectBuildings({BuildingId(2)}, SelectionMode::Toggle);
|
|
|
|
REQUIRE(controller.getSelectedBuildings()
|
|
== std::vector<BuildingId>{BuildingId(1), BuildingId(2)});
|
|
}
|
|
|
|
TEST_CASE("Toggle removes a building that was already selected", "[selection]")
|
|
{
|
|
// Ctrl+clicking a selected building deselects it, leaving the rest alone.
|
|
SelectionController controller;
|
|
controller.selectBuildings({BuildingId(1), BuildingId(2), BuildingId(3)},
|
|
SelectionMode::Replace);
|
|
|
|
controller.selectBuildings({BuildingId(2)}, SelectionMode::Toggle);
|
|
|
|
REQUIRE(controller.getSelectedBuildings()
|
|
== std::vector<BuildingId>{BuildingId(1), BuildingId(3)});
|
|
}
|
|
|
|
TEST_CASE("Add never deselects", "[selection]")
|
|
{
|
|
// This is where a Ctrl box drag differs from a Ctrl click: dragging over
|
|
// already-selected buildings must not toggle them off (REQ-UI-MULTI-SELECT).
|
|
SelectionController controller;
|
|
controller.selectBuildings({BuildingId(1), BuildingId(2)}, SelectionMode::Replace);
|
|
|
|
controller.selectBuildings({BuildingId(2), BuildingId(3)}, SelectionMode::Add);
|
|
|
|
REQUIRE(controller.getSelectedBuildings()
|
|
== std::vector<BuildingId>{BuildingId(1), BuildingId(2), BuildingId(3)});
|
|
}
|
|
|
|
TEST_CASE("An additive field selection leaves the untouched category alone",
|
|
"[selection]")
|
|
{
|
|
// Ctrl+clicking an actor passes an empty debris list, which must mean "do not
|
|
// touch debris" rather than "clear debris" (REQ-UI-DEBRIS-MULTI-SELECT).
|
|
SelectionController controller;
|
|
controller.selectFieldObjects({}, {makeEntity(5)}, SelectionMode::Replace);
|
|
|
|
controller.selectFieldObjects({makeEntity(1)}, {}, SelectionMode::Toggle);
|
|
|
|
REQUIRE(controller.getSelectedActors() == std::vector<entt::entity>{makeEntity(1)});
|
|
REQUIRE(controller.getSelectedDebris() == std::vector<entt::entity>{makeEntity(5)});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Membership queries used by the renderer
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("Membership queries answer per category", "[selection]")
|
|
{
|
|
SelectionController controller;
|
|
controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)},
|
|
SelectionMode::Replace);
|
|
|
|
REQUIRE(controller.isActorSelected(makeEntity(1)));
|
|
REQUIRE_FALSE(controller.isActorSelected(makeEntity(2)));
|
|
REQUIRE(controller.isDebrisSelected(makeEntity(2)));
|
|
REQUIRE_FALSE(controller.isDebrisSelected(makeEntity(1)));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Published events
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("Selecting a building announces the categories it cleared", "[selection]")
|
|
{
|
|
SelectionController controller;
|
|
controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)},
|
|
SelectionMode::Replace);
|
|
|
|
SelectionEventSpy spy;
|
|
controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);
|
|
|
|
REQUIRE(spy.buildingEvents == 1);
|
|
REQUIRE(spy.actorEvents == 1);
|
|
REQUIRE(spy.debrisEvents == 1);
|
|
}
|
|
|
|
TEST_CASE("Clearing an already-empty category announces nothing", "[selection]")
|
|
{
|
|
// The panels re-read on every event, so a redundant one is only noise — but the
|
|
// point-click and box paths used to disagree about this, so it is pinned.
|
|
SelectionController controller;
|
|
|
|
SelectionEventSpy spy;
|
|
controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);
|
|
|
|
REQUIRE(spy.buildingEvents == 1);
|
|
REQUIRE(spy.actorEvents == 0);
|
|
REQUIRE(spy.debrisEvents == 0);
|
|
}
|
|
|
|
TEST_CASE("Re-selecting the same building still announces it", "[selection]")
|
|
{
|
|
// Clicking an already-selected building refreshes its panel, so the event
|
|
// fires even though the selection did not change.
|
|
SelectionController controller;
|
|
controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);
|
|
|
|
SelectionEventSpy spy;
|
|
controller.selectBuildings({BuildingId(7)}, SelectionMode::Replace);
|
|
|
|
REQUIRE(spy.buildingEvents == 1);
|
|
}
|
|
|
|
TEST_CASE("Clearing an empty selection announces nothing at all", "[selection]")
|
|
{
|
|
SelectionController controller;
|
|
|
|
SelectionEventSpy spy;
|
|
controller.clearAll();
|
|
|
|
REQUIRE(spy.buildingEvents == 0);
|
|
REQUIRE(spy.actorEvents == 0);
|
|
REQUIRE(spy.debrisEvents == 0);
|
|
}
|
|
|
|
TEST_CASE("Clearing a populated selection announces every populated category",
|
|
"[selection]")
|
|
{
|
|
SelectionController controller;
|
|
controller.selectFieldObjects({makeEntity(1)}, {makeEntity(2)},
|
|
SelectionMode::Replace);
|
|
|
|
SelectionEventSpy spy;
|
|
controller.clearAll();
|
|
|
|
REQUIRE(spy.buildingEvents == 0); // buildings were already empty
|
|
REQUIRE(spy.actorEvents == 1);
|
|
REQUIRE(spy.debrisEvents == 1);
|
|
REQUIRE(controller.getSelectedActors().empty());
|
|
REQUIRE(controller.getSelectedDebris().empty());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Pruning despawned entities
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("Pruning announces only when something actually went", "[selection]")
|
|
{
|
|
// Runs every frame, so re-announcing an unchanged selection would spam the
|
|
// panels 60 times a second.
|
|
SelectionController controller;
|
|
controller.selectFieldObjects({makeEntity(1), makeEntity(2)}, {},
|
|
SelectionMode::Replace);
|
|
|
|
SelectionEventSpy spy;
|
|
controller.setSelectedActors({makeEntity(1), makeEntity(2)});
|
|
REQUIRE(spy.actorEvents == 0);
|
|
|
|
controller.setSelectedActors({makeEntity(1)});
|
|
REQUIRE(spy.actorEvents == 1);
|
|
REQUIRE(controller.getSelectedActors() == std::vector<entt::entity>{makeEntity(1)});
|
|
}
|