Back to Pursuit PIT
SOURCE / PINNED RELEASE

Made of little things.

Pursuit PIT

Release
565f4981bdc3…
Author-recorded commit
197afdbed44b…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-951414e7fd7

Archive hash verified: 3b0485589942fd5c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/controller-hosted.mjs
// Run against the URL printed by soyli dev: pnpm test:hosted http://127.0.0.1:PORT/
// Uses Playwright already installed by the reference conformance CLI.
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { execFileSync } from 'node:child_process';
import { mkdir } from 'node:fs/promises';
const require = createRequire(import.meta.url);
const { chromium } = createRequire(require.resolve('@napplet/conformance-cli/package.json'))('playwright');
const url = process.argv[2];
if (!url) throw new Error('Supply the URL printed by soyli dev.');
const paths = JSON.parse(execFileSync('soyli', ['browser', 'path', '--json'], { encoding: 'utf8' }));
const executablePath = paths.executables.find((p) => p.installed && p.name.includes('chromium'))?.path;
const browser = await chromium.launch({ executablePath, args: ['--no-sandbox'] });
const errors = [];
const page = await browser.newPage({ viewport: { width: 1400, height: 1100 } });
page.on('pageerror', (e) => errors.push(e.message));
const output = '.napplet-space/gamepad-tests';
await mkdir(output, { recursive: true });
try {
  await page.goto(url);
  const iframe = page.locator('#stage iframe');
  await iframe.waitFor();
  await iframe.evaluate((el) => { el.style.cssText = 'width:900px;height:600px;min-height:0;flex:none'; });
  const frame = await (await iframe.elementHandle()).contentFrame();
  await frame.locator('#modePursuit').waitFor();
  assert.equal(await iframe.getAttribute('sandbox'), 'allow-scripts');
  const domains = await frame.evaluate(() => Object.keys(window.napplet ?? {}));
  assert.ok(domains.includes('storage'), 'target shell injected storage');
  console.log('Injected domains:', domains.join(', '));
  await page.evaluate(() => {
    window.__gamepadWire = [];
    window.addEventListener('message', (event) => {
      if (typeof event.data?.type === 'string') window.__gamepadWire.push(event.data.type);
    });
  });
  // Test-only synthetic input, confined to the napplet realm. This is not a hardware test.
  await frame.evaluate(() => {
    window.__gamepadResults = [];
    window.addEventListener('message', (event) => {
      if (event.data?.type === 'storage.set.result') window.__gamepadResults.push(event.data);
    });
    window.__pad = { index: 2, id: 'Synthetic standard', connected: true, mapping: 'standard', axes: [0, 0],
      buttons: Array.from({ length: 16 }, () => ({ value: 0, pressed: false })) };
    Object.defineProperty(navigator, 'getGamepads', { configurable: true, value: () => window.__pad.connected ? [null, null, window.__pad] : [] });
  });
  await frame.locator('#title .route').click();
  const settle = () => frame.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
  const press = async (button) => {
    await frame.evaluate((i) => { window.__pad.buttons[i] = { value: 1, pressed: true }; }, button);
    await settle();
    await frame.evaluate((i) => { window.__pad.buttons[i] = { value: 0, pressed: false }; }, button);
    await settle();
  };
  const visible = (selector) => frame.locator(selector).isVisible();
  await settle(); await press(0);
  assert.match(await frame.locator('#title .controller-status').innerText(), /connected/);
  assert.equal(await visible('#timePicker'), false, 'join must not also confirm');
  // Controller-only mode/time navigation and analog driving.
  for (const mode of ['modePursuit', 'modeGetaway', 'modeFree']) {
    await frame.locator(`#${mode}`).focus();
    await press(0); assert.equal(await visible('#timePicker'), true);
    await press(14); await press(15); await press(0);
    assert.equal(await visible('#title'), false);
    assert.equal(await visible('#overlay'), false);
    await frame.evaluate(() => { window.__pad.buttons[7].value = 0.7; });
    await frame.waitForFunction(() => document.querySelector('#clock').textContent !== '0:00', null, { timeout: 30000 });
    console.log('Driving mode:', mode);
    await frame.evaluate(() => { window.__pad.buttons[7].value = 0; });
    await press(0); assert.equal(await frame.locator('#wipeButton').evaluate((b) => b.classList.contains('on')), true);
    await press(0);
    await press(9); assert.equal(await frame.locator('#overlayKicker').innerText(), 'PAUSED');
    await press(1); assert.equal(await visible('#overlay'), false);
    // Disconnect pauses. Rejoin consumes confirm; resume is a separate action.
    await frame.evaluate(() => { window.__pad.connected = false; }); await settle();
    assert.equal(await visible('#overlay'), true);
    await frame.evaluate(() => { window.__pad.connected = true; }); await settle();
    await press(0); assert.equal(await visible('#overlay'), true);
    await press(9); assert.equal(await visible('#overlay'), false);
    await press(9); await press(13); await press(0);
    assert.equal(await visible('#title'), true);
  }
  // Garage edits use the existing real SDK storage operation.
  await press(13); await press(13); await press(13); await press(0);
  assert.equal(await visible('#garage'), true);
  await press(15); await press(5); await press(13); await press(15);
  assert.equal(await frame.evaluate(() => document.activeElement.getAttribute('aria-pressed')), 'true', 'garage focuses selected paint');
  assert.ok((await page.evaluate(() => window.__gamepadWire)).includes('storage.set'), 'garage edit emits SDK storage.set');
  await frame.waitForFunction(() => window.__gamepadResults.length > 0);
  assert.ok(await frame.evaluate(() => window.__gamepadResults.some((result) => !result.error)), 'host accepts SDK storage operation');
  await press(1); assert.equal(await visible('#title'), true);
  // Help and back navigation.
  await press(13); await press(13); await press(13); await press(13); await press(0);
  assert.equal(await frame.locator('details.controls').getAttribute('open'), '');
  await press(1); assert.equal(await frame.locator('details.controls').getAttribute('open'), null);
  await frame.locator('#modeFree').focus(); await press(0); await press(1);
  assert.equal(await visible('#timePicker'), false);
  // Frame dimensions are explicit test overrides; soyLI normally enforces 300px height.
  for (const [width, height] of [[200, 160], [320, 560], [900, 600], [1400, 900]]) {
    await iframe.evaluate((el, { width, height }) => {
      el.style.cssText = `width:${width}px;height:${height}px;min-height:0;max-width:none;flex:none`;
    }, { width, height });
    await frame.waitForFunction(({ width, height }) => innerWidth === width && innerHeight === height, { width, height });
    await frame.locator('#modePursuit').focus();
    await frame.locator('#modePursuit').evaluate((el) => el.scrollIntoView({ block: 'nearest' }));
    await iframe.screenshot({ path: `${output}/title-${width}x${height}.png` });
    await press(0); await press(15);
    await iframe.screenshot({ path: `${output}/time-${width}x${height}.png` });
    await press(1);
  }
  // App-owned palette does not change when host appearance changes or theme is absent.
  const palette = () => frame.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--amber'));
  const before = await palette();
  for (const theme of ['dark', 'light']) {
    await page.evaluate((value) => { document.documentElement.dataset.theme = value; }, theme);
    assert.equal(await palette(), before);
  }
  await frame.evaluate(() => { delete window.napplet.theme; delete window.napplet.storage; });
  await frame.locator('#titleGarage').click(); await press(15); await press(1);
  assert.equal(await visible('#title'), true);
  // Missing native API produces a useful fallback, without stopping the frame loop.
  await frame.evaluate(() => Object.defineProperty(navigator, 'getGamepads', { configurable: true, value: undefined }));
  await settle();
  assert.match(await frame.locator('#title .controller-status').innerText(), /unavailable/);
  assert.deepEqual(errors, []);
  console.log('PASS: synthetic controller flows, real SDK storage operation, four iframe sizes, theme fallback.');
  console.log('Screenshots:', output);
} finally {
  await browser.close();
}