I'd like to write some Puppeteer-based tests to test some logic that makes use of the Gamepad API, but I can't find any documentation on the Puppeteer docs that explain how to simulate a gamepad and how to send button presses to the browser.
What's the proper way to do it?
There isn't really a "proper" way to do it.
What I've done is made a somewhat clean way you should be able to use. It essentially involves creating your own game controller management code to use in puppeteer, and injecting the controller state into a page using the puppeteer evaluate
API call.
Essentially we hijack the global scope navigator.getGamepads
function and inject our own implementation.
We start out by modelling a game controller in code. The most basic part of a controller is a button. So lets do that.
class Button {
constructor() {
this.value = 0.0
this.pressed = false
}
press() {
this.value = 1.0
this.pressed = true
}
unpress() {
this.value = 0.0
this.pressed = false
}
toObject() {
return {
value: this.value,
pressed: this.pressed,
}
}
}
Next, according to the W3C Gamepad Specification a gamepad has two analog sticks, so we'll model those.
class AnalogStick {
constructor() {
this.button = new Button()
this.xAxis = 0.0
this.yAxis = 0.0
}
setXAxis(value) {
this.xAxis = value
}
setYAxis(value) {
this.yAxis = value
}
}
Finally, let's create a class to represent a game pad. This class will have a helper function that translates the controllers internal state to match the W3C Gamepad Interface signature.
class GamePad {
constructor(index = 0) {
this.id = 'Standard Gamepad'
this.displayId = null // this is used for VR
this.connected = true
this.index = index
this.mapping = 'standard'
this.dPad = {
up: new Button(),
right: new Button(),
down: new Button(),
left: new Button(),
}
this.select = new Button()
this.home = new Button()
this.start = new Button()
this.actions = {
top: new Button(),
right: new Button(),
bottom: new Button(),
left: new Button(),
}
this.leftStick = new AnalogStick()
this.rightStick = new AnalogStick()
this.lButton = new Button()
this.lTrigger = new Button()
this.rButton = new Button()
this.rTrigger = new Button()
}
getState() {
return {
axes: [
this.leftStick.xAxis,
this.leftStick.yAxis,
this.rightStick.xAxis,
this.rightStick.yAxis,
],
buttons: [
this.actions.bottom.toObject(),
this.actions.right.toObject(),
this.actions.left.toObject(),
this.actions.top.toObject(),
this.lButton.toObject(),
this.rButton.toObject(),
this.lTrigger.toObject(),
this.rTrigger.toObject(),
this.select.toObject(),
this.start.toObject(),
this.leftStick.button.toObject(),
this.rightStick.button.toObject(),
this.dPad.up.toObject(),
this.dPad.down.toObject(),
this.dPad.left.toObject(),
this.dPad.right.toObject(),
this.home.toObject(),
],
connected: this.connected,
displayId: this.displayId,
id: this.id,
index: this.index,
mapping: this.mapping,
}
}
}
Now we have a convenient way of representing a gamecontroller and its state in code, we can hijack navigator.getGamepads
and replace it with our own function that returns the state of our virtual controllers.
Now we'll define a couple of helper functions. One that sets the gamepads state that navigator.getGamepads
will return.
const setGamepadsState = async (page, gamepads) => {
const result = await page.evaluate((controllers) => {
navigator.getGamepads = () => controllers
}, gamepads)
return result
}
Now that we've done that, we need a way to trigger the gamepadconnected
event. We can do that using the puppeteer page.emit
function call.
const connectGamepad = async (page, gamepad) => {
const connectedEvent = {
gamepad,
}
page.emit('gamepadconnected', connectedEvent)
}
We now have all the building blocks to simulate a controller using puppeteer! Example usage is below:
;(async () => {
const controller1 = new GamePad(0)
const controller2 = new GamePad(1)
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goTo('https://www.yourgamepadpage.com')
// Set the current gamepad state for both controllers in the puppeteer page.
// We need to call this each time we change a controllers state
await setGamepadsState(page, [controller1.getState(), controller2.getState()])
// fires a 'gamepadconnected' event in the page for controller1
await connectGamepad(page, controller1.getState())
// fires a 'gamepadconnected' event in the page for controller2
await connectGamepad(page, controller2.getState())
// toggles the state of the bottom action button to pressed on controller1, 'X' on a playstation pad or 'A' on an xbox pad
controller1.actions.bottom.press()
await setGamepadsState(page, [controller1.getState(), controller2.getState()]) // passes controller1's current state into puppeteer's 'page.evaluate'
// do a check here in your puppeteer based test!
console.log('this should be whatever test code you need!')
controller1.actions.bottom.unpress() // untoggles the state of the bottom action button on controller1
// now lets simulate an analog stick axis shift, e.g. left analog stick on the horizontal axis all the way to the left.
controller1.leftStick.setXAxis(-1.0)
await setGamepadsState(page, [controller1.getState(), controller2.getState()]) // and now we pass it to the page context!
await browser.close()
})()
Hopefully this should point you in the right direction. If you have any questions feel free to follow up here :)