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);
|
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.
|
* 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).
|
* 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 create = () => run('create', () => net.request('room.create', { mode: roomMode, difficulty }));
|
||||||
const join = (c = code) => {
|
const join = (c = code) => {
|
||||||
const k = normalizeCode(c);
|
// `onClick=${join}` hands the click EVENT as the first argument, and a default parameter only applies to
|
||||||
if (!CODE_RE.test(k)) { toast(`同盟密钥为 ${ROOM_CODE_LEN} 位字母或数字`, 'warn'); return; }
|
// `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 }));
|
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)
|
// a spectator seat: no player seat taken, nothing to do but watch (also a match already running)
|
||||||
const spectate = () => {
|
const spectate = (c = code) => {
|
||||||
const k = normalizeCode(code);
|
// same guard as join: `onClick=${spectate}` passes the click event, not a code
|
||||||
if (!CODE_RE.test(k)) { toast(`同盟密钥为 ${ROOM_CODE_LEN} 位字母或数字`, 'warn'); return; }
|
const k = codeArg(c, code);
|
||||||
run('spectate', () => net.request('room.spectate', { code: k }));
|
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 = () => {
|
const backToTitle = () => {
|
||||||
identity.setEntered(false);
|
identity.setEntered(false);
|
||||||
@@ -305,7 +334,8 @@ export function LobbyScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="join-foot">
|
<div class="join-foot">
|
||||||
${recent.length ? html`<span class="t-lo">最近的同盟</span>
|
${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>`}
|
: html`<span class="t-dim">向同伴索取 ${ROOM_CODE_LEN} 位同盟密钥,或直接打开邀请链接</span>`}
|
||||||
</div>
|
</div>
|
||||||
<//>
|
<//>
|
||||||
|
|||||||
@@ -992,6 +992,28 @@ describe('screen helpers', () => {
|
|||||||
assert.equal(difficultyInfo('coop', 'BOGUS').rounds, 14);
|
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 () => {
|
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 { difficultyInfo, stageNote, stageLabel, STAGE_POOL } = await mod('screens/lobby.js');
|
||||||
const cfg = JSON.parse(readFileSync(path.join(ROOT, 'data/config.json'), 'utf8'));
|
const cfg = JSON.parse(readFileSync(path.join(ROOT, 'data/config.json'), 'utf8'));
|
||||||
|
|||||||
Reference in new issue
Block a user