Add special-target activation without bypassing attack cadence, cooperative card preferences and emotes, skill sound banks, per-operator voice choices, text sizing, install prompts, closed-tab recovery and unanimous setup rerolls. Retain late deployment elapsed time and resolve echo/counter pulses without recursive hook overflow. Independent final review remains pending. Golden: roster-016, roster-035, roster-036, roster-045; boss-boss_1-pair, boss-boss_3-pair, boss-boss_5-pair, boss-boss_7-pair, hidden-boss_9-pair; solo-HARD-9-boosted: Lemuen attack cadence. diy-132: Necras extra activation targets. coop2-NORMAL-14-standins: cooperative purchase preference and simulation fixes. hidden-boss_10-solo: echo pulse FIFO resolution. See DESIGN 28.13-28.24 for assumptions, sources and contributor credit.
55 lines
2.7 KiB
JavaScript
55 lines
2.7 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { createPwaInstall } from '../../public/js/pwa.js';
|
|
function rig(standalone = false) {
|
|
const win = new EventTarget(), display = new EventTarget();
|
|
display.matches = standalone;
|
|
win.matchMedia = () => display;
|
|
const pwa = createPwaInstall(win);
|
|
const offer = (outcome = 'dismissed', fail = false) => {
|
|
let calls = 0;
|
|
const e = new Event('beforeinstallprompt', { cancelable: true });
|
|
e.prompt = async () => { calls++; if (fail) throw Error('unavailable'); return { outcome }; };
|
|
win.dispatchEvent(e);
|
|
return { calls: () => calls, e };
|
|
};
|
|
return { pwa, win, display, offer };
|
|
}
|
|
test('install offer is consumed even on dismissal, duplicate clicks, or rejected prompt', async () => {
|
|
const { pwa, offer } = rig();
|
|
const states = []; const off = pwa.subscribe((x) => states.push(x));
|
|
assert.equal(pwa.available(), false);
|
|
const first = offer();
|
|
assert.equal(first.e.defaultPrevented, true);
|
|
assert.equal(pwa.available(), true);
|
|
const a = pwa.request(), b = pwa.request();
|
|
assert.equal(await a, false); assert.equal(await b, false); assert.equal(first.calls(), 1);
|
|
const broken = offer('accepted', true);
|
|
assert.equal(await pwa.request(), false); assert.equal(await pwa.request(), false); assert.equal(broken.calls(), 1);
|
|
const next = offer('accepted');
|
|
assert.equal(await pwa.request(), true); assert.equal(next.calls(), 1);
|
|
assert.deepEqual(states, [false, true, false, true, false, true, false]);
|
|
off(); pwa.dispose();
|
|
});
|
|
test('installed / standalone hides stale offers; disposing removes listeners', async () => {
|
|
const { pwa, win, display, offer } = rig();
|
|
offer(); display.matches = true; display.dispatchEvent(new Event('change'));
|
|
assert.equal(pwa.available(), false); assert.equal(await pwa.request(), false);
|
|
display.matches = false; win.dispatchEvent(new Event('appinstalled')); offer();
|
|
assert.equal(pwa.available(), false);
|
|
pwa.dispose();
|
|
assert.equal(offer().e.defaultPrevented, false);
|
|
const native = rig(true); native.offer(); assert.equal(native.pwa.available(), false); native.pwa.dispose();
|
|
});
|
|
test('manifest uses actual 192/512 icons shipped outside optional game assets', () => {
|
|
const m = JSON.parse(readFileSync(new URL('../../public/manifest.json', import.meta.url)));
|
|
assert.equal(m.start_url, '/'); assert.equal(m.scope, '/');
|
|
for (const icon of m.icons) {
|
|
assert.ok(icon.src.startsWith('/icons/'));
|
|
const bytes = readFileSync(new URL('../../public' + icon.src, import.meta.url));
|
|
const size = Number(icon.sizes.split('x')[0]);
|
|
assert.equal(bytes.readUInt32BE(16), size); assert.equal(bytes.readUInt32BE(20), size);
|
|
}
|
|
});
|