LibWeb: Automatically disconnect all testing gamepads on test completion

Connected gamepads are stored on the Navigator, which means that when we
navigate to a new test, they stick around, including the virtual ones
we create for testing purposes. This caused issues such as the one
fixed in #7329 where failing to manually disconnect a virtual gamepad
meant that it would interfere with subsequent tests.

We don't want to rely on tests cleaning things up correctly, so this
commit introduces an automatic clean-up of these virtual gamepads.
InternalGamepad objects are now tracked in Internals, and all
disconnected in `Internals::perform_per_test_cleanup()`. It's named
generically as I'm expecting other systems (eg, localStorage or
cookies) to want similar cleanup between tests, but implementing those
is left as an exercise for a future person.

This method is called from signal_test_is_done(). This method is always
called in the case where the test completes. A test that crashes means
the WebContent isn't reused. A test that times out should also not have
its WebContent process reused, though it's not clear to me what we
currently do there.

Verified by commenting-out the `gamepad.disconnect()` line from the
Text/input/GamepadAPI/gamepad-is-available-in-new-navigables.html test
and then running:

```bash
for run in {1..20}; do
  Meta/ladybird.py run test-web -s -j1 -f GamepadAPI;
done
```
This commit is contained in:
Sam Atkins 2026-01-15 16:37:46 +00:00 committed by Tim Flynn
parent f2dd3d89d1
commit 5510867d58
4 changed files with 37 additions and 3 deletions

View file

@ -7,6 +7,7 @@
#include <LibWeb/Bindings/InternalGamepadPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Internals/InternalGamepad.h>
#include <LibWeb/Internals/Internals.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_joystick.h>
@ -61,8 +62,9 @@ static SDLCALL bool rumble_triggers(void* user_data, u16 left_rumble, u16 right_
return true;
}
InternalGamepad::InternalGamepad(JS::Realm& realm)
InternalGamepad::InternalGamepad(JS::Realm& realm, GC::Ref<Internals> internals)
: Bindings::PlatformObject(realm)
, m_internals(internals)
{
SDL_VirtualJoystickDesc virtual_joystick_desc {};
SDL_INIT_INTERFACE(&virtual_joystick_desc);
@ -108,6 +110,7 @@ void InternalGamepad::visit_edges(Cell::Visitor& visitor)
Base::visit_edges(visitor);
visitor.visit(m_received_rumble_effects);
visitor.visit(m_received_rumble_trigger_effects);
visitor.visit(m_internals);
}
void InternalGamepad::finalize()
@ -174,6 +177,7 @@ void InternalGamepad::received_rumble_triggers(u16 left_rumble, u16 right_rumble
void InternalGamepad::disconnect()
{
m_internals->disconnect_virtual_gamepad(*this);
SDL_CloseJoystick(m_sdl_joystick);
SDL_DetachVirtualJoystick(m_sdl_joystick_id);
}

View file

@ -38,7 +38,7 @@ public:
void disconnect();
private:
InternalGamepad(JS::Realm&);
InternalGamepad(JS::Realm&, GC::Ref<Internals>);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
virtual void finalize() override;
@ -47,6 +47,7 @@ private:
SDL_Joystick* m_sdl_joystick;
Vector<GC::Ref<JS::Object>> m_received_rumble_effects;
Vector<GC::Ref<JS::Object>> m_received_rumble_trigger_effects;
GC::Ref<Internals> m_internals;
};
}

View file

@ -49,8 +49,15 @@ void Internals::initialize(JS::Realm& realm)
Base::initialize(realm);
}
void Internals::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_gamepads);
}
void Internals::signal_test_is_done(String const& text)
{
perform_per_test_cleanup();
page().client().page_did_finish_test(text);
}
@ -407,7 +414,23 @@ void Internals::handle_sdl_input_events()
GC::Ref<InternalGamepad> Internals::connect_virtual_gamepad()
{
auto& realm = this->realm();
return realm.create<InternalGamepad>(realm);
auto gamepad = realm.create<InternalGamepad>(realm, *this);
m_gamepads.append(gamepad);
return gamepad;
}
void Internals::disconnect_virtual_gamepad(GC::Ref<InternalGamepad> gamepad)
{
if (auto index = m_gamepads.find_first_index(gamepad); index.has_value())
m_gamepads.remove(index.value());
}
void Internals::perform_per_test_cleanup()
{
// Detach any virtual gamepads
for (auto gamepad : m_gamepads)
gamepad->disconnect();
m_gamepads.clear();
}
}

View file

@ -80,14 +80,20 @@ public:
void handle_sdl_input_events();
GC::Ref<InternalGamepad> connect_virtual_gamepad();
void disconnect_virtual_gamepad(GC::Ref<InternalGamepad>);
void perform_per_test_cleanup();
private:
explicit Internals(JS::Realm&);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Visitor&) override;
void click(double x, double y, UIEvents::MouseButton);
void mouse_down(double x, double y, UIEvents::MouseButton);
Vector<GC::Ref<InternalGamepad>> m_gamepads;
};
}