Skip to content

Commit 60fa74d

Browse files
authored
fix(rest): enforce sequential lifecycle stages
1 parent 1936e73 commit 60fa74d

3 files changed

Lines changed: 35 additions & 7 deletions

File tree

frontend/src/topics/rest/lab.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
type RestScenarioId,
1515
type RestTraceStageId,
1616
} from "./content";
17-
import { createInitialRestState, isRestLabComplete, runRestEvent } from "./simulator";
17+
import { createInitialRestState, isRestLabComplete, isRestStageUnlocked, runRestEvent } from "./simulator";
1818

1919
type RestCodeMode = "annotated" | "source";
2020

@@ -163,12 +163,13 @@ export function RestLab({ onComplete }: { onComplete?: () => void }) {
163163
{restTraceStages.map((stage, index) => {
164164
const isBlocked = index > terminalIndex;
165165
const isVisited = state.currentVisitedStageIds.includes(stage.id);
166+
const isLocked = state.requestStarted && !isRestStageUnlocked(state, stage.id);
166167
return (
167168
<button
168169
className={`${state.activeStageId === stage.id ? "active" : ""} ${isVisited ? "visited" : ""}`}
169170
type="button"
170171
key={stage.id}
171-
disabled={!state.requestStarted || isBlocked}
172+
disabled={!state.requestStarted || isBlocked || isLocked}
172173
onClick={() => dispatch({ type: "inspect-stage", stageId: stage.id })}
173174
>
174175
<span>{stage.label}</span><b>{stage.actor}</b><small>{isBlocked ? "此 request 不執行" : stage.summary}</small>

frontend/src/topics/rest/simulator.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ describe("REST simulator", () => {
6262
expect(result.state.databaseItems).toEqual(restDatabaseFixture);
6363
});
6464

65+
it("rejects future stages until the previous lifecycle stages are visited", () => {
66+
const started = runRestEvents([{ type: "start-request" }]);
67+
const skipped = runRestEvent(started, { type: "inspect-stage", stageId: "routing" });
68+
69+
expect(skipped.accepted).toBe(false);
70+
expect(skipped.state.activeStageId).toBe("browser");
71+
expect(skipped.state.currentVisitedStageIds).toEqual(["browser"]);
72+
73+
const next = runRestEvent(started, { type: "inspect-stage", stageId: "cors" });
74+
expect(next.accepted).toBe(true);
75+
expect(next.state.activeStageId).toBe("cors");
76+
});
77+
6578
it("completes only after all requests and lifecycle stages", () => {
6679
const events = restScenarios.flatMap((scenario) => completeScenarioEvents(scenario.id));
6780
const state = runRestEvents(events);

frontend/src/topics/rest/simulator.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,23 @@ function stageIndex(stageId: RestTraceStageId): number {
3838
return restTraceStages.findIndex((stage) => stage.id === stageId);
3939
}
4040

41-
function isKnownStage(stageId: RestTraceStageId): boolean {
42-
return stageIndex(stageId) >= 0;
41+
function highestVisitedStageIndex(state: RestLabState): number {
42+
return state.currentVisitedStageIds.reduce(
43+
(highestIndex, visitedStageId) => Math.max(highestIndex, stageIndex(visitedStageId)),
44+
-1,
45+
);
46+
}
47+
48+
export function isRestStageUnlocked(state: RestLabState, stageId: RestTraceStageId): boolean {
49+
const scenario = findRestScenario(state.selectedScenarioId);
50+
const targetIndex = stageIndex(stageId);
51+
const terminalIndex = stageIndex(scenario.terminalStageId);
52+
53+
if (!state.requestStarted || targetIndex < 0 || targetIndex > terminalIndex) {
54+
return false;
55+
}
56+
57+
return targetIndex <= highestVisitedStageIndex(state) + 1;
4358
}
4459

4560
function isComplete(state: RestLabState): boolean {
@@ -62,7 +77,7 @@ function withStage(current: RestLabState, stageId: RestTraceStageId): RestLabSta
6277
const terminalIndex = stageIndex(scenario.terminalStageId);
6378
const nextIndex = stageIndex(stageId);
6479

65-
if (!state.requestStarted || !isKnownStage(stageId) || nextIndex > terminalIndex) {
80+
if (!isRestStageUnlocked(state, stageId)) {
6681
return { ...state, lastMessage: "這個 stage 不會在目前 request 執行;請依 lifecycle 前進。" };
6782
}
6883

@@ -150,8 +165,7 @@ export function runRestEvent(current: RestLabState, event: RestLabEvent): RestEv
150165
}
151166

152167
if (event.type === "inspect-stage") {
153-
const scenario = findRestScenario(state.selectedScenarioId);
154-
const accepted = isKnownStage(event.stageId) && stageIndex(event.stageId) <= stageIndex(scenario.terminalStageId);
168+
const accepted = isRestStageUnlocked(state, event.stageId);
155169
return { state: withStage(state, event.stageId), accepted };
156170
}
157171

0 commit comments

Comments
 (0)