Compare commits
2
Commits
03830cd15e
...
5ebc9fc27c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ebc9fc27c | ||
|
|
99eafaf02d |
@@ -123,9 +123,12 @@ export function FloatingObservationWindow({
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [boundsRef]);
|
}, [boundsRef]);
|
||||||
|
|
||||||
const initialRect = bounds ? initialObservationWindowRect(index, count, bounds) : null;
|
// Camera media admission must not wait for the viewport observer. A source
|
||||||
|
// can arrive in the same commit as a workspace transition; mounting it with
|
||||||
|
// the canonical fallback geometry keeps the browser transport alive until
|
||||||
|
// the real bounds are measured immediately afterward.
|
||||||
|
const initialRect = initialObservationWindowRect(index, count, bounds ?? undefined);
|
||||||
const windowRect = rect ?? initialRect;
|
const windowRect = rect ?? initialRect;
|
||||||
if (!windowRect) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceWindow
|
<WorkspaceWindow
|
||||||
|
|||||||
@@ -6788,6 +6788,38 @@ test("read-only reconnect keeps its card and loader across a transient projectio
|
|||||||
)),
|
)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const runningVerificationState = structuredClone(initialState);
|
||||||
|
runningVerificationState.connection_attempt = {
|
||||||
|
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||||||
|
attempt_id: "verify-saved-k1-running",
|
||||||
|
connection_mode: "bridge",
|
||||||
|
status: "running",
|
||||||
|
phase: "network_applied",
|
||||||
|
control_state: "unknown",
|
||||||
|
stage: "host-route-and-control-endpoint",
|
||||||
|
public_error_code: null,
|
||||||
|
side_effect_status: "network-applied",
|
||||||
|
safe_next_action: "wait-for-current-attempt",
|
||||||
|
automatic_retry: false,
|
||||||
|
accepted_at: OBSERVED_AT,
|
||||||
|
completed_at: null,
|
||||||
|
timeline: [],
|
||||||
|
};
|
||||||
|
currentState = runningVerificationState;
|
||||||
|
tree = harness.render(props());
|
||||||
|
const runningMarkup = renderToStaticMarkup(tree);
|
||||||
|
const runningReconnect = buttonMarkupWithText(
|
||||||
|
runningMarkup,
|
||||||
|
"Переподключение…",
|
||||||
|
);
|
||||||
|
assert.equal(runningReconnect.length, 1);
|
||||||
|
assert.match(runningReconnect[0], /disabled/);
|
||||||
|
assert.match(runningReconnect[0], /aria-busy="true"/);
|
||||||
|
assert.match(runningReconnect[0], /nodedc-activity-indicator/);
|
||||||
|
assert.match(runningMarkup, /Сохранённое подключение/);
|
||||||
|
assert.doesNotMatch(runningMarkup, /Команда настройки завершена/);
|
||||||
|
assert.doesNotMatch(runningMarkup, /Подтверждение управления/);
|
||||||
|
|
||||||
const transientState = structuredClone(initialState);
|
const transientState = structuredClone(initialState);
|
||||||
transientState.connection_policy.allowed_actions =
|
transientState.connection_policy.allowed_actions =
|
||||||
transientState.connection_policy.allowed_actions.filter(
|
transientState.connection_policy.allowed_actions.filter(
|
||||||
@@ -7461,7 +7493,11 @@ test("applied-network recovery spends the old intent and admits only explicit se
|
|||||||
assert.match(source, /BLE-команда не повторяется/);
|
assert.match(source, /BLE-команда не повторяется/);
|
||||||
assert.match(
|
assert.match(
|
||||||
source,
|
source,
|
||||||
/status=\{\s*searchActive[\s\S]*?: presentedPhysicalRecoveryRequired[\s\S]*?: networkRecoveryRequired[\s\S]*?>\s*\{searchActive \? \([\s\S]*?Поиск Bluetooth · \{scanSecondsRemaining \?\? 6\} с[\s\S]*?: networkRecoveryRequired && !connectionAttemptSettling \? \(/,
|
/status=\{\s*searchActive[\s\S]*?: presentedPhysicalRecoveryRequired[\s\S]*?: presentedConnectionRecoveryRequired[\s\S]*?: networkRecoveryRequired/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
source,
|
||||||
|
/networkRecoveryRequired\s*&& !connectionAttemptSettling\s*&& !connectionRecoveryVerificationPending \? \(/,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ let cameraTransportCallbackIsCurrent;
|
|||||||
let cameraBrowserTransportIdentity;
|
let cameraBrowserTransportIdentity;
|
||||||
let liveRerunRecoveryAuthorityIdentity;
|
let liveRerunRecoveryAuthorityIdentity;
|
||||||
let initialObservationWindowRect;
|
let initialObservationWindowRect;
|
||||||
|
let FloatingObservationWindow;
|
||||||
let ObservationTimeline;
|
let ObservationTimeline;
|
||||||
let shouldCaptureWorkspacePointer;
|
let shouldCaptureWorkspacePointer;
|
||||||
let normalizeTimelineRange;
|
let normalizeTimelineRange;
|
||||||
@@ -86,9 +87,11 @@ before(async () => {
|
|||||||
} = await server.ssrLoadModule(
|
} = await server.ssrLoadModule(
|
||||||
"/src/core/observation/liveCameraRecovery.ts",
|
"/src/core/observation/liveCameraRecovery.ts",
|
||||||
));
|
));
|
||||||
({ initialObservationWindowRect, shouldCaptureWorkspacePointer } = await server.ssrLoadModule(
|
({
|
||||||
"/src/components/FloatingObservationWindow.tsx",
|
FloatingObservationWindow,
|
||||||
));
|
initialObservationWindowRect,
|
||||||
|
shouldCaptureWorkspacePointer,
|
||||||
|
} = await server.ssrLoadModule("/src/components/FloatingObservationWindow.tsx"));
|
||||||
({ liveRerunRecoveryAuthorityIdentity } = await server.ssrLoadModule(
|
({ liveRerunRecoveryAuthorityIdentity } = await server.ssrLoadModule(
|
||||||
"/src/core/observation/liveReceiverWatchdog.ts",
|
"/src/core/observation/liveReceiverWatchdog.ts",
|
||||||
));
|
));
|
||||||
@@ -133,6 +136,59 @@ test("observation camera windows tile from the bottom-right above the live timel
|
|||||||
assert.ok(right.y + right.height <= bounds.height - 64);
|
assert.ok(right.y + right.height <= bounds.height - 64);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("live camera window keeps its player mounted before viewport measurement", () => {
|
||||||
|
const source = {
|
||||||
|
id: "k1:sensor.camera.right",
|
||||||
|
sourceId: "sensor.camera.right",
|
||||||
|
label: "Правая камера",
|
||||||
|
description: "Live camera",
|
||||||
|
modality: "video",
|
||||||
|
availability: "streaming",
|
||||||
|
transport: "websocket",
|
||||||
|
endpointLabel: "Локальный видеопоток",
|
||||||
|
previewUrl: null,
|
||||||
|
delivery: {
|
||||||
|
id: "camera-preview-4",
|
||||||
|
kind: "mse-fmp4-websocket",
|
||||||
|
url: "/camera-preview/4",
|
||||||
|
mediaType: 'video/mp4; codecs="avc1.641028"',
|
||||||
|
},
|
||||||
|
activation: {
|
||||||
|
groupId: "k1:device-session:camera.preview.decoder",
|
||||||
|
maxActive: 1,
|
||||||
|
selected: true,
|
||||||
|
controllable: false,
|
||||||
|
},
|
||||||
|
binding: {
|
||||||
|
deviceId: "k1-a",
|
||||||
|
deviceSessionId: "device-session",
|
||||||
|
acquisitionId: "acquisition-quick",
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
defaultVisible: true,
|
||||||
|
overlay: true,
|
||||||
|
resizable: true,
|
||||||
|
timelineMode: "live-only",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const noop = () => undefined;
|
||||||
|
const markup = renderToStaticMarkup(createElement(FloatingObservationWindow, {
|
||||||
|
source,
|
||||||
|
index: 0,
|
||||||
|
count: 1,
|
||||||
|
boundsRef: { current: null },
|
||||||
|
maximized: false,
|
||||||
|
active: true,
|
||||||
|
onRectChange: noop,
|
||||||
|
onMaximizedChange: noop,
|
||||||
|
onActivate: noop,
|
||||||
|
onClose: noop,
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.match(markup, /<video/);
|
||||||
|
assert.match(markup, /Правая камера/);
|
||||||
|
});
|
||||||
|
|
||||||
test("observation camera windows stack without overlap when the viewport is narrow", () => {
|
test("observation camera windows stack without overlap when the viewport is narrow", () => {
|
||||||
const bounds = { width: 420, height: 700 };
|
const bounds = { width: 420, height: 700 };
|
||||||
const lower = initialObservationWindowRect(0, 2, bounds);
|
const lower = initialObservationWindowRect(0, 2, bounds);
|
||||||
|
|||||||
@@ -3265,16 +3265,16 @@ export function K1ProvisioningPipeline({
|
|||||||
: savedBridgeNetworkSetupRequired
|
: savedBridgeNetworkSetupRequired
|
||||||
? "Нужна настройка сети"
|
? "Нужна настройка сети"
|
||||||
: "Нужно выбрать действие"
|
: "Нужно выбрать действие"
|
||||||
: networkRecoveryRequired
|
|
||||||
? appliedControlSettlementPending
|
|
||||||
? "Подтверждение управления"
|
|
||||||
: "Управление не подтверждено · выбор заблокирован"
|
|
||||||
: presentedConnectionRecoveryRequired
|
: presentedConnectionRecoveryRequired
|
||||||
? connectionReconnectPending
|
? connectionReconnectPending
|
||||||
? "Переподключение…"
|
? "Переподключение…"
|
||||||
: connectionRecoveryTarget
|
: connectionRecoveryTarget
|
||||||
? "Можно переподключиться"
|
? "Можно переподключиться"
|
||||||
: "Подключить заново"
|
: "Подключить заново"
|
||||||
|
: networkRecoveryRequired
|
||||||
|
? appliedControlSettlementPending
|
||||||
|
? "Подтверждение управления"
|
||||||
|
: "Управление не подтверждено · выбор заблокирован"
|
||||||
: connectionEstablished
|
: connectionEstablished
|
||||||
? "Подключение установлено"
|
? "Подключение установлено"
|
||||||
: explicitProvisioningDraftStale
|
: explicitProvisioningDraftStale
|
||||||
@@ -3319,7 +3319,9 @@ export function K1ProvisioningPipeline({
|
|||||||
Поиск Bluetooth · {scanSecondsRemaining ?? 6} с
|
Поиск Bluetooth · {scanSecondsRemaining ?? 6} с
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
) : networkRecoveryRequired && !connectionAttemptSettling ? (
|
) : networkRecoveryRequired
|
||||||
|
&& !connectionAttemptSettling
|
||||||
|
&& !connectionRecoveryVerificationPending ? (
|
||||||
<div className="connection-summary" role="status">
|
<div className="connection-summary" role="status">
|
||||||
<span>
|
<span>
|
||||||
{appliedControlSettlementPending
|
{appliedControlSettlementPending
|
||||||
|
|||||||
Reference in New Issue
Block a user