Fix nuScore double lineup synchronization
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 1m0s

This commit is contained in:
Torsten Schulz (local)
2026-10-28 10:12:28 +01:00
parent 34807adf3b
commit 6f4f865b4a
3 changed files with 244 additions and 130 deletions

View File

@@ -0,0 +1,146 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
import { resolveLineupToken } from '../src/services/lineupResolver.mjs';
// Exercise the actual dialog methods without mounting its network/UI lifecycle.
const source = readFileSync(new URL('../src/components/MatchReportApiDialog.vue', import.meta.url), 'utf8');
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1]
.replace(/^import .*;\n/gm, '')
.replace('export default', 'globalThis.component =');
const context = vm.createContext({ resolveLineupToken, console });
vm.runInContext(script, context);
const methods = context.component.methods;
const player = (id, name, single, double, selected = true) => ({
nuLigaPersonId: id, firstname: name, isSelected: selected,
positionSingle: single, positionDouble: double,
});
const home = [
player(1, 'Fred Swyter', null, null, false),
player(2, 'Lukas Wypior', 1, 1),
player(3, 'Magdalena Schwalbach', 2, 1),
player(4, 'Timo Wolf', 3, 2),
player(5, 'Joshua Koch', 4, 2),
];
const guest = [
player(11, 'Anton Schottdorf', 1, 1),
player(12, 'Anna Wolf', 2, 2),
player(13, 'Raffaele Garofano', 3, 2),
player(14, 'Edis Toprak Basaraner', 4, 1),
];
function dialog(players = structuredClone(home)) {
return Object.assign({}, methods, {
meetingDetails: {
teamLineupHomePlayers: players,
teamLineupGuestPlayers: structuredClone(guest),
meetingPlayMode: { matchFormations: [
{ matchNr: 1, homeLineupPosition: 'D1', guestLineupPosition: 'D1' },
{ matchNr: 2, homeLineupPosition: 'D2', guestLineupPosition: 'D2' },
{ matchNr: 3, homeLineupPosition: 'E1', guestLineupPosition: 'E2' },
] },
},
currentPlayModeMatrix: [],
});
}
test('screenshot lineup resolves both explicit doubles, including the crossed guest pairs', () => {
const report = dialog();
assert.equal(report.resolveMatchSide({}, 0, 'home'), 'Lukas Wypior / Magdalena Schwalbach');
assert.equal(report.resolveMatchSide({}, 0, 'guest'), 'Anton Schottdorf / Edis Toprak Basaraner');
assert.equal(report.resolveMatchSide({}, 1, 'home'), 'Timo Wolf / Joshua Koch');
assert.equal(report.resolveMatchSide({}, 1, 'guest'), 'Anna Wolf / Raffaele Garofano');
});
test('current positions override obsolete match names and unselected singles', () => {
const report = dialog();
const stale = { playerA1: { firstname: 'Fred Swyter' }, homePlayer: 'Fred Swyter' };
assert.equal(report.resolveMatchSide(stale, 2, 'home'), 'Lukas Wypior');
assert.equal(report.resolveMatchSide(stale, 0, 'home'), 'Lukas Wypior / Magdalena Schwalbach');
assert.equal(report.resolveSide('A4 – B4', 'home'), 'Joshua Koch');
});
test('clearing a double does not resurrect server names or invent a pair from rank', () => {
const report = dialog();
for (const p of report.meetingDetails.teamLineupHomePlayers) p.positionDouble = null;
assert.equal(report.resolveMatchSide({ homePlayer: 'Old / Pair' }, 0, 'home'), '');
assert.equal(report.resolveSide('DA1 – DB1', 'home'), '');
assert.equal(dialog([{ nuLigaPersonId: 1, firstname: 'One', isSelected: true },
{ nuLigaPersonId: 2, firstname: 'Two', isSelected: true }]).resolveSide('DA1 – DB1', 'home'), '');
});
test('nuScore participants remain available when lineup positions are absent', () => {
const report = dialog([]);
assert.equal(report.resolveMatchSide({ playerA1: { firstname: 'One' }, playerA2: { firstname: 'Two' } }, 0, 'home'), 'One / Two');
assert.equal(report.resolveMatchSide({ homePlayer: 'One / Two' }, 0, 'home'), 'One / Two');
assert.equal(report.resolveMatchSide({ homePlayer: 'nicht angegeben' }, 0, 'home'), '');
});
test('numeric strings and legacy positions work; double-only players stay out of singles', () => {
const players = [player(1, 'One', '1', '2'), player(2, 'Two', null, '2', false)];
assert.equal(resolveLineupToken(players, 'DA2'), 'One / Two');
assert.equal(resolveLineupToken(players, 'A2'), '');
assert.equal(resolveLineupToken([{ nuLigaPersonId: 1, firstname: 'Legacy', isSelected: true, positions: ['E1', 'D2'] }], 'DB2'), 'Legacy');
assert.equal(resolveLineupToken([{ ...players[0], positionDouble: null, positions: ['D2'] }], 'DA2'), '');
});
test('formation lookup uses match number rather than array order', () => {
assert.equal(dialog().resolveMatchSide({ matchNr: 2 }, 0, 'guest'), 'Anna Wolf / Raffaele Garofano');
});
test('a stale three-player matrix gains D2 for four against four without moving singles scores into it', () => {
const report = dialog();
report.getPlayMode = () => 'braunschweiger system';
report.currentPlayModeMatrix = [
'DA1 – DB1', 'DA2 – DB2', 'A1 – B1', 'A2 – B2', 'A3 – B3',
'A4 – B4', 'A1 – B2', 'A2 – B1', 'A3 – B4', 'A4 – B3',
];
report.meetingDetails.meetingPlayMode.matchFormations = [
{ matchNr: 1, homeLineupPosition: 'D1', guestLineupPosition: 'D1' },
{ matchNr: 2, homeLineupPosition: 'E1', guestLineupPosition: 'E2' },
];
report.meetingDetails.matches = [
{ matchNr: 1, set1A: 11, set1B: 5 },
{ matchNr: 2, set1A: 11, set1B: 8, playerA1: { firstname: 'Fred Swyter' } },
];
report.ensureTtrSnapshots = () => {};
report.prepareResults();
assert.equal(report.results.length, 10);
assert.equal(report.results[1].homeName, 'Timo Wolf / Joshua Koch');
assert.equal(report.results[1].guestName, 'Anna Wolf / Raffaele Garofano');
assert.equal(report.meetingDetails.matches[1].set1A, 0);
assert.equal(report.meetingDetails.matches[6].set1B, 8);
assert.equal(report.meetingDetails.matches[6].matchNr, 7);
assert.equal(report.meetingDetails.matches[0].set1B, 5);
});
test('an extra double-only player does not change the Braunschweig team-size matrix', () => {
const report = dialog([...structuredClone(home), player(100, 'Reserve', null, 2)]);
report.getPlayMode = () => 'braunschweiger system';
assert.equal(report.getEffectivePlayerCount('home'), 4);
});
test('a player may move between doubles without having to be the final selection', () => {
const report = dialog();
const lukas = report.meetingDetails.teamLineupHomePlayers[1];
const timo = report.meetingDetails.teamLineupHomePlayers[3];
// D2 contains Timo and Joshua, so it is full; after freeing one place,
// Lukas can be moved directly from D1 to D2.
timo.positionDouble = null;
report.persistLocalDraftDebounced = () => {};
report.broadcastDraftDebounced = () => {};
report.syncDraftToServerDebounced = () => {};
assert.equal(report.canSelectDoublePosition(lukas, 'home', 2), true);
report.toggleDoublePosition(lukas, 'home', 2);
assert.equal(lukas.positionDouble, 2);
assert.deepEqual(Array.from(lukas.positions), ['E1', 'D2']);
});
test('position arrays retain both doubles after any single-position recalculation', () => {
const report = dialog();
report.recalculatePositions('teamLineupHomePlayers');
assert.deepEqual(Array.from(report.meetingDetails.teamLineupHomePlayers[1].positions), ['E1', 'D1']);
assert.deepEqual(Array.from(report.meetingDetails.teamLineupHomePlayers[2].positions), ['E2', 'D1']);
assert.deepEqual(Array.from(report.meetingDetails.teamLineupHomePlayers[3].positions), ['E3', 'D2']);
assert.deepEqual(Array.from(report.meetingDetails.teamLineupHomePlayers[4].positions), ['E4', 'D2']);
});