86d6d157f0
Очередь NPC/игроков с DnD, курсором на сцене, состояниями и запасом; сброс при смене сцены. Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
8.3 KiB
TypeScript
188 lines
8.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const rendererRoot = path.resolve(here, '../..');
|
|
|
|
const SECONDARY_WINDOW_MAINS = [
|
|
'editor/main.tsx',
|
|
'npcs/npcsEditorMain.tsx',
|
|
'npcs/npcsMain.tsx',
|
|
'materials/main.tsx',
|
|
'playlists/main.tsx',
|
|
'timer/main.tsx',
|
|
'initiative/main.tsx',
|
|
'sceneEditor/main.tsx',
|
|
'sceneDescription/main.tsx',
|
|
'control/main.tsx',
|
|
'presentation/main.tsx',
|
|
] as const;
|
|
|
|
void test('secondary window mains: WindowErrorBoundary wraps app root', () => {
|
|
for (const rel of SECONDARY_WINDOW_MAINS) {
|
|
const src = fs.readFileSync(path.join(rendererRoot, rel), 'utf8');
|
|
assert.ok(
|
|
src.includes('WindowErrorBoundary'),
|
|
`${rel}: must wrap with WindowErrorBoundary to avoid black screen on React crash`,
|
|
);
|
|
assert.match(
|
|
src,
|
|
/<WindowErrorBoundary[\s\S]*?>[\s\S]*<\/WindowErrorBoundary>/,
|
|
`${rel}: WindowErrorBoundary must wrap children`,
|
|
);
|
|
}
|
|
});
|
|
|
|
void test('NpcDescriptionField: TipTap StrictMode-safe (no black screen on NPC open)', () => {
|
|
const src = fs.readFileSync(path.join(rendererRoot, 'npcs/NpcDescriptionField.tsx'), 'utf8');
|
|
assert.match(src, /immediatelyRender:\s*false/);
|
|
assert.ok(src.includes('readTipTapHtmlSafe'));
|
|
assert.ok(src.includes('isDestroyed'));
|
|
assert.doesNotMatch(src, /immediatelyRender:\s*true/);
|
|
});
|
|
|
|
void test('NpcsEditorApp: no undefined controlStyles (inspector crash)', () => {
|
|
const src = fs.readFileSync(path.join(rendererRoot, 'npcs/NpcsEditorApp.tsx'), 'utf8');
|
|
assert.doesNotMatch(src, /controlStyles/);
|
|
});
|
|
|
|
void test('NPC binding removed from editor UI', () => {
|
|
const editor = fs.readFileSync(path.join(rendererRoot, 'npcs/NpcsEditorApp.tsx'), 'utf8');
|
|
const modal = fs.readFileSync(path.join(rendererRoot, 'npcs/NpcEditModal.tsx'), 'utf8');
|
|
assert.doesNotMatch(editor, /NpcBindingFields|binding/);
|
|
assert.doesNotMatch(modal, /NpcBindingFields|binding|noneBinding/);
|
|
assert.ok(!fs.existsSync(path.join(rendererRoot, 'npcs/NpcBindingFields.tsx')));
|
|
assert.ok(!fs.existsSync(path.join(rendererRoot, '../shared/npcs/npcBinding.ts')));
|
|
});
|
|
|
|
void test('storyline export modal: NPC selection step', () => {
|
|
const src = fs.readFileSync(path.join(rendererRoot, 'editor/StorylineTransferModals.tsx'), 'utf8');
|
|
assert.match(src, /step === 'npcs'/);
|
|
assert.match(src, /export\.selectAllNpcs/);
|
|
assert.match(src, /npcIds/);
|
|
assert.match(src, /Array\.isArray\(res\?\.npcs\)/);
|
|
assert.doesNotMatch(src, /filterNpcsForStorylineExport/);
|
|
});
|
|
|
|
void test('WindowErrorBoundary component exists and catches errors', () => {
|
|
const src = fs.readFileSync(path.join(here, 'WindowErrorBoundary.tsx'), 'utf8');
|
|
assert.ok(src.includes('getDerivedStateFromError'));
|
|
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'));
|
|
assert.ok(control.includes('ScenePlayerTokensOverlay'));
|
|
assert.ok(presentation.includes('ScenePlayerTokensOverlay'));
|
|
assert.ok(control.includes('toggle-session-players'));
|
|
});
|
|
|
|
void test('EditorApp: split Run button and launch-with-players modal', () => {
|
|
const editor = fs.readFileSync(path.join(rendererRoot, 'editor/EditorApp.tsx'), 'utf8');
|
|
assert.ok(editor.includes('splitRun'));
|
|
assert.ok(editor.includes('run-menu-btn'));
|
|
assert.ok(editor.includes('LaunchPlayersModal'));
|
|
assert.ok(editor.includes('top.runWithPlayers'));
|
|
});
|
|
|
|
void test('EditorApp: Playlists header and modal', () => {
|
|
const src = fs.readFileSync(path.join(rendererRoot, 'editor/EditorApp.tsx'), 'utf8');
|
|
assert.ok(src.includes('data-testid="playlists-header-btn"'));
|
|
assert.ok(src.includes('PlaylistsManagerModal'));
|
|
assert.ok(src.includes('setPlaylistsManagerOpen(true)'));
|
|
assert.ok(src.includes('setPlaylistsManagerOpen(false)'));
|
|
});
|
|
|
|
void test('ControlApp: Playlists tool opens window', () => {
|
|
const src = fs.readFileSync(path.join(rendererRoot, 'control/ControlApp.tsx'), 'utf8');
|
|
assert.ok(src.includes('data-testid="control-playlists-tool"'));
|
|
assert.ok(src.includes('windows.openPlaylists'));
|
|
});
|
|
|
|
void test('ControlApp: Timer tool opens window', () => {
|
|
const src = fs.readFileSync(path.join(rendererRoot, 'control/ControlApp.tsx'), 'utf8');
|
|
assert.ok(src.includes('data-testid="control-timer-tool"'));
|
|
assert.ok(src.includes('windows.openTimer'));
|
|
assert.ok(src.includes('SessionTimerLayer'));
|
|
});
|
|
|
|
void test('ControlApp: Initiative tool opens window', () => {
|
|
const src = fs.readFileSync(path.join(rendererRoot, 'control/ControlApp.tsx'), 'utf8');
|
|
assert.ok(src.includes('data-testid="control-initiative-tool"'));
|
|
assert.ok(src.includes('windows.openInitiative'));
|
|
});
|
|
|
|
void test('PlayerTokenView: name plate shows full text without ellipsis', () => {
|
|
const css = fs.readFileSync(
|
|
path.join(rendererRoot, 'shared/playerToken/PlayerTokenView.module.css'),
|
|
'utf8',
|
|
);
|
|
const block = css.match(/\.namePlate\s*\{[^}]+\}/)?.[0] ?? '';
|
|
assert.match(block, /width:\s*max-content/);
|
|
assert.doesNotMatch(block, /text-overflow:\s*ellipsis/);
|
|
assert.doesNotMatch(block, /overflow:\s*hidden/);
|
|
});
|
|
|
|
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('disposition') || src.includes('updateNpcFields'));
|
|
assert.ok(src.includes('avatarPickHint'));
|
|
assert.ok(src.includes('useFileDropZone'));
|
|
assert.ok(!src.includes("t('npcs.chooseAvatar')"));
|
|
assert.ok(src.includes('externalUrl'));
|
|
});
|
|
|
|
void test('NpcsApp: inspect cards vs Show checkbox, webview for external link', () => {
|
|
const src = fs.readFileSync(path.join(rendererRoot, 'npcs/NpcsApp.tsx'), 'utf8');
|
|
assert.ok(src.includes("kind: 'inspect.toggle'"));
|
|
assert.ok(src.includes('onActivate()'));
|
|
assert.ok(src.includes("kind: 'toggle'"));
|
|
assert.ok(src.includes('npcs.showOnScreen'));
|
|
assert.ok(src.includes('NpcExternalWebview') || src.includes('webview'));
|
|
assert.ok(src.includes('npcs.tabExternalLink'));
|
|
});
|