• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

paulmthompson / WhiskerToolbox / 17465586740

04 Sep 2025 01:21PM UTC coverage: 70.828% (-0.1%) from 70.97%
17465586740

push

github

paulmthompson
feature tree widget shouldn't emit signals during rebuild

121 of 131 new or added lines in 4 files covered. (92.37%)

108 existing lines in 7 files now uncovered.

34146 of 48210 relevant lines covered (70.83%)

1299.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.76
/src/WhiskerToolbox/Feature_Tree_Widget/Feature_Tree_Widget.test.cpp
1
#include "Feature_Tree_Widget.hpp"
2

3
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
4
#include "CoreGeometry/lines.hpp"
5
#include "CoreGeometry/points.hpp"
6
#include "DataManager.hpp"
7
#include "DigitalTimeSeries/Digital_Event_Series.hpp"
8
#include "Lines/Line_Data.hpp"
9
#include "Points/Point_Data.hpp"
10
#include "TimeFrame/StrongTimeTypes.hpp"
11
#include "TimeFrame/TimeFrame.hpp"
12

13
#include <catch2/catch_test_macros.hpp>
14

15
#include <QApplication>
16
#include <QMetaObject>
17
#include <QSignalBlocker>
18
#include <QTimer>
19
#include <QTreeWidget>
20
#include <QTreeWidgetItem>
21
#include <QWidget>
22

23
#include <memory>
24
#include <vector>
25

26
/**
27
 * @brief Test fixture for Feature_Tree_Widget tests
28
 * 
29
 * Creates a simple Qt application and DataManager with test data
30
 * for testing Feature_Tree_Widget state preservation functionality.
31
 */
32
class FeatureTreeWidgetTestFixture {
33
protected:
34
    FeatureTreeWidgetTestFixture() {
9✔
35
        // Create Qt application if one doesn't exist
36
        if (!QApplication::instance()) {
9✔
37
            static int argc = 1;
38
            static char * argv[] = {const_cast<char *>("test")};
39
            m_app = std::make_unique<QApplication>(argc, argv);
9✔
40
        }
41

42
        // Initialize DataManager
43
        m_data_manager = std::make_shared<DataManager>();
9✔
44

45
        // Create test data
46
        populateWithTestData();
9✔
47

48
        // Create the widget
49
        m_widget = std::make_unique<Feature_Tree_Widget>();
9✔
50
        m_widget->setDataManager(m_data_manager);
9✔
51
        m_widget->setOrganizeByDataType(true);
9✔
52
        // Group by common prefix before the final underscore (e.g., test_points_1, test_points_new -> test_points)
53
        m_widget->setGroupingPattern("(.+)_.*$");
27✔
54
    }
9✔
55

56
    ~FeatureTreeWidgetTestFixture() = default;
9✔
57

58
    /**
59
     * @brief Get the Feature_Tree_Widget instance
60
     * @return Reference to the widget
61
     */
62
    Feature_Tree_Widget & getWidget() { return *m_widget; }
9✔
63

64
    /**
65
     * @brief Get the DataManager instance
66
     * @return Reference to the DataManager
67
     */
68
    DataManager & getDataManager() { return *m_data_manager; }
7✔
69

70
    /**
71
     * @brief Get the internal tree widget for direct access
72
     * @return Pointer to the QTreeWidget
73
     */
74
    QTreeWidget * getTreeWidget() {
8✔
75
        return m_widget->findChild<QTreeWidget *>("treeWidget");
16✔
76
    }
77

78
private:
79
    void populateWithTestData() {
9✔
80
        // Create a default time frame
81
        auto timeframe = std::make_shared<TimeFrame>();
9✔
82
        TimeKey time_key("time");
9✔
83
        m_data_manager->setTime(time_key, timeframe);
9✔
84

85
        // Add some test PointData with grouping pattern
86
        auto point_data1 = std::make_shared<PointData>();
9✔
87
        point_data1->addAtTime(TimeFrameIndex(0), Point2D<float>{100.0f, 200.0f});
9✔
88
        point_data1->addAtTime(TimeFrameIndex(0), Point2D<float>{150.0f, 250.0f});
9✔
89
        m_data_manager->setData<PointData>("test_points_1", point_data1, time_key);
27✔
90

91
        auto point_data2 = std::make_shared<PointData>();
9✔
92
        point_data2->addAtTime(TimeFrameIndex(0), Point2D<float>{300.0f, 400.0f});
9✔
93
        m_data_manager->setData<PointData>("test_points_2", point_data2, time_key);
27✔
94

95
        // Add some test LineData
96
        auto line_data = std::make_shared<LineData>();
9✔
97
        line_data->addAtTime(TimeFrameIndex(0), Line2D(std::vector<Point2D<float>>{
27✔
98
                                                        Point2D<float>{0.0f, 0.0f},
99
                                                        Point2D<float>{100.0f, 100.0f}}));
100
        m_data_manager->setData<LineData>("test_lines_1", line_data, time_key);
27✔
101

102
        auto line_data2 = std::make_shared<LineData>();
9✔
103
        line_data2->addAtTime(TimeFrameIndex(0), Line2D(std::vector<Point2D<float>>{
27✔
104
                                                         Point2D<float>{50.0f, 50.0f},
105
                                                         Point2D<float>{150.0f, 150.0f}}));
106
        m_data_manager->setData<LineData>("test_lines_2", line_data2, time_key);
27✔
107

108
        // Add some test AnalogTimeSeries
109
        std::vector<float> values = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
27✔
110
        std::vector<int64_t> timestamps = {0, 1000, 2000, 3000, 4000};
27✔
111
        auto analog_data = std::make_shared<AnalogTimeSeries>(values, values.size());
9✔
112
        m_data_manager->setData<AnalogTimeSeries>("test_analog", analog_data, time_key);
27✔
113

114
        // Add some test DigitalEventSeries
115
        auto event_data = std::make_shared<DigitalEventSeries>();
9✔
116
        event_data->addEvent(1000);
9✔
117
        event_data->addEvent(2000);
9✔
118
        event_data->addEvent(3000);
9✔
119
        m_data_manager->setData<DigitalEventSeries>("test_events", event_data, time_key);
27✔
120
    }
18✔
121

122
    std::unique_ptr<QApplication> m_app;
123
    std::shared_ptr<DataManager> m_data_manager;
124
    std::unique_ptr<Feature_Tree_Widget> m_widget;
125
};
126

127
/**
128
 * @brief Helper function to find a tree item by text in column 0
129
 * @param tree The tree widget to search in
130
 * @param text The text to search for
131
 * @return Pointer to the found item, or nullptr if not found
132
 */
133
QTreeWidgetItem * findItemByText(QTreeWidget * tree, std::string const & text) {
10✔
134
    if (!tree) return nullptr;
10✔
135

136
    std::function<QTreeWidgetItem *(QTreeWidgetItem *)> searchItem = [&](QTreeWidgetItem * item) -> QTreeWidgetItem * {
20✔
137
        if (!item) return nullptr;
53✔
138

139
        if (item->text(0).toStdString() == text) {
53✔
140
            return item;
10✔
141
        }
142

143
        for (int i = 0; i < item->childCount(); ++i) {
70✔
144
            if (auto found = searchItem(item->child(i))) {
27✔
145
                return found;
×
146
            }
147
        }
148
        return nullptr;
43✔
149
    };
10✔
150

151
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
26✔
152
        if (auto found = searchItem(tree->topLevelItem(i))) {
26✔
153
            return found;
10✔
154
        }
155
    }
156
    return nullptr;
×
157
}
10✔
158

159
TEST_CASE_METHOD(FeatureTreeWidgetTestFixture, "Feature_Tree_Widget - Basic Functionality", "[Feature_Tree_Widget]") {
3✔
160
    auto & widget = getWidget();
3✔
161
    auto & dm = getDataManager();
3✔
162

163
    SECTION("Widget initialization") {
3✔
164
        REQUIRE(&widget != nullptr);
1✔
165
        REQUIRE(&dm != nullptr);
1✔
166
    }
3✔
167

168
    SECTION("Tree population") {
3✔
169
        widget.refreshTree();
1✔
170

171
        auto * tree = getTreeWidget();
1✔
172
        REQUIRE(tree != nullptr);
1✔
173
        REQUIRE(tree->topLevelItemCount() > 0);
1✔
174
        REQUIRE(tree->columnCount() == 4);// Feature, Type, Enabled, Color
1✔
175
    }
3✔
176

177
    SECTION("Data type organization") {
3✔
178
        widget.refreshTree();
1✔
179

180
        auto * tree = getTreeWidget();
1✔
181
        REQUIRE(tree != nullptr);
1✔
182

183
        // Should have data type groups
184
        bool foundPointData = false;
1✔
185
        bool foundLineData = false;
1✔
186
        bool foundAnalogTimeSeries = false;
1✔
187
        bool foundDigitalEventSeries = false;
1✔
188

189
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
6✔
190
            QTreeWidgetItem * item = tree->topLevelItem(i);
5✔
191
            std::string itemText = item->text(0).toStdString();
5✔
192

193
            if (itemText == "points") foundPointData = true;
5✔
194
            if (itemText == "line") foundLineData = true;
5✔
195
            if (itemText == "analog") foundAnalogTimeSeries = true;
5✔
196
            if (itemText == "digital_event") foundDigitalEventSeries = true;
5✔
197
        }
5✔
198

199
        REQUIRE(foundPointData);
1✔
200
        REQUIRE(foundLineData);
1✔
201
        REQUIRE(foundAnalogTimeSeries);
1✔
202
        REQUIRE(foundDigitalEventSeries);
1✔
203
    }
3✔
204
}
3✔
205

206
TEST_CASE_METHOD(FeatureTreeWidgetTestFixture, "Feature_Tree_Widget - State Preservation", "[Feature_Tree_Widget]") {
3✔
207
    auto & widget = getWidget();
3✔
208
    auto & dm = getDataManager();
3✔
209

210
    // Populate the tree initially
211
    widget.refreshTree();
3✔
212

213
    auto * tree = getTreeWidget();
3✔
214
    REQUIRE(tree != nullptr);
3✔
215

216
    SECTION("Checkbox state preservation after tree rebuild") {
3✔
217
        // Enable some checkboxes manually - start with a simpler test
218
        std::vector<std::string> enabledFeatures;
1✔
219

220
        // First, let's just enable a single leaf item to test basic state preservation
221
        auto * pointDataItem = findItemByText(tree, "points");
3✔
222
        REQUIRE(pointDataItem != nullptr);
1✔
223
        
224
        // Expand to access children
225
        pointDataItem->setExpanded(true);
1✔
226
        QApplication::processEvents();
1✔
227

228
        // Find the test_points group
229
        QTreeWidgetItem * testPointsGroup = nullptr;
1✔
230
        for (int i = 0; i < pointDataItem->childCount(); ++i) {
1✔
231
            QTreeWidgetItem * child = pointDataItem->child(i);
1✔
232
            if (child->text(0).toStdString() == "test_points") {
1✔
233
                testPointsGroup = child;
1✔
234
                testPointsGroup->setExpanded(true);
1✔
235
                break;
1✔
236
            }
237
        }
238
        REQUIRE(testPointsGroup != nullptr);
1✔
239
        
240
        // Enable just one individual feature first
241
        QTreeWidgetItem * testPoints1Item = nullptr;
1✔
242
        for (int i = 0; i < testPointsGroup->childCount(); ++i) {
2✔
243
            QTreeWidgetItem * child = testPointsGroup->child(i);
2✔
244
            if (child->text(0).toStdString() == "test_points_1") {
2✔
245
                testPoints1Item = child;
1✔
246
                child->setCheckState(2, Qt::Checked);
1✔
247
                enabledFeatures.push_back("test_points_1");
3✔
248
                
249
                // The issue is that setCheckState doesn't automatically emit itemChanged in the test environment
250
                // Let's manually emit the signal to trigger the widget's _itemChanged slot
251
                emit tree->itemChanged(child, 2);
1✔
252
                QApplication::processEvents();
1✔
253
                break;
1✔
254
            }
255
        }
256
        REQUIRE(testPoints1Item != nullptr);
1✔
257

258
        // Process Qt events to ensure signals are processed
259
        QApplication::processEvents();
1✔
260

261
        // Add a new feature to trigger tree rebuild
262
        auto new_point_data = std::make_shared<PointData>();
1✔
263
        new_point_data->addAtTime(TimeFrameIndex(0), Point2D<float>{500.0f, 600.0f});
1✔
264
        TimeKey time_key("time");
1✔
265
        dm.setData<PointData>("test_points_new", new_point_data, time_key);
3✔
266

267
        // Process Qt events to ensure the tree is rebuilt
268
        QApplication::processEvents();
1✔
269

270
        // Verify that the enabled checkbox is still enabled
271
        auto * newPointDataItem = findItemByText(tree, "points");
3✔
272
        REQUIRE(newPointDataItem != nullptr);
1✔
273

274
        // Find the test_points group again
275
        QTreeWidgetItem * newTestPointsGroup = nullptr;
1✔
276
        for (int i = 0; i < newPointDataItem->childCount(); ++i) {
1✔
277
            QTreeWidgetItem * child = newPointDataItem->child(i);
1✔
278
            if (child->text(0).toStdString() == "test_points") {
1✔
279
                newTestPointsGroup = child;
1✔
280
                break;
1✔
281
            }
282
        }
283
        REQUIRE(newTestPointsGroup != nullptr);
1✔
284
        
285
        // Check that test_points_1 is still enabled
286
        bool foundEnabledChild = false;
1✔
287
        for (int i = 0; i < newTestPointsGroup->childCount(); ++i) {
3✔
288
            QTreeWidgetItem * child = newTestPointsGroup->child(i);
3✔
289
            if (child->text(0).toStdString() == "test_points_1") {
3✔
290
                REQUIRE(child->checkState(2) == Qt::Checked);
1✔
291
                foundEnabledChild = true;
1✔
292
                break;
1✔
293
            }
294
        }
295
        REQUIRE(foundEnabledChild);
1✔
296

297
        // Verify the new feature is present but not enabled
298
        bool foundNewFeature = false;
1✔
299
        for (int i = 0; i < newTestPointsGroup->childCount(); ++i) {
1✔
300
            QTreeWidgetItem * child = newTestPointsGroup->child(i);
1✔
301
            if (child->text(0).toStdString() == "test_points_new") {
1✔
302
                REQUIRE(child->checkState(2) == Qt::Unchecked);
1✔
303
                foundNewFeature = true;
1✔
304
                break;
1✔
305
            }
306
        }
307
        REQUIRE(foundNewFeature);
1✔
308
    }
4✔
309

310
    SECTION("Selection state preservation after tree rebuild") {
3✔
311
        // Select a specific feature
312
        auto * lineDataItem = findItemByText(tree, "line");
3✔
313
        REQUIRE(lineDataItem != nullptr);
1✔
314

315
        // Expand the line group to access children
316
        lineDataItem->setExpanded(true);
1✔
317

318
        // Look for the test_lines group first, then find test_lines_1 within it
319
        QTreeWidgetItem * testLinesGroup = nullptr;
1✔
320
        for (int i = 0; i < lineDataItem->childCount(); ++i) {
1✔
321
            QTreeWidgetItem * child = lineDataItem->child(i);
1✔
322
            if (child->text(0).toStdString() == "test_lines") {
1✔
323
                testLinesGroup = child;
1✔
324
                testLinesGroup->setExpanded(true);
1✔
325
                break;
1✔
326
            }
327
        }
328

329
        QTreeWidgetItem * selectedChild = nullptr;
1✔
330
        if (testLinesGroup) {
1✔
331
            for (int i = 0; i < testLinesGroup->childCount(); ++i) {
2✔
332
                QTreeWidgetItem * child = testLinesGroup->child(i);
2✔
333
                if (child->text(0).toStdString() == "test_lines_1") {
2✔
334
                    selectedChild = child;
1✔
335
                    break;
1✔
336
                }
337
            }
338
        }
339
        REQUIRE(selectedChild != nullptr);
1✔
340

341
        // Select the child
342
        tree->setCurrentItem(selectedChild);
1✔
343
        QApplication::processEvents();
1✔
344

345
        // Verify the feature is selected
346
        std::string initialSelected = widget.getSelectedFeature();
1✔
347
        REQUIRE(initialSelected == "test_lines_1");
1✔
348

349
        // Add a new feature to trigger tree rebuild
350
        std::vector<float> values = {10.0f, 20.0f, 30.0f};
3✔
351
        auto new_analog_data = std::make_shared<AnalogTimeSeries>(values, values.size());
1✔
352
        TimeKey time_key("time");
1✔
353
        dm.setData<AnalogTimeSeries>("test_analog_new", new_analog_data, time_key);
3✔
354

355
        // Process Qt events to ensure the tree is rebuilt
356
        QApplication::processEvents();
1✔
357

358
        // Verify that the previously selected feature is still selected
359
        std::string currentlySelected = widget.getSelectedFeature();
1✔
360
        REQUIRE(currentlySelected == "test_lines_1");
1✔
361

362
        // Verify the tree item is still selected
363
        QTreeWidgetItem * currentItem = tree->currentItem();
1✔
364
        REQUIRE(currentItem != nullptr);
1✔
365
        REQUIRE(currentItem->text(0).toStdString() == "test_lines_1");
1✔
366
    }
4✔
367

368
    SECTION("Expansion state preservation after tree rebuild") {
3✔
369
        // Expand some groups
370
        auto * pointDataItem = findItemByText(tree, "points");
3✔
371
        auto * lineDataItem = findItemByText(tree, "line");
3✔
372
        REQUIRE(pointDataItem != nullptr);
1✔
373
        REQUIRE(lineDataItem != nullptr);
1✔
374

375
        // Expand points but not line
376
        pointDataItem->setExpanded(true);
1✔
377
        lineDataItem->setExpanded(false);
1✔
378

379
        QApplication::processEvents();
1✔
380

381
        // Add a new feature to trigger tree rebuild
382
        auto new_event_data = std::make_shared<DigitalEventSeries>();
1✔
383
        new_event_data->addEvent(5000);
1✔
384
        TimeKey time_key("time");
1✔
385
        dm.setData<DigitalEventSeries>("test_events_new", new_event_data, time_key);
3✔
386

387
        // Process Qt events to ensure the tree is rebuilt
388
        QApplication::processEvents();
1✔
389

390
        // Verify expansion states are preserved
391
        auto * newPointDataItem = findItemByText(tree, "points");
3✔
392
        auto * newLineDataItem = findItemByText(tree, "line");
3✔
393
        REQUIRE(newPointDataItem != nullptr);
1✔
394
        REQUIRE(newLineDataItem != nullptr);
1✔
395

396
        REQUIRE(newPointDataItem->isExpanded() == true);
1✔
397
        REQUIRE(newLineDataItem->isExpanded() == false);
1✔
398
    }
4✔
399
}
3✔
400

401
TEST_CASE_METHOD(FeatureTreeWidgetTestFixture, "Feature_Tree_Widget - Signal Emission", "[Feature_Tree_Widget]") {
2✔
402
    auto & widget = getWidget();
2✔
403

404
    // Populate the tree
405
    widget.refreshTree();
2✔
406

407
    auto * tree = getTreeWidget();
2✔
408
    REQUIRE(tree != nullptr);
2✔
409
    REQUIRE(tree->topLevelItemCount() > 0);
2✔
410

411
    SECTION("Feature selection signal emission") {
2✔
412
        bool signalEmitted = false;
1✔
413
        std::string emittedFeature;
1✔
414

415
        // Connect to the signal
416
        QObject::connect(&widget, &Feature_Tree_Widget::featureSelected,
3✔
417
                         [&signalEmitted, &emittedFeature](std::string const & feature) {
2✔
418
                             signalEmitted = true;
×
419
                             emittedFeature = feature;
×
420
                         });
×
421

422
        // Find and click on a leaf feature
423
        auto * pointDataItem = findItemByText(tree, "points");
3✔
424
        REQUIRE(pointDataItem != nullptr);
1✔
425

426
        pointDataItem->setExpanded(true);
1✔
427
        QApplication::processEvents();
1✔
428

429
        QTreeWidgetItem * leafItem = nullptr;
1✔
430
        for (int i = 0; i < pointDataItem->childCount(); ++i) {
2✔
431
            QTreeWidgetItem * child = pointDataItem->child(i);
1✔
432
            if (child->childCount() == 0) {// This is a leaf
1✔
433
                leafItem = child;
×
434
                break;
×
435
            }
436
        }
437

438
        if (leafItem) {
1✔
439
            // Simulate item selection by calling the slot directly
440
            widget.findChild<QTreeWidget *>()->setCurrentItem(leafItem);
×
441

442
            // Process events
443
            QApplication::processEvents();
×
444

445
            // The signal should have been emitted
446
            // Note: This might not work in headless testing, but the structure is correct
447
        }
448
    }
3✔
449

450
    SECTION("Add/Remove feature signal emission") {
2✔
451
        bool addSignalEmitted = false;
1✔
452
        bool removeSignalEmitted = false;
1✔
453
        std::string addedFeature;
1✔
454
        std::string removedFeature;
1✔
455

456
        // Connect to the signals
457
        QObject::connect(&widget, &Feature_Tree_Widget::addFeature,
3✔
458
                         [&addSignalEmitted, &addedFeature](std::string const & feature) {
2✔
459
                             addSignalEmitted = true;
×
460
                             addedFeature = feature;
×
461
                         });
×
462

463
        QObject::connect(&widget, &Feature_Tree_Widget::removeFeature,
3✔
464
                         [&removeSignalEmitted, &removedFeature](std::string const & feature) {
2✔
465
                             removeSignalEmitted = true;
×
466
                             removedFeature = feature;
×
467
                         });
×
468

469
        // Find a leaf item and toggle its checkbox
470
        auto * pointDataItem = findItemByText(tree, "points");
3✔
471
        REQUIRE(pointDataItem != nullptr);
1✔
472

473
        pointDataItem->setExpanded(true);
1✔
474
        QApplication::processEvents();
1✔
475

476
        QTreeWidgetItem * leafItem = nullptr;
1✔
477
        for (int i = 0; i < pointDataItem->childCount(); ++i) {
2✔
478
            QTreeWidgetItem * child = pointDataItem->child(i);
1✔
479
            if (child->childCount() == 0) {// This is a leaf
1✔
480
                leafItem = child;
×
481
                break;
×
482
            }
483
        }
484

485
        if (leafItem) {
1✔
486
            // Enable the checkbox
487
            leafItem->setCheckState(2, Qt::Checked);
×
488
            QApplication::processEvents();
×
489

490
            // Disable the checkbox
491
            leafItem->setCheckState(2, Qt::Unchecked);
×
492
            QApplication::processEvents();
×
493

494
            // The signals should have been emitted
495
            // Note: This might not work in headless testing, but the structure is correct
496
        }
497
    }
3✔
498
}
2✔
499

500
TEST_CASE_METHOD(FeatureTreeWidgetTestFixture, "Feature_Tree_Widget - No emissions during rebuild", "[Feature_Tree_Widget][Signals]") {
1✔
501
    auto & widget = getWidget();
1✔
502
    auto & dm = getDataManager();
1✔
503

504
    // Populate the tree
505
    widget.refreshTree();
1✔
506
    QApplication::processEvents();
1✔
507

508
    // Counters for signal emissions
509
    int addFeaturesCount = 0;
1✔
510
    int removeFeaturesCount = 0;
1✔
511
    int addFeatureCount = 0;
1✔
512
    int removeFeatureCount = 0;
1✔
513
    int featureSelectedCount = 0;
1✔
514

515
    QObject::connect(&widget, &Feature_Tree_Widget::addFeatures, [&addFeaturesCount](std::vector<std::string> const &) {
1✔
516
        addFeaturesCount++;
2✔
517
    });
2✔
518
    QObject::connect(&widget, &Feature_Tree_Widget::removeFeatures, [&removeFeaturesCount](std::vector<std::string> const &) {
1✔
519
        removeFeaturesCount++;
1✔
520
    });
1✔
521
    QObject::connect(&widget, &Feature_Tree_Widget::addFeature, [&addFeatureCount](std::string const &) {
1✔
522
        addFeatureCount++;
2✔
523
    });
2✔
524
    QObject::connect(&widget, &Feature_Tree_Widget::removeFeature, [&removeFeatureCount](std::string const &) {
1✔
NEW
525
        removeFeatureCount++;
×
NEW
526
    });
×
527
    QObject::connect(&widget, &Feature_Tree_Widget::featureSelected, [&featureSelectedCount](std::string const &) {
1✔
NEW
528
        featureSelectedCount++;
×
NEW
529
    });
×
530

531
    // Trigger a rebuild by adding analog data
532
    std::vector<float> values = {10.0f, 20.0f, 30.0f};
3✔
533
    auto new_analog_data = std::make_shared<AnalogTimeSeries>(values, values.size());
1✔
534
    TimeKey time_key("time");
1✔
535
    dm.setTime(time_key, std::make_shared<TimeFrame>()); // ensure time exists
1✔
536
    dm.setData<AnalogTimeSeries>("probe_analog_1", new_analog_data, time_key);
3✔
537

538
    QApplication::processEvents();
1✔
539

540
    // No signals should have been emitted during the rebuild
541
    REQUIRE(addFeaturesCount == 0);
1✔
542
    REQUIRE(removeFeaturesCount == 0);
1✔
543
    REQUIRE(addFeatureCount == 0);
1✔
544
    REQUIRE(removeFeatureCount == 0);
1✔
545
    REQUIRE(featureSelectedCount == 0);
1✔
546

547
    // Trigger another rebuild by adding digital event data
548
    auto event_data = std::make_shared<DigitalEventSeries>();
1✔
549
    event_data->addEvent(1000);
1✔
550
    dm.setData<DigitalEventSeries>("probe_events_1", event_data, time_key);
3✔
551

552
    QApplication::processEvents();
1✔
553

554
    // Still no signals from rebuild
555
    REQUIRE(addFeaturesCount == 0);
1✔
556
    REQUIRE(removeFeaturesCount == 0);
1✔
557
    REQUIRE(addFeatureCount == 0);
1✔
558
    REQUIRE(removeFeatureCount == 0);
1✔
559
    REQUIRE(featureSelectedCount == 0);
1✔
560

561
    // Now simulate user action: toggle a leaf checkbox to verify signals do emit on user change
562
    auto * tree = getTreeWidget();
1✔
563
    REQUIRE(tree != nullptr);
1✔
564
    // Find a leaf under the "analog" group if available
565
    QTreeWidgetItem * analogGroup = findItemByText(tree, "analog");
3✔
566
    if (analogGroup) analogGroup->setExpanded(true);
1✔
567
    QApplication::processEvents();
1✔
568

569
    QTreeWidgetItem * leaf = nullptr;
1✔
570
    if (analogGroup) {
1✔
571
        for (int i = 0; i < analogGroup->childCount() && !leaf; ++i) {
2✔
572
            QTreeWidgetItem * child = analogGroup->child(i);
1✔
573
            if (child->childCount() == 0) leaf = child;
1✔
574
        }
575
    }
576

577
    if (!leaf) {
1✔
578
        // Fallback: search any top-level leaf
NEW
579
        for (int i = 0; i < tree->topLevelItemCount() && !leaf; ++i) {
×
NEW
580
            QTreeWidgetItem * tl = tree->topLevelItem(i);
×
NEW
581
            if (tl->childCount() == 0) leaf = tl;
×
582
        }
583
    }
584

585
    if (leaf) {
1✔
586
        int addFeaturesBefore = addFeaturesCount;
1✔
587
        int addFeatureBefore = addFeatureCount;
1✔
588

589
        leaf->setCheckState(2, Qt::Checked);
1✔
590
        // Manually emit for reliability in headless test as in other sections
591
        emit tree->itemChanged(leaf, 2);
1✔
592
        QApplication::processEvents();
1✔
593

594
        REQUIRE(addFeaturesCount >= addFeaturesBefore); // may emit group and single depending on structure
1✔
595
        REQUIRE(addFeatureCount >= addFeatureBefore);
1✔
596
    }
597
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc