import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { before, after, test } from 'node:test'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { createServer } from 'vite'; let server, SpatialScene; before(async () => { server = await createServer({ appType: 'custom', logLevel: 'silent', server: { middlewareMode: true } }); ({ SpatialScene } = await server.ssrLoadModule(new URL('../../../packages/spatial-ui/src/SpatialScene.tsx', import.meta.url).pathname)); }); after(async () => { await server?.close(); }); const render = (focused = false) => renderToStaticMarkup(createElement(SpatialScene, { viewportRef: { current: null }, primaryFocused: focused, toolbar: null, renderer: createElement('div', { 'data-testid': 'renderer' }, 'RENDERER'), sourceControls: createElement('div', { className: focused ? 'scene-focus-exit' : 'scene-source-controls' }, 'SOURCE_CONTROLS'), status: { label: 'Накопление данных', tone: 'neutral', message: 'Сканер неподвижен.' }, metrics: createElement('div', null, 'METRICS'), })); test('scene tools, visual engine and metrics share one top-left flow in that order', () => { const markup = render(); const stack = markup.indexOf('class="scene-information"'); const controls = markup.indexOf('SOURCE_CONTROLS'); const status = markup.indexOf('ВИЗУАЛЬНЫЙ ДВИЖОК'); const metrics = markup.indexOf('METRICS'); assert.ok(stack < controls && controls < status && status < metrics); assert.equal((markup.match(/SOURCE_CONTROLS/g) ?? []).length, 1); assert.doesNotMatch(markup, /scene-status--top-left/); }); test('focus exit remains viewport-owned outside the hidden information stack', () => { const markup = render(true); assert.ok(markup.indexOf('scene-focus-exit') < markup.indexOf('scene-information')); assert.match(markup, /class="scene-information" aria-hidden="true"/); assert.equal((markup.match(/SOURCE_CONTROLS/g) ?? []).length, 1); }); test('normal and expanded scenes retain the renderer without mouse-navigation copy', async () => { for (const focused of [false, true]) { const markup = render(focused); assert.match(markup, /data-testid="renderer">RENDERER/); assert.doesNotMatch(markup, /scene-navigation-hint|Навигация по 3D-сцене|ЛКМ|ПКМ|Колесо/); } const source = await readFile(new URL('../../../packages/spatial-ui/src/SpatialScene.tsx', import.meta.url), 'utf8'); const css = await readFile(new URL('../../../packages/spatial-ui/src/observation.css', import.meta.url), 'utf8'); assert.doesNotMatch(source, /navigationReady|scene-navigation-hint/); assert.doesNotMatch(css, /scene-navigation-hint/); }); test('scene layout uses flow, retains compact metrics and removes only the calibration perimeter', async () => { const css = await readFile(new URL('../../../packages/spatial-ui/src/spatial.css', import.meta.url), 'utf8'); const responsive = await readFile(new URL('../src/styles/responsive.css', import.meta.url), 'utf8'); const calibration = await readFile(new URL('../../../plugins/xgrids-k1/frontend/src/components/K1SpatialSession.css', import.meta.url), 'utf8'); assert.match(css, /\.scene-information \{[^}]*position: absolute;[^}]*display: grid;/); assert.match(css, /\.scene-information > \.scene-source-controls \{ position: static; pointer-events: auto;/); assert.match(css, /\.scene-information\[aria-hidden="true"\] \{ display: none;/); assert.doesNotMatch(css.match(/\.scene-metrics \{[^}]*\}/)?.[0] ?? '', /top:|right:|position: absolute/); assert.doesNotMatch(responsive, /\.scene-metrics\s*\{\s*display: none/); assert.match(calibration, /\.xgrids-k1-spatial-controls \{[^}]*border: 0;/); }); test('preparation status shares the source controls top axis on the opposite side', async () => { const css = await readFile(new URL('../../../packages/spatial-ui/src/spatial.css', import.meta.url), 'utf8'); const stack = css.match(/\.scene-operation-status-stack \{[^}]*\}/)?.[0] ?? ''; assert.match(stack, /top: 0\.85rem/); assert.match(stack, /right: 0\.85rem/); assert.match(stack, /min-height: 2\.75rem/); assert.match(stack, /align-content: center/); assert.doesNotMatch(stack, /left:|bottom:/); });