Skip to content

Commit

Permalink
Merge pull request #16465 from storybookjs/16426-callback-call-ids
Browse files Browse the repository at this point in the history
Interactions: Fix duplicate rows in waitFor
  • Loading branch information
ghengeveld committed Nov 24, 2021
2 parents 9c9625a + 161598a commit ca6a34d
Show file tree
Hide file tree
Showing 39 changed files with 1,484 additions and 612 deletions.
4 changes: 2 additions & 2 deletions addons/interactions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@
"ts-dedent": "^2.2.0"
},
"devDependencies": {
"@storybook/jest": "^0.0.2",
"@storybook/testing-library": "^0.0.3",
"@storybook/jest": "^0.0.5",
"@storybook/testing-library": "^0.0.7",
"formik": "^2.2.9"
},
"peerDependencies": {
Expand Down
24 changes: 16 additions & 8 deletions addons/interactions/src/Panel.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ComponentStoryObj, ComponentMeta } from '@storybook/react';
import { CallStates } from '@storybook/instrumenter';
import { styled } from '@storybook/theming';

import { getCall } from './mocks';
import { AddonPanelPure } from './Panel';
import SubnavStories from './components/Subnav/Subnav.stories';

const StyledWrapper = styled.div(({ theme }) => ({
backgroundColor: theme.background.content,
Expand Down Expand Up @@ -33,16 +35,14 @@ export default {
},
args: {
calls: new Map(),
endRef: null,
controls: SubnavStories.args.controls,
controlStates: SubnavStories.args.controlStates,
interactions: [getCall(CallStates.DONE)],
fileName: 'addon-interactions.stories.tsx',
hasException: false,
hasNext: false,
hasPrevious: true,
interactions: [getCall(CallStates.DONE)],
isDisabled: false,
isPlaying: false,
showTabIcon: false,
isDebuggingEnabled: true,
onScrollToEnd: action('onScrollToEnd'),
endRef: null,
// prop for the AddonPanel used as wrapper of Panel
active: true,
},
Expand All @@ -60,6 +60,14 @@ export const Paused: Story = {
args: {
isPlaying: true,
interactions: [getCall(CallStates.WAITING)],
controlStates: {
debugger: true,
start: false,
back: false,
goto: true,
next: true,
end: true,
},
},
};

Expand All @@ -78,7 +86,7 @@ export const Failed: Story = {
};

export const WithDebuggingDisabled: Story = {
args: { isDebuggingEnabled: false },
args: { controlStates: { ...SubnavStories.args.controlStates, debugger: false } },
};

export const NoInteractions: Story = {
Expand Down
144 changes: 64 additions & 80 deletions addons/interactions/src/Panel.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,49 @@
import global from 'global';
import * as React from 'react';
import ReactDOM from 'react-dom';
import { useChannel, useParameter, useStorybookState } from '@storybook/api';
import { useChannel, useParameter, StoryId } from '@storybook/api';
import { STORY_RENDER_PHASE_CHANGED } from '@storybook/core-events';
import { AddonPanel, Link, Placeholder } from '@storybook/components';
import { EVENTS, Call, CallStates, LogItem } from '@storybook/instrumenter';
import { EVENTS, Call, CallStates, ControlStates, LogItem } from '@storybook/instrumenter';
import { styled } from '@storybook/theming';

import { StatusIcon } from './components/StatusIcon/StatusIcon';
import { Subnav } from './components/Subnav/Subnav';
import { Interaction } from './components/Interaction/Interaction';

const { FEATURES } = global;
export interface Controls {
start: (args: any) => void;
back: (args: any) => void;
goto: (args: any) => void;
next: (args: any) => void;
end: (args: any) => void;
}

interface AddonPanelProps {
active: boolean;
}

interface InteractionsPanelProps {
active: boolean;
interactions: (Call & { state?: CallStates })[];
isDisabled?: boolean;
hasPrevious?: boolean;
hasNext?: boolean;
controls: Controls;
controlStates: ControlStates;
interactions: (Call & { status?: CallStates })[];
fileName?: string;
hasException?: boolean;
isPlaying?: boolean;
calls: Map<string, any>;
endRef?: React.Ref<HTMLDivElement>;
isDebuggingEnabled?: boolean;
onStart?: () => void;
onPrevious?: () => void;
onNext?: () => void;
onEnd?: () => void;
onScrollToEnd?: () => void;
onInteractionClick?: (callId: string) => void;
}

const pendingStates = [CallStates.ACTIVE, CallStates.WAITING];
const completedStates = [CallStates.DONE, CallStates.ERROR];
const INITIAL_CONTROL_STATES = {
debugger: false,
start: false,
back: false,
goto: false,
next: false,
end: false,
};

const TabIcon = styled(StatusIcon)({
marginLeft: 5,
Expand All @@ -51,39 +56,27 @@ const TabStatus = ({ children }: { children: React.ReactChild }) => {

export const AddonPanelPure: React.FC<InteractionsPanelProps> = React.memo(
({
calls,
controls,
controlStates,
interactions,
isDisabled,
hasPrevious,
hasNext,
fileName,
hasException,
isPlaying,
onStart,
onPrevious,
onNext,
onEnd,
onScrollToEnd,
calls,
onInteractionClick,
endRef,
isDebuggingEnabled,
...panelProps
}) => (
<AddonPanel {...panelProps}>
{isDebuggingEnabled && interactions.length > 0 && (
{controlStates.debugger && interactions.length > 0 && (
<Subnav
isDisabled={isDisabled}
hasPrevious={hasPrevious}
hasNext={hasNext}
storyFileName={fileName}
controls={controls}
controlStates={controlStates}
status={
// eslint-disable-next-line no-nested-ternary
isPlaying ? CallStates.ACTIVE : hasException ? CallStates.ERROR : CallStates.DONE
}
onStart={onStart}
onPrevious={onPrevious}
onNext={onNext}
onEnd={onEnd}
storyFileName={fileName}
onScrollToEnd={onScrollToEnd}
/>
)}
Expand All @@ -92,9 +85,8 @@ export const AddonPanelPure: React.FC<InteractionsPanelProps> = React.memo(
key={call.id}
call={call}
callsById={calls}
isDebuggingEnabled={isDebuggingEnabled}
isDisabled={isDisabled}
onClick={() => onInteractionClick(call.id)}
controls={controls}
controlStates={controlStates}
/>
))}
<div ref={endRef} />
Expand All @@ -115,15 +107,17 @@ export const AddonPanelPure: React.FC<InteractionsPanelProps> = React.memo(
);

export const Panel: React.FC<AddonPanelProps> = (props) => {
const [isLocked, setLock] = React.useState(false);
const [isPlaying, setPlaying] = React.useState(true);
const [storyId, setStoryId] = React.useState<StoryId>();
const [controlStates, setControlStates] = React.useState<ControlStates>(INITIAL_CONTROL_STATES);
const [isPlaying, setPlaying] = React.useState(false);
const [scrollTarget, setScrollTarget] = React.useState<HTMLElement>();

const calls = React.useRef<Map<Call['id'], Omit<Call, 'state'>>>(new Map());
const setCall = ({ state, ...call }: Call) => calls.current.set(call.id, call);
// Calls are tracked in a ref so we don't needlessly rerender.
const calls = React.useRef<Map<Call['id'], Omit<Call, 'status'>>>(new Map());
const setCall = ({ status, ...call }: Call) => calls.current.set(call.id, call);

const [log, setLog] = React.useState<LogItem[]>([]);
const interactions = log.map(({ callId, state }) => ({ ...calls.current.get(callId), state }));
const interactions = log.map(({ callId, status }) => ({ ...calls.current.get(callId), status }));

const endRef = React.useRef();
React.useEffect(() => {
Expand All @@ -135,41 +129,38 @@ export const Panel: React.FC<AddonPanelProps> = (props) => {
return () => observer.disconnect();
}, []);

const emit = useChannel({
[EVENTS.CALL]: setCall,
[EVENTS.SYNC]: setLog,
[EVENTS.LOCK]: setLock,
[STORY_RENDER_PHASE_CHANGED]: ({ newPhase }) => {
setLock(false);
setPlaying(newPhase === 'playing');
const emit = useChannel(
{
[EVENTS.CALL]: setCall,
[EVENTS.SYNC]: (payload) => {
setControlStates(payload.controlStates);
setLog(payload.logItems);
},
[STORY_RENDER_PHASE_CHANGED]: (event) => {
setStoryId(event.storyId);
setPlaying(event.newPhase === 'playing');
},
},
});
[]
);

const controls = React.useMemo(
() => ({
start: () => emit(EVENTS.START, { storyId }),
back: () => emit(EVENTS.BACK, { storyId }),
goto: (callId: string) => emit(EVENTS.GOTO, { storyId, callId }),
next: () => emit(EVENTS.NEXT, { storyId }),
end: () => emit(EVENTS.END, { storyId }),
}),
[storyId]
);

const { storyId } = useStorybookState();
const storyFilePath = useParameter('fileName', '');
const [fileName] = storyFilePath.toString().split('/').slice(-1);
const scrollToTarget = () => scrollTarget?.scrollIntoView({ behavior: 'smooth', block: 'end' });

const isDebuggingEnabled = FEATURES.interactionsDebugger === true;

const showStatus = log.length > 0 && !isPlaying;
const isDebugging = log.some((item) => pendingStates.includes(item.state));
const hasPrevious = log.some((item) => completedStates.includes(item.state));
const hasNext = log.some((item) => item.state === CallStates.WAITING);
const hasActive = log.some((item) => item.state === CallStates.ACTIVE);
const hasException = log.some((item) => item.state === CallStates.ERROR);
const isDisabled = isDebuggingEnabled
? hasActive || isLocked || (isPlaying && !isDebugging)
: true;

const onStart = React.useCallback(() => emit(EVENTS.START, { storyId }), [storyId]);
const onPrevious = React.useCallback(() => emit(EVENTS.BACK, { storyId }), [storyId]);
const onNext = React.useCallback(() => emit(EVENTS.NEXT, { storyId }), [storyId]);
const onEnd = React.useCallback(() => emit(EVENTS.END, { storyId }), [storyId]);
const onInteractionClick = React.useCallback(
(callId: string) => emit(EVENTS.GOTO, { storyId, callId }),
[storyId]
);
const hasException = log.some((item) => item.status === CallStates.ERROR);

return (
<React.Fragment key="interactions">
Expand All @@ -178,21 +169,14 @@ export const Panel: React.FC<AddonPanelProps> = (props) => {
(hasException ? <TabIcon status={CallStates.ERROR} /> : ` (${interactions.length})`)}
</TabStatus>
<AddonPanelPure
calls={calls.current}
controls={controls}
controlStates={controlStates}
interactions={interactions}
isDisabled={isDisabled}
hasPrevious={hasPrevious}
hasNext={hasNext}
fileName={fileName}
hasException={hasException}
isPlaying={isPlaying}
calls={calls.current}
endRef={endRef}
isDebuggingEnabled={isDebuggingEnabled}
onStart={onStart}
onPrevious={onPrevious}
onNext={onNext}
onEnd={onEnd}
onInteractionClick={onInteractionClick}
onScrollToEnd={scrollTarget && scrollToTarget}
{...props}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ Demo.play = async ({ args, canvasElement }) => {
await expect(args.onSubmit).toHaveBeenCalledWith(expect.stringMatching(/([A-Z])\w+/gi));
};

export const FindBy: CSF2Story = (args) => {
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
setTimeout(() => setIsLoading(false), 500);
}, []);
return isLoading ? <div>Loading...</div> : <button type="button">Loaded!</button>;
};
FindBy.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByRole('button');
await expect(true).toBe(true);
};

export const WaitFor: CSF2Story = (args) => (
<button type="button" onClick={() => setTimeout(() => args.onSubmit('clicked'), 100)}>
Click
Expand All @@ -46,6 +59,7 @@ WaitFor.play = async ({ args, canvasElement }) => {
await userEvent.click(await within(canvasElement).findByText('Click'));
await waitFor(async () => {
await expect(args.onSubmit).toHaveBeenCalledWith(expect.stringMatching(/([A-Z])\w+/gi));
await expect(true).toBe(true);
});
};

Expand All @@ -54,7 +68,6 @@ export const WaitForElementToBeRemoved: CSF2Story = () => {
React.useEffect(() => {
setTimeout(() => setIsLoading(false), 1500);
}, []);

return isLoading ? <div>Loading...</div> : <button type="button">Loaded!</button>;
};
WaitForElementToBeRemoved.play = async ({ canvasElement }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { userEvent, within } from '@storybook/testing-library';
import { getCall } from '../../mocks';

import { Interaction } from './Interaction';
import SubnavStories from '../Subnav/Subnav.stories';

type Story = ComponentStoryObj<typeof Interaction>;

Expand All @@ -13,8 +14,8 @@ export default {
component: Interaction,
args: {
callsById: new Map(),
isDisabled: false,
isDebuggingEnabled: true,
controls: SubnavStories.args.controls,
controlStates: SubnavStories.args.controlStates,
},
} as ComponentMeta<typeof Interaction>;

Expand Down Expand Up @@ -43,7 +44,7 @@ export const Done: Story = {
};

export const Disabled: Story = {
args: { ...Done.args, isDisabled: true },
args: { ...Done.args, controlStates: { ...SubnavStories.args.controlStates, goto: false } },
};

export const Hovered: Story = {
Expand Down

0 comments on commit ca6a34d

Please sign in to comment.