Files
DndGamePlayer/app/renderer/shared/sceneOverlay/sceneOverlayHost.test.ts
Ivan Fontosh 153be98780 fix(materials): синхронизация легенды и маркеров на превью и презентации
Легенда и цифры на материале масштабируются от экрана игроков и не обрезаются жёлтой рамкой. Переключение сцен без записи project.json на диск.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 10:57:50 +08:00

129 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 root = path.resolve(here, '..', '..', '..', '..');
void test('SceneOverlayHost: один dim для materials + npcs в Control и Presentation', () => {
const host = fs.readFileSync(path.join(here, 'SceneOverlayHost.tsx'), 'utf8');
const control = fs.readFileSync(path.join(root, 'app/renderer/control/ControlApp.tsx'), 'utf8');
const presentation = fs.readFileSync(path.join(root, 'app/renderer/shared/PresentationView.tsx'), 'utf8');
const css = fs.readFileSync(
path.join(root, 'app/renderer/shared/materials/MaterialOverlay.module.css'),
'utf8',
);
assert.ok(host.includes('styles.dim'));
assert.ok(host.includes('hostHitThrough'));
// Dim только при active; жёлтая рамка без затемнения.
assert.match(host, /\{active \? <div className=\{styles\.dim\}/);
assert.ok(host.includes('showViewportGuide'));
assert.ok(control.includes('SceneOverlayHost'));
assert.ok(control.includes('embedded'));
assert.ok(presentation.includes('SceneOverlayHost'));
assert.ok(presentation.includes('embedded'));
// Control/Presentation монтируют оверлеи только как embedded внутри host.
assert.ok(control.includes('<MaterialOverlay'));
assert.ok(control.includes('<NpcsSceneOverlay'));
assert.ok(control.includes('embedded'));
assert.ok(presentation.includes('embedded'));
assert.match(css, /\.hostHitThrough\s*\{[^}]*pointer-events:\s*none/s);
assert.match(css, /\.dim\s*\{[^}]*inset:\s*0/s);
});
void test('MaterialOverlay / NpcsSceneOverlay поддерживают embedded без собственного dim', () => {
const material = fs.readFileSync(
path.join(root, 'app/renderer/shared/materials/MaterialOverlay.tsx'),
'utf8',
);
const npcs = fs.readFileSync(path.join(root, 'app/renderer/shared/npcs/NpcsSceneOverlay.tsx'), 'utf8');
assert.ok(material.includes('embedded'));
assert.ok(material.includes('useSceneOverlayView'));
assert.ok(npcs.includes('embedded'));
assert.ok(npcs.includes('useSceneOverlayView'));
// В embedded-ветке не рисуем второй dim.
assert.match(material, /if \(embedded\) \{[\s\S]*createPortal/);
assert.match(npcs, /if \(embedded\) \{\s*return <>\{frames\}<\/>;/);
});
void test('overlays: layout IPC live через rAF coalesce, RO host coalesced', () => {
const host = fs.readFileSync(path.join(here, 'SceneOverlayHost.tsx'), 'utf8');
const material = fs.readFileSync(
path.join(root, 'app/renderer/shared/materials/MaterialOverlay.tsx'),
'utf8',
);
const npcs = fs.readFileSync(path.join(root, 'app/renderer/shared/npcs/NpcsSceneOverlay.tsx'), 'utf8');
assert.ok(host.includes('requestAnimationFrame'));
assert.ok(host.includes('prev.w === w && prev.h === h'));
// Лайв: publishDraft шлёт onLayoutChange внутри rAF (не на каждый pointermove).
assert.ok(material.includes('publishDraft'));
assert.ok(material.includes('onLayoutChangeRef'));
assert.match(material, /requestAnimationFrame\(\(\) => \{[\s\S]*?onLayoutChangeRef\.current\?\.\(pending\)/);
assert.match(
material,
/if \(drag\.mode === 'move'\) \{[\s\S]*?publishDraft\(\{[\s\S]*?return;\s*\}/,
);
assert.ok(npcs.includes('publishDraft'));
assert.ok(npcs.includes('onLayoutChangeRef'));
assert.match(
npcs,
/requestAnimationFrame\(\(\) => \{[\s\S]*?onLayoutChangeRef\.current\?\.\(item\.npcId, pending\)/,
);
assert.match(npcs, /if \(drag\.mode === 'move'\) \{[\s\S]*?publishDraft\(\{[\s\S]*?return;\s*\}/,
);
});
void test('SceneOverlayHost: presentation and control share player-screen viewport for overlays', () => {
const presentation = fs.readFileSync(path.join(root, 'app/renderer/shared/PresentationView.tsx'), 'utf8');
const control = fs.readFileSync(path.join(root, 'app/renderer/control/ControlApp.tsx'), 'utf8');
const host = fs.readFileSync(path.join(here, 'SceneOverlayHost.tsx'), 'utf8');
assert.ok(presentation.includes('<SceneOverlayHost'));
assert.doesNotMatch(presentation, /<SceneOverlayHost[\s\S]*?viewport=\{contentRect\}/);
assert.ok(control.includes('overlayViewport'));
assert.ok(control.includes('const overlayViewport = screenRect'));
assert.ok(control.includes('legendLayer'));
assert.ok(host.includes('legendLayer'));
});
void test('MaterialLegendPanel: uniform scale, no inner scroll', () => {
const panel = fs.readFileSync(
path.join(root, 'app/renderer/shared/materials/MaterialLegendPanel.tsx'),
'utf8',
);
const css = fs.readFileSync(
path.join(root, 'app/renderer/shared/materials/MaterialLegendPanel.module.css'),
'utf8',
);
assert.ok(panel.includes('legendPanelVisualLayout'));
assert.match(css, /overflow:\s*visible/);
assert.doesNotMatch(css, /overflow:\s*auto/);
assert.doesNotMatch(css, /max-height/);
});
void test('MaterialOverlay: legend markers use viewport positioning', () => {
const material = fs.readFileSync(
path.join(root, 'app/renderer/shared/materials/MaterialOverlay.tsx'),
'utf8',
);
const css = fs.readFileSync(
path.join(root, 'app/renderer/shared/materials/MaterialOverlay.module.css'),
'utf8',
);
const host = fs.readFileSync(path.join(here, 'SceneOverlayHost.tsx'), 'utf8');
assert.ok(material.includes('legendMarkerPositionInOverlayHost'));
assert.ok(material.includes('legendMarkerScreenScale'));
assert.ok(material.includes('createPortal'));
assert.ok(material.includes('markerLayerRef'));
assert.match(css, /object-fit:\s*contain/);
assert.ok(host.includes('legendLayer'));
assert.ok(host.includes('markerLayer'));
assert.ok(host.includes('hostViewport'));
});