153be98780
Легенда и цифры на материале масштабируются от экрана игроков и не обрезаются жёлтой рамкой. Переключение сцен без записи project.json на диск. Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
5.8 KiB
TypeScript
129 lines
5.8 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 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'));
|
||
});
|