From eba273d8fc7f0d7fa034be98918431a6b07cca83 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Mon, 28 Sep 2026 15:11:51 -0400 Subject: [PATCH 1/2] fix(polyfill): report already-connected gamepads to the first listener hookGamepadListeners started the gamepad backend before registering the caller's listener. The backends report already-connected controllers synchronously on start, so the first gamepadconnected listener missed them. Register the listener first. --- .../canvas-polyfill/__test__/gamepad.test.mjs | 58 +++++++++++++++++++ packages/canvas-polyfill/gamepad.ts | 4 +- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 packages/canvas-polyfill/__test__/gamepad.test.mjs diff --git a/packages/canvas-polyfill/__test__/gamepad.test.mjs b/packages/canvas-polyfill/__test__/gamepad.test.mjs new file mode 100644 index 000000000..b9d39870d --- /dev/null +++ b/packages/canvas-polyfill/__test__/gamepad.test.mjs @@ -0,0 +1,58 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { fileURLToPath } from 'node:url'; +import { build } from 'esbuild'; + +// Like the native backends, the fake reports already-connected pads synchronously when monitoring starts. +const fakeGamepad = ` +const pad = { index: 0, id: 'Fake (STANDARD GAMEPAD)', connected: true }; +let started = false; +export const gamepads = { + addListener(listener) { + if (!started) { + started = true; + listener({ type: 'gamepadconnected', gamepad: pad }); + } + }, +}; +export function getGamepads() { + return started ? [pad, null, null, null] : [null, null, null, null]; +} +`; + +async function loadPolyfill() { + const entry = fileURLToPath(new URL('../gamepad.ts', import.meta.url)); + const { outputFiles } = await build({ + entryPoints: [entry], + bundle: true, + format: 'esm', + write: false, + platform: 'neutral', + plugins: [ + { + name: 'fake-canvas-gamepad', + setup(b) { + b.onResolve({ filter: /^@nativescript\/canvas-gamepad$/ }, () => ({ path: 'fake', namespace: 'fake' })); + b.onLoad({ filter: /.*/, namespace: 'fake' }, () => ({ contents: fakeGamepad, loader: 'js' })); + }, + }, + ], + }); + return import('data:text/javascript;base64,' + Buffer.from(outputFiles[0].text).toString('base64') + '#' + Math.random()); +} + +test('first gamepadconnected listener sees pads that were already connected', async () => { + const target = new EventTarget(); + globalThis.addEventListener = target.addEventListener.bind(target); + globalThis.removeEventListener = target.removeEventListener.bind(target); + globalThis.dispatchEvent = target.dispatchEvent.bind(target); + globalThis.window = globalThis; + + const { hookGamepadListeners } = await loadPolyfill(); + hookGamepadListeners(); + + const seen = []; + window.addEventListener('gamepadconnected', (e) => seen.push(e.gamepad.id)); + + assert.deepEqual(seen, ['Fake (STANDARD GAMEPAD)']); +}); diff --git a/packages/canvas-polyfill/gamepad.ts b/packages/canvas-polyfill/gamepad.ts index f23a38a98..ee86e4a3a 100644 --- a/packages/canvas-polyfill/gamepad.ts +++ b/packages/canvas-polyfill/gamepad.ts @@ -32,10 +32,12 @@ export function hookGamepadListeners() { continue; } const hooked = function (this: unknown, type: string, ...rest: unknown[]) { + const result = add.call(this, type, ...rest); + // Register first: starting the backend reports already-connected pads synchronously. if (type === 'gamepadconnected' || type === 'gamepaddisconnected') { gamepads(); } - return add.call(this, type, ...rest); + return result; }; hooked.__gamepadHook = true; host.addEventListener = hooked; From d42fec0268fb171fcd35fd040025f5eae7c19b62 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Mon, 28 Sep 2026 15:11:51 -0400 Subject: [PATCH 2/2] chore: canvas-polyfill 3.0.0-alpha.19 --- packages/canvas-polyfill/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/canvas-polyfill/package.json b/packages/canvas-polyfill/package.json index dfc6c6ad0..725cd242f 100644 --- a/packages/canvas-polyfill/package.json +++ b/packages/canvas-polyfill/package.json @@ -1,6 +1,6 @@ { "name": "@nativescript/canvas-polyfill", - "version": "3.0.0-alpha.18", + "version": "3.0.0-alpha.19", "description": "Polyfill for making NativeScript compatible with web libs like pixi.js, three.js, phaser.js, babylon.js, etc....", "main": "index", "typings": "global.d.ts",