refactor: move prepared run controls out of scene content
This commit is contained in:
@@ -1,156 +0,0 @@
|
|||||||
import { cleanup, render, screen } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
|
||||||
import type { TimelineAgentController, TimelineAgentMode } from "../demo/agent/timelineAgent.js";
|
|
||||||
import { initialDemoTimelineState } from "../demo/timeline/reducer.js";
|
|
||||||
import type { PresentationTargetHealth } from "./presentation-target-status.js";
|
|
||||||
import { DemoRunLaunchControl } from "./DemoRunLaunchControl.js";
|
|
||||||
|
|
||||||
const target = "http://127.0.0.1:8765/rpc";
|
|
||||||
|
|
||||||
const demoController = (
|
|
||||||
phase: DemoTimelineController["state"]["phase"] = "ready",
|
|
||||||
): DemoTimelineController => ({
|
|
||||||
state: { ...initialDemoTimelineState, phase },
|
|
||||||
inFlight: false,
|
|
||||||
interruptPayload: null,
|
|
||||||
output: null,
|
|
||||||
trace: null,
|
|
||||||
missingDeploymentMessage: null,
|
|
||||||
recordingId: null,
|
|
||||||
canStart: true,
|
|
||||||
setMode: vi.fn(),
|
|
||||||
start: vi.fn(),
|
|
||||||
pause: vi.fn(),
|
|
||||||
play: vi.fn(),
|
|
||||||
next: vi.fn(async () => {}),
|
|
||||||
submitSelectedIssues: vi.fn(async () => {}),
|
|
||||||
requestRevision: vi.fn(async () => {}),
|
|
||||||
restart: vi.fn(),
|
|
||||||
primeReplayToStage: vi.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const timelineAgent = (
|
|
||||||
runPreparedWorkflow: (mode?: TimelineAgentMode) => Promise<void>,
|
|
||||||
overrides: Partial<TimelineAgentController> = {},
|
|
||||||
): TimelineAgentController => ({
|
|
||||||
messages: [],
|
|
||||||
canRun: true,
|
|
||||||
canRunLive: true,
|
|
||||||
runLabel: "Run prepared workflow",
|
|
||||||
runPreparedWorkflow,
|
|
||||||
submitSelectedIssues: vi.fn(async () => {}),
|
|
||||||
requestRevision: vi.fn(async () => {}),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
const readyStatus: PresentationTargetHealth = {
|
|
||||||
kind: "ready",
|
|
||||||
target,
|
|
||||||
label: "Live target ready",
|
|
||||||
detail: "127.0.0.1:8765",
|
|
||||||
};
|
|
||||||
|
|
||||||
const replayStatus: PresentationTargetHealth = {
|
|
||||||
kind: "replay",
|
|
||||||
label: "Replay evidence",
|
|
||||||
detail: "reviewed recording",
|
|
||||||
};
|
|
||||||
|
|
||||||
const failedStatus: PresentationTargetHealth = {
|
|
||||||
kind: "failed",
|
|
||||||
target,
|
|
||||||
label: "Replay fallback",
|
|
||||||
detail: "connection refused",
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkingStatus: PresentationTargetHealth = {
|
|
||||||
kind: "checking",
|
|
||||||
target,
|
|
||||||
label: "Live target configured",
|
|
||||||
detail: "checking",
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderControl = (options: {
|
|
||||||
readonly status?: PresentationTargetHealth;
|
|
||||||
readonly liveTargetReady?: boolean;
|
|
||||||
readonly demo?: DemoTimelineController;
|
|
||||||
readonly agent?: TimelineAgentController;
|
|
||||||
} = {}) => render(
|
|
||||||
<DemoRunLaunchControl
|
|
||||||
status={options.status ?? readyStatus}
|
|
||||||
liveTargetReady={options.liveTargetReady ?? true}
|
|
||||||
demo={options.demo ?? demoController()}
|
|
||||||
timelineAgent={options.agent ?? timelineAgent(async () => {})}
|
|
||||||
retryHealth={vi.fn()}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
afterEach(() => cleanup());
|
|
||||||
|
|
||||||
describe("DemoRunLaunchControl", () => {
|
|
||||||
it("launches live from a healthy target", async () => {
|
|
||||||
const run = vi.fn(async () => {});
|
|
||||||
renderControl({ agent: timelineAgent(run) });
|
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Run prepared workflow" }));
|
|
||||||
|
|
||||||
expect(run).toHaveBeenCalledWith("live");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps the live action available from a direct replay view", async () => {
|
|
||||||
const run = vi.fn(async () => {});
|
|
||||||
renderControl({
|
|
||||||
status: replayStatus,
|
|
||||||
liveTargetReady: true,
|
|
||||||
agent: timelineAgent(run),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByText("Live target ready")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Direct view is replay; launch starts live operations.")).toBeInTheDocument();
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Run prepared workflow" }));
|
|
||||||
|
|
||||||
expect(run).toHaveBeenCalledWith("live");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("offers explicit replay and retry after health failure", async () => {
|
|
||||||
const run = vi.fn(async () => {});
|
|
||||||
const retry = vi.fn();
|
|
||||||
render(
|
|
||||||
<DemoRunLaunchControl
|
|
||||||
status={failedStatus}
|
|
||||||
liveTargetReady={false}
|
|
||||||
demo={demoController()}
|
|
||||||
timelineAgent={timelineAgent(run)}
|
|
||||||
retryHealth={retry}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Play replay walkthrough" }));
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Retry live service" }));
|
|
||||||
|
|
||||||
expect(run).toHaveBeenCalledWith("replay");
|
|
||||||
expect(retry).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not offer retry when no live target is configured", () => {
|
|
||||||
renderControl({ status: replayStatus, liveTargetReady: false });
|
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: "Play replay walkthrough" })).toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole("button", { name: "Retry live service" })).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps the action visible but disabled while health is checking", () => {
|
|
||||||
renderControl({ status: checkingStatus, liveTargetReady: false });
|
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: "Checking live service" })).toBeDisabled();
|
|
||||||
expect(screen.getByRole("button", { name: "Retry live service" })).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables duplicate starts while the live timeline is running", () => {
|
|
||||||
renderControl({ status: { ...readyStatus, kind: "active", label: "Live run active", detail: "operations sent" }, demo: demoController("running") });
|
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: "Live workflow running" })).toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
|
||||||
import type { TimelineAgentController } from "../demo/agent/timelineAgent.js";
|
|
||||||
import type { PresentationTargetHealth } from "./presentation-target-status.js";
|
|
||||||
|
|
||||||
export type DemoRunLaunchControlProps = {
|
|
||||||
readonly status: PresentationTargetHealth;
|
|
||||||
readonly liveTargetReady: boolean;
|
|
||||||
readonly demo: DemoTimelineController;
|
|
||||||
readonly timelineAgent: TimelineAgentController | undefined;
|
|
||||||
readonly retryHealth: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DemoRunLaunchControl = ({
|
|
||||||
status,
|
|
||||||
liveTargetReady,
|
|
||||||
demo,
|
|
||||||
timelineAgent,
|
|
||||||
retryHealth,
|
|
||||||
}: DemoRunLaunchControlProps) => {
|
|
||||||
if (!timelineAgent) return null;
|
|
||||||
|
|
||||||
const isChecking = status.kind === "checking";
|
|
||||||
const isRunning = demo.inFlight || demo.state.phase === "running" || status.kind === "active";
|
|
||||||
const launchLive = liveTargetReady;
|
|
||||||
const launchLabel = isChecking
|
|
||||||
? "Checking live service"
|
|
||||||
: isRunning && launchLive
|
|
||||||
? "Live workflow running"
|
|
||||||
: launchLive
|
|
||||||
? "Run prepared workflow"
|
|
||||||
: "Play replay walkthrough";
|
|
||||||
const canLaunch = launchLive ? timelineAgent.canRunLive : timelineAgent.canRun;
|
|
||||||
const canRetryLive = status.kind !== "replay" || liveTargetReady;
|
|
||||||
const statusLabel = liveTargetReady ? "Live target ready" : status.label;
|
|
||||||
const statusDetail = liveTargetReady && status.kind === "replay"
|
|
||||||
? "Direct view is replay; launch starts live operations."
|
|
||||||
: status.detail;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
className="demo-run-launch-control"
|
|
||||||
data-target-kind={status.kind}
|
|
||||||
data-launch-mode={launchLive ? "live" : "replay"}
|
|
||||||
aria-label="prepared workflow launch"
|
|
||||||
>
|
|
||||||
<div className="demo-run-launch-control__copy" role="status" aria-live="polite">
|
|
||||||
<span className="demo-run-launch-control__eyebrow">Prepared workflow</span>
|
|
||||||
<strong>{statusLabel}</strong>
|
|
||||||
<p>{statusDetail}</p>
|
|
||||||
</div>
|
|
||||||
<div className="demo-run-launch-control__actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="demo-run-launch-control__primary"
|
|
||||||
onClick={() => void timelineAgent.runPreparedWorkflow(launchLive ? "live" : "replay")}
|
|
||||||
disabled={isChecking || isRunning || !canLaunch}
|
|
||||||
>
|
|
||||||
{launchLabel}
|
|
||||||
</button>
|
|
||||||
{canRetryLive ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="demo-run-launch-control__retry"
|
|
||||||
onClick={retryHealth}
|
|
||||||
disabled={isChecking}
|
|
||||||
>
|
|
||||||
Retry live service
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -3,11 +3,9 @@ import userEvent from "@testing-library/user-event";
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||||
import type { TimelineAgentController } from "../demo/agent/timelineAgent.js";
|
|
||||||
import type { DemoApprovalActions } from "./demo-approval-actions.js";
|
import type { DemoApprovalActions } from "./demo-approval-actions.js";
|
||||||
import { DemoWorkflowScene } from "./DemoWorkflowScene.js";
|
import { DemoWorkflowScene } from "./DemoWorkflowScene.js";
|
||||||
import { findBeat, findScene } from "./storyboard.js";
|
import { findBeat, findScene } from "./storyboard.js";
|
||||||
import type { PresentationTargetHealth } from "./presentation-target-status.js";
|
|
||||||
|
|
||||||
afterEach(() => cleanup());
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
@@ -55,10 +53,6 @@ const renderBeat = (
|
|||||||
options: {
|
options: {
|
||||||
readonly openEvidence?: () => void;
|
readonly openEvidence?: () => void;
|
||||||
readonly approvalActions?: DemoApprovalActions;
|
readonly approvalActions?: DemoApprovalActions;
|
||||||
readonly timelineAgent?: TimelineAgentController;
|
|
||||||
readonly targetStatus?: PresentationTargetHealth;
|
|
||||||
readonly retryHealth?: () => void;
|
|
||||||
readonly liveTargetReady?: boolean;
|
|
||||||
} = {},
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
const openEvidence = options.openEvidence ?? vi.fn();
|
const openEvidence = options.openEvidence ?? vi.fn();
|
||||||
@@ -72,10 +66,6 @@ const renderBeat = (
|
|||||||
selectNode={noop}
|
selectNode={noop}
|
||||||
openEvidence={openEvidence}
|
openEvidence={openEvidence}
|
||||||
approvalActions={options.approvalActions}
|
approvalActions={options.approvalActions}
|
||||||
timelineAgent={options.timelineAgent}
|
|
||||||
targetStatus={options.targetStatus}
|
|
||||||
retryHealth={options.retryHealth}
|
|
||||||
liveTargetReady={options.liveTargetReady}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
return { ...rendered, openEvidence };
|
return { ...rendered, openEvidence };
|
||||||
@@ -91,36 +81,11 @@ describe("DemoWorkflowScene", () => {
|
|||||||
expect(openEvidence).toHaveBeenCalledOnce();
|
expect(openEvidence).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes live launch only on the Scene 10 operation beat", async () => {
|
it("keeps run controls out of the operation beat", () => {
|
||||||
const runPreparedWorkflow = vi.fn(async () => {});
|
renderBeat("operation");
|
||||||
const targetStatus: PresentationTargetHealth = {
|
|
||||||
kind: "ready",
|
|
||||||
target: "http://127.0.0.1:8765/rpc",
|
|
||||||
label: "Live target ready",
|
|
||||||
detail: "127.0.0.1:8765",
|
|
||||||
};
|
|
||||||
const timelineAgent: TimelineAgentController = {
|
|
||||||
messages: [],
|
|
||||||
canRun: true,
|
|
||||||
canRunLive: true,
|
|
||||||
runLabel: "Run prepared workflow",
|
|
||||||
runPreparedWorkflow,
|
|
||||||
submitSelectedIssues: vi.fn(async () => {}),
|
|
||||||
requestRevision: vi.fn(async () => {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const operation = renderBeat("operation", "run-from-deployment", {
|
|
||||||
timelineAgent,
|
|
||||||
targetStatus,
|
|
||||||
liveTargetReady: true,
|
|
||||||
retryHealth: vi.fn(),
|
|
||||||
});
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Run prepared workflow" }));
|
|
||||||
expect(runPreparedWorkflow).toHaveBeenCalledWith("live");
|
|
||||||
operation.unmount();
|
|
||||||
|
|
||||||
renderBeat("graph");
|
|
||||||
expect(screen.queryByRole("region", { name: "prepared workflow launch" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("region", { name: "prepared workflow launch" })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Retry live service")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the run receipt visible when the graph takes over", () => {
|
it("keeps the run receipt visible when the graph takes over", () => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { DemoEvent } from "../demo/timeline/models.js";
|
import type { DemoEvent } from "../demo/timeline/models.js";
|
||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||||
import type { TimelineAgentController } from "../demo/agent/timelineAgent.js";
|
|
||||||
import {
|
import {
|
||||||
demoBeatLensForBeat,
|
demoBeatLensForBeat,
|
||||||
graphExecutionForBeat,
|
graphExecutionForBeat,
|
||||||
@@ -19,9 +18,7 @@ import { OperationBlock } from "./OperationBlock.js";
|
|||||||
import { RunInputFileBrowser } from "./RunInputFileBrowser.js";
|
import { RunInputFileBrowser } from "./RunInputFileBrowser.js";
|
||||||
import { StageCaption } from "./StageCaption.js";
|
import { StageCaption } from "./StageCaption.js";
|
||||||
import type { SceneBeatDefinition, SceneDefinition } from "./storyboard.js";
|
import type { SceneBeatDefinition, SceneDefinition } from "./storyboard.js";
|
||||||
import type { PresentationTargetHealth } from "./presentation-target-status.js";
|
|
||||||
import { WorkflowGraphStage } from "./WorkflowGraphStage.js";
|
import { WorkflowGraphStage } from "./WorkflowGraphStage.js";
|
||||||
import { DemoRunLaunchControl } from "./DemoRunLaunchControl.js";
|
|
||||||
|
|
||||||
type DemoWorkflowSceneProps = {
|
type DemoWorkflowSceneProps = {
|
||||||
readonly scene: SceneDefinition;
|
readonly scene: SceneDefinition;
|
||||||
@@ -31,10 +28,6 @@ type DemoWorkflowSceneProps = {
|
|||||||
readonly selectNode: (nodeId: string | null) => void;
|
readonly selectNode: (nodeId: string | null) => void;
|
||||||
readonly openEvidence: () => void;
|
readonly openEvidence: () => void;
|
||||||
readonly approvalActions?: DemoApprovalActions | undefined;
|
readonly approvalActions?: DemoApprovalActions | undefined;
|
||||||
readonly timelineAgent?: TimelineAgentController | undefined;
|
|
||||||
readonly targetStatus?: PresentationTargetHealth | undefined;
|
|
||||||
readonly retryHealth?: (() => void) | undefined;
|
|
||||||
readonly liveTargetReady?: boolean | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type DemoWorkflowLayout = "operation" | "graph" | "interrupt" | "approval" | "evidence";
|
type DemoWorkflowLayout = "operation" | "graph" | "interrupt" | "approval" | "evidence";
|
||||||
@@ -66,10 +59,6 @@ export const DemoWorkflowScene = ({
|
|||||||
selectNode,
|
selectNode,
|
||||||
openEvidence,
|
openEvidence,
|
||||||
approvalActions,
|
approvalActions,
|
||||||
timelineAgent,
|
|
||||||
targetStatus,
|
|
||||||
retryHealth,
|
|
||||||
liveTargetReady,
|
|
||||||
}: DemoWorkflowSceneProps) => {
|
}: DemoWorkflowSceneProps) => {
|
||||||
const runStart = findEvent(demo, "run_start");
|
const runStart = findEvent(demo, "run_start");
|
||||||
const runResume = findEvent(demo, "run_resume");
|
const runResume = findEvent(demo, "run_resume");
|
||||||
@@ -122,15 +111,6 @@ export const DemoWorkflowScene = ({
|
|||||||
data-support-surface={surface.supportSurface}
|
data-support-surface={surface.supportSurface}
|
||||||
aria-label="demo workflow stage"
|
aria-label="demo workflow stage"
|
||||||
>
|
>
|
||||||
{beat.id === "operation" && timelineAgent && targetStatus && retryHealth ? (
|
|
||||||
<DemoRunLaunchControl
|
|
||||||
status={targetStatus}
|
|
||||||
liveTargetReady={liveTargetReady ?? false}
|
|
||||||
demo={demo}
|
|
||||||
timelineAgent={timelineAgent}
|
|
||||||
retryHealth={retryHealth}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{isGuidedDemoMoment ? (
|
{isGuidedDemoMoment ? (
|
||||||
<GuidedProductMoment
|
<GuidedProductMoment
|
||||||
beat={beat}
|
beat={beat}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
import { callOperation } from "../connection/api.js";
|
import { callOperation } from "../connection/api.js";
|
||||||
@@ -286,19 +286,19 @@ describe("PresentationRoute", () => {
|
|||||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||||
render(<PresentationRoute />);
|
render(<PresentationRoute />);
|
||||||
|
|
||||||
expect(await screen.findAllByRole("button", { name: /run prepared workflow/i })).toHaveLength(3);
|
expect(await screen.findAllByRole("button", { name: /run prepared workflow/i })).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes an explicit live launch on the Scene 10 operation beat", async () => {
|
it("owns the live run action in the footer rail", async () => {
|
||||||
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
|
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
|
||||||
window.location.hash = "#scene/run-from-deployment/operation";
|
window.location.hash = "#scene/run-from-deployment/operation";
|
||||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||||
render(<PresentationRoute />);
|
render(<PresentationRoute />);
|
||||||
|
|
||||||
const launches = await screen.findAllByRole("button", { name: "Run prepared workflow" });
|
const footer = await screen.findByRole("contentinfo", { name: /presentation footer/i });
|
||||||
const launch = launches.at(-1);
|
const runButton = within(footer).getByRole("button", { name: "Run prepared workflow" });
|
||||||
if (!launch) throw new Error("Expected a live workflow launch control");
|
expect(screen.queryByRole("region", { name: "prepared workflow launch" })).not.toBeInTheDocument();
|
||||||
await userEvent.click(launch);
|
await userEvent.click(runButton);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedCallOperation.mock.calls.some(([operation]) => operation === "workflow.deployments.inspect"))
|
expect(mockedCallOperation.mock.calls.some(([operation]) => operation === "workflow.deployments.inspect"))
|
||||||
@@ -406,7 +406,10 @@ describe("PresentationRoute", () => {
|
|||||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||||
render(<PresentationRoute />);
|
render(<PresentationRoute />);
|
||||||
|
|
||||||
const runButton = await screen.findByRole("button", { name: /run replay walkthrough/i });
|
const footer = await screen.findByRole("contentinfo", { name: /presentation footer/i });
|
||||||
|
const runButton = within(footer).getByRole("button", { name: /play replay walkthrough/i });
|
||||||
|
expect(screen.queryByRole("region", { name: "prepared workflow launch" })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Retry live service")).not.toBeInTheDocument();
|
||||||
await user.click(runButton);
|
await user.click(runButton);
|
||||||
expect(await screen.findByLabelText("workflow.runs.start operation")).toBeInTheDocument();
|
expect(await screen.findByLabelText("workflow.runs.start operation")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,9 +108,6 @@ export const PresentationStage = ({
|
|||||||
}}
|
}}
|
||||||
motionDisabled={state.motionDisabled}
|
motionDisabled={state.motionDisabled}
|
||||||
approvalActions={approvalActions}
|
approvalActions={approvalActions}
|
||||||
targetStatus={targetStatus}
|
|
||||||
retryHealth={retryHealth}
|
|
||||||
liveTargetReady={liveTargetReady}
|
|
||||||
onScene9Advance={onScene9Advance}
|
onScene9Advance={onScene9Advance}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { projectPreparedAuthoringPhase } from "./authoring/authoring-projection.
|
|||||||
import type { AuthoringPhaseId } from "./authoring/authoring-recording.js";
|
import type { AuthoringPhaseId } from "./authoring/authoring-recording.js";
|
||||||
import { OpeningThesisScene } from "./opening/OpeningThesisScene.js";
|
import { OpeningThesisScene } from "./opening/OpeningThesisScene.js";
|
||||||
import { ProblemLoopScene } from "./opening/ProblemLoopScene.js";
|
import { ProblemLoopScene } from "./opening/ProblemLoopScene.js";
|
||||||
import type { PresentationTargetHealth } from "./presentation-target-status.js";
|
|
||||||
|
|
||||||
type SceneBodyProps = {
|
type SceneBodyProps = {
|
||||||
readonly location: PresentationLocation;
|
readonly location: PresentationLocation;
|
||||||
@@ -35,9 +34,6 @@ type SceneBodyProps = {
|
|||||||
readonly onFocusPathChange: (path: readonly string[]) => void;
|
readonly onFocusPathChange: (path: readonly string[]) => void;
|
||||||
readonly motionDisabled: boolean;
|
readonly motionDisabled: boolean;
|
||||||
readonly approvalActions?: DemoApprovalActions | undefined;
|
readonly approvalActions?: DemoApprovalActions | undefined;
|
||||||
readonly targetStatus?: PresentationTargetHealth | undefined;
|
|
||||||
readonly retryHealth?: (() => void) | undefined;
|
|
||||||
readonly liveTargetReady?: boolean | undefined;
|
|
||||||
readonly onScene9Advance?: (() => void) | undefined;
|
readonly onScene9Advance?: (() => void) | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -316,7 +312,7 @@ const assertNever = (value: never): never => {
|
|||||||
throw new Error(`Unexpected view: ${value}`);
|
throw new Error(`Unexpected view: ${value}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SceneBody = ({ location, demo, timelineAgent, selectedNodeId, selectNode, openEvidence, openDiscussion, onFocusPathChange, motionDisabled, approvalActions, targetStatus, retryHealth, liveTargetReady, onScene9Advance }: SceneBodyProps) => {
|
export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvidence, openDiscussion, onFocusPathChange, motionDisabled, approvalActions, onScene9Advance }: SceneBodyProps) => {
|
||||||
const sceneId = location.kind === "main" ? location.sceneId : "positioning";
|
const sceneId = location.kind === "main" ? location.sceneId : "positioning";
|
||||||
const beatId = location.kind === "main" ? location.beatId : "landscape";
|
const beatId = location.kind === "main" ? location.beatId : "landscape";
|
||||||
const scene = findScene(sceneId) ?? findScene("thesis")!;
|
const scene = findScene(sceneId) ?? findScene("thesis")!;
|
||||||
@@ -368,10 +364,6 @@ export const SceneBody = ({ location, demo, timelineAgent, selectedNodeId, selec
|
|||||||
selectNode={selectNode}
|
selectNode={selectNode}
|
||||||
openEvidence={openEvidence}
|
openEvidence={openEvidence}
|
||||||
approvalActions={approvalActions}
|
approvalActions={approvalActions}
|
||||||
timelineAgent={timelineAgent}
|
|
||||||
targetStatus={targetStatus}
|
|
||||||
retryHealth={retryHealth}
|
|
||||||
liveTargetReady={liveTargetReady}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "evaluation":
|
case "evaluation":
|
||||||
|
|||||||
@@ -88,90 +88,6 @@
|
|||||||
font-family: var(--font-mono, monospace);
|
font-family: var(--font-mono, monospace);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-run-launch-control {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 0.7rem;
|
|
||||||
border: 1px solid color-mix(in oklch, var(--accent-cyan) 32%, var(--stage-line));
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
padding: 0.7rem 0.85rem;
|
|
||||||
background: color-mix(in oklch, var(--stage-surface) 84%, var(--accent-cyan));
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__copy {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__eyebrow {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.15rem;
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
font: 650 0.58rem/1 var(--font-mono, monospace);
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__copy strong {
|
|
||||||
display: block;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__copy p {
|
|
||||||
margin: 0.15rem 0 0;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__actions {
|
|
||||||
display: flex;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
gap: 0.45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__actions button {
|
|
||||||
border: 1px solid var(--stage-line);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 0.5rem 0.7rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font: 650 0.68rem/1 var(--font-mono, monospace);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__primary {
|
|
||||||
background: var(--accent-cyan);
|
|
||||||
color: oklch(0.12 0.03 250) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__retry {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__actions button:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.48;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container presentation-canvas (max-width: 760px) {
|
|
||||||
.demo-run-launch-control {
|
|
||||||
align-items: stretch;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-run-launch-control__actions button {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Operation handoff: a single large receipt, not a stack of dashboard cards. */
|
/* Operation handoff: a single large receipt, not a stack of dashboard cards. */
|
||||||
.presentation-route .operation-block--expanded {
|
.presentation-route .operation-block--expanded {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
Reference in New Issue
Block a user