/**************************************************************************** ** ** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the test suite of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include #include #include #include #include #include "../../shared/util.h" #include "../shared/viewtestutil.h" #include "../shared/visualtestutil.h" using namespace QQuickViewTestUtil; using namespace QQuickVisualTestUtil; class PolygonProvider : public QObject { Q_OBJECT Q_PROPERTY(QVector paths READ paths WRITE setPaths NOTIFY pathsChanged) public: QVector paths() const { return m_paths; } void setPaths(QVector paths) { if (m_paths == paths) return; m_paths = paths; emit pathsChanged(); } signals: void pathsChanged(); private: QVector m_paths; }; class tst_QQuickShape : public QQmlDataTest { Q_OBJECT public: tst_QQuickShape(); private slots: void initValues(); void vpInitValues(); void basicShape(); void changeSignals(); void render(); void renderWithMultipleSp(); void radialGrad(); void conicalGrad(); void renderPolyline(); void renderMultiline(); void polylineDataTypes_data(); void polylineDataTypes(); void multilineDataTypes_data(); void multilineDataTypes(); void multilineStronglyTyped(); private: QVector m_lowPolyLogo; }; tst_QQuickShape::tst_QQuickShape() { // Force the software backend to get reliable rendering results regardless of the hw and drivers. QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software); const char *uri = "tst_qquickpathitem"; qmlRegisterType(uri, 1, 0, "Shape"); qmlRegisterType(uri, 1, 0, "ShapePath"); qmlRegisterUncreatableType(uri, 1, 0, "ShapeGradient", QQuickShapeGradient::tr("ShapeGradient is an abstract base class")); qmlRegisterType(uri, 1, 0, "LinearGradient"); qmlRegisterType(uri, 1, 0, "RadialGradient"); qmlRegisterType(uri, 1, 0, "ConicalGradient"); qmlRegisterType(uri, 1, 0, "PathPolyline"); qmlRegisterType("Qt.test", 1, 0, "PolygonProvider"); m_lowPolyLogo << (QPolygonF() << QPointF(20, 0) << QPointF(140, 0) << QPointF(140, 80) << QPointF(120, 100) << QPointF(0, 100) << QPointF(0, 20) << QPointF(20, 0) ) << (QPolygonF() << QPointF(20, 80) << QPointF(60, 80) << QPointF(80, 60) << QPointF(80, 20) << QPointF(40, 20) << QPointF(20, 40) << QPointF(20, 80) ) << (QPolygonF() << QPointF(80, 80) << QPointF(70, 70) ) << (QPolygonF() << QPointF(120, 80) << QPointF(100, 60) << QPointF(100, 20) ) << (QPolygonF() << QPointF(100, 40) << QPointF(120, 40) ); } void tst_QQuickShape::initValues() { QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("pathitem1.qml")); QQuickShape *obj = qobject_cast(c.create()); QVERIFY(obj != nullptr); QVERIFY(obj->rendererType() == QQuickShape::UnknownRenderer); QVERIFY(!obj->asynchronous()); QVERIFY(!obj->vendorExtensionsEnabled()); QVERIFY(obj->status() == QQuickShape::Null); auto vps = obj->data(); QVERIFY(vps.count(&vps) == 0); delete obj; } void tst_QQuickShape::vpInitValues() { QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("pathitem2.qml")); QQuickShape *obj = qobject_cast(c.create()); QVERIFY(obj != nullptr); QVERIFY(obj->rendererType() == QQuickShape::UnknownRenderer); QVERIFY(!obj->asynchronous()); QVERIFY(!obj->vendorExtensionsEnabled()); QVERIFY(obj->status() == QQuickShape::Null); auto vps = obj->data(); QVERIFY(vps.count(&vps) == 2); QQuickShapePath *vp = qobject_cast(vps.at(&vps, 0)); QVERIFY(vp != nullptr); QQmlListReference pathList(vp, "pathElements"); QCOMPARE(pathList.count(), 0); QCOMPARE(vp->strokeColor(), QColor(Qt::white)); QCOMPARE(vp->strokeWidth(), 1.0f); QCOMPARE(vp->fillColor(), QColor(Qt::white)); QCOMPARE(vp->fillRule(), QQuickShapePath::OddEvenFill); QCOMPARE(vp->joinStyle(), QQuickShapePath::BevelJoin); QCOMPARE(vp->miterLimit(), 2); QCOMPARE(vp->capStyle(), QQuickShapePath::SquareCap); QCOMPARE(vp->strokeStyle(), QQuickShapePath::SolidLine); QCOMPARE(vp->dashOffset(), 0.0f); QCOMPARE(vp->dashPattern(), QVector() << 4 << 2); QVERIFY(!vp->fillGradient()); delete obj; } void tst_QQuickShape::basicShape() { QScopedPointer window(createView()); window->setSource(testFileUrl("pathitem3.qml")); qApp->processEvents(); QQuickShape *obj = findItem(window->rootObject(), "pathItem"); QVERIFY(obj != nullptr); QQmlListReference list(obj, "data"); QCOMPARE(list.count(), 1); QQuickShapePath *vp = qobject_cast(list.at(0)); QVERIFY(vp != nullptr); QCOMPARE(vp->strokeWidth(), 4.0f); QVERIFY(vp->fillGradient() != nullptr); QCOMPARE(vp->strokeStyle(), QQuickShapePath::DashLine); vp->setStrokeWidth(5.0f); QCOMPARE(vp->strokeWidth(), 5.0f); QQuickShapeLinearGradient *lgrad = qobject_cast(vp->fillGradient()); QVERIFY(lgrad != nullptr); QCOMPARE(lgrad->spread(), QQuickShapeGradient::PadSpread); QCOMPARE(lgrad->x1(), 20.0f); QQmlListReference stopList(lgrad, "stops"); QCOMPARE(stopList.count(), 5); QVERIFY(stopList.at(2) != nullptr); QQuickPath *path = vp; QCOMPARE(path->startX(), 20.0f); QQmlListReference pathList(path, "pathElements"); QCOMPARE(pathList.count(), 3); } void tst_QQuickShape::changeSignals() { QScopedPointer window(createView()); window->setSource(testFileUrl("pathitem3.qml")); qApp->processEvents(); QQuickShape *obj = findItem(window->rootObject(), "pathItem"); QVERIFY(obj != nullptr); QSignalSpy asyncPropSpy(obj, SIGNAL(asynchronousChanged())); obj->setAsynchronous(true); obj->setAsynchronous(false); QCOMPARE(asyncPropSpy.count(), 2); QQmlListReference list(obj, "data"); QQuickShapePath *vp = qobject_cast(list.at(0)); QVERIFY(vp != nullptr); // Verify that VisualPath property changes emit shapePathChanged(). QSignalSpy vpChangeSpy(vp, SIGNAL(shapePathChanged())); QSignalSpy strokeColorPropSpy(vp, SIGNAL(strokeColorChanged())); vp->setStrokeColor(Qt::blue); vp->setStrokeWidth(1.0f); QQuickShapeGradient *g = vp->fillGradient(); vp->setFillGradient(nullptr); vp->setFillColor(Qt::yellow); vp->setFillRule(QQuickShapePath::WindingFill); vp->setJoinStyle(QQuickShapePath::MiterJoin); vp->setMiterLimit(5); vp->setCapStyle(QQuickShapePath::RoundCap); vp->setDashOffset(10); vp->setDashPattern(QVector() << 1 << 2 << 3 << 4); QCOMPARE(strokeColorPropSpy.count(), 1); QCOMPARE(vpChangeSpy.count(), 10); // Verify that property changes from Path and its elements bubble up and result in shapePathChanged(). QQuickPath *path = vp; path->setStartX(30); QCOMPARE(vpChangeSpy.count(), 11); QQmlListReference pathList(path, "pathElements"); qobject_cast(pathList.at(1))->setY(200); QCOMPARE(vpChangeSpy.count(), 12); // Verify that property changes from the gradient bubble up and result in shapePathChanged(). vp->setFillGradient(g); QCOMPARE(vpChangeSpy.count(), 13); QQuickShapeLinearGradient *lgrad = qobject_cast(g); lgrad->setX2(200); QCOMPARE(vpChangeSpy.count(), 14); QQmlListReference stopList(lgrad, "stops"); QCOMPARE(stopList.count(), 5); qobject_cast(stopList.at(1))->setPosition(0.3); QCOMPARE(vpChangeSpy.count(), 15); qobject_cast(stopList.at(1))->setColor(Qt::black); QCOMPARE(vpChangeSpy.count(), 16); } void tst_QQuickShape::render() { QScopedPointer window(createView()); window->setSource(testFileUrl("pathitem3.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); if ((QGuiApplication::platformName() == QLatin1String("offscreen")) || (QGuiApplication::platformName() == QLatin1String("minimal"))) QEXPECT_FAIL("", "Failure due to grabWindow not functional on offscreen/minimimal platforms", Abort); QImage img = window->grabWindow(); QVERIFY(!img.isNull()); QImage refImg(testFileUrl("pathitem3.png").toLocalFile()); QVERIFY(!refImg.isNull()); QString errorMessage; const QImage actualImg = img.convertToFormat(refImg.format()); QVERIFY2(QQuickVisualTestUtil::compareImages(actualImg, refImg, &errorMessage), qPrintable(errorMessage)); } void tst_QQuickShape::renderWithMultipleSp() { QScopedPointer window(createView()); window->setSource(testFileUrl("pathitem4.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); if ((QGuiApplication::platformName() == QLatin1String("offscreen")) || (QGuiApplication::platformName() == QLatin1String("minimal"))) QEXPECT_FAIL("", "Failure due to grabWindow not functional on offscreen/minimimal platforms", Abort); QImage img = window->grabWindow(); QVERIFY(!img.isNull()); QImage refImg(testFileUrl("pathitem4.png").toLocalFile()); QVERIFY(!refImg.isNull()); QString errorMessage; const QImage actualImg = img.convertToFormat(refImg.format()); QVERIFY2(QQuickVisualTestUtil::compareImages(actualImg, refImg, &errorMessage), qPrintable(errorMessage)); } void tst_QQuickShape::radialGrad() { QScopedPointer window(createView()); window->setSource(testFileUrl("pathitem5.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); if ((QGuiApplication::platformName() == QLatin1String("offscreen")) || (QGuiApplication::platformName() == QLatin1String("minimal"))) QEXPECT_FAIL("", "Failure due to grabWindow not functional on offscreen/minimimal platforms", Abort); QImage img = window->grabWindow(); QVERIFY(!img.isNull()); QImage refImg(testFileUrl("pathitem5.png").toLocalFile()); QVERIFY(!refImg.isNull()); QString errorMessage; const QImage actualImg = img.convertToFormat(refImg.format()); QVERIFY2(QQuickVisualTestUtil::compareImages(actualImg, refImg, &errorMessage), qPrintable(errorMessage)); } void tst_QQuickShape::conicalGrad() { QScopedPointer window(createView()); window->setSource(testFileUrl("pathitem6.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); if ((QGuiApplication::platformName() == QLatin1String("offscreen")) || (QGuiApplication::platformName() == QLatin1String("minimal"))) QEXPECT_FAIL("", "Failure due to grabWindow not functional on offscreen/minimimal platforms", Abort); QImage img = window->grabWindow(); QVERIFY(!img.isNull()); QImage refImg(testFileUrl("pathitem6.png").toLocalFile()); QVERIFY(!refImg.isNull()); QString errorMessage; const QImage actualImg = img.convertToFormat(refImg.format()); QVERIFY2(QQuickVisualTestUtil::compareImages(actualImg, refImg, &errorMessage), qPrintable(errorMessage)); } void tst_QQuickShape::renderPolyline() { QScopedPointer window(createView()); window->setSource(testFileUrl("pathitem7.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); if ((QGuiApplication::platformName() == QLatin1String("offscreen")) || (QGuiApplication::platformName() == QLatin1String("minimal"))) QEXPECT_FAIL("", "Failure due to grabWindow not functional on offscreen/minimimal platforms", Abort); QImage img = window->grabWindow(); QVERIFY(!img.isNull()); QImage refImg(testFileUrl("pathitem3.png").toLocalFile()); // It's a recreation of pathitem3 using PathPolyline QVERIFY(!refImg.isNull()); QString errorMessage; const QImage actualImg = img.convertToFormat(refImg.format()); const bool res = QQuickVisualTestUtil::compareImages(actualImg, refImg, &errorMessage); if (!res) { // For visual inspection purposes. QTest::qWait(5000); const QString &tempLocation = QStandardPaths::writableLocation(QStandardPaths::TempLocation); actualImg.save(tempLocation + QLatin1String("/pathitem7.png")); } QVERIFY2(res, qPrintable(errorMessage)); } void tst_QQuickShape::renderMultiline() { QScopedPointer window(createView()); window->setSource(testFileUrl("pathitem8.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); if ((QGuiApplication::platformName() == QLatin1String("offscreen")) || (QGuiApplication::platformName() == QLatin1String("minimal"))) QEXPECT_FAIL("", "Failure due to grabWindow not functional on offscreen/minimimal platforms", Abort); QImage img = window->grabWindow(); QVERIFY(!img.isNull()); QImage refImg(testFileUrl("pathitem8.png").toLocalFile()); QVERIFY(!refImg.isNull()); QString errorMessage; const QImage actualImg = img.convertToFormat(refImg.format()); const bool res = QQuickVisualTestUtil::compareImages(actualImg, refImg, &errorMessage); if (!res) { // For visual inspection purposes. QTest::qWait(5000); const QString &tempLocation = QStandardPaths::writableLocation(QStandardPaths::TempLocation); actualImg.save(tempLocation + QLatin1String("/pathitem8.png")); } QVERIFY2(res, qPrintable(errorMessage)); } void tst_QQuickShape::polylineDataTypes_data() { QTest::addColumn("path"); QTest::newRow("polygon") << QVariant::fromValue(m_lowPolyLogo.first()); { QVector points; points << m_lowPolyLogo.first(); QTest::newRow("vector of points") << QVariant::fromValue(points); } { QList points; for (const auto &point : m_lowPolyLogo.first()) points << point; QTest::newRow("list of points") << QVariant::fromValue(points); } { QVariantList points; for (const auto &point : m_lowPolyLogo.first()) points << point; QTest::newRow("QVariantList of points") << QVariant::fromValue(points); } { QVector points; for (const auto &point : m_lowPolyLogo.first()) points << point.toPoint(); QTest::newRow("vector of QPoint (integer points)") << QVariant::fromValue(points); } // Oddly, QPolygon is not supported, even though it's really QVector. // We don't want to have a special case for it in QQuickPathPolyline::setPath(), // but it could potentially be supported by fixing one of the QVariant conversions. } void tst_QQuickShape::polylineDataTypes() { QFETCH(QVariant, path); QScopedPointer window(createView()); window->setSource(testFileUrl("polyline.qml")); QQuickShape *shape = qobject_cast(window->rootObject()); QVERIFY(shape); shape->setProperty("path", path); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); if ((QGuiApplication::platformName() == QLatin1String("offscreen")) || (QGuiApplication::platformName() == QLatin1String("minimal"))) QEXPECT_FAIL("", "Failure due to grabWindow not functional on offscreen/minimimal platforms", Abort); QImage img = window->grabWindow(); QVERIFY(!img.isNull()); QImage refImg(testFileUrl("polyline.png").toLocalFile()); QVERIFY(!refImg.isNull()); QString errorMessage; const QImage actualImg = img.convertToFormat(refImg.format()); const bool res = QQuickVisualTestUtil::compareImages(actualImg, refImg, &errorMessage); if (!res) { // For visual inspection purposes. QTest::qWait(5000); const QString &tempLocation = QStandardPaths::writableLocation(QStandardPaths::TempLocation); actualImg.save(tempLocation + QLatin1String("/polyline.png")); } QVERIFY2(res, qPrintable(errorMessage)); QCOMPARE(shape->property("path").value>(), m_lowPolyLogo.first()); // Verify that QML sees it as an array of points int i = 0; for (QPointF p : m_lowPolyLogo.first()) { QMetaObject::invokeMethod(shape, "checkVertexAt", Q_ARG(QVariant, QVariant::fromValue(i++))); QCOMPARE(shape->property("vertexBeingChecked").toPointF(), p); } } void tst_QQuickShape::multilineDataTypes_data() { QTest::addColumn("paths"); QTest::newRow("vector of polygons") << QVariant::fromValue(m_lowPolyLogo); { QVector> paths; for (const auto &poly : m_lowPolyLogo) { QVector points; points << poly; paths << points; } QTest::newRow("vector of point vectors") << QVariant::fromValue(paths); } { QList> paths; for (const auto &poly : m_lowPolyLogo) { QVector points; points << poly; paths << points; } QTest::newRow("list of point vectors") << QVariant::fromValue(paths); } { QList> paths; for (const auto &poly : m_lowPolyLogo) { QList points; for (const auto &point : poly) points << point; paths << points; } QTest::newRow("list of point lists") << QVariant::fromValue(paths); } { QVariantList paths; for (const auto &poly : m_lowPolyLogo) { QVector points; points << poly; paths << QVariant::fromValue(points); } QTest::newRow("QVariantList of point vectors") << QVariant::fromValue(paths); } { QVariantList paths; for (const auto &poly : m_lowPolyLogo) { QList points; for (const auto &point : poly) points << point; paths << QVariant::fromValue(points); } QTest::newRow("QVariantList of point lists") << QVariant::fromValue(paths); } { QVariantList paths; for (const auto &poly : m_lowPolyLogo) { QVariantList points; for (const auto &point : poly) points << point; paths << QVariant::fromValue(points); } QTest::newRow("QVariantList of QVariantLists") << QVariant::fromValue(paths); } /* These could be supported if QVariant knew how to convert lists and vectors of QPolygon to QPolygonF. But they are omitted for now because we don't want to have special cases for them in QQuickPathMultiline::setPaths(). Floating point is preferred for geometry in Qt Quick. { QList paths; for (const auto &poly : m_lowPolyLogo) paths << poly.toPolygon(); QTest::newRow("list of QPolygon (integer points)") << QVariant::fromValue(paths); } { QVector paths; for (const auto &poly : m_lowPolyLogo) paths << poly.toPolygon(); QTest::newRow("vector of QPolygon (integer points)") << QVariant::fromValue(paths); } */ { QList> paths; for (const auto &poly : m_lowPolyLogo) { QList points; for (const auto &point : poly) points << point.toPoint(); paths << points; } QTest::newRow("list of integer point lists") << QVariant::fromValue(paths); } { QVector> paths; for (const auto &poly : m_lowPolyLogo) { QList points; for (const auto &point : poly) points << point.toPoint(); paths << points; } QTest::newRow("vector of integer point lists") << QVariant::fromValue(paths); } { QList> paths; for (const auto &poly : m_lowPolyLogo) { QVector points; for (const auto &point : poly) points << point.toPoint(); paths << points; } QTest::newRow("list of integer point vectors") << QVariant::fromValue(paths); } } void tst_QQuickShape::multilineDataTypes() { QFETCH(QVariant, paths); QScopedPointer window(createView()); window->setSource(testFileUrl("multiline.qml")); QQuickShape *shape = qobject_cast(window->rootObject()); QVERIFY(shape); shape->setProperty("paths", paths); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); if ((QGuiApplication::platformName() == QLatin1String("offscreen")) || (QGuiApplication::platformName() == QLatin1String("minimal"))) QEXPECT_FAIL("", "Failure due to grabWindow not functional on offscreen/minimimal platforms", Abort); QImage img = window->grabWindow(); QVERIFY(!img.isNull()); QImage refImg(testFileUrl("multiline.png").toLocalFile()); QVERIFY(!refImg.isNull()); QString errorMessage; const QImage actualImg = img.convertToFormat(refImg.format()); const bool res = QQuickVisualTestUtil::compareImages(actualImg, refImg, &errorMessage); if (!res) { // For visual inspection purposes. QTest::qWait(5000); const QString &tempLocation = QStandardPaths::writableLocation(QStandardPaths::TempLocation); actualImg.save(tempLocation + QLatin1String("/multiline.png")); } QVERIFY2(res, qPrintable(errorMessage)); QVector> pointVectors; for (auto v : m_lowPolyLogo) pointVectors << v; QCOMPARE(shape->property("paths").value>>(), pointVectors); // Verify that QML sees it as an array of arrays of points int i = 0; for (auto pv : m_lowPolyLogo) { int j = 0; for (QPointF p : pv) { QMetaObject::invokeMethod(shape, "checkVertexAt", Q_ARG(QVariant, QVariant::fromValue(i)), Q_ARG(QVariant, QVariant::fromValue(j++))); QCOMPARE(shape->property("vertexBeingChecked").toPointF(), p); } ++i; } } void tst_QQuickShape::multilineStronglyTyped() { QScopedPointer window(createView()); window->setSource(testFileUrl("multilineStronglyTyped.qml")); QQuickShape *shape = qobject_cast(window->rootObject()); QVERIFY(shape); PolygonProvider *provider = shape->findChild("provider"); QVERIFY(provider); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); provider->setPaths(m_lowPolyLogo); if ((QGuiApplication::platformName() == QLatin1String("offscreen")) || (QGuiApplication::platformName() == QLatin1String("minimal"))) QEXPECT_FAIL("", "Failure due to grabWindow not functional on offscreen/minimimal platforms", Abort); QImage img = window->grabWindow(); QVERIFY(!img.isNull()); QImage refImg(testFileUrl("multiline.png").toLocalFile()); QVERIFY(!refImg.isNull()); QString errorMessage; const QImage actualImg = img.convertToFormat(refImg.format()); const bool res = QQuickVisualTestUtil::compareImages(actualImg, refImg, &errorMessage); if (!res) { // For visual inspection purposes. QTest::qWait(5000); const QString &tempLocation = QStandardPaths::writableLocation(QStandardPaths::TempLocation); actualImg.save(tempLocation + QLatin1String("/multilineStronglyTyped.png")); } QVERIFY2(res, qPrintable(errorMessage)); QVector> pointVectors; for (auto v : m_lowPolyLogo) pointVectors << v; QCOMPARE(shape->property("paths").value>>(), pointVectors); // Verify that QML sees it as an array of arrays of points int i = 0; for (auto pv : m_lowPolyLogo) { int j = 0; for (QPointF p : pv) { QMetaObject::invokeMethod(shape, "checkVertexAt", Q_ARG(QVariant, QVariant::fromValue(i)), Q_ARG(QVariant, QVariant::fromValue(j++))); QCOMPARE(shape->property("vertexBeingChecked").toPointF(), p); } ++i; } } QTEST_MAIN(tst_QQuickShape) #include "tst_qquickshape.moc"