From 7a0a3ff9ac747b6187927b8df615b662bf7c354c Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Thu, 26 Feb 2026 15:50:30 -0500 Subject: [PATCH] feat(command-zone): add commander tax counter system with tests Implement CommanderTaxCounter - tracks casting cost increases for commanders that have been cast multiple times. Key components: - CommanderTaxCounter: specialized counter with value clamping - AbstractCounter modifications: support for counter ID system - CounterGeneral/TranslateCounterName: UI integration for display Design decisions: - Counter values clamped to valid range (0 to max) - Builds on counter_ids.h constants from commit 1 - Uses z_values.h from commit 4 for proper visual layering - Counter visibility tied to zone state (hidden when zone hidden) Test coverage: - commander_tax_counter_test: verifies setValue clamping behavior - counter_visibility_test: verifies visibility state transitions Note: Tests include for INT_MIN usage (required on some compilers like GCC on Debian 11 where INT_MIN is not transitively included). --- cockatrice/CMakeLists.txt | 1 + .../src/game/board/abstract_counter.cpp | 4 +- cockatrice/src/game/board/abstract_counter.h | 7 +- .../src/game/board/commander_tax_counter.cpp | 74 ++++ .../src/game/board/commander_tax_counter.h | 59 ++++ cockatrice/src/game/board/counter_general.cpp | 5 + .../src/game/board/translate_counter_name.cpp | 6 +- tests/command_zone/CMakeLists.txt | 34 ++ .../commander_tax_counter_test.cpp | 190 +++++++++++ .../command_zone/counter_visibility_test.cpp | 322 ++++++++++++++++++ 10 files changed, 694 insertions(+), 8 deletions(-) create mode 100644 cockatrice/src/game/board/commander_tax_counter.cpp create mode 100644 cockatrice/src/game/board/commander_tax_counter.h create mode 100644 tests/command_zone/commander_tax_counter_test.cpp create mode 100644 tests/command_zone/counter_visibility_test.cpp diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index ca0be91a2..f40c8561d 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -57,6 +57,7 @@ set(cockatrice_SOURCES src/game/board/abstract_card_drag_item.cpp src/game/board/abstract_card_item.cpp src/game/board/abstract_counter.cpp + src/game/board/commander_tax_counter.cpp src/game/board/arrow_item.cpp src/game/board/arrow_target.cpp src/game/board/card_drag_item.cpp diff --git a/cockatrice/src/game/board/abstract_counter.cpp b/cockatrice/src/game/board/abstract_counter.cpp index 08d19ec8a..d7bc3235b 100644 --- a/cockatrice/src/game/board/abstract_counter.cpp +++ b/cockatrice/src/game/board/abstract_counter.cpp @@ -23,12 +23,10 @@ AbstractCounter::AbstractCounter(Player *_player, int _value, bool _useNameForShortcut, QGraphicsItem *parent) - : QGraphicsItem(parent), player(_player), id(_id), name(_name), value(_value), + : QGraphicsObject(parent), player(_player), id(_id), name(_name), value(_value), useNameForShortcut(_useNameForShortcut), hovered(false), aDec(nullptr), aInc(nullptr), dialogSemaphore(false), deleteAfterDialog(false), shownInCounterArea(_shownInCounterArea) { - setAcceptHoverEvents(true); - shortcutActive = false; if (player->getPlayerInfo()->getLocalOrJudge()) { diff --git a/cockatrice/src/game/board/abstract_counter.h b/cockatrice/src/game/board/abstract_counter.h index ea13cb00f..2c1f35e32 100644 --- a/cockatrice/src/game/board/abstract_counter.h +++ b/cockatrice/src/game/board/abstract_counter.h @@ -9,7 +9,7 @@ #include "../../interface/widgets/menus/tearoff_menu.h" -#include +#include #include class Player; @@ -18,10 +18,9 @@ class QKeyEvent; class QMenu; class QString; -class AbstractCounter : public QObject, public QGraphicsItem +class AbstractCounter : public QGraphicsObject { Q_OBJECT - Q_INTERFACES(QGraphicsItem) protected: Player *player; @@ -57,7 +56,7 @@ public: ~AbstractCounter() override; void retranslateUi(); - void setValue(int _value); + virtual void setValue(int _value); void setShortcutsActive(); void setShortcutsInactive(); void delCounter(); diff --git a/cockatrice/src/game/board/commander_tax_counter.cpp b/cockatrice/src/game/board/commander_tax_counter.cpp new file mode 100644 index 000000000..38e402f9b --- /dev/null +++ b/cockatrice/src/game/board/commander_tax_counter.cpp @@ -0,0 +1,74 @@ +#include "commander_tax_counter.h" + +#include "../../interface/theme_manager.h" +#include "../player/player.h" +#include "../player/player_actions.h" +#include "../z_values.h" + +#include +#include +#include + +static constexpr qreal CORNER_RADIUS = 4.0; +static constexpr qreal FONT_SIZE_RATIO = 0.6; + +CommanderTaxCounter::CommanderTaxCounter(Player *_player, + int _id, + const QString &_name, + int _size, + int _value, + QGraphicsItem *parent) + : AbstractCounter(_player, _id, _name, false, _value, false, parent), size(_size) +{ + // Graphics configuration must be in concrete class, not AbstractCounter. + // Calling setCursor/setCacheMode in abstract base triggers pure virtual calls. + // See PileZone constructor for the same pattern. + setCacheMode(DeviceCoordinateCache); + setAcceptHoverEvents(true); + setCursor(Qt::ArrowCursor); + + // Update appearance when theme changes + connect(themeManager, &ThemeManager::themeChanged, this, [this]() { update(); }); + + setToolTip(tr("Commander tax: %1").arg(_value)); +} + +QRectF CommanderTaxCounter::boundingRect() const +{ + return QRectF(0, 0, size, size); +} + +void CommanderTaxCounter::paint(QPainter *painter, + [[maybe_unused]] const QStyleOptionGraphicsItem *option, + [[maybe_unused]] QWidget *widget) +{ + painter->save(); + + // Draw square background with rounded corners - matches PlayerCounter style + QRectF rect = boundingRect().adjusted(1, 1, -1, -1); + + // Use grey background color for tax counters + QColor bgColor = hovered ? GameColors::OVERLAY_BG_HOVERED : GameColors::OVERLAY_BG_NORMAL; + + painter->setPen(Qt::NoPen); + painter->setBrush(bgColor); + painter->drawRoundedRect(rect, CORNER_RADIUS, CORNER_RADIUS); + + // Draw the value text using system font for cross-platform consistency + QFont f = QFontDatabase::systemFont(QFontDatabase::GeneralFont); + f.setPixelSize(static_cast(size * FONT_SIZE_RATIO)); + f.setWeight(QFont::Bold); + painter->setFont(f); + painter->setPen(Qt::white); + painter->drawText(rect, Qt::AlignCenter, QString::number(value)); + + painter->restore(); +} + +void CommanderTaxCounter::setValue(int _value) +{ + // Clamp to 0 - commander tax cannot be negative + int clampedValue = qMax(0, _value); + AbstractCounter::setValue(clampedValue); + setToolTip(tr("Commander tax: %1").arg(clampedValue)); +} diff --git a/cockatrice/src/game/board/commander_tax_counter.h b/cockatrice/src/game/board/commander_tax_counter.h new file mode 100644 index 000000000..b3bc3c794 --- /dev/null +++ b/cockatrice/src/game/board/commander_tax_counter.h @@ -0,0 +1,59 @@ +/** + * @file commander_tax_counter.h + * @ingroup GameGraphicsPlayers + * @brief Square counter for commander tax, clamped to non-negative values. + */ + +#ifndef COCKATRICE_COMMANDER_TAX_COUNTER_H +#define COCKATRICE_COMMANDER_TAX_COUNTER_H + +#include "abstract_counter.h" + +/** + * @class CommanderTaxCounter + * @brief Counter for tracking commander tax in Commander format. + * + * Displays cumulative cost increase for casting a commander. The counter + * is manually adjusted by the player to track their commander tax. Values + * are clamped to >= 0. + * + * Appearance: square with rounded corners, semi-transparent background, + * positioned at top-left of command zone. + * + * Two instances per player: CounterIds::CommanderTax and CounterIds::PartnerTax. + * + * @see AbstractCounter + * @see CounterIds + */ +class CommanderTaxCounter : public AbstractCounter +{ + Q_OBJECT +private: + int size; + +public: + /** + * @brief Constructs a CommanderTaxCounter. + * @param _player The player who owns this counter + * @param _id Counter ID (CounterIds::CommanderTax or CounterIds::PartnerTax) + * @param _name Display name for the counter + * @param _size Size in pixels (both width and height, as the counter is square) + * @param _value Initial value for the counter + * @param parent Parent graphics item (typically the command zone) + */ + CommanderTaxCounter(Player *_player, + int _id, + const QString &_name, + int _size, + int _value, + QGraphicsItem *parent = nullptr); + + [[nodiscard]] QRectF boundingRect() const override; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + + /// @brief Sets counter value, clamping to >= 0. + /// @param _value New value (clamped if negative) + void setValue(int _value) override; +}; + +#endif // COCKATRICE_COMMANDER_TAX_COUNTER_H diff --git a/cockatrice/src/game/board/counter_general.cpp b/cockatrice/src/game/board/counter_general.cpp index d68486a1b..db2c07fe8 100644 --- a/cockatrice/src/game/board/counter_general.cpp +++ b/cockatrice/src/game/board/counter_general.cpp @@ -15,7 +15,12 @@ GeneralCounter::GeneralCounter(Player *_player, QGraphicsItem *parent) : AbstractCounter(_player, _id, _name, true, _value, useNameForShortcut, parent), color(_color), radius(_radius) { + // Graphics configuration must be in concrete class, not AbstractCounter. + // Calling setCursor/setCacheMode in abstract base triggers pure virtual calls. + // See PileZone constructor for the same pattern. setCacheMode(DeviceCoordinateCache); + setAcceptHoverEvents(true); + setCursor(Qt::ArrowCursor); } QRectF GeneralCounter::boundingRect() const diff --git a/cockatrice/src/game/board/translate_counter_name.cpp b/cockatrice/src/game/board/translate_counter_name.cpp index 4f1c97657..70e0d73ea 100644 --- a/cockatrice/src/game/board/translate_counter_name.cpp +++ b/cockatrice/src/game/board/translate_counter_name.cpp @@ -1,5 +1,7 @@ #include "translate_counter_name.h" +#include + const QMap TranslateCounterName::translated = { {"life", QT_TRANSLATE_NOOP("TranslateCounterName", "Life")}, {"w", QT_TRANSLATE_NOOP("TranslateCounterName", "White")}, @@ -8,4 +10,6 @@ const QMap TranslateCounterName::translated = { {"r", QT_TRANSLATE_NOOP("TranslateCounterName", "Red")}, {"g", QT_TRANSLATE_NOOP("TranslateCounterName", "Green")}, {"x", QT_TRANSLATE_NOOP("TranslateCounterName", "Colorless")}, - {"storm", QT_TRANSLATE_NOOP("TranslateCounterName", "Other")}}; + {"storm", QT_TRANSLATE_NOOP("TranslateCounterName", "Other")}, + {CounterNames::CommanderTax, QT_TRANSLATE_NOOP("TranslateCounterName", "commander tax")}, + {CounterNames::PartnerTax, QT_TRANSLATE_NOOP("TranslateCounterName", "partner tax")}}; diff --git a/tests/command_zone/CMakeLists.txt b/tests/command_zone/CMakeLists.txt index c8f9c408a..783949b7e 100644 --- a/tests/command_zone/CMakeLists.txt +++ b/tests/command_zone/CMakeLists.txt @@ -31,6 +31,22 @@ target_link_libraries(command_zone_logic_test add_test(NAME command_zone_logic_test COMMAND command_zone_logic_test) +# ------------------------ +# Commander Tax Counter Test +# Tests setValue clamping behavior +# ------------------------ +add_executable(commander_tax_counter_test + commander_tax_counter_test.cpp +) + +target_link_libraries(commander_tax_counter_test + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} +) + +add_test(NAME commander_tax_counter_test COMMAND commander_tax_counter_test) + # ------------------------ # Command Zone State Test # Tests the extracted CommandZoneState pure state machine @@ -53,10 +69,28 @@ target_link_libraries(command_zone_state_test add_test(NAME command_zone_state_test COMMAND command_zone_state_test) +# ------------------------ +# Counter Visibility Test +# Tests counter visibility during zone state transitions +# ------------------------ +add_executable(counter_visibility_test + counter_visibility_test.cpp +) + +target_link_libraries(counter_visibility_test + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} +) + +add_test(NAME counter_visibility_test COMMAND counter_visibility_test) + # ------------------------ # Dependencies on gtest # ------------------------ if(NOT GTEST_FOUND) add_dependencies(command_zone_logic_test gtest) + add_dependencies(commander_tax_counter_test gtest) add_dependencies(command_zone_state_test gtest) + add_dependencies(counter_visibility_test gtest) endif() diff --git a/tests/command_zone/commander_tax_counter_test.cpp b/tests/command_zone/commander_tax_counter_test.cpp new file mode 100644 index 000000000..6408ded57 --- /dev/null +++ b/tests/command_zone/commander_tax_counter_test.cpp @@ -0,0 +1,190 @@ +/** + * @file commander_tax_counter_test.cpp + * @brief Unit tests for CommanderTaxCounter::setValue + * + * Tests the value clamping behavior for commander tax: + * - Positive values pass through unchanged + * - Zero values pass through unchanged + * - Negative values are clamped to 0 + */ + +#include +#include +#include +#include + +/** + * @brief Testable implementation of CommanderTaxCounter value handling. + * + * Extracts the setValue and boundingRect logic for unit testing without + * requiring Player, ThemeManager, and QGraphicsScene dependencies. + */ +class CommanderTaxCounterTestHarness +{ + int value; + int size; + +public: + explicit CommanderTaxCounterTestHarness(int _size = 24, int initialValue = 0) : value(initialValue), size(_size) + { + } + + /** + * @brief Implementation of CommanderTaxCounter::setValue logic. + * + * Clamps negative values to 0, matching the real implementation: + * AbstractCounter::setValue(qMax(0, _value)); + */ + void setValue(int _value) + { + // Clamp to 0 - commander tax cannot be negative + value = std::max(0, _value); + } + + [[nodiscard]] int getValue() const + { + return value; + } + + /** + * @brief Implementation of CommanderTaxCounter::boundingRect logic. + * + * Returns a square QRectF based on the counter size. + */ + [[nodiscard]] QRectF boundingRect() const + { + return QRectF(0, 0, size, size); + } + + [[nodiscard]] int getSize() const + { + return size; + } +}; + +class CommanderTaxCounterTest : public ::testing::Test +{ +protected: + CommanderTaxCounterTestHarness *counter; + + void SetUp() override + { + counter = new CommanderTaxCounterTestHarness(24, 0); + } + + void TearDown() override + { + delete counter; + } +}; + +// Test: Positive value is stored correctly +TEST_F(CommanderTaxCounterTest, SetValue_Positive) +{ + counter->setValue(5); + + EXPECT_EQ(5, counter->getValue()) << "Positive value should be stored unchanged"; +} + +// Test: Zero value is stored correctly +TEST_F(CommanderTaxCounterTest, SetValue_Zero) +{ + counter->setValue(10); // First set to non-zero + counter->setValue(0); + + EXPECT_EQ(0, counter->getValue()) << "Zero value should be stored unchanged"; +} + +// Test: Negative value is clamped to zero +TEST_F(CommanderTaxCounterTest, SetValue_Negative_ClampedToZero) +{ + counter->setValue(-5); + + EXPECT_EQ(0, counter->getValue()) << "Negative value should be clamped to 0"; +} + +// Test: Large negative value is clamped to zero +TEST_F(CommanderTaxCounterTest, SetValue_LargeNegative_ClampedToZero) +{ + counter->setValue(-1000); + + EXPECT_EQ(0, counter->getValue()) << "Large negative value should be clamped to 0"; +} + +// Test: Boundary condition - negative one +TEST_F(CommanderTaxCounterTest, SetValue_NegativeOne_ClampedToZero) +{ + counter->setValue(-1); + + EXPECT_EQ(0, counter->getValue()) << "-1 should be clamped to 0"; +} + +// Test: Large positive value is stored correctly +TEST_F(CommanderTaxCounterTest, SetValue_LargePositive) +{ + counter->setValue(100); + + EXPECT_EQ(100, counter->getValue()) << "Large positive value should be stored unchanged"; +} + +// Test: Multiple setValue calls work correctly +TEST_F(CommanderTaxCounterTest, SetValue_MultipleCalls) +{ + counter->setValue(5); + EXPECT_EQ(5, counter->getValue()); + + counter->setValue(10); + EXPECT_EQ(10, counter->getValue()); + + counter->setValue(-3); + EXPECT_EQ(0, counter->getValue()); + + counter->setValue(7); + EXPECT_EQ(7, counter->getValue()); +} + +// Test: BoundingRect returns correct size +TEST_F(CommanderTaxCounterTest, BoundingRect_CorrectSize) +{ + QRectF rect = counter->boundingRect(); + + EXPECT_EQ(0, rect.x()) << "BoundingRect x should be 0"; + EXPECT_EQ(0, rect.y()) << "BoundingRect y should be 0"; + EXPECT_EQ(24, rect.width()) << "BoundingRect width should equal size"; + EXPECT_EQ(24, rect.height()) << "BoundingRect height should equal size"; +} + +// Test: BoundingRect with different size +TEST_F(CommanderTaxCounterTest, BoundingRect_DifferentSize) +{ + delete counter; + counter = new CommanderTaxCounterTestHarness(36, 0); + + QRectF rect = counter->boundingRect(); + + EXPECT_EQ(36, rect.width()) << "BoundingRect width should match constructor size"; + EXPECT_EQ(36, rect.height()) << "BoundingRect height should match constructor size"; +} + +// Test: Initial value from constructor +TEST_F(CommanderTaxCounterTest, Constructor_InitialValue) +{ + delete counter; + counter = new CommanderTaxCounterTestHarness(24, 5); + + EXPECT_EQ(5, counter->getValue()) << "Initial value from constructor should be set"; +} + +// Test: INT_MIN is clamped to zero +TEST_F(CommanderTaxCounterTest, SetValue_IntMin_ClampedToZero) +{ + counter->setValue(INT_MIN); + + EXPECT_EQ(0, counter->getValue()) << "INT_MIN should be clamped to 0"; +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/command_zone/counter_visibility_test.cpp b/tests/command_zone/counter_visibility_test.cpp new file mode 100644 index 000000000..e086f10a4 --- /dev/null +++ b/tests/command_zone/counter_visibility_test.cpp @@ -0,0 +1,322 @@ +/** + * @file counter_visibility_test.cpp + * @brief Unit tests for counter visibility during zone state transitions + * + * Tests the visibility logic for commander tax counters: + * - Commander tax visible when command zone visible + * - Partner tax visible when command zones visible AND partner expanded + * - Counters remain visible during minimize (only height changes) + * - Signal-driven visibility updates + */ + +#include +#include + +/** + * @brief Testable implementation of counter visibility logic. + * + * Extracts the visibility determination logic from + * PlayerGraphicsItem::rearrangeCounters for isolated testing. + * + * @warning SYNC REQUIRED: This harness duplicates visibility logic from + * player_graphics_item.cpp lines 172-195. Update if implementation changes. + */ +class CounterVisibilityTestHarness : public QObject +{ + Q_OBJECT + + bool commandZonesVisible_; + bool partnerZoneExpanded_; + + bool commanderTaxVisible_; + bool partnerTaxVisible_; + +public: + explicit CounterVisibilityTestHarness(QObject *parent = nullptr) + : QObject(parent), commandZonesVisible_(true), partnerZoneExpanded_(false), commanderTaxVisible_(false), + partnerTaxVisible_(false) + { + } + + /** + * @brief Sets the command zones visibility state. + * + * This simulates the server enabling/disabling command zones + * based on game format (Commander vs other formats). + */ + void setCommandZonesVisible(bool visible) + { + commandZonesVisible_ = visible; + updateCounterVisibility(); + } + + /** + * @brief Sets the partner zone expanded state. + * + * Simulates user toggling the partner zone via button or menu. + */ + void setPartnerZoneExpanded(bool expanded) + { + partnerZoneExpanded_ = expanded; + updateCounterVisibility(); + } + + [[nodiscard]] bool isCommanderTaxVisible() const + { + return commanderTaxVisible_; + } + + [[nodiscard]] bool isPartnerTaxVisible() const + { + return partnerTaxVisible_; + } + + [[nodiscard]] bool areCommandZonesVisible() const + { + return commandZonesVisible_; + } + + [[nodiscard]] bool isPartnerZoneExpanded() const + { + return partnerZoneExpanded_; + } + +signals: + void commanderTaxVisibilityChanged(bool visible); + void partnerTaxVisibilityChanged(bool visible); + +private: + /** + * @brief Implementation of counter visibility logic. + * + * Commander tax: visible when command zones visible + * Partner tax: visible when command zones visible AND partner expanded + */ + void updateCounterVisibility() + { + bool newCommanderVisible = commandZonesVisible_; + bool newPartnerVisible = commandZonesVisible_ && partnerZoneExpanded_; + + if (newCommanderVisible != commanderTaxVisible_) { + commanderTaxVisible_ = newCommanderVisible; + emit commanderTaxVisibilityChanged(commanderTaxVisible_); + } + + if (newPartnerVisible != partnerTaxVisible_) { + partnerTaxVisible_ = newPartnerVisible; + emit partnerTaxVisibilityChanged(partnerTaxVisible_); + } + } +}; + +class CounterVisibilityTest : public ::testing::Test +{ +protected: + CounterVisibilityTestHarness *harness; + + void SetUp() override + { + harness = new CounterVisibilityTestHarness(); + } + + void TearDown() override + { + delete harness; + } +}; + +// ============ Commander Tax Visibility Tests ============ + +// Test: Commander tax visible when zones visible +TEST_F(CounterVisibilityTest, CommanderTax_VisibleWhenZonesVisible) +{ + harness->setCommandZonesVisible(true); + + EXPECT_TRUE(harness->isCommanderTaxVisible()) << "Commander tax should be visible when zones visible"; +} + +// Test: Commander tax hidden when zones not visible +TEST_F(CounterVisibilityTest, CommanderTax_HiddenWhenZonesNotVisible) +{ + harness->setCommandZonesVisible(false); + + EXPECT_FALSE(harness->isCommanderTaxVisible()) << "Commander tax should be hidden when zones not visible"; +} + +// Test: Commander tax visibility independent of partner zone state +TEST_F(CounterVisibilityTest, CommanderTax_IndependentOfPartnerState) +{ + harness->setCommandZonesVisible(true); + + harness->setPartnerZoneExpanded(false); + EXPECT_TRUE(harness->isCommanderTaxVisible()) << "Commander tax visible with partner collapsed"; + + harness->setPartnerZoneExpanded(true); + EXPECT_TRUE(harness->isCommanderTaxVisible()) << "Commander tax visible with partner expanded"; +} + +// ============ Partner Tax Visibility Tests ============ + +// Test: Partner tax visible when zones visible AND partner expanded +TEST_F(CounterVisibilityTest, PartnerTax_VisibleWhenExpandedAndZonesVisible) +{ + harness->setCommandZonesVisible(true); + harness->setPartnerZoneExpanded(true); + + EXPECT_TRUE(harness->isPartnerTaxVisible()) << "Partner tax should be visible when expanded and zones visible"; +} + +// Test: Partner tax hidden when partner collapsed +TEST_F(CounterVisibilityTest, PartnerTax_HiddenWhenCollapsed) +{ + harness->setCommandZonesVisible(true); + harness->setPartnerZoneExpanded(false); + + EXPECT_FALSE(harness->isPartnerTaxVisible()) << "Partner tax should be hidden when partner collapsed"; +} + +// Test: Partner tax hidden when zones not visible (even if expanded) +TEST_F(CounterVisibilityTest, PartnerTax_HiddenWhenZonesNotVisible) +{ + harness->setCommandZonesVisible(false); + harness->setPartnerZoneExpanded(true); + + EXPECT_FALSE(harness->isPartnerTaxVisible()) + << "Partner tax should be hidden when zones not visible, even if expanded"; +} + +// ============ Visibility Transition Tests ============ + +// Test: Correct sequence during expand +TEST_F(CounterVisibilityTest, ExpandTransition_PartnerBecomesVisible) +{ + harness->setCommandZonesVisible(true); + harness->setPartnerZoneExpanded(false); + ASSERT_FALSE(harness->isPartnerTaxVisible()); + + harness->setPartnerZoneExpanded(true); + + EXPECT_TRUE(harness->isPartnerTaxVisible()) << "Partner tax should become visible on expand"; +} + +// Test: Correct sequence during collapse +TEST_F(CounterVisibilityTest, CollapseTransition_PartnerBecomesHidden) +{ + harness->setCommandZonesVisible(true); + harness->setPartnerZoneExpanded(true); + ASSERT_TRUE(harness->isPartnerTaxVisible()); + + harness->setPartnerZoneExpanded(false); + + EXPECT_FALSE(harness->isPartnerTaxVisible()) << "Partner tax should become hidden on collapse"; +} + +// Test: Hiding zones hides both counters +TEST_F(CounterVisibilityTest, HideZones_HidesBothCounters) +{ + harness->setCommandZonesVisible(true); + harness->setPartnerZoneExpanded(true); + ASSERT_TRUE(harness->isCommanderTaxVisible()); + ASSERT_TRUE(harness->isPartnerTaxVisible()); + + harness->setCommandZonesVisible(false); + + EXPECT_FALSE(harness->isCommanderTaxVisible()) << "Commander tax hidden when zones hidden"; + EXPECT_FALSE(harness->isPartnerTaxVisible()) << "Partner tax hidden when zones hidden"; +} + +// Test: Showing zones restores commander tax, partner depends on state +TEST_F(CounterVisibilityTest, ShowZones_RestoresCorrectVisibility) +{ + harness->setCommandZonesVisible(true); + harness->setPartnerZoneExpanded(false); + harness->setCommandZonesVisible(false); + ASSERT_FALSE(harness->isCommanderTaxVisible()); + ASSERT_FALSE(harness->isPartnerTaxVisible()); + + harness->setCommandZonesVisible(true); + + EXPECT_TRUE(harness->isCommanderTaxVisible()) << "Commander tax restored when zones shown"; + EXPECT_FALSE(harness->isPartnerTaxVisible()) << "Partner tax stays hidden (partner still collapsed)"; +} + +// ============ Signal Emission Tests ============ + +// Test: Commander tax visibility signal emitted on change +TEST_F(CounterVisibilityTest, CommanderTax_SignalEmittedOnChange) +{ + harness->setCommandZonesVisible(true); // Start visible + QSignalSpy spy(harness, &CounterVisibilityTestHarness::commanderTaxVisibilityChanged); + + harness->setCommandZonesVisible(false); + + ASSERT_EQ(1, spy.count()) << "Signal should be emitted once"; + EXPECT_FALSE(spy.at(0).at(0).toBool()) << "Signal argument should be false"; +} + +// Test: Partner tax visibility signal emitted on change +TEST_F(CounterVisibilityTest, PartnerTax_SignalEmittedOnChange) +{ + harness->setCommandZonesVisible(true); + QSignalSpy spy(harness, &CounterVisibilityTestHarness::partnerTaxVisibilityChanged); + + harness->setPartnerZoneExpanded(true); + + ASSERT_EQ(1, spy.count()) << "Signal should be emitted once"; + EXPECT_TRUE(spy.at(0).at(0).toBool()) << "Signal argument should be true"; +} + +// Test: No signal when visibility unchanged +TEST_F(CounterVisibilityTest, NoSignal_WhenVisibilityUnchanged) +{ + harness->setCommandZonesVisible(true); + QSignalSpy spy(harness, &CounterVisibilityTestHarness::commanderTaxVisibilityChanged); + + harness->setCommandZonesVisible(true); // Same value + + EXPECT_EQ(0, spy.count()) << "Signal should not emit when visibility unchanged"; +} + +// ============ Edge Cases ============ + +// Test: Multiple rapid toggles maintain correct state +TEST_F(CounterVisibilityTest, RapidToggles_MaintainCorrectState) +{ + harness->setCommandZonesVisible(true); + + // Rapid expand/collapse cycle + harness->setPartnerZoneExpanded(true); + EXPECT_TRUE(harness->isPartnerTaxVisible()); + + harness->setPartnerZoneExpanded(false); + EXPECT_FALSE(harness->isPartnerTaxVisible()); + + harness->setPartnerZoneExpanded(true); + EXPECT_TRUE(harness->isPartnerTaxVisible()); + + harness->setPartnerZoneExpanded(false); + EXPECT_FALSE(harness->isPartnerTaxVisible()); + + // Final state check + EXPECT_FALSE(harness->isPartnerTaxVisible()) << "Partner tax should be hidden after toggling"; + EXPECT_TRUE(harness->isCommanderTaxVisible()) << "Commander tax should remain visible throughout"; +} + +// Test: Initial state has commander hidden (zones not yet set visible) +TEST_F(CounterVisibilityTest, InitialState_BothHidden) +{ + // Fresh harness starts with commandZonesVisible_ = true but counters not updated + CounterVisibilityTestHarness freshHarness; + + // Before any setters called, counters start hidden + EXPECT_FALSE(freshHarness.isCommanderTaxVisible()) << "Commander tax should start hidden"; + EXPECT_FALSE(freshHarness.isPartnerTaxVisible()) << "Partner tax should start hidden"; +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + +#include "counter_visibility_test.moc"