- PyQt6 main window with Tx/Rx tables, connection dock, LDF toolbar, control bar with global send rate, and status bar - C++ Qt6 equivalent with identical layout and feature parity - About dialog: TeqanyLogix LTD / Developer: Mohamed Salem - Application logo (SVG + PNG) with LIN bus waveform design - Full test suites: Python (32 tests), C++ QTest (34 tests) - Project plan and Step 1 documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
279 lines
8.2 KiB
C++
279 lines
8.2 KiB
C++
/**
|
|
* test_main_window.cpp — Tests for the GUI skeleton (Step 1, C++).
|
|
*
|
|
* QTest FRAMEWORK:
|
|
* ================
|
|
* Qt provides its own test framework, QTest. It's simpler than GoogleTest
|
|
* but integrates perfectly with Qt's event loop and widget system.
|
|
*
|
|
* Key differences from pytest:
|
|
* pytest: assert window.windowTitle() == "..."
|
|
* QTest: QCOMPARE(window.windowTitle(), QString("..."))
|
|
*
|
|
* Test discovery:
|
|
* - Each test class inherits QObject and has Q_OBJECT
|
|
* - Test methods are private slots named test_*() or *_test()
|
|
* - QTEST_MAIN() generates main() that runs all test slots
|
|
*
|
|
* QCOMPARE vs QVERIFY:
|
|
* QVERIFY(condition) — like assert condition
|
|
* QCOMPARE(actual, expected) — like assert actual == expected, but prints both values on failure
|
|
*/
|
|
|
|
#include <QtTest/QtTest>
|
|
#include <QDockWidget>
|
|
#include <QTableWidget>
|
|
#include <QSpinBox>
|
|
#include <QLineEdit>
|
|
#include <QPushButton>
|
|
#include <QCheckBox>
|
|
#include <QComboBox>
|
|
#include <QLabel>
|
|
#include "main_window.h"
|
|
|
|
class TestMainWindow : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private:
|
|
MainWindow* m_window;
|
|
|
|
private slots:
|
|
// ── Setup/Teardown ──
|
|
// init() runs BEFORE each test, cleanup() runs AFTER each test.
|
|
// This gives each test a fresh MainWindow (like pytest fixtures).
|
|
void init()
|
|
{
|
|
m_window = new MainWindow();
|
|
}
|
|
|
|
void cleanup()
|
|
{
|
|
delete m_window;
|
|
m_window = nullptr;
|
|
}
|
|
|
|
// ─── Window Basics ────────────────────────────────────────────
|
|
|
|
void test_windowTitle()
|
|
{
|
|
QCOMPARE(m_window->windowTitle(), QString("LIN Simulator"));
|
|
}
|
|
|
|
void test_minimumSize()
|
|
{
|
|
QVERIFY(m_window->minimumWidth() >= 1024);
|
|
QVERIFY(m_window->minimumHeight() >= 768);
|
|
}
|
|
|
|
void test_centralWidgetExists()
|
|
{
|
|
QVERIFY(m_window->centralWidget() != nullptr);
|
|
}
|
|
|
|
// ─── Menu Bar ─────────────────────────────────────────────────
|
|
|
|
void test_menuBarExists()
|
|
{
|
|
QVERIFY(m_window->menuBar() != nullptr);
|
|
}
|
|
|
|
void test_loadLdfActionExists()
|
|
{
|
|
QVERIFY(m_window->loadLdfAction() != nullptr);
|
|
QCOMPARE(m_window->loadLdfAction()->text(), QString("&Load LDF..."));
|
|
}
|
|
|
|
void test_loadLdfShortcut()
|
|
{
|
|
QCOMPARE(m_window->loadLdfAction()->shortcut().toString(), QString("Ctrl+O"));
|
|
}
|
|
|
|
// ─── LDF Toolbar ──────────────────────────────────────────────
|
|
|
|
void test_ldfPathFieldExists()
|
|
{
|
|
QVERIFY(m_window->ldfPathEdit() != nullptr);
|
|
QVERIFY(m_window->ldfPathEdit()->isReadOnly());
|
|
}
|
|
|
|
void test_ldfPathPlaceholder()
|
|
{
|
|
QCOMPARE(m_window->ldfPathEdit()->placeholderText(),
|
|
QString("No LDF file loaded"));
|
|
}
|
|
|
|
void test_browseButtonExists()
|
|
{
|
|
QVERIFY(m_window->browseButton() != nullptr);
|
|
}
|
|
|
|
void test_autoReloadDefaultChecked()
|
|
{
|
|
QVERIFY(m_window->autoReloadCheck()->isChecked());
|
|
}
|
|
|
|
// ─── Tx Table ─────────────────────────────────────────────────
|
|
|
|
void test_txTableExists()
|
|
{
|
|
auto* table = m_window->txTable();
|
|
QVERIFY(table != nullptr);
|
|
// qobject_cast is Qt's type-safe dynamic cast.
|
|
// Returns nullptr if the object isn't the expected type.
|
|
QVERIFY(qobject_cast<QTableWidget*>(table) != nullptr);
|
|
}
|
|
|
|
void test_txTableColumns()
|
|
{
|
|
auto* table = m_window->txTable();
|
|
QCOMPARE(table->columnCount(), 7);
|
|
|
|
QStringList expected = {
|
|
"Frame Name", "Frame ID", "Length", "Interval (ms)",
|
|
"Data", "Signals", "Action"
|
|
};
|
|
for (int i = 0; i < table->columnCount(); ++i) {
|
|
QCOMPARE(table->horizontalHeaderItem(i)->text(), expected[i]);
|
|
}
|
|
}
|
|
|
|
void test_txTableAlternatingColors()
|
|
{
|
|
QVERIFY(m_window->txTable()->alternatingRowColors());
|
|
}
|
|
|
|
// ─── Rx Table ─────────────────────────────────────────────────
|
|
|
|
void test_rxTableExists()
|
|
{
|
|
auto* table = m_window->rxTable();
|
|
QVERIFY(table != nullptr);
|
|
QVERIFY(qobject_cast<QTableWidget*>(table) != nullptr);
|
|
}
|
|
|
|
void test_rxTableColumns()
|
|
{
|
|
auto* table = m_window->rxTable();
|
|
QCOMPARE(table->columnCount(), 5);
|
|
|
|
QStringList expected = {
|
|
"Timestamp", "Frame Name", "Frame ID", "Data", "Signals"
|
|
};
|
|
for (int i = 0; i < table->columnCount(); ++i) {
|
|
QCOMPARE(table->horizontalHeaderItem(i)->text(), expected[i]);
|
|
}
|
|
}
|
|
|
|
void test_rxTableNotEditable()
|
|
{
|
|
QCOMPARE(m_window->rxTable()->editTriggers(),
|
|
QAbstractItemView::NoEditTriggers);
|
|
}
|
|
|
|
// ─── Connection Dock ──────────────────────────────────────────
|
|
|
|
void test_dockExists()
|
|
{
|
|
// findChildren<T>() searches the widget tree for all children of type T
|
|
auto docks = m_window->findChildren<QDockWidget*>();
|
|
QCOMPARE(docks.size(), 1);
|
|
QCOMPARE(docks[0]->windowTitle(), QString("Connection"));
|
|
}
|
|
|
|
void test_deviceComboExists()
|
|
{
|
|
QVERIFY(m_window->deviceCombo() != nullptr);
|
|
}
|
|
|
|
void test_connectButtonExists()
|
|
{
|
|
QVERIFY(m_window->connectButton() != nullptr);
|
|
QVERIFY(m_window->connectButton()->isEnabled());
|
|
}
|
|
|
|
void test_disconnectButtonDisabledInitially()
|
|
{
|
|
QVERIFY(!m_window->disconnectButton()->isEnabled());
|
|
}
|
|
|
|
void test_statusLabelShowsDisconnected()
|
|
{
|
|
QVERIFY(m_window->connStatusLabel()->text().contains("Disconnected"));
|
|
}
|
|
|
|
void test_baudRateLabelExists()
|
|
{
|
|
QVERIFY(m_window->baudRateLabel() != nullptr);
|
|
}
|
|
|
|
void test_baudRateShowsPlaceholderBeforeLdf()
|
|
{
|
|
QVERIFY(m_window->baudRateLabel()->text().contains("load LDF"));
|
|
}
|
|
|
|
// ─── Control Bar ──────────────────────────────────────────────
|
|
|
|
void test_scheduleComboExists()
|
|
{
|
|
QVERIFY(m_window->scheduleCombo() != nullptr);
|
|
}
|
|
|
|
void test_schedulerButtonsDisabledInitially()
|
|
{
|
|
QVERIFY(!m_window->startButton()->isEnabled());
|
|
QVERIFY(!m_window->stopButton()->isEnabled());
|
|
QVERIFY(!m_window->pauseButton()->isEnabled());
|
|
}
|
|
|
|
void test_manualSendDisabledInitially()
|
|
{
|
|
QVERIFY(!m_window->manualSendButton()->isEnabled());
|
|
}
|
|
|
|
void test_globalRateSpinboxExists()
|
|
{
|
|
QVERIFY(m_window->globalRateSpin() != nullptr);
|
|
}
|
|
|
|
void test_globalRateDefault50ms()
|
|
{
|
|
QCOMPARE(m_window->globalRateSpin()->value(), 50);
|
|
}
|
|
|
|
void test_globalRateRange()
|
|
{
|
|
QCOMPARE(m_window->globalRateSpin()->minimum(), 1);
|
|
QCOMPARE(m_window->globalRateSpin()->maximum(), 10000);
|
|
}
|
|
|
|
void test_globalRateSuffix()
|
|
{
|
|
QCOMPARE(m_window->globalRateSpin()->suffix(), QString(" ms"));
|
|
}
|
|
|
|
// ─── Status Bar ───────────────────────────────────────────────
|
|
|
|
void test_statusBarExists()
|
|
{
|
|
QVERIFY(m_window->statusBar() != nullptr);
|
|
}
|
|
|
|
void test_connectionStatusLabel()
|
|
{
|
|
QVERIFY(m_window->statusConnectionLabel()->text().contains("Disconnected"));
|
|
}
|
|
};
|
|
|
|
// QTEST_MAIN generates a main() function that:
|
|
// 1. Creates a QApplication
|
|
// 2. Instantiates TestMainWindow
|
|
// 3. Runs all private slots as test cases
|
|
// 4. Reports results
|
|
QTEST_MAIN(TestMainWindow)
|
|
|
|
// This #include is required when the test class is defined in a .cpp file
|
|
// (not a .h file). It includes the MOC-generated code for our Q_OBJECT class.
|
|
// Without it, the linker would fail with "undefined reference to vtable".
|
|
#include "test_main_window.moc"
|