Files
DndGamePlayer/app/shared/graph/activeStorylineScenes.test.ts
T
Ivan Fontosh cfb6e62331 feat(control): аккордеон «Сцены» на пульте управления
Список сцен активной линии в порядке бокового списка редактора; переход с записью в историю.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 13:29:12 +08:00

126 lines
4.0 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import type { Project, SceneGraphEdge, SceneGraphNode } from '../types';
import { asGraphNodeId, asSceneId } from '../types/ids';
import { getActiveStorylineGraphNodeIds, listActiveStorylineScenes } from './activeStorylineScenes';
function node(id: string, sceneId: string, extra: Partial<SceneGraphNode> = {}): SceneGraphNode {
return {
id: asGraphNodeId(id),
sceneId: asSceneId(sceneId),
x: 0,
y: 0,
isStartScene: false,
isSideStoryStart: false,
sideStoryLineTitle: '',
...extra,
};
}
function edge(source: string, target: string): SceneGraphEdge {
return { sourceGraphNodeId: asGraphNodeId(source), targetGraphNodeId: asGraphNodeId(target) };
}
function minimalProject(partial: Partial<Project> & Pick<Project, 'sceneGraphNodes' | 'sceneGraphEdges'>): Project {
return {
id: partial.id ?? ('p1' as Project['id']),
title: partial.title ?? 'Test',
scenes: partial.scenes ?? {},
sceneListOrder: partial.sceneListOrder ?? [],
sceneGraphNodes: partial.sceneGraphNodes,
sceneGraphEdges: partial.sceneGraphEdges,
currentGraphNodeId: partial.currentGraphNodeId ?? null,
currentSceneId: partial.currentSceneId ?? null,
materials: partial.materials ?? [],
campaignAudio: partial.campaignAudio ?? [],
npcs: partial.npcs ?? [],
npcGroups: partial.npcGroups ?? [],
npcRelations: partial.npcRelations ?? [],
};
}
void test('listActiveStorylineScenes: main line ordered by sceneListOrder, deduped', () => {
const s1 = asSceneId('s1');
const s2 = asSceneId('s2');
const s3 = asSceneId('s3');
const nodes = [
node('main', 's1', { isStartScene: true }),
node('a', 's2'),
node('b', 's3'),
node('dup', 's2'),
];
const edges = [edge('main', 'a'), edge('a', 'b'), edge('a', 'dup')];
const project = minimalProject({
scenes: {
[s1]: { id: s1, title: 'Start' },
[s2]: { id: s2, title: 'Mid' },
[s3]: { id: s3, title: 'End' },
},
sceneListOrder: [s3, s1, s2],
sceneGraphNodes: nodes,
sceneGraphEdges: edges,
currentGraphNodeId: asGraphNodeId('a'),
});
const list = listActiveStorylineScenes(project);
assert.deepEqual(
list.map((e) => e.sceneId),
[s3, s1, s2],
);
assert.equal(list[2]?.graphNodeId, asGraphNodeId('a'));
});
void test('listActiveStorylineScenes: side line only', () => {
const s1 = asSceneId('s1');
const s2 = asSceneId('s2');
const sSide = asSceneId('sSide');
const nodes = [
node('main', 's1', { isStartScene: true }),
node('side', 'sSide', { isSideStoryStart: true }),
node('x', 's2'),
];
const edges = [edge('side', 'x')];
const project = minimalProject({
scenes: {
[s1]: { id: s1, title: 'Main' },
[s2]: { id: s2, title: 'Side step' },
[sSide]: { id: sSide, title: 'Side start' },
},
sceneListOrder: [s1, s2, sSide],
sceneGraphNodes: nodes,
sceneGraphEdges: edges,
currentGraphNodeId: asGraphNodeId('x'),
});
const ids = getActiveStorylineGraphNodeIds(nodes, edges, asGraphNodeId('x'));
assert.deepEqual([...ids].sort(), [asGraphNodeId('side'), asGraphNodeId('x')].sort());
const list = listActiveStorylineScenes(project);
assert.deepEqual(
list.map((e) => e.sceneId),
[s2, sSide],
);
});
void test('listActiveStorylineScenes: current node preferred for duplicate sceneId', () => {
const s2 = asSceneId('s2');
const nodes = [
node('main', 's1', { isStartScene: true }),
node('a', 's2'),
node('dup', 's2'),
];
const edges = [edge('main', 'a'), edge('main', 'dup')];
const project = minimalProject({
scenes: { [asSceneId('s1')]: { id: asSceneId('s1'), title: 'S1' }, [s2]: { id: s2, title: 'S2' } },
sceneListOrder: [asSceneId('s1'), s2],
sceneGraphNodes: nodes,
sceneGraphEdges: edges,
currentGraphNodeId: asGraphNodeId('dup'),
});
const list = listActiveStorylineScenes(project);
assert.equal(list.find((e) => e.sceneId === s2)?.graphNodeId, asGraphNodeId('dup'));
});