mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 11:11:53 +08:00
hoist testProvider state to root, add WIP sidebar contextMenu
This commit is contained in:
parent
1abc35379e
commit
c69c1f30a7
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { IconButton } from 'storybook/internal/components';
|
||||
import { addons } from 'storybook/internal/manager-api';
|
||||
@ -14,22 +14,26 @@ addons.setConfig({
|
||||
},
|
||||
});
|
||||
|
||||
// TEMP, to demo new api
|
||||
addons.add('my-addon', {
|
||||
type: Addon_TypesEnum.experimental_CONTEXT,
|
||||
render(props) {
|
||||
console.log({ props });
|
||||
return <div>My Test</div>;
|
||||
},
|
||||
});
|
||||
// // TEMP, to demo new api
|
||||
// addons.add('my-addon', {
|
||||
// type: Addon_TypesEnum.experimental_CONTEXT,
|
||||
// render(props) {
|
||||
// console.log({ props });
|
||||
|
||||
// if (props.entry.type === 'docs') {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// return <div>My Test</div>;
|
||||
// },
|
||||
// });
|
||||
|
||||
// TEMP, to set status once, to have the status bullet show up
|
||||
addons.register('my-addon', (api) => {
|
||||
addons.add('my-addon2', {
|
||||
type: Addon_TypesEnum.TOOL,
|
||||
title: 'My Addon 2',
|
||||
render(props) {
|
||||
console.log({ props });
|
||||
render() {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
|
@ -93,6 +93,9 @@ addons.register(ADDON_ID, (api) => {
|
||||
watchable: true,
|
||||
|
||||
name: 'Component tests',
|
||||
contextMenu: ({ context, state }) => {
|
||||
return <div>Testing {state.running ? '!' : '?'}</div>;
|
||||
},
|
||||
title: ({ crashed, failed }) =>
|
||||
crashed || failed ? 'Component tests failed' : 'Component tests',
|
||||
description: ({ failed, running, watching, progress, crashed, error }) => {
|
||||
|
@ -121,7 +121,7 @@ const WithTooltipPure = ({
|
||||
}
|
||||
);
|
||||
|
||||
const tooltipComponent = (
|
||||
const tooltipComponent = isVisible ? (
|
||||
<Tooltip
|
||||
placement={state?.placement}
|
||||
ref={setTooltipRef}
|
||||
@ -133,7 +133,7 @@ const WithTooltipPure = ({
|
||||
{/* @ts-expect-error (non strict) */}
|
||||
{typeof tooltip === 'function' ? tooltip({ onHide: () => onVisibleChange(false) }) : tooltip}
|
||||
</Tooltip>
|
||||
);
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -9,16 +9,16 @@ export type TestProviderState = Addon_TestProviderState;
|
||||
export type TestProviders = Record<TestProviderId, TestProviderConfig & TestProviderState>;
|
||||
|
||||
export type TestingModuleRunRequestStories = {
|
||||
id: string;
|
||||
name: string;
|
||||
id: string; // button--primary
|
||||
name: string; // Primary
|
||||
};
|
||||
|
||||
export type TestingModuleRunRequestPayload = {
|
||||
providerId: TestProviderId;
|
||||
payload: {
|
||||
stories: TestingModuleRunRequestStories[];
|
||||
importPath: string;
|
||||
componentPath: string;
|
||||
importPath: string; // ./.../button.stories.tsx
|
||||
componentPath: string; // ./.../button.tsx
|
||||
}[];
|
||||
};
|
||||
|
||||
|
@ -20,7 +20,6 @@ import { global } from '@storybook/global';
|
||||
import { logger } from '@storybook/core/client-logger';
|
||||
import { SET_CONFIG } from '@storybook/core/core-events';
|
||||
|
||||
import type { Addon_ContextType } from '../../types';
|
||||
import type { API } from '../root';
|
||||
import { mockChannel } from './storybook-channel-mock';
|
||||
|
||||
@ -96,7 +95,6 @@ export class AddonStore {
|
||||
| Omit<Addon_SidebarBottomType, 'id'>
|
||||
| Omit<Addon_TestProviderType, 'id'>
|
||||
| Omit<Addon_PageType, 'id'>
|
||||
| Omit<Addon_ContextType, 'id'>
|
||||
| Omit<Addon_WrapperType, 'id'>
|
||||
): void {
|
||||
const { type } = addon;
|
||||
|
100
code/core/src/manager-api/modules/experimental_testmodule.ts
Normal file
100
code/core/src/manager-api/modules/experimental_testmodule.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { Addon_TypesEnum } from '@storybook/core/types';
|
||||
|
||||
import {
|
||||
TESTING_MODULE_CANCEL_TEST_RUN_REQUEST,
|
||||
TESTING_MODULE_RUN_ALL_REQUEST,
|
||||
type TestProviderId,
|
||||
type TestProviderState,
|
||||
type TestProviders,
|
||||
} from '@storybook/core/core-events';
|
||||
|
||||
import type { ModuleFn } from '../lib/types';
|
||||
|
||||
export type SubState = {
|
||||
testProviders: TestProviders;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = '@storybook/manager/test-providers';
|
||||
|
||||
const initialTestProviderState: TestProviderState = {
|
||||
details: {} as { [key: string]: any },
|
||||
cancellable: false,
|
||||
cancelling: false,
|
||||
running: false,
|
||||
watching: false,
|
||||
failed: false,
|
||||
crashed: false,
|
||||
};
|
||||
|
||||
export type SubAPI = {
|
||||
getTestproviderState(id: string): TestProviderState | undefined;
|
||||
updateTestproviderState(id: TestProviderId, update: Partial<TestProviderState>): void;
|
||||
clearTestproviderState(id: TestProviderId): void;
|
||||
runTestprovider(id: TestProviderId): void;
|
||||
cancelTestprovider(id: TestProviderId): void;
|
||||
};
|
||||
|
||||
export const init: ModuleFn = ({ store, fullAPI }) => {
|
||||
let sessionState: TestProviders = {};
|
||||
try {
|
||||
sessionState = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
|
||||
} catch (_) {
|
||||
//
|
||||
}
|
||||
|
||||
const state: SubState = {
|
||||
testProviders: sessionState,
|
||||
};
|
||||
|
||||
const api: SubAPI = {
|
||||
getTestproviderState(id) {
|
||||
const { testProviders } = store.getState();
|
||||
|
||||
return testProviders?.[id];
|
||||
},
|
||||
updateTestproviderState(id, update) {
|
||||
return store.setState(
|
||||
({ testProviders }) => {
|
||||
return { testProviders: { ...testProviders, [id]: { ...testProviders[id], ...update } } };
|
||||
},
|
||||
{ persistence: 'session' }
|
||||
);
|
||||
},
|
||||
clearTestproviderState(id) {
|
||||
const update = {
|
||||
cancelling: false,
|
||||
running: true,
|
||||
failed: false,
|
||||
crashed: false,
|
||||
progress: undefined,
|
||||
};
|
||||
return store.setState(
|
||||
({ testProviders }) => {
|
||||
return { testProviders: { ...testProviders, [id]: { ...testProviders[id], ...update } } };
|
||||
},
|
||||
{ persistence: 'session' }
|
||||
);
|
||||
},
|
||||
runTestprovider(id) {
|
||||
fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id });
|
||||
|
||||
return () => api.cancelTestprovider(id);
|
||||
},
|
||||
cancelTestprovider(id) {
|
||||
api.updateTestproviderState(id, { cancelling: true });
|
||||
fullAPI.emit(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, { providerId: id });
|
||||
},
|
||||
};
|
||||
|
||||
const initModule = async () => {
|
||||
const initialState = Object.fromEntries(
|
||||
Object.entries(fullAPI.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER)).map(
|
||||
([id, config]) => [id, { ...config, ...initialTestProviderState, ...sessionState[id] }]
|
||||
)
|
||||
);
|
||||
|
||||
store.setState({ testProviders: initialState }, { persistence: 'session' });
|
||||
};
|
||||
|
||||
return { init: initModule, state, api };
|
||||
};
|
@ -50,6 +50,7 @@ import { noArrayMerge } from './lib/merge';
|
||||
import type { ModuleFn } from './lib/types';
|
||||
import * as addons from './modules/addons';
|
||||
import * as channel from './modules/channel';
|
||||
import * as testproviders from './modules/experimental_testmodule';
|
||||
import * as globals from './modules/globals';
|
||||
import * as layout from './modules/layout';
|
||||
import * as notifications from './modules/notifications';
|
||||
@ -79,6 +80,7 @@ export type State = layout.SubState &
|
||||
stories.SubState &
|
||||
refs.SubState &
|
||||
notifications.SubState &
|
||||
testproviders.SubState &
|
||||
version.SubState &
|
||||
url.SubState &
|
||||
shortcuts.SubState &
|
||||
@ -98,6 +100,7 @@ export type API = addons.SubAPI &
|
||||
globals.SubAPI &
|
||||
layout.SubAPI &
|
||||
notifications.SubAPI &
|
||||
testproviders.SubAPI &
|
||||
shortcuts.SubAPI &
|
||||
settings.SubAPI &
|
||||
version.SubAPI &
|
||||
@ -178,6 +181,7 @@ class ManagerProvider extends Component<ManagerProviderProps, State> {
|
||||
addons,
|
||||
layout,
|
||||
notifications,
|
||||
testproviders,
|
||||
settings,
|
||||
shortcuts,
|
||||
stories,
|
||||
|
@ -104,18 +104,14 @@ export const SidebarBottomBase = ({
|
||||
const spacerRef = useRef<HTMLDivElement | null>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const [warningsActive, setWarningsActive] = useState(false);
|
||||
const { testProviders } = useStorybookState();
|
||||
const {
|
||||
updateTestproviderState: updateTestProvider,
|
||||
clearTestproviderState,
|
||||
runTestprovider: onRunTests,
|
||||
cancelTestprovider: onCancelTests,
|
||||
} = useStorybookApi();
|
||||
const [errorsActive, setErrorsActive] = useState(false);
|
||||
const [testProviders, setTestProviders] = useState<TestProviders>(() => {
|
||||
let sessionState: TestProviders = {};
|
||||
try {
|
||||
sessionState = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
|
||||
} catch (_) {}
|
||||
return Object.fromEntries(
|
||||
Object.entries(api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER)).map(
|
||||
([id, config]) => [id, { ...config, ...initialTestProviderState, ...sessionState[id] }]
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const warnings = Object.values(status).filter((statusByAddonId) =>
|
||||
Object.values(statusByAddonId).some((value) => value?.status === 'warn')
|
||||
@ -126,45 +122,14 @@ export const SidebarBottomBase = ({
|
||||
const hasWarnings = warnings.length > 0;
|
||||
const hasErrors = errors.length > 0;
|
||||
|
||||
const updateTestProvider = useCallback(
|
||||
(id: TestProviderId, update: Partial<TestProviderState>) =>
|
||||
setTestProviders((state) => {
|
||||
const newValue = { ...state, [id]: { ...state[id], ...update } };
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(newValue));
|
||||
return newValue;
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const clearState = useCallback(
|
||||
({ providerId }: { providerId: TestProviderId }) => {
|
||||
updateTestProvider(providerId, {
|
||||
cancelling: false,
|
||||
running: true,
|
||||
failed: false,
|
||||
crashed: false,
|
||||
progress: undefined,
|
||||
});
|
||||
clearTestproviderState(providerId);
|
||||
api.experimental_updateStatus(providerId, (state = {}) =>
|
||||
Object.fromEntries(Object.keys(state).map((key) => [key, null]))
|
||||
);
|
||||
},
|
||||
[api, updateTestProvider]
|
||||
);
|
||||
|
||||
const onRunTests = useCallback(
|
||||
(id: TestProviderId) => {
|
||||
api.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id });
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
const onCancelTests = useCallback(
|
||||
(id: TestProviderId) => {
|
||||
updateTestProvider(id, { cancelling: true });
|
||||
api.emit(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, { providerId: id });
|
||||
},
|
||||
[api, updateTestProvider]
|
||||
[api, clearTestproviderState]
|
||||
);
|
||||
|
||||
const onSetWatchMode = useCallback(
|
||||
@ -214,14 +179,14 @@ export const SidebarBottomBase = ({
|
||||
}
|
||||
};
|
||||
|
||||
api.getChannel()?.on(TESTING_MODULE_CRASH_REPORT, onCrashReport);
|
||||
api.getChannel()?.on(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
|
||||
api.getChannel()?.on(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
|
||||
api.on(TESTING_MODULE_CRASH_REPORT, onCrashReport);
|
||||
api.on(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
|
||||
api.on(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
|
||||
|
||||
return () => {
|
||||
api.getChannel()?.off(TESTING_MODULE_CRASH_REPORT, onCrashReport);
|
||||
api.getChannel()?.off(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
|
||||
api.getChannel()?.off(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
|
||||
api.off(TESTING_MODULE_CRASH_REPORT, onCrashReport);
|
||||
api.off(TESTING_MODULE_PROGRESS_REPORT, onProgressReport);
|
||||
api.off(TESTING_MODULE_RUN_ALL_REQUEST, clearState);
|
||||
};
|
||||
}, [api, testProviders, updateTestProvider, clearState]);
|
||||
|
||||
|
@ -170,6 +170,14 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
return null;
|
||||
}
|
||||
|
||||
const StatusIconMap = {
|
||||
success: <StatusPassIcon color={theme.color.positive} />,
|
||||
error: <StatusFailIcon color={theme.color.negative} />,
|
||||
warn: <StatusWarnIcon color={theme.color.warning} />,
|
||||
pending: <SyncIcon size={12} color={theme.color.defaultText} />,
|
||||
unknown: null,
|
||||
};
|
||||
|
||||
const id = createId(item.id, refId);
|
||||
if (item.type === 'story' || item.type === 'docs') {
|
||||
const LeafNode = item.type === 'docs' ? DocumentNode : StoryNode;
|
||||
@ -179,6 +187,42 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
|
||||
const statusOrder: API_StatusValue[] = ['success', 'error', 'warn', 'pending', 'unknown'];
|
||||
|
||||
function createLinks(onHide: () => void): Link[] | Link[][] {
|
||||
const elements = api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER);
|
||||
const links: Link[] = Object.entries(elements)
|
||||
.filter(([k, e]) => e.contextMenu)
|
||||
.map(([k, e]) => {
|
||||
const R = e.contextMenu;
|
||||
|
||||
const state = api.getTestproviderState(k);
|
||||
console.log({ R, k, e, s: state });
|
||||
|
||||
return {
|
||||
id: k,
|
||||
content: R && state ? <R context={item} state={state} /> : null,
|
||||
};
|
||||
});
|
||||
|
||||
links.push(
|
||||
...Object.entries(status || {})
|
||||
.sort((a, b) => statusOrder.indexOf(a[1].status) - statusOrder.indexOf(b[1].status))
|
||||
.map(([addonId, value]) => ({
|
||||
id: addonId,
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
'aria-label': `Test status for ${value.title}: ${value.status}`,
|
||||
icon: StatusIconMap[value.status],
|
||||
onClick: () => {
|
||||
onSelectStoryId(item.id);
|
||||
value.onClick?.();
|
||||
onHide();
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
return (
|
||||
<LeafNodeStyleWrapper
|
||||
key={id}
|
||||
@ -220,32 +264,7 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
closeOnTriggerHidden
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
placement="bottom"
|
||||
tooltip={({ onHide }) => (
|
||||
<TooltipLinkList
|
||||
links={Object.entries(status || {})
|
||||
.sort(
|
||||
(a, b) => statusOrder.indexOf(a[1].status) - statusOrder.indexOf(b[1].status)
|
||||
)
|
||||
.map(([addonId, value]) => ({
|
||||
id: addonId,
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
'aria-label': `Test status for ${value.title}: ${value.status}`,
|
||||
icon: {
|
||||
success: <StatusPassIcon color={theme.color.positive} />,
|
||||
error: <StatusFailIcon color={theme.color.negative} />,
|
||||
warn: <StatusWarnIcon color={theme.color.warning} />,
|
||||
pending: <SyncIcon size={12} color={theme.color.defaultText} />,
|
||||
unknown: null,
|
||||
}[value.status],
|
||||
onClick: () => {
|
||||
onSelectStoryId(item.id);
|
||||
value.onClick?.();
|
||||
onHide();
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
tooltip={({ onHide }) => <TooltipLinkList links={createLinks(onHide)} />}
|
||||
>
|
||||
<StatusButton
|
||||
aria-label={`Test status: ${statusValue}`}
|
||||
@ -308,15 +327,21 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
const color = itemStatus ? statusMapping[itemStatus][1] : null;
|
||||
const BranchNode = item.type === 'component' ? ComponentNode : GroupNode;
|
||||
|
||||
const elements = api.getElements(Addon_TypesEnum.experimental_CONTEXT);
|
||||
function createLinks(onHide: () => void): Link[] | Link[][] {
|
||||
const elements = api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER);
|
||||
const links: Link[] = Object.entries(elements)
|
||||
.filter(([k, e]) => e.contextMenu)
|
||||
.map(([k, e]) => {
|
||||
const R = e.contextMenu;
|
||||
|
||||
const createLinks: (onHide: () => void) => ComponentProps<typeof TooltipLinkList>['links'] = (
|
||||
onHide
|
||||
) => {
|
||||
const links: Link[] = Object.entries(elements).map(([k, e]) => ({
|
||||
id: k,
|
||||
content: e.render({ entry: item }),
|
||||
}));
|
||||
const state = api.getTestproviderState(k);
|
||||
console.log({ R, k, e, s: state });
|
||||
|
||||
return {
|
||||
id: k,
|
||||
content: R && state ? <R context={item} state={state} /> : null,
|
||||
};
|
||||
});
|
||||
if (counts.error) {
|
||||
links.push({
|
||||
id: 'errors',
|
||||
@ -344,7 +369,7 @@ const Node = React.memo<NodeProps>(function Node({
|
||||
});
|
||||
}
|
||||
return links;
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<LeafNodeStyleWrapper
|
||||
|
@ -2,7 +2,7 @@
|
||||
import type { FC, PropsWithChildren, ReactElement, ReactNode } from 'react';
|
||||
|
||||
import type { TestingModuleProgressReportProgress } from '../../core-events';
|
||||
import type { Addon } from '../../manager-api';
|
||||
import type { Addon, StoryEntry } from '../../manager-api';
|
||||
import type { RenderData as RouterData } from '../../router/types';
|
||||
import type { ThemeVars } from '../../theming/types';
|
||||
import type { API_SidebarOptions } from './api';
|
||||
@ -327,7 +327,6 @@ export interface Addon_RenderOptions {
|
||||
export type Addon_Type =
|
||||
| Addon_BaseType
|
||||
| Addon_PageType
|
||||
| Addon_ContextType
|
||||
| Addon_WrapperType
|
||||
| Addon_SidebarBottomType
|
||||
| Addon_SidebarTopType
|
||||
@ -429,12 +428,6 @@ export interface Addon_PageType {
|
||||
*/
|
||||
render: FC;
|
||||
}
|
||||
export interface Addon_ContextType {
|
||||
type: Addon_TypesEnum.experimental_CONTEXT;
|
||||
/** The unique id. */
|
||||
id: string;
|
||||
render: FC<{ entry: API_HashEntry }>;
|
||||
}
|
||||
|
||||
export interface Addon_WrapperType {
|
||||
type: Addon_TypesEnum.PREVIEW;
|
||||
@ -482,6 +475,7 @@ export interface Addon_TestProviderType<
|
||||
name: string;
|
||||
title: (state: Addon_TestProviderState<Details>) => ReactNode;
|
||||
description: (state: Addon_TestProviderState<Details>) => ReactNode;
|
||||
contextMenu?: FC<{ context: API_HashEntry; state: Addon_TestProviderState<Details> }>;
|
||||
mapStatusUpdate?: (
|
||||
state: Addon_TestProviderState<Details>
|
||||
) => API_StatusUpdate | ((state: API_StatusState) => API_StatusUpdate);
|
||||
@ -517,7 +511,6 @@ type Addon_TypeBaseNames = Exclude<
|
||||
|
||||
export interface Addon_TypesMapping extends Record<Addon_TypeBaseNames, Addon_BaseType> {
|
||||
[Addon_TypesEnum.PREVIEW]: Addon_WrapperType;
|
||||
[Addon_TypesEnum.experimental_CONTEXT]: Addon_ContextType;
|
||||
[Addon_TypesEnum.experimental_PAGE]: Addon_PageType;
|
||||
[Addon_TypesEnum.experimental_SIDEBAR_BOTTOM]: Addon_SidebarBottomType;
|
||||
[Addon_TypesEnum.experimental_SIDEBAR_TOP]: Addon_SidebarTopType;
|
||||
|
Loading…
x
Reference in New Issue
Block a user