Create AddonCard and Card components (fix #1421)

This commit is contained in:
Matthew Riley MacPherson 2016-11-21 19:56:02 +00:00
Родитель c19c3f8799
Коммит 35e16f6501
35 изменённых файлов: 545 добавлений и 252 удалений

Просмотреть файл

@ -0,0 +1,4 @@
.AddonsCard-list {
margin: 0;
padding: 0;
}

Просмотреть файл

@ -0,0 +1,34 @@
import React, { PropTypes } from 'react';
import SearchResult from 'admin/components/SearchResult';
import Card from 'ui/components/Card';
import './AddonsCard.scss';
export default class AddonsCard extends React.Component {
static propTypes = {
addons: PropTypes.array.isRequired,
children: PropTypes.node,
footer: PropTypes.node,
header: PropTypes.node,
}
render() {
const { addons, children, footer, header } = this.props;
return (
<Card header={header} footer={footer} className="AddonsCard"
ref={(ref) => { this.cardContainer = ref; }}>
{children}
{addons && addons.length ? (
<ul className="AddonsCard-list">
{addons.map((addon) => (
<SearchResult addon={addon} key={addon.slug} />
))}
</ul>
) : null}
</Card>
);
}
}

Просмотреть файл

@ -3,7 +3,7 @@ import React, { PropTypes } from 'react';
import Paginate from 'core/components/Paginate';
import { convertFiltersToQueryParams } from 'core/searchUtils';
import { gettext as _ } from 'core/utils';
import SearchResults from 'core/components/Search/SearchResults';
import SearchResults from 'admin/components/SearchResults';
import SearchForm from './SearchForm';
import AdminSearchResult from './SearchResult';

Просмотреть файл

@ -37,40 +37,40 @@ ResultLink.propTypes = {
export default class AdminSearchResult extends React.Component {
static propTypes = {
result: PropTypes.object.isRequired,
addon: PropTypes.object.isRequired,
}
render() {
const { result } = this.props;
const { addon } = this.props;
return (
<li className="SearchResult">
<div>
<img className="SearchResult-icon" src={result.icon_url} alt="Icon" />
<img className="SearchResult-icon" src={addon.icon_url} alt="Icon" />
</div>
<section className="SearchResult-main">
<h2 className="SearchResult-heading">
<Link to={`/search/addons/${result.slug}`} className="SearchResult-name"
<Link to={`/search/addons/${addon.slug}`} className="SearchResult-name"
ref={(el) => { this.name = el; }}>
{result.name}
{addon.name}
</Link>
</h2>
<div className="SearchResult-info" ref={(el) => { this.guid = el; }}>
{result.guid}
{addon.guid}
</div>
<span className="SearchResult-info" ref={(el) => { this.type = el; }}>
{result.type}
{addon.type}
</span>
<span className="SearchResult-info" ref={(el) => { this.status = el; }}>
{result.status}
{addon.status}
</span>
<span className="SearchResult-info" ref={(el) => { this.fileCount = el; }}>
{fileCountText(result.current_version)}
{fileCountText(addon.current_version)}
</span>
</section>
<section className="SearchResult-actions">
<ResultLink href={result.url} start>Listing</ResultLink>
<ResultLink href={result.edit_url} middle>Edit</ResultLink>
<ResultLink href={result.review_url} end>Editors</ResultLink>
<ResultLink href={addon.url} start>Listing</ResultLink>
<ResultLink href={addon.edit_url} middle>Edit</ResultLink>
<ResultLink href={addon.review_url} end>Editors</ResultLink>
</section>
</li>
);

Просмотреть файл

@ -0,0 +1,63 @@
import React, { PropTypes } from 'react';
import AddonsCard from 'admin/components/AddonsCard';
import translate from 'core/i18n/translate';
class SearchResults extends React.Component {
static propTypes = {
count: PropTypes.number,
filters: PropTypes.object,
hasSearchParams: PropTypes.bool,
i18n: PropTypes.object.isRequired,
loading: PropTypes.bool,
results: PropTypes.arrayOf(PropTypes.object),
}
static defaultProps = {
count: 0,
filters: {},
hasSearchParams: false,
results: [],
}
render() {
const {
count, hasSearchParams, filters, i18n, loading, results,
} = this.props;
const { query } = filters;
let messageText;
if (hasSearchParams && loading) {
messageText = i18n.gettext('Searching...');
} else if (!loading && count === 0) {
if (query) {
messageText = i18n.sprintf(
i18n.gettext('No results were found for "%(query)s".'), { query });
} else if (hasSearchParams) {
// TODO: Add the extension type, if available, so it says
// "no extensions" found that match your search or something.
messageText = i18n.gettext('No results were found.');
} else {
messageText = i18n.gettext(
'Please enter a search term to search Mozilla Add-ons.');
}
}
return (
<div ref={(ref) => { this.container = ref; }} className="SearchResults">
<AddonsCard addons={results}>
{messageText ? (
<p ref={(ref) => { this.message = ref; }}
className="SearchResults-message">
{messageText}
</p>
) : null}
</AddonsCard>
</div>
);
}
}
export default translate({ withRef: true })(SearchResults);

Просмотреть файл

@ -3,6 +3,7 @@ import { compose } from 'redux';
import Link from 'amo/components/Link';
import translate from 'core/i18n/translate';
import Card from 'ui/components/Card';
import './AddonMoreInfo.scss';
@ -16,14 +17,8 @@ export class AddonMoreInfoBase extends React.Component {
render() {
const { addon, i18n } = this.props;
// TODO: Use the addonType for the privacy text, so it reads
// "for this extension", "for this theme", etc.
return (
<section className="AddonMoreInfo">
<h2 className="AddonMoreInfo-header">
{i18n.gettext('More information')}
</h2>
<Card className="AddonMoreInfo" header={i18n.gettext('More information')}>
<dl className="AddonMoreInfo-contents">
{addon.homepage ? <dt>{i18n.gettext('Website')}</dt> : null}
{addon.homepage ? (
@ -62,7 +57,7 @@ export class AddonMoreInfoBase extends React.Component {
</dd>
) : null}
</dl>
</section>
</Card>
);
}
}

Просмотреть файл

@ -1,22 +1,13 @@
@import "~core/css/inc/mixins";
@import "~core/css/inc/vars";
.AddonMoreInfo-header {
@include addonSection();
border-top-left-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
font-size: $font-size-default;
margin-bottom: 1px;
text-align: center;
.AddonMoreInfo {
padding: $padding-page;
}
.AddonMoreInfo-contents {
@include addonSection();
border-bottom-left-radius: $border-radius-default;
border-bottom-right-radius: $border-radius-default;
font-size: $font-size-s;
margin: 0;
padding: 0;
dt {
font-weight: bold;

Просмотреть файл

@ -0,0 +1,4 @@
.AddonsCard-list {
margin: 0;
padding: 0;
}

Просмотреть файл

@ -0,0 +1,34 @@
import React, { PropTypes } from 'react';
import SearchResult from 'amo/components/SearchResult';
import Card from 'ui/components/Card';
import './AddonsCard.scss';
export default class AddonsCard extends React.Component {
static propTypes = {
addons: PropTypes.array.isRequired,
children: PropTypes.node,
footer: PropTypes.node,
header: PropTypes.node,
}
render() {
const { addons, children, footer, header } = this.props;
return (
<Card header={header} footer={footer} className="AddonsCard"
ref={(ref) => { this.cardContainer = ref; }}>
{children}
{addons && addons.length ? (
<ul className="AddonsCard-list">
{addons.map((addon) => (
<SearchResult addon={addon} key={addon.slug} />
))}
</ul>
) : null}
</Card>
);
}
}

Просмотреть файл

@ -2,11 +2,13 @@ import React, { PropTypes } from 'react';
import Link from 'amo/components/Link';
import Paginate from 'core/components/Paginate';
import SearchResults from 'core/components/Search/SearchResults';
import SearchResults from 'amo/components/SearchResults';
import { convertFiltersToQueryParams } from 'core/searchUtils';
import SearchResult from './SearchResult';
import './SearchPage.scss';
export default class SearchPage extends React.Component {
static propTypes = {
@ -42,7 +44,7 @@ export default class SearchPage extends React.Component {
) : [];
return (
<div className="search-page">
<div className="SearchPage">
<SearchResults ResultComponent={ResultComponent} count={count}
hasSearchParams={hasSearchParams} loading={loading} results={results}
filters={filters} />

Просмотреть файл

@ -0,0 +1,5 @@
@import "~core/css/inc/vars";
.SearchPage {
padding: $padding-page $padding-page 0;
}

Просмотреть файл

@ -9,19 +9,19 @@ import 'core/css/SearchResult.scss';
export class SearchResultBase extends React.Component {
static propTypes = {
addon: PropTypes.object.isRequired,
i18n: PropTypes.object.isRequired,
result: PropTypes.object.isRequired,
}
render() {
const { i18n, result } = this.props;
const { addon, i18n } = this.props;
// TODO: Implement a rating component and style the stars.
const rating = result.ratings && result.ratings.average ? (
const rating = addon.ratings && addon.ratings.average ? (
<h3 className="SearchResult-rating">
<span className="visually-hidden">{i18n.sprintf(
i18n.gettext('Average rating: %(rating)s out of 5'),
{ rating: Math.round(result.ratings.average * 2) / 2 }
{ rating: Math.round(addon.ratings.average * 2) / 2 }
)}</span>
</h3>
) : (
@ -31,18 +31,18 @@ export class SearchResultBase extends React.Component {
);
return (
<li className="SearchResult">
<Link to={`/addon/${result.slug}/`}
<Link to={`/addon/${addon.slug}/`}
className="SearchResult-link"
ref={(el) => { this.name = el; }}>
<section className="SearchResult-main">
<img className="SearchResult-icon" src={result.icon_url} alt="" />
<h2 className="SearchResult-heading">{result.name}</h2>
<img className="SearchResult-icon" src={addon.icon_url} alt="" />
<h2 className="SearchResult-heading">{addon.name}</h2>
{rating}
<h3 className="SearchResult-author">{result.authors[0].name}</h3>
<h3 className="SearchResult-author">{addon.authors[0].name}</h3>
<h3 className="SearchResult-users">{i18n.sprintf(
i18n.ngettext('%(users)s user', '%(users)s users',
result.average_daily_users),
{ users: result.average_daily_users }
addon.average_daily_users),
{ users: addon.average_daily_users }
)}
</h3>
</section>

Просмотреть файл

@ -1,16 +1,13 @@
import classNames from 'classnames';
import React, { PropTypes } from 'react';
import AddonsCard from 'amo/components/AddonsCard';
import translate from 'core/i18n/translate';
import 'core/css/SearchResults.scss';
import SearchResult from './SearchResult';
import './SearchResults.scss';
class SearchResults extends React.Component {
static propTypes = {
ResultComponent: PropTypes.object.isRequired,
count: PropTypes.number,
filters: PropTypes.object,
hasSearchParams: PropTypes.bool,
@ -20,7 +17,6 @@ class SearchResults extends React.Component {
}
static defaultProps = {
ResultComponent: SearchResult,
count: 0,
filters: {},
hasSearchParams: false,
@ -29,34 +25,13 @@ class SearchResults extends React.Component {
render() {
const {
ResultComponent, count, hasSearchParams, filters, i18n, loading, results,
count, hasSearchParams, filters, i18n, loading, results,
} = this.props;
const { query } = filters;
let hideMessageText = false;
let messageText;
let resultHeader;
let searchResults;
if (hasSearchParams && count > 0) {
hideMessageText = true;
messageText = i18n.sprintf(
i18n.ngettext(
'Your search for "%(query)s" returned %(count)s result.',
'Your search for "%(query)s" returned %(count)s results.',
count,
),
{ query, count }
);
searchResults = (
<ul className="SearchResults-list"
ref={(ref) => { this.results = ref; }}>
{results.map((result) => (
<ResultComponent result={result} key={result.slug} />
))}
</ul>
);
} else if (hasSearchParams && loading) {
if (hasSearchParams && loading) {
messageText = i18n.gettext('Searching...');
} else if (!loading && count === 0) {
if (query) {
@ -72,18 +47,16 @@ class SearchResults extends React.Component {
}
}
const message = (
(<p ref={(ref) => { this.message = ref; }} className={classNames({
'visually-hidden': hideMessageText,
'SearchResults-message': !hideMessageText,
})}>{messageText}</p>)
);
return (
<div ref={(ref) => { this.container = ref; }} className="SearchResults">
{resultHeader}
{message}
{searchResults}
<AddonsCard addons={results}>
{messageText ? (
<p ref={(ref) => { this.message = ref; }}
className="SearchResults-message">
{messageText}
</p>
) : null}
</AddonsCard>
</div>
);
}

Просмотреть файл

@ -0,0 +1,5 @@
@import "~core/css/inc/vars";
.SearchResults {
margin: 0 0 $padding-page;
}

Просмотреть файл

@ -11,7 +11,7 @@ import { loadAddonIfNeeded } from 'core/utils';
export class DetailPageBase extends React.Component {
render() {
return (
<div className="full-width no-top-padding">
<div className="DetailPage">
<AddonDetail {...this.props} />
</div>
);

Просмотреть файл

@ -24,7 +24,7 @@ export class HomePageBase extends React.Component {
<li className="HomePage-be-social"><Link to="#share-stuff"><span>{i18n.gettext('Be social')}</span></Link></li>
<li className="HomePage-share-stuff"><Link to="#share-stuff"><span>{i18n.gettext('Share stuff')}</span></Link></li>
</ul>
<Link className="HomePage-extensions-link" to="#extensions">
<Link className="HomePage-extensions-link" to="/extensions/">
{i18n.gettext('Browse all extensions')}
</Link>
@ -37,7 +37,7 @@ export class HomePageBase extends React.Component {
<li className="HomePage-sporty"><Link to="#sporty"><span>{i18n.gettext('Sporty')}</span></Link></li>
<li className="HomePage-mystical"><Link to="#mystical"><span>{i18n.gettext('Mystical')}</span></Link></li>
</ul>
<Link className="HomePage-themes-link" to="#themes">
<Link className="HomePage-themes-link" to="/themes/">
{i18n.gettext('Browse all themes')}
</Link>
</div>

Просмотреть файл

@ -11,5 +11,5 @@ export default compose(
deferred: true,
promise: loadSearchResultsIfNeeded,
}]),
connect(mapStateToProps)
connect(mapStateToProps),
)(SearchPage);

Просмотреть файл

@ -40,14 +40,7 @@ body {
.App-content {
background: $accent-color;
flex-grow: 1;
> div {
padding: $padding-page;
&.full-width {
padding: 0 0 $padding-page;
}
}
padding: 0 0 $padding-page;
}
h1,

Просмотреть файл

@ -1,3 +1,9 @@
@import "~core/css/inc/vars";
.HomePage {
padding: $padding-page;
}
.HomePage-subheading {
text-align: center;
}

Просмотреть файл

@ -1,28 +0,0 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
export default class SearchResult extends React.Component {
static propTypes = {
result: PropTypes.object.isRequired,
}
render() {
const { result } = this.props;
return (
<li className="SearchResult">
<Link to={`/addon/${result.slug}/`}
className="SearchResult-link"
ref={(el) => { this.name = el; }}>
<section className="SearchResult-main">
<h2 className="SearchResult-heading">
<span className="SearchResult-name">
{result.name}
</span>
</h2>
</section>
</Link>
</li>
);
}
}

Просмотреть файл

@ -22,10 +22,18 @@ $secondary-color: #6a6a6a;
color: $text-color-default;
display: flex;
margin: 0;
padding: 10px;
padding: 10px 0;
text-decoration: none;
width: 100%;
.SearchResult:first-of-type & {
padding-top: 0;
}
.SearchResult:last-of-type & {
padding-bottom: 0;
}
&:focus,
&:hover {
.SearchResult-heading {

Просмотреть файл

@ -1,20 +0,0 @@
@import "~core/css/inc/vars";
.SearchResults-list {
margin: 0;
padding: 0;
}
.SearchResults-type-header {
color: #000;
font-size: $font-size-default;
text-align: center;
}
.SearchResults-message {
background: $base-color;
font-size: $font-size-m;
margin: 0;
padding: 1em;
text-align: center;
}

Просмотреть файл

@ -244,7 +244,6 @@ $color: $text-color-message) {
@mixin addonSection() {
background: $base-color;
margin: 0 10px;
padding: 10px;
overflow: hidden;
}

Просмотреть файл

@ -0,0 +1,39 @@
@import "~core/css/inc/mixins";
@import "~core/css/inc/vars";
.Card-header {
@include addonSection();
border-top-left-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
font-size: $font-size-default;
margin-bottom: 1px;
text-align: center;
}
.Card-contents {
@include addonSection();
margin: 0;
.Card--no-header & {
border-top-left-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
}
.Card--no-footer & {
border-bottom-left-radius: $border-radius-default;
border-bottom-right-radius: $border-radius-default;
}
font-size: $font-size-s;
}
.Card-footer {
@include addonSection();
border-bottom-left-radius: $border-radius-default;
border-bottom-right-radius: $border-radius-default;
margin-top: 1px;
text-align: center;
}

Просмотреть файл

@ -0,0 +1,44 @@
import classNames from 'classnames';
import React, { PropTypes } from 'react';
import './Card.scss';
export default class Card extends React.Component {
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
footer: PropTypes.node,
header: PropTypes.node,
}
render() {
const { children, className, footer, header } = this.props;
return (
<section className={classNames('Card', className, {
'Card--no-header': !header,
'Card--no-footer': !footer,
})} ref={(ref) => { this.cardContainer = ref; }}>
{header ? (
<h2 className="Card-header" ref={(ref) => { this.header = ref; }}>
{header}
</h2>
) : null}
{children ? (
<div className="Card-contents"
ref={(ref) => { this.contents = ref; }}>
{children}
</div>
) : null}
{footer ? (
<footer className="Card-footer" ref={(ref) => { this.footer = ref; }}>
{footer}
</footer>
) : null}
</section>
);
}
}

Просмотреть файл

@ -0,0 +1,41 @@
import React from 'react';
import AddonsCard from 'admin/components/AddonsCard';
import AdminSearchResult from 'admin/components/SearchResult';
import { shallowRender } from 'tests/client/helpers';
describe('<AdminAddonsCard />', () => {
let addons;
function render(props = {}) {
return shallowRender(<AddonsCard {...props} />);
}
before(() => {
addons = [
{ name: 'I am add-on! ', slug: 'i-am-addon' },
{ name: 'I am also add-on!', slug: 'i-am-also-addon' },
];
});
it('renders add-ons when supplied', () => {
const root = render({ addons });
const list = root.props.children[1];
assert.equal(typeof root.props.children[0], 'undefined');
assert.equal(list.type, 'ul');
assert.deepEqual(list.props.children.map((c) => c.type),
[AdminSearchResult, AdminSearchResult]);
assert.deepEqual(list.props.children.map((c) => c.props.addon), addons);
});
it('renders children', () => {
const root = render({ addons, children: (<div>I am content</div>) });
const list = root.props.children[1];
assert.equal(root.props.children[0].type, 'div');
assert.equal(list.type, 'ul');
assert.deepEqual(list.props.children.map((c) => c.type),
[AdminSearchResult, AdminSearchResult]);
assert.deepEqual(list.props.children.map((c) => c.props.addon), addons);
});
});

Просмотреть файл

@ -13,7 +13,7 @@ describe('<AdminSearchResult />', () => {
files: [{}, {}],
},
};
const root = renderIntoDocument(<AdminSearchResult result={result} />);
const root = renderIntoDocument(<AdminSearchResult addon={result} />);
it('renders the name', () => {
assert.equal(root.name.props.children, 'A search result');
@ -34,7 +34,7 @@ describe('<AdminSearchResult />', () => {
it('outputs zero if no files exist', () => {
const thisResult = { ...result, current_version: { } };
const thisRoot = renderIntoDocument(
<AdminSearchResult result={thisResult} />);
<AdminSearchResult addon={thisResult} />);
assert.equal(thisRoot.fileCount.textContent, '0 files');
});
@ -45,7 +45,7 @@ describe('<AdminSearchResult />', () => {
it('renders the number of files singularly', () => {
const thisResult = { ...result, current_version: { files: [{}] } };
const thisRoot = renderIntoDocument(
<AdminSearchResult result={thisResult} />);
<AdminSearchResult addon={thisResult} />);
assert.equal(thisRoot.fileCount.textContent, '1 file');
});
});

Просмотреть файл

@ -0,0 +1,55 @@
import React from 'react';
import {
renderIntoDocument as render,
findRenderedComponentWithType,
} from 'react-addons-test-utils';
import SearchResults from 'admin/components/SearchResults';
import { getFakeI18nInst } from 'tests/client/helpers';
describe('<SearchResults />', () => {
function renderResults(props) {
return findRenderedComponentWithType(render(
<SearchResults i18n={getFakeI18nInst()} {...props} />
), SearchResults).getWrappedInstance();
}
it('renders empty search results container', () => {
const root = renderResults();
assert.include(root.message.textContent, 'enter a search term');
});
it('renders no results when searched but nothing is found', () => {
const root = renderResults({
count: 0,
hasSearchParams: true,
loading: false,
results: [],
});
assert.include(root.message.textContent, 'No results were found');
});
it('renders error when no search params exist', () => {
const root = renderResults({ hasSearchParams: false });
assert.include(root.message.textContent, 'enter a search term');
});
it('renders error when no results and valid query', () => {
const root = renderResults({
filters: { query: 'test' },
hasSearchParams: true,
});
assert.include(root.message.firstChild.textContent,
'No results were found');
});
it('renders a loading message when loading', () => {
const root = renderResults({
filters: { query: 'test' },
hasSearchParams: true,
loading: true,
});
assert.equal(root.message.textContent, 'Searching...');
});
});

Просмотреть файл

@ -0,0 +1,37 @@
import React from 'react';
import AddonsCard from 'amo/components/AddonsCard';
import SearchResult from 'amo/components/SearchResult';
import { shallowRender } from 'tests/client/helpers';
describe('<AddonsCard />', () => {
let addons;
function render(props = {}) {
return shallowRender(<AddonsCard {...props} />);
}
before(() => {
addons = [
{ name: 'I am add-on! ', slug: 'i-am-addon' },
{ name: 'I am also add-on!', slug: 'i-am-also-addon' },
];
});
it('renders add-ons when supplied', () => {
const root = render({ addons });
const list = root.props.children[1];
assert.equal(typeof root.props.children[0], 'undefined');
assert.equal(list.type, 'ul');
assert.deepEqual(list.props.children.map((c) => c.type),
[SearchResult, SearchResult]);
assert.deepEqual(list.props.children.map((c) => c.props.addon), addons);
});
it('renders children', () => {
const root = render({ addons, children: (<div>I am content</div>) });
assert.equal(root.props.children[0].type, 'div');
assert.equal(root.props.children[1].type, 'ul');
});
});

Просмотреть файл

@ -2,7 +2,7 @@ import React from 'react';
import SearchPage from 'amo/components/SearchPage';
import SearchResult from 'amo/components/SearchResult';
import SearchResults from 'core/components/Search/SearchResults';
import SearchResults from 'amo/components/SearchResults';
import Paginate from 'core/components/Paginate';
import { findAllByTag, findByTag, shallowRender } from 'tests/client/helpers';

Просмотреть файл

@ -17,7 +17,7 @@ describe('<SearchResult />', () => {
return findRenderedComponentWithType(renderIntoDocument(
<Provider store={createStore(initialState)}>
<SearchResult i18n={getFakeI18nInst()} result={result} />
<SearchResult i18n={getFakeI18nInst()} addon={result} />
</Provider>
), SearchResult).getWrappedInstance();
}

Просмотреть файл

@ -0,0 +1,55 @@
import React from 'react';
import {
renderIntoDocument as render,
findRenderedComponentWithType,
} from 'react-addons-test-utils';
import SearchResults from 'amo/components/SearchResults';
import { getFakeI18nInst } from 'tests/client/helpers';
describe('<SearchResults />', () => {
function renderResults(props) {
return findRenderedComponentWithType(render(
<SearchResults i18n={getFakeI18nInst()} {...props} />
), SearchResults).getWrappedInstance();
}
it('renders empty search results container', () => {
const root = renderResults();
assert.include(root.message.textContent, 'enter a search term');
});
it('renders no results when searched but nothing is found', () => {
const root = renderResults({
count: 0,
hasSearchParams: true,
loading: false,
results: [],
});
assert.include(root.message.textContent, 'No results were found');
});
it('renders error when no search params exist', () => {
const root = renderResults({ hasSearchParams: false });
assert.include(root.message.textContent, 'enter a search term');
});
it('renders error when no results and valid query', () => {
const root = renderResults({
filters: { query: 'test' },
hasSearchParams: true,
});
assert.include(root.message.firstChild.textContent,
'No results were found');
});
it('renders a loading message when loading', () => {
const root = renderResults({
filters: { query: 'test' },
hasSearchParams: true,
loading: true,
});
assert.equal(root.message.textContent, 'Searching...');
});
});

Просмотреть файл

@ -10,7 +10,7 @@ import { shallowRender } from 'tests/client/helpers';
describe('DetailPage', () => {
it('renders AddonDetail with the same props', () => {
const root = shallowRender(<DetailPageBase foo="bar" baz="quux" />);
assert.equal(root.props.className, 'full-width no-top-padding', sinon.format(root));
assert.equal(root.props.className, 'DetailPage', sinon.format(root));
assert.deepEqual(root.props.children.props, { foo: 'bar', baz: 'quux' });
assert.equal(root.props.children.type, AddonDetail);
});

Просмотреть файл

@ -1,99 +0,0 @@
import React from 'react';
import {
renderIntoDocument as render,
findRenderedComponentWithType,
isDOMComponent,
} from 'react-addons-test-utils';
import SearchResults from 'core/components/Search/SearchResults';
import { getFakeI18nInst } from 'tests/client/helpers';
describe('<SearchResults />', () => {
function renderResults(props) {
return findRenderedComponentWithType(render(
<SearchResults i18n={getFakeI18nInst()} {...props} />
), SearchResults).getWrappedInstance();
}
it('renders empty search results container', () => {
const root = renderResults();
const searchResults = root.container;
assert.ok(isDOMComponent(searchResults));
assert.equal(searchResults.childNodes.length, 1);
assert.include(searchResults.textContent, 'enter a search term');
});
it('renders no results when searched but nothing is found', () => {
const root = renderResults({
count: 0,
hasSearchParams: true,
loading: false,
results: [],
});
const searchResultsMessage = root.message;
assert.include(searchResultsMessage.firstChild.nodeValue,
'No results were found');
});
it('renders error when no search params exist', () => {
const root = renderResults({ hasSearchParams: false });
const searchResultsMessage = root.message;
assert.include(searchResultsMessage.firstChild.nodeValue,
'enter a search term');
});
it('renders error when no results and valid query', () => {
const root = renderResults({
filters: { query: 'test' },
hasSearchParams: true,
});
const searchResultsMessage = root.message;
// Using textContent here since we want to see the text inside the p.
// Since it has dynamic content is wrapped in a span implicitly.
assert.include(searchResultsMessage.textContent, 'No results were found');
});
it('renders a loading message when loading', () => {
const root = renderResults({
filters: { query: 'test' },
hasSearchParams: true,
loading: true,
});
const searchResultsMessage = root.message;
assert.equal(searchResultsMessage.textContent, 'Searching...');
});
it('renders search results when supplied', () => {
const root = renderResults({
count: 5,
filters: { query: 'test' },
hasSearchParams: true,
results: [
{ name: 'result 1', slug: '1' },
{ name: 'result 2', slug: '2' },
],
});
const searchResultsMessage = root.message;
assert.include(searchResultsMessage.textContent,
'Your search for "test" returned 5 results');
const searchResultsList = root.results;
assert.include(searchResultsList.textContent, 'result 1');
assert.include(searchResultsList.textContent, 'result 2');
});
it('renders search results in the singular', () => {
const root = renderResults({
count: 1,
filters: { query: 'test' },
hasSearchParams: true,
results: [
{ name: 'result 1', slug: '1' },
],
});
const searchResultsMessage = root.message;
assert.include(searchResultsMessage.textContent,
'Your search for "test" returned 1 result');
});
});

Просмотреть файл

@ -0,0 +1,53 @@
import React from 'react';
import { renderIntoDocument } from 'react-addons-test-utils';
import Card from 'ui/components/Card';
describe('<Card />', () => {
function render(props = {}) {
return renderIntoDocument(<Card {...props} />);
}
it('renders a Card', () => {
const root = render({ className: 'TofuSection' });
assert(root.cardContainer);
assert.equal(root.cardContainer.tagName, 'SECTION');
assert.include(root.cardContainer.className, 'Card');
assert.include(root.cardContainer.className, 'TofuSection');
});
it('shows header if supplied', () => {
const root = render({ header: 'foo' });
assert(root.header);
});
it('hides header if none supplied', () => {
const root = render({ children: 'hello' });
assert(!root.header);
assert.include(root.cardContainer.className, 'Card--no-header');
});
it('shows footer if supplied', () => {
const root = render({ footer: 'foo' });
assert(root.footer);
assert.equal(root.footer.textContent, 'foo');
});
it('hides footer if none supplied', () => {
const root = render({ children: 'hello' });
assert(!root.footer);
assert.include(root.cardContainer.className, 'Card--no-footer');
});
it('renders children', () => {
const root = render({ children: 'hello' });
assert(root.contents);
assert.include(root.contents.textContent, 'hello');
});
it('omits the content div with no children', () => {
const root = render();
assert(!root.contents);
});
});