#include "Document/EditorSession.h" #include "IO/CanvasResizer.h" #include "IO/ImageExporter.h" #include "IO/ImageResizer.h" #include #include // The session cases of Swift's GroupTests. namespace { std::vector rowIDs(const EditorSession &session) { std::vector result; for (const LayerHierarchy::Entry &row : session.layerRows()) result.push_back(row.layer.id); return result; } std::vector renderIDs(const EditorSession &session) { std::vector result; for (const ImageLayer &layer : session.document().value().renderLayers()) result.push_back(layer.id); return result; } std::optional parentOf(const EditorSession &session, QUuid id) { for (const ImageLayer &layer : session.document().value().layers) { if (layer.id == id) return layer.parentID; } throw std::runtime_error("no such layer"); } } class GroupSessionTests : public QObject { Q_OBJECT private slots: void nestedGroupsMoveOutCollapseAndDeleteUndo(); void layersArePlacedAboveBelowAndInsideFolders(); void placingRespectsTheNestingLimit(); void hiddenParentOverridesChildrenAndExportOrderFollowsGroups(); void groupsRoundTripAndSurviveImageAndCanvasResize(); void rowsOfASessionWithoutADocument(); }; void GroupSessionTests::nestedGroupsMoveOutCollapseAndDeleteUndo() { EditorSession session; session.createDocument(100, 100); session.addGroup(); const QUuid outer = session.activeLayerID().value(); session.addGroup(); const QUuid inner = session.activeLayerID().value(); session.addBlankLayer(); const QUuid child = session.activeLayerID().value(); QCOMPARE(session.activeLayer().value().parentID, std::optional(inner)); QVERIFY(!session.placeLayer(outer, inner)); QVERIFY(!session.placeLayer(inner, inner)); // The panel asks before a drop; validation never sees these. QVERIFY(!session.canPlaceLayer(outer, inner)); QVERIFY(!session.canPlaceLayer(inner, inner)); QVERIFY(!session.canPlaceLayer(outer, child)); QVERIFY(!session.canPlaceLayer(outer, QUuid::createUuid())); QVERIFY(session.canPlaceLayer(child, outer)); session.toggleGroupExpansion(outer); QCOMPARE(rowIDs(session), std::vector{outer}); QCOMPARE(session.activeLayerID(), std::optional(outer)); session.toggleGroupExpansion(outer); std::vector depths; for (const LayerHierarchy::Entry &row : session.layerRows()) depths.push_back(row.depth); QCOMPARE(depths, (std::vector{0, 1, 2})); session.selectLayer(child); session.moveActiveLayerOutOfGroup(); QCOMPARE(session.activeLayer().value().parentID, std::optional(outer)); QCOMPARE(session.history.undoName(), QString("Move Layer")); session.undo(); QCOMPARE(parentOf(session, child), std::optional(inner)); session.selectLayer(outer); session.deleteActiveLayer(); QVERIFY(session.document().value().layers.empty()); session.undo(); QCOMPARE(int(session.document().value().layers.size()), 3); QCOMPARE(parentOf(session, child), std::optional(inner)); } void GroupSessionTests::layersArePlacedAboveBelowAndInsideFolders() { EditorSession session; session.createDocument(100, 100); session.addBlankLayer(); const QUuid bottom = session.activeLayerID().value(); session.addBlankLayer(); const QUuid middle = session.activeLayerID().value(); session.addGroup(); const QUuid folder = session.activeLayerID().value(); session.addBlankLayer(); const QUuid inside = session.activeLayerID().value(); // To the top of the root, then to its bottom. QVERIFY(session.placeLayer(bottom, std::nullopt)); QCOMPARE(session.document().value().layers.back().id, bottom); QCOMPARE(session.activeLayerID(), std::optional(bottom)); QVERIFY(session.placeLayer(bottom, std::nullopt, std::nullopt, true)); QCOMPARE(session.document().value().layers.front().id, bottom); // Just above a sibling; the target shares the new parent. QVERIFY(session.placeLayer(bottom, std::nullopt, middle)); QCOMPARE(rowIDs(session), (std::vector{folder, inside, bottom, middle})); QVERIFY(!session.placeLayer(bottom, std::nullopt, inside)); // A target beats the bottom. QVERIFY(session.placeLayer(bottom, std::nullopt, middle, true)); QCOMPARE(rowIDs(session), (std::vector{folder, inside, bottom, middle})); QVERIFY(!session.placeLayer(bottom, std::nullopt, bottom)); QVERIFY(!session.placeLayer(bottom, std::nullopt, QUuid::createUuid())); // Into a folded folder, which opens, above a layer inside. session.selectLayer(middle); session.toggleGroupExpansion(folder); QVERIFY(session.placeLayer(bottom, folder, inside)); QVERIFY(!session.collapsedGroupIDs().contains(folder)); QCOMPARE(parentOf(session, bottom), std::optional(folder)); QCOMPARE(rowIDs(session), (std::vector{folder, bottom, inside, middle})); // Only folders take layers; strangers and busy sessions move none. QVERIFY(!session.placeLayer(bottom, middle)); QVERIFY(!session.canPlaceLayer(bottom, middle)); QVERIFY(!session.placeLayer(QUuid::createUuid(), std::nullopt)); QVERIFY(!session.placeLayer(bottom, QUuid::createUuid())); QVERIFY(session.canPlaceLayer(bottom, std::nullopt) && session.canPlaceLayer(bottom, folder)); session.setIsImporting(true); QVERIFY(!session.canPlaceLayer(bottom, std::nullopt)); QVERIFY(!session.placeLayer(bottom, std::nullopt)); // Outside a folder there is no group to leave. session.setIsImporting(false); session.selectLayer(middle); const int count = session.history.undoCount(); session.moveActiveLayerOutOfGroup(); QCOMPARE(session.history.undoCount(), count); // A layer leaves its folder to sit just above it. session.selectLayer(std::nullopt); session.addBlankLayer(); const QUuid top = session.activeLayerID().value(); session.selectLayer(inside); session.moveActiveLayerOutOfGroup(); QCOMPARE(rowIDs(session), (std::vector{top, inside, folder, bottom, middle})); } void GroupSessionTests::placingRespectsTheNestingLimit() { EditorSession session; session.createDocument(8, 8); // Each new folder goes inside the active one: 64 deep. for (int depth = 0; depth < 64; ++depth) session.addGroup(); const QUuid deepest = session.activeLayerID().value(); QCOMPARE(int(session.document().value().layers.size()), 64); session.selectLayer(std::nullopt); session.addGroup(); const QUuid folder = session.activeLayerID().value(); session.selectLayer(std::nullopt); session.addBlankLayer(); const QUuid leaf = session.activeLayerID().value(); const std::optional before = session.document(); // A folder may go no deeper; a layer still may. QVERIFY(session.canPlaceLayer(folder, deepest)); QVERIFY(!session.placeLayer(folder, deepest)); QCOMPARE(session.document(), before); QVERIFY(session.placeLayer(leaf, deepest)); QCOMPARE(parentOf(session, leaf), std::optional(deepest)); } void GroupSessionTests::hiddenParentOverridesChildrenAndExportOrderFollowsGroups() { EditorSession session; session.createDocument(64, 32); session.addGroup(); const QUuid group = session.activeLayerID().value(); QTemporaryDir folder; QImage fixture(64, 32, QImage::Format_RGBA8888); fixture.fill(Qt::red); QVERIFY(fixture.save(folder.filePath("fixture.png"), "PNG")); bool done = false; session.importImages({QUrl::fromLocalFile(folder.filePath("fixture.png"))}, std::nullopt, [&] { done = true; }); QVERIFY(QTest::qWaitFor([&] { return done; }, 10'000)); const QUuid child = session.activeLayerID().value(); QCOMPARE(session.activeLayer().value().parentID, std::optional(group)); const ImageIdentity source = session.activeLayer().value().asset.value().identity(); session.toggleLayerVisibility(group); QVERIFY(session.activeLayer().value().isVisible); QVERIFY(session.document().value().renderLayers().empty()); QVERIFY(!session.canTransform()); // A hidden folder exports nothing of what it holds. const QImage hiddenExport = QImage::fromData(ImageExporter::pngData(session.projectSnapshot().value()), "PNG"); QCOMPARE(hiddenExport.size(), QSize(64, 32)); QCOMPARE(hiddenExport.pixelColor(0, 0).alpha(), 0); session.toggleLayerVisibility(group); QCOMPARE(renderIDs(session), std::vector{child}); QCOMPARE(QImage::fromData(ImageExporter::pngData(session.projectSnapshot().value()), "PNG").pixelColor(0, 0), QColor(255, 0, 0)); QVERIFY(session.activeLayer().value().asset.value().identity() == source); session.selectLayer(std::nullopt); session.addGroup(); const QUuid other = session.activeLayerID().value(); session.addBlankLayer(); const QUuid otherChild = session.activeLayerID().value(); QCOMPARE(renderIDs(session), (std::vector{child, otherChild})); session.selectLayer(other); session.moveActiveLayer(-1); QCOMPARE(renderIDs(session), (std::vector{otherChild, child})); QCOMPARE(parentOf(session, otherChild), std::optional(other)); } void GroupSessionTests::groupsRoundTripAndSurviveImageAndCanvasResize() { EditorSession session; session.createDocument(20, 10); session.addGroup(); const QUuid group = session.activeLayerID().value(); session.renameLayer(group, "Artwork"); session.addBlankLayer(); const QUuid child = session.activeLayerID().value(); const ProjectSnapshot snapshot = session.projectSnapshot().value(); QCOMPARE(snapshot.manifest.version, qint64(10)); const auto record = [](const ProjectSnapshot &from, QUuid id) { for (const ProjectLayerRecord &layer : from.manifest.layers) { if (layer.id == id) return layer; } throw std::runtime_error("no such record"); }; QTemporaryDir folder; const QString path = folder.filePath("Groups.comp"); ProjectStore::save(snapshot, path); const ProjectSnapshot loaded = ProjectStore::load(path); QCOMPARE(record(loaded, group).isGroup, std::optional(true)); QCOMPARE(record(loaded, group).name, QString("Artwork")); QCOMPARE(record(loaded, child).parentID, std::optional(group)); const ProjectSnapshot resized = ImageResizer::resize(loaded, {.width = 40, .height = 20, .resolution = 72}); const ProjectSnapshot cropped = CanvasResizer::resize(resized, {.width = 30, .height = 15}); QCOMPARE(record(cropped, child).parentID, std::optional(group)); QCOMPARE(record(cropped, group).isGroup, std::optional(true)); // A version 1 project keeps its version through the store. ProjectSnapshot legacy{.manifest = {.documentID = QUuid::createUuid(), .width = 20, .height = 10, .activeLayerID = std::nullopt, .layers = {}}, .images = {}}; legacy.manifest.version = 1; ProjectStore::save(legacy, path); QCOMPARE(ProjectStore::load(path).manifest.version, qint64(1)); } void GroupSessionTests::rowsOfASessionWithoutADocument() { QVERIFY(EditorSession().layerRows().empty()); } QTEST_GUILESS_MAIN(GroupSessionTests) #include "GroupSessionTests.moc"