diff --git a/docs/acceptance.md b/docs/acceptance.md index cdea347..4a1acec 100644 --- a/docs/acceptance.md +++ b/docs/acceptance.md @@ -1,13 +1,13 @@ -# OmaPhoto 1.4.5 acceptance +# OmaPhoto 1.4.9 acceptance -The acceptance table for OmaPhoto 1.4.5, the Linux port of Compositor 1.4.5. Each row is a feature from the README. The rows under "New in 1.4.5" cover what changed since 1.3.3. A "pending" result waits for the release. +The acceptance table for OmaPhoto 1.4.9, the Linux port of Compositor 1.4.9. Each row is a feature from the README. The rows under "New in 1.4.5" cover what changed since 1.3.3, those under "New in 1.4.6 to 1.4.9" what changed since 1.4.5. A "pending" result waits for the release. -The acceptance run drives the app's own window, the way a person uses it. It chooses menu entries, drags on the canvas, types into sheets and answers the real file panels. Then it checks concrete results: pixels, layers, sizes and files on disk. The run is six test programs: `AcceptanceTests`, `AcceptanceColorTests` and `AcceptanceProjectTests` for the table, `AcceptanceNewTests`, `AcceptanceEditTests` and `AcceptanceFinishTests` for the rows new in 1.4.5. They take the screenshots below when `ACCEPTANCE_SHOTS` names a folder. +The acceptance run drives the app's own window, the way a person uses it. It chooses menu entries, drags on the canvas, types into sheets and answers the real file panels. Then it checks concrete results: pixels, layers, sizes and files on disk. The run is seven test programs: `AcceptanceTests`, `AcceptanceColorTests` and `AcceptanceProjectTests` for the table, `AcceptanceNewTests`, `AcceptanceEditTests` and `AcceptanceFinishTests` for the rows new in 1.4.5, and `AcceptanceLatestTests` for those new in 1.4.6 to 1.4.9. They take the screenshots below when `ACCEPTANCE_SHOTS` names a folder. ```sh docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -e QT_QPA_PLATFORM=offscreen -v "$PWD:$PWD" -w "$PWD" omaphoto-dev \ - sh -c 'cmake -S . -B build && cmake --build build --target AcceptanceTests AcceptanceColorTests AcceptanceProjectTests AcceptanceNewTests AcceptanceEditTests AcceptanceFinishTests && - for t in AcceptanceTests AcceptanceColorTests AcceptanceProjectTests AcceptanceNewTests AcceptanceEditTests AcceptanceFinishTests; do ACCEPTANCE_SHOTS=$PWD/docs/acceptance build/$t || exit 1; done' + sh -c 'cmake -S . -B build && cmake --build build --target AcceptanceTests AcceptanceColorTests AcceptanceProjectTests AcceptanceNewTests AcceptanceEditTests AcceptanceFinishTests AcceptanceLatestTests && + for t in AcceptanceTests AcceptanceColorTests AcceptanceProjectTests AcceptanceNewTests AcceptanceEditTests AcceptanceFinishTests AcceptanceLatestTests; do ACCEPTANCE_SHOTS=$PWD/docs/acceptance build/$t || exit 1; done' ``` The screenshots come from Qt's offscreen platform, so floating panels appear without the window manager's title bars. The run uses the built-in dark theme, except for the last row. @@ -32,7 +32,7 @@ The screenshots come from Qt's offscreen platform, so floating panels appear wit | Reload a project changed on disk | With unsaved edits open, another program renames a layer on disk. The app's alert asks, Revert is chosen, and the saved state shows with the new name. | [15](acceptance/15-shortcuts.png) | `ExternalChangeTests` | pass | | Follows the Omarchy theme | Starts in the built-in dark. A Catppuccin Latte `colors.toml` written while the app runs retints the window. | [16](acceptance/16-omarchy-light.png) | `OmarchyThemeTests` | pass | | Builds and tests on each system | The full suite, 334 binaries, passes on Ubuntu 24.04 (Qt 6.4.2), Ubuntu 26.04 (6.10.2), Arch (6.11.2), Fedora 43 and 44, and NixOS (6.11.2). Warnings count as errors. | — | `scripts/distro-check.sh` | pass | -| Packages and install scripts | Each install script runs from Gitea in a fresh container. On Ubuntu 24.04 and 26.04, Fedora 43 and 44 and Arch, it checks its package against `SHA256SUMS`, installs it and starts 1.4.5. On NixOS, `nixos.sh` resolves `v1.4.5`, builds its flake and starts 1.4.5. Debian 13 is refused, as intended. | — | `DesktopIntegrationTests`, `scripts/release.sh` | pending | +| Packages and install scripts | Each install script runs from Gitea in a fresh container. On Ubuntu 24.04 and 26.04, Fedora 43 and 44 and Arch, it checks its package against `SHA256SUMS`, installs it and starts 1.4.9. On NixOS, `nixos.sh` resolves `v1.4.9`, builds its flake and starts 1.4.9. Debian 13 is refused, as intended. | — | `DesktopIntegrationTests`, `scripts/release.sh` | pending | ## New in 1.4.5 @@ -56,6 +56,18 @@ The screenshots come from Qt's offscreen platform, so floating panels appear wit | Export JPEG preview | The View keys zoom the preview to 100%, in, and back to Fit. | — | `JPEGPreviewTests`, `ExportAsSheetTests` | pass | | Quit mid-edit | Quits with a gradient pending: it is applied before the one question. With Levels open, Levels closes and the window goes. | — | `QuitPendingEditsTests`, `WindowTeardownTests` | pass | +## New in 1.4.6 to 1.4.9 + +| Feature | What the run checks | Screenshot | Suites that hold the details | Result | +| --- | --- | --- | --- | --- | +| Search Commands and Rotate Canvas 90° | Ctrl+F, "rotate canvas clockwise" and Return turn a 1200 × 800 photo to 800 × 1200, one undo step. | [19](acceptance/19-search.png) | `CommandPaletteTests`, `CommandPaletteWindowTests`, `CanvasRotationTests` | pass | +| Toggle Fullscreen | F puts the menus and toolbar away over the whole screen; Escape brings them back. | — | `CanvasOnlyWindowTests`, `CanvasOnlySessionTests` | pass | +| Last Filter | After a Gaussian Blur, Filter › Last Filter is titled for it and blurs again as one more step, without its panel. | — | `LastFilterTests` | pass | +| Camera Raw as Photoshop's | Temperature +100 typed into the docked panel turns mid gray to Photoshop's own warm (235, 189, 117), within four levels. | [20](acceptance/20-camera-raw.png) | `CameraRawTests`, `CameraRawTablesTests`, `CameraRawFoldTests` | pass | +| Navigator and Ctrl with the middle button | At 400% the minimap shows in the corner; a click at its edge moves the view there. Ctrl with the middle button dragged up 100 points doubles the zoom. | [21](acceptance/21-navigator.png) | `NavigatorTests`, `CanvasNavigationTests` | pass | +| New Canvas in units, DPI and background | The blank start tab has no close button and Close Project leaves it. Inches, 300 DPI and White make a 2 × 1 inch canvas of 600 × 300 white pixels at 300 DPI. | — | `NewCanvasUnitTests`, `NewCanvasSheetTests`, `ProjectTabStripTests` | pass | +| Export As PDF | Export As in PDF writes a one-page PDF 1.4 whose page is 144 × 72 points, two inches by one. | [22](acceptance/22-export-as.png) | `ExportPDFTests`, `ExportAsSheetTests`, `ProjectExportTests` | pass | + ## Known differences from Compositor `AGENTS.md` lists each place where the port differs from the Swift app, and why. The main ones: diff --git a/docs/acceptance/19-search.png b/docs/acceptance/19-search.png new file mode 100644 index 0000000..7127c79 Binary files /dev/null and b/docs/acceptance/19-search.png differ diff --git a/docs/acceptance/20-camera-raw.png b/docs/acceptance/20-camera-raw.png new file mode 100644 index 0000000..22d1b89 Binary files /dev/null and b/docs/acceptance/20-camera-raw.png differ diff --git a/docs/acceptance/21-navigator.png b/docs/acceptance/21-navigator.png new file mode 100644 index 0000000..8656950 Binary files /dev/null and b/docs/acceptance/21-navigator.png differ diff --git a/docs/acceptance/22-export-as.png b/docs/acceptance/22-export-as.png new file mode 100644 index 0000000..6da16ca Binary files /dev/null and b/docs/acceptance/22-export-as.png differ diff --git a/tests/AcceptanceLatestTests.cpp b/tests/AcceptanceLatestTests.cpp new file mode 100644 index 0000000..bc0c31f --- /dev/null +++ b/tests/AcceptanceLatestTests.cpp @@ -0,0 +1,185 @@ +#include "AcceptanceFixtures.h" +#include "DialogDesk.h" +#include "UI/ExportAsSheet.h" +#include "UI/NewCanvasSheet.h" +#include +#include +#include +#include +#include +#include + +// The acceptance run, part seven: 1.4.6 to 1.4.9. +class AcceptanceLatestTests : public QObject { + Q_OBJECT +private slots: + void initTestCase() { QStandardPaths::setTestModeEnabled(true); } + void searchRotateAndFullscreen(); + void lastFilterAndCameraRaw(); + void navigatorAndMiddleZoom(); + void newCanvasExportAndTabs(); +}; + +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(); +} + +// A shown widget of that name, once its panel shows. +template Widget &shownChild(QWidget &root, const QString &name) +{ + Widget *shown = nullptr; + static_cast(QTest::qWaitFor([&] { + for (Widget *each : root.findChildren(name)) + if (each->isVisible()) + shown = each; + return shown != nullptr; + }, 5000)); + if (!shown) + throw std::runtime_error("no shown " + name.toStdString()); + return *shown; +} + +QSize documentSize(App &app) +{ + return app.session().document().value().size().toSize(); +} +} + +void AcceptanceLatestTests::searchRotateAndFullscreen() +{ + App app; + photo(app); + // Ctrl+F finds Rotate Canvas 90° Clockwise by words. + QTest::keyClick(&app.window, Qt::Key_F, Qt::ControlModifier); + QLineEdit &search = shownChild(app.window, QStringLiteral("commandSearch")); + QTest::keyClicks(&search, QStringLiteral("rotate canvas clockwise")); + QTest::keyClick(&search, Qt::Key_Return); + QTRY_COMPARE(documentSize(app), QSize(800, 1200)); + QCOMPARE(app.session().history.undoName(), QString("Rotate Canvas 90° Clockwise")); + app.shot(QStringLiteral("19-search")); + // F: the canvas alone, bars away; Escape brings them back. + app.canvas().setFocus(); + QTest::keyClick(&app.canvas(), Qt::Key_F); + QTRY_VERIFY(app.window.isFullScreen()); + QVERIFY(app.window.menuBar()->isHidden() && app.window.findChild()->isHidden()); + QTest::keyClick(&app.canvas(), Qt::Key_Escape); + QTRY_VERIFY(!app.window.isFullScreen()); + QVERIFY(!app.window.menuBar()->isHidden() && !app.window.findChild()->isHidden()); +} + +void AcceptanceLatestTests::lastFilterAndCameraRaw() +{ + App app; + const QUuid layer = photo(app); + // Gaussian Blur once, then Last Filter repeats it. + app.action("gaussianBlur").trigger(); + shownChild(app.window, QStringLiteral("filterOK")).click(); + QTRY_VERIFY(!app.session().filterEdit() && !app.session().isProjectBusy()); + QCOMPARE(app.action("lastFilter").text(), QString("Last Filter: Gaussian Blur")); + const int steps = app.session().history.undoCount(); + app.action("lastFilter").trigger(); + QTRY_COMPARE(app.session().history.undoCount(), steps + 1); + QCOMPARE(app.session().history.undoName(), QString("Gaussian Blur")); + QVERIFY(!app.session().filterEdit()); + // Camera Raw's Temperature +100 on mid gray, as Photoshop's. + app.session().createDocument(64, 64); + QImage gray(64, 64, QImage::Format_RGB32); + gray.fill(QColor(128, 128, 128)); + QVERIFY(gray.save(app.path("gray.png"))); + app.importFile(app.path("gray.png")); + app.action("cameraRawFilter").trigger(); + QLineEdit &temperature = shownChild(app.window, QStringLiteral("temperatureField")); + temperature.setFocus(); + temperature.selectAll(); + QTest::keyClicks(&temperature, QStringLiteral("100")); + // Return takes the number and goes on to OK. + QTest::keyClick(&temperature, Qt::Key_Return); + QTRY_VERIFY(!app.session().filterEdit() && !app.session().isProjectBusy()); + QCOMPARE(app.session().history.undoName(), QString("Camera Raw Filter")); + app.shot(QStringLiteral("20-camera-raw")); + QCOMPARE(app.session().history.undoName(), QString("Camera Raw Filter")); + const QColor warm = app.shown(32, 32); + QVERIFY2(std::abs(warm.red() - 235) <= 4 && std::abs(warm.green() - 189) <= 4 && std::abs(warm.blue() - 117) <= 4, qPrintable(warm.name())); + static_cast(layer); +} + +void AcceptanceLatestTests::navigatorAndMiddleZoom() +{ + App app; + photo(app); + // From 300% the Navigator shows; a click moves the view. + app.action("navigator").trigger(); + app.session().zoom(4, app.session().viewport.center()); + QWidget &minimap = shownChild(app.window, QStringLiteral("navigatorMinimap")); + const QRectF before = app.session().viewport.visibleDocumentRect(QSizeF(1200, 800)); + QTest::mouseClick(&minimap, Qt::LeftButton, Qt::NoModifier, QPoint(4, 4)); + const QRectF after = app.session().viewport.visibleDocumentRect(QSizeF(1200, 800)); + QVERIFY2(after.center().x() < before.center().x() - 100, qPrintable(QString::number(after.center().x()))); + app.shot(QStringLiteral("21-navigator")); + app.action("navigator").trigger(); + // Ctrl with the middle button: a hundred points up doubles. + app.session().zoom(1, app.session().viewport.center()); + const QPoint start = app.at(QPointF(600, 400)); + QTest::mousePress(&app.canvas(), Qt::MiddleButton, Qt::ControlModifier, start); + const QPointF up = start - QPointF(0, 100); + QMouseEvent event(QEvent::MouseMove, up, up, app.canvas().mapToGlobal(up.toPoint()), Qt::NoButton, Qt::MiddleButton, Qt::ControlModifier); + QApplication::sendEvent(&app.canvas(), &event); + QTest::mouseRelease(&app.canvas(), Qt::MiddleButton, Qt::ControlModifier, up.toPoint()); + QCOMPARE(app.session().viewport.zoom(), 2.0); +} + +void AcceptanceLatestTests::newCanvasExportAndTabs() +{ + App app; + // The blank start tab has no cross; Close leaves it. + QToolButton *cross = app.window.findChild(QStringLiteral("closeTab")); + QVERIFY(cross && cross->isHidden()); + app.action("closeProject").trigger(); + QTest::qWait(100); + QCOMPARE(int(app.workspace.tabs().size()), 1); + // New Canvas: two by one inches, 300 DPI, white. + NewCanvasSheet &sheet = shownChild(app.window, QString()); + QAbstractButton &unit = shownChild(sheet, QStringLiteral("canvasUnit")); + while (!unit.text().contains(QStringLiteral("Inches"))) + unit.click(); + QAbstractButton &dpi = shownChild(sheet, QStringLiteral("canvasResolution")); + while (!dpi.text().startsWith(QStringLiteral("300"))) + dpi.click(); + QAbstractButton &ground = shownChild(sheet, QStringLiteral("canvasBackground")); + while (!ground.text().startsWith(QStringLiteral("White"))) + ground.click(); + QLineEdit &width = shownChild(sheet, QStringLiteral("widthInput")); + QLineEdit &height = shownChild(sheet, QStringLiteral("heightInput")); + width.selectAll(); + QTest::keyClicks(&width, QStringLiteral("2")); + height.selectAll(); + QTest::keyClicks(&height, QStringLiteral("1")); + shownChild(sheet, QStringLiteral("createCanvas")).click(); + QTRY_COMPARE(documentSize(app), QSize(600, 300)); + QCOMPARE(app.session().document().value().resolution, 300.0); + QCOMPARE(app.shown(300, 150), QColor(Qt::white)); + QTRY_VERIFY(!cross->isHidden()); + // Export As a one-page PDF, two inches by one. + DialogDesk desk; + desk.replies = {app.path("page.pdf")}; + bool done = false; + app.workspace.current().controller.exportAs(ExportFormat::pdf, [&done] { done = true; }); + QPushButton &exporting = shownChild(app.window, QStringLiteral("exportExport")); + QTRY_VERIFY(exporting.isEnabled()); + app.shot(QStringLiteral("22-export-as")); + exporting.click(); + QTRY_VERIFY_WITH_TIMEOUT(done, 30000); + QFile pdf(app.path("page.pdf")); + QVERIFY(pdf.open(QIODevice::ReadOnly)); + const QByteArray bytes = pdf.readAll(); + QVERIFY(bytes.startsWith("%PDF-1.4") && bytes.contains("/MediaBox [0 0 144.0000 72.0000]")); +} + +QTEST_MAIN(AcceptanceLatestTests) +#include "AcceptanceLatestTests.moc" diff --git a/tests/Tests.cmake b/tests/Tests.cmake index d408581..d20abe3 100644 --- a/tests/Tests.cmake +++ b/tests/Tests.cmake @@ -220,6 +220,7 @@ compositor_test(AcceptanceProjectTests) compositor_test(AcceptanceNewTests) compositor_test(AcceptanceEditTests) compositor_test(AcceptanceFinishTests) +compositor_test(AcceptanceLatestTests) compositor_test(WindowTeardownTests) compositor_test(RecentProjectsCoreTests) compositor_test(ProjectWorkspaceSignalTests)