329 lines
13 KiB
C++
329 lines
13 KiB
C++
#include "catch.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <vector>
|
|
|
|
#include "BuildModeController.h"
|
|
#include "BuildingType.h"
|
|
#include "ControlAction.h"
|
|
|
|
// REQ-UI-CONTROLS-ACCURACY. The panel and the input handling read one table, and these
|
|
// tests are what makes that pay: the round-trip case below fails the moment a row is
|
|
// shown whose binding resolves elsewhere, which is the drift the whole design exists to
|
|
// prevent. Everything here works on action ids -- the display strings live in the ui
|
|
// target and are not what can silently go wrong.
|
|
|
|
namespace
|
|
{
|
|
|
|
ControlContext generalContext()
|
|
{
|
|
return ControlContext();
|
|
}
|
|
|
|
ControlContext selectionContext(bool placeable = true)
|
|
{
|
|
ControlContext context;
|
|
context.selection = ControlSelection::Buildings;
|
|
context.selectionCount = 3;
|
|
context.placeableBuildingSelected = placeable;
|
|
return context;
|
|
}
|
|
|
|
ControlContext builderContext(BuildingType type)
|
|
{
|
|
ControlContext context;
|
|
context.mode = BuildMode::Builder;
|
|
context.builderType = type;
|
|
return context;
|
|
}
|
|
|
|
ControlContext blueprintContext(bool transfer = false)
|
|
{
|
|
ControlContext context;
|
|
context.mode = BuildMode::Blueprint;
|
|
context.hoveredGhostIsTransfer = transfer;
|
|
return context;
|
|
}
|
|
|
|
ControlContext deconstructContext()
|
|
{
|
|
ControlContext context;
|
|
context.mode = BuildMode::Deconstruct;
|
|
return context;
|
|
}
|
|
|
|
// A spread wide enough that every availability rule and every binding condition is
|
|
// exercised by the whole-table properties below.
|
|
std::vector<ControlContext> allContexts()
|
|
{
|
|
ControlContext beltDragging = builderContext(BuildingType::Belt);
|
|
beltDragging.draggingBelt = true;
|
|
|
|
ControlContext generalWithBlueprint = generalContext();
|
|
generalWithBlueprint.temporaryBlueprintExists = true;
|
|
|
|
ControlContext fieldSelection = selectionContext(false);
|
|
fieldSelection.selection = ControlSelection::FieldObjects;
|
|
|
|
return {generalContext(),
|
|
generalWithBlueprint,
|
|
selectionContext(),
|
|
fieldSelection,
|
|
builderContext(BuildingType::Belt),
|
|
builderContext(BuildingType::Assembler),
|
|
beltDragging,
|
|
blueprintContext(false),
|
|
blueprintContext(true),
|
|
deconstructContext()};
|
|
}
|
|
|
|
std::vector<ControlAction> shownActions(const ControlContext& context)
|
|
{
|
|
std::vector<ControlAction> actions = getContextActions(context);
|
|
const std::vector<ControlAction> always = getAlwaysAvailableActions(context);
|
|
actions.insert(actions.end(), always.begin(), always.end());
|
|
return actions;
|
|
}
|
|
|
|
bool contains(const std::vector<ControlAction>& actions, ControlAction action)
|
|
{
|
|
return std::find(actions.begin(), actions.end(), action) != actions.end();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("ControlAction: every shown row is available", "[controls]")
|
|
{
|
|
for (const ControlContext& context : allContexts())
|
|
{
|
|
for (ControlAction action : shownActions(context))
|
|
{
|
|
REQUIRE(isControlActionAvailable(action, context));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("ControlAction: no row is shown twice", "[controls]")
|
|
{
|
|
for (const ControlContext& context : allContexts())
|
|
{
|
|
std::vector<ControlAction> actions = shownActions(context);
|
|
std::vector<ControlAction> unique = actions;
|
|
std::sort(unique.begin(), unique.end());
|
|
unique.erase(std::unique(unique.begin(), unique.end()), unique.end());
|
|
REQUIRE(unique.size() == actions.size());
|
|
}
|
|
}
|
|
|
|
// The core anti-drift property: a row's badges are rendered from its bindings, so every
|
|
// binding a row advertises must actually trigger that row's action in that same context.
|
|
TEST_CASE("ControlAction: every advertised binding resolves back to its own action",
|
|
"[controls]")
|
|
{
|
|
for (const ControlContext& context : allContexts())
|
|
{
|
|
for (ControlAction action : shownActions(context))
|
|
{
|
|
const std::vector<ControlBinding> bindings =
|
|
getControlActionBindings(action, context);
|
|
REQUIRE_FALSE(bindings.empty());
|
|
|
|
for (const ControlBinding& binding : bindings)
|
|
{
|
|
if (binding.isMouse)
|
|
{
|
|
REQUIRE(resolveMouseAction(binding.mouse, context) == action);
|
|
}
|
|
else
|
|
{
|
|
REQUIRE(resolveKeyAction(binding.key, binding.modifiers, context)
|
|
== action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The other direction: an input that resolves to something must resolve to an action
|
|
// that is actually available there, so no input can trigger a no-op. This is weaker
|
|
// than "must be a shown row" on purpose -- a context may omit an available binding
|
|
// (Ctrl+click in the General context), which REQ-UI-CONTROLS-ACCURACY permits; what it
|
|
// may never do is act on an unavailable one.
|
|
TEST_CASE("ControlAction: every resolvable input is available", "[controls]")
|
|
{
|
|
const std::vector<MouseBinding> mouseBindings = {
|
|
MouseBinding::LeftClick, MouseBinding::LeftDrag, MouseBinding::CtrlLeftClick,
|
|
MouseBinding::CtrlLeftDrag, MouseBinding::RightClick};
|
|
|
|
for (const ControlContext& context : allContexts())
|
|
{
|
|
for (MouseBinding binding : mouseBindings)
|
|
{
|
|
const ControlAction action = resolveMouseAction(binding, context);
|
|
if (action != ControlAction::None)
|
|
{
|
|
REQUIRE(isControlActionAvailable(action, context));
|
|
}
|
|
}
|
|
|
|
const std::vector<int> keys = {Qt::Key_A, Qt::Key_D, Qt::Key_W, Qt::Key_S,
|
|
Qt::Key_Space, Qt::Key_C, Qt::Key_V, Qt::Key_R,
|
|
Qt::Key_Q, Qt::Key_Escape};
|
|
for (int key : keys)
|
|
{
|
|
for (Qt::KeyboardModifiers modifiers :
|
|
{Qt::KeyboardModifiers(Qt::NoModifier),
|
|
Qt::KeyboardModifiers(Qt::ShiftModifier),
|
|
Qt::KeyboardModifiers(Qt::ControlModifier)})
|
|
{
|
|
const ControlAction action = resolveKeyAction(key, modifiers, context);
|
|
if (action != ControlAction::None)
|
|
{
|
|
REQUIRE(isControlActionAvailable(action, context));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("ControlAction: contexts are named by mode and selection", "[controls]")
|
|
{
|
|
REQUIRE(getControlContextKind(generalContext()) == ControlContextKind::General);
|
|
REQUIRE(getControlContextKind(selectionContext()) == ControlContextKind::Selection);
|
|
REQUIRE(getControlContextKind(builderContext(BuildingType::Belt))
|
|
== ControlContextKind::Build);
|
|
REQUIRE(getControlContextKind(blueprintContext()) == ControlContextKind::Blueprint);
|
|
REQUIRE(getControlContextKind(deconstructContext())
|
|
== ControlContextKind::Deconstruct);
|
|
}
|
|
|
|
TEST_CASE("ControlAction: Q enters deconstruct mode, or leaves the active one",
|
|
"[controls]")
|
|
{
|
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, generalContext())
|
|
== ControlAction::EnterDeconstruct);
|
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, selectionContext())
|
|
== ControlAction::EnterDeconstruct);
|
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, builderContext(BuildingType::Belt))
|
|
== ControlAction::ExitMode);
|
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, blueprintContext())
|
|
== ControlAction::ExitMode);
|
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, deconstructContext())
|
|
== ControlAction::ExitMode);
|
|
}
|
|
|
|
TEST_CASE("ControlAction: Ctrl distinguishes the chords, other modifiers do not",
|
|
"[controls]")
|
|
{
|
|
const ControlContext context = selectionContext();
|
|
|
|
REQUIRE(resolveKeyAction(Qt::Key_C, Qt::NoModifier, context)
|
|
== ControlAction::CopyTemporary);
|
|
REQUIRE(resolveKeyAction(Qt::Key_C, Qt::ControlModifier, context)
|
|
== ControlAction::CreateBlueprint);
|
|
REQUIRE(resolveKeyAction(Qt::Key_V, Qt::ControlModifier, context)
|
|
== ControlAction::OpenBlueprints);
|
|
|
|
// Shift+A must still pan, as it always has -- matching modifiers exactly instead of
|
|
// testing Ctrl alone would silently swallow the press.
|
|
REQUIRE(resolveKeyAction(Qt::Key_A, Qt::ShiftModifier, context) == ControlAction::Move);
|
|
REQUIRE(resolveKeyAction(Qt::Key_R, Qt::ShiftModifier, blueprintContext())
|
|
== ControlAction::Rotate);
|
|
}
|
|
|
|
TEST_CASE("ControlAction: a transfer target turns the click into Apply settings",
|
|
"[controls]")
|
|
{
|
|
const ControlContext plain = blueprintContext(false);
|
|
const ControlContext transfer = blueprintContext(true);
|
|
|
|
REQUIRE(resolveMouseAction(MouseBinding::LeftClick, plain) == ControlAction::Place);
|
|
REQUIRE(resolveMouseAction(MouseBinding::LeftClick, transfer)
|
|
== ControlAction::ApplySettings);
|
|
|
|
REQUIRE(contains(getContextActions(plain), ControlAction::Place));
|
|
REQUIRE_FALSE(contains(getContextActions(plain), ControlAction::ApplySettings));
|
|
REQUIRE(contains(getContextActions(transfer), ControlAction::ApplySettings));
|
|
REQUIRE_FALSE(contains(getContextActions(transfer), ControlAction::Place));
|
|
}
|
|
|
|
TEST_CASE("ControlAction: a belt drag takes the right mouse button from ExitMode",
|
|
"[controls]")
|
|
{
|
|
ControlContext dragging = builderContext(BuildingType::Belt);
|
|
dragging.draggingBelt = true;
|
|
const ControlContext idle = builderContext(BuildingType::Belt);
|
|
|
|
REQUIRE(resolveMouseAction(MouseBinding::RightClick, idle) == ControlAction::ExitMode);
|
|
REQUIRE(resolveMouseAction(MouseBinding::RightClick, dragging)
|
|
== ControlAction::CancelBeltLine);
|
|
|
|
// Q still leaves the mode outright while the drag runs, so ExitMode stays shown --
|
|
// with its right-click badge dropped, since the button now means something else.
|
|
REQUIRE(resolveKeyAction(Qt::Key_Q, Qt::NoModifier, dragging) == ControlAction::ExitMode);
|
|
|
|
const std::vector<ControlBinding> idleBindings =
|
|
getControlActionBindings(ControlAction::ExitMode, idle);
|
|
const std::vector<ControlBinding> dragBindings =
|
|
getControlActionBindings(ControlAction::ExitMode, dragging);
|
|
REQUIRE(idleBindings.size() == 2);
|
|
REQUIRE(dragBindings.size() == 1);
|
|
REQUIRE_FALSE(dragBindings.front().isMouse);
|
|
}
|
|
|
|
TEST_CASE("ControlAction: dragging a line is offered for belts only", "[controls]")
|
|
{
|
|
REQUIRE(resolveMouseAction(MouseBinding::LeftDrag, builderContext(BuildingType::Belt))
|
|
== ControlAction::PlaceBeltLine);
|
|
REQUIRE(resolveMouseAction(MouseBinding::LeftDrag,
|
|
builderContext(BuildingType::Assembler))
|
|
== ControlAction::None);
|
|
REQUIRE(resolveMouseAction(MouseBinding::LeftDrag, blueprintContext())
|
|
== ControlAction::None);
|
|
}
|
|
|
|
TEST_CASE("ControlAction: blueprint keys are offered only where they do something",
|
|
"[controls]")
|
|
{
|
|
// V does nothing until C has captured something (REQ-UI-BLUEPRINT-TEMP).
|
|
ControlContext withBlueprint = generalContext();
|
|
withBlueprint.temporaryBlueprintExists = true;
|
|
REQUIRE_FALSE(contains(shownActions(generalContext()), ControlAction::PasteTemporary));
|
|
REQUIRE(contains(shownActions(withBlueprint), ControlAction::PasteTemporary));
|
|
REQUIRE(resolveKeyAction(Qt::Key_V, Qt::NoModifier, generalContext())
|
|
== ControlAction::None);
|
|
|
|
// C and Ctrl+C need a selection holding something placeable (REQ-UI-HOTKEYS).
|
|
ControlContext fieldSelection = selectionContext(false);
|
|
fieldSelection.selection = ControlSelection::FieldObjects;
|
|
REQUIRE(contains(getContextActions(selectionContext()), ControlAction::CopyTemporary));
|
|
REQUIRE_FALSE(contains(getContextActions(fieldSelection), ControlAction::CopyTemporary));
|
|
REQUIRE(resolveKeyAction(Qt::Key_C, Qt::NoModifier, fieldSelection)
|
|
== ControlAction::None);
|
|
|
|
// Ctrl+V opens the dialog whatever is going on (REQ-UI-HOTKEYS).
|
|
for (const ControlContext& context : allContexts())
|
|
{
|
|
REQUIRE(resolveKeyAction(Qt::Key_V, Qt::ControlModifier, context)
|
|
== ControlAction::OpenBlueprints);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("ControlAction: selection rows appear only once something is selected",
|
|
"[controls]")
|
|
{
|
|
const std::vector<ControlAction> general = getContextActions(generalContext());
|
|
const std::vector<ControlAction> selection = getContextActions(selectionContext());
|
|
|
|
REQUIRE(contains(general, ControlAction::Select));
|
|
REQUIRE(contains(general, ControlAction::EnterDeconstruct));
|
|
REQUIRE_FALSE(contains(general, ControlAction::AddToSelection));
|
|
REQUIRE_FALSE(contains(general, ControlAction::CreateBlueprint));
|
|
|
|
REQUIRE(contains(selection, ControlAction::AddToSelection));
|
|
REQUIRE(contains(selection, ControlAction::AddAreaToSelection));
|
|
REQUIRE(contains(selection, ControlAction::CreateBlueprint));
|
|
REQUIRE(contains(selection, ControlAction::EnterDeconstruct));
|
|
}
|