feat(command-zone): add logic layer with tests

Implement CommandZoneLogic - the card data management layer for zones.
This layer handles card insertion, removal, and ordering independently
of Qt graphics rendering.

Key components:
- CommandZoneLogic: manages card data within a zone
- AddCardAlgorithm: generic insertion algorithm for ordered zones
- CardZoneLogic modifications: integrate with existing zone hierarchy

Design decisions:
- Separation of data logic from graphics enables unit testing
- addCardImpl uses a stable insertion algorithm for consistent ordering
- Mock card items enable testing without Qt graphics dependencies

Test coverage verifies card insertion behavior across edge cases.
This commit is contained in:
DawnFire42 2026-02-26 15:49:38 -05:00
parent 3c2e4a3fb2
commit 8422357878
10 changed files with 480 additions and 5 deletions

View file

@ -10,6 +10,27 @@ set(TEST_QT_MODULES
${COCKATRICE_QT_VERSION_NAME}::Widgets
)
# ------------------------
# Command Zone Logic Test
# Tests addCardImpl insertion behavior
# ------------------------
add_executable(command_zone_logic_test
mocks/mock_card_item.cpp
command_zone_logic_test.cpp
)
target_include_directories(command_zone_logic_test
PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src/game/zones/logic
)
target_link_libraries(command_zone_logic_test
PRIVATE Threads::Threads
PRIVATE ${GTEST_BOTH_LIBRARIES}
PRIVATE ${TEST_QT_MODULES}
)
add_test(NAME command_zone_logic_test COMMAND command_zone_logic_test)
# ------------------------
# Command Zone State Test
# Tests the extracted CommandZoneState pure state machine
@ -36,5 +57,6 @@ add_test(NAME command_zone_state_test COMMAND command_zone_state_test)
# Dependencies on gtest
# ------------------------
if(NOT GTEST_FOUND)
add_dependencies(command_zone_logic_test gtest)
add_dependencies(command_zone_state_test gtest)
endif()

View file

@ -0,0 +1,238 @@
/**
* @file command_zone_logic_test.cpp
* @brief Unit tests for CommandZoneLogic::addCardImpl
*
* Tests the card insertion logic for command zones:
* - Position handling (negative x, beyond range, valid x)
* - Card visibility and state reset
* - Unknown contents handling (sets card ID to -1)
*/
#include "add_card_algorithm.h"
#include "mocks/mock_card_item.h"
#include <QList>
#include <gtest/gtest.h>
/**
* @brief Standalone CardList implementation for testing.
*
* Mimics the behavior of the real CardList class without dependencies.
*/
class TestCardList : public QList<MockCardItem *>
{
bool contentsKnown;
public:
explicit TestCardList(bool _contentsKnown = true) : contentsKnown(_contentsKnown)
{
}
[[nodiscard]] bool getContentsKnown() const
{
return contentsKnown;
}
void setContentsKnown(bool known)
{
contentsKnown = known;
}
};
/**
* @brief Testable implementation of CommandZoneLogic::addCardImpl.
*
* Uses the shared CardZoneAlgorithms::addCardToList template to ensure
* test behavior matches production code exactly.
*/
class CommandZoneLogicTestHarness
{
public:
TestCardList cards;
explicit CommandZoneLogicTestHarness(bool contentsKnown = true) : cards(contentsKnown)
{
}
/**
* @brief Delegates to shared algorithm for testing.
*
* Uses CardZoneAlgorithms::addCardToList which is the same template
* used by CommandZoneLogic::addCardImpl in production code.
*/
void addCardImpl(MockCardItem *card, int x, int /*y*/)
{
CardZoneAlgorithms::addCardToList(cards, card, x);
}
};
class CommandZoneLogicTest : public ::testing::Test
{
protected:
CommandZoneLogicTestHarness *harness;
void SetUp() override
{
harness = new CommandZoneLogicTestHarness(true);
}
void TearDown() override
{
qDeleteAll(harness->cards);
harness->cards.clear();
delete harness;
}
MockCardItem *createCard(int id = 1)
{
return new MockCardItem(id);
}
void addExistingCards(int count)
{
for (int i = 0; i < count; ++i) {
harness->cards.append(createCard(100 + i));
}
}
};
// Test: Negative x appends card to end of list
TEST_F(CommandZoneLogicTest, AddCard_NegativeX_AppendsToEnd)
{
addExistingCards(2); // Cards at indices 0, 1
MockCardItem *newCard = createCard(42);
harness->addCardImpl(newCard, -1, 0);
ASSERT_EQ(3, harness->cards.size());
EXPECT_EQ(newCard, harness->cards.at(2)) << "Card should be at end (index 2)";
}
// Test: x beyond range appends card to end
TEST_F(CommandZoneLogicTest, AddCard_BeyondRange_AppendsToEnd)
{
addExistingCards(2); // Cards at indices 0, 1
MockCardItem *newCard = createCard(42);
harness->addCardImpl(newCard, 100, 0); // x=100 is way beyond size=2
ASSERT_EQ(3, harness->cards.size());
EXPECT_EQ(newCard, harness->cards.at(2)) << "Card should be at end (index 2)";
}
// Test: Valid x inserts card at that position
TEST_F(CommandZoneLogicTest, AddCard_ValidX_InsertsAtPosition)
{
addExistingCards(3); // Cards at indices 0, 1, 2
MockCardItem *cardAtIndex1 = harness->cards.at(1);
MockCardItem *newCard = createCard(42);
harness->addCardImpl(newCard, 1, 0);
ASSERT_EQ(4, harness->cards.size());
EXPECT_EQ(newCard, harness->cards.at(1)) << "New card should be at index 1";
EXPECT_EQ(cardAtIndex1, harness->cards.at(2)) << "Original card at index 1 should shift to index 2";
}
// Test: y parameter is ignored
TEST_F(CommandZoneLogicTest, AddCard_YIsIgnored)
{
MockCardItem *newCard = createCard(42);
harness->addCardImpl(newCard, 0, 999); // y=999 should be ignored
ASSERT_EQ(1, harness->cards.size());
EXPECT_EQ(newCard, harness->cards.at(0)) << "Card should be at index 0 regardless of y value";
}
// Test: Card is set to visible after adding
TEST_F(CommandZoneLogicTest, AddCard_SetsVisibleTrue)
{
MockCardItem *newCard = createCard(42);
ASSERT_FALSE(newCard->visible) << "Card should start invisible";
harness->addCardImpl(newCard, 0, 0);
EXPECT_TRUE(newCard->visible) << "Card should be visible after adding";
}
// Test: resetState is called with true
TEST_F(CommandZoneLogicTest, AddCard_CallsResetState)
{
MockCardItem *newCard = createCard(42);
ASSERT_FALSE(newCard->resetStateCalled) << "resetState should not be called initially";
harness->addCardImpl(newCard, 0, 0);
EXPECT_TRUE(newCard->resetStateCalled) << "resetState should be called";
EXPECT_TRUE(newCard->resetStateShowFaceDown) << "resetState should be called with true";
}
// Test: When contents unknown, card ID is set to -1
TEST_F(CommandZoneLogicTest, AddCard_UnknownContents_SetsIdNegative)
{
delete harness;
harness = new CommandZoneLogicTestHarness(false); // contentsKnown = false
MockCardItem *newCard = createCard(42);
ASSERT_EQ(42, newCard->getId()) << "Card should start with original ID";
harness->addCardImpl(newCard, 0, 0);
EXPECT_EQ(-1, newCard->getId()) << "Card ID should be -1 when contents unknown";
}
// Test: When contents known, card ID is preserved
TEST_F(CommandZoneLogicTest, AddCard_KnownContents_PreservesId)
{
MockCardItem *newCard = createCard(42);
ASSERT_EQ(42, newCard->getId()) << "Card should start with original ID";
harness->addCardImpl(newCard, 0, 0);
EXPECT_EQ(42, newCard->getId()) << "Card ID should be preserved when contents known";
}
// Test: Insert at position 0 (beginning)
TEST_F(CommandZoneLogicTest, AddCard_AtZero_InsertsAtBeginning)
{
addExistingCards(2);
MockCardItem *originalFirst = harness->cards.at(0);
MockCardItem *newCard = createCard(42);
harness->addCardImpl(newCard, 0, 0);
ASSERT_EQ(3, harness->cards.size());
EXPECT_EQ(newCard, harness->cards.at(0)) << "New card should be at beginning";
EXPECT_EQ(originalFirst, harness->cards.at(1)) << "Original first card should shift to index 1";
}
// Test: Insert into empty list
TEST_F(CommandZoneLogicTest, AddCard_EmptyList_AppendsCard)
{
ASSERT_EQ(0, harness->cards.size()) << "List should start empty";
MockCardItem *newCard = createCard(42);
harness->addCardImpl(newCard, 0, 0);
ASSERT_EQ(1, harness->cards.size());
EXPECT_EQ(newCard, harness->cards.at(0));
}
// Test: x equal to size appends (boundary condition)
TEST_F(CommandZoneLogicTest, AddCard_XEqualToSize_AppendsToEnd)
{
addExistingCards(2); // size = 2
MockCardItem *newCard = createCard(42);
harness->addCardImpl(newCard, 2, 0); // x == size, should append
ASSERT_EQ(3, harness->cards.size());
EXPECT_EQ(newCard, harness->cards.at(2)) << "Card should be at end";
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -0,0 +1,4 @@
#include "mock_card_item.h"
// MOC requires at least one non-inline method in a translation unit
// when using Q_OBJECT macro. This file provides that.

View file

@ -0,0 +1,72 @@
/**
* @file mock_card_item.h
* @brief Lightweight CardItem stub for command zone unit tests.
*
* This mock provides minimal CardItem functionality needed to test
* CommandZoneLogic::addCardImpl without pulling in graphics dependencies.
*/
#ifndef MOCK_CARD_ITEM_H
#define MOCK_CARD_ITEM_H
#include <QObject>
#include <QString>
/**
* @brief Stub CardRef for tests (avoids network/protobuf dependencies)
*/
struct CardRef
{
QString name;
QString setCode;
};
/**
* @brief Lightweight CardItem mock for testing zone logic.
*
* Tracks method calls and state changes for verification in tests.
*/
class MockCardItem : public QObject
{
Q_OBJECT
public:
explicit MockCardItem(int _id = 1, QObject *parent = nullptr) : QObject(parent), id(_id)
{
}
// State tracking
int id;
bool visible = false;
bool resetStateCalled = false;
bool resetStateShowFaceDown = false;
CardRef cardRef;
// CardItem interface methods used by CommandZoneLogic::addCardImpl
[[nodiscard]] int getId() const
{
return id;
}
void setId(int _id)
{
id = _id;
}
void setVisible(bool v)
{
visible = v;
}
[[nodiscard]] bool isVisible() const
{
return visible;
}
void resetState(bool showFaceDown)
{
resetStateCalled = true;
resetStateShowFaceDown = showFaceDown;
}
void setCardRef(const CardRef &ref)
{
cardRef = ref;
}
};
#endif // MOCK_CARD_ITEM_H