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:
@@ -17,6 +17,7 @@ type ButtonProps = {
|
||||
iconOnly?: boolean;
|
||||
/** Позиция тултипа относительно кнопки. */
|
||||
tooltipPlacement?: 'top' | 'bottom' | 'bottom-left';
|
||||
'data-testid'?: string;
|
||||
};
|
||||
|
||||
export function Button({
|
||||
@@ -28,6 +29,7 @@ export function Button({
|
||||
ariaLabel,
|
||||
iconOnly = false,
|
||||
tooltipPlacement = 'top',
|
||||
'data-testid': testId,
|
||||
}: ButtonProps) {
|
||||
const btnRef = useRef<HTMLButtonElement | null>(null);
|
||||
const hostRef = useRef<HTMLSpanElement | null>(null);
|
||||
@@ -85,6 +87,7 @@ export function Button({
|
||||
className={btnClass}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testId}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
onMouseEnter={disabled ? undefined : showTip}
|
||||
onMouseLeave={disabled ? undefined : hideTip}
|
||||
|
||||
@@ -70,3 +70,52 @@ void test('WindowErrorBoundary component exists and catches errors', () => {
|
||||
assert.ok(src.includes('componentDidCatch'));
|
||||
assert.ok(src.includes('role="alert"'));
|
||||
});
|
||||
|
||||
void test('EditorApp: Players header after File, opens modal', () => {
|
||||
const src = fs.readFileSync(path.join(rendererRoot, 'editor/EditorApp.tsx'), 'utf8');
|
||||
const fileIdx = src.indexOf("t('top.file')");
|
||||
const playersIdx = src.indexOf('data-testid="players-header-btn"');
|
||||
assert.ok(fileIdx > 0, 'File menu present');
|
||||
assert.ok(playersIdx > fileIdx, 'Players button must follow File in source order');
|
||||
assert.ok(src.includes('PlayersManagerModal'));
|
||||
assert.ok(src.includes('setPlayersManagerOpen(true)'));
|
||||
});
|
||||
|
||||
void test('Players modal: flat teams only, progress overlay, PlayerTokenView', () => {
|
||||
const src = fs.readFileSync(path.join(rendererRoot, 'editor/PlayersModals.tsx'), 'utf8');
|
||||
assert.ok(src.includes('PlayerTokenView'));
|
||||
assert.ok(src.includes('data-testid="players-modal"'));
|
||||
assert.ok(src.includes('data-testid="players-save-progress"'));
|
||||
assert.doesNotMatch(src, /parentId|подкоманд|subteam|sub-team/i);
|
||||
assert.ok(src.includes("application/x-dnd-player-id"));
|
||||
});
|
||||
|
||||
void test('Scene NPC accordion: separate mime and PlayerTokenView markers', () => {
|
||||
const scene = fs.readFileSync(path.join(rendererRoot, 'sceneEditor/SceneEditorApp.tsx'), 'utf8');
|
||||
const tokenTile = fs.readFileSync(path.join(rendererRoot, 'sceneEditor/TokenTile.tsx'), 'utf8');
|
||||
assert.ok(scene.includes('data-testid="scene-npc-accordion"'));
|
||||
assert.ok(scene.includes("application/x-dnd-scene-npc-id"));
|
||||
assert.ok(tokenTile.includes("application/x-dnd-app-token-id"));
|
||||
assert.notEqual(
|
||||
'application/x-dnd-scene-npc-id',
|
||||
'application/x-dnd-app-token-id',
|
||||
'NPC scene mime must differ from app token mime',
|
||||
);
|
||||
assert.ok(scene.includes('PlayerTokenView'));
|
||||
assert.ok(scene.includes('persistNpcTokens([])'));
|
||||
assert.doesNotMatch(scene, /SceneTokenMarker[\s\S]{0,80}npcTokens/);
|
||||
});
|
||||
|
||||
void test('Control and Presentation use SceneNpcTokensOverlay', () => {
|
||||
const control = fs.readFileSync(path.join(rendererRoot, 'control/ControlApp.tsx'), 'utf8');
|
||||
const presentation = fs.readFileSync(path.join(rendererRoot, 'shared/PresentationView.tsx'), 'utf8');
|
||||
assert.ok(control.includes('SceneNpcTokensOverlay'));
|
||||
assert.ok(presentation.includes('SceneNpcTokensOverlay'));
|
||||
assert.ok(control.includes('sceneNpcTokensSession'));
|
||||
});
|
||||
|
||||
void test('NpcsEditorApp: token appearance via PlayerTokenView', () => {
|
||||
const src = fs.readFileSync(path.join(rendererRoot, 'npcs/NpcsEditorApp.tsx'), 'utf8');
|
||||
assert.ok(src.includes('PlayerTokenView'));
|
||||
assert.ok(src.includes('ringColor') || src.includes('updateNpcFields'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user