mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 05:00:43 +10:00
Merge pull request #50 from sharetribe/listing-search-with-sdk
E2E listing search with the SDK Duck
This commit is contained in:
commit
96b519a9de
15 changed files with 315 additions and 120 deletions
|
|
@ -1,10 +1,24 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import { NamedLink } from '../../components';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import css from './ListingCard.css';
|
||||
|
||||
const ListingCard = props => {
|
||||
const { id, title, price, description, location, review, author } = props;
|
||||
const { listing } = props;
|
||||
const id = listing.id.uuid;
|
||||
const { title = '', description = '', address = '' } = listing.attributes || {};
|
||||
const slug = encodeURIComponent(title.split(' ').join('-'));
|
||||
const authorName = listing.author && listing.author.attributes
|
||||
? `${listing.author.attributes.profile.firstName} ${listing.author.attributes.profile.lastName}`
|
||||
: '';
|
||||
|
||||
// TODO: these are not yet present in the API data, figure out what
|
||||
// to do with them
|
||||
const price = '55\u20AC / day';
|
||||
const review = { rating: 3, count: 10 };
|
||||
const authorAvatar = 'http://placehold.it/44x44';
|
||||
const authorReview = { rating: 4 };
|
||||
|
||||
return (
|
||||
<div className={css.listing}>
|
||||
<div className={css.squareWrapper}>
|
||||
|
|
@ -26,22 +40,22 @@ const ListingCard = props => {
|
|||
</div>
|
||||
<div className={css.details}>
|
||||
<div className={css.location}>
|
||||
{location}
|
||||
{address}
|
||||
</div>
|
||||
<div className={css.reviews}>
|
||||
(<span>{review.rating}</span><span>/5</span>){' '}
|
||||
<span>{review.count}</span>
|
||||
<span>{review.count} reviews</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className={css.authorInfo}>
|
||||
<div className={css.avatarWrapper}>
|
||||
<img className={css.avatar} src={author.avatar} alt={author.name} />
|
||||
<img className={css.avatar} src={authorAvatar} alt={authorName} />
|
||||
</div>
|
||||
<div className={css.authorDetails}>
|
||||
<span className={css.authorName}>{author.name}</span>
|
||||
<span className={css.authorName}>{authorName}</span>
|
||||
<div className={css.authorReview}>
|
||||
review: <span>{author.review.rating}</span><span>/5</span>
|
||||
review: <span>{authorReview.rating}</span><span>/5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -50,22 +64,6 @@ const ListingCard = props => {
|
|||
);
|
||||
};
|
||||
|
||||
ListingCard.defaultProps = { location: null, review: {}, author: {} };
|
||||
|
||||
const { shape, string } = PropTypes;
|
||||
|
||||
ListingCard.propTypes = {
|
||||
id: string.isRequired,
|
||||
title: string.isRequired,
|
||||
price: string.isRequired,
|
||||
description: string.isRequired,
|
||||
location: string,
|
||||
review: shape({ rating: string.isRequired, count: string.isRequired }),
|
||||
author: shape({
|
||||
name: string.isRequired,
|
||||
avatar: string.isRequired,
|
||||
review: shape({ rating: string.isRequired }),
|
||||
}),
|
||||
};
|
||||
ListingCard.propTypes = { listing: propTypes.listing.isRequired };
|
||||
|
||||
export default ListingCard;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,12 @@
|
|||
import React from 'react';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { renderShallow, createUser, createListing } from '../../util/test-helpers';
|
||||
import ListingCard from './ListingCard';
|
||||
|
||||
describe('ListingCard', () => {
|
||||
it('matches snapshot', () => {
|
||||
const listing = {
|
||||
id: 'some-id',
|
||||
title: 'Banyan Studios',
|
||||
price: '55\u20AC / day',
|
||||
description: 'Organic Music Production in a Sustainable, Ethical and Professional Studio.',
|
||||
location: 'New York, NY \u2022 40mi away',
|
||||
review: { count: '8 reviews', rating: '4' },
|
||||
author: {
|
||||
name: 'The Stardust Collective',
|
||||
avatar: 'http://placehold.it/44x44',
|
||||
review: { rating: '4' },
|
||||
},
|
||||
};
|
||||
const tree = renderShallow(<ListingCard {...listing} />);
|
||||
const author = createUser('user1');
|
||||
const listing = { ...createListing('listing1'), author };
|
||||
const tree = renderShallow(<ListingCard listing={listing} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,27 +13,27 @@ exports[`ListingCard matches snapshot 1`] = `
|
|||
name="ListingPage"
|
||||
params={
|
||||
Object {
|
||||
"id": "some-id",
|
||||
"slug": "Banyan-Studios",
|
||||
"id": "listing1",
|
||||
"slug": "listing1-title",
|
||||
}
|
||||
}>
|
||||
Banyan Studios
|
||||
listing1 title
|
||||
</withFlattenedRoutes(withRouter(NamedLink))>
|
||||
<div>
|
||||
55€ / day
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Organic Music Production in a Sustainable, Ethical and Professional Studio.
|
||||
listing1 description
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
New York, NY • 40mi away
|
||||
listing1 address
|
||||
</div>
|
||||
<div>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
3
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
|
|
@ -41,7 +41,8 @@ exports[`ListingCard matches snapshot 1`] = `
|
|||
)
|
||||
|
||||
<span>
|
||||
8 reviews
|
||||
10
|
||||
reviews
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -49,12 +50,12 @@ exports[`ListingCard matches snapshot 1`] = `
|
|||
<div>
|
||||
<div>
|
||||
<img
|
||||
alt="The Stardust Collective"
|
||||
alt="user1 first name user1 last name"
|
||||
src="http://placehold.it/44x44" />
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
The Stardust Collective
|
||||
user1 first name user1 last name
|
||||
</span>
|
||||
<div>
|
||||
review:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
import { NamedLink } from '../../components';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import css from './ListingCardSmall.css';
|
||||
|
||||
// <NamedLink name="SearchListingsPage">X</NamedLink>
|
||||
const ListingCardSmall = props => {
|
||||
const { id, title, price, review } = props;
|
||||
const { listing } = props;
|
||||
const { title = '' } = listing.attributes || {};
|
||||
const id = listing.id.uuid;
|
||||
const slug = encodeURIComponent(title.split(' ').join('-'));
|
||||
|
||||
// TODO: these are not yet present in the API data, figure out what
|
||||
// to do with them
|
||||
const price = '55\u20AC / day';
|
||||
const review = { rating: 3, count: 10 };
|
||||
|
||||
return (
|
||||
<div className={css.listing}>
|
||||
<div className={css.squareWrapper}>
|
||||
|
|
@ -29,15 +37,6 @@ const ListingCardSmall = props => {
|
|||
);
|
||||
};
|
||||
|
||||
ListingCardSmall.defaultProps = { review: {} };
|
||||
|
||||
const { number, shape, string } = PropTypes;
|
||||
|
||||
ListingCardSmall.propTypes = {
|
||||
id: number.isRequired,
|
||||
title: string.isRequired,
|
||||
price: string.isRequired,
|
||||
review: shape({ rating: string.isRequired, count: string.isRequired }),
|
||||
};
|
||||
ListingCardSmall.propTypes = { listing: propTypes.listing.isRequired };
|
||||
|
||||
export default ListingCardSmall;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,12 @@
|
|||
import React from 'react';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { renderShallow, createUser, createListing } from '../../util/test-helpers';
|
||||
import ListingCardSmall from './ListingCardSmall';
|
||||
|
||||
describe('ListingCardSmall', () => {
|
||||
it('matches snapshot', () => {
|
||||
const listing = {
|
||||
id: 123,
|
||||
title: 'Banyan Studios',
|
||||
price: '55\u20AC / day',
|
||||
description: 'Organic Music Production in a Sustainable, Ethical and Professional Studio.',
|
||||
location: 'New York, NY \u2022 40mi away',
|
||||
review: { count: '8 reviews', rating: '4' },
|
||||
author: {
|
||||
name: 'The Stardust Collective',
|
||||
avatar: 'http://placehold.it/44x44',
|
||||
review: { rating: '4' },
|
||||
},
|
||||
};
|
||||
const tree = renderShallow(<ListingCardSmall {...listing} />);
|
||||
const author = createUser('user1');
|
||||
const listing = { ...createListing('listing1'), author };
|
||||
const tree = renderShallow(<ListingCardSmall listing={listing} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,16 +12,16 @@ exports[`ListingCardSmall matches snapshot 1`] = `
|
|||
name="ListingPage"
|
||||
params={
|
||||
Object {
|
||||
"id": 123,
|
||||
"slug": "Banyan-Studios",
|
||||
"id": "listing1",
|
||||
"slug": "listing1-title",
|
||||
}
|
||||
}>
|
||||
Banyan Studios
|
||||
listing1 title
|
||||
</withFlattenedRoutes(withRouter(NamedLink))>
|
||||
<div>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
3
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
|
|
@ -29,7 +29,7 @@ exports[`ListingCardSmall matches snapshot 1`] = `
|
|||
)
|
||||
|
||||
<span>
|
||||
8 reviews
|
||||
10
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { addFlashNotification } from '../../ducks/FlashNotification.duck';
|
||||
import { addFilter, callFetchListings, loadListings } from './SearchPage.duck';
|
||||
import css from './SearchPage.css';
|
||||
import { types } from 'sharetribe-sdk';
|
||||
import { searchListings, getListingsById } from '../../ducks/sdk.duck';
|
||||
import {
|
||||
FilterPanel,
|
||||
ListingCard,
|
||||
|
|
@ -13,13 +12,13 @@ import {
|
|||
SearchResultsPanel,
|
||||
} from '../../components';
|
||||
|
||||
import css from './SearchPage.css';
|
||||
|
||||
const { LatLng } = types;
|
||||
|
||||
export class SearchPageComponent extends Component {
|
||||
componentDidMount() {
|
||||
// TODO: This should be moved to Router
|
||||
const { initialListingsLoaded } = this.props;
|
||||
if (!initialListingsLoaded) {
|
||||
this.props.onLoadListings();
|
||||
}
|
||||
SearchPageComponent.loadData(this.props.dispatch);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
@ -37,12 +36,12 @@ export class SearchPageComponent extends Component {
|
|||
</div>
|
||||
<div className={listingsClassName}>
|
||||
<SearchResultsPanel>
|
||||
{listings.map(l => <ListingCard key={l.id} {...l} />)}
|
||||
{listings.map(l => <ListingCard key={l.id.uuid} listing={l} />)}
|
||||
</SearchResultsPanel>
|
||||
</div>
|
||||
<div className={mapClassName}>
|
||||
<MapPanel>
|
||||
{listings.map(l => <ListingCardSmall key={l.id} {...l} />)}
|
||||
{listings.map(l => <ListingCardSmall key={l.id.uuid} listing={l} />)}
|
||||
</MapPanel>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -51,30 +50,23 @@ export class SearchPageComponent extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
SearchPageComponent.loadData = callFetchListings;
|
||||
SearchPageComponent.loadData = dispatch => {
|
||||
dispatch(searchListings({ origin: new LatLng(40, 70), include: ['author'] }));
|
||||
};
|
||||
|
||||
SearchPageComponent.defaultProps = { initialListingsLoaded: false, listings: [], tab: 'listings' };
|
||||
|
||||
const { array, bool, func, oneOf } = PropTypes;
|
||||
const { array, func, oneOf } = PropTypes;
|
||||
|
||||
SearchPageComponent.propTypes = {
|
||||
onLoadListings: func.isRequired,
|
||||
initialListingsLoaded: bool,
|
||||
listings: array,
|
||||
tab: oneOf(['filters', 'listings', 'map']).isRequired,
|
||||
dispatch: func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
initialListingsLoaded: state.SearchPage.initialListingsLoaded,
|
||||
listings: state.SearchPage.listings,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onAddNotice: msg => dispatch(addFlashNotification('notice', msg)),
|
||||
onAddFilter: (k, v) => dispatch(addFilter(k, v)),
|
||||
onLoadListings: () => dispatch(loadListings.request()),
|
||||
};
|
||||
const mapStateToProps = state => {
|
||||
const listingIds = state.data.searchPageResults;
|
||||
return { listings: getListingsById(state.data, listingIds) };
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SearchPageComponent);
|
||||
export default connect(mapStateToProps)(SearchPageComponent);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ const { LatLng } = types;
|
|||
|
||||
describe('SearchPageComponent', () => {
|
||||
it('matches snapshot', () => {
|
||||
const tree = renderShallow(<SearchPageComponent onLoadListings={v => v} />);
|
||||
const tree = renderShallow(
|
||||
<SearchPageComponent onLoadListings={v => v} dispatch={() => null} />,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable no-constant-condition, no-console */
|
||||
import { call, put, take, fork } from 'redux-saga/effects';
|
||||
import { updatedEntities } from '../util/data';
|
||||
import { updatedEntities, denormalisedEntities } from '../util/data';
|
||||
|
||||
const requestAction = actionType => params => ({ type: actionType, payload: { params } });
|
||||
|
||||
|
|
@ -41,6 +41,7 @@ const initialState = {
|
|||
showUsersError: null,
|
||||
// Database of all the fetched entities.
|
||||
entities: {},
|
||||
searchPageResults: [],
|
||||
};
|
||||
|
||||
const merge = (state, payload) => {
|
||||
|
|
@ -48,6 +49,8 @@ const merge = (state, payload) => {
|
|||
return { ...state, entities };
|
||||
};
|
||||
|
||||
const searchResults = payload => payload.data.data.map(listing => listing.id);
|
||||
|
||||
export default function sdkReducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
|
|
@ -70,7 +73,7 @@ export default function sdkReducer(state = initialState, action = {}) {
|
|||
case SEARCH_LISTINGS_REQUEST:
|
||||
return { ...state, searchListingsError: null };
|
||||
case SEARCH_LISTINGS_SUCCESS:
|
||||
return merge(state, payload);
|
||||
return { ...merge(state, payload), searchPageResults: searchResults(payload) };
|
||||
case SEARCH_LISTINGS_ERROR:
|
||||
console.error(payload);
|
||||
return { ...state, searchListingsError: payload };
|
||||
|
|
@ -96,24 +99,44 @@ export default function sdkReducer(state = initialState, action = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
// ================ Selectors ================ //
|
||||
|
||||
/**
|
||||
* Get the denormalised listing entities with the given IDs
|
||||
*
|
||||
* @param {Object} data the state part of the Redux store for this SDK reducer
|
||||
* @param {Array<UUID>} listingIds listing IDs to select from the store
|
||||
*/
|
||||
export const getListingsById = (data, listingIds) =>
|
||||
denormalisedEntities(data.entities, 'listing', listingIds);
|
||||
|
||||
// ================ Action creators ================ //
|
||||
|
||||
// All the action creators that don't have the {Success, Error} suffix
|
||||
// take the params object that the corresponding SDK endpoint method
|
||||
// expects.
|
||||
|
||||
// SDK method: listings.show
|
||||
export const showListings = requestAction(SHOW_LISTINGS_REQUEST);
|
||||
export const showListingsSuccess = successAction(SHOW_LISTINGS_SUCCESS);
|
||||
export const showListingsError = errorAction(SHOW_LISTINGS_ERROR);
|
||||
|
||||
// SDK method: listings.query
|
||||
export const queryListings = requestAction(QUERY_LISTINGS_REQUEST);
|
||||
export const queryListingsSuccess = successAction(QUERY_LISTINGS_SUCCESS);
|
||||
export const queryListingsError = errorAction(QUERY_LISTINGS_ERROR);
|
||||
|
||||
// SDK method: listings.search
|
||||
export const searchListings = requestAction(SEARCH_LISTINGS_REQUEST);
|
||||
export const searchListingsSuccess = successAction(SEARCH_LISTINGS_SUCCESS);
|
||||
export const searchListingsError = errorAction(SEARCH_LISTINGS_ERROR);
|
||||
|
||||
// SDK method: marketplace.show
|
||||
export const showMarketplace = requestAction(SHOW_MARKETPLACE_REQUEST);
|
||||
export const showMarketplaceSuccess = successAction(SHOW_MARKETPLACE_SUCCESS);
|
||||
export const showMarketplaceError = errorAction(SHOW_MARKETPLACE_ERROR);
|
||||
|
||||
// SDK method: users.show
|
||||
export const showUsers = requestAction(SHOW_USERS_REQUEST);
|
||||
export const showUsersSuccess = successAction(SHOW_USERS_SUCCESS);
|
||||
export const showUsersError = errorAction(SHOW_USERS_ERROR);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
showMarketplace,
|
||||
showUsers,
|
||||
} from './ducks/sdk.duck';
|
||||
import routeConfiguration from './routesConfiguration';
|
||||
|
||||
import './index.css';
|
||||
|
||||
|
|
@ -75,7 +76,7 @@ if (typeof window !== 'undefined') {
|
|||
{ showListings, queryListings, searchListings, showMarketplace, showUsers },
|
||||
store.dispatch,
|
||||
);
|
||||
window.app = { config, sdk, sdkTypes: types, actions, store, sample };
|
||||
window.app = { config, sdk, sdkTypes: types, actions, store, sample, routeConfiguration };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,28 +29,24 @@ const routesConfiguration = [
|
|||
exact: true,
|
||||
name: 'SearchPage',
|
||||
component: props => <SearchPage {...props} />,
|
||||
loadData: SearchPage ? SearchPage.loadData : null,
|
||||
routes: [
|
||||
{
|
||||
path: '/s/filters',
|
||||
exact: true,
|
||||
name: 'SearchFiltersPage',
|
||||
component: props => <SearchPage {...props} tab="filters" />,
|
||||
loadData: SearchPage ? SearchPage.loadData : null,
|
||||
},
|
||||
{
|
||||
path: '/s/listings',
|
||||
exact: true,
|
||||
name: 'SearchListingsPage',
|
||||
component: props => <SearchPage {...props} tab="listings" />,
|
||||
loadData: SearchPage ? SearchPage.loadData : null,
|
||||
},
|
||||
{
|
||||
path: '/s/map',
|
||||
exact: true,
|
||||
name: 'SearchMapPage',
|
||||
component: props => <SearchPage {...props} tab="map" />,
|
||||
loadData: SearchPage ? SearchPage.loadData : null,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { reduce } from 'lodash';
|
||||
|
||||
/**
|
||||
* Combine the given relationships objects
|
||||
*
|
||||
|
|
@ -52,3 +54,49 @@ export const updatedEntities = (oldEntities, apiResponse) => {
|
|||
|
||||
return newEntities;
|
||||
};
|
||||
|
||||
/**
|
||||
* Denormalise the entities with the given IDs from the entities object
|
||||
*
|
||||
* This function calculates the dernormalised tree structure from the
|
||||
* normalised entities object with all the relationships joined in.
|
||||
*
|
||||
* @param {Object} entities entities object in the SDK Redux store
|
||||
* @param {String} type entity type of the given IDs
|
||||
* @param {Array<UUID>} ids IDs to pick from the entities
|
||||
*/
|
||||
export const denormalisedEntities = (entities, type, ids) => {
|
||||
if (!entities[type] && ids.length > 0) {
|
||||
throw new Error(`No entities of type ${type}`);
|
||||
}
|
||||
return ids.map(id => {
|
||||
const entity = entities[type][id.uuid];
|
||||
if (!entity) {
|
||||
throw new Error(`Entity ${type} with id ${id.uuid} not found`);
|
||||
}
|
||||
const { relationships, ...entityData } = entity;
|
||||
|
||||
if (relationships) {
|
||||
// Recursively join in all the relationship entities
|
||||
return reduce(
|
||||
relationships,
|
||||
(ent, relRef, relName) => {
|
||||
// A relationship reference can be either a single object or
|
||||
// an array of objects. We want to keep that form in the final
|
||||
// result.
|
||||
const hasMultipleRefs = Array.isArray(relRef);
|
||||
const refs = hasMultipleRefs ? relRef : [relRef];
|
||||
const relIds = refs.map(ref => ref.data.id);
|
||||
const relType = refs[0].data.type;
|
||||
const rels = denormalisedEntities(entities, relType, relIds);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
ent[relName] = hasMultipleRefs ? rels : rels[0];
|
||||
return ent;
|
||||
},
|
||||
entityData,
|
||||
);
|
||||
}
|
||||
return entityData;
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { types } from 'sharetribe-sdk';
|
||||
import { combinedRelationships, combinedResourceObjects, updatedEntities } from './data';
|
||||
import {
|
||||
combinedRelationships,
|
||||
combinedResourceObjects,
|
||||
updatedEntities,
|
||||
denormalisedEntities,
|
||||
} from './data';
|
||||
|
||||
const { UUID } = types;
|
||||
|
||||
|
|
@ -162,4 +167,110 @@ describe('data utils', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('denormalisedEntities()', () => {
|
||||
const createListing = id => ({
|
||||
id: new UUID(id),
|
||||
type: 'listing',
|
||||
attributes: { title: `Listing ${id} title`, description: `Listing ${id} description` },
|
||||
});
|
||||
|
||||
const createUser = id => ({
|
||||
id: new UUID(id),
|
||||
type: 'user',
|
||||
attributes: { name: `User ${id} name` },
|
||||
});
|
||||
|
||||
const createImage = id => ({
|
||||
id: new UUID(id),
|
||||
type: 'image',
|
||||
attributes: { url: `https://example.com/${id}.jpg`, width: 300, height: 200 },
|
||||
});
|
||||
|
||||
it('return no results with empty ids', () => {
|
||||
const entities = { listing: { listing1: createListing('listing1') } };
|
||||
expect(denormalisedEntities(entities, 'listing', [])).toEqual([]);
|
||||
});
|
||||
|
||||
it('return no results with empty entities and empty ids', () => {
|
||||
const entities = {};
|
||||
expect(denormalisedEntities(entities, 'listing', [])).toEqual([]);
|
||||
});
|
||||
|
||||
it('throws when selecting a nonexistent type', () => {
|
||||
const entities = { listing: { listing1: createListing('listing1') } };
|
||||
expect(() => denormalisedEntities(entities, 'user', [new UUID('user1')])).toThrow(
|
||||
'No entities of type user',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when selecting a nonexistent id', () => {
|
||||
const entities = { listing: { listing1: createListing('listing1') } };
|
||||
const ids = [new UUID('listing2')];
|
||||
expect(() => denormalisedEntities(entities, 'listing', ids)).toThrow(
|
||||
'Entity listing with id listing2 not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if a related entity is not found', () => {
|
||||
const listing1 = createListing('listing1');
|
||||
listing1.relationships = { author: { data: createUser('user1') } };
|
||||
const entities = { listing: { listing1 } };
|
||||
const ids = [listing1.id];
|
||||
expect(() => denormalisedEntities(entities, 'listing', ids)).toThrow(
|
||||
'No entities of type user',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the selected entities', () => {
|
||||
const listing1 = createListing('listing1');
|
||||
const listing2 = createListing('listing2');
|
||||
const listing3 = createListing('listing3');
|
||||
|
||||
const entities = { listing: { listing1, listing2, listing3 } };
|
||||
const ids = [listing1.id, listing3.id];
|
||||
expect(denormalisedEntities(entities, 'listing', ids)).toEqual([listing1, listing3]);
|
||||
});
|
||||
|
||||
it('denormalises simple relationships', () => {
|
||||
const user1 = createUser('user1');
|
||||
const listing1 = createListing('listing1');
|
||||
const listing1Relationships = { author: { data: user1 } };
|
||||
const listing1WithRelationships = { ...listing1, relationships: listing1Relationships };
|
||||
const listing2 = createListing('listing2');
|
||||
const entities = {
|
||||
listing: { listing1: listing1WithRelationships, listing2 },
|
||||
user: { user1 },
|
||||
};
|
||||
const ids = [listing1.id];
|
||||
expect(denormalisedEntities(entities, 'listing', ids)).toEqual([
|
||||
{ ...listing1, author: user1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('denormalises multiple relationships', () => {
|
||||
const user1 = createUser('user1');
|
||||
const image1 = createImage('image1');
|
||||
const image2 = createImage('image2');
|
||||
const listing1 = createListing('listing1');
|
||||
const listing1Relationships = {
|
||||
author: { data: user1 },
|
||||
images: [{ data: image1 }, { data: image2 }],
|
||||
};
|
||||
const listing1WithRelationships = { ...listing1, relationships: listing1Relationships };
|
||||
const listing2 = createListing('listing2');
|
||||
const listing3 = createListing('listing3');
|
||||
const entities = {
|
||||
listing: { listing1: listing1WithRelationships, listing2, listing3 },
|
||||
user: { user1 },
|
||||
image: { image1, image2 },
|
||||
};
|
||||
const ids = [listing1.id, listing2.id];
|
||||
|
||||
expect(denormalisedEntities(entities, 'listing', ids)).toEqual([
|
||||
{ ...listing1, author: user1, images: [image1, image2] },
|
||||
listing2,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,8 +41,29 @@ export const route = shape({
|
|||
loadData: func,
|
||||
});
|
||||
|
||||
// User object from the API
|
||||
export const user = shape({ id: uuid.isRequired, type: value('user').isRequired });
|
||||
// Denormalised user object
|
||||
export const user = shape({
|
||||
id: uuid.isRequired,
|
||||
type: value('user').isRequired,
|
||||
attributes: shape({
|
||||
email: string.isRequired,
|
||||
profile: shape({
|
||||
firstName: string.isRequired,
|
||||
lastName: string.isRequired,
|
||||
slug: string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
});
|
||||
|
||||
// Listing object from the API
|
||||
export const listing = shape({ id: uuid.isRequired, type: value('listing').isRequired });
|
||||
// Denormalised listing object
|
||||
export const listing = shape({
|
||||
id: uuid.isRequired,
|
||||
type: value('listing').isRequired,
|
||||
attributes: shape({
|
||||
title: string.isRequired,
|
||||
description: string.isRequired,
|
||||
address: string.isRequired,
|
||||
geolocation: latlng.isRequired,
|
||||
}),
|
||||
author: user,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import toJson from 'enzyme-to-json';
|
|||
import { IntlProvider } from 'react-intl';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { types } from 'sharetribe-sdk';
|
||||
import configureStore from '../store';
|
||||
|
||||
const { UUID, LatLng } = types;
|
||||
|
||||
// Provide all the context for components that connect to the Redux
|
||||
// store, i18n, router, etc.
|
||||
export const TestProvider = props => {
|
||||
|
|
@ -46,3 +49,25 @@ export const renderDeep = component => {
|
|||
);
|
||||
return comp.toJSON();
|
||||
};
|
||||
|
||||
// Create a user that conforms to the util/propTypes user schema
|
||||
export const createUser = id => ({
|
||||
id: new UUID(id),
|
||||
type: 'user',
|
||||
attributes: {
|
||||
email: '${id}@example.com',
|
||||
profile: { firstName: `${id} first name`, lastName: `${id} last name`, slug: `${id}-slug` },
|
||||
},
|
||||
});
|
||||
|
||||
// Create a user that conforms to the util/propTypes listing schema
|
||||
export const createListing = id => ({
|
||||
id: new UUID(id),
|
||||
type: 'listing',
|
||||
attributes: {
|
||||
title: `${id} title`,
|
||||
description: `${id} description`,
|
||||
address: `${id} address`,
|
||||
geolocation: new LatLng(40, 60),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue