Unverified Commit bce8f76b authored by Tamika Tannis's avatar Tamika Tannis Committed by GitHub

Establish Unit Tests (#87)

* Cleanup test configs + initial tests for js/components/common

* Lint

* Add a common test-setup

* Some test cleanup

* Lint fix plus add test script

* Fixing configs and TS errors

* Update doc
parent bb6b9d7f
......@@ -20,6 +20,7 @@ matrix:
- npm install
script:
- npm run build
- npm run test
deploy:
provider: pypi
user: amundsen-dev
......@@ -29,4 +30,4 @@ deploy:
on:
tags: true
repo: lyft/amundsenfrontendlibrary
condition: $IS_DEPLOYABLE = true
\ No newline at end of file
condition: $IS_DEPLOYABLE = true
module.exports = {
coverageThreshold: {
'./js/components': {
branches: 10, // 75
functions: 10, // 75
lines: 10, // 75
statements: 10, // 75
},
'./js/ducks': {
branches: 0, // 75
functions: 0, // 75
lines: 0, // 75
statements: 0, // 75
},
},
roots: [
'<rootDir>/js',
],
setupFiles: [
'<rootDir>/test-setup.ts',
],
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.js$': 'babel-jest',
'^.+\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(j|t)sx?$',
testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(j|t)sx?$',
moduleFileExtensions: [
'ts',
'tsx',
......
import * as React from 'react';
import Avatar from 'react-avatar';
import { Link, NavLink } from 'react-router-dom';
import { Link, NavLink, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withRouter } from 'react-router-dom'
import AppConfig from '../../../config/config';
import { GlobalState } from "../../ducks/rootReducer";
......
......@@ -108,7 +108,7 @@ class ProfilePage extends React.Component<ProfilePageProps, ProfilePageState> {
<div className="profile-title">
<h1>{ user.display_name }</h1>
{
(user.is_active === false) &&
(!user.is_active) &&
<Flag caseType="sentenceCase" labelStyle="label-danger" text="Alumni"/>
}
</div>
......
import * as React from 'react';
import * as Adapter from 'enzyme-adapter-react-16';
import { configure, mount, shallow } from 'enzyme';
configure({ adapter: new Adapter() });
import SearchPage from '../../SearchPage';
describe('SearchPage', () => {
const executeSearch = jest.fn();
const testResults = [
{
database: "test_db",
description: "This is the description.",
key: "test_key",
last_updated: 1527283287,
name: "test_table",
schema_name: "test_schema",
cluster: "gold"
},
{
database: "test_db2",
description: "This is the description.",
key: "test_key2",
last_updated: 1527283287,
name: "test_table2",
schema_name: "test_schema2",
cluster: "gold"
}
];
let testProps;
beforeEach(() => {
testProps = {
executeSearch,
getPopularTables: jest.fn(),
searchResults: [],
searchTerm: '',
pageIndex: 0,
popularTables: [],
totalResults: 0,
};
});
it('renders search results correctly', () => {
testProps['searchResults'] = testResults;
testProps['totalResults'] = 2;
testProps['searchTerm'] = 'test';
const wrapper = shallow<SearchPage>(<SearchPage {...testProps} />);
// @ts-ignore
expect(wrapper).toMatchSnapshot();
});
it('renders popular tables correctly', () => {
testProps['popularTables'] = testResults;
const wrapper = shallow<SearchPage>(<SearchPage {...testProps} />);
// @ts-ignore
expect(wrapper).toMatchSnapshot();
});
it('renders no results message', () => {
testProps['searchTerm'] = 'thisisnotarealsearch';
const wrapper = shallow<SearchPage>(<SearchPage {...testProps} />);
// @ts-ignore
expect(wrapper).toMatchSnapshot();
});
it('renders pageindex out of bounds message', () => {
testProps['searchResults'] = testResults;
testProps['totalResults'] = 2;
testProps['pageIndex'] = 50;
const wrapper = shallow<SearchPage>(<SearchPage {...testProps} />);
// @ts-ignore
expect(wrapper).toMatchSnapshot();
});
it('executes page change', () => {
testProps['searchTerm'] = 'test';
const wrapper = mount<SearchPage>(<SearchPage {...testProps} />);
wrapper.instance().handlePageChange(2);
expect(executeSearch).toHaveBeenCalledWith('test', 1);
});
});
......@@ -225,7 +225,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
)
}
let title =`${startIndex}-${Math.min(endIndex, total_results)} of ${total_results} results`;
const title =`${startIndex}-${Math.min(endIndex, total_results)} of ${total_results} results`;
return (
<div className="search-list-container">
<div className="search-list-header">
......
......@@ -12,10 +12,10 @@ export interface AvatarLabelProps {
const AvatarLabel: React.SFC<AvatarLabelProps> = ({ label, src }) => {
return (
<div className='avatar-label-component'>
<div className='component-avatar'>
<div id='component-avatar' className='component-avatar'>
<Avatar name={label} src={src} size={24} round={true} />
</div>
<label className='component-label'>{label}</label>
<label id='component-label' className='component-label'>{label}</label>
</div>
);
};
......
import * as React from 'react';
import { mount, shallow } from 'enzyme';
import { Avatar } from 'react-avatar';
import AvatarLabel, { AvatarLabelProps } from '../';
describe('AvatarLabel', () => {
let props: AvatarLabelProps;
let subject;
beforeEach(() => {
props = {
label: 'testLabel',
src: 'testSrc',
};
subject = shallow(<AvatarLabel {...props} />);
});
describe('render', () => {
it('renders Avatar with correct props', () => {
/* Note: subject.find(Avatar) does not work - workaround is to directly check the content */
const expectedContent = <Avatar name={props.label} src={props.src} size={24} round={true} />;
expect(subject.find('#component-avatar').props().children).toEqual(expectedContent);
});
it('renders label with correct text', () => {
expect(subject.find('#component-label').text()).toEqual(props.label);
});
});
});
......@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import './styles.scss';
interface BreadcrumbProps {
export interface BreadcrumbProps {
path: string;
text: string;
}
......
import * as React from 'react';
import { shallow } from 'enzyme';
import { Link } from 'react-router-dom';
import Breadcrumb, { BreadcrumbProps } from '../';
describe('Breadcrumb', () => {
let props: BreadcrumbProps;
let subject;
beforeEach(() => {
props = {
path: 'testPath',
text: 'testText',
};
subject = shallow(<Breadcrumb {...props} />);
});
describe('render', () => {
it('renders Link with correct path', () => {
expect(subject.find(Link).props()).toMatchObject({
to: props.path,
});
});
it('renders button with correct text within the Link', () => {
expect(subject.find(Link).find('button').text()).toEqual(props.text);
});
});
});
import * as React from 'react';
import ReactDOM from 'react-dom';
import { Overlay, Popover } from 'react-bootstrap';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
interface ConfirmDeleteButtonProps {
onConfirmHandler: (event: React.SyntheticEvent<any>) => any;
popoverTitle?: string;
}
interface ConfirmDeleteButtonState {
showPopover: boolean;
}
class ConfirmDeleteButton extends React.Component<ConfirmDeleteButtonProps, ConfirmDeleteButtonState> {
private buttonRef: React.RefObject<HTMLButtonElement>;
public static defaultProps: ConfirmDeleteButtonProps = {
onConfirmHandler: () => {},
popoverTitle: 'Confirm Delete?',
};
constructor(props) {
super(props);
this.state = {
showPopover: false,
}
this.buttonRef = React.createRef();
this.cancelDelete = this.cancelDelete.bind(this);
this.confirmDelete = this.confirmDelete.bind(this);
this.triggerPopup = this.triggerPopup.bind(this);
this.getTarget = this.getTarget.bind(this);
}
cancelDelete() {
this.setState({showPopover: false});
}
confirmDelete(event) {
this.props.onConfirmHandler(event);
this.setState({showPopover: false});
}
triggerPopup() {
this.setState({showPopover: true});
}
getTarget() {
return ReactDOM.findDOMNode(this.buttonRef.current);
}
render() {
/*
TODO Internationalization: need confirm/delete icons to replace "Yes/No"
in order to minimize harcoded English text
TODO Accessibility: The popover interaction will need to be revisited for
accesibility, interactive content currently cannot be reached via keyboard.
https://reactjs.org/docs/accessibility.html
*/
return (
<div className='confirm-delete-button'>
<button
className='delete-button'
aria-label='delete'
onClick={this.triggerPopup}
ref={this.buttonRef}
>
<span aria-hidden='true'>&times;</span>
</button>
<Overlay
placement='top'
show={this.state.showPopover}
target={this.getTarget}
>
<Popover title={this.props.popoverTitle}>
<button aria-label='cancel' onClick={this.confirmDelete}>Yes</button>
<button aria-label='confirm' onClick={this.cancelDelete}>No</button>
</Popover>
</Overlay>
</div>
);
}
}
export default ConfirmDeleteButton;
@import 'variables';
.delete-button {
color: $text-medium;
height: 24px;
width: 24px;
margin: auto;
font-size: 20px;
font-weight: 700;
line-height: 1;
padding: 0;
cursor: pointer;
border: 0;
}
.delete-button button:hover,
.delete-button button:focus {
color: $brand-color-4;
opacity: 1;
}
.popover-content {
padding: 0px;
}
.popover-content button {
height: 24px;
width: 50%;
font-size: 12px;
border-radius: 3px;
border: none;
background-color: $gray-darker;
color: $body-bg;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
outline: none;
}
.popover-content button:hover,
.popover-content button:focus {
color: $text-medium;
}
......@@ -20,7 +20,7 @@ export interface ComponentProps {
value?: string;
}
type EditableTextProps = ComponentProps & DispatchFromProps & StateFromProps;
export type EditableTextProps = ComponentProps & DispatchFromProps & StateFromProps;
interface EditableTextState {
editable: boolean;
......@@ -112,14 +112,14 @@ class EditableText extends React.Component<EditableTextProps, EditableTextState>
render() {
if (!this.state.editable) {
return (
<div className='editable-container'>
<div className='editable-text'>{ this.state.value }</div>
<div id='editable-container' className='editable-container'>
<div id='editable-text' className='editable-text'>{ this.state.value }</div>
</div>
);
}
if (!this.state.inEditMode || (this.state.inEditMode && this.state.isDisabled)) {
return (
<div className='editable-container'>
<div id='editable-container' className='editable-container'>
<Overlay
placement='top'
show={this.state.isDisabled}
......@@ -132,9 +132,9 @@ class EditableText extends React.Component<EditableTextProps, EditableTextState>
</div>
</Tooltip>
</Overlay>
<div className={"editable-text"}>
<div id='editable-text' className={"editable-text"}>
{ this.state.value }
<a className={ "edit-link " + (this.state.value ? "" : "no-value") }
<a className={ "edit-link" + (this.state.value ? "" : " no-value") }
href="JavaScript:void(0)"
onClick={ this.enterEditMode }
ref={ anchor => {
......@@ -151,9 +151,9 @@ class EditableText extends React.Component<EditableTextProps, EditableTextState>
}
return (
<div className='editable-container'>
<div id='editable-container' className='editable-container'>
<textarea
id='textAreaInput'
id='editable-textarea'
className='editable-textarea'
rows={2}
maxLength={this.props.maxLength}
......
import * as React from 'react';
import { shallow } from 'enzyme';
import { Overlay, Popover, Tooltip } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import EditableText, { EditableTextProps } from '../';
describe('EditableText', () => {
let props: EditableTextProps;
let subject;
beforeEach(() => {
props = {
editable: true,
maxLength: 250,
onSubmitValue: jest.fn(),
getLatestValue: jest.fn(),
refreshValue: 'newValue',
value: 'currentValue',
};
subject = shallow(<EditableText {...props} />);
});
describe('render', () => {
it('renders value in a div if not editable', () => {
props.editable = false;
/* Note: Do not copy this pattern, for some reason setProps is not updating the content in this case */
subject = shallow(<EditableText {...props} />);
expect(subject.find('div#editable-text').text()).toEqual(props.value);
});
describe('renders correctly if !this.state.inEditMode', () => {
beforeEach(() => {
subject.setState({ inEditMode: false });
});
it('renders value as first child', () => {
expect(subject.find('#editable-text').props().children[0]).toEqual(props.value);
});
it('renders edit link to enterEditMode', () => {
expect(subject.find('#editable-text').find('a').props().onClick).toEqual(subject.instance().enterEditMode);
});
it('renders edit link with correct class if state.value exists', () => {
expect(subject.find('#editable-text').find('a').props().className).toEqual('edit-link');
});
it('renders edit link with correct text if state.value exists', () => {
expect(subject.find('#editable-text').find('a').text()).toEqual('edit');
});
it('renders edit link with correct class if state.value does not exist', () => {
subject.setState({ value: null });
expect(subject.find('#editable-text').find('a').props().className).toEqual('edit-link no-value');
});
it('renders edit link with correct text if state.value does not exist', () => {
subject.setState({ value: null });
expect(subject.find('#editable-text').find('a').text()).toEqual('Add Description');
});
});
/* TODO: The use of ReactDOM.findDOMNode is difficult to test, preventing further coverage
describe('renders correctly if this.state.inEditMode && this.state.isDisabled', () => {
beforeEach(() => {
subject.setState({ inEditMode: true, isDisabled: true });
});
it('renders value as first child', () => {
expect(subject.find('#editable-text').props().children[0]).toEqual(props.value);
});
it('renders edit link to enterEditMode', () => {
expect(subject.find('#editable-text').find('a').props().onClick).toEqual(subject.instance().enterEditMode);
});
it('renders edit link with correct class if state.value exists', () => {
expect(subject.find('#editable-text').find('a').props().className).toEqual('edit-link');
});
it('renders edit link with correct text if state.value exists', () => {
expect(subject.find('#editable-text').find('a').text()).toEqual('edit');
});
it('renders edit link with correct class if state.value does not exist', () => {
subject.setState({ value: null });
expect(subject.find('#editable-text').find('a').props().className).toEqual('edit-link no-value');
});
it('renders edit link with correct text if state.value does not exist', () => {
subject.setState({ value: null });
expect(subject.find('#editable-text').find('a').text()).toEqual('Add Description');
});
// TODO: Test Overlay & Tooltip
});
*/
// TODO: Test rendering of textarea with Overlay & Tooltip
});
// TODO: Test component methods
});
......@@ -40,7 +40,7 @@ class EntityCardSection extends React.Component<EntityCardSectionProps, EntityCa
return (
<div className="entity-card-section">
<div className="content-header">
<div className="title">
<div id="section-title" className="title">
{ this.props.title.toUpperCase() }
{
this.props.infoText &&
......@@ -52,7 +52,7 @@ class EntityCardSection extends React.Component<EntityCardSectionProps, EntityCa
}
</div>
</div>
<div className="content">
<div id="section-content" className="content">
{
this.props.contentRenderer(this.state.readOnly)
}
......
import * as React from 'react';
import { shallow } from 'enzyme';
import InfoButton from "../../../InfoButton";
import EntityCardSection, { EntityCardSectionProps } from '../';
describe('EntityCardSection', () => {
let props: EntityCardSectionProps;
let subject;
beforeEach(() => {
props = {
title: 'Title',
contentRenderer: jest.fn(() => (<div>HI!</div>)),
isEditable: true,
};
subject = shallow(<EntityCardSection {...props} />);
});
describe('render', () => {
it('renders the title', () => {
expect(subject.find('.title').text()).toEqual('TITLE');
});
it('renders InfoButton w/ correct props if props.infoText', () => {
props.infoText = 'Here is some info';
subject.setProps(props);
expect(subject.find(InfoButton).props()).toMatchObject({
infoText: props.infoText,
placement: 'top',
size: 'small',
});
});
it('renders button to toggle edit mode if props.isEditable', () => {
expect(subject.find('button').props().onClick).toEqual(subject.instance().toggleEditMode);
});
it('renders with correct class if state.readOnly', () => {
subject.setState({ readOnly: true });
expect(subject.find('button').props().className).toEqual('btn icon edit-button');
});
it('renders with correct class if !state.readOnly', () => {
subject.setState({ readOnly: false });
expect(subject.find('button').props().className).toEqual('btn active-edit-button');
});
it('renders with expected content', () => {
expect(subject.find('#section-content').props().children).toEqual(<div>HI!</div>);
});
});
describe('toggleEditMode', () => {
const mockBlur = jest.fn();
beforeEach(() => {
subject.instance().editButton = { current: { blur: mockBlur } };
});
it('negates state.readOnly if props.isEditable', () => {
subject.setState({ readOnly: true });
subject.instance().toggleEditMode();
expect(subject.instance().state.readOnly).toEqual(false);
});
it('does not update state.readyOnly if !props.isEditable', () => {
props.isEditable = false;
subject.setProps(props);
subject.setState({ readOnly: true });
subject.instance().toggleEditMode();
expect(subject.instance().state.readOnly).toEqual(true);
});
it('calls blur on editButton', () => {
subject.instance().toggleEditMode();
expect(mockBlur).toHaveBeenCalled();
});
});
});
......@@ -4,8 +4,8 @@ import EntityCardSection, { EntityCardSectionProps } from './EntityCardSection';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
interface EntityCardProps {
sections?: EntityCardSectionProps[];
export interface EntityCardProps {
sections: EntityCardSectionProps[];
}
const EntityCard: React.SFC<EntityCardProps> = ({ sections }) => {
......
import * as React from 'react';
import { shallow } from 'enzyme';
import EntityCard, { EntityCardProps } from '../';
import EntityCardSection from '../EntityCardSection';
describe('EntityCard', () => {
let props: EntityCardProps;
let subject;
beforeEach(() => {
props = {
sections: [
{
title: 'Title',
infoText: 'Here is some info',
contentRenderer: jest.fn(),
isEditable: true,
},
{
title: 'Title2',
infoText: 'Here is some other info',
contentRenderer: jest.fn(),
isEditable: false,
}
]
};
subject = shallow(<EntityCard {...props} />);
});
describe('render', () => {
it('renders EntityCardSections', () => {
expect(subject.find(EntityCardSection).length).toEqual(2);
});
it('passes correct props to EntityCardSection', () => {
expect(subject.find(EntityCardSection).at(0).props()).toMatchObject({
title: props.sections[0].title,
infoText: props.sections[0].infoText,
contentRenderer: props.sections[0].contentRenderer,
isEditable: props.sections[0].isEditable,
});
});
});
});
......@@ -3,19 +3,19 @@ import * as React from 'react';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
enum CaseType {
export enum CaseType {
LOWER_CASE = 'lowerCase',
SENTENCE_CASE = 'sentenceCase',
UPPER_CASE = 'upperCase',
}
interface FlagProps {
export interface FlagProps {
caseType?: string | null;
text: string;
labelStyle?: string;
}
function convertText(str: string, caseType: string): string {
export function convertText(str: string, caseType: string): string {
switch (caseType) {
case CaseType.LOWER_CASE:
return str.toLowerCase();
......
import * as React from 'react';
import { shallow } from 'enzyme';
import Flag, { CaseType, FlagProps, convertText } from '../';
describe('Flag', () => {
let props: FlagProps;
let subject;
beforeEach(() => {
props = {
text: 'Testing',
};
subject = shallow(<Flag {...props} />);
});
describe('render', () => {
it('renders span with correct default className', () => {
expect(subject.find('span').props().className).toEqual('flag label label-default');
});
it('renders span with correct custom className', () => {
props.labelStyle = 'primary';
subject.setProps(props);
expect(subject.find('span').props().className).toEqual('flag label label-primary');
});
it('renders span with correct text', () => {
expect(subject.find('span').text()).toEqual(props.text);
});
});
describe('convertText', () => {
let text;
beforeEach(() => {
text = 'RandOM teXT';
});
it('returns lowercase text if caseType=CaseType.LOWER_CASE', () => {
expect(convertText(text, CaseType.LOWER_CASE)).toEqual('random text');
});
it('returns UPPERCASE text if caseType=CaseType.UPPER_CASE', () => {
expect(convertText(text, CaseType.UPPER_CASE)).toEqual('RANDOM TEXT');
});
it('returns Sentence case text if caseType=CaseType.SENTENCE_CASE', () => {
expect(convertText(text, CaseType.SENTENCE_CASE)).toEqual('Random text');
});
it('returns text in defauilt case', () => {
expect(convertText(text, 'not a valid options')).toEqual(text);
});
});
});
......@@ -4,7 +4,7 @@ import { OverlayTrigger, Popover } from 'react-bootstrap';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
interface InfoButtonProps {
export interface InfoButtonProps {
infoText?: string;
title?: string;
placement?: string;
......
import * as React from 'react';
import { shallow } from 'enzyme';
import { OverlayTrigger, Popover } from 'react-bootstrap';
import InfoButton, { InfoButtonProps } from '../';
describe('InfoButton', () => {
let props: InfoButtonProps;
let subject;
beforeEach(() => {
props = {
infoText: 'Some info text to share',
title: 'Popover Title',
placement: 'left',
size: 'size',
};
subject = shallow(<InfoButton {...props} />);
});
describe('render', () => {
it('renders OverlayTrigger w/ correct placement', () => {
expect(subject.find(OverlayTrigger).props().placement).toEqual(props.placement);
});
it('renders OverlayTrigger w/ correct Popover', () => {
const expectedPopover = (
<Popover id="popover-trigger-hover-focus" title={ props.title }>
{ props.infoText }
</Popover>
);
expect(subject.find(OverlayTrigger).props().overlay).toEqual(expectedPopover);
});
it('renders OverlayTrigger w/ correct placement', () => {
expect(subject.find(OverlayTrigger).find('button').props().className).toEqual(`btn icon info-button ${props.size}`);
});
});
});
import * as React from 'react';
import { shallow } from 'enzyme';
import LoadingSpinner from '../';
describe('LoadingSpinner', () => {
let subject;
beforeEach(() => {
subject = shallow(<LoadingSpinner />);
});
describe('render', () => {
it('renders img with props', () => {
expect(subject.find('img').props()).toMatchObject({
alt: 'loading...',
className: 'loading-spinner',
src: '/static/images/loading_spinner.gif',
});
});
});
});
......@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import { LoggingParams, TableResource} from '../types';
interface TableListItemProps {
export interface TableListItemProps {
table: TableResource;
logging: LoggingParams;
}
......@@ -13,6 +13,12 @@ class TableListItem extends React.Component<TableListItemProps, {}> {
super(props);
}
getDateLabel = () => {
const { table } = this.props;
const dateTokens = new Date(table.last_updated_epoch * 1000).toDateString().split(' ');
return `${dateTokens[1]} ${dateTokens[2]}, ${dateTokens[3]}`;
};
getLink = () => {
const { table, logging } = this.props;
return `/table_detail/${table.cluster}/${table.database}/${table.schema_name}/${table.name}`
......@@ -21,10 +27,7 @@ class TableListItem extends React.Component<TableListItemProps, {}> {
render() {
const { table } = this.props;
const hasLastUpdated = !!table.last_updated_epoch;
const dateTokens = new Date(table.last_updated_epoch * 1000).toDateString().split(' ');
const dateLabel = `${dateTokens[1]} ${dateTokens[2]}, ${dateTokens[3]}`;
return (
<li className="list-group-item">
......@@ -32,8 +35,8 @@ class TableListItem extends React.Component<TableListItemProps, {}> {
<img className="icon icon-database icon-color" />
<div className="content">
<div className={ hasLastUpdated? "col-sm-9 col-md-10" : "col-sm-12"}>
<div className="main-title truncated">{ `${table.schema_name}.${table.name}`}</div>
<div className="description truncated">{ table.description }</div>
<div id="main-title" className="main-title truncated">{ `${table.schema_name}.${table.name}`}</div>
<div id="main-description" className="description truncated">{ table.description }</div>
</div>
{/*<div className={ hasLastUpdated? "hidden-xs col-sm-3 col-md-4" : "hidden-xs col-sm-6"}>*/}
{/*<div className="secondary-title">Frequent Users</div>*/}
......@@ -44,9 +47,9 @@ class TableListItem extends React.Component<TableListItemProps, {}> {
{
hasLastUpdated &&
<div className="hidden-xs col-sm-3 col-md-2">
<div className="secondary-title">Latest Data</div>
<div className="description truncated">
{ dateLabel }
<div id="secondary-title" className="secondary-title">Latest Data</div>
<div id="secondary-description" className="description truncated">
{ this.getDateLabel() }
</div>
</div>
}
......
import * as React from 'react';
import { shallow } from 'enzyme';
import Avatar from 'react-avatar';
import Flag from '../../../Flag';
import { Link } from 'react-router-dom';
import TableListItem, { TableListItemProps } from '../';
import { ResourceType } from '../../types';
describe('TableListItem', () => {
let props: TableListItemProps;
let subject;
beforeEach(() => {
props = {
logging: { source: 'src', index: 0 },
table: {
type: ResourceType.table,
cluster: '',
database: '',
description: '',
key: '',
last_updated_epoch: null,
name: '',
schema_name: '',
},
}
subject = shallow(<TableListItem {...props} />);
});
describe('render', () => {
it('renders item as Link', () => {
expect(subject.find(Link).exists()).toBeTruthy();
});
it('renders correct text in main-title', () => {
const { table } = props;
expect(subject.find('#main-title').text()).toEqual(`${table.schema_name}.${table.name}`);
});
it('renders main-description', () => {
const { table } = props;
expect(subject.find('#main-description').text()).toEqual(table.description);
});
it('renders secondary-description w/ getDateLabel value if table has last_updated_epoch ', () => {
subject.instance().getDateLabel = jest.fn(() => 'Mar 28, 2019')
props.table.last_updated_epoch = 1553829681;
subject.setProps(props);
expect(subject.find('#secondary-description').text()).toEqual('Mar 28, 2019');
});
});
describe('getDateLabel', () => {
it('getDateLabel returns correct string', () => {
props.table.last_updated_epoch = 1553829681;
subject.setProps(props);
/* Note: Jest will convert date to UTC, expect to see different strings for an epoch value in the tests vs UI */
expect(subject.instance().getDateLabel()).toEqual('Mar 29, 2019');
});
});
describe('getLink', () => {
it('getLink returns correct string', () => {
const { table, logging } = props;
expect(subject.instance().getLink()).toEqual(`/table_detail/${table.cluster}/${table.database}/${table.schema_name}/${table.name}?index=${logging.index}&source=${logging.source}`);
});
});
});
......@@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
import { LoggingParams, UserResource} from '../types';
import Flag from '../../Flag';
interface UserListItemProps {
export interface UserListItemProps {
user: UserResource;
logging: LoggingParams;
}
......@@ -28,20 +28,20 @@ class UserListItem extends React.Component<UserListItemProps, {}> {
<Avatar name={ user.name } size={ 24 } round={ true } />
<div className="content">
<div className="col-xs-12 col-sm-6">
<div className="main-title">
<div id="main-title" className="main-title">
{ user.name }
{
!user.active &&
<Flag text="Alumni" labelStyle='danger' />
}
</div>
<div className="description">
<div id="main-description" className="description">
{ `${user.role} on ${user.team_name}` }
</div>
</div>
<div className="hidden-xs col-sm-6">
<div className="secondary-title">Frequently Uses</div>
<div className="description truncated">
<div id="secondary-title" className="secondary-title">Frequently Uses</div>
<div id="secondary-description" className="description truncated">
{ /*TODO Fill this with a real value*/ }
<label>{ user.title }</label>
</div>
......
import * as React from 'react';
import { shallow } from 'enzyme';
import Avatar from 'react-avatar';
import Flag from '../../../Flag';
import { Link } from 'react-router-dom';
import UserListItem, { UserListItemProps } from '../';
import { ResourceType } from '../../types';
describe('UserListItem', () => {
let props: UserListItemProps;
let subject;
beforeEach(() => {
props = {
logging: { source: 'src', index: 0 },
user: {
type: ResourceType.user,
active: true,
birthday: null,
department: 'Department',
email: 'test@test.com',
first_name: '',
github_username: '',
id: 0,
last_name: '',
manager_email: '',
name: 'Test Tester',
offboarded: true,
office: '',
role: '',
start_date: '',
team_name: '',
title: '',
},
}
subject = shallow(<UserListItem {...props} />);
});
describe('render', () => {
it('renders item as Link', () => {
expect(subject.find(Link).exists()).toBeTruthy();
});
/* TODO (ttannis): Avatar tests wont pass
it('renders Avatar', () => {
expect(subject.find(Link).find(Avatar).exists()).toBeTruthy();
});*/
it('renders user.name in title', () => {
expect(subject.find('#main-title').text()).toEqual(props.user.name);
});
it('renders Alumni flag if user not active', () => {
props.user.active = false;
subject.setProps(props);
expect(subject.find('#main-title').find(Flag).exists()).toBeTruthy();
});
it('renders description', () => {
const { user } = props;
expect(subject.find('#main-description').text()).toEqual(`${user.role} on ${user.team_name}`);
});
});
describe('getLink', () => {
it('getLink returns correct string', () => {
const { user, logging } = props;
expect(subject.instance().getLink()).toEqual(`/user/${user.id}/?index=${logging.index}&source=${logging.source}`);
});
});
});
......@@ -6,8 +6,7 @@ import UserListItem from './UserListItem';
import './styles.scss';
interface ListItemProps {
export interface ListItemProps {
logging: LoggingParams;
item: Resource;
}
......
import * as React from 'react';
import { shallow } from 'enzyme';
import TableListItem from '../TableListItem';
import UserListItem from '../UserListItem';
import ResourceListItem, { ListItemProps } from '../';
import { ResourceType } from '../types';
describe('ResourceListItem', () => {
let props: ListItemProps;
let subject;
beforeEach(() => {
props = {
logging: { source: 'src', index: 0 },
item: { type: ResourceType.table },
}
subject = shallow(<ResourceListItem {...props} />);
});
describe('render', () => {
it('renders TableListItem with correct props', () => {
expect(subject.find(TableListItem).props()).toMatchObject({
logging: props.logging,
table: props.item,
});
});
it('renders UserListItem with correct props', () => {
props.item.type = ResourceType.user;
subject.setProps(props);
expect(subject.find(UserListItem).props()).toMatchObject({
logging: props.logging,
user: props.item,
});
});
it('renders nothing if invalid props.item.type', () => {
// @ts-ignore
props.item.type = 'not a valid type';
subject.setProps(props);
expect(subject.props().children).toBeFalsy();
});
});
});
import * as React from 'react';
import { shallow } from 'enzyme';
import { Tab, Tabs } from 'react-bootstrap';
import TabsComponent, { TabsProps } from '../';
describe('Tabs', () => {
let props: TabsProps;
let tabContent: JSX.Element;
let subject;
beforeEach(() => {
tabContent = (<div>I am content</div>);
props = {
activeKey: 'activeTab',
defaultTab: 'defaultTab',
onSelect: jest.fn(),
tabs: [
{
content: tabContent,
key: 'defaultTab',
title: 'Tabby Tab',
},
{
content: tabContent,
key: 'defaultTab2',
title: 'Tabby Tab2',
},
],
}
subject = shallow(<TabsComponent {...props} />);
});
describe('render', () => {
it('renders Tabs with correct props', () => {
expect(subject.find(Tabs).props()).toMatchObject({
id: 'tab',
className: 'tabs-component',
defaultActiveKey: props.defaultTab,
activeKey: props.activeKey,
onSelect: props.onSelect,
});
});
it('renders Tab for each props.tabs', () => {
expect(subject.find(Tab)).toHaveLength(2);
});
it('passes correct props to Tab', () => {
expect(subject.find(Tab).at(0).props()).toMatchObject({
eventKey: props.tabs[0].key,
title: props.tabs[0].title,
});
});
it('passes correct content to Tab', () => {
expect(subject.find(Tab).at(0).props().children).toEqual(tabContent);
});
});
});
import { createStore } from 'redux';
import rootReducer from '../reducer';
import search, { EXECUTE_SEARCH } from '../search';
import popularTables, { GET_POPULAR_TABLES } from '../popularTables';
describe('test root reducer', () => {
let store;
let mockResults;
beforeEach(() => {
mockResults = [{
key: 'test_key',
name: 'test_table',
description: 'This is a test',
database: 'test_db',
schema_name: 'test_schema',
}];
store = createStore(rootReducer);
});
it('matches expected default state', () => {
expect(store.getState().search.pageIndex).toEqual(search(undefined, {}).pageIndex);
expect(store.getState().popularTables).toEqual(popularTables(undefined, {}));
expect(store.getState().search.searchResults).toEqual(search(undefined, {}).searchResults);
expect(store.getState().search.searchTerm).toEqual(search(undefined, {}).searchTerm);
expect(store.getState().search.totalResults).toEqual(search(undefined, {}).totalResults);
});
it('verifies app state after EXECUTE_SEARCH action', () => {
const payload = {
data: {
page_index: 1,
results: mockResults,
search_term: 'test',
total_results: 1,
},
};
const action = { type: EXECUTE_SEARCH, payload };
store.dispatch(action);
expect(store.getState().search.pageIndex).toEqual(1);
expect(store.getState().popularTables).toEqual([]);
expect(store.getState().search.searchResults).toEqual(mockResults);
expect(store.getState().search.searchTerm).toEqual('test');
expect(store.getState().search.totalResults).toEqual(1);
});
it('verified app state after GET_POPULAR_TABLES action', () => {
const payload = {
data: {
results: mockResults,
},
};
const action = { type: GET_POPULAR_TABLES, payload };
store.dispatch(action);
expect(store.getState().search.pageIndex).toEqual(0);
expect(store.getState().popularTables).toEqual(mockResults);
expect(store.getState().search.searchResults).toEqual([]);
expect(store.getState().search.searchTerm).toEqual('');
expect(store.getState().search.totalResults).toEqual(0);
});
});
......@@ -58,7 +58,7 @@ const initialState: SearchReducerState = {
};
export default function reducer(state: SearchReducerState = initialState, action: SearchReducerAction): SearchReducerState {
let newState = action.payload;
const newState = action.payload;
switch (action.type) {
// SearchAll will reset all resources with search results or the initial state
case SearchAll.SUCCESS:
......
......@@ -103,9 +103,6 @@
"fsevents": "*"
},
"jest": {
"setupFiles": [
"<rootDir>/test-shim.js"
],
"moduleFileExtensions": [
"ts",
"tsx",
......
import { configure } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
......@@ -14,7 +14,7 @@
"moduleResolution": "node",
"noResolve": false,
"removeComments": true,
"types": [],
"types": ["jest"],
},
"exclude": [
"node_modules",
......
......@@ -39,4 +39,4 @@ Fix all errors before submitting a PR.
### JS Assets
By default, the build commands that are run to verify local changes -- `npm run build` and `npm run dev-build` -- also conduct linting and type checking. During development be sure to fix all errors before submitting a PR.
**TODO: JS unit tests are in progress - document unit test instructions after work is complete**
Run unit tests by executing `npm run test`. Fix all failures before submitting a PR.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment