Skip to content

Commit

Permalink
fix: Disable fixture actions when fixture path is invalid (#911)
Browse files Browse the repository at this point in the history
  • Loading branch information
ovidiuch committed Dec 23, 2018
1 parent 3eee5a6 commit 30629e8
Show file tree
Hide file tree
Showing 25 changed files with 452 additions and 116 deletions.
2 changes: 2 additions & 0 deletions .jest/setup-framework.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'jest-dom/extend-expect';

// Some end to end server tests can be very slow...
// Especially slow are the server tests which test webpack compilation
jest.setTimeout(180000); // 3min
3 changes: 2 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ We need a decent parity with the existing Cosmos UI to test JSX fixtures. This i
- [x] Persist state
- [x] Responsive mode
- [x] Style
- [ ] Style minimalistic renderer blank state
- [ ] Fixture blank state
- [ ] Fixture error state
- [x] JSX decorators

**Not** included in the first beta release: The Fixture Editor. This is because it'll be replaced by something _much_ more powerful (a new Control Panel). But this section will have arrive later, after the core of Cosmos Next has been thoroughly tested.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,31 @@
import React, { Component } from 'react';
import styled from 'styled-components';
import { PluginsConsumer } from 'react-plugin';
import { getPrimaryRendererState } from '../Renderer/selectors';
import { PropsState } from './PropsState';

import type { RendererId } from 'react-cosmos-shared2/renderer';
import type { ComponentFixtureState } from 'react-cosmos-shared2/fixtureState';
import type { UrlParams } from '../Router';
import type { RendererState } from '../Renderer';
import type { RendererState, RendererItemState } from '../Renderer';

type Props = {
webUrl: null | string,
urlParams: UrlParams,
rendererState: RendererState,
primaryRendererState: null | RendererItemState,
setComponentsFixtureState: (components: ComponentFixtureState[]) => void,
selectPrimaryRenderer: (rendererId: RendererId) => void
};

export class ControlPanel extends Component<Props> {
render() {
const { webUrl, urlParams, rendererState } = this.props;
const {
webUrl,
urlParams,
rendererState,
primaryRendererState
} = this.props;
const { primaryRendererId, renderers } = rendererState;
const primaryRendererState = getPrimaryRendererState(rendererState);

if (!primaryRendererState) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function register() {
webUrl,
urlParams,
rendererState,
primaryRendererState: callMethod('renderer.getPrimaryRendererState'),
setComponentsFixtureState: components => {
callMethod('renderer.setFixtureState', fixtureState => ({
...fixtureState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ import type { UrlParams } from '../Router';

type Props = {
urlParams: UrlParams,
setUrlParams: UrlParams => void
setUrlParams: UrlParams => void,
isFixturePathValid: (fixturePath: string) => boolean
};

// TODO: Improve UX of refresh button, which seems like it's not doing anything
export function FixtureHeader({ urlParams, setUrlParams }: Props) {
export function FixtureHeader({
urlParams,
setUrlParams,
isFixturePathValid
}: Props) {
const { fixturePath, fullScreen } = urlParams;

if (fullScreen) {
Expand All @@ -35,6 +40,25 @@ export function FixtureHeader({ urlParams, setUrlParams }: Props) {
);
}

if (!isFixturePathValid(fixturePath)) {
return (
<Container>
<Left>
<BlankMessage>Fixture not found</BlankMessage>
<Button
icon={<XCircleIcon />}
label="close"
onClick={() => setUrlParams({})}
/>
</Left>
<Right>
<Slot name="fixtureActions" />
<Button disabled icon={<MaximizeIcon />} label="fullscreen" />
</Right>
</Container>
);
}

return (
<Container>
<Left>
Expand Down Expand Up @@ -87,6 +111,6 @@ const Right = styled.div`
`;

const BlankMessage = styled.span`
margin-left: 4px;
margin: 0 4px;
color: var(--grey3);
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// @flow

import React from 'react';
import delay from 'delay';
import { render } from 'react-testing-library';
import { Slot, loadPlugins } from 'react-plugin';
import {
cleanup,
mockState,
mockMethod,
mockPlug
} from '../../../testHelpers/plugin';
import { register } from '..';

afterEach(cleanup);

function registerTestPlugins() {
register();
mockState('router', { urlParams: { fixturePath: 'foo', fullScreen: true } });
mockMethod('renderer.isFixturePathValid', () => true);
}

function loadTestPlugins() {
loadPlugins();

return render(<Slot name="fixtureHeader" />);
}

it('does not render close button', async () => {
registerTestPlugins();
mockPlug({ slotName: 'fixtureActions', render: 'pluggable actions' });
const { queryByText } = loadTestPlugins();

// Make sure the element doesn't appear async in the next event loops
await delay(100);
expect(queryByText(/close/i)).toBeNull();
});

it('does not render "fixtureActions" slot', async () => {
registerTestPlugins();
mockPlug({ slotName: 'fixtureActions', render: 'pluggable actions' });
const { queryByText } = loadTestPlugins();

// Make sure the element doesn't appear async in the next event loops
await delay(100);
expect(queryByText(/pluggable actions/i)).toBeNull();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @flow

import React from 'react';
import { render, fireEvent, waitForElement } from 'react-testing-library';
import { Slot, loadPlugins } from 'react-plugin';
import { cleanup, mockState, mockMethod } from '../../../testHelpers/plugin';
import { register } from '..';

afterEach(cleanup);

function registerTestPlugins(handleSetUrlParams = () => {}) {
register();
mockState('router', { urlParams: { fixturePath: 'foo' } });
mockMethod('router.setUrlParams', handleSetUrlParams);
mockMethod('renderer.isFixturePathValid', () => false);
}

function loadTestPlugins() {
loadPlugins();

return render(<Slot name="fixtureHeader" />);
}

it('renders missing state message', async () => {
registerTestPlugins();
const { getByText } = loadTestPlugins();

await waitForElement(() => getByText(/fixture not found/i));
});

it('renders close button', async () => {
const handleSetUrlParams = jest.fn();
registerTestPlugins(handleSetUrlParams);

const { getByText } = loadTestPlugins();
fireEvent.click(getByText(/close/));

expect(handleSetUrlParams).toBeCalledWith(expect.any(Object), {});
});

it('renders disabled fullscreen button', async () => {
registerTestPlugins();
const { getByText } = loadTestPlugins();

expect(getByText(/fullscreen/i)).toHaveAttribute('disabled');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// @flow

import React from 'react';
import { render, waitForElement } from 'react-testing-library';
import { Slot, loadPlugins } from 'react-plugin';
import { cleanup, mockState, mockMethod } from '../../../testHelpers/plugin';
import { register } from '..';

afterEach(cleanup);

function registerTestPlugins() {
register();
mockState('router', { urlParams: {} });
mockMethod('renderer.isFixturePathValid', () => false);
}

function loadTestPlugins() {
loadPlugins();

return render(<Slot name="fixtureHeader" />);
}

it('renders blank state message', async () => {
registerTestPlugins();
const { getByText } = loadTestPlugins();

await waitForElement(() => getByText(/no fixture selected/i));
});

it('renders disabled fullscreen button', async () => {
registerTestPlugins();
const { getByText } = loadTestPlugins();

expect(getByText(/fullscreen/i)).toHaveAttribute('disabled');
});
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
// @flow

import React from 'react';
import delay from 'delay';
import { render, fireEvent, waitForElement } from 'react-testing-library';
import { Slot, loadPlugins } from 'react-plugin';
import {
cleanup,
mockState,
mockMethod,
mockPlug
} from '../../testHelpers/plugin';
import { register } from '.';
} from '../../../testHelpers/plugin';
import { register } from '..';

afterEach(cleanup);

function registerTestPlugins({ urlParams = {} } = {}) {
function registerTestPlugins(handleSetUrlParams = () => {}) {
register();
mockState('router', { urlParams });
mockState('router', { urlParams: { fixturePath: 'foo' } });
mockMethod('router.setUrlParams', handleSetUrlParams);
mockMethod('renderer.isFixturePathValid', () => true);
}

function loadTestPlugins() {
Expand All @@ -25,26 +26,9 @@ function loadTestPlugins() {
return render(<Slot name="fixtureHeader" />);
}

it('renders blank state', async () => {
registerTestPlugins();
const { getByText } = loadTestPlugins();
await waitForElement(() => getByText(/no fixture selected/i));
});

it('does not render in fullscreen mode', async () => {
registerTestPlugins({ urlParams: { fixturePath: 'foo', fullScreen: true } });
const { queryByText } = loadTestPlugins();

// Make sure the element doesn't appear async in the next event loops
await delay(100);
expect(queryByText(/no fixture selected/i)).toBeNull();
});

it('renders close button', async () => {
registerTestPlugins({ urlParams: { fixturePath: 'foo' } });

const handleSetUrlParams = jest.fn();
mockMethod('router.setUrlParams', handleSetUrlParams);
registerTestPlugins(handleSetUrlParams);

const { getByText } = loadTestPlugins();
fireEvent.click(getByText(/close/));
Expand All @@ -53,10 +37,8 @@ it('renders close button', async () => {
});

it('renders refresh button', async () => {
registerTestPlugins({ urlParams: { fixturePath: 'foo' } });

const handleSetUrlParams = jest.fn();
mockMethod('router.setUrlParams', handleSetUrlParams);
registerTestPlugins(handleSetUrlParams);

const { getByText } = loadTestPlugins();
fireEvent.click(getByText(/refresh/));
Expand All @@ -67,10 +49,8 @@ it('renders refresh button', async () => {
});

it('renders fullscreen button', async () => {
registerTestPlugins({ urlParams: { fixturePath: 'foo' } });

const handleSetUrlParams = jest.fn();
mockMethod('router.setUrlParams', handleSetUrlParams);
registerTestPlugins(handleSetUrlParams);

const { getByText } = loadTestPlugins();
fireEvent.click(getByText(/fullscreen/));
Expand All @@ -82,7 +62,7 @@ it('renders fullscreen button', async () => {
});

it('renders "fixtureActions" slot', async () => {
registerTestPlugins({ urlParams: { fixturePath: 'foo' } });
registerTestPlugins();
mockPlug({ slotName: 'fixtureActions', render: 'pluggable actions' });

const { getByText } = loadTestPlugins();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { ComponentType, Node } from 'react';
// into the "fixtureActions" slot.
export function createFixtureAction<T>(
BtnComponent: ComponentType<T>
): ComponentType<{ ...T, children: Node }> {
): ComponentType<T & { children: Node }> {
return ({ children, ...otherProps }: { ...T, children: Node }) => (
<>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ export function register() {

return {
urlParams,
setUrlParams: newUrlParams => {
callMethod('router.setUrlParams', newUrlParams);
}
setUrlParams: newUrlParams =>
callMethod('router.setUrlParams', newUrlParams),
isFixturePathValid: fixturePath =>
callMethod('renderer.isFixturePathValid', fixturePath)
};
}
});
Expand Down
5 changes: 1 addition & 4 deletions packages/react-cosmos-playground2/src/plugins/Nav/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
// @flow

import { registerPlugin } from 'react-plugin';
import { getPrimaryRendererState } from '../Renderer/selectors';
import { Nav } from './Nav';

import type { CoreConfig } from '../Core';
import type { RendererState } from '../Renderer';
import type { RouterState } from '../Router';

export function register() {
Expand All @@ -16,14 +14,13 @@ export function register() {
render: Nav,
getProps: ({ getConfigOf, getStateOf, callMethod }) => {
const { projectId, fixturesDir }: CoreConfig = getConfigOf('core');
const rendererState: RendererState = getStateOf('renderer');
const { urlParams }: RouterState = getStateOf('router');

return {
projectId,
fixturesDir,
urlParams,
primaryRendererState: getPrimaryRendererState(rendererState),
primaryRendererState: callMethod('renderer.getPrimaryRendererState'),
setUrlParams: newUrlParams => {
callMethod('router.setUrlParams', newUrlParams);
},
Expand Down
Loading

0 comments on commit 30629e8

Please sign in to comment.