Merge pull request #14684 from storybookjs/test/addon-controls-infer-and-e2e

Controls: Tighten color control inference heuristic and test
This commit is contained in:
Michael Shilman 2021-05-15 09:11:36 +08:00 committed by GitHub
commit 2626146dbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 179 additions and 5 deletions

View File

@ -0,0 +1,49 @@
describe('addon-controls', () => {
it('should change component when changing controls', () => {
cy.visitStorybook();
cy.navigateToStory('example-button', 'Primary');
cy.viewAddonPanel('Controls');
// Text input: Label
cy.getStoryElement().find('button').should('contain.text', 'Button');
cy.get('#label').clear().type('Hello world');
cy.getStoryElement().find('button').should('contain.text', 'Hello world');
// Args in URL
cy.url().should('include', 'args=label:Hello+world');
// Boolean toggle: Primary/secondary
cy.getStoryElement().find('button').should('have.css', 'background-color', 'rgb(30, 167, 253)');
cy.get('#primary').click();
cy.getStoryElement().find('button').should('have.css', 'background-color', 'rgba(0, 0, 0, 0)');
// Color picker: Background color
cy.get('input[placeholder="Choose color"]').type('red');
cy.getStoryElement().find('button').should('have.css', 'background-color', 'rgb(255, 0, 0)');
// TODO: enable this once the controls for size are aligned in all CLI templates.
// Radio buttons: Size
// cy.getStoryElement().find('button').should('have.css', 'font-size', '14px');
// cy.get('label[for="size-large"]').click();
// cy.getStoryElement().find('button').should('have.css', 'font-size', '16px');
// Reset controls: assert that the component is back to original state
cy.get('button[title="Reset controls"]').click();
cy.getStoryElement().find('button').should('have.css', 'font-size', '14px');
cy.getStoryElement().find('button').should('have.css', 'background-color', 'rgb(30, 167, 253)');
cy.getStoryElement().find('button').should('contain.text', 'Button');
});
it('should apply controls automatically when passed via url', () => {
cy.visit('/', {
qs: {
path: '/story/example-button--primary',
args: 'label:Hello world',
},
});
cy.getStoryElement().find('button').should('contain.text', 'Hello world');
});
});

View File

@ -1,17 +1,18 @@
import { html } from 'lit-html';
import { styleMap } from 'lit-html/directives/style-map';
import './button.css';
/**
* Primary UI component for user interaction
*/
export const Button = ({ primary, backgroundColor, size, label, onClick }) => {
export const Button = ({ primary, backgroundColor = null, size, label, onClick }) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return html`
<button
type="button"
class=${['storybook-button', `storybook-button--${size || 'medium'}`, mode].join(' ')}
style=${backgroundColor && { backgroundColor }}
style=${styleMap({ backgroundColor })}
@click=${onClick}
>
${label}

View File

@ -1,4 +1,5 @@
import { html } from 'lit-html';
import { styleMap } from 'lit-html/directives/style-map';
import './button.css';
export interface ButtonProps {
@ -26,14 +27,14 @@ export interface ButtonProps {
/**
* Primary UI component for user interaction
*/
export const Button = ({ primary, backgroundColor, size, label, onClick }: ButtonProps) => {
export const Button = ({ primary, backgroundColor = null, size, label, onClick }: ButtonProps) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return html`
<button
type="button"
class=${['storybook-button', `storybook-button--${size || 'medium'}`, mode].join(' ')}
style=${backgroundColor && { backgroundColor }}
style=${styleMap({ backgroundColor })}
@click=${onClick}
>
${label}

View File

@ -0,0 +1,114 @@
import { StoryContext } from '@storybook/addons';
import { logger } from '@storybook/client-logger';
import { inferControls } from './inferControls';
const getStoryContext = (customParams = {}): StoryContext => ({
id: '',
kind: '',
name: '',
args: {},
globals: {},
argTypes: {},
parameters: {
argTypes: {
label: { control: 'text' },
labelName: { control: 'text' },
borderWidth: { control: { type: 'number', min: 0, max: 10 } },
},
__isArgsStory: true,
...customParams,
},
});
describe('inferControls', () => {
describe('with custom matchers', () => {
let warnSpy: jest.SpyInstance;
beforeEach(() => {
warnSpy = jest.spyOn(logger, 'warn');
warnSpy.mockImplementation(() => {});
});
afterEach(() => {
warnSpy.mockRestore();
});
it('should return color type when matching color', () => {
// passing a string, should return control type color
const inferredControls = inferControls(
getStoryContext({
argTypes: {
background: {
type: {
name: 'string',
value: 'red',
},
name: 'background',
},
},
controls: {
matchers: {
color: /background/,
},
},
})
);
expect(inferredControls.background.control.type).toEqual('color');
});
it('should return inferred type when matches color but arg is not a string', () => {
// passing an object which is unsupported, should infer the type to object
const inferredControls = inferControls(
getStoryContext({
argTypes: {
background: {
type: {
name: 'object',
value: {
rgb: [255, 255, 0],
},
},
name: 'background',
},
},
controls: {
matchers: {
color: /background/,
},
},
})
);
expect(warnSpy).toHaveBeenCalled();
expect(inferredControls.background.control.type).toEqual('object');
});
});
it('should return argTypes as is when no exclude or include is passed', () => {
const controls = inferControls(getStoryContext());
expect(Object.keys(controls)).toEqual(['label', 'labelName', 'borderWidth']);
});
it('should return filtered argTypes when include is passed', () => {
const [includeString, includeArray, includeRegex] = [
inferControls(getStoryContext({ controls: { include: 'label' } })),
inferControls(getStoryContext({ controls: { include: ['label'] } })),
inferControls(getStoryContext({ controls: { include: /label*/ } })),
];
expect(Object.keys(includeString)).toEqual(['label', 'labelName']);
expect(Object.keys(includeArray)).toEqual(['label']);
expect(Object.keys(includeRegex)).toEqual(['label', 'labelName']);
});
it('should return filtered argTypes when exclude is passed', () => {
const [excludeString, excludeArray, excludeRegex] = [
inferControls(getStoryContext({ controls: { exclude: 'label' } })),
inferControls(getStoryContext({ controls: { exclude: ['label'] } })),
inferControls(getStoryContext({ controls: { exclude: /label*/ } })),
];
expect(Object.keys(excludeString)).toEqual(['borderWidth']);
expect(Object.keys(excludeArray)).toEqual(['labelName', 'borderWidth']);
expect(Object.keys(excludeRegex)).toEqual(['borderWidth']);
});
});

View File

@ -1,5 +1,7 @@
import mapValues from 'lodash/mapValues';
import { ArgType } from '@storybook/addons';
import { logger } from '@storybook/client-logger';
import { SBEnumType, ArgTypesEnhancer } from './types';
import { combineParameters } from './parameters';
import { filterArgTypes } from './filterArgTypes';
@ -17,7 +19,14 @@ const inferControl = (argType: ArgType, name: string, matchers: ControlsMatchers
// args that end with background or color e.g. iconColor
if (matchers.color && matchers.color.test(name)) {
return { control: { type: 'color' } };
const controlType = typeof argType.type.value;
if (controlType === 'string') {
return { control: { type: 'color' } };
}
logger.warn(
`Addon controls: Control of type color only supports string, received "${controlType}" instead`
);
}
// args that end with date e.g. purchaseDate