Skip to content

Commit

Permalink
Refactor stories, remove nested RAFs, dev feedback (#43)
Browse files Browse the repository at this point in the history
* Save everything

* fix storybook

* small fixes

* move tests again

* working

* fixing things
  • Loading branch information
roginfarrer authored Apr 11, 2020
1 parent bb2197b commit c14377e
Show file tree
Hide file tree
Showing 30 changed files with 649 additions and 573 deletions.
7 changes: 7 additions & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const webpack = require('webpack');

module.exports = {
stories: ['../stories/**/*.stories.(ts|tsx)'],
addons: [
Expand All @@ -23,6 +25,11 @@ module.exports = {
});

config.resolve.extensions.push('.ts', '.tsx');
config.plugins.push(
new webpack.DefinePlugin({
__DEV__: JSON.stringify(process.env.NODE_ENV),
})
);

return config;
},
Expand Down
3 changes: 3 additions & 0 deletions .storybook/preview-body.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<style>
#root {max-width: 800px;}
</style>
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,14 @@
"@storybook/addon-storysource": "^5.3.17",
"@storybook/addons": "^5.3.12",
"@storybook/react": "^5.3.12",
"@testing-library/jest-dom": "^5.1.1",
"@testing-library/jest-dom": "^5.3.0",
"@testing-library/react": "^9.4.0",
"@testing-library/react-hooks": "^3.2.1",
"@types/jest": "^25.1.2",
"@types/raf": "^3.4.0",
"@types/react": "^16.9.19",
"@types/react-dom": "^16.9.5",
"@types/styled-components": "^5.0.1",
"@types/warning": "^3.0.0",
"babel-loader": "^8.0.6",
"cypress": "^4.2.0",
"cypress-storybook": "^0.1.1",
Expand All @@ -72,16 +71,20 @@
"ts-loader": "^6.2.1",
"tsdx": "^0.12.3",
"tslib": "^1.10.0",
"typescript": "^3.7.5"
"typescript": "^3.7.5",
"webpack": "^4.42.1"
},
"dependencies": {
"raf": "^3.4.1",
"warning": "^4.0.3"
"tiny-warning": "^1.0.3"
},
"jest": {
"setupFilesAfterEnv": [
"<rootDir>/tests/jest.setup.ts"
]
"<rootDir>/src/__tests__/setupTests.ts"
],
"globals": {
"__DEV__": true
}
},
"eslintConfig": {
"extends": [
Expand Down
102 changes: 102 additions & 0 deletions src/__tests__/hooks.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { render } from '@testing-library/react';
import { useEffectAfterMount, useControlledState } from '../hooks';

describe('useEffectAfterMount', () => {
it('does not run callback on first render', () => {
const cb = jest.fn();

// Provide a dependency that changes, so it re-renders
let x = 0;
const { rerender } = renderHook(() => {
x++;
return useEffectAfterMount(cb, [x]);
});

expect(cb).not.toHaveBeenCalled();
rerender();
expect(cb).toHaveBeenCalled();
});
});

describe('useControlledState', () => {
it('should match default snapshot', () => {
const {
result: { current },
} = renderHook(() => useControlledState({}));

expect(current).toMatchInlineSnapshot(`
Array [
false,
[Function],
]
`);
});

it('returns the defaultValue value', () => {
const { result } = renderHook(() =>
useControlledState({ defaultOpen: true })
);

const [value] = result.current;

expect(value).toBe(true);
});

it('setter toggles the value', () => {
const { result } = renderHook(() =>
useControlledState({ defaultOpen: true })
);

expect(result.current[0]).toBe(true);

act(() => {
result.current[1]();
});

expect(result.current[0]).toBe(false);
});

describe('dev feedback', () => {
// Mocking console.warn so it does not log to the console,
// but we can still intercept the message
const originalWarn = console.warn;
let consoleOutput: string[] = [];
const mockWarn = (output: any) => consoleOutput.push(output);
console.warn = jest.fn(mockWarn);

beforeEach(() => (console.warn = mockWarn));
afterEach(() => {
console.warn = originalWarn;
consoleOutput = [];
});

function Foo({ isOpen }: { isOpen?: boolean }) {
useControlledState({ isOpen });
return <div />;
}

it('warns about changing from uncontrolled to controlled', () => {
const { rerender } = render(<Foo />);
rerender(<Foo isOpen />);

expect(consoleOutput[0]).toMatchInlineSnapshot(
`"Warning: useCollapse is changing from uncontrolled to controlled. useCollapse should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the \`isOpen\` prop."`
);
expect(consoleOutput.length).toBe(1);
});

it('warns about changing from controlled to uncontrolled', () => {
// Initially control the value
const { rerender } = render(<Foo isOpen={true} />);
// Then re-render without controlling it
rerender(<Foo isOpen={undefined} />);

expect(consoleOutput[0]).toMatchInlineSnapshot(
`"Warning: useCollapse is changing from controlled to uncontrolled. useCollapse should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the \`isOpen\` prop."`
);
expect(consoleOutput.length).toBe(1);
});
});
});
55 changes: 40 additions & 15 deletions tests/react-collapsed.test.tsx → src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react-hooks';
import { mocked } from 'ts-jest/utils';
import useCollapse from '../src/';
import { getElementHeight } from '../src/utils';
import useCollapse from '../';
import { getElementHeight } from '../utils';
import {
GetTogglePropsShape,
GetCollapsePropsShape,
CollapseConfig,
} from '../src/types';
} from '../types';

const mockedGetElementHeight = mocked(getElementHeight, true);

Expand All @@ -27,7 +27,9 @@ const Collapse: React.FC<{
Toggle
</div>
<div {...getCollapseProps(collapseProps)} data-testid="collapse">
{unmountChildren && mountChildren && <div style={{ height: 400 }} />}
{unmountChildren && mountChildren && (
<div style={{ height: 400 }}>content</div>
)}
</div>
</>
);
Expand Down Expand Up @@ -63,7 +65,7 @@ test('toggleOpen toggles isOpen', () => {
expect(isOpen).toBe(true);
});

test.only('Toggle has expected props when closed (default)', () => {
test('Toggle has expected props when closed (default)', () => {
const { getByTestId } = render(<Collapse />);
const toggle = getByTestId('toggle');
expect(toggle).toHaveAttribute('type', 'button');
Expand Down Expand Up @@ -94,9 +96,9 @@ test('Collapse has expected props when closed (default)', () => {
test('Collapse has expected props when open', () => {
const { getByTestId } = render(<Collapse props={{ defaultOpen: true }} />);
const collapse = getByTestId('collapse');
expect(collapse.getAttribute('id')).toBeDefined();
expect(collapse.getAttribute('aria-hidden')).toBe(null);
expect(collapse.style).not.toEqual(
expect(collapse).toHaveAttribute('id');
expect(collapse).toHaveAttribute('aria-hidden', 'false');
expect(collapse.style).not.toContain(
expect.objectContaining({
display: 'none',
height: '0px',
Expand All @@ -122,7 +124,7 @@ test('Re-render does not modify id', () => {
expect(collapseId).toEqual(collapse.getAttribute('id'));
});

test('clicking the toggle expands the collapse', () => {
test.skip('clicking the toggle expands the collapse', () => {
// Mocked since ref element sizes = :( in jsdom
mockedGetElementHeight.mockReturnValue(400);

Expand All @@ -135,7 +137,7 @@ test('clicking the toggle expands the collapse', () => {
expect(collapse.style.height).toBe('400px');
});

test('clicking the toggle closes the collapse', () => {
test.skip('clicking the toggle closes the collapse', () => {
// Mocked since ref element sizes = :( in jsdom
mockedGetElementHeight.mockReturnValue(0);

Expand All @@ -162,14 +164,37 @@ test('toggle click calls onClick argument with isOpen', () => {

describe('mountChildren', () => {
it('children not rendered when mounted closed', () => {
const { getByTestId } = render(<Collapse />);
const { getByTestId } = render(<Collapse unmountChildren />);
const collapse = getByTestId('collapse');
expect(collapse.textContent).toBe('');
});

it('children not rendered when mounted open', () => {
const { getByTestId } = render(<Collapse props={{ defaultOpen: true }} />);
const collapse = getByTestId('collapse');
expect(collapse.textContent).toBe('content');
it('children rendered when mounted open', () => {
const { queryByText } = render(
<Collapse props={{ defaultOpen: true }} unmountChildren />
);
expect(queryByText('content')).toBeInTheDocument();
});
});

test('warns if using padding on collapse', () => {
// Even though the error is caught, it still gets printed to the console
// so we mock that out to avoid the wall of red text.
jest.spyOn(console, 'error');
// @ts-ignore
console.error.mockImplementation(() => {});

expect(() =>
render(
<Collapse
props={{ defaultOpen: true }}
collapseProps={{ style: { padding: 20 } }}
/>
)
).toThrowErrorMatchingInlineSnapshot(
`"Padding applied to the collapse element in react-collapsed will cause the animation to break, and never end. To fix, apply equivalent padding to the direct descendent of the collapse element."`
);

// @ts-ignore
console.error.mockRestore();
});
10 changes: 10 additions & 0 deletions src/__tests__/mocks/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const actualUtils = require.requireActual('../../src/utils.ts');

Object.defineProperty(window, 'getComputedStyle', {
value: jest.fn(),
});

module.exports = Object.assign(actualUtils, {
// eslint-disable-next-line no-undef
getElementHeight: jest.fn(),
});
File renamed without changes.
2 changes: 1 addition & 1 deletion tests/utils.test.ts → src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getElementHeight, callAll } from '../src/utils';
import { getElementHeight, callAll } from '../utils';

const mockRef = {
current: {
Expand Down
1 change: 1 addition & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare const __DEV__: boolean;
30 changes: 16 additions & 14 deletions src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import warning from 'warning';
import warning from 'tiny-warning';

export function useStateOrProps({
export function useControlledState({
isOpen,
defaultOpen,
}: {
isOpen?: boolean;
defaultOpen?: boolean;
}): [boolean, () => void] {
const [stateIsOpen, setStateIsOpen] = useState<boolean>(defaultOpen || false);
const { current: isControlled } = useRef(typeof isOpen !== 'undefined');
const open = isControlled ? isOpen || false : stateIsOpen;
const initiallyControlled = useRef<boolean>(isOpen != null);
const open = initiallyControlled.current ? isOpen || false : stateIsOpen;
const toggleOpen = useCallback(() => {
if (!isControlled) {
if (!initiallyControlled.current) {
setStateIsOpen(oldOpen => !oldOpen);
}
}, [isControlled]);
}, []);

warning(
!(isControlled && isOpen == null),
'useCollapse is changing from controlled to uncontrolled. useCollapse should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the `isOpen` prop being passed in.'
);
warning(
!(!isControlled && isOpen != null),
'useCollapse is changing from uncontrolled to controlled. useCollapse should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the `isOpen` prop being passed in.'
);
useEffect(() => {
warning(
!(initiallyControlled.current && isOpen == null),
'useCollapse is changing from controlled to uncontrolled. useCollapse should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the `isOpen` prop.'
);
warning(
!(!initiallyControlled.current && isOpen != null),
'useCollapse is changing from uncontrolled to controlled. useCollapse should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the `isOpen` prop.'
);
}, [isOpen]);

return [open, toggleOpen];
}
Expand Down
Loading

0 comments on commit c14377e

Please sign in to comment.