From 2ee2c90094e4a8a310929dba7b56c01eeffeef46 Mon Sep 17 00:00:00 2001 From: Oleksandr Movchan Date: Mon, 9 Feb 2026 14:45:01 +0200 Subject: [PATCH] Add root CMakeLists.txt. Add googletest submodule. Add some unit tests --- .gitmodules | 3 + 3rdParty/googletest | 1 + CMakeLists.txt | 16 ++ examples/QmlBarcodeGenerator/CMakeLists.txt | 8 +- .../QmlBarcodeGenerator/cmake/Locations.cmake | 37 ---- examples/QmlBarcodeReader/CMakeLists.txt | 8 +- .../QmlBarcodeReader/cmake/Locations.cmake | 37 ---- src/SBarcodeGenerator.cpp | 5 + src/SBarcodeGenerator.h | 6 + src/SBarcodeScanner.cpp | 6 +- tests/CMakeLists.txt | 17 ++ tests/main.cpp | 6 + tests/test_SBarcodeFormat.cpp | 208 ++++++++++++++++++ tests/test_SBarcodeGenerator.cpp | 160 ++++++++++++++ 14 files changed, 427 insertions(+), 91 deletions(-) create mode 160000 3rdParty/googletest create mode 100644 CMakeLists.txt delete mode 100644 examples/QmlBarcodeGenerator/cmake/Locations.cmake delete mode 100644 examples/QmlBarcodeReader/cmake/Locations.cmake create mode 100644 tests/CMakeLists.txt create mode 100644 tests/main.cpp create mode 100644 tests/test_SBarcodeFormat.cpp create mode 100644 tests/test_SBarcodeGenerator.cpp diff --git a/.gitmodules b/.gitmodules index ab0adce..d300981 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "src/zxing-cpp"] path = src/zxing-cpp url = https://github.com/nu-book/zxing-cpp.git +[submodule "3rdParty/googletest"] + path = 3rdParty/googletest + url = https://github.com/google/googletest.git diff --git a/3rdParty/googletest b/3rdParty/googletest new file mode 160000 index 0000000..5a9c3f9 --- /dev/null +++ b/3rdParty/googletest @@ -0,0 +1 @@ +Subproject commit 5a9c3f9e8d9b90bbbe8feb32902146cb8f7c1757 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b5b7efc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.16) +project(QmlBarcode LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(QT NAMES Qt5 Qt6) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Multimedia Concurrent Quick REQUIRED) + + +set(BUILD_GMOCK OFF) +add_subdirectory(3rdParty/googletest) +add_subdirectory(src) +add_subdirectory(examples/QmlBarcodeReader) +add_subdirectory(examples/QmlBarcodeGenerator) +add_subdirectory(tests) diff --git a/examples/QmlBarcodeGenerator/CMakeLists.txt b/examples/QmlBarcodeGenerator/CMakeLists.txt index 4a098d1..a76998a 100644 --- a/examples/QmlBarcodeGenerator/CMakeLists.txt +++ b/examples/QmlBarcodeGenerator/CMakeLists.txt @@ -10,17 +10,12 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -#Check this file for any *_DIR variable definitions and other -include("cmake/Locations.cmake") - if(Qt${QT_VERSION_MAJOR} STREQUAL Qt5) qt5_add_resources(RSCS qml.qrc) else() qt_add_resources(RSCS qml.qrc) endif() -add_subdirectory(${LIB_DIR} ${CMAKE_BINARY_DIR}/SCodes) - if(ANDROID) if(Qt${QT_VERSION_MAJOR} STREQUAL Qt5) @@ -56,8 +51,7 @@ else() endif() -target_link_libraries(${PROJECT_NAME} PRIVATE ${REQUIRED_QT_LIBS} SCodes) - +target_link_libraries(${PROJECT_NAME} PRIVATE SCodes) if(QT_VERSION_MAJOR EQUAL 6) qt_import_qml_plugins(${PROJECT_NAME}) diff --git a/examples/QmlBarcodeGenerator/cmake/Locations.cmake b/examples/QmlBarcodeGenerator/cmake/Locations.cmake deleted file mode 100644 index 0c773e7..0000000 --- a/examples/QmlBarcodeGenerator/cmake/Locations.cmake +++ /dev/null @@ -1,37 +0,0 @@ -set(COMPANY "Scythe Studio") -set(COPYRIGHT "Copyright (c) 2022 Scythe Studio. Licensed under the Apache License, Version 2.0.") -set(IDENTIFIER "com.scythestudio.scodes.example") - -# ---CONFIGURATION--- -option(USE_QML "Add QML support" ON) -option(USE_LIBS "Use external libraries" ON) - -# Locations - directories in project structure -set(LIB_DIR ${CMAKE_SOURCE_DIR}/../../src) - -set(SRC_DIR ".") -set(RES_DIR ".") -set(QML_DIR "qml") - -# Check Qt version -find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core) -find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core) - -# ---PACKAGES SECTION--- -list(APPEND REQUIRED_QT_PACKAGES Core Quick Gui Multimedia) - -if(USE_QML) - list(APPEND REQUIRED_QT_PACKAGES Quick) - list(APPEND QML_IMPORT_PATH "${CMAKE_SOURCE_DIR}/${QML_DIR}") - set(QML_IMPORT_PATH ${QML_IMPORT_PATH} - CACHE STRING "Qt Creator Import Path" - FORCE) -endif() - -foreach(QT_PACKAGE ${REQUIRED_QT_PACKAGES}) - list(APPEND REQUIRED_QT_LIBS Qt${QT_VERSION_MAJOR}::${QT_PACKAGE}) -endforeach() - -find_package(QT NAMES Qt${QT_VERSION_MAJOR} COMPONENTS ${REQUIRED_QT_PACKAGES} REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS ${REQUIRED_QT_PACKAGES} REQUIRED) - diff --git a/examples/QmlBarcodeReader/CMakeLists.txt b/examples/QmlBarcodeReader/CMakeLists.txt index 55cffba..cca188d 100644 --- a/examples/QmlBarcodeReader/CMakeLists.txt +++ b/examples/QmlBarcodeReader/CMakeLists.txt @@ -10,17 +10,12 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -#Check this file for any *_DIR variable definitions and other -include("cmake/Locations.cmake") - if(Qt${QT_VERSION_MAJOR} STREQUAL Qt5) qt5_add_resources(RSCS Qt5qml.qrc) else() qt_add_resources(RSCS Qt6qml.qrc) endif() -add_subdirectory(${LIB_DIR} ${CMAKE_BINARY_DIR}/SCodes) - if(ANDROID) if(Qt${QT_VERSION_MAJOR} STREQUAL Qt5) @@ -54,8 +49,7 @@ else() endif() -target_link_libraries(${PROJECT_NAME} PRIVATE ${REQUIRED_QT_LIBS} SCodes) - +target_link_libraries(${PROJECT_NAME} PRIVATE SCodes) if(QT_VERSION_MAJOR EQUAL 6) qt_import_qml_plugins(${PROJECT_NAME}) diff --git a/examples/QmlBarcodeReader/cmake/Locations.cmake b/examples/QmlBarcodeReader/cmake/Locations.cmake deleted file mode 100644 index 0c773e7..0000000 --- a/examples/QmlBarcodeReader/cmake/Locations.cmake +++ /dev/null @@ -1,37 +0,0 @@ -set(COMPANY "Scythe Studio") -set(COPYRIGHT "Copyright (c) 2022 Scythe Studio. Licensed under the Apache License, Version 2.0.") -set(IDENTIFIER "com.scythestudio.scodes.example") - -# ---CONFIGURATION--- -option(USE_QML "Add QML support" ON) -option(USE_LIBS "Use external libraries" ON) - -# Locations - directories in project structure -set(LIB_DIR ${CMAKE_SOURCE_DIR}/../../src) - -set(SRC_DIR ".") -set(RES_DIR ".") -set(QML_DIR "qml") - -# Check Qt version -find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core) -find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core) - -# ---PACKAGES SECTION--- -list(APPEND REQUIRED_QT_PACKAGES Core Quick Gui Multimedia) - -if(USE_QML) - list(APPEND REQUIRED_QT_PACKAGES Quick) - list(APPEND QML_IMPORT_PATH "${CMAKE_SOURCE_DIR}/${QML_DIR}") - set(QML_IMPORT_PATH ${QML_IMPORT_PATH} - CACHE STRING "Qt Creator Import Path" - FORCE) -endif() - -foreach(QT_PACKAGE ${REQUIRED_QT_PACKAGES}) - list(APPEND REQUIRED_QT_LIBS Qt${QT_VERSION_MAJOR}::${QT_PACKAGE}) -endforeach() - -find_package(QT NAMES Qt${QT_VERSION_MAJOR} COMPONENTS ${REQUIRED_QT_PACKAGES} REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS ${REQUIRED_QT_PACKAGES} REQUIRED) - diff --git a/src/SBarcodeGenerator.cpp b/src/SBarcodeGenerator.cpp index 525239c..361ebfd 100644 --- a/src/SBarcodeGenerator.cpp +++ b/src/SBarcodeGenerator.cpp @@ -241,6 +241,11 @@ QColor SBarcodeGenerator::backgroundColor() const return m_backgroundColor; } +QString SBarcodeGenerator::generatedFilePath() const +{ + return m_filePath; +} + void SBarcodeGenerator::setBackgroundColor(const QColor &backgroundColor) { if (m_backgroundColor == backgroundColor) { diff --git a/src/SBarcodeGenerator.h b/src/SBarcodeGenerator.h index 226800d..f50108f 100644 --- a/src/SBarcodeGenerator.h +++ b/src/SBarcodeGenerator.h @@ -99,6 +99,12 @@ class SBarcodeGenerator : public QQuickItem */ QColor backgroundColor() const; + /*! + * \brief Returns the full path to the last successfully generated barcode image (temp file). + * Exposed for unit tests only. + */ + QString generatedFilePath() const; + public slots: /*! diff --git a/src/SBarcodeScanner.cpp b/src/SBarcodeScanner.cpp index 247b6e9..6250d01 100644 --- a/src/SBarcodeScanner.cpp +++ b/src/SBarcodeScanner.cpp @@ -89,13 +89,13 @@ QCamera *SBarcodeScanner::makeDefaultCamera() auto defaultCamera = QMediaDevices::defaultVideoInput(); if (defaultCamera.isNull()) { - errorOccured("No default camera could be found on the system"); + emit errorOccured("No default camera could be found on the system"); return nullptr; } auto camera = new QCamera(defaultCamera, this); if (camera->error()) { - errorOccured("Error during camera initialization: " + camera->errorString()); + emit errorOccured("Error during camera initialization: " + camera->errorString()); return nullptr; } @@ -109,7 +109,7 @@ QCamera *SBarcodeScanner::makeDefaultCamera() } if(supportedFormats.empty()) { - errorOccured("A default camera was found but it has no supported formats. The Camera may be wrongly configured."); + emit errorOccured("A default camera was found but it has no supported formats. The Camera may be wrongly configured."); return nullptr; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..862ed54 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,17 @@ +project(SCodes_tests LANGUAGES CXX) +enable_testing() + +file(GLOB_RECURSE SOURCES *.cpp) +add_executable(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../3rdParty/googletest +) + +target_link_libraries(${PROJECT_NAME} PRIVATE + SCodes + GTest::gtest + GTest::gtest_main + Qt6::Core + Qt6::Gui +) diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 0000000..2dc3787 --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,6 @@ +#include + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/tests/test_SBarcodeFormat.cpp b/tests/test_SBarcodeFormat.cpp new file mode 100644 index 0000000..19c1de8 --- /dev/null +++ b/tests/test_SBarcodeFormat.cpp @@ -0,0 +1,208 @@ +#include + +#include + +#include "SBarcodeFormat.h" + +using namespace SCodes; + +/* ============================================================ + * Single enum → ZXing enum + * ============================================================ */ + +TEST(SBarcodeFormatTest, ToZXingFormat_SingleEnum) +{ + EXPECT_EQ(toZXingFormat(SBarcodeFormat::None), + ZXing::BarcodeFormat::None); + + EXPECT_EQ(toZXingFormat(SBarcodeFormat::QRCode), + ZXing::BarcodeFormat::QRCode); + + EXPECT_EQ(toZXingFormat(SBarcodeFormat::Code128), + ZXing::BarcodeFormat::Code128); + + EXPECT_EQ(toZXingFormat(SBarcodeFormat::DXFilmEdge), + ZXing::BarcodeFormat::DXFilmEdge); +} + +/* ============================================================ + * toString() + * ============================================================ */ + +TEST(SBarcodeFormatTest, ToString_UsesZXingCanonicalNames) +{ + EXPECT_EQ(toString(SBarcodeFormat::QRCode), + QStringLiteral("QRCode")); + + EXPECT_EQ(toString(SBarcodeFormat::EAN13), + QStringLiteral("EAN-13")); + + EXPECT_EQ(toString(SBarcodeFormat::UPCA), + QStringLiteral("UPC-A")); + + EXPECT_EQ(toString(SBarcodeFormat::MicroQRCode), + QStringLiteral("MicroQRCode")); +} + +/* ============================================================ + * fromString() + * ============================================================ */ + +TEST(SBarcodeFormatTest, FromString_IsCaseInsensitive) +{ + EXPECT_EQ(fromString(QStringLiteral("qrcode")), + SBarcodeFormat::QRCode); + + EXPECT_EQ(fromString(QStringLiteral("QrCoDe")), + SBarcodeFormat::QRCode); +} + +TEST(SBarcodeFormatTest, FromString_IgnoresDashesUnderscores) +{ + EXPECT_EQ(fromString(QStringLiteral("EAN13")), + SBarcodeFormat::EAN13); + + EXPECT_EQ(fromString(QStringLiteral("EAN-13")), + SBarcodeFormat::EAN13); + + EXPECT_EQ(fromString(QStringLiteral("EAN_13")), + SBarcodeFormat::EAN13); + + EXPECT_EQ(fromString(QStringLiteral("Code[128]")), + SBarcodeFormat::Code128); +} + +TEST(SBarcodeFormatTest, FromString_UnknownReturnsNone) +{ + EXPECT_EQ(fromString(QStringLiteral("TotallyInvalid")), + SBarcodeFormat::None); + + EXPECT_EQ(fromString(QString()), + SBarcodeFormat::None); +} + +/* ============================================================ + * Round-trip consistency + * ============================================================ */ + +TEST(SBarcodeFormatTest, ToStringFromString_RoundTrip) +{ + const SBarcodeFormat values[] = { + SBarcodeFormat::Aztec, + SBarcodeFormat::Code39, + SBarcodeFormat::Code93, + SBarcodeFormat::Code128, + SBarcodeFormat::DataMatrix, + SBarcodeFormat::QRCode, + SBarcodeFormat::PDF417, + SBarcodeFormat::MicroQRCode, + SBarcodeFormat::RMQRCode, + }; + + for (auto v : values) { + EXPECT_EQ(fromString(toString(v)), v); + } +} + +/* ============================================================ + * Composite ZXing formats are intentionally NOT mapped + * ============================================================ */ + +TEST(SBarcodeFormatTest, FromString_DoesNotMapCompositeZXingFormats) +{ + EXPECT_EQ(fromString(QStringLiteral("Linear-Codes")), + SBarcodeFormat::None); + + EXPECT_EQ(fromString(QStringLiteral("Matrix-Codes")), + SBarcodeFormat::None); + + EXPECT_EQ(fromString(QStringLiteral("Any")), + SBarcodeFormat::None); +} + +/* ============================================================ + * Flags → ZXing::BarcodeFormats + * ============================================================ */ + +TEST(SBarcodeFormatTest, ToZXingFormat_FlagsSingle) +{ + const auto zxing = + toZXingFormat(SBarcodeFormats{SBarcodeFormat::QRCode}); + + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::QRCode)); + EXPECT_FALSE(zxing.testFlag(ZXing::BarcodeFormat::EAN13)); +} + +TEST(SBarcodeFormatTest, ToZXingFormat_MultipleFlags) +{ + const SBarcodeFormats formats = + SBarcodeFormat::QRCode | + SBarcodeFormat::EAN13 | + SBarcodeFormat::Code128; + + const auto zxing = toZXingFormat(formats); + + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::QRCode)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::EAN13)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::Code128)); + EXPECT_FALSE(zxing.testFlag(ZXing::BarcodeFormat::PDF417)); +} + +/* ============================================================ + * Predefined SCodes groups + * ============================================================ */ + +TEST(SBarcodeFormatTest, OneDCodes_GroupContainsOnlyLinearFormats) +{ + const auto zxing = + toZXingFormat(SBarcodeFormats{SBarcodeFormat::OneDCodes}); + + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::Codabar)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::Code39)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::Code93)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::Code128)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::EAN8)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::EAN13)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::ITF)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::UPCA)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::UPCE)); + + EXPECT_FALSE(zxing.testFlag(ZXing::BarcodeFormat::QRCode)); + EXPECT_FALSE(zxing.testFlag(ZXing::BarcodeFormat::DataMatrix)); +} + +TEST(SBarcodeFormatTest, TwoDCodes_GroupContainsOnlyMatrixFormats) +{ + const auto zxing = + toZXingFormat(SBarcodeFormats{SBarcodeFormat::TwoDCodes}); + + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::Aztec)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::DataMatrix)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::MaxiCode)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::PDF417)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::QRCode)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::MicroQRCode)); + EXPECT_TRUE(zxing.testFlag(ZXing::BarcodeFormat::RMQRCode)); + + EXPECT_FALSE(zxing.testFlag(ZXing::BarcodeFormat::Code128)); + EXPECT_FALSE(zxing.testFlag(ZXing::BarcodeFormat::EAN13)); +} + +/* ============================================================ + * Edge cases + * ============================================================ */ + +TEST(SBarcodeFormatTest, EmptyFlagsProduceEmptyZXingFormats) +{ + const auto zxing = toZXingFormat(SBarcodeFormats{}); + EXPECT_TRUE(zxing.empty()); +} + +TEST(SBarcodeFormatTest, InvalidEnumValueReturnsNone) +{ + const auto invalid = + static_cast(0x7FFFFFFF); + + EXPECT_EQ(toZXingFormat(invalid), + ZXing::BarcodeFormat::None); +} diff --git a/tests/test_SBarcodeGenerator.cpp b/tests/test_SBarcodeGenerator.cpp new file mode 100644 index 0000000..6451b42 --- /dev/null +++ b/tests/test_SBarcodeGenerator.cpp @@ -0,0 +1,160 @@ +#include + +#include "SBarcodeGenerator.h" +#include "SBarcodeDecoder.h" // reuse the project's own decoder +#include "SBarcodeFormat.h" + +#include +#include +#include +#include + +// Helper to decode a generated image using the exact same code the library uses +static QString decodeGeneratedImage(const QString& imagePath) +{ + QImage img(imagePath); + if (img.isNull()) + return ""; + + SBarcodeDecoder decoder; + ZXing::BarcodeFormats formats = ZXing::BarcodeFormat::Any; + + decoder.process(img, formats); + return decoder.captured(); +} + +TEST(SBarcodeGenerator, DefaultsAreSensible) +{ + SBarcodeGenerator gen; + EXPECT_EQ(gen.format(), SCodes::SBarcodeFormat::Code128); + EXPECT_EQ(gen.foregroundColor(), QColor("black")); + EXPECT_EQ(gen.backgroundColor(), QColor("white")); + EXPECT_EQ(gen.centerImageRatio(), 5); + EXPECT_TRUE(gen.generatedFilePath().isEmpty()); // or property("filePath") +} + +TEST(SBarcodeGenerator, GenerateRejectsEmptyString) +{ + SBarcodeGenerator gen; + EXPECT_FALSE(gen.generate("")); +} + +TEST(SBarcodeGenerator, GenerateCreatesValidQRCode) +{ + SBarcodeGenerator gen; + gen.setFormat(SCodes::SBarcodeFormat::QRCode); + + const QString input = "https://github.com/somcosoftware/scodes"; + EXPECT_TRUE(gen.generate(input)); + + const QString path = gen.generatedFilePath(); + EXPECT_FALSE(path.isEmpty()); + EXPECT_TRUE(QFile::exists(path)); + + // Verify it decodes back to the original text + EXPECT_EQ(decodeGeneratedImage(path), input); + + QFile::remove(path); // cleanup +} + +TEST(SBarcodeGenerator, CenterImageOnQRCodeIsSupportedAndStillDecodable) +{ + SBarcodeGenerator gen; + gen.setFormat(SCodes::SBarcodeFormat::QRCode); + gen.setCenterImageRatio(4); // smaller logo + + // Create a fake center image + QImage logo(200, 200, QImage::Format_RGB32); + logo.fill(Qt::red); + const QString logoPath = QDir::tempPath() + "/test_logo.png"; + logo.save(logoPath); + + gen.setImagePath(logoPath); + + const QString input = "QR with logo"; + EXPECT_TRUE(gen.generate(input)); + + const QString path = gen.generatedFilePath(); + EXPECT_EQ(decodeGeneratedImage(path), input); // still decodable thanks to ECC=8 + + QFile::remove(path); + QFile::remove(logoPath); +} + +TEST(SBarcodeGenerator, CenterImageIsIgnoredForNonQR) +{ + SBarcodeGenerator gen; + gen.setFormat(SCodes::SBarcodeFormat::Code128); + + QImage logo(100, 100, QImage::Format_RGB32); + logo.fill(Qt::green); + const QString logoPath = QDir::tempPath() + "/test_logo2.png"; + logo.save(logoPath); + gen.setImagePath(logoPath); + + const QString input = "1234567890"; + EXPECT_TRUE(gen.generate(input)); + + const QString path = gen.generatedFilePath(); + EXPECT_EQ(decodeGeneratedImage(path), input); // still works, logo was ignored + + QFile::remove(path); + QFile::remove(logoPath); +} + +TEST(SBarcodeGenerator, ForegroundAndBackgroundColorsAreApplied) +{ + SBarcodeGenerator gen; + gen.setFormat(SCodes::SBarcodeFormat::QRCode); + gen.setForegroundColor(Qt::red); + gen.setBackgroundColor(Qt::blue); + + EXPECT_TRUE(gen.generate("Color test")); + + QImage img(gen.generatedFilePath()); + ASSERT_FALSE(img.isNull()); + + // Margin area should be background + EXPECT_EQ(img.pixelColor(0, 0), Qt::blue); + // At least one module should be foreground (QR has many black modules normally) + bool hasRed = false; + for (int y = 0; y < img.height() && !hasRed; ++y) + for (int x = 0; x < img.width(); ++x) + if (img.pixelColor(x, y) == Qt::red) { + hasRed = true; + break; + } + EXPECT_TRUE(hasRed); + + QFile::remove(gen.generatedFilePath()); +} + +TEST(SBarcodeGenerator, SaveImageCopiesToDocuments) +{ + SBarcodeGenerator gen; + gen.setFormat(SCodes::SBarcodeFormat::QRCode); + gen.generate("Save test"); + + const QString original = gen.generatedFilePath(); + EXPECT_TRUE(gen.saveImage()); + + const QString docPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + + "/" + gen.property("fileName").toString() + "." + + gen.property("extension").toString(); + + EXPECT_TRUE(QFile::exists(docPath)); + + // cleanup + QFile::remove(original); + QFile::remove(docPath); +} + +TEST(SBarcodeGenerator, FormatStringOverloadWorks) +{ + SBarcodeGenerator gen; + gen.setFormat("QRCode"); + EXPECT_EQ(gen.format(), SCodes::SBarcodeFormat::QRCode); + + gen.setFormat("invalid"); + EXPECT_EQ(gen.format(), SCodes::SBarcodeFormat::QRCode); // unchanged +}