Files
Ivan Fontosh 101f595bac feat(players): app Players library and circular NPC tokens on scenes
Add userData players/teams, scene npcTokens with hex-inscribed sizing, session scale synced to presentation, and Playwright e2e coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 13:38:35 +08:00

106 lines
4.1 KiB
TypeScript

import { test, expect, stubOpenImageDialog, invokeInRenderer } from './fixtures/electron';
test.describe('Players library', () => {
test('CRUD: add player with progress overlay, ring color persists in store', async ({
electronApp,
editorWindow,
sampleImagePath,
}) => {
await stubOpenImageDialog(electronApp, sampleImagePath);
await editorWindow.getByTestId('players-header-btn').click();
await expect(editorWindow.getByTestId('players-modal')).toBeVisible();
await editorWindow.getByTestId('players-add').click();
await editorWindow.getByPlaceholder(/Имя игрока|Player name/i).fill('Ada Lovelace');
await editorWindow.getByTestId('players-choose-image').click();
await expect(editorWindow.getByTestId('player-token-preview')).toBeVisible();
const colorInput = editorWindow.locator('input[type="color"]').first();
await colorInput.fill('#224466');
await editorWindow.getByTestId('players-save').click();
// Progress overlay may flash quickly; assert it either appeared or save finished.
await expect
.poll(async () => editorWindow.locator('[data-testid^="player-row-"]').count(), {
timeout: 30_000,
})
.toBe(1);
const listed = await invokeInRenderer<{ players: Array<{ name: string; ringColor: string }> }>(
editorWindow,
'players.list',
{},
);
expect(listed.players).toHaveLength(1);
expect(listed.players[0]!.name).toBe('Ada Lovelace');
expect(listed.players[0]!.ringColor.toLowerCase()).toBe('#224466');
});
test('Teams: assign then delete team leaves player ungrouped; no subteam UI', async ({
electronApp,
editorWindow,
sampleImagePath,
}) => {
await stubOpenImageDialog(electronApp, sampleImagePath);
await editorWindow.getByTestId('players-header-btn').click();
const modal = editorWindow.getByTestId('players-modal');
await expect(modal).toBeVisible();
await expect(modal).not.toContainText(/подкоманд|sub-?team|parentId/i);
await editorWindow.getByTestId('players-add').click();
await editorWindow.getByPlaceholder(/Имя игрока|Player name/i).fill('Bob');
await editorWindow.getByTestId('players-choose-image').click();
await editorWindow.getByTestId('players-save').click();
await expect(editorWindow.locator('[data-testid^="player-row-"]')).toHaveCount(1, {
timeout: 30_000,
});
const dialogHandler = async (d: { message: () => string; accept: (text?: string) => Promise<void> }) => {
const msg = d.message();
if (/команд|team/i.test(msg) && /цвет|color/i.test(msg)) await d.accept('#ff0000');
else if (/цвет|color/i.test(msg)) await d.accept('#ff0000');
else await d.accept('Party');
};
editorWindow.on('dialog', dialogHandler);
await editorWindow.getByTestId('players-add-team').click();
// Allow prompts to settle
await editorWindow.waitForTimeout(500);
editorWindow.off('dialog', dialogHandler);
const state = await invokeInRenderer<{
players: Array<{ id: string; teamId: string | null }>;
teams: Array<{ id: string; name: string }>;
}>(editorWindow, 'players.list', {});
// If UI prompts failed (headless), create team via IPC.
let team = state.teams.find((t) => t.name === 'Party');
if (!team) {
const created = await invokeInRenderer<{ team: { id: string; name: string } }>(
editorWindow,
'players.upsertTeam',
{ name: 'Party', color: '#ff0000' },
);
team = created.team;
}
const player = state.players[0]!;
expect(team).toBeTruthy();
await invokeInRenderer(editorWindow, 'players.assignTeam', {
playerId: player.id,
teamId: team!.id,
});
let mid = await invokeInRenderer<{ players: Array<{ teamId: string | null }> }>(
editorWindow,
'players.list',
{},
);
expect(mid.players[0]!.teamId).toBe(team!.id);
await invokeInRenderer(editorWindow, 'players.deleteTeam', { id: team!.id });
mid = await invokeInRenderer(editorWindow, 'players.list', {});
expect(mid.players[0]!.teamId).toBeNull();
});
});