feat(Storybook): read Figma links directly from pages

This commit is contained in:
Michał Cieślak 2023-10-03 11:48:40 +02:00 committed by Michał
parent 158bb87b4a
commit 853641fb89
15 changed files with 71 additions and 878 deletions

View File

@ -45,11 +45,9 @@ set(PROJECT_LIB "${PROJECT_NAME}Lib")
add_library(${PROJECT_LIB}
cachecleaner.cpp cachecleaner.h
directorieswatcher.cpp directorieswatcher.h
figmadecoratormodel.cpp figmadecoratormodel.h
figmaio.cpp figmaio.h
figmalinks.cpp figmalinks.h
figmalinksmodel.cpp figmalinksmodel.h
figmalinkssource.cpp figmalinkssource.h
modelutils.cpp modelutils.h
pagesmodel.h pagesmodel.cpp
sectionsdecoratormodel.cpp sectionsdecoratormodel.h
@ -99,10 +97,6 @@ add_executable(SectionsDecoratorModelTest tests/tst_SectionsDecoratorModel.cpp)
target_link_libraries(SectionsDecoratorModelTest PRIVATE Qt5::Test ${PROJECT_LIB})
add_test(NAME SectionsDecoratorModelTest COMMAND SectionsDecoratorModelTest)
add_executable(FigmaDecoratorModelTest tests/tst_FigmaDecoratorModel.cpp)
target_link_libraries(FigmaDecoratorModelTest PRIVATE Qt5::Test ${PROJECT_LIB})
add_test(NAME FigmaModelTest COMMAND FigmaModelTest)
add_executable(QmlTests
qmlTests/main.cpp
qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h

View File

@ -1,94 +0,0 @@
#include "figmadecoratormodel.h"
#include "figmalinks.h"
#include "figmalinksmodel.h"
#include "modelutils.h"
FigmaDecoratorModel::FigmaDecoratorModel(QObject *parent)
: QIdentityProxyModel{parent}
{
}
QHash<int, QByteArray> FigmaDecoratorModel::roleNames() const
{
auto roles = QIdentityProxyModel::roleNames();
roles.insert(FigmaRole, QByteArrayLiteral("figma"));
return roles;
}
QVariant FigmaDecoratorModel::data(const QModelIndex &proxyIndex, int role) const
{
if (!checkIndex(proxyIndex, CheckIndexOption::IndexIsValid))
return {};
if (role == FigmaRole) {
static FigmaLinksModel empty({});
if (!m_titleRole)
return QVariant::fromValue(&empty);
const auto title = data(proxyIndex, m_titleRole.value()).toString();
auto it = m_submodels.find(title);
if (it == m_submodels.end()) {
QStringList links;
if (m_figmaLinks)
links = m_figmaLinks->getLinksMap().value(title, {});
auto linksModel = new FigmaLinksModel(
links, const_cast<FigmaDecoratorModel*>(this));
it = m_submodels.insert(title, linksModel);
}
return QVariant::fromValue(it.value());
}
return QIdentityProxyModel::data(proxyIndex, role);
}
FigmaLinks* FigmaDecoratorModel::getFigmaLinks() const
{
return m_figmaLinks;
}
void FigmaDecoratorModel::setFigmaLinks(FigmaLinks *figmaLinks)
{
if (figmaLinks == m_figmaLinks)
return;
m_figmaLinks = figmaLinks;
const auto& linksMap = m_figmaLinks
? m_figmaLinks->getLinksMap()
: QMap<QString, QStringList>{};
auto linksIt = linksMap.constBegin();
while (linksIt != linksMap.constEnd()) {
if (m_submodels.contains(linksIt.key()))
m_submodels.value(linksIt.key())->setContent(linksIt.value());
++linksIt;
}
auto submodelsIt = m_submodels.constBegin();
while (submodelsIt != m_submodels.constEnd()) {
if (!linksMap.contains(submodelsIt.key()))
submodelsIt.value()->setContent({});
++submodelsIt;
}
emit figmaLinksChanged();
}
void FigmaDecoratorModel::setSourceModel(QAbstractItemModel *sourceModel)
{
qDeleteAll(m_submodels);
m_submodels.clear();
m_titleRole = ModelUtils::findRole(QByteArrayLiteral("title"), sourceModel);
if(!m_titleRole)
qWarning("The source model is missing title role!");
QIdentityProxyModel::setSourceModel(sourceModel);
}

View File

@ -1,34 +0,0 @@
#pragma once
#include <QIdentityProxyModel>
#include <optional>
class FigmaLinks;
class FigmaLinksModel;
class FigmaDecoratorModel : public QIdentityProxyModel
{
Q_OBJECT
Q_PROPERTY(FigmaLinks* figmaLinks READ getFigmaLinks
WRITE setFigmaLinks NOTIFY figmaLinksChanged)
public:
static constexpr auto FigmaRole = Qt::UserRole + 100;
explicit FigmaDecoratorModel(QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &proxyIndex, int role) const override;
FigmaLinks* getFigmaLinks() const;
void setFigmaLinks(FigmaLinks *figmaLinks);
void setSourceModel(QAbstractItemModel *sourceModel) override;
signals:
void figmaLinksChanged();
private:
std::optional<int> m_titleRole;
FigmaLinks* m_figmaLinks = nullptr;
mutable QMap<QString, FigmaLinksModel*> m_submodels;
};

View File

@ -1,139 +0,0 @@
#include "figmalinkssource.h"
#include <QQmlEngine>
#include "figmaio.h"
#include "figmalinks.h"
FigmaLinksSource::FigmaLinksSource(QObject *parent)
: QObject{parent}
{
connect(&m_watcher, &QFileSystemWatcher::fileChanged,
this, [this](const QString &path) {
this->readFile();
if (!this->m_watcher.files().contains(path))
this->m_watcher.addPath(path);
});
}
const QUrl& FigmaLinksSource::getFilePath() const
{
return m_filePath;
}
void FigmaLinksSource::setFilePath(const QUrl& path)
{
if (path == m_filePath)
return;
m_filePath = path;
readFile();
setupWatcher();
emit filePathChanged();
}
FigmaLinks* FigmaLinksSource::getFigmaLinks() const
{
return m_figmaLinks;
}
void FigmaLinksSource::remove(const QString &key, const QList<int> &indexes)
{
if (m_filePath.isEmpty()) {
qWarning("FigmaLinksSource::remove - file path is not set!");
return;
}
QMap<QString, QStringList> linksMap;
if (m_figmaLinks)
linksMap = m_figmaLinks->getLinksMap();
auto it = linksMap.find(key);
if (it == linksMap.end()) {
qWarning("FigmaLinksSource::remove - provided key doesn't exist!");
return;
}
if (indexes.isEmpty())
return;
auto indexesSorted = indexes;
std::sort(indexesSorted.begin(), indexesSorted.end());
if (std::adjacent_find(indexesSorted.cbegin(), indexesSorted.cend())
!= indexesSorted.cend()) {
qWarning("FigmaLinksSource::remove - provided indexes list contains duplicates!");
return;
}
auto& linksList = it.value();
if (indexesSorted.first() < 0 || indexesSorted.last() >= linksList.size()) {
qWarning("FigmaLinksSource::remove - at least one provided index is out of range!");
return;
}
if (linksList.size() == indexesSorted.size()) {
linksMap.erase(it);
} else {
std::for_each(std::crbegin(indexesSorted), std::crend(indexesSorted),
[&linksList](int idx) {
linksList.removeAt(idx);
});
}
FigmaIO::write(m_filePath.path(), linksMap);
}
void FigmaLinksSource::append(const QString &key, const QList<QString> &links)
{
QMap<QString, QStringList> linksMap;
if (m_filePath.isEmpty()) {
qWarning("FigmaLinksSource::append - file path is not set!");
return;
}
if (m_figmaLinks)
linksMap = m_figmaLinks->getLinksMap();
linksMap[key].append(links);
FigmaIO::write(m_filePath.path(), linksMap);
}
void FigmaLinksSource::updateFigmaLinks(const QMap<QString, QStringList>& map)
{
FigmaLinks *mapping = new FigmaLinks(map, this);
if (m_figmaLinks && qjsEngine(m_figmaLinks)) {
m_figmaLinks->setParent(nullptr);
QQmlEngine::setObjectOwnership(m_figmaLinks, QQmlEngine::JavaScriptOwnership);
}
m_figmaLinks = mapping;
emit figmaLinksChanged();
}
void FigmaLinksSource::readFile()
{
QMap<QString, QStringList> figmaLinks = FigmaIO::read(m_filePath.path());
updateFigmaLinks(figmaLinks);
}
void FigmaLinksSource::setupWatcher()
{
auto currentlyWatched = m_watcher.files();
if (!currentlyWatched.isEmpty())
m_watcher.removePaths(currentlyWatched);
if (m_filePath.isEmpty())
return;
m_watcher.addPath(m_filePath.path());
}

View File

@ -1,37 +0,0 @@
#pragma once
#include <QObject>
#include <QFileSystemWatcher>
#include <QUrl>
class FigmaLinks;
class FigmaLinksSource : public QObject
{
Q_OBJECT
Q_PROPERTY(QUrl filePath READ getFilePath WRITE setFilePath NOTIFY filePathChanged)
Q_PROPERTY(FigmaLinks* figmaLinks READ getFigmaLinks NOTIFY figmaLinksChanged)
public:
explicit FigmaLinksSource(QObject *parent = nullptr);
const QUrl& getFilePath() const;
void setFilePath(const QUrl& path);
FigmaLinks* getFigmaLinks() const;
Q_INVOKABLE void remove(const QString &key, const QList<int> &indexes);
Q_INVOKABLE void append(const QString &key, const QList<QString> &links);
signals:
void filePathChanged();
void figmaLinksChanged();
private:
void updateFigmaLinks(const QMap<QString, QStringList>& map);
void readFile();
void setupWatcher();
FigmaLinks *m_figmaLinks = nullptr;
QUrl m_filePath;
QFileSystemWatcher m_watcher;
};

View File

@ -4,9 +4,7 @@
#include "cachecleaner.h"
#include "directorieswatcher.h"
#include "figmadecoratormodel.h"
#include "figmalinks.h"
#include "figmalinkssource.h"
#include "pagesmodel.h"
#include "sectionsdecoratormodel.h"
@ -46,8 +44,6 @@ int main(int argc, char *argv[])
engine.rootContext()->setContextProperty(
"pagesFolder", QML_IMPORT_ROOT + QStringLiteral("/pages"));
qmlRegisterType<FigmaDecoratorModel>("Storybook", 1, 0, "FigmaDecoratorModel");
qmlRegisterType<FigmaLinksSource>("Storybook", 1, 0, "FigmaLinksSource");
qmlRegisterType<PagesModelInitialized>("Storybook", 1, 0, "PagesModel");
qmlRegisterType<SectionsDecoratorModel>("Storybook", 1, 0, "SectionsDecoratorModel");
qmlRegisterUncreatableType<FigmaLinks>("Storybook", 1, 0, "FigmaLinks", {});

View File

@ -1,6 +1,6 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Qt.labs.settings 1.0
@ -78,19 +78,6 @@ ApplicationWindow {
id: pagesModel
}
FigmaLinksSource {
id: figmaLinksSource
filePath: "figma.json"
}
FigmaDecoratorModel {
id: figmaModel
sourceModel: pagesModel
figmaLinks: figmaLinksSource.figmaLinks
}
HotReloader {
id: reloader
@ -212,7 +199,7 @@ ApplicationWindow {
id: currentPageModelItem
model: SingleItemProxyModel {
sourceModel: figmaModel
sourceModel: pagesModel
roleName: "title"
value: root.currentPage
}
@ -320,9 +307,6 @@ Tips:
figmaLinksCache: figmaImageLinksCache
}
onRemoveLinksRequested: figmaLinksSource.remove(pageTitle, indexes)
onAppendLinksRequested: figmaLinksSource.append(pageTitle, links)
onClosing: Qt.callLater(destroy)
}
}

View File

@ -1,6 +1,6 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Storybook 1.0
@ -50,63 +50,13 @@ Pane {
}
}
ColumnLayout {
ImagesGridView {
id: grid
anchors.fill: parent
RowLayout {
Layout.fillWidth: true
CheckBox {
id: selectableCheckBox
Layout.alignment: Qt.AlignVCenter
text: "selectable"
}
ToolSeparator {
Layout.alignment: Qt.AlignVCenter
}
Label {
id: selectionText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
property string selectionAsString: ""
text: `selected indexes: [${selectionAsString}]`
Connections {
target: grid.selection
function onSelectionChanged() {
const indexes = grid.selection.selectedIndexes
const rows = indexes.map(idx => idx.row)
selectionText.selectionAsString = rows.join(", ")
}
}
}
Button {
text: "Clear selection"
onClicked: grid.selection.clear()
}
}
ImagesGridView {
id: grid
Layout.fillWidth: true
Layout.fillHeight: true
selectable: selectableCheckBox.checked
clip: true
model: imagesModel
}
clip: true
model: imagesModel
}
}

View File

@ -4,6 +4,8 @@
#include <QFileSystemWatcher>
#include <QRegularExpression>
#include <unordered_map>
namespace {
const auto categoryUncategorized QStringLiteral("Uncategorized");
}
@ -15,6 +17,10 @@ PagesModel::PagesModel(const QString &path, QObject *parent)
m_items = load();
readMetadata(m_items);
for (const auto& item : qAsConst(m_items)) {
setFigmaLinks(item.title, item.figmaLinks);
}
fsWatcher->addPath(path);
connect(fsWatcher, &QFileSystemWatcher::directoryChanged,
@ -62,6 +68,21 @@ void PagesModel::readMetadata(PagesModelItem& item) {
? categoryMatch.captured(2).trimmed() : categoryUncategorized;
item.category = category;
static QRegularExpression figmaRegex(
"^(\\/\\/\\s*)((?:https:\\/\\/)?(?:www\\.)?figma\\.com\\/.*)$",
QRegularExpression::MultilineOption);
QRegularExpressionMatchIterator i = figmaRegex.globalMatch(content);
QStringList links;
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
QString link = match.captured(2);
links << link;
}
item.figmaLinks = links;
}
void PagesModel::readMetadata(QList<PagesModelItem> &items) {
@ -129,6 +150,7 @@ void PagesModel::reload() {
auto index = std::distance(m_items.begin(), it);
const auto& previous = *it;
readMetadata(item);
setFigmaLinks(item.title, item.figmaLinks);
if (previous.category != item.category) {
// For simplicity category change is handled by removing and
@ -149,7 +171,8 @@ QHash<int, QByteArray> PagesModel::roleNames() const
{
static const QHash<int,QByteArray> roles {
{ TitleRole, QByteArrayLiteral("title") },
{ CategoryRole, QByteArrayLiteral("category") }
{ CategoryRole, QByteArrayLiteral("category") },
{ FigmaRole, QByteArrayLiteral("figma") }
};
return roles;
@ -167,8 +190,28 @@ QVariant PagesModel::data(const QModelIndex &index, int role) const
if (role == TitleRole)
return m_items.at(index.row()).title;
if (role == CategoryRole)
return m_items.at(index.row()).category;
if (role == FigmaRole) {
auto title = m_items.at(index.row()).title;
auto it = m_figmaSubmodels.find(title);
assert(it != m_figmaSubmodels.end());
return QVariant::fromValue(it.value());
}
return {};
}
void PagesModel::setFigmaLinks(const QString& title, const QStringList& links)
{
auto it = m_figmaSubmodels.find(title);
if (it == m_figmaSubmodels.end()) {
m_figmaSubmodels.insert(title, new FigmaLinksModel(links, this));
} else {
it.value()->setContent(links);
}
}

View File

@ -3,6 +3,8 @@
#include <QAbstractListModel>
#include <QDateTime>
#include "figmalinksmodel.h"
class QFileSystemWatcher;
struct PagesModelItem {
@ -10,6 +12,7 @@ struct PagesModelItem {
QDateTime lastModified;
QString title;
QString category;
QStringList figmaLinks;
};
class PagesModel : public QAbstractListModel
@ -20,7 +23,8 @@ public:
enum Roles {
TitleRole = Qt::UserRole + 1,
CategoryRole
CategoryRole,
FigmaRole
};
QHash<int, QByteArray> roleNames() const override;
@ -34,7 +38,10 @@ private:
static void readMetadata(PagesModelItem &item);
static void readMetadata(QList<PagesModelItem> &items);
void setFigmaLinks(const QString& title, const QStringList& links);
QString m_path;
QList<PagesModelItem> m_items;
QMap<QString, FigmaLinksModel*> m_figmaSubmodels;
QFileSystemWatcher* fsWatcher;
};

View File

@ -1,9 +1,9 @@
import QtQuick 2.14
import QtQuick 2.15
ListModel {
id: root
/* required */ property FigmaLinksCache figmaLinksCache
required property FigmaLinksCache figmaLinksCache
property alias sourceModel: d.model
readonly property Instantiator _d: Instantiator {
@ -29,7 +29,7 @@ ListModel {
d.idCounter++
figmaLinksCache.getImageUrl(model.link, link => {
if (delegate)
if (delegate && link !== null)
root.setProperty(model.index, "imageLink", link)
})
}

View File

@ -1,4 +1,4 @@
import QtQml 2.14
import QtQml 2.15
QtObject {
id: root

View File

@ -14,10 +14,6 @@ ApplicationWindow {
signal removeLinksRequested(var indexes)
signal appendLinksRequested(var links)
readonly property var urlRegex:
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/
SwipeView {
id: topSwipeView
@ -40,37 +36,6 @@ ApplicationWindow {
topSwipeView.incrementCurrentIndex()
}
}
footer: ToolBar {
RowLayout {
anchors.fill: parent
Button {
id: removeButton
readonly property int selectionCount:
grid.selection.selectedIndexes.length
text: "Remove selected"
+ (enabled ? ` (${selectionCount})` : "")
enabled: grid.selection.hasSelection
onClicked: removeConfirmDialog.open()
}
ToolSeparator {}
Button {
text: "Add new links"
onClicked: addNewLinksDialog.open()
}
Item {
Layout.fillWidth: true
}
}
}
}
Item {
@ -106,81 +71,4 @@ ApplicationWindow {
}
}
}
Dialog {
id: removeConfirmDialog
readonly property var selected: grid.selection.selectedIndexes
anchors.centerIn: Overlay.overlay
title: "Links removal"
standardButtons: Dialog.Ok | Dialog.Cancel
Label {
text: "Are you sure that you want to remove "
+ removeButton.selectionCount + " link(s)?"
}
onAccepted: root.removeLinksRequested(selected.map(idx => idx.row))
onSelectedChanged: close()
}
Dialog {
id: addNewLinksDialog
anchors.centerIn: Overlay.overlay
title: "Add new Figma links"
standardButtons: Dialog.Save | Dialog.Cancel
width: parent.width * 0.8
height: parent.height * 0.4
GroupBox {
anchors.fill: parent
title: "Figma links, 1 per line"
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
contentHeight: linksTextEdit.implicitHeight
contentWidth: linksTextEdit.implicitWidth
TextEdit {
id: linksTextEdit
property var links: []
width: scrollView.width
height: scrollView.height
font.pixelSize: 13
selectByMouse: true
onTextChanged: {
const allLines = text.split("\n")
const nonEmptyLines = allLines.filter(
line => line.trim().length > 0)
const trimmed = nonEmptyLines.map(line => line.trim())
links = trimmed.every(line => root.urlRegex.test(line))
? trimmed : []
}
}
}
}
onClosed: Qt.callLater(linksTextEdit.clear)
onAccepted: root.appendLinksRequested(linksTextEdit.links)
Component.onCompleted: {
standardButton(Dialog.Save).enabled
= Qt.binding(() => linksTextEdit.links.length > 0)
}
}
}

View File

@ -1,6 +1,6 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQml.Models 2.14
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQml.Models 2.15
GridView {
id: root
@ -10,15 +10,6 @@ GridView {
signal clicked(int index)
property bool selectable: true
readonly property alias selection: selectionModel
ItemSelectionModel {
id: selectionModel
model: root.model
}
delegate: Item {
width: root.cellWidth
height: root.cellHeight
@ -60,21 +51,6 @@ GridView {
ToolTip.visible: hovered
ToolTip.text: model.rawLink
}
CheckBox {
visible: root.selectable
anchors.top: parent.top
anchors.right: parent.right
checked: {
selectionModel.selection
return selectionModel.isSelected(root.model.index(index, 0))
}
onToggled: selectionModel.select(root.model.index(index, 0),
ItemSelectionModel.Toggle)
}
}
}
}

View File

@ -1,341 +0,0 @@
#include <QSignalSpy>
#include <QTest>
#include <QTemporaryFile>
#include <QStringListModel>
#include "figmadecoratormodel.h"
#include "figmaio.h"
#include "figmalinks.h"
#include "figmalinksmodel.h"
#include "figmalinkssource.h"
namespace {
auto constexpr sampleJson1 = R"(
{
"Component_1": [
"link_1", "link_2"
],
"Component_2": [
"link_3", "link_4"
]
}
)";
auto constexpr sampleJson2 = R"(
{
"Component_1": [
"link_1"
],
"Component_2": [
"link_3", "link_5"
]
}
)";
class TestSourceModel : public QAbstractListModel {
public:
static constexpr auto TitleRole = 0;
TestSourceModel(int count = 1) : m_count(count) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return m_count;
}
QVariant data(const QModelIndex &index, int role) const override {
if (!index.isValid())
return {};
return QString("title_%1").arg(index.row());
}
QHash<int, QByteArray> roleNames() const override {
QHash<int, QByteArray> roles;
roles.insert(TitleRole, QByteArrayLiteral("title"));
return roles;
}
int m_count;
};
} // unnamed namespace
class FigmaDecoratorModelTest: public QObject
{
Q_OBJECT
private slots:
void figmaIOTest() {
QTemporaryFile file;
QVERIFY(file.open());
file.close();
const auto readEmpty = FigmaIO::read(file.fileName());
QCOMPARE(readEmpty, {});
const QMap<QString, QStringList> content = {
{ "k_1", { "l_1", "l_2"}},
{ "k_2", { "l_3", "l_4"}}
};
FigmaIO::write(file.fileName(), content);
const auto readContent = FigmaIO::read(file.fileName());
QCOMPARE(readContent, content);
}
void readingFigmaFileTest() {
FigmaLinksSource figmaLinksSource;
QSignalSpy spy(&figmaLinksSource, &FigmaLinksSource::figmaLinksChanged);
QCOMPARE(figmaLinksSource.getFigmaLinks(), nullptr);
QTemporaryFile file;
if (file.open()) {
QTextStream stream(&file);
stream << sampleJson1;
}
figmaLinksSource.setFilePath(file.fileName());
QVERIFY(figmaLinksSource.getFigmaLinks() != nullptr);
const FigmaLinks *links = figmaLinksSource.getFigmaLinks();
QCOMPARE(links->getLinksMap(), (QMap<QString, QStringList> {
{{"Component_1"}, {"link_1", "link_2"}},
{{"Component_2"}, {"link_3", "link_4"}}}));
QCOMPARE(spy.count(), 1);
QTemporaryFile file2;
if (file2.open()) {
QTextStream stream(&file2);
stream << sampleJson2;
}
figmaLinksSource.setFilePath(file2.fileName());
QVERIFY(figmaLinksSource.getFigmaLinks() != nullptr);
const FigmaLinks *links2 = figmaLinksSource.getFigmaLinks();
QCOMPARE(links2->getLinksMap(), (QMap<QString, QStringList> {
{{"Component_1"}, {"link_1"}},
{{"Component_2"}, {"link_3", "link_5"}}}));
QCOMPARE(spy.count(), 2);
}
void readingAfterFigmaFileChangedTest() {
FigmaLinksSource figmaLinksSource;
QSignalSpy spy(&figmaLinksSource, &FigmaLinksSource::figmaLinksChanged);
QCOMPARE(figmaLinksSource.getFigmaLinks(), nullptr);
QTemporaryFile file;
if (file.open()) {
QTextStream stream(&file);
stream << sampleJson1;
}
figmaLinksSource.setFilePath(file.fileName());
QVERIFY(figmaLinksSource.getFigmaLinks() != nullptr);
const FigmaLinks *links = figmaLinksSource.getFigmaLinks();
QCOMPARE(links->getLinksMap(), (QMap<QString, QStringList> {
{{"Component_1"}, {"link_1", "link_2"}},
{{"Component_2"}, {"link_3", "link_4"}}}));
QCOMPARE(spy.count(), 1);
if (QFile f(file.fileName());
f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QTextStream stream(&f);
stream << sampleJson2;
}
QVERIFY(spy.wait());
QCOMPARE(spy.count(), 2);
const FigmaLinks *links2 = figmaLinksSource.getFigmaLinks();
QCOMPARE(links2->getLinksMap(), (QMap<QString, QStringList> {
{{"Component_1"}, {"link_1"}},
{{"Component_2"}, {"link_3", "link_5"}}}));
if (QFile f(file.fileName());
f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QTextStream stream(&f);
stream << sampleJson1;
}
QVERIFY(spy.wait());
QCOMPARE(spy.count(), 3);
const FigmaLinks *links3 = figmaLinksSource.getFigmaLinks();
QCOMPARE(links3->getLinksMap(), (QMap<QString, QStringList> {
{{"Component_1"}, {"link_1", "link_2"}},
{{"Component_2"}, {"link_3", "link_4"}}}));
}
void emptyFigmaModelTest() {
FigmaDecoratorModel model;
QCOMPARE(model.rowCount(), 0);
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
QCOMPARE(model.roleNames().value(
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
}
void figmaModelWithoutSourceModel() {
FigmaLinks links({
{{"Component_1"}, {"link_1", "link_2"}},
{{"Component_2"}, {"link_3", "link_4"}}
});
FigmaDecoratorModel model;
model.setFigmaLinks(&links);
QCOMPARE(model.rowCount(), 0);
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
QCOMPARE(model.roleNames().value(
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
}
void figmaModelWithIncompatibleSourceModel() {
QStringListModel stringsModel({"s1", "s2"});
FigmaDecoratorModel model;
QTest::ignoreMessage(QtWarningMsg,
"The source model is missing title role!");
model.setSourceModel(&stringsModel);
QCOMPARE(model.rowCount(), 2);
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
QCOMPARE(model.roleNames().value(
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
QCOMPARE(model.data(
model.index(0, 0),
FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>()->rowCount(), 0);
}
void figmaModelWithoutLinksTest() {
TestSourceModel sourceModel;
FigmaDecoratorModel model;
model.setSourceModel(&sourceModel);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.roleNames().count(), 2);
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
QCOMPARE(model.roleNames().value(
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
QCOMPARE(model.data(
model.index(0, 0),
FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>()->rowCount(), 0);
}
void figmaModelTest() {
TestSourceModel sourceModel{2};
FigmaLinks links({
{{"title_0"}, {"link_1", "link_2"}},
{{"title_x"}, {"link_3", "link_4"}}
});
FigmaDecoratorModel model;
QSignalSpy spy(&model, &FigmaDecoratorModel::dataChanged);
model.setSourceModel(&sourceModel);
QCOMPARE(spy.size(), 0);
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.roleNames().count(), 2);
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
QVERIFY(model.roleNames().contains(TestSourceModel::TitleRole));
QCOMPARE(model.roleNames().value(
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
QCOMPARE(model.roleNames().value(
TestSourceModel::TitleRole), QStringLiteral("title"));
QCOMPARE(model.data(model.index(0, 0),
TestSourceModel::TitleRole).toString(), "title_0");
QCOMPARE(model.data(model.index(1, 0),
TestSourceModel::TitleRole).toString(), "title_1");
auto figmaLinksModel1 = model.data(model.index(0, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>();
QVERIFY(figmaLinksModel1 != nullptr);
QCOMPARE(figmaLinksModel1->rowCount(), 0);
auto figmaLinksModel2 = model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>();
QVERIFY(figmaLinksModel2 != nullptr);
QCOMPARE(figmaLinksModel2->rowCount(), 0);
QSignalSpy linksModelspy1(figmaLinksModel1,
&QAbstractItemModel::modelReset);
QSignalSpy linksModelspy2(figmaLinksModel2,
&QAbstractItemModel::modelReset);
model.setFigmaLinks(&links);
QCOMPARE(spy.size(), 0);
QCOMPARE(linksModelspy1.size(), 1);
QCOMPARE(linksModelspy2.size(), 0);
QCOMPARE(model.data(model.index(0, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>(), figmaLinksModel1);
QCOMPARE(model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>(), figmaLinksModel2);
QCOMPARE(figmaLinksModel1->rowCount(), 2);
QCOMPARE(figmaLinksModel2->rowCount(), 0);
QCOMPARE(model.data(model.index(0, 0),
TestSourceModel::TitleRole).toString(), "title_0");
QCOMPARE(model.data(model.index(1, 0),
TestSourceModel::TitleRole).toString(), "title_1");
QCOMPARE(figmaLinksModel1->roleNames().size(), 1);
QCOMPARE(figmaLinksModel2->roleNames().size(), 1);
QCOMPARE(figmaLinksModel1->data(figmaLinksModel1->index(0, 0),
FigmaLinksModel::LinkRole).toString(), "link_1");
QCOMPARE(figmaLinksModel1->data(figmaLinksModel1->index(1, 0),
FigmaLinksModel::LinkRole).toString(), "link_2");
model.setFigmaLinks(nullptr);
QCOMPARE(spy.size(), 0);
QCOMPARE(linksModelspy1.size(), 2);
QCOMPARE(linksModelspy2.size(), 0);
QCOMPARE(model.data(model.index(0, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>(), figmaLinksModel1);
QCOMPARE(model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole)
.value<QAbstractItemModel*>(), figmaLinksModel2);
QCOMPARE(figmaLinksModel1->rowCount(), 0);
QCOMPARE(figmaLinksModel2->rowCount(), 0);
}
};
QTEST_MAIN(FigmaDecoratorModelTest)
#include "tst_FigmaDecoratorModel.moc"