feedback7: merge fb7-g-small — PR #368 (AI teammates draft last, default off), #374 (double-click a teammate's avatar in prep), #321 (copy local diagnostics; whole-token / exact-value redaction) with the review's fixes

- Golden unchanged (union OK); broad node --test 5292 (5291 pass, 0 fail, 1 skipped); lint 0 errors.
This commit is contained in:
sganggs committed 2026-10-08 20:23:26 +08:00
commit fc2e57ebff
28 files changed
+1256 -35

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
// The room screen's 「AI 队友最后选择」 switch (GitHub #338; server: room.setAiPicksLast, room.state.aiPicksLast): co-op
// rooms only, the host toggles it, everybody else sees its state read-only; the texts go through t() with English in
// public/i18n/en.json.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const SRC = readFileSync(path.join(ROOT, 'public/js/screens/room.js'), 'utf8');
const EN = JSON.parse(readFileSync(path.join(ROOT, 'public/i18n/en.json'), 'utf8'));
const mod = () => import(pathToFileURL(path.join(ROOT, 'public/js/screens/room.js')).href);
const room = (o = {}) => ({
code: 'ABCD', hostId: 'h', mode: 'coop', difficulty: 'NORMAL', aiPicksLast: false,
seats: [{ seat: 0, playerId: 'h', name: 'Host', isBot: false, ready: false, connected: true }, { seat: 1, playerId: 'g', name: 'G', isBot: false, ready: false, connected: true }, null, null],
...o,
});
test('aiLastOption: hidden in solo, editable by the host only, the state from room.state (off when absent)', async () => {
const { aiLastOption } = await mod();
assert.equal(aiLastOption(room({ mode: 'solo' }), 'h'), null, 'solo: no AI teammates');
assert.deepEqual(aiLastOption(room(), 'h'), { on: false, editable: true });
assert.deepEqual(aiLastOption(room({ aiPicksLast: true }), 'h'), { on: true, editable: true });
assert.deepEqual(aiLastOption(room({ aiPicksLast: true }), 'g'), { on: true, editable: false });
assert.deepEqual(aiLastOption(room({ aiPicksLast: undefined }), 'g'), { on: false, editable: false }, 'an older server');
assert.equal(aiLastOption(null, 'h'), null);
});
test('room.js sends room.setAiPicksLast { on } and shows the switch through t(); en.json has the English', () => {
assert.match(SRC, /net\.request\('room\.setAiPicksLast', \{ on \}\)/);
for (const zh of ['AI 队友最后选择', '策略与机变轮选时,所有博士先于 AI 队友选择', '由创建者设置']) {
assert.ok(SRC.includes(`t('${zh}')`), zh);
assert.equal(typeof EN[zh], 'string', `en.json: ${zh}`);
assert.ok(EN[zh].length > 0 && !/[一-鿿]/.test(EN[zh]), `English for ${zh}`);
}
});
+59
View File
@@ -0,0 +1,59 @@
// PHASE.PREP double-click on a teammate avatar opens their field (GitHub #131, PR #374).
// 「前往查看」 stays. Two guards: a teammate with no field is not forced open, and a click while that
// teammate is already on screen does not open the view again (a double-click fires click first).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { PHASE } from '../../shared/constants.js';
import { prepDblClickWatches, teammateClickWatches, teammateFieldShown } from '../../public/js/ui/teamPanel.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const SRC = readFileSync(path.join(ROOT, 'public/js/ui/teamPanel.js'), 'utf8');
const mate = { playerId: 'p_1', fieldId: 'f1', name: '队友' };
const observeOf = (fieldId) => ({ canObserve: () => (fieldId ? { fieldId } : { reason: '该队友当前没有战场' }) });
test('a prep double-click watches once when the teammate has a field and is not already on screen', () => {
const o = { phase: PHASE.PREP, self: false, observe: observeOf('n:p_1'), watching: null, player: mate };
assert.equal(prepDblClickWatches(o), true);
assert.equal(prepDblClickWatches({ ...o, watching: 'n:p_1' }), false, 'already the prep scout');
assert.equal(prepDblClickWatches({ ...o, watching: 'f1' }), false, 'already their live field');
assert.equal(prepDblClickWatches({ ...o, askedRecently: true }), false, 'the click of this double-click already opened it');
});
test('no field: the double-click does not watch; the single click still toasts through onWatch', () => {
const o = { phase: PHASE.PREP, self: false, observe: observeOf(null), watching: null, player: mate };
assert.equal(prepDblClickWatches(o), false);
assert.equal(prepDblClickWatches({ ...o, observe: { canObserve: () => null } }), false);
assert.equal(prepDblClickWatches({ ...o, observe: {} }), false);
});
test('outside PREP, on your own row, or without client-combat observe, a double-click does not watch', () => {
const base = { phase: PHASE.PREP, self: false, observe: observeOf('n:p_1'), watching: null, player: mate };
assert.equal(prepDblClickWatches({ ...base, phase: PHASE.COMBAT }), false);
assert.equal(prepDblClickWatches({ ...base, phase: PHASE.FINAL_ASSAULT }), false, '最终攻势 prep is not PHASE.PREP');
assert.equal(prepDblClickWatches({ ...base, phase: PHASE.HIDDEN_CORE }), false, '隐秘核心 prep is not PHASE.PREP');
assert.equal(prepDblClickWatches({ ...base, self: true }), false);
assert.equal(prepDblClickWatches({ ...base, observe: null }), false, 'the click already watches in server-run combat');
assert.equal(prepDblClickWatches({ ...base, player: null }), false);
});
test('a single click while already watching that teammate does not open the view again', () => {
assert.equal(teammateClickWatches({ watching: null, player: mate }), true, 'server-run combat: the first click watches');
assert.equal(teammateClickWatches({ watching: 'f1', player: mate }), false);
assert.equal(teammateClickWatches({ watching: 'n:p_1', player: mate }), false);
assert.equal(teammateClickWatches({ watching: 'other', player: mate }), true);
assert.equal(teammateClickWatches({ watching: null, player: mate, askedRecently: true }), false, 'the other click of a double-click');
assert.equal(teammateClickWatches({ observe: observeOf('n:p_1'), watching: null, player: mate }), false, 'client combat opens 「前往查看」, it does not watch on click');
assert.equal(teammateFieldShown('n:p_1', mate), true);
assert.equal(teammateFieldShown(null, mate), false);
});
test('the panel wires both guards and keeps 「前往查看」', () => {
assert.ok(SRC.includes('onDblClick=${(e) => dblClick(e, p, self)}'));
assert.ok(SRC.includes('prepDblClickWatches('));
assert.ok(SRC.includes('teammateClickWatches('));
assert.ok(SRC.includes("${t('前往查看')}"));
});