Skip to content

Commit

Permalink
Support client-only tasks globally, and per-story.
Browse files Browse the repository at this point in the history
For projects that don't server render their components, the
server-related data is not helpful.

Clients can opt-in to client-only performance tests by adding:

    import { addParameters } from '@storybook/client-api';

    addParameters({
      performance: {
        clientOnly: true,
      },
    });

to their .storybook/preview.js file.

On a per-story basis, the `clientOnly` parameter can be set alongside
the `interactions` parameter:

    select.story = {
      name: 'React select',
      parameters: {
        performance: {
          interactions: interactionTasks,
          clientOnly: true,
        },
      },
    };

This change removes all service-sid tasks, as well as the "hydrate"
task in from the client-side tasks.
  • Loading branch information
nickpresta committed May 27, 2020
1 parent e1ea11f commit e6b8516
Show file tree
Hide file tree
Showing 13 changed files with 79 additions and 34 deletions.
7 changes: 7 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { addDecorator } from '@storybook/react';
import { addParameters } from '@storybook/client-api';
import { withPerformance } from '../src';

addParameters({
performance: {
clientOnly: false,
},
});

addDecorator(withPerformance);
4 changes: 3 additions & 1 deletion cypress/integration/run-all.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { startAllButtonId } from '../../src/selectors';
import preset from '../../src/tasks/preset';
import getPresets from '../../src/tasks/preset';
import { Task, TaskGroup } from '../../src/types';
import flatten from '../../src/util/flatten';
import { wait } from '../custom/guards';

const preset = getPresets({ clientOnly: false });

describe('run all', () => {
it('be able to run all tasks', () => {
// start the tasks
Expand Down
3 changes: 2 additions & 1 deletion cypress/integration/run-single.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import preset from '../../src/tasks/preset';
import getPresets from '../../src/tasks/preset';
import { Task } from '../../src/types';
import { wait } from '../custom/guards';

const preset = getPresets({ clientOnly: false });
const task: Task = preset[0].tasks[0];

describe('run single', () => {
Expand Down
11 changes: 4 additions & 7 deletions examples/landing.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,24 @@ function SelectExample() {

export const select = () => <SelectExample />;


const interactionTasks: PublicInteractionTask[] = [
{
name: 'Display dropdown',
description: 'Open the dropdown and wait for Option 5 to load',
run: async ({ container, controls }: InteractionTaskArgs): Promise<void> => {
const element: HTMLElement | null = container.querySelector(
'.addon__dropdown-indicator',
);
const element: HTMLElement | null = container.querySelector('.addon__dropdown-indicator');
invariant(element);
fireEvent.mouseDown(element);
await findByText(container, 'Option 5', undefined, { timeout: 20000 });
},
},
]
];

select.story = {
name: 'React select',
parameters: {
performance: {
interactions: interactionTasks
interactions: interactionTasks,
},
},
};
Expand All @@ -55,7 +52,7 @@ export const noInteractions = () => <p>A story with no interactions 👋</p>;

function burnCpu() {
const start = performance.now();
while (performance.now() - start < 200) { }
while (performance.now() - start < 200) {}
}

function Slow() {
Expand Down
2 changes: 1 addition & 1 deletion examples/panel.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function ManagedPanel({ mode }: { mode: 'dark' | 'normal' }) {

return (
<WithStorybookTheme mode={mode}>
<Panel channel={channel} interactions={[]} />
<Panel channel={channel} interactions={[]} clientOnly={false} />
</WithStorybookTheme>
);
}
Expand Down
4 changes: 3 additions & 1 deletion src/decorator/decorator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ export default makeDecorator({
// 'Interactions' need to be provided by consumers
skipIfNoParametersOrOptions: false,
wrapper: (getStory, context, { parameters }) => {
const interactions: PublicInteractionTask[] | undefined = parameters && parameters.interactions;
const interactions: PublicInteractionTask[] = (parameters && parameters.interactions) || [];
const clientOnly: boolean = Boolean(parameters && parameters.clientOnly);

// Sadly need to add cast channel for storybook ts-loader
return (
<TaskHarness
getNode={() => getStory(context)}
channel={addons.getChannel() as any}
interactions={interactions}
clientOnly={clientOnly}
/>
);
},
Expand Down
8 changes: 5 additions & 3 deletions src/decorator/task-harness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,23 @@ import {
} from '../types';
import getElement from '../task-runner/get-element';
import { bindAll } from '../util/bind-channel-events';
import preset from '../tasks/preset';
import getPresets from '../tasks/preset';
import getTaskMap from '../tasks/get-tasks-map';

type Props = {
getNode: () => React.ReactNode;
channel: Channel;
interactions: PublicInteractionTask[] | undefined;
clientOnly: boolean;
};

export default function TaskHarness({ getNode, channel, interactions = [] }: Props) {
export default function TaskHarness({ getNode, channel, interactions = [], clientOnly }: Props) {
const groups: TaskGroup[] = useMemo(
function merge() {
const preset = getPresets({ clientOnly });
return [...preset, getInteractionGroup(interactions)];
},
[interactions],
[interactions, clientOnly],
);
const tasks: TaskMap = useMemo(() => getTaskMap(groups), [groups]);

Expand Down
7 changes: 5 additions & 2 deletions src/panel/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Channel } from '@storybook/channels';
import { styled } from '@storybook/theming';
import React, { useMemo } from 'react';
import { getInteractionGroup } from '../tasks/get-interaction-group';
import preset from '../tasks/preset';
import getPresets from '../tasks/preset';
import { Nullable, PublicInteractionTask, TaskGroup, TaskGroupResult } from '../types';
import machine, { RunContext } from './machine';
import ServiceContext from './service-context';
Expand Down Expand Up @@ -50,17 +50,20 @@ function getResult(group: TaskGroup, context: RunContext): TaskGroupResult {
export default function Panel({
channel,
interactions,
clientOnly,
}: {
channel: Channel;
interactions: PublicInteractionTask[];
clientOnly: boolean;
}) {
const { state, service } = usePanelMachine(machine, channel);

const groups: TaskGroup[] = useMemo(
function merge() {
const preset = getPresets({ clientOnly });
return [...preset, getInteractionGroup(interactions)];
},
[interactions],
[interactions, clientOnly],
);

return (
Expand Down
15 changes: 11 additions & 4 deletions src/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,23 @@ type EnvProps = {
children: (args: {
interactions: PublicInteractionTask[];
channel: Channel;
clientOnly: boolean;
}) => React.ReactElement<any>;
};

// We create a wrapper to supply all of the storybook stuff so
// that we can test and display panel without needing any of it
function Env({ children }: EnvProps) {
const parameters = useParameter(constants.paramKey, { interactions: [] });
const interactions: PublicInteractionTask[] = parameters.interactions;
const parameters = useParameter(constants.paramKey, {
interactions: [],
clientOnly: false,
});
const interactions: PublicInteractionTask[] = parameters.interactions || [];
const clientOnly = Boolean(parameters.clientOnly);

// sadly need to add cast for storybook ts-loader
const channel: Channel = addons.getChannel() as any;
return children({ channel: channel as any, interactions });
return children({ channel: channel as any, interactions, clientOnly });
}

addons.register(constants.addonKey, () => {
Expand All @@ -32,7 +37,9 @@ addons.register(constants.addonKey, () => {
render: ({ active, key }) => (
<AddonPanel active={active} key={key}>
<Env>
{({ interactions, channel }) => <Panel channel={channel} interactions={interactions} />}
{({ interactions, channel, clientOnly }) => (
<Panel channel={channel} interactions={interactions} clientOnly={clientOnly} />
)}
</Env>
</AddonPanel>
),
Expand Down
4 changes: 1 addition & 3 deletions src/task-runner/get-element.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React from 'react';

export default function getElement(
getNode: () => React.ReactNode,
): () => React.ReactElement {
export default function getElement(getNode: () => React.ReactNode): () => React.ReactElement {
return () => <React.Fragment>{getNode()}</React.Fragment>;
}
19 changes: 13 additions & 6 deletions src/tasks/preset/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,17 @@ const domElementCountWithoutSvg: StaticTask = staticTask({
},
});

const group: TaskGroup = {
groupId: 'Client',
name: 'Client 👩‍💻',
tasks: [render, reRender, hydrate, domElementCount, domElementCountWithoutSvg],
};
function getGroup(clientOnly: boolean): TaskGroup {
const include = [];
if (!clientOnly) {
include.push(hydrate);
}
const tasks = [render, reRender, ...include, domElementCount, domElementCountWithoutSvg];
return {
groupId: 'Client',
name: 'Client 👩‍💻',
tasks,
};
}

export default group;
export default getGroup;
9 changes: 5 additions & 4 deletions src/tasks/preset/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import serverSide from './server-side';
import initialMount from './client';
import getGroups from './client';
import { TaskGroup } from '../../types';

const preset: TaskGroup[] = [serverSide, initialMount];

export default preset;
export default function getPresets({ clientOnly = false }: { clientOnly: boolean }): TaskGroup[] {
const clientGroups = getGroups(clientOnly);
return clientOnly ? [clientGroups] : [serverSide, clientGroups];
}
20 changes: 19 additions & 1 deletion test/tasks/run-all.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { runAll } from '../../src/task-runner';
import preset from '../../src/tasks/preset';
import getPresets from '../../src/tasks/preset';
import { Task, TaskGroup, TaskGroupResult, TimedResult } from '../../src/types';
import toResultMap from '../../src/util/to-result-map';
import { StaticResult } from '../../src/types';

it('should only include client tasks', () => {
const clientPresets = getPresets({ clientOnly: true });
const groupNames = clientPresets.map((p) => p.name);
const groupTasks = clientPresets.reduce(
(acc, curr) => acc.concat(curr.tasks.map((t) => t.name)),
[],
);

expect(groupNames).toEqual(['Client 👩‍💻']);
expect(groupTasks).toEqual([
'Initial render',
'Re render',
'DOM element count',
'DOM element count (no nested inline svg elements)',
]);
});

it('should run all the standard tests', async () => {
const preset = getPresets({ clientOnly: false });
const result = await runAll({
groups: preset,
getNode: () => 'hey',
Expand Down

0 comments on commit e6b8516

Please sign in to comment.