File indexing completed on 2024-11-10 04:55:55

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2018 David Edmundson <davidedmundson@kde.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "kwin_wayland_test.h"
0011 
0012 #include "effect/anidata_p.h"
0013 #include "effect/effecthandler.h"
0014 #include "effect/effectloader.h"
0015 #include "scripting/scriptedeffect.h"
0016 #include "virtualdesktops.h"
0017 #include "wayland_server.h"
0018 #include "window.h"
0019 #include "workspace.h"
0020 
0021 #include <KConfigGroup>
0022 #include <KGlobalAccel>
0023 #include <KWayland/Client/compositor.h>
0024 #include <KWayland/Client/connection_thread.h>
0025 #include <KWayland/Client/registry.h>
0026 #include <KWayland/Client/slide.h>
0027 #include <KWayland/Client/surface.h>
0028 
0029 #include <QJSValue>
0030 #include <QQmlEngine>
0031 
0032 using namespace KWin;
0033 using namespace std::chrono_literals;
0034 
0035 static const QString s_socketName = QStringLiteral("wayland_test_effects_scripts-0");
0036 
0037 class ScriptedEffectsTest : public QObject
0038 {
0039     Q_OBJECT
0040 private Q_SLOTS:
0041     void initTestCase();
0042     void init();
0043     void cleanup();
0044 
0045     void testEffectsHandler();
0046     void testEffectsContext();
0047     void testShortcuts();
0048     void testAnimations_data();
0049     void testAnimations();
0050     void testScreenEdge();
0051     void testScreenEdgeTouch();
0052     void testFullScreenEffect_data();
0053     void testFullScreenEffect();
0054     void testKeepAlive_data();
0055     void testKeepAlive();
0056     void testGrab();
0057     void testGrabAlreadyGrabbedWindow();
0058     void testGrabAlreadyGrabbedWindowForced();
0059     void testUngrab();
0060     void testRedirect_data();
0061     void testRedirect();
0062     void testComplete();
0063 
0064 private:
0065     ScriptedEffect *loadEffect(const QString &name);
0066 };
0067 
0068 class ScriptedEffectWithDebugSpy : public KWin::ScriptedEffect
0069 {
0070     Q_OBJECT
0071 public:
0072     ScriptedEffectWithDebugSpy();
0073     bool load(const QString &name);
0074     using AnimationEffect::AniMap;
0075     using AnimationEffect::state;
0076     Q_INVOKABLE void sendTestResponse(const QString &out); // proxies triggers out from the tests
0077     QList<QAction *> actions(); // returns any QActions owned by the ScriptEngine
0078 Q_SIGNALS:
0079     void testOutput(const QString &data);
0080 };
0081 
0082 void ScriptedEffectWithDebugSpy::sendTestResponse(const QString &out)
0083 {
0084     Q_EMIT testOutput(out);
0085 }
0086 
0087 QList<QAction *> ScriptedEffectWithDebugSpy::actions()
0088 {
0089     return findChildren<QAction *>(QString(), Qt::FindDirectChildrenOnly);
0090 }
0091 
0092 ScriptedEffectWithDebugSpy::ScriptedEffectWithDebugSpy()
0093     : ScriptedEffect()
0094 {
0095 }
0096 
0097 bool ScriptedEffectWithDebugSpy::load(const QString &name)
0098 {
0099     auto selfContext = engine()->newQObject(this);
0100     QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
0101     const QString path = QFINDTESTDATA("./scripts/" + name + ".js");
0102     engine()->globalObject().setProperty("sendTestResponse", selfContext.property("sendTestResponse"));
0103     if (!init(name, path)) {
0104         return false;
0105     }
0106 
0107     // inject our newly created effect to be registered with the EffectsHandler::loaded_effects
0108     // this is private API so some horrible code is used to find the internal effectloader
0109     // and register ourselves
0110     auto children = effects->children();
0111     for (auto it = children.begin(); it != children.end(); ++it) {
0112         if (qstrcmp((*it)->metaObject()->className(), "KWin::EffectLoader") != 0) {
0113             continue;
0114         }
0115         QMetaObject::invokeMethod(*it, "effectLoaded", Q_ARG(KWin::Effect *, this), Q_ARG(QString, name));
0116         break;
0117     }
0118 
0119     return effects->isEffectLoaded(name);
0120 }
0121 
0122 void ScriptedEffectsTest::initTestCase()
0123 {
0124     if (!Test::renderNodeAvailable()) {
0125         QSKIP("no render node available");
0126         return;
0127     }
0128     qRegisterMetaType<KWin::Window *>();
0129     qRegisterMetaType<KWin::Effect *>();
0130     QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
0131     QVERIFY(waylandServer()->init(s_socketName));
0132     Test::setOutputConfig({QRect(0, 0, 1280, 1024)});
0133 
0134     // disable all effects - we don't want to have it interact with the rendering
0135     auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
0136     KConfigGroup plugins(config, QStringLiteral("Plugins"));
0137     const auto builtinNames = EffectLoader().listOfKnownEffects();
0138     for (QString name : builtinNames) {
0139         plugins.writeEntry(name + QStringLiteral("Enabled"), false);
0140     }
0141 
0142     config->sync();
0143     kwinApp()->setConfig(config);
0144 
0145     qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2"));
0146     qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1");
0147     kwinApp()->start();
0148     QVERIFY(applicationStartedSpy.wait());
0149 
0150     KWin::VirtualDesktopManager::self()->setCount(2);
0151 }
0152 
0153 void ScriptedEffectsTest::init()
0154 {
0155     QVERIFY(Test::setupWaylandConnection());
0156 }
0157 
0158 void ScriptedEffectsTest::cleanup()
0159 {
0160     Test::destroyWaylandConnection();
0161 
0162     effects->unloadAllEffects();
0163     QVERIFY(effects->loadedEffects().isEmpty());
0164 
0165     KWin::VirtualDesktopManager::self()->setCurrent(1);
0166 }
0167 
0168 void ScriptedEffectsTest::testEffectsHandler()
0169 {
0170     // this triggers and tests some of the signals in EffectHandler, which is exposed to JS as context property "effects"
0171     auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
0172     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0173     auto waitFor = [&effectOutputSpy](const QString &expected) {
0174         QVERIFY(effectOutputSpy.count() > 0 || effectOutputSpy.wait());
0175         QCOMPARE(effectOutputSpy.first().first(), expected);
0176         effectOutputSpy.removeFirst();
0177     };
0178     QVERIFY(effect->load("effectsHandler"));
0179 
0180     // trigger windowAdded signal
0181 
0182     // create a window
0183     std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
0184     QVERIFY(surface);
0185     auto *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
0186     QVERIFY(shellSurface);
0187     shellSurface->set_title("WindowA");
0188     auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
0189     QVERIFY(c);
0190     QCOMPARE(workspace()->activeWindow(), c);
0191 
0192     waitFor("windowAdded - WindowA");
0193     waitFor("stackingOrder - 1 WindowA");
0194 
0195     // windowMinimsed
0196     c->setMinimized(true);
0197     waitFor("windowMinimized - WindowA");
0198 
0199     c->setMinimized(false);
0200     waitFor("windowUnminimized - WindowA");
0201 
0202     surface.reset();
0203     waitFor("windowClosed - WindowA");
0204 
0205     // desktop management
0206     KWin::VirtualDesktopManager::self()->setCurrent(2);
0207     waitFor("desktopChanged - 1 2");
0208 }
0209 
0210 void ScriptedEffectsTest::testEffectsContext()
0211 {
0212     // this tests misc non-objects exposed to the script engine: animationTime, displaySize, use of external enums
0213 
0214     auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
0215     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0216     QVERIFY(effect->load("effectContext"));
0217     QCOMPARE(effectOutputSpy[0].first(), "1280x1024");
0218     QCOMPARE(effectOutputSpy[1].first(), "100");
0219     QCOMPARE(effectOutputSpy[2].first(), "2");
0220     QCOMPARE(effectOutputSpy[3].first(), "0");
0221 }
0222 
0223 void ScriptedEffectsTest::testShortcuts()
0224 {
0225 #if !KWIN_BUILD_GLOBALSHORTCUTS
0226     QSKIP("Can't test shortcuts without shortcuts");
0227     return;
0228 #endif
0229 
0230     // this tests method registerShortcut
0231     auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
0232     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0233     QVERIFY(effect->load("shortcutsTest"));
0234     QCOMPARE(effect->actions().count(), 1);
0235     auto action = effect->actions()[0];
0236     QCOMPARE(action->objectName(), "testShortcut");
0237     QCOMPARE(action->text(), "Test Shortcut");
0238     QCOMPARE(KGlobalAccel::self()->shortcut(action).first(), QKeySequence("Meta+Shift+Y"));
0239     action->trigger();
0240     QCOMPARE(effectOutputSpy[0].first(), "shortcutTriggered");
0241 }
0242 
0243 void ScriptedEffectsTest::testAnimations_data()
0244 {
0245     QTest::addColumn<QString>("file");
0246     QTest::addColumn<int>("animationCount");
0247 
0248     QTest::newRow("single") << "animationTest" << 1;
0249     QTest::newRow("multi") << "animationTestMulti" << 2;
0250 }
0251 
0252 void ScriptedEffectsTest::testAnimations()
0253 {
0254     // this tests animate/set/cancel
0255     // methods take either an int or an array, as forced in the data above
0256     // also splits animate vs effects.animate(..)
0257 
0258     QFETCH(QString, file);
0259     QFETCH(int, animationCount);
0260 
0261     auto *effect = new ScriptedEffectWithDebugSpy;
0262     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0263     QVERIFY(effect->load(file));
0264 
0265     // animated after window added connect
0266     std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
0267     QVERIFY(surface);
0268     auto *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
0269     QVERIFY(shellSurface);
0270     shellSurface->set_title("Window 1");
0271     auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
0272     QVERIFY(c);
0273     QCOMPARE(workspace()->activeWindow(), c);
0274 
0275     {
0276         const auto state = effect->state();
0277         QCOMPARE(state.count(), 1);
0278         QCOMPARE(state.firstKey(), c->effectWindow());
0279         const auto &animationsForWindow = state.first().first;
0280         QCOMPARE(animationsForWindow.count(), animationCount);
0281         QCOMPARE(animationsForWindow[0].timeLine.duration(), 100ms);
0282         QCOMPARE(animationsForWindow[0].to, FPx2(1.4));
0283         QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale);
0284         QCOMPARE(animationsForWindow[0].timeLine.easingCurve().type(), QEasingCurve::OutCubic);
0285         QCOMPARE(animationsForWindow[0].terminationFlags,
0286                  AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget);
0287 
0288         if (animationCount == 2) {
0289             QCOMPARE(animationsForWindow[1].timeLine.duration(), 100ms);
0290             QCOMPARE(animationsForWindow[1].to, FPx2(0.0));
0291             QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity);
0292             QCOMPARE(animationsForWindow[1].terminationFlags,
0293                      AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget);
0294         }
0295     }
0296     QCOMPARE(effectOutputSpy[0].first(), "true");
0297 
0298     // window state changes, scale should be retargetted
0299 
0300     c->setMinimized(true);
0301     {
0302         const auto state = effect->state();
0303         QCOMPARE(state.count(), 1);
0304         const auto &animationsForWindow = state.first().first;
0305         QCOMPARE(animationsForWindow.count(), animationCount);
0306         QCOMPARE(animationsForWindow[0].timeLine.duration(), 200ms);
0307         QCOMPARE(animationsForWindow[0].to, FPx2(1.5));
0308         QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale);
0309         QCOMPARE(animationsForWindow[0].terminationFlags,
0310                  AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget);
0311         if (animationCount == 2) {
0312             QCOMPARE(animationsForWindow[1].timeLine.duration(), 200ms);
0313             QCOMPARE(animationsForWindow[1].to, FPx2(1.5));
0314             QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity);
0315             QCOMPARE(animationsForWindow[1].terminationFlags,
0316                      AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget);
0317         }
0318     }
0319     c->setMinimized(false);
0320     {
0321         const auto state = effect->state();
0322         QCOMPARE(state.count(), 0);
0323     }
0324 }
0325 
0326 void ScriptedEffectsTest::testScreenEdge()
0327 {
0328     // this test checks registerScreenEdge functions
0329     auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
0330     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0331     QVERIFY(effect->load("screenEdgeTest"));
0332     effect->borderActivated(KWin::ElectricTopRight);
0333     QCOMPARE(effectOutputSpy.count(), 1);
0334 }
0335 
0336 void ScriptedEffectsTest::testScreenEdgeTouch()
0337 {
0338     // this test checks registerTouchScreenEdge functions
0339     auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
0340     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0341     QVERIFY(effect->load("screenEdgeTouchTest"));
0342     effect->actions()[0]->trigger();
0343     QCOMPARE(effectOutputSpy.count(), 1);
0344 }
0345 
0346 void ScriptedEffectsTest::testFullScreenEffect_data()
0347 {
0348     QTest::addColumn<QString>("file");
0349 
0350     QTest::newRow("single") << "fullScreenEffectTest";
0351     QTest::newRow("multi") << "fullScreenEffectTestMulti";
0352     QTest::newRow("global") << "fullScreenEffectTestGlobal";
0353 }
0354 
0355 void ScriptedEffectsTest::testFullScreenEffect()
0356 {
0357     QFETCH(QString, file);
0358 
0359     auto *effectMain = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
0360     QSignalSpy effectOutputSpy(effectMain, &ScriptedEffectWithDebugSpy::testOutput);
0361     QSignalSpy fullScreenEffectActiveSpy(effects, &EffectsHandler::hasActiveFullScreenEffectChanged);
0362     QSignalSpy isActiveFullScreenEffectSpy(effectMain, &ScriptedEffect::isActiveFullScreenEffectChanged);
0363 
0364     QVERIFY(effectMain->load(file));
0365 
0366     // load any random effect from another test to confirm fullscreen effect state is correctly
0367     // shown as being someone else
0368     auto effectOther = new ScriptedEffectWithDebugSpy();
0369     QVERIFY(effectOther->load("screenEdgeTouchTest"));
0370     QSignalSpy isActiveFullScreenEffectSpyOther(effectOther, &ScriptedEffect::isActiveFullScreenEffectChanged);
0371 
0372     std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
0373     QVERIFY(surface);
0374     auto *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
0375     QVERIFY(shellSurface);
0376     shellSurface->set_title("Window 1");
0377     auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
0378     QVERIFY(c);
0379     QCOMPARE(workspace()->activeWindow(), c);
0380 
0381     QCOMPARE(effects->hasActiveFullScreenEffect(), false);
0382     QCOMPARE(effectMain->isActiveFullScreenEffect(), false);
0383 
0384     // trigger animation
0385     KWin::VirtualDesktopManager::self()->setCurrent(2);
0386 
0387     QCOMPARE(effects->activeFullScreenEffect(), effectMain);
0388     QCOMPARE(effects->hasActiveFullScreenEffect(), true);
0389     QCOMPARE(fullScreenEffectActiveSpy.count(), 1);
0390 
0391     QCOMPARE(effectMain->isActiveFullScreenEffect(), true);
0392     QCOMPARE(isActiveFullScreenEffectSpy.count(), 1);
0393 
0394     QCOMPARE(effectOther->isActiveFullScreenEffect(), false);
0395     QCOMPARE(isActiveFullScreenEffectSpyOther.count(), 0);
0396 
0397     // after 500ms trigger another full screen animation
0398     QTest::qWait(500);
0399     KWin::VirtualDesktopManager::self()->setCurrent(1);
0400     QCOMPARE(effects->activeFullScreenEffect(), effectMain);
0401 
0402     // after 1000ms (+a safety margin for time based tests) we should still be the active full screen effect
0403     // despite first animation expiring
0404     QTest::qWait(500 + 100);
0405     QCOMPARE(effects->activeFullScreenEffect(), effectMain);
0406 
0407     // after 1500ms (+a safetey margin) we should have no full screen effect
0408     QTest::qWait(500 + 100);
0409     QCOMPARE(effects->activeFullScreenEffect(), nullptr);
0410 }
0411 
0412 void ScriptedEffectsTest::testKeepAlive_data()
0413 {
0414     QTest::addColumn<QString>("file");
0415     QTest::addColumn<bool>("keepAlive");
0416 
0417     QTest::newRow("keep") << "keepAliveTest" << true;
0418     QTest::newRow("don't keep") << "keepAliveTestDontKeep" << false;
0419 }
0420 
0421 void ScriptedEffectsTest::testKeepAlive()
0422 {
0423     // this test checks whether closed windows are kept alive
0424     // when keepAlive property is set to true(false)
0425 
0426     QFETCH(QString, file);
0427     QFETCH(bool, keepAlive);
0428 
0429     auto *effect = new ScriptedEffectWithDebugSpy;
0430     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0431     QVERIFY(effect->load(file));
0432 
0433     // create a window
0434     std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
0435     QVERIFY(surface);
0436     auto *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
0437     QVERIFY(shellSurface);
0438     auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
0439     QVERIFY(c);
0440     QCOMPARE(workspace()->activeWindow(), c);
0441 
0442     // no active animations at the beginning
0443     QCOMPARE(effect->state().count(), 0);
0444 
0445     // trigger windowClosed signal
0446     QSignalSpy deletedRemovedSpy(workspace(), &Workspace::deletedRemoved);
0447     surface.reset();
0448     QVERIFY(effectOutputSpy.count() == 1 || effectOutputSpy.wait());
0449 
0450     if (keepAlive) {
0451         QCOMPARE(effect->state().count(), 1);
0452         QCOMPARE(deletedRemovedSpy.count(), 0);
0453 
0454         QTest::qWait(500);
0455         QCOMPARE(effect->state().count(), 1);
0456         QCOMPARE(deletedRemovedSpy.count(), 0);
0457 
0458         QTest::qWait(500 + 100); // 100ms is extra safety margin
0459         QCOMPARE(deletedRemovedSpy.count(), 1);
0460         QCOMPARE(effect->state().count(), 0);
0461     } else {
0462         // the test effect doesn't keep the window alive, so it should be
0463         // removed immediately
0464         QVERIFY(deletedRemovedSpy.count() == 1 || deletedRemovedSpy.wait(100)); // 100ms is less than duration of the animation
0465         QCOMPARE(effect->state().count(), 0);
0466     }
0467 }
0468 
0469 void ScriptedEffectsTest::testGrab()
0470 {
0471     // this test verifies that scripted effects can grab windows that are
0472     // not already grabbed
0473 
0474     // load the test effect
0475     auto effect = new ScriptedEffectWithDebugSpy;
0476     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0477     QVERIFY(effect->load(QStringLiteral("grabTest")));
0478 
0479     // create test window
0480     std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
0481     QVERIFY(surface);
0482     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
0483     QVERIFY(shellSurface);
0484     Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
0485     QVERIFY(window);
0486     QCOMPARE(workspace()->activeWindow(), window);
0487 
0488     // the test effect should grab the test window successfully
0489     QCOMPARE(effectOutputSpy.count(), 1);
0490     QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
0491     QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value<void *>(), effect);
0492 }
0493 
0494 void ScriptedEffectsTest::testGrabAlreadyGrabbedWindow()
0495 {
0496     // this test verifies that scripted effects cannot grab already grabbed
0497     // windows (unless force is set to true of course)
0498 
0499     // load effect that will hold the window grab
0500     auto owner = new ScriptedEffectWithDebugSpy;
0501     QSignalSpy ownerOutputSpy(owner, &ScriptedEffectWithDebugSpy::testOutput);
0502     QVERIFY(owner->load(QStringLiteral("grabAlreadyGrabbedWindowTest_owner")));
0503 
0504     // load effect that will try to grab already grabbed window
0505     auto grabber = new ScriptedEffectWithDebugSpy;
0506     QSignalSpy grabberOutputSpy(grabber, &ScriptedEffectWithDebugSpy::testOutput);
0507     QVERIFY(grabber->load(QStringLiteral("grabAlreadyGrabbedWindowTest_grabber")));
0508 
0509     // create test window
0510     std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
0511     QVERIFY(surface);
0512     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
0513     QVERIFY(shellSurface);
0514     Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
0515     QVERIFY(window);
0516     QCOMPARE(workspace()->activeWindow(), window);
0517 
0518     // effect that initially held the grab should still hold the grab
0519     QCOMPARE(ownerOutputSpy.count(), 1);
0520     QCOMPARE(ownerOutputSpy.first().first(), QStringLiteral("ok"));
0521     QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value<void *>(), owner);
0522 
0523     // effect that tried to grab already grabbed window should fail miserably
0524     QCOMPARE(grabberOutputSpy.count(), 1);
0525     QCOMPARE(grabberOutputSpy.first().first(), QStringLiteral("fail"));
0526 }
0527 
0528 void ScriptedEffectsTest::testGrabAlreadyGrabbedWindowForced()
0529 {
0530     // this test verifies that scripted effects can steal window grabs when
0531     // they forcefully try to grab windows
0532 
0533     // load effect that initially will be holding the window grab
0534     auto owner = new ScriptedEffectWithDebugSpy;
0535     QSignalSpy ownerOutputSpy(owner, &ScriptedEffectWithDebugSpy::testOutput);
0536     QVERIFY(owner->load(QStringLiteral("grabAlreadyGrabbedWindowForcedTest_owner")));
0537 
0538     // load effect that will try to steal the window grab
0539     auto thief = new ScriptedEffectWithDebugSpy;
0540     QSignalSpy thiefOutputSpy(thief, &ScriptedEffectWithDebugSpy::testOutput);
0541     QVERIFY(thief->load(QStringLiteral("grabAlreadyGrabbedWindowForcedTest_thief")));
0542 
0543     // create test window
0544     std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
0545     QVERIFY(surface);
0546     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
0547     QVERIFY(shellSurface);
0548     Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
0549     QVERIFY(window);
0550     QCOMPARE(workspace()->activeWindow(), window);
0551 
0552     // verify that the owner in fact held the grab
0553     QCOMPARE(ownerOutputSpy.count(), 1);
0554     QCOMPARE(ownerOutputSpy.first().first(), QStringLiteral("ok"));
0555 
0556     // effect that grabbed the test window forcefully should now hold the grab
0557     QCOMPARE(thiefOutputSpy.count(), 1);
0558     QCOMPARE(thiefOutputSpy.first().first(), QStringLiteral("ok"));
0559     QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value<void *>(), thief);
0560 }
0561 
0562 void ScriptedEffectsTest::testUngrab()
0563 {
0564     // this test verifies that scripted effects can ungrab windows that they
0565     // are previously grabbed
0566 
0567     // load the test effect
0568     auto effect = new ScriptedEffectWithDebugSpy;
0569     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0570     QVERIFY(effect->load(QStringLiteral("ungrabTest")));
0571 
0572     // create test window
0573     std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
0574     QVERIFY(surface);
0575     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
0576     QVERIFY(shellSurface);
0577     Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
0578     QVERIFY(window);
0579     QCOMPARE(workspace()->activeWindow(), window);
0580 
0581     // the test effect should grab the test window successfully
0582     QCOMPARE(effectOutputSpy.count(), 1);
0583     QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
0584     QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value<void *>(), effect);
0585 
0586     // when the test effect sees that a window was minimized, it will try to ungrab it
0587     effectOutputSpy.clear();
0588     window->setMinimized(true);
0589 
0590     QCOMPARE(effectOutputSpy.count(), 1);
0591     QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
0592     QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value<void *>(), nullptr);
0593 }
0594 
0595 void ScriptedEffectsTest::testRedirect_data()
0596 {
0597     QTest::addColumn<QString>("file");
0598     QTest::addColumn<bool>("shouldTerminate");
0599     QTest::newRow("animate/DontTerminateAtSource") << "redirectAnimateDontTerminateTest" << false;
0600     QTest::newRow("animate/TerminateAtSource") << "redirectAnimateTerminateTest" << true;
0601     QTest::newRow("set/DontTerminate") << "redirectSetDontTerminateTest" << false;
0602     QTest::newRow("set/Terminate") << "redirectSetTerminateTest" << true;
0603 }
0604 
0605 void ScriptedEffectsTest::testRedirect()
0606 {
0607     // this test verifies that redirect() works
0608 
0609     // load the test effect
0610     auto effect = new ScriptedEffectWithDebugSpy;
0611     QFETCH(QString, file);
0612     QVERIFY(effect->load(file));
0613 
0614     // create test window
0615     std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
0616     QVERIFY(surface);
0617     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
0618     QVERIFY(shellSurface);
0619     Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
0620     QVERIFY(window);
0621     QCOMPARE(workspace()->activeWindow(), window);
0622 
0623     auto around = [](std::chrono::milliseconds elapsed,
0624                      std::chrono::milliseconds pivot,
0625                      std::chrono::milliseconds margin) {
0626         return std::abs(elapsed.count() - pivot.count()) < margin.count();
0627     };
0628 
0629     // initially, the test animation is at the source position
0630 
0631     {
0632         const auto state = effect->state();
0633         QCOMPARE(state.count(), 1);
0634         QCOMPARE(state.firstKey(), window->effectWindow());
0635         const QList<AniData> animations = state.first().first;
0636         QCOMPARE(animations.count(), 1);
0637         QCOMPARE(animations[0].timeLine.direction(), TimeLine::Forward);
0638         QVERIFY(around(animations[0].timeLine.elapsed(), 0ms, 50ms));
0639     }
0640 
0641     // minimize the test window after 250ms, when the test effect sees that
0642     // a window was minimized, it will try to reverse animation for it
0643     QTest::qWait(250);
0644 
0645     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0646 
0647     window->setMinimized(true);
0648 
0649     QCOMPARE(effectOutputSpy.count(), 1);
0650     QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
0651 
0652     {
0653         const auto state = effect->state();
0654         QCOMPARE(state.count(), 1);
0655         QCOMPARE(state.firstKey(), window->effectWindow());
0656         const QList<AniData> animations = state.first().first;
0657         QCOMPARE(animations.count(), 1);
0658         QCOMPARE(animations[0].timeLine.direction(), TimeLine::Backward);
0659         QVERIFY(around(animations[0].timeLine.elapsed(), 1000ms - 250ms, 50ms));
0660     }
0661 
0662     // wait for the animation to reach the start position, 100ms is an extra
0663     // safety margin
0664     QTest::qWait(250 + 100);
0665 
0666     QFETCH(bool, shouldTerminate);
0667     if (shouldTerminate) {
0668         const auto state = effect->state();
0669         QCOMPARE(state.count(), 0);
0670     } else {
0671         const auto state = effect->state();
0672         QCOMPARE(state.count(), 1);
0673         QCOMPARE(state.firstKey(), window->effectWindow());
0674         const QList<AniData> animations = state.first().first;
0675         QCOMPARE(animations.count(), 1);
0676         QCOMPARE(animations[0].timeLine.direction(), TimeLine::Backward);
0677         QCOMPARE(animations[0].timeLine.elapsed(), 1000ms);
0678         QCOMPARE(animations[0].timeLine.value(), 0.0);
0679     }
0680 }
0681 
0682 void ScriptedEffectsTest::testComplete()
0683 {
0684     // this test verifies that complete works
0685 
0686     // load the test effect
0687     auto effect = new ScriptedEffectWithDebugSpy;
0688     QVERIFY(effect->load(QStringLiteral("completeTest")));
0689 
0690     // create test window
0691     std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
0692     QVERIFY(surface);
0693     Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
0694     QVERIFY(shellSurface);
0695     Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
0696     QVERIFY(window);
0697     QCOMPARE(workspace()->activeWindow(), window);
0698 
0699     auto around = [](std::chrono::milliseconds elapsed,
0700                      std::chrono::milliseconds pivot,
0701                      std::chrono::milliseconds margin) {
0702         return std::abs(elapsed.count() - pivot.count()) < margin.count();
0703     };
0704 
0705     // initially, the test animation should be at the start position
0706     {
0707         const auto state = effect->state();
0708         QCOMPARE(state.count(), 1);
0709         QCOMPARE(state.firstKey(), window->effectWindow());
0710         const QList<AniData> animations = state.first().first;
0711         QCOMPARE(animations.count(), 1);
0712         QVERIFY(around(animations[0].timeLine.elapsed(), 0ms, 50ms));
0713         QVERIFY(!animations[0].timeLine.done());
0714     }
0715 
0716     // wait for 250ms
0717     QTest::qWait(250);
0718 
0719     {
0720         const auto state = effect->state();
0721         QCOMPARE(state.count(), 1);
0722         QCOMPARE(state.firstKey(), window->effectWindow());
0723         const QList<AniData> animations = state.first().first;
0724         QCOMPARE(animations.count(), 1);
0725         QVERIFY(around(animations[0].timeLine.elapsed(), 250ms, 50ms));
0726         QVERIFY(!animations[0].timeLine.done());
0727     }
0728 
0729     // minimize the test window, when the test effect sees that a window was
0730     // minimized, it will try to complete animation for it
0731     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
0732 
0733     window->setMinimized(true);
0734 
0735     QCOMPARE(effectOutputSpy.count(), 1);
0736     QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
0737 
0738     {
0739         const auto state = effect->state();
0740         QCOMPARE(state.count(), 1);
0741         QCOMPARE(state.firstKey(), window->effectWindow());
0742         const QList<AniData> animations = state.first().first;
0743         QCOMPARE(animations.count(), 1);
0744         QCOMPARE(animations[0].timeLine.elapsed(), 1000ms);
0745         QVERIFY(animations[0].timeLine.done());
0746     }
0747 }
0748 
0749 WAYLANDTEST_MAIN(ScriptedEffectsTest)
0750 #include "scripted_effects_test.moc"