Refactor attachment handling system with enhanced UI (#12085)

* Renamed NewEntryAttachmentsDialog to EditEntryAttachmentsDialog for clarity.
* Introduced EditEntryAttachmentsDialog class to manage editing of existing attachments.
* Added functionality to preview attachments while editing them.
* Enhanced EntryAttachmentsModel with rowByKey method for better key management.
* Add image attachment support with zoom functionality.
* Add html and markdown detection.
* Improve button layout on the attachment section when editing an entry
This commit is contained in:
Kuznetsov Oleg
2025-06-19 20:27:23 +03:00
committed by GitHub
parent c4b4be48a5
commit f2a4cc7e66
54 changed files with 3254 additions and 388 deletions

View File

@@ -18,7 +18,9 @@
#include "TestTools.h"
#include "core/Clock.h"
#include "core/Tools.h"
#include <QFileInfo>
#include <QRegularExpression>
#include <QTest>
#include <QUuid>
@@ -277,10 +279,8 @@ void TestTools::testMimeTypes()
{
const QStringList TextMimeTypes = {
"text/plain", // Plain text
"text/html", // HTML documents
"text/css", // CSS stylesheets
"text/javascript", // JavaScript files
"text/markdown", // Markdown documents
"text/xml", // XML documents
"text/rtf", // Rich Text Format
"text/vcard", // vCard files
@@ -327,6 +327,9 @@ void TestTools::testMimeTypes()
"application/x-shellscript", // Shell scripts
};
QCOMPARE(Tools::toMimeType("text/html"), Tools::MimeType::Html);
QCOMPARE(Tools::toMimeType("text/markdown"), Tools::MimeType::Markdown);
for (const auto& mime : TextMimeTypes) {
QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::PlainText);
}
@@ -339,3 +342,89 @@ void TestTools::testMimeTypes()
QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::Unknown);
}
}
void TestTools::testGetMimeType()
{
const QStringList Text = {"0x42", ""};
for (const auto& text : Text) {
QCOMPARE(Tools::getMimeType(text.toUtf8()), Tools::MimeType::PlainText);
}
const QByteArrayList ImageHeaders = {
// JPEG: starts with 0xFF 0xD8 0xFF (Start of Image marker)
QByteArray::fromHex("FFD8FF"),
// PNG: starts with 0x89 0x50 0x4E 0x47 0D 0A 1A 0A (PNG signature)
QByteArray::fromHex("89504E470D0A1A0A"),
// GIF87a: original GIF format (1987 standard)
QByteArray("GIF87a"),
// GIF89a: extended GIF format (1989, supports animation, transparency, etc.)
QByteArray("GIF89a"),
};
for (const auto& image : ImageHeaders) {
QCOMPARE(Tools::getMimeType(image), Tools::MimeType::Image);
}
const QByteArrayList UnknownHeaders = {
// MP3: typically starts with ID3 tag (ID3v2)
QByteArray("ID3"),
// MP4: usually starts with a 'ftyp' box (ISO base media file format)
// Common major brands: isom, mp42, avc1, etc.
QByteArray::fromHex("000000186674797069736F6D"), // size + 'ftyp' + 'isom'
// PDF: starts with "%PDF-" followed by version (e.g., %PDF-1.7)
QByteArray("%PDF-"),
};
for (const auto& unknown : UnknownHeaders) {
QCOMPARE(Tools::getMimeType(unknown), Tools::MimeType::Unknown);
}
}
void TestTools::testGetMimeTypeByFileInfo()
{
const QStringList Text = {"test.txt", "test.csv", "test.xml", "test.json"};
for (const auto& text : Text) {
QCOMPARE(Tools::getMimeType(QFileInfo(text)), Tools::MimeType::PlainText);
}
const QStringList Images = {"test.jpg", "test.png", "test.bmp", "test.svg"};
for (const auto& image : Images) {
QCOMPARE(Tools::getMimeType(QFileInfo(image)), Tools::MimeType::Image);
}
const QStringList Htmls = {"test.html", "test.htm"};
for (const auto& html : Htmls) {
QCOMPARE(Tools::getMimeType(QFileInfo(html)), Tools::MimeType::Html);
}
const QStringList Markdowns = {"test.md", "test.markdown"};
for (const auto& makdown : Markdowns) {
QCOMPARE(Tools::getMimeType(QFileInfo(makdown)), Tools::MimeType::Markdown);
}
const QStringList UnknownHeaders = {"test.doc", "test.pdf", "test.docx"};
for (const auto& unknown : UnknownHeaders) {
QCOMPARE(Tools::getMimeType(unknown), Tools::MimeType::Unknown);
}
}
void TestTools::testIsTextMimeType()
{
const auto Text = {Tools::MimeType::PlainText, Tools::MimeType::Html, Tools::MimeType::Markdown};
for (const auto& text : Text) {
QVERIFY(Tools::isTextMimeType(text));
}
const auto NoText = {Tools::MimeType::Image, Tools::MimeType::Unknown};
for (const auto& noText : NoText) {
QVERIFY(!Tools::isTextMimeType(noText));
}
}

View File

@@ -18,7 +18,7 @@
#ifndef KEEPASSX_TESTTOOLS_H
#define KEEPASSX_TESTTOOLS_H
#include "core/Tools.h"
#include <QObject>
class TestTools : public QObject
{
@@ -38,6 +38,9 @@ private slots:
void testConvertToRegex_data();
void testArrayContainsValues();
void testMimeTypes();
void testGetMimeType();
void testGetMimeTypeByFileInfo();
void testIsTextMimeType();
};
#endif // KEEPASSX_TESTTOOLS_H

View File

@@ -18,6 +18,10 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..)
add_unit_test(NAME testgui SOURCES TestGui.cpp ../util/TemporaryFile.cpp ../mock/MockRemoteProcess.cpp LIBS ${TEST_LIBRARIES})
add_unit_test(NAME testguipixmaps SOURCES TestGuiPixmaps.cpp LIBS ${TEST_LIBRARIES})
file(GLOB_RECURSE ATTACHMENTS_TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/attachments/*.cpp)
add_unit_test(NAME testguiattachments SOURCES ${ATTACHMENTS_TEST_SOURCES} LIBS ${TEST_LIBRARIES})
include_directories(testguiattachments PRIVATE ${PROJECT_SOURCE_DIR}/src/gui/entry)
if(WITH_XC_BROWSER)
add_unit_test(NAME testguibrowser SOURCES TestGuiBrowser.cpp ../util/TemporaryFile.cpp LIBS ${TEST_LIBRARIES})
endif()

View File

@@ -0,0 +1,94 @@
#include "TestAttachmentWidget.h"
#include <attachments/AttachmentWidget.h>
#include <attachments/AttachmentTypes.h>
#include <attachments/ImageAttachmentsWidget.h>
#include <attachments/TextAttachmentsWidget.h>
#include <QLabel>
#include <QTest>
#include <QVBoxLayout>
void TestAttachmentsWidget::initTestCase()
{
m_attachmentWidget.reset(new AttachmentWidget());
QVERIFY(m_attachmentWidget);
}
void TestAttachmentsWidget::testTextAttachment()
{
for (const auto& attachment : {attachments::Attachment{.name = "Test.txt", .data = "Test"},
attachments::Attachment{.name = "Test.html", .data = "<h1> test </h1>"},
attachments::Attachment{.name = "Test.md", .data = "**bold**"}}) {
for (auto mode : {attachments::OpenMode::ReadWrite, attachments::OpenMode::ReadOnly}) {
m_attachmentWidget->openAttachment(attachment, mode);
QCoreApplication::processEvents();
auto layout = m_attachmentWidget->findChild<QVBoxLayout*>("verticalLayout");
QVERIFY(layout);
QCOMPARE(layout->count(), 1);
auto item = layout->itemAt(0);
QVERIFY(item);
QVERIFY(qobject_cast<TextAttachmentsWidget*>(item->widget()));
auto actualAttachment = m_attachmentWidget->getAttachment();
QCOMPARE(actualAttachment.name, attachment.name);
QCOMPARE(actualAttachment.data, attachment.data);
}
}
}
void TestAttachmentsWidget::testImageAttachment()
{
const auto Attachment = attachments::Attachment{.name = "Test.jpg", .data = QByteArray::fromHex("FFD8FF")};
m_attachmentWidget->openAttachment(Attachment, attachments::OpenMode::ReadWrite);
QCoreApplication::processEvents();
auto layout = m_attachmentWidget->findChild<QVBoxLayout*>("verticalLayout");
QVERIFY(layout);
QCOMPARE(layout->count(), 1);
auto item = layout->itemAt(0);
QVERIFY(item);
QVERIFY(qobject_cast<ImageAttachmentsWidget*>(item->widget()));
auto actualAttachment = m_attachmentWidget->getAttachment();
QCOMPARE(actualAttachment.name, Attachment.name);
QCOMPARE(actualAttachment.data, Attachment.data);
}
void TestAttachmentsWidget::testUnknownAttachment()
{
const auto Attachment = attachments::Attachment{.name = "Test", .data = QByteArray{"ID3"}};
m_attachmentWidget->openAttachment(Attachment, attachments::OpenMode::ReadWrite);
QCoreApplication::processEvents();
auto layout = m_attachmentWidget->findChild<QVBoxLayout*>("verticalLayout");
QVERIFY(layout);
QCOMPARE(layout->count(), 1);
auto item = layout->itemAt(0);
QVERIFY(item);
auto label = qobject_cast<QLabel*>(item->widget());
QVERIFY(label);
QVERIFY(!label->text().isEmpty());
auto actualAttachment = m_attachmentWidget->getAttachment();
QCOMPARE(actualAttachment.name, Attachment.name);
QCOMPARE(actualAttachment.data, Attachment.data);
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <attachments/AttachmentWidget.h>
#include <QObject>
#include <QScopedPointer>
class TestAttachmentsWidget : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testTextAttachment();
void testImageAttachment();
void testUnknownAttachment();
private:
QScopedPointer<AttachmentWidget> m_attachmentWidget;
};

View File

@@ -0,0 +1,46 @@
#include <QtTest>
#include "TestAttachmentWidget.h"
#include "TestEditEntryAttachmentsDialog.h"
#include "TestImageAttachmentsView.h"
#include "TestImageAttachmentsWidget.h"
#include "TestPreviewEntryAttachmentsDialog.h"
#include "TestTextAttachmentsEditWidget.h"
#include "TestTextAttachmentsPreviewWidget.h"
#include "TestTextAttachmentsWidget.h"
#include <config-keepassx.h>
#include <gui/Application.h>
int main(int argc, char* argv[])
{
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
Application app(argc, argv);
app.setApplicationName("KeePassXC");
app.setApplicationVersion(KEEPASSXC_VERSION);
app.setQuitOnLastWindowClosed(false);
app.setAttribute(Qt::AA_Use96Dpi, true);
app.applyTheme();
TestPreviewEntryAttachmentsDialog previewDialogTest{};
TestEditEntryAttachmentsDialog editDialogTest{};
TestTextAttachmentsWidget textAttachmentsWidget{};
TestTextAttachmentsPreviewWidget textPreviewWidget{};
TestTextAttachmentsEditWidget textEditWidget{};
TestImageAttachmentsWidget imageWidget{};
TestImageAttachmentsView imageView{};
TestAttachmentsWidget attachmentWidget{};
int result = 0;
result |= QTest::qExec(&previewDialogTest, argc, argv);
result |= QTest::qExec(&editDialogTest, argc, argv);
result |= QTest::qExec(&textAttachmentsWidget, argc, argv);
result |= QTest::qExec(&textPreviewWidget, argc, argv);
result |= QTest::qExec(&textEditWidget, argc, argv);
result |= QTest::qExec(&imageWidget, argc, argv);
result |= QTest::qExec(&imageView, argc, argv);
result |= QTest::qExec(&attachmentWidget, argc, argv);
return result;
}

View File

@@ -0,0 +1,95 @@
#include "TestEditEntryAttachmentsDialog.h"
#include <attachments/AttachmentWidget.h>
#include <PreviewEntryAttachmentsDialog.h>
#include <QAbstractButton>
#include <QDialogButtonBox>
#include <QSignalSpy>
#include <QSizePolicy>
#include <QTest>
#include <QTestMouseEvent>
#include <QVBoxLayout>
void TestEditEntryAttachmentsDialog::initTestCase()
{
m_editDialog.reset(new EditEntryAttachmentsDialog());
QVERIFY(m_editDialog);
}
void TestEditEntryAttachmentsDialog::testSetAttachment()
{
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
m_editDialog->setAttachment(Test);
QCoreApplication::processEvents();
QVERIFY2(m_editDialog->windowTitle().contains(Test.name), "Expected file name in the title");
auto layout = m_editDialog->findChild<QVBoxLayout*>("verticalLayout");
QVERIFY2(layout, "QVBoxLayout not found");
QCOMPARE(layout->count(), 2);
auto widget = qobject_cast<AttachmentWidget*>(layout->itemAt(0)->widget());
QVERIFY2(widget, "Expected AttachmentWidget");
auto sizePolicy = widget->sizePolicy();
QCOMPARE(sizePolicy.horizontalPolicy(), QSizePolicy::Expanding);
QCOMPARE(sizePolicy.verticalPolicy(), QSizePolicy::Expanding);
auto attachments = widget->getAttachment();
QCOMPARE(attachments.name, Test.name);
QCOMPARE(attachments.data, Test.data);
}
void TestEditEntryAttachmentsDialog::testSetAttachmentTwice()
{
const attachments::Attachment TestText{.name = "text.txt", .data = "Test"};
m_editDialog->setAttachment(TestText);
QCoreApplication::processEvents();
const attachments::Attachment TestImage{
.name = "test.jpg", .data = QByteArray::fromHex("FFD8FFE000104A46494600010101006000600000FFD9")};
m_editDialog->setAttachment(TestImage);
QCoreApplication::processEvents();
QVERIFY2(m_editDialog->windowTitle().contains(TestImage.name), "Expected file name in the title");
auto layout = m_editDialog->findChild<QVBoxLayout*>("verticalLayout");
QVERIFY2(layout, "QVBoxLayout not found");
QCOMPARE(layout->count(), 2);
auto widget = qobject_cast<AttachmentWidget*>(layout->itemAt(0)->widget());
QVERIFY2(widget, "Expected AttachmentWidget");
auto attachments = widget->getAttachment();
QCOMPARE(attachments.name, TestImage.name);
QCOMPARE(attachments.data, TestImage.data);
}
void TestEditEntryAttachmentsDialog::testBottonsBox()
{
const attachments::Attachment TestText{.name = "text.txt", .data = "Test"};
m_editDialog->setAttachment(TestText);
QCoreApplication::processEvents();
QSignalSpy acceptButton(m_editDialog.data(), &PreviewEntryAttachmentsDialog::accepted);
QSignalSpy closeButton(m_editDialog.data(), &PreviewEntryAttachmentsDialog::rejected);
auto buttonsBox = m_editDialog->findChild<QDialogButtonBox*>();
QVERIFY2(buttonsBox, "ButtonsBox not found");
for (auto button : buttonsBox->buttons()) {
QTest::mouseClick(button, Qt::LeftButton);
}
QCOMPARE(acceptButton.count(), 1);
QCOMPARE(closeButton.count(), 1);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "EditEntryAttachmentsDialog.h"
#include <QObject>
#include <QScopedPointer>
class TestEditEntryAttachmentsDialog : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testSetAttachment();
void testSetAttachmentTwice();
void testBottonsBox();
private:
QScopedPointer<EditEntryAttachmentsDialog> m_editDialog{};
};

View File

@@ -0,0 +1,76 @@
#include "TestImageAttachmentsView.h"
#include <attachments/ImageAttachmentsView.h>
#include <QBuffer>
#include <QPixmap>
#include <QSignalSpy>
#include <QTest>
#include <QWheelEvent>
void TestImageAttachmentsView::initTestCase()
{
m_view.reset(new ImageAttachmentsView());
// Generate the black rectange.
QImage image(1000, 1000, QImage::Format_RGB32);
image.fill(Qt::black);
auto scene = new QGraphicsScene();
scene->addPixmap(QPixmap::fromImage(image));
m_view->setScene(scene);
m_view->show();
QCoreApplication::processEvents();
}
void TestImageAttachmentsView::testEmitWheelEvent()
{
QSignalSpy ctrlWheelEvent{m_view.data(), &ImageAttachmentsView::ctrlWheelEvent};
QPoint center = m_view->rect().center();
m_view->setFocus();
QWheelEvent event(center, // local pos
m_view->mapToGlobal(center), // global pos
QPoint(0, 0),
QPoint(0, 120),
Qt::NoButton,
Qt::ControlModifier,
Qt::ScrollBegin,
false);
QCoreApplication::sendEvent(m_view->viewport(), &event);
QCOMPARE(ctrlWheelEvent.count(), 1);
}
void TestImageAttachmentsView::testEnableFit()
{
m_view->enableAutoFitInView();
QVERIFY(m_view->isAutoFitInViewActivated());
const auto oldTransform = m_view->transform();
m_view->resize(m_view->size() + QSize(100, 100));
QCoreApplication::processEvents();
QVERIFY(m_view->transform() != oldTransform);
}
void TestImageAttachmentsView::testDisableFit()
{
m_view->disableAutoFitInView();
QVERIFY(!m_view->isAutoFitInViewActivated());
const auto expectedTransform = m_view->transform();
m_view->resize(m_view->size() + QSize(100, 100));
QCoreApplication::processEvents();
QCOMPARE(m_view->transform(), expectedTransform);
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <attachments/ImageAttachmentsView.h>
#include <QObject>
#include <QScopedPointer>
class TestImageAttachmentsView : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testEmitWheelEvent();
void testEnableFit();
void testDisableFit();
private:
QScopedPointer<ImageAttachmentsView> m_view{};
};

View File

@@ -0,0 +1,244 @@
#include "TestImageAttachmentsWidget.h"
#include <attachments/ImageAttachmentsView.h>
#include <attachments/ImageAttachmentsWidget.h>
#include <QBuffer>
#include <QComboBox>
#include <QCoreApplication>
#include <QTest>
#include <QTransform>
void TestImageAttachmentsWidget::initTestCase()
{
m_widget.reset(new ImageAttachmentsWidget());
m_zoomCombobox = m_widget->findChild<QComboBox*>("zoomComboBox");
QVERIFY(m_zoomCombobox);
m_imageAttachmentsView = m_widget->findChild<ImageAttachmentsView*>("imagesView");
QVERIFY(m_imageAttachmentsView);
// Generate the black rectange.
QImage image(1000, 1000, QImage::Format_RGB32);
image.fill(Qt::black);
QByteArray imageBytes{};
QBuffer buffer(&imageBytes);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "PNG");
m_widget->openAttachment({.name = "black.png", .data = std::move(imageBytes)}, attachments::OpenMode::ReadOnly);
m_widget->show();
QCoreApplication::processEvents();
}
void TestImageAttachmentsWidget::testFitInView()
{
QCOMPARE(m_zoomCombobox->currentText(), tr("Fit"));
QVERIFY(m_imageAttachmentsView->isAutoFitInViewActivated());
auto zoomFactor = m_imageAttachmentsView->transform();
m_widget->setMinimumSize(m_widget->size() + QSize{100, 100});
QCoreApplication::processEvents();
QVERIFY(zoomFactor != m_imageAttachmentsView->transform());
}
void TestImageAttachmentsWidget::testZoomCombobox()
{
for (const auto zoom : {0.25, 0.5, 1.0, 2.0}) {
auto index = m_zoomCombobox->findData(zoom);
QVERIFY(index != -1);
m_zoomCombobox->setCurrentIndex(index);
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(zoom, zoom));
}
}
void TestImageAttachmentsWidget::testEditZoomCombobox()
{
for (double i = 0.25; i < 5; i += 0.25) {
m_zoomCombobox->setCurrentText(QString::number(i * 100));
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
}
}
void TestImageAttachmentsWidget::testEditWithPercentZoomCombobox()
{
// Example 100 %
for (double i = 0.25; i < 5; i += 0.25) {
m_zoomCombobox->setCurrentText(QString("%1 %").arg(i * 100));
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
}
// Example 100%
for (double i = 0.25; i < 5; i += 0.25) {
m_zoomCombobox->setCurrentText(QString("%1%").arg(i * 100));
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
}
}
void TestImageAttachmentsWidget::testInvalidValueZoomCombobox()
{
auto index = m_zoomCombobox->findData(1.0);
QVERIFY(index != -1);
m_zoomCombobox->setCurrentIndex(index);
QCoreApplication::processEvents();
const QTransform expectedTransform = m_imageAttachmentsView->transform();
for (const auto& invalidValue : {"Help", "3,4", "", ".", "% 100"}) {
m_zoomCombobox->setCurrentText(invalidValue);
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), expectedTransform);
}
}
void TestImageAttachmentsWidget::testZoomInByMouse()
{
QPoint center = m_imageAttachmentsView->rect().center();
// Set zoom: 100%
auto index = m_zoomCombobox->findData(1.0);
QVERIFY(index != -1);
m_zoomCombobox->setCurrentIndex(index);
m_imageAttachmentsView->setFocus();
QCoreApplication::processEvents();
const auto transform = m_imageAttachmentsView->transform();
QWheelEvent event(center, // local pos
m_imageAttachmentsView->mapToGlobal(center), // global pos
QPoint(0, 0),
QPoint(0, 120),
Qt::NoButton,
Qt::ControlModifier,
Qt::ScrollBegin,
false);
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
QCoreApplication::processEvents();
QTransform t = m_imageAttachmentsView->transform();
QVERIFY(t.m11() > transform.m11());
QVERIFY(t.m22() > transform.m22());
}
void TestImageAttachmentsWidget::testZoomOutByMouse()
{
QPoint center = m_imageAttachmentsView->rect().center();
// Set zoom: 100%
auto index = m_zoomCombobox->findData(1.0);
QVERIFY(index != -1);
m_zoomCombobox->setCurrentIndex(index);
m_imageAttachmentsView->setFocus();
QCoreApplication::processEvents();
const auto transform = m_imageAttachmentsView->transform();
QWheelEvent event(center, // local pos
center, // global pos
QPoint(0, 0),
QPoint(0, -120),
Qt::NoButton,
Qt::ControlModifier,
Qt::ScrollBegin,
true);
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
QCoreApplication::processEvents();
QTransform t = m_imageAttachmentsView->transform();
QVERIFY(t.m11() < transform.m11());
QVERIFY(t.m22() < transform.m22());
}
void TestImageAttachmentsWidget::testZoomLowerBound()
{
m_widget->setMinimumSize(100, 100);
QCoreApplication::processEvents();
auto minFactor = m_imageAttachmentsView->calculateFitInViewFactor();
// Set size less then minFactor
m_zoomCombobox->setCurrentText(QString::number((minFactor * 100.0) / 2));
QCoreApplication::processEvents();
const auto expectTransform = m_imageAttachmentsView->transform();
QPoint center = m_imageAttachmentsView->rect().center();
QWheelEvent event(center, // local pos
center, // global pos
QPoint(0, 0),
QPoint(0, -120),
Qt::NoButton,
Qt::ControlModifier,
Qt::ScrollBegin,
true);
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), expectTransform);
}
void TestImageAttachmentsWidget::testZoomUpperBound()
{
m_widget->setMinimumSize(100, 100);
// Set size less then minFactor
m_zoomCombobox->setCurrentText(QString::number(500));
QCoreApplication::processEvents();
const auto expectTransform = m_imageAttachmentsView->transform();
QPoint center = m_imageAttachmentsView->rect().center();
QWheelEvent event(center, // local pos
center, // global pos
QPoint(0, 0),
QPoint(0, 120),
Qt::NoButton,
Qt::ControlModifier,
Qt::ScrollBegin,
true);
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), expectTransform);
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <attachments/ImageAttachmentsWidget.h>
#include <QComboBox>
#include <QObject>
#include <QScopedPointer>
class ImageAttachmentsView;
class TestImageAttachmentsWidget : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testFitInView();
void testZoomCombobox();
void testEditZoomCombobox();
void testEditWithPercentZoomCombobox();
void testInvalidValueZoomCombobox();
void testZoomInByMouse();
void testZoomOutByMouse();
void testZoomLowerBound();
void testZoomUpperBound();
private:
QScopedPointer<ImageAttachmentsWidget> m_widget{};
QPointer<QComboBox> m_zoomCombobox{};
QPointer<ImageAttachmentsView> m_imageAttachmentsView{};
};

View File

@@ -0,0 +1,97 @@
#include "TestPreviewEntryAttachmentsDialog.h"
#include <attachments/AttachmentWidget.h>
#include <PreviewEntryAttachmentsDialog.h>
#include <QAbstractButton>
#include <QDialogButtonBox>
#include <QSignalSpy>
#include <QSizePolicy>
#include <QTest>
#include <QTestMouseEvent>
#include <QVBoxLayout>
void TestPreviewEntryAttachmentsDialog::initTestCase()
{
m_previewDialog.reset(new PreviewEntryAttachmentsDialog());
QVERIFY(m_previewDialog);
}
void TestPreviewEntryAttachmentsDialog::testSetAttachment()
{
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
m_previewDialog->setAttachment(Test);
QCoreApplication::processEvents();
QVERIFY2(m_previewDialog->windowTitle().contains(Test.name), "Expected file name in the title");
auto layout = m_previewDialog->findChild<QVBoxLayout*>("verticalLayout");
QVERIFY2(layout, "QVBoxLayout not found");
QCOMPARE(layout->count(), 2);
auto widget = qobject_cast<AttachmentWidget*>(layout->itemAt(0)->widget());
QVERIFY2(widget, "Expected AbstractAttachmentWidget");
auto sizePolicy = widget->sizePolicy();
QCOMPARE(sizePolicy.horizontalPolicy(), QSizePolicy::Expanding);
QCOMPARE(sizePolicy.verticalPolicy(), QSizePolicy::Expanding);
auto attachments = widget->getAttachment();
QCOMPARE(attachments.name, Test.name);
QCOMPARE(attachments.data, Test.data);
}
void TestPreviewEntryAttachmentsDialog::testSetAttachmentTwice()
{
const attachments::Attachment TestText{.name = "text.txt", .data = "Test"};
m_previewDialog->setAttachment(TestText);
QCoreApplication::processEvents();
const attachments::Attachment TestImage{
.name = "test.jpg", .data = QByteArray::fromHex("FFD8FFE000104A46494600010101006000600000FFD9")};
m_previewDialog->setAttachment(TestImage);
QCoreApplication::processEvents();
QVERIFY2(m_previewDialog->windowTitle().contains(TestImage.name), "Expected file name in the title");
auto layout = m_previewDialog->findChild<QVBoxLayout*>("verticalLayout");
QVERIFY2(layout, "QVBoxLayout not found");
QCOMPARE(layout->count(), 2);
auto widget = qobject_cast<AttachmentWidget*>(layout->itemAt(0)->widget());
QVERIFY2(widget, "Expected AbstractAttachmentWidget");
auto attachments = widget->getAttachment();
QCOMPARE(attachments.name, TestImage.name);
QCOMPARE(attachments.data, TestImage.data);
}
void TestPreviewEntryAttachmentsDialog::testBottonsBox()
{
const attachments::Attachment TestText{.name = "text.txt", .data = "Test"};
m_previewDialog->setAttachment(TestText);
QCoreApplication::processEvents();
QSignalSpy saveButton(m_previewDialog.data(), &PreviewEntryAttachmentsDialog::saveAttachment);
QSignalSpy openButton(m_previewDialog.data(), &PreviewEntryAttachmentsDialog::openAttachment);
QSignalSpy closeButton(m_previewDialog.data(), &PreviewEntryAttachmentsDialog::rejected);
auto buttonsBox = m_previewDialog->findChild<QDialogButtonBox*>("dialogButtons");
QVERIFY2(buttonsBox, "ButtonsBox not found");
for (auto button : buttonsBox->buttons()) {
QTest::mouseClick(button, Qt::LeftButton);
}
QCOMPARE(saveButton.count(), 1);
QCOMPARE(openButton.count(), 1);
QCOMPARE(closeButton.count(), 1);
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "PreviewEntryAttachmentsDialog.h"
#include <QObject>
#include <qscopedpointer.h>
class TestPreviewEntryAttachmentsDialog : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testSetAttachment();
void testSetAttachmentTwice();
void testBottonsBox();
private:
QScopedPointer<PreviewEntryAttachmentsDialog> m_previewDialog{};
};

View File

@@ -0,0 +1,49 @@
#include "TestTextAttachmentsEditWidget.h"
#include <attachments/TextAttachmentsEditWidget.h>
#include <QComboBox>
#include <QMouseEvent>
#include <QPushButton>
#include <QSignalSpy>
#include <QTest>
#include <QTestMouseEvent>
#include <QTextEdit>
void TestTextAttachmentsEditWidget::initTestCase()
{
m_widget.reset(new TextAttachmentsEditWidget());
}
void TestTextAttachmentsEditWidget::testEmitTextChanged()
{
QSignalSpy textChangedSignal(m_widget.data(), &TextAttachmentsEditWidget::textChanged);
m_widget->openAttachment({.name = "test.txt", .data = {}}, attachments::OpenMode::ReadWrite);
QCoreApplication::processEvents();
auto textEdit = m_widget->findChild<QTextEdit*>("attachmentsTextEdit");
QVERIFY(textEdit);
const QByteArray NewText = "New test text";
textEdit->setText(NewText);
QVERIFY(textChangedSignal.count() > 0);
}
void TestTextAttachmentsEditWidget::testEmitPreviewButtonClicked()
{
QSignalSpy previwButtonClickedSignal(m_widget.data(), &TextAttachmentsEditWidget::previewButtonClicked);
m_widget->openAttachment({.name = "test.txt", .data = {}}, attachments::OpenMode::ReadWrite);
QCoreApplication::processEvents();
auto previewButton = m_widget->findChild<QPushButton*>("previewPushButton");
QVERIFY(previewButton);
QTest::mouseClick(previewButton, Qt::LeftButton);
QCOMPARE(previwButtonClickedSignal.count(), 1);
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <attachments/TextAttachmentsEditWidget.h>
#include <QObject>
#include <QScopedPointer>
class TestTextAttachmentsEditWidget : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testEmitTextChanged();
void testEmitPreviewButtonClicked();
private:
QScopedPointer<TextAttachmentsEditWidget> m_widget{};
};

View File

@@ -0,0 +1,40 @@
#include "TestTextAttachmentsPreviewWidget.h"
#include <attachments/TextAttachmentsPreviewWidget.h>
#include <QComboBox>
#include <QTest>
void TestTextAttachmentsPreviewWidget::initTestCase()
{
m_widget.reset(new TextAttachmentsPreviewWidget());
}
void TestTextAttachmentsPreviewWidget::testDetectMimeByFile()
{
const auto combobox = m_widget->findChild<QComboBox*>("typeComboBox");
QVERIFY(combobox);
const attachments::Attachment Text{.name = "test.txt", .data = {}};
m_widget->openAttachment(Text, attachments::OpenMode::ReadOnly);
QCoreApplication::processEvents();
QCOMPARE(combobox->currentData().toInt(), TextAttachmentsPreviewWidget::PlainText);
const attachments::Attachment Html{.name = "test.html", .data = {}};
m_widget->openAttachment(Html, attachments::OpenMode::ReadOnly);
QCoreApplication::processEvents();
QCOMPARE(combobox->currentData().toInt(), TextAttachmentsPreviewWidget::Html);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
const attachments::Attachment Markdown{.name = "test.md", .data = {}};
m_widget->openAttachment(Markdown, attachments::OpenMode::ReadOnly);
QCoreApplication::processEvents();
QCOMPARE(combobox->currentData().toInt(), TextAttachmentsPreviewWidget::Markdown);
#endif
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <attachments/TextAttachmentsPreviewWidget.h>
#include <QObject>
#include <QScopedPointer>
class TestTextAttachmentsPreviewWidget : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testDetectMimeByFile();
private:
QScopedPointer<TextAttachmentsPreviewWidget> m_widget{};
};

View File

@@ -0,0 +1,224 @@
#include "TestTextAttachmentsWidget.h"
#include <attachments/TextAttachmentsEditWidget.h>
#include <attachments/TextAttachmentsPreviewWidget.h>
#include <attachments/TextAttachmentsWidget.h>
#include <QPushButton>
#include <QSignalSpy>
#include <QSplitter>
#include <QTest>
#include <QTestMouseEvent>
#include <QTextEdit>
#include <QTimer>
void TestTextAttachmentsWidget::initTestCase()
{
m_textWidget.reset(new TextAttachmentsWidget());
}
void TestTextAttachmentsWidget::testInitTextWidget()
{
auto splitter = m_textWidget->findChild<QSplitter*>();
QVERIFY2(splitter, "Splitter not found");
QCOMPARE(splitter->count(), 2);
QVERIFY2(qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0)), "EditTextWidget not found");
QVERIFY2(qobject_cast<TextAttachmentsPreviewWidget*>(splitter->widget(1)), "PreviewTextWidget not found");
}
void TestTextAttachmentsWidget::testTextReadWriteWidget()
{
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadWrite);
m_textWidget->show();
QCoreApplication::processEvents();
auto splitter = m_textWidget->findChild<QSplitter*>();
QVERIFY2(splitter, "Splitter not found");
auto sizes = splitter->sizes();
QCOMPARE(sizes.size(), 2);
QVERIFY2(sizes[0] > 0, "EditTextWidget width must be greater than zero");
QCOMPARE(sizes[1], 0);
auto widget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
QVERIFY(widget);
auto attachments = widget->getAttachment();
QCOMPARE(attachments.name, Test.name);
QCOMPARE(attachments.data, Test.data);
auto previewWidget = qobject_cast<TextAttachmentsPreviewWidget*>(splitter->widget(1));
QVERIFY(previewWidget);
attachments = previewWidget->getAttachment();
QCOMPARE(attachments.name, Test.name);
QCOMPARE(attachments.data, Test.data);
}
void TestTextAttachmentsWidget::testTextReadWidget()
{
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadOnly);
m_textWidget->show();
QCoreApplication::processEvents();
auto splitter = m_textWidget->findChild<QSplitter*>();
QVERIFY2(splitter, "Splitter not found");
auto sizes = splitter->sizes();
QCOMPARE(sizes.size(), 2);
QVERIFY2(sizes[1] > 0, "PreviewTextWidget width must be greater then zero");
QVERIFY(splitter->widget(0)->isHidden());
auto widget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
QVERIFY(widget);
auto attachments = widget->getAttachment();
QCOMPARE(attachments.name, Test.name);
QCOMPARE(attachments.data, Test.data);
auto previewWidget = qobject_cast<TextAttachmentsPreviewWidget*>(splitter->widget(1));
QVERIFY(previewWidget);
attachments = previewWidget->getAttachment();
QCOMPARE(attachments.name, Test.name);
QCOMPARE(attachments.data, Test.data);
}
void TestTextAttachmentsWidget::testTextChanged()
{
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadWrite);
QCoreApplication::processEvents();
auto splitter = m_textWidget->findChild<QSplitter*>();
QVERIFY2(splitter, "Splitter not found");
QCOMPARE(splitter->sizes().size(), 2);
auto editWidget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
QVERIFY2(editWidget, "Edit widget not found");
auto textEdit = editWidget->findChild<QTextEdit*>();
QVERIFY(textEdit);
const QByteArray NewText = "New test text";
textEdit->setText(NewText);
QCoreApplication::processEvents();
auto attachments = m_textWidget->getAttachment();
QCOMPARE(attachments.data, NewText);
}
void TestTextAttachmentsWidget::testTextChangedInReadOnlyMode()
{
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadOnly);
QCoreApplication::processEvents();
auto splitter = m_textWidget->findChild<QSplitter*>();
QVERIFY2(splitter, "Splitter not found");
QCOMPARE(splitter->sizes().size(), 2);
auto editWidget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
QVERIFY2(editWidget, "Edit widget not found");
auto textEdit = editWidget->findChild<QTextEdit*>();
QVERIFY(textEdit);
const QByteArray NewText = "New test text";
textEdit->setText(NewText);
QCoreApplication::processEvents();
auto attachments = m_textWidget->getAttachment();
QCOMPARE(attachments.data, Test.data);
}
void TestTextAttachmentsWidget::testPreviewTextChanged()
{
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
auto previewTimer = m_textWidget->findChild<QTimer*>();
QVERIFY2(previewTimer, "PreviewTimer not found!");
QSignalSpy timeout(previewTimer, &QTimer::timeout);
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadWrite);
// Waiting for the first timeout
while (timeout.count() < 1) {
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
}
auto splitter = m_textWidget->findChild<QSplitter*>();
QVERIFY2(splitter, "Splitter not found");
QCOMPARE(splitter->sizes().size(), 2);
splitter->setSizes({1, 1});
QCoreApplication::processEvents();
auto editWidget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
QVERIFY2(editWidget, "Edit widget not found");
auto textEdit = editWidget->findChild<QTextEdit*>();
QVERIFY(textEdit);
const QByteArray NewText = "New test text";
textEdit->setText(NewText);
// Waiting for the second timeout
while (timeout.count() < 2) {
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
}
auto previewWidget = qobject_cast<TextAttachmentsPreviewWidget*>(splitter->widget(1));
auto attachments = previewWidget->getAttachment();
QCOMPARE(attachments.data, NewText);
}
void TestTextAttachmentsWidget::testOpenPreviewButton()
{
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadWrite);
m_textWidget->show();
QCoreApplication::processEvents();
auto splitter = m_textWidget->findChild<QSplitter*>();
QVERIFY2(splitter, "Splitter not found");
QCOMPARE(splitter->sizes().size(), 2);
auto editWidget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
QVERIFY2(editWidget, "Edit widget not found");
QVERIFY(editWidget->isVisible());
auto previewButton = editWidget->findChild<QPushButton*>("previewPushButton");
auto sizes = splitter->sizes();
QVERIFY(sizes[0] > 0);
QCOMPARE(sizes[1], 0);
QTest::mouseClick(previewButton, Qt::LeftButton);
sizes = splitter->sizes();
QCOMPARE(sizes.size(), 2);
QVERIFY(sizes[0] > 0);
QVERIFY(sizes[1] > 0);
QTest::mouseClick(previewButton, Qt::LeftButton);
sizes = splitter->sizes();
QCOMPARE(sizes.size(), 2);
QVERIFY(sizes[0] > 0);
QCOMPARE(sizes[1], 0);
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <attachments/TextAttachmentsWidget.h>
#include <QObject>
#include <QScopedPointer>
class TestTextAttachmentsWidget : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void testInitTextWidget();
void testTextReadWriteWidget();
void testTextReadWidget();
void testOpenPreviewButton();
void testPreviewTextChanged();
void testTextChanged();
void testTextChangedInReadOnlyMode();
private:
QScopedPointer<TextAttachmentsWidget> m_textWidget;
};