Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <qtest.h>
#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlexpression.h>
#include <QtQml/qqmlcomponent.h>
#include <QtQuick/qquickitemgrabresult.h>
#include <QtQuick/qquickview.h>
#include <QtQuick/private/qquickrectangle_p.h>
#include <private/qquickimage_p.h>
#include <private/qquickanimatedimage_p.h>
#include <QSignalSpy>
#include <QtQml/qqmlcontext.h>
#include <QtQuickTestUtils/private/testhttpserver_p.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
Q_DECLARE_METATYPE(QQuickImageBase::Status)
static QImage grabItem(QQuickItem *item)
{
const QSharedPointer<QQuickItemGrabResult> result = item->grabToImage();
if (!result)
return {};
QSignalSpy readySpy(result.data(), &QQuickItemGrabResult::ready);
if (result->image().isNull() && !readySpy.wait())
return {};
return result->image();
}
template <typename T> static T evaluate(QObject *scope, const QString &expression)
{
QQmlExpression expr(qmlContext(scope), scope, expression);
QVariant result = expr.evaluate();
if (expr.hasError())
qWarning() << expr.error().toString();
return result.value<T>();
}
template <> void evaluate<void>(QObject *scope, const QString &expression)
{
QQmlExpression expr(qmlContext(scope), scope, expression);
expr.evaluate();
if (expr.hasError())
qWarning() << expr.error().toString();
}
class tst_qquickanimatedimage : public QQmlDataTest
{
Q_OBJECT
public:
tst_qquickanimatedimage() : QQmlDataTest(QT_QMLTEST_DATADIR) {}
private:
// Helper: create an AnimatedImage loading the given image file
QQuickAnimatedImage *createAnimatedImage(QQmlComponent &component, const QString &imageFile)
{
component.setData(QByteArray("import QtQuick\nAnimatedImage { source: \"")
+ imageFile.toUtf8() + "\" }",
testFileUrl(""));
return qobject_cast<QQuickAnimatedImage *>(component.create());
}
// Common data rows for tests that should run with both GIF loop types
static void loopsTestData()
{
QTest::addColumn<QString>("imageFile");
// colors.gif: NETSCAPE loop=0 (infinite internal looping)
QTest::newRow("infinite-gif") << "colors.gif";
// colors_once.gif: NETSCAPE loop=1 (finite internal looping)
QTest::newRow("finite-gif") << "colors_once.gif";
// colors_noloop.gif: no NETSCAPE extension (loopCount=0, single pass)
QTest::newRow("noloop-gif") << "colors_noloop.gif";
// colors.svg: animated SVG with repeatCount="indefinite"
QTest::newRow("infinite-svg") << "colors.svg";
// colors_once.svg: animated SVG with repeatCount="1" (single pass)
QTest::newRow("finite-svg") << "colors_once.svg";
}
private slots:
void cleanup();
void play();
void pause();
void stopped();
void setFrame();
void frameCount();
void mirror_running();
void mirror_notRunning();
void mirror_notRunning_data();
void remote();
void remote_data();
void sourceSize();
void setSourceSize();
void sourceSizeChanges();
void sourceSizeChanges_intermediate();
void invalidSource();
void qtbug_16520();
void progressAndStatusChanges();
void playingAndPausedChanges();
void noCaching();
void sourceChangesOnFrameChanged();
void currentFrame();
void qtbug_120555();
void loopsProperties();
void loopsFinished_data();
void loopsFinished();
void loopsInfinite_data();
void loopsInfinite();
void loopsFinishBehavior_data();
void loopsFinishBehavior();
void loopsStopAndRestart_data();
void loopsStopAndRestart();
void loopsExternalInterference_data();
void loopsExternalInterference();
void loopsChangeWhilePlaying_data();
void loopsChangeWhilePlaying();
void loopsZeroDoesNotPlay_data();
void loopsZeroDoesNotPlay();
void loopsReload_data();
void loopsReload();
};
void tst_qquickanimatedimage::cleanup()
{
QQuickWindow window;
window.releaseResources();
}
void tst_qquickanimatedimage::play()
{
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("stickman.qml"));
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(component.create());
QVERIFY(anim);
QVERIFY(anim->isPlaying());
delete anim;
}
void tst_qquickanimatedimage::pause()
{
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("stickmanpause.qml"));
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(component.create());
QVERIFY(anim);
QTRY_VERIFY(anim->isPaused());
QTRY_VERIFY(anim->isPlaying());
delete anim;
}
void tst_qquickanimatedimage::stopped()
{
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("stickmanstopped.qml"));
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(component.create());
QVERIFY(anim);
QTRY_VERIFY(!anim->isPlaying());
QCOMPARE(anim->currentFrame(), 0);
delete anim;
}
void tst_qquickanimatedimage::setFrame()
{
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("stickmanpause.qml"));
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(component.create());
QVERIFY(anim);
QVERIFY(anim->isPlaying());
QCOMPARE(anim->currentFrame(), 2);
delete anim;
}
void tst_qquickanimatedimage::frameCount()
{
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("colors.qml"));
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(component.create());
QVERIFY(anim);
QVERIFY(anim->isPlaying());
QCOMPARE(anim->frameCount(), 3);
QSignalSpy frameCountChangedSpy(anim, &QQuickAnimatedImage::frameCountChanged);
const QUrl origSource = anim->source();
anim->setSource(QUrl());
QCOMPARE(anim->frameCount(), 0);
QCOMPARE(frameCountChangedSpy.size(), 1);
anim->setSource(origSource);
QCOMPARE(anim->frameCount(), 3);
QCOMPARE(frameCountChangedSpy.size(), 2);
delete anim;
}
void tst_qquickanimatedimage::mirror_running()
{
// test where mirror is set to true after animation has started
QQuickView window;
window.setSource(testFileUrl("hearts.qml"));
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(window.rootObject());
QVERIFY(anim);
int width = anim->property("width").toInt();
QCOMPARE(anim->frameCount(), 2);
QCOMPARE(anim->currentFrame(), 0);
QImage frame0 = grabItem(anim);
QVERIFY(!frame0.isNull());
anim->setCurrentFrame(1);
QCOMPARE(anim->currentFrame(), 1);
QImage frame1 = grabItem(anim);
QVERIFY(!frame1.isNull());
anim->setCurrentFrame(0);
QSignalSpy spy(anim, SIGNAL(frameChanged()));
QVERIFY(spy.isValid());
anim->setPlaying(true);
QTRY_VERIFY(spy.size() == 1); spy.clear();
anim->setMirror(true);
QCOMPARE(anim->currentFrame(), 1);
QImage frame1_flipped = grabItem(anim);
QVERIFY(!frame1_flipped.isNull());
QCOMPARE(anim->currentFrame(), 1);
QTRY_VERIFY(spy.size() == 1); spy.clear();
QCOMPARE(anim->currentFrame(), 0); // animation only has 2 frames, should cycle back to first
QImage frame0_flipped = grabItem(anim);
QVERIFY(!frame0_flipped.isNull());
QCOMPARE(anim->currentFrame(), 0);
QTransform transform;
transform.translate(width, 0).scale(-1, 1.0);
QImage frame0_expected = frame0.transformed(transform);
QImage frame1_expected = frame1.transformed(transform);
if (window.devicePixelRatio() != 1.0 && window.rendererInterface()->graphicsApi() == QSGRendererInterface::Software)
QSKIP("QTBUG-53823");
QCOMPARE(frame0_flipped, frame0_expected);
if (window.devicePixelRatio() != 1.0 && window.rendererInterface()->graphicsApi() == QSGRendererInterface::Software)
QSKIP("QTBUG-53823");
QCOMPARE(frame1_flipped, frame1_expected);
delete anim;
}
void tst_qquickanimatedimage::mirror_notRunning()
{
QFETCH(QUrl, fileUrl);
QQuickView window;
window.setSource(fileUrl);
window.show();
QTRY_VERIFY(window.isExposed());
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(window.rootObject());
QVERIFY(anim);
int width = anim->property("width").toInt();
QImage screenshot = grabItem(anim);
QVERIFY(!screenshot.isNull());
QTransform transform;
transform.translate(width, 0).scale(-1, 1.0);
QImage expected = screenshot.transformed(transform);
int frame = anim->currentFrame();
bool playing = anim->isPlaying();
bool paused = anim->isPaused();
anim->setProperty("mirror", true);
screenshot = grabItem(anim);
QVERIFY(!screenshot.isNull());
if (window.devicePixelRatio() != 1.0 && window.rendererInterface()->graphicsApi() == QSGRendererInterface::Software)
QSKIP("QTBUG-53823");
QCOMPARE(screenshot, expected);
// mirroring should not change the current frame or playing status
QCOMPARE(anim->currentFrame(), frame);
QCOMPARE(anim->isPlaying(), playing);
QCOMPARE(anim->isPaused(), paused);
delete anim;
}
void tst_qquickanimatedimage::mirror_notRunning_data()
{
QTest::addColumn<QUrl>("fileUrl");
QTest::newRow("paused") << testFileUrl("stickmanpause.qml");
QTest::newRow("stopped") << testFileUrl("stickmanstopped.qml");
}
void tst_qquickanimatedimage::remote_data()
{
QTest::addColumn<QString>("fileName");
QTest::addColumn<bool>("paused");
QTest::newRow("playing") << "stickman.qml" << false;
QTest::newRow("paused") << "stickmanpause.qml" << true;
}
void tst_qquickanimatedimage::remote()
{
QFETCH(QString, fileName);
QFETCH(bool, paused);
ThreadedTestHTTPServer server(dataDirectory());
QQmlEngine engine;
QQmlComponent component(&engine, server.url(fileName));
QTRY_VERIFY(component.isReady());
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(component.create());
QVERIFY(anim);
QTRY_VERIFY(anim->isPlaying());
if (paused) {
QTRY_VERIFY(anim->isPaused());
QCOMPARE(anim->currentFrame(), 2);
}
QVERIFY(anim->status() != QQuickAnimatedImage::Error);
delete anim;
}
void tst_qquickanimatedimage::sourceSize()
{
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("stickmanscaled.qml"));
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(component.create());
QVERIFY(anim);
QCOMPARE(anim->width(),240.0);
QCOMPARE(anim->height(),180.0);
QCOMPARE(anim->sourceSize(),QSize(160,120));
delete anim;
}
void tst_qquickanimatedimage::setSourceSize()
{
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("stickmansourcesized.qml"));
QScopedPointer<QQuickAnimatedImage> anim(qobject_cast<QQuickAnimatedImage *>(component.create()));
QVERIFY(anim);
QCOMPARE(anim->sourceSize(), QSize(80, 60));
anim->setSourceSize(QSize(40, 30));
QCOMPARE(anim->sourceSize(), QSize(40, 30));
anim->setSourceSize(QSize());
QCOMPARE(anim->sourceSize(), QSize(160, 120));
}
void tst_qquickanimatedimage::invalidSource()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.setData("import QtQuick 2.0\n AnimatedImage { source: \"no-such-file.gif\" }", QUrl::fromLocalFile("relative"));
QVERIFY(component.isReady());
QTest::ignoreMessage(QtWarningMsg, "file:relative:2:2: QML AnimatedImage: Error Reading Animated Image File file:no-such-file.gif");
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(component.create());
QVERIFY(anim);
QVERIFY(anim->isPlaying());
QVERIFY(!anim->isPaused());
QCOMPARE(anim->currentFrame(), 0);
QCOMPARE(anim->frameCount(), 0);
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Error);
delete anim;
}
void tst_qquickanimatedimage::sourceSizeChanges()
{
TestHTTPServer server;
QVERIFY2(server.listen(), qPrintable(server.errorString()));
server.serveDirectory(dataDirectory());
QQmlEngine engine;
QQmlComponent component(&engine);
component.setData("import QtQuick 2.0\nAnimatedImage { source: srcImage }", QUrl::fromLocalFile(""));
QTRY_VERIFY(component.isReady());
QQmlContext *ctxt = engine.rootContext();
ctxt->setContextProperty("srcImage", "");
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage*>(component.create());
QVERIFY(anim != nullptr);
QSignalSpy sourceSizeSpy(anim, SIGNAL(sourceSizeChanged()));
// Local
ctxt->setContextProperty("srcImage", QUrl(""));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Null);
QTRY_COMPARE(sourceSizeSpy.size(), 0);
ctxt->setContextProperty("srcImage", testFileUrl("hearts.gif"));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Ready);
QTRY_COMPARE(sourceSizeSpy.size(), 1);
ctxt->setContextProperty("srcImage", testFileUrl("hearts.gif"));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Ready);
QTRY_COMPARE(sourceSizeSpy.size(), 1);
ctxt->setContextProperty("srcImage", testFileUrl("hearts_copy.gif"));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Ready);
QTRY_COMPARE(sourceSizeSpy.size(), 1);
ctxt->setContextProperty("srcImage", testFileUrl("colors.gif"));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Ready);
QTRY_COMPARE(sourceSizeSpy.size(), 2);
ctxt->setContextProperty("srcImage", QUrl(""));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Null);
QTRY_COMPARE(sourceSizeSpy.size(), 3);
// Remote
ctxt->setContextProperty("srcImage", server.url("/hearts.gif"));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Ready);
QTRY_COMPARE(sourceSizeSpy.size(), 4);
ctxt->setContextProperty("srcImage", server.url("/hearts.gif"));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Ready);
QTRY_COMPARE(sourceSizeSpy.size(), 4);
ctxt->setContextProperty("srcImage", server.url("/hearts_copy.gif"));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Ready);
QTRY_COMPARE(sourceSizeSpy.size(), 4);
ctxt->setContextProperty("srcImage", server.url("/colors.gif"));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Ready);
QTRY_COMPARE(sourceSizeSpy.size(), 5);
ctxt->setContextProperty("srcImage", QUrl(""));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Null);
QTRY_COMPARE(sourceSizeSpy.size(), 6);
delete anim;
}
void tst_qquickanimatedimage::sourceSizeChanges_intermediate()
{
QQmlEngine engine;
QQmlComponent component(&engine);
component.setData("import QtQuick 2.0\nAnimatedImage { readonly property int testWidth: status === AnimatedImage.Ready ? sourceSize.width : -1; source: srcImage }", QUrl::fromLocalFile(""));
QTRY_VERIFY(component.isReady());
QQmlContext *ctxt = engine.rootContext();
ctxt->setContextProperty("srcImage", "");
QScopedPointer<QQuickAnimatedImage> anim(qobject_cast<QQuickAnimatedImage*>(component.create()));
QVERIFY(anim != nullptr);
ctxt->setContextProperty("srcImage", testFileUrl("hearts.gif"));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Ready);
QTRY_COMPARE(anim->property("testWidth").toInt(), anim->sourceSize().width());
ctxt->setContextProperty("srcImage", testFileUrl("hearts_copy.gif"));
QTRY_COMPARE(anim->status(), QQuickAnimatedImage::Ready);
QTRY_COMPARE(anim->property("testWidth").toInt(), anim->sourceSize().width());
}
void tst_qquickanimatedimage::qtbug_16520()
{
TestHTTPServer server;
QVERIFY2(server.listen(), qPrintable(server.errorString()));
server.serveDirectory(dataDirectory());
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("qtbug-16520.qml"));
QTRY_VERIFY(component.isReady());
QQuickRectangle *root = qobject_cast<QQuickRectangle *>(component.create());
QVERIFY(root);
QQuickAnimatedImage *anim = root->findChild<QQuickAnimatedImage*>("anim");
QVERIFY(anim != nullptr);
anim->setProperty("source", server.urlString("/stickman.gif"));
QTRY_COMPARE(anim->opacity(), qreal(0));
QTRY_COMPARE_WITH_TIMEOUT(anim->opacity(), qreal(1), 2s);
delete anim;
delete root;
}
void tst_qquickanimatedimage::progressAndStatusChanges()
{
TestHTTPServer server;
QVERIFY2(server.listen(), qPrintable(server.errorString()));
server.serveDirectory(dataDirectory());
QQmlEngine engine;
QString componentStr = "import QtQuick 2.0\nAnimatedImage { source: srcImage }";
QQmlContext *ctxt = engine.rootContext();
ctxt->setContextProperty("srcImage", testFileUrl("stickman.gif"));
QQmlComponent component(&engine);
component.setData(componentStr.toLatin1(), QUrl::fromLocalFile(""));
QQuickImage *obj = qobject_cast<QQuickImage*>(component.create());
QVERIFY(obj != nullptr);
QCOMPARE(obj->status(), QQuickImage::Ready);
QTRY_COMPARE(obj->progress(), 1.0);
qRegisterMetaType<QQuickImageBase::Status>();
QSignalSpy sourceSpy(obj, SIGNAL(sourceChanged(QUrl)));
QSignalSpy progressSpy(obj, SIGNAL(progressChanged(qreal)));
QSignalSpy statusSpy(obj, SIGNAL(statusChanged(QQuickImageBase::Status)));
// Same image
ctxt->setContextProperty("srcImage", testFileUrl("stickman.gif"));
QTRY_COMPARE(obj->status(), QQuickImage::Ready);
QTRY_COMPARE(obj->progress(), 1.0);
QTRY_COMPARE(sourceSpy.size(), 0);
QTRY_COMPARE(progressSpy.size(), 0);
QTRY_COMPARE(statusSpy.size(), 0);
// Loading local file
ctxt->setContextProperty("srcImage", testFileUrl("colors.gif"));
QTRY_COMPARE(obj->status(), QQuickImage::Ready);
QTRY_COMPARE(obj->progress(), 1.0);
QTRY_COMPARE(sourceSpy.size(), 1);
QTRY_COMPARE(progressSpy.size(), 0);
QTRY_COMPARE(statusSpy.size(), 1);
// Loading remote file
ctxt->setContextProperty("srcImage", server.url("/stickman.gif"));
QTRY_COMPARE(obj->status(), QQuickImage::Loading);
QTRY_COMPARE(obj->progress(), 0.0);
QTRY_COMPARE(obj->status(), QQuickImage::Ready);
QTRY_COMPARE(obj->progress(), 1.0);
QTRY_COMPARE(sourceSpy.size(), 2);
QTRY_VERIFY(progressSpy.size() > 1);
QTRY_COMPARE(statusSpy.size(), 3);
ctxt->setContextProperty("srcImage", "");
QTRY_COMPARE(obj->status(), QQuickImage::Null);
QTRY_COMPARE(obj->progress(), 0.0);
QTRY_COMPARE(sourceSpy.size(), 3);
QTRY_VERIFY(progressSpy.size() > 2);
QTRY_COMPARE(statusSpy.size(), 4);
delete obj;
}
void tst_qquickanimatedimage::playingAndPausedChanges()
{
QQmlEngine engine;
QString componentStr = "import QtQuick 2.0\nAnimatedImage { source: srcImage }";
QQmlContext *ctxt = engine.rootContext();
ctxt->setContextProperty("srcImage", QUrl(""));
QQmlComponent component(&engine);
component.setData(componentStr.toLatin1(), QUrl::fromLocalFile(""));
QQuickAnimatedImage *obj = qobject_cast<QQuickAnimatedImage*>(component.create());
QVERIFY(obj != nullptr);
QCOMPARE(obj->status(), QQuickAnimatedImage::Null);
QTRY_VERIFY(obj->isPlaying());
QTRY_VERIFY(!obj->isPaused());
QSignalSpy playingSpy(obj, SIGNAL(playingChanged()));
QSignalSpy pausedSpy(obj, SIGNAL(pausedChanged()));
// initial state
obj->setProperty("playing", true);
obj->setProperty("paused", false);
QTRY_VERIFY(obj->isPlaying());
QTRY_VERIFY(!obj->isPaused());
QTRY_COMPARE(playingSpy.size(), 0);
QTRY_COMPARE(pausedSpy.size(), 0);
obj->setProperty("playing", false);
obj->setProperty("paused", true);
QTRY_VERIFY(!obj->isPlaying());
QTRY_VERIFY(obj->isPaused());
QTRY_COMPARE(playingSpy.size(), 1);
QTRY_COMPARE(pausedSpy.size(), 1);
obj->setProperty("playing", true);
obj->setProperty("paused", false);
QTRY_VERIFY(obj->isPlaying());
QTRY_VERIFY(!obj->isPaused());
QTRY_COMPARE(playingSpy.size(), 2);
QTRY_COMPARE(pausedSpy.size(), 2);
ctxt->setContextProperty("srcImage", testFileUrl("stickman.gif"));
QTRY_VERIFY(obj->isPlaying());
QTRY_VERIFY(!obj->isPaused());
QTRY_COMPARE(playingSpy.size(), 2);
QTRY_COMPARE(pausedSpy.size(), 2);
obj->setProperty("paused", true);
QTRY_VERIFY(obj->isPlaying());
QTRY_VERIFY(obj->isPaused());
QTRY_COMPARE(playingSpy.size(), 2);
QTRY_COMPARE(pausedSpy.size(), 3);
obj->setProperty("playing", false);
QTRY_VERIFY(!obj->isPlaying());
QTRY_VERIFY(!obj->isPaused());
QTRY_COMPARE(playingSpy.size(), 3);
QTRY_COMPARE(pausedSpy.size(), 4);
obj->setProperty("playing", true);
// Cannot animate this image, playing will be false
ctxt->setContextProperty("srcImage", testFileUrl("green.png"));
QTRY_VERIFY_WITH_TIMEOUT(!obj->isPlaying(), 2s);
QTRY_VERIFY(!obj->isPaused());
QTRY_COMPARE(playingSpy.size(), 5);
QTRY_COMPARE(pausedSpy.size(), 4);
delete obj;
}
void tst_qquickanimatedimage::noCaching()
{
QQuickView window, window_nocache;
window.setSource(testFileUrl("colors.qml"));
window_nocache.setSource(testFileUrl("colors_nocache.qml"));
window.show();
window_nocache.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
QVERIFY(QTest::qWaitForWindowExposed(&window_nocache));
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(window.rootObject());
QVERIFY(anim);
QQuickAnimatedImage *anim_nocache = qobject_cast<QQuickAnimatedImage *>(window_nocache.rootObject());
QVERIFY(anim_nocache);
QCOMPARE(anim->frameCount(), anim_nocache->frameCount());
anim->setPlaying(false);
anim_nocache->setPlaying(false);
// colors.gif only has 3 frames so this should be fast
for (int loops = 0; loops <= 2; ++loops) {
for (int frame = 0; frame < anim->frameCount(); ++frame) {
anim->setCurrentFrame(frame);
anim_nocache->setCurrentFrame(frame);
QImage image_cache = grabItem(anim);
QVERIFY(!image_cache.isNull());
QImage image_nocache = grabItem(anim_nocache);
QVERIFY(!image_nocache.isNull());
QCOMPARE(image_cache, image_nocache);
}
}
}
void tst_qquickanimatedimage::sourceChangesOnFrameChanged()
{
QQmlEngine engine;
QQmlComponent component(&engine, testFileUrl("colors.qml"));
QList<QQuickAnimatedImage*> images;
// Run multiple animations in parallel, this should be fast
for (int loops = 0; loops < 25; ++loops) {
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(component.create());
// QTBUG-67427: this should not produce a segfault
QObject::connect(anim,
&QQuickAnimatedImage::frameChanged,
anim,
[this, anim]() { anim->setSource(testFileUrl("hearts.gif")); });
QVERIFY(anim);
QVERIFY(anim->isPlaying());
images.append(anim);
}
for (auto *anim : images)
QTRY_COMPARE(anim->source(), testFileUrl("hearts.gif"));
qDeleteAll(images);
}
void tst_qquickanimatedimage::currentFrame()
{
QQuickView window;
window.setSource(testFileUrl("currentframe.qml"));
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage *>(window.rootObject());
QVERIFY(anim);
QSignalSpy frameChangedSpy(anim, SIGNAL(frameChanged()));
QSignalSpy currentFrameChangedSpy(anim, SIGNAL(currentFrameChanged()));
anim->setCurrentFrame(1);
QCOMPARE(anim->currentFrame(), 1);
QCOMPARE(frameChangedSpy.size(), 1);
QCOMPARE(currentFrameChangedSpy.size(), 1);
QCOMPARE(anim->property("currentFrameChangeCount"), 1);
QCOMPARE(anim->property("frameChangeCount"), 1);
evaluate<void>(anim, "scriptedSetCurrentFrame(2)");
QCOMPARE(anim->currentFrame(), 2);
QCOMPARE(frameChangedSpy.size(), 2);
QCOMPARE(currentFrameChangedSpy.size(), 2);
QCOMPARE(anim->property("currentFrameChangeCount"), 2);
QCOMPARE(anim->property("frameChangeCount"), 2);
}
void tst_qquickanimatedimage::qtbug_120555()
{
TestHTTPServer server;
QVERIFY2(server.listen(), qPrintable(server.errorString()));
server.serveDirectory(dataDirectory());
QQmlEngine engine;
QQmlComponent component(&engine);
component.setData("import QtQuick 2.0\nAnimatedImage {}", {});
QQuickAnimatedImage *anim = qobject_cast<QQuickAnimatedImage*>(component.create());
QVERIFY(anim);
anim->setSource(server.url("/stickman.gif"));
QTRY_COMPARE(anim->status(), QQuickImage::Loading);
anim->setFillMode(QQuickImage::PreserveAspectFit);
QCOMPARE(anim->fillMode(), QQuickImage::PreserveAspectFit);
anim->setMipmap(true);
QCOMPARE(anim->mipmap(), true);
anim->setCache(false);
QCOMPARE(anim->cache(), false);
anim->setSourceSize(QSize(200, 200));
QCOMPARE(anim->sourceSize(), QSize(200, 200));
QTRY_COMPARE(anim->status(), QQuickImage::Ready);
delete anim;
}
void tst_qquickanimatedimage::loopsProperties()
{
// Default values
QQuickAnimatedImage image;
QCOMPARE(image.loops(), -1);
QCOMPARE(static_cast<int>(QQuickAnimatedImage::Infinite), -1);
QCOMPARE(image.finishBehavior(), QQuickAnimatedImage::FinishAtInitialFrame);
// loops setter/getter
QSignalSpy loopsSpy(&image, &QQuickAnimatedImage::loopsChanged);
image.setLoops(3);
QCOMPARE(image.loops(), 3);
QCOMPARE(loopsSpy.size(), 1);
// Same value does not emit
image.setLoops(3);
QCOMPARE(loopsSpy.size(), 1);
// Invalid values normalize to Infinite
image.setLoops(-5);
QCOMPARE(image.loops(), -1);
// Zero means "do not play"
image.setLoops(0);
QCOMPARE(image.loops(), 0);
// finishBehavior setter/getter
QSignalSpy fbSpy(&image, &QQuickAnimatedImage::finishBehaviorChanged);
image.setFinishBehavior(QQuickAnimatedImage::FinishAtFinalFrame);
QCOMPARE(image.finishBehavior(), QQuickAnimatedImage::FinishAtFinalFrame);
QCOMPARE(fbSpy.size(), 1);
// Same value does not emit
image.setFinishBehavior(QQuickAnimatedImage::FinishAtFinalFrame);
QCOMPARE(fbSpy.size(), 1);
// QML-level access
QQmlEngine engine;
QQmlComponent component(&engine);
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, "stickman.gif"));
QVERIFY(anim);
anim->setLoops(2);
QCOMPARE(anim->loops(), 2);
QCOMPARE(evaluate<int>(anim.data(), "AnimatedImage.Infinite"), -1);
QCOMPARE(evaluate<int>(anim.data(), "AnimatedImage.FinishAtInitialFrame"), 0);
QCOMPARE(evaluate<int>(anim.data(), "AnimatedImage.FinishAtFinalFrame"), 1);
}
void tst_qquickanimatedimage::loopsFinished_data()
{
loopsTestData();
}
void tst_qquickanimatedimage::loopsFinished()
{
QFETCH(QString, imageFile);
QQmlEngine engine;
QQmlComponent component(&engine);
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, imageFile));
QVERIFY(anim);
anim->setLoops(3);
QVERIFY(anim->isPlaying());
QSignalSpy finishedSpy(anim.data(), &QQuickAnimatedImage::finished);
QSignalSpy frameSpy(anim.data(), &QQuickAnimatedImage::frameChanged);
QTRY_VERIFY_WITH_TIMEOUT(!anim->isPlaying(), 30000);
QCOMPARE(finishedSpy.size(), 1);
// 3 frames per cycle * 3 loops = 9, minus 1 because the initial
// frame at startup does not emit frameChanged.
QVERIFY2(frameSpy.size() >= 8,
qPrintable(QString("Expected >= 8 frame changes for 3 loops, got %1")
.arg(frameSpy.size())));
}
void tst_qquickanimatedimage::loopsInfinite_data()
{
loopsTestData();
}
void tst_qquickanimatedimage::loopsInfinite()
{
QFETCH(QString, imageFile);
QQmlEngine engine;
QQmlComponent component(&engine);
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, imageFile));
QVERIFY(anim);
QCOMPARE(anim->loops(), -1);
QVERIFY(anim->isPlaying());
QSignalSpy finishedSpy(anim.data(), &QQuickAnimatedImage::finished);
const int previousFrame = anim->currentFrame();
QTRY_VERIFY(anim->currentFrame() != previousFrame);
QCOMPARE(finishedSpy.size(), 0);
QVERIFY(anim->isPlaying());
}
void tst_qquickanimatedimage::loopsFinishBehavior_data()
{
loopsTestData();
}
void tst_qquickanimatedimage::loopsFinishBehavior()
{
QFETCH(QString, imageFile);
// Helper: sample the center pixel color from a window grab
auto samplePixelColor = [](QQuickView &window) -> QColor {
QImage img = window.grabWindow();
return img.isNull() ? QColor() : img.pixelColor(1, 1);
};
// FinishAtInitialFrame (default) — stops at frame 0 and renders it
{
QQuickView window;
QQmlComponent component(window.engine());
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, imageFile));
QVERIFY(anim);
anim->setParentItem(window.contentItem());
window.resize(anim->width(), anim->height());
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
// Capture the reference color for frame 0
anim->setPlaying(false);
anim->setCurrentFrame(0);
const QColor refInitialColor = samplePixelColor(window);
QVERIFY(refInitialColor.isValid());
// Also capture the last frame color — must differ from frame 0
anim->setCurrentFrame(anim->frameCount() - 1);
const QColor refFinalColor = samplePixelColor(window);
QVERIFY(refInitialColor != refFinalColor);
anim->setLoops(1);
anim->setPlaying(true);
QSignalSpy finishedSpy(anim.data(), &QQuickAnimatedImage::finished);
QTRY_VERIFY_WITH_TIMEOUT(!anim->isPlaying(), 30000);
QCOMPARE(finishedSpy.size(), 1);
QCOMPARE(anim->currentFrame(), 0);
// Verify the actually rendered pixel matches the initial frame
const QColor actualColor = samplePixelColor(window);
QCOMPARE(actualColor, refInitialColor);
}
// FinishAtFinalFrame — stops at last frame and renders it
{
QQuickView window;
QQmlComponent component(window.engine());
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, imageFile));
QVERIFY(anim);
anim->setParentItem(window.contentItem());
window.resize(anim->width(), anim->height());
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
const int lastFrame = anim->frameCount() - 1;
// Capture reference colors for both the initial and final frames
anim->setPlaying(false);
anim->setCurrentFrame(0);
const QColor refInitialColor = samplePixelColor(window);
anim->setCurrentFrame(lastFrame);
const QColor refFinalColor = samplePixelColor(window);
QVERIFY(refInitialColor != refFinalColor);
anim->setLoops(1);
anim->setFinishBehavior(QQuickAnimatedImage::FinishAtFinalFrame);
anim->setPlaying(true);
QSignalSpy finishedSpy(anim.data(), &QQuickAnimatedImage::finished);
QTRY_VERIFY_WITH_TIMEOUT(!anim->isPlaying(), 30000);
QCOMPARE(finishedSpy.size(), 1);
QCOMPARE(anim->currentFrame(), lastFrame);
// Verify the actually rendered pixel matches the final frame
const QColor actualColor = samplePixelColor(window);
QCOMPARE(actualColor, refFinalColor);
QVERIFY(actualColor != refInitialColor);
}
}
void tst_qquickanimatedimage::loopsStopAndRestart_data()
{
loopsTestData();
}
void tst_qquickanimatedimage::loopsStopAndRestart()
{
QFETCH(QString, imageFile);
QQuickView window;
QQmlComponent component(window.engine());
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, imageFile));
QVERIFY(anim);
anim->setParentItem(window.contentItem());
window.resize(anim->width(), anim->height());
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
auto samplePixelColor = [](QQuickView &w) -> QColor {
QImage img = w.grabWindow();
return img.isNull() ? QColor() : img.pixelColor(1, 1);
};
// Capture reference color for frame 0
anim->setPlaying(false);
anim->setCurrentFrame(0);
const QColor refInitialColor = samplePixelColor(window);
anim->setLoops(1);
QSignalSpy finishedSpy(anim.data(), &QQuickAnimatedImage::finished);
// Manual stop should not emit finished()
anim->setPlaying(false);
QVERIFY(!anim->isPlaying());
QCOMPARE(finishedSpy.size(), 0);
// Restart — should play the full loop count and emit finished()
anim->setPlaying(true);
QVERIFY(anim->isPlaying());
QTRY_VERIFY_WITH_TIMEOUT(!anim->isPlaying(), 30000);
QCOMPARE(finishedSpy.size(), 1);
QCOMPARE(samplePixelColor(window), refInitialColor);
// Second restart — should work again and render correctly
anim->setPlaying(true);
QVERIFY(anim->isPlaying());
QTRY_VERIFY_WITH_TIMEOUT(!anim->isPlaying(), 30000);
QCOMPARE(finishedSpy.size(), 2);
QCOMPARE(samplePixelColor(window), refInitialColor);
}
/*
External interference (setCurrentFrame, pause/resume) during playback
should not corrupt loop counting.
*/
void tst_qquickanimatedimage::loopsExternalInterference_data()
{
loopsTestData();
}
void tst_qquickanimatedimage::loopsExternalInterference()
{
QFETCH(QString, imageFile);
QQmlEngine engine;
QQmlComponent component(&engine);
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, imageFile));
QVERIFY(anim);
anim->setLoops(3);
QSignalSpy finishedSpy(anim.data(), &QQuickAnimatedImage::finished);
// setCurrentFrame() back to 0 should not trigger false wrap-around
QTRY_VERIFY(anim->currentFrame() > 0);
anim->setCurrentFrame(0);
QTRY_VERIFY(anim->currentFrame() > 0);
anim->setCurrentFrame(0);
// Pause and resume should not interfere with counting
QTRY_VERIFY(anim->currentFrame() > 0);
anim->setPaused(true);
QVERIFY(anim->isPaused());
QCOMPARE(finishedSpy.size(), 0);
anim->setPaused(false);
// Should still complete all 3 loops normally
QTRY_VERIFY_WITH_TIMEOUT(!anim->isPlaying(), 30000);
QCOMPARE(finishedSpy.size(), 1);
}
/*
Changing loops while the animation is playing should take effect.
*/
void tst_qquickanimatedimage::loopsChangeWhilePlaying_data()
{
loopsTestData();
}
void tst_qquickanimatedimage::loopsChangeWhilePlaying()
{
QFETCH(QString, imageFile);
// Increase loops — animation continues and finishes with new count
{
QQmlEngine engine;
QQmlComponent component(&engine);
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, imageFile));
QVERIFY(anim);
anim->setLoops(2);
QSignalSpy finishedSpy(anim.data(), &QQuickAnimatedImage::finished);
QTRY_VERIFY(anim->currentFrame() > 0);
anim->setLoops(5);
QVERIFY(anim->isPlaying());
QTRY_VERIFY_WITH_TIMEOUT(!anim->isPlaying(), 30000);
QCOMPARE(finishedSpy.size(), 1);
}
// Switch to Infinite — animation never stops on its own
{
QQmlEngine engine;
QQmlComponent component(&engine);
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, imageFile));
QVERIFY(anim);
anim->setLoops(2);
QSignalSpy finishedSpy(anim.data(), &QQuickAnimatedImage::finished);
QTRY_VERIFY(anim->currentFrame() > 0);
anim->setLoops(QQuickAnimatedImage::Infinite);
const int frame = anim->currentFrame();
QTRY_VERIFY(anim->currentFrame() != frame);
QVERIFY(anim->isPlaying());
QCOMPARE(finishedSpy.size(), 0);
anim->setPlaying(false);
}
}
void tst_qquickanimatedimage::loopsZeroDoesNotPlay_data()
{
loopsTestData();
}
void tst_qquickanimatedimage::loopsZeroDoesNotPlay()
{
QFETCH(QString, imageFile);
QQmlEngine engine;
QQmlComponent component(&engine);
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, imageFile));
QVERIFY(anim);
anim->setLoops(0);
QCOMPARE(anim->loops(), 0);
// The animation should not be running
QVERIFY(!anim->isPlaying());
QSignalSpy finishedSpy(anim.data(), &QQuickAnimatedImage::finished);
QSignalSpy frameSpy(anim.data(), &QQuickAnimatedImage::frameChanged);
// Explicitly trying to play should have no effect
anim->setPlaying(true);
QVERIFY(!anim->isPlaying());
// Wait a bit to confirm nothing happens
QTest::qWait(500);
QVERIFY(!anim->isPlaying());
QCOMPARE(finishedSpy.size(), 0);
QCOMPARE(frameSpy.size(), 0);
}
void tst_qquickanimatedimage::loopsReload_data()
{
loopsTestData();
}
void tst_qquickanimatedimage::loopsReload()
{
QFETCH(QString, imageFile);
QQmlEngine engine;
QQmlComponent component(&engine);
QScopedPointer<QQuickAnimatedImage> anim(createAnimatedImage(component, imageFile));
QVERIFY(anim);
anim->setLoops(2);
QVERIFY(anim->isPlaying());
// Reload the image in the middle of the first loop. The reloaded
// animation starts from the first frame and plays all the loops.
QTRY_VERIFY_WITH_TIMEOUT(anim->currentFrame() >= 1, 10000);
QSignalSpy finishedSpy(anim.data(), &QQuickAnimatedImage::finished);
QSignalSpy frameSpy(anim.data(), &QQuickAnimatedImage::frameChanged);
anim->setCache(!anim->cache());
QTRY_VERIFY_WITH_TIMEOUT(!anim->isPlaying(), 30000);
QCOMPARE(finishedSpy.size(), 1);
// 3 frames per cycle * 2 loops after reloading.
QVERIFY2(frameSpy.size() >= 6,
qPrintable(QString("Expected >= 6 frame changes for 2 loops, got %1")
.arg(frameSpy.size())));
}
QTEST_MAIN(tst_qquickanimatedimage)
#include "tst_qquickanimatedimage.moc"