cc50e64e21
Add Hostile/Neutral/Friendly ring types with session-only inactive overrides on control, plus launch-with-players flow and live player tokens on the map. Co-authored-by: Cursor <cursoragent@cursor.com>
206 lines
6.6 KiB
TypeScript
206 lines
6.6 KiB
TypeScript
import { test, expect, invokeInRenderer, stubOpenImageDialog } from './fixtures/electron';
|
|
|
|
type Project = {
|
|
id: string;
|
|
currentSceneId: string | null;
|
|
scenes: Record<
|
|
string,
|
|
{
|
|
tokens: unknown[];
|
|
npcTokens: Array<{ id: string; npcId: string; nx: number; ny: number; sizeN: number }>;
|
|
}
|
|
>;
|
|
npcs: Array<{ id: string; name: string }>;
|
|
};
|
|
|
|
async function seedProjectWithNpcAndPlacement(
|
|
page: import('@playwright/test').Page,
|
|
sampleImagePath: string,
|
|
) {
|
|
await invokeInRenderer<{ project: Project }>(page, 'project.create', {
|
|
name: `E2E Scene ${Date.now()}`,
|
|
});
|
|
|
|
const sceneId = `scene_e2e_${Date.now()}`;
|
|
await invokeInRenderer(page, 'project.updateScene', {
|
|
sceneId,
|
|
patch: {
|
|
title: 'Map',
|
|
description: '',
|
|
media: { videos: [], audios: [] },
|
|
settings: { autoplayVideo: false, autoplayAudio: false, loopVideo: false, loopAudio: false },
|
|
layout: { x: 0, y: 0 },
|
|
previewAssetId: null,
|
|
previewAssetType: null,
|
|
previewVideoAutostart: false,
|
|
npcTokens: [],
|
|
tokens: [],
|
|
traps: [],
|
|
},
|
|
});
|
|
await invokeInRenderer(page, 'project.setCurrentScene', { sceneId });
|
|
await invokeInRenderer(page, 'project.importScenePreview', {
|
|
sceneId,
|
|
filePath: sampleImagePath,
|
|
});
|
|
|
|
const npcUpsert = await invokeInRenderer<{ project: Project }>(page, 'project.upsertNpc', {
|
|
name: `Guard ${Date.now()}`,
|
|
filePath: sampleImagePath,
|
|
disposition: 'neutral',
|
|
imageOffset: { x: 0, y: 0 },
|
|
});
|
|
const npc = npcUpsert.project.npcs[npcUpsert.project.npcs.length - 1]!;
|
|
const placementId = `snt_e2e_${Date.now()}`;
|
|
|
|
await invokeInRenderer(page, 'project.updateScene', {
|
|
sceneId,
|
|
patch: {
|
|
npcTokens: [
|
|
{
|
|
id: placementId,
|
|
npcId: npc.id,
|
|
nx: 0.35,
|
|
ny: 0.4,
|
|
sizeN: 0.12,
|
|
disposition: 'neutral',
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const refreshed = await invokeInRenderer<{ project: Project }>(page, 'project.get', {});
|
|
return {
|
|
sceneId,
|
|
npcId: npc.id,
|
|
placementId,
|
|
project: refreshed.project,
|
|
};
|
|
}
|
|
|
|
test.describe('Scene NPC tokens', () => {
|
|
test('scene editor shows circular NPC token; clear removes both kinds', async ({
|
|
electronApp,
|
|
editorWindow,
|
|
sampleImagePath,
|
|
}) => {
|
|
await stubOpenImageDialog(electronApp, sampleImagePath);
|
|
const seeded = await seedProjectWithNpcAndPlacement(editorWindow, sampleImagePath);
|
|
|
|
const token = await invokeInRenderer<{ token: { id: string } }>(editorWindow, 'tokens.upsert', {
|
|
name: `Prop ${Date.now()}`,
|
|
filePath: sampleImagePath,
|
|
});
|
|
await invokeInRenderer(editorWindow, 'project.updateScene', {
|
|
sceneId: seeded.sceneId,
|
|
patch: {
|
|
tokens: [
|
|
{
|
|
id: `st_${Date.now()}`,
|
|
tokenId: token.token.id,
|
|
nx: 0.7,
|
|
ny: 0.7,
|
|
sizeN: 0.1,
|
|
rotationDeg: 0,
|
|
},
|
|
],
|
|
npcTokens: [
|
|
{
|
|
id: seeded.placementId,
|
|
npcId: seeded.npcId,
|
|
nx: 0.35,
|
|
ny: 0.4,
|
|
sizeN: 0.12,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const winPromise = electronApp.waitForEvent('window');
|
|
await invokeInRenderer(editorWindow, 'windows.openSceneEditor', {});
|
|
const sceneWin = await winPromise;
|
|
await sceneWin.waitForLoadState('domcontentloaded');
|
|
|
|
const accordion = sceneWin.getByTestId('scene-npc-accordion');
|
|
await expect(accordion).toBeVisible({ timeout: 30_000 });
|
|
// Expand if collapsed
|
|
await accordion.locator('button').first().click().catch(() => undefined);
|
|
await expect(sceneWin.getByTestId(`scene-npc-tile-${seeded.npcId}`)).toBeVisible({
|
|
timeout: 15_000,
|
|
});
|
|
await expect(sceneWin.getByTestId(`scene-npc-token-${seeded.placementId}`)).toBeVisible();
|
|
|
|
await sceneWin.getByTestId('scene-clear-btn').click();
|
|
await expect(sceneWin.getByTestId(`scene-npc-token-${seeded.placementId}`)).toHaveCount(0);
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const after = await invokeInRenderer<{ project: Project }>(editorWindow, 'project.get', {});
|
|
const scene = after.project.scenes[seeded.sceneId]!;
|
|
return { tokens: scene.tokens.length, npcTokens: scene.npcTokens.length };
|
|
},
|
|
{ timeout: 10_000 },
|
|
)
|
|
.toEqual({ tokens: 0, npcTokens: 0 });
|
|
|
|
await invokeInRenderer(editorWindow, 'windows.closeSceneEditor', {}).catch(() => undefined);
|
|
});
|
|
|
|
test('session move resets after re-open multiwindow', async ({
|
|
editorWindow,
|
|
sampleImagePath,
|
|
electronApp,
|
|
}) => {
|
|
await stubOpenImageDialog(electronApp, sampleImagePath);
|
|
const seeded = await seedProjectWithNpcAndPlacement(editorWindow, sampleImagePath);
|
|
|
|
// Need a graph start node for session; open multiwindow may still work with currentSceneId.
|
|
await invokeInRenderer(editorWindow, 'project.addSceneGraphNode', {
|
|
sceneId: seeded.sceneId,
|
|
x: 0,
|
|
y: 0,
|
|
});
|
|
const withNode = await invokeInRenderer<{ project: { sceneGraphNodes: Array<{ id: string }> } }>(
|
|
editorWindow,
|
|
'project.get',
|
|
{},
|
|
);
|
|
const nodeId = withNode.project.sceneGraphNodes[0]?.id;
|
|
if (nodeId) {
|
|
await invokeInRenderer(editorWindow, 'project.setSceneGraphNodeStart', {
|
|
graphNodeId: nodeId,
|
|
});
|
|
}
|
|
|
|
await invokeInRenderer(editorWindow, 'windows.openMultiWindow', {});
|
|
await editorWindow.waitForTimeout(1000);
|
|
|
|
await invokeInRenderer(editorWindow, 'sceneNpcTokensSession.dispatch', {
|
|
event: { kind: 'move', placementId: seeded.placementId, nx: 0.8, ny: 0.8 },
|
|
});
|
|
|
|
let session = await invokeInRenderer<{
|
|
state: { byPlacementId: Record<string, { nx: number; ny: number }> };
|
|
}>(editorWindow, 'sceneNpcTokensSession.getState', {});
|
|
expect(session.state.byPlacementId[seeded.placementId]?.nx).toBeCloseTo(0.8, 5);
|
|
|
|
await invokeInRenderer(editorWindow, 'windows.closeMultiWindow', {});
|
|
await editorWindow.waitForTimeout(500);
|
|
|
|
await invokeInRenderer(editorWindow, 'windows.openMultiWindow', {});
|
|
await editorWindow.waitForTimeout(500);
|
|
session = await invokeInRenderer(editorWindow, 'sceneNpcTokensSession.getState', {});
|
|
expect(session.state.byPlacementId[seeded.placementId]).toBeUndefined();
|
|
|
|
const proj = await invokeInRenderer<{ project: Project }>(editorWindow, 'project.get', {});
|
|
const placement = proj.project.scenes[seeded.sceneId]!.npcTokens.find(
|
|
(t) => t.id === seeded.placementId,
|
|
);
|
|
expect(placement?.nx).toBeCloseTo(0.35, 5);
|
|
expect(placement?.ny).toBeCloseTo(0.4, 5);
|
|
|
|
await invokeInRenderer(editorWindow, 'windows.closeMultiWindow', {}).catch(() => undefined);
|
|
});
|
|
});
|