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>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user