My browser game Void Parry went to a portal's moderation with dozens of automated browser tests passing. The answer came back a few days later:
The WASD keys aren't working in the game.
That was the whole review. On my machine the keys worked. In every test the keys worked. On the portal's page they did nothing. It took one real key press on the real page to see why, and the cause was a line I wrote on day one.
canvas.addEventListener("pointerdown", (e) => {
e.preventDefault(); // no text selection, no touch scrolling, no long-press menu
handlePress(e);
});
Calling preventDefault() on pointerdown is common in canvas games. It also cancels the default action of the press - and one of those defaults is moving keyboard focus to what you clicked.
On my own site that never mattered: the game is the top-level page, the document already has focus, keys arrive.
A portal does not load your game as the top-level page. It puts it in an <iframe> (one portal I checked nests it two frames deep). An iframe only receives key events while it has focus, and it normally gets focus when the player clicks inside it. My handler cancelled exactly that. Pointer events kept arriving - mouse aim and buttons worked, which hid the problem - but keydown went to the portal's page forever.
You can see it in two lines from the devtools console of the game frame:
document.hasFocus() // false, even right after clicking the game
The obvious repair is to take focus yourself:
canvas.addEventListener("pointerdown", (e) => {
e.preventDefault();
window.focus();
handlePress(e);
});
I verified it on the live page - click, press W A S D, all four arrive - and sent the build back.
Hours later I walked through what the reviewer had actually done, and it was not what I had tested. A new player in my game lands straight in the first boss fight. The mouse only aims; you never have to click. So the reviewer opened the game, pressed W, A, S, D, saw nothing move, and wrote the review. No click ever happened. My fix repaired a path the reporter never took.
A frame is allowed to focus itself without a click, so the real fix has three parts:
window.focus(); // when the game loads
function startFight() { window.focus(); /* ... */ }
canvas.addEventListener("pointermove", () => { // the player is over the game
if (!document.hasFocus()) window.focus();
});
Plus window.focus() after an ad closes, because an ad overlay takes the keyboard and does not hand it back.
All my tests loaded index.html directly. The fix for that is small: a host page with the game in an iframe, and a test that never clicks.
fs.writeFileSync("host.html", '<iframe src="index.html" style="width:1280px;height:720px"></iframe>');
fs.writeFileSync("host2.html", '<iframe src="host.html" style="width:1280px;height:720px"></iframe>');
for (const host of ["host.html", "host2.html"]) { // one and two frames deep
await page.goto(host);
const game = page.frames().find((f) => /index\.html/.test(f.url()));
await game.evaluate(() => { window.keys = []; addEventListener("keydown", (e) => keys.push(e.code)); });
for (const k of ["KeyW", "KeyA", "KeyS", "KeyD"]) await page.keyboard.press(k); // no click first
assert.equal(await game.evaluate(() => keys.join()), "KeyW,KeyA,KeyS,KeyD");
}
It has three cases now: no click at all, the portal page has focus and the player only moves the mouse, and the portal page has focus and the player clicks. The old build fails the first two. Then I ran the same check against the live portal page, because a test page I wrote can share my blind spots.
I went back to a second portal that had rejected the game twice, the last time for "overall quality", opened the build they had reviewed in their own preview tool, clicked in the game and pressed the keys. Nothing arrived there either. Two reviews of a keyboard game whose keyboard did not work on their site. I do not know how much of "overall quality" that was. I do know the reviewers could not move.
preventDefault() on pointer events cancels focus too.
The game and its tests were written with an AI coding assistant; the bug, the review text and the fix are from the real build.