From 69179b236673e30679c0fdfa183a8832fbc2f7e9 Mon Sep 17 00:00:00 2001 From: Joseph Dale Banares Date: Sat, 24 Oct 2026 00:40:19 +0800 Subject: [PATCH] fix(viewer): keep the flow playing when its speed changes --- .../src/explore/ExploreContext.test.tsx | 54 +++++++++++++++++++ .../viewer/src/explore/ExploreContext.tsx | 7 ++- packages/viewer/src/explore/emphasis.ts | 5 +- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 packages/viewer/src/explore/ExploreContext.test.tsx diff --git a/packages/viewer/src/explore/ExploreContext.test.tsx b/packages/viewer/src/explore/ExploreContext.test.tsx new file mode 100644 index 0000000..8460b28 --- /dev/null +++ b/packages/viewer/src/explore/ExploreContext.test.tsx @@ -0,0 +1,54 @@ +import { act, render } from '@testing-library/react'; +import { afterEach, describe, expect, it } from 'vitest'; +import { commerceApiLayout } from '../samples/commerce-api.layout'; +import { ExploreProvider, useExplore } from './ExploreContext'; +import type { Emphasis } from './emphasis'; +import type { ExploreAction } from './state'; + +const draft = commerceApiLayout.draft; +const first = draft.nodes[0]!; + +afterEach(() => history.replaceState(null, '', location.pathname)); + +function mount() { + const seen: Emphasis[] = []; + let send!: (a: ExploreAction) => void; + function Probe() { + const { emphasis, dispatch } = useExplore(); + seen.push(emphasis); + send = dispatch; + return null; + } + render( + + + , + ); + return { latest: () => seen[seen.length - 1]!, dispatch: (a: ExploreAction) => act(() => send(a)) }; +} + +describe('ExploreProvider emphasis', () => { + it('keeps the same emphasis through play and speed changes', () => { + const { latest, dispatch } = mount(); + const before = latest(); + dispatch({ type: 'togglePlay' }); + dispatch({ type: 'cycleSpeed' }); + expect(latest()).toBe(before); + }); + + it('rebuilds emphasis when the selection changes', () => { + const { latest, dispatch } = mount(); + const before = latest(); + dispatch({ type: 'select', id: first.id }); + expect(latest()).not.toBe(before); + expect(latest().nodes.get(first.id)).toBe('focus'); + }); + + it('rebuilds emphasis when a type is hidden', () => { + const { latest, dispatch } = mount(); + const before = latest(); + dispatch({ type: 'toggleType', nodeType: first.type }); + expect(latest()).not.toBe(before); + expect(latest().nodes.get(first.id)).toBe('dim'); + }); +}); diff --git a/packages/viewer/src/explore/ExploreContext.tsx b/packages/viewer/src/explore/ExploreContext.tsx index 38e721e..32fd7e7 100644 --- a/packages/viewer/src/explore/ExploreContext.tsx +++ b/packages/viewer/src/explore/ExploreContext.tsx @@ -49,7 +49,12 @@ export function ExploreProvider({ const known = useMemo(() => knownOf(draft), [draft]); const [state, dispatch] = useReducer(explore, undefined, () => (syncHash ? parseHash(location.hash, known) : INITIAL)); const graph = useMemo(() => buildGraph(draft.nodes.map((n) => n.id), draft.edges, { timed: draft.kind === 'sequence' }), [draft]); - const em = useMemo(() => emphasis(draft, graph, state), [draft, graph, state]); + const { peek, view, query, trace, selected, route, routing, hiddenTypes } = state; + // Only what emphasis reads: a speed or play toggle must not rebuild it, or the playing flow restarts from 0. + const em = useMemo( + () => emphasis(draft, graph, { peek, view, query, trace, selected, route, routing, hiddenTypes }), + [draft, graph, peek, view, query, trace, selected, route, routing, hiddenTypes], + ); const stateRef = useRef(state); stateRef.current = state; diff --git a/packages/viewer/src/explore/emphasis.ts b/packages/viewer/src/explore/emphasis.ts index 6d510e5..97f59f0 100644 --- a/packages/viewer/src/explore/emphasis.ts +++ b/packages/viewer/src/explore/emphasis.ts @@ -26,7 +26,10 @@ export function searchMatches(draft: DiagramDraft, query: string): DiagramNode[] return hits.map((n, i) => ({ n, i })).sort((a, b) => prefix(a.n) - prefix(b.n) || a.i - b.i).map((x) => x.n); } -export function emphasis(draft: DiagramDraft, graph: Graph, s: ExploreState): Emphasis { +/** The explorer state emphasis reads; playback fields (playing, speed) and reveal don't change what's lit. */ +export type EmphasisInput = Pick; + +export function emphasis(draft: DiagramDraft, graph: Graph, s: EmphasisInput): Emphasis { if (s.peek) return peekAt(draft, s.peek); const view = s.view ? new Set(draft.views?.find((v) => v.id === s.view)?.nodes ?? []) : null; const q = s.query?.trim() ? new Set(searchMatches(draft, s.query).map((n) => n.id)) : null;