Purchase flow without a router

This commit is contained in:
Kumar McMillan 2015-06-27 13:54:19 -07:00
Родитель e595a4c122
Коммит 89919b8049
19 изменённых файлов: 466 добавлений и 352 удалений

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

@ -3,4 +3,6 @@
module.exports = {
APP_ERROR: 'APP_ERROR',
USER_SIGNED_IN: 'USER_SIGNED_IN',
COMPLETE_PURCHASE: 'COMPLETE_PURCHASE',
PAY_WITH_NEW_CARD: 'PAY_WITH_NEW_CARD',
};

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

@ -23,9 +23,44 @@ exports.user = function(state, action) {
if (action.type === actionTypes.USER_SIGNED_IN) {
console.log('user store: got action', action);
return {
signedIn: true,
email: action.user.email,
payment_methods: action.user.payment_methods,
};
}
return state || {};
return state || {
signedIn: false,
};
};
exports.purchase = function(state, action) {
if (action.type === actionTypes.COMPLETE_PURCHASE) {
console.log('purchase store: got action', action);
return {
completed: true,
};
}
if (action.type === actionTypes.PAY_WITH_NEW_CARD) {
console.log('purchase store: got action', action);
return {
completed: false,
payment_methods: [],
};
}
if (action.type === actionTypes.USER_SIGNED_IN) {
console.log('purchase store: got action', action);
return {
completed: false,
payment_methods: action.user.payment_methods,
};
}
return state || {
completed: false,
payment_methods: [],
};
};

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

@ -0,0 +1,22 @@
'use strict';
var actionTypes = require('action-types');
// TODO: expand these actions to encapsulate the Ajax
// logic more directly. This will allow the Ajax requests to
// be tested more easily. CardForm and CardChoice will need
// to be refactored.
exports.complete = function() {
return {
type: actionTypes.COMPLETE_PURCHASE,
};
};
exports.payWithNewCard = function() {
return {
type: actionTypes.PAY_WITH_NEW_CARD,
};
};

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

@ -6,6 +6,7 @@ var assign = require('object-assign');
var actionTypes = require('action-types');
var appActions = require('app-actions');
module.exports = assign({}, {
signIn: function(accessToken) {
@ -19,6 +20,14 @@ module.exports = assign({}, {
context: this,
}).then(function(data) {
console.log('setting CSRF token for subsequent requests:',
data.csrf_token);
$.ajaxSetup({
headers: {
'X-CSRFToken': data.csrf_token,
},
});
console.log('login succeeded, setting user');
dispatch({
type: actionTypes.USER_SIGNED_IN,
@ -28,14 +37,6 @@ module.exports = assign({}, {
},
});
console.log('setting CSRF token for subsequent requests:',
data.csrf_token);
$.ajaxSetup({
headers: {
'X-CSRFToken': data.csrf_token,
},
});
}).fail(function() {
console.log('login failed');

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

@ -1,42 +1,55 @@
'use strict';
var React = require('react');
var Router = require('react-router');
var Route = Router.Route;
var RouteHandler = Router.RouteHandler;
var Provider = require('redux/react').Provider;
var Connector = require('redux/react').Connector;
var bindActionCreators = require('redux').bindActionCreators;
var reduxConfig = require('redux-config');
var CardDetails = require('views/card-details');
var CardListing = require('views/card-listing');
var CompletePayment = require('views/complete-payment');
var ErrorMessage = require('components/error');
var Login = require('views/login');
var Purchase = require('views/purchase');
var userActions = require('user-actions');
var products = require('products');
function parseQuery(url) {
// TODO: replace with querystring library or something.
var urlParts = url.split('?');
var query;
var data = {};
if (urlParts.length > 1) {
query = urlParts[1].split('&');
query.forEach(function(nameVal) {
var parts = nameVal.split('=');
data[parts[0]] = decodeURIComponent(parts[1] || '');
});
}
return data;
}
var App = React.createClass({
displayName: 'App',
contextTypes: {
router: React.PropTypes.func,
},
getInitialState: function() {
var {router} = this.context;
var productId = router.getCurrentQuery().product;
var qs = parseQuery(window.location.href);
// TODO: we should validate/clean this input to raise early errors.
return {
productId: productId,
accessToken: qs.access_token,
productId: qs.product,
};
},
selectData: function(state) {
return {
app: state.app,
user: state.user,
};
},
@ -53,9 +66,19 @@ var App = React.createClass({
if (result.app.error) {
console.log('rendering app error');
return <ErrorMessage error={result.app.error} />;
} else if (!result.user.signedIn) {
console.log('rendering login');
return (
<Login
accessToken={state.accessToken}
{...bindActionCreators(userActions, result.dispatch) }
/>
);
} else {
console.log('rendering app route handler');
return <RouteHandler productId={state.productId} />;
console.log('rendering purchase flow');
return (
<Purchase user={result.user} productId={state.productId} />
);
}
}}
</Connector>
@ -64,29 +87,16 @@ var App = React.createClass({
},
});
// declare our routes and their hierarchy
var routes = (
<Route handler={App}>
<Route name="login" path="/" handler={Login}/>
<Route name="card-form" path="/payment/card/" handler={CardDetails}/>
<Route
name="complete" path="/payment/complete/" handler={CompletePayment}/>
<Route
name="card-listing" path="/payment/card-list/" handler={CardListing}/>
</Route>
);
module.exports = {
component: App,
init: function() {
Router.run(routes, Router.HashLocation, function(Root) {
React.render((
<Provider redux={reduxConfig.default}>
{function() {
return <Root/>;
}}
</Provider>
), document.body);
});
React.render((
<Provider redux={reduxConfig.default}>
{function() {
return <App/>;
}}
</Provider>
), document.body);
},
};

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

@ -2,12 +2,13 @@
var $ = require('jquery');
var React = require('react');
var Navigation = require('react-router').Navigation;
var CardItem = require('components/card-item');
var SubmitButton = require('components/submit-button');
var gettext = require('utils').gettext;
var purchaseActions = require('purchase-actions');
var reduxConfig = require('redux-config');
module.exports = React.createClass({
@ -17,8 +18,6 @@ module.exports = React.createClass({
cards: React.PropTypes.array.isRequired,
},
mixins: [Navigation],
getInitialState: function() {
return {
isSubmitting: false,
@ -27,12 +26,7 @@ module.exports = React.createClass({
};
},
contextTypes: {
router: React.PropTypes.func,
},
handleSubmit: function(e) {
var { router } = this.context;
e.preventDefault();
this.setState({isSubmitting: true});
@ -48,7 +42,11 @@ module.exports = React.createClass({
context: this,
}).done(function() {
console.log('Successfully subscribed with existing card');
router.transitionTo('complete');
reduxConfig.default.dispatch(
purchaseActions.complete()
);
}).fail(function() {
// TODO: handler errors.
});

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

@ -2,7 +2,6 @@
var $ = require('jquery');
var CardValidator = require('card-validator');
var Navigation = require('react-router').Navigation;
var React = require('react');
var braintree = require('braintree-web');
@ -11,6 +10,8 @@ var gettext = utils.gettext;
var CardInput = require('components/card-input');
var SubmitButton = require('components/submit-button');
var purchaseActions = require('purchase-actions');
var reduxConfig = require('redux-config');
module.exports = React.createClass({
@ -26,8 +27,6 @@ module.exports = React.createClass({
'productId': React.PropTypes.string.isRequired,
},
mixins: [Navigation],
getInitialState: function() {
return {
isSubmitting: false,
@ -78,10 +77,6 @@ module.exports = React.createClass({
},
},
contextTypes: {
router: React.PropTypes.func,
},
handleChange: function(e) {
var fieldId = e.target.id;
var val = e.target.value;
@ -107,7 +102,6 @@ module.exports = React.createClass({
},
handleSubmit: function(e) {
var { router } = this.context;
e.preventDefault();
this.setState({isSubmitting: true});
var that = this;
@ -134,7 +128,11 @@ module.exports = React.createClass({
context: that,
}).done(function() {
console.log('Successfully subscribed + completed payment');
router.transitionTo('complete');
reduxConfig.default.dispatch(
purchaseActions.complete()
);
}).fail(function($xhr) {
this.processApiErrors($xhr.responseJSON);
});

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

@ -14,6 +14,10 @@ module.exports = React.createClass({
displayName: 'CardDetailsView',
propTypes: {
productId: React.PropTypes.string.isRequired,
},
getInitialState: function() {
return {
braintree_token: false,
@ -22,6 +26,7 @@ module.exports = React.createClass({
componentDidMount: function() {
console.log('Requesting braintree token');
// TODO: move this to a purchase action.
$.ajax({
method: 'post',
url: '/api/braintree/token/generate/',

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

@ -1,61 +1,37 @@
'use strict';
var React = require('react');
var Link = require('react-router').Link;
var Navigation = require('react-router').Navigation;
var Connector = require('redux/react').Connector;
var CardChoice = require('components/card-choice');
var ProductDetail = require('components/product-detail');
var Spinner = require('components/spinner');
var gettext = require('utils').gettext;
var purchaseActions = require('purchase-actions');
module.exports = React.createClass({
displayName: 'CardListingView',
displayName: 'CardListing',
mixins: [Navigation],
contextTypes: {
router: React.PropTypes.func,
},
selectData: function(state) {
console.log('card-listing: selectData() firing with', state);
return {
user: state.user,
}
propTypes: {
payWithNewCard: React.PropTypes.func.isRequired,
paymentMethods: React.PropTypes.array.isRequired,
productId: React.PropTypes.string.isRequired,
},
render: function() {
var component = this;
return (
<Connector select={this.selectData}>
{function(result) {
if (!result.user.email) {
return <Spinner text={gettext('Loading')}/>;
} else if (result.user.payment_methods.length) {
console.log('user:', result.user);
return (
<div className="card-listing">
<ProductDetail productId={component.props.productId} />
<CardChoice
cards={result.user.payment_methods}
productId={component.props.productId}
/>
<Link
className="card-add bottom-link"
to="card-form">{gettext('Add new credit card')}</Link>
</div>
);
} else {
console.log('No card data found, showing card entry');
component.transitionTo('card-form');
return <Spinner text={gettext('Loading')}/>;
}
}}
</Connector>
<div className="card-listing">
<ProductDetail productId={this.props.productId} />
<CardChoice
cards={this.props.paymentMethods}
productId={this.props.productId}
/>
<a className="card-add bottom-link"
onClick={this.props.payWithNewCard}>
{gettext('Add new credit card')}
</a>
</div>
);
},
});

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

@ -15,6 +15,7 @@ module.exports = React.createClass({
propTypes: {
productId: React.PropTypes.string.isRequired,
userEmail: React.PropTypes.string.isRequired,
},
handleClick: function(e) {
@ -34,32 +35,19 @@ module.exports = React.createClass({
}
},
selectData: function(state) {
console.log('complete-payment: selectData() firing with', state);
return {
user: state.user,
}
},
render: function() {
var component = this;
return (
<Connector select={this.selectData}>
{function(result) {
return (
<div className="complete">
<ProductDetail productId={component.props.productId} />
<p className="accepted">{gettext('Payment Accepted')}</p>
<p className="receipt">
{gettext('Your receipt has been sent to')}
<span className="email">{result.user.email}</span>
</p>
<SubmitButton text={gettext('OK')}
onClick={component.handleClick} />
</div>
);
}}
</Connector>
<div className="complete">
<ProductDetail productId={component.props.productId} />
<p className="accepted">{gettext('Payment Accepted')}</p>
<p className="receipt">
{gettext('Your receipt has been sent to')}
<span className="email">{this.props.userEmail}</span>
</p>
<SubmitButton text={gettext('OK')}
onClick={component.handleClick} />
</div>
);
},
});

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

@ -2,57 +2,28 @@
var $ = require('jquery');
var React = require('react');
var Navigation = require('react-router').Navigation;
var Connector = require('redux/react').Connector;
var userActions = require('user-actions');
var Spinner = require('components/spinner');
var gettext = require('utils').gettext;
var reduxConfig = require('redux-config');
module.exports = React.createClass({
displayName: 'LoginView',
mixins: [Navigation],
displayName: 'Login',
contextTypes: {
router: React.PropTypes.func,
propTypes: {
accessToken: React.PropTypes.string.isRequired,
signIn: React.PropTypes.func.isRequired,
},
componentDidMount: function() {
var { router } = this.context;
var defer = userActions.signIn(router.getCurrentQuery().access_token);
defer(reduxConfig.default.dispatch);
},
selectData: function(state) {
console.log('login: selectData() firing with', state);
return {
user: state.user,
}
},
watchUser: function(user) {
if (!user) {
console.log('user is not signed in');
return;
}
console.log('current user is signed in; continuing to card-listing');
this.transitionTo('card-listing');
this.props.signIn(this.props.accessToken);
},
render: function() {
var component = this;
return (
<Connector select={component.selectData}>
{function(result) {
console.log('login: rendering after state change', result);
component.watchUser(result.user);
return <Spinner text={gettext('Logging in')}/>;
}}
</Connector>
);
return <Spinner text={gettext('Signing in')}/>;
},
});

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

@ -0,0 +1,56 @@
'use strict';
var React = require('react');
var bindActionCreators = require('redux').bindActionCreators;
var Connector = require('redux/react').Connector;
var reduxConfig = require('redux-config');
var CardDetails = require('views/card-details');
var CardListing = require('views/card-listing');
var CompletePayment = require('views/complete-payment');
var purchaseActions = require('purchase-actions.js');
module.exports = React.createClass({
displayName: 'Purchase',
propTypes: {
user: React.PropTypes.object.isRequired,
productId: React.PropTypes.string.isRequired,
},
selectData: function(state) {
return {
purchase: state.purchase,
};
},
render () {
var props = this.props;
return (
<Connector select={this.selectData}>
{function(result) {
if (result.purchase.completed) {
return (
<CompletePayment productId={props.productId}
userEmail={props.user.email} />
);
} else if (result.purchase.payment_methods.length > 0) {
console.log('rendering card listing');
return (
<CardListing
productId={props.productId}
paymentMethods={result.purchase.payment_methods}
{...bindActionCreators(purchaseActions, result.dispatch)}
/>
);
} else {
console.log('rendering card entry');
return <CardDetails productId={props.productId} />;
}
}}
</Connector>
);
},
});

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

@ -1,8 +1,6 @@
'use strict';
var assign = require('object-assign');
var React = require('react');
var TestUtils = require('react/lib/ReactTestUtils');
module.exports = {
@ -44,50 +42,16 @@ module.exports = {
return TestUtils.findRenderedDOMComponentWithTag(component, tag);
},
getRouterStub: function(routerStubs) {
function RouterStub() {}
assign(RouterStub, {
makePath () {},
makeHref () {},
transitionTo (path) {
console.log('RouterStub: transitionTo:', path);
},
replaceWith () {},
goBack () {},
getCurrentPath () {},
getCurrentRoutes () {},
getCurrentPathname () {},
getCurrentParams () {
return {};
},
getCurrentQuery () {
return {};
},
isActive () {},
getRouteAtDepth() {},
setRouteComponentAtDepth() {},
}, routerStubs || {});
return RouterStub;
},
getFluxContainer: function(redux, routerStubs) {
getFluxContainer: function(redux) {
//
// Get a container component to set context stubs so you can use it
// to wrap a component for testing.
// You'd only need this to test a component that uses the router
// and/or uses the redux Connector component.
// You'd only need this to test a component that uses the
// redux Connector component.
//
// componentProps
// Optional object of properties to render the component with.
//
// routerStubs
// Option object of addtional stub methods for the stub router.
//
var RouterStub = this.getRouterStub(routerStubs);
var FluxContainer = React.createClass({
@ -96,12 +60,11 @@ module.exports = {
},
childContextTypes: {
router: React.PropTypes.func.isRequired,
redux: React.PropTypes.object.isRequired,
},
getChildContext: function() {
return {router: RouterStub, redux: redux};
return {redux: redux};
},
render () {
@ -110,8 +73,6 @@ module.exports = {
});
FluxContainer.router = RouterStub;
return FluxContainer;
},
@ -165,4 +126,13 @@ module.exports = {
};
},
stubComponent: function() {
return React.createClass({
displayName: 'StubComponent',
render: function() {
return <div></div>;
},
});
},
};

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

@ -1,9 +1,10 @@
'use strict';
var React;
var React = require('react');
var TestUtils;
var rewire = require('rewire');
var app = require('app');
var actionTypes = require('action-types');
var appActions = require('app-actions');
var reduxConfig = require('redux-config');
var ErrorMessage = require('components/error');
@ -12,20 +13,29 @@ var helpers = require('./helpers');
describe('App', function() {
var accessToken = 'some-oauth-token';
var productId = 'mozilla-concrete-brick';
var FakeLogin = helpers.stubComponent();
var FakePurchase = helpers.stubComponent();
var redux;
beforeEach(function() {
React = require('react');
TestUtils = require('react/lib/ReactTestUtils');
redux = reduxConfig.create();
});
function mountView() {
var FluxContainer = helpers.getFluxContainer(redux, {
getCurrentQuery: function() {
return {
product: 'mozilla-concrete-brick',
};
var FluxContainer = helpers.getFluxContainer(redux);
var app = rewire('app');
app.__set__({
'Login': FakeLogin,
'Purchase': FakePurchase,
'window': {
'location': {
'href': ('http://pay.dev/?access_token=' + accessToken +
'&product=' + productId),
},
},
});
@ -33,28 +43,50 @@ describe('App', function() {
var container = TestUtils.renderIntoDocument(
<FluxContainer>
{function() {
return (<App />);
return <App />;
}}
</FluxContainer>
);
var component = TestUtils.findRenderedComponentWithType(
return TestUtils.findRenderedComponentWithType(
container, App
);
return {
component: component,
transitionSpy: FluxContainer.transitionSpy,
};
}
it('should render an error', function() {
var view = mountView();
var View = mountView();
redux.dispatch(appActions.error('this is some error'));
var error = TestUtils.findRenderedComponentWithType(
view.component, ErrorMessage
View, ErrorMessage
);
// Maybe expand this test later if we pass in custom properties.
assert.ok(error);
});
it('should render a sign-in page', function() {
var View = mountView();
var login = TestUtils.findRenderedComponentWithType(
View, FakeLogin
);
assert.equal(login.props.accessToken, accessToken);
});
it('should render a purchase page', function() {
var user = {
email: 'f@f.com',
payment_methods: [],
signedIn: true,
};
var View = mountView();
redux.dispatch({
type: actionTypes.USER_SIGNED_IN,
user: user,
});
var purchase = TestUtils.findRenderedComponentWithType(
View, FakePurchase
);
assert.deepEqual(purchase.props.user, user);
});
});

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

@ -3,91 +3,41 @@
var React;
var TestUtils;
var actionTypes = require('action-types');
var reduxConfig = require('redux-config');
var CardChoice = require('components/card-choice');
var CardListing = require('views/card-listing');
var helpers = require('./helpers');
var TestUtils;
var savedVisa = {provider_id: '3vr3ym', type_name: 'Visa'};
describe('CardListingView', function() {
var redux;
var payWithNewCardSpy;
var View;
var savedVisa = {provider_id: '3vr3ym', type_name: 'Visa'};
beforeEach(function() {
React = require('react');
TestUtils = require('react/lib/ReactTestUtils');
redux = reduxConfig.create();
payWithNewCardSpy = sinon.spy();
View = TestUtils.renderIntoDocument(
<CardListing payWithNewCard={payWithNewCardSpy}
paymentMethods={[savedVisa]}
productId='mozilla-concrete-brick' />
);
});
function mountView() {
var FluxContainer = helpers.getFluxContainer(redux);
var transitionSpy = sinon.spy(FluxContainer.router, 'transitionTo');
var container = TestUtils.renderIntoDocument(
<FluxContainer>
{function() {
return (
<CardListing productId='mozilla-concrete-brick' />
);
}}
</FluxContainer>
);
var component = TestUtils.findRenderedComponentWithType(
container, CardListing
);
return {
component: component,
transitionSpy: transitionSpy,
};
}
function setUser(user) {
user.email = user.email || 'f@f.com';
redux.dispatch({
type: actionTypes.USER_SIGNED_IN,
user: user,
});
}
it('should redirect to card entry form when no saved cards', function() {
setUser({
payment_methods: [],
});
var view = mountView();
assert.equal(view.transitionSpy.firstCall.args[0], 'card-form');
});
it('should show the card form if user has saved cards', function() {
setUser({
payment_methods: [savedVisa],
});
var view = mountView();
it('should show card choice', function() {
var card = TestUtils.findRenderedComponentWithType(
view.component, CardChoice
View, CardChoice
);
assert.deepEqual(card.props.cards, [savedVisa]);
});
it('should show card form when clicking add link', function() {
// Need some payment methods to ensure the listing is shown.
setUser({
payment_methods: [savedVisa],
});
// Link.js checks which button is clicked, so we need to provide that
// in the event.
var event = {
button: 0,
};
var view = mountView();
it('should request to pay with new card when clicking link', function() {
var addLink = TestUtils.findRenderedDOMComponentWithTag(
view.component, 'a');
TestUtils.Simulate.click(addLink.getDOMNode(), event);
assert.equal(view.transitionSpy.firstCall.args[0], 'card-form');
View, 'a');
TestUtils.Simulate.click(addLink.getDOMNode());
assert.ok(payWithNewCardSpy.called);
});
});

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

@ -2,10 +2,7 @@
var React;
var TestUtils;
var Provider = require('redux/react').Provider;
var actionTypes = require('action-types');
var reduxConfig = require('redux-config');
var CompletePayment = require('views/complete-payment');
var helpers = require('./helpers');
@ -19,21 +16,11 @@ describe('CompletePayment', function() {
React = require('react');
TestUtils = require('react/lib/ReactTestUtils');
var redux = reduxConfig.create();
this.CompletePayment = TestUtils.renderIntoDocument(
<Provider redux={redux}>
{function() {
return <CompletePayment productId='mozilla-concrete-brick' />;
}}
</Provider>
<CompletePayment productId='mozilla-concrete-brick'
userEmail={this.email} />
);
redux.dispatch({
type: actionTypes.USER_SIGNED_IN,
user: {email: this.email},
});
sinon.stub(window.parent, 'postMessage');
});

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

@ -2,10 +2,11 @@
var actionTypes = require('action-types');
var appActions = require('app-actions');
var purchaseActions = require('purchase-actions');
var dataStore = require('data-store');
describe('appStore', function() {
describe('app', function() {
function appWithError() {
return {
@ -45,10 +46,11 @@ describe('appStore', function() {
});
describe('userStore', function() {
describe('user', function() {
function userData() {
return {
signedIn: true,
email: 'f@f.com',
payment_methods: [{provider_id: '1234'}],
};
@ -64,7 +66,9 @@ describe('userStore', function() {
it('should initialize an empty user', function() {
var user = dataStore.user(undefined, {});
assert.deepEqual(user, {});
assert.deepEqual(user, {
signedIn: false,
});
});
it('should return a user on sign-in', function() {
@ -89,3 +93,39 @@ describe('userStore', function() {
});
});
describe('purchase', function() {
it('should initialize a purchase', function() {
var purchase = dataStore.purchase(undefined, {});
assert.deepEqual(purchase, {
completed: false,
payment_methods: [],
});
});
it('should handle completed purchases', function() {
var purchase = dataStore.purchase(undefined, purchaseActions.complete());
assert.deepEqual(purchase, {
completed: true,
});
});
it('should infer saved payment methods when user signs in', function() {
var paymentMethods = [{type: 'Visa'}];
var purchase = dataStore.purchase(undefined, {
type: actionTypes.USER_SIGNED_IN,
user: {
payment_methods: paymentMethods,
},
});
assert.deepEqual(purchase, {
completed: false,
payment_methods: paymentMethods,
});
});
});

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

@ -2,68 +2,29 @@
var React;
var TestUtils;
var rewire = require('rewire');
var reduxConfig = require('redux-config');
var Login = require('views/login');
var helpers = require('./helpers');
describe('Login', function() {
var userActions;
var redux;
var accessToken = 'some-oauth-access-token';
var signInSpy = sinon.spy();
beforeEach(function() {
React = require('react');
TestUtils = require('react/lib/ReactTestUtils');
userActions = {
signIn: sinon.spy(function() {
return function() {};
}),
};
redux = reduxConfig.create();
});
function mountView() {
var FluxContainer = helpers.getFluxContainer(redux);
var transitionSpy = sinon.spy(FluxContainer.router, 'transitionTo');
var Login = rewire('views/login');
Login.__set__({
userActions: userActions,
});
var container = TestUtils.renderIntoDocument(
<FluxContainer>
{function() {
return (<Login />);
}}
</FluxContainer>
return TestUtils.renderIntoDocument(
<Login accessToken={accessToken} signIn={signInSpy} />
);
var component = TestUtils.findRenderedComponentWithType(
container, Login
);
return {
component: component,
transitionSpy: transitionSpy,
};
}
it('should sign in on mount', function() {
mountView();
assert.ok(userActions.signIn.called);
});
it('should navigate to card-listing when user signs in', function() {
var view = mountView();
redux.dispatch({
type: 'user-signed-in',
user: {email: 'f@f.com'},
});
assert.equal(view.transitionSpy.firstCall.args[0], 'card-listing');
assert.equal(signInSpy.firstCall.args[0], accessToken);
});
});

112
tests/test.purchase.jsx Normal file
Просмотреть файл

@ -0,0 +1,112 @@
'use strict';
var React;
var TestUtils;
var assign = require('object-assign');
var rewire = require('rewire');
var actionTypes = require('action-types');
var reduxConfig = require('redux-config');
var purchaseActions = require('purchase-actions');
var helpers = require('./helpers');
describe('Purchase', function() {
var defaultUser = {
email: 'f@f.com',
payment_methods: [],
};
var productId = 'mozilla-concrete-brick';
var FakeCompletePayment = helpers.stubComponent();
var FakeCardListing = helpers.stubComponent();
var FakeCardDetails = helpers.stubComponent();
var redux;
beforeEach(function() {
React = require('react');
TestUtils = require('react/lib/ReactTestUtils');
redux = reduxConfig.create();
});
function mountView(userOverrides) {
var user = assign({}, defaultUser, userOverrides);
var FluxContainer = helpers.getFluxContainer(redux);
var Purchase = rewire('views/purchase');
Purchase.__set__({
'CompletePayment': FakeCompletePayment,
'CardListing': FakeCardListing,
'CardDetails': FakeCardDetails,
});
var container = TestUtils.renderIntoDocument(
<FluxContainer>
{function() {
return <Purchase user={user} productId={productId} />;
}}
</FluxContainer>
);
return TestUtils.findRenderedComponentWithType(
container, Purchase
);
}
it('should render a card listing', function() {
var paymentMethods = [{type: 'Visa'}];
var View = mountView();
redux.dispatch({
type: actionTypes.USER_SIGNED_IN,
user: assign({}, defaultUser, {
payment_methods: paymentMethods,
}),
});
var child = TestUtils.findRenderedComponentWithType(
View, FakeCardListing
);
assert.equal(child.props.productId, productId);
assert.equal(child.props.paymentMethods, paymentMethods);
});
it('should render a card entry form', function() {
var View = mountView();
var child = TestUtils.findRenderedComponentWithType(
View, FakeCardDetails
);
assert.equal(child.props.productId, productId);
});
it('should render a payment completed page', function() {
var View = mountView();
redux.dispatch(purchaseActions.complete());
var child = TestUtils.findRenderedComponentWithType(
View, FakeCompletePayment
);
assert.equal(child.props.userEmail, defaultUser.email);
});
it('should render new card entry on explicit request', function() {
var paymentMethods = [{type: 'Visa'}];
var View = mountView();
// Dispatch a user that would normally trigger a card listing.
redux.dispatch({
type: actionTypes.USER_SIGNED_IN,
user: assign({}, defaultUser, {
payment_methods: paymentMethods,
}),
});
redux.dispatch(purchaseActions.payWithNewCard());
var child = TestUtils.findRenderedComponentWithType(
View, FakeCardDetails
);
assert.ok(child);
});
});