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:
1 parent
8bd3fc3264
commit
c59e85babc
23 files changed
+709
-39
No files matched your search
@@ -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; });
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user