#include "AcceptanceFixtures.h" #include "DialogDesk.h" #include "UI/ExportAsSheet.h" #include #include #include #include #include // 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 Widget &shownChild(App &app, const char *name) { Widget *shown = nullptr; static_cast(QTest::qWaitFor([&] { for (Widget *each : app.window.findChildren(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(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(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 glyph(const QImage &image) { const QRect ground(400, 640, 400, 120); std::map 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 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 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(QStringLiteral("CurveSection"))); app.window.findChild(QStringLiteral("CurveSection"))->click(); QWidget *graph = app.window.findChild(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(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(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(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, its own filter: dark gaps between the beam's rows. app.action("scanlines").trigger(); 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.exportAs(ExportFormat::jpeg, [&done] { done = true; }); QTRY_VERIFY(app.window.findChild()); auto *preview = app.window.findChild(); QVERIFY(preview && !preview->zoom()); ExportAsSheet *sheet = app.window.findChild(); QTest::keyClick(sheet, Qt::Key_1, Qt::ControlModifier); QCOMPARE(preview->zoom(), std::optional(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"