The 观战 and 加入 buttons send the typed key, not the click event (#119)

Preact hands the click event to onClick handlers, so the 0.1.3 spectate button normalised an element into the key "OBJE" and every spectate request failed. codeArg() takes only a string key and otherwise falls back to the input field; spectate errors now say what went wrong; a recent-key chip fills the field instead of joining at once. Contributed by @salt-fishes.
This commit is contained in:
saltfish authored and GitHub committed 2026-10-05 14:20:08 +08:00
1 parent 3f05c65dec
commit 7bea774f01
2 files changed
+59 -7

No files matched your search

+37 -7
View File
@@ -115,6 +115,20 @@ export function normalizeCode(v) {
return s.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, ROOM_CODE_LEN);
}
/**
* A handler that Preact binds as `onClick=${fn}` receives the click EVENT as its first argument, and a default
* parameter only applies to `undefined` — so `fn(c = code)` would normalise the event target into a nonsense code
* (`String(el)` → `"[object HTMLElement]"` → "OBJE"). Only a string is ever a code; anything else falls back to the
* input field. Returns null when neither yields a well-formed code.
* @param {unknown} arg the argument a handler was called with
* @param {string} field the current input-field value
* @returns {string|null}
*/
export function codeArg(arg, field) {
const k = normalizeCode(typeof arg === 'string' ? arg : field);
return CODE_RE.test(k) ? k : null;
}
/**
* Room code from a deep link query string (`?room=CODE`), or null when absent/malformed.
* Accepts ROOM_CODE_LEN..ROOM_CODE_LEN+2 alphanumerics (the protocol's join limit).
@@ -248,15 +262,30 @@ export function LobbyScreen() {
};
const create = () => run('create', () => net.request('room.create', { mode: roomMode, difficulty }));
const join = (c = code) => {
const k = normalizeCode(c);
if (!CODE_RE.test(k)) { toast(`同盟密钥为 ${ROOM_CODE_LEN} 位字母或数字`, 'warn'); return; }
// `onClick=${join}` hands the click EVENT as the first argument, and a default parameter only applies to
// `undefined` — codeArg keeps an event target out of the key and falls back to the input field
const k = codeArg(c, code);
if (!k) { toast(`同盟密钥为 ${ROOM_CODE_LEN} 位字母或数字`, 'warn'); return; }
run('join', () => net.request('room.join', { code: k }));
};
// a spectator seat: no player seat taken, nothing to do but watch (also a match already running)
const spectate = () => {
const k = normalizeCode(code);
if (!CODE_RE.test(k)) { toast(`同盟密钥为 ${ROOM_CODE_LEN} 位字母或数字`, 'warn'); return; }
run('spectate', () => net.request('room.spectate', { code: k }));
const spectate = (c = code) => {
// same guard as join: `onClick=${spectate}` passes the click event, not a code
const k = codeArg(c, code);
if (!k) { toast(`同盟密钥为 ${ROOM_CODE_LEN} 位字母或数字`, 'warn'); return; }
run('spectate', () => net.request('room.spectate', { code: k }).catch((err) => {
// Clearer than the bare ERR_TEXT: the usual cause is a code that is not the host's (a remembered one from an
// earlier room, or another machine's) — the server can only answer "no such room".
if (err?.code === ERR.ROOM_NOT_FOUND) {
toast(`没有找到密钥 ${k} 对应的同盟:请和房主核对密钥(同盟结束后密钥即失效)`, 'warn');
return;
}
if (err?.code === ERR.ALREADY) {
toast('你已经是该同盟的博士:先离开同盟,才能以观战身份进入', 'warn');
return;
}
throw err;
}));
};
const backToTitle = () => {
identity.setEntered(false);
@@ -305,7 +334,8 @@ export function LobbyScreen() {
</div>
<div class="join-foot">
${recent.length ? html`<span class="t-lo">最近的同盟</span>
${recent.map((c) => html`<button key=${c} type="button" class="code-chip num" onClick=${() => { setCode(c); join(c); }}>${c}</button>`)}`
${recent.map((c) => html`<button key=${c} type="button" class="code-chip num" title="填入密钥(不会直接加入)"
onClick=${() => setCode(c)}>${c}</button>`)}`
: html`<span class="t-dim">向同伴索取 ${ROOM_CODE_LEN} 位同盟密钥,或直接打开邀请链接</span>`}
</div>
<//>
+22
View File
@@ -992,6 +992,28 @@ describe('screen helpers', () => {
assert.equal(difficultyInfo('coop', 'BOGUS').rounds, 14);
});
// Regression: `onClick=${handler}` hands Preact's click EVENT as the first argument, and a default parameter only
// applies to `undefined` — so `normalizeCode(eventTarget)` produced "[object HTMLElement]" → "OBJE", and every
// 观战 click sent the same nonsense key no matter what was typed in the field. codeArg() is the guard for that.
test('lobby: a click event is never mistaken for an alliance key (codeArg)', async () => {
const { codeArg, normalizeCode } = await mod('screens/lobby.js');
// a DOM element stringifies to "[object HTMLElement]" — the shape that caused the bug
const element = { toString: () => '[object HTMLElement]' };
assert.equal(normalizeCode(element), 'OBJE', 'the old bug: the element normalises into a fake 4-letter code');
assert.equal(codeArg(element, 'NJBU'), 'NJBU', 'a non-string argument falls back to the input field');
// plain event-like objects and other non-strings behave the same
for (const weird of [{}, [], 42, true, null, undefined, Symbol('x'), () => {}]) {
assert.equal(codeArg(weird, 'NJBU'), 'NJBU', `falls back for ${String(weird)}`);
}
// an explicit string always wins (the full-room prompt passes the code it asked for)
assert.equal(codeArg('ab cd', 'NJBU'), 'ABCD');
// no usable code at all
assert.equal(codeArg(element, ''), null);
assert.equal(codeArg(element, 'AB'), null);
assert.equal(codeArg(undefined, undefined), null);
assert.equal(codeArg('ZZZ QQQ', ''), 'ZZZQ', 'a real string is still truncated to ROOM_CODE_LEN');
});
test('lobby: battlefield note per difficulty (标准 fixed 战场#01, 险境 8 / 绝境·终极 7 random) matches config.json modes[].stages', async () => {
const { difficultyInfo, stageNote, stageLabel, STAGE_POOL } = await mod('screens/lobby.js');
const cfg = JSON.parse(readFileSync(path.join(ROOT, 'data/config.json'), 'utf8'));