Skip to content

Commit 6d29a35

Browse files
committed
VibrationActuator should stop vibrating when its document becomes hidden
https://bugs.webkit.org/show_bug.cgi?id=250414 Reviewed by Youenn Fablet. VibrationActuator should stop vibrating when its document becomes hidden: - https://w3c.github.io/gamepad/extensions.html#gamepadhapticactuator-interface (Page Visibility section) Also address test flakiness by properly clearing mock gamepads between tests. We used to call MockGamepadProvider::clearMockGamepads() inside the WebContent process even though they are stored in the UIProcess nowadays. * LayoutTests/gamepad/gamepad-vibration-document-no-longer-fully-active.html: Added. * LayoutTests/gamepad/gamepad-vibration-document-no-longer-fully-active-expected.txt: Added. * LayoutTests/gamepad/gamepad-vibration-visibility-change-expected.txt: Added. * LayoutTests/gamepad/gamepad-vibration-visibility-change.html: Added. * Source/WebCore/Modules/gamepad/Gamepad.cpp: (WebCore::Gamepad::Gamepad): (WebCore::Gamepad::vibrationActuator): * Source/WebCore/Modules/gamepad/Gamepad.h: * Source/WebCore/Modules/gamepad/GamepadHapticActuator.cpp: (WebCore::GamepadHapticActuator::create): (WebCore::GamepadHapticActuator::GamepadHapticActuator): (WebCore::GamepadHapticActuator::~GamepadHapticActuator): (WebCore::GamepadHapticActuator::playEffect): (WebCore::GamepadHapticActuator::reset): (WebCore::GamepadHapticActuator::stopEffects): (WebCore::GamepadHapticActuator::document): (WebCore::GamepadHapticActuator::activeDOMObjectName const): (WebCore::GamepadHapticActuator::suspend): (WebCore::GamepadHapticActuator::stop): (WebCore::GamepadHapticActuator::visibilityStateChanged): * Source/WebCore/Modules/gamepad/GamepadHapticActuator.h: * Source/WebCore/Modules/gamepad/GamepadHapticActuator.idl: * Source/WebCore/Modules/gamepad/GamepadManager.cpp: (WebCore::navigatorGamepadFromDOMWindow): * Source/WebCore/Modules/gamepad/NavigatorGamepad.cpp: (WebCore::NavigatorGamepad::NavigatorGamepad): (WebCore::NavigatorGamepad::from): (WebCore::NavigatorGamepad::gamepadFromPlatformGamepad): (WebCore::NavigatorGamepad::getGamepads): (WebCore::NavigatorGamepad::gamepadsBecameVisible): (WebCore::NavigatorGamepad::gamepadConnected): * Source/WebCore/Modules/gamepad/NavigatorGamepad.h: * Source/WebCore/Modules/webxr/WebXRInputSource.cpp: (WebCore::WebXRInputSource::WebXRInputSource): * Source/WebCore/page/Navigator.cpp: (WebCore::Navigator::document): * Source/WebCore/page/Navigator.h: * Source/WebCore/testing/MockGamepad.cpp: (WebCore::MockGamepad::MockGamepad): Canonical link: https://commits.webkit.org/258802@main
1 parent e1ba903 commit 6d29a35

22 files changed

+227
-64
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Tests that the [playingEffectPromise] gets resolved with 'preempted' when the document is no longer fully active
2+
3+
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
4+
5+
6+
* Starting dual-rumble effect
7+
* Detach iframe
8+
PASS result is "preempted"
9+
PASS successfullyParsed is true
10+
11+
TEST COMPLETE
12+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<head>
2+
<script src="../resources/js-test.js"></script>
3+
<body>
4+
<script>
5+
description("Tests that the [playingEffectPromise] gets resolved with 'preempted' when the document is no longer fully active");
6+
jsTestIsAsync = true;
7+
8+
function runTest() {
9+
testFrame = document.getElementById("testFrame");
10+
testFrame.contentWindow.addEventListener("gamepadconnected", async e => {
11+
gamepad = testFrame.contentWindow.navigator.getGamepads()[0];
12+
debug("* Starting dual-rumble effect");
13+
let promise = gamepad.vibrationActuator.playEffect('dual-rumble', { duration: 3000 });
14+
debug("* Detach iframe");
15+
testFrame.remove();
16+
try {
17+
result = await promise;
18+
shouldBeEqualToString("result", "preempted");
19+
} catch (error) {
20+
testFailed("Promise got unexpectedly rejected: " + error);
21+
}
22+
finishJSTest();
23+
});
24+
testRunner.setMockGamepadDetails(0, "Test Gamepad", "", 2, 2);
25+
testRunner.setMockGamepadAxisValue(0, 0, 0.7);
26+
testRunner.setMockGamepadAxisValue(0, 1, -1.0);
27+
testRunner.setMockGamepadButtonValue(0, 0, 1.0);
28+
testRunner.setMockGamepadButtonValue(0, 1, 1.0);
29+
testRunner.connectMockGamepad(0);
30+
}
31+
onload = runTest;
32+
</script>
33+
<iframe id="testFrame" src="about:blank"></iframe>
34+
</body>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Tests that the [playingEffectPromise] gets resolved with 'preempted' when the document becomes hidden
2+
3+
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
4+
5+
6+
* Starting dual-rumble effect
7+
* Changing page visibility to hidden
8+
PASS result is "preempted"
9+
PASS successfullyParsed is true
10+
11+
TEST COMPLETE
12+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<head>
2+
<script src="../resources/js-test.js"></script>
3+
<body>
4+
<script>
5+
description("Tests that the [playingEffectPromise] gets resolved with 'preempted' when the document becomes hidden");
6+
jsTestIsAsync = true;
7+
8+
function runTest() {
9+
addEventListener("gamepadconnected", async e => {
10+
gamepad = e.gamepad;
11+
debug("* Starting dual-rumble effect");
12+
let promise = gamepad.vibrationActuator.playEffect('dual-rumble', { duration: 3000 });
13+
debug("* Changing page visibility to hidden");
14+
if (window.internals)
15+
internals.setPageVisibility(false);
16+
try {
17+
result = await promise;
18+
shouldBeEqualToString("result", "preempted");
19+
} catch (error) {
20+
testFailed("Promise got unexpectedly rejected: " + error);
21+
}
22+
finishJSTest();
23+
});
24+
testRunner.setMockGamepadDetails(0, "Test Gamepad", "", 2, 2);
25+
testRunner.setMockGamepadAxisValue(0, 0, 0.7);
26+
testRunner.setMockGamepadAxisValue(0, 1, -1.0);
27+
testRunner.setMockGamepadButtonValue(0, 0, 1.0);
28+
testRunner.setMockGamepadButtonValue(0, 1, 1.0);
29+
testRunner.connectMockGamepad(0);
30+
}
31+
onload = runTest;
32+
</script>
33+
</body>

Source/WebCore/Modules/gamepad/Gamepad.cpp

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@
3535

3636
namespace WebCore {
3737

38-
Gamepad::Gamepad(const PlatformGamepad& platformGamepad)
38+
Gamepad::Gamepad(Document* document, const PlatformGamepad& platformGamepad)
3939
: m_id(platformGamepad.id())
4040
, m_index(platformGamepad.index())
4141
, m_connected(true)
4242
, m_timestamp(platformGamepad.lastUpdateTime())
4343
, m_mapping(platformGamepad.mapping())
4444
, m_supportedEffectTypes(platformGamepad.supportedEffectTypes())
4545
, m_axes(platformGamepad.axisValues().size(), 0.0)
46+
, m_vibrationActuator(GamepadHapticActuator::create(document, GamepadHapticActuator::Type::DualRumble, *this))
4647
{
4748
unsigned buttonCount = platformGamepad.buttonValues().size();
4849
m_buttons.reserveInitialCapacity(buttonCount);
@@ -74,9 +75,7 @@ void Gamepad::updateFromPlatformGamepad(const PlatformGamepad& platformGamepad)
7475

7576
GamepadHapticActuator& Gamepad::vibrationActuator()
7677
{
77-
if (!m_vibrationActuator)
78-
m_vibrationActuator = GamepadHapticActuator::create(GamepadHapticActuator::Type::DualRumble, *this);
79-
return *m_vibrationActuator;
78+
return m_vibrationActuator.get();
8079
}
8180

8281
} // namespace WebCore

Source/WebCore/Modules/gamepad/Gamepad.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,16 @@
3636

3737
namespace WebCore {
3838

39+
class Document;
3940
class GamepadButton;
4041
class GamepadHapticActuator;
4142
class PlatformGamepad;
4243

4344
class Gamepad: public RefCounted<Gamepad>, public CanMakeWeakPtr<Gamepad> {
4445
public:
45-
static Ref<Gamepad> create(const PlatformGamepad& platformGamepad)
46+
static Ref<Gamepad> create(Document* document, const PlatformGamepad& platformGamepad)
4647
{
47-
return adoptRef(*new Gamepad(platformGamepad));
48+
return adoptRef(*new Gamepad(document, platformGamepad));
4849
}
4950
~Gamepad();
5051

@@ -64,7 +65,7 @@ class Gamepad: public RefCounted<Gamepad>, public CanMakeWeakPtr<Gamepad> {
6465
GamepadHapticActuator& vibrationActuator();
6566

6667
private:
67-
explicit Gamepad(const PlatformGamepad&);
68+
Gamepad(Document*, const PlatformGamepad&);
6869
String m_id;
6970
unsigned m_index;
7071
bool m_connected;
@@ -75,7 +76,7 @@ class Gamepad: public RefCounted<Gamepad>, public CanMakeWeakPtr<Gamepad> {
7576
Vector<double> m_axes;
7677
Vector<Ref<GamepadButton>> m_buttons;
7778

78-
RefPtr<GamepadHapticActuator> m_vibrationActuator;
79+
Ref<GamepadHapticActuator> m_vibrationActuator;
7980
};
8081

8182
} // namespace WebCore

Source/WebCore/Modules/gamepad/GamepadHapticActuator.cpp

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,6 @@
3838

3939
namespace WebCore {
4040

41-
Ref<GamepadHapticActuator> GamepadHapticActuator::create(Type type, Gamepad& gamepad)
42-
{
43-
return adoptRef(*new GamepadHapticActuator(type, gamepad));
44-
}
45-
46-
GamepadHapticActuator::GamepadHapticActuator(Type type, Gamepad& gamepad)
47-
: m_type { type }
48-
, m_gamepad { gamepad }
49-
{
50-
}
51-
5241
static bool areEffectParametersValid(GamepadHapticEffectType effectType, const GamepadEffectParameters& parameters)
5342
{
5443
if (parameters.duration < 0 || parameters.startDelay < 0)
@@ -61,25 +50,43 @@ static bool areEffectParametersValid(GamepadHapticEffectType effectType, const G
6150
return true;
6251
}
6352

53+
Ref<GamepadHapticActuator> GamepadHapticActuator::create(Document* document, Type type, Gamepad& gamepad)
54+
{
55+
auto actuator = adoptRef(*new GamepadHapticActuator(document, type, gamepad));
56+
actuator->suspendIfNeeded();
57+
return actuator;
58+
}
59+
60+
GamepadHapticActuator::GamepadHapticActuator(Document* document, Type type, Gamepad& gamepad)
61+
: ActiveDOMObject(document)
62+
, m_type { type }
63+
, m_gamepad { gamepad }
64+
{
65+
if (document)
66+
document->registerForVisibilityStateChangedCallbacks(*this);
67+
}
68+
6469
GamepadHapticActuator::~GamepadHapticActuator() = default;
6570

6671
bool GamepadHapticActuator::canPlayEffectType(EffectType effectType) const
6772
{
6873
return m_gamepad && m_gamepad->supportedEffectTypes().contains(effectType);
6974
}
7075

71-
void GamepadHapticActuator::playEffect(Document& document, EffectType effectType, GamepadEffectParameters&& effectParameters, Ref<DeferredPromise>&& promise)
76+
void GamepadHapticActuator::playEffect(EffectType effectType, GamepadEffectParameters&& effectParameters, Ref<DeferredPromise>&& promise)
7277
{
7378
if (!areEffectParametersValid(effectType, effectParameters)) {
7479
promise->reject(Exception { TypeError, "Invalid effect parameter"_s });
7580
return;
7681
}
77-
if (!document.isFullyActive() || document.hidden() || !m_gamepad) {
82+
83+
auto document = this->document();
84+
if (!document || !document->isFullyActive() || document->hidden() || !m_gamepad) {
7885
promise->resolve<IDLEnumeration<Result>>(Result::Preempted);
7986
return;
8087
}
8188
if (auto playingEffectPromise = std::exchange(m_playingEffectPromise, nullptr)) {
82-
document.eventLoop().queueTask(TaskSource::Gamepad, [playingEffectPromise = WTFMove(playingEffectPromise)] {
89+
queueTaskKeepingObjectAlive(*this, TaskSource::Gamepad, [playingEffectPromise = WTFMove(playingEffectPromise)] {
8390
playingEffectPromise->resolve<IDLEnumeration<Result>>(Result::Preempted);
8491
});
8592
}
@@ -91,33 +98,68 @@ void GamepadHapticActuator::playEffect(Document& document, EffectType effectType
9198
effectParameters.duration = std::min(effectParameters.duration, GamepadEffectParameters::maximumDuration.milliseconds());
9299

93100
m_playingEffectPromise = WTFMove(promise);
94-
GamepadProvider::singleton().playEffect(m_gamepad->index(), m_gamepad->id(), effectType, effectParameters, [this, protectedThis = Ref { *this }, document = Ref { document }, playingEffectPromise = m_playingEffectPromise](bool success) {
101+
GamepadProvider::singleton().playEffect(m_gamepad->index(), m_gamepad->id(), effectType, effectParameters, [this, protectedThis = makePendingActivity(*this), playingEffectPromise = m_playingEffectPromise](bool success) mutable {
95102
if (m_playingEffectPromise != playingEffectPromise)
96103
return; // Was already pre-empted.
97-
document->eventLoop().queueTask(TaskSource::Gamepad, [playingEffectPromise = std::exchange(m_playingEffectPromise, nullptr), success] {
104+
queueTaskKeepingObjectAlive(*this, TaskSource::Gamepad, [playingEffectPromise = std::exchange(m_playingEffectPromise, nullptr), success] {
98105
playingEffectPromise->resolve<IDLEnumeration<Result>>(success ? Result::Complete : Result::Preempted);
99106
});
100107
});
101108
}
102109

103-
void GamepadHapticActuator::reset(Document& document, Ref<DeferredPromise>&& promise)
110+
void GamepadHapticActuator::reset(Ref<DeferredPromise>&& promise)
104111
{
105-
if (!document.isFullyActive() || document.hidden() || !m_gamepad) {
112+
auto document = this->document();
113+
if (!document || !document->isFullyActive() || document->hidden() || !m_gamepad) {
106114
promise->resolve<IDLEnumeration<Result>>(Result::Preempted);
107115
return;
108116
}
109-
if (auto playingEffectPromise = std::exchange(m_playingEffectPromise, nullptr)) {
110-
document.eventLoop().queueTask(TaskSource::Gamepad, [playingEffectPromise = WTFMove(playingEffectPromise)] {
111-
playingEffectPromise->resolve<IDLEnumeration<Result>>(Result::Preempted);
112-
});
113-
}
114-
GamepadProvider::singleton().stopEffects(m_gamepad->index(), m_gamepad->id(), [document = Ref { document }, promise = WTFMove(promise)]() mutable {
115-
document->eventLoop().queueTask(TaskSource::Gamepad, [promise = WTFMove(promise)] {
117+
stopEffects([this, protectedThis = makePendingActivity(*this), promise = WTFMove(promise)]() mutable {
118+
queueTaskKeepingObjectAlive(*this, TaskSource::Gamepad, [promise = WTFMove(promise)] {
116119
promise->resolve<IDLEnumeration<Result>>(Result::Complete);
117120
});
118121
});
119122
}
120123

124+
void GamepadHapticActuator::stopEffects(CompletionHandler<void()>&& completionHandler)
125+
{
126+
if (!m_playingEffectPromise)
127+
return completionHandler();
128+
129+
queueTaskKeepingObjectAlive(*this, TaskSource::Gamepad, [playingEffectPromise = std::exchange(m_playingEffectPromise, nullptr)] {
130+
playingEffectPromise->resolve<IDLEnumeration<Result>>(Result::Preempted);
131+
});
132+
GamepadProvider::singleton().stopEffects(m_gamepad->index(), m_gamepad->id(), WTFMove(completionHandler));
133+
}
134+
135+
Document* GamepadHapticActuator::document()
136+
{
137+
return downcast<Document>(scriptExecutionContext());
138+
}
139+
140+
const char* GamepadHapticActuator::activeDOMObjectName() const
141+
{
142+
return "GamepadHapticActuator";
143+
}
144+
145+
void GamepadHapticActuator::suspend(ReasonForSuspension)
146+
{
147+
stopEffects([] { });
148+
}
149+
150+
void GamepadHapticActuator::stop()
151+
{
152+
stopEffects([] { });
153+
}
154+
155+
void GamepadHapticActuator::visibilityStateChanged()
156+
{
157+
auto* document = this->document();
158+
if (!document || !document->hidden())
159+
return;
160+
stopEffects([] { });
161+
}
162+
121163
} // namespace WebCore
122164

123165
#endif // ENABLE(GAMEPAD)

Source/WebCore/Modules/gamepad/GamepadHapticActuator.h

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727

2828
#if ENABLE(GAMEPAD)
2929

30+
#include "ActiveDOMObject.h"
3031
#include "GamepadHapticEffectType.h"
32+
#include "VisibilityChangeClient.h"
3133
#include <wtf/Forward.h>
3234
#include <wtf/RefCounted.h>
3335
#include <wtf/WeakPtr.h>
@@ -39,22 +41,33 @@ class Document;
3941
class Gamepad;
4042
struct GamepadEffectParameters;
4143

42-
class GamepadHapticActuator : public RefCounted<GamepadHapticActuator> {
44+
class GamepadHapticActuator : public RefCounted<GamepadHapticActuator>, public ActiveDOMObject, public VisibilityChangeClient {
4345
public:
4446
using EffectType = GamepadHapticEffectType;
4547
enum class Type : uint8_t { Vibration, DualRumble };
4648
enum class Result : uint8_t { Complete, Preempted };
4749

48-
static Ref<GamepadHapticActuator> create(Type, Gamepad&);
50+
static Ref<GamepadHapticActuator> create(Document*, Type, Gamepad&);
4951
~GamepadHapticActuator();
5052

5153
Type type() const { return m_type; }
5254
bool canPlayEffectType(EffectType) const;
53-
void playEffect(Document&, EffectType, GamepadEffectParameters&&, Ref<DeferredPromise>&&);
54-
void reset(Document&, Ref<DeferredPromise>&&);
55+
void playEffect(EffectType, GamepadEffectParameters&&, Ref<DeferredPromise>&&);
56+
void reset(Ref<DeferredPromise>&&);
5557

5658
private:
57-
GamepadHapticActuator(Type, Gamepad&);
59+
GamepadHapticActuator(Document*, Type, Gamepad&);
60+
61+
Document* document();
62+
void stopEffects(CompletionHandler<void()>&&);
63+
64+
// ActiveDOMObject.
65+
const char* activeDOMObjectName() const final;
66+
void suspend(ReasonForSuspension) final;
67+
void stop() final;
68+
69+
// VisibilityChangeClient.
70+
void visibilityStateChanged() final;
5871

5972
Type m_type;
6073
WeakPtr<Gamepad> m_gamepad;

Source/WebCore/Modules/gamepad/GamepadHapticActuator.idl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@ enum GamepadHapticResult {
3434
};
3535

3636
[
37+
ActiveDOMObject,
3738
Conditional=GAMEPAD,
3839
EnabledBySetting=GamepadVibrationActuatorEnabled,
3940
Exposed=Window
4041
] interface GamepadHapticActuator {
4142
readonly attribute GamepadHapticActuatorType type;
4243
boolean canPlayEffectType(GamepadHapticEffectType type);
43-
[CallWith=RelevantDocument] Promise<GamepadHapticResult> playEffect(GamepadHapticEffectType type, optional GamepadEffectParameters params = {});
44-
[CallWith=RelevantDocument] Promise<GamepadHapticResult> reset();
44+
Promise<GamepadHapticResult> playEffect(GamepadHapticEffectType type, optional GamepadEffectParameters params = {});
45+
Promise<GamepadHapticResult> reset();
4546

4647
// Non standard: Supported for compatibility with Blink.
4748
[ImplementedAs=canPlayEffectType] boolean canPlay(GamepadHapticEffectType type);

Source/WebCore/Modules/gamepad/GamepadManager.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ namespace WebCore {
4343

4444
static NavigatorGamepad* navigatorGamepadFromDOMWindow(DOMWindow* window)
4545
{
46-
return NavigatorGamepad::from(&window->navigator());
46+
return NavigatorGamepad::from(window->navigator());
4747
}
4848

4949
GamepadManager& GamepadManager::singleton()

0 commit comments

Comments
 (0)