SOURCE / PINNED RELEASE
Made of little things.
Pickle Rustlers
- Release
- 039e51f55e34…
- Author-recorded commit
- 40608586a8a1…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-bad44e7ab35
Archive hash verified: f38e99afa0992052…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { realpathSync } from 'node:fs';
import { build } from 'vite';
import test from 'node:test';
const require = createRequire(import.meta.url);
const conformance = dirname(require.resolve('@napplet/conformance-cli/package.json'));
const { chromium } = require(require.resolve('playwright', { paths: [conformance] }));
const nap = resolve(realpathSync('node_modules/@napplet/sdk'), '../nap/dist');
const bundle = await build({
configFile: false, logLevel: 'silent',
plugins: [{ name: 'keyboard-fixture', resolveId(id) { if (id === 'fixture' || id === resolve('fixture')) return '\0fixture'; }, load(id) {
if (id !== '\0fixture') return;
return `import { KeyboardInput } from ${JSON.stringify(resolve('src/game/keyboard.ts'))};
import * as shim from ${JSON.stringify(nap + '/keys/shim.js')};
window.fixture = { KeyboardInput, shim };`;
} }],
build: { write: false, minify: false, lib: { entry: 'fixture', formats: ['iife'], name: 'KeyboardTests' } },
});
const code = (Array.isArray(bundle) ? bundle[0] : bundle).output[0].code;
test('keyboard behavior with the real injected keys shim', async () => {
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
await page.route('http://localhost/**', route => route.fulfill({ body: '<html><body></body></html>', contentType: 'text/html' }));
await page.goto('http://localhost/');
await page.setContent('<button id="focus">Game</button><input id="text"><div contenteditable="true"><span id="nested">edit</span></div>');
await page.addScriptTag({ content: code });
await page.evaluate(() => {
const { shim, KeyboardInput } = window.fixture;
window.messages = [];
window.bindings = [];
window.activity = 0;
window.allowed = true;
// Host side only: answer registration and apply full binding replacements.
window.addEventListener('message', ({ data }) => {
if (!data?.type?.startsWith('keys.')) return;
window.messages.push(data);
if (data.type === 'keys.unregisterAction') {
window.bindings = window.bindings.filter(b => b.actionId !== data.actionId);
shim.handleKeysMessage({ type: 'keys.bindings', bindings: window.bindings });
}
if (data.type === 'keys.registerAction') {
window.bindings.push({ actionId: data.action.id, key: data.action.defaultKey });
shim.handleKeysMessage({ type: 'keys.bindings', bindings: window.bindings });
shim.handleKeysMessage({ type: 'keys.registerAction.result', id: data.id, actionId: data.action.id, binding: data.action.defaultKey });
}
});
shim.installKeysShim();
window.napplet = { keys: { registerAction: shim.registerAction, unregisterAction: shim.unregisterAction, onAction: shim.onAction } };
window.keyboard = new KeyboardInput(() => window.activity++, () => window.allowed);
window.send = (key, extra = {}, target = document.body) => target.dispatchEvent(new KeyboardEvent('keydown', { key, code: key === ' ' ? 'Space' : `Key${key.toUpperCase()}`, bubbles: true, cancelable: true, ...extra }));
window.read = () => [...window.keyboard.read()];
});
await page.waitForFunction(() => window.messages.filter(m => m.type === 'keys.registerAction').length === 3);
await page.locator('#focus').focus();
// Trusted input matters: browsers may checkpoint microtasks between capture listeners.
await page.keyboard.down('Space');
assert.deepEqual(await page.evaluate(() => window.read()), ['shoot']);
await page.keyboard.down('Space');
assert.deepEqual(await page.evaluate(() => window.read()), []);
await page.keyboard.up('Space');
assert.deepEqual(await page.evaluate(() => { window.send(' '); return window.read(); }), ['shoot']);
assert.deepEqual(await page.evaluate(() => { window.send(' ', { repeat: true }); return window.read(); }), []);
assert.equal(await page.evaluate(() => window.messages.filter(m => m.type === 'keys.forward' && m.key === ' ').length), 0);
// Replacement updates: remap shoot to x, leave lasso unbound, retain pause.
await page.evaluate(() => window.fixture.shim.handleKeysMessage({ type: 'keys.bindings', bindings: [
{ actionId: 'pickle-rustlers.shoot', key: 'x' }, { actionId: 'pickle-rustlers.pause', key: 'p' },
] }));
assert.deepEqual(await page.evaluate(() => { for (const k of [' ', 'j', 'Enter', 'e', 'Shift', 'Escape']) window.send(k); return window.read(); }), []);
assert.deepEqual(await page.evaluate(() => { window.send('x'); return window.read(); }), ['shoot']);
assert.deepEqual(await page.evaluate(() => {
const s = window.fixture.shim;
s.handleKeysMessage({ type: 'keys.action', actionId: 'unknown' });
s.handleKeysMessage({ type: 'keys.action', actionId: 'pickle-rustlers.lasso' });
return window.read();
}), ['lasso']);
assert.deepEqual(await page.evaluate(() => {
window.allowed = false;
window.fixture.shim.handleKeysMessage({ type: 'keys.action', actionId: 'pickle-rustlers.shoot' });
window.allowed = true;
return window.read();
}), []);
const beforeText = await page.evaluate(() => window.messages.filter(m => m.type === 'keys.forward').length);
for (const target of ['#text', '#nested']) {
await page.locator(target).focus();
assert.deepEqual(await page.evaluate((selector) => { window.send('x', {}, document.querySelector(selector)); return window.read(); }, target), []);
}
assert.equal(await page.evaluate(() => window.messages.filter(m => m.type === 'keys.forward').length), beforeText);
await page.locator('#text').focus();
assert.deepEqual(await page.evaluate(() => {
window.fixture.shim.handleKeysMessage({ type: 'keys.action', actionId: 'pickle-rustlers.shoot' });
return window.read();
}), []);
await page.locator('#focus').focus();
assert.deepEqual(await page.evaluate(() => { window.send('x', { isComposing: true }); window.send('w', { ctrlKey: true }); return window.read(); }), []);
assert.equal(await page.evaluate(() => window.keyboard.down('w')), false);
await page.evaluate(() => { window.send('w'); window.send('d'); });
assert.equal(await page.evaluate(() => window.keyboard.down('w') && window.keyboard.down('d')), true);
await page.evaluate(() => window.dispatchEvent(new KeyboardEvent('keyup', { key: 'W', code: 'KeyW', ctrlKey: true })));
assert.equal(await page.evaluate(() => window.keyboard.down('w')), false);
await page.evaluate(() => window.dispatchEvent(new Event('blur')));
assert.equal(await page.evaluate(() => window.keyboard.down('d')), false);
await page.evaluate(() => window.fixture.shim.handleKeysMessage({ type: 'keys.bindings', bindings: [
{ actionId: 'pickle-rustlers.shoot', key: 'Tab' },
{ actionId: 'pickle-rustlers.shoot', key: 'Shift+Tab' },
{ actionId: 'pickle-rustlers.shoot', key: 'Escape' },
] }));
assert.equal(await page.evaluate(() => window.send('Tab')), true);
assert.equal(await page.evaluate(() => window.send('Tab', { shiftKey: true })), true);
assert.deepEqual(await page.evaluate(() => window.read()), []);
assert.equal(await page.evaluate(() => window.send('Escape')), true);
await page.evaluate(() => window.send('z', { code: 'KeyZ', altKey: true, ctrlKey: true, shiftKey: true, metaKey: true }));
await page.waitForFunction(() => window.messages.some(m => m.type === 'keys.forward' && m.key === 'z'));
const forwarded = await page.evaluate(() => window.messages.find(m => m.type === 'keys.forward' && m.key === 'z'));
assert.deepEqual(forwarded, { type: 'keys.forward', key: 'z', code: 'KeyZ', alt: true, ctrl: true, shift: true, meta: true });
await page.evaluate(() => { window.keyboard.dispose(); window.keyboard.dispose(); });
await page.waitForFunction(() => window.messages.filter(m => m.type === 'keys.unregisterAction').length === 3);
assert.deepEqual(await page.evaluate(() => { window.fixture.shim.handleKeysMessage({ type: 'keys.action', actionId: 'pickle-rustlers.shoot' }); return window.read(); }), []);
await page.evaluate(() => { window.keyboard = new window.fixture.KeyboardInput(() => window.activity++, () => true); });
await page.waitForFunction(() => window.messages.filter(m => m.type === 'keys.registerAction').length === 6);
assert.deepEqual(await page.evaluate(() => { window.send(' '); return window.read(); }), ['shoot']);
await page.evaluate(() => window.keyboard.dispose());
await page.waitForFunction(() => window.messages.filter(m => m.type === 'keys.unregisterAction').length === 6);
// Optional absence/failure and late completion, using the SDK over injected fixtures.
for (const mode of ['absent', 'throw', 'reject', 'partial', 'listenerFailure', 'late']) {
const result = await page.evaluate(async (mode) => {
window.fixture.shim.installKeysShim()(); // remove real shim and its suppress list
let closed = 0;
const pending = [];
window.napplet = mode === 'absent' ? {} : { keys: {
registerAction(action) {
if (mode === 'throw') throw Error('denied');
if (mode === 'reject' || (mode === 'partial' && action.id.endsWith('lasso'))) return Promise.reject(Error('denied'));
if (mode === 'late') return new Promise(resolve => pending.push(() => resolve({ actionId: action.id })));
return Promise.resolve({ actionId: action.id });
},
onAction() { if (mode === 'listenerFailure') throw Error('unsupported'); return { close() {} }; },
unregisterAction() { closed++; },
} };
const keyboard = new window.fixture.KeyboardInput(() => {}, () => true);
window.keyboard = keyboard;
if (mode === 'late') keyboard.dispose();
for (const resolve of pending) resolve();
await new Promise(resolve => setTimeout(resolve, 0));
window.send('e');
const pressed = [...keyboard.read()];
if (mode === 'absent') {
window.send('ArrowUp', { code: 'ArrowUp', repeat: true });
if (!keyboard.down('arrowup')) throw Error('Repeated movement lost');
window.send('p', { ctrlKey: true });
window.send('j', { isComposing: true });
window.send('j', {}, document.querySelector('#text'));
if (keyboard.read().size) throw Error('Unsafe fallback action');
document.dispatchEvent(new Event('visibilitychange'));
if (keyboard.down('arrowup')) throw Error('Visibility reset failed');
}
keyboard.dispose();
window.send('w');
return { closed, pressed, heldAfterDispose: keyboard.down('w') };
}, mode);
assert.equal(result.heldAfterDispose, false, mode);
assert.deepEqual(result.pressed, mode === 'late' ? [] : ['lasso'], mode);
assert.equal(result.closed, ['partial'].includes(mode) ? 2 : ['late', 'listenerFailure'].includes(mode) ? 3 : 0, mode);
}
} finally { await browser.close(); }
});
