KWin
Loading...
Searching...
No Matches
scripted_effects_test.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2018 David Edmundson <davidedmundson@kde.org>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9
10#include "kwin_wayland_test.h"
11
12#include "effect/anidata_p.h"
14#include "effect/effectloader.h"
16#include "virtualdesktops.h"
17#include "wayland_server.h"
18#include "window.h"
19#include "workspace.h"
20
21#include <KConfigGroup>
22#include <KGlobalAccel>
23#include <KWayland/Client/compositor.h>
24#include <KWayland/Client/connection_thread.h>
25#include <KWayland/Client/registry.h>
26#include <KWayland/Client/slide.h>
27#include <KWayland/Client/surface.h>
28
29#include <QJSValue>
30#include <QQmlEngine>
31
32using namespace KWin;
33using namespace std::chrono_literals;
34
35static const QString s_socketName = QStringLiteral("wayland_test_effects_scripts-0");
36
37class ScriptedEffectsTest : public QObject
38{
39 Q_OBJECT
40private Q_SLOTS:
41 void initTestCase();
42 void init();
43 void cleanup();
44
45 void testEffectsHandler();
46 void testEffectsContext();
47 void testShortcuts();
48 void testAnimations_data();
49 void testAnimations();
50 void testScreenEdge();
51 void testScreenEdgeTouch();
52 void testFullScreenEffect_data();
53 void testFullScreenEffect();
54 void testKeepAlive_data();
55 void testKeepAlive();
56 void testGrab();
57 void testGrabAlreadyGrabbedWindow();
58 void testGrabAlreadyGrabbedWindowForced();
59 void testUngrab();
60 void testRedirect_data();
61 void testRedirect();
62 void testComplete();
63
64private:
65 ScriptedEffect *loadEffect(const QString &name);
66};
67
69{
70 Q_OBJECT
71public:
73 bool load(const QString &name);
76 Q_INVOKABLE void sendTestResponse(const QString &out); // proxies triggers out from the tests
77 QList<QAction *> actions(); // returns any QActions owned by the ScriptEngine
78Q_SIGNALS:
79 void testOutput(const QString &data);
80};
81
83{
84 Q_EMIT testOutput(out);
85}
86
88{
89 return findChildren<QAction *>(QString(), Qt::FindDirectChildrenOnly);
90}
91
96
97bool ScriptedEffectWithDebugSpy::load(const QString &name)
98{
99 auto selfContext = engine()->newQObject(this);
100 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
101 const QString path = QFINDTESTDATA("./scripts/" + name + ".js");
102 engine()->globalObject().setProperty("sendTestResponse", selfContext.property("sendTestResponse"));
103 if (!init(name, path)) {
104 return false;
105 }
106
107 // inject our newly created effect to be registered with the EffectsHandler::loaded_effects
108 // this is private API so some horrible code is used to find the internal effectloader
109 // and register ourselves
110 auto children = effects->children();
111 for (auto it = children.begin(); it != children.end(); ++it) {
112 if (qstrcmp((*it)->metaObject()->className(), "KWin::EffectLoader") != 0) {
113 continue;
114 }
115 QMetaObject::invokeMethod(*it, "effectLoaded", Q_ARG(KWin::Effect *, this), Q_ARG(QString, name));
116 break;
117 }
118
119 return effects->isEffectLoaded(name);
120}
121
122void ScriptedEffectsTest::initTestCase()
123{
125 QSKIP("no render node available");
126 return;
127 }
128 qRegisterMetaType<KWin::Window *>();
129 qRegisterMetaType<KWin::Effect *>();
130 QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
131 QVERIFY(waylandServer()->init(s_socketName));
132 Test::setOutputConfig({QRect(0, 0, 1280, 1024)});
133
134 // disable all effects - we don't want to have it interact with the rendering
135 auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
136 KConfigGroup plugins(config, QStringLiteral("Plugins"));
137 const auto builtinNames = EffectLoader().listOfKnownEffects();
138 for (QString name : builtinNames) {
139 plugins.writeEntry(name + QStringLiteral("Enabled"), false);
140 }
141
142 config->sync();
143 kwinApp()->setConfig(config);
144
145 qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2"));
146 qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1");
147 kwinApp()->start();
148 QVERIFY(applicationStartedSpy.wait());
149
150 KWin::VirtualDesktopManager::self()->setCount(2);
151}
152
153void ScriptedEffectsTest::init()
154{
156}
157
158void ScriptedEffectsTest::cleanup()
159{
161
163 QVERIFY(effects->loadedEffects().isEmpty());
164
165 KWin::VirtualDesktopManager::self()->setCurrent(1);
166}
167
168void ScriptedEffectsTest::testEffectsHandler()
169{
170 // this triggers and tests some of the signals in EffectHandler, which is exposed to JS as context property "effects"
171 auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
172 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
173 auto waitFor = [&effectOutputSpy](const QString &expected) {
174 QVERIFY(effectOutputSpy.count() > 0 || effectOutputSpy.wait());
175 QCOMPARE(effectOutputSpy.first().first(), expected);
176 effectOutputSpy.removeFirst();
177 };
178 QVERIFY(effect->load("effectsHandler"));
179
180 // trigger windowAdded signal
181
182 // create a window
183 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
184 QVERIFY(surface);
185 auto *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
186 QVERIFY(shellSurface);
187 shellSurface->set_title("WindowA");
188 auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
189 QVERIFY(c);
190 QCOMPARE(workspace()->activeWindow(), c);
191
192 waitFor("windowAdded - WindowA");
193 waitFor("stackingOrder - 1 WindowA");
194
195 // windowMinimsed
196 c->setMinimized(true);
197 waitFor("windowMinimized - WindowA");
198
199 c->setMinimized(false);
200 waitFor("windowUnminimized - WindowA");
201
202 surface.reset();
203 waitFor("windowClosed - WindowA");
204
205 // desktop management
206 KWin::VirtualDesktopManager::self()->setCurrent(2);
207 waitFor("desktopChanged - 1 2");
208}
209
210void ScriptedEffectsTest::testEffectsContext()
211{
212 // this tests misc non-objects exposed to the script engine: animationTime, displaySize, use of external enums
213
214 auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
215 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
216 QVERIFY(effect->load("effectContext"));
217 QCOMPARE(effectOutputSpy[0].first(), "1280x1024");
218 QCOMPARE(effectOutputSpy[1].first(), "100");
219 QCOMPARE(effectOutputSpy[2].first(), "2");
220 QCOMPARE(effectOutputSpy[3].first(), "0");
221}
222
223void ScriptedEffectsTest::testShortcuts()
224{
225#if !KWIN_BUILD_GLOBALSHORTCUTS
226 QSKIP("Can't test shortcuts without shortcuts");
227 return;
228#endif
229
230 // this tests method registerShortcut
231 auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
232 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
233 QVERIFY(effect->load("shortcutsTest"));
234 QCOMPARE(effect->actions().count(), 1);
235 auto action = effect->actions()[0];
236 QCOMPARE(action->objectName(), "testShortcut");
237 QCOMPARE(action->text(), "Test Shortcut");
238 QCOMPARE(KGlobalAccel::self()->shortcut(action).first(), QKeySequence("Meta+Shift+Y"));
239 action->trigger();
240 QCOMPARE(effectOutputSpy[0].first(), "shortcutTriggered");
241}
242
243void ScriptedEffectsTest::testAnimations_data()
244{
245 QTest::addColumn<QString>("file");
246 QTest::addColumn<int>("animationCount");
247
248 QTest::newRow("single") << "animationTest" << 1;
249 QTest::newRow("multi") << "animationTestMulti" << 2;
250}
251
252void ScriptedEffectsTest::testAnimations()
253{
254 // this tests animate/set/cancel
255 // methods take either an int or an array, as forced in the data above
256 // also splits animate vs effects.animate(..)
257
258 QFETCH(QString, file);
259 QFETCH(int, animationCount);
260
261 auto *effect = new ScriptedEffectWithDebugSpy;
262 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
263 QVERIFY(effect->load(file));
264
265 // animated after window added connect
266 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
267 QVERIFY(surface);
268 auto *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
269 QVERIFY(shellSurface);
270 shellSurface->set_title("Window 1");
271 auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
272 QVERIFY(c);
273 QCOMPARE(workspace()->activeWindow(), c);
274
275 {
276 const auto state = effect->state();
277 QCOMPARE(state.count(), 1);
278 QCOMPARE(state.firstKey(), c->effectWindow());
279 const auto &animationsForWindow = state.first().first;
280 QCOMPARE(animationsForWindow.count(), animationCount);
281 QCOMPARE(animationsForWindow[0].timeLine.duration(), 100ms);
282 QCOMPARE(animationsForWindow[0].to, FPx2(1.4));
283 QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale);
284 QCOMPARE(animationsForWindow[0].timeLine.easingCurve().type(), QEasingCurve::OutCubic);
285 QCOMPARE(animationsForWindow[0].terminationFlags,
287
288 if (animationCount == 2) {
289 QCOMPARE(animationsForWindow[1].timeLine.duration(), 100ms);
290 QCOMPARE(animationsForWindow[1].to, FPx2(0.0));
291 QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity);
292 QCOMPARE(animationsForWindow[1].terminationFlags,
294 }
295 }
296 QCOMPARE(effectOutputSpy[0].first(), "true");
297
298 // window state changes, scale should be retargetted
299
300 c->setMinimized(true);
301 {
302 const auto state = effect->state();
303 QCOMPARE(state.count(), 1);
304 const auto &animationsForWindow = state.first().first;
305 QCOMPARE(animationsForWindow.count(), animationCount);
306 QCOMPARE(animationsForWindow[0].timeLine.duration(), 200ms);
307 QCOMPARE(animationsForWindow[0].to, FPx2(1.5));
308 QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale);
309 QCOMPARE(animationsForWindow[0].terminationFlags,
311 if (animationCount == 2) {
312 QCOMPARE(animationsForWindow[1].timeLine.duration(), 200ms);
313 QCOMPARE(animationsForWindow[1].to, FPx2(1.5));
314 QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity);
315 QCOMPARE(animationsForWindow[1].terminationFlags,
317 }
318 }
319 c->setMinimized(false);
320 {
321 const auto state = effect->state();
322 QCOMPARE(state.count(), 0);
323 }
324}
325
326void ScriptedEffectsTest::testScreenEdge()
327{
328 // this test checks registerScreenEdge functions
329 auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
330 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
331 QVERIFY(effect->load("screenEdgeTest"));
332 effect->borderActivated(KWin::ElectricTopRight);
333 QCOMPARE(effectOutputSpy.count(), 1);
334}
335
336void ScriptedEffectsTest::testScreenEdgeTouch()
337{
338 // this test checks registerTouchScreenEdge functions
339 auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
340 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
341 QVERIFY(effect->load("screenEdgeTouchTest"));
342 effect->actions()[0]->trigger();
343 QCOMPARE(effectOutputSpy.count(), 1);
344}
345
346void ScriptedEffectsTest::testFullScreenEffect_data()
347{
348 QTest::addColumn<QString>("file");
349
350 QTest::newRow("single") << "fullScreenEffectTest";
351 QTest::newRow("multi") << "fullScreenEffectTestMulti";
352 QTest::newRow("global") << "fullScreenEffectTestGlobal";
353}
354
355void ScriptedEffectsTest::testFullScreenEffect()
356{
357 QFETCH(QString, file);
358
359 auto *effectMain = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
360 QSignalSpy effectOutputSpy(effectMain, &ScriptedEffectWithDebugSpy::testOutput);
361 QSignalSpy fullScreenEffectActiveSpy(effects, &EffectsHandler::hasActiveFullScreenEffectChanged);
362 QSignalSpy isActiveFullScreenEffectSpy(effectMain, &ScriptedEffect::isActiveFullScreenEffectChanged);
363
364 QVERIFY(effectMain->load(file));
365
366 // load any random effect from another test to confirm fullscreen effect state is correctly
367 // shown as being someone else
368 auto effectOther = new ScriptedEffectWithDebugSpy();
369 QVERIFY(effectOther->load("screenEdgeTouchTest"));
370 QSignalSpy isActiveFullScreenEffectSpyOther(effectOther, &ScriptedEffect::isActiveFullScreenEffectChanged);
371
372 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
373 QVERIFY(surface);
374 auto *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
375 QVERIFY(shellSurface);
376 shellSurface->set_title("Window 1");
377 auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
378 QVERIFY(c);
379 QCOMPARE(workspace()->activeWindow(), c);
380
381 QCOMPARE(effects->hasActiveFullScreenEffect(), false);
382 QCOMPARE(effectMain->isActiveFullScreenEffect(), false);
383
384 // trigger animation
385 KWin::VirtualDesktopManager::self()->setCurrent(2);
386
387 QCOMPARE(effects->activeFullScreenEffect(), effectMain);
388 QCOMPARE(effects->hasActiveFullScreenEffect(), true);
389 QCOMPARE(fullScreenEffectActiveSpy.count(), 1);
390
391 QCOMPARE(effectMain->isActiveFullScreenEffect(), true);
392 QCOMPARE(isActiveFullScreenEffectSpy.count(), 1);
393
394 QCOMPARE(effectOther->isActiveFullScreenEffect(), false);
395 QCOMPARE(isActiveFullScreenEffectSpyOther.count(), 0);
396
397 // after 500ms trigger another full screen animation
398 QTest::qWait(500);
399 KWin::VirtualDesktopManager::self()->setCurrent(1);
400 QCOMPARE(effects->activeFullScreenEffect(), effectMain);
401
402 // after 1000ms (+a safety margin for time based tests) we should still be the active full screen effect
403 // despite first animation expiring
404 QTest::qWait(500 + 100);
405 QCOMPARE(effects->activeFullScreenEffect(), effectMain);
406
407 // after 1500ms (+a safetey margin) we should have no full screen effect
408 QTest::qWait(500 + 100);
409 QCOMPARE(effects->activeFullScreenEffect(), nullptr);
410}
411
412void ScriptedEffectsTest::testKeepAlive_data()
413{
414 QTest::addColumn<QString>("file");
415 QTest::addColumn<bool>("keepAlive");
416
417 QTest::newRow("keep") << "keepAliveTest" << true;
418 QTest::newRow("don't keep") << "keepAliveTestDontKeep" << false;
419}
420
421void ScriptedEffectsTest::testKeepAlive()
422{
423 // this test checks whether closed windows are kept alive
424 // when keepAlive property is set to true(false)
425
426 QFETCH(QString, file);
427 QFETCH(bool, keepAlive);
428
429 auto *effect = new ScriptedEffectWithDebugSpy;
430 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
431 QVERIFY(effect->load(file));
432
433 // create a window
434 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
435 QVERIFY(surface);
436 auto *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
437 QVERIFY(shellSurface);
438 auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
439 QVERIFY(c);
440 QCOMPARE(workspace()->activeWindow(), c);
441
442 // no active animations at the beginning
443 QCOMPARE(effect->state().count(), 0);
444
445 // trigger windowClosed signal
446 QSignalSpy deletedRemovedSpy(workspace(), &Workspace::deletedRemoved);
447 surface.reset();
448 QVERIFY(effectOutputSpy.count() == 1 || effectOutputSpy.wait());
449
450 if (keepAlive) {
451 QCOMPARE(effect->state().count(), 1);
452 QCOMPARE(deletedRemovedSpy.count(), 0);
453
454 QTest::qWait(500);
455 QCOMPARE(effect->state().count(), 1);
456 QCOMPARE(deletedRemovedSpy.count(), 0);
457
458 QTest::qWait(500 + 100); // 100ms is extra safety margin
459 QCOMPARE(deletedRemovedSpy.count(), 1);
460 QCOMPARE(effect->state().count(), 0);
461 } else {
462 // the test effect doesn't keep the window alive, so it should be
463 // removed immediately
464 QVERIFY(deletedRemovedSpy.count() == 1 || deletedRemovedSpy.wait(100)); // 100ms is less than duration of the animation
465 QCOMPARE(effect->state().count(), 0);
466 }
467}
468
469void ScriptedEffectsTest::testGrab()
470{
471 // this test verifies that scripted effects can grab windows that are
472 // not already grabbed
473
474 // load the test effect
475 auto effect = new ScriptedEffectWithDebugSpy;
476 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
477 QVERIFY(effect->load(QStringLiteral("grabTest")));
478
479 // create test window
480 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
481 QVERIFY(surface);
482 Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
483 QVERIFY(shellSurface);
484 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
485 QVERIFY(window);
486 QCOMPARE(workspace()->activeWindow(), window);
487
488 // the test effect should grab the test window successfully
489 QCOMPARE(effectOutputSpy.count(), 1);
490 QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
491 QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value<void *>(), effect);
492}
493
494void ScriptedEffectsTest::testGrabAlreadyGrabbedWindow()
495{
496 // this test verifies that scripted effects cannot grab already grabbed
497 // windows (unless force is set to true of course)
498
499 // load effect that will hold the window grab
500 auto owner = new ScriptedEffectWithDebugSpy;
501 QSignalSpy ownerOutputSpy(owner, &ScriptedEffectWithDebugSpy::testOutput);
502 QVERIFY(owner->load(QStringLiteral("grabAlreadyGrabbedWindowTest_owner")));
503
504 // load effect that will try to grab already grabbed window
505 auto grabber = new ScriptedEffectWithDebugSpy;
506 QSignalSpy grabberOutputSpy(grabber, &ScriptedEffectWithDebugSpy::testOutput);
507 QVERIFY(grabber->load(QStringLiteral("grabAlreadyGrabbedWindowTest_grabber")));
508
509 // create test window
510 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
511 QVERIFY(surface);
512 Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
513 QVERIFY(shellSurface);
514 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
515 QVERIFY(window);
516 QCOMPARE(workspace()->activeWindow(), window);
517
518 // effect that initially held the grab should still hold the grab
519 QCOMPARE(ownerOutputSpy.count(), 1);
520 QCOMPARE(ownerOutputSpy.first().first(), QStringLiteral("ok"));
521 QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value<void *>(), owner);
522
523 // effect that tried to grab already grabbed window should fail miserably
524 QCOMPARE(grabberOutputSpy.count(), 1);
525 QCOMPARE(grabberOutputSpy.first().first(), QStringLiteral("fail"));
526}
527
528void ScriptedEffectsTest::testGrabAlreadyGrabbedWindowForced()
529{
530 // this test verifies that scripted effects can steal window grabs when
531 // they forcefully try to grab windows
532
533 // load effect that initially will be holding the window grab
534 auto owner = new ScriptedEffectWithDebugSpy;
535 QSignalSpy ownerOutputSpy(owner, &ScriptedEffectWithDebugSpy::testOutput);
536 QVERIFY(owner->load(QStringLiteral("grabAlreadyGrabbedWindowForcedTest_owner")));
537
538 // load effect that will try to steal the window grab
539 auto thief = new ScriptedEffectWithDebugSpy;
540 QSignalSpy thiefOutputSpy(thief, &ScriptedEffectWithDebugSpy::testOutput);
541 QVERIFY(thief->load(QStringLiteral("grabAlreadyGrabbedWindowForcedTest_thief")));
542
543 // create test window
544 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
545 QVERIFY(surface);
546 Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
547 QVERIFY(shellSurface);
548 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
549 QVERIFY(window);
550 QCOMPARE(workspace()->activeWindow(), window);
551
552 // verify that the owner in fact held the grab
553 QCOMPARE(ownerOutputSpy.count(), 1);
554 QCOMPARE(ownerOutputSpy.first().first(), QStringLiteral("ok"));
555
556 // effect that grabbed the test window forcefully should now hold the grab
557 QCOMPARE(thiefOutputSpy.count(), 1);
558 QCOMPARE(thiefOutputSpy.first().first(), QStringLiteral("ok"));
559 QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value<void *>(), thief);
560}
561
562void ScriptedEffectsTest::testUngrab()
563{
564 // this test verifies that scripted effects can ungrab windows that they
565 // are previously grabbed
566
567 // load the test effect
568 auto effect = new ScriptedEffectWithDebugSpy;
569 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
570 QVERIFY(effect->load(QStringLiteral("ungrabTest")));
571
572 // create test window
573 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
574 QVERIFY(surface);
575 Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
576 QVERIFY(shellSurface);
577 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
578 QVERIFY(window);
579 QCOMPARE(workspace()->activeWindow(), window);
580
581 // the test effect should grab the test window successfully
582 QCOMPARE(effectOutputSpy.count(), 1);
583 QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
584 QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value<void *>(), effect);
585
586 // when the test effect sees that a window was minimized, it will try to ungrab it
587 effectOutputSpy.clear();
588 window->setMinimized(true);
589
590 QCOMPARE(effectOutputSpy.count(), 1);
591 QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
592 QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value<void *>(), nullptr);
593}
594
595void ScriptedEffectsTest::testRedirect_data()
596{
597 QTest::addColumn<QString>("file");
598 QTest::addColumn<bool>("shouldTerminate");
599 QTest::newRow("animate/DontTerminateAtSource") << "redirectAnimateDontTerminateTest" << false;
600 QTest::newRow("animate/TerminateAtSource") << "redirectAnimateTerminateTest" << true;
601 QTest::newRow("set/DontTerminate") << "redirectSetDontTerminateTest" << false;
602 QTest::newRow("set/Terminate") << "redirectSetTerminateTest" << true;
603}
604
605void ScriptedEffectsTest::testRedirect()
606{
607 // this test verifies that redirect() works
608
609 // load the test effect
610 auto effect = new ScriptedEffectWithDebugSpy;
611 QFETCH(QString, file);
612 QVERIFY(effect->load(file));
613
614 // create test window
615 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
616 QVERIFY(surface);
617 Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
618 QVERIFY(shellSurface);
619 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
620 QVERIFY(window);
621 QCOMPARE(workspace()->activeWindow(), window);
622
623 auto around = [](std::chrono::milliseconds elapsed,
624 std::chrono::milliseconds pivot,
625 std::chrono::milliseconds margin) {
626 return std::abs(elapsed.count() - pivot.count()) < margin.count();
627 };
628
629 // initially, the test animation is at the source position
630
631 {
632 const auto state = effect->state();
633 QCOMPARE(state.count(), 1);
634 QCOMPARE(state.firstKey(), window->effectWindow());
635 const QList<AniData> animations = state.first().first;
636 QCOMPARE(animations.count(), 1);
637 QCOMPARE(animations[0].timeLine.direction(), TimeLine::Forward);
638 QVERIFY(around(animations[0].timeLine.elapsed(), 0ms, 50ms));
639 }
640
641 // minimize the test window after 250ms, when the test effect sees that
642 // a window was minimized, it will try to reverse animation for it
643 QTest::qWait(250);
644
645 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
646
647 window->setMinimized(true);
648
649 QCOMPARE(effectOutputSpy.count(), 1);
650 QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
651
652 {
653 const auto state = effect->state();
654 QCOMPARE(state.count(), 1);
655 QCOMPARE(state.firstKey(), window->effectWindow());
656 const QList<AniData> animations = state.first().first;
657 QCOMPARE(animations.count(), 1);
658 QCOMPARE(animations[0].timeLine.direction(), TimeLine::Backward);
659 QVERIFY(around(animations[0].timeLine.elapsed(), 1000ms - 250ms, 50ms));
660 }
661
662 // wait for the animation to reach the start position, 100ms is an extra
663 // safety margin
664 QTest::qWait(250 + 100);
665
666 QFETCH(bool, shouldTerminate);
667 if (shouldTerminate) {
668 const auto state = effect->state();
669 QCOMPARE(state.count(), 0);
670 } else {
671 const auto state = effect->state();
672 QCOMPARE(state.count(), 1);
673 QCOMPARE(state.firstKey(), window->effectWindow());
674 const QList<AniData> animations = state.first().first;
675 QCOMPARE(animations.count(), 1);
676 QCOMPARE(animations[0].timeLine.direction(), TimeLine::Backward);
677 QCOMPARE(animations[0].timeLine.elapsed(), 1000ms);
678 QCOMPARE(animations[0].timeLine.value(), 0.0);
679 }
680}
681
682void ScriptedEffectsTest::testComplete()
683{
684 // this test verifies that complete works
685
686 // load the test effect
687 auto effect = new ScriptedEffectWithDebugSpy;
688 QVERIFY(effect->load(QStringLiteral("completeTest")));
689
690 // create test window
691 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
692 QVERIFY(surface);
693 Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), surface.get());
694 QVERIFY(shellSurface);
695 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
696 QVERIFY(window);
697 QCOMPARE(workspace()->activeWindow(), window);
698
699 auto around = [](std::chrono::milliseconds elapsed,
700 std::chrono::milliseconds pivot,
701 std::chrono::milliseconds margin) {
702 return std::abs(elapsed.count() - pivot.count()) < margin.count();
703 };
704
705 // initially, the test animation should be at the start position
706 {
707 const auto state = effect->state();
708 QCOMPARE(state.count(), 1);
709 QCOMPARE(state.firstKey(), window->effectWindow());
710 const QList<AniData> animations = state.first().first;
711 QCOMPARE(animations.count(), 1);
712 QVERIFY(around(animations[0].timeLine.elapsed(), 0ms, 50ms));
713 QVERIFY(!animations[0].timeLine.done());
714 }
715
716 // wait for 250ms
717 QTest::qWait(250);
718
719 {
720 const auto state = effect->state();
721 QCOMPARE(state.count(), 1);
722 QCOMPARE(state.firstKey(), window->effectWindow());
723 const QList<AniData> animations = state.first().first;
724 QCOMPARE(animations.count(), 1);
725 QVERIFY(around(animations[0].timeLine.elapsed(), 250ms, 50ms));
726 QVERIFY(!animations[0].timeLine.done());
727 }
728
729 // minimize the test window, when the test effect sees that a window was
730 // minimized, it will try to complete animation for it
731 QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
732
733 window->setMinimized(true);
734
735 QCOMPARE(effectOutputSpy.count(), 1);
736 QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok"));
737
738 {
739 const auto state = effect->state();
740 QCOMPARE(state.count(), 1);
741 QCOMPARE(state.firstKey(), window->effectWindow());
742 const QList<AniData> animations = state.first().first;
743 QCOMPARE(animations.count(), 1);
744 QCOMPARE(animations[0].timeLine.elapsed(), 1000ms);
745 QVERIFY(animations[0].timeLine.done());
746 }
747}
748
750#include "scripted_effects_test.moc"
QMap< EffectWindow *, QPair< QList< AniData >, QRect > > AniMap
Base class for all KWin effects.
Definition effect.h:535
QStringList listOfKnownEffects() const override
All the Effects this loader knows of.
QStringList loadedEffects
Q_SCRIPTABLE bool isEffectLoaded(const QString &name) const
void hasActiveFullScreenEffectChanged()
Effect * activeFullScreenEffect() const
bool init(const QString &effectName, const QString &pathToScript)
void isActiveFullScreenEffectChanged()
QJSEngine * engine() const
void deletedRemoved(KWin::Window *)
bool load(const QString &name)
void testOutput(const QString &data)
Q_INVOKABLE void sendTestResponse(const QString &out)
#define WAYLANDTEST_MAIN(TestObject)
Window * renderAndWaitForShown(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format=QImage::Format_ARGB32, int timeout=5000)
void destroyWaylandConnection()
void setOutputConfig(const QList< QRect > &geometries)
bool setupWaylandConnection(AdditionalWaylandInterfaces flags=AdditionalWaylandInterfaces())
bool renderNodeAvailable()
std::unique_ptr< KWayland::Client::Surface > createSurface()
XdgToplevel * createXdgToplevelSurface(KWayland::Client::Surface *surface, QObject *parent=nullptr)
@ WindowAddedGrabRole
@ ElectricTopRight
Definition globals.h:62
WaylandServer * waylandServer()
Workspace * workspace()
Definition workspace.h:830
EffectsHandler * effects