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:
1 parent
3f05c65dec
commit
7bea774f01
2 files changed
+59
-7
No files matched your search
@@ -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>
|
||||
<//>
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in new issue
Block a user