13.19h: the acceptance run checks the seventeen rows new in 1.4.5 through the window's own controls; shots retaken

This commit is contained in:
ZacharyZhang-NY committed 2026-10-03 23:45:53 -04:00
1 parent 8bd3fc3264
commit c59e85babc
23 files changed
+709 -39

No files matched your search

+6 -1
View File
@@ -65,7 +65,12 @@ void AcceptanceColorTests::cameraRawDocked()
FilterSettings settings = app.session().filterSettings();
settings.cameraRaw.exposure = 1;
app.session().updateFilter(settings, true);
QTRY_VERIFY_WITH_TIMEOUT(app.session().filterEdit().value().previewImage(layer), 30000);
// The shot waits for the brightened preview.
const auto brightened = [&app, layer] {
const std::optional<QImage> preview = app.session().filterEdit().value().previewImage(layer);
return preview && preview.value().pixelColor(preview.value().width() / 2, preview.value().height() * 7 / 8).green() > 170;
};
QTRY_VERIFY_WITH_TIMEOUT(brightened(), 30000);
app.shot(QStringLiteral("06-camera-raw"));
bool done = false;
app.session().commitFilter([&done] { done = true; });
+239
View File
@@ -0,0 +1,239 @@
#include "AcceptanceFixtures.h"
#include "Document/LayerAppearance.h"
#include "Document/SmudgeLiquify.h"
#include "UI/NativeLayerList.h"
#include "UI/TransformInspector.h"
#include "UI/TypeControls.h"
#include <QCheckBox>
#include <QLineEdit>
// The acceptance run, part five: 1.4.5's editing.
class AcceptanceEditTests : public QObject {
Q_OBJECT
private slots:
void initTestCase() { QStandardPaths::setTestModeEnabled(true); }
void textPaintingSmearAndMasks();
void moveKeysAndBlending();
};
namespace {
QImage filled(int width, int height, QColor color)
{
QImage image = BrushRaster::context(width, height, false);
image.fill(color);
return image;
}
QUuid add(App &app, const QImage &image, const QString &name, QPointF centre)
{
app.session().insert(ImportedImage(image, image, name), centre);
return app.session().activeLayerID().value();
}
int red(App &app, QUuid id, int x, int y)
{
for (const ImageLayer &layer : app.session().document().value().layers)
if (layer.id == id)
return qRed(layer.asset.value().image().pixel(x, y));
throw std::runtime_error("no such layer");
}
// The canvas's pixel at a document point, at 100%.
QColor onCanvas(App &app, QPointF point)
{
const QPointF view = app.session().viewport.viewPoint(point + QPointF(0.5, 0.5), app.session().document().value().size());
return app.canvas().grab().toImage().pixelColor(int(std::floor(view.x())), int(std::floor(view.y())));
}
bool near(QColor a, QColor b)
{
return qAbs(a.red() - b.red()) <= 2 && qAbs(a.green() - b.green()) <= 2 && qAbs(a.blue() - b.blue()) <= 2;
}
}
void AcceptanceEditTests::textPaintingSmearAndMasks()
{
App app;
app.session().createDocument(1200, 800, true);
// A face for the selected word, a preview, and Undo.
app.session().selectTool(NavigationTool::type);
app.press(QPointF(100, 200));
app.release(QPointF(100, 200));
QTRY_VERIFY(app.session().textDraft());
QTest::keyClicks(&app.canvas(), QStringLiteral("Hello World"));
for (int letter = 0; letter < 5; ++letter)
QTest::keyClick(&app.canvas(), Qt::Key_Left, Qt::ShiftModifier);
auto *faces = app.window.findChild<TypeFontPicker *>(QStringLiteral("typeFont"));
QVERIFY(faces);
const QString mono = QStringLiteral("DejaVuSansMono");
// Pointing at a face previews it; Escape puts it back.
QTest::mouseMove(faces->view()->viewport(), popupRow(*faces, mono));
QTRY_COMPARE(app.session().textDraft().value().style.fontNameAt(6), mono);
QTest::keyClick(faces->view(), Qt::Key_Escape);
QTRY_VERIFY(app.session().textDraft().value().style.fontNameAt(6) != mono);
choose(*faces, mono);
QTRY_COMPARE(app.session().textDraft().value().style.fontRuns.value(), (std::vector<LayerTextFontRun>{{6, 5, mono}}));
app.window.activateWindow();
QVERIFY(QTest::qWaitForWindowActive(&app.window));
app.canvas().setFocus();
QTRY_VERIFY(app.canvas().hasFocus());
QTest::keyClick(&app.canvas(), Qt::Key_End);
QTest::keyClicks(&app.canvas(), QStringLiteral("!!"));
QTest::keyClick(&app.canvas(), Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(app.session().textDraft().value().style.content, QStringLiteral("Hello World"));
QTest::keyClick(&app.canvas(), Qt::Key_Return, Qt::ControlModifier);
QVERIFY(!app.session().textDraft());
// A brush paints in a scaled-down layer's own pixels.
const QUuid half = add(app, filled(800, 800, Qt::black), QStringLiteral("Half"), QPointF(600, 400));
app.session().beginTransform();
LayerTransform quarter = app.session().activeLayer().value().transform;
quarter.origin = QPointF(500, 300);
quarter.size = QSizeF(200, 200);
app.session().previewTransform(quarter);
app.session().commitTransform();
app.session().selectTool(NavigationTool::brush);
app.session().setBrushSettings({.diameter = 20, .hardness = 1, .red = 1, .green = 1, .blue = 1});
app.drag(QPointF(600, 330), QPointF(600, 470));
app.settle();
QCOMPARE(app.session().activeLayer().value().asset.value().size(), QSize(800, 800));
int painted = 0;
for (int x = 0; x < 800; ++x)
painted += red(app, half, x, 400) > 128;
QVERIFY2(painted >= 76 && painted <= 84, qPrintable(QString::number(painted)));
// A hidden layer says why it cannot paint.
app.session().toggleLayerVisibility(half);
app.press(QPointF(560, 400));
app.release(QPointF(560, 400));
QVERIFY(app.session().brushError().value().contains(QStringLiteral("is hidden")));
app.session().setBrushError(std::nullopt);
app.session().toggleLayerVisibility(half);
// The Blur brush's Radius sets how far it softens.
QImage step = filled(400, 200, Qt::black);
QPainter(&step).fillRect(200, 0, 200, 200, Qt::white);
const QUuid edge = add(app, step, QStringLiteral("Edge"), QPointF(200, 600));
const auto softened = [&](const QString &radius) {
app.session().selectTool(NavigationTool::blur);
app.session().setBlurMode(BlurToolMode::blur);
app.session().setBrushSettings({.diameter = 120, .hardness = 1, .opacity = 1});
// The bar's own Radius field.
auto *field = app.window.findChild<QLineEdit *>(QStringLiteral("blurRadius"));
field->setFocus();
field->selectAll();
QTest::keyClicks(field, radius);
QTest::keyClick(field, Qt::Key_Return);
app.drag(QPointF(200, 520), QPointF(200, 680));
app.settle();
int soft = 0;
for (int x = 100; x < 300; ++x)
soft += red(app, edge, x, 100) > 10 && red(app, edge, x, 100) < 245;
app.action("undo").trigger();
return soft;
};
const int narrow = softened(QStringLiteral("2")), wide = softened(QStringLiteral("20"));
QVERIFY2(narrow < 16 && wide > 40, qPrintable(QString("%1 %2").arg(narrow).arg(wide)));
// Liquify keeps the stripes sharp as it pushes them.
QImage stripes = filled(400, 200, Qt::black);
for (int x = 0; x < 400; x += 20)
QPainter(&stripes).fillRect(x, 0, 10, 200, Qt::white);
const QUuid striped = add(app, stripes, QStringLiteral("Stripes"), QPointF(800, 600));
app.session().selectTool(NavigationTool::blur);
app.session().setBlurMode(BlurToolMode::liquify);
app.session().setBrushSettings({.diameter = 80, .hardness = 0.5, .opacity = 0.7});
app.drag(QPointF(720, 600), QPointF(840, 600));
app.settle();
int moved = 0, grey = 0;
for (int x = 0; x < 400; ++x) {
const int value = red(app, striped, x, 100);
moved += value != qRed(stripes.pixel(x, 100));
grey += value > 40 && value < 215;
}
QVERIFY2(moved > 20 && grey * 5 < moved, qPrintable(QString("%1 %2").arg(moved).arg(grey)));
// Smudge leaves one fading trail, no copies along it.
QImage block = filled(400, 200, Qt::white);
QPainter(&block).fillRect(0, 0, 100, 200, Qt::red);
const QUuid smeared = add(app, block, QStringLiteral("Block"), QPointF(200, 200));
app.session().setBlurMode(BlurToolMode::smudge);
app.session().setBrushSettings({.diameter = 60, .hardness = 0.5, .opacity = 0.8});
app.drag(QPointF(50, 200), QPointF(300, 200));
app.settle();
const auto green = [&](int x) {
for (const ImageLayer &layer : app.session().document().value().layers)
if (layer.id == smeared)
return qGreen(layer.asset.value().image().pixel(x, 100));
throw std::runtime_error("no such layer");
};
QVERIFY2(green(110) < 200, qPrintable(QString::number(green(110))));
for (int x = 110; x < 300; x += 10)
QVERIFY2(green(x + 10) >= green(x) - 3, qPrintable(QString("%1: %2 then %3").arg(x).arg(green(x)).arg(green(x + 10))));
app.session().selectLayer(striped);
// The mask button reveals; Alt hides; Alt-click shows alone.
app.session().selectTool(NavigationTool::marquee);
app.drag(QPointF(650, 520), QPointF(750, 680));
auto *mask = app.window.findChild<QToolButton *>(QStringLiteral("addLayerMask"));
QTest::mouseClick(mask, Qt::LeftButton);
QImage shown = app.session().activeLayer().value().mask.value().asset.image();
QCOMPARE(qGray(shown.pixel(100, 100)), 255);
QCOMPARE(qGray(shown.pixel(5, 100)), 0);
app.action("undo").trigger();
QTest::mouseClick(mask, Qt::LeftButton, Qt::AltModifier);
shown = app.session().activeLayer().value().mask.value().asset.image();
QVERIFY(qGray(shown.pixel(100, 100)) == 0 && qGray(shown.pixel(5, 100)) == 255);
LayerThumbnailButton *thumbnail = nullptr;
for (LayerCell *cell : app.window.findChild<NativeLayerList *>()->cells())
if (cell->layerID() == striped)
thumbnail = cell->findChild<LayerThumbnailButton *>("maskThumbnail");
QVERIFY(thumbnail);
QTest::mouseClick(thumbnail, Qt::LeftButton, Qt::AltModifier, thumbnail->rect().center());
QCOMPARE(app.session().maskAloneLayer().value().id, striped);
QTRY_VERIFY(app.window.findChild<QWidget *>(QStringLiteral("maskAloneBadge"))->isVisible());
// The canvas shows the mask: black where hidden, white past.
QTRY_COMPARE(onCanvas(app, QPointF(700, 600)), QColor(Qt::black));
QCOMPARE(onCanvas(app, QPointF(620, 600)), QColor(Qt::white));
QCOMPARE(onCanvas(app, QPointF(50, 150)), QColor(Qt::white));
app.shot(QStringLiteral("18-mask-alone"));
}
void AcceptanceEditTests::moveKeysAndBlending()
{
App app;
QVERIFY(scene(1200, 800).save(app.path("photo.png")));
app.session().createDocument(1200, 800);
app.importFile(app.path("photo.png"));
// Held Ctrl flips Auto Select in the bar.
app.session().selectTool(NavigationTool::move);
auto *autoSelect = app.window.findChild<QCheckBox *>(QStringLiteral("transformAutoSelect"));
QVERIFY(autoSelect);
const bool before = autoSelect->isChecked();
QTest::keyPress(&app.canvas(), Qt::Key_Control, Qt::ControlModifier);
QCOMPARE(autoSelect->isChecked(), !before);
QTest::keyRelease(&app.canvas(), Qt::Key_Control, Qt::NoModifier);
QCOMPARE(autoSelect->isChecked(), before);
// A typed width applies as one step on leaving.
auto *width = app.window.findChild<QLineEdit *>(QStringLiteral("transformW"));
QVERIFY(width);
const int steps = app.session().history.undoCount();
width->setFocus();
QTRY_VERIFY(width->hasFocus());
width->selectAll();
QTest::keyClicks(width, QStringLiteral("600"));
app.canvas().setFocus();
QTRY_COMPARE(app.session().activeLayer().value().transform.size.width(), 600.0);
QCOMPARE(app.session().history.undoCount(), steps + 1);
app.action("undo").trigger();
// Soft Light and a Linear Dodge stack show as exported.
app.session().zoom(1);
const QUuid grey = add(app, filled(1200, 800, QColor(77, 77, 77)), QStringLiteral("Grey"), QPointF(600, 400));
app.session().setLayerBlendMode(LayerBlendMode::softLight);
const QList<QPointF> points{QPointF(600, 400), QPointF(300, 100), QPointF(900, 700)};
for (const QPointF point : points)
QTRY_VERIFY2(near(onCanvas(app, point), app.shown(int(point.x()), int(point.y()))), qPrintable(onCanvas(app, point).name()));
add(app, filled(1200, 800, QColor(0, 0, 160, 160)), QStringLiteral("Clipped"), QPointF(600, 400));
app.session().toggleClippingMask(app.session().activeLayerID().value());
app.session().selectLayer(grey);
app.session().setLayerBlendMode(LayerBlendMode::linearDodge);
for (const QPointF point : points)
QTRY_VERIFY2(near(onCanvas(app, point), app.shown(int(point.x()), int(point.y()))), qPrintable(onCanvas(app, point).name()));
}
QTEST_MAIN(AcceptanceEditTests)
#include "AcceptanceEditTests.moc"
+203
View File
@@ -0,0 +1,203 @@
#include "AcceptanceFixtures.h"
#include "DialogDesk.h"
#include "UI/JPEGExportSheet.h"
#include <QComboBox>
#include <map>
#include <QLineEdit>
#include <QPushButton>
#include <QToolButton>
// The acceptance run, part six: 1.4.5's filters, export, Quit.
class AcceptanceFinishTests : public QObject {
Q_OBJECT
private slots:
void initTestCase() { QStandardPaths::setTestModeEnabled(true); }
void cameraRawCurvesAndDither();
void jpegPreviewAndQuitMidEdit();
};
namespace {
QUuid photo(App &app)
{
if (!scene(1200, 800).save(app.path("photo.png")))
throw std::runtime_error("the photo could not be written");
app.session().createDocument(1200, 800);
app.importFile(app.path("photo.png"));
return app.session().activeLayerID().value();
}
void drag(QWidget *widget, QPoint from, QPoint to)
{
QTest::mousePress(widget, Qt::LeftButton, Qt::NoModifier, from);
for (int step = 1; step <= 4; ++step) {
const QPointF at = from + (to - from) * (step / 4.0);
QMouseEvent event(QEvent::MouseMove, at, widget->mapToGlobal(at), Qt::NoButton, Qt::LeftButton, Qt::NoModifier);
QApplication::sendEvent(widget, &event);
}
QTest::mouseRelease(widget, Qt::LeftButton, Qt::NoModifier, to);
}
// A shown widget of that name, once its panel shows.
template <typename Widget> Widget &shownChild(App &app, const char *name)
{
Widget *shown = nullptr;
static_cast<void>(QTest::qWaitFor([&] {
for (Widget *each : app.window.findChildren<Widget *>(QString::fromLatin1(name)))
if (each->isVisible())
shown = each;
return shown != nullptr;
}, 5000));
if (!shown)
throw std::runtime_error(std::string("no shown ") + name);
return *shown;
}
// The filter panel's OK, then the commit's landing.
bool committed(App &app)
{
shownChild<QPushButton>(app, "filterOK").click();
return QTest::qWaitFor([&app] { return !app.session().filterEdit() && !app.session().isProjectBusy(); }, 30000);
}
void typeInto(App &app, const char *name, const QString &text, bool submit)
{
auto &field = shownChild<QLineEdit>(app, name);
field.setFocus();
field.selectAll();
QTest::keyClicks(&field, text);
if (submit)
QTest::keyClick(&field, Qt::Key_Return);
}
// The first glyph in the ground: its box, inked share.
std::pair<QRect, double> glyph(const QImage &image)
{
const QRect ground(400, 640, 400, 120);
std::map<QRgb, int> counts;
for (int y = ground.top(); y < ground.bottom(); ++y)
for (int x = ground.left(); x < ground.right(); ++x)
++counts[image.pixel(x, y)];
const int paper = qGray(std::max_element(counts.begin(), counts.end(), [](auto a, auto b) { return a.second < b.second; })->first);
// Ink lies past halfway from the paper's gray.
const auto inked = [&image, paper](QPoint at) { return std::abs(qGray(image.pixel(at)) - paper) > 127; };
std::vector<bool> seen(size_t(image.width()) * size_t(image.height()));
const auto index = [&image](QPoint at) { return size_t(at.y()) * size_t(image.width()) + size_t(at.x()); };
for (int y = ground.top() + 20; y < ground.bottom() - 40; ++y)
for (int x = ground.left(); x < ground.right(); ++x) {
if (!inked(QPoint(x, y)))
continue;
QRect box(x, y, 1, 1);
int count = 0;
std::vector<QPoint> stack{QPoint(x, y)};
while (!stack.empty()) {
const QPoint at = stack.back();
stack.pop_back();
if (!image.rect().contains(at) || !inked(at) || seen[index(at)])
continue;
seen[index(at)] = true;
++count;
box |= QRect(at, QSize(1, 1));
for (const QPoint step : {QPoint(1, 0), QPoint(-1, 0), QPoint(0, 1), QPoint(0, -1), QPoint(1, 1), QPoint(-1, -1), QPoint(1, -1), QPoint(-1, 1)})
stack.push_back(at + step);
}
return {box, double(count) / (box.width() * box.height())};
}
throw std::runtime_error("no glyph in the ground");
}
}
void AcceptanceFinishTests::cameraRawCurvesAndDither()
{
App app;
const QUuid layer = photo(app);
const int before = pixel(app.session(), layer, 600, 700).green();
// Camera Raw: a curve region lifted, a point dragged.
app.action("cameraRawFilter").trigger();
QTRY_VERIFY(app.window.findChild<QToolButton *>(QStringLiteral("CurveSection")));
app.window.findChild<QToolButton *>(QStringLiteral("CurveSection"))->click();
QWidget *graph = app.window.findChild<QWidget *>(QStringLiteral("cameraRawCurveGraph"));
QTRY_VERIFY(graph && graph->isVisible());
const int w = graph->width(), h = graph->height();
drag(graph, QPoint(int(w * 0.6), h / 2), QPoint(int(w * 0.6), h / 2 - h / 4));
QVERIFY2(app.session().filterEdit().value().settings.cameraRaw.curve.lights > 20,
qPrintable(QString::number(app.session().filterEdit().value().settings.cameraRaw.curve.lights)));
app.window.findChild<QToolButton *>(QStringLiteral("curvePage1"))->click();
const size_t points = app.session().filterEdit().value().settings.cameraRaw.curve.rgb.size();
drag(graph, QPoint(w / 4, h * 3 / 4), QPoint(w / 4, h / 2));
QCOMPARE(app.session().filterEdit().value().settings.cameraRaw.curve.rgb.size(), points + 1);
QVERIFY(committed(app));
QVERIFY2(pixel(app.session(), layer, 600, 700).green() > before + 10, qPrintable(pixel(app.session(), layer, 600, 700).name()));
app.action("undo").trigger();
// ASCII of "@" draws that letter, holes and all.
app.action("dither").trigger();
choose(shownChild<QComboBox>(app, "ditherStyle"), QStringLiteral("ASCII"));
typeInto(app, "ditherCharacters", QStringLiteral("@"), false);
QVERIFY(committed(app));
const QImage small = app.session().activeLayer().value().asset.value().image();
const auto [letter, inked] = glyph(small);
QVERIFY2(inked < 0.75 && letter.height() >= 6, qPrintable(QString("%1 %2").arg(inked).arg(letter.height())));
app.action("undo").trigger();
app.action("dither").trigger();
QCOMPARE(shownChild<QComboBox>(app, "ditherStyle").currentText(), QStringLiteral("ASCII"));
typeInto(app, "textSizeField", QStringLiteral("28"), true);
QTRY_VERIFY(!app.session().filterEdit() && !app.session().isProjectBusy());
const auto [large, inkedLarge] = glyph(app.session().activeLayer().value().asset.value().image());
const double grown = double(large.height()) / letter.height();
QVERIFY2(grown > 1.6 && grown < 2.4 && inkedLarge < 0.75, qPrintable(QString::number(grown)));
app.action("undo").trigger();
// Scanlines: dark gaps between the beam's rows across the sky.
app.action("dither").trigger();
choose(shownChild<QComboBox>(app, "ditherStyle"), QStringLiteral("Scanlines (CRT)"));
QVERIFY(committed(app));
const QImage lines = app.session().activeLayer().value().asset.value().image();
int darkest = 255, brightest = 0;
for (int y = 100; y < 110; ++y) {
darkest = std::min(darkest, qGray(lines.pixel(600, y)));
brightest = std::max(brightest, qGray(lines.pixel(600, y)));
}
QVERIFY2(brightest - darkest > 40, qPrintable(QString("%1 %2").arg(darkest).arg(brightest)));
}
void AcceptanceFinishTests::jpegPreviewAndQuitMidEdit()
{
App app;
photo(app);
// Export JPEG: View keys zoom to 100% and back.
bool done = false;
app.workspace.current().controller.exportJPEG([&done] { done = true; });
QTRY_VERIFY(app.window.findChild<JPEGExportSheet *>());
auto *preview = app.window.findChild<JPEGPreview *>();
QVERIFY(preview && !preview->zoom());
JPEGExportSheet *sheet = app.window.findChild<JPEGExportSheet *>();
QTest::keyClick(sheet, Qt::Key_1, Qt::ControlModifier);
QCOMPARE(preview->zoom(), std::optional<double>(1));
QTest::keyClick(sheet, Qt::Key_Equal, Qt::ControlModifier);
QVERIFY(preview->zoom().value() > 1);
QTest::keyClick(sheet, Qt::Key_0, Qt::ControlModifier);
QVERIFY(!preview->zoom());
QTest::keyClick(sheet, Qt::Key_Escape);
QTRY_VERIFY(done);
// Quit with a gradient pending: applied before the one question.
app.session().selectTool(NavigationTool::gradient);
app.drag(QPointF(0, 400), QPointF(1200, 400));
QVERIFY(app.session().gradientEdit());
DialogDesk desk;
desk.replies = {"Cancel"};
desk.note = [&] { return QStringLiteral("pending=%1 levels=%2").arg(app.session().gradientEdit().has_value()).arg(app.session().levels().has_value()); };
app.window.close();
QTRY_COMPARE(desk.seen.size(), 1);
QVERIFY2(desk.seen[0].endsWith("|pending=0 levels=0"), qPrintable(desk.seen[0]));
QTRY_VERIFY(!app.workspace.isManaging());
QVERIFY(app.window.isVisible());
// With Levels open: cancelled, one question, then the window goes.
app.action("levels").trigger();
QVERIFY(app.session().levels());
desk.replies = {"Don’t Save"};
app.window.close();
QTRY_COMPARE(desk.seen.size(), 2);
QVERIFY2(desk.seen[1].endsWith("|pending=0 levels=0"), qPrintable(desk.seen[1]));
QTRY_VERIFY(!app.window.isVisible());
}
QTEST_MAIN(AcceptanceFinishTests)
#include "AcceptanceFinishTests.moc"
+21
View File
@@ -5,6 +5,8 @@
#include "UI/CompositorMenus.h"
#include "UI/OmarchyTheme.h"
#include "UI/ProjectWorkspaceView.h"
#include <QAbstractItemView>
#include <QComboBox>
#include <QDir>
#include <QPainter>
#include <QTemporaryDir>
@@ -88,6 +90,25 @@ struct App {
}
};
// A combo box popup row, where the pointer finds it.
inline QPoint popupRow(QComboBox &box, const QString &text)
{
box.showPopup();
if (!QTest::qWaitFor([&box] { return box.view()->isVisible(); }, 2000))
throw std::runtime_error("the menu never opened");
const int row = box.findText(text);
if (row < 0)
throw std::runtime_error("no such entry");
return box.view()->visualRect(box.model()->index(row, 0)).center();
}
// Chooses an entry with a click in its popup.
inline void choose(QComboBox &box, const QString &text)
{
const QPoint row = popupRow(box, text);
QTest::mouseClick(box.view()->viewport(), Qt::LeftButton, Qt::NoModifier, row);
}
// A photograph's stand-in: sky, ground and a red disc.
inline QImage scene(int width, int height)
{
+195
View File
@@ -0,0 +1,195 @@
#include "AcceptanceFixtures.h"
#include "DialogDesk.h"
#include "Document/Guides.h"
#include "Document/ShapeTool.h"
#include "IO/ProjectStore.h"
#include "UI/ColorPickerSheet.h"
#include "UI/ColorRangeSheet.h"
#include "UI/GridSettingsSheet.h"
#include "UI/ProjectTabs.h"
#include <QMenu>
// The acceptance run, part four: 1.4.5's selections, grid, canvases, tabs.
class AcceptanceNewTests : public QObject {
Q_OBJECT
private slots:
void initTestCase() { QStandardPaths::setTestModeEnabled(true); }
void colorRangeGridAndSnapping();
void presetsQuickLookUngroupAndTabs();
};
namespace {
// A drag with keys held at every step.
void drag(App &app, QPointF from, QPointF to, Qt::KeyboardModifiers keys)
{
app.press(from, keys);
for (int step = 1; step <= 8; ++step) {
const QPointF view = app.at(from + (to - from) * (step / 8.0));
QMouseEvent event(QEvent::MouseMove, view, view, app.canvas().mapToGlobal(view.toPoint()), Qt::NoButton, Qt::LeftButton, keys);
QApplication::sendEvent(&app.canvas(), &event);
}
QTest::mouseRelease(&app.canvas(), Qt::LeftButton, keys, app.at(to));
}
void typeInSheet(App &app, const char *name, const QString &text)
{
auto *field = app.window.findChild<GridSettingsSheet *>()->findChild<PickerField *>(QString::fromLatin1(name));
QVERIFY(field);
field->setFocus();
field->selectAll();
QTest::keyClicks(field, text);
QTest::keyClick(field, Qt::Key_Return);
}
ProjectTabButton &tabButton(App &app, const QString &title)
{
for (ProjectTabButton *button : app.window.tabs()->buttons())
if (button->tab->title() == title)
return *button;
throw std::runtime_error("no such tab");
}
}
void AcceptanceNewTests::colorRangeGridAndSnapping()
{
App app;
QVERIFY(scene(1200, 800).save(app.path("photo.png")));
app.session().createDocument(1200, 800);
app.importFile(app.path("photo.png"));
// Color Range samples the red disc on the canvas.
app.action("colorRange").trigger();
QVERIFY(app.session().colorRange());
app.press(QPointF(600, 400));
app.release(QPointF(600, 400));
QTRY_VERIFY(app.session().selection().has_value());
// The panel's OK applies it.
QPushButton *ok = nullptr;
for (QPushButton *button : app.window.findChild<ColorRangeSheet *>()->findChildren<QPushButton *>())
if (button->text() == QStringLiteral("OK"))
ok = button;
QVERIFY(ok);
ok->click();
QVERIFY(!app.session().colorRange());
const QRectF disc = app.session().selection().value().path.boundingRect();
QVERIFY2(qAbs(disc.center().x() - 600) < 4 && qAbs(disc.width() - 400) < 12 && qAbs(disc.height() - 400) < 12,
qPrintable(QString("%1 %2").arg(disc.width()).arg(disc.height())));
app.action("deselect").trigger();
// Grid Settings: typed spacing applies; Restore Defaults undoes it.
ProjectController &controller = app.workspace.current().controller;
bool done = false;
controller.gridSettings([&done] { done = true; });
QTRY_VERIFY(app.window.findChild<GridSettingsSheet *>());
typeInSheet(app, "gridSpacing", QStringLiteral("100"));
QTRY_VERIFY(done);
QVERIFY(app.session().layoutGrid() == LayoutGrid(100, 8));
done = false;
controller.gridSettings([&done] { done = true; });
QTRY_VERIFY(app.window.findChild<GridSettingsSheet *>());
app.window.findChild<GridSettingsSheet *>()->findChild<QPushButton *>(QStringLiteral("gridDefaults"))->click();
app.window.findChild<GridSettingsSheet *>()->findChild<QPushButton *>(QStringLiteral("gridOK"))->click();
QTRY_VERIFY(done);
QVERIFY(app.session().layoutGrid() == LayoutGrid());
// Marquee and rectangle land on a guide, unless Ctrl.
app.session().setShowsGuides(true);
app.session().addGuide({.id = QUuid::createUuid(), .axis = CanvasGuide::Axis::vertical, .position = 300});
app.session().selectTool(NavigationTool::marquee);
drag(app, QPointF(296, 100), QPointF(500, 300), Qt::NoModifier);
QCOMPARE(app.session().selection().value().path.boundingRect().left(), 300.0);
app.action("deselect").trigger();
drag(app, QPointF(296, 100), QPointF(500, 300), Qt::ControlModifier);
QVERIFY(qAbs(app.session().selection().value().path.boundingRect().left() - 296) <= 2);
app.action("deselect").trigger();
app.session().selectTool(NavigationTool::shape);
app.session().setShapeKind(ShapeKind::rectangle);
drag(app, QPointF(296, 500), QPointF(450, 650), Qt::NoModifier);
QCOMPARE(app.session().activeLayer().value().transform.origin.x(), 300.0);
drag(app, QPointF(296, 100), QPointF(450, 250), Qt::ControlModifier);
QVERIFY(qAbs(app.session().activeLayer().value().transform.origin.x() - 296) <= 2);
}
void AcceptanceNewTests::presetsQuickLookUngroupAndTabs()
{
App app;
// A preset fills the fields and checks the match.
auto *more = app.window.findChild<QToolButton *>(QStringLiteral("presetSizes"));
QVERIFY(more && more->menu());
QAction *story = nullptr;
for (QAction *entry : more->menu()->actions())
if (entry->text() == QStringLiteral("Instagram Story"))
story = entry;
QVERIFY(story);
story->trigger();
QVERIFY(story->isChecked());
QCOMPARE(app.window.findChild<QLineEdit *>(QStringLiteral("heightInput"))->text(), QStringLiteral("1920"));
app.window.findChild<QPushButton *>(QStringLiteral("createCanvas"))->click();
QCOMPARE(QSize(int(app.session().document().value().width), int(app.session().document().value().height)), QSize(1080, 1920));
// Ungroup: the layers take the folder's place; undo returns.
app.action("newBlankLayer").trigger();
const QUuid first = app.session().activeLayerID().value();
app.action("newBlankLayer").trigger();
const QUuid second = app.session().activeLayerID().value();
app.session().selectLayers({first, second}, second);
app.session().groupSelectedLayers();
const QUuid folder = app.session().activeLayerID().value();
const auto order = [&app] {
QList<QUuid> ids;
for (const ImageLayer &layer : app.session().document().value().layers)
ids << layer.id;
return ids;
};
const QList<QUuid> grouped = order();
app.canvas().setFocus();
QTest::keyClick(&app.canvas(), Qt::Key_G, Qt::ControlModifier | Qt::ShiftModifier);
// The two layers stand where the folder stood, in order.
QList<QUuid> expected = grouped;
expected.removeAll(folder);
QCOMPARE(order(), expected);
QCOMPARE(int(order().indexOf(first)) + 1, int(order().indexOf(second)));
for (const ImageLayer &layer : app.session().document().value().layers)
QVERIFY(!layer.isGroup && !layer.parentID);
QTest::keyClick(&app.canvas(), Qt::Key_Z, Qt::ControlModifier);
QCOMPARE(order(), grouped);
// A save writes the Quick Look preview, 1024 long.
const QString project = app.path("Story.comp");
DialogDesk desk;
desk.replies = {project};
app.action("saveAs").trigger();
QTRY_VERIFY_WITH_TIMEOUT(QFileInfo(project).isDir() && !app.session().isModified(), 30000);
const QImage preview(QDir(project).filePath(QStringLiteral("QuickLook/Preview.jpg")));
QCOMPARE(preview.size(), QSize(576, 1024));
QCOMPARE(int(ProjectStore::load(project).manifest.layers.size()), 4);
// A tab dragged past its neighbour; the rest gather.
app.workspace.newCanvas();
app.workspace.newCanvas();
QStringList before;
for (const std::shared_ptr<ProjectTab> &tab : app.workspace.tabs())
before << tab->title();
const int from = int(before.indexOf(QStringLiteral("Story")));
QVERIFY(from >= 0 && from + 1 < before.size());
QWindow *window = app.window.windowHandle();
ProjectTabButton &dragged = tabButton(app, QStringLiteral("Story"));
const QPoint start = dragged.mapTo(&app.window, QPoint(20, 14));
QTest::mousePress(window, Qt::LeftButton, {}, start);
QTest::mouseMove(window, start + QPoint(10, 0));
QTest::mouseMove(window, start + QPoint(dragged.width() + 40, 0));
QTest::mouseRelease(window, Qt::LeftButton, {}, start + QPoint(dragged.width() + 40, 0));
QCOMPARE(app.workspace.tabs()[from]->title(), before[from + 1]);
QCOMPARE(app.workspace.tabs()[from + 1]->title(), QStringLiteral("Story"));
for (int tab = 0; tab < 12; ++tab)
app.workspace.newCanvas();
OverflowTabsPill &pill = *app.window.tabs()->pill();
QTRY_VERIFY(pill.isVisible());
QTest::mousePress(&pill, Qt::LeftButton);
auto *menu = pill.findChild<QMenu *>(QStringLiteral("projectTabsOverflowMenu"));
QVERIFY(menu && menu->isVisible());
app.shot(QStringLiteral("17-tabs"));
QAction *hidden = menu->actions().front();
const QString chosen = hidden->text();
hidden->trigger();
menu->close();
QTest::mouseRelease(&pill, Qt::LeftButton);
QCOMPARE(app.workspace.current().title(), chosen);
}
QTEST_MAIN(AcceptanceNewTests)
#include "AcceptanceNewTests.moc"
+3
View File
@@ -203,6 +203,9 @@ compositor_test(DitherKernelTests)
compositor_test(AcceptanceTests)
compositor_test(AcceptanceColorTests)
compositor_test(AcceptanceProjectTests)
compositor_test(AcceptanceNewTests)
compositor_test(AcceptanceEditTests)
compositor_test(AcceptanceFinishTests)
compositor_test(WindowTeardownTests)
compositor_test(RecentProjectsCoreTests)
compositor_test(ProjectWorkspaceSignalTests)