fix(observatory): refine catalog and replay UX
This commit is contained in:
@@ -886,8 +886,8 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
|
||||
assert.match(canonical, /primary=\{mediaPane\}/);
|
||||
assert.match(canonical, /secondary=\{spatialPane/);
|
||||
assert.match(canonical, /primarySize=\{splitView \? splitPrimarySize : mediaMode !== "none" \? 100 : 0\}/);
|
||||
assert.match(canonical, /resizable=\{splitView\}/);
|
||||
assert.match(canonical, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
|
||||
assert.match(canonical, /resizable=\{splitView && !unifiedContent\}/);
|
||||
assert.match(canonical, /separatorLabel="Изменить размер видео\/камеры и 3D\/плана"/);
|
||||
assert.match(canonical, /secondaryMode=\{\{/);
|
||||
assert.match(visual, /playback=\{playbackController\.playback\}/);
|
||||
assert.match(visual, /clock: "external"/);
|
||||
|
||||
@@ -4,8 +4,12 @@ import { after, before, test } from "node:test";
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let applyObservatoryCatalogMutationOverlay;
|
||||
let buildObservatoryCatalog;
|
||||
let fetchObservatoryCatalog;
|
||||
let findObservatoryEvidence;
|
||||
let observatoryCatalogConfirmsEvidenceDeletion;
|
||||
let reconcileObservatoryCatalogMutationOverlay;
|
||||
let ObservatoryCatalogContractError;
|
||||
|
||||
before(async () => {
|
||||
@@ -15,8 +19,12 @@ before(async () => {
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({
|
||||
applyObservatoryCatalogMutationOverlay,
|
||||
buildObservatoryCatalog,
|
||||
fetchObservatoryCatalog,
|
||||
findObservatoryEvidence,
|
||||
observatoryCatalogConfirmsEvidenceDeletion,
|
||||
reconcileObservatoryCatalogMutationOverlay,
|
||||
ObservatoryCatalogContractError,
|
||||
} = await server.ssrLoadModule("/src/core/observatory/catalog.ts"));
|
||||
});
|
||||
@@ -233,3 +241,134 @@ test("Observatory projects a typed canonical run only through its exact sourceSe
|
||||
activation: "explicit",
|
||||
});
|
||||
});
|
||||
|
||||
test("Observatory applies an exact rename locally without mutating canonical identity", () => {
|
||||
const catalog = buildObservatoryCatalog(
|
||||
[source("source", "2026-08-29T10:00:00Z")],
|
||||
[evidence("result", "source", "2026-08-29T11:00:00Z")],
|
||||
);
|
||||
const original = catalog.items[0].evidence[0];
|
||||
const projected = applyObservatoryCatalogMutationOverlay(
|
||||
catalog,
|
||||
new Map([["result", {
|
||||
kind: "rename",
|
||||
displayName: "Операторское имя",
|
||||
revision: 1,
|
||||
}]]),
|
||||
);
|
||||
const renamed = findObservatoryEvidence(projected, "result");
|
||||
|
||||
assert.equal(renamed.label, "Операторское имя");
|
||||
assert.equal(renamed.sessionId, original.sessionId);
|
||||
assert.equal(renamed.lab, original.lab);
|
||||
assert.equal(renamed.recordedRun, original.recordedRun);
|
||||
assert.equal(projected.items[0].source, catalog.items[0].source);
|
||||
assert.equal(projected.window.laboratoryCount, 1);
|
||||
});
|
||||
|
||||
test("Observatory tombstone removes only its catalog evidence projection", () => {
|
||||
const catalog = buildObservatoryCatalog(
|
||||
[source("source", "2026-08-29T10:00:00Z")],
|
||||
[
|
||||
evidence("remove", "source", "2026-08-29T12:00:00Z"),
|
||||
evidence("keep", "source", "2026-08-29T11:00:00Z"),
|
||||
],
|
||||
);
|
||||
const projected = applyObservatoryCatalogMutationOverlay(
|
||||
catalog,
|
||||
new Map([["remove", { kind: "delete", revision: 1 }]]),
|
||||
);
|
||||
|
||||
assert.equal(projected.items.length, 1);
|
||||
assert.equal(projected.items[0].source, catalog.items[0].source);
|
||||
assert.deepEqual(
|
||||
projected.items[0].evidence.map(({ sessionId }) => sessionId),
|
||||
["keep"],
|
||||
);
|
||||
assert.equal(projected.window.sourceCount, 1);
|
||||
assert.equal(projected.window.laboratoryCount, 1);
|
||||
});
|
||||
|
||||
test("Observatory confirms an ambiguous delete only from a complete LAB window", () => {
|
||||
const bounded = buildObservatoryCatalog(
|
||||
[source("source", "2026-08-29T10:00:00Z")],
|
||||
[evidence("other", "source", "2026-08-29T11:00:00Z")],
|
||||
1,
|
||||
);
|
||||
const complete = buildObservatoryCatalog(
|
||||
[source("source", "2026-08-29T10:00:00Z")],
|
||||
[],
|
||||
);
|
||||
|
||||
assert.equal(observatoryCatalogConfirmsEvidenceDeletion(bounded, "result"), false);
|
||||
assert.equal(observatoryCatalogConfirmsEvidenceDeletion(complete, "result"), true);
|
||||
});
|
||||
|
||||
test("Observatory reconciliation cannot let a stale in-flight fetch undo a local mutation", () => {
|
||||
const oldCatalog = buildObservatoryCatalog(
|
||||
[source("source", "2026-08-29T10:00:00Z")],
|
||||
[evidence("result", "source", "2026-08-29T11:00:00Z")],
|
||||
);
|
||||
const renameOverlay = new Map([["result", {
|
||||
kind: "rename",
|
||||
displayName: "Новое имя",
|
||||
revision: 1,
|
||||
}]]);
|
||||
|
||||
const staleRename = reconcileObservatoryCatalogMutationOverlay(
|
||||
oldCatalog,
|
||||
renameOverlay,
|
||||
0,
|
||||
);
|
||||
assert.equal(findObservatoryEvidence(staleRename.catalog, "result").label, "Новое имя");
|
||||
assert.equal(staleRename.overlay.has("result"), true);
|
||||
|
||||
const reconciledOldRename = reconcileObservatoryCatalogMutationOverlay(
|
||||
oldCatalog,
|
||||
renameOverlay,
|
||||
1,
|
||||
);
|
||||
assert.equal(findObservatoryEvidence(reconciledOldRename.catalog, "result").label, "Новое имя");
|
||||
assert.equal(reconciledOldRename.overlay.has("result"), true);
|
||||
|
||||
const renamedEvidence = evidence("result", "source", "2026-08-29T11:00:00Z");
|
||||
renamedEvidence.label = "Новое имя";
|
||||
const confirmedRename = reconcileObservatoryCatalogMutationOverlay(
|
||||
buildObservatoryCatalog(
|
||||
[source("source", "2026-08-29T10:00:00Z")],
|
||||
[renamedEvidence],
|
||||
),
|
||||
staleRename.overlay,
|
||||
1,
|
||||
);
|
||||
assert.equal(findObservatoryEvidence(confirmedRename.catalog, "result").label, "Новое имя");
|
||||
assert.equal(confirmedRename.overlay.size, 0);
|
||||
|
||||
const deleteOverlay = new Map([["result", { kind: "delete", revision: 2 }]]);
|
||||
const staleDelete = reconcileObservatoryCatalogMutationOverlay(
|
||||
oldCatalog,
|
||||
deleteOverlay,
|
||||
1,
|
||||
);
|
||||
assert.equal(findObservatoryEvidence(staleDelete.catalog, "result"), null);
|
||||
assert.equal(staleDelete.overlay.has("result"), true);
|
||||
|
||||
const reconciledOldDelete = reconcileObservatoryCatalogMutationOverlay(
|
||||
oldCatalog,
|
||||
deleteOverlay,
|
||||
2,
|
||||
);
|
||||
assert.equal(findObservatoryEvidence(reconciledOldDelete.catalog, "result"), null);
|
||||
assert.equal(reconciledOldDelete.overlay.has("result"), true);
|
||||
|
||||
const confirmedDelete = reconcileObservatoryCatalogMutationOverlay(
|
||||
buildObservatoryCatalog(
|
||||
[source("source", "2026-08-29T10:00:00Z")],
|
||||
[],
|
||||
),
|
||||
staleDelete.overlay,
|
||||
2,
|
||||
);
|
||||
assert.equal(findObservatoryEvidence(confirmedDelete.catalog, "result"), null);
|
||||
assert.equal(confirmedDelete.overlay.size, 0);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let deleteObservatoryLabProjection;
|
||||
let renameObservatoryLabProjection;
|
||||
let ObservatoryCatalogMutationError;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({
|
||||
deleteObservatoryLabProjection,
|
||||
renameObservatoryLabProjection,
|
||||
ObservatoryCatalogMutationError,
|
||||
} = await server.ssrLoadModule("/src/core/observatory/catalogMutations.ts"));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
const sessionId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
|
||||
const binding = {
|
||||
kind: "canonical-recorded-rerun",
|
||||
evidenceSessionId: sessionId,
|
||||
sourceSessionId: "20260828T130511Z_viewer_live",
|
||||
resultId: sessionId,
|
||||
viewerProfile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
};
|
||||
|
||||
test("Observatory rename sends the closed projection-only contract", async () => {
|
||||
const calls = [];
|
||||
const result = await renameObservatoryLabProjection(binding, " Новый разбор ", {
|
||||
fetcher: async (input, init) => {
|
||||
calls.push({ input, init });
|
||||
return Response.json({
|
||||
schema_version: "missioncore.observatory-lab-projection/v1",
|
||||
session_id: sessionId,
|
||||
display_name: "Новый разбор",
|
||||
});
|
||||
},
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
schemaVersion: "missioncore.observatory-lab-projection/v1",
|
||||
sessionId,
|
||||
displayName: "Новый разбор",
|
||||
});
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].input, `/api/v1/observatory/lab-projections/${sessionId}`);
|
||||
assert.equal(calls[0].init.method, "PATCH");
|
||||
assert.equal(calls[0].init.headers.Accept, "application/json");
|
||||
assert.equal(calls[0].init.headers["Content-Type"], "application/json");
|
||||
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
||||
schema_version: "missioncore.observatory-lab-projection-rename/v1",
|
||||
display_name: "Новый разбор",
|
||||
});
|
||||
});
|
||||
|
||||
test("Observatory delete accepts only an empty 204 response", async () => {
|
||||
const calls = [];
|
||||
await deleteObservatoryLabProjection(binding, {
|
||||
fetcher: async (input, init) => {
|
||||
calls.push({ input, init });
|
||||
return new Response(null, { status: 204 });
|
||||
},
|
||||
});
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].input, `/api/v1/observatory/lab-projections/${sessionId}`);
|
||||
assert.equal(calls[0].init.method, "DELETE");
|
||||
await assert.rejects(
|
||||
deleteObservatoryLabProjection(binding, {
|
||||
fetcher: async () => new Response("unexpected", { status: 200 }),
|
||||
}),
|
||||
ObservatoryCatalogMutationError,
|
||||
);
|
||||
});
|
||||
|
||||
test("Observatory mutations fail closed before network access", async () => {
|
||||
let calls = 0;
|
||||
const fetcher = async () => {
|
||||
calls += 1;
|
||||
return new Response(null, { status: 204 });
|
||||
};
|
||||
await assert.rejects(
|
||||
renameObservatoryLabProjection(binding, " ", { fetcher }),
|
||||
/от 1 до 160/,
|
||||
);
|
||||
await assert.rejects(
|
||||
deleteObservatoryLabProjection({ ...binding, resultId: "different" }, { fetcher }),
|
||||
/не допущен/,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
|
||||
test("Observatory mutations reject response drift and preserve API detail", async () => {
|
||||
await assert.rejects(
|
||||
renameObservatoryLabProjection(binding, "Разбор", {
|
||||
fetcher: async () => Response.json({
|
||||
schema_version: "missioncore.observatory-lab-projection/v1",
|
||||
session_id: sessionId,
|
||||
display_name: "Разбор",
|
||||
unexpected: true,
|
||||
}),
|
||||
}),
|
||||
/неизвестные поля/,
|
||||
);
|
||||
await assert.rejects(
|
||||
deleteObservatoryLabProjection(binding, {
|
||||
fetcher: async () => Response.json({ detail: "Проекция не принадлежит Обсерватории." }, { status: 409 }),
|
||||
}),
|
||||
(error) => error instanceof ObservatoryCatalogMutationError
|
||||
&& error.status === 409
|
||||
&& error.message === "Проекция не принадлежит Обсерватории.",
|
||||
);
|
||||
});
|
||||
@@ -81,12 +81,22 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
|
||||
assert.match(workspace, /вне текущего загруженного среза/);
|
||||
assert.match(workspace, /observatory-notice__copy/);
|
||||
assert.match(workspace, /observatory-evidence-card/);
|
||||
assert.match(workspace, /observatory-session-stack/);
|
||||
assert.match(workspace, /observatory-session-summary__facts/);
|
||||
assert.match(workspace, /observatory-evidence-card__copy/);
|
||||
assert.match(
|
||||
workspace,
|
||||
/evidence\.recordedRun \? \([\s\S]*name="trash"[\s\S]*name="edit"[\s\S]*Открыть визуальный разбор:[\s\S]*name="eye"/,
|
||||
);
|
||||
assert.doesNotMatch(workspace, /observatory-evidence-card[^>]*tone="soft"/);
|
||||
assert.match(workspace, /Открыть визуальный разбор/);
|
||||
assert.match(workspace, /replay\.kind === "ready"[\s\S]*<CanonicalVegetationRerunReplay/);
|
||||
assert.match(workspace, /Проверяем точную связь результата/);
|
||||
assert.match(workspace, /role="alert"/);
|
||||
assert.match(workspace, /Повторить/);
|
||||
assert.match(workspace, /Закрыть разбор/);
|
||||
assert.match(workspace, /<h3>\{replayEvidence\?\.label \?\? replay\.binding\.resultId\}<\/h3>/);
|
||||
assert.doesNotMatch(workspace, /RAVNOVES004TREE · полный маршрут восприятия/);
|
||||
assert.match(workspace, /const selectSession[\s\S]*closeReplay\(\);[\s\S]*setSelectedSessionId/);
|
||||
assert.match(workspace, /data-observatory-authority="observation-only"/);
|
||||
assert.doesNotMatch(workspace, /Нарушена связь|compactIdentity/);
|
||||
@@ -112,5 +122,70 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
|
||||
styles,
|
||||
/@media \(max-width: 920px\)[\s\S]*\.observatory-catalog-bar__controls \.nodedc-select-anchor \{[\s\S]*flex: 0 0 auto;/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/\.observatory-session-summary \{[\s\S]*grid-template-columns:[\s\S]*\.observatory-evidence-card \{[\s\S]*background: var\(--nodedc-glass-control-bg\);/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/\.observatory-session-stack \{[\s\S]*container-name: observatory-session;[\s\S]*container-type: inline-size;/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/@container observatory-session \(max-width: 56rem\) \{[\s\S]*\.observatory-session-summary \{[\s\S]*grid-template-columns: minmax\(0, 1fr\);/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/@container observatory-session \(max-width: 38rem\) \{[\s\S]*\.observatory-session-summary__facts \{[\s\S]*grid-template-columns: minmax\(0, 1fr\);/,
|
||||
);
|
||||
assert.doesNotMatch(styles, /\.observatory-evidence-card[\s\S]*background:\s*(?:#0{3,6}|black|rgb\(0[ ,])/i);
|
||||
assert.doesNotMatch(styles, /nodedc-glass-surface|nodedc-status-badge/);
|
||||
});
|
||||
|
||||
test("Observatory rename and delete use admitted projection mutations and canonical windows", async () => {
|
||||
const [workspace, hook] = await Promise.all([
|
||||
read("workspaces/observatory/ObservatoryWorkspace.tsx"),
|
||||
read("core/observatory/useObservatoryCatalog.ts"),
|
||||
]);
|
||||
|
||||
assert.match(
|
||||
workspace,
|
||||
/deleteObservatoryLabProjection,[\s\S]*renameObservatoryLabProjection,[\s\S]*from "\.\.\/\.\.\/core\/observatory\/catalogMutations"/,
|
||||
);
|
||||
assert.match(workspace, /<Window[\s\S]*title="Переименовать лабораторный результат"/);
|
||||
assert.match(workspace, /<TextField[\s\S]*label="Название"[\s\S]*maxLength=\{160\}/);
|
||||
assert.match(workspace, /<WindowFooterActions>[\s\S]*Сохранить/);
|
||||
assert.match(workspace, /<ConfirmationModal[\s\S]*title="Удалить результат из Обсерватории\?"/);
|
||||
assert.match(workspace, /Исходная сессия, запечатанный лабораторный результат и файлы доказательств/);
|
||||
assert.match(
|
||||
workspace,
|
||||
/const result = await renameObservatoryLabProjection\([\s\S]*controller\.applyEvidenceRename\(result\.sessionId, result\.displayName\);[\s\S]*setRenameTarget\(null\);[\s\S]*void controller\.refresh\(\);/,
|
||||
);
|
||||
assert.match(
|
||||
workspace,
|
||||
/catch \(error\) \{[\s\S]*const reconciled = await controller\.refresh\(\);[\s\S]*evidence\?\.label === reconciliation\.displayName[\s\S]*setRenameTarget\(null\);/,
|
||||
);
|
||||
|
||||
const deleteFlowStart = workspace.indexOf("const confirmDelete = useCallback");
|
||||
const deleteFlowEnd = workspace.indexOf("}, [closeReplay, controller", deleteFlowStart);
|
||||
assert.ok(deleteFlowStart >= 0 && deleteFlowEnd > deleteFlowStart);
|
||||
const deleteFlow = workspace.slice(deleteFlowStart, deleteFlowEnd);
|
||||
const teardown = deleteFlow.indexOf("closeReplay();");
|
||||
const remove = deleteFlow.indexOf("await deleteObservatoryLabProjection");
|
||||
const tombstone = deleteFlow.indexOf("controller.applyEvidenceDeletion");
|
||||
const refresh = deleteFlow.indexOf("void controller.refresh();");
|
||||
assert.ok(teardown >= 0 && teardown < remove, "active replay must unmount before projection delete");
|
||||
assert.ok(remove < tombstone, "local tombstone must follow the exact empty 204");
|
||||
assert.ok(tombstone < refresh, "reconciliation refresh must follow the local tombstone");
|
||||
assert.match(deleteFlow, /flushSync\(\(\) => \{[\s\S]*closeReplay\(\);[\s\S]*\}\);/);
|
||||
assert.match(
|
||||
deleteFlow,
|
||||
/catch \(error\) \{[\s\S]*const reconciled = await controller\.refresh\(\);[\s\S]*observatoryCatalogConfirmsEvidenceDeletion\([\s\S]*setDeleteTarget\(null\);/,
|
||||
);
|
||||
assert.match(workspace, /mutationError[\s\S]*role="alert"/);
|
||||
assert.match(hook, /activeRequestRef\.current\?\.controller\.abort\(\)/);
|
||||
assert.match(hook, /requestSequenceRef\.current !== id/);
|
||||
assert.match(hook, /const requestMutationRevision = mutationRevisionRef\.current/);
|
||||
assert.match(hook, /reconcileObservatoryCatalogMutationOverlay\(/);
|
||||
assert.match(hook, /applyObservatoryCatalogMutationOverlay\(/);
|
||||
});
|
||||
|
||||
@@ -452,7 +452,7 @@ test("canonical recorded LAB spatial frame keeps source, SLAM and body identity
|
||||
});
|
||||
|
||||
test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival review separate", async () => {
|
||||
const [resultSource, benchmarkSource, m49Source, canonicalSource, rerunSource] = await Promise.all([
|
||||
const [resultSource, benchmarkSource, m49Source, canonicalSource, rerunSource, replayStyles] = await Promise.all([
|
||||
readFile(
|
||||
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
|
||||
"utf8",
|
||||
@@ -473,6 +473,10 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
|
||||
new URL("../src/components/laboratory/CanonicalVegetationRerunReplay.tsx", import.meta.url),
|
||||
"utf8",
|
||||
),
|
||||
readFile(
|
||||
new URL("../src/styles/m4-replay-threat.css", import.meta.url),
|
||||
"utf8",
|
||||
),
|
||||
]);
|
||||
assert.doesNotMatch(resultSource, /M48MaskComparisonVisual/);
|
||||
assert.match(resultSource, /M49TgsFullShadowEvidence/);
|
||||
@@ -493,6 +497,34 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
|
||||
assert.match(rerunSource, /resolveCanonicalLabReplay/);
|
||||
assert.doesNotMatch(rerunSource, /canonical-overlay\.rrd/);
|
||||
assert.match(rerunSource, /unifiedPerception: splitView/);
|
||||
assert.match(rerunSource, /lockPerceptionCameraInteraction: mediaMode !== null/);
|
||||
assert.match(rerunSource, /!splitView[\s\S]*event\.button !== 0/);
|
||||
assert.match(rerunSource, /if \(!splitView\) stopNativeSplitTracking\(\)/);
|
||||
assert.match(rerunSource, /event\.target instanceof HTMLCanvasElement/);
|
||||
assert.match(rerunSource, /--canonical-rerun-camera-pane/);
|
||||
assert.match(rerunSource, /onPointerDownCapture=\{splitView \? trackNativeSplit : undefined\}/);
|
||||
assert.match(rerunSource, /data-split-view=\{splitView \? "true" : undefined\}/);
|
||||
assert.match(rerunSource, /mediaMode === null[\s\S]*\? 0[\s\S]*: splitView[\s\S]*\? nativeSplitPercentRef\.current[\s\S]*: 100/);
|
||||
assert.match(rerunSource, /isRecordedPlaybackPresentationReady\(viewerStatus, playback\)/);
|
||||
assert.match(rerunSource, /data-presentation-state=\{presentationState\}/);
|
||||
assert.match(rerunSource, /<ActivityIndicator label="Загружаем синхронизированную запись"/);
|
||||
assert.match(rerunSource, /presentationReady && playback && playbackController/);
|
||||
assert.match(
|
||||
replayStyles,
|
||||
/canonical-vegetation-rerun-replay:not\(\[data-presentation-state="ready"\]\)[\s\S]*laboratory-evidence-viewer__controls/,
|
||||
);
|
||||
assert.match(
|
||||
replayStyles,
|
||||
/canonical-vegetation-rerun-replay__timeline \.observation-timeline__playback \{[\s\S]*grid-template-columns: auto auto minmax\(0, 1fr\) auto;/,
|
||||
);
|
||||
assert.match(
|
||||
replayStyles,
|
||||
/canonical-vegetation-rerun-replay__viewport-lock[\s\S]*rerun-viewport__camera-lock \{[\s\S]*width: var\(--canonical-rerun-camera-pane, 100%\);/,
|
||||
);
|
||||
assert.match(
|
||||
replayStyles,
|
||||
/canonical-vegetation-rerun-replay__viewport-lock\[data-split-view="true"\][\s\S]*width: calc\(var\(--canonical-rerun-camera-pane, 46%\) - 0\.75rem\);/,
|
||||
);
|
||||
assert.doesNotMatch(rerunSource, /LaboratoryRecordedClipPlayer|LaboratoryMetricEvidenceScene/);
|
||||
assert.match(resultSource, /point-aligned 3D semantics пока не запечатаны/);
|
||||
assert.match(rerunSource, /value: "tgs", label: "TGS", disabled: true/);
|
||||
@@ -500,6 +532,7 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
|
||||
assert.match(canonicalSource, /secondary=\{spatialPane/);
|
||||
assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/);
|
||||
assert.match(canonicalSource, /separatorLabel="Изменить размер видео\/камеры и 3D\/плана"/);
|
||||
assert.match(canonicalSource, /resizable=\{splitView && !unifiedContent\}/);
|
||||
assert.match(resultSource, /linked canonical M4\.9 TGS evidence/);
|
||||
assert.match(resultSource, /linkedTgsResultId/);
|
||||
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
||||
|
||||
Reference in New Issue
Block a user