From 8fc71fbbce8787235e60176e020a33243e81d0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 11 Oct 2026 17:22:28 -0400 Subject: [PATCH] 14.31 round 1: the acceptance scenarios check what each row claims; WebP in the window --- docs/acceptance.md | 12 ++++---- tests/AcceptanceLatestTests.cpp | 48 +++++++++++++++++++++++--------- tests/AcceptanceProjectTests.cpp | 15 +++++++--- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/docs/acceptance.md b/docs/acceptance.md index 7098afe..410f345 100644 --- a/docs/acceptance.md +++ b/docs/acceptance.md @@ -25,7 +25,7 @@ The screenshots come from Qt's offscreen platform, so floating panels appear wit | Layer effects | A 12 px red stroke rings a shape outside its edge, and a drop shadow's sheet shows. The shape keeps its fill. | [07](acceptance/07-effects.png) | `LayerEffectsTests`, `LayerEffectsKernelTests`, `EffectsPanelTests`, `EffectsCanvasTests`, `InnerGlowTests` | pass | | Filters | Dither leaves only black and white dots. Remove Background masks the photo around its subject: the centre stays opaque and a corner turns clear. | [08](acceptance/08-filters-dither.png), [09](acceptance/09-remove-background.png) | `DitherTests`, `FilterTests`, `FinishingFilterTests`, `SubjectRemovalTests`, `RemoveBackgroundTests` | pass | | Crop, trim, canvas size, image size; PNG and JPEG export | Crops to 1000 × 600. Canvas Size widens it to 1400. Trim finds the photo's own 1200 again. Image Size halves it. Exports a PNG through the save panel and a JPEG through its sheet and panel, then reads both back at 600 × 300. | [10](acceptance/10-canvas-size.png), [11](acceptance/11-export-jpeg.png) | `CropTests`, `ImageTrimTests`, `ProjectResizeTests`, `ProjectExportTests`, `JPEGExportTests` | pass | -| Imports | Takes JPEG, PNG, TIFF, HEIC, SVG, a layered Photoshop file (it arrives as a folder) and a camera RAW file, which asks how to develop it. That makes eight layers with no import error. | [12](acceptance/12-raw-develop.png), [13](acceptance/13-imports.png) | `ImageImportTests`, `PSDImportTests`, `RawImporterTests`, `RawDevelopTests`, `SVGImportTests` | pass | +| Imports | Takes JPEG, PNG, TIFF, WebP (red and clear halves kept), HEIC, SVG, a layered Photoshop file (it arrives as a folder) and a camera RAW file, which asks how to develop it. That makes eight layers with no import error. | [12](acceptance/12-raw-develop.png), [13](acceptance/13-imports.png) | `ImageImportTests`, `PSDImportTests`, `RawImporterTests`, `RawDevelopTests`, `SVGImportTests` | pass | | Open Recent; Keyboard Shortcuts window | Saves a project, and Open Recent lists it. The shortcuts window opens over the editor. | [15](acceptance/15-shortcuts.png) | `OpenRecentTests`, `RecentProjectsCoreTests`, `KeyboardShortcutsTests`, `KeyboardShortcutsSheetTests` | pass | | Scrub a number by its label | Dragging the Opacity title 40 points left lowers the layer's opacity. | [14](acceptance/14-scrub.png) | `NumericScrubTests`, `ScrubbableLabelsTests`, `ScrubbableSheetsTests` | pass | | Keep working while a project saves | Another layer is added while a save runs. The file holds the state the save began with. | — | `ProjectSaveWhileEditingTests` | pass | @@ -60,13 +60,13 @@ The screenshots come from Qt's offscreen platform, so floating panels appear wit | 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 | +| Search Commands and Rotate Canvas 90° | Ctrl+F, "rotate canvas clockwise" and Return turn a 1200 × 800 photo to 800 × 1200, one undo step; the new top left holds the old bottom left's pixels. | [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 | +| Last Filter | After a Gaussian Blur of radius 3 typed in its sheet, Filter › Last Filter is titled for it and blurs again as one more step, without its panel, to the same pixels the sheet makes at radius 3. | — | `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 | +| Navigator and Ctrl with the middle button | At 400% the minimap shows in the corner; a click at its edge centres the view on the document point under it, both ways. 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 that very tab. 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 | File › Export As, PDF chosen in the sheet, 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 diff --git a/tests/AcceptanceLatestTests.cpp b/tests/AcceptanceLatestTests.cpp index bc0c31f..1120a5b 100644 --- a/tests/AcceptanceLatestTests.cpp +++ b/tests/AcceptanceLatestTests.cpp @@ -2,6 +2,7 @@ #include "DialogDesk.h" #include "UI/ExportAsSheet.h" #include "UI/NewCanvasSheet.h" +#include "Rendering/NavigatorGeometry.h" #include #include #include @@ -55,6 +56,7 @@ void AcceptanceLatestTests::searchRotateAndFullscreen() { App app; photo(app); + const int steps = app.session().history.undoCount(); // 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")); @@ -62,6 +64,11 @@ void AcceptanceLatestTests::searchRotateAndFullscreen() QTest::keyClick(&search, Qt::Key_Return); QTRY_COMPARE(documentSize(app), QSize(800, 1200)); QCOMPARE(app.session().history.undoName(), QString("Rotate Canvas 90° Clockwise")); + QCOMPARE(app.session().history.undoCount(), steps + 1); + // Clockwise: the new top left was the old bottom left. + const QImage old = scene(1200, 800); + QCOMPARE(app.shown(100, 50), old.pixelColor(50, 799 - 100)); + QCOMPARE(app.shown(700, 1100), old.pixelColor(1100, 799 - 700)); app.shot(QStringLiteral("19-search")); // F: the canvas alone, bars away; Escape brings them back. app.canvas().setFocus(); @@ -76,10 +83,14 @@ void AcceptanceLatestTests::searchRotateAndFullscreen() void AcceptanceLatestTests::lastFilterAndCameraRaw() { App app; - const QUuid layer = photo(app); - // Gaussian Blur once, then Last Filter repeats it. + photo(app); + // Gaussian Blur at radius 3; Last Filter repeats it. app.action("gaussianBlur").trigger(); - shownChild(app.window, QStringLiteral("filterOK")).click(); + QLineEdit &radius = shownChild(app.window, QStringLiteral("radiusField")); + radius.setFocus(); + radius.selectAll(); + QTest::keyClicks(&radius, QStringLiteral("3")); + QTest::keyClick(&radius, Qt::Key_Return); 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(); @@ -87,6 +98,14 @@ void AcceptanceLatestTests::lastFilterAndCameraRaw() QTRY_COMPARE(app.session().history.undoCount(), steps + 1); QCOMPARE(app.session().history.undoName(), QString("Gaussian Blur")); QVERIFY(!app.session().filterEdit()); + const QImage repeated = app.session().activeLayer().value().asset.value().image(); + // The sheet again, as it opens, blurs the same. + app.action("undo").trigger(); + app.action("gaussianBlur").trigger(); + QCOMPARE(shownChild(app.window, QStringLiteral("radiusField")).text(), QString("3")); + shownChild(app.window, QStringLiteral("filterOK")).click(); + QTRY_VERIFY(!app.session().filterEdit() && !app.session().isProjectBusy()); + QVERIFY(app.session().activeLayer().value().asset.value().image() == repeated); // Camera Raw's Temperature +100 on mid gray, as Photoshop's. app.session().createDocument(64, 64); QImage gray(64, 64, QImage::Format_RGB32); @@ -106,7 +125,6 @@ void AcceptanceLatestTests::lastFilterAndCameraRaw() 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() @@ -117,10 +135,13 @@ void AcceptanceLatestTests::navigatorAndMiddleZoom() 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()))); + // The view's middle on the document point under the click. + const QPointF target = NavigatorGeometry{QSizeF(1200, 800), QSizeF(minimap.size())}.documentPoint(QPointF(4, 4)); + const QPointF middle = app.session().viewport.visibleDocumentRect(QSizeF(1200, 800)).center(); + QVERIFY2(std::abs(middle.x() - target.x()) < 1 && std::abs(middle.y() - target.y()) < 1, + qPrintable(QStringLiteral("%1,%2 %3,%4").arg(middle.x()).arg(middle.y()).arg(target.x()).arg(target.y()))); + QVERIFY(target.x() < 300 && target.y() < 200); app.shot(QStringLiteral("21-navigator")); app.action("navigator").trigger(); // Ctrl with the middle button: a hundred points up doubles. @@ -140,9 +161,10 @@ void AcceptanceLatestTests::newCanvasExportAndTabs() // The blank start tab has no cross; Close leaves it. QToolButton *cross = app.window.findChild(QStringLiteral("closeTab")); QVERIFY(cross && cross->isHidden()); + const QUuid start = app.workspace.current().id; app.action("closeProject").trigger(); QTest::qWait(100); - QCOMPARE(int(app.workspace.tabs().size()), 1); + QVERIFY(app.workspace.tabs().size() == 1 && app.workspace.selectedID() == start && app.workspace.tabs()[0]->id == start); // New Canvas: two by one inches, 300 DPI, white. NewCanvasSheet &sheet = shownChild(app.window, QString()); QAbstractButton &unit = shownChild(sheet, QStringLiteral("canvasUnit")); @@ -165,16 +187,16 @@ void AcceptanceLatestTests::newCanvasExportAndTabs() 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. + // File › Export As, PDF chosen: 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; }); + app.action("exportAs").trigger(); + shownChild(app.window, QStringLiteral("exportFormatPDF")).click(); QPushButton &exporting = shownChild(app.window, QStringLiteral("exportExport")); - QTRY_VERIFY(exporting.isEnabled()); + QTRY_VERIFY(shownChild(app.window, QStringLiteral("exportPage")).isVisible() && exporting.isEnabled()); app.shot(QStringLiteral("22-export-as")); exporting.click(); - QTRY_VERIFY_WITH_TIMEOUT(done, 30000); + QTRY_VERIFY_WITH_TIMEOUT(QFileInfo::exists(app.path("page.pdf")) && !app.session().isProjectBusy(), 30000); QFile pdf(app.path("page.pdf")); QVERIFY(pdf.open(QIODevice::ReadOnly)); const QByteArray bytes = pdf.readAll(); diff --git a/tests/AcceptanceProjectTests.cpp b/tests/AcceptanceProjectTests.cpp index e75b68a..c54b4b6 100644 --- a/tests/AcceptanceProjectTests.cpp +++ b/tests/AcceptanceProjectTests.cpp @@ -106,6 +106,11 @@ void AcceptanceProjectTests::imports() QVERIFY(scene(200, 150).save(app.path("Scene.jpg"))); QVERIFY(scene(200, 150).save(app.path("Scene.png"))); QVERIFY(scene(200, 150).save(app.path("Scene.tif"))); + // WebP, lossless: red on the left, clear on the right. + QImage half(40, 20, QImage::Format_ARGB32); + half.fill(Qt::transparent); + QPainter(&half).fillRect(QRect(0, 0, 20, 20), QColor(200, 20, 20)); + QVERIFY(half.save(app.path("Half.webp"), "webp", 100)); QVERIFY(QFile::copy(QFINDTESTDATA("fixtures/red.heic"), app.path("Red.heic"))); QFile svg(app.path("Mark.svg")); QVERIFY(svg.open(QIODevice::WriteOnly)); @@ -118,7 +123,7 @@ void AcceptanceProjectTests::imports() psd.close(); DNGFixture::write(app.path("Gray.dng"), {}); QList urls; - for (const char *name : {"Scene.jpg", "Scene.png", "Scene.tif", "Red.heic", "Mark.svg", "Trip.psd", "Gray.dng"}) + for (const char *name : {"Scene.jpg", "Scene.png", "Scene.tif", "Half.webp", "Red.heic", "Mark.svg", "Trip.psd", "Gray.dng"}) urls << QUrl::fromLocalFile(app.path(QString::fromLatin1(name))); bool done = false; app.session().importImages(urls, std::nullopt, [&done] { done = true; }); @@ -132,10 +137,12 @@ void AcceptanceProjectTests::imports() for (const ImageLayer &layer : app.session().document().value().layers) names << layer.name; // A layered Photoshop file arrives as a folder of layers. - QCOMPARE(names, (QStringList{"Scene", "Scene", "Scene", "Red", "Mark", "Trip", "Sky", "Gray"})); + QCOMPARE(names, (QStringList{"Scene", "Scene", "Scene", "Half", "Red", "Mark", "Trip", "Sky", "Gray"})); const std::vector &layers = app.session().document().value().layers; - QVERIFY(layers[5].isGroup); - QCOMPARE(layers[6].parentID, std::optional(layers[5].id)); + QVERIFY(layers[6].isGroup); + QCOMPARE(layers[7].parentID, std::optional(layers[6].id)); + const QImage webp = layers[3].asset.value().image(); + QVERIFY(webp.size() == QSize(40, 20) && webp.pixelColor(5, 10) == QColor(200, 20, 20) && webp.pixelColor(35, 10).alpha() == 0); app.shot(QStringLiteral("13-imports")); }