Skip to content

Commit

Permalink
Merge pull request #53 from Lelchik/storybook-addon
Browse files Browse the repository at this point in the history
Storybook addon
  • Loading branch information
wKich committed Sep 11, 2020
2 parents 8bd7660 + d8d787c commit 50f1ee4
Show file tree
Hide file tree
Showing 23 changed files with 951 additions and 8 deletions.
1 change: 1 addition & 0 deletions .storybook/main.ts
Expand Up @@ -2,6 +2,7 @@ import { Configuration, DefinePlugin } from 'webpack';

export default {
stories: ['../stories/**/*.stories.tsx'],
addons: ['./register.js'],
webpackFinal(config: Configuration) {
config.resolve.extensions = ['.tsx', '.ts', '.jsx', '.js'];

Expand Down
1 change: 1 addition & 0 deletions .storybook/register.js
@@ -0,0 +1 @@
require('./../src/addon/register.tsx');
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -50,7 +50,9 @@
"@babel/parser": "^7.10.5",
"@babel/traverse": "^7.10.5",
"@babel/types": "^7.10.5",
"@storybook/components": "^6.0.21",
"@storybook/csf": "^0.0.1",
"@storybook/theming": "^6.0.21",
"@types/chai": "^4.2.12",
"@types/selenium-webdriver": "^4.0.9",
"airbnb-js-shims": "^2.2.1",
Expand Down
56 changes: 56 additions & 0 deletions src/addon/Addon.tsx
@@ -0,0 +1,56 @@
import React, { useState, Fragment, useCallback } from 'react';
import { withCreeveyTests } from './utils';
import { Test, isDefined, TestStatus } from '../types';
import { Tabs } from '@storybook/components';
import { ResultsPage } from './ResultsPage';

interface PanelProps {
statuses: Test[];
}

const PanelInternal = ({ statuses }: PanelProps): JSX.Element => {
const [selectedItem, setSelectedItem] = useState(0);
const browsers = statuses
.map((x) => x.path)
.filter(isDefined)
.map((x) => x[0]);

const handleBrowserChange = useCallback((id) => setSelectedItem(Number(id)), []);
const result = statuses[selectedItem];

return (
<Fragment>
<Tabs selected={`${selectedItem}`} actions={{ onSelect: handleBrowserChange }}>
{browsers.map((x, i) => (
<div key={x} id={`${i}`} title={`${x} ${getEmogyByTestStatus(result.status, result.skip)}`} />
))}
</Tabs>
{statuses.length ? (
<ResultsPage id={result.id} path={result.path} results={result.results} approved={result.approved} />
) : null}
</Fragment>
);
};

function getEmogyByTestStatus(status: TestStatus | undefined, skip: string | boolean): string {
switch (status) {
case 'failed': {
return '❌';
}
case 'success': {
return '✔';
}
case 'running': {
return '🟡';
}
case 'pending': {
return '🕗';
}
default: {
if (skip) return '⏸';
return '';
}
}
}

export const Panel = withCreeveyTests(PanelInternal);
45 changes: 45 additions & 0 deletions src/addon/ImageViews/BlendView.tsx
@@ -0,0 +1,45 @@
import React from 'react';
import { ViewProps, borderColors } from './ImagesView';
import { styled } from '@storybook/theming';

const Container = styled.div({
margin: '20px',
position: 'relative',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'flex-start',
filter: 'invert(100%)',
});

const ImageContainer = styled.div({
position: 'absolute',
width: '100%',
height: '100%',
});

const Image = styled.img<{ borderColor: string }>(({ borderColor }) => ({
border: `1px solid ${borderColor}`,
maxWidth: '100%',
filter: 'invert(100%)',
}));

const DiffImage = styled.img({
maxWidth: '100%',
opacity: '0',
border: '1px solid transparent',
});

export function BlendView({ actual, diff, expect }: ViewProps): JSX.Element {
const colors: ViewProps = borderColors;
return (
<Container>
<ImageContainer>
<Image borderColor={colors.expect} alt="actual" src={actual} />
</ImageContainer>
<ImageContainer>
<Image borderColor={colors.actual} style={{ mixBlendMode: 'difference' }} alt="expect" src={expect} />
</ImageContainer>
<DiffImage alt="diff" src={diff} />
</Container>
);
}
78 changes: 78 additions & 0 deletions src/addon/ImageViews/ImagesView.tsx
@@ -0,0 +1,78 @@
import React, { FunctionComponent } from 'react';
import { SideBySideView } from './SideBySideView';
import { SwapView } from './SwapView';
import { SlideView } from './SlideView';
import { BlendView } from './BlendView';
import { Images, ImagesViewMode } from './../../types';
import { styled, Theme, withTheme } from '@storybook/theming';

export const borderColors: ViewProps = {
actual: '#d9472b',
expect: '#419d14',
diff: '#1d85d0',
};

export interface ViewProps {
actual: string;
diff: string;
expect: string;
}

export interface ViewPropsWithTheme extends ViewProps {
theme: Theme;
}

interface ImagesViewProps {
url: string;
image: Images;
canApprove: boolean;
mode: ImagesViewMode;
}

const views: { [mode in ImagesViewMode]: FunctionComponent<ViewProps> } = {
'side-by-side': SideBySideView,
swap: SwapView,
slide: SlideView,
blend: BlendView,
};

const Container = withTheme(
styled.div(({ theme }) => ({
background: theme.background.content,
height: '100%',
display: 'flex',
textAlign: 'center',
alignItems: 'center',
justifyContent: 'center',
})),
);

const ActualImage = styled.img({
border: `1px solid ${borderColors.expect}`,
maxWidth: '100%',
});

export function ImagesView({ url, image, canApprove, mode }: ImagesViewProps): JSX.Element {
const ViewComponent = views[mode];

const { actual, diff, expect } = image;

return (
<Container>
{canApprove && diff && expect ? (
<ViewComponent actual={`${url}/${actual}`} diff={`${url}/${diff}`} expect={`${url}/${expect}`} />
) : (
<a
style={{
margin: '20px',
}}
href={`${url}/${actual}`}
target="_blank"
rel="noopener noreferrer"
>
<ActualImage alt="actual" src={`${url}/${actual}`} />
</a>
)}
</Container>
);
}
45 changes: 45 additions & 0 deletions src/addon/ImageViews/SideBySideView.tsx
@@ -0,0 +1,45 @@
import React from 'react';
import { ViewProps, borderColors } from './ImagesView';
import { styled } from '@storybook/theming';

const Container = styled.div({
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'space-evenly',
alignItems: 'center',
height: '100%',
margin: '20px 0',
});

const ImageContainer = styled.a({
margin: '0 10px',

'&::first-of-type': {
marginLeft: '20px',
},

'&::last-of-type': {
marginRight: '20px',
},
});

const Image = styled.img<{ borderColor: string }>(({ borderColor }) => ({
border: `1px solid ${borderColor}`,
maxWidth: '100%',
}));

export function SideBySideView({ actual, diff, expect }: ViewProps): JSX.Element {
return (
<Container>
<ImageContainer href={expect} target="_blank" rel="noopener noreferrer">
<Image borderColor={borderColors.expect} alt="expect" src={expect} />
</ImageContainer>
<ImageContainer href={diff} target="_blank" rel="noopener noreferrer">
<Image borderColor={borderColors.diff} alt="diff" src={diff} />
</ImageContainer>
<ImageContainer href={actual} target="_blank" rel="noopener noreferrer">
<Image borderColor={borderColors.actual} alt="actual" src={actual} />
</ImageContainer>
</Container>
);
}
101 changes: 101 additions & 0 deletions src/addon/ImageViews/SlideView.tsx
@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import { styled } from '@storybook/theming';
import { borderColors, ViewProps } from './ImagesView';

const Container = styled.div({
position: 'relative',
margin: '20px',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'flex-start',
});

const Input = styled.input({
position: 'absolute',
cursor: 'ew-resize',
background: 'none',
boxShadow: 'none',
outline: 'none',
height: '100%',
width: '100%',
margin: '0',
zIndex: 1,
appearance: 'none',

'&::-webkit-slider-runnable-track': {
height: '100%',
},
'&::-webkit-slider-thumb': {
boxShadow: '0 0 0 0.5px #888',
height: '100%',
width: '0px',
appearance: 'none',
},

'&::-moz-focus-outer': {
border: '0',
},
'&::-moz-range-track': {
height: '0',
},
'&::-moz-range-thumb': {
border: 'none',
boxShadow: '0 0 0 0.5px #888',
height: '100%',
width: '0px',
},
});

const ImageContainer = styled.div({
position: 'absolute',
width: '100%',
height: '100%',
overflow: 'hidden',
});

const ImageWrapper = styled.div({
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'flex-start',
});

const Image = styled.img<{ borderColor: string }>(({ borderColor }) => ({
border: `1px solid ${borderColor}`,
maxWidth: '100%',
}));

const DiffImage = styled.img({
maxWidth: '100%',
opacity: '0',
border: '1px solid transparent',
});

export const SlideView = ({ actual, diff, expect }: ViewProps): JSX.Element => {
const [step, setStep] = useState(0);
const [offset, setOffset] = useState(0);

const handleImageLoad = (event: React.SyntheticEvent<HTMLImageElement>): void =>
setStep(100 / (event.currentTarget.width + 2));

const handleSlide = (event: React.ChangeEvent<HTMLInputElement>): void => setOffset(Number(event.target.value));

return (
<Container>
<Input type="range" min={0} max={100} step={step} defaultValue={offset} onChange={handleSlide} />
<ImageContainer>
<ImageWrapper>
<Image alt="actual" src={actual} borderColor={borderColors.actual} />
</ImageWrapper>
</ImageContainer>
<ImageContainer style={{ right: `${100 - offset}%` }}>
<ImageWrapper style={{ left: `${100 - offset}%` }}>
<Image alt="expect" src={expect} borderColor={borderColors.expect} />
</ImageWrapper>
</ImageContainer>
<DiffImage alt="diff" src={diff} onLoad={handleImageLoad} />
</Container>
);
};
45 changes: 45 additions & 0 deletions src/addon/ImageViews/SwapView.tsx
@@ -0,0 +1,45 @@
import React, { useState } from 'react';
import { ViewProps, borderColors } from './ImagesView';
import { styled } from '@storybook/theming';

type ImageState = keyof typeof borderColors;

const Container = styled.div({
margin: '20px',
position: 'relative',
});

const Button = styled.button({
position: 'absolute',
width: '100%',
height: '100%',
appearance: 'none',
background: 'none',
color: 'inherit',
border: 'none',
padding: '0',
font: 'inherit',
cursor: 'pointer',
outline: 'none',
zIndex: 1,
});

const Image = styled.img<{ borderColor: string }>(({ borderColor }) => ({
maxWidth: '100%',
border: `1px solid ${borderColor}`,
}));

export function SwapView(props: ViewProps): JSX.Element {
const [image, setImage] = useState<ImageState>('actual');

const handleChangeView = (): void => setImage(image == 'actual' ? 'expect' : 'actual');
const colors: ViewProps = borderColors;
return (
<Container>
<Button onClick={handleChangeView}>
<Image borderColor={colors[image]} alt={image} src={props[image]} />
</Button>
<Image borderColor={'transparent'} style={{ opacity: 0 }} alt="diff" src={props.diff} />
</Container>
);
}

0 comments on commit 50f1ee4

Please sign in to comment.