107 lines
6.1 KiB
JavaScript
107 lines
6.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import React, {createElement} from 'react';
|
|
import {renderToStaticMarkup} from 'react-dom/server';
|
|
import {readFileSync} from 'node:fs';
|
|
import {before, after, test} from 'node:test';
|
|
import {createServer} from 'vite';
|
|
|
|
let server, resolve, awaitsCapture, Guard, Provider, Device, Host;
|
|
before(async () => {
|
|
server = await createServer({appType:'custom', logLevel:'silent', server:{middlewareMode:true}});
|
|
({workspaceLaunchProfile:resolve} = await server.ssrLoadModule('/src/core/observation/workspaceLaunch.ts'));
|
|
({planningAwaitsCapture:awaitsCapture, PlanningTestProvider:Provider} = await server.ssrLoadModule('/src/core/missions/PlanningTestContext.tsx'));
|
|
({PlanningCaptureGuard:Guard} = await server.ssrLoadModule('/src/components/missions/PlanningCaptureGuard.tsx'));
|
|
({DeviceWorkspace:Device} = await server.ssrLoadModule('/src/workspaces/DeviceWorkspace.tsx'));
|
|
({DevicePluginHostProvider:Host} = await server.ssrLoadModule('/src/core/device-plugins/DevicePluginHost.tsx'));
|
|
});
|
|
after(async () => { await server?.close(); });
|
|
|
|
test('direct entry overrides an earlier planning profile for both shared surfaces', () => {
|
|
for (const kind of ['device','spatial']) {
|
|
assert.equal(resolve('planning',kind),'direct');
|
|
assert.equal(resolve('direct',kind),'direct');
|
|
assert.equal(resolve('direct',kind,'planning'),'planning');
|
|
assert.equal(resolve('planning',kind,'planning'),'planning');
|
|
}
|
|
assert.equal(resolve('planning','missions'),'planning');
|
|
assert.equal(resolve('direct','missions'),'direct');
|
|
});
|
|
|
|
test('a prepared consumer cannot silently claim a direct capture; old/bound runs do not block it', () => {
|
|
assert.equal(awaitsCapture(null),false);
|
|
for (const state of ['completed','cancelled','interrupted','error']) {
|
|
for (const query_session_id of [null,'recording']) assert.equal(awaitsCapture({state,query_session_id}),false);
|
|
}
|
|
for (const state of ['preparing','waiting','running']) {
|
|
assert.equal(awaitsCapture({state,query_session_id:null}),true);
|
|
assert.equal(awaitsCapture({state,query_session_id:'recording'}),false);
|
|
}
|
|
});
|
|
|
|
function withHooks(hooks, fn) {
|
|
// Same bounded pre-effect harness used by observatoryHooks.test.mjs.
|
|
const internals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
const previous = internals.H;
|
|
internals.H = hooks;
|
|
try { return fn(); } finally { internals.H = previous; }
|
|
}
|
|
|
|
test('retained completed run renders the ordinary connection, without issuing any commands', () => {
|
|
let calls = 0;
|
|
const child = createElement('div',null,'ORDINARY DEVICE');
|
|
const tree = withHooks({useContext:()=>({test:{state:'completed',query_session_id:'old'},finish:()=>{calls++;}})},
|
|
() => Guard({enabled:true,onResume:()=>{calls++;},children:child}));
|
|
assert.match(renderToStaticMarkup(tree),/ORDINARY DEVICE/);
|
|
assert.equal(calls,0);
|
|
});
|
|
|
|
test('pending run handoff is explicit and only finishes the research, not the scanner', () => {
|
|
let stopped = 0, resumed = 0;
|
|
const context = {test:{state:'waiting',query_session_id:null,draft:{name:'PENDING'}},busy:false,error:null,finish:()=>{stopped++;}};
|
|
const child = createElement('div',null,'ORDINARY DEVICE');
|
|
const tree = withHooks({useContext:()=>context}, () => Guard({enabled:true,onResume:()=>{resumed++;},children:child}));
|
|
assert.doesNotMatch(renderToStaticMarkup(tree),/ORDINARY DEVICE/);
|
|
assert.equal(stopped,0);
|
|
const actions = tree.props.children.at(-1).props.children;
|
|
actions[0].props.onClick(); assert.equal(stopped,1); assert.equal(resumed,0);
|
|
actions[1].props.onClick(); assert.equal(resumed,1);
|
|
const planningTree = withHooks({useContext:()=>context}, () => Guard({enabled:false,onResume:()=>{},children:child}));
|
|
assert.match(renderToStaticMarkup(planningTree),/ORDINARY DEVICE/);
|
|
});
|
|
|
|
test('generic device catalog renders without any PlanningTestProvider', () => {
|
|
const html = renderToStaticMarkup(createElement(Host,{plugins:[]},
|
|
createElement(Device,{onOpenSpatialScene:()=>{},onActivateAutomaticSpatialSource:()=>{}})));
|
|
assert.match(html,/Выберите модель устройства/);
|
|
assert.doesNotMatch(html,/Профиль · Планирование|Подключение сканера · Планирование/);
|
|
});
|
|
|
|
test('failed planner selection does not grant a successful launch', async () => {
|
|
const writes = [];
|
|
const context = withHooks({
|
|
useState:initial=>[typeof initial==='function'?initial():initial,value=>writes.push(value)],
|
|
useRef:value=>({current:value}),useCallback:fn=>fn,useEffect:()=>{},
|
|
}, () => Provider({children:null}).props.value);
|
|
const originalFetch = globalThis.fetch;
|
|
try {
|
|
globalThis.fetch = async () => new Response(JSON.stringify({detail:'Выбранный проход недоступен'}),{status:409});
|
|
assert.equal(await context.select('missing'),false);
|
|
assert.ok(writes.includes('Выбранный проход недоступен'));
|
|
assert.equal('selected' in context,false);
|
|
assert.equal('resume' in context,false);
|
|
} finally { globalThis.fetch = originalFetch; }
|
|
});
|
|
|
|
test('composition owns explicit profile propagation; server polling cannot restore it', () => {
|
|
const source = path => readFileSync(new URL('../src/'+path,import.meta.url),'utf8');
|
|
assert.doesNotMatch(source('workspaces/DeviceWorkspace.tsx'),/Planning|missions\//);
|
|
assert.doesNotMatch(source('core/missions/PlanningTestContext.tsx'),/dismissed|sessionStorage|launchProfile/);
|
|
assert.match(source('App.tsx'),/useState<WorkspaceLaunchProfile>\('direct'\)/);
|
|
assert.match(source('App.tsx'),/openView\("spatial-scene", 'planning'\)/);
|
|
assert.match(source('App.tsx'),/openView\("spatial-scene", 'direct'\)/);
|
|
assert.match(source('workspaces/spatial/SpatialWorkspace.tsx'),/openView\("spatial-scene", launchProfile\)/);
|
|
assert.match(source('workspaces/Workspaces.tsx'),/props\.launchProfile === 'planning'/);
|
|
assert.match(source('workspaces/missions/MissionPlannerWorkspace.tsx'),/openView\('local-device','planning'\)/);
|
|
assert.match(source('workspaces/missions/MissionPlannerWorkspace.tsx'),/if\(await live.select\(project.id\)\)openView/);
|
|
});
|