diff --git a/addons/contexts/src/manager/components/ToolBar.test.tsx b/addons/contexts/src/manager/components/ToolBar.test.tsx new file mode 100644 index 000000000000..b5a09c187e19 --- /dev/null +++ b/addons/contexts/src/manager/components/ToolBar.test.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { ToolBar } from './ToolBar'; + +describe('Tests on addon-contexts component: ToolBar', () => { + it('should render nothing if receive an empty contextNodes', () => { + // when + const result = shallow(); + + // then + expect(result).toMatchInlineSnapshot(`""`); + }); + + it('should spawn ToolBarControl based on the given contextNodes', () => { + // given + const someContextNodes = [ + { + components: ['span'], + icon: 'box' as const, + nodeId: 'Some Context A', + options: { cancelable: false, deep: false, disable: false }, + params: [{ name: '', props: {} }], + title: 'Some Context A', + }, + { + components: ['div'], + icon: 'box' as const, + nodeId: 'Some Context B', + options: { cancelable: true, deep: false, disable: false }, + params: [{ name: 'Some Param X', props: {} }, { name: 'Some Param Y', props: {} }], + title: 'Some Context B', + }, + ]; + const someSelectionState = { + 'Some Context B': 'Some Param Y', + }; + + // when + const result = shallow( + + ); + + // then + expect(result).toMatchInlineSnapshot(` + + + + + + `); + }); +}); diff --git a/addons/contexts/src/manager/components/ToolBar.tsx b/addons/contexts/src/manager/components/ToolBar.tsx index 0f956275483d..16a04113c843 100644 --- a/addons/contexts/src/manager/components/ToolBar.tsx +++ b/addons/contexts/src/manager/components/ToolBar.tsx @@ -1,12 +1,12 @@ import React, { ComponentProps } from 'react'; import { Separator } from '@storybook/components'; -import { ToolbarControl } from './ToolbarControl'; +import { ToolBarControl } from './ToolBarControl'; import { ContextNode, FCNoChildren, SelectionState } from '../../shared/types.d'; type ToolBar = FCNoChildren<{ nodes: ContextNode[]; state: SelectionState; - setSelected: ComponentProps['setSelected']; + setSelected: ComponentProps['setSelected']; }>; export const ToolBar: ToolBar = React.memo(({ nodes, state, setSelected }) => @@ -14,10 +14,10 @@ export const ToolBar: ToolBar = React.memo(({ nodes, state, setSelected }) => <> {nodes.map(({ components, ...forwardProps }) => ( - ))} diff --git a/addons/contexts/src/manager/components/ToolBarControl.test.tsx b/addons/contexts/src/manager/components/ToolBarControl.test.tsx new file mode 100644 index 000000000000..c0131fd4a47f --- /dev/null +++ b/addons/contexts/src/manager/components/ToolBarControl.test.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { ToolBarControl } from './ToolBarControl'; +import { OPT_OUT } from '../../shared/constants'; + +describe('Tests on addon-contexts component: ToolBarControl', () => { + // given + const someBasicProps = { + icon: 'box' as const, + nodeId: 'Some Context', + options: { cancelable: true, deep: false, disable: false }, + params: [{ name: 'A', props: {} }, { name: 'B', props: {} }], + title: 'Some Context', + selected: '', + setSelected: jest.fn, + }; + + it('should control menu: set as inactive if being out-out (if cancelable)', () => { + // when + const result = shallow(); + + // then + expect(result.props().active).toBe(false); + }); + + it('should control menu: valid "selected" to give "activeName"', () => { + // given + const selected = 'C'; + const anotherSelected = 'B'; + + // when + const result = shallow(); + const anotherResult = shallow( + + ); + + // then + expect(result.props().optionsProps.activeName).not.toBe(selected); + expect(anotherResult.props().optionsProps.activeName).toBe(anotherSelected); + }); + + it('should control menu: fallback "activeName" to the default param', () => { + // given + const name = 'C'; + const params = [...someBasicProps.params, { name, props: {}, default: true }]; + + // when + const result = shallow(); + + // then + expect(result.props().optionsProps.activeName).toBe(name); + }); + + it('should control menu: fallback "activeName" to the first (if default not found)', () => { + // when + const result = shallow(); + + // then + expect(result.props().optionsProps.activeName).toBe(someBasicProps.params[0].name); + }); + + it('should render nothing if being disabled', () => { + // given + const options = { ...someBasicProps.options, disable: true }; + + // when + const result = shallow(); + + // then + expect(result).toMatchInlineSnapshot(`""`); + }); + + it('should document the shallowly rendered result', () => { + // when + const result = shallow(); + + // then + expect(result).toMatchInlineSnapshot(` + + `); + }); +}); diff --git a/addons/contexts/src/manager/components/ToolbarControl.tsx b/addons/contexts/src/manager/components/ToolBarControl.tsx similarity index 93% rename from addons/contexts/src/manager/components/ToolbarControl.tsx rename to addons/contexts/src/manager/components/ToolBarControl.tsx index b173a088ad8f..0fab1e873ed6 100644 --- a/addons/contexts/src/manager/components/ToolbarControl.tsx +++ b/addons/contexts/src/manager/components/ToolBarControl.tsx @@ -3,7 +3,7 @@ import { ToolBarMenu } from './ToolBarMenu'; import { OPT_OUT } from '../../shared/constants'; import { ContextNode, FCNoChildren, Omit } from '../../shared/types.d'; -type ToolbarControl = FCNoChildren< +type ToolBarControl = FCNoChildren< Omit< ContextNode & { selected: string; @@ -13,7 +13,7 @@ type ToolbarControl = FCNoChildren< > >; -export const ToolbarControl: ToolbarControl = ({ +export const ToolBarControl: ToolBarControl = ({ nodeId, icon, title, diff --git a/addons/contexts/src/manager/components/ToolBarMenu.test.tsx b/addons/contexts/src/manager/components/ToolBarMenu.test.tsx new file mode 100644 index 000000000000..dd1e7ef26f4c --- /dev/null +++ b/addons/contexts/src/manager/components/ToolBarMenu.test.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { ToolBarMenu } from './ToolBarMenu'; + +describe('Tests on addon-contexts component: ToolBarMenu', () => { + it('should glue `@storybook/ui` components to produce a context menu', () => { + // given + const someProps = { + icon: 'globe' as const, + title: 'Some Context', + active: true, + expanded: false, + setExpanded: jest.fn, + optionsProps: { + activeName: 'A', + list: ['A', 'B'], + onSelectOption: jest.fn, + }, + }; + + // when + const result = shallow(); + + // then + expect(result).toMatchInlineSnapshot(` + + } + tooltipShown={false} + trigger="click" + > + + + + + `); + }); +}); diff --git a/addons/contexts/src/manager/components/ToolBarMenuOptions.test.tsx b/addons/contexts/src/manager/components/ToolBarMenuOptions.test.tsx new file mode 100644 index 000000000000..d22a88153572 --- /dev/null +++ b/addons/contexts/src/manager/components/ToolBarMenuOptions.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { ToolBarMenuOptions } from './ToolBarMenuOptions'; +import { OPT_OUT } from '../../shared/constants'; + +describe('Tests on addon-contexts component: ToolBarMenuOptions', () => { + it('should glue TooltipLinkList and set the active item correspondingly', () => { + // given + const list = [OPT_OUT, 'A', 'B']; + const activeName = 'B'; + + // when + const result = shallow( + + ); + + // then + expect(result.props().links.length).toBe(list.length); + expect(result.props().links.find((link: any) => link.title === activeName).active).toBe(true); + expect(result).toMatchInlineSnapshot(` + + `); + }); +}); diff --git a/addons/contexts/src/preview/libs/decorators.test.ts b/addons/contexts/src/preview/libs/decorators.test.ts index 7aa5095a1056..81250ec6897c 100644 --- a/addons/contexts/src/preview/libs/decorators.test.ts +++ b/addons/contexts/src/preview/libs/decorators.test.ts @@ -2,16 +2,16 @@ import { memorize, singleton } from './decorators'; describe('Test on functional helpers: memorize', () => { it('should memorize the calculated result', () => { - // setup + // given const someFn = jest.fn(x => [x]); const someFnMemo = memorize(someFn); - // exercise + // when const resultA = someFnMemo(1); const resultB = someFnMemo(2); const resultC = someFnMemo(1); - // assertion + // then expect(someFn).toHaveBeenCalledTimes(2); expect(resultA).toEqual(someFn(1)); expect(resultA).not.toEqual(resultB); @@ -20,16 +20,16 @@ describe('Test on functional helpers: memorize', () => { }); it('should memorize based on the second argument', () => { - // setup + // given const someFn = jest.fn((x, y) => [x, y]); const someFnMemo = memorize(someFn, (x, y) => y); - // exercise + // when const resultA = someFnMemo(1, 2); const resultB = someFnMemo(2, 2); const resultC = someFnMemo(1, 3); - // assertion + // then expect(someFn).toHaveBeenCalledTimes(2); expect(resultA).toEqual(someFn(1, 2)); expect(resultA).toBe(resultB); @@ -40,15 +40,16 @@ describe('Test on functional helpers: memorize', () => { describe('Test on functional helpers: singleton', () => { it('should make a function singleton', () => { + // given const someFn = jest.fn((x, y, z) => [x, y, z]); const someFnSingleton = singleton(someFn); - // exercise + // when const resultA = someFnSingleton(1, 2, 3); const resultB = someFnSingleton(4, 5, 6); const resultC = someFnSingleton(7, 8, 9); - // assertion + // then expect(someFn).toHaveBeenCalledTimes(1); expect(resultA).toEqual(someFn(1, 2, 3)); expect(resultA).toBe(resultB); diff --git a/addons/contexts/src/preview/libs/getContextNodes.test.ts b/addons/contexts/src/preview/libs/getContextNodes.test.ts index e191dd954858..cf6b1c2e20f8 100644 --- a/addons/contexts/src/preview/libs/getContextNodes.test.ts +++ b/addons/contexts/src/preview/libs/getContextNodes.test.ts @@ -2,8 +2,10 @@ import { _getMergedSettings, getContextNodes } from './getContextNodes'; describe('Test on the merging result of a pair of settings', () => { it('should retain the basic structure even receiving empty objects', () => { + // when const result = _getMergedSettings({}, {}); + // then expect(result).toEqual({ components: [], icon: '', @@ -15,7 +17,7 @@ describe('Test on the merging result of a pair of settings', () => { }); it('should correctly merge two settings', () => { - // setup + // given const someTopLevelSettings = { icon: 'box' as const, title: 'Some Context', @@ -37,10 +39,10 @@ describe('Test on the merging result of a pair of settings', () => { }, }; - // exercise + // when const result = _getMergedSettings(someTopLevelSettings, someStoryLevelSettings); - // assertion + // then expect(result).toEqual({ // topLevel over storyLevel nodeId: someTopLevelSettings.title, @@ -63,6 +65,7 @@ describe('Test on the merging result of a pair of settings', () => { describe('Test on reconciliation of settings', () => { it('should have a stable array ordering after normalization', () => { + // when const result = getContextNodes({ // from the topLevel options: [ @@ -96,6 +99,7 @@ describe('Test on reconciliation of settings', () => { ], }); + // then expect(result).toEqual([ { components: ['div'], diff --git a/addons/contexts/src/preview/libs/getPropsMap.test.ts b/addons/contexts/src/preview/libs/getPropsMap.test.ts index c610eca94deb..130cba8df63f 100644 --- a/addons/contexts/src/preview/libs/getPropsMap.test.ts +++ b/addons/contexts/src/preview/libs/getPropsMap.test.ts @@ -37,7 +37,7 @@ describe('Test on behaviors from collecting the propsMap', () => { describe('Test on the integrity of the method to get the propMaps', () => { it('should return the correct propsMap from the specified selectionState', () => { - // setup + // given const someContextNodes = [ { components: ['div'], @@ -69,10 +69,10 @@ describe('Test on the integrity of the method to get the propMaps', () => { 'Another Context': OPT_OUT, // an inconsistent but possible state being introduced via query param }; - // exercise + // when const result = getPropsMap(someContextNodes, someSelectionState); - // assertion + // then expect(result).toEqual({ 'Some Context': { a: 1 }, 'Another Context': { b: 1 }, // not equal to `OPT_OUT` due to the context is not cancelable diff --git a/addons/contexts/src/preview/libs/getRendererFrom.test.ts b/addons/contexts/src/preview/libs/getRendererFrom.test.ts index 53d30d598450..210b54c7733d 100644 --- a/addons/contexts/src/preview/libs/getRendererFrom.test.ts +++ b/addons/contexts/src/preview/libs/getRendererFrom.test.ts @@ -15,32 +15,52 @@ describe('Test on aggregation of a single context', () => { const fakeComponent = () => ''; it('should skip wrapping when being set to disable', () => { + // given const testedProps = {}; const testedOption = { disable: true }; + + // when spiedAggregator([fakeTag, fakeComponent], testedProps, testedOption)(); + + // then expect(h).toHaveBeenCalledTimes(0); }); it('should skip wrapping when props is marked as "OPT_OUT"', () => { + // given const testedProps = OPT_OUT; const testedOption = { cancelable: true }; + + // when spiedAggregator([fakeTag, fakeComponent], testedProps, testedOption)(); + + // then expect(h).toHaveBeenCalledTimes(0); }); it('should wrap components in the stacking order', () => { + // given const testedProps = {}; const testedOption = {}; + + // when spiedAggregator([fakeTag, fakeComponent], testedProps, testedOption)(); + + // then expect(h).toHaveBeenCalledTimes(2); expect(h.mock.calls[0][0]).toBe(fakeComponent); expect(h.mock.calls[1][0]).toBe(fakeTag); }); it('should NOT pass props deeply by default', () => { + // given const testedProps = {}; const testedOption = {}; + + // when spiedAggregator([fakeTag, fakeComponent], testedProps, testedOption)(); + + // then expect(h.mock.calls[0][1]).toBe(null); expect(h.mock.calls[1][1]).toBe(testedProps); }); @@ -56,7 +76,7 @@ describe('Test on aggregation of a single context', () => { describe('Test on aggregation of contexts', () => { it('should aggregate contexts in the stacking order', () => { - // setup + // given const someContextNodes = [ { components: ['div'], @@ -80,10 +100,10 @@ describe('Test on aggregation of contexts', () => { 'Another Context': {}, }; - // exercise + // when getRendererFrom(h)(someContextNodes, propsMap, () => {}); - // assertion + // then expect(h.mock.calls[0][0]).toBe(someContextNodes[1].components[0]); expect(h.mock.calls[1][0]).toBe(someContextNodes[0].components[0]); }); diff --git a/addons/contexts/src/shared/serializers.test.ts b/addons/contexts/src/shared/serializers.test.ts index d348233d6b33..b4cc47a2648f 100644 --- a/addons/contexts/src/shared/serializers.test.ts +++ b/addons/contexts/src/shared/serializers.test.ts @@ -1,20 +1,21 @@ import { deserialize, serialize } from './serializers'; describe('Test on serializers', () => { + // given const someContextsQueryParam = 'CSS Themes=Forests,Languages=Fr'; const someSelectionState = { 'CSS Themes': 'Forests', Languages: 'Fr', }; - it('Should serialize selection state into its string representation', () => { - expect(serialize(null)).toEqual(null); - expect(serialize(someSelectionState)).toEqual(someContextsQueryParam); - }); - it('Should deserialize a string representation into the represented selection state', () => { expect(deserialize('')).toEqual(null); expect(deserialize('An invalid string=')).toEqual(null); expect(deserialize(someContextsQueryParam)).toEqual(someSelectionState); }); + + it('Should serialize selection state into its string representation', () => { + expect(serialize(null)).toEqual(null); + expect(serialize(someSelectionState)).toEqual(someContextsQueryParam); + }); }); diff --git a/addons/contexts/src/shared/serializers.ts b/addons/contexts/src/shared/serializers.ts index 6085502ad204..54b26dde4272 100644 --- a/addons/contexts/src/shared/serializers.ts +++ b/addons/contexts/src/shared/serializers.ts @@ -1,17 +1,5 @@ import { SelectionState } from './types.d'; -/** - * Serialize the selection state in its string representation. - */ -type serialize = (state: ReturnType) => string | null; - -export const serialize: serialize = state => - !state - ? null - : Object.entries(state) - .map(tuple => tuple.join('=')) - .join(','); - /** * Deserialize URL query param into the specified selection state. */ @@ -27,3 +15,15 @@ export const deserialize: deserialize = param => (acc, [nodeId, name]) => (nodeId && name ? { ...acc, [nodeId]: name } : acc), null ); + +/** + * Serialize the selection state in its string representation. + */ +type serialize = (state: ReturnType) => string | null; + +export const serialize: serialize = state => + !state + ? null + : Object.entries(state) + .map(tuple => tuple.join('=')) + .join(','); diff --git a/addons/contexts/src/shared/types.d.ts b/addons/contexts/src/shared/types.d.ts index f827430a7517..e52027bea0a8 100644 --- a/addons/contexts/src/shared/types.d.ts +++ b/addons/contexts/src/shared/types.d.ts @@ -8,7 +8,7 @@ export declare type AnyFunctionReturns = (...arg: any[]) => T; export declare type FCNoChildren

= FunctionComponent<{ children?: never } & P>; export declare type Omit = Pick>; export declare type GenericProp = null | { - [key: string]: unknown; + readonly [key: string]: unknown; }; // interfaces @@ -36,7 +36,7 @@ export declare interface ContextNode extends Required { } export declare interface SelectionState { - readonly [key: string]: string; + readonly [key: string]: string | undefined; } export declare interface PropsMap {