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

paulmthompson / WhiskerToolbox / 18840223125

27 Oct 2025 12:01PM UTC coverage: 73.058% (+0.2%) from 72.822%
18840223125

push

github

paulmthompson
fix failing tests from table designer redesign

69 of 74 new or added lines in 2 files covered. (93.24%)

669 existing lines in 10 files now uncovered.

56029 of 76691 relevant lines covered (73.06%)

45039.63 hits per line

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

98.84
/src/WhiskerToolbox/TableDesignerWidget/TableDesignerWidget.test.cpp
1
#include <catch2/catch_test_macros.hpp>
2
#include <catch2/catch_approx.hpp>
3

4
#include "TableDesignerWidget.hpp"
5
#include "TableJSONWidget.hpp"
6
// DataManager and related includes
7
#include "DataManager.hpp"
8
#include "DataManager/utils/TableView/adapters/DataManagerExtension.h"
9
#include "DataManager/utils/TableView/TableRegistry.hpp"
10
#include "DataManager/utils/TableView/ComputerRegistry.hpp"
11
#include "DataManager/utils/TableView/computers/EventInIntervalComputer.h"
12
#include "DataManager/utils/TableView/computers/AnalogSliceGathererComputer.h"
13
#include "DataManager/utils/TableView/core/TableView.h"
14
#include "DataManager/DigitalTimeSeries/Digital_Event_Series.hpp"
15
#include "DataManager/DigitalTimeSeries/Digital_Interval_Series.hpp"
16
#include "DataManager/AnalogTimeSeries/Analog_Time_Series.hpp"
17
#include "DataManager/Lines/Line_Data.hpp"
18
#include "TimeFrame/TimeFrame.hpp"
19

20
// Qt includes for widget testing
21
#include <QApplication>
22
#include <QTreeWidget>
23
#include <QTreeWidgetItem>
24
#include <QComboBox>
25
#include <QTableView>
26
#include <QHeaderView>
27
#include <QTextEdit>
28
#include <QPushButton>
29
#include <QTemporaryFile>
30
#include <QDir>
31
#include <QMessageBox>
32
#include <QRadioButton>
33

34
#include <memory>
35
#include <vector>
36
#include <algorithm>
37
#include <numeric>
38

39
/**
40
 * @brief Test fixture for TableDesignerWidget that creates test data and computers
41
 * 
42
 * This fixture provides:
43
 * - DataManager with TimeFrames and test data
44
 * - TableRegistry with registered computers
45
 * - Methods to verify tree widget functionality
46
 */
47
class TableDesignerWidgetTestFixture {
48
protected:
49
    TableDesignerWidgetTestFixture() {
23✔
50
        // Initialize Qt application if not already done
51
        if (!QApplication::instance()) {
23✔
52
            int argc = 0;
23✔
53
            char** argv = nullptr;
23✔
54
            app = std::make_unique<QApplication>(argc, argv);
23✔
55
        }
56
        
57
        // Initialize the DataManager
58
        m_data_manager = std::make_unique<DataManager>();
23✔
59
        
60
        // Populate with test data
61
        populateWithTestData();
23✔
62
        
63
        // Register computers in the registry
64
        registerTestComputers();
23✔
65
    }
23✔
66

67
    ~TableDesignerWidgetTestFixture() = default;
23✔
68

69
    /**
70
     * @brief Get the DataManager instance
71
     */
72
    DataManager & getDataManager() { return *m_data_manager; }
2✔
73
    std::shared_ptr<DataManager> getDataManagerPtr() { 
23✔
74
        return std::shared_ptr<DataManager>(m_data_manager.get(), [](DataManager*){}); 
23✔
75
    }
76

77
    /**
78
     * @brief Get the TableRegistry from DataManager
79
     */
80
    TableRegistry & getTableRegistry() { return *m_data_manager->getTableRegistry(); }
39✔
81

82
private:
83
    std::unique_ptr<DataManager> m_data_manager;
84
    std::unique_ptr<QApplication> app;
85

86
    /**
87
     * @brief Populate the DataManager with test data
88
     */
89
    void populateWithTestData() {
23✔
90
        createTimeFrames();
23✔
91
        createBehaviorIntervals();
23✔
92
        createSpikeEvents();
23✔
93
        createAnalogData();
23✔
94
        createLineData();
23✔
95
    }
23✔
96

97
    /**
98
     * @brief Create TimeFrame objects for different data streams
99
     */
100
    void createTimeFrames() {
23✔
101
        // Create "behavior_time" timeframe: 0 to 100 (101 points)
102
        std::vector<int> behavior_time_values(101);
69✔
103
        std::iota(behavior_time_values.begin(), behavior_time_values.end(), 0);
23✔
104
        auto behavior_time_frame = std::make_shared<TimeFrame>(behavior_time_values);
23✔
105
        m_data_manager->setTime(TimeKey("behavior_time"), behavior_time_frame, true);
23✔
106

107
        // Create "spike_time" timeframe: 0, 2, 4, 6, ..., 100 (51 points)
108
        std::vector<int> spike_time_values;
23✔
109
        spike_time_values.reserve(51);
23✔
110
        for (int i = 0; i <= 50; ++i) {
1,196✔
111
            spike_time_values.push_back(i * 2);
1,173✔
112
        }
113
        auto spike_time_frame = std::make_shared<TimeFrame>(spike_time_values);
23✔
114
        m_data_manager->setTime(TimeKey("spike_time"), spike_time_frame, true);
23✔
115

116
        // Create "analog_time" timeframe: 0 to 200 (201 points, higher resolution)
117
        std::vector<int> analog_time_values(201);
69✔
118
        std::iota(analog_time_values.begin(), analog_time_values.end(), 0);
23✔
119
        auto analog_time_frame = std::make_shared<TimeFrame>(analog_time_values);
23✔
120
        m_data_manager->setTime(TimeKey("analog_time"), analog_time_frame, true);
23✔
121
    }
46✔
122

123
    /**
124
     * @brief Create behavior intervals (row intervals for testing)
125
     */
126
    void createBehaviorIntervals() {
23✔
127
        auto behavior_intervals = std::make_shared<DigitalIntervalSeries>();
23✔
128
        
129
        // Create 4 behavior periods for testing
130
        behavior_intervals->addEvent(TimeFrameIndex(10), TimeFrameIndex(25));  // Exploration 1
23✔
131
        behavior_intervals->addEvent(TimeFrameIndex(30), TimeFrameIndex(40));  // Rest
23✔
132
        behavior_intervals->addEvent(TimeFrameIndex(50), TimeFrameIndex(70));  // Exploration 2
23✔
133
        behavior_intervals->addEvent(TimeFrameIndex(80), TimeFrameIndex(95));  // Social
23✔
134

135
        m_data_manager->setData<DigitalIntervalSeries>("BehaviorPeriods", behavior_intervals, TimeKey("behavior_time"));
69✔
136
    }
46✔
137

138
    /**
139
     * @brief Create spike event data
140
     */
141
    void createSpikeEvents() {
23✔
142
        // Create spike train for Neuron1
143
        std::vector<float> neuron1_spikes = {
23✔
144
            1.0f, 6.0f, 7.0f, 11.0f, 16.0f, 26.0f, 27.0f, 34.0f, 41.0f, 45.0f
145
        };
69✔
146
        auto neuron1_series = std::make_shared<DigitalEventSeries>(neuron1_spikes);
23✔
147
        m_data_manager->setData<DigitalEventSeries>("Neuron1Spikes", neuron1_series, TimeKey("spike_time"));
69✔
148
        
149
        // Create spike train for Neuron2
150
        std::vector<float> neuron2_spikes = {
23✔
151
            0.0f, 1.0f, 2.0f, 5.0f, 6.0f, 8.0f, 9.0f, 15.0f, 16.0f, 18.0f,
152
            25.0f, 26.0f, 28.0f, 29.0f, 33.0f, 34.0f, 40.0f, 41.0f, 42.0f, 45.0f, 46.0f
153
        };
69✔
154
        auto neuron2_series = std::make_shared<DigitalEventSeries>(neuron2_spikes);
23✔
155
        m_data_manager->setData<DigitalEventSeries>("Neuron2Spikes", neuron2_series, TimeKey("spike_time"));
69✔
156
    }
46✔
157

158
    /**
159
     * @brief Create analog data for testing
160
     */
161
    void createAnalogData() {
23✔
162
        // Create LFP signal (sine wave)
163
        std::vector<float> lfp_values;
23✔
164
        std::vector<TimeFrameIndex> lfp_indices;
23✔
165
        lfp_values.reserve(201);
23✔
166
        lfp_indices.reserve(201);
23✔
167
        
168
        for (int i = 0; i < 201; ++i) {
4,646✔
169
            float value = std::sin(2.0f * M_PI * i / 50.0f); // 4 cycles over 200 points
4,623✔
170
            lfp_values.push_back(value);
4,623✔
171
            lfp_indices.emplace_back(i);
4,623✔
172
        }
173
        
174
        auto lfp_series = std::make_shared<AnalogTimeSeries>(lfp_values, lfp_indices);
23✔
175
        m_data_manager->setData<AnalogTimeSeries>("LFP", lfp_series, TimeKey("analog_time"));
69✔
176

177
        // Create EMG signal (random noise)
178
        std::vector<float> emg_values;
23✔
179
        std::vector<TimeFrameIndex> emg_indices;
23✔
180
        emg_values.reserve(201);
23✔
181
        emg_indices.reserve(201);
23✔
182
        
183
        std::srand(12345); // Deterministic random for testing
23✔
184
        for (int i = 0; i < 201; ++i) {
4,646✔
185
            float value = static_cast<float>(std::rand()) / RAND_MAX - 0.5f; // [-0.5, 0.5]
4,623✔
186
            emg_values.push_back(value);
4,623✔
187
            emg_indices.emplace_back(i);
4,623✔
188
        }
189
        
190
        auto emg_series = std::make_shared<AnalogTimeSeries>(emg_values, emg_indices);
23✔
191
        m_data_manager->setData<AnalogTimeSeries>("EMG", emg_series, TimeKey("analog_time"));
69✔
192
    }
46✔
193

194
    /**
195
     * @brief Create line data for testing
196
     */
197
    void createLineData() {
23✔
198
        // Create line data with simple geometric shapes
199
        auto line_data = std::make_shared<LineData>();
23✔
200
        
201
        // Create a simple line at t=0
202
        std::vector<float> xs1 = {0.0f, 10.0f, 20.0f, 30.0f};
69✔
203
        std::vector<float> ys1 = {0.0f, 5.0f, 10.0f, 15.0f};
69✔
204
        line_data->addAtTime(TimeFrameIndex(0), xs1, ys1, false);
23✔
205
        
206
        // Create another line at t=10
207
        std::vector<float> xs2 = {5.0f, 15.0f, 25.0f};
69✔
208
        std::vector<float> ys2 = {2.0f, 8.0f, 12.0f};
69✔
209
        line_data->addAtTime(TimeFrameIndex(10), xs2, ys2, false);
23✔
210
        
211
        // Create a third line at t=20
212
        std::vector<float> xs3 = {10.0f, 20.0f, 30.0f, 40.0f};
69✔
213
        std::vector<float> ys3 = {1.0f, 6.0f, 11.0f, 16.0f};
69✔
214
        line_data->addAtTime(TimeFrameIndex(20), xs3, ys3, false);
23✔
215
        
216
        // Set identity context and rebuild entity IDs
217
        line_data->setIdentityContext("TestLines", m_data_manager->getEntityRegistry());
69✔
218
        line_data->rebuildAllEntityIds();
23✔
219
        
220
        m_data_manager->setData<LineData>("TestLines", line_data, TimeKey("behavior_time"));
69✔
221
    }
46✔
222

223
    /**
224
     * @brief Register test computers in the computer registry
225
     */
226
    void registerTestComputers() {
23✔
227
        auto& registry = getTableRegistry();
23✔
228
        auto& computer_registry = registry.getComputerRegistry();
23✔
229
    }
23✔
230
};
231

232
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Basic tree functionality", "[TableDesignerWidget][Tree]") {
3✔
233
    
234
    SECTION("Create widget and verify tree is populated") {
3✔
235
        TableDesignerWidget widget(getDataManagerPtr());
1✔
236

237
        // Select an interval-based row source so relevant computers are visible
238
        auto* row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
239
        REQUIRE(row_combo != nullptr);
1✔
240
        int intervals_index = -1;
1✔
241
        for (int i = 0; i < row_combo->count(); ++i) {
7✔
242
            if (row_combo->itemText(i).startsWith("Intervals: ")) { intervals_index = i; break; }
7✔
243
        }
244
        REQUIRE(intervals_index >= 0);
1✔
245
        row_combo->setCurrentIndex(intervals_index);
1✔
246
        
247
        // Get the computers tree
248
        auto* tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
249
        REQUIRE(tree != nullptr);
1✔
250
        
251
        // Verify tree has been populated with data sources
252
        REQUIRE(tree->topLevelItemCount() > 0);
1✔
253
        
254
        // Check for expected data source categories
255
        QStringList found_sources;
1✔
256
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
6✔
257
            auto* item = tree->topLevelItem(i);
5✔
258
            found_sources << item->text(0);
5✔
259
        }
260
        
261
        // Should have event sources and analog, under interval-based selection
262
        bool has_neuron1_events = false;
1✔
263
        bool has_neuron2_events = false;
1✔
264
        bool has_behavior_intervals = false;
1✔
265
        bool has_analog_sources = false;
1✔
266
        
267
        for (const QString& source : found_sources) {
6✔
268
            if (source.contains("Neuron1Spikes")) has_neuron1_events = true;
5✔
269
            if (source.contains("Neuron2Spikes")) has_neuron2_events = true;
5✔
270
            if (source.contains("BehaviorPeriods")) has_behavior_intervals = true;
5✔
271
            if (source.contains("LFP") || source.contains("EMG")) has_analog_sources = true;
5✔
272
        }
273
        
274
        REQUIRE(has_neuron1_events);
1✔
275
        REQUIRE(has_neuron2_events);
1✔
276
        REQUIRE(has_behavior_intervals);
1✔
277
        REQUIRE(has_analog_sources);
1✔
278
    }
4✔
279
    
280
    SECTION("Verify tree structure - data sources have computer children") {
3✔
281
        TableDesignerWidget widget(getDataManagerPtr());
1✔
282

283
        // Select an interval-based row source so event computers are visible
284
        auto* row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
285
        REQUIRE(row_combo != nullptr);
1✔
286
        int intervals_index = -1;
1✔
287
        for (int i = 0; i < row_combo->count(); ++i) {
7✔
288
            if (row_combo->itemText(i).startsWith("Intervals: ")) { intervals_index = i; break; }
7✔
289
        }
290
        REQUIRE(intervals_index >= 0);
1✔
291
        row_combo->setCurrentIndex(intervals_index);
1✔
292

293
        auto* tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
294
        REQUIRE(tree != nullptr);
1✔
295
        
296
        // Find an event source and verify it has event computers
297
        QTreeWidgetItem* event_source_item = nullptr;
1✔
298
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
4✔
299
            auto* item = tree->topLevelItem(i);
4✔
300
            if (item->text(0).contains("Events: Neuron1Spikes")) {
4✔
301
                event_source_item = item;
1✔
302
                break;
1✔
303
            }
304
        }
305
        
306
        REQUIRE(event_source_item != nullptr);
1✔
307
        REQUIRE(event_source_item->childCount() > 0);
1✔
308
        
309
        // Check that children are computers with checkboxes
310
        bool has_presence_computer = false;
1✔
311
        bool has_count_computer = false;
1✔
312
        
313
        for (int j = 0; j < event_source_item->childCount(); ++j) {
4✔
314
            auto* computer_item = event_source_item->child(j);
3✔
315
            
316
            // Verify computer items have checkboxes in column 1
317
            REQUIRE((computer_item->flags() & Qt::ItemIsUserCheckable) != 0);
3✔
318
            REQUIRE(computer_item->checkState(1) == Qt::Unchecked); // Initially unchecked
3✔
319
            
320
            // Check for expected computers
321
            QString computer_name = computer_item->text(0);
3✔
322
            if (computer_name.contains("Event Presence")) has_presence_computer = true;
3✔
323
            if (computer_name.contains("Event Count")) has_count_computer = true;
3✔
324
            
325
            // Verify column name is editable and has default value
326
            REQUIRE((computer_item->flags() & Qt::ItemIsEditable) != 0);
3✔
327
            REQUIRE(!computer_item->text(2).isEmpty()); // Should have default column name
3✔
328
        }
3✔
329
        
330
        REQUIRE(has_presence_computer);
1✔
331
        REQUIRE(has_count_computer);
1✔
332
    }
4✔
333
    
334
    SECTION("Verify analog sources have analog computers") {
3✔
335
        TableDesignerWidget widget(getDataManagerPtr());
1✔
336
        
337
        // Select an interval-based row source so interval analog computers appear
338
        auto* row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
339
        REQUIRE(row_combo != nullptr);
1✔
340
        int intervals_index = -1;
1✔
341
        for (int i = 0; i < row_combo->count(); ++i) {
7✔
342
            if (row_combo->itemText(i).startsWith("Intervals: ")) { intervals_index = i; break; }
7✔
343
        }
344
        REQUIRE(intervals_index >= 0);
1✔
345
        row_combo->setCurrentIndex(intervals_index);
1✔
346
        
347
        auto* tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
348
        REQUIRE(tree != nullptr);
1✔
349
        
350
        // Find an analog source
351
        QTreeWidgetItem* analog_source_item = nullptr;
1✔
352
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
3✔
353
            auto* item = tree->topLevelItem(i);
3✔
354
            if (item->text(0).contains("analog:LFP")) {
3✔
355
                analog_source_item = item;
1✔
356
                break;
1✔
357
            }
358
        }
359
        
360
        REQUIRE(analog_source_item != nullptr);
1✔
361
        REQUIRE(analog_source_item->childCount() > 0);
1✔
362
        
363
        // Check for analog computers
364
        bool has_slice_gatherer = false;
1✔
365
        bool has_mean_computer = false;
1✔
366
        
367
        for (int j = 0; j < analog_source_item->childCount(); ++j) {
9✔
368
            auto* computer_item = analog_source_item->child(j);
8✔
369
            QString computer_name = computer_item->text(0);
8✔
370
            
371
            if (computer_name.contains("Analog Slice Gatherer")) has_slice_gatherer = true;
8✔
372
            if (computer_name.contains("Analog Mean")) has_mean_computer = true;
8✔
373
        }
8✔
374
        
375
        // Should have at least some analog computers
376
        REQUIRE((has_slice_gatherer || has_mean_computer));
1✔
377
    }
4✔
378
}
3✔
379

380
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Computer enabling and column generation", "[TableDesignerWidget][Computers]") {
2✔
381
    
382
    SECTION("Enable computers and verify column info generation") {
2✔
383
        TableDesignerWidget widget(getDataManagerPtr());
1✔
384
        
385
        // Select an interval-based row source so event computers are available
386
        auto* row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
387
        REQUIRE(row_combo != nullptr);
1✔
388
        int intervals_index = -1;
1✔
389
        for (int i = 0; i < row_combo->count(); ++i) {
7✔
390
            if (row_combo->itemText(i).startsWith("Intervals: ")) { intervals_index = i; break; }
7✔
391
        }
392
        REQUIRE(intervals_index >= 0);
1✔
393
        row_combo->setCurrentIndex(intervals_index);
1✔
394
        
395
        auto* tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
396
        REQUIRE(tree != nullptr);
1✔
397
        
398
        // Find and enable some computers
399
        QTreeWidgetItem* presence_computer = nullptr;
1✔
400
        QTreeWidgetItem* count_computer = nullptr;
1✔
401
        
402
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
6✔
403
            auto* data_source_item = tree->topLevelItem(i);
5✔
404
            if (!data_source_item->text(0).contains("Events: Neuron1Spikes")) continue;
5✔
405
            
406
            for (int j = 0; j < data_source_item->childCount(); ++j) {
4✔
407
                auto* computer_item = data_source_item->child(j);
3✔
408
                QString computer_name = computer_item->text(0);
3✔
409
                
410
                if (computer_name.contains("Event Presence")) {
3✔
411
                    presence_computer = computer_item;
1✔
412
                }
413
                if (computer_name.contains("Event Count")) {
3✔
414
                    count_computer = computer_item;
1✔
415
                }
416
            }
3✔
417
        }
418
        
419
        REQUIRE(presence_computer != nullptr);
1✔
420
        REQUIRE(count_computer != nullptr);
1✔
421
        
422
        // Enable the computers
423
        presence_computer->setCheckState(1, Qt::Checked);
1✔
424
        count_computer->setCheckState(1, Qt::Checked);
1✔
425
        
426
        // Get enabled column infos
427
        auto column_infos = widget.getEnabledColumnInfos();
1✔
428
        
429
        REQUIRE(column_infos.size() == 2);
1✔
430
        
431
        // Verify column infos have correct properties
432
        bool has_presence_column = false;
1✔
433
        bool has_count_column = false;
1✔
434
        
435
        for (const auto& info : column_infos) {
3✔
436
            if (info.computerName.find("Event Presence") != std::string::npos) {
2✔
437
                has_presence_column = true;
1✔
438
                REQUIRE(info.outputTypeName == "bool");
1✔
439
                REQUIRE(!info.isVectorType);
1✔
440
            }
441
            if (info.computerName.find("Event Count") != std::string::npos) {
2✔
442
                has_count_column = true;
1✔
443
                REQUIRE(info.outputTypeName == "int");
1✔
444
                REQUIRE(!info.isVectorType);
1✔
445
            }
446
        }
447
        
448
        REQUIRE(has_presence_column);
1✔
449
        REQUIRE(has_count_column);
1✔
450
    }
3✔
451
    
452
    SECTION("Custom column names are preserved") {
2✔
453
        TableDesignerWidget widget(getDataManagerPtr());
1✔
454
        
455
        auto* tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
456
        REQUIRE(tree != nullptr);
1✔
457
        
458
        // Find a computer item
459
        QTreeWidgetItem* computer_item = nullptr;
1✔
460
        for (int i = 0; i < tree->topLevelItemCount() && !computer_item; ++i) {
2✔
461
            auto* data_source_item = tree->topLevelItem(i);
1✔
462
            if (data_source_item->childCount() > 0) {
1✔
463
                computer_item = data_source_item->child(0);
1✔
464
            }
465
        }
466
        
467
        REQUIRE(computer_item != nullptr);
1✔
468
        
469
        // Set custom column name
470
        QString custom_name = "MyCustomColumnName";
1✔
471
        computer_item->setText(2, custom_name);
1✔
472
        computer_item->setCheckState(1, Qt::Checked);
1✔
473
        
474
        // Get enabled column infos
475
        auto column_infos = widget.getEnabledColumnInfos();
1✔
476
        
477
        REQUIRE(column_infos.size() == 1);
1✔
478
        REQUIRE(QString::fromStdString(column_infos[0].name) == custom_name);
1✔
479
    }
3✔
480
}
2✔
481
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - JSON widget updates after enabling computer", "[TableDesignerWidget][JSON]") {
1✔
482
    TableDesignerWidget widget(getDataManagerPtr());
1✔
483

484
    // Create a table and select it
485
    auto & registry = getTableRegistry();
1✔
486
    auto table_id = registry.generateUniqueTableId("JsonTable");
3✔
487
    REQUIRE(registry.createTable(table_id, "JSON Table"));
5✔
488
    auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
489
    REQUIRE(table_combo != nullptr);
1✔
490
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
491
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) { table_combo->setCurrentIndex(i); break; }
1✔
492
    }
493

494
    // Select intervals row source
495
    auto * row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
496
    REQUIRE(row_combo != nullptr);
1✔
497
    int interval_index = -1;
1✔
498
    for (int i = 0; i < row_combo->count(); ++i) {
7✔
499
        if (row_combo->itemText(i).contains("Intervals: BehaviorPeriods")) { interval_index = i; break; }
7✔
500
    }
501
    REQUIRE(interval_index >= 0);
1✔
502
    row_combo->setCurrentIndex(interval_index);
1✔
503

504
    // Enable one event computer under Neuron1Spikes
505
    auto * tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
506
    REQUIRE(tree != nullptr);
1✔
507
    QTreeWidgetItem * event_source_item = nullptr;
1✔
508
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
4✔
509
        auto * item = tree->topLevelItem(i);
4✔
510
        if (item->text(0).contains("Events: Neuron1Spikes")) { event_source_item = item; break; }
4✔
511
    }
512
    REQUIRE(event_source_item != nullptr);
1✔
513
    QTreeWidgetItem * presence = nullptr;
1✔
514
    for (int j = 0; j < event_source_item->childCount(); ++j) {
1✔
515
        auto * c = event_source_item->child(j);
1✔
516
        if (c->text(0).contains("Event Presence")) { presence = c; break; }
1✔
517
    }
518
    REQUIRE(presence != nullptr);
1✔
519
    presence->setCheckState(1, Qt::Checked);
1✔
520

521
    // Build table which should populate JSON widget from current state
522
    REQUIRE(widget.buildTableFromTree());
1✔
523

524
    // Find JSON widget's editor by object name from ui
525
    auto * json_text = widget.findChild<QTextEdit*>("json_text_edit");
2✔
526
    REQUIRE(json_text != nullptr);
1✔
527
    auto text = json_text->toPlainText();
1✔
528
    REQUIRE(text.contains("\"columns\""));
1✔
529
    REQUIRE(text.contains("Neuron1Spikes"));
1✔
530
    REQUIRE(text.contains("Event Presence"));
1✔
531

532
    // Create a new empty table and populate it from the JSON text editor
533
    auto table_id2 = registry.generateUniqueTableId("JsonTable2");
3✔
534
    REQUIRE(registry.createTable(table_id2, "JSON Table 2"));
5✔
535

536
    // Select the new table
537
    table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
538
    REQUIRE(table_combo != nullptr);
1✔
539
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
540
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id2)) { table_combo->setCurrentIndex(i); break; }
1✔
541
    }
542

543
    // Ensure row/computers cleared for new table
544
    auto * row_combo2 = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
545
    REQUIRE(row_combo2 != nullptr);
1✔
546
    auto * tree2 = widget.findChild<QTreeWidget*>("computers_tree");
2✔
547
    REQUIRE(tree2 != nullptr);
1✔
548

549
    // Apply previously captured JSON to new table
550
    json_text->setPlainText(text);
1✔
551
    auto * apply_btn = widget.findChild<QPushButton*>("apply_json_btn");
2✔
552
    REQUIRE(apply_btn != nullptr);
1✔
553
    apply_btn->click();
1✔
554

555
    // Verify row selector got populated
556
    REQUIRE(row_combo2->currentText().contains("Intervals: BehaviorPeriods"));
1✔
557

558
    // Verify the expected computer is enabled under the data source
559
    QTreeWidgetItem * n1_item = nullptr;
1✔
560
    for (int i = 0; i < tree2->topLevelItemCount(); ++i) {
4✔
561
        auto * item = tree2->topLevelItem(i);
4✔
562
        if (item->text(0).contains("Events: Neuron1Spikes")) { n1_item = item; break; }
4✔
563
    }
564
    REQUIRE(n1_item != nullptr);
1✔
565
    bool presence_enabled = false;
1✔
566
    for (int j = 0; j < n1_item->childCount(); ++j) {
1✔
567
        auto * c = n1_item->child(j);
1✔
568
        if (c->text(0).contains("Event Presence")) {
1✔
569
            presence_enabled = (c->checkState(1) == Qt::Checked);
1✔
570
            break;
1✔
571
        }
572
    }
573
    REQUIRE(presence_enabled);
1✔
574

575
    // Verify enabled columns reflect JSON
576
    auto column_infos2 = widget.getEnabledColumnInfos();
1✔
577
    REQUIRE(column_infos2.size() >= 1);
1✔
578
    bool found_presence = false;
1✔
579
    for (auto const & ci : column_infos2) {
1✔
580
        if (ci.computerName.find("Event Presence") != std::string::npos && ci.dataSourceName.find("Neuron1Spikes") != std::string::npos) {
1✔
581
            found_presence = true;
1✔
582
            break;
1✔
583
        }
584
    }
585
    REQUIRE(found_presence);
1✔
586

587
    // Building from populated UI should succeed
588
    REQUIRE(widget.buildTableFromTree());
1✔
589

590
    // Now, save the JSON to a temporary file and load it using the widget's Load JSON button
591
    QTemporaryFile tmpFile(QDir::temp().filePath("table_json_XXXXXX.json"));
1✔
592
    REQUIRE(tmpFile.open());
1✔
593
    QByteArray jsonBytes = text.toUtf8();
1✔
594
    REQUIRE(tmpFile.write(jsonBytes) == jsonBytes.size());
1✔
595
    tmpFile.flush();
1✔
596

597
    // Create a third table and select it
598
    auto table_id3 = registry.generateUniqueTableId("JsonTable3");
3✔
599
    REQUIRE(registry.createTable(table_id3, "JSON Table 3"));
5✔
600
    for (int i = 0; i < table_combo->count(); ++i) {
2✔
601
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id3)) { table_combo->setCurrentIndex(i); break; }
2✔
602
    }
603

604
    // Find the JSON widget's load button and force path
605
    auto * json_widget_load_btn = widget.findChild<QPushButton*>("load_json_btn");
2✔
606
    REQUIRE(json_widget_load_btn != nullptr);
1✔
607
    auto * json_widget = widget.findChild<TableJSONWidget*>();
1✔
608
    REQUIRE(json_widget != nullptr);
1✔
609
    json_widget->setForcedLoadPathForTests(tmpFile.fileName());
1✔
610

611
    // Click Load JSON to read from the temporary file
612
    json_widget_load_btn->click();
1✔
613

614
    // After loading, click Update Table to apply
615
    apply_btn->click();
1✔
616

617
    // Verify that the selection and computer enabling have been applied again
618
    REQUIRE(row_combo2->currentText().contains("Intervals: BehaviorPeriods"));
1✔
619
    n1_item = nullptr;
1✔
620
    for (int i = 0; i < tree2->topLevelItemCount(); ++i) {
4✔
621
        auto * item = tree2->topLevelItem(i);
4✔
622
        if (item->text(0).contains("Events: Neuron1Spikes")) { n1_item = item; break; }
4✔
623
    }
624
    REQUIRE(n1_item != nullptr);
1✔
625
    presence_enabled = false;
1✔
626
    for (int j = 0; j < n1_item->childCount(); ++j) {
1✔
627
        auto * c = n1_item->child(j);
1✔
628
        if (c->text(0).contains("Event Presence")) { presence_enabled = (c->checkState(1) == Qt::Checked); break; }
1✔
629
    }
630
    REQUIRE(presence_enabled);
1✔
631
}
2✔
632

633
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Invalid JSON shows error with line/column", "[TableDesignerWidget][JSON][Error]") {
1✔
634
    TableDesignerWidget widget(getDataManagerPtr());
1✔
635

636
    // Select a valid table to enable UI elements
637
    auto & registry = getTableRegistry();
1✔
638
    auto table_id = registry.generateUniqueTableId("InvalidJson");
3✔
639
    REQUIRE(registry.createTable(table_id, "Invalid JSON Test"));
5✔
640
    auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
641
    REQUIRE(table_combo != nullptr);
1✔
642
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
643
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) { table_combo->setCurrentIndex(i); break; }
1✔
644
    }
645

646
    // Provide invalid JSON with a known error position (missing comma)
647
    QString bad_json = R"({
1✔
648
  "tables": [
649
    {
650
      "table_id": "t1",
651
      "name": "Bad",
652
      "row_selector": { "type": "interval", "source": "BehaviorPeriods" }
653
      "columns": []
654
    }
655
  ]
656
})";
657

658
    // Put into editor
659
    auto * json_text = widget.findChild<QTextEdit*>("json_text_edit");
2✔
660
    REQUIRE(json_text != nullptr);
1✔
661
    json_text->setPlainText(bad_json);
1✔
662

663
    // Click Update Table to trigger parsing
664
    auto * apply_btn = widget.findChild<QPushButton*>("apply_json_btn");
2✔
665
    REQUIRE(apply_btn != nullptr);
1✔
666

667
    // Intercept message boxes by tracking active widgets before/after
668
    QList<QWidget*> before = QApplication::topLevelWidgets();
1✔
669
    apply_btn->click();
1✔
670
    QApplication::processEvents();
1✔
671
    QList<QWidget*> after = QApplication::topLevelWidgets();
1✔
672

673
    // Find a newly created QMessageBox with expected title/text
674
    QMessageBox * found = nullptr;
1✔
675
    for (QWidget * w : after) {
5✔
676
        if (!before.contains(w)) {
5✔
677
            auto * box = qobject_cast<QMessageBox*>(w);
1✔
678
            if (box && box->windowTitle() == "Invalid JSON") {
1✔
679
                found = box;
1✔
680
                break;
1✔
681
            }
682
        }
683
    }
684
    REQUIRE(found != nullptr);
1✔
685
    QString text = found->text();
1✔
686
    REQUIRE(text.contains("JSON format is invalid"));
1✔
687
    REQUIRE(text.contains("line"));
1✔
688
    REQUIRE(text.contains("column"));
1✔
689
}
2✔
690

691
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Valid JSON with missing keys reports errors", "[TableDesignerWidget][JSON][Error]") {
1✔
692
    TableDesignerWidget widget(getDataManagerPtr());
1✔
693
    auto & registry = getTableRegistry();
1✔
694
    auto table_id = registry.generateUniqueTableId("BadKeys");
3✔
695
    REQUIRE(registry.createTable(table_id, "Bad Keys"));
5✔
696
    auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
697
    REQUIRE(table_combo != nullptr);
1✔
698
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
699
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) { table_combo->setCurrentIndex(i); break; }
1✔
700
    }
701
    // Missing row_selector entirely
702
    QString json1 = R"({ "tables": [ { "columns": [ { "name": "c1", "data_source": "Neuron1Spikes", "computer": "Event Presence" } ] } ] })";
1✔
703
    auto * json_text = widget.findChild<QTextEdit*>("json_text_edit");
2✔
704
    REQUIRE(json_text != nullptr);
1✔
705
    json_text->setPlainText(json1);
1✔
706
    auto * apply_btn = widget.findChild<QPushButton*>("apply_json_btn");
2✔
707
    REQUIRE(apply_btn != nullptr);
1✔
708
    QList<QWidget*> before = QApplication::topLevelWidgets();
1✔
709
    apply_btn->click();
1✔
710
    QApplication::processEvents();
1✔
711
    QList<QWidget*> after = QApplication::topLevelWidgets();
1✔
712
    bool saw_error = false;
1✔
713
    for (QWidget * w : after) {
5✔
714
        if (!before.contains(w)) {
5✔
715
            if (auto * box = qobject_cast<QMessageBox*>(w)) {
1✔
716
                if (box->windowTitle() == "Invalid Table JSON" && box->text().contains("Missing required key: row_selector")) {
1✔
717
                    saw_error = true; break;
1✔
718
                }
719
            }
720
        }
721
    }
722
    REQUIRE(saw_error);
1✔
723
}
2✔
724

725
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Unknown computer reports error", "[TableDesignerWidget][JSON][Error]") {
1✔
726
    TableDesignerWidget widget(getDataManagerPtr());
1✔
727
    auto & registry = getTableRegistry();
1✔
728
    auto table_id = registry.generateUniqueTableId("BadComputer");
3✔
729
    REQUIRE(registry.createTable(table_id, "Bad Computer"));
5✔
730
    auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
731
    REQUIRE(table_combo != nullptr);
1✔
732
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
733
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) { table_combo->setCurrentIndex(i); break; }
1✔
734
    }
735
    QString json = R"({
1✔
736
      "tables": [
737
        {
738
          "row_selector": { "type": "interval", "source": "BehaviorPeriods" },
739
          "columns": [ { "name": "c1", "data_source": "Neuron1Spikes", "computer": "Does Not Exist" } ]
740
        }
741
      ]
742
    })";
743
    auto * json_text = widget.findChild<QTextEdit*>("json_text_edit");
2✔
744
    REQUIRE(json_text != nullptr);
1✔
745
    json_text->setPlainText(json);
1✔
746
    auto * apply_btn = widget.findChild<QPushButton*>("apply_json_btn");
2✔
747
    REQUIRE(apply_btn != nullptr);
1✔
748
    QList<QWidget*> before = QApplication::topLevelWidgets();
1✔
749
    apply_btn->click();
1✔
750
    QApplication::processEvents();
1✔
751
    QList<QWidget*> after = QApplication::topLevelWidgets();
1✔
752
    bool saw_error = false;
1✔
753
    for (QWidget * w : after) {
11✔
754
        if (!before.contains(w)) {
11✔
755
            if (auto * box = qobject_cast<QMessageBox*>(w)) {
5✔
756
                if (box->windowTitle() == "Invalid Table JSON" && box->text().contains("requested computer does not exist", Qt::CaseInsensitive)) { saw_error = true; break; }
1✔
757
            }
758
        }
759
    }
760
    REQUIRE(saw_error);
1✔
761
}
2✔
762

763
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Computer incompatible with data type reports error", "[TableDesignerWidget][JSON][Error]") {
1✔
764
    TableDesignerWidget widget(getDataManagerPtr());
1✔
765
    auto & registry = getTableRegistry();
1✔
766
    auto table_id = registry.generateUniqueTableId("BadCompat");
3✔
767
    REQUIRE(registry.createTable(table_id, "Bad Compat"));
5✔
768
    auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
769
    REQUIRE(table_combo != nullptr);
1✔
770
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
771
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) { table_combo->setCurrentIndex(i); break; }
1✔
772
    }
773
    // Use an analog-only computer on an Events source
774
    QString json = R"({
1✔
775
      "tables": [
776
        {
777
          "row_selector": { "type": "interval", "source": "BehaviorPeriods" },
778
          "columns": [ { "name": "c1", "data_source": "Neuron1Spikes", "computer": "Analog Mean" } ]
779
        }
780
      ]
781
    })";
782
    auto * json_text = widget.findChild<QTextEdit*>("json_text_edit");
2✔
783
    REQUIRE(json_text != nullptr);
1✔
784
    json_text->setPlainText(json);
1✔
785
    auto * apply_btn = widget.findChild<QPushButton*>("apply_json_btn");
2✔
786
    REQUIRE(apply_btn != nullptr);
1✔
787
    QList<QWidget*> before = QApplication::topLevelWidgets();
1✔
788
    apply_btn->click();
1✔
789
    QApplication::processEvents();
1✔
790
    QList<QWidget*> after = QApplication::topLevelWidgets();
1✔
791
    bool saw_error = false;
1✔
792
    for (QWidget * w : after) {
8✔
793
        if (!before.contains(w)) {
8✔
794
            if (auto * box = qobject_cast<QMessageBox*>(w)) {
4✔
795
                if (box->windowTitle() == "Invalid Table JSON" && box->text().contains("not valid for data source type", Qt::CaseInsensitive)) { saw_error = true; break; }
1✔
796
            }
797
        }
798
    }
799
    REQUIRE(saw_error);
1✔
800
}
2✔
801

802
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Data key not in DataManager reports error", "[TableDesignerWidget][JSON][Error]") {
1✔
803
    TableDesignerWidget widget(getDataManagerPtr());
1✔
804
    auto & registry = getTableRegistry();
1✔
805
    auto table_id = registry.generateUniqueTableId("BadDataKey");
3✔
806
    REQUIRE(registry.createTable(table_id, "Bad Data Key"));
5✔
807
    auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
808
    REQUIRE(table_combo != nullptr);
1✔
809
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
810
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) { table_combo->setCurrentIndex(i); break; }
1✔
811
    }
812
    QString json = R"({
1✔
813
      "tables": [
814
        {
815
          "row_selector": { "type": "interval", "source": "DoesNotExistIntervals" },
816
          "columns": [ { "name": "c1", "data_source": "DoesNotExistEvents", "computer": "Event Presence" } ]
817
        }
818
      ]
819
    })";
820
    auto * json_text = widget.findChild<QTextEdit*>("json_text_edit");
2✔
821
    REQUIRE(json_text != nullptr);
1✔
822
    json_text->setPlainText(json);
1✔
823
    auto * apply_btn = widget.findChild<QPushButton*>("apply_json_btn");
2✔
824
    REQUIRE(apply_btn != nullptr);
1✔
825
    QList<QWidget*> before = QApplication::topLevelWidgets();
1✔
826
    apply_btn->click();
1✔
827
    QApplication::processEvents();
1✔
828
    QList<QWidget*> after = QApplication::topLevelWidgets();
1✔
829
    bool saw_error = false;
1✔
830
    for (QWidget * w : after) {
3✔
831
        if (!before.contains(w)) {
3✔
832
            if (auto * box = qobject_cast<QMessageBox*>(w)) {
1✔
833
                if (box->windowTitle() == "Invalid Table JSON" && (box->text().contains("not found in DataManager", Qt::CaseInsensitive) || box->text().contains("Row selector data key not found", Qt::CaseInsensitive))) {
1✔
834
                    saw_error = true; break;
1✔
835
                }
836
            }
837
        }
838
    }
839
    REQUIRE(saw_error);
1✔
840
}
2✔
841

842
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - IntervalOverlap AssignID preview builds and values", "[TableDesignerWidget][Preview][IntervalOverlap]") {
1✔
843
    TableDesignerWidget widget(getDataManagerPtr());
1✔
844

845
    // Prepare a longer timeframe 0..300
846
    auto & dm = getDataManager();
1✔
847
    {
848
        std::vector<int> long_time_values(301);
3✔
849
        std::iota(long_time_values.begin(), long_time_values.end(), 0);
1✔
850
        auto long_time = std::make_shared<TimeFrame>(long_time_values);
1✔
851
        dm.setTime(TimeKey("long_time"), long_time, true);
1✔
852
    }
1✔
853

854
    // Row intervals: [10,20], [50,100], [200,300]
855
    auto row_series = std::make_shared<DigitalIntervalSeries>();
1✔
856
    row_series->addEvent(TimeFrameIndex(10), TimeFrameIndex(20));
1✔
857
    row_series->addEvent(TimeFrameIndex(50), TimeFrameIndex(100));
1✔
858
    row_series->addEvent(TimeFrameIndex(200), TimeFrameIndex(300));
1✔
859
    dm.setData<DigitalIntervalSeries>("RowIntervals", row_series, TimeKey("long_time"));
3✔
860

861
    // Column intervals: [0,100], [200,300]
862
    auto col_series = std::make_shared<DigitalIntervalSeries>();
1✔
863
    col_series->addEvent(TimeFrameIndex(0), TimeFrameIndex(100));
1✔
864
    col_series->addEvent(TimeFrameIndex(200), TimeFrameIndex(300));
1✔
865
    dm.setData<DigitalIntervalSeries>("ColumnIntervals", col_series, TimeKey("long_time"));
3✔
866

867
    QApplication::processEvents();
1✔
868

869
    // Create table and select it
870
    auto & registry = getTableRegistry();
1✔
871
    auto table_id = registry.generateUniqueTableId("OverlapTest");
3✔
872
    REQUIRE(registry.createTable(table_id, "Overlap Preview Test"));
5✔
873
    auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
874
    REQUIRE(table_combo != nullptr);
1✔
875
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
876
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) { table_combo->setCurrentIndex(i); break; }
1✔
877
    }
878

879
    // Select row selector: Intervals: RowIntervals
880
    auto * row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
881
    REQUIRE(row_combo != nullptr);
1✔
882
    int row_idx = -1;
1✔
883
    for (int i = 0; i < row_combo->count(); ++i) {
8✔
884
        if (row_combo->itemText(i).contains("Intervals: RowIntervals")) { row_idx = i; break; }
8✔
885
    }
886
    REQUIRE(row_idx >= 0);
1✔
887
    row_combo->setCurrentIndex(row_idx);
1✔
888

889
    // Use the intervals themselves (no capture range expansion)
890
    auto * interval_itself = widget.findChild<QRadioButton*>("interval_itself_radio");
2✔
891
    REQUIRE(interval_itself != nullptr);
1✔
892
    interval_itself->setChecked(true);
1✔
893

894
    // Enable Interval Overlap Assign ID under Intervals: ColumnIntervals
895
    auto * tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
896
    REQUIRE(tree != nullptr);
1✔
897
    QTreeWidgetItem * col_item = nullptr;
1✔
898
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
2✔
899
        auto * item = tree->topLevelItem(i);
2✔
900
        if (item->text(0).contains("Intervals: ColumnIntervals")) { col_item = item; break; }
2✔
901
    }
902
    REQUIRE(col_item != nullptr);
1✔
903
    QTreeWidgetItem * assign_id = nullptr;
1✔
904
    for (int j = 0; j < col_item->childCount(); ++j) {
4✔
905
        auto * c = col_item->child(j);
4✔
906
        if (c->text(0) == "Interval Overlap Assign ID") { assign_id = c; break; }
4✔
907
    }
908
    REQUIRE(assign_id != nullptr);
1✔
909
    assign_id->setCheckState(1, Qt::Checked);
1✔
910

911
    // Find preview table
912
    auto * table_view_widget = widget.findChild<QTableView*>();
1✔
913
    REQUIRE(table_view_widget != nullptr);
1✔
914
    QApplication::processEvents();
1✔
915
    auto * model = table_view_widget->model();
1✔
916
    REQUIRE(model != nullptr);
1✔
917

918
    // Expect 3 rows, 1 column
919
    REQUIRE(model->rowCount() == 3);
1✔
920
    REQUIRE(model->columnCount() >= 1);
1✔
921

922
    // Values should be 0, 0, 1 for AssignID mapping
923
    QVariant v0 = model->index(0, 0).data(Qt::DisplayRole);
1✔
924
    QVariant v1 = model->index(1, 0).data(Qt::DisplayRole);
1✔
925
    QVariant v2 = model->index(2, 0).data(Qt::DisplayRole);
1✔
926
    std::cout << "v0: " << v0.toString().toStdString() << std::endl;
1✔
927
    std::cout << "v1: " << v1.toString().toStdString() << std::endl;
1✔
928
    std::cout << "v2: " << v2.toString().toStdString() << std::endl;
1✔
929
    REQUIRE(v0.isValid()); REQUIRE(v1.isValid()); REQUIRE(v2.isValid());
3✔
930
    REQUIRE(v0.toLongLong() == 0);
1✔
931
    REQUIRE(v1.toLongLong() == 0);
1✔
932
    REQUIRE(v2.toLongLong() == 1);
1✔
933
}
2✔
934

935
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Table creation workflow", "[TableDesignerWidget][Workflow]") {
1✔
936
    
937
    SECTION("Complete workflow - create table, enable computers, build table") {
1✔
938
        TableDesignerWidget widget(getDataManagerPtr());
1✔
939
        
940
        // Create a new table first
941
        auto& registry = getTableRegistry();
1✔
942
        auto table_id = registry.generateUniqueTableId("TestTable");
3✔
943
        REQUIRE(registry.createTable(table_id, "Test Table for Workflow"));
5✔
944
        
945
        // Set the table in the widget (simulate user selection)
946
        auto* table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
947
        REQUIRE(table_combo != nullptr);
1✔
948
        
949
        // Find the table in the combo and select it
950
        for (int i = 0; i < table_combo->count(); ++i) {
1✔
951
            if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) {
1✔
952
                table_combo->setCurrentIndex(i);
1✔
953
                break;
1✔
954
            }
955
        }
956
        
957
        // Set row data source (simulate user selection)
958
        auto* row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
959
        REQUIRE(row_combo != nullptr);
1✔
960
        
961
        // Find and select an interval source for rows
962
        for (int i = 0; i < row_combo->count(); ++i) {
7✔
963
            if (row_combo->itemText(i).contains("Intervals: BehaviorPeriods")) {
7✔
964
                row_combo->setCurrentIndex(i);
1✔
965
                break;
1✔
966
            }
967
        }
968
        
969
        // Enable some computers in the tree
970
        auto* tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
971
        REQUIRE(tree != nullptr);
1✔
972
        
973
        int enabled_computers = 0;
1✔
974
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
6✔
975
            auto* data_source_item = tree->topLevelItem(i);
5✔
976
            
977
            for (int j = 0; j < data_source_item->childCount() && enabled_computers < 3; ++j) {
8✔
978
                auto* computer_item = data_source_item->child(j);
3✔
979
                computer_item->setCheckState(1, Qt::Checked);
3✔
980
                enabled_computers++;
3✔
981
            }
982
        }
983
        
984
        REQUIRE(enabled_computers > 0);
1✔
985
        
986
        // Verify column infos are generated
987
        auto column_infos = widget.getEnabledColumnInfos();
1✔
988
        REQUIRE(column_infos.size() == enabled_computers);
1✔
989
        
990
        // Build the table
991
        bool build_success = widget.buildTableFromTree();
1✔
992
        
993
        // Verify the table was built and stored
994
        auto built_table = registry.getBuiltTable(table_id);
1✔
995
        REQUIRE(built_table != nullptr);
1✔
996
        REQUIRE(built_table->getColumnCount() == column_infos.size());
1✔
997
    }
2✔
998
}
1✔
999

1000
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Preview updates when columns enabled", "[TableDesignerWidget][Preview]") {
1✔
1001
    TableDesignerWidget widget(getDataManagerPtr());
1✔
1002

1003
    // Create a new table and select it
1004
    auto & registry = getTableRegistry();
1✔
1005
    auto table_id = registry.generateUniqueTableId("PreviewTable");
3✔
1006
    REQUIRE(registry.createTable(table_id, "Preview Table"));
5✔
1007

1008
    auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
1009
    REQUIRE(table_combo != nullptr);
1✔
1010
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
1011
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) {
1✔
1012
            table_combo->setCurrentIndex(i);
1✔
1013
            break;
1✔
1014
        }
1015
    }
1016

1017
    // Select intervals row source
1018
    auto * row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
1019
    REQUIRE(row_combo != nullptr);
1✔
1020
    int interval_index = -1;
1✔
1021
    for (int i = 0; i < row_combo->count(); ++i) {
7✔
1022
        if (row_combo->itemText(i).contains("Intervals: BehaviorPeriods")) { interval_index = i; break; }
7✔
1023
    }
1024
    REQUIRE(interval_index >= 0);
1✔
1025
    row_combo->setCurrentIndex(interval_index);
1✔
1026

1027
    // Enable two event computers under Neuron1Spikes
1028
    auto * tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1029
    REQUIRE(tree != nullptr);
1✔
1030

1031
    QTreeWidgetItem * event_source_item = nullptr;
1✔
1032
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
4✔
1033
        auto * item = tree->topLevelItem(i);
4✔
1034
        if (item->text(0).contains("Events: Neuron1Spikes")) { event_source_item = item; break; }
4✔
1035
    }
1036
    REQUIRE(event_source_item != nullptr);
1✔
1037
    REQUIRE(event_source_item->childCount() > 0);
1✔
1038

1039
    QTreeWidgetItem * presence_computer = nullptr;
1✔
1040
    QTreeWidgetItem * count_computer = nullptr;
1✔
1041
    for (int j = 0; j < event_source_item->childCount(); ++j) {
4✔
1042
        auto * computer_item = event_source_item->child(j);
3✔
1043
        auto name = computer_item->text(0);
3✔
1044
        if (name.contains("Event Presence")) presence_computer = computer_item;
3✔
1045
        if (name.contains("Event Count")) count_computer = computer_item;
3✔
1046
    }
3✔
1047
    REQUIRE(presence_computer != nullptr);
1✔
1048
    REQUIRE(count_computer != nullptr);
1✔
1049

1050
    presence_computer->setCheckState(1, Qt::Checked);
1✔
1051
    count_computer->setCheckState(1, Qt::Checked);
1✔
1052

1053
    // Find the embedded TableView and its model
1054
    auto * table_view_widget = widget.findChild<QTableView*>();
1✔
1055
    REQUIRE(table_view_widget != nullptr);
1✔
1056
    auto * model = table_view_widget->model();
1✔
1057
    REQUIRE(model != nullptr);
1✔
1058

1059
    // Expect 4 behavior intervals as rows, and 2 enabled columns
1060
    REQUIRE(model->rowCount() == 4);
1✔
1061
    REQUIRE(model->columnCount() == 2);
1✔
1062
}
2✔
1063

1064
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Observes DataManager and updates tree on add/remove", "[TableDesignerWidget][Observer]") {
1✔
1065
    TableDesignerWidget widget(getDataManagerPtr());
1✔
1066

1067
    // Select an interval-based row source so event sources/computers are visible
1068
    auto * row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
1069
    REQUIRE(row_combo != nullptr);
1✔
1070
    int intervals_index = -1;
1✔
1071
    for (int i = 0; i < row_combo->count(); ++i) {
7✔
1072
        if (row_combo->itemText(i).startsWith("Intervals: ") && row_combo->itemText(i).contains("BehaviorPeriods")) {
7✔
1073
            intervals_index = i;
1✔
1074
            break;
1✔
1075
        }
1076
    }
1077
    REQUIRE(intervals_index >= 0);
1✔
1078
    row_combo->setCurrentIndex(intervals_index);
1✔
1079
    QApplication::processEvents();
1✔
1080

1081
    auto * tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1082
    REQUIRE(tree != nullptr);
1✔
1083

1084
    // Initially, no "Events: NewSpikes" source
1085
    bool found_new = false;
1✔
1086
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
6✔
1087
        if (tree->topLevelItem(i)->text(0).contains("Events: NewSpikes")) { found_new = true; break; }
5✔
1088
    }
1089
    REQUIRE(!found_new);
1✔
1090

1091
    // Enable a computer under existing Neuron1Spikes to test state preservation
1092
    QTreeWidgetItem * n1_item = nullptr;
1✔
1093
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
4✔
1094
        auto * item = tree->topLevelItem(i);
4✔
1095
        if (item->text(0).contains("Events: Neuron1Spikes")) { n1_item = item; break; }
4✔
1096
    }
1097
    REQUIRE(n1_item != nullptr);
1✔
1098
    QTreeWidgetItem * presence = nullptr;
1✔
1099
    for (int j = 0; j < n1_item->childCount(); ++j) {
1✔
1100
        auto * c = n1_item->child(j);
1✔
1101
        if (c->text(0).contains("Event Presence")) { presence = c; break; }
1✔
1102
    }
1103
    REQUIRE(presence != nullptr);
1✔
1104
    presence->setCheckState(1, Qt::Checked);
1✔
1105

1106
    // Add a new event key to DataManager and ensure tree updates
1107
    auto & dm = getDataManager();
1✔
1108
    std::vector<float> spikes = {1.0f, 2.0f, 3.0f};
3✔
1109
    auto series = std::make_shared<DigitalEventSeries>(spikes);
1✔
1110
    dm.setData<DigitalEventSeries>("NewSpikes", series, TimeKey("spike_time"));
3✔
1111

1112
    // Process events to allow observer callback to run
1113
    QApplication::processEvents();
1✔
1114

1115
    // Tree should now include the new source
1116
    // Refresh pointer (not strictly necessary but clearer)
1117
    tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1118
    REQUIRE(tree != nullptr);
1✔
1119
    bool found_after = false;
1✔
1120
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
6✔
1121
        if (tree->topLevelItem(i)->text(0).contains("Events: NewSpikes")) { found_after = true; break; }
6✔
1122
    }
1123
    REQUIRE(found_after);
1✔
1124

1125
    // State for previously enabled computer should be preserved
1126
    // Re-find Neuron1Spikes item
1127
    n1_item = nullptr;
1✔
1128
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
4✔
1129
        auto * item = tree->topLevelItem(i);
4✔
1130
        if (item->text(0).contains("Events: Neuron1Spikes")) { n1_item = item; break; }
4✔
1131
    }
1132
    REQUIRE(n1_item != nullptr);
1✔
1133
    bool still_checked = false;
1✔
1134
    for (int j = 0; j < n1_item->childCount(); ++j) {
1✔
1135
        auto * c = n1_item->child(j);
1✔
1136
        if (c->text(0).contains("Event Presence")) { still_checked = (c->checkState(1) == Qt::Checked); break; }
1✔
1137
    }
1138
    REQUIRE(still_checked);
1✔
1139

1140
    // Enable a computer under the new source to test removal cleanup
1141
    QTreeWidgetItem * new_item = nullptr;
1✔
1142
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
6✔
1143
        auto * item = tree->topLevelItem(i);
6✔
1144
        if (item->text(0).contains("Events: NewSpikes")) { new_item = item; break; }
6✔
1145
    }
1146
    REQUIRE(new_item != nullptr);
1✔
1147
    int new_checked_before = 0;
1✔
1148
    for (int j = 0; j < new_item->childCount(); ++j) {
1✔
1149
        auto * c = new_item->child(j);
1✔
1150
        if (c->text(0).contains("Event Presence") || c->text(0).contains("Event Count")) {
1✔
1151
            c->setCheckState(1, Qt::Checked);
1✔
1152
            ++new_checked_before;
1✔
1153
            break; // enable one
1✔
1154
        }
1155
    }
1156
    REQUIRE(new_checked_before >= 1);
1✔
1157

1158
    // Now remove the new data source and ensure its entry disappears
1159
    REQUIRE(dm.deleteData("NewSpikes"));
3✔
1160
    QApplication::processEvents();
1✔
1161

1162
    bool removed = true;
1✔
1163
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
6✔
1164
        if (tree->topLevelItem(i)->text(0).contains("Events: NewSpikes")) { removed = false; break; }
5✔
1165
    }
1166
    REQUIRE(removed);
1✔
1167

1168
    // Also verify enabled columns no longer include NewSpikes
1169
    auto enabled_columns = widget.getEnabledColumnInfos();
1✔
1170
    for (auto const & info : enabled_columns) {
2✔
1171
        REQUIRE(std::string(info.dataSourceName).find("NewSpikes") == std::string::npos);
1✔
1172
    }
1173
}
2✔
1174

1175
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Drag-reorder columns updates visual order", "[TableDesignerWidget][Reorder]") {
1✔
1176
    TableDesignerWidget widget(getDataManagerPtr());
1✔
1177

1178
    // Create a table and select it
1179
    auto & registry = getTableRegistry();
1✔
1180
    auto table_id = registry.generateUniqueTableId("Reorder");
3✔
1181
    REQUIRE(registry.createTable(table_id, "Reorder Table"));
5✔
1182
    auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
1183
    REQUIRE(table_combo != nullptr);
1✔
1184
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
1185
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) { table_combo->setCurrentIndex(i); break; }
1✔
1186
    }
1187

1188
    // Select intervals as rows and enable two computers under Neuron1Spikes
1189
    auto * row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
1190
    REQUIRE(row_combo != nullptr);
1✔
1191
    for (int i = 0; i < row_combo->count(); ++i) {
7✔
1192
        if (row_combo->itemText(i).contains("Intervals: BehaviorPeriods")) { row_combo->setCurrentIndex(i); break; }
7✔
1193
    }
1194
    auto * tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1195
    REQUIRE(tree != nullptr);
1✔
1196
    QTreeWidgetItem * n1 = nullptr;
1✔
1197
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
4✔
1198
        if (tree->topLevelItem(i)->text(0).contains("Events: Neuron1Spikes")) { n1 = tree->topLevelItem(i); break; }
4✔
1199
    }
1200
    REQUIRE(n1 != nullptr);
1✔
1201
    QTreeWidgetItem * presence = nullptr; QTreeWidgetItem * count = nullptr;
1✔
1202
    for (int j = 0; j < n1->childCount(); ++j) {
4✔
1203
        auto * c = n1->child(j);
3✔
1204
        if (c->text(0).contains("Event Presence")) presence = c;
3✔
1205
        if (c->text(0).contains("Event Count")) count = c;
3✔
1206
    }
1207
    REQUIRE(presence != nullptr);
1✔
1208
    REQUIRE(count != nullptr);
1✔
1209
    presence->setCheckState(1, Qt::Checked);
1✔
1210
    count->setCheckState(1, Qt::Checked);
1✔
1211

1212
    // Get the embedded preview table and reorder columns visually
1213
    auto * tv = widget.findChild<QTableView*>();
1✔
1214
    REQUIRE(tv != nullptr);
1✔
1215
    auto * header = tv->horizontalHeader();
1✔
1216
    REQUIRE(header != nullptr);
1✔
1217

1218
    // Expect initial order [Presence, Count] (order determined by tree population)
1219
    int col0 = header->logicalIndex(0);
1✔
1220
    int col1 = header->logicalIndex(1);
1✔
1221
    REQUIRE(col0 != col1);
1✔
1222

1223
    // Move second column to the first position
1224
    header->moveSection(1, 0);
1✔
1225
    QApplication::processEvents();
1✔
1226

1227
    // Verify visual order changed
1228
    REQUIRE(header->visualIndex(col1) == 0);
1✔
1229
    REQUIRE(header->visualIndex(col0) == 1);
1✔
1230
}
2✔
1231

1232
/*
1233
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Data source compatibility", "[TableDesignerWidget][Compatibility]") {
1234
    
1235
    SECTION("Event sources should have event computers") {
1236
        TableDesignerWidget widget(getDataManagerPtr());
1237
        
1238
        // Test computer compatibility
1239
        REQUIRE(widget.isComputerCompatibleWithDataSource("Event Presence", "Events: Neuron1Spikes"));
1240
        REQUIRE(widget.isComputerCompatibleWithDataSource("Event Count", "Events: Neuron1Spikes"));
1241
        REQUIRE(widget.isComputerCompatibleWithDataSource("Event Gather", "Events: Neuron1Spikes"));
1242
        
1243
        // Event computers should not be compatible with analog sources
1244
        REQUIRE(!widget.isComputerCompatibleWithDataSource("Event Presence", "analog:LFP"));
1245
        REQUIRE(!widget.isComputerCompatibleWithDataSource("Event Count", "analog:LFP"));
1246
    }
1247
    
1248
    SECTION("Analog sources should have analog computers") {
1249
        TableDesignerWidget widget(getDataManagerPtr());
1250
        
1251
        // Test analog computer compatibility
1252
        REQUIRE(widget.isComputerCompatibleWithDataSource("Analog Slice Gatherer", "analog:LFP"));
1253
        REQUIRE(widget.isComputerCompatibleWithDataSource("Analog Mean", "analog:LFP"));
1254
        REQUIRE(widget.isComputerCompatibleWithDataSource("Analog Max", "analog:EMG"));
1255
        
1256
        // Analog computers should not be compatible with event sources
1257
        REQUIRE(!widget.isComputerCompatibleWithDataSource("Analog Mean", "Events: Neuron1Spikes"));
1258
        REQUIRE(!widget.isComputerCompatibleWithDataSource("Analog Slice Gatherer", "Events: Neuron1Spikes"));
1259
    }
1260
    
1261
    SECTION("Default column name generation") {
1262
        TableDesignerWidget widget(getDataManagerPtr());
1263
        
1264
        // Test default name generation
1265
        QString name1 = widget.generateDefaultColumnName("Events: Neuron1Spikes", "Event Presence");
1266
        QString name2 = widget.generateDefaultColumnName("analog:LFP", "Analog Mean");
1267
        QString name3 = widget.generateDefaultColumnName("Intervals: BehaviorPeriods", "Event Count");
1268
        
1269
        REQUIRE(name1 == "Neuron1Spikes_Event Presence");
1270
        REQUIRE(name2 == "LFP_Analog Mean");
1271
        REQUIRE(name3 == "BehaviorPeriods_Event Count");
1272
        
1273
        // Names should be unique for different combinations
1274
    REQUIRE(name1 != name2);
1275
    REQUIRE(name2 != name3);
1276
    REQUIRE(name1 != name3);
1277
    }
1278
}
1279
*/
1280

1281
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - LineData with timestamp row selector shows LineSamplingMultiComputer", "[TableDesignerWidget][LineData][Timestamp]") {
1✔
1282
    TableDesignerWidget widget(getDataManagerPtr());
1✔
1283
    
1284
    // Get the computers tree
1285
    auto* tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1286
    REQUIRE(tree != nullptr);
1✔
1287
    
1288
    // Find the LineData source in the tree
1289
    QTreeWidgetItem* line_source_item = nullptr;
1✔
1290
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
4✔
1291
        auto* item = tree->topLevelItem(i);
4✔
1292
        if (item->text(0).contains("lines:TestLines")) {
4✔
1293
            line_source_item = item;
1✔
1294
            break;
1✔
1295
        }
1296
    }
1297
    
1298
    REQUIRE(line_source_item != nullptr);
1✔
1299
    REQUIRE(line_source_item->childCount() > 0);
1✔
1300
    
1301
    // Check for LineSamplingMultiComputer (registered as "Line Sample XY")
1302
    bool has_line_sampling_computer = false;
1✔
1303
    for (int j = 0; j < line_source_item->childCount(); ++j) {
1✔
1304
        auto* computer_item = line_source_item->child(j);
1✔
1305
        QString computer_name = computer_item->text(0);
1✔
1306
        
1307
        if (computer_name.contains("Line Sample XY")) {
1✔
1308
            has_line_sampling_computer = true;
1✔
1309
            
1310
            // Verify it's a multi-output computer with parameters
1311
            REQUIRE((computer_item->flags() & Qt::ItemIsUserCheckable) != 0);
1✔
1312
            REQUIRE((computer_item->flags() & Qt::ItemIsEditable) != 0);
1✔
1313
            REQUIRE(!computer_item->text(2).isEmpty()); // Should have default column name
1✔
1314
            
1315
            // Check that it has parameter widgets (segments parameter)
1316
            auto* param_widget = tree->itemWidget(computer_item, 3);
1✔
1317
            REQUIRE(param_widget != nullptr);
1✔
1318
            
1319
            break;
1✔
1320
        }
1321
    }
1✔
1322
    
1323
    REQUIRE(has_line_sampling_computer);
1✔
1324
}
2✔
1325

1326
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - LineData computers work with timestamp row selector", "[TableDesignerWidget][LineData][Workflow]") {
1✔
1327
    TableDesignerWidget widget(getDataManagerPtr());
1✔
1328
    
1329
    // Create a new table and select it
1330
    auto& registry = getTableRegistry();
1✔
1331
    auto table_id = registry.generateUniqueTableId("LineDataTest");
3✔
1332
    REQUIRE(registry.createTable(table_id, "Line Data Test Table"));
5✔
1333
    
1334
    auto* table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
1335
    REQUIRE(table_combo != nullptr);
1✔
1336
    for (int i = 0; i < table_combo->count(); ++i) {
1✔
1337
        if (table_combo->itemData(i).toString() == QString::fromStdString(table_id)) {
1✔
1338
            table_combo->setCurrentIndex(i);
1✔
1339
            break;
1✔
1340
        }
1341
    }
1342
    
1343
    // Select TimeFrame as row source (timestamp-based)
1344
    auto* row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
1345
    REQUIRE(row_combo != nullptr);
1✔
1346
    int timeframe_index = -1;
1✔
1347
    for (int i = 0; i < row_combo->count(); ++i) {
3✔
1348
        if (row_combo->itemText(i).contains("TimeFrame: behavior_time")) {
3✔
1349
            timeframe_index = i;
1✔
1350
            break;
1✔
1351
        }
1352
    }
1353
    REQUIRE(timeframe_index >= 0);
1✔
1354
    row_combo->setCurrentIndex(timeframe_index);
1✔
1355
    
1356
    // Enable LineSamplingMultiComputer under TestLines
1357
    auto* tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1358
    REQUIRE(tree != nullptr);
1✔
1359
    
1360
    QTreeWidgetItem* line_source_item = nullptr;
1✔
1361
    for (int i = 0; i < tree->topLevelItemCount(); ++i) {
4✔
1362
        auto* item = tree->topLevelItem(i);
4✔
1363
        if (item->text(0).contains("lines:TestLines")) {
4✔
1364
            line_source_item = item;
1✔
1365
            break;
1✔
1366
        }
1367
    }
1368
    REQUIRE(line_source_item != nullptr);
1✔
1369
    
1370
    QTreeWidgetItem* line_sampling_computer = nullptr;
1✔
1371
    for (int j = 0; j < line_source_item->childCount(); ++j) {
1✔
1372
        auto* computer_item = line_source_item->child(j);
1✔
1373
        if (computer_item->text(0).contains("Line Sample XY")) {
1✔
1374
            line_sampling_computer = computer_item;
1✔
1375
            break;
1✔
1376
        }
1377
    }
1378
    REQUIRE(line_sampling_computer != nullptr);
1✔
1379
    
1380
    // Enable the computer
1381
    line_sampling_computer->setCheckState(1, Qt::Checked);
1✔
1382
    
1383
    // Get enabled column infos
1384
    auto column_infos = widget.getEnabledColumnInfos();
1✔
1385
    REQUIRE(column_infos.size() > 0);
1✔
1386
    
1387
    // Verify that we have LineSamplingMultiComputer columns
1388
    bool has_line_sampling_columns = false;
1✔
1389
    for (const auto& info : column_infos) {
1✔
1390
        if (info.computerName.find("Line Sample XY") != std::string::npos) {
1✔
1391
            has_line_sampling_columns = true;
1✔
1392
            REQUIRE(info.dataSourceName.find("lines:TestLines") != std::string::npos);
1✔
1393
            REQUIRE(info.isVectorType); // LineSamplingMultiComputer is a multi-output computer
1✔
1394
            break;
1✔
1395
        }
1396
    }
1397
    REQUIRE(has_line_sampling_columns);
1✔
1398
    
1399
    // Build the table to verify it works end-to-end
1400
    bool build_success = widget.buildTableFromTree();
1✔
1401
    REQUIRE(build_success);
1✔
1402
    
1403
    // Verify the table was built and stored
1404
    auto built_table = registry.getBuiltTable(table_id);
1✔
1405
    REQUIRE(built_table != nullptr);
1✔
1406
    REQUIRE(built_table->getColumnCount() > 0);
1✔
1407
}
2✔
1408

1409
TEST_CASE_METHOD(TableDesignerWidgetTestFixture, "TableDesignerWidget - Row selector type determines available computers", "[TableDesignerWidget][RowSelector][Compatibility]") {
5✔
1410
    
1411
    SECTION("TimeFrame row selector shows only timestamp-compatible computers for analog sources") {
5✔
1412
        TableDesignerWidget widget(getDataManagerPtr());
1✔
1413
        
1414
        // Create a table
1415
        auto & registry = getTableRegistry();
1✔
1416
        auto table_id = registry.generateUniqueTableId("TimestampTest");
3✔
1417
        REQUIRE(registry.createTable(table_id, "Timestamp Compatibility Test"));
5✔
1418
        
1419
        // Select table
1420
        auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
1421
        REQUIRE(table_combo != nullptr);
1✔
1422
        for (int i = 0; i < table_combo->count(); ++i) {
2✔
1423
            if (table_combo->itemText(i).contains(QString::fromStdString(table_id))) {
1✔
UNCOV
1424
                table_combo->setCurrentIndex(i);
×
UNCOV
1425
                break;
×
1426
            }
1427
        }
1428
        
1429
        // Select TimeFrame as row source
1430
        auto * row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
1431
        REQUIRE(row_combo != nullptr);
1✔
1432
        int timeframe_index = -1;
1✔
1433
        for (int i = 0; i < row_combo->count(); ++i) {
1✔
1434
            if (row_combo->itemText(i).startsWith("TimeFrame: ")) {
1✔
1435
                timeframe_index = i;
1✔
1436
                break;
1✔
1437
            }
1438
        }
1439
        REQUIRE(timeframe_index >= 0);
1✔
1440
        row_combo->setCurrentIndex(timeframe_index);
1✔
1441
        
1442
        // Get the computers tree
1443
        auto * tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1444
        REQUIRE(tree != nullptr);
1✔
1445
        
1446
        // Find analog data source items
1447
        QTreeWidgetItem * analog_source_item = nullptr;
1✔
1448
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
2✔
1449
            auto * item = tree->topLevelItem(i);
2✔
1450
            if (item->text(0).startsWith("analog:")) {
2✔
1451
                analog_source_item = item;
1✔
1452
                break;
1✔
1453
            }
1454
        }
1455
        REQUIRE(analog_source_item != nullptr);
1✔
1456
        
1457
        // Verify only timestamp-compatible computers are shown
1458
        bool has_timestamp_value = false;
1✔
1459
        bool has_timestamp_offsets = false;
1✔
1460
        bool has_interval_mean = false;
1✔
1461
        bool has_interval_max = false;
1✔
1462
        bool has_slice_gatherer = false;
1✔
1463
        
1464
        for (int j = 0; j < analog_source_item->childCount(); ++j) {
3✔
1465
            auto * computer_item = analog_source_item->child(j);
2✔
1466
            QString computer_name = computer_item->text(0);
2✔
1467
            
1468
            if (computer_name.contains("Timestamp Value")) has_timestamp_value = true;
2✔
1469
            if (computer_name.contains("Timestamp Offsets")) has_timestamp_offsets = true;
2✔
1470
            if (computer_name.contains("Interval Mean")) has_interval_mean = true;
2✔
1471
            if (computer_name.contains("Interval Max")) has_interval_max = true;
2✔
1472
            if (computer_name.contains("Slice Gatherer")) has_slice_gatherer = true;
2✔
1473
        }
2✔
1474
        
1475
        // Should have timestamp computers
1476
        REQUIRE(has_timestamp_value);
1✔
1477
        // Should NOT have interval-based computers
1478
        REQUIRE_FALSE(has_interval_mean);
1✔
1479
        REQUIRE_FALSE(has_interval_max);
1✔
1480
        REQUIRE_FALSE(has_slice_gatherer);
1✔
1481
    }
6✔
1482
    
1483
    SECTION("Intervals row selector shows only interval-compatible computers for analog sources") {
5✔
1484
        TableDesignerWidget widget(getDataManagerPtr());
1✔
1485
        
1486
        // Create a table
1487
        auto & registry = getTableRegistry();
1✔
1488
        auto table_id = registry.generateUniqueTableId("IntervalTest");
3✔
1489
        REQUIRE(registry.createTable(table_id, "Interval Compatibility Test"));
5✔
1490
        
1491
        // Select table
1492
        auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
1493
        REQUIRE(table_combo != nullptr);
1✔
1494
        for (int i = 0; i < table_combo->count(); ++i) {
2✔
1495
            if (table_combo->itemText(i).contains(QString::fromStdString(table_id))) {
1✔
UNCOV
1496
                table_combo->setCurrentIndex(i);
×
UNCOV
1497
                break;
×
1498
            }
1499
        }
1500
        
1501
        // Select Intervals as row source
1502
        auto * row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
1503
        REQUIRE(row_combo != nullptr);
1✔
1504
        int intervals_index = -1;
1✔
1505
        for (int i = 0; i < row_combo->count(); ++i) {
7✔
1506
            if (row_combo->itemText(i).startsWith("Intervals: ")) {
7✔
1507
                intervals_index = i;
1✔
1508
                break;
1✔
1509
            }
1510
        }
1511
        REQUIRE(intervals_index >= 0);
1✔
1512
        row_combo->setCurrentIndex(intervals_index);
1✔
1513
        
1514
        // Get the computers tree
1515
        auto * tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1516
        REQUIRE(tree != nullptr);
1✔
1517
        
1518
        // Find analog data source items
1519
        QTreeWidgetItem * analog_source_item = nullptr;
1✔
1520
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
2✔
1521
            auto * item = tree->topLevelItem(i);
2✔
1522
            if (item->text(0).startsWith("analog:")) {
2✔
1523
                analog_source_item = item;
1✔
1524
                break;
1✔
1525
            }
1526
        }
1527
        REQUIRE(analog_source_item != nullptr);
1✔
1528
        
1529
        // Verify only interval-compatible computers are shown
1530
        bool has_timestamp_value = false;
1✔
1531
        bool has_interval_mean = false;
1✔
1532
        bool has_interval_max = false;
1✔
1533
        bool has_interval_min = false;
1✔
1534
        bool has_slice_gatherer = false;
1✔
1535
        
1536
        for (int j = 0; j < analog_source_item->childCount(); ++j) {
9✔
1537
            auto * computer_item = analog_source_item->child(j);
8✔
1538
            QString computer_name = computer_item->text(0);
8✔
1539
            
1540
            if (computer_name.contains("Timestamp Value")) has_timestamp_value = true;
8✔
1541
            if (computer_name.contains("Interval Mean")) has_interval_mean = true;
8✔
1542
            if (computer_name.contains("Interval Max")) has_interval_max = true;
8✔
1543
            if (computer_name.contains("Interval Min")) has_interval_min = true;
8✔
1544
            if (computer_name.contains("Slice Gatherer")) has_slice_gatherer = true;
8✔
1545
        }
8✔
1546
        
1547
        // Should have interval computers
1548
        REQUIRE(has_interval_mean);
1✔
1549
        REQUIRE(has_interval_max);
1✔
1550
        REQUIRE(has_interval_min);
1✔
1551
        REQUIRE(has_slice_gatherer);
1✔
1552
        // Should NOT have timestamp-based computers
1553
        REQUIRE_FALSE(has_timestamp_value);
1✔
1554
    }
6✔
1555
    
1556
    SECTION("Events row selector shows timestamp-compatible computers") {
5✔
1557
        TableDesignerWidget widget(getDataManagerPtr());
1✔
1558
        
1559
        // Create a table
1560
        auto & registry = getTableRegistry();
1✔
1561
        auto table_id = registry.generateUniqueTableId("EventsTest");
3✔
1562
        REQUIRE(registry.createTable(table_id, "Events Compatibility Test"));
5✔
1563
        
1564
        // Select table
1565
        auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
1566
        REQUIRE(table_combo != nullptr);
1✔
1567
        for (int i = 0; i < table_combo->count(); ++i) {
2✔
1568
            if (table_combo->itemText(i).contains(QString::fromStdString(table_id))) {
1✔
UNCOV
1569
                table_combo->setCurrentIndex(i);
×
UNCOV
1570
                break;
×
1571
            }
1572
        }
1573
        
1574
        // Select Events as row source
1575
        auto * row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
1576
        REQUIRE(row_combo != nullptr);
1✔
1577
        int events_index = -1;
1✔
1578
        for (int i = 0; i < row_combo->count(); ++i) {
5✔
1579
            if (row_combo->itemText(i).startsWith("Events: ")) {
5✔
1580
                events_index = i;
1✔
1581
                break;
1✔
1582
            }
1583
        }
1584
        REQUIRE(events_index >= 0);
1✔
1585
        row_combo->setCurrentIndex(events_index);
1✔
1586
        
1587
        // Get the computers tree
1588
        auto * tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1589
        REQUIRE(tree != nullptr);
1✔
1590
        
1591
        // Verify that the tree is populated (not empty when row selector is chosen)
1592
        REQUIRE(tree->topLevelItemCount() > 0);
1✔
1593
    }
6✔
1594
    
1595
    SECTION("Changing row selector type updates available computers") {
5✔
1596
        TableDesignerWidget widget(getDataManagerPtr());
1✔
1597
        
1598
        // Create a table
1599
        auto & registry = getTableRegistry();
1✔
1600
        auto table_id = registry.generateUniqueTableId("ChangeTest");
3✔
1601
        REQUIRE(registry.createTable(table_id, "Change Row Selector Test"));
5✔
1602
        
1603
        // Select table
1604
        auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
1605
        REQUIRE(table_combo != nullptr);
1✔
1606
        for (int i = 0; i < table_combo->count(); ++i) {
2✔
1607
            if (table_combo->itemText(i).contains(QString::fromStdString(table_id))) {
1✔
UNCOV
1608
                table_combo->setCurrentIndex(i);
×
UNCOV
1609
                break;
×
1610
            }
1611
        }
1612
        
1613
        auto * row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
1614
        REQUIRE(row_combo != nullptr);
1✔
1615
        auto * tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1616
        REQUIRE(tree != nullptr);
1✔
1617
        
1618
        // First select TimeFrame
1619
        int timeframe_index = -1;
1✔
1620
        for (int i = 0; i < row_combo->count(); ++i) {
1✔
1621
            if (row_combo->itemText(i).startsWith("TimeFrame: ")) {
1✔
1622
                timeframe_index = i;
1✔
1623
                break;
1✔
1624
            }
1625
        }
1626
        REQUIRE(timeframe_index >= 0);
1✔
1627
        row_combo->setCurrentIndex(timeframe_index);
1✔
1628
        
1629
        // Count computers for analog source with TimeFrame
1630
        int timestamp_computer_count = 0;
1✔
1631
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
2✔
1632
            auto * item = tree->topLevelItem(i);
2✔
1633
            if (item->text(0).startsWith("analog:")) {
2✔
1634
                timestamp_computer_count = item->childCount();
1✔
1635
                break;
1✔
1636
            }
1637
        }
1638
        REQUIRE(timestamp_computer_count > 0);
1✔
1639
        
1640
        // Now change to Intervals
1641
        int intervals_index = -1;
1✔
1642
        for (int i = 0; i < row_combo->count(); ++i) {
7✔
1643
            if (row_combo->itemText(i).startsWith("Intervals: ")) {
7✔
1644
                intervals_index = i;
1✔
1645
                break;
1✔
1646
            }
1647
        }
1648
        REQUIRE(intervals_index >= 0);
1✔
1649
        row_combo->setCurrentIndex(intervals_index);
1✔
1650
        
1651
        // Count computers for analog source with Intervals
1652
        int interval_computer_count = 0;
1✔
1653
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
2✔
1654
            auto * item = tree->topLevelItem(i);
2✔
1655
            if (item->text(0).startsWith("analog:")) {
2✔
1656
                interval_computer_count = item->childCount();
1✔
1657
                break;
1✔
1658
            }
1659
        }
1660
        REQUIRE(interval_computer_count > 0);
1✔
1661
        
1662
        // The counts should be different (different computers for different row selector types)
1663
        REQUIRE(timestamp_computer_count != interval_computer_count);
1✔
1664
    }
6✔
1665
    
1666
    SECTION("Data sources without compatible computers are not displayed") {
5✔
1667
        TableDesignerWidget widget(getDataManagerPtr());
1✔
1668
        
1669
        // Create a table
1670
        auto & registry = getTableRegistry();
1✔
1671
        auto table_id = registry.generateUniqueTableId("EmptySourceTest");
3✔
1672
        REQUIRE(registry.createTable(table_id, "Empty Source Test"));
5✔
1673
        
1674
        // Select table
1675
        auto * table_combo = widget.findChild<QComboBox*>("table_combo");
2✔
1676
        REQUIRE(table_combo != nullptr);
1✔
1677
        for (int i = 0; i < table_combo->count(); ++i) {
2✔
1678
            if (table_combo->itemText(i).contains(QString::fromStdString(table_id))) {
1✔
UNCOV
1679
                table_combo->setCurrentIndex(i);
×
UNCOV
1680
                break;
×
1681
            }
1682
        }
1683
        
1684
        // Select TimeFrame as row source
1685
        auto * row_combo = widget.findChild<QComboBox*>("row_data_source_combo");
2✔
1686
        REQUIRE(row_combo != nullptr);
1✔
1687
        int timeframe_index = -1;
1✔
1688
        for (int i = 0; i < row_combo->count(); ++i) {
1✔
1689
            if (row_combo->itemText(i).startsWith("TimeFrame: ")) {
1✔
1690
                timeframe_index = i;
1✔
1691
                break;
1✔
1692
            }
1693
        }
1694
        REQUIRE(timeframe_index >= 0);
1✔
1695
        row_combo->setCurrentIndex(timeframe_index);
1✔
1696
        
1697
        // Get the computers tree
1698
        auto * tree = widget.findChild<QTreeWidget*>("computers_tree");
2✔
1699
        REQUIRE(tree != nullptr);
1✔
1700
        
1701
        // Verify that event sources are NOT displayed (no timestamp-compatible computers for events)
1702
        bool has_event_source = false;
1✔
1703
        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
5✔
1704
            auto * item = tree->topLevelItem(i);
4✔
1705
            if (item->text(0).startsWith("Events: ")) {
4✔
UNCOV
1706
                has_event_source = true;
×
UNCOV
1707
                break;
×
1708
            }
1709
        }
1710
        
1711
        // Event sources should not appear when TimeFrame is selected as row source
1712
        // because event computers require IntervalBased row selectors
1713
        REQUIRE_FALSE(has_event_source);
1✔
1714
    }
6✔
1715
}
5✔
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