Skip to content

Commit

Permalink
Addon-jest: UI Redesign (#7424)
Browse files Browse the repository at this point in the history
Addon-jest: UI Redesign
  • Loading branch information
shilman committed Jul 20, 2019
2 parents 9702d3a + 214504a commit 81e3996
Show file tree
Hide file tree
Showing 15 changed files with 408 additions and 441 deletions.
1 change: 1 addition & 0 deletions addons/jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"core-js": "^3.0.1",
"global": "^4.3.2",
"react": "^16.8.3",
"react-sizeme": "^2.5.2",
"upath": "^1.1.0",
"util-deprecate": "^1.0.2"
},
Expand Down
6 changes: 0 additions & 6 deletions addons/jest/src/colors.ts

This file was deleted.

37 changes: 0 additions & 37 deletions addons/jest/src/components/Indicator.ts

This file was deleted.

315 changes: 137 additions & 178 deletions addons/jest/src/components/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,205 +1,164 @@
/* eslint-disable react/no-array-index-key */
/* eslint-disable no-param-reassign */
/* eslint-disable no-control-regex */
/* tslint:disable:object-literal-sort-keys */

import React from 'react';
import React, { Fragment } from 'react';
import { styled } from '@storybook/theming';
import colors from '../colors';

const patterns = [/^\x08+/, /^\x1b\[[012]?K/, /^\x1b\[?[\d;]{0,3}/];
const positiveConsoleRegex = /\[32m(.*?)\[39m/;
const negativeConsoleRegex = /\[31m(.*?)\[39m/;
const positiveType = 'positive';
const negativeType = 'negative';
const endToken = '[39m';
const failStartToken = '[31m';
const passStartToken = '[32m';
const stackTraceStartToken = 'at';
const titleEndToken = ':';

const Pre = styled.pre({
margin: 0,
});
type MsgElement = string | JSX.Element;

const Positive = styled.strong({
color: colors.success,
fontWeight: 500,
});
const Negative = styled.strong({
color: colors.error,
fontWeight: 500,
});
class TestDetail {
description: MsgElement[];

interface StackTraceProps {
trace: MsgElement[];
className?: string;
}
result: MsgElement[];

const StackTrace = styled(({ trace, className }: StackTraceProps) => (
<details className={className}>
<summary>Callstack</summary>
{trace
.join('')
.trim()
.split(/\n/)
.map((traceLine, traceLineIndex) => (
<div key={traceLineIndex}>{traceLine.trim()}</div>
))}
</details>
))({
background: '#e2e2e2',
padding: 10,
stackTrace: string;
}
const StackTrace = styled.pre<{}>(({ theme }) => ({
background: theme.color.lighter,
paddingTop: '4px',
paddingBottom: '4px',
paddingLeft: '6px',
borderRadius: '2px',
overflow: 'auto',
margin: '10px 30px 10px 30px',
whiteSpace: 'pre',
}));

const Results = styled.div({
paddingTop: '10px',
marginLeft: '31px',
marginRight: '30px',
});

const Main = styled(({ msg, className }) => <section className={className}>{msg}</section>)({
padding: 10,
borderBottom: '1px solid #e2e2e2',
});

interface SubProps {
msg: MsgElement[];
className?: string;
}
const Description = styled.div<{}>(({ theme }) => ({
paddingBottom: '10px',
paddingTop: '10px',
borderBottom: theme.appBorderColor,
marginLeft: '31px',
marginRight: '30px',
overflowWrap: 'break-word',
}));

const StatusColor = styled.strong<{ status: string }>(({ status, theme }) => ({
color: status === positiveType ? theme.color.positive : theme.color.negative,
fontWeight: 500,
}));

const Sub = styled(({ msg, className }: SubProps) => (
<section className={className}>
{msg
.filter(item => typeof item !== 'string' || item.trim() !== '')
.map((item, index, list) => {
if (typeof item === 'string') {
if (index === 0 && index === list.length - 1) {
return item.trim();
}
if (index === 0) {
return item.replace(/^[\s\n]*/, '');
}
if (index === list.length - 1) {
return item.replace(/[\s\n]*$/, '');
}
}
return item;
})}
</section>
))({
padding: 10,
const Main = styled(({ msg, className }) => <section className={className}>{msg}</section>)({
padding: 5,
});

interface SubgroupOptions {
startTrigger: (e: MsgElement) => boolean;
endTrigger: (e: MsgElement) => boolean;
grouper: (list: MsgElement[], key: number) => JSX.Element;
accList?: MsgElement[];
grouped?: MsgElement[];
grouperIndex?: number;
mode?: 'inject' | 'stop';
injectionPoint?: number;
}

const createSubgroup = ({
startTrigger,
endTrigger,
grouper,
accList = [],
grouped = [],
grouperIndex = 0,
mode,
injectionPoint,
}: SubgroupOptions) => (acc: MsgElement[], item: MsgElement, i: number, list: MsgElement[]) => {
grouperIndex += 1;

// start or stop extraction
if (startTrigger(item)) {
mode = 'inject';
injectionPoint = i;
}
if (endTrigger(item)) {
mode = 'stop';
const colorizeText: (msg: string, type: string) => MsgElement[] = (msg: string, type: string) => {
if (type) {
return msg
.split(type === positiveType ? positiveConsoleRegex : negativeConsoleRegex)
.map((i, index) =>
index % 2 ? (
<StatusColor key={`${type}_${i}`} status={type}>
{i}
</StatusColor>
) : (
i
)
);
}
return [msg];
};

// push item in correct aggregator
if (mode === 'inject') {
grouped.push(item);
} else {
accList.push(item);
}
const getConvertedText: (msg: string) => MsgElement[] = (msg: string) => {
let elementArray: MsgElement[] = [];

if (!msg) return elementArray;

// on last iteration inject at detected injection point, and group
if (i === list.length - 1) {
// Provide a "safety net" when Jest returns a partially recognized "group"
// (recognized by acc.startTrigger but acc.endTrigger was never found) and
// it's the only group in output for a test result. In that case, accList
// will be empty, so return whatever was found, even if it will be unstyled
// and prevent next createSubgroup calls from throwing due to empty lists.
accList.push(null);

return accList.reduce<MsgElement[]>((eacc, el, ei) => {
if (injectionPoint === 0 && ei === 0) {
// at index 0, inject before
return eacc.concat(grouper(grouped, grouperIndex)).concat(el);
const splitText = msg
.split(/\[2m/)
.join('')
.split(/\[22m/);

splitText.forEach(element => {
if (element && element.trim()) {
if (
element.indexOf(failStartToken) > -1 &&
element.indexOf(failStartToken) < element.indexOf(endToken)
) {
elementArray = elementArray.concat(colorizeText(element, negativeType));
} else if (
element.indexOf(passStartToken) > -1 &&
element.indexOf(passStartToken) < element.indexOf(endToken)
) {
elementArray = elementArray.concat(colorizeText(element, positiveType));
} else {
elementArray = elementArray.concat(element);
}
if (injectionPoint > 0 && injectionPoint === ei + 1) {
// at index > 0, and next index WOULD BE injectionPoint, inject after
return eacc.concat(el).concat(grouper(grouped, grouperIndex));
}
});
return elementArray;
};

const getTestDetail: (msg: string) => TestDetail = (msg: string) => {
const lines = msg.split('\n').filter(Boolean);

const testDetail: TestDetail = new TestDetail();
testDetail.description = getConvertedText(lines[0]);
testDetail.stackTrace = '';
testDetail.result = [];

for (let index = 1; index < lines.length; index += 1) {
const current = lines[index];
const next = lines[index + 1];

if (
current
.trim()
.toLowerCase()
.indexOf(stackTraceStartToken) === 0
) {
testDetail.stackTrace += `${current.trim()}\n`;
} else if (current.trim().indexOf(titleEndToken) > -1) {
let title;
let value = null;
if (current.trim().indexOf(titleEndToken) === current.length - 1) {
// there are breaks in the middle of result
title = current.trim();
value = getConvertedText(next);
index += 1;
} else {
// results come in a single line
title = current.substring(0, current.indexOf(titleEndToken)).trim();
value = getConvertedText(current.substring(current.indexOf(titleEndToken), current.length));
}
// do not inject
return eacc.concat(el);
}, []);
testDetail.result = [...testDetail.result, title, ' ', ...value, <br key={index} />];
} else {
// results come in an unexpected format
testDetail.result = [...testDetail.result, ' ', ...getConvertedText(current)];
}
}
return acc;

return testDetail;
};

interface MessageProps {
msg: string;
}

type MsgElement = string | JSX.Element;

const Message = ({ msg }: MessageProps) => {
const data = patterns
.reduce((acc, regex) => acc.replace(regex, ''), msg)
.split(/\[2m/)
.join('')
.split(/\[22m/)
.reduce((acc, item) => acc.concat(item), [] as string[])
.map((item, li) =>
item
.split(/\[32m(.*?)\[39m/)
.map((i, index) => (index % 2 ? <Positive key={`p_${li}_${i}`}>{i}</Positive> : i))
)
.reduce((acc, item) => acc.concat(item))
.map((item, li) =>
typeof item === 'string'
? item
.split(/\[31m(.*?)\[39m/)
.map((i, index) => (index % 2 ? <Negative key={`n_${li}_${i}`}>{i}</Negative> : i))
: item
)
.reduce<MsgElement[]>((acc, item) => acc.concat(item), [])
.reduce(
createSubgroup({
startTrigger: e => typeof e === 'string' && e.indexOf('Error: ') === 0,
endTrigger: e => typeof e === 'string' && Boolean(e.match('Expected ')),
grouper: (list, key) => <Main key={key} msg={list} />,
}),
[]
)
.reduce(
(acc, it) =>
typeof it === 'string' ? acc.concat(it.split(/(at(.|\n)+\d+:\d+\))/)) : acc.concat(it),
[] as MsgElement[]
)
.reduce((acc, item) => acc.concat(item), [] as MsgElement[])
.reduce(
createSubgroup({
startTrigger: e => typeof e === 'string' && e.indexOf('Expected ') !== -1,
endTrigger: e => typeof e === 'string' && Boolean(e.match(/^at/)),
grouper: (list, key) => <Sub key={key} msg={list} />,
}),
[]
)
.reduce(
createSubgroup({
startTrigger: e => typeof e === 'string' && Boolean(e.match(/at(.|\n)+\d+:\d+\)/)),
endTrigger: () => false,
grouper: (list, key) => <StackTrace key={key} trace={list} />,
}),
[]
);

return <Pre>{data}</Pre>;
export const Message = (props: any) => {
const { msg } = props;

const detail: TestDetail = getTestDetail(msg);
return (
<Fragment>
{detail.description ? <Description>{detail.description}</Description> : null}
{detail.result ? <Results>{detail.result}</Results> : null}
{detail.stackTrace ? <StackTrace>{detail.stackTrace}</StackTrace> : null}
</Fragment>
);
};

export default Message;
Loading

1 comment on commit 81e3996

@vercel
Copy link

@vercel vercel bot commented on 81e3996 Jul 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.